Merge pull request #657 from chillu/pull/tutorials

Documentation: Tutorials and Howto
This commit is contained in:
Ingo Schommer 2012-08-07 12:10:04 -07:00
commit 36dd729b08
46 changed files with 640 additions and 871 deletions

View File

@ -31,6 +31,7 @@ class TestRunner extends Controller {
'coverage' => 'coverageAll',
'sessionloadyml' => 'sessionloadyml',
'startsession' => 'startsession',
'selectsession' => 'selectsession',
'endsession' => 'endsession',
'cleanupdb' => 'cleanupdb',
'emptydb' => 'emptydb',
@ -48,6 +49,7 @@ class TestRunner extends Controller {
'coverageModule',
'coverageOnly',
'startsession',
'selectsession',
'endsession',
'cleanupdb',
'module',
@ -423,6 +425,54 @@ HTML;
return "<p>dev/tests/emptydb can only be used with a temporary database. Perhaps you should use dev/tests/startsession first?</p>";
}
}
function selectsession() {
if(!Director::isLive()) {
$tempDir = '/tmp';
$testSessionsDir = $tempDir . DIRECTORY_SEPARATOR . 'testsessions';
if (!is_dir($testSessionsDir)) {
return "<p>There are no test sessions available to select from.</p>";
}
if(!isset($_POST['testSessionKey'])) {
$me = Director::baseURL() . "dev/tests/selectsession";
return <<<HTML
<form action="$me" method="post">
<p>Enter a testSessionKey to select test session associated with that key. Don't forget to visit dev/tests/endsession when you're done!</p>
<input type="text" id="testSessionKey" name="testSessionKey" value="">
<p><input id="select-session" value="Select test session" type="submit" /></p>
</form>
HTML;
} else {
$testSessionKey = $_POST['testSessionKey'];
$testSessionFile = $testSessionsDir . DIRECTORY_SEPARATOR . $testSessionKey;
if (!is_file($testSessionFile) || !is_readable($testSessionFile)) {
return "<p>Invalid session key.</p>";
}
$testSessionDict = json_decode(file_get_contents($testSessionFile));
if (!isset($testSessionDict->databaseConfig, $testSessionDict->databaseConfig->database)) {
return "<p>Invalid database config.</p>";
}
DB::set_alternative_database_name($testSessionDict->databaseConfig->database);
return "<p>Selected test session $testSessionKey.</p>
<p>Time to start testing; where would you like to start?</p>
<ul>
<li><a id=\"home-link\" href=\"" .Director::baseURL() . "\">Homepage - published site</a></li>
<li><a id=\"draft-link\" href=\"" .Director::baseURL() . "?stage=Stage\">Homepage - draft site</a></li>
<li><a id=\"admin-link\" href=\"" .Director::baseURL() . "admin/\">CMS Admin</a></li>
<li><a id=\"endsession-link\" href=\"" .Director::baseURL() . "dev/tests/endsession\">End your test session</a></li>
</ul>";
}
} else {
return "<p>setdb can only be used on dev and test sites</p>";
}
}
function endsession() {
SapphireTest::kill_temp_db();

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,21 @@
# How to create a navigation menu
In this how-to, we'll create a simple menu which
you can use as the primary navigation for your website.
Add the following code to your main template,
most likely the "Page" template in your theme,
located in `themes/<mytheme>/templates/Page.ss`.
:::ss
<ul>
<% loop Menu(1) %>
<li>
<a href="$Link" title="Go to the $Title page" class="$LinkingMode">
<span>$MenuTitle</span>
</a>
</li>
<% end_loop %>
</ul>
More details on creating a menu are explained as part of ["Tutorial 1: Building a basic site"](/tutorials/1-building-a-basic-site), as well as ["Page type templates" topic](/topics/page-type-templates).

View File

@ -0,0 +1,114 @@
# How to make a simple contact form
In this how-to, we'll explain how to set up a specific page type
holding a contact form, which submits a message via email.
Let's start by defining a new `ContactPage` page type:
:::php
<?php
class ContactPage extends Page {
}
class ContactPage_Controller extends Page_Controller {
function Form() {
$fields = new FieldList(
new TextField('Name'),
new EmailField('Email'),
new TextareaField('Message')
);
$actions = new FieldList(
new FormAction('submit', 'Submit')
);
return new Form($this, 'Form', $fields, $actions);
}
}
To create a form, we instanciate a `Form` object on a function on our page controller. We'll call this function `Form()`. You're free to choose this name, but it's standard practice to name the function `Form()` if there's only a single form on the page.
There's quite a bit in this function, so we'll step through one piece at a time.
:::php
$fields = new FieldList(
new TextField('Name'),
new EmailField('Email'),
new TextareaField('Message')
);
First we create all the fields we want in the contact form, and put them inside a FieldList. You can find a list of form fields available on the `[api:FormField]` page.
:::php
$actions = FieldList(
new FormAction('submit', 'Submit')
);
We then create a `[api:FieldList]` of the form actions, or the buttons that submit the form. Here we add a single form action, with the name 'submit', and the label 'Submit'. We'll use the name of the form action later.
:::php
return new Form('Form', $this, $fields, $actions);
Finally we create the `Form` object and return it. The first argument is the name of the form this has to be the same as the name of the function that creates the form, so we've used 'Form'. The second argument is the controller that the form is on this is almost always $this. The third and fourth arguments are the fields and actions we created earlier.
To show the form on the page, we need to render it in our template. We do this by appending $ to the name of the form so for the form we just created we need to add $Form. Add $Form to the themes/currenttheme/Layout/Page.ss template, below $Content.
The reason it's standard practice to name the form function 'Form' is so that we don't have to create a separate template for each page with a form. By adding $Form to the generic Page.ss template, all pages with a form named 'Form' will have their forms shown.
If you now create a ContactPage in the CMS (making sure you have rebuilt the database and flushed the templates /dev/build?flush=all) and visit the page, you will now see a contact form.
![](../_images/howto_contactForm.jpg)
Now that we have a contact form, we need some way of collecting the data submitted. We do this by creating a function on the controller with the same name as the form action. In this case, we create the function 'submit' on the ContactPage_Controller class.
:::php
class ContactPage_Controller extends Page_Controller {
function Form() {
// ...
}
function submit($data, $form) {
$email = new Email();
$email->setTo('siteowner@mysite.com');
$email->setFrom($data['Email']);
$email->setSubject("Contact Message from {$data["Name"]}");
$messageBody = "
<p><strong>Name:</strong> {$data['Name']}</p>
<p><strong>Website:</strong> {$data['Website']}</p>
<p><strong>Message:</strong> {$data['Message']}</p>
";
$email->setBody($messageBody);
$email->send();
return array(
'Content' => '<p>Thank you for your feedback.</p>',
'Form' => ''
);
}
}
<div class="hint" markdown="1">
Caution: This form is prone to abuse by spammers,
since it doesn't enforce a rate limitation, or checks for bots.
We recommend to use a validation service like the ["recaptcha" module](http://www.silverstripe.org/recaptcha-module/)
for better security.
</div>
Any function that receives a form submission takes two arguments: the data passed to the form as an indexed array, and the form itself. In order to extract the data, you can either use functions on the form object to get the fields and query their values, or just use the raw data in the array. In the example above, we used the array, as it's the easiest way to get data without requiring the form fields to perform any special transformations.
This data is used to create an email, which you then send to the address you choose.
The final thing we do is return a 'thank you for your feedback' message to the user. To do this we override some of the methods called in the template by returning an array. We return the HTML content we want rendered instead of the usual CMS-entered content, and we return false for Form, as we don't want the form to render.
##How to add form validation
All forms have some basic validation built in email fields will only let the user enter email addresses, number fields will only accept numbers, and so on. Sometimes you need more complicated validation, so you can define your own validation by extending the Validator class.
The framework comes with a predefined validator called `[api:RequiredFields]`, which performs the common task of making sure particular fields are filled out. Below is the code to add validation to a contact form:
function Form() {
// ...
$validator = new RequiredFields('Name', 'Message');
return new Form($this, 'Form', $fields, $actions, $validator);
}
We've created a RequiredFields object, passing the name of the fields we want to be required. The validator we have created is then passed as the fifth argument of the form constructor. If we now try to submit the form without filling out the required fields, JavaScript validation will kick in, and the user will be presented with a message about the missing fields. If the user has JavaScript disabled, PHP validation will kick in when the form is submitted, and the user will be redirected back to the Form with messages about their missing fields.

View File

@ -111,15 +111,4 @@ The information documented in this page is reflected in a few places in the code
* augmentQuery() is responsible for altering the normal data selection queries to support versions.
* augmentDatabase() is responsible for specifying the altered database schema to support versions.
* `[api:MySQLDatabase]`: getNextID() is used when creating new objects; it also handles the mechanics of
updating the database to have the required schema.
## Future work
* We realise that a fixed mapping between the database and object-model isn't appropriate in all cases. In particular,
it could be beneficial to set up a SilverStripe data-object as an interface layer to the databases of other
applications. This kind of configuration support is on the cards for development once we start looking more seriously
at providing avenues for clean integration between systems.
* Some developers have commented that the the database layer could be used to maintain the relational integrity of this
database structure.
* It could be desirable to implement a non-repeating auto-numbering system.
updating the database to have the required schema.

View File

@ -97,8 +97,7 @@ You should ensure the URL for the home page is *home*, as that's the page Silver
## Templates
All pages on a SilverStripe site are rendered using a template. A template is an file
with a special `*.ss` file extension, containing HTML augmented with some
control codes. Because of this, you can have as much control of your sites HTML code as you like.
with a special `*.ss` file extension, containing HTML augmented with some control codes. Through the use of templates, you can have as much control over your sites HTML code as you like. In SilverStripe, these files and others for controlling your sites appearance the CSS, images, and some javascript are collectively described as a theme. Themes live in the 'themes' folder of your site.
Every page in your site has a **page type**. We will briefly talk about page types later, and go into much more detail
in tutorial two; right now all our pages will be of the page type *Page*. When rendering a page, SilverStripe will look

View File

@ -135,9 +135,9 @@ To add our new fields to the CMS we have to override the *getCMSFields()* method
public function getCMSFields() {
$fields = parent::getCMSFields();
$datefield = new DateField('Date');
$datefield->setConfig('showcalendar', true);
$fields->addFieldToTab('Root.Main', $datefield, 'Content');
$dateField = new DateField('Date');
$dateField->setConfig('showcalendar', true);
$fields->addFieldToTab('Root.Main', $dateField, 'Content');
$fields->addFieldToTab('Root.Main', new TextField('Author'), 'Content');
return $fields;
@ -202,18 +202,17 @@ the date field will have the date format defined by your locale.
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Main', $dateField = new DateField('Date','Article Date (for example: 20/12/2010)'), 'Content');
$dateField->setConfig('showcalendar', true);
$dateField->setConfig('dateformat', 'dd/MM/YYYY');
$fields->addFieldToTab('Root.Main', new TextField('Author','Author Name'), 'Content');
$dateField->setConfig('showcalendar', true);
$fields->addFieldToTab('Root.Main', $dateField, 'Content');
$fields->addFieldToTab('Root.Main', new TextField('Author'), 'Content');
return $fields;
}
Let's walk through these changes.
:::php
$fields->addFieldToTab('Root.Content', $dateField = new DateField('Date','Article Date (for example: 20/12/2010)'), 'Content');
$fields->addFieldToTab('Root.Main', $dateField = new DateField('Date','Article Date (for example: 20/12/2010)'), 'Content');
*$dateField* is declared only to in order to change the configuration of the DateField.
@ -228,7 +227,7 @@ By enabling *showCalendar* you show a calendar overlay when clicking on the fiel
*dateFormat* allows you to specify how you wish the date to be entered and displayed in the CMS field. See the `[api:DateField]` documentation for more configuration options.
:::php
$fields->addFieldToTab('Root.Content', new TextField('Author','Author Name'), 'Content');
$fields->addFieldToTab('Root.Main', new TextField('Author','Author Name'), 'Content');
By default the field name *'Date'* or *'Author'* is shown as the title, however this might not be that helpful so to change the title, add the new title as the second argument.
@ -237,7 +236,7 @@ By default the field name *'Date'* or *'Author'* is shown as the title, however
Because our new pages inherit their templates from *Page*, we can view anything entered in the content area when navigating to these pages on our site. However, as there is no reference to the date or author fields in the *Page* template this data is not being displayed.
To fix this we will create a template for each of our new page types. We'll put these in *themes/tutorial/templates/Layout* so we only have to define the page specific parts: SilverStripe will use *themes/tutorial/templates/Page.ss* for the basic
To fix this we will create a template for each of our new page types. We'll put these in *themes/simple/templates/Layout* so we only have to define the page specific parts: SilverStripe will use *themes/simple/templates/Page.ss* for the basic
page layout.
### ArticlePage Template
@ -309,7 +308,7 @@ We can make our templates more modular and easier to maintain by separating comm
We'll separate the display of linked articles as we want to reuse this code later on.
Cut the code in *ArticleHolder.ss** and replace it with an include statement:
Cut the code between "loop Children" in *ArticleHolder.ss** and replace it with an include statement:
**themes/simple/templates/Layout/ArticleHolder.ss**
@ -367,11 +366,11 @@ It would be nice to greet page visitors with a summary of the latest news when t
This function simply runs a database query that gets the latest news articles from the database. By default, this is five, but you can change it by passing a number to the function. See the [Data Model](../topics/datamodel) documentation for details. We can reference this function as a page control in our *HomePage* template:
**themes/tutorial/templates/Layout/Homepage.ss**
**themes/simple/templates/Layout/Homepage.ss**
:::ss
...
<div class="content">$Content</div>
<div class="content">$Content</div>
</article>
<% loop LatestNews %>
<% include ArticleTeaser %>
@ -515,7 +514,7 @@ resize the image every time the page is viewed.
The *StaffPage* template is also very straight forward.
**themes/tutorial/templates/Layout/StaffPage.ss**
**themes/simple/templates/Layout/StaffPage.ss**
:::ss
<div class="content-container">

View File

@ -2,30 +2,22 @@
## Overview
This tutorial is intended to be a continuation of the first two tutorials, and will build on the site produced in those
two tutorials.
This tutorial is intended to be a continuation of the first two tutorials ([first tutorial](1-building-a-basic-site), [second tutorial](2-extending-a-basic-site)). In this tutorial we will build on the site we developed in the earlier tutorials and explore forms in SilverStripe. We will look at custom coded forms: forms which need to be written in PHP.
This tutorial explores forms in SilverStripe. It will look at coded forms. Forms which need to be written in PHP.
Another method which allows you to construct forms via the CMS is by using the [userforms module](http://silverstripe.org/user-forms-module).
A UserDefinedForm is much quicker to implement, but lacks the flexibility of a coded form.
Instead of using a custom coded form, we could use the [userforms module](http://silverstripe.org/user-forms-module). This module allows users to construct forms via the CMS. A form created this way is much quicker to implement, but also lacks the flexibility of a coded form.
## What are we working towards?
We will create a poll on the home page that asks the user their favourite web browser, and displays a bar graph of the
results.
We will create a poll on the home page that asks the user their favourite web browser, and displays a bar graph of the results.
![tutorial:pollresults-small.png](_images/pollresults-small.jpg)
![tutorial:tutorial3_pollresults.png](_images/tutorial3_pollresults.jpg)
## Creating the form
We will be creating a form for a poll on the home page.
The poll we will be creating on our homepage will ask the user for their name and favourite web browser. It will then collate the results into a bar graph. We create the form in a method on *HomePage_Controller*.
The poll will ask the user's name and favourite web browser, and then collate the results into a bar graph. We create
the form in a method on *HomePage_Controller*.
*mysite/code/HomePage.php*
**mysite/code/HomePage.php**
:::php
class HomePage_Controller extends Page_Controller {
@ -63,27 +55,24 @@ Let's step through this code.
:::php
// Create fields
$fields = new FieldList(
new TextField('Name'),
new OptionsetField('Browser', 'Your Favourite Browser', array(
'Firefox' => 'Firefox',
'Chrome' => 'Chrome',
'Internet Explorer' => 'Internet Explorer',
'Safari' => 'Safari',
'Opera' => 'Opera',
'Lynx' => 'Lynx'
))
);
$fields = new FieldList(
new TextField('Name'),
new OptionsetField('Browser', 'Your Favourite Browser', array(
'Firefox' => 'Firefox',
'Chrome' => 'Chrome',
'Internet Explorer' => 'Internet Explorer',
'Safari' => 'Safari',
'Opera' => 'Opera',
'Lynx' => 'Lynx'
))
);
First we create our form fields.
We do this by creating a `[api:FieldList]` and passing our fields as arguments. The first field is a new
`[api:TextField]` with the name 'Name'.
First we create our form fields.
We do this by creating a `[api:FieldList]` and passing our fields as arguments.
The first field is a `[api:TextField]` with the name 'Name'.
There is a second argument when creating a field which specifies the text on the label of the field. If no second
argument is passed, as in this case, it is assumed the label is the same as the name of the field.
The second field we create is an `[api:OptionsetField]`. This is a dropdown, and takes a third argument - an
array mapping the values to the options listed in the dropdown.
@ -93,13 +82,9 @@ array mapping the values to the options listed in the dropdown.
);
After creating the fields, we create the form actions. Form actions appear as buttons at the bottom of the form.
The first argument is the name of the function to call when the button is pressed, and the second is the label of the
button.
After creating the fields, we create the form actions. Form actions appear as buttons at the bottom of the form.
The first argument is the name of the function to call when the button is pressed, and the second is the label of the button.
Here we create a 'Submit' button which calls the 'doBrowserPoll' method, which we will create later.
All the form actions (in this case only one) are collected into a `[api:FieldList]` object the same way we did with
the fields.
@ -108,16 +93,14 @@ the fields.
Finally we create the `[api:Form]` object and return it.
The first argument is the controller that contains the form, in most cases '$this'. The second is the name of the method
that returns the form, which is 'BrowserPollForm' in our case. The third and fourth arguments are the
FieldLists containing the fields and form actions respectively.
After creating the form function, we need to add the form to our home page template.
Add the following code to the top of your home page template, just before `<div class="Content">`:
Add the following code to the home page template, just before the Content `<div>`:
*themes/tutorial/templates/Layout/HomePage.ss*
**themes/simple/templates/Layout/HomePage.ss**
:::ss
...
@ -125,12 +108,14 @@ Add the following code to the home page template, just before the Content `<div>
<h2>Browser Poll</h2>
$BrowserPollForm
</div>
<div id="Content">
<div class="Content">
...
Add the following code to the form style sheet:
In order to make the graphs render correctly,
we need to add some CSS styling.
Add the following code to the existing `form.css` file:
*themes/tutorial/css/form.css*
**themes/simple/css/form.css**
:::css
/* BROWSER POLL */
@ -142,21 +127,27 @@ Add the following code to the form style sheet:
form FieldList {
border:0;
}
#BrowserPoll .message {
display: block;
color:red;
background:#ddd;
border:1px solid #ccc;
padding:5px;
margin:5px;
}
#BrowserPoll .message {
float:left;
display: block;
color:red;
background:#efefef;
border:1px solid #ccc;
padding:5px;
margin:5px;
}
#BrowserPoll h2 {
font-size: 1.5em;
line-height:2em;
color: #0083C8;
}
#BrowserPoll .field {
padding:3px 0;
}
#BrowserPoll input.text {
padding: 0;
font-size:1em;
}
#BrowserPoll .Actions {
padding:5px 0;
}
@ -165,25 +156,19 @@ Add the following code to the form style sheet:
}
This CSS code will ensure that the form is formatted and positioned correctly. All going according to plan, if you visit
[http://localhost/home?flush=1](http://localhost/home?flush=1) it should look something like below.
All going according to plan, if you visit [http://localhost/your_site_name/home?flush=all](http://localhost/your_site_name/home?flush=all) it should look something like this:
![](_images/pollform.jpg)
![](_images/tutorial3_pollform.jpg)
## Processing the form
Great! We now have a browser poll form, but it doesn't actually do anything. In order to make the form work, we have to
implement the 'doBrowserPoll' method that we told it about.
Great! We now have a browser poll form, but it doesn't actually do anything. In order to make the form work, we have to implement the 'doBrowserPoll()' method that we told it about.
First, we need some way of saving the poll submissions to the database, so we can retrieve the results later. We can do
this by creating a new object that extends from `[api:DataObject]`.
First, we need some way of saving the poll submissions to the database, so we can retrieve the results later. We can do this by creating a new object that extends from `[api:DataObject]`.
If you recall, in the [second tutorial](2-extending-a-basic-site) we said that all objects that inherit from DataObject and have their own fields are stored in tables the database. Also recall that all pages extend DataObject indirectly through `[api:SiteTree]`. Here instead of extending SiteTree (or `[api:Page]`) to create a page type, we will extend `[api:DataObject]` directly:
If you recall, in tutorial two we said that all objects that inherit from DataObject and that add fields are stored in
the database. Also recall that all pages extend DataObject indirectly through `[api:SiteTree]`. Here instead of
extending SiteTree (or `[api:Page]`) to create a page type, we extend DataObject directly.
*mysite/code/BrowserPollSubmission.php*
**mysite/code/BrowserPollSubmission.php**
:::php
<?php
@ -194,11 +179,9 @@ extending SiteTree (or `[api:Page]`) to create a page type, we extend DataObject
);
}
If we then rebuild the database ([http://localhost/your_site_name/dev/build?flush=all](http://localhost/your_site_name/dev/build?flush=all)), we will see that the *BrowserPollSubmission* table is created. Now we just need to define 'doBrowserPoll' on *HomePage_Controller*:
If we then rebuild the database ([http://localhost/dev/build?flush=1](http://localhost/dev/build?flush=1)), we will see
that the *BrowserPollSubmission* table is created. Now we just need to define 'doBrowserPoll' on *HomePage_Controller*.
*mysite/code/HomePage.php*
**mysite/code/HomePage.php**
:::php
class HomePage_Controller extends Page_Controller {
@ -212,28 +195,19 @@ that the *BrowserPollSubmission* table is created. Now we just need to define 'd
}
A function that processes a form submission takes two arguments - the first is the data in the form, the second is the
`[api:Form]` object.
In our function we create a new *BrowserPollSubmission* object. Since the name of our form fields and the name of the
database fields are the same we can save the form directly into the data object.
We call the 'write' method to write our data to the database, and 'redirectBack()' will redirect the user back
to the home page.
A function that processes a form submission takes two arguments - the first is the data in the form, the second is the `[api:Form]` object.
In our function we create a new *BrowserPollSubmission* object. Since the name of our form fields, and the name of the database fields, are the same we can save the form directly into the data object.
We call the 'write' method to write our data to the database, and '$this->redirectBack()' will redirect the user back to the home page.
## Form validation
SilverStripe forms all have automatic validation on fields where it is logical. For example, all email fields check that
they contain a valid email address. You can write your own validation by subclassing the *Validator* class.
SilverStripe forms all have automatic validation on fields where it is logical. For example, all email fields check that they contain a valid email address. You can write your own validation by subclassing the *Validator* class.
SilverStripe provides the *RequiredFields* validator, which ensures that the fields specified are filled in before the
form is submitted. To use it we create a new *RequiredFields* object with the name of the fields we wish to be required
as the arguments, then pass this as a fifth argument to the Form constructor.
SilverStripe provides the *RequiredFields* validator, which ensures that the fields specified are filled in before the form is submitted. To use it we create a new *RequiredFields* object with the name of the fields we wish to be required as the arguments, then pass this as a fifth argument to the Form constructor.
Change the end of the 'BrowserPollForm' function so it looks like this:
** mysite/code/HomePage.php **
**mysite/code/HomePage.php**
:::php
public function BrowserPollForm() {
@ -243,24 +217,19 @@ Change the end of the 'BrowserPollForm' function so it looks like this:
}
If we then open the homepage and attempt to submit the form without filling in the required fields an error will be
shown.
![](_images/validation.jpg)
If we then open the homepage and attempt to submit the form without filling in the required fields errors should appear.
![](_images/tutorial3_validation.jpg)
## Showing the poll results
Now that we have a working form, we need some way of showing the results.
The first thing to do is make it so a user can only vote once per session. If the user hasn't voted, show the form, otherwise show the results.
The first thing to do is make it so a user can only vote once per session. If the user hasn't voted, show the form,
otherwise show the results.
We can do this using a session variable. The `[api:Session]` class handles all session variables in SilverStripe. First modify the 'doBrowserPoll' to set the session variable 'BrowserPollVoted' when a user votes.
We can do this using a session variable. The `[api:Session]` class handles all session variables in SilverStripe.
First modify the 'doBrowserPoll' to set the session variable 'BrowserPollVoted' when a user votes.
*mysite/code/HomePage.php*
**mysite/code/HomePage.php**
:::php
// ...
@ -280,31 +249,28 @@ Then we simply need to check if the session variable has been set in 'BrowserPol
it is.
:::php
public function BrowserPollForm() {
if(Session::get('BrowserPollVoted')) {
return false;
}
// ...
class HomePage_Controller extends Page_Controller {
// ...
}
public function BrowserPollForm() {
if(Session::get('BrowserPollVoted')) return false;
// ...
}
}
If you visit the home page now you will see you can only vote once per session;
after that the form won't be shown.
You can start a new session by closing and reopening your browser.
If you visit the home page now you will see you can only vote once per session; after that the form won't be shown. You can start a new session by closing and reopening your browser,
or clearing your browsing session through your browsers preferences.
Now that we're collecting data, it would be nice to show the results
on the website as well. We could simply output every vote, but that's boring.
Let's group the results by browser, through the SilverStripe data model.
Although the form is not shown, you'll still see the 'Browser Poll' heading. We'll leave this for now: after we've built the bar graph of the results, we'll modify the template to show the graph instead of the form if the user has already voted.
In the [second tutorial](/tutorials/2-extending-a-basic-site),
we got a collection of news articles for the home page by
using the 'ArticleHolder::get()' function, which returns a `[api:DataList]`.
We can get all submissions in the same fashion, through `BrowserPollSubmission::get()`.
This list will be the starting point for our result aggregation.
Now that we're collecting data, it would be nice to show the results on the website as well. We could simply output every vote, but that's boring. Let's group the results by browser, through the SilverStripe data model.
In the [second tutorial](/tutorials/2-extending-a-basic-site), we got a collection of news articles for the home page by using the 'ArticleHolder::get()' function, which returns a `[api:DataList]`. We can get all submissions in the same fashion, through `BrowserPollSubmission::get()`. This list will be the starting point for our result aggregation.
Create the function 'BrowserPollResults' on the *HomePage_Controller* class.
** mysite/code/HomePage.php **
**mysite/code/HomePage.php**
:::php
public function BrowserPollResults() {
@ -327,17 +293,11 @@ This code introduces a few new concepts, so let's step through it.
:::php
$submissions = new GroupedList(BrowserPollSubmission::get());
First we get all of the `BrowserPollSubmission` records from the database.
This returns the submissions as a `[api:DataList]`.
Then we wrap it inside a `[api:GroupedList]`, which adds the ability
to group those records. The resulting object will behave just like
the original `DataList`, though (with the addition of a `groupBy()` method).
First we get all of the `BrowserPollSubmission` records from the database. This returns the submissions as a `[api:DataList]`.Then we wrap it inside a `[api:GroupedList]`, which adds the ability to group those records. The resulting object will behave just like the original `DataList`, though (with the addition of a `groupBy()` method).
:::php
$total = $submissions->Count();
We get the total number of submissions, which is needed to calculate the percentages.
:::php
@ -351,17 +311,14 @@ We get the total number of submissions, which is needed to calculate the percent
}
Now we create an empty `[api:ArrayList]` to hold the data we'll pass to the template.
Its similar to `[api:DataList]`, but can hold arbitrary objects rather than just `DataObject` instances.
Then iterate over the 'Browser' submissions field.
The `groupBy()` method splits our list by the 'Browser' field passed to it,
creating new lists with submissions just for a specific browser.
Each of those lists is keyed by the browser name.
The aggregated result is then contained in an `[api:ArrayData]` object,
which behaves much like a standard PHP array, but allows us to use it in SilverStripe templates.
Now we create an empty `[api:ArrayList]` to hold the data we'll pass to the template. Its similar to `[api:DataList]`, but can hold arbitrary objects rather than just DataObject` instances. Then we iterate over the 'Browser' submissions field.
The final step is to create the template to display our data. Change the 'BrowserPoll' div in
*themes/tutorial/templates/Layout/HomePage.ss* to the below.
The `groupBy()` method splits our list by the 'Browser' field passed to it, creating new lists with submissions just for a specific browser. Each of those lists is keyed by the browser name. The aggregated result is then contained in an `[api:ArrayData]` object, which behaves much like a standard PHP array, but allows us to use it in SilverStripe templates.
The final step is to create the template to display our data. Change the 'BrowserPoll' div to the below.
**themes/simple/templates/Layout/HomePage.ss**
:::ss
<div id="BrowserPoll">
@ -384,20 +341,12 @@ The final step is to create the template to display our data. Change the 'Browse
Here we first check if the *BrowserPollForm* is returned, and if it is display it. Otherwise the user has already voted,
and the poll results need to be displayed.
We use the normal tactic of putting the data into an unordered list and using CSS to style it, except here we use inline
styles to display a bar that is sized proportionate to the number of votes the browser has received. You should now have
a complete poll.
We use the normal tactic of putting the data into an unordered list and using CSS to style it, except here we use inline styles to display a bar that is sized proportionate to the number of votes the browser has received. You should now have a complete poll.
![](_images/pollresults.jpg)
<div class="hint" markdown="1">
While the ORM is
</div>
![](_images/tutorial3_pollresults.jpg)
## Summary
In this tutorial we have explored forms, and seen the different approaches to creating and using forms. Whether you
decide to use the [userforms module](http://silverstripe.org/user-forms-module) or create a form in PHP depends on the situation and flexibility
required.
In this tutorial we have explored custom php forms, and displayed result sets through Grouped Lists. We have briefly covered the different approaches to creating and using forms. Whether you decide to use the [userforms module](http://silverstripe.org/user-forms-module) or create a form in PHP depends on the situation and flexibility required.
[Next Tutorial >>](4-site-search)

View File

@ -2,17 +2,13 @@
## Overview
This is a short tutorial demonstrating how to add search functionality to a SilverStripe site. It is recommended that
you have completed the earlier tutorials, especially the tutorial on forms, before attempting this tutorial. While this
tutorial will add search functionality to the site built in the previous tutorials, it should be straight forward to
follow this tutorial on any site of your own.
This is a short tutorial demonstrating how to add search functionality to a SilverStripe site. It is recommended that you have completed the earlier tutorials ([Building a basic site](1-building-a-basic-site), [Extending a basic site](2-extending-a-basic-site), [Forms](3-forms)), especially the tutorial on forms, before attempting this tutorial. While this tutorial will add search functionality to the site built in the previous tutorials, it should be straight forward to follow this tutorial on any site of your own.
## What are we working towards?
We are going to add a search box on the top of the page. When a user types something in the box, they are taken to a
results page.
We are going to add a search box on the top of the page. When a user types something in the box, they are taken to a results page.
![](_images/tutorial4_search.png)
![](_images/tutorial4_search.jpg)
## Creating the search form
@ -22,20 +18,18 @@ This will enable fulltext search on page content as well as names of all files i
:::php
FulltextSearchable::enable();
After including that in your `_config.php` you will need to rebuild the database by visiting `http://yoursite.com/dev/build` in your web browser. This will add the fulltext search columns.
After including that in your `_config.php` you will need to rebuild the database by visiting [http://localhost/your_site_name/home?flush=all](http://localhost/your_site_name/home?flush=all) in your web browser (replace localhost/your_site_name with a domain if applicable). This will add fulltext search columns.
The actual search form code is already provided in FulltextSearchable so when you add the enable line above to your
`_config.php` you can add your form as `$SearchForm`.
The actual search form code is already provided in FulltextSearchable so when you add the enable line above to your `_config.php` you can add your form as `$SearchForm`.
In the simple theme, the SearchForm is already added to the header. We will go through the code and explain it.
## Adding the search form
To add the search form, we can add `$SearchForm` anywhere in our templates. In the simple theme, this is in
*themes/simple/templates/Includes/Header.ss*
To add the search form, we can add `$SearchForm` anywhere in our templates. In the simple theme, this is in *themes/simple/templates/Includes/Header.ss*
*themes/simple/templates/Includes/Header.ss*
**themes/simple/templates/Includes/Header.ss**
:::ss
...
@ -43,21 +37,20 @@ To add the search form, we can add `$SearchForm` anywhere in our templates. In t
<span class="search-dropdown-icon">L</span>
<div class="search-bar">
$SearchForm
<span class="search-bubble-arrow">}</span>
</div>
<% end_if %>
<% include Navigation %>
This results in:
This displays as:
![](_images/tutorial4_searchbox.png)
![](_images/tutorial4_searchbox.jpg)
## Showing the results
The results function is already included in the `ContentControllerSearchExtension` which
is applied via `FulltextSearchable::enable()`
*cms/code/search/ContentControllerSearchExtension.php*
**cms/code/search/ContentControllerSearchExtension.php**
:::php
class ContentControllerSearchExtension extends Extension {
@ -74,11 +67,9 @@ is applied via `FulltextSearchable::enable()`
}
The code populates an array with the data we wish to pass to the template - the search results, query and title of the
page. The final line is a little more complicated.
The code populates an array with the data we wish to pass to the template - the search results, query and title of the page. The final line is a little more complicated.
When we call a function by its url (eg http://localhost/home/results), SilverStripe will look for a template with the
name `PageType_function.ss`. As we are implementing the *results* function on the *Page* page type, we create our
When we call a function by its url (eg http://localhost/home/results), SilverStripe will look for a template with the name `PageType_function.ss`. As we are implementing the *results* function on the *Page* page type, we create our
results page template as *Page_results.ss*. Unfortunately this doesn't work when we are using page types that are
children of the *Page* page type. For example, if someone used the search on the homepage, it would be rendered with
*Homepage.ss* rather than *Page_results.ss*. SilverStripe always looks for the template from the most specific page type
@ -155,14 +146,13 @@ class.
<% end_if %>
</div>
Then finally add ?flush=1 to the URL and you should see the new template.
Then finally add ?flush=all to the URL and you should see the new template.
![](_images/tutorial4_search.png)
![](_images/tutorial4_search.jpg)
## Summary
This tutorial has demonstrated how easy it is to have full text searching on your site. To add search to a SilverStripe
site, only a search form and a results page need to be created.
This tutorial has demonstrated how easy it is to have full text searching on your site. To add search to a SilverStripe site add a search form, a results page, and you're done!
[Next Tutorial >>](5-dataobject-relationship-management)

View File

@ -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'
);
}
Lets 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, lets 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 &nbsp;
<% 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB