Skip to main content

This site requires you to update your browser. Your browsing experience maybe affected by not having the most up to date version.

UncleCheese
12th February 2015

In this lesson, we're going to dig a bit deeper into relational data by introducing some plural relationships with $has_many.

Working with data relationships - $has_many

Level: Beginner

Duration: 13:32

In this lesson:

So far, we've only been dealing with content that is directly associated with a page, but often times, pages are made up of content that is stored outside the page, and merely brought into the page through a data relationship. We saw a bit of this in Lesson #7 when we created $has_one relationships to File objects. All of the information about the file is stored on its own table, and the page merely references it. We're going to dig a bit deeper into relational data in this lesson by introducing some plural relationships with $has_many.

What we'll cover

  • Creating a generic DataObject
  • Setting up a $has_many relationship
  • Introduction to the GridField
  • Working with relational data on the template

Creating a generic DataObject

Attached to this lesson is a download link to a new static template we'll be working with, regions-page.html. Our designer has once again stripped away all the chrome and left us with just the $Layout section, so before we go any further, let's download that file and migrate the page into a SilverStripe template.

Copy the contents of regions-page.html into a new file, themes/one-ring/templates/Layout/RegionsPage.ss.

Then, create a new page type to go with it.

mysite/code/RegionsPage.php

class RegionsPage extends Page {

}

class RegionsPage_Controller extends Page_Controller {

}

Run a dev/build?flush, and go into the CMS. Change the Regions page to type RegionsPage (on the Settings tab). Now go back to your website and click on "Regions." You should see the new layout.

All of this is static content, of course, and we need to start carving it up into something editable in the CMS. One of the most apparent features of this content is that it clearly belongs in a loop. We see the regions Northeast, Southeast, etc., all with the same structure and data. Each contains a title, a photo, and a short description.

We could hand this off to the rich text editor and create all this content in the $Content block, but that would be pretty ugly. Our content editor would have to take care to create very specific markup, and that would push the boundaries of WYSIWYG utility. We need to make each of these regions editable pieces of structured data.

Let's first create the Region data type. Just like we discussed in the previous lesson, generic, non-page content types should subclass DataObject.

mysite/code/Region.php

class Region extends DataObject {

    private static $db = array (
        'Title' => 'Varchar',
        'Description' => 'Text',
    );

    private static $has_one = array (
        'Photo' => 'Image'
    );

    public function getCMSFields() {
        $fields = FieldList::create(
            TextField::create('Title'),
            TextareaField::create('Description'),
            $uploader = UploadField::create('Photo')
        );

        $uploader->setFolderName('region-photos');
        $uploader->getValidator()->setAllowedExtensions(array('png','gif','jpeg','jpg'));

        return $fields;
    }
}

You might have noticed that our getCMSFields() function looks a bit different. That's because we're not going to be using the typical page editing interface for this object, so we're not going to have the tabs that come with Page objects. We could very easily create one, but since this data type is so simple, we'll just leave it as a simple field list, and add all the form fields to the constructor.

Setting up a $has_many relationship

Now that we have our standalone Region object, we need to relate it to our RegionsPage. We know from our design that the page can contain any number of Regions, so for this, we'll use the $has_many relationship.

mysite/code/RegionsPage.php

class RegionsPage extends Page {

    private static $has_many = array (
        'Regions' => 'Region'
    );
}

This follows the same convention as the $has_one we used in a previous lesson. The key, "Regions" is the arbitrary name we'll give to the relationship. It's the method we'll use to get a list of all the related regions. The value, "Region" is the name of the related class.

Run a dev/build and see if you get any database changes. If you don't see any, that is the expected result! Why? Well, we're not quite done yet.

Reciprocating the $has_many

$has_many relationships are a bit of a special case because they have to be reciprocated by the related class. While every RegionsPage has many Regions, it is also true that every Region has one RegionPage that contains it. Regions cannot belong to any more than one region page.

This is important, because the database mutation happens at the $has_one level, not the $has_many. What will really bind these two objects together is a Region object providing one, and only one, RegionsPageID. Let's make that update now.

mysite/code/Region.php

class Region extends DataObject {

        //...
    private static $has_one = array (
        'Photo' => 'Image',
        'RegionsPage' => 'RegionsPage'
    );

        //...

Typically, reciprocal has_one's like this can just be named after the parent class.

Now let's run a dev/build and see that we get a new RegionsPageID field.

Introduction to GridField

We've got our $has_many relationship defined, with a $has_one on the other side, and we're ready to start populating the relationship with data. For this, we'll need a need one of the workhorses of the CMS interface -- GridField.

GridField is a highly configurable form field that allows you to manage an arbitrary table of data. In its most primitive sense, you can think of it as an abstraction of a database table, but there's much more that you can do with it. To manage our Region objects, we'll want a tab on the RegionsPage that allows us to create, read, edit, and delete associated Region records.

Let's make the following update to our RegionsPage object.

class RegionsPage extends Page {

