Merge 4.0 -> 4

This commit is contained in:
Damian Mooyman 2017-11-02 16:52:05 +13:00
commit 0b3ed7ff15
No known key found for this signature in database
GPG Key ID: 78B823A10DE27D1A
183 changed files with 7717 additions and 7030 deletions

View File

@ -1150,6 +1150,8 @@ warnings:
message: 'Removed' message: 'Removed'
'dontEscape()': 'dontEscape()':
message: 'FormField::dontEscape() has been removed. Escaping is now managed on a class by class basis.' message: 'FormField::dontEscape() has been removed. Escaping is now managed on a class by class basis.'
'SilverStripe\Forms\CompositeField::setID()':
message: 'ID is generated from name indirectly; Use SilverStripe\Form\FormField::setName() instead'
'SilverStripe\Forms\FormField::createTag()': 'SilverStripe\Forms\FormField::createTag()':
message: 'moved to SilverStripe\View\HTML->createTag()' message: 'moved to SilverStripe\View\HTML->createTag()'
'SilverStripe\Forms\Form->transformTo()': 'SilverStripe\Forms\Form->transformTo()':
@ -1370,4 +1372,4 @@ warnings:
url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#module-paths' url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#module-paths'
'THIRDPARTY_DIR': 'THIRDPARTY_DIR':
message: 'Path constants have been deprecated, use the Requirements and ModuleResourceLoader APIs' message: 'Path constants have been deprecated, use the Requirements and ModuleResourceLoader APIs'
url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#module-paths' url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#module-paths'

View File

@ -6,7 +6,7 @@ SilverStripe\Control\HTTP:
max-age: 0 max-age: 0
must-revalidate: "true" must-revalidate: "true"
no-transform: "true" no-transform: "true"
vary: "Cookie, X-Forwarded-Protocol, User-Agent, Accept" vary: "Cookie, X-Forwarded-Protocol, X-Forwarded-Proto, User-Agent, Accept"
SilverStripe\Core\Manifest\VersionProvider: SilverStripe\Core\Manifest\VersionProvider:
modules: modules:
silverstripe/framework: Framework silverstripe/framework: Framework

View File

@ -11,6 +11,7 @@ SilverStripe\Core\Injector\Injector:
SessionMiddleware: '%$SilverStripe\Control\Middleware\SessionMiddleware' SessionMiddleware: '%$SilverStripe\Control\Middleware\SessionMiddleware'
RequestProcessorMiddleware: '%$SilverStripe\Control\RequestProcessor' RequestProcessorMiddleware: '%$SilverStripe\Control\RequestProcessor'
FlushMiddleware: '%$SilverStripe\Control\Middleware\FlushMiddleware' FlushMiddleware: '%$SilverStripe\Control\Middleware\FlushMiddleware'
CanonicalURLMiddleware: '%$SilverStripe\Control\Middleware\CanonicalURLMiddleware'
SilverStripe\Control\Middleware\AllowedHostsMiddleware: SilverStripe\Control\Middleware\AllowedHostsMiddleware:
properties: properties:
AllowedHosts: '`SS_ALLOWED_HOSTS`' AllowedHosts: '`SS_ALLOWED_HOSTS`'
@ -37,3 +38,12 @@ After:
SilverStripe\Core\Injector\Injector: SilverStripe\Core\Injector\Injector:
# Note: If Director config changes, take note it will affect this config too # Note: If Director config changes, take note it will affect this config too
SilverStripe\Core\Startup\ErrorDirector: '%$SilverStripe\Control\Director' SilverStripe\Core\Startup\ErrorDirector: '%$SilverStripe\Control\Director'
---
Name: canonicalurls
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Control\Middleware\CanonicalURLMiddleware:
properties:
ForceSSL: false
ForceWWW: false

View File

@ -1,3 +1,10 @@
# Run framework behat tests with this command (installed with silverstripe/installer)
# Note that framework behat tests require CMS module
# ========================================================================= #
# vendor/bin/selenium-server-standalone -Dwebdriver.firefox.bin="/Applications/Firefox31.app/Contents/MacOS/firefox-bin"
# vendor/bin/serve --bootstrap-file vendor/silverstripe/framework/tests/behat/serve-bootstrap.php
# vendor/bin/behat @framework
# ========================================================================= #
default: default:
suites: suites:
framework: framework:

View File

@ -79,8 +79,8 @@ since it makes permissions easier to handle when running commands both
from the command line and through the web server. Find and adjust the following options, from the command line and through the web server. Find and adjust the following options,
replacing the `<user>` placeholder: replacing the `<user>` placeholder:
User <user> user <user>
Group staff Group staff
Now start the web server: Now start the web server:

View File

@ -3,33 +3,33 @@
1. Lighttpd works fine so long as you provide a custom config. Add the following to lighttpd.conf **BEFORE** installing 1. Lighttpd works fine so long as you provide a custom config. Add the following to lighttpd.conf **BEFORE** installing
Silverstripe. Replace "yoursite.com" and "/home/yoursite/public_html/" below. Silverstripe. Replace "yoursite.com" and "/home/yoursite/public_html/" below.
$HTTP["host"] == "yoursite.com" { $HTTP["host"] == "yoursite.com" {
server.document-root = "/home/yoursite/public_html/" server.document-root = "/home/yoursite/public_html/"
# Disable directory listings # Disable directory listings
dir-listing.activate = "disable" dir-listing.activate = "disable"
# Deny access to template files # Deny access to template files
url.access-deny += ( ".ss" ) url.access-deny += ( ".ss" )
static-file.exclude-extensions += ( ".ss" ) static-file.exclude-extensions += ( ".ss" )
# Deny access to SilverStripe command-line interface # Deny access to SilverStripe command-line interface
$HTTP["url"] =~ "^/vendor/silverstripe/framework/cli-script.php" { $HTTP["url"] =~ "^/vendor/silverstripe/framework/cli-script.php" {
url.access-deny = ( "" ) url.access-deny = ( "" )
} }
# Disable FastCGI in assets directory (so that PHP files are not executed) # Disable FastCGI in assets directory (so that PHP files are not executed)
$HTTP["url"] =~ "^/assets/" { $HTTP["url"] =~ "^/assets/" {
fastcgi.server = () fastcgi.server = ()
} }
# Rewrite URLs so they are nicer # Rewrite URLs so they are nicer
url.rewrite-once = ( url.rewrite-once = (
"^/.*\.[A-Za-z0-9]+.*?$" => "$0", "^/.*\.[A-Za-z0-9]+.*?$" => "$0",
"^/(.*?)(\?|$)(.*)" => "/index.php?$3" "^/(.*?)(\?|$)(.*)" => "/index.php?$3"
) )
# Show SilverStripe error page # Show SilverStripe error page
server.error-handler-404 = "/index.php" server.error-handler-404 = "/index.php"
} }
@ -48,7 +48,7 @@ recommend using subdomains instead if you can, for exampe: site1.yourdomain.com
things a lot simpler, as you just use two of the above host example blocks. But if you really must run multiple copies things a lot simpler, as you just use two of the above host example blocks. But if you really must run multiple copies
of Silverstripe on the same host, you can use something like this (be warned, it's quite nasty): of Silverstripe on the same host, you can use something like this (be warned, it's quite nasty):
$HTTP["host"] == "yoursite.com" { $HTTP["host"] == "yoursite.com" {
url.rewrite-once = ( url.rewrite-once = (
"(?i)(/copy1/.*\.([A-Za-z0-9]+))(.*?)$" => "$0", "(?i)(/copy1/.*\.([A-Za-z0-9]+))(.*?)$" => "$0",

View File

@ -21,21 +21,21 @@ But enough of the disclaimer, on to the actual configuration — typically in `n
server { server {
listen 80; listen 80;
root /path/to/ss/folder; root /path/to/ss/folder;
server_name site.com www.site.com; server_name site.com www.site.com;
# Defend against SS-2015-013 -- http://www.silverstripe.org/software/download/security-releases/ss-2015-013 # Defend against SS-2015-013 -- http://www.silverstripe.org/software/download/security-releases/ss-2015-013
if ($http_x_forwarded_host) { if ($http_x_forwarded_host) {
return 400; return 400;
} }
location / { location / {
try_files $uri /index.php?$query_string; try_files $uri /index.php?$query_string;
} }
error_page 404 /assets/error-404.html; error_page 404 /assets/error-404.html;
error_page 500 /assets/error-500.html; error_page 500 /assets/error-500.html;
location ^~ /assets/ { location ^~ /assets/ {
location ~ /\. { location ~ /\. {
deny all; deny all;
@ -43,7 +43,7 @@ But enough of the disclaimer, on to the actual configuration — typically in `n
sendfile on; sendfile on;
try_files $uri index.php?$query_string; try_files $uri index.php?$query_string;
} }
location ~ /framework/.*(main|rpc|tiny_mce_gzip)\.php$ { location ~ /framework/.*(main|rpc|tiny_mce_gzip)\.php$ {
fastcgi_keep_conn on; fastcgi_keep_conn on;
fastcgi_pass 127.0.0.1:9000; fastcgi_pass 127.0.0.1:9000;
@ -51,45 +51,45 @@ But enough of the disclaimer, on to the actual configuration — typically in `n
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params; include fastcgi_params;
} }
location ~ /(mysite|framework|cms)/.*\.(php|php3|php4|php5|phtml|inc)$ { location ~ /(mysite|framework|cms)/.*\.(php|php3|php4|php5|phtml|inc)$ {
deny all; deny all;
} }
location ~ /\.. { location ~ /\.. {
deny all; deny all;
} }
location ~ \.ss$ { location ~ \.ss$ {
satisfy any; satisfy any;
allow 127.0.0.1; allow 127.0.0.1;
deny all; deny all;
} }
location ~ web\.config$ { location ~ web\.config$ {
deny all; deny all;
} }
location ~ \.ya?ml$ { location ~ \.ya?ml$ {
deny all; deny all;
} }
location ^~ /vendor/ { location ^~ /vendor/ {
deny all; deny all;
} }
location ~* /silverstripe-cache/ { location ~* /silverstripe-cache/ {
deny all; deny all;
} }
location ~* composer\.(json|lock)$ { location ~* composer\.(json|lock)$ {
deny all; deny all;
} }
location ~* /(cms|framework)/silverstripe_version$ { location ~* /(cms|framework)/silverstripe_version$ {
deny all; deny all;
} }
location ~ \.php$ { location ~ \.php$ {
fastcgi_keep_conn on; fastcgi_keep_conn on;
fastcgi_pass 127.0.0.1:9000; fastcgi_pass 127.0.0.1:9000;

View File

@ -34,11 +34,11 @@ The following commands will work on Linux/Unix based servers. For other servers
:::bash :::bash
# Create directory # Create directory
sudo mkdir ssl sudo mkdir ssl
cd ssl cd ssl
# Generate CA key and CA cert # Generate CA key and CA cert
sudo openssl genrsa 2048 | sudo tee -a ca-key.pem sudo openssl genrsa 2048 | sudo tee -a ca-key.pem
sudo openssl req -new -x509 -nodes -days 365000 -key ca-key.pem -out ca-cert.pem sudo openssl req -new -x509 -nodes -days 365000 -key ca-key.pem -out ca-cert.pem
@ -47,20 +47,20 @@ The following commands will work on Linux/Unix based servers. For other servers
# IMPORTANT: the common name of the certificate should match the domain name of your host! # IMPORTANT: the common name of the certificate should match the domain name of your host!
sudo openssl rsa -in server-key.pem -out server-key.pem sudo openssl rsa -in server-key.pem -out server-key.pem
sudo openssl req -newkey rsa:2048 -days 365000 -nodes -keyout server-key.pem -out server-req.pem sudo openssl req -newkey rsa:2048 -days 365000 -nodes -keyout server-key.pem -out server-req.pem
# Generate and sign SERVER certificate # Generate and sign SERVER certificate
sudo openssl x509 -req -in server-req.pem -days 365000 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out server-cert.pem sudo openssl x509 -req -in server-req.pem -days 365000 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out server-cert.pem
# Generate CLIENT key and certificate signing request # Generate CLIENT key and certificate signing request
sudo openssl rsa -in client-key.pem -out client-key.pem sudo openssl rsa -in client-key.pem -out client-key.pem
sudo openssl req -newkey rsa:2048 -days 365000 -nodes -keyout client-key.pem -out client-req.pem sudo openssl req -newkey rsa:2048 -days 365000 -nodes -keyout client-key.pem -out client-req.pem
# Generate and sign CLIENT certificate # Generate and sign CLIENT certificate
sudo openssl x509 -req -in client-req.pem -days 365000 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out client-cert.pem sudo openssl x509 -req -in client-req.pem -days 365000 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out client-cert.pem
# Verify validity of generated certificates # Verify validity of generated certificates
sudo openssl verify -CAfile ca-cert.pem server-cert.pem client-cert.pem sudo openssl verify -CAfile ca-cert.pem server-cert.pem client-cert.pem
<div class="warning" markdown='1'> <div class="warning" markdown='1'>
After generating the certificates, make sure to set the correct permissions to prevent unauthorized access to your keys! After generating the certificates, make sure to set the correct permissions to prevent unauthorized access to your keys!
@ -128,7 +128,7 @@ On your Silverstripe instance:
:::bash :::bash
# Secure copy over SSH via rsync command. You may use an alternative method if desired. # Secure copy over SSH via rsync command. You may use an alternative method if desired.
rsync -avP user@db1.example.com:/path/to/client/certs /path/to/secure/folder rsync -avP user@db1.example.com:/path/to/client/certs /path/to/secure/folder
# Depending on your web server configuration, allow web server to read to SSL files # Depending on your web server configuration, allow web server to read to SSL files
sudo chown -R www-data:www-data /path/to/secure/folder sudo chown -R www-data:www-data /path/to/secure/folder
sudo chmod 750 /path/to/secure/folder sudo chmod 750 /path/to/secure/folder
@ -144,14 +144,14 @@ Add or edit your `_ss_environment.php` configuration file. (See [Environment Man
:::php :::php
<?php <?php
// These four define set the database connection details. // These four define set the database connection details.
define('SS_DATABASE_CLASS', 'MySQLPDODatabase'); define('SS_DATABASE_CLASS', 'MySQLPDODatabase');
define('SS_DATABASE_SERVER', 'db1.example.com'); define('SS_DATABASE_SERVER', 'db1.example.com');
define('SS_DATABASE_USERNAME', 'dbuser'); define('SS_DATABASE_USERNAME', 'dbuser');
define('SS_DATABASE_PASSWORD', '<password>'); define('SS_DATABASE_PASSWORD', '<password>');
// These define the paths to the SSL key, certificate, and CA certificate bundle. // These define the paths to the SSL key, certificate, and CA certificate bundle.
define('SS_DATABASE_SSL_KEY', '/home/newdrafts/mysqlssltest/client-key.pem'); define('SS_DATABASE_SSL_KEY', '/home/newdrafts/mysqlssltest/client-key.pem');
define('SS_DATABASE_SSL_CERT', '/home/newdrafts/mysqlssltest/client-cert.pem'); define('SS_DATABASE_SSL_CERT', '/home/newdrafts/mysqlssltest/client-cert.pem');
@ -160,10 +160,10 @@ Add or edit your `_ss_environment.php` configuration file. (See [Environment Man
// When using SSL connections, you also need to supply a username and password to override the default settings // When using SSL connections, you also need to supply a username and password to override the default settings
define('SS_DEFAULT_ADMIN_USERNAME', 'username'); define('SS_DEFAULT_ADMIN_USERNAME', 'username');
define('SS_DEFAULT_ADMIN_PASSWORD', 'password'); define('SS_DEFAULT_ADMIN_PASSWORD', 'password');
When running the installer, make sure to check on the `Use _ss_environment file for configuration` option under the `Database Configuration` section to use the environment file. When running the installer, make sure to check on the `Use _ss_environment file for configuration` option under the `Database Configuration` section to use the environment file.
## Conclusion ## Conclusion
That's it! We hope that this article was able to help you configure your remote MySQL SSL secure database connection. That's it! We hope that this article was able to help you configure your remote MySQL SSL secure database connection.

View File

@ -46,14 +46,14 @@ Create `/etc/nginx/silverstripe.conf` and add this configuration:
fastcgi_buffer_size 32k; fastcgi_buffer_size 32k;
fastcgi_busy_buffers_size 64k; fastcgi_busy_buffers_size 64k;
fastcgi_buffers 4 32k; fastcgi_buffers 4 32k;
location / { location / {
try_files $uri /index.php?$query_string; try_files $uri /index.php?$query_string;
} }
error_page 404 /assets/error-404.html; error_page 404 /assets/error-404.html;
error_page 500 /assets/error-500.html; error_page 500 /assets/error-500.html;
location ^~ /assets/ { location ^~ /assets/ {
try_files $uri =404; try_files $uri =404;
} }
@ -99,10 +99,10 @@ e.g. `/etc/nginx/sites-enabled/mysite`:
listen 80; listen 80;
root /var/www/mysite; root /var/www/mysite;
server_name www.mysite.com; server_name www.mysite.com;
error_log /var/log/nginx/mysite.error.log; error_log /var/log/nginx/mysite.error.log;
access_log /var/log/nginx/mysite.access.log; access_log /var/log/nginx/mysite.access.log;
include /etc/nginx/hhvm.conf; include /etc/nginx/hhvm.conf;
include /etc/nginx/silverstripe.conf; include /etc/nginx/silverstripe.conf;
} }

View File

@ -112,27 +112,27 @@ 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: Open *themes/simple/templates/Page.ss*. It uses standard HTML apart from these exceptions:
```ss ```ss
<% base_tag %> <% 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 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. ensures the browser knows where to locate your site's images and css files.
```ss ```ss
$Title $Title
$SiteConfig.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. 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 ```ss
$MetaTags $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. 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 ```ss
$Layout $Layout
``` ```
The Layout variable is replaced with the contents of a template file with the same name as the page type we are using. The Layout variable is replaced with the contents of a template file with the same name as the page type we are using.
@ -140,8 +140,8 @@ The Layout variable is replaced with the contents of a template file with the sa
Open *themes/simple/templates/Layout/Page.ss*. You will see more HTML and more SilverStripe template replacement tags and variables. Open *themes/simple/templates/Layout/Page.ss*. You will see more HTML and more SilverStripe template replacement tags and variables.
```ss ```ss
$Content $Content
``` ```
The Content variable is replaced with the content of the page currently being viewed. This allows you to make all changes to 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. your site's content in the CMS.
@ -151,7 +151,7 @@ browser and are either prefixed with a dollar sign ($)
or placed between SilverStripe template tags: or placed between SilverStripe template tags:
```ss ```ss
<% %> <% %>
``` ```
**Flushing the cache** **Flushing the cache**
@ -169,8 +169,8 @@ 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. 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 ```ss
<% loop $Menu(1) %> <% loop $Menu(1) %>
``` ```
returns a set of first level menu items. We can then use the template variable 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). *$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).
@ -179,13 +179,13 @@ returns a set of first level menu items. We can then use the template variable
```ss ```ss
<ul> <ul>
<% loop $Menu(1) %> <% loop $Menu(1) %>
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>"> <li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
<a href="$Link" title="$Title.XML">$MenuTitle.XML</a> <a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
</li> </li>
<% end_loop %> <% end_loop %>
</ul> </ul>
``` ```
Here we've created an unordered list called *Menu1*, which *themes/simple/css/layout.css* will style into the menu. Here we've created an unordered list called *Menu1*, which *themes/simple/css/layout.css* will style into the menu.
@ -195,8 +195,6 @@ This creates the navigation at the top of the page:
![](../_images/tutorial1_menu.jpg) ![](../_images/tutorial1_menu.jpg)
### Highlighting the current page ### Highlighting the current page
A useful feature is highlighting the current page the user is looking at. We can do this by using the `is` methods `$isSection` and `$isCurrent`. A useful feature is highlighting the current page the user is looking at. We can do this by using the `is` methods `$isSection` and `$isCurrent`.
@ -204,15 +202,15 @@ 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. 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 ```ss
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>"> <li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
<a href="$Link" title="$Title.XML">$MenuTitle.XML</a> <a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
</li> </li>
``` ```
you will then be able to target a section in css (*simple/css/layout.css*), e.g.: you will then be able to target a section in css (*simple/css/layout.css*), e.g.:
```css ```css
.section { background:#ccc; } .section { background:#ccc; }
``` ```
## A second level of navigation ## A second level of navigation
@ -235,16 +233,16 @@ 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: 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 ```ss
<ul> <ul>
<% loop $Menu(2) %> <% loop $Menu(2) %>
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>"> <li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
<a href="$Link" title="Go to the $Title.XML page"> <a href="$Link" title="Go to the $Title.XML page">
<span class="arrow"></span> <span class="arrow"></span>
<span class="text">$MenuTitle.XML</span> <span class="text">$MenuTitle.XML</span>
</a> </a>
</li> </li>
<% end_loop %> <% end_loop %>
</ul> </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)*. 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)*.
@ -257,20 +255,20 @@ Look again in the *Sidebar.ss* file and you will see that the menu is surrounded
like this: like this:
```ss ```ss
<% if $Menu(2) %> <% if $Menu(2) %>
... ...
<ul> <ul>
<% loop $Menu(2) %> <% loop $Menu(2) %>
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>"> <li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
<a href="$Link" title="Go to the $Title.XML page"> <a href="$Link" title="Go to the $Title.XML page">
<span class="arrow"></span> <span class="arrow"></span>
<span class="text">$MenuTitle.XML</span> <span class="text">$MenuTitle.XML</span>
</a> </a>
</li> </li>
<% end_loop %> <% end_loop %>
</ul> </ul>
... ...
<% end_if %> <% 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 The if block only includes the code inside it if the condition is true. In this case, it checks for the existence of
@ -282,11 +280,11 @@ 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: Open up */themes/simple/templates/Includes/BreadCrumbs.ss* template and look at the following code:
```ss ```ss
<% if $Level(2) %> <% if $Level(2) %>
<div id="Breadcrumbs"> <div id="Breadcrumbs">
$Breadcrumbs $Breadcrumbs
</div> </div>
<% end_if %> <% 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 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
@ -295,7 +293,7 @@ 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: 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 ```ss
$Level(1).Title $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. 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.
@ -309,28 +307,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. 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 ```ss
<ul> <ul>
<% loop $Menu(1) %> <% loop $Menu(1) %>
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>"> <li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
<a href="$Link" title="$Title.XML">$MenuTitle.XML</a> <a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
<% if $Children %> <% if $Children %>
<ul> <ul>
<% loop $Children %> <% loop $Children %>
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>"> <li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
<a href="$Link" title="Go to the $Title.XML page"> <a href="$Link" title="Go to the $Title.XML page">
<span class="arrow"></span> <span class="arrow"></span>
<span class="text">$MenuTitle.XML</span> <span class="text">$MenuTitle.XML</span>
</a> </a>
</li> </li>
<% end_loop %> <% end_loop %>
</ul> </ul>
<% end_if %> <% end_if %>
</li> </li>
<% end_loop %> <% end_loop %>
</ul> </ul>
``` ```
## Using a different template for the home page ## Using a different template for the home page
So far, a single template layout *Layouts/Page.ss* is being used for the entire site. This is useful for the purpose of this So far, a single template layout *Layouts/Page.ss* is being used for the entire site. This is useful for the purpose of this
@ -393,21 +390,21 @@ To create a new template layout, create a copy of *Page.ss* (found in *themes/si
First, we don't need the breadcrumbs and the secondary menu for the homepage. Let's remove them: First, we don't need the breadcrumbs and the secondary menu for the homepage. Let's remove them:
```ss ```ss
<% include SideBar %> <% include SideBar %>
``` ```
We'll also replace the title text with an image. Find this line: We'll also replace the title text with an image. Find this line:
```ss ```ss
<h1>$Title</h1> <h1>$Title</h1>
``` ```
and replace it with: and replace it with:
```ss ```ss
<div id="Banner"> <div id="Banner">
<img src="http://www.silverstripe.org/assets/SilverStripe-200.png" alt="Homepage image" /> <img src="http://www.silverstripe.org/assets/SilverStripe-200.png" alt="Homepage image" />
</div> </div>
``` ```
Your Home page should now look like this: Your Home page should now look like this:

View File

@ -9,7 +9,6 @@ This tutorial is deprecated, and has been replaced by Lessons 4, 5, and 6 in the
## Overview ## Overview
In the [first tutorial (Building a basic site)](/tutorials/building_a_basic_site) we learnt how to create a basic site using SilverStripe. This tutorial will build on that, and explore extending SilverStripe by creating our own page types. After doing this we should have a better understanding of how SilverStripe works. In the [first tutorial (Building a basic site)](/tutorials/building_a_basic_site) we learnt how to create a basic site using SilverStripe. This tutorial will build on that, and explore extending SilverStripe by creating our own page types. After doing this we should have a better understanding of how SilverStripe works.
## What are we working towards? ## What are we working towards?
@ -68,24 +67,20 @@ We'll start with the *ArticlePage* page type. First we create the model, a class
**mysite/code/ArticlePage.php** **mysite/code/ArticlePage.php**
```php ```php
use Page; class ArticlePage extends Page
{
}
```
**mysite/code/ArticlePageController.php**
class ArticlePage extends Page
{
}
```
**mysite/code/ArticlePageController.php**
```php ```php
use PageController; class ArticlePageController extends PageController
{
class ArticlePageController extends PageController
{
}
```
}
```
Here we've created our data object/controller pair, but we haven't extended them at all. SilverStripe will use the template for the *Page* page type as explained in the first tutorial, so we don't need Here we've created our data object/controller pair, but we haven't extended them at all. SilverStripe will use the template for the *Page* page type as explained in the first tutorial, so we don't need
to specifically create the view for this page type. to specifically create the view for this page type.
@ -95,21 +90,19 @@ Let's create the *ArticleHolder* page type.
**mysite/code/ArticleHolder.php** **mysite/code/ArticleHolder.php**
```php ```php
use Page; class ArticleHolder extends Page
{
class ArticleHolder extends Page private static $allowed_children = ['ArticlePage'];
{ }
private static $allowed_children = ['ArticlePage'];
}
``` ```
**mysite/code/ArticleHolderController.php**
```php
use PageController;
class ArticleHolderController extends PageController **mysite/code/ArticleHolderController.php**
{
```php
} 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 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
@ -132,17 +125,13 @@ the $db array to add extra fields to the database. It would be nice to know when
it. Add a *$db* property definition in the *ArticlePage* class: it. Add a *$db* property definition in the *ArticlePage* class:
```php ```php
use Page; class ArticlePage extends Page
{
class ArticlePage extends Page private static $db = [
{ 'Date' => 'Date',
private static $db = [ 'Author' => 'Text'
'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. 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.
@ -157,43 +146,36 @@ 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. 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
use Page; class ArticlePage extends Page
{
class ArticlePage extends Page public function getCMSFields()
{ {
// ... $fields = parent::getCMSFields();
public function getCMSFields() $dateField = new DateField('Date');
{ $dateField->setConfig('showcalendar', true);
$fields = parent::getCMSFields(); $fields->addFieldToTab('Root.Main', $dateField, 'Content');
$fields->addFieldToTab('Root.Main', new TextField('Author'), 'Content');
$dateField = new DateField('Date');
$dateField->setConfig('showcalendar', true); return $fields;
$fields->addFieldToTab('Root.Main', $dateField, 'Content');
$fields->addFieldToTab('Root.Main', new TextField('Author'), 'Content');
return $fields;
}
} }
}
// ...
``` ```
Let's walk through this method. Let's walk through this method.
```php ```php
$fields = parent::getCMSFields(); $fields = parent::getCMSFields();
``` ```
Firstly, we get the fields from the parent class; we want to add fields, not replace them. The *$fields* variable 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. returned is a [FieldList](api:SilverStripe\Forms\FieldList) object.
```php ```php
$fields->addFieldToTab('Root.Main', new TextField('Author'), 'Content'); $fields->addFieldToTab('Root.Main', new TextField('Author'), 'Content');
$fields->addFieldToTab('Root.Main', new DateField('Date'), '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: We can then add our new fields with *addFieldToTab*. The first argument is the tab on which we want to add the field to:
@ -208,8 +190,8 @@ We have added two fields: A simple [TextField](api:SilverStripe\Forms\TextField)
There are many more fields available in the default installation, listed in ["form field types"](/developer_guides/forms/field_types/common_subclasses). There are many more fields available in the default installation, listed in ["form field types"](/developer_guides/forms/field_types/common_subclasses).
```php ```php
return $fields; 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. 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.
@ -227,51 +209,47 @@ To make the date field a bit more user friendly, you can add a dropdown calendar
the date field will have the date format defined by your locale. the date field will have the date format defined by your locale.
```php ```php
use Page; class ArticlePage extends Page
{
class ArticlePage extends Page public function getCMSFields()
{ {
$fields = parent::getCMSFields();
// .....
$dateField = new DateField('Date', 'Article Date (for example: 20/12/2010)');
public function getCMSFields() $dateField->setConfig('showcalendar', true);
{ $dateField->setConfig('dateformat', 'dd/MM/YYYY');
$fields = parent::getCMSFields();
$dateField = new DateField('Date', 'Article Date (for example: 20/12/2010)'); $fields->addFieldToTab('Root.Main', $dateField, 'Content');
$dateField->setConfig('showcalendar', true); $fields->addFieldToTab('Root.Main', new TextField('Author', 'Author Name'), 'Content');
$dateField->setConfig('dateformat', 'dd/MM/YYYY');
$fields->addFieldToTab('Root.Main', $dateField, 'Content');
$fields->addFieldToTab('Root.Main', new TextField('Author', 'Author Name'), 'Content');
return $fields; return $fields;
} }
}
``` ```
Let's walk through these changes. Let's walk through these changes.
```php ```php
$dateField = new DateField('Date', 'Article Date (for example: 20/12/2010)'); $dateField = new DateField('Date', 'Article Date (for example: 20/12/2010)');
``` ```
*$dateField* is declared in order to change the configuration of the DateField. *$dateField* is declared in order to change the configuration of the DateField.
```php ```php
$dateField->setConfig('showcalendar', true); $dateField->setConfig('showcalendar', true);
``` ```
By enabling *showCalendar* you show a calendar overlay when clicking on the field. By enabling *showCalendar* you show a calendar overlay when clicking on the field.
```php ```php
$dateField->setConfig('dateformat', 'dd/MM/YYYY'); $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. *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 ```php
$fields->addFieldToTab('Root.Main', new TextField('Author', 'Author Name'), 'Content'); $fields->addFieldToTab('Root.Main', new TextField('Author', 'Author Name'), 'Content');
``` ```
By default the field name *'Date'* or *'Author'* is shown as the title, however this might not be that helpful so to change the title, add the new title as the second argument. 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.
@ -290,17 +268,17 @@ First, the template for displaying a single article:
```ss ```ss
<% include SideBar %> <% include SideBar %>
<div class="content-container unit size3of4 lastUnit"> <div class="content-container unit size3of4 lastUnit">
<article> <article>
<h1>$Title</h1> <h1>$Title</h1>
<div class="news-details"> <div class="news-details">
<p>Posted on $Date.Nice by $Author</p> <p>Posted on $Date.Nice by $Author</p>
</div> </div>
<div class="content">$Content</div> <div class="content">$Content</div>
</article> </article>
$Form $Form
</div> </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. 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.
@ -323,22 +301,22 @@ We'll now create a template for the article holder. We want our news section to
**themes/simple/templates/Layout/ArticleHolder.ss** **themes/simple/templates/Layout/ArticleHolder.ss**
```ss ```ss
<% include SideBar %> <% include SideBar %>
<div class="content-container unit size3of4 lastUnit"> <div class="content-container unit size3of4 lastUnit">
<article>
<h1>$Title</h1>
$Content
<div class="content">$Content</div>
</article>
<% loop $Children %>
<article> <article>
<h1>$Title</h1> <h2><a href="$Link" title="Read more on &quot;{$Title}&quot;">$Title</a></h2>
$Content <p>$Content.FirstParagraph</p>
<div class="content">$Content</div> <a href="$Link" title="Read more on &quot;{$Title}&quot;">Read more &gt;&gt;</a>
</article> </article>
<% loop $Children %> <% end_loop %>
<article> $Form
<h2><a href="$Link" title="Read more on &quot;{$Title}&quot;">$Title</a></h2> </div>
<p>$Content.FirstParagraph</p>
<a href="$Link" title="Read more on &quot;{$Title}&quot;">Read more &gt;&gt;</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. 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.
@ -357,22 +335,21 @@ Cut the code between "loop Children" in *ArticleHolder.ss** and replace it with
**themes/simple/templates/Layout/ArticleHolder.ss** **themes/simple/templates/Layout/ArticleHolder.ss**
```ss ```ss
... <% loop $Children %>
<% loop $Children %> <% include ArticleTeaser %>
<% include ArticleTeaser %> <% end_loop %>
<% end_loop %>
...
``` ```
Paste the code that was in ArticleHolder into a new include file called ArticleTeaser.ss: Paste the code that was in ArticleHolder into a new include file called ArticleTeaser.ss:
**themes/simple/templates/Includes/ArticleTeaser.ss** **themes/simple/templates/Includes/ArticleTeaser.ss**
```ss ```ss
<article> <article>
<h2><a href="$Link" title="Read more on &quot;{$Title}&quot;">$Title</a></h2> <h2><a href="$Link" title="Read more on &quot;{$Title}&quot;">$Title</a></h2>
<p>$Content.FirstParagraph</p> <p>$Content.FirstParagraph</p>
<a href="$Link" title="Read more on &quot;{$Title}&quot;">Read more &gt;&gt;</a> <a href="$Link" title="Read more on &quot;{$Title}&quot;">Read more &gt;&gt;</a>
</article> </article>
``` ```
### Changing the icons of pages in the CMS ### Changing the icons of pages in the CMS
@ -381,13 +358,13 @@ Now let's make a purely cosmetic change that nevertheless helps to make the info
Add the following field to the *ArticleHolder* and *ArticlePage* classes: Add the following field to the *ArticleHolder* and *ArticlePage* classes:
```php ```php
private static $icon = "cms/images/treeicons/news-file.gif"; private static $icon = "cms/images/treeicons/news-file.gif";
``` ```
And this one to the *HomePage* class: And this one to the *HomePage* class:
```php ```php
private static $icon = "cms/images/treeicons/home-file.png"; private static $icon = "cms/images/treeicons/home-file.png";
``` ```
This will change the icons for the pages in the CMS. This will change the icons for the pages in the CMS.
@ -405,12 +382,11 @@ It would be nice to greet page visitors with a summary of the latest news when t
**mysite/code/HomePage.php** **mysite/code/HomePage.php**
```php ```php
// ... public function LatestNews($num=5)
public function LatestNews($num=5) {
{ $holder = ArticleHolder::get()->First();
$holder = ArticleHolder::get()->First(); return ($holder) ? ArticlePage::get()->filter('ParentID', $holder->ID)->sort('Date DESC')->limit($num) : false;
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: 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:
@ -418,12 +394,12 @@ This function simply runs a database query that gets the latest news articles fr
**themes/simple/templates/Layout/Homepage.ss** **themes/simple/templates/Layout/Homepage.ss**
```ss ```ss
<!-- ... --> <!-- ... -->
<div class="content">$Content</div> <div class="content">$Content</div>
</article> </article>
<% loop $LatestNews %> <% loop $LatestNews %>
<% include ArticleTeaser %> <% include ArticleTeaser %>
<% end_loop %> <% 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. 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.
@ -432,8 +408,6 @@ The controller for a page is only created when page is actually visited, while t
![](../_images/tutorial2_homepage-news.jpg) ![](../_images/tutorial2_homepage-news.jpg)
## Creating a RSS feed ## 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 *ArticleHolderController* 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:
@ -441,15 +415,15 @@ An RSS feed is something that no news section should be without. SilverStripe ma
**mysite/code/ArticleHolder.php** **mysite/code/ArticleHolder.php**
```php ```php
private static $allowed_actions = [ private static $allowed_actions = [
'rss' 'rss'
]; ];
public function rss() public function rss()
{ {
$rss = new RSSFeed($this->Children(), $this->Link(), "The coolest news around"); $rss = new RSSFeed($this->Children(), $this->Link(), "The coolest news around");
return $rss->outputToBrowser(); return $rss->outputToBrowser();
} }
``` ```
Ensure that when you have input the code to implement an RSS feed; flush the webpage afterwards Ensure that when you have input the code to implement an RSS feed; flush the webpage afterwards
@ -466,11 +440,11 @@ Now all we need is to let the user know that our RSS feed exists. Add this funct
**mysite/code/ArticleHolder.php** **mysite/code/ArticleHolder.php**
```php ```php
public function init() public function init()
{ {
RSSFeed::linkToFeed($this->Link() . "rss"); RSSFeed::linkToFeed($this->Link() . "rss");
parent::init(); 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. 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.
@ -482,25 +456,21 @@ Now that we have a complete news section, let's take a look at the staff section
**mysite/code/StaffHolder.php** **mysite/code/StaffHolder.php**
```php ```php
use Page; class StaffHolder extends Page
{
class StaffHolder extends Page private static $db = [];
{ private static $has_one = [];
private static $db = []; private static $allowed_children = [StaffPage::class];
private static $has_one = []; }
private static $allowed_children = [StaffPage::class];
}
``` ```
**mysite/code/StaffHolderController.php** **mysite/code/StaffHolderController.php**
```php ```php
use PageController; class StaffHolderController extends 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). 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).
@ -508,37 +478,35 @@ Nothing here should be new. The *StaffPage* page type is more interesting though
**mysite/code/StaffPage.php** **mysite/code/StaffPage.php**
```php ```php
use SilverStripe\AssetAdmin\Forms\UploadField; use SilverStripe\AssetAdmin\Forms\UploadField;
use SilverStripe\Assets\Image; use SilverStripe\Assets\Image;
class StaffPage extends Page class StaffPage extends Page
{
private static $db = [];
private static $has_one = [
'Photo' => Image::class
];
public function getCMSFields()
{ {
private static $db = []; $fields = parent::getCMSFields();
private static $has_one = [
'Photo' => Image::class
];
public function getCMSFields() $fields->addFieldToTab("Root.Images", new UploadField('Photo'));
{
$fields = parent::getCMSFields(); return $fields;
$fields->addFieldToTab("Root.Images", new UploadField('Photo'));
return $fields;
}
} }
}
``` ```
**mysite/code/StaffPageController.php** **mysite/code/StaffPageController.php**
```php ```php
use PageController; class StaffPageController extends 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. 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.
@ -562,24 +530,23 @@ The staff section templates aren't too difficult to create, thanks to the utilit
**themes/simple/templates/Layout/StaffHolder.ss** **themes/simple/templates/Layout/StaffHolder.ss**
```ss ```ss
<% include SideBar %> <% include SideBar %>
<div class="content-container unit size3of4 lastUnit"> <div class="content-container unit size3of4 lastUnit">
<article>
<h1>$Title</h1>
$Content
<div class="content">$Content</div>
</article>
<% loop $Children %>
<article> <article>
<h1>$Title</h1> <h2><a href="$Link" title="Read more on &quot;{$Title}&quot;">$Title</a></h2>
$Content $Photo.ScaleWidth(150)
<div class="content">$Content</div> <p>$Content.FirstParagraph</p>
<a href="$Link" title="Read more on &quot;{$Title}&quot;">Read more &gt;&gt;</a>
</article> </article>
<% loop $Children %> <% end_loop %>
<article> $Form
<h2><a href="$Link" title="Read more on &quot;{$Title}&quot;">$Title</a></h2> </div>
$Photo.ScaleWidth(150)
<p>$Content.FirstParagraph</p>
<a href="$Link" title="Read more on &quot;{$Title}&quot;">Read more &gt;&gt;</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 This template is very similar to the *ArticleHolder* template. The *ScaleWidth* method of the [Image](api:SilverStripe\Assets\Image) class
@ -593,16 +560,16 @@ The *StaffPage* template is also very straight forward.
**themes/simple/templates/Layout/StaffPage.ss** **themes/simple/templates/Layout/StaffPage.ss**
```ss ```ss
<% include SideBar %> <% include SideBar %>
<div class="content-container unit size3of4 lastUnit"> <div class="content-container unit size3of4 lastUnit">
<article> <article>
<h1>$Title</h1> <h1>$Title</h1>
<div class="content"> <div class="content">
$Photo.ScaleWidth(433) $Photo.ScaleWidth(433)
$Content</div> $Content</div>
</article> </article>
$Form $Form
</div> </div>
``` ```
Here we use the *ScaleWidth* method to get a different sized image from the same source image. You should now have Here we use the *ScaleWidth* method to get a different sized image from the same source image. You should now have

View File

@ -19,8 +19,8 @@ To enable the search engine you need to include the following code in your `mysi
This will enable fulltext search on page content as well as names of all files in the `/assets` folder. This will enable fulltext search on page content as well as names of all files in the `/assets` folder.
```php ```php
FulltextSearchable::enable(); 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. 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.
@ -36,14 +36,13 @@ To add the search form, we can add `$SearchForm` anywhere in our templates. In t
**themes/simple/templates/Includes/Header.ss** **themes/simple/templates/Includes/Header.ss**
```ss ```ss
... <% if $SearchForm %>
<% if $SearchForm %> <span class="search-dropdown-icon">L</span>
<span class="search-dropdown-icon">L</span> <div class="search-bar">
<div class="search-bar"> $SearchForm
$SearchForm </div>
</div> <% end_if %>
<% end_if %> <% include Navigation %>
<% include Navigation %>
``` ```
This displays as: This displays as:
@ -58,23 +57,20 @@ is applied via `FulltextSearchable::enable()`
**cms/code/search/ContentControllerSearchExtension.php** **cms/code/search/ContentControllerSearchExtension.php**
```php ```php
use SilverStripe\Core\Extension; use SilverStripe\Core\Extension;
class ContentControllerSearchExtension extends Extension class ContentControllerSearchExtension extends Extension
{
public function results($data, $form, $request)
{ {
... $data = [
'Results' => $form->getResults(),
public function results($data, $form, $request) 'Query' => $form->getSearchQuery(),
{ 'Title' => _t('SearchForm.SearchResults', 'Search Results')
$data = [ ];
'Results' => $form->getResults(), return $this->owner->customise($data)->renderWith(['Page_results', 'Page']);
'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. 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.
@ -105,56 +101,56 @@ class.
*themes/simple/templates/Layout/Page_results.ss* *themes/simple/templates/Layout/Page_results.ss*
```ss ```ss
<div id="Content" class="searchResults"> <div id="Content" class="searchResults">
<h1>$Title</h1> <h1>$Title</h1>
<% if $Query %>
<p class="searchQuery"><strong>You searched for &quot;{$Query}&quot;</strong></p>
<% end_if %>
<% if $Query %> <% if $Results %>
<p class="searchQuery"><strong>You searched for &quot;{$Query}&quot;</strong></p> <ul id="SearchResults">
<% end_if %> <% 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 &quot;{$Title}&quot;"
>Read more about &quot;{$Title}&quot;...</a>
</li>
<% end_loop %>
</ul>
<% else %>
<p>Sorry, your search query did not return any results.</p>
<% end_if %>
<% if $Results %> <% if $Results.MoreThanOnePage %>
<ul id="SearchResults"> <div id="PageNumbers">
<% loop $Results %> <% if $Results.NotLastPage %>
<li> <a class="next" href="$Results.NextLink" title="View the next page">Next</a>
<a class="searchResultHeader" href="$Link"> <% end_if %>
<% if $MenuTitle %> <% if $Results.NotFirstPage %>
$MenuTitle <a class="prev" href="$Results.PrevLink" title="View the previous page">Prev</a>
<% else %> <% end_if %>
$Title <span>
<% end_if %> <% loop $Results.Pages %>
</a> <% if $CurrentBool %>
<p>$Content.LimitWordCountXML</p> $PageNum
<a class="readMoreLink" href="$Link" <% else %>
title="Read more about &quot;{$Title}&quot;" <a href="$Link" title="View page number $PageNum">$PageNum</a>
>Read more about &quot;{$Title}&quot;...</a> <% end_if %>
</li>
<% end_loop %> <% end_loop %>
</ul> </span>
<% else %> <p>Page $Results.CurrentPage of $Results.TotalPages</p>
<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> </div>
<% end_if %>
</div>
``` ```
Then finally add ?flush=1 to the URL and you should see the new template. Then finally add ?flush=1 to the URL and you should see the new template.

View File

@ -20,19 +20,17 @@ Let's look at a simple example:
**mysite/code/Player.php** **mysite/code/Player.php**
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject
{
private static $db = [
'PlayerNumber' => 'Int',
'FirstName' => 'Varchar(255)',
'LastName' => 'Text',
'Birthday' => 'Date'
];
}
class Player extends DataObject
{
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 This `Player` class definition will create a database table `Player` with columns for `PlayerNumber`, `FirstName` and
@ -78,49 +76,49 @@ automatically set on the `DataObject`.
**mysite/code/Player.php** **mysite/code/Player.php**
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject
{
private static $db = [
'PlayerNumber' => 'Int',
'FirstName' => 'Varchar(255)',
'LastName' => 'Text',
'Birthday' => 'Date'
];
}
class Player extends DataObject
{
private static $db = [
'PlayerNumber' => 'Int',
'FirstName' => 'Varchar(255)',
'LastName' => 'Text',
'Birthday' => 'Date'
];
}
``` ```
Generates the following `SQL`. Generates the following `SQL`.
CREATE TABLE `Player` ( ```sql
`ID` int(11) NOT NULL AUTO_INCREMENT, CREATE TABLE `Player` (
`ClassName` enum('Player') DEFAULT 'Player', `ID` int(11) NOT NULL AUTO_INCREMENT,
`LastEdited` datetime DEFAULT NULL, `ClassName` enum('Player') DEFAULT 'Player',
`Created` datetime DEFAULT NULL, `LastEdited` datetime DEFAULT NULL,
`PlayerNumber` int(11) NOT NULL DEFAULT '0', `Created` datetime DEFAULT NULL,
`FirstName` varchar(255) DEFAULT NULL, `PlayerNumber` int(11) NOT NULL DEFAULT '0',
`LastName` mediumtext, `FirstName` varchar(255) DEFAULT NULL,
`Birthday` datetime DEFAULT NULL, `LastName` mediumtext,
`Birthday` datetime DEFAULT NULL,
PRIMARY KEY (`ID`),
KEY `ClassName` (`ClassName`) PRIMARY KEY (`ID`),
); KEY `ClassName` (`ClassName`)
);
```
## Creating Data Records ## Creating Data Records
A new instance of a [DataObject](api:SilverStripe\ORM\DataObject) can be created using the `new` syntax. A new instance of a [DataObject](api:SilverStripe\ORM\DataObject) can be created using the `new` syntax.
```php ```php
$player = new Player(); $player = new Player();
``` ```
Or, a better way is to use the `create` method. Or, a better way is to use the `create` method.
```php ```php
$player = Player::create(); $player = Player::create();
``` ```
<div class="notice" markdown='1'> <div class="notice" markdown='1'>
@ -132,22 +130,22 @@ Database columns and properties can be set as class properties on the object. Th
of the values through a custom `__set()` method. of the values through a custom `__set()` method.
```php ```php
$player->FirstName = "Sam"; $player->FirstName = "Sam";
$player->PlayerNumber = 07; $player->PlayerNumber = 07;
``` ```
To save the `DataObject` to the database, use the `write()` method. The first time `write()` is called, an `ID` will be To save the `DataObject` to the database, use the `write()` method. The first time `write()` is called, an `ID` will be
set. set.
```php ```php
$player->write(); $player->write();
``` ```
For convenience, the `write()` method returns the record's ID. This is particularly useful when creating new records. For convenience, the `write()` method returns the record's ID. This is particularly useful when creating new records.
```php ```php
$player = Player::create(); $player = Player::create();
$id = $player->write(); $id = $player->write();
``` ```
## Querying Data ## Querying Data
@ -156,28 +154,28 @@ With the `Player` class defined we can query our data using the `ORM` or Object-
shortcuts and methods for fetching, sorting and filtering data from our database. shortcuts and methods for fetching, sorting and filtering data from our database.
```php ```php
$players = Player::get(); $players = Player::get();
// returns a `DataList` containing all the `Player` objects. // returns a `DataList` containing all the `Player` objects.
$player = Player::get()->byID(2); $player = Player::get()->byID(2);
// returns a single `Player` object instance that has the ID of 2. // returns a single `Player` object instance that has the ID of 2.
echo $player->ID; echo $player->ID;
// returns the players 'ID' column value // returns the players 'ID' column value
echo $player->dbObject('LastEdited')->Ago(); echo $player->dbObject('LastEdited')->Ago();
// calls the `Ago` method on the `LastEdited` property. // 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 The `ORM` uses a "fluent" syntax, where you specify a query by chaining together different methods. Two common methods
are `filter()` and `sort()`: are `filter()` and `sort()`:
```php ```php
$members = Player::get()->filter([ $members = Player::get()->filter([
'FirstName' => 'Sam' 'FirstName' => 'Sam'
])->sort('Surname'); ])->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'
``` ```
@ -193,50 +191,48 @@ It's smart enough to generate a single efficient query at the last moment in tim
result set in PHP. In `MySQL` the query generated by the ORM may look something like this result set in PHP. In `MySQL` the query generated by the ORM may look something like this
```php ```php
$players = Player::get()->filter([ $players = Player::get()->filter([
'FirstName' => 'Sam' '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. This also means that getting the count of a list of objects will be done with a single, efficient query.
```php ```php
$players = Player::get()->filter([ $players = Player::get()->filter([
'FirstName' => 'Sam' 'FirstName' => 'Sam'
])->sort('Surname'); ])->sort('Surname');
// This will create an single SELECT COUNT query
// SELECT COUNT(*) FROM Player WHERE FirstName = 'Sam'
echo $players->Count();
// This will create an single SELECT COUNT query
// SELECT COUNT(*) FROM Player WHERE FirstName = 'Sam'
echo $players->Count();
``` ```
## Looping over a list of objects ## Looping over a list of objects
`get()` returns a `DataList` instance. You can loop over `DataList` instances in both PHP and templates. `get()` returns a `DataList` instance. You can loop over `DataList` instances in both PHP and templates.
```php
$players = Player::get();
foreach($players as $player) { ```php
echo $player->FirstName; $players = Player::get();
}
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. 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 ```php
$players = Player::get(); $players = Player::get();
if($players->exists()) { if($players->exists()) {
// do something here // do something here
} }
``` ```
See the [Lists](lists) documentation for more information on dealing with [SS_List](api:SilverStripe\ORM\SS_List) instances. See the [Lists](lists) documentation for more information on dealing with [SS_List](api:SilverStripe\ORM\SS_List) instances.
@ -247,16 +243,16 @@ There are a couple of ways of getting a single DataObject from the ORM. If you k
can use `byID($id)`: can use `byID($id)`:
```php ```php
$player = Player::get()->byID(5); $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. `get()` returns a [DataList](api:SilverStripe\ORM\DataList) instance. You can use operations on that to get back a single record.
```php ```php
$players = Player::get(); $players = Player::get();
$first = $players->first(); $first = $players->first();
$last = $players->last(); $last = $players->last();
``` ```
## Sorting ## Sorting
@ -264,38 +260,37 @@ can use `byID($id)`:
If would like to sort the list by `FirstName` in a ascending way (from A to Z). If would like to sort the list by `FirstName` in a ascending way (from A to Z).
```php ```php
// Sort can either be Ascending (ASC) or Descending (DESC) // Sort can either be Ascending (ASC) or Descending (DESC)
$players = Player::get()->sort('FirstName', 'ASC'); $players = Player::get()->sort('FirstName', 'ASC');
// Ascending is implied // Ascending is implied
$players = Player::get()->sort('FirstName'); $players = Player::get()->sort('FirstName');
``` ```
To reverse the sort To reverse the sort
```php ```php
$players = Player::get()->sort('FirstName', 'DESC'); $players = Player::get()->sort('FirstName', 'DESC');
// or.. // or..
$players = Player::get()->sort('FirstName', 'ASC')->reverse(); $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 However you might have several entries with the same `FirstName` and would like to sort them by `FirstName` and
`LastName` `LastName`
```php ```php
$players = Players::get()->sort([ $players = Players::get()->sort([
'FirstName' => 'ASC', 'FirstName' => 'ASC',
'LastName'=>'ASC' 'LastName'=>'ASC'
]); ]);
``` ```
You can also sort randomly. Using the `DB` class, you can get the random sort method per database type. You can also sort randomly. Using the `DB` class, you can get the random sort method per database type.
```php ```php
$random = DB::get_conn()->random(); $random = DB::get_conn()->random();
$players = Player::get()->sort($random) $players = Player::get()->sort($random)
``` ```
## Filtering Results ## Filtering Results
@ -303,10 +298,9 @@ You can also sort randomly. Using the `DB` class, you can get the random sort me
The `filter()` method filters the list of objects that gets returned. The `filter()` method filters the list of objects that gets returned.
```php ```php
$players = Player::get()->filter([ $players = Player::get()->filter([
'FirstName' => 'Sam' 'FirstName' => 'Sam'
]); ]);
``` ```
Each element of the array specifies a filter. You can specify as many filters as you like, and they **all** must be Each element of the array specifies a filter. You can specify as many filters as you like, and they **all** must be
@ -318,41 +312,38 @@ value that you want to filter to.
So, this would return only those players called "Sam Minnée". So, this would return only those players called "Sam Minnée".
```php ```php
$players = Player::get()->filter([ $players = Player::get()->filter([
'FirstName' => 'Sam', 'FirstName' => 'Sam',
'LastName' => 'Minnée', '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. There is also a shorthand way of getting Players with the FirstName of Sam.
```php ```php
$players = Player::get()->filter('FirstName', 'Sam'); $players = Player::get()->filter('FirstName', 'Sam');
``` ```
Or if you want to find both Sam and Sig. Or if you want to find both Sam and Sig.
```php ```php
$players = Player::get()->filter( $players = Player::get()->filter(
'FirstName', ['Sam', 'Sig'] '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 You can use [SearchFilters](searchfilters) to add additional behavior to your `filter` command rather than an
exact match. exact match.
```php ```php
$players = Player::get()->filter([ $players = Player::get()->filter([
'FirstName:StartsWith' => 'S' 'FirstName:StartsWith' => 'S',
'PlayerNumber:GreaterThan' => '10' 'PlayerNumber:GreaterThan' => '10',
]); ]);
``` ```
### filterAny ### filterAny
@ -360,38 +351,35 @@ exact match.
Use the `filterAny()` method to match multiple criteria non-exclusively (with an "OR" disjunctive), Use the `filterAny()` method to match multiple criteria non-exclusively (with an "OR" disjunctive),
```php ```php
$players = Player::get()->filterAny([ $players = Player::get()->filterAny([
'FirstName' => 'Sam', 'FirstName' => 'Sam',
'Age' => 17, '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. You can combine both conjunctive ("AND") and disjunctive ("OR") statements.
```php ```php
$players = Player::get() $players = Player::get()
->filter([ ->filter([
'LastName' => 'Minnée' 'LastName' => 'Minnée',
]) ])
->filterAny([ ->filterAny([
'FirstName' => 'Sam', 'FirstName' => 'Sam',
'Age' => 17, 'Age' => 17,
]); ]);
// SELECT * FROM Player WHERE ("LastName" = 'Minnée' AND ("FirstName" = 'Sam' OR "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. You can use [SearchFilters](searchfilters) to add additional behavior to your `filterAny` command.
```php ```php
$players = Player::get()->filterAny([ $players = Player::get()->filterAny([
'FirstName:StartsWith' => 'S' 'FirstName:StartsWith' => 'S',
'PlayerNumber:GreaterThan' => '10' 'PlayerNumber:GreaterThan' => '10',
]); ]);
``` ```
### Filtering by null values ### Filtering by null values
@ -405,30 +393,28 @@ For instance, the below code will select only values that do not match the given
```php ```php
$players = Player::get()->filter('FirstName:not', 'Sam'); $players = Player::get()->filter('FirstName:not', 'Sam');
// ... WHERE "FirstName" != 'Sam' OR "FirstName" IS NULL // ... WHERE "FirstName" != 'Sam' OR "FirstName" IS NULL
// Returns rows with any value (even null) other than Sam // Returns rows with any value (even null) other than Sam
``` ```
If null values should be excluded, include the null in your check. If null values should be excluded, include the null in your check.
```php ```php
$players = Player::get()->filter('FirstName:not', ['Sam', null]); $players = Player::get()->filter('FirstName:not', ['Sam', null]);
// ... WHERE "FirstName" != 'Sam' AND "FirstName" IS NOT NULL // ... WHERE "FirstName" != 'Sam' AND "FirstName" IS NOT NULL
// Only returns non-null values for "FirstName" that aren't Sam. // Only returns non-null values for "FirstName" that aren't Sam.
// Strictly the IS NOT NULL isn't necessary, but is included for explicitness // 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. It is also often useful to filter by all rows with either empty or null for a given field.
```php ```php
$players = Player::get()->filter('FirstName', [null, '']); $players = Player::get()->filter('FirstName', [null, '']);
// ... WHERE "FirstName" == '' OR "FirstName" IS NULL // ... WHERE "FirstName" == '' OR "FirstName" IS NULL
// Returns rows with FirstName which is either empty or null // Returns rows with FirstName which is either empty or null
``` ```
### Filtering by aggregates ### Filtering by aggregates
@ -436,17 +422,17 @@ It is also often useful to filter by all rows with either empty or null for a gi
You can use aggregate expressions in your filters, as well. You can use aggregate expressions in your filters, as well.
```php ```php
// get the teams that have more than 10 players // get the teams that have more than 10 players
$teams = Team::get()->filter('Players.Count():GreaterThan', 10); $teams = Team::get()->filter('Players.Count():GreaterThan', 10);
// get the teams with at least one player who has scored 5 or more points // get the teams with at least one player who has scored 5 or more points
$teams = Team::get()->filter('Players.Min(PointsScored):GreaterThanOrEqual', 5); $teams = Team::get()->filter('Players.Min(PointsScored):GreaterThanOrEqual', 5);
// get the teams with players who are averaging more than 15 points // get the teams with players who are averaging more than 15 points
$teams = Team::get()->filter('Players.Avg(PointsScored):GreaterThan', 15); $teams = Team::get()->filter('Players.Avg(PointsScored):GreaterThan', 15);
// get the teams whose players have scored less than 300 points combined // get the teams whose players have scored less than 300 points combined
$teams = Team::get()->filter('Players.Sum(PointsScored):LessThan', 300); $teams = Team::get()->filter('Players.Sum(PointsScored):LessThan', 300);
``` ```
### filterByCallback ### filterByCallback
@ -466,9 +452,9 @@ 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. The below example will get all `Players` aged over 10.
```php ```php
$players = Player::get()->filterByCallback(function($item, $list) { $players = Player::get()->filterByCallback(function($item, $list) {
return ($item->Age() > 10); return ($item->Age() > 10);
}); });
``` ```
### Exclude ### Exclude
@ -476,59 +462,56 @@ The below example will get all `Players` aged over 10.
The `exclude()` method is the opposite to the filter in that it removes entries from a list. The `exclude()` method is the opposite to the filter in that it removes entries from a list.
```php ```php
$players = Player::get()->exclude('FirstName', 'Sam'); $players = Player::get()->exclude('FirstName', 'Sam');
// SELECT * FROM Player WHERE FirstName != 'Sam' // SELECT * FROM Player WHERE FirstName != 'Sam'
``` ```
Remove both Sam and Sig.. Remove both Sam and Sig..
```php ```php
$players = Player::get()->exclude( $players = Player::get()->exclude(
'FirstName', ['Sam','Sig'] 'FirstName', ['Sam','Sig']
); );
``` ```
`Exclude` follows the same pattern as filter, so for removing only Sam Minnée from the list: `Exclude` follows the same pattern as filter, so for removing only Sam Minnée from the list:
```php ```php
$players = Player::get()->exclude(array( $players = Player::get()->exclude(array(
'FirstName' => 'Sam', 'FirstName' => 'Sam',
'Surname' => 'Minnée', 'Surname' => 'Minnée',
)); ));
// SELECT * FROM Player WHERE (FirstName != 'Sam' OR LastName != 'Minnée') // SELECT * FROM Player WHERE (FirstName != 'Sam' OR LastName != 'Minnée')
``` ```
Removing players with *either* the first name of Sam or the last name of Minnée requires multiple `->exclude` calls: Removing players with *either* the first name of Sam or the last name of Minnée requires multiple `->exclude` calls:
```php ```php
$players = Player::get()->exclude('FirstName', 'Sam')->exclude('Surname', 'Minnée'); $players = Player::get()->exclude('FirstName', 'Sam')->exclude('Surname', 'Minnée');
// SELECT * FROM Player WHERE FirstName != 'Sam' AND LastName != 'Minnée' // SELECT * FROM Player WHERE FirstName != 'Sam' AND LastName != 'Minnée'
``` ```
And removing Sig and Sam with that are either age 17 or 43. And removing Sig and Sam with that are either age 17 or 43.
```php ```php
$players = Player::get()->exclude([ $players = Player::get()->exclude([
'FirstName' => ['Sam', 'Sig'], 'FirstName' => ['Sam', 'Sig'],
'Age' => [17, 43] '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. You can use [SearchFilters](searchfilters) to add additional behavior to your `exclude` command.
```php ```php
$players = Player::get()->exclude([ $players = Player::get()->exclude([
'FirstName:EndsWith' => 'S' 'FirstName:EndsWith' => 'S',
'PlayerNumber:LessThanOrEqual' => '10' 'PlayerNumber:LessThanOrEqual' => '10'
]); ]);
``` ```
### Subtract ### Subtract
@ -536,18 +519,19 @@ You can use [SearchFilters](searchfilters) to add additional behavior to your `e
You can subtract entries from a [DataList](api:SilverStripe\ORM\DataList) by passing in another DataList to `subtract()` You can subtract entries from a [DataList](api:SilverStripe\ORM\DataList) by passing in another DataList to `subtract()`
```php ```php
$sam = Player::get()->filter('FirstName', 'Sam'); $sam = Player::get()->filter('FirstName', 'Sam');
$players = Player::get(); $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 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. when you want to find all the members that does not exist in a Group.
```php ```php
// ... Finding all members that does not belong to $group. // ... Finding all members that does not belong to $group.
$otherMembers = Member::get()->subtract($group->Members()); use SilverStripe\Security\Member;
$otherMembers = Member::get()->subtract($group->Members());
``` ```
### Limit ### Limit
@ -555,7 +539,8 @@ when you want to find all the members that does not exist in a Group.
You can limit the amount of records returned in a DataList by using the `limit()` method. You can limit the amount of records returned in a DataList by using the `limit()` method.
```php ```php
$members = Member::get()->limit(5); use SilverStripe\Security\Member;
$members = Member::get()->limit(5);
``` ```
`limit()` accepts two arguments, the first being the amount of results you want returned, with an optional second `limit()` accepts two arguments, the first being the amount of results you want returned, with an optional second
@ -563,8 +548,8 @@ parameter to specify the offset, which allows you to tell the system where to st
offset, if not provided as an argument, will default to 0. offset, if not provided as an argument, will default to 0.
```php ```php
// Return 10 members with an offset of 4 (starting from the 5th result). // Return 10 members with an offset of 4 (starting from the 5th result).
$members = Member::get()->sort('Surname')->limit(10, 4); $members = Member::get()->sort('Surname')->limit(10, 4);
``` ```
<div class="alert"> <div class="alert">
@ -581,11 +566,13 @@ For instance, the below model will be stored in the table name `BannerImage`
```php ```php
namespace SilverStripe\BannerManager; namespace SilverStripe\BannerManager;
class BannerImage extends \DataObject use SilverStripe\ORM\DataObject;
{
private static $table_name = 'BannerImage'; 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 Note that any model class which does not explicitly declare a `table_name` config option will have a name
@ -616,14 +603,17 @@ table and column.
```php ```php
public function countDuplicates($model, $fieldToCheck) use SilverStripe\ORM\Queries\SQLSelect;
{ use SilverStripe\ORM\DataObject;
$table = DataObject::getSchema()->tableForField($model, $field);
$query = new SQLSelect(); public function countDuplicates($model, $fieldToCheck)
$query->setFrom("\"{$table}\""); {
$query->setWhere(["\"{$table}\".\"{$field}\"" => $model->$fieldToCheck]); $table = DataObject::getSchema()->tableForField($model, $field);
return $query->count(); $query = new SQLSelect();
} $query->setFrom("\"{$table}\"");
$query->setWhere(["\"{$table}\".\"{$field}\"" => $model->$fieldToCheck]);
return $query->count();
}
``` ```
### Raw SQL ### Raw SQL
@ -643,7 +633,7 @@ 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: You can specify a WHERE clause fragment (that will be combined with other filters using AND) with the `where()` method:
```php ```php
$members = Member::get()->where("\"FirstName\" = 'Sam'") $members = Member::get()->where("\"FirstName\" = 'Sam'")
``` ```
#### Joining Tables #### Joining Tables
@ -655,12 +645,12 @@ You can specify a join with the `innerJoin` and `leftJoin` methods. Both of the
* An optional alias. * An optional alias.
```php ```php
// Without an alias // Without an alias
$members = Member::get() $members = Member::get()
->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\""); ->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
$members = Member::get() $members = Member::get()
->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel"); ->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel");
``` ```
<div class="alert" markdown="1"> <div class="alert" markdown="1">
@ -674,16 +664,15 @@ Define the default values for all the `$db` fields. This example sets the "Statu
whenever a new object is created. whenever a new object is created.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject class Player extends DataObject
{ {
private static $defaults = [
"Status" => 'Active',
];
}
private static $defaults = [
"Status" => 'Active',
];
}
``` ```
<div class="notice" markdown='1'> <div class="notice" markdown='1'>
@ -693,7 +682,6 @@ Note: Alternatively you can set defaults directly in the database-schema (rather
## Subclasses ## Subclasses
Inheritance is supported in the data model: separate tables will be linked together, the data spread across these Inheritance is supported in the data model: separate tables will be linked together, the data spread across these
tables. The mapping and saving logic is handled by SilverStripe, you don't need to worry about writing SQL most of the tables. The mapping and saving logic is handled by SilverStripe, you don't need to worry about writing SQL most of the
time. time.
@ -701,47 +689,43 @@ time.
For example, suppose we have the following set of classes: For example, suppose we have the following set of classes:
```php ```php
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
use Page;
class Page extends SiteTree class Page extends SiteTree
{ {
}
class NewsPage extends Page
{
private static $db = [
'Summary' => 'Text'
];
}
}
class NewsPage extends Page
{
private static $db = [
'Summary' => 'Text'
];
}
``` ```
The data for the following classes would be stored across the following tables: The data for the following classes would be stored across the following tables:
```yml ```yml
SilverStripe\CMS\Model\SiteTree:
SiteTree: ID: Int
- ID: Int ClassName: Enum('SiteTree', 'Page', 'NewsPage')
- ClassName: Enum('SiteTree', 'Page', 'NewsPage') Created: Datetime
- Created: Datetime LastEdited: Datetime
- LastEdited: Datetime Title: Varchar
- Title: Varchar Content: Text
- Content: Text NewsPage:
NewsPage: ID: Int
- ID: Int Summary: Text
- Summary: Text
``` ```
Accessing the data is transparent to the developer. Accessing the data is transparent to the developer.
```php ```php
$news = NewsPage::get(); $news = NewsPage::get();
foreach($news as $article) { foreach($news as $article) {
echo $article->Title; echo $article->Title;
} }
``` ```
The way the ORM stores the data is this: The way the ORM stores the data is this:

View File

@ -16,27 +16,24 @@ A 1-to-1 relation creates a database-column called "`<relationship-name>`ID", in
"TeamID" on the "Player"-table. "TeamID" on the "Player"-table.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Team extends DataObject class Team extends DataObject
{ {
private static $db = [
'Title' => 'Varchar'
];
private static $db = [ private static $has_many = [
'Title' => 'Varchar' 'Players' => 'Player'
]; ];
}
private static $has_many = [ class Player extends DataObject
'Players' => 'Player' {
]; private static $has_one = [
}
class Player extends DataObject
{
private static $has_one = [
"Team" => "Team", "Team" => "Team",
]; ];
} }
``` ```
This defines a relationship called `Team` which links to a `Team` class. The `ORM` handles navigating the relationship This defines a relationship called `Team` which links to a `Team` class. The `ORM` handles navigating the relationship
@ -45,24 +42,23 @@ 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()`) 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 ```php
$player = Player::get()->byId(1); $player = Player::get()->byId(1);
$team = $player->Team(); $team = $player->Team();
// returns a 'Team' instance. // returns a 'Team' instance.
echo $player->Team()->Title; echo $player->Team()->Title;
// returns the 'Title' column on the 'Team' or `getTitle` if it exists. // returns the 'Title' column on the 'Team' or `getTitle` if it exists.
``` ```
The relationship can also be navigated in [templates](../templates). The relationship can also be navigated in [templates](../templates).
```ss
<% with $Player %> ```ss
<% if $Team %> <% with $Player %>
Plays for $Team.Title <% if $Team %>
<% end_if %> Plays for $Team.Title
<% end_with %> <% end_if %>
<% end_with %>
``` ```
## Polymorphic has_one ## Polymorphic has_one
@ -77,31 +73,30 @@ 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. Ideally, the associated has_many (or belongs_to) should be specified with dot notation.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject class Player extends DataObject
{ {
private static $has_many = [ private static $has_many = [
"Fans" => "Fan.FanOf" "Fans" => "Fan.FanOf"
]; ];
} }
class Team extends DataObject class Team extends DataObject
{ {
private static $has_many = [ private static $has_many = [
"Fans" => "Fan.FanOf" "Fans" => "Fan.FanOf"
]; ];
} }
// Type of object returned by $fan->FanOf() will vary // Type of object returned by $fan->FanOf() will vary
class Fan extends DataObject class Fan extends DataObject
{ {
// Generates columns FanOfID and FanOfClass
private static $has_one = [
"FanOf" => "DataObject"
];
}
// Generates columns FanOfID and FanOfClass
private static $has_one = [
"FanOf" => "DataObject"
];
}
``` ```
<div class="warning" markdown='1'> <div class="warning" markdown='1'>
@ -122,68 +117,63 @@ available on both ends.
</div> </div>
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Team extends DataObject class Team extends DataObject
{ {
private static $db = [
'Title' => 'Varchar'
];
private static $db = [ private static $has_many = [
'Title' => 'Varchar' 'Players' => 'Player'
]; ];
}
class Player extends DataObject
{
private static $has_many = [ private static $has_one = [
'Players' => 'Player'
];
}
class Player extends DataObject
{
private static $has_one = [
"Team" => "Team", "Team" => "Team",
]; ];
} }
``` ```
Much like the `has_one` relationship, `has_many` can be navigated through the `ORM` as well. The only difference being 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. you will get an instance of [HasManyList](api:SilverStripe\ORM\HasManyList) rather than the object.
```php ```php
$team = Team::get()->first(); $team = Team::get()->first();
echo $team->Players(); echo $team->Players();
// [HasManyList] // [HasManyList]
echo $team->Players()->Count(); echo $team->Players()->Count();
// returns '14'; // returns '14';
foreach($team->Players() as $player) { foreach($team->Players() as $player) {
echo $player->FirstName; echo $player->FirstName;
} }
``` ```
To specify multiple `$has_many` to the same object you can use dot notation to distinguish them like below: To specify multiple `$has_many` to the same object you can use dot notation to distinguish them like below:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Person extends DataObject
{
private static $has_many = [
"Managing" => "Company.Manager",
"Cleaning" => "Company.Cleaner",
];
}
class Company extends DataObject
{
private static $has_one = [
"Manager" => "Person",
"Cleaner" => "Person"
];
}
class Person extends DataObject
{
private static $has_many = [
"Managing" => "Company.Manager",
"Cleaning" => "Company.Cleaner",
];
}
class Company extends DataObject
{
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 Multiple `$has_one` relationships are okay if they aren't linking to the same object type. Otherwise, they have to be
@ -192,12 +182,12 @@ 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: 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() public function getCMSFields()
{ {
$fields = parent::getCMSFields(); $fields = parent::getCMSFields();
$fields->removeByName(array('ManagerID', 'CleanerID')); $fields->removeByName(array('ManagerID', 'CleanerID'));
return $fields; return $fields;
} }
``` ```
## belongs_to ## belongs_to
@ -211,23 +201,22 @@ Similarly with `$has_many`, dot notation can be used to explicitly specify the `
This is not mandatory unless the relationship would be otherwise ambiguous. This is not mandatory unless the relationship would be otherwise ambiguous.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Team extends DataObject
{
private static $has_one = [
'Coach' => 'Coach'
];
}
class Coach extends DataObject
{
private static $belongs_to = [
'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 ## many_many
@ -246,10 +235,10 @@ The only difference being you will get an instance of [ManyManyList](api:SilverS
[ManyManyThroughList](api:SilverStripe\ORM\ManyManyThroughList) rather than the object. [ManyManyThroughList](api:SilverStripe\ORM\ManyManyThroughList) rather than the object.
```php ```php
$team = Team::get()->byId(1); $team = Team::get()->byId(1);
$supporters = $team->Supporters(); $supporters = $team->Supporters();
// returns a 'ManyManyList' instance. // returns a 'ManyManyList' instance.
``` ```
### Automatic many_many table ### Automatic many_many table
@ -261,28 +250,28 @@ be created with a pair of ID fields.
Extra fields on the mapping table can be created by declaring a `many_many_extraFields` Extra fields on the mapping table can be created by declaring a `many_many_extraFields`
config to add extra columns. config to add extra columns.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Team extends DataObject class Team extends DataObject
{ {
private static $many_many = [ private static $many_many = [
"Supporters" => "Supporter", "Supporters" => "Supporter",
]; ];
private static $many_many_extraFields = [
private static $many_many_extraFields = [
'Supporters' => [ 'Supporters' => [
'Ranking' => 'Int' 'Ranking' => 'Int'
] ]
]; ];
} }
class Supporter extends DataObject
{
private static $belongs_many_many = [ class Supporter extends DataObject
{
private static $belongs_many_many = [
"Supports" => "Team", "Supports" => "Team",
]; ];
} }
``` ```
### many_many through relationship joined on a separate DataObject ### many_many through relationship joined on a separate DataObject
@ -308,44 +297,43 @@ or child record.
The syntax for `belongs_many_many` is unchanged. The syntax for `belongs_many_many` is unchanged.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Team extends DataObject class Team extends DataObject
{ {
private static $many_many = [ private static $many_many = [
"Supporters" => [ "Supporters" => [
'through' => 'TeamSupporter', 'through' => 'TeamSupporter',
'from' => 'Team', 'from' => 'Team',
'to' => 'Supporter', 'to' => 'Supporter',
] ]
]; ];
} }
class Supporter extends DataObject class Supporter extends DataObject
{ {
private static $belongs_many_many = [ private static $belongs_many_many = [
"Supports" => "Team", "Supports" => "Team",
]; ];
} }
class TeamSupporter extends DataObject class TeamSupporter extends DataObject
{ {
private static $db = [ private static $db = [
'Ranking' => 'Int', 'Ranking' => 'Int',
]; ];
private static $has_one = [ private static $has_one = [
'Team' => 'Team', 'Team' => 'Team',
'Supporter' => 'Supporter' 'Supporter' => 'Supporter',
]; ];
} }
``` ```
In order to filter on the join table during queries, you can use the class name of the joining table In order to filter on the join table during queries, you can use the class name of the joining table
for any sql conditions. for any sql conditions.
```php ```php
$team = Team::get()->byId(1); $team = Team::get()->byId(1);
$supporters = $team->Supporters()->where(['"TeamSupporter"."Ranking"' => 1]); $supporters = $team->Supporters()->where(['"TeamSupporter"."Ranking"' => 1]);
``` ```
Note: ->filter() currently does not support joined fields natively due to the fact that the Note: ->filter() currently does not support joined fields natively due to the fact that the
@ -356,14 +344,13 @@ query for the join table is isolated from the outer query controlled by DataList
The relationship can also be navigated in [templates](../templates). The relationship can also be navigated in [templates](../templates).
The joined record can be accessed via `Join` or `TeamSupporter` property (many_many through only) The joined record can be accessed via `Join` or `TeamSupporter` property (many_many through only)
```ss
<% with $Supporter %> ```ss
<% loop $Supports %> <% with $Supporter %>
Supports $Title <% if $TeamSupporter %>(rank $TeamSupporter.Ranking)<% end_if %> <% loop $Supports %>
<% end_if %> Supports $Title <% if $TeamSupporter %>(rank $TeamSupporter.Ranking)<% end_if %>
<% end_with %> <% end_if %>
<% end_with %>
``` ```
You can also use `$Join` in place of the join class alias (`$TeamSupporter`), if your template You can also use `$Join` in place of the join class alias (`$TeamSupporter`), if your template
@ -379,25 +366,24 @@ distinguish them like below:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Category extends DataObject class Category extends DataObject
{ {
private static $many_many = [ private static $many_many = [
'Products' => 'Product', 'Products' => 'Product',
'FeaturedProducts' => 'Product' 'FeaturedProducts' => 'Product'
]; ];
} }
class Product extends DataObject
{
private static $belongs_many_many = [
'Categories' => 'Category.Products',
'FeaturedInCategories' => 'Category.FeaturedProducts'
];
}
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`, If you're unsure about whether an object should take on `many_many` or `belongs_many_many`,
@ -412,6 +398,8 @@ Relationships between objects can cause cascading deletions, if necessary, throu
`cascade_deletes` config on the parent class. `cascade_deletes` config on the parent class.
```php ```php
use SilverStripe\ORM\DataObject;
class ParentObject extends DataObject { class ParentObject extends DataObject {
private static $has_one = [ private static $has_one = [
'Child' => ChildObject::class, 'Child' => ChildObject::class,
@ -439,15 +427,15 @@ encapsulated by [HasManyList](api:SilverStripe\ORM\HasManyList) and [ManyManyLis
and `remove()` method. and `remove()` method.
```php ```php
$team = Team::get()->byId(1); $team = Team::get()->byId(1);
// create a new supporter // create a new supporter
$supporter = new Supporter(); $supporter = new Supporter();
$supporter->Name = "Foo"; $supporter->Name = "Foo";
$supporter->write(); $supporter->write();
// add the supporter. // add the supporter.
$team->Supporters()->add($supporter); $team->Supporters()->add($supporter);
``` ```
## Custom Relations ## Custom Relations
@ -458,20 +446,19 @@ 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. See [DataObject::$has_many](api:SilverStripe\ORM\DataObject::$has_many) for more info on the described relations.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Team extends DataObject class Team extends DataObject
{ {
private static $has_many = [
private static $has_many = [
"Players" => "Player" "Players" => "Player"
]; ];
public function ActivePlayers() public function ActivePlayers()
{ {
return $this->Players()->filter('Status', 'Active'); return $this->Players()->filter('Status', 'Active');
}
} }
}
``` ```

View File

@ -12,32 +12,33 @@ modify.
[SS_List](api:SilverStripe\ORM\SS_List) implements `IteratorAggregate`, allowing you to loop over the instance. [SS_List](api:SilverStripe\ORM\SS_List) implements `IteratorAggregate`, allowing you to loop over the instance.
```php ```php
$members = Member::get(); use SilverStripe\Security\Member;
foreach($members as $member) { $members = Member::get();
echo $member->Name;
} foreach($members as $member) {
echo $member->Name;
}
``` ```
Or in the template engine: Or in the template engine:
```ss ```ss
<% loop $Members %>
<% loop $Members %> <!-- -->
<!-- --> <% end_loop %>
<% end_loop %>
``` ```
## Finding an item by value. ## Finding an item by value.
```php ```php
// $list->find($key, $value); // $list->find($key, $value);
// //
$members = Member::get(); $members = Member::get();
echo $members->find('ID', 4)->FirstName; echo $members->find('ID', 4)->FirstName;
// returns 'Sam' // returns 'Sam'
``` ```
## Maps ## Maps
@ -45,36 +46,34 @@ Or in the template engine:
A map is an array where the array indexes contain data as well as the values. You can build a map from any list 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 ```php
$members = Member::get()->map('ID', 'FirstName'); $members = Member::get()->map('ID', 'FirstName');
// $members = array(
// 1 => 'Sam'
// 2 => 'Sig'
// 3 => 'Will'
// );
// $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`. 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 ```php
$members = Member::get(); $members = Member::get();
$map = new Map($members, 'ID', 'FirstName'); $map = new Map($members, 'ID', 'FirstName');
``` ```
## Column ## Column
```php ```php
$members = Member::get(); $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
@ -82,16 +81,15 @@ This functionality is provided by the [Map](api:SilverStripe\ORM\Map) class, whi
[ArrayList](api:SilverStripe\ORM\ArrayList) exists to wrap a standard PHP array in the same API as a database backed list. [ArrayList](api:SilverStripe\ORM\ArrayList) exists to wrap a standard PHP array in the same API as a database backed list.
```php ```php
$sam = Member::get()->byId(5); $sam = Member::get()->byId(5);
$sig = Member::get()->byId(6); $sig = Member::get()->byId(6);
$list = new ArrayList(); $list = new ArrayList();
$list->push($sam); $list->push($sam);
$list->push($sig); $list->push($sig);
echo $list->Count();
// returns '2'
echo $list->Count();
// returns '2'
``` ```
## API Documentation ## API Documentation

View File

@ -14,19 +14,17 @@ In the `Player` example, we have four database columns each with a different dat
**mysite/code/Player.php** **mysite/code/Player.php**
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject
{
private static $db = [
'PlayerNumber' => 'Int',
'FirstName' => 'Varchar(255)',
'LastName' => 'Text',
'Birthday' => 'Date'
];
}
class Player extends DataObject
{
private static $db = [
'PlayerNumber' => 'Int',
'FirstName' => 'Varchar(255)',
'LastName' => 'Text',
'Birthday' => 'Date'
];
}
``` ```
## Available Types ## Available Types
@ -55,22 +53,20 @@ For complex default values for newly instantiated objects see [Dynamic Default V
For simple values you can make use of the `$defaults` array. For example: For simple values you can make use of the `$defaults` array. For example:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Car extends DataObject
{
private static $db = [
'Wheels' => 'Int',
'Condition' => 'Enum(array("New","Fair","Junk"))'
];
private static $defaults = [
'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 ### Default values for new database columns
@ -88,18 +84,16 @@ For enum values, it's the second parameter.
For example: For example:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Car extends DataObject
{
private static $db = [
'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 ## Formatting Output
@ -111,36 +105,33 @@ If this case, we'll create a new method for our `Player` that returns the full n
object we can control the formatting and it allows us to call methods defined from `Varchar` as `LimitCharacters`. object we can control the formatting and it allows us to call methods defined from `Varchar` as `LimitCharacters`.
**mysite/code/Player.php** **mysite/code/Player.php**
```php ```php
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\DataObject; 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. 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 ```php
$player = Player::get()->byId(1); $player = Player::get()->byId(1);
echo $player->Name; echo $player->Name;
// returns "Sam Minnée" // returns "Sam Minnée"
echo $player->getName(); echo $player->getName();
// returns "Sam Minnée"; // returns "Sam Minnée";
echo $player->getName()->LimitCharacters(2); echo $player->getName()->LimitCharacters(2);
// returns "Sa.." // returns "Sa.."
``` ```
## Casting ## Casting
@ -148,21 +139,19 @@ Then we can refer to a new `Name` column on our `Player` instances. In templates
Rather than manually returning objects from your custom functions. You can use the `$casting` property. Rather than manually returning objects from your custom functions. You can use the `$casting` property.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject class Player extends DataObject
{ {
private static $casting = [
private static $casting = [
"Name" => 'Varchar', "Name" => 'Varchar',
]; ];
public function getName() public function getName()
{ {
return $this->FirstName . ' '. $this->LastName; return $this->FirstName . ' '. $this->LastName;
}
} }
}
``` ```
The properties on any SilverStripe object can be type casted automatically, by transforming its scalar value into an The properties on any SilverStripe object can be type casted automatically, by transforming its scalar value into an
@ -173,14 +162,16 @@ On the most basic level, the class can be used as simple conversion class from o
number. number.
```php ```php
DBField::create_field('Double', 1.23456)->Round(2); // results in 1.23 use SilverStripe\ORM\FieldType\DBField;
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 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: sophisticated helpers, like showing the time difference to the current date:
```php ```php
DBField::create_field('Date', '1982-01-01')->TimeDiff(); // shows "30 years ago" use SilverStripe\ORM\FieldType\DBField;
DBField::create_field('Date', '1982-01-01')->TimeDiff(); // shows "30 years ago"
``` ```
## Casting ViewableData ## Casting ViewableData
@ -189,27 +180,26 @@ Most objects in SilverStripe extend from [ViewableData](api:SilverStripe\View\Vi
context. Through a `$casting` array, arbitrary properties and getters can be casted: context. Through a `$casting` array, arbitrary properties and getters can be casted:
```php ```php
use SilverStripe\View\ViewableData; use SilverStripe\View\ViewableData;
class MyObject extends ViewableData class MyObject extends ViewableData
{
private static $casting = [
'MyDate' => 'Date'
];
public function getMyDate()
{ {
return '1982-01-01';
private static $casting = [
'MyDate' => 'Date'
];
public function getMyDate()
{
return '1982-01-01';
}
} }
}
$obj = new MyObject; $obj = new MyObject;
$obj->getMyDate(); // returns string $obj->getMyDate(); // returns string
$obj->MyDate; // returns string $obj->MyDate; // returns string
$obj->obj('MyDate'); // returns object $obj->obj('MyDate'); // returns object
$obj->obj('MyDate')->InPast(); // returns boolean $obj->obj('MyDate')->InPast(); // returns boolean
``` ```
## Casting HTML Text ## Casting HTML Text
@ -231,20 +221,19 @@ The following example will use the result of `getStatus` instead of the 'Status'
database column using `dbObject`. database column using `dbObject`.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject class Player extends DataObject
{ {
private static $db = [
private static $db = [
"Status" => "Enum(array('Active', 'Injured', 'Retired'))" "Status" => "Enum(array('Active', 'Injured', 'Retired'))"
]; ];
public function getStatus() public function getStatus()
{ {
return (!$this->obj("Birthday")->InPast()) ? "Unborn" : $this->dbObject('Status')->Value(); return (!$this->obj("Birthday")->InPast()) ? "Unborn" : $this->dbObject('Status')->Value();
} }
}
``` ```
## API Documentation ## API Documentation

View File

@ -19,39 +19,38 @@ a `ModelAdmin` record.
Example: Disallow creation of new players if the currently logged-in player is not a team-manager. Example: Disallow creation of new players if the currently logged-in player is not a team-manager.
```php ```php
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject class Player extends DataObject
{ {
private static $has_many = [
private static $has_many = [ "Teams" => "Team",
"Teams"=>"Team" ];
];
public function onBeforeWrite() public function onBeforeWrite()
{ {
// check on first write action, aka "database row creation" (ID-property is not set) // check on first write action, aka "database row creation" (ID-property is not set)
if(!$this->isInDb()) { if(!$this->isInDb()) {
$currentPlayer = Security::getCurrentUser(); $currentPlayer = Security::getCurrentUser();
if(!$currentPlayer->IsTeamManager()) { if(!$currentPlayer->IsTeamManager()) {
user_error('Player-creation not allowed', E_USER_ERROR); user_error('Player-creation not allowed', E_USER_ERROR);
exit(); exit();
} }
} }
// check on every write action // check on every write action
if(!$this->record['TeamID']) { if(!$this->record['TeamID']) {
user_error('Cannot save player without a valid team', E_USER_ERROR); user_error('Cannot save player without a valid team', E_USER_ERROR);
exit(); exit();
} }
// CAUTION: You are required to call the parent-function, otherwise // CAUTION: You are required to call the parent-function, otherwise
// SilverStripe will not execute the request. // SilverStripe will not execute the request.
parent::onBeforeWrite(); parent::onBeforeWrite();
}
} }
}
``` ```
@ -63,28 +62,27 @@ Example: Checking for a specific [permission](permissions) to delete this type o
member is logged in who belongs to a group containing the permission "PLAYER_DELETE". member is logged in who belongs to a group containing the permission "PLAYER_DELETE".
```php ```php
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject class Player extends DataObject
{ {
private static $has_many = [ private static $has_many = [
"Teams" => "Team" "Teams" => "Team"
]; ];
public function onBeforeDelete() public function onBeforeDelete()
{ {
if(!Permission::check('PLAYER_DELETE')) { if(!Permission::check('PLAYER_DELETE')) {
Security::permissionFailure($this); Security::permissionFailure($this);
exit(); exit();
} }
parent::onBeforeDelete(); parent::onBeforeDelete();
}
} }
}
``` ```
<div class="notice" markdown='1'> <div class="notice" markdown='1'>

View File

@ -15,20 +15,19 @@ you can put on field names to change this behavior. These are represented as `Se
* [LessThanOrEqualFilter](api:SilverStripe\ORM\Filters\LessThanOrEqualFilter) * [LessThanOrEqualFilter](api:SilverStripe\ORM\Filters\LessThanOrEqualFilter)
An example of a `SearchFilter` in use: An example of a `SearchFilter` in use:
```php ```php
// fetch any player that starts with a S // fetch any player that starts with a S
$players = Player::get()->filter([ $players = Player::get()->filter([
'FirstName:StartsWith' => 'S', 'FirstName:StartsWith' => 'S',
'PlayerNumber:GreaterThan' => '10' 'PlayerNumber:GreaterThan' => '10'
]); ]);
// to fetch any player that's name contains the letter 'z'
$players = Player::get()->filterAny([
'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. Developers can define their own [SearchFilter](api:SilverStripe\ORM\Filters\SearchFilter) if needing to extend the ORM filter and exclude behaviors.
@ -44,24 +43,22 @@ config:
```yaml ```yaml
SilverStripe\Core\Injector\Injector:
SilverStripe\Core\Injector\Injector: DataListFilter.CustomMatch:
DataListFilter.CustomMatch: class: MyVendor\Search\CustomMatchFilter
class: MyVendor/Search/CustomMatchFilter
``` ```
The following is a query which will return everyone whose first name starts with "S", either lowercase or uppercase: The following is a query which will return everyone whose first name starts with "S", either lowercase or uppercase:
```php ```php
$players = Player::get()->filter([ $players = Player::get()->filter([
'FirstName:StartsWith:nocase' => 'S' 'FirstName:StartsWith:nocase' => 'S'
]); ]);
// use :not to perform a converse operation to filter anything but a 'W'
$players = Player::get()->filter([
'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 ## API Documentation

View File

@ -17,32 +17,31 @@ code.
</div> </div>
```php ```php
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyDataObject extends DataObject class MyDataObject extends DataObject
{
public function canView($member = null)
{ {
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
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 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 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 canCreate($member = null)
{
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
}
}
``` ```
<div class="alert" markdown="1"> <div class="alert" markdown="1">

View File

@ -19,15 +19,19 @@ For example, if you want to run a simple `COUNT` SQL statement,
the following three statements are functionally equivalent: the following three statements are functionally equivalent:
```php ```php
// Through raw SQL. use SilverStripe\ORM\DB;
$count = DB::query('SELECT COUNT(*) FROM "Member"')->value(); use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\Security\Member;
// Through SQLSelect abstraction layer. // Through raw SQL.
$query = new SQLSelect(); $count = DB::query('SELECT COUNT(*) FROM "Member"')->value();
$count = $query->setFrom('Member')->setSelect('COUNT(*)')->value();
// Through the ORM. // Through SQLSelect abstraction layer.
$count = Member::get()->count(); $query = new SQLSelect();
$count = $query->setFrom('Member')->setSelect('COUNT(*)')->value();
// Through the ORM.
$count = Member::get()->count();
``` ```
If you do use raw SQL, you'll run the risk of breaking If you do use raw SQL, you'll run the risk of breaking
@ -58,30 +62,28 @@ conditional filters, grouping, limiting, and sorting.
E.g. E.g.
```php ```php
$sqlQuery = new SQLSelect();
$sqlQuery = new SQLSelect(); $sqlQuery->setFrom('Player');
$sqlQuery->setFrom('Player'); $sqlQuery->selectField('FieldName', 'Name');
$sqlQuery->selectField('FieldName', 'Name'); $sqlQuery->selectField('YEAR("Birthday")', 'Birthyear');
$sqlQuery->selectField('YEAR("Birthday")', 'Birthyear'); $sqlQuery->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"');
$sqlQuery->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"'); $sqlQuery->addWhere(['YEAR("Birthday") = ?' => 1982]);
$sqlQuery->addWhere(['YEAR("Birthday") = ?' => 1982]); // $sqlQuery->setOrderBy(...);
// $sqlQuery->setOrderBy(...); // $sqlQuery->setGroupBy(...);
// $sqlQuery->setGroupBy(...); // $sqlQuery->setHaving(...);
// $sqlQuery->setHaving(...); // $sqlQuery->setLimit(...);
// $sqlQuery->setLimit(...); // $sqlQuery->setDistinct(true);
// $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 // Get the raw SQL (optional) and parameters
foreach($result as $row) { $rawSQL = $sqlQuery->sql($parameters);
echo $row['BirthYear'];
}
// Execute and return a Query object
$result = $sqlQuery->execute();
// 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). The result of `SQLSelect::execute()` is an array lightly wrapped in a database-specific subclass of [Query](api:SilverStripe\ORM\Connect\Query).
@ -96,32 +98,31 @@ object instead.
For example, creating a `SQLDelete` object For example, creating a `SQLDelete` object
```php ```php
use SilverStripe\ORM\Queries\SQLDelete;
$query = SQLDelete::create()
->setFrom('"SiteTree"')
->setWhere(['"SiteTree"."ShowInMenus"' => 0]);
$query->execute();
$query = SQLDelete::create()
->setFrom('"SiteTree"')
->setWhere(['"SiteTree"."ShowInMenus"' => 0]);
$query->execute();
``` ```
Alternatively, turning an existing `SQLSelect` into a delete Alternatively, turning an existing `SQLSelect` into a delete
```php ```php
use SilverStripe\ORM\Queries\SQLSelect;
$query = SQLSelect::create()
->setFrom('"SiteTree"')
->setWhere(['"SiteTree"."ShowInMenus"' => 0])
->toDelete();
$query->execute();
$query = SQLSelect::create()
->setFrom('"SiteTree"')
->setWhere(['"SiteTree"."ShowInMenus"' => 0])
->toDelete();
$query->execute();
``` ```
Directly querying the database Directly querying the database
```php ```php
use SilverStripe\ORM\DB;
DB::prepared_query('DELETE FROM "SiteTree" WHERE "SiteTree"."ShowInMenus" = ?', [0]); DB::prepared_query('DELETE FROM "SiteTree" WHERE "SiteTree"."ShowInMenus" = ?', [0]);
``` ```
### INSERT/UPDATE ### INSERT/UPDATE
@ -169,30 +170,31 @@ SQLInsert also includes the following api methods:
E.g. E.g.
```php ```php
$update = SQLUpdate::create('"SiteTree"')->addWhere(['ID' => 3]); use SilverStripe\ORM\Queries\SQLUpdate;
// assigning a list of items $update = SQLUpdate::create('"SiteTree"')->addWhere(['ID' => 3]);
$update->addAssignments([
'"Title"' => 'Our Products',
'"MenuTitle"' => 'Products'
]);
// Assigning a single value // assigning a list of items
$update->assign('"MenuTitle"', 'Products'); $update->addAssignments([
'"Title"' => 'Our Products',
'"MenuTitle"' => 'Products'
]);
// Assigning a value using parameterised expression // Assigning a single value
$title = 'Products'; $update->assign('"MenuTitle"', 'Products');
$update->assign('"MenuTitle"', [
'CASE WHEN LENGTH("MenuTitle") > LENGTH(?) THEN "MenuTitle" ELSE ? END' =>
[$title, $title]
]);
// Assigning a value using a pure SQL expression // Assigning a value using parameterised expression
$update->assignSQL('"Date"', 'NOW()'); $title = 'Products';
$update->assign('"MenuTitle"', [
'CASE WHEN LENGTH("MenuTitle") > LENGTH(?) THEN "MenuTitle" ELSE ? END' =>
[$title, $title]
]);
// Perform the update // Assigning a value using a pure SQL expression
$update->execute(); $update->assignSQL('"Date"', 'NOW()');
// Perform the update
$update->execute();
``` ```
In addition to assigning values, the SQLInsert object also supports multi-row In addition to assigning values, the SQLInsert object also supports multi-row
@ -202,26 +204,27 @@ these are translated internally as multiple single row inserts.
For example, For example,
```php ```php
$insert = SQLInsert::create('"SiteTree"'); use SilverStripe\ORM\Queries\SQLInsert;
// Add multiple rows in a single call. Note that column names do not need $insert = SQLInsert::create('"SiteTree"');
// 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 // Add multiple rows in a single call. Note that column names do not need
$insert->assign('"Content"', '<p>This is about us</p>'); // to be symmetric
$insert->addRows([
['"Title"' => 'Home', '"Content"' => '<p>This is our home page</p>'],
['"Title"' => 'About Us', '"ClassName"' => 'AboutPage']
]);
// Add another row // Adjust an assignment on the last row
$insert->addRow(['"Title"' => 'Contact Us']); $insert->assign('"Content"', '<p>This is about us</p>');
$columns = $insert->getColumns(); // Add another row
// $columns will be array('"Title"', '"Content"', '"ClassName"'); $insert->addRow(['"Title"' => 'Contact Us']);
$insert->execute(); $columns = $insert->getColumns();
// $columns will be array('"Title"', '"Content"', '"ClassName"');
$insert->execute();
``` ```
### Value Checks ### Value Checks
@ -232,19 +235,20 @@ e.g. when you want a single column rather than a full-blown object representatio
Example: Get the count from a relationship. Example: Get the count from a relationship.
```php ```php
$sqlQuery = new SQLSelect(); use SilverStripe\ORM\Queries\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();
$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: Note that in the ORM, this call would be executed in an efficient manner as well:
```php ```php
$count = $myTeam->Players()->count(); $count = $myTeam->Players()->count();
``` ```
### Mapping ### Mapping
@ -255,12 +259,15 @@ This can be useful for creating dropdowns.
Example: Show player names with their birth year, but set their birth dates as values. Example: Show player names with their birth year, but set their birth dates as values.
```php ```php
$sqlQuery = new SQLSelect(); use SilverStripe\ORM\Queries\SQLSelect;
$sqlQuery->setFrom('Player'); use SilverStripe\Forms\DropdownField;
$sqlQuery->setSelect('Birthdate');
$sqlQuery->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear'); $sqlQuery = new SQLSelect();
$map = $sqlQuery->execute()->map(); $sqlQuery->setFrom('Player');
$field = new DropdownField('Birthdates', 'Birthdates', $map); $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 Note that going through SQLSelect is just necessary here
@ -268,21 +275,20 @@ because of the custom SQL value transformation (`YEAR()`).
An alternative approach would be a custom getter in the object definition. An alternative approach would be a custom getter in the object definition.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject class Player extends DataObject
{ {
private static $db = [ private static $db = [
'Name' => 'Varchar', 'Name' => 'Varchar',
'Birthdate' => 'Date' 'Birthdate' => 'Date'
]; ];
function getNameWithBirthyear() { function getNameWithBirthyear() {
return date('y', $this->Birthdate); return date('y', $this->Birthdate);
}
} }
$players = Player::get(); }
$map = $players->map('Name', 'NameWithBirthyear'); $players = Player::get();
$map = $players->map('Name', 'NameWithBirthyear');
``` ```
## Related ## Related

View File

@ -22,28 +22,27 @@ write, and respond appropriately if it isn't.
The return value of `validate()` is a [ValidationResult](api:SilverStripe\ORM\ValidationResult) object. The return value of `validate()` is a [ValidationResult](api:SilverStripe\ORM\ValidationResult) object.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyObject extends DataObject class MyObject extends DataObject
{
private static $db = [
'Country' => 'Varchar',
'Postcode' => 'Varchar'
];
public function validate()
{ {
$result = parent::validate();
private static $db = [ if($this->Country == 'DE' && $this->Postcode && strlen($this->Postcode) != 5) {
'Country' => 'Varchar', $result->error('Need five digits for German postcodes');
'Postcode' => 'Varchar'
];
public function validate()
{
$result = parent::validate();
if($this->Country == 'DE' && $this->Postcode && strlen($this->Postcode) != 5) {
$result->error('Need five digits for German postcodes');
}
return $result;
} }
}
return $result;
}
}
``` ```
## API Documentation ## API Documentation

View File

@ -174,6 +174,8 @@ By default, all records are retrieved from the "Draft" stage (so the `MyRecord`
explicitly request a certain stage through various getters on the `Versioned` class. explicitly request a certain stage through various getters on the `Versioned` class.
```php ```php
use SilverStripe\Versioned\Versioned;
// Fetching multiple records // Fetching multiple records
$stageRecords = Versioned::get_by_stage('MyRecord', Versioned::DRAFT); $stageRecords = Versioned::get_by_stage('MyRecord', Versioned::DRAFT);
$liveRecords = Versioned::get_by_stage('MyRecord', Versioned::LIVE); $liveRecords = Versioned::get_by_stage('MyRecord', Versioned::LIVE);
@ -200,7 +202,7 @@ rather than modifying the existing one.
In order to get a list of all versions for a specific record, we need to generate specialized [Versioned_Version](api:SilverStripe\Versioned\Versioned_Version) In order to get a list of all versions for a specific record, we need to generate specialized [Versioned_Version](api:SilverStripe\Versioned\Versioned_Version)
objects, which expose the same database information as a `DataObject`, but also include information about when and how objects, which expose the same database information as a `DataObject`, but also include information about when and how
a record was published. a record was published.
```php ```php
$record = MyRecord::get()->byID(99); // stage doesn't matter here $record = MyRecord::get()->byID(99); // stage doesn't matter here
$versions = $record->allVersions(); $versions = $record->allVersions();

View File

@ -13,46 +13,44 @@ customise those fields as required.
An example is `DataObject`, SilverStripe will automatically create your CMS interface so you can modify what you need. An example is `DataObject`, SilverStripe will automatically create your CMS interface so you can modify what you need.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyDataObject extends DataObject 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?');
private static $db = [ return $fields;
'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;
}
} }
}
``` ```
To fully customise your form fields, start with an empty FieldList. 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)
)
);
public function getCMSFields() return $fields;
{ }
$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 You can also alter the fields of built-in and module `DataObject` classes through your own
@ -64,17 +62,16 @@ The `$searchable_fields` property uses a mixed array format that can be used to
system. The default is a set of array values listing the fields. system. The default is a set of array values listing the fields.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyDataObject extends DataObject class MyDataObject extends DataObject
{ {
private static $searchable_fields = [
'Name',
'ProductCode'
];
}
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 Searchable fields will be appear in the search interface with a default form field (usually a [TextField](api:SilverStripe\Forms\TextField)) and a
@ -82,76 +79,73 @@ default search filter assigned (usually an [ExactMatchFilter](api:SilverStripe\O
additional information on `$searchable_fields`: additional information on `$searchable_fields`:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyDataObject extends DataObject class MyDataObject extends DataObject
{ {
private static $searchable_fields = [
'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 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: both, you can assign an array:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyDataObject extends DataObject class MyDataObject extends DataObject
{ {
private static $searchable_fields = [
'Name' => [
'field' => 'TextField',
'filter' => 'PartialMatchFilter',
],
'ProductCode' => [
'title' => 'Product code #',
'field' => 'NumericField',
'filter' => 'PartialMatchFilter',
],
];
}
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. To include relations (`$has_one`, `$has_many` and `$many_many`) in your search, you can use a dot-notation.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Team extends DataObject class Team extends DataObject
{ {
private static $db = [
private static $db = [
'Title' => 'Varchar' 'Title' => 'Varchar'
]; ];
private static $many_many = [ private static $many_many = [
'Players' => 'Player' 'Players' => 'Player'
]; ];
private static $searchable_fields = [ private static $searchable_fields = [
'Title', 'Title',
'Players.Name', 'Players.Name',
]; ];
} }
class Player extends DataObject
{ class Player extends DataObject
{
private static $db = [ private static $db = [
'Name' => 'Varchar', 'Name' => 'Varchar',
'Birthday' => 'Date' 'Birthday' => 'Date',
]; ];
private static $belongs_many_many = [ private static $belongs_many_many = [
'Teams' => 'Team' 'Teams' => 'Team'
]; ];
} }
``` ```
@ -161,79 +155,75 @@ Summary fields can be used to show a quick overview of the data for a specific [
is their display as table columns, e.g. in the search results of a [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) CMS interface. is their display as table columns, e.g. in the search results of a [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) CMS interface.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyDataObject extends DataObject class MyDataObject extends DataObject
{ {
private static $db = [
private static $db = [
'Name' => 'Text', 'Name' => 'Text',
'OtherProperty' => 'Text', 'OtherProperty' => 'Text',
'ProductCode' => 'Int', 'ProductCode' => 'Int',
]; ];
private static $summary_fields = [ private static $summary_fields = [
'Name', 'Name',
'ProductCode' 'ProductCode',
]; ];
} }
``` ```
To include relations or field manipulations in your summaries, you can use a dot-notation. To include relations or field manipulations in your summaries, you can use a dot-notation.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class OtherObject extends DataObject class OtherObject extends DataObject
{ {
private static $db = [
private static $db = [ 'Title' => 'Varchar',
'Title' => 'Varchar' ];
]; }
}
class MyDataObject extends DataObject class MyDataObject extends DataObject
{ {
private static $db = [
private static $db = [
'Name' => 'Text', 'Name' => 'Text',
'Description' => 'HTMLText' 'Description' => 'HTMLText',
]; ];
private static $has_one = [ private static $has_one = [
'OtherObject' => 'OtherObject' 'OtherObject' => 'OtherObject',
]; ];
private static $summary_fields = [ private static $summary_fields = [
'Name' => 'Name', 'Name' => 'Name',
'Description.Summary' => 'Description (summary)', 'Description.Summary' => 'Description (summary)',
'OtherObject.Title' => 'Other Object Title' 'OtherObject.Title' => 'Other Object Title',
]; ];
} }
``` ```
Non-textual elements (such as images and their manipulations) can also be used in summaries. Non-textual elements (such as images and their manipulations) can also be used in summaries.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyDataObject extends DataObject class MyDataObject extends DataObject
{ {
private static $db = [
'Name' => 'Text',
];
private static $db = [ private static $has_one = [
'Name' => 'Text' 'HeroImage' => 'Image',
]; ];
private static $has_one = [ private static $summary_fields = [
'HeroImage' => 'Image'
];
private static $summary_fields = [
'Name' => 'Name', 'Name' => 'Name',
'HeroImage.CMSThumbnail' => 'Hero Image' 'HeroImage.CMSThumbnail' => 'Hero Image',
]; ];
} }
``` ```

View File

@ -25,20 +25,19 @@ Indexes are represented on a `DataObject` through the `DataObject::$indexes` arr
descriptor. There are several supported notations: descriptor. There are several supported notations:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyObject extends DataObject class MyObject extends DataObject
{ {
private static $indexes = [
private static $indexes = [ '<column-name>' => true,
'<column-name>' => true, '<index-name>' => [
'<index-name>' => [ 'type' => '<type>',
'type' => '<type>', 'columns' => ['<column-name>', '<other-column-name>'],
'columns' => ['<column-name>', '<other-column-name>'], ],
], '<index-name>' => ['<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 The `<column-name>` is used to put a standard non-unique index on the column specified. For complex or large tables
@ -55,20 +54,19 @@ support the following:
**mysite/code/MyTestObject.php** **mysite/code/MyTestObject.php**
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyTestObject extends DataObject class MyTestObject extends DataObject
{ {
private static $db = [
'MyField' => 'Varchar',
'MyOtherField' => 'Varchar',
];
private static $db = [ private static $indexes = [
'MyField' => 'Varchar', 'MyIndexName' => ['MyField', 'MyOtherField'],
'MyOtherField' => 'Varchar', ];
]; }
private static $indexes = [
'MyIndexName' => ['MyField', 'MyOtherField'],
];
}
``` ```
<div class="alert" markdown="1"> <div class="alert" markdown="1">

View File

@ -10,30 +10,31 @@ object!
A simple example is to set a field to the current date and time: A simple example is to set a field to the current date and time:
```php ```php
/** /**
* Sets the Date field to the current date. * Sets the Date field to the current date.
*/ */
public function populateDefaults() public function populateDefaults()
{ {
$this->Date = date('Y-m-d'); $this->Date = date('Y-m-d');
parent::populateDefaults(); parent::populateDefaults();
} }
``` ```
It's also possible to get the data from any other source, or another object, just by using the usual data retrieval 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: methods. For example:
```php ```php
/** /**
* This method combines the Title of the parent object with the Title of this * This method combines the Title of the parent object with the Title of this
* object in the FullTitle field. * object in the FullTitle field.
*/ */
public function populateDefaults() public function populateDefaults()
{ {
if($parent = $this->Parent()) { if($parent = $this->Parent()) {
$this->FullTitle = $parent->Title . ': ' . $this->Title; $this->FullTitle = $parent->Title . ': ' . $this->Title;
} else { } else {
$this->FullTitle = $this->Title; $this->FullTitle = $this->Title;
}
parent::populateDefaults();
} }
``` parent::populateDefaults();
}
```

View File

@ -35,48 +35,43 @@ 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. will be used both for grouping and for the title in the template.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Module extends 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()
{ {
private static $db = [ return $this->Title[0];
'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, 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. sorted by title. For this example this will be a method on the `Page` class.
```php ```php
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\ORM\GroupedList; use SilverStripe\ORM\GroupedList;
class Page extends SiteTree class Page extends SiteTree
{
/**
* Returns all modules, sorted by their title.
* @return GroupedList
*/
public function getGroupedModules()
{ {
return GroupedList::create(Module::get()->sort('Title'));
// ...
/**
* 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 The final step is to render this into a template. The `GroupedBy()` method breaks up the set into
@ -84,16 +79,16 @@ 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. In this case, the `getTitleFirstLetter()` method defined earlier is used to break them up.
```ss ```ss
<%-- Modules list grouped by TitleFirstLetter --%> <%-- Modules list grouped by TitleFirstLetter --%>
<h2>Modules</h2> <h2>Modules</h2>
<% loop $GroupedModules.GroupedBy(TitleFirstLetter) %> <% loop $GroupedModules.GroupedBy(TitleFirstLetter) %>
<h3>$TitleFirstLetter</h3> <h3>$TitleFirstLetter</h3>
<ul> <ul>
<% loop $Children %> <% loop $Children %>
<li>$Title</li> <li>$Title</li>
<% end_loop %> <% end_loop %>
</ul> </ul>
<% end_loop %> <% end_loop %>
``` ```
## Grouping Sets By Month ## Grouping Sets By Month
@ -109,63 +104,55 @@ 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: This will have a method which returns the month it was posted in:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Module extends 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));
// ...
/**
* 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, 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: sorted by month name from January to December. This can be accomplshed by sorting by the `Created` field:
```php ```php
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\ORM\GroupedList; use SilverStripe\ORM\GroupedList;
class Page extends SiteTree 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'));
// ...
/**
* 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. The final step is the render this into the template using the [GroupedList::GroupedBy()](api:SilverStripe\ORM\GroupedList::GroupedBy()) method.
```ss ```ss
// Modules list grouped by the Month Posted // Modules list grouped by the Month Posted
<h2>Modules</h2> <h2>Modules</h2>
<% loop $GroupedModulesByDate.GroupedBy(MonthCreated) %> <% loop $GroupedModulesByDate.GroupedBy(MonthCreated) %>
<h3>$MonthCreated</h3> <h3>$MonthCreated</h3>
<ul> <ul>
<% loop $Children %> <% loop $Children %>
<li>$Title ($Created.Nice)</li> <li>$Title ($Created.Nice)</li>
<% end_loop %> <% end_loop %>
</ul> </ul>
<% end_loop %> <% end_loop %>
``` ```
## Related ## Related
* [Howto: "Pagination"](/developer_guides/templates/how_tos/pagination) * [Howto: "Pagination"](/developer_guides/templates/how_tos/pagination)

View File

@ -12,33 +12,32 @@ An example of a SilverStripe template is below:
**mysite/templates/Page.ss** **mysite/templates/Page.ss**
```ss ```ss
<html>
<head>
<% base_tag %>
<title>$Title</title>
<% require themedCSS("screen") %>
</head>
<body>
<header>
<h1>Bob's Chicken Shack</h1>
</header>
<html> <% with $CurrentMember %>
<head> <p>Welcome $FirstName $Surname.</p>
<% base_tag %> <% end_with %>
<title>$Title</title>
<% require themedCSS("screen") %>
</head>
<body>
<header>
<h1>Bob's Chicken Shack</h1>
</header>
<% with $CurrentMember %> <% if $Dishes %>
<p>Welcome $FirstName $Surname.</p> <ul>
<% end_with %> <% loop $Dishes %>
<li>$Title ($Price.Nice)</li>
<% end_loop %>
</ul>
<% end_if %>
<% if $Dishes %> <% include Footer %>
<ul> </body>
<% loop $Dishes %> </html>
<li>$Title ($Price.Nice)</li>
<% end_loop %>
</ul>
<% end_if %>
<% include Footer %>
</body>
</html>
``` ```
<div class="note"> <div class="note">
@ -69,8 +68,7 @@ Variables are placeholders that will be replaced with data from the [DataModel](
alphabetic character or underscore, with subsequent characters being alphanumeric or underscore: alphabetic character or underscore, with subsequent characters being alphanumeric or underscore:
```ss ```ss
$Title
$Title
``` ```
This inserts the value of the Title database field of the page being displayed in place of `$Title`. This inserts the value of the Title database field of the page being displayed in place of `$Title`.
@ -78,10 +76,9 @@ This inserts the value of the Title database field of the page being displayed i
Variables can be chained together, and include arguments. Variables can be chained together, and include arguments.
```ss ```ss
$Foo
$Foo $Foo(param)
$Foo(param) $Foo.Bar
$Foo.Bar
``` ```
These variables will call a method / field on the object and insert the returned value as a string into the template. These variables will call a method / field on the object and insert the returned value as a string into the template.
@ -104,17 +101,16 @@ Variables can come from your database fields, or custom methods you define on yo
**mysite/code/Page.php** **mysite/code/Page.php**
```php ```php
public function UsersIpAddress() public function UsersIpAddress()
{ {
return $this->getRequest()->getIP(); return $this->getRequest()->getIP();
} }
``` ```
**mysite/code/Page.ss** **mysite/code/Page.ss**
```html ```html
<p>You are coming from $UsersIpAddress.</p>
<p>You are coming from $UsersIpAddress.</p>
``` ```
<div class="node" markdown="1"> <div class="node" markdown="1">
@ -129,12 +125,11 @@ record and any subclasses of those two.
**mysite/code/Layout/Page.ss** **mysite/code/Layout/Page.ss**
```ss ```ss
$Title
// returns the page `Title` property
$Title $Content
// returns the page `Title` property // returns the page `Content` property
$Content
// returns the page `Content` property
``` ```
## Conditional Logic ## Conditional Logic
@ -142,19 +137,17 @@ record and any subclasses of those two.
The simplest conditional block is to check for the presence of a value (does not equal 0, null, false). The simplest conditional block is to check for the presence of a value (does not equal 0, null, false).
```ss ```ss
<% if $CurrentMember %>
<% if $CurrentMember %> <p>You are logged in as $CurrentMember.FirstName $CurrentMember.Surname.</p>
<p>You are logged in as $CurrentMember.FirstName $CurrentMember.Surname.</p> <% end_if %>
<% end_if %>
``` ```
A conditional can also check for a value other than falsy. A conditional can also check for a value other than falsy.
```ss ```ss
<% if $MyDinner == "kipper" %>
<% if $MyDinner == "kipper" %> Yummy, kipper for tea.
Yummy, kipper for tea. <% end_if %>
<% end_if %>
``` ```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
@ -164,25 +157,23 @@ When inside template tags variables should have a '$' prefix, and literals shoul
Conditionals can also provide the `else` case. Conditionals can also provide the `else` case.
```ss ```ss
<% if $MyDinner == "kipper" %>
<% if $MyDinner == "kipper" %> Yummy, kipper for tea
Yummy, kipper for tea <% else %>
<% else %> I wish I could have kipper :-(
I wish I could have kipper :-( <% end_if %>
<% end_if %>
``` ```
`else_if` commands can be used to handle multiple `if` statements. `else_if` commands can be used to handle multiple `if` statements.
```ss ```ss
<% if $MyDinner == "quiche" %>
<% if $MyDinner == "quiche" %> Real men don't eat quiche
Real men don't eat quiche <% else_if $MyDinner == $YourDinner %>
<% else_if $MyDinner == $YourDinner %> We both have good taste
We both have good taste <% else %>
<% else %> Can I have some of your chips?
Can I have some of your chips? <% end_if %>
<% end_if %>
``` ```
### Negation ### Negation
@ -190,10 +181,9 @@ Conditionals can also provide the `else` case.
The inverse of `<% if %>` is `<% if not %>`. The inverse of `<% if %>` is `<% if not %>`.
```ss ```ss
<% if not $DinnerInOven %>
<% if not $DinnerInOven %> I'm going out for dinner tonight.
I'm going out for dinner tonight. <% end_if %>
<% end_if %>
``` ```
### Boolean Logic ### Boolean Logic
@ -203,19 +193,17 @@ Multiple checks can be done using `||`, `or`, `&&` or `and`.
If *either* of the conditions is true. If *either* of the conditions is true.
```ss ```ss
<% if $MyDinner == "kipper" || $MyDinner == "salmon" %>
<% if $MyDinner == "kipper" || $MyDinner == "salmon" %> yummy, fish for tea
yummy, fish for tea <% end_if %>
<% end_if %>
``` ```
If *both* of the conditions are true. If *both* of the conditions are true.
```ss ```ss
<% if $MyDinner == "quiche" && $YourDinner == "kipper" %>
<% if $MyDinner == "quiche" && $YourDinner == "kipper" %> Lets swap dinners
Lets swap dinners <% end_if %>
<% end_if %>
``` ```
### Inequalities ### Inequalities
@ -223,10 +211,9 @@ If *both* of the conditions are true.
You can use inequalities like `<`, `<=`, `>`, `>=` to compare numbers. You can use inequalities like `<`, `<=`, `>`, `>=` to compare numbers.
```ss ```ss
<% if $Number >= "5" && $Number <= "10" %>
<% if $Number >= "5" && $Number <= "10" %> Number between 5 and 10
Number between 5 and 10 <% end_if %>
<% end_if %>
``` ```
## Includes ## Includes
@ -236,37 +223,33 @@ will be searched for using the same filename look-up rules as a regular template
an additional `Includes` directory will be inserted into the resolved path just prior to the filename. an additional `Includes` directory will be inserted into the resolved path just prior to the filename.
```ss ```ss
<% include SideBar %> <!-- chooses templates/Includes/Sidebar.ss -->
<% include SideBar %> <!-- chooses templates/Includes/Sidebar.ss --> <% include MyNamespace/SideBar %> <!-- chooses templates/MyNamespace/Includes/Sidebar.ss -->
<% include MyNamespace/SideBar %> <!-- chooses templates/MyNamespace/Includes/Sidebar.ss -->
``` ```
When using subfolders in your template structure 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. (e.g. to fit with namespaces in your PHP class structure), the `Includes/` folder needs to be innermost.
```ss ```ss
<% include MyNamespace/SideBar %> <!-- chooses templates/MyNamespace/Includes/Sidebar.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` 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. the include only happens if the user is logged in.
```ss ```ss
<% if $CurrentMember %>
<% if $CurrentMember %> <% include MembersOnlyInclude %>
<% include MembersOnlyInclude %> <% end_if %>
<% end_if %>
``` ```
Includes can't directly access the parent scope when the include is included. However you can pass arguments to the Includes can't directly access the parent scope when the include is included. However you can pass arguments to the
include. include.
```ss ```ss
<% with $CurrentMember %>
<% with $CurrentMember %> <% include MemberDetails Top=$Top, Name=$Name %>
<% include MemberDetails Top=$Top, Name=$Name %> <% end_with %>
<% end_with %>
``` ```
## Looping Over Lists ## Looping Over Lists
@ -275,14 +258,12 @@ The `<% loop %>` tag is used to iterate or loop over a collection of items such
collection. collection.
```ss ```ss
<h1>Children of $Title</h1>
<h1>Children of $Title</h1> <ul>
<% loop $Children %>
<ul> <li>$Title</li>
<% loop $Children %> <% end_loop %>
<li>$Title</li> </ul>
<% end_loop %>
</ul>
``` ```
This snippet loops over the children of a page, and generates an unordered list showing the `Title` property from each This snippet loops over the children of a page, and generates an unordered list showing the `Title` property from each
@ -304,56 +285,51 @@ templates can call [DataList](api:SilverStripe\ORM\DataList) methods.
Sorting the list by a given field. Sorting the list by a given field.
```ss ```ss
<ul>
<ul> <% loop $Children.Sort(Title, ASC) %>
<% loop $Children.Sort(Title, ASC) %> <li>$Title</li>
<li>$Title</li> <% end_loop %>
<% end_loop %> </ul>
</ul>
``` ```
Limiting the number of items displayed. Limiting the number of items displayed.
```ss ```ss
<ul>
<ul> <% loop $Children.Limit(10) %>
<% loop $Children.Limit(10) %> <li>$Title</li>
<li>$Title</li> <% end_loop %>
<% end_loop %> </ul>
</ul>
``` ```
Reversing the loop. Reversing the loop.
```ss ```ss
<ul>
<ul> <% loop $Children.Reverse %>
<% loop $Children.Reverse %> <li>$Title</li>
<li>$Title</li> <% end_loop %>
<% end_loop %> </ul>
</ul>
``` ```
Filtering the loop. Filtering the loop.
```ss ```ss
<ul>
<ul> <% loop $Children.Filter('School', 'College') %>
<% loop $Children.Filter('School', 'College') %> <li>$Title</li>
<li>$Title</li> <% end_loop %>
<% end_loop %> </ul>
</ul>
``` ```
Methods can also be chained. Methods can also be chained.
```ss ```ss
<ul>
<ul> <% loop $Children.Filter('School', 'College').Sort(Score, DESC) %>
<% loop $Children.Filter('School', 'College').Sort(Score, DESC) %> <li>$Title</li>
<li>$Title</li> <% end_loop %>
<% end_loop %> </ul>
</ul>
``` ```
### Position Indicators ### Position Indicators
@ -372,16 +348,15 @@ iteration.
* `$TotalItems`: Number of items in the list (integer). * `$TotalItems`: Number of items in the list (integer).
```ss ```ss
<ul>
<% loop $Children.Reverse %>
<% if First %>
<li>My Favourite</li>
<% end_if %>
<ul> <li class="$EvenOdd">Child $Pos of $TotalItems - $Title</li>
<% loop $Children.Reverse %> <% end_loop %>
<% if First %> </ul>
<li>My Favourite</li>
<% end_if %>
<li class="$EvenOdd">Child $Pos of $TotalItems - $Title</li>
<% end_loop %>
</ul>
``` ```
<div class="info" markdown="1"> <div class="info" markdown="1">
@ -394,20 +369,19 @@ pagination.
$Modulus and $MultipleOf can help to build column and grid layouts. $Modulus and $MultipleOf can help to build column and grid layouts.
```ss ```ss
// returns an int
$Modulus(value, offset)
// returns an int // returns a boolean.
$Modulus(value, offset) $MultipleOf(factor, offset)
// returns a boolean. <% loop $Children %>
$MultipleOf(factor, offset) <div class="column-{$Modulus(4)}">
...
</div>
<% end_loop %>
<% loop $Children %> // returns <div class="column-3">, <div class="column-2">,
<div class="column-{$Modulus(4)}">
...
</div>
<% end_loop %>
// returns <div class="column-3">, <div class="column-2">,
``` ```
<div class="hint" markdown="1"> <div class="hint" markdown="1">
@ -419,12 +393,11 @@ $MultipleOf(value, offset) can also be utilized to build column and grid layouts
after every 3rd item. after every 3rd item.
```ss ```ss
<% loop $Children %>
<% loop $Children %> <% if $MultipleOf(3) %>
<% if $MultipleOf(3) %> <br>
<br> <% end_if %>
<% end_if %> <% end_loop %>
<% end_loop %>
``` ```
### Escaping ### Escaping
@ -432,25 +405,22 @@ after every 3rd item.
Sometimes you will have template tags which need to roll into one another. Use `{}` to contain variables. Sometimes you will have template tags which need to roll into one another. Use `{}` to contain variables.
```ss ```ss
$Foopx // will returns "" (as it looks for a `Foopx` value)
$Foopx // will returns "" (as it looks for a `Foopx` value) {$Foo}px // returns "3px" (CORRECT)
{$Foo}px // returns "3px" (CORRECT)
``` ```
Or when having a `$` sign in front of the variable such as displaying money. Or when having a `$` sign in front of the variable such as displaying money.
```ss ```ss
$$Foo // returns ""
$$Foo // returns "" ${$Foo} // returns "$3"
${$Foo} // returns "$3"
``` ```
You can also use a backslash to escape the name of the variable, such as: You can also use a backslash to escape the name of the variable, such as:
```ss ```ss
$Foo // returns "3"
$Foo // returns "3" \$Foo // returns "$Foo"
\$Foo // returns "$Foo"
``` ```
<div class="hint" markdown="1"> <div class="hint" markdown="1">
@ -480,16 +450,15 @@ 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. When in a particular scope, `$Up` takes the scope back to the previous level.
```ss ```ss
<h1>Children of '$Title'</h1>
<h1>Children of '$Title'</h1> <% loop $Children %>
<p>Page '$Title' is a child of '$Up.Title'</p>
<% loop $Children %> <% loop $Children %>
<p>Page '$Title' is a child of '$Up.Title'</p> <p>Page '$Title' is a grandchild of '$Up.Up.Title'</p>
<% loop $Children %>
<p>Page '$Title' is a grandchild of '$Up.Up.Title'</p>
<% end_loop %>
<% end_loop %> <% end_loop %>
<% end_loop %>
``` ```
Given the following structure, it will output the text. Given the following structure, it will output the text.
@ -513,12 +482,11 @@ Additional selectors implicitely change the scope so you need to put additional
</div> </div>
```ss ```ss
<h1>Children of '$Title'</h1>
<h1>Children of '$Title'</h1> <% loop $Children.Sort('Title').First %>
<% loop $Children.Sort('Title').First %> <%-- We have two additional selectors in the loop expression so... --%>
<%-- We have two additional selectors in the loop expression so... --%> <p>Page '$Title' is a child of '$Up.Up.Up.Title'</p>
<p>Page '$Title' is a child of '$Up.Up.Up.Title'</p> <% end_loop %>
<% end_loop %>
``` ```
#### Top #### Top
@ -527,16 +495,15 @@ While `$Up` provides us a way to go up one level of scope, `$Top` is a shortcut
page. The previous example could be rewritten to use the following syntax. page. The previous example could be rewritten to use the following syntax.
```ss ```ss
<h1>Children of '$Title'</h1>
<h1>Children of '$Title'</h1> <% loop $Children %>
<p>Page '$Title' is a child of '$Top.Title'</p>
<% loop $Children %> <% loop $Children %>
<p>Page '$Title' is a child of '$Top.Title'</p> <p>Page '$Title' is a grandchild of '$Top.Title'</p>
<% loop $Children %>
<p>Page '$Title' is a grandchild of '$Top.Title'</p>
<% end_loop %>
<% end_loop %> <% end_loop %>
<% end_loop %>
``` ```
### With ### With
@ -544,17 +511,15 @@ page. The previous example could be rewritten to use the following syntax.
The `<% with %>` tag lets you change into a new scope. Consider the following example: The `<% with %>` tag lets you change into a new scope. Consider the following example:
```ss ```ss
<% with $CurrentMember %>
<% with $CurrentMember %> Hello, $FirstName, welcome back. Your current balance is $Balance.
Hello, $FirstName, welcome back. Your current balance is $Balance. <% end_with %>
<% end_with %>
``` ```
This is functionalty the same as the following: This is functionalty the same as the following:
```ss ```ss
Hello, $CurrentMember.FirstName, welcome back. Your current balance is $CurrentMember.Balance
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. Notice that the first example is much tidier, as it removes the repeated use of the `$CurrentMember` accessor.
@ -568,8 +533,7 @@ 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. `$Me` outputs the current object in scope. This will call the `forTemplate` of the object.
```ss ```ss
$Me
$Me
``` ```
## Comments ## Comments
@ -577,16 +541,14 @@ refer directly to properties and methods of the [Member](api:SilverStripe\Securi
Using standard HTML comments is supported. These comments will be included in the published site. Using standard HTML comments is supported. These comments will be included in the published site.
```ss ```ss
$EditForm <!-- Some public comment about the form -->
$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 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. for adding notes for other developers but for things you don't want published in the public html.
```ss ```ss
$EditForm <%-- Some hidden comment about the form --%>
$EditForm <%-- Some hidden comment about the form --%>
``` ```
## Related ## Related

View File

@ -31,12 +31,11 @@ functionality may not be included.
## Base Tag ## Base Tag
```ss ```ss
<head>
<% base_tag %>
<head> ..
<% base_tag %> </head>
..
</head>
``` ```
The `<% base_tag %>` placeholder is replaced with the HTML base element. Relative links within a document (such as <img The `<% base_tag %>` placeholder is replaced with the HTML base element. Relative links within a document (such as <img
@ -51,19 +50,19 @@ A `<% base_tag %>` is nearly always required or assumed by SilverStripe to exist
## CurrentMember ## CurrentMember
Returns the currently logged in [Member](api:SilverStripe\Security\Member) instance, if there is one logged in.```ss Returns the currently logged in [Member](api:SilverStripe\Security\Member) instance, if there is one logged in.
<% if $CurrentMember %> ```ss
Welcome Back, $CurrentMember.FirstName <% if $CurrentMember %>
<% end_if %> Welcome Back, $CurrentMember.FirstName
<% end_if %>
``` ```
## Title and Menu Title ## Title and Menu Title
```ss ```ss
$Title
$Title $MenuTitle
$MenuTitle
``` ```
Most objects within SilverStripe will respond to `$Title` (i.e they should have a `Title` database field or at least a Most objects within SilverStripe will respond to `$Title` (i.e they should have a `Title` database field or at least a
@ -79,8 +78,7 @@ If `MenuTitle` is left blank by the CMS author, it'll just default to the value
## Page Content ## Page Content
```ss ```ss
$Content
$Content
``` ```
It returns the database content of the `Content` property. With the CMS Module, this is the value of the WYSIWYG editor It returns the database content of the `Content` property. With the CMS Module, this is the value of the WYSIWYG editor
@ -103,8 +101,7 @@ web pages. You'll need to install it via `composer`.
</div> </div>
```ss ```ss
$SiteConfig.Title
$SiteConfig.Title
``` ```
The [SiteConfig](../configuration/siteconfig) object allows content authors to modify global data in the CMS, rather The [SiteConfig](../configuration/siteconfig) object allows content authors to modify global data in the CMS, rather
@ -127,31 +124,29 @@ If you dont want to include the title tag use `$MetaTags(false)`.
By default `$MetaTags` renders: By default `$MetaTags` renders:
```ss ```ss
<title>Title of the Page</title>
<title>Title of the Page</title> <meta name="generator" http-equiv="generator" content="SilverStripe 3.0" />
<meta name="generator" http-equiv="generator" content="SilverStripe 3.0" /> <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
``` ```
`$MetaTags(false)` will render```ss `$MetaTags(false)` will render
<meta name="generator" http-equiv="generator" content="SilverStripe 3.0" /> ```ss
<meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <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`. If using `$MetaTags(false)` we can provide a more custom `title`.
```ss ```ss
$MetaTags(false)
$MetaTags(false) <title>$Title - Bob's Fantasy Football</title>
<title>$Title - Bob's Fantasy Football</title>
``` ```
## Links ## Links
```ss ```ss
<a href="$Link">..</a>
<a href="$Link">..</a>
``` ```
All objects that could be accessible in SilverStripe should define a `Link` method and an `AbsoluteLink` method. Link All objects that could be accessible in SilverStripe should define a `Link` method and an `AbsoluteLink` method. Link
@ -159,20 +154,18 @@ returns the relative URL for the object and `AbsoluteLink` outputs your full web
link. link.
```ss ```ss
$Link
<!-- returns /about-us/offices/ -->
$Link $AbsoluteLink
<!-- returns /about-us/offices/ --> <!-- returns http://yoursite.com/about-us/offices/ -->
$AbsoluteLink
<!-- returns http://yoursite.com/about-us/offices/ -->
``` ```
### Linking Modes ### Linking Modes
```ss ```ss
$isSection
$isSection $isCurrent
$isCurrent
``` ```
When looping over a list of `SiteTree` instances through a `<% loop $Menu %>` or `<% loop $Children %>`, `$isSection` and `$isCurrent` When looping over a list of `SiteTree` instances through a `<% loop $Menu %>` or `<% loop $Children %>`, `$isSection` and `$isCurrent`
@ -181,19 +174,17 @@ will return true or false based on page being looped over relative to the curren
For instance, to only show the menu item linked if it's the current one: For instance, to only show the menu item linked if it's the current one:
```ss ```ss
<% if $isCurrent %>
<% if $isCurrent %> $Title
$Title <% else %>
<% else %> <a href="$Link">$Title</a>
<a href="$Link">$Title</a> <% end_if %>
<% end_if %>
``` ```
An example for checking for `current` or `section` is as follows: 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>
<a class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>" href="$Link">$MenuTitle</a>
``` ```
**Additional Utility Method** **Additional Utility Method**
@ -201,10 +192,9 @@ An example for checking for `current` or `section` is as follows:
* `$InSection(page-url)`: This if block will pass if we're currently on the page-url page or one of its children. * `$InSection(page-url)`: This if block will pass if we're currently on the page-url page or one of its children.
```ss ```ss
<% if $InSection(about-us) %>
<% if $InSection(about-us) %> <p>You are viewing the about us section</p>
<p>You are viewing the about us section</p> <% end_if %>
<% end_if %>
``` ```
### URLSegment ### URLSegment
@ -214,12 +204,11 @@ This returns the part of the URL of the page you're currently on. For example on
It can be used within templates to generate anchors or other CSS classes. It can be used within templates to generate anchors or other CSS classes.
```ss ```ss
<div id="section-$URLSegment">
<div id="section-$URLSegment"> </div>
</div> <!-- returns <div id="section-offices"> -->
<!-- returns <div id="section-offices"> -->
``` ```
## ClassName ## ClassName
@ -229,19 +218,17 @@ handy for a number of uses. A common use case is to add to your `<body>` tag to
behavior based on the page type used: behavior based on the page type used:
```ss ```ss
<body class="$ClassName">
<body class="$ClassName"> <!-- returns <body class="HomePage">, <body class="BlogPage"> -->
<!-- returns <body class="HomePage">, <body class="BlogPage"> -->
``` ```
## Children Loops ## Children Loops
```ss ```ss
<% loop $Children %>
<% loop $Children %> <% end_loop %>
<% end_loop %>
``` ```
Will loop over all Children records of the current object context. Children are pages that sit under the current page in Will loop over all Children records of the current object context. Children are pages that sit under the current page in
@ -255,10 +242,9 @@ context.
### ChildrenOf ### ChildrenOf
```ss ```ss
<% loop $ChildrenOf(<my-page-url>) %>
<% loop $ChildrenOf(<my-page-url>) %> <% end_loop %>
<% end_loop %>
``` ```
Will create a list of the children of the given page, as identified by its `URLSegment` value. This can come in handy Will create a list of the children of the given page, as identified by its `URLSegment` value. This can come in handy
@ -273,19 +259,17 @@ This option will be honored by `<% loop $Children %>` and `<% loop $Menu %>` how
preference, `AllChildren` does not filter by `ShowInMenus`. preference, `AllChildren` does not filter by `ShowInMenus`.
```ss ```ss
<% loop $AllChildren %>
<% loop $AllChildren %> ...
... <% end_loop %>
<% end_loop %>
``` ```
### Menu Loops ### Menu Loops
```ss ```ss
<% loop $Menu(1) %>
<% loop $Menu(1) %> ...
... <% end_loop %>
<% 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. `$Menu(1)` returns the top-level menu of the website. You can also create a sub-menu using `$Menu(2)`, and so forth.
@ -297,10 +281,9 @@ Pages with the `ShowInMenus` property set to `false` will be filtered out.
## Access to a specific Page ## Access to a specific Page
```ss ```ss
<% with $Page(my-page) %>
<% with $Page(my-page) %> $Title
$Title <% end_with %>
<% end_with %>
``` ```
Page will return a single page from site, looking it up by URL. Page will return a single page from site, looking it up by URL.
@ -310,10 +293,9 @@ Page will return a single page from site, looking it up by URL.
### Level ### Level
```ss ```ss
<% with $Level(1) %>
<% with $Level(1) %> $Title
$Title <% end_with %>
<% 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, Will return a page in the current path, at the level specified by the numbers. It is based on the current page context,
@ -328,14 +310,13 @@ For example, imagine you're on the "bob marley" page, which is three levels in:
### Parent ### Parent
```ss ```ss
<!-- given we're on 'Bob Marley' in "about us > staff > bob marley" -->
<!-- given we're on 'Bob Marley' in "about us > staff > bob marley" --> $Parent.Title
<!-- returns 'staff' -->
$Parent.Title
<!-- returns 'staff' -->
$Parent.Parent.Title $Parent.Parent.Title
<!-- returns 'about us' --> <!-- returns 'about us' -->
``` ```
## Navigating Scope ## Navigating Scope
@ -351,20 +332,18 @@ While you can achieve breadcrumbs through the `$Level(<level>)` control manually
`$Breadcrumbs` variable. `$Breadcrumbs` variable.
```ss ```ss
$Breadcrumbs
$Breadcrumbs
``` ```
By default, it uses the template defined in `templates/BreadcrumbsTemplate.ss` By default, it uses the template defined in `templates/BreadcrumbsTemplate.ss`
of the `silverstripe/cms` module. of the `silverstripe/cms` module.
```ss ```ss
<% if $Pages %>
<% if $Pages %> <% loop $Pages %>
<% loop $Pages %> <% if $Last %>$Title.XML<% else %><a href="$Link">$MenuTitle.XML</a> &raquo;<% end_if %>
<% if $Last %>$Title.XML<% else %><a href="$Link">$MenuTitle.XML</a> &raquo;<% end_if %> <% end_loop %>
<% end_loop %> <% end_if %>
<% end_if %>
``` ```
<div class="info" markdown="1"> <div class="info" markdown="1">
@ -376,8 +355,7 @@ To customise the markup that the `$Breadcrumbs` generates, copy `templates/Bread
## Forms ## Forms
```ss ```ss
$Form
$Form
``` ```
A page will normally contain some content and potentially a form of some kind. For example, the log-in page has a the A page will normally contain some content and potentially a form of some kind. For example, the log-in page has a the

View File

@ -16,10 +16,9 @@ The `Requirements` class can work with arbitrary file paths.
**<my-module-dir>/templates/SomeTemplate.ss** **<my-module-dir>/templates/SomeTemplate.ss**
```ss ```ss
<% require css("<my-module-dir>/css/some_file.css") %>
<% require css("<my-module-dir>/css/some_file.css") %> <% require themedCSS("some_themed_file") %>
<% require themedCSS("some_themed_file") %> <% require javascript("<my-module-dir>/javascript/some_file.js") %>
<% require javascript("<my-module-dir>/javascript/some_file.js") %>
``` ```
<div class="alert" markdown="1"> <div class="alert" markdown="1">
@ -50,6 +49,8 @@ class MyCustomController extends Controller
### CSS Files ### CSS Files
```php ```php
use SilverStripe\View\Requirements;
Requirements::css($path, $media); Requirements::css($path, $media);
``` ```

View File

@ -3,97 +3,40 @@ summary: Override and extend module and core markup templates from your applicat
# Template Inheritance # Template Inheritance
Bundled within SilverStripe are default templates for any markup the framework outputs for things like Form templates, ## Theme types
Emails or RSS Feeds. These templates are provided to make getting your site up and running quick with sensible defaults
but it's easy to replace and customise SilverStripe (and add-on's) by providing custom templates in your own
`mysite/templates` folder or in your `themes/your_theme/templates` folder.
Take for instance the `GenericEmail` template in SilverStripe. This is the HTML default template that any email created Templates in SilverStripe are bundled into one of two groups:
in SilverStripe is rendered with. It's bundled in the core framework at `framework/templates/email/GenericEmail.ss`. - Default Templates, such as those provided in `mymodule/templates` folder.
- Theme templates, such as those provided in `themes/mytheme/templates` folders.
Instead of editing that file to provide a custom template for your application, simply define a template of the same The default templates provide basic HTML formatting for elements such as Forms, Email, or RSS Feeds, and provide a
name in the `mysite/templates/email` folder or in the `themes/your_theme/templates/email` folder if you're using themes. generic base for web content to be built on.
**mysite/templates/email/GenericEmail.ss** ## Template types and locations
```ss
$Body
<p>Thanks from Bob's Fantasy Football League.</p> Typically all templates within one of the above locations will be nested in a folder deterministically through
``` the fully qualified namespace of the underlying class, and an optional `type` specifier to segment template types.
All emails going out of our application will have the footer `Thanks from Bob's Fantasy Football Leaguee` added. Basic template types include `Layout` and `Includes`, and a less commonly used `Content` type.
<div class="alert" markdown="1"> For instance, a class `SilverStripe\Blog\BlogPage` will have a default template of type `Layout`
As we've added a new file, make sure you flush your SilverStripe cache by visiting `http://yoursite.com/?flush=1` in the folder `vendor/silverstripe/blog/templates/SilverStripe/Blog/Layout/BlogPage.ss`.
</div>
Template inheritance works on more than email templates. All files within the `templates` directory including `includes`, Note: The optional `type`, if specified, will require a nested folder at the end of the parent namespace
`layout` or anything else from core (or add-on's) template directory can be overridden by being located inside your (`SilverStripe\Blog`) to the class, but before the filename (`BlogPage`).
`mysite/templates` directory. SilverStripe keeps an eye on what templates have been overridden and the location of the
correct template through a [ThemeResourceLoader](api:SilverStripe\View\ThemeResourceLoader).
## ThemeResourceLoader Templates not backed by any class can exist in any location, but must always be referred to in code
by the full path (from the `templates` folder onwards).
The location of each template and the hierarchy of what template to use is stored within a [ThemeResourceLoader](api:SilverStripe\View\ThemeResourceLoader) ### Nested Layouts through `$Layout` type
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
The order in which templates are selected from themes can be explicitly declared
through configuration. To specify the order you want, make a list of the module
names under `SilverStripe\Core\Manifest\ModuleManifest.module_priority` in a
configuration YAML file.
*some-module/_config.yml*
```yml
SilverStripe\Core\Manifest\ModuleManifest:
module_priority:
- 'example/module-one'
- 'example/module-two'
- '$other_modules'
- 'example/module-three'
```
The placeholder `$other_modules` is used to mark where all of the modules not specified
in the list should appear. (In alphabetical order of their containing directory names).
In this example, the module named `example/module-one` has the highest level of precedence,
followed by `example/module-two`. The module `example/module-three` is guaranteed the lowest
level of precedence.
### Defining a "project"
It is a good idea to define one of your modules as the `project`. Commonly, this is the
`mysite/` module, but there is nothing compulsory about that module name. The "project"
module can be specified as a variable in the `module_priorities` list, as well.
*some-module/_config.yml*
```yml
SilverStripe\Core\Manifest\ModuleManifest:
project: 'myapp'
module_priority:
- '$project'
- '$other_modules'
```
### About module "names"
Module names are derived their local `composer.json` files using the following precedence:
* The value of the `name` attribute in `composer.json`
* The value of `extras.installer_name` in `composer.json`
* The basename of the directory that contains the module
## Nested Layouts through `$Layout`
SilverStripe has basic support for nested layouts through a fixed template variable named `$Layout`. It's used for SilverStripe has basic support for nested layouts through a fixed template variable named `$Layout`. It's used for
storing top level template information separate to individual page layouts. storing top level template information separate to individual page layouts.
When `$Layout` is found within a root template file (one in `templates`), SilverStripe will attempt to fetch a child When `$Layout` is found within a root template file (one in `templates`), SilverStripe will attempt to fetch a child
template from the `templates/Layout` directory. It will do a full sweep of your modules, core and custom code as it template from the `templates/<namespace>/Layout/<class>.ss` path, where `<namespace>` and `<class>` represent
would if it was looking for a new root template. the class being rendered. It will do a full sweep of your modules, core and custom code as it
would if it was looking for a new root template, as well as looking down the class hierarchy until
it finds a template.
This is better illustrated with an example. Take for instance our website that has two page types `Page` and `HomePage`. This is better illustrated with an example. Take for instance our website that has two page types `Page` and `HomePage`.
@ -102,35 +45,138 @@ 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. `$Layout` function allows us to define the child template area which can be overridden.
**mysite/templates/Page.ss** **mysite/templates/Page.ss**
```ss ```ss
<html> <html>
<head> <head>
.. ..
</head> </head>
<body>
<% include Header %>
<% include Navigation %>
$Layout <body>
<% include Header %>
<% include Navigation %>
<% include Footer %> $Layout
</body>
``
**mysite/templates/Layout/Page.ss**
```ss
<p>You are on a $Title page</p>
$Content <% include Footer %>
</body>
``` ```
**mysite/templates/Layout/HomePage.ss**
```ss
<h1>This is the homepage!</h1>
<blink>Hi!</blink> **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. If your classes have in a namespace, the Layout folder will be a found inside of the appropriate namespace folder.
For example, the layout template for `SilverStripe\Control\Controller` will be For example, the layout template for `SilverStripe\Control\Controller` will be
found at `templates/SilverStripe/Control/Layout/Controller.ss`. found at `templates/SilverStripe/Control/Layout/Controller.ss`.
## Cascading themes
Within each theme or templates folder, a specific path representing a template can potentially be found. As
there may be multiple instances of any matching path for a template across the set of all themes, a cascading
search is done in order to determine the resolved template for any specified string.
In order to declare the priority for this search, themes can be declared in a cascading fashion in order
to determine resolution priority. This search is based on the following three configuration values:
- `SilverStripe\View\SSViewer.themes` - The list of all themes in order of priority (highest first).
This includes the default set via `$default` as a theme set. This config is normally set by the web
developer.
- `SilverStripe\Core\Manifest\ModuleManifest.module_priority` - The list of modules within which $default
theme templates should be sorted, in order of priority (highest first). This config is normally set by
the module author, and does not normally need to be customised. This includes the `$project` and
`$other_modules` placeholder values.
- `SilverStripe\Core\Manifest\ModuleManifest.project` - The name of the `$project` module, which
defaults to `mysite`.
### ThemeResourceLoader
The resolution of themes is performed by a [ThemeResourceLoader](api:SilverStripe\View\ThemeResourceLoader)
instance, which resolves a template (or list of templates) and a set of themes to a system template path.
For each path the loader will search in this order:
- Loop through each theme which is configured.
- If a theme is a set (declared with the `$` prefix, e.g. `$default`) it will perform a nested search within
that set.
- When searching the `$default` set, all modules will be searched in the order declared via the `module_priority`
config, interpolating keys `$project` and `$other_modules` as necessary.
- When the first template is found, it will be immediately returned, and will not continue to search.
### Declaring themes
All themes can be enabled and sorted via the `SilverStripe\View\SSViewer.themes` config value. For reference
on what syntax styles you can use for this value please see the [themes configuration](./themes) documentation.
Basic example:
```yaml
---
Name: mytheme
---
SilverStripe\View\SSViewer:
themes:
- theme_name
- '$default'
```
### Declaring module priority
The order in which templates are selected from themes can be explicitly declared
through configuration. To specify the order you want, make a list of the module
names under `SilverStripe\Core\Manifest\ModuleManifest.module_priority` in a
configuration YAML file.
Note: In order for modules to sort relative to other modules, it's normally necessary
to provide `before:` / `after:` declarations.
*mymodule/_config.yml*
```yml
Name: modules-mymodule
After:
- '#modules-framework'
- `#modules-other`
---
SilverStripe\Core\Manifest\ModuleManifest:
module_priority:
- myvendor/mymodule
```
In this example, our module has applied its priority lower than framework modules, meaning template lookup
will only defer to our modules templates folder if not found elsewhere.
### Declaring project
It is a good idea to define one of your modules as the `project`. Commonly, this is the
`mysite/` module, but there is nothing compulsory about that module name.
*myapp/_config/config.yml*
```yml
---
Name: myproject
---
SilverStripe\Core\Manifest\ModuleManifest:
project: 'myapp'
```
### About module "names"
Module names are derived their local `composer.json` files using the following precedence:
* The value of the `name` attribute in `composer.json`
* The value of `extras.installer_name` in `composer.json`
* The basename of the directory that contains the module

View File

@ -9,22 +9,21 @@ All functions that provide data to templates must have no side effects, as the v
example, this controller method will not behave as you might imagine. example, this controller method will not behave as you might imagine.
```php ```php
private $counter = 0; private $counter = 0;
public function Counter() public function Counter()
{ {
$this->counter += 1; $this->counter += 1;
return $this->counter; return $this->counter;
} }
``` ```
```ss ```ss
$Counter, $Counter, $Counter
$Counter, $Counter, $Counter // returns 1, 1, 1
// 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 When we render `$Counter` to the template we would expect the value to increase and output `1, 2, 3`. However, as
@ -37,10 +36,9 @@ Partial caching is a feature that allows the caching of just a portion of a page
from the database to display, the contents of the area are fetched from a [cache backend](../performance/caching). from the database to display, the contents of the area are fetched from a [cache backend](../performance/caching).
```ss ```ss
<% cached 'MyCachedContent', LastEdited %>
<% cached 'MyCachedContent', LastEdited %> $Title
$Title <% end_cached %>
<% end_cached %>
``` ```

View File

@ -5,11 +5,13 @@ 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 Translations are easy to use with a template, and give access to SilverStripe's translation facilities. Here is an
example: example:
```ss
<%t Foo.BAR 'Bar' %>
<%t Member.WELCOME 'Welcome {name} to {site}' name=$Member.Name site="Foobar.com" %> ```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 `Member.WELCOME` is an identifier in the translation system, for which different translations may be available. This
string may include named placeholders, in braces. string may include named placeholders, in braces.

View File

@ -13,27 +13,25 @@ output the result of the [DBHtmlText::FirstParagraph()](api:SilverStripe\ORM\Fie
**mysite/code/Page.ss** **mysite/code/Page.ss**
```ss ```ss
$Content.FirstParagraph
<!-- returns the result of HtmlText::FirstParagragh() -->
$Content.FirstParagraph $LastEdited.Format("d/m/Y")
<!-- returns the result of HtmlText::FirstParagragh() --> <!-- returns the result of SS_Datetime::Format("d/m/Y") -->
$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 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. `ViewableData` instance, you can chain the method calls.
```ss ```ss
$Content.FirstParagraph.NoHTML
<!-- "First Paragraph" -->
$Content.FirstParagraph.NoHTML <p>Copyright {$Now.Year}</p>
<!-- "First Paragraph" --> <!-- "Copyright 2014" -->
<p>Copyright {$Now.Year}</p> <div class="$URLSegment.LowerCase">
<!-- "Copyright 2014" --> <!-- <div class="about-us"> -->
<div class="$URLSegment.LowerCase">
<!-- <div class="about-us"> -->
``` ```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
@ -47,24 +45,25 @@ When rendering an object to the template such as `$Me` the `forTemplate` method
provide default template for an object. provide default template for an object.
**mysite/code/Page.php** **mysite/code/Page.php**
```php ```php
use SilverStripe\CMS\Model\SiteTree; 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** **mysite/templates/Page.ss**
```ss
$Me ```ss
<!-- returns Page: Home --> $Me
<!-- returns Page: Home -->
``` ```
## Casting ## Casting
@ -74,21 +73,20 @@ content that method sends back, or, provide a type in the `$casting` array for t
to a template, SilverStripe will ensure that the object is wrapped in the correct type and values are safely escaped. to a template, SilverStripe will ensure that the object is wrapped in the correct type and values are safely escaped.
```php ```php
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
class Page extends SiteTree class Page extends SiteTree
{
private static $casting = [
'MyCustomMethod' => 'HTMLText'
];
public function MyCustomMethod()
{ {
return "<h1>This is my header</h1>";
private static $casting = [
'MyCustomMethod' => 'HTMLText'
];
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 When calling `$MyCustomMethod` SilverStripe now has the context that this method will contain HTML and escape the data

View File

@ -8,26 +8,26 @@ top level menu with a nested second level using the `Menu` loop and a `Children`
**mysite/templates/Page.ss** **mysite/templates/Page.ss**
```ss ```ss
<ul> <ul>
<% loop $Menu(1) %> <% loop $Menu(1) %>
<li> <li>
<a href="$Link" title="Go to the $Title page" class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>"> <a href="$Link" title="Go to the $Title page" class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>">
$MenuTitle $MenuTitle
</a> </a>
<% if $isSection %> <% if $isSection %>
<% if $Children %> <% if $Children %>
<ul class="secondary"> <ul class="secondary">
<% loop $Children %> <% loop $Children %>
<li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>"><a href="$Link">$MenuTitle</a></li> <li class="<% if $isCurrent %>current<% else_if $isSection %>section<% end_if %>"><a href="$Link">$MenuTitle</a></li>
<% end_loop %> <% end_loop %>
</ul> </ul>
<% end_if %> <% end_if %>
<% end_if %> <% end_if %>
</li> </li>
<% end_loop %> <% end_loop %>
</ul> </ul>
``` ```w
## Related ## Related
* [Template Syntax](../syntax) * [Template Syntax](../syntax)

View File

@ -11,15 +11,17 @@ The `PaginatedList` will automatically set up query limits and read the request
**mysite/code/Page.php** **mysite/code/Page.php**
```php ```php
/** use SilverStripe\ORM\PaginatedList;
* Returns a paginated list of all pages in the site.
*/
public function PaginatedPages()
{
$list = Page::get();
return new PaginatedList($list, $this->getRequest()); /**
} * Returns a paginated list of all pages in the site.
*/
public function PaginatedPages()
{
$list = Page::get();
return new PaginatedList($list, $this->getRequest());
}
``` ```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
@ -35,11 +37,11 @@ The first step is to simply list the objects in the template:
**mysite/templates/Page.ss** **mysite/templates/Page.ss**
```ss ```ss
<ul> <ul>
<% loop $PaginatedPages %> <% loop $PaginatedPages %>
<li><a href="$Link">$Title</a></li> <li><a href="$Link">$Title</a></li>
<% end_loop %> <% end_loop %>
</ul> </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 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: switch between pages:
@ -47,25 +49,25 @@ switch between pages:
**mysite/templates/Page.ss** **mysite/templates/Page.ss**
```ss ```ss
<% if $PaginatedPages.MoreThanOnePage %> <% if $PaginatedPages.MoreThanOnePage %>
<% if $PaginatedPages.NotFirstPage %> <% if $PaginatedPages.NotFirstPage %>
<a class="prev" href="$PaginatedPages.PrevLink">Prev</a> <a class="prev" href="$PaginatedPages.PrevLink">Prev</a>
<% end_if %> <% end_if %>
<% loop $PaginatedPages.Pages %> <% loop $PaginatedPages.Pages %>
<% if $CurrentBool %> <% if $CurrentBool %>
$PageNum $PageNum
<% else %> <% else %>
<% if $Link %> <% if $Link %>
<a href="$Link">$PageNum</a> <a href="$Link">$PageNum</a>
<% else %> <% else %>
... ...
<% end_if %> <% end_if %>
<% end_if %> <% end_if %>
<% end_loop %> <% end_loop %>
<% if $PaginatedPages.NotLastPage %> <% if $PaginatedPages.NotLastPage %>
<a class="next" href="$PaginatedPages.NextLink">Next</a> <a class="next" href="$PaginatedPages.NextLink">Next</a>
<% end_if %> <% end_if %>
<% end_if %> <% end_if %>
``` ```
If there is more than one page, this block will render a set of pagination controls in the form If there is more than one page, this block will render a set of pagination controls in the form
@ -79,17 +81,19 @@ will break the pagination. You can disable automatic limiting using the [Paginat
when using custom lists. when using custom lists.
```php ```php
$myPreLimitedList = Page::get()->limit(10); use SilverStripe\ORM\PaginatedList;
$pages = new PaginatedList($myPreLimitedList, $this->getRequest()); $myPreLimitedList = Page::get()->limit(10);
$pages->setLimitItems(false);
$pages = new PaginatedList($myPreLimitedList, $this->getRequest());
$pages->setLimitItems(false);
``` ```
## Setting the limit of items ## Setting the limit of items
```php ```php
$pages = new PaginatedList(Page::get(), $this->getRequest()); $pages = new PaginatedList(Page::get(), $this->getRequest());
$pages->setPageLength(25); $pages->setPageLength(25);
``` ```
If you set this limit to 0 it will disable paging entirely, effectively causing it to appear as a single page If you set this limit to 0 it will disable paging entirely, effectively causing it to appear as a single page

View File

@ -6,10 +6,10 @@ Anchor links are links with a "#" in them. A frequent use-case is to use anchor
the current page. For example, we might have this in our template: the current page. For example, we might have this in our template:
```ss ```ss
<ul> <ul>
<li><a href="#section1">Section 1</a></li> <li><a href="#section1">Section 1</a></li>
<li><a href="#section2">Section 2</a></li> <li><a href="#section2">Section 2</a></li>
</ul> </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 Things get tricky because of we have set our `<base>` tag to point to the root of the site. So, when you click the
@ -20,10 +20,10 @@ doesn't specify a URL before the anchor, prefixing the URL of the current page.
would be created in the final HTML would be created in the final HTML
```ss ```ss
<ul> <ul>
<li><a href="my-long-page/#section1">Section 1</a></li> <li><a href="my-long-page/#section1">Section 1</a></li>
<li><a href="my-long-page/#section2">Section 2</a></li> <li><a href="my-long-page/#section2">Section 2</a></li>
</ul> </ul>
``` ```
There are cases where this can be unhelpful. HTML anchors created from Ajax responses are the most common. In these There are cases where this can be unhelpful. HTML anchors created from Ajax responses are the most common. In these
@ -33,19 +33,21 @@ situations, you can disable anchor link rewriting by setting the `SSViewer.rewri
**mysite/_config/app.yml** **mysite/_config/app.yml**
```yml ```yml
SSViewer: SilverStripe\View\SSViewer:
rewrite_hash_links: false rewrite_hash_links: false
``` ```
Or, a better way is to call this just for the rendering phase of this particular file: Or, a better way is to call this just for the rendering phase of this particular file:
```php ```php
public function RenderCustomTemplate() use SilverStripe\View\SSViewer;
{
SSViewer::setRewriteHashLinks(false);
$html = $this->renderWith('MyCustomTemplate');
SSViewer::setRewriteHashLinks(true);
return $html; public function RenderCustomTemplate()
} {
``` SSViewer::setRewriteHashLinks(false);
$html = $this->renderWith('MyCustomTemplate');
SSViewer::setRewriteHashLinks(true);
return $html;
}
```

View File

@ -9,27 +9,26 @@ subclass the base `Controller` class.
**mysite/code/controllers/TeamController.php** **mysite/code/controllers/TeamController.php**
```php ```php
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class TeamController extends Controller class TeamController extends Controller
{ {
private static $allowed_actions = [
'players',
'index'
];
public function index(HTTPRequest $request) private static $allowed_actions = [
{ 'players',
// .. 'index'
} ];
public function players(HTTPRequest $request) public function index(HTTPRequest $request)
{ {
print_r($request->allParams()); // ..
}
} }
public function players(HTTPRequest $request)
{
print_r($request->allParams());
}
}
``` ```
## Routing ## Routing
@ -50,14 +49,13 @@ Make sure that after you have modified the `routes.yml` file, that you clear you
**mysite/_config/routes.yml** **mysite/_config/routes.yml**
```yml ```yml
---
--- Name: mysiteroutes
Name: mysiteroutes After: framework/routes#coreroutes
After: framework/routes#coreroutes ---
--- SilverStripe\Control\Director:
SilverStripe\Control\Director: rules:
rules: 'teams//$Action/$ID/$Name': 'TeamController'
'teams//$Action/$ID/$Name': 'TeamController'
``` ```
For more information about creating custom routes, see the [Routing](routing) documentation. For more information about creating custom routes, see the [Routing](routing) documentation.
@ -81,63 +79,62 @@ Action methods can return one of four main things:
**mysite/code/controllers/TeamController.php** **mysite/code/controllers/TeamController.php**
```php ```php
/** /**
* Return some additional data to the current response that is waiting to go out, this makes $Title set to * 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. * 'MyTeamName' and continues on with generating the response.
*/ */
public function index(HTTPRequest $request) public function index(HTTPRequest $request)
{ {
return [ return [
'Title' => 'My Team Name' 'Title' => 'My Team Name'
]; ];
} }
/** /**
* We can manually create a response and return that to ignore any previous data. * We can manually create a response and return that to ignore any previous data.
*/ */
public function someaction(HTTPRequest $request) public function someaction(HTTPRequest $request)
{ {
$this->setResponse(new HTTPResponse()); $this->setResponse(new HTTPResponse());
$this->getResponse()->setStatusCode(400); $this->getResponse()->setStatusCode(400);
$this->getResponse()->setBody('invalid'); $this->getResponse()->setBody('invalid');
return $this->getResponse(); return $this->getResponse();
} }
/** /**
* Or, we can modify the response that is waiting to go out. * Or, we can modify the response that is waiting to go out.
*/ */
public function anotheraction(HTTPRequest $request) public function anotheraction(HTTPRequest $request)
{ {
$this->getResponse()->setStatusCode(400); $this->getResponse()->setStatusCode(400);
return $this->getResponse(); return $this->getResponse();
} }
/** /**
* We can render HTML and leave SilverStripe to set the response code and body. * We can render HTML and leave SilverStripe to set the response code and body.
*/ */
public function htmlaction() public function htmlaction()
{ {
return $this->customise(new ArrayData([ return $this->customise(new ArrayData([
'Title' => 'HTML Action' 'Title' => 'HTML Action'
]))->renderWith('MyCustomTemplate'); ]))->renderWith('MyCustomTemplate');
} }
/** /**
* We can send stuff to the browser which isn't HTML * We can send stuff to the browser which isn't HTML
*/ */
public function ajaxaction() public function ajaxaction()
{ {
$this->getResponse()->setBody(json_encode([ $this->getResponse()->setBody(json_encode([
'json' => true '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. For more information on how a URL gets mapped to an action see the [Routing](routing) documentation.
@ -167,10 +164,10 @@ Each controller should define a `Link()` method. This should be used to avoid ha
**mysite/code/controllers/TeamController.php** **mysite/code/controllers/TeamController.php**
```php ```php
public function Link($action = null) public function Link($action = null)
{ {
return Controller::join_links('teams', $action); return Controller::join_links('teams', $action);
} }
``` ```
<div class="info" markdown="1"> <div class="info" markdown="1">

View File

@ -12,32 +12,31 @@ Any action you define on a controller must be defined in a `$allowed_actions` st
directly calling methods that they shouldn't. directly calling methods that they shouldn't.
```php ```php
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends Controller class MyController extends Controller
{ {
private static $allowed_actions = [
// someaction can be accessed by anyone, any time
'someaction',
// So can otheraction
'otheraction' => true,
private static $allowed_actions = [ // restrictedaction can only be people with ADMIN privilege
// someaction can be accessed by anyone, any time 'restrictedaction' => 'ADMIN',
'someaction',
// So can otheraction // restricted to uses that have the 'CMS_ACCESS_CMSMain' access
'otheraction' => true, 'cmsrestrictedaction' => 'CMS_ACCESS_CMSMain',
// restrictedaction can only be people with ADMIN privilege // complexaction can only be accessed if $this->canComplexAction() returns true.
'restrictedaction' => 'ADMIN', '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"> <div class="info">
@ -48,88 +47,86 @@ An action named "index" is white listed by default, unless `allowed_actions` is
is specifically restricted. is specifically restricted.
```php ```php
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends Controller
{
<?php public function index()
class MyController extends Controller
{ {
// 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. `$allowed_actions` can be defined on `Extension` classes applying to the controller.
```php ```php
use SilverStripe\Core\Extension; use SilverStripe\Core\Extension;
class MyExtension extends Extension class MyExtension extends Extension
{ {
private static $allowed_actions = [
'mycustomaction'
];
}
private static $allowed_actions = [
'mycustomaction'
];
}
``` ```
Only public methods can be made accessible. Only public methods can be made accessible.
```php ```php
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends Controller class MyController extends Controller
{
private static $allowed_actions = [
'secure',
// secureaction won't work as it's private.
];
public function secure()
{ {
// ..
private static $allowed_actions = [
'secure',
// secureaction won't work as it's private.
];
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. If a method on a parent class is overwritten, access control for it has to be redefined as well.
```php ```php
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends Controller class MyController extends Controller
{
private static $allowed_actions = [
'action',
];
public function action()
{
// ..
}
}
class MyChildController extends MyController
{
private static $allowed_actions = [
'action', // required as we are redefining action
];
public function action()
{ {
private static $allowed_actions = [
'action',
];
public function action()
{
// ..
}
}
class MyChildController extends MyController
{
private static $allowed_actions = [
'action', // required as we are redefining action
];
public function action()
{
}
} }
}
``` ```
@ -141,28 +138,29 @@ 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 Form action methods should **not** be included in `$allowed_actions`. However, the form method **should** be included
as an `allowed_action`. as an `allowed_action`.
```php ```php
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends Controller class MyController extends Controller
{
private static $allowed_actions = [
'ContactForm' // use the Form method, not the action
];
public function ContactForm()
{ {
return new Form(..);
private static $allowed_actions = [
'ContactForm' // use the Form method, not the action
];
public function ContactForm()
{
return new Form(..);
}
public function doContactForm($data, $form)
{
// ..
}
} }
public function doContactForm($data, $form)
{
// ..
}
}
``` ```
## Action Level Checks ## Action Level Checks
@ -171,24 +169,24 @@ Each method responding to a URL can also implement custom permission checks, e.g
the passed request data. the passed request data.
```php ```php
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends Controller class MyController extends Controller
{
private static $allowed_actions = [
'myaction'
];
public function myaction($request)
{ {
if(!$request->getVar('apikey')) {
private static $allowed_actions = [ return $this->httpError(403, 'No API key provided');
'myaction' }
];
return 'valid';
public function myaction($request)
{
if(!$request->getVar('apikey')) {
return $this->httpError(403, 'No API key provided');
}
return 'valid';
}
} }
}
``` ```
@ -206,25 +204,25 @@ execution. This behavior can be used to implement permission checks.
<div class="info" markdown="1"> <div class="info" markdown="1">
`init` is called for any possible action on the controller and before any specific method such as `index`. `init` is called for any possible action on the controller and before any specific method such as `index`.
</div> </div>
```php ```php
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends Controller class MyController extends Controller
{
private static $allowed_actions = [];
public function init()
{ {
parent::init();
private static $allowed_actions = [];
public function init()
{
parent::init();
if(!Permission::check('ADMIN')) { if(!Permission::check('ADMIN')) {
return $this->httpError(403); return $this->httpError(403);
}
} }
} }
}
``` ```
## Related Documentation ## Related Documentation

View File

@ -10,26 +10,27 @@ HTTP header.
```php ```php
$this->redirect('goherenow'); $this->redirect('goherenow');
// redirect to Page::goherenow(), i.e on the contact-us page this will redirect to /contact-us/goherenow/ // redirect to Page::goherenow(), i.e on the contact-us page this will redirect to /contact-us/goherenow/
$this->redirect('goherenow/'); $this->redirect('goherenow/');
// redirect to the URL on yoursite.com/goherenow/. (note the trailing slash) // redirect to the URL on yoursite.com/goherenow/. (note the trailing slash)
$this->redirect('http://google.com'); $this->redirect('http://google.com');
// redirect to http://google.com // redirect to http://google.com
$this->redirectBack(); $this->redirectBack();
// go back to the previous page. // go back to the previous page.
``` ```
## Status Codes ## Status Codes
The `redirect()` method takes an optional HTTP status code, either `301` for permanent redirects, or `302` for The `redirect()` method takes an optional HTTP status code, either `301` for permanent redirects, or `302` for
temporary redirects (default). temporary redirects (default).
```php ```php
$this->redirect('/', 302); $this->redirect('/', 302);
// go back to the homepage, don't cache that this page has moved // go back to the homepage, don't cache that this page has moved
``` ```
## Redirection in URL Handling ## Redirection in URL Handling
@ -37,12 +38,10 @@ temporary redirects (default).
Controllers can specify redirections in the `$url_handlers` property rather than defining a method by using the '~' Controllers can specify redirections in the `$url_handlers` property rather than defining a method by using the '~'
operator. operator.
```php ```php
private static $url_handlers = [ private static $url_handlers = [
'players/john' => '~>coach' 'players/john' => '~>coach'
]; ];
``` ```
For more information on `$url_handlers` see the [Routing](routing) documenation. For more information on `$url_handlers` see the [Routing](routing) documenation.

View File

@ -16,13 +16,16 @@ Creating a [Form](api:SilverStripe\Forms\Form) has the following signature.
```php ```php
$form = new Form( use SilverStripe\Forms\Form;
$controller, // the Controller to render this form on use SilverStripe\Forms\FieldList;
$name, // name of the method that returns this form on the controller
FieldList $fields, // list of FormField instances $form = new Form(
FieldList $actions, // list of FormAction instances $controller, // the Controller to render this form on
$required // optional use of RequiredFields object $name, // name of the method that returns this form on the controller
); FieldList $fields, // list of FormField instances
FieldList $actions, // list of FormAction instances
$required // optional use of RequiredFields object
);
``` ```
In practice, this looks like: In practice, this looks like:
@ -341,7 +344,6 @@ $validator = new SilverStripe\Forms\RequiredFields([
]); ]);
$form = new Form($this, 'MyForm', $fields, $actions, $validator); $form = new Form($this, 'MyForm', $fields, $actions, $validator);
``` ```
## API Documentation ## API Documentation

View File

@ -174,9 +174,8 @@ class Page_Controller extends ContentController
return $this->redirectBack(); return $this->redirectBack();
} }
} }
``` ```
## Exempt validation actions ## Exempt validation actions
In some cases you might need to disable validation for specific actions. E.g. actions which discard submitted In some cases you might need to disable validation for specific actions. E.g. actions which discard submitted

View File

@ -8,12 +8,12 @@ can be rendered out using custom templates using `setTemplate`.
```php ```php
$form = new Form(..); $form = new Form(..);
$form->setTemplate('MyCustomFormTemplate'); $form->setTemplate('MyCustomFormTemplate');
// or, just a field // or, just a field
$field = new TextField(..); $field = new TextField(..);
$field->setTemplate('MyCustomTextField'); $field->setTemplate('MyCustomTextField');
``` ```
Both `MyCustomTemplate.ss` and `MyCustomTextField.ss` should be located in **mysite/templates/forms/** or the same directory as the core. Both `MyCustomTemplate.ss` and `MyCustomTextField.ss` should be located in **mysite/templates/forms/** or the same directory as the core.
@ -37,27 +37,27 @@ For [FormField](api:SilverStripe\Forms\FormField) instances, there are several o
```php ```php
$field = new TextField(); $field = new TextField();
$field->setTemplate('CustomTextField'); $field->setTemplate('CustomTextField');
// Sets the template for the <input> tag. i.e '<input $AttributesHTML />' // 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'); $field->setFieldHolderTemplate('CustomTextField_Holder');
// Sets the template for the wrapper around the text field. // Sets the template for the wrapper around the text field. i.e
// // '<div class="text">'
// 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 // The actual FormField is rendered into the holder via the `$Field`
// part of a `FieldGroup` or `CompositeField` alongside other fields. // 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 All templates are rendered within the scope of the [FormField](api:SilverStripe\Forms\FormField). To understand more about Scope within Templates as

View File

@ -22,16 +22,18 @@ The `SecurityToken` automatically added looks something like:
```php ```php
$form = new Form(..); use SilverStripe\Forms\Form;
echo $form->getSecurityToken()->getValue();
// 'c443076989a7f24cf6b35fe1360be8683a753e2c' $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). 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. The token should be present whenever a operation has a side effect such as a `POST` operation.
@ -41,8 +43,8 @@ normally require a security token).
```php ```php
$form = new Form(..); $form = new Form(..);
$form->disableSecurityToken(); $form->disableSecurityToken();
``` ```
<div class="alert" markdown="1"> <div class="alert" markdown="1">
@ -58,13 +60,13 @@ application errors or edge cases. If you need to disable this setting follow the
```php ```php
$form = new Form(..); $form = new Form(..);
$form->setFormMethod('POST'); $form->setFormMethod('POST');
$form->setStrictFormMethodCheck(false); $form->setStrictFormMethodCheck(false);
// or alternative short notation.. // or alternative short notation..
$form->setFormMethod('POST', false); $form->setFormMethod('POST', false);
``` ```
## Spam and Bot Attacks ## Spam and Bot Attacks

View File

@ -11,36 +11,43 @@ To make an entire [Form](api:SilverStripe\Forms\Form) read-only.
```php ```php
$form = new Form(..); use SilverStripe\Forms\Form;
$form->makeReadonly();
$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). 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 ```php
$fields = new FieldList(..); use SilverStripe\Forms\FieldList;
$fields = $fields->makeReadonly();
$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 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 ```php
$field = new TextField(..); use SilverStripe\Forms\TextField;
$field = $field->performReadonlyTransformation(); use SilverStripe\Forms\FieldList;
$fields = new FieldList( $field = new TextField(..);
$field $field = $field->performReadonlyTransformation();
);
// Or, $fields = new FieldList(
$field = new TextField(..); $field
$field->setReadonly(true); );
$fields = new FieldList( // Or,
$field $field = new TextField(..);
); $field->setReadonly(true);
$fields = new FieldList(
$field
);
``` ```
## Disabled FormFields ## Disabled FormFields
@ -49,11 +56,10 @@ Disabling [FormField](api:SilverStripe\Forms\FormField) instances, sets the `dis
a normal form, but set the `disabled` attribute on the `input` tag. a normal form, but set the `disabled` attribute on the `input` tag.
```php ```php
$field = new TextField(..); $field = new TextField(..);
$field->setDisabled(true); $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" />'
```
```

View File

@ -23,42 +23,41 @@ display up to two levels of tabs in the interface.
```php ```php
$fields->addFieldToTab('Root.Main', new TextField(..)); $fields->addFieldToTab('Root.Main', new TextField(..));
``` ```
## Removing a field from a tab ## Removing a field from a tab
```php ```php
$fields->removeFieldFromTab('Root.Main', 'Content'); $fields->removeFieldFromTab('Root.Main', 'Content');
``` ```
## Creating a new tab ## Creating a new tab
```php ```php
$fields->addFieldToTab('Root.MyNewTab', new TextField(..)); $fields->addFieldToTab('Root.MyNewTab', new TextField(..));
``` ```
## Moving a field between tabs ## Moving a field between tabs
```php ```php
$content = $fields->dataFieldByName('Content'); $content = $fields->dataFieldByName('Content');
$fields->removeFieldFromTab('Root.Main', 'Content'); $fields->removeFieldFromTab('Root.Main', 'Content');
$fields->addFieldToTab('Root.MyContent', $content); $fields->addFieldToTab('Root.MyContent', $content);
``` ```
## Add multiple fields at once ## Add multiple fields at once
```php ```php
$fields->addFieldsToTab('Root.Content', [ $fields->addFieldsToTab('Root.Content', [
TextField::create('Name'), TextField::create('Name'),
TextField::create('Email') TextField::create('Email')
]); ]);
``` ```
## API Documentation ## API Documentation

View File

@ -16,29 +16,27 @@ The following example will add a simple DateField to your Page, allowing you to
```php ```php
use SilverStripe\Forms\DateField; use SilverStripe\Forms\DateField;
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
class Page extends SiteTree class Page extends SiteTree
{
private static $db = [
'MyDate' => 'Date',
];
public function getCMSFields()
{ {
$fields = parent::getCMSFields();
private static $db = [
'MyDate' => 'Date', $fields->addFieldToTab(
]; 'Root.Main',
DateField::create('MyDate', 'Enter a date')
public function getCMSFields() );
{
$fields = parent::getCMSFields(); return $fields;
}
$fields->addFieldToTab( }
'Root.Main',
DateField::create('MyDate', 'Enter a date')
);
return $fields;
}
}
``` ```
## Custom Date Format ## Custom Date Format
@ -48,10 +46,10 @@ This is only necessary if you want to opt-out of the built-in browser localisati
```php ```php
// will display a date in the following format: 31/06/2012 // will display a date in the following format: 31/06/2012
DateField::create('MyDate') DateField::create('MyDate')
->setHTML5(false) ->setHTML5(false)
->setDateFormat('dd/MM/yyyy'); ->setDateFormat('dd/MM/yyyy');
``` ```
<div class="info" markdown="1"> <div class="info" markdown="1">
@ -66,9 +64,9 @@ Sets the minimum and maximum allowed date values using the `min` and `max` confi
```php ```php
DateField::create('MyDate') DateField::create('MyDate')
->setMinDate('-7 days') ->setMinDate('-7 days')
->setMaxDate('2012-12-31') ->setMaxDate('2012-12-31')
``` ```
## Formatting Hints ## Formatting Hints
@ -79,17 +77,17 @@ field description as an example.
```php ```php
$dateField = DateField::create('MyDate'); $dateField = DateField::create('MyDate');
// Show long format as text below the field // Show long format as text below the field
$dateField->setDescription(_t( $dateField->setDescription(_t(
'FormField.Example', 'FormField.Example',
'e.g. {format}', 'e.g. {format}',
[ 'format' => $dateField->getDateFormat() ] [ 'format' => $dateField->getDateFormat() ]
)); ));
// Alternatively, set short format as a placeholder in the field // Alternatively, set short format as a placeholder in the field
$dateField->setAttribute('placeholder', $dateField->getDateFormat()); $dateField->setAttribute('placeholder', $dateField->getDateFormat());
``` ```
<div class="notice" markdown="1"> <div class="notice" markdown="1">

View File

@ -17,25 +17,24 @@ functionality. It is usually added through the [DataObject::getCMSFields()](api:
```php ```php
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HTMLEditor\HTMLEditorField; use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyObject extends DataObject class MyObject extends DataObject
{
private static $db = [
'Content' => 'HTMLText'
];
public function getCMSFields()
{ {
return new FieldList(
private static $db = [ new HTMLEditorField('Content')
'Content' => 'HTMLText' );
];
public function getCMSFields()
{
return new FieldList(
new HTMLEditorField('Content')
);
}
} }
}
``` ```
### Specify which configuration to use ### Specify which configuration to use
@ -51,25 +50,25 @@ This is particularly useful if you need different configurations for multiple [H
```php ```php
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HTMLEditor\HTMLEditorField; use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyObject extends DataObject class MyObject extends DataObject
{
private static $db = [
'Content' => 'HTMLText',
'OtherContent' => 'HTMLText'
];
public function getCMSFields()
{ {
private static $db = [ return new FieldList([
'Content' => 'HTMLText', new HTMLEditorField('Content'),
'OtherContent' => 'HTMLText' new HTMLEditorField('OtherContent', 'Other content', $this->OtherContent, 'myConfig')
]; ]);
public function getCMSFields()
{
return new FieldList([
new HTMLEditorField('Content'),
new HTMLEditorField('OtherContent', 'Other content', $this->OtherContent, 'myConfig')
]);
}
} }
}
``` ```
@ -103,7 +102,9 @@ transparently generate the relevant underlying TinyMCE code.
**mysite/_config.php** **mysite/_config.php**
```php ```php
HtmlEditorConfig::get('cms')->enablePlugins('media'); use SilverStripe\Forms\HTMLEditor\HtmlEditorConfig;
HtmlEditorConfig::get('cms')->enablePlugins('media');
``` ```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
@ -118,7 +119,7 @@ configuration. Here is an example of adding a `ssmacron` button after the `charm
**mysite/_config.php** **mysite/_config.php**
```php ```php
HtmlEditorConfig::get('cms')->insertButtonsAfter('charmap', 'ssmacron'); HtmlEditorConfig::get('cms')->insertButtonsAfter('charmap', 'ssmacron');
``` ```
Buttons can also be removed: Buttons can also be removed:
@ -126,7 +127,7 @@ Buttons can also be removed:
**mysite/_config.php** **mysite/_config.php**
```php ```php
HtmlEditorConfig::get('cms')->removeButtons('tablecontrols', 'blockquote', 'hr'); HtmlEditorConfig::get('cms')->removeButtons('tablecontrols', 'blockquote', 'hr');
``` ```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
@ -147,18 +148,18 @@ from the HTML source by the editor.
**mysite/_config.php** **mysite/_config.php**
```php ```php
// Add start and type attributes for <ol>, add <object> and <embed> with all attributes. // Add start and type attributes for <ol>, add <object> and <embed> with all attributes.
HtmlEditorConfig::get('cms')->setOption( HtmlEditorConfig::get('cms')->setOption(
'extended_valid_elements', 'extended_valid_elements',
'img[class|src|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name|usemap],' . '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],' . 'iframe[src|name|width|height|title|align|allowfullscreen|frameborder|marginwidth|marginheight|scrolling],' .
'object[classid|codebase|width|height|data|type],' . 'object[classid|codebase|width|height|data|type],' .
'embed[src|type|pluginspage|width|height|autoplay],' . 'embed[src|type|pluginspage|width|height|autoplay],' .
'param[name|value],' . 'param[name|value],' .
'map[class|name|id],' . 'map[class|name|id],' .
'area[shape|coords|href|target|alt],' . 'area[shape|coords|href|target|alt],' .
'ol[start|type]' 'ol[start|type]'
); );
``` ```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
@ -174,8 +175,7 @@ You can enable them through [HtmlEditorConfig::enablePlugins()](api:SilverStripe
**mysite/_config.php** **mysite/_config.php**
```php ```php
HtmlEditorConfig::get('cms')->enablePlugins(['myplugin' => '../../../mysite/javascript/myplugin/editor_plugin.js']); 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. You can learn how to [create a plugin](http://www.tinymce.com/wiki.php/Creating_a_plugin) from the TinyMCE documentation.
@ -229,7 +229,7 @@ In case you want to adhere to HTML4 instead, use the following configuration:
```php ```php
HtmlEditorConfig::get('cms')->setOption('element_format', 'html'); 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 By default, TinyMCE and SilverStripe will generate valid HTML5 markup, but it will strip out HTML5 tags like
@ -254,23 +254,23 @@ Example: Remove field for "image captions"
```php ```php
use SilverStripe\Core\Extension; use SilverStripe\Core\Extension;
// File: mysite/code/MyToolbarExtension.php // File: mysite/code/MyToolbarExtension.php
class MyToolbarExtension extends Extension class MyToolbarExtension extends Extension
{
public function updateFieldsForImage(&$fields, $url, $file)
{ {
public function updateFieldsForImage(&$fields, $url, $file) $fields->removeByName('CaptionText');
{
$fields->removeByName('CaptionText');
}
} }
}
``` ```
```php ```php
// File: mysite/_config.php // File: mysite/_config.php
ModalController::add_extension('MyToolbarExtension'); use SilverStripe\Admin\ModalController;
ModalController::add_extension('MyToolbarExtension');
``` ```
Adding functionality is a bit more advanced, you'll most likely Adding functionality is a bit more advanced, you'll most likely
@ -298,28 +298,27 @@ of the CMS you have to take care of instantiate yourself:
```php ```php
use SilverStripe\Admin\ModalController; use SilverStripe\Admin\ModalController;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
// File: mysite/code/MyController.php // File: mysite/code/MyController.php
class MyObjectController extends Controller class MyObjectController extends Controller
{
public function Modals()
{ {
public function Modals() return ModalController::create($this, "Modals");
{
return ModalController::create($this, "Modals");
}
} }
}
``` ```
Note: The dialogs rely on CMS-access, e.g. for uploading and browsing files, Note: The dialogs rely on CMS-access, e.g. for uploading and browsing files,
so this is considered advanced usage of the field. so this is considered advanced usage of the field.
```php ```php
// File: mysite/_config.php // File: mysite/_config.php
HtmlEditorConfig::get('cms')->disablePlugins('ssbuttons'); HtmlEditorConfig::get('cms')->disablePlugins('ssbuttons');
HtmlEditorConfig::get('cms')->removeButtons('sslink', 'ssmedia'); HtmlEditorConfig::get('cms')->removeButtons('sslink', 'ssmedia');
HtmlEditorConfig::get('cms')->addButtonsToLine(2, 'link', 'media'); HtmlEditorConfig::get('cms')->addButtonsToLine(2, 'link', 'media');
``` ```
### Developing a wrapper to use a different WYSIWYG editors with HTMLEditorField ### Developing a wrapper to use a different WYSIWYG editors with HTMLEditorField
@ -330,4 +329,4 @@ mainly around selecting and inserting content into the editor view.
Have a look in `HtmlEditorField.js` and the `ss.editorWrapper` object to get you started Have a look in `HtmlEditorField.js` and the `ss.editorWrapper` object to get you started
on your own editor wrapper. Note that the [HtmlEditorConfig](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig) is currently hardwired to support TinyMCE, on your own editor wrapper. Note that the [HtmlEditorConfig](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig) is currently hardwired to support TinyMCE,
so its up to you to either convert existing configuration as applicable, so its up to you to either convert existing configuration as applicable,
or start your own configuration. or start your own configuration.

View File

@ -8,7 +8,9 @@ tabular data in a format that is easy to view and modify. It can be thought of a
```php ```php
$field = new GridField($name, $title, $list); use SilverStripe\Forms\GridField\GridField;
$field = new GridField($name, $title, $list);
``` ```
<div class="hint" markdown='1'> <div class="hint" markdown='1'>
@ -29,23 +31,23 @@ actions such as deleting records.
```php ```php
use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridField;
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
class Page extends SiteTree class Page extends SiteTree
{
public function getCMSFields()
{ {
$fields = parent::getCMSFields();
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Pages', $fields->addFieldToTab('Root.Pages',
new GridField('Pages', 'All pages', SiteTree::get()) new GridField('Pages', 'All pages', SiteTree::get())
); );
return $fields; return $fields;
}
} }
}
``` ```
This will display a bare bones `GridField` instance under `Pages` tab in the CMS. As we have not specified the This will display a bare bones `GridField` instance under `Pages` tab in the CMS. As we have not specified the
@ -65,82 +67,91 @@ the `getConfig()` method on `GridField`.
```php ```php
use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridField;
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
class Page extends SiteTree class Page extends SiteTree
{
public function getCMSFields()
{ {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Pages',
$grid = new GridField('Pages', 'All pages', SiteTree::get())
);
// GridField configuration
$config = $grid->getConfig();
//
// 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');
public function getCMSFields() $dataColumns->setDisplayFields([
{ 'Title' => 'Title',
$fields = parent::getCMSFields(); 'Link'=> 'URL',
'LastEdited' => 'Changed'
]);
$fields->addFieldToTab('Root.Pages', return $fields;
$grid = new GridField('Pages', 'All pages', SiteTree::get())
);
// GridField configuration
$config = $grid->getConfig();
//
// 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`. With the `GridFieldConfig` instance, we can modify the behavior of the `GridField`.
```php ```php
// `GridFieldConfig::create()` will create an empty configuration (no components). use SilverStripe\Forms\GridField\GridFieldConfig;
$config = GridFieldConfig::create(); use SilverStripe\Forms\GridField\GridFieldDataColumns;
// add a component // `GridFieldConfig::create()` will create an empty configuration (no components).
$config->addComponent(new GridFieldDataColumns()); $config = GridFieldConfig::create();
// Update the GridField with our custom configuration // add a component
$gridField->setConfig($config); $config->addComponent(new GridFieldDataColumns());
// 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 `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. before another component by passing the second parameter.
```php ```php
$config->addComponent(new GridFieldFilterHeader(), 'GridFieldDataColumns'); use SilverStripe\Forms\GridField\GridFieldFilterHeader;
use SilverStripe\Forms\GridField\GridFieldDataColumns;
$config->addComponent(new GridFieldFilterHeader(), GridFieldDataColumns::class);
``` ```
We can add multiple components in one call. We can add multiple components in one call.
```php ```php
$config->addComponents( $config->addComponents(
new GridFieldDataColumns(), new GridFieldDataColumns(),
new GridFieldToolbarHeader() new GridFieldToolbarHeader()
); );
``` ```
Or, remove a component. Or, remove a component.
```php ```php
$config->removeComponentsByType('GridFieldDeleteAction'); use SilverStripe\Forms\GridField\GridFieldDeleteAction;
$config->removeComponentsByType(GridFieldDeleteAction::class);
``` ```
Fetch a component to modify it later on. Fetch a component to modify it later on.
```php ```php
$component = $config->getComponentByType('GridFieldFilterHeader') use SilverStripe\Forms\GridField\GridFieldFilterHeader;
$component = $config->getComponentByType(GridFieldFilterHeader::class)
``` ```
Here is a list of components for use bundled with the core framework. Many more components are provided by third-party Here is a list of components for use bundled with the core framework. Many more components are provided by third-party
@ -169,17 +180,19 @@ A simple read-only and paginated view of records with sortable and searchable he
```php ```php
$config = GridFieldConfig_Base::create(); use SilverStripe\Forms\GridField\GridFieldConfig_Base;
$gridField->setConfig($config); $config = GridFieldConfig_Base::create();
// Is the same as adding the following components.. $gridField->setConfig($config);
// .. new GridFieldToolbarHeader()
// .. new GridFieldSortableHeader() // Is the same as adding the following components..
// .. new GridFieldFilterHeader() // .. new GridFieldToolbarHeader()
// .. new GridFieldDataColumns() // .. new GridFieldSortableHeader()
// .. new GridFieldPageCount('toolbar-header-right') // .. new GridFieldFilterHeader()
// .. new GridFieldPaginator($itemsPerPage) // .. new GridFieldDataColumns()
// .. new GridFieldPageCount('toolbar-header-right')
// .. new GridFieldPaginator($itemsPerPage)
``` ```
### GridFieldConfig_RecordViewer ### GridFieldConfig_RecordViewer
@ -199,13 +212,15 @@ this record.
```php ```php
$config = GridFieldConfig_RecordViewer::create(); use SilverStripe\Forms\GridField\GridFieldConfig_RecordViewer;
$gridField->setConfig($config);
// Same as GridFieldConfig_Base with the addition of $config = GridFieldConfig_RecordViewer::create();
// .. new GridFieldViewButton(),
// .. new GridFieldDetailForm() $gridField->setConfig($config);
// Same as GridFieldConfig_Base with the addition of
// .. new GridFieldViewButton(),
// .. new GridFieldDetailForm()
``` ```
### GridFieldConfig_RecordEditor ### GridFieldConfig_RecordEditor
@ -224,24 +239,25 @@ Permission control for editing and deleting the record uses the `canEdit()` and
```php ```php
$config = GridFieldConfig_RecordEditor::create(); $config = GridFieldConfig_RecordEditor::create();
$gridField->setConfig($config);
// Same as GridFieldConfig_RecordViewer with the addition of $gridField->setConfig($config);
// .. new GridFieldAddNewButton(),
// .. new GridFieldEditButton(), // Same as GridFieldConfig_RecordViewer with the addition of
// .. new GridFieldDeleteAction() // .. new GridFieldAddNewButton(),
// .. new GridFieldEditButton(),
// .. new GridFieldDeleteAction()
``` ```
### GridFieldConfig_RelationEditor ### GridFieldConfig_RelationEditor
Similar to `GridFieldConfig_RecordEditor`, but adds features to work on a record's has-many or many-many relationships. 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();
$gridField->setConfig($config); ```php
$config = GridFieldConfig_RelationEditor::create();
$gridField->setConfig($config);
``` ```
This configuration adds the ability to searched for existing records and add a relationship This configuration adds the ability to searched for existing records and add a relationship
@ -258,10 +274,12 @@ The `GridFieldDetailForm` component drives the record viewing and editing form.
```php ```php
$form = $gridField->getConfig()->getComponentByType('GridFieldDetailForm'); use SilverStripe\Forms\GridField\GridFieldDetailForm;
$form->setFields(new FieldList(
new TextField('Title') $form = $gridField->getConfig()->getComponentByType(GridFieldDetailForm::class);
)); $form->setFields(new FieldList(
new TextField('Title')
));
``` ```
### many_many_extraFields ### many_many_extraFields
@ -277,62 +295,61 @@ The namespace notation is `ManyMany[<extradata-field-name>]`, so for example `Ma
```php ```php
use SilverStripe\Forms\TextField; use SilverStripe\Forms\TextField;
use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor; use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Team extends DataObject class Team extends DataObject
{ {
private static $db = [
'Name' => 'Text'
];
public static $many_many = [
'Players' => 'Player'
];
}
class Player extends DataObject
{
private static $db = [ private static $db = [
'Name' => 'Text' 'Name' => 'Text'
]; ];
public static $many_many = [
'Teams' => 'Team'
];
public static $many_many_extraFields = [
'Teams' => [
'Position' => 'Text'
]
];
public function getCMSFields() public static $many_many = [
{ 'Players' => 'Player'
$fields = parent::getCMSFields(); ];
}
class Player extends DataObject
{
if($this->ID) { private static $db = [
$teamFields = singleton('Team')->getCMSFields(); 'Name' => 'Text'
$teamFields->addFieldToTab( ];
'Root.Main',
// The "ManyMany[<extradata-name>]" convention public static $many_many = [
new TextField('ManyMany[Position]', 'Current Position') 'Teams' => 'Team'
); ];
public static $many_many_extraFields = [
'Teams' => [
'Position' => 'Text'
]
];
$config = GridFieldConfig_RelationEditor::create(); public function getCMSFields()
$config->getComponentByType('GridFieldDetailForm')->setFields($teamFields); {
$fields = parent::getCMSFields();
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config); if($this->ID) {
$fields->findOrMakeTab('Root.Teams')->replaceField('Teams', $gridField); $teamFields = singleton('Team')->getCMSFields();
} $teamFields->addFieldToTab(
'Root.Main',
// The "ManyMany[<extradata-name>]" convention
new TextField('ManyMany[Position]', 'Current Position')
);
return $fields; $config = GridFieldConfig_RelationEditor::create();
$config->getComponentByType('GridFieldDetailForm')->setFields($teamFields);
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
$fields->findOrMakeTab('Root.Teams')->replaceField('Teams', $gridField);
} }
}
return $fields;
}
}
``` ```
## Flexible Area Assignment through Fragments ## Flexible Area Assignment through Fragments
@ -352,10 +369,12 @@ 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 [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. bottom right of the table.
```php ```php
$config->addComponent(new GridFieldButtonRow('after')); use SilverStripe\Forms\GridField\GridFieldButtonRow;
$config->addComponent(new GridFieldPrintButton('buttons-after-right')); use SilverStripe\Forms\GridField\GridFieldPrintButton;
$config->addComponent(new GridFieldButtonRow('after'));
$config->addComponent(new GridFieldPrintButton('buttons-after-right'));
``` ```
### Creating your own Fragments ### Creating your own Fragments
@ -366,16 +385,18 @@ create an area rendered before the table wrapped in a simple `<div>`.
```php ```php
class MyAreaComponent implements GridField_HTMLProvider use SilverStripe\Forms\GridField\GridField_HTMLProvider;
class MyAreaComponent implements GridField_HTMLProvider
{
public function getHTMLFragments( $gridField)
{ {
return [
public function getHTMLFragments( $gridField) 'before' => '<div class="my-area">$DefineFragment(my-area)</div>'
{ ];
return [
'before' => '<div class="my-area">$DefineFragment(my-area)</div>'
];
}
} }
}
``` ```
@ -389,16 +410,18 @@ Now you can add other components into this area by returning them as an array fr
```php ```php
class MyShareLinkComponent implements GridField_HTMLProvider use SilverStripe\Forms\GridField\GridField_HTMLProvider;
{
class MyShareLinkComponent implements GridField_HTMLProvider
public function getHTMLFragments( $gridField) {
{
return [ public function getHTMLFragments( $gridField)
'my-area' => '<a href>...</a>' {
]; return [
} 'my-area' => '<a href>...</a>'
];
} }
}
``` ```
@ -406,7 +429,7 @@ Your new area can also be used by existing components, e.g. the [GridFieldPrintB
```php ```php
new GridFieldPrintButton('my-component-area'); new GridFieldPrintButton('my-component-area');
``` ```
## Creating a Custom GridFieldComponent ## Creating a Custom GridFieldComponent

View File

@ -6,71 +6,69 @@ Form definitions can often get long, complex and often end up cluttering up a `C
to reuse the `Form` across multiple `Controller` classes rather than just one. A nice way to encapsulate the logic and to reuse the `Form` across multiple `Controller` classes rather than just one. A nice way to encapsulate the logic and
code for a `Form` is to create it as a subclass to `Form`. Let's look at a example of a `Form` which is on our code for a `Form` is to create it as a subclass to `Form`. Let's look at a example of a `Form` which is on our
`Controller` but would be better written as a subclass. `Controller` but would be better written as a subclass.
**mysite/code/Page.php** **mysite/code/Page.php**
```php ```php
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\HeaderField; use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\OptionsetField; use SilverStripe\Forms\OptionsetField;
use SilverStripe\Forms\CompositeField; use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\CheckboxSetField; use SilverStripe\Forms\CheckboxSetField;
use SilverStripe\Forms\NumericField; use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\CMS\Controllers\ContentController; use SilverStripe\CMS\Controllers\ContentController;
class PageController extends ContentController class PageController extends ContentController
{
public function SearchForm()
{ {
$fields = new FieldList(
public function SearchForm() HeaderField::create('Header', 'Step 1. Basics'),
{ OptionsetField::create('Type', '', [
$fields = new FieldList( 'foo' => 'Search Foo',
HeaderField::create('Header', 'Step 1. Basics'), 'bar' => 'Search Bar',
OptionsetField::create('Type', '', [ 'baz' => 'Search Baz'
'foo' => 'Search Foo', ]),
'bar' => 'Search Bar',
'baz' => 'Search Baz' CompositeField::create(
HeaderField::create('Header2', 'Step 2. Advanced '),
CheckboxSetField::create('Foo', 'Select Option', [
'qux' => 'Search Qux'
]), ]),
CompositeField::create( CheckboxSetField::create('Category', 'Category', [
HeaderField::create('Header2', 'Step 2. Advanced '), 'Foo' => 'Foo',
CheckboxSetField::create('Foo', 'Select Option', [ 'Bar' => 'Bar'
'qux' => 'Search Qux' ]),
]),
CheckboxSetField::create('Category', 'Category', [ NumericField::create('Minimum', 'Minimum'),
'Foo' => 'Foo', NumericField::create('Maximum', 'Maximum')
'Bar' => 'Bar' )
]), );
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; $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;
} }
}
``` ```
@ -79,72 +77,71 @@ should be. Good practice would be to move this to a subclass and create a new in
**mysite/code/forms/SearchForm.php** **mysite/code/forms/SearchForm.php**
```php ```php
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\HeaderField; use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\OptionsetField; use SilverStripe\Forms\OptionsetField;
use SilverStripe\Forms\CompositeField; use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\CheckboxSetField; use SilverStripe\Forms\CheckboxSetField;
use SilverStripe\Forms\NumericField; use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
class SearchForm extends Form class SearchForm extends 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', '', [
'foo' => 'Search Foo',
'bar' => 'Search Bar',
'baz' => 'Search Baz'
]),
/** CompositeField::create(
* Our constructor only requires the controller and the name of the form HeaderField::create('Header2', 'Step 2. Advanced '),
* method. We'll create the fields and actions in here. CheckboxSetField::create('Foo', 'Select Option', [
* 'qux' => 'Search Qux'
*/
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'
]), ]),
CompositeField::create( CheckboxSetField::create('Category', 'Category', [
HeaderField::create('Header2', 'Step 2. Advanced '), 'Foo' => 'Foo',
CheckboxSetField::create('Foo', 'Select Option', [ 'Bar' => 'Bar'
'qux' => 'Search Qux' ]),
]),
CheckboxSetField::create('Category', 'Category', [ NumericField::create('Minimum', 'Minimum'),
'Foo' => 'Foo', NumericField::create('Maximum', 'Maximum')
'Bar' => 'Bar' )
]), );
NumericField::create('Minimum', 'Minimum'),
NumericField::create('Maximum', 'Maximum')
)
);
$actions = new FieldList(
FormAction::create('doSearchForm', 'Search')
);
$required = new RequiredFields([
'Type'
]);
// 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'); $actions = new FieldList(
$this->disableSecurityToken(); FormAction::create('doSearchForm', 'Search')
$this->loadDataFrom($_REQUEST); );
}
$required = new RequiredFields([
'Type'
]);
// 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);
} }
}
``` ```
@ -154,22 +151,21 @@ Our controller will now just have to create a new instance of this form object.
```php ```php
use SearchForm; use SearchForm;
use SilverStripe\CMS\Controllers\ContentController; use SilverStripe\CMS\Controllers\ContentController;
class PageController extends ContentController class PageController extends ContentController
{
private static $allowed_actions = [
'SearchForm',
];
public function SearchForm()
{ {
return new SearchForm($this, 'SearchForm');
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. Form actions can also be defined within your `Form` subclass to keep the entire form logic encapsulated.

View File

@ -13,38 +13,36 @@ totally custom template to meet our needs. To do this, we'll provide the class w
```php ```php
public function SearchForm()
public function SearchForm() {
{ $fields = new FieldList(
$fields = new FieldList( TextField::create('q')
TextField::create('q') );
);
$actions = new FieldList( $actions = new FieldList(
FormAction::create('doSearch', 'Search') FormAction::create('doSearch', 'Search')
); );
$form = new Form($this, 'SearchForm', $fields, $actions); $form = new Form($this, 'SearchForm', $fields, $actions);
$form->setTemplate('SearchForm'); $form->setTemplate('SearchForm');
return $form; return $form;
} }
``` ```
**mysite/templates/Includes/SearchForm.ss** **mysite/templates/Includes/SearchForm.ss**
```ss ```ss
<form $FormAttributes>
<form $FormAttributes> <fieldset>
<fieldset> $Fields.dataFieldByName(q)
$Fields.dataFieldByName(q) </fieldset>
</fieldset>
<div class="Actions">
<div class="Actions"> <% loop $Actions %>$Field<% end_loop %>
<% loop $Actions %>$Field<% end_loop %> </div>
</div> </form>
</form>
``` ```
`SearchForm.ss` will be executed within the scope of the `Form` object so has access to any of the methods and `SearchForm.ss` will be executed within the scope of the `Form` object so has access to any of the methods and

View File

@ -19,67 +19,71 @@ below:
```php ```php
class GridFieldCustomAction implements GridField_ColumnProvider, GridField_ActionProvider use SilverStripe\Forms\GridField\GridField_ColumnProvider;
use SilverStripe\Forms\GridField\GridField_ActionProvider;
use SilverStripe\Forms\GridField\GridField_FormAction;
use SilverStripe\Control\Controller;
class GridFieldCustomAction implements GridField_ColumnProvider, GridField_ActionProvider
{
public function augmentColumns($gridField, &$columns)
{ {
if(!in_array('Actions', $columns)) {
public function augmentColumns($gridField, &$columns) $columns[] = 'Actions';
{
if(!in_array('Actions', $columns)) {
$columns[] = 'Actions';
}
}
public function getColumnAttributes($gridField, $record, $columnName)
{
return ['class' => 'grid-field__col-compact'];
}
public function getColumnMetadata($gridField, $columnName)
{
if($columnName == 'Actions') {
return ['title' => ''];
}
}
public function getColumnsHandled($gridField)
{
return ['Actions'];
}
public function getColumnContent($gridField, $record, $columnName)
{
if(!$record->canEdit()) return;
$field = GridField_FormAction::create(
$gridField,
'CustomAction'.$record->ID,
'Do Action',
"docustomaction",
['RecordID' => $record->ID]
);
return $field->Field();
}
public function getActions($gridField)
{
return ['docustomaction'];
}
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.'
);
}
} }
} }
public function getColumnAttributes($gridField, $record, $columnName)
{
return ['class' => 'grid-field__col-compact'];
}
public function getColumnMetadata($gridField, $columnName)
{
if($columnName == 'Actions') {
return ['title' => ''];
}
}
public function getColumnsHandled($gridField)
{
return ['Actions'];
}
public function getColumnContent($gridField, $record, $columnName)
{
if(!$record->canEdit()) return;
$field = GridField_FormAction::create(
$gridField,
'CustomAction'.$record->ID,
'Do Action',
"docustomaction",
['RecordID' => $record->ID]
);
return $field->Field();
}
public function getActions($gridField)
{
return ['docustomaction'];
}
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` ## Add the GridFieldCustomAction to the current `GridFieldConfig`
@ -91,14 +95,14 @@ manipulating the `GridFieldConfig` instance if required.
```php ```php
// option 1: creating a new GridField with the CustomAction // option 1: creating a new GridField with the CustomAction
$config = GridFieldConfig::create(); $config = GridFieldConfig::create();
$config->addComponent(new GridFieldCustomAction()); $config->addComponent(new GridFieldCustomAction());
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config); $gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
// option 2: adding the CustomAction to an exisitng GridField // option 2: adding the CustomAction to an exisitng GridField
$gridField->getConfig()->addComponent(new GridFieldCustomAction()); $gridField->getConfig()->addComponent(new GridFieldCustomAction());
``` ```
For documentation on adding a Component to a `GridField` created by `ModelAdmin` For documentation on adding a Component to a `GridField` created by `ModelAdmin`

View File

@ -6,34 +6,34 @@ Let's start by defining a new `ContactPage` page type:
```php ```php
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField; use SilverStripe\Forms\TextField;
use SilverStripe\Forms\EmailField; use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\TextareaField; use SilverStripe\Forms\TextareaField;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use Page; use Page;
use PageController; use PageController;
class ContactPage extends Page class ContactPage extends Page
{ {
} }
class ContactPageController extends PageController class ContactPageController extends PageController
{ {
private static $allowed_actions = ['Form']; private static $allowed_actions = ['Form'];
public function Form() public function Form()
{ {
$fields = new FieldList( $fields = new FieldList(
new TextField('Name'), new TextField('Name'),
new EmailField('Email'), new EmailField('Email'),
new TextareaField('Message') new TextareaField('Message')
); );
$actions = new FieldList( $actions = new FieldList(
new FormAction('submit', 'Submit') new FormAction('submit', 'Submit')
); );
return new Form($this, 'Form', $fields, $actions); return new Form($this, 'Form', $fields, $actions);
}
} }
}
``` ```
@ -43,27 +43,32 @@ There's quite a bit in this function, so we'll step through one piece at a time.
```php ```php
$fields = new FieldList( use SilverStripe\Forms\FieldList;
new TextField('Name'), use SilverStripe\Forms\TextField;
new EmailField('Email'), use SilverStripe\Forms\EmailField;
new TextareaField('Message') use SilverStripe\Forms\TextareaField;
);
$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. 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 ```php
$actions = FieldList( $actions = FieldList(
new FormAction('submit', 'Submit') 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. 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 ```php
return new Form($this, 'Form', $fields, $actions); 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. 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.
@ -82,36 +87,35 @@ Now that we have a contact form, we need some way of collecting the data submitt
```php ```php
use SilverStripe\Control\Email\Email; use SilverStripe\Control\Email\Email;
use PageController;
class ContactPageController extends PageController class ContactPageController extends PageController
{
private static $allowed_actions = ['Form'];
public function Form()
{ {
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' => ''
];
}
} }
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' => ''
];
}
}
``` ```
@ -137,13 +141,15 @@ The framework comes with a predefined validator called [RequiredFields](api:Silv
```php ```php
public function Form() use SilverStripe\Forms\Form;
{ use SilverStripe\Forms\RequiredFields;
// ...
$validator = new RequiredFields('Name', 'Message'); public function Form()
return new Form($this, 'Form', $fields, $actions, $validator); {
} // ...
$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. We've created a RequiredFields object, passing the name of the fields we want to be required. The validator we have created is then passed as the fifth argument of the form constructor. If we now try to submit the form without filling out the required fields, JavaScript validation will kick in, and the user will be presented with a message about the missing fields. If the user has JavaScript disabled, PHP validation will kick in when the form is submitted, and the user will be redirected back to the Form with messages about their missing fields.

View File

@ -29,23 +29,19 @@ be marked `private static` and follow the `lower_case_with_underscores` structur
```php ```php
use Page; class MyClass extends Page
{
class MyClass extends Page /**
{ * @config
*/
private static $option_one = true;
/** /**
* @config * @config
*/ */
private static $option_one = true; private static $option_two = [];
}
/**
* @config
*/
private static $option_two = [];
// ..
}
``` ```
@ -55,12 +51,13 @@ This can be done by calling the static method [Config::inst()](api:SilverStripe\
```php ```php
$config = Config::inst()->get('MyClass', 'property'); $config = Config::inst()->get('MyClass', 'property');
``` ```
Or through the `config()` object on the class. Or through the `config()` object on the class.
```php ```php
$config = $this->config()->get('property')'; $config = $this->config()->get('property')';
``` ```
Note that by default `Config::inst()` returns only an immutable version of config. Use `Config::modify()` Note that by default `Config::inst()` returns only an immutable version of config. Use `Config::modify()`
@ -86,45 +83,44 @@ To set those configuration options on our previously defined class we can define
```yml ```yml
MyClass:
MyClass: option_one: false
option_one: false option_two:
option_two: - Foo
- Foo - Bar
- Bar - Baz
- Baz
``` ```
To use those variables in your application code: To use those variables in your application code:
```php ```php
$me = new MyClass(); $me = new MyClass();
echo $me->config()->option_one; echo $me->config()->option_one;
// returns false // returns false
echo implode(', ', $me->config()->option_two); echo implode(', ', $me->config()->option_two);
// returns 'Foo, Bar, Baz' // returns 'Foo, Bar, Baz'
echo Config::inst()->get('MyClass', 'option_one'); echo Config::inst()->get('MyClass', 'option_one');
// returns false // returns false
echo implode(', ', Config::inst()->get('MyClass', 'option_two')); echo implode(', ', Config::inst()->get('MyClass', 'option_two'));
// returns 'Foo, Bar, Baz' // returns 'Foo, Bar, Baz'
Config::modify()->set('MyClass', 'option_one', true); Config::modify()->set('MyClass', 'option_one', true);
echo Config::inst()->get('MyClass', 'option_one'); echo Config::inst()->get('MyClass', 'option_one');
// returns true // returns true
// You can also use the static version // You can also use the static version
MyClass::config()->option_two = [ MyClass::config()->option_two = [
'Qux' 'Qux'
]; ];
echo implode(', ', MyClass::config()->option_one); echo implode(', ', MyClass::config()->option_one);
// returns 'Qux' // returns 'Qux'
``` ```
@ -174,9 +170,11 @@ be raised due to optimizations in the lookup code.
At some of these levels you can also set masks. These remove values from the composite value at their priority point At some of these levels you can also set masks. These remove values from the composite value at their priority point
rather than add. rather than add.
$actionsWithoutExtra = $this->config()->get( ```php
'allowed_actions', Config::UNINHERITED $actionsWithoutExtra = $this->config()->get(
); 'allowed_actions', Config::UNINHERITED
);
```
Available masks include: Available masks include:
@ -202,18 +200,18 @@ The name of the files within the applications `_config` directly are arbitrary.
</div> </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: Name: adminroutes
- '#rootroutes' After:
- '#coreroutes' - '#rootroutes'
--- - '#coreroutes'
Director: ---
rules: SilverStripe\Control\Director:
'admin': 'AdminRootController' rules:
--- 'admin': 'SilverStripe\Admin\AdminRootController'
---
``` ```
<div class="info"> <div class="info">
@ -253,17 +251,16 @@ keys is a list of reference paths to other value sections. A basic example:
```yml ```yml
---
--- Name: adminroutes
Name: adminroutes After:
After: - '#rootroutes'
- '#rootroutes' - '#coreroutes'
- '#coreroutes' ---
--- SilverStripe\Control\Director:
Director: rules:
rules: 'admin': 'SilverStripe\Admin\AdminRootController'
'admin': 'AdminRootController' ---
---
``` ```
You do not have to specify all portions of a reference path. Any portion may be replaced with a wildcard "\*", or left You do not have to specify all portions of a reference path. Any portion may be replaced with a wildcard "\*", or left
@ -319,33 +316,30 @@ For instance, to add a property to "foo" when a module exists, and "bar" otherwi
```yml ```yml
---
--- Only:
Only: moduleexists: 'MyFineModule'
moduleexists: 'MyFineModule' ---
--- MyClass:
MyClass: property: 'foo'
property: 'foo' ---
--- Except:
Except: moduleexists: 'MyFineModule'
moduleexists: 'MyFineModule' ---
--- MyClass:
MyClass: property: 'bar'
property: 'bar' ---
---
``` ```
Multiple conditions of the same type can be declared via array format Multiple conditions of the same type can be declared via array format
```yaml ```yaml
---
--- Only:
Only: moduleexists:
moduleexists: - 'silverstripe/blog'
- 'silverstripe/blog' - 'silverstripe/lumberjack'
- 'silverstripe/lumberjack' ---
``` ```
<div class="alert" markdown="1"> <div class="alert" markdown="1">

View File

@ -12,24 +12,23 @@ throughout the site. Out of the box this includes selecting the current site the
```ss ```ss
$SiteConfig.Title
$SiteConfig.Tagline
$SiteConfig.Title <% with $SiteConfig %>
$SiteConfig.Tagline $Title $AnotherField
<% end_with %>
<% with $SiteConfig %>
$Title $AnotherField
<% end_with %>
``` ```
To access variables in the PHP: To access variables in the PHP:
```php ```php
$config = SiteConfig::current_site_config(); $config = SiteConfig::current_site_config();
echo $config->Title;
// returns "Website Name" echo $config->Title;
// returns "Website Name"
``` ```
## Extending SiteConfig ## Extending SiteConfig
@ -40,25 +39,24 @@ To extend the options available in the panel, define your own fields via a [Data
```php ```php
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HTMLEditor\HTMLEditorField; use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataExtension;
class CustomSiteConfig extends DataExtension class CustomSiteConfig extends DataExtension
{ {
private static $db = [
'FooterContent' => 'HTMLText'
];
public function updateCMSFields(FieldList $fields) private static $db = [
{ 'FooterContent' => 'HTMLText'
$fields->addFieldToTab("Root.Main", ];
new HTMLEditorField("FooterContent", "Footer Content")
);
}
}
public function updateCMSFields(FieldList $fields)
{
$fields->addFieldToTab("Root.Main",
new HTMLEditorField("FooterContent", "Footer Content")
);
}
}
``` ```
Then activate the extension. Then activate the extension.
@ -67,10 +65,9 @@ Then activate the extension.
```yml ```yml
Silverstripe\SiteConfig\SiteConfig:
Silverstripe\SiteConfig\SiteConfig: extensions:
extensions: - CustomSiteConfig
- CustomSiteConfig
``` ```
<div class="notice" markdown="1"> <div class="notice" markdown="1">

View File

@ -19,22 +19,21 @@ and `RequestHandler`. You can still apply extensions to descendants of these cla
```php ```php
use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataExtension;
class MyMemberExtension extends DataExtension 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`.
private static $db = [ return "Hi " . $this->owner->Name;
'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"> <div class="info" markdown="1">
@ -48,17 +47,16 @@ we want to add the `MyMemberExtension` too. To activate this extension, add the
```yml ```yml
SilverStripe\Security\Member:
Member: extensions:
extensions: - MyMemberExtension
- MyMemberExtension
``` ```
Alternatively, we can add extensions through PHP code (in the `_config.php` file). Alternatively, we can add extensions through PHP code (in the `_config.php` file).
```php ```php
Member::add_extension('MyMemberExtension'); SilverStripe\Security\Member::add_extension('MyMemberExtension');
``` ```
This class now defines a `MyMemberExtension` that applies to all `Member` instances on the website. It will have This class now defines a `MyMemberExtension` that applies to all `Member` instances on the website. It will have
@ -82,35 +80,33 @@ $has_one etc.
```php ```php
use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataExtension;
class MyMemberExtension extends DataExtension class MyMemberExtension extends DataExtension
{
private static $db = [
'Position' => 'Varchar',
];
private static $has_one = [
'Image' => 'Image',
];
public function SayHi()
{ {
// $this->owner refers to the original instance. In this case a `Member`.
private static $db = [ return "Hi " . $this->owner->Name;
'Position' => 'Varchar',
];
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** **mysite/templates/Page.ss**
```ss ```ss
$CurrentMember.Position
$CurrentMember.Position $CurrentMember.Image
$CurrentMember.Image
``` ```
## Adding Methods ## Adding Methods
@ -121,19 +117,19 @@ we added a `SayHi` method which is unique to our extension.
**mysite/templates/Page.ss** **mysite/templates/Page.ss**
```ss ```ss
<p>$CurrentMember.SayHi</p>
<p>$CurrentMember.SayHi</p> // "Hi Sam"
// "Hi Sam"
``` ```
**mysite/code/Page.php** **mysite/code/Page.php**
```php ```php
$member = Security::getCurrentUser(); use SilverStripe\Security\Security;
echo $member->SayHi;
// "Hi Sam" $member = Security::getCurrentUser();
echo $member->SayHi;
// "Hi Sam"
``` ```
## Modifying Existing Methods ## Modifying Existing Methods
@ -146,14 +142,14 @@ through the `extend()` method of the [Extensible](api:SilverStripe\Core\Extensib
```php ```php
public function getValidator() public function getValidator()
{ {
// .. // ..
$this->extend('updateValidator', $validator); $this->extend('updateValidator', $validator);
// .. // ..
} }
``` ```
Extension Hooks can be located anywhere in the method and provide a point for any `Extension` instances to modify the Extension Hooks can be located anywhere in the method and provide a point for any `Extension` instances to modify the
@ -165,19 +161,16 @@ validator by defining the `updateValidator` method.
```php ```php
use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataExtension;
class MyMemberExtension extends 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"> <div class="info" markdown="1">
@ -189,29 +182,28 @@ extension. The `CMS` provides a `updateCMSFields` Extension Hook to tie into.
```php ```php
use SilverStripe\Forms\TextField; use SilverStripe\Forms\TextField;
use SilverStripe\AssetAdmin\Forms\UploadField; use SilverStripe\AssetAdmin\Forms\UploadField;
use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataExtension;
class MyMemberExtension extends DataExtension class MyMemberExtension extends DataExtension
{
private static $db = [
'Position' => 'Varchar',
];
private static $has_one = [
'Image' => 'Image',
];
public function updateCMSFields(FieldList $fields)
{ {
$fields->push(new TextField('Position'));
private static $db = [ $fields->push($upload = new UploadField('Image', 'Profile Image'));
'Position' => 'Varchar', $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"> <div class="notice" markdown="1">
@ -221,14 +213,14 @@ which allows an Extension to modify the results.
```php ```php
public function Foo() public function Foo()
{ {
$foo = // .. $foo = // ..
$this->extend('updateFoo', $foo); $this->extend('updateFoo', $foo);
return $foo; return $foo;
} }
``` ```
The convention for extension hooks is to provide an `update{$Function}` hook at the end before you return the result. If The convention for extension hooks is to provide an `update{$Function}` hook at the end before you return the result. If
@ -241,31 +233,31 @@ In your [Extension](api:SilverStripe\Core\Extension) class you can only refer to
```php ```php
use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataExtension;
class MyMemberExtension extends DataExtension class MyMemberExtension extends DataExtension
{
public function updateFoo($foo)
{ {
// outputs the original class
public function updateFoo($foo) var_dump($this->owner);
{
// outputs the original class
var_dump($this->owner);
}
} }
}
``` ```
## Checking to see if an Object has an Extension ## Checking to see if an Object has an Extension
To see what extensions are currently enabled on an object, use the [getExtensionInstances()](api:SilverStripe\Core\Extensible::getExtensionInstances()) and 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. [hasExtension()](api:SilverStripe\Core\Extensible::hasExtension()) methods of the [Extensible](api:SilverStripe\Core\Extensible) trait.
```php
$member = Security::getCurrentUser();
print_r($member->getExtensionInstances()); ```php
$member = Security::getCurrentUser();
if($member->hasExtension('MyCustomMemberExtension')) {
// .. print_r($member->getExtensionInstances());
}
if($member->hasExtension('MyCustomMemberExtension')) {
// ..
}
``` ```
## Extension injection points ## Extension injection points
@ -286,17 +278,16 @@ if not specified in `self::$defaults`, but before extensions have been called:
```php ```php
function __construct() { public function __construct()
$self = $this; {
$this->beforeExtending('populateDefaults', function() {
if(empty($this->MyField)) {
$this->MyField = 'Value we want as a default if not specified in $defaults, but set before extensions';
}
});
$this->beforeExtending('populateDefaults', function() use ($self) { parent::__construct();
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. Example 2: User code can intervene in the process of extending cms fields.
@ -307,18 +298,17 @@ This method is preferred to disabling, enabling, and calling field extensions ma
```php ```php
public function getCMSFields() 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));
});
$this->beforeUpdateCMSFields(function($fields) { $fields = parent::getCMSFields();
// Include field which must be present when updateCMSFields is called on extensions // ... additional fields here
$fields->addFieldToTab("Root.Main", new TextField('Detail', 'Details', null, 255)); return $fields;
}); }
$fields = parent::getCMSFields();
// ... additional fields here
return $fields;
}
``` ```
## Related Documentaion ## Related Documentaion

View File

@ -12,22 +12,22 @@ in their WYSIWYG editor. Shortcodes are a semi-technical solution for this. A go
viewer or a Google Map at a certain location. viewer or a Google Map at a certain location.
```php ```php
$text = "<h1>My Map</h1>[map]" $text = "<h1>My Map</h1>[map]"
// Will output // Will output
// <h1>My Map</h1><iframe ..></iframe> // <h1>My Map</h1><iframe ..></iframe>
``` ```
Here's some syntax variations: Here's some syntax variations:
```php ```php
[my_shortcode] [my_shortcode]
# #
[my_shortcode /] [my_shortcode /]
# #
[my_shortcode,myparameter="value"] [my_shortcode,myparameter="value"]
# #
[my_shortcode,myparameter="value"]Enclosed Content[/my_shortcode] [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), 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),
@ -38,8 +38,10 @@ Other fields can be manually parsed with shortcodes through the `parse` method.
```php ```php
$text = "My awesome [my_shortcode] is here."; use SilverStripe\View\Parsers\ShortcodeParser;
ShortcodeParser::get_active()->parse($text);
$text = "My awesome [my_shortcode] is here.";
ShortcodeParser::get_active()->parse($text);
``` ```
## Defining Custom Shortcodes ## Defining Custom Shortcodes
@ -50,21 +52,19 @@ First we need to define a callback for the shortcode.
```php ```php
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
class Page extends SiteTree 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.";
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: These parameters are passed to the `MyShortCodeMethod` callback:
@ -85,10 +85,9 @@ To register a shortcode you call the following.
```php ```php
// ShortcodeParser::get('default')->register($shortcode, $callback); // ShortcodeParser::get('default')->register($shortcode, $callback);
ShortcodeParser::get('default')->register('my_shortcode', ['Page', 'MyShortCodeMethod']);
ShortcodeParser::get('default')->register('my_shortcode', ['Page', 'MyShortCodeMethod']);
``` ```
## Built-in Shortcodes ## Built-in Shortcodes
@ -103,14 +102,14 @@ shortcode, which takes an `id` parameter.
```php ```php
<a href="[sitetree_link,id=99]"> <a href="[sitetree_link,id=99]">
``` ```
Links to internal `File` database records work exactly the same, but with the `[file_link]` shortcode. Links to internal `File` database records work exactly the same, but with the `[file_link]` shortcode.
```php ```php
<a href="[file_link,id=99]"> <a href="[file_link,id=99]">
``` ```
### Images ### Images
@ -119,7 +118,9 @@ Images inserted through the "Insert Media" form (WYSIWYG editor) need to retain
the underlying `[Image](api:SilverStripe\Assets\Image)` database record. The `[image]` shortcode saves this database reference the underlying `[Image](api:SilverStripe\Assets\Image)` database record. The `[image]` shortcode saves this database reference
instead of hard-linking to the filesystem path of a given image. instead of hard-linking to the filesystem path of a given image.
[image id="99" alt="My text"] ```html
[image id="99" alt="My text"]
```
### Media (Photo, Video and Rich Content) ### Media (Photo, Video and Rich Content)
@ -130,11 +131,11 @@ Youtube link pasted into the "Insert Media" form of the CMS.
Since TinyMCE can't represent all these variations, we're showing a placeholder instead, and storing the URL with a Since TinyMCE can't represent all these variations, we're showing a placeholder instead, and storing the URL with a
custom `[embed]` shortcode. custom `[embed]` shortcode.
```html
[embed width=480 height=270 class=left thumbnail=http://i1.ytimg.com/vi/lmWeD-vZAMY/hqdefault.jpg?r=8767] [embed width=480 height=270 class=left thumbnail=http://i1.ytimg.com/vi/lmWeD-vZAMY/hqdefault.jpg?r=8767]
http://www.youtube.com/watch?v=lmWeD-vZAMY http://www.youtube.com/watch?v=lmWeD-vZAMY
[/embed] [/embed]
```
### Attribute and element scope ### Attribute and element scope
@ -147,84 +148,97 @@ 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 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: change the name of a tag. These usages are forbidden:
```ss
<[paragraph]>Some test</[paragraph]>
<a [titleattribute]>link</a> ```html
<[paragraph]>Some test</[paragraph]>
<a [titleattribute]>link</a>
``` ```
You may need to escape text inside attributes `>` becomes `&gt;`, You can include HTML tags inside a shortcode tag, but You may need to escape text inside attributes `>` becomes `&gt;`, 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. you need to be careful of nesting to ensure you don't break the output.
```ss ```html
<!-- Good -->
<div>
[shortcode]
<p>Caption</p>
[/shortcode]
</div>
<!-- Good --> <!-- Bad: -->
<div>
[shortcode]
<p>Caption</p>
[/shortcode]
</div>
<!-- Bad: --> <div>
[shortcode]
<div> </div>
[shortcode] <p>
</div> [/shortcode]
<p> </p>
[/shortcode]
</p>
``` ```
### Location ### Location
Element scoped shortcodes have a special ability to move the location they are inserted at to comply with HTML lexical 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: rules. Take for example this basic paragraph tag:
```ss
<p><a href="#">Head [figure,src="assets/a.jpg",caption="caption"] Tail</a></p> ```html
<p><a href="#">Head [figure,src="assets/a.jpg",caption="caption"] Tail</a></p>
``` ```
When converted naively would become: When converted naively would become:
```ss
<p><a href="#">Head <figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure> Tail</a></p> ```html
<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. 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" 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: the inserted content will be moved to immediately before the block tag. The result is this:
```ss
<figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure><p><a href="#">Head Tail</a></p> ```html
<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: When the location attribute is "leftAlone" or "center" then the DOM is split around the element. The result is this:
```ss
<p><a href="#">Head </a></p><figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure><p><a href="#"> Tail</a></p> ```html
<p><a href="#">Head </a></p><figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure><p><a href="#"> Tail</a></p>
``` ```
### Parameter values ### Parameter values
Here is a summary of the callback parameter values based on some example shortcodes. Here is a summary of the callback parameter values based on some example shortcodes.
```php ```php
public function MyCustomShortCode($arguments, $content = null, $parser = null, $tagName) public function MyCustomShortCode($arguments, $content = null, $parser = null, $tagName)
{ {
// .. // ..
} }
```
[my_shortcode] ```
$attributes => []; [my_shortcode]
$content => null; $attributes => [];
$parser => ShortcodeParser instance, $content => null;
$tagName => 'my_shortcode') $parser => ShortcodeParser instance,
$tagName => 'my_shortcode')
```
[my_shortcode,attribute="foo",other="bar"] ```
[my_shortcode,attribute="foo",other="bar"]
$attributes => ['attribute' => 'foo', 'other' => 'bar'] $attributes => ['attribute' => 'foo', 'other' => 'bar']
$enclosedContent => null $enclosedContent => null
$parser => ShortcodeParser instance $parser => ShortcodeParser instance
$tagName => 'my_shortcode' $tagName => 'my_shortcode'
```
[my_shortcode,attribute="foo"]content[/my_shortcode]
$attributes => ['attribute' => 'foo']
$enclosedContent => 'content'
$parser => ShortcodeParser instance
$tagName => 'my_shortcode'
```
[my_shortcode,attribute="foo"]content[/my_shortcode]
$attributes => ['attribute' => 'foo']
$enclosedContent => 'content'
$parser => ShortcodeParser instance
$tagName => 'my_shortcode'
``` ```
## Limitations ## Limitations
@ -232,9 +246,11 @@ Here is a summary of the callback parameter values based on some example shortco
Since the shortcode parser is based on a simple regular expression it cannot properly handle nested shortcodes. For Since the shortcode parser is based on a simple regular expression it cannot properly handle nested shortcodes. For
example the below code will not work as expected: example the below code will not work as expected:
[shortcode] ```html
[shortcode][/shortcode] [shortcode]
[/shortcode] [shortcode][/shortcode]
[/shortcode]
```
The parser will raise an error if it can not find a matching opening tag for any particular closing tag The parser will raise an error if it can not find a matching opening tag for any particular closing tag

View File

@ -20,31 +20,32 @@ The following sums up the simplest usage of the `Injector` it creates a new obje
```php ```php
$object = Injector::inst()->create('MyClassName'); use SilverStripe\Core\Injector\Injector;
$object = Injector::inst()->create('MyClassName');
``` ```
The benefit of constructing objects through this syntax is `ClassName` can be swapped out using the The benefit of constructing objects through this syntax is `ClassName` can be swapped out using the
[Configuration API](../configuration) by developers. [Configuration API](../configuration) by developers.
**mysite/_config/app.yml** **mysite/_config/app.yml**
```yml ```yml
SilverStripe\Core\Injector\Injector:
Injector: MyClassName:
MyClassName: class: MyBetterClassName
class: MyBetterClassName
``` ```
Repeated calls to `create()` create a new object each time. Repeated calls to `create()` create a new object each time.
```php ```php
$object = Injector::inst()->create('MyClassName'); $object = Injector::inst()->create('MyClassName');
$object2 = Injector::inst()->create('MyClassName'); $object2 = Injector::inst()->create('MyClassName');
echo $object !== $object2; echo $object !== $object2;
// returns true; // returns true;
``` ```
## Singleton Pattern ## Singleton Pattern
@ -54,13 +55,13 @@ object instance as the first call.
```php ```php
// sets up MyClassName as a singleton // sets up MyClassName as a singleton
$object = Injector::inst()->get('MyClassName'); $object = Injector::inst()->get('MyClassName');
$object2 = Injector::inst()->get('MyClassName'); $object2 = Injector::inst()->get('MyClassName');
echo ($object === $object2); echo ($object === $object2);
// returns true; // returns true;
``` ```
## Dependencies ## Dependencies
@ -69,83 +70,78 @@ The `Injector` API can be used to define the types of `$dependencies` that an ob
```php ```php
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends 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',
];
}
// 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. When creating a new instance of `MyController` the dependencies on that class will be met.
```php ```php
$object = Injector::inst()->get('MyController'); $object = Injector::inst()->get('MyController');
echo ($object->permissions instanceof PermissionService);
// returns true;
echo (is_string($object->textProperty)); echo ($object->permissions instanceof PermissionService);
// returns true; // returns true;
echo (is_string($object->textProperty));
// returns true;
``` ```
The [Configuration YAML](../configuration) does the hard work of configuring those `$dependencies` for us. The [Configuration YAML](../configuration) does the hard work of configuring those `$dependencies` for us.
**mysite/_config/app.yml** **mysite/_config/app.yml**
```yml ```yml
Injector:
Injector: PermissionService:
PermissionService: class: MyCustomPermissionService
class: MyCustomPermissionService MyController
MyController properties:
properties: textProperty: 'My Text Value'
textProperty: 'My Text Value'
``` ```
Now the dependencies will be replaced with our configuration. Now the dependencies will be replaced with our configuration.
```php ```php
$object = Injector::inst()->get('MyController'); $object = Injector::inst()->get('MyController');
echo ($object->permissions instanceof MyCustomPermissionService);
// returns true;
echo ($object->textProperty == 'My Text Value'); echo ($object->permissions instanceof MyCustomPermissionService);
// returns true; // returns true;
echo ($object->textProperty == 'My Text Value');
// returns true;
``` ```
As well as properties, method calls can also be specified: As well as properties, method calls can also be specified:
```yml ```yml
SilverStripe\Core\Injector\Injector:
Injector: Logger:
Logger: class: Monolog\Logger
class: Monolog\Logger calls:
calls: - [ pushHandler, [ %$DefaultHandler ] ]
- [ pushHandler, [ %$DefaultHandler ] ]
``` ```
## Using constants as variables ## Using constants as variables
Any of the core constants can be used as a service argument by quoting with back ticks "`". Please ensure you also quote the entire value (see below). Any of the core constants can be used as a service argument by quoting with back ticks "`". Please ensure you also quote the entire value (see below).
```yaml ```yaml
CachingService: CachingService:
class: SilverStripe\Cache\CacheProvider class: SilverStripe\Cache\CacheProvider
@ -166,30 +162,27 @@ An example using the `MyFactory` service to create instances of the `MyService`
**mysite/_config/app.yml** **mysite/_config/app.yml**
```yml ```yml
SilverStripe\Core\Injector\Injector:
Injector: MyService:
MyService: factory: MyFactory
factory: MyFactory
``` ```
**mysite/code/MyFactory.php** **mysite/code/MyFactory.php**
```php ```php
class MyFactory implements SilverStripe\Core\Injector\Factory class MyFactory implements SilverStripe\Core\Injector\Factory
{
public function create($service, array $params = [])
{ {
return new MyServiceImplementation();
public function create($service, array $params = [])
{
return new MyServiceImplementation();
}
} }
}
// Will use MyFactoryImplementation::create() to create the service instance. // Will use MyFactoryImplementation::create() to create the service instance.
$instance = Injector::inst()->get('MyService'); $instance = Injector::inst()->get('MyService');
``` ```
## Dependency overrides ## Dependency overrides
@ -197,12 +190,14 @@ An example using the `MyFactory` service to create instances of the `MyService`
To override the `$dependency` declaration for a class, define the following configuration file. To override the `$dependency` declaration for a class, define the following configuration file.
**mysite/_config/app.yml** **mysite/_config/app.yml**
```yml ```yml
MyController: MyController:
dependencies: dependencies:
textProperty: a string value textProperty: a string value
permissions: %$PermissionService permissions: %$PermissionService
``` ```
## Managed objects ## Managed objects
Simple dependencies can be specified by the `$dependencies`, but more complex configurations are possible by specifying Simple dependencies can be specified by the `$dependencies`, but more complex configurations are possible by specifying
@ -213,57 +208,55 @@ runtime.
Assuming a class structure such as Assuming a class structure such as
```php ```php
class RestrictivePermissionService class RestrictivePermissionService
{ {
private $database; private $database;
public function setDatabase($d) public function setDatabase($d)
{ {
$this->database = $d; $this->database = $d;
}
} }
class MySQLDatabase }
class MySQLDatabase
{
private $username;
private $password;
public function __construct($username, $password)
{ {
private $username; $this->username = $username;
private $password; $this->password = $password;
public function __construct($username, $password)
{
$this->username = $username;
$this->password = $password;
}
} }
}
``` ```
And the following configuration.. And the following configuration..
```yml ```yml
---
name: MyController name: MyController
--- ---
MyController: MyController:
dependencies: dependencies:
permissions: %$PermissionService permissions: %$PermissionService
Injector: SilverStripe\Core\Injector\Injector:
PermissionService: PermissionService:
class: RestrictivePermissionService class: RestrictivePermissionService
properties: properties:
database: %$MySQLDatabase database: %$MySQLDatabase
MySQLDatabase MySQLDatabase
constructor: constructor:
0: 'dbusername' 0: 'dbusername'
1: 'dbpassword' 1: 'dbpassword'
``` ```
Calling.. Calling..
```php ```php
// sets up ClassName as a singleton // sets up ClassName as a singleton
$controller = Injector::inst()->get('MyController'); $controller = Injector::inst()->get('MyController');
``` ```
Would setup the following Would setup the following
@ -283,12 +276,12 @@ Thus if you want an object to have the injected dependencies of a service of ano
assign a reference to that service. assign a reference to that service.
```yaml ```yaml
Injector: SilverStripe\Core\Injector\Injector:
JSONServiceDefinition: JSONServiceDefinition:
class: JSONServiceImplementor class: JSONServiceImplementor
properties: properties:
Serialiser: JSONSerialiser Serialiser: JSONSerialiser
GZIPJSONProvider: %$JSONServiceDefinition GZIPJSONProvider: %$JSONServiceDefinition
``` ```
`Injector::inst()->get('GZIPJSONProvider')` will then be an instance of `JSONServiceImplementor` with the injected `Injector::inst()->get('GZIPJSONProvider')` will then be an instance of `JSONServiceImplementor` with the injected
@ -300,11 +293,11 @@ If class is not specified, then the class will be inherited from the outer servi
For example with this config: For example with this config:
```yml ```yml
Injector: SilverStripe\Core\Injector\Injector:
Connector: Connector:
properties: properties:
AsString: true AsString: true
ServiceConnector: %$Connector ServiceConnector: %$Connector
``` ```
Both `Connector` and `ServiceConnector` will have the `AsString` property set to true, but the resulting Both `Connector` and `ServiceConnector` will have the `AsString` property set to true, but the resulting
@ -319,18 +312,20 @@ This is useful when writing test cases, as certain services may be necessary to
```php ```php
// Setup default service use SilverStripe\Core\Injector\Injector;
Injector::inst()->registerService(new LiveService(), 'ServiceName');
// Test substitute service temporarily // Setup default service
Injector::nest(); Injector::inst()->registerService(new LiveService(), 'ServiceName');
Injector::inst()->registerService(new TestingService(), 'ServiceName'); // Test substitute service temporarily
$service = Injector::inst()->get('ServiceName'); Injector::nest();
// ... do something with $service
// revert changes Injector::inst()->registerService(new TestingService(), 'ServiceName');
Injector::unnest(); $service = Injector::inst()->get('ServiceName');
// ... do something with $service
// revert changes
Injector::unnest();
``` ```
## API Documentation ## API Documentation

View File

@ -47,32 +47,31 @@ used.
```php ```php
class MySQLWriteDbAspect implements BeforeCallAspect class MySQLWriteDbAspect implements BeforeCallAspect
{
/**
* @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;
/** if (in_array(strtolower(substr($sql,0,strpos($sql,' '))), $this->writeQueries)) {
* @var MySQLDatabase $alternateReturn = $this->writeDb->query($sql, $code);
*/ return false;
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;
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` To actually make use of this class, a few different objects need to be configured. First up, define the `writeDb`
@ -82,15 +81,15 @@ object that's made use of above.
```yml ```yml
SilverStripe\Core\Injector\Injector:
WriteMySQLDatabase: WriteMySQLDatabase:
class: MySQLDatabase class: MySQLDatabase
constructor: constructor:
- type: MySQLDatabase - type: MySQLDatabase
server: write.hostname.db server: write.hostname.db
username: user username: user
password: pass password: pass
database: write_database 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 This means that whenever something asks the [Injector](api:SilverStripe\Core\Injector\Injector) for the `WriteMySQLDatabase` object, it'll receive an object
@ -100,23 +99,23 @@ Next, this should be bound into an instance of the `Aspect` class
**mysite/_config/app.yml** **mysite/_config/app.yml**
```yml ```yml
SilverStripe\Core\Injector\Injector:
MySQLWriteDbAspect: MySQLWriteDbAspect:
properties: properties:
writeDb: %$WriteMySQLDatabase writeDb: %$WriteMySQLDatabase
``` ```
Next, we need to define the database connection that will be used for all non-write queries Next, we need to define the database connection that will be used for all non-write queries
**mysite/_config/app.yml** **mysite/_config/app.yml**
```yml
ReadMySQLDatabase: ```yml
class: MySQLDatabase SilverStripe\Core\Injector\Injector:
constructor: ReadMySQLDatabase:
- type: MySQLDatabase class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: slavecluster.hostname.db server: slavecluster.hostname.db
username: user username: user
password: pass password: pass
@ -127,15 +126,16 @@ The final piece that ties everything together is the [AopProxyService](api:Silve
object when the framework creates the database connection. object when the framework creates the database connection.
**mysite/_config/app.yml** **mysite/_config/app.yml**
```yml
MySQLDatabase: ```yml
class: AopProxyService SilverStripe\Core\Injector\Injector:
properties: MySQLDatabase:
proxied: %$ReadMySQLDatabase class: AopProxyService
beforeCall: properties:
query: proxied: %$ReadMySQLDatabase
- %$MySQLWriteDbAspect beforeCall:
query:
- %$MySQLWriteDbAspect
``` ```
The two important parts here are in the `properties` declared for the object. The two important parts here are in the `properties` declared for the object.
@ -147,35 +147,35 @@ defined method\_name
Overall configuration for this would look as follows Overall configuration for this would look as follows
**mysite/_config/app.yml** **mysite/_config/app.yml**
```yml
Injector: ```yml
ReadMySQLDatabase: SilverStripe\Core\Injector\Injector:
class: MySQLDatabase ReadMySQLDatabase:
constructor: class: MySQLDatabase
- type: MySQLDatabase constructor:
server: slavecluster.hostname.db - type: MySQLDatabase
username: user server: slavecluster.hostname.db
password: pass username: user
database: read_database password: pass
MySQLWriteDbAspect: database: read_database
properties: MySQLWriteDbAspect:
writeDb: %$WriteMySQLDatabase properties:
WriteMySQLDatabase: writeDb: %$WriteMySQLDatabase
class: MySQLDatabase WriteMySQLDatabase:
constructor: class: MySQLDatabase
- type: MySQLDatabase constructor:
server: write.hostname.db - type: MySQLDatabase
username: user server: write.hostname.db
password: pass username: user
database: write_database password: pass
MySQLDatabase: database: write_database
class: AopProxyService MySQLDatabase:
properties: class: AopProxyService
proxied: %$ReadMySQLDatabase properties:
beforeCall: proxied: %$ReadMySQLDatabase
query: beforeCall:
- %$MySQLWriteDbAspect query:
- %$MySQLWriteDbAspect
``` ```
## Changing what a method returns ## Changing what a method returns
@ -183,10 +183,10 @@ Overall configuration for this would look as follows
One major feature of an `Aspect` is the ability to modify what is returned from the client's call to the proxied method. 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` As seen in the above example, the `beforeCall` method modifies the `&$alternateReturn` variable, and returns `false`
after doing so. after doing so.
```php
$alternateReturn = $this->writeDb->query($sql, $code);
return false; ```php
$alternateReturn = $this->writeDb->query($sql, $code);
return false;
``` ```
By returning `false` from the `beforeCall()` method, the wrapping proxy class will_not_ call any additional `beforeCall` By returning `false` from the `beforeCall()` method, the wrapping proxy class will_not_ call any additional `beforeCall`

View File

@ -95,6 +95,8 @@ Linking to resources in vendor modules uses exactly the same syntax as non-vendo
this is how you would require a script in this module: this is how you would require a script in this module:
```php ```php
use SilverStripe\View\Requirements;
Requirements::javascript('tractorcow/test-vendor-module:client/js/script.js'); Requirements::javascript('tractorcow/test-vendor-module:client/js/script.js');
``` ```

View File

@ -6,8 +6,8 @@ To demonstrate how easy it is to build custom shortcodes, we'll build one to dis
address. We want our CMS authors to be able to embed the map using the following code: address. We want our CMS authors to be able to embed the map using the following code:
```php ```html
[googlemap,width=500,height=300]97-99 Courtenay Place, Wellington, New Zealand[/googlemap] [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. So we've got the address as "content" of our new `googlemap` shortcode tags, plus some `width` and `height` arguments.
@ -15,22 +15,24 @@ We'll add defaults to those in our shortcode parser so they're optional.
**mysite/_config.php** **mysite/_config.php**
```php ```php
ShortcodeParser::get('default')->register('googlemap', function($arguments, $address, $parser, $shortcode) { use SilverStripe\View\Parsers\ShortcodeParser;
$iframeUrl = sprintf(
'http://maps.google.com/maps?q=%s&amp;hnear=%s&amp;ie=UTF8&hq=&amp;t=m&amp;z=14&amp;output=embed', ShortcodeParser::get('default')->register('googlemap', function($arguments, $address, $parser, $shortcode) {
urlencode($address), $iframeUrl = sprintf(
urlencode($address) 'http://maps.google.com/maps?q=%s&amp;hnear=%s&amp;ie=UTF8&hq=&amp;t=m&amp;z=14&amp;output=embed',
); urlencode($address),
urlencode($address)
);
$width = (isset($arguments['width']) && $arguments['width']) ? $arguments['width'] : 400; $width = (isset($arguments['width']) && $arguments['width']) ? $arguments['width'] : 400;
$height = (isset($arguments['height']) && $arguments['height']) ? $arguments['height'] : 300; $height = (isset($arguments['height']) && $arguments['height']) ? $arguments['height'] : 300;
return sprintf( return sprintf(
'<iframe width="%d" height="%d" src="%s" frameborder="0" scrolling="no" marginheight="0" marginwidth="0"></iframe>', '<iframe width="%d" height="%d" src="%s" frameborder="0" scrolling="no" marginheight="0" marginwidth="0"></iframe>',
$width, $width,
$height, $height,
$iframeUrl $iframeUrl
); );
}); });
``` ```

View File

@ -11,55 +11,54 @@ explicitly logging in or by invoking the "remember me" functionality.
```php ```php
use SilverStripe\Forms\ReadonlyField; use SilverStripe\Forms\ReadonlyField;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataExtension;
class MyMemberExtension extends DataExtension class MyMemberExtension extends DataExtension
{
private static $db = [
'LastVisited' => 'Datetime',
'NumVisit' => 'Int',
];
public function memberLoggedIn()
{ {
private static $db = [ $this->logVisit();
'LastVisited' => 'Datetime',
'NumVisit' => 'Int',
];
public function memberLoggedIn()
{
$this->logVisit();
}
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
));
}
} }
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: Now you just need to apply this extension through your config:
```yml ```yml
Member: SilverStripe\Security\Member:
extensions: extensions:
- MyMemberExtension - MyMemberExtension
```
```

View File

@ -10,33 +10,30 @@ to ensure that it works as it should. A simple example would be to test the resu
```php ```php
use SilverStripe\CMS\Model\SiteTree;
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** **mysite/tests/PageTest.php**
```php ```php
use SilverStripe\Dev\SapphireTest;
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"> <div class="info" markdown="1">
@ -89,23 +86,16 @@ needs.
```xml ```xml
<phpunit bootstrap="framework/tests/bootstrap.php" colors="true"> <phpunit bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true">
<testsuite name="Default"> <testsuite name="Default">
<directory>mysite/tests</directory> <directory>mysite/tests</directory>
<directory>cms/tests</directory> </testsuite>
<directory>framework/tests</directory> <groups>
</testsuite> <exclude>
<group>sanitychecks</group>
<listeners> </exclude>
<listener class="SS_TestListener" file="framework/dev/TestListener.php" /> </groups>
</listeners> </phpunit>
<groups>
<exclude>
<group>sanitychecks</group>
</exclude>
</groups>
</phpunit>
``` ```
### setUp() and tearDown() ### setUp() and tearDown()
@ -114,40 +104,37 @@ 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 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. end of each test.
```php ```php
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Config; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\SapphireTest;
class PageTest extends SapphireTest class PageTest extends SapphireTest
{
public function setUp()
{ {
function setUp() parent::setUp();
{
parent::setUp();
// create 100 pages // create 100 pages
for ($i = 0; $i < 100; $i++) { for ($i = 0; $i < 100; $i++) {
$page = new Page(['Title' => "Page $i"]); $page = new Page(['Title' => "Page $i"]);
$page->write(); $page->write();
$page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); $page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
}
// set custom configuration for the test.
Config::inst()->update('Foo', 'bar', 'Hello!');
} }
public function testMyMethod() // set custom configuration for the test.
{ Config::modify()->update('Foo', 'bar', 'Hello!');
// ..
}
public function testMySecondMethod()
{
// ..
}
} }
public function testMyMethod()
{
// ..
}
public function testMySecondMethod()
{
// ..
}
}
``` ```
`tearDownAfterClass` and `setUpBeforeClass` can be used to run code just once for the file rather than before and after `tearDownAfterClass` and `setUpBeforeClass` can be used to run code just once for the file rather than before and after
@ -156,25 +143,24 @@ takes place.
```php ```php
use SilverStripe\Dev\SapphireTest;
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()
{
parent::tearDownAfterClass();
// ..
}
} }
public static function tearDownAfterClass()
{
parent::tearDownAfterClass();
// ..
}
}
``` ```
### Config and Injector Nesting ### Config and Injector Nesting
@ -189,23 +175,23 @@ It's important to remember that the `parent::setUp();` functions will need to be
```php ```php
public static function setUpBeforeClass() public static function setUpBeforeClass()
{ {
parent::setUpBeforeClass(); parent::setUpBeforeClass();
//this will remain for the whole suite and be removed for any other tests //this will remain for the whole suite and be removed for any other tests
Config::inst()->update('ClassName', 'var_name', 'var_value'); Config::inst()->update('ClassName', 'var_name', 'var_value');
} }
public function testFeatureDoesAsExpected() public function testFeatureDoesAsExpected()
{ {
//this will be reset to 'var_value' at the end of this test function //this will be reset to 'var_value' at the end of this test function
Config::inst()->update('ClassName', 'var_name', 'new_var_value'); Config::inst()->update('ClassName', 'var_name', 'new_var_value');
} }
public function testAnotherFeatureDoesAsExpected() 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 ## Related Documentation

View File

@ -8,8 +8,9 @@ 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. creating [HTTPRequest](api:SilverStripe\Control\HTTPRequest), receiving [HTTPResponse](api:SilverStripe\Control\HTTPResponse) objects and modifying the current user session.
## Get ## Get
```php ```php
$page = $this->get($url); $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 Performs a GET request on $url and retrieves the [HTTPResponse](api:SilverStripe\Control\HTTPResponse). This also changes the current page to the value
@ -17,7 +18,7 @@ of the response.
## Post ## Post
```php ```php
$page = $this->post($url); $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 Performs a POST request on $url and retrieves the [HTTPResponse](api:SilverStripe\Control\HTTPResponse). This also changes the current page to the value
@ -27,8 +28,7 @@ of the response.
```php ```php
$submit = $this->submitForm($formID, $button = null, $data = []); $submit = $this->submitForm($formID, $button = null, $data = []);
``` ```
Submits the given form (`#ContactForm`) on the current page and returns the [HTTPResponse](api:SilverStripe\Control\HTTPResponse). Submits the given form (`#ContactForm`) on the current page and returns the [HTTPResponse](api:SilverStripe\Control\HTTPResponse).
@ -37,14 +37,14 @@ Submits the given form (`#ContactForm`) on the current page and returns the [HTT
```php ```php
$this->logInAs($member); $this->logInAs($member);
``` ```
Logs a given user in, sets the current session. To log all users out pass `null` to the method. Logs a given user in, sets the current session. To log all users out pass `null` to the method.
```php ```php
$this->logInAs(null); $this->logInAs(null);
``` ```
## Assertions ## Assertions
@ -55,10 +55,9 @@ The `FunctionalTest` class also provides additional asserts to validate your tes
```php ```php
$this->assertPartialMatchBySelector('p.good',[ $this->assertPartialMatchBySelector('p.good',[
'Test save was successful' '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 Asserts that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS
@ -70,10 +69,9 @@ assertion fails if one of the expectedMatches fails to appear.
```php ```php
$this->assertExactMatchBySelector("#MyForm_ID p.error", [ $this->assertExactMatchBySelector("#MyForm_ID p.error", [
"That email address is invalid." "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 Asserts that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS
@ -81,11 +79,11 @@ selector will be applied to the HTML of the most recent page. The full HTML of e
assertion fails if one of the expectedMatches fails to appear. assertion fails if one of the expectedMatches fails to appear.
### assertPartialHTMLMatchBySelector ### assertPartialHTMLMatchBySelector
```php
$this->assertPartialHTMLMatchBySelector("#MyForm_ID p.error", [
"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 Assert that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS
@ -98,10 +96,9 @@ assertion fails if one of the expectedMatches fails to appear.
### assertExactHTMLMatchBySelector ### assertExactHTMLMatchBySelector
```php ```php
$this->assertExactHTMLMatchBySelector("#MyForm_ID p.error", [ $this->assertExactHTMLMatchBySelector("#MyForm_ID p.error", [
"That email address is invalid." "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 Assert that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS

View File

@ -3,10 +3,10 @@ summary: Populate test databases with fake seed data.
# Fixtures # Fixtures
To test functionality correctly, we must use consistent data. If we are testing our code with the same data each To test functionality correctly, we must use consistent data. If we are testing our code with the same data each
time, we can trust our tests to yield reliable results and to identify when the logic changes. Each test run in time, we can trust our tests to yield reliable results and to identify when the logic changes. Each test run in
SilverStripe starts with a fresh database containing no records. `Fixtures` provide a way to describe the initial data SilverStripe starts with a fresh database containing no records. `Fixtures` provide a way to describe the initial data
to load into the database. The [SapphireTest](api:SilverStripe\Dev\SapphireTest) class takes care of populating a test database with data from to load into the database. The [SapphireTest](api:SilverStripe\Dev\SapphireTest) class takes care of populating a test database with data from
fixtures - all we have to do is define them. fixtures - all we have to do is define them.
To include your fixture file in your tests, you should define it as your `$fixture_file`: To include your fixture file in your tests, you should define it as your `$fixture_file`:
@ -16,111 +16,114 @@ To include your fixture file in your tests, you should define it as your `$fixtu
```php ```php
class MyNewTest extends SapphireTest use SilverStripe\Dev\SapphireTest;
{
class MyNewTest extends SapphireTest
protected static $fixture_file = 'fixtures.yml'; {
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: You can also use an array of fixture files, if you want to use parts of multiple other tests.
If you are using [api:SilverStripe\Dev\TestOnly] dataobjects in your fixtures, you must
declare these classes within the $extra_dataobjects variable.
**mysite/tests/MyNewTest.php** **mysite/tests/MyNewTest.php**
```php ```php
class MyNewTest extends SapphireTest use SilverStripe\Dev\SapphireTest;
{
protected static $fixture_file = [
'fixtures.yml',
'otherfixtures.yml'
];
}
class MyNewTest extends SapphireTest
{
protected static $fixture_file = [
'fixtures.yml',
'otherfixtures.yml'
];
protected static $extra_dataobjects = [
Player::class,
Team::class,
];
}
``` ```
Typically, you'd have a separate fixture file for each class you are testing - although overlap between tests is common. 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 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: ideal for fixture generation. Say we have the following two DataObjects:
```php ```php
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObject; use SilverStripe\Dev\TestOnly;
class Player extends DataObject class Player extends DataObject implements TestOnly
{ {
private static $db = [ private static $db = [
'Name' => 'Varchar(255)' 'Name' => 'Varchar(255)'
]; ];
private static $has_one = [ private static $has_one = [
'Team' => 'Team' 'Team' => 'Team'
]; ];
} }
class Team extends DataObject class Team extends DataObject implements TestOnly
{ {
private static $db = [ private static $db = [
'Name' => 'Varchar(255)', 'Name' => 'Varchar(255)',
'Origin' => 'Varchar(255)' 'Origin' => 'Varchar(255)'
]; ];
private static $has_many = [
'Players' => 'Player'
];
}
private static $has_many = [
'Players' => 'Player'
];
}
``` ```
We can represent multiple instances of them in `YAML` as follows: We can represent multiple instances of them in `YAML` as follows:
**mysite/tests/fixtures.yml** **mysite/tests/fixtures.yml**
```yml ```yml
Team: Team:
hurricanes: hurricanes:
Name: The Hurricanes Name: The Hurricanes
Origin: Wellington Origin: Wellington
crusaders: crusaders:
Name: The Crusaders Name: The Crusaders
Origin: Canterbury Origin: Canterbury
Player: Player:
john: john:
Name: John Name: John
Team: =>Team.hurricanes Team: =>Team.hurricanes
joe: joe:
Name: Joe Name: Joe
Team: =>Team.crusaders Team: =>Team.crusaders
jack: jack:
Name: Jack Name: Jack
Team: =>Team.crusaders Team: =>Team.crusaders
``` ```
This `YAML` is broken up into three levels, signified by the indentation of each line. In the first level of 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. indentation, `Player` and `Team`, represent the class names of the objects we want to be created.
The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are **identifiers**. Each identifier you specify 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` represents a new object and can be referenced in the PHP using `objFromFixture`
```php ```php
$player = $this->objFromFixture('Player', 'jack'); $player = $this->objFromFixture('Player', 'jack');
``` ```
The third and final level represents each individual object's fields. The third and final level represents each individual object's fields.
A field can either be provided with raw data (such as the names for our Players), or we can define a relationship, as A field can either be provided with raw data (such as the names for our Players), or we can define a relationship, as
seen by the fields prefixed with `=>`. seen by the fields prefixed with `=>`.
Each one of our Players has a relationship to a Team, this is shown with the `Team` field for each `Player` being set Each one of our Players has a relationship to a Team, this is shown with the `Team` field for each `Player` being set
to `=>Team.` followed by a team name. to `=>Team.` followed by a team name.
<div class="info" markdown="1"> <div class="info" markdown="1">
@ -129,7 +132,7 @@ sets the `has_one` relationship for John with with the `Team` object `hurricanes
</div> </div>
<div class="hint" markdown='1'> <div class="hint" markdown='1'>
Note that we use the name of the relationship (Team), and not the name of the Note that we use the name of the relationship (Team), and not the name of the
database field (TeamID). database field (TeamID).
</div> </div>
@ -143,44 +146,42 @@ We can also declare the relationships conversely. Another way we could write the
```yml ```yml
Player:
Player: john:
john: Name: John
Name: John joe:
joe: Name: Joe
Name: Joe jack:
jack: Name: Jack
Name: Jack Team:
Team: hurricanes:
hurricanes: Name: Hurricanes
Name: Hurricanes Origin: Wellington
Origin: Wellington Players: =>Player.john
Players: =>Player.john crusaders:
crusaders: Name: Crusaders
Name: Crusaders Origin: Canterbury
Origin: Canterbury Players: =>Player.joe,=>Player.jack
Players: =>Player.joe,=>Player.jack
``` ```
The database is populated by instantiating `DataObject` objects and setting the fields declared in the `YAML`, then 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 calling `write()` on those objects. Take for instance the `hurricances` record in the `YAML`. It is equivalent to
writing: writing:
```php ```php
$team = new Team([ $team = new Team([
'Name' => 'Hurricanes', 'Name' => 'Hurricanes',
'Origin' => 'Wellington' 'Origin' => 'Wellington'
]); ]);
$team->write(); $team->write();
$team->Players()->add($john);
$team->Players()->add($john);
``` ```
<div class="notice" markdown="1"> <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 As the YAML fixtures will call `write`, any `onBeforeWrite()` or default value logic will be executed as part of the
test. test.
</div> </div>
@ -190,16 +191,14 @@ As of SilverStripe 4 you will need to use fully qualfied class names in your YAM
```yml ```yml
MyProject\Model\Player:
MyProject\Model\Player: john:
john: Name: join
Name: join MyProject\Model\Team:
crusaders:
MyProject\Model\Team: Name: Crusaders
crusaders: Origin: Canterbury
Name: Crusaders Players: =>MyProject\Model\Player.john
Origin: Canterbury
Players: =>MyProject\Model\Player.john
``` ```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
@ -208,108 +207,107 @@ If your tests are failing and your database has table names that follow the full
### Defining many_many_extraFields ### Defining many_many_extraFields
`many_many` relations can have additional database fields attached to the relationship. For example we may want to `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. declare the role each player has in the team.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject
{
private static $db = [
'Name' => 'Varchar(255)'
];
private static $belongs_many_many = [ class Player extends DataObject
'Teams' => 'Team' {
]; private static $db = [
} 'Name' => 'Varchar(255)'
];
class Team extends DataObject private static $belongs_many_many = [
{ 'Teams' => 'Team'
private static $db = [ ];
'Name' => 'Varchar(255)' }
];
private static $many_many = [ class Team extends DataObject
'Players' => 'Player' {
]; private static $db = [
'Name' => 'Varchar(255)'
];
private static $many_many_extraFields = [ private static $many_many = [
'Players' => [ 'Players' => 'Player'
'Role' => "Varchar" ];
]
];
}
private static $many_many_extraFields = [
'Players' => [
'Role' => "Varchar"
]
];
}
``` ```
To provide the value for the `many_many_extraField` use the YAML list syntax. To provide the value for the `many_many_extraField` use the YAML list syntax.
```yml ```yml
Player:
john:
Name: John
joe:
Name: Joe
jack:
Name: Jack
Team:
hurricanes:
Name: The Hurricanes
Players:
- =>Player.john:
Role: Captain
Player: crusaders:
john: Name: The Crusaders
Name: John Players:
joe: - =>Player.joe:
Name: Joe Role: Captain
jack: - =>Player.jack:
Name: Jack Role: Winger
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 ## Fixture Factories
While manually defined fixtures provide full flexibility, they offer very little in terms of structure and convention. While manually defined fixtures provide full flexibility, they offer very little in terms of structure and convention.
Alternatively, you can use the [FixtureFactory](api:SilverStripe\Dev\FixtureFactory) class, which allows you to set default values, callbacks on object Alternatively, you can use the [FixtureFactory](api:SilverStripe\Dev\FixtureFactory) class, which allows you to set default values, callbacks on object
creation, and dynamic/lazy value setting. creation, and dynamic/lazy value setting.
<div class="hint" markdown='1'> <div class="hint" markdown='1'>
`SapphireTest` uses `FixtureFactory` under the hood when it is provided with YAML based fixtures. `SapphireTest` uses `FixtureFactory` under the hood when it is provided with YAML based fixtures.
</div> </div>
The idea is that rather than instantiating objects directly, we'll have a factory class for them. This factory can have The idea is that rather than instantiating objects directly, we'll have a factory class for them. This factory can have
*blueprints* defined on it, which tells the factory how to instantiate an object of a specific type. Blueprints need a *blueprints* defined on it, which tells the factory how to instantiate an object of a specific type. Blueprints need a
name, which is usually set to the class it creates such as `Member` or `Page`. 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 Blueprints are auto-created for all available DataObject subclasses, you only need to instantiate a factory to start
using them. using them.
```php ```php
$factory = Injector::inst()->create('FixtureFactory'); use SilverStripe\Core\Injector\Injector;
$obj = $factory->createObject('Team', 'hurricanes'); $factory = Injector::inst()->create('FixtureFactory');
$obj = $factory->createObject('Team', 'hurricanes');
``` ```
In order to create an object with certain properties, just add a third argument: In order to create an object with certain properties, just add a third argument:
```php ```php
$obj = $factory->createObject('Team', 'hurricanes', [ $obj = $factory->createObject('Team', 'hurricanes', [
'Name' => 'My Value' 'Name' => 'My Value'
]); ]);
``` ```
<div class="warning" markdown="1"> <div class="warning" markdown="1">
It is important to remember that fixtures are referenced by arbitrary identifiers ('hurricanes'). These are internally It is important to remember that fixtures are referenced by arbitrary identifiers ('hurricanes'). These are internally
mapped to their database identifiers. mapped to their database identifiers.
</div> </div>
@ -317,7 +315,7 @@ After we've created this object in the factory, `getId` is used to retrieve it b
```php ```php
$databaseId = $factory->getId('Team', 'hurricanes'); $databaseId = $factory->getId('Team', 'hurricanes');
``` ```
### Default Properties ### Default Properties
@ -327,59 +325,56 @@ name, we can set the default to be `Unknown Team`.
```php ```php
$factory->define('Team', [ $factory->define('Team', [
'Name' => 'Unknown Team' 'Name' => 'Unknown Team'
]); ]);
``` ```
### Dependent Properties ### Dependent Properties
Values can be set on demand through anonymous functions, which can either generate random defaults, or create composite Values can be set on demand through anonymous functions, which can either generate random defaults, or create composite
values based on other fixture data. values based on other fixture data.
```php ```php
$factory->define('Member', [ $factory->define('Member', [
'Email' => function($obj, $data, $fixtures) { 'Email' => function($obj, $data, $fixtures) {
if(isset($data['FirstName']) { if(isset($data['FirstName']) {
$obj->Email = strtolower($data['FirstName']) . '@example.org'; $obj->Email = strtolower($data['FirstName']) . '@example.org';
}
},
'Score' => function($obj, $data, $fixtures) {
$obj->Score = rand(0,10);
} }
)]; },
'Score' => function($obj, $data, $fixtures) {
$obj->Score = rand(0,10);
}
)];
``` ```
### Relations ### Relations
Model relations can be expressed through the same notation as in the YAML fixture format described earlier, through the Model relations can be expressed through the same notation as in the YAML fixture format described earlier, through the
`=>` prefix on data values. `=>` prefix on data values.
```php ```php
$obj = $factory->createObject('Team', 'hurricanes', [ $obj = $factory->createObject('Team', 'hurricanes', [
'MyHasManyRelation' => '=>Player.john,=>Player.joe' 'MyHasManyRelation' => '=>Player.john,=>Player.joe'
]); ]);
``` ```
#### Callbacks #### Callbacks
Sometimes new model instances need to be modified in ways which can't be expressed in their properties, for example to 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. publish a page, which requires a method call.
```php ```php
$blueprint = Injector::inst()->create('FixtureBlueprint', 'Member'); $blueprint = Injector::inst()->create('FixtureBlueprint', 'Member');
$blueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) { $blueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) {
$obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); $obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
}); });
$page = $factory->define('Page', $blueprint); $page = $factory->define('Page', $blueprint);
``` ```
Available callbacks: Available callbacks:
@ -389,26 +384,26 @@ Available callbacks:
### Multiple Blueprints ### Multiple Blueprints
Data of the same type can have variations, for example forum members vs. CMS admins could both inherit from the `Member` Data of the same type can have variations, for example forum members vs. CMS admins could both inherit from the `Member`
class, but have completely different properties. This is where named blueprints come in. By default, blueprint names class, but have completely different properties. This is where named blueprints come in. By default, blueprint names
equal the class names they manage. equal the class names they manage.
```php ```php
$memberBlueprint = Injector::inst()->create('FixtureBlueprint', 'Member', 'Member'); $memberBlueprint = Injector::inst()->create('FixtureBlueprint', 'Member', 'Member');
$adminBlueprint = Injector::inst()->create('FixtureBlueprint', 'AdminMember', 'Member'); $adminBlueprint = Injector::inst()->create('FixtureBlueprint', 'AdminMember', 'Member');
$adminBlueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) { $adminBlueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) {
if(isset($fixtures['Group']['admin'])) { if(isset($fixtures['Group']['admin'])) {
$adminGroup = Group::get()->byId($fixtures['Group']['admin']); $adminGroup = Group::get()->byId($fixtures['Group']['admin']);
$obj->Groups()->add($adminGroup); $obj->Groups()->add($adminGroup);
} }
}); });
$member = $factory->createObject('Member'); // not in admin group $member = $factory->createObject('Member'); // not in admin group
$admin = $factory->createObject('AdminMember'); // in admin group $admin = $factory->createObject('AdminMember'); // in admin group
``` ```
## Related Documentation ## Related Documentation
@ -419,4 +414,3 @@ equal the class names they manage.
* [FixtureFactory](api:SilverStripe\Dev\FixtureFactory) * [FixtureFactory](api:SilverStripe\Dev\FixtureFactory)
* [FixtureBlueprint](api:SilverStripe\Dev\FixtureBlueprint) * [FixtureBlueprint](api:SilverStripe\Dev\FixtureBlueprint)

View File

@ -7,44 +7,41 @@ how you can load default records into the test database.
**mysite/tests/PageTest.php** **mysite/tests/PageTest.php**
```php ```php
use SilverStripe\Dev\SapphireTest;
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';
/**
* 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 = [
* Defines the fixture file to use for this test class 'home' => 'home',
* @var string 'staff' => 'my-staff',
*/ 'about' => 'about-us',
protected static $fixture_file = 'SiteTreeTest.yml'; 'staffduplicate' => 'my-staff-2'
];
/** foreach ($expectedURLs as $fixture => $urlSegment) {
* Test generation of the URLSegment values. $obj = $this->objFromFixture('Page', $fixture);
*
* 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 = [
'home' => 'home',
'staff' => 'my-staff',
'about' => 'about-us',
'staffduplicate' => 'my-staff-2'
];
foreach($expectedURLs as $fixture => $urlSegment) { $this->assertEquals($urlSegment, $obj->URLSegment);
$obj = $this->objFromFixture('Page', $fixture);
$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, Firstly we define a static `$fixture_file`, this should point to a file that represents the data we want to test,

View File

@ -11,44 +11,43 @@ response and modify the session within a test.
```php ```php
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
class HomePageTest extends FunctionalTest class HomePageTest extends FunctionalTest
{
/**
* Test generation of the view
*/
public function testViewHomePage()
{ {
$page = $this->get('home/');
/** // Home page should load..
* Test generation of the view $this->assertEquals(200, $page->getStatusCode());
*/
public function testViewHomePage()
{
$page = $this->get('home/');
// Home page should load.. // We should see a login form
$this->assertEquals(200, $page->getStatusCode()); $login = $this->submitForm("LoginFormID", null, [
'Email' => 'test@test.com',
'Password' => 'wrongpassword'
]);
// We should see a login form // wrong details, should now see an error message
$login = $this->submitForm("LoginFormID", null, [ $this->assertExactHTMLMatchBySelector("#LoginForm p.error", [
'Email' => 'test@test.com', "That email address is invalid."
'Password' => 'wrongpassword' ]);
]);
// wrong details, should now see an error message // If we login as a user we should see a welcome message
$this->assertExactHTMLMatchBySelector("#LoginForm p.error", [ $me = Member::get()->first();
"That email address is invalid."
]);
// If we login as a user we should see a welcome message $this->logInAs($me);
$me = Member::get()->first(); $page = $this->get('home/');
$this->logInAs($me); $this->assertExactHTMLMatchBySelector("#Welcome", [
$page = $this->get('home/'); 'Welcome Back'
]);
$this->assertExactHTMLMatchBySelector("#Welcome", [
'Welcome Back'
]);
}
} }
}
``` ```
## Related Documentation ## Related Documentation

View File

@ -10,40 +10,41 @@ with information that we need.
```php ```php
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Core\Injector\Injector;
class MyObjectTest extends SapphireTest class MyObjectTest extends SapphireTest
{ {
protected $factory; protected $factory;
function __construct() { function __construct() {
parent::__construct(); parent::__construct();
$factory = Injector::inst()->create('FixtureFactory'); $factory = Injector::inst()->create('FixtureFactory');
// Defines a "blueprint" for new objects // Defines a "blueprint" for new objects
$factory->define('MyObject', [ $factory->define('MyObject', [
'MyProperty' => 'My Default Value' 'MyProperty' => 'My Default Value'
]); ]);
$this->factory = $factory; $this->factory = $factory;
}
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"
}
} }
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 ## Related Documentation

View File

@ -8,24 +8,26 @@ email was sent using this method.
```php ```php
public function MyMethod() use SilverStripe\Control\Email\Email;
{
$e = new Email(); public function MyMethod()
$e->To = "someone@example.com"; {
$e->Subject = "Hi there"; $e = new Email();
$e->Body = "I just really wanted to email you and say hi."; $e->To = "someone@example.com";
$e->send(); $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. To test that `MyMethod` sends the correct email, use the [SapphireTest::assertEmailSent()](api:SilverStripe\Dev\SapphireTest::assertEmailSent()) method.
```php ```php
$this->assertEmailSent($to, $from, $subject, $body); $this->assertEmailSent($to, $from, $subject, $body);
// to assert that the email is sent to the correct person // to assert that the email is sent to the correct person
$this->assertEmailSent("someone@example.com", null, "/th.*e$/"); $this->assertEmailSent("someone@example.com", null, "/th.*e$/");
``` ```
Each of the arguments (`$to`, `$from`, `$subject` and `$body`) can be either one of the following. Each of the arguments (`$to`, `$from`, `$subject` and `$body`) can be either one of the following.

View File

@ -57,7 +57,7 @@ some `thirdparty/` directories add the following to the `phpunit.xml` configurat
<directory suffix=".php">framework/dev/</directory> <directory suffix=".php">framework/dev/</directory>
<directory suffix=".php">framework/thirdparty/</directory> <directory suffix=".php">framework/thirdparty/</directory>
<directory suffix=".php">cms/thirdparty/</directory> <directory suffix=".php">cms/thirdparty/</directory>
<!-- Add your custom rules here --> <!-- Add your custom rules here -->
<directory suffix=".php">mysite/thirdparty/</directory> <directory suffix=".php">mysite/thirdparty/</directory>
</blacklist> </blacklist>

View File

@ -29,13 +29,12 @@ want to password protect the site. You can enable that by adding this to your `m
```yml ```yml
---
--- Only:
Only: environment: 'test'
environment: 'test' ---
--- SilverStripe\Security\BasicAuth:
SilverStripe\Security\BasicAuth: entire_site_protected: true
entire_site_protected: true
``` ```
### Live Mode ### Live Mode
@ -52,30 +51,34 @@ 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. You can check for the current environment type in [config files](../configuration) through the `environment` variant.
**mysite/_config/app.yml** **mysite/_config/app.yml**
```yml ```yml
--- ---
Only: Only:
environment: 'live' environment: 'live'
--- ---
MyClass: MyClass:
myvar: live_value myvar: live_value
--- ---
Only: Only:
environment: 'test' environment: 'test'
--- ---
MyClass: MyClass:
myvar: test_value myvar: test_value
``` ```
Checking for what environment you're running in can also be done in PHP. Your application code may disable or enable 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 ```php
if (Director::isLive()) { use SilverStripe\Control\Director;
// is in live
} elseif (Director::isTest()) { if (Director::isLive()) {
// is in test mode // is in live
} elseif (Director::isDev()) { } elseif (Director::isTest()) {
// is in dev mode // is in test mode
} } elseif (Director::isDev()) {
// is in dev mode
}
``` ```

View File

@ -14,6 +14,8 @@ For informational and debug logs, you can use the Logger directly. The Logger is
can be accessed via the `Injector`: can be accessed via the `Injector`:
```php ```php
use SilverStripe\Core\Injector\Injector;
Injector::inst()->get(LoggerInterface::class)->info('User has logged in: ID #' . Security::getCurrentUser()->ID); Injector::inst()->get(LoggerInterface::class)->info('User has logged in: ID #' . Security::getCurrentUser()->ID);
Injector::inst()->get(LoggerInterface::class)->debug('Query executed: ' . $sql); Injector::inst()->get(LoggerInterface::class)->debug('Query executed: ' . $sql);
``` ```

View File

@ -10,11 +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 source code comments indicating which template is responsible for rendering each
block of html on your page. block of html on your page.
```yaml ```yml
--- ---
Only: Only:
environment: 'dev' environment: 'dev'
--- ---
SSViewer: SilverStripe\View\SSViewer:
source_file_comments: true source_file_comments: true
``` ```

View File

@ -18,14 +18,17 @@ 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. The [Debug](api:SilverStripe\Dev\Debug) class contains a number of static utility methods for more advanced debugging.
```php ```php
Debug::show($myVariable); use SilverStripe\Dev\Debug;
// similar to print_r($myVariable) but shows it in a more useful format. use SilverStripe\Dev\Backtrace;
Debug::message("Wow, that's great"); Debug::show($myVariable);
// prints a short debugging message. // similar to print_r($myVariable) but shows it in a more useful format.
Backtrace::backtrace(); Debug::message("Wow, that's great");
// prints a calls-stack // prints a short debugging message.
Backtrace::backtrace();
// prints a calls-stack
``` ```
## API Documentation ## API Documentation

View File

@ -4,12 +4,12 @@ summary: Cache SilverStripe templates to reduce database queries.
# Partial Caching # Partial Caching
Partial caching is a feature that allows the caching of just a portion of a page. Partial caching is a feature that allows the caching of just a portion of a page.
```ss
<% cached 'CacheKey' %> ```ss
<% cached 'CacheKey' %>
$DataTable $DataTable
... ...
<% end_cached %> <% end_cached %>
``` ```
Each cache block has a cache key. A cache key is an unlimited number of comma separated variables and quoted strings. Each cache block has a cache key. A cache key is an unlimited number of comma separated variables and quoted strings.
@ -23,18 +23,17 @@ Here are some more complex examples:
```ss ```ss
<% cached 'database', $LastEdited %>
<!-- that updates every time the record changes. -->
<% end_cached %>
<% cached 'database', $LastEdited %> <% cached 'loginblock', $CurrentMember.ID %>
<!-- that updates every time the record changes. --> <!-- cached unique to the user. i.e for user 2, they will see a different cache to user 1 -->
<% end_cached %> <% 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 %> <% cached 'loginblock', $LastEdited, $CurrentMember.isAdmin %>
<!-- recached when block object changes, and if the user is admin --> <!-- recached when block object changes, and if the user is admin -->
<% end_cached %> <% end_cached %>
``` ```
An additional global key is incorporated in the cache lookup. The default value for this is An additional global key is incorporated in the cache lookup. The default value for this is
@ -48,10 +47,9 @@ user does not influence your template content, you can update this key as below;
**mysite/_config/app.yml** **mysite/_config/app.yml**
```yaml ```yml
SilverStripe\View\SSViewer:
SilverStripe\View\SSViewer: global_key: '$CurrentReadingMode, $Locale'
global_key: '$CurrentReadingMode, $Locale'
``` ```
## Aggregates ## Aggregates
@ -65,8 +63,7 @@ otherwise. By using aggregates, we do that like this:
```ss ```ss
<% cached 'navigation', $List('SiteTree').max('LastEdited'), $List('SiteTree').count() %>
<% cached 'navigation', $List('SiteTree').max('LastEdited'), $List('SiteTree').count() %>
``` ```
The cache for this will update whenever a page is added, removed or edited. The cache for this will update whenever a page is added, removed or edited.
@ -76,8 +73,7 @@ or edited
```ss ```ss
<% cached 'categorylist', $List('Category').max('LastEdited'), $List('Category').count() %>
<% cached 'categorylist', $List('Category').max('LastEdited'), $List('Category').count() %>
``` ```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
@ -98,26 +94,24 @@ For example, a block that shows a collection of rotating slides needs to update
```php ```php
public function SliderCacheKey() public function SliderCacheKey()
{ {
$fragments = [ $fragments = [
'Page-Slides', 'Page-Slides',
$this->ID, $this->ID,
// identify which objects are in the list and their sort order // identify which objects are in the list and their sort order
implode('-', $this->Slides()->Column('ID')), implode('-', $this->Slides()->Column('ID')),
$this->Slides()->max('LastEdited') $this->Slides()->max('LastEdited')
]; ];
return implode('-_-', $fragments); return implode('-_-', $fragments);
} }
``` ```
Then reference that function in the cache key: Then reference that function in the cache key:
```ss ```ss
<% cached $SliderCacheKey %>
<% cached $SliderCacheKey %>
``` ```
The example above would work for both a has_many and many_many relationship. The example above would work for both a has_many and many_many relationship.
@ -138,8 +132,7 @@ For instance, if we show some blog statistics, but are happy having them be slig
```ss ```ss
<% cached 'blogstatistics', $Blog.ID %>
<% cached 'blogstatistics', $Blog.ID %>
``` ```
which will invalidate after the cache lifetime expires. If you need more control than that (cache lifetime is which will invalidate after the cache lifetime expires. If you need more control than that (cache lifetime is
@ -147,10 +140,10 @@ configurable only on a site-wide basis), you could add a special function to you
```php ```php
public function BlogStatisticsCounter() public function BlogStatisticsCounter()
{ {
return (int)(time() / 60 / 5); // Returns a new number every five minutes return (int)(time() / 60 / 5); // Returns a new number every five minutes
} }
``` ```
@ -158,8 +151,7 @@ and then use it in the cache key
```ss ```ss
<% cached 'blogstatistics', $Blog.ID, $BlogStatisticsCounter %>
<% cached 'blogstatistics', $Blog.ID, $BlogStatisticsCounter %>
``` ```
## Cache block conditionals ## Cache block conditionals
@ -173,8 +165,7 @@ heavy load:
```ss ```ss
<% cached 'blogstatistics', $Blog.ID if $HighLoad %>
<% cached 'blogstatistics', $Blog.ID if $HighLoad %>
``` ```
By adding a `HighLoad` function to your `PageController`, you could enable or disable caching dynamically. By adding a `HighLoad` function to your `PageController`, you could enable or disable caching dynamically.
@ -184,8 +175,7 @@ To cache the contents of a page for all anonymous users, but dynamically calcula
```ss ```ss
<% cached unless $CurrentUser %>
<% cached unless $CurrentUser %>
``` ```
## Uncached ## Uncached
@ -196,8 +186,7 @@ particular cache block by changing just the tag, leaving the key and conditional
```ss ```ss
<% uncached %>
<% uncached %>
``` ```
## Nested cache blocks ## Nested cache blocks
@ -212,16 +201,15 @@ An example:
```ss ```ss
<% cached $LastEdited %>
<% cached $LastEdited %> Our wonderful site
Our wonderful site
<% cached $Member.ID %> <% cached $Member.ID %>
Welcome $Member.Name Welcome $Member.Name
<% end_cached %>
$ASlowCalculation
<% end_cached %> <% 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 This will cache the entire outer section until the next time the page is edited, but will display a different welcome
@ -232,16 +220,15 @@ could also write the last example as:
```ss ```ss
<% cached $LastEdited %>
<% cached $LastEdited %> Our wonderful site
Our wonderful site
<% uncached %> <% uncached %>
Welcome $Member.Name Welcome $Member.Name
<% end_uncached %> <% end_uncached %>
$ASlowCalculation $ASlowCalculation
<% end_cached %> <% end_cached %>
``` ```
<div class="warning" markdown="1"> <div class="warning" markdown="1">
@ -253,46 +240,40 @@ Failing example:
```ss ```ss
<% cached $LastEdited %>
<% cached $LastEdited %> <% loop $Children %>
<% loop $Children %>
<% cached $LastEdited %> <% cached $LastEdited %>
$Name $Name
<% end_cached %> <% end_cached %>
<% end_loop %> <% end_loop %>
<% end_cached %> <% end_cached %>
``` ```
Can be re-written as: Can be re-written as:
```ss ```ss
<% cached $LastEdited %>
<% cached $LastEdited %> <% cached $AllChildren.max('LastEdited') %>
<% cached $AllChildren.max('LastEdited') %>
<% loop $Children %> <% loop $Children %>
$Name $Name
<% end_loop %> <% end_loop %>
<% end_cached %>
<% end_cached %> <% end_cached %>
<% end_cached %>
``` ```
Or: Or:
```ss ```ss
<% cached $LastEdited %>
(other code)
<% end_cached %>
<% loop $Children %>
<% cached $LastEdited %> <% cached $LastEdited %>
(other code) $Name
<% end_cached %> <% end_cached %>
<% end_loop %>
<% loop $Children %>
<% cached $LastEdited %>
$Name
<% end_cached %>
<% end_loop %>
``` ```

View File

@ -32,12 +32,11 @@ and SilverStripe's [dependency injection](/developer-guides/extending/injector)
```yml ```yml
SilverStripe\Core\Injector\Injector:
SilverStripe\Core\Injector\Injector: Psr\SimpleCache\CacheInterface.myCache:
Psr\SimpleCache\CacheInterface.myCache: factory: SilverStripe\Core\Cache\CacheFactory
factory: SilverStripe\Core\Cache\CacheFactory constructor:
constructor: namespace: "myCache"
namespace: "myCache"
``` ```
Cache objects are instantiated through a [CacheFactory](SilverStripe\Core\Cache\CacheFactory), Cache objects are instantiated through a [CacheFactory](SilverStripe\Core\Cache\CacheFactory),
@ -46,8 +45,10 @@ This factory allows us you to globally define an adapter for all cache instances
```php ```php
use Psr\SimpleCache\CacheInterface use Psr\SimpleCache\CacheInterface
$cache = Injector::inst()->get(CacheInterface::class . '.myCache'); use SilverStripe\Core\Injector\Injector;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
``` ```
Caches are namespaced, which might allow granular clearing of a particular cache without affecting others. Caches are namespaced, which might allow granular clearing of a particular cache without affecting others.
@ -65,22 +66,24 @@ Cache objects follow the [PSR-16](http://www.php-fig.org/psr/psr-16/) class inte
```php ```php
use Psr\SimpleCache\CacheInterface use Psr\SimpleCache\CacheInterface;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache'); use SilverStripe\Core\Injector\Injector;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache');
// create a new item by trying to get it from the cache // create a new item by trying to get it from the cache
$myValue = $cache->get('myCacheKey'); $myValue = $cache->get('myCacheKey');
// set a value and save it via the adapter // set a value and save it via the adapter
$cache->set('myCacheKey', 1234); $cache->set('myCacheKey', 1234);
// retrieve the cache item // retrieve the cache item
if (!$cache->has('myCacheKey')) { if (!$cache->has('myCacheKey')) {
// ... item does not exists in the cache // ... item does not exists in the cache
} }
``` ```
## Invalidation ## Invalidation
Caches can be invalidated in different ways. The easiest is to actively clear the Caches can be invalidated in different ways. The easiest is to actively clear the
@ -89,34 +92,39 @@ this will only affect a subset of cache keys ("myCache" in this example):
```php ```php
use Psr\SimpleCache\CacheInterface use Psr\SimpleCache\CacheInterface;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache'); use SilverStripe\Core\Injector\Injector;
// remove all items in this (namespaced) cache $cache = Injector::inst()->get(CacheInterface::class . '.myCache');
$cache->clear();
// remove all items in this (namespaced) cache
$cache->clear();
``` ```
You can also delete a single item based on it's cache key: You can also delete a single item based on it's cache key:
```php ```php
use Psr\SimpleCache\CacheInterface use Psr\SimpleCache\CacheInterface;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache'); use SilverStripe\Core\Injector\Injector;
// remove the cache item $cache = Injector::inst()->get(CacheInterface::class . '.myCache');
$cache->delete('myCacheKey');
// remove the cache item
$cache->delete('myCacheKey');
``` ```
Individual cache items can define a lifetime, after which the cached value is marked as expired: Individual cache items can define a lifetime, after which the cached value is marked as expired:
```php ```php
use Psr\SimpleCache\CacheInterface use Psr\SimpleCache\CacheInterface;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache'); use SilverStripe\Core\Injector\Injector;
// remove the cache item $cache = Injector::inst()->get(CacheInterface::class . '.myCache');
$cache->set('myCacheKey', 'myValue', 300); // cache for 300 seconds
// 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. If a lifetime isn't defined on the `set()` call, it'll use the adapter default.
@ -128,11 +136,10 @@ you need to be careful with resources here (e.g. filesystem space).
```yml ```yml
SilverStripe\Core\Injector\Injector:
SilverStripe\Core\Injector\Injector: Psr\SimpleCache\CacheInterface.cacheblock:
Psr\SimpleCache\CacheInterface.cacheblock: constructor:
constructor: defaultLifetime: 3600
defaultLifetime: 3600
``` ```
In most cases, invalidation and expiry should be handled by your cache key. In most cases, invalidation and expiry should be handled by your cache key.
@ -144,12 +151,14 @@ old cache keys will be garbage collected as the cache fills up.
```php ```php
use Psr\SimpleCache\CacheInterface use Psr\SimpleCache\CacheInterface;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache'); use SilverStripe\Core\Injector\Injector;
// Automatically changes when any group is edited $cache = Injector::inst()->get(CacheInterface::class . '.myCache');
$cacheKey = implode(['groupNames', $member->ID, Group::get()->max('LastEdited')]);
$cache->set($cacheKey, $member->Groups()->column('Title')); // Automatically changes when any group is edited
$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 If `?flush=1` is requested in the URL, this will trigger a call to `flush()` on
@ -184,20 +193,19 @@ and takes a `MemcachedClient` instance as a constructor argument.
```yml ```yml
---
--- After:
After: - '#corecache'
- '#corecache' ---
--- SilverStripe\Core\Injector\Injector:
SilverStripe\Core\Injector\Injector: MemcachedClient:
MemcachedClient: class: 'Memcached'
class: 'Memcached' calls:
calls: - [ addServer, [ 'localhost', 11211 ] ]
- [ addServer, [ 'localhost', 11211 ] ] SilverStripe\Core\Cache\CacheFactory:
SilverStripe\Core\Cache\CacheFactory: class: 'SilverStripe\Core\Cache\MemcachedCacheFactory'
class: 'SilverStripe\Core\Cache\MemcachedCacheFactory' constructor:
constructor: client: '%$MemcachedClient
client: '%$MemcachedClient
``` ```
## Additional Caches ## Additional Caches

View File

@ -17,8 +17,11 @@ headers:
## Customizing Cache Headers ## Customizing Cache Headers
### HTTP::set_cache_age ### HTTP::set_cache_age
```php ```php
HTTP::set_cache_age(0); use SilverStripe\Control\HTTP;
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" Used to set the max-age component of the cache-control line, in seconds. Set it to 0 to disable caching; the "no-cache"
@ -28,7 +31,7 @@ clause in `Cache-Control` and `Pragma` will be included.
```php ```php
HTTP::register_modification_date('2014-10-10'); 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 Used to set the modification date to something more recent than the default. [DataObject::__construct](api:SilverStripe\ORM\DataObject::__construct) calls
@ -46,10 +49,6 @@ Cookie, X-Forwarded-Protocol, User-Agent, Accept
To change the value of the `Vary` header, you can change this value by specifying the header in configuration To change the value of the `Vary` header, you can change this value by specifying the header in configuration
```yml ```yml
HTTP: SilverStripe\Control\HTTP:
vary: "" vary: ""
``` ```

View File

@ -24,8 +24,10 @@ SilverStripe can request more resources through `Environment::increaseMemoryLimi
</div> </div>
```php ```php
public function myBigFunction() use SilverStripe\Core\Environment;
{
Environment::increaseTimeLimitTo(400); public function myBigFunction()
} {
``` Environment::increaseTimeLimitTo(400);
}
```

View File

@ -15,11 +15,13 @@ The [api:Security] class comes with a static method for getting information abou
Retrieves the current logged in member. Returns *null* if user is not logged in, otherwise, the Member object is returned. Retrieves the current logged in member. Returns *null* if user is not logged in, otherwise, the Member object is returned.
```php ```php
if( $member = Security::getCurrentUser() ) { use SilverStripe\Security\Security;
// Work with $member
} else { if( $member = Security::getCurrentUser() ) {
// Do non-member stuff // Work with $member
} } else {
// Do non-member stuff
}
``` ```
## Subclassing ## Subclassing
@ -32,6 +34,8 @@ 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. You can define subclasses of [Member](api:SilverStripe\Security\Member) to add extra fields or functionality to the built-in membership system.
```php ```php
use SilverStripe\Security\Member;
class MyMember extends Member { class MyMember extends Member {
private static $db = array( private static $db = array(
"Age" => "Int", "Age" => "Int",
@ -45,8 +49,8 @@ To ensure that all new members are created using this class, put a call to [api:
```yml ```yml
SilverStripe\Core\Injector\Injector: SilverStripe\Core\Injector\Injector:
SilverStripe\Security\Member: SilverStripe\Security\Member:
class: MyVendor\MyNamespace\MyMemberClass class: MyVendor\MyNamespace\MyMemberClass
``` ```
Note that if you want to look this class-name up, you can call `Injector::inst()->get('Member')->ClassName` Note that if you want to look this class-name up, you can call `Injector::inst()->get('Member')->ClassName`
@ -58,13 +62,15 @@ details in the newsletter system. This function returns a [FieldList](api:Silve
parent::getCMSFields() and manipulate the [FieldList](api:SilverStripe\Forms\FieldList) from there. parent::getCMSFields() and manipulate the [FieldList](api:SilverStripe\Forms\FieldList) from there.
```php ```php
public function getCMSFields() { use SilverStripe\Forms\TextField;
$fields = parent::getCMSFields();
$fields->insertBefore("HTMLEmail", new TextField("Age")); public function getCMSFields() {
$fields->removeByName("JobTitle"); $fields = parent::getCMSFields();
$fields->removeByName("Organisation"); $fields->insertBefore("HTMLEmail", new TextField("Age"));
return $fields; $fields->removeByName("JobTitle");
} $fields->removeByName("Organisation");
return $fields;
}
``` ```
## Extending Member or DataObject? ## Extending Member or DataObject?
@ -99,7 +105,6 @@ use SilverStripe\ORM\DataExtension;
class MyMemberExtension extends DataExtension class MyMemberExtension extends DataExtension
{ {
/** /**
* Modify the field set to be displayed in the CMS detail pop-up * Modify the field set to be displayed in the CMS detail pop-up
*/ */
public function updateCMSFields(FieldList $currentFields) public function updateCMSFields(FieldList $currentFields)
@ -150,8 +155,6 @@ reasonably be expected to be allowed to do.
E.g. E.g.
```php ```php
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;

View File

@ -8,7 +8,7 @@ authentication system.
The main login system uses these controllers to handle the various security requests: The main login system uses these controllers to handle the various security requests:
[Security](api:SilverStripe\Security\Security) - Which is the controller which handles most front-end security requests, including logging in, logging out, resetting password, or changing password. This class also provides an interface to allow configured [Authenticator](api:SilverStripe\Security\Authenticator) classes to each display a custom login form. [Security](api:SilverStripe\Security\Security) - Which is the controller which handles most front-end security requests, including logging in, logging out, resetting password, or changing password. This class also provides an interface to allow configured [Authenticator](api:SilverStripe\Security\Authenticator) classes to each display a custom login form.
[CMSSecurity](api:SilverStripe\Security\CMSSecurity) - Which is the controller which handles security requests within the CMS, and allows users to re-login without leaving the CMS. [CMSSecurity](api:SilverStripe\Security\CMSSecurity) - Which is the controller which handles security requests within the CMS, and allows users to re-login without leaving the CMS.
@ -16,7 +16,7 @@ The main login system uses these controllers to handle the various security requ
The default member authentication system is implemented in the following classes: The default member authentication system is implemented in the following classes:
[MemberAuthenticator](api:SilverStripe\Security\MemberAuthenticator) - Which is the default member authentication implementation. This uses the email and password stored internally for each member to authenticate them. [MemberAuthenticator](api:SilverStripe\Security\MemberAuthenticator) - Which is the default member authentication implementation. This uses the email and password stored internally for each member to authenticate them.
[MemberLoginForm](api:SilverStripe\Security\MemberAuthenticator\MemberLoginForm) - Is the default form used by `MemberAuthenticator`, and is displayed on the public site at the url `Security/login` by default. [MemberLoginForm](api:SilverStripe\Security\MemberAuthenticator\MemberLoginForm) - Is the default form used by `MemberAuthenticator`, and is displayed on the public site at the url `Security/login` by default.
@ -38,11 +38,13 @@ 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. 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: 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. # Configure a default username and password to access the CMS on all sites in this environment.
SS_DEFAULT_ADMIN_USERNAME="admin" SS_DEFAULT_ADMIN_USERNAME="admin"
SS_DEFAULT_ADMIN_PASSWORD="password" 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 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 the database, but without any password information. This means that the password can be reset or changed by simply
updating the `.env` file. updating the `.env` file.
@ -68,24 +70,24 @@ SilverStripe\Core\Injector\Injector:
By default, the `SilverStripe\Security\MemberAuthenticator\MemberAuthenticator` is seen as the default authenticator until it's explicitly set in the config. By default, the `SilverStripe\Security\MemberAuthenticator\MemberAuthenticator` is seen as the default authenticator until it's explicitly set in the config.
Every Authenticator is expected to handle services. The `Authenticator` Interface provides the available services: Every Authenticator is expected to handle services. The `Authenticator` Interface provides the available services:
```php ```php
const LOGIN = 1;
const LOGOUT = 2;
const CHANGE_PASSWORD = 4;
const RESET_PASSWORD = 8;
const CMS_LOGIN = 16;
const LOGIN = 1; /**
const LOGOUT = 2; * Returns the services supported by this authenticator
const CHANGE_PASSWORD = 4; *
const RESET_PASSWORD = 8; * The number should be a bitwise-OR of 1 or more of the following constants:
const CMS_LOGIN = 16; * Authenticator::LOGIN, Authenticator::LOGOUT, Authenticator::CHANGE_PASSWORD,
* Authenticator::RESET_PASSWORD, or Authenticator::CMS_LOGIN
/** *
* Returns the services supported by this authenticator * @return int
* */
* The number should be a bitwise-OR of 1 or more of the following constants: public function supportedServices();
* Authenticator::LOGIN, Authenticator::LOGOUT, Authenticator::CHANGE_PASSWORD,
* Authenticator::RESET_PASSWORD, or Authenticator::CMS_LOGIN
*
* @return int
*/
public function supportedServices();
``` ```
If there is no available authenticator for the required action (either one of the constants above), an error will be thrown. If there is no available authenticator for the required action (either one of the constants above), an error will be thrown.

View File

@ -25,36 +25,42 @@ must still be taken when working with literal values or table/column identifiers
come from user input. come from user input.
Example: Example:
```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();
```php
use SilverStripe\ORM\DB;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Queries\SQLSelect;
$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 Parameterised updates and inserts are also supported, but the syntax is a little different
```php ```php
SQLInsert::create('"MyClass"') use SilverStripe\ORM\Queries\SQLInsert;
->assign('"Name"', 'Daniel') use SilverStripe\ORM\DB;
->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]
);
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 ### Automatic escaping
@ -79,16 +85,18 @@ Data is not escaped when writing to object-properties, as inserts and updates ar
handled via prepared statements. handled via prepared statements.
Example: Example:
```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)));
```php
use SilverStripe\Security\Member;
// 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'> <div class="warning" markdown='1'>
@ -114,19 +122,19 @@ Example:
```php ```php
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
class MyForm extends Form class MyForm extends Form
{
public function save($RAW_data, $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
// Pass true as the second parameter of raw2sql to quote the value safely $objs = Player::get()->where("Name = " . $SQL_data['name']);
$SQL_data = Convert::raw2sql($RAW_data, true); // works recursively on an array
$objs = Player::get()->where("Name = " . $SQL_data['name']);
// ... // ...
}
} }
}
``` ```
@ -135,23 +143,21 @@ Example:
Example: Example:
```php ```php
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends Controller class MyController extends Controller
{
private static $allowed_actions = ['myurlaction'];
public function myurlaction($RAW_urlParams)
{ {
private static $allowed_actions = ['myurlaction']; // Pass true as the second parameter of raw2sql to quote the value safely
public function myurlaction($RAW_urlParams) $SQL_urlParams = Convert::raw2sql($RAW_urlParams, true); // works recursively on an array
{ $objs = Player::get()->where("Name = " . $SQL_data['OtherID']);
// 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** As a rule of thumb, you should escape your data **as close to querying as possible**
@ -160,28 +166,27 @@ passing data through, escaping should happen at the end of the chain.
```php ```php
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends Controller class MyController extends Controller
{
/**
* @param array $RAW_data All names in an indexed array (not SQL-safe)
*/
public function saveAllNames($RAW_data)
{ {
/**
* @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 // $SQL_data = Convert::raw2sql($RAW_data); // premature escaping
foreach($RAW_data as $item) $this->saveName($item); 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}");
}
} }
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 This might not be applicable in all cases - especially if you are building an API thats likely to be customised. If
@ -216,9 +221,11 @@ stripped out
To enable filtering, set the HtmlEditorField::$sanitise_server_side [configuration](/developer_guides/configuration/configuration) property to To enable filtering, set the HtmlEditorField::$sanitise_server_side [configuration](/developer_guides/configuration/configuration) property to
true, e.g. 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 The built in sanitiser enforces the TinyMCE whitelist rules on the server side, and is sufficient to eliminate the
most common XSS vectors. most common XSS vectors.
@ -248,15 +255,15 @@ PHP:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyObject extends DataObject class MyObject extends DataObject
{ {
private static $db = [ private static $db = [
'MyEscapedValue' => 'Text', // Example value: <b>not bold</b> 'MyEscapedValue' => 'Text', // Example value: <b>not bold</b>
'MyUnescapedValue' => 'HTMLText' // Example value: <b>bold</b> 'MyUnescapedValue' => 'HTMLText' // Example value: <b>bold</b>
]; ];
} }
``` ```
@ -264,10 +271,10 @@ Template:
```php ```php
<ul> <ul>
<li>$MyEscapedValue</li> // output: &lt;b&gt;not bold&lt;b&gt; <li>$MyEscapedValue</li> // output: &lt;b&gt;not bold&lt;b&gt;
<li>$MyUnescapedValue</li> // output: <b>bold</b> <li>$MyUnescapedValue</li> // output: <b>bold</b>
</ul> </ul>
``` ```
The example below assumes that data wasn't properly filtered when saving to the database, but are escaped before The example below assumes that data wasn't properly filtered when saving to the database, but are escaped before
@ -282,13 +289,13 @@ Template (see above):
```php ```php
<ul> <ul>
// output: <a href="#" title="foo &amp; &#quot;bar&quot;">foo &amp; "bar"</a> // output: <a href="#" title="foo &amp; &#quot;bar&quot;">foo &amp; "bar"</a>
<li><a href="#" title="$Title.ATT">$Title</a></li> <li><a href="#" title="$Title.ATT">$Title</a></li>
<li>$MyEscapedValue</li> // output: &lt;b&gt;not bold&lt;b&gt; <li>$MyEscapedValue</li> // output: &lt;b&gt;not bold&lt;b&gt;
<li>$MyUnescapedValue</li> // output: <b>bold</b> <li>$MyUnescapedValue</li> // output: <b>bold</b>
<li>$MyUnescapedValue.XML</li> // output: &lt;b&gt;bold&lt;b&gt; <li>$MyUnescapedValue.XML</li> // output: &lt;b&gt;bold&lt;b&gt;
</ul> </ul>
``` ```
### Escaping custom attributes and getters ### Escaping custom attributes and getters
@ -298,37 +305,35 @@ static *$casting* array. Caution: Casting only applies when using values in a te
PHP: PHP:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyObject extends 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)
{ {
public $Title = '<b>not bold</b>'; // will be escaped due to Text casting // $this->Title is not casted in PHP
return $this->Title . '<small>(' . $suffix. ')</small>';
$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: Template:
```php ```php
<ul> <ul>
<li>$Title</li> // output: &lt;b&gt;not bold&lt;b&gt; <li>$Title</li> // output: &lt;b&gt;not bold&lt;b&gt;
<li>$Title.RAW</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> <li>$TitleWithHTMLSuffix</li> // output: <b>not bold</b>: <small>(...)</small>
</ul> </ul>
``` ```
Note: Avoid generating HTML by string concatenation in PHP wherever possible to minimize risk and separate your Note: Avoid generating HTML by string concatenation in PHP wherever possible to minimize risk and separate your
@ -344,33 +349,30 @@ also used by *XML* and *ATT* in template code).
PHP: PHP:
```php ```php
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\ORM\FieldType\DBText; use SilverStripe\ORM\FieldType\DBText;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
class MyController extends Controller class MyController extends Controller
{
private static $allowed_actions = ['search'];
public function search($request)
{ {
private static $allowed_actions = ['search']; $htmlTitle = '<p>Your results for:' . Convert::raw2xml($request->getVar('Query')) . '</p>';
public function search($request) return $this->customise([
{ 'Query' => DBText::create($request->getVar('Query')),
$htmlTitle = '<p>Your results for:' . Convert::raw2xml($request->getVar('Query')) . '</p>'; 'HTMLTitle' => DBHTMLText::create($htmlTitle)
return $this->customise([ ]);
'Query' => DBText::create($request->getVar('Query')),
'HTMLTitle' => DBHTMLText::create($htmlTitle)
]);
}
} }
}
``` ```
Template: Template:
```php ```php
<h2 title="Searching for $Query.ATT">$HTMLTitle</h2> <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. Whenever you insert a variable into an HTML attribute within a template, use $VarName.ATT, no not $VarName.
@ -386,29 +388,28 @@ PHP:
```php ```php
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\ORM\FieldType\DBText; use SilverStripe\ORM\FieldType\DBText;
class MyController extends Controller class MyController extends Controller
{
private static $allowed_actions = ['search'];
public function search($request)
{ {
private static $allowed_actions = ['search']; $rssRelativeLink = "/rss?Query=" . urlencode($_REQUEST['query']) . "&sortOrder=asc";
public function search($request) $rssLink = Controller::join_links($this->Link(), $rssRelativeLink);
{ return $this->customise([
$rssRelativeLink = "/rss?Query=" . urlencode($_REQUEST['query']) . "&sortOrder=asc"; "RSSLink" => DBText::create($rssLink),
$rssLink = Controller::join_links($this->Link(), $rssRelativeLink); ]);
return $this->customise([
"RSSLink" => DBText::create($rssLink),
]);
}
} }
}
``` ```
Template: Template:
```php ```php
<a href="$RSSLink.ATT">RSS feed</a> <a href="$RSSLink.ATT">RSS feed</a>
``` ```
Some rules of thumb: Some rules of thumb:
@ -457,21 +458,21 @@ Below is an example with different ways you would use this casting technique:
```php ```php
public function CaseStudies() public function CaseStudies()
{ {
// cast an ID from URL parameters e.g. (mysite.com/home/action/ID)
$anotherID = (int)Director::urlParam['ID'];
// cast an ID from URL parameters e.g. (mysite.com/home/action/ID) // perform a calculation, the prerequisite being $anotherID must be an integer
$anotherID = (int)Director::urlParam['ID']; $calc = $anotherID + (5 - 2) / 2;
// perform a calculation, the prerequisite being $anotherID must be an integer // cast the 'category' GET variable as an integer
$calc = $anotherID + (5 - 2) / 2; $categoryID = (int)$_GET['category'];
// cast the 'category' GET variable as an integer // perform a byID(), which ensures the ID is an integer before querying
$categoryID = (int)$_GET['category']; return CaseStudy::get()->byID($categoryID);
}
// 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 The same technique can be employed anywhere in your PHP code you know something must be of a certain type. A list of PHP
@ -503,13 +504,14 @@ with a `.yml` or `.yaml` extension through the default web server rewriting rule
If you need users to access files with this extension, If you need users to access files with this extension,
you can bypass the rules for a specific directory. you can bypass the rules for a specific directory.
Here's an example for a `.htaccess` file used by the Apache web server: 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 ### User uploaded files
Certain file types are by default excluded from user upload. html, xhtml, htm, and xml files may have embedded, Certain file types are by default excluded from user upload. html, xhtml, htm, and xml files may have embedded,
@ -531,7 +533,6 @@ take the following precautions:
[Cookie Law and Flash Cookies](http://eucookiedirective.com/cookie-law-and-flash-cookies/). [Cookie Law and Flash Cookies](http://eucookiedirective.com/cookie-law-and-flash-cookies/).
See [the Adobe Flash security page](http://www.adobe.com/devnet/flashplayer/security.html) for more information. See [the Adobe Flash security page](http://www.adobe.com/devnet/flashplayer/security.html) for more information.
ADMIN privileged users may be allowed to override the above upload restrictions if the ADMIN privileged users may be allowed to override the above upload restrictions if the
`File.apply_restrictions_to_admin` config is set to false. By default this is true, which enforces these `File.apply_restrictions_to_admin` config is set to false. By default this is true, which enforces these
@ -555,12 +556,14 @@ a [PasswordValidator](api:SilverStripe\Security\PasswordValidator):
```php ```php
$validator = new PasswordValidator(); use SilverStripe\Security\Member;
$validator->minLength(7); use SilverStripe\Security\PasswordValidator;
$validator->checkHistoricalPasswords(6);
$validator->characterStrength(3, ["lowercase", "uppercase", "digits", "punctuation"]);
Member::set_password_validator($validator);
$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: In addition, you can tighten password security with the following configuration settings:
@ -585,16 +588,16 @@ controller's `init()` method:
```php ```php
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends Controller class MyController extends Controller
{
public function init()
{ {
public function init() parent::init();
{ $this->getResponse()->addHeader('X-Frame-Options', 'SAMEORIGIN');
parent::init();
$this->getResponse()->addHeader('X-Frame-Options', 'SAMEORIGIN');
}
} }
}
``` ```
This is a recommended option to secure any controller which displays This is a recommended option to secure any controller which displays
@ -607,9 +610,11 @@ 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 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 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: _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.) Please note that if this configuration is defined, you _must_ include _all_ subdomains (eg www.)
that will be accessing the site. that will be accessing the site.
@ -624,24 +629,27 @@ into visiting external sites.
In order to prevent this kind of attack, it's necessary to whitelist trusted proxy 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`. 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 If you wish to change the headers that are used to find the proxy information, you should reconfigure the
TrustedProxyMiddleware service: TrustedProxyMiddleware service:
```yml ```yml
SilverStripe\Control\TrustedProxyMiddleware:
properties:
ProxyHostHeaders: X-Forwarded-Host
ProxySchemeHeaders: X-Forwarded-Protocol
ProxyIPHeaders: X-Forwarded-Ip
```
SilverStripe\Control\TrustedProxyMiddleware: ```
properties: SS_TRUSTED_PROXY_HOST_HEADER="HTTP_X_FORWARDED_HOST"
ProxyHostHeaders: X-Forwarded-Host SS_TRUSTED_PROXY_IP_HEADER="HTTP_X_FORWARDED_FOR"
ProxySchemeHeaders: X-Forwarded-Protocol SS_TRUSTED_PROXY_PROTOCOL_HEADER="HTTP_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"
``` ```
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. 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.
@ -655,12 +663,12 @@ This behaviour is enabled whenever `SS_TRUSTED_PROXY_IPS` is defined, or if the
following in your .htaccess to ensure this behaviour is activated. following in your .htaccess to ensure this behaviour is activated.
``` ```
<IfModule mod_env.c> <IfModule mod_env.c>
# Ensure that X-Forwarded-Host is only allowed to determine the request # 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 # 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. # Note that in a future release this setting will be always on.
SetEnv BlockUntrustedIPs true SetEnv BlockUntrustedIPs true
</IfModule> </IfModule>
``` ```
In a future release this behaviour will be changed to be on by default, and this environment In a future release this behaviour will be changed to be on by default, and this environment
@ -671,10 +679,13 @@ 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 SilverStripe recommends the use of TLS(HTTPS) for your application, and you can easily force the use through the
director function `forceSSL()` director function `forceSSL()`
```php ```php
if (!Director::isDev()) { use SilverStripe\Control\Director;
Director::forceSSL();
} if (!Director::isDev()) {
Director::forceSSL();
}
``` ```
Forcing HTTPS so requires a certificate to be purchased or obtained through a vendor such as Forcing HTTPS so requires a certificate to be purchased or obtained through a vendor such as
@ -683,10 +694,10 @@ 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 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). 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` To do this, you may set the `cookie_secure` parameter to `true` in your `config.yml` for `Session`
```yml
Session: ```yml
cookie_secure: true SilverStripe\Control\Session:
cookie_secure: true
``` ```
For other cookies set by your application we should also ensure the users are provided with secure cookies by setting For other cookies set by your application we should also ensure the users are provided with secure cookies by setting
@ -699,11 +710,13 @@ 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 - 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 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. as this prevents an attacker who achieves cross-site scripting from accessing these cookies.
```php ```php
use SilverStripe\Control\Cookie;
Cookie::set('cookie-name', 'chocolate-chip', $expiry = 30, $path = null, $domain = null, $secure = true,
$httpOnly = false Cookie::set('cookie-name', 'chocolate-chip', $expiry = 30, $path = null, $domain = null, $secure = true,
); $httpOnly = false
);
``` ```
## Security Headers ## Security Headers
@ -715,9 +728,9 @@ ensuring an HTTPS connection will provide a better and more secure user experien
- The `Strict-Transport-Security` header instructs the browser to record that the website and assets on that website - The `Strict-Transport-Security` header instructs the browser to record that the website and assets on that website
MUST use a secure connection. This prevents websites from becoming insecure in the future from stray absolute links MUST use a secure connection. This prevents websites from becoming insecure in the future from stray absolute links
or references without https from external sites. Check if your browser supports [HSTS](https://hsts.badssl.com/) or references without https from external sites. Check if your browser supports [HSTS](https://hsts.badssl.com/)
- `max-age` can be configured to anything in seconds: `max-age=31536000` (1 year), for roll out, consider something - `max-age` can be configured to anything in seconds: `max-age=31536000` (1 year), for roll out, consider something
lower lower
- `includeSubDomains` to ensure all present and future sub domains will also be HTTPS - `includeSubDomains` to ensure all present and future sub domains will also be HTTPS
For sensitive pages, such as members areas, or places where sensitive information is present, adding cache control For sensitive pages, such as members areas, or places where sensitive information is present, adding cache control
headers can explicitly instruct browsers not to keep a local cached copy of content and can prevent content from headers can explicitly instruct browsers not to keep a local cached copy of content and can prevent content from
@ -729,27 +742,27 @@ unauthorised local persons. SilverStripe adds the current date for every request
headers to the request for our secure controllers: headers to the request for our secure controllers:
```php ```php
use SilverStripe\Control\HTTP; use SilverStripe\Control\HTTP;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MySecureController extends Controller class MySecureController extends Controller
{
public function init()
{ {
parent::init();
public function init() // Add cache headers to ensure sensitive content isn't cached.
{ $this->response->addHeader('Cache-Control', 'max-age=0, must-revalidate, no-transform');
parent::init(); $this->response->addHeader('Pragma', 'no-cache'); // for HTTP 1.0 support
// 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::set_cache_age(0);
HTTP::add_cache_headers($this->response); HTTP::add_cache_headers($this->response);
// Add HSTS header to force TLS for document content // Add HSTS header to force TLS for document content
$this->response->addHeader('Strict-Transport-Security', 'max-age=86400; includeSubDomains'); $this->response->addHeader('Strict-Transport-Security', 'max-age=86400; includeSubDomains');
}
} }
}
``` ```
## Related ## Related

View File

@ -24,8 +24,10 @@ SilverStripe\Core\Injector\Injector:
```php ```php
$email = new Email($from, $to, $subject, $body); use SilverStripe\Control\Email\Email;
$email->sendPlain();
$email = new Email($from, $to, $subject, $body);
$email->sendPlain();
``` ```
### Sending combined HTML and plain text ### Sending combined HTML and plain text
@ -36,8 +38,8 @@ to `*text*`).
```php ```php
$email = new Email($from, $to, $subject, $body); $email = new Email($from, $to, $subject, $body);
$email->send(); $email->send();
``` ```
<div class="info" markdown="1"> <div class="info" markdown="1">
@ -56,9 +58,8 @@ email object additional information using the `setData` and `addData` methods.
```ss ```ss
<h1>Hi $Member.FirstName</h1>
<h1>Hi $Member.FirstName</h1> <p>You can go to $Link.</p>
<p>You can go to $Link.</p>
``` ```
The PHP Logic.. The PHP Logic..
@ -107,10 +108,8 @@ You can set the default sender address of emails through the `Email.admin_email`
```yaml ```yaml
SilverStripe\Control\Email\Email:
SilverStripe\Control\Email\Email: admin_email: support@silverstripe.org
admin_email: support@silverstripe.org
``` ```
<div class="alert" markdown="1"> <div class="alert" markdown="1">
@ -133,13 +132,14 @@ Configuration of those properties looks like the following:
**mysite/_config.php** **mysite/_config.php**
```php ```php
if(Director::isLive()) { use SilverStripe\Control\Director;
Config::inst()->update('Email', 'bcc_all_emails_to', "client@example.com"); use SilverStripe\Core\Config\Config;
} else { if(Director::isLive()) {
Config::inst()->update('Email', 'send_all_emails_to', "developer@example.com"); 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. ### Setting custom "Reply To" email address.
@ -148,9 +148,10 @@ For email messages that should have an email address which is replied to that ac
email, do the following. This is encouraged especially when the domain responsible for sending the message isn't 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 necessarily the same which should be used for return correspondence and should help prevent your message from being
marked as spam. marked as spam.
```php ```php
$email = new Email(..); $email = new Email(..);
$email->setReplyTo('me@address.com'); $email->setReplyTo('me@address.com');
``` ```
### Setting Custom Headers ### Setting Custom Headers
@ -160,9 +161,8 @@ For email headers which do not have getters or setters (like setTo(), setFrom())
```php ```php
$email = new Email(...); $email = new Email(...);
$email->getSwiftMessage()->getHeaders()->addTextHeader('HeaderName', 'HeaderValue'); $email->getSwiftMessage()->getHeaders()->addTextHeader('HeaderName', 'HeaderValue');
..
``` ```
<div class="info" markdown="1"> <div class="info" markdown="1">

View File

@ -28,16 +28,19 @@ You can use the CsvBulkLoader without subclassing or other customizations, if th
in your CSV file match `$db` properties in your dataobject. E.g. a simple import for the 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: [Member](api:SilverStripe\Security\Member) class could have this data in a file:
``` ```
FirstName,LastName,Email FirstName,LastName,Email
Donald,Duck,donald@disney.com Donald,Duck,donald@disney.com
Daisy,Duck,daisy@disney.com Daisy,Duck,daisy@disney.com
``` ```
The loader would be triggered through the `load()` method: The loader would be triggered through the `load()` method:
```php ```php
$loader = new CsvBulkLoader('Member'); use SilverStripe\Dev\CsvBulkLoader;
$result = $loader->load('<my-file-path>');
$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` By the way, you can import [Member](api:SilverStripe\Security\Member) and [Group](api:SilverStripe\Security\Group) data through `http://localhost/admin/security`
@ -49,20 +52,18 @@ The simplest way to use [CsvBulkLoader](api:SilverStripe\Dev\CsvBulkLoader) is t
```php ```php
use SilverStripe\Admin\ModelAdmin; use SilverStripe\Admin\ModelAdmin;
class PlayerAdmin extends ModelAdmin
{
private static $managed_models = [
'Player'
];
private static $model_importers = [
'Player' => 'CsvBulkLoader',
];
private static $url_segment = 'players';
}
?>
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 The new admin interface will be available under `http://localhost/admin/players`, the import form is located
@ -77,57 +78,56 @@ You'll need to add a route to your controller to make it accessible via URL
```php ```php
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FileField; use SilverStripe\Forms\FileField;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\RequiredFields;
use SilverStripe\Dev\CsvBulkLoader; use SilverStripe\Dev\CsvBulkLoader;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends Controller class MyController extends Controller
{
private static $allowed_actions = ['Form'];
protected $template = "BlankPage";
public function Link($action = null)
{ {
return Controller::join_links('MyController', $action);
private static $allowed_actions = ['Form'];
protected $template = "BlankPage";
public function Link($action = null)
{
return Controller::join_links('MyController', $action);
}
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();
}
} }
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 Note: This interface is not secured, consider using [Permission::check()](api:SilverStripe\Security\Permission::check()) to limit the controller to users
@ -139,51 +139,46 @@ We're going to use our knowledge from the previous example to import a more soph
Sample CSV Content Sample CSV Content
``` ```
"Number","Name","Birthday","Team" "Number","Name","Birthday","Team"
11,"John Doe",1982-05-12,"FC Bayern" 11,"John Doe",1982-05-12,"FC Bayern"
12,"Jane Johnson", 1982-05-12,"FC Bayern" 12,"Jane Johnson", 1982-05-12,"FC Bayern"
13,"Jimmy Dole",,"Schalke 04" 13,"Jimmy Dole",,"Schalke 04"
``` ```
Datamodel for Player Datamodel for Player
```php ```php
use SilverStripe\ORM\DataObject; 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'
];
}
?>
class Player extends DataObject
{
private static $db = [
'PlayerNumber' => 'Int',
'FirstName' => 'Text',
'LastName' => 'Text',
'Birthday' => 'Date',
];
private static $has_one = [
'Team' => 'FootballTeam'
];
}
``` ```
Datamodel for FootballTeam: Datamodel for FootballTeam:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class FootballTeam extends DataObject
{
private static $db = [
'Title' => 'Text',
];
private static $has_many = [
'Players' => 'Player'
];
}
?>
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). Sample implementation of a custom loader. Assumes a CSV-file in a certain format (see below).
@ -192,60 +187,57 @@ 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 * 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 * Avoids duplicate imports by a custom `$duplicateChecks` definition
* Creates `Team` relations automatically based on the `Gruppe` column in the CSV data * Creates `Team` relations automatically based on the `Gruppe` column in the CSV data
```php ```php
use SilverStripe\Dev\CsvBulkLoader; 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();
}
}
?>
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();
}
}
``` ```
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`. 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
use SilverStripe\Admin\ModelAdmin; use SilverStripe\Admin\ModelAdmin;
class PlayerAdmin extends ModelAdmin
{
private static $managed_models = [
'Player'
];
private static $model_importers = [
'Player' => 'PlayerCsvBulkLoader',
];
private static $url_segment = 'players';
}
?>
class PlayerAdmin extends ModelAdmin
{
private static $managed_models = [
'Player'
];
private static $model_importers = [
'Player' => 'PlayerCsvBulkLoader',
];
private static $url_segment = 'players';
}
``` ```
## Related ## Related

View File

@ -24,25 +24,28 @@ An outline of step one looks like:
```php ```php
$feed = new RSSFeed( use SilverStripe\Control\RSS\RSSFeed;
$list,
$link,
$title,
$description,
$titleField,
$descriptionField,
$authorField,
$lastModifiedTime,
$etag
);
$feed->outputToBrowser(); $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 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. will normally go in your `Controllers` `init` method.
```php ```php
RSSFeed::linkToFeed($link, $title); RSSFeed::linkToFeed($link, $title);
``` ```
## Examples ## Examples
@ -54,46 +57,41 @@ You can use [RSSFeed](api:SilverStripe\Control\RSS\RSSFeed) to easily create a f
**mysite/code/Page.php** **mysite/code/Page.php**
```php ```php
use SilverStripe\Control\RSS\RSSFeed; use SilverStripe\Control\RSS\RSSFeed;
use Page; use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\CMS\Controllers\ContentController;
class PageController extends ContentController
.. {
class PageController extends ContentController private static $allowed_actions = [
'rss'
];
public function init()
{ {
parent::init();
private static $allowed_actions = [ RSSFeed::linkToFeed($this->Link() . "rss", "10 Most Recently Updated Pages");
'rss'
];
public function init()
{
parent::init();
RSSFeed::linkToFeed($this->Link() . "rss", "10 Most Recently Updated Pages");
}
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."
);
return $rss->outputToBrowser();
}
public function LatestUpdates()
{
return Page::get()->sort("LastEdited", "DESC")->limit(10);
}
} }
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."
);
return $rss->outputToBrowser();
}
public function LatestUpdates()
{
return Page::get()->sort("LastEdited", "DESC")->limit(10);
}
}
``` ```
### Rendering DataObjects in a RSSFeed ### Rendering DataObjects in a RSSFeed
@ -110,59 +108,58 @@ method is defined and returns a string to the full website URL.
```php ```php
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject class Player extends DataObject
{
public function AbsoluteLink()
{ {
// assumes players can be accessed at yoursite.com/players/2
public function AbsoluteLink() return Controller::join_links(
{ Director::absoluteBaseUrl(),
// assumes players can be accessed at yoursite.com/players/2 'players',
$this->ID
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`. Then in our controller, we add a new action which returns a the XML list of `Players`.
```php ```php
use SilverStripe\Control\RSS\RSSFeed; use SilverStripe\Control\RSS\RSSFeed;
use SilverStripe\CMS\Controllers\ContentController; use SilverStripe\CMS\Controllers\ContentController;
class PageController extends ContentController class PageController extends ContentController
{
private static $allowed_actions = [
'players'
];
public function init()
{ {
parent::init();
private static $allowed_actions = [ RSSFeed::linkToFeed($this->Link("players"), "Players");
'players'
];
public function init()
{
parent::init();
RSSFeed::linkToFeed($this->Link("players"), "Players");
}
public function players()
{
$rss = new RSSFeed(
Player::get(),
$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 ### Customizing the RSS Feed template
@ -174,45 +171,44 @@ Say from that last example we want to include the Players Team in the XML feed w
**mysite/templates/PlayersRss.ss** **mysite/templates/PlayersRss.ss**
```xml ```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>
<?xml version="1.0"?> <% loop $Entries %>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"> <item>
<channel> <title>$Title.XML</title>
<title>$Title</title> <team>$Team.Title</team>
<link>$Link</link> </item>
<atom:link href="$Link" rel="self" type="application/rss+xml" /> <% end_loop %>
<description>$Description.XML</description> </channel>
</rss>
<% 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. `setTemplate` can then be used to tell RSSFeed to use that new template.
**mysite/code/Page.php** **mysite/code/Page.php**
```php ```php
public function players() use SilverStripe\Control\RSS\RSSFeed;
{
$rss = new RSSFeed(
Player::get(),
$this->Link("players"),
"Players"
);
$rss->setTemplate('PlayersRss');
return $rss->outputToBrowser(); public function players()
} {
$rss = new RSSFeed(
Player::get(),
$this->Link("players"),
"Players"
);
$rss->setTemplate('PlayersRss');
return $rss->outputToBrowser();
}
``` ```
<div class="warning"> <div class="warning">

View File

@ -8,72 +8,71 @@ form (which is used for `MyDataObject` instances). You can access it through
```php ```php
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FileField; use SilverStripe\Forms\FileField;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\RequiredFields;
use SilverStripe\Dev\CsvBulkLoader; use SilverStripe\Dev\CsvBulkLoader;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyController extends Controller class MyController extends Controller
{
private static $allowed_actions = [
'Form'
];
protected $template = "BlankPage";
public function Link($action = null)
{ {
return Controller::join_links('MyController', $action);
private static $allowed_actions = [
'Form'
];
protected $template = "BlankPage";
public function Link($action = null)
{
return Controller::join_links('MyController', $action);
}
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();
}
} }
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();
}
}
``` ```
<div class="alert" markdown="1"> <div class="alert" markdown="1">

View File

@ -4,12 +4,14 @@ title: A custom CSVBulkLoader instance
A an implementation of a custom `CSVBulkLoader` loader. In this example. we're provided with a unique CSV file 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. 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" "SpielerNummer", "Name", "Geburtsdatum", "Gruppe"
11, "John Doe", 1982-05-12,"FC Bayern" 11, "John Doe", 1982-05-12,"FC Bayern"
12, "Jane Johnson", 1982-05-12,"FC Bayern" 12, "Jane Johnson", 1982-05-12,"FC Bayern"
13, "Jimmy Dole",,"Schalke 04" 13, "Jimmy Dole",,"Schalke 04"
``` ```
This data needs to be imported into our application. For this, we have two `DataObjects` setup. `Player` contains 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`. information about the individual player and a relation set up for managing the `Team`.
@ -17,43 +19,40 @@ information about the individual player and a relation set up for managing the `
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Player extends DataObject class Player extends DataObject
{ {
private static $db = [
'PlayerNumber' => 'Int',
'FirstName' => 'Text',
'LastName' => 'Text',
'Birthday' => 'Date'
];
private static $has_one = [
'Team' => 'FootballTeam'
];
}
private static $db = [
'PlayerNumber' => 'Int',
'FirstName' => 'Text',
'LastName' => 'Text',
'Birthday' => 'Date'
];
private static $has_one = [
'Team' => 'FootballTeam'
];
}
``` ```
**mysite/code/FootballTeam.php** **mysite/code/FootballTeam.php**
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class FootballTeam extends DataObject class FootballTeam extends DataObject
{ {
private static $db = [
private static $db = [ 'Title' => 'Text'
'Title' => 'Text' ];
];
private static $has_many = [
'Players' => 'Player'
];
}
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 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
@ -72,43 +71,42 @@ Our final import looks like this.
```php ```php
use SilverStripe\Dev\CsvBulkLoader; use SilverStripe\Dev\CsvBulkLoader;
class PlayerCsvBulkLoader extends CsvBulkLoader class PlayerCsvBulkLoader extends CsvBulkLoader
{ {
public $columnMap = [ public $columnMap = [
'Number' => 'PlayerNumber', 'Number' => 'PlayerNumber',
'Name' => '->importFirstAndLastName', 'Name' => '->importFirstAndLastName',
'Geburtsdatum' => 'Birthday', 'Geburtsdatum' => 'Birthday',
'Gruppe' => 'Team.Title', 'Gruppe' => 'Team.Title',
]; ];
public $duplicateChecks = [ public $duplicateChecks = [
'SpielerNummer' => 'PlayerNumber' 'SpielerNummer' => 'PlayerNumber'
]; ];
public $relationCallbacks = [ public $relationCallbacks = [
'Team.Title' => [ 'Team.Title' => [
'relationname' => 'Team', 'relationname' => 'Team',
'callback' => 'getTeamByTitle' 'callback' => 'getTeamByTitle'
] ]
]; ];
public static function importFirstAndLastName(&$obj, $val, $record) public static function importFirstAndLastName(&$obj, $val, $record)
{ {
$parts = explode(' ', $val); $parts = explode(' ', $val);
if(count($parts) != 2) return false; if(count($parts) != 2) return false;
$obj->FirstName = $parts[0]; $obj->FirstName = $parts[0];
$obj->LastName = $parts[1]; $obj->LastName = $parts[1];
} }
public static function getTeamByTitle(&$obj, $val, $record)
{
return FootballTeam::get()->filter('Title', $val)->First();
}
}
public static function getTeamByTitle(&$obj, $val, $record)
{
return FootballTeam::get()->filter('Title', $val)->First();
}
}
``` ```
## Related ## Related

View File

@ -1,64 +0,0 @@
title: Embed an RSS Feed
# Embed an RSS Feed
[RestfulService](api:RestfulService) can be used to easily embed an RSS feed from a site. In this How to we'll embed the latest
weather information from the Yahoo Weather API.
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([
'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([
'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 %>
```
## Related
* [RestfulService Documentation](../restfulservice)
* [RestfulService](api:RestfulService)

View File

@ -20,16 +20,15 @@ Defining search-able fields on your DataObject.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyDataObject extends DataObject class MyDataObject extends DataObject
{ {
private static $searchable_fields = [
private static $searchable_fields = [ 'Name',
'Name', 'ProductCode'
'ProductCode' ];
]; }
}
``` ```
@ -41,39 +40,38 @@ and `MyDate`. The attribute `HiddenProperty` should not be searchable, and `MyDa
```php ```php
use SilverStripe\ORM\Filters\PartialMatchFilter; use SilverStripe\ORM\Filters\PartialMatchFilter;
use SilverStripe\ORM\Filters\GreaterThanFilter; use SilverStripe\ORM\Filters\GreaterThanFilter;
use SilverStripe\ORM\Search\SearchContext; use SilverStripe\ORM\Search\SearchContext;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyDataObject extends DataObject class MyDataObject extends DataObject
{
private static $db = [
'PublicProperty' => 'Text'
'HiddenProperty' => 'Text',
'MyDate' => 'Date'
];
public function getDefaultSearchContext()
{ {
$fields = $this->scaffoldSearchFields([
'restrictFields' => ['PublicProperty','MyDate']
]);
private static $db = [ $filters = [
'PublicProperty' => 'Text' 'PublicProperty' => new PartialMatchFilter('PublicProperty'),
'HiddenProperty' => 'Text', 'MyDate' => new GreaterThanFilter('MyDate')
'MyDate' => 'Date'
]; ];
public function getDefaultSearchContext()
{
$fields = $this->scaffoldSearchFields([
'restrictFields' => ['PublicProperty','MyDate']
]);
$filters = [ return new SearchContext(
'PublicProperty' => new PartialMatchFilter('PublicProperty'), $this->class,
'MyDate' => new GreaterThanFilter('MyDate') $fields,
]; $filters
);
return new SearchContext(
$this->class,
$fields,
$filters
);
}
} }
}
``` ```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
@ -90,42 +88,41 @@ the `$fields` constructor parameter.
```php ```php
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\CMS\Controllers\ContentController; 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() $form = new Form($this, "SearchForm",
{ $fields,
$context = singleton('MyDataObject')->getCustomSearchContext(); new FieldList(
$fields = $context->getSearchFields(); new FormAction('doSearch')
)
);
$form = new Form($this, "SearchForm", return $form;
$fields,
new FieldList(
new FormAction('doSearch')
)
);
return $form;
}
public function doSearch($data, $form)
{
$context = singleton('MyDataObject')->getCustomSearchContext();
$results = $context->getResults($data);
return $this->customise([
'Results' => $results
])->renderWith('Page_results');
}
} }
public function doSearch($data, $form)
{
$context = singleton('MyDataObject')->getCustomSearchContext();
$results = $context->getResults($data);
return $this->customise([
'Results' => $results
])->renderWith('Page_results');
}
}
``` ```
### Pagination ### Pagination
@ -137,40 +134,40 @@ in order to read page limit information. It is also passed the current
```php ```php
public function getResults($searchCriteria = []) use SilverStripe\ORM\PaginatedList;
{
$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;
}
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: notice that if you want to use this getResults function, you need to change the function doSearch for this one:
```php ```php
public function doSearch($data, $form) public function doSearch($data, $form)
{ {
$context = singleton('MyDataObject')->getCustomSearchContext(); $context = singleton('MyDataObject')->getCustomSearchContext();
$results = $this->getResults($data); $results = $this->getResults($data);
return $this->customise([ return $this->customise([
'Results' => $results 'Results' => $results
])->renderWith(['Catalogo_results', 'Page']); ])->renderWith(['Catalogo_results', 'Page']);
} }
``` ```
The change is in **$results = $this->getResults($data);**, because you are using a custom getResults function. The change is in **$results = $this->getResults($data);**, because you are using a custom getResults function.
@ -187,45 +184,45 @@ 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: Results.PaginationSummary(4) defines how many pages the search will show in the search results. something like:
**Next 1 2 *3* 4 5 &hellip; 558** **Next 1 2 *3* 4 5 &hellip; 558**
```ss
<% if $Results %> ```ss
<ul> <% if $Results %>
<% loop $Results %> <ul>
<li>$Title, $Autor</li> <% loop $Results %>
<% end_loop %> <li>$Title, $Autor</li>
</ul> <% end_loop %>
<% else %> </ul>
<p>Sorry, your search query did not return any results.</p> <% else %>
<% end_if %> <p>Sorry, your search query did not return any results.</p>
<% end_if %>
<% if $Results.MoreThanOnePage %>
<div id="PageNumbers"> <% if $Results.MoreThanOnePage %>
<p> <div id="PageNumbers">
<% if $Results.NotFirstPage %> <p>
<a class="prev" href="$Results.PrevLink" title="View the previous page">Prev</a> <% if $Results.NotFirstPage %>
<% end_if %> <a class="prev" href="$Results.PrevLink" title="View the previous page">Prev</a>
<% end_if %>
<span>
<% loop $Results.PaginationSummary(4) %> <span>
<% if $CurrentBool %> <% loop $Results.PaginationSummary(4) %>
$PageNum <% if $CurrentBool %>
$PageNum
<% else %>
<% if $Link %>
<a href="$Link" title="View page number $PageNum">$PageNum</a>
<% else %> <% else %>
<% if $Link %> &hellip;
<a href="$Link" title="View page number $PageNum">$PageNum</a>
<% else %>
&hellip;
<% end_if %>
<% end_if %> <% end_if %>
<% end_loop %> <% end_if %>
</span> <% end_loop %>
</span>
<% if $Results.NotLastPage %>
<a class="next" href="$Results.NextLink" title="View the next page">Next</a> <% if $Results.NotLastPage %>
<% end_if %> <a class="next" href="$Results.NextLink" title="View the next page">Next</a>
</p> <% end_if %>
</div> </p>
<% end_if %> </div>
<% end_if %>
``` ```
## Available SearchFilters ## Available SearchFilters

View File

@ -23,16 +23,15 @@ You can do so by adding this static variable to your class definition:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Connect\MySQLSchemaManager;
class MyDataObject extends DataObject
{
private static $create_table_options = [
'MySQLDatabase' => 'ENGINE=MyISAM'
];
}
class MyDataObject extends DataObject
{
private static $create_table_options = [
MySQLSchemaManager::ID => 'ENGINE=MyISAM'
];
}
``` ```
The [FulltextSearchable](api:SilverStripe\ORM\Search\FulltextSearchable) extension will add the correct `Fulltext` indexes to the data model. The [FulltextSearchable](api:SilverStripe\ORM\Search\FulltextSearchable) extension will add the correct `Fulltext` indexes to the data model.
@ -52,28 +51,29 @@ Example DataObject:
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Connect\MySQLSchemaManager;
class SearchableDataObject extends DataObject class SearchableDataObject extends DataObject
{ {
private static $db = [ private static $db = [
"Title" => "Varchar(255)", "Title" => "Varchar(255)",
"Content" => "HTMLText", "Content" => "HTMLText",
]; ];
private static $indexes = [ private static $indexes = [
'SearchFields' => [ 'SearchFields' => [
'type' => 'fulltext', 'type' => 'fulltext',
'columns' => ['Title', 'Content'], 'columns' => ['Title', 'Content'],
] ]
]; ];
private static $create_table_options = [ private static $create_table_options = [
'MySQLDatabase' => 'ENGINE=MyISAM' MySQLSchemaManager::ID => 'ENGINE=MyISAM'
]; ];
} }
``` ```
@ -81,7 +81,7 @@ Performing the search:
```php ```php
SearchableDataObject::get()->filter('SearchFields:Fulltext', 'search term'); 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 If your search index is a single field size, then you may also specify the search filter by the name of the

View File

@ -29,9 +29,11 @@ you want to set.
```php ```php
// mysite/_config.php use SilverStripe\i18n\i18n;
i18n::set_locale('de_DE'); // Setting the locale to German (Germany)
i18n::set_locale('ca_AD'); // Setting to Catalan (Andorra) // 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 Once we set a locale, all the calls to the translator function will return strings according to the set locale value, if
@ -54,12 +56,11 @@ To let browsers know which language they're displaying a document in, you can de
```html ```html
//'Page.ss' (HTML)
<html lang="$ContentLocale">
//'Page.ss' (HTML) //'Page.ss' (XHTML)
<html lang="$ContentLocale"> <html lang="$ContentLocale" xml:lang="$ContentLocale" xmlns="http://www.w3.org/1999/xhtml">
//'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 Setting the `<html>` attribute is the most commonly used technique. There are other ways to specify content languages
@ -71,8 +72,7 @@ and default alignment of paragraphs and tables to browsers.
```html ```html
<html lang="$ContentLocale" dir="$i18nScriptDirection">
<html lang="$ContentLocale" dir="$i18nScriptDirection">
``` ```
### Date and time formats ### Date and time formats
@ -82,8 +82,12 @@ You can use these settings for your own view logic.
```php ```php
Config::inst()->update('i18n', 'date_format', 'dd.MM.yyyy'); use SilverStripe\Core\Config\Config;
Config::inst()->update('i18n', 'time_format', 'HH:mm'); use SilverStripe\i18n\i18n;
i18n::config()
->set('date_format', 'dd.MM.yyyy')
->set('time_format', 'HH:mm');
``` ```
Localization in SilverStripe uses PHP's [intl extension](http://php.net/intl). Localization in SilverStripe uses PHP's [intl extension](http://php.net/intl).
@ -106,23 +110,21 @@ In order to add a value, add the following to your `config.yml`:
```yml ```yml
SilverStripe\i18n\i18n:
i18n: common_locales:
common_locales: de_CGN:
de_CGN: name: German (Cologne)
name: German (Cologne) native: Kölsch
native: Kölsch
``` ```
Similarly, to change an existing language label, you can overwrite one of these keys: Similarly, to change an existing language label, you can overwrite one of these keys:
```yml ```yml
SilverStripe\i18n\i18n:
i18n: common_locales:
common_locales: en_NZ:
en_NZ: native: Niu Zillund
native: Niu Zillund
``` ```
### i18n in URLs ### i18n in URLs
@ -155,9 +157,11 @@ followed by `setLocale()` or `setDateFormat()`/`setTimeFormat()`.
```php ```php
$field = new DateField(); use SilverStripe\Forms\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) $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 ## Translating text
@ -167,10 +171,10 @@ language-dependent and use a translator function call instead.
```php ```php
// without i18n // without i18n
echo "This is a string"; echo "This is a string";
// with i18n // with i18n
echo _t("Namespace.Entity","This is a string"); 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. 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.
@ -211,38 +215,35 @@ For instance, this is an example of how to correctly declare pluralisations for
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class MyObject extends DataObject, implements i18nEntityProvider class MyObject extends DataObject implements i18nEntityProvider
{
public function provideI18nEntities()
{ {
public function provideI18nEntities() return [
{ 'MyObject.SINGULAR_NAME' => 'object',
return [ 'MyObject.PLURAL_NAME' => 'objects',
'MyObject.SINGULAR_NAME' => 'object', 'MyObject.PLURALS' => [
'MyObject.PLURAL_NAME' => 'objects', 'one' => 'An object',
'MyObject.PLURALS' => [ 'other' => '{count} objects',
'one' => 'An object', ],
'other' => '{count} objects', ];
],
];
}
} }
}
``` ```
In YML format this will be expressed as the below. This follows the 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. [ruby i18n convention](guides.rubyonrails.org/i18n.html#pluralization) for plural forms.
```yaml ```yaml
en:
en: MyObject:
MyObject: SINGULAR_NAME: 'object'
SINGULAR_NAME: 'object' PLURAL_NAME: 'objects'
PLURAL_NAME: 'objects' PLURALS:
PLURALS: one: 'An object',
one: 'An object', other: '{count} objects'
other: '{count} objects'
``` ```
Note: i18nTextCollector support for pluralisation is not yet available. Note: i18nTextCollector support for pluralisation is not yet available.
@ -252,18 +253,17 @@ Please ensure that any required plurals are exposed via provideI18nEntities.
```php ```php
// Simple string translation // Simple string translation
_t('LeftAndMain.FILESIMAGES','Files & Images'); _t('LeftAndMain.FILESIMAGES','Files & Images');
// Using injection to add variables into the translated strings. // Using injection to add variables into the translated strings.
_t('CMSMain.RESTORED', _t('CMSMain.RESTORED',
"Restored {value} successfully", "Restored {value} successfully",
['value' => $itemRestored] ['value' => $itemRestored]
); );
// Plurals are invoked via a `|` pipe-delimeter with a {count} argument
_t('MyObject.PLURALS', 'An object|{count} objects', [ 'count' => '$count ]);
// Plurals are invoked via a `|` pipe-delimeter with a {count} argument
_t('MyObject.PLURALS', 'An object|{count} objects', [ 'count' => '$count ]);
``` ```
#### Usage in Template Files #### Usage in Template Files
@ -281,15 +281,14 @@ the PHP version of the function.
```ss ```ss
// Simple string translation
<%t Namespace.Entity "String to translate" %>
// Simple string translation // Using injection to add variables into the translated strings (note that $Name and $Greeting must be available in the current template scope).
<%t Namespace.Entity "String to translate" %> <%t Header.Greeting "Hello {name} {greeting}" name=$Name greeting=$Greeting %>
// Using injection to add variables into the translated strings (note that $Name and $Greeting must be available in the current template scope). // Plurals follow the same convention, required a `|` and `{count}` in the default string
<%t Header.Greeting "Hello {name} {greeting}" name=$Name greeting=$Greeting %> <%t MyObject.PLURALS 'An item|{count} items' count=$Count %>
// 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 #### Caching in Template Files with locale switching
@ -297,14 +296,12 @@ the PHP version of the function.
When caching a `<% loop %>` or `<% with %>` with `<%t params %>`. It is important to add the Locale to the cache key 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. otherwise it won't pick up locale changes.
```ss ```ss
<% cached 'MyIdentifier', $CurrentLocale %>
<% cached 'MyIdentifier', $CurrentLocale %> <% loop $Students %>
<% loop $Students %> $Name
$Name <% end_loop %>
<% end_loop %> <% end_cached %>
<% end_cached %>
``` ```
## Collecting text ## Collecting text
@ -336,16 +333,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. 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 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
```yml ```yml
--- ---
Name: customi18n Name: customi18n
Before: 'defaulti18n' Before: 'defaulti18n'
--- ---
i18n: SilverStripe\i18n\i18n:
module_priority: module_priority:
- module1 - module1
- module2 - module2
- module3 - module3
``` ```
The config option being set is `i18n.module_priority`, and it is a list of module names. The config option being set is `i18n.module_priority`, and it is a list of module names.
@ -365,21 +363,25 @@ By default, SilverStripe uses a YAML format which is loaded via the
[symfony/translate](http://symfony.com/doc/current/translation.html) library. [symfony/translate](http://symfony.com/doc/current/translation.html) library.
Example: framework/lang/en.yml (extract) Example: framework/lang/en.yml (extract)
```yml ```yml
en: en:
ImageUploader: ImageUploader:
Attach: 'Attach {title}' Attach: 'Attach {title}'
UploadField: UploadField:
NOTEADDFILES: 'You can add files once you have saved for the first time.' NOTEADDFILES: 'You can add files once you have saved for the first time.'
``` ```
Translation table: framework/lang/de.yml (extract) Translation table: framework/lang/de.yml (extract)
```yml ```yml
de: de:
ImageUploader: ImageUploader:
ATTACH: '{title} anhängen' ATTACH: '{title} anhängen'
UploadField: UploadField:
NOTEADDFILES: 'Sie können Dateien hinzufügen sobald Sie das erste mal gespeichert haben' NOTEADDFILES: 'Sie können Dateien hinzufügen sobald Sie das erste mal gespeichert haben'
``` ```
Note that translations are cached across requests. Note that translations are cached across requests.
The cache can be cleared through the `?flush=1` query parameter, The cache can be cleared through the `?flush=1` query parameter,
or explicitly through `Zend_Translate::getCache()->clean(Zend_Cache::CLEANING_MODE_ALL)`. or explicitly through `Zend_Translate::getCache()->clean(Zend_Cache::CLEANING_MODE_ALL)`.
@ -402,6 +404,8 @@ If using this on the frontend, it's also necessary to include the stand-alone i1
js file. js file.
```php ```php
use SilverStripe\View\Requirements;
Requirements::javascript('silverstripe/admin:client/dist/js/i18n.js'); Requirements::javascript('silverstripe/admin:client/dist/js/i18n.js');
Requirements::add_i18n_javascript('<my-module-dir>/javascript/lang'); Requirements::add_i18n_javascript('<my-module-dir>/javascript/lang');
``` ```
@ -415,24 +419,22 @@ Master Table (`<my-module-dir>/javascript/lang/en.js`)
```js ```js
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') { console.error('Class ss.i18n not defined');
console.error('Class ss.i18n not defined'); } else {
} else { ss.i18n.addDictionary('en', {
ss.i18n.addDictionary('en', { 'MYMODULE.MYENTITY' : "Really delete these articles?"
'MYMODULE.MYENTITY' : "Really delete these articles?" });
}); }
}
``` ```
Example Translation Table (`<my-module-dir>/javascript/lang/de.js`) Example Translation Table (`<my-module-dir>/javascript/lang/de.js`)
```js ```js
ss.i18n.addDictionary('de', {
ss.i18n.addDictionary('de', { 'MYMODULE.MYENTITY' : "Artikel wirklich löschen?"
'MYMODULE.MYENTITY' : "Artikel wirklich löschen?" });
});
``` ```
For most core modules, these files are generated by a For most core modules, these files are generated by a
@ -444,8 +446,7 @@ format which can be processed more easily by external translation providers (see
```js ```js
alert(ss.i18n._t('MYMODULE.MYENTITY'));
alert(ss.i18n._t('MYMODULE.MYENTITY'));
``` ```
### Advanced Use ### Advanced Use
@ -454,33 +455,34 @@ The `ss.i18n` object contain a couple functions to help and replace dynamic vari
#### Legacy sequential replacement with sprintf() #### Legacy sequential replacement with sprintf()
`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. `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 ```js
// MYMODULE.MYENTITY contains "Really delete %s articles by %s?"
// MYMODULE.MYENTITY contains "Really delete %s articles by %s?" alert(ss.i18n.sprintf(
alert(ss.i18n.sprintf( ss.i18n._t('MYMODULE.MYENTITY'),
ss.i18n._t('MYMODULE.MYENTITY'), 42,
42, 'Douglas Adams'
'Douglas Adams' ));
)); // Displays: "Really delete 42 articles by Douglas Adams?"
// Displays: "Really delete 42 articles by Douglas Adams?"
``` ```
#### Variable injection with inject() #### 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. `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 ```js
// MYMODULE.MYENTITY contains "Really delete {count} articles by {author}?"
// MYMODULE.MYENTITY contains "Really delete {count} articles by {author}?" alert(ss.i18n.inject(
alert(ss.i18n.inject( ss.i18n._t('MYMODULE.MYENTITY'),
ss.i18n._t('MYMODULE.MYENTITY'), {count: 42, author: 'Douglas Adams'}
{count: 42, author: 'Douglas Adams'} ));
)); // Displays: "Really delete 42 articles by Douglas Adams?"
// Displays: "Really delete 42 articles by Douglas Adams?"
``` ```
## Limitations ## Limitations

View File

@ -130,6 +130,8 @@ to live until a publish is made (either on this object, or cascading from a pare
When files are renamed using the ORM, all file variants are automatically renamed at the same time. When files are renamed using the ORM, all file variants are automatically renamed at the same time.
```php ```php
use SilverStripe\Assets\File;
$file = File::get()->filter('Name', 'oldname.jpg')->first(); $file = File::get()->filter('Name', 'oldname.jpg')->first();
if ($file) { if ($file) {
// The below will move 'oldname.jpg' and 'oldname__variant.jpg' // The below will move 'oldname.jpg' and 'oldname__variant.jpg'

View File

@ -37,45 +37,42 @@ Here are some examples, assuming the `$Image` object has dimensions of 200x100px
```ss ```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)
// Scaling functions // Warning: This method can distort images that are not the correct aspect ratio
$Image.ScaleWidth(150) // Returns a 150x75px image $Image.ResizedImage(200, 300) // Forces dimensions of this image to the given values.
$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) // Cropping functions
$Image.ScaleMaxHeight(150) // Returns a 200x100px image (like ScaleHeight but prevents up-sampling) $Image.Fill(150,150) // Returns a 150x150px image resized and cropped to fill specified dimensions (up-sampled)
$Image.Fit(300,300) // Returns an image that fits within a 300x300px boundary, resulting in a 300x150px image (up-sampled) $Image.FillMax(150,150) // Returns a 100x100px image (like Fill but prevents up-sampling)
$Image.FitMax(300,300) // Returns a 200x100px image (like Fit 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)
// 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. // 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
// Cropping functions $Image.Pad(100, 100, CCCCCC) // Same as above but with a grey background
$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) // Metadata
$Image.CropWidth(150) // Returns a 150x100px image (trims excess pixels off the x axis from the center) $Image.Width // Returns width of image
$Image.CropHeight(50) // Returns a 200x50px image (trims excess pixels off the y axis from the center) $Image.Height // Returns height of image
$Image.Orientation // Returns Orientation
// Padding functions (add space around an image) $Image.Title // Returns the friendly file name
$Image.Pad(100,100) // Returns a 100x100px padded image, with white bars added at the top and bottom $Image.Name // Returns the actual file name
$Image.Pad(100, 100, CCCCCC) // Same as above but with a grey background $Image.FileName // Returns the actual file name including directory path from web root
$Image.Link // Returns relative URL path to image
// Metadata $Image.AbsoluteLink // Returns absolute URL path to image
$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: Image methods are chainable. Example:
```ss ```ss
<body style="background-image:url($Image.ScaleWidth(800).CropHeight(800).Link)">
<body style="background-image:url($Image.ScaleWidth(800).CropHeight(800).Link)">
``` ```
### Padded Image Resize ### Padded Image Resize
@ -88,9 +85,9 @@ png images.
```php ```php
$Image.Pad(80, 80, FFFFFF, 50) // white padding with 50% transparency $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, 100) // white padding with 100% transparency
$Image.Pad(80, 80, FFFFFF) // white padding with no transparency $Image.Pad(80, 80, FFFFFF) // white padding with no transparency
``` ```
### Manipulating images in PHP ### Manipulating images in PHP
@ -107,42 +104,43 @@ You can also create your own functions by decorating the `Image` class.
```php ```php
class ImageExtension extends \SilverStripe\Core\Extension use SilverStripe\Core\Extension;
class ImageExtension extends Extension
{
public function Square($width)
{ {
$variant = $this->owner->variantName(__FUNCTION__, $width);
public function Square($width) return $this->owner->manipulateImage($variant, function (\SilverStripe\Assets\Image_Backend $backend) use($width) {
{ $clone = clone $backend;
$variant = $this->owner->variantName(__FUNCTION__, $width); $resource = clone $backend->getImageResource();
return $this->owner->manipulateImage($variant, function (\SilverStripe\Assets\Image_Backend $backend) use($width) { $resource->fit($width);
$clone = clone $backend; $clone->setImageResource($resource);
$resource = clone $backend->getImageResource(); return $clone;
$resource->fit($width); });
$clone->setImageResource($resource);
return $clone;
});
}
public function Blur($amount = null)
{
$variant = $this->owner->variantName(__FUNCTION__, $amount);
return $this->owner->manipulateImage($variant, function (\SilverStripe\Assets\Image_Backend $backend) use ($amount) {
$clone = clone $backend;
$resource = clone $backend->getImageResource();
$resource->blur($amount);
$clone->setImageResource($resource);
return $clone;
});
}
} }
:::yml public function Blur($amount = null)
SilverStripe\Assets\Image: {
extensions: $variant = $this->owner->variantName(__FUNCTION__, $amount);
- ImageExtension return $this->owner->manipulateImage($variant, function (\SilverStripe\Assets\Image_Backend $backend) use ($amount) {
SilverStripe\Filesystem\Storage\DBFile: $clone = clone $backend;
extensions: $resource = clone $backend->getImageResource();
- ImageExtension $resource->blur($amount);
$clone->setImageResource($resource);
return $clone;
});
}
}
```
```yml
SilverStripe\Assets\Image:
extensions:
- ImageExtension
SilverStripe\Filesystem\Storage\DBFile:
extensions:
- ImageExtension
``` ```
### Form Upload ### Form Upload
@ -176,13 +174,12 @@ necessary, you can add this to your mysite/config/config.yml file:
```yml ```yml
# Configure resampling for File dataobject
# Configure resampling for File dataobject SilverStripe\Assets\File:
File: force_resample: false
force_resample: false # DBFile can be configured independently
# DBFile can be configured independently SilverStripe\Assets\Storage\DBFile:
SilverStripe\Filesystem\Storage\DBFile: force_resample: false
force_resample: false
``` ```
#### Resampled image quality #### Resampled image quality
@ -192,11 +189,10 @@ following to your mysite/config/config.yml file:
```yml ```yml
SilverStripe\Core\Injector\Injector:
SilverStripe\Core\Injector\Injector: SilverStripe\Assets\Image_Backend:
SilverStripe\Assets\Image_Backend: properties:
properties: Quality: 90
Quality: 90
``` ```
## Changing the manipulation driver to Imagick ## Changing the manipulation driver to Imagick

View File

@ -190,6 +190,8 @@ will be moved to `assets/a870de278b/NewCompanyLogo.gif`, and will be served dire
the web server, bypassing the need for additional PHP requests. the web server, bypassing the need for additional PHP requests.
```php ```php
use SilverStripe\Assets\Storage\AssetStore;
$store = singleton(AssetStore::class); $store = singleton(AssetStore::class);
$store->publish('NewCompanyLogo.gif', 'a870de278b475cb75f5d9f451439b2d378e13af1'); $store->publish('NewCompanyLogo.gif', 'a870de278b475cb75f5d9f451439b2d378e13af1');
``` ```

View File

@ -13,7 +13,7 @@ has been added to assist in migration of legacy files.
You can run this task on the command line: You can run this task on the command line:
``` ```
$ ./vendor/silverstripe/framework/sake dev/tasks/MigrateFileTask $ ./vendor/bin/sake dev/tasks/MigrateFileTask
``` ```
This task will also support migration of existing File DataObjects to file versioning. Any This task will also support migration of existing File DataObjects to file versioning. Any

View File

@ -21,42 +21,40 @@ a category.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Product extends DataObject class Product extends DataObject
{ {
private static $db = [ private static $db = [
'Name' => 'Varchar', 'Name' => 'Varchar',
'ProductCode' => 'Varchar', 'ProductCode' => 'Varchar',
'Price' => 'Currency' 'Price' => 'Currency'
]; ];
private static $has_one = [
'Category' => 'Category'
];
}
private static $has_one = [
'Category' => 'Category'
];
}
``` ```
**mysite/code/Category.php** **mysite/code/Category.php**
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Category extends DataObject class Category extends DataObject
{ {
private static $db = [ private static $db = [
'Title' => 'Text' 'Title' => 'Text'
]; ];
private static $has_many = [
'Products' => 'Product'
];
}
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 To create your own `ModelAdmin`, simply extend the base class, and edit the `$managed_models` property with the list of
@ -68,21 +66,20 @@ We'll name it `MyAdmin`, but the class name can be anything you want.
```php ```php
use SilverStripe\Admin\ModelAdmin; use SilverStripe\Admin\ModelAdmin;
class MyAdmin extends ModelAdmin class MyAdmin extends ModelAdmin
{ {
private static $managed_models = [ private static $managed_models = [
'Product', 'Product',
'Category' 'Category'
]; ];
private static $url_segment = 'products'; private static $url_segment = 'products';
private static $menu_title = 'My Product Admin';
}
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 This will automatically add a new menu entry to the SilverStripe Admin UI entitled `My Product Admin` and logged in
@ -110,31 +107,31 @@ permissions by default. For most cases, less restrictive checks make sense, e.g.
```php ```php
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Category extends DataObject class Category extends DataObject
{
public function canView($member = null)
{ {
// ... return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
public function canView($member = null) }
{
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
}
public function canEdit($member = null) public function canEdit($member = null)
{ {
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member); return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
} }
public function canDelete($member = null) public function canDelete($member = null)
{ {
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member); return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
} }
public function canCreate($member = null) public function canCreate($member = null)
{ {
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member); return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
} }
}
``` ```
## Searching Records ## Searching Records
@ -151,17 +148,16 @@ class (see [SearchContext](../search/searchcontext) docs for details).
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Product extends DataObject class Product extends DataObject
{ {
private static $searchable_fields = [
'Name',
'ProductCode'
];
}
private static $searchable_fields = [
'Name',
'ProductCode'
];
}
``` ```
<div class="hint" markdown="1"> <div class="hint" markdown="1">
@ -178,21 +174,19 @@ model class, where you can add or remove columns. To change the title, use [Data
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
class Product extends DataObject class Product extends DataObject
{ {
private static $field_labels = [
private static $field_labels = [ 'Price' => 'Cost' // renames the column to "Cost"
'Price' => 'Cost' // renames the column to "Cost" ];
];
private static $summary_fields = [
'Name',
'Price'
];
}
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 The results list are retrieved from [SearchContext::getResults()](api:SilverStripe\ORM\Search\SearchContext::getResults()), based on the parameters passed through the search
@ -205,23 +199,22 @@ For example, we might want to exclude all products without prices in our sample
```php ```php
use SilverStripe\Admin\ModelAdmin; use SilverStripe\Admin\ModelAdmin;
class MyAdmin extends ModelAdmin class MyAdmin extends ModelAdmin
{
public function getList()
{ {
$list = parent::getList();
public function getList() // Always limit by model class, in case you're managing multiple
{ if($this->modelClass == 'Product') {
$list = parent::getList(); $list = $list->exclude('Price', '0');
// Always limit by model class, in case you're managing multiple
if($this->modelClass == 'Product') {
$list = $list->exclude('Price', '0');
}
return $list;
} }
return $list;
} }
}
``` ```
You can also customize the search behavior directly on your `ModelAdmin` instance. For example, we might want to have a You can also customize the search behavior directly on your `ModelAdmin` instance. For example, we might want to have a
@ -229,38 +222,36 @@ checkbox which limits search results to expensive products (over $100).
**mysite/code/MyAdmin.php** **mysite/code/MyAdmin.php**
```php ```php
use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\CheckboxField;
use SilverStripe\Admin\ModelAdmin; use SilverStripe\Admin\ModelAdmin;
class MyAdmin extends ModelAdmin class MyAdmin extends ModelAdmin
{
public function getSearchContext()
{ {
$context = parent::getSearchContext();
public function getSearchContext() if($this->modelClass == 'Product') {
{ $context->getFields()->push(new CheckboxField('q[ExpensiveOnly]', 'Only expensive stuff'));
$context = parent::getSearchContext();
if($this->modelClass == 'Product') {
$context->getFields()->push(new CheckboxField('q[ExpensiveOnly]', 'Only expensive stuff'));
}
return $context;
} }
public function getList() return $context;
{
$list = parent::getList();
$params = $this->getRequest()->requestVar('q'); // use this to access search parameters
if($this->modelClass == 'Product' && isset($params['ExpensiveOnly']) && $params['ExpensiveOnly']) {
$list = $list->exclude('Price:LessThan', '100');
}
return $list;
}
} }
public function getList()
{
$list = parent::getList();
$params = $this->getRequest()->requestVar('q'); // use this to access search parameters
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 To alter how the results are displayed (via [GridField](api:SilverStripe\Forms\GridField\GridField)), you can also overload the `getEditForm()` method. For
@ -270,35 +261,34 @@ example, to add a new component.
```php ```php
use SilverStripe\Forms\GridField\GridFieldFilterHeader; use SilverStripe\Forms\GridField\GridFieldFilterHeader;
use SilverStripe\Admin\ModelAdmin; use SilverStripe\Admin\ModelAdmin;
class MyAdmin extends ModelAdmin class MyAdmin extends ModelAdmin
{
private static $managed_models = [
'Product',
'Category'
];
// ...
public function getEditForm($id = null, $fields = null)
{ {
$form = parent::getEditForm($id, $fields);
private static $managed_models = [ // $gridFieldName is generated from the ModelClass, eg if the Class 'Product'
'Product', // is managed by this ModelAdmin, the GridField for it will also be named 'Product'
'Category'
];
// ... $gridFieldName = $this->sanitiseClassName($this->modelClass);
public function getEditForm($id = null, $fields = null) $gridField = $form->Fields()->fieldByName($gridFieldName);
{
$form = parent::getEditForm($id, $fields);
// $gridFieldName is generated from the ModelClass, eg if the Class 'Product' // modify the list view.
// is managed by this ModelAdmin, the GridField for it will also be named 'Product' $gridField->getConfig()->addComponent(new GridFieldFilterHeader());
$gridFieldName = $this->sanitiseClassName($this->modelClass); return $form;
$gridField = $form->Fields()->fieldByName($gridFieldName);
// 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 The above example will add the component to all `GridField`s (of all managed models). Alternatively we can also add it
@ -308,32 +298,31 @@ to only one specific `GridField`:
```php ```php
use SilverStripe\Forms\GridField\GridFieldFilterHeader; use SilverStripe\Forms\GridField\GridFieldFilterHeader;
use SilverStripe\Admin\ModelAdmin; use SilverStripe\Admin\ModelAdmin;
class MyAdmin extends ModelAdmin class MyAdmin extends ModelAdmin
{
private static $managed_models = [
'Product',
'Category'
];
public function getEditForm($id = null, $fields = null)
{ {
$form = parent::getEditForm($id, $fields);
private static $managed_models = [ $gridFieldName = 'Product';
'Product', $gridField = $form->Fields()->fieldByName($gridFieldName);
'Category'
];
public function getEditForm($id = null, $fields = null) if ($gridField) {
{ $gridField->getConfig()->addComponent(new GridFieldFilterHeader());
$form = parent::getEditForm($id, $fields);
$gridFieldName = 'Product';
$gridField = $form->Fields()->fieldByName($gridFieldName);
if ($gridField) {
$gridField->getConfig()->addComponent(new GridFieldFilterHeader());
}
return $form;
} }
}
return $form;
}
}
``` ```
## Data Import ## Data Import
@ -354,22 +343,21 @@ To customize the exported columns, create a new method called `getExportFields`
```php ```php
use SilverStripe\Admin\ModelAdmin; use SilverStripe\Admin\ModelAdmin;
class MyAdmin extends ModelAdmin class MyAdmin extends ModelAdmin
{
// ...
public function getExportFields()
{ {
// ... return [
'Name' => 'Name',
public function getExportFields() 'ProductCode' => 'Product Code',
{ 'Category.Title' => 'Category'
return [ ];
'Name' => 'Name',
'ProductCode' => 'Product Code',
'Category.Title' => 'Category'
];
}
} }
}
``` ```
## Related Documentation ## Related Documentation

View File

@ -57,16 +57,16 @@ The CMS interface can be accessed by default through the `admin/` URL. You can c
```yml ```yml
--- ---
Name: myadmin Name: myadmin
After: After:
- '#adminroutes' - '#adminroutes'
--- ---
Director: SilverStripe\Control\Director:
rules: rules:
'admin': '' 'admin': ''
'newAdmin': 'AdminRootController' '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): 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):
@ -75,23 +75,21 @@ In PHP you should use:
```php ```php
AdminRootController::admin_url() SilverStripe\Admin\AdminRootController::admin_url()
``` ```
When writing templates use: When writing templates use:
```ss ```ss
$AdminURL
$AdminURL
``` ```
And in JavaScript, this is avaible through the `ss` namespace And in JavaScript, this is avaible through the `ss` namespace
```js ```js
ss.config.adminUrl
ss.config.adminUrl
``` ```
### Multiple Admin URL and overrides ### Multiple Admin URL and overrides
@ -162,45 +160,45 @@ Basic example form in a CMS controller subclass:
```php ```php
use SilverStripe\Forms\TabSet; use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Tab; use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TextField; use SilverStripe\Forms\TextField;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Admin\LeftAndMain; use SilverStripe\Admin\LeftAndMain;
use SilverStripe\Admin\LeftAndMainFormRequestHandler; use SilverStripe\Admin\LeftAndMainFormRequestHandler;
class MyAdmin extends LeftAndMain class MyAdmin extends LeftAndMain
{ {
function getEditForm() { public function getEditForm() {
return Form::create( return Form::create(
$this, $this,
'EditForm', 'EditForm',
new FieldList( new FieldList(
TabSet::create( TabSet::create(
'Root', 'Root',
Tab::create('Main', Tab::create('Main',
TextField::create('MyText') TextField::create('MyText')
) )
)->setTemplate('CMSTabset') )->setTemplate('CMSTabset')
), ),
new FieldList( new FieldList(
FormAction::create('doSubmit') FormAction::create('doSubmit')
)
) )
// Use a custom request handler )
->setRequestHandler( // Use a custom request handler
LeftAndMainFormRequestHandler::create($form) ->setRequestHandler(
) LeftAndMainFormRequestHandler::create($form)
// JS and CSS use this identifier )
->setHTMLID('Form_EditForm') // JS and CSS use this identifier
// Render correct responses on validation errors ->setHTMLID('Form_EditForm')
->setResponseNegotiator($this->getResponseNegotiator()); // Render correct responses on validation errors
// Required for correct CMS layout ->setResponseNegotiator($this->getResponseNegotiator());
->addExtraClass('cms-edit-form') // Required for correct CMS layout
->setTemplate($this->getTemplatesWithSuffix('_EditForm')); ->addExtraClass('cms-edit-form')
} ->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
} }
}
``` ```
Note: Usually you don't need to worry about these settings, Note: Usually you don't need to worry about these settings,
@ -306,48 +304,48 @@ 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 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 controlled by the react section, and thus should suppress registration of a page.js route
for this section. 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 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 with the reactRouteRegister component. This will need to be done on the `DOMContentLoaded` event
to ensure routes are registered before window.load is invoked. to ensure routes are registered before window.load is invoked.
```js
import { withRouter } from 'react-router'; ```js
import ConfigHelpers from 'lib/Config'; import { withRouter } from 'react-router';
import reactRouteRegister from 'lib/ReactRouteRegister'; import ConfigHelpers from 'lib/Config';
import MyAdmin from './MyAdmin'; import reactRouteRegister from 'lib/ReactRouteRegister';
import MyAdmin from './MyAdmin';
document.addEventListener('DOMContentLoaded', () => {
const sectionConfig = ConfigHelpers.getSection('MyAdmin'); document.addEventListener('DOMContentLoaded', () => {
const sectionConfig = ConfigHelpers.getSection('MyAdmin');
reactRouteRegister.add({
path: sectionConfig.url, reactRouteRegister.add({
component: withRouter(MyAdminComponent), path: sectionConfig.url,
childRoutes: [ component: withRouter(MyAdminComponent),
{ path: 'form/:id/:view', component: MyAdminComponent }, childRoutes: [
], { path: 'form/:id/:view', component: MyAdminComponent },
}); ],
}); });
});
``` ```
Child routes can be registered post-boot by using `ReactRouteRegister` in the same way. Child routes can be registered post-boot by using `ReactRouteRegister` in the same way.
```js
// Register a nested url under `sectionConfig.url` ```js
const sectionConfig = ConfigHelpers.getSection('MyAdmin'); // Register a nested url under `sectionConfig.url`
reactRouteRegister.add({ const sectionConfig = ConfigHelpers.getSection('MyAdmin');
path: 'nested', reactRouteRegister.add({
component: NestedComponent, path: 'nested',
}, [ sectionConfig.url ]); component: NestedComponent,
}, [ sectionConfig.url ]);
``` ```
## PJAX: Partial template replacement through Ajax ## PJAX: Partial template replacement through Ajax
@ -375,57 +373,57 @@ in a single Ajax request.
```php ```php
use SilverStripe\Admin\LeftAndMain; use SilverStripe\Admin\LeftAndMain;
// mysite/code/MyAdmin.php // mysite/code/MyAdmin.php
class MyAdmin extends LeftAndMain class MyAdmin extends LeftAndMain
{
private static $url_segment = 'myadmin';
public function getResponseNegotiator()
{ {
private static $url_segment = 'myadmin'; $negotiator = parent::getResponseNegotiator();
public function getResponseNegotiator() $controller = $this;
{ // Register a new callback
$negotiator = parent::getResponseNegotiator(); $negotiator->setCallback('MyRecordInfo', function() use(&$controller) {
$controller = $this; return $controller->MyRecordInfo();
// Register a new callback });
$negotiator->setCallback('MyRecordInfo', function() use(&$controller) { return $negotiator;
return $controller->MyRecordInfo();
});
return $negotiator;
}
public function MyRecordInfo()
{
return $this->renderWith('MyRecordInfo');
}
} }
public function MyRecordInfo()
{
return $this->renderWith('MyRecordInfo');
}
}
``` ```
```js ```js
// MyAdmin.ss // MyAdmin.ss
<% include SilverStripe\\Admin\\CMSBreadcrumbs %> <% include SilverStripe\\Admin\\CMSBreadcrumbs %>
<div>Static content (not affected by update)</div> <div>Static content (not affected by update)</div>
<% include MyRecordInfo %> <% include MyRecordInfo %>
<a href="{$AdminURL}myadmin" class="cms-panel-link" data-pjax-target="MyRecordInfo,Breadcrumbs"> <a href="{$AdminURL}myadmin" class="cms-panel-link" data-pjax-target="MyRecordInfo,Breadcrumbs">
Update record info Update record info
</a> </a>
``` ```
```ss ```ss
// MyRecordInfo.ss // MyRecordInfo.ss
<div data-pjax-fragment="MyRecordInfo"> <div data-pjax-fragment="MyRecordInfo">
Current Record: $currentPage.Title Current Record: $currentPage.Title
</div> </div>
``` ```
A click on the link will cause the following (abbreviated) ajax HTTP request: A click on the link will cause the following (abbreviated) ajax HTTP request:
``` ```
GET /admin/myadmin HTTP/1.1 GET /admin/myadmin HTTP/1.1
X-Pjax:MyRecordInfo,Breadcrumbs X-Pjax:MyRecordInfo,Breadcrumbs
X-Requested-With:XMLHttpRequest X-Requested-With:XMLHttpRequest
``` ```
... and result in the following response: ... 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 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, is fired, so the server might decide to change it based on its own logic,
@ -434,9 +432,11 @@ sending back different `X-Pjax` headers and content.
On the client, you can set your preference through the `data-pjax-target` attributes 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 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. tracked in the browser history, use the `pjax` attribute on the state data.
```js ```js
$('.cms-container').loadPanel(ss.config.adminUrl+'pages', null, {pjax: 'Content'}); $('.cms-container').loadPanel(ss.config.adminUrl+'pages', null, {pjax: 'Content'});
``` ```
## Loading lightweight PJAX fragments ## Loading lightweight PJAX fragments
Normal navigation between URLs in the admin section of the Framework occurs through `loadPanel` and `submitForm`. Normal navigation between URLs in the admin section of the Framework occurs through `loadPanel` and `submitForm`.
@ -450,17 +450,21 @@ 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 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. parallel as you want. This will not disturb the main navigation.
```js ```js
$('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment1'); $('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment1');
$('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment2'); $('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment2');
$('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment3'); $('.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 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. result in the prior request for this fragment to be aborted. Other parallel requests will continue undisturbed.
You can also load multiple fragments in one request, as long as they are to the same controller (i.e. URL): You can also load multiple fragments in one request, as long as they are to the same controller (i.e. URL):
$('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment2,Fragment3'); ```js
$('.cms-container').loadFragment(ss.config.adminUrl+'foobar/', 'Fragment2,Fragment3');
```
This counts as a separate request type from the perspective of the request tracking, so will not abort the singular This counts as a separate request type from the perspective of the request tracking, so will not abort the singular
`Fragment2` nor `Fragment3`. `Fragment2` nor `Fragment3`.
@ -470,23 +474,27 @@ 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. 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: You can hook up a response handler that obtains all the details of the XHR request via Entwine handler:
```js ```js
'from .cms-container': { 'from .cms-container': {
onafterloadfragment: function(e, data) { onafterloadfragment: function(e, data) {
// Say 'success'! // Say 'success'!
alert(data.status); alert(data.status);
} }
} }
``` ```
Alternatively you can use the jQuery deferred API: Alternatively you can use the jQuery deferred API:
```js ```js
$('.cms-container') $('.cms-container')
.loadFragment(ss.config.adminUrl+'foobar/', 'Fragment1') .loadFragment(ss.config.adminUrl+'foobar/', 'Fragment1')
.success(function(data, status, xhr) { .success(function(data, status, xhr) {
// Say 'success'! // Say 'success'!
alert(status); alert(status);
}); });
``` ```
## Ajax Redirects ## Ajax Redirects
Sometimes, a server response represents a new URL state, e.g. when submitting an "add record" form, Sometimes, a server response represents a new URL state, e.g. when submitting an "add record" form,
@ -508,26 +516,26 @@ without affecting the response body.
```php ```php
use SilverStripe\Admin\LeftAndMain; use SilverStripe\Admin\LeftAndMain;
class MyController extends LeftAndMain class MyController extends LeftAndMain
{ {
class myaction() class myaction()
{ {
// ... // ...
$this->getResponse()->addHeader('X-Controller', 'MyOtherController'); $this->getResponse()->addHeader('X-Controller', 'MyOtherController');
return $html; return $html;
}
} }
}
``` ```
Built-in headers are: Built-in headers are:
* `X-Title`: Set window title (requires URL encoding) * `X-Title`: Set window title (requires URL encoding)
* `X-Controller`: PHP class name matching a menu entry, which is marked active * `X-Controller`: PHP class name matching a menu entry, which is marked active
* `X-ControllerURL`: Alternative URL to record in the HTML5 browser history * `X-ControllerURL`: Alternative URL to record in the HTML5 browser history
* `X-Status`: Extended status information, used for an information popover. * `X-Status`: Extended status information, used for an information popover.
* `X-Reload`: Force a full page reload based on `X-ControllerURL` * `X-Reload`: Force a full page reload based on `X-ControllerURL`
## Special Links ## Special Links
@ -572,12 +580,12 @@ which is picked up by the menu:
```php ```php
public function mycontrollermethod() public function mycontrollermethod()
{ {
// .. logic here // .. logic here
$this->getResponse()->addHeader('X-Controller', 'AssetAdmin'); $this->getResponse()->addHeader('X-Controller', 'AssetAdmin');
return 'my response'; return 'my response';
} }
``` ```
This is usually handled by the existing [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) logic, This is usually handled by the existing [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) logic,
@ -626,29 +634,29 @@ Form template with custom tab navigation (trimmed down):
```ss ```ss
<form $FormAttributes data-layout-type="border"> <form $FormAttributes data-layout-type="border">
<div class="cms-content-header north"> <div class="cms-content-header north">
<% if Fields.hasTabset %> <% if Fields.hasTabset %>
<% with Fields.fieldByName('Root') %> <% with Fields.fieldByName('Root') %>
<div class="cms-content-header-tabs"> <div class="cms-content-header-tabs">
<ul> <ul>
<% loop Tabs %> <% loop Tabs %>
<li><a href="#$id">$Title</a></li> <li><a href="#$id">$Title</a></li>
<% end_loop %> <% end_loop %>
</ul> </ul>
</div> </div>
<% end_with %> <% end_with %>
<% end_if %> <% end_if %>
</div> </div>
<div class="cms-content-fields center"> <div class="cms-content-fields center">
<fieldset> <fieldset>
<% loop Fields %>$FieldHolder<% end_loop %> <% loop Fields %>$FieldHolder<% end_loop %>
</fieldset> </fieldset>
</div> </div>
</form> </form>
``` ```
Tabset template without tab navigation (e.g. `CMSTabset.ss`) Tabset template without tab navigation (e.g. `CMSTabset.ss`)
@ -656,19 +664,19 @@ Tabset template without tab navigation (e.g. `CMSTabset.ss`)
```ss ```ss
<div $AttributesHTML> <div $AttributesHTML>
<% loop Tabs %> <% loop Tabs %>
<% if Tabs %> <% if Tabs %>
$FieldHolder $FieldHolder
<% else %> <% else %>
<div $AttributesHTML> <div $AttributesHTML>
<% loop Fields %> <% loop Fields %>
$FieldHolder $FieldHolder
<% end_loop %> <% end_loop %>
</div> </div>
<% end_if %> <% end_if %>
<% end_loop %> <% end_loop %>
</div> </div>
``` ```
Lazy loading works based on the `href` attribute of the tab navigation. Lazy loading works based on the `href` attribute of the tab navigation.
@ -682,20 +690,20 @@ and load the HTML content into the main view. Example:
```ss ```ss
<div id="my-tab-id" class="cms-tabset" data-ignore-tab-state="true"> <div id="my-tab-id" class="cms-tabset" data-ignore-tab-state="true">
<ul> <ul>
<li class="<% if MyActiveCondition %> ui-tabs-active<% end_if %>"> <li class="<% if MyActiveCondition %> ui-tabs-active<% end_if %>">
<a href="{$AdminURL}mytabs/tab1" class="cms-panel-link"> <a href="{$AdminURL}mytabs/tab1" class="cms-panel-link">
Tab1 Tab1
</a> </a>
</li> </li>
<li class="<% if MyActiveCondition %> ui-tabs-active<% end_if %>"> <li class="<% if MyActiveCondition %> ui-tabs-active<% end_if %>">
<a href="{$AdminURL}mytabs/tab2" class="cms-panel-link"> <a href="{$AdminURL}mytabs/tab2" class="cms-panel-link">
Tab2 Tab2
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
``` ```
The URL endpoints `{$AdminURL}mytabs/tab1` and `{$AdminURL}mytabs/tab2` The URL endpoints `{$AdminURL}mytabs/tab1` and `{$AdminURL}mytabs/tab2`

View File

@ -23,8 +23,7 @@ The easiest way to update the layout of the CMS is to call `redraw` on the top-l
```js ```js
$('.cms-container').redraw();
$('.cms-container').redraw();
``` ```
This causes the framework to: This causes the framework to:
@ -65,13 +64,12 @@ Layout manager will automatically apply algorithms to the children of `.cms-cont
```html ```html
<div class="cms-content-tools west cms-panel cms-panel-layout"
<div class="cms-content-tools west cms-panel cms-panel-layout" data-expandOnClick="true"
data-expandOnClick="true" data-layout-type="border"
data-layout-type="border" id="cms-content-tools-CMSMain">
id="cms-content-tools-CMSMain"> <%-- content utilising border's north, south, east, west and center classes --%>
<%-- content utilising border's north, south, east, west and center classes --%> </div>
</div>
``` ```
For detailed discussion on available algorithms refer to For detailed discussion on available algorithms refer to
@ -112,8 +110,7 @@ Use provided factory method to generate algorithm instances.
```js ```js
jLayout.threeColumnCompressor(<column-spec-object>, <options-object>);
jLayout.threeColumnCompressor(<column-spec-object>, <options-object>);
``` ```
The parameters are as follows: The parameters are as follows:

View File

@ -52,28 +52,27 @@ Note how the configuration happens in different entwine namespaces
```js ```js
(function($) {
(function($) { $.entwine('ss.preview', function($){
$.entwine('ss.preview', function($){ $('.cms-preview').entwine({
$('.cms-preview').entwine({ DefaultMode: 'content',
DefaultMode: 'content', getSizes: function() {
getSizes: function() { var sizes = this._super();
var sizes = this._super(); sizes.mobile.width = '400px';
sizes.mobile.width = '400px'; return sizes;
return sizes; }
}
});
}); });
$.entwine('ss', function($){ });
$('.cms-container').entwine({ $.entwine('ss', function($){
getLayoutOptions: function() { $('.cms-container').entwine({
var opts = this._super(); getLayoutOptions: function() {
opts.minPreviewWidth = 600; var opts = this._super();
return opts; opts.minPreviewWidth = 600;
} return opts;
}); }
}); });
}(jQuery)); });
}(jQuery));
``` ```
Load the file in the CMS via setting adding 'mysite/javascript/MyLeftAndMain.Preview.js' Load the file in the CMS via setting adding 'mysite/javascript/MyLeftAndMain.Preview.js'
@ -81,10 +80,9 @@ to the `LeftAndMain.extra_requirements_javascript` [configuration value](../conf
```yml ```yml
SilverStripe\Admin\LeftAndMain:
LeftAndMain: extra_requirements_javascript:
extra_requirements_javascript: - mysite/javascript/MyLeftAndMain.Preview.js
- mysite/javascript/MyLeftAndMain.Preview.js
``` ```
In order to find out which configuration values are available, the source code In order to find out which configuration values are available, the source code
@ -115,9 +113,9 @@ property.
States are the site stages: _live_, _stage_ etc. Preview states are picked up States are the site stages: _live_, _stage_ etc. Preview states are picked up
from the `SilverStripeNavigator`. You can invoke the state change by calling: from the `SilverStripeNavigator`. You can invoke the state change by calling:
```js ```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 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 the _Link_ in their names. This call will also redraw the state selector to fit
@ -126,9 +124,9 @@ list of supported states.
You can get the current state by calling: You can get the current state by calling:
```js ```js
$('.cms-preview').entwine('.ss.preview').getCurrentStateName(); $('.cms-preview').entwine('.ss.preview').getCurrentStateName();
``` ```
## Preview sizes ## Preview sizes
@ -145,15 +143,15 @@ You can switch between different types of display sizes programmatically, which
has the benefit of redrawing the related selector and maintaining a consistent has the benefit of redrawing the related selector and maintaining a consistent
internal state: internal state:
```js ```js
$('.cms-preview').entwine('.ss.preview').changeSize('auto'); $('.cms-preview').entwine('.ss.preview').changeSize('auto');
``` ```
You can find out current size by calling: You can find out current size by calling:
```js ```js
$('.cms-preview').entwine('.ss.preview').getCurrentSizeName(); $('.cms-preview').entwine('.ss.preview').getCurrentSizeName();
``` ```
## Preview modes ## Preview modes
@ -161,16 +159,16 @@ Preview modes map to the modes supported by the _threeColumnCompressor_ layout
algorithm, see [layout reference](cms_layout) for more details. You algorithm, see [layout reference](cms_layout) for more details. You
can change modes by calling: can change modes by calling:
```js ```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 Currently active mode is stored on the `.cms-container` along with related
internal states of the layout. You can reach it by calling: internal states of the layout. You can reach it by calling:
```js ```js
$('.cms-container').entwine('.ss').getLayoutOptions().mode; $('.cms-container').entwine('.ss').getLayoutOptions().mode;
``` ```
<div class="notice" markdown='1'> <div class="notice" markdown='1'>
Caveat: the `.preview-mode-selector` appears twice, once in the preview and Caveat: the `.preview-mode-selector` appears twice, once in the preview and

View File

@ -8,7 +8,9 @@ SilverStripe lets you customise the style of content in the CMS. This is done by
```php ```php
HtmlEditorConfig::get('cms')->setOption('content_css', project() . '/css/editor.css'); use SilverStripe\Forms\HTMLEditor\HtmlEditorConfig;
HtmlEditorConfig::get('cms')->setOption('content_css', project() . '/css/editor.css');
``` ```
Will load the `mysite/css/editor.css` file. Will load the `mysite/css/editor.css` file.
@ -17,7 +19,7 @@ If using this config option in `mysite/_config.php`, you will have to instead ca
```php ```php
HtmlEditorConfig::get('cms')->setOption('content_css', project() . '/css/editor.css'); 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 Any CSS classes within this file will be automatically added to the `WYSIWYG` editors 'style' dropdown. For instance, to
@ -25,10 +27,9 @@ add the color 'red' as an option within the `WYSIWYG` add the following to the `
```css ```css
.red {
.red { color: red;
color: red; }
}
``` ```
<div class="notice" markdown="1"> <div class="notice" markdown="1">

View File

@ -45,13 +45,12 @@ 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. You should write all your custom jQuery code in a closure.
```javascript ```js
(function($) {
(function($) { $(document).ready(function(){
$(document).ready(function(){ // your code here.
// your code here. })
}) })(jQuery);
})(jQuery);
``` ```
## jQuery Plugins ## jQuery Plugins
@ -75,49 +74,47 @@ Example: A plugin to highlight a collection of elements with a configurable fore
```js ```js
// create closure
// create closure (function($) {
(function($) { // plugin definition
// plugin definition $.fn.hilight = function(options) {
$.fn.hilight = function(options) { // build main options before element iteration
// build main options before element iteration var opts = $.extend({}, $.fn.hilight.defaults, options);
var opts = $.extend({}, $.fn.hilight.defaults, options); // iterate and reformat each matched element
// iterate and reformat each matched element return this.each(function() {
return this.each(function() { $this = $(this);
$this = $(this); // build element specific options
// build element specific options var o = $.meta ? $.extend({}, opts, $this.data()) : opts;
var o = $.meta ? $.extend({}, opts, $this.data()) : opts; // update element styles
// update element styles $this.css({
$this.css({ backgroundColor: o.background,
backgroundColor: o.background, color: o.foreground
color: o.foreground });
}); });
}); };
}; // plugin defaults
// plugin defaults $.fn.hilight.defaults = {
$.fn.hilight.defaults = { foreground: "red",
foreground: "red", background: "yellow"
background: "yellow" };
}; // end of closure
// end of closure })(jQuery);
})(jQuery);
``` ```
Usage: Usage:
```js ```js
(function($) {
// Highlight all buttons with default colours
jQuery(':button').highlight();
(function($) { // Highlight all buttons with green background
// Highlight all buttons with default colours jQuery(':button').highlight({background: "green"});
jQuery(':button').highlight();
// Highlight all buttons with green background // Set all further highlight() calls to have a green background
jQuery(':button').highlight({background: "green"}); $.fn.hilight.defaults.background = "green";
})(jQuery);
// Set all further highlight() calls to have a green background
$.fn.hilight.defaults.background = "green";
})(jQuery);
``` ```
## jQuery UI Widgets ## jQuery UI Widgets
@ -137,57 +134,54 @@ See the [official developer guide](http://jqueryui.com/docs/Developer_Guide) and
Example: Highlighter Example: Highlighter
```js ```js
(function($) {
(function($) { $.widget("ui.myHighlight", {
$.widget("ui.myHighlight", { getBlink: function () {
getBlink: function () { return this._getData('blink');
return this._getData('blink'); },
}, setBlink: function (blink) {
setBlink: function (blink) { this._setData('blink', blink);
this._setData('blink', blink); if(blink) this.element.wrapInner('<blink></blink>');
if(blink) this.element.wrapInner('<blink></blink>'); else this.element.html(this.element.children().html());
else this.element.html(this.element.children().html()); },
}, _init: function() {
_init: function() { // grab the default value and use it
// grab the default value and use it this.element.css('background',this.options.background);
this.element.css('background',this.options.background); this.element.css('color',this.options.foreground);
this.element.css('color',this.options.foreground); this.setBlink(this.options.blink);
this.setBlink(this.options.blink); }
} });
}); // For demonstration purposes, this is also possible with jQuery.css()
// For demonstration purposes, this is also possible with jQuery.css() $.ui.myHighlight.getter = "getBlink";
$.ui.myHighlight.getter = "getBlink"; $.ui.myHighlight.defaults = {
$.ui.myHighlight.defaults = { foreground: "red",
foreground: "red", background: "yellow",
background: "yellow", blink: false
blink: false };
}; })(jQuery);
})(jQuery);
``` ```
Usage: Usage:
```js ```js
(function($) {
// call with default options
$(':button').myHighlight();
(function($) { // call with custom options
// call with default options $(':button').myHighlight({background: "green"});
$(':button').myHighlight();
// call with custom options // set defaults for all future instances
$(':button').myHighlight({background: "green"}); $.ui.myHighlight.defaults.background = "green";
// set defaults for all future instances // Adjust property after initialization
$.ui.myHighlight.defaults.background = "green"; $(':button').myHighlight('setBlink', true);
// Adjust property after initialization // Get property
$(':button').myHighlight('setBlink', true); $(':button').myHighlight('getBlink');
})(jQuery);
// Get property
$(':button').myHighlight('getBlink');
})(jQuery);
``` ```
### jQuery.Entwine ### jQuery.Entwine
@ -205,34 +199,32 @@ Example: Highlighter
```js ```js
(function($) {
(function($) { $(':button').entwine({
$(':button').entwine({ Foreground: 'red',
Foreground: 'red', Background: 'yellow',
Background: 'yellow', highlight: function() {
highlight: function() { this.css('background', this.getBackground());
this.css('background', this.getBackground()); this.css('color', this.getForeground());
this.css('color', this.getForeground()); }
} });
}); })(jQuery);
})(jQuery);
``` ```
Usage: Usage:
```js ```js
(function($) {
// call with default options
$(':button').entwine().highlight();
(function($) { // set options for existing and new instances
// call with default options $(':button').entwine().setBackground('green');
$(':button').entwine().highlight();
// set options for existing and new instances // get property
$(':button').entwine().setBackground('green'); $(':button').entwine().getBackground();
})(jQuery);
// 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 This is a deliberately simple example, the strength of jQuery.entwine over simple jQuery plugins lies in its public
@ -255,12 +247,11 @@ Global properties are evil. They are accessible by other scripts, might be overw
```js ```js
// you can't rely on '$' being defined outside of the closure
// you can't rely on '$' being defined outside of the closure (function($) {
(function($) { var myPrivateVar; // only available inside the closure
var myPrivateVar; // only available inside the closure // inside here you can use the 'jQuery' object as '$'
// inside here you can use the 'jQuery' object as '$' })(jQuery);
})(jQuery);
``` ```
You can run `[jQuery.noConflict()](http://docs.jquery.com/Core/jQuery.noConflict)` to avoid namespace clashes. You can run `[jQuery.noConflict()](http://docs.jquery.com/Core/jQuery.noConflict)` to avoid namespace clashes.
@ -273,11 +264,10 @@ the `window.onload` and `document.ready` events.
```js ```js
// DOM elements might not be available here
// DOM elements might not be available here $(document).ready(function() {
$(document).ready(function() { // The DOM is fully loaded here
// The DOM is fully loaded here });
});
``` ```
See [jQuery FAQ: Launching Code on Document See [jQuery FAQ: Launching Code on Document
@ -292,18 +282,16 @@ Caution: Only applies to certain events, see the [jQuery.on() documentation](htt
Example: Add a 'loading' classname to all pressed buttons Example: Add a 'loading' classname to all pressed buttons
```js ```js
// manual binding, only applies to existing elements
$('input[[type=submit]]').on('click', function() {
$(this).addClass('loading');
});
// manual binding, only applies to existing elements // binding, applies to any inserted elements as well
$('input[[type=submit]]').on('click', function() { $('.cms-container').on('click', 'input[[type=submit]]', function() {
$(this).addClass('loading'); $(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 ### Assume Element Collections
@ -313,13 +301,12 @@ makes sense). Encapsulate your code by nesting your jQuery commands inside a `jQ
```js ```js
$('div.MyGridField').each(function() {
$('div.MyGridField').each(function() { // This is the over code for the tr elements inside a GridField.
// This is the over code for the tr elements inside a GridField. $(this).find('tr').hover(
$(this).find('tr').hover( // ...
// ... );
); });
});
``` ```
### Use plain HTML and jQuery.data() to store data ### Use plain HTML and jQuery.data() to store data
@ -333,27 +320,24 @@ Through CSS properties
```js ```js
$('form :input').bind('change', function(e) {
$('form :input').bind('change', function(e) { $(this.form).addClass('isChanged');
$(this.form).addClass('isChanged'); });
}); $('form').bind('submit', function(e) {
$('form').bind('submit', function(e) { if($(this).hasClass('isChanged')) return false;
if($(this).hasClass('isChanged')) return false; });
});
``` ```
Through jQuery.data() Through jQuery.data()
```js ```js
$('form :input').bind('change', function(e) {
$('form :input').bind('change', function(e) { $(this.form).data('isChanged', true);
$(this.form).data('isChanged', true); });
}); $('form').bind('submit', function(e) {
$('form').bind('submit', function(e) { alert($(this).data('isChanged'));
alert($(this).data('isChanged')); if($(this).data('isChanged')) return false;
if($(this).data('isChanged')) return false; });
});
``` ```
See [interactive example on jsbin.com](http://jsbin.com/opuva) See [interactive example on jsbin.com](http://jsbin.com/opuva)
@ -364,25 +348,20 @@ rendering a form element through the SilverStripe templating engine.
Example: Restricted numeric value field Example: Restricted numeric value field
```ss ```ss
<input type="text" class="restricted-text {min:4,max:10}" />
<input type="text" class="restricted-text {min:4,max:10}" />
``` ```
```js ```js
$('.restricted-text').bind('change', function(e) {
$('.restricted-text').bind('change', function(e) { if(
if( e.target.value < $(this).metadata().min
e.target.value < $(this).metadata().min || e.target.value > $(this).metadata().max
|| e.target.value > $(this).metadata().max ) {
) { alert('Invalid value');
alert('Invalid value'); return false;
return false; }
} });
});
``` ```
See [interactive example on jsbin.com](http://jsbin.com/axafa) See [interactive example on jsbin.com](http://jsbin.com/axafa)
@ -407,74 +386,73 @@ Template:
```ss ```ss
<ul>
<ul> <% loop $Results %>
<% loop $Results %> <li id="Result-$ID">$Title</li>
<li id="Result-$ID">$Title</li> <% end_loop %>
<% end_loop %> </ul>
</ul>
``` ```
PHP: PHP:
```php ```php
class MyController use SilverStripe\Control\HTTPResponse;
{ use SilverStripe\View\ViewableData;
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 class MyController
$this->getResponse()->setStatusCode(200, "Found " . $results->Count() . " elements"); {
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 // Use HTTPResponse to pass custom status messages
$vd = new ViewableData(); $this->getResponse()->setStatusCode(200, "Found " . $results->Count() . " elements");
return $vd->customise([
"Results" => $results
])->renderWith('AutoComplete');
}
}
// render all results with a custom template
$vd = new ViewableData();
return $vd->customise([
"Results" => $results
])->renderWith('AutoComplete');
}
}
``` ```
HTML HTML
```ss ```ss
<form action"#">
<form action"#"> <div class="autocomplete {url:'MyController/autocomplete'}">
<div class="autocomplete {url:'MyController/autocomplete'}"> <input type="text" name="title" />
<input type="text" name="title" /> <div class="results" style="display: none;">
<div class="results" style="display: none;"> </div>
</div> <input type="submit" value="action_autocomplete" />
<input type="submit" value="action_autocomplete" /> </form>
</form>
``` ```
JavaScript: JavaScript:
```js ```js
$('.autocomplete input').on('change', function() {
$('.autocomplete input').on('change', function() { var resultsEl = $(this).siblings('.results');
var resultsEl = $(this).siblings('.results'); resultsEl.load(
resultsEl.load( // get form action, using the jQuery.metadata plugin
// get form action, using the jQuery.metadata plugin $(this).parent().metadata().url,
$(this).parent().metadata().url, // submit all form values
// submit all form values $(this.form).serialize(),
$(this.form).serialize(), // callback after data is loaded
// callback after data is loaded function(data, status) {
function(data, status) { resultsEl.show();
resultsEl.show(); // get all record IDs from the new HTML
// get all record IDs from the new HTML var ids = jQuery('.results').find('li').map(function() {
var ids = jQuery('.results').find('li').map(function() { return $(this).attr('id').replace(/Record\-/,'');
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 Although they are the minority of cases, there are times when a simple HTML fragment isn't enough. For example, if you
@ -498,21 +476,20 @@ Example: Trigger custom 'validationfailed' event on form submission for each emp
```js ```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;
});
$('form').bind('submit', function(e) { // listen to custom event on each <input> field
// $(this) refers to form $('form :input').bind('validationfailed',function(e) {
$(this).find(':input').each(function() { // $(this) refers to input field
// $(this) in here refers to input field alert($(this).attr('name'));
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). See [interactive example on jsbin.com](http://jsbin.com/ipeca).
@ -555,55 +532,53 @@ JSDoc-toolkit is a command line utility, see [usage](http://code.google.com/p/js
Example: jQuery.entwine Example: jQuery.entwine
```js ```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 */ {
/**
* Reference to some property
* @type Number
*/
MyProperty: 123,
/** /**
* Available Custom Events: * Renders the provided data into an unordered list.
* <ul>
* <li>ajaxsubmit</li>
* <li>validate</li>
* <li>reloadeditform</li>
* </ul>
* *
* @class Main LeftAndMain interface with some control panel and an edit form. * @param {Object} data
* @name ss.LeftAndMain * @param {String} status
* @return {String} HTML unordered list
*/ */
$('.LeftAndMain').entwine('ss', function($){ publicMethod: function(data, status) {
return/** @lends ss.LeftAndMain */ { return '<ul>'
/** + /...
+ '</ul>';
},
* Reference to some property /**
* @type Number
*/
MyProperty: 123,
/** * Won't show in documentation, but still worth documenting.
*
* Renders the provided data into an unordered list. * @return {String} Something else.
* */
* @param {Object} data _privateMethod: function() {
* @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 ### Unit Testing
@ -616,31 +591,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)): Example: QUnit test (from [jquery.com](http://docs.jquery.com/QUnit#Using_QUnit)):
```js ```js
test("a basic test example", function() {
test("a basic test example", function() { ok( true, "this test is fine" );
ok( true, "this test is fine" ); var value = "hello";
var value = "hello"; equals( "hello", value, "We expect value to be hello" );
equals( "hello", value, "We expect value to be hello" ); });
});
``` ```
Example: JSpec Shopping cart test (from [visionmedia.github.com](http://visionmedia.github.com/jspec/)) Example: JSpec Shopping cart test (from [visionmedia.github.com](http://visionmedia.github.com/jspec/))
``` ```
describe 'ShoppingCart' describe 'ShoppingCart'
before_each before_each
cart = new ShoppingCart cart = new ShoppingCart
end end
describe 'addProduct' describe 'addProduct'
it 'should add a product' it 'should add a product'
cart.addProduct('cookie') cart.addProduct('cookie')
cart.addProduct('icecream') cart.addProduct('icecream')
cart.should.have 2, 'products' cart.should.have 2, 'products'
end
end
end end
end
end
``` ```
## Related ## Related
* [Unobtrusive Javascript](http://www.onlinetools.org/articles/unobtrusivejavascript/chapter1.html) * [Unobtrusive Javascript](http://www.onlinetools.org/articles/unobtrusivejavascript/chapter1.html)

View File

@ -28,21 +28,21 @@ Here is the configuration code for the button:
```php ```php
public function getCMSActions() public function getCMSActions()
{ {
$fields = parent::getCMSActions(); $fields = parent::getCMSActions();
$fields->fieldByName('MajorActions')->push( $fields->fieldByName('MajorActions')->push(
$cleanupAction = FormAction::create('cleanup', 'Cleaned') $cleanupAction = FormAction::create('cleanup', 'Cleaned')
// Set up an icon for the neutral state that will use the default text. // Set up an icon for the neutral state that will use the default text.
->setAttribute('data-icon', 'accept') ->setAttribute('data-icon', 'accept')
// Initialise the alternate constructive state. // Initialise the alternate constructive state.
->setAttribute('data-icon-alternate', 'addpage') ->setAttribute('data-icon-alternate', 'addpage')
->setAttribute('data-text-alternate', 'Clean-up now') ->setAttribute('data-text-alternate', 'Clean-up now')
); );
return $fields; return $fields;
} }
``` ```
You can control the state of the button from the backend by applying `ss-ui-alternate` class to the `FormAction`. To You can control the state of the button from the backend by applying `ss-ui-alternate` class to the `FormAction`. To
@ -55,15 +55,15 @@ Here we initialise the button based on the backend check, and assume that the bu
```php ```php
public function getCMSActions() public function getCMSActions()
{ {
// ... // ...
if ($this->needsCleaning()) { if ($this->needsCleaning()) {
// Will initialise the button into alternate state. // Will initialise the button into alternate state.
$cleanupAction->addExtraClass('ss-ui-alternate'); $cleanupAction->addExtraClass('ss-ui-alternate');
}
// ...
} }
// ...
}
``` ```
## Frontend support ## Frontend support
@ -80,24 +80,21 @@ First of all, you can toggle the state of the button - execute this code in the
```js ```js
jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button('toggleAlternate');
jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button('toggleAlternate');
``` ```
Another, more useful, scenario is to check the current state. Another, more useful, scenario is to check the current state.
```js ```js
jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button('option', 'showingAlternate');
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. You can also force the button into a specific state by using UI options.
```js ```js
jQuery('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').button({showingAlternate: true});
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 This will allow you to react to user actions in the CMS and give immediate feedback. Here is an example taken from the
@ -106,24 +103,23 @@ CMS core that tracks the changes to the input fields and reacts by enabling the
```js ```js
/**
/** * Enable save buttons upon detecting changes to content.
* Enable save buttons upon detecting changes to content. * "changed" class is added by jQuery.changetracker.
* "changed" class is added by jQuery.changetracker. */
*/ $('.cms-edit-form .changed').entwine({
$('.cms-edit-form .changed').entwine({ // This will execute when the class is added to the element.
// This will execute when the class is added to the element. onmatch: function(e) {
onmatch: function(e) { var form = this.closest('.cms-edit-form');
var form = this.closest('.cms-edit-form'); form.find('#Form_EditForm_action_save').button({showingAlternate: true});
form.find('#Form_EditForm_action_save').button({showingAlternate: true}); form.find('#Form_EditForm_action_publish').button({showingAlternate: true});
form.find('#Form_EditForm_action_publish').button({showingAlternate: true}); this._super(e);
this._super(e); },
}, // Entwine requires us to define this, even if we don't use it.
// Entwine requires us to define this, even if we don't use it. onunmatch: function(e) {
onunmatch: function(e) { this._super(e);
this._super(e); }
} });
});
``` ```
## Frontend hooks ## Frontend hooks
@ -156,27 +152,26 @@ cases.
```js ```js
(function($) {
(function($) { $.entwine('mysite', function($){
$('.cms-edit-form .btn-toolbar #Form_EditForm_action_cleanup').entwine({
$.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).
* 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')) {
onbuttonafterrefreshalternate: function() { this.addClass('ss-ui-action-constructive');
if (this.button('option', 'showingAlternate')) {
this.addClass('ss-ui-action-constructive');
}
else {
this.removeClass('ss-ui-action-constructive');
}
} }
}); else {
this.removeClass('ss-ui-action-constructive');
}
}
}); });
});
}(jQuery)); }(jQuery));
``` ```
## Summary ## Summary

View File

@ -11,8 +11,10 @@ at the last position within the field, and expects unescaped HTML content.
```php ```php
TextField::create('MyText', 'My Text Label') use SilverStripe\Forms\TextField;
->setDescription('More <strong>detailed</strong> help');
TextField::create('MyText', 'My Text Label')
->setDescription('More <strong>detailed</strong> help');
``` ```
To show the help text as a tooltip instead of inline, To show the help text as a tooltip instead of inline,
@ -20,9 +22,9 @@ add a `.cms-description-tooltip` class.
```php ```php
TextField::create('MyText', 'My Text Label') TextField::create('MyText', 'My Text Label')
->setDescription('More <strong>detailed</strong> help') ->setDescription('More <strong>detailed</strong> help')
->addExtraClass('cms-description-tooltip'); ->addExtraClass('cms-description-tooltip');
``` ```
Tooltips are only supported Tooltips are only supported
@ -40,9 +42,9 @@ by clicking the 'info' icon displayed alongside the field.
```php ```php
TextField::create('MyText', 'My Text Label') TextField::create('MyText', 'My Text Label')
->setDescription('More <strong>detailed</strong> help') ->setDescription('More <strong>detailed</strong> help')
->addExtraClass('cms-description-toggle'); ->addExtraClass('cms-description-toggle');
``` ```
If you want to provide a custom icon for toggling the description, you can do that If you want to provide a custom icon for toggling the description, you can do that
@ -50,10 +52,10 @@ by setting an additional `RightTitle`.
```php ```php
TextField::create('MyText', 'My Text Label') TextField::create('MyText', 'My Text Label')
->setDescription('More <strong>detailed</strong> help') ->setDescription('More <strong>detailed</strong> help')
->addExtraClass('cms-description-toggle') ->addExtraClass('cms-description-toggle')
->setRightTitle('<a class="cms-description-trigger">My custom icon</a>'); ->setRightTitle('<a class="cms-description-trigger">My custom icon</a>');
``` ```
Note: For more advanced help text we recommend using Note: For more advanced help text we recommend using

View File

@ -22,13 +22,13 @@ black-and-transparent PNG graphics. In this case we'll place the icon in
```php ```php
use SilverStripe\Admin\ModelAdmin; use SilverStripe\Admin\ModelAdmin;
class ProductAdmin extends ModelAdmin class ProductAdmin extends ModelAdmin
{ {
// ... // ...
private static $menu_icon = 'mysite/images/product-icon.png'; private static $menu_icon = 'mysite/images/product-icon.png';
} }
``` ```
### Defining a Custom Title ### Defining a Custom Title
@ -39,13 +39,13 @@ controller, removing the "Admin" bit at the end.
```php ```php
use SilverStripe\Admin\ModelAdmin; use SilverStripe\Admin\ModelAdmin;
class ProductAdmin extends ModelAdmin class ProductAdmin extends ModelAdmin
{ {
// ... // ...
private static $menu_title = 'My Custom Admin'; private static $menu_title = 'My Custom Admin';
} }
``` ```
In order to localize the menu title in different languages, use the In order to localize the menu title in different languages, use the
@ -66,37 +66,36 @@ button configuration.
```php ```php
use SilverStripe\Admin\CMSMenu; use SilverStripe\Admin\CMSMenu;
use SilverStripe\Admin\LeftAndMainExtension; use SilverStripe\Admin\LeftAndMainExtension;
class CustomLeftAndMain extends LeftAndMainExtension class CustomLeftAndMain extends LeftAndMainExtension
{
public function init()
{ {
// unique identifier for this item. Will have an ID of Menu-$ID
$id = 'LinkToGoogle';
public function init() // your 'nice' title
{ $title = 'Google';
// unique identifier for this item. Will have an ID of Menu-$ID
$id = 'LinkToGoogle';
// your 'nice' title // the link you want to item to go to
$title = 'Google'; $link = 'http://google.com';
// the link you want to item to go to // priority controls the ordering of the link in the stack. The
$link = 'http://google.com'; // lower the number, the lower in the list
$priority = -2;
// priority controls the ordering of the link in the stack. The // Add your own attributes onto the link. In our case, we want to
// lower the number, the lower in the list // open the link in a new window (not the original)
$priority = -2; $attributes = [
'target' => '_blank'
];
// Add your own attributes onto the link. In our case, we want to CMSMenu::add_link($id, $title, $link, $priority, $attributes);
// 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` To have the link appear, make sure you add the extension to the `LeftAndMain`
@ -105,7 +104,7 @@ class. For more information about configuring extensions see the
```php ```php
LeftAndMain::add_extension('CustomLeftAndMain') LeftAndMain::add_extension('CustomLeftAndMain')
``` ```
## Related ## Related

View File

@ -18,23 +18,24 @@ Here's a brief example on how to add sorting and a new column for a
hypothetical `NewsPageHolder` type, which contains `NewsPage` children. hypothetical `NewsPageHolder` type, which contains `NewsPage` children.
**mysite/code/NewsPageHolder.php**
```php ```php
use Page; class NewsPageHolder extends Page
{
private static $allowed_children = ['NewsPage'];
}
```
// mysite/code/NewsPageHolder.php **mysite/code/NewsPage.php**
class NewsPageHolder extends Page
{
private static $allowed_children = ['NewsPage'];
}
// mysite/code/NewsPage.php
class NewsPage extends Page
{
private static $has_one = [
'Author' => 'Member',
];
}
```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. We'll now add an `Extension` subclass to `LeftAndMain`, which is the main CMS controller.
@ -43,43 +44,44 @@ 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, although it's just as easy to implement changes which apply to all page types,
or across page types with common characteristics. or across page types with common characteristics.
**mysite/code/NewsPageHolderCMSMainExtension.php**
```php ```php
use Page; use SilverStripe\Core\Extension;
use SilverStripe\Core\Extension;
// mysite/code/NewsPageHolderCMSMainExtension.php class NewsPageHolderCMSMainExtension extends Extension
class NewsPageHolderCMSMainExtension extends Extension {
{ public function updateListView($listView) {
function updateListView($listView) { $parentId = $listView->getController()->getRequest()->requestVar('ParentID');
$parentId = $listView->getController()->getRequest()->requestVar('ParentID'); $parent = ($parentId) ? Page::get()->byId($parentId) : new Page();
$parent = ($parentId) ? Page::get()->byId($parentId) : new Page();
// Only apply logic for this page type // Only apply logic for this page type
if($parent && $parent instanceof NewsPageHolder) { if($parent && $parent instanceof NewsPageHolder) {
$gridField = $listView->Fields()->dataFieldByName('Page'); $gridField = $listView->Fields()->dataFieldByName('Page');
if($gridField) { if($gridField) {
// Sort by created // Sort by created
$list = $gridField->getList(); $list = $gridField->getList();
$gridField->setList($list->sort('Created', 'DESC')); $gridField->setList($list->sort('Created', 'DESC'));
// Add author to columns // Add author to columns
$cols = $gridField->getConfig()->getComponentByType('GridFieldDataColumns'); $cols = $gridField->getConfig()->getComponentByType('GridFieldDataColumns');
if($cols) { if($cols) {
$fields = $cols->getDisplayFields($gridField); $fields = $cols->getDisplayFields($gridField);
$fields['Author.Title'] = 'Author'; $fields['Author.Title'] = 'Author';
$cols->setDisplayFields($fields); $cols->setDisplayFields($fields);
}
} }
} }
} }
} }
}
``` ```
Now you just need to enable the extension in your [configuration file](../../configuration). Now you just need to enable the extension in your [configuration file](../../configuration).
```yml ```yml
// mysite/_config/config.yml // mysite/_config/config.yml
LeftAndMain: SilverStripe\Admin\LeftAndMain:
extensions: extensions:
- NewsPageHolderCMSMainExtension - NewsPageHolderCMSMainExtension
``` ```
You're all set! Don't forget to flush the caches by appending `?flush=all` to the URL. You're all set! Don't forget to flush the caches by appending `?flush=all` to the URL.

View File

@ -23,24 +23,24 @@ code like this:
```ss ```ss
...
<ul>
... ...
<ul> <li id="record-15" class="class-Page closed jstree-leaf jstree-unchecked" data-id="15">
... <ins class="jstree-icon">&nbsp;</ins>
<li id="record-15" class="class-Page closed jstree-leaf jstree-unchecked" data-id="15"> <a class="" title="Page type: Page" href="{$AdminURL}page/edit/show/15">
<ins class="jstree-icon">&nbsp;</ins> <ins class="jstree-checkbox">&nbsp;</ins>
<a class="" title="Page type: Page" href="{$AdminURL}page/edit/show/15"> <ins class="jstree-icon">&nbsp;</ins>
<ins class="jstree-checkbox">&nbsp;</ins> <span class="text">
<ins class="jstree-icon">&nbsp;</ins> <span class="jstree-pageicon"></span>
<span class="text"> <span class="item" title="Deleted">New Page</span>
<span class="jstree-pageicon"></span> <span class="badge deletedonlive">Deleted</span>
<span class="item" title="Deleted">New Page</span> </span>
<span class="badge deletedonlive">Deleted</span> </a>
</span> </li>
</a>
</li>
...
</ul>
... ...
</ul>
...
``` ```
By applying the proper style sheet, the snippet html above could produce the look of: By applying the proper style sheet, the snippet html above could produce the look of:
@ -66,22 +66,22 @@ __Example: using a subclass__
```php ```php
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
class Page extends SiteTree class Page extends SiteTree
{
public function getScheduledToPublish()
{ {
public function getScheduledToPublish() // return either true or false
{
// return either true or false
}
public function getStatusFlags($cached = true)
{
$flags = parent::getStatusFlags($cached);
$flags['scheduledtopublish'] = "Scheduled To Publish";
return $flags;
}
} }
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 The above subclass of [SiteTree](api:SilverStripe\CMS\Model\SiteTree) will add a new flag for indicating its

View File

@ -43,13 +43,13 @@ into the admin page.
__my-module/\_config/config.yml__ __my-module/\_config/config.yml__
```yaml ```yaml
--- ---
Name: my-module Name: my-module
--- ---
SilverStripe\Admin\LeftAndMain: SilverStripe\Admin\LeftAndMain:
extra_requirements_javascript: extra_requirements_javascript:
# The name of this file will depend on how you've configured your build process # The name of this file will depend on how you've configured your build process
- 'my-module/js/dist/main.bundle.js' - 'my-module/js/dist/main.bundle.js'
``` ```
Now that the customisation is applied, our text fields look like this: Now that the customisation is applied, our text fields look like this:
@ -61,6 +61,7 @@ Let's add another customisation to TextField. If the text goes beyond a specifie
length, let's throw a warning in the UI. length, let's throw a warning in the UI.
__my-module/js/components/TextLengthChecker.js__ __my-module/js/components/TextLengthChecker.js__
```js ```js
const TextLengthCheker = (TextField) => (props) => { const TextLengthCheker = (TextField) => (props) => {
const {limit, value } = props; const {limit, value } = props;
@ -86,6 +87,7 @@ For the purposes of demonstration, let's imagine this customisation comes from a
module. module.
__my-module/js/main.js__ __my-module/js/main.js__
```js ```js
import Injector from 'lib/Injector'; import Injector from 'lib/Injector';
import TextLengthChecker from './components/TextLengthChecker'; import TextLengthChecker from './components/TextLengthChecker';

View File

@ -33,36 +33,35 @@ 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. The following example will create a report to list every page on the current site.
###CustomSideReport.php ###CustomSideReport.php
```php ```php
use Page; use SilverStripe\Reports\Report;
use SilverStripe\Reports\Report;
class CustomSideReport_NameOfReport extends Report class CustomSideReport_NameOfReport extends Report
{
// the name of the report
public function title()
{ {
return 'All Pages';
// 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;
}
} }
// 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;
}
}
``` ```
More useful reports can be created by changing the `DataList` returned in the `sourceRecords` function. More useful reports can be created by changing the `DataList` returned in the `sourceRecords` function.

View File

@ -31,18 +31,17 @@ the CMS logic. Add a new section into the `<ul class="cms-menu-list">`
```ss ```ss
...
... <ul class="cms-menu-list">
<ul class="cms-menu-list"> <!-- ... -->
<!-- ... --> <li class="bookmarked-link first">
<li class="bookmarked-link first"> <a href="{$AdminURL}pages/edit/show/1">Edit "My popular page"</a>
<a href="{$AdminURL}pages/edit/show/1">Edit "My popular page"</a> </li>
</li> <li class="bookmarked-link last">
<li class="bookmarked-link last"> <a href="{$AdminURL}pages/edit/show/99">Edit "My other page"</a>
<a href="{$AdminURL}pages/edit/show/99">Edit "My other page"</a> </li>
</li> </ul>
</ul> ...
...
``` ```
Refresh the CMS interface with `admin/?flush=all`, and you should see those Refresh the CMS interface with `admin/?flush=all`, and you should see those
@ -57,8 +56,7 @@ with the CMS interface. Paste the following content into a new file called
```css ```css
.bookmarked-link.first {margin-top: 1em;}
.bookmarked-link.first {margin-top: 1em;}
``` ```
Load the new CSS file into the CMS, by setting the `LeftAndMain.extra_requirements_css` Load the new CSS file into the CMS, by setting the `LeftAndMain.extra_requirements_css`
@ -66,10 +64,9 @@ Load the new CSS file into the CMS, by setting the `LeftAndMain.extra_requiremen
```yml ```yml
SilverStripe\Admin\LeftAndMain:
LeftAndMain: extra_requirements_css:
extra_requirements_css: - mysite/css/BookmarkedPages.css
- mysite/css/BookmarkedPages.css
``` ```
## Create a "bookmark" flag on pages ## Create a "bookmark" flag on pages
@ -81,23 +78,23 @@ and insert the following code.
```php ```php
use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\CheckboxField;
use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataExtension;
class BookmarkedPageExtension extends DataExtension class BookmarkedPageExtension extends DataExtension
{
private static $db = [
'IsBookmarked' => 'Boolean'
];
public function updateCMSFields(FieldList $fields)
{ {
$fields->addFieldToTab('Root.Main',
private static $db = [ new CheckboxField('IsBookmarked', "Show in CMS bookmarks?")
'IsBookmarked' => 'Boolean' );
];
public function updateCMSFields(FieldList $fields)
{
$fields->addFieldToTab('Root.Main',
new CheckboxField('IsBookmarked', "Show in CMS bookmarks?")
);
}
} }
}
``` ```
@ -105,10 +102,9 @@ Enable the extension in your [configuration file](../../configuration)
```yml ```yml
SilverStripe\CMS\Model\SiteTree:
SiteTree: extensions:
extensions: - BookmarkedPageExtension
- BookmarkedPageExtension
``` ```
In order to add the field to the database, run a `dev/build/?flush=all`. In order to add the field to the database, run a `dev/build/?flush=all`.
@ -125,27 +121,25 @@ Add the following code to a new file `mysite/code/BookmarkedLeftAndMainExtension
```php ```php
use Page; use SilverStripe\Admin\LeftAndMainExtension;
use SilverStripe\Admin\LeftAndMainExtension;
class BookmarkedPagesLeftAndMainExtension extends LeftAndMainExtension class BookmarkedPagesLeftAndMainExtension extends LeftAndMainExtension
{
public function BookmarkedPages()
{ {
return Page::get()->filter("IsBookmarked", 1);
public function BookmarkedPages()
{
return Page::get()->filter("IsBookmarked", 1);
}
} }
}
``` ```
Enable the extension in your [configuration file](../../configuration) Enable the extension in your [configuration file](../../configuration)
```yml ```yml
SilverStripe\Admin\LeftAndMain:
LeftAndMain: extensions:
extensions: - BookmarkedPagesLeftAndMainExtension
- BookmarkedPagesLeftAndMainExtension
``` ```
As the last step, replace the hardcoded links with our list from the database. As the last step, replace the hardcoded links with our list from the database.
@ -154,15 +148,14 @@ and replace it with the following:
```ss ```ss
<ul class="cms-menu-list">
<ul class="cms-menu-list"> <!-- ... -->
<!-- ... --> <% loop $BookmarkedPages %>
<% loop $BookmarkedPages %> <li class="bookmarked-link $FirstLast">
<li class="bookmarked-link $FirstLast"> <li><a href="{$AdminURL}pages/edit/show/$ID">Edit "$Title"</a></li>
<li><a href="{$AdminURL}pages/edit/show/$ID">Edit "$Title"</a></li> </li>
</li> <% end_loop %>
<% end_loop %> </ul>
</ul>
``` ```
## Extending the CMS actions ## Extending the CMS actions
@ -197,7 +190,7 @@ button group (`CompositeField`) in a similar fashion.
```php ```php
$fields->unshift(FormAction::create('normal', 'Normal button')); $fields->unshift(FormAction::create('normal', 'Normal button'));
``` ```
We can affect the existing button group by manipulating the `CompositeField` We can affect the existing button group by manipulating the `CompositeField`
@ -205,7 +198,7 @@ already present in the `FieldList`.
```php ```php
$fields->fieldByName('MajorActions')->push(FormAction::create('grouped', 'New group button')); $fields->fieldByName('MajorActions')->push(FormAction::create('grouped', 'New group button'));
``` ```
Another option is adding actions into the drop-up - best place for placing Another option is adding actions into the drop-up - best place for placing
@ -213,7 +206,7 @@ infrequently used minor actions.
```php ```php
$fields->addFieldToTab('ActionMenus.MoreOptions', FormAction::create('minor', 'Minor action')); $fields->addFieldToTab('ActionMenus.MoreOptions', FormAction::create('minor', 'Minor action'));
``` ```
We can also easily create new drop-up menus by defining new tabs within the We can also easily create new drop-up menus by defining new tabs within the
@ -221,7 +214,7 @@ We can also easily create new drop-up menus by defining new tabs within the
```php ```php
$fields->addFieldToTab('ActionMenus.MyDropUp', FormAction::create('minor', 'Minor action in a new drop-up')); $fields->addFieldToTab('ActionMenus.MyDropUp', FormAction::create('minor', 'Minor action in a new drop-up'));
``` ```
<div class="hint" markdown='1'> <div class="hint" markdown='1'>
@ -245,21 +238,21 @@ applicable controller actions to it:
```php ```php
use SilverStripe\Admin\LeftAndMainExtension; use SilverStripe\Admin\LeftAndMainExtension;
class CustomActionsExtension extends LeftAndMainExtension class CustomActionsExtension extends LeftAndMainExtension
{
private static $allowed_actions = [
'sampleAction'
];
public function sampleAction()
{ {
// Create the web
private static $allowed_actions = [
'sampleAction'
];
public function sampleAction()
{
// Create the web
}
} }
}
``` ```
@ -267,17 +260,16 @@ The extension then needs to be registered:
```yaml ```yaml
SilverStripe\Admin\LeftAndMain:
LeftAndMain: extensions:
extensions: - CustomActionsExtension
- CustomActionsExtension
``` ```
You can now use these handlers with your buttons: You can now use these handlers with your buttons:
```php ```php
$fields->push(FormAction::create('sampleAction', 'Perform Sample Action')); $fields->push(FormAction::create('sampleAction', 'Perform Sample Action'));
``` ```
## Summary ## Summary

View File

@ -5,26 +5,25 @@ also another tool at your disposal: The [Extension](api:SilverStripe\Core\Extens
```php ```php
use SilverStripe\Core\Extension; use SilverStripe\Core\Extension;
class MyAdminExtension extends Extension class MyAdminExtension extends Extension
{
// ...
public function updateEditForm(&$form)
{ {
// ... $form->Fields()->push(/* ... */)
public function updateEditForm(&$form)
{
$form->Fields()->push(/* ... */)
}
} }
}
``` ```
Now enable this extension through your `[config.yml](/topics/configuration)` file. Now enable this extension through your `[config.yml](/topics/configuration)` file.
```yml ```yml
MyAdmin:
MyAdmin: extensions:
extensions: - MyAdminExtension
- MyAdminExtension
``` ```
The following extension points are available: `updateEditForm()`, `updateSearchContext()`, The following extension points are available: `updateEditForm()`, `updateSearchContext()`,

View File

@ -20,31 +20,31 @@ This example uses [Cache](api:Cache) in some custom code, and the same cache is
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Flushable; use SilverStripe\Core\Flushable;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
class MyClass extends DataObject implements Flushable class MyClass extends DataObject implements Flushable
{
public static function flush()
{ {
Injector::inst()->get(CacheInterface::class . '.mycache')->clear();
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;
}
} }
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 ### Using with filesystem
@ -54,18 +54,18 @@ useful in an example like `GD` or `Imagick` generating resampled images, but we
flush so they are re-created on demand. flush so they are re-created on demand.
```php ```php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\Core\Flushable; use SilverStripe\Core\Flushable;
class MyClass extends DataObject implements Flushable class MyClass extends DataObject implements Flushable
{
public static function flush()
{ {
foreach(glob(ASSETS_PATH . '/_tempfiles/*.jpg') as $file) {
public static function flush() unlink($file);
{
foreach(glob(ASSETS_PATH . '/_tempfiles/*.jpg') as $file) {
unlink($file);
}
} }
} }
```
}
```

View File

@ -10,8 +10,11 @@ This can be accessed in user code via Injector
```php ```php
$kernel = Injector::inst()->get(Kernel::class); use SilverStripe\Core\Kernel;
echo "Current environment: " . $kernel->getEnvironment(); use SilverStripe\Core\Injector\Injector;
$kernel = Injector::inst()->get(Kernel::class);
echo "Current environment: " . $kernel->getEnvironment();
``` ```
## Kernel services ## Kernel services
@ -39,16 +42,16 @@ you should call `->activate()` on the kernel instance you would like to unnest t
```php ```php
$oldKernel = Injector::inst()->get(Kernel::class); $oldKernel = Injector::inst()->get(Kernel::class);
try { try {
// Injector::inst() / Config::inst() are automatically updated to the new kernel // Injector::inst() / Config::inst() are automatically updated to the new kernel
$newKernel = $oldKernel->nest(); $newKernel = $oldKernel->nest();
Config::modify()->set(Director::class, 'alternate_base_url', '/myurl'); Config::modify()->set(Director::class, 'alternate_base_url', '/myurl');
} }
finally { finally {
// Any changes to config (or other application state) have now been reverted // Any changes to config (or other application state) have now been reverted
$oldKernel->activate(); $oldKernel->activate();
} }
``` ```
# Application # Application
@ -70,7 +73,6 @@ You can customise it as required.
```php ```php
<?php
use SilverStripe\Control\HTTPApplication; use SilverStripe\Control\HTTPApplication;
use SilverStripe\Control\HTTPRequestBuilder; use SilverStripe\Control\HTTPRequestBuilder;
@ -105,19 +107,19 @@ routing.
```php ```php
$request = CLIRequestBuilder::createFromEnvironment(); $request = CLIRequestBuilder::createFromEnvironment();
$kernel = new TestKernel(BASE_PATH); $kernel = new TestKernel(BASE_PATH);
$app = new HTTPApplication($kernel); $app = new HTTPApplication($kernel);
$app->execute($request, function (HTTPRequest $request) { $app->execute($request, function (HTTPRequest $request) {
// Start session and execute // Start session and execute
$request->getSession()->init(); $request->getSession()->init();
// Set dummy controller // Set dummy controller
$controller = Controller::create(); $controller = Controller::create();
$controller->setRequest($request); $controller->setRequest($request);
$controller->pushCurrent(); $controller->pushCurrent();
$controller->doInit(); $controller->doInit();
}, true); }, true);
``` ```

View File

@ -8,10 +8,10 @@ needs to interface over the command line.
The main entry point for any command line execution is `cli-script.php` in the framework module. The main entry point for any command line execution is `cli-script.php` in the framework module.
For example, to run a database rebuild from the command line, use this command: For example, to run a database rebuild from the command line, use this command:
```bash
cd your-webroot/ ```bash
php vendor/silverstripe/framework/cli-script.php dev/build cd your-webroot/
php vendor/silverstripe/framework/cli-script.php dev/build
``` ```
<div class="notice"> <div class="notice">
@ -23,7 +23,7 @@ to have.
## Sake - SilverStripe Make ## Sake - SilverStripe Make
Sake is a simple wrapper around `cli-script.php`. It also tries to detect which `php` executable to use if more than one Sake is a simple wrapper around `cli-script.php`. It also tries to detect which `php` executable to use if more than one
are available. are available. It is accessible via `vendor/bin/sake`.
<div class="info" markdown='1'> <div class="info" markdown='1'>
If you are using a Debian server: Check you have the php-cli package installed for sake to work. If you get an error If you are using a Debian server: Check you have the php-cli package installed for sake to work. If you get an error
@ -32,11 +32,13 @@ when running the command php -v, then you may not have php-cli installed so sake
### Installation ### Installation
`sake` can be invoked using `./vendor/silverstripe/framework/sake`. For easier access, copy the `sake` file into `/usr/bin/sake`. `sake` can be invoked using `./vendor/bin/sake`. For easier access, copy the `sake` file into `/usr/bin/sake`.
``` ```
cd your-webroot/ cd your-webroot/
sudo ./vendor/silverstripe/framework/sake installsake sudo ./vendor/bin/sake installsake
``` ```
<div class="warning"> <div class="warning">
This currently only works on UNIX like systems, not on Windows. This currently only works on UNIX like systems, not on Windows.
</div> </div>
@ -55,28 +57,27 @@ SS_BASE_URL="http://localhost/base-url"
### Usage ### Usage
Sake can run any controller by passing the relative URL to that controller. `sake` can run any controller by passing the relative URL to that controller.
```bash ```bash
sake /
# returns the homepage
sake / sake dev/
# returns the homepage # shows a list of development operations
sake dev/
# shows a list of development operations
``` ```
Sake is particularly useful for running build tasks. `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.. It can also be handy if you have a long running script..
```bash
sake dev/tasks/MyReallyLongTask ```bash
sake dev/tasks/MyReallyLongTask
``` ```
### Running processes ### Running processes
@ -86,45 +87,42 @@ It can also be handy if you have a long running script..
Make a task or controller class that runs a loop. To avoid memory leaks, you should make the PHP process exit when it Make a task or controller class that runs a loop. To avoid memory leaks, you should make the PHP process exit when it
hits some reasonable memory limit. Sake will automatically restart your process whenever it exits. hits some reasonable memory limit. Sake will automatically restart your process whenever it exits.
Include some appropriate sleep()s so that your process doesn't hog the system. The best thing to do is to have a short Include some appropriate `sleep()`s so that your process doesn't hog the system. The best thing to do is to have a short
sleep when the process is in the middle of doing things, and a long sleep when doesn't have anything to do. sleep when the process is in the middle of doing things, and a long sleep when doesn't have anything to do.
This code provides a good template: This code provides a good template:
```php ```php
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
class MyProcess extends Controller class MyProcess extends Controller
{ {
private static $allowed_actions = [ private static $allowed_actions = [
'index' 'index'
]; ];
function index() { function index() {
set_time_limit(0); set_time_limit(0);
while(memory_get_usage() < 32*1024*1024) { while(memory_get_usage() < 32*1024*1024) {
if($this->somethingToDo()) { if($this->somethingToDo()) {
$this->doSomething(); $this->doSomething();
sleep(1) sleep(1)
} else { } else {
sleep(300); sleep(300);
}
} }
} }
} }
}
``` ```
Then the process can be managed through `sake` Then the process can be managed through `sake`
```bash ```bash
sake -start MyProcess
sake -start MyProcess sake -stop MyProcess
sake -stop MyProcess
``` ```
<div class="notice"> <div class="notice">
@ -135,19 +133,15 @@ 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. Parameters can be added to the command. All parameters will be available in `$_GET` array on the server.
```bash ```bash
cd your-webroot/
cd your-webroot/ php vendor/silverstripe/framework/cli-script.php myurl myparam=1 myotherparam=2
php vendor/silverstripe/framework/cli-script.php myurl myparam=1 myotherparam=2
``` ```
Or if you're using `sake` Or if you're using `sake`
```bash ```bash
vendor/bin/sake myurl "myparam=1&myotherparam=2"
sake myurl "myparam=1&myotherparam=2"
``` ```
## Running Regular Tasks With Cron ## Running Regular Tasks With Cron
@ -158,5 +152,5 @@ On a UNIX machine, you can typically run a scheduled task with a [cron job](http
The following will run `MyTask` every minute. The following will run `MyTask` every minute.
```bash ```bash
* * * * * /your/site/folder/sake dev/tasks/MyTask * * * * * /your/site/folder/vendor/bin/sake dev/tasks/MyTask
``` ```

View File

@ -15,9 +15,11 @@ Sets the value of cookie with configuration.
```php ```php
Cookie::set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = false); use SilverStripe\Control\Cookie;
// Cookie::set('MyApplicationPreference', 'Yes'); Cookie::set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = false);
// Cookie::set('MyApplicationPreference', 'Yes');
``` ```
### get ### get
@ -26,10 +28,10 @@ Returns the value of cookie.
```php ```php
Cookie::get($name); Cookie::get($name);
// Cookie::get('MyApplicationPreference'); // Cookie::get('MyApplicationPreference');
// returns 'Yes' // returns 'Yes'
``` ```
### force_expiry ### force_expiry
@ -38,9 +40,9 @@ Clears a given cookie.
```php ```php
Cookie::force_expiry($name, $path = null, $domain = null); Cookie::force_expiry($name, $path = null, $domain = null);
// Cookie::force_expiry('MyApplicationPreference') // Cookie::force_expiry('MyApplicationPreference')
``` ```
## Cookie_Backend ## Cookie_Backend
@ -54,15 +56,19 @@ from the browser.
```php ```php
$myCookies = [ use SilverStripe\Core\Injector\Injector;
'cookie1' => 'value1', use SilverStripe\Control\Cookie;
]; use SilverStripe\Control\CookieJar;
$newBackend = new CookieJar($myCookies); $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');
``` ```
@ -74,9 +80,9 @@ create a new service for you using the `$_COOKIE` superglobal.
```php ```php
Injector::inst()->unregisterNamedObject('Cookie_Backend'); Injector::inst()->unregisterNamedObject('Cookie_Backend');
Cookie::get('cookiename'); // will return $_COOKIE['cookiename'] if set 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 Alternatively, if you know that the superglobal has been changed (or you aren't sure it hasn't) you can attempt to use
@ -84,11 +90,11 @@ the current `CookieJar` service to tell you what it was like when it was registe
```php ```php
//store the cookies that were loaded into the `CookieJar` //store the cookies that were loaded into the `CookieJar`
$recievedCookie = Cookie::get_inst()->getAll(false); $recievedCookie = Cookie::get_inst()->getAll(false);
//set a new `CookieJar` //set a new `CookieJar`
Injector::inst()->registerService(new CookieJar($recievedCookie), 'CookieJar'); Injector::inst()->registerService(new CookieJar($recievedCookie), 'CookieJar');
``` ```
### Using your own Cookie_Backend ### Using your own Cookie_Backend
@ -97,14 +103,13 @@ If you need to implement your own Cookie_Backend you can use the injector system
```yml ```yml
---
--- Name: mycookie
Name: mycookie After: '#cookie'
After: '#cookie' ---
--- SilverStripe\Core\Injector\Injector:
Injector: Cookie_Backend:
Cookie_Backend: class: MyCookieJar
class: MyCookieJar
``` ```
To be a valid backend your class must implement the [Cookie_Backend](api:SilverStripe\Control\Cookie_Backend) interface. To be a valid backend your class must implement the [Cookie_Backend](api:SilverStripe\Control\Cookie_Backend) interface.
@ -120,12 +125,12 @@ Using the `Cookie_Backend` we can do this like such:
```php ```php
Cookie::set('CookieName', 'CookieVal'); Cookie::set('CookieName', 'CookieVal');
Cookie::get('CookieName'); //gets the cookie as we set it Cookie::get('CookieName'); //gets the cookie as we set it
//will return the cookie as it was when it was sent in the request //will return the cookie as it was when it was sent in the request
Cookie::get('CookieName', false); Cookie::get('CookieName', false);
``` ```
### Accessing all the cookies at once ### Accessing all the cookies at once
@ -134,9 +139,9 @@ One can also access all of the cookies in one go using the `Cookie_Backend`
```php ```php
Cookie::get_inst()->getAll(); //returns all the cookies including ones set during the current process 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 Cookie::get_inst()->getAll(false); //returns all the cookies in the request
``` ```
## API Documentation ## API Documentation

View File

@ -29,6 +29,8 @@ class MyController extends Controller
Otherwise, if you're not in a controller, get the request as a service. Otherwise, if you're not in a controller, get the request as a service.
```php ```php
use SilverStripe\Control\HTTPRequest;
$request = Injector::inst()->get(HTTPRequest::class); $request = Injector::inst()->get(HTTPRequest::class);
$session = $request->getSession(); $session = $request->getSession();
``` ```
@ -37,7 +39,7 @@ $session = $request->getSession();
```php ```php
$session->set('MyValue', 6); $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 Saves the value of to session data. You can also save arrays or serialized objects in session (but note there may be
@ -45,12 +47,12 @@ size restrictions as to how much you can save).
```php ```php
// saves an array // saves an array
$session->set('MyArrayOfValues', ['1','2','3']); $session->set('MyArrayOfValues', ['1','2','3']);
// saves an object (you'll have to unserialize it back) // saves an object (you'll have to unserialize it back)
$object = new Object(); $object = new Object();
$session->set('MyObject', serialize($object)); $session->set('MyObject', serialize($object));
``` ```
@ -62,14 +64,14 @@ can use this anywhere in your PHP files.
```php ```php
echo $session->get('MyValue'); echo $session->get('MyValue');
// returns 6 // returns 6
$data = $session->get('MyArrayOfValues'); $data = $session->get('MyArrayOfValues');
// $data = array(1,2,3) // $data = array(1,2,3)
$object = unserialize($session->get('MyObject', $object)); $object = unserialize($session->get('MyObject', $object));
// $object = Object() // $object = Object()
``` ```
@ -77,9 +79,8 @@ can use this anywhere in your PHP files.
You can also get all the values in the session at once. This is useful for debugging. You can also get all the values in the session at once. This is useful for debugging.
```php ```php
$session->getAll(); $session->getAll();
// returns an array of all the session values. // returns an array of all the session values.
``` ```
## clear ## clear
@ -87,13 +88,13 @@ You can also get all the values in the session at once. This is useful for debug
Once you have accessed a value from the Session it doesn't automatically wipe the value from the Session, you have 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. to specifically remove it.
```php ```php
$session->clear('MyValue'); $session->clear('MyValue');
``` ```
Or you can clear every single value in the session at once. Note SilverStripe stores some of its own session data 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. including form and page comment information. None of this is vital but `clear_all` will clear everything.
```php ```php
$session->clearAll(); $session->clearAll();
``` ```
## Secure Session Cookie ## Secure Session Cookie
@ -102,7 +103,6 @@ In certain circumstances, you may want to use a different `session_name` cookie
```yml ```yml
SilverStripe\Control\Session: SilverStripe\Control\Session:
cookie_secure: true cookie_secure: true
``` ```

View File

@ -115,17 +115,17 @@ specifies "these are arguments to the controller".
In other words, change this: In other words, change this:
```php ```php
Director::addRules(50, array( Director::addRules(50, array(
'admin/ImageEditor/$Action' => 'ImageEditor', 'admin/ImageEditor/$Action' => 'ImageEditor',
)); ));
``` ```
To this: To this:
```php ```php
Director::addRules(50, array( Director::addRules(50, array(
'admin/ImageEditor//$Action' => 'ImageEditor', 'admin/ImageEditor//$Action' => 'ImageEditor',
)); ));
``` ```
@ -214,7 +214,7 @@ like Validator.js and behaviour.js. If you want to disable JavaScript validation
_config.php: _config.php:
```php ```php
Validator::set_javascript_validation_handler('none'); Validator::set_javascript_validation_handler('none');
``` ```
See http://open.silverstripe.com/changeset/69688 See http://open.silverstripe.com/changeset/69688

View File

@ -74,31 +74,31 @@ Because of this, you will need to change the static definitions in your ModelAdm
change this: change this:
```php ```php
class MyCatalogAdmin extends ModelAdmin class MyCatalogAdmin extends ModelAdmin
{ {
protected static $managed_models = array( protected static $managed_models = array(
'Product', 'Product',
'Category' 'Category'
); );
... ...
} }
``` ```
To this: To this:
```php ```php
class MyCatalogAdmin extends ModelAdmin class MyCatalogAdmin extends ModelAdmin
{ {
public static $managed_models = array( public static $managed_models = array(
'Product', 'Product',
'Category' 'Category'
); );
... ...
} }
``` ```

View File

@ -130,7 +130,7 @@ You can enable it manually for existing websites. Existing URLs will automatical
republication (your old URLs should redirect automatically). republication (your old URLs should redirect automatically).
```php ```php
SiteTree::enable_nested_urls(); SiteTree::enable_nested_urls();
``` ```
### SiteTree->Link() instead of SiteTree->URLSegment ### SiteTree->Link() instead of SiteTree->URLSegment

View File

@ -124,7 +124,7 @@ To clarify: Leaving existing decorators unchanged might mean that you allow acti
// In mysite/_config.php // In mysite/_config.php
:::php :::php
Object::add_extension('SiteTree', 'MyDecorator'); Object::add_extension('SiteTree', 'MyDecorator');
// 2.4.0 // 2.4.0
:::php :::php
class MyDecorator extends DataObjectDecorator class MyDecorator extends DataObjectDecorator
@ -137,7 +137,7 @@ To clarify: Leaving existing decorators unchanged might mean that you allow acti
} }
} }
} }
// 2.4.1 // 2.4.1
:::php :::php
class MyDecorator extends DataObjectDecorator class MyDecorator extends DataObjectDecorator

View File

@ -50,7 +50,7 @@ parameters (//$data, $form// instead of *$request*).
// Form field actions // Form field actions
class MyFormField extends FormField class MyFormField extends FormField
{ {
// Form fields always have a reference to their form. // Form fields always have a reference to their form.
// Use the form-specific token instance. // Use the form-specific token instance.
function delete($request) { function delete($request) {
@ -64,7 +64,7 @@ parameters (//$data, $form// instead of *$request*).
// Controller actions (GET and POST) without form // Controller actions (GET and POST) without form
class MyController extends Controller class MyController extends Controller
{ {
// Manually adds token to link // Manually adds token to link
function DeleteLink() { function DeleteLink() {
$token = SecurityToken::inst(); $token = SecurityToken::inst();
@ -73,7 +73,7 @@ parameters (//$data, $form// instead of *$request*).
$link = $token->addToUrl($link); $link = $token->addToUrl($link);
return $link; return $link;
} }
// Controller actions pass through the request object, // Controller actions pass through the request object,
// not called through a form. // not called through a form.
// Use a global token instance. // Use a global token instance.
@ -84,7 +84,7 @@ parameters (//$data, $form// instead of *$request*).
// valid controller delete action // valid controller delete action
} }
} }
// Controller actions (GET and POST) with form // Controller actions (GET and POST) with form
class MyController extends Controller class MyController extends Controller
{ {
@ -155,7 +155,7 @@ parameters don't break through string concatenation.
:::php :::php
// bad // bad
$link = $this->Link() . 'export?csv=1'; $link = $this->Link() . 'export?csv=1';
// good // good
$link = Controller::join_links($this->Link(), 'export', '?csv=1'); $link = Controller::join_links($this->Link(), 'export', '?csv=1');
@ -165,15 +165,15 @@ Full controller example:
:::php :::php
class MyController extends Controller class MyController extends Controller
{ {
function export($request) { function export($request) {
// ... // ...
} }
function Link($action = null) { function Link($action = null) {
return Controller::join_links('MyController', $action); return Controller::join_links('MyController', $action);
} }
function ExportLink() { function ExportLink() {
return Controller::join_links($this->Link('export'), '?csv=1'); return Controller::join_links($this->Link('export'), '?csv=1');
} }
@ -195,13 +195,13 @@ You can manually enable security tokens, either globally or for a specific form.
:::php :::php
class MyTest extends SapphireTest class MyTest extends SapphireTest
{ {
// option 1: enable for all forms created through this test // option 1: enable for all forms created through this test
function setUp() { function setUp() {
parent::setUp(); parent::setUp();
SecurityToken::enable(); SecurityToken::enable();
} }
// option 2: enable for one specific form // option 2: enable for one specific form
function testMyForm() { function testMyForm() {
$form = new MyForm(); $form = new MyForm();
@ -352,4 +352,4 @@ You can manually enable security tokens, either globally or for a specific form.
* [rev:111040] API-CHANGE: remove include which is not required. * [rev:111040] API-CHANGE: remove include which is not required.
* [rev:111038] ENHACENEMENT: Change behaviour of the !MenufestBuilder to use spl_autoload_register instead of traditional __autoload. * [rev:111038] ENHACENEMENT: Change behaviour of the !MenufestBuilder to use spl_autoload_register instead of traditional __autoload.
<code>sscreatechangelog --version 2.4.3 --branch branches/2.4 --stopbranch tags/2.4.2</code> <code>sscreatechangelog --version 2.4.3 --branch branches/2.4 --stopbranch tags/2.4.2</code>

View File

@ -145,7 +145,7 @@ you can use `add_to_class()` as a replacement to `extraStatics()`.
:::php :::php
class MyExtension extends Extension class MyExtension extends Extension
{ {
// before // before
function extraStatics($class, $extensionClass) { function extraStatics($class, $extensionClass) {
if($class == 'MyClass') { if($class == 'MyClass') {
@ -171,7 +171,7 @@ you can use `add_to_class()` as a replacement to `extraStatics()`.
)); ));
} }
parent::add_to_class($class, $extensionClass, $args); parent::add_to_class($class, $extensionClass, $args);
} }
} }
@ -185,7 +185,7 @@ expressive notation (instead of unnamed arguments).
DataObject::get('Member', '"FirstName" = \'Sam'\', '"Surname" ASC"); DataObject::get('Member', '"FirstName" = \'Sam'\', '"Surname" ASC");
// after // after
Member::get()->filter(array('FirstName' => 'Sam'))->sort('Surname'); Member::get()->filter(array('FirstName' => 'Sam'))->sort('Surname');
The underlying record retrieval and management is rewritten from scratch, and features The underlying record retrieval and management is rewritten from scratch, and features
lazy loading which fetches only the records it needs, as late as possible. lazy loading which fetches only the records it needs, as late as possible.
In order to retrieve all ORM records manually (as the previous ORM would've done), In order to retrieve all ORM records manually (as the previous ORM would've done),
@ -226,7 +226,7 @@ this command would have been intolerably slow:
:::php :::php
SiteTree::get()->count(); SiteTree::get()->count();
The 3.0 ORM is more intelligent gives you tools you need to create high-performance code without The 3.0 ORM is more intelligent gives you tools you need to create high-performance code without
bypassing the ORM: bypassing the ORM:
@ -478,7 +478,7 @@ as well as the HTML form element itself.
<div class="field checkbox extraClass"...> <div class="field checkbox extraClass"...>
<input type="checkbox".../> <input type="checkbox".../>
</div> </div>
After (abbreviated): After (abbreviated):
<div class="field checkbox extraClass"...> <div class="field checkbox extraClass"...>
<input type="checkbox" class="checkbox extraClass".../> <input type="checkbox" class="checkbox extraClass".../>
@ -670,7 +670,7 @@ will only produce errors if the API was deprecated in the release equal to or ea
in 3.0. in 3.0.
Deprecation::notification_version('3.0.0'); Deprecation::notification_version('3.0.0');
If you change the notification version to 3.0.0-dev, then only methods deprecated in older versions If you change the notification version to 3.0.0-dev, then only methods deprecated in older versions
(e.g. 2.4) will trigger notices, and the other methods will silently pass. This can be useful if (e.g. 2.4) will trigger notices, and the other methods will silently pass. This can be useful if
you don't yet have time to remove all calls to deprecated methods. you don't yet have time to remove all calls to deprecated methods.

View File

@ -161,21 +161,21 @@ Here's an example on how to rewrite a common `_config.php` configuration:
<?php <?php
global $project; global $project;
$project = 'mysite'; $project = 'mysite';
global $database; global $database;
$database = 'SS_mydb'; $database = 'SS_mydb';
require_once('conf/ConfigureFromEnv.php'); require_once('conf/ConfigureFromEnv.php');
SSViewer::set_theme('simple'); SSViewer::set_theme('simple');
if(class_exists('SiteTree')) SiteTree::enable_nested_urls(); if(class_exists('SiteTree')) SiteTree::enable_nested_urls();
if(Director::isLive()) Email::setAdminEmail('support@mydomain.com'); if(Director::isLive()) Email::setAdminEmail('support@mydomain.com');
if(is_defined('MY_REDIRECT_EMAILS')) Email::send_all_emails_to('developer@mydomain.com'); if(is_defined('MY_REDIRECT_EMAILS')) Email::send_all_emails_to('developer@mydomain.com');
SS_Log::add_writer(new SS_LogFileWriter(BASE_PATH . '/mylog.log'), SS_Log::WARN); SS_Log::add_writer(new SS_LogFileWriter(BASE_PATH . '/mylog.log'), SS_Log::WARN);
if(strpos('Internet Explorer', $_SERVER['HTTP_USER_AGENT']) !== false) { if(strpos('Internet Explorer', $_SERVER['HTTP_USER_AGENT']) !== false) {
SSViewer::set_theme('basic'); SSViewer::set_theme('basic');
} }
@ -188,16 +188,16 @@ The upgraded `_config.php`:
<?php <?php
global $project; global $project;
$project = 'mysite'; $project = 'mysite';
global $database; global $database;
$database = 'SS_mydb'; $database = 'SS_mydb';
require_once('conf/ConfigureFromEnv.php'); require_once('conf/ConfigureFromEnv.php');
// Removed SiteTree::enable_nested_urls() since its configured by default // Removed SiteTree::enable_nested_urls() since its configured by default
// Requires PHP objects, keep in PHP config // Requires PHP objects, keep in PHP config
SS_Log::add_writer(new SS_LogFileWriter(BASE_PATH . '/mylog.log'), SS_Log::WARN); SS_Log::add_writer(new SS_LogFileWriter(BASE_PATH . '/mylog.log'), SS_Log::WARN);
// Non-trivial conditional, keep in PHP config // Non-trivial conditional, keep in PHP config
if(strpos('Internet Explorer', $_SERVER['HTTP_USER_AGENT']) !== false) { if(strpos('Internet Explorer', $_SERVER['HTTP_USER_AGENT']) !== false) {
// Overwrites any earlier YAML config // Overwrites any earlier YAML config

View File

@ -38,7 +38,7 @@ E.g.
public function canCreate($member) public function canCreate($member)
{ {
if(static::get()->count()) return false; if(static::get()->count()) return false;
$context = func_num_args() > 1 ? func_get_arg(1) : array(); $context = func_num_args() > 1 ? func_get_arg(1) : array();
return parent::canCreate($member, $context); return parent::canCreate($member, $context);
} }

View File

@ -6,7 +6,7 @@ This version introduces many breaking changes, which in most projects can be man
of automatic upgrade processes as well as manual code review. This document reviews these changes and will of automatic upgrade processes as well as manual code review. This document reviews these changes and will
guide developers in preparing existing 3.x code for compatibility with 4.0 guide developers in preparing existing 3.x code for compatibility with 4.0
## <a name="overview"></a>Overview ## Overview {#overview}
* Minimum version dependencies have increased; PHP 5.5 and Internet Explorer 11 (or other modern browser) * Minimum version dependencies have increased; PHP 5.5 and Internet Explorer 11 (or other modern browser)
is required. is required.
@ -17,7 +17,7 @@ guide developers in preparing existing 3.x code for compatibility with 4.0
arrangement of templates, as well as other references to classes via string literals or configuration. arrangement of templates, as well as other references to classes via string literals or configuration.
Automatic upgrading tools have been developed to cope with the bulk of these changes (see Automatic upgrading tools have been developed to cope with the bulk of these changes (see
[upgrading notes](#upgrading)). [upgrading notes](#upgrading)).
* Object class has been replaced with traits ([details](object-replace)). * Object class has been replaced with traits ([details](#object-replace)).
* Asset storage has been abstracted, and a new concept of `DBFile` references via database column references * Asset storage has been abstracted, and a new concept of `DBFile` references via database column references
now exists in addition to references via the existing `File` dataobject. File security and protected files now exists in addition to references via the existing `File` dataobject. File security and protected files
are now a core feature ([details](#asset-storage)) are now a core feature ([details](#asset-storage))
@ -52,13 +52,13 @@ guide developers in preparing existing 3.x code for compatibility with 4.0
* Core modules are installed in the `vendor/` folder by default (other modules can opt-in, see [guide](/developer_guides/extending/how_tos/publish_a_module)) * Core modules are installed in the `vendor/` folder by default (other modules can opt-in, see [guide](/developer_guides/extending/how_tos/publish_a_module))
* Renamed constant for temp folder from `TEMP_FOLDER` to `TEMP_PATH` for naming consistency with other path variables and constants * Renamed constant for temp folder from `TEMP_FOLDER` to `TEMP_PATH` for naming consistency with other path variables and constants
## <a name="upgrading"></a>Upgrading Guide ## Upgrading Guide {#upgrading}
The below sections describe how to go about updating an existing site to be prepared for upgrade to 4.0. The below sections describe how to go about updating an existing site to be prepared for upgrade to 4.0.
Most of these upgrading tasks will involve manual code review, although in some cases there are Most of these upgrading tasks will involve manual code review, although in some cases there are
some automated processes that users can run. some automated processes that users can run.
### <a name="deps"></a>Composer dependency update ### Composer dependency update {#deps}
As a first step, you need to update your composer dependencies. As a first step, you need to update your composer dependencies.
The easiest way is to start with a new `composer.json` file The easiest way is to start with a new `composer.json` file
@ -126,7 +126,7 @@ you should raise an issue on the repository asking for 4.0 compatibility.
For now, you should attempt to continue the upgrade without the module For now, you should attempt to continue the upgrade without the module
and temporarily disable its functionality. and temporarily disable its functionality.
### <a name="upgrader-tool"></a>Install the upgrader tool ### Install the upgrader tool {#upgrader-tool}
A lot of upgrade work can be automated, and we've written an A lot of upgrade work can be automated, and we've written an
[upgrader tool](https://github.com/silverstripe/silverstripe-upgrader/) for this purpose. [upgrader tool](https://github.com/silverstripe/silverstripe-upgrader/) for this purpose.
@ -136,7 +136,7 @@ Install it via composer:
composer global require silverstripe/upgrader composer global require silverstripe/upgrader
``` ```
### <a name="index-php-rewrites"></a>index.php and .htaccess rewrites ### index.php and .htaccess rewrites {#index-php-rewrites}
The location of SilverStripe's "entry file" has changed. Your project and server environment will need The location of SilverStripe's "entry file" has changed. Your project and server environment will need
to adjust the path to this file from `framework/main.php` to `index.php`. to adjust the path to this file from `framework/main.php` to `index.php`.
@ -167,7 +167,7 @@ for a clean installation. If you have applied customisations to your `.htaccess`
file (e.g. a custom `main.php`, HTTP header configuration, deny file access), file (e.g. a custom `main.php`, HTTP header configuration, deny file access),
you'll need to manually reapply these to the copied default file. you'll need to manually reapply these to the copied default file.
### <a name="namespaced-classes"></a>Renamed and namespaced classes ### Renamed and namespaced classes {#namespaced-classes}
Nearly all core PHP classes have been namespaced. For example, `DataObject` is now called `SilverStripe\ORM\DataObject`. Nearly all core PHP classes have been namespaced. For example, `DataObject` is now called `SilverStripe\ORM\DataObject`.
The below tasks describe how to upgrade an existing site to remain compatible with the newly upgraded classes. The below tasks describe how to upgrade an existing site to remain compatible with the newly upgraded classes.
@ -189,7 +189,7 @@ For a full list of renamed classes, check the `.upgrade.yml` definitions in each
The rename won't affect class-based permission codes or database table names. The rename won't affect class-based permission codes or database table names.
### <a name="env"></a>`_ss_environment.php` changed to`.env` ### `_ss_environment.php` changed to`.env` {#env}
The php configuration `_ss_environment.php` file has been replaced in favour of a non-executable The php configuration `_ss_environment.php` file has been replaced in favour of a non-executable
`.env` file, which follows a syntax similar to an `.ini` file for key/value pair assignment. Like `.env` file, which follows a syntax similar to an `.ini` file for key/value pair assignment. Like
@ -215,10 +215,8 @@ define('SS_DATABASE_PASSWORD', '');
define('SS_DATABASE_SERVER', '127.0.0.1'); define('SS_DATABASE_SERVER', '127.0.0.1');
``` ```
`.env`: `.env`:
``` ```
## Environment ## Environment
SS_ENVIRONMENT_TYPE="dev" SS_ENVIRONMENT_TYPE="dev"
@ -234,7 +232,6 @@ SS_DATABASE_PASSWORD=""
SS_DATABASE_SERVER="127.0.0.1" SS_DATABASE_SERVER="127.0.0.1"
``` ```
The removal of the `_ss_environment.php` file means that conditional logic is no longer available in the environment The removal of the `_ss_environment.php` file means that conditional logic is no longer available in the environment
variable set-up process. This generally encouraged bad practice and should be avoided. If you still require conditional variable set-up process. This generally encouraged bad practice and should be avoided. If you still require conditional
logic early in the bootstrap, this is best placed in the `_config.php` files. logic early in the bootstrap, this is best placed in the `_config.php` files.
@ -253,9 +250,9 @@ which is no longer necessary.
To access environment variables you can use the `SilverStripe\Core\Environment::getEnv()` method. To access environment variables you can use the `SilverStripe\Core\Environment::getEnv()` method.
See [Environment Management docs](/getting-started/environment_management/) for full details. See [Environment Management docs](/getting_started/environment_management/) for full details.
### <a name="migrate-file"></a>Migrate File DataObject ### Migrate File DataObject {#migrate-file}
Since the structure of `File` dataobjects has changed, a new task `MigrateFileTask` Since the structure of `File` dataobjects has changed, a new task `MigrateFileTask`
has been added to assist in migration of legacy files (see [file migration documentation](/developer_guides/files/file_migration)). has been added to assist in migration of legacy files (see [file migration documentation](/developer_guides/files/file_migration)).
@ -274,7 +271,7 @@ SilverStripe\Assets\FileMigrationHelper:
delete_invalid_files: false delete_invalid_files: false
``` ```
### <a name="inspect-hints"></a>Get upgrade tips on your code ### Get upgrade tips on your code {#inspect-hints}
While there's some code we can automatically rewrite, other uses of changed SilverStripe APIs aren't that obvious. While there's some code we can automatically rewrite, other uses of changed SilverStripe APIs aren't that obvious.
You can use our heuristics to get some hints on where you need to review code manually. You can use our heuristics to get some hints on where you need to review code manually.
@ -288,7 +285,7 @@ This task should be run *after* `upgrade-code upgrade`.
These hints only cover a part of the upgrade work, These hints only cover a part of the upgrade work,
but can serve as a good indicator for where to start. but can serve as a good indicator for where to start.
### <a name="literal-table-names"></a>Rewrite literal table names ### Rewrite literal table names {#literal-table-names}
In 3.x the class name of any DataObject matched the table name, but in 4.x all classes are namespaced, and it is In 3.x the class name of any DataObject matched the table name, but in 4.x all classes are namespaced, and it is
necessary to map between table and class for querying the database. necessary to map between table and class for querying the database.
@ -311,7 +308,7 @@ public function countDuplicates($model, $fieldToCheck)
} }
``` ```
### <a name="literal-class-names"></a>Rewrite literal class names ### Rewrite literal class names {#literal-class-names}
You'll need to update any strings that represent class names and make sure they're fully You'll need to update any strings that represent class names and make sure they're fully
qualified. In particular, relationship definitions such as `has_one` and `has_many` will need qualified. In particular, relationship definitions such as `has_one` and `has_many` will need
@ -341,7 +338,7 @@ In the context of YAML, the magic constant `::class` does not apply. Fully quali
property: value property: value
``` ```
### <a name="controllers-own-files"></a>Move controllers to their own files ### Move controllers to their own files {#controllers-own-files}
The convention for naming controllers is now `[MyPageType]Controller`, where it used to be `[MyPageType]_Controller`. This change was made to be more compatible with the PSR-2 standards. The convention for naming controllers is now `[MyPageType]Controller`, where it used to be `[MyPageType]_Controller`. This change was made to be more compatible with the PSR-2 standards.
@ -351,7 +348,7 @@ other thirdparty code that extend `PageController` are likely to assume that cla
By default, a controller for a page type *must* reside in the same namespace as its page. To use different logic, override `SiteTree::getControllerName()`. By default, a controller for a page type *must* reside in the same namespace as its page. To use different logic, override `SiteTree::getControllerName()`.
### <a name="template-locations"></a>Template locations and references ### Template locations and references {#template-locations}
Templates are now more strict about their locations. Templates are now more strict about their locations.
Case is now also checked on case-sensitive filesystems. Case is now also checked on case-sensitive filesystems.
@ -372,7 +369,7 @@ if the former is not present.
Please refer to our [template syntax](/developer_guides/templates/syntax) for details. Please refer to our [template syntax](/developer_guides/templates/syntax) for details.
### <a name="private-static"></a>Config settings should be set to `private static` ### Config settings should be set to `private static` {#private-static}
Class configuration defined as `static` properties need to be marked as `private` to take effect: Class configuration defined as `static` properties need to be marked as `private` to take effect:
@ -383,7 +380,7 @@ Class configuration defined as `static` properties need to be marked as `private
]; ];
``` ```
### <a name="module-paths"></a>Module paths can't be hardcoded ### Module paths can't be hardcoded {#module-paths}
You should no longer rely on modules being placed in a deterministic folder (e.g. `/framework`), You should no longer rely on modules being placed in a deterministic folder (e.g. `/framework`),
and use getters on the [Module](api:SilverStripe\Core\Manifest\Module) object instead. and use getters on the [Module](api:SilverStripe\Core\Manifest\Module) object instead.
@ -453,7 +450,7 @@ To ensure consistency, we've also deprecated support for path constants:
* Deprecated `THEMES_DIR` and `THEMES_PATH` * Deprecated `THEMES_DIR` and `THEMES_PATH`
* Deprecated `MODULES_PATH` and `MODULES_DIR` * Deprecated `MODULES_PATH` and `MODULES_DIR`
### <a name="vendor-folder"></a>Adapt tooling to modules in vendor folder ### Adapt tooling to modules in vendor folder {#vendor-folder}
SilverStripe modules can now be installed like any other composer package: In the `vendor/` folder SilverStripe modules can now be installed like any other composer package: In the `vendor/` folder
instead of the webroot. Modules need to opt in to this behaviour after they've ensured instead of the webroot. Modules need to opt in to this behaviour after they've ensured
@ -479,7 +476,7 @@ and this environment supports symlinks, you don't need to change anything.
If you deploy release archives, either ensure those archives can correctly extract symlinks, If you deploy release archives, either ensure those archives can correctly extract symlinks,
or explicitly switch to the "copy" mode to avoid symlinks. or explicitly switch to the "copy" mode to avoid symlinks.
### <a name="psr3-logging"></a>SS_Log replaced with PSR-3 logging ### SS_Log replaced with PSR-3 logging {#psr3-logging}
One of the great changes that comes with SilverStripe 4 is the introduction of One of the great changes that comes with SilverStripe 4 is the introduction of
[PSR-3](http://www.php-fig.org/psr/psr-3/) compatible logger interfaces. This [PSR-3](http://www.php-fig.org/psr/psr-3/) compatible logger interfaces. This
@ -532,7 +529,7 @@ SilverStripe\Core\Injector\Injector:
`WebDesignGroup\ShopSite\Logging\ErrorPageFormatter` should be a class that `WebDesignGroup\ShopSite\Logging\ErrorPageFormatter` should be a class that
implements the `Monolog\Formatter\FormatterInterface` interface. implements the `Monolog\Formatter\FormatterInterface` interface.
### <a name="config-php"></a>Upgrade `mysite/_config.php` ### Upgrade `mysite/_config.php` {#config-php}
The globals `$database` and `$databaseConfig` are deprecated. You should upgrade your The globals `$database` and `$databaseConfig` are deprecated. You should upgrade your
site `_config.php` files to use the [.env configuration](#env) site `_config.php` files to use the [.env configuration](#env)
@ -561,7 +558,7 @@ SilverStripe\Core\Manifest\ModuleManifest:
project: mysite project: mysite
``` ```
### <a name="object-replace"></a>Object class replaced by traits ### Object class replaced by traits {#object-replace}
Object has been superseded by traits. Object has been superseded by traits.
@ -630,7 +627,7 @@ Upgrade extension use
+$extensions = DataObject::get_extensions(File::class); // alternate +$extensions = DataObject::get_extensions(File::class); // alternate
``` ```
### <a name="session"></a>Session object removes static methods ### Session object removes static methods {#session}
Session object is no longer statically accessible via `Session::inst()`. Instead, Session Session object is no longer statically accessible via `Session::inst()`. Instead, Session
is a member of the current request. is a member of the current request.
@ -648,7 +645,7 @@ In some places it may still be necessary to access the session object where no r
In rare cases it is still possible to access the request of the current controller via In rare cases it is still possible to access the request of the current controller via
`Controller::curr()->getRequest()` to gain access to the current session. `Controller::curr()->getRequest()` to gain access to the current session.
### <a name="extensions-singletons"></a>Extensions are now singletons ### Extensions are now singletons {#extensions-singletons}
Extensions are now all singletons, meaning that state stored as protected vars Extensions are now all singletons, meaning that state stored as protected vars
within extensions are now shared across all object instances that use this extension. within extensions are now shared across all object instances that use this extension.
@ -707,7 +704,7 @@ class MyClass extends DataObject {
} }
``` ```
### <a name="static-asset-paths"></a>Static references to asset paths ### Static references to asset paths {#static-asset-paths}
All static files (images, javascript, stylesheets, fonts) used for the CMS and forms interfaces All static files (images, javascript, stylesheets, fonts) used for the CMS and forms interfaces
in `framework` and `cms` have moved locations. These assets are now placed in a `client/` subfolder, in `framework` and `cms` have moved locations. These assets are now placed in a `client/` subfolder,
@ -781,7 +778,7 @@ framework/thirdparty/jquery/jquery.js => framework/admin/thirdparty/jquery/jquer
If you have customised the CMS UI (via JavaScript or CSS), please read our guide to If you have customised the CMS UI (via JavaScript or CSS), please read our guide to
[customise the admin interface](/developer_guides/customising_the_admin_interface/). [customise the admin interface](/developer_guides/customising_the_admin_interface/).
### <a name="template-casting"></a>Explicit text casting on template variables ### Explicit text casting on template variables {#template-casting}
Now whenever a `$Variable` is used in a template, regardless of whether any casts or methods are Now whenever a `$Variable` is used in a template, regardless of whether any casts or methods are
suffixed to the reference, it will be cast to either an explicit DBField for that field, or suffixed to the reference, it will be cast to either an explicit DBField for that field, or
@ -816,7 +813,7 @@ class MyObject extends ViewableData
If you need to encode a field (such as `HTMLText`) for use in HTML attributes, use `.ATT` If you need to encode a field (such as `HTMLText`) for use in HTML attributes, use `.ATT`
instead, or if used in an actual XML file use `.CDATA` (see [template casting](/developer_guides/templates/casting)). instead, or if used in an actual XML file use `.CDATA` (see [template casting](/developer_guides/templates/casting)).
### <a name="uploadfield"></a>Replace UploadField with injected service ### Replace UploadField with injected service {#uploadfield}
This field has been superceded by a new class provided by the This field has been superceded by a new class provided by the
[asset-admin](https://github.com/silverstripe/silverstripe-asset-admin) module, which provides a more [asset-admin](https://github.com/silverstripe/silverstripe-asset-admin) module, which provides a more
@ -844,7 +841,7 @@ class MyClass extends DataObject
} }
``` ```
### <a name="i18n"></a>i18n placeholders, plurals and i18nEntityProvider ### i18n placeholders, plurals and i18nEntityProvider {#i18n}
In many cases, localisation strings which worked in 3.x will continue to work in 4.0, however certain patterns In many cases, localisation strings which worked in 3.x will continue to work in 4.0, however certain patterns
have been deprecated and will be removed in 5.0. These include: have been deprecated and will be removed in 5.0. These include:
@ -937,7 +934,7 @@ In templates this can also be invoked as below:
<%t MyObject.PLURALS 'An item|{count} items' count=$Count %> <%t MyObject.PLURALS 'An item|{count} items' count=$Count %>
``` ```
### <a name="member-date-time-fields"></a>Removed Member.DateFormat and Member.TimeFormat database settings ### Removed Member.DateFormat and Member.TimeFormat database settings {#member-date-time-fields}
We're using [native HTML5 date and time pickers](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date) We're using [native HTML5 date and time pickers](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date)
in `DateField` and `TimeField` now ([discussion](https://github.com/silverstripe/silverstripe-framework/issues/6626)), in `DateField` and `TimeField` now ([discussion](https://github.com/silverstripe/silverstripe-framework/issues/6626)),
@ -950,7 +947,7 @@ Consequently, we've also removed `MemberDatetimeOptionsetField`.
the [IntlDateFormatter defaults](http://php.net/manual/en/class.intldateformatter.php) for the selected locale. the [IntlDateFormatter defaults](http://php.net/manual/en/class.intldateformatter.php) for the selected locale.
### <a name="asset-storage"></a>New asset storage mechanism ### New asset storage mechanism {#asset-storage}
File system has been abstracted into an abstract interface. By default, the out of the box filesystem File system has been abstracted into an abstract interface. By default, the out of the box filesystem
uses [Flysystem](http://flysystem.thephpleague.com/) with a local storage mechanism (under the assets directory). uses [Flysystem](http://flysystem.thephpleague.com/) with a local storage mechanism (under the assets directory).
@ -975,7 +972,7 @@ Depending on your server configuration, it may also be necessary to adjust your
permissions. Please see the [common installation problems](/getting_started/installation/common_problems) permissions. Please see the [common installation problems](/getting_started/installation/common_problems)
guide for configuration instruction. guide for configuration instruction.
### <a name="image-handling"></a>Image handling ### Image handling {#image-handling}
As all image-specific manipulations has been refactored from `Image` into an `ImageManipulations` trait, which As all image-specific manipulations has been refactored from `Image` into an `ImageManipulations` trait, which
is applied to both `File` and `DBFile`. These both implement a common interface `AssetContainer`, which is applied to both `File` and `DBFile`. These both implement a common interface `AssetContainer`, which
@ -1015,7 +1012,7 @@ class MyObject extends SilverStripe\ORM\DataObject
} }
``` ```
### <a name="write-file-dataobject"></a>Writing to `File` dataobjects or the assets folder ### Writing to `File` dataobjects or the assets folder {#write-file-dataobject}
In the past all that was necessary to write a `File` DataObject to the database was to ensure a physical file In the past all that was necessary to write a `File` DataObject to the database was to ensure a physical file
existed in the assets folder, and that the Filename of the DataObject was set to the same location. existed in the assets folder, and that the Filename of the DataObject was set to the same location.
@ -1062,7 +1059,7 @@ You can disable File versioning by adding the following to your `_config.php`
SilverStripe\Assets\File::remove_extension('Versioned'); SilverStripe\Assets\File::remove_extension('Versioned');
``` ```
### <a name="image-manipulations"></a>Custom image manipulations ### Custom image manipulations {#image-manipulations}
As file storage and handling has been refactored into the abstract interface, many other components which were As file storage and handling has been refactored into the abstract interface, many other components which were
once specific to Image.php have now been moved into a shared `ImageManipulation` trait. Manipulations of file content, once specific to Image.php have now been moved into a shared `ImageManipulation` trait. Manipulations of file content,
@ -1130,7 +1127,7 @@ There are a few differences in this new API:
A generic `manipulate` method may be used, although the callback for this method both is given, and should return, A generic `manipulate` method may be used, although the callback for this method both is given, and should return,
an `AssetStore` instance and file tuple (Filename, Hash, and Variant) rather than an Image_Backend. an `AssetStore` instance and file tuple (Filename, Hash, and Variant) rather than an Image_Backend.
### <a name="file-shortcode"></a>File or Image shortcode handler ### File or Image shortcode handler {#file-shortcode}
The `handle_shortcode` methods have been removed from the core File and Image classes The `handle_shortcode` methods have been removed from the core File and Image classes
and moved to separate classes in their own respective namespace. and moved to separate classes in their own respective namespace.
@ -1155,7 +1152,7 @@ class MyShortcodeUser extends Object
} }
``` ```
### <a name="compositedbfield"></a>Composite db fields ### Composite db fields {#compositedbfield}
The `CompositeDBField` interface has been replaced with an abstract class, `DBComposite`. In many cases, custom code The `CompositeDBField` interface has been replaced with an abstract class, `DBComposite`. In many cases, custom code
that handled saving of content into composite fields can be removed, as it is now handled by the base class. that handled saving of content into composite fields can be removed, as it is now handled by the base class.
@ -1181,23 +1178,18 @@ class MyAddressField extends
} }
``` ```
### <a name="dataobject-db-database-fields"></a>`DataObject::database_fields` or `DataObject::db` ### Removed `DataObject::database_fields` or `DataObject::db` {#dataobject-db-database-fields}
These methods have been updated to include base fields (such as ID, ClassName, Created, and LastEdited), as The methods `DataObject::database_fields()`, `DataObject::custom_database_fields()` and `DataObject::db()` have
well as composite DB fields. been removed.
`DataObject::database_fields` does not have a second parameter anymore, and can be called directly on an object Instead, to get all database fields for a dataobject, including base fields (such as ID, ClassName, Created, and LastEdited), use `DataObject::getSchema()->databaseFields($className, $aggregate = true)`.
or class. E.g. `Member::database_fields()`. To omit the base fields, pass a value of `false` as the `$aggregate` parameter, e.g. `DataObject::getSchema()->databaseFields(Member::class, false)`.
If user code requires the list of fields excluding base fields, then use custom_database_fields instead, or
make sure to call `unset($fields['ID']);` if this field should be excluded.
`DataObject:db()` will return all logical fields, including foreign key ids and composite DB Fields, alongside Composite database fields are omitted from the `databaseFields()` method. To get those, use `DataObject::getSchema()->compositeFields($className, $aggregate = true)`.
any child fields of these composites. This method can now take a second parameter $includesTable, which
when set to true (with a field name as the first parameter), will also include the table prefix in
`Table.ClassName(args)` format.
### <a name="sqlquery"></a>Rewrite SQLQuery to more specific classes ### Rewrite SQLQuery to more specific classes {#sqlquery}
Instead of `SQLQuery`, you should now use `SQLSelect`, `SQLUpdate`, `SQLInsert` Instead of `SQLQuery`, you should now use `SQLSelect`, `SQLUpdate`, `SQLInsert`
or `SQLDelete` - check the [3.2.0](3.2.0#sqlquery) upgrading notes for details. or `SQLDelete` - check the [3.2.0](3.2.0#sqlquery) upgrading notes for details.
@ -1215,7 +1207,7 @@ Example:
} }
``` ```
### <a name="buildtask-segment"></a>Upgrade BuildTask classes ### Upgrade BuildTask classes {#buildtask-segment}
Similarly to the `$table_name` configuration property for DataObjects, you should define a `private static $segment` for `BuildTask` Similarly to the `$table_name` configuration property for DataObjects, you should define a `private static $segment` for `BuildTask`
instances to ensure that you can still run your task via `sake dev/tasks/MyTask`. Without defining it, the default instances to ensure that you can still run your task via `sake dev/tasks/MyTask`. Without defining it, the default
@ -1230,7 +1222,7 @@ class MyTask extends BuildTask
} }
``` ```
### <a name="errorpage"></a>Moved ErrorPage into a new module ### Moved ErrorPage into a new module {#errorpage}
ErrorPage has been moved to a separate [silverstripe/errorpage module](http://addons.silverstripe.org/add-ons/silverstripe/errorpage) ErrorPage has been moved to a separate [silverstripe/errorpage module](http://addons.silverstripe.org/add-ons/silverstripe/errorpage)
to allow for alternative approaches to managing error responses. to allow for alternative approaches to managing error responses.
@ -1244,7 +1236,7 @@ By default, SilverStripe will display a plaintext "not found" message when the m
Check the [module upgrading guide](http://addons.silverstripe.org/add-ons/silverstripe/errorpage) Check the [module upgrading guide](http://addons.silverstripe.org/add-ons/silverstripe/errorpage)
for more configuration API changes on the `ErrorPage` class. for more configuration API changes on the `ErrorPage` class.
### <a name="assets-server-config"></a>Server configuration files for assets ### Server configuration files for assets {#assets-server-config}
Server configuration files for `/assets` are no longer static, and are regenerated via a set of Server configuration files for `/assets` are no longer static, and are regenerated via a set of
standard SilverStripe templates on flush. These templates include: standard SilverStripe templates on flush. These templates include:
@ -1265,7 +1257,7 @@ If upgrading from an existing installation, make sure to invoke `?flush=all` at
See our ["File Security" guide](/developer_guides/files/file_security) for more information. See our ["File Security" guide](/developer_guides/files/file_security) for more information.
### <a name="tinymce"></a>TinyMCE v4 ### TinyMCE v4 {#tinymce}
Please see the [tinymce upgrading guide](http://archive.tinymce.com/wiki.php/Tutorial:Migration_guide_from_3.x) Please see the [tinymce upgrading guide](http://archive.tinymce.com/wiki.php/Tutorial:Migration_guide_from_3.x)
to assist with upgrades to customisations to TinyMCE v3. to assist with upgrades to customisations to TinyMCE v3.
@ -1306,7 +1298,7 @@ $editor->setOption('charmap_append', [
]); ]);
``` ```
### <a name="dataobject-versioned"></a>DataObjects with the `Versioned` extension ### DataObjects with the `Versioned` extension {#dataobject-versioned}
In most cases, versioned models with the default versioning parameters will not need to be changed. However, In most cases, versioned models with the default versioning parameters will not need to be changed. However,
there are now additional restrictions on the use of custom stage names. there are now additional restrictions on the use of custom stage names.
@ -1354,7 +1346,7 @@ These methods are deprecated:
* `Versioned::publish` Replaced by `Versioned::copyVersionToStage` * `Versioned::publish` Replaced by `Versioned::copyVersionToStage`
* `Versioned::doPublish` Replaced by `Versioned::publishRecursive` * `Versioned::doPublish` Replaced by `Versioned::publishRecursive`
### <a name="ownership"></a>New Ownership API ### New Ownership API {#ownership}
In order to support the recursive publishing of dataobjects, a new API has been developed to allow In order to support the recursive publishing of dataobjects, a new API has been developed to allow
developers to declare dependencies between objects. This is done to ensure that the published state developers to declare dependencies between objects. This is done to ensure that the published state
@ -1399,14 +1391,14 @@ setting on the child object.
For more information, see the [DataObject ownership](https://docs.silverstripe.org/en/4/developer_guides/model/versioning/#dataobject-ownership) documentation and the [versioning](/developer_guides/model/versioning) documentation For more information, see the [DataObject ownership](https://docs.silverstripe.org/en/4/developer_guides/model/versioning/#dataobject-ownership) documentation and the [versioning](/developer_guides/model/versioning) documentation
### <a name="changeset"></a>ChangeSet batch publishing ### ChangeSet batch publishing {#changeset}
ChangeSet objects have been added, which allow groups of objects to be published in ChangeSet objects have been added, which allow groups of objects to be published in
a single atomic transaction. a single atomic transaction.
This API will utilise the ownership API to ensure that changes to any object include This API will utilise the ownership API to ensure that changes to any object include
all necessary changes to owners or owned entities within the same changeset. all necessary changes to owners or owned entities within the same changeset.
### <a name="image-shortcode"></a>New `[image]` shortcode in `HTMLText` fields ### New `[image]` shortcode in `HTMLText` fields {#image-shortcode}
The new Ownership API relies on relationships between objects. The new Ownership API relies on relationships between objects.
Many of these relationships are already made explicit through `has_one`, `has_many` and `many_many`. Many of these relationships are already made explicit through `has_one`, `has_many` and `many_many`.
@ -1416,7 +1408,7 @@ of the `Image` record rather than its path on the filesystem. The shortcode will
when the field is rendered. Newly inserted images will automatically receive the shortcode and ownership tracking, when the field is rendered. Newly inserted images will automatically receive the shortcode and ownership tracking,
and existing `<img>` will continue to work. and existing `<img>` will continue to work.
### <a name="dbfield-rename"></a>Renamed DBField and subclasses ### Renamed DBField and subclasses {#dbfield-rename}
All `DBField` subclasses are namespaced, have a `DB` prefix, and drop any existing `SS_` prefix. All `DBField` subclasses are namespaced, have a `DB` prefix, and drop any existing `SS_` prefix.
For example, `Text` becomes `SilverStripe\ORM\FieldType\DBText`, For example, `Text` becomes `SilverStripe\ORM\FieldType\DBText`,
@ -1448,17 +1440,17 @@ class MyObject extends DataObject
} }
``` ```
### <a name="restfulservice"></a>Removed RestfulService ### Removed RestfulService {#restfulservice}
The `RestfulService` API was a (poor) attempt at a built-in HTTP client. The `RestfulService` API was a (poor) attempt at a built-in HTTP client.
We've removed it, and recommend using [Guzzle](http://docs.guzzlephp.org/en/latest/) instead. We've removed it, and recommend using [Guzzle](http://docs.guzzlephp.org/en/latest/) instead.
### <a name="oembed"></a>Removed Oembed ### Removed Oembed {#oembed}
Instead of Oembed, the framework now relies on [oscarotero/Embed](https://github.com/oscarotero/Embed) to handle getting the shortcode-data for embedding. Instead of Oembed, the framework now relies on [oscarotero/Embed](https://github.com/oscarotero/Embed) to handle getting the shortcode-data for embedding.
If you have custom embedding-code relying on `Oembed`, please refer to the documentation provided by this package. If you have custom embedding-code relying on `Oembed`, please refer to the documentation provided by this package.
### <a name="admin-url"></a>Configurable Admin URL ### Configurable Admin URL {#admin-url}
The default `admin/` URL to access the CMS interface can now be changed via a custom Director routing rule for The default `admin/` URL to access the CMS interface can now be changed via a custom Director routing rule for
`AdminRootController`. If your website or module has hard coded `admin` URLs in PHP, templates or JavaScript, make sure `AdminRootController`. If your website or module has hard coded `admin` URLs in PHP, templates or JavaScript, make sure
@ -1466,7 +1458,7 @@ to update those with the appropriate function or config call. See
[CMS architecture](/developer_guides/customising_the_admin_interface/cms-architecture#the-admin-url) for language [CMS architecture](/developer_guides/customising_the_admin_interface/cms-architecture#the-admin-url) for language
specific functions. specific functions.
### <a name="custom-authenticators"></a>Custom Authenticators ### Custom Authenticators {#custom-authenticators}
The methods `register()` and `unregister()` on `Authenticator` are deprecated in favour The methods `register()` and `unregister()` on `Authenticator` are deprecated in favour
of the `Config` system. This means that any custom `Authenticator` needs to be registered of the `Config` system. This means that any custom `Authenticator` needs to be registered
@ -1496,7 +1488,7 @@ which are called from the `AuthenticationHandler`.
If there is a valid `Member`, it is set on `Security::setCurrentUser()`, which defaults to `null`. If there is a valid `Member`, it is set on `Security::setCurrentUser()`, which defaults to `null`.
IdentityStores are responsible for logging members in and out (e.g. destroy cookies and sessions, or instantiate them). IdentityStores are responsible for logging members in and out (e.g. destroy cookies and sessions, or instantiate them).
### <a name="config"></a>Config is now immutable ### Config is now immutable {#config}
Performance optimisations have been made to Config which, under certain circumstances, require developer Performance optimisations have been made to Config which, under certain circumstances, require developer
care when modifying or caching config values. The top level config object is now immutable on application care when modifying or caching config values. The top level config object is now immutable on application
@ -1514,7 +1506,7 @@ One removed feature is the `Config::FIRST_SET` option. Either use uninherited co
directly, or use the inherited config lookup. As falsey values now overwrite all parent class values, it is directly, or use the inherited config lookup. As falsey values now overwrite all parent class values, it is
now generally safer to use the default inherited config, where in the past you would need to use `FIRST_SET`. now generally safer to use the default inherited config, where in the past you would need to use `FIRST_SET`.
### <a name="cache"></a>Replace Zend_Cache with symfony/cache ### Replace Zend_Cache with symfony/cache {#cache}
We have replaced the unsupported `Zend_Cache` library with [symfony/cache](https://github.com/symfony/cache). We have replaced the unsupported `Zend_Cache` library with [symfony/cache](https://github.com/symfony/cache).
This also allowed us to remove SilverStripe's `Cache` API and use dependency injection with a standard This also allowed us to remove SilverStripe's `Cache` API and use dependency injection with a standard
@ -1599,7 +1591,7 @@ SilverStripe\Core\Injector\Injector:
client: '%$MemcachedClient client: '%$MemcachedClient
``` ```
### <a name="usercode-style-upgrades"></a>User-code style upgrades ### User-code style upgrades {#usercode-style-upgrades}
Although it is not mandatory to upgrade project code to follow SilverStripe and Although it is not mandatory to upgrade project code to follow SilverStripe and
PSR-2 standard it is highly recommended to ensure that code is consistent. The below sections PSR-2 standard it is highly recommended to ensure that code is consistent. The below sections
@ -1675,7 +1667,7 @@ class GalleryPage extends Page
} }
``` ```
### <a name="class-name-remapping"></a>Class name remapping ### Class name remapping {#class-name-remapping}
If you've namespaced one of your custom page types, you may notice a message in the CMS If you've namespaced one of your custom page types, you may notice a message in the CMS
telling you it's obsolete. This is likely because the `ClassName` telling you it's obsolete. This is likely because the `ClassName`
@ -1699,7 +1691,7 @@ SilverStripe\ORM\DatabaseAdmin:
The next time you run a dev/build the class name for all `GalleryPage` pages will The next time you run a dev/build the class name for all `GalleryPage` pages will
be automatically updated to the new `WebDesignGroup\ShopSite\GalleryPage` be automatically updated to the new `WebDesignGroup\ShopSite\GalleryPage`
### <a name="psr2"></a>PSR-2 Coding Standard compliance ### PSR-2 Coding Standard compliance {#psr2}
You can use the [php codesniffer](https://github.com/squizlabs/PHP_CodeSniffer) tool You can use the [php codesniffer](https://github.com/squizlabs/PHP_CodeSniffer) tool
to not only detect and lint PSR-2 coding errors, but also do some minimal automatic to not only detect and lint PSR-2 coding errors, but also do some minimal automatic
@ -1717,7 +1709,7 @@ code style migration.
Repeat the final step and manually repair suggested changes, as necessary, Repeat the final step and manually repair suggested changes, as necessary,
until you no longer have any linting issues. until you no longer have any linting issues.
### <a name="psr4"></a>PSR-4 autoloading for project code ### PSR-4 autoloading for project code {#psr4}
While not critical to an upgrade, SilverStripe 4.0 has adopted the [PS-4 autoloading](http://www.php-fig.org/psr/psr-4/) While not critical to an upgrade, SilverStripe 4.0 has adopted the [PS-4 autoloading](http://www.php-fig.org/psr/psr-4/)
standard for the core modules, so it's probably a good idea to be consistent. standard for the core modules, so it's probably a good idea to be consistent.
@ -1751,9 +1743,9 @@ Please note that there are changes to template structure which in some cases
require templates to be in a folder location that matches the namespace of the class require templates to be in a folder location that matches the namespace of the class
that it belongs to, e.g. `themes/mytheme/templates/MyVendor/Foobar/Model/MyModel.ss`. that it belongs to, e.g. `themes/mytheme/templates/MyVendor/Foobar/Model/MyModel.ss`.
## <a name="api-changes"></a>API Changes ## API Changes {#api-changes}
### <a name="overview-general"></a>General ### General {#overview-general}
* Minimum PHP version raised to 5.6 (with support for PHP 7.x) * Minimum PHP version raised to 5.6 (with support for PHP 7.x)
* Dropped support for PHP safe mode (removed php 5.4). * Dropped support for PHP safe mode (removed php 5.4).
@ -1771,7 +1763,7 @@ that it belongs to, e.g. `themes/mytheme/templates/MyVendor/Foobar/Model/MyModel
* Admin URL can now be configured via custom Director routing rule * Admin URL can now be configured via custom Director routing rule
* `Controller::init` visibility changed to protected. Use `Controller::doInit()` instead. * `Controller::init` visibility changed to protected. Use `Controller::doInit()` instead.
* `Controller::join_links` supports an array of link sections. * `Controller::join_links` supports an array of link sections.
* <a name="object-usecustomclass"></a>`Object::useCustomClass` has been removed. You should use the config API with Injector instead. * `Object::useCustomClass` has been removed. You should use the config API with Injector instead. {#object-usecustomclass}
* `Object::invokeWithExtensions` now has the same method signature as `Object::extend` and behaves the same way. * `Object::invokeWithExtensions` now has the same method signature as `Object::extend` and behaves the same way.
* `ServiceConfigurationLocator` is now an interface not a class. * `ServiceConfigurationLocator` is now an interface not a class.
* `i18nTextCollectorTask` merge is now true by default. * `i18nTextCollectorTask` merge is now true by default.
@ -1823,7 +1815,7 @@ instance rather than a `HTMLEditorField_Toolbar`
* Removed `Director.alternate_protocol`. Use `Director.alternate_base_url` instead. * Removed `Director.alternate_protocol`. Use `Director.alternate_base_url` instead.
* 'BlockUntrustedIPS' env setting has been removed. * 'BlockUntrustedIPS' env setting has been removed.
All IPs are untrusted unless `SS_TRUSTED_PROXY_IPS` is set to '*' All IPs are untrusted unless `SS_TRUSTED_PROXY_IPS` is set to '*'
See [Environment Management docs](/getting-started/environment_management/) for full details. See [Environment Management docs](/getting_started/environment_management/) for full details.
* `SS_TRUSTED_PROXY_HOST_HEADER`, `SS_TRUSTED_PROXY_PROTOCOL_HEADER`, and `SS_TRUSTED_PROXY_IP_HEADER` * `SS_TRUSTED_PROXY_HOST_HEADER`, `SS_TRUSTED_PROXY_PROTOCOL_HEADER`, and `SS_TRUSTED_PROXY_IP_HEADER`
are no longer supported. These settings should go into the Injector service configuration for are no longer supported. These settings should go into the Injector service configuration for
TrustedProxyMiddleware instead. TrustedProxyMiddleware instead.
@ -1925,7 +1917,7 @@ TrustedProxyMiddleware instead.
* Deprecated `Config::inst()->update()`. Use `Config::modify()->set()` or `Config::modify()->merge()` * Deprecated `Config::inst()->update()`. Use `Config::modify()->set()` or `Config::modify()->merge()`
instead. instead.
### <a name="overview-orm"></a>ORM ### ORM {#overview-orm}
* Deprecated `SQLQuery` in favour `SQLSelect` ([details](#sqlquery)) * Deprecated `SQLQuery` in favour `SQLSelect` ([details](#sqlquery))
* Added `DataObject.many_many` 'through' relationships now support join dataobjects in place of * Added `DataObject.many_many` 'through' relationships now support join dataobjects in place of
@ -2050,7 +2042,7 @@ usercode before invocation.
* Moved `DataObject::manyManyExtraFieldsForComponent()` to `DataObjectSchema` * Moved `DataObject::manyManyExtraFieldsForComponent()` to `DataObjectSchema`
* Deprecated `DataObject::$destroyed` * Deprecated `DataObject::$destroyed`
* Removed `DataObject::validateModelDefinitions`. Relations are now validated within `DataObjectSchema` * Removed `DataObject::validateModelDefinitions`. Relations are now validated within `DataObjectSchema`
* <a name="dataobject-has-own"></a>Removed `DataObject` methods `hasOwnTableDatabaseField`, `has_own_table_database_field` and * Removed `DataObject` methods `hasOwnTableDatabaseField`, `has_own_table_database_field` and {#dataobject-has-own}
`hasDatabaseFields` are superceded by `DataObjectSchema::fieldSpec`. `hasDatabaseFields` are superceded by `DataObjectSchema::fieldSpec`.
Use `$schema->fieldSpec($class, $field, DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED )`. Use `$schema->fieldSpec($class, $field, DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED )`.
Exclude `uninherited` option to search all tables in the class hierarchy. Exclude `uninherited` option to search all tables in the class hierarchy.
@ -2093,7 +2085,7 @@ usercode before invocation.
* Removed `Hierarchy::naturalPrev()` * Removed `Hierarchy::naturalPrev()`
* Removed `Hierarchy::markingFinished()` * Removed `Hierarchy::markingFinished()`
### <a name="overview-filesystem"></a>Filesystem ### Filesystem {#overview-filesystem}
* Image manipulations have been moved into a new `[ImageManipulation](api:SilverStripe\Assets\ImageManipulation)` trait. * Image manipulations have been moved into a new `[ImageManipulation](api:SilverStripe\Assets\ImageManipulation)` trait.
* Removed `CMSFileAddController` * Removed `CMSFileAddController`
@ -2121,7 +2113,6 @@ usercode before invocation.
cache or combined files). cache or combined files).
* `Requirements_Minifier` API can be used to declare any new mechanism for minifying combined required files. * `Requirements_Minifier` API can be used to declare any new mechanism for minifying combined required files.
By default this api is provided by the `JSMinifier` class, but user code can substitute their own. By default this api is provided by the `JSMinifier` class, but user code can substitute their own.
* `AssetField` form field to provide an `UploadField` style uploader for the new `DBFile` database field.
* `AssetControlExtension` is applied by default to all DataObjects, in order to support the management * `AssetControlExtension` is applied by default to all DataObjects, in order to support the management
of linked assets and file protection. of linked assets and file protection.
* `ProtectedFileController` class is used to serve up protected assets. * `ProtectedFileController` class is used to serve up protected assets.
@ -2167,7 +2158,7 @@ appropriate mime types. The following file manipulations classes and methods hav
* Removed `Filesystem::sync()` * Removed `Filesystem::sync()`
* Removed `AssetAdmin::doSync()` * Removed `AssetAdmin::doSync()`
### <a name="overview-template"></a>Templates and Form ### Templates and Form {#overview-template}
* Upgrade to TinyMCE 4.x * Upgrade to TinyMCE 4.x
* Templates now use a standard template lookup system via `SSViewer::get_templates_by_class()` * Templates now use a standard template lookup system via `SSViewer::get_templates_by_class()`
@ -2198,6 +2189,8 @@ appropriate mime types. The following file manipulations classes and methods hav
submission, in contrast to `FormField::setValue($value, $data)` which is intended to load its submission, in contrast to `FormField::setValue($value, $data)` which is intended to load its
value from the ORM. The second argument to `setValue()` has been added. value from the ORM. The second argument to `setValue()` has been added.
* `FormField::create_tag()` moved to `SilverStripe\View\HTML->createTag()`. * `FormField::create_tag()` moved to `SilverStripe\View\HTML->createTag()`.
* `CompositeField::setID()` is removed. ID is generated from name indirectly.
Use SilverStripe\Form\FormField::setName() instead
* Changed `ListboxField` to multiple only. Previously, this field would operate as either a * Changed `ListboxField` to multiple only. Previously, this field would operate as either a
single select (default) or multi-select through `setMultiple()`. single select (default) or multi-select through `setMultiple()`.
Now this field should only be used for multi-selection. Single-selection should be done using Now this field should only be used for multi-selection. Single-selection should be done using
@ -2206,7 +2199,7 @@ appropriate mime types. The following file manipulations classes and methods hav
instead of a list of grouped values. The method now expectes instead of a list of grouped values. The method now expectes
a non-associative array of values (not titles) or an `SS_List`. a non-associative array of values (not titles) or an `SS_List`.
<a name="requirements"></a>The following methods and properties on `Requirements_Backend` have been renamed: The following methods and properties on `Requirements_Backend` have been renamed: {#requirements}
* Renamed `$combine_files` to `$combinedFiles` * Renamed `$combine_files` to `$combinedFiles`
* Renamed `$combine_js_with_min` to `$minifyCombinedFiles` * Renamed `$combine_js_with_min` to `$minifyCombinedFiles`
@ -2234,7 +2227,7 @@ appropriate mime types. The following file manipulations classes and methods hav
A new config `Requirements_Backend.combine_in_dev` has been added in order to allow combined files to be A new config `Requirements_Backend.combine_in_dev` has been added in order to allow combined files to be
forced on during development. If this is off, combined files is only enabled in live environments. forced on during development. If this is off, combined files is only enabled in live environments.
<a name="form-validation"></a>Form validation has been refactored significantly. A new `FormMessage` trait has been created to Form validation has been refactored significantly. A new `FormMessage` trait has been created to {#form-validation}
handle `FormField` and `Form` messages. This trait has a new`setMessage()` API to assign a message, type, and cast. handle `FormField` and `Form` messages. This trait has a new`setMessage()` API to assign a message, type, and cast.
Use `getMessage()`, `getMessageType()`, `getMessageCast()` and `getMessageCastingHelper()` to retrieve them. Use `getMessage()`, `getMessageType()`, `getMessageCast()` and `getMessageCastingHelper()` to retrieve them.
@ -2282,7 +2275,7 @@ Use `getMessage()`, `getMessageType()`, `getMessageCast()` and `getMessageCastin
* Changed constructor to remove second argument (`$message`). It now only accepts `$result`, * Changed constructor to remove second argument (`$message`). It now only accepts `$result`,
which may be a string, and optional `$code` which may be a string, and optional `$code`
<a name="datetimefield"></a>New `DatetimeField` methods replace `getConfig()` / `setConfig()`: New `DatetimeField` methods replace `getConfig()` / `setConfig()`: {#datetimefield}
* Added `getTimezone()` / `setTimezone()` * Added `getTimezone()` / `setTimezone()`
* Added `getDateTimeOrder()` / `setDateTimeOrder()` * Added `getDateTimeOrder()` / `setDateTimeOrder()`
@ -2300,7 +2293,7 @@ The `DatetimeField` has changed behaviour:
* It no longer accepts `setValue()` as an array with 'date' and 'time' keys * It no longer accepts `setValue()` as an array with 'date' and 'time' keys
* Added `getHTML5()` / `setHTML5()` * Added `getHTML5()` / `setHTML5()`
<a name="datefield"></a>New `DateField` methods replace `getConfig()` / `setConfig()`: New `DateField` methods replace `getConfig()` / `setConfig()`: {#datefield}
* Added `getDateFormat()` / `setDateFormat()` * Added `getDateFormat()` / `setDateFormat()`
* Added `getMinDate()` / `setMinDate()` * Added `getMinDate()` / `setMinDate()`
@ -2316,7 +2309,7 @@ The `DateField` has changed behavior:
* The `dmyfields` option has been replced with native HTML5 behaviour (as one single `<input type=date>`). * The `dmyfields` option has been replced with native HTML5 behaviour (as one single `<input type=date>`).
* `getClientLocale` / `setClientLocale` have been removed (handled by `DateField->locale` and browser settings) * `getClientLocale` / `setClientLocale` have been removed (handled by `DateField->locale` and browser settings)
<a name="timefield"></a>New `TimeField` methods replace `getConfig()` / `setConfig()` New `TimeField` methods replace `getConfig()` / `setConfig()` {#timefield}
* Added `getTimeFormat()` / `setTimeFormat()` * Added `getTimeFormat()` / `setTimeFormat()`
* Added `getLocale()` / `setLocale()` * Added `getLocale()` / `setLocale()`
@ -2358,7 +2351,7 @@ Further API changes:
* Removed `HTMLEditorField_Flash` * Removed `HTMLEditorField_Flash`
* Removed `HTMLEditorField_Image` * Removed `HTMLEditorField_Image`
### <a name="overview-i18n"></a>i18n ### i18n {#overview-i18n}
* Upgrade of i18n to symfony/translation * Upgrade of i18n to symfony/translation
* Localisation based on language-only (without any specific locale) is now supported * Localisation based on language-only (without any specific locale) is now supported
@ -2391,7 +2384,7 @@ Further API changes:
* Removed `i18n::get_common_locales()` * Removed `i18n::get_common_locales()`
* Removed `i18n.common_locales` * Removed `i18n.common_locales`
### <a name="overview-mailer"></a>Email ### Email {#overview-mailer}
* Changed `Mailer` to an interface * Changed `Mailer` to an interface
* `Email` re-written to be powered by [SwiftMailer](https://github.com/swiftmailer/swiftmailer) * `Email` re-written to be powered by [SwiftMailer](https://github.com/swiftmailer/swiftmailer)
@ -2400,7 +2393,7 @@ Further API changes:
* Added `Email->setPlainTemplate()` for rendering plain versions of email * Added `Email->setPlainTemplate()` for rendering plain versions of email
* Renamed `Email->populateTemplate()` to `Email->setData()` * Renamed `Email->populateTemplate()` to `Email->setData()`
### <a name="overview-testing"></a>SapphireTest ### SapphireTest {#overview-testing}
* `is_running_tests()` is no longer public and user code should not rely on this. Test-specific behaviour * `is_running_tests()` is no longer public and user code should not rely on this. Test-specific behaviour
should be implemented in `setUp()` and `tearDown()` should be implemented in `setUp()` and `tearDown()`
@ -2411,7 +2404,7 @@ Further API changes:
* Renamed `$extraDataObjects` to `$extra_dataobjects` (and made static) * Renamed `$extraDataObjects` to `$extra_dataobjects` (and made static)
* Renamed `$extraControllers` to `$extra_controllers` (and made static) * Renamed `$extraControllers` to `$extra_controllers` (and made static)
### <a name="overview-security"></a>Security ### Security {#overview-security}
* `LoginForm` now has an abstract method `getAuthenticatorName()`. If you have made subclasses of this, * `LoginForm` now has an abstract method `getAuthenticatorName()`. If you have made subclasses of this,
you will need to define this method and return a short name describing the login method. you will need to define this method and return a short name describing the login method.

View File

@ -737,7 +737,7 @@ For instance:
"Versioned('StagedVersioned')" "Versioned('StagedVersioned')"
); );
} }
/** /**
* This model has versioning only, and will not has a draft or live stage, nor be affected by the current stage. * This model has versioning only, and will not has a draft or live stage, nor be affected by the current stage.
*/ */
@ -814,7 +814,7 @@ For example:
Will become: Will become:
use SilverStripe\Model\FieldType\DBVarchar; use SilverStripe\Model\FieldType\DBVarchar;
class MyObject extends DataObject class MyObject extends DataObject
{ {
private static $db = array( private static $db = array(

View File

@ -0,0 +1,42 @@
# 4.0.0-rc2
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Features and Enhancements
* 2017-10-31 [34ed2cf](https://github.com/silverstripe/silverstripe-admin/commit/34ed2cf39fcdd5ca7b5fd9243903cf4f39535da9) add more font icons (Christopher Joe)
* 2017-10-31 [c82752f](https://github.com/silverstripe/silverstripe-campaign-admin/commit/c82752ffc6521405d5064e66ffd7ca122aaa69cb) a help panel to describe what campaigns are for - extensible for later to add links to userhelp docs (Christopher Joe)
* 2017-10-31 [bde7395](https://github.com/silverstripe/silverstripe-asset-admin/commit/bde73952d804fb8cbfd1d785b1f2b04ed42b5c4d) Add icon to add to campaign button (Damian Mooyman)
* 2017-10-30 [0c178f934](https://github.com/silverstripe/silverstripe-framework/commit/0c178f934de942b8b3f6b8fda78b1228656d9906) Adjust tinymce footer, remove branding and restore path (Damian Mooyman)
* 2017-10-26 [f6b7cf888](https://github.com/silverstripe/silverstripe-framework/commit/f6b7cf88893fdc5bc50f1c10d59696b41f924dc2) disable current user from removing their admin permission (Christopher Joe)
* 2017-10-26 [6086b0d](https://github.com/silverstripe/silverstripe-admin/commit/6086b0d48327b3e66191f2c949912a0a7841d0ed) Allow popover menu icon size class to be adjustable via class props (Robbie Averill)
* 2017-10-24 [324bdad48](https://github.com/silverstripe/silverstripe-framework/commit/324bdad48c7ad3c3faa75388e22a34dfdf7ae4b9) Ensure DBVarchar scaffolds text field with TextField with appropriate max length (Damian Mooyman)
### Bugfixes
* 2017-11-02 [37508a9](https://github.com/silverstripe/silverstripe-campaign-admin/commit/37508a91fa75512083b36ef311d3bc5d1ec70092) Behat tests (Damian Mooyman)
* 2017-11-02 [052d039](https://github.com/silverstripe/silverstripe-campaign-admin/commit/052d039d0cccd0215cfeddc915865e9c89644d02) error detection is incorrect for successful messages (Christopher Joe)
* 2017-11-01 [df50c8da0](https://github.com/silverstripe/silverstripe-framework/commit/df50c8da03033c3282a9589b292b00002f4f08e8) Use parse_str in place of guzzle library (Damian Mooyman)
* 2017-11-01 [4b80a3c](https://github.com/silverstripe/silverstripe-versioned/commit/4b80a3c4c6646ef2627432f2e0525a663b8d9832) Reset reading stage when user logs out of the CMS (Robbie Averill)
* 2017-11-01 [897cba55c](https://github.com/silverstripe/silverstripe-framework/commit/897cba55cbf6bf2fae6ec0bc2f464b4b61b4dd22) Move Member log out extension points to non-deprecated methods (Robbie Averill)
* 2017-11-01 [f7e8277](https://github.com/silverstripe/silverstripe-admin/commit/f7e827777dc95e66d75f1bcfe806724ca89f0684) Do not hide all columns in the gridfield when in mobile view (Christopher Joe)
* 2017-11-01 [c331deda](https://github.com/silverstripe/silverstripe-cms/commit/c331dedae929478c971348a5cfd431d99ea09bbd) Fix ambiguous query for content report (Christopher Joe)
* 2017-11-01 [b85a10b](https://github.com/silverstripe/silverstripe-graphql/commit/b85a10bf5d2e71c24d185f9017b3f15ec573030d) Prevent PHP7 warnings on sizeof(Callable) (Damian Mooyman)
* 2017-10-31 [42acb9f](https://github.com/silverstripe/silverstripe-admin/commit/42acb9f8f740e05745a8bed64b477a6ffe49c0f8) Adjust optionset gutter to offset different base REM (Damian Mooyman)
* 2017-10-30 [7b75690](https://github.com/silverstripe/silverstripe-admin/commit/7b7569051d76a18324dde344c7e62d978ce38cd8) Restore missing i18n localisations (Damian Mooyman)
* 2017-10-30 [0dfdb5a](https://github.com/silverstripe/silverstripe-errorpage/commit/0dfdb5a608d46d12b90562f7440328dc5f8f5533) Prevent cms themes + user from interfering with error static cache (Damian Mooyman)
* 2017-10-30 [4fb53060](https://github.com/silverstripe/silverstripe-cms/commit/4fb5306008328f38886c7ddb7b0b2a31addffc47) Safely check for is_site_url before parsing a shortcode (Damian Mooyman)
* 2017-10-27 [4d063289](https://github.com/silverstripe/silverstripe-cms/commit/4d0632892bf1c4bc56041881fccb9e0ff9c1d5db) Add warning state to revert action in CMS page history (Robbie Averill)
* 2017-10-27 [33161a4](https://github.com/silverstripe/silverstripe-admin/commit/33161a4735c6527ebedc1c3b0dce96430162b24f) Remove magnifying glass icon from GridField view link (Robbie Averill)
* 2017-10-26 [6145f4b](https://github.com/silverstripe/silverstripe-assets/commit/6145f4b07dea1e0af10f915e117afd4d5ab3f250) Prevent massive recursion of publish writes (Damian Mooyman)
* 2017-10-26 [9d3277f3d](https://github.com/silverstripe/silverstripe-framework/commit/9d3277f3d3937845d893ce9a93863a63b99b4546) Fix forceWWW and forceSSL not working in _config.php (Damian Mooyman)
* 2017-10-26 [7ee5f2f](https://github.com/silverstripe/silverstripe-assets/commit/7ee5f2fcedc375e1cbe1da97170db1f9f2b90b4e) Fix migrations to delete the migrated files (Christopher Joe)
* 2017-10-26 [68c3279fd](https://github.com/silverstripe/silverstripe-framework/commit/68c3279fd9b342f9664146c0131d185ca17c340a) Ensure readonly tree dropdown is safely encoded (Damian Mooyman)
* 2017-10-25 [8725672ea](https://github.com/silverstripe/silverstripe-framework/commit/8725672eaec2a85bfb1e6af05c37ad0e8a6e1790) changelog anchors (Ingo Schommer)
* 2017-10-25 [4276951](https://github.com/silverstripe/silverstripe-versioned/commit/427695106c49a538ca58baf5c7628f5a884121c2) Combine if statements to simplify logic (Robbie Averill)
* 2017-10-25 [da4989e8f](https://github.com/silverstripe/silverstripe-framework/commit/da4989e8f624247cb3618c1244d7c19055672a6c) Do not escape the readonly values since they get escaped when rendered (Robbie Averill)
* 2017-10-23 [abfeb0c](https://github.com/silverstripe/silverstripe-versioned/commit/abfeb0cccddd0af9254756208d5374b26d2bae7c) Dont override ItemRequest if already versioned (Will Rossiter)
* 2017-10-22 [6f341d3](https://github.com/silverstripe/silverstripe-admin/commit/6f341d364ee3ddb8b7468c8b24ef3d7f7466fad0) amend preview URL correctly, more flexibly (NightjarNZ)
* 2016-06-22 [e0c829f47](https://github.com/silverstripe/silverstripe-framework/commit/e0c829f471f464d4ab23ab5b18775f2d16ccba6e) es issue 5188: X-Forwarded Proto (Ian Walls)

View File

@ -212,15 +212,18 @@ Further guidelines:
* Mention important changed classes and methods in the commit summary. * Mention important changed classes and methods in the commit summary.
Example: Bad commit message Example: Bad commit message
```
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()
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). finally fixed this dumb rendering bug that Joe talked about ... LOL
Added documentation for DBField->writeToManipulation() (related to a4bd42fd). also added another form field for password validation
``` ```
Example: Good commit message
```
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).
```

View File

@ -21,7 +21,7 @@ The benefits of writing an RFC for non-trivial feature proposals are:
* Obtaining a preliminary approval from core-committers on an architecture before code is completed, to mitigate the risk of a non-merge after a PR is submitted * Obtaining a preliminary approval from core-committers on an architecture before code is completed, to mitigate the risk of a non-merge after a PR is submitted
* Community becomes aware of incoming changes prior to the implementation * Community becomes aware of incoming changes prior to the implementation
* RFC can be used as a basis for documentation of the feature * RFC can be used as a basis for documentation of the feature
## How to write an RFC? ## How to write an RFC?
### Template ### Template
The following heading can act as a template to starting your RFC. The following heading can act as a template to starting your RFC.

View File

@ -63,15 +63,15 @@ Here's an example for replacing `Director::isDev()` with a (theoretical) `Env::i
```php ```php
/** /**
* Returns true if your are in development mode * Returns true if your are in development mode
* @deprecated 4.0 Use {@link Env::is_dev()} instead. * @deprecated 4.0 Use {@link Env::is_dev()} instead.
*/ */
public function isDev() public function isDev()
{ {
Deprecation::notice('4.0', 'Use Env::is_dev() instead'); Deprecation::notice('4.0', 'Use Env::is_dev() instead');
return Env::is_dev(); 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 This change could be committed to a minor release like *3.2.0*, and remains deprecated in all subsequent minor releases
@ -87,13 +87,14 @@ notices are always disabled on both live and test.
```php ```php
Deprecation::set_enabled(false); Deprecation::set_enabled(false);
``` ```
`.env` `.env`
```
SS_DEPRECATION_ENABLED="0" SS_DEPRECATION_ENABLED="0"
```
## Security Releases ## Security Releases

View File

@ -39,26 +39,27 @@ As a core contributor it is necessary to have installed the following set of too
* A good `.env` setup in your localhost webroot. * A good `.env` setup in your localhost webroot.
Example `.env`: Example `.env`:
``` ```
# Environent # Environent
SS_TRUSTED_PROXY_IPS="*" SS_TRUSTED_PROXY_IPS="*"
SS_ENVIRONMENT_TYPE="dev" SS_ENVIRONMENT_TYPE="dev"
# DB Credentials # DB Credentials
SS_DATABASE_CLASS="MySQLDatabase" SS_DATABASE_CLASS="MySQLDatabase"
SS_DATABASE_SERVER="127.0.0.1" SS_DATABASE_SERVER="127.0.0.1"
SS_DATABASE_USERNAME="root" SS_DATABASE_USERNAME="root"
SS_DATABASE_PASSWORD="" SS_DATABASE_PASSWORD=""
# Each release will have its own DB # Each release will have its own DB
SS_DATABASE_CHOOSE_NAME=1 SS_DATABASE_CHOOSE_NAME=1
# So you can test releases # So you can test releases
SS_DEFAULT_ADMIN_USERNAME="admin" SS_DEFAULT_ADMIN_USERNAME="admin"
SS_DEFAULT_ADMIN_PASSWORD="password" SS_DEFAULT_ADMIN_PASSWORD="password"
# Basic CLI request url default # Basic CLI request url default
SS_BASE_URL="http://localhost/" SS_BASE_URL="http://localhost/"
``` ```
You will also need to be assigned the following permissions. Contact one of the SS staff from You will also need to be assigned the following permissions. Contact one of the SS staff from
@ -168,9 +169,11 @@ doe not make any upstream changes (so it's safe to run without worrying about
any mistakes migrating their way into the public sphere). any mistakes migrating their way into the public sphere).
Invoked by running `cow release` in the format as below: Invoked by running `cow release` in the format as below:
``` ```
cow release <version> -vvv cow release <version> -vvv
``` ```
This command has the following parameters: This command has the following parameters:
* `<version>` The version that is to be released. E.g. 3.2.4 or 4.0.0-alpha4 * `<version>` The version that is to be released. E.g. 3.2.4 or 4.0.0-alpha4
@ -238,8 +241,9 @@ building an archive, and uploading to
[www.silverstripe.org](http://www.silverstripe.org/software/download/) download page. [www.silverstripe.org](http://www.silverstripe.org/software/download/) download page.
Invoked by running `cow release:publish` in the format as below: Invoked by running `cow release:publish` in the format as below:
``` ```
cow release:publish <version> -vvv cow release:publish <version> -vvv
``` ```
As with the `cow release` command, this step is broken down into the following As with the `cow release` command, this step is broken down into the following
subtasks which are invoked in sequence: subtasks which are invoked in sequence:

View File

@ -90,31 +90,37 @@ sparingly.
</div> </div>
Code for a Tip box: Code for a Tip box:
``` ```
<div class="hint" markdown='1'> <div class="hint" markdown='1'>
... ...
</div> </div>
``` ```
<div class="notice" markdown='1'> <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. "Notification box": A notification box is good for technical notifications relating to the main text. For example, notifying users about a deprecated feature.
</div> </div>
Code for a Notification box: Code for a Notification box:
``` ```
<div class="notice" markdown='1'> <div class="notice" markdown='1'>
... ...
</div> </div>
``` ```
<div class="warning" markdown='1'> <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. "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> </div>
Code for a Warning box: Code for a Warning box:
``` ```
<div class="warning" markdown='1'> <div class="warning" markdown='1'>
... ...
</div> </div>
``` ```
See [markdown extra documentation](http://michelf.com/projects/php-markdown/extra/#html) for more restrictions See [markdown extra documentation](http://michelf.com/projects/php-markdown/extra/#html) for more restrictions
on placing HTML blocks inside Markdown. on placing HTML blocks inside Markdown.

View File

@ -20,7 +20,7 @@ creating a new project, you have to upload the `en.yml` master file as a new "Re
the web interface, there's a convenient the web interface, there's a convenient
[commandline client](http://support.transifex.com/customer/portal/topics/440187-transifex-client/articles) for this [commandline client](http://support.transifex.com/customer/portal/topics/440187-transifex-client/articles) for this
purpose. In order to use it, set up a new `.tx/config` file in your module folder: purpose. In order to use it, set up a new `.tx/config` file in your module folder:
```yaml ```yaml
[main] [main]
host = https://www.transifex.com host = https://www.transifex.com
@ -71,13 +71,13 @@ 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 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 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: should be applied to the oldest release branch, and then merged forward into newer branches:
```bash
tx pull
# Manually review changes through git diff, then commit ```bash
git add lang/* tx pull
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"> <div class="notice" markdown="1">
@ -89,9 +89,9 @@ 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 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 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. `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. But Transifex only accepts structured formats like JSON.
@ -101,33 +101,35 @@ 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. 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 ```ruby
[main]
host = https://www.transifex.com
[main] [silverstripe-mymodule.master]
host = https://www.transifex.com file_filter = lang/<lang>.yml
source_file = lang/en.yml
source_lang = en
type = YML
[silverstripe-mymodule.master] [silverstripe-mymodule.master-js]
file_filter = lang/<lang>.yml file_filter = javascript/lang/src/<lang>.js
source_file = lang/en.yml source_file = javascript/lang/src/en.js
source_lang = en source_lang = en
type = YML type = KEYVALUEJSON
[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 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 files back into the JS files SilverStripe can actually read. This requires an installation of our
[buildtools](https://github.com/silverstripe/silverstripe-buildtools). [buildtools](https://github.com/silverstripe/silverstripe-buildtools).
``` ```
tx pull tx pull
(cd .. && phing -Dmodule=mymodule translation-generate-javascript-for-module) (cd .. && phing -Dmodule=mymodule translation-generate-javascript-for-module)
git add javascript/lang/* git add javascript/lang/*
git commit -m "Updated javascript translations" git commit -m "Updated javascript translations"
``` ```
# Related # Related
* [i18n](/developer_guides/i18n/): Developer-level documentation of Silverstripe's i18n capabilities * [i18n](/developer_guides/i18n/): Developer-level documentation of Silverstripe's i18n capabilities

View File

@ -27,10 +27,10 @@ As opposed to other variables, these should be declared as lower case with under
```php ```php
class MyClass class MyClass
{ {
private static $my_config_variable = 'foo'; private static $my_config_variable = 'foo';
} }
``` ```
## Prefer identical (===) comparisons over equality (==) ## Prefer identical (===) comparisons over equality (==)
@ -40,15 +40,15 @@ Read more in the PHP documentation for [comparison operators](http://php.net/man
```php ```php
// good - only need to cast to (int) if $a might not already be an int // good - only need to cast to (int) if $a might not already be an int
if ((int)$a === 100) { if ((int)$a === 100) {
doThis(); doThis();
} }
// bad // bad
if ($a == 100) { if ($a == 100) {
doThis(); doThis();
} }
``` ```
## Separation of Logic and Presentation ## Separation of Logic and Presentation
@ -57,28 +57,32 @@ Try to avoid using PHP's ability to mix HTML into the code.
```php ```php
// PHP code // PHP code
public function getTitle() public function getTitle()
{ {
return "<h2>Bad Example</h2>"; return "<h2>Bad Example</h2>";
} }
```
// Template code ```ss
$Title // Template code
$Title
``` ```
Better: Keep HTML in template files: Better: Keep HTML in template files:
```php ```php
// PHP code // PHP code
public function getTitle() public function getTitle()
{ {
return "Better Example"; return "Better Example";
} }
```
// Template code ```ss
<h2>$Title</h2> // Template code
<h2>$Title</h2>
``` ```
## Comments ## Comments
@ -97,34 +101,34 @@ Example:
```php ```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 short description for this class. * My Method.
* My longer description with * This method returns something cool. {@link MyParentMethod} has other cool stuff in it.
* multiple lines and richer formatting.
* *
* Usage: * @param string $colour The colour of cool things that you want
* <code> * @return DataList A list of everything cool
* $c = new MyClass();
* $c->myMethod();
* </code>
*
* @package custom
*/ */
class MyClass extends Class public function myMethod($colour)
{ {
/** // ...
* My Method.
* This method returns something cool. {@link MyParentMethod} has other cool stuff in it.
*
* @param string $colour The colour of cool things that you want
* @return DataList A list of everything cool
*/
public function myMethod($foo)
{
// ...
}
} }
}
``` ```
## Class Member Ordering ## Class Member Ordering
@ -148,7 +152,7 @@ with the column or table name escaped with double quotes as below.
```php ```php
MyClass::get()->where(["\"Score\" > ?" => 50]); MyClass::get()->where(['"Score" > ?' => 50]);
``` ```
@ -159,7 +163,7 @@ are single quoted.
```php ```php
MyClass::get()->where("\"Title\" = 'my title'"); MyClass::get()->where("\"Title\" = 'my title'");
``` ```
Use [ANSI SQL](http://en.wikipedia.org/wiki/SQL#Standardization) format where possible. Use [ANSI SQL](http://en.wikipedia.org/wiki/SQL#Standardization) format where possible.

View File

@ -6,7 +6,7 @@ ar:
SHOWONCLICKTITLE: 'تغيير كلمة المرور' SHOWONCLICKTITLE: 'تغيير كلمة المرور'
SilverStripe\Forms\DateField: SilverStripe\Forms\DateField:
NOTSET: 'غير محدد' NOTSET: 'غير محدد'
TODAY: 'اليوم' TODAY: اليوم
VALIDDATEFORMAT2: 'الرجاء إدخال صيغة تاريخ صحيحة ({صيغة})' VALIDDATEFORMAT2: 'الرجاء إدخال صيغة تاريخ صحيحة ({صيغة})'
VALIDDATEMAXDATE: 'التسجيل الخاص بك قد يكون أقدم أو مطابق لأقصى تاريخ مسموح به ({تاريخ})' VALIDDATEMAXDATE: 'التسجيل الخاص بك قد يكون أقدم أو مطابق لأقصى تاريخ مسموح به ({تاريخ})'
VALIDDATEMINDATE: 'التسجيل الخاص بك قد يكون أحدث أو مطابق للحد الأدنى للتاريخ المسموح به ({تاريخ})' VALIDDATEMINDATE: 'التسجيل الخاص بك قد يكون أحدث أو مطابق للحد الأدنى للتاريخ المسموح به ({تاريخ})'
@ -18,14 +18,14 @@ ar:
VALIDATIONPASSWORDSDONTMATCH: 'رقم المرور غير صحيح' VALIDATIONPASSWORDSDONTMATCH: 'رقم المرور غير صحيح'
VALIDATIONPASSWORDSNOTEMPTY: 'أرقام المرور لا يمكن أن تكون فارغة' VALIDATIONPASSWORDSNOTEMPTY: 'أرقام المرور لا يمكن أن تكون فارغة'
VALIDATIONSTRONGPASSWORD: 'كلمات المرور يجب أن تحتوي على رقم و حرف على الأقل' VALIDATIONSTRONGPASSWORD: 'كلمات المرور يجب أن تحتوي على رقم و حرف على الأقل'
VALIDATOR: 'المحقق' VALIDATOR: المحقق
VALIDCURRENCY: 'يرجى إدخال عملة صحيحة' VALIDCURRENCY: 'يرجى إدخال عملة صحيحة'
SilverStripe\Forms\FormField: SilverStripe\Forms\FormField:
NONE: لايوجد NONE: لايوجد
SilverStripe\Forms\GridField\GridField: SilverStripe\Forms\GridField\GridField:
Add: 'إضافة {اسم}' Add: 'إضافة {اسم}'
CSVEXPORT: 'تصدير إلى CSV' CSVEXPORT: 'تصدير إلى CSV'
Filter: 'مرشح' Filter: مرشح
FilterBy: 'ترشيح باستخدام' FilterBy: 'ترشيح باستخدام'
Find: ابحث Find: ابحث
LinkExisting: 'الرابط موجود' LinkExisting: 'الرابط موجود'
@ -54,8 +54,8 @@ ar:
SilverStripe\Forms\GridField\GridFieldEditButton_ss: SilverStripe\Forms\GridField\GridFieldEditButton_ss:
EDIT: تعديل EDIT: تعديل
SilverStripe\Forms\MoneyField: SilverStripe\Forms\MoneyField:
FIELDLABELAMOUNT: 'الكمية' FIELDLABELAMOUNT: الكمية
FIELDLABELCURRENCY: 'العملة' FIELDLABELCURRENCY: العملة
SilverStripe\Forms\NullableField: SilverStripe\Forms\NullableField:
IsNullLabel: باطل IsNullLabel: باطل
SilverStripe\Forms\NumericField: SilverStripe\Forms\NumericField:
@ -120,7 +120,7 @@ ar:
ValidationIdentifierFailed: 'لا يمكن الكتابة فوق رقم العضوية الحالي {معرف} مع معرف مطابق ({اسم} = {قيمة}))' ValidationIdentifierFailed: 'لا يمكن الكتابة فوق رقم العضوية الحالي {معرف} مع معرف مطابق ({اسم} = {قيمة}))'
WELCOMEBACK: 'مرحبا بك مرة أخرى، {الاسم الأول}' WELCOMEBACK: 'مرحبا بك مرة أخرى، {الاسم الأول}'
YOUROLDPASSWORD: 'رقم المرور السابق' YOUROLDPASSWORD: 'رقم المرور السابق'
belongs_many_many_Groups: 'المجموعات' belongs_many_many_Groups: المجموعات
db_Locale: 'واجهة الموقع' db_Locale: 'واجهة الموقع'
db_LockedOutUntil: 'مغلق حتى تاريخ' db_LockedOutUntil: 'مغلق حتى تاريخ'
db_Password: 'الرقم السري' db_Password: 'الرقم السري'

View File

@ -2,10 +2,10 @@ bg:
SilverStripe\Admin\LeftAndMain: SilverStripe\Admin\LeftAndMain:
VersionUnknown: непозната VersionUnknown: непозната
SilverStripe\AssetAdmin\Forms\UploadField: SilverStripe\AssetAdmin\Forms\UploadField:
Dimensions: 'Размери' Dimensions: Размери
EDIT: Промени EDIT: Промени
EDITINFO: 'Редактирай този файл' EDITINFO: 'Редактирай този файл'
REMOVE: 'Премахни' REMOVE: Премахни
SilverStripe\Control\ChangePasswordEmail_ss: SilverStripe\Control\ChangePasswordEmail_ss:
CHANGEPASSWORDFOREMAIL: 'Паролата за акаунт с адрес {email} беше променена. Ако не Вие сте променили паролата си, направете го от адреса долу' CHANGEPASSWORDFOREMAIL: 'Паролата за акаунт с адрес {email} беше променена. Ако не Вие сте променили паролата си, направете го от адреса долу'
CHANGEPASSWORDTEXT1: 'Променихте паролата си за' CHANGEPASSWORDTEXT1: 'Променихте паролата си за'
@ -99,7 +99,7 @@ bg:
Deleted: 'Изтрити {type} {name}' Deleted: 'Изтрити {type} {name}'
Save: Запис Save: Запис
SilverStripe\Forms\GridField\GridFieldEditButton_ss: SilverStripe\Forms\GridField\GridFieldEditButton_ss:
EDIT: 'Редактиране' EDIT: Редактиране
SilverStripe\Forms\GridField\GridFieldPaginator: SilverStripe\Forms\GridField\GridFieldPaginator:
OF: от OF: от
Page: Страница Page: Страница
@ -185,18 +185,18 @@ bg:
MEMBERS: Потребители MEMBERS: Потребители
NEWGROUP: 'Нова група' NEWGROUP: 'Нова група'
NoRoles: 'Няма намерени роли' NoRoles: 'Няма намерени роли'
PERMISSIONS: 'Разрешения' PERMISSIONS: Разрешения
PLURALNAME: Групи PLURALNAME: Групи
PLURALS: PLURALS:
one: Група one: Група
other: '{count} групи' other: '{count} групи'
Parent: 'Група източник' Parent: 'Група източник'
ROLES: 'Роли' ROLES: Роли
ROLESDESCRIPTION: 'Ролите са предварително зададени сетове от разрешения и могат да бъдат присвоявани на групи.<br />Ако е нужно, те се наследяват от родителските групи.' ROLESDESCRIPTION: 'Ролите са предварително зададени сетове от разрешения и могат да бъдат присвоявани на групи.<br />Ако е нужно, те се наследяват от родителските групи.'
RolesAddEditLink: 'Управление на ролите' RolesAddEditLink: 'Управление на ролите'
SINGULARNAME: Група SINGULARNAME: Група
Sort: Сортиране Sort: Сортиране
has_many_Permissions: 'Разрешения' has_many_Permissions: Разрешения
many_many_Members: Потребители many_many_Members: Потребители
SilverStripe\Security\LoginAttempt: SilverStripe\Security\LoginAttempt:
Email: 'Email адрес' Email: 'Email адрес'
@ -212,7 +212,7 @@ bg:
BUTTONCHANGEPASSWORD: 'Променете паролата' BUTTONCHANGEPASSWORD: 'Променете паролата'
BUTTONLOGIN: Влез BUTTONLOGIN: Влез
BUTTONLOGINOTHER: 'Влез като някой друг' BUTTONLOGINOTHER: 'Влез като някой друг'
BUTTONLOGOUT: 'Изход' BUTTONLOGOUT: Изход
BUTTONLOSTPASSWORD: 'Загубих си паролата' BUTTONLOSTPASSWORD: 'Загубих си паролата'
CONFIRMNEWPASSWORD: 'Потвърдете новата парола' CONFIRMNEWPASSWORD: 'Потвърдете новата парола'
CONFIRMPASSWORD: 'Потвърдете паролата' CONFIRMPASSWORD: 'Потвърдете паролата'
@ -274,11 +274,11 @@ bg:
FULLADMINRIGHTS: 'Пълни административни права' FULLADMINRIGHTS: 'Пълни административни права'
FULLADMINRIGHTS_HELP: 'Включва и отменя другите присвоени разрешения.' FULLADMINRIGHTS_HELP: 'Включва и отменя другите присвоени разрешения.'
PERMISSIONS_CATEGORY: 'Роли и права за достъп' PERMISSIONS_CATEGORY: 'Роли и права за достъп'
PLURALNAME: 'Разрешения' PLURALNAME: Разрешения
PLURALS: PLURALS:
one: 'Разрешение' one: Разрешение
other: '{count} разрешения' other: '{count} разрешения'
SINGULARNAME: 'Разрешение' SINGULARNAME: Разрешение
UserPermissionsIntro: 'Присвояването на групи на този потребител ще коригира разрешенията, които има. Вижте раздела за групи за подробности относно разрешенията на отделните групи.' UserPermissionsIntro: 'Присвояването на групи на този потребител ще коригира разрешенията, които има. Вижте раздела за групи за подробности относно разрешенията на отделните групи.'
SilverStripe\Security\PermissionCheckboxSetField: SilverStripe\Security\PermissionCheckboxSetField:
AssignedTo: 'приписана на "{title}"' AssignedTo: 'приписана на "{title}"'
@ -287,11 +287,11 @@ bg:
FromRoleOnGroup: 'унаследено от роля "{roletitle}" на група "{grouptitle}"' FromRoleOnGroup: 'унаследено от роля "{roletitle}" на група "{grouptitle}"'
SilverStripe\Security\PermissionRole: SilverStripe\Security\PermissionRole:
OnlyAdminCanApply: 'Може да се приложи само от администратор' OnlyAdminCanApply: 'Може да се приложи само от администратор'
PLURALNAME: 'Роли' PLURALNAME: Роли
PLURALS: PLURALS:
one: 'Роля' one: Роля
other: '{count} роли' other: '{count} роли'
SINGULARNAME: 'Роля' SINGULARNAME: Роля
Title: Заглавие Title: Заглавие
SilverStripe\Security\PermissionRoleCode: SilverStripe\Security\PermissionRoleCode:
PLURALNAME: 'Кодове за роли с разрешения' PLURALNAME: 'Кодове за роли с разрешения'
@ -315,7 +315,7 @@ bg:
ENTERNEWPASSWORD: 'Моля, въведете нова парола.' ENTERNEWPASSWORD: 'Моля, въведете нова парола.'
ERRORPASSWORDPERMISSION: 'Трябва да сте влезли, за да можете да промените вашата парола!' ERRORPASSWORDPERMISSION: 'Трябва да сте влезли, за да можете да промените вашата парола!'
LOGIN: 'Влезте в системата' LOGIN: 'Влезте в системата'
LOGOUT: 'Изход' LOGOUT: Изход
LOSTPASSWORDHEADER: 'Забравена парола' LOSTPASSWORDHEADER: 'Забравена парола'
NOTEPAGESECURED: 'Тази страница е защитена. Въведете вашите данни по-долу, за да продължите.' NOTEPAGESECURED: 'Тази страница е защитена. Въведете вашите данни по-долу, за да продължите.'
NOTERESETLINKINVALID: '<p>Връзката за нулиране на парола не е вярна или е просрочена.</p><p>Можете да заявите нова <a href="{link1}">тук</a> или да промените паролата си след като <a href="{link2}">влезете</a>.</p>' NOTERESETLINKINVALID: '<p>Връзката за нулиране на парола не е вярна или е просрочена.</p><p>Можете да заявите нова <a href="{link1}">тук</a> или да промените паролата си след като <a href="{link2}">влезете</a>.</p>'

View File

@ -39,7 +39,7 @@ bs:
FIRSTNAME: Ime FIRSTNAME: Ime
INTERFACELANG: 'Jezik sučelja' INTERFACELANG: 'Jezik sučelja'
NEWPASSWORD: 'Nova šifra' NEWPASSWORD: 'Nova šifra'
PASSWORD: 'Šifra' PASSWORD: Šifra
SUBJECTPASSWORDCHANGED: 'Vaša šifra je promijenjena' SUBJECTPASSWORDCHANGED: 'Vaša šifra je promijenjena'
SUBJECTPASSWORDRESET: 'Link za ponovno podešavanje Vaše šifre' SUBJECTPASSWORDRESET: 'Link za ponovno podešavanje Vaše šifre'
SURNAME: Prezime SURNAME: Prezime

View File

@ -100,6 +100,8 @@ en:
Save: Save Save: Save
SilverStripe\Forms\GridField\GridFieldEditButton_ss: SilverStripe\Forms\GridField\GridFieldEditButton_ss:
EDIT: Edit EDIT: Edit
SilverStripe\Forms\GridField\GridFieldGroupDeleteAction:
UnlinkSelfFailure: 'Cannot remove yourself from this group, you will lose admin rights'
SilverStripe\Forms\GridField\GridFieldPaginator: SilverStripe\Forms\GridField\GridFieldPaginator:
OF: of OF: of
Page: Page Page: Page
@ -250,6 +252,7 @@ en:
SUBJECTPASSWORDCHANGED: 'Your password has been changed' SUBJECTPASSWORDCHANGED: 'Your password has been changed'
SUBJECTPASSWORDRESET: 'Your password reset link' SUBJECTPASSWORDRESET: 'Your password reset link'
SURNAME: Surname SURNAME: Surname
VALIDATIONADMINLOSTACCESS: 'Cannot remove all admin groups from your profile'
ValidationIdentifierFailed: 'Can''t overwrite existing member #{id} with identical identifier ({name} = {value}))' ValidationIdentifierFailed: 'Can''t overwrite existing member #{id} with identical identifier ({name} = {value}))'
WELCOMEBACK: 'Welcome Back, {firstname}' WELCOMEBACK: 'Welcome Back, {firstname}'
YOUROLDPASSWORD: 'Your old password' YOUROLDPASSWORD: 'Your old password'

View File

@ -13,8 +13,8 @@ fa_IR:
SilverStripe\Forms\CurrencyField: SilverStripe\Forms\CurrencyField:
CURRENCYSYMBOL: CURRENCYSYMBOL:
SilverStripe\Forms\DateField: SilverStripe\Forms\DateField:
NOTSET: 'نامشخص' NOTSET: نامشخص
TODAY: 'امروز' TODAY: امروز
VALIDDATEFORMAT2: 'لطفاً یک قالب تاریخ معتبر وارد نمایید ({format})' VALIDDATEFORMAT2: 'لطفاً یک قالب تاریخ معتبر وارد نمایید ({format})'
VALIDDATEMAXDATE: 'تاریخ شما باید قدیمی‌تر یا برابر با حداکثر تاریخ مجاز ({date}) باشد' VALIDDATEMAXDATE: 'تاریخ شما باید قدیمی‌تر یا برابر با حداکثر تاریخ مجاز ({date}) باشد'
VALIDDATEMINDATE: 'تاریخ شما باید جدیدتر یا برابر با حداقل تاریخ مجاز ({date}) باشد' VALIDDATEMINDATE: 'تاریخ شما باید جدیدتر یا برابر با حداقل تاریخ مجاز ({date}) باشد'
@ -32,7 +32,7 @@ fa_IR:
VALIDATOR: اعتبارسنج VALIDATOR: اعتبارسنج
VALIDCURRENCY: 'لطفاً یک واحد پول معتبر وارد نمایید' VALIDCURRENCY: 'لطفاً یک واحد پول معتبر وارد نمایید'
SilverStripe\Forms\FormField: SilverStripe\Forms\FormField:
NONE: 'هیچ‌کدام' NONE: هیچ‌کدام
SilverStripe\Forms\GridField\GridField: SilverStripe\Forms\GridField\GridField:
Add: 'افزودن {name}' Add: 'افزودن {name}'
CSVEXPORT: 'خروجی‌گیری به CSV' CSVEXPORT: 'خروجی‌گیری به CSV'
@ -65,7 +65,7 @@ fa_IR:
SilverStripe\Forms\GridField\GridFieldEditButton_ss: SilverStripe\Forms\GridField\GridFieldEditButton_ss:
EDIT: ویرایش EDIT: ویرایش
SilverStripe\Forms\MoneyField: SilverStripe\Forms\MoneyField:
FIELDLABELAMOUNT: 'مقدار' FIELDLABELAMOUNT: مقدار
FIELDLABELCURRENCY: 'واحد پول' FIELDLABELCURRENCY: 'واحد پول'
SilverStripe\Forms\NullableField: SilverStripe\Forms\NullableField:
IsNullLabel: 'خالی است' IsNullLabel: 'خالی است'
@ -89,12 +89,12 @@ fa_IR:
PASSWORDEXPIRED: '<p>گذرواژه شما منقضی شده‌است. <a target="_top" href="{link}">لطفاً یکی دیگر برگزینید.</a></p>' PASSWORDEXPIRED: '<p>گذرواژه شما منقضی شده‌است. <a target="_top" href="{link}">لطفاً یکی دیگر برگزینید.</a></p>'
SilverStripe\Security\CMSSecurity: SilverStripe\Security\CMSSecurity:
INVALIDUSER: '<p>کاربر نامعتبر. جهت ادامه <a target="_top" href="{link}">لطفاً اینجا مجدداً وارد شوید.</a></p>' INVALIDUSER: '<p>کاربر نامعتبر. جهت ادامه <a target="_top" href="{link}">لطفاً اینجا مجدداً وارد شوید.</a></p>'
SUCCESS: 'موفقیت' SUCCESS: موفقیت
SUCCESSCONTENT: '<p>ورود موفق. اگر به‌طور خودکار ارجاع نشدید <a target="_top" href="{link}">اینجا را کلیک کنید.</a></p>' SUCCESSCONTENT: '<p>ورود موفق. اگر به‌طور خودکار ارجاع نشدید <a target="_top" href="{link}">اینجا را کلیک کنید.</a></p>'
SilverStripe\Security\Group: SilverStripe\Security\Group:
AddRole: 'اعمال یک وظیفه به گروه' AddRole: 'اعمال یک وظیفه به گروه'
Code: 'كد گروه' Code: 'كد گروه'
DefaultGroupTitleAdministrators: 'مدیران' DefaultGroupTitleAdministrators: مدیران
DefaultGroupTitleContentAuthors: 'نویسندگان مطالب' DefaultGroupTitleContentAuthors: 'نویسندگان مطالب'
Description: توضحیات Description: توضحیات
GroupReminder: 'اگر یک گروه مادر را برگزینید،‌ این گروه تمامی وظایف آن را می‌پذیرد' GroupReminder: 'اگر یک گروه مادر را برگزینید،‌ این گروه تمامی وظایف آن را می‌پذیرد'
@ -128,7 +128,7 @@ fa_IR:
ENTEREMAIL: 'لطفاً یک نشانی ایمیل وارد نمایید تا لینک ازنوسازی گذرواژه را دریافت کنید.' ENTEREMAIL: 'لطفاً یک نشانی ایمیل وارد نمایید تا لینک ازنوسازی گذرواژه را دریافت کنید.'
ERRORNEWPASSWORD: 'شما گذرواژه جدید خود را متفاوت وارد کرده‌اید، دوباره امتحان نمایید' ERRORNEWPASSWORD: 'شما گذرواژه جدید خود را متفاوت وارد کرده‌اید، دوباره امتحان نمایید'
ERRORPASSWORDNOTMATCH: 'گذرواژه کنونی همانند نیست، لطفاً مجدداً تلاش نمایید' ERRORPASSWORDNOTMATCH: 'گذرواژه کنونی همانند نیست، لطفاً مجدداً تلاش نمایید'
FIRSTNAME: 'نام' FIRSTNAME: نام
INTERFACELANG: 'زبان برنامه' INTERFACELANG: 'زبان برنامه'
KEEPMESIGNEDIN: 'مرا واردشده نگه‌دار' KEEPMESIGNEDIN: 'مرا واردشده نگه‌دار'
LOGGEDINAS: 'شما به {name} عنوان وارد شده‌اید.' LOGGEDINAS: 'شما به {name} عنوان وارد شده‌اید.'
@ -155,8 +155,8 @@ fa_IR:
AdminGroup: 'مدیر کل' AdminGroup: 'مدیر کل'
CONTENT_CATEGORY: 'دسترسی محتوا' CONTENT_CATEGORY: 'دسترسی محتوا'
FULLADMINRIGHTS: 'توانایی‌های کامل مدیریتی:' FULLADMINRIGHTS: 'توانایی‌های کامل مدیریتی:'
PLURALNAME: 'مجوز‌ها' PLURALNAME: مجوز‌ها
SINGULARNAME: 'مجوز' SINGULARNAME: مجوز
SilverStripe\Security\PermissionCheckboxSetField: SilverStripe\Security\PermissionCheckboxSetField:
AssignedTo: 'اختصاص داده‌شده به "{title}"' AssignedTo: 'اختصاص داده‌شده به "{title}"'
FromGroup: 'از گروه "{title}" ارث برده است' FromGroup: 'از گروه "{title}" ارث برده است'

View File

@ -104,6 +104,15 @@ fi:
OF: / OF: /
Page: Sivu Page: Sivu
View: Näytä View: Näytä
SilverStripe\Forms\GridField\GridFieldVersionedState:
ADDEDTODRAFTHELP: 'Kohdetta ei ole julkaistu vielä'
ADDEDTODRAFTSHORT: Luonnos
ARCHIVEDPAGEHELP: 'Kohde on poistettu luonnoksista ja julkaisusta'
ARCHIVEDPAGESHORT: Arkistoitu
MODIFIEDONDRAFTHELP: 'Kohteella on julkaisemattomia muutoksia'
MODIFIEDONDRAFTSHORT: Muokattu
ONLIVEONLYSHORT: 'Vain julkaistuna'
ONLIVEONLYSHORTHELP: 'Kohde on julkaistu, mutta poistettu luonnoksista'
SilverStripe\Forms\MoneyField: SilverStripe\Forms\MoneyField:
FIELDLABELAMOUNT: Määrä FIELDLABELAMOUNT: Määrä
FIELDLABELCURRENCY: Valuutta FIELDLABELCURRENCY: Valuutta

View File

@ -11,4 +11,4 @@ hi:
ERRORNOTADMIN: 'वह उपयोगकर्ता एक व्यवस्थापक नहीं है.' ERRORNOTADMIN: 'वह उपयोगकर्ता एक व्यवस्थापक नहीं है.'
SilverStripe\Security\Group: SilverStripe\Security\Group:
RolesAddEditLink: 'भूमिकाओं का प्रबंधन करे ' RolesAddEditLink: 'भूमिकाओं का प्रबंधन करे '
has_many_Permissions: 'अनुमतियाँ' has_many_Permissions: अनुमतियाँ

View File

@ -13,13 +13,13 @@ ja:
SilverStripe\Forms\DropdownField: SilverStripe\Forms\DropdownField:
CHOOSE: (選択) CHOOSE: (選択)
SilverStripe\Forms\EmailField: SilverStripe\Forms\EmailField:
VALIDATION: 'メールアドレスを入力してください' VALIDATION: メールアドレスを入力してください
SilverStripe\Forms\Form: SilverStripe\Forms\Form:
VALIDATIONPASSWORDSDONTMATCH: パスワードが一致しません VALIDATIONPASSWORDSDONTMATCH: パスワードが一致しません
VALIDATIONPASSWORDSNOTEMPTY: パスワードが空欄です VALIDATIONPASSWORDSNOTEMPTY: パスワードが空欄です
VALIDATIONSTRONGPASSWORD: 'パスワードは少なくとも1桁の数字と1つの英字を含んでいる必要があります' VALIDATIONSTRONGPASSWORD: パスワードは少なくとも1桁の数字と1つの英字を含んでいる必要があります
VALIDATOR: 検証 VALIDATOR: 検証
VALIDCURRENCY: '有効な通貨を入力してください' VALIDCURRENCY: 有効な通貨を入力してください
SilverStripe\Forms\FormField: SilverStripe\Forms\FormField:
NONE: 何もありません NONE: 何もありません
SilverStripe\Forms\GridField\GridField: SilverStripe\Forms\GridField\GridField:
@ -30,7 +30,7 @@ ja:
Find: 探す Find: 探す
LinkExisting: 既存のリンク LinkExisting: 既存のリンク
NewRecord: '新しい{type}' NewRecord: '新しい{type}'
NoItemsFound: '項目が見つかりませんでした' NoItemsFound: 項目が見つかりませんでした
PRINTEDAT: で印刷 PRINTEDAT: で印刷
PRINTEDBY: によって印刷 PRINTEDBY: によって印刷
PlaceHolder: '{type}を探す' PlaceHolder: '{type}を探す'
@ -73,28 +73,28 @@ ja:
SilverStripe\ORM\Hierarchy\Hierarchy: SilverStripe\ORM\Hierarchy\Hierarchy:
InfiniteLoopNotAllowed: '無限ループが"{型}"階層内で見つかりました。 これを解決するために親を変更してください。' InfiniteLoopNotAllowed: '無限ループが"{型}"階層内で見つかりました。 これを解決するために親を変更してください。'
SilverStripe\Security\BasicAuth: SilverStripe\Security\BasicAuth:
ENTERINFO: 'ユーザー名とパスワードを入力してください' ENTERINFO: ユーザー名とパスワードを入力してください
ERRORNOTADMIN: 'このユーザーは管理者(アドミニストレーター)ではありません' ERRORNOTADMIN: このユーザーは管理者(アドミニストレーター)ではありません
ERRORNOTREC: 'ユーザー名 / パスワードは認識されませんでした' ERRORNOTREC: 'ユーザー名 / パスワードは認識されませんでした'
SilverStripe\Security\Group: SilverStripe\Security\Group:
AddRole: 'このグループに役割を追加' AddRole: このグループに役割を追加
Code: グループコード Code: グループコード
DefaultGroupTitleAdministrators: '管理者' DefaultGroupTitleAdministrators: 管理者
DefaultGroupTitleContentAuthors: 'コンテンツの作成者' DefaultGroupTitleContentAuthors: コンテンツの作成者
Description: 説明文 Description: 説明文
GroupReminder: 'あなたが親グループを選択した場合、このグループはそのすべての役割を選択します' GroupReminder: あなたが親グループを選択した場合、このグループはそのすべての役割を選択します
Locked: ロックしますか? Locked: ロックしますか?
NoRoles: 役割が見つかりませんでした NoRoles: 役割が見つかりませんでした
Parent: '元グループ' Parent: 元グループ
RolesAddEditLink: 役割の管理 RolesAddEditLink: 役割の管理
Sort: '並び順' Sort: 並び順
has_many_Permissions: 承認 has_many_Permissions: 承認
many_many_Members: メンバー many_many_Members: メンバー
SilverStripe\Security\LoginAttempt: SilverStripe\Security\LoginAttempt:
IP: IPアドレス IP: IPアドレス
Status: ステータス Status: ステータス
SilverStripe\Security\Member: SilverStripe\Security\Member:
ADDGROUP: 'グループを追加' ADDGROUP: グループを追加
BUTTONCHANGEPASSWORD: パスワードの変更 BUTTONCHANGEPASSWORD: パスワードの変更
BUTTONLOGIN: ログイン BUTTONLOGIN: ログイン
BUTTONLOGINOTHER: 他の誰かとしてログイン BUTTONLOGINOTHER: 他の誰かとしてログイン
@ -102,12 +102,12 @@ ja:
CONFIRMNEWPASSWORD: 新しいパスワードを確認します CONFIRMNEWPASSWORD: 新しいパスワードを確認します
CONFIRMPASSWORD: パスワード(確認のためもう一度) CONFIRMPASSWORD: パスワード(確認のためもう一度)
EMAIL: メールアドレス EMAIL: メールアドレス
EMPTYNEWPASSWORD: 'パスワードが空です。もう一度入力して下さい。' EMPTYNEWPASSWORD: パスワードが空です。もう一度入力して下さい。
ENTEREMAIL: 'パスワードをリセットするためにメールアドレスを入力してください。' ENTEREMAIL: パスワードをリセットするためにメールアドレスを入力してください。
ERRORLOCKEDOUT2: '複数回ログインに失敗したため、あなたのアカウントは一時的に使用不可能になっています。 {count} 分後に再びログインしてください。' ERRORLOCKEDOUT2: '複数回ログインに失敗したため、あなたのアカウントは一時的に使用不可能になっています。 {count} 分後に再びログインしてください。'
ERRORNEWPASSWORD: '入力されたパスワードが一致しません。再度お試しください' ERRORNEWPASSWORD: 入力されたパスワードが一致しません。再度お試しください
ERRORPASSWORDNOTMATCH: '登録されているパスワードと一致しません、もう一度入力し直してください' ERRORPASSWORDNOTMATCH: 登録されているパスワードと一致しません、もう一度入力し直してください
ERRORWRONGCRED: 'メールアドレスまたはパスワードが正しくありません、もう一度入力し直してください' ERRORWRONGCRED: メールアドレスまたはパスワードが正しくありません、もう一度入力し直してください
FIRSTNAME: FIRSTNAME:
INTERFACELANG: 画面言語 INTERFACELANG: 画面言語
LOGGEDINAS: '{name}としてログインしています。' LOGGEDINAS: '{name}としてログインしています。'
@ -126,32 +126,32 @@ ja:
db_PasswordExpiry: パスワードの有効期限 db_PasswordExpiry: パスワードの有効期限
SilverStripe\Security\PasswordValidator: SilverStripe\Security\PasswordValidator:
LOWCHARSTRENGTH: '次の文字のいくつかを追加してパスワードを強化してください: {chars}' LOWCHARSTRENGTH: '次の文字のいくつかを追加してパスワードを強化してください: {chars}'
PREVPASSWORD: 'このパスワードは過去に使用されています、新しいパスワードを選択してください' PREVPASSWORD: このパスワードは過去に使用されています、新しいパスワードを選択してください
TOOSHORT: パスワードが短すぎます、%文字以上でなければなりません TOOSHORT: パスワードが短すぎます、%文字以上でなければなりません
SilverStripe\Security\Permission: SilverStripe\Security\Permission:
AdminGroup: '管理者' AdminGroup: 管理者
CONTENT_CATEGORY: コンテンツに関する権限 CONTENT_CATEGORY: コンテンツに関する権限
FULLADMINRIGHTS: '完全な管理権' FULLADMINRIGHTS: 完全な管理権
FULLADMINRIGHTS_HELP: 暗黙的に定義または他のすべての割り当てられた権限を無効にする。 FULLADMINRIGHTS_HELP: 暗黙的に定義または他のすべての割り当てられた権限を無効にする。
UserPermissionsIntro: 'このユーザーにグループを割り当てると、彼らが持っている権限を調整します。個々のグループの権限の詳細については、グループセクションを参照してください。' UserPermissionsIntro: このユーザーにグループを割り当てると、彼らが持っている権限を調整します。個々のグループの権限の詳細については、グループセクションを参照してください。
SilverStripe\Security\PermissionCheckboxSetField: SilverStripe\Security\PermissionCheckboxSetField:
AssignedTo: '"{title}" に割り当てられた' AssignedTo: '"{title}" に割り当てられた'
FromGroup: 'グループ"{title}"から継承' FromGroup: 'グループ"{title}"から継承'
FromRole: '役割"{title}"から継承' FromRole: '役割"{title}"から継承'
FromRoleOnGroup: 'グループ "{roletitle}" のロール "{grouptitle}"から継承' FromRoleOnGroup: 'グループ "{roletitle}" のロール "{grouptitle}"から継承'
SilverStripe\Security\PermissionRole: SilverStripe\Security\PermissionRole:
OnlyAdminCanApply: '管理者のみ適用可能' OnlyAdminCanApply: 管理者のみ適用可能
Title: タイトル Title: タイトル
SilverStripe\Security\Security: SilverStripe\Security\Security:
ALREADYLOGGEDIN: 'あなたはこのページにアクセスできません。別のアカウントを持っていたら 再ログインを行ってください。' ALREADYLOGGEDIN: 'あなたはこのページにアクセスできません。別のアカウントを持っていたら 再ログインを行ってください。'
BUTTONSEND: 'パスワードリセットのリンクを送信してください' BUTTONSEND: パスワードリセットのリンクを送信してください
CHANGEPASSWORDBELOW: 以下のパスワードを変更できます CHANGEPASSWORDBELOW: 以下のパスワードを変更できます
CHANGEPASSWORDHEADER: パスワードを変更しました CHANGEPASSWORDHEADER: パスワードを変更しました
ENTERNEWPASSWORD: '新しいパスワードを入力してください' ENTERNEWPASSWORD: 新しいパスワードを入力してください
ERRORPASSWORDPERMISSION: パスワードを変更する為に、ログインしなければなりません! ERRORPASSWORDPERMISSION: パスワードを変更する為に、ログインしなければなりません!
LOGIN: ログイン LOGIN: ログイン
NOTEPAGESECURED: 'このページはセキュリティで保護されております証明書キーを下記に入力してください。こちらからすぐに送信します' NOTEPAGESECURED: このページはセキュリティで保護されております証明書キーを下記に入力してください。こちらからすぐに送信します
NOTERESETLINKINVALID: '<p>パスワードのリセットリンクは有効でないか期限切れです。</p><p> 新しいパスワードを要求することができます <a href="{link1}"> ここ </a> もしくはパスワードを変更することができます <a href="{link2}"> ログインした後 </a>.</p>' NOTERESETLINKINVALID: '<p>パスワードのリセットリンクは有効でないか期限切れです。</p><p> 新しいパスワードを要求することができます <a href="{link1}"> ここ </a> もしくはパスワードを変更することができます <a href="{link2}"> ログインした後 </a>.</p>'
NOTERESETPASSWORD: 'メールアドレスを入力してください、パスワードをリセットするURLを送信致します' NOTERESETPASSWORD: メールアドレスを入力してください、パスワードをリセットするURLを送信致します
PASSWORDSENTHEADER: 'パスワードリセットリンクは ''{email}'' に送信されました' PASSWORDSENTHEADER: 'パスワードリセットリンクは ''{email}'' に送信されました'
PASSWORDSENTTEXT: 'ありがとうございました! リセットリンクは、''{email}'' に、このアカウントが存在することを前提として送信されました。' PASSWORDSENTTEXT: 'ありがとうございました! リセットリンクは、''{email}'' に、このアカウントが存在することを前提として送信されました。'

View File

@ -7,8 +7,8 @@ km:
IsNullLabel: ទទេ IsNullLabel: ទទេ
SilverStripe\Security\Group: SilverStripe\Security\Group:
Code: លេខកូដក្រុម Code: លេខកូដក្រុម
Locked: 'មិនអាចប្រើ' Locked: មិនអាចប្រើ
Parent: 'ចំណាត់ក្រុមដើម' Parent: ចំណាត់ក្រុមដើម
has_many_Permissions: ការអនុញ្ញាតិ្ត has_many_Permissions: ការអនុញ្ញាតិ្ត
many_many_Members: សមាជិក many_many_Members: សមាជិក
SilverStripe\Security\Member: SilverStripe\Security\Member:
@ -16,8 +16,8 @@ km:
INTERFACELANG: ភាសាប្រើសំរាប់ទំព័រមុខ INTERFACELANG: ភាសាប្រើសំរាប់ទំព័រមុខ
SUBJECTPASSWORDCHANGED: ពាក្យសំងាត់របស់អ្នកបានផ្លាស់ប្តូរ SUBJECTPASSWORDCHANGED: ពាក្យសំងាត់របស់អ្នកបានផ្លាស់ប្តូរ
SUBJECTPASSWORDRESET: លីងសំរាប់ប្តូរពាក្យសំងាត់របស់អ្នក SUBJECTPASSWORDRESET: លីងសំរាប់ប្តូរពាក្យសំងាត់របស់អ្នក
belongs_many_many_Groups: 'ចំណាត់ក្រុម' belongs_many_many_Groups: ចំណាត់ក្រុម
db_LockedOutUntil: 'ដោះចេញរហូតដល់' db_LockedOutUntil: ដោះចេញរហូតដល់
db_PasswordExpiry: 'កាលបរិច្ឆេទផុតកំណត់ពាក្យសំងាត់' db_PasswordExpiry: កាលបរិច្ឆេទផុតកំណត់ពាក្យសំងាត់
SilverStripe\Security\Security: SilverStripe\Security\Security:
ALREADYLOGGEDIN: 'អ្នកមិនអាចមើលទំព័រនេះបានទេ។ សូមប្រើប្រាស់ព័ត៌មានសំរាប់ថ្មី មួយទៀតសំរាប់ចូលមើល។ សូមចូលតាម' ALREADYLOGGEDIN: 'អ្នកមិនអាចមើលទំព័រនេះបានទេ។ សូមប្រើប្រាស់ព័ត៌មានសំរាប់ថ្មី មួយទៀតសំរាប់ចូលមើល។ សូមចូលតាម'

View File

@ -1,7 +1,7 @@
ru: ru:
SilverStripe\AssetAdmin\Forms\UploadField: SilverStripe\AssetAdmin\Forms\UploadField:
Dimensions: 'Размеры' Dimensions: Размеры
EDIT: 'Редактировать' EDIT: Редактировать
EDITINFO: 'Редактировать этот файл' EDITINFO: 'Редактировать этот файл'
REMOVE: Удалить REMOVE: Удалить
SilverStripe\Control\ChangePasswordEmail_ss: SilverStripe\Control\ChangePasswordEmail_ss:
@ -93,9 +93,9 @@ ru:
Delete: Удалить Delete: Удалить
DeletePermissionsFailure: 'Нет прав на удаление' DeletePermissionsFailure: 'Нет прав на удаление'
Deleted: 'Удалено {type} {name}' Deleted: 'Удалено {type} {name}'
Save: 'Сохранить' Save: Сохранить
SilverStripe\Forms\GridField\GridFieldEditButton_ss: SilverStripe\Forms\GridField\GridFieldEditButton_ss:
EDIT: 'Редактировать' EDIT: Редактировать
SilverStripe\Forms\GridField\GridFieldPaginator: SilverStripe\Forms\GridField\GridFieldPaginator:
OF: из OF: из
Page: Страница Page: Страница
@ -203,7 +203,7 @@ ru:
many: '{count} групп' many: '{count} групп'
other: '{count} групп' other: '{count} групп'
Parent: 'Родительская группа' Parent: 'Родительская группа'
ROLES: 'Роли' ROLES: Роли
ROLESDESCRIPTION: 'Роли представляют собой сочетания различных прав доступа, которые могут быть присвоены группам.<br />При необходимости они наследуются от групп более высокого уровня.' ROLESDESCRIPTION: 'Роли представляют собой сочетания различных прав доступа, которые могут быть присвоены группам.<br />При необходимости они наследуются от групп более высокого уровня.'
RolesAddEditLink: 'Добавить/редактировать роли' RolesAddEditLink: 'Добавить/редактировать роли'
SINGULARNAME: Группа SINGULARNAME: Группа
@ -294,7 +294,7 @@ ru:
PERMISSIONS_CATEGORY: 'Роли и права доступа' PERMISSIONS_CATEGORY: 'Роли и права доступа'
PLURALNAME: 'Права доступа' PLURALNAME: 'Права доступа'
PLURALS: PLURALS:
one: 'Разрешение' one: Разрешение
few: '{count} разрешения' few: '{count} разрешения'
many: '{count} разрешений' many: '{count} разрешений'
other: '{count} разрешений' other: '{count} разрешений'
@ -307,13 +307,13 @@ ru:
FromRoleOnGroup: 'перенято из роли "{roletitle}" для группы "{grouptitle}"' FromRoleOnGroup: 'перенято из роли "{roletitle}" для группы "{grouptitle}"'
SilverStripe\Security\PermissionRole: SilverStripe\Security\PermissionRole:
OnlyAdminCanApply: 'Может применяться только администратором' OnlyAdminCanApply: 'Может применяться только администратором'
PLURALNAME: 'Роли' PLURALNAME: Роли
PLURALS: PLURALS:
one: 'Роль' one: Роль
few: '{count} роли' few: '{count} роли'
many: '{count} ролей' many: '{count} ролей'
other: '{count} ролей' other: '{count} ролей'
SINGULARNAME: 'Роль' SINGULARNAME: Роль
Title: Название Title: Название
SilverStripe\Security\PermissionRoleCode: SilverStripe\Security\PermissionRoleCode:
PLURALNAME: 'Коды ролей доступа' PLURALNAME: 'Коды ролей доступа'
@ -340,7 +340,7 @@ ru:
CONFIRMLOGOUT: 'Нажмите кнопку ниже чтобы подтвердить что вы хотите выйти из системы.' CONFIRMLOGOUT: 'Нажмите кнопку ниже чтобы подтвердить что вы хотите выйти из системы.'
ENTERNEWPASSWORD: 'Пожалуйста, введите новый пароль.' ENTERNEWPASSWORD: 'Пожалуйста, введите новый пароль.'
ERRORPASSWORDPERMISSION: 'Вы должны войти в систему, чтобы изменить Ваш пароль!' ERRORPASSWORDPERMISSION: 'Вы должны войти в систему, чтобы изменить Ваш пароль!'
LOGIN: 'Вход' LOGIN: Вход
LOGOUT: Выйти LOGOUT: Выйти
LOSTPASSWORDHEADER: 'Восстановление пароля' LOSTPASSWORDHEADER: 'Восстановление пароля'
NOTEPAGESECURED: 'Эта страница защищена. Пожалуйста, введите свои учетные данные для входа.' NOTEPAGESECURED: 'Эта страница защищена. Пожалуйста, введите свои учетные данные для входа.'

View File

@ -14,7 +14,7 @@ si:
Code: 'කාන්ඩ සංකේතය' Code: 'කාන්ඩ සංකේතය'
Locked: 'අගුලුලාද?' Locked: 'අගුලුලාද?'
Parent: 'මවු කාන්ඩය' Parent: 'මවු කාන්ඩය'
has_many_Permissions: 'අවසර' has_many_Permissions: අවසර
many_many_Members: සාමාජිකයින් many_many_Members: සාමාජිකයින්
SilverStripe\Security\Member: SilverStripe\Security\Member:
BUTTONCHANGEPASSWORD: 'මුර පදය අලුත් කරන්න' BUTTONCHANGEPASSWORD: 'මුර පදය අලුත් කරන්න'

View File

@ -37,7 +37,7 @@ sr@latin:
PRINTEDBY: Odštampao PRINTEDBY: Odštampao
PlaceHolder: 'Pronađi {type}' PlaceHolder: 'Pronađi {type}'
PlaceHolderWithLabels: 'Pronađi {type} po {name}' PlaceHolderWithLabels: 'Pronađi {type} po {name}'
Print: 'Štampaj' Print: Štampaj
RelationSearch: 'Pretraživanje relacije' RelationSearch: 'Pretraživanje relacije'
ResetFilter: 'Vrati u pređašnje stanje' ResetFilter: 'Vrati u pređašnje stanje'
SilverStripe\Forms\GridField\GridFieldDeleteAction: SilverStripe\Forms\GridField\GridFieldDeleteAction:

View File

@ -37,7 +37,7 @@ sr_RS@latin:
PRINTEDBY: Odštampao PRINTEDBY: Odštampao
PlaceHolder: 'Pronađi {type}' PlaceHolder: 'Pronađi {type}'
PlaceHolderWithLabels: 'Pronađi {type} po {name}' PlaceHolderWithLabels: 'Pronađi {type} po {name}'
Print: 'Štampaj' Print: Štampaj
RelationSearch: 'Pretraživanje relacije' RelationSearch: 'Pretraživanje relacije'
ResetFilter: 'Vrati u pređašnje stanje' ResetFilter: 'Vrati u pređašnje stanje'
SilverStripe\Forms\GridField\GridFieldDeleteAction: SilverStripe\Forms\GridField\GridFieldDeleteAction:

View File

@ -78,7 +78,7 @@ th:
ENTEREMAIL: กรุณากรอกที่อยู่อีเมลเพื่อขอรับลิงก์สำหรับรีเซ็ตรหัสผ่านใหม่ ENTEREMAIL: กรุณากรอกที่อยู่อีเมลเพื่อขอรับลิงก์สำหรับรีเซ็ตรหัสผ่านใหม่
ERRORPASSWORDNOTMATCH: 'รหัสผ่านไม่ตรงกัน กรุณาลองใหม่อีกครั้ง' ERRORPASSWORDNOTMATCH: 'รหัสผ่านไม่ตรงกัน กรุณาลองใหม่อีกครั้ง'
FIRSTNAME: ชื่อจริง FIRSTNAME: ชื่อจริง
INTERFACELANG: 'ภาษาสำหรับหน้าจอติดต่อผู้ใช้' INTERFACELANG: ภาษาสำหรับหน้าจอติดต่อผู้ใช้
NEWPASSWORD: รหัสผ่านใหม่ NEWPASSWORD: รหัสผ่านใหม่
PASSWORD: รหัสผ่าน PASSWORD: รหัสผ่าน
SUBJECTPASSWORDCHANGED: รหัสผ่านได้รับการเปลี่ยนแปลงแล้ว SUBJECTPASSWORDCHANGED: รหัสผ่านได้รับการเปลี่ยนแปลงแล้ว
@ -86,7 +86,7 @@ th:
SURNAME: นามสกุล SURNAME: นามสกุล
YOUROLDPASSWORD: รหัสผ่านเก่าของคุณ YOUROLDPASSWORD: รหัสผ่านเก่าของคุณ
belongs_many_many_Groups: กลุ่ม belongs_many_many_Groups: กลุ่ม
db_Locale: 'ภาษาสำหรับส่วนอินเทอร์เฟซ' db_Locale: ภาษาสำหรับส่วนอินเทอร์เฟซ
db_LockedOutUntil: ออกจากระบบจนกว่า db_LockedOutUntil: ออกจากระบบจนกว่า
db_Password: รหัสผ่าน db_Password: รหัสผ่าน
db_PasswordExpiry: วันที่รหัสผ่านหมดอายุ db_PasswordExpiry: วันที่รหัสผ่านหมดอายุ

View File

@ -6,7 +6,7 @@ zh:
ATLEAST: '密码长度必须至少 {min} 个字符。' ATLEAST: '密码长度必须至少 {min} 个字符。'
BETWEEN: '密码长度必须含 {min} 到 {max} 个字符。' BETWEEN: '密码长度必须含 {min} 到 {max} 个字符。'
MAXIMUM: '密码长度必须至多 {max} 个字符。' MAXIMUM: '密码长度必须至多 {max} 个字符。'
SHOWONCLICKTITLE: '更改密码' SHOWONCLICKTITLE: 更改密码
SilverStripe\Forms\CurrencyField: SilverStripe\Forms\CurrencyField:
CURRENCYSYMBOL: 货币字符 CURRENCYSYMBOL: 货币字符
SilverStripe\Forms\DateField: SilverStripe\Forms\DateField:
@ -19,16 +19,16 @@ zh:
CHOOSE: (选择) CHOOSE: (选择)
SOURCE_VALIDATION: '请选择列表内提供的选项。{value}不是一个有效的选项' SOURCE_VALIDATION: '请选择列表内提供的选项。{value}不是一个有效的选项'
SilverStripe\Forms\EmailField: SilverStripe\Forms\EmailField:
VALIDATION: '请输入一个电子邮件地址' VALIDATION: 请输入一个电子邮件地址
SilverStripe\Forms\Form: SilverStripe\Forms\Form:
CSRF_FAILED_MESSAGE: 似乎是一个技术问题。请点击返回按钮,刷新浏览器,然后再试一次。 CSRF_FAILED_MESSAGE: 似乎是一个技术问题。请点击返回按钮,刷新浏览器,然后再试一次。
VALIDATIONPASSWORDSDONTMATCH: '密码不匹配' VALIDATIONPASSWORDSDONTMATCH: 密码不匹配
VALIDATIONPASSWORDSNOTEMPTY: '密码不得为空' VALIDATIONPASSWORDSNOTEMPTY: 密码不得为空
VALIDATIONSTRONGPASSWORD: '密码必须至少有一个数字和一个字母数字字符' VALIDATIONSTRONGPASSWORD: 密码必须至少有一个数字和一个字母数字字符
VALIDATOR: 验证器 VALIDATOR: 验证器
VALIDCURRENCY: '请输入一个有效的货币' VALIDCURRENCY: 请输入一个有效的货币
SilverStripe\Forms\FormField: SilverStripe\Forms\FormField:
NONE: '无' NONE:
SilverStripe\Forms\GridField\GridField: SilverStripe\Forms\GridField\GridField:
Add: '添加 {name}' Add: '添加 {name}'
CSVEXPORT: '导出到 CSV' CSVEXPORT: '导出到 CSV'
@ -43,19 +43,19 @@ zh:
PlaceHolder: '查找 {type}' PlaceHolder: '查找 {type}'
PlaceHolderWithLabels: '通过 {name} 查找 {type}' PlaceHolderWithLabels: '通过 {name} 查找 {type}'
Print: 打印 Print: 打印
RelationSearch: '关系搜索' RelationSearch: 关系搜索
ResetFilter: 重设 ResetFilter: 重设
SilverStripe\Forms\GridField\GridFieldDeleteAction: SilverStripe\Forms\GridField\GridFieldDeleteAction:
DELETE_DESCRIPTION: '删除' DELETE_DESCRIPTION: 删除
Delete: '删除' Delete: 删除
DeletePermissionsFailure: '没有删除权限' DeletePermissionsFailure: 没有删除权限
EditPermissionsFailure: 没有解除记录链接的权限 EditPermissionsFailure: 没有解除记录链接的权限
UnlinkRelation: 解除链接 UnlinkRelation: 解除链接
SilverStripe\Forms\GridField\GridFieldDetailForm: SilverStripe\Forms\GridField\GridFieldDetailForm:
CancelBtn: 取消 CancelBtn: 取消
Create: 创建 Create: 创建
Delete: '删除' Delete: 删除
DeletePermissionsFailure: '没有删除权限' DeletePermissionsFailure: 没有删除权限
Deleted: '已删除的 {type} {name}' Deleted: '已删除的 {type} {name}'
Save: 保存 Save: 保存
SilverStripe\Forms\GridField\GridFieldEditButton_ss: SilverStripe\Forms\GridField\GridFieldEditButton_ss:
@ -82,7 +82,7 @@ zh:
SilverStripe\ORM\Hierarchy\Hierarchy: SilverStripe\ORM\Hierarchy\Hierarchy:
InfiniteLoopNotAllowed: '"{type}" 层次结构中发现无限循环。请更改父类型来解决此问题' InfiniteLoopNotAllowed: '"{type}" 层次结构中发现无限循环。请更改父类型来解决此问题'
SilverStripe\Security\BasicAuth: SilverStripe\Security\BasicAuth:
ENTERINFO: '请输入一个用户名和密码。' ENTERINFO: 请输入一个用户名和密码。
ERRORNOTADMIN: 那个用户不是一名管理员。 ERRORNOTADMIN: 那个用户不是一名管理员。
ERRORNOTREC: '那个用户名 / 密码无法被辨认' ERRORNOTREC: '那个用户名 / 密码无法被辨认'
SilverStripe\Security\CMSMemberLoginForm: SilverStripe\Security\CMSMemberLoginForm:
@ -92,12 +92,12 @@ zh:
SUCCESS: 成功 SUCCESS: 成功
SUCCESSCONTENT: '<p>登录成功。如果您没有自动重定向<a target="_top" href="{link}">点击此处</a></p>' SUCCESSCONTENT: '<p>登录成功。如果您没有自动重定向<a target="_top" href="{link}">点击此处</a></p>'
SilverStripe\Security\Group: SilverStripe\Security\Group:
AddRole: '在这个小组中添加一个角色' AddRole: 在这个小组中添加一个角色
Code: '小组代码' Code: 小组代码
DefaultGroupTitleAdministrators: 管理员 DefaultGroupTitleAdministrators: 管理员
DefaultGroupTitleContentAuthors: '内容作者' DefaultGroupTitleContentAuthors: 内容作者
Description: 描述 Description: 描述
GroupReminder: '如果您选择了某父组,该组别将会承担起所有功能角色' GroupReminder: 如果您选择了某父组,该组别将会承担起所有功能角色
HierarchyPermsError: '无法为父组 "{group}" 分配特权权限(要求具备 ADMIN 访问)' HierarchyPermsError: '无法为父组 "{group}" 分配特权权限(要求具备 ADMIN 访问)'
Locked: 锁定? Locked: 锁定?
NoRoles: 没有找到角色 NoRoles: 没有找到角色
@ -110,47 +110,47 @@ zh:
IP: 'IP 地址' IP: 'IP 地址'
Status: 状态 Status: 状态
SilverStripe\Security\Member: SilverStripe\Security\Member:
ADDGROUP: '添加组别' ADDGROUP: 添加组别
BUTTONCHANGEPASSWORD: '更改密码' BUTTONCHANGEPASSWORD: 更改密码
BUTTONLOGIN: 登录 BUTTONLOGIN: 登录
BUTTONLOGINOTHER: 以不同身份登录 BUTTONLOGINOTHER: 以不同身份登录
BUTTONLOSTPASSWORD: '我丢失了密码' BUTTONLOSTPASSWORD: 我丢失了密码
CONFIRMNEWPASSWORD: '确认新密码' CONFIRMNEWPASSWORD: 确认新密码
CONFIRMPASSWORD: '确认密码' CONFIRMPASSWORD: 确认密码
EMAIL: 电子邮件 EMAIL: 电子邮件
EMPTYNEWPASSWORD: '新密码不能为空,请重试' EMPTYNEWPASSWORD: 新密码不能为空,请重试
ENTEREMAIL: '请输入电子邮件地址以获取密码重置链接' ENTEREMAIL: 请输入电子邮件地址以获取密码重置链接
ERRORLOCKEDOUT2: '由于登录失败次数过多,您的账户暂时被冻结。请在 {count} 分钟后重试。' ERRORLOCKEDOUT2: '由于登录失败次数过多,您的账户暂时被冻结。请在 {count} 分钟后重试。'
ERRORNEWPASSWORD: '您输入的新密码不同,请重试' ERRORNEWPASSWORD: 您输入的新密码不同,请重试
ERRORPASSWORDNOTMATCH: '您的现有密码不匹配,请重试' ERRORPASSWORDNOTMATCH: 您的现有密码不匹配,请重试
ERRORWRONGCRED: 所提供的资料似乎是不正确的。请再试一次。 ERRORWRONGCRED: 所提供的资料似乎是不正确的。请再试一次。
FIRSTNAME: 名字 FIRSTNAME: 名字
INTERFACELANG: 界面语言 INTERFACELANG: 界面语言
LOGGEDINAS: '您以 {name} 身份登录。' LOGGEDINAS: '您以 {name} 身份登录。'
NEWPASSWORD: '新密码' NEWPASSWORD: 新密码
PASSWORD: '密码' PASSWORD: 密码
PASSWORDEXPIRED: '您的密码已过期。 请选择一个新的。' PASSWORDEXPIRED: '您的密码已过期。 请选择一个新的。'
SUBJECTPASSWORDCHANGED: '您的密码已更改' SUBJECTPASSWORDCHANGED: 您的密码已更改
SUBJECTPASSWORDRESET: '您的密码重设链接' SUBJECTPASSWORDRESET: 您的密码重设链接
SURNAME: 姓氏 SURNAME: 姓氏
ValidationIdentifierFailed: '不能用相同的标识符 ({name} = {value})) 重写现有成员 #{id}' ValidationIdentifierFailed: '不能用相同的标识符 ({name} = {value})) 重写现有成员 #{id}'
WELCOMEBACK: '欢迎回来, {firstname}' WELCOMEBACK: '欢迎回来, {firstname}'
YOUROLDPASSWORD: '您的旧密码' YOUROLDPASSWORD: 您的旧密码
belongs_many_many_Groups: 群组 belongs_many_many_Groups: 群组
db_Locale: 界面区域设置 db_Locale: 界面区域设置
db_LockedOutUntil: 保持锁定直到 db_LockedOutUntil: 保持锁定直到
db_Password: '密码' db_Password: 密码
db_PasswordExpiry: '密码失效日期' db_PasswordExpiry: 密码失效日期
SilverStripe\Security\PasswordValidator: SilverStripe\Security\PasswordValidator:
LOWCHARSTRENGTH: '请添加下列部分字符以提升密码强度:{chars}' LOWCHARSTRENGTH: '请添加下列部分字符以提升密码强度:{chars}'
PREVPASSWORD: '您已经使用过这个密码,请选用新的密码' PREVPASSWORD: 您已经使用过这个密码,请选用新的密码
TOOSHORT: '密码长度过短,必须为 {minimum} 个字符或更长' TOOSHORT: '密码长度过短,必须为 {minimum} 个字符或更长'
SilverStripe\Security\Permission: SilverStripe\Security\Permission:
AdminGroup: 管理员 AdminGroup: 管理员
CONTENT_CATEGORY: '内容权限' CONTENT_CATEGORY: 内容权限
FULLADMINRIGHTS: '完全管理权' FULLADMINRIGHTS: 完全管理权
FULLADMINRIGHTS_HELP: '包含并支配其它所有已分配权限' FULLADMINRIGHTS_HELP: 包含并支配其它所有已分配权限
UserPermissionsIntro: '把群组分配给该用户会改变其权限。请查看群组部分以获取关于个体组别的权限详情。' UserPermissionsIntro: 把群组分配给该用户会改变其权限。请查看群组部分以获取关于个体组别的权限详情。
SilverStripe\Security\PermissionCheckboxSetField: SilverStripe\Security\PermissionCheckboxSetField:
AssignedTo: '已分配至 "{title}"' AssignedTo: '已分配至 "{title}"'
FromGroup: '从小组 "{title}"继承' FromGroup: '从小组 "{title}"继承'
@ -158,20 +158,20 @@ zh:
FromRoleOnGroup: '从 "{roletitle}" 小组的 "{grouptitle}" 角色继承' FromRoleOnGroup: '从 "{roletitle}" 小组的 "{grouptitle}" 角色继承'
SilverStripe\Security\PermissionRole: SilverStripe\Security\PermissionRole:
OnlyAdminCanApply: 只有管理员可以应用 OnlyAdminCanApply: 只有管理员可以应用
Title: '标题' Title: 标题
SilverStripe\Security\PermissionRoleCode: SilverStripe\Security\PermissionRoleCode:
PermsError: '无法为代码 "{code}"分配特权权限(要求具备 ADMIN 访问)' PermsError: '无法为代码 "{code}"分配特权权限(要求具备 ADMIN 访问)'
SilverStripe\Security\Security: SilverStripe\Security\Security:
ALREADYLOGGEDIN: '您无法进入这个页面。如果您有另一个帐号可以进入这个页面,您可以在下面再次登录。' ALREADYLOGGEDIN: 您无法进入这个页面。如果您有另一个帐号可以进入这个页面,您可以在下面再次登录。
BUTTONSEND: '将密码重设链接发送给我' BUTTONSEND: 将密码重设链接发送给我
CHANGEPASSWORDBELOW: '您可以在下面更改您的密码。' CHANGEPASSWORDBELOW: 您可以在下面更改您的密码。
CHANGEPASSWORDHEADER: '更改您的密码' CHANGEPASSWORDHEADER: 更改您的密码
ENTERNEWPASSWORD: '请输入一个新密码。' ENTERNEWPASSWORD: 请输入一个新密码。
ERRORPASSWORDPERMISSION: '您必须登录才能更改您的密码!' ERRORPASSWORDPERMISSION: 您必须登录才能更改您的密码!
LOGIN: 登录 LOGIN: 登录
LOSTPASSWORDHEADER: '忘记密码' LOSTPASSWORDHEADER: 忘记密码
NOTEPAGESECURED: '该页面受安全保护。请在下面输入您的证书,然后我们会立即将您引导至该页面。' NOTEPAGESECURED: 该页面受安全保护。请在下面输入您的证书,然后我们会立即将您引导至该页面。
NOTERESETLINKINVALID: '<p>密码重设链接无效或已过期。</p><p>您可以在<a href="{link1}">这里</a> 要求一个新的或在<a href="{link2}">登录</a>后更改您的密码。</p>' NOTERESETLINKINVALID: '<p>密码重设链接无效或已过期。</p><p>您可以在<a href="{link1}">这里</a> 要求一个新的或在<a href="{link2}">登录</a>后更改您的密码。</p>'
NOTERESETPASSWORD: '请输入您的电子邮件地址,然后我们会将一个链接发送给您,您可以用它来重设您的密码' NOTERESETPASSWORD: 请输入您的电子邮件地址,然后我们会将一个链接发送给您,您可以用它来重设您的密码
PASSWORDSENTHEADER: '密码重设链接已发送至''{email}''' PASSWORDSENTHEADER: '密码重设链接已发送至''{email}'''
PASSWORDSENTTEXT: '谢谢!复位链接已发送到 ''{email}'',假定此电子邮件地址存在一个帐户。' PASSWORDSENTTEXT: '谢谢!复位链接已发送到 ''{email}'',假定此电子邮件地址存在一个帐户。'

View File

@ -1,48 +1,48 @@
zh_CN: zh_CN:
SilverStripe\Forms\ConfirmedPasswordField: SilverStripe\Forms\ConfirmedPasswordField:
SHOWONCLICKTITLE: '更改密码' SHOWONCLICKTITLE: 更改密码
SilverStripe\Forms\DropdownField: SilverStripe\Forms\DropdownField:
CHOOSE: (选择) CHOOSE: (选择)
SilverStripe\Forms\Form: SilverStripe\Forms\Form:
VALIDATIONPASSWORDSDONTMATCH: '(密码相互不匹配)' VALIDATIONPASSWORDSDONTMATCH: (密码相互不匹配)
VALIDATIONPASSWORDSNOTEMPTY: '密码不能空白' VALIDATIONPASSWORDSNOTEMPTY: 密码不能空白
SilverStripe\Security\BasicAuth: SilverStripe\Security\BasicAuth:
ENTERINFO: '请输入用户名和密码' ENTERINFO: 请输入用户名和密码
ERRORNOTADMIN: 此用户没有管理员权限。 ERRORNOTADMIN: 此用户没有管理员权限。
ERRORNOTREC: '没有找到此用户名/密码' ERRORNOTREC: 没有找到此用户名/密码
SilverStripe\Security\Group: SilverStripe\Security\Group:
Code: '团队代码' Code: 团队代码
Locked: 锁定? Locked: 锁定?
Parent: 主团队 Parent: 主团队
has_many_Permissions: 权限 has_many_Permissions: 权限
many_many_Members: 成员 many_many_Members: 成员
SilverStripe\Security\Member: SilverStripe\Security\Member:
BUTTONCHANGEPASSWORD: '更改密码' BUTTONCHANGEPASSWORD: 更改密码
BUTTONLOGIN: 登录 BUTTONLOGIN: 登录
BUTTONLOGINOTHER: '使用其他帐户登录' BUTTONLOGINOTHER: 使用其他帐户登录
BUTTONLOSTPASSWORD: '忘记密码' BUTTONLOSTPASSWORD: 忘记密码
CONFIRMNEWPASSWORD: '确认新密码' CONFIRMNEWPASSWORD: 确认新密码
CONFIRMPASSWORD: '确认密码' CONFIRMPASSWORD: 确认密码
EMAIL: 电子邮件 EMAIL: 电子邮件
ERRORNEWPASSWORD: '您输入了一个不同的新密码,请重新输入' ERRORNEWPASSWORD: 您输入了一个不同的新密码,请重新输入
ERRORPASSWORDNOTMATCH: '您当前的密码不正确,请再次输入' ERRORPASSWORDNOTMATCH: 您当前的密码不正确,请再次输入
FIRSTNAME: FIRSTNAME:
INTERFACELANG: 界面语言 INTERFACELANG: 界面语言
NEWPASSWORD: '新密码' NEWPASSWORD: 新密码
PASSWORD: '密码' PASSWORD: 密码
SUBJECTPASSWORDCHANGED: '您的密码已更改' SUBJECTPASSWORDCHANGED: 您的密码已更改
SUBJECTPASSWORDRESET: '重设您的密码链接' SUBJECTPASSWORDRESET: 重设您的密码链接
SURNAME: SURNAME:
YOUROLDPASSWORD: '您的旧密码' YOUROLDPASSWORD: 您的旧密码
belongs_many_many_Groups: 团队 belongs_many_many_Groups: 团队
db_LockedOutUntil: 禁止直至 db_LockedOutUntil: 禁止直至
db_PasswordExpiry: '密码过期日期' db_PasswordExpiry: 密码过期日期
SilverStripe\Security\Security: SilverStripe\Security\Security:
ALREADYLOGGEDIN: '您无访问此页的权限。如果您拥有另一个可访问次页的帐户,请在下面登录。' ALREADYLOGGEDIN: 您无访问此页的权限。如果您拥有另一个可访问次页的帐户,请在下面登录。
BUTTONSEND: '给我发送密码重设链接' BUTTONSEND: 给我发送密码重设链接
CHANGEPASSWORDBELOW: '您可在下面更改您的密码' CHANGEPASSWORDBELOW: 您可在下面更改您的密码
CHANGEPASSWORDHEADER: '更改您的密码' CHANGEPASSWORDHEADER: 更改您的密码
ENTERNEWPASSWORD: '请输入新密码' ENTERNEWPASSWORD: 请输入新密码
ERRORPASSWORDPERMISSION: '您必需登录以更改您的密码' ERRORPASSWORDPERMISSION: 您必需登录以更改您的密码
NOTEPAGESECURED: '此页是受安全保护的。输入您的登录信息,我们会将您送达。' NOTEPAGESECURED: 此页是受安全保护的。输入您的登录信息,我们会将您送达。
NOTERESETPASSWORD: '输入您的电邮地址,我们会给您发送一个您可重设密码的链接' NOTERESETPASSWORD: 输入您的电邮地址,我们会给您发送一个您可重设密码的链接

View File

@ -2,21 +2,21 @@ zh_TW:
SilverStripe\Forms\DropdownField: SilverStripe\Forms\DropdownField:
CHOOSE: (選擇) CHOOSE: (選擇)
SilverStripe\Forms\Form: SilverStripe\Forms\Form:
VALIDATIONPASSWORDSDONTMATCH: '密碼不相配' VALIDATIONPASSWORDSDONTMATCH: 密碼不相配
VALIDATIONPASSWORDSNOTEMPTY: 密碼不能是空的 VALIDATIONPASSWORDSNOTEMPTY: 密碼不能是空的
SilverStripe\Security\BasicAuth: SilverStripe\Security\BasicAuth:
ENTERINFO: '請輸入帳號密碼。' ENTERINFO: 請輸入帳號密碼。
ERRORNOTADMIN: '那個使用者不是管理員。' ERRORNOTADMIN: 那個使用者不是管理員。
ERRORNOTREC: 那組帳號密碼不對。 ERRORNOTREC: 那組帳號密碼不對。
SilverStripe\Security\Member: SilverStripe\Security\Member:
BUTTONCHANGEPASSWORD: 更改密碼 BUTTONCHANGEPASSWORD: 更改密碼
BUTTONLOGIN: '登入' BUTTONLOGIN: 登入
BUTTONLOGINOTHER: '用別的帳戶登入' BUTTONLOGINOTHER: 用別的帳戶登入
BUTTONLOSTPASSWORD: 忘記密碼 BUTTONLOSTPASSWORD: 忘記密碼
CONFIRMNEWPASSWORD: 確認新密碼 CONFIRMNEWPASSWORD: 確認新密碼
CONFIRMPASSWORD: 確認密碼 CONFIRMPASSWORD: 確認密碼
EMAIL: 電子郵件 EMAIL: 電子郵件
ERRORNEWPASSWORD: '新密碼不相配,請再試一次。' ERRORNEWPASSWORD: 新密碼不相配,請再試一次。
ERRORPASSWORDNOTMATCH: 舊密碼不對,請再試一次。 ERRORPASSWORDNOTMATCH: 舊密碼不對,請再試一次。
FIRSTNAME: FIRSTNAME:
INTERFACELANG: 介面語言 INTERFACELANG: 介面語言
@ -27,11 +27,11 @@ zh_TW:
SURNAME: SURNAME:
YOUROLDPASSWORD: 舊密碼 YOUROLDPASSWORD: 舊密碼
SilverStripe\Security\Security: SilverStripe\Security\Security:
ALREADYLOGGEDIN: '你不能瀏覽此頁。請用別的帳戶登入。' ALREADYLOGGEDIN: 你不能瀏覽此頁。請用別的帳戶登入。
BUTTONSEND: 寄給我密碼重設網址。 BUTTONSEND: 寄給我密碼重設網址。
CHANGEPASSWORDBELOW: 請在下面更改密碼。 CHANGEPASSWORDBELOW: 請在下面更改密碼。
CHANGEPASSWORDHEADER: 更改密碼 CHANGEPASSWORDHEADER: 更改密碼
ENTERNEWPASSWORD: '請輸入新的密碼。' ENTERNEWPASSWORD: 請輸入新的密碼。
ERRORPASSWORDPERMISSION: '你必須先登入才能改密碼!' ERRORPASSWORDPERMISSION: 你必須先登入才能改密碼!
NOTEPAGESECURED: '那的網頁是被保護的。請先登入。' NOTEPAGESECURED: 那的網頁是被保護的。請先登入。
NOTERESETPASSWORD: '請輸入您的電子郵件。我們將寄給你重設密媽的網址。' NOTERESETPASSWORD: 請輸入您的電子郵件。我們將寄給你重設密媽的網址。

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Middleware\CanonicalURLMiddleware;
use SilverStripe\Control\Middleware\HTTPMiddlewareAware; use SilverStripe\Control\Middleware\HTTPMiddlewareAware;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Environment; use SilverStripe\Core\Environment;
@ -242,7 +243,7 @@ class Director implements TemplateGlobalProvider
// If a port is mentioned in the absolute URL, be sure to add that into the HTTP host // If a port is mentioned in the absolute URL, be sure to add that into the HTTP host
$newVars['_SERVER']['HTTP_HOST'] = isset($bits['port']) $newVars['_SERVER']['HTTP_HOST'] = isset($bits['port'])
? $bits['host'].':'.$bits['port'] ? $bits['host'] . ':' . $bits['port']
: $bits['host']; : $bits['host'];
} }
@ -595,53 +596,34 @@ class Director implements TemplateGlobalProvider
* Turns an absolute URL or folder into one that's relative to the root of the site. This is useful * Turns an absolute URL or folder into one that's relative to the root of the site. This is useful
* when turning a URL into a filesystem reference, or vice versa. * when turning a URL into a filesystem reference, or vice versa.
* *
* @param string $url Accepts both a URL or a filesystem path. * Note: You should check {@link Director::is_site_url()} if making an untrusted url relative prior
* to calling this function.
* *
* @param string $url Accepts both a URL or a filesystem path.
* @return string * @return string
*/ */
public static function makeRelative($url) public static function makeRelative($url)
{ {
// Allow for the accidental inclusion whitespace and // in the URL // Allow for the accidental inclusion whitespace and // in the URL
$url = trim(preg_replace('#([^:])//#', '\\1/', $url)); $url = preg_replace('#([^:])//#', '\\1/', trim($url));
$base1 = self::absoluteBaseURL(); // If using a real url, remove protocol / hostname / auth / port
$baseDomain = substr($base1, strlen(self::protocol())); if (preg_match('#^(?<protocol>https?:)?//(?<hostpart>[^/]*)(?<url>(/.*)?)$#i', $url, $matches)) {
$url = $matches['url'];
}
// Only bother comparing the URL to the absolute version if $url looks like a URL. // Empty case
if (preg_match('/^https?[^:]*:\/\//', $url, $matches)) { if (trim($url, '\\/') === '') {
$urlProtocol = $matches[0]; return '';
$urlWithoutProtocol = substr($url, strlen($urlProtocol)); }
// If we are already looking at baseURL, return '' (substr will return false) // Remove base folder or url
if ($url == $base1) { foreach ([self::baseFolder(), self::baseURL()] as $base) {
return ''; // Ensure single / doesn't break comparison (unless it would make base empty)
} elseif (substr($url, 0, strlen($base1)) == $base1) { $base = rtrim($base, '\\/') ?: $base;
return substr($url, strlen($base1)); if (stripos($url, $base) === 0) {
} elseif (substr($base1, -1) == "/" && $url == substr($base1, 0, -1)) { return ltrim(substr($url, strlen($base)), '\\/');
// Convert http://www.mydomain.com/mysitedir to ''
return "";
} }
if (substr($urlWithoutProtocol, 0, strlen($baseDomain)) == $baseDomain) {
return substr($urlWithoutProtocol, strlen($baseDomain));
}
}
// test for base folder, e.g. /var/www
$base2 = self::baseFolder();
if (substr($url, 0, strlen($base2)) == $base2) {
return substr($url, strlen($base2));
}
// Test for relative base url, e.g. mywebsite/ if the full URL is http://localhost/mywebsite/
$base3 = self::baseURL();
if (substr($url, 0, strlen($base3)) == $base3) {
return substr($url, strlen($base3));
}
// Test for relative base url, e.g mywebsite/ if the full url is localhost/myswebsite
if (substr($url, 0, strlen($baseDomain)) == $baseDomain) {
return substr($url, strlen($baseDomain));
} }
// Nothing matched, fall back to returning the original URL // Nothing matched, fall back to returning the original URL
@ -697,10 +679,10 @@ class Director implements TemplateGlobalProvider
{ {
// Strip off the query and fragment parts of the URL before checking // Strip off the query and fragment parts of the URL before checking
if (($queryPosition = strpos($url, '?')) !== false) { if (($queryPosition = strpos($url, '?')) !== false) {
$url = substr($url, 0, $queryPosition-1); $url = substr($url, 0, $queryPosition - 1);
} }
if (($hashPosition = strpos($url, '#')) !== false) { if (($hashPosition = strpos($url, '#')) !== false) {
$url = substr($url, 0, $hashPosition-1); $url = substr($url, 0, $hashPosition - 1);
} }
$colonPosition = strpos($url, ':'); $colonPosition = strpos($url, ':');
$slashPosition = strpos($url, '/'); $slashPosition = strpos($url, '/');
@ -809,7 +791,7 @@ class Director implements TemplateGlobalProvider
$login = "$_SERVER[PHP_AUTH_USER]:$_SERVER[PHP_AUTH_PW]@"; $login = "$_SERVER[PHP_AUTH_USER]:$_SERVER[PHP_AUTH_PW]@";
} }
return Director::protocol($request) . $login . static::host($request) . Director::baseURL(); return Director::protocol($request) . $login . static::host($request) . Director::baseURL();
} }
/** /**
@ -855,62 +837,29 @@ class Director implements TemplateGlobalProvider
* *
* @param array $patterns Array of regex patterns to match URLs that should be HTTPS. * @param array $patterns Array of regex patterns to match URLs that should be HTTPS.
* @param string $secureDomain Secure domain to redirect to. Defaults to the current domain. * @param string $secureDomain Secure domain to redirect to. Defaults to the current domain.
* @return bool true if already on SSL, false if doesn't match patterns (or cannot redirect) * @param HTTPRequest|null $request Request object to check
* @throws HTTPResponse_Exception Throws exception with redirect, if successful
*/ */
public static function forceSSL($patterns = null, $secureDomain = null) public static function forceSSL($patterns = null, $secureDomain = null, HTTPRequest $request = null)
{ {
// Already on SSL $handler = CanonicalURLMiddleware::singleton()->setForceSSL(true);
if (static::is_https()) {
return true;
}
// Can't redirect without a url
if (!isset($_SERVER['REQUEST_URI'])) {
return false;
}
if ($patterns) { if ($patterns) {
$matched = false; $handler->setForceSSLPatterns($patterns);
$relativeURL = self::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI']));
// protect portions of the site based on the pattern
foreach ($patterns as $pattern) {
if (preg_match($pattern, $relativeURL)) {
$matched = true;
break;
}
}
if (!$matched) {
return false;
}
} }
if ($secureDomain) {
// if an domain is specified, redirect to that instead of the current domain $handler->setForceSSLDomain($secureDomain);
if (!$secureDomain) {
$secureDomain = static::host();
} }
$url = 'https://' . $secureDomain . $_SERVER['REQUEST_URI']; $handler->throwRedirectIfNeeded($request);
// Force redirect
self::force_redirect($url);
return true;
} }
/** /**
* Force a redirect to a domain starting with "www." * Force a redirect to a domain starting with "www."
*
* @param HTTPRequest $request
*/ */
public static function forceWWW() public static function forceWWW(HTTPRequest $request = null)
{ {
if (!Director::isDev() && !Director::isTest() && strpos(static::host(), 'www') !== 0) { $handler = CanonicalURLMiddleware::singleton()->setForceWWW(true);
$destURL = str_replace( $handler->throwRedirectIfNeeded($request);
Director::protocol(),
Director::protocol() . 'www.',
Director::absoluteURL($_SERVER['REQUEST_URI'])
);
self::force_redirect($destURL);
}
} }
/** /**
@ -947,7 +896,7 @@ class Director implements TemplateGlobalProvider
* Can also be checked with {@link Director::isDev()}, {@link Director::isTest()}, and * Can also be checked with {@link Director::isDev()}, {@link Director::isTest()}, and
* {@link Director::isLive()}. * {@link Director::isLive()}.
* *
* @return bool * @return string
*/ */
public static function get_environment_type() public static function get_environment_type()
{ {

View File

@ -0,0 +1,331 @@
<?php
namespace SilverStripe\Control\Middleware;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTP;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\CoreKernel;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;
/**
* Allows events to be registered and passed through middleware.
* Useful for event registered prior to the beginning of a middleware chain.
*/
class CanonicalURLMiddleware implements HTTPMiddleware
{
use Injectable;
/**
* Set if we should redirect to WWW
*
* @var bool
*/
protected $forceWWW = false;
/**
* Set if we should force SSL
*
* @var bool
*/
protected $forceSSL = false;
/**
* Redirect type
*
* @var int
*/
protected $redirectType = 301;
/**
* Environment variables this middleware is enabled in, or a fixed boolean flag to
* apply to all environments
*
* @var array|bool
*/
protected $enabledEnvs = [
CoreKernel::LIVE
];
/**
* If forceSSL is enabled, this is the list of patterns that the url must match (at least one)
*
* @var array Array of regexps to match against relative url
*/
protected $forceSSLPatterns = [];
/**
* SSL Domain to use
*
* @var string
*/
protected $forceSSLDomain = null;
/**
* @return array
*/
public function getForceSSLPatterns()
{
return $this->forceSSLPatterns ?: [];
}
/**
* @param array $forceSSLPatterns
* @return $this
*/
public function setForceSSLPatterns($forceSSLPatterns)
{
$this->forceSSLPatterns = $forceSSLPatterns;
return $this;
}
/**
* @return string
*/
public function getForceSSLDomain()
{
return $this->forceSSLDomain;
}
/**
* @param string $forceSSLDomain
* @return $this
*/
public function setForceSSLDomain($forceSSLDomain)
{
$this->forceSSLDomain = $forceSSLDomain;
return $this;
}
/**
* @return bool
*/
public function getForceWWW()
{
return $this->forceWWW;
}
/**
* @param bool $forceWWW
* @return $this
*/
public function setForceWWW($forceWWW)
{
$this->forceWWW = $forceWWW;
return $this;
}
/**
* @return bool
*/
public function getForceSSL()
{
return $this->forceSSL;
}
/**
* @param bool $forceSSL
* @return $this
*/
public function setForceSSL($forceSSL)
{
$this->forceSSL = $forceSSL;
return $this;
}
/**
* Generate response for the given request
*
* @param HTTPRequest $request
* @param callable $delegate
* @return HTTPResponse
*/
public function process(HTTPRequest $request, callable $delegate)
{
// Handle any redirects
$redirect = $this->getRedirect($request);
if ($redirect) {
return $redirect;
}
return $delegate($request);
}
/**
* Given request object determine if we should redirect.
*
* @param HTTPRequest $request Pre-validated request object
* @return HTTPResponse|null If a redirect is needed return the response
*/
protected function getRedirect(HTTPRequest $request)
{
// Check global disable
if (!$this->isEnabled()) {
return null;
}
// Get properties of current request
$host = $request->getHost();
$scheme = $request->getScheme();
// Check https
if ($this->requiresSSL($request)) {
$scheme = 'https';
// Promote ssl domain if configured
$host = $this->getForceSSLDomain() ?: $host;
}
// Check www.
if ($this->getForceWWW() && strpos($host, 'www.') !== 0) {
$host = "www.{$host}";
}
// No-op if no changes
if ($request->getScheme() === $scheme && $request->getHost() === $host) {
return null;
}
// Rebuild url for request
$url = Controller::join_links("{$scheme}://{$host}", Director::baseURL(), $request->getURL(true));
// Force redirect
$response = new HTTPResponse();
$response->redirect($url, $this->getRedirectType());
HTTP::add_cache_headers($response);
return $response;
}
/**
* Handles redirection to canonical urls outside of the main middleware chain
* using HTTPResponseException.
* Will not do anything if a current HTTPRequest isn't available
*
* @param HTTPRequest|null $request Allow HTTPRequest to be used for the base comparison
* @throws HTTPResponse_Exception
*/
public function throwRedirectIfNeeded(HTTPRequest $request = null)
{
$request = $this->getOrValidateRequest($request);
if (!$request) {
return;
}
$response = $this->getRedirect($request);
if ($response) {
throw new HTTPResponse_Exception($response);
}
}
/**
* Return a valid request, if one is available, or null if none is available
*
* @param HTTPRequest $request
* @return mixed|null
*/
protected function getOrValidateRequest(HTTPRequest $request = null)
{
if ($request instanceof HTTPRequest) {
return $request;
}
if (Injector::inst()->has(HTTPRequest::class)) {
return Injector::inst()->get(HTTPRequest::class);
}
return null;
}
/**
* Check if a redirect for SSL is necessary
*
* @param HTTPRequest $request
* @return bool
*/
protected function requiresSSL(HTTPRequest $request)
{
// Check if force SSL is enabled
if (!$this->getForceSSL()) {
return false;
}
// Already on SSL
if ($request->getScheme() === 'https') {
return false;
}
// Veto if any existing patterns fail
$patterns = $this->getForceSSLPatterns();
if (!$patterns) {
return true;
}
// Filter redirect based on url
$relativeURL = $request->getURL(true);
foreach ($patterns as $pattern) {
if (preg_match($pattern, $relativeURL)) {
return true;
}
}
// No patterns match
return false;
}
/**
* @return int
*/
public function getRedirectType()
{
return $this->redirectType;
}
/**
* @param int $redirectType
* @return $this
*/
public function setRedirectType($redirectType)
{
$this->redirectType = $redirectType;
return $this;
}
/**
* Get enabled flag, or list of environments to enable in
*
* @return array|bool
*/
public function getEnabledEnvs()
{
return $this->enabledEnvs;
}
/**
* @param array|bool $enabledEnvs
* @return $this
*/
public function setEnabledEnvs($enabledEnvs)
{
$this->enabledEnvs = $enabledEnvs;
return $this;
}
/**
* Ensure this middleware is enabled
*/
protected function isEnabled()
{
// At least one redirect must be enabled
if (!$this->getForceWWW() && !$this->getForceSSL()) {
return false;
}
// Filter by env vars
$enabledEnvs = $this->getEnabledEnvs();
if (is_bool($enabledEnvs)) {
return $enabledEnvs;
}
return empty($enabledEnvs) || in_array(Director::get_environment_type(), $enabledEnvs);
}
}

View File

@ -2,7 +2,6 @@
namespace SilverStripe\Core\Startup; namespace SilverStripe\Core\Startup;
use function GuzzleHttp\Psr7\parse_query;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
@ -138,7 +137,7 @@ class ParameterConfirmationToken
// Filter backURL if it contains the given request parameter // Filter backURL if it contains the given request parameter
list(,$query) = explode('?', $backURL); list(,$query) = explode('?', $backURL);
$queryArgs = parse_query($query); parse_str($query, $queryArgs);
$name = $this->getName(); $name = $this->getName();
if (isset($queryArgs[$name])) { if (isset($queryArgs[$name])) {
return $queryArgs[$name]; return $queryArgs[$name];

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Dev; namespace SilverStripe\Dev;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\Module; use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
@ -148,7 +149,7 @@ class Deprecation
if (isset(self::$enabled)) { if (isset(self::$enabled)) {
return self::$enabled; return self::$enabled;
} }
return getenv('SS_DEPRECATION_ENABLED') ?: true; return Environment::getEnv('SS_DEPRECATION_ENABLED') ?: true;
} }
/** /**

View File

@ -119,6 +119,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
if (!$record->canEdit()) { if (!$record->canEdit()) {
return null; return null;
} }
$title = _t(__CLASS__.'.UnlinkRelation', "Unlink");
$field = GridField_FormAction::create( $field = GridField_FormAction::create(
$gridField, $gridField,
@ -128,7 +129,8 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
array('RecordID' => $record->ID) array('RecordID' => $record->ID)
) )
->addExtraClass('btn btn--no-text btn--icon-md font-icon-link-broken grid-field__icon-action gridfield-button-unlink') ->addExtraClass('btn btn--no-text btn--icon-md font-icon-link-broken grid-field__icon-action gridfield-button-unlink')
->setAttribute('title', _t('SilverStripe\\Forms\\GridField\\GridFieldDeleteAction.UnlinkRelation', "Unlink")); ->setAttribute('title', $title)
->setAttribute('aria-label', $title);
} else { } else {
if (!$record->canDelete()) { if (!$record->canDelete()) {
return null; return null;
@ -142,8 +144,8 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
array('RecordID' => $record->ID) array('RecordID' => $record->ID)
) )
->addExtraClass('gridfield-button-delete btn--icon-md font-icon-trash-bin btn--no-text grid-field__icon-action') ->addExtraClass('gridfield-button-delete btn--icon-md font-icon-trash-bin btn--no-text grid-field__icon-action')
->setAttribute('title', _t('SilverStripe\\Forms\\GridField\\GridFieldDeleteAction.Delete', "Delete")) ->setAttribute('title', _t(__CLASS__.'.Delete', "Delete"))
->setDescription(_t('SilverStripe\\Forms\\GridField\\GridFieldDeleteAction.DELETE_DESCRIPTION', 'Delete')); ->setDescription(_t(__CLASS__.'.DELETE_DESCRIPTION', 'Delete'));
} }
return $field->Field(); return $field->Field();
} }
@ -153,8 +155,8 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
* *
* @param GridField $gridField * @param GridField $gridField
* @param string $actionName * @param string $actionName
* @param mixed $arguments * @param array $arguments
* @param array $data - form data * @param array $data Form data
* @throws ValidationException * @throws ValidationException
*/ */
public function handleAction(GridField $gridField, $actionName, $arguments, $data) public function handleAction(GridField $gridField, $actionName, $arguments, $data)
@ -169,7 +171,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
if ($actionName == 'deleterecord') { if ($actionName == 'deleterecord') {
if (!$item->canDelete()) { if (!$item->canDelete()) {
throw new ValidationException( throw new ValidationException(
_t('SilverStripe\\Forms\\GridField\\GridFieldDeleteAction.DeletePermissionsFailure', "No delete permissions") _t(__CLASS__.'.DeletePermissionsFailure', "No delete permissions")
); );
} }
@ -177,7 +179,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
} else { } else {
if (!$item->canEdit()) { if (!$item->canEdit()) {
throw new ValidationException( throw new ValidationException(
_t('SilverStripe\\Forms\\GridField\\GridFieldDeleteAction.EditPermissionsFailure', "No permission to unlink record") _t(__CLASS__.'.EditPermissionsFailure', "No permission to unlink record")
); );
} }

View File

@ -0,0 +1,90 @@
<?php
namespace SilverStripe\Forms\GridField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ValidationException;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
/**
* Adds a delete action for the gridfield to remove a relationship from group.
* This is a special case where it captures whether the current user is the record being removed and
* prevents removal from happening.
*/
class GridFieldGroupDeleteAction extends GridFieldDeleteAction
{
/**
* @var int
*/
protected $groupID;
public function __construct($groupID)
{
$this->groupID = $groupID;
parent::__construct(true);
}
/**
*
* @param GridField $gridField
* @param DataObject $record
* @param string $columnName
* @return string the HTML for the column
*/
public function getColumnContent($gridField, $record, $columnName)
{
if ($this->canUnlink($record)) {
return parent::getColumnContent($gridField, $record, $columnName);
}
return null;
}
/**
* Handle the actions and apply any changes to the GridField
*
* @param GridField $gridField
* @param string $actionName
* @param array $arguments
* @param array $data Form data
* @throws ValidationException
*/
public function handleAction(GridField $gridField, $actionName, $arguments, $data)
{
$record = $gridField->getList()->find('ID', $arguments['RecordID']);
if (!$record || !$actionName == 'unlinkrelation' || $this->canUnlink($record)) {
parent::handleAction($gridField, $actionName, $arguments, $data);
return;
}
throw new ValidationException(
_t(__CLASS__ . '.UnlinkSelfFailure', 'Cannot remove yourself from this group, you will lose admin rights')
);
}
/**
* @param $record - the record of the User to unlink with
* @return bool
*/
protected function canUnlink($record)
{
$currentUser = Security::getCurrentUser();
if ($currentUser
&& $record instanceof Member
&& (int)$record->ID === (int)$currentUser->ID
&& Permission::checkMember($record, 'ADMIN')
) {
$adminGroups = array_intersect(
$record->Groups()->column(),
Permission::get_groups_by_permission('ADMIN')->column()
);
if (count($adminGroups) === 1 && array_search($this->groupID, $adminGroups) !== false) {
return false;
}
}
return true;
}
}

View File

@ -92,10 +92,12 @@ class TinyMCECombinedGenerator implements TinyMCEScriptGenerator, Flushable
foreach ($config->getPlugins() as $plugin => $path) { foreach ($config->getPlugins() as $plugin => $path) {
// Add external plugin // Add external plugin
if ($path) { if ($path) {
// Skip external urls
if (is_string($path) && !Director::is_site_url($path)) {
continue;
}
// Convert URLS to relative paths // Convert URLS to relative paths
if (is_string($path) if (is_string($path)) {
&& (Director::is_absolute_url($path) || strpos($path, '/') === 0)
) {
$path = Director::makeRelative($path); $path = Director::makeRelative($path);
} }
// Ensure file exists // Ensure file exists

View File

@ -225,12 +225,14 @@ class TinyMCEConfig extends HTMLEditorConfig
'priority' => 0, // used for Per-member config override 'priority' => 0, // used for Per-member config override
'browser_spellcheck' => true, 'browser_spellcheck' => true,
'body_class' => 'typography', 'body_class' => 'typography',
'elementpath' => false, // https://www.tinymce.com/docs/configure/editor-appearance/#elementpath 'statusbar' => true,
'elementpath' => true, // https://www.tinymce.com/docs/configure/editor-appearance/#elementpath
'relative_urls' => true, 'relative_urls' => true,
'remove_script_host' => true, 'remove_script_host' => true,
'convert_urls' => false, // Prevent site-root images being rewritten to base relative 'convert_urls' => false, // Prevent site-root images being rewritten to base relative
'menubar' => false, 'menubar' => false,
'language' => 'en', 'language' => 'en',
'branding' => false,
); );
/** /**

View File

@ -2,10 +2,10 @@
namespace SilverStripe\Forms; namespace SilverStripe\Forms;
use SilverStripe\ORM\ArrayLib;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\ORM\ArrayLib;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBField;
/** /**
* Read-only complement of {@link DropdownField}. * Read-only complement of {@link DropdownField}.
@ -36,24 +36,22 @@ class LookupField extends MultiSelectField
$values = $this->getValueArray(); $values = $this->getValueArray();
// Get selected values // Get selected values
$mapped = array(); $mapped = [];
foreach ($values as $value) { foreach ($values as $value) {
if (isset($source[$value])) { if (isset($source[$value])) {
$mapped[] = $source[$value]; $mapped[] = Convert::raw2xml($source[$value]);
} }
} }
// Don't check if string arguments are matching against the source, // Don't check if string arguments are matching against the source,
// as they might be generated HTML diff views instead of the actual values // as they might be generated HTML diff views instead of the actual values
if ($this->value && is_string($this->value) && empty($mapped)) { if ($this->value && is_string($this->value) && empty($mapped)) {
$mapped = array(trim($this->value)); $mapped[] = Convert::raw2xml(trim($this->value));
$values = array(); $values = [];
} }
if ($mapped) { if ($mapped) {
$attrValue = implode(', ', array_values($mapped)); $attrValue = implode(', ', array_values($mapped));
$attrValue = Convert::raw2xml($attrValue);
$inputValue = implode(', ', array_values($values)); $inputValue = implode(', ', array_values($values));
} else { } else {
$attrValue = '<i>('._t('SilverStripe\\Forms\\FormField.NONE', 'none').')</i>'; $attrValue = '<i>('._t('SilverStripe\\Forms\\FormField.NONE', 'none').')</i>';

View File

@ -22,7 +22,9 @@ class TextField extends FormField
* @param string $name * @param string $name
* @param null|string $title * @param null|string $title
* @param string $value * @param string $value
* @param null|int $maxLength * @param null|int $maxLength Max characters to allow for this field. If this value is stored
* against a DB field with a fixed size it's recommended to set an appropriate max length
* matching this size.
* @param null|Form $form * @param null|Form $form
*/ */
public function __construct($name, $title = null, $value = '', $maxLength = null, $form = null) public function __construct($name, $title = null, $value = '', $maxLength = null, $form = null)
@ -40,8 +42,7 @@ class TextField extends FormField
/** /**
* @param int $maxLength * @param int $maxLength
* * @return $this
* @return static
*/ */
public function setMaxLength($maxLength) public function setMaxLength($maxLength)
{ {

View File

@ -2,18 +2,16 @@
namespace SilverStripe\Forms; namespace SilverStripe\Forms;
use Exception;
use InvalidArgumentException;
use SilverStripe\Assets\Folder; use SilverStripe\Assets\Folder;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\Hierarchy\Hierarchy; use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\ORM\Hierarchy\MarkedSet; use SilverStripe\ORM\Hierarchy\MarkedSet;
use SilverStripe\View\ViewableData;
use Exception;
use InvalidArgumentException;
/** /**
* Dropdown-like field that allows you to select an item from a hierarchical * Dropdown-like field that allows you to select an item from a hierarchical
@ -425,39 +423,6 @@ class TreeDropdownField extends FormField
return $this; return $this;
} }
/**
* @param array $properties
* @return string
*/
public function Field($properties = array())
{
$record = $this->Value() ? $this->objectForKey($this->Value()) : null;
if ($record instanceof ViewableData) {
$title = $record->obj($this->getLabelField())->forTemplate();
} elseif ($record) {
$title = Convert::raw2xml($record->{$this->getLabelField()});
} else {
$title = $this->getEmptyString();
}
// TODO Implement for TreeMultiSelectField
$metadata = array(
'id' => $record ? $record->ID : null,
'ClassName' => $record ? $record->ClassName : $this->getSourceObject()
);
$properties = array_merge(
$properties,
array(
'Title' => $title,
'EmptyTitle' => $this->getEmptyString(),
'Metadata' => ($metadata) ? Convert::raw2json($metadata) : null,
)
);
return parent::Field($properties);
}
public function extraClass() public function extraClass()
{ {
return implode(' ', array(parent::extraClass(), ($this->getShowSearch() ? "searchable" : null))); return implode(' ', array(parent::extraClass(), ($this->getShowSearch() ? "searchable" : null)));
@ -633,6 +598,9 @@ class TreeDropdownField extends FormField
} }
/** /**
* HTML-encoded label for this node, including css classes and other markup.
*
* @deprecated 4.0...5.0 Use setTitleField()
* @param string $field * @param string $field
* @return $this * @return $this
*/ */
@ -643,6 +611,9 @@ class TreeDropdownField extends FormField
} }
/** /**
* HTML-encoded label for this node, including css classes and other markup.
*
* @deprecated 4.0...5.0 Use getTitleField()
* @return string * @return string
*/ */
public function getLabelField() public function getLabelField()
@ -651,7 +622,7 @@ class TreeDropdownField extends FormField
} }
/** /**
* Field to use for item titles * Field to use for plain text item titles.
* *
* @return string * @return string
*/ */
@ -795,14 +766,16 @@ class TreeDropdownField extends FormField
$sourceObject = $this->getSourceObject(); $sourceObject = $this->getSourceObject();
$filters = array(); $filters = array();
if (singleton($sourceObject)->hasDatabaseField($this->getLabelField())) { $sourceObjectInstance = DataObject::singleton($sourceObject);
$filters["{$this->getLabelField()}:PartialMatch"] = $this->search; $candidates = array_unique([
} else { $this->getLabelField(),
if (singleton($sourceObject)->hasDatabaseField('Title')) { $this->getTitleField(),
$filters["Title:PartialMatch"] = $this->search; 'Title',
} 'Name'
if (singleton($sourceObject)->hasDatabaseField('Name')) { ]);
$filters["Name:PartialMatch"] = $this->search; foreach ($candidates as $candidate) {
if ($sourceObjectInstance->hasDatabaseField($candidate)) {
$filters["{$candidate}:PartialMatch"] = $this->search;
} }
} }
@ -810,7 +783,7 @@ class TreeDropdownField extends FormField
throw new InvalidArgumentException(sprintf( throw new InvalidArgumentException(sprintf(
'Cannot query by %s.%s, not a valid database column', 'Cannot query by %s.%s, not a valid database column',
$sourceObject, $sourceObject,
$this->getLabelField() $this->getTitleField()
)); ));
} }
return DataObject::get($this->getSourceObject())->filterAny($filters); return DataObject::get($this->getSourceObject())->filterAny($filters);

View File

@ -8,18 +8,15 @@ class TreeDropdownField_Readonly extends TreeDropdownField
public function Field($properties = array()) public function Field($properties = array())
{ {
$fieldName = $this->getLabelField(); $fieldName = $this->getTitleField();
if ($this->value) { if ($this->value) {
$keyObj = $this->objectForKey($this->value); $keyObj = $this->objectForKey($this->value);
$obj = $keyObj ? $keyObj->$fieldName : ''; $title = $keyObj ? $keyObj->$fieldName : '';
} else { } else {
$obj = null; $title = null;
} }
$source = array( $source = [ $this->value => $title ];
$this->value => $obj
);
$field = new LookupField($this->name, $this->title, $source); $field = new LookupField($this->name, $this->title, $source);
$field->setValue($this->value); $field->setValue($this->value);
$field->setForm($this->form); $field->setForm($this->form);

View File

@ -9,27 +9,28 @@ class TreeMultiselectField_Readonly extends TreeMultiselectField
public function Field($properties = array()) public function Field($properties = array())
{ {
$titleArray = $itemIDs = array(); // Build list of titles
$titleList = $itemIDsList = ""; $titleField = $this->getTitleField();
if ($items = $this->getItems()) { $items = $this->getItems();
foreach ($items as $item) { $titleArray = [];
$titleArray[] = $item->Title; foreach ($items as $item) {
} $titleArray[] = $item->$titleField;
foreach ($items as $item) {
$itemIDs[] = $item->ID;
}
if ($titleArray) {
$titleList = implode(", ", $titleArray);
}
if ($itemIDs) {
$itemIDsList = implode(",", $itemIDs);
}
} }
$titleList = implode(", ", $titleArray);
// Build list of values
$itemIDs = [];
foreach ($items as $item) {
$itemIDs[] = $item->ID;
}
$itemIDsList = implode(",", $itemIDs);
// Readonly field for display
$field = new ReadonlyField($this->name . '_ReadonlyValue', $this->title); $field = new ReadonlyField($this->name . '_ReadonlyValue', $this->title);
$field->setValue($titleList); $field->setValue($titleList);
$field->setForm($this->form); $field->setForm($this->form);
// Store values to hidden field
$valueField = new HiddenField($this->name); $valueField = new HiddenField($this->name);
$valueField->setValue($itemIDsList); $valueField->setValue($itemIDsList);
$valueField->setForm($this->form); $valueField->setForm($this->form);

View File

@ -548,9 +548,7 @@ abstract class DBField extends ViewableData implements DBIndexable
*/ */
public function scaffoldFormField($title = null, $params = null) public function scaffoldFormField($title = null, $params = null)
{ {
$field = new TextField($this->name, $title); return TextField::create($this->name, $title);
return $field;
} }
/** /**

View File

@ -124,12 +124,12 @@ class DBHTMLVarchar extends DBVarchar
public function scaffoldFormField($title = null, $params = null) public function scaffoldFormField($title = null, $params = null)
{ {
return new HTMLEditorField($this->name, $title, 1); return HTMLEditorField::create($this->name, $title, 1);
} }
public function scaffoldSearchField($title = null) public function scaffoldSearchField($title = null)
{ {
return new TextField($this->name, $title); return TextField::create($this->name, $title);
} }
public function getSchemaValue() public function getSchemaValue()

View File

@ -2,10 +2,11 @@
namespace SilverStripe\ORM\FieldType; namespace SilverStripe\ORM\FieldType;
use SilverStripe\ORM\DB;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\NullableField; use SilverStripe\Forms\NullableField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\Connect\MySQLDatabase;
use SilverStripe\ORM\DB;
/** /**
* Class Varchar represents a variable-length string of up to 255 characters, designed to store raw text * Class Varchar represents a variable-length string of up to 255 characters, designed to store raw text
@ -22,6 +23,11 @@ class DBVarchar extends DBString
"URL" => "Text", "URL" => "Text",
); );
/**
* Max size of this field
*
* @var int
*/
protected $size; protected $size;
/** /**
@ -58,8 +64,8 @@ class DBVarchar extends DBString
*/ */
public function requireField() public function requireField()
{ {
$charset = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'charset'); $charset = Config::inst()->get(MySQLDatabase::class, 'charset');
$collation = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'collation'); $collation = Config::inst()->get(MySQLDatabase::class, 'collation');
$parts = array( $parts = array(
'datatype'=>'varchar', 'datatype'=>'varchar',
@ -117,12 +123,14 @@ class DBVarchar extends DBString
public function scaffoldFormField($title = null, $params = null) public function scaffoldFormField($title = null, $params = null)
{ {
if (!$this->nullifyEmpty) { // Set field with appropriate size
// Allow the user to select if it's null instead of automatically assuming empty string is $field = TextField::create($this->name, $title);
return new NullableField(new TextField($this->name, $title)); $field->setMaxLength($this->getSize());
} else {
// Automatically determine null (empty string) // Allow the user to select if it's null instead of automatically assuming empty string is
return parent::scaffoldFormField($title); if (!$this->getNullifyEmpty()) {
return NullableField::create($field);
} }
return $field;
} }
} }

View File

@ -11,8 +11,11 @@ use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter; use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
use SilverStripe\Forms\GridField\GridFieldButtonRow; use SilverStripe\Forms\GridField\GridFieldButtonRow;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor; use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
use SilverStripe\Forms\GridField\GridFieldDetailForm; use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\Forms\GridField\GridFieldExportButton; use SilverStripe\Forms\GridField\GridFieldExportButton;
use SilverStripe\Forms\GridField\GridFieldGroupDeleteAction;
use SilverStripe\Forms\GridField\GridFieldPageCount;
use SilverStripe\Forms\GridField\GridFieldPrintButton; use SilverStripe\Forms\GridField\GridFieldPrintButton;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig; use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
@ -150,6 +153,9 @@ class Group extends DataObject
$config->addComponent(new GridFieldButtonRow('after')); $config->addComponent(new GridFieldButtonRow('after'));
$config->addComponents(new GridFieldExportButton('buttons-after-left')); $config->addComponents(new GridFieldExportButton('buttons-after-left'));
$config->addComponents(new GridFieldPrintButton('buttons-after-left')); $config->addComponents(new GridFieldPrintButton('buttons-after-left'));
$config->removeComponentsByType(GridFieldDeleteAction::class);
$config->addComponent(new GridFieldGroupDeleteAction($this->ID), GridFieldPageCount::class);
/** @var GridFieldAddExistingAutocompleter $autocompleter */ /** @var GridFieldAddExistingAutocompleter $autocompleter */
$autocompleter = $config->getComponentByType(GridFieldAddExistingAutocompleter::class); $autocompleter = $config->getComponentByType(GridFieldAddExistingAutocompleter::class);
/** @skipUpgrade */ /** @skipUpgrade */

View File

@ -10,6 +10,7 @@ use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Email; use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\Email\Mailer;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
@ -539,14 +540,30 @@ class Member extends DataObject
{ {
Deprecation::notice( Deprecation::notice(
'5.0.0', '5.0.0',
'This method is deprecated and now does not persist. Please use Security::setCurrentUser(null) or an IdenityStore' 'This method is deprecated and now does not persist. Please use Security::setCurrentUser(null) or an IdentityStore'
); );
$this->extend('beforeMemberLoggedOut');
Injector::inst()->get(IdentityStore::class)->logOut(Controller::curr()->getRequest()); Injector::inst()->get(IdentityStore::class)->logOut(Controller::curr()->getRequest());
// Audit logging hook }
$this->extend('afterMemberLoggedOut');
/**
* Audit logging hook, called before a member is logged out
*
* @param HTTPRequest|null $request
*/
public function beforeMemberLoggedOut(HTTPRequest $request = null)
{
$this->extend('beforeMemberLoggedOut', $request);
}
/**
* Audit logging hook, called after a member is logged out
*
* @param HTTPRequest|null $request
*/
public function afterMemberLoggedOut(HTTPRequest $request = null)
{
$this->extend('afterMemberLoggedOut', $request);
} }
/** /**

View File

@ -101,8 +101,8 @@ class Member_Validator extends RequiredFields
// Only validate identifier field if it's actually set. This could be the case if // Only validate identifier field if it's actually set. This could be the case if
// somebody removes `Email` from the list of required fields. // somebody removes `Email` from the list of required fields.
$id = isset($data['ID']) ? (int)$data['ID'] : 0;
if (isset($data[$identifierField])) { if (isset($data[$identifierField])) {
$id = isset($data['ID']) ? (int)$data['ID'] : 0;
if (!$id && ($ctrl = $this->form->getController())) { if (!$id && ($ctrl = $this->form->getController())) {
// get the record when within GridField (Member editing page in CMS) // get the record when within GridField (Member editing page in CMS)
if ($ctrl instanceof GridFieldDetailForm_ItemRequest && $record = $ctrl->getRecord()) { if ($ctrl instanceof GridFieldDetailForm_ItemRequest && $record = $ctrl->getRecord()) {
@ -137,6 +137,38 @@ class Member_Validator extends RequiredFields
} }
} }
$currentUser = Security::getCurrentUser();
if ($currentUser
&& $id
&& $id === (int)$currentUser->ID
&& Permission::checkMember($currentUser, 'ADMIN')
) {
$stillAdmin = true;
if (!isset($data['DirectGroups'])) {
$stillAdmin = false;
} else {
$adminGroups = array_intersect(
$data['DirectGroups'],
Permission::get_groups_by_permission('ADMIN')->column()
);
if (count($adminGroups) === 0) {
$stillAdmin = false;
}
}
if (!$stillAdmin) {
$this->validationError(
'DirectGroups',
_t(
'SilverStripe\\Security\\Member.VALIDATIONADMINLOSTACCESS',
'Cannot remove all admin groups from your profile'
),
'required'
);
}
}
// Execute the validators on the extensions // Execute the validators on the extensions
$results = $this->extend('updatePHP', $data, $this->form); $results = $this->extend('updatePHP', $data, $this->form);

View File

@ -9,6 +9,7 @@ use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use Traversable;
/** /**
* Shows a categorized list of available permissions (through {@link Permission::get_codes()}). * Shows a categorized list of available permissions (through {@link Permission::get_codes()}).
@ -248,7 +249,7 @@ class PermissionCheckboxSetField extends FormField
Permission::checkMember($record, 'ADMIN') && $code != 'ADMIN') { Permission::checkMember($record, 'ADMIN') && $code != 'ADMIN') {
$icon = 'plus-circled'; $icon = 'plus-circled';
} }
$options .= "<li class=\"$extraClass\">" $options .= "<li class=\"$extraClass\">"
. "<input id=\"$itemID\"$disabled name=\"$this->name[$code]\" type=\"checkbox\"" . "<input id=\"$itemID\"$disabled name=\"$this->name[$code]\" type=\"checkbox\""
. " value=\"$code\"$checked class=\"checkbox\" />" . " value=\"$code\"$checked class=\"checkbox\" />"
@ -298,7 +299,9 @@ class PermissionCheckboxSetField extends FormField
// Remove all "privileged" permissions if the currently logged-in user is not an admin // Remove all "privileged" permissions if the currently logged-in user is not an admin
$privilegedPermissions = Permission::config()->privileged_permissions; $privilegedPermissions = Permission::config()->privileged_permissions;
if (!Permission::check('ADMIN')) { if ((is_array($this->value) || $this->value instanceof Traversable)
&& !Permission::check('ADMIN')
) {
foreach ($this->value as $id => $bool) { foreach ($this->value as $id => $bool) {
if (in_array($id, $privilegedPermissions)) { if (in_array($id, $privilegedPermissions)) {
unset($this->value[$id]); unset($this->value[$id]);
@ -321,7 +324,7 @@ class PermissionCheckboxSetField extends FormField
$record->write(); // We need a record ID to write permissions $record->write(); // We need a record ID to write permissions
} }
if ($this->value) { if (is_array($this->value) || $this->value instanceof Traversable) {
foreach ($this->value as $id => $bool) { foreach ($this->value as $id => $bool) {
if ($bool) { if ($bool) {
$perm = new $managedClass(); $perm = new $managedClass();

View File

@ -74,10 +74,19 @@ class RequestAuthenticationHandler implements AuthenticationHandler
*/ */
public function logOut(HTTPRequest $request = null) public function logOut(HTTPRequest $request = null)
{ {
$member = Security::getCurrentUser();
if ($member) {
$member->beforeMemberLoggedOut($request);
}
foreach ($this->getHandlers() as $handler) { foreach ($this->getHandlers() as $handler) {
$handler->logOut($request); $handler->logOut($request);
} }
Security::setCurrentUser(null); Security::setCurrentUser(null);
if ($member) {
$member->afterMemberLoggedOut($request);
}
} }
} }

View File

@ -14,6 +14,8 @@ use DOMDocument;
* *
* It's designed to allow dependancy injection to replace the standard HTML4 version with one that * It's designed to allow dependancy injection to replace the standard HTML4 version with one that
* handles XHTML or HTML5 instead * handles XHTML or HTML5 instead
*
* @mixin DOMDocument
*/ */
abstract class HTMLValue extends ViewableData abstract class HTMLValue extends ViewableData
{ {

View File

@ -1 +1 @@
<a class="action action-detail view-link" href="$Link">View</a> <a class="grid-field__icon-action font-icon-right-open btn--icon-large action action-detail view-link" href="$Link"><span class="sr-only">View</span></a>

View File

@ -5,18 +5,18 @@ Feature: Lost Password
Using my email Using my email
Background: Background:
Given a "member" "Admin" with "Email"="admin@test.com" Given a "member" "Admin" with "Email"="admin@example.org"
Scenario: I can request a password reset by email Scenario: I can request a password reset by email
Given I go to "Security/login" Given I go to "Security/login"
When I follow "I've lost my password" When I follow "I've lost my password"
And I fill in "admin@test.com" for "Email" And I fill in "admin@example.org" for "Email"
And I press the "Send me the password reset link" button And I press the "Send me the password reset link" button
Then I should see "A reset link has been sent to 'admin@test.com'" Then I should see "A reset link has been sent to 'admin@example.org'"
And there should be an email to "admin@test.com" titled "Your password reset link" And there should be an email to "admin@example.org" titled "Your password reset link"
When I click on the "password reset link" link in the email to "admin@test.com" When I click on the "password reset link" link in the email to "admin@example.org"
Then I should see "Please enter a new password" Then I should see "Please enter a new password"
When I fill in "newpassword" for "New Password" When I fill in "newpassword" for "New Password"
And I fill in "newpassword" for "Confirm New Password" And I fill in "newpassword" for "Confirm New Password"
And I press the "Change Password" button And I press the "Change Password" button
Then the password for "admin@test.com" should be "newpassword" Then the password for "admin@example.org" should be "newpassword"

View File

@ -5,23 +5,41 @@ Feature: Manage users
So that I can control access to the CMS So that I can control access to the CMS
Background: Background:
Given a "member" "ADMIN" belonging to "ADMIN Group" with "Email"="admin@test.com" Given a "member" "ADMIN" belonging to "ADMIN group" with "Email"="admin@example.org"
And a "member" "Staff" belonging to "Staff Group" with "Email"="staffmember@test.com" And the "member" "ADMIN" belonging to "ADMIN group2"
And a "member" "Staff" belonging to "Staff group" with "Email"="staffmember@example.org"
And the "group" "ADMIN group" has permissions "Full administrative rights" And the "group" "ADMIN group" has permissions "Full administrative rights"
And the "group" "ADMIN group2" has permissions "Full administrative rights"
And I am logged in with "ADMIN" permissions And I am logged in with "ADMIN" permissions
And I go to "/admin/security" And I go to "/admin/security"
Scenario: I cannot remove my admin access, but can remove myself from an admin group
When I click the "Groups" CMS tab
And I click "ADMIN group" in the "#Root_Groups" element
And I should see the "Unlink" button in the "Members" gridfield for the "ADMIN" row
Then I click "Groups" in the ".breadcrumbs-wrapper" element
And I click the "Groups" CMS tab
And I click "ADMIN group2" in the "#Root_Groups" element
And I should see the "Unlink" button in the "Members" gridfield for the "ADMIN" row
Then I click the "Unlink" button in the "Members" gridfield for the "ADMIN" row
And I should not see the "Unlink" button in the "Members" gridfield for the "ADMIN" row
Then I click "Groups" in the ".breadcrumbs-wrapper" element
And I click the "Groups" CMS tab
And I click "ADMIN group" in the "#Root_Groups" element
And I should not see the "Unlink" button in the "Members" gridfield for the "ADMIN" row
Scenario: I can list all users regardless of group Scenario: I can list all users regardless of group
When I click the "Users" CMS tab When I click the "Users" CMS tab
Then I should see "admin@test.com" in the "#Root_Users" element Then I should see "admin@example.org" in the "#Root_Users" element
And I should see "staffmember@test.com" in the "#Root_Users" element And I should see "staffmember@example.org" in the "#Root_Users" element
Scenario: I can list all users in a specific group Scenario: I can list all users in a specific group
When I click the "Groups" CMS tab When I click the "Groups" CMS tab
# TODO Please check how performant this is # TODO Please check how performant this is
And I click "ADMIN group" in the "#Root_Groups" element And I click "ADMIN group" in the "#Root_Groups" element
Then I should see "admin@test.com" in the "#Root_Members" element Then I should see "admin@example.org" in the "#Root_Members" element
And I should not see "staffmember@test.com" in the "#Root_Members" element And I should not see "staffmember@example.org" in the "#Root_Members" element
Scenario: I can add a user to the system Scenario: I can add a user to the system
When I click the "Users" CMS tab When I click the "Users" CMS tab
@ -29,16 +47,16 @@ Feature: Manage users
And I fill in the following: And I fill in the following:
| First Name | John | | First Name | John |
| Surname | Doe | | Surname | Doe |
| Email | john.doe@test.com | | Email | john.doe@example.org |
And I press the "Create" button And I press the "Create" button
Then I should see a "Saved member" message Then I should see a "Saved member" message
When I go to "admin/security/" When I go to "admin/security/"
Then I should see "john.doe@test.com" in the "#Root_Users" element Then I should see "john.doe@example.org" in the "#Root_Users" element
Scenario: I can edit an existing user and add him to an existing group Scenario: I can edit an existing user and add him to an existing group
When I click the "Users" CMS tab When I click the "Users" CMS tab
And I click "staffmember@test.com" in the "#Root_Users" element And I click "staffmember@example.org" in the "#Root_Users" element
And I select "ADMIN group" from "Groups" And I select "ADMIN group" from "Groups"
And I press the "Save" button And I press the "Save" button
Then I should see a "Saved Member" message Then I should see a "Saved Member" message
@ -46,11 +64,11 @@ Feature: Manage users
When I go to "admin/security" When I go to "admin/security"
And I click the "Groups" CMS tab And I click the "Groups" CMS tab
And I click "ADMIN group" in the "#Root_Groups" element And I click "ADMIN group" in the "#Root_Groups" element
Then I should see "staffmember@test.com" Then I should see "staffmember@example.org"
Scenario: I can delete an existing user Scenario: I can delete an existing user
When I click the "Users" CMS tab When I click the "Users" CMS tab
And I click "staffmember@test.com" in the "#Root_Users" element And I click "staffmember@example.org" in the "#Root_Users" element
And I press the "Delete" button, confirming the dialog And I press the "Delete" button, confirming the dialog
Then I should see "admin@test.com" Then I should see "admin@example.org"
And I should not see "staffmember@test.com" And I should not see "staffmember@example.org"

View File

@ -1,48 +1,62 @@
@retry @retry
Feature: Manage my own settings Feature: Manage my own settings
As a CMS user As a CMS user
I want to be able to change personal settings I want to be able to change personal settings
In order to streamline my CMS experience In order to streamline my CMS experience
Background: Background:
Given a "member" "Joe" belonging to "Admin Group" with "Email"="joe@test.com" and "Password"="secret" Given a "member" "Joe" belonging to "Admin group" with "Email"="joe@example.org" and "Password"="secret"
And the "group" "Admin Group" has permissions "Full administrative rights" And the "group" "Admin group" has permissions "Full administrative rights"
And I log in with "joe@test.com" and "secret" And the "member" "Joe" belonging to "Admin group2"
And I go to "admin/myprofile" And the "group" "Admin group2" has permissions "Full administrative rights"
And I log in with "joe@example.org" and "secret"
And I go to "admin/myprofile"
Scenario: I can edit my personal details Scenario: I cannot remove all my admin groups
Given I fill in "First Name" with "Jack" When I click the "Admin group" option in the "DirectGroups" listbox
And I fill in "Surname" with "Johnson" And I click the "Admin group2" option in the "DirectGroups" listbox
And I fill in "Email" with "jack@test.com" And I press the "Save" button
When I press the "Save" button Then I should see "Cannot remove all admin groups from your profile" in the "#Form_EditForm" element
Given I go to "admin/myprofile"
Then I should not see "Joe"
Then I should see "Jack"
And I should see "Johnson"
Scenario: I can't reset the password without the original Scenario: I can remove one of my admin groups
Given I follow "Change Password" When I click the "Admin group" option in the "DirectGroups" listbox
And I fill in "Current Password" with "idontknow" And I press the "Save" button
And I fill in "New Password" with "newsecret" Then I should see a "Saved" notice
And I fill in "Confirm Password" with "newsecret" And I should not see "Cannot remove all admin groups from your profile" in the "#Form_EditForm" element
And I press the "Save" button
Then I should see "The current password you have entered is not correct."
Scenario: I can change my password Scenario: I can edit my personal details
Given I follow "Change Password" Given I fill in "First Name" with "Jack"
And I fill in "Current Password" with "secret" And I fill in "Surname" with "Johnson"
And I fill in "New Password" with "newsecret" And I fill in "Email" with "jack@example.org"
And I fill in "Confirm Password" with "newsecret" When I press the "Save" button
And I press the "Save" button Given I go to "admin/myprofile"
And I am not logged in Then I should not see "Joe"
When I log in with "joe@test.com" and "newsecret" Then I should see "Jack"
And I go to "admin/myprofile" And I should see "Johnson"
Then I should see the CMS
Scenario: I can change the interface language Scenario: I can't reset the password without the original
And I select "German (Germany)" from "Interface Language" Given I follow "Change Password"
And I press the "Save" button And I fill in "Current Password" with "idontknow"
Then I should see "Sprache" And I fill in "New Password" with "newsecret"
And I fill in "Confirm Password" with "newsecret"
And I press the "Save" button
Then I should see "The current password you have entered is not correct."
Scenario: I can change my password
Given I follow "Change Password"
And I fill in "Current Password" with "secret"
And I fill in "New Password" with "newsecret"
And I fill in "Confirm Password" with "newsecret"
And I press the "Save" button
And I am not logged in
When I log in with "joe@example.org" and "newsecret"
And I go to "admin/myprofile"
Then I should see the CMS
Scenario: I can change the interface language
And I select "German (Germany)" from "Interface Language"
And I press the "Save" button
Then I should see "Sprache"
# TODO Date/time format - Difficult because its not exposed anywhere in the CMS? # TODO Date/time format - Difficult because its not exposed anywhere in the CMS?
# TODO Group modification as ADMIN user # TODO Group modification as ADMIN user

View File

@ -6,7 +6,7 @@ Feature: Manage Security Permissions for Groups
Background: Background:
Given a "group" "test group" Given a "group" "test group"
And a "member" "ADMIN" belonging to "ADMIN Group" with "Email"="admin@test.com" And a "member" "ADMIN" belonging to "ADMIN group" with "Email"="admin@example.org"
And the "group" "ADMIN group" has permissions "Full administrative rights" And the "group" "ADMIN group" has permissions "Full administrative rights"
And I am logged in with "ADMIN" permissions And I am logged in with "ADMIN" permissions
And I go to "/admin/security" And I go to "/admin/security"

View File

@ -95,7 +95,7 @@ class CmsFormsContext implements Context
{ {
$element = $this->getHtmlField($locator); $element = $this->getHtmlField($locator);
$actual = $element->getValue(); $actual = $element->getValue();
$regex = '/'.preg_quote($html, '/').'/ui'; $regex = '/' . preg_quote($html, '/') . '/ui';
$failed = false; $failed = false;
if (trim($negative)) { if (trim($negative)) {
@ -230,7 +230,7 @@ JS;
*/ */
public function iClickOnTheHtmlFieldButton($button) public function iClickOnTheHtmlFieldButton($button)
{ {
$xpath = "//*[@aria-label='".$button."']"; $xpath = "//*[@aria-label='" . $button . "']";
$session = $this->getSession(); $session = $this->getSession();
$element = $session->getPage()->find('xpath', $xpath); $element = $session->getPage()->find('xpath', $xpath);
if (null === $element) { if (null === $element) {
@ -352,4 +352,87 @@ JS;
// Destroy cookie to detach session // Destroy cookie to detach session
$this->getMainContext()->getSession()->setCookie('PHPSESSID', null); $this->getMainContext()->getSession()->setCookie('PHPSESSID', null);
} }
/**
* @When /^I should see the "([^"]*)" button in the "([^"]*)" gridfield for the "([^"]*)" row$/
* @param string $buttonLabel
* @param string $gridFieldName
* @param string $rowName
*/
public function assertIShouldSeeTheGridFieldButtonForRow($buttonLabel, $gridFieldName, $rowName)
{
$button = $this->getGridFieldButton($gridFieldName, $rowName, $buttonLabel);
assertNotNull($button, sprintf('Button "%s" not found', $buttonLabel));
}
/**
* @When /^I should not see the "([^"]*)" button in the "([^"]*)" gridfield for the "([^"]*)" row$/
* @param string $buttonLabel
* @param string $gridFieldName
* @param string $rowName
*/
public function assertIShouldNotSeeTheGridFieldButtonForRow($buttonLabel, $gridFieldName, $rowName)
{
$button = $this->getGridFieldButton($gridFieldName, $rowName, $buttonLabel);
assertNull($button, sprintf('Button "%s" found', $buttonLabel));
}
/**
* @When /^I click the "([^"]*)" button in the "([^"]*)" gridfield for the "([^"]*)" row$/
* @param string $buttonLabel
* @param string $gridFieldName
* @param string $rowName
*/
public function stepIClickTheGridFieldButtonForRow($buttonLabel, $gridFieldName, $rowName)
{
$button = $this->getGridFieldButton($gridFieldName, $rowName, $buttonLabel);
assertNotNull($button, sprintf('Button "%s" not found', $buttonLabel));
$button->click();
}
/**
* Finds a button in the gridfield row
*
* @param $gridFieldName
* @param $rowName
* @param $buttonLabel
* @return $button
*/
protected function getGridFieldButton($gridFieldName, $rowName, $buttonLabel)
{
$page = $this->getSession()->getPage();
$gridField = $page->find('xpath', sprintf('//*[@data-name="%s"]', $gridFieldName));
assertNotNull($gridField, sprintf('Gridfield "%s" not found', $gridFieldName));
$name = $gridField->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $rowName));
if (!$name) {
return null;
}
$button = $name->getParent()->find('xpath', sprintf('//*[@aria-label="%s"]', $buttonLabel));
return $button;
}
/**
* @When /^I click the "([^"]*)" option in the "([^"]*)" listbox$/
* @param $optionLabel
* @param $fieldName
*/
public function stepIClickTheListBoxOption($optionLabel, $fieldName)
{
$page = $this->getSession()->getPage();
$listBox = $page->find('xpath', sprintf('//*[@name="%s[]"]', $fieldName));
assertNotNull($listBox, sprintf('The listbox %s is not found', $fieldName));
$option = $listBox->getParent()
->find('css', '.chosen-choices')
->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $optionLabel));
assertNotNull($option, sprintf('Option %s is not found', $optionLabel));
$button = $option->getParent()->find('css', 'a');
$button->click();
}
} }

View File

@ -7,7 +7,7 @@ use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPRequestBuilder; use SilverStripe\Control\HTTPRequestBuilder;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\Middleware\HTTPMiddleware; use SilverStripe\Control\Middleware\CanonicalURLMiddleware;
use SilverStripe\Control\Middleware\RequestHandlerMiddlewareAdapter; use SilverStripe\Control\Middleware\RequestHandlerMiddlewareAdapter;
use SilverStripe\Control\Middleware\TrustedProxyMiddleware; use SilverStripe\Control\Middleware\TrustedProxyMiddleware;
use SilverStripe\Control\RequestProcessor; use SilverStripe\Control\RequestProcessor;
@ -30,6 +30,9 @@ class DirectorTest extends SapphireTest
{ {
parent::setUp(); parent::setUp();
Director::config()->set('alternate_base_url', 'http://www.mysite.com/'); Director::config()->set('alternate_base_url', 'http://www.mysite.com/');
// Ensure redirects enabled on all environments
CanonicalURLMiddleware::singleton()->setEnabledEnvs(true);
$this->expectedRedirect = null; $this->expectedRedirect = null;
} }
@ -239,26 +242,132 @@ class DirectorTest extends SapphireTest
$this->assertTrue(Director::is_relative_url('/relative/#=http://test.com')); $this->assertTrue(Director::is_relative_url('/relative/#=http://test.com'));
} }
public function testMakeRelative() /**
* @return array
*/
public function providerMakeRelative()
{ {
$siteUrl = Director::absoluteBaseURL(); return [
$siteUrlNoProtocol = preg_replace('/https?:\/\//', '', $siteUrl); // Resilience to slash position
[
'http://www.mysite.com/base/folder',
'http://www.mysite.com/base/folder',
''
],
[
'http://www.mysite.com/base/folder',
'http://www.mysite.com/base/folder/',
''
],
[
'http://www.mysite.com/base/folder/',
'http://www.mysite.com/base/folder',
''
],
[
'http://www.mysite.com/',
'http://www.mysite.com/',
''
],
[
'http://www.mysite.com/',
'http://www.mysite.com',
''
],
[
'http://www.mysite.com',
'http://www.mysite.com/',
''
],
[
'http://www.mysite.com/base/folder',
'http://www.mysite.com/base/folder/page',
'page'
],
[
'http://www.mysite.com/',
'http://www.mysite.com/page/',
'page/'
],
// Parsing protocol safely
[
'http://www.mysite.com/base/folder',
'https://www.mysite.com/base/folder',
''
],
[
'https://www.mysite.com/base/folder',
'http://www.mysite.com/base/folder/testpage',
'testpage'
],
[
'http://www.mysite.com/base/folder',
'//www.mysite.com/base/folder/testpage',
'testpage'
],
// Dirty input
[
'http://www.mysite.com/base/folder',
' https://www.mysite.com/base/folder/testpage ',
'testpage'
],
[
'http://www.mysite.com/base/folder',
'//www.mysite.com/base//folder/testpage//subpage',
'testpage/subpage'
],
// Non-http protocol isn't modified
[
'http://www.mysite.com/base/folder',
'ftp://test.com',
'ftp://test.com'
],
// Alternate hostnames are redirected
[
'https://www.mysite.com/base/folder',
'http://mysite.com/base/folder/testpage',
'testpage'
],
[
'http://www.otherdomain.com/base/folder',
'//www.mysite.com/base/folder/testpage',
'testpage'
],
// Base folder is found
[
'http://www.mysite.com/base/folder',
BASE_PATH . '/some/file.txt',
'some/file.txt',
],
// querystring is protected
[
'http://www.mysite.com/base/folder',
'//www.mysite.com/base//folder/testpage//subpage?args=hello',
'testpage/subpage?args=hello'
],
[
'http://www.mysite.com/base/folder',
'//www.mysite.com/base//folder/?args=hello',
'?args=hello'
],
];
}
$this->assertEquals(Director::makeRelative("$siteUrl"), ''); /**
$this->assertEquals(Director::makeRelative("https://$siteUrlNoProtocol"), ''); * @dataProvider providerMakeRelative
$this->assertEquals(Director::makeRelative("http://$siteUrlNoProtocol"), ''); * @param string $baseURL Site base URL
* @param string $requestURL Request URL
$this->assertEquals(Director::makeRelative(" $siteUrl/testpage "), 'testpage'); * @param string $relativeURL Expected relative URL
$this->assertEquals(Director::makeRelative("$siteUrlNoProtocol/testpage"), 'testpage'); */
public function testMakeRelative($baseURL, $requestURL, $relativeURL)
$this->assertEquals(Director::makeRelative('ftp://test.com'), 'ftp://test.com'); {
$this->assertEquals(Director::makeRelative('http://test.com'), 'http://test.com'); Director::config()->set('alternate_base_url', $baseURL);
$actualRelative = Director::makeRelative($requestURL);
$this->assertEquals(Director::makeRelative('relative'), 'relative'); $this->assertEquals(
$this->assertEquals(Director::makeRelative("$siteUrl/?url=http://test.com"), '?url=http://test.com'); $relativeURL,
$actualRelative,
$this->assertEquals("test", Director::makeRelative("https://".$siteUrlNoProtocol."/test")); "Expected relativeURL of {$requestURL} to be {$relativeURL}"
$this->assertEquals("test", Director::makeRelative("http://".$siteUrlNoProtocol."/test")); );
} }
/** /**
@ -412,43 +521,101 @@ class DirectorTest extends SapphireTest
); );
} }
public function testForceWWW()
{
$this->expectExceptionRedirect('http://www.mysite.com/some-url');
Director::mockRequest(function ($request) {
Injector::inst()->registerService($request, HTTPRequest::class);
Director::forceWWW();
}, 'http://mysite.com/some-url');
}
public function testPromisedForceWWW()
{
Director::forceWWW();
// Flag is set but not redirected yet
$middleware = CanonicalURLMiddleware::singleton();
$this->assertTrue($middleware->getForceWWW());
// Middleware forces the redirection eventually
/** @var HTTPResponse $response */
$response = Director::mockRequest(function ($request) use ($middleware) {
return $middleware->process($request, function ($request) {
return null;
});
}, 'http://mysite.com/some-url');
// Middleware returns non-exception redirect
$this->assertEquals('http://www.mysite.com/some-url', $response->getHeader('Location'));
$this->assertEquals(301, $response->getStatusCode());
}
public function testForceSSLProtectsEntireSite() public function testForceSSLProtectsEntireSite()
{ {
$this->expectExceptionRedirect('https://www.mysite.com/some-url'); $this->expectExceptionRedirect('https://www.mysite.com/some-url');
Director::mockRequest(function () { Director::mockRequest(function ($request) {
Injector::inst()->registerService($request, HTTPRequest::class);
Director::forceSSL(); Director::forceSSL();
}, '/some-url'); }, 'http://www.mysite.com/some-url');
}
public function testPromisedForceSSL()
{
Director::forceSSL();
// Flag is set but not redirected yet
$middleware = CanonicalURLMiddleware::singleton();
$this->assertTrue($middleware->getForceSSL());
// Middleware forces the redirection eventually
/** @var HTTPResponse $response */
$response = Director::mockRequest(function ($request) use ($middleware) {
return $middleware->process($request, function ($request) {
return null;
});
}, 'http://www.mysite.com/some-url');
// Middleware returns non-exception redirect
$this->assertEquals('https://www.mysite.com/some-url', $response->getHeader('Location'));
$this->assertEquals(301, $response->getStatusCode());
} }
public function testForceSSLOnTopLevelPagePattern() public function testForceSSLOnTopLevelPagePattern()
{ {
// Expect admin to trigger redirect // Expect admin to trigger redirect
$this->expectExceptionRedirect('https://www.mysite.com/admin'); $this->expectExceptionRedirect('https://www.mysite.com/admin');
Director::mockRequest(function () { Director::mockRequest(function (HTTPRequest $request) {
Injector::inst()->registerService($request, HTTPRequest::class);
Director::forceSSL(array('/^admin/')); Director::forceSSL(array('/^admin/'));
}, '/admin'); }, 'http://www.mysite.com/admin');
} }
public function testForceSSLOnSubPagesPattern() public function testForceSSLOnSubPagesPattern()
{ {
// Expect to redirect to security login page // Expect to redirect to security login page
$this->expectExceptionRedirect('https://www.mysite.com/Security/login'); $this->expectExceptionRedirect('https://www.mysite.com/Security/login');
Director::mockRequest(function () { Director::mockRequest(function (HTTPRequest $request) {
Injector::inst()->registerService($request, HTTPRequest::class);
Director::forceSSL(array('/^Security/')); Director::forceSSL(array('/^Security/'));
}, '/Security/login'); }, 'http://www.mysite.com/Security/login');
} }
public function testForceSSLWithPatternDoesNotMatchOtherPages() public function testForceSSLWithPatternDoesNotMatchOtherPages()
{ {
// Not on same url should not trigger redirect // Not on same url should not trigger redirect
Director::mockRequest(function () { $response = Director::mockRequest(function (HTTPRequest $request) {
$this->assertFalse(Director::forceSSL(array('/^admin/'))); Injector::inst()->registerService($request, HTTPRequest::class);
}, Director::baseURL() . 'normal-page'); Director::forceSSL(array('/^admin/'));
}, 'http://www.mysite.com/normal-page');
$this->assertNull($response, 'Non-matching patterns do not trigger redirect');
// nested url should not triger redirect either // nested url should not triger redirect either
Director::mockRequest(function () { $response = Director::mockRequest(function (HTTPRequest $request) {
$this->assertFalse(Director::forceSSL(array('/^admin/', '/^Security/'))); Injector::inst()->registerService($request, HTTPRequest::class);
}, Director::baseURL() . 'just-another-page/sub-url'); Director::forceSSL(array('/^admin/', '/^Security/'));
}, 'http://www.mysite.com/just-another-page/sub-url');
$this->assertNull($response, 'Non-matching patterns do not trigger redirect');
} }
public function testForceSSLAlternateDomain() public function testForceSSLAlternateDomain()
@ -456,8 +623,35 @@ class DirectorTest extends SapphireTest
// Ensure that forceSSL throws the appropriate exception // Ensure that forceSSL throws the appropriate exception
$this->expectExceptionRedirect('https://secure.mysite.com/admin'); $this->expectExceptionRedirect('https://secure.mysite.com/admin');
Director::mockRequest(function (HTTPRequest $request) { Director::mockRequest(function (HTTPRequest $request) {
Injector::inst()->registerService($request, HTTPRequest::class);
return Director::forceSSL(array('/^admin/'), 'secure.mysite.com'); return Director::forceSSL(array('/^admin/'), 'secure.mysite.com');
}, Director::baseURL() . 'admin'); }, 'http://www.mysite.com/admin');
}
/**
* Test that combined forceWWW and forceSSL combine safely
*/
public function testForceSSLandForceWWW()
{
Director::forceWWW();
Director::forceSSL();
// Flag is set but not redirected yet
$middleware = CanonicalURLMiddleware::singleton();
$this->assertTrue($middleware->getForceWWW());
$this->assertTrue($middleware->getForceSSL());
// Middleware forces the redirection eventually
/** @var HTTPResponse $response */
$response = Director::mockRequest(function ($request) use ($middleware) {
return $middleware->process($request, function ($request) {
return null;
});
}, 'http://mysite.com/some-url');
// Middleware returns non-exception redirect
$this->assertEquals('https://www.mysite.com/some-url', $response->getHeader('Location'));
$this->assertEquals(301, $response->getStatusCode());
} }
/** /**

View File

@ -19,18 +19,18 @@ class TreeDropdownFieldTest extends SapphireTest
{ {
$field = new TreeDropdownField('TestTree', 'Test tree', Folder::class); $field = new TreeDropdownField('TestTree', 'Test tree', Folder::class);
$folder = $this->objFromFixture(Folder::class, 'folder1-subfolder1'); $folder = $this->objFromFixture(Folder::class, 'folder1-subfolder1');
$schema = $field->getSchemaStateDefaults(); $schema = $field->getSchemaStateDefaults();
$this->assertFalse(isset($schema['data']['valueObject'])); $this->assertFalse(isset($schema['data']['valueObject']));
$field->setValue($folder->ID); $field->setValue($folder->ID);
$schema = $field->getSchemaStateDefaults(); $schema = $field->getSchemaStateDefaults();
$this->assertEquals($folder->ID, $schema['data']['valueObject']['id']); $this->assertEquals($folder->ID, $schema['data']['valueObject']['id']);
$this->assertTrue(isset($schema['data']['valueObject'])); $this->assertTrue(isset($schema['data']['valueObject']));
$this->assertFalse($schema['data']['showSelectedPath']); $this->assertFalse($schema['data']['showSelectedPath']);
$this->assertEquals('', $schema['data']['valueObject']['titlePath']); $this->assertEquals('', $schema['data']['valueObject']['titlePath']);
$field->setShowSelectedPath(true); $field->setShowSelectedPath(true);
$schema = $field->getSchemaStateDefaults(); $schema = $field->getSchemaStateDefaults();
$this->assertTrue($schema['data']['showSelectedPath']); $this->assertTrue($schema['data']['showSelectedPath']);
@ -39,64 +39,64 @@ class TreeDropdownFieldTest extends SapphireTest
$schema['data']['valueObject']['titlePath'] $schema['data']['valueObject']['titlePath']
); );
} }
public function testTreeSearchJson() public function testTreeSearchJson()
{ {
$field = new TreeDropdownField('TestTree', 'Test tree', Folder::class); $field = new TreeDropdownField('TestTree', 'Test tree', Folder::class);
// case insensitive search against keyword 'sub' for folders // case insensitive search against keyword 'sub' for folders
$request = new HTTPRequest('GET', 'url', array('search'=>'sub', 'format' => 'json')); $request = new HTTPRequest('GET', 'url', array('search'=>'sub', 'format' => 'json'));
$request->setSession(new Session([])); $request->setSession(new Session([]));
$response = $field->tree($request); $response = $field->tree($request);
$tree = json_decode($response->getBody(), true); $tree = json_decode($response->getBody(), true);
$folder1 = $this->objFromFixture(Folder::class, 'folder1'); $folder1 = $this->objFromFixture(Folder::class, 'folder1');
$folder1Subfolder1 = $this->objFromFixture(Folder::class, 'folder1-subfolder1'); $folder1Subfolder1 = $this->objFromFixture(Folder::class, 'folder1-subfolder1');
$this->assertContains( $this->assertContains(
$folder1->Name, $folder1->Name,
array_column($tree['children'], 'title'), array_column($tree['children'], 'title'),
$folder1->Name.' is found in the json' $folder1->Name.' is found in the json'
); );
$filtered = array_filter($tree['children'], function ($entry) use ($folder1) { $filtered = array_filter($tree['children'], function ($entry) use ($folder1) {
return $folder1->Name === $entry['title']; return $folder1->Name === $entry['title'];
}); });
$folder1Tree = array_pop($filtered); $folder1Tree = array_pop($filtered);
$this->assertContains( $this->assertContains(
$folder1Subfolder1->Name, $folder1Subfolder1->Name,
array_column($folder1Tree['children'], 'title'), array_column($folder1Tree['children'], 'title'),
$folder1Subfolder1->Name.' is found in the folder1 entry in the json' $folder1Subfolder1->Name.' is found in the folder1 entry in the json'
); );
} }
public function testTreeSearchJsonFlatlist() public function testTreeSearchJsonFlatlist()
{ {
$field = new TreeDropdownField('TestTree', 'Test tree', Folder::class); $field = new TreeDropdownField('TestTree', 'Test tree', Folder::class);
// case insensitive search against keyword 'sub' for folders // case insensitive search against keyword 'sub' for folders
$request = new HTTPRequest('GET', 'url', array('search'=>'sub', 'format' => 'json', 'flatList' => '1')); $request = new HTTPRequest('GET', 'url', array('search'=>'sub', 'format' => 'json', 'flatList' => '1'));
$request->setSession(new Session([])); $request->setSession(new Session([]));
$response = $field->tree($request); $response = $field->tree($request);
$tree = json_decode($response->getBody(), true); $tree = json_decode($response->getBody(), true);
$folder1 = $this->objFromFixture(Folder::class, 'folder1'); $folder1 = $this->objFromFixture(Folder::class, 'folder1');
$folder1Subfolder1 = $this->objFromFixture(Folder::class, 'folder1-subfolder1'); $folder1Subfolder1 = $this->objFromFixture(Folder::class, 'folder1-subfolder1');
$this->assertNotContains( $this->assertNotContains(
$folder1->Name, $folder1->Name,
array_column($tree['children'], 'title'), array_column($tree['children'], 'title'),
$folder1->Name.' is not found in the json' $folder1->Name.' is not found in the json'
); );
$this->assertContains( $this->assertContains(
$folder1Subfolder1->Name, $folder1Subfolder1->Name,
array_column($tree['children'], 'title'), array_column($tree['children'], 'title'),
$folder1Subfolder1->Name.' is found in the json' $folder1Subfolder1->Name.' is found in the json'
); );
} }
public function testTreeSearch() public function testTreeSearch()
{ {
$field = new TreeDropdownField('TestTree', 'Test tree', Folder::class); $field = new TreeDropdownField('TestTree', 'Test tree', Folder::class);
@ -194,4 +194,19 @@ class TreeDropdownFieldTest extends SapphireTest
$file3->Name.' is not found' $file3->Name.' is not found'
); );
} }
public function testReadonly()
{
$field = new TreeDropdownField('TestTree', 'Test tree', File::class);
$asdf = $this->objFromFixture(File::class, 'asdf');
$field->setValue($asdf->ID);
$readonlyField = $field->performReadonlyTransformation();
$this->assertEquals(
<<<"HTML"
<span class="readonly" id="TestTree">&lt;Special &amp; characters&gt;</span><input type="hidden" name="TestTree" value="{$asdf->ID}" />
HTML
,
(string)$readonlyField->Field()
);
}
} }

View File

@ -11,6 +11,7 @@ SilverStripe\Assets\Folder:
SilverStripe\Assets\File: SilverStripe\Assets\File:
asdf: asdf:
Filename: assets/FileTest.txt Filename: assets/FileTest.txt
Title: '<Special & characters>'
subfolderfile1: subfolderfile1:
Filename: assets/FileTest-subfolder/TestFile1InSubfolder.txt Filename: assets/FileTest-subfolder/TestFile1InSubfolder.txt
Name: TestFile1InSubfolder Name: TestFile1InSubfolder

View File

@ -0,0 +1,31 @@
<?php
namespace SilverStripe\Forms\Tests;
use SilverStripe\Assets\File;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\TreeMultiselectField;
class TreeMultiselectFieldTest extends SapphireTest
{
protected static $fixture_file = 'TreeDropdownFieldTest.yml';
public function testReadonly()
{
$field = new TreeMultiselectField('TestTree', 'Test tree', File::class);
$asdf = $this->objFromFixture(File::class, 'asdf');
$subfolderfile1 = $this->objFromFixture(File::class, 'subfolderfile1');
$field->setValue(implode(',', [$asdf->ID, $subfolderfile1->ID]));
$readonlyField = $field->performReadonlyTransformation();
$this->assertEquals(
<<<"HTML"
<span id="TestTree_ReadonlyValue" class="readonly">
&lt;Special &amp; characters&gt;, TestFile1InSubfolder
</span><input type="hidden" name="TestTree" value="{$asdf->ID},{$subfolderfile1->ID}" class="hidden" id="TestTree" />
HTML
,
(string)$readonlyField->Field()
);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace SilverStripe\ORM\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\NullableField;
use SilverStripe\Forms\TextField;
class DBVarcharTest extends SapphireTest
{
protected static $extra_dataobjects = [
DBVarcharTest\TestObject::class,
];
public function testScaffold()
{
$obj = new DBVarcharTest\TestObject();
/** @var TextField $field */
$field = $obj->dbObject('Title')->scaffoldFormField();
$this->assertInstanceOf(TextField::class, $field);
$this->assertEquals(129, $field->getMaxLength());
/** @var NullableField $nullable */
$nullable = $obj->dbObject('NullableField')->scaffoldFormField();
$this->assertInstanceOf(NullableField::class, $nullable);
$innerField = $nullable->valueField;
$this->assertInstanceOf(TextField::class, $innerField);
$this->assertEquals(111, $innerField->getMaxLength());
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace SilverStripe\ORM\Tests\DBVarcharTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class TestObject extends DataObject implements TestOnly
{
private static $table_name = 'DBVarcharTest_TestObject';
private static $db = [
'Title' => 'Varchar(129)',
'NullableField' => 'Varchar(111, ["nullifyEmpty" => false])'
];
}

View File

@ -106,7 +106,8 @@ class SearchContextTest extends SapphireTest
$context = $company->getDefaultSearchContext(); $context = $company->getDefaultSearchContext();
$this->assertEquals( $this->assertEquals(
new FieldList( new FieldList(
new TextField("Name", 'Name'), (new TextField("Name", 'Name'))
->setMaxLength(255),
new TextareaField("Industry", 'Industry'), new TextareaField("Industry", 'Industry'),
new NumericField("AnnualProfit", 'The Almighty Annual Profit') new NumericField("AnnualProfit", 'The Almighty Annual Profit')
), ),