Merge pull request #7248 from open-sausages/pulls/4.0/remove-3.0-api-docs
MINOR: Clean up code references in docs
This commit is contained in:
commit
364a14da1d
|
@ -66,10 +66,10 @@ Due to some changes to `mod_dir` in [Apache 2.4](http://httpd.apache.org/docs/cu
|
|||
|
||||
```
|
||||
<IfModule mod_rewrite.c>
|
||||
# Turn off index.php handling requests to the homepage fixes issue in apache >=2.4
|
||||
<IfModule mod_dir.c>
|
||||
DirectoryIndex disabled
|
||||
</IfModule>
|
||||
# Turn off index.php handling requests to the homepage fixes issue in apache >=2.4
|
||||
<IfModule mod_dir.c>
|
||||
DirectoryIndex disabled
|
||||
</IfModule>
|
||||
# ------ #
|
||||
</IfModule>
|
||||
```
|
||||
|
|
|
@ -15,10 +15,11 @@ If you can log-in to the CMS as an administrator, append `?isDev=1` to any URL t
|
|||
"dev mode". If you can't log-in in the first place because of the error, add this directive to your `mysite/_config/config.yml`
|
||||
(don't forget to remove it afterwards!):
|
||||
|
||||
:::php
|
||||
```yml
|
||||
Director:
|
||||
# temporary debugging statement
|
||||
environment_type: 'dev'
|
||||
```
|
||||
|
||||
<div class="warning" markdown='1'>
|
||||
On "live" environments, the `?isDev=1` solution is preferred, as it means that your other visitors don't see ugly
|
||||
|
@ -31,10 +32,10 @@ Due to some changes to `mod_dir` in [Apache 2.4](http://httpd.apache.org/docs/cu
|
|||
|
||||
```
|
||||
<IfModule mod_rewrite.c>
|
||||
# Turn off index.php handling requests to the homepage fixes issue in apache >=2.4
|
||||
<IfModule mod_dir.c>
|
||||
DirectoryIndex disabled
|
||||
</IfModule>
|
||||
# Turn off index.php handling requests to the homepage fixes issue in apache >=2.4
|
||||
<IfModule mod_dir.c>
|
||||
DirectoryIndex disabled
|
||||
</IfModule>
|
||||
# ------ #
|
||||
</IfModule>
|
||||
```
|
||||
|
@ -103,23 +104,22 @@ Save it as `check.php` into your webroot, and run it as `php check.php` (or open
|
|||
After using the script (and fixing errors afterwards), please remember to remove it again.
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Check for whitespace around PHP brackets which show in output,
|
||||
// and hence can break HTML rendering and HTTP operations.
|
||||
$path = dirname(__FILE__);
|
||||
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::SELF_FIRST);
|
||||
$matched = false;
|
||||
foreach($files as $name => $file){
|
||||
if($file->getExtension() != 'php') continue;
|
||||
if(preg_match('/thirdparty|vendor/',$file->getPathname())) continue;
|
||||
if($file->getExtension() != 'php') continue;
|
||||
if(preg_match('/thirdparty|vendor/',$file->getPathname())) continue;
|
||||
$content = file_get_contents($file->getPathname());
|
||||
if(preg_match('/^[[:blank:]]+<\?' . 'php/', $content)) {
|
||||
echo sprintf("%s: Space before opening bracket\n", $file->getPathname());
|
||||
$matched = true;
|
||||
}
|
||||
echo sprintf("%s: Space before opening bracket\n", $file->getPathname());
|
||||
$matched = true;
|
||||
}
|
||||
if(preg_match('/^\?' . '>\n?[[:blank:]]+/m', $content)) {
|
||||
echo sprintf("%s: Space after closing bracket\n", $file->getPathname());
|
||||
$matched = true;
|
||||
echo sprintf("%s: Space after closing bracket\n", $file->getPathname());
|
||||
$matched = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -180,23 +180,23 @@ Full example of composer.json with the SSAutoGitIgnore installed and enabled
|
|||
|
||||
```json
|
||||
{
|
||||
"name": "silverstripe/installer",
|
||||
"description": "The SilverStripe Framework Installer",
|
||||
"require": {
|
||||
"php": ">=5.5.0",
|
||||
"silverstripe/cms": "^4.0",
|
||||
"silverstripe/framework": "^4.0",
|
||||
"silverstripe-themes/simple": "~3.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"silverstripe/docsviewer": "^3.0",
|
||||
"gdmedia/ss-auto-git-ignore": "^1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"post-update-cmd": "GDM\\SSAutoGitIgnore\\UpdateScript::Go"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
"name": "silverstripe/installer",
|
||||
"description": "The SilverStripe Framework Installer",
|
||||
"require": {
|
||||
"php": ">=5.5.0",
|
||||
"silverstripe/cms": "^4.0",
|
||||
"silverstripe/framework": "^4.0",
|
||||
"silverstripe-themes/simple": "~3.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"silverstripe/docsviewer": "^3.0",
|
||||
"gdmedia/ss-auto-git-ignore": "^1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"post-update-cmd": "GDM\\SSAutoGitIgnore\\UpdateScript::Go"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -48,9 +48,9 @@ database credentials could be the same. To do this, you can add this snippet to
|
|||
|
||||
```php
|
||||
try {
|
||||
(new \Dotenv\Dotenv('/path/to/env/'))->load();
|
||||
(new \Dotenv\Dotenv('/path/to/env/'))->load();
|
||||
} catch (\Dotenv\Exception\InvalidPathException $e) {
|
||||
// no file found
|
||||
// no file found
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -111,31 +111,37 @@ for a template file in the *simple/templates* folder, with the name `<PageType>`
|
|||
|
||||
Open *themes/simple/templates/Page.ss*. It uses standard HTML apart from these exceptions:
|
||||
|
||||
:::ss
|
||||
<% base_tag %>
|
||||
```ss
|
||||
<% base_tag %>
|
||||
```
|
||||
|
||||
The base_tag variable is replaced with the HTML [base element](http://www.w3.org/TR/html401/struct/links.html#h-12.4). This
|
||||
ensures the browser knows where to locate your site's images and css files.
|
||||
|
||||
:::ss
|
||||
$Title
|
||||
$SiteConfig.Title
|
||||
```ss
|
||||
$Title
|
||||
$SiteConfig.Title
|
||||
```
|
||||
|
||||
These two variables are found within the html `<title>` tag, and are replaced by the "Page Name" and "Settings -> Site Title" fields in the CMS.
|
||||
|
||||
:::ss
|
||||
$MetaTags
|
||||
```ss
|
||||
$MetaTags
|
||||
```
|
||||
|
||||
The MetaTags variable will add meta tags, which are used by search engines. You can define your meta tags in the tab fields at the bottom of the content editor in the CMS.
|
||||
:::ss
|
||||
$Layout
|
||||
|
||||
```ss
|
||||
$Layout
|
||||
```
|
||||
|
||||
The Layout variable is replaced with the contents of a template file with the same name as the page type we are using.
|
||||
|
||||
Open *themes/simple/templates/Layout/Page.ss*. You will see more HTML and more SilverStripe template replacement tags and variables.
|
||||
|
||||
:::ss
|
||||
$Content
|
||||
```ss
|
||||
$Content
|
||||
```
|
||||
|
||||
The Content variable is replaced with the content of the page currently being viewed. This allows you to make all changes to
|
||||
your site's content in the CMS.
|
||||
|
@ -144,9 +150,9 @@ These template markers are processed by SilverStripe into HTML before being sent
|
|||
browser and are either prefixed with a dollar sign ($)
|
||||
or placed between SilverStripe template tags:
|
||||
|
||||
:::ss
|
||||
<% %>
|
||||
|
||||
```ss
|
||||
<% %>
|
||||
```
|
||||
|
||||
**Flushing the cache**
|
||||
|
||||
|
@ -162,8 +168,9 @@ Open up *themes/simple/templates/Includes/Navigation.ss*
|
|||
|
||||
The Menu for our site is created using a **loop**. Loops allow us to iterate over a data set, and render each item using a sub-template.
|
||||
|
||||
:::ss
|
||||
<% loop $Menu(1) %>
|
||||
```ss
|
||||
<% loop $Menu(1) %>
|
||||
```
|
||||
|
||||
returns a set of first level menu items. We can then use the template variable
|
||||
*$MenuTitle* to show the title of the page we are linking to, *$Link* for the URL of the page, and `$isSection` and `$isCurrent` to help style our menu with CSS (explained in more detail shortly).
|
||||
|
@ -171,14 +178,15 @@ returns a set of first level menu items. We can then use the template variable
|
|||
> *$Title* refers to **Page Name** in the CMS, whereas *$MenuTitle* refers to (the often shorter) **Navigation label**
|
||||
|
||||
|
||||
:::ss
|
||||
<ul>
|
||||
<% loop $Menu(1) %>
|
||||
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
|
||||
<a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```ss
|
||||
<ul>
|
||||
<% loop $Menu(1) %>
|
||||
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
|
||||
<a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Here we've created an unordered list called *Menu1*, which *themes/simple/css/layout.css* will style into the menu.
|
||||
Then, using a loop over the page control *Menu(1)*, we add a link to the list for each menu item.
|
||||
|
@ -195,15 +203,17 @@ A useful feature is highlighting the current page the user is looking at. We can
|
|||
|
||||
For example, if you were here: "Home > Company > Staff > Bob Smith", you may want to highlight 'Company' to say you are in that section.
|
||||
|
||||
:::ss
|
||||
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
|
||||
<a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
|
||||
</li>
|
||||
```ss
|
||||
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
|
||||
<a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
you will then be able to target a section in css (*simple/css/layout.css*), e.g.:
|
||||
|
||||
:::css
|
||||
.section { background:#ccc; }
|
||||
```css
|
||||
.section { background:#ccc; }
|
||||
```
|
||||
|
||||
## A second level of navigation
|
||||
|
||||
|
@ -224,17 +234,18 @@ Great, we now have a hierarchical site structure! Let's look at how this is crea
|
|||
|
||||
Adding a second level menu is very similar to adding the first level menu. Open up */themes/simple/templates/Includes/Sidebar.ss* template and look at the following code:
|
||||
|
||||
:::ss
|
||||
<ul>
|
||||
<% loop $Menu(2) %>
|
||||
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
|
||||
<a href="$Link" title="Go to the $Title.XML page">
|
||||
<span class="arrow">→</span>
|
||||
<span class="text">$MenuTitle.XML</span>
|
||||
</a>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```ss
|
||||
<ul>
|
||||
<% loop $Menu(2) %>
|
||||
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
|
||||
<a href="$Link" title="Go to the $Title.XML page">
|
||||
<span class="arrow">→</span>
|
||||
<span class="text">$MenuTitle.XML</span>
|
||||
</a>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```
|
||||
|
||||
This should look very familiar. It is the same idea as our first menu, except the loop block now uses *Menu(2)* instead of *Menu(1)*.
|
||||
As we can see here, the *Menu* control takes a single
|
||||
|
@ -245,21 +256,22 @@ To make sure the menu is not displayed on every page, for example, those that *d
|
|||
Look again in the *Sidebar.ss* file and you will see that the menu is surrounded with an **if block**
|
||||
like this:
|
||||
|
||||
:::ss
|
||||
<% if $Menu(2) %>
|
||||
...
|
||||
<ul>
|
||||
<% loop $Menu(2) %>
|
||||
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
|
||||
<a href="$Link" title="Go to the $Title.XML page">
|
||||
<span class="arrow">→</span>
|
||||
<span class="text">$MenuTitle.XML</span>
|
||||
</a>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
...
|
||||
<% end_if %>
|
||||
```ss
|
||||
<% if $Menu(2) %>
|
||||
...
|
||||
<ul>
|
||||
<% loop $Menu(2) %>
|
||||
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
|
||||
<a href="$Link" title="Go to the $Title.XML page">
|
||||
<span class="arrow">→</span>
|
||||
<span class="text">$MenuTitle.XML</span>
|
||||
</a>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
...
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
The if block only includes the code inside it if the condition is true. In this case, it checks for the existence of
|
||||
*Menu(2)*. If it exists then the code inside will be processed and the menu will be shown. Otherwise the code will not
|
||||
|
@ -269,20 +281,22 @@ Now that we have two levels of navigation, it would also be useful to include so
|
|||
|
||||
Open up */themes/simple/templates/Includes/BreadCrumbs.ss* template and look at the following code:
|
||||
|
||||
:::ss
|
||||
<% if $Level(2) %>
|
||||
<div id="Breadcrumbs">
|
||||
$Breadcrumbs
|
||||
</div>
|
||||
<% end_if %>
|
||||
```ss
|
||||
<% if $Level(2) %>
|
||||
<div id="Breadcrumbs">
|
||||
$Breadcrumbs
|
||||
</div>
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
Breadcrumbs are only useful on pages that aren't in the top level. We can ensure that we only show them if we aren't in
|
||||
the top level with another if statement.
|
||||
|
||||
The *Level* page control allows you to get data from the page's parents, e.g. if you used *Level(1)*, you could use:
|
||||
|
||||
:::ss
|
||||
$Level(1).Title
|
||||
```ss
|
||||
$Level(1).Title
|
||||
```
|
||||
|
||||
to get the top level page title. In this case, we merely use it to check the existence of a second level page: if one exists then we include breadcrumbs.
|
||||
|
||||
|
@ -294,27 +308,27 @@ Feel free to experiment with the if and loop statements. For example, you could
|
|||
|
||||
The following example runs an if statement and a loop on *Children*, checking to see if any sub-pages exist within each top level navigation item. You will need to come up with your own CSS to correctly style this approach.
|
||||
|
||||
:::ss
|
||||
<ul>
|
||||
<% loop $Menu(1) %>
|
||||
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
|
||||
<a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
|
||||
<% if $Children %>
|
||||
<ul>
|
||||
<% loop $Children %>
|
||||
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
|
||||
<a href="$Link" title="Go to the $Title.XML page">
|
||||
<span class="arrow">→</span>
|
||||
<span class="text">$MenuTitle.XML</span>
|
||||
</a>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% end_if %>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
|
||||
```ss
|
||||
<ul>
|
||||
<% loop $Menu(1) %>
|
||||
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
|
||||
<a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
|
||||
<% if $Children %>
|
||||
<ul>
|
||||
<% loop $Children %>
|
||||
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
|
||||
<a href="$Link" title="Go to the $Title.XML page">
|
||||
<span class="arrow">→</span>
|
||||
<span class="text">$MenuTitle.XML</span>
|
||||
</a>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% end_if %>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```
|
||||
|
||||
|
||||
## Using a different template for the home page
|
||||
|
@ -336,8 +350,6 @@ 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
|
||||
|
||||
use Page;
|
||||
use PageController;
|
||||
|
||||
|
@ -379,21 +391,24 @@ It always tries to use the most specific template in an inheritance chain.
|
|||
To create a new template layout, create a copy of *Page.ss* (found in *themes/simple/templates/Layout*) and call it *HomePage.ss*. If we flush the cache (*?flush=1*), SilverStripe should now be using *HomePage.ss* for the homepage, and *Page.ss* for the rest of the site. Now let's customise the *HomePage* template.
|
||||
|
||||
First, we don't need the breadcrumbs and the secondary menu for the homepage. Let's remove them:
|
||||
:::ss
|
||||
<% include SideBar %>
|
||||
|
||||
|
||||
```ss
|
||||
<% include SideBar %>
|
||||
```
|
||||
|
||||
We'll also replace the title text with an image. Find this line:
|
||||
|
||||
:::ss
|
||||
<h1>$Title</h1>
|
||||
```ss
|
||||
<h1>$Title</h1>
|
||||
```
|
||||
|
||||
and replace it with:
|
||||
|
||||
:::ss
|
||||
<div id="Banner">
|
||||
<img src="http://www.silverstripe.org/assets/SilverStripe-200.png" alt="Homepage image" />
|
||||
</div>
|
||||
|
||||
```ss
|
||||
<div id="Banner">
|
||||
<img src="http://www.silverstripe.org/assets/SilverStripe-200.png" alt="Homepage image" />
|
||||
</div>
|
||||
```
|
||||
|
||||
Your Home page should now look like this:
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ presentation of our website.
|
|||
|
||||
### Controller
|
||||
|
||||
Each page type also has a **"controller"**. The controller contains all the code used to manipulate our data before it is rendered. For example, suppose we were making an auction site, and we only wanted to display the auctions closing in the next ten minutes. We would implement this logic in the controller. The controller for a page should inherit from [ContentController](api:SilverStripe\CMS\Controllers\ContentController). Just as we create a "Page" data object and subclass it for the rest of the site, we also create a "Page_Controller" that is subclassed.
|
||||
Each page type also has a **"controller"**. The controller contains all the code used to manipulate our data before it is rendered. For example, suppose we were making an auction site, and we only wanted to display the auctions closing in the next ten minutes. We would implement this logic in the controller. The controller for a page should inherit from [ContentController](api:SilverStripe\CMS\Controllers\ContentController). Just as we create a "Page" data object and subclass it for the rest of the site, we also create a "PageController" that is subclassed.
|
||||
|
||||
|
||||
Creating a new page type requires creating each of these three elements. We will then have full control over presentation, the database, and editable CMS fields.
|
||||
|
@ -63,16 +63,27 @@ A more in-depth introduction of Model-View-Controller can be found
|
|||
|
||||
To create a News section we'll need two new page types. The first one is obvious: we need an *ArticlePage* page type. The second is a little less obvious: we need an *ArticleHolder* page type to contain our article pages.
|
||||
|
||||
We'll start with the *ArticlePage* page type. First we create the model, a class called "ArticlePage". We put the *ArticlePage* class into a file called "ArticlePage.php" inside *mysite/code*. All other classes relating to *ArticlePage* should be placed within "ArticlePage.php", this includes our controller (*ArticlePage_Controller*).
|
||||
We'll start with the *ArticlePage* page type. First we create the model, a class called "ArticlePage". We put the *ArticlePage* class into a file called "ArticlePage.php" inside *mysite/code*. All other classes relating to *ArticlePage* should be placed within "ArticlePage.php", this includes our controller (*ArticlePageController*).
|
||||
|
||||
**mysite/code/ArticlePage.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class ArticlePage extends Page {
|
||||
}
|
||||
class ArticlePage_Controller extends Page_Controller {
|
||||
}
|
||||
```php
|
||||
use Page;
|
||||
|
||||
class ArticlePage extends Page
|
||||
{
|
||||
|
||||
}
|
||||
```
|
||||
**mysite/code/ArticlePageController.php**
|
||||
```php
|
||||
use PageController;
|
||||
|
||||
class ArticlePageController extends PageController
|
||||
{
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
@ -83,14 +94,23 @@ Let's create the *ArticleHolder* page type.
|
|||
|
||||
**mysite/code/ArticleHolder.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class ArticleHolder extends Page {
|
||||
private static $allowed_children = array('ArticlePage');
|
||||
}
|
||||
class ArticleHolder_Controller extends Page_Controller {
|
||||
}
|
||||
```php
|
||||
use Page;
|
||||
|
||||
class ArticleHolder extends Page
|
||||
{
|
||||
private static $allowed_children = ['ArticlePage'];
|
||||
}
|
||||
```
|
||||
**mysite/code/ArticleHolderController.php**
|
||||
```php
|
||||
use PageController;
|
||||
|
||||
class ArticleHolderController extends PageController
|
||||
{
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Here we have done something interesting: the *$allowed_children* field. This is one of a number of static fields we can define to change the properties of a page type. The *$allowed_children* field is an array of page types that are allowed
|
||||
to be children of the page in the site tree. As we only want **news articles** in the news section, we only want pages of the type *ArticlePage* as children. We can enforce this in the CMS by setting the *$allowed_children* field within this class.
|
||||
|
@ -111,17 +131,19 @@ Now that we have an *ArticlePage* page type, let's make it a little more useful.
|
|||
the $db array to add extra fields to the database. It would be nice to know when each article was posted, and who posted
|
||||
it. Add a *$db* property definition in the *ArticlePage* class:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class ArticlePage extends Page {
|
||||
private static $db = array(
|
||||
'Date' => 'Date',
|
||||
'Author' => 'Text'
|
||||
);
|
||||
|
||||
// .....
|
||||
}
|
||||
```php
|
||||
use Page;
|
||||
|
||||
class ArticlePage extends Page
|
||||
{
|
||||
private static $db = [
|
||||
'Date' => 'Date',
|
||||
'Author' => 'Text'
|
||||
];
|
||||
|
||||
// .....
|
||||
}
|
||||
```
|
||||
|
||||
Every entry in the array is a *key => value* pair. The **key** is the name of the field, and the **value** is the type. See ["data types and casting"](/developer_guides/model/data_types_and_casting) for a complete list of types.
|
||||
|
||||
|
@ -134,39 +156,44 @@ When we rebuild the database, we will see that the *ArticlePage* table has been
|
|||
|
||||
To add our new fields to the CMS we have to override the *getCMSFields()* method, which is called by the CMS when it creates the form to edit a page. Add the method to the *ArticlePage* class.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class ArticlePage extends Page {
|
||||
// ...
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$dateField = new DateField('Date');
|
||||
$dateField->setConfig('showcalendar', true);
|
||||
$fields->addFieldToTab('Root.Main', $dateField, 'Content');
|
||||
$fields->addFieldToTab('Root.Main', new TextField('Author'), 'Content');
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
```php
|
||||
use Page;
|
||||
|
||||
class ArticlePage extends Page
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getCMSFields()
|
||||
{
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$dateField = new DateField('Date');
|
||||
$dateField->setConfig('showcalendar', true);
|
||||
$fields->addFieldToTab('Root.Main', $dateField, 'Content');
|
||||
$fields->addFieldToTab('Root.Main', new TextField('Author'), 'Content');
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
```
|
||||
|
||||
Let's walk through this method.
|
||||
|
||||
:::php
|
||||
$fields = parent::getCMSFields();
|
||||
```php
|
||||
$fields = parent::getCMSFields();
|
||||
```
|
||||
|
||||
|
||||
Firstly, we get the fields from the parent class; we want to add fields, not replace them. The *$fields* variable
|
||||
returned is a [FieldList](api:SilverStripe\Forms\FieldList) object.
|
||||
|
||||
:::php
|
||||
$fields->addFieldToTab('Root.Main', new TextField('Author'), 'Content');
|
||||
$fields->addFieldToTab('Root.Main', new DateField('Date'), 'Content');
|
||||
```php
|
||||
$fields->addFieldToTab('Root.Main', new TextField('Author'), 'Content');
|
||||
$fields->addFieldToTab('Root.Main', new DateField('Date'), 'Content');
|
||||
```
|
||||
|
||||
|
||||
We can then add our new fields with *addFieldToTab*. The first argument is the tab on which we want to add the field to:
|
||||
|
@ -180,8 +207,9 @@ would create a new tab called "New Tab", and a single "Author" textfield inside.
|
|||
We have added two fields: A simple [TextField](api:SilverStripe\Forms\TextField) and a [DateField](api:SilverStripe\Forms\DateField).
|
||||
There are many more fields available in the default installation, listed in ["form field types"](/developer_guides/forms/field_types/common_subclasses).
|
||||
|
||||
:::php
|
||||
return $fields;
|
||||
```php
|
||||
return $fields;
|
||||
```
|
||||
|
||||
|
||||
Finally, we return the fields to the CMS. If we flush the cache (by adding ?flush=1 at the end of the URL), we will be able to edit the fields in the CMS.
|
||||
|
@ -198,44 +226,52 @@ This makes it confusing and doesn't give the user much help when adding a date.
|
|||
To make the date field a bit more user friendly, you can add a dropdown calendar, set the date format and add a better title. By default,
|
||||
the date field will have the date format defined by your locale.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class ArticlePage extends Page {
|
||||
|
||||
// .....
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$dateField = new DateField('Date', 'Article Date (for example: 20/12/2010)');
|
||||
$dateField->setConfig('showcalendar', true);
|
||||
$dateField->setConfig('dateformat', 'dd/MM/YYYY');
|
||||
|
||||
$fields->addFieldToTab('Root.Main', $dateField, 'Content');
|
||||
$fields->addFieldToTab('Root.Main', new TextField('Author', 'Author Name'), 'Content');
|
||||
```php
|
||||
use Page;
|
||||
|
||||
return $fields;
|
||||
}
|
||||
class ArticlePage extends Page
|
||||
{
|
||||
|
||||
// .....
|
||||
|
||||
public function getCMSFields()
|
||||
{
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$dateField = new DateField('Date', 'Article Date (for example: 20/12/2010)');
|
||||
$dateField->setConfig('showcalendar', true);
|
||||
$dateField->setConfig('dateformat', 'dd/MM/YYYY');
|
||||
|
||||
$fields->addFieldToTab('Root.Main', $dateField, 'Content');
|
||||
$fields->addFieldToTab('Root.Main', new TextField('Author', 'Author Name'), 'Content');
|
||||
|
||||
return $fields;
|
||||
}
|
||||
```
|
||||
|
||||
Let's walk through these changes.
|
||||
|
||||
:::php
|
||||
$dateField = new DateField('Date', 'Article Date (for example: 20/12/2010)');
|
||||
```php
|
||||
$dateField = new DateField('Date', 'Article Date (for example: 20/12/2010)');
|
||||
```
|
||||
|
||||
*$dateField* is declared in order to change the configuration of the DateField.
|
||||
|
||||
:::php
|
||||
$dateField->setConfig('showcalendar', true);
|
||||
```php
|
||||
$dateField->setConfig('showcalendar', true);
|
||||
```
|
||||
|
||||
By enabling *showCalendar* you show a calendar overlay when clicking on the field.
|
||||
|
||||
:::php
|
||||
$dateField->setConfig('dateformat', 'dd/MM/YYYY');
|
||||
```php
|
||||
$dateField->setConfig('dateformat', 'dd/MM/YYYY');
|
||||
```
|
||||
|
||||
*dateFormat* allows you to specify how you wish the date to be entered and displayed in the CMS field. See the [DBDateField](api:SilverStripe\ORM\FieldType\DBDateField) documentation for more configuration options.
|
||||
|
||||
:::php
|
||||
$fields->addFieldToTab('Root.Main', new TextField('Author', 'Author Name'), 'Content');
|
||||
```php
|
||||
$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.
|
||||
|
||||
|
@ -250,22 +286,22 @@ page layout.
|
|||
### ArticlePage Template
|
||||
First, the template for displaying a single article:
|
||||
|
||||
**themes/simple/templates/Layout/ArticlePage.ss**
|
||||
**themes/simple/templates/Layout/ArticlePage.ss**
|
||||
|
||||
|
||||
:::ss
|
||||
<% include SideBar %>
|
||||
<div class="content-container unit size3of4 lastUnit">
|
||||
<article>
|
||||
<h1>$Title</h1>
|
||||
<div class="news-details">
|
||||
<p>Posted on $Date.Nice by $Author</p>
|
||||
</div>
|
||||
<div class="content">$Content</div>
|
||||
</article>
|
||||
$Form
|
||||
</div>
|
||||
|
||||
```ss
|
||||
<% include SideBar %>
|
||||
<div class="content-container unit size3of4 lastUnit">
|
||||
<article>
|
||||
<h1>$Title</h1>
|
||||
<div class="news-details">
|
||||
<p>Posted on $Date.Nice by $Author</p>
|
||||
</div>
|
||||
<div class="content">$Content</div>
|
||||
</article>
|
||||
$Form
|
||||
</div>
|
||||
```
|
||||
|
||||
Most of the code is just like the regular Page.ss, we include an informational div with the date and the author of the Article.
|
||||
|
||||
|
@ -286,24 +322,24 @@ We'll now create a template for the article holder. We want our news section to
|
|||
|
||||
**themes/simple/templates/Layout/ArticleHolder.ss**
|
||||
|
||||
:::ss
|
||||
<% include SideBar %>
|
||||
<div class="content-container unit size3of4 lastUnit">
|
||||
<article>
|
||||
<h1>$Title</h1>
|
||||
$Content
|
||||
<div class="content">$Content</div>
|
||||
</article>
|
||||
<% loop $Children %>
|
||||
<article>
|
||||
<h2><a href="$Link" title="Read more on "{$Title}"">$Title</a></h2>
|
||||
<p>$Content.FirstParagraph</p>
|
||||
<a href="$Link" title="Read more on "{$Title}"">Read more >></a>
|
||||
</article>
|
||||
<% end_loop %>
|
||||
$Form
|
||||
</div>
|
||||
|
||||
```ss
|
||||
<% include SideBar %>
|
||||
<div class="content-container unit size3of4 lastUnit">
|
||||
<article>
|
||||
<h1>$Title</h1>
|
||||
$Content
|
||||
<div class="content">$Content</div>
|
||||
</article>
|
||||
<% loop $Children %>
|
||||
<article>
|
||||
<h2><a href="$Link" title="Read more on "{$Title}"">$Title</a></h2>
|
||||
<p>$Content.FirstParagraph</p>
|
||||
<a href="$Link" title="Read more on "{$Title}"">Read more >></a>
|
||||
</article>
|
||||
<% end_loop %>
|
||||
$Form
|
||||
</div>
|
||||
```
|
||||
|
||||
Here we use the page control *Children*. As the name suggests, this control allows you to iterate over the children of a page. In this case, the children are our news articles. The *$Link* variable will give the address of the article which we can use to create a link, and the *FirstParagraph* function of the [DBHTMLText](api:SilverStripe\ORM\FieldType\DBHTMLText) field gives us a nice summary of the article. The function strips all tags from the paragraph extracted.
|
||||
|
||||
|
@ -320,38 +356,39 @@ Cut the code between "loop Children" in *ArticleHolder.ss** and replace it with
|
|||
|
||||
**themes/simple/templates/Layout/ArticleHolder.ss**
|
||||
|
||||
:::ss
|
||||
...
|
||||
<% loop $Children %>
|
||||
<% include ArticleTeaser %>
|
||||
<% end_loop %>
|
||||
...
|
||||
|
||||
```ss
|
||||
...
|
||||
<% loop $Children %>
|
||||
<% include ArticleTeaser %>
|
||||
<% end_loop %>
|
||||
...
|
||||
```
|
||||
Paste the code that was in ArticleHolder into a new include file called ArticleTeaser.ss:
|
||||
|
||||
**themes/simple/templates/Includes/ArticleTeaser.ss**
|
||||
|
||||
:::ss
|
||||
<article>
|
||||
<h2><a href="$Link" title="Read more on "{$Title}"">$Title</a></h2>
|
||||
<p>$Content.FirstParagraph</p>
|
||||
<a href="$Link" title="Read more on "{$Title}"">Read more >></a>
|
||||
</article>
|
||||
|
||||
```ss
|
||||
<article>
|
||||
<h2><a href="$Link" title="Read more on "{$Title}"">$Title</a></h2>
|
||||
<p>$Content.FirstParagraph</p>
|
||||
<a href="$Link" title="Read more on "{$Title}"">Read more >></a>
|
||||
</article>
|
||||
```
|
||||
|
||||
### Changing the icons of pages in the CMS
|
||||
|
||||
Now let's make a purely cosmetic change that nevertheless helps to make the information presented in the CMS clearer.
|
||||
Add the following field to the *ArticleHolder* and *ArticlePage* classes:
|
||||
|
||||
:::php
|
||||
private static $icon = "cms/images/treeicons/news-file.gif";
|
||||
|
||||
```php
|
||||
private static $icon = "cms/images/treeicons/news-file.gif";
|
||||
```
|
||||
|
||||
And this one to the *HomePage* class:
|
||||
|
||||
:::php
|
||||
private static $icon = "cms/images/treeicons/home-file.png";
|
||||
|
||||
```php
|
||||
private static $icon = "cms/images/treeicons/home-file.png";
|
||||
```
|
||||
|
||||
This will change the icons for the pages in the CMS.
|
||||
|
||||
|
@ -363,30 +400,31 @@ Note: The `news-file` icon may not exist in a default SilverStripe installation.
|
|||
|
||||
## Showing the latest news on the homepage
|
||||
|
||||
It would be nice to greet page visitors with a summary of the latest news when they visit the homepage. This requires a little more code though - the news articles are not direct children of the homepage, so we can't use the *Children* control. We can get the data for news articles by implementing our own function in *HomePage_Controller*.
|
||||
It would be nice to greet page visitors with a summary of the latest news when they visit the homepage. This requires a little more code though - the news articles are not direct children of the homepage, so we can't use the *Children* control. We can get the data for news articles by implementing our own function in *HomePageController*.
|
||||
|
||||
**mysite/code/HomePage.php**
|
||||
|
||||
:::php
|
||||
// ...
|
||||
public function LatestNews($num=5) {
|
||||
$holder = ArticleHolder::get()->First();
|
||||
return ($holder) ? ArticlePage::get()->filter('ParentID', $holder->ID)->sort('Date DESC')->limit($num) : false;
|
||||
}
|
||||
|
||||
```php
|
||||
// ...
|
||||
public function LatestNews($num=5)
|
||||
{
|
||||
$holder = ArticleHolder::get()->First();
|
||||
return ($holder) ? ArticlePage::get()->filter('ParentID', $holder->ID)->sort('Date DESC')->limit($num) : false;
|
||||
}
|
||||
```
|
||||
|
||||
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 and ORM](/developer_guides/model/data_model_and_orm) documentation for details. We can reference this function as a page control in our *HomePage* template:
|
||||
|
||||
**themes/simple/templates/Layout/Homepage.ss**
|
||||
|
||||
:::ss
|
||||
<!-- ... -->
|
||||
<div class="content">$Content</div>
|
||||
</article>
|
||||
<% loop $LatestNews %>
|
||||
<% include ArticleTeaser %>
|
||||
<% end_loop %>
|
||||
|
||||
```ss
|
||||
<!-- ... -->
|
||||
<div class="content">$Content</div>
|
||||
</article>
|
||||
<% loop $LatestNews %>
|
||||
<% include ArticleTeaser %>
|
||||
<% end_loop %>
|
||||
```
|
||||
|
||||
When SilverStripe comes across a variable or page control it doesn't recognize, it first passes control to the controller. If the controller doesn't have a function for the variable or page control, it then passes control to the data object. If it has no matching functions, it then searches its database fields. Failing that it will return nothing.
|
||||
|
||||
|
@ -398,23 +436,24 @@ The controller for a page is only created when page is actually visited, while t
|
|||
|
||||
## Creating a RSS feed
|
||||
|
||||
An RSS feed is something that no news section should be without. SilverStripe makes it easy to create RSS feeds by providing an [RSSFeed](api:SilverStripe\Control\RSS\RSSFeed) class to do all the hard work for us. Add the following in the *ArticleHolder_Controller* class:
|
||||
An RSS feed is something that no news section should be without. SilverStripe makes it easy to create RSS feeds by providing an [RSSFeed](api:SilverStripe\Control\RSS\RSSFeed) class to do all the hard work for us. Add the following in the *ArticleHolderController* class:
|
||||
|
||||
**mysite/code/ArticleHolder.php**
|
||||
|
||||
:::php
|
||||
private static $allowed_actions = array(
|
||||
'rss'
|
||||
);
|
||||
|
||||
public function rss() {
|
||||
$rss = new RSSFeed($this->Children(), $this->Link(), "The coolest news around");
|
||||
return $rss->outputToBrowser();
|
||||
}
|
||||
```php
|
||||
private static $allowed_actions = [
|
||||
'rss'
|
||||
];
|
||||
|
||||
public function rss()
|
||||
{
|
||||
$rss = new RSSFeed($this->Children(), $this->Link(), "The coolest news around");
|
||||
return $rss->outputToBrowser();
|
||||
}
|
||||
```
|
||||
|
||||
Ensure that when you have input the code to implement an RSS feed; flush the webpage afterwards
|
||||
(add ?flush=all on the end of your URL). This is because allowed_actions has changed.
|
||||
(add `?flush=1` on the end of your URL). This is because `allowed_actions` has changed.
|
||||
|
||||
This function creates an RSS feed of all the news articles, and outputs it to the browser. If we go to [http://localhost/your_site_name/news/rss](http://localhost/your_site_name/news/rss) we should see our RSS feed. When there is more to a URL after a page's base URL, "rss" in this case, SilverStripe will call the function with that name on the controller if it exists.
|
||||
|
||||
|
@ -422,16 +461,17 @@ Depending on your browser, you should see something like the picture below. If y
|
|||
|
||||
![](../_images/tutorial2_rss-feed.jpg)
|
||||
|
||||
Now all we need is to let the user know that our RSS feed exists. Add this function to *ArticleHolder_Controller*:
|
||||
Now all we need is to let the user know that our RSS feed exists. Add this function to *ArticleHolderController*:
|
||||
|
||||
**mysite/code/ArticleHolder.php**
|
||||
|
||||
:::php
|
||||
public function init() {
|
||||
RSSFeed::linkToFeed($this->Link() . "rss");
|
||||
parent::init();
|
||||
}
|
||||
|
||||
```php
|
||||
public function init()
|
||||
{
|
||||
RSSFeed::linkToFeed($this->Link() . "rss");
|
||||
parent::init();
|
||||
}
|
||||
```
|
||||
|
||||
This automatically generates a link-tag in the header of our template. The *init* function is then called on the parent class to ensure any initialization the parent would have done if we hadn't overridden the *init* function is still called. Depending on your browser, you can see the RSS feed link in the address bar.
|
||||
|
||||
|
@ -441,45 +481,65 @@ Now that we have a complete news section, let's take a look at the staff section
|
|||
|
||||
**mysite/code/StaffHolder.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class StaffHolder extends Page {
|
||||
private static $db = array();
|
||||
private static $has_one = array();
|
||||
private static $allowed_children = array('StaffPage');
|
||||
}
|
||||
|
||||
class StaffHolder_Controller extends Page_Controller {
|
||||
|
||||
}
|
||||
```php
|
||||
use Page;
|
||||
|
||||
class StaffHolder extends Page
|
||||
{
|
||||
private static $db = [];
|
||||
private static $has_one = [];
|
||||
private static $allowed_children = [StaffPage::class];
|
||||
}
|
||||
```
|
||||
|
||||
**mysite/code/StaffHolderController.php**
|
||||
|
||||
```php
|
||||
use PageController;
|
||||
|
||||
class StaffHolderController extends PageController
|
||||
{
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Nothing here should be new. The *StaffPage* page type is more interesting though. Each staff member has a portrait image. We want to make a permanent connection between this image and the specific *StaffPage* (otherwise we could simply insert an image in the *$Content* field).
|
||||
|
||||
**mysite/code/StaffPage.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class StaffPage extends Page {
|
||||
private static $db = array(
|
||||
);
|
||||
private static $has_one = array(
|
||||
'Photo' => 'Image'
|
||||
);
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$fields->addFieldToTab("Root.Images", new UploadField('Photo'));
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
class StaffPage_Controller extends Page_Controller {
|
||||
}
|
||||
```php
|
||||
use SilverStripe\AssetAdmin\Forms\UploadField;
|
||||
use SilverStripe\Assets\Image;
|
||||
|
||||
class StaffPage extends Page
|
||||
{
|
||||
private static $db = [];
|
||||
|
||||
private static $has_one = [
|
||||
'Photo' => Image::class
|
||||
];
|
||||
|
||||
public function getCMSFields()
|
||||
{
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$fields->addFieldToTab("Root.Images", new UploadField('Photo'));
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**mysite/code/StaffPageController.php**
|
||||
|
||||
```php
|
||||
use PageController;
|
||||
|
||||
class StaffPageController extends PageController
|
||||
{
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Instead of adding our *Image* as a field in *$db*, we have used the *$has_one* array. This is because an *Image* is not a simple database field like all the fields we have seen so far, but has its own database table. By using the *$has_one* array, we create a relationship between the *StaffPage* table and the *Image* table by storing the id of the respective *Image* in the *StaffPage* table.
|
||||
|
||||
|
@ -501,25 +561,26 @@ The staff section templates aren't too difficult to create, thanks to the utilit
|
|||
|
||||
**themes/simple/templates/Layout/StaffHolder.ss**
|
||||
|
||||
:::ss
|
||||
<% include SideBar %>
|
||||
<div class="content-container unit size3of4 lastUnit">
|
||||
<article>
|
||||
<h1>$Title</h1>
|
||||
$Content
|
||||
<div class="content">$Content</div>
|
||||
</article>
|
||||
<% loop $Children %>
|
||||
<article>
|
||||
<h2><a href="$Link" title="Read more on "{$Title}"">$Title</a></h2>
|
||||
$Photo.ScaleWidth(150)
|
||||
<p>$Content.FirstParagraph</p>
|
||||
<a href="$Link" title="Read more on "{$Title}"">Read more >></a>
|
||||
</article>
|
||||
<% end_loop %>
|
||||
$Form
|
||||
</div>
|
||||
```ss
|
||||
<% include SideBar %>
|
||||
<div class="content-container unit size3of4 lastUnit">
|
||||
<article>
|
||||
<h1>$Title</h1>
|
||||
$Content
|
||||
<div class="content">$Content</div>
|
||||
</article>
|
||||
<% loop $Children %>
|
||||
<article>
|
||||
<h2><a href="$Link" title="Read more on "{$Title}"">$Title</a></h2>
|
||||
$Photo.ScaleWidth(150)
|
||||
<p>$Content.FirstParagraph</p>
|
||||
<a href="$Link" title="Read more on "{$Title}"">Read more >></a>
|
||||
</article>
|
||||
<% end_loop %>
|
||||
$Form
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
This template is very similar to the *ArticleHolder* template. The *ScaleWidth* method of the [Image](api:SilverStripe\Assets\Image) class
|
||||
will resize the image before sending it to the browser. The resized image is cached, so the server doesn't have to
|
||||
|
@ -531,17 +592,18 @@ The *StaffPage* template is also very straight forward.
|
|||
|
||||
**themes/simple/templates/Layout/StaffPage.ss**
|
||||
|
||||
:::ss
|
||||
<% include SideBar %>
|
||||
<div class="content-container unit size3of4 lastUnit">
|
||||
<article>
|
||||
<h1>$Title</h1>
|
||||
<div class="content">
|
||||
$Photo.ScaleWidth(433)
|
||||
$Content</div>
|
||||
</article>
|
||||
$Form
|
||||
</div>
|
||||
```ss
|
||||
<% include SideBar %>
|
||||
<div class="content-container unit size3of4 lastUnit">
|
||||
<article>
|
||||
<h1>$Title</h1>
|
||||
<div class="content">
|
||||
$Photo.ScaleWidth(433)
|
||||
$Content</div>
|
||||
</article>
|
||||
$Form
|
||||
</div>
|
||||
```
|
||||
|
||||
Here we use the *ScaleWidth* method to get a different sized image from the same source image. You should now have
|
||||
a complete staff section.
|
||||
|
|
|
@ -32,7 +32,7 @@ use SilverStripe\Forms\TextField;
|
|||
|
||||
class HomePageController extends PageController
|
||||
{
|
||||
private static $allowed_actions = array('BrowserPollForm');
|
||||
private static $allowed_actions = ['BrowserPollForm'];
|
||||
|
||||
// ...
|
||||
|
||||
|
@ -41,14 +41,14 @@ class HomePageController extends PageController
|
|||
// Create fields
|
||||
$fields = new FieldList(
|
||||
new TextField('Name'),
|
||||
new OptionsetField('Browser', 'Your Favourite Browser', array(
|
||||
new OptionsetField('Browser', 'Your Favourite Browser', [
|
||||
'Firefox' => 'Firefox',
|
||||
'Chrome' => 'Chrome',
|
||||
'Internet Explorer' => 'Internet Explorer',
|
||||
'Safari' => 'Safari',
|
||||
'Opera' => 'Opera',
|
||||
'Lynx' => 'Lynx'
|
||||
))
|
||||
])
|
||||
);
|
||||
|
||||
// Create actions
|
||||
|
@ -62,6 +62,7 @@ class HomePageController extends PageController
|
|||
// ...
|
||||
}
|
||||
// ...
|
||||
|
||||
```
|
||||
|
||||
Let's step through this code.
|
||||
|
@ -70,15 +71,16 @@ Let's step through this code.
|
|||
// Create fields
|
||||
$fields = new FieldList(
|
||||
new TextField('Name'),
|
||||
new OptionsetField('Browser', 'Your Favourite Browser', array(
|
||||
new OptionsetField('Browser', 'Your Favourite Browser', [
|
||||
'Firefox' => 'Firefox',
|
||||
'Chrome' => 'Chrome',
|
||||
'Internet Explorer' => 'Internet Explorer',
|
||||
'Safari' => 'Safari',
|
||||
'Opera' => 'Opera',
|
||||
'Lynx' => 'Lynx'
|
||||
))
|
||||
])
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
First we create our form fields.
|
||||
|
@ -192,17 +194,16 @@ If you recall, in the [second tutorial](/tutorials/extending_a_basic_site) we sa
|
|||
**mysite/code/BrowserPollSubmission.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class BrowserPollSubmission extends DataObject
|
||||
{
|
||||
private static $db = array(
|
||||
private static $db = [
|
||||
'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 *HomePageController*:
|
||||
|
||||
|
@ -215,7 +216,8 @@ use PageController;
|
|||
class HomePageController extends PageController
|
||||
{
|
||||
// ...
|
||||
public function doBrowserPoll($data, $form) {
|
||||
public function doBrowserPoll($data, $form)
|
||||
{
|
||||
$submission = new BrowserPollSubmission();
|
||||
$form->saveInto($submission);
|
||||
$submission->write();
|
||||
|
@ -265,6 +267,9 @@ We can do this using a session variable. The [Session](api:SilverStripe\Control\
|
|||
**mysite/code/HomePageController.php**
|
||||
|
||||
```php
|
||||
use SilverStripe\Control\Session;
|
||||
use PageController;
|
||||
|
||||
// ...
|
||||
class HomePageController extends PageController
|
||||
{
|
||||
|
@ -274,7 +279,7 @@ class HomePageController extends PageController
|
|||
$submission = new BrowserPollSubmission();
|
||||
$form->saveInto($submission);
|
||||
$submission->write();
|
||||
Session::set('BrowserPollVoted', true);
|
||||
$this->getRequest()->getSession()->set('BrowserPollVoted', true);
|
||||
return $this->redirectBack();
|
||||
}
|
||||
}
|
||||
|
@ -284,13 +289,15 @@ Then we simply need to check if the session variable has been set in 'BrowserPol
|
|||
it is.
|
||||
|
||||
```php
|
||||
use PageController;
|
||||
|
||||
// ...
|
||||
class HomePageController extends PageController
|
||||
{
|
||||
// ...
|
||||
public function BrowserPollForm()
|
||||
{
|
||||
if (SilverStripe\Control\Session::get('BrowserPollVoted')) {
|
||||
if ($this->getRequest()->getSession()->get('BrowserPollVoted')) {
|
||||
return false;
|
||||
}
|
||||
// ...
|
||||
|
@ -324,13 +331,14 @@ public function BrowserPollResults()
|
|||
$list = new ArrayList();
|
||||
foreach($submissions->groupBy('Browser') as $browserName => $browserSubmissions) {
|
||||
$percentage = (int) ($browserSubmissions->Count() / $total * 100);
|
||||
$list->push(new ArrayData(array(
|
||||
$list->push(new ArrayData([
|
||||
'Browser' => $browserName,
|
||||
'Percentage' => $percentage
|
||||
)));
|
||||
]));
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
```
|
||||
This code introduces a few new concepts, so let's step through it.
|
||||
|
||||
|
@ -349,11 +357,12 @@ We get the total number of submissions, which is needed to calculate the percent
|
|||
$list = new ArrayList();
|
||||
foreach ($submissions->groupBy('Browser') as $browserName => $browserSubmissions) {
|
||||
$percentage = (int) ($browserSubmissions->Count() / $total * 100);
|
||||
$list->push(new ArrayData(array(
|
||||
$list->push(new ArrayData([
|
||||
'Browser' => $browserName,
|
||||
'Percentage' => $percentage
|
||||
)));
|
||||
]));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Now we create an empty [ArrayList](api:SilverStripe\ORM\ArrayList) to hold the data we'll pass to the template. Its similar to [DataList](api:SilverStripe\ORM\DataList), but can hold arbitrary objects rather than just DataObject` instances. Then we iterate over the 'Browser' submissions field.
|
||||
|
|
|
@ -18,8 +18,9 @@ We are going to add a search box on the top of the page. When a user types somet
|
|||
To enable the search engine you need to include the following code in your `mysite/_config.php` file.
|
||||
This will enable fulltext search on page content as well as names of all files in the `/assets` folder.
|
||||
|
||||
:::php
|
||||
FulltextSearchable::enable();
|
||||
```php
|
||||
FulltextSearchable::enable();
|
||||
```
|
||||
|
||||
After including that in your `_config.php` you will need to rebuild the database by visiting [http://localhost/your_site_name/dev/build](http://localhost/your_site_name/dev/build) in your web browser (replace localhost/your_site_name with a domain if applicable). This will add fulltext search columns.
|
||||
|
||||
|
@ -34,15 +35,16 @@ To add the search form, we can add `$SearchForm` anywhere in our templates. In t
|
|||
|
||||
**themes/simple/templates/Includes/Header.ss**
|
||||
|
||||
:::ss
|
||||
...
|
||||
<% if $SearchForm %>
|
||||
<span class="search-dropdown-icon">L</span>
|
||||
<div class="search-bar">
|
||||
$SearchForm
|
||||
</div>
|
||||
<% end_if %>
|
||||
<% include Navigation %>
|
||||
```ss
|
||||
...
|
||||
<% if $SearchForm %>
|
||||
<span class="search-dropdown-icon">L</span>
|
||||
<div class="search-bar">
|
||||
$SearchForm
|
||||
</div>
|
||||
<% end_if %>
|
||||
<% include Navigation %>
|
||||
```
|
||||
|
||||
This displays as:
|
||||
|
||||
|
@ -55,20 +57,25 @@ is applied via `FulltextSearchable::enable()`
|
|||
|
||||
**cms/code/search/ContentControllerSearchExtension.php**
|
||||
|
||||
:::php
|
||||
class ContentControllerSearchExtension extends Extension {
|
||||
...
|
||||
|
||||
function results($data, $form, $request) {
|
||||
$data = array(
|
||||
'Results' => $form->getResults(),
|
||||
'Query' => $form->getSearchQuery(),
|
||||
'Title' => _t('SearchForm.SearchResults', 'Search Results')
|
||||
);
|
||||
return $this->owner->customise($data)->renderWith(array('Page_results', 'Page'));
|
||||
}
|
||||
}
|
||||
```php
|
||||
use SilverStripe\Core\Extension;
|
||||
|
||||
class ContentControllerSearchExtension extends Extension
|
||||
{
|
||||
...
|
||||
|
||||
public function results($data, $form, $request)
|
||||
{
|
||||
$data = [
|
||||
'Results' => $form->getResults(),
|
||||
'Query' => $form->getSearchQuery(),
|
||||
'Title' => _t('SearchForm.SearchResults', 'Search Results')
|
||||
];
|
||||
return $this->owner->customise($data)->renderWith(['Page_results', 'Page']);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
|
@ -97,57 +104,58 @@ class.
|
|||
|
||||
*themes/simple/templates/Layout/Page_results.ss*
|
||||
|
||||
:::ss
|
||||
<div id="Content" class="searchResults">
|
||||
<h1>$Title</h1>
|
||||
|
||||
<% if $Query %>
|
||||
<p class="searchQuery"><strong>You searched for "{$Query}"</strong></p>
|
||||
<% end_if %>
|
||||
|
||||
<% if $Results %>
|
||||
<ul id="SearchResults">
|
||||
<% loop $Results %>
|
||||
<li>
|
||||
<a class="searchResultHeader" href="$Link">
|
||||
<% if $MenuTitle %>
|
||||
$MenuTitle
|
||||
<% else %>
|
||||
$Title
|
||||
<% end_if %>
|
||||
</a>
|
||||
<p>$Content.LimitWordCountXML</p>
|
||||
<a class="readMoreLink" href="$Link"
|
||||
title="Read more about "{$Title}""
|
||||
>Read more about "{$Title}"...</a>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p>Sorry, your search query did not return any results.</p>
|
||||
<% end_if %>
|
||||
|
||||
<% if $Results.MoreThanOnePage %>
|
||||
<div id="PageNumbers">
|
||||
<% if $Results.NotLastPage %>
|
||||
<a class="next" href="$Results.NextLink" title="View the next page">Next</a>
|
||||
<% end_if %>
|
||||
<% if $Results.NotFirstPage %>
|
||||
<a class="prev" href="$Results.PrevLink" title="View the previous page">Prev</a>
|
||||
<% end_if %>
|
||||
<span>
|
||||
<% loop $Results.Pages %>
|
||||
<% if $CurrentBool %>
|
||||
$PageNum
|
||||
<% else %>
|
||||
<a href="$Link" title="View page number $PageNum">$PageNum</a>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
</span>
|
||||
<p>Page $Results.CurrentPage of $Results.TotalPages</p>
|
||||
</div>
|
||||
<% end_if %>
|
||||
</div>
|
||||
```ss
|
||||
<div id="Content" class="searchResults">
|
||||
<h1>$Title</h1>
|
||||
|
||||
<% if $Query %>
|
||||
<p class="searchQuery"><strong>You searched for "{$Query}"</strong></p>
|
||||
<% end_if %>
|
||||
|
||||
<% if $Results %>
|
||||
<ul id="SearchResults">
|
||||
<% loop $Results %>
|
||||
<li>
|
||||
<a class="searchResultHeader" href="$Link">
|
||||
<% if $MenuTitle %>
|
||||
$MenuTitle
|
||||
<% else %>
|
||||
$Title
|
||||
<% end_if %>
|
||||
</a>
|
||||
<p>$Content.LimitWordCountXML</p>
|
||||
<a class="readMoreLink" href="$Link"
|
||||
title="Read more about "{$Title}""
|
||||
>Read more about "{$Title}"...</a>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p>Sorry, your search query did not return any results.</p>
|
||||
<% end_if %>
|
||||
|
||||
<% if $Results.MoreThanOnePage %>
|
||||
<div id="PageNumbers">
|
||||
<% if $Results.NotLastPage %>
|
||||
<a class="next" href="$Results.NextLink" title="View the next page">Next</a>
|
||||
<% end_if %>
|
||||
<% if $Results.NotFirstPage %>
|
||||
<a class="prev" href="$Results.PrevLink" title="View the previous page">Prev</a>
|
||||
<% end_if %>
|
||||
<span>
|
||||
<% loop $Results.Pages %>
|
||||
<% if $CurrentBool %>
|
||||
$PageNum
|
||||
<% else %>
|
||||
<a href="$Link" title="View page number $PageNum">$PageNum</a>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
</span>
|
||||
<p>Page $Results.CurrentPage of $Results.TotalPages</p>
|
||||
</div>
|
||||
<% end_if %>
|
||||
</div>
|
||||
```
|
||||
|
||||
Then finally add ?flush=1 to the URL and you should see the new template.
|
||||
|
||||
|
|
|
@ -39,41 +39,38 @@ Let's create the `Student` and `Project` objects.
|
|||
**mysite/code/Student.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Student extends DataObject
|
||||
{
|
||||
private static $db = array(
|
||||
private static $db = [
|
||||
'Name' => 'Varchar',
|
||||
'University' => 'Varchar',
|
||||
);
|
||||
private static $has_one = array(
|
||||
];
|
||||
private static $has_one = [
|
||||
'Project' => 'Project'
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**mysite/code/Project.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Page;
|
||||
|
||||
class Project extends Page
|
||||
{
|
||||
private static $has_many = array(
|
||||
private static $has_many = [
|
||||
'Students' => 'Student'
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**mysite/code/ProjectController.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
use PageController;
|
||||
|
||||
class ProjectController extends PageController
|
||||
|
@ -116,22 +113,20 @@ The restriction is enforced through the `$allowed_children` directive.
|
|||
**mysite/code/ProjectsHolder.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Page;
|
||||
|
||||
class ProjectsHolder extends Page {
|
||||
private static $allowed_children = array(
|
||||
class ProjectsHolder extends Page
|
||||
{
|
||||
private static $allowed_children = [
|
||||
'Project'
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**mysite/code/ProjectsHolderController.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use PageController;
|
||||
|
||||
class ProjectsHolderController extends PageController
|
||||
|
@ -164,8 +159,6 @@ All customization to fields for a page type are managed through a method called
|
|||
**mysite/code/Project.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Page;
|
||||
use SilverStripe\Forms\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
|
||||
|
@ -182,10 +175,10 @@ class Project extends Page
|
|||
// Set the names and data for our gridfield columns
|
||||
$config
|
||||
->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns')
|
||||
->setDisplayFields(array(
|
||||
->setDisplayFields([
|
||||
'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
|
||||
|
@ -198,6 +191,7 @@ class Project extends Page
|
|||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This creates a tabular field, which lists related student records, one row at a time.
|
||||
|
@ -246,35 +240,33 @@ The first step is to create the `Mentor` object and set the relation with the `P
|
|||
**mysite/code/Mentor.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Mentor extends DataObject
|
||||
{
|
||||
private static $db = array(
|
||||
private static $db = [
|
||||
'Name' => 'Varchar',
|
||||
);
|
||||
private static $belongs_many_many = array(
|
||||
];
|
||||
private static $belongs_many_many = [
|
||||
'Projects' => 'Project'
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**mysite/code/Project.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Page;
|
||||
|
||||
class Project extends Page
|
||||
{
|
||||
// ...
|
||||
private static $many_many = array(
|
||||
private static $many_many = [
|
||||
'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"
|
||||
|
@ -288,8 +280,6 @@ to configure it a bit differently.
|
|||
**mysite/code/Project.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Page;
|
||||
use SilverStripe\Forms\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
|
||||
|
|
|
@ -19,19 +19,21 @@ Let's look at a simple example:
|
|||
|
||||
**mysite/code/Player.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject {
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
private static $db = array(
|
||||
'PlayerNumber' => 'Int',
|
||||
'FirstName' => 'Varchar(255)',
|
||||
'LastName' => 'Text',
|
||||
'Birthday' => 'Date'
|
||||
);
|
||||
}
|
||||
private static $db = [
|
||||
'PlayerNumber' => 'Int',
|
||||
'FirstName' => 'Varchar(255)',
|
||||
'LastName' => 'Text',
|
||||
'Birthday' => 'Date'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This `Player` class definition will create a database table `Player` with columns for `PlayerNumber`, `FirstName` and
|
||||
so on. After writing this class, we need to regenerate the database schema.
|
||||
|
@ -76,18 +78,21 @@ system. Instead, it will generate a new `ID` by adding 1 to the current maximum
|
|||
|
||||
**mysite/code/Player.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject {
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
private static $db = array(
|
||||
'PlayerNumber' => 'Int',
|
||||
'FirstName' => 'Varchar(255)',
|
||||
'LastName' => 'Text',
|
||||
'Birthday' => 'Date'
|
||||
);
|
||||
}
|
||||
private static $db = [
|
||||
'PlayerNumber' => 'Int',
|
||||
'FirstName' => 'Varchar(255)',
|
||||
'LastName' => 'Text',
|
||||
'Birthday' => 'Date'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Generates the following `SQL`.
|
||||
|
||||
|
@ -109,13 +114,15 @@ Generates the following `SQL`.
|
|||
|
||||
A new instance of a [DataObject](api:SilverStripe\ORM\DataObject) can be created using the `new` syntax.
|
||||
|
||||
:::php
|
||||
$player = new Player();
|
||||
```php
|
||||
$player = new Player();
|
||||
```
|
||||
|
||||
Or, a better way is to use the `create` method.
|
||||
|
||||
:::php
|
||||
$player = Player::create();
|
||||
```php
|
||||
$player = Player::create();
|
||||
```
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
Using the `create()` method provides chainability, which can add elegance and brevity to your code, e.g. `Player::create()->write()`. More importantly, however, it will look up the class in the [Injector](../extending/injector) so that the class can be overriden by [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection).
|
||||
|
@ -125,49 +132,55 @@ Using the `create()` method provides chainability, which can add elegance and br
|
|||
Database columns and properties can be set as class properties on the object. The SilverStripe ORM handles the saving
|
||||
of the values through a custom `__set()` method.
|
||||
|
||||
:::php
|
||||
$player->FirstName = "Sam";
|
||||
$player->PlayerNumber = 07;
|
||||
```php
|
||||
$player->FirstName = "Sam";
|
||||
$player->PlayerNumber = 07;
|
||||
```
|
||||
|
||||
To save the `DataObject` to the database, use the `write()` method. The first time `write()` is called, an `ID` will be
|
||||
set.
|
||||
|
||||
:::php
|
||||
$player->write();
|
||||
```php
|
||||
$player->write();
|
||||
```
|
||||
|
||||
For convenience, the `write()` method returns the record's ID. This is particularly useful when creating new records.
|
||||
|
||||
:::php
|
||||
$player = Player::create();
|
||||
$id = $player->write();
|
||||
```php
|
||||
$player = Player::create();
|
||||
$id = $player->write();
|
||||
```
|
||||
|
||||
## Querying Data
|
||||
|
||||
With the `Player` class defined we can query our data using the `ORM` or Object-Relational Model. The `ORM` provides
|
||||
shortcuts and methods for fetching, sorting and filtering data from our database.
|
||||
|
||||
:::php
|
||||
$players = Player::get();
|
||||
// returns a `DataList` containing all the `Player` objects.
|
||||
```php
|
||||
$players = Player::get();
|
||||
// returns a `DataList` containing all the `Player` objects.
|
||||
|
||||
$player = Player::get()->byID(2);
|
||||
// returns a single `Player` object instance that has the ID of 2.
|
||||
$player = Player::get()->byID(2);
|
||||
// returns a single `Player` object instance that has the ID of 2.
|
||||
|
||||
echo $player->ID;
|
||||
// returns the players 'ID' column value
|
||||
echo $player->ID;
|
||||
// returns the players 'ID' column value
|
||||
|
||||
echo $player->dbObject('LastEdited')->Ago();
|
||||
// calls the `Ago` method on the `LastEdited` property.
|
||||
echo $player->dbObject('LastEdited')->Ago();
|
||||
// calls the `Ago` method on the `LastEdited` property.
|
||||
```
|
||||
|
||||
The `ORM` uses a "fluent" syntax, where you specify a query by chaining together different methods. Two common methods
|
||||
are `filter()` and `sort()`:
|
||||
|
||||
:::php
|
||||
$members = Player::get()->filter(array(
|
||||
'FirstName' => 'Sam'
|
||||
))->sort('Surname');
|
||||
```php
|
||||
$members = Player::get()->filter([
|
||||
'FirstName' => 'Sam'
|
||||
])->sort('Surname');
|
||||
|
||||
// returns a `DataList` containing all the `Player` records that have the `FirstName` of 'Sam'
|
||||
// returns a `DataList` containing all the `Player` records that have the `FirstName` of 'Sam'
|
||||
|
||||
```
|
||||
|
||||
<div class="info" markdown="1">
|
||||
Provided `filter` values are automatically escaped and do not require any escaping.
|
||||
|
@ -180,47 +193,52 @@ The `ORM` doesn't actually execute the [SQLSelect](api:SilverStripe\ORM\Queries\
|
|||
It's smart enough to generate a single efficient query at the last moment in time without needing to post-process the
|
||||
result set in PHP. In `MySQL` the query generated by the ORM may look something like this
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName' => 'Sam'
|
||||
));
|
||||
```php
|
||||
$players = Player::get()->filter([
|
||||
'FirstName' => 'Sam'
|
||||
]);
|
||||
|
||||
$players = $players->sort('Surname');
|
||||
$players = $players->sort('Surname');
|
||||
|
||||
// executes the following single query
|
||||
// SELECT * FROM Player WHERE FirstName = 'Sam' ORDER BY Surname
|
||||
// executes the following single query
|
||||
// SELECT * FROM Player WHERE FirstName = 'Sam' ORDER BY Surname
|
||||
|
||||
```
|
||||
|
||||
This also means that getting the count of a list of objects will be done with a single, efficient query.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName' => 'Sam'
|
||||
))->sort('Surname');
|
||||
|
||||
// This will create an single SELECT COUNT query
|
||||
// SELECT COUNT(*) FROM Player WHERE FirstName = 'Sam'
|
||||
echo $players->Count();
|
||||
|
||||
```php
|
||||
$players = Player::get()->filter([
|
||||
'FirstName' => 'Sam'
|
||||
])->sort('Surname');
|
||||
|
||||
// This will create an single SELECT COUNT query
|
||||
// SELECT COUNT(*) FROM Player WHERE FirstName = 'Sam'
|
||||
echo $players->Count();
|
||||
|
||||
```
|
||||
|
||||
## Looping over a list of objects
|
||||
|
||||
`get()` returns a `DataList` instance. You can loop over `DataList` instances in both PHP and templates.
|
||||
|
||||
:::php
|
||||
$players = Player::get();
|
||||
```php
|
||||
$players = Player::get();
|
||||
|
||||
foreach($players as $player) {
|
||||
echo $player->FirstName;
|
||||
}
|
||||
foreach($players as $player) {
|
||||
echo $player->FirstName;
|
||||
}
|
||||
```
|
||||
|
||||
Notice that we can step into the loop safely without having to check if `$players` exists. The `get()` call is robust, and will at worst return an empty `DataList` object. If you do want to check if the query returned any records, you can use the `exists()` method, e.g.
|
||||
|
||||
:::php
|
||||
$players = Player::get();
|
||||
```php
|
||||
$players = Player::get();
|
||||
|
||||
if($players->exists()) {
|
||||
// do something here
|
||||
}
|
||||
if($players->exists()) {
|
||||
// do something here
|
||||
}
|
||||
```
|
||||
|
||||
See the [Lists](lists) documentation for more information on dealing with [SS_List](api:SilverStripe\ORM\SS_List) instances.
|
||||
|
||||
|
@ -229,60 +247,68 @@ See the [Lists](lists) documentation for more information on dealing with [SS_Li
|
|||
There are a couple of ways of getting a single DataObject from the ORM. If you know the ID number of the object, you
|
||||
can use `byID($id)`:
|
||||
|
||||
:::php
|
||||
$player = Player::get()->byID(5);
|
||||
```php
|
||||
$player = Player::get()->byID(5);
|
||||
```
|
||||
|
||||
`get()` returns a [DataList](api:SilverStripe\ORM\DataList) instance. You can use operations on that to get back a single record.
|
||||
|
||||
:::php
|
||||
$players = Player::get();
|
||||
```php
|
||||
$players = Player::get();
|
||||
|
||||
$first = $players->first();
|
||||
$last = $players->last();
|
||||
$first = $players->first();
|
||||
$last = $players->last();
|
||||
```
|
||||
|
||||
## Sorting
|
||||
|
||||
If would like to sort the list by `FirstName` in a ascending way (from A to Z).
|
||||
|
||||
:::php
|
||||
// Sort can either be Ascending (ASC) or Descending (DESC)
|
||||
$players = Player::get()->sort('FirstName', 'ASC');
|
||||
```php
|
||||
// Sort can either be Ascending (ASC) or Descending (DESC)
|
||||
$players = Player::get()->sort('FirstName', 'ASC');
|
||||
|
||||
// Ascending is implied
|
||||
$players = Player::get()->sort('FirstName');
|
||||
// Ascending is implied
|
||||
$players = Player::get()->sort('FirstName');
|
||||
```
|
||||
|
||||
To reverse the sort
|
||||
|
||||
:::php
|
||||
$players = Player::get()->sort('FirstName', 'DESC');
|
||||
```php
|
||||
$players = Player::get()->sort('FirstName', 'DESC');
|
||||
|
||||
// or..
|
||||
$players = Player::get()->sort('FirstName', 'ASC')->reverse();
|
||||
// or..
|
||||
$players = Player::get()->sort('FirstName', 'ASC')->reverse();
|
||||
```
|
||||
|
||||
However you might have several entries with the same `FirstName` and would like to sort them by `FirstName` and
|
||||
`LastName`
|
||||
|
||||
:::php
|
||||
$players = Players::get()->sort(array(
|
||||
'FirstName' => 'ASC',
|
||||
'LastName'=>'ASC'
|
||||
));
|
||||
```php
|
||||
$players = Players::get()->sort([
|
||||
'FirstName' => 'ASC',
|
||||
'LastName'=>'ASC'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
You can also sort randomly. Using the `DB` class, you can get the random sort method per database type.
|
||||
|
||||
:::php
|
||||
$random = DB::get_conn()->random();
|
||||
$players = Player::get()->sort($random)
|
||||
|
||||
```php
|
||||
$random = DB::get_conn()->random();
|
||||
$players = Player::get()->sort($random)
|
||||
```
|
||||
|
||||
## Filtering Results
|
||||
|
||||
The `filter()` method filters the list of objects that gets returned.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName' => 'Sam'
|
||||
));
|
||||
```php
|
||||
$players = Player::get()->filter([
|
||||
'FirstName' => 'Sam'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
Each element of the array specifies a filter. You can specify as many filters as you like, and they **all** must be
|
||||
true for the record to be included in the result.
|
||||
|
@ -292,69 +318,82 @@ value that you want to filter to.
|
|||
|
||||
So, this would return only those players called "Sam Minnée".
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName' => 'Sam',
|
||||
'LastName' => 'Minnée',
|
||||
));
|
||||
```php
|
||||
$players = Player::get()->filter([
|
||||
'FirstName' => 'Sam',
|
||||
'LastName' => 'Minnée',
|
||||
]);
|
||||
|
||||
// SELECT * FROM Player WHERE FirstName = 'Sam' AND LastName = 'Minnée'
|
||||
// SELECT * FROM Player WHERE FirstName = 'Sam' AND LastName = 'Minnée'
|
||||
|
||||
```
|
||||
|
||||
There is also a shorthand way of getting Players with the FirstName of Sam.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter('FirstName', 'Sam');
|
||||
```php
|
||||
$players = Player::get()->filter('FirstName', 'Sam');
|
||||
```
|
||||
|
||||
Or if you want to find both Sam and Sig.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(
|
||||
'FirstName', array('Sam', 'Sig')
|
||||
);
|
||||
```php
|
||||
$players = Player::get()->filter(
|
||||
'FirstName', ['Sam', 'Sig']
|
||||
);
|
||||
|
||||
// SELECT * FROM Player WHERE FirstName IN ('Sam', 'Sig')
|
||||
// SELECT * FROM Player WHERE FirstName IN ('Sam', 'Sig')
|
||||
|
||||
```
|
||||
|
||||
You can use [SearchFilters](searchfilters) to add additional behavior to your `filter` command rather than an
|
||||
exact match.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName:StartsWith' => 'S'
|
||||
'PlayerNumber:GreaterThan' => '10'
|
||||
));
|
||||
```php
|
||||
$players = Player::get()->filter([
|
||||
'FirstName:StartsWith' => 'S'
|
||||
'PlayerNumber:GreaterThan' => '10'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
### filterAny
|
||||
|
||||
Use the `filterAny()` method to match multiple criteria non-exclusively (with an "OR" disjunctive),
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filterAny(array(
|
||||
'FirstName' => 'Sam',
|
||||
'Age' => 17,
|
||||
));
|
||||
```php
|
||||
$players = Player::get()->filterAny([
|
||||
'FirstName' => 'Sam',
|
||||
'Age' => 17,
|
||||
]);
|
||||
|
||||
// SELECT * FROM Player WHERE ("FirstName" = 'Sam' OR "Age" = '17')
|
||||
// SELECT * FROM Player WHERE ("FirstName" = 'Sam' OR "Age" = '17')
|
||||
|
||||
```
|
||||
|
||||
You can combine both conjunctive ("AND") and disjunctive ("OR") statements.
|
||||
|
||||
:::php
|
||||
$players = Player::get()
|
||||
->filter(array(
|
||||
'LastName' => 'Minnée'
|
||||
))
|
||||
->filterAny(array(
|
||||
'FirstName' => 'Sam',
|
||||
'Age' => 17,
|
||||
));
|
||||
// SELECT * FROM Player WHERE ("LastName" = 'Minnée' AND ("FirstName" = 'Sam' OR "Age" = '17'))
|
||||
```php
|
||||
$players = Player::get()
|
||||
->filter([
|
||||
'LastName' => 'Minnée'
|
||||
])
|
||||
->filterAny([
|
||||
'FirstName' => 'Sam',
|
||||
'Age' => 17,
|
||||
]);
|
||||
// SELECT * FROM Player WHERE ("LastName" = 'Minnée' AND ("FirstName" = 'Sam' OR "Age" = '17'))
|
||||
|
||||
```
|
||||
|
||||
You can use [SearchFilters](searchfilters) to add additional behavior to your `filterAny` command.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filterAny(array(
|
||||
'FirstName:StartsWith' => 'S'
|
||||
'PlayerNumber:GreaterThan' => '10'
|
||||
));
|
||||
```php
|
||||
$players = Player::get()->filterAny([
|
||||
'FirstName:StartsWith' => 'S'
|
||||
'PlayerNumber:GreaterThan' => '10'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
### Filtering by null values
|
||||
|
||||
|
@ -366,46 +405,49 @@ checks to ensure that exclusion filters behave predictably.
|
|||
For instance, the below code will select only values that do not match the given value, including nulls.
|
||||
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter('FirstName:not', 'Sam');
|
||||
// ... WHERE "FirstName" != 'Sam' OR "FirstName" IS NULL
|
||||
// Returns rows with any value (even null) other than Sam
|
||||
|
||||
```php
|
||||
$players = Player::get()->filter('FirstName:not', 'Sam');
|
||||
// ... WHERE "FirstName" != 'Sam' OR "FirstName" IS NULL
|
||||
// Returns rows with any value (even null) other than Sam
|
||||
```
|
||||
|
||||
If null values should be excluded, include the null in your check.
|
||||
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter('FirstName:not', array('Sam', null));
|
||||
// ... WHERE "FirstName" != 'Sam' AND "FirstName" IS NOT NULL
|
||||
// Only returns non-null values for "FirstName" that aren't Sam.
|
||||
// Strictly the IS NOT NULL isn't necessary, but is included for explicitness
|
||||
```php
|
||||
$players = Player::get()->filter('FirstName:not', ['Sam', null]);
|
||||
// ... WHERE "FirstName" != 'Sam' AND "FirstName" IS NOT NULL
|
||||
// Only returns non-null values for "FirstName" that aren't Sam.
|
||||
// Strictly the IS NOT NULL isn't necessary, but is included for explicitness
|
||||
|
||||
```
|
||||
|
||||
It is also often useful to filter by all rows with either empty or null for a given field.
|
||||
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter('FirstName', array(null, ''));
|
||||
// ... WHERE "FirstName" == '' OR "FirstName" IS NULL
|
||||
// Returns rows with FirstName which is either empty or null
|
||||
```php
|
||||
$players = Player::get()->filter('FirstName', [null, '']);
|
||||
// ... WHERE "FirstName" == '' OR "FirstName" IS NULL
|
||||
// Returns rows with FirstName which is either empty or null
|
||||
|
||||
```
|
||||
|
||||
### Filtering by aggregates
|
||||
|
||||
You can use aggregate expressions in your filters, as well.
|
||||
|
||||
```php
|
||||
// get the teams that have more than 10 players
|
||||
$teams = Team::get()->filter('Players.Count():GreaterThan', 10);
|
||||
// get the teams that have more than 10 players
|
||||
$teams = Team::get()->filter('Players.Count():GreaterThan', 10);
|
||||
|
||||
// get the teams with at least one player who has scored 5 or more points
|
||||
$teams = Team::get()->filter('Players.Min(PointsScored):GreaterThanOrEqual', 5);
|
||||
// get the teams with at least one player who has scored 5 or more points
|
||||
$teams = Team::get()->filter('Players.Min(PointsScored):GreaterThanOrEqual', 5);
|
||||
|
||||
// get the teams with players who are averaging more than 15 points
|
||||
$teams = Team::get()->filter('Players.Avg(PointsScored):GreaterThan', 15);
|
||||
// get the teams with players who are averaging more than 15 points
|
||||
$teams = Team::get()->filter('Players.Avg(PointsScored):GreaterThan', 15);
|
||||
|
||||
// get the teams whose players have scored less than 300 points combined
|
||||
$teams = Team::get()->filter('Players.Sum(PointsScored):LessThan', 300);
|
||||
// get the teams whose players have scored less than 300 points combined
|
||||
$teams = Team::get()->filter('Players.Sum(PointsScored):LessThan', 300);
|
||||
```
|
||||
|
||||
### filterByCallback
|
||||
|
@ -424,84 +466,98 @@ for each record, if the callback returns true, this record will be added to the
|
|||
|
||||
The below example will get all `Players` aged over 10.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filterByCallback(function($item, $list) {
|
||||
return ($item->Age() > 10);
|
||||
});
|
||||
```php
|
||||
$players = Player::get()->filterByCallback(function($item, $list) {
|
||||
return ($item->Age() > 10);
|
||||
});
|
||||
```
|
||||
|
||||
### Exclude
|
||||
|
||||
The `exclude()` method is the opposite to the filter in that it removes entries from a list.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->exclude('FirstName', 'Sam');
|
||||
```php
|
||||
$players = Player::get()->exclude('FirstName', 'Sam');
|
||||
|
||||
// SELECT * FROM Player WHERE FirstName != 'Sam'
|
||||
// SELECT * FROM Player WHERE FirstName != 'Sam'
|
||||
```
|
||||
|
||||
Remove both Sam and Sig..
|
||||
|
||||
:::php
|
||||
$players = Player::get()->exclude(
|
||||
'FirstName', array('Sam','Sig')
|
||||
);
|
||||
```php
|
||||
$players = Player::get()->exclude(
|
||||
'FirstName', ['Sam','Sig']
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
`Exclude` follows the same pattern as filter, so for removing only Sam Minnée from the list:
|
||||
|
||||
:::php
|
||||
$players = Player::get()->exclude(array(
|
||||
'FirstName' => 'Sam',
|
||||
'Surname' => 'Minnée',
|
||||
));
|
||||
```php
|
||||
$players = Player::get()->exclude([
|
||||
'FirstName' => 'Sam',
|
||||
'Surname' => 'Minnée',
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
And removing Sig and Sam with that are either age 17 or 43.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->exclude(array(
|
||||
'FirstName' => array('Sam', 'Sig'),
|
||||
'Age' => array(17, 43)
|
||||
));
|
||||
```php
|
||||
$players = Player::get()->exclude([
|
||||
'FirstName' => ['Sam', 'Sig'],
|
||||
'Age' => [17, 43]
|
||||
]);
|
||||
|
||||
// SELECT * FROM Player WHERE ("FirstName" NOT IN ('Sam','Sig) OR "Age" NOT IN ('17', '43'));
|
||||
// SELECT * FROM Player WHERE ("FirstName" NOT IN ('Sam','Sig) OR "Age" NOT IN ('17', '43'));
|
||||
|
||||
```
|
||||
|
||||
You can use [SearchFilters](searchfilters) to add additional behavior to your `exclude` command.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->exclude(array(
|
||||
'FirstName:EndsWith' => 'S'
|
||||
'PlayerNumber:LessThanOrEqual' => '10'
|
||||
));
|
||||
```php
|
||||
$players = Player::get()->exclude([
|
||||
'FirstName:EndsWith' => 'S'
|
||||
'PlayerNumber:LessThanOrEqual' => '10'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
### Subtract
|
||||
|
||||
You can subtract entries from a [DataList](api:SilverStripe\ORM\DataList) by passing in another DataList to `subtract()`
|
||||
|
||||
:::php
|
||||
$sam = Player::get()->filter('FirstName', 'Sam');
|
||||
$players = Player::get();
|
||||
```php
|
||||
$sam = Player::get()->filter('FirstName', 'Sam');
|
||||
$players = Player::get();
|
||||
|
||||
$noSams = $players->subtract($sam);
|
||||
$noSams = $players->subtract($sam);
|
||||
```
|
||||
|
||||
Though for the above example it would probably be easier to use `filter()` and `exclude()`. A better use case could be
|
||||
when you want to find all the members that does not exist in a Group.
|
||||
|
||||
:::php
|
||||
// ... Finding all members that does not belong to $group.
|
||||
$otherMembers = Member::get()->subtract($group->Members());
|
||||
```php
|
||||
// ... Finding all members that does not belong to $group.
|
||||
$otherMembers = Member::get()->subtract($group->Members());
|
||||
```
|
||||
|
||||
### Limit
|
||||
|
||||
You can limit the amount of records returned in a DataList by using the `limit()` method.
|
||||
|
||||
:::php
|
||||
$members = Member::get()->limit(5);
|
||||
|
||||
```php
|
||||
$members = Member::get()->limit(5);
|
||||
```
|
||||
|
||||
`limit()` accepts two arguments, the first being the amount of results you want returned, with an optional second
|
||||
parameter to specify the offset, which allows you to tell the system where to start getting the results from. The
|
||||
offset, if not provided as an argument, will default to 0.
|
||||
|
||||
:::php
|
||||
// Return 10 members with an offset of 4 (starting from the 5th result).
|
||||
$members = Member::get()->sort('Surname')->limit(10, 4);
|
||||
```php
|
||||
// Return 10 members with an offset of 4 (starting from the 5th result).
|
||||
$members = Member::get()->sort('Surname')->limit(10, 4);
|
||||
```
|
||||
|
||||
<div class="alert">
|
||||
Note that the `limit` argument order is different from a MySQL LIMIT clause.
|
||||
|
@ -516,12 +572,13 @@ slashes in table names, it is necessary to provide an alternate mapping.
|
|||
For instance, the below model will be stored in the table name `BannerImage`
|
||||
|
||||
|
||||
:::php
|
||||
namespace SilverStripe\BannerManager;
|
||||
class BannerImage extends \DataObject {
|
||||
private static $table_name = 'BannerImage';
|
||||
}
|
||||
|
||||
```php
|
||||
namespace SilverStripe\BannerManager;
|
||||
class BannerImage extends \DataObject
|
||||
{
|
||||
private static $table_name = 'BannerImage';
|
||||
}
|
||||
```
|
||||
|
||||
Note that any model class which does not explicitly declare a `table_name` config option will have a name
|
||||
automatically generated for them. In the above case, the table name would have been
|
||||
|
@ -550,15 +607,16 @@ For example, if running a query against a particular model, you will need to ens
|
|||
table and column.
|
||||
|
||||
|
||||
:::php
|
||||
public function countDuplicates($model, $fieldToCheck) {
|
||||
$table = DataObject::getSchema()->tableForField($model, $field);
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("\"{$table}\"");
|
||||
$query->setWhere(["\"{$table}\".\"{$field}\"" => $model->$fieldToCheck]);
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
```php
|
||||
public function countDuplicates($model, $fieldToCheck)
|
||||
{
|
||||
$table = DataObject::getSchema()->tableForField($model, $field);
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("\"{$table}\"");
|
||||
$query->setWhere(["\"{$table}\".\"{$field}\"" => $model->$fieldToCheck]);
|
||||
return $query->count();
|
||||
}
|
||||
```
|
||||
|
||||
### Raw SQL
|
||||
|
||||
|
@ -576,8 +634,9 @@ you need it to, you may also consider extending the ORM with new data types or f
|
|||
|
||||
You can specify a WHERE clause fragment (that will be combined with other filters using AND) with the `where()` method:
|
||||
|
||||
:::php
|
||||
$members = Member::get()->where("\"FirstName\" = 'Sam'")
|
||||
```php
|
||||
$members = Member::get()->where("\"FirstName\" = 'Sam'")
|
||||
```
|
||||
|
||||
#### Joining Tables
|
||||
|
||||
|
@ -587,14 +646,15 @@ You can specify a join with the `innerJoin` and `leftJoin` methods. Both of the
|
|||
* The filter clause for the join.
|
||||
* An optional alias.
|
||||
|
||||
:::php
|
||||
// Without an alias
|
||||
$members = Member::get()
|
||||
->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
|
||||
```php
|
||||
// Without an alias
|
||||
$members = Member::get()
|
||||
->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
|
||||
|
||||
$members = Member::get()
|
||||
->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel");
|
||||
```
|
||||
|
||||
$members = Member::get()
|
||||
->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel");
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
Passing a *$join* statement to will filter results further by the JOINs performed against the foreign table. It will
|
||||
**not** return the additionally joined data.
|
||||
|
@ -605,15 +665,18 @@ Passing a *$join* statement to will filter results further by the JOINs performe
|
|||
Define the default values for all the `$db` fields. This example sets the "Status"-column on Player to "Active"
|
||||
whenever a new object is created.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject {
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
private static $defaults = array(
|
||||
"Status" => 'Active',
|
||||
);
|
||||
}
|
||||
private static $defaults = [
|
||||
"Status" => 'Active',
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
Note: Alternatively you can set defaults directly in the database-schema (rather than the object-model). See
|
||||
|
@ -629,42 +692,49 @@ time.
|
|||
|
||||
For example, suppose we have the following set of classes:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
use Page;
|
||||
|
||||
class Page extends SiteTree {
|
||||
class Page extends SiteTree
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
class NewsPage extends Page
|
||||
{
|
||||
|
||||
class NewsPage extends Page {
|
||||
private static $db = [
|
||||
'Summary' => 'Text'
|
||||
];
|
||||
}
|
||||
|
||||
private static $db = array(
|
||||
'Summary' => 'Text'
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The data for the following classes would be stored across the following tables:
|
||||
|
||||
:::yml
|
||||
SiteTree:
|
||||
- ID: Int
|
||||
- ClassName: Enum('SiteTree', 'Page', 'NewsPage')
|
||||
- Created: Datetime
|
||||
- LastEdited: Datetime
|
||||
- Title: Varchar
|
||||
- Content: Text
|
||||
NewsPage:
|
||||
- ID: Int
|
||||
- Summary: Text
|
||||
```yml
|
||||
|
||||
SiteTree:
|
||||
- ID: Int
|
||||
- ClassName: Enum('SiteTree', 'Page', 'NewsPage')
|
||||
- Created: Datetime
|
||||
- LastEdited: Datetime
|
||||
- Title: Varchar
|
||||
- Content: Text
|
||||
NewsPage:
|
||||
- ID: Int
|
||||
- Summary: Text
|
||||
```
|
||||
|
||||
Accessing the data is transparent to the developer.
|
||||
|
||||
:::php
|
||||
$news = NewsPage::get();
|
||||
```php
|
||||
$news = NewsPage::get();
|
||||
|
||||
foreach($news as $article) {
|
||||
echo $article->Title;
|
||||
}
|
||||
foreach($news as $article) {
|
||||
echo $article->Title;
|
||||
}
|
||||
```
|
||||
|
||||
The way the ORM stores the data is this:
|
||||
|
||||
|
|
|
@ -15,49 +15,55 @@ SilverStripe supports a number of relationship types and each relationship type
|
|||
A 1-to-1 relation creates a database-column called "`<relationship-name>`ID", in the example below this would be
|
||||
"TeamID" on the "Player"-table.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Team extends DataObject {
|
||||
class Team extends DataObject
|
||||
{
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
private static $db = [
|
||||
'Title' => 'Varchar'
|
||||
];
|
||||
|
||||
private static $has_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
}
|
||||
private static $has_many = [
|
||||
'Players' => 'Player'
|
||||
];
|
||||
}
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
class Player extends DataObject {
|
||||
private static $has_one = [
|
||||
"Team" => "Team",
|
||||
];
|
||||
}
|
||||
|
||||
private static $has_one = array(
|
||||
"Team" => "Team",
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This defines a relationship called `Team` which links to a `Team` class. The `ORM` handles navigating the relationship
|
||||
and provides a short syntax for accessing the related object.
|
||||
|
||||
At the database level, the `has_one` creates a `TeamID` field on `Player`. A `has_many` field does not impose any database changes. It merely injects a new method into the class to access the related records (in this case, `Players()`)
|
||||
|
||||
:::php
|
||||
$player = Player::get()->byId(1);
|
||||
```php
|
||||
$player = Player::get()->byId(1);
|
||||
|
||||
$team = $player->Team();
|
||||
// returns a 'Team' instance.
|
||||
$team = $player->Team();
|
||||
// returns a 'Team' instance.
|
||||
|
||||
echo $player->Team()->Title;
|
||||
// returns the 'Title' column on the 'Team' or `getTitle` if it exists.
|
||||
echo $player->Team()->Title;
|
||||
// returns the 'Title' column on the 'Team' or `getTitle` if it exists.
|
||||
```
|
||||
|
||||
The relationship can also be navigated in [templates](../templates).
|
||||
|
||||
:::ss
|
||||
<% with $Player %>
|
||||
<% if $Team %>
|
||||
Plays for $Team.Title
|
||||
<% end_if %>
|
||||
<% end_with %>
|
||||
```ss
|
||||
|
||||
<% with $Player %>
|
||||
<% if $Team %>
|
||||
Plays for $Team.Title
|
||||
<% end_if %>
|
||||
<% end_with %>
|
||||
```
|
||||
|
||||
## Polymorphic has_one
|
||||
|
||||
|
@ -70,28 +76,33 @@ with the ID column identifies the object.
|
|||
To specify that a has_one relation is polymorphic set the type to 'DataObject'.
|
||||
Ideally, the associated has_many (or belongs_to) should be specified with dot notation.
|
||||
|
||||
::php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject {
|
||||
private static $has_many = array(
|
||||
"Fans" => "Fan.FanOf"
|
||||
);
|
||||
}
|
||||
class Player extends DataObject
|
||||
{
|
||||
private static $has_many = [
|
||||
"Fans" => "Fan.FanOf"
|
||||
];
|
||||
}
|
||||
class Team extends DataObject
|
||||
{
|
||||
private static $has_many = [
|
||||
"Fans" => "Fan.FanOf"
|
||||
];
|
||||
}
|
||||
|
||||
class Team extends DataObject {
|
||||
private static $has_many = array(
|
||||
"Fans" => "Fan.FanOf"
|
||||
);
|
||||
}
|
||||
// Type of object returned by $fan->FanOf() will vary
|
||||
class Fan extends DataObject
|
||||
{
|
||||
|
||||
// Type of object returned by $fan->FanOf() will vary
|
||||
class Fan extends DataObject {
|
||||
// Generates columns FanOfID and FanOfClass
|
||||
private static $has_one = [
|
||||
"FanOf" => "DataObject"
|
||||
];
|
||||
}
|
||||
|
||||
// Generates columns FanOfID and FanOfClass
|
||||
private static $has_one = array(
|
||||
"FanOf" => "DataObject"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
<div class="warning" markdown='1'>
|
||||
Note: The use of polymorphic relationships can affect query performance, especially
|
||||
|
@ -110,78 +121,84 @@ Please specify a $has_one-relationship on the related child-class as well, in or
|
|||
available on both ends.
|
||||
</div>
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Team extends DataObject {
|
||||
class Team extends DataObject
|
||||
{
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
private static $db = [
|
||||
'Title' => 'Varchar'
|
||||
];
|
||||
|
||||
private static $has_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
}
|
||||
private static $has_many = [
|
||||
'Players' => 'Player'
|
||||
];
|
||||
}
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
class Player extends DataObject {
|
||||
private static $has_one = [
|
||||
"Team" => "Team",
|
||||
];
|
||||
}
|
||||
|
||||
private static $has_one = array(
|
||||
"Team" => "Team",
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Much like the `has_one` relationship, `has_many` can be navigated through the `ORM` as well. The only difference being
|
||||
you will get an instance of [HasManyList](api:SilverStripe\ORM\HasManyList) rather than the object.
|
||||
|
||||
:::php
|
||||
$team = Team::get()->first();
|
||||
```php
|
||||
$team = Team::get()->first();
|
||||
|
||||
echo $team->Players();
|
||||
// [HasManyList]
|
||||
echo $team->Players();
|
||||
// [HasManyList]
|
||||
|
||||
echo $team->Players()->Count();
|
||||
// returns '14';
|
||||
echo $team->Players()->Count();
|
||||
// returns '14';
|
||||
|
||||
foreach($team->Players() as $player) {
|
||||
echo $player->FirstName;
|
||||
}
|
||||
foreach($team->Players() as $player) {
|
||||
echo $player->FirstName;
|
||||
}
|
||||
```
|
||||
|
||||
To specify multiple `$has_many` to the same object you can use dot notation to distinguish them like below:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Person extends DataObject {
|
||||
class Person extends DataObject
|
||||
{
|
||||
|
||||
private static $has_many = array(
|
||||
"Managing" => "Company.Manager",
|
||||
"Cleaning" => "Company.Cleaner",
|
||||
);
|
||||
}
|
||||
|
||||
class Company extends DataObject {
|
||||
private static $has_many = [
|
||||
"Managing" => "Company.Manager",
|
||||
"Cleaning" => "Company.Cleaner",
|
||||
];
|
||||
}
|
||||
class Company extends DataObject
|
||||
{
|
||||
|
||||
private static $has_one = array(
|
||||
"Manager" => "Person",
|
||||
"Cleaner" => "Person"
|
||||
);
|
||||
}
|
||||
private static $has_one = [
|
||||
"Manager" => "Person",
|
||||
"Cleaner" => "Person"
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Multiple `$has_one` relationships are okay if they aren't linking to the same object type. Otherwise, they have to be
|
||||
named.
|
||||
|
||||
If you're using the default scaffolded form fields with multiple `has_one` relationships, you will end up with a CMS field for each relation. If you don't want these you can remove them by their IDs:
|
||||
|
||||
:::php
|
||||
```php
|
||||
public function getCMSFields()
|
||||
{
|
||||
$fields = parent::getCMSFields();
|
||||
$fields->removeByName(array('ManagerID', 'CleanerID'));
|
||||
return $fields;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## belongs_to
|
||||
|
||||
|
@ -193,23 +210,25 @@ declaring the `$belongs_to`.
|
|||
Similarly with `$has_many`, dot notation can be used to explicitly specify the `$has_one` which refers to this relation.
|
||||
This is not mandatory unless the relationship would be otherwise ambiguous.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Team extends DataObject {
|
||||
|
||||
private static $has_one = array(
|
||||
'Coach' => 'Coach'
|
||||
);
|
||||
}
|
||||
|
||||
class Coach extends DataObject {
|
||||
|
||||
private static $belongs_to = array(
|
||||
'Team' => 'Team.Coach'
|
||||
);
|
||||
}
|
||||
class Team extends DataObject
|
||||
{
|
||||
|
||||
private static $has_one = [
|
||||
'Coach' => 'Coach'
|
||||
];
|
||||
}
|
||||
class Coach extends DataObject
|
||||
{
|
||||
|
||||
private static $belongs_to = [
|
||||
'Team' => 'Team.Coach'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## many_many
|
||||
|
||||
|
@ -226,12 +245,12 @@ Much like the `has_one` relationship, `many_many` can be navigated through the `
|
|||
The only difference being you will get an instance of [ManyManyList](api:SilverStripe\ORM\ManyManyList) or
|
||||
[ManyManyThroughList](api:SilverStripe\ORM\ManyManyThroughList) rather than the object.
|
||||
|
||||
:::php
|
||||
$team = Team::get()->byId(1);
|
||||
|
||||
$supporters = $team->Supporters();
|
||||
// returns a 'ManyManyList' instance.
|
||||
```php
|
||||
$team = Team::get()->byId(1);
|
||||
|
||||
$supporters = $team->Supporters();
|
||||
// returns a 'ManyManyList' instance.
|
||||
```
|
||||
|
||||
### Automatic many_many table
|
||||
|
||||
|
@ -243,27 +262,28 @@ Extra fields on the mapping table can be created by declaring a `many_many_extra
|
|||
config to add extra columns.
|
||||
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Team extends DataObject {
|
||||
private static $many_many = [
|
||||
"Supporters" => "Supporter",
|
||||
];
|
||||
private static $many_many_extraFields = [
|
||||
'Supporters' => [
|
||||
'Ranking' => 'Int'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
class Supporter extends DataObject {
|
||||
|
||||
private static $belongs_many_many = [
|
||||
"Supports" => "Team",
|
||||
];
|
||||
}
|
||||
class Team extends DataObject
|
||||
{
|
||||
private static $many_many = [
|
||||
"Supporters" => "Supporter",
|
||||
];
|
||||
private static $many_many_extraFields = [
|
||||
'Supporters' => [
|
||||
'Ranking' => 'Int'
|
||||
]
|
||||
];
|
||||
}
|
||||
class Supporter extends DataObject
|
||||
{
|
||||
|
||||
private static $belongs_many_many = [
|
||||
"Supports" => "Team",
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### many_many through relationship joined on a separate DataObject
|
||||
|
||||
|
@ -287,44 +307,46 @@ or child record.
|
|||
|
||||
The syntax for `belongs_many_many` is unchanged.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Team extends DataObject {
|
||||
private static $many_many = [
|
||||
"Supporters" => [
|
||||
'through' => 'TeamSupporter',
|
||||
'from' => 'Team',
|
||||
'to' => 'Supporter',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
class Supporter extends DataObject {
|
||||
private static $belongs_many_many = [
|
||||
"Supports" => "Team",
|
||||
];
|
||||
}
|
||||
|
||||
class TeamSupporter extends DataObject {
|
||||
private static $db = [
|
||||
'Ranking' => 'Int',
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'Team' => 'Team',
|
||||
'Supporter' => 'Supporter'
|
||||
];
|
||||
}
|
||||
class Team extends DataObject
|
||||
{
|
||||
private static $many_many = [
|
||||
"Supporters" => [
|
||||
'through' => 'TeamSupporter',
|
||||
'from' => 'Team',
|
||||
'to' => 'Supporter',
|
||||
]
|
||||
];
|
||||
}
|
||||
class Supporter extends DataObject
|
||||
{
|
||||
private static $belongs_many_many = [
|
||||
"Supports" => "Team",
|
||||
];
|
||||
}
|
||||
class TeamSupporter extends DataObject
|
||||
{
|
||||
private static $db = [
|
||||
'Ranking' => 'Int',
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'Team' => 'Team',
|
||||
'Supporter' => 'Supporter'
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
In order to filter on the join table during queries, you can use the class name of the joining table
|
||||
for any sql conditions.
|
||||
|
||||
|
||||
:::php
|
||||
$team = Team::get()->byId(1);
|
||||
$supporters = $team->Supporters()->where(['"TeamSupporter"."Ranking"' => 1]);
|
||||
|
||||
```php
|
||||
$team = Team::get()->byId(1);
|
||||
$supporters = $team->Supporters()->where(['"TeamSupporter"."Ranking"' => 1]);
|
||||
```
|
||||
|
||||
Note: ->filter() currently does not support joined fields natively due to the fact that the
|
||||
query for the join table is isolated from the outer query controlled by DataList.
|
||||
|
@ -335,13 +357,14 @@ query for the join table is isolated from the outer query controlled by DataList
|
|||
The relationship can also be navigated in [templates](../templates).
|
||||
The joined record can be accessed via `Join` or `TeamSupporter` property (many_many through only)
|
||||
|
||||
:::ss
|
||||
<% with $Supporter %>
|
||||
<% loop $Supports %>
|
||||
Supports $Title <% if $TeamSupporter %>(rank $TeamSupporter.Ranking)<% end_if %>
|
||||
<% end_if %>
|
||||
<% end_with %>
|
||||
```ss
|
||||
|
||||
<% with $Supporter %>
|
||||
<% loop $Supports %>
|
||||
Supports $Title <% if $TeamSupporter %>(rank $TeamSupporter.Ranking)<% end_if %>
|
||||
<% end_if %>
|
||||
<% end_with %>
|
||||
```
|
||||
|
||||
You can also use `$Join` in place of the join class alias (`$TeamSupporter`), if your template
|
||||
is class-agnostic and doesn't know the type of the join table.
|
||||
|
@ -355,25 +378,27 @@ To specify multiple $many_manys between the same classes, specify use the dot no
|
|||
distinguish them like below:
|
||||
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Category extends DataObject {
|
||||
|
||||
private static $many_many = array(
|
||||
'Products' => 'Product',
|
||||
'FeaturedProducts' => 'Product'
|
||||
);
|
||||
}
|
||||
|
||||
class Product extends DataObject {
|
||||
|
||||
private static $belongs_many_many = array(
|
||||
'Categories' => 'Category.Products',
|
||||
'FeaturedInCategories' => 'Category.FeaturedProducts'
|
||||
);
|
||||
}
|
||||
class Category extends DataObject
|
||||
{
|
||||
|
||||
private static $many_many = [
|
||||
'Products' => 'Product',
|
||||
'FeaturedProducts' => 'Product'
|
||||
];
|
||||
}
|
||||
class Product extends DataObject
|
||||
{
|
||||
|
||||
private static $belongs_many_many = [
|
||||
'Categories' => 'Category.Products',
|
||||
'FeaturedInCategories' => 'Category.FeaturedProducts'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If you're unsure about whether an object should take on `many_many` or `belongs_many_many`,
|
||||
the best way to think about it is that the object where the relationship will be edited
|
||||
|
@ -387,17 +412,17 @@ Adding new items to a relations works the same, regardless if you're editing a *
|
|||
encapsulated by [HasManyList](api:SilverStripe\ORM\HasManyList) and [ManyManyList](api:SilverStripe\ORM\ManyManyList), both of which provide very similar APIs, e.g. an `add()`
|
||||
and `remove()` method.
|
||||
|
||||
:::php
|
||||
$team = Team::get()->byId(1);
|
||||
```php
|
||||
$team = Team::get()->byId(1);
|
||||
|
||||
// create a new supporter
|
||||
$supporter = new Supporter();
|
||||
$supporter->Name = "Foo";
|
||||
$supporter->write();
|
||||
|
||||
// add the supporter.
|
||||
$team->Supporters()->add($supporter);
|
||||
// create a new supporter
|
||||
$supporter = new Supporter();
|
||||
$supporter->Name = "Foo";
|
||||
$supporter->write();
|
||||
|
||||
// add the supporter.
|
||||
$team->Supporters()->add($supporter);
|
||||
```
|
||||
|
||||
## Custom Relations
|
||||
|
||||
|
@ -406,19 +431,23 @@ You can use the ORM to get a filtered result list without writing any SQL. For e
|
|||
|
||||
See [DataObject::$has_many](api:SilverStripe\ORM\DataObject::$has_many) for more info on the described relations.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Team extends DataObject {
|
||||
class Team extends DataObject
|
||||
{
|
||||
|
||||
private static $has_many = array(
|
||||
"Players" => "Player"
|
||||
);
|
||||
|
||||
public function ActivePlayers() {
|
||||
return $this->Players()->filter('Status', 'Active');
|
||||
}
|
||||
}
|
||||
private static $has_many = [
|
||||
"Players" => "Player"
|
||||
];
|
||||
|
||||
public function ActivePlayers()
|
||||
{
|
||||
return $this->Players()->filter('Status', 'Active');
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
Adding new records to a filtered `RelationList` like in the example above doesn't automatically set the filtered
|
||||
|
|
|
@ -11,79 +11,88 @@ modify.
|
|||
|
||||
[SS_List](api:SilverStripe\ORM\SS_List) implements `IteratorAggregate`, allowing you to loop over the instance.
|
||||
|
||||
:::php
|
||||
$members = Member::get();
|
||||
```php
|
||||
$members = Member::get();
|
||||
|
||||
foreach($members as $member) {
|
||||
echo $member->Name;
|
||||
}
|
||||
foreach($members as $member) {
|
||||
echo $member->Name;
|
||||
}
|
||||
```
|
||||
|
||||
Or in the template engine:
|
||||
|
||||
:::ss
|
||||
<% loop $Members %>
|
||||
<!-- -->
|
||||
<% end_loop %>
|
||||
```ss
|
||||
|
||||
<% loop $Members %>
|
||||
<!-- -->
|
||||
<% end_loop %>
|
||||
```
|
||||
|
||||
## Finding an item by value.
|
||||
|
||||
:::php
|
||||
// $list->find($key, $value);
|
||||
```php
|
||||
// $list->find($key, $value);
|
||||
|
||||
//
|
||||
$members = Member::get();
|
||||
|
||||
echo $members->find('ID', 4)->FirstName;
|
||||
// returns 'Sam'
|
||||
//
|
||||
$members = Member::get();
|
||||
|
||||
echo $members->find('ID', 4)->FirstName;
|
||||
// returns 'Sam'
|
||||
```
|
||||
|
||||
## Maps
|
||||
|
||||
A map is an array where the array indexes contain data as well as the values. You can build a map from any list
|
||||
|
||||
:::php
|
||||
$members = Member::get()->map('ID', 'FirstName');
|
||||
|
||||
// $members = array(
|
||||
// 1 => 'Sam'
|
||||
// 2 => 'Sig'
|
||||
// 3 => 'Will'
|
||||
// );
|
||||
|
||||
```php
|
||||
$members = Member::get()->map('ID', 'FirstName');
|
||||
|
||||
// $members = array(
|
||||
// 1 => 'Sam'
|
||||
// 2 => 'Sig'
|
||||
// 3 => 'Will'
|
||||
// );
|
||||
|
||||
```
|
||||
|
||||
This functionality is provided by the [Map](api:SilverStripe\ORM\Map) class, which can be used to build a map around any `SS_List`.
|
||||
|
||||
:::php
|
||||
$members = Member::get();
|
||||
$map = new Map($members, 'ID', 'FirstName');
|
||||
```php
|
||||
$members = Member::get();
|
||||
$map = new Map($members, 'ID', 'FirstName');
|
||||
```
|
||||
|
||||
## Column
|
||||
|
||||
:::php
|
||||
$members = Member::get();
|
||||
```php
|
||||
$members = Member::get();
|
||||
|
||||
echo $members->column('Email');
|
||||
echo $members->column('Email');
|
||||
|
||||
// returns array(
|
||||
// 'sam@silverstripe.com',
|
||||
// 'sig@silverstripe.com',
|
||||
// 'will@silverstripe.com'
|
||||
// );
|
||||
// returns array(
|
||||
// 'sam@silverstripe.com',
|
||||
// 'sig@silverstripe.com',
|
||||
// 'will@silverstripe.com'
|
||||
// );
|
||||
|
||||
```
|
||||
|
||||
## ArrayList
|
||||
|
||||
[ArrayList](api:SilverStripe\ORM\ArrayList) exists to wrap a standard PHP array in the same API as a database backed list.
|
||||
|
||||
:::php
|
||||
$sam = Member::get()->byId(5);
|
||||
$sig = Member::get()->byId(6);
|
||||
```php
|
||||
$sam = Member::get()->byId(5);
|
||||
$sig = Member::get()->byId(6);
|
||||
|
||||
$list = new ArrayList();
|
||||
$list->push($sam);
|
||||
$list->push($sig);
|
||||
$list = new ArrayList();
|
||||
$list->push($sam);
|
||||
$list->push($sig);
|
||||
|
||||
echo $list->Count();
|
||||
// returns '2'
|
||||
echo $list->Count();
|
||||
// returns '2'
|
||||
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
|
|
|
@ -13,18 +13,21 @@ In the `Player` example, we have four database columns each with a different dat
|
|||
|
||||
**mysite/code/Player.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject {
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
private static $db = array(
|
||||
'PlayerNumber' => 'Int',
|
||||
'FirstName' => 'Varchar(255)',
|
||||
'LastName' => 'Text',
|
||||
'Birthday' => 'Date'
|
||||
);
|
||||
}
|
||||
private static $db = [
|
||||
'PlayerNumber' => 'Int',
|
||||
'FirstName' => 'Varchar(255)',
|
||||
'LastName' => 'Text',
|
||||
'Birthday' => 'Date'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Available Types
|
||||
|
||||
|
@ -51,21 +54,24 @@ See the [API documentation](api:SilverStripe\ORM\FieldType\DBField) for a full l
|
|||
For complex default values for newly instantiated objects see [Dynamic Default Values](how_tos/dynamic_default_fields).
|
||||
For simple values you can make use of the `$defaults` array. For example:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Car extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Wheels' => 'Int',
|
||||
'Condition' => 'Enum(array("New","Fair","Junk"))'
|
||||
);
|
||||
|
||||
private static $defaults = array(
|
||||
'Wheels' => 4,
|
||||
'Condition' => 'New'
|
||||
);
|
||||
}
|
||||
class Car extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'Wheels' => 'Int',
|
||||
'Condition' => 'Enum(array("New","Fair","Junk"))'
|
||||
];
|
||||
|
||||
private static $defaults = [
|
||||
'Wheels' => 4,
|
||||
'Condition' => 'New'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Default values for new database columns
|
||||
|
||||
|
@ -81,17 +87,20 @@ For enum values, it's the second parameter.
|
|||
|
||||
For example:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Car extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Wheels' => 'Int(4)',
|
||||
'Condition' => 'Enum(array("New","Fair","Junk"), "New")',
|
||||
'Make' => 'Varchar(["default" => "Honda"]),
|
||||
);
|
||||
}
|
||||
class Car extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'Wheels' => 'Int(4)',
|
||||
'Condition' => 'Enum(array("New","Fair","Junk"), "New")',
|
||||
'Make' => 'Varchar(["default" => "Honda"]),
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Formatting Output
|
||||
|
||||
|
@ -103,49 +112,58 @@ object we can control the formatting and it allows us to call methods defined fr
|
|||
|
||||
**mysite/code/Player.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject {
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
..
|
||||
..
|
||||
|
||||
public function getName() {
|
||||
return DBField::create_field('Varchar', $this->FirstName . ' '. $this->LastName);
|
||||
}
|
||||
}
|
||||
public function getName()
|
||||
{
|
||||
return DBField::create_field('Varchar', $this->FirstName . ' '. $this->LastName);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then we can refer to a new `Name` column on our `Player` instances. In templates we don't need to use the `get` prefix.
|
||||
|
||||
:::php
|
||||
$player = Player::get()->byId(1);
|
||||
```php
|
||||
$player = Player::get()->byId(1);
|
||||
|
||||
echo $player->Name;
|
||||
// returns "Sam Minnée"
|
||||
echo $player->Name;
|
||||
// returns "Sam Minnée"
|
||||
|
||||
echo $player->getName();
|
||||
// returns "Sam Minnée";
|
||||
echo $player->getName();
|
||||
// returns "Sam Minnée";
|
||||
|
||||
echo $player->getName()->LimitCharacters(2);
|
||||
// returns "Sa.."
|
||||
echo $player->getName()->LimitCharacters(2);
|
||||
// returns "Sa.."
|
||||
```
|
||||
|
||||
## Casting
|
||||
|
||||
Rather than manually returning objects from your custom functions. You can use the `$casting` property.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject {
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
private static $casting = array(
|
||||
"Name" => 'Varchar',
|
||||
);
|
||||
|
||||
public function getName() {
|
||||
return $this->FirstName . ' '. $this->LastName;
|
||||
}
|
||||
}
|
||||
private static $casting = [
|
||||
"Name" => 'Varchar',
|
||||
];
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return $this->FirstName . ' '. $this->LastName;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The properties on any SilverStripe object can be type casted automatically, by transforming its scalar value into an
|
||||
instance of the [DBField](api:SilverStripe\ORM\FieldType\DBField) class, providing additional helpers. For example, a string can be cast as a [DBText](api:SilverStripe\ORM\FieldType\DBText)
|
||||
|
@ -154,40 +172,45 @@ type, which has a `FirstSentence()` method to retrieve the first sentence in a l
|
|||
On the most basic level, the class can be used as simple conversion class from one value to another, e.g. to round a
|
||||
number.
|
||||
|
||||
:::php
|
||||
DBField::create_field('Double', 1.23456)->Round(2); // results in 1.23
|
||||
```php
|
||||
DBField::create_field('Double', 1.23456)->Round(2); // results in 1.23
|
||||
```
|
||||
|
||||
Of course that's much more verbose than the equivalent PHP call. The power of [DBField](api:SilverStripe\ORM\FieldType\DBField) comes with its more
|
||||
sophisticated helpers, like showing the time difference to the current date:
|
||||
|
||||
:::php
|
||||
DBField::create_field('Date', '1982-01-01')->TimeDiff(); // shows "30 years ago"
|
||||
```php
|
||||
DBField::create_field('Date', '1982-01-01')->TimeDiff(); // shows "30 years ago"
|
||||
```
|
||||
|
||||
## Casting ViewableData
|
||||
|
||||
Most objects in SilverStripe extend from [ViewableData](api:SilverStripe\View\ViewableData), which means they know how to present themselves in a view
|
||||
context. Through a `$casting` array, arbitrary properties and getters can be casted:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
class MyObject extends ViewableData {
|
||||
|
||||
private static $casting = array(
|
||||
'MyDate' => 'Date'
|
||||
);
|
||||
class MyObject extends ViewableData
|
||||
{
|
||||
|
||||
private static $casting = [
|
||||
'MyDate' => 'Date'
|
||||
];
|
||||
|
||||
public function getMyDate() {
|
||||
return '1982-01-01';
|
||||
}
|
||||
}
|
||||
public function getMyDate()
|
||||
{
|
||||
return '1982-01-01';
|
||||
}
|
||||
}
|
||||
|
||||
$obj = new MyObject;
|
||||
$obj->getMyDate(); // returns string
|
||||
$obj->MyDate; // returns string
|
||||
$obj->obj('MyDate'); // returns object
|
||||
$obj->obj('MyDate')->InPast(); // returns boolean
|
||||
$obj = new MyObject;
|
||||
$obj->getMyDate(); // returns string
|
||||
$obj->MyDate; // returns string
|
||||
$obj->obj('MyDate'); // returns object
|
||||
$obj->obj('MyDate')->InPast(); // returns boolean
|
||||
|
||||
```
|
||||
|
||||
## Casting HTML Text
|
||||
|
||||
|
@ -207,19 +230,22 @@ We can overload the default behavior by making a function called "get`<fieldname
|
|||
The following example will use the result of `getStatus` instead of the 'Status' database column. We can refer to the
|
||||
database column using `dbObject`.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject {
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
private static $db = array(
|
||||
"Status" => "Enum(array('Active', 'Injured', 'Retired'))"
|
||||
);
|
||||
private static $db = [
|
||||
"Status" => "Enum(array('Active', 'Injured', 'Retired'))"
|
||||
];
|
||||
|
||||
public function getStatus() {
|
||||
return (!$this->obj("Birthday")->InPast()) ? "Unborn" : $this->dbObject('Status')->Value();
|
||||
}
|
||||
public function getStatus()
|
||||
{
|
||||
return (!$this->obj("Birthday")->InPast()) ? "Unborn" : $this->dbObject('Status')->Value();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
|
|
|
@ -18,37 +18,42 @@ a `ModelAdmin` record.
|
|||
|
||||
Example: Disallow creation of new players if the currently logged-in player is not a team-manager.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject {
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
private static $has_many = array(
|
||||
"Teams"=>"Team"
|
||||
);
|
||||
|
||||
public function onBeforeWrite() {
|
||||
// check on first write action, aka "database row creation" (ID-property is not set)
|
||||
if(!$this->isInDb()) {
|
||||
$currentPlayer = Security::getCurrentUser();
|
||||
private static $has_many = [
|
||||
"Teams"=>"Team"
|
||||
];
|
||||
|
||||
public function onBeforeWrite()
|
||||
{
|
||||
// check on first write action, aka "database row creation" (ID-property is not set)
|
||||
if(!$this->isInDb()) {
|
||||
$currentPlayer = Security::getCurrentUser();
|
||||
|
||||
if(!$currentPlayer->IsTeamManager()) {
|
||||
user_error('Player-creation not allowed', E_USER_ERROR);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// check on every write action
|
||||
if(!$this->record['TeamID']) {
|
||||
user_error('Cannot save player without a valid team', E_USER_ERROR);
|
||||
exit();
|
||||
}
|
||||
|
||||
// CAUTION: You are required to call the parent-function, otherwise
|
||||
// SilverStripe will not execute the request.
|
||||
parent::onBeforeWrite();
|
||||
}
|
||||
}
|
||||
if(!$currentPlayer->IsTeamManager()) {
|
||||
user_error('Player-creation not allowed', E_USER_ERROR);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// check on every write action
|
||||
if(!$this->record['TeamID']) {
|
||||
user_error('Cannot save player without a valid team', E_USER_ERROR);
|
||||
exit();
|
||||
}
|
||||
|
||||
// CAUTION: You are required to call the parent-function, otherwise
|
||||
// SilverStripe will not execute the request.
|
||||
parent::onBeforeWrite();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## onBeforeDelete
|
||||
|
||||
|
@ -57,26 +62,30 @@ Triggered before executing *delete()* on an existing object.
|
|||
Example: Checking for a specific [permission](permissions) to delete this type of object. It checks if a
|
||||
member is logged in who belongs to a group containing the permission "PLAYER_DELETE".
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $has_many = array(
|
||||
"Teams" => "Team"
|
||||
);
|
||||
|
||||
public function onBeforeDelete() {
|
||||
if(!Permission::check('PLAYER_DELETE')) {
|
||||
Security::permissionFailure($this);
|
||||
exit();
|
||||
}
|
||||
|
||||
parent::onBeforeDelete();
|
||||
}
|
||||
}
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
private static $has_many = [
|
||||
"Teams" => "Team"
|
||||
];
|
||||
|
||||
public function onBeforeDelete()
|
||||
{
|
||||
if(!Permission::check('PLAYER_DELETE')) {
|
||||
Security::permissionFailure($this);
|
||||
exit();
|
||||
}
|
||||
|
||||
parent::onBeforeDelete();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
Note: There are no separate methods for *onBeforeCreate* and *onBeforeUpdate*. Please check `$this->isInDb()` to toggle
|
||||
|
|
|
@ -16,18 +16,20 @@ you can put on field names to change this behavior. These are represented as `Se
|
|||
|
||||
An example of a `SearchFilter` in use:
|
||||
|
||||
:::php
|
||||
// fetch any player that starts with a S
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName:StartsWith' => 'S',
|
||||
'PlayerNumber:GreaterThan' => '10'
|
||||
));
|
||||
```php
|
||||
// fetch any player that starts with a S
|
||||
$players = Player::get()->filter([
|
||||
'FirstName:StartsWith' => 'S',
|
||||
'PlayerNumber:GreaterThan' => '10'
|
||||
]);
|
||||
|
||||
// to fetch any player that's name contains the letter 'z'
|
||||
$players = Player::get()->filterAny(array(
|
||||
'FirstName:PartialMatch' => 'z',
|
||||
'LastName:PartialMatch' => 'z'
|
||||
));
|
||||
// to fetch any player that's name contains the letter 'z'
|
||||
$players = Player::get()->filterAny([
|
||||
'FirstName:PartialMatch' => 'z',
|
||||
'LastName:PartialMatch' => 'z'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
Developers can define their own [SearchFilter](api:SilverStripe\ORM\Filters\SearchFilter) if needing to extend the ORM filter and exclude behaviors.
|
||||
|
||||
|
@ -41,23 +43,26 @@ within the `DataListFilter.` prefixed namespace. New filters can be registered u
|
|||
config:
|
||||
|
||||
|
||||
:::yaml
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
DataListFilter.CustomMatch:
|
||||
class: MyVendor/Search/CustomMatchFilter
|
||||
```yaml
|
||||
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
DataListFilter.CustomMatch:
|
||||
class: MyVendor/Search/CustomMatchFilter
|
||||
```
|
||||
|
||||
The following is a query which will return everyone whose first name starts with "S", either lowercase or uppercase:
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName:StartsWith:nocase' => 'S'
|
||||
));
|
||||
```php
|
||||
$players = Player::get()->filter([
|
||||
'FirstName:StartsWith:nocase' => 'S'
|
||||
]);
|
||||
|
||||
// use :not to perform a converse operation to filter anything but a 'W'
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName:StartsWith:not' => 'W'
|
||||
));
|
||||
// use :not to perform a converse operation to filter anything but a 'W'
|
||||
$players = Player::get()->filter([
|
||||
'FirstName:StartsWith:not' => 'W'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
|
|
|
@ -16,27 +16,34 @@ By default, all `DataObject` subclasses can only be edited, created and viewed b
|
|||
code.
|
||||
</div>
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
public function canView($member = null) {
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
class MyDataObject extends DataObject
|
||||
{
|
||||
|
||||
public function canView($member = null)
|
||||
{
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
|
||||
public function canEdit($member = null) {
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
public function canEdit($member = null)
|
||||
{
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
|
||||
public function canDelete($member = null) {
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
public function canDelete($member = null)
|
||||
{
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
|
||||
public function canCreate($member = null) {
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
}
|
||||
public function canCreate($member = null)
|
||||
{
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
These checks are not enforced on low-level ORM operations such as `write()` or `delete()`, but rather rely on being
|
||||
|
|
|
@ -18,16 +18,17 @@ such as counts or returning a single column.
|
|||
For example, if you want to run a simple `COUNT` SQL statement,
|
||||
the following three statements are functionally equivalent:
|
||||
|
||||
:::php
|
||||
// Through raw SQL.
|
||||
$count = DB::query('SELECT COUNT(*) FROM "Member"')->value();
|
||||
```php
|
||||
// Through raw SQL.
|
||||
$count = DB::query('SELECT COUNT(*) FROM "Member"')->value();
|
||||
|
||||
// Through SQLSelect abstraction layer.
|
||||
$query = new SQLSelect();
|
||||
$count = $query->setFrom('Member')->setSelect('COUNT(*)')->value();
|
||||
// Through SQLSelect abstraction layer.
|
||||
$query = new SQLSelect();
|
||||
$count = $query->setFrom('Member')->setSelect('COUNT(*)')->value();
|
||||
|
||||
// Through the ORM.
|
||||
$count = Member::get()->count();
|
||||
// Through the ORM.
|
||||
$count = Member::get()->count();
|
||||
```
|
||||
|
||||
If you do use raw SQL, you'll run the risk of breaking
|
||||
various assumptions the ORM and code based on it have:
|
||||
|
@ -56,31 +57,32 @@ conditional filters, grouping, limiting, and sorting.
|
|||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
|
||||
$sqlQuery = new SQLSelect();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->selectField('FieldName', 'Name');
|
||||
$sqlQuery->selectField('YEAR("Birthday")', 'Birthyear');
|
||||
$sqlQuery->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"');
|
||||
$sqlQuery->addWhere(['YEAR("Birthday") = ?' => 1982]);
|
||||
// $sqlQuery->setOrderBy(...);
|
||||
// $sqlQuery->setGroupBy(...);
|
||||
// $sqlQuery->setHaving(...);
|
||||
// $sqlQuery->setLimit(...);
|
||||
// $sqlQuery->setDistinct(true);
|
||||
|
||||
// Get the raw SQL (optional) and parameters
|
||||
$rawSQL = $sqlQuery->sql($parameters);
|
||||
|
||||
// Execute and return a Query object
|
||||
$result = $sqlQuery->execute();
|
||||
|
||||
$sqlQuery = new SQLSelect();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->selectField('FieldName', 'Name');
|
||||
$sqlQuery->selectField('YEAR("Birthday")', 'Birthyear');
|
||||
$sqlQuery->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"');
|
||||
$sqlQuery->addWhere(array('YEAR("Birthday") = ?' => 1982));
|
||||
// $sqlQuery->setOrderBy(...);
|
||||
// $sqlQuery->setGroupBy(...);
|
||||
// $sqlQuery->setHaving(...);
|
||||
// $sqlQuery->setLimit(...);
|
||||
// $sqlQuery->setDistinct(true);
|
||||
|
||||
// Get the raw SQL (optional) and parameters
|
||||
$rawSQL = $sqlQuery->sql($parameters);
|
||||
|
||||
// Execute and return a Query object
|
||||
$result = $sqlQuery->execute();
|
||||
// Iterate over results
|
||||
foreach($result as $row) {
|
||||
echo $row['BirthYear'];
|
||||
}
|
||||
|
||||
// Iterate over results
|
||||
foreach($result as $row) {
|
||||
echo $row['BirthYear'];
|
||||
}
|
||||
```
|
||||
|
||||
The result of `SQLSelect::execute()` is an array lightly wrapped in a database-specific subclass of [Query](api:SilverStripe\ORM\Connect\Query).
|
||||
This class implements the *Iterator*-interface, and provides convenience-methods for accessing the data.
|
||||
|
@ -88,36 +90,39 @@ This class implements the *Iterator*-interface, and provides convenience-methods
|
|||
### DELETE
|
||||
|
||||
Deletion can be done either by calling `DB::query`/`DB::prepared_query` directly,
|
||||
by creating a `SQLDelete` object, or by transforming a `SQLQuery` into a `SQLDelete`
|
||||
by creating a `SQLDelete` object, or by transforming a `SQLSelect` into a `SQLDelete`
|
||||
object instead.
|
||||
|
||||
For example, creating a `SQLDelete` object
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
|
||||
$query = SQLDelete::create()
|
||||
->setFrom('"SiteTree"')
|
||||
->setWhere(['"SiteTree"."ShowInMenus"' => 0]);
|
||||
$query->execute();
|
||||
|
||||
$query = SQLDelete::create()
|
||||
->setFrom('"SiteTree"')
|
||||
->setWhere(array('"SiteTree"."ShowInMenus"' => 0));
|
||||
$query->execute();
|
||||
```
|
||||
|
||||
Alternatively, turning an existing `SQLQuery` into a delete
|
||||
Alternatively, turning an existing `SQLSelect` into a delete
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
|
||||
$query = SQLSelect::create()
|
||||
->setFrom('"SiteTree"')
|
||||
->setWhere(['"SiteTree"."ShowInMenus"' => 0])
|
||||
->toDelete();
|
||||
$query->execute();
|
||||
|
||||
$query = SQLQuery::create()
|
||||
->setFrom('"SiteTree"')
|
||||
->setWhere(array('"SiteTree"."ShowInMenus"' => 0))
|
||||
->toDelete();
|
||||
$query->execute();
|
||||
```
|
||||
|
||||
Directly querying the database
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
|
||||
DB::prepared_query('DELETE FROM "SiteTree" WHERE "SiteTree"."ShowInMenus" = ?', [0]);
|
||||
|
||||
DB::prepared_query('DELETE FROM "SiteTree" WHERE "SiteTree"."ShowInMenus" = ?', array(0));
|
||||
```
|
||||
|
||||
### INSERT/UPDATE
|
||||
|
||||
|
@ -163,31 +168,32 @@ SQLInsert also includes the following api methods:
|
|||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
$update = SQLUpdate::create('"SiteTree"')->addWhere(array('ID' => 3));
|
||||
```php
|
||||
$update = SQLUpdate::create('"SiteTree"')->addWhere(['ID' => 3]);
|
||||
|
||||
// assigning a list of items
|
||||
$update->addAssignments(array(
|
||||
'"Title"' => 'Our Products',
|
||||
'"MenuTitle"' => 'Products'
|
||||
));
|
||||
// assigning a list of items
|
||||
$update->addAssignments([
|
||||
'"Title"' => 'Our Products',
|
||||
'"MenuTitle"' => 'Products'
|
||||
]);
|
||||
|
||||
// Assigning a single value
|
||||
$update->assign('"MenuTitle"', 'Products');
|
||||
// Assigning a single value
|
||||
$update->assign('"MenuTitle"', 'Products');
|
||||
|
||||
// Assigning a value using parameterised expression
|
||||
$title = 'Products';
|
||||
$update->assign('"MenuTitle"', array(
|
||||
'CASE WHEN LENGTH("MenuTitle") > LENGTH(?) THEN "MenuTitle" ELSE ? END' =>
|
||||
array($title, $title)
|
||||
));
|
||||
// Assigning a value using parameterised expression
|
||||
$title = 'Products';
|
||||
$update->assign('"MenuTitle"', [
|
||||
'CASE WHEN LENGTH("MenuTitle") > LENGTH(?) THEN "MenuTitle" ELSE ? END' =>
|
||||
[$title, $title]
|
||||
]);
|
||||
|
||||
// Assigning a value using a pure SQL expression
|
||||
$update->assignSQL('"Date"', 'NOW()');
|
||||
// Assigning a value using a pure SQL expression
|
||||
$update->assignSQL('"Date"', 'NOW()');
|
||||
|
||||
// Perform the update
|
||||
$update->execute();
|
||||
// Perform the update
|
||||
$update->execute();
|
||||
|
||||
```
|
||||
|
||||
In addition to assigning values, the SQLInsert object also supports multi-row
|
||||
inserts. For database connectors and API that don't have multi-row insert support
|
||||
|
@ -195,27 +201,28 @@ these are translated internally as multiple single row inserts.
|
|||
|
||||
For example,
|
||||
|
||||
:::php
|
||||
<?php
|
||||
$insert = SQLInsert::create('"SiteTree"');
|
||||
```php
|
||||
$insert = SQLInsert::create('"SiteTree"');
|
||||
|
||||
// Add multiple rows in a single call. Note that column names do not need
|
||||
// to be symmetric
|
||||
$insert->addRows(array(
|
||||
array('"Title"' => 'Home', '"Content"' => '<p>This is our home page</p>'),
|
||||
array('"Title"' => 'About Us', '"ClassName"' => 'AboutPage')
|
||||
));
|
||||
// Add multiple rows in a single call. Note that column names do not need
|
||||
// to be symmetric
|
||||
$insert->addRows([
|
||||
['"Title"' => 'Home', '"Content"' => '<p>This is our home page</p>'],
|
||||
['"Title"' => 'About Us', '"ClassName"' => 'AboutPage']
|
||||
]);
|
||||
|
||||
// Adjust an assignment on the last row
|
||||
$insert->assign('"Content"', '<p>This is about us</p>');
|
||||
// Adjust an assignment on the last row
|
||||
$insert->assign('"Content"', '<p>This is about us</p>');
|
||||
|
||||
// Add another row
|
||||
$insert->addRow(array('"Title"' => 'Contact Us'));
|
||||
// Add another row
|
||||
$insert->addRow(['"Title"' => 'Contact Us']);
|
||||
|
||||
$columns = $insert->getColumns();
|
||||
// $columns will be array('"Title"', '"Content"', '"ClassName"');
|
||||
$columns = $insert->getColumns();
|
||||
// $columns will be array('"Title"', '"Content"', '"ClassName"');
|
||||
|
||||
$insert->execute();
|
||||
$insert->execute();
|
||||
|
||||
```
|
||||
|
||||
### Value Checks
|
||||
|
||||
|
@ -224,18 +231,21 @@ e.g. when you want a single column rather than a full-blown object representatio
|
|||
|
||||
Example: Get the count from a relationship.
|
||||
|
||||
:::php
|
||||
$sqlQuery = new SQLSelect();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->addSelect('COUNT("Player"."ID")');
|
||||
$sqlQuery->addWhere(array('"Team"."ID"' => 99));
|
||||
$sqlQuery->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"');
|
||||
$count = $sqlQuery->execute()->value();
|
||||
|
||||
```php
|
||||
$sqlQuery = new SQLSelect();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->addSelect('COUNT("Player"."ID")');
|
||||
$sqlQuery->addWhere(['"Team"."ID"' => 99]);
|
||||
$sqlQuery->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"');
|
||||
$count = $sqlQuery->execute()->value();
|
||||
|
||||
```
|
||||
|
||||
Note that in the ORM, this call would be executed in an efficient manner as well:
|
||||
|
||||
:::php
|
||||
$count = $myTeam->Players()->count();
|
||||
```php
|
||||
$count = $myTeam->Players()->count();
|
||||
```
|
||||
|
||||
### Mapping
|
||||
|
||||
|
@ -244,30 +254,36 @@ This can be useful for creating dropdowns.
|
|||
|
||||
Example: Show player names with their birth year, but set their birth dates as values.
|
||||
|
||||
:::php
|
||||
$sqlQuery = new SQLSelect();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->setSelect('Birthdate');
|
||||
$sqlQuery->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear');
|
||||
$map = $sqlQuery->execute()->map();
|
||||
$field = new DropdownField('Birthdates', 'Birthdates', $map);
|
||||
```php
|
||||
$sqlQuery = new SQLSelect();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->setSelect('Birthdate');
|
||||
$sqlQuery->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear');
|
||||
$map = $sqlQuery->execute()->map();
|
||||
$field = new DropdownField('Birthdates', 'Birthdates', $map);
|
||||
```
|
||||
|
||||
Note that going through SQLSelect is just necessary here
|
||||
because of the custom SQL value transformation (`YEAR()`).
|
||||
An alternative approach would be a custom getter in the object definition.
|
||||
|
||||
:::php
|
||||
class Player extends DataObject {
|
||||
private static $db = array(
|
||||
'Name' => 'Varchar',
|
||||
'Birthdate' => 'Date'
|
||||
);
|
||||
function getNameWithBirthyear() {
|
||||
return date('y', $this->Birthdate);
|
||||
}
|
||||
}
|
||||
$players = Player::get();
|
||||
$map = $players->map('Name', 'NameWithBirthyear');
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject
|
||||
{
|
||||
private static $db = [
|
||||
'Name' => 'Varchar',
|
||||
'Birthdate' => 'Date'
|
||||
];
|
||||
function getNameWithBirthyear() {
|
||||
return date('y', $this->Birthdate);
|
||||
}
|
||||
}
|
||||
$players = Player::get();
|
||||
$map = $players->map('Name', 'NameWithBirthyear');
|
||||
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
|
|
|
@ -21,26 +21,30 @@ write, and respond appropriately if it isn't.
|
|||
|
||||
The return value of `validate()` is a [ValidationResult](api:SilverStripe\ORM\ValidationResult) object.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyObject extends DataObject {
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
|
||||
private static $db = array(
|
||||
'Country' => 'Varchar',
|
||||
'Postcode' => 'Varchar'
|
||||
);
|
||||
private static $db = [
|
||||
'Country' => 'Varchar',
|
||||
'Postcode' => 'Varchar'
|
||||
];
|
||||
|
||||
public function validate() {
|
||||
$result = parent::validate();
|
||||
public function validate()
|
||||
{
|
||||
$result = parent::validate();
|
||||
|
||||
if($this->Country == 'DE' && $this->Postcode && strlen($this->Postcode) != 5) {
|
||||
$result->error('Need five digits for German postcodes');
|
||||
}
|
||||
if($this->Country == 'DE' && $this->Postcode && strlen($this->Postcode) != 5) {
|
||||
$result->error('Need five digits for German postcodes');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
|
|
|
@ -20,25 +20,32 @@ By default, adding the `Versioned extension will create a "Stage" and "Live" sta
|
|||
also track versioned history.
|
||||
|
||||
|
||||
:::php
|
||||
class MyStagedModel extends DataObject {
|
||||
private static $extensions = [
|
||||
Versioned::class
|
||||
];
|
||||
}
|
||||
```php
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyStagedModel extends DataObject
|
||||
{
|
||||
private static $extensions = [
|
||||
Versioned::class
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, staging can be disabled, so that only versioned changes are tracked for your model. This
|
||||
can be specified by setting the constructor argument to "Versioned"
|
||||
|
||||
|
||||
:::php
|
||||
class VersionedModel extends DataObject {
|
||||
private static $extensions = [
|
||||
"SilverStripe\\ORM\\Versioning\\Versioned('Versioned')"
|
||||
];
|
||||
}
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class VersionedModel extends DataObject
|
||||
{
|
||||
private static $extensions = [
|
||||
"SilverStripe\\ORM\\Versioning\\Versioned('Versioned')"
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
The extension is automatically applied to `SiteTree` class. For more information on extensions see
|
||||
|
@ -79,22 +86,24 @@ automatically joined as required:
|
|||
By default, all records are retrieved from the "Draft" stage (so the `MyRecord` table in our example). You can
|
||||
explicitly request a certain stage through various getters on the `Versioned` class.
|
||||
|
||||
:::php
|
||||
// Fetching multiple records
|
||||
$stageRecords = Versioned::get_by_stage('MyRecord', Versioned::DRAFT);
|
||||
$liveRecords = Versioned::get_by_stage('MyRecord', Versioned::LIVE);
|
||||
```php
|
||||
// Fetching multiple records
|
||||
$stageRecords = Versioned::get_by_stage('MyRecord', Versioned::DRAFT);
|
||||
$liveRecords = Versioned::get_by_stage('MyRecord', Versioned::LIVE);
|
||||
|
||||
// Fetching a single record
|
||||
$stageRecord = Versioned::get_by_stage('MyRecord', Versioned::DRAFT)->byID(99);
|
||||
$liveRecord = Versioned::get_by_stage('MyRecord', Versioned::LIVE)->byID(99);
|
||||
// Fetching a single record
|
||||
$stageRecord = Versioned::get_by_stage('MyRecord', Versioned::DRAFT)->byID(99);
|
||||
$liveRecord = Versioned::get_by_stage('MyRecord', Versioned::LIVE)->byID(99);
|
||||
```
|
||||
|
||||
### Historical Versions
|
||||
|
||||
The above commands will just retrieve the latest version of its respective stage for you, but not older versions stored
|
||||
in the `<class>_versions` tables.
|
||||
|
||||
:::php
|
||||
$historicalRecord = Versioned::get_version('MyRecord', <record-id>, <version-id>);
|
||||
```php
|
||||
$historicalRecord = Versioned::get_version('MyRecord', <record-id>, <version-id>);
|
||||
```
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
The record is retrieved as a `DataObject`, but saving back modifications via `write()` will create a new version,
|
||||
|
@ -105,10 +114,11 @@ In order to get a list of all versions for a specific record, we need to generat
|
|||
objects, which expose the same database information as a `DataObject`, but also include information about when and how
|
||||
a record was published.
|
||||
|
||||
:::php
|
||||
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
||||
$versions = $record->allVersions();
|
||||
echo $versions->First()->Version; // instance of Versioned_Version
|
||||
```php
|
||||
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
||||
$versions = $record->allVersions();
|
||||
echo $versions->First()->Version; // instance of Versioned_Version
|
||||
```
|
||||
|
||||
### Writing Versions and Changing Stages
|
||||
|
||||
|
@ -130,33 +140,36 @@ done via one of several ways:
|
|||
* `publishRecursive` Publishes this record, and any dependant objects this record may refer to.
|
||||
See "DataObject ownership" for reference on dependant objects.
|
||||
|
||||
:::php
|
||||
$record = Versioned::get_by_stage('MyRecord', Versioned::DRAFT)->byID(99);
|
||||
$record->MyField = 'changed';
|
||||
// will update `MyRecord` table (assuming Versioned::current_stage() == 'Stage'),
|
||||
// and write a row to `MyRecord_versions`.
|
||||
$record->write();
|
||||
// will copy the saved record information to the `MyRecord_Live` table
|
||||
$record->publishRecursive();
|
||||
```php
|
||||
$record = Versioned::get_by_stage('MyRecord', Versioned::DRAFT)->byID(99);
|
||||
$record->MyField = 'changed';
|
||||
// will update `MyRecord` table (assuming Versioned::current_stage() == 'Stage'),
|
||||
// and write a row to `MyRecord_versions`.
|
||||
$record->write();
|
||||
// will copy the saved record information to the `MyRecord_Live` table
|
||||
$record->publishRecursive();
|
||||
```
|
||||
|
||||
Similarly, an "unpublish" operation does the reverse, and removes a record from a specific stage.
|
||||
|
||||
:::php
|
||||
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
||||
// will remove the row from the `MyRecord_Live` table
|
||||
$record->deleteFromStage(Versioned::LIVE);
|
||||
```php
|
||||
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
||||
// will remove the row from the `MyRecord_Live` table
|
||||
$record->deleteFromStage(Versioned::LIVE);
|
||||
```
|
||||
|
||||
### Forcing the Current Stage
|
||||
|
||||
The current stage is stored as global state on the object. It is usually modified by controllers, e.g. when a preview
|
||||
is initialized. But it can also be set and reset temporarily to force a specific operation to run on a certain stage.
|
||||
|
||||
:::php
|
||||
$origMode = Versioned::get_reading_mode(); // save current mode
|
||||
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Live' records
|
||||
Versioned::set_reading_mode(Versioned::DRAFT); // temporarily overwrite mode
|
||||
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
|
||||
Versioned::set_reading_mode($origMode); // reset current mode
|
||||
```php
|
||||
$origMode = Versioned::get_reading_mode(); // save current mode
|
||||
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Live' records
|
||||
Versioned::set_reading_mode(Versioned::DRAFT); // temporarily overwrite mode
|
||||
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
|
||||
Versioned::set_reading_mode($origMode); // reset current mode
|
||||
```
|
||||
|
||||
### DataObject ownership
|
||||
|
||||
|
@ -177,29 +190,35 @@ When pages of type `MyPage` are published, any owned images and banners will be
|
|||
without requiring any custom code.
|
||||
|
||||
|
||||
:::php
|
||||
class MyPage extends Page {
|
||||
private static $has_many = array(
|
||||
'Banners' => Banner::class
|
||||
);
|
||||
private static $owns = array(
|
||||
'Banners'
|
||||
);
|
||||
}
|
||||
|
||||
class Banner extends Page {
|
||||
private static $extensions = array(
|
||||
Versioned::class
|
||||
);
|
||||
private static $has_one = array(
|
||||
'Parent' => MyPage::class,
|
||||
'Image' => Image::class,
|
||||
);
|
||||
private static $owns = array(
|
||||
'Image'
|
||||
);
|
||||
}
|
||||
```php
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\Assets\Image;
|
||||
use Page;
|
||||
|
||||
class MyPage extends Page
|
||||
{
|
||||
private static $has_many = [
|
||||
'Banners' => Banner::class
|
||||
];
|
||||
private static $owns = [
|
||||
'Banners'
|
||||
];
|
||||
}
|
||||
class Banner extends Page
|
||||
{
|
||||
private static $extensions = [
|
||||
Versioned::class
|
||||
];
|
||||
private static $has_one = [
|
||||
'Parent' => MyPage::class,
|
||||
'Image' => Image::class,
|
||||
];
|
||||
private static $owns = [
|
||||
'Image'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Note that ownership cannot be used with polymorphic relations. E.g. has_one to non-type specific `DataObject`.
|
||||
|
||||
|
@ -216,29 +235,38 @@ that can be used to traverse between each, and then by ensuring you configure bo
|
|||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
class MyParent extends DataObject {
|
||||
private static $extensions = array(
|
||||
Versioned::class
|
||||
);
|
||||
private static $owns = array(
|
||||
'ChildObjects'
|
||||
);
|
||||
public function ChildObjects() {
|
||||
return MyChild::get();
|
||||
}
|
||||
}
|
||||
class MyChild extends DataObject {
|
||||
private static $extensions = array(
|
||||
Versioned::class
|
||||
);
|
||||
private static $owned_by = array(
|
||||
'Parent'
|
||||
);
|
||||
public function Parent() {
|
||||
return MyParent::get()->first();
|
||||
}
|
||||
}
|
||||
```php
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyParent extends DataObject
|
||||
{
|
||||
private static $extensions = [
|
||||
Versioned::class
|
||||
];
|
||||
private static $owns = [
|
||||
'ChildObjects'
|
||||
];
|
||||
public function ChildObjects()
|
||||
{
|
||||
return MyChild::get();
|
||||
}
|
||||
}
|
||||
class MyChild extends DataObject
|
||||
{
|
||||
private static $extensions = [
|
||||
Versioned::class
|
||||
];
|
||||
private static $owned_by = [
|
||||
'Parent'
|
||||
];
|
||||
public function Parent()
|
||||
{
|
||||
return MyParent::get()->first();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### DataObject Ownership in HTML Content
|
||||
|
||||
|
@ -257,8 +285,9 @@ smaller modifications of the generated `DataList` objects.
|
|||
|
||||
Example: Get the first 10 live records, filtered by creation date:
|
||||
|
||||
:::php
|
||||
$records = Versioned::get_by_stage('MyRecord', Versioned::LIVE)->limit(10)->sort('Created', 'ASC');
|
||||
```php
|
||||
$records = Versioned::get_by_stage('MyRecord', Versioned::LIVE)->limit(10)->sort('Created', 'ASC');
|
||||
```
|
||||
|
||||
### Permissions
|
||||
|
||||
|
@ -280,13 +309,19 @@ Versioned object visibility can be customised in one of the following ways by ed
|
|||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
private static $extensions = array(
|
||||
```php
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
private static $extensions = [
|
||||
Versioned::class,
|
||||
);
|
||||
];
|
||||
|
||||
public function canViewVersioned($member = null) {
|
||||
public function canViewVersioned($member = null)
|
||||
{
|
||||
// Check if site is live
|
||||
$mode = $this->getSourceQueryParam("Versioned.mode");
|
||||
$stage = $this->getSourceQueryParam("Versioned.stage");
|
||||
|
@ -299,6 +334,8 @@ E.g.
|
|||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If you want to control permissions of an object in an extension, you can also use
|
||||
one of the below extension points in your `DataExtension` subclass:
|
||||
|
||||
|
@ -310,26 +347,38 @@ only be invoked if the object is in a non-published state.
|
|||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
class MyObjectExtension extends DataExtension {
|
||||
public function canViewNonLive($member = null) {
|
||||
```php
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
|
||||
class MyObjectExtension extends DataExtension
|
||||
{
|
||||
public function canViewNonLive($member = null)
|
||||
{
|
||||
return Permission::check($member, 'DRAFT_STATUS');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If none of the above checks are overridden, visibility will be determined by the
|
||||
permissions in the `TargetObject.non_live_permissions` config.
|
||||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
private static $extensions = array(
|
||||
```php
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
private static $extensions = [
|
||||
Versioned::class,
|
||||
);
|
||||
private static $non_live_permissions = array('ADMIN');
|
||||
];
|
||||
private static $non_live_permissions = ['ADMIN'];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Versioned applies no additional permissions to `canEdit` or `canCreate`, and such
|
||||
these permissions should be implemented as per standard unversioned DataObjects.
|
||||
|
||||
|
@ -345,12 +394,13 @@ default, and only preview draft content if explicitly requested (e.g. by the "pr
|
|||
to force a specific stage, we recommend the `Controller->init()` method for this purpose, for example:
|
||||
|
||||
**mysite/code/MyController.php**
|
||||
:::php
|
||||
public function init() {
|
||||
parent::init();
|
||||
Versioned::set_stage(Versioned::DRAFT);
|
||||
}
|
||||
|
||||
```php
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
Versioned::set_stage(Versioned::DRAFT);
|
||||
}
|
||||
```
|
||||
|
||||
### Controllers
|
||||
|
||||
|
|
|
@ -12,45 +12,48 @@ customise those fields as required.
|
|||
|
||||
An example is `DataObject`, SilverStripe will automatically create your CMS interface so you can modify what you need.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'IsActive' => 'Boolean',
|
||||
'Title' => 'Varchar',
|
||||
'Content' => 'Text'
|
||||
);
|
||||
class MyDataObject extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'IsActive' => 'Boolean',
|
||||
'Title' => 'Varchar',
|
||||
'Content' => 'Text'
|
||||
];
|
||||
|
||||
public function getCMSFields() {
|
||||
// parent::getCMSFields() does all the hard work and creates the fields for Title, IsActive and Content.
|
||||
$fields = parent::getCMSFields();
|
||||
$fields->dataFieldByName('IsActive')->setTitle('Is active?');
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
public function getCMSFields()
|
||||
{
|
||||
// parent::getCMSFields() does all the hard work and creates the fields for Title, IsActive and Content.
|
||||
$fields = parent::getCMSFields();
|
||||
$fields->dataFieldByName('IsActive')->setTitle('Is active?');
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To fully customise your form fields, start with an empty FieldList.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = FieldList::create(
|
||||
TabSet::create("Root.Main",
|
||||
CheckboxSetField::create('IsActive','Is active?'),
|
||||
TextField::create('Title'),
|
||||
TextareaField::create('Content')
|
||||
->setRows(5)
|
||||
)
|
||||
);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
|
||||
```php
|
||||
|
||||
public function getCMSFields()
|
||||
{
|
||||
$fields = FieldList::create(
|
||||
TabSet::create("Root.Main",
|
||||
CheckboxSetField::create('IsActive','Is active?'),
|
||||
TextField::create('Title'),
|
||||
TextareaField::create('Content')
|
||||
->setRows(5)
|
||||
)
|
||||
);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
```
|
||||
|
||||
You can also alter the fields of built-in and module `DataObject` classes through your own
|
||||
[DataExtension](/developer_guides/extending/extensions), and a call to `DataExtension->updateCMSFields`.
|
||||
|
@ -60,163 +63,179 @@ You can also alter the fields of built-in and module `DataObject` classes throug
|
|||
The `$searchable_fields` property uses a mixed array format that can be used to further customise your generated admin
|
||||
system. The default is a set of array values listing the fields.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'Name',
|
||||
'ProductCode'
|
||||
);
|
||||
}
|
||||
class MyDataObject extends DataObject
|
||||
{
|
||||
|
||||
private static $searchable_fields = [
|
||||
'Name',
|
||||
'ProductCode'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Searchable fields will be appear in the search interface with a default form field (usually a [TextField](api:SilverStripe\Forms\TextField)) and a
|
||||
default search filter assigned (usually an [ExactMatchFilter](api:SilverStripe\ORM\Filters\ExactMatchFilter)). To override these defaults, you can specify
|
||||
additional information on `$searchable_fields`:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
class MyDataObject extends DataObject
|
||||
{
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'Name' => 'PartialMatchFilter',
|
||||
'ProductCode' => 'NumericField'
|
||||
);
|
||||
}
|
||||
private static $searchable_fields = [
|
||||
'Name' => 'PartialMatchFilter',
|
||||
'ProductCode' => 'NumericField'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If you assign a single string value, you can set it to be either a [FormField](api:SilverStripe\Forms\FormField) or [SearchFilter](api:SilverStripe\ORM\Filters\SearchFilter). To specify
|
||||
both, you can assign an array:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'Name' => array(
|
||||
'field' => 'TextField',
|
||||
'filter' => 'PartialMatchFilter',
|
||||
),
|
||||
'ProductCode' => array(
|
||||
'title' => 'Product code #',
|
||||
'field' => 'NumericField',
|
||||
'filter' => 'PartialMatchFilter',
|
||||
),
|
||||
);
|
||||
}
|
||||
class MyDataObject extends DataObject
|
||||
{
|
||||
|
||||
private static $searchable_fields = [
|
||||
'Name' => [
|
||||
'field' => 'TextField',
|
||||
'filter' => 'PartialMatchFilter',
|
||||
],
|
||||
'ProductCode' => [
|
||||
'title' => 'Product code #',
|
||||
'field' => 'NumericField',
|
||||
'filter' => 'PartialMatchFilter',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To include relations (`$has_one`, `$has_many` and `$many_many`) in your search, you can use a dot-notation.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Team extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
|
||||
private static $many_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'Title',
|
||||
'Players.Name',
|
||||
);
|
||||
}
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Varchar',
|
||||
'Birthday' => 'Date'
|
||||
);
|
||||
|
||||
private static $belongs_many_many = array(
|
||||
'Teams' => 'Team'
|
||||
);
|
||||
}
|
||||
class Team extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'Title' => 'Varchar'
|
||||
];
|
||||
|
||||
private static $many_many = [
|
||||
'Players' => 'Player'
|
||||
];
|
||||
|
||||
private static $searchable_fields = [
|
||||
'Title',
|
||||
'Players.Name',
|
||||
];
|
||||
}
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'Name' => 'Varchar',
|
||||
'Birthday' => 'Date'
|
||||
];
|
||||
|
||||
private static $belongs_many_many = [
|
||||
'Teams' => 'Team'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Summary Fields
|
||||
|
||||
Summary fields can be used to show a quick overview of the data for a specific [DataObject](api:SilverStripe\ORM\DataObject) record. The most common use
|
||||
is their display as table columns, e.g. in the search results of a [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) CMS interface.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Text',
|
||||
'OtherProperty' => 'Text',
|
||||
'ProductCode' => 'Int',
|
||||
);
|
||||
|
||||
private static $summary_fields = array(
|
||||
'Name',
|
||||
'ProductCode'
|
||||
);
|
||||
}
|
||||
class MyDataObject extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'Name' => 'Text',
|
||||
'OtherProperty' => 'Text',
|
||||
'ProductCode' => 'Int',
|
||||
];
|
||||
|
||||
private static $summary_fields = [
|
||||
'Name',
|
||||
'ProductCode'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To include relations or field manipulations in your summaries, you can use a dot-notation.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class OtherObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
}
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Text',
|
||||
'Description' => 'HTMLText'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'OtherObject' => 'OtherObject'
|
||||
);
|
||||
|
||||
private static $summary_fields = array(
|
||||
'Name' => 'Name',
|
||||
'Description.Summary' => 'Description (summary)',
|
||||
'OtherObject.Title' => 'Other Object Title'
|
||||
);
|
||||
}
|
||||
class OtherObject extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'Title' => 'Varchar'
|
||||
];
|
||||
}
|
||||
class MyDataObject extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'Name' => 'Text',
|
||||
'Description' => 'HTMLText'
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'OtherObject' => 'OtherObject'
|
||||
];
|
||||
|
||||
private static $summary_fields = [
|
||||
'Name' => 'Name',
|
||||
'Description.Summary' => 'Description (summary)',
|
||||
'OtherObject.Title' => 'Other Object Title'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Non-textual elements (such as images and their manipulations) can also be used in summaries.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Text'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'HeroImage' => 'Image'
|
||||
);
|
||||
|
||||
private static $summary_fields = array(
|
||||
'Name' => 'Name',
|
||||
'HeroImage.CMSThumbnail' => 'Hero Image'
|
||||
);
|
||||
}
|
||||
class MyDataObject extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'Name' => 'Text'
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'HeroImage' => 'Image'
|
||||
];
|
||||
|
||||
private static $summary_fields = [
|
||||
'Name' => 'Name',
|
||||
'HeroImage.CMSThumbnail' => 'Hero Image'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
|
|
|
@ -24,20 +24,22 @@ this index is present on the associative entity).
|
|||
Indexes are represented on a `DataObject` through the `DataObject::$indexes` array which maps index names to a
|
||||
descriptor. There are several supported notations:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyObject extends DataObject {
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
|
||||
private static $indexes = [
|
||||
'<column-name>' => true,
|
||||
'<index-name>' => [
|
||||
'type' => '<type>',
|
||||
'columns' => ['<column-name>', '<other-column-name>'],
|
||||
],
|
||||
'<index-name>' => ['<column-name>', '<other-column-name>'],
|
||||
];
|
||||
}
|
||||
private static $indexes = [
|
||||
'<column-name>' => true,
|
||||
'<index-name>' => [
|
||||
'type' => '<type>',
|
||||
'columns' => ['<column-name>', '<other-column-name>'],
|
||||
],
|
||||
'<index-name>' => ['<column-name>', '<other-column-name>'],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
The `<column-name>` is used to put a standard non-unique index on the column specified. For complex or large tables
|
||||
we recommend building the index to suite the requirements of your data.
|
||||
|
@ -52,20 +54,22 @@ support the following:
|
|||
|
||||
**mysite/code/MyTestObject.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyTestObject extends DataObject {
|
||||
class MyTestObject extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'MyField' => 'Varchar',
|
||||
'MyOtherField' => 'Varchar',
|
||||
];
|
||||
private static $db = [
|
||||
'MyField' => 'Varchar',
|
||||
'MyOtherField' => 'Varchar',
|
||||
];
|
||||
|
||||
private static $indexes = [
|
||||
'MyIndexName' => ['MyField', 'MyOtherField'],
|
||||
];
|
||||
}
|
||||
private static $indexes = [
|
||||
'MyIndexName' => ['MyField', 'MyOtherField'],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Complex/Composite Indexes
|
||||
For complex queries it may be necessary to define a complex or composite index on the supporting object. To create a
|
||||
|
|
|
@ -9,28 +9,31 @@ object!
|
|||
|
||||
A simple example is to set a field to the current date and time:
|
||||
|
||||
:::php
|
||||
/**
|
||||
* Sets the Date field to the current date.
|
||||
*/
|
||||
public function populateDefaults() {
|
||||
$this->Date = date('Y-m-d');
|
||||
parent::populateDefaults();
|
||||
}
|
||||
|
||||
```php
|
||||
/**
|
||||
* Sets the Date field to the current date.
|
||||
*/
|
||||
public function populateDefaults()
|
||||
{
|
||||
$this->Date = date('Y-m-d');
|
||||
parent::populateDefaults();
|
||||
}
|
||||
```
|
||||
It's also possible to get the data from any other source, or another object, just by using the usual data retrieval
|
||||
methods. For example:
|
||||
|
||||
:::php
|
||||
/**
|
||||
* This method combines the Title of the parent object with the Title of this
|
||||
* object in the FullTitle field.
|
||||
*/
|
||||
public function populateDefaults() {
|
||||
if($parent = $this->Parent()) {
|
||||
$this->FullTitle = $parent->Title . ': ' . $this->Title;
|
||||
} else {
|
||||
$this->FullTitle = $this->Title;
|
||||
}
|
||||
parent::populateDefaults();
|
||||
}
|
||||
```php
|
||||
/**
|
||||
* This method combines the Title of the parent object with the Title of this
|
||||
* object in the FullTitle field.
|
||||
*/
|
||||
public function populateDefaults()
|
||||
{
|
||||
if($parent = $this->Parent()) {
|
||||
$this->FullTitle = $parent->Title . ': ' . $this->Title;
|
||||
} else {
|
||||
$this->FullTitle = $this->Title;
|
||||
}
|
||||
parent::populateDefaults();
|
||||
}
|
||||
```
|
|
@ -34,54 +34,67 @@ The first step is to set up the basic data model,
|
|||
along with a method that returns the first letter of the title. This
|
||||
will be used both for grouping and for the title in the template.
|
||||
|
||||
:::php
|
||||
class Module extends DataObject {
|
||||
private static $db = array(
|
||||
'Title' => 'Text'
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the first letter of the module title, used for grouping.
|
||||
* @return string
|
||||
*/
|
||||
public function getTitleFirstLetter() {
|
||||
return $this->Title[0];
|
||||
}
|
||||
}
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Module extends DataObject
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Text'
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the first letter of the module title, used for grouping.
|
||||
* @return string
|
||||
*/
|
||||
public function getTitleFirstLetter()
|
||||
{
|
||||
return $this->Title[0];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The next step is to create a method or variable that will contain/return all the objects,
|
||||
sorted by title. For this example this will be a method on the `Page` class.
|
||||
|
||||
:::php
|
||||
class Page extends SiteTree {
|
||||
|
||||
// ...
|
||||
|
||||
/**
|
||||
* Returns all modules, sorted by their title.
|
||||
* @return GroupedList
|
||||
*/
|
||||
public function getGroupedModules() {
|
||||
return GroupedList::create(Module::get()->sort('Title'));
|
||||
}
|
||||
|
||||
}
|
||||
```php
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
use SilverStripe\ORM\GroupedList;
|
||||
|
||||
class Page extends SiteTree
|
||||
{
|
||||
|
||||
// ...
|
||||
|
||||
/**
|
||||
* Returns all modules, sorted by their title.
|
||||
* @return GroupedList
|
||||
*/
|
||||
public function getGroupedModules()
|
||||
{
|
||||
return GroupedList::create(Module::get()->sort('Title'));
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
The final step is to render this into a template. The `GroupedBy()` method breaks up the set into
|
||||
a number of sets, grouped by the field that is passed as the parameter.
|
||||
In this case, the `getTitleFirstLetter()` method defined earlier is used to break them up.
|
||||
|
||||
:::ss
|
||||
<%-- Modules list grouped by TitleFirstLetter --%>
|
||||
<h2>Modules</h2>
|
||||
<% loop $GroupedModules.GroupedBy(TitleFirstLetter) %>
|
||||
<h3>$TitleFirstLetter</h3>
|
||||
<ul>
|
||||
<% loop $Children %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% end_loop %>
|
||||
```ss
|
||||
<%-- Modules list grouped by TitleFirstLetter --%>
|
||||
<h2>Modules</h2>
|
||||
<% loop $GroupedModules.GroupedBy(TitleFirstLetter) %>
|
||||
<h3>$TitleFirstLetter</h3>
|
||||
<ul>
|
||||
<% loop $Children %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% end_loop %>
|
||||
```
|
||||
|
||||
## Grouping Sets By Month
|
||||
|
||||
|
@ -95,53 +108,63 @@ but grouping by its built-in `Created` property instead,
|
|||
which is automatically set when the record is first written to the database.
|
||||
This will have a method which returns the month it was posted in:
|
||||
|
||||
:::php
|
||||
class Module extends DataObject {
|
||||
|
||||
// ...
|
||||
|
||||
/**
|
||||
* Returns the month name this news item was posted in.
|
||||
* @return string
|
||||
*/
|
||||
public function getMonthCreated() {
|
||||
return date('F', strtotime($this->Created));
|
||||
}
|
||||
|
||||
}
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Module extends DataObject
|
||||
{
|
||||
|
||||
// ...
|
||||
|
||||
/**
|
||||
* Returns the month name this news item was posted in.
|
||||
* @return string
|
||||
*/
|
||||
public function getMonthCreated()
|
||||
{
|
||||
return date('F', strtotime($this->Created));
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
The next step is to create a method that will return all records that exist,
|
||||
sorted by month name from January to December. This can be accomplshed by sorting by the `Created` field:
|
||||
|
||||
:::php
|
||||
class Page extends SiteTree {
|
||||
|
||||
// ...
|
||||
|
||||
/**
|
||||
* Returns all news items, sorted by the month they were posted
|
||||
* @return GroupedList
|
||||
*/
|
||||
public function getGroupedModulesByDate() {
|
||||
return GroupedList::create(Module::get()->sort('Created'));
|
||||
}
|
||||
|
||||
}
|
||||
```php
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
use SilverStripe\ORM\GroupedList;
|
||||
|
||||
class Page extends SiteTree
|
||||
{
|
||||
|
||||
// ...
|
||||
|
||||
/**
|
||||
* Returns all news items, sorted by the month they were posted
|
||||
* @return GroupedList
|
||||
*/
|
||||
public function getGroupedModulesByDate()
|
||||
{
|
||||
return GroupedList::create(Module::get()->sort('Created'));
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
The final step is the render this into the template using the [GroupedList::GroupedBy()](api:SilverStripe\ORM\GroupedList::GroupedBy()) method.
|
||||
|
||||
:::ss
|
||||
// Modules list grouped by the Month Posted
|
||||
<h2>Modules</h2>
|
||||
<% loop $GroupedModulesByDate.GroupedBy(MonthCreated) %>
|
||||
<h3>$MonthCreated</h3>
|
||||
<ul>
|
||||
<% loop $Children %>
|
||||
<li>$Title ($Created.Nice)</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% end_loop %>
|
||||
|
||||
```ss
|
||||
// Modules list grouped by the Month Posted
|
||||
<h2>Modules</h2>
|
||||
<% loop $GroupedModulesByDate.GroupedBy(MonthCreated) %>
|
||||
<h3>$MonthCreated</h3>
|
||||
<ul>
|
||||
<% loop $Children %>
|
||||
<li>$Title ($Created.Nice)</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% end_loop %>
|
||||
```
|
||||
## Related
|
||||
|
||||
* [Howto: "Pagination"](/developer_guides/templates/how_tos/pagination)
|
||||
|
|
|
@ -11,33 +11,35 @@ An example of a SilverStripe template is below:
|
|||
|
||||
**mysite/templates/Page.ss**
|
||||
|
||||
:::ss
|
||||
<html>
|
||||
<head>
|
||||
<% base_tag %>
|
||||
<title>$Title</title>
|
||||
<% require themedCSS("screen") %>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Bob's Chicken Shack</h1>
|
||||
</header>
|
||||
```ss
|
||||
|
||||
<% with $CurrentMember %>
|
||||
<p>Welcome $FirstName $Surname.</p>
|
||||
<% end_with %>
|
||||
<html>
|
||||
<head>
|
||||
<% base_tag %>
|
||||
<title>$Title</title>
|
||||
<% require themedCSS("screen") %>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Bob's Chicken Shack</h1>
|
||||
</header>
|
||||
|
||||
<% if $Dishes %>
|
||||
<ul>
|
||||
<% loop $Dishes %>
|
||||
<li>$Title ($Price.Nice)</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% end_if %>
|
||||
<% with $CurrentMember %>
|
||||
<p>Welcome $FirstName $Surname.</p>
|
||||
<% end_with %>
|
||||
|
||||
<% include Footer %>
|
||||
</body>
|
||||
</html>
|
||||
<% if $Dishes %>
|
||||
<ul>
|
||||
<% loop $Dishes %>
|
||||
<li>$Title ($Price.Nice)</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% end_if %>
|
||||
|
||||
<% include Footer %>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
<div class="note">
|
||||
Templates can be used for more than HTML output. You can use them to output your data as JSON, XML, CSV or any other
|
||||
|
@ -66,17 +68,21 @@ Variables are placeholders that will be replaced with data from the [DataModel](
|
|||
[Controller](../controllers). Variables are prefixed with a `$` character. Variable names must start with an
|
||||
alphabetic character or underscore, with subsequent characters being alphanumeric or underscore:
|
||||
|
||||
:::ss
|
||||
$Title
|
||||
```ss
|
||||
|
||||
$Title
|
||||
```
|
||||
|
||||
This inserts the value of the Title database field of the page being displayed in place of `$Title`.
|
||||
|
||||
Variables can be chained together, and include arguments.
|
||||
|
||||
:::ss
|
||||
$Foo
|
||||
$Foo(param)
|
||||
$Foo.Bar
|
||||
```ss
|
||||
|
||||
$Foo
|
||||
$Foo(param)
|
||||
$Foo.Bar
|
||||
```
|
||||
|
||||
These variables will call a method / field on the object and insert the returned value as a string into the template.
|
||||
|
||||
|
@ -97,16 +103,19 @@ Variables can come from your database fields, or custom methods you define on yo
|
|||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
public function UsersIpAddress()
|
||||
```php
|
||||
public function UsersIpAddress()
|
||||
{
|
||||
return $this->getRequest()->getIP();
|
||||
}
|
||||
return $this->getRequest()->getIP();
|
||||
}
|
||||
```
|
||||
|
||||
**mysite/code/Page.ss**
|
||||
|
||||
:::html
|
||||
<p>You are coming from $UsersIpAddress.</p>
|
||||
```html
|
||||
|
||||
<p>You are coming from $UsersIpAddress.</p>
|
||||
```
|
||||
|
||||
<div class="node" markdown="1">
|
||||
Method names that begin with `get` will automatically be resolved when their prefix is excluded. For example, the above method call `$UsersIpAddress` would also invoke a method named `getUsersIpAddress()`.
|
||||
|
@ -119,29 +128,34 @@ record and any subclasses of those two.
|
|||
|
||||
**mysite/code/Layout/Page.ss**
|
||||
|
||||
:::ss
|
||||
$Title
|
||||
// returns the page `Title` property
|
||||
```ss
|
||||
|
||||
$Content
|
||||
// returns the page `Content` property
|
||||
$Title
|
||||
// returns the page `Title` property
|
||||
|
||||
$Content
|
||||
// returns the page `Content` property
|
||||
```
|
||||
|
||||
## Conditional Logic
|
||||
|
||||
The simplest conditional block is to check for the presence of a value (does not equal 0, null, false).
|
||||
|
||||
:::ss
|
||||
<% if $CurrentMember %>
|
||||
<p>You are logged in as $CurrentMember.FirstName $CurrentMember.Surname.</p>
|
||||
<% end_if %>
|
||||
```ss
|
||||
|
||||
<% if $CurrentMember %>
|
||||
<p>You are logged in as $CurrentMember.FirstName $CurrentMember.Surname.</p>
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
A conditional can also check for a value other than falsy.
|
||||
|
||||
:::ss
|
||||
<% if $MyDinner == "kipper" %>
|
||||
Yummy, kipper for tea.
|
||||
<% end_if %>
|
||||
```ss
|
||||
|
||||
<% if $MyDinner == "kipper" %>
|
||||
Yummy, kipper for tea.
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
When inside template tags variables should have a '$' prefix, and literals should have quotes.
|
||||
|
@ -149,32 +163,38 @@ When inside template tags variables should have a '$' prefix, and literals shoul
|
|||
|
||||
Conditionals can also provide the `else` case.
|
||||
|
||||
:::ss
|
||||
<% if $MyDinner == "kipper" %>
|
||||
Yummy, kipper for tea
|
||||
<% else %>
|
||||
I wish I could have kipper :-(
|
||||
<% end_if %>
|
||||
```ss
|
||||
|
||||
<% if $MyDinner == "kipper" %>
|
||||
Yummy, kipper for tea
|
||||
<% else %>
|
||||
I wish I could have kipper :-(
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
`else_if` commands can be used to handle multiple `if` statements.
|
||||
|
||||
:::ss
|
||||
<% if $MyDinner == "quiche" %>
|
||||
Real men don't eat quiche
|
||||
<% else_if $MyDinner == $YourDinner %>
|
||||
We both have good taste
|
||||
<% else %>
|
||||
Can I have some of your chips?
|
||||
<% end_if %>
|
||||
```ss
|
||||
|
||||
<% if $MyDinner == "quiche" %>
|
||||
Real men don't eat quiche
|
||||
<% else_if $MyDinner == $YourDinner %>
|
||||
We both have good taste
|
||||
<% else %>
|
||||
Can I have some of your chips?
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
### Negation
|
||||
|
||||
The inverse of `<% if %>` is `<% if not %>`.
|
||||
|
||||
:::ss
|
||||
<% if not $DinnerInOven %>
|
||||
I'm going out for dinner tonight.
|
||||
<% end_if %>
|
||||
```ss
|
||||
|
||||
<% if not $DinnerInOven %>
|
||||
I'm going out for dinner tonight.
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
### Boolean Logic
|
||||
|
||||
|
@ -182,27 +202,32 @@ Multiple checks can be done using `||`, `or`, `&&` or `and`.
|
|||
|
||||
If *either* of the conditions is true.
|
||||
|
||||
:::ss
|
||||
<% if $MyDinner == "kipper" || $MyDinner == "salmon" %>
|
||||
yummy, fish for tea
|
||||
<% end_if %>
|
||||
```ss
|
||||
|
||||
<% if $MyDinner == "kipper" || $MyDinner == "salmon" %>
|
||||
yummy, fish for tea
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
If *both* of the conditions are true.
|
||||
|
||||
:::ss
|
||||
<% if $MyDinner == "quiche" && $YourDinner == "kipper" %>
|
||||
Lets swap dinners
|
||||
<% end_if %>
|
||||
```ss
|
||||
|
||||
<% if $MyDinner == "quiche" && $YourDinner == "kipper" %>
|
||||
Lets swap dinners
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
### Inequalities
|
||||
|
||||
You can use inequalities like `<`, `<=`, `>`, `>=` to compare numbers.
|
||||
|
||||
:::ss
|
||||
<% if $Number >= "5" && $Number <= "10" %>
|
||||
Number between 5 and 10
|
||||
<% end_if %>
|
||||
```ss
|
||||
|
||||
<% if $Number >= "5" && $Number <= "10" %>
|
||||
Number between 5 and 10
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
## Includes
|
||||
|
||||
|
@ -210,46 +235,55 @@ Within SilverStripe templates we have the ability to include other templates usi
|
|||
will be searched for using the same filename look-up rules as a regular template. However in the case of the include tag
|
||||
an additional `Includes` directory will be inserted into the resolved path just prior to the filename.
|
||||
|
||||
:::ss
|
||||
<% include SideBar %> <!-- chooses templates/Includes/Sidebar.ss -->
|
||||
<% include MyNamespace/SideBar %> <!-- chooses templates/MyNamespace/Includes/Sidebar.ss -->
|
||||
```ss
|
||||
|
||||
<% include SideBar %> <!-- chooses templates/Includes/Sidebar.ss -->
|
||||
<% include MyNamespace/SideBar %> <!-- chooses templates/MyNamespace/Includes/Sidebar.ss -->
|
||||
```
|
||||
|
||||
When using subfolders in your template structure
|
||||
(e.g. to fit with namespaces in your PHP class structure), the `Includes/` folder needs to be innermost.
|
||||
|
||||
:::ss
|
||||
<% include MyNamespace/SideBar %> <!-- chooses templates/MyNamespace/Includes/Sidebar.ss -->
|
||||
```ss
|
||||
|
||||
<% include MyNamespace/SideBar %> <!-- chooses templates/MyNamespace/Includes/Sidebar.ss -->
|
||||
```
|
||||
|
||||
The `include` tag can be particularly helpful for nested functionality and breaking large templates up. In this example,
|
||||
the include only happens if the user is logged in.
|
||||
|
||||
:::ss
|
||||
<% if $CurrentMember %>
|
||||
<% include MembersOnlyInclude %>
|
||||
<% end_if %>
|
||||
```ss
|
||||
|
||||
<% if $CurrentMember %>
|
||||
<% include MembersOnlyInclude %>
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
Includes can't directly access the parent scope when the include is included. However you can pass arguments to the
|
||||
include.
|
||||
|
||||
:::ss
|
||||
<% with $CurrentMember %>
|
||||
<% include MemberDetails Top=$Top, Name=$Name %>
|
||||
<% end_with %>
|
||||
```ss
|
||||
|
||||
<% with $CurrentMember %>
|
||||
<% include MemberDetails Top=$Top, Name=$Name %>
|
||||
<% end_with %>
|
||||
```
|
||||
|
||||
## Looping Over Lists
|
||||
|
||||
The `<% loop %>` tag is used to iterate or loop over a collection of items such as [DataList](api:SilverStripe\ORM\DataList) or a [ArrayList](api:SilverStripe\ORM\ArrayList)
|
||||
collection.
|
||||
|
||||
:::ss
|
||||
<h1>Children of $Title</h1>
|
||||
```ss
|
||||
|
||||
<ul>
|
||||
<% loop $Children %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<h1>Children of $Title</h1>
|
||||
|
||||
<ul>
|
||||
<% loop $Children %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```
|
||||
|
||||
This snippet loops over the children of a page, and generates an unordered list showing the `Title` property from each
|
||||
page.
|
||||
|
@ -269,48 +303,58 @@ templates can call [DataList](api:SilverStripe\ORM\DataList) methods.
|
|||
|
||||
Sorting the list by a given field.
|
||||
|
||||
:::ss
|
||||
<ul>
|
||||
<% loop $Children.Sort(Title, ASC) %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```ss
|
||||
|
||||
<ul>
|
||||
<% loop $Children.Sort(Title, ASC) %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Limiting the number of items displayed.
|
||||
|
||||
:::ss
|
||||
<ul>
|
||||
<% loop $Children.Limit(10) %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```ss
|
||||
|
||||
<ul>
|
||||
<% loop $Children.Limit(10) %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Reversing the loop.
|
||||
|
||||
:::ss
|
||||
<ul>
|
||||
<% loop $Children.Reverse %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```ss
|
||||
|
||||
<ul>
|
||||
<% loop $Children.Reverse %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Filtering the loop.
|
||||
|
||||
:::ss
|
||||
<ul>
|
||||
<% loop $Children.Filter('School', 'College') %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```ss
|
||||
|
||||
<ul>
|
||||
<% loop $Children.Filter('School', 'College') %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Methods can also be chained.
|
||||
|
||||
:::ss
|
||||
<ul>
|
||||
<% loop $Children.Filter('School', 'College').Sort(Score, DESC) %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```ss
|
||||
|
||||
<ul>
|
||||
<% loop $Children.Filter('School', 'College').Sort(Score, DESC) %>
|
||||
<li>$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```
|
||||
|
||||
### Position Indicators
|
||||
|
||||
|
@ -327,16 +371,18 @@ iteration.
|
|||
Last item defaults to 1, but can be passed as a parameter.
|
||||
* `$TotalItems`: Number of items in the list (integer).
|
||||
|
||||
:::ss
|
||||
<ul>
|
||||
<% loop $Children.Reverse %>
|
||||
<% if First %>
|
||||
<li>My Favourite</li>
|
||||
<% end_if %>
|
||||
```ss
|
||||
|
||||
<li class="$EvenOdd">Child $Pos of $TotalItems - $Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<ul>
|
||||
<% loop $Children.Reverse %>
|
||||
<% if First %>
|
||||
<li>My Favourite</li>
|
||||
<% end_if %>
|
||||
|
||||
<li class="$EvenOdd">Child $Pos of $TotalItems - $Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```
|
||||
|
||||
<div class="info" markdown="1">
|
||||
A common task is to paginate your lists. See the [Pagination](how_tos/pagination) how to for a tutorial on adding
|
||||
|
@ -347,20 +393,22 @@ pagination.
|
|||
|
||||
$Modulus and $MultipleOf can help to build column and grid layouts.
|
||||
|
||||
:::ss
|
||||
// returns an int
|
||||
$Modulus(value, offset)
|
||||
```ss
|
||||
|
||||
// returns a boolean.
|
||||
$MultipleOf(factor, offset)
|
||||
// returns an int
|
||||
$Modulus(value, offset)
|
||||
|
||||
<% loop $Children %>
|
||||
<div class="column-{$Modulus(4)}">
|
||||
...
|
||||
</div>
|
||||
<% end_loop %>
|
||||
// returns a boolean.
|
||||
$MultipleOf(factor, offset)
|
||||
|
||||
// returns <div class="column-3">, <div class="column-2">,
|
||||
<% loop $Children %>
|
||||
<div class="column-{$Modulus(4)}">
|
||||
...
|
||||
</div>
|
||||
<% end_loop %>
|
||||
|
||||
// returns <div class="column-3">, <div class="column-2">,
|
||||
```
|
||||
|
||||
<div class="hint" markdown="1">
|
||||
`$Modulus` is useful for floated grid CSS layouts. If you want 3 rows across, put $Modulus(3) as a class and add a
|
||||
|
@ -370,33 +418,40 @@ $Modulus and $MultipleOf can help to build column and grid layouts.
|
|||
$MultipleOf(value, offset) can also be utilized to build column and grid layouts. In this case we want to add a `<br>`
|
||||
after every 3rd item.
|
||||
|
||||
:::ss
|
||||
<% loop $Children %>
|
||||
<% if $MultipleOf(3) %>
|
||||
<br>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
```ss
|
||||
|
||||
<% loop $Children %>
|
||||
<% if $MultipleOf(3) %>
|
||||
<br>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
```
|
||||
|
||||
### Escaping
|
||||
|
||||
Sometimes you will have template tags which need to roll into one another. Use `{}` to contain variables.
|
||||
|
||||
:::ss
|
||||
$Foopx // will returns "" (as it looks for a `Foopx` value)
|
||||
{$Foo}px // returns "3px" (CORRECT)
|
||||
```ss
|
||||
|
||||
$Foopx // will returns "" (as it looks for a `Foopx` value)
|
||||
{$Foo}px // returns "3px" (CORRECT)
|
||||
```
|
||||
|
||||
Or when having a `$` sign in front of the variable such as displaying money.
|
||||
|
||||
:::ss
|
||||
$$Foo // returns ""
|
||||
${$Foo} // returns "$3"
|
||||
```ss
|
||||
|
||||
$$Foo // returns ""
|
||||
${$Foo} // returns "$3"
|
||||
```
|
||||
|
||||
You can also use a backslash to escape the name of the variable, such as:
|
||||
|
||||
:::ss
|
||||
$Foo // returns "3"
|
||||
\$Foo // returns "$Foo"
|
||||
```ss
|
||||
|
||||
$Foo // returns "3"
|
||||
\$Foo // returns "$Foo"
|
||||
```
|
||||
|
||||
<div class="hint" markdown="1">
|
||||
For more information on formatting and casting variables see [Formating, Modifying and Casting Variables](casting)
|
||||
|
@ -424,16 +479,18 @@ classes of the current scope object, and any [Extension](api:SilverStripe\Core\E
|
|||
|
||||
When in a particular scope, `$Up` takes the scope back to the previous level.
|
||||
|
||||
:::ss
|
||||
<h1>Children of '$Title'</h1>
|
||||
```ss
|
||||
|
||||
<% loop $Children %>
|
||||
<p>Page '$Title' is a child of '$Up.Title'</p>
|
||||
|
||||
<% loop $Children %>
|
||||
<p>Page '$Title' is a grandchild of '$Up.Up.Title'</p>
|
||||
<% end_loop %>
|
||||
<% end_loop %>
|
||||
<h1>Children of '$Title'</h1>
|
||||
|
||||
<% loop $Children %>
|
||||
<p>Page '$Title' is a child of '$Up.Title'</p>
|
||||
|
||||
<% loop $Children %>
|
||||
<p>Page '$Title' is a grandchild of '$Up.Up.Title'</p>
|
||||
<% end_loop %>
|
||||
<% end_loop %>
|
||||
```
|
||||
|
||||
Given the following structure, it will output the text.
|
||||
|
||||
|
@ -455,42 +512,50 @@ Given the following structure, it will output the text.
|
|||
Additional selectors implicitely change the scope so you need to put additional `$Up` to get what you expect.
|
||||
</div>
|
||||
|
||||
:::ss
|
||||
<h1>Children of '$Title'</h1>
|
||||
<% loop $Children.Sort('Title').First %>
|
||||
<%-- We have two additional selectors in the loop expression so... --%>
|
||||
<p>Page '$Title' is a child of '$Up.Up.Up.Title'</p>
|
||||
<% end_loop %>
|
||||
```ss
|
||||
|
||||
<h1>Children of '$Title'</h1>
|
||||
<% loop $Children.Sort('Title').First %>
|
||||
<%-- We have two additional selectors in the loop expression so... --%>
|
||||
<p>Page '$Title' is a child of '$Up.Up.Up.Title'</p>
|
||||
<% end_loop %>
|
||||
```
|
||||
|
||||
#### Top
|
||||
|
||||
While `$Up` provides us a way to go up one level of scope, `$Top` is a shortcut to jump to the top most scope of the
|
||||
page. The previous example could be rewritten to use the following syntax.
|
||||
|
||||
:::ss
|
||||
<h1>Children of '$Title'</h1>
|
||||
```ss
|
||||
|
||||
<% loop $Children %>
|
||||
<p>Page '$Title' is a child of '$Top.Title'</p>
|
||||
|
||||
<% loop $Children %>
|
||||
<p>Page '$Title' is a grandchild of '$Top.Title'</p>
|
||||
<% end_loop %>
|
||||
<% end_loop %>
|
||||
<h1>Children of '$Title'</h1>
|
||||
|
||||
<% loop $Children %>
|
||||
<p>Page '$Title' is a child of '$Top.Title'</p>
|
||||
|
||||
<% loop $Children %>
|
||||
<p>Page '$Title' is a grandchild of '$Top.Title'</p>
|
||||
<% end_loop %>
|
||||
<% end_loop %>
|
||||
```
|
||||
|
||||
### With
|
||||
|
||||
The `<% with %>` tag lets you change into a new scope. Consider the following example:
|
||||
|
||||
:::ss
|
||||
<% with $CurrentMember %>
|
||||
Hello, $FirstName, welcome back. Your current balance is $Balance.
|
||||
<% end_with %>
|
||||
```ss
|
||||
|
||||
<% with $CurrentMember %>
|
||||
Hello, $FirstName, welcome back. Your current balance is $Balance.
|
||||
<% end_with %>
|
||||
```
|
||||
|
||||
This is functionalty the same as the following:
|
||||
|
||||
:::ss
|
||||
Hello, $CurrentMember.FirstName, welcome back. Your current balance is $CurrentMember.Balance
|
||||
```ss
|
||||
|
||||
Hello, $CurrentMember.FirstName, welcome back. Your current balance is $CurrentMember.Balance
|
||||
```
|
||||
|
||||
Notice that the first example is much tidier, as it removes the repeated use of the `$CurrentMember` accessor.
|
||||
|
||||
|
@ -502,22 +567,27 @@ refer directly to properties and methods of the [Member](api:SilverStripe\Securi
|
|||
|
||||
`$Me` outputs the current object in scope. This will call the `forTemplate` of the object.
|
||||
|
||||
:::ss
|
||||
$Me
|
||||
```ss
|
||||
|
||||
$Me
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
Using standard HTML comments is supported. These comments will be included in the published site.
|
||||
|
||||
:::ss
|
||||
$EditForm <!-- Some public comment about the form -->
|
||||
```ss
|
||||
|
||||
$EditForm <!-- Some public comment about the form -->
|
||||
```
|
||||
|
||||
However you can also use special SilverStripe comments which will be stripped out of the published site. This is useful
|
||||
for adding notes for other developers but for things you don't want published in the public html.
|
||||
|
||||
:::ss
|
||||
$EditForm <%-- Some hidden comment about the form --%>
|
||||
```ss
|
||||
|
||||
$EditForm <%-- Some hidden comment about the form --%>
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
|
@ -530,4 +600,4 @@ for adding notes for other developers but for things you don't want published in
|
|||
## API Documentation
|
||||
|
||||
* [SSViewer](api:SilverStripe\View\SSViewer)
|
||||
* [SS_TemplateManifest](api:SS_TemplateManifest)
|
||||
* [ThemeManifest](api:SilverStripe\View\ThemeManifest)
|
||||
|
|
|
@ -30,12 +30,14 @@ functionality may not be included.
|
|||
|
||||
## Base Tag
|
||||
|
||||
:::ss
|
||||
<head>
|
||||
<% base_tag %>
|
||||
```ss
|
||||
|
||||
..
|
||||
</head>
|
||||
<head>
|
||||
<% base_tag %>
|
||||
|
||||
..
|
||||
</head>
|
||||
```
|
||||
|
||||
The `<% base_tag %>` placeholder is replaced with the HTML base element. Relative links within a document (such as <img
|
||||
src="someimage.jpg" />) will become relative to the URI specified in the base tag. This ensures the browser knows where
|
||||
|
@ -49,19 +51,20 @@ A `<% base_tag %>` is nearly always required or assumed by SilverStripe to exist
|
|||
|
||||
## CurrentMember
|
||||
|
||||
Returns the currently logged in [Member](api:SilverStripe\Security\Member) instance, if there is one logged in.
|
||||
|
||||
:::ss
|
||||
<% if $CurrentMember %>
|
||||
Welcome Back, $CurrentMember.FirstName
|
||||
<% end_if %>
|
||||
Returns the currently logged in [Member](api:SilverStripe\Security\Member) instance, if there is one logged in.```ss
|
||||
|
||||
<% if $CurrentMember %>
|
||||
Welcome Back, $CurrentMember.FirstName
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
## Title and Menu Title
|
||||
|
||||
:::ss
|
||||
$Title
|
||||
$MenuTitle
|
||||
```ss
|
||||
|
||||
$Title
|
||||
$MenuTitle
|
||||
```
|
||||
|
||||
Most objects within SilverStripe will respond to `$Title` (i.e they should have a `Title` database field or at least a
|
||||
`getTitle()` method).
|
||||
|
@ -75,8 +78,10 @@ If `MenuTitle` is left blank by the CMS author, it'll just default to the value
|
|||
|
||||
## Page Content
|
||||
|
||||
:::ss
|
||||
$Content
|
||||
```ss
|
||||
|
||||
$Content
|
||||
```
|
||||
|
||||
It returns the database content of the `Content` property. With the CMS Module, this is the value of the WYSIWYG editor
|
||||
but it is also the standard for any object that has a body of content to output.
|
||||
|
@ -97,8 +102,10 @@ more details).
|
|||
web pages. You'll need to install it via `composer`.
|
||||
</div>
|
||||
|
||||
:::ss
|
||||
$SiteConfig.Title
|
||||
```ss
|
||||
|
||||
$SiteConfig.Title
|
||||
```
|
||||
|
||||
The [SiteConfig](../configuration/siteconfig) object allows content authors to modify global data in the CMS, rather
|
||||
than PHP code. By default, this includes a Website title and a Tagline.
|
||||
|
@ -119,73 +126,86 @@ If you don’t want to include the title tag use `$MetaTags(false)`.
|
|||
|
||||
By default `$MetaTags` renders:
|
||||
|
||||
:::ss
|
||||
<title>Title of the Page</title>
|
||||
<meta name="generator" http-equiv="generator" content="SilverStripe 3.0" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
```ss
|
||||
|
||||
`$MetaTags(false)` will render
|
||||
|
||||
:::ss
|
||||
<meta name="generator" http-equiv="generator" content="SilverStripe 3.0" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<title>Title of the Page</title>
|
||||
<meta name="generator" http-equiv="generator" content="SilverStripe 3.0" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
```
|
||||
|
||||
`$MetaTags(false)` will render```ss
|
||||
|
||||
<meta name="generator" http-equiv="generator" content="SilverStripe 3.0" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
```
|
||||
|
||||
If using `$MetaTags(false)` we can provide a more custom `title`.
|
||||
|
||||
:::ss
|
||||
$MetaTags(false)
|
||||
<title>$Title - Bob's Fantasy Football</title>
|
||||
```ss
|
||||
|
||||
$MetaTags(false)
|
||||
<title>$Title - Bob's Fantasy Football</title>
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
:::ss
|
||||
<a href="$Link">..</a>
|
||||
```ss
|
||||
|
||||
<a href="$Link">..</a>
|
||||
```
|
||||
|
||||
All objects that could be accessible in SilverStripe should define a `Link` method and an `AbsoluteLink` method. Link
|
||||
returns the relative URL for the object and `AbsoluteLink` outputs your full website address along with the relative
|
||||
link.
|
||||
|
||||
:::ss
|
||||
$Link
|
||||
<!-- returns /about-us/offices/ -->
|
||||
```ss
|
||||
|
||||
$AbsoluteLink
|
||||
<!-- returns http://yoursite.com/about-us/offices/ -->
|
||||
$Link
|
||||
<!-- returns /about-us/offices/ -->
|
||||
|
||||
$AbsoluteLink
|
||||
<!-- returns http://yoursite.com/about-us/offices/ -->
|
||||
```
|
||||
|
||||
### Linking Modes
|
||||
|
||||
:::ss
|
||||
$isSection
|
||||
$isCurrent
|
||||
```ss
|
||||
|
||||
$isSection
|
||||
$isCurrent
|
||||
```
|
||||
|
||||
When looping over a list of `SiteTree` instances through a `<% loop $Menu %>` or `<% loop $Children %>`, `$isSection` and `$isCurrent`
|
||||
will return true or false based on page being looped over relative to the currently viewed page.
|
||||
|
||||
For instance, to only show the menu item linked if it's the current one:
|
||||
|
||||
:::ss
|
||||
<% if $isCurrent %>
|
||||
$Title
|
||||
<% else %>
|
||||
<a href="$Link">$Title</a>
|
||||
<% end_if %>
|
||||
|
||||
```ss
|
||||
|
||||
<% if $isCurrent %>
|
||||
$Title
|
||||
<% else %>
|
||||
<a href="$Link">$Title</a>
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
An example for checking for `current` or `section` is as follows:
|
||||
|
||||
:::ss
|
||||
```ss
|
||||
|
||||
<a class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>" href="$Link">$MenuTitle</a>
|
||||
|
||||
|
||||
```
|
||||
|
||||
**Additional Utility Method**
|
||||
|
||||
* `$InSection(page-url)`: This if block will pass if we're currently on the page-url page or one of its children.
|
||||
|
||||
:::ss
|
||||
<% if $InSection(about-us) %>
|
||||
<p>You are viewing the about us section</p>
|
||||
<% end_if %>
|
||||
```ss
|
||||
|
||||
<% if $InSection(about-us) %>
|
||||
<p>You are viewing the about us section</p>
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
### URLSegment
|
||||
|
||||
|
@ -193,12 +213,14 @@ This returns the part of the URL of the page you're currently on. For example on
|
|||
`URLSegment` will be `offices`. `URLSegment` cannot be used to generate a link since it does not output the full path.
|
||||
It can be used within templates to generate anchors or other CSS classes.
|
||||
|
||||
:::ss
|
||||
<div id="section-$URLSegment">
|
||||
```ss
|
||||
|
||||
</div>
|
||||
<div id="section-$URLSegment">
|
||||
|
||||
<!-- returns <div id="section-offices"> -->
|
||||
</div>
|
||||
|
||||
<!-- returns <div id="section-offices"> -->
|
||||
```
|
||||
|
||||
## ClassName
|
||||
|
||||
|
@ -206,17 +228,21 @@ Returns the class of the current object in [scope](syntax#scope) such as `Page`
|
|||
handy for a number of uses. A common use case is to add to your `<body>` tag to influence CSS styles and JavaScript
|
||||
behavior based on the page type used:
|
||||
|
||||
:::ss
|
||||
<body class="$ClassName">
|
||||
```ss
|
||||
|
||||
<!-- returns <body class="HomePage">, <body class="BlogPage"> -->
|
||||
<body class="$ClassName">
|
||||
|
||||
<!-- returns <body class="HomePage">, <body class="BlogPage"> -->
|
||||
```
|
||||
|
||||
## Children Loops
|
||||
|
||||
:::ss
|
||||
<% loop $Children %>
|
||||
```ss
|
||||
|
||||
<% end_loop %>
|
||||
<% loop $Children %>
|
||||
|
||||
<% end_loop %>
|
||||
```
|
||||
|
||||
Will loop over all Children records of the current object context. Children are pages that sit under the current page in
|
||||
the `CMS` or a custom list of data. This originates in the `Versioned` extension's `getChildren` method.
|
||||
|
@ -228,10 +254,12 @@ context.
|
|||
|
||||
### ChildrenOf
|
||||
|
||||
:::ss
|
||||
<% loop $ChildrenOf(<my-page-url>) %>
|
||||
```ss
|
||||
|
||||
<% end_loop %>
|
||||
<% loop $ChildrenOf(<my-page-url>) %>
|
||||
|
||||
<% end_loop %>
|
||||
```
|
||||
|
||||
Will create a list of the children of the given page, as identified by its `URLSegment` value. This can come in handy
|
||||
because it's not dependent on the context of the current page. For example, it would allow you to list all staff member
|
||||
|
@ -244,18 +272,21 @@ Content authors have the ability to hide pages from menus by un-selecting the `S
|
|||
This option will be honored by `<% loop $Children %>` and `<% loop $Menu %>` however if you want to ignore the user
|
||||
preference, `AllChildren` does not filter by `ShowInMenus`.
|
||||
|
||||
:::ss
|
||||
<% loop $AllChildren %>
|
||||
...
|
||||
<% end_loop %>
|
||||
```ss
|
||||
|
||||
<% loop $AllChildren %>
|
||||
...
|
||||
<% end_loop %>
|
||||
```
|
||||
|
||||
### Menu Loops
|
||||
|
||||
:::ss
|
||||
<% loop $Menu(1) %>
|
||||
...
|
||||
<% end_loop %>
|
||||
```ss
|
||||
|
||||
<% loop $Menu(1) %>
|
||||
...
|
||||
<% end_loop %>
|
||||
```
|
||||
|
||||
`$Menu(1)` returns the top-level menu of the website. You can also create a sub-menu using `$Menu(2)`, and so forth.
|
||||
|
||||
|
@ -265,10 +296,12 @@ Pages with the `ShowInMenus` property set to `false` will be filtered out.
|
|||
|
||||
## Access to a specific Page
|
||||
|
||||
:::ss
|
||||
<% with $Page(my-page) %>
|
||||
$Title
|
||||
<% end_with %>
|
||||
```ss
|
||||
|
||||
<% with $Page(my-page) %>
|
||||
$Title
|
||||
<% end_with %>
|
||||
```
|
||||
|
||||
Page will return a single page from site, looking it up by URL.
|
||||
|
||||
|
@ -276,10 +309,12 @@ Page will return a single page from site, looking it up by URL.
|
|||
|
||||
### Level
|
||||
|
||||
:::ss
|
||||
<% with $Level(1) %>
|
||||
$Title
|
||||
<% end_with %>
|
||||
```ss
|
||||
|
||||
<% with $Level(1) %>
|
||||
$Title
|
||||
<% end_with %>
|
||||
```
|
||||
|
||||
Will return a page in the current path, at the level specified by the numbers. It is based on the current page context,
|
||||
looking back through its parent pages. `Level(1)` being the top most level.
|
||||
|
@ -292,15 +327,16 @@ For example, imagine you're on the "bob marley" page, which is three levels in:
|
|||
|
||||
### Parent
|
||||
|
||||
:::ss
|
||||
<!-- given we're on 'Bob Marley' in "about us > staff > bob marley" -->
|
||||
|
||||
$Parent.Title
|
||||
<!-- returns 'staff' -->
|
||||
```ss
|
||||
|
||||
$Parent.Parent.Title
|
||||
<!-- returns 'about us' -->
|
||||
<!-- given we're on 'Bob Marley' in "about us > staff > bob marley" -->
|
||||
|
||||
$Parent.Title
|
||||
<!-- returns 'staff' -->
|
||||
|
||||
$Parent.Parent.Title
|
||||
<!-- returns 'about us' -->
|
||||
```
|
||||
|
||||
## Navigating Scope
|
||||
|
||||
|
@ -314,17 +350,21 @@ for website users.
|
|||
While you can achieve breadcrumbs through the `$Level(<level>)` control manually, there's a nicer shortcut: The
|
||||
`$Breadcrumbs` variable.
|
||||
|
||||
:::ss
|
||||
$Breadcrumbs
|
||||
```ss
|
||||
|
||||
$Breadcrumbs
|
||||
```
|
||||
|
||||
By default, it uses the template defined in `cms/templates/BreadcrumbsTemplate.ss`
|
||||
|
||||
:::ss
|
||||
<% if $Pages %>
|
||||
<% loop $Pages %>
|
||||
<% if $Last %>$Title.XML<% else %><a href="$Link">$MenuTitle.XML</a> »<% end_if %>
|
||||
<% end_loop %>
|
||||
<% end_if %>
|
||||
```ss
|
||||
|
||||
<% if $Pages %>
|
||||
<% loop $Pages %>
|
||||
<% if $Last %>$Title.XML<% else %><a href="$Link">$MenuTitle.XML</a> »<% end_if %>
|
||||
<% end_loop %>
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
<div class="info" markdown="1">
|
||||
To customise the markup that the `$Breadcrumbs` generates, copy `cms/templates/BreadcrumbsTemplate.ss` to
|
||||
|
@ -333,8 +373,10 @@ To customise the markup that the `$Breadcrumbs` generates, copy `cms/templates/B
|
|||
|
||||
## Forms
|
||||
|
||||
:::ss
|
||||
$Form
|
||||
```ss
|
||||
|
||||
$Form
|
||||
```
|
||||
|
||||
A page will normally contain some content and potentially a form of some kind. For example, the log-in page has a the
|
||||
SilverStripe log-in form. If you are on such a page, the `$Form` variable will contain the HTML content of the form.
|
||||
|
|
|
@ -15,10 +15,12 @@ The `Requirements` class can work with arbitrary file paths.
|
|||
|
||||
**<my-module-dir>/templates/SomeTemplate.ss**
|
||||
|
||||
:::ss
|
||||
<% require css("<my-module-dir>/css/some_file.css") %>
|
||||
<% require themedCSS("some_themed_file") %>
|
||||
<% require javascript("<my-module-dir>/javascript/some_file.js") %>
|
||||
```ss
|
||||
|
||||
<% require css("<my-module-dir>/css/some_file.css") %>
|
||||
<% require themedCSS("some_themed_file") %>
|
||||
<% require javascript("<my-module-dir>/javascript/some_file.js") %>
|
||||
```
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
Requiring assets from the template is restricted compared to the PHP API.
|
||||
|
@ -30,8 +32,6 @@ It is common practice to include most Requirements either in the *init()*-method
|
|||
as close to rendering as possible (e.g. in [FormField](api:SilverStripe\Forms\FormField)).
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\View\Requirements;
|
||||
|
||||
|
@ -71,7 +71,7 @@ JavaScript in a separate file and instead load, via search and replace, several
|
|||
|
||||
```php
|
||||
$vars = [
|
||||
"MemberID" => Member::currentUserID(),
|
||||
"MemberID" => Security::getCurrentUser()->ID,
|
||||
];
|
||||
|
||||
Requirements::javascriptTemplate("<my-module-dir>/javascript/some_file.js", $vars);
|
||||
|
@ -97,11 +97,11 @@ You can also use the second argument to add the 'async' and/or 'defer attributes
|
|||
|
||||
```php
|
||||
Requirements::javascript(
|
||||
"<my-module-dir>/javascript/some_file.js",
|
||||
[
|
||||
"async" => true,
|
||||
"defer" => true,
|
||||
]
|
||||
"<my-module-dir>/javascript/some_file.js",
|
||||
[
|
||||
"async" => true,
|
||||
"defer" => true,
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
|
@ -281,13 +281,13 @@ class MyMinifier implements Requirements_Minifier
|
|||
* @param string $type Either js or css
|
||||
* @param string $filename Name of file to display in case of error
|
||||
* @return string minified content
|
||||
*/
|
||||
public function minify ($content, $type, $fileName)
|
||||
{
|
||||
// Minify $content;
|
||||
*/
|
||||
public function minify ($content, $type, $fileName)
|
||||
{
|
||||
// Minify $content;
|
||||
|
||||
return $minifiedContent;
|
||||
}
|
||||
return $minifiedContent;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -22,14 +22,15 @@ instance with a template name or an array of templates to render.
|
|||
**mysite/code/Page.php**
|
||||
|
||||
```php
|
||||
$arrayData = new SilverStripe/View/ArrayData(array(
|
||||
$arrayData = new SilverStripe\View\ArrayData([
|
||||
'Name' => 'John',
|
||||
'Role' => 'Head Coach'
|
||||
));
|
||||
]);
|
||||
|
||||
echo $arrayData->renderWith('Coach_Message');
|
||||
|
||||
// returns "<strong>John</strong> is the Head Coach on our team."
|
||||
|
||||
```
|
||||
|
||||
<div class="info" markdown="1">
|
||||
|
@ -38,22 +39,21 @@ includes [Controller](api:SilverStripe\Control\Controller), [FormField](api:Silv
|
|||
</div>
|
||||
|
||||
```php
|
||||
$controller->renderWith(array('MyController', 'MyBaseController'));
|
||||
$controller->renderWith(['MyController', 'MyBaseController']);
|
||||
|
||||
SilverStripe\Security\Security::getCurrentUser()->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
|
||||
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
private static $allowed_actions = array('iwantmyajax');
|
||||
private static $allowed_actions = ['iwantmyajax'];
|
||||
|
||||
public function iwantmyajax()
|
||||
{
|
||||
|
@ -64,13 +64,13 @@ class PageController extends ContentController
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
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
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\Control\Director;
|
||||
|
@ -83,18 +83,19 @@ class PageController extends ContentController
|
|||
{
|
||||
if (Director::is_ajax()) {
|
||||
$experience = new ArrayList();
|
||||
$experience->push(new ArrayData(array(
|
||||
$experience->push(new ArrayData([
|
||||
'Title' => 'First Job'
|
||||
)));
|
||||
]));
|
||||
|
||||
return $this->customise(new ArrayData(array(
|
||||
return $this->customise(new ArrayData([
|
||||
'Name' => 'John',
|
||||
'Role' => 'Head Coach',
|
||||
'Experience' => $experience
|
||||
)))->renderWith('AjaxTemplate');
|
||||
]))->renderWith('AjaxTemplate');
|
||||
} else {
|
||||
return $this->httpError(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
|
|
@ -16,11 +16,11 @@ name in the `mysite/templates/email` folder or in the `themes/your_theme/templat
|
|||
|
||||
**mysite/templates/email/GenericEmail.ss**
|
||||
|
||||
:::ss
|
||||
```ss
|
||||
$Body
|
||||
|
||||
<p>Thanks from Bob's Fantasy Football League.</p>
|
||||
|
||||
```
|
||||
All emails going out of our application will have the footer `Thanks from Bob's Fantasy Football Leaguee` added.
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
|
@ -30,24 +30,14 @@ As we've added a new file, make sure you flush your SilverStripe cache by visiti
|
|||
Template inheritance works on more than email templates. All files within the `templates` directory including `includes`,
|
||||
`layout` or anything else from core (or add-on's) template directory can be overridden by being located inside your
|
||||
`mysite/templates` directory. SilverStripe keeps an eye on what templates have been overridden and the location of the
|
||||
correct template through a [SS_TemplateManifest](api:SS_TemplateManifest).
|
||||
correct template through a [ThemeResourceLoader](api:SilverStripe\View\ThemeResourceLoader).
|
||||
|
||||
## Template Manifest
|
||||
## ThemeResourceLoader
|
||||
|
||||
The location of each template and the hierarchy of what template to use is stored within a [SS_TemplateManifest](api:SS_TemplateManifest)
|
||||
instance. This is a serialized object containing a map of template names, paths and other meta data for each template
|
||||
and is cached in your applications `TEMP_FOLDER` for performance. For SilverStripe to find the `GenericEmail` template
|
||||
it does not check all your `template` folders on the fly, it simply asks the `manifest`.
|
||||
|
||||
The manifest is created whenever you flush your SilverStripe cache by appending `?flush=1` to any SilverStripe URL. For
|
||||
example by visiting `http://yoursite.com/?flush=1`. When your include the `flush=1` flag, the manifest class will search
|
||||
your entire project for the appropriate `.ss` files located in `template` directory and save that information for later.
|
||||
|
||||
<div class="warning">
|
||||
Whenever you add or remove template files, rebuild the manifest by visiting `http://yoursite.com/?flush=1`. You can
|
||||
flush the cache from any page, (.com/home?flush=1, .com/admin?flush=1, etc.). Flushing the cache can be slow, so you
|
||||
only need to do it when you're developing new templates.
|
||||
</div>
|
||||
The location of each template and the hierarchy of what template to use is stored within a [ThemeResourceLoader](api:SilverStripe\View\ThemeResourceLoader)
|
||||
instance. This is a serialized object containing a map of [ThemeManifest](api:SilverStripe\View\ThemeManifest) instances. For SilverStripe to find the `GenericEmail` template
|
||||
it does not check all your `template` folders on the fly, it simply asks the manifests. The manifests are created and added to the loader when the
|
||||
[kernel](api:SilverStripe\Core\CoreKernel) is instantiated.
|
||||
|
||||
## Template Priority
|
||||
|
||||
|
@ -112,33 +102,33 @@ footer and navigation will remain the same and we don't want to replicate this w
|
|||
`$Layout` function allows us to define the child template area which can be overridden.
|
||||
|
||||
**mysite/templates/Page.ss**
|
||||
```ss
|
||||
<html>
|
||||
<head>
|
||||
..
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<% include Header %>
|
||||
<% include Navigation %>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
..
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<% include Header %>
|
||||
<% include Navigation %>
|
||||
|
||||
$Layout
|
||||
|
||||
<% include Footer %>
|
||||
</body>
|
||||
$Layout
|
||||
|
||||
<% include Footer %>
|
||||
</body>
|
||||
``
|
||||
**mysite/templates/Layout/Page.ss**
|
||||
|
||||
```ss
|
||||
<p>You are on a $Title page</p>
|
||||
|
||||
$Content
|
||||
|
||||
```
|
||||
**mysite/templates/Layout/HomePage.ss**
|
||||
|
||||
```ss
|
||||
<h1>This is the homepage!</h1>
|
||||
|
||||
<blink>Hi!</blink>
|
||||
|
||||
```
|
||||
|
||||
If your classes have in a namespace, the Layout folder will be a found inside of the appropriate namespace folder.
|
||||
|
||||
|
|
|
@ -8,20 +8,24 @@ summary: Reduce rendering time with cached templates and understand the limitati
|
|||
All functions that provide data to templates must have no side effects, as the value is cached after first access. For
|
||||
example, this controller method will not behave as you might imagine.
|
||||
|
||||
:::php
|
||||
private $counter = 0;
|
||||
```php
|
||||
private $counter = 0;
|
||||
|
||||
public function Counter() {
|
||||
$this->counter += 1;
|
||||
public function Counter()
|
||||
{
|
||||
$this->counter += 1;
|
||||
|
||||
return $this->counter;
|
||||
}
|
||||
return $this->counter;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
:::ss
|
||||
$Counter, $Counter, $Counter
|
||||
```ss
|
||||
|
||||
// returns 1, 1, 1
|
||||
$Counter, $Counter, $Counter
|
||||
|
||||
// returns 1, 1, 1
|
||||
```
|
||||
|
||||
When we render `$Counter` to the template we would expect the value to increase and output `1, 2, 3`. However, as
|
||||
`$Counter` is cached at the first access, the value of `1` is saved.
|
||||
|
@ -32,7 +36,11 @@ When we render `$Counter` to the template we would expect the value to increase
|
|||
Partial caching is a feature that allows the caching of just a portion of a page. Instead of fetching the required data
|
||||
from the database to display, the contents of the area are fetched from a [cache backend](../performance/caching).
|
||||
|
||||
:::ss
|
||||
<% cached 'MyCachedContent', LastEdited %>
|
||||
$Title
|
||||
<% end_cached %>
|
||||
```ss
|
||||
|
||||
<% cached 'MyCachedContent', LastEdited %>
|
||||
$Title
|
||||
<% end_cached %>
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -5,11 +5,11 @@ summary: Definition of the syntax for writing i18n compatible templates.
|
|||
|
||||
Translations are easy to use with a template, and give access to SilverStripe's translation facilities. Here is an
|
||||
example:
|
||||
|
||||
<%t Foo.BAR 'Bar' %>
|
||||
```ss
|
||||
<%t Foo.BAR 'Bar' %>
|
||||
|
||||
<%t Member.WELCOME 'Welcome {name} to {site}' name=$Member.Name site="Foobar.com" %>
|
||||
|
||||
```
|
||||
`Member.WELCOME` is an identifier in the translation system, for which different translations may be available. This
|
||||
string may include named placeholders, in braces.
|
||||
|
||||
|
|
|
@ -12,25 +12,29 @@ output the result of the [DBHtmlText::FirstParagraph()](api:SilverStripe\ORM\Fie
|
|||
|
||||
**mysite/code/Page.ss**
|
||||
|
||||
:::ss
|
||||
$Content.FirstParagraph
|
||||
<!-- returns the result of HtmlText::FirstParagragh() -->
|
||||
```ss
|
||||
|
||||
$LastEdited.Format("d/m/Y")
|
||||
<!-- returns the result of SS_Datetime::Format("d/m/Y") -->
|
||||
$Content.FirstParagraph
|
||||
<!-- returns the result of HtmlText::FirstParagragh() -->
|
||||
|
||||
$LastEdited.Format("d/m/Y")
|
||||
<!-- returns the result of SS_Datetime::Format("d/m/Y") -->
|
||||
```
|
||||
|
||||
Any public method from the object in scope can be called within the template. If that method returns another
|
||||
`ViewableData` instance, you can chain the method calls.
|
||||
|
||||
:::ss
|
||||
$Content.FirstParagraph.NoHTML
|
||||
<!-- "First Paragraph" -->
|
||||
```ss
|
||||
|
||||
<p>Copyright {$Now.Year}</p>
|
||||
<!-- "Copyright 2014" -->
|
||||
$Content.FirstParagraph.NoHTML
|
||||
<!-- "First Paragraph" -->
|
||||
|
||||
<div class="$URLSegment.LowerCase">
|
||||
<!-- <div class="about-us"> -->
|
||||
<p>Copyright {$Now.Year}</p>
|
||||
<!-- "Copyright 2014" -->
|
||||
|
||||
<div class="$URLSegment.LowerCase">
|
||||
<!-- <div class="about-us"> -->
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
See the API documentation for [DBHtmlText](api:SilverStripe\ORM\FieldType\DBHtmlText), [FieldType](api:SilverStripe\ORM\FieldType), [DBText](api:SilverStripe\ORM\FieldType\DBText) for all the methods you can use to format
|
||||
|
@ -43,22 +47,25 @@ When rendering an object to the template such as `$Me` the `forTemplate` method
|
|||
provide default template for an object.
|
||||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
|
||||
class Page extends SiteTree {
|
||||
class Page extends SiteTree
|
||||
{
|
||||
|
||||
public function forTemplate() {
|
||||
return "Page: ". $this->Title;
|
||||
}
|
||||
}
|
||||
public function forTemplate()
|
||||
{
|
||||
return "Page: ". $this->Title;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**mysite/templates/Page.ss**
|
||||
|
||||
:::ss
|
||||
$Me
|
||||
<!-- returns Page: Home -->
|
||||
```ss
|
||||
|
||||
$Me
|
||||
<!-- returns Page: Home -->
|
||||
```
|
||||
|
||||
## Casting
|
||||
|
||||
|
@ -66,19 +73,23 @@ Methods which return data to the template should either return an explicit objec
|
|||
content that method sends back, or, provide a type in the `$casting` array for the object. When rendering that method
|
||||
to a template, SilverStripe will ensure that the object is wrapped in the correct type and values are safely escaped.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
|
||||
class Page extends SiteTree {
|
||||
class Page extends SiteTree
|
||||
{
|
||||
|
||||
private static $casting = array(
|
||||
'MyCustomMethod' => 'HTMLText'
|
||||
);
|
||||
private static $casting = [
|
||||
'MyCustomMethod' => 'HTMLText'
|
||||
];
|
||||
|
||||
public function MyCustomMethod() {
|
||||
return "<h1>This is my header</h1>";
|
||||
}
|
||||
}
|
||||
public function MyCustomMethod()
|
||||
{
|
||||
return "<h1>This is my header</h1>";
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
When calling `$MyCustomMethod` SilverStripe now has the context that this method will contain HTML and escape the data
|
||||
accordingly.
|
||||
|
|
|
@ -7,7 +7,7 @@ top level menu with a nested second level using the `Menu` loop and a `Children`
|
|||
|
||||
**mysite/templates/Page.ss**
|
||||
|
||||
:::ss
|
||||
```ss
|
||||
<ul>
|
||||
<% loop $Menu(1) %>
|
||||
<li>
|
||||
|
@ -27,7 +27,7 @@ top level menu with a nested second level using the `Menu` loop and a `Children`
|
|||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
|
||||
```
|
||||
## Related
|
||||
|
||||
* [Template Syntax](../syntax)
|
||||
|
|
|
@ -10,15 +10,17 @@ The `PaginatedList` will automatically set up query limits and read the request
|
|||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
```php
|
||||
/**
|
||||
* Returns a paginated list of all pages in the site.
|
||||
*/
|
||||
public function PaginatedPages() {
|
||||
public function PaginatedPages()
|
||||
{
|
||||
$list = Page::get();
|
||||
|
||||
return new PaginatedList($list, $this->getRequest());
|
||||
}
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
Note that the concept of "pages" used in pagination does not necessarily mean that we're dealing with `Page` classes,
|
||||
|
@ -32,19 +34,19 @@ The first step is to simply list the objects in the template:
|
|||
|
||||
**mysite/templates/Page.ss**
|
||||
|
||||
:::ss
|
||||
```ss
|
||||
<ul>
|
||||
<% loop $PaginatedPages %>
|
||||
<li><a href="$Link">$Title</a></li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
|
||||
```
|
||||
By default this will display 10 pages at a time. The next step is to add pagination controls below this so the user can
|
||||
switch between pages:
|
||||
|
||||
**mysite/templates/Page.ss**
|
||||
|
||||
:::ss
|
||||
```ss
|
||||
<% if $PaginatedPages.MoreThanOnePage %>
|
||||
<% if $PaginatedPages.NotFirstPage %>
|
||||
<a class="prev" href="$PaginatedPages.PrevLink">Prev</a>
|
||||
|
@ -64,6 +66,7 @@ switch between pages:
|
|||
<a class="next" href="$PaginatedPages.NextLink">Next</a>
|
||||
<% end_if %>
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
If there is more than one page, this block will render a set of pagination controls in the form
|
||||
`[1] ... [3] [4] [5] [6] [7] ... [10]`.
|
||||
|
@ -75,18 +78,19 @@ that you wish to display on the current page. In this situation the automatic li
|
|||
will break the pagination. You can disable automatic limiting using the [PaginatedList::setLimitItems()](api:SilverStripe\ORM\PaginatedList::setLimitItems()) method
|
||||
when using custom lists.
|
||||
|
||||
:::php
|
||||
```php
|
||||
$myPreLimitedList = Page::get()->limit(10);
|
||||
|
||||
$pages = new PaginatedList($myPreLimitedList, $this->getRequest());
|
||||
$pages->setLimitItems(false);
|
||||
|
||||
```
|
||||
|
||||
## Setting the limit of items
|
||||
|
||||
:::php
|
||||
```php
|
||||
$pages = new PaginatedList(Page::get(), $this->getRequest());
|
||||
$pages->setPageLength(25);
|
||||
```
|
||||
|
||||
If you set this limit to 0 it will disable paging entirely, effectively causing it to appear as a single page
|
||||
list.
|
||||
|
|
|
@ -5,12 +5,12 @@ title: Disable Anchor Rewriting
|
|||
Anchor links are links with a "#" in them. A frequent use-case is to use anchor links to point to different sections of
|
||||
the current page. For example, we might have this in our template:
|
||||
|
||||
:::ss
|
||||
```ss
|
||||
<ul>
|
||||
<li><a href="#section1">Section 1</a></li>
|
||||
<li><a href="#section2">Section 2</a></li>
|
||||
</ul>
|
||||
|
||||
```
|
||||
|
||||
Things get tricky because of we have set our `<base>` tag to point to the root of the site. So, when you click the
|
||||
first link you will be sent to http://yoursite.com/#section1 instead of http://yoursite.com/my-long-page/#section1
|
||||
|
@ -19,28 +19,33 @@ In order to prevent this situation, the SSViewer template renderer will automati
|
|||
doesn't specify a URL before the anchor, prefixing the URL of the current page. For our example above, the following
|
||||
would be created in the final HTML
|
||||
|
||||
:::ss
|
||||
```ss
|
||||
<ul>
|
||||
<li><a href="my-long-page/#section1">Section 1</a></li>
|
||||
<li><a href="my-long-page/#section2">Section 2</a></li>
|
||||
</ul>
|
||||
|
||||
```
|
||||
|
||||
There are cases where this can be unhelpful. HTML anchors created from Ajax responses are the most common. In these
|
||||
situations, you can disable anchor link rewriting by setting the `SSViewer.rewrite_hash_links` configuration value to
|
||||
`false`.
|
||||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
```yml
|
||||
SSViewer:
|
||||
rewrite_hash_links: false
|
||||
```
|
||||
|
||||
Or, a better way is to call this just for the rendering phase of this particular file:
|
||||
|
||||
:::php
|
||||
public function RenderCustomTemplate() {
|
||||
```php
|
||||
public function RenderCustomTemplate()
|
||||
{
|
||||
Config::inst()->update('SSViewer', 'rewrite_hash_links', false);
|
||||
$html = $this->renderWith('MyCustomTemplate');
|
||||
Config::inst()->update('SSViewer', 'rewrite_hash_links', true);
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
```
|
|
@ -8,24 +8,29 @@ subclass the base `Controller` class.
|
|||
|
||||
**mysite/code/controllers/TeamController.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class TeamController extends Controller {
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'players',
|
||||
'index'
|
||||
);
|
||||
|
||||
public function index(HTTPRequest $request) {
|
||||
// ..
|
||||
}
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
public function players(HTTPRequest $request) {
|
||||
print_r($request->allParams());
|
||||
}
|
||||
}
|
||||
class TeamController extends Controller
|
||||
{
|
||||
|
||||
private static $allowed_actions = [
|
||||
'players',
|
||||
'index'
|
||||
];
|
||||
|
||||
public function index(HTTPRequest $request)
|
||||
{
|
||||
// ..
|
||||
}
|
||||
|
||||
public function players(HTTPRequest $request)
|
||||
{
|
||||
print_r($request->allParams());
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Routing
|
||||
|
||||
|
@ -44,15 +49,16 @@ Make sure that after you have modified the `routes.yml` file, that you clear you
|
|||
|
||||
**mysite/_config/routes.yml**
|
||||
|
||||
:::yml
|
||||
---
|
||||
Name: mysiteroutes
|
||||
After: framework/routes#coreroutes
|
||||
---
|
||||
SilverStripe\Control\Director:
|
||||
rules:
|
||||
'teams//$Action/$ID/$Name': 'TeamController'
|
||||
```yml
|
||||
|
||||
---
|
||||
Name: mysiteroutes
|
||||
After: framework/routes#coreroutes
|
||||
---
|
||||
SilverStripe\Control\Director:
|
||||
rules:
|
||||
'teams//$Action/$ID/$Name': 'TeamController'
|
||||
```
|
||||
|
||||
For more information about creating custom routes, see the [Routing](routing) documentation.
|
||||
|
||||
|
@ -74,58 +80,65 @@ Action methods can return one of four main things:
|
|||
|
||||
**mysite/code/controllers/TeamController.php**
|
||||
|
||||
:::php
|
||||
/**
|
||||
* Return some additional data to the current response that is waiting to go out, this makes $Title set to
|
||||
* 'MyTeamName' and continues on with generating the response.
|
||||
*/
|
||||
public function index(HTTPRequest $request) {
|
||||
return array(
|
||||
'Title' => 'My Team Name'
|
||||
);
|
||||
}
|
||||
```php
|
||||
/**
|
||||
* Return some additional data to the current response that is waiting to go out, this makes $Title set to
|
||||
* 'MyTeamName' and continues on with generating the response.
|
||||
*/
|
||||
public function index(HTTPRequest $request)
|
||||
{
|
||||
return [
|
||||
'Title' => 'My Team Name'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* We can manually create a response and return that to ignore any previous data.
|
||||
*/
|
||||
public function someaction(HTTPRequest $request) {
|
||||
$this->setResponse(new HTTPResponse());
|
||||
$this->getResponse()->setStatusCode(400);
|
||||
$this->getResponse()->setBody('invalid');
|
||||
/**
|
||||
* We can manually create a response and return that to ignore any previous data.
|
||||
*/
|
||||
public function someaction(HTTPRequest $request)
|
||||
{
|
||||
$this->setResponse(new HTTPResponse());
|
||||
$this->getResponse()->setStatusCode(400);
|
||||
$this->getResponse()->setBody('invalid');
|
||||
|
||||
return $this->getResponse();
|
||||
}
|
||||
return $this->getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Or, we can modify the response that is waiting to go out.
|
||||
*/
|
||||
public function anotheraction(HTTPRequest $request) {
|
||||
$this->getResponse()->setStatusCode(400);
|
||||
/**
|
||||
* Or, we can modify the response that is waiting to go out.
|
||||
*/
|
||||
public function anotheraction(HTTPRequest $request)
|
||||
{
|
||||
$this->getResponse()->setStatusCode(400);
|
||||
|
||||
return $this->getResponse();
|
||||
}
|
||||
return $this->getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* We can render HTML and leave SilverStripe to set the response code and body.
|
||||
*/
|
||||
public function htmlaction() {
|
||||
return $this->customise(new ArrayData(array(
|
||||
'Title' => 'HTML Action'
|
||||
)))->renderWith('MyCustomTemplate');
|
||||
}
|
||||
/**
|
||||
* We can render HTML and leave SilverStripe to set the response code and body.
|
||||
*/
|
||||
public function htmlaction()
|
||||
{
|
||||
return $this->customise(new ArrayData([
|
||||
'Title' => 'HTML Action'
|
||||
]))->renderWith('MyCustomTemplate');
|
||||
}
|
||||
|
||||
/**
|
||||
* We can send stuff to the browser which isn't HTML
|
||||
*/
|
||||
public function ajaxaction() {
|
||||
$this->getResponse()->setBody(json_encode(array(
|
||||
'json' => true
|
||||
)));
|
||||
/**
|
||||
* We can send stuff to the browser which isn't HTML
|
||||
*/
|
||||
public function ajaxaction()
|
||||
{
|
||||
$this->getResponse()->setBody(json_encode([
|
||||
'json' => true
|
||||
]));
|
||||
|
||||
$this->getResponse()->addHeader("Content-type", "application/json");
|
||||
$this->getResponse()->addHeader("Content-type", "application/json");
|
||||
|
||||
return $this->getResponse().
|
||||
}
|
||||
return $this->getResponse().
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
For more information on how a URL gets mapped to an action see the [Routing](routing) documentation.
|
||||
|
||||
|
@ -153,10 +166,12 @@ Each controller should define a `Link()` method. This should be used to avoid ha
|
|||
|
||||
**mysite/code/controllers/TeamController.php**
|
||||
|
||||
:::php
|
||||
public function Link($action = null) {
|
||||
return Controller::join_links('teams', $action);
|
||||
}
|
||||
```php
|
||||
public function Link($action = null)
|
||||
{
|
||||
return Controller::join_links('teams', $action);
|
||||
}
|
||||
```
|
||||
|
||||
<div class="info" markdown="1">
|
||||
The [Controller::join_links()](api:SilverStripe\Control\Controller::join_links()) is optional, but makes `Link()` more flexible by allowing an `$action` argument, and concatenates the path segments with slashes. The action should map to a method on your controller.
|
||||
|
|
|
@ -84,6 +84,7 @@ print_r($this->getRequest()->params());
|
|||
// [ID] => 1
|
||||
// [Name] => null
|
||||
// )
|
||||
|
||||
```
|
||||
|
||||
You can also fetch one parameter at a time.
|
||||
|
@ -158,20 +159,19 @@ This is useful when you want to provide custom actions for the mapping of `teams
|
|||
**mysite/code/controllers/TeamController.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class TeamController extends Controller
|
||||
{
|
||||
private static $allowed_actions = array(
|
||||
private static $allowed_actions = [
|
||||
'payroll'
|
||||
);
|
||||
];
|
||||
|
||||
private static $url_handlers = array(
|
||||
private static $url_handlers = [
|
||||
'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.
|
||||
|
@ -188,10 +188,10 @@ use SilverStripe\CMS\Controllers\ContentController;
|
|||
|
||||
class FeedController extends ContentController
|
||||
{
|
||||
private static $allowed_actions = array('go');
|
||||
private static $url_handlers = array(
|
||||
private static $allowed_actions = ['go'];
|
||||
private static $url_handlers = [
|
||||
'go/$UserName/$AuthToken/$Timestamp/$OutputType/$DeleteMode' => 'go'
|
||||
);
|
||||
];
|
||||
|
||||
public function go()
|
||||
{
|
||||
|
@ -206,6 +206,7 @@ class FeedController extends ContentController
|
|||
The YAML rule, in contrast, is simple. It needs to provide only enough
|
||||
information for the framework to choose the desired controller.
|
||||
|
||||
|
||||
```yml
|
||||
Director:
|
||||
rules:
|
||||
|
|
|
@ -11,31 +11,34 @@ actions on the website they shouldn't be able to.
|
|||
Any action you define on a controller must be defined in a `$allowed_actions` static array. This prevents users from
|
||||
directly calling methods that they shouldn't.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller {
|
||||
|
||||
private static $allowed_actions = array(
|
||||
// someaction can be accessed by anyone, any time
|
||||
'someaction',
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
private static $allowed_actions = [
|
||||
// someaction can be accessed by anyone, any time
|
||||
'someaction',
|
||||
|
||||
// So can otheraction
|
||||
'otheraction' => true,
|
||||
|
||||
// restrictedaction can only be people with ADMIN privilege
|
||||
'restrictedaction' => 'ADMIN',
|
||||
// So can otheraction
|
||||
'otheraction' => true,
|
||||
|
||||
// restrictedaction can only be people with ADMIN privilege
|
||||
'restrictedaction' => 'ADMIN',
|
||||
|
||||
// restricted to uses that have the 'CMS_ACCESS_CMSMain' access
|
||||
'cmsrestrictedaction' => 'CMS_ACCESS_CMSMain',
|
||||
|
||||
// complexaction can only be accessed if $this->canComplexAction() returns true.
|
||||
'complexaction' => '->canComplexAction',
|
||||
// restricted to uses that have the 'CMS_ACCESS_CMSMain' access
|
||||
'cmsrestrictedaction' => 'CMS_ACCESS_CMSMain',
|
||||
|
||||
// complexaction can only be accessed if $this->canComplexAction() returns true.
|
||||
'complexaction' => '->canComplexAction',
|
||||
|
||||
// complexactioncheck can only be accessed if $this->canComplexAction("MyRestrictedAction", false, 42) is true.
|
||||
'complexactioncheck' => '->canComplexAction("MyRestrictedAction", false, 42)',
|
||||
);
|
||||
}
|
||||
// complexactioncheck can only be accessed if $this->canComplexAction("MyRestrictedAction", false, 42) is true.
|
||||
'complexactioncheck' => '->canComplexAction("MyRestrictedAction", false, 42)',
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="info">
|
||||
If the permission check fails, SilverStripe will return a `403` Forbidden HTTP status.
|
||||
|
@ -44,75 +47,91 @@ If the permission check fails, SilverStripe will return a `403` Forbidden HTTP s
|
|||
An action named "index" is white listed by default, unless `allowed_actions` is defined as an empty array, or the action
|
||||
is specifically restricted.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller {
|
||||
<?php
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
public function index() {
|
||||
// allowed without an $allowed_action defined
|
||||
}
|
||||
}
|
||||
public function index()
|
||||
{
|
||||
// allowed without an $allowed_action defined
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`$allowed_actions` can be defined on `Extension` classes applying to the controller.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\Core\Extension;
|
||||
|
||||
class MyExtension extends Extension {
|
||||
class MyExtension extends Extension
|
||||
{
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'mycustomaction'
|
||||
);
|
||||
}
|
||||
private static $allowed_actions = [
|
||||
'mycustomaction'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Only public methods can be made accessible.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'secure',
|
||||
// secureaction won't work as it's private.
|
||||
);
|
||||
private static $allowed_actions = [
|
||||
'secure',
|
||||
// secureaction won't work as it's private.
|
||||
];
|
||||
|
||||
public function secure() {
|
||||
// ..
|
||||
}
|
||||
public function secure()
|
||||
{
|
||||
// ..
|
||||
}
|
||||
|
||||
private function secureaction() {
|
||||
// ..
|
||||
}
|
||||
}
|
||||
private function secureaction()
|
||||
{
|
||||
// ..
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If a method on a parent class is overwritten, access control for it has to be redefined as well.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'action',
|
||||
);
|
||||
private static $allowed_actions = [
|
||||
'action',
|
||||
];
|
||||
|
||||
public function action() {
|
||||
// ..
|
||||
}
|
||||
}
|
||||
public function action()
|
||||
{
|
||||
// ..
|
||||
}
|
||||
}
|
||||
class MyChildController extends MyController
|
||||
{
|
||||
|
||||
class MyChildController extends MyController {
|
||||
private static $allowed_actions = [
|
||||
'action', // required as we are redefining action
|
||||
];
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'action', // required as we are redefining action
|
||||
);
|
||||
public function action()
|
||||
{
|
||||
|
||||
public function action() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
Access checks on parent classes need to be overwritten via the [Configuration API](../configuration).
|
||||
|
@ -122,47 +141,56 @@ Access checks on parent classes need to be overwritten via the [Configuration AP
|
|||
|
||||
Form action methods should **not** be included in `$allowed_actions`. However, the form method **should** be included
|
||||
as an `allowed_action`.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'ContactForm' // use the Form method, not the action
|
||||
);
|
||||
private static $allowed_actions = [
|
||||
'ContactForm' // use the Form method, not the action
|
||||
];
|
||||
|
||||
public function ContactForm() {
|
||||
return new Form(..);
|
||||
}
|
||||
public function ContactForm()
|
||||
{
|
||||
return new Form(..);
|
||||
}
|
||||
|
||||
public function doContactForm($data, $form) {
|
||||
// ..
|
||||
}
|
||||
}
|
||||
public function doContactForm($data, $form)
|
||||
{
|
||||
// ..
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Action Level Checks
|
||||
|
||||
Each method responding to a URL can also implement custom permission checks, e.g. to handle responses conditionally on
|
||||
the passed request data.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller {
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'myaction'
|
||||
);
|
||||
|
||||
public function myaction($request) {
|
||||
if(!$request->getVar('apikey')) {
|
||||
return $this->httpError(403, 'No API key provided');
|
||||
}
|
||||
|
||||
return 'valid';
|
||||
}
|
||||
}
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
private static $allowed_actions = [
|
||||
'myaction'
|
||||
];
|
||||
|
||||
public function myaction($request)
|
||||
{
|
||||
if(!$request->getVar('apikey')) {
|
||||
return $this->httpError(403, 'No API key provided');
|
||||
}
|
||||
|
||||
return 'valid';
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
This is recommended as an addition for `$allowed_actions`, in order to handle more complex checks, rather than a
|
||||
|
@ -178,21 +206,26 @@ execution. This behavior can be used to implement permission checks.
|
|||
<div class="info" markdown="1">
|
||||
`init` is called for any possible action on the controller and before any specific method such as `index`.
|
||||
</div>
|
||||
:::php
|
||||
<?php
|
||||
```php
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller {
|
||||
|
||||
private static $allowed_actions = array();
|
||||
|
||||
public function init() {
|
||||
parent::init();
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
private static $allowed_actions = [];
|
||||
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
|
||||
if(!Permission::check('ADMIN')) {
|
||||
return $this->httpError(403);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!Permission::check('ADMIN')) {
|
||||
return $this->httpError(403);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
|
|
|
@ -8,37 +8,42 @@ HTTP header.
|
|||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
$this->redirect('goherenow');
|
||||
// redirect to Page::goherenow(), i.e on the contact-us page this will redirect to /contact-us/goherenow/
|
||||
|
||||
$this->redirect('goherenow/');
|
||||
// redirect to the URL on yoursite.com/goherenow/. (note the trailing slash)
|
||||
```php
|
||||
$this->redirect('goherenow');
|
||||
// redirect to Page::goherenow(), i.e on the contact-us page this will redirect to /contact-us/goherenow/
|
||||
|
||||
$this->redirect('http://google.com');
|
||||
// redirect to http://google.com
|
||||
$this->redirect('goherenow/');
|
||||
// redirect to the URL on yoursite.com/goherenow/. (note the trailing slash)
|
||||
|
||||
$this->redirectBack();
|
||||
// go back to the previous page.
|
||||
$this->redirect('http://google.com');
|
||||
// redirect to http://google.com
|
||||
|
||||
$this->redirectBack();
|
||||
// go back to the previous page.
|
||||
```
|
||||
|
||||
## Status Codes
|
||||
|
||||
The `redirect()` method takes an optional HTTP status code, either `301` for permanent redirects, or `302` for
|
||||
temporary redirects (default).
|
||||
|
||||
:::php
|
||||
$this->redirect('/', 302);
|
||||
// go back to the homepage, don't cache that this page has moved
|
||||
```php
|
||||
$this->redirect('/', 302);
|
||||
// go back to the homepage, don't cache that this page has moved
|
||||
```
|
||||
|
||||
## Redirection in URL Handling
|
||||
|
||||
Controllers can specify redirections in the `$url_handlers` property rather than defining a method by using the '~'
|
||||
operator.
|
||||
|
||||
:::php
|
||||
private static $url_handlers = array(
|
||||
'players/john' => '~>coach'
|
||||
);
|
||||
|
||||
```php
|
||||
private static $url_handlers = [
|
||||
'players/john' => '~>coach'
|
||||
];
|
||||
|
||||
```
|
||||
|
||||
For more information on `$url_handlers` see the [Routing](routing) documenation.
|
||||
|
||||
|
|
|
@ -19,8 +19,6 @@ will deliberately return a different response, e.g. an error response or a redir
|
|||
**mysite/code/CustomMiddleware.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\Control\Middleware\HTTPMiddleware
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ See the [Forms Tutorial](../../tutorials/forms/) for a step by step process of c
|
|||
|
||||
Creating a [Form](api:SilverStripe\Forms\Form) has the following signature.
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
$form = new Form(
|
||||
$controller, // the Controller to render this form on
|
||||
$name, // name of the method that returns this form on the controller
|
||||
|
@ -22,14 +23,13 @@ Creating a [Form](api:SilverStripe\Forms\Form) has the following signature.
|
|||
FieldList $actions, // list of FormAction instances
|
||||
$required // optional use of RequiredFields object
|
||||
);
|
||||
```
|
||||
|
||||
In practice, this looks like:
|
||||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
|
@ -39,9 +39,9 @@ use SilverStripe\Forms\TextField;
|
|||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
private static $allowed_actions = array(
|
||||
private static $allowed_actions = [
|
||||
'HelloForm'
|
||||
);
|
||||
];
|
||||
|
||||
public function HelloForm()
|
||||
{
|
||||
|
@ -67,6 +67,7 @@ class PageController extends ContentController
|
|||
return $this->redirectBack();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**mysite/templates/Page.ss**
|
||||
|
@ -91,9 +92,10 @@ Because the `HelloForm()` method will be the location the user is taken to, it n
|
|||
controller action. To grant it access through URLs, we add it to the `$allowed_actions` array.
|
||||
|
||||
```php
|
||||
private static $allowed_actions = array(
|
||||
private static $allowed_actions = [
|
||||
'HelloForm'
|
||||
);
|
||||
];
|
||||
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
|
@ -276,8 +278,6 @@ The `$action` method takes two arguments:
|
|||
* `$form` the submitted [Form](api:SilverStripe\Forms\Form) instance.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
use SilverStripe\Forms\EmailField;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
|
@ -287,9 +287,9 @@ use SilverStripe\Forms\TextField;
|
|||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
private static $allowed_actions = array(
|
||||
private static $allowed_actions = [
|
||||
'MyForm'
|
||||
);
|
||||
];
|
||||
|
||||
public function MyForm()
|
||||
{
|
||||
|
@ -323,6 +323,7 @@ class PageController extends ContentController
|
|||
return $this->redirectBack();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
@ -334,12 +335,13 @@ validating its' own data value.
|
|||
For more information, see the [Form Validation](validation) documentation.
|
||||
|
||||
```php
|
||||
$validator = new SilverStripe\Forms\RequiredFields(array(
|
||||
$validator = new SilverStripe\Forms\RequiredFields([
|
||||
'Name',
|
||||
'Email'
|
||||
));
|
||||
]);
|
||||
|
||||
$form = new Form($this, 'MyForm', $fields, $actions, $validator);
|
||||
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
|
|
@ -8,8 +8,6 @@ SilverStripe provides server-side form validation out of the box through the [Va
|
|||
the [Form](api:SilverStripe\Forms\Form) constructor or through the function `setValidator`.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
use SilverStripe\Forms\EmailField;
|
||||
use SilverStripe\Forms\Form;
|
||||
|
@ -19,9 +17,9 @@ use SilverStripe\Forms\RequiredFields;
|
|||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
private static $allowed_actions = array(
|
||||
private static $allowed_actions = [
|
||||
'MyForm'
|
||||
);
|
||||
];
|
||||
|
||||
public function MyForm()
|
||||
{
|
||||
|
@ -35,9 +33,9 @@ class PageController extends ContentController
|
|||
);
|
||||
|
||||
// the fields 'Name' and 'Email' are required.
|
||||
$required = new RequiredFields(array(
|
||||
$required = new RequiredFields([
|
||||
'Name', 'Email'
|
||||
));
|
||||
]);
|
||||
|
||||
// $required can be set as an argument
|
||||
$form = new Form($controller, 'MyForm', $fields, $actions, $required);
|
||||
|
@ -53,6 +51,7 @@ class PageController extends ContentController
|
|||
//..
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
In this example we will be required to input a value for `Name` and a valid email address for `Email` before the
|
||||
|
@ -96,8 +95,6 @@ the same validation logic applied to it throughout.
|
|||
**mysite/code/CustomNumberField.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\Forms\TextField;
|
||||
|
||||
class CustomNumberField extends TextField
|
||||
|
@ -128,8 +125,6 @@ reusable and would not be possible within the `CMS` or other automated `UI` but
|
|||
`FormField` classes.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
use SilverStripe\Forms\EmailField;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
|
@ -140,9 +135,9 @@ use SilverStripe\Security\Member;
|
|||
|
||||
class Page_Controller extends ContentController
|
||||
{
|
||||
private static $allowed_actions = array(
|
||||
private static $allowed_actions = [
|
||||
'MyForm'
|
||||
);
|
||||
];
|
||||
|
||||
public function MyForm()
|
||||
{
|
||||
|
@ -179,6 +174,7 @@ class Page_Controller extends ContentController
|
|||
return $this->redirectBack();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Exempt validation actions
|
||||
|
@ -283,17 +279,15 @@ Again, custom error messages can be provided through the `FormField`
|
|||
</div>
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
use SilverStripe\Forms\TextField;
|
||||
use SilverStripe\Forms\RequiredFields;
|
||||
|
||||
class Page extends SiteTree
|
||||
{
|
||||
private static $db = array(
|
||||
private static $db = [
|
||||
'MyRequiredField' => 'Text'
|
||||
);
|
||||
];
|
||||
|
||||
public function getCMSFields()
|
||||
{
|
||||
|
@ -306,11 +300,12 @@ class Page extends SiteTree
|
|||
|
||||
public function getCMSValidator()
|
||||
{
|
||||
return new RequiredFields(array(
|
||||
return new RequiredFields([
|
||||
'MyRequiredField'
|
||||
));
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
|
|
@ -4,15 +4,17 @@ summary: Customize the generated HTML for a FormField or an entire Form.
|
|||
# Form Templates
|
||||
|
||||
Most markup generated in SilverStripe can be replaced by custom templates. Both [Form](api:SilverStripe\Forms\Form) and [FormField](api:SilverStripe\Forms\FormField) instances
|
||||
can be rendered out using custom templates using `setTemplate`.
|
||||
can be rendered out using custom templates using `setTemplate`.
|
||||
|
||||
:::php
|
||||
$form = new Form(..);
|
||||
$form->setTemplate('MyCustomFormTemplate');
|
||||
|
||||
// or, just a field
|
||||
$field = new TextField(..);
|
||||
$field->setTemplate('MyCustomTextField');
|
||||
```php
|
||||
$form = new Form(..);
|
||||
$form->setTemplate('MyCustomFormTemplate');
|
||||
|
||||
// or, just a field
|
||||
$field = new TextField(..);
|
||||
$field->setTemplate('MyCustomTextField');
|
||||
```
|
||||
|
||||
Both `MyCustomTemplate.ss` and `MyCustomTextField.ss` should be located in **mysite/templates/forms/** or the same directory as the core.
|
||||
|
||||
|
@ -33,28 +35,30 @@ the core template structure. It is recommended to use `setTemplate` and unique t
|
|||
|
||||
For [FormField](api:SilverStripe\Forms\FormField) instances, there are several other templates that are used on top of the main `setTemplate`.
|
||||
|
||||
:::php
|
||||
$field = new TextField();
|
||||
|
||||
$field->setTemplate('CustomTextField');
|
||||
// Sets the template for the <input> tag. i.e '<input $AttributesHTML />'
|
||||
|
||||
$field->setFieldHolderTemplate('CustomTextField_Holder');
|
||||
// Sets the template for the wrapper around the text field. i.e
|
||||
// '<div class="text">'
|
||||
//
|
||||
// The actual FormField is rendered into the holder via the `$Field`
|
||||
// variable.
|
||||
//
|
||||
// setFieldHolder() is used in most `Form` instances and needs to output
|
||||
// labels, error messages and the like.
|
||||
```php
|
||||
$field = new TextField();
|
||||
|
||||
$field->setSmallFieldHolderTemplate('CustomTextField_Holder_Small');
|
||||
// Sets the template for the wrapper around the text field.
|
||||
//
|
||||
// The difference here is the small field holder template is used when the
|
||||
// field is embedded within another field. For example, if the field is
|
||||
// part of a `FieldGroup` or `CompositeField` alongside other fields.
|
||||
$field->setTemplate('CustomTextField');
|
||||
// Sets the template for the <input> tag. i.e '<input $AttributesHTML />'
|
||||
|
||||
$field->setFieldHolderTemplate('CustomTextField_Holder');
|
||||
// Sets the template for the wrapper around the text field. i.e
|
||||
// '<div class="text">'
|
||||
//
|
||||
// The actual FormField is rendered into the holder via the `$Field`
|
||||
// variable.
|
||||
//
|
||||
// setFieldHolder() is used in most `Form` instances and needs to output
|
||||
// labels, error messages and the like.
|
||||
|
||||
$field->setSmallFieldHolderTemplate('CustomTextField_Holder_Small');
|
||||
// Sets the template for the wrapper around the text field.
|
||||
//
|
||||
// The difference here is the small field holder template is used when the
|
||||
// field is embedded within another field. For example, if the field is
|
||||
// part of a `FieldGroup` or `CompositeField` alongside other fields.
|
||||
```
|
||||
|
||||
All templates are rendered within the scope of the [FormField](api:SilverStripe\Forms\FormField). To understand more about Scope within Templates as
|
||||
well as the available syntax, see the [Templates](../templates) documentation.
|
||||
|
|
|
@ -20,25 +20,30 @@ website.
|
|||
|
||||
The `SecurityToken` automatically added looks something like:
|
||||
|
||||
:::php
|
||||
$form = new Form(..);
|
||||
echo $form->getSecurityToken()->getValue();
|
||||
|
||||
// 'c443076989a7f24cf6b35fe1360be8683a753e2c'
|
||||
```php
|
||||
$form = new Form(..);
|
||||
echo $form->getSecurityToken()->getValue();
|
||||
|
||||
// 'c443076989a7f24cf6b35fe1360be8683a753e2c'
|
||||
```
|
||||
|
||||
This token value is passed through the rendered Form HTML as a [HiddenField](api:SilverStripe\Forms\HiddenField).
|
||||
|
||||
:::html
|
||||
<input type="hidden" name="SecurityID" value="c443076989a7f24cf6b35fe1360be8683a753e2c" class="hidden" />
|
||||
```html
|
||||
|
||||
<input type="hidden" name="SecurityID" value="c443076989a7f24cf6b35fe1360be8683a753e2c" class="hidden" />
|
||||
```
|
||||
|
||||
The token should be present whenever a operation has a side effect such as a `POST` operation.
|
||||
|
||||
It can be safely disabled for `GET` requests as long as it does not modify the database (i.e a search form does not
|
||||
normally require a security token).
|
||||
|
||||
:::php
|
||||
$form = new Form(..);
|
||||
$form->disableSecurityToken();
|
||||
|
||||
```php
|
||||
$form = new Form(..);
|
||||
$form->disableSecurityToken();
|
||||
```
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
Do not disable the SecurityID for forms that perform some modification to the users session. This will open your
|
||||
|
@ -51,14 +56,16 @@ To reduce attack exposure forms are limited, by default, to the intended HTTP ve
|
|||
this check, forms that rely on `GET` can be submitted via `POST` or `PUT` or vice-versa potentially leading to
|
||||
application errors or edge cases. If you need to disable this setting follow the below example:
|
||||
|
||||
:::php
|
||||
$form = new Form(..);
|
||||
|
||||
$form->setFormMethod('POST');
|
||||
$form->setStrictFormMethodCheck(false);
|
||||
```php
|
||||
$form = new Form(..);
|
||||
|
||||
// or alternative short notation..
|
||||
$form->setFormMethod('POST', false);
|
||||
$form->setFormMethod('POST');
|
||||
$form->setStrictFormMethodCheck(false);
|
||||
|
||||
// or alternative short notation..
|
||||
$form->setFormMethod('POST', false);
|
||||
```
|
||||
|
||||
## Spam and Bot Attacks
|
||||
|
||||
|
|
|
@ -9,45 +9,51 @@ when certain fields cannot be edited due to permissions. Creating the form is do
|
|||
|
||||
To make an entire [Form](api:SilverStripe\Forms\Form) read-only.
|
||||
|
||||
:::php
|
||||
$form = new Form(..);
|
||||
$form->makeReadonly();
|
||||
|
||||
```php
|
||||
$form = new Form(..);
|
||||
$form->makeReadonly();
|
||||
```
|
||||
|
||||
To make all the fields within a [FieldList](api:SilverStripe\Forms\FieldList) read-only (i.e to make fields read-only but not buttons).
|
||||
|
||||
:::php
|
||||
$fields = new FieldList(..);
|
||||
$fields = $fields->makeReadonly();
|
||||
|
||||
```php
|
||||
$fields = new FieldList(..);
|
||||
$fields = $fields->makeReadonly();
|
||||
```
|
||||
|
||||
To make a [FormField](api:SilverStripe\Forms\FormField) read-only you need to know the name of the form field or call it direct on the object
|
||||
|
||||
:::php
|
||||
$field = new TextField(..);
|
||||
$field = $field->performReadonlyTransformation();
|
||||
|
||||
$fields = new FieldList(
|
||||
$field
|
||||
);
|
||||
```php
|
||||
$field = new TextField(..);
|
||||
$field = $field->performReadonlyTransformation();
|
||||
|
||||
// Or,
|
||||
$field = new TextField(..);
|
||||
$field->setReadonly(true);
|
||||
$fields = new FieldList(
|
||||
$field
|
||||
);
|
||||
|
||||
$fields = new FieldList(
|
||||
$field
|
||||
);
|
||||
// Or,
|
||||
$field = new TextField(..);
|
||||
$field->setReadonly(true);
|
||||
|
||||
$fields = new FieldList(
|
||||
$field
|
||||
);
|
||||
```
|
||||
|
||||
## Disabled FormFields
|
||||
|
||||
Disabling [FormField](api:SilverStripe\Forms\FormField) instances, sets the `disabled` property on the class. This will use the same HTML markup as
|
||||
a normal form, but set the `disabled` attribute on the `input` tag.
|
||||
|
||||
:::php
|
||||
$field = new TextField(..);
|
||||
$field->setDisabled(true);
|
||||
```php
|
||||
$field = new TextField(..);
|
||||
$field->setDisabled(true);
|
||||
|
||||
echo $field->forTemplate();
|
||||
echo $field->forTemplate();
|
||||
|
||||
// returns '<input type="text" class="text" .. disabled="disabled" />'
|
||||
// returns '<input type="text" class="text" .. disabled="disabled" />'
|
||||
|
||||
```
|
|
@ -16,39 +16,50 @@ name, and normally they all exist under the `Root` [TabSet](api:SilverStripe\For
|
|||
|
||||
<div class="notice" markdown="1">
|
||||
[TabSet](api:SilverStripe\Forms\TabSet) instances can contain child [Tab](api:SilverStripe\Forms\Tab) and further [TabSet](api:SilverStripe\Forms\TabSet) instances, however the CMS UI will only
|
||||
display up to two levels of tabs in the interface. If you want to group data further than that, try [ToggleField](api:ToggleField).
|
||||
display up to two levels of tabs in the interface.
|
||||
</div>
|
||||
|
||||
## Adding a field to a tab
|
||||
|
||||
:::php
|
||||
$fields->addFieldToTab('Root.Main', new TextField(..));
|
||||
|
||||
```php
|
||||
$fields->addFieldToTab('Root.Main', new TextField(..));
|
||||
```
|
||||
|
||||
## Removing a field from a tab
|
||||
|
||||
:::php
|
||||
$fields->removeFieldFromTab('Root.Main', 'Content');
|
||||
|
||||
|
||||
```php
|
||||
$fields->removeFieldFromTab('Root.Main', 'Content');
|
||||
```
|
||||
|
||||
## Creating a new tab
|
||||
|
||||
:::php
|
||||
$fields->addFieldToTab('Root.MyNewTab', new TextField(..));
|
||||
|
||||
```php
|
||||
$fields->addFieldToTab('Root.MyNewTab', new TextField(..));
|
||||
```
|
||||
|
||||
## Moving a field between tabs
|
||||
|
||||
:::php
|
||||
$content = $fields->dataFieldByName('Content');
|
||||
|
||||
$fields->removeFieldFromTab('Root.Main', 'Content');
|
||||
$fields->addFieldToTab('Root.MyContent', $content);
|
||||
```php
|
||||
$content = $fields->dataFieldByName('Content');
|
||||
|
||||
$fields->removeFieldFromTab('Root.Main', 'Content');
|
||||
$fields->addFieldToTab('Root.MyContent', $content);
|
||||
```
|
||||
|
||||
## Add multiple fields at once
|
||||
|
||||
:::php
|
||||
$fields->addFieldsToTab('Root.Content', array(
|
||||
TextField::create('Name'),
|
||||
TextField::create('Email')
|
||||
));
|
||||
|
||||
```php
|
||||
$fields->addFieldsToTab('Root.Content', [
|
||||
TextField::create('Name'),
|
||||
TextField::create('Email')
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ on the SilverStripe API documentation.
|
|||
## Actions
|
||||
|
||||
* [FormAction](api:SilverStripe\Forms\FormAction): Button element for forms, both for `<input type="submit">` and `<button>`.
|
||||
* [ResetFormAction](api:ResetFormAction): Action that clears all fields on a form.
|
||||
|
||||
## Formatted input
|
||||
|
||||
|
@ -53,7 +52,6 @@ doesn't necessarily have any visible styling.
|
|||
## Relations
|
||||
|
||||
* [CheckboxSetField](api:SilverStripe\Forms\CheckboxSetField): Displays a set of checkboxes as a logical group.
|
||||
* [TableField](api:TableField): In-place editing of tabular data.
|
||||
* [TreeDropdownField](api:SilverStripe\Forms\TreeDropdownField): Dropdown-like field that allows you to select an item from a hierarchical AJAX-expandable tree.
|
||||
* [TreeMultiselectField](api:SilverStripe\Forms\TreeMultiselectField): Represents many-many joins using a tree selector shown in a dropdown-like element
|
||||
* [GridField](api:SilverStripe\Forms\GridField\GridField): Displays a [SS_List](api:SilverStripe\ORM\SS_List) in a tabular format. Versatile base class which can be configured to allow editing, sorting, etc.
|
||||
|
|
|
@ -14,37 +14,45 @@ The following example will add a simple DateField to your Page, allowing you to
|
|||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Page extends SiteTree {
|
||||
```php
|
||||
use SilverStripe\Forms\DateField;
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
|
||||
private static $db = array(
|
||||
'MyDate' => 'Date',
|
||||
);
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$fields->addFieldToTab(
|
||||
'Root.Main',
|
||||
DateField::create('MyDate', 'Enter a date')
|
||||
);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
class Page extends SiteTree
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'MyDate' => 'Date',
|
||||
];
|
||||
|
||||
public function getCMSFields()
|
||||
{
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$fields->addFieldToTab(
|
||||
'Root.Main',
|
||||
DateField::create('MyDate', 'Enter a date')
|
||||
);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Custom Date Format
|
||||
|
||||
A custom date format for a [DateField](api:SilverStripe\Forms\DateField) can be provided through `setDateFormat`.
|
||||
This is only necessary if you want to opt-out of the built-in browser localisation via `type=date`.
|
||||
|
||||
:::php
|
||||
// will display a date in the following format: 31/06/2012
|
||||
DateField::create('MyDate')
|
||||
->setHTML5(false)
|
||||
->setDateFormat('dd/MM/yyyy');
|
||||
|
||||
```php
|
||||
// will display a date in the following format: 31/06/2012
|
||||
DateField::create('MyDate')
|
||||
->setHTML5(false)
|
||||
->setDateFormat('dd/MM/yyyy');
|
||||
```
|
||||
|
||||
<div class="info" markdown="1">
|
||||
The formats are based on [ICU format](http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details).
|
||||
|
@ -56,10 +64,12 @@ The formats are based on [ICU format](http://www.icu-project.org/apiref/icu4c/cl
|
|||
Sets the minimum and maximum allowed date values using the `min` and `max` configuration settings (in ISO format or
|
||||
`strtotime()`).
|
||||
|
||||
:::php
|
||||
DateField::create('MyDate')
|
||||
->setMinDate('-7 days')
|
||||
->setMaxDate('2012-12-31')
|
||||
|
||||
```php
|
||||
DateField::create('MyDate')
|
||||
->setMinDate('-7 days')
|
||||
->setMaxDate('2012-12-31')
|
||||
```
|
||||
|
||||
## Formatting Hints
|
||||
|
||||
|
@ -67,18 +77,20 @@ It's often not immediate apparent which format a field accepts, and showing the
|
|||
of limited use to the average user. An alternative is to show the current date in the desired format alongside the
|
||||
field description as an example.
|
||||
|
||||
:::php
|
||||
$dateField = DateField::create('MyDate');
|
||||
|
||||
// Show long format as text below the field
|
||||
$dateField->setDescription(_t(
|
||||
'FormField.Example',
|
||||
'e.g. {format}',
|
||||
[ 'format' => $dateField->getDateFormat() ]
|
||||
));
|
||||
```php
|
||||
$dateField = DateField::create('MyDate');
|
||||
|
||||
// Alternatively, set short format as a placeholder in the field
|
||||
$dateField->setAttribute('placeholder', $dateField->getDateFormat());
|
||||
// Show long format as text below the field
|
||||
$dateField->setDescription(_t(
|
||||
'FormField.Example',
|
||||
'e.g. {format}',
|
||||
[ 'format' => $dateField->getDateFormat() ]
|
||||
));
|
||||
|
||||
// Alternatively, set short format as a placeholder in the field
|
||||
$dateField->setAttribute('placeholder', $dateField->getDateFormat());
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
Fields scaffolded through [DataObject::scaffoldCMSFields()](api:SilverStripe\ORM\DataObject::scaffoldCMSFields()) automatically have a description attached to them.
|
||||
|
|
|
@ -15,21 +15,28 @@ functionality. It is usually added through the [DataObject::getCMSFields()](api:
|
|||
|
||||
**mysite/code/MyObject.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Content' => 'HTMLText'
|
||||
);
|
||||
|
||||
public function getCMSFields() {
|
||||
return new FieldList(
|
||||
new HTMLEditorField('Content')
|
||||
);
|
||||
}
|
||||
}
|
||||
```php
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'Content' => 'HTMLText'
|
||||
];
|
||||
|
||||
public function getCMSFields()
|
||||
{
|
||||
return new FieldList(
|
||||
new HTMLEditorField('Content')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Specify which configuration to use
|
||||
|
||||
|
@ -42,20 +49,29 @@ will use the configuration with the name 'myConfig'.
|
|||
You can also specify which [HtmlEditorConfig](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig) to use on a per field basis via the construct argument.
|
||||
This is particularly useful if you need different configurations for multiple [HTMLEditorField](api:SilverStripe\Forms\HTMLEditor\HTMLEditorField) on the same page or form.
|
||||
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
private static $db = array(
|
||||
'Content' => 'HTMLText',
|
||||
'OtherContent' => 'HTMLText'
|
||||
);
|
||||
|
||||
public function getCMSFields() {
|
||||
return new FieldList(array(
|
||||
new HTMLEditorField('Content'),
|
||||
new HTMLEditorField('OtherContent', 'Other content', $this->OtherContent, 'myConfig')
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
private static $db = [
|
||||
'Content' => 'HTMLText',
|
||||
'OtherContent' => 'HTMLText'
|
||||
];
|
||||
|
||||
public function getCMSFields()
|
||||
{
|
||||
return new FieldList([
|
||||
new HTMLEditorField('Content'),
|
||||
new HTMLEditorField('OtherContent', 'Other content', $this->OtherContent, 'myConfig')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
In the above example, the 'Content' field will use the default 'cms' config while 'OtherContent' will be using 'myConfig'.
|
||||
|
||||
|
@ -85,8 +101,10 @@ You can add plugins to the editor using the Framework's [HtmlEditorConfig::enabl
|
|||
transparently generate the relevant underlying TinyMCE code.
|
||||
|
||||
**mysite/_config.php**
|
||||
:::php
|
||||
HtmlEditorConfig::get('cms')->enablePlugins('media');
|
||||
|
||||
```php
|
||||
HtmlEditorConfig::get('cms')->enablePlugins('media');
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
This utilities the TinyMCE's `PluginManager::load` function under the hood (check the
|
||||
|
@ -98,14 +116,18 @@ Plugins and advanced themes can provide additional buttons that can be added (or
|
|||
configuration. Here is an example of adding a `ssmacron` button after the `charmap` button:
|
||||
|
||||
**mysite/_config.php**
|
||||
:::php
|
||||
HtmlEditorConfig::get('cms')->insertButtonsAfter('charmap', 'ssmacron');
|
||||
|
||||
```php
|
||||
HtmlEditorConfig::get('cms')->insertButtonsAfter('charmap', 'ssmacron');
|
||||
```
|
||||
|
||||
Buttons can also be removed:
|
||||
|
||||
**mysite/_config.php**
|
||||
:::php
|
||||
HtmlEditorConfig::get('cms')->removeButtons('tablecontrols', 'blockquote', 'hr');
|
||||
|
||||
```php
|
||||
HtmlEditorConfig::get('cms')->removeButtons('tablecontrols', 'blockquote', 'hr');
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
Internally [HtmlEditorConfig](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig) uses the TinyMCE's `theme_advanced_buttons` option to configure these. See the
|
||||
|
@ -123,19 +145,21 @@ tags](http://www.tinymce.com/wiki.php/Configuration:extended_valid_elements) - t
|
|||
from the HTML source by the editor.
|
||||
|
||||
**mysite/_config.php**
|
||||
:::php
|
||||
// Add start and type attributes for <ol>, add <object> and <embed> with all attributes.
|
||||
HtmlEditorConfig::get('cms')->setOption(
|
||||
'extended_valid_elements',
|
||||
'img[class|src|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name|usemap],' .
|
||||
'iframe[src|name|width|height|title|align|allowfullscreen|frameborder|marginwidth|marginheight|scrolling],' .
|
||||
'object[classid|codebase|width|height|data|type],' .
|
||||
'embed[src|type|pluginspage|width|height|autoplay],' .
|
||||
'param[name|value],' .
|
||||
'map[class|name|id],' .
|
||||
'area[shape|coords|href|target|alt],' .
|
||||
'ol[start|type]'
|
||||
);
|
||||
|
||||
```php
|
||||
// Add start and type attributes for <ol>, add <object> and <embed> with all attributes.
|
||||
HtmlEditorConfig::get('cms')->setOption(
|
||||
'extended_valid_elements',
|
||||
'img[class|src|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name|usemap],' .
|
||||
'iframe[src|name|width|height|title|align|allowfullscreen|frameborder|marginwidth|marginheight|scrolling],' .
|
||||
'object[classid|codebase|width|height|data|type],' .
|
||||
'embed[src|type|pluginspage|width|height|autoplay],' .
|
||||
'param[name|value],' .
|
||||
'map[class|name|id],' .
|
||||
'area[shape|coords|href|target|alt],' .
|
||||
'ol[start|type]'
|
||||
);
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
The default setting for the CMS's `extended_valid_elements` we are overriding here can be found in
|
||||
|
@ -148,8 +172,11 @@ It is also possible to add custom plugins to TinyMCE, for example toolbar button
|
|||
You can enable them through [HtmlEditorConfig::enablePlugins()](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig::enablePlugins()):
|
||||
|
||||
**mysite/_config.php**
|
||||
:::php
|
||||
HtmlEditorConfig::get('cms')->enablePlugins(array('myplugin' => '../../../mysite/javascript/myplugin/editor_plugin.js'));
|
||||
|
||||
```php
|
||||
HtmlEditorConfig::get('cms')->enablePlugins(['myplugin' => '../../../mysite/javascript/myplugin/editor_plugin.js']);
|
||||
|
||||
```
|
||||
|
||||
You can learn how to [create a plugin](http://www.tinymce.com/wiki.php/Creating_a_plugin) from the TinyMCE documentation.
|
||||
|
||||
|
@ -200,8 +227,10 @@ defaults to the stricter 'xhtml' setting, for example rendering self closing tag
|
|||
|
||||
In case you want to adhere to HTML4 instead, use the following configuration:
|
||||
|
||||
:::php
|
||||
HtmlEditorConfig::get('cms')->setOption('element_format', 'html');
|
||||
|
||||
```php
|
||||
HtmlEditorConfig::get('cms')->setOption('element_format', 'html');
|
||||
```
|
||||
|
||||
By default, TinyMCE and SilverStripe will generate valid HTML5 markup, but it will strip out HTML5 tags like
|
||||
`<article>` or `<figure>`. If you plan to use those, add them to the
|
||||
|
@ -223,17 +252,26 @@ back and forth between a content representation the editor can understand, prese
|
|||
|
||||
Example: Remove field for "image captions"
|
||||
|
||||
:::php
|
||||
// File: mysite/code/MyToolbarExtension.php
|
||||
class MyToolbarExtension extends Extension {
|
||||
public function updateFieldsForImage(&$fields, $url, $file) {
|
||||
$fields->removeByName('CaptionText');
|
||||
}
|
||||
}
|
||||
|
||||
:::php
|
||||
// File: mysite/_config.php
|
||||
ModalController::add_extension('MyToolbarExtension');
|
||||
```php
|
||||
use SilverStripe\Core\Extension;
|
||||
|
||||
// File: mysite/code/MyToolbarExtension.php
|
||||
class MyToolbarExtension extends Extension
|
||||
{
|
||||
public function updateFieldsForImage(&$fields, $url, $file)
|
||||
{
|
||||
$fields->removeByName('CaptionText');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
```php
|
||||
// File: mysite/_config.php
|
||||
ModalController::add_extension('MyToolbarExtension');
|
||||
```
|
||||
|
||||
Adding functionality is a bit more advanced, you'll most likely
|
||||
need to add some fields to the PHP forms, as well as write some
|
||||
|
@ -258,22 +296,31 @@ encapsulated in the [ModalController](api:SilverStripe\Admin\ModalController) cl
|
|||
In the CMS, those dialogs are automatically instantiate, but in your own interfaces outside
|
||||
of the CMS you have to take care of instantiate yourself:
|
||||
|
||||
:::php
|
||||
// File: mysite/code/MyController.php
|
||||
class MyObjectController extends Controller {
|
||||
public function Modals() {
|
||||
return ModalController::create($this, "Modals");
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Admin\ModalController;
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
// File: mysite/code/MyController.php
|
||||
class MyObjectController extends Controller
|
||||
{
|
||||
public function Modals()
|
||||
{
|
||||
return ModalController::create($this, "Modals");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: The dialogs rely on CMS-access, e.g. for uploading and browsing files,
|
||||
so this is considered advanced usage of the field.
|
||||
|
||||
:::php
|
||||
// File: mysite/_config.php
|
||||
HtmlEditorConfig::get('cms')->disablePlugins('ssbuttons');
|
||||
HtmlEditorConfig::get('cms')->removeButtons('sslink', 'ssmedia');
|
||||
HtmlEditorConfig::get('cms')->addButtonsToLine(2, 'link', 'media');
|
||||
|
||||
```php
|
||||
// File: mysite/_config.php
|
||||
HtmlEditorConfig::get('cms')->disablePlugins('ssbuttons');
|
||||
HtmlEditorConfig::get('cms')->removeButtons('sslink', 'ssmedia');
|
||||
HtmlEditorConfig::get('cms')->addButtonsToLine(2, 'link', 'media');
|
||||
```
|
||||
|
||||
### Developing a wrapper to use a different WYSIWYG editors with HTMLEditorField
|
||||
|
||||
|
@ -293,17 +340,24 @@ Most modern browsers support it, although Internet Explorer only has limited
|
|||
support in IE10. Alternatively, you can use the PSpell PHP module for server side checks.
|
||||
Assuming you have the module installed, here's how you enable its use in `mysite/_config.php`:
|
||||
|
||||
:::php
|
||||
HtmlEditorConfig::get('cms')->enablePlugins('spellchecker', 'contextmenu');
|
||||
HtmlEditorConfig::get('cms')->addButtonsToLine(2, 'spellchecker');
|
||||
HtmlEditorConfig::get('cms')->setOption(
|
||||
'spellchecker_rpc_url',
|
||||
THIRDPARTY_DIR . '/tinymce-spellchecker/rpc.php'
|
||||
);
|
||||
HtmlEditorConfig::get('cms')->setOption('browser_spellcheck', false);
|
||||
|
||||
```php
|
||||
HtmlEditorConfig::get('cms')->enablePlugins('spellchecker', 'contextmenu');
|
||||
HtmlEditorConfig::get('cms')->addButtonsToLine(2, 'spellchecker');
|
||||
HtmlEditorConfig::get('cms')->setOption(
|
||||
'spellchecker_rpc_url',
|
||||
THIRDPARTY_DIR . '/tinymce-spellchecker/rpc.php'
|
||||
);
|
||||
HtmlEditorConfig::get('cms')->setOption('browser_spellcheck', false);
|
||||
```
|
||||
|
||||
Now change the default spellchecker in `framework/thirdparty/tinymce-spellchecker/config.php`:
|
||||
|
||||
:::php
|
||||
// ...
|
||||
$config['general.engine'] = 'PSpell';
|
||||
|
||||
```php
|
||||
|
||||
// ...
|
||||
$config['general.engine'] = 'PSpell';
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -6,10 +6,10 @@ summary: How to use the GridField class for managing tabular data.
|
|||
[GridField](api:SilverStripe\Forms\GridField\GridField) is SilverStripe's implementation of data grids. The main purpose of the `FormField` is to display
|
||||
tabular data in a format that is easy to view and modify. It can be thought of as a HTML table with some tricks.
|
||||
|
||||
:::php
|
||||
$field = new GridField($name, $title, $list);
|
||||
|
||||
|
||||
```php
|
||||
$field = new GridField($name, $title, $list);
|
||||
```
|
||||
|
||||
<div class="hint" markdown='1'>
|
||||
GridField can only be used with `$list` data sets that are of the type `SS_List` such as `DataList` or `ArrayList`.
|
||||
|
@ -27,21 +27,26 @@ actions such as deleting records.
|
|||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Page extends SiteTree {
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
```php
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
|
||||
$fields->addFieldToTab('Root.Pages',
|
||||
new GridField('Pages', 'All pages', SiteTree::get())
|
||||
);
|
||||
class Page extends SiteTree
|
||||
{
|
||||
|
||||
public function getCMSFields()
|
||||
{
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
$fields->addFieldToTab('Root.Pages',
|
||||
new GridField('Pages', 'All pages', SiteTree::get())
|
||||
);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This will display a bare bones `GridField` instance under `Pages` tab in the CMS. As we have not specified the
|
||||
`GridField` configuration, the default configuration is an instance of [GridFieldConfig_Base](api:SilverStripe\Forms\GridField\GridFieldConfig_Base) which provides:
|
||||
|
@ -58,75 +63,85 @@ the `getConfig()` method on `GridField`.
|
|||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Page extends SiteTree {
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
```php
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
|
||||
$fields->addFieldToTab('Root.Pages',
|
||||
$grid = new GridField('Pages', 'All pages', SiteTree::get())
|
||||
);
|
||||
class Page extends SiteTree
|
||||
{
|
||||
|
||||
public function getCMSFields()
|
||||
{
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
// GridField configuration
|
||||
$config = $grid->getConfig();
|
||||
$fields->addFieldToTab('Root.Pages',
|
||||
$grid = new GridField('Pages', 'All pages', SiteTree::get())
|
||||
);
|
||||
|
||||
//
|
||||
// Modification of existing components can be done by fetching that component.
|
||||
// Consult the API documentation for each component to determine the configuration
|
||||
// you can do.
|
||||
//
|
||||
$dataColumns = $config->getComponentByType('GridFieldDataColumns');
|
||||
|
||||
$dataColumns->setDisplayFields(array(
|
||||
'Title' => 'Title',
|
||||
'Link'=> 'URL',
|
||||
'LastEdited' => 'Changed'
|
||||
));
|
||||
// GridField configuration
|
||||
$config = $grid->getConfig();
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
//
|
||||
// Modification of existing components can be done by fetching that component.
|
||||
// Consult the API documentation for each component to determine the configuration
|
||||
// you can do.
|
||||
//
|
||||
$dataColumns = $config->getComponentByType('GridFieldDataColumns');
|
||||
|
||||
$dataColumns->setDisplayFields([
|
||||
'Title' => 'Title',
|
||||
'Link'=> 'URL',
|
||||
'LastEdited' => 'Changed'
|
||||
]);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
With the `GridFieldConfig` instance, we can modify the behavior of the `GridField`.
|
||||
```
|
||||
|
||||
:::php
|
||||
// `GridFieldConfig::create()` will create an empty configuration (no components).
|
||||
$config = GridFieldConfig::create();
|
||||
With the `GridFieldConfig` instance, we can modify the behavior of the `GridField`.
|
||||
```php
|
||||
// `GridFieldConfig::create()` will create an empty configuration (no components).
|
||||
$config = GridFieldConfig::create();
|
||||
|
||||
// add a component
|
||||
$config->addComponent(new GridFieldDataColumns());
|
||||
// add a component
|
||||
$config->addComponent(new GridFieldDataColumns());
|
||||
|
||||
// Update the GridField with our custom configuration
|
||||
$gridField->setConfig($config);
|
||||
// Update the GridField with our custom configuration
|
||||
$gridField->setConfig($config);
|
||||
```
|
||||
|
||||
`GridFieldConfig` provides a number of methods to make setting the configuration easier. We can insert a component
|
||||
before another component by passing the second parameter.
|
||||
|
||||
:::php
|
||||
$config->addComponent(new GridFieldFilterHeader(), 'GridFieldDataColumns');
|
||||
|
||||
```php
|
||||
$config->addComponent(new GridFieldFilterHeader(), 'GridFieldDataColumns');
|
||||
```
|
||||
|
||||
We can add multiple components in one call.
|
||||
|
||||
:::php
|
||||
$config->addComponents(
|
||||
new GridFieldDataColumns(),
|
||||
new GridFieldToolbarHeader()
|
||||
);
|
||||
|
||||
```php
|
||||
$config->addComponents(
|
||||
new GridFieldDataColumns(),
|
||||
new GridFieldToolbarHeader()
|
||||
);
|
||||
```
|
||||
|
||||
Or, remove a component.
|
||||
|
||||
:::php
|
||||
$config->removeComponentsByType('GridFieldDeleteAction');
|
||||
|
||||
```php
|
||||
$config->removeComponentsByType('GridFieldDeleteAction');
|
||||
```
|
||||
|
||||
Fetch a component to modify it later on.
|
||||
|
||||
:::php
|
||||
$component = $config->getComponentByType('GridFieldFilterHeader')
|
||||
|
||||
```php
|
||||
$component = $config->getComponentByType('GridFieldFilterHeader')
|
||||
```
|
||||
|
||||
Here is a list of components for use bundled with the core framework. Many more components are provided by third-party
|
||||
modules and extensions.
|
||||
|
@ -152,18 +167,20 @@ developers manually adding each component.
|
|||
|
||||
A simple read-only and paginated view of records with sortable and searchable headers.
|
||||
|
||||
:::php
|
||||
$config = GridFieldConfig_Base::create();
|
||||
|
||||
$gridField->setConfig($config);
|
||||
```php
|
||||
$config = GridFieldConfig_Base::create();
|
||||
|
||||
// Is the same as adding the following components..
|
||||
// .. new GridFieldToolbarHeader()
|
||||
// .. new GridFieldSortableHeader()
|
||||
// .. new GridFieldFilterHeader()
|
||||
// .. new GridFieldDataColumns()
|
||||
// .. new GridFieldPageCount('toolbar-header-right')
|
||||
// .. new GridFieldPaginator($itemsPerPage)
|
||||
$gridField->setConfig($config);
|
||||
|
||||
// Is the same as adding the following components..
|
||||
// .. new GridFieldToolbarHeader()
|
||||
// .. new GridFieldSortableHeader()
|
||||
// .. new GridFieldFilterHeader()
|
||||
// .. new GridFieldDataColumns()
|
||||
// .. new GridFieldPageCount('toolbar-header-right')
|
||||
// .. new GridFieldPaginator($itemsPerPage)
|
||||
```
|
||||
|
||||
### GridFieldConfig_RecordViewer
|
||||
|
||||
|
@ -180,14 +197,16 @@ The `DataObject` class displayed must define a `canView()` method that returns a
|
|||
this record.
|
||||
</div>
|
||||
|
||||
:::php
|
||||
$config = GridFieldConfig_RecordViewer::create();
|
||||
|
||||
$gridField->setConfig($config);
|
||||
|
||||
// Same as GridFieldConfig_Base with the addition of
|
||||
// .. new GridFieldViewButton(),
|
||||
// .. new GridFieldDetailForm()
|
||||
```php
|
||||
$config = GridFieldConfig_RecordViewer::create();
|
||||
|
||||
$gridField->setConfig($config);
|
||||
|
||||
// Same as GridFieldConfig_Base with the addition of
|
||||
// .. new GridFieldViewButton(),
|
||||
// .. new GridFieldDetailForm()
|
||||
```
|
||||
|
||||
### GridFieldConfig_RecordEditor
|
||||
|
||||
|
@ -203,25 +222,27 @@ Permission control for editing and deleting the record uses the `canEdit()` and
|
|||
`DataObject` object.
|
||||
</div>
|
||||
|
||||
:::php
|
||||
$config = GridFieldConfig_RecordEditor::create();
|
||||
|
||||
$gridField->setConfig($config);
|
||||
|
||||
// Same as GridFieldConfig_RecordViewer with the addition of
|
||||
// .. new GridFieldAddNewButton(),
|
||||
// .. new GridFieldEditButton(),
|
||||
// .. new GridFieldDeleteAction()
|
||||
```php
|
||||
$config = GridFieldConfig_RecordEditor::create();
|
||||
|
||||
$gridField->setConfig($config);
|
||||
|
||||
// Same as GridFieldConfig_RecordViewer with the addition of
|
||||
// .. new GridFieldAddNewButton(),
|
||||
// .. new GridFieldEditButton(),
|
||||
// .. new GridFieldDeleteAction()
|
||||
```
|
||||
|
||||
### GridFieldConfig_RelationEditor
|
||||
|
||||
Similar to `GridFieldConfig_RecordEditor`, but adds features to work on a record's has-many or many-many relationships.
|
||||
As such, it expects the list used with the `GridField` to be a instance of `RelationList`.
|
||||
As such, it expects the list used with the `GridField` to be a instance of `RelationList`.
|
||||
```php
|
||||
$config = GridFieldConfig_RelationEditor::create();
|
||||
|
||||
:::php
|
||||
$config = GridFieldConfig_RelationEditor::create();
|
||||
|
||||
$gridField->setConfig($config);
|
||||
$gridField->setConfig($config);
|
||||
```
|
||||
|
||||
This configuration adds the ability to searched for existing records and add a relationship
|
||||
(`GridFieldAddExistingAutocompleter`).
|
||||
|
@ -235,11 +256,13 @@ The `GridFieldDetailForm` component drives the record viewing and editing form.
|
|||
`DataObject->getCMSFields()` method but can be customised to accept different fields via the
|
||||
[GridFieldDetailForm::setFields()](api:SilverStripe\Forms\GridField\GridFieldDetailForm::setFields()) method.
|
||||
|
||||
:::php
|
||||
$form = $gridField->getConfig()->getComponentByType('GridFieldDetailForm');
|
||||
$form->setFields(new FieldList(
|
||||
new TextField('Title')
|
||||
));
|
||||
|
||||
```php
|
||||
$form = $gridField->getConfig()->getComponentByType('GridFieldDetailForm');
|
||||
$form->setFields(new FieldList(
|
||||
new TextField('Title')
|
||||
));
|
||||
```
|
||||
|
||||
### many_many_extraFields
|
||||
|
||||
|
@ -252,58 +275,65 @@ them as fields for relation extra data, and to avoid clashes with the other form
|
|||
|
||||
The namespace notation is `ManyMany[<extradata-field-name>]`, so for example `ManyMany[MyExtraField]`.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Team extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Text'
|
||||
);
|
||||
```php
|
||||
use SilverStripe\Forms\TextField;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
public static $many_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
}
|
||||
class Team extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'Name' => 'Text'
|
||||
];
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Text'
|
||||
);
|
||||
|
||||
public static $many_many = array(
|
||||
'Teams' => 'Team'
|
||||
);
|
||||
|
||||
public static $many_many_extraFields = array(
|
||||
'Teams' => array(
|
||||
'Position' => 'Text'
|
||||
)
|
||||
);
|
||||
public static $many_many = [
|
||||
'Players' => 'Player'
|
||||
];
|
||||
}
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'Name' => 'Text'
|
||||
];
|
||||
|
||||
public static $many_many = [
|
||||
'Teams' => 'Team'
|
||||
];
|
||||
|
||||
public static $many_many_extraFields = [
|
||||
'Teams' => [
|
||||
'Position' => 'Text'
|
||||
]
|
||||
];
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
public function getCMSFields()
|
||||
{
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
if($this->ID) {
|
||||
$teamFields = singleton('Team')->getCMSFields();
|
||||
$teamFields->addFieldToTab(
|
||||
'Root.Main',
|
||||
// The "ManyMany[<extradata-name>]" convention
|
||||
new TextField('ManyMany[Position]', 'Current Position')
|
||||
);
|
||||
if($this->ID) {
|
||||
$teamFields = singleton('Team')->getCMSFields();
|
||||
$teamFields->addFieldToTab(
|
||||
'Root.Main',
|
||||
// The "ManyMany[<extradata-name>]" convention
|
||||
new TextField('ManyMany[Position]', 'Current Position')
|
||||
);
|
||||
|
||||
$config = GridFieldConfig_RelationEditor::create();
|
||||
$config->getComponentByType('GridFieldDetailForm')->setFields($teamFields);
|
||||
$config = GridFieldConfig_RelationEditor::create();
|
||||
$config->getComponentByType('GridFieldDetailForm')->setFields($teamFields);
|
||||
|
||||
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
|
||||
$fields->findOrMakeTab('Root.Teams')->replaceField('Teams', $gridField);
|
||||
}
|
||||
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
|
||||
$fields->findOrMakeTab('Root.Teams')->replaceField('Teams', $gridField);
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Flexible Area Assignment through Fragments
|
||||
|
||||
|
@ -322,10 +352,11 @@ These built-ins can be used by passing the fragment names into the constructor o
|
|||
[GridFieldConfig](api:SilverStripe\Forms\GridField\GridFieldConfig) classes will already have rows added to them. The following example will add a print button at the
|
||||
bottom right of the table.
|
||||
|
||||
:::php
|
||||
$config->addComponent(new GridFieldButtonRow('after'));
|
||||
$config->addComponent(new GridFieldPrintButton('buttons-after-right'));
|
||||
|
||||
```php
|
||||
$config->addComponent(new GridFieldButtonRow('after'));
|
||||
$config->addComponent(new GridFieldPrintButton('buttons-after-right'));
|
||||
```
|
||||
|
||||
### Creating your own Fragments
|
||||
|
||||
|
@ -333,17 +364,20 @@ Fragments are designated areas within a `GridField` which can be shared between
|
|||
your own fragments by using a `\$DefineFragment' placeholder in your components' template. This example will simply
|
||||
create an area rendered before the table wrapped in a simple `<div>`.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyAreaComponent implements GridField_HTMLProvider {
|
||||
|
||||
public function getHTMLFragments( $gridField) {
|
||||
return array(
|
||||
'before' => '<div class="my-area">$DefineFragment(my-area)</div>'
|
||||
);
|
||||
}
|
||||
}
|
||||
```php
|
||||
class MyAreaComponent implements GridField_HTMLProvider
|
||||
{
|
||||
|
||||
public function getHTMLFragments( $gridField)
|
||||
{
|
||||
return [
|
||||
'before' => '<div class="my-area">$DefineFragment(my-area)</div>'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
Please note that in templates, you'll need to escape the dollar sign on `\$DefineFragment`. These are specially
|
||||
|
@ -353,22 +387,27 @@ processed placeholders as opposed to native template syntax.
|
|||
Now you can add other components into this area by returning them as an array from your
|
||||
[GridFieldComponent::getHTMLFragments()](api:SilverStripe\Forms\GridField\GridFieldComponent::getHTMLFragments()) implementation:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyShareLinkComponent implements GridField_HTMLProvider {
|
||||
|
||||
public function getHTMLFragments( $gridField) {
|
||||
return array(
|
||||
'my-area' => '<a href>...</a>'
|
||||
);
|
||||
}
|
||||
}
|
||||
```php
|
||||
class MyShareLinkComponent implements GridField_HTMLProvider
|
||||
{
|
||||
|
||||
public function getHTMLFragments( $gridField)
|
||||
{
|
||||
return [
|
||||
'my-area' => '<a href>...</a>'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Your new area can also be used by existing components, e.g. the [GridFieldPrintButton](api:SilverStripe\Forms\GridField\GridFieldPrintButton)
|
||||
|
||||
:::php
|
||||
new GridFieldPrintButton('my-component-area');
|
||||
|
||||
```php
|
||||
new GridFieldPrintButton('my-component-area');
|
||||
```
|
||||
|
||||
## Creating a Custom GridFieldComponent
|
||||
|
||||
|
|
|
@ -9,135 +9,168 @@ code for a `Form` is to create it as a subclass to `Form`. Let's look at a examp
|
|||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class PageController extends ContentController {
|
||||
|
||||
public function SearchForm() {
|
||||
$fields = new FieldList(
|
||||
HeaderField::create('Header', 'Step 1. Basics'),
|
||||
OptionsetField::create('Type', '', array(
|
||||
'foo' => 'Search Foo',
|
||||
'bar' => 'Search Bar',
|
||||
'baz' => 'Search Baz'
|
||||
)),
|
||||
```php
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\RequiredFields;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\HeaderField;
|
||||
use SilverStripe\Forms\OptionsetField;
|
||||
use SilverStripe\Forms\CompositeField;
|
||||
use SilverStripe\Forms\CheckboxSetField;
|
||||
use SilverStripe\Forms\NumericField;
|
||||
use SilverStripe\Forms\FormAction;
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
CompositeField::create(
|
||||
HeaderField::create('Header2', 'Step 2. Advanced '),
|
||||
CheckboxSetField::create('Foo', 'Select Option', array(
|
||||
'qux' => 'Search Qux'
|
||||
)),
|
||||
class PageController extends ContentController
|
||||
{
|
||||
|
||||
public function SearchForm()
|
||||
{
|
||||
$fields = new FieldList(
|
||||
HeaderField::create('Header', 'Step 1. Basics'),
|
||||
OptionsetField::create('Type', '', [
|
||||
'foo' => 'Search Foo',
|
||||
'bar' => 'Search Bar',
|
||||
'baz' => 'Search Baz'
|
||||
]),
|
||||
|
||||
CheckboxSetField::create('Category', 'Category', array(
|
||||
'Foo' => 'Foo',
|
||||
'Bar' => 'Bar'
|
||||
)),
|
||||
CompositeField::create(
|
||||
HeaderField::create('Header2', 'Step 2. Advanced '),
|
||||
CheckboxSetField::create('Foo', 'Select Option', [
|
||||
'qux' => 'Search Qux'
|
||||
]),
|
||||
|
||||
NumericField::create('Minimum', 'Minimum'),
|
||||
NumericField::create('Maximum', 'Maximum')
|
||||
)
|
||||
);
|
||||
|
||||
$actions = new FieldList(
|
||||
FormAction::create('doSearchForm', 'Search')
|
||||
);
|
||||
|
||||
$required = new RequiredFields(array(
|
||||
'Type'
|
||||
));
|
||||
CheckboxSetField::create('Category', 'Category', [
|
||||
'Foo' => 'Foo',
|
||||
'Bar' => 'Bar'
|
||||
]),
|
||||
|
||||
$form = new Form($this, 'SearchForm', $fields, $actions, $required);
|
||||
$form->setFormMethod('GET');
|
||||
|
||||
$form->addExtraClass('no-action-styles');
|
||||
$form->disableSecurityToken();
|
||||
$form->loadDataFrom($_REQUEST);
|
||||
|
||||
return $form;
|
||||
}
|
||||
NumericField::create('Minimum', 'Minimum'),
|
||||
NumericField::create('Maximum', 'Maximum')
|
||||
)
|
||||
);
|
||||
|
||||
$actions = new FieldList(
|
||||
FormAction::create('doSearchForm', 'Search')
|
||||
);
|
||||
|
||||
$required = new RequiredFields([
|
||||
'Type'
|
||||
]);
|
||||
|
||||
..
|
||||
}
|
||||
$form = new Form($this, 'SearchForm', $fields, $actions, $required);
|
||||
$form->setFormMethod('GET');
|
||||
|
||||
$form->addExtraClass('no-action-styles');
|
||||
$form->disableSecurityToken();
|
||||
$form->loadDataFrom($_REQUEST);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
..
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Now that is a bit of code to include on our controller and generally makes the file look much more complex than it
|
||||
should be. Good practice would be to move this to a subclass and create a new instance for your particular controller.
|
||||
|
||||
**mysite/code/forms/SearchForm.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class SearchForm extends Form {
|
||||
```php
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\RequiredFields;
|
||||
use SilverStripe\Forms\HeaderField;
|
||||
use SilverStripe\Forms\OptionsetField;
|
||||
use SilverStripe\Forms\CompositeField;
|
||||
use SilverStripe\Forms\CheckboxSetField;
|
||||
use SilverStripe\Forms\NumericField;
|
||||
use SilverStripe\Forms\FormAction;
|
||||
use SilverStripe\Forms\Form;
|
||||
|
||||
/**
|
||||
* Our constructor only requires the controller and the name of the form
|
||||
* method. We'll create the fields and actions in here.
|
||||
*
|
||||
*/
|
||||
public function __construct($controller, $name) {
|
||||
$fields = new FieldList(
|
||||
HeaderField::create('Header', 'Step 1. Basics'),
|
||||
OptionsetField::create('Type', '', array(
|
||||
'foo' => 'Search Foo',
|
||||
'bar' => 'Search Bar',
|
||||
'baz' => 'Search Baz'
|
||||
)),
|
||||
class SearchForm extends Form
|
||||
{
|
||||
|
||||
CompositeField::create(
|
||||
HeaderField::create('Header2', 'Step 2. Advanced '),
|
||||
CheckboxSetField::create('Foo', 'Select Option', array(
|
||||
'qux' => 'Search Qux'
|
||||
)),
|
||||
/**
|
||||
* Our constructor only requires the controller and the name of the form
|
||||
* method. We'll create the fields and actions in here.
|
||||
*
|
||||
*/
|
||||
public function __construct($controller, $name)
|
||||
{
|
||||
$fields = new FieldList(
|
||||
HeaderField::create('Header', 'Step 1. Basics'),
|
||||
OptionsetField::create('Type', '', [
|
||||
'foo' => 'Search Foo',
|
||||
'bar' => 'Search Bar',
|
||||
'baz' => 'Search Baz'
|
||||
]),
|
||||
|
||||
CheckboxSetField::create('Category', 'Category', array(
|
||||
'Foo' => 'Foo',
|
||||
'Bar' => 'Bar'
|
||||
)),
|
||||
CompositeField::create(
|
||||
HeaderField::create('Header2', 'Step 2. Advanced '),
|
||||
CheckboxSetField::create('Foo', 'Select Option', [
|
||||
'qux' => 'Search Qux'
|
||||
]),
|
||||
|
||||
NumericField::create('Minimum', 'Minimum'),
|
||||
NumericField::create('Maximum', 'Maximum')
|
||||
)
|
||||
);
|
||||
|
||||
$actions = new FieldList(
|
||||
FormAction::create('doSearchForm', 'Search')
|
||||
);
|
||||
|
||||
$required = new RequiredFields(array(
|
||||
'Type'
|
||||
));
|
||||
CheckboxSetField::create('Category', 'Category', [
|
||||
'Foo' => 'Foo',
|
||||
'Bar' => 'Bar'
|
||||
]),
|
||||
|
||||
// now we create the actual form with our fields and actions defined
|
||||
// within this class
|
||||
parent::__construct($controller, $name, $fields, $actions, $required);
|
||||
NumericField::create('Minimum', 'Minimum'),
|
||||
NumericField::create('Maximum', 'Maximum')
|
||||
)
|
||||
);
|
||||
|
||||
$actions = new FieldList(
|
||||
FormAction::create('doSearchForm', 'Search')
|
||||
);
|
||||
|
||||
$required = new RequiredFields([
|
||||
'Type'
|
||||
]);
|
||||
|
||||
// any modifications we need to make to the form.
|
||||
$this->setFormMethod('GET');
|
||||
|
||||
$this->addExtraClass('no-action-styles');
|
||||
$this->disableSecurityToken();
|
||||
$this->loadDataFrom($_REQUEST);
|
||||
}
|
||||
}
|
||||
// now we create the actual form with our fields and actions defined
|
||||
// within this class
|
||||
parent::__construct($controller, $name, $fields, $actions, $required);
|
||||
|
||||
// any modifications we need to make to the form.
|
||||
$this->setFormMethod('GET');
|
||||
|
||||
$this->addExtraClass('no-action-styles');
|
||||
$this->disableSecurityToken();
|
||||
$this->loadDataFrom($_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Our controller will now just have to create a new instance of this form object. Keeping the file light and easy to read.
|
||||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class PageController extends ContentController {
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'SearchForm',
|
||||
);
|
||||
|
||||
public function SearchForm() {
|
||||
return new SearchForm($this, 'SearchForm');
|
||||
}
|
||||
}
|
||||
```php
|
||||
use SearchForm;
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
|
||||
private static $allowed_actions = [
|
||||
'SearchForm',
|
||||
];
|
||||
|
||||
public function SearchForm()
|
||||
{
|
||||
return new SearchForm($this, 'SearchForm');
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Form actions can also be defined within your `Form` subclass to keep the entire form logic encapsulated.
|
||||
|
||||
|
|
|
@ -11,36 +11,41 @@ totally custom template to meet our needs. To do this, we'll provide the class w
|
|||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
public function SearchForm() {
|
||||
$fields = new FieldList(
|
||||
TextField::create('q')
|
||||
);
|
||||
```php
|
||||
|
||||
public function SearchForm()
|
||||
{
|
||||
$fields = new FieldList(
|
||||
TextField::create('q')
|
||||
);
|
||||
|
||||
$actions = new FieldList(
|
||||
FormAction::create('doSearch', 'Search')
|
||||
);
|
||||
$actions = new FieldList(
|
||||
FormAction::create('doSearch', 'Search')
|
||||
);
|
||||
|
||||
$form = new Form($this, 'SearchForm', $fields, $actions);
|
||||
$form->setTemplate('SearchForm');
|
||||
$form = new Form($this, 'SearchForm', $fields, $actions);
|
||||
$form->setTemplate('SearchForm');
|
||||
|
||||
return $form;
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
```
|
||||
|
||||
**mysite/templates/Includes/SearchForm.ss**
|
||||
|
||||
:::ss
|
||||
<form $FormAttributes>
|
||||
<fieldset>
|
||||
$Fields.dataFieldByName(q)
|
||||
</fieldset>
|
||||
|
||||
<div class="Actions">
|
||||
<% loop $Actions %>$Field<% end_loop %>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
```ss
|
||||
|
||||
<form $FormAttributes>
|
||||
<fieldset>
|
||||
$Fields.dataFieldByName(q)
|
||||
</fieldset>
|
||||
|
||||
<div class="Actions">
|
||||
<% loop $Actions %>$Field<% end_loop %>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
|
||||
`SearchForm.ss` will be executed within the scope of the `Form` object so has access to any of the methods and
|
||||
properties on [Form](api:SilverStripe\Forms\Form) such as `$Fields` and `$Actions`.
|
||||
|
|
|
@ -17,63 +17,70 @@ perform custom operations on a row.
|
|||
A basic outline of our new `GridFieldCustomAction.php` will look like something
|
||||
below:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class GridFieldCustomAction implements GridField_ColumnProvider, GridField_ActionProvider {
|
||||
```php
|
||||
class GridFieldCustomAction implements GridField_ColumnProvider, GridField_ActionProvider
|
||||
{
|
||||
|
||||
public function augmentColumns($gridField, &$columns) {
|
||||
if(!in_array('Actions', $columns)) {
|
||||
$columns[] = 'Actions';
|
||||
}
|
||||
}
|
||||
public function augmentColumns($gridField, &$columns)
|
||||
{
|
||||
if(!in_array('Actions', $columns)) {
|
||||
$columns[] = 'Actions';
|
||||
}
|
||||
}
|
||||
|
||||
public function getColumnAttributes($gridField, $record, $columnName) {
|
||||
return array('class' => 'grid-field__col-compact');
|
||||
}
|
||||
public function getColumnAttributes($gridField, $record, $columnName)
|
||||
{
|
||||
return ['class' => 'grid-field__col-compact'];
|
||||
}
|
||||
|
||||
public function getColumnMetadata($gridField, $columnName)
|
||||
{
|
||||
if($columnName == 'Actions') {
|
||||
return ['title' => ''];
|
||||
}
|
||||
}
|
||||
|
||||
public function getColumnMetadata($gridField, $columnName) {
|
||||
if($columnName == 'Actions') {
|
||||
return array('title' => '');
|
||||
}
|
||||
}
|
||||
public function getColumnsHandled($gridField)
|
||||
{
|
||||
return ['Actions'];
|
||||
}
|
||||
|
||||
public function getColumnsHandled($gridField) {
|
||||
return array('Actions');
|
||||
}
|
||||
public function getColumnContent($gridField, $record, $columnName)
|
||||
{
|
||||
if(!$record->canEdit()) return;
|
||||
|
||||
public function getColumnContent($gridField, $record, $columnName) {
|
||||
if(!$record->canEdit()) return;
|
||||
$field = GridField_FormAction::create(
|
||||
$gridField,
|
||||
'CustomAction'.$record->ID,
|
||||
'Do Action',
|
||||
"docustomaction",
|
||||
['RecordID' => $record->ID]
|
||||
);
|
||||
|
||||
$field = GridField_FormAction::create(
|
||||
$gridField,
|
||||
'CustomAction'.$record->ID,
|
||||
'Do Action',
|
||||
"docustomaction",
|
||||
array('RecordID' => $record->ID)
|
||||
);
|
||||
return $field->Field();
|
||||
}
|
||||
|
||||
public function getActions($gridField)
|
||||
{
|
||||
return ['docustomaction'];
|
||||
}
|
||||
|
||||
return $field->Field();
|
||||
}
|
||||
public function handleAction(GridField $gridField, $actionName, $arguments, $data)
|
||||
{
|
||||
if($actionName == 'docustomaction') {
|
||||
// perform your action here
|
||||
|
||||
public function getActions($gridField) {
|
||||
return array('docustomaction');
|
||||
}
|
||||
// output a success message to the user
|
||||
Controller::curr()->getResponse()->setStatusCode(
|
||||
200,
|
||||
'Do Custom Action Done.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
|
||||
if($actionName == 'docustomaction') {
|
||||
// perform your action here
|
||||
|
||||
// output a success message to the user
|
||||
Controller::curr()->getResponse()->setStatusCode(
|
||||
200,
|
||||
'Do Custom Action Done.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Add the GridFieldCustomAction to the current `GridFieldConfig`
|
||||
|
||||
|
@ -82,15 +89,17 @@ a new instance of the class to the [GridFieldConfig](api:SilverStripe\Forms\Grid
|
|||
[Reference](/developer_guides/forms/field_types/gridfield) documentation has more information about
|
||||
manipulating the `GridFieldConfig` instance if required.
|
||||
|
||||
:::php
|
||||
// option 1: creating a new GridField with the CustomAction
|
||||
$config = GridFieldConfig::create();
|
||||
$config->addComponent(new GridFieldCustomAction());
|
||||
|
||||
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
|
||||
```php
|
||||
// option 1: creating a new GridField with the CustomAction
|
||||
$config = GridFieldConfig::create();
|
||||
$config->addComponent(new GridFieldCustomAction());
|
||||
|
||||
// option 2: adding the CustomAction to an exisitng GridField
|
||||
$gridField->getConfig()->addComponent(new GridFieldCustomAction());
|
||||
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
|
||||
|
||||
// option 2: adding the CustomAction to an exisitng GridField
|
||||
$gridField->getConfig()->addComponent(new GridFieldCustomAction());
|
||||
```
|
||||
|
||||
For documentation on adding a Component to a `GridField` created by `ModelAdmin`
|
||||
please view the [GridField Customization](/developer_guides/forms/how_tos/create_a_gridfield_actionprovider) section.
|
||||
|
|
|
@ -4,47 +4,67 @@ 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 ContactPageController extends PageController {
|
||||
private static $allowed_actions = array('Form');
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\TextField;
|
||||
use SilverStripe\Forms\EmailField;
|
||||
use SilverStripe\Forms\TextareaField;
|
||||
use SilverStripe\Forms\FormAction;
|
||||
use SilverStripe\Forms\Form;
|
||||
use Page;
|
||||
use PageController;
|
||||
|
||||
class ContactPage extends Page
|
||||
{
|
||||
}
|
||||
class ContactPageController extends PageController
|
||||
{
|
||||
private static $allowed_actions = ['Form'];
|
||||
public 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')
|
||||
);
|
||||
|
||||
```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 [FormField](api:SilverStripe\Forms\FormField) page.
|
||||
|
||||
:::php
|
||||
$actions = FieldList(
|
||||
new FormAction('submit', 'Submit')
|
||||
);
|
||||
|
||||
```php
|
||||
$actions = FieldList(
|
||||
new FormAction('submit', 'Submit')
|
||||
);
|
||||
```
|
||||
|
||||
We then create a [FieldList](api:SilverStripe\Forms\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($this, 'Form', $fields, $actions);
|
||||
|
||||
```php
|
||||
return new Form($this, 'Form', $fields, $actions);
|
||||
```
|
||||
|
||||
Finally we create the `Form` object and return it. The first argument is the controller that the form is on – this is almost always $this. The second 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 third and fourth arguments are the fields and actions we created earlier.
|
||||
|
||||
|
@ -60,31 +80,40 @@ 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 ContactPageController extends PageController {
|
||||
private static $allowed_actions = array('Form');
|
||||
public function Form() {
|
||||
// ...
|
||||
}
|
||||
public 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>Message:</strong> {$data['Message']}</p>
|
||||
";
|
||||
$email->setBody($messageBody);
|
||||
$email->send();
|
||||
return array(
|
||||
'Content' => '<p>Thank you for your feedback.</p>',
|
||||
'Form' => ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Control\Email\Email;
|
||||
use PageController;
|
||||
|
||||
class ContactPageController extends PageController
|
||||
{
|
||||
private static $allowed_actions = ['Form'];
|
||||
public function Form()
|
||||
{
|
||||
// ...
|
||||
}
|
||||
public 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>Message:</strong> {$data['Message']}</p>
|
||||
";
|
||||
$email->setBody($messageBody);
|
||||
$email->send();
|
||||
return [
|
||||
'Content' => '<p>Thank you for your feedback.</p>',
|
||||
'Form' => ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="hint" markdown="1">
|
||||
Caution: This form is prone to abuse by spammers,
|
||||
|
@ -106,12 +135,15 @@ All forms have some basic validation built in – email fields will only let the
|
|||
|
||||
The framework comes with a predefined validator called [RequiredFields](api:SilverStripe\Forms\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:
|
||||
|
||||
:::php
|
||||
public function Form() {
|
||||
// ...
|
||||
$validator = new RequiredFields('Name', 'Message');
|
||||
return new Form($this, 'Form', $fields, $actions, $validator);
|
||||
}
|
||||
|
||||
```php
|
||||
public 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.
|
||||
|
||||
|
|
|
@ -27,35 +27,42 @@ be marked `private static` and follow the `lower_case_with_underscores` structur
|
|||
|
||||
**mysite/code/MyClass.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyClass extends Page {
|
||||
```php
|
||||
use Page;
|
||||
|
||||
/**
|
||||
* @config
|
||||
*/
|
||||
private static $option_one = true;
|
||||
class MyClass extends Page
|
||||
{
|
||||
|
||||
/**
|
||||
* @config
|
||||
*/
|
||||
private static $option_two = array();
|
||||
/**
|
||||
* @config
|
||||
*/
|
||||
private static $option_one = true;
|
||||
|
||||
// ..
|
||||
}
|
||||
/**
|
||||
* @config
|
||||
*/
|
||||
private static $option_two = [];
|
||||
|
||||
// ..
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Accessing and Setting Configuration Properties
|
||||
|
||||
This can be done by calling the static method [Config::inst()](api:SilverStripe\Core\Config\Config::inst()), like so:
|
||||
|
||||
:::php
|
||||
$config = Config::inst()->get('MyClass', 'property');
|
||||
|
||||
```php
|
||||
$config = Config::inst()->get('MyClass', 'property');
|
||||
```
|
||||
|
||||
Or through the `config()` object on the class.
|
||||
|
||||
$config = $this->config()->get('property')';
|
||||
|
||||
```php
|
||||
$config = $this->config()->get('property')';
|
||||
```
|
||||
|
||||
Note that by default `Config::inst()` returns only an immutable version of config. Use `Config::modify()`
|
||||
if it's necessary to alter class config. This is generally undesirable in most applications, as modification
|
||||
of the config can immediately have performance implications, so this should be used sparingly, or
|
||||
|
@ -77,43 +84,49 @@ To set those configuration options on our previously defined class we can define
|
|||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yml
|
||||
MyClass:
|
||||
option_one: false
|
||||
option_two:
|
||||
- Foo
|
||||
- Bar
|
||||
- Baz
|
||||
|
||||
```yml
|
||||
|
||||
MyClass:
|
||||
option_one: false
|
||||
option_two:
|
||||
- Foo
|
||||
- Bar
|
||||
- Baz
|
||||
```
|
||||
|
||||
To use those variables in your application code:
|
||||
|
||||
:::php
|
||||
$me = new MyClass();
|
||||
|
||||
echo $me->config()->option_one;
|
||||
// returns false
|
||||
```php
|
||||
$me = new MyClass();
|
||||
|
||||
echo implode(', ', $me->config()->option_two);
|
||||
// returns 'Foo, Bar, Baz'
|
||||
echo $me->config()->option_one;
|
||||
// returns false
|
||||
|
||||
echo Config::inst()->get('MyClass', 'option_one');
|
||||
// returns false
|
||||
echo implode(', ', $me->config()->option_two);
|
||||
// returns 'Foo, Bar, Baz'
|
||||
|
||||
echo implode(', ', Config::inst()->get('MyClass', 'option_two'));
|
||||
// returns 'Foo, Bar, Baz'
|
||||
echo Config::inst()->get('MyClass', 'option_one');
|
||||
// returns false
|
||||
|
||||
Config::modify()->set('MyClass', 'option_one', true);
|
||||
echo implode(', ', Config::inst()->get('MyClass', 'option_two'));
|
||||
// returns 'Foo, Bar, Baz'
|
||||
|
||||
echo Config::inst()->get('MyClass', 'option_one');
|
||||
// returns true
|
||||
Config::modify()->set('MyClass', 'option_one', true);
|
||||
|
||||
// You can also use the static version
|
||||
MyClass::config()->option_two = array(
|
||||
'Qux'
|
||||
);
|
||||
echo Config::inst()->get('MyClass', 'option_one');
|
||||
// returns true
|
||||
|
||||
echo implode(', ', MyClass::config()->option_one);
|
||||
// returns 'Qux'
|
||||
// You can also use the static version
|
||||
MyClass::config()->option_two = [
|
||||
'Qux'
|
||||
];
|
||||
|
||||
echo implode(', ', MyClass::config()->option_one);
|
||||
// returns 'Qux'
|
||||
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
There is no way currently to restrict read or write access to any configuration property, or influence/check the values
|
||||
|
@ -188,19 +201,20 @@ The name of the files within the applications `_config` directly are arbitrary.
|
|||
`email.yml` if you want. For add-on's and modules, it is recommended that you name them with `<module_name>.yml`.
|
||||
</div>
|
||||
|
||||
The structure of each YAML file is a series of headers and values separated by YAML document separators.
|
||||
The structure of each YAML file is a series of headers and values separated by YAML document separators.
|
||||
```yml
|
||||
|
||||
:::yml
|
||||
---
|
||||
Name: adminroutes
|
||||
After:
|
||||
- '#rootroutes'
|
||||
- '#coreroutes'
|
||||
---
|
||||
Director:
|
||||
rules:
|
||||
'admin': 'AdminRootController'
|
||||
---
|
||||
---
|
||||
Name: adminroutes
|
||||
After:
|
||||
- '#rootroutes'
|
||||
- '#coreroutes'
|
||||
---
|
||||
Director:
|
||||
rules:
|
||||
'admin': 'AdminRootController'
|
||||
---
|
||||
```
|
||||
|
||||
<div class="info">
|
||||
If there is only one set of values the header can be omitted.
|
||||
|
@ -237,17 +251,20 @@ before (lower priority than) or after (higher priority than) some other value se
|
|||
To specify these rules you add an "After" and/or "Before" key to the relevant header section. The value for these
|
||||
keys is a list of reference paths to other value sections. A basic example:
|
||||
|
||||
:::yml
|
||||
---
|
||||
Name: adminroutes
|
||||
After:
|
||||
- '#rootroutes'
|
||||
- '#coreroutes'
|
||||
---
|
||||
Director:
|
||||
rules:
|
||||
'admin': 'AdminRootController'
|
||||
---
|
||||
|
||||
```yml
|
||||
|
||||
---
|
||||
Name: adminroutes
|
||||
After:
|
||||
- '#rootroutes'
|
||||
- '#coreroutes'
|
||||
---
|
||||
Director:
|
||||
rules:
|
||||
'admin': 'AdminRootController'
|
||||
---
|
||||
```
|
||||
|
||||
You do not have to specify all portions of a reference path. Any portion may be replaced with a wildcard "\*", or left
|
||||
out all together. Either has the same affect - that portion will be ignored when checking a value section's reference
|
||||
|
@ -300,31 +317,36 @@ You then list any of the following rules as sub-keys, with informational values
|
|||
|
||||
For instance, to add a property to "foo" when a module exists, and "bar" otherwise, you could do this:
|
||||
|
||||
:::yml
|
||||
---
|
||||
Only:
|
||||
moduleexists: 'MyFineModule'
|
||||
---
|
||||
MyClass:
|
||||
property: 'foo'
|
||||
---
|
||||
Except:
|
||||
moduleexists: 'MyFineModule'
|
||||
---
|
||||
MyClass:
|
||||
property: 'bar'
|
||||
---
|
||||
|
||||
```yml
|
||||
|
||||
---
|
||||
Only:
|
||||
moduleexists: 'MyFineModule'
|
||||
---
|
||||
MyClass:
|
||||
property: 'foo'
|
||||
---
|
||||
Except:
|
||||
moduleexists: 'MyFineModule'
|
||||
---
|
||||
MyClass:
|
||||
property: 'bar'
|
||||
---
|
||||
```
|
||||
|
||||
Multiple conditions of the same type can be declared via array format
|
||||
|
||||
|
||||
:::yaml
|
||||
---
|
||||
Only:
|
||||
moduleexists:
|
||||
- 'silverstripe/blog'
|
||||
- 'silverstripe/lumberjack'
|
||||
|
||||
```yaml
|
||||
|
||||
---
|
||||
Only:
|
||||
moduleexists:
|
||||
- 'silverstripe/blog'
|
||||
- 'silverstripe/lumberjack'
|
||||
```
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
When you have more than one rule for a nested fragment, they're joined like
|
||||
|
|
|
@ -10,23 +10,27 @@ throughout the site. Out of the box this includes selecting the current site the
|
|||
|
||||
`SiteConfig` options can be accessed from any template by using the $SiteConfig variable.
|
||||
|
||||
:::ss
|
||||
$SiteConfig.Title
|
||||
$SiteConfig.Tagline
|
||||
|
||||
<% with $SiteConfig %>
|
||||
$Title $AnotherField
|
||||
<% end_with %>
|
||||
|
||||
```ss
|
||||
|
||||
$SiteConfig.Title
|
||||
$SiteConfig.Tagline
|
||||
|
||||
<% with $SiteConfig %>
|
||||
$Title $AnotherField
|
||||
<% end_with %>
|
||||
```
|
||||
|
||||
To access variables in the PHP:
|
||||
|
||||
:::php
|
||||
$config = SiteConfig::current_site_config();
|
||||
|
||||
echo $config->Title;
|
||||
|
||||
// returns "Website Name"
|
||||
```php
|
||||
$config = SiteConfig::current_site_config();
|
||||
|
||||
echo $config->Title;
|
||||
|
||||
// returns "Website Name"
|
||||
```
|
||||
|
||||
## Extending SiteConfig
|
||||
|
||||
|
@ -34,30 +38,39 @@ To extend the options available in the panel, define your own fields via a [Data
|
|||
|
||||
**mysite/code/extensions/CustomSiteConfig.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class CustomSiteConfig extends DataExtension {
|
||||
|
||||
private static $db = array(
|
||||
'FooterContent' => 'HTMLText'
|
||||
);
|
||||
|
||||
public function updateCMSFields(FieldList $fields) {
|
||||
$fields->addFieldToTab("Root.Main",
|
||||
new HTMLEditorField("FooterContent", "Footer Content")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
|
||||
class CustomSiteConfig extends DataExtension
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'FooterContent' => 'HTMLText'
|
||||
];
|
||||
|
||||
public function updateCMSFields(FieldList $fields)
|
||||
{
|
||||
$fields->addFieldToTab("Root.Main",
|
||||
new HTMLEditorField("FooterContent", "Footer Content")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Then activate the extension.
|
||||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yml
|
||||
Silverstripe\SiteConfig\SiteConfig:
|
||||
extensions:
|
||||
- CustomSiteConfig
|
||||
|
||||
```yml
|
||||
|
||||
Silverstripe\SiteConfig\SiteConfig:
|
||||
extensions:
|
||||
- CustomSiteConfig
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
After adding the class and the YAML change, make sure to rebuild your database by visiting http://yoursite.com/dev/build.
|
||||
|
|
|
@ -17,20 +17,25 @@ and `RequestHandler`. You can still apply extensions to descendants of these cla
|
|||
|
||||
**mysite/code/extensions/MyMemberExtension.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyMemberExtension extends DataExtension {
|
||||
|
||||
private static $db = array(
|
||||
'DateOfBirth' => 'SS_Datetime'
|
||||
);
|
||||
```php
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
|
||||
public function SayHi() {
|
||||
// $this->owner refers to the original instance. In this case a `Member`.
|
||||
return "Hi " . $this->owner->Name;
|
||||
}
|
||||
}
|
||||
class MyMemberExtension extends DataExtension
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'DateOfBirth' => 'SS_Datetime'
|
||||
];
|
||||
|
||||
public function SayHi()
|
||||
{
|
||||
// $this->owner refers to the original instance. In this case a `Member`.
|
||||
return "Hi " . $this->owner->Name;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="info" markdown="1">
|
||||
Convention is for extension class names to end in `Extension`. This isn't a requirement but makes it clearer
|
||||
|
@ -41,15 +46,20 @@ we want to add the `MyMemberExtension` too. To activate this extension, add the
|
|||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yml
|
||||
Member:
|
||||
extensions:
|
||||
- MyMemberExtension
|
||||
|
||||
```yml
|
||||
|
||||
Member:
|
||||
extensions:
|
||||
- MyMemberExtension
|
||||
```
|
||||
|
||||
Alternatively, we can add extensions through PHP code (in the `_config.php` file).
|
||||
|
||||
:::php
|
||||
Member::add_extension('MyMemberExtension');
|
||||
|
||||
```php
|
||||
Member::add_extension('MyMemberExtension');
|
||||
```
|
||||
|
||||
This class now defines a `MyMemberExtension` that applies to all `Member` instances on the website. It will have
|
||||
transformed the original `Member` class in two ways:
|
||||
|
@ -70,67 +80,81 @@ $has_one etc.
|
|||
|
||||
**mysite/code/extensions/MyMemberExtension.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyMemberExtension extends DataExtension {
|
||||
```php
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
|
||||
private static $db = array(
|
||||
'Position' => 'Varchar',
|
||||
);
|
||||
class MyMemberExtension extends DataExtension
|
||||
{
|
||||
|
||||
private static $has_one = array(
|
||||
'Image' => 'Image',
|
||||
);
|
||||
private static $db = [
|
||||
'Position' => 'Varchar',
|
||||
];
|
||||
|
||||
public function SayHi() {
|
||||
// $this->owner refers to the original instance. In this case a `Member`.
|
||||
return "Hi " . $this->owner->Name;
|
||||
}
|
||||
}
|
||||
private static $has_one = [
|
||||
'Image' => 'Image',
|
||||
];
|
||||
|
||||
public function SayHi()
|
||||
{
|
||||
// $this->owner refers to the original instance. In this case a `Member`.
|
||||
return "Hi " . $this->owner->Name;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**mysite/templates/Page.ss**
|
||||
|
||||
:::ss
|
||||
$CurrentMember.Position
|
||||
$CurrentMember.Image
|
||||
|
||||
|
||||
```ss
|
||||
|
||||
$CurrentMember.Position
|
||||
$CurrentMember.Image
|
||||
```
|
||||
|
||||
## Adding Methods
|
||||
|
||||
Methods that have a unique name will be called as part of the `__call` method on [Object](api:Object). In the previous example
|
||||
we added a `SayHi` method which is unique to our extension.
|
||||
|
||||
**mysite/templates/Page.ss**
|
||||
:::ss
|
||||
<p>$CurrentMember.SayHi</p>
|
||||
|
||||
// "Hi Sam"
|
||||
```ss
|
||||
|
||||
<p>$CurrentMember.SayHi</p>
|
||||
|
||||
// "Hi Sam"
|
||||
```
|
||||
|
||||
**mysite/code/Page.php**
|
||||
:::php
|
||||
$member = Security::getCurrentUser();
|
||||
echo $member->SayHi;
|
||||
|
||||
// "Hi Sam"
|
||||
```php
|
||||
$member = Security::getCurrentUser();
|
||||
echo $member->SayHi;
|
||||
|
||||
// "Hi Sam"
|
||||
```
|
||||
|
||||
## Modifying Existing Methods
|
||||
|
||||
If the `Extension` needs to modify an existing method it's a little trickier. It requires that the method you want to
|
||||
customise has provided an *Extension Hook* in the place where you want to modify the data. An *Extension Hook* is done
|
||||
through the [Object::extend()](api:Object::extend()) method.
|
||||
through the `extend()` method of the [Extensible](api:SilverStripe\Core\Extensible) trait.
|
||||
|
||||
**framework/security/Member.php**
|
||||
|
||||
:::php
|
||||
public function getValidator() {
|
||||
// ..
|
||||
|
||||
$this->extend('updateValidator', $validator);
|
||||
|
||||
// ..
|
||||
}
|
||||
```php
|
||||
public function getValidator()
|
||||
{
|
||||
// ..
|
||||
|
||||
$this->extend('updateValidator', $validator);
|
||||
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
Extension Hooks can be located anywhere in the method and provide a point for any `Extension` instances to modify the
|
||||
variables at that given point. In this case, the core function `getValidator` on the `Member` class provides an
|
||||
|
@ -139,18 +163,22 @@ validator by defining the `updateValidator` method.
|
|||
|
||||
**mysite/code/extensions/MyMemberExtension.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyMemberExtension extends DataExtension {
|
||||
```php
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
|
||||
// ..
|
||||
class MyMemberExtension extends DataExtension
|
||||
{
|
||||
|
||||
public function updateValidator($validator) {
|
||||
// we want to make date of birth required for each member
|
||||
$validator->addRequiredField('DateOfBirth');
|
||||
}
|
||||
}
|
||||
// ..
|
||||
|
||||
public function updateValidator($validator)
|
||||
{
|
||||
// we want to make date of birth required for each member
|
||||
$validator->addRequiredField('DateOfBirth');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<div class="info" markdown="1">
|
||||
The `$validator` parameter is passed by reference, as it is an object.
|
||||
|
@ -159,40 +187,49 @@ The `$validator` parameter is passed by reference, as it is an object.
|
|||
Another common example of when you will want to modify a method is to update the default CMS fields for an object in an
|
||||
extension. The `CMS` provides a `updateCMSFields` Extension Hook to tie into.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyMemberExtension extends DataExtension {
|
||||
```php
|
||||
use SilverStripe\Forms\TextField;
|
||||
use SilverStripe\AssetAdmin\Forms\UploadField;
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
|
||||
private static $db = array(
|
||||
'Position' => 'Varchar',
|
||||
);
|
||||
class MyMemberExtension extends DataExtension
|
||||
{
|
||||
|
||||
private static $has_one = array(
|
||||
'Image' => 'Image',
|
||||
);
|
||||
private static $db = [
|
||||
'Position' => 'Varchar',
|
||||
];
|
||||
|
||||
public function updateCMSFields(FieldList $fields) {
|
||||
$fields->push(new TextField('Position'));
|
||||
$fields->push($upload = new UploadField('Image', 'Profile Image'));
|
||||
$upload->setAllowedFileCategories('image/supported');
|
||||
}
|
||||
}
|
||||
private static $has_one = [
|
||||
'Image' => 'Image',
|
||||
];
|
||||
|
||||
public function updateCMSFields(FieldList $fields)
|
||||
{
|
||||
$fields->push(new TextField('Position'));
|
||||
$fields->push($upload = new UploadField('Image', 'Profile Image'));
|
||||
$upload->setAllowedFileCategories('image/supported');
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
If you're providing a module or working on code that may need to be extended by other code, it should provide a *hook*
|
||||
which allows an Extension to modify the results.
|
||||
</div>
|
||||
|
||||
:::php
|
||||
public function Foo() {
|
||||
$foo = // ..
|
||||
|
||||
$this->extend('updateFoo', $foo);
|
||||
```php
|
||||
public function Foo()
|
||||
{
|
||||
$foo = // ..
|
||||
|
||||
return $foo;
|
||||
}
|
||||
$this->extend('updateFoo', $foo);
|
||||
|
||||
return $foo;
|
||||
}
|
||||
```
|
||||
|
||||
The convention for extension hooks is to provide an `update{$Function}` hook at the end before you return the result. If
|
||||
you need to provide extension hooks at the beginning of the method use `before{..}`.
|
||||
|
@ -202,37 +239,39 @@ you need to provide extension hooks at the beginning of the method use `before{.
|
|||
In your [Extension](api:SilverStripe\Core\Extension) class you can only refer to the source object through the `owner` property on the class as
|
||||
`$this` will refer to your `Extension` instance.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyMemberExtension extends DataExtension {
|
||||
```php
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
|
||||
public function updateFoo($foo) {
|
||||
// outputs the original class
|
||||
var_dump($this->owner);
|
||||
}
|
||||
}
|
||||
class MyMemberExtension extends DataExtension
|
||||
{
|
||||
|
||||
public function updateFoo($foo)
|
||||
{
|
||||
// outputs the original class
|
||||
var_dump($this->owner);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Checking to see if an Object has an Extension
|
||||
|
||||
To see what extensions are currently enabled on an object, use [Object::getExtensionInstances()](api:Object::getExtensionInstances()) and
|
||||
[Object::hasExtension()](api:Object::hasExtension())
|
||||
To see what extensions are currently enabled on an object, use the [getExtensionInstances()](api:SilverStripe\Core\Extensible::getExtensionInstances()) and
|
||||
[hasExtension()](api:SilverStripe\Core\Extensible::hasExtension()) methods of the [Extensible](api:SilverStripe\Core\Extensible) trait.
|
||||
```php
|
||||
$member = Security::getCurrentUser();
|
||||
|
||||
print_r($member->getExtensionInstances());
|
||||
|
||||
if($member->hasExtension('MyCustomMemberExtension')) {
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
:::php
|
||||
$member = Security::getCurrentUser();
|
||||
## Extension injection points
|
||||
|
||||
print_r($member->getExtensionInstances());
|
||||
|
||||
if($member->hasExtension('MyCustomMemberExtension')) {
|
||||
// ..
|
||||
}
|
||||
|
||||
|
||||
## Object extension injection points
|
||||
|
||||
`Object` has two additional methods, `beforeExtending` and `afterExtending`, each of which takes a method name and a
|
||||
callback to be executed immediately before and after `Object::extend()` is called on extensions.
|
||||
`Extensible` has two additional methods, `beforeExtending` and `afterExtending`, each of which takes a method name and a
|
||||
callback to be executed immediately before and after `extend()` is called on extensions.
|
||||
|
||||
This is useful in many cases where working with modules such as `Translatable` which operate on `DataObject` fields
|
||||
that must exist in the `FieldList` at the time that `$this->extend('UpdateCMSFields')` is called.
|
||||
|
@ -245,18 +284,20 @@ require that a callback is registered each time, if necessary.
|
|||
Example: A class that wants to control default values during object initialization. The code needs to assign a value
|
||||
if not specified in `self::$defaults`, but before extensions have been called:
|
||||
|
||||
:::php
|
||||
function __construct() {
|
||||
$self = $this;
|
||||
|
||||
$this->beforeExtending('populateDefaults', function() use ($self) {
|
||||
if(empty($self->MyField)) {
|
||||
$self->MyField = 'Value we want as a default if not specified in $defaults, but set before extensions';
|
||||
}
|
||||
});
|
||||
```php
|
||||
function __construct() {
|
||||
$self = $this;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
$this->beforeExtending('populateDefaults', function() use ($self) {
|
||||
if(empty($self->MyField)) {
|
||||
$self->MyField = 'Value we want as a default if not specified in $defaults, but set before extensions';
|
||||
}
|
||||
});
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
```
|
||||
|
||||
Example 2: User code can intervene in the process of extending cms fields.
|
||||
|
||||
|
@ -264,24 +305,25 @@ Example 2: User code can intervene in the process of extending cms fields.
|
|||
This method is preferred to disabling, enabling, and calling field extensions manually.
|
||||
</div>
|
||||
|
||||
:::php
|
||||
public function getCMSFields() {
|
||||
|
||||
$this->beforeUpdateCMSFields(function($fields) {
|
||||
// Include field which must be present when updateCMSFields is called on extensions
|
||||
$fields->addFieldToTab("Root.Main", new TextField('Detail', 'Details', null, 255));
|
||||
});
|
||||
```php
|
||||
public function getCMSFields()
|
||||
{
|
||||
|
||||
$fields = parent::getCMSFields();
|
||||
// ... additional fields here
|
||||
return $fields;
|
||||
}
|
||||
$this->beforeUpdateCMSFields(function($fields) {
|
||||
// Include field which must be present when updateCMSFields is called on extensions
|
||||
$fields->addFieldToTab("Root.Main", new TextField('Detail', 'Details', null, 255));
|
||||
});
|
||||
|
||||
$fields = parent::getCMSFields();
|
||||
// ... additional fields here
|
||||
return $fields;
|
||||
}
|
||||
```
|
||||
|
||||
## Related Documentaion
|
||||
|
||||
* [Injector](injector/)
|
||||
* [Object::useCustomClass](api:Object::useCustomClass)
|
||||
|
||||
## API Documentation
|
||||
|
||||
|
|
|
@ -11,25 +11,24 @@ In the CMS, authors often want to insert content elements which go beyond standa
|
|||
in their WYSIWYG editor. Shortcodes are a semi-technical solution for this. A good example would be embedding a 3D file
|
||||
viewer or a Google Map at a certain location.
|
||||
|
||||
|
||||
:::php
|
||||
$text = "<h1>My Map</h1>[map]"
|
||||
|
||||
// Will output
|
||||
// <h1>My Map</h1><iframe ..></iframe>
|
||||
|
||||
```php
|
||||
$text = "<h1>My Map</h1>[map]"
|
||||
|
||||
// Will output
|
||||
// <h1>My Map</h1><iframe ..></iframe>
|
||||
```
|
||||
|
||||
Here's some syntax variations:
|
||||
|
||||
|
||||
:::php
|
||||
[my_shortcode]
|
||||
#
|
||||
[my_shortcode /]
|
||||
#
|
||||
[my_shortcode,myparameter="value"]
|
||||
#
|
||||
[my_shortcode,myparameter="value"]Enclosed Content[/my_shortcode]
|
||||
```php
|
||||
[my_shortcode]
|
||||
#
|
||||
[my_shortcode /]
|
||||
#
|
||||
[my_shortcode,myparameter="value"]
|
||||
#
|
||||
[my_shortcode,myparameter="value"]Enclosed Content[/my_shortcode]
|
||||
```
|
||||
|
||||
Shortcodes are automatically parsed on any database field which is declared as [HTMLValue](api:SilverStripe\View\Parsers\HTMLValue) or [DBHTMLText](api:SilverStripe\ORM\FieldType\DBHTMLText),
|
||||
when rendered into a template. This means you can use shortcodes on common fields like `SiteTree.Content`, and any
|
||||
|
@ -37,9 +36,11 @@ other [DataObject::$db](api:SilverStripe\ORM\DataObject::$db) definitions of the
|
|||
|
||||
Other fields can be manually parsed with shortcodes through the `parse` method.
|
||||
|
||||
:::php
|
||||
$text = "My awesome [my_shortcode] is here.";
|
||||
ShortcodeParser::get_active()->parse($text);
|
||||
|
||||
```php
|
||||
$text = "My awesome [my_shortcode] is here.";
|
||||
ShortcodeParser::get_active()->parse($text);
|
||||
```
|
||||
|
||||
## Defining Custom Shortcodes
|
||||
|
||||
|
@ -47,19 +48,24 @@ First we need to define a callback for the shortcode.
|
|||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Page extends SiteTree {
|
||||
|
||||
private static $casting = array(
|
||||
'MyShortCodeMethod' => 'HTMLText'
|
||||
);
|
||||
```php
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
|
||||
public static function MyShortCodeMethod($arguments, $content = null, $parser = null, $tagName) {
|
||||
return "<em>" . $tagName . "</em> " . $content . "; " . count($arguments) . " arguments.";
|
||||
}
|
||||
}
|
||||
class Page extends SiteTree
|
||||
{
|
||||
|
||||
private static $casting = [
|
||||
'MyShortCodeMethod' => 'HTMLText'
|
||||
];
|
||||
|
||||
public static function MyShortCodeMethod($arguments, $content = null, $parser = null, $tagName)
|
||||
{
|
||||
return "<em>" . $tagName . "</em> " . $content . "; " . count($arguments) . " arguments.";
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
These parameters are passed to the `MyShortCodeMethod` callback:
|
||||
|
||||
|
@ -77,11 +83,13 @@ To register a shortcode you call the following.
|
|||
|
||||
**mysite/_config.php**
|
||||
|
||||
:::php
|
||||
// ShortcodeParser::get('default')->register($shortcode, $callback);
|
||||
|
||||
ShortcodeParser::get('default')->register('my_shortcode', array('Page', 'MyShortCodeMethod'));
|
||||
```php
|
||||
// ShortcodeParser::get('default')->register($shortcode, $callback);
|
||||
|
||||
ShortcodeParser::get('default')->register('my_shortcode', ['Page', 'MyShortCodeMethod']);
|
||||
|
||||
```
|
||||
|
||||
## Built-in Shortcodes
|
||||
|
||||
|
@ -91,15 +99,19 @@ SilverStripe comes with several shortcode parsers already.
|
|||
|
||||
Internal page links keep references to their database IDs rather than the URL, in order to make these links resilient
|
||||
against moving the target page to a different location in the page tree. This is done through the `[sitetree_link]`
|
||||
shortcode, which takes an `id` parameter.
|
||||
shortcode, which takes an `id` parameter.
|
||||
|
||||
:::php
|
||||
<a href="[sitetree_link,id=99]">
|
||||
|
||||
```php
|
||||
<a href="[sitetree_link,id=99]">
|
||||
```
|
||||
|
||||
Links to internal `File` database records work exactly the same, but with the `[file_link]` shortcode.
|
||||
|
||||
:::php
|
||||
<a href="[file_link,id=99]">
|
||||
|
||||
```php
|
||||
<a href="[file_link,id=99]">
|
||||
```
|
||||
|
||||
### Images
|
||||
|
||||
|
@ -135,81 +147,85 @@ The first is called "element scope" use, the second "attribute scope"
|
|||
|
||||
You may not use shortcodes in any other location. Specifically, you can not use shortcodes to generate attributes or
|
||||
change the name of a tag. These usages are forbidden:
|
||||
```ss
|
||||
<[paragraph]>Some test</[paragraph]>
|
||||
|
||||
<[paragraph]>Some test</[paragraph]>
|
||||
|
||||
<a [titleattribute]>link</a>
|
||||
|
||||
<a [titleattribute]>link</a>
|
||||
```
|
||||
You may need to escape text inside attributes `>` becomes `>`, You can include HTML tags inside a shortcode tag, but
|
||||
you need to be careful of nesting to ensure you don't break the output.
|
||||
|
||||
:::ss
|
||||
<!-- Good -->
|
||||
<div>
|
||||
[shortcode]
|
||||
<p>Caption</p>
|
||||
[/shortcode]
|
||||
</div>
|
||||
|
||||
```ss
|
||||
|
||||
<!-- Bad: -->
|
||||
<!-- Good -->
|
||||
<div>
|
||||
[shortcode]
|
||||
<p>Caption</p>
|
||||
[/shortcode]
|
||||
</div>
|
||||
|
||||
<div>
|
||||
[shortcode]
|
||||
</div>
|
||||
<p>
|
||||
[/shortcode]
|
||||
</p>
|
||||
<!-- Bad: -->
|
||||
|
||||
<div>
|
||||
[shortcode]
|
||||
</div>
|
||||
<p>
|
||||
[/shortcode]
|
||||
</p>
|
||||
```
|
||||
|
||||
### Location
|
||||
|
||||
Element scoped shortcodes have a special ability to move the location they are inserted at to comply with HTML lexical
|
||||
rules. Take for example this basic paragraph tag:
|
||||
|
||||
<p><a href="#">Head [figure,src="assets/a.jpg",caption="caption"] Tail</a></p>
|
||||
|
||||
```ss
|
||||
<p><a href="#">Head [figure,src="assets/a.jpg",caption="caption"] Tail</a></p>
|
||||
```
|
||||
When converted naively would become:
|
||||
|
||||
<p><a href="#">Head <figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure> Tail</a></p>
|
||||
|
||||
```ss
|
||||
<p><a href="#">Head <figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure> Tail</a></p>
|
||||
```
|
||||
However this is not valid HTML - P elements can not contain other block level elements.
|
||||
|
||||
To fix this you can specify a "location" attribute on a shortcode. When the location attribute is "left" or "right"
|
||||
the inserted content will be moved to immediately before the block tag. The result is this:
|
||||
|
||||
<figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure><p><a href="#">Head Tail</a></p>
|
||||
|
||||
```ss
|
||||
<figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure><p><a href="#">Head Tail</a></p>
|
||||
```
|
||||
When the location attribute is "leftAlone" or "center" then the DOM is split around the element. The result is this:
|
||||
|
||||
<p><a href="#">Head </a></p><figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure><p><a href="#"> Tail</a></p>
|
||||
|
||||
```ss
|
||||
<p><a href="#">Head </a></p><figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure><p><a href="#"> Tail</a></p>
|
||||
```
|
||||
### Parameter values
|
||||
|
||||
Here is a summary of the callback parameter values based on some example shortcodes.
|
||||
|
||||
:::php
|
||||
public function MyCustomShortCode($arguments, $content = null, $parser = null, $tagName) {
|
||||
// ..
|
||||
}
|
||||
```php
|
||||
public function MyCustomShortCode($arguments, $content = null, $parser = null, $tagName)
|
||||
{
|
||||
// ..
|
||||
}
|
||||
|
||||
[my_shortcode]
|
||||
$attributes => array();
|
||||
$content => null;
|
||||
$parser => ShortcodeParser instance,
|
||||
$tagName => 'my_shortcode')
|
||||
[my_shortcode]
|
||||
$attributes => [];
|
||||
$content => null;
|
||||
$parser => ShortcodeParser instance,
|
||||
$tagName => 'my_shortcode')
|
||||
|
||||
[my_shortcode,attribute="foo",other="bar"]
|
||||
[my_shortcode,attribute="foo",other="bar"]
|
||||
|
||||
$attributes => array ('attribute' => 'foo', 'other' => 'bar')
|
||||
$enclosedContent => null
|
||||
$parser => ShortcodeParser instance
|
||||
$tagName => 'my_shortcode'
|
||||
$attributes => ['attribute' => 'foo', 'other' => 'bar']
|
||||
$enclosedContent => null
|
||||
$parser => ShortcodeParser instance
|
||||
$tagName => 'my_shortcode'
|
||||
|
||||
[my_shortcode,attribute="foo"]content[/my_shortcode]
|
||||
[my_shortcode,attribute="foo"]content[/my_shortcode]
|
||||
|
||||
$attributes => array('attribute' => 'foo')
|
||||
$enclosedContent => 'content'
|
||||
$parser => ShortcodeParser instance
|
||||
$tagName => 'my_shortcode'
|
||||
$attributes => ['attribute' => 'foo']
|
||||
$enclosedContent => 'content'
|
||||
$parser => ShortcodeParser instance
|
||||
$tagName => 'my_shortcode'
|
||||
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
|
|
|
@ -18,108 +18,128 @@ Some of the goals of dependency injection are:
|
|||
|
||||
The following sums up the simplest usage of the `Injector` it creates a new object of type `MyClassName` through `create`
|
||||
|
||||
:::php
|
||||
$object = Injector::inst()->create('MyClassName');
|
||||
|
||||
```php
|
||||
$object = Injector::inst()->create('MyClassName');
|
||||
```
|
||||
|
||||
The benefit of constructing objects through this syntax is `ClassName` can be swapped out using the
|
||||
[Configuration API](../configuration) by developers.
|
||||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yml
|
||||
Injector:
|
||||
MyClassName:
|
||||
class: MyBetterClassName
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
```yml
|
||||
|
||||
Injector:
|
||||
MyClassName:
|
||||
class: MyBetterClassName
|
||||
```
|
||||
|
||||
Repeated calls to `create()` create a new object each time.
|
||||
|
||||
:::php
|
||||
$object = Injector::inst()->create('MyClassName');
|
||||
$object2 = Injector::inst()->create('MyClassName');
|
||||
|
||||
echo $object !== $object2;
|
||||
```php
|
||||
$object = Injector::inst()->create('MyClassName');
|
||||
$object2 = Injector::inst()->create('MyClassName');
|
||||
|
||||
// returns true;
|
||||
echo $object !== $object2;
|
||||
|
||||
// returns true;
|
||||
```
|
||||
|
||||
## Singleton Pattern
|
||||
|
||||
The `Injector` API can be used for the singleton pattern through `get()`. Subsequent calls to `get` return the same
|
||||
object instance as the first call.
|
||||
|
||||
:::php
|
||||
// sets up MyClassName as a singleton
|
||||
$object = Injector::inst()->get('MyClassName');
|
||||
$object2 = Injector::inst()->get('MyClassName');
|
||||
|
||||
echo ($object === $object2);
|
||||
```php
|
||||
// sets up MyClassName as a singleton
|
||||
$object = Injector::inst()->get('MyClassName');
|
||||
$object2 = Injector::inst()->get('MyClassName');
|
||||
|
||||
// returns true;
|
||||
echo ($object === $object2);
|
||||
|
||||
// returns true;
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
The `Injector` API can be used to define the types of `$dependencies` that an object requires.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyController extends Controller {
|
||||
|
||||
// both of these properties will be automatically
|
||||
// set by the injector on object creation
|
||||
public $permissions;
|
||||
public $textProperty;
|
||||
|
||||
// we declare the types for each of the properties on the object. Anything we pass in via the Injector API must
|
||||
// match these data types.
|
||||
static $dependencies = array(
|
||||
'textProperty' => 'a string value',
|
||||
'permissions' => '%$PermissionService',
|
||||
);
|
||||
}
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
// both of these properties will be automatically
|
||||
// set by the injector on object creation
|
||||
public $permissions;
|
||||
public $textProperty;
|
||||
|
||||
// we declare the types for each of the properties on the object. Anything we pass in via the Injector API must
|
||||
// match these data types.
|
||||
static $dependencies = [
|
||||
'textProperty' => 'a string value',
|
||||
'permissions' => '%$PermissionService',
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
When creating a new instance of `MyController` the dependencies on that class will be met.
|
||||
|
||||
:::php
|
||||
$object = Injector::inst()->get('MyController');
|
||||
|
||||
echo ($object->permissions instanceof PermissionService);
|
||||
// returns true;
|
||||
|
||||
echo (is_string($object->textProperty));
|
||||
// returns true;
|
||||
```php
|
||||
$object = Injector::inst()->get('MyController');
|
||||
|
||||
echo ($object->permissions instanceof PermissionService);
|
||||
// returns true;
|
||||
|
||||
echo (is_string($object->textProperty));
|
||||
// returns true;
|
||||
```
|
||||
|
||||
The [Configuration YAML](../configuration) does the hard work of configuring those `$dependencies` for us.
|
||||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yml
|
||||
Injector:
|
||||
PermissionService:
|
||||
class: MyCustomPermissionService
|
||||
MyController
|
||||
properties:
|
||||
textProperty: 'My Text Value'
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
```yml
|
||||
|
||||
Injector:
|
||||
PermissionService:
|
||||
class: MyCustomPermissionService
|
||||
MyController
|
||||
properties:
|
||||
textProperty: 'My Text Value'
|
||||
```
|
||||
|
||||
Now the dependencies will be replaced with our configuration.
|
||||
|
||||
:::php
|
||||
$object = Injector::inst()->get('MyController');
|
||||
|
||||
echo ($object->permissions instanceof MyCustomPermissionService);
|
||||
// returns true;
|
||||
|
||||
echo ($object->textProperty == 'My Text Value');
|
||||
// returns true;
|
||||
```php
|
||||
$object = Injector::inst()->get('MyController');
|
||||
|
||||
echo ($object->permissions instanceof MyCustomPermissionService);
|
||||
// returns true;
|
||||
|
||||
echo ($object->textProperty == 'My Text Value');
|
||||
// returns true;
|
||||
```
|
||||
|
||||
As well as properties, method calls can also be specified:
|
||||
|
||||
:::yml
|
||||
Injector:
|
||||
Logger:
|
||||
class: Monolog\Logger
|
||||
calls:
|
||||
- [ pushHandler, [ %$DefaultHandler ] ]
|
||||
|
||||
```yml
|
||||
|
||||
Injector:
|
||||
Logger:
|
||||
class: Monolog\Logger
|
||||
calls:
|
||||
- [ pushHandler, [ %$DefaultHandler ] ]
|
||||
```
|
||||
|
||||
## Using constants as variables
|
||||
|
||||
|
@ -146,37 +166,43 @@ An example using the `MyFactory` service to create instances of the `MyService`
|
|||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yml
|
||||
Injector:
|
||||
MyService:
|
||||
factory: MyFactory
|
||||
|
||||
```yml
|
||||
|
||||
Injector:
|
||||
MyService:
|
||||
factory: MyFactory
|
||||
```
|
||||
|
||||
**mysite/code/MyFactory.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyFactory implements SilverStripe\Core\Injector\Factory {
|
||||
```php
|
||||
class MyFactory implements SilverStripe\Core\Injector\Factory
|
||||
{
|
||||
|
||||
public function create($service, array $params = array()) {
|
||||
return new MyServiceImplementation();
|
||||
}
|
||||
}
|
||||
public function create($service, array $params = [])
|
||||
{
|
||||
return new MyServiceImplementation();
|
||||
}
|
||||
}
|
||||
|
||||
// Will use MyFactoryImplementation::create() to create the service instance.
|
||||
$instance = Injector::inst()->get('MyService');
|
||||
// Will use MyFactoryImplementation::create() to create the service instance.
|
||||
$instance = Injector::inst()->get('MyService');
|
||||
|
||||
```
|
||||
|
||||
## Dependency overrides
|
||||
|
||||
To override the `$dependency` declaration for a class, define the following configuration file.
|
||||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
MyController:
|
||||
dependencies:
|
||||
textProperty: a string value
|
||||
permissions: %$PermissionService
|
||||
|
||||
```yml
|
||||
MyController:
|
||||
dependencies:
|
||||
textProperty: a string value
|
||||
permissions: %$PermissionService
|
||||
```
|
||||
## Managed objects
|
||||
|
||||
Simple dependencies can be specified by the `$dependencies`, but more complex configurations are possible by specifying
|
||||
|
@ -187,50 +213,58 @@ runtime.
|
|||
|
||||
Assuming a class structure such as
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class RestrictivePermissionService {
|
||||
private $database;
|
||||
```php
|
||||
class RestrictivePermissionService
|
||||
{
|
||||
private $database;
|
||||
|
||||
public function setDatabase($d) {
|
||||
$this->database = $d;
|
||||
}
|
||||
}
|
||||
|
||||
class MySQLDatabase {
|
||||
private $username;
|
||||
private $password;
|
||||
|
||||
public function __construct($username, $password) {
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
}
|
||||
}
|
||||
public function setDatabase($d)
|
||||
{
|
||||
$this->database = $d;
|
||||
}
|
||||
}
|
||||
class MySQLDatabase
|
||||
{
|
||||
private $username;
|
||||
private $password;
|
||||
|
||||
public function __construct($username, $password)
|
||||
{
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And the following configuration..
|
||||
|
||||
:::yml
|
||||
name: MyController
|
||||
---
|
||||
MyController:
|
||||
dependencies:
|
||||
permissions: %$PermissionService
|
||||
Injector:
|
||||
PermissionService:
|
||||
class: RestrictivePermissionService
|
||||
properties:
|
||||
database: %$MySQLDatabase
|
||||
MySQLDatabase
|
||||
constructor:
|
||||
0: 'dbusername'
|
||||
1: 'dbpassword'
|
||||
|
||||
```yml
|
||||
|
||||
name: MyController
|
||||
---
|
||||
MyController:
|
||||
dependencies:
|
||||
permissions: %$PermissionService
|
||||
Injector:
|
||||
PermissionService:
|
||||
class: RestrictivePermissionService
|
||||
properties:
|
||||
database: %$MySQLDatabase
|
||||
MySQLDatabase
|
||||
constructor:
|
||||
0: 'dbusername'
|
||||
1: 'dbpassword'
|
||||
```
|
||||
|
||||
Calling..
|
||||
|
||||
:::php
|
||||
// sets up ClassName as a singleton
|
||||
$controller = Injector::inst()->get('MyController');
|
||||
|
||||
```php
|
||||
// sets up ClassName as a singleton
|
||||
$controller = Injector::inst()->get('MyController');
|
||||
```
|
||||
|
||||
Would setup the following
|
||||
|
||||
|
@ -248,15 +282,14 @@ named services, which may not be actual classes, and thus should not behave as t
|
|||
Thus if you want an object to have the injected dependencies of a service of another name, you must
|
||||
assign a reference to that service.
|
||||
|
||||
|
||||
:::yaml
|
||||
Injector:
|
||||
JSONServiceDefinition:
|
||||
class: JSONServiceImplementor
|
||||
properties:
|
||||
Serialiser: JSONSerialiser
|
||||
GZIPJSONProvider: %$JSONServiceDefinition
|
||||
|
||||
```yaml
|
||||
Injector:
|
||||
JSONServiceDefinition:
|
||||
class: JSONServiceImplementor
|
||||
properties:
|
||||
Serialiser: JSONSerialiser
|
||||
GZIPJSONProvider: %$JSONServiceDefinition
|
||||
```
|
||||
|
||||
`Injector::inst()->get('GZIPJSONProvider')` will then be an instance of `JSONServiceImplementor` with the injected
|
||||
properties.
|
||||
|
@ -266,13 +299,13 @@ If class is not specified, then the class will be inherited from the outer servi
|
|||
|
||||
For example with this config:
|
||||
|
||||
:::yaml
|
||||
Injector:
|
||||
Connector:
|
||||
properties:
|
||||
AsString: true
|
||||
ServiceConnector: %$Connector
|
||||
|
||||
```yml
|
||||
Injector:
|
||||
Connector:
|
||||
properties:
|
||||
AsString: true
|
||||
ServiceConnector: %$Connector
|
||||
```
|
||||
|
||||
Both `Connector` and `ServiceConnector` will have the `AsString` property set to true, but the resulting
|
||||
instances will be classes which match their respective service names, due to the lack of a `class` specification.
|
||||
|
@ -284,20 +317,21 @@ which may be later discarded, reverting the application to the original state. T
|
|||
|
||||
This is useful when writing test cases, as certain services may be necessary to override for a single method call.
|
||||
|
||||
:::php
|
||||
// Setup default service
|
||||
Injector::inst()->registerService(new LiveService(), 'ServiceName');
|
||||
|
||||
// Test substitute service temporarily
|
||||
Injector::nest();
|
||||
```php
|
||||
// Setup default service
|
||||
Injector::inst()->registerService(new LiveService(), 'ServiceName');
|
||||
|
||||
Injector::inst()->registerService(new TestingService(), 'ServiceName');
|
||||
$service = Injector::inst()->get('ServiceName');
|
||||
// ... do something with $service
|
||||
// Test substitute service temporarily
|
||||
Injector::nest();
|
||||
|
||||
// revert changes
|
||||
Injector::unnest();
|
||||
Injector::inst()->registerService(new TestingService(), 'ServiceName');
|
||||
$service = Injector::inst()->get('ServiceName');
|
||||
// ... do something with $service
|
||||
|
||||
// revert changes
|
||||
Injector::unnest();
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
|
|
|
@ -45,48 +45,53 @@ used.
|
|||
|
||||
**mysite/code/MySQLWriteDbAspect.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MySQLWriteDbAspect implements BeforeCallAspect {
|
||||
```php
|
||||
class MySQLWriteDbAspect implements BeforeCallAspect
|
||||
{
|
||||
|
||||
/**
|
||||
* @var MySQLDatabase
|
||||
*/
|
||||
public $writeDb;
|
||||
|
||||
public $writeQueries = array(
|
||||
'insert','update','delete','replace'
|
||||
);
|
||||
/**
|
||||
* @var MySQLDatabase
|
||||
*/
|
||||
public $writeDb;
|
||||
|
||||
public $writeQueries = [
|
||||
'insert','update','delete','replace'
|
||||
];
|
||||
|
||||
public function beforeCall($proxied, $method, $args, &$alternateReturn)
|
||||
{
|
||||
if (isset($args[0])) {
|
||||
$sql = $args[0];
|
||||
$code = isset($args[1]) ? $args[1] : E_USER_ERROR;
|
||||
|
||||
public function beforeCall($proxied, $method, $args, &$alternateReturn) {
|
||||
if (isset($args[0])) {
|
||||
$sql = $args[0];
|
||||
$code = isset($args[1]) ? $args[1] : E_USER_ERROR;
|
||||
if (in_array(strtolower(substr($sql,0,strpos($sql,' '))), $this->writeQueries)) {
|
||||
$alternateReturn = $this->writeDb->query($sql, $code);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array(strtolower(substr($sql,0,strpos($sql,' '))), $this->writeQueries)) {
|
||||
$alternateReturn = $this->writeDb->query($sql, $code);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To actually make use of this class, a few different objects need to be configured. First up, define the `writeDb`
|
||||
object that's made use of above.
|
||||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yml
|
||||
WriteMySQLDatabase:
|
||||
class: MySQLDatabase
|
||||
constructor:
|
||||
- type: MySQLDatabase
|
||||
server: write.hostname.db
|
||||
username: user
|
||||
password: pass
|
||||
database: write_database
|
||||
|
||||
```yml
|
||||
|
||||
WriteMySQLDatabase:
|
||||
class: MySQLDatabase
|
||||
constructor:
|
||||
- type: MySQLDatabase
|
||||
server: write.hostname.db
|
||||
username: user
|
||||
password: pass
|
||||
database: write_database
|
||||
```
|
||||
|
||||
This means that whenever something asks the [Injector](api:SilverStripe\Core\Injector\Injector) for the `WriteMySQLDatabase` object, it'll receive an object
|
||||
of type `MySQLDatabase`, configured to point at the 'write_database'.
|
||||
|
@ -95,39 +100,43 @@ Next, this should be bound into an instance of the `Aspect` class
|
|||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yml
|
||||
MySQLWriteDbAspect:
|
||||
properties:
|
||||
writeDb: %$WriteMySQLDatabase
|
||||
|
||||
```yml
|
||||
|
||||
MySQLWriteDbAspect:
|
||||
properties:
|
||||
writeDb: %$WriteMySQLDatabase
|
||||
```
|
||||
|
||||
Next, we need to define the database connection that will be used for all non-write queries
|
||||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yml
|
||||
ReadMySQLDatabase:
|
||||
class: MySQLDatabase
|
||||
constructor:
|
||||
- type: MySQLDatabase
|
||||
server: slavecluster.hostname.db
|
||||
username: user
|
||||
password: pass
|
||||
database: read_database
|
||||
```yml
|
||||
|
||||
ReadMySQLDatabase:
|
||||
class: MySQLDatabase
|
||||
constructor:
|
||||
- type: MySQLDatabase
|
||||
server: slavecluster.hostname.db
|
||||
username: user
|
||||
password: pass
|
||||
database: read_database
|
||||
```
|
||||
|
||||
The final piece that ties everything together is the [AopProxyService](api:SilverStripe\Core\Injector\AopProxyService) instance that will be used as the replacement
|
||||
object when the framework creates the database connection.
|
||||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yml
|
||||
MySQLDatabase:
|
||||
class: AopProxyService
|
||||
properties:
|
||||
proxied: %$ReadMySQLDatabase
|
||||
beforeCall:
|
||||
query:
|
||||
- %$MySQLWriteDbAspect
|
||||
```yml
|
||||
|
||||
MySQLDatabase:
|
||||
class: AopProxyService
|
||||
properties:
|
||||
proxied: %$ReadMySQLDatabase
|
||||
beforeCall:
|
||||
query:
|
||||
- %$MySQLWriteDbAspect
|
||||
```
|
||||
|
||||
The two important parts here are in the `properties` declared for the object.
|
||||
|
||||
|
@ -138,47 +147,47 @@ defined method\_name
|
|||
Overall configuration for this would look as follows
|
||||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yml
|
||||
Injector:
|
||||
ReadMySQLDatabase:
|
||||
class: MySQLDatabase
|
||||
constructor:
|
||||
- type: MySQLDatabase
|
||||
server: slavecluster.hostname.db
|
||||
username: user
|
||||
password: pass
|
||||
database: read_database
|
||||
MySQLWriteDbAspect:
|
||||
properties:
|
||||
writeDb: %$WriteMySQLDatabase
|
||||
WriteMySQLDatabase:
|
||||
class: MySQLDatabase
|
||||
constructor:
|
||||
- type: MySQLDatabase
|
||||
server: write.hostname.db
|
||||
username: user
|
||||
password: pass
|
||||
database: write_database
|
||||
MySQLDatabase:
|
||||
class: AopProxyService
|
||||
properties:
|
||||
proxied: %$ReadMySQLDatabase
|
||||
beforeCall:
|
||||
query:
|
||||
- %$MySQLWriteDbAspect
|
||||
```yml
|
||||
|
||||
Injector:
|
||||
ReadMySQLDatabase:
|
||||
class: MySQLDatabase
|
||||
constructor:
|
||||
- type: MySQLDatabase
|
||||
server: slavecluster.hostname.db
|
||||
username: user
|
||||
password: pass
|
||||
database: read_database
|
||||
MySQLWriteDbAspect:
|
||||
properties:
|
||||
writeDb: %$WriteMySQLDatabase
|
||||
WriteMySQLDatabase:
|
||||
class: MySQLDatabase
|
||||
constructor:
|
||||
- type: MySQLDatabase
|
||||
server: write.hostname.db
|
||||
username: user
|
||||
password: pass
|
||||
database: write_database
|
||||
MySQLDatabase:
|
||||
class: AopProxyService
|
||||
properties:
|
||||
proxied: %$ReadMySQLDatabase
|
||||
beforeCall:
|
||||
query:
|
||||
- %$MySQLWriteDbAspect
|
||||
```
|
||||
|
||||
## Changing what a method returns
|
||||
|
||||
One major feature of an `Aspect` is the ability to modify what is returned from the client's call to the proxied method.
|
||||
As seen in the above example, the `beforeCall` method modifies the `&$alternateReturn` variable, and returns `false`
|
||||
after doing so.
|
||||
|
||||
:::php
|
||||
$alternateReturn = $this->writeDb->query($sql, $code);
|
||||
```php
|
||||
$alternateReturn = $this->writeDb->query($sql, $code);
|
||||
|
||||
return false;
|
||||
return false;
|
||||
```
|
||||
|
||||
By returning `false` from the `beforeCall()` method, the wrapping proxy class will_not_ call any additional `beforeCall`
|
||||
handlers defined for the called method. Assigning the `$alternateReturn` variable also indicates to return that value
|
||||
|
|
|
@ -16,33 +16,35 @@ A basic usage of a module for 3.1 that requires the CMS would look similar to
|
|||
this:
|
||||
|
||||
**mycustommodule/composer.json**
|
||||
:::js
|
||||
{
|
||||
"name": "your-vendor-name/module-name",
|
||||
"description": "One-liner describing your module",
|
||||
"type": "silverstripe-module",
|
||||
"homepage": "http://github.com/your-vendor-name/module-name",
|
||||
"keywords": ["silverstripe", "some-tag", "some-other-tag"],
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{"name": "Your Name","email": "your@email.com"}
|
||||
],
|
||||
"support": {
|
||||
"issues": "http://github.com/your-vendor-name/module-name/issues"
|
||||
},
|
||||
"require": {
|
||||
"silverstripe/cms": "~3.1",
|
||||
"silverstripe/framework": "~3.1"
|
||||
},
|
||||
"extra": {
|
||||
"installer-name": "module-name",
|
||||
"screenshots": [
|
||||
"relative/path/screenshot1.png",
|
||||
"http://myhost.com/screenshot2.png"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
```js
|
||||
|
||||
{
|
||||
"name": "your-vendor-name/module-name",
|
||||
"description": "One-liner describing your module",
|
||||
"type": "silverstripe-module",
|
||||
"homepage": "http://github.com/your-vendor-name/module-name",
|
||||
"keywords": ["silverstripe", "some-tag", "some-other-tag"],
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{"name": "Your Name","email": "your@email.com"}
|
||||
],
|
||||
"support": {
|
||||
"issues": "http://github.com/your-vendor-name/module-name/issues"
|
||||
},
|
||||
"require": {
|
||||
"silverstripe/cms": "~3.1",
|
||||
"silverstripe/framework": "~3.1"
|
||||
},
|
||||
"extra": {
|
||||
"installer-name": "module-name",
|
||||
"screenshots": [
|
||||
"relative/path/screenshot1.png",
|
||||
"http://myhost.com/screenshot2.png"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once your module is published online with a service like Github.com or Bitbucket.com, submit the repository to
|
||||
[Packagist](https://packagist.org/) to have the module accessible to developers. It'll automatically get picked
|
||||
|
|
|
@ -4,30 +4,33 @@ title: How to Create a Google Maps Shortcode
|
|||
|
||||
To demonstrate how easy it is to build custom shortcodes, we'll build one to display a Google Map based on a provided
|
||||
address. We want our CMS authors to be able to embed the map using the following code:
|
||||
|
||||
:::php
|
||||
[googlemap,width=500,height=300]97-99 Courtenay Place, Wellington, New Zealand[/googlemap]
|
||||
|
||||
|
||||
```php
|
||||
[googlemap,width=500,height=300]97-99 Courtenay Place, Wellington, New Zealand[/googlemap]
|
||||
```
|
||||
|
||||
So we've got the address as "content" of our new `googlemap` shortcode tags, plus some `width` and `height` arguments.
|
||||
We'll add defaults to those in our shortcode parser so they're optional.
|
||||
|
||||
**mysite/_config.php**
|
||||
|
||||
:::php
|
||||
ShortcodeParser::get('default')->register('googlemap', function($arguments, $address, $parser, $shortcode) {
|
||||
$iframeUrl = sprintf(
|
||||
'http://maps.google.com/maps?q=%s&hnear=%s&ie=UTF8&hq=&t=m&z=14&output=embed',
|
||||
urlencode($address),
|
||||
urlencode($address)
|
||||
);
|
||||
```php
|
||||
ShortcodeParser::get('default')->register('googlemap', function($arguments, $address, $parser, $shortcode) {
|
||||
$iframeUrl = sprintf(
|
||||
'http://maps.google.com/maps?q=%s&hnear=%s&ie=UTF8&hq=&t=m&z=14&output=embed',
|
||||
urlencode($address),
|
||||
urlencode($address)
|
||||
);
|
||||
|
||||
$width = (isset($arguments['width']) && $arguments['width']) ? $arguments['width'] : 400;
|
||||
$height = (isset($arguments['height']) && $arguments['height']) ? $arguments['height'] : 300;
|
||||
$width = (isset($arguments['width']) && $arguments['width']) ? $arguments['width'] : 400;
|
||||
$height = (isset($arguments['height']) && $arguments['height']) ? $arguments['height'] : 300;
|
||||
|
||||
return sprintf(
|
||||
'<iframe width="%d" height="%d" src="%s" frameborder="0" scrolling="no" marginheight="0" marginwidth="0"></iframe>',
|
||||
$width,
|
||||
$height,
|
||||
$iframeUrl
|
||||
);
|
||||
});
|
||||
return sprintf(
|
||||
'<iframe width="%d" height="%d" src="%s" frameborder="0" scrolling="no" marginheight="0" marginwidth="0"></iframe>',
|
||||
$width,
|
||||
$height,
|
||||
$iframeUrl
|
||||
);
|
||||
});
|
||||
```
|
|
@ -9,44 +9,57 @@ often the member has visited. Or more specifically,
|
|||
how often he has started a browser session, either through
|
||||
explicitly logging in or by invoking the "remember me" functionality.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class MyMemberExtension extends DataExtension {
|
||||
private static $db = array(
|
||||
'LastVisited' => 'Datetime',
|
||||
'NumVisit' => 'Int',
|
||||
);
|
||||
|
||||
public function memberLoggedIn() {
|
||||
$this->logVisit();
|
||||
}
|
||||
```php
|
||||
use SilverStripe\Forms\ReadonlyField;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
|
||||
public function memberAutoLoggedIn() {
|
||||
$this->logVisit();
|
||||
}
|
||||
class MyMemberExtension extends DataExtension
|
||||
{
|
||||
private static $db = [
|
||||
'LastVisited' => 'Datetime',
|
||||
'NumVisit' => 'Int',
|
||||
];
|
||||
|
||||
public function updateCMSFields(FieldList $fields) {
|
||||
$fields->addFieldsToTab('Root.Main', array(
|
||||
ReadonlyField::create('LastVisited', 'Last visited'),
|
||||
ReadonlyField::create('NumVisit', 'Number of visits')
|
||||
));
|
||||
}
|
||||
public function memberLoggedIn()
|
||||
{
|
||||
$this->logVisit();
|
||||
}
|
||||
|
||||
protected function logVisit() {
|
||||
if(!Security::database_is_ready()) return;
|
||||
|
||||
DB::query(sprintf(
|
||||
'UPDATE "Member" SET "LastVisited" = %s, "NumVisit" = "NumVisit" + 1 WHERE "ID" = %d',
|
||||
DB::get_conn()->now(),
|
||||
$this->owner->ID
|
||||
));
|
||||
}
|
||||
}
|
||||
public function memberAutoLoggedIn()
|
||||
{
|
||||
$this->logVisit();
|
||||
}
|
||||
|
||||
public function updateCMSFields(FieldList $fields)
|
||||
{
|
||||
$fields->addFieldsToTab('Root.Main', [
|
||||
ReadonlyField::create('LastVisited', 'Last visited'),
|
||||
ReadonlyField::create('NumVisit', 'Number of visits')
|
||||
]);
|
||||
}
|
||||
|
||||
protected function logVisit()
|
||||
{
|
||||
if(!Security::database_is_ready()) return;
|
||||
|
||||
DB::query(sprintf(
|
||||
'UPDATE "Member" SET "LastVisited" = %s, "NumVisit" = "NumVisit" + 1 WHERE "ID" = %d',
|
||||
DB::get_conn()->now(),
|
||||
$this->owner->ID
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Now you just need to apply this extension through your config:
|
||||
|
||||
:::yml
|
||||
Member:
|
||||
extensions:
|
||||
- MyMemberExtension
|
||||
```yml
|
||||
Member:
|
||||
extensions:
|
||||
- MyMemberExtension
|
||||
|
||||
```
|
|
@ -8,34 +8,36 @@ to ensure that it works as it should. A simple example would be to test the resu
|
|||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
```php
|
||||
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
|
||||
class Page extends SiteTree
|
||||
class Page extends SiteTree
|
||||
{
|
||||
public static function MyMethod()
|
||||
public static function MyMethod()
|
||||
{
|
||||
return (1 + 1);
|
||||
}
|
||||
}
|
||||
return (1 + 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**mysite/tests/PageTest.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
|
||||
```php
|
||||
|
||||
use Page;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
|
||||
class PageTest extends SapphireTest
|
||||
class PageTest extends SapphireTest
|
||||
{
|
||||
public function testMyMethod()
|
||||
public function testMyMethod()
|
||||
{
|
||||
$this->assertEquals(2, Page::MyMethod());
|
||||
}
|
||||
}
|
||||
$this->assertEquals(2, Page::MyMethod());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<div class="info" markdown="1">
|
||||
Tests for your application should be stored in the `mysite/tests` directory. Test cases for add-ons should be stored in
|
||||
|
@ -84,24 +86,27 @@ needs.
|
|||
|
||||
**phpunit.xml**
|
||||
|
||||
:::xml
|
||||
<phpunit bootstrap="framework/tests/bootstrap.php" colors="true">
|
||||
<testsuite name="Default">
|
||||
<directory>mysite/tests</directory>
|
||||
<directory>cms/tests</directory>
|
||||
<directory>framework/tests</directory>
|
||||
</testsuite>
|
||||
|
||||
<listeners>
|
||||
<listener class="SS_TestListener" file="framework/dev/TestListener.php" />
|
||||
</listeners>
|
||||
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>sanitychecks</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
</phpunit>
|
||||
|
||||
```xml
|
||||
|
||||
<phpunit bootstrap="framework/tests/bootstrap.php" colors="true">
|
||||
<testsuite name="Default">
|
||||
<directory>mysite/tests</directory>
|
||||
<directory>cms/tests</directory>
|
||||
<directory>framework/tests</directory>
|
||||
</testsuite>
|
||||
|
||||
<listeners>
|
||||
<listener class="SS_TestListener" file="framework/dev/TestListener.php" />
|
||||
</listeners>
|
||||
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>sanitychecks</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
</phpunit>
|
||||
```
|
||||
|
||||
### setUp() and tearDown()
|
||||
|
||||
|
@ -109,66 +114,69 @@ In addition to loading data through a [Fixture File](fixtures), a test case may
|
|||
run before each test method. For this, use the PHPUnit `setUp` and `tearDown` methods. These are run at the start and
|
||||
end of each test.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
```php
|
||||
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
|
||||
class PageTest extends SapphireTest
|
||||
class PageTest extends SapphireTest
|
||||
{
|
||||
function setUp()
|
||||
function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
parent::setUp();
|
||||
|
||||
// create 100 pages
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
$page = new Page(array('Title' => "Page $i"));
|
||||
$page->write();
|
||||
$page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
|
||||
}
|
||||
// create 100 pages
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
$page = new Page(['Title' => "Page $i"]);
|
||||
$page->write();
|
||||
$page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
|
||||
}
|
||||
|
||||
// set custom configuration for the test.
|
||||
Config::inst()->update('Foo', 'bar', 'Hello!');
|
||||
}
|
||||
// set custom configuration for the test.
|
||||
Config::inst()->update('Foo', 'bar', 'Hello!');
|
||||
}
|
||||
|
||||
public function testMyMethod()
|
||||
public function testMyMethod()
|
||||
{
|
||||
// ..
|
||||
}
|
||||
// ..
|
||||
}
|
||||
|
||||
public function testMySecondMethod()
|
||||
public function testMySecondMethod()
|
||||
{
|
||||
// ..
|
||||
}
|
||||
}
|
||||
// ..
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`tearDownAfterClass` and `setUpBeforeClass` can be used to run code just once for the file rather than before and after
|
||||
each individual test case. Remember to class the parent method in each method to ensure the core boot-strapping of tests
|
||||
takes place.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
```php
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
|
||||
class PageTest extends SapphireTest
|
||||
|
||||
class PageTest extends SapphireTest
|
||||
{
|
||||
public static function setUpBeforeClass()
|
||||
public static function setUpBeforeClass()
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
// ..
|
||||
}
|
||||
// ..
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass()
|
||||
public static function tearDownAfterClass()
|
||||
{
|
||||
parent::tearDownAfterClass();
|
||||
parent::tearDownAfterClass();
|
||||
|
||||
// ..
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
// ..
|
||||
}
|
||||
}
|
||||
|
||||
### Config and Injector Nesting
|
||||
|
||||
A powerful feature of both [`Config`](/developer_guides/configuration/configuration/) and [`Injector`](/developer_guides/extending/injector/) is the ability to "nest" them so that you can make changes that can easily be discarded without having to manage previous values.
|
||||
|
@ -179,24 +187,26 @@ If you need to make changes to `Config` (or `Injector`) for each test (or the wh
|
|||
|
||||
It's important to remember that the `parent::setUp();` functions will need to be called first to ensure the nesting feature works as expected.
|
||||
|
||||
:::php
|
||||
public static function setUpBeforeClass()
|
||||
|
||||
```php
|
||||
public static function setUpBeforeClass()
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
//this will remain for the whole suite and be removed for any other tests
|
||||
Config::inst()->update('ClassName', 'var_name', 'var_value');
|
||||
}
|
||||
|
||||
public function testFeatureDoesAsExpected()
|
||||
parent::setUpBeforeClass();
|
||||
//this will remain for the whole suite and be removed for any other tests
|
||||
Config::inst()->update('ClassName', 'var_name', 'var_value');
|
||||
}
|
||||
|
||||
public function testFeatureDoesAsExpected()
|
||||
{
|
||||
//this will be reset to 'var_value' at the end of this test function
|
||||
Config::inst()->update('ClassName', 'var_name', 'new_var_value');
|
||||
}
|
||||
|
||||
public function testAnotherFeatureDoesAsExpected()
|
||||
//this will be reset to 'var_value' at the end of this test function
|
||||
Config::inst()->update('ClassName', 'var_name', 'new_var_value');
|
||||
}
|
||||
|
||||
public function testAnotherFeatureDoesAsExpected()
|
||||
{
|
||||
Config::inst()->get('ClassName', 'var_name'); // this will be 'var_value'
|
||||
}
|
||||
Config::inst()->get('ClassName', 'var_name'); // this will be 'var_value'
|
||||
}
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
|
|
|
@ -8,37 +8,44 @@ core idea of these tests is the same as `SapphireTest` unit tests but `Functiona
|
|||
creating [HTTPRequest](api:SilverStripe\Control\HTTPRequest), receiving [HTTPResponse](api:SilverStripe\Control\HTTPResponse) objects and modifying the current user session.
|
||||
|
||||
## Get
|
||||
|
||||
:::php
|
||||
$page = $this->get($url);
|
||||
|
||||
```php
|
||||
$page = $this->get($url);
|
||||
```
|
||||
|
||||
Performs a GET request on $url and retrieves the [HTTPResponse](api:SilverStripe\Control\HTTPResponse). This also changes the current page to the value
|
||||
of the response.
|
||||
|
||||
## Post
|
||||
|
||||
:::php
|
||||
$page = $this->post($url);
|
||||
|
||||
```php
|
||||
$page = $this->post($url);
|
||||
```
|
||||
|
||||
Performs a POST request on $url and retrieves the [HTTPResponse](api:SilverStripe\Control\HTTPResponse). This also changes the current page to the value
|
||||
of the response.
|
||||
|
||||
## Submit
|
||||
|
||||
:::php
|
||||
$submit = $this->submitForm($formID, $button = null, $data = array());
|
||||
|
||||
```php
|
||||
$submit = $this->submitForm($formID, $button = null, $data = []);
|
||||
|
||||
```
|
||||
|
||||
Submits the given form (`#ContactForm`) on the current page and returns the [HTTPResponse](api:SilverStripe\Control\HTTPResponse).
|
||||
|
||||
## LogInAs
|
||||
|
||||
:::php
|
||||
$this->logInAs($member);
|
||||
|
||||
```php
|
||||
$this->logInAs($member);
|
||||
```
|
||||
|
||||
Logs a given user in, sets the current session. To log all users out pass `null` to the method.
|
||||
|
||||
:::php
|
||||
$this->logInAs(null);
|
||||
|
||||
```php
|
||||
$this->logInAs(null);
|
||||
```
|
||||
|
||||
## Assertions
|
||||
|
||||
|
@ -46,10 +53,13 @@ The `FunctionalTest` class also provides additional asserts to validate your tes
|
|||
|
||||
### assertPartialMatchBySelector
|
||||
|
||||
:::php
|
||||
$this->assertPartialMatchBySelector('p.good',array(
|
||||
'Test save was successful'
|
||||
));
|
||||
|
||||
```php
|
||||
$this->assertPartialMatchBySelector('p.good',[
|
||||
'Test save was successful'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
Asserts that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS
|
||||
selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The
|
||||
|
@ -58,21 +68,25 @@ assertion fails if one of the expectedMatches fails to appear.
|
|||
|
||||
### assertExactMatchBySelector
|
||||
|
||||
:::php
|
||||
$this->assertExactMatchBySelector("#MyForm_ID p.error", array(
|
||||
"That email address is invalid."
|
||||
));
|
||||
|
||||
```php
|
||||
$this->assertExactMatchBySelector("#MyForm_ID p.error", [
|
||||
"That email address is invalid."
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
Asserts that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS
|
||||
selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The
|
||||
assertion fails if one of the expectedMatches fails to appear.
|
||||
|
||||
### assertPartialHTMLMatchBySelector
|
||||
|
||||
:::php
|
||||
$this->assertPartialHTMLMatchBySelector("#MyForm_ID p.error", array(
|
||||
"That email address is invalid."
|
||||
));
|
||||
```php
|
||||
$this->assertPartialHTMLMatchBySelector("#MyForm_ID p.error", [
|
||||
"That email address is invalid."
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
Assert that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS
|
||||
selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The
|
||||
|
@ -83,11 +97,12 @@ assertion fails if one of the expectedMatches fails to appear.
|
|||
</div>
|
||||
|
||||
### assertExactHTMLMatchBySelector
|
||||
|
||||
:::php
|
||||
$this->assertExactHTMLMatchBySelector("#MyForm_ID p.error", array(
|
||||
"That email address is invalid."
|
||||
));
|
||||
```php
|
||||
$this->assertExactHTMLMatchBySelector("#MyForm_ID p.error", [
|
||||
"That email address is invalid."
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
Assert that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS
|
||||
selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The
|
||||
|
|
|
@ -14,87 +14,95 @@ To include your fixture file in your tests, you should define it as your `$fixtu
|
|||
|
||||
**mysite/tests/MyNewTest.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyNewTest extends SapphireTest {
|
||||
|
||||
protected static $fixture_file = 'fixtures.yml';
|
||||
|
||||
}
|
||||
|
||||
```php
|
||||
class MyNewTest extends SapphireTest
|
||||
{
|
||||
|
||||
protected static $fixture_file = 'fixtures.yml';
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
You can also use an array of fixture files, if you want to use parts of multiple other tests:
|
||||
|
||||
|
||||
**mysite/tests/MyNewTest.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyNewTest extends SapphireTest {
|
||||
|
||||
protected static $fixture_file = array(
|
||||
'fixtures.yml',
|
||||
'otherfixtures.yml'
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
```php
|
||||
class MyNewTest extends SapphireTest
|
||||
{
|
||||
|
||||
protected static $fixture_file = [
|
||||
'fixtures.yml',
|
||||
'otherfixtures.yml'
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Typically, you'd have a separate fixture file for each class you are testing - although overlap between tests is common.
|
||||
|
||||
Fixtures are defined in `YAML`. `YAML` is a markup language which is deliberately simple and easy to read, so it is
|
||||
ideal for fixture generation. Say we have the following two DataObjects:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
|
||||
```php
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject
|
||||
class Player extends DataObject
|
||||
{
|
||||
private static $db = array (
|
||||
'Name' => 'Varchar(255)'
|
||||
);
|
||||
private static $db = [
|
||||
'Name' => 'Varchar(255)'
|
||||
];
|
||||
|
||||
private static $has_one = array(
|
||||
'Team' => 'Team'
|
||||
);
|
||||
}
|
||||
private static $has_one = [
|
||||
'Team' => 'Team'
|
||||
];
|
||||
}
|
||||
|
||||
class Team extends DataObject
|
||||
class Team extends DataObject
|
||||
{
|
||||
private static $db = array (
|
||||
'Name' => 'Varchar(255)',
|
||||
'Origin' => 'Varchar(255)'
|
||||
);
|
||||
private static $db = [
|
||||
'Name' => 'Varchar(255)',
|
||||
'Origin' => 'Varchar(255)'
|
||||
];
|
||||
|
||||
private static $has_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
}
|
||||
private static $has_many = [
|
||||
'Players' => 'Player'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
We can represent multiple instances of them in `YAML` as follows:
|
||||
|
||||
**mysite/tests/fixtures.yml**
|
||||
|
||||
:::yml
|
||||
Team:
|
||||
hurricanes:
|
||||
Name: The Hurricanes
|
||||
Origin: Wellington
|
||||
crusaders:
|
||||
Name: The Crusaders
|
||||
Origin: Canterbury
|
||||
Player:
|
||||
john:
|
||||
Name: John
|
||||
Team: =>Team.hurricanes
|
||||
joe:
|
||||
Name: Joe
|
||||
Team: =>Team.crusaders
|
||||
jack:
|
||||
Name: Jack
|
||||
Team: =>Team.crusaders
|
||||
|
||||
```yml
|
||||
|
||||
Team:
|
||||
hurricanes:
|
||||
Name: The Hurricanes
|
||||
Origin: Wellington
|
||||
crusaders:
|
||||
Name: The Crusaders
|
||||
Origin: Canterbury
|
||||
Player:
|
||||
john:
|
||||
Name: John
|
||||
Team: =>Team.hurricanes
|
||||
joe:
|
||||
Name: Joe
|
||||
Team: =>Team.crusaders
|
||||
jack:
|
||||
Name: Jack
|
||||
Team: =>Team.crusaders
|
||||
```
|
||||
|
||||
This `YAML` is broken up into three levels, signified by the indentation of each line. In the first level of
|
||||
indentation, `Player` and `Team`, represent the class names of the objects we want to be created.
|
||||
|
@ -102,8 +110,10 @@ indentation, `Player` and `Team`, represent the class names of the objects we wa
|
|||
The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are **identifiers**. Each identifier you specify
|
||||
represents a new object and can be referenced in the PHP using `objFromFixture`
|
||||
|
||||
:::php
|
||||
$player = $this->objFromFixture('Player', 'jack');
|
||||
|
||||
```php
|
||||
$player = $this->objFromFixture('Player', 'jack');
|
||||
```
|
||||
|
||||
The third and final level represents each individual object's fields.
|
||||
|
||||
|
@ -131,37 +141,43 @@ This style of relationship declaration can be used for any type of relationship
|
|||
|
||||
We can also declare the relationships conversely. Another way we could write the previous example is:
|
||||
|
||||
:::yml
|
||||
Player:
|
||||
john:
|
||||
Name: John
|
||||
joe:
|
||||
Name: Joe
|
||||
jack:
|
||||
Name: Jack
|
||||
Team:
|
||||
hurricanes:
|
||||
Name: Hurricanes
|
||||
Origin: Wellington
|
||||
Players: =>Player.john
|
||||
crusaders:
|
||||
Name: Crusaders
|
||||
Origin: Canterbury
|
||||
Players: =>Player.joe,=>Player.jack
|
||||
|
||||
```yml
|
||||
|
||||
Player:
|
||||
john:
|
||||
Name: John
|
||||
joe:
|
||||
Name: Joe
|
||||
jack:
|
||||
Name: Jack
|
||||
Team:
|
||||
hurricanes:
|
||||
Name: Hurricanes
|
||||
Origin: Wellington
|
||||
Players: =>Player.john
|
||||
crusaders:
|
||||
Name: Crusaders
|
||||
Origin: Canterbury
|
||||
Players: =>Player.joe,=>Player.jack
|
||||
```
|
||||
|
||||
The database is populated by instantiating `DataObject` objects and setting the fields declared in the `YAML`, then
|
||||
calling `write()` on those objects. Take for instance the `hurricances` record in the `YAML`. It is equivalent to
|
||||
writing:
|
||||
|
||||
:::php
|
||||
$team = new Team(array(
|
||||
'Name' => 'Hurricanes',
|
||||
'Origin' => 'Wellington'
|
||||
));
|
||||
|
||||
$team->write();
|
||||
```php
|
||||
$team = new Team([
|
||||
'Name' => 'Hurricanes',
|
||||
'Origin' => 'Wellington'
|
||||
]);
|
||||
|
||||
$team->Players()->add($john);
|
||||
$team->write();
|
||||
|
||||
$team->Players()->add($john);
|
||||
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
As the YAML fixtures will call `write`, any `onBeforeWrite()` or default value logic will be executed as part of the
|
||||
|
@ -172,7 +188,9 @@ test.
|
|||
|
||||
As of SilverStripe 4 you will need to use fully qualfied class names in your YAML fixture files. In the above examples, they belong to the global namespace so there is nothing requires, but if you have a deeper DataObject, or it has a relationship to models that are part of the framework for example, you will need to include their namespaces:
|
||||
|
||||
:::yml
|
||||
|
||||
```yml
|
||||
|
||||
MyProject\Model\Player:
|
||||
john:
|
||||
Name: join
|
||||
|
@ -182,6 +200,7 @@ As of SilverStripe 4 you will need to use fully qualfied class names in your YAM
|
|||
Name: Crusaders
|
||||
Origin: Canterbury
|
||||
Players: =>MyProject\Model\Player.john
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
If your tests are failing and your database has table names that follow the fully qualified class names, you've probably forgotten to implement `private static $table_name = 'Player';` on your namespaced class. This property was introduced in SilverStripe 4 to reduce data migration work. See [DataObject](api:SilverStripe\ORM\DataObject) for an example.
|
||||
|
@ -192,61 +211,67 @@ If your tests are failing and your database has table names that follow the full
|
|||
`many_many` relations can have additional database fields attached to the relationship. For example we may want to
|
||||
declare the role each player has in the team.
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject
|
||||
class Player extends DataObject
|
||||
{
|
||||
private static $db = array (
|
||||
'Name' => 'Varchar(255)'
|
||||
);
|
||||
private static $db = [
|
||||
'Name' => 'Varchar(255)'
|
||||
];
|
||||
|
||||
private static $belongs_many_many = array(
|
||||
'Teams' => 'Team'
|
||||
);
|
||||
}
|
||||
private static $belongs_many_many = [
|
||||
'Teams' => 'Team'
|
||||
];
|
||||
}
|
||||
|
||||
class Team extends DataObject
|
||||
class Team extends DataObject
|
||||
{
|
||||
private static $db = array (
|
||||
'Name' => 'Varchar(255)'
|
||||
);
|
||||
private static $db = [
|
||||
'Name' => 'Varchar(255)'
|
||||
];
|
||||
|
||||
private static $many_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
private static $many_many = [
|
||||
'Players' => 'Player'
|
||||
];
|
||||
|
||||
private static $many_many_extraFields = array(
|
||||
'Players' => array(
|
||||
'Role' => "Varchar"
|
||||
)
|
||||
);
|
||||
}
|
||||
private static $many_many_extraFields = [
|
||||
'Players' => [
|
||||
'Role' => "Varchar"
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To provide the value for the `many_many_extraField` use the YAML list syntax.
|
||||
|
||||
:::yml
|
||||
Player:
|
||||
john:
|
||||
Name: John
|
||||
joe:
|
||||
Name: Joe
|
||||
jack:
|
||||
Name: Jack
|
||||
Team:
|
||||
hurricanes:
|
||||
Name: The Hurricanes
|
||||
Players:
|
||||
- =>Player.john:
|
||||
Role: Captain
|
||||
|
||||
crusaders:
|
||||
Name: The Crusaders
|
||||
Players:
|
||||
- =>Player.joe:
|
||||
Role: Captain
|
||||
- =>Player.jack:
|
||||
Role: Winger
|
||||
```yml
|
||||
|
||||
Player:
|
||||
john:
|
||||
Name: John
|
||||
joe:
|
||||
Name: Joe
|
||||
jack:
|
||||
Name: Jack
|
||||
Team:
|
||||
hurricanes:
|
||||
Name: The Hurricanes
|
||||
Players:
|
||||
- =>Player.john:
|
||||
Role: Captain
|
||||
|
||||
crusaders:
|
||||
Name: The Crusaders
|
||||
Players:
|
||||
- =>Player.joe:
|
||||
Role: Captain
|
||||
- =>Player.jack:
|
||||
Role: Winger
|
||||
```
|
||||
|
||||
## Fixture Factories
|
||||
|
||||
|
@ -266,17 +291,22 @@ name, which is usually set to the class it creates such as `Member` or `Page`.
|
|||
Blueprints are auto-created for all available DataObject subclasses, you only need to instantiate a factory to start
|
||||
using them.
|
||||
|
||||
:::php
|
||||
$factory = Injector::inst()->create('FixtureFactory');
|
||||
|
||||
$obj = $factory->createObject('Team', 'hurricanes');
|
||||
```php
|
||||
$factory = Injector::inst()->create('FixtureFactory');
|
||||
|
||||
$obj = $factory->createObject('Team', 'hurricanes');
|
||||
```
|
||||
|
||||
In order to create an object with certain properties, just add a third argument:
|
||||
|
||||
:::php
|
||||
$obj = $factory->createObject('Team', 'hurricanes', array(
|
||||
'Name' => 'My Value'
|
||||
));
|
||||
|
||||
```php
|
||||
$obj = $factory->createObject('Team', 'hurricanes', [
|
||||
'Name' => 'My Value'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
<div class="warning" markdown="1">
|
||||
It is important to remember that fixtures are referenced by arbitrary identifiers ('hurricanes'). These are internally
|
||||
|
@ -285,60 +315,72 @@ mapped to their database identifiers.
|
|||
|
||||
After we've created this object in the factory, `getId` is used to retrieve it by the identifier.
|
||||
|
||||
:::php
|
||||
$databaseId = $factory->getId('Team', 'hurricanes');
|
||||
|
||||
```php
|
||||
$databaseId = $factory->getId('Team', 'hurricanes');
|
||||
```
|
||||
|
||||
### Default Properties
|
||||
|
||||
Blueprints can be overwritten in order to customise their behavior. For example, if a Fixture does not provide a Team
|
||||
name, we can set the default to be `Unknown Team`.
|
||||
|
||||
:::php
|
||||
$factory->define('Team', array(
|
||||
'Name' => 'Unknown Team'
|
||||
));
|
||||
|
||||
```php
|
||||
$factory->define('Team', [
|
||||
'Name' => 'Unknown Team'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
### Dependent Properties
|
||||
|
||||
Values can be set on demand through anonymous functions, which can either generate random defaults, or create composite
|
||||
values based on other fixture data.
|
||||
|
||||
:::php
|
||||
$factory->define('Member', array(
|
||||
'Email' => function($obj, $data, $fixtures) {
|
||||
if(isset($data['FirstName']) {
|
||||
$obj->Email = strtolower($data['FirstName']) . '@example.org';
|
||||
}
|
||||
},
|
||||
'Score' => function($obj, $data, $fixtures) {
|
||||
$obj->Score = rand(0,10);
|
||||
}
|
||||
));
|
||||
|
||||
```php
|
||||
$factory->define('Member', [
|
||||
'Email' => function($obj, $data, $fixtures) {
|
||||
if(isset($data['FirstName']) {
|
||||
$obj->Email = strtolower($data['FirstName']) . '@example.org';
|
||||
}
|
||||
},
|
||||
'Score' => function($obj, $data, $fixtures) {
|
||||
$obj->Score = rand(0,10);
|
||||
}
|
||||
)];
|
||||
|
||||
```
|
||||
|
||||
### Relations
|
||||
|
||||
Model relations can be expressed through the same notation as in the YAML fixture format described earlier, through the
|
||||
`=>` prefix on data values.
|
||||
|
||||
:::php
|
||||
$obj = $factory->createObject('Team', 'hurricanes', array(
|
||||
'MyHasManyRelation' => '=>Player.john,=>Player.joe'
|
||||
));
|
||||
|
||||
```php
|
||||
$obj = $factory->createObject('Team', 'hurricanes', [
|
||||
'MyHasManyRelation' => '=>Player.john,=>Player.joe'
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
#### Callbacks
|
||||
|
||||
Sometimes new model instances need to be modified in ways which can't be expressed in their properties, for example to
|
||||
publish a page, which requires a method call.
|
||||
|
||||
:::php
|
||||
$blueprint = Injector::inst()->create('FixtureBlueprint', 'Member');
|
||||
|
||||
$blueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) {
|
||||
$obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
|
||||
});
|
||||
```php
|
||||
$blueprint = Injector::inst()->create('FixtureBlueprint', 'Member');
|
||||
|
||||
$page = $factory->define('Page', $blueprint);
|
||||
$blueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) {
|
||||
$obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
|
||||
});
|
||||
|
||||
$page = $factory->define('Page', $blueprint);
|
||||
```
|
||||
|
||||
Available callbacks:
|
||||
|
||||
|
@ -351,21 +393,23 @@ Data of the same type can have variations, for example forum members vs. CMS adm
|
|||
class, but have completely different properties. This is where named blueprints come in. By default, blueprint names
|
||||
equal the class names they manage.
|
||||
|
||||
:::php
|
||||
$memberBlueprint = Injector::inst()->create('FixtureBlueprint', 'Member', 'Member');
|
||||
|
||||
$adminBlueprint = Injector::inst()->create('FixtureBlueprint', 'AdminMember', 'Member');
|
||||
```php
|
||||
$memberBlueprint = Injector::inst()->create('FixtureBlueprint', 'Member', 'Member');
|
||||
|
||||
$adminBlueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) {
|
||||
if(isset($fixtures['Group']['admin'])) {
|
||||
$adminGroup = Group::get()->byId($fixtures['Group']['admin']);
|
||||
$obj->Groups()->add($adminGroup);
|
||||
}
|
||||
});
|
||||
$adminBlueprint = Injector::inst()->create('FixtureBlueprint', 'AdminMember', 'Member');
|
||||
|
||||
$member = $factory->createObject('Member'); // not in admin group
|
||||
$adminBlueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) {
|
||||
if(isset($fixtures['Group']['admin'])) {
|
||||
$adminGroup = Group::get()->byId($fixtures['Group']['admin']);
|
||||
$obj->Groups()->add($adminGroup);
|
||||
}
|
||||
});
|
||||
|
||||
$admin = $factory->createObject('AdminMember'); // in admin group
|
||||
$member = $factory->createObject('Member'); // not in admin group
|
||||
|
||||
$admin = $factory->createObject('AdminMember'); // in admin group
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
|
|
|
@ -7,43 +7,45 @@ how you can load default records into the test database.
|
|||
|
||||
**mysite/tests/PageTest.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
|
||||
```php
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
|
||||
class PageTest extends SapphireTest
|
||||
class PageTest extends SapphireTest
|
||||
{
|
||||
/**
|
||||
* Defines the fixture file to use for this test class
|
||||
* @var string
|
||||
*/
|
||||
protected static $fixture_file = 'SiteTreeTest.yml';
|
||||
/**
|
||||
* Defines the fixture file to use for this test class
|
||||
* @var string
|
||||
*/
|
||||
protected static $fixture_file = 'SiteTreeTest.yml';
|
||||
|
||||
/**
|
||||
* Test generation of the URLSegment values.
|
||||
*
|
||||
* Makes sure to:
|
||||
* - Turn things into lowercase-hyphen-format
|
||||
* - Generates from Title by default, unless URLSegment is explicitly set
|
||||
* - Resolves duplicates by appending a number
|
||||
*/
|
||||
public function testURLGeneration()
|
||||
/**
|
||||
* Test generation of the URLSegment values.
|
||||
*
|
||||
* Makes sure to:
|
||||
* - Turn things into lowercase-hyphen-format
|
||||
* - Generates from Title by default, unless URLSegment is explicitly set
|
||||
* - Resolves duplicates by appending a number
|
||||
*/
|
||||
public function testURLGeneration()
|
||||
{
|
||||
$expectedURLs = array(
|
||||
'home' => 'home',
|
||||
'staff' => 'my-staff',
|
||||
'about' => 'about-us',
|
||||
'staffduplicate' => 'my-staff-2'
|
||||
);
|
||||
$expectedURLs = [
|
||||
'home' => 'home',
|
||||
'staff' => 'my-staff',
|
||||
'about' => 'about-us',
|
||||
'staffduplicate' => 'my-staff-2'
|
||||
];
|
||||
|
||||
foreach($expectedURLs as $fixture => $urlSegment) {
|
||||
$obj = $this->objFromFixture('Page', $fixture);
|
||||
foreach($expectedURLs as $fixture => $urlSegment) {
|
||||
$obj = $this->objFromFixture('Page', $fixture);
|
||||
|
||||
$this->assertEquals($urlSegment, $obj->URLSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->assertEquals($urlSegment, $obj->URLSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Firstly we define a static `$fixture_file`, this should point to a file that represents the data we want to test,
|
||||
represented as a YAML [Fixture](../fixtures). When our test is run, the data from this file will be loaded into a test
|
||||
|
|
|
@ -9,42 +9,47 @@ response and modify the session within a test.
|
|||
|
||||
**mysite/tests/HomePageTest.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class HomePageTest extends FunctionalTest {
|
||||
```php
|
||||
use SilverStripe\Security\Member;
|
||||
|
||||
/**
|
||||
* Test generation of the view
|
||||
*/
|
||||
public function testViewHomePage() {
|
||||
$page = $this->get('home/');
|
||||
class HomePageTest extends FunctionalTest
|
||||
{
|
||||
|
||||
// Home page should load..
|
||||
$this->assertEquals(200, $page->getStatusCode());
|
||||
/**
|
||||
* Test generation of the view
|
||||
*/
|
||||
public function testViewHomePage()
|
||||
{
|
||||
$page = $this->get('home/');
|
||||
|
||||
// We should see a login form
|
||||
$login = $this->submitForm("LoginFormID", null, array(
|
||||
'Email' => 'test@test.com',
|
||||
'Password' => 'wrongpassword'
|
||||
));
|
||||
// Home page should load..
|
||||
$this->assertEquals(200, $page->getStatusCode());
|
||||
|
||||
// wrong details, should now see an error message
|
||||
$this->assertExactHTMLMatchBySelector("#LoginForm p.error", array(
|
||||
"That email address is invalid."
|
||||
));
|
||||
// We should see a login form
|
||||
$login = $this->submitForm("LoginFormID", null, [
|
||||
'Email' => 'test@test.com',
|
||||
'Password' => 'wrongpassword'
|
||||
]);
|
||||
|
||||
// If we login as a user we should see a welcome message
|
||||
$me = Member::get()->first();
|
||||
// wrong details, should now see an error message
|
||||
$this->assertExactHTMLMatchBySelector("#LoginForm p.error", [
|
||||
"That email address is invalid."
|
||||
]);
|
||||
|
||||
$this->logInAs($me);
|
||||
$page = $this->get('home/');
|
||||
// If we login as a user we should see a welcome message
|
||||
$me = Member::get()->first();
|
||||
|
||||
$this->assertExactHTMLMatchBySelector("#Welcome", array(
|
||||
'Welcome Back'
|
||||
));
|
||||
}
|
||||
}
|
||||
$this->logInAs($me);
|
||||
$page = $this->get('home/');
|
||||
|
||||
$this->assertExactHTMLMatchBySelector("#Welcome", [
|
||||
'Welcome Back'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
|
|
|
@ -8,37 +8,43 @@ see the [Fixtures](../fixtures) documentation.
|
|||
In this how to we'll use a `FixtureFactory` and a custom blue print for giving us a shortcut for creating new objects
|
||||
with information that we need.
|
||||
|
||||
:::php
|
||||
class MyObjectTest extends SapphireTest {
|
||||
|
||||
protected $factory;
|
||||
```php
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
|
||||
function __construct() {
|
||||
parent::__construct();
|
||||
class MyObjectTest extends SapphireTest
|
||||
{
|
||||
|
||||
$factory = Injector::inst()->create('FixtureFactory');
|
||||
protected $factory;
|
||||
|
||||
// Defines a "blueprint" for new objects
|
||||
$factory->define('MyObject', array(
|
||||
'MyProperty' => 'My Default Value'
|
||||
));
|
||||
function __construct() {
|
||||
parent::__construct();
|
||||
|
||||
$this->factory = $factory;
|
||||
}
|
||||
$factory = Injector::inst()->create('FixtureFactory');
|
||||
|
||||
function testSomething() {
|
||||
$MyObjectObj = $this->factory->createObject(
|
||||
'MyObject',
|
||||
array('MyOtherProperty' => 'My Custom Value')
|
||||
);
|
||||
// Defines a "blueprint" for new objects
|
||||
$factory->define('MyObject', [
|
||||
'MyProperty' => 'My Default Value'
|
||||
]);
|
||||
|
||||
echo $MyObjectObj->MyProperty;
|
||||
// returns "My Default Value"
|
||||
$this->factory = $factory;
|
||||
}
|
||||
|
||||
echo $myPageObj->MyOtherProperty;
|
||||
// returns "My Custom Value"
|
||||
}
|
||||
}
|
||||
function testSomething() {
|
||||
$MyObjectObj = $this->factory->createObject(
|
||||
'MyObject',
|
||||
['MyOtherProperty' => 'My Custom Value']
|
||||
);
|
||||
|
||||
echo $MyObjectObj->MyProperty;
|
||||
// returns "My Default Value"
|
||||
|
||||
echo $myPageObj->MyOtherProperty;
|
||||
// returns "My Custom Value"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
|
|
|
@ -6,23 +6,27 @@ SilverStripe's test system has built-in support for testing emails sent using th
|
|||
running a [SapphireTest](api:SilverStripe\Dev\SapphireTest) test, then it holds off actually sending the email, and instead lets you assert that an
|
||||
email was sent using this method.
|
||||
|
||||
:::php
|
||||
public function MyMethod() {
|
||||
$e = new Email();
|
||||
$e->To = "someone@example.com";
|
||||
$e->Subject = "Hi there";
|
||||
$e->Body = "I just really wanted to email you and say hi.";
|
||||
$e->send();
|
||||
}
|
||||
|
||||
```php
|
||||
public function MyMethod()
|
||||
{
|
||||
$e = new Email();
|
||||
$e->To = "someone@example.com";
|
||||
$e->Subject = "Hi there";
|
||||
$e->Body = "I just really wanted to email you and say hi.";
|
||||
$e->send();
|
||||
}
|
||||
```
|
||||
|
||||
To test that `MyMethod` sends the correct email, use the [SapphireTest::assertEmailSent()](api:SilverStripe\Dev\SapphireTest::assertEmailSent()) method.
|
||||
|
||||
:::php
|
||||
$this->assertEmailSent($to, $from, $subject, $body);
|
||||
|
||||
// to assert that the email is sent to the correct person
|
||||
$this->assertEmailSent("someone@example.com", null, "/th.*e$/");
|
||||
```php
|
||||
$this->assertEmailSent($to, $from, $subject, $body);
|
||||
|
||||
// to assert that the email is sent to the correct person
|
||||
$this->assertEmailSent("someone@example.com", null, "/th.*e$/");
|
||||
```
|
||||
|
||||
Each of the arguments (`$to`, `$from`, `$subject` and `$body`) can be either one of the following.
|
||||
|
||||
|
|
|
@ -9,14 +9,17 @@ and behaviors. The environment is managed either through a [YML configuration fi
|
|||
|
||||
The definition of setting an environment type in a `mysite/_config/app.yml` looks like
|
||||
|
||||
:::yml
|
||||
SilverStripe\Control\Director:
|
||||
environment_type: 'dev'
|
||||
|
||||
```yml
|
||||
|
||||
SilverStripe\Control\Director:
|
||||
environment_type: 'dev'
|
||||
```
|
||||
|
||||
The definition of setting an environment type in a `.env` file looks like
|
||||
|
||||
SS_ENVIRONMENT_TYPE="dev"
|
||||
|
||||
```
|
||||
SS_ENVIRONMENT_TYPE="dev"
|
||||
```
|
||||
The three environment types you can set are `dev`, `test` and `live`.
|
||||
|
||||
### Dev
|
||||
|
@ -38,13 +41,16 @@ Test mode is designed for staging environments or other private collaboration si
|
|||
In this mode error messages are hidden from the user and SilverStripe includes [BasicAuth](api:SilverStripe\Security\BasicAuth) integration if you
|
||||
want to password protect the site. You can enable that by adding this to your `mysite/_config/app.yml` file:
|
||||
|
||||
:::yml
|
||||
---
|
||||
Only:
|
||||
environment: 'test'
|
||||
---
|
||||
SilverStripe\Security\BasicAuth:
|
||||
entire_site_protected: true
|
||||
|
||||
```yml
|
||||
|
||||
---
|
||||
Only:
|
||||
environment: 'test'
|
||||
---
|
||||
SilverStripe\Security\BasicAuth:
|
||||
entire_site_protected: true
|
||||
```
|
||||
|
||||
### Live Mode
|
||||
|
||||
|
@ -60,28 +66,30 @@ Live sites should always run in live mode. You should not run production website
|
|||
You can check for the current environment type in [config files](../configuration) through the `environment` variant.
|
||||
|
||||
**mysite/_config/app.yml**
|
||||
---
|
||||
Only:
|
||||
environment: 'live'
|
||||
---
|
||||
MyClass:
|
||||
myvar: live_value
|
||||
---
|
||||
Only:
|
||||
environment: 'test'
|
||||
---
|
||||
MyClass:
|
||||
myvar: test_value
|
||||
|
||||
```yml
|
||||
---
|
||||
Only:
|
||||
environment: 'live'
|
||||
---
|
||||
MyClass:
|
||||
myvar: live_value
|
||||
---
|
||||
Only:
|
||||
environment: 'test'
|
||||
---
|
||||
MyClass:
|
||||
myvar: test_value
|
||||
```
|
||||
Checking for what environment you're running in can also be done in PHP. Your application code may disable or enable
|
||||
certain functionality depending on the environment type.
|
||||
certain functionality depending on the environment type.
|
||||
```php
|
||||
if (Director::isLive()) {
|
||||
// is in live
|
||||
} elseif (Director::isTest()) {
|
||||
// is in test mode
|
||||
} elseif (Director::isDev()) {
|
||||
// is in dev mode
|
||||
}
|
||||
```
|
||||
|
||||
:::php
|
||||
if (Director::isLive()) {
|
||||
// is in live
|
||||
} elseif (Director::isTest()) {
|
||||
// is in test mode
|
||||
} elseif (Director::isDev()) {
|
||||
// is in dev mode
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ For informational and debug logs, you can use the Logger directly. The Logger is
|
|||
can be accessed via the `Injector`:
|
||||
|
||||
```php
|
||||
Injector::inst()->get(LoggerInterface::class)->info('User has logged in: ID #' . Member::currentUserID());
|
||||
Injector::inst()->get(LoggerInterface::class)->info('User has logged in: ID #' . Security::getCurrentUser()->ID);
|
||||
Injector::inst()->get(LoggerInterface::class)->debug('Query executed: ' . $sql);
|
||||
```
|
||||
|
||||
|
@ -59,6 +59,8 @@ approach is to use depedency injection to pass the logger in for you. The [Injec
|
|||
can help with this. The most straightforward is to specify a `dependencies` config setting, like this:
|
||||
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller
|
||||
{
|
||||
private static $dependencies = [
|
||||
|
|
|
@ -10,10 +10,11 @@ to track down a template or two. The template engine can help you along by displ
|
|||
source code comments indicating which template is responsible for rendering each
|
||||
block of html on your page.
|
||||
|
||||
:::yaml
|
||||
---
|
||||
Only:
|
||||
environment: 'dev'
|
||||
---
|
||||
SSViewer:
|
||||
source_file_comments: true
|
||||
```yaml
|
||||
---
|
||||
Only:
|
||||
environment: 'dev'
|
||||
---
|
||||
SSViewer:
|
||||
source_file_comments: true
|
||||
```
|
|
@ -17,7 +17,7 @@ bottle-necks and identify slow moving parts of your application chain.
|
|||
|
||||
The [Debug](api:SilverStripe\Dev\Debug) class contains a number of static utility methods for more advanced debugging.
|
||||
|
||||
:::php
|
||||
```php
|
||||
Debug::show($myVariable);
|
||||
// similar to print_r($myVariable) but shows it in a more useful format.
|
||||
|
||||
|
@ -26,9 +26,9 @@ The [Debug](api:SilverStripe\Dev\Debug) class contains a number of static utilit
|
|||
|
||||
Backtrace::backtrace();
|
||||
// prints a calls-stack
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [SS_Log](api:SS_Log)
|
||||
* [Backtrace](api:SilverStripe\Dev\Backtrace)
|
||||
* [Debug](api:SilverStripe\Dev\Debug)
|
||||
|
|
|
@ -3,14 +3,14 @@ summary: Cache SilverStripe templates to reduce database queries.
|
|||
|
||||
# Partial Caching
|
||||
|
||||
Partial caching is a feature that allows the caching of just a portion of a page.
|
||||
|
||||
:::ss
|
||||
<% cached 'CacheKey' %>
|
||||
$DataTable
|
||||
...
|
||||
<% end_cached %>
|
||||
Partial caching is a feature that allows the caching of just a portion of a page.
|
||||
```ss
|
||||
|
||||
<% cached 'CacheKey' %>
|
||||
$DataTable
|
||||
...
|
||||
<% end_cached %>
|
||||
```
|
||||
|
||||
Each cache block has a cache key. A cache key is an unlimited number of comma separated variables and quoted strings.
|
||||
Every time the cache key returns a different result, the contents of the block are recalculated. If the cache key is
|
||||
|
@ -21,18 +21,21 @@ will invalidate the cache after a given amount of time has expired (default 10 m
|
|||
|
||||
Here are some more complex examples:
|
||||
|
||||
:::ss
|
||||
<% cached 'database', $LastEdited %>
|
||||
<!-- that updates every time the record changes. -->
|
||||
<% end_cached %>
|
||||
|
||||
<% cached 'loginblock', $CurrentMember.ID %>
|
||||
<!-- cached unique to the user. i.e for user 2, they will see a different cache to user 1 -->
|
||||
<% end_cached %>
|
||||
|
||||
<% cached 'loginblock', $LastEdited, $CurrentMember.isAdmin %>
|
||||
<!-- recached when block object changes, and if the user is admin -->
|
||||
<% end_cached %>
|
||||
```ss
|
||||
|
||||
<% cached 'database', $LastEdited %>
|
||||
<!-- that updates every time the record changes. -->
|
||||
<% end_cached %>
|
||||
|
||||
<% cached 'loginblock', $CurrentMember.ID %>
|
||||
<!-- cached unique to the user. i.e for user 2, they will see a different cache to user 1 -->
|
||||
<% end_cached %>
|
||||
|
||||
<% cached 'loginblock', $LastEdited, $CurrentMember.isAdmin %>
|
||||
<!-- recached when block object changes, and if the user is admin -->
|
||||
<% end_cached %>
|
||||
```
|
||||
|
||||
An additional global key is incorporated in the cache lookup. The default value for this is
|
||||
`$CurrentReadingMode, $CurrentUser.ID`. This ensures that the current [Versioned](api:SilverStripe\Versioned\Versioned) state and user ID are used.
|
||||
|
@ -44,10 +47,12 @@ user does not influence your template content, you can update this key as below;
|
|||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yaml
|
||||
SSViewer:
|
||||
global_key: '$CurrentReadingMode, $Locale'
|
||||
|
||||
|
||||
```yaml
|
||||
|
||||
SilverStripe\View\SSViewer:
|
||||
global_key: '$CurrentReadingMode, $Locale'
|
||||
```
|
||||
|
||||
## Aggregates
|
||||
|
||||
|
@ -58,16 +63,22 @@ on sets of [DataObject](api:SilverStripe\ORM\DataObject)s - the most useful for
|
|||
For example, if we have a menu, we want that menu to update whenever _any_ page is edited, but would like to cache it
|
||||
otherwise. By using aggregates, we do that like this:
|
||||
|
||||
:::ss
|
||||
<% cached 'navigation', $List('SiteTree').max('LastEdited'), $List('SiteTree').count() %>
|
||||
|
||||
```ss
|
||||
|
||||
<% cached 'navigation', $List('SiteTree').max('LastEdited'), $List('SiteTree').count() %>
|
||||
```
|
||||
|
||||
The cache for this will update whenever a page is added, removed or edited.
|
||||
|
||||
If we have a block that shows a list of categories, we can make sure the cache updates every time a category is added
|
||||
or edited
|
||||
|
||||
:::ss
|
||||
<% cached 'categorylist', $List('Category').max('LastEdited'), $List('Category').count() %>
|
||||
|
||||
```ss
|
||||
|
||||
<% cached 'categorylist', $List('Category').max('LastEdited'), $List('Category').count() %>
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
Note the use of both `.max('LastEdited')` and `.count()` - this takes care of both the case where an object has been
|
||||
|
@ -85,23 +96,29 @@ 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 `PageController`:
|
||||
|
||||
:::php
|
||||
|
||||
public function SliderCacheKey() {
|
||||
$fragments = array(
|
||||
'Page-Slides',
|
||||
$this->ID,
|
||||
// identify which objects are in the list and their sort order
|
||||
implode('-', $this->Slides()->Column('ID')),
|
||||
$this->Slides()->max('LastEdited')
|
||||
);
|
||||
return implode('-_-', $fragments);
|
||||
}
|
||||
```php
|
||||
public function SliderCacheKey()
|
||||
{
|
||||
$fragments = [
|
||||
'Page-Slides',
|
||||
$this->ID,
|
||||
// identify which objects are in the list and their sort order
|
||||
implode('-', $this->Slides()->Column('ID')),
|
||||
$this->Slides()->max('LastEdited')
|
||||
];
|
||||
return implode('-_-', $fragments);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Then reference that function in the cache key:
|
||||
|
||||
:::ss
|
||||
<% cached $SliderCacheKey %>
|
||||
|
||||
```ss
|
||||
|
||||
<% cached $SliderCacheKey %>
|
||||
```
|
||||
|
||||
The example above would work for both a has_many and many_many relationship.
|
||||
|
||||
|
@ -119,24 +136,31 @@ data updates.
|
|||
|
||||
For instance, if we show some blog statistics, but are happy having them be slightly stale, we could do
|
||||
|
||||
:::ss
|
||||
<% cached 'blogstatistics', $Blog.ID %>
|
||||
|
||||
```ss
|
||||
|
||||
<% cached 'blogstatistics', $Blog.ID %>
|
||||
```
|
||||
|
||||
which will invalidate after the cache lifetime expires. If you need more control than that (cache lifetime is
|
||||
configurable only on a site-wide basis), you could add a special function to your controller:
|
||||
|
||||
:::php
|
||||
public function BlogStatisticsCounter() {
|
||||
return (int)(time() / 60 / 5); // Returns a new number every five minutes
|
||||
}
|
||||
|
||||
```php
|
||||
public function BlogStatisticsCounter()
|
||||
{
|
||||
return (int)(time() / 60 / 5); // Returns a new number every five minutes
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
and then use it in the cache key
|
||||
|
||||
:::ss
|
||||
<% cached 'blogstatistics', $Blog.ID, $BlogStatisticsCounter %>
|
||||
|
||||
```ss
|
||||
|
||||
<% cached 'blogstatistics', $Blog.ID, $BlogStatisticsCounter %>
|
||||
```
|
||||
|
||||
## Cache block conditionals
|
||||
|
||||
|
@ -147,17 +171,22 @@ value must be true for that block to be cached. Conversely if 'unless' is used,
|
|||
Following on from the previous example, you might wish to only cache slightly-stale data if the server is experiencing
|
||||
heavy load:
|
||||
|
||||
:::ss
|
||||
<% cached 'blogstatistics', $Blog.ID if $HighLoad %>
|
||||
|
||||
```ss
|
||||
|
||||
<% cached 'blogstatistics', $Blog.ID if $HighLoad %>
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
:::ss
|
||||
<% cached unless $CurrentUser %>
|
||||
|
||||
```ss
|
||||
|
||||
<% cached unless $CurrentUser %>
|
||||
```
|
||||
|
||||
## Uncached
|
||||
|
||||
|
@ -165,9 +194,11 @@ The template tag 'uncached' can be used - it is the exact equivalent of a cached
|
|||
returns false. The key and conditionals in an uncached tag are ignored, so you can easily temporarily disable a
|
||||
particular cache block by changing just the tag, leaving the key and conditional intact.
|
||||
|
||||
:::ss
|
||||
<% uncached %>
|
||||
|
||||
```ss
|
||||
|
||||
<% uncached %>
|
||||
```
|
||||
|
||||
## Nested cache blocks
|
||||
|
||||
|
@ -179,17 +210,19 @@ portion dynamic, without having to include any member info in the page's cache k
|
|||
|
||||
An example:
|
||||
|
||||
:::ss
|
||||
<% cached $LastEdited %>
|
||||
Our wonderful site
|
||||
|
||||
<% cached $Member.ID %>
|
||||
Welcome $Member.Name
|
||||
<% end_cached %>
|
||||
|
||||
$ASlowCalculation
|
||||
<% end_cached %>
|
||||
|
||||
```ss
|
||||
|
||||
<% cached $LastEdited %>
|
||||
Our wonderful site
|
||||
|
||||
<% cached $Member.ID %>
|
||||
Welcome $Member.Name
|
||||
<% end_cached %>
|
||||
|
||||
$ASlowCalculation
|
||||
<% end_cached %>
|
||||
```
|
||||
|
||||
This will cache the entire outer section until the next time the page is edited, but will display a different welcome
|
||||
message depending on the logged in member.
|
||||
|
@ -197,16 +230,19 @@ message depending on the logged in member.
|
|||
Cache conditionals and the uncached tag also work in the same nested manner. Since Member.Name is fast to calculate, you
|
||||
could also write the last example as:
|
||||
|
||||
:::ss
|
||||
<% cached $LastEdited %>
|
||||
Our wonderful site
|
||||
|
||||
<% uncached %>
|
||||
Welcome $Member.Name
|
||||
<% end_uncached %>
|
||||
|
||||
$ASlowCalculation
|
||||
<% end_cached %>
|
||||
|
||||
```ss
|
||||
|
||||
<% cached $LastEdited %>
|
||||
Our wonderful site
|
||||
|
||||
<% uncached %>
|
||||
Welcome $Member.Name
|
||||
<% end_uncached %>
|
||||
|
||||
$ASlowCalculation
|
||||
<% end_cached %>
|
||||
```
|
||||
|
||||
<div class="warning" markdown="1">
|
||||
Currently a nested cache block can not be contained within an if or loop block. The template engine will throw an error
|
||||
|
@ -215,52 +251,48 @@ letting you know if you've done this. You can often get around this using aggreg
|
|||
|
||||
Failing example:
|
||||
|
||||
:::ss
|
||||
<% cached $LastEdited %>
|
||||
|
||||
<% loop $Children %>
|
||||
<% cached $LastEdited %>
|
||||
$Name
|
||||
<% end_cached %>
|
||||
<% end_loop %>
|
||||
|
||||
<% end_cached %>
|
||||
|
||||
```ss
|
||||
|
||||
<% cached $LastEdited %>
|
||||
|
||||
<% loop $Children %>
|
||||
<% cached $LastEdited %>
|
||||
$Name
|
||||
<% end_cached %>
|
||||
<% end_loop %>
|
||||
|
||||
<% end_cached %>
|
||||
```
|
||||
|
||||
Can be re-written as:
|
||||
|
||||
:::ss
|
||||
<% cached $LastEdited %>
|
||||
|
||||
<% cached $AllChildren.max('LastEdited') %>
|
||||
<% loop $Children %>
|
||||
$Name
|
||||
<% end_loop %>
|
||||
<% end_cached %>
|
||||
|
||||
<% end_cached %>
|
||||
|
||||
```ss
|
||||
|
||||
<% cached $LastEdited %>
|
||||
|
||||
<% cached $AllChildren.max('LastEdited') %>
|
||||
<% loop $Children %>
|
||||
$Name
|
||||
<% end_loop %>
|
||||
<% end_cached %>
|
||||
|
||||
<% end_cached %>
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
:::ss
|
||||
<% cached $LastEdited %>
|
||||
(other code)
|
||||
<% end_cached %>
|
||||
|
||||
<% loop $Children %>
|
||||
<% cached $LastEdited %>
|
||||
$Name
|
||||
<% end_cached %>
|
||||
<% end_loop %>
|
||||
|
||||
## Cache expiry
|
||||
```ss
|
||||
|
||||
The default expiry for partial caches is 10 minutes. The advantage of a short cache expiry is that if you have a problem
|
||||
with your caching logic, the window in which stale content may be shown is short. The disadvantage, particularly for
|
||||
low-traffic sites, is that cache blocks may expire before they can be utilised. If you're confident that you're caching
|
||||
logic is sound, you could increase the expiry dramatically.
|
||||
|
||||
**mysite/_config.php**
|
||||
|
||||
:::php
|
||||
// Set partial cache expiry to 7 days
|
||||
SS_Cache::set_cache_lifetime('cacheblock', 60 * 60 * 24 * 7);
|
||||
<% cached $LastEdited %>
|
||||
(other code)
|
||||
<% end_cached %>
|
||||
|
||||
<% loop $Children %>
|
||||
<% cached $LastEdited %>
|
||||
$Name
|
||||
<% end_cached %>
|
||||
<% end_loop %>
|
||||
```
|
||||
|
|
|
@ -9,7 +9,6 @@ By default, the storage mechanism chooses the most performant adapter available
|
|||
The most common caches are manifests of various resources:
|
||||
|
||||
* PHP class locations ([ClassManifest](api:SilverStripe\Core\Manifest\ClassManifest))
|
||||
* Template file locations and compiled templates ([SS_TemplateManifest](api:SS_TemplateManifest))
|
||||
* Configuration settings from YAML files ([ConfigManifest](api:ConfigManifest))
|
||||
* Language files ([i18n](api:SilverStripe\i18n\i18n))
|
||||
|
||||
|
@ -31,20 +30,25 @@ but also exposes caches following the PSR-16 interface.
|
|||
Cache objects are configured via YAML
|
||||
and SilverStripe's [dependency injection](/developer-guides/extending/injector) system.
|
||||
|
||||
:::yml
|
||||
|
||||
```yml
|
||||
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
Psr\SimpleCache\CacheInterface.myCache:
|
||||
factory: SilverStripe\Core\Cache\CacheFactory
|
||||
constructor:
|
||||
namespace: "myCache"
|
||||
```
|
||||
|
||||
Cache objects are instantiated through a [CacheFactory](SilverStripe\Core\Cache\CacheFactory),
|
||||
which determines which cache adapter is used (see "Adapters" below for details).
|
||||
This factory allows us you to globally define an adapter for all cache instances.
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
use Psr\SimpleCache\CacheInterface
|
||||
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
|
||||
```
|
||||
|
||||
Caches are namespaced, which might allow granular clearing of a particular cache without affecting others.
|
||||
In our example, the namespace is "myCache", expressed in the service name as
|
||||
|
@ -59,10 +63,12 @@ service doesn't support this. See "Invalidation" for alternative strategies.
|
|||
|
||||
Cache objects follow the [PSR-16](http://www.php-fig.org/psr/psr-16/) class interface.
|
||||
|
||||
:::php
|
||||
use Psr\SimpleCache\CacheInterface
|
||||
|
||||
```php
|
||||
use Psr\SimpleCache\CacheInterface
|
||||
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
|
||||
|
||||
|
||||
// create a new item by trying to get it from the cache
|
||||
$myValue = $cache->get('myCacheKey');
|
||||
|
||||
|
@ -73,6 +79,7 @@ Cache objects follow the [PSR-16](http://www.php-fig.org/psr/psr-16/) class inte
|
|||
if (!$cache->has('myCacheKey')) {
|
||||
// ... item does not exists in the cache
|
||||
}
|
||||
```
|
||||
|
||||
## Invalidation
|
||||
|
||||
|
@ -80,30 +87,37 @@ Caches can be invalidated in different ways. The easiest is to actively clear th
|
|||
entire cache. If the adapter supports namespaced cache clearing,
|
||||
this will only affect a subset of cache keys ("myCache" in this example):
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
use Psr\SimpleCache\CacheInterface
|
||||
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
|
||||
|
||||
// remove all items in this (namespaced) cache
|
||||
$cache->clear();
|
||||
|
||||
```
|
||||
|
||||
You can also delete a single item based on it's cache key:
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
use Psr\SimpleCache\CacheInterface
|
||||
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
|
||||
|
||||
// remove the cache item
|
||||
$cache->delete('myCacheKey');
|
||||
```
|
||||
|
||||
Individual cache items can define a lifetime, after which the cached value is marked as expired:
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
use Psr\SimpleCache\CacheInterface
|
||||
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
|
||||
|
||||
// remove the cache item
|
||||
$cache->set('myCacheKey', 'myValue', 300); // cache for 300 seconds
|
||||
```
|
||||
|
||||
If a lifetime isn't defined on the `set()` call, it'll use the adapter default.
|
||||
In order to increase the chance of your cache actually being hit,
|
||||
|
@ -112,11 +126,14 @@ You can also set your lifetime to `0`, which means they won't expire.
|
|||
Since many adapters don't have a way to actively remove expired caches,
|
||||
you need to be careful with resources here (e.g. filesystem space).
|
||||
|
||||
:::yml
|
||||
|
||||
```yml
|
||||
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
Psr\SimpleCache\CacheInterface.cacheblock:
|
||||
constructor:
|
||||
defaultLifetime: 3600
|
||||
```
|
||||
|
||||
In most cases, invalidation and expiry should be handled by your cache key.
|
||||
For example, including the `LastEdited` value when caching `DataObject` results
|
||||
|
@ -125,13 +142,15 @@ The following example caches a member's group names, and automatically
|
|||
creates a new cache key when any group is edited. Depending on the used adapter,
|
||||
old cache keys will be garbage collected as the cache fills up.
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
use Psr\SimpleCache\CacheInterface
|
||||
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
|
||||
|
||||
// Automatically changes when any group is edited
|
||||
$cacheKey = implode(['groupNames', $member->ID, Groups::get()->max('LastEdited')]);
|
||||
$cacheKey = implode(['groupNames', $member->ID, Group::get()->max('LastEdited')]);
|
||||
$cache->set($cacheKey, $member->Groups()->column('Title'));
|
||||
```
|
||||
|
||||
If `?flush=1` is requested in the URL, this will trigger a call to `flush()` on
|
||||
any classes that implement the [Flushable](/developer_guides/execution_pipeline/flushable/)
|
||||
|
@ -163,7 +182,9 @@ Example: Configure core caches to use [memcached](http://www.danga.com/memcached
|
|||
which requires the [memcached PHP extension](http://php.net/memcached),
|
||||
and takes a `MemcachedClient` instance as a constructor argument.
|
||||
|
||||
:::yml
|
||||
|
||||
```yml
|
||||
|
||||
---
|
||||
After:
|
||||
- '#corecache'
|
||||
|
@ -177,6 +198,7 @@ and takes a `MemcachedClient` instance as a constructor argument.
|
|||
class: 'SilverStripe\Core\Cache\MemcachedCacheFactory'
|
||||
constructor:
|
||||
client: '%$MemcachedClient
|
||||
```
|
||||
|
||||
## Additional Caches
|
||||
|
||||
|
|
|
@ -17,17 +17,19 @@ headers:
|
|||
## Customizing Cache Headers
|
||||
|
||||
### HTTP::set_cache_age
|
||||
|
||||
:::php
|
||||
HTTP::set_cache_age(0);
|
||||
```php
|
||||
HTTP::set_cache_age(0);
|
||||
```
|
||||
|
||||
Used to set the max-age component of the cache-control line, in seconds. Set it to 0 to disable caching; the "no-cache"
|
||||
clause in `Cache-Control` and `Pragma` will be included.
|
||||
|
||||
### HTTP::register_modification_date
|
||||
|
||||
:::php
|
||||
HTTP::register_modification_date('2014-10-10');
|
||||
|
||||
```php
|
||||
HTTP::register_modification_date('2014-10-10');
|
||||
```
|
||||
|
||||
Used to set the modification date to something more recent than the default. [DataObject::__construct](api:SilverStripe\ORM\DataObject::__construct) calls
|
||||
[HTTP::register_modification_date(](api:SilverStripe\Control\HTTP::register_modification_date() whenever a record comes from the database ensuring the newest date is present.
|
||||
|
|
|
@ -23,7 +23,9 @@ SilverStripe can request more resources through `Environment::increaseMemoryLimi
|
|||
`Environment::increaseTimeLimitTo()` functions.
|
||||
</div>
|
||||
|
||||
:::php
|
||||
public function myBigFunction() {
|
||||
Environment::increaseTimeLimitTo(400);
|
||||
}
|
||||
```php
|
||||
public function myBigFunction()
|
||||
{
|
||||
Environment::increaseTimeLimitTo(400);
|
||||
}
|
||||
```
|
|
@ -8,33 +8,18 @@ The [Member](api:SilverStripe\Security\Member) class is used to represent user a
|
|||
|
||||
## Testing For Logged In Users
|
||||
|
||||
The [Member](api:SilverStripe\Security\Member) class comes with 2 static methods for getting information about the current logged in user.
|
||||
|
||||
**Member::currentUserID()**
|
||||
|
||||
Retrieves the ID (int) of the current logged in member. Returns *0* if user is not logged in. Much lighter than the
|
||||
next method for testing if you just need to test.
|
||||
|
||||
:::php
|
||||
// Is a member logged in?
|
||||
if( Member::currentUserID() ) {
|
||||
// Yes!
|
||||
} else {
|
||||
// No!
|
||||
}
|
||||
|
||||
|
||||
**Security::getCurrentUser()**
|
||||
|
||||
Returns the full *Member* Object for the current user, returns *null* if user is not logged in.
|
||||
|
||||
:::php
|
||||
if( $member = Security::getCurrentUser() ) {
|
||||
// Work with $member
|
||||
} else {
|
||||
// Do non-member stuff
|
||||
}
|
||||
|
||||
```php
|
||||
if( $member = Security::getCurrentUser() ) {
|
||||
// Work with $member
|
||||
} else {
|
||||
// Do non-member stuff
|
||||
}
|
||||
```
|
||||
|
||||
## Subclassing
|
||||
|
||||
|
@ -45,22 +30,28 @@ This is the least desirable way of extending the [Member](api:SilverStripe\Secur
|
|||
|
||||
You can define subclasses of [Member](api:SilverStripe\Security\Member) to add extra fields or functionality to the built-in membership system.
|
||||
|
||||
:::php
|
||||
class MyMember extends Member {
|
||||
private static $db = array(
|
||||
"Age" => "Int",
|
||||
"Address" => "Text",
|
||||
);
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Security\Member;
|
||||
|
||||
class MyMember extends Member
|
||||
{
|
||||
private static $db = [
|
||||
"Age" => "Int",
|
||||
"Address" => "Text",
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To ensure that all new members are created using this class, put it in `Injector`.
|
||||
|
||||
|
||||
To ensure that all new members are created using this class, put a call to [Object::useCustomClass()](api:Object::useCustomClass()) in
|
||||
(project)/_config.php:
|
||||
|
||||
:::php
|
||||
Object::useCustomClass("Member", "MyMember");
|
||||
|
||||
Note that if you want to look this class-name up, you can call Object::getCustomClass("Member")
|
||||
```yaml
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
SilverStripe\Security\Member:
|
||||
class: My\Project\MyMember
|
||||
```
|
||||
|
||||
## Overloading getCMSFields()
|
||||
|
||||
|
@ -68,15 +59,17 @@ If you overload the built-in public function getCMSFields(), then you can change
|
|||
details in the newsletter system. This function returns a [FieldList](api:SilverStripe\Forms\FieldList) object. You should generally start by calling
|
||||
parent::getCMSFields() and manipulate the [FieldList](api:SilverStripe\Forms\FieldList) from there.
|
||||
|
||||
:::php
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
$fields->insertBefore("HTMLEmail", new TextField("Age"));
|
||||
$fields->removeByName("JobTitle");
|
||||
$fields->removeByName("Organisation");
|
||||
return $fields;
|
||||
}
|
||||
|
||||
```php
|
||||
public function getCMSFields()
|
||||
{
|
||||
$fields = parent::getCMSFields();
|
||||
$fields->insertBefore("HTMLEmail", new TextField("Age"));
|
||||
$fields->removeByName("JobTitle");
|
||||
$fields->removeByName("Organisation");
|
||||
return $fields;
|
||||
}
|
||||
```
|
||||
|
||||
## Extending Member or DataObject?
|
||||
|
||||
|
@ -93,39 +86,51 @@ Using inheritance to add extra behaviour or data fields to a member is limiting,
|
|||
class. A better way is to use role extensions to add this behaviour. Add the following to your
|
||||
`[config.yml](/developer_guides/configuration/configuration/#configuration-yaml-syntax-and-rules)`.
|
||||
|
||||
:::yml
|
||||
Member:
|
||||
extensions:
|
||||
- MyMemberExtension
|
||||
|
||||
```yml
|
||||
|
||||
Member:
|
||||
extensions:
|
||||
- MyMemberExtension
|
||||
```
|
||||
|
||||
A role extension is simply a subclass of [DataExtension](api:SilverStripe\ORM\DataExtension) that is designed to be used to add behaviour to [Member](api:SilverStripe\Security\Member).
|
||||
The roles affect the entire class - all members will get the additional behaviour. However, if you want to restrict
|
||||
things, you should add appropriate [Permission::checkMember()](api:SilverStripe\Security\Permission::checkMember()) calls to the role's methods.
|
||||
|
||||
:::php
|
||||
class MyMemberExtension extends DataExtension {
|
||||
/**
|
||||
|
||||
* Modify the field set to be displayed in the CMS detail pop-up
|
||||
*/
|
||||
public function updateCMSFields(FieldList $currentFields) {
|
||||
// Only show the additional fields on an appropriate kind of use
|
||||
if(Permission::checkMember($this->owner->ID, "VIEW_FORUM")) {
|
||||
// Edit the FieldList passed, adding or removing fields as necessary
|
||||
}
|
||||
}
|
||||
|
||||
// define additional properties
|
||||
private static $db = array();
|
||||
private static $has_one = array();
|
||||
private static $has_many = array();
|
||||
private static $many_many = array();
|
||||
private static $belongs_many_many = array();
|
||||
|
||||
public function somethingElse() {
|
||||
// You can add any other methods you like, which you can call directly on the member object.
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
|
||||
class MyMemberExtension extends DataExtension
|
||||
{
|
||||
/**
|
||||
|
||||
* Modify the field set to be displayed in the CMS detail pop-up
|
||||
*/
|
||||
public function updateCMSFields(FieldList $currentFields)
|
||||
{
|
||||
// Only show the additional fields on an appropriate kind of use
|
||||
if(Permission::checkMember($this->owner->ID, "VIEW_FORUM")) {
|
||||
// Edit the FieldList passed, adding or removing fields as necessary
|
||||
}
|
||||
}
|
||||
|
||||
// define additional properties
|
||||
private static $db = [];
|
||||
private static $has_one = [];
|
||||
private static $has_many = [];
|
||||
private static $many_many = [];
|
||||
private static $belongs_many_many = [];
|
||||
|
||||
public function somethingElse()
|
||||
{
|
||||
// You can add any other methods you like, which you can call directly on the member object.
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Saved User Logins ##
|
||||
|
||||
|
@ -153,7 +158,13 @@ reasonably be expected to be allowed to do.
|
|||
E.g.
|
||||
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Dev\BuildTask;
|
||||
|
||||
class CleanRecordsTask extends BuildTask
|
||||
{
|
||||
public function run($request)
|
||||
|
@ -166,7 +177,7 @@ E.g.
|
|||
DataRecord::get()->filter('Dirty', true)->removeAll();
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
|
|
|
@ -38,11 +38,12 @@ class PageController implements PermissionProvider
|
|||
|
||||
public function providePermissions()
|
||||
{
|
||||
return array(
|
||||
return [
|
||||
'VIEW_SITE' => 'Access the site'
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -38,11 +38,11 @@ CMS access for the first time. SilverStripe provides a default admin configurati
|
|||
and password to be configured for a single special user outside of the normal membership system.
|
||||
|
||||
It is advisable to configure this user in your `.env` file inside of the web root, as below:
|
||||
|
||||
# Configure a default username and password to access the CMS on all sites in this environment.
|
||||
SS_DEFAULT_ADMIN_USERNAME="admin"
|
||||
SS_DEFAULT_ADMIN_PASSWORD="password"
|
||||
|
||||
```
|
||||
# Configure a default username and password to access the CMS on all sites in this environment.
|
||||
SS_DEFAULT_ADMIN_USERNAME="admin"
|
||||
SS_DEFAULT_ADMIN_PASSWORD="password"
|
||||
```
|
||||
When a user logs in with these credentials, then a [Member](api:SilverStripe\Security\Member) with the Email 'admin' will be generated in
|
||||
the database, but without any password information. This means that the password can be reset or changed by simply
|
||||
updating the `.env` file.
|
||||
|
|
|
@ -25,34 +25,37 @@ must still be taken when working with literal values or table/column identifiers
|
|||
come from user input.
|
||||
|
||||
Example:
|
||||
|
||||
:::php
|
||||
$records = DB::prepared_query('SELECT * FROM "MyClass" WHERE "ID" = ?', array(3));
|
||||
$records = MyClass::get()->where(array('"ID" = ?' => 3));
|
||||
$records = MyClass::get()->where(array('"ID"' => 3));
|
||||
$records = DataObject::get_by_id('MyClass', 3);
|
||||
$records = DataObject::get_one('MyClass', array('"ID" = ?' => 3));
|
||||
$records = MyClass::get()->byID(3);
|
||||
$records = SQLSelect::create()->addWhere(array('"ID"' => 3))->execute();
|
||||
```php
|
||||
$records = DB::prepared_query('SELECT * FROM "MyClass" WHERE "ID" = ?', [3]);
|
||||
$records = MyClass::get()->where(['"ID" = ?' => 3]);
|
||||
$records = MyClass::get()->where(['"ID"' => 3]);
|
||||
$records = DataObject::get_by_id('MyClass', 3);
|
||||
$records = DataObject::get_one('MyClass', ['"ID" = ?' => 3]);
|
||||
$records = MyClass::get()->byID(3);
|
||||
$records = SQLSelect::create()->addWhere(['"ID"' => 3])->execute();
|
||||
|
||||
```
|
||||
|
||||
Parameterised updates and inserts are also supported, but the syntax is a little different
|
||||
|
||||
:::php
|
||||
SQLInsert::create('"MyClass"')
|
||||
->assign('"Name"', 'Daniel')
|
||||
->addAssignments(array(
|
||||
'"Position"' => 'Accountant',
|
||||
'"Age"' => array(
|
||||
'GREATEST(0,?,?)' => array(24, 28)
|
||||
)
|
||||
))
|
||||
->assignSQL('"Created"', 'NOW()')
|
||||
->execute();
|
||||
DB::prepared_query(
|
||||
'INSERT INTO "MyClass" ("Name", "Position", "Age", "Created") VALUES(?, ?, GREATEST(0,?,?), NOW())'
|
||||
array('Daniel', 'Accountant', 24, 28)
|
||||
);
|
||||
|
||||
```php
|
||||
SQLInsert::create('"MyClass"')
|
||||
->assign('"Name"', 'Daniel')
|
||||
->addAssignments([
|
||||
'"Position"' => 'Accountant',
|
||||
'"Age"' => [
|
||||
'GREATEST(0,?,?)' => [24, 28]
|
||||
]
|
||||
])
|
||||
->assignSQL('"Created"', 'NOW()')
|
||||
->execute();
|
||||
DB::prepared_query(
|
||||
'INSERT INTO "MyClass" ("Name", "Position", "Age", "Created") VALUES(?, ?, GREATEST(0,?,?), NOW())'
|
||||
['Daniel', 'Accountant', 24, 28]
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
### Automatic escaping
|
||||
|
||||
|
@ -76,16 +79,17 @@ Data is not escaped when writing to object-properties, as inserts and updates ar
|
|||
handled via prepared statements.
|
||||
|
||||
Example:
|
||||
|
||||
:::php
|
||||
// automatically escaped/quoted
|
||||
$members = Member::get()->filter('Name', $_GET['name']);
|
||||
// automatically escaped/quoted
|
||||
$members = Member::get()->filter(array('Name' => $_GET['name']));
|
||||
// parameterised condition
|
||||
$members = Member::get()->where(array('"Name" = ?' => $_GET['name']));
|
||||
// needs to be escaped and quoted manually (note raw2sql called with the $quote parameter set to true)
|
||||
$members = Member::get()->where(sprintf('"Name" = %s', Convert::raw2sql($_GET['name'], true)));
|
||||
```php
|
||||
// automatically escaped/quoted
|
||||
$members = Member::get()->filter('Name', $_GET['name']);
|
||||
// automatically escaped/quoted
|
||||
$members = Member::get()->filter(['Name' => $_GET['name']]);
|
||||
// parameterised condition
|
||||
$members = Member::get()->where(['"Name" = ?' => $_GET['name']]);
|
||||
// needs to be escaped and quoted manually (note raw2sql called with the $quote parameter set to true)
|
||||
$members = Member::get()->where(sprintf('"Name" = %s', Convert::raw2sql($_GET['name'], true)));
|
||||
|
||||
```
|
||||
|
||||
<div class="warning" markdown='1'>
|
||||
It is NOT good practice to "be sure" and convert the data passed to the functions above manually. This might
|
||||
|
@ -108,53 +112,77 @@ and [datamodel](/developer_guides/model) for ways to parameterise, cast, and con
|
|||
|
||||
Example:
|
||||
|
||||
:::php
|
||||
class MyForm extends Form {
|
||||
public function save($RAW_data, $form) {
|
||||
// Pass true as the second parameter of raw2sql to quote the value safely
|
||||
$SQL_data = Convert::raw2sql($RAW_data, true); // works recursively on an array
|
||||
$objs = Player::get()->where("Name = " . $SQL_data['name']);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Forms\Form;
|
||||
|
||||
class MyForm extends Form
|
||||
{
|
||||
public function save($RAW_data, $form)
|
||||
{
|
||||
// Pass true as the second parameter of raw2sql to quote the value safely
|
||||
$SQL_data = Convert::raw2sql($RAW_data, true); // works recursively on an array
|
||||
$objs = Player::get()->where("Name = " . $SQL_data['name']);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
* `FormField->Value()`
|
||||
* URLParams passed to a Controller-method
|
||||
|
||||
Example:
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
private static $allowed_actions = array('myurlaction');
|
||||
public function myurlaction($RAW_urlParams) {
|
||||
// Pass true as the second parameter of raw2sql to quote the value safely
|
||||
$SQL_urlParams = Convert::raw2sql($RAW_urlParams, true); // works recursively on an array
|
||||
$objs = Player::get()->where("Name = " . $SQL_data['OtherID']);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller
|
||||
{
|
||||
private static $allowed_actions = ['myurlaction'];
|
||||
public function myurlaction($RAW_urlParams)
|
||||
{
|
||||
// Pass true as the second parameter of raw2sql to quote the value safely
|
||||
$SQL_urlParams = Convert::raw2sql($RAW_urlParams, true); // works recursively on an array
|
||||
$objs = Player::get()->where("Name = " . $SQL_data['OtherID']);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
As a rule of thumb, you should escape your data **as close to querying as possible**
|
||||
(or preferably, use parameterised queries). This means if you've got a chain of functions
|
||||
passing data through, escaping should happen at the end of the chain.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
/**
|
||||
* @param array $RAW_data All names in an indexed array (not SQL-safe)
|
||||
*/
|
||||
public function saveAllNames($RAW_data) {
|
||||
// $SQL_data = Convert::raw2sql($RAW_data); // premature escaping
|
||||
foreach($RAW_data as $item) $this->saveName($item);
|
||||
}
|
||||
|
||||
public function saveName($RAW_name) {
|
||||
$SQL_name = Convert::raw2sql($RAW_name, true);
|
||||
DB::query("UPDATE Player SET Name = {$SQL_name}");
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller
|
||||
{
|
||||
/**
|
||||
* @param array $RAW_data All names in an indexed array (not SQL-safe)
|
||||
*/
|
||||
public function saveAllNames($RAW_data)
|
||||
{
|
||||
// $SQL_data = Convert::raw2sql($RAW_data); // premature escaping
|
||||
foreach($RAW_data as $item) $this->saveName($item);
|
||||
}
|
||||
|
||||
public function saveName($RAW_name)
|
||||
{
|
||||
$SQL_name = Convert::raw2sql($RAW_name, true);
|
||||
DB::query("UPDATE Player SET Name = {$SQL_name}");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This might not be applicable in all cases - especially if you are building an API thats likely to be customised. If
|
||||
you're passing unescaped data, make sure to be explicit about it by writing *phpdoc*-documentation and *prefixing* your
|
||||
|
@ -188,9 +216,9 @@ stripped out
|
|||
|
||||
To enable filtering, set the HtmlEditorField::$sanitise_server_side [configuration](/developer_guides/configuration/configuration) property to
|
||||
true, e.g.
|
||||
|
||||
HtmlEditorField::config()->sanitise_server_side = true
|
||||
|
||||
```
|
||||
HtmlEditorField::config()->sanitise_server_side = true
|
||||
```
|
||||
The built in sanitiser enforces the TinyMCE whitelist rules on the server side, and is sufficient to eliminate the
|
||||
most common XSS vectors.
|
||||
|
||||
|
@ -218,23 +246,29 @@ object-properties by [casting](/developer_guides/model/data_types_and_casting) i
|
|||
|
||||
PHP:
|
||||
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
private static $db = array(
|
||||
'MyEscapedValue' => 'Text', // Example value: <b>not bold</b>
|
||||
'MyUnescapedValue' => 'HTMLText' // Example value: <b>bold</b>
|
||||
);
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
private static $db = [
|
||||
'MyEscapedValue' => 'Text', // Example value: <b>not bold</b>
|
||||
'MyUnescapedValue' => 'HTMLText' // Example value: <b>bold</b>
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Template:
|
||||
|
||||
:::php
|
||||
<ul>
|
||||
<li>$MyEscapedValue</li> // output: <b>not bold<b>
|
||||
<li>$MyUnescapedValue</li> // output: <b>bold</b>
|
||||
</ul>
|
||||
|
||||
```php
|
||||
<ul>
|
||||
<li>$MyEscapedValue</li> // output: <b>not bold<b>
|
||||
<li>$MyUnescapedValue</li> // output: <b>bold</b>
|
||||
</ul>
|
||||
```
|
||||
|
||||
The example below assumes that data wasn't properly filtered when saving to the database, but are escaped before
|
||||
outputting through SSViewer.
|
||||
|
@ -246,15 +280,16 @@ You can force escaping on a casted value/object by using an [escape type](/devel
|
|||
|
||||
Template (see above):
|
||||
|
||||
:::php
|
||||
<ul>
|
||||
// output: <a href="#" title="foo & &#quot;bar"">foo & "bar"</a>
|
||||
<li><a href="#" title="$Title.ATT">$Title</a></li>
|
||||
<li>$MyEscapedValue</li> // output: <b>not bold<b>
|
||||
<li>$MyUnescapedValue</li> // output: <b>bold</b>
|
||||
<li>$MyUnescapedValue.XML</li> // output: <b>bold<b>
|
||||
</ul>
|
||||
|
||||
```php
|
||||
<ul>
|
||||
// output: <a href="#" title="foo & &#quot;bar"">foo & "bar"</a>
|
||||
<li><a href="#" title="$Title.ATT">$Title</a></li>
|
||||
<li>$MyEscapedValue</li> // output: <b>not bold<b>
|
||||
<li>$MyUnescapedValue</li> // output: <b>bold</b>
|
||||
<li>$MyUnescapedValue.XML</li> // output: <b>bold<b>
|
||||
</ul>
|
||||
```
|
||||
|
||||
### Escaping custom attributes and getters
|
||||
|
||||
|
@ -263,31 +298,38 @@ static *$casting* array. Caution: Casting only applies when using values in a te
|
|||
|
||||
PHP:
|
||||
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
public $Title = '<b>not bold</b>'; // will be escaped due to Text casting
|
||||
|
||||
$casting = array(
|
||||
"Title" => "Text", // forcing a casting
|
||||
'TitleWithHTMLSuffix' => 'HTMLText' // optional, as HTMLText is the default casting
|
||||
);
|
||||
|
||||
public function TitleWithHTMLSuffix($suffix) {
|
||||
// $this->Title is not casted in PHP
|
||||
return $this->Title . '<small>(' . $suffix. ')</small>';
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
public $Title = '<b>not bold</b>'; // will be escaped due to Text casting
|
||||
|
||||
$casting = [
|
||||
"Title" => "Text", // forcing a casting
|
||||
'TitleWithHTMLSuffix' => 'HTMLText' // optional, as HTMLText is the default casting
|
||||
];
|
||||
|
||||
public function TitleWithHTMLSuffix($suffix)
|
||||
{
|
||||
// $this->Title is not casted in PHP
|
||||
return $this->Title . '<small>(' . $suffix. ')</small>';
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Template:
|
||||
|
||||
:::php
|
||||
<ul>
|
||||
<li>$Title</li> // output: <b>not bold<b>
|
||||
<li>$Title.RAW</li> // output: <b>not bold</b>
|
||||
<li>$TitleWithHTMLSuffix</li> // output: <b>not bold</b>: <small>(...)</small>
|
||||
</ul>
|
||||
|
||||
```php
|
||||
<ul>
|
||||
<li>$Title</li> // output: <b>not bold<b>
|
||||
<li>$Title.RAW</li> // output: <b>not bold</b>
|
||||
<li>$TitleWithHTMLSuffix</li> // output: <b>not bold</b>: <small>(...)</small>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Note: Avoid generating HTML by string concatenation in PHP wherever possible to minimize risk and separate your
|
||||
presentation from business logic.
|
||||
|
@ -302,24 +344,34 @@ also used by *XML* and *ATT* in template code).
|
|||
|
||||
PHP:
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
private static $allowed_actions = array('search');
|
||||
public function search($request) {
|
||||
$htmlTitle = '<p>Your results for:' . Convert::raw2xml($request->getVar('Query')) . '</p>';
|
||||
return $this->customise(array(
|
||||
'Query' => Text::create($request->getVar('Query')),
|
||||
'HTMLTitle' => HTMLText::create($htmlTitle)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\ORM\FieldType\DBText;
|
||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||
|
||||
class MyController extends Controller
|
||||
{
|
||||
private static $allowed_actions = ['search'];
|
||||
public function search($request)
|
||||
{
|
||||
$htmlTitle = '<p>Your results for:' . Convert::raw2xml($request->getVar('Query')) . '</p>';
|
||||
return $this->customise([
|
||||
'Query' => DBText::create($request->getVar('Query')),
|
||||
'HTMLTitle' => DBHTMLText::create($htmlTitle)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Template:
|
||||
|
||||
:::php
|
||||
<h2 title="Searching for $Query.ATT">$HTMLTitle</h2>
|
||||
|
||||
```php
|
||||
<h2 title="Searching for $Query.ATT">$HTMLTitle</h2>
|
||||
```
|
||||
|
||||
Whenever you insert a variable into an HTML attribute within a template, use $VarName.ATT, no not $VarName.
|
||||
|
||||
|
@ -332,24 +384,32 @@ user data, not *Convert::raw2att()*. Use raw ampersands in your URL, and cast t
|
|||
|
||||
PHP:
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
private static $allowed_actions = array('search');
|
||||
public function search($request) {
|
||||
$rssRelativeLink = "/rss?Query=" . urlencode($_REQUEST['query']) . "&sortOrder=asc";
|
||||
$rssLink = Controller::join_links($this->Link(), $rssRelativeLink);
|
||||
return $this->customise(array(
|
||||
"RSSLink" => Text::create($rssLink),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\ORM\FieldType\DBText;
|
||||
|
||||
class MyController extends Controller
|
||||
{
|
||||
private static $allowed_actions = ['search'];
|
||||
public function search($request)
|
||||
{
|
||||
$rssRelativeLink = "/rss?Query=" . urlencode($_REQUEST['query']) . "&sortOrder=asc";
|
||||
$rssLink = Controller::join_links($this->Link(), $rssRelativeLink);
|
||||
return $this->customise([
|
||||
"RSSLink" => DBText::create($rssLink),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Template:
|
||||
|
||||
:::php
|
||||
<a href="$RSSLink.ATT">RSS feed</a>
|
||||
|
||||
```php
|
||||
<a href="$RSSLink.ATT">RSS feed</a>
|
||||
```
|
||||
|
||||
Some rules of thumb:
|
||||
|
||||
|
@ -395,22 +455,24 @@ passed, such as *mysite.com/home/add/dfsdfdsfd*, then it returns 0.
|
|||
|
||||
Below is an example with different ways you would use this casting technique:
|
||||
|
||||
:::php
|
||||
public function CaseStudies() {
|
||||
|
||||
// cast an ID from URL parameters e.g. (mysite.com/home/action/ID)
|
||||
$anotherID = (int)Director::urlParam['ID'];
|
||||
|
||||
// perform a calculation, the prerequisite being $anotherID must be an integer
|
||||
$calc = $anotherID + (5 - 2) / 2;
|
||||
|
||||
// cast the 'category' GET variable as an integer
|
||||
$categoryID = (int)$_GET['category'];
|
||||
|
||||
// perform a byID(), which ensures the ID is an integer before querying
|
||||
return CaseStudy::get()->byID($categoryID);
|
||||
}
|
||||
|
||||
```php
|
||||
public function CaseStudies()
|
||||
{
|
||||
|
||||
// cast an ID from URL parameters e.g. (mysite.com/home/action/ID)
|
||||
$anotherID = (int)Director::urlParam['ID'];
|
||||
|
||||
// perform a calculation, the prerequisite being $anotherID must be an integer
|
||||
$calc = $anotherID + (5 - 2) / 2;
|
||||
|
||||
// cast the 'category' GET variable as an integer
|
||||
$categoryID = (int)$_GET['category'];
|
||||
|
||||
// perform a byID(), which ensures the ID is an integer before querying
|
||||
return CaseStudy::get()->byID($categoryID);
|
||||
}
|
||||
```
|
||||
|
||||
The same technique can be employed anywhere in your PHP code you know something must be of a certain type. A list of PHP
|
||||
cast types can be found here:
|
||||
|
@ -441,13 +503,13 @@ with a `.yml` or `.yaml` extension through the default web server rewriting rule
|
|||
If you need users to access files with this extension,
|
||||
you can bypass the rules for a specific directory.
|
||||
Here's an example for a `.htaccess` file used by the Apache web server:
|
||||
```
|
||||
<Files *.yml>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Files>
|
||||
|
||||
<Files *.yml>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Files>
|
||||
|
||||
|
||||
```
|
||||
### User uploaded files
|
||||
|
||||
Certain file types are by default excluded from user upload. html, xhtml, htm, and xml files may have embedded,
|
||||
|
@ -491,12 +553,15 @@ So in addition to storing the password in a secure fashion,
|
|||
you can also enforce specific password policies by configuring
|
||||
a [PasswordValidator](api:SilverStripe\Security\PasswordValidator):
|
||||
|
||||
:::php
|
||||
$validator = new PasswordValidator();
|
||||
$validator->minLength(7);
|
||||
$validator->checkHistoricalPasswords(6);
|
||||
$validator->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation"));
|
||||
Member::set_password_validator($validator);
|
||||
|
||||
```php
|
||||
$validator = new PasswordValidator();
|
||||
$validator->minLength(7);
|
||||
$validator->checkHistoricalPasswords(6);
|
||||
$validator->characterStrength(3, ["lowercase", "uppercase", "digits", "punctuation"]);
|
||||
Member::set_password_validator($validator);
|
||||
|
||||
```
|
||||
|
||||
In addition, you can tighten password security with the following configuration settings:
|
||||
|
||||
|
@ -518,14 +583,19 @@ included in HTML "frame" or "iframe" elements, and thereby prevent the most comm
|
|||
attack vector. This is done through a HTTP header, which is usually added in your
|
||||
controller's `init()` method:
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
public function init() {
|
||||
parent::init();
|
||||
$this->getResponse()->addHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller
|
||||
{
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
$this->getResponse()->addHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is a recommended option to secure any controller which displays
|
||||
or submits sensitive user input, and is enabled by default in all CMS controllers,
|
||||
|
@ -537,9 +607,9 @@ To prevent a forged hostname appearing being used by the application, SilverStri
|
|||
allows the configure of a whitelist of hosts that are allowed to access the system. By defining
|
||||
this whitelist in your `.env` file, any request presenting a `Host` header that is
|
||||
_not_ in this list will be blocked with a HTTP 400 error:
|
||||
|
||||
SS_ALLOWED_HOSTS="www.mysite.com,mysite.com,subdomain.mysite.com"
|
||||
|
||||
```
|
||||
SS_ALLOWED_HOSTS="www.mysite.com,mysite.com,subdomain.mysite.com"
|
||||
```
|
||||
Please note that if this configuration is defined, you _must_ include _all_ subdomains (eg www.)
|
||||
that will be accessing the site.
|
||||
|
||||
|
@ -554,23 +624,25 @@ into visiting external sites.
|
|||
|
||||
In order to prevent this kind of attack, it's necessary to whitelist trusted proxy
|
||||
server IPs using the SS_TRUSTED_PROXY_IPS define in your `.env`.
|
||||
|
||||
SS_TRUSTED_PROXY_IPS="127.0.0.1,192.168.0.1"
|
||||
|
||||
```
|
||||
SS_TRUSTED_PROXY_IPS="127.0.0.1,192.168.0.1"
|
||||
```
|
||||
If you wish to change the headers that are used to find the proxy information, you should reconfigure the
|
||||
TrustedProxyMiddleware service:
|
||||
|
||||
:::yml
|
||||
|
||||
```yml
|
||||
|
||||
SilverStripe\Control\TrustedProxyMiddleware:
|
||||
properties:
|
||||
ProxyHostHeaders: X-Forwarded-Host
|
||||
ProxySchemeHeaders: X-Forwarded-Protocol
|
||||
ProxyIPHeaders: X-Forwarded-Ip
|
||||
|
||||
|
||||
SS_TRUSTED_PROXY_HOST_HEADER="HTTP_X_FORWARDED_HOST"
|
||||
SS_TRUSTED_PROXY_IP_HEADER="HTTP_X_FORWARDED_FOR"
|
||||
SS_TRUSTED_PROXY_PROTOCOL_HEADER="HTTP_X_FORWARDED_PROTOCOL"
|
||||
SS_TRUSTED_PROXY_HOST_HEADER="HTTP_X_FORWARDED_HOST"
|
||||
SS_TRUSTED_PROXY_IP_HEADER="HTTP_X_FORWARDED_FOR"
|
||||
SS_TRUSTED_PROXY_PROTOCOL_HEADER="HTTP_X_FORWARDED_PROTOCOL"
|
||||
```
|
||||
|
||||
At the same time, you'll also need to define which headers you trust from these proxy IPs. Since there are multiple ways through which proxies can pass through HTTP information on the original hostname, IP and protocol, these values need to be adjusted for your specific proxy. The header names match their equivalent `$_SERVER` values.
|
||||
|
||||
|
@ -582,14 +654,14 @@ This behaviour is enabled whenever `SS_TRUSTED_PROXY_IPS` is defined, or if the
|
|||
`BlockUntrustedIPs` environment variable is declared. It is advisable to include the
|
||||
following in your .htaccess to ensure this behaviour is activated.
|
||||
|
||||
|
||||
<IfModule mod_env.c>
|
||||
# Ensure that X-Forwarded-Host is only allowed to determine the request
|
||||
# hostname for servers ips defined by SS_TRUSTED_PROXY_IPS in your .env
|
||||
# Note that in a future release this setting will be always on.
|
||||
SetEnv BlockUntrustedIPs true
|
||||
</IfModule>
|
||||
|
||||
```
|
||||
<IfModule mod_env.c>
|
||||
# Ensure that X-Forwarded-Host is only allowed to determine the request
|
||||
# hostname for servers ips defined by SS_TRUSTED_PROXY_IPS in your .env
|
||||
# Note that in a future release this setting will be always on.
|
||||
SetEnv BlockUntrustedIPs true
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
In a future release this behaviour will be changed to be on by default, and this environment
|
||||
variable will be no longer necessary, thus it will be necessary to always set
|
||||
|
@ -599,12 +671,11 @@ variable will be no longer necessary, thus it will be necessary to always set
|
|||
|
||||
SilverStripe recommends the use of TLS(HTTPS) for your application, and you can easily force the use through the
|
||||
director function `forceSSL()`
|
||||
|
||||
:::php
|
||||
|
||||
if (!Director::isDev()) {
|
||||
Director::forceSSL();
|
||||
}
|
||||
```php
|
||||
if (!Director::isDev()) {
|
||||
Director::forceSSL();
|
||||
}
|
||||
```
|
||||
|
||||
Forcing HTTPS so requires a certificate to be purchased or obtained through a vendor such as
|
||||
[lets encrypt](https://letsencrypt.org/) and configured on your web server.
|
||||
|
@ -612,10 +683,11 @@ Forcing HTTPS so requires a certificate to be purchased or obtained through a ve
|
|||
We also want to ensure cookies are not shared between secure and non-secure sessions, so we must tell SilverStripe to
|
||||
use a [secure session](https://docs.silverstripe.org/en/3/developer_guides/cookies_and_sessions/sessions/#secure-session-cookie).
|
||||
To do this, you may set the `cookie_secure` parameter to `true` in your `config.yml` for `Session`
|
||||
|
||||
:::yml
|
||||
Session:
|
||||
cookie_secure: true
|
||||
```yml
|
||||
|
||||
Session:
|
||||
cookie_secure: true
|
||||
```
|
||||
|
||||
For other cookies set by your application we should also ensure the users are provided with secure cookies by setting
|
||||
the "Secure" and "HTTPOnly" flags. These flags prevent them from being stolen by an attacker through javascript.
|
||||
|
@ -627,13 +699,12 @@ clear text and can be intercepted and stolen by an attacker who is listening on
|
|||
- The `HTTPOnly` flag lets the browser know whether or not a cookie should be accessible by client-side JavaScript
|
||||
code. It is best practice to set this flag unless the application is known to use JavaScript to access these cookies
|
||||
as this prevents an attacker who achieves cross-site scripting from accessing these cookies.
|
||||
|
||||
|
||||
:::php
|
||||
|
||||
Cookie::set('cookie-name', 'chocolate-chip', $expiry = 30, $path = null, $domain = null, $secure = true,
|
||||
$httpOnly = false
|
||||
);
|
||||
```php
|
||||
|
||||
Cookie::set('cookie-name', 'chocolate-chip', $expiry = 30, $path = null, $domain = null, $secure = true,
|
||||
$httpOnly = false
|
||||
);
|
||||
```
|
||||
|
||||
## Security Headers
|
||||
|
||||
|
@ -656,27 +727,31 @@ For sensitive pages, such as members areas, or places where sensitive informatio
|
|||
and `Date: <current date>` will ensure that sensitive content is not stored locally or able to be retrieved by
|
||||
unauthorised local persons. SilverStripe adds the current date for every request, and we can add the other cache
|
||||
headers to the request for our secure controllers:
|
||||
|
||||
```php
|
||||
use SilverStripe\Control\HTTP;
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MySecureController extends Controller
|
||||
{
|
||||
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
|
||||
// Add cache headers to ensure sensitive content isn't cached.
|
||||
$this->response->addHeader('Cache-Control', 'max-age=0, must-revalidate, no-transform');
|
||||
$this->response->addHeader('Pragma', 'no-cache'); // for HTTP 1.0 support
|
||||
|
||||
:::php
|
||||
HTTP::set_cache_age(0);
|
||||
HTTP::add_cache_headers($this->response);
|
||||
|
||||
// Add HSTS header to force TLS for document content
|
||||
$this->response->addHeader('Strict-Transport-Security', 'max-age=86400; includeSubDomains');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
class MySecureController extends Controller {
|
||||
|
||||
public function init() {
|
||||
parent::init();
|
||||
|
||||
// Add cache headers to ensure sensitive content isn't cached.
|
||||
$this->response->addHeader('Cache-Control', 'max-age=0, must-revalidate, no-transform');
|
||||
$this->response->addHeader('Pragma', 'no-cache'); // for HTTP 1.0 support
|
||||
|
||||
HTTP::set_cache_age(0);
|
||||
HTTP::add_cache_headers($this->response);
|
||||
|
||||
// Add HSTS header to force TLS for document content
|
||||
$this->response->addHeader('Strict-Transport-Security', 'max-age=86400; includeSubDomains');
|
||||
}
|
||||
}
|
||||
|
||||
## Related
|
||||
|
||||
* [http://silverstripe.org/security-releases/](http://silverstripe.org/security-releases/)
|
||||
|
|
|
@ -22,9 +22,11 @@ SilverStripe\Core\Injector\Injector:
|
|||
|
||||
### Sending plain text only
|
||||
|
||||
:::php
|
||||
$email = new Email($from, $to, $subject, $body);
|
||||
$email->sendPlain();
|
||||
|
||||
```php
|
||||
$email = new Email($from, $to, $subject, $body);
|
||||
$email->sendPlain();
|
||||
```
|
||||
|
||||
### Sending combined HTML and plain text
|
||||
|
||||
|
@ -32,9 +34,11 @@ By default, emails are sent in both HTML and Plaintext format. A plaintext repre
|
|||
from the system by stripping HTML markup, or transforming it where possible (e.g. `<strong>text</strong>` is converted
|
||||
to `*text*`).
|
||||
|
||||
:::php
|
||||
$email = new Email($from, $to, $subject, $body);
|
||||
$email->send();
|
||||
|
||||
```php
|
||||
$email = new Email($from, $to, $subject, $body);
|
||||
$email->send();
|
||||
```
|
||||
|
||||
<div class="info" markdown="1">
|
||||
The default HTML template for emails is named `GenericEmail` and is located in `framework/templates/SilverStripe/Email/`.
|
||||
|
@ -50,19 +54,22 @@ email object additional information using the `setData` and `addData` methods.
|
|||
|
||||
**mysite/templates/Email/MyCustomEmail.ss**
|
||||
|
||||
:::ss
|
||||
<h1>Hi $Member.FirstName</h1>
|
||||
<p>You can go to $Link.</p>
|
||||
|
||||
```ss
|
||||
|
||||
<h1>Hi $Member.FirstName</h1>
|
||||
<p>You can go to $Link.</p>
|
||||
```
|
||||
|
||||
The PHP Logic..
|
||||
|
||||
```php
|
||||
$email = SilverStripe\Control\Email\Email::create()
|
||||
->setHTMLTemplate('Email\\MyCustomEmail')
|
||||
->setData(array(
|
||||
->setData([
|
||||
'Member' => Security::getCurrentUser(),
|
||||
'Link'=> $link,
|
||||
))
|
||||
])
|
||||
->setFrom($from)
|
||||
->setTo($to)
|
||||
->setSubject($subject);
|
||||
|
@ -72,6 +79,7 @@ if ($email->send()) {
|
|||
} else {
|
||||
// there may have been 1 or more failures
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
|
@ -97,10 +105,13 @@ You can set the default sender address of emails through the `Email.admin_email`
|
|||
|
||||
**mysite/_config/app.yml**
|
||||
|
||||
:::yaml
|
||||
SilverStripe\Control\Email\Email:
|
||||
admin_email: support@silverstripe.org
|
||||
|
||||
```yaml
|
||||
|
||||
SilverStripe\Control\Email\Email:
|
||||
admin_email: support@silverstripe.org
|
||||
|
||||
```
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
Remember, setting a `from` address that doesn't come from your domain (such as the users email) will likely see your
|
||||
|
@ -122,33 +133,37 @@ Configuration of those properties looks like the following:
|
|||
|
||||
**mysite/_config.php**
|
||||
|
||||
:::php
|
||||
if(Director::isLive()) {
|
||||
Config::inst()->update('Email', 'bcc_all_emails_to', "client@example.com");
|
||||
} else {
|
||||
Config::inst()->update('Email', 'send_all_emails_to', "developer@example.com");
|
||||
}
|
||||
|
||||
```php
|
||||
if(Director::isLive()) {
|
||||
Config::inst()->update('Email', 'bcc_all_emails_to', "client@example.com");
|
||||
} else {
|
||||
Config::inst()->update('Email', 'send_all_emails_to', "developer@example.com");
|
||||
}
|
||||
```
|
||||
|
||||
### Setting custom "Reply To" email address.
|
||||
|
||||
For email messages that should have an email address which is replied to that actually differs from the original "from"
|
||||
email, do the following. This is encouraged especially when the domain responsible for sending the message isn't
|
||||
necessarily the same which should be used for return correspondence and should help prevent your message from being
|
||||
marked as spam.
|
||||
|
||||
:::php
|
||||
$email = new Email(..);
|
||||
$email->setReplyTo('me@address.com');
|
||||
marked as spam.
|
||||
```php
|
||||
$email = new Email(..);
|
||||
$email->setReplyTo('me@address.com');
|
||||
```
|
||||
|
||||
### Setting Custom Headers
|
||||
|
||||
For email headers which do not have getters or setters (like setTo(), setFrom()) you can manipulate the underlying
|
||||
`Swift_Message` that we provide a wrapper for.
|
||||
|
||||
:::php
|
||||
$email = new Email(...);
|
||||
$email->getSwiftMessage()->getHeaders()->addTextHeader('HeaderName', 'HeaderValue');
|
||||
..
|
||||
|
||||
```php
|
||||
$email = new Email(...);
|
||||
$email->getSwiftMessage()->getHeaders()->addTextHeader('HeaderName', 'HeaderValue');
|
||||
..
|
||||
```
|
||||
|
||||
<div class="info" markdown="1">
|
||||
See this [Wikipedia](http://en.wikipedia.org/wiki/E-mail#Message_header) entry for a list of header names.
|
||||
|
|
|
@ -27,16 +27,18 @@ Feature overview:
|
|||
You can use the CsvBulkLoader without subclassing or other customizations, if the column names
|
||||
in your CSV file match `$db` properties in your dataobject. E.g. a simple import for the
|
||||
[Member](api:SilverStripe\Security\Member) class could have this data in a file:
|
||||
|
||||
FirstName,LastName,Email
|
||||
Donald,Duck,donald@disney.com
|
||||
Daisy,Duck,daisy@disney.com
|
||||
|
||||
```
|
||||
FirstName,LastName,Email
|
||||
Donald,Duck,donald@disney.com
|
||||
Daisy,Duck,daisy@disney.com
|
||||
```
|
||||
The loader would be triggered through the `load()` method:
|
||||
|
||||
:::php
|
||||
$loader = new CsvBulkLoader('Member');
|
||||
$result = $loader->load('<my-file-path>');
|
||||
|
||||
```php
|
||||
$loader = new CsvBulkLoader('Member');
|
||||
$result = $loader->load('<my-file-path>');
|
||||
```
|
||||
|
||||
By the way, you can import [Member](api:SilverStripe\Security\Member) and [Group](api:SilverStripe\Security\Group) data through `http://localhost/admin/security`
|
||||
interface out of the box.
|
||||
|
@ -45,18 +47,23 @@ interface out of the box.
|
|||
|
||||
The simplest way to use [CsvBulkLoader](api:SilverStripe\Dev\CsvBulkLoader) is through a [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) interface - you get an upload form out of the box.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class PlayerAdmin extends ModelAdmin {
|
||||
private static $managed_models = array(
|
||||
'Player'
|
||||
);
|
||||
private static $model_importers = array(
|
||||
'Player' => 'CsvBulkLoader',
|
||||
);
|
||||
private static $url_segment = 'players';
|
||||
}
|
||||
?>
|
||||
|
||||
```php
|
||||
use SilverStripe\Admin\ModelAdmin;
|
||||
|
||||
class PlayerAdmin extends ModelAdmin
|
||||
{
|
||||
private static $managed_models = [
|
||||
'Player'
|
||||
];
|
||||
private static $model_importers = [
|
||||
'Player' => 'CsvBulkLoader',
|
||||
];
|
||||
private static $url_segment = 'players';
|
||||
}
|
||||
?>
|
||||
|
||||
```
|
||||
|
||||
The new admin interface will be available under `http://localhost/admin/players`, the import form is located
|
||||
below the search form on the left.
|
||||
|
@ -68,46 +75,60 @@ Let's create a simple upload form (which is used for `MyDataObject` instances).
|
|||
You'll need to add a route to your controller to make it accessible via URL
|
||||
(see [director](/reference/director)).
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class MyController extends Controller {
|
||||
|
||||
private static $allowed_actions = array('Form');
|
||||
```php
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\FileField;
|
||||
use SilverStripe\Forms\FormAction;
|
||||
use SilverStripe\Forms\RequiredFields;
|
||||
use SilverStripe\Dev\CsvBulkLoader;
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
protected $template = "BlankPage";
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
public function Link($action = null) {
|
||||
return Controller::join_links('MyController', $action);
|
||||
}
|
||||
private static $allowed_actions = ['Form'];
|
||||
|
||||
public function Form() {
|
||||
$form = new Form(
|
||||
$this,
|
||||
'Form',
|
||||
new FieldList(
|
||||
new FileField('CsvFile', false)
|
||||
),
|
||||
new FieldList(
|
||||
new FormAction('doUpload', 'Upload')
|
||||
),
|
||||
new RequiredFields()
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
protected $template = "BlankPage";
|
||||
|
||||
public function doUpload($data, $form) {
|
||||
$loader = new CsvBulkLoader('MyDataObject');
|
||||
$results = $loader->load($_FILES['CsvFile']['tmp_name']);
|
||||
$messages = array();
|
||||
if($results->CreatedCount()) $messages[] = sprintf('Imported %d items', $results->CreatedCount());
|
||||
if($results->UpdatedCount()) $messages[] = sprintf('Updated %d items', $results->UpdatedCount());
|
||||
if($results->DeletedCount()) $messages[] = sprintf('Deleted %d items', $results->DeletedCount());
|
||||
if(!$messages) $messages[] = 'No changes';
|
||||
$form->sessionMessage(implode(', ', $messages), 'good');
|
||||
public function Link($action = null)
|
||||
{
|
||||
return Controller::join_links('MyController', $action);
|
||||
}
|
||||
|
||||
return $this->redirectBack();
|
||||
}
|
||||
}
|
||||
public function Form()
|
||||
{
|
||||
$form = new Form(
|
||||
$this,
|
||||
'Form',
|
||||
new FieldList(
|
||||
new FileField('CsvFile', false)
|
||||
),
|
||||
new FieldList(
|
||||
new FormAction('doUpload', 'Upload')
|
||||
),
|
||||
new RequiredFields()
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function doUpload($data, $form)
|
||||
{
|
||||
$loader = new CsvBulkLoader('MyDataObject');
|
||||
$results = $loader->load($_FILES['CsvFile']['tmp_name']);
|
||||
$messages = [];
|
||||
if($results->CreatedCount()) $messages[] = sprintf('Imported %d items', $results->CreatedCount());
|
||||
if($results->UpdatedCount()) $messages[] = sprintf('Updated %d items', $results->UpdatedCount());
|
||||
if($results->DeletedCount()) $messages[] = sprintf('Deleted %d items', $results->DeletedCount());
|
||||
if(!$messages) $messages[] = 'No changes';
|
||||
$form->sessionMessage(implode(', ', $messages), 'good');
|
||||
|
||||
return $this->redirectBack();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Note: This interface is not secured, consider using [Permission::check()](api:SilverStripe\Security\Permission::check()) to limit the controller to users
|
||||
with certain access rights.
|
||||
|
@ -117,45 +138,53 @@ with certain access rights.
|
|||
We're going to use our knowledge from the previous example to import a more sophisticated CSV file.
|
||||
|
||||
Sample CSV Content
|
||||
|
||||
"Number","Name","Birthday","Team"
|
||||
11,"John Doe",1982-05-12,"FC Bayern"
|
||||
12,"Jane Johnson", 1982-05-12,"FC Bayern"
|
||||
13,"Jimmy Dole",,"Schalke 04"
|
||||
|
||||
```
|
||||
"Number","Name","Birthday","Team"
|
||||
11,"John Doe",1982-05-12,"FC Bayern"
|
||||
12,"Jane Johnson", 1982-05-12,"FC Bayern"
|
||||
13,"Jimmy Dole",,"Schalke 04"
|
||||
```
|
||||
|
||||
Datamodel for Player
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class Player extends DataObject {
|
||||
private static $db = array(
|
||||
'PlayerNumber' => 'Int',
|
||||
'FirstName' => 'Text',
|
||||
'LastName' => 'Text',
|
||||
'Birthday' => 'Date',
|
||||
);
|
||||
private static $has_one = array(
|
||||
'Team' => 'FootballTeam'
|
||||
);
|
||||
}
|
||||
?>
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject
|
||||
{
|
||||
private static $db = [
|
||||
'PlayerNumber' => 'Int',
|
||||
'FirstName' => 'Text',
|
||||
'LastName' => 'Text',
|
||||
'Birthday' => 'Date',
|
||||
];
|
||||
private static $has_one = [
|
||||
'Team' => 'FootballTeam'
|
||||
];
|
||||
}
|
||||
?>
|
||||
|
||||
```
|
||||
|
||||
Datamodel for FootballTeam:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class FootballTeam extends DataObject {
|
||||
private static $db = array(
|
||||
'Title' => 'Text',
|
||||
);
|
||||
private static $has_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
}
|
||||
?>
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class FootballTeam extends DataObject
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Text',
|
||||
];
|
||||
private static $has_many = [
|
||||
'Players' => 'Player'
|
||||
];
|
||||
}
|
||||
?>
|
||||
|
||||
```
|
||||
|
||||
Sample implementation of a custom loader. Assumes a CSV-file in a certain format (see below).
|
||||
|
||||
|
@ -163,53 +192,61 @@ Sample implementation of a custom loader. Assumes a CSV-file in a certain format
|
|||
* Splits a combined "Name" fields from the CSV-data into `FirstName` and `Lastname` by a custom importer method
|
||||
* Avoids duplicate imports by a custom `$duplicateChecks` definition
|
||||
* Creates `Team` relations automatically based on the `Gruppe` column in the CSV data
|
||||
```php
|
||||
use SilverStripe\Dev\CsvBulkLoader;
|
||||
|
||||
class PlayerCsvBulkLoader extends CsvBulkLoader
|
||||
{
|
||||
public $columnMap = [
|
||||
'Number' => 'PlayerNumber',
|
||||
'Name' => '->importFirstAndLastName',
|
||||
'Birthday' => 'Birthday',
|
||||
'Team' => 'Team.Title',
|
||||
];
|
||||
public $duplicateChecks = [
|
||||
'Number' => 'PlayerNumber'
|
||||
];
|
||||
public $relationCallbacks = [
|
||||
'Team.Title' => [
|
||||
'relationname' => 'Team',
|
||||
'callback' => 'getTeamByTitle'
|
||||
]
|
||||
];
|
||||
public static function importFirstAndLastName(&$obj, $val, $record)
|
||||
{
|
||||
$parts = explode(' ', $val);
|
||||
if(count($parts) != 2) return false;
|
||||
$obj->FirstName = $parts[0];
|
||||
$obj->LastName = $parts[1];
|
||||
}
|
||||
public static function getTeamByTitle(&$obj, $val, $record)
|
||||
{
|
||||
return FootballTeam::get()->filter('Title', $val)->First();
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
```
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class PlayerCsvBulkLoader extends CsvBulkLoader {
|
||||
public $columnMap = array(
|
||||
'Number' => 'PlayerNumber',
|
||||
'Name' => '->importFirstAndLastName',
|
||||
'Birthday' => 'Birthday',
|
||||
'Team' => 'Team.Title',
|
||||
);
|
||||
public $duplicateChecks = array(
|
||||
'Number' => 'PlayerNumber'
|
||||
);
|
||||
public $relationCallbacks = array(
|
||||
'Team.Title' => array(
|
||||
'relationname' => 'Team',
|
||||
'callback' => 'getTeamByTitle'
|
||||
)
|
||||
);
|
||||
public static function importFirstAndLastName(&$obj, $val, $record) {
|
||||
$parts = explode(' ', $val);
|
||||
if(count($parts) != 2) return false;
|
||||
$obj->FirstName = $parts[0];
|
||||
$obj->LastName = $parts[1];
|
||||
}
|
||||
public static function getTeamByTitle(&$obj, $val, $record) {
|
||||
return FootballTeam::get()->filter('Title', $val)->First();
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
Building off of the ModelAdmin example up top, use a custom loader instead of the default loader by adding it to `$model_importers`. In this example, `CsvBulkLoader` is replaced with `PlayerCsvBulkLoader`.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class PlayerAdmin extends ModelAdmin {
|
||||
private static $managed_models = array(
|
||||
'Player'
|
||||
);
|
||||
private static $model_importers = array(
|
||||
'Player' => 'PlayerCsvBulkLoader',
|
||||
);
|
||||
private static $url_segment = 'players';
|
||||
}
|
||||
?>
|
||||
|
||||
```php
|
||||
use SilverStripe\Admin\ModelAdmin;
|
||||
|
||||
class PlayerAdmin extends ModelAdmin
|
||||
{
|
||||
private static $managed_models = [
|
||||
'Player'
|
||||
];
|
||||
private static $model_importers = [
|
||||
'Player' => 'PlayerCsvBulkLoader',
|
||||
];
|
||||
private static $url_segment = 'players';
|
||||
}
|
||||
?>
|
||||
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
|
|
|
@ -1,223 +0,0 @@
|
|||
summary: Consume external data through their RESTFul interfaces.
|
||||
|
||||
# Restful Service
|
||||
|
||||
[RestfulService](api:RestfulService) is used to enable connections to remote web services through PHP's `curl` command. It provides an
|
||||
interface and utility functions for generating a valid request and parsing the response returned from the web service.
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
RestfulService currently only supports XML. It has no JSON support at this stage.
|
||||
</div>
|
||||
|
||||
## Examples
|
||||
|
||||
### Creating a new RestfulObject
|
||||
|
||||
`getWellingtonWeather` queries the Yahoo Weather API for an XML file of the latest weather information. We pass a query
|
||||
string parameter `q` with the search query and then convert the resulting XML data to an `ArrayList` object to display
|
||||
in the template.
|
||||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
public function getWellingtonWeather() {
|
||||
$fetch = new RestfulService(
|
||||
'https://query.yahooapis.com/v1/public/yql'
|
||||
);
|
||||
|
||||
$fetch->setQueryString(array(
|
||||
'q' => 'select * from weather.forecast where woeid in (select woeid from geo.places(1) where text="Wellington, NZ")'
|
||||
));
|
||||
|
||||
// perform the query
|
||||
$conn = $fetch->request();
|
||||
|
||||
// parse the XML body
|
||||
$msgs = $fetch->getValues($conn->getBody(), "results");
|
||||
|
||||
// generate an object our templates can read
|
||||
$output = new ArrayList();
|
||||
|
||||
if($msgs) {
|
||||
foreach($msgs as $msg) {
|
||||
$output->push(new ArrayData(array(
|
||||
'Description' => Convert::xml2raw($msg->channel_item_description)
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
## Features
|
||||
|
||||
### Basic Authenication
|
||||
|
||||
:::php
|
||||
$service = new RestfulService("http://example.harvestapp.com");
|
||||
$service->basicAuth('username', 'password');
|
||||
|
||||
### Make multiple requests
|
||||
|
||||
:::php
|
||||
$service = new RestfulService("http://example.harvestapp.com");
|
||||
|
||||
$peopleXML = $service->request('/people');
|
||||
$people = $service->getValues($peopleXML, 'user');
|
||||
|
||||
// ...
|
||||
|
||||
$taskXML = $service->request('/tasks');
|
||||
$tasks = $service->getValues($taskXML, 'task');
|
||||
|
||||
|
||||
### Caching
|
||||
|
||||
To set the cache interval you can pass it as the 2nd argument to constructor.
|
||||
|
||||
:::php
|
||||
$expiry = 60 * 60; // 1 hour;
|
||||
|
||||
$request = new RestfulService("http://example.harvestapp.com", $expiry );
|
||||
|
||||
|
||||
### Getting Values & Attributes
|
||||
|
||||
You can traverse through document tree to get the values or attribute of a particular node using XPath. Take for example
|
||||
the following XML that is returned.
|
||||
|
||||
:::xml
|
||||
<entries>
|
||||
<entry id='12'>Sally</entry>
|
||||
<entry id='15'>Ted</entry>
|
||||
<entry id='30'>Matt</entry>
|
||||
<entry id='22'>John</entry>
|
||||
</entries>
|
||||
|
||||
To extract the id attributes of the entries use:
|
||||
|
||||
:::php
|
||||
$this->getAttributes($xml, "entries", "entry");
|
||||
|
||||
// array(array('id' => 12), array('id' => '15'), ..)
|
||||
|
||||
To extract the values (the names) of the entries use:
|
||||
|
||||
:::php
|
||||
$this->getValues($xml, "entries", "entry");
|
||||
|
||||
// array('Sally', 'Ted', 'Matt', 'John')
|
||||
|
||||
### Searching for Values & Attributes
|
||||
|
||||
If you don't know the exact position of DOM tree where the node will appear you can use xpath to search for the node.
|
||||
|
||||
<div class="note">
|
||||
This is the recommended method for retrieving values of name spaced nodes.
|
||||
</div>
|
||||
|
||||
:::xml
|
||||
<media:guide>
|
||||
<media:entry id="2030">video</media:entry>
|
||||
</media:guide>
|
||||
|
||||
To get the value of entry node with the namespace media, use:
|
||||
|
||||
:::php
|
||||
$this->searchValue($response, "//media:guide/media:entry");
|
||||
|
||||
// array('video');
|
||||
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Handling Errors
|
||||
|
||||
If the web service returned an error (for example, API key not available or inadequate parameters),
|
||||
[RestfulService](api:RestfulService) can delegate the error handling to it's descendant class. To handle the errors, subclass
|
||||
`RestfulService and define a function called errorCatch.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyRestfulService extends RestfulService {
|
||||
|
||||
public function errorCatch($response) {
|
||||
$err_msg = $response;
|
||||
|
||||
if(strpos($err_msg, '<') === false) {
|
||||
user_error("YouTube Service Error : $err_msg", E_USER_ERROR);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
If you want to bypass error handling, define `checkErrors` in the constructor for `RestfulService`
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyRestfulService extends RestfulService {
|
||||
|
||||
public function __construct($expiry = NULL) {
|
||||
parent::__construct('http://www.flickr.com/services/rest/', $expiry);
|
||||
|
||||
$this->checkErrors = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
### Setting cURL options
|
||||
|
||||
Restful service uses cURL to make requests. There are various settings that can be defined on the cURL
|
||||
request (see http://www.php.net/manual/en/function.curl-setopt.php) via the curl_setopts function.
|
||||
|
||||
There are two ways to define these for `RestfulService`; they can be global settings or per request settings.
|
||||
|
||||
It is important to note that your cURL options will be applied LAST and so take preference over any default
|
||||
values that `RestfulService` sets (such as `CURLOPT_RETURNTRANSFER`) so changing these options may result
|
||||
in unexpected behaviour or broken functionality.
|
||||
|
||||
|
||||
#### Global cURL settings
|
||||
|
||||
To set global cURL settings you can update the `RestfulService` config via the Config system or YAML.
|
||||
|
||||
Here is an example to increase the HTTP Timeout globally. Insert this in your `_config.php` file:
|
||||
|
||||
```php
|
||||
SilverStripe\Core\Config\Config::inst()->update('RestfulService', 'default_curl_options', array(
|
||||
CURLOPT_DNS_CACHE_TIMEOUT => 3600,
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
));
|
||||
```
|
||||
|
||||
|
||||
#### Per request settings
|
||||
|
||||
When making a request using `RestfulService` one can also pass through an array of cURL options in the last
|
||||
parameter in `RestfulService::request()`.
|
||||
|
||||
For example:
|
||||
|
||||
```php
|
||||
|
||||
//cURL options
|
||||
$curlOptions = array(
|
||||
CURLOPT_UNRESTRICTED_AUTH => true,
|
||||
);
|
||||
|
||||
$service = new RestfulService('http://example.com/');
|
||||
$service->request('service.json', 'GET', null, null, $curlOptions);
|
||||
|
||||
```
|
||||
|
||||
|
||||
## How to's
|
||||
|
||||
* [Embed an RSS Feed](how_tos/embed_rss)
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [RestfulService](api:RestfulService)
|
|
@ -22,26 +22,28 @@ web pages need to link to the URL to notify users that the RSS feed is available
|
|||
|
||||
An outline of step one looks like:
|
||||
|
||||
:::php
|
||||
$feed = new RSSFeed(
|
||||
$list,
|
||||
$link,
|
||||
$title,
|
||||
$description,
|
||||
$titleField,
|
||||
$descriptionField,
|
||||
$authorField,
|
||||
$lastModifiedTime,
|
||||
$etag
|
||||
);
|
||||
|
||||
$feed->outputToBrowser();
|
||||
```php
|
||||
$feed = new RSSFeed(
|
||||
$list,
|
||||
$link,
|
||||
$title,
|
||||
$description,
|
||||
$titleField,
|
||||
$descriptionField,
|
||||
$authorField,
|
||||
$lastModifiedTime,
|
||||
$etag
|
||||
);
|
||||
|
||||
$feed->outputToBrowser();
|
||||
```
|
||||
|
||||
To achieve step two include the following code where ever you want to include the `<link>` tag to the RSS Feed. This
|
||||
will normally go in your `Controllers` `init` method.
|
||||
|
||||
:::php
|
||||
RSSFeed::linkToFeed($link, $title);
|
||||
```php
|
||||
RSSFeed::linkToFeed($link, $title);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
|
@ -52,38 +54,47 @@ You can use [RSSFeed](api:SilverStripe\Control\RSS\RSSFeed) to easily create a f
|
|||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
..
|
||||
|
||||
class PageController extends ContentController {
|
||||
```php
|
||||
use SilverStripe\Control\RSS\RSSFeed;
|
||||
use Page;
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'rss'
|
||||
);
|
||||
|
||||
..
|
||||
class PageController extends ContentController
|
||||
{
|
||||
|
||||
public function init() {
|
||||
parent::init();
|
||||
private static $allowed_actions = [
|
||||
'rss'
|
||||
];
|
||||
|
||||
RSSFeed::linkToFeed($this->Link() . "rss", "10 Most Recently Updated Pages");
|
||||
}
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
|
||||
public function rss() {
|
||||
$rss = new RSSFeed(
|
||||
$this->LatestUpdates(),
|
||||
$this->Link(),
|
||||
"10 Most Recently Updated Pages",
|
||||
"Shows a list of the 10 most recently updated pages."
|
||||
);
|
||||
RSSFeed::linkToFeed($this->Link() . "rss", "10 Most Recently Updated Pages");
|
||||
}
|
||||
|
||||
return $rss->outputToBrowser();
|
||||
}
|
||||
public function rss()
|
||||
{
|
||||
$rss = new RSSFeed(
|
||||
$this->LatestUpdates(),
|
||||
$this->Link(),
|
||||
"10 Most Recently Updated Pages",
|
||||
"Shows a list of the 10 most recently updated pages."
|
||||
);
|
||||
|
||||
public function LatestUpdates() {
|
||||
return Page::get()->sort("LastEdited", "DESC")->limit(10);
|
||||
}
|
||||
}
|
||||
return $rss->outputToBrowser();
|
||||
}
|
||||
|
||||
public function LatestUpdates()
|
||||
{
|
||||
return Page::get()->sort("LastEdited", "DESC")->limit(10);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Rendering DataObjects in a RSSFeed
|
||||
|
||||
|
@ -97,49 +108,62 @@ If the items are all displayed on a single page you may simply hard code the lin
|
|||
Take an example, we want to create an RSS feed of all the `Players` objects in our site. We make sure the `AbsoluteLink`
|
||||
method is defined and returns a string to the full website URL.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Player extends DataObject {
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
public function AbsoluteLink() {
|
||||
// assumes players can be accessed at yoursite.com/players/2
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
return Controller::join_links(
|
||||
Director::absoluteBaseUrl(),
|
||||
'players',
|
||||
$this->ID
|
||||
);
|
||||
}
|
||||
}
|
||||
public function AbsoluteLink()
|
||||
{
|
||||
// assumes players can be accessed at yoursite.com/players/2
|
||||
|
||||
return Controller::join_links(
|
||||
Director::absoluteBaseUrl(),
|
||||
'players',
|
||||
$this->ID
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then in our controller, we add a new action which returns a the XML list of `Players`.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class PageController extends ContentController {
|
||||
```php
|
||||
use SilverStripe\Control\RSS\RSSFeed;
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'players'
|
||||
);
|
||||
class PageController extends ContentController
|
||||
{
|
||||
|
||||
public function init() {
|
||||
parent::init();
|
||||
private static $allowed_actions = [
|
||||
'players'
|
||||
];
|
||||
|
||||
RSSFeed::linkToFeed($this->Link("players"), "Players");
|
||||
}
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
|
||||
public function players() {
|
||||
$rss = new RSSFeed(
|
||||
Player::get(),
|
||||
$this->Link("players"),
|
||||
"Players"
|
||||
);
|
||||
RSSFeed::linkToFeed($this->Link("players"), "Players");
|
||||
}
|
||||
|
||||
return $rss->outputToBrowser();
|
||||
}
|
||||
}
|
||||
public function players()
|
||||
{
|
||||
$rss = new RSSFeed(
|
||||
Player::get(),
|
||||
$this->Link("players"),
|
||||
"Players"
|
||||
);
|
||||
|
||||
return $rss->outputToBrowser();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Customizing the RSS Feed template
|
||||
|
||||
|
@ -150,41 +174,46 @@ Say from that last example we want to include the Players Team in the XML feed w
|
|||
|
||||
**mysite/templates/PlayersRss.ss**
|
||||
|
||||
:::xml
|
||||
<?xml version="1.0"?>
|
||||
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>$Title</title>
|
||||
<link>$Link</link>
|
||||
<atom:link href="$Link" rel="self" type="application/rss+xml" />
|
||||
<description>$Description.XML</description>
|
||||
|
||||
<% loop $Entries %>
|
||||
<item>
|
||||
<title>$Title.XML</title>
|
||||
<team>$Team.Title</team>
|
||||
</item>
|
||||
<% end_loop %>
|
||||
</channel>
|
||||
</rss>
|
||||
```xml
|
||||
|
||||
<?xml version="1.0"?>
|
||||
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>$Title</title>
|
||||
<link>$Link</link>
|
||||
<atom:link href="$Link" rel="self" type="application/rss+xml" />
|
||||
<description>$Description.XML</description>
|
||||
|
||||
<% loop $Entries %>
|
||||
<item>
|
||||
<title>$Title.XML</title>
|
||||
<team>$Team.Title</team>
|
||||
</item>
|
||||
<% end_loop %>
|
||||
</channel>
|
||||
</rss>
|
||||
```
|
||||
|
||||
`setTemplate` can then be used to tell RSSFeed to use that new template.
|
||||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
|
||||
public function players() {
|
||||
$rss = new RSSFeed(
|
||||
Player::get(),
|
||||
$this->Link("players"),
|
||||
"Players"
|
||||
);
|
||||
|
||||
$rss->setTemplate('PlayersRss');
|
||||
```php
|
||||
public function players()
|
||||
{
|
||||
$rss = new RSSFeed(
|
||||
Player::get(),
|
||||
$this->Link("players"),
|
||||
"Players"
|
||||
);
|
||||
|
||||
$rss->setTemplate('PlayersRss');
|
||||
|
||||
return $rss->outputToBrowser();
|
||||
}
|
||||
return $rss->outputToBrowser();
|
||||
}
|
||||
```
|
||||
|
||||
<div class="warning">
|
||||
As we've added a new template (PlayersRss.ss) make sure you clear your SilverStripe cache.
|
||||
|
|
|
@ -6,62 +6,75 @@ You can have more customised logic and interface feedback through a custom contr
|
|||
form (which is used for `MyDataObject` instances). You can access it through
|
||||
`http://yoursite.com/MyController/?flush=all`.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyController extends Controller {
|
||||
```php
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\FileField;
|
||||
use SilverStripe\Forms\FormAction;
|
||||
use SilverStripe\Forms\RequiredFields;
|
||||
use SilverStripe\Dev\CsvBulkLoader;
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'Form'
|
||||
);
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
protected $template = "BlankPage";
|
||||
private static $allowed_actions = [
|
||||
'Form'
|
||||
];
|
||||
|
||||
public function Link($action = null) {
|
||||
return Controller::join_links('MyController', $action);
|
||||
}
|
||||
protected $template = "BlankPage";
|
||||
|
||||
public function Form() {
|
||||
$form = new Form(
|
||||
$this,
|
||||
'Form',
|
||||
new FieldList(
|
||||
new FileField('CsvFile', false)
|
||||
),
|
||||
new FieldList(
|
||||
new FormAction('doUpload', 'Upload')
|
||||
),
|
||||
new RequiredFields()
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
public function Link($action = null)
|
||||
{
|
||||
return Controller::join_links('MyController', $action);
|
||||
}
|
||||
|
||||
public function doUpload($data, $form) {
|
||||
$loader = new CsvBulkLoader('MyDataObject');
|
||||
$results = $loader->load($_FILES['CsvFile']['tmp_name']);
|
||||
$messages = array();
|
||||
public function Form()
|
||||
{
|
||||
$form = new Form(
|
||||
$this,
|
||||
'Form',
|
||||
new FieldList(
|
||||
new FileField('CsvFile', false)
|
||||
),
|
||||
new FieldList(
|
||||
new FormAction('doUpload', 'Upload')
|
||||
),
|
||||
new RequiredFields()
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
|
||||
if($results->CreatedCount()) {
|
||||
$messages[] = sprintf('Imported %d items', $results->CreatedCount());
|
||||
}
|
||||
public function doUpload($data, $form)
|
||||
{
|
||||
$loader = new CsvBulkLoader('MyDataObject');
|
||||
$results = $loader->load($_FILES['CsvFile']['tmp_name']);
|
||||
$messages = [];
|
||||
|
||||
if($results->UpdatedCount()) {
|
||||
$messages[] = sprintf('Updated %d items', $results->UpdatedCount());
|
||||
}
|
||||
if($results->CreatedCount()) {
|
||||
$messages[] = sprintf('Imported %d items', $results->CreatedCount());
|
||||
}
|
||||
|
||||
if($results->DeletedCount()) {
|
||||
$messages[] = sprintf('Deleted %d items', $results->DeletedCount());
|
||||
}
|
||||
if($results->UpdatedCount()) {
|
||||
$messages[] = sprintf('Updated %d items', $results->UpdatedCount());
|
||||
}
|
||||
|
||||
if(!$messages) {
|
||||
$messages[] = 'No changes';
|
||||
}
|
||||
if($results->DeletedCount()) {
|
||||
$messages[] = sprintf('Deleted %d items', $results->DeletedCount());
|
||||
}
|
||||
|
||||
$form->sessionMessage(implode(', ', $messages), 'good');
|
||||
if(!$messages) {
|
||||
$messages[] = 'No changes';
|
||||
}
|
||||
|
||||
return $this->redirectBack();
|
||||
}
|
||||
}
|
||||
$form->sessionMessage(implode(', ', $messages), 'good');
|
||||
|
||||
return $this->redirectBack();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
This interface is not secured, consider using [Permission::check()](api:SilverStripe\Security\Permission::check()) to limit the controller to users with certain
|
||||
|
|
|
@ -4,49 +4,57 @@ title: A custom CSVBulkLoader instance
|
|||
|
||||
A an implementation of a custom `CSVBulkLoader` loader. In this example. we're provided with a unique CSV file
|
||||
containing a list of football players and the team they play for. The file we have is in the format like below.
|
||||
|
||||
"SpielerNummer", "Name", "Geburtsdatum", "Gruppe"
|
||||
11, "John Doe", 1982-05-12,"FC Bayern"
|
||||
12, "Jane Johnson", 1982-05-12,"FC Bayern"
|
||||
13, "Jimmy Dole",,"Schalke 04"
|
||||
|
||||
```
|
||||
"SpielerNummer", "Name", "Geburtsdatum", "Gruppe"
|
||||
11, "John Doe", 1982-05-12,"FC Bayern"
|
||||
12, "Jane Johnson", 1982-05-12,"FC Bayern"
|
||||
13, "Jimmy Dole",,"Schalke 04"
|
||||
```
|
||||
This data needs to be imported into our application. For this, we have two `DataObjects` setup. `Player` contains
|
||||
information about the individual player and a relation set up for managing the `Team`.
|
||||
|
||||
**mysite/code/Player.php**.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'PlayerNumber' => 'Int',
|
||||
'FirstName' => 'Text',
|
||||
'LastName' => 'Text',
|
||||
'Birthday' => 'Date'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Team' => 'FootballTeam'
|
||||
);
|
||||
}
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Player extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'PlayerNumber' => 'Int',
|
||||
'FirstName' => 'Text',
|
||||
'LastName' => 'Text',
|
||||
'Birthday' => 'Date'
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'Team' => 'FootballTeam'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**mysite/code/FootballTeam.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class FootballTeam extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Text'
|
||||
);
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
private static $has_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
}
|
||||
class FootballTeam extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
'Title' => 'Text'
|
||||
];
|
||||
|
||||
private static $has_many = [
|
||||
'Players' => 'Player'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Now going back to look at the CSV, we can see that what we're provided with does not match what our data model looks
|
||||
like, so we have to create a sub class of `CsvBulkLoader` to handle the unique file. Things we need to consider with
|
||||
|
@ -62,40 +70,46 @@ Our final import looks like this.
|
|||
|
||||
**mysite/code/PlayerCsvBulkLoader.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class PlayerCsvBulkLoader extends CsvBulkLoader {
|
||||
```php
|
||||
use SilverStripe\Dev\CsvBulkLoader;
|
||||
|
||||
public $columnMap = array(
|
||||
'Number' => 'PlayerNumber',
|
||||
'Name' => '->importFirstAndLastName',
|
||||
'Geburtsdatum' => 'Birthday',
|
||||
'Gruppe' => 'Team.Title',
|
||||
);
|
||||
class PlayerCsvBulkLoader extends CsvBulkLoader
|
||||
{
|
||||
|
||||
public $duplicateChecks = array(
|
||||
'SpielerNummer' => 'PlayerNumber'
|
||||
);
|
||||
public $columnMap = [
|
||||
'Number' => 'PlayerNumber',
|
||||
'Name' => '->importFirstAndLastName',
|
||||
'Geburtsdatum' => 'Birthday',
|
||||
'Gruppe' => 'Team.Title',
|
||||
];
|
||||
|
||||
public $relationCallbacks = array(
|
||||
'Team.Title' => array(
|
||||
'relationname' => 'Team',
|
||||
'callback' => 'getTeamByTitle'
|
||||
)
|
||||
);
|
||||
public $duplicateChecks = [
|
||||
'SpielerNummer' => 'PlayerNumber'
|
||||
];
|
||||
|
||||
public static function importFirstAndLastName(&$obj, $val, $record) {
|
||||
$parts = explode(' ', $val);
|
||||
if(count($parts) != 2) return false;
|
||||
$obj->FirstName = $parts[0];
|
||||
$obj->LastName = $parts[1];
|
||||
}
|
||||
public $relationCallbacks = [
|
||||
'Team.Title' => [
|
||||
'relationname' => 'Team',
|
||||
'callback' => 'getTeamByTitle'
|
||||
]
|
||||
];
|
||||
|
||||
public static function getTeamByTitle(&$obj, $val, $record) {
|
||||
return FootballTeam::get()->filter('Title', $val)->First();
|
||||
}
|
||||
}
|
||||
public static function importFirstAndLastName(&$obj, $val, $record)
|
||||
{
|
||||
$parts = explode(' ', $val);
|
||||
if(count($parts) != 2) return false;
|
||||
$obj->FirstName = $parts[0];
|
||||
$obj->LastName = $parts[1];
|
||||
}
|
||||
|
||||
public static function getTeamByTitle(&$obj, $val, $record)
|
||||
{
|
||||
return FootballTeam::get()->filter('Title', $val)->First();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
|
|
|
@ -9,47 +9,54 @@ First, we write the code to query the API feed.
|
|||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
public function getWellingtonWeather() {
|
||||
$fetch = new RestfulService(
|
||||
'https://query.yahooapis.com/v1/public/yql'
|
||||
);
|
||||
|
||||
$fetch->setQueryString(array(
|
||||
'q' => 'select * from weather.forecast where woeid in (select woeid from geo.places(1) where text="Wellington, NZ")'
|
||||
));
|
||||
|
||||
// perform the query
|
||||
$conn = $fetch->request();
|
||||
|
||||
// parse the XML body
|
||||
$msgs = $fetch->getValues($conn->getBody(), "results");
|
||||
```php
|
||||
public function getWellingtonWeather()
|
||||
{
|
||||
$fetch = new RestfulService(
|
||||
'https://query.yahooapis.com/v1/public/yql'
|
||||
);
|
||||
|
||||
$fetch->setQueryString([
|
||||
'q' => 'select * from weather.forecast where woeid in (select woeid from geo.places(1) where text="Wellington, NZ")'
|
||||
]);
|
||||
|
||||
// perform the query
|
||||
$conn = $fetch->request();
|
||||
|
||||
// generate an object our templates can read
|
||||
$output = new ArrayList();
|
||||
// parse the XML body
|
||||
$msgs = $fetch->getValues($conn->getBody(), "results");
|
||||
|
||||
if($msgs) {
|
||||
foreach($msgs as $msg) {
|
||||
$output->push(new ArrayData(array(
|
||||
'Description' => Convert::xml2raw($msg->channel_item_description)
|
||||
)));
|
||||
}
|
||||
}
|
||||
// generate an object our templates can read
|
||||
$output = new ArrayList();
|
||||
|
||||
return $output;
|
||||
}
|
||||
if($msgs) {
|
||||
foreach($msgs as $msg) {
|
||||
$output->push(new ArrayData([
|
||||
'Description' => Convert::xml2raw($msg->channel_item_description)
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This will provide our `Page` template with a new `WellingtonWeather` variable (an [ArrayList](api:SilverStripe\ORM\ArrayList)). Each item has a
|
||||
single field `Description`.
|
||||
|
||||
**mysite/templates/Page.ss**
|
||||
|
||||
:::ss
|
||||
<% if WellingtonWeather %>
|
||||
<% loop WellingtonWeather %>
|
||||
$Description
|
||||
<% end_loop %>
|
||||
<% end_if %>
|
||||
|
||||
```ss
|
||||
|
||||
<% if WellingtonWeather %>
|
||||
<% loop WellingtonWeather %>
|
||||
$Description
|
||||
<% end_loop %>
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
|
|
|
@ -18,16 +18,20 @@ The default output of a [SearchContext](api:SilverStripe\ORM\Search\SearchContex
|
|||
|
||||
Defining search-able fields on your DataObject.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'Name',
|
||||
'ProductCode'
|
||||
);
|
||||
}
|
||||
class MyDataObject extends DataObject
|
||||
{
|
||||
|
||||
private static $searchable_fields = [
|
||||
'Name',
|
||||
'ProductCode'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Customizing fields and filters
|
||||
|
||||
|
@ -35,34 +39,42 @@ In this example we're defining three attributes on our MyDataObject subclass: `P
|
|||
and `MyDate`. The attribute `HiddenProperty` should not be searchable, and `MyDate` should only search for dates
|
||||
*after* the search entry (with a `GreaterThanFilter`).
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
```php
|
||||
use SilverStripe\ORM\Filters\PartialMatchFilter;
|
||||
use SilverStripe\ORM\Filters\GreaterThanFilter;
|
||||
use SilverStripe\ORM\Search\SearchContext;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
private static $db = array(
|
||||
'PublicProperty' => 'Text'
|
||||
'HiddenProperty' => 'Text',
|
||||
'MyDate' => 'Date'
|
||||
);
|
||||
|
||||
public function getDefaultSearchContext() {
|
||||
$fields = $this->scaffoldSearchFields(array(
|
||||
'restrictFields' => array('PublicProperty','MyDate')
|
||||
));
|
||||
class MyDataObject extends DataObject
|
||||
{
|
||||
|
||||
$filters = array(
|
||||
'PublicProperty' => new PartialMatchFilter('PublicProperty'),
|
||||
'MyDate' => new GreaterThanFilter('MyDate')
|
||||
);
|
||||
private static $db = [
|
||||
'PublicProperty' => 'Text'
|
||||
'HiddenProperty' => 'Text',
|
||||
'MyDate' => 'Date'
|
||||
];
|
||||
|
||||
public function getDefaultSearchContext()
|
||||
{
|
||||
$fields = $this->scaffoldSearchFields([
|
||||
'restrictFields' => ['PublicProperty','MyDate']
|
||||
]);
|
||||
|
||||
return new SearchContext(
|
||||
$this->class,
|
||||
$fields,
|
||||
$filters
|
||||
);
|
||||
}
|
||||
}
|
||||
$filters = [
|
||||
'PublicProperty' => new PartialMatchFilter('PublicProperty'),
|
||||
'MyDate' => new GreaterThanFilter('MyDate')
|
||||
];
|
||||
|
||||
return new SearchContext(
|
||||
$this->class,
|
||||
$fields,
|
||||
$filters
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
See the [SearchFilter](../model/searchfilters) documentation for more information about filters to use such as the
|
||||
|
@ -76,36 +88,45 @@ the `$fields` constructor parameter.
|
|||
|
||||
### Generating a search form from the context
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
// ..
|
||||
```php
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\FormAction;
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
class PageController extends ContentController {
|
||||
|
||||
// ..
|
||||
class PageController extends ContentController
|
||||
{
|
||||
|
||||
public function SearchForm() {
|
||||
$context = singleton('MyDataObject')->getCustomSearchContext();
|
||||
$fields = $context->getSearchFields();
|
||||
public function SearchForm()
|
||||
{
|
||||
$context = singleton('MyDataObject')->getCustomSearchContext();
|
||||
$fields = $context->getSearchFields();
|
||||
|
||||
$form = new Form($this, "SearchForm",
|
||||
$fields,
|
||||
new FieldList(
|
||||
new FormAction('doSearch')
|
||||
)
|
||||
);
|
||||
$form = new Form($this, "SearchForm",
|
||||
$fields,
|
||||
new FieldList(
|
||||
new FormAction('doSearch')
|
||||
)
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function doSearch($data, $form) {
|
||||
$context = singleton('MyDataObject')->getCustomSearchContext();
|
||||
$results = $context->getResults($data);
|
||||
public function doSearch($data, $form)
|
||||
{
|
||||
$context = singleton('MyDataObject')->getCustomSearchContext();
|
||||
$results = $context->getResults($data);
|
||||
|
||||
return $this->customise(array(
|
||||
'Results' => $results
|
||||
))->renderWith('Page_results');
|
||||
}
|
||||
}
|
||||
return $this->customise([
|
||||
'Results' => $results
|
||||
])->renderWith('Page_results');
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
|
@ -114,37 +135,43 @@ For pagination records on multiple pages, you need to wrap the results in a
|
|||
in order to read page limit information. It is also passed the current
|
||||
`HTTPRequest` object so it can read the current page from a GET var.
|
||||
|
||||
:::php
|
||||
public function getResults($searchCriteria = array()) {
|
||||
$start = ($this->getRequest()->getVar('start')) ? (int)$this->getRequest()->getVar('start') : 0;
|
||||
$limit = 10;
|
||||
|
||||
$context = singleton('MyDataObject')->getCustomSearchContext();
|
||||
$query = $context->getQuery($searchCriteria, null, array('start'=>$start,'limit'=>$limit));
|
||||
$records = $context->getResults($searchCriteria, null, array('start'=>$start,'limit'=>$limit));
|
||||
|
||||
if($records) {
|
||||
$records = new PaginatedList($records, $this->getRequest());
|
||||
$records->setPageStart($start);
|
||||
$records->setPageLength($limit);
|
||||
$records->setTotalItems($query->unlimitedRowCount());
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
```php
|
||||
public function getResults($searchCriteria = [])
|
||||
{
|
||||
$start = ($this->getRequest()->getVar('start')) ? (int)$this->getRequest()->getVar('start') : 0;
|
||||
$limit = 10;
|
||||
|
||||
$context = singleton('MyDataObject')->getCustomSearchContext();
|
||||
$query = $context->getQuery($searchCriteria, null, ['start'=>$start,'limit'=>$limit]);
|
||||
$records = $context->getResults($searchCriteria, null, ['start'=>$start,'limit'=>$limit]);
|
||||
|
||||
if($records) {
|
||||
$records = new PaginatedList($records, $this->getRequest());
|
||||
$records->setPageStart($start);
|
||||
$records->setPageLength($limit);
|
||||
$records->setTotalItems($query->unlimitedRowCount());
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
notice that if you want to use this getResults function, you need to change the function doSearch for this one:
|
||||
|
||||
:::php
|
||||
public function doSearch($data, $form) {
|
||||
$context = singleton('MyDataObject')->getCustomSearchContext();
|
||||
$results = $this->getResults($data);
|
||||
return $this->customise(array(
|
||||
'Results' => $results
|
||||
))->renderWith(array('Catalogo_results', 'Page'));
|
||||
}
|
||||
|
||||
```php
|
||||
public function doSearch($data, $form)
|
||||
{
|
||||
$context = singleton('MyDataObject')->getCustomSearchContext();
|
||||
$results = $this->getResults($data);
|
||||
return $this->customise([
|
||||
'Results' => $results
|
||||
])->renderWith(['Catalogo_results', 'Page']);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The change is in **$results = $this->getResults($data);**, because you are using a custom getResults function.
|
||||
|
||||
|
@ -160,47 +187,46 @@ to show the results of your custom search you need at least this content in your
|
|||
Results.PaginationSummary(4) defines how many pages the search will show in the search results. something like:
|
||||
|
||||
**Next 1 2 *3* 4 5 … 558**
|
||||
```ss
|
||||
|
||||
|
||||
:::ss
|
||||
<% if $Results %>
|
||||
<ul>
|
||||
<% loop $Results %>
|
||||
<li>$Title, $Autor</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p>Sorry, your search query did not return any results.</p>
|
||||
<% end_if %>
|
||||
|
||||
<% if $Results.MoreThanOnePage %>
|
||||
<div id="PageNumbers">
|
||||
<p>
|
||||
<% if $Results.NotFirstPage %>
|
||||
<a class="prev" href="$Results.PrevLink" title="View the previous page">Prev</a>
|
||||
<% end_if %>
|
||||
|
||||
<span>
|
||||
<% loop $Results.PaginationSummary(4) %>
|
||||
<% if $CurrentBool %>
|
||||
$PageNum
|
||||
<% else %>
|
||||
<% if $Link %>
|
||||
<a href="$Link" title="View page number $PageNum">$PageNum</a>
|
||||
<% else %>
|
||||
…
|
||||
<% end_if %>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
</span>
|
||||
|
||||
<% if $Results.NotLastPage %>
|
||||
<a class="next" href="$Results.NextLink" title="View the next page">Next</a>
|
||||
<% end_if %>
|
||||
</p>
|
||||
</div>
|
||||
<% end_if %>
|
||||
|
||||
<% if $Results %>
|
||||
<ul>
|
||||
<% loop $Results %>
|
||||
<li>$Title, $Autor</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p>Sorry, your search query did not return any results.</p>
|
||||
<% end_if %>
|
||||
|
||||
<% if $Results.MoreThanOnePage %>
|
||||
<div id="PageNumbers">
|
||||
<p>
|
||||
<% if $Results.NotFirstPage %>
|
||||
<a class="prev" href="$Results.PrevLink" title="View the previous page">Prev</a>
|
||||
<% end_if %>
|
||||
|
||||
<span>
|
||||
<% loop $Results.PaginationSummary(4) %>
|
||||
<% if $CurrentBool %>
|
||||
$PageNum
|
||||
<% else %>
|
||||
<% if $Link %>
|
||||
<a href="$Link" title="View page number $PageNum">$PageNum</a>
|
||||
<% else %>
|
||||
…
|
||||
<% end_if %>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
</span>
|
||||
|
||||
<% if $Results.NotLastPage %>
|
||||
<a class="next" href="$Results.NextLink" title="View the next page">Next</a>
|
||||
<% end_if %>
|
||||
</p>
|
||||
</div>
|
||||
<% end_if %>
|
||||
```
|
||||
|
||||
## Available SearchFilters
|
||||
|
||||
|
|
|
@ -21,15 +21,19 @@ storage engine.
|
|||
|
||||
You can do so by adding this static variable to your class definition:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
private static $create_table_options = array(
|
||||
'MySQLDatabase' => 'ENGINE=MyISAM'
|
||||
);
|
||||
}
|
||||
class MyDataObject extends DataObject
|
||||
{
|
||||
|
||||
private static $create_table_options = [
|
||||
'MySQLDatabase' => 'ENGINE=MyISAM'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The [FulltextSearchable](api:SilverStripe\ORM\Search\FulltextSearchable) extension will add the correct `Fulltext` indexes to the data model.
|
||||
|
||||
|
@ -46,31 +50,39 @@ SilverStripe provides a [FulltextFilter](api:SilverStripe\ORM\Filters\FulltextFi
|
|||
|
||||
Example DataObject:
|
||||
|
||||
:::php
|
||||
class SearchableDataObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
"Title" => "Varchar(255)",
|
||||
"Content" => "HTMLText",
|
||||
);
|
||||
|
||||
private static $indexes = array(
|
||||
'SearchFields' => array(
|
||||
'type' => 'fulltext',
|
||||
'columns' => ['Title', 'Content'],
|
||||
)
|
||||
);
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
private static $create_table_options = array(
|
||||
'MySQLDatabase' => 'ENGINE=MyISAM'
|
||||
);
|
||||
class SearchableDataObject extends DataObject
|
||||
{
|
||||
|
||||
private static $db = [
|
||||
"Title" => "Varchar(255)",
|
||||
"Content" => "HTMLText",
|
||||
];
|
||||
|
||||
}
|
||||
private static $indexes = [
|
||||
'SearchFields' => [
|
||||
'type' => 'fulltext',
|
||||
'columns' => ['Title', 'Content'],
|
||||
]
|
||||
];
|
||||
|
||||
private static $create_table_options = [
|
||||
'MySQLDatabase' => 'ENGINE=MyISAM'
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Performing the search:
|
||||
|
||||
:::php
|
||||
SearchableDataObject::get()->filter('SearchFields:Fulltext', 'search term');
|
||||
|
||||
```php
|
||||
SearchableDataObject::get()->filter('SearchFields:Fulltext', 'search term');
|
||||
```
|
||||
|
||||
If your search index is a single field size, then you may also specify the search filter by the name of the
|
||||
field instead of the index.
|
||||
|
|
|
@ -27,11 +27,12 @@ The i18n class is enabled by default.
|
|||
To set the locale you just need to call [i18n::set_locale()](api:SilverStripe\i18n\i18n::set_locale()) passing, as a parameter, the name of the locale that
|
||||
you want to set.
|
||||
|
||||
:::php
|
||||
// mysite/_config.php
|
||||
i18n::set_locale('de_DE'); // Setting the locale to German (Germany)
|
||||
i18n::set_locale('ca_AD'); // Setting to Catalan (Andorra)
|
||||
|
||||
```php
|
||||
// mysite/_config.php
|
||||
i18n::set_locale('de_DE'); // Setting the locale to German (Germany)
|
||||
i18n::set_locale('ca_AD'); // Setting to Catalan (Andorra)
|
||||
```
|
||||
|
||||
Once we set a locale, all the calls to the translator function will return strings according to the set locale value, if
|
||||
these translations are available. See [unicode.org](http://unicode.org/cldr/data/diff/supplemental/languages_and_territories.html)
|
||||
|
@ -51,13 +52,15 @@ As you set the locale you can also get the current value, just by calling [i18n:
|
|||
|
||||
To let browsers know which language they're displaying a document in, you can declare a language in your template.
|
||||
|
||||
:::html
|
||||
//'Page.ss' (HTML)
|
||||
<html lang="$ContentLocale">
|
||||
|
||||
//'Page.ss' (XHTML)
|
||||
<html lang="$ContentLocale" xml:lang="$ContentLocale" xmlns="http://www.w3.org/1999/xhtml">
|
||||
```html
|
||||
|
||||
//'Page.ss' (HTML)
|
||||
<html lang="$ContentLocale">
|
||||
|
||||
//'Page.ss' (XHTML)
|
||||
<html lang="$ContentLocale" xml:lang="$ContentLocale" xmlns="http://www.w3.org/1999/xhtml">
|
||||
```
|
||||
|
||||
Setting the `<html>` attribute is the most commonly used technique. There are other ways to specify content languages
|
||||
(meta tags, HTTP headers), explained in this [w3.org article](http://www.w3.org/International/tutorials/language-decl/).
|
||||
|
@ -66,17 +69,22 @@ You can also set the [script direction](http://www.w3.org/International/question
|
|||
which is determined by the current locale, in order to indicate the preferred flow of characters
|
||||
and default alignment of paragraphs and tables to browsers.
|
||||
|
||||
:::html
|
||||
<html lang="$ContentLocale" dir="$i18nScriptDirection">
|
||||
|
||||
```html
|
||||
|
||||
<html lang="$ContentLocale" dir="$i18nScriptDirection">
|
||||
```
|
||||
|
||||
### Date and time formats
|
||||
|
||||
Formats can be set globally in the i18n class.
|
||||
You can use these settings for your own view logic.
|
||||
|
||||
:::php
|
||||
Config::inst()->update('i18n', 'date_format', 'dd.MM.yyyy');
|
||||
Config::inst()->update('i18n', 'time_format', 'HH:mm');
|
||||
|
||||
```php
|
||||
Config::inst()->update('i18n', 'date_format', 'dd.MM.yyyy');
|
||||
Config::inst()->update('i18n', 'time_format', 'HH:mm');
|
||||
```
|
||||
|
||||
Localization in SilverStripe uses PHP's [intl extension](http://php.net/intl).
|
||||
Formats for it's [IntlDateFormatter](http://php.net/manual/en/class.intldateformatter.php)
|
||||
|
@ -96,20 +104,26 @@ They can be accessed via the `i18n.common_languages` and `i18n.common_locales` [
|
|||
|
||||
In order to add a value, add the following to your `config.yml`:
|
||||
|
||||
:::yml
|
||||
i18n:
|
||||
common_locales:
|
||||
de_CGN:
|
||||
name: German (Cologne)
|
||||
native: Kölsch
|
||||
|
||||
```yml
|
||||
|
||||
i18n:
|
||||
common_locales:
|
||||
de_CGN:
|
||||
name: German (Cologne)
|
||||
native: Kölsch
|
||||
```
|
||||
|
||||
Similarly, to change an existing language label, you can overwrite one of these keys:
|
||||
|
||||
:::yml
|
||||
i18n:
|
||||
common_locales:
|
||||
en_NZ:
|
||||
native: Niu Zillund
|
||||
|
||||
```yml
|
||||
|
||||
i18n:
|
||||
common_locales:
|
||||
en_NZ:
|
||||
native: Niu Zillund
|
||||
```
|
||||
|
||||
### i18n in URLs
|
||||
|
||||
|
@ -139,22 +153,25 @@ in a localised format chosen by the browser and operating system.
|
|||
Fields can be forced to use a certain locale and date/time format by calling `setHTML5(false)`,
|
||||
followed by `setLocale()` or `setDateFormat()`/`setTimeFormat()`.
|
||||
|
||||
:::php
|
||||
$field = new DateField();
|
||||
$field->setLocale('de_AT'); // set Austrian/German locale, defaulting format to dd.MM.y
|
||||
$field->setDateFormat('d.M.y'); // set a more specific date format (single digit day/month)
|
||||
|
||||
```php
|
||||
$field = new DateField();
|
||||
$field->setLocale('de_AT'); // set Austrian/German locale, defaulting format to dd.MM.y
|
||||
$field->setDateFormat('d.M.y'); // set a more specific date format (single digit day/month)
|
||||
```
|
||||
|
||||
## Translating text
|
||||
|
||||
Adapting a module to make it localizable is easy with SilverStripe. You just need to avoid hardcoding strings that are
|
||||
language-dependent and use a translator function call instead.
|
||||
|
||||
:::php
|
||||
// without i18n
|
||||
echo "This is a string";
|
||||
// with i18n
|
||||
echo _t("Namespace.Entity","This is a string");
|
||||
|
||||
```php
|
||||
// without i18n
|
||||
echo "This is a string";
|
||||
// with i18n
|
||||
echo _t("Namespace.Entity","This is a string");
|
||||
```
|
||||
|
||||
All strings passed through the `_t()` function will be collected in a separate language table (see [Collecting text](#collecting-text)), which is the starting point for translations.
|
||||
|
||||
|
@ -192,7 +209,10 @@ with both a 'one' and 'other' key (as per the CLDR for the default `en` language
|
|||
For instance, this is an example of how to correctly declare pluralisations for an object
|
||||
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyObject extends DataObject, implements i18nEntityProvider
|
||||
{
|
||||
public function provideI18nEntities()
|
||||
|
@ -207,13 +227,15 @@ For instance, this is an example of how to correctly declare pluralisations for
|
|||
];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
In YML format this will be expressed as the below. This follows the
|
||||
[ruby i18n convention](guides.rubyonrails.org/i18n.html#pluralization) for plural forms.
|
||||
|
||||
|
||||
:::yaml
|
||||
|
||||
```yaml
|
||||
|
||||
en:
|
||||
MyObject:
|
||||
SINGULAR_NAME: 'object'
|
||||
|
@ -221,27 +243,28 @@ In YML format this will be expressed as the below. This follows the
|
|||
PLURALS:
|
||||
one: 'An object',
|
||||
other: '{count} objects'
|
||||
|
||||
```
|
||||
|
||||
Note: i18nTextCollector support for pluralisation is not yet available.
|
||||
Please ensure that any required plurals are exposed via provideI18nEntities.
|
||||
|
||||
#### Usage in PHP Files
|
||||
|
||||
:::php
|
||||
|
||||
// Simple string translation
|
||||
_t('LeftAndMain.FILESIMAGES','Files & Images');
|
||||
```php
|
||||
// Simple string translation
|
||||
_t('LeftAndMain.FILESIMAGES','Files & Images');
|
||||
|
||||
// Using injection to add variables into the translated strings.
|
||||
_t('CMSMain.RESTORED',
|
||||
"Restored {value} successfully",
|
||||
array('value' => $itemRestored)
|
||||
);
|
||||
|
||||
// Plurals are invoked via a `|` pipe-delimeter with a {count} argument
|
||||
_t('MyObject.PLURALS', 'An object|{count} objects', [ 'count' => '$count ]);
|
||||
// Using injection to add variables into the translated strings.
|
||||
_t('CMSMain.RESTORED',
|
||||
"Restored {value} successfully",
|
||||
['value' => $itemRestored]
|
||||
);
|
||||
|
||||
// Plurals are invoked via a `|` pipe-delimeter with a {count} argument
|
||||
_t('MyObject.PLURALS', 'An object|{count} objects', [ 'count' => '$count ]);
|
||||
|
||||
```
|
||||
|
||||
#### Usage in Template Files
|
||||
|
||||
|
@ -256,28 +279,33 @@ the PHP version of the function.
|
|||
* The original language string and the natural language comment parameters are separated by ` on `.
|
||||
* The final parameter (which is an array in PHP) is passed as a space separated list of key/value pairs.
|
||||
|
||||
:::ss
|
||||
// Simple string translation
|
||||
<%t Namespace.Entity "String to translate" %>
|
||||
|
||||
// Using injection to add variables into the translated strings (note that $Name and $Greeting must be available in the current template scope).
|
||||
<%t Header.Greeting "Hello {name} {greeting}" name=$Name greeting=$Greeting %>
|
||||
|
||||
// Plurals follow the same convention, required a `|` and `{count}` in the default string
|
||||
```ss
|
||||
|
||||
// Simple string translation
|
||||
<%t Namespace.Entity "String to translate" %>
|
||||
|
||||
// Using injection to add variables into the translated strings (note that $Name and $Greeting must be available in the current template scope).
|
||||
<%t Header.Greeting "Hello {name} {greeting}" name=$Name greeting=$Greeting %>
|
||||
|
||||
// Plurals follow the same convention, required a `|` and `{count}` in the default string
|
||||
<%t MyObject.PLURALS 'An item|{count} items' count=$Count %>
|
||||
|
||||
```
|
||||
|
||||
#### Caching in Template Files with locale switching
|
||||
|
||||
When caching a `<% loop %>` or `<% with %>` with `<%t params %>`. It is important to add the Locale to the cache key
|
||||
otherwise it won't pick up locale changes.
|
||||
|
||||
:::ss
|
||||
<% cached 'MyIdentifier', $CurrentLocale %>
|
||||
<% loop $Students %>
|
||||
$Name
|
||||
<% end_loop %>
|
||||
<% end_cached %>
|
||||
|
||||
```ss
|
||||
|
||||
<% cached 'MyIdentifier', $CurrentLocale %>
|
||||
<% loop $Students %>
|
||||
$Name
|
||||
<% end_loop %>
|
||||
<% end_cached %>
|
||||
```
|
||||
|
||||
## Collecting text
|
||||
|
||||
|
@ -308,17 +336,17 @@ By default, the language files are loaded from modules in this order:
|
|||
This default order is configured in `framework/_config/i18n.yml`. This file specifies two blocks of module ordering: `basei18n`, listing admin, and framework, and `defaulti18n` listing all other modules.
|
||||
|
||||
To create a custom module order, you need to specify a config fragment that inserts itself either after or before those items. For example, you may have a number of modules that have to come after the framework/admin, but before anyhting else. To do that, you would use this
|
||||
|
||||
---
|
||||
Name: customi18n
|
||||
Before: 'defaulti18n'
|
||||
---
|
||||
i18n:
|
||||
module_priority:
|
||||
- module1
|
||||
- module2
|
||||
- module3
|
||||
|
||||
```yml
|
||||
---
|
||||
Name: customi18n
|
||||
Before: 'defaulti18n'
|
||||
---
|
||||
i18n:
|
||||
module_priority:
|
||||
- module1
|
||||
- module2
|
||||
- module3
|
||||
```
|
||||
The config option being set is `i18n.module_priority`, and it is a list of module names.
|
||||
|
||||
There are a few special cases:
|
||||
|
@ -337,21 +365,21 @@ By default, SilverStripe uses a YAML format which is loaded via the
|
|||
[symfony/translate](http://symfony.com/doc/current/translation.html) library.
|
||||
|
||||
Example: framework/lang/en.yml (extract)
|
||||
|
||||
en:
|
||||
ImageUploader:
|
||||
Attach: 'Attach {title}'
|
||||
UploadField:
|
||||
NOTEADDFILES: 'You can add files once you have saved for the first time.'
|
||||
|
||||
```yml
|
||||
en:
|
||||
ImageUploader:
|
||||
Attach: 'Attach {title}'
|
||||
UploadField:
|
||||
NOTEADDFILES: 'You can add files once you have saved for the first time.'
|
||||
```
|
||||
Translation table: framework/lang/de.yml (extract)
|
||||
|
||||
de:
|
||||
ImageUploader:
|
||||
ATTACH: '{title} anhängen'
|
||||
UploadField:
|
||||
NOTEADDFILES: 'Sie können Dateien hinzufügen sobald Sie das erste mal gespeichert haben'
|
||||
|
||||
```yml
|
||||
de:
|
||||
ImageUploader:
|
||||
ATTACH: '{title} anhängen'
|
||||
UploadField:
|
||||
NOTEADDFILES: 'Sie können Dateien hinzufügen sobald Sie das erste mal gespeichert haben'
|
||||
```
|
||||
Note that translations are cached across requests.
|
||||
The cache can be cleared through the `?flush=1` query parameter,
|
||||
or explicitly through `Zend_Translate::getCache()->clean(Zend_Cache::CLEANING_MODE_ALL)`.
|
||||
|
@ -370,9 +398,10 @@ the browser: The current locale, and the default locale as a fallback.
|
|||
The `Requirements` class has a special method to determine these includes:
|
||||
Just point it to a directory instead of a file, and the class will figure out the includes.
|
||||
|
||||
:::php
|
||||
Requirements::add_i18n_javascript('<my-module-dir>/javascript/lang');
|
||||
|
||||
```php
|
||||
Requirements::add_i18n_javascript('<my-module-dir>/javascript/lang');
|
||||
```
|
||||
|
||||
### Translation Tables in JavaScript
|
||||
|
||||
|
@ -381,22 +410,27 @@ As a fallback for partially translated tables we always include the master table
|
|||
|
||||
Master Table (`<my-module-dir>/javascript/lang/en.js`)
|
||||
|
||||
:::js
|
||||
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
|
||||
console.error('Class ss.i18n not defined');
|
||||
} else {
|
||||
ss.i18n.addDictionary('en', {
|
||||
'MYMODULE.MYENTITY' : "Really delete these articles?"
|
||||
});
|
||||
}
|
||||
|
||||
```js
|
||||
|
||||
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
|
||||
console.error('Class ss.i18n not defined');
|
||||
} else {
|
||||
ss.i18n.addDictionary('en', {
|
||||
'MYMODULE.MYENTITY' : "Really delete these articles?"
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Example Translation Table (`<my-module-dir>/javascript/lang/de.js`)
|
||||
|
||||
:::js
|
||||
ss.i18n.addDictionary('de', {
|
||||
'MYMODULE.MYENTITY' : "Artikel wirklich löschen?"
|
||||
});
|
||||
|
||||
```js
|
||||
|
||||
ss.i18n.addDictionary('de', {
|
||||
'MYMODULE.MYENTITY' : "Artikel wirklich löschen?"
|
||||
});
|
||||
```
|
||||
|
||||
For most core modules, these files are generated by a
|
||||
[build task](https://github.com/silverstripe/silverstripe-buildtools/blob/master/src/GenerateJavascriptI18nTask.php),
|
||||
|
@ -405,9 +439,11 @@ format which can be processed more easily by external translation providers (see
|
|||
|
||||
### Basic Usage
|
||||
|
||||
:::js
|
||||
alert(ss.i18n._t('MYMODULE.MYENTITY'));
|
||||
|
||||
```js
|
||||
|
||||
alert(ss.i18n._t('MYMODULE.MYENTITY'));
|
||||
```
|
||||
|
||||
### Advanced Use
|
||||
|
||||
|
@ -417,28 +453,32 @@ The `ss.i18n` object contain a couple functions to help and replace dynamic vari
|
|||
|
||||
`sprintf()` will substitute occurencies of `%s` in the main string with each of the following arguments passed to the function. The substitution is done sequentially.
|
||||
|
||||
:::js
|
||||
// MYMODULE.MYENTITY contains "Really delete %s articles by %s?"
|
||||
alert(ss.i18n.sprintf(
|
||||
ss.i18n._t('MYMODULE.MYENTITY'),
|
||||
42,
|
||||
'Douglas Adams'
|
||||
));
|
||||
// Displays: "Really delete 42 articles by Douglas Adams?"
|
||||
|
||||
```js
|
||||
|
||||
// MYMODULE.MYENTITY contains "Really delete %s articles by %s?"
|
||||
alert(ss.i18n.sprintf(
|
||||
ss.i18n._t('MYMODULE.MYENTITY'),
|
||||
42,
|
||||
'Douglas Adams'
|
||||
));
|
||||
// Displays: "Really delete 42 articles by Douglas Adams?"
|
||||
```
|
||||
|
||||
#### Variable injection with inject()
|
||||
|
||||
`inject()` will substitute variables in the main string like `{myVar}` by the keys in the object passed as second argument. Each variable can be in any order and appear multiple times.
|
||||
|
||||
:::js
|
||||
// MYMODULE.MYENTITY contains "Really delete {count} articles by {author}?"
|
||||
alert(ss.i18n.inject(
|
||||
ss.i18n._t('MYMODULE.MYENTITY'),
|
||||
{count: 42, author: 'Douglas Adams'}
|
||||
));
|
||||
// Displays: "Really delete 42 articles by Douglas Adams?"
|
||||
|
||||
```js
|
||||
|
||||
// MYMODULE.MYENTITY contains "Really delete {count} articles by {author}?"
|
||||
alert(ss.i18n.inject(
|
||||
ss.i18n._t('MYMODULE.MYENTITY'),
|
||||
{count: 42, author: 'Douglas Adams'}
|
||||
));
|
||||
// Displays: "Really delete 42 articles by Douglas Adams?"
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
|
|
|
@ -38,9 +38,12 @@ In order to retain existing file paths in line with framework version 3 you shou
|
|||
`\SilverStripe\Assets\Flysystem\FlysystemAssetStore.legacy_filenames` config to true.
|
||||
Note that this will not allow you to utilise certain file versioning features in 4.0.
|
||||
|
||||
:::yaml
|
||||
\SilverStripe\Assets\Flysystem\FlysystemAssetStore:
|
||||
legacy_filenames: true
|
||||
|
||||
```yaml
|
||||
|
||||
\SilverStripe\Assets\Flysystem\FlysystemAssetStore:
|
||||
legacy_filenames: true
|
||||
```
|
||||
|
||||
## Loading content into `DBFile`
|
||||
|
||||
|
@ -48,20 +51,22 @@ A file can be written to the backend from a file which exists on the local files
|
|||
within the assets folder).
|
||||
|
||||
For example, to load a temporary file into a DataObject you could use the below:
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
<?
|
||||
class Banner extends DataObject
|
||||
{
|
||||
private static $db = [
|
||||
'Image' => 'DBFile'
|
||||
];
|
||||
}
|
||||
|
||||
:::php
|
||||
<?
|
||||
class Banner extends DataObject {
|
||||
private static $db = array(
|
||||
'Image' => 'DBFile'
|
||||
);
|
||||
}
|
||||
|
||||
// Image could be assigned in other parts of the code using the below
|
||||
$banner = new Banner();
|
||||
$banner->Image->setFromLocalFile($tempfile['path'], 'uploads/banner-file.jpg');
|
||||
// Image could be assigned in other parts of the code using the below
|
||||
$banner = new Banner();
|
||||
$banner->Image->setFromLocalFile($tempfile['path'], 'uploads/banner-file.jpg');
|
||||
|
||||
```
|
||||
|
||||
When uploading a file it's normally necessary to give the file a useful name and directory, otherwise the
|
||||
asset storage backend will choose one for you.
|
||||
|
|
|
@ -35,42 +35,48 @@ images are preserved (meaning images are not stretched).
|
|||
|
||||
Here are some examples, assuming the `$Image` object has dimensions of 200x100px:
|
||||
|
||||
:::ss
|
||||
// Scaling functions
|
||||
$Image.ScaleWidth(150) // Returns a 150x75px image
|
||||
$Image.ScaleMaxWidth(100) // Returns a 100x50px image (like ScaleWidth but prevents up-sampling)
|
||||
$Image.ScaleHeight(150) // Returns a 300x150px image (up-sampled. Try to avoid doing this)
|
||||
$Image.ScaleMaxHeight(150) // Returns a 200x100px image (like ScaleHeight but prevents up-sampling)
|
||||
$Image.Fit(300,300) // Returns an image that fits within a 300x300px boundary, resulting in a 300x150px image (up-sampled)
|
||||
$Image.FitMax(300,300) // Returns a 200x100px image (like Fit but prevents up-sampling)
|
||||
|
||||
// Warning: This method can distort images that are not the correct aspect ratio
|
||||
$Image.ResizedImage(200, 300) // Forces dimensions of this image to the given values.
|
||||
|
||||
// Cropping functions
|
||||
$Image.Fill(150,150) // Returns a 150x150px image resized and cropped to fill specified dimensions (up-sampled)
|
||||
$Image.FillMax(150,150) // Returns a 100x100px image (like Fill but prevents up-sampling)
|
||||
$Image.CropWidth(150) // Returns a 150x100px image (trims excess pixels off the x axis from the center)
|
||||
$Image.CropHeight(50) // Returns a 200x50px image (trims excess pixels off the y axis from the center)
|
||||
|
||||
// Padding functions (add space around an image)
|
||||
$Image.Pad(100,100) // Returns a 100x100px padded image, with white bars added at the top and bottom
|
||||
$Image.Pad(100, 100, CCCCCC) // Same as above but with a grey background
|
||||
|
||||
// Metadata
|
||||
$Image.Width // Returns width of image
|
||||
$Image.Height // Returns height of image
|
||||
$Image.Orientation // Returns Orientation
|
||||
$Image.Title // Returns the friendly file name
|
||||
$Image.Name // Returns the actual file name
|
||||
$Image.FileName // Returns the actual file name including directory path from web root
|
||||
$Image.Link // Returns relative URL path to image
|
||||
$Image.AbsoluteLink // Returns absolute URL path to image
|
||||
|
||||
```ss
|
||||
|
||||
// Scaling functions
|
||||
$Image.ScaleWidth(150) // Returns a 150x75px image
|
||||
$Image.ScaleMaxWidth(100) // Returns a 100x50px image (like ScaleWidth but prevents up-sampling)
|
||||
$Image.ScaleHeight(150) // Returns a 300x150px image (up-sampled. Try to avoid doing this)
|
||||
$Image.ScaleMaxHeight(150) // Returns a 200x100px image (like ScaleHeight but prevents up-sampling)
|
||||
$Image.Fit(300,300) // Returns an image that fits within a 300x300px boundary, resulting in a 300x150px image (up-sampled)
|
||||
$Image.FitMax(300,300) // Returns a 200x100px image (like Fit but prevents up-sampling)
|
||||
|
||||
// Warning: This method can distort images that are not the correct aspect ratio
|
||||
$Image.ResizedImage(200, 300) // Forces dimensions of this image to the given values.
|
||||
|
||||
// Cropping functions
|
||||
$Image.Fill(150,150) // Returns a 150x150px image resized and cropped to fill specified dimensions (up-sampled)
|
||||
$Image.FillMax(150,150) // Returns a 100x100px image (like Fill but prevents up-sampling)
|
||||
$Image.CropWidth(150) // Returns a 150x100px image (trims excess pixels off the x axis from the center)
|
||||
$Image.CropHeight(50) // Returns a 200x50px image (trims excess pixels off the y axis from the center)
|
||||
|
||||
// Padding functions (add space around an image)
|
||||
$Image.Pad(100,100) // Returns a 100x100px padded image, with white bars added at the top and bottom
|
||||
$Image.Pad(100, 100, CCCCCC) // Same as above but with a grey background
|
||||
|
||||
// Metadata
|
||||
$Image.Width // Returns width of image
|
||||
$Image.Height // Returns height of image
|
||||
$Image.Orientation // Returns Orientation
|
||||
$Image.Title // Returns the friendly file name
|
||||
$Image.Name // Returns the actual file name
|
||||
$Image.FileName // Returns the actual file name including directory path from web root
|
||||
$Image.Link // Returns relative URL path to image
|
||||
$Image.AbsoluteLink // Returns absolute URL path to image
|
||||
```
|
||||
|
||||
Image methods are chainable. Example:
|
||||
|
||||
:::ss
|
||||
<body style="background-image:url($Image.ScaleWidth(800).CropHeight(800).Link)">
|
||||
|
||||
```ss
|
||||
|
||||
<body style="background-image:url($Image.ScaleWidth(800).CropHeight(800).Link)">
|
||||
```
|
||||
|
||||
### Padded Image Resize
|
||||
|
||||
|
@ -80,10 +86,12 @@ pad any surplus space. You can specify the color of the padding using a hex code
|
|||
You can also specify a level of transparency to apply to the padding color in a fourth param. This will only effect
|
||||
png images.
|
||||
|
||||
:::php
|
||||
$Image.Pad(80, 80, FFFFFF, 50) // white padding with 50% transparency
|
||||
$Image.Pad(80, 80, FFFFFF, 100) // white padding with 100% transparency
|
||||
$Image.Pad(80, 80, FFFFFF) // white padding with no transparency
|
||||
|
||||
```php
|
||||
$Image.Pad(80, 80, FFFFFF, 50) // white padding with 50% transparency
|
||||
$Image.Pad(80, 80, FFFFFF, 100) // white padding with 100% transparency
|
||||
$Image.Pad(80, 80, FFFFFF) // white padding with no transparency
|
||||
```
|
||||
|
||||
### Manipulating images in PHP
|
||||
|
||||
|
@ -97,8 +105,9 @@ Please refer to the [ImageManipulation](api:SilverStripe\Assets\ImageManipulatio
|
|||
|
||||
You can also create your own functions by decorating the `Image` class.
|
||||
|
||||
:::php
|
||||
class ImageExtension extends \SilverStripe\Core\Extension
|
||||
|
||||
```php
|
||||
class ImageExtension extends \SilverStripe\Core\Extension
|
||||
{
|
||||
|
||||
public function Square($width)
|
||||
|
@ -127,13 +136,14 @@ You can also create your own functions by decorating the `Image` class.
|
|||
|
||||
}
|
||||
|
||||
:::yml
|
||||
SilverStripe\Assets\Image:
|
||||
extensions:
|
||||
- ImageExtension
|
||||
SilverStripe\Filesystem\Storage\DBFile:
|
||||
extensions:
|
||||
- ImageExtension
|
||||
:::yml
|
||||
SilverStripe\Assets\Image:
|
||||
extensions:
|
||||
- ImageExtension
|
||||
SilverStripe\Filesystem\Storage\DBFile:
|
||||
extensions:
|
||||
- ImageExtension
|
||||
```
|
||||
|
||||
### Form Upload
|
||||
|
||||
|
@ -164,24 +174,30 @@ place. If you expect the images in your asset store to already have
|
|||
compression applied and want to serve up the original when no resampling is
|
||||
necessary, you can add this to your mysite/config/config.yml file:
|
||||
|
||||
:::yml
|
||||
# Configure resampling for File dataobject
|
||||
File:
|
||||
force_resample: false
|
||||
# DBFile can be configured independently
|
||||
SilverStripe\Filesystem\Storage\DBFile:
|
||||
force_resample: false
|
||||
|
||||
```yml
|
||||
|
||||
# Configure resampling for File dataobject
|
||||
File:
|
||||
force_resample: false
|
||||
# DBFile can be configured independently
|
||||
SilverStripe\Filesystem\Storage\DBFile:
|
||||
force_resample: false
|
||||
```
|
||||
|
||||
#### Resampled image quality
|
||||
|
||||
To adjust the quality of the generated images when they are resampled, add the
|
||||
following to your mysite/config/config.yml file:
|
||||
|
||||
:::yml
|
||||
|
||||
```yml
|
||||
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
SilverStripe\Assets\InterventionBackend:
|
||||
properties:
|
||||
Quality: 90
|
||||
```
|
||||
|
||||
## Changing the manipulation driver to Imagick
|
||||
|
||||
|
|
|
@ -18,12 +18,14 @@ For instance, in order to write an asset to a protected location you can use the
|
|||
config option:
|
||||
|
||||
|
||||
:::php
|
||||
$store = singleton(AssetStore::class);
|
||||
$store->setFromString('My protected content', 'Documents/Mydocument.txt', null, null, array(
|
||||
'visibility' => AssetStore::VISIBILITY_PROTECTED
|
||||
));
|
||||
|
||||
```php
|
||||
$store = singleton(AssetStore::class);
|
||||
$store->setFromString('My protected content', 'Documents/Mydocument.txt', null, null, [
|
||||
'visibility' => AssetStore::VISIBILITY_PROTECTED
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
## User access control
|
||||
|
||||
|
@ -36,7 +38,9 @@ An automated system will, in most cases, handle this whitelisting for you. Calls
|
|||
will automatically whitelist access to that file for the current user. Using this as a guide, you can easily
|
||||
control access to embedded assets at a template level.
|
||||
|
||||
:::ss
|
||||
|
||||
```ss
|
||||
|
||||
<ul class="files">
|
||||
<% loop $File %>
|
||||
<% if $canView %>
|
||||
|
@ -46,6 +50,7 @@ control access to embedded assets at a template level.
|
|||
<% end_if %>
|
||||
<% end_loop >
|
||||
</ul>
|
||||
```
|
||||
|
||||
Users who are able to guess the value of $URL will not be able to access those urls without being
|
||||
authorised by this code.
|
||||
|
@ -58,9 +63,14 @@ authorised users, the following should be considered:
|
|||
file via PHP for the current user instead, by using the following code to grant access.
|
||||
|
||||
|
||||
:::php
|
||||
class PageController extends ContentController {
|
||||
public function init() {
|
||||
|
||||
```php
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
// Whitelist any protected files on this page for the current user
|
||||
foreach($this->Files() as $file) {
|
||||
|
@ -70,6 +80,7 @@ authorised users, the following should be considered:
|
|||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* If a user does not have access to a file, you can still generate the URL but suppress the default
|
||||
permission whitelist by invoking the getter as a method, but pass in a falsey value as a parameter.
|
||||
|
@ -87,9 +98,14 @@ authorised users, the following should be considered:
|
|||
the `revokeFile` method.
|
||||
|
||||
|
||||
:::php
|
||||
class PageController extends ContentController {
|
||||
public function init() {
|
||||
|
||||
```php
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
// Whitelist any protected files on this page for the current user
|
||||
foreach($this->Files() as $file) {
|
||||
|
@ -102,7 +118,7 @@ authorised users, the following should be considered:
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Controlling asset visibility
|
||||
|
||||
|
@ -118,22 +134,25 @@ public facing area.
|
|||
E.g.
|
||||
|
||||
|
||||
:::php
|
||||
$object->MyFile->setFromLocalFile($tmpFile['Path'], $filename, null, null, array(
|
||||
'visibility' => AssetStore::VISIBILITY_PROTECTED
|
||||
));
|
||||
|
||||
```php
|
||||
$object->MyFile->setFromLocalFile($tmpFile['Path'], $filename, null, null, [
|
||||
'visibility' => AssetStore::VISIBILITY_PROTECTED
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
You can also adjust the visibility of any existing file to either public or protected.
|
||||
|
||||
|
||||
:::
|
||||
|
||||
```php
|
||||
// This will make the file available only when a user calls `->grant()`
|
||||
$object->SecretFile->protectFile();
|
||||
|
||||
// This file will be available to everyone with the URL
|
||||
$object->PublicFile->publishFile();
|
||||
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
One thing to note is that all variants of a single file will be treated as
|
||||
|
@ -157,7 +176,7 @@ in the protected store, awaiting publishing.
|
|||
Internally your folder structure would look something like:
|
||||
|
||||
|
||||
:::
|
||||
```
|
||||
assets/
|
||||
.htaccess
|
||||
.protected/
|
||||
|
@ -166,7 +185,7 @@ Internally your folder structure would look something like:
|
|||
NewCompanyLogo.gif
|
||||
33be1b95cb/
|
||||
OldCompanyLogo.gif
|
||||
|
||||
```
|
||||
|
||||
The urls for these two files, however, do not reflect the physical structure directly.
|
||||
|
||||
|
@ -181,15 +200,16 @@ will be moved to `assets/a870de278b/NewCompanyLogo.gif`, and will be served dire
|
|||
the web server, bypassing the need for additional PHP requests.
|
||||
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
$store = singleton(AssetStore::class);
|
||||
$store->publish('NewCompanyLogo.gif', 'a870de278b475cb75f5d9f451439b2d378e13af1');
|
||||
|
||||
```
|
||||
|
||||
After this the filesystem will now look like below:
|
||||
|
||||
|
||||
:::
|
||||
```
|
||||
assets/
|
||||
.htaccess
|
||||
.protected/
|
||||
|
@ -198,7 +218,7 @@ After this the filesystem will now look like below:
|
|||
OldCompanyLogo.gif
|
||||
a870de278b/
|
||||
NewCompanyLogo.gif
|
||||
|
||||
```
|
||||
|
||||
## Performance considerations
|
||||
|
||||
|
@ -234,23 +254,22 @@ root altogether.
|
|||
For instance, given your web root is in the folder `/sites/mysite/www`, you can tell the asset store
|
||||
to put protected files into `/sites/mysite/protected` with the below `.env` setting:
|
||||
|
||||
|
||||
```
|
||||
SS_PROTECTED_ASSETS_PATH="/sites/mysite/protected"
|
||||
|
||||
```
|
||||
|
||||
### Configuring: File types
|
||||
|
||||
In addition to configuring file locations, it's also important to ensure that you have allowed the
|
||||
appropriate file extensions for your instance. This can be done by setting the `File.allowed_extensions`
|
||||
config.
|
||||
```yaml
|
||||
|
||||
|
||||
:::yaml
|
||||
File:
|
||||
allowed_extensions:
|
||||
- 7zip
|
||||
- xzip
|
||||
|
||||
File:
|
||||
allowed_extensions:
|
||||
- 7zip
|
||||
- xzip
|
||||
```
|
||||
|
||||
<div class="warning" markdown="1">
|
||||
Any file not included in this config, or in the default list of extensions, will be blocked from
|
||||
|
@ -268,11 +287,13 @@ When a protected file is served it will also be transmitted with all headers def
|
|||
You can customise this with the below config:
|
||||
|
||||
|
||||
:::yaml
|
||||
|
||||
```yaml
|
||||
|
||||
SilverStripe\Filesystem\Flysystem\FlysystemAssetStore:
|
||||
file_response_headers:
|
||||
Pragma: 'no-cache'
|
||||
|
||||
```
|
||||
|
||||
### Configuring: Archive behaviour
|
||||
|
||||
|
@ -294,22 +315,29 @@ config to true on that class. Note that this feature only works with dataobjects
|
|||
the `Versioned` extension.
|
||||
|
||||
|
||||
:::php
|
||||
class MyVersiondObject extends DataObject {
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyVersiondObject extends DataObject
|
||||
{
|
||||
/** Ensure assets are archived along with the DataObject */
|
||||
private static $keep_archived_assets = true;
|
||||
/** Versioned */
|
||||
private static $extensions = array('Versioned');
|
||||
private static $extensions = ['Versioned'];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The extension can also be globally disabled by removing it at the root level:
|
||||
|
||||
|
||||
:::yaml
|
||||
|
||||
```yaml
|
||||
|
||||
DataObject:
|
||||
AssetControl: null
|
||||
|
||||
```
|
||||
|
||||
### Configuring: Web server settings
|
||||
|
||||
|
@ -330,12 +358,12 @@ In order to ensure that public files are served correctly, you should check that
|
|||
.htaccess bypasses PHP requests for files that do exist. The default template
|
||||
(declared by `Assets_HTAccess.ss`) has the following section, which may be customised in your project:
|
||||
|
||||
|
||||
```
|
||||
# Non existant files passed to requesthandler
|
||||
RewriteCond %{REQUEST_URI} ^(.*)$
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule .* ../framework/main.php?url=%1 [QSA]
|
||||
|
||||
```
|
||||
|
||||
You will need to ensure that your core apache configuration has the necessary `AllowOverride`
|
||||
settings to support the local .htaccess file.
|
||||
|
@ -348,7 +376,7 @@ while ensuring non-existent files are processed via the Framework.
|
|||
|
||||
The default rule for IIS is as below (only partial configuration displayed):
|
||||
|
||||
|
||||
```
|
||||
<rule name="Protected and 404 File rewrite" stopProcessing="true">
|
||||
<match url="^(.*)$" />
|
||||
<conditions>
|
||||
|
@ -356,7 +384,7 @@ The default rule for IIS is as below (only partial configuration displayed):
|
|||
</conditions>
|
||||
<action type="Rewrite" url="../framework/main.php?url={R:1}" appendQueryString="true" />
|
||||
</rule>
|
||||
|
||||
```
|
||||
|
||||
You will need to make sure that the `allowOverride` property of your root web.config is not set
|
||||
to false, to allow these to take effect.
|
||||
|
@ -370,9 +398,9 @@ For instance, this will allow your nginx site to serve files directly, while ens
|
|||
dynamic requests are processed via the Framework:
|
||||
|
||||
|
||||
:::
|
||||
```
|
||||
location ^~ /assets/ {
|
||||
sendfile on;
|
||||
try_files $uri /framework/main.php?url=$uri&$query_string;
|
||||
}
|
||||
|
||||
```
|
||||
|
|
|
@ -19,37 +19,45 @@ a category.
|
|||
|
||||
**mysite/code/Product.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Product extends DataObject {
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Varchar',
|
||||
'ProductCode' => 'Varchar',
|
||||
'Price' => 'Currency'
|
||||
);
|
||||
class Product extends DataObject
|
||||
{
|
||||
|
||||
private static $has_one = array(
|
||||
'Category' => 'Category'
|
||||
);
|
||||
}
|
||||
private static $db = [
|
||||
'Name' => 'Varchar',
|
||||
'ProductCode' => 'Varchar',
|
||||
'Price' => 'Currency'
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'Category' => 'Category'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**mysite/code/Category.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Category extends DataObject {
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Text'
|
||||
);
|
||||
class Category extends DataObject
|
||||
{
|
||||
|
||||
private static $has_many = array(
|
||||
'Products' => 'Product'
|
||||
);
|
||||
}
|
||||
private static $db = [
|
||||
'Title' => 'Text'
|
||||
];
|
||||
|
||||
private static $has_many = [
|
||||
'Products' => 'Product'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To create your own `ModelAdmin`, simply extend the base class, and edit the `$managed_models` property with the list of
|
||||
DataObject's you want to scaffold an interface for. The class can manage multiple models in parallel, if required.
|
||||
|
@ -58,20 +66,24 @@ We'll name it `MyAdmin`, but the class name can be anything you want.
|
|||
|
||||
**mysite/code/MyAdmin.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyAdmin extends ModelAdmin {
|
||||
```php
|
||||
use SilverStripe\Admin\ModelAdmin;
|
||||
|
||||
private static $managed_models = array(
|
||||
'Product',
|
||||
'Category'
|
||||
);
|
||||
class MyAdmin extends ModelAdmin
|
||||
{
|
||||
|
||||
private static $url_segment = 'products';
|
||||
private static $managed_models = [
|
||||
'Product',
|
||||
'Category'
|
||||
];
|
||||
|
||||
private static $menu_title = 'My Product Admin';
|
||||
}
|
||||
private static $url_segment = 'products';
|
||||
|
||||
private static $menu_title = 'My Product Admin';
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This will automatically add a new menu entry to the SilverStripe Admin UI entitled `My Product Admin` and logged in
|
||||
users will be able to upload and manage `Product` and `Category` instances through http://yoursite.com/admin/products.
|
||||
|
@ -96,26 +108,34 @@ permissions by default. For most cases, less restrictive checks make sense, e.g.
|
|||
|
||||
**mysite/code/Category.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Category extends DataObject {
|
||||
// ...
|
||||
public function canView($member = null) {
|
||||
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
|
||||
}
|
||||
```php
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
public function canEdit($member = null) {
|
||||
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
|
||||
}
|
||||
class Category extends DataObject
|
||||
{
|
||||
// ...
|
||||
public function canView($member = null)
|
||||
{
|
||||
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
|
||||
}
|
||||
|
||||
public function canDelete($member = null) {
|
||||
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
|
||||
}
|
||||
public function canEdit($member = null)
|
||||
{
|
||||
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
|
||||
}
|
||||
|
||||
public function canCreate($member = null) {
|
||||
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
|
||||
}
|
||||
public function canDelete($member = null)
|
||||
{
|
||||
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
|
||||
}
|
||||
|
||||
public function canCreate($member = null)
|
||||
{
|
||||
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
|
||||
}
|
||||
```
|
||||
|
||||
## Searching Records
|
||||
|
||||
|
@ -129,16 +149,20 @@ class (see [SearchContext](../search/searchcontext) docs for details).
|
|||
|
||||
**mysite/code/Product.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Product extends DataObject {
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'Name',
|
||||
'ProductCode'
|
||||
);
|
||||
}
|
||||
class Product extends DataObject
|
||||
{
|
||||
|
||||
private static $searchable_fields = [
|
||||
'Name',
|
||||
'ProductCode'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<div class="hint" markdown="1">
|
||||
[SearchContext](../search/searchcontext) documentation has more information on providing the search functionality.
|
||||
|
@ -152,20 +176,24 @@ model class, where you can add or remove columns. To change the title, use [Data
|
|||
|
||||
**mysite/code/Product.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Product extends DataObject {
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
private static $field_labels = array(
|
||||
'Price' => 'Cost' // renames the column to "Cost"
|
||||
);
|
||||
class Product extends DataObject
|
||||
{
|
||||
|
||||
private static $summary_fields = array(
|
||||
'Name',
|
||||
'Price'
|
||||
);
|
||||
}
|
||||
private static $field_labels = [
|
||||
'Price' => 'Cost' // renames the column to "Cost"
|
||||
];
|
||||
|
||||
private static $summary_fields = [
|
||||
'Name',
|
||||
'Price'
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The results list are retrieved from [SearchContext::getResults()](api:SilverStripe\ORM\Search\SearchContext::getResults()), based on the parameters passed through the search
|
||||
form. If no search parameters are given, the results will show every record. Results are a [DataList](api:SilverStripe\ORM\DataList) instance, so
|
||||
|
@ -175,116 +203,138 @@ For example, we might want to exclude all products without prices in our sample
|
|||
|
||||
**mysite/code/MyAdmin.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyAdmin extends ModelAdmin {
|
||||
```php
|
||||
use SilverStripe\Admin\ModelAdmin;
|
||||
|
||||
public function getList() {
|
||||
$list = parent::getList();
|
||||
class MyAdmin extends ModelAdmin
|
||||
{
|
||||
|
||||
// Always limit by model class, in case you're managing multiple
|
||||
if($this->modelClass == 'Product') {
|
||||
$list = $list->exclude('Price', '0');
|
||||
}
|
||||
public function getList()
|
||||
{
|
||||
$list = parent::getList();
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
// Always limit by model class, in case you're managing multiple
|
||||
if($this->modelClass == 'Product') {
|
||||
$list = $list->exclude('Price', '0');
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also customize the search behavior directly on your `ModelAdmin` instance. For example, we might want to have a
|
||||
checkbox which limits search results to expensive products (over $100).
|
||||
|
||||
**mysite/code/MyAdmin.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyAdmin extends ModelAdmin {
|
||||
```php
|
||||
use SilverStripe\Forms\CheckboxField;
|
||||
use SilverStripe\Admin\ModelAdmin;
|
||||
|
||||
public function getSearchContext() {
|
||||
$context = parent::getSearchContext();
|
||||
class MyAdmin extends ModelAdmin
|
||||
{
|
||||
|
||||
if($this->modelClass == 'Product') {
|
||||
$context->getFields()->push(new CheckboxField('q[ExpensiveOnly]', 'Only expensive stuff'));
|
||||
}
|
||||
public function getSearchContext()
|
||||
{
|
||||
$context = parent::getSearchContext();
|
||||
|
||||
return $context;
|
||||
}
|
||||
if($this->modelClass == 'Product') {
|
||||
$context->getFields()->push(new CheckboxField('q[ExpensiveOnly]', 'Only expensive stuff'));
|
||||
}
|
||||
|
||||
public function getList() {
|
||||
$list = parent::getList();
|
||||
return $context;
|
||||
}
|
||||
|
||||
$params = $this->getRequest()->requestVar('q'); // use this to access search parameters
|
||||
public function getList()
|
||||
{
|
||||
$list = parent::getList();
|
||||
|
||||
if($this->modelClass == 'Product' && isset($params['ExpensiveOnly']) && $params['ExpensiveOnly']) {
|
||||
$list = $list->exclude('Price:LessThan', '100');
|
||||
}
|
||||
$params = $this->getRequest()->requestVar('q'); // use this to access search parameters
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
if($this->modelClass == 'Product' && isset($params['ExpensiveOnly']) && $params['ExpensiveOnly']) {
|
||||
$list = $list->exclude('Price:LessThan', '100');
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To alter how the results are displayed (via [GridField](api:SilverStripe\Forms\GridField\GridField)), you can also overload the `getEditForm()` method. For
|
||||
example, to add a new component.
|
||||
|
||||
**mysite/code/MyAdmin.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyAdmin extends ModelAdmin {
|
||||
```php
|
||||
use SilverStripe\Forms\GridField\GridFieldFilterHeader;
|
||||
use SilverStripe\Admin\ModelAdmin;
|
||||
|
||||
private static $managed_models = array(
|
||||
'Product',
|
||||
'Category'
|
||||
);
|
||||
class MyAdmin extends ModelAdmin
|
||||
{
|
||||
|
||||
// ...
|
||||
public function getEditForm($id = null, $fields = null) {
|
||||
$form = parent::getEditForm($id, $fields);
|
||||
private static $managed_models = [
|
||||
'Product',
|
||||
'Category'
|
||||
];
|
||||
|
||||
// $gridFieldName is generated from the ModelClass, eg if the Class 'Product'
|
||||
// is managed by this ModelAdmin, the GridField for it will also be named 'Product'
|
||||
// ...
|
||||
public function getEditForm($id = null, $fields = null)
|
||||
{
|
||||
$form = parent::getEditForm($id, $fields);
|
||||
|
||||
$gridFieldName = $this->sanitiseClassName($this->modelClass);
|
||||
$gridField = $form->Fields()->fieldByName($gridFieldName);
|
||||
// $gridFieldName is generated from the ModelClass, eg if the Class 'Product'
|
||||
// is managed by this ModelAdmin, the GridField for it will also be named 'Product'
|
||||
|
||||
// modify the list view.
|
||||
$gridField->getConfig()->addComponent(new GridFieldFilterHeader());
|
||||
$gridFieldName = $this->sanitiseClassName($this->modelClass);
|
||||
$gridField = $form->Fields()->fieldByName($gridFieldName);
|
||||
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
// modify the list view.
|
||||
$gridField->getConfig()->addComponent(new GridFieldFilterHeader());
|
||||
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The above example will add the component to all `GridField`s (of all managed models). Alternatively we can also add it
|
||||
to only one specific `GridField`:
|
||||
|
||||
**mysite/code/MyAdmin.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyAdmin extends ModelAdmin {
|
||||
```php
|
||||
use SilverStripe\Forms\GridField\GridFieldFilterHeader;
|
||||
use SilverStripe\Admin\ModelAdmin;
|
||||
|
||||
private static $managed_models = array(
|
||||
'Product',
|
||||
'Category'
|
||||
);
|
||||
class MyAdmin extends ModelAdmin
|
||||
{
|
||||
|
||||
public function getEditForm($id = null, $fields = null) {
|
||||
$form = parent::getEditForm($id, $fields);
|
||||
private static $managed_models = [
|
||||
'Product',
|
||||
'Category'
|
||||
];
|
||||
|
||||
$gridFieldName = 'Product';
|
||||
$gridField = $form->Fields()->fieldByName($gridFieldName);
|
||||
public function getEditForm($id = null, $fields = null)
|
||||
{
|
||||
$form = parent::getEditForm($id, $fields);
|
||||
|
||||
if ($gridField) {
|
||||
$gridField->getConfig()->addComponent(new GridFieldFilterHeader());
|
||||
}
|
||||
$gridFieldName = 'Product';
|
||||
$gridField = $form->Fields()->fieldByName($gridFieldName);
|
||||
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
if ($gridField) {
|
||||
$gridField->getConfig()->addComponent(new GridFieldFilterHeader());
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Data Import
|
||||
|
||||
|
@ -302,21 +352,25 @@ This is handled through the [GridFieldExportButton](api:SilverStripe\Forms\GridF
|
|||
|
||||
To customize the exported columns, create a new method called `getExportFields` in your `ModelAdmin`:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyAdmin extends ModelAdmin {
|
||||
// ...
|
||||
```php
|
||||
use SilverStripe\Admin\ModelAdmin;
|
||||
|
||||
public function getExportFields() {
|
||||
return array(
|
||||
'Name' => 'Name',
|
||||
'ProductCode' => 'Product Code',
|
||||
'Category.Title' => 'Category'
|
||||
);
|
||||
}
|
||||
}
|
||||
class MyAdmin extends ModelAdmin
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getExportFields()
|
||||
{
|
||||
return [
|
||||
'Name' => 'Name',
|
||||
'ProductCode' => 'Product Code',
|
||||
'Category.Title' => 'Category'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
|
|
|
@ -54,34 +54,45 @@ coding conventions.
|
|||
|
||||
The CMS interface can be accessed by default through the `admin/` URL. You can change this by setting your own [Director routing rule](director#routing-rules) to the `[AdminRootController](api:SilverStripe\Admin\AdminRootController)` and clear the old rule like in the example below.
|
||||
|
||||
:::yml
|
||||
---
|
||||
Name: myadmin
|
||||
After:
|
||||
- '#adminroutes'
|
||||
---
|
||||
Director:
|
||||
rules:
|
||||
'admin': ''
|
||||
'newAdmin': 'AdminRootController'
|
||||
---
|
||||
|
||||
```yml
|
||||
|
||||
---
|
||||
Name: myadmin
|
||||
After:
|
||||
- '#adminroutes'
|
||||
---
|
||||
Director:
|
||||
rules:
|
||||
'admin': ''
|
||||
'newAdmin': 'AdminRootController'
|
||||
---
|
||||
```
|
||||
|
||||
When extending the CMS or creating modules, you can take advantage of various functions that will return the configured admin URL (by default 'admin/' is returned):
|
||||
|
||||
In PHP you should use:
|
||||
|
||||
:::php
|
||||
AdminRootController::admin_url()
|
||||
|
||||
```php
|
||||
AdminRootController::admin_url()
|
||||
```
|
||||
|
||||
When writing templates use:
|
||||
|
||||
:::ss
|
||||
$AdminURL
|
||||
|
||||
```ss
|
||||
|
||||
$AdminURL
|
||||
```
|
||||
|
||||
And in JavaScript, this is avaible through the `ss` namespace
|
||||
|
||||
:::js
|
||||
ss.config.adminUrl
|
||||
|
||||
```js
|
||||
|
||||
ss.config.adminUrl
|
||||
```
|
||||
|
||||
### Multiple Admin URL and overrides
|
||||
|
||||
|
@ -144,38 +155,53 @@ In order to set the correct layout classes, we also need a custom template.
|
|||
To obey the inheritance chain, we use `$this->getTemplatesWithSuffix('_EditForm')` for
|
||||
selecting the most specific template (so `MyAdmin_EditForm.ss`, if it exists).
|
||||
|
||||
The form should be of type `CMSForm` rather than `Form`, since it allows the use
|
||||
The form should use a `LeftAndMainFormRequestHandler`, since it allows the use
|
||||
of a `PjaxResponseNegotiator` to handle its display.
|
||||
|
||||
Basic example form in a CMS controller subclass:
|
||||
|
||||
:::php
|
||||
class MyAdmin extends LeftAndMain {
|
||||
function getEditForm() {
|
||||
return CMSForm::create(
|
||||
$this,
|
||||
'EditForm',
|
||||
new FieldSet(
|
||||
TabSet::create(
|
||||
'Root',
|
||||
Tab::create('Main',
|
||||
TextField::create('MyText')
|
||||
)
|
||||
)->setTemplate('CMSTabset')
|
||||
),
|
||||
new FieldSet(
|
||||
FormAction::create('doSubmit')
|
||||
)
|
||||
)
|
||||
// JS and CSS use this identifier
|
||||
->setHTMLID('Form_EditForm')
|
||||
// Render correct responses on validation errors
|
||||
->setResponseNegotiator($this->getResponseNegotiator());
|
||||
// Required for correct CMS layout
|
||||
->addExtraClass('cms-edit-form')
|
||||
->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Forms\TabSet;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Tab;
|
||||
use SilverStripe\Forms\TextField;
|
||||
use SilverStripe\Forms\FormAction;
|
||||
use SilverStripe\Admin\LeftAndMain;
|
||||
use SilverStripe\Admin\LeftAndMainFormRequestHandler;
|
||||
|
||||
class MyAdmin extends LeftAndMain
|
||||
{
|
||||
function getEditForm() {
|
||||
return Form::create(
|
||||
$this,
|
||||
'EditForm',
|
||||
new FieldList(
|
||||
TabSet::create(
|
||||
'Root',
|
||||
Tab::create('Main',
|
||||
TextField::create('MyText')
|
||||
)
|
||||
)->setTemplate('CMSTabset')
|
||||
),
|
||||
new FieldList(
|
||||
FormAction::create('doSubmit')
|
||||
)
|
||||
)
|
||||
// Use a custom request handler
|
||||
->setRequestHandler(
|
||||
LeftAndMainFormRequestHandler::create($form)
|
||||
)
|
||||
// JS and CSS use this identifier
|
||||
->setHTMLID('Form_EditForm')
|
||||
// Render correct responses on validation errors
|
||||
->setResponseNegotiator($this->getResponseNegotiator());
|
||||
// Required for correct CMS layout
|
||||
->addExtraClass('cms-edit-form')
|
||||
->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: Usually you don't need to worry about these settings,
|
||||
and will simply call `parent::getEditForm()` to modify an existing,
|
||||
|
@ -280,51 +306,49 @@ routing mechanism for this section. However, there are two major differences:
|
|||
Firstly, `reactRouter` must be passed as a boolean flag to indicate that this section is
|
||||
controlled by the react section, and thus should suppress registration of a page.js route
|
||||
for this section.
|
||||
```php
|
||||
public function getClientConfig()
|
||||
{
|
||||
return array_merge(parent::getClientConfig(), [
|
||||
'reactRouter' => true
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
:::php
|
||||
public function getClientConfig() {
|
||||
return array_merge(parent::getClientConfig(), [
|
||||
'reactRouter' => true
|
||||
]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Secondly, you should ensure that your react CMS section triggers route registration on the client side
|
||||
with the reactRouteRegister component. This will need to be done on the `DOMContentLoaded` event
|
||||
to ensure routes are registered before window.load is invoked.
|
||||
```js
|
||||
|
||||
|
||||
:::js
|
||||
import { withRouter } from 'react-router';
|
||||
import ConfigHelpers from 'lib/Config';
|
||||
import reactRouteRegister from 'lib/ReactRouteRegister';
|
||||
import MyAdmin from './MyAdmin';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const sectionConfig = ConfigHelpers.getSection('MyAdmin');
|
||||
|
||||
reactRouteRegister.add({
|
||||
path: sectionConfig.url,
|
||||
component: withRouter(MyAdminComponent),
|
||||
childRoutes: [
|
||||
{ path: 'form/:id/:view', component: MyAdminComponent },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
import { withRouter } from 'react-router';
|
||||
import ConfigHelpers from 'lib/Config';
|
||||
import reactRouteRegister from 'lib/ReactRouteRegister';
|
||||
import MyAdmin from './MyAdmin';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const sectionConfig = ConfigHelpers.getSection('MyAdmin');
|
||||
|
||||
reactRouteRegister.add({
|
||||
path: sectionConfig.url,
|
||||
component: withRouter(MyAdminComponent),
|
||||
childRoutes: [
|
||||
{ path: 'form/:id/:view', component: MyAdminComponent },
|
||||
],
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Child routes can be registered post-boot by using `ReactRouteRegister` in the same way.
|
||||
```js
|
||||
|
||||
|
||||
:::js
|
||||
// Register a nested url under `sectionConfig.url`
|
||||
const sectionConfig = ConfigHelpers.getSection('MyAdmin');
|
||||
reactRouteRegister.add({
|
||||
path: 'nested',
|
||||
component: NestedComponent,
|
||||
}, [ sectionConfig.url ]);
|
||||
|
||||
// Register a nested url under `sectionConfig.url`
|
||||
const sectionConfig = ConfigHelpers.getSection('MyAdmin');
|
||||
reactRouteRegister.add({
|
||||
path: 'nested',
|
||||
component: NestedComponent,
|
||||
}, [ sectionConfig.url ]);
|
||||
```
|
||||
|
||||
## PJAX: Partial template replacement through Ajax
|
||||
|
||||
|
@ -349,49 +373,60 @@ Example: Create a bare-bones CMS subclass which shows breadcrumbs (a built-in me
|
|||
as well as info on the current record. A single link updates both sections independently
|
||||
in a single Ajax request.
|
||||
|
||||
:::php
|
||||
// mysite/code/MyAdmin.php
|
||||
class MyAdmin extends LeftAndMain {
|
||||
private static $url_segment = 'myadmin';
|
||||
public function getResponseNegotiator() {
|
||||
$negotiator = parent::getResponseNegotiator();
|
||||
$controller = $this;
|
||||
// Register a new callback
|
||||
$negotiator->setCallback('MyRecordInfo', function() use(&$controller) {
|
||||
return $controller->MyRecordInfo();
|
||||
});
|
||||
return $negotiator;
|
||||
}
|
||||
public function MyRecordInfo() {
|
||||
return $this->renderWith('MyRecordInfo');
|
||||
}
|
||||
}
|
||||
|
||||
:::js
|
||||
// MyAdmin.ss
|
||||
<% include SilverStripe\\Admin\\CMSBreadcrumbs %>
|
||||
<div>Static content (not affected by update)</div>
|
||||
<% include MyRecordInfo %>
|
||||
<a href="{$AdminURL}myadmin" class="cms-panel-link" data-pjax-target="MyRecordInfo,Breadcrumbs">
|
||||
Update record info
|
||||
</a>
|
||||
```php
|
||||
use SilverStripe\Admin\LeftAndMain;
|
||||
|
||||
:::ss
|
||||
// MyRecordInfo.ss
|
||||
<div data-pjax-fragment="MyRecordInfo">
|
||||
Current Record: $currentPage.Title
|
||||
</div>
|
||||
// mysite/code/MyAdmin.php
|
||||
class MyAdmin extends LeftAndMain
|
||||
{
|
||||
private static $url_segment = 'myadmin';
|
||||
public function getResponseNegotiator()
|
||||
{
|
||||
$negotiator = parent::getResponseNegotiator();
|
||||
$controller = $this;
|
||||
// Register a new callback
|
||||
$negotiator->setCallback('MyRecordInfo', function() use(&$controller) {
|
||||
return $controller->MyRecordInfo();
|
||||
});
|
||||
return $negotiator;
|
||||
}
|
||||
public function MyRecordInfo()
|
||||
{
|
||||
return $this->renderWith('MyRecordInfo');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// MyAdmin.ss
|
||||
<% include SilverStripe\\Admin\\CMSBreadcrumbs %>
|
||||
<div>Static content (not affected by update)</div>
|
||||
<% include MyRecordInfo %>
|
||||
<a href="{$AdminURL}myadmin" class="cms-panel-link" data-pjax-target="MyRecordInfo,Breadcrumbs">
|
||||
Update record info
|
||||
</a>
|
||||
```
|
||||
|
||||
```ss
|
||||
// MyRecordInfo.ss
|
||||
<div data-pjax-fragment="MyRecordInfo">
|
||||
Current Record: $currentPage.Title
|
||||
</div>
|
||||
```
|
||||
|
||||
A click on the link will cause the following (abbreviated) ajax HTTP request:
|
||||
|
||||
GET /admin/myadmin HTTP/1.1
|
||||
X-Pjax:MyRecordInfo,Breadcrumbs
|
||||
X-Requested-With:XMLHttpRequest
|
||||
|
||||
```
|
||||
GET /admin/myadmin HTTP/1.1
|
||||
X-Pjax:MyRecordInfo,Breadcrumbs
|
||||
X-Requested-With:XMLHttpRequest
|
||||
```
|
||||
... and result in the following response:
|
||||
|
||||
{"MyRecordInfo": "<div...", "CMSBreadcrumbs": "<div..."}
|
||||
|
||||
```
|
||||
{"MyRecordInfo": "<div...", "CMSBreadcrumbs": "<div..."}
|
||||
```
|
||||
Keep in mind that the returned view isn't always decided upon when the Ajax request
|
||||
is fired, so the server might decide to change it based on its own logic,
|
||||
sending back different `X-Pjax` headers and content.
|
||||
|
@ -399,9 +434,9 @@ sending back different `X-Pjax` headers and content.
|
|||
On the client, you can set your preference through the `data-pjax-target` attributes
|
||||
on links or through the `X-Pjax` header. For firing off an Ajax request that is
|
||||
tracked in the browser history, use the `pjax` attribute on the state data.
|
||||
|
||||
$('.cms-container').loadPanel(ss.config.adminUrl+'pages', null, {pjax: 'Content'});
|
||||
|
||||
```js
|
||||
$('.cms-container').loadPanel(ss.config.adminUrl+'pages', null, {pjax: 'Content'});
|
||||
```
|
||||
## Loading lightweight PJAX fragments
|
||||
|
||||
Normal navigation between URLs in the admin section of the Framework occurs through `loadPanel` and `submitForm`.
|
||||
|
@ -415,11 +450,11 @@ unrelated to the main flow.
|
|||
|
||||
In this case you can use the `loadFragment` call supplied by `LeftAndMain.js`. You can trigger as many of these in
|
||||
parallel as you want. This will not disturb the main navigation.
|
||||
|
||||
$('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment1');
|
||||
$('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment2');
|
||||
$('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment3');
|
||||
|
||||
```js
|
||||
$('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment1');
|
||||
$('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment2');
|
||||
$('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment3');
|
||||
```
|
||||
The ongoing requests are tracked by the PJAX fragment name (Fragment1, 2, and 3 above) - resubmission will
|
||||
result in the prior request for this fragment to be aborted. Other parallel requests will continue undisturbed.
|
||||
|
||||
|
@ -435,23 +470,23 @@ has been found on an element (this element will get completely replaced). Afterw
|
|||
will be triggered. In case of a request error a `loadfragmenterror` will be raised and DOM will not be touched.
|
||||
|
||||
You can hook up a response handler that obtains all the details of the XHR request via Entwine handler:
|
||||
|
||||
'from .cms-container': {
|
||||
onafterloadfragment: function(e, data) {
|
||||
// Say 'success'!
|
||||
alert(data.status);
|
||||
}
|
||||
}
|
||||
|
||||
```js
|
||||
'from .cms-container': {
|
||||
onafterloadfragment: function(e, data) {
|
||||
// Say 'success'!
|
||||
alert(data.status);
|
||||
}
|
||||
}
|
||||
```
|
||||
Alternatively you can use the jQuery deferred API:
|
||||
|
||||
$('.cms-container')
|
||||
.loadFragment(ss.config.adminUrl+'foobar/', 'Fragment1')
|
||||
.success(function(data, status, xhr) {
|
||||
// Say 'success'!
|
||||
alert(status);
|
||||
});
|
||||
|
||||
```js
|
||||
$('.cms-container')
|
||||
.loadFragment(ss.config.adminUrl+'foobar/', 'Fragment1')
|
||||
.success(function(data, status, xhr) {
|
||||
// Say 'success'!
|
||||
alert(status);
|
||||
});
|
||||
```
|
||||
## Ajax Redirects
|
||||
|
||||
Sometimes, a server response represents a new URL state, e.g. when submitting an "add record" form,
|
||||
|
@ -471,14 +506,20 @@ For example, the currently used controller class might've changed due to a "redi
|
|||
which affects the currently active menu entry. We're using HTTP response headers to contain this data
|
||||
without affecting the response body.
|
||||
|
||||
:::php
|
||||
class MyController extends LeftAndMain {
|
||||
class myaction() {
|
||||
// ...
|
||||
$this->getResponse()->addHeader('X-Controller', 'MyOtherController');
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Admin\LeftAndMain;
|
||||
|
||||
class MyController extends LeftAndMain
|
||||
{
|
||||
class myaction()
|
||||
{
|
||||
// ...
|
||||
$this->getResponse()->addHeader('X-Controller', 'MyOtherController');
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Built-in headers are:
|
||||
|
||||
|
@ -529,12 +570,15 @@ from "Page" to "Files & Images". To communicate this state change, a controller
|
|||
response has the option to pass along a special HTTP response header,
|
||||
which is picked up by the menu:
|
||||
|
||||
:::php
|
||||
public function mycontrollermethod() {
|
||||
// .. logic here
|
||||
$this->getResponse()->addHeader('X-Controller', 'AssetAdmin');
|
||||
return 'my response';
|
||||
}
|
||||
|
||||
```php
|
||||
public function mycontrollermethod()
|
||||
{
|
||||
// .. logic here
|
||||
$this->getResponse()->addHeader('X-Controller', 'AssetAdmin');
|
||||
return 'my response';
|
||||
}
|
||||
```
|
||||
|
||||
This is usually handled by the existing [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) logic,
|
||||
so you don't need to worry about it. The same concept applies for
|
||||
|
@ -579,47 +623,53 @@ since all others should render with their tab navigation inline.
|
|||
|
||||
Form template with custom tab navigation (trimmed down):
|
||||
|
||||
:::ss
|
||||
<form $FormAttributes data-layout-type="border">
|
||||
|
||||
<div class="cms-content-header north">
|
||||
<% if Fields.hasTabset %>
|
||||
<% with Fields.fieldByName('Root') %>
|
||||
<div class="cms-content-header-tabs">
|
||||
<ul>
|
||||
<% loop Tabs %>
|
||||
<li><a href="#$id">$Title</a></li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end_with %>
|
||||
<% end_if %>
|
||||
</div>
|
||||
```ss
|
||||
|
||||
<div class="cms-content-fields center">
|
||||
<fieldset>
|
||||
<% loop Fields %>$FieldHolder<% end_loop %>
|
||||
</fieldset>
|
||||
</div>
|
||||
<form $FormAttributes data-layout-type="border">
|
||||
|
||||
</form>
|
||||
<div class="cms-content-header north">
|
||||
<% if Fields.hasTabset %>
|
||||
<% with Fields.fieldByName('Root') %>
|
||||
<div class="cms-content-header-tabs">
|
||||
<ul>
|
||||
<% loop Tabs %>
|
||||
<li><a href="#$id">$Title</a></li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end_with %>
|
||||
<% end_if %>
|
||||
</div>
|
||||
|
||||
<div class="cms-content-fields center">
|
||||
<fieldset>
|
||||
<% loop Fields %>$FieldHolder<% end_loop %>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
```
|
||||
|
||||
Tabset template without tab navigation (e.g. `CMSTabset.ss`)
|
||||
|
||||
:::ss
|
||||
<div $AttributesHTML>
|
||||
<% loop Tabs %>
|
||||
<% if Tabs %>
|
||||
$FieldHolder
|
||||
<% else %>
|
||||
<div $AttributesHTML>
|
||||
<% loop Fields %>
|
||||
$FieldHolder
|
||||
<% end_loop %>
|
||||
</div>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
</div>
|
||||
|
||||
```ss
|
||||
|
||||
<div $AttributesHTML>
|
||||
<% loop Tabs %>
|
||||
<% if Tabs %>
|
||||
$FieldHolder
|
||||
<% else %>
|
||||
<div $AttributesHTML>
|
||||
<% loop Fields %>
|
||||
$FieldHolder
|
||||
<% end_loop %>
|
||||
</div>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
</div>
|
||||
```
|
||||
|
||||
Lazy loading works based on the `href` attribute of the tab navigation.
|
||||
The base behaviour is applied through adding a class `.cms-tabset` to a container.
|
||||
|
@ -629,21 +679,24 @@ This is achieved by template conditionals (see "MyActiveCondition").
|
|||
The `.cms-panel-link` class will automatically trigger the ajax loading,
|
||||
and load the HTML content into the main view. Example:
|
||||
|
||||
:::ss
|
||||
<div id="my-tab-id" class="cms-tabset" data-ignore-tab-state="true">
|
||||
<ul>
|
||||
<li class="<% if MyActiveCondition %> ui-tabs-active<% end_if %>">
|
||||
<a href="{$AdminURL}mytabs/tab1" class="cms-panel-link">
|
||||
Tab1
|
||||
</a>
|
||||
</li>
|
||||
<li class="<% if MyActiveCondition %> ui-tabs-active<% end_if %>">
|
||||
<a href="{$AdminURL}mytabs/tab2" class="cms-panel-link">
|
||||
Tab2
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
```ss
|
||||
|
||||
<div id="my-tab-id" class="cms-tabset" data-ignore-tab-state="true">
|
||||
<ul>
|
||||
<li class="<% if MyActiveCondition %> ui-tabs-active<% end_if %>">
|
||||
<a href="{$AdminURL}mytabs/tab1" class="cms-panel-link">
|
||||
Tab1
|
||||
</a>
|
||||
</li>
|
||||
<li class="<% if MyActiveCondition %> ui-tabs-active<% end_if %>">
|
||||
<a href="{$AdminURL}mytabs/tab2" class="cms-panel-link">
|
||||
Tab2
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
The URL endpoints `{$AdminURL}mytabs/tab1` and `{$AdminURL}mytabs/tab2`
|
||||
should return HTML fragments suitable for inserting into the content area,
|
||||
|
|
|
@ -21,8 +21,11 @@ children setting sizes and positions, which in turn requires redrawing of some o
|
|||
|
||||
The easiest way to update the layout of the CMS is to call `redraw` on the top-level `.cms-container` element.
|
||||
|
||||
:::js
|
||||
$('.cms-container').redraw();
|
||||
|
||||
```js
|
||||
|
||||
$('.cms-container').redraw();
|
||||
```
|
||||
|
||||
This causes the framework to:
|
||||
|
||||
|
@ -60,13 +63,16 @@ Call `redraw` on `.cms-container` to re-layout the CMS.
|
|||
Layout manager will automatically apply algorithms to the children of `.cms-container` by inspecting the
|
||||
`data-layout-type` attribute. Let's take the content toolbar as an example of a second-level layout application:
|
||||
|
||||
:::html
|
||||
<div class="cms-content-tools west cms-panel cms-panel-layout"
|
||||
data-expandOnClick="true"
|
||||
data-layout-type="border"
|
||||
id="cms-content-tools-CMSMain">
|
||||
<%-- content utilising border's north, south, east, west and center classes --%>
|
||||
</div>
|
||||
|
||||
```html
|
||||
|
||||
<div class="cms-content-tools west cms-panel cms-panel-layout"
|
||||
data-expandOnClick="true"
|
||||
data-layout-type="border"
|
||||
id="cms-content-tools-CMSMain">
|
||||
<%-- content utilising border's north, south, east, west and center classes --%>
|
||||
</div>
|
||||
```
|
||||
|
||||
For detailed discussion on available algorithms refer to
|
||||
[jLayout algorithms](https://github.com/bramstein/jlayout#layout-algorithms).
|
||||
|
@ -104,8 +110,11 @@ by the algorithm that are initially taken from the `LeftAndMain::LayoutOptions`
|
|||
|
||||
Use provided factory method to generate algorithm instances.
|
||||
|
||||
:::js
|
||||
jLayout.threeColumnCompressor(<column-spec-object>, <options-object>);
|
||||
|
||||
```js
|
||||
|
||||
jLayout.threeColumnCompressor(<column-spec-object>, <options-object>);
|
||||
```
|
||||
|
||||
The parameters are as follows:
|
||||
|
||||
|
|
|
@ -50,36 +50,42 @@ Note how the configuration happens in different entwine namespaces
|
|||
("ss.preview" and "ss"), as well as applies to different selectors
|
||||
(".cms-preview" and ".cms-container").
|
||||
|
||||
:::js
|
||||
(function($) {
|
||||
$.entwine('ss.preview', function($){
|
||||
$('.cms-preview').entwine({
|
||||
DefaultMode: 'content',
|
||||
getSizes: function() {
|
||||
var sizes = this._super();
|
||||
sizes.mobile.width = '400px';
|
||||
return sizes;
|
||||
}
|
||||
});
|
||||
});
|
||||
$.entwine('ss', function($){
|
||||
$('.cms-container').entwine({
|
||||
getLayoutOptions: function() {
|
||||
var opts = this._super();
|
||||
opts.minPreviewWidth = 600;
|
||||
return opts;
|
||||
}
|
||||
});
|
||||
});
|
||||
}(jQuery));
|
||||
|
||||
```js
|
||||
|
||||
(function($) {
|
||||
$.entwine('ss.preview', function($){
|
||||
$('.cms-preview').entwine({
|
||||
DefaultMode: 'content',
|
||||
getSizes: function() {
|
||||
var sizes = this._super();
|
||||
sizes.mobile.width = '400px';
|
||||
return sizes;
|
||||
}
|
||||
});
|
||||
});
|
||||
$.entwine('ss', function($){
|
||||
$('.cms-container').entwine({
|
||||
getLayoutOptions: function() {
|
||||
var opts = this._super();
|
||||
opts.minPreviewWidth = 600;
|
||||
return opts;
|
||||
}
|
||||
});
|
||||
});
|
||||
}(jQuery));
|
||||
```
|
||||
|
||||
Load the file in the CMS via setting adding 'mysite/javascript/MyLeftAndMain.Preview.js'
|
||||
to the `LeftAndMain.extra_requirements_javascript` [configuration value](../configuration)
|
||||
|
||||
:::yml
|
||||
LeftAndMain:
|
||||
extra_requirements_javascript:
|
||||
- mysite/javascript/MyLeftAndMain.Preview.js
|
||||
|
||||
```yml
|
||||
|
||||
LeftAndMain:
|
||||
extra_requirements_javascript:
|
||||
- mysite/javascript/MyLeftAndMain.Preview.js
|
||||
```
|
||||
|
||||
In order to find out which configuration values are available, the source code
|
||||
is your best reference at the moment - have a look in `framework/admin/javascript/src/LeftAndMain.Preview.js`.
|
||||
|
@ -109,8 +115,8 @@ States are the site stages: _live_, _stage_ etc. Preview states are picked up
|
|||
from the `SilverStripeNavigator`. You can invoke the state change by calling:
|
||||
|
||||
```js
|
||||
$('.cms-preview').entwine('.ss.preview').changeState('StageLink');
|
||||
```
|
||||
$('.cms-preview').entwine('.ss.preview').changeState('StageLink');
|
||||
```
|
||||
|
||||
Note the state names come from `SilverStripeNavigatorItems` class names - thus
|
||||
the _Link_ in their names. This call will also redraw the state selector to fit
|
||||
|
@ -120,8 +126,8 @@ list of supported states.
|
|||
You can get the current state by calling:
|
||||
|
||||
```js
|
||||
$('.cms-preview').entwine('.ss.preview').getCurrentStateName();
|
||||
```
|
||||
$('.cms-preview').entwine('.ss.preview').getCurrentStateName();
|
||||
```
|
||||
|
||||
## Preview sizes
|
||||
|
||||
|
@ -139,14 +145,14 @@ has the benefit of redrawing the related selector and maintaining a consistent
|
|||
internal state:
|
||||
|
||||
```js
|
||||
$('.cms-preview').entwine('.ss.preview').changeSize('auto');
|
||||
```
|
||||
$('.cms-preview').entwine('.ss.preview').changeSize('auto');
|
||||
```
|
||||
|
||||
You can find out current size by calling:
|
||||
|
||||
```js
|
||||
$('.cms-preview').entwine('.ss.preview').getCurrentSizeName();
|
||||
```
|
||||
$('.cms-preview').entwine('.ss.preview').getCurrentSizeName();
|
||||
```
|
||||
|
||||
## Preview modes
|
||||
|
||||
|
@ -155,15 +161,15 @@ algorithm, see [layout reference](cms_layout) for more details. You
|
|||
can change modes by calling:
|
||||
|
||||
```js
|
||||
$('.cms-preview').entwine('.ss.preview').changeMode('preview');
|
||||
```
|
||||
$('.cms-preview').entwine('.ss.preview').changeMode('preview');
|
||||
```
|
||||
|
||||
Currently active mode is stored on the `.cms-container` along with related
|
||||
internal states of the layout. You can reach it by calling:
|
||||
|
||||
```js
|
||||
$('.cms-container').entwine('.ss').getLayoutOptions().mode;
|
||||
```
|
||||
$('.cms-container').entwine('.ss').getLayoutOptions().mode;
|
||||
```
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
Caveat: the `.preview-mode-selector` appears twice, once in the preview and
|
||||
|
|
|
@ -6,23 +6,30 @@ summary: Add custom CSS properties to the rich-text editor.
|
|||
SilverStripe lets you customise the style of content in the CMS. This is done by setting up a CSS file called
|
||||
`editor.css` in either your theme or in your `mysite` folder. This is set through
|
||||
|
||||
:::php
|
||||
HtmlEditorConfig::get('cms')->setOption('content_css', project() . '/css/editor.css');
|
||||
|
||||
```php
|
||||
HtmlEditorConfig::get('cms')->setOption('content_css', project() . '/css/editor.css');
|
||||
```
|
||||
|
||||
Will load the `mysite/css/editor.css` file.
|
||||
|
||||
If using this config option in `mysite/_config.php`, you will have to instead call:
|
||||
|
||||
:::php
|
||||
HtmlEditorConfig::get('cms')->setOption('content_css', project() . '/css/editor.css');
|
||||
|
||||
```php
|
||||
HtmlEditorConfig::get('cms')->setOption('content_css', project() . '/css/editor.css');
|
||||
```
|
||||
|
||||
Any CSS classes within this file will be automatically added to the `WYSIWYG` editors 'style' dropdown. For instance, to
|
||||
add the color 'red' as an option within the `WYSIWYG` add the following to the `editor.css`
|
||||
|
||||
:::css
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
```css
|
||||
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
After you have defined the `editor.css` make sure you clear your SilverStripe cache for it to take effect.
|
||||
|
|
|
@ -44,12 +44,15 @@ plugin. See "[How jQuery Works](http://docs.jquery.com/How_jQuery_Works)" for a
|
|||
|
||||
You should write all your custom jQuery code in a closure.
|
||||
|
||||
:::javascript
|
||||
(function($) {
|
||||
$(document).ready(function(){
|
||||
// your code here.
|
||||
})
|
||||
})(jQuery);
|
||||
|
||||
```javascript
|
||||
|
||||
(function($) {
|
||||
$(document).ready(function(){
|
||||
// your code here.
|
||||
})
|
||||
})(jQuery);
|
||||
```
|
||||
|
||||
## jQuery Plugins
|
||||
|
||||
|
@ -70,48 +73,52 @@ development, most importantly:
|
|||
Example: A plugin to highlight a collection of elements with a configurable foreground and background colour
|
||||
(abbreviated example from [learningjquery.com](http://www.learningjquery.com/2007/10/a-plugin-development-pattern)).
|
||||
|
||||
:::js
|
||||
// create closure
|
||||
(function($) {
|
||||
// plugin definition
|
||||
$.fn.hilight = function(options) {
|
||||
// build main options before element iteration
|
||||
var opts = $.extend({}, $.fn.hilight.defaults, options);
|
||||
// iterate and reformat each matched element
|
||||
return this.each(function() {
|
||||
$this = $(this);
|
||||
// build element specific options
|
||||
var o = $.meta ? $.extend({}, opts, $this.data()) : opts;
|
||||
// update element styles
|
||||
$this.css({
|
||||
backgroundColor: o.background,
|
||||
color: o.foreground
|
||||
});
|
||||
});
|
||||
};
|
||||
// plugin defaults
|
||||
$.fn.hilight.defaults = {
|
||||
foreground: "red",
|
||||
background: "yellow"
|
||||
};
|
||||
// end of closure
|
||||
})(jQuery);
|
||||
|
||||
```js
|
||||
|
||||
// create closure
|
||||
(function($) {
|
||||
// plugin definition
|
||||
$.fn.hilight = function(options) {
|
||||
// build main options before element iteration
|
||||
var opts = $.extend({}, $.fn.hilight.defaults, options);
|
||||
// iterate and reformat each matched element
|
||||
return this.each(function() {
|
||||
$this = $(this);
|
||||
// build element specific options
|
||||
var o = $.meta ? $.extend({}, opts, $this.data()) : opts;
|
||||
// update element styles
|
||||
$this.css({
|
||||
backgroundColor: o.background,
|
||||
color: o.foreground
|
||||
});
|
||||
});
|
||||
};
|
||||
// plugin defaults
|
||||
$.fn.hilight.defaults = {
|
||||
foreground: "red",
|
||||
background: "yellow"
|
||||
};
|
||||
// end of closure
|
||||
})(jQuery);
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
:::js
|
||||
(function($) {
|
||||
// Highlight all buttons with default colours
|
||||
jQuery(':button').highlight();
|
||||
|
||||
// Highlight all buttons with green background
|
||||
jQuery(':button').highlight({background: "green"});
|
||||
```js
|
||||
|
||||
// Set all further highlight() calls to have a green background
|
||||
$.fn.hilight.defaults.background = "green";
|
||||
})(jQuery);
|
||||
(function($) {
|
||||
// Highlight all buttons with default colours
|
||||
jQuery(':button').highlight();
|
||||
|
||||
// Highlight all buttons with green background
|
||||
jQuery(':button').highlight({background: "green"});
|
||||
|
||||
// Set all further highlight() calls to have a green background
|
||||
$.fn.hilight.defaults.background = "green";
|
||||
})(jQuery);
|
||||
```
|
||||
|
||||
## jQuery UI Widgets
|
||||
|
||||
|
@ -130,54 +137,58 @@ See the [official developer guide](http://jqueryui.com/docs/Developer_Guide) and
|
|||
|
||||
Example: Highlighter
|
||||
|
||||
:::js
|
||||
(function($) {
|
||||
$.widget("ui.myHighlight", {
|
||||
getBlink: function () {
|
||||
return this._getData('blink');
|
||||
},
|
||||
setBlink: function (blink) {
|
||||
this._setData('blink', blink);
|
||||
if(blink) this.element.wrapInner('<blink></blink>');
|
||||
else this.element.html(this.element.children().html());
|
||||
},
|
||||
_init: function() {
|
||||
// grab the default value and use it
|
||||
this.element.css('background',this.options.background);
|
||||
this.element.css('color',this.options.foreground);
|
||||
this.setBlink(this.options.blink);
|
||||
}
|
||||
});
|
||||
// For demonstration purposes, this is also possible with jQuery.css()
|
||||
$.ui.myHighlight.getter = "getBlink";
|
||||
$.ui.myHighlight.defaults = {
|
||||
foreground: "red",
|
||||
background: "yellow",
|
||||
blink: false
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
```js
|
||||
|
||||
(function($) {
|
||||
$.widget("ui.myHighlight", {
|
||||
getBlink: function () {
|
||||
return this._getData('blink');
|
||||
},
|
||||
setBlink: function (blink) {
|
||||
this._setData('blink', blink);
|
||||
if(blink) this.element.wrapInner('<blink></blink>');
|
||||
else this.element.html(this.element.children().html());
|
||||
},
|
||||
_init: function() {
|
||||
// grab the default value and use it
|
||||
this.element.css('background',this.options.background);
|
||||
this.element.css('color',this.options.foreground);
|
||||
this.setBlink(this.options.blink);
|
||||
}
|
||||
});
|
||||
// For demonstration purposes, this is also possible with jQuery.css()
|
||||
$.ui.myHighlight.getter = "getBlink";
|
||||
$.ui.myHighlight.defaults = {
|
||||
foreground: "red",
|
||||
background: "yellow",
|
||||
blink: false
|
||||
};
|
||||
})(jQuery);
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
:::js
|
||||
(function($) {
|
||||
// call with default options
|
||||
$(':button').myHighlight();
|
||||
|
||||
// call with custom options
|
||||
$(':button').myHighlight({background: "green"});
|
||||
```js
|
||||
|
||||
// set defaults for all future instances
|
||||
$.ui.myHighlight.defaults.background = "green";
|
||||
(function($) {
|
||||
// call with default options
|
||||
$(':button').myHighlight();
|
||||
|
||||
// Adjust property after initialization
|
||||
$(':button').myHighlight('setBlink', true);
|
||||
// call with custom options
|
||||
$(':button').myHighlight({background: "green"});
|
||||
|
||||
// Get property
|
||||
$(':button').myHighlight('getBlink');
|
||||
})(jQuery);
|
||||
// set defaults for all future instances
|
||||
$.ui.myHighlight.defaults.background = "green";
|
||||
|
||||
// Adjust property after initialization
|
||||
$(':button').myHighlight('setBlink', true);
|
||||
|
||||
// Get property
|
||||
$(':button').myHighlight('getBlink');
|
||||
})(jQuery);
|
||||
```
|
||||
|
||||
### jQuery.Entwine
|
||||
|
||||
|
@ -192,33 +203,37 @@ It is also suited for more complex applications beyond a single-purpose plugin.
|
|||
|
||||
Example: Highlighter
|
||||
|
||||
:::js
|
||||
(function($) {
|
||||
$(':button').entwine({
|
||||
Foreground: 'red',
|
||||
Background: 'yellow',
|
||||
highlight: function() {
|
||||
this.css('background', this.getBackground());
|
||||
this.css('color', this.getForeground());
|
||||
}
|
||||
});
|
||||
})(jQuery);
|
||||
|
||||
```js
|
||||
|
||||
(function($) {
|
||||
$(':button').entwine({
|
||||
Foreground: 'red',
|
||||
Background: 'yellow',
|
||||
highlight: function() {
|
||||
this.css('background', this.getBackground());
|
||||
this.css('color', this.getForeground());
|
||||
}
|
||||
});
|
||||
})(jQuery);
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
:::js
|
||||
(function($) {
|
||||
// call with default options
|
||||
$(':button').entwine().highlight();
|
||||
|
||||
// set options for existing and new instances
|
||||
$(':button').entwine().setBackground('green');
|
||||
```js
|
||||
|
||||
// get property
|
||||
$(':button').entwine().getBackground();
|
||||
})(jQuery);
|
||||
(function($) {
|
||||
// call with default options
|
||||
$(':button').entwine().highlight();
|
||||
|
||||
// set options for existing and new instances
|
||||
$(':button').entwine().setBackground('green');
|
||||
|
||||
// get property
|
||||
$(':button').entwine().getBackground();
|
||||
})(jQuery);
|
||||
```
|
||||
|
||||
This is a deliberately simple example, the strength of jQuery.entwine over simple jQuery plugins lies in its public
|
||||
properties, namespacing, as well as its inheritance based on CSS selectors. Please see the [project
|
||||
|
@ -238,13 +253,15 @@ jQuery with a few lines of code. Your jQuery code will normally end up as a ser
|
|||
|
||||
Global properties are evil. They are accessible by other scripts, might be overwritten or misused. A popular case is the `$` shortcut in different libraries: in PrototypeJS it stands for `document.getElementByID()`, in jQuery for `jQuery()`.
|
||||
|
||||
:::js
|
||||
// you can't rely on '$' being defined outside of the closure
|
||||
(function($) {
|
||||
var myPrivateVar; // only available inside the closure
|
||||
// inside here you can use the 'jQuery' object as '$'
|
||||
})(jQuery);
|
||||
|
||||
```js
|
||||
|
||||
// you can't rely on '$' being defined outside of the closure
|
||||
(function($) {
|
||||
var myPrivateVar; // only available inside the closure
|
||||
// inside here you can use the 'jQuery' object as '$'
|
||||
})(jQuery);
|
||||
```
|
||||
|
||||
You can run `[jQuery.noConflict()](http://docs.jquery.com/Core/jQuery.noConflict)` to avoid namespace clashes.
|
||||
NoConflict mode is enabled by default in the SilverStripe CMS javascript.
|
||||
|
@ -254,12 +271,14 @@ NoConflict mode is enabled by default in the SilverStripe CMS javascript.
|
|||
You have to ensure that DOM elements you want to act on are loaded before using them. jQuery provides a wrapper around
|
||||
the `window.onload` and `document.ready` events.
|
||||
|
||||
:::js
|
||||
// DOM elements might not be available here
|
||||
$(document).ready(function() {
|
||||
// The DOM is fully loaded here
|
||||
});
|
||||
|
||||
```js
|
||||
|
||||
// DOM elements might not be available here
|
||||
$(document).ready(function() {
|
||||
// The DOM is fully loaded here
|
||||
});
|
||||
```
|
||||
|
||||
See [jQuery FAQ: Launching Code on Document
|
||||
Ready](http://docs.jquery.com/How_jQuery_Works#Launching_Code_on_Document_Ready).
|
||||
|
@ -273,33 +292,35 @@ Caution: Only applies to certain events, see the [jQuery.on() documentation](htt
|
|||
|
||||
Example: Add a 'loading' classname to all pressed buttons
|
||||
|
||||
:::js
|
||||
// manual binding, only applies to existing elements
|
||||
$('input[[type=submit]]').on('click', function() {
|
||||
$(this).addClass('loading');
|
||||
});
|
||||
|
||||
// binding, applies to any inserted elements as well
|
||||
$('.cms-container').on('click', 'input[[type=submit]]', function() {
|
||||
$(this).addClass('loading');
|
||||
});
|
||||
```js
|
||||
|
||||
// manual binding, only applies to existing elements
|
||||
$('input[[type=submit]]').on('click', function() {
|
||||
$(this).addClass('loading');
|
||||
});
|
||||
|
||||
// binding, applies to any inserted elements as well
|
||||
$('.cms-container').on('click', 'input[[type=submit]]', function() {
|
||||
$(this).addClass('loading');
|
||||
});
|
||||
```
|
||||
|
||||
### Assume Element Collections
|
||||
|
||||
jQuery is based around collections of DOM elements, the library functions typically handle multiple elements (where it
|
||||
makes sense). Encapsulate your code by nesting your jQuery commands inside a `jQuery().each()` call.
|
||||
|
||||
Example: ComplexTableField implements a paginated table with a pop-up for displaying
|
||||
|
||||
:::js
|
||||
$('div.ComplexTableField').each(function() {
|
||||
// This is the over code for the tr elements inside a ComplexTableField.
|
||||
$(this).find('tr').hover(
|
||||
// ...
|
||||
);
|
||||
});
|
||||
```js
|
||||
|
||||
$('div.MyGridField').each(function() {
|
||||
// This is the over code for the tr elements inside a GridField.
|
||||
$(this).find('tr').hover(
|
||||
// ...
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### Use plain HTML and jQuery.data() to store data
|
||||
|
||||
|
@ -310,26 +331,30 @@ Example: Simple form change tracking to prevent submission of unchanged data
|
|||
|
||||
Through CSS properties
|
||||
|
||||
:::js
|
||||
$('form :input').bind('change', function(e) {
|
||||
$(this.form).addClass('isChanged');
|
||||
});
|
||||
$('form').bind('submit', function(e) {
|
||||
if($(this).hasClass('isChanged')) return false;
|
||||
});
|
||||
|
||||
```js
|
||||
|
||||
$('form :input').bind('change', function(e) {
|
||||
$(this.form).addClass('isChanged');
|
||||
});
|
||||
$('form').bind('submit', function(e) {
|
||||
if($(this).hasClass('isChanged')) return false;
|
||||
});
|
||||
```
|
||||
|
||||
Through jQuery.data()
|
||||
|
||||
:::js
|
||||
$('form :input').bind('change', function(e) {
|
||||
$(this.form).data('isChanged', true);
|
||||
});
|
||||
$('form').bind('submit', function(e) {
|
||||
alert($(this).data('isChanged'));
|
||||
if($(this).data('isChanged')) return false;
|
||||
});
|
||||
|
||||
```js
|
||||
|
||||
$('form :input').bind('change', function(e) {
|
||||
$(this.form).data('isChanged', true);
|
||||
});
|
||||
$('form').bind('submit', function(e) {
|
||||
alert($(this).data('isChanged'));
|
||||
if($(this).data('isChanged')) return false;
|
||||
});
|
||||
```
|
||||
|
||||
See [interactive example on jsbin.com](http://jsbin.com/opuva)
|
||||
|
||||
|
@ -339,21 +364,26 @@ rendering a form element through the SilverStripe templating engine.
|
|||
|
||||
Example: Restricted numeric value field
|
||||
|
||||
:::ss
|
||||
<input type="text" class="restricted-text {min:4,max:10}" />
|
||||
|
||||
```ss
|
||||
|
||||
<input type="text" class="restricted-text {min:4,max:10}" />
|
||||
```
|
||||
|
||||
|
||||
:::js
|
||||
$('.restricted-text').bind('change', function(e) {
|
||||
if(
|
||||
e.target.value < $(this).metadata().min
|
||||
|| e.target.value > $(this).metadata().max
|
||||
) {
|
||||
alert('Invalid value');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
```js
|
||||
|
||||
$('.restricted-text').bind('change', function(e) {
|
||||
if(
|
||||
e.target.value < $(this).metadata().min
|
||||
|| e.target.value > $(this).metadata().max
|
||||
) {
|
||||
alert('Invalid value');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
See [interactive example on jsbin.com](http://jsbin.com/axafa)
|
||||
|
||||
|
@ -375,67 +405,77 @@ Example: Autocomplete input field loading page matches through AJAX
|
|||
|
||||
Template:
|
||||
|
||||
:::ss
|
||||
<ul>
|
||||
<% loop $Results %>
|
||||
<li id="Result-$ID">$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
|
||||
```ss
|
||||
|
||||
<ul>
|
||||
<% loop $Results %>
|
||||
<li id="Result-$ID">$Title</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```
|
||||
|
||||
PHP:
|
||||
|
||||
:::php
|
||||
class MyController {
|
||||
public function autocomplete($request) {
|
||||
$results = Page::get()->filter("Title", $request->getVar('title'));
|
||||
if(!$results) return new HTTPResponse("Not found", 404);
|
||||
|
||||
// Use HTTPResponse to pass custom status messages
|
||||
$this->getResponse()->setStatusCode(200, "Found " . $results->Count() . " elements");
|
||||
```php
|
||||
class MyController
|
||||
{
|
||||
public function autocomplete($request)
|
||||
{
|
||||
$results = Page::get()->filter("Title", $request->getVar('title'));
|
||||
if(!$results) return new HTTPResponse("Not found", 404);
|
||||
|
||||
// render all results with a custom template
|
||||
$vd = new ViewableData();
|
||||
return $vd->customise(array(
|
||||
"Results" => $results
|
||||
))->renderWith('AutoComplete');
|
||||
}
|
||||
}
|
||||
// Use HTTPResponse to pass custom status messages
|
||||
$this->getResponse()->setStatusCode(200, "Found " . $results->Count() . " elements");
|
||||
|
||||
// render all results with a custom template
|
||||
$vd = new ViewableData();
|
||||
return $vd->customise([
|
||||
"Results" => $results
|
||||
])->renderWith('AutoComplete');
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
HTML
|
||||
|
||||
:::ss
|
||||
<form action"#">
|
||||
<div class="autocomplete {url:'MyController/autocomplete'}">
|
||||
<input type="text" name="title" />
|
||||
<div class="results" style="display: none;">
|
||||
</div>
|
||||
<input type="submit" value="action_autocomplete" />
|
||||
</form>
|
||||
|
||||
```ss
|
||||
|
||||
<form action"#">
|
||||
<div class="autocomplete {url:'MyController/autocomplete'}">
|
||||
<input type="text" name="title" />
|
||||
<div class="results" style="display: none;">
|
||||
</div>
|
||||
<input type="submit" value="action_autocomplete" />
|
||||
</form>
|
||||
```
|
||||
|
||||
JavaScript:
|
||||
|
||||
:::js
|
||||
$('.autocomplete input').on('change', function() {
|
||||
var resultsEl = $(this).siblings('.results');
|
||||
resultsEl.load(
|
||||
// get form action, using the jQuery.metadata plugin
|
||||
$(this).parent().metadata().url,
|
||||
// submit all form values
|
||||
$(this.form).serialize(),
|
||||
// callback after data is loaded
|
||||
function(data, status) {
|
||||
resultsEl.show();
|
||||
// get all record IDs from the new HTML
|
||||
var ids = jQuery('.results').find('li').map(function() {
|
||||
return $(this).attr('id').replace(/Record\-/,'');
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
```js
|
||||
|
||||
$('.autocomplete input').on('change', function() {
|
||||
var resultsEl = $(this).siblings('.results');
|
||||
resultsEl.load(
|
||||
// get form action, using the jQuery.metadata plugin
|
||||
$(this).parent().metadata().url,
|
||||
// submit all form values
|
||||
$(this.form).serialize(),
|
||||
// callback after data is loaded
|
||||
function(data, status) {
|
||||
resultsEl.show();
|
||||
// get all record IDs from the new HTML
|
||||
var ids = jQuery('.results').find('li').map(function() {
|
||||
return $(this).attr('id').replace(/Record\-/,'');
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
Although they are the minority of cases, there are times when a simple HTML fragment isn't enough. For example, if you
|
||||
have server side code that needs to trigger the update of a couple of elements in the CMS left-hand tree, it would be
|
||||
|
@ -456,22 +496,24 @@ events](http://docs.jquery.com/Namespaced_Events).
|
|||
|
||||
Example: Trigger custom 'validationfailed' event on form submission for each empty element
|
||||
|
||||
:::js
|
||||
$('form').bind('submit', function(e) {
|
||||
// $(this) refers to form
|
||||
$(this).find(':input').each(function() {
|
||||
// $(this) in here refers to input field
|
||||
if(!$(this).val()) $(this).trigger('validationfailed');
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
// listen to custom event on each <input> field
|
||||
$('form :input').bind('validationfailed',function(e) {
|
||||
// $(this) refers to input field
|
||||
alert($(this).attr('name'));
|
||||
});
|
||||
```js
|
||||
|
||||
$('form').bind('submit', function(e) {
|
||||
// $(this) refers to form
|
||||
$(this).find(':input').each(function() {
|
||||
// $(this) in here refers to input field
|
||||
if(!$(this).val()) $(this).trigger('validationfailed');
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
// listen to custom event on each <input> field
|
||||
$('form :input').bind('validationfailed',function(e) {
|
||||
// $(this) refers to input field
|
||||
alert($(this).attr('name'));
|
||||
});
|
||||
```
|
||||
|
||||
See [interactive example on jsbin.com](http://jsbin.com/ipeca).
|
||||
|
||||
|
@ -513,54 +555,56 @@ JSDoc-toolkit is a command line utility, see [usage](http://code.google.com/p/js
|
|||
|
||||
Example: jQuery.entwine
|
||||
|
||||
:::js
|
||||
/**
|
||||
|
||||
* Available Custom Events:
|
||||
* <ul>
|
||||
* <li>ajaxsubmit</li>
|
||||
* <li>validate</li>
|
||||
* <li>reloadeditform</li>
|
||||
* </ul>
|
||||
*
|
||||
* @class Main LeftAndMain interface with some control panel and an edit form.
|
||||
* @name ss.LeftAndMain
|
||||
*/
|
||||
$('.LeftAndMain').entwine('ss', function($){
|
||||
return/** @lends ss.LeftAndMain */ {
|
||||
/**
|
||||
```js
|
||||
|
||||
* Reference to some property
|
||||
* @type Number
|
||||
*/
|
||||
MyProperty: 123,
|
||||
/**
|
||||
|
||||
/**
|
||||
* Available Custom Events:
|
||||
* <ul>
|
||||
* <li>ajaxsubmit</li>
|
||||
* <li>validate</li>
|
||||
* <li>reloadeditform</li>
|
||||
* </ul>
|
||||
*
|
||||
* @class Main LeftAndMain interface with some control panel and an edit form.
|
||||
* @name ss.LeftAndMain
|
||||
*/
|
||||
$('.LeftAndMain').entwine('ss', function($){
|
||||
return/** @lends ss.LeftAndMain */ {
|
||||
/**
|
||||
|
||||
* Renders the provided data into an unordered list.
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {String} status
|
||||
* @return {String} HTML unordered list
|
||||
*/
|
||||
publicMethod: function(data, status) {
|
||||
return '<ul>'
|
||||
+ /...
|
||||
+ '</ul>';
|
||||
},
|
||||
* Reference to some property
|
||||
* @type Number
|
||||
*/
|
||||
MyProperty: 123,
|
||||
|
||||
/**
|
||||
/**
|
||||
|
||||
* Won't show in documentation, but still worth documenting.
|
||||
*
|
||||
* @return {String} Something else.
|
||||
*/
|
||||
_privateMethod: function() {
|
||||
// ...
|
||||
}
|
||||
};
|
||||
]]);
|
||||
* Renders the provided data into an unordered list.
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {String} status
|
||||
* @return {String} HTML unordered list
|
||||
*/
|
||||
publicMethod: function(data, status) {
|
||||
return '<ul>'
|
||||
+ /...
|
||||
+ '</ul>';
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
* Won't show in documentation, but still worth documenting.
|
||||
*
|
||||
* @return {String} Something else.
|
||||
*/
|
||||
_privateMethod: function() {
|
||||
// ...
|
||||
}
|
||||
};
|
||||
]]);
|
||||
```
|
||||
|
||||
### Unit Testing
|
||||
|
||||
|
@ -572,29 +616,31 @@ start with JSpec, as it provides a much more powerful testing framework.
|
|||
|
||||
Example: QUnit test (from [jquery.com](http://docs.jquery.com/QUnit#Using_QUnit)):
|
||||
|
||||
:::js
|
||||
test("a basic test example", function() {
|
||||
ok( true, "this test is fine" );
|
||||
var value = "hello";
|
||||
equals( "hello", value, "We expect value to be hello" );
|
||||
});
|
||||
|
||||
```js
|
||||
|
||||
test("a basic test example", function() {
|
||||
ok( true, "this test is fine" );
|
||||
var value = "hello";
|
||||
equals( "hello", value, "We expect value to be hello" );
|
||||
});
|
||||
```
|
||||
|
||||
Example: JSpec Shopping cart test (from [visionmedia.github.com](http://visionmedia.github.com/jspec/))
|
||||
|
||||
describe 'ShoppingCart'
|
||||
before_each
|
||||
cart = new ShoppingCart
|
||||
end
|
||||
describe 'addProduct'
|
||||
it 'should add a product'
|
||||
cart.addProduct('cookie')
|
||||
cart.addProduct('icecream')
|
||||
cart.should.have 2, 'products'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
```
|
||||
describe 'ShoppingCart'
|
||||
before_each
|
||||
cart = new ShoppingCart
|
||||
end
|
||||
describe 'addProduct'
|
||||
it 'should add a product'
|
||||
cart.addProduct('cookie')
|
||||
cart.addProduct('icecream')
|
||||
cart.should.have 2, 'products'
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
## Related
|
||||
|
||||
* [Unobtrusive Javascript](http://www.onlinetools.org/articles/unobtrusivejavascript/chapter1.html)
|
||||
|
|
|
@ -23,19 +23,19 @@ React's job is to render UI. Its UI elements are known as "components" and repre
|
|||
|
||||
```js
|
||||
<PhotoItem size={200} caption="Angkor Wat" onSelect={openLightbox}>
|
||||
<img src="path/to/image.jpg" />
|
||||
<img src="path/to/image.jpg" />
|
||||
</PhotoItem>
|
||||
```
|
||||
|
||||
Might actually render HTML that looks like this:
|
||||
```html
|
||||
<div class="photo-item">
|
||||
<div class="photo" style="width:200px;height:200px;">
|
||||
<img src="path/to/image.jpg">
|
||||
</div>
|
||||
<div class="photo-caption">
|
||||
<h3><a>Angkor Wat/a></h3>
|
||||
</div>
|
||||
<div class="photo" style="width:200px;height:200px;">
|
||||
<img src="path/to/image.jpg">
|
||||
</div>
|
||||
<div class="photo-caption">
|
||||
<h3><a>Angkor Wat/a></h3>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
|
@ -91,18 +91,18 @@ engine.
|
|||
|
||||
```
|
||||
query GetUser($ID: Int!) {
|
||||
user {
|
||||
name
|
||||
email
|
||||
blogPosts {
|
||||
title
|
||||
comments(Limit: 5) {
|
||||
author
|
||||
comment
|
||||
}
|
||||
}
|
||||
user {
|
||||
name
|
||||
email
|
||||
blogPosts {
|
||||
title
|
||||
comments(Limit: 5) {
|
||||
author
|
||||
comment
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -110,21 +110,21 @@ The above query is almost self-descriptive. It gets a user by ID, returns his or
|
|||
|
||||
```js
|
||||
{
|
||||
"user": {
|
||||
"name": "Test user",
|
||||
"email": "me@example.com",
|
||||
"blogPosts": [
|
||||
{
|
||||
"title": "How to be awesome at GraphQL",
|
||||
"comments": [
|
||||
{
|
||||
"author": "Uncle Cheese",
|
||||
"comment": "Nice stuff, bro"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"user": {
|
||||
"name": "Test user",
|
||||
"email": "me@example.com",
|
||||
"blogPosts": [
|
||||
{
|
||||
"title": "How to be awesome at GraphQL",
|
||||
"comments": [
|
||||
{
|
||||
"author": "Uncle Cheese",
|
||||
"comment": "Nice stuff, bro"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -176,10 +176,10 @@ Where `next()` is the next customisation in the "chain" of middleware. Before in
|
|||
```js
|
||||
import thirdPartyLogger from 'third-party-logger';
|
||||
const addLoggingMiddleware = (next) => (error) => {
|
||||
if (error.type === LoggingService.CRITICAL) {
|
||||
thirdpartyLogger.send(error.message);
|
||||
}
|
||||
return next(error);
|
||||
if (error.type === LoggingService.CRITICAL) {
|
||||
thirdpartyLogger.send(error.message);
|
||||
}
|
||||
return next(error);
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -198,18 +198,18 @@ import LoggingService from './LoggingService';
|
|||
import thirdPartyLogger from 'third-party-logger';
|
||||
|
||||
const addLoggingMiddleware = (next) => (error) => {
|
||||
// Critical errors go to a thirdparty service
|
||||
if (error.type === LoggingService.CRITICAL) {
|
||||
thirdPartyLogger.send(error.message);
|
||||
}
|
||||
// Other errors get logged, but not to our thirdparty
|
||||
else if (error.type === LoggingService.ERROR) {
|
||||
next(error);
|
||||
}
|
||||
// Minor errors are ignored
|
||||
else {
|
||||
// Do nothing!
|
||||
}
|
||||
// Critical errors go to a thirdparty service
|
||||
if (error.type === LoggingService.CRITICAL) {
|
||||
thirdPartyLogger.send(error.message);
|
||||
}
|
||||
// Other errors get logged, but not to our thirdparty
|
||||
else if (error.type === LoggingService.ERROR) {
|
||||
next(error);
|
||||
}
|
||||
// Minor errors are ignored
|
||||
else {
|
||||
// Do nothing!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -253,12 +253,12 @@ __someone-elses-module/js/main.js__
|
|||
|
||||
```js
|
||||
Injector.transform(
|
||||
'my-transformation',
|
||||
(updater) => {
|
||||
updater.component('MyComponent', MyCustomComponent);
|
||||
updater.reducer('myCustom', MyCustomReducer);
|
||||
'my-transformation',
|
||||
(updater) => {
|
||||
updater.component('MyComponent', MyCustomComponent);
|
||||
updater.reducer('myCustom', MyCustomReducer);
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
```
|
||||
|
@ -296,13 +296,13 @@ __my-module/js/main.js__
|
|||
|
||||
```js
|
||||
Injector.transform(
|
||||
'my-transformation',
|
||||
(updater) => {
|
||||
updater.component('MyComponent', MyCustomComponent);
|
||||
updater.reducer('myCustom', MyCustomReducer);
|
||||
'my-transformation',
|
||||
(updater) => {
|
||||
updater.component('MyComponent', MyCustomComponent);
|
||||
updater.reducer('myCustom', MyCustomReducer);
|
||||
|
||||
},
|
||||
{ after: 'another-module' }
|
||||
},
|
||||
{ after: 'another-module' }
|
||||
);
|
||||
|
||||
```
|
||||
|
@ -349,17 +349,17 @@ Likewise, services can be applied for specific contexts.
|
|||
|
||||
```js
|
||||
Injector.transform('my-transform', (updater) => {
|
||||
// Applies to all text fields in AssetAdmin
|
||||
updater.component('TextField.AssetAdmin', MyTextField);
|
||||
// Applies to all text fields in AssetAdmin
|
||||
updater.component('TextField.AssetAdmin', MyTextField);
|
||||
|
||||
// Applies to all text fields in AssetAdmin editform
|
||||
updater.component('TextField.AssetAdmin.FileEditForm', MyTextField);
|
||||
// Applies to all text fields in AssetAdmin editform
|
||||
updater.component('TextField.AssetAdmin.FileEditForm', MyTextField);
|
||||
|
||||
// Applies to any textfield named "Title" in AssetAdmin
|
||||
updater.component('TextField.AssetAdmin.*.Title', MyTextField);
|
||||
// Applies to any textfield named "Title" in AssetAdmin
|
||||
updater.component('TextField.AssetAdmin.*.Title', MyTextField);
|
||||
|
||||
// Applies to any textfield named "Title" in any admin
|
||||
updater.component('TextField.*.*.Title', MyTextField);
|
||||
// Applies to any textfield named "Title" in any admin
|
||||
updater.component('TextField.*.*.Title', MyTextField);
|
||||
})
|
||||
```
|
||||
|
||||
|
@ -379,16 +379,16 @@ Using the `PhotoItem` example above, let's create a customised `PhotoItem` that
|
|||
|
||||
```js
|
||||
const enhancedPhoto = (PhotoItem) => (props) => {
|
||||
const badge = props.isNew ?
|
||||
<div className="badge">New!</div> :
|
||||
null;
|
||||
const badge = props.isNew ?
|
||||
<div className="badge">New!</div> :
|
||||
null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{badge}
|
||||
<PhotoItem {...props} />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{badge}
|
||||
<PhotoItem {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const EnhancedPhotoItem = enhancedPhoto(PhotoItem);
|
||||
|
@ -401,21 +401,21 @@ function.
|
|||
|
||||
```js
|
||||
const enhancedPhoto = (PhotoItem) => {
|
||||
return class EnhancedPhotoItem extends React.Component {
|
||||
render() {
|
||||
const badge = this.props.isNew ?
|
||||
<div className="badge">New!</div> :
|
||||
null;
|
||||
return class EnhancedPhotoItem extends React.Component {
|
||||
render() {
|
||||
const badge = this.props.isNew ?
|
||||
<div className="badge">New!</div> :
|
||||
null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{badge}
|
||||
<PhotoItem {...this.props} />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{badge}
|
||||
<PhotoItem {...this.props} />
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -450,7 +450,9 @@ __my-module/js/components/Gallery.js__
|
|||
import React from 'react';
|
||||
import { inject } from 'lib/Injector';
|
||||
|
||||
class Gallery extends React.Component {
|
||||
class Gallery extends React.Component
|
||||
|
||||
{
|
||||
render() {
|
||||
const { SearchComponent, ItemComponent } = this.props;
|
||||
return (
|
||||
|
@ -482,7 +484,8 @@ declare them in `inject()`. In cases like this, use `withInjector()`. This highe
|
|||
component puts the `Injector` instance in `context`.
|
||||
|
||||
```js
|
||||
class MyGallery extends React.Component {
|
||||
class MyGallery extends React.Component
|
||||
{
|
||||
render () {
|
||||
<div>
|
||||
{this.props.items.map(item => {
|
||||
|
@ -506,17 +509,17 @@ Most behavioural and aesthetic customisations will happen via a mutation of the
|
|||
|
||||
```js
|
||||
Injector.transform(
|
||||
'my-custom-form',
|
||||
(updater) => {
|
||||
updater.form.alterSchema(
|
||||
'AssetAdmin.*',
|
||||
(form) =>
|
||||
'my-custom-form',
|
||||
(updater) => {
|
||||
updater.form.alterSchema(
|
||||
'AssetAdmin.*',
|
||||
(form) =>
|
||||
form.updateField('Title', {
|
||||
myCustomProp: true
|
||||
})
|
||||
.getState()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
|
|
|
@ -26,21 +26,24 @@ state already, so you just need to add the alternate state using two data additi
|
|||
|
||||
Here is the configuration code for the button:
|
||||
|
||||
:::php
|
||||
public function getCMSActions() {
|
||||
$fields = parent::getCMSActions();
|
||||
|
||||
$fields->fieldByName('MajorActions')->push(
|
||||
$cleanupAction = FormAction::create('cleanup', 'Cleaned')
|
||||
// Set up an icon for the neutral state that will use the default text.
|
||||
->setAttribute('data-icon', 'accept')
|
||||
// Initialise the alternate constructive state.
|
||||
->setAttribute('data-icon-alternate', 'addpage')
|
||||
->setAttribute('data-text-alternate', 'Clean-up now')
|
||||
);
|
||||
```php
|
||||
public function getCMSActions()
|
||||
{
|
||||
$fields = parent::getCMSActions();
|
||||
|
||||
return $fields;
|
||||
}
|
||||
$fields->fieldByName('MajorActions')->push(
|
||||
$cleanupAction = FormAction::create('cleanup', 'Cleaned')
|
||||
// Set up an icon for the neutral state that will use the default text.
|
||||
->setAttribute('data-icon', 'accept')
|
||||
// Initialise the alternate constructive state.
|
||||
->setAttribute('data-icon-alternate', 'addpage')
|
||||
->setAttribute('data-text-alternate', 'Clean-up now')
|
||||
);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
```
|
||||
|
||||
You can control the state of the button from the backend by applying `ss-ui-alternate` class to the `FormAction`. To
|
||||
simplify our example, let's assume the button state is controlled on the backend only, but you'd usually be better off
|
||||
|
@ -50,15 +53,18 @@ used for initialisation though.
|
|||
Here we initialise the button based on the backend check, and assume that the button will only update after page reload
|
||||
(or on CMS action).
|
||||
|
||||
:::php
|
||||
public function getCMSActions() {
|
||||
// ...
|
||||
if ($this->needsCleaning()) {
|
||||
// Will initialise the button into alternate state.
|
||||
$cleanupAction->addExtraClass('ss-ui-alternate');
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
```php
|
||||
public function getCMSActions()
|
||||
{
|
||||
// ...
|
||||
if ($this->needsCleaning()) {
|
||||
// Will initialise the button into alternate state.
|
||||
$cleanupAction->addExtraClass('ss-ui-alternate');
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend support
|
||||
|
||||
|
@ -72,41 +78,53 @@ frontend. You can affect the state of the button through the jQuery UI calls.
|
|||
|
||||
First of all, you can toggle the state of the button - execute this code in the browser's console to see how it works.
|
||||
|
||||
:::js
|
||||
jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button('toggleAlternate');
|
||||
|
||||
```js
|
||||
|
||||
jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button('toggleAlternate');
|
||||
```
|
||||
|
||||
Another, more useful, scenario is to check the current state.
|
||||
|
||||
:::js
|
||||
jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button('option', 'showingAlternate');
|
||||
|
||||
```js
|
||||
|
||||
jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button('option', 'showingAlternate');
|
||||
```
|
||||
|
||||
You can also force the button into a specific state by using UI options.
|
||||
|
||||
:::js
|
||||
jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button({showingAlternate: true});
|
||||
|
||||
```js
|
||||
|
||||
jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button({showingAlternate: true});
|
||||
```
|
||||
|
||||
This will allow you to react to user actions in the CMS and give immediate feedback. Here is an example taken from the
|
||||
CMS core that tracks the changes to the input fields and reacts by enabling the *Save* and *Save & publish* buttons
|
||||
(changetracker will automatically add `changed` class to the form if a modification is detected).
|
||||
|
||||
:::js
|
||||
/**
|
||||
* Enable save buttons upon detecting changes to content.
|
||||
* "changed" class is added by jQuery.changetracker.
|
||||
*/
|
||||
$('.cms-edit-form .changed').entwine({
|
||||
// This will execute when the class is added to the element.
|
||||
onmatch: function(e) {
|
||||
var form = this.closest('.cms-edit-form');
|
||||
form.find('#Form_EditForm_action_save').button({showingAlternate: true});
|
||||
form.find('#Form_EditForm_action_publish').button({showingAlternate: true});
|
||||
this._super(e);
|
||||
},
|
||||
// Entwine requires us to define this, even if we don't use it.
|
||||
onunmatch: function(e) {
|
||||
this._super(e);
|
||||
}
|
||||
});
|
||||
|
||||
```js
|
||||
|
||||
/**
|
||||
* Enable save buttons upon detecting changes to content.
|
||||
* "changed" class is added by jQuery.changetracker.
|
||||
*/
|
||||
$('.cms-edit-form .changed').entwine({
|
||||
// This will execute when the class is added to the element.
|
||||
onmatch: function(e) {
|
||||
var form = this.closest('.cms-edit-form');
|
||||
form.find('#Form_EditForm_action_save').button({showingAlternate: true});
|
||||
form.find('#Form_EditForm_action_publish').button({showingAlternate: true});
|
||||
this._super(e);
|
||||
},
|
||||
// Entwine requires us to define this, even if we don't use it.
|
||||
onunmatch: function(e) {
|
||||
this._super(e);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend hooks
|
||||
|
||||
|
@ -136,27 +154,30 @@ disassembled into:
|
|||
Here is the entire handler put together. You don't need to add any separate initialisation code, this will handle all
|
||||
cases.
|
||||
|
||||
:::js
|
||||
(function($) {
|
||||
|
||||
$.entwine('mysite', function($){
|
||||
$('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').entwine({
|
||||
/**
|
||||
* onafterrefreshalternate is SS-specific jQuery UI hook that is executed
|
||||
* every time the button is rendered (including on initialisation).
|
||||
*/
|
||||
onbuttonafterrefreshalternate: function() {
|
||||
if (this.button('option', 'showingAlternate')) {
|
||||
this.addClass('ss-ui-action-constructive');
|
||||
}
|
||||
else {
|
||||
this.removeClass('ss-ui-action-constructive');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
```js
|
||||
|
||||
}(jQuery));
|
||||
(function($) {
|
||||
|
||||
$.entwine('mysite', function($){
|
||||
$('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').entwine({
|
||||
/**
|
||||
* onafterrefreshalternate is SS-specific jQuery UI hook that is executed
|
||||
* every time the button is rendered (including on initialisation).
|
||||
*/
|
||||
onbuttonafterrefreshalternate: function() {
|
||||
if (this.button('option', 'showingAlternate')) {
|
||||
this.addClass('ss-ui-action-constructive');
|
||||
}
|
||||
else {
|
||||
this.removeClass('ss-ui-action-constructive');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}(jQuery));
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
|
|
|
@ -9,17 +9,21 @@ shown alongside the field, a tooltip which shows on demand, or toggleable descri
|
|||
The `FormField->setDescription()` method will add a `<span class="description">`
|
||||
at the last position within the field, and expects unescaped HTML content.
|
||||
|
||||
:::php
|
||||
TextField::create('MyText', 'My Text Label')
|
||||
->setDescription('More <strong>detailed</strong> help');
|
||||
|
||||
```php
|
||||
TextField::create('MyText', 'My Text Label')
|
||||
->setDescription('More <strong>detailed</strong> help');
|
||||
```
|
||||
|
||||
To show the help text as a tooltip instead of inline,
|
||||
add a `.cms-description-tooltip` class.
|
||||
|
||||
:::php
|
||||
TextField::create('MyText', 'My Text Label')
|
||||
->setDescription('More <strong>detailed</strong> help')
|
||||
->addExtraClass('cms-description-tooltip');
|
||||
|
||||
```php
|
||||
TextField::create('MyText', 'My Text Label')
|
||||
->setDescription('More <strong>detailed</strong> help')
|
||||
->addExtraClass('cms-description-tooltip');
|
||||
```
|
||||
|
||||
Tooltips are only supported
|
||||
for native, focusable input elements, which excludes
|
||||
|
@ -34,19 +38,23 @@ Another option you have available is making the field's description togglable. T
|
|||
the UI tidy by hiding the description until the user requests more information
|
||||
by clicking the 'info' icon displayed alongside the field.
|
||||
|
||||
:::php
|
||||
TextField::create('MyText', 'My Text Label')
|
||||
->setDescription('More <strong>detailed</strong> help')
|
||||
->addExtraClass('cms-description-toggle');
|
||||
|
||||
```php
|
||||
TextField::create('MyText', 'My Text Label')
|
||||
->setDescription('More <strong>detailed</strong> help')
|
||||
->addExtraClass('cms-description-toggle');
|
||||
```
|
||||
|
||||
If you want to provide a custom icon for toggling the description, you can do that
|
||||
by setting an additional `RightTitle`.
|
||||
|
||||
:::php
|
||||
TextField::create('MyText', 'My Text Label')
|
||||
->setDescription('More <strong>detailed</strong> help')
|
||||
->addExtraClass('cms-description-toggle')
|
||||
->setRightTitle('<a class="cms-description-trigger">My custom icon</a>');
|
||||
|
||||
```php
|
||||
TextField::create('MyText', 'My Text Label')
|
||||
->setDescription('More <strong>detailed</strong> help')
|
||||
->addExtraClass('cms-description-toggle')
|
||||
->setRightTitle('<a class="cms-description-trigger">My custom icon</a>');
|
||||
```
|
||||
|
||||
Note: For more advanced help text we recommend using
|
||||
[Custom form field templates](/developer_guides/forms/form_templates);
|
||||
|
|
|
@ -20,11 +20,16 @@ First we'll need a custom icon. For this purpose SilverStripe uses 16x16
|
|||
black-and-transparent PNG graphics. In this case we'll place the icon in
|
||||
`mysite/images`, but you are free to use any location.
|
||||
|
||||
:::php
|
||||
class ProductAdmin extends ModelAdmin {
|
||||
// ...
|
||||
private static $menu_icon = 'mysite/images/product-icon.png';
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Admin\ModelAdmin;
|
||||
|
||||
class ProductAdmin extends ModelAdmin
|
||||
{
|
||||
// ...
|
||||
private static $menu_icon = 'mysite/images/product-icon.png';
|
||||
}
|
||||
```
|
||||
|
||||
### Defining a Custom Title
|
||||
|
||||
|
@ -32,11 +37,16 @@ The title of menu entries is configured through the `$menu_title` static.
|
|||
If its not defined, the CMS falls back to using the class name of the
|
||||
controller, removing the "Admin" bit at the end.
|
||||
|
||||
:::php
|
||||
class ProductAdmin extends ModelAdmin {
|
||||
// ...
|
||||
private static $menu_title = 'My Custom Admin';
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Admin\ModelAdmin;
|
||||
|
||||
class ProductAdmin extends ModelAdmin
|
||||
{
|
||||
// ...
|
||||
private static $menu_title = 'My Custom Admin';
|
||||
}
|
||||
```
|
||||
|
||||
In order to localize the menu title in different languages, use the
|
||||
`<classname>.MENUTITLE` entity name, which is automatically created when running
|
||||
|
@ -54,42 +64,49 @@ Google to the menu.
|
|||
First, we need to define a [LeftAndMainExtension](api:SilverStripe\Admin\LeftAndMainExtension) which will contain our
|
||||
button configuration.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class CustomLeftAndMain extends LeftAndMainExtension {
|
||||
```php
|
||||
use SilverStripe\Admin\CMSMenu;
|
||||
use SilverStripe\Admin\LeftAndMainExtension;
|
||||
|
||||
public function init() {
|
||||
// unique identifier for this item. Will have an ID of Menu-$ID
|
||||
$id = 'LinkToGoogle';
|
||||
class CustomLeftAndMain extends LeftAndMainExtension
|
||||
{
|
||||
|
||||
// your 'nice' title
|
||||
$title = 'Google';
|
||||
public function init()
|
||||
{
|
||||
// unique identifier for this item. Will have an ID of Menu-$ID
|
||||
$id = 'LinkToGoogle';
|
||||
|
||||
// the link you want to item to go to
|
||||
$link = 'http://google.com';
|
||||
// your 'nice' title
|
||||
$title = 'Google';
|
||||
|
||||
// priority controls the ordering of the link in the stack. The
|
||||
// lower the number, the lower in the list
|
||||
$priority = -2;
|
||||
// the link you want to item to go to
|
||||
$link = 'http://google.com';
|
||||
|
||||
// Add your own attributes onto the link. In our case, we want to
|
||||
// open the link in a new window (not the original)
|
||||
$attributes = array(
|
||||
'target' => '_blank'
|
||||
);
|
||||
// priority controls the ordering of the link in the stack. The
|
||||
// lower the number, the lower in the list
|
||||
$priority = -2;
|
||||
|
||||
CMSMenu::add_link($id, $title, $link, $priority, $attributes);
|
||||
}
|
||||
}
|
||||
// Add your own attributes onto the link. In our case, we want to
|
||||
// open the link in a new window (not the original)
|
||||
$attributes = [
|
||||
'target' => '_blank'
|
||||
];
|
||||
|
||||
CMSMenu::add_link($id, $title, $link, $priority, $attributes);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To have the link appear, make sure you add the extension to the `LeftAndMain`
|
||||
class. For more information about configuring extensions see the
|
||||
[extensions reference](/developer_guides/extending/extensions).
|
||||
|
||||
:::php
|
||||
LeftAndMain::add_extension('CustomLeftAndMain')
|
||||
|
||||
```php
|
||||
LeftAndMain::add_extension('CustomLeftAndMain')
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
|
|
|
@ -17,18 +17,25 @@ You can use these two classes as a starting point for your customizations.
|
|||
Here's a brief example on how to add sorting and a new column for a
|
||||
hypothetical `NewsPageHolder` type, which contains `NewsPage` children.
|
||||
|
||||
:::php
|
||||
// mysite/code/NewsPageHolder.php
|
||||
class NewsPageHolder extends Page {
|
||||
private static $allowed_children = array('NewsPage');
|
||||
}
|
||||
|
||||
// mysite/code/NewsPage.php
|
||||
class NewsPage extends Page {
|
||||
private static $has_one = array(
|
||||
'Author' => 'Member',
|
||||
);
|
||||
}
|
||||
```php
|
||||
use Page;
|
||||
|
||||
// mysite/code/NewsPageHolder.php
|
||||
class NewsPageHolder extends Page
|
||||
{
|
||||
private static $allowed_children = ['NewsPage'];
|
||||
}
|
||||
|
||||
// mysite/code/NewsPage.php
|
||||
class NewsPage extends Page
|
||||
{
|
||||
private static $has_one = [
|
||||
'Author' => 'Member',
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
We'll now add an `Extension` subclass to `LeftAndMain`, which is the main CMS controller.
|
||||
This allows us to intercept the list building logic, and alter the `GridField`
|
||||
|
@ -36,37 +43,43 @@ before its rendered. In this case, we limit our logic to the desired page type,
|
|||
although it's just as easy to implement changes which apply to all page types,
|
||||
or across page types with common characteristics.
|
||||
|
||||
:::php
|
||||
// mysite/code/NewsPageHolderCMSMainExtension.php
|
||||
class NewsPageHolderCMSMainExtension extends Extension {
|
||||
function updateListView($listView) {
|
||||
$parentId = $listView->getController()->getRequest()->requestVar('ParentID');
|
||||
$parent = ($parentId) ? Page::get()->byId($parentId) : new Page();
|
||||
|
||||
// Only apply logic for this page type
|
||||
if($parent && $parent instanceof NewsPageHolder) {
|
||||
$gridField = $listView->Fields()->dataFieldByName('Page');
|
||||
if($gridField) {
|
||||
// Sort by created
|
||||
$list = $gridField->getList();
|
||||
$gridField->setList($list->sort('Created', 'DESC'));
|
||||
// Add author to columns
|
||||
$cols = $gridField->getConfig()->getComponentByType('GridFieldDataColumns');
|
||||
if($cols) {
|
||||
$fields = $cols->getDisplayFields($gridField);
|
||||
$fields['Author.Title'] = 'Author';
|
||||
$cols->setDisplayFields($fields);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```php
|
||||
use Page;
|
||||
use SilverStripe\Core\Extension;
|
||||
|
||||
// mysite/code/NewsPageHolderCMSMainExtension.php
|
||||
class NewsPageHolderCMSMainExtension extends Extension
|
||||
{
|
||||
function updateListView($listView) {
|
||||
$parentId = $listView->getController()->getRequest()->requestVar('ParentID');
|
||||
$parent = ($parentId) ? Page::get()->byId($parentId) : new Page();
|
||||
|
||||
// Only apply logic for this page type
|
||||
if($parent && $parent instanceof NewsPageHolder) {
|
||||
$gridField = $listView->Fields()->dataFieldByName('Page');
|
||||
if($gridField) {
|
||||
// Sort by created
|
||||
$list = $gridField->getList();
|
||||
$gridField->setList($list->sort('Created', 'DESC'));
|
||||
// Add author to columns
|
||||
$cols = $gridField->getConfig()->getComponentByType('GridFieldDataColumns');
|
||||
if($cols) {
|
||||
$fields = $cols->getDisplayFields($gridField);
|
||||
$fields['Author.Title'] = 'Author';
|
||||
$cols->setDisplayFields($fields);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you just need to enable the extension in your [configuration file](../../configuration).
|
||||
|
||||
// mysite/_config/config.yml
|
||||
LeftAndMain:
|
||||
extensions:
|
||||
- NewsPageHolderCMSMainExtension
|
||||
|
||||
```yml
|
||||
// mysite/_config/config.yml
|
||||
LeftAndMain:
|
||||
extensions:
|
||||
- NewsPageHolderCMSMainExtension
|
||||
```
|
||||
You're all set! Don't forget to flush the caches by appending `?flush=all` to the URL.
|
||||
|
|
|
@ -20,25 +20,28 @@ link that wraps around the node title, a node's id which is given as id attribut
|
|||
tags showing the node status, etc. SilverStripe tree node will be typically rendered into html
|
||||
code like this:
|
||||
|
||||
:::ss
|
||||
...
|
||||
<ul>
|
||||
...
|
||||
<li id="record-15" class="class-Page closed jstree-leaf jstree-unchecked" data-id="15">
|
||||
<ins class="jstree-icon"> </ins>
|
||||
<a class="" title="Page type: Page" href="{$AdminURL}page/edit/show/15">
|
||||
<ins class="jstree-checkbox"> </ins>
|
||||
<ins class="jstree-icon"> </ins>
|
||||
<span class="text">
|
||||
<span class="jstree-pageicon"></span>
|
||||
<span class="item" title="Deleted">New Page</span>
|
||||
<span class="badge deletedonlive">Deleted</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
...
|
||||
</ul>
|
||||
...
|
||||
|
||||
```ss
|
||||
|
||||
...
|
||||
<ul>
|
||||
...
|
||||
<li id="record-15" class="class-Page closed jstree-leaf jstree-unchecked" data-id="15">
|
||||
<ins class="jstree-icon"> </ins>
|
||||
<a class="" title="Page type: Page" href="{$AdminURL}page/edit/show/15">
|
||||
<ins class="jstree-checkbox"> </ins>
|
||||
<ins class="jstree-icon"> </ins>
|
||||
<span class="text">
|
||||
<span class="jstree-pageicon"></span>
|
||||
<span class="item" title="Deleted">New Page</span>
|
||||
<span class="badge deletedonlive">Deleted</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
...
|
||||
</ul>
|
||||
...
|
||||
```
|
||||
|
||||
By applying the proper style sheet, the snippet html above could produce the look of:
|
||||
![Page Node Screenshot](../../../_images/tree_node.png "Page Node")
|
||||
|
@ -61,18 +64,25 @@ will be used for the class attribute of <li> tag of the tree node.
|
|||
### Add new flag
|
||||
__Example: using a subclass__
|
||||
|
||||
:::php
|
||||
class Page extends SiteTree {
|
||||
public function getScheduledToPublish(){
|
||||
// return either true or false
|
||||
}
|
||||
|
||||
public function getStatusFlags($cached = true) {
|
||||
$flags = parent::getStatusFlags($cached);
|
||||
$flags['scheduledtopublish'] = "Scheduled To Publish";
|
||||
return $flags;
|
||||
}
|
||||
}
|
||||
```php
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
|
||||
class Page extends SiteTree
|
||||
{
|
||||
public function getScheduledToPublish()
|
||||
{
|
||||
// return either true or false
|
||||
}
|
||||
|
||||
public function getStatusFlags($cached = true)
|
||||
{
|
||||
$flags = parent::getStatusFlags($cached);
|
||||
$flags['scheduledtopublish'] = "Scheduled To Publish";
|
||||
return $flags;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The above subclass of [SiteTree](api:SilverStripe\CMS\Model\SiteTree) will add a new flag for indicating its
|
||||
__'Scheduled To Publish'__ status. The look of the page node will be changed
|
||||
|
|
|
@ -123,7 +123,7 @@ const CharacterCounter = (TextField) => (props) => {
|
|||
<div>
|
||||
<TextField {...props} />
|
||||
{showWarning &&
|
||||
<small>Characters remaining: {remainingChars}</small>
|
||||
<small>Characters remaining: {remainingChars}</small>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -161,7 +161,8 @@ _my-module/js/src/ConfirmingFormAction.js_
|
|||
import React from 'react';
|
||||
|
||||
export default (FormAction) => {
|
||||
class ConfirmingFormAction extends React.Component {
|
||||
class ConfirmingFormAction extends React.Component
|
||||
{
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { confirming: false };
|
||||
|
|
|
@ -20,7 +20,7 @@ By default the CMS ships with several basic reports:
|
|||
* Broken links report
|
||||
* Pages with no content
|
||||
* Pages edited in the last 2 weeks
|
||||
|
||||
|
||||
Modules may come with their own additional reports.
|
||||
|
||||
## Creating custom reports
|
||||
|
@ -32,36 +32,44 @@ Inside the *mysite/code* folder create a file called *CustomSideReport.php*. Ins
|
|||
|
||||
The following example will create a report to list every page on the current site.
|
||||
|
||||
###CustomSideReport.php
|
||||
###CustomSideReport.php
|
||||
```php
|
||||
use Page;
|
||||
use SilverStripe\Reports\Report;
|
||||
|
||||
class CustomSideReport_NameOfReport extends Report
|
||||
{
|
||||
|
||||
// the name of the report
|
||||
public function title()
|
||||
{
|
||||
return 'All Pages';
|
||||
}
|
||||
|
||||
// what we want the report to return
|
||||
public function sourceRecords($params = null)
|
||||
{
|
||||
return Page::get()->sort('Title');
|
||||
}
|
||||
|
||||
// which fields on that object we want to show
|
||||
public function columns()
|
||||
{
|
||||
$fields = [
|
||||
'Title' => 'Title'
|
||||
];
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
:::php
|
||||
class CustomSideReport_NameOfReport extends SS_Report {
|
||||
|
||||
// the name of the report
|
||||
public function title() {
|
||||
return 'All Pages';
|
||||
}
|
||||
|
||||
// what we want the report to return
|
||||
public function sourceRecords($params = null) {
|
||||
return Page::get()->sort('Title');
|
||||
}
|
||||
|
||||
// which fields on that object we want to show
|
||||
public function columns() {
|
||||
$fields = array(
|
||||
'Title' => 'Title'
|
||||
);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
More useful reports can be created by changing the `DataList` returned in the `sourceRecords` function.
|
||||
|
||||
## Notes
|
||||
|
||||
* `CustomSideReport_ReportName` must extend `SS_Report`
|
||||
* `CustomSideReport_ReportName` must extend `Report`
|
||||
* It is recommended to place all custom reports in the 1 file.
|
||||
* Create a *CustomSideReport.php* file and add classes as you need them inside for each report
|
||||
|
||||
|
|
|
@ -28,18 +28,21 @@ Copy the template markup of the base implementation at `framework/admin/template
|
|||
into `mysite/templates/Includes/LeftAndMain_Menu.ss`. It will automatically be picked up by
|
||||
the CMS logic. Add a new section into the `<ul class="cms-menu-list">`
|
||||
|
||||
:::ss
|
||||
...
|
||||
<ul class="cms-menu-list">
|
||||
<!-- ... -->
|
||||
<li class="bookmarked-link first">
|
||||
<a href="{$AdminURL}pages/edit/show/1">Edit "My popular page"</a>
|
||||
</li>
|
||||
<li class="bookmarked-link last">
|
||||
<a href="{$AdminURL}pages/edit/show/99">Edit "My other page"</a>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
|
||||
```ss
|
||||
|
||||
...
|
||||
<ul class="cms-menu-list">
|
||||
<!-- ... -->
|
||||
<li class="bookmarked-link first">
|
||||
<a href="{$AdminURL}pages/edit/show/1">Edit "My popular page"</a>
|
||||
</li>
|
||||
<li class="bookmarked-link last">
|
||||
<a href="{$AdminURL}pages/edit/show/99">Edit "My other page"</a>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
```
|
||||
|
||||
Refresh the CMS interface with `admin/?flush=all`, and you should see those
|
||||
hardcoded links underneath the left-hand menu. We'll make these dynamic further down.
|
||||
|
@ -51,16 +54,22 @@ we'll add some CSS, and get it to load
|
|||
with the CMS interface. Paste the following content into a new file called
|
||||
`mysite/css/BookmarkedPages.css`:
|
||||
|
||||
:::css
|
||||
.bookmarked-link.first {margin-top: 1em;}
|
||||
|
||||
```css
|
||||
|
||||
.bookmarked-link.first {margin-top: 1em;}
|
||||
```
|
||||
|
||||
Load the new CSS file into the CMS, by setting the `LeftAndMain.extra_requirements_css`
|
||||
[configuration value](../../configuration).
|
||||
|
||||
:::yml
|
||||
LeftAndMain:
|
||||
extra_requirements_css:
|
||||
- mysite/css/BookmarkedPages.css
|
||||
|
||||
```yml
|
||||
|
||||
LeftAndMain:
|
||||
extra_requirements_css:
|
||||
- mysite/css/BookmarkedPages.css
|
||||
```
|
||||
|
||||
## Create a "bookmark" flag on pages
|
||||
|
||||
|
@ -69,28 +78,37 @@ the database. For this we need to decorate the page record with a
|
|||
`DataExtension`. Create a new file called `mysite/code/BookmarkedPageExtension.php`
|
||||
and insert the following code.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class BookmarkedPageExtension extends DataExtension {
|
||||
```php
|
||||
use SilverStripe\Forms\CheckboxField;
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
|
||||
private static $db = array(
|
||||
'IsBookmarked' => 'Boolean'
|
||||
);
|
||||
class BookmarkedPageExtension extends DataExtension
|
||||
{
|
||||
|
||||
public function updateCMSFields(FieldList $fields) {
|
||||
$fields->addFieldToTab('Root.Main',
|
||||
new CheckboxField('IsBookmarked', "Show in CMS bookmarks?")
|
||||
);
|
||||
}
|
||||
}
|
||||
private static $db = [
|
||||
'IsBookmarked' => 'Boolean'
|
||||
];
|
||||
|
||||
public function updateCMSFields(FieldList $fields)
|
||||
{
|
||||
$fields->addFieldToTab('Root.Main',
|
||||
new CheckboxField('IsBookmarked', "Show in CMS bookmarks?")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Enable the extension in your [configuration file](../../configuration)
|
||||
|
||||
:::yml
|
||||
SiteTree:
|
||||
extensions:
|
||||
- BookmarkedPageExtension
|
||||
|
||||
```yml
|
||||
|
||||
SiteTree:
|
||||
extensions:
|
||||
- BookmarkedPageExtension
|
||||
```
|
||||
|
||||
In order to add the field to the database, run a `dev/build/?flush=all`.
|
||||
Refresh the CMS, open a page for editing and you should see the new checkbox.
|
||||
|
@ -104,36 +122,47 @@ links)? Again, we extend a core class: The main CMS controller called
|
|||
|
||||
Add the following code to a new file `mysite/code/BookmarkedLeftAndMainExtension.php`;
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class BookmarkedPagesLeftAndMainExtension extends LeftAndMainExtension {
|
||||
```php
|
||||
use Page;
|
||||
use SilverStripe\Admin\LeftAndMainExtension;
|
||||
|
||||
public function BookmarkedPages() {
|
||||
return Page::get()->filter("IsBookmarked", 1);
|
||||
}
|
||||
}
|
||||
class BookmarkedPagesLeftAndMainExtension extends LeftAndMainExtension
|
||||
{
|
||||
|
||||
public function BookmarkedPages()
|
||||
{
|
||||
return Page::get()->filter("IsBookmarked", 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enable the extension in your [configuration file](../../configuration)
|
||||
|
||||
:::yml
|
||||
LeftAndMain:
|
||||
extensions:
|
||||
- BookmarkedPagesLeftAndMainExtension
|
||||
|
||||
```yml
|
||||
|
||||
LeftAndMain:
|
||||
extensions:
|
||||
- BookmarkedPagesLeftAndMainExtension
|
||||
```
|
||||
|
||||
As the last step, replace the hardcoded links with our list from the database.
|
||||
Find the `<ul>` you created earlier in `mysite/admin/templates/LeftAndMain.ss`
|
||||
and replace it with the following:
|
||||
|
||||
:::ss
|
||||
<ul class="cms-menu-list">
|
||||
<!-- ... -->
|
||||
<% loop $BookmarkedPages %>
|
||||
<li class="bookmarked-link $FirstLast">
|
||||
<li><a href="{$AdminURL}pages/edit/show/$ID">Edit "$Title"</a></li>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
|
||||
```ss
|
||||
|
||||
<ul class="cms-menu-list">
|
||||
<!-- ... -->
|
||||
<% loop $BookmarkedPages %>
|
||||
<li class="bookmarked-link $FirstLast">
|
||||
<li><a href="{$AdminURL}pages/edit/show/$ID">Edit "$Title"</a></li>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
```
|
||||
|
||||
## Extending the CMS actions
|
||||
|
||||
|
@ -165,26 +194,34 @@ First of all we can add a regular standalone button anywhere in the set. Here
|
|||
we are inserting it in the front of all other actions. We could also add a
|
||||
button group (`CompositeField`) in a similar fashion.
|
||||
|
||||
:::php
|
||||
$fields->unshift(FormAction::create('normal', 'Normal button'));
|
||||
|
||||
```php
|
||||
$fields->unshift(FormAction::create('normal', 'Normal button'));
|
||||
```
|
||||
|
||||
We can affect the existing button group by manipulating the `CompositeField`
|
||||
already present in the `FieldList`.
|
||||
|
||||
:::php
|
||||
$fields->fieldByName('MajorActions')->push(FormAction::create('grouped', 'New group button'));
|
||||
|
||||
```php
|
||||
$fields->fieldByName('MajorActions')->push(FormAction::create('grouped', 'New group button'));
|
||||
```
|
||||
|
||||
Another option is adding actions into the drop-up - best place for placing
|
||||
infrequently used minor actions.
|
||||
|
||||
:::php
|
||||
$fields->addFieldToTab('ActionMenus.MoreOptions', FormAction::create('minor', 'Minor action'));
|
||||
|
||||
```php
|
||||
$fields->addFieldToTab('ActionMenus.MoreOptions', FormAction::create('minor', 'Minor action'));
|
||||
```
|
||||
|
||||
We can also easily create new drop-up menus by defining new tabs within the
|
||||
`TabSet`.
|
||||
|
||||
:::php
|
||||
$fields->addFieldToTab('ActionMenus.MyDropUp', FormAction::create('minor', 'Minor action in a new drop-up'));
|
||||
|
||||
```php
|
||||
$fields->addFieldToTab('ActionMenus.MyDropUp', FormAction::create('minor', 'Minor action in a new drop-up'));
|
||||
```
|
||||
|
||||
<div class="hint" markdown='1'>
|
||||
Empty tabs will be automatically removed from the `FieldList` to prevent clutter.
|
||||
|
@ -205,31 +242,42 @@ Your newly created buttons need handlers to bind to before they will do anything
|
|||
To implement these handlers, you will need to create a `LeftAndMainExtension` and add
|
||||
applicable controller actions to it:
|
||||
|
||||
:::php
|
||||
class CustomActionsExtension extends LeftAndMainExtension {
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'sampleAction'
|
||||
);
|
||||
|
||||
public function sampleAction()
|
||||
{
|
||||
// Create the web
|
||||
}
|
||||
|
||||
|
||||
```php
|
||||
use SilverStripe\Admin\LeftAndMainExtension;
|
||||
|
||||
class CustomActionsExtension extends LeftAndMainExtension
|
||||
{
|
||||
|
||||
private static $allowed_actions = [
|
||||
'sampleAction'
|
||||
];
|
||||
|
||||
public function sampleAction()
|
||||
{
|
||||
// Create the web
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
The extension then needs to be registered:
|
||||
|
||||
:::yaml
|
||||
LeftAndMain:
|
||||
extensions:
|
||||
- CustomActionsExtension
|
||||
|
||||
|
||||
```yaml
|
||||
|
||||
LeftAndMain:
|
||||
extensions:
|
||||
- CustomActionsExtension
|
||||
```
|
||||
|
||||
You can now use these handlers with your buttons:
|
||||
|
||||
:::php
|
||||
$fields->push(FormAction::create('sampleAction', 'Perform Sample Action'));
|
||||
|
||||
```php
|
||||
$fields->push(FormAction::create('sampleAction', 'Perform Sample Action'));
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
|
|
|
@ -3,20 +3,29 @@
|
|||
Sometimes you'll work with ModelAdmins from other modules. To customise these interfaces, you can always subclass. But there's
|
||||
also another tool at your disposal: The [Extension](api:SilverStripe\Core\Extension) API.
|
||||
|
||||
:::php
|
||||
class MyAdminExtension extends Extension {
|
||||
// ...
|
||||
public function updateEditForm(&$form) {
|
||||
$form->Fields()->push(/* ... */)
|
||||
}
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\Core\Extension;
|
||||
|
||||
class MyAdminExtension extends Extension
|
||||
{
|
||||
// ...
|
||||
public function updateEditForm(&$form)
|
||||
{
|
||||
$form->Fields()->push(/* ... */)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now enable this extension through your `[config.yml](/topics/configuration)` file.
|
||||
|
||||
:::yml
|
||||
MyAdmin:
|
||||
extensions:
|
||||
- MyAdminExtension
|
||||
|
||||
```yml
|
||||
|
||||
MyAdmin:
|
||||
extensions:
|
||||
- MyAdminExtension
|
||||
```
|
||||
|
||||
The following extension points are available: `updateEditForm()`, `updateSearchContext()`,
|
||||
`updateSearchForm()`, `updateList()`, `updateImportForm`.
|
||||
|
|
|
@ -18,25 +18,34 @@ this defines the actions that need to be executed on a flush request.
|
|||
|
||||
This example uses [Cache](api:Cache) in some custom code, and the same cache is cleaned on flush:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class MyClass extends DataObject implements Flushable {
|
||||
|
||||
public static function flush() {
|
||||
Cache::factory('mycache')->clean(Zend_Cache::CLEANING_MODE_ALL);
|
||||
}
|
||||
|
||||
public function MyCachedContent() {
|
||||
$cache = Cache::factory('mycache')
|
||||
$something = $cache->load('mykey');
|
||||
if(!$something) {
|
||||
$something = 'value to be cached';
|
||||
$cache->save($something, 'mykey');
|
||||
}
|
||||
return $something;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Core\Flushable;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
|
||||
class MyClass extends DataObject implements Flushable
|
||||
{
|
||||
|
||||
public static function flush()
|
||||
{
|
||||
Injector::inst()->get(CacheInterface::class . '.mycache')->clear();
|
||||
}
|
||||
|
||||
public function MyCachedContent()
|
||||
{
|
||||
$cache = Injector::inst()->get(CacheInterface::class . '.mycache')
|
||||
$something = $cache->get('mykey');
|
||||
if(!$something) {
|
||||
$something = 'value to be cached';
|
||||
$cache->set('mykey', $something);
|
||||
}
|
||||
return $something;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Using with filesystem
|
||||
|
||||
|
@ -44,15 +53,19 @@ Another example, some temporary files are created in a directory in assets, and
|
|||
useful in an example like `GD` or `Imagick` generating resampled images, but we want to delete any cached images on
|
||||
flush so they are re-created on demand.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class MyClass extends DataObject implements Flushable {
|
||||
|
||||
public static function flush() {
|
||||
foreach(glob(ASSETS_PATH . '/_tempfiles/*.jpg') as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Core\Flushable;
|
||||
|
||||
class MyClass extends DataObject implements Flushable
|
||||
{
|
||||
|
||||
public static function flush()
|
||||
{
|
||||
foreach(glob(ASSETS_PATH . '/_tempfiles/*.jpg') as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
|
@ -53,9 +53,9 @@ excluded from manifests by creating a blank `_manifest_exclude` file in the modu
|
|||
By default, the finder implementation will exclude any classes stored in files within
|
||||
a `tests/` folder, unless tests are executed.
|
||||
|
||||
## Template Manifest
|
||||
## Theme Manifests
|
||||
|
||||
The [SS_TemplateManifest](api:SS_TemplateManifest) class builds a manifest of all templates present in a directory,
|
||||
The [ThemeManifest](api:SilverStripe\View\ThemeManifest) class builds a manifest of all templates present in a directory,
|
||||
in both modules and themes. Templates in `tests/` folders are automatically excluded.
|
||||
The chapter on [template inheritance](../templates/template_inheritance) provides more details
|
||||
on its operation.
|
||||
|
|
|
@ -8,9 +8,11 @@ which a SilverStripe application must have available in order for requests to be
|
|||
|
||||
This can be accessed in user code via Injector
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
$kernel = Injector::inst()->get(Kernel::class);
|
||||
echo "Current environment: " . $kernel->getEnvironment();
|
||||
```
|
||||
|
||||
## Kernel services
|
||||
|
||||
|
@ -35,7 +37,8 @@ As with Config and Injector the Kernel can be nested to safely modify global app
|
|||
and subsequently restore state. Unlike those classes, however, there is no `::unnest()`. Instead
|
||||
you should call `->activate()` on the kernel instance you would like to unnest to.
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
$oldKernel = Injector::inst()->get(Kernel::class);
|
||||
try {
|
||||
// Injector::inst() / Config::inst() are automatically updated to the new kernel
|
||||
|
@ -46,7 +49,7 @@ you should call `->activate()` on the kernel instance you would like to unnest t
|
|||
// Any changes to config (or other application state) have now been reverted
|
||||
$oldKernel->activate();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
# Application
|
||||
|
||||
|
@ -64,9 +67,9 @@ This class provides basic support for HTTP Middleware, such as [ErrorControlChai
|
|||
|
||||
`main.php` contains the default application implementation.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
|
||||
```php
|
||||
|
||||
use SilverStripe\Control\HTTPApplication;
|
||||
use SilverStripe\Control\HTTPRequestBuilder;
|
||||
use SilverStripe\Core\CoreKernel;
|
||||
|
@ -83,7 +86,7 @@ This class provides basic support for HTTP Middleware, such as [ErrorControlChai
|
|||
$app->addMiddleware(new ErrorControlChainMiddleware($app));
|
||||
$response = $app->handle($request);
|
||||
$response->output();
|
||||
|
||||
```
|
||||
|
||||
Users can customise their own application by coping the above to a file in `mysite/main.php`, and
|
||||
updating their `.htaccess` to point to the new file.
|
||||
|
@ -102,7 +105,9 @@ Note: This config must also be duplicated in the below template which provide as
|
|||
|
||||
`silverstripe-assets/templates/SilverStripe/Assets/Flysystem/PublicAssetAdapter_HTAccess.ss`:
|
||||
|
||||
:::ss
|
||||
|
||||
```ss
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
# ...
|
||||
# Non existant files passed to requesthandler
|
||||
|
@ -110,6 +115,7 @@ Note: This config must also be duplicated in the below template which provide as
|
|||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule .* ../mysite/main.php?url=%1 [QSA]
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
## Custom application actions
|
||||
|
||||
|
@ -124,20 +130,21 @@ For example, the below will setup a request, session, and current controller,
|
|||
but will leave the application in a "ready" state without performing any
|
||||
routing.
|
||||
|
||||
:::php
|
||||
|
||||
```php
|
||||
$request = CLIRequestBuilder::createFromEnvironment();
|
||||
$kernel = new TestKernel(BASE_PATH);
|
||||
$app = new HTTPApplication($kernel);
|
||||
$app->execute($request, function (HTTPRequest $request) {
|
||||
// Start session and execute
|
||||
$request->getSession()->init();
|
||||
|
||||
|
||||
// Set dummy controller
|
||||
$controller = Controller::create();
|
||||
$controller->setRequest($request);
|
||||
$controller->pushCurrent();
|
||||
$controller->doInit();
|
||||
}, true);
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -8,10 +8,11 @@ needs to interface over the command line.
|
|||
|
||||
The main entry point for any command line execution is `framework/cli-script.php`. For example, to run a database
|
||||
rebuild from the command line, use this command:
|
||||
|
||||
:::bash
|
||||
cd your-webroot/
|
||||
php framework/cli-script.php dev/build
|
||||
```bash
|
||||
|
||||
cd your-webroot/
|
||||
php framework/cli-script.php dev/build
|
||||
```
|
||||
|
||||
<div class="notice">
|
||||
Your command line php version is likely to use a different configuration as your webserver (run `php -i` to find out
|
||||
|
@ -32,10 +33,10 @@ when running the command php -v, then you may not have php-cli installed so sake
|
|||
### Installation
|
||||
|
||||
`sake` can be invoked using `./framework/sake`. For easier access, copy the `sake` file into `/usr/bin/sake`.
|
||||
|
||||
cd your-webroot/
|
||||
sudo ./framework/sake installsake
|
||||
|
||||
```
|
||||
cd your-webroot/
|
||||
sudo ./framework/sake installsake
|
||||
```
|
||||
<div class="warning">
|
||||
This currently only works on UNIX like systems, not on Windows.
|
||||
</div>
|
||||
|
@ -56,22 +57,27 @@ SS_BASE_URL="http://localhost/base-url"
|
|||
|
||||
Sake can run any controller by passing the relative URL to that controller.
|
||||
|
||||
:::bash
|
||||
sake /
|
||||
# returns the homepage
|
||||
|
||||
sake dev/
|
||||
# shows a list of development operations
|
||||
```bash
|
||||
|
||||
sake /
|
||||
# returns the homepage
|
||||
|
||||
sake dev/
|
||||
# shows a list of development operations
|
||||
```
|
||||
|
||||
Sake is particularly useful for running build tasks.
|
||||
|
||||
:::bash
|
||||
sake dev/build "flush=1"
|
||||
```bash
|
||||
|
||||
sake dev/build "flush=1"
|
||||
```
|
||||
|
||||
It can also be handy if you have a long running script..
|
||||
|
||||
:::bash
|
||||
sake dev/tasks/MyReallyLongTask
|
||||
```bash
|
||||
|
||||
sake dev/tasks/MyReallyLongTask
|
||||
```
|
||||
|
||||
### Running processes
|
||||
|
||||
|
@ -85,36 +91,41 @@ sleep when the process is in the middle of doing things, and a long sleep when d
|
|||
|
||||
This code provides a good template:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyProcess extends Controller {
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'index'
|
||||
);
|
||||
class MyProcess extends Controller
|
||||
{
|
||||
|
||||
function index() {
|
||||
set_time_limit(0);
|
||||
private static $allowed_actions = [
|
||||
'index'
|
||||
];
|
||||
|
||||
while(memory_get_usage() < 32*1024*1024) {
|
||||
if($this->somethingToDo()) {
|
||||
$this->doSomething();
|
||||
sleep(1)
|
||||
} else {
|
||||
sleep(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function index() {
|
||||
set_time_limit(0);
|
||||
|
||||
while(memory_get_usage() < 32*1024*1024) {
|
||||
if($this->somethingToDo()) {
|
||||
$this->doSomething();
|
||||
sleep(1)
|
||||
} else {
|
||||
sleep(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Then the process can be managed through `sake`
|
||||
|
||||
:::bash
|
||||
sake -start MyProcess
|
||||
sake -stop MyProcess
|
||||
|
||||
```bash
|
||||
|
||||
sake -start MyProcess
|
||||
sake -stop MyProcess
|
||||
```
|
||||
|
||||
<div class="notice">
|
||||
`sake` stores `pid` and log files in the site root directory.
|
||||
|
@ -124,14 +135,20 @@ Then the process can be managed through `sake`
|
|||
|
||||
Parameters can be added to the command. All parameters will be available in `$_GET` array on the server.
|
||||
|
||||
:::bash
|
||||
cd your-webroot/
|
||||
php framework/cli-script.php myurl myparam=1 myotherparam=2
|
||||
|
||||
```bash
|
||||
|
||||
cd your-webroot/
|
||||
php framework/cli-script.php myurl myparam=1 myotherparam=2
|
||||
```
|
||||
|
||||
Or if you're using `sake`
|
||||
|
||||
:::bash
|
||||
sake myurl "myparam=1&myotherparam=2"
|
||||
|
||||
```bash
|
||||
|
||||
sake myurl "myparam=1&myotherparam=2"
|
||||
```
|
||||
|
||||
## Running Regular Tasks With Cron
|
||||
|
||||
|
@ -140,5 +157,6 @@ On a UNIX machine, you can typically run a scheduled task with a [cron job](http
|
|||
|
||||
The following will run `MyTask` every minute.
|
||||
|
||||
:::bash
|
||||
* * * * * /your/site/folder/sake dev/tasks/MyTask
|
||||
```bash
|
||||
* * * * * /your/site/folder/sake dev/tasks/MyTask
|
||||
```
|
|
@ -13,30 +13,35 @@ the [Cookie](api:SilverStripe\Control\Cookie) class. This class mostly follows t
|
|||
|
||||
Sets the value of cookie with configuration.
|
||||
|
||||
:::php
|
||||
Cookie::set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = false);
|
||||
|
||||
// Cookie::set('MyApplicationPreference', 'Yes');
|
||||
```php
|
||||
Cookie::set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = false);
|
||||
|
||||
// Cookie::set('MyApplicationPreference', 'Yes');
|
||||
```
|
||||
|
||||
### get
|
||||
|
||||
Returns the value of cookie.
|
||||
|
||||
:::php
|
||||
Cookie::get($name);
|
||||
|
||||
// Cookie::get('MyApplicationPreference');
|
||||
// returns 'Yes'
|
||||
```php
|
||||
Cookie::get($name);
|
||||
|
||||
// Cookie::get('MyApplicationPreference');
|
||||
// returns 'Yes'
|
||||
```
|
||||
|
||||
### force_expiry
|
||||
|
||||
Clears a given cookie.
|
||||
|
||||
:::php
|
||||
Cookie::force_expiry($name, $path = null, $domain = null);
|
||||
|
||||
// Cookie::force_expiry('MyApplicationPreference')
|
||||
```php
|
||||
Cookie::force_expiry($name, $path = null, $domain = null);
|
||||
|
||||
// Cookie::force_expiry('MyApplicationPreference')
|
||||
```
|
||||
|
||||
## Cookie_Backend
|
||||
|
||||
|
@ -47,16 +52,19 @@ that fetches, sets and expires cookies. By default we use a [CookieJar](api:Silv
|
|||
The [CookieJar](api:SilverStripe\Control\CookieJar) keeps track of cookies that have been set by the current process as well as those that were received
|
||||
from the browser.
|
||||
|
||||
:::php
|
||||
$myCookies = array(
|
||||
'cookie1' => 'value1',
|
||||
);
|
||||
|
||||
$newBackend = new CookieJar($myCookies);
|
||||
```php
|
||||
$myCookies = [
|
||||
'cookie1' => 'value1',
|
||||
];
|
||||
|
||||
Injector::inst()->registerService($newBackend, 'Cookie_Backend');
|
||||
$newBackend = new CookieJar($myCookies);
|
||||
|
||||
Cookie::get('cookie1');
|
||||
Injector::inst()->registerService($newBackend, 'Cookie_Backend');
|
||||
|
||||
Cookie::get('cookie1');
|
||||
|
||||
```
|
||||
|
||||
### Resetting the Cookie_Backend state
|
||||
|
||||
|
@ -64,35 +72,40 @@ Assuming that your application hasn't messed around with the `$_COOKIE` superglo
|
|||
`Cookie_Backend` by simply unregistering the `CookieJar` service with `Injector`. Next time you access `Cookie` it'll
|
||||
create a new service for you using the `$_COOKIE` superglobal.
|
||||
|
||||
:::php
|
||||
Injector::inst()->unregisterNamedObject('Cookie_Backend');
|
||||
|
||||
Cookie::get('cookiename'); // will return $_COOKIE['cookiename'] if set
|
||||
```php
|
||||
Injector::inst()->unregisterNamedObject('Cookie_Backend');
|
||||
|
||||
Cookie::get('cookiename'); // will return $_COOKIE['cookiename'] if set
|
||||
```
|
||||
|
||||
Alternatively, if you know that the superglobal has been changed (or you aren't sure it hasn't) you can attempt to use
|
||||
the current `CookieJar` service to tell you what it was like when it was registered.
|
||||
|
||||
:::php
|
||||
//store the cookies that were loaded into the `CookieJar`
|
||||
$recievedCookie = Cookie::get_inst()->getAll(false);
|
||||
|
||||
//set a new `CookieJar`
|
||||
Injector::inst()->registerService(new CookieJar($recievedCookie), 'CookieJar');
|
||||
```php
|
||||
//store the cookies that were loaded into the `CookieJar`
|
||||
$recievedCookie = Cookie::get_inst()->getAll(false);
|
||||
|
||||
//set a new `CookieJar`
|
||||
Injector::inst()->registerService(new CookieJar($recievedCookie), 'CookieJar');
|
||||
```
|
||||
|
||||
### Using your own Cookie_Backend
|
||||
|
||||
If you need to implement your own Cookie_Backend you can use the injector system to force a different class to be used.
|
||||
|
||||
:::yml
|
||||
---
|
||||
Name: mycookie
|
||||
After: '#cookie'
|
||||
---
|
||||
Injector:
|
||||
Cookie_Backend:
|
||||
class: MyCookieJar
|
||||
|
||||
```yml
|
||||
|
||||
---
|
||||
Name: mycookie
|
||||
After: '#cookie'
|
||||
---
|
||||
Injector:
|
||||
Cookie_Backend:
|
||||
class: MyCookieJar
|
||||
```
|
||||
|
||||
To be a valid backend your class must implement the [Cookie_Backend](api:SilverStripe\Control\Cookie_Backend) interface.
|
||||
|
||||
|
@ -105,23 +118,26 @@ came from the browser as part of the request.
|
|||
|
||||
Using the `Cookie_Backend` we can do this like such:
|
||||
|
||||
:::php
|
||||
Cookie::set('CookieName', 'CookieVal');
|
||||
|
||||
Cookie::get('CookieName'); //gets the cookie as we set it
|
||||
```php
|
||||
Cookie::set('CookieName', 'CookieVal');
|
||||
|
||||
//will return the cookie as it was when it was sent in the request
|
||||
Cookie::get('CookieName', false);
|
||||
Cookie::get('CookieName'); //gets the cookie as we set it
|
||||
|
||||
//will return the cookie as it was when it was sent in the request
|
||||
Cookie::get('CookieName', false);
|
||||
```
|
||||
|
||||
### Accessing all the cookies at once
|
||||
|
||||
One can also access all of the cookies in one go using the `Cookie_Backend`
|
||||
|
||||
:::php
|
||||
Cookie::get_inst()->getAll(); //returns all the cookies including ones set during the current process
|
||||
|
||||
Cookie::get_inst()->getAll(false); //returns all the cookies in the request
|
||||
```php
|
||||
Cookie::get_inst()->getAll(); //returns all the cookies including ones set during the current process
|
||||
|
||||
Cookie::get_inst()->getAll(false); //returns all the cookies in the request
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
|
|
|
@ -9,67 +9,103 @@ information and security tokens.
|
|||
In order to support things like testing, the session is associated with a particular Controller. In normal usage,
|
||||
this is loaded from and saved to the regular PHP session, but for things like static-page-generation and
|
||||
unit-testing, you can create multiple Controllers, each with their own session.
|
||||
|
||||
|
||||
## Getting the session instance
|
||||
|
||||
If you're in a controller, the `Session` object will be bound to the `HTTPRequest` for your controller.
|
||||
|
||||
```php
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class MyController extends Controller
|
||||
{
|
||||
public function MySession()
|
||||
{
|
||||
return $this->getRequest()->getSession();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Otherwise, if you're not in a controller, get the request as a service.
|
||||
|
||||
```php
|
||||
$request = Injector::inst()->get(HTTPRequest::class);
|
||||
$session = $request->getSession();
|
||||
```
|
||||
|
||||
## set
|
||||
|
||||
:::php
|
||||
Session::set('MyValue', 6);
|
||||
|
||||
```php
|
||||
$session->set('MyValue', 6);
|
||||
```
|
||||
|
||||
Saves the value of to session data. You can also save arrays or serialized objects in session (but note there may be
|
||||
size restrictions as to how much you can save).
|
||||
|
||||
:::php
|
||||
// saves an array
|
||||
Session::set('MyArrayOfValues', array('1','2','3'));
|
||||
|
||||
// saves an object (you'll have to unserialize it back)
|
||||
$object = new Object();
|
||||
Session::set('MyObject', serialize($object));
|
||||
```php
|
||||
// saves an array
|
||||
$session->set('MyArrayOfValues', ['1','2','3']);
|
||||
|
||||
// saves an object (you'll have to unserialize it back)
|
||||
$object = new Object();
|
||||
$session->set('MyObject', serialize($object));
|
||||
|
||||
```
|
||||
|
||||
|
||||
## get
|
||||
|
||||
Once you have saved a value to the Session you can access it by using the `get` function. Like the `set` function you
|
||||
can use this anywhere in your PHP files.
|
||||
|
||||
:::php
|
||||
echo Session::get('MyValue');
|
||||
// returns 6
|
||||
|
||||
$data = Session::get('MyArrayOfValues');
|
||||
// $data = array(1,2,3)
|
||||
```php
|
||||
echo $session->get('MyValue');
|
||||
// returns 6
|
||||
|
||||
$object = unserialize(Session::get('MyObject', $object));
|
||||
// $object = Object()
|
||||
$data = $session->get('MyArrayOfValues');
|
||||
// $data = array(1,2,3)
|
||||
|
||||
$object = unserialize($session->get('MyObject', $object));
|
||||
// $object = Object()
|
||||
|
||||
```
|
||||
|
||||
## get_all
|
||||
|
||||
You can also get all the values in the session at once. This is useful for debugging.
|
||||
|
||||
:::php
|
||||
Session::get_all();
|
||||
// returns an array of all the session values.
|
||||
```php
|
||||
$session->getAll();
|
||||
// returns an array of all the session values.
|
||||
|
||||
```
|
||||
|
||||
## clear
|
||||
|
||||
Once you have accessed a value from the Session it doesn't automatically wipe the value from the Session, you have
|
||||
to specifically remove it.
|
||||
|
||||
:::php
|
||||
Session::clear('MyValue');
|
||||
to specifically remove it.
|
||||
```php
|
||||
$session->clear('MyValue');
|
||||
```
|
||||
|
||||
Or you can clear every single value in the session at once. Note SilverStripe stores some of its own session data
|
||||
including form and page comment information. None of this is vital but `clear_all` will clear everything.
|
||||
|
||||
:::php
|
||||
Session::clear_all();
|
||||
```php
|
||||
$session->clearAll();
|
||||
```
|
||||
|
||||
## Secure Session Cookie
|
||||
|
||||
In certain circumstances, you may want to use a different `session_name` cookie when using the `https` protocol for security purposes. To do this, you may set the `cookie_secure` parameter to `true` on your `config.yml`
|
||||
|
||||
:::yml
|
||||
Session:
|
||||
cookie_secure: true
|
||||
|
||||
```yml
|
||||
|
||||
SilverStripe\Control\Session:
|
||||
cookie_secure: true
|
||||
```
|
||||
|
||||
This uses the session_name `SECSESSID` for `https` connections instead of the default `PHPSESSID`. Doing so adds an extra layer of security to your session cookie since you no longer share `http` and `https` sessions.
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@ For projects managed through Composer, update the version number of `framework`
|
|||
|
||||
```json
|
||||
"require": {
|
||||
"silverstripe/framework": "^4.0",
|
||||
"silverstripe/cms": "^4.0"
|
||||
"silverstripe/framework": "^4.0",
|
||||
"silverstripe/cms": "^4.0"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -114,19 +114,19 @@ specifies "these are arguments to the controller".
|
|||
|
||||
In other words, change this:
|
||||
|
||||
:::php
|
||||
```php
|
||||
Director::addRules(50, array(
|
||||
'admin/ImageEditor/$Action' => 'ImageEditor',
|
||||
));
|
||||
|
||||
```
|
||||
|
||||
To this:
|
||||
|
||||
:::php
|
||||
```php
|
||||
Director::addRules(50, array(
|
||||
'admin/ImageEditor//$Action' => 'ImageEditor',
|
||||
));
|
||||
|
||||
```
|
||||
|
||||
|
||||
### Decorators
|
||||
|
@ -213,8 +213,9 @@ In most cases this won't have any effect on existing forms, although you might g
|
|||
like Validator.js and behaviour.js. If you want to disable JavaScript validation across forms, add the following to your
|
||||
_config.php:
|
||||
|
||||
:::php
|
||||
```php
|
||||
Validator::set_javascript_validation_handler('none');
|
||||
```
|
||||
|
||||
See http://open.silverstripe.com/changeset/69688
|
||||
|
||||
|
|
|
@ -73,8 +73,9 @@ can access them. In particular the following static variables have been changed
|
|||
Because of this, you will need to change the static definitions in your ModelAdmin subclasses. For example, you should
|
||||
change this:
|
||||
|
||||
:::php
|
||||
class MyCatalogAdmin extends ModelAdmin {
|
||||
```php
|
||||
class MyCatalogAdmin extends ModelAdmin
|
||||
{
|
||||
|
||||
protected static $managed_models = array(
|
||||
'Product',
|
||||
|
@ -83,12 +84,13 @@ change this:
|
|||
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To this:
|
||||
|
||||
:::php
|
||||
class MyCatalogAdmin extends ModelAdmin {
|
||||
```php
|
||||
class MyCatalogAdmin extends ModelAdmin
|
||||
{
|
||||
|
||||
public static $managed_models = array(
|
||||
'Product',
|
||||
|
@ -97,7 +99,7 @@ To this:
|
|||
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
#### Deprecated Translatable::enable()
|
||||
|
|
|
@ -129,9 +129,9 @@ post](http://www.silverstripe.org/preview-of-silverstripe-2-4-hierarchical-urls-
|
|||
You can enable it manually for existing websites. Existing URLs will automatically change to the nested format without
|
||||
republication (your old URLs should redirect automatically).
|
||||
|
||||
:::php
|
||||
```php
|
||||
SiteTree::enable_nested_urls();
|
||||
|
||||
```
|
||||
|
||||
### SiteTree->Link() instead of SiteTree->URLSegment
|
||||
|
||||
|
@ -188,7 +188,8 @@ getter. You need to manually call *setOwner($this)* before using the instance.
|
|||
Base setup:
|
||||
|
||||
:::php
|
||||
class MyExtension extends Extension {
|
||||
class MyExtension extends Extension
|
||||
{
|
||||
function myExtensionMethod() { // ... }
|
||||
}
|
||||
Object::add_extension('MyObject', 'MyExtension');
|
||||
|
@ -197,7 +198,8 @@ Base setup:
|
|||
Wrong:
|
||||
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
function myExtensionMethod() {
|
||||
$ext = $this->extension_instances['MyExtension'];
|
||||
return $ext->myExtensionMethod();
|
||||
|
@ -208,7 +210,8 @@ Wrong:
|
|||
Right:
|
||||
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
function myExtensionMethod() {
|
||||
$ext = $this->getExtensionInstance('MyExtension');
|
||||
$ext->setOwner($this);
|
||||
|
|
|
@ -127,7 +127,8 @@ To clarify: Leaving existing decorators unchanged might mean that you allow acti
|
|||
|
||||
// 2.4.0
|
||||
:::php
|
||||
class MyDecorator extends DataObjectDecorator {
|
||||
class MyDecorator extends DataObjectDecorator
|
||||
{
|
||||
function canEdit($member) {
|
||||
if(Permission::checkMember($member, 'MYPERMISSION')) {
|
||||
return true;
|
||||
|
@ -139,7 +140,8 @@ To clarify: Leaving existing decorators unchanged might mean that you allow acti
|
|||
|
||||
// 2.4.1
|
||||
:::php
|
||||
class MyDecorator extends DataObjectDecorator {
|
||||
class MyDecorator extends DataObjectDecorator
|
||||
{
|
||||
function canEdit($member) {
|
||||
if(Permission::checkMember($member, 'MYPERMISSION')) {
|
||||
return null; // Means the permission check will be ignored, instead of forced to TRUE
|
||||
|
|
|
@ -48,7 +48,8 @@ parameters (//$data, $form// instead of *$request*).
|
|||
|
||||
:::php
|
||||
// Form field actions
|
||||
class MyFormField extends FormField {
|
||||
class MyFormField extends FormField
|
||||
{
|
||||
|
||||
// Form fields always have a reference to their form.
|
||||
// Use the form-specific token instance.
|
||||
|
@ -61,7 +62,8 @@ parameters (//$data, $form// instead of *$request*).
|
|||
}
|
||||
|
||||
// Controller actions (GET and POST) without form
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
// Manually adds token to link
|
||||
function DeleteLink() {
|
||||
|
@ -84,7 +86,8 @@ parameters (//$data, $form// instead of *$request*).
|
|||
}
|
||||
|
||||
// Controller actions (GET and POST) with form
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
// Forms have CSRF protection turned on by default,
|
||||
// will add a HiddenField instance called "SecurityID"
|
||||
|
@ -160,7 +163,8 @@ parameters don't break through string concatenation.
|
|||
Full controller example:
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
|
||||
function export($request) {
|
||||
// ...
|
||||
|
@ -189,7 +193,8 @@ mandatory "SecurityID" GET parameter appended to the base link.
|
|||
You can manually enable security tokens, either globally or for a specific form.
|
||||
|
||||
:::php
|
||||
class MyTest extends SapphireTest {
|
||||
class MyTest extends SapphireTest
|
||||
{
|
||||
|
||||
// option 1: enable for all forms created through this test
|
||||
function setUp() {
|
||||
|
|
|
@ -143,7 +143,8 @@ If you need custom logic, e.g. checking for a class before applying the statics
|
|||
you can use `add_to_class()` as a replacement to `extraStatics()`.
|
||||
|
||||
:::php
|
||||
class MyExtension extends Extension {
|
||||
class MyExtension extends Extension
|
||||
{
|
||||
|
||||
// before
|
||||
function extraStatics($class, $extensionClass) {
|
||||
|
@ -162,7 +163,8 @@ you can use `add_to_class()` as a replacement to `extraStatics()`.
|
|||
);
|
||||
|
||||
// advanced syntax
|
||||
static function add_to_class($class, $extensionClass, $args = null) {
|
||||
static function add_to_class($class, $extensionClass, $args = null)
|
||||
{
|
||||
if($class == 'MyClass') {
|
||||
Config::inst()->update($class, 'db', array(
|
||||
'Title' => 'Varchar'
|
||||
|
|
|
@ -21,9 +21,11 @@ and doesn't check if the user is authenticated to view it. As with any other con
|
|||
please use `DataObject->canView()` to determine permissions.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
private static $allowed_actions = array('showpage');
|
||||
public function showpage($request) {
|
||||
public function showpage($request)
|
||||
{
|
||||
$page = Page::get()->byID($request->param('ID'));
|
||||
if(!$page->canView()) return $this->httpError(401);
|
||||
// continue with authenticated logic...
|
||||
|
|
|
@ -75,10 +75,12 @@ Before:
|
|||
|
||||
:::php
|
||||
<?php
|
||||
class Page extends SiteTree {
|
||||
class Page extends SiteTree
|
||||
{
|
||||
static $db = array('MyVar' => 'Text');
|
||||
}
|
||||
class Page_Controller extends ContentController {
|
||||
class Page_Controller extends ContentController
|
||||
{
|
||||
static $allowed_actions = array('myaction');
|
||||
}
|
||||
|
||||
|
@ -86,10 +88,12 @@ After:
|
|||
|
||||
:::php
|
||||
<?php
|
||||
class Page extends SiteTree {
|
||||
class Page extends SiteTree
|
||||
{
|
||||
private static $db = array('MyVar' => 'Text');
|
||||
}
|
||||
class Page_Controller extends ContentController {
|
||||
class Page_Controller extends ContentController
|
||||
{
|
||||
private static $allowed_actions = array('myaction');
|
||||
}
|
||||
|
||||
|
@ -116,7 +120,8 @@ as such. This can either be done by returning an HTMLText object, like:
|
|||
or by defining the casting of the accessor method, like:
|
||||
|
||||
:::php
|
||||
class Page extends SiteTree {
|
||||
class Page extends SiteTree
|
||||
{
|
||||
private static $casting = array(
|
||||
'MyDiv' => 'HTMLText'
|
||||
)
|
||||
|
@ -242,9 +247,11 @@ understand, the routing will require definition of `$allowed_actions`
|
|||
on your own `Controller` subclasses if they contain any actions accessible through URLs.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
// This action is now denied because no $allowed_actions are specified
|
||||
public function myaction($request) {}
|
||||
public function myaction($request)
|
||||
{}
|
||||
}
|
||||
|
||||
You can overwrite the default behaviour on undefined `$allowed_actions` to allow all actions,
|
||||
|
@ -263,12 +270,15 @@ particularly around inherited rules. We've decided to remove the feature,
|
|||
you'll need to specificy each accessible action individually.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
public static $allowed_actions = array('*' => 'ADMIN');
|
||||
// Always denied because not explicitly listed in $allowed_actions
|
||||
public function myaction($request) {}
|
||||
public function myaction($request)
|
||||
{}
|
||||
// Always denied because not explicitly listed in $allowed_actions
|
||||
public function myotheraction($request) {}
|
||||
public function myotheraction($request)
|
||||
{}
|
||||
}
|
||||
|
||||
Please review all rules governing allowed actions in the
|
||||
|
@ -281,11 +291,14 @@ to methods defined on the class they're also defined on.
|
|||
Overriding inherited access definitions is no longer possible.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
public static $allowed_actions = array('myaction' => 'ADMIN');
|
||||
public function myaction($request) {}
|
||||
public function myaction($request)
|
||||
{}
|
||||
}
|
||||
class MySubController extends MyController {
|
||||
class MySubController extends MyController
|
||||
{
|
||||
// No longer works
|
||||
public static $allowed_actions = array('myaction' => 'CMS_ACCESS_CMSMAIN');
|
||||
}
|
||||
|
@ -298,8 +311,10 @@ can only grant or deny access or methods they define themselves.
|
|||
New approach with the [Config API](/developer_guides/configuration/configuration)
|
||||
|
||||
:::php
|
||||
class MySubController extends MyController {
|
||||
public function init() {
|
||||
class MySubController extends MyController
|
||||
{
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
|
||||
Config::inst()->update('MyController', 'allowed_actions',
|
||||
|
@ -321,7 +336,8 @@ If you have previously added, removed or altered built-in CMS actions in any way
|
|||
you'll need to adjust your code.
|
||||
|
||||
:::php
|
||||
class MyPage extends Page {
|
||||
class MyPage extends Page
|
||||
{
|
||||
function getCMSActions() {
|
||||
$actions = parent::getCMSActions();
|
||||
|
||||
|
@ -363,17 +379,22 @@ Since `GridField` is used in `ModelAdmin`, this change will affect both classes.
|
|||
Example: Require "CMS: Pages section" access
|
||||
|
||||
:::php
|
||||
class MyModel extends DataObject {
|
||||
public function canView($member = null) {
|
||||
class MyModel extends DataObject
|
||||
{
|
||||
public function canView($member = null)
|
||||
{
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
public function canEdit($member = null) {
|
||||
public function canEdit($member = null)
|
||||
{
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
public function canDelete($member = null) {
|
||||
public function canDelete($member = null)
|
||||
{
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
public function canCreate($member = null) {
|
||||
public function canCreate($member = null)
|
||||
{
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
|
||||
|
|
|
@ -33,8 +33,10 @@ E.g.
|
|||
|
||||
:::php
|
||||
<?php
|
||||
class SingletonPage extends Page {
|
||||
public function canCreate($member) {
|
||||
class SingletonPage extends Page
|
||||
{
|
||||
public function canCreate($member)
|
||||
{
|
||||
if(static::get()->count()) return false;
|
||||
|
||||
$context = func_num_args() > 1 ? func_get_arg(1) : array();
|
||||
|
|
|
@ -13,8 +13,10 @@ For example:
|
|||
|
||||
|
||||
:::php
|
||||
class MyCustomValidator extends Validator {
|
||||
public function php($data) {
|
||||
class MyCustomValidator extends Validator
|
||||
{
|
||||
public function php($data)
|
||||
{
|
||||
$this->validationError(
|
||||
'EmailAddress',
|
||||
DBField::create_field('HTMLText', "Invalid email. Please sign up at <a href='signup'>this page</a>")
|
||||
|
|
|
@ -8,14 +8,17 @@ login forms or other changes. To re-enable this, you'll need to use the `Injecto
|
|||
Define a login form:
|
||||
|
||||
```php
|
||||
class CustomLoginForm extends MemberLoginForm {
|
||||
use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
|
||||
|
||||
public function __construct($controller, $name, $fields = null, $actions = null, $checkCurrentUser = true)
|
||||
{
|
||||
parent::__construct($controller, $name, $fields, $actions, $checkCurrentUser);
|
||||
class CustomLoginForm extends MemberLoginForm
|
||||
{
|
||||
|
||||
$this->disableSecurityToken();
|
||||
}
|
||||
public function __construct($controller, $name, $fields = null, $actions = null, $checkCurrentUser = true)
|
||||
{
|
||||
parent::__construct($controller, $name, $fields, $actions, $checkCurrentUser);
|
||||
|
||||
$this->disableSecurityToken();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
|
|
@ -31,9 +31,11 @@ and doesn't check if the user is authenticated to view it. As with any other con
|
|||
please use `DataObject->canView()` to determine permissions.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
private static $allowed_actions = array('showpage');
|
||||
public function showpage($request) {
|
||||
public function showpage($request)
|
||||
{
|
||||
$page = Page::get()->byID($request->param('ID'));
|
||||
if(!$page->canView()) return $this->httpError(401);
|
||||
// continue with authenticated logic...
|
||||
|
|
|
@ -15,8 +15,10 @@ E.g.
|
|||
|
||||
:::php
|
||||
<?php
|
||||
class FileSecurityExtension extends DataExtension {
|
||||
public function canEdit($member) {
|
||||
class FileSecurityExtension extends DataExtension
|
||||
{
|
||||
public function canEdit($member)
|
||||
{
|
||||
return Permission::checkMember($member, 'MyCustomPermission');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -308,9 +308,11 @@ simply use the provided page URLs.
|
|||
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
static $allowed_actions = array('myaction');
|
||||
public function myaction($request) {
|
||||
public function myaction($request)
|
||||
{
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
@ -500,7 +502,8 @@ of strings. This format is inherently unsafe, and should be avoided where possib
|
|||
|
||||
:::php
|
||||
<?php
|
||||
public function augmentSQL(SQLQuery &$query) {
|
||||
public function augmentSQL(SQLQuery &$query)
|
||||
{
|
||||
$query->getWhere(); // Will be flattened (unsafe 3.1 compatible format)
|
||||
$expression = $query->toAppropriateExpression(); // Either SQLSelect or SQLDelete
|
||||
$expression->getWhere(); // Will be parameterised (preferred 3.2 compatible format)
|
||||
|
@ -731,7 +734,8 @@ Before:
|
|||
|
||||
|
||||
:::php
|
||||
public function filtersOnColumn($query, $column) {
|
||||
public function filtersOnColumn($query, $column)
|
||||
{
|
||||
$regexp = '/^(.*\.)?("|`)?' . $column . ' ("|`)?\s?=/';
|
||||
foreach($this->getWhere() as $predicate) {
|
||||
if(preg_match($regexp, $predicate)) return true;
|
||||
|
@ -744,7 +748,8 @@ After:
|
|||
|
||||
|
||||
:::php
|
||||
public function filtersOnColumn($query, $column) {
|
||||
public function filtersOnColumn($query, $column)
|
||||
{
|
||||
$regexp = '/^(.*\.)?("|`)?' . $column . ' ("|`)?\s?=/';
|
||||
foreach($this->getWhereParameterised($parameters) as $predicate) {
|
||||
if(preg_match($regexp, $predicate)) return true;
|
||||
|
|
|
@ -13,8 +13,10 @@ For example:
|
|||
|
||||
|
||||
:::php
|
||||
class MyCustomValidator extends Validator {
|
||||
public function php($data) {
|
||||
class MyCustomValidator extends Validator
|
||||
{
|
||||
public function php($data)
|
||||
{
|
||||
$this->validationError(
|
||||
'EmailAddress',
|
||||
DBField::create_field('HTMLText', "Invalid email. Please sign up at <a href='signup'>this page</a>")
|
||||
|
|
|
@ -8,14 +8,17 @@ login forms or other changes. To re-enable this, you'll need to use the `Injecto
|
|||
Define a login form:
|
||||
|
||||
```php
|
||||
class CustomLoginForm extends MemberLoginForm {
|
||||
use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
|
||||
|
||||
public function __construct($controller, $name, $fields = null, $actions = null, $checkCurrentUser = true)
|
||||
{
|
||||
parent::__construct($controller, $name, $fields, $actions, $checkCurrentUser);
|
||||
class CustomLoginForm extends MemberLoginForm
|
||||
{
|
||||
|
||||
$this->disableSecurityToken();
|
||||
}
|
||||
public function __construct($controller, $name, $fields = null, $actions = null, $checkCurrentUser = true)
|
||||
{
|
||||
parent::__construct($controller, $name, $fields, $actions, $checkCurrentUser);
|
||||
|
||||
$this->disableSecurityToken();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
|
|
@ -8,14 +8,17 @@ login forms or other changes. To re-enable this, you'll need to use the `Injecto
|
|||
Define a login form:
|
||||
|
||||
```php
|
||||
class CustomLoginForm extends MemberLoginForm {
|
||||
use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
|
||||
|
||||
public function __construct($controller, $name, $fields = null, $actions = null, $checkCurrentUser = true)
|
||||
{
|
||||
parent::__construct($controller, $name, $fields, $actions, $checkCurrentUser);
|
||||
class CustomLoginForm extends MemberLoginForm
|
||||
{
|
||||
|
||||
$this->disableSecurityToken();
|
||||
}
|
||||
public function __construct($controller, $name, $fields = null, $actions = null, $checkCurrentUser = true)
|
||||
{
|
||||
parent::__construct($controller, $name, $fields, $actions, $checkCurrentUser);
|
||||
|
||||
$this->disableSecurityToken();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
|
|
@ -116,7 +116,7 @@ For example, the below shows how you would update a query with a hard-coded tabl
|
|||
```diff
|
||||
public function countDuplicates($model, $fieldToCheck)
|
||||
{
|
||||
$query = new SilverStripe\ORM\Queries\SQLSelect();
|
||||
$query = new SilverStripe\ORM\Queries\SQLSelect();
|
||||
+ $table = SilverStripe\ORM\DataObject::getSchema()->tableForField($model, $field);
|
||||
- $query->setFrom("\"{$model}\"");
|
||||
+ $query->setFrom("\"{$table}\"");
|
||||
|
@ -377,7 +377,6 @@ For example, if you have the below `_ss_environment.php` file, your `.env` would
|
|||
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Environment
|
||||
define('SS_ENVIRONMENT_TYPE', 'dev');
|
||||
define('SS_DEFAULT_ADMIN_USERNAME', 'admin');
|
||||
|
@ -606,11 +605,11 @@ use SilverStripe\Core\Convert;
|
|||
|
||||
class MyObject extends ViewableData
|
||||
{
|
||||
public function getSomeHTML
|
||||
{
|
||||
$title = Convert::raw2xml($this->Title);
|
||||
return "<h1>{$title}</h1>";
|
||||
}
|
||||
public function getSomeHTML
|
||||
{
|
||||
$title = Convert::raw2xml($this->Title);
|
||||
return "<h1>{$title}</h1>";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -619,15 +618,15 @@ After:
|
|||
```php
|
||||
class MyObject extends SilverStripe\View\ViewableData
|
||||
{
|
||||
private static $casting = [
|
||||
'SomeHTML' => 'HTMLText'
|
||||
];
|
||||
private static $casting = [
|
||||
'SomeHTML' => 'HTMLText'
|
||||
];
|
||||
|
||||
public function getSomeHTML
|
||||
public function getSomeHTML
|
||||
{
|
||||
$title = SilverStripe\Core\Convert::raw2xml($this->Title);
|
||||
return "<h1>{$title}</h1>";
|
||||
}
|
||||
$title = SilverStripe\Core\Convert::raw2xml($this->Title);
|
||||
return "<h1>{$title}</h1>";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -651,6 +650,10 @@ Usages of UploadField will need to be upgraded as below.
|
|||
Before:
|
||||
|
||||
```php
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\AssetAdmin\Forms\UploadField;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyClass extends DataObject
|
||||
{
|
||||
public function getCMSFields()
|
||||
|
@ -708,6 +711,8 @@ including both plurals and cross-module localisations.
|
|||
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class MyObject extends DataObject, implements i18nEntityProvider
|
||||
{
|
||||
public function provideI18nEntities()
|
||||
|
@ -847,12 +852,12 @@ is intended to upload images for manipulation.
|
|||
|
||||
```diff
|
||||
-if($file instanceof Image) {
|
||||
- $upload = new UploadField();
|
||||
- $upload->setAllowedFileCategories('image');
|
||||
- $upload = new UploadField();
|
||||
- $upload->setAllowedFileCategories('image');
|
||||
-}
|
||||
+if ($file->getIsImage()) {
|
||||
+ $upload = new UploadField();
|
||||
+ $upload->setAllowedFileCategories('image/supported');
|
||||
+ $upload = new UploadField();
|
||||
+ $upload->setAllowedFileCategories('image/supported');
|
||||
+}
|
||||
```
|
||||
|
||||
|
@ -862,12 +867,12 @@ an `Image` datatype, or refer to `DBFile('image/supported')`.
|
|||
```php
|
||||
class MyObject extends SilverStripe\ORM\DataObject
|
||||
{
|
||||
private static $has_one = [
|
||||
"ImageObject" => "Image"
|
||||
];
|
||||
private static $db = [
|
||||
"ImageField" => "DBFile('image/supported')"
|
||||
];
|
||||
private static $has_one = [
|
||||
"ImageObject" => "Image"
|
||||
];
|
||||
private static $db = [
|
||||
"ImageField" => "DBFile('image/supported')"
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -884,18 +889,18 @@ You would need to upgrade your code as below.
|
|||
```diff
|
||||
-function importTempFile($tmp)
|
||||
-{
|
||||
- copy($tmp, ASSETS_PATH . '/imported/' . basename($tmp));
|
||||
- $file = new File();
|
||||
- $file->setFilename('assets/imported/'.basename($tmp));
|
||||
- $file->write();
|
||||
- copy($tmp, ASSETS_PATH . '/imported/' . basename($tmp));
|
||||
- $file = new File();
|
||||
- $file->setFilename('assets/imported/'.basename($tmp));
|
||||
- $file->write();
|
||||
-}
|
||||
+public function importTempFile($tmp)
|
||||
+{
|
||||
+ Versioned::reading_stage('Stage');
|
||||
+ $file = new File();
|
||||
+ $file->setFromLocalFile($tmp, 'imported/' . basename($tmp));
|
||||
+ $file->write();
|
||||
+ $file->doPublish();
|
||||
+ $file->write();
|
||||
+ $file->doPublish();
|
||||
+}
|
||||
```
|
||||
|
||||
|
@ -934,15 +939,15 @@ Before:
|
|||
// in MyImageExtension.php
|
||||
class MyImageExtension extends SilverStripe\ORM\DataExtension
|
||||
{
|
||||
public function GalleryThumbnail($height)
|
||||
public function GalleryThumbnail($height)
|
||||
{
|
||||
return $this->getFormattedImage('GalleryThumbnail', $height);
|
||||
}
|
||||
return $this->getFormattedImage('GalleryThumbnail', $height);
|
||||
}
|
||||
|
||||
public function generateGalleryThumbnail(Image_Backend $backend, $height)
|
||||
public function generateGalleryThumbnail(Image_Backend $backend, $height)
|
||||
{
|
||||
return $backend->paddedResize(300, $height);
|
||||
}
|
||||
return $backend->paddedResize(300, $height);
|
||||
}
|
||||
}
|
||||
|
||||
// in _config.php
|
||||
|
@ -952,20 +957,22 @@ SilverStripe\Assets\Image::add_extension('MyImageExtension');
|
|||
Now image manipulations are implemented with a single method via a callback generator:
|
||||
|
||||
```php
|
||||
use SilverStripe\Assets\File;
|
||||
|
||||
// in MyImageExtension.php
|
||||
class MyImageExtension extends SilverStripe\Core\Extension
|
||||
{
|
||||
public function GalleryThumbnail($height)
|
||||
public function GalleryThumbnail($height)
|
||||
{
|
||||
// Generates the manipulation key
|
||||
$variant = $this->owner->variantName(__FUNCTION__, $height);
|
||||
// Generates the manipulation key
|
||||
$variant = $this->owner->variantName(__FUNCTION__, $height);
|
||||
|
||||
// Instruct the backend to search for an existing variant with this key,
|
||||
// and include a callback used to generate this image if it doesn't exist
|
||||
return $this->owner->manipulateImage($variant, function (Image_Backend $backend) use ($height) {
|
||||
return $backend->paddedResize(300, $height);
|
||||
});
|
||||
}
|
||||
// Instruct the backend to search for an existing variant with this key,
|
||||
// and include a callback used to generate this image if it doesn't exist
|
||||
return $this->owner->manipulateImage($variant, function (Image_Backend $backend) use ($height) {
|
||||
return $backend->paddedResize(300, $height);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// in _config.php
|
||||
|
@ -995,12 +1002,15 @@ Before this change, to use the handle_shortcode method, you would do something l
|
|||
```
|
||||
<?php
|
||||
|
||||
class MyShortcodeUser extends Object {
|
||||
class MyShortcodeUser extends Object
|
||||
|
||||
{
|
||||
|
||||
|
||||
private $content;
|
||||
private $content;
|
||||
|
||||
public function Content($arguments, $parser, $shortcode) {
|
||||
public function Content($arguments, $parser, $shortcode)
|
||||
{
|
||||
return File::handle_shortcode($arguments, $this->content, $parser, $shortcode);
|
||||
}
|
||||
}
|
||||
|
@ -1013,11 +1023,14 @@ In the new situation, this would look like this:
|
|||
|
||||
use SilverStripe\Assets\Shortcodes\FileShortcodeProvider;
|
||||
|
||||
class MyShortcodeUser extends Object {
|
||||
class MyShortcodeUser extends Object
|
||||
|
||||
private $content;
|
||||
{
|
||||
|
||||
public function Content($arguments, $parser, $shortcode) {
|
||||
private $content;
|
||||
|
||||
public function Content($arguments, $parser, $shortcode)
|
||||
{
|
||||
return FileShortcodeProvider::handle_shortcode($arguments, $this->content, $parser, $shortcode);
|
||||
}
|
||||
}
|
||||
|
@ -1031,22 +1044,21 @@ that handled saving of content into composite fields can be removed, as it is no
|
|||
The below describes the minimum amount of effort required to implement a composite DB field.
|
||||
|
||||
```php
|
||||
<?php
|
||||
use SilverStripe\ORM\FieldType\DBComposite;
|
||||
|
||||
class MyAddressField extends DBComposite
|
||||
{
|
||||
private static $composite_db = [
|
||||
'Street' => 'Varchar(200)',
|
||||
'Suburb' => 'Varchar(100)',
|
||||
'City' => 'Varchar(100)',
|
||||
'Country' => 'Varchar(100)'
|
||||
];
|
||||
private static $composite_db = [
|
||||
'Street' => 'Varchar(200)',
|
||||
'Suburb' => 'Varchar(100)',
|
||||
'City' => 'Varchar(100)',
|
||||
'Country' => 'Varchar(100)'
|
||||
];
|
||||
|
||||
public function scaffoldFormField($title = null, $params = null)
|
||||
public function scaffoldFormField($title = null, $params = null)
|
||||
{
|
||||
new SilverStripe\Forms\TextField($this->getName(), $title);
|
||||
}
|
||||
new SilverStripe\Forms\TextField($this->getName(), $title);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -1102,11 +1114,11 @@ Before:
|
|||
```php
|
||||
function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery = null)
|
||||
{
|
||||
$locale = Translatable::get_current_locale();
|
||||
if(!preg_match('/("|\'|`)Locale("|\'|`)/', implode(' ', $query->getWhere()))) {
|
||||
$qry = sprintf('"Locale" = \'%s\'', Convert::raw2sql($locale));
|
||||
$query->addWhere($qry);
|
||||
}
|
||||
$locale = Translatable::get_current_locale();
|
||||
if(!preg_match('/("|\'|`)Locale("|\'|`)/', implode(' ', $query->getWhere()))) {
|
||||
$qry = sprintf('"Locale" = \'%s\'', Convert::raw2sql($locale));
|
||||
$query->addWhere($qry);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -1115,12 +1127,12 @@ After:
|
|||
```php
|
||||
public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null)
|
||||
{
|
||||
$locale = Translatable::get_current_locale();
|
||||
if (!preg_match('/("|\'|`)Locale("|\'|`)/', implode(' ', $query->getWhereParameterised($parameters)))) {
|
||||
$query->addWhere([
|
||||
'"Locale"' => $locale
|
||||
]);
|
||||
}
|
||||
$locale = Translatable::get_current_locale();
|
||||
if (!preg_match('/("|\'|`)Locale("|\'|`)/', implode(' ', $query->getWhereParameterised($parameters)))) {
|
||||
$query->addWhere([
|
||||
'"Locale"' => $locale
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -1223,14 +1235,16 @@ parameter, which declares whether or not the model is versioned and has a draft
|
|||
if it only has versioning without staging.
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
/**
|
||||
* This model has staging and versioning. Stages will be "Stage" and "Live"
|
||||
*/
|
||||
class MyStagedModel extends SilverStripe\ORM\DataObject
|
||||
{
|
||||
private staic $extensions = [
|
||||
"SilverStripe\\ORM\\Versioning\\Versioned('StagedVersioned')"
|
||||
];
|
||||
private staic $extensions = [
|
||||
"SilverStripe\\ORM\\Versioning\\Versioned('StagedVersioned')"
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1238,9 +1252,9 @@ class MyStagedModel extends SilverStripe\ORM\DataObject
|
|||
*/
|
||||
class MyVersionedModel extends DataObject
|
||||
{
|
||||
private static $extensions = [
|
||||
"SilverStripe\\ORM\\Versioning\\Versioned('Versioned')"
|
||||
];
|
||||
private static $extensions = [
|
||||
"SilverStripe\\ORM\\Versioning\\Versioned('Versioned')"
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -1268,6 +1282,10 @@ For instance, on a products page which has a list of products, the products shou
|
|||
(`has_many`, `many_many`, etc.).
|
||||
|
||||
```php
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use Page;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class ProductPage extends Page
|
||||
{
|
||||
private static $has_many = [
|
||||
|
|
|
@ -369,7 +369,8 @@ E.g.
|
|||
|
||||
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
private static $has_one = array(
|
||||
"ImageObject" => "Image"
|
||||
);
|
||||
|
@ -451,11 +452,14 @@ Before:
|
|||
|
||||
:::php
|
||||
// in MyImageExtension.php
|
||||
class MyImageExtension extends DataExtension {
|
||||
public function GalleryThumbnail($height) {
|
||||
class MyImageExtension extends DataExtension
|
||||
{
|
||||
public function GalleryThumbnail($height)
|
||||
{
|
||||
return $this->getFormattedImage('GalleryThumbnail', $height);
|
||||
}
|
||||
public function generateGalleryThumbnail(Image_Backend $backend, $height) {
|
||||
public function generateGalleryThumbnail(Image_Backend $backend, $height)
|
||||
{
|
||||
return $backend->paddedResize(300, $height);
|
||||
}
|
||||
}
|
||||
|
@ -471,8 +475,10 @@ After:
|
|||
|
||||
:::php
|
||||
// in MyImageExtension.php
|
||||
class MyImageExtension extends Extension {
|
||||
public function GalleryThumbnail($height) {
|
||||
class MyImageExtension extends Extension
|
||||
{
|
||||
public function GalleryThumbnail($height)
|
||||
{
|
||||
// Generates the manipulation key
|
||||
$variant = $this->owner->variantName(__FUNCTION__, $height);
|
||||
|
||||
|
@ -509,7 +515,8 @@ The below describes the minimum amount of effort required to implement a composi
|
|||
|
||||
:::php
|
||||
<?
|
||||
class MyAddressField extends DBComposite {
|
||||
class MyAddressField extends DBComposite
|
||||
{
|
||||
|
||||
private static $composite_db = array(
|
||||
'Street' => 'Varchar(200)',
|
||||
|
@ -518,7 +525,8 @@ The below describes the minimum amount of effort required to implement a composi
|
|||
'Country' => 'Varchar(100)'
|
||||
);
|
||||
|
||||
public function scaffoldFormField($title = null) {
|
||||
public function scaffoldFormField($title = null)
|
||||
{
|
||||
new AddressFormField($this->getName(), $title);
|
||||
}
|
||||
}
|
||||
|
@ -607,8 +615,10 @@ E.g.
|
|||
|
||||
|
||||
:::php
|
||||
class MyErrorPageExtension extends SiteTreeExtension {
|
||||
public function updateErrorFilename(&$name, &$statuscode) {
|
||||
class MyErrorPageExtension extends SiteTreeExtension
|
||||
{
|
||||
public function updateErrorFilename(&$name, &$statuscode)
|
||||
{
|
||||
if($this->owner->exists()) {
|
||||
$locale = $this->Locale;
|
||||
} else {
|
||||
|
@ -721,7 +731,8 @@ For instance:
|
|||
/**
|
||||
* This model has staging and versioning. Stages will be "Stage" and "Live"
|
||||
*/
|
||||
class MyStagedModel extends DataObject {
|
||||
class MyStagedModel extends DataObject
|
||||
{
|
||||
private staic $extensions = array(
|
||||
"Versioned('StagedVersioned')"
|
||||
);
|
||||
|
@ -730,7 +741,8 @@ For instance:
|
|||
/**
|
||||
* This model has versioning only, and will not has a draft or live stage, nor be affected by the current stage.
|
||||
*/
|
||||
class MyVersionedModel extends DataObject {
|
||||
class MyVersionedModel extends DataObject
|
||||
{
|
||||
private static $extensions = array(
|
||||
"Versioned('Versioned')"
|
||||
);
|
||||
|
@ -783,8 +795,8 @@ This means that for the most part, code that worked in 3.0 won't need to be chan
|
|||
An exception to this is any classes which once had the `SS_` prefix, which will now be instead prefixed with `DB`, and have an un-aliased prefix. For example `SS_Datetime` is now `DBDateTime`, and has the alias `DateTime` which may be used in config.
|
||||
|
||||
For example:
|
||||
|
||||
class MyObject extends DataObject {
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
private static $db = array(
|
||||
'Number' => 'Int',
|
||||
'Time' => 'SS_Datetime'
|
||||
|
@ -794,7 +806,8 @@ For example:
|
|||
* @param Int $val
|
||||
* @return Varchar
|
||||
*/
|
||||
public function TextNumber() {
|
||||
public function TextNumber()
|
||||
{
|
||||
return new Varchar('TextNumber', 'Number is ' . $this->Number);
|
||||
}
|
||||
}
|
||||
|
@ -802,8 +815,8 @@ For example:
|
|||
Will become:
|
||||
|
||||
use SilverStripe\Model\FieldType\DBVarchar;
|
||||
|
||||
class MyObject extends DataObject {
|
||||
class MyObject extends DataObject
|
||||
{
|
||||
private static $db = array(
|
||||
'Number' => 'Int',
|
||||
'Time' => 'Datetime'
|
||||
|
@ -813,7 +826,8 @@ Will become:
|
|||
* @param Int $val
|
||||
* @return Varchar
|
||||
*/
|
||||
public function TextNumber() {
|
||||
public function TextNumber()
|
||||
{
|
||||
return new DBVarchar('TextNumber', 'Number is ' . $this->Number);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -276,9 +276,11 @@ simply use the provided page URLs.
|
|||
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
static $allowed_actions = array('myaction');
|
||||
public function myaction($request) {
|
||||
public function myaction($request)
|
||||
{
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
@ -468,7 +470,8 @@ of strings. This format is inherently unsafe, and should be avoided where possib
|
|||
|
||||
:::php
|
||||
<?php
|
||||
public function augmentSQL(SQLQuery &$query) {
|
||||
public function augmentSQL(SQLQuery &$query)
|
||||
{
|
||||
$query->getWhere(); // Will be flattened (unsafe 3.1 compatible format)
|
||||
$expression = $query->toAppropriateExpression(); // Either SQLSelect or SQLDelete
|
||||
$expression->getWhere(); // Will be parameterised (preferred 3.2 compatible format)
|
||||
|
@ -699,7 +702,8 @@ Before:
|
|||
|
||||
|
||||
:::php
|
||||
public function filtersOnColumn($query, $column) {
|
||||
public function filtersOnColumn($query, $column)
|
||||
{
|
||||
$regexp = '/^(.*\.)?("|`)?' . $column . ' ("|`)?\s?=/';
|
||||
foreach($this->getWhere() as $predicate) {
|
||||
if(preg_match($regexp, $predicate)) return true;
|
||||
|
@ -712,7 +716,8 @@ After:
|
|||
|
||||
|
||||
:::php
|
||||
public function filtersOnColumn($query, $column) {
|
||||
public function filtersOnColumn($query, $column)
|
||||
{
|
||||
$regexp = '/^(.*\.)?("|`)?' . $column . ' ("|`)?\s?=/';
|
||||
foreach($this->getWhereParameterised($parameters) as $predicate) {
|
||||
if(preg_match($regexp, $predicate)) return true;
|
||||
|
|
|
@ -31,9 +31,11 @@ and doesn't check if the user is authenticated to view it. As with any other con
|
|||
please use `DataObject->canView()` to determine permissions.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
class MyController extends Controller
|
||||
{
|
||||
private static $allowed_actions = array('showpage');
|
||||
public function showpage($request) {
|
||||
public function showpage($request)
|
||||
{
|
||||
$page = Page::get()->byID($request->param('ID'));
|
||||
if(!$page->canView()) return $this->httpError(401);
|
||||
// continue with authenticated logic...
|
||||
|
|
|
@ -212,14 +212,15 @@ Further guidelines:
|
|||
* Mention important changed classes and methods in the commit summary.
|
||||
|
||||
Example: Bad commit message
|
||||
|
||||
finally fixed this dumb rendering bug that Joe talked about ... LOL
|
||||
also added another form field for password validation
|
||||
|
||||
```
|
||||
finally fixed this dumb rendering bug that Joe talked about ... LOL
|
||||
also added another form field for password validation
|
||||
```
|
||||
Example: Good commit message
|
||||
```
|
||||
BUG Formatting through prepValueForDB()
|
||||
|
||||
BUG Formatting through prepValueForDB()
|
||||
|
||||
Added prepValueForDB() which is called on DBField->writeToManipulation()
|
||||
to ensure formatting of value before insertion to DB on a per-DBField type basis (fixes #1234).
|
||||
Added documentation for DBField->writeToManipulation() (related to a4bd42fd).
|
||||
Added prepValueForDB() which is called on DBField->writeToManipulation()
|
||||
to ensure formatting of value before insertion to DB on a per-DBField type basis (fixes #1234).
|
||||
Added documentation for DBField->writeToManipulation() (related to a4bd42fd).
|
||||
```
|
||||
|
|
|
@ -61,15 +61,18 @@ How to deprecate an API:
|
|||
|
||||
Here's an example for replacing `Director::isDev()` with a (theoretical) `Env::is_dev()`:
|
||||
|
||||
:::php
|
||||
/**
|
||||
* Returns true if your are in development mode
|
||||
* @deprecated 4.0 Use {@link Env::is_dev()} instead.
|
||||
*/
|
||||
public function isDev() {
|
||||
Deprecation::notice('4.0', 'Use Env::is_dev() instead');
|
||||
return Env::is_dev();
|
||||
}
|
||||
|
||||
```php
|
||||
/**
|
||||
* Returns true if your are in development mode
|
||||
* @deprecated 4.0 Use {@link Env::is_dev()} instead.
|
||||
*/
|
||||
public function isDev()
|
||||
{
|
||||
Deprecation::notice('4.0', 'Use Env::is_dev() instead');
|
||||
return Env::is_dev();
|
||||
}
|
||||
```
|
||||
|
||||
This change could be committed to a minor release like *3.2.0*, and remains deprecated in all subsequent minor releases
|
||||
(e.g. *3.3.0*, *3.4.0*), until a new major release (e.g. *4.0.0*), at which point it gets removed from the codebase.
|
||||
|
@ -82,9 +85,10 @@ notices are always disabled on both live and test.
|
|||
`mysite/_config.php`
|
||||
|
||||
|
||||
:::php
|
||||
Deprecation::set_enabled(false);
|
||||
|
||||
```php
|
||||
Deprecation::set_enabled(false);
|
||||
```
|
||||
|
||||
`.env`
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ As a core contributor it is necessary to have installed the following set of too
|
|||
* A good `.env` setup in your localhost webroot.
|
||||
|
||||
Example `.env`:
|
||||
|
||||
```
|
||||
# Environent
|
||||
SS_TRUSTED_PROXY_IPS="*"
|
||||
SS_ENVIRONMENT_TYPE="dev"
|
||||
|
@ -59,7 +59,7 @@ Example `.env`:
|
|||
|
||||
# Basic CLI request url default
|
||||
SS_BASE_URL="http://localhost/"
|
||||
|
||||
```
|
||||
|
||||
You will also need to be assigned the following permissions. Contact one of the SS staff from
|
||||
the [core committers](core_committers), who will assist with setting up your credentials.
|
||||
|
@ -168,9 +168,9 @@ doe not make any upstream changes (so it's safe to run without worrying about
|
|||
any mistakes migrating their way into the public sphere).
|
||||
|
||||
Invoked by running `cow release` in the format as below:
|
||||
|
||||
```
|
||||
cow release <version> -vvv
|
||||
|
||||
```
|
||||
This command has the following parameters:
|
||||
|
||||
* `<version>` The version that is to be released. E.g. 3.2.4 or 4.0.0-alpha4
|
||||
|
@ -238,9 +238,9 @@ building an archive, and uploading to
|
|||
[www.silverstripe.org](http://www.silverstripe.org/software/download/) download page.
|
||||
|
||||
Invoked by running `cow release:publish` in the format as below:
|
||||
|
||||
```
|
||||
cow release:publish <version> -vvv
|
||||
|
||||
```
|
||||
As with the `cow release` command, this step is broken down into the following
|
||||
subtasks which are invoked in sequence:
|
||||
|
||||
|
|
|
@ -90,31 +90,31 @@ sparingly.
|
|||
</div>
|
||||
|
||||
Code for a Tip box:
|
||||
|
||||
<div class="hint" markdown='1'>
|
||||
...
|
||||
</div>
|
||||
|
||||
```
|
||||
<div class="hint" markdown='1'>
|
||||
...
|
||||
</div>
|
||||
```
|
||||
<div class="notice" markdown='1'>
|
||||
"Notification box": A notification box is good for technical notifications relating to the main text. For example, notifying users about a deprecated feature.
|
||||
</div>
|
||||
|
||||
Code for a Notification box:
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
...
|
||||
</div>
|
||||
|
||||
```
|
||||
<div class="notice" markdown='1'>
|
||||
...
|
||||
</div>
|
||||
```
|
||||
<div class="warning" markdown='1'>
|
||||
"Warning box": A warning box is useful for highlighting a severe bug or a technical issue requiring a user's attention. For example, suppose a rare edge case sometimes leads to a variable being overwritten incorrectly. A warning box can be used to alert the user to this case so they can write their own code to handle it.
|
||||
</div>
|
||||
|
||||
Code for a Warning box:
|
||||
|
||||
<div class="warning" markdown='1'>
|
||||
...
|
||||
</div>
|
||||
|
||||
```
|
||||
<div class="warning" markdown='1'>
|
||||
...
|
||||
</div>
|
||||
```
|
||||
See [markdown extra documentation](http://michelf.com/projects/php-markdown/extra/#html) for more restrictions
|
||||
on placing HTML blocks inside Markdown.
|
||||
|
||||
|
|
|
@ -71,13 +71,14 @@ under the "silverstripe" user, see
|
|||
Translations need to be reviewed before being committed, which is a process that happens roughly once per month. We're
|
||||
merging back translations into all supported release branches as well as the `master` branch. The following script
|
||||
should be applied to the oldest release branch, and then merged forward into newer branches:
|
||||
|
||||
:::bash
|
||||
tx pull
|
||||
```bash
|
||||
|
||||
tx pull
|
||||
|
||||
# Manually review changes through git diff, then commit
|
||||
git add lang/*
|
||||
git commit -m "Updated translations"
|
||||
# Manually review changes through git diff, then commit
|
||||
git add lang/*
|
||||
git commit -m "Updated translations"
|
||||
```
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
You can download your work right from Transifex in order to speed up the process for your desired language.
|
||||
|
@ -88,9 +89,10 @@ You can download your work right from Transifex in order to speed up the process
|
|||
SilverStripe also supports translating strings in JavaScript (see [i18n](/developer_guides/i18n)), but there's a
|
||||
conversion step involved in order to get those translations syncing with Transifex. Our translation files stored in
|
||||
`mymodule/javascript/lang/*.js` call `ss.i18n.addDictionary()` to add files.
|
||||
|
||||
:::js
|
||||
ss.i18n.addDictionary('de', {'MyNamespace.MyKey': 'My Translation'});
|
||||
```js
|
||||
|
||||
ss.i18n.addDictionary('de', {'MyNamespace.MyKey': 'My Translation'});
|
||||
```
|
||||
|
||||
But Transifex only accepts structured formats like JSON.
|
||||
|
||||
|
@ -99,32 +101,33 @@ But Transifex only accepts structured formats like JSON.
|
|||
```
|
||||
|
||||
First of all, you need to create those source files in JSON, and store them in `mymodule/javascript/lang/src/*.js`. In your `.tx/config` you can configure this path as a separate master location.
|
||||
|
||||
:::ruby
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
```ruby
|
||||
|
||||
[silverstripe-mymodule.master]
|
||||
file_filter = lang/<lang>.yml
|
||||
source_file = lang/en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[silverstripe-mymodule.master-js]
|
||||
file_filter = javascript/lang/src/<lang>.js
|
||||
source_file = javascript/lang/src/en.js
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
[silverstripe-mymodule.master]
|
||||
file_filter = lang/<lang>.yml
|
||||
source_file = lang/en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[silverstripe-mymodule.master-js]
|
||||
file_filter = javascript/lang/src/<lang>.js
|
||||
source_file = javascript/lang/src/en.js
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
```
|
||||
|
||||
Then you can upload the source files via a normal `tx push`. Once translations come in, you need to convert the source
|
||||
files back into the JS files SilverStripe can actually read. This requires an installation of our
|
||||
[buildtools](https://github.com/silverstripe/silverstripe-buildtools).
|
||||
|
||||
tx pull
|
||||
(cd .. && phing -Dmodule=mymodule translation-generate-javascript-for-module)
|
||||
git add javascript/lang/*
|
||||
git commit -m "Updated javascript translations"
|
||||
|
||||
```
|
||||
tx pull
|
||||
(cd .. && phing -Dmodule=mymodule translation-generate-javascript-for-module)
|
||||
git add javascript/lang/*
|
||||
git commit -m "Updated javascript translations"
|
||||
```
|
||||
# Related
|
||||
|
||||
* [i18n](/developer_guides/i18n/): Developer-level documentation of Silverstripe's i18n capabilities
|
||||
|
|
|
@ -25,53 +25,61 @@ except when necessitated by third party conventions (e.g using PHP's `Serializab
|
|||
SilverStripe's [Config API]() can read its defaults from variables declared as `private static` on classes.
|
||||
As opposed to other variables, these should be declared as lower case with underscores.
|
||||
|
||||
:::php
|
||||
class MyClass
|
||||
{
|
||||
private static $my_config_variable = 'foo';
|
||||
}
|
||||
|
||||
```php
|
||||
class MyClass
|
||||
{
|
||||
private static $my_config_variable = 'foo';
|
||||
}
|
||||
```
|
||||
|
||||
## Prefer identical (===) comparisons over equality (==)
|
||||
|
||||
Where possible, use type-strict identical comparisons instead of loosely typed equality comparisons.
|
||||
Read more in the PHP documentation for [comparison operators](http://php.net/manual/en/language.operators.comparison.php) and [object comparison](http://php.net/manual/en/language.oop5.object-comparison.php).
|
||||
|
||||
:::php
|
||||
// good - only need to cast to (int) if $a might not already be an int
|
||||
if ((int)$a === 100) {
|
||||
doThis();
|
||||
}
|
||||
|
||||
// bad
|
||||
if ($a == 100) {
|
||||
doThis();
|
||||
}
|
||||
|
||||
```php
|
||||
// good - only need to cast to (int) if $a might not already be an int
|
||||
if ((int)$a === 100) {
|
||||
doThis();
|
||||
}
|
||||
|
||||
// bad
|
||||
if ($a == 100) {
|
||||
doThis();
|
||||
}
|
||||
```
|
||||
|
||||
## Separation of Logic and Presentation
|
||||
|
||||
Try to avoid using PHP's ability to mix HTML into the code.
|
||||
|
||||
:::php
|
||||
// PHP code
|
||||
public function getTitle() {
|
||||
return "<h2>Bad Example</h2>";
|
||||
}
|
||||
|
||||
// Template code
|
||||
$Title
|
||||
```php
|
||||
// PHP code
|
||||
public function getTitle()
|
||||
{
|
||||
return "<h2>Bad Example</h2>";
|
||||
}
|
||||
|
||||
// Template code
|
||||
$Title
|
||||
```
|
||||
|
||||
Better: Keep HTML in template files:
|
||||
|
||||
:::php
|
||||
// PHP code
|
||||
public function getTitle() {
|
||||
return "Better Example";
|
||||
}
|
||||
|
||||
// Template code
|
||||
<h2>$Title</h2>
|
||||
```php
|
||||
// PHP code
|
||||
public function getTitle()
|
||||
{
|
||||
return "Better Example";
|
||||
}
|
||||
|
||||
// Template code
|
||||
<h2>$Title</h2>
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
|
@ -87,23 +95,23 @@ and [tag overview](http://manual.phpdoc.org/HTMLSmartyConverter/HandS/phpDocumen
|
|||
|
||||
Example:
|
||||
|
||||
:::php
|
||||
/**
|
||||
* My short description for this class.
|
||||
* My longer description with
|
||||
* multiple lines and richer formatting.
|
||||
*
|
||||
* Usage:
|
||||
* <code>
|
||||
* $c = new MyClass();
|
||||
* $c->myMethod();
|
||||
* </code>
|
||||
*
|
||||
* @package custom
|
||||
*/
|
||||
class MyClass extends Class
|
||||
{
|
||||
|
||||
```php
|
||||
/**
|
||||
* My short description for this class.
|
||||
* My longer description with
|
||||
* multiple lines and richer formatting.
|
||||
*
|
||||
* Usage:
|
||||
* <code>
|
||||
* $c = new MyClass();
|
||||
* $c->myMethod();
|
||||
* </code>
|
||||
*
|
||||
* @package custom
|
||||
*/
|
||||
class MyClass extends Class
|
||||
{
|
||||
/**
|
||||
* My Method.
|
||||
* This method returns something cool. {@link MyParentMethod} has other cool stuff in it.
|
||||
|
@ -116,7 +124,8 @@ Example:
|
|||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Class Member Ordering
|
||||
|
||||
|
@ -137,16 +146,21 @@ Put code into the classes in the following order (where applicable).
|
|||
If you have to use raw SQL, make sure your code works across databases. Make sure you escape your queries like below,
|
||||
with the column or table name escaped with double quotes as below.
|
||||
|
||||
:::php
|
||||
MyClass::get()->where(array("\"Score\" > ?" => 50));
|
||||
|
||||
```php
|
||||
MyClass::get()->where(["\"Score\" > ?" => 50]);
|
||||
|
||||
```
|
||||
|
||||
It is preferable to use parameterised queries whenever necessary to provide conditions
|
||||
to a SQL query, where values placeholders are each replaced with a single unquoted question mark.
|
||||
If it's absolutely necessary to use literal values in a query make sure that values
|
||||
are single quoted.
|
||||
|
||||
:::php
|
||||
MyClass::get()->where("\"Title\" = 'my title'");
|
||||
|
||||
```php
|
||||
MyClass::get()->where("\"Title\" = 'my title'");
|
||||
```
|
||||
|
||||
Use [ANSI SQL](http://en.wikipedia.org/wiki/SQL#Standardization) format where possible.
|
||||
|
||||
|
|
Loading…
Reference in New Issue