NEW Rewritten tutorial 5 to GridField API
- Removed redundant "module" relationship, has_one is already present in Student->Project relationship - Simplified FirstName/LastName to Name on Student and Mentor models, makes for less sample code - Removed detail views for Student and Mentor, as they don't add much educational value - Fixed indentation in code examples - Using GridField relation editor, listing students on relation instead of holder - Added optional exercises for the readers - Added conclusion, explaining when to use Page vs. DataObject - Updated sample data to GSOC '12 rather than '07, anonymized and randomised a bit - Thanks to Naomi Guyer for contributing!
@ -2,774 +2,432 @@
|
||||
|
||||
## Overview
|
||||
|
||||
In the [second tutorial](2-extending-a-basic-site) we have learned how to add extrafields to a page type thanks
|
||||
to the *$db* array and how to add an image using the *$has_one* array and so create a relationship between a table and
|
||||
the *Image* table by storing the id of the respective *Image* in the first table. This tutorial explores all this
|
||||
relations between [DataObjects](/topics/datamodel#relations) and the way to manage them easily.
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
I'm using the default tutorial theme in the following examples so the templates may vary or you may need to change
|
||||
the template code in this example to fit your theme
|
||||
</div>
|
||||
This tutorial explores the relationship and management of [DataObjects](/topics/datamodel#relations). It builds on the [second tutorial](2-extending-a-basic-site) where we learnt how to define
|
||||
additional fields on our models, and attach images to them.
|
||||
|
||||
## What are we working towards?
|
||||
|
||||
To simulate these relations between objects, we are going to simulate the management via the CMS of the **[Google Summer
|
||||
Of Code 2007](http://www.silverstripe.com/google-summer-of-code-2007-we-are-in/)** that SilverStripe was part of.
|
||||
To demonstrate relationships between objects,
|
||||
we are going to use the example of students working on various projects.
|
||||
Each student has a single project, and each project has one or more
|
||||
mentors supervising their progress. We'll create these objects,
|
||||
make them editable in the CMS, and render them on the website.
|
||||
|
||||
To do this, we are gonna use the following objects :
|
||||
This table shows some example data we'll be using:
|
||||
|
||||
* Project : Project on SilverStripe system for the GSOC
|
||||
* Student : Student involved in the project
|
||||
* Mentor : SilverStripe developer
|
||||
* Module : Module used for the project
|
||||
| Project | Student | Mentor |
|
||||
| ------- | ------- | ------- |
|
||||
| Developer Toolbar | Jakob,Ofir | Mark,Sean |
|
||||
| Behaviour Testing | Michal,Wojtek | Ingo, Sean |
|
||||
| Content Personalization | Yuki | Philipp |
|
||||
| Module Management | Andrew | Marcus,Sam |
|
||||
|
||||
### Has-One and Has-Many Relationships: Project and Student
|
||||
|
||||
This is a table which sums up the relations between them :
|
||||
| Project | Student | Mentor | Modules |
|
||||
| ------- | ------- | ------ | ------------------
|
||||
| i18n Multi-Language | Bernat Foj Capell | Ingo Schommer | Cms, Framework, i18n, Translation |
|
||||
| Image Manipulation | Mateusz Ujma | Sam Minnee | Cms, Framework, ImageManipulation |
|
||||
| Google Maps | Ofir Picazo Navarro | Hayden Smith | Cms, Framework, Maps |
|
||||
| Mashups | Lakshan Perera | Matt Peel | Cms, Framework, MashUps |
|
||||
| Multiple Databases | Philipp Krenn | Brian Calhoun | Cms, Framework, MultipleDatabases |
|
||||
| Reporting | Quin Hoxie | Sam Minnee | Cms, Framework, Reporting |
|
||||
| Security & OpenID | Markus Lanthaler | Hayden Smith | Cms, Framework, auth_openid |
|
||||
| SEO | Will Scott | Brian Calhoun | Cms, Framework, googleadwords, googleanalytics |
|
||||
| Usability | Elijah Lofgren | Sean Harvey | Cms, Framework, UsabilityElijah |
|
||||
| Safari 3 Support | Meg Risen | Sean Harvey | Cms, Framework, UsabilityMeg |
|
||||
A student can have only one project, it'll keep them busy enough.
|
||||
But each project can be done by one or more students.
|
||||
This is called a **one-to-many** relationship.
|
||||
Let's create the `Student` and `Project` objects.
|
||||
|
||||
## GSOC Projects
|
||||
|
||||
Before starting the relations management, we need to create a *ProjectsHolder* class where we will save the GSOC Project
|
||||
pages.
|
||||
|
||||
*tutorial/code/ProjectsHolder.php*
|
||||
**mysite/code/Student.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class ProjectsHolder extends Page {
|
||||
|
||||
static $allowed_children = array( 'Project' );
|
||||
|
||||
}
|
||||
|
||||
class ProjectsHolder_Controller extends Page_Controller {
|
||||
|
||||
}
|
||||
|
||||
## Project - Student relation
|
||||
|
||||
**A project can only be done by one student.**
|
||||
|
||||
**A student has only one project.**
|
||||
|
||||
This relation is called a **1-to-1** relation.
|
||||
|
||||
The first step is to create the student and project objects.
|
||||
|
||||
*tutorial/code/Student.php*
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Student extends DataObject {
|
||||
|
||||
static $db = array(
|
||||
'FirstName' => 'Text',
|
||||
'Lastname' => 'Text',
|
||||
'Nationality' => 'Text'
|
||||
'Name' => 'Varchar',
|
||||
'University' => 'Varchar',
|
||||
);
|
||||
|
||||
public function getCMSFields_forPopup() {
|
||||
$fields = new FieldList();
|
||||
|
||||
$fields->push( new TextField( 'FirstName', 'First Name' ) );
|
||||
$fields->push( new TextField( 'Lastname' ) );
|
||||
$fields->push( new TextField( 'Nationality' ) );
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
*tutorial/code/Project.php*
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Project extends Page {
|
||||
|
||||
static $has_one = array(
|
||||
'MyStudent' => 'Student'
|
||||
);
|
||||
|
||||
}
|
||||
class Project_Controller extends Page_Controller {}
|
||||
|
||||
This code will create a relationship between the *Project* table and the *Student* table by storing the id of the
|
||||
respective *Student* in the *Project* table.
|
||||
|
||||
The second step is to add the table in the method *getCMSFields* which will allow you to manage the *has_one* relation.
|
||||
|
||||
:::php
|
||||
class Project extends Page {
|
||||
|
||||
...
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$tablefield = new HasOneComplexTableField(
|
||||
$this,
|
||||
'MyStudent',
|
||||
'Student',
|
||||
array(
|
||||
'FirstName' => 'First Name',
|
||||
'Lastname' => 'Family Name',
|
||||
'Nationality' => 'Nationality'
|
||||
),
|
||||
'getCMSFields_forPopup'
|
||||
);
|
||||
$tablefield->setParentClass('Project');
|
||||
|
||||
$fields->addFieldToTab( 'Root.Student', $tablefield );
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
'Project' => 'Project'
|
||||
);
|
||||
}
|
||||
|
||||
Let’s walk through the parameters of the *HasOneComplexTableField* constructor.
|
||||
|
||||
1. **$this** : The first object concerned by the relation
|
||||
2. **'MyStudent'** : The name of the second object of the relation
|
||||
3. **'Student'** : The type of the second object of the relation
|
||||
4. **array(...)** : The fields of the second object which will be in the table
|
||||
5. **'getCMSFields_forPopup'** : The method which will be called to add, edit or only show a second object
|
||||
|
||||
You can also directly replace the last parameter by this code :
|
||||
|
||||
:::php
|
||||
new FieldList(
|
||||
new TextField( 'FirstName', 'First Name' ),
|
||||
new TextField( 'Lastname' ),
|
||||
new TextField( 'Nationality' )
|
||||
);
|
||||
|
||||
|
||||
<div class="tip" markdown='1'>
|
||||
Don't forget to rebuild the database using *dev/build?flush=1* before you
|
||||
proceed to the next part of this tutorial.
|
||||
</div>
|
||||
|
||||
Now that we have created our *Project* page type and *Student* data object, let’s add some content.
|
||||
|
||||
Go into the CMS and create one *Project* page for each project listed [above](#what-are-we-working-towards) under a
|
||||
*ProjectsHolder* page named **GSOC Projects** for instance.
|
||||
|
||||
![tutorial:gsoc-project-creation.png](_images/gsoc-project-creation.jpg)
|
||||
|
||||
As you can see in the tab panel *Student*, the adding functionality is titled *Add Student*. However, if you want to
|
||||
modify this title, you have to add this code in the *getCMSFields* method of the *Project* class :
|
||||
|
||||
:::php
|
||||
$tablefield->setAddTitle( 'A Student' );
|
||||
|
||||
|
||||
Select now one of the *Project* page that you have created, go in the tab panel *Student* and add all the students
|
||||
listed [above](#what-are-we-working-towards) by clicking on the link **Add A Student** of your
|
||||
*HasOneComplexTableField* table.
|
||||
|
||||
![tutorial:gsoc-student-creation.png](_images/gsoc-student-creation.jpg)
|
||||
|
||||
After having added all the students, you will see that, in the tab panel *Student* of all the *Project* pages, the
|
||||
*HasOneComplexTableField* tables have the same content.
|
||||
|
||||
For each *Project* page, you can now affect **one and only one** student to it ( see the
|
||||
[list](#What_are_we_working_towards?) ).
|
||||
|
||||
![tutorial:gsoc-project-student-selection.png](_images/gsoc-project-student-selection.jpg)
|
||||
|
||||
You will also notice, that you have the possibility to **unselect** a student which will make your *Project* page
|
||||
without any student affected to it.
|
||||
|
||||
**At the moment, the *HasOneComplexTableField* table doesn't manage totally the *1-to-1* relation because you can easily
|
||||
select the same student for two ( or more ) differents *Project* pages which corresponds to a *1-to-many* relation.**
|
||||
|
||||
To use your *HasOneComplexTableField* table for a **1-to-1** relation, make this modification in the class *Project* :
|
||||
|
||||
:::php
|
||||
class Project extends Page {
|
||||
|
||||
...
|
||||
|
||||
public function getCMSFields() {
|
||||
|
||||
...
|
||||
|
||||
$tablefield->setParentClass('Project');
|
||||
|
||||
$tablefield->setOneToOne();
|
||||
|
||||
$fields->addFieldToTab( 'Root.Student', $tablefield );
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Now, you will notice that by checking a student in a *Project* page, you will be unable to select him again in any other
|
||||
*Project* page which is the definition of a **1-to-1** relation.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Student - Mentor relation
|
||||
|
||||
**A student has one mentor.**
|
||||
|
||||
**A mentor has several students.**
|
||||
|
||||
This relation is called a **1-to-many** relation.
|
||||
|
||||
The first step is to create the mentor object and set the relation with the *Student* data object.
|
||||
|
||||
*tutorial/code/Mentor.php*
|
||||
**mysite/code/Project.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Mentor extends Page {
|
||||
|
||||
static $db = array(
|
||||
'FirstName' => 'Text',
|
||||
'Lastname' => 'Text',
|
||||
'Nationality' => 'Text'
|
||||
);
|
||||
|
||||
class Project extends Page {
|
||||
static $has_many = array(
|
||||
'Students' => 'Student'
|
||||
);
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$fields->addFieldToTab( 'Root.Content', new TextField( 'FirstName' ) );
|
||||
$fields->addFieldToTab( 'Root.Content', new TextField( 'Lastname' ) );
|
||||
$fields->addFieldToTab( 'Root.Content', new TextField( 'Nationality' ) );
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
||||
class Mentor_Controller extends Page_Controller {}
|
||||
|
||||
*tutorial/code/Student.php*
|
||||
|
||||
:::php
|
||||
class Student extends DataObject {
|
||||
|
||||
...
|
||||
|
||||
static $has_one = array(
|
||||
'MyMentor' => 'Mentor'
|
||||
);
|
||||
|
||||
class Project_Controller extends Page_Controller {
|
||||
}
|
||||
|
||||
The relationships are defined through the `$has_one`
|
||||
and `$has_many` properties on the objects.
|
||||
The array keys declares the name of the relationship,
|
||||
the array values contain the class name (see the ["database structure"](/reference/database-structure)
|
||||
and ["datamodel"](/topics/datamodel) topics for more information).
|
||||
|
||||
This code will create a relationship between the *Student* table and the *Mentor* table by storing the id of the
|
||||
respective *Mentor* in the *Student* table.
|
||||
As you can see, only the `Project` model extends `Page`,
|
||||
while `Student` is a plain `DataObject` subclass.
|
||||
This allows us to view projects through the standard
|
||||
theme setup, just like any other page.
|
||||
It would be possible to render students separately as well,
|
||||
but for now we'll assume they're just listed as part of their `Project` page.
|
||||
Since `Project` inherits all properties (e.g. a title) from its parent class,
|
||||
we don't need to define any additional ones for our purposes.
|
||||
|
||||
The second step is to add the table in the method *getCMSFields* which will allow you to manage the *has_many* relation.
|
||||
Now that we have our models defined in PHP code,
|
||||
we need to tell the database to create the related tables.
|
||||
Trigger a rebuild through *dev/build?flush=all* before you
|
||||
proceed to the next part of this tutorial.
|
||||
|
||||
*tutorial/code/Mentor.php*
|
||||
### Organizing pages: ProjectHolder
|
||||
|
||||
:::php
|
||||
class Mentor extends Page {
|
||||
|
||||
...
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
...
|
||||
|
||||
$tablefield = new HasManyComplexTableField(
|
||||
$this,
|
||||
'Students',
|
||||
'Student',
|
||||
array(
|
||||
'FirstName' => 'FirstName',
|
||||
'Lastname' => 'Family Name',
|
||||
'Nationality' => 'Nationality'
|
||||
),
|
||||
'getCMSFields_forPopup'
|
||||
);
|
||||
$tablefield->setAddTitle( 'A Student' );
|
||||
|
||||
$fields->addFieldToTab( 'Root.Students', $tablefield );
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
||||
class Mentor_Controller extends Page_Controller {}
|
||||
A `Project` is just a page, so we could create it anywhere in the CMS.
|
||||
In order to list and organize them, it makes sense to collect them under a common parent page.
|
||||
We'll create a new page type called `ProjectsHolder` for this purpose,
|
||||
which is a common pattern in SilverStripe's page types. Holders
|
||||
are useful for listing their children, and usually restrict these children to a specific class,
|
||||
in our case pages of type `Project`.
|
||||
The restriction is enforced through the `$allowed_children` directive.
|
||||
|
||||
To know more about the parameters of the *HasManyComplexTableField* constructor, [check](#project_-_student_relation)
|
||||
those of the *HasOneComplexTableField* constructor.
|
||||
|
||||
<div class="tip" markdown='1'>
|
||||
Don't forget to rebuild the database using *dev/build?flush=1* before you
|
||||
proceed to the next part of this tutorial.
|
||||
</div>
|
||||
|
||||
Now that we have created our *Mentor* page type, go into the CMS and create one *Mentor* page for each mentor listed
|
||||
[above](#what-are-we-working-towards) under a simple *Page* named
|
||||
**Mentors** for instance.
|
||||
|
||||
![tutorial:gsoc-mentor-creation.png](_images/gsoc-mentor-creation.jpg)
|
||||
|
||||
For each *Mentor* page, you can now affect **many** students created previously ( see the
|
||||
[list](#What_are_we_working_towards?) ) by going in the tab panel *Students*.
|
||||
|
||||
![tutorial:gsoc-mentor-student-selection.png](_images/gsoc-mentor-student-selection.jpg)
|
||||
|
||||
You will also notice, that by checking a student in a *Mentor* page, you will be unable to select him again in any other
|
||||
*Mentor* page which is the definition of a **1-to-many** relation.
|
||||
|
||||
As the *HasOneComplexTableField* table, you also have the possibility not to select any student which will make your
|
||||
*Mentor* page without any student affected to it.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Project - Module relation
|
||||
|
||||
**A project uses several modules.**
|
||||
|
||||
**A module is used by several projects.**
|
||||
|
||||
This relation is called a **many-to-many** relation.
|
||||
|
||||
The first step is to create the module object and set the relation with the *Project* page type.
|
||||
|
||||
*tutorial/code/Module.php*
|
||||
**mysite/code/ProjectsHolder.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Module extends DataObject {
|
||||
|
||||
static $db = array(
|
||||
'Name' => 'Text'
|
||||
);
|
||||
|
||||
static $belongs_many_many = array(
|
||||
'Projects' => 'Project'
|
||||
);
|
||||
|
||||
public function getCMSFields_forPopup() {
|
||||
$fields = new FieldList();
|
||||
$fields->push( new TextField( 'Name' ) );
|
||||
return $fields;
|
||||
}
|
||||
|
||||
class ProjectsHolder extends Page {
|
||||
static $allowed_children = array(
|
||||
'Project'
|
||||
);
|
||||
}
|
||||
class ProjectsHolder_Controller extends Page_Controller {
|
||||
}
|
||||
|
||||
*tutorial/code/Project.php*
|
||||
You might have noticed that we don't specify the relationship
|
||||
to a project. That's because its already inherited from the parent implementation,
|
||||
as part of the normal page hierarchy in the CMS.
|
||||
|
||||
Now that we have created our `ProjectsHolder` and `Project` page types, we'll add some content.
|
||||
Go into the CMS and create a `ProjectsHolder` page named **Projects**.
|
||||
Then create one `Project` page for each project listed [above](#what-are-we-working-towards).
|
||||
|
||||
### Data Management Interface: GridField
|
||||
|
||||
So we have our models, and can create pages of type
|
||||
`Project` through the standard CMS interface,
|
||||
and collect those within a `ProjectsHolder`.
|
||||
But what about creating `Student` records?
|
||||
|
||||
Since students are related to a single project, we will
|
||||
allow editing them right the on the CMS interface in the `Project` page type.
|
||||
We do this through a powerful field called `[GridField](/topics/grid-field)`.
|
||||
All customization to fields for a page type are managed through a method called
|
||||
`getCMSFields()`, so let's add it there:
|
||||
|
||||
**mysite/code/Project.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class Project extends Page {
|
||||
|
||||
...
|
||||
|
||||
static $many_many = array(
|
||||
'Modules' => 'Module'
|
||||
);
|
||||
|
||||
// ...
|
||||
public function getCMSFields() {
|
||||
// Get the fields from the parent implementation
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
// Create a default configuration for the new GridField, allowing record editing
|
||||
$config = GridFieldConfig_RelationEditor::create();
|
||||
|
||||
// Set the names and data for our gridfield columns
|
||||
$config->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
|
||||
'Name' => 'Name',
|
||||
'Project.Title'=> 'Project' // Retrieve from a has-one relationship
|
||||
));
|
||||
|
||||
// Create a gridfield to hold the student relationship
|
||||
$studentsField = new GridField(
|
||||
'Students', // Field name
|
||||
'Student', // Field title
|
||||
$this->Students(), // List of all related students
|
||||
$config
|
||||
);
|
||||
|
||||
// Create a tab named "Students" and add our field to it
|
||||
$fields->addFieldToTab('Root.Students', $studentsField);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
This creates a tabular field, which lists related student records, one row at a time.
|
||||
Its empty by default, but you can add new students as required,
|
||||
or relate them to the project by typing in the box above the table.
|
||||
|
||||
This code will create a relationship between the *Project* table and the *Module* table by storing the ids of the
|
||||
respective *Project* and *Module* in a another table named **Project_Modules**.
|
||||
In our case, want to manage those records, edit their details, and add new ones.
|
||||
To accomplish this, we have added a specific `[api:GridFieldConfig]`.
|
||||
While we could've built the config from scratch, there's several
|
||||
preconfigured instances. The `GridFieldConfig_RecordEditor` default configures
|
||||
the field to edit records, rather than just viewing them.
|
||||
The GridField API is composed of "components", which makes it very flexible.
|
||||
One example of this is the configuration of column names on our table:
|
||||
We call `setDisplayFields()` directly on the component responsible for their rendering.
|
||||
|
||||
The second step is to add the table in the method *getCMSFields* which will allow you to manage the *many_many*
|
||||
relation.
|
||||
|
||||
:::php
|
||||
class Project extends Page {
|
||||
|
||||
...
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
...
|
||||
|
||||
$modulesTablefield = new ManyManyComplexTableField(
|
||||
$this,
|
||||
'Modules',
|
||||
'Module',
|
||||
array(
|
||||
'Name' => 'Name'
|
||||
),
|
||||
'getCMSFields_forPopup'
|
||||
);
|
||||
$modulesTablefield->setAddTitle( 'A Module' );
|
||||
|
||||
$fields->addFieldToTab( 'Root.Modules', $modulesTablefield );
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
To know more about the parameters of the *ManyManyComplexTableField* constructor,
|
||||
[check](#project_-_student_relation) those of the *HasOneComplexTableField*
|
||||
constructor.
|
||||
|
||||
<div class="tip" markdown='1'>
|
||||
Don't forget to rebuild the database using *dev/build?flush=1* before you
|
||||
proceed to the next part of this tutorial.
|
||||
<div class="note" markdown="1">
|
||||
Adding a `GridField` to a page type is a popular way to manage data,
|
||||
but not the only one. If your data requires a dedicated interface
|
||||
with more sophisticated search and management logic, consider
|
||||
using the `[ModelAdmin](reference/modeladmin)` interface instead.
|
||||
</div>
|
||||
|
||||
Select now one of the *Project* page, go in the tab panel *Modules* and add all the modules listed
|
||||
[above](#what-are-we-working-towards) by clicking on the link **Add A
|
||||
Module** of your *ManyManyComplexTableField* table.
|
||||
![tutorial:tutorial5_project_creation.jpg](_images/tutorial5_project_creation.jpg)
|
||||
|
||||
![tutorial:gsoc-module-creation.png](_images/gsoc-module-creation.jpg)
|
||||
Select each `Project` page you have created before,
|
||||
go in the tab panel called "Students", and add all students as required,
|
||||
by clicking on the link **Add Student** of your *GridField* table.
|
||||
|
||||
For each *Project* page, you can now affect **many** modules created previously ( see the
|
||||
[list](#What_are_we_working_towards?) ) by going in the tab panel
|
||||
*Modules*.
|
||||
![tutorial:tutorial5_addNew.jpg](_images/tutorial5_addNew.jpg)
|
||||
|
||||
![tutorial:gsoc-project-module-selection.png](_images/gsoc-project-module-selection.jpg)
|
||||
Once you have added all the students, and selected their projects, it should look a little like this:
|
||||
|
||||
You will also notice, that you are able to select several times a *Module* on different *Project* pages which is the
|
||||
definition of a **many-to-many** relation.
|
||||
![tutorial:tutorial5_students.jpg](_images/tutorial5_students.jpg)
|
||||
|
||||
As the *HasOneComplexTableField* and *HasManyComplexTableField* table, you also have the possibility not to select any
|
||||
module which will make your *Project* page without any module affected to it.
|
||||
### Many-many relationships: Mentor
|
||||
|
||||
Now we have a fairly good picture of how students relate to their projects.
|
||||
But students generally have somebody looking them over the shoulder.
|
||||
In our case, that's the "mentor". Each project can have many of them,
|
||||
and each mentor can be have one or more projects. They're busy guys!
|
||||
This is called a *many-many* relationship.
|
||||
|
||||
The first step is to create the `Mentor` object and set the relation with the `Project` page type.
|
||||
|
||||
**mysite/code/Mentor.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class Mentor extends DataObject {
|
||||
static $db = array(
|
||||
'Name' => 'Varchar',
|
||||
);
|
||||
static $belongs_many_many = array(
|
||||
'Projects' => 'Project'
|
||||
);
|
||||
}
|
||||
|
||||
**mysite/code/Project.php**
|
||||
|
||||
:::php
|
||||
class Project extends Page {
|
||||
// ...
|
||||
static $many_many = array(
|
||||
'Mentors' => 'Mentor'
|
||||
);
|
||||
}
|
||||
|
||||
This code will create a relationship between the `Project` table and the `Mentor` table by storing the ids of the respective `Project` and `Mentor` in a another table named "Project_Mentors"
|
||||
(after you've performed a `dev/build` command, of course).
|
||||
|
||||
The second step is to add the table in the method `getCMSFields()`,
|
||||
which will allow you to manage the *many_many* relation.
|
||||
Again, GridField will come in handy here, we just have
|
||||
to configure it a bit differently.
|
||||
|
||||
**mysite/code/Project.php**
|
||||
|
||||
:::php
|
||||
class Project extends Page {
|
||||
// ...
|
||||
public function getCMSFields() {
|
||||
// ...
|
||||
|
||||
// Same setup, but for mentors
|
||||
$mentorsField = new GridField(
|
||||
'Mentors',
|
||||
'Mentors',
|
||||
$this->Mentors(),
|
||||
GridFieldConfig_RelationEditor::create()
|
||||
);
|
||||
$fields->addFieldToTab('Root.Mentors', $mentorsField);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
The important difference to our student management UI is the usage
|
||||
of `$this->Mentor()` (rather than `Mentor::get()`). It will limit
|
||||
the list of records to those related through the many-many relationship.
|
||||
|
||||
In the CMS, open one of your `Project` pages and select the "Mentors" tab.
|
||||
Add all the mentors listed [above](#what-are-we-working-towards)
|
||||
by clicking on the **Add Mentor** button.
|
||||
|
||||
![tutorial:tutorial5_module_creation.jpg](_images/tutorial5_module_creation.jpg)
|
||||
|
||||
To associate the mentor with a project, select one the the mentors, and click on the projects tab. Add all the projects a mentor is associated with (see the [list](#What_are_we_working_towards?)), by typing the name in "Find Projects by Page name" and clicking the "Link Existing" button.
|
||||
You will notice that you are able to select the same `Project` for multiple mentors.
|
||||
This is the definition of a **many-to-many** relation.
|
||||
|
||||
![tutorial:tutorial5_module_selection.jpg](_images/tutorial5_module_selection.jpg)
|
||||
|
||||
|
||||
## Website Display
|
||||
|
||||
|
||||
|
||||
## Displaying the data on your website
|
||||
|
||||
Now that we have created all the *Page* and *DataObject* classes necessary and the relational tables to
|
||||
manage the [relations](../topics/datamodel#relations) between them, we would like to see these relations on the website.
|
||||
|
||||
We will see in this section how to display all these relations but also how to create a template for a *DataObject*.
|
||||
Now that we have created all the *Page* and *DataObject* classes necessary and the relational tables to manage the [relations](/topics/datamodel#relations) between them, we would like to see these relations on the website. We will see in this section how to display all these relations,
|
||||
but also how to create a template for a *DataObject*.
|
||||
|
||||
For every kind of *Page* or *DataObject*, you can access to their relations thanks to the **control** loop.
|
||||
|
||||
**1. GSOC Projects**
|
||||
### Projects Overview Template
|
||||
|
||||
Let's start with the *ProjectsHolder* page created before. For this template, we are will display the same table than
|
||||
[above](#what-are-we-working-towards).
|
||||
We'll start by creating a `ProjectsHolder` template,
|
||||
which lists all projects, and condenses their
|
||||
student and mentor relationships into a single line.
|
||||
You'll notice that there's no difference between
|
||||
accessing a "has-many" and "many-many" relationship
|
||||
in the template loops: To the template, its just
|
||||
a named list of object.
|
||||
|
||||
![tutorial:gsoc-projects-table.png](_images/gsoc-projects-table.jpg)
|
||||
![tutorial:tutorial5_projects_table.jpg](_images/tutorial5_projects_table.jpg)
|
||||
|
||||
*tutorial/templates/Layout/ProjectsHolder.ss*
|
||||
**themes/simple/templates/Layout/ProjectsHolder.ss**
|
||||
|
||||
:::ss
|
||||
<% include Menu2 %>
|
||||
|
||||
<div id="Content" class="typography">
|
||||
<% if Level(2) %>
|
||||
<% include BreadCrumbs %>
|
||||
<% end_if %>
|
||||
|
||||
$Content
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project</th>
|
||||
<th>Student</th>
|
||||
<th>Mentor</th>
|
||||
<th>Modules</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% loop Children %>
|
||||
<tr>
|
||||
<td>$Title</td>
|
||||
<td>
|
||||
<% if MyStudent %>
|
||||
<% loop MyStudent %>
|
||||
$FirstName $Lastname
|
||||
<% end_loop %>
|
||||
<% else %>
|
||||
No Student
|
||||
<% end_if %>
|
||||
</td>
|
||||
<td>
|
||||
<% if MyStudent %>
|
||||
<% loop MyStudent %>
|
||||
<% if MyMentor %>
|
||||
<% loop MyMentor %>
|
||||
$FirstName $Lastname
|
||||
<% end_loop %>
|
||||
<% else %>
|
||||
No Mentor
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
<% else %>
|
||||
No Mentor
|
||||
<% end_if %>
|
||||
</td>
|
||||
<td>
|
||||
<% if Modules %>
|
||||
<% loop Modules %>
|
||||
$Name
|
||||
<% end_loop %>
|
||||
<% else %>
|
||||
No Modules
|
||||
<% end_if %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end_loop %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
$Form
|
||||
|
||||
<div class="content-container typography">
|
||||
<article>
|
||||
<h1>$Title</h1>
|
||||
<div class="content">
|
||||
$Content
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project</th>
|
||||
<th>Students</th>
|
||||
<th>Mentors</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% loop Children %>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="$Link">$Title</a>
|
||||
</td>
|
||||
<td>
|
||||
<% loop Students %>
|
||||
$Name ($University)<% if Last !=1 %>,<% end_if %>
|
||||
<% end_loop %>
|
||||
</td>
|
||||
<td>
|
||||
<% loop Mentor %>
|
||||
$Name<% if Last !=1 %>,<% end_if %>
|
||||
<% end_loop %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end_loop %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<% include SideBar %>
|
||||
|
||||
Navigate to the holder page through your website navigation,
|
||||
or the "Preview" feature in the CMS. You should see a list of all projects now.
|
||||
Add `?flush=all` to the page URL to force a refresh of the template cache.
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
If you are using the blackcandy template: You might want to move the `<% include Sidebar %>`
|
||||
(tutorial/templates/Includes/SideBar.ss) include in the *tutorial/templates/Layout/Page.ss* template above
|
||||
the typography div to get rid of the bullets
|
||||
</div>
|
||||
To get a list of all projects, we've looped through the "Children" list,
|
||||
which is a relationship we didn't define explictly.
|
||||
It is provided to us by the parent implementation,
|
||||
since projects are nothing other than children pages in the standard page hierarchy.
|
||||
|
||||
### Project Detail Template
|
||||
|
||||
**2. Project**
|
||||
Creating the detail view for each `Project` page works in a very similar way.
|
||||
Given that we're in the context of a single project,
|
||||
we can access the "Students" and "Mentors" relationships directly in the template.
|
||||
|
||||
We know now how to easily access and show [relations](../topics/datamodel#relations) between *DataObject* in a template.
|
||||
![tutorial:tutorial5_project.jpg](_images/tutorial5_project.jpg)
|
||||
|
||||
We can now do the same for every *Project* page by creating its own template.
|
||||
|
||||
![tutorial:gsoc-project.png](_images/gsoc-project.jpg)
|
||||
|
||||
*tutorial/templates/Layout/Project.ss*
|
||||
**themes/simple/templates/Layout/Project.ss**
|
||||
|
||||
:::ss
|
||||
<% include Menu2 %>
|
||||
<div class="content-container typography">
|
||||
<article>
|
||||
<h1>$Title</h1>
|
||||
<div class="content">
|
||||
$Content
|
||||
|
||||
<div id="Content" class="typography">
|
||||
<% if Level(2) %>
|
||||
<% include BreadCrumbs %>
|
||||
<% end_if %>
|
||||
<h2>Students</h2>
|
||||
<% if Students %>
|
||||
<ul>
|
||||
<% loop Students %>
|
||||
<li>$Name ($University)</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p>No students found</p>
|
||||
<% end_if %>
|
||||
|
||||
$Content
|
||||
|
||||
<% if MyStudent %>
|
||||
<% loop MyStudent %>
|
||||
<p>First Name: <strong>$FirstName</strong></p>
|
||||
<p>Lastname: <strong>$Lastname</strong></p>
|
||||
<p>Nationality: <strong>$Nationality</strong></p>
|
||||
|
||||
<h3>Mentor</h3>
|
||||
|
||||
<% if MyMentor %>
|
||||
<% loop MyMentor %>
|
||||
<p>First Name: <strong>$FirstName</strong></p>
|
||||
<p>Lastname: <strong>$Lastname</strong></p>
|
||||
<p>Nationality: <strong>$Nationality</strong></p>
|
||||
<% end_loop %>
|
||||
<% else %>
|
||||
<p>This student doesn't have any mentor.</p>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
<% else %>
|
||||
<p>There is no any student working on this project.</p>
|
||||
<% end_if %>
|
||||
|
||||
<h3>Modules</h3>
|
||||
|
||||
<% if Modules %>
|
||||
<ul>
|
||||
<% loop Modules %>
|
||||
<li>$Name</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p>This project has not used any modules.</p>
|
||||
<% end_if %>
|
||||
|
||||
$Form
|
||||
<h2>Mentors</h2>
|
||||
<% if Mentors %>
|
||||
<ul>
|
||||
<% loop Mentors %>
|
||||
<li>$Name</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p>No mentors found</p>
|
||||
<% end_if %>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<% include SideBar %>
|
||||
|
||||
Follow the link to a project detail from from your holder page,
|
||||
or navigate to it through the submenu provided by the theme.
|
||||
|
||||
What we would like now is to create a special template for the *DataObject* *Student* and the *Page* *Mentor* which will
|
||||
be used when we will call directly the variable in the *Project* template. In our case, we will use the same template
|
||||
because these two classes have the same fields ( FirstName, Surname and Nationality ).
|
||||
### Student Detail Template
|
||||
|
||||
*tutorial/templates/Includes/GSOCPerson.ss*
|
||||
You might have noticed that we duplicate the same template code
|
||||
between both views when it comes to displaying the details
|
||||
on students and mentors. We'll fix this for students,
|
||||
by introducing a new template for them.
|
||||
|
||||
**themes/simple/templates/Includes/StudentInfo.ss**
|
||||
|
||||
:::ss
|
||||
<p>First Name: <strong>$FirstName</strong></p>
|
||||
<p>Lastname: <strong>$Lastname</strong></p>
|
||||
<p>Nationality: <strong>$Nationality</strong></p>
|
||||
$Name ($University)
|
||||
|
||||
|
||||
Now the template is created, we need to establish the link between the *Student* and *Mentor* classes with their common
|
||||
template.
|
||||
|
||||
To do so, add this code in the two classes. This will create a control on each of those objects which can be called
|
||||
from templates either within a control block or dot notation.
|
||||
|
||||
*tutorial/code/Student.php, tutorial/code/Mentor.php*
|
||||
|
||||
:::php
|
||||
public function PersonalInfo() {
|
||||
$template = 'GSOCPerson';
|
||||
return $this->renderWith( $template );
|
||||
}
|
||||
|
||||
|
||||
We can now modify the *Project.ss* template.
|
||||
|
||||
:::ss
|
||||
|
||||
...
|
||||
|
||||
<% if MyStudent %>
|
||||
$MyStudent.PersonalInfo
|
||||
|
||||
<h3>Mentor</h3>
|
||||
|
||||
<% loop MyStudent %>
|
||||
<% if MyMentor %>
|
||||
$MyMentor.PersonalInfo
|
||||
<% else %>
|
||||
<p>This student doesn't have any mentor.</p>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
<% else %>
|
||||
<p>There is no any student working on this project.</p>
|
||||
<% end_if %>
|
||||
|
||||
...
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
Remember to add `?flush=1` to the url when refreshing the project page or otherwise you will get a template error
|
||||
</div>
|
||||
|
||||
In the *Project* template, it has been really easy to display the **1-to-1** relation with a *Student* object just by
|
||||
calling the variable **$MyStudent**. This has been made possible thanks to the code below present in the *Project*
|
||||
class.
|
||||
|
||||
:::php
|
||||
static $has_one = array(
|
||||
'MyStudent' => 'Student'
|
||||
);
|
||||
|
||||
|
||||
However, in the *Student* class, there is no any code relating to the **1-to-1** relation with a *Project* *Page*. So
|
||||
how to access it from a *Student* *DataObject* ?
|
||||
|
||||
**3. Mentor**
|
||||
|
||||
In this template, we are gonna try to access the *Project* details from a *Student* *DataObject*.
|
||||
|
||||
What we want to do is to access to the *Project* page in the same way than we have done for the other relations
|
||||
**without modifying the relations between *Page* and *DataObject* and the database structure**.
|
||||
|
||||
![tutorial:gsoc-mentor.png](_images/gsoc-mentor.jpg)
|
||||
|
||||
To do so, we have to create a function in the *Student* class which will return the *Project* linked with it. Let's call
|
||||
it *MyProject* for instance.
|
||||
To use this template, we need to add a new method to our student class:
|
||||
|
||||
:::php
|
||||
class Student extends DataObject {
|
||||
|
||||
...
|
||||
|
||||
public function MyProject() {
|
||||
return Project::get()->filter("MyStudentID", $this->ID);
|
||||
function getInfo() {
|
||||
return $this->renderWith('StudentInfo');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
We can now use this value in the same way that we have used the other relations.
|
||||
That's how we can use this function in the *Mentor* template.
|
||||
|
||||
*tutorial/templates/Layout/Mentor.ss*
|
||||
|
||||
:::ss
|
||||
<% include Menu2 %>
|
||||
|
||||
<div id="Content" class="typography">
|
||||
<% include BreadCrumbs %>
|
||||
$Content
|
||||
|
||||
<h3>Personal Details</h3>
|
||||
|
||||
<p>First Name: <strong>$FirstName</strong></p>
|
||||
<p>Lastname: <strong>$Lastname</strong></p>
|
||||
<p>Nationality: <strong>$Nationality</strong></p>
|
||||
|
||||
<h3>Students</h3>
|
||||
|
||||
<% if Students %>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Student</th>
|
||||
<th>Project</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% loop Students %>
|
||||
<tr>
|
||||
<td>$FirstName $Lastname</td>
|
||||
<td>
|
||||
<% if MyProject %>
|
||||
<% loop MyProject %>
|
||||
$Title
|
||||
<% end_loop %>
|
||||
<% else %>
|
||||
No Project
|
||||
<% end_if %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end_loop %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>There is no any student working with this mentor.</p>
|
||||
<% end_if %>
|
||||
|
||||
$Form
|
||||
</div>
|
||||
|
||||
Replace the student template code in both `Project.ss`
|
||||
and `ProjectHolder.ss` templates with the new placeholder, `$Info`.
|
||||
That's the code enclosed in `<% loop Students %>` and `<% end_loop %>`.
|
||||
With this pattern, you can increase code reuse across templates.
|
||||
|
||||
## Summary
|
||||
|
||||
This tutorial has demonstrated how easy it is to manage all the type of relations between *DataObject* objects in the
|
||||
CMS and how to display them on the website.
|
||||
This tutorial has demonstrated how you can manage data with
|
||||
different types of relations between in the CMS,
|
||||
and how you can display this data on your website.
|
||||
We illustrated how the powerful `Page` class can be useful to structure
|
||||
your own content, and how we can correlate it to more
|
||||
lightweight `DataObject` classes. The transition between
|
||||
the two classes is intentionally fluent in the CMS, you can
|
||||
manage them depending on your needs.
|
||||
`DataObject` gives you a no-frills solution to data storage,
|
||||
but `Page` allows for built-in WYSIWIG editing, versioning,
|
||||
publication and hierarchical organization.
|
||||
|
||||
## Exercises
|
||||
|
||||
## Download the code
|
||||
This is a simplified example, so there's naturally room for improvement.
|
||||
In order to challenge your knowledge gained in the tutorials so far,
|
||||
we suggest some excercises to make the solution more flexible:
|
||||
|
||||
Download all the [code](http://doc.silverstripe.org/framework/docs/en/tutorials/_images/tutorial5-completecode.zip) for this tutorial.
|
||||
|
||||
You can also download the [code](http://doc.silverstripe.org/framework/docs/en/tutorials/_images/tutorial5-completecode-blackcandy.zip) for use in the blackcandy template.
|
||||
* Refactor the `Student` and `Mentor` classes to inherit from a common parent class `Person`,
|
||||
and avoid any duplication between the two subclasses.
|
||||
* Render mentor details in their own template
|
||||
* Change the `GridField` to list only five records per page (the default is 20).
|
||||
This configuration is stored in the `[api:GridFieldPaginator]` component
|
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 42 KiB |
BIN
docs/en/tutorials/_images/tutorial5_addNew.jpg
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
docs/en/tutorials/_images/tutorial5_mentor.jpg
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
docs/en/tutorials/_images/tutorial5_mentor_creation.jpg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
docs/en/tutorials/_images/tutorial5_mentor_students.jpg
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
docs/en/tutorials/_images/tutorial5_module_creation.jpg
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
docs/en/tutorials/_images/tutorial5_module_selection.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
docs/en/tutorials/_images/tutorial5_project.jpg
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
docs/en/tutorials/_images/tutorial5_project_creation.jpg
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
docs/en/tutorials/_images/tutorial5_projects_table.jpg
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
docs/en/tutorials/_images/tutorial5_student_tab.jpg
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
docs/en/tutorials/_images/tutorial5_students.jpg
Normal file
After Width: | Height: | Size: 86 KiB |