Merge pull request #6446 from robbieaverill/feature/controllers-without-underscores

API Allow controller discovery without underscores (PSR-2 compliance)
This commit is contained in:
Daniel Hensby 2017-01-11 15:27:56 +00:00 committed by GitHub
commit 747c0770e7
24 changed files with 1077 additions and 867 deletions

View File

@ -1087,6 +1087,7 @@ mappings:
ConfigTest_TestNest: SilverStripe\Core\Tests\Config\ConfigTest\TestNest
ConfigTest: SilverStripe\Core\Tests\Config\ConfigTest
ConfigTest_Config_MemCache: SilverStripe\Core\Tests\Config\ConfigTest\ConfigTestMemCache
Page_Controller: PageController
skipConfigs:
- db
- casting

View File

@ -335,13 +335,22 @@ types right now, we will go into much more detail in the [next tutorial](/tutori
Create a new file *HomePage.php* in *mysite/code*. Copy the following code into it:
:::php
```php
<?php
class HomePage extends Page {
}
class HomePage_Controller extends Page_Controller {
use Page;
use PageController;
class HomePage extends Page
{
}
class HomePageController extends PageController
{
}
```
Every page type also has a database table corresponding to it. Every time we modify the database, we need to rebuild it.
We can do this by going to `http://localhost/your_site_name/dev/build`.

View File

@ -18,17 +18,26 @@ We will create a poll on the home page that asks the user their favourite web br
## Creating the form
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 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 *HomePageController*.
**mysite/code/HomePage.php**
**mysite/code/HomePageController.php**
```php
class HomePage_Controller extends Page_Controller {
use PageController;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\OptionSetField;
use SilverStripe\Forms\TextField;
class HomePageController extends PageController
{
private static $allowed_actions = array('BrowserPollForm');
// ...
public function BrowserPollForm() {
public function BrowserPollForm()
{
// Create fields
$fields = new FieldList(
new TextField('Name'),
@ -50,10 +59,9 @@ The poll we will be creating on our homepage will ask the user for their name an
return new Form($this, 'BrowserPollForm', $fields, $actions);
}
...
// ...
}
...
// ...
```
Let's step through this code.
@ -185,19 +193,27 @@ If you recall, in the [second tutorial](/tutorials/extending_a_basic_site) we sa
```php
<?php
class BrowserPollSubmission extends DataObject {
use SilverStripe\ORM\DataObject;
class BrowserPollSubmission extends DataObject
{
private static $db = array(
'Name' => 'Text',
'Browser' => 'Text'
);
}
```
If we then rebuild the database ([http://localhost/your_site_name/dev/build](http://localhost/your_site_name/dev/build)), 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/your_site_name/dev/build](http://localhost/your_site_name/dev/build)), we will see that the *BrowserPollSubmission* table is created. Now we just need to define 'doBrowserPoll' on *HomePageController*:
**mysite/code/HomePage.php**
**mysite/code/HomePageController.php**
```php
class HomePage_Controller extends Page_Controller {
use BrowserPollSubmission;
use PageController;
class HomePageController extends PageController
{
// ...
public function doBrowserPoll($data, $form) {
$submission = new BrowserPollSubmission();
@ -218,12 +234,13 @@ SilverStripe forms all have automatic validation on fields where it is logical.
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:
Add a namespace import for `SilverStripe\Forms\RequiredFields`, then change the end of the 'BrowserPollForm' function so it looks like this:
**mysite/code/HomePage.php**
```php
public function BrowserPollForm() {
public function BrowserPollForm()
{
// ...
$validator = new RequiredFields('Name', 'Browser');
return new Form($this, 'BrowserPollForm', $fields, $actions, $validator);
@ -242,13 +259,15 @@ The first thing to do is make it so a user can only vote once per session. If th
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/HomePageController.php**
```php
// ...
class HomePage_Controller extends Page_Controller {
class HomePageController extends PageController
{
// ...
public function doBrowserPoll($data, $form) {
public function doBrowserPoll($data, $form)
{
$submission = new BrowserPollSubmission();
$form->saveInto($submission);
$submission->write();
@ -263,10 +282,14 @@ it is.
```php
// ...
class HomePage_Controller extends Page_Controller {
class HomePageController extends PageController
{
// ...
public function BrowserPollForm() {
if(Session::get('BrowserPollVoted')) return false;
public function BrowserPollForm()
{
if (Session::get('BrowserPollVoted')) {
return false;
}
// ...
}
}
@ -281,12 +304,13 @@ Now that we're collecting data, it would be nice to show the results on the webs
In the [second tutorial](/tutorials/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.
Add the appropriate namespace imports, then create the function 'BrowserPollResults' on the *HomePageController* class.
**mysite/code/HomePage.php**
**mysite/code/HomePageController.php**
```php
public function BrowserPollResults() {
public function BrowserPollResults()
{
$submissions = new GroupedList(BrowserPollSubmission::get());
$total = $submissions->Count();
@ -306,6 +330,7 @@ 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).
```php

View File

@ -38,9 +38,13 @@ Let's create the `Student` and `Project` objects.
**mysite/code/Student.php**
:::php
```php
<?php
class Student extends DataObject {
use SilverStripe\ORM\DataObject;
class Student extends DataObject
{
private static $db = array(
'Name' => 'Varchar',
'University' => 'Varchar',
@ -49,18 +53,34 @@ Let's create the `Student` and `Project` objects.
'Project' => 'Project'
);
}
```
**mysite/code/Project.php**
:::php
```php
<?php
class Project extends Page {
use Page;
class Project extends Page
{
private static $has_many = array(
'Students' => 'Student'
);
}
class Project_Controller extends Page_Controller {
```
**mysite/code/ProjectController.php**
```php
<?php
use PageController;
class ProjectController extends PageController
{
}
```
The relationships are defined through the `$has_one`
and `$has_many` properties on the objects.
@ -97,13 +117,28 @@ The restriction is enforced through the `$allowed_children` directive.
:::php
<?php
use Page;
class ProjectsHolder extends Page {
private static $allowed_children = array(
'Project'
);
}
class ProjectsHolder_Controller extends Page_Controller {
```
**mysite/code/ProjectsHolderController.php
```php
<?php
use PageController;
class ProjectsHolderController extends PageController
{
}
```
You might have noticed that we don't specify the relationship
to a project. That's because it's already inherited from the parent implementation,
@ -128,17 +163,26 @@ All customization to fields for a page type are managed through a method called
**mysite/code/Project.php**
:::php
```php
<?php
class Project extends Page {
use Page;
use SilverStripe\Forms\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
class Project extends Page
{
// ...
public function getCMSFields() {
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(
$config
->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns')
->setDisplayFields(array(
'Name' => 'Name',
'Project.Title'=> 'Project' // Retrieve from a has-one relationship
));
@ -154,6 +198,7 @@ All customization to fields for a page type are managed through a method called
return $fields;
}
}
```
This creates a tabular field, which lists related student records, one row at a time.
It's empty by default, but you can add new students as required,
@ -200,9 +245,13 @@ The first step is to create the `Mentor` object and set the relation with the `P
**mysite/code/Mentor.php**
:::php
```php
<?php
class Mentor extends DataObject {
use SilverStripe\ORM\DataObject;
class Mentor extends DataObject
{
private static $db = array(
'Name' => 'Varchar',
);
@ -210,16 +259,23 @@ The first step is to create the `Mentor` object and set the relation with the `P
'Projects' => 'Project'
);
}
```
**mysite/code/Project.php**
:::php
class Project extends Page {
```php
<?php
use Page;
class Project extends Page
{
// ...
private 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).
@ -231,10 +287,18 @@ to configure it a bit differently.
**mysite/code/Project.php**
:::php
class Project extends Page {
```php
<?php
use Page;
use SilverStripe\Forms\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
class Project extends Page
{
// ...
public function getCMSFields() {
public function getCMSFields()
{
// ...
// Same setup, but for mentors
$mentorsField = new GridField(
@ -247,6 +311,7 @@ to configure it a bit differently.
return $fields;
}
}
```
The important difference to our student management UI is the usage
of `$this->Mentor()` (rather than `Mentor::get()`). It will limit
@ -286,7 +351,7 @@ a named list of object.
**themes/simple/templates/Layout/ProjectsHolder.ss**
:::ss
```ss
<% include SideBar %>
<div class="content-container unit size3of4 lastUnit">
<article>
@ -309,12 +374,12 @@ a named list of object.
</td>
<td>
<% loop $Students %>
$Name ($University)<% if $Last !=1 %>,<% end_if %>
$Name ($University)<% if not $Last %>, <% end_if %>
<% end_loop %>
</td>
<td>
<% loop $Mentors %>
$Name<% if $Last !=1 %>,<% end_if %>
$Name<% if not $Last %>, <% end_if %>
<% end_loop %>
</td>
</tr>
@ -324,6 +389,7 @@ a named list of object.
</div>
</article>
</div>
```
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.
@ -344,7 +410,7 @@ we can access the "Students" and "Mentors" relationships directly in the templat
**themes/simple/templates/Layout/Project.ss**
:::ss
```ss
<% include SideBar %>
<div class="content-container unit size3of4 lastUnit">
<article>
@ -374,6 +440,7 @@ we can access the "Students" and "Mentors" relationships directly in the templat
</div>
</article>
</div>
```
Follow the link to a project detail from from your holder page,
or navigate to it through the submenu provided by the theme.
@ -387,17 +454,23 @@ by introducing a new template for them.
**themes/simple/templates/Includes/StudentInfo.ss**
:::ss
```ss
$Name ($University)
```
To use this template, we need to add a new method to our student class:
:::php
class Student extends DataObject {
function getInfo() {
```php
use SilverStripe\ORM\DataObject;
class Student extends DataObject
{
public function getInfo()
{
return $this->renderWith('StudentInfo');
}
}
```
Replace the student template code in both `Project.ss`
and `ProjectHolder.ss` templates with the new placeholder, `$Info`.

View File

@ -98,7 +98,8 @@ Variables can come from your database fields, or custom methods you define on yo
**mysite/code/Page.php**
:::php
public function UsersIpAddress() {
public function UsersIpAddress()
{
return $this->getRequest()->getIP();
}
@ -112,8 +113,8 @@ Variables can come from your database fields, or custom methods you define on yo
</div>
The variables that can be used in a template vary based on the object currently in [scope](#scope). Scope defines what
object the methods get called on. For the standard `Page.ss` template the scope is the current [api:Page_Controller]
class. This object gives you access to all the database fields on [api:Page_Controller], its corresponding [api:Page]
object the methods get called on. For the standard `Page.ss` template the scope is the current [api:PageController]
class. This object gives you access to all the database fields on [api:PageController], its corresponding [api:Page]
record and any subclasses of those two.
**mysite/code/Layout/Page.ss**
@ -407,12 +408,12 @@ In the `<% loop %>` section, we saw an example of two **scopes**. Outside the `<
the scope of the top level `Page`. But inside the loop, we were in the scope of an item in the list (i.e the `Child`)
The scope determines where the value comes from when you refer to a variable. Typically the outer scope of a `Page.ss`
layout template is the [api:Page_Controller] that is currently being rendered.
layout template is the [api:PageController] that is currently being rendered.
When the scope is a `Page_Controller` it will automatically also look up any methods in the corresponding `Page` data
When the scope is a `PageController` it will automatically also look up any methods in the corresponding `Page` data
record. In the case of `$Title` the flow looks like
$Title --> [Looks up: Current Page_Controller and parent classes] --> [Looks up: Current Page and parent classes]
$Title --> [Looks up: Current PageController and parent classes] --> [Looks up: Current Page and parent classes]
The list of variables you could use in your template is the total of all the methods in the current scope object, parent
classes of the current scope object, and any [api:Extension] instances you have.

View File

@ -15,7 +15,7 @@ scope, and you can specify additional static methods to be available globally in
<div class="notice" markdown="1">
Want a quick way of knowing what scope you're in? Try putting `$ClassName` in your template. You should see a string
such as `Page` of the object that's in scope. The methods you can call on that object then are any functions, database
properties or relations on the `Page` class, `Page_Controller` class as well as anything from their subclasses **or**
properties or relations on the `Page` class, `PageController` class as well as anything from their subclasses **or**
extensions.
</div>

View File

@ -12,15 +12,16 @@ The following will render the given data into a template. Given the template:
**mysite/templates/Coach_Message.ss**
:::ss
```ss
<strong>$Name</strong> is the $Role on our team.
```
Our application code can render into that view using `renderWith`. This method is called on the [api:ViewableData]
instance with a template name or an array of templates to render.
**mysite/code/Page.php**
:::php
```php
$arrayData = new ArrayData(array(
'Name' => 'John',
'Role' => 'Head Coach'
@ -29,47 +30,55 @@ instance with a template name or an array of templates to render.
echo $arrayData->renderWith('Coach_Message');
// returns "<strong>John</strong> is the Head Coach on our team."
```
<div class="info" markdown="1">
Most classes in SilverStripe you want in your template extend `ViewableData` and allow you to call `renderWith`. This
includes [api:Controller], [api:FormField] and [api:DataObject] instances.
</div>
:::php
$controller->renderWith(array("MyController", "MyBaseController"));
```php
$controller->renderWith(array('MyController', 'MyBaseController'));
Member::currentUser()->renderWith('Member_Profile');
```
`renderWith` can be used to override the default template process. For instance, to provide an ajax version of a
template.
:::php
```php
<?php
class Page_Controller extends ContentController {
use SilverStripe\CMS\Controllers\ContentController;
class PageController extends ContentController
{
private static $allowed_actions = array('iwantmyajax');
public function iwantmyajax() {
public function iwantmyajax()
{
if (Director::is_ajax()) {
return $this->renderWith("AjaxTemplate");
return $this->renderWith('AjaxTemplate');
} else {
return $this->httpError(404);
}
}
}
```
Any data you want to render into the template that does not extend `ViewableData` should be wrapped in an object that
does, such as `ArrayData` or `ArrayList`.
:::php
```php
<?php
class Page_Controller extends ContentController {
use SilverStripe\CMS\Controllers\ContentController;
..
public function iwantmyajax() {
class PageController extends ContentController
{
// ..
public function iwantmyajax()
{
if (Director::is_ajax()) {
$experience = new ArrayList();
$experience->push(new ArrayData(array(
@ -80,10 +89,10 @@ does, such as `ArrayData` or `ArrayList`.
'Name' => 'John',
'Role' => 'Head Coach',
'Experience' => $experience
)))->renderWith("AjaxTemplate");
)))->renderWith('AjaxTemplate');
} else {
return $this->httpError(404);
}
}
}
```

View File

@ -34,7 +34,7 @@ at http://yoursite.com/teams/ and the `players` custom action is at http://yours
<div class="info" markdown="1">
If you're using the `cms` module with and dealing with `Page` objects then for your custom `Page Type` controllers you
would extend `ContentController` or `Page_Controller`. You don't need to define the routes value as the `cms` handles
would extend `ContentController` or `PageController`. You don't need to define the routes value as the `cms` handles
routing.
</div>

View File

@ -4,11 +4,11 @@ summary: A more in depth look at how to map requests to particular controllers a
# Routing
Routing is the process of mapping URL's to [api:Controllers] and actions. In the introduction we defined a new custom route
for our `TeamsController` mapping any `teams` URL to our `TeamsController`
for our `TeamController` mapping any `teams` URL to our `TeamController`
<div class="info" markdown="1">
If you're using the `cms` module with and dealing with `Page` objects then for your custom `Page Type` controllers you
would extend `ContentController` or `Page_Controller`. You don't need to define the routes value as the `cms` handles
would extend `ContentController` or `PageController`. You don't need to define the routes value as the `cms` handles
routing.
</div>
@ -17,7 +17,7 @@ These routes by standard, go into a `routes.yml` file in your applications `_con
**mysite/_config/routes.yml**
:::yml
```yml
---
Name: mysiteroutes
After: framework/routes#coreroutes
@ -27,6 +27,7 @@ These routes by standard, go into a `routes.yml` file in your applications `_con
'teams//$Action/$ID/$Name': 'TeamController'
'player/': 'PlayerController'
'': 'HomeController'
```
<div class="notice" markdown="1">
To understand the syntax for the `routes.yml` file better, read the [Configuration](../configuration) documentation.
@ -34,8 +35,9 @@ To understand the syntax for the `routes.yml` file better, read the [Configurati
## Parameters
:::yml
```yml
'teams//$Action/$ID/$Name': 'TeamController'
```
This route has defined that any URL beginning with `team` should create, and be handled by a `TeamController` instance.
@ -49,7 +51,7 @@ All Controllers have access to `$this->getRequest()` for the request object and
Here is what those parameters would look like for certain requests
:::php
```php
// GET /teams/
print_r($this->getRequest()->params());
@ -82,16 +84,16 @@ Here is what those parameters would look like for certain requests
// [ID] => 1
// [Name] => null
// )
```
You can also fetch one parameter at a time.
:::php
```php
// GET /teams/players/1/
echo $this->getRequest()->param('ID');
// returns '1'
```
## URL Patterns
@ -108,26 +110,29 @@ A rule must always start with alphabetical ([A-Za-z]) characters or a $Variable
| `!` | **Require Variable** - Placing this after a parameter variable requires data to be present for the rule to match |
| `//` | **Shift Point** - Declares that only variables denoted with a $ are parsed into the $params AFTER this point in the regex |
:::yml
```yml
'teams/$Action/$ID/$OtherID': 'TeamController'
# /teams/
# /teams/players/
# /teams/
```
Standard URL handler syntax. For any URL that contains 'team' this rule will match and hand over execution to the
matching controller. The `TeamsController` is passed an optional action, id and other id parameters to do any more
decision making.
:::yml
```yml
'teams/$Action!/$ID!/': 'TeamController'
```
This does the same matching as the previous example, any URL starting with `teams` will look at this rule **but** both
`$Action` and `$ID` are required. Any requests to `team/` will result in a `404` error rather than being handed off to
the `TeamController`.
:::yml
`admin/help//$Action/$ID`: 'AdminHelp'
```yml
'admin/help//$Action/$ID: 'AdminHelp'
```
Match an url starting with `/admin/help/`, but don't include `/help/` as part of the action (the shift point is set to
start parsing variables and the appropriate controller action AFTER the `//`).
@ -152,11 +157,13 @@ This is useful when you want to provide custom actions for the mapping of `teams
**mysite/code/controllers/TeamController.php**
:::php
```
<?php
class TeamController extends Controller {
use SilverStripe\Control\Controller;
class TeamController extends Controller
{
private static $allowed_actions = array(
'payroll'
);
@ -165,6 +172,7 @@ This is useful when you want to provide custom actions for the mapping of `teams
'staff/$ID/$Name' => 'payroll',
'coach/$ID/$Name' => 'payroll'
);
```
The syntax for the `$url_handlers` array users the same pattern matches as the `YAML` configuration rules.
@ -175,28 +183,34 @@ class specifies the URL pattern in `$url_handlers`. Notice that it defines 5
parameters.
:::php
class FeedController extends ContentController {
```php
use SilverStripe\CMS\Controllers\ContentController;
class FeedController extends ContentController
{
private static $allowed_actions = array('go');
private static $url_handlers = array(
'go/$UserName/$AuthToken/$Timestamp/$OutputType/$DeleteMode' => 'go'
);
public function go() {
public function go()
{
$this->validateUser(
$this->getRequest()->param('UserName'),
$this->getRequest()->param('AuthToken')
);
/* more processing goes here */
}
}
The YAML rule, in contrast, is simple. It needs to provide only enough
information for the framework to choose the desired controller.
:::yaml
```yml
Director:
rules:
'feed': 'FeedController'
```
## Links

View File

@ -27,22 +27,30 @@ In practice, this looks like:
**mysite/code/Page.php**
:::php
```php
<?php
class Page_Controller extends ContentController {
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\TextField;
class PageController extends ContentController
{
private static $allowed_actions = array(
'HelloForm'
);
public function HelloForm() {
public function HelloForm()
{
$fields = new FieldList(
TextField::create('Name', 'Your Name')
);
$actions = new FieldList(
FormAction::create("doSayHello")->setTitle("Say hello")
FormAction::create('doSayHello')->setTitle('Say hello')
);
$required = new RequiredFields('Name');
@ -52,18 +60,20 @@ In practice, this looks like:
return $form;
}
public function doSayHello($data, Form $form) {
public function doSayHello($data, Form $form)
{
$form->sessionMessage('Hello ' . $data['Name'], 'success');
return $this->redirectBack();
}
}
```
**mysite/templates/Page.ss**
:::ss
```ss
$HelloForm
```
<div class="info" markdown="1">
The examples above use `FormField::create()` instead of the `new` operator (`new FormField()`). These are functionally
@ -80,10 +90,11 @@ the [api:FormActions]. The URL is known as the `$controller` instance will know
Because the `HelloForm()` method will be the location the user is taken to, it needs to be handled like any other
controller action. To grant it access through URLs, we add it to the `$allowed_actions` array.
:::php
```php
private static $allowed_actions = array(
'HelloForm'
);
```
<div class="notice" markdown="1">
Form actions (`doSayHello`), on the other hand, should _not_ be included in `$allowed_actions`; these are handled
@ -96,8 +107,9 @@ separately through [api:Form::httpSubmission()].
Fields in a [api:Form] are represented as a single [api:FieldList] instance containing subclasses of [api:FormField].
Some common examples are [api:TextField] or [api:DropdownField].
:::php
```php
TextField::create($name, $title, $value);
```
<div class="info" markdown='1'>
A list of the common FormField subclasses is available on the [Common Subclasses](field_types/common_subclasses/) page.
@ -106,7 +118,7 @@ A list of the common FormField subclasses is available on the [Common Subclasses
The fields are added to the [api:FieldList] `fields` property on the `Form` and can be modified at up to the point the
`Form` is rendered.
:::php
```php
$fields = new FieldList(
TextField::create('Name'),
EmailField::create('Email')
@ -119,35 +131,39 @@ The fields are added to the [api:FieldList] `fields` property on the `Form` and
// to fetch the current fields..
$fields = $form->getFields();
```
A field can be appended to the [api:FieldList].
:::php
```php
$fields = $form->Fields();
// add a field
$fields->push(TextField::create(..));
$fields->push(TextField::create(/* ... */));
// insert a field before another one
$fields->insertBefore(TextField::create(..), 'Email');
$fields->insertBefore(TextField::create(/* ... */), 'Email');
// insert a field after another one
$fields->insertAfter(TextField::create(..), 'Name');
$fields->insertAfter(TextField::create(/* ... */), 'Name');
// insert a tab before the main content tab (used to position tabs in the CMS)
$fields->insertBefore(Tab::create(...), 'Main');
$fields->insertBefore(Tab::create(/* ... */), 'Main');
// Note: you need to create and position the new tab prior to adding fields via addFieldToTab()
```
Fields can be fetched after they have been added in.
:::php
```php
$email = $form->Fields()->dataFieldByName('Email');
$email->setTitle('Your Email Address');
```
Fields can be removed from the form.
:::php
```php
$form->getFields()->removeByName('Email');
```
<div class="alert" markdown="1">
Forms can be tabbed (such as the CMS interface). In these cases, there are additional functions such as `addFieldToTab`
@ -164,13 +180,14 @@ default `FormField` object has several methods for doing common operations.
Most of the `set` operations will return the object back so methods can be chained.
</div>
:::php
```php
$field = new TextField(..);
$field
->setMaxLength(100)
->setAttribute('placeholder', 'Enter a value..')
->setTitle('');
```
### Custom Templates
@ -178,7 +195,7 @@ The [api:Form] HTML markup and each of the [api:FormField] instances are rendere
templates by using the `setTemplate` method on either the `Form` or `FormField`. For more details on providing custom
templates see [Form Templates](form_templates)
:::php
```php
$form = new Form(..);
$form->setTemplate('CustomForm');
@ -188,21 +205,24 @@ templates see [Form Templates](form_templates)
$field->setTemplate('CustomTextField');
$field->setFieldHolderTemplate('CustomTextField_Holder');
```
## Adding FormActions
[api:FormAction] objects are displayed at the bottom of the `Form` in the form of a `button` or `input` tag. When a
user presses the button, the form is submitted to the corresponding method.
:::php
```php
FormAction::create($action, $title);
```
As with [api:FormField], the actions for a `Form` are stored within a [api:FieldList] instance in the `actions` property
on the form.
:::php
public function MyForm() {
$fields = new FieldList(..);
```php
public function MyForm()
{
$fields = new FieldList(/* .. */);
$actions = new FieldList(
FormAction::create('doSubmitForm', 'Submit')
@ -226,13 +246,16 @@ on the form.
return $form
}
public function doSubmitForm($data, $form) {
public function doSubmitForm($data, $form)
{
//
}
public function doSecondaryFormAction($data, $form) {
public function doSecondaryFormAction($data, $form)
{
//
}
```
The first `$action` argument for creating a `FormAction` is the name of the method to invoke when submitting the form
with the particular button. In the previous example, clicking the 'Another Button' would invoke the
@ -252,16 +275,24 @@ The `$action` method takes two arguments:
* `$data` an array containing the values of the form mapped from `$name => $value`
* `$form` the submitted [api:Form] instance.
:::php
```php
<?php
class Page_Controller extends ContentController {
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\TextField;
class PageController extends ContentController
{
private static $allowed_actions = array(
'MyForm'
);
public function MyForm() {
public function MyForm()
{
$fields = new FieldList(
TextField::create('Name'),
EmailField::create('Email')
@ -276,7 +307,8 @@ The `$action` method takes two arguments:
return $form
}
public function doSubmitForm($data, $form) {
public function doSubmitForm($data, $form)
{
// Submitted data is available as a map.
echo $data['Name'];
echo $data['Email'];
@ -285,12 +317,13 @@ The `$action` method takes two arguments:
echo $form->Fields()->dataFieldByName('Email')->Value();
// Using the Form instance you can get / set status such as error messages.
$form->sessionMessage("Successful!", 'good');
$form->sessionMessage('Successful!', 'good');
// After dealing with the data you can redirect the user back.
return $this->redirectBack();
}
}
```
## Validation
@ -300,12 +333,14 @@ validating its' own data value.
For more information, see the [Form Validation](validation) documentation.
:::php
```php
$validator = new RequiredFields(array(
'Name', 'Email'
'Name',
'Email'
));
$form = new Form($this, 'MyForm', $fields, $actions, $validator);
```
## API Documentation

View File

@ -7,16 +7,24 @@ SilverStripe provides server-side form validation out of the box through the [ap
[api:RequiredFields]. A single `Validator` instance is set on each `Form`. Validators are implemented as an argument to
the [api:Form] constructor or through the function `setValidator`.
:::php
```php
<?php
class Page_Controller extends ContentController {
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\RequiredFields;
class PageController extends ContentController
{
private static $allowed_actions = array(
'MyForm'
);
public function MyForm() {
public function MyForm()
{
$fields = new FieldList(
TextField::create('Name'),
EmailField::create('Email')
@ -40,10 +48,12 @@ the [api:Form] constructor or through the function `setValidator`.
return $form;
}
public function doSubmitForm($data, $form) {
public function doSubmitForm($data, $form)
{
//..
}
}
```
In this example we will be required to input a value for `Name` and a valid email address for `Email` before the
`doSubmitForm` method is called.
@ -63,15 +73,17 @@ The data value of the `FormField` submitted is not passed into validate. It is s
the `setValue` method.
</div>
:::php
public function validate($validator) {
if($this->Value() == 10) {
```php
public function validate($validator)
{
if ((int) $this->Value() === 10) {
$validator->validationError($this->Name(), 'This value cannot be 10');
return false;
}
return true;
}
```
The `validate` method should return `true` if the value passes any validation and `false` if SilverStripe should trigger
a validation error on the page. In addition a useful error message must be set on the given validator.
@ -86,24 +98,26 @@ two ways to go about this:
A custom `FormField` which handles the validation. This means the `FormField` can be reused throughout the site and have
the same validation logic applied to it throughout.
**mysite/code/formfields/CustomNumberField.php**
**mysite/code/CustomNumberField.php**
:::php
```php
<?php
class CustomNumberField extends TextField {
use SilverStripe\Forms\TextField;
public function validate($validator) {
class CustomNumberField extends TextField
{
public function validate($validator)
{
if (!is_numeric($this->value)) {
$validator->validationError(
$this->name, "Not a number. This must be between 2 and 5", "validation", false
$this->name, 'Not a number. This must be between 2 and 5', 'validation', false
);
return false;
}
else if($this->value > 5 || $this->value < 2) {
} elseif ($this->value > 5 || $this->value < 2) {
$validator->validationError(
$this->name, "Your number must be between 2 and 5", "validation", false
$this->name, 'Your number must be between 2 and 5', 'validation', false
);
return false;
@ -112,21 +126,31 @@ the same validation logic applied to it throughout.
return true;
}
}
```
Or, an alternative approach to the custom class is to define the behavior inside the Form's action method. This is less
reusable and would not be possible within the `CMS` or other automated `UI` but does not rely on creating custom
`FormField` classes.
:::php
```php
<?php
class Page_Controller extends ContentController {
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\TextField;
use SilverStripe\Security\Member;
class Page_Controller extends ContentController
{
private static $allowed_actions = array(
'MyForm'
);
public function MyForm() {
public function MyForm()
{
$fields = new FieldList(
TextField::create('Name'),
EmailField::create('Email')
@ -141,7 +165,8 @@ reusable and would not be possible within the `CMS` or other automated `UI` but
return $form;
}
public function doSubmitForm($data, $form) {
public function doSubmitForm($data, $form)
{
// At this point, RequiredFields->isValid() will have been called already,
// so we can assume that the values exist. Say we want to make sure that email hasn't already been used.
@ -154,11 +179,12 @@ reusable and would not be possible within the `CMS` or other automated `UI` but
}
$form->sessionMessage("You have been added to our mailing list", 'good');
$form->sessionMessage('You have been added to our mailing list', 'good');
return $this->redirectBack();
}
}
```
## Exempt validation actions
@ -167,8 +193,7 @@ data may not need to check the validity of the posted content.
You can disable validation on individual using one of two methods:
:::php
```php
$actions = new FieldList(
$action = FormAction::create('doSubmitForm', 'Submit')
);
@ -179,19 +204,20 @@ You can disable validation on individual using one of two methods:
// Alternatively, you can whitelist individual actions on the form object by name
$form->setValidationExemptActions(['doSubmitForm']);
```
## Server-side validation messages
If a `FormField` fails to pass `validate()` the default error message is returned.
:::php
```
'$Name' is required
```
Use `setCustomValidationMessage` to provide a custom message.
:::php
$field = new TextField(..);
```php
$field = new TextField(/* .. */);
$field->setCustomValidationMessage('Whoops, looks like you have missed me!');
## JavaScript validation
@ -201,15 +227,15 @@ to provide the information required in order to plug in custom libraries like [P
[jQuery.Validate](http://jqueryvalidation.org/). Most of these libraries work on HTML `data-` attributes or special
classes added to each input. For Parsley we can structure the form like.
:::php
$form = new Form(..);
```php
$form = new Form(/* .. */);
$form->setAttribute('data-parsley-validate', true);
$field = $fields->dataFieldByName('Name');
$field->setAttribute('required', true);
$field->setAttribute('data-parsley-mincheck', '2');
```
## Model Validation
@ -228,11 +254,14 @@ error message, or a [api:ValidationResult] object containing the list of errors
E.g.
```php
use SilverStripe\Control\Controller;
use SilverStripe\ORM\ValidationException;
:::php
class MyController extends Controller
{
public function doSave($data, $form) {
public function doSave($data, $form)
{
$success = $this->sendEmail($data);
// Example error handling
@ -244,7 +273,7 @@ E.g.
return $this->redirect($this->Link('success'));
}
}
```
### Validation in the CMS
@ -257,16 +286,21 @@ respect the provided `Validator` and handle displaying error and success respons
Again, custom error messages can be provided through the `FormField`
</div>
:::php
```php
<?php
class Page extends SiteTree {
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\RequiredFields;
class Page extends SiteTree
{
private static $db = array(
'MyRequiredField' => 'Text'
);
public function getCMSFields() {
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Main',
@ -274,11 +308,14 @@ Again, custom error messages can be provided through the `FormField`
);
}
public function getCMSValidator() {
public function getCMSValidator()
{
return new RequiredFields(array(
'MyRequiredField'
));
}
}
```
## API Documentation

View File

@ -431,7 +431,7 @@ code could be used:
:::php
class GalleryPage extends Page {}
class GalleryPage_Controller extends Page_Controller {
class GalleryPageController extends PageController {
private static $allowed_actions = array('Form');
public function Form() {
$fields = new FieldList(

View File

@ -12,7 +12,7 @@ code for a `Form` is to create it as a subclass to `Form`. Let's look at a examp
:::php
<?php
class Page_Controller extends ContentController {
class PageController extends ContentController {
public function SearchForm() {
$fields = new FieldList(
@ -128,7 +128,7 @@ Our controller will now just have to create a new instance of this form object.
:::php
<?php
class Page_Controller extends ContentController {
class PageController extends ContentController {
private static $allowed_actions = array(
'SearchForm',

View File

@ -8,7 +8,7 @@ Let's start by defining a new `ContactPage` page type:
<?php
class ContactPage extends Page {
}
class ContactPage_Controller extends Page_Controller {
class ContactPageController extends PageController {
private static $allowed_actions = array('Form');
public function Form() {
$fields = new FieldList(
@ -61,7 +61,7 @@ If you now create a ContactPage in the CMS (making sure you have rebuilt the dat
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 {
class ContactPageController extends PageController {
private static $allowed_actions = array('Form');
public function Form() {
// ...

View File

@ -83,7 +83,7 @@ If your caching logic is complex or re-usable, you can define a method on your c
fragment.
For example, a block that shows a collection of rotating slides needs to update whenever the relationship
`Page::$many_many = array('Slides' => 'Slide')` changes. In Page_Controller:
`Page::$many_many = array('Slides' => 'Slide')` changes. In `PageController`:
:::php
@ -151,7 +151,7 @@ heavy load:
<% cached 'blogstatistics', $Blog.ID if $HighLoad %>
By adding a `HighLoad` function to your `Page_Controller`, you could enable or disable caching dynamically.
By adding a `HighLoad` function to your `PageController`, you could enable or disable caching dynamically.
To cache the contents of a page for all anonymous users, but dynamically calculate the contents for logged in members,
use something like:

View File

@ -14,27 +14,35 @@ The simple usage, Permission::check("PERM_CODE") will detect if the currently lo
**Group ACLs**
* Call **Permission::check("MY_PERMISSION_CODE")** to see if the current user has MY_PERMISSION_CODE.
* MY_PERMISSION_CODE can be loaded into the Security admin on the appropriate group, using the "Permissions" tab.
* Call **Permission::check('MY_PERMISSION_CODE')** to see if the current user has MY_PERMISSION_CODE.
* `MY_PERMISSION_CODE` can be loaded into the Security admin on the appropriate group, using the "Permissions" tab.
## PermissionProvider
[api:PermissionProvider] is an interface which lets you define a method *providePermissions()*.
This method should return a map of permission code names with a human readable explanation of its purpose.
:::php
class Page_Controller implements PermissionProvider {
public function init() {
```php
use SilverStripe\Security\PermissionProvider;
class PageController implements PermissionProvider
{
public function init()
{
parent::init();
if(!Permission::check("VIEW_SITE")) Security::permissionFailure();
if (!Permission::check('VIEW_SITE')) {
Security::permissionFailure();
}
}
public function providePermissions() {
public function providePermissions()
{
return array(
"VIEW_SITE" => "Access the site",
'VIEW_SITE' => 'Access the site'
);
}
}
```
This can then be used to add a dropdown for permission codes to the security panel. Permission::get_all_codes() will be
@ -89,10 +97,11 @@ This works much like ADMIN permissions (see above)
You can check if a user has access to the CMS by simply performing a check against `CMS_ACCESS`.
:::php
```php
if (Permission::checkMember($member, 'CMS_ACCESS')) {
//user can access the CMS
}
```
Internally, this checks that the user has any of the defined `CMS_ACCESS_*` permissions.

View File

@ -57,7 +57,7 @@ You can use [api:RSSFeed] to easily create a feed showing your latest Page updat
..
class Page_Controller extends ContentController {
class PageController extends ContentController {
private static $allowed_actions = array(
'rss'
@ -118,7 +118,7 @@ Then in our controller, we add a new action which returns a the XML list of `Pla
:::php
<?php
class Page_Controller extends ContentController {
class PageController extends ContentController {
private static $allowed_actions = array(
'players'

View File

@ -79,9 +79,9 @@ the `$fields` constructor parameter.
:::php
<?php
..
// ..
class Page_Controller extends ContentController {
class PageController extends ContentController {
public function SearchForm() {
$context = singleton('MyDataObject')->getCustomSearchContext();

View File

@ -59,7 +59,7 @@ authorised users, the following should be considered:
:::php
class Page_Controller extends ContentController {
class PageController extends ContentController {
public function init() {
parent::init();
// Whitelist any protected files on this page for the current user
@ -88,7 +88,7 @@ authorised users, the following should be considered:
:::php
class Page_Controller extends ContentController {
class PageController extends ContentController {
public function init() {
parent::init();
// Whitelist any protected files on this page for the current user

View File

@ -23,7 +23,7 @@ use SilverStripe\Core\Object;
* If you want to implement a FileField into a form element, you need to pass it an array of source data.
*
* <code>
* class ExampleForm_Controller extends Page_Controller {
* class ExampleFormController extends PageController {
*
* function Form() {
* $fields = new FieldList(

View File

@ -518,7 +518,7 @@ class Security extends Controller implements TemplateGlobalProvider
$tmpPage = new SiteTree();
$tmpPage->Title = $title;
/** @skipUpgrade */
$tmpPage->URLSegment = "Security";
$tmpPage->URLSegment = 'Security';
// Disable ID-based caching of the log-in page by making it a random number
$tmpPage->ID = -1 * rand(1, 10000000);

View File

@ -253,9 +253,9 @@ class SSViewer implements Flushable
$templates[] = $template;
$templates[] = ['type' => 'Includes', $template];
// If the class is "Page_Controller", look for Page.ss
if (stripos($class, '_controller') !== false) {
$templates[] = str_ireplace('_controller', '', $class) . $suffix;
// If the class is "PageController" (PSR-2 compatibility) or "Page_Controller" (legacy), look for Page.ss
if (preg_match('/^(?<name>.+[^\\\\])_?Controller$/iU', $class, $matches)) {
$templates[] = $matches['name'] . $suffix;
}
if ($baseClass && $class == $baseClass) {

View File

@ -23,7 +23,7 @@ use SilverStripe\View\Requirements_Backend;
use SilverStripe\View\SSViewer;
use SilverStripe\View\Requirements;
use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModel;
use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModel_Controller;
use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModelController;
use SilverStripe\View\ViewableData;
use SilverStripe\View\SSViewer_FromString;
use SilverStripe\View\SSTemplateParser;
@ -1526,17 +1526,15 @@ after'
public function testLayout()
{
$self = $this;
$this->useTestTheme(
__DIR__.'/SSViewerTest',
'layouttest',
function () use ($self) {
function () {
$template = new SSViewer(array('Page'));
$self->assertEquals("Foo\n\n", $template->process(new ArrayData(array())));
$this->assertEquals("Foo\n\n", $template->process(new ArrayData(array())));
$template = new SSViewer(array('Shortcodes', 'Page'));
$self->assertEquals("[file_link]\n\n", $template->process(new ArrayData(array())));
$this->assertEquals("[file_link]\n\n", $template->process(new ArrayData(array())));
}
);
}
@ -1546,23 +1544,22 @@ after'
*/
public function testGetTemplatesByClass()
{
$self = $this;
$this->useTestTheme(
__DIR__ . '/SSViewerTest',
'layouttest',
function () use ($self) {
function () {
// Test passing a string
$templates = SSViewer::get_templates_by_class(
SSViewerTestModel_Controller::class,
SSViewerTestModelController::class,
'',
Controller::class
);
$self->assertEquals(
$this->assertEquals(
[
SSViewerTestModel_Controller::class,
SSViewerTestModelController::class,
[
'type' => 'Includes',
SSViewerTestModel_Controller::class,
SSViewerTestModelController::class,
],
SSViewerTestModel::class,
Controller::class,
@ -1576,16 +1573,16 @@ after'
// Test to ensure we're stopping at the base class.
$templates = SSViewer::get_templates_by_class(
SSViewerTestModel_Controller::class,
SSViewerTestModelController::class,
'',
SSViewerTestModel_Controller::class
SSViewerTestModelController::class
);
$self->assertEquals(
$this->assertEquals(
[
SSViewerTestModel_Controller::class,
SSViewerTestModelController::class,
[
'type' => 'Includes',
SSViewerTestModel_Controller::class,
SSViewerTestModelController::class,
],
SSViewerTestModel::class,
],
@ -1595,27 +1592,27 @@ after'
// Make sure we can search templates by suffix.
$templates = SSViewer::get_templates_by_class(
SSViewerTestModel::class,
'_Controller',
'Controller',
DataObject::class
);
$self->assertEquals(
$this->assertEquals(
[
SSViewerTestModel_Controller::class,
SSViewerTestModelController::class,
[
'type' => 'Includes',
SSViewerTestModel_Controller::class,
SSViewerTestModelController::class,
],
DataObject::class.'_Controller',
DataObject::class . 'Controller',
[
'type' => 'Includes',
DataObject::class.'_Controller',
DataObject::class . 'Controller',
],
],
$templates
);
// Let's throw something random in there.
$self->setExpectedException('InvalidArgumentException');
$this->setExpectedException('InvalidArgumentException');
SSViewer::get_templates_by_class(array());
}
);

View File

@ -5,7 +5,7 @@ namespace SilverStripe\View\Tests\SSViewerTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Control\Controller;
class SSViewerTestModel_Controller extends Controller implements TestOnly
class SSViewerTestModelController extends Controller implements TestOnly
{
}