    private static $has_many = array (
        'Regions' => 'Region'
    );

    public function getCMSFields() {
        $fields = parent::getCMSFields();
        $fields->addFieldToTab('Root.Regions', GridField::create(
            'Regions',
            'Regions on this page',
            $this->Regions(),
            GridFieldConfig_RecordEditor::create()
        ));

        return $fields;
    }
}

The GridField constructor

Let's take a look at the argument signature for GridField:

  • 'Regions': A required, arbitrary name for the GridField. You'll need this if you ever want to make updates to your GridField after it's been added to the FieldList.
  • 'Regions on this page': A title for the GridField. Should be user-friendly.
  • $this->Regions(): This is the most substantial component of your GridField. It populates the grid with data. In this case, we're using the magic method created by our $has_many relationship to fill the grid with all the records that are currently associated with the page.
  • GridFieldConfig_RecordEditor::create(): This is a bit more complex. It creates a object that contains a number of GridFieldComponent objects, which provide various UI tools to the grid, such as pagination, an "add new" button, delete/edit buttons, etc. These GridFieldConfig objects can be configured with any variety of components you like, but SilverStripe ships with a few common configurations that are often used. GridFieldConfig_RecordEditor is a great one, because it provides all the basic UI you'd expect to have for managing data.

Let's go back into the CMS and edit our "Regions" page. See that we have a tab now that contains a grid. Give it a try and add some test records.

Configuring the GridField

As stated earlier, GridField is highly configurable. One of the most common customisations you'll want to make to your grid is the columns that are displayed. In this case, there are only a handful of fields, so only showing Title and Description isn't so bad, but you can imagine that if the DataObject had 10 distinct fields, we would probably want to tighten that up a bit.

In our case, let's make a minor enhancement, and just add the Photo field to our list view. By default, only fields in the $db array get included. Since Photo is in the $has_one, we have to specifically include it.

Let's define a $summary_fields variable on our Region object.

class Region extends DataObject {
    //...    
    private static $summary_fields = array (
        'Photo' => '',
        'Title' => 'Title of region',
        'Description' => 'Short description'
    );
    //...
}

This array maps the name of the field to the human-readable column name. We'll let the photo speak for itself and leave its column header empty.

Because we changed a private static variable, we need to run ?flush to see the update to the grid. How does it look? Pretty bad, right? The photo is not being resized to fit the grid.

Using a custom getter

Fortunately, $summary_fields accepts more than just field names. We can provide any public method on our DataObject as a column value. Let's create a method that returns a resized photo.

class Region extends DataObject {
        //...
    private static $summary_fields = array (
        'GridThumbnail' => '',
        'Title' => 'Title',
        'Description' => 'Description'
    );

    public function getGridThumbnail() {
        if($this->Photo()->exists()) {
            return $this->Photo()->SetWidth(100);
        }

        return "(no image)";
    }
        // ...
}

Run ?flush again, and see that our grid looks much cleaner now.

We could have saved ourselves a lot of time by using a method that SilverStripe provides to all Image objects by default -- CMSThumbnail(). Photo.CMSThumbnail would have achieved a very similar result, without the fallback text (no image).

Traversing fields

Another great feature of $summary_fields is that you can traverse relationships to get foreign fields, using a dot-separated syntax. Suppose we wanted to show the Filename field on the Photo:

class Region extends DataObject {
        //...
    private static $summary_fields = array (
        'Photo.Filename' => 'Photo file name',
        'Title' => 'Title',
        'Description' => 'Description'
    );
        //...
}

This type of syntax becomes especially useful when formatting dates, or getting the title of a related $has_one rather than just showing its numeric ID.

Working with relational data on the template

Now that our relational data is all in place, it's time to display it on the template. This should be pretty straight forward. Let's start with RegionsPage.ss

themes/one-ring/templates/Layout/RegionsPage.ss, line 9

<div class="grid-style1 clearfix">
    <% loop $Regions %>
    <div class="item col-md-12"><!-- Set width to 4 columns for grid view mode only -->
        <div class="image image-large">
            <a href="#">
                <span class="btn btn-default"><i class="fa fa-file-o"></i> Read More</span>
            </a>
            $Photo.CroppedImage(720,255)
        </div>
        <div class="info-blog">
            <h3>
                <a href="#">$Title</a>
            </h3>
            <p>$Description</p>
        </div>
    </div>
    <% end_loop %>
</div>

Notice that we're skipping over the links. We'll address that in a future lesson.

Questions and Feedback

Don't have RegionsPage

by Rahul at 08:01pm, 9 April 2015

Author

What does this mean?

by UncleCheese at 11:02pm, 9 April 2015

I think he's trying to say that yet again the necassary files are missing? I'm noticing this in a lot of the demos. Wonder if its worth someone taking a few minutes to update all the code so the download links are working correctly etc? Just thinking aloud...

I am enjoying the series so far... so thanks!

by Anonymous at 05:30am, 24 May 2015

Hi, is there a way to change the add button text, when creating a gridField?

Thx in advanced

by Erick at 08:06am, 17 April 2015

Author

Hi, Erick,

Absolutely. This gets into modifying the GridFieldConfig objects that are used to customise the features of the GridField.

$myGrid
    ->getConfig()
    ->getComponentByType('GridFieldAddNewButton')
    ->setButtonName('Whatever');

Keep in mind that the button is automatically prepended with the word "Add" (localised).

by UncleCheese at 10:46am, 20 April 2015

HI There,

I have tried this now for hours and for some reason i just keep getting a blank page as soon as i click on my page (called Speakers). Im not using Regions but instead Speakers that must follow the same principle.

This is my code on Speakers.php <?php

class Speakers extends DataObject{ private static $db = array( 'Title' => 'Varchar', 'Description' => 'Text', );

private static $has_one = array(
  'Photo' => 'Image',
    'SpeakersPage' => 'SpeakersPage'
);

public function getCMSFields(){
    $fields = FieldList::create(
    TextField::create('Title'),
    TextareaField::create('Description'),
    $uploader = UploadField::create('Photo')
);
    $uploader->setFolderName('Speakers-image');
    $uploader->getValidator()->setAllowedExtensions(array(
        'png', 'gif', 'jpeg', 'jpg'
    ));
    return $fields;
}

}

And this is on my SpeakersPage.php <?php

class SpeakersPage extends Page{ private static $has_many = array( 'Speakers' => 'Speaker', );

public function getCMSFields(){
    $fields = Parent::getCMSFields();
    $fields ->addFieldsToTab('Root.Speakers', GridField::create(
       'Speakers',
        'Speakers on this page',
        $this ->Speakers(),
        GridFieldConfig_RecordEditor::create()
));
    return $fields;
}

}

class SpeakersPage_controller extends Page_Controller{ }

Please help?

by Tiaan Theunissen at 07:55pm, 4 June 2015

Author

Hi, Tiaan,

If you're just getting a white screen, that's a generic 500 error. You've probably got a syntax error somewhere. You'll want to make sure you have error reporting turned up in your PHP configuration, and that display_errors is on. Otherwise, you can check your error logs, and you should see something in there.

by UncleCheese at 05:12pm, 8 June 2015

Why is it necessary to set $has_many? When declaring $has_one to the Regions class, each Region is linked using a foreign ID (PageID) and since multiple instances (Regions) can be linked to a single page the "has many" relationship is established . But $has_many seems redundant at this point.. What does it do?

by Faloude at 11:27pm, 2 July 2016

Great tut, thank you!

What would the path/syntax, to filter a Dataobject in the next situation: DataObject_A has_many DataObject_B, DataObject_B has_one DataObject_C. Now I have the id of B and C, and I would like to get a datalist of DataObject_A based on ID-s. How can I achieve that?

by XorPush at 06:03am, 27 July 2016

Hi there, I have two questions...

1: My DataObject is "workshops". It includes has_many "lessons". Can I restrict the number of "lessons" a content author can add? (I don't want more than 6 lessons per workshop and there should be no less than 2)

2: A "lesson" will typically be one page, however sometimes will contain a second page. Do I use this the same way with has_many pages and has_one lesson? If so, how do I prevent the page being visible without a lesson being added? Is there something more I have to do to set this extended relationship up Or perhaps there is a better/clearer way to do this?

by Arbee at 06:27am, 16 October 2016

Stuck on something? Have something to share? Don't be shy!

Keep learning!

Introduction to the ORM

In this lesson, we’ll use the backbone of SilverStripe Framework -- the Object Relational Model (ORM) to syndicate content to the home page of our...

Working with Files and Images

In the previous lesson, we started adding some basic custom fields to our Article pages. We’ll now continue working on getting those pages more integrated with...

Adding custom fields to a page

In the previous lesson, we developed a structure for our Travel Guides section that provides a list view of articles, each with a link to their...