Replace all legacy ::: syntax with GFMD tags

This commit is contained in:
Aaron Carlino 2017-08-03 12:51:32 +12:00
parent 16407b9185
commit eb1695c03d
97 changed files with 2522 additions and 1014 deletions

View File

@ -19,7 +19,8 @@ Let's look at a simple example:
**mysite/code/Player.php** **mysite/code/Player.php**
:::php ```php
<?php <?php
class Player extends DataObject { class Player extends DataObject {
@ -31,7 +32,7 @@ Let's look at a simple example:
'Birthday' => 'Date' '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
so on. After writing this class, we need to regenerate the database schema. so on. After writing this class, we need to regenerate the database schema.
@ -76,7 +77,8 @@ system. Instead, it will generate a new `ID` by adding 1 to the current maximum
**mysite/code/Player.php** **mysite/code/Player.php**
:::php ```php
<?php <?php
class Player extends DataObject { class Player extends DataObject {
@ -88,6 +90,7 @@ system. Instead, it will generate a new `ID` by adding 1 to the current maximum
'Birthday' => 'Date' 'Birthday' => 'Date'
); );
} }
```
Generates the following `SQL`. Generates the following `SQL`.
@ -109,13 +112,17 @@ Generates the following `SQL`.
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'>
Using the `create()` method provides chainability, which can add elegance and brevity to your code, e.g. `Player::create()->write()`. More importantly, however, it will look up the class in the [Injector](../extending/injector) so that the class can be overriden by [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection). Using the `create()` method provides chainability, which can add elegance and brevity to your code, e.g. `Player::create()->write()`. More importantly, however, it will look up the class in the [Injector](../extending/injector) so that the class can be overriden by [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection).
@ -125,28 +132,35 @@ Using the `create()` method provides chainability, which can add elegance and br
Database columns and properties can be set as class properties on the object. The SilverStripe ORM handles the saving Database columns and properties can be set as class properties on the object. The SilverStripe ORM handles the saving
of the values through a custom `__set()` method. 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
With the `Player` class defined we can query our data using the `ORM` or Object-Relational Model. The `ORM` provides With the `Player` class defined we can query our data using the `ORM` or Object-Relational Model. The `ORM` provides
shortcuts and methods for fetching, sorting and filtering data from our database. 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.
@ -158,16 +172,19 @@ shortcuts and methods for fetching, sorting and filtering data from our database
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(array( $members = Player::get()->filter(array(
'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'
```
<div class="info" markdown="1"> <div class="info" markdown="1">
Provided `filter` values are automatically escaped and do not require any escaping. Provided `filter` values are automatically escaped and do not require any escaping.
@ -180,7 +197,8 @@ The `ORM` doesn't actually execute the [SQLSelect](api:SilverStripe\ORM\Queries\
It's smart enough to generate a single efficient query at the last moment in time without needing to post-process the It's smart enough to generate a single efficient query at the last moment in time without needing to post-process the
result set in PHP. In `MySQL` the query generated by the ORM may look something like this result set in PHP. In `MySQL` the query generated by the ORM may look something like this
:::php ```php
$players = Player::get()->filter(array( $players = Player::get()->filter(array(
'FirstName' => 'Sam' 'FirstName' => 'Sam'
)); ));
@ -189,11 +207,12 @@ result set in PHP. In `MySQL` the query generated by the ORM may look something
// executes the following single query // executes the following single query
// SELECT * FROM Player WHERE FirstName = 'Sam' ORDER BY Surname // 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(array( $players = Player::get()->filter(array(
'FirstName' => 'Sam' 'FirstName' => 'Sam'
))->sort('Surname'); ))->sort('Surname');
@ -201,26 +220,31 @@ This also means that getting the count of a list of objects will be done with a
// This will create an single SELECT COUNT query // This will create an single SELECT COUNT query
// SELECT COUNT(*) FROM Player WHERE FirstName = 'Sam' // SELECT COUNT(*) FROM Player WHERE FirstName = 'Sam'
echo $players->Count(); 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 ```php
$players = Player::get(); $players = Player::get();
foreach($players as $player) { foreach($players as $player) {
echo $player->FirstName; 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.
@ -229,60 +253,73 @@ See the [Lists](lists) documentation for more information on dealing with [SS_Li
There are a couple of ways of getting a single DataObject from the ORM. If you know the ID number of the object, you There are a couple of ways of getting a single DataObject from the ORM. If you know the ID number of the object, you
can use `byID($id)`: 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
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(array( $players = Players::get()->sort(array(
'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
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(array( $players = Player::get()->filter(array(
'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
true for the record to be included in the result. true for the record to be included in the result.
@ -292,52 +329,63 @@ 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(array( $players = Player::get()->filter(array(
'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', array('Sam', 'Sig') 'FirstName', array('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(array( $players = Player::get()->filter(array(
'FirstName:StartsWith' => 'S' 'FirstName:StartsWith' => 'S'
'PlayerNumber:GreaterThan' => '10' 'PlayerNumber:GreaterThan' => '10'
)); ));
```
### filterAny ### filterAny
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(array( $players = Player::get()->filterAny(array(
'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(array( ->filter(array(
'LastName' => 'Minnée' 'LastName' => 'Minnée'
@ -347,14 +395,17 @@ You can combine both conjunctive ("AND") and disjunctive ("OR") statements.
'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(array( $players = Player::get()->filterAny(array(
'FirstName:StartsWith' => 'S' 'FirstName:StartsWith' => 'S'
'PlayerNumber:GreaterThan' => '10' 'PlayerNumber:GreaterThan' => '10'
)); ));
```
### Filtering by null values ### Filtering by null values
@ -366,29 +417,33 @@ checks to ensure that exclusion filters behave predictably.
For instance, the below code will select only values that do not match the given value, including nulls. For instance, the below code will select only values that do not match the given value, including nulls.
:::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', array('Sam', null)); $players = Player::get()->filter('FirstName:not', array('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', array(null, '')); $players = Player::get()->filter('FirstName', array(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
@ -424,84 +479,104 @@ 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
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', array('Sam','Sig') 'FirstName', array('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',
)); ));
```
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(array( $players = Player::get()->exclude(array(
'FirstName' => array('Sam', 'Sig'), 'FirstName' => array('Sam', 'Sig'),
'Age' => array(17, 43) 'Age' => array(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(array( $players = Player::get()->exclude(array(
'FirstName:EndsWith' => 'S' 'FirstName:EndsWith' => 'S'
'PlayerNumber:LessThanOrEqual' => '10' 'PlayerNumber:LessThanOrEqual' => '10'
)); ));
```
### Subtract ### Subtract
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()); $otherMembers = Member::get()->subtract($group->Members());
```
### Limit ### Limit
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); $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
parameter to specify the offset, which allows you to tell the system where to start getting the results from. The parameter to specify the offset, which allows you to tell the system where to start getting the results from. The
offset, if not provided as an argument, will default to 0. 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">
Note that the `limit` argument order is different from a MySQL LIMIT clause. Note that the `limit` argument order is different from a MySQL LIMIT clause.
@ -516,12 +591,13 @@ slashes in table names, it is necessary to provide an alternate mapping.
For instance, the below model will be stored in the table name `BannerImage` 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 { class BannerImage extends \DataObject {
private static $table_name = 'BannerImage'; 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
automatically generated for them. In the above case, the table name would have been automatically generated for them. In the above case, the table name would have been
@ -550,7 +626,8 @@ For example, if running a query against a particular model, you will need to ens
table and column. table and column.
:::php ```php
public function countDuplicates($model, $fieldToCheck) { public function countDuplicates($model, $fieldToCheck) {
$table = DataObject::getSchema()->tableForField($model, $field); $table = DataObject::getSchema()->tableForField($model, $field);
$query = new SQLSelect(); $query = new SQLSelect();
@ -558,7 +635,7 @@ table and column.
$query->setWhere(["\"{$table}\".\"{$field}\"" => $model->$fieldToCheck]); $query->setWhere(["\"{$table}\".\"{$field}\"" => $model->$fieldToCheck]);
return $query->count(); return $query->count();
} }
```
### Raw SQL ### Raw SQL
@ -576,8 +653,10 @@ 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
@ -587,13 +666,15 @@ You can specify a join with the `innerJoin` and `leftJoin` methods. Both of the
* The filter clause for the join. * The filter clause for the join.
* 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">
Passing a *$join* statement to will filter results further by the JOINs performed against the foreign table. It will Passing a *$join* statement to will filter results further by the JOINs performed against the foreign table. It will
@ -605,7 +686,8 @@ Passing a *$join* statement to will filter results further by the JOINs performe
Define the default values for all the `$db` fields. This example sets the "Status"-column on Player to "Active" Define the default values for all the `$db` fields. This example sets the "Status"-column on Player to "Active"
whenever a new object is created. whenever a new object is created.
:::php ```php
<?php <?php
class Player extends DataObject { class Player extends DataObject {
@ -614,6 +696,7 @@ whenever a new object is created.
"Status" => 'Active', "Status" => 'Active',
); );
} }
```
<div class="notice" markdown='1'> <div class="notice" markdown='1'>
Note: Alternatively you can set defaults directly in the database-schema (rather than the object-model). See Note: Alternatively you can set defaults directly in the database-schema (rather than the object-model). See
@ -629,7 +712,8 @@ time.
For example, suppose we have the following set of classes: For example, suppose we have the following set of classes:
:::php ```php
<?php <?php
class Page extends SiteTree { class Page extends SiteTree {
@ -642,10 +726,12 @@ For example, suppose we have the following set of classes:
'Summary' => 'Text' '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
SiteTree: SiteTree:
- ID: Int - ID: Int
- ClassName: Enum('SiteTree', 'Page', 'NewsPage') - ClassName: Enum('SiteTree', 'Page', 'NewsPage')
@ -656,15 +742,18 @@ The data for the following classes would be stored across the following tables:
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

@ -15,7 +15,8 @@ SilverStripe supports a number of relationship types and each relationship type
A 1-to-1 relation creates a database-column called "`<relationship-name>`ID", in the example below this would be A 1-to-1 relation creates a database-column called "`<relationship-name>`ID", in the example below this would be
"TeamID" on the "Player"-table. "TeamID" on the "Player"-table.
:::php ```php
<?php <?php
class Team extends DataObject { class Team extends DataObject {
@ -35,13 +36,15 @@ A 1-to-1 relation creates a database-column called "`<relationship-name>`ID", in
"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
and provides a short syntax for accessing the related object. 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();
@ -49,15 +52,18 @@ At the database level, the `has_one` creates a `TeamID` field on `Player`. A `ha
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 ```ss
<% with $Player %> <% with $Player %>
<% if $Team %> <% if $Team %>
Plays for $Team.Title Plays for $Team.Title
<% end_if %> <% end_if %>
<% end_with %> <% end_with %>
```
## Polymorphic has_one ## Polymorphic has_one
@ -70,7 +76,8 @@ with the ID column identifies the object.
To specify that a has_one relation is polymorphic set the type to 'DataObject'. 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
class Player extends DataObject { class Player extends DataObject {
private static $has_many = array( private static $has_many = array(
@ -92,6 +99,7 @@ Ideally, the associated has_many (or belongs_to) should be specified with dot no
"FanOf" => "DataObject" "FanOf" => "DataObject"
); );
} }
```
<div class="warning" markdown='1'> <div class="warning" markdown='1'>
Note: The use of polymorphic relationships can affect query performance, especially Note: The use of polymorphic relationships can affect query performance, especially
@ -110,7 +118,8 @@ Please specify a $has_one-relationship on the related child-class as well, in or
available on both ends. available on both ends.
</div> </div>
:::php ```php
<?php <?php
class Team extends DataObject { class Team extends DataObject {
@ -130,11 +139,13 @@ available on both ends.
"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();
@ -146,10 +157,12 @@ you will get an instance of [HasManyList](api:SilverStripe\ORM\HasManyList) rath
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
<?php <?php
class Person extends DataObject { class Person extends DataObject {
@ -167,7 +180,7 @@ To specify multiple `$has_many` to the same object you can use dot notation to d
"Cleaner" => "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
named. named.
@ -193,7 +206,8 @@ declaring the `$belongs_to`.
Similarly with `$has_many`, dot notation can be used to explicitly specify the `$has_one` which refers to this relation. Similarly with `$has_many`, dot notation can be used to explicitly specify the `$has_one` which refers to this relation.
This is not mandatory unless the relationship would be otherwise ambiguous. This is not mandatory unless the relationship would be otherwise ambiguous.
:::php ```php
<?php <?php
class Team extends DataObject { class Team extends DataObject {
@ -209,7 +223,7 @@ This is not mandatory unless the relationship would be otherwise ambiguous.
'Team' => 'Team.Coach' 'Team' => 'Team.Coach'
); );
} }
```
## many_many ## many_many
@ -226,12 +240,13 @@ Much like the `has_one` relationship, `many_many` can be navigated through the `
The only difference being you will get an instance of [ManyManyList](api:SilverStripe\ORM\ManyManyList) or The only difference being you will get an instance of [ManyManyList](api:SilverStripe\ORM\ManyManyList) or
[ManyManyThroughList](api:SilverStripe\ORM\ManyManyThroughList) rather than the object. [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
@ -243,7 +258,8 @@ Extra fields on the mapping table can be created by declaring a `many_many_extra
config to add extra columns. config to add extra columns.
:::php ```php
<?php <?php
class Team extends DataObject { class Team extends DataObject {
@ -263,7 +279,7 @@ config to add extra columns.
"Supports" => "Team", "Supports" => "Team",
]; ];
} }
```
### many_many through relationship joined on a separate DataObject ### many_many through relationship joined on a separate DataObject
@ -287,7 +303,8 @@ or child record.
The syntax for `belongs_many_many` is unchanged. The syntax for `belongs_many_many` is unchanged.
:::php ```php
<?php <?php
class Team extends DataObject { class Team extends DataObject {
@ -316,15 +333,17 @@ The syntax for `belongs_many_many` is unchanged.
'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
query for the join table is isolated from the outer query controlled by DataList. query for the join table is isolated from the outer query controlled by DataList.
@ -335,13 +354,14 @@ query for the join table is isolated from the outer query controlled by DataList
The relationship can also be navigated in [templates](../templates). The 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 ```ss
<% with $Supporter %> <% with $Supporter %>
<% loop $Supports %> <% loop $Supports %>
Supports $Title <% if $TeamSupporter %>(rank $TeamSupporter.Ranking)<% end_if %> Supports $Title <% if $TeamSupporter %>(rank $TeamSupporter.Ranking)<% end_if %>
<% end_if %> <% end_if %>
<% end_with %> <% 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
is class-agnostic and doesn't know the type of the join table. is class-agnostic and doesn't know the type of the join table.
@ -355,7 +375,8 @@ To specify multiple $many_manys between the same classes, specify use the dot no
distinguish them like below: distinguish them like below:
:::php ```php
<?php <?php
class Category extends DataObject { class Category extends DataObject {
@ -373,7 +394,7 @@ distinguish them like below:
'FeaturedInCategories' => 'Category.FeaturedProducts' '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`,
the best way to think about it is that the object where the relationship will be edited the best way to think about it is that the object where the relationship will be edited
@ -387,7 +408,8 @@ Adding new items to a relations works the same, regardless if you're editing a *
encapsulated by [HasManyList](api:SilverStripe\ORM\HasManyList) and [ManyManyList](api:SilverStripe\ORM\ManyManyList), both of which provide very similar APIs, e.g. an `add()` encapsulated by [HasManyList](api:SilverStripe\ORM\HasManyList) and [ManyManyList](api:SilverStripe\ORM\ManyManyList), both of which provide very similar APIs, e.g. an `add()`
and `remove()` method. and `remove()` method.
:::php ```php
$team = Team::get()->byId(1); $team = Team::get()->byId(1);
// create a new supporter // create a new supporter
@ -397,7 +419,7 @@ and `remove()` method.
// add the supporter. // add the supporter.
$team->Supporters()->add($supporter); $team->Supporters()->add($supporter);
```
## Custom Relations ## Custom Relations
@ -406,7 +428,8 @@ 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
<?php <?php
class Team extends DataObject { class Team extends DataObject {
@ -419,6 +442,7 @@ See [DataObject::$has_many](api:SilverStripe\ORM\DataObject::$has_many) for more
return $this->Players()->filter('Status', 'Active'); return $this->Players()->filter('Status', 'Active');
} }
} }
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
Adding new records to a filtered `RelationList` like in the example above doesn't automatically set the filtered Adding new records to a filtered `RelationList` like in the example above doesn't automatically set the filtered

View File

@ -11,23 +11,28 @@ 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(); $members = Member::get();
foreach($members as $member) { foreach($members as $member) {
echo $member->Name; 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);
// //
@ -35,13 +40,14 @@ Or in the template engine:
echo $members->find('ID', 4)->FirstName; echo $members->find('ID', 4)->FirstName;
// returns 'Sam' // returns 'Sam'
```
## Maps ## Maps
A map is an array where the array indexes contain data as well as the values. You can build a map from any list 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( // $members = array(
@ -49,16 +55,20 @@ A map is an array where the array indexes contain data as well as the values. Yo
// 2 => 'Sig' // 2 => 'Sig'
// 3 => 'Will' // 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');
@ -68,12 +78,14 @@ This functionality is provided by the [Map](api:SilverStripe\ORM\Map) class, whi
// 'sig@silverstripe.com', // 'sig@silverstripe.com',
// 'will@silverstripe.com' // 'will@silverstripe.com'
// ); // );
```
## ArrayList ## ArrayList
[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);
@ -83,7 +95,7 @@ This functionality is provided by the [Map](api:SilverStripe\ORM\Map) class, whi
echo $list->Count(); echo $list->Count();
// returns '2' // returns '2'
```
## API Documentation ## API Documentation

View File

@ -13,7 +13,8 @@ In the `Player` example, we have four database columns each with a different dat
**mysite/code/Player.php** **mysite/code/Player.php**
:::php ```php
<?php <?php
class Player extends DataObject { class Player extends DataObject {
@ -25,6 +26,7 @@ In the `Player` example, we have four database columns each with a different dat
'Birthday' => 'Date' 'Birthday' => 'Date'
); );
} }
```
## Available Types ## Available Types
@ -51,7 +53,8 @@ See the [API documentation](api:SilverStripe\ORM\FieldType\DBField) for a full l
For complex default values for newly instantiated objects see [Dynamic Default Values](how_tos/dynamic_default_fields). For complex default values for newly instantiated objects see [Dynamic Default Values](how_tos/dynamic_default_fields).
For simple values you can make use of the `$defaults` array. For example: For simple values you can make use of the `$defaults` array. For example:
:::php ```php
<?php <?php
class Car extends DataObject { class Car extends DataObject {
@ -66,6 +69,7 @@ For simple values you can make use of the `$defaults` array. For example:
'Condition' => 'New' 'Condition' => 'New'
); );
} }
```
### Default values for new database columns ### Default values for new database columns
@ -81,7 +85,8 @@ For enum values, it's the second parameter.
For example: For example:
:::php ```php
<?php <?php
class Car extends DataObject { class Car extends DataObject {
@ -92,6 +97,7 @@ For example:
'Make' => 'Varchar(["default" => "Honda"]), 'Make' => 'Varchar(["default" => "Honda"]),
); );
} }
```
## Formatting Output ## Formatting Output
@ -103,7 +109,8 @@ object we can control the formatting and it allows us to call methods defined fr
**mysite/code/Player.php** **mysite/code/Player.php**
:::php ```php
<?php <?php
class Player extends DataObject { class Player extends DataObject {
@ -114,10 +121,12 @@ object we can control the formatting and it allows us to call methods defined fr
return DBField::create_field('Varchar', $this->FirstName . ' '. $this->LastName); 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;
@ -128,12 +137,14 @@ Then we can refer to a new `Name` column on our `Player` instances. In templates
echo $player->getName()->LimitCharacters(2); echo $player->getName()->LimitCharacters(2);
// returns "Sa.." // returns "Sa.."
```
## Casting ## Casting
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
<?php <?php
class Player extends DataObject { class Player extends DataObject {
@ -146,6 +157,7 @@ Rather than manually returning objects from your custom functions. You can use t
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
instance of the [DBField](api:SilverStripe\ORM\FieldType\DBField) class, providing additional helpers. For example, a string can be cast as a [DBText](api:SilverStripe\ORM\FieldType\DBText) instance of the [DBField](api:SilverStripe\ORM\FieldType\DBField) class, providing additional helpers. For example, a string can be cast as a [DBText](api:SilverStripe\ORM\FieldType\DBText)
@ -154,21 +166,26 @@ type, which has a `FirstSentence()` method to retrieve the first sentence in a l
On the most basic level, the class can be used as simple conversion class from one value to another, e.g. to round a On the most basic level, the class can be used as simple conversion class from one value to another, e.g. to round a
number. number.
:::php ```php
DBField::create_field('Double', 1.23456)->Round(2); // results in 1.23 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" DBField::create_field('Date', '1982-01-01')->TimeDiff(); // shows "30 years ago"
```
## Casting ViewableData ## Casting ViewableData
Most objects in SilverStripe extend from [ViewableData](api:SilverStripe\View\ViewableData), which means they know how to present themselves in a view Most objects in SilverStripe extend from [ViewableData](api:SilverStripe\View\ViewableData), which means they know how to present themselves in a view
context. Through a `$casting` array, arbitrary properties and getters can be casted: context. Through a `$casting` array, arbitrary properties and getters can be casted:
:::php ```php
<?php <?php
class MyObject extends ViewableData { class MyObject extends ViewableData {
@ -187,7 +204,7 @@ context. Through a `$casting` array, arbitrary properties and getters can be cas
$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
@ -207,7 +224,8 @@ We can overload the default behavior by making a function called "get`<fieldname
The following example will use the result of `getStatus` instead of the 'Status' database column. We can refer to the The following example will use the result of `getStatus` instead of the 'Status' database column. We can refer to the
database column using `dbObject`. database column using `dbObject`.
:::php ```php
<?php <?php
class Player extends DataObject { class Player extends DataObject {
@ -219,7 +237,7 @@ database column using `dbObject`.
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

@ -18,7 +18,8 @@ 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
<?php <?php
class Player extends DataObject { class Player extends DataObject {
@ -49,6 +50,7 @@ Example: Disallow creation of new players if the currently logged-in player is n
parent::onBeforeWrite(); parent::onBeforeWrite();
} }
} }
```
## onBeforeDelete ## onBeforeDelete
@ -57,7 +59,8 @@ Triggered before executing *delete()* on an existing object.
Example: Checking for a specific [permission](permissions) to delete this type of object. It checks if a Example: Checking for a specific [permission](permissions) to delete this type of object. It checks if a
member is logged in who belongs to a group containing the permission "PLAYER_DELETE". member is logged in who belongs to a group containing the permission "PLAYER_DELETE".
:::php ```php
<?php <?php
class Player extends DataObject { class Player extends DataObject {
@ -75,8 +78,7 @@ member is logged in who belongs to a group containing the permission "PLAYER_DEL
parent::onBeforeDelete(); parent::onBeforeDelete();
} }
} }
```
<div class="notice" markdown='1'> <div class="notice" markdown='1'>
Note: There are no separate methods for *onBeforeCreate* and *onBeforeUpdate*. Please check `$this->isInDb()` to toggle Note: There are no separate methods for *onBeforeCreate* and *onBeforeUpdate*. Please check `$this->isInDb()` to toggle

View File

@ -16,7 +16,8 @@ you can put on field names to change this behavior. These are represented as `Se
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(array( $players = Player::get()->filter(array(
'FirstName:StartsWith' => 'S', 'FirstName:StartsWith' => 'S',
@ -28,6 +29,7 @@ An example of a `SearchFilter` in use:
'FirstName:PartialMatch' => 'z', 'FirstName:PartialMatch' => 'z',
'LastName: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.
@ -41,15 +43,17 @@ within the `DataListFilter.` prefixed namespace. New filters can be registered u
config: 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(array( $players = Player::get()->filter(array(
'FirstName:StartsWith:nocase' => 'S' 'FirstName:StartsWith:nocase' => 'S'
)); ));
@ -58,6 +62,7 @@ The following is a query which will return everyone whose first name starts with
$players = Player::get()->filter(array( $players = Player::get()->filter(array(
'FirstName:StartsWith:not' => 'W' 'FirstName:StartsWith:not' => 'W'
)); ));
```
## API Documentation ## API Documentation

View File

@ -16,7 +16,8 @@ By default, all `DataObject` subclasses can only be edited, created and viewed b
code. code.
</div> </div>
:::php ```php
<?php <?php
class MyDataObject extends DataObject { class MyDataObject extends DataObject {
@ -37,6 +38,7 @@ code.
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member); return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
} }
} }
```
<div class="alert" markdown="1"> <div class="alert" markdown="1">
These checks are not enforced on low-level ORM operations such as `write()` or `delete()`, but rather rely on being These checks are not enforced on low-level ORM operations such as `write()` or `delete()`, but rather rely on being

View File

@ -18,7 +18,8 @@ such as counts or returning a single column.
For example, if you want to run a simple `COUNT` SQL statement, 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. // Through raw SQL.
$count = DB::query('SELECT COUNT(*) FROM "Member"')->value(); $count = DB::query('SELECT COUNT(*) FROM "Member"')->value();
@ -28,6 +29,7 @@ the following three statements are functionally equivalent:
// Through the ORM. // Through the ORM.
$count = Member::get()->count(); $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
various assumptions the ORM and code based on it have: various assumptions the ORM and code based on it have:
@ -56,7 +58,8 @@ conditional filters, grouping, limiting, and sorting.
E.g. E.g.
:::php ```php
<?php <?php
$sqlQuery = new SQLSelect(); $sqlQuery = new SQLSelect();
@ -81,6 +84,7 @@ E.g.
foreach($result as $row) { foreach($result as $row) {
echo $row['BirthYear']; 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).
This class implements the *Iterator*-interface, and provides convenience-methods for accessing the data. This class implements the *Iterator*-interface, and provides convenience-methods for accessing the data.
@ -93,17 +97,20 @@ object instead.
For example, creating a `SQLDelete` object For example, creating a `SQLDelete` object
:::php ```php
<?php <?php
$query = SQLDelete::create() $query = SQLDelete::create()
->setFrom('"SiteTree"') ->setFrom('"SiteTree"')
->setWhere(array('"SiteTree"."ShowInMenus"' => 0)); ->setWhere(array('"SiteTree"."ShowInMenus"' => 0));
$query->execute(); $query->execute();
```
Alternatively, turning an existing `SQLQuery` into a delete Alternatively, turning an existing `SQLQuery` into a delete
:::php ```php
<?php <?php
$query = SQLQuery::create() $query = SQLQuery::create()
@ -111,13 +118,16 @@ Alternatively, turning an existing `SQLQuery` into a delete
->setWhere(array('"SiteTree"."ShowInMenus"' => 0)) ->setWhere(array('"SiteTree"."ShowInMenus"' => 0))
->toDelete(); ->toDelete();
$query->execute(); $query->execute();
```
Directly querying the database Directly querying the database
:::php ```php
<?php <?php
DB::prepared_query('DELETE FROM "SiteTree" WHERE "SiteTree"."ShowInMenus" = ?', array(0)); DB::prepared_query('DELETE FROM "SiteTree" WHERE "SiteTree"."ShowInMenus" = ?', array(0));
```
### INSERT/UPDATE ### INSERT/UPDATE
@ -163,7 +173,8 @@ SQLInsert also includes the following api methods:
E.g. E.g.
:::php ```php
<?php <?php
$update = SQLUpdate::create('"SiteTree"')->addWhere(array('ID' => 3)); $update = SQLUpdate::create('"SiteTree"')->addWhere(array('ID' => 3));
@ -188,6 +199,7 @@ E.g.
// Perform the update // Perform the update
$update->execute(); $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
inserts. For database connectors and API that don't have multi-row insert support inserts. For database connectors and API that don't have multi-row insert support
@ -195,7 +207,8 @@ these are translated internally as multiple single row inserts.
For example, For example,
:::php ```php
<?php <?php
$insert = SQLInsert::create('"SiteTree"'); $insert = SQLInsert::create('"SiteTree"');
@ -216,6 +229,7 @@ For example,
// $columns will be array('"Title"', '"Content"', '"ClassName"'); // $columns will be array('"Title"', '"Content"', '"ClassName"');
$insert->execute(); $insert->execute();
```
### Value Checks ### Value Checks
@ -224,18 +238,22 @@ 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(); $sqlQuery = new SQLSelect();
$sqlQuery->setFrom('Player'); $sqlQuery->setFrom('Player');
$sqlQuery->addSelect('COUNT("Player"."ID")'); $sqlQuery->addSelect('COUNT("Player"."ID")');
$sqlQuery->addWhere(array('"Team"."ID"' => 99)); $sqlQuery->addWhere(array('"Team"."ID"' => 99));
$sqlQuery->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"'); $sqlQuery->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"');
$count = $sqlQuery->execute()->value(); $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
@ -244,19 +262,22 @@ 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(); $sqlQuery = new SQLSelect();
$sqlQuery->setFrom('Player'); $sqlQuery->setFrom('Player');
$sqlQuery->setSelect('Birthdate'); $sqlQuery->setSelect('Birthdate');
$sqlQuery->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear'); $sqlQuery->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear');
$map = $sqlQuery->execute()->map(); $map = $sqlQuery->execute()->map();
$field = new DropdownField('Birthdates', 'Birthdates', $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
because of the custom SQL value transformation (`YEAR()`). 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
class Player extends DataObject { class Player extends DataObject {
private static $db = array( private static $db = array(
'Name' => 'Varchar', 'Name' => 'Varchar',
@ -268,6 +289,7 @@ An alternative approach would be a custom getter in the object definition.
} }
$players = Player::get(); $players = Player::get();
$map = $players->map('Name', 'NameWithBirthyear'); $map = $players->map('Name', 'NameWithBirthyear');
```
## Related ## Related

View File

@ -21,7 +21,8 @@ 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
<?php <?php
class MyObject extends DataObject { class MyObject extends DataObject {
@ -41,6 +42,7 @@ The return value of `validate()` is a [ValidationResult](api:SilverStripe\ORM\Va
return $result; return $result;
} }
} }
```
## API Documentation ## API Documentation

View File

@ -20,25 +20,27 @@ By default, adding the `Versioned extension will create a "Stage" and "Live" sta
also track versioned history. also track versioned history.
:::php ```php
class MyStagedModel extends DataObject { class MyStagedModel extends DataObject {
private static $extensions = [ private static $extensions = [
Versioned::class Versioned::class
]; ];
} }
```
Alternatively, staging can be disabled, so that only versioned changes are tracked for your model. This Alternatively, staging can be disabled, so that only versioned changes are tracked for your model. This
can be specified by setting the constructor argument to "Versioned" can be specified by setting the constructor argument to "Versioned"
:::php ```php
class VersionedModel extends DataObject { class VersionedModel extends DataObject {
private static $extensions = [ private static $extensions = [
"SilverStripe\\ORM\\Versioning\\Versioned('Versioned')" "SilverStripe\\ORM\\Versioning\\Versioned('Versioned')"
]; ];
} }
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
The extension is automatically applied to `SiteTree` class. For more information on extensions see The extension is automatically applied to `SiteTree` class. For more information on extensions see
@ -79,7 +81,8 @@ automatically joined as required:
By default, all records are retrieved from the "Draft" stage (so the `MyRecord` table in our example). You can By default, all records are retrieved from the "Draft" stage (so the `MyRecord` table in our example). You can
explicitly request a certain stage through various getters on the `Versioned` class. explicitly request a certain stage through various getters on the `Versioned` class.
:::php ```php
// 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);
@ -87,14 +90,17 @@ explicitly request a certain stage through various getters on the `Versioned` cl
// Fetching a single record // Fetching a single record
$stageRecord = Versioned::get_by_stage('MyRecord', Versioned::DRAFT)->byID(99); $stageRecord = Versioned::get_by_stage('MyRecord', Versioned::DRAFT)->byID(99);
$liveRecord = Versioned::get_by_stage('MyRecord', Versioned::LIVE)->byID(99); $liveRecord = Versioned::get_by_stage('MyRecord', Versioned::LIVE)->byID(99);
```
### Historical Versions ### Historical Versions
The above commands will just retrieve the latest version of its respective stage for you, but not older versions stored The above commands will just retrieve the latest version of its respective stage for you, but not older versions stored
in the `<class>_versions` tables. in the `<class>_versions` tables.
:::php ```php
$historicalRecord = Versioned::get_version('MyRecord', <record-id>, <version-id>); $historicalRecord = Versioned::get_version('MyRecord', <record-id>, <version-id>);
```
<div class="alert" markdown="1"> <div class="alert" markdown="1">
The record is retrieved as a `DataObject`, but saving back modifications via `write()` will create a new version, The record is retrieved as a `DataObject`, but saving back modifications via `write()` will create a new version,
@ -105,10 +111,12 @@ In order to get a list of all versions for a specific record, we need to generat
objects, which expose the same database information as a `DataObject`, but also include information about when and how 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();
echo $versions->First()->Version; // instance of Versioned_Version echo $versions->First()->Version; // instance of Versioned_Version
```
### Writing Versions and Changing Stages ### Writing Versions and Changing Stages
@ -130,7 +138,8 @@ done via one of several ways:
* `publishRecursive` Publishes this record, and any dependant objects this record may refer to. * `publishRecursive` Publishes this record, and any dependant objects this record may refer to.
See "DataObject ownership" for reference on dependant objects. See "DataObject ownership" for reference on dependant objects.
:::php ```php
$record = Versioned::get_by_stage('MyRecord', Versioned::DRAFT)->byID(99); $record = Versioned::get_by_stage('MyRecord', Versioned::DRAFT)->byID(99);
$record->MyField = 'changed'; $record->MyField = 'changed';
// will update `MyRecord` table (assuming Versioned::current_stage() == 'Stage'), // will update `MyRecord` table (assuming Versioned::current_stage() == 'Stage'),
@ -138,25 +147,30 @@ done via one of several ways:
$record->write(); $record->write();
// will copy the saved record information to the `MyRecord_Live` table // will copy the saved record information to the `MyRecord_Live` table
$record->publishRecursive(); $record->publishRecursive();
```
Similarly, an "unpublish" operation does the reverse, and removes a record from a specific stage. Similarly, an "unpublish" operation does the reverse, and removes a record from a specific stage.
:::php ```php
$record = MyRecord::get()->byID(99); // stage doesn't matter here $record = MyRecord::get()->byID(99); // stage doesn't matter here
// will remove the row from the `MyRecord_Live` table // will remove the row from the `MyRecord_Live` table
$record->deleteFromStage(Versioned::LIVE); $record->deleteFromStage(Versioned::LIVE);
```
### Forcing the Current Stage ### Forcing the Current Stage
The current stage is stored as global state on the object. It is usually modified by controllers, e.g. when a preview The current stage is stored as global state on the object. It is usually modified by controllers, e.g. when a preview
is initialized. But it can also be set and reset temporarily to force a specific operation to run on a certain stage. is initialized. But it can also be set and reset temporarily to force a specific operation to run on a certain stage.
:::php ```php
$origMode = Versioned::get_reading_mode(); // save current mode $origMode = Versioned::get_reading_mode(); // save current mode
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Live' records $obj = MyRecord::getComplexObjectRetrieval(); // returns 'Live' records
Versioned::set_reading_mode(Versioned::DRAFT); // temporarily overwrite mode Versioned::set_reading_mode(Versioned::DRAFT); // temporarily overwrite mode
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records $obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
Versioned::set_reading_mode($origMode); // reset current mode Versioned::set_reading_mode($origMode); // reset current mode
```
### DataObject ownership ### DataObject ownership
@ -177,7 +191,8 @@ When pages of type `MyPage` are published, any owned images and banners will be
without requiring any custom code. without requiring any custom code.
:::php ```php
class MyPage extends Page { class MyPage extends Page {
private static $has_many = array( private static $has_many = array(
'Banners' => Banner::class 'Banners' => Banner::class
@ -199,7 +214,7 @@ without requiring any custom code.
'Image' 'Image'
); );
} }
```
Note that ownership cannot be used with polymorphic relations. E.g. has_one to non-type specific `DataObject`. Note that ownership cannot be used with polymorphic relations. E.g. has_one to non-type specific `DataObject`.
@ -216,7 +231,8 @@ that can be used to traverse between each, and then by ensuring you configure bo
E.g. E.g.
:::php ```php
class MyParent extends DataObject { class MyParent extends DataObject {
private static $extensions = array( private static $extensions = array(
Versioned::class Versioned::class
@ -239,6 +255,7 @@ E.g.
return MyParent::get()->first(); return MyParent::get()->first();
} }
} }
```
#### DataObject Ownership in HTML Content #### DataObject Ownership in HTML Content
@ -257,8 +274,10 @@ smaller modifications of the generated `DataList` objects.
Example: Get the first 10 live records, filtered by creation date: Example: Get the first 10 live records, filtered by creation date:
:::php ```php
$records = Versioned::get_by_stage('MyRecord', Versioned::LIVE)->limit(10)->sort('Created', 'ASC'); $records = Versioned::get_by_stage('MyRecord', Versioned::LIVE)->limit(10)->sort('Created', 'ASC');
```
### Permissions ### Permissions
@ -280,7 +299,8 @@ Versioned object visibility can be customised in one of the following ways by ed
E.g. E.g.
:::php ```php
class MyObject extends DataObject { class MyObject extends DataObject {
private static $extensions = array( private static $extensions = array(
Versioned::class, Versioned::class,
@ -298,6 +318,7 @@ E.g.
return Permission::checkMember($member, 'ADMIN'); return Permission::checkMember($member, 'ADMIN');
} }
} }
```
If you want to control permissions of an object in an extension, you can also use If you want to control permissions of an object in an extension, you can also use
one of the below extension points in your `DataExtension` subclass: one of the below extension points in your `DataExtension` subclass:
@ -310,25 +331,29 @@ only be invoked if the object is in a non-published state.
E.g. E.g.
:::php ```php
class MyObjectExtension extends DataExtension { class MyObjectExtension extends DataExtension {
public function canViewNonLive($member = null) { public function canViewNonLive($member = null) {
return Permission::check($member, 'DRAFT_STATUS'); return Permission::check($member, 'DRAFT_STATUS');
} }
} }
```
If none of the above checks are overridden, visibility will be determined by the If none of the above checks are overridden, visibility will be determined by the
permissions in the `TargetObject.non_live_permissions` config. permissions in the `TargetObject.non_live_permissions` config.
E.g. E.g.
:::php ```php
class MyObject extends DataObject { class MyObject extends DataObject {
private static $extensions = array( private static $extensions = array(
Versioned::class, Versioned::class,
); );
private static $non_live_permissions = array('ADMIN'); private static $non_live_permissions = array('ADMIN');
} }
```
Versioned applies no additional permissions to `canEdit` or `canCreate`, and such Versioned applies no additional permissions to `canEdit` or `canCreate`, and such
these permissions should be implemented as per standard unversioned DataObjects. these permissions should be implemented as per standard unversioned DataObjects.
@ -345,12 +370,13 @@ default, and only preview draft content if explicitly requested (e.g. by the "pr
to force a specific stage, we recommend the `Controller->init()` method for this purpose, for example: to force a specific stage, we recommend the `Controller->init()` method for this purpose, for example:
**mysite/code/MyController.php** **mysite/code/MyController.php**
:::php ```php
public function init() { public function init() {
parent::init(); parent::init();
Versioned::set_stage(Versioned::DRAFT); Versioned::set_stage(Versioned::DRAFT);
} }
```
### Controllers ### Controllers

View File

@ -12,7 +12,8 @@ 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
<?php <?php
class MyDataObject extends DataObject { class MyDataObject extends DataObject {
@ -31,10 +32,12 @@ An example is `DataObject`, SilverStripe will automatically create your CMS inte
return $fields; 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
<?php <?php
public function getCMSFields() { public function getCMSFields() {
@ -49,8 +52,7 @@ To fully customise your form fields, start with an empty FieldList.
return $fields; 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
[DataExtension](/developer_guides/extending/extensions), and a call to `DataExtension->updateCMSFields`. [DataExtension](/developer_guides/extending/extensions), and a call to `DataExtension->updateCMSFields`.
@ -60,7 +62,8 @@ You can also alter the fields of built-in and module `DataObject` classes throug
The `$searchable_fields` property uses a mixed array format that can be used to further customise your generated admin The `$searchable_fields` property uses a mixed array format that can be used to further customise your generated admin
system. The default is a set of array values listing the fields. system. The default is a set of array values listing the fields.
:::php ```php
<?php <?php
class MyDataObject extends DataObject { class MyDataObject extends DataObject {
@ -70,13 +73,14 @@ system. The default is a set of array values listing the fields.
'ProductCode' '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
default search filter assigned (usually an [ExactMatchFilter](api:SilverStripe\ORM\Filters\ExactMatchFilter)). To override these defaults, you can specify default search filter assigned (usually an [ExactMatchFilter](api:SilverStripe\ORM\Filters\ExactMatchFilter)). To override these defaults, you can specify
additional information on `$searchable_fields`: additional information on `$searchable_fields`:
:::php ```php
<?php <?php
class MyDataObject extends DataObject { class MyDataObject extends DataObject {
@ -86,11 +90,13 @@ additional information on `$searchable_fields`:
'ProductCode' => 'NumericField' '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
<?php <?php
class MyDataObject extends DataObject { class MyDataObject extends DataObject {
@ -107,11 +113,12 @@ both, you can assign an array:
), ),
); );
} }
```
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
<?php <?php
class Team extends DataObject { class Team extends DataObject {
@ -141,14 +148,15 @@ To include relations (`$has_one`, `$has_many` and `$many_many`) in your search,
'Teams' => 'Team' 'Teams' => 'Team'
); );
} }
```
### Summary Fields ### Summary Fields
Summary fields can be used to show a quick overview of the data for a specific [DataObject](api:SilverStripe\ORM\DataObject) record. The most common use Summary fields can be used to show a quick overview of the data for a specific [DataObject](api:SilverStripe\ORM\DataObject) record. The most common use
is their display as table columns, e.g. in the search results of a [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) CMS interface. is their display as table columns, e.g. in the search results of a [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) CMS interface.
:::php ```php
<?php <?php
class MyDataObject extends DataObject { class MyDataObject extends DataObject {
@ -164,11 +172,12 @@ is their display as table columns, e.g. in the search results of a [ModelAdmin](
'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
<?php <?php
class OtherObject extends DataObject { class OtherObject extends DataObject {
@ -195,11 +204,12 @@ To include relations or field manipulations in your summaries, you can use a dot
'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
<?php <?php
class MyDataObject extends DataObject { class MyDataObject extends DataObject {
@ -217,6 +227,7 @@ Non-textual elements (such as images and their manipulations) can also be used i
'HeroImage.CMSThumbnail' => 'Hero Image' 'HeroImage.CMSThumbnail' => 'Hero Image'
); );
} }
```
## Related Documentation ## Related Documentation

View File

@ -24,7 +24,8 @@ this index is present on the associative entity).
Indexes are represented on a `DataObject` through the `DataObject::$indexes` array which maps index names to a Indexes are represented on a `DataObject` through the `DataObject::$indexes` array which maps index names to a
descriptor. There are several supported notations: descriptor. There are several supported notations:
:::php ```php
<?php <?php
class MyObject extends DataObject { class MyObject extends DataObject {
@ -38,6 +39,7 @@ descriptor. There are several supported notations:
'<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
we recommend building the index to suite the requirements of your data. we recommend building the index to suite the requirements of your data.
@ -52,7 +54,8 @@ support the following:
**mysite/code/MyTestObject.php** **mysite/code/MyTestObject.php**
:::php ```php
<?php <?php
class MyTestObject extends DataObject { class MyTestObject extends DataObject {
@ -66,6 +69,7 @@ support the following:
'MyIndexName' => ['MyField', 'MyOtherField'], 'MyIndexName' => ['MyField', 'MyOtherField'],
]; ];
} }
```
## Complex/Composite Indexes ## Complex/Composite Indexes
For complex queries it may be necessary to define a complex or composite index on the supporting object. To create a For complex queries it may be necessary to define a complex or composite index on the supporting object. To create a

View File

@ -11,7 +11,8 @@ An example of a SilverStripe template is below:
**mysite/templates/Page.ss** **mysite/templates/Page.ss**
:::ss ```ss
<html> <html>
<head> <head>
<% base_tag %> <% base_tag %>
@ -38,6 +39,7 @@ An example of a SilverStripe template is below:
<% include Footer %> <% include Footer %>
</body> </body>
</html> </html>
```
<div class="note"> <div class="note">
Templates can be used for more than HTML output. You can use them to output your data as JSON, XML, CSV or any other Templates can be used for more than HTML output. You can use them to output your data as JSON, XML, CSV or any other
@ -66,17 +68,21 @@ Variables are placeholders that will be replaced with data from the [DataModel](
[Controller](../controllers). Variables are prefixed with a `$` character. Variable names must start with an [Controller](../controllers). Variables are prefixed with a `$` character. Variable names must start with an
alphabetic character or underscore, with subsequent characters being alphanumeric or underscore: 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`.
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.
@ -97,16 +103,20 @@ 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">
Method names that begin with `get` will automatically be resolved when their prefix is excluded. For example, the above method call `$UsersIpAddress` would also invoke a method named `getUsersIpAddress()`. Method names that begin with `get` will automatically be resolved when their prefix is excluded. For example, the above method call `$UsersIpAddress` would also invoke a method named `getUsersIpAddress()`.
@ -119,29 +129,34 @@ record and any subclasses of those two.
**mysite/code/Layout/Page.ss** **mysite/code/Layout/Page.ss**
:::ss ```ss
$Title $Title
// returns the page `Title` property // returns the page `Title` property
$Content $Content
// returns the page `Content` property // returns the page `Content` property
```
## Conditional Logic ## Conditional Logic
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">
When inside template tags variables should have a '$' prefix, and literals should have quotes. When inside template tags variables should have a '$' prefix, and literals should have quotes.
@ -149,16 +164,19 @@ 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 %>
@ -166,15 +184,18 @@ Conditionals can also provide the `else` case.
<% else %> <% else %>
Can I have some of your chips? Can I have some of your chips?
<% end_if %> <% end_if %>
```
### Negation ### Negation
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
@ -182,27 +203,32 @@ 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
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
@ -210,39 +236,47 @@ Within SilverStripe templates we have the ability to include other templates usi
will be searched for using the same filename look-up rules as a regular template. However in the case of the include tag will be searched for using the same filename look-up rules as a regular template. However in the case of the include tag
an additional `Includes` directory will be inserted into the resolved path just prior to the filename. 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
The `<% loop %>` tag is used to iterate or loop over a collection of items such as [DataList](api:SilverStripe\ORM\DataList) or a [ArrayList](api:SilverStripe\ORM\ArrayList) The `<% loop %>` tag is used to iterate or loop over a collection of items such as [DataList](api:SilverStripe\ORM\DataList) or a [ArrayList](api:SilverStripe\ORM\ArrayList)
collection. collection.
:::ss ```ss
<h1>Children of $Title</h1> <h1>Children of $Title</h1>
<ul> <ul>
@ -250,6 +284,7 @@ collection.
<li>$Title</li> <li>$Title</li>
<% end_loop %> <% end_loop %>
</ul> </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
page. page.
@ -269,48 +304,58 @@ 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
@ -327,7 +372,8 @@ iteration.
Last item defaults to 1, but can be passed as a parameter. Last item defaults to 1, but can be passed as a parameter.
* `$TotalItems`: Number of items in the list (integer). * `$TotalItems`: Number of items in the list (integer).
:::ss ```ss
<ul> <ul>
<% loop $Children.Reverse %> <% loop $Children.Reverse %>
<% if First %> <% if First %>
@ -337,6 +383,7 @@ iteration.
<li class="$EvenOdd">Child $Pos of $TotalItems - $Title</li> <li class="$EvenOdd">Child $Pos of $TotalItems - $Title</li>
<% end_loop %> <% end_loop %>
</ul> </ul>
```
<div class="info" markdown="1"> <div class="info" markdown="1">
A common task is to paginate your lists. See the [Pagination](how_tos/pagination) how to for a tutorial on adding A common task is to paginate your lists. See the [Pagination](how_tos/pagination) how to for a tutorial on adding
@ -347,7 +394,8 @@ 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 // returns an int
$Modulus(value, offset) $Modulus(value, offset)
@ -361,6 +409,7 @@ $Modulus and $MultipleOf can help to build column and grid layouts.
<% end_loop %> <% end_loop %>
// returns <div class="column-3">, <div class="column-2">, // returns <div class="column-3">, <div class="column-2">,
```
<div class="hint" markdown="1"> <div class="hint" markdown="1">
`$Modulus` is useful for floated grid CSS layouts. If you want 3 rows across, put $Modulus(3) as a class and add a `$Modulus` is useful for floated grid CSS layouts. If you want 3 rows across, put $Modulus(3) as a class and add a
@ -370,33 +419,40 @@ $Modulus and $MultipleOf can help to build column and grid layouts.
$MultipleOf(value, offset) can also be utilized to build column and grid layouts. In this case we want to add a `<br>` $MultipleOf(value, offset) can also be utilized to build column and grid layouts. In this case we want to add a `<br>`
after every 3rd item. 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
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">
For more information on formatting and casting variables see [Formating, Modifying and Casting Variables](casting) For more information on formatting and casting variables see [Formating, Modifying and Casting Variables](casting)
@ -424,7 +480,8 @@ 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 %> <% loop $Children %>
@ -434,6 +491,7 @@ When in a particular scope, `$Up` takes the scope back to the previous level.
<p>Page '$Title' is a grandchild of '$Up.Up.Title'</p> <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.
@ -455,19 +513,22 @@ Given the following structure, it will output the text.
Additional selectors implicitely change the scope so you need to put additional `$Up` to get what you expect. Additional selectors implicitely change the scope so you need to put additional `$Up` to get what you expect.
</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
While `$Up` provides us a way to go up one level of scope, `$Top` is a shortcut to jump to the top most scope of the While `$Up` provides us a way to go up one level of scope, `$Top` is a shortcut to jump to the top most scope of the
page. The previous example could be rewritten to use the following syntax. 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 %> <% loop $Children %>
@ -477,20 +538,25 @@ page. The previous example could be rewritten to use the following syntax.
<p>Page '$Title' is a grandchild of '$Top.Title'</p> <p>Page '$Title' is a grandchild of '$Top.Title'</p>
<% end_loop %> <% end_loop %>
<% end_loop %> <% end_loop %>
```
### With ### With
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.
@ -502,22 +568,27 @@ refer directly to properties and methods of the [Member](api:SilverStripe\Securi
`$Me` outputs the current object in scope. This will call the `forTemplate` of the object. `$Me` outputs the current object in scope. This will call the `forTemplate` of the object.
:::ss ```ss
$Me $Me
```
## Comments ## Comments
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

@ -30,12 +30,14 @@ functionality may not be included.
## Base Tag ## Base Tag
:::ss ```ss
<head> <head>
<% base_tag %> <% 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
src="someimage.jpg" />) will become relative to the URI specified in the base tag. This ensures the browser knows where src="someimage.jpg" />) will become relative to the URI specified in the base tag. This ensures the browser knows where
@ -49,19 +51,20 @@ A `<% base_tag %>` is nearly always required or assumed by SilverStripe to exist
## CurrentMember ## CurrentMember
Returns the currently logged in [Member](api:SilverStripe\Security\Member) instance, if there is one logged in. Returns the currently logged in [Member](api:SilverStripe\Security\Member) instance, if there is one logged in.```ss
:::ss
<% if $CurrentMember %> <% if $CurrentMember %>
Welcome Back, $CurrentMember.FirstName Welcome Back, $CurrentMember.FirstName
<% end_if %> <% 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
`getTitle()` method). `getTitle()` method).
@ -75,8 +78,10 @@ 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
but it is also the standard for any object that has a body of content to output. but it is also the standard for any object that has a body of content to output.
@ -97,8 +102,10 @@ more details).
web pages. You'll need to install it via `composer`. 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
than PHP code. By default, this includes a Website title and a Tagline. than PHP code. By default, this includes a Website title and a Tagline.
@ -119,73 +126,86 @@ 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 `$MetaTags(false)` will render```ss
:::ss
<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" />
```
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
returns the relative URL for the object and `AbsoluteLink` outputs your full website address along with the relative returns the relative URL for the object and `AbsoluteLink` outputs your full website address along with the relative
link. link.
:::ss ```ss
$Link $Link
<!-- returns /about-us/offices/ --> <!-- returns /about-us/offices/ -->
$AbsoluteLink $AbsoluteLink
<!-- returns http://yoursite.com/about-us/offices/ --> <!-- 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`
will return true or false based on page being looped over relative to the currently viewed page. will return true or false based on page being looped over relative to the currently viewed page.
For instance, to only show the menu item linked if it's the current one: 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**
* `$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
@ -193,12 +213,14 @@ This returns the part of the URL of the page you're currently on. For example on
`URLSegment` will be `offices`. `URLSegment` cannot be used to generate a link since it does not output the full path. `URLSegment` will be `offices`. `URLSegment` cannot be used to generate a link since it does not output the full path.
It can be used within templates to generate anchors or other CSS classes. 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
@ -206,17 +228,21 @@ Returns the class of the current object in [scope](syntax#scope) such as `Page`
handy for a number of uses. A common use case is to add to your `<body>` tag to influence CSS styles and JavaScript handy for a number of uses. A common use case is to add to your `<body>` tag to influence CSS styles and JavaScript
behavior based on the page type used: 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
the `CMS` or a custom list of data. This originates in the `Versioned` extension's `getChildren` method. the `CMS` or a custom list of data. This originates in the `Versioned` extension's `getChildren` method.
@ -228,10 +254,12 @@ 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
because it's not dependent on the context of the current page. For example, it would allow you to list all staff member because it's not dependent on the context of the current page. For example, it would allow you to list all staff member
@ -244,18 +272,21 @@ Content authors have the ability to hide pages from menus by un-selecting the `S
This option will be honored by `<% loop $Children %>` and `<% loop $Menu %>` however if you want to ignore the user This option will be honored by `<% loop $Children %>` and `<% loop $Menu %>` however if you want to ignore the user
preference, `AllChildren` does not filter by `ShowInMenus`. 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.
@ -265,10 +296,12 @@ 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.
@ -276,10 +309,12 @@ 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,
looking back through its parent pages. `Level(1)` being the top most level. looking back through its parent pages. `Level(1)` being the top most level.
@ -292,7 +327,8 @@ 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 $Parent.Title
@ -300,7 +336,7 @@ For example, imagine you're on the "bob marley" page, which is three levels in:
$Parent.Parent.Title $Parent.Parent.Title
<!-- returns 'about us' --> <!-- returns 'about us' -->
```
## Navigating Scope ## Navigating Scope
@ -314,17 +350,21 @@ for website users.
While you can achieve breadcrumbs through the `$Level(<level>)` control manually, there's a nicer shortcut: The While you can achieve breadcrumbs through the `$Level(<level>)` control manually, there's a nicer shortcut: The
`$Breadcrumbs` variable. `$Breadcrumbs` variable.
:::ss ```ss
$Breadcrumbs $Breadcrumbs
```
By default, it uses the template defined in `cms/templates/BreadcrumbsTemplate.ss` By default, it uses the template defined in `cms/templates/BreadcrumbsTemplate.ss`
:::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">
To customise the markup that the `$Breadcrumbs` generates, copy `cms/templates/BreadcrumbsTemplate.ss` to To customise the markup that the `$Breadcrumbs` generates, copy `cms/templates/BreadcrumbsTemplate.ss` to
@ -333,8 +373,10 @@ To customise the markup that the `$Breadcrumbs` generates, copy `cms/templates/B
## Forms ## 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
SilverStripe log-in form. If you are on such a page, the `$Form` variable will contain the HTML content of the form. SilverStripe log-in form. If you are on such a page, the `$Form` variable will contain the HTML content of the form.

View File

@ -15,10 +15,12 @@ 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">
Requiring assets from the template is restricted compared to the PHP API. Requiring assets from the template is restricted compared to the PHP API.

View File

@ -112,7 +112,7 @@ 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
<html> <html>
<head> <head>
.. ..
@ -126,19 +126,19 @@ footer and navigation will remain the same and we don't want to replicate this w
<% include Footer %> <% include Footer %>
</body> </body>
``
**mysite/templates/Layout/Page.ss** **mysite/templates/Layout/Page.ss**
```ss
<p>You are on a $Title page</p> <p>You are on a $Title page</p>
$Content $Content
```
**mysite/templates/Layout/HomePage.ss** **mysite/templates/Layout/HomePage.ss**
```ss
<h1>This is the homepage!</h1> <h1>This is the homepage!</h1>
<blink>Hi!</blink> <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.

View File

@ -8,7 +8,8 @@ summary: Reduce rendering time with cached templates and understand the limitati
All functions that provide data to templates must have no side effects, as the value is cached after first access. For All functions that provide data to templates must have no side effects, as the value is cached after first access. For
example, this controller method will not behave as you might imagine. 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() {
@ -16,12 +17,15 @@ example, this controller method will not behave as you might imagine.
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
`$Counter` is cached at the first access, the value of `1` is saved. `$Counter` is cached at the first access, the value of `1` is saved.
@ -32,7 +36,11 @@ When we render `$Counter` to the template we would expect the value to increase
Partial caching is a feature that allows the caching of just a portion of a page. Instead of fetching the required data Partial caching is a feature that allows the caching of just a portion of a page. Instead of fetching the required data
from the database to display, the contents of the area are fetched from a [cache backend](../performance/caching). 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,11 @@ summary: Definition of the syntax for writing i18n compatible templates.
Translations are easy to use with a template, and give access to SilverStripe's translation facilities. Here is an 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 Foo.BAR 'Bar' %>
<%t Member.WELCOME 'Welcome {name} to {site}' name=$Member.Name site="Foobar.com" %> <%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

@ -12,17 +12,20 @@ output the result of the [DBHtmlText::FirstParagraph()](api:SilverStripe\ORM\Fie
**mysite/code/Page.ss** **mysite/code/Page.ss**
:::ss ```ss
$Content.FirstParagraph $Content.FirstParagraph
<!-- returns the result of HtmlText::FirstParagragh() --> <!-- returns the result of HtmlText::FirstParagragh() -->
$LastEdited.Format("d/m/Y") $LastEdited.Format("d/m/Y")
<!-- returns the result of SS_Datetime::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 $Content.FirstParagraph.NoHTML
<!-- "First Paragraph" --> <!-- "First Paragraph" -->
@ -31,6 +34,7 @@ Any public method from the object in scope can be called within the template. If
<div class="$URLSegment.LowerCase"> <div class="$URLSegment.LowerCase">
<!-- <div class="about-us"> --> <!-- <div class="about-us"> -->
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
See the API documentation for [DBHtmlText](api:SilverStripe\ORM\FieldType\DBHtmlText), [FieldType](api:SilverStripe\ORM\FieldType), [DBText](api:SilverStripe\ORM\FieldType\DBText) for all the methods you can use to format See the API documentation for [DBHtmlText](api:SilverStripe\ORM\FieldType\DBHtmlText), [FieldType](api:SilverStripe\ORM\FieldType), [DBText](api:SilverStripe\ORM\FieldType\DBText) for all the methods you can use to format
@ -43,8 +47,8 @@ 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
<?php <?php
class Page extends SiteTree { class Page extends SiteTree {
@ -53,12 +57,14 @@ provide default template for an object.
return "Page: ". $this->Title; return "Page: ". $this->Title;
} }
} }
```
**mysite/templates/Page.ss** **mysite/templates/Page.ss**
```ss
:::ss
$Me $Me
<!-- returns Page: Home --> <!-- returns Page: Home -->
```
## Casting ## Casting
@ -66,7 +72,8 @@ Methods which return data to the template should either return an explicit objec
content that method sends back, or, provide a type in the `$casting` array for the object. When rendering that method content that method sends back, or, provide a type in the `$casting` array for the object. When rendering that method
to a template, SilverStripe will ensure that the object is wrapped in the correct type and values are safely escaped. to a template, SilverStripe will ensure that the object is wrapped in the correct type and values are safely escaped.
:::php ```php
<?php <?php
class Page extends SiteTree { class Page extends SiteTree {
@ -79,6 +86,7 @@ to a template, SilverStripe will ensure that the object is wrapped in the correc
return "<h1>This is my header</h1>"; 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
accordingly. accordingly.

View File

@ -8,7 +8,8 @@ subclass the base `Controller` class.
**mysite/code/controllers/TeamController.php** **mysite/code/controllers/TeamController.php**
:::php ```php
<?php <?php
class TeamController extends Controller { class TeamController extends Controller {
@ -26,6 +27,7 @@ subclass the base `Controller` class.
print_r($request->allParams()); print_r($request->allParams());
} }
} }
```
## Routing ## Routing
@ -44,7 +46,8 @@ 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
@ -52,7 +55,7 @@ Make sure that after you have modified the `routes.yml` file, that you clear you
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.
@ -74,7 +77,8 @@ 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.
@ -126,6 +130,7 @@ Action methods can return one of four main things:
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.
@ -153,10 +158,12 @@ 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">
The [Controller::join_links()](api:SilverStripe\Control\Controller::join_links()) is optional, but makes `Link()` more flexible by allowing an `$action` argument, and concatenates the path segments with slashes. The action should map to a method on your controller. The [Controller::join_links()](api:SilverStripe\Control\Controller::join_links()) is optional, but makes `Link()` more flexible by allowing an `$action` argument, and concatenates the path segments with slashes. The action should map to a method on your controller.

View File

@ -11,7 +11,8 @@ actions on the website they shouldn't be able to.
Any action you define on a controller must be defined in a `$allowed_actions` static array. This prevents users from Any action you define on a controller must be defined in a `$allowed_actions` static array. This prevents users from
directly calling methods that they shouldn't. directly calling methods that they shouldn't.
:::php ```php
<?php <?php
class MyController extends Controller { class MyController extends Controller {
@ -36,6 +37,7 @@ directly calling methods that they shouldn't.
'complexactioncheck' => '->canComplexAction("MyRestrictedAction", false, 42)', 'complexactioncheck' => '->canComplexAction("MyRestrictedAction", false, 42)',
); );
} }
```
<div class="info"> <div class="info">
If the permission check fails, SilverStripe will return a `403` Forbidden HTTP status. If the permission check fails, SilverStripe will return a `403` Forbidden HTTP status.
@ -44,7 +46,8 @@ If the permission check fails, SilverStripe will return a `403` Forbidden HTTP s
An action named "index" is white listed by default, unless `allowed_actions` is defined as an empty array, or the action An action named "index" is white listed by default, unless `allowed_actions` is defined as an empty array, or the action
is specifically restricted. is specifically restricted.
:::php ```php
<?php <?php
class MyController extends Controller { class MyController extends Controller {
@ -53,10 +56,12 @@ is specifically restricted.
// allowed without an $allowed_action defined // 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
<?php <?php
class MyExtension extends Extension { class MyExtension extends Extension {
@ -65,10 +70,12 @@ is specifically restricted.
'mycustomaction' 'mycustomaction'
); );
} }
```
Only public methods can be made accessible. Only public methods can be made accessible.
:::php ```php
<?php <?php
class MyController extends Controller { class MyController extends Controller {
@ -86,10 +93,11 @@ Only public methods can be made accessible.
// .. // ..
} }
} }
```
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
<?php <?php
class MyController extends Controller { class MyController extends Controller {
@ -113,6 +121,7 @@ If a method on a parent class is overwritten, access control for it has to be re
} }
} }
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
Access checks on parent classes need to be overwritten via the [Configuration API](../configuration). Access checks on parent classes need to be overwritten via the [Configuration API](../configuration).
@ -122,8 +131,8 @@ 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
<?php <?php
class MyController extends Controller { class MyController extends Controller {
@ -140,13 +149,15 @@ as an `allowed_action`.
// .. // ..
} }
} }
```
## Action Level Checks ## Action Level Checks
Each method responding to a URL can also implement custom permission checks, e.g. to handle responses conditionally on Each method responding to a URL can also implement custom permission checks, e.g. to handle responses conditionally on
the passed request data. the passed request data.
:::php ```php
<?php <?php
class MyController extends Controller { class MyController extends Controller {
@ -163,6 +174,7 @@ the passed request data.
return 'valid'; return 'valid';
} }
} }
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
This is recommended as an addition for `$allowed_actions`, in order to handle more complex checks, rather than a This is recommended as an addition for `$allowed_actions`, in order to handle more complex checks, rather than a
@ -178,7 +190,8 @@ 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
<?php <?php
class MyController extends Controller { class MyController extends Controller {
@ -193,6 +206,7 @@ execution. This behavior can be used to implement permission checks.
} }
} }
} }
```
## Related Documentation ## Related Documentation

View File

@ -8,7 +8,9 @@ HTTP header.
**mysite/code/Page.php** **mysite/code/Page.php**
:::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/
@ -20,25 +22,30 @@ HTTP header.
$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
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 = array( private static $url_handlers = array(
'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

@ -14,7 +14,9 @@ See the [Forms Tutorial](../../tutorials/forms/) for a step by step process of c
Creating a [Form](api:SilverStripe\Forms\Form) has the following signature. Creating a [Form](api:SilverStripe\Forms\Form) has the following signature.
:::php
```php
$form = new Form( $form = new Form(
$controller, // the Controller to render this form on $controller, // the Controller to render this form on
$name, // name of the method that returns this form on the controller $name, // name of the method that returns this form on the controller
@ -22,6 +24,7 @@ Creating a [Form](api:SilverStripe\Forms\Form) has the following signature.
FieldList $actions, // list of FormAction instances FieldList $actions, // list of FormAction instances
$required // optional use of RequiredFields object $required // optional use of RequiredFields object
); );
```
In practice, this looks like: In practice, this looks like:

View File

@ -6,13 +6,17 @@ summary: Customize the generated HTML for a FormField or an entire Form.
Most markup generated in SilverStripe can be replaced by custom templates. Both [Form](api:SilverStripe\Forms\Form) and [FormField](api:SilverStripe\Forms\FormField) instances Most markup generated in SilverStripe can be replaced by custom templates. Both [Form](api:SilverStripe\Forms\Form) and [FormField](api:SilverStripe\Forms\FormField) instances
can be rendered out using custom templates using `setTemplate`. can be rendered out using custom templates using `setTemplate`.
:::php
```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.
@ -33,7 +37,9 @@ the core template structure. It is recommended to use `setTemplate` and unique t
For [FormField](api:SilverStripe\Forms\FormField) instances, there are several other templates that are used on top of the main `setTemplate`. For [FormField](api:SilverStripe\Forms\FormField) instances, there are several other templates that are used on top of the main `setTemplate`.
:::php
```php
$field = new TextField(); $field = new TextField();
$field->setTemplate('CustomTextField'); $field->setTemplate('CustomTextField');
@ -55,6 +61,7 @@ For [FormField](api:SilverStripe\Forms\FormField) instances, there are several o
// The difference here is the small field holder template is used when the // 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 // field is embedded within another field. For example, if the field is
// part of a `FieldGroup` or `CompositeField` alongside other fields. // 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
well as the available syntax, see the [Templates](../templates) documentation. well as the available syntax, see the [Templates](../templates) documentation.

View File

@ -20,25 +20,32 @@ website.
The `SecurityToken` automatically added looks something like: The `SecurityToken` automatically added looks something like:
:::php
```php
$form = new Form(..); $form = new Form(..);
echo $form->getSecurityToken()->getValue(); echo $form->getSecurityToken()->getValue();
// 'c443076989a7f24cf6b35fe1360be8683a753e2c' // '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
:::html
<input type="hidden" name="SecurityID" value="c443076989a7f24cf6b35fe1360be8683a753e2c" class="hidden" /> <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.
It can be safely disabled for `GET` requests as long as it does not modify the database (i.e a search form does not It can be safely disabled for `GET` requests as long as it does not modify the database (i.e a search form does not
normally require a security token). 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">
Do not disable the SecurityID for forms that perform some modification to the users session. This will open your Do not disable the SecurityID for forms that perform some modification to the users session. This will open your
@ -51,7 +58,9 @@ To reduce attack exposure forms are limited, by default, to the intended HTTP ve
this check, forms that rely on `GET` can be submitted via `POST` or `PUT` or vice-versa potentially leading to this check, forms that rely on `GET` can be submitted via `POST` or `PUT` or vice-versa potentially leading to
application errors or edge cases. If you need to disable this setting follow the below example: application errors or edge cases. If you need to disable this setting follow the below example:
:::php
```php
$form = new Form(..); $form = new Form(..);
$form->setFormMethod('POST'); $form->setFormMethod('POST');
@ -59,6 +68,7 @@ application errors or edge cases. If you need to disable this setting follow the
// 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

@ -9,21 +9,27 @@ when certain fields cannot be edited due to permissions. Creating the form is do
To make an entire [Form](api:SilverStripe\Forms\Form) read-only. To make an entire [Form](api:SilverStripe\Forms\Form) read-only.
:::php
```php
$form = new Form(..); $form = new Form(..);
$form->makeReadonly(); $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(..); $fields = new FieldList(..);
$fields = $fields->makeReadonly(); $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(..); $field = new TextField(..);
$field = $field->performReadonlyTransformation(); $field = $field->performReadonlyTransformation();
@ -38,16 +44,19 @@ To make a [FormField](api:SilverStripe\Forms\FormField) read-only you need to kn
$fields = new FieldList( $fields = new FieldList(
$field $field
); );
```
## Disabled FormFields ## Disabled FormFields
Disabling [FormField](api:SilverStripe\Forms\FormField) instances, sets the `disabled` property on the class. This will use the same HTML markup as Disabling [FormField](api:SilverStripe\Forms\FormField) instances, sets the `disabled` property on the class. This will use the same HTML markup as
a normal form, but set the `disabled` attribute on the `input` tag. 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

@ -21,34 +21,49 @@ display up to two levels of tabs in the interface. If you want to group data fur
## Adding a field to a tab ## Adding a field to a tab
:::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', array( $fields->addFieldsToTab('Root.Content', array(
TextField::create('Name'), TextField::create('Name'),
TextField::create('Email') TextField::create('Email')
)); ));
```
## API Documentation ## API Documentation

View File

@ -14,7 +14,9 @@ The following example will add a simple DateField to your Page, allowing you to
**mysite/code/Page.php** **mysite/code/Page.php**
:::php
```php
<?php <?php
class Page extends SiteTree { class Page extends SiteTree {
@ -34,17 +36,21 @@ The following example will add a simple DateField to your Page, allowing you to
return $fields; return $fields;
} }
} }
```
## Custom Date Format ## Custom Date Format
A custom date format for a [DateField](api:SilverStripe\Forms\DateField) can be provided through `setDateFormat`. A custom date format for a [DateField](api:SilverStripe\Forms\DateField) can be provided through `setDateFormat`.
This is only necessary if you want to opt-out of the built-in browser localisation via `type=date`. This is only necessary if you want to opt-out of the built-in browser localisation via `type=date`.
:::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">
The formats are based on [ICU format](http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details). The formats are based on [ICU format](http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details).
@ -56,10 +62,13 @@ The formats are based on [ICU format](http://www.icu-project.org/apiref/icu4c/cl
Sets the minimum and maximum allowed date values using the `min` and `max` configuration settings (in ISO format or Sets the minimum and maximum allowed date values using the `min` and `max` configuration settings (in ISO format or
`strtotime()`). `strtotime()`).
:::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
@ -67,7 +76,9 @@ It's often not immediate apparent which format a field accepts, and showing the
of limited use to the average user. An alternative is to show the current date in the desired format alongside the of limited use to the average user. An alternative is to show the current date in the desired format alongside the
field description as an example. 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
@ -79,6 +90,7 @@ field description as an example.
// 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">
Fields scaffolded through [DataObject::scaffoldCMSFields()](api:SilverStripe\ORM\DataObject::scaffoldCMSFields()) automatically have a description attached to them. Fields scaffolded through [DataObject::scaffoldCMSFields()](api:SilverStripe\ORM\DataObject::scaffoldCMSFields()) automatically have a description attached to them.

View File

@ -15,7 +15,9 @@ functionality. It is usually added through the [DataObject::getCMSFields()](api:
**mysite/code/MyObject.php** **mysite/code/MyObject.php**
:::php
```php
<?php <?php
class MyObject extends DataObject { class MyObject extends DataObject {
@ -30,6 +32,7 @@ functionality. It is usually added through the [DataObject::getCMSFields()](api:
); );
} }
} }
```
### Specify which configuration to use ### Specify which configuration to use
@ -42,7 +45,9 @@ will use the configuration with the name 'myConfig'.
You can also specify which [HtmlEditorConfig](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig) to use on a per field basis via the construct argument. You can also specify which [HtmlEditorConfig](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig) to use on a per field basis via the construct argument.
This is particularly useful if you need different configurations for multiple [HTMLEditorField](api:SilverStripe\Forms\HTMLEditor\HTMLEditorField) on the same page or form. This is particularly useful if you need different configurations for multiple [HTMLEditorField](api:SilverStripe\Forms\HTMLEditor\HTMLEditorField) on the same page or form.
:::php
```php
class MyObject extends DataObject { class MyObject extends DataObject {
private static $db = array( private static $db = array(
'Content' => 'HTMLText', 'Content' => 'HTMLText',
@ -56,6 +61,7 @@ This is particularly useful if you need different configurations for multiple [H
)); ));
} }
} }
```
In the above example, the 'Content' field will use the default 'cms' config while 'OtherContent' will be using 'myConfig'. In the above example, the 'Content' field will use the default 'cms' config while 'OtherContent' will be using 'myConfig'.
@ -85,8 +91,11 @@ You can add plugins to the editor using the Framework's [HtmlEditorConfig::enabl
transparently generate the relevant underlying TinyMCE code. transparently generate the relevant underlying TinyMCE code.
**mysite/_config.php** **mysite/_config.php**
:::php
```php
HtmlEditorConfig::get('cms')->enablePlugins('media'); HtmlEditorConfig::get('cms')->enablePlugins('media');
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
This utilities the TinyMCE's `PluginManager::load` function under the hood (check the This utilities the TinyMCE's `PluginManager::load` function under the hood (check the
@ -98,14 +107,20 @@ Plugins and advanced themes can provide additional buttons that can be added (or
configuration. Here is an example of adding a `ssmacron` button after the `charmap` button: configuration. Here is an example of adding a `ssmacron` button after the `charmap` button:
**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:
**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">
Internally [HtmlEditorConfig](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig) uses the TinyMCE's `theme_advanced_buttons` option to configure these. See the Internally [HtmlEditorConfig](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig) uses the TinyMCE's `theme_advanced_buttons` option to configure these. See the
@ -123,7 +138,9 @@ tags](http://www.tinymce.com/wiki.php/Configuration:extended_valid_elements) - t
from the HTML source by the editor. 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',
@ -136,6 +153,7 @@ from the HTML source by the editor.
'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">
The default setting for the CMS's `extended_valid_elements` we are overriding here can be found in The default setting for the CMS's `extended_valid_elements` we are overriding here can be found in
@ -148,8 +166,11 @@ It is also possible to add custom plugins to TinyMCE, for example toolbar button
You can enable them through [HtmlEditorConfig::enablePlugins()](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig::enablePlugins()): You can enable them through [HtmlEditorConfig::enablePlugins()](api:SilverStripe\Forms\HTMLEditor\HtmlEditorConfig::enablePlugins()):
**mysite/_config.php** **mysite/_config.php**
:::php
```php
HtmlEditorConfig::get('cms')->enablePlugins(array('myplugin' => '../../../mysite/javascript/myplugin/editor_plugin.js')); HtmlEditorConfig::get('cms')->enablePlugins(array('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.
@ -200,8 +221,11 @@ defaults to the stricter 'xhtml' setting, for example rendering self closing tag
In case you want to adhere to HTML4 instead, use the following configuration: 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
`<article>` or `<figure>`. If you plan to use those, add them to the `<article>` or `<figure>`. If you plan to use those, add them to the
@ -223,17 +247,24 @@ back and forth between a content representation the editor can understand, prese
Example: Remove field for "image captions" Example: Remove field for "image captions"
:::php
```php
// 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'); 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
need to add some fields to the PHP forms, as well as write some need to add some fields to the PHP forms, as well as write some
@ -258,22 +289,28 @@ encapsulated in the [ModalController](api:SilverStripe\Admin\ModalController) cl
In the CMS, those dialogs are automatically instantiate, but in your own interfaces outside In the CMS, those dialogs are automatically instantiate, but in your own interfaces outside
of the CMS you have to take care of instantiate yourself: of the CMS you have to take care of instantiate yourself:
:::php
```php
// 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
@ -293,7 +330,9 @@ Most modern browsers support it, although Internet Explorer only has limited
support in IE10. Alternatively, you can use the PSpell PHP module for server side checks. support in IE10. Alternatively, you can use the PSpell PHP module for server side checks.
Assuming you have the module installed, here's how you enable its use in `mysite/_config.php`: Assuming you have the module installed, here's how you enable its use in `mysite/_config.php`:
:::php
```php
HtmlEditorConfig::get('cms')->enablePlugins('spellchecker', 'contextmenu'); HtmlEditorConfig::get('cms')->enablePlugins('spellchecker', 'contextmenu');
HtmlEditorConfig::get('cms')->addButtonsToLine(2, 'spellchecker'); HtmlEditorConfig::get('cms')->addButtonsToLine(2, 'spellchecker');
HtmlEditorConfig::get('cms')->setOption( HtmlEditorConfig::get('cms')->setOption(
@ -301,9 +340,16 @@ Assuming you have the module installed, here's how you enable its use in `mysite
THIRDPARTY_DIR . '/tinymce-spellchecker/rpc.php' THIRDPARTY_DIR . '/tinymce-spellchecker/rpc.php'
); );
HtmlEditorConfig::get('cms')->setOption('browser_spellcheck', false); HtmlEditorConfig::get('cms')->setOption('browser_spellcheck', false);
```
Now change the default spellchecker in `framework/thirdparty/tinymce-spellchecker/config.php`: Now change the default spellchecker in `framework/thirdparty/tinymce-spellchecker/config.php`:
:::php
```php
// ... // ...
$config['general.engine'] = 'PSpell'; $config['general.engine'] = 'PSpell';
```

View File

@ -6,10 +6,11 @@ summary: How to use the GridField class for managing tabular data.
[GridField](api:SilverStripe\Forms\GridField\GridField) is SilverStripe's implementation of data grids. The main purpose of the `FormField` is to display [GridField](api:SilverStripe\Forms\GridField\GridField) is SilverStripe's implementation of data grids. The main purpose of the `FormField` is to display
tabular data in a format that is easy to view and modify. It can be thought of as a HTML table with some tricks. tabular data in a format that is easy to view and modify. It can be thought of as a HTML table with some tricks.
:::php
```php
$field = new GridField($name, $title, $list); $field = new GridField($name, $title, $list);
```
<div class="hint" markdown='1'> <div class="hint" markdown='1'>
GridField can only be used with `$list` data sets that are of the type `SS_List` such as `DataList` or `ArrayList`. GridField can only be used with `$list` data sets that are of the type `SS_List` such as `DataList` or `ArrayList`.
@ -27,7 +28,9 @@ actions such as deleting records.
**mysite/code/Page.php** **mysite/code/Page.php**
:::php
```php
<?php <?php
class Page extends SiteTree { class Page extends SiteTree {
@ -42,6 +45,7 @@ actions such as deleting records.
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
`GridField` configuration, the default configuration is an instance of [GridFieldConfig_Base](api:SilverStripe\Forms\GridField\GridFieldConfig_Base) which provides: `GridField` configuration, the default configuration is an instance of [GridFieldConfig_Base](api:SilverStripe\Forms\GridField\GridFieldConfig_Base) which provides:
@ -58,7 +62,9 @@ the `getConfig()` method on `GridField`.
**mysite/code/Page.php** **mysite/code/Page.php**
:::php
```php
<?php <?php
class Page extends SiteTree { class Page extends SiteTree {
@ -89,11 +95,11 @@ the `getConfig()` method on `GridField`.
return $fields; 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). // `GridFieldConfig::create()` will create an empty configuration (no components).
$config = GridFieldConfig::create(); $config = GridFieldConfig::create();
@ -102,31 +108,41 @@ With the `GridFieldConfig` instance, we can modify the behavior of the `GridFiel
// Update the GridField with our custom configuration // Update the GridField with our custom configuration
$gridField->setConfig($config); $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'); $config->addComponent(new GridFieldFilterHeader(), 'GridFieldDataColumns');
```
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'); $config->removeComponentsByType('GridFieldDeleteAction');
```
Fetch a component to modify it later on. Fetch a component to modify it later on.
:::php
$component = $config->getComponentByType('GridFieldFilterHeader')
```php
$component = $config->getComponentByType('GridFieldFilterHeader')
```
Here is a list of components for use bundled with the core framework. Many more components are provided by third-party Here is a list of components for use bundled with the core framework. Many more components are provided by third-party
modules and extensions. modules and extensions.
@ -152,7 +168,9 @@ developers manually adding each component.
A simple read-only and paginated view of records with sortable and searchable headers. A simple read-only and paginated view of records with sortable and searchable headers.
:::php
```php
$config = GridFieldConfig_Base::create(); $config = GridFieldConfig_Base::create();
$gridField->setConfig($config); $gridField->setConfig($config);
@ -164,6 +182,7 @@ A simple read-only and paginated view of records with sortable and searchable he
// .. new GridFieldDataColumns() // .. new GridFieldDataColumns()
// .. new GridFieldPageCount('toolbar-header-right') // .. new GridFieldPageCount('toolbar-header-right')
// .. new GridFieldPaginator($itemsPerPage) // .. new GridFieldPaginator($itemsPerPage)
```
### GridFieldConfig_RecordViewer ### GridFieldConfig_RecordViewer
@ -180,7 +199,9 @@ The `DataObject` class displayed must define a `canView()` method that returns a
this record. this record.
</div> </div>
:::php
```php
$config = GridFieldConfig_RecordViewer::create(); $config = GridFieldConfig_RecordViewer::create();
$gridField->setConfig($config); $gridField->setConfig($config);
@ -188,6 +209,7 @@ this record.
// Same as GridFieldConfig_Base with the addition of // Same as GridFieldConfig_Base with the addition of
// .. new GridFieldViewButton(), // .. new GridFieldViewButton(),
// .. new GridFieldDetailForm() // .. new GridFieldDetailForm()
```
### GridFieldConfig_RecordEditor ### GridFieldConfig_RecordEditor
@ -203,7 +225,9 @@ Permission control for editing and deleting the record uses the `canEdit()` and
`DataObject` object. `DataObject` object.
</div> </div>
:::php
```php
$config = GridFieldConfig_RecordEditor::create(); $config = GridFieldConfig_RecordEditor::create();
$gridField->setConfig($config); $gridField->setConfig($config);
@ -212,16 +236,18 @@ Permission control for editing and deleting the record uses the `canEdit()` and
// .. new GridFieldAddNewButton(), // .. new GridFieldAddNewButton(),
// .. new GridFieldEditButton(), // .. new GridFieldEditButton(),
// .. new GridFieldDeleteAction() // .. 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
:::php
$config = GridFieldConfig_RelationEditor::create(); $config = GridFieldConfig_RelationEditor::create();
$gridField->setConfig($config); $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
(`GridFieldAddExistingAutocompleter`). (`GridFieldAddExistingAutocompleter`).
@ -235,11 +261,14 @@ The `GridFieldDetailForm` component drives the record viewing and editing form.
`DataObject->getCMSFields()` method but can be customised to accept different fields via the `DataObject->getCMSFields()` method but can be customised to accept different fields via the
[GridFieldDetailForm::setFields()](api:SilverStripe\Forms\GridField\GridFieldDetailForm::setFields()) method. [GridFieldDetailForm::setFields()](api:SilverStripe\Forms\GridField\GridFieldDetailForm::setFields()) method.
:::php
```php
$form = $gridField->getConfig()->getComponentByType('GridFieldDetailForm'); $form = $gridField->getConfig()->getComponentByType('GridFieldDetailForm');
$form->setFields(new FieldList( $form->setFields(new FieldList(
new TextField('Title') new TextField('Title')
)); ));
```
### many_many_extraFields ### many_many_extraFields
@ -252,7 +281,9 @@ them as fields for relation extra data, and to avoid clashes with the other form
The namespace notation is `ManyMany[<extradata-field-name>]`, so for example `ManyMany[MyExtraField]`. The namespace notation is `ManyMany[<extradata-field-name>]`, so for example `ManyMany[MyExtraField]`.
:::php
```php
<?php <?php
class Team extends DataObject { class Team extends DataObject {
@ -303,7 +334,7 @@ The namespace notation is `ManyMany[<extradata-field-name>]`, so for example `Ma
return $fields; return $fields;
} }
} }
```
## Flexible Area Assignment through Fragments ## Flexible Area Assignment through Fragments
@ -322,10 +353,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')); $config->addComponent(new GridFieldButtonRow('after'));
$config->addComponent(new GridFieldPrintButton('buttons-after-right')); $config->addComponent(new GridFieldPrintButton('buttons-after-right'));
```
### Creating your own Fragments ### Creating your own Fragments
@ -333,7 +366,9 @@ Fragments are designated areas within a `GridField` which can be shared between
your own fragments by using a `\$DefineFragment' placeholder in your components' template. This example will simply your own fragments by using a `\$DefineFragment' placeholder in your components' template. This example will simply
create an area rendered before the table wrapped in a simple `<div>`. create an area rendered before the table wrapped in a simple `<div>`.
:::php
```php
<?php <?php
class MyAreaComponent implements GridField_HTMLProvider { class MyAreaComponent implements GridField_HTMLProvider {
@ -344,6 +379,7 @@ create an area rendered before the table wrapped in a simple `<div>`.
); );
} }
} }
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
Please note that in templates, you'll need to escape the dollar sign on `\$DefineFragment`. These are specially Please note that in templates, you'll need to escape the dollar sign on `\$DefineFragment`. These are specially
@ -353,7 +389,9 @@ processed placeholders as opposed to native template syntax.
Now you can add other components into this area by returning them as an array from your Now you can add other components into this area by returning them as an array from your
[GridFieldComponent::getHTMLFragments()](api:SilverStripe\Forms\GridField\GridFieldComponent::getHTMLFragments()) implementation: [GridFieldComponent::getHTMLFragments()](api:SilverStripe\Forms\GridField\GridFieldComponent::getHTMLFragments()) implementation:
:::php
```php
<?php <?php
class MyShareLinkComponent implements GridField_HTMLProvider { class MyShareLinkComponent implements GridField_HTMLProvider {
@ -364,11 +402,15 @@ Now you can add other components into this area by returning them as an array fr
); );
} }
} }
```
Your new area can also be used by existing components, e.g. the [GridFieldPrintButton](api:SilverStripe\Forms\GridField\GridFieldPrintButton) Your new area can also be used by existing components, e.g. the [GridFieldPrintButton](api:SilverStripe\Forms\GridField\GridFieldPrintButton)
:::php
```php
new GridFieldPrintButton('my-component-area'); new GridFieldPrintButton('my-component-area');
```
## Creating a Custom GridFieldComponent ## Creating a Custom GridFieldComponent

View File

@ -9,7 +9,9 @@ code for a `Form` is to create it as a subclass to `Form`. Let's look at a examp
**mysite/code/Page.php** **mysite/code/Page.php**
:::php
```php
<?php <?php
class PageController extends ContentController { class PageController extends ContentController {
@ -59,13 +61,16 @@ code for a `Form` is to create it as a subclass to `Form`. Let's look at a examp
.. ..
} }
```
Now that is a bit of code to include on our controller and generally makes the file look much more complex than it Now that is a bit of code to include on our controller and generally makes the file look much more complex than it
should be. Good practice would be to move this to a subclass and create a new instance for your particular controller. should be. Good practice would be to move this to a subclass and create a new instance for your particular controller.
**mysite/code/forms/SearchForm.php** **mysite/code/forms/SearchForm.php**
:::php
```php
<?php <?php
class SearchForm extends Form { class SearchForm extends Form {
@ -120,12 +125,15 @@ should be. Good practice would be to move this to a subclass and create a new in
$this->loadDataFrom($_REQUEST); $this->loadDataFrom($_REQUEST);
} }
} }
```
Our controller will now just have to create a new instance of this form object. Keeping the file light and easy to read. Our controller will now just have to create a new instance of this form object. Keeping the file light and easy to read.
**mysite/code/Page.php** **mysite/code/Page.php**
:::php
```php
<?php <?php
class PageController extends ContentController { class PageController extends ContentController {
@ -138,6 +146,7 @@ Our controller will now just have to create a new instance of this form object.
return new SearchForm($this, '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

@ -11,7 +11,9 @@ totally custom template to meet our needs. To do this, we'll provide the class w
**mysite/code/Page.php** **mysite/code/Page.php**
:::php
```php
<?php <?php
public function SearchForm() { public function SearchForm() {
@ -28,10 +30,13 @@ totally custom template to meet our needs. To do this, we'll provide the class w
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)
@ -41,6 +46,7 @@ totally custom template to meet our needs. To do this, we'll provide the class w
<% 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
properties on [Form](api:SilverStripe\Forms\Form) such as `$Fields` and `$Actions`. properties on [Form](api:SilverStripe\Forms\Form) such as `$Fields` and `$Actions`.

View File

@ -17,7 +17,9 @@ perform custom operations on a row.
A basic outline of our new `GridFieldCustomAction.php` will look like something A basic outline of our new `GridFieldCustomAction.php` will look like something
below: below:
:::php
```php
<?php <?php
class GridFieldCustomAction implements GridField_ColumnProvider, GridField_ActionProvider { class GridFieldCustomAction implements GridField_ColumnProvider, GridField_ActionProvider {
@ -32,7 +34,6 @@ below:
return array('class' => 'grid-field__col-compact'); return array('class' => 'grid-field__col-compact');
} }
public function getColumnMetadata($gridField, $columnName) { public function getColumnMetadata($gridField, $columnName) {
if($columnName == 'Actions') { if($columnName == 'Actions') {
return array('title' => ''); return array('title' => '');
@ -54,7 +55,6 @@ below:
array('RecordID' => $record->ID) array('RecordID' => $record->ID)
); );
return $field->Field(); return $field->Field();
} }
@ -74,6 +74,7 @@ below:
} }
} }
} }
```
## Add the GridFieldCustomAction to the current `GridFieldConfig` ## Add the GridFieldCustomAction to the current `GridFieldConfig`
@ -82,7 +83,9 @@ a new instance of the class to the [GridFieldConfig](api:SilverStripe\Forms\Grid
[Reference](/developer_guides/forms/field_types/gridfield) documentation has more information about [Reference](/developer_guides/forms/field_types/gridfield) documentation has more information about
manipulating the `GridFieldConfig` instance if required. 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());
@ -91,6 +94,7 @@ manipulating the `GridFieldConfig` instance if required.
// 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`
please view the [GridField Customization](/developer_guides/forms/how_tos/create_a_gridfield_actionprovider) section. please view the [GridField Customization](/developer_guides/forms/how_tos/create_a_gridfield_actionprovider) section.

View File

@ -4,7 +4,9 @@ In this how-to, we'll explain how to set up a specific page type
holding a contact form, which submits a message via email. holding a contact form, which submits a message via email.
Let's start by defining a new `ContactPage` page type: Let's start by defining a new `ContactPage` page type:
:::php
```php
<?php <?php
class ContactPage extends Page { class ContactPage extends Page {
} }
@ -22,29 +24,39 @@ Let's start by defining a new `ContactPage` page type:
return new Form($this, 'Form', $fields, $actions); return new Form($this, 'Form', $fields, $actions);
} }
} }
```
To create a form, we instanciate a `Form` object on a function on our page controller. We'll call this function `Form()`. You're free to choose this name, but it's standard practice to name the function `Form()` if there's only a single form on the page. To create a form, we instanciate a `Form` object on a function on our page controller. We'll call this function `Form()`. You're free to choose this name, but it's standard practice to name the function `Form()` if there's only a single form on the page.
There's quite a bit in this function, so we'll step through one piece at a time. There's quite a bit in this function, so we'll step through one piece at a time.
:::php
```php
$fields = new FieldList( $fields = new FieldList(
new TextField('Name'), new TextField('Name'),
new EmailField('Email'), new EmailField('Email'),
new TextareaField('Message') 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.
@ -60,7 +72,9 @@ If you now create a ContactPage in the CMS (making sure you have rebuilt the dat
Now that we have a contact form, we need some way of collecting the data submitted. We do this by creating a function on the controller with the same name as the form action. In this case, we create the function 'submit' on the ContactPage_Controller class. Now that we have a contact form, we need some way of collecting the data submitted. We do this by creating a function on the controller with the same name as the form action. In this case, we create the function 'submit' on the ContactPage_Controller class.
:::php
```php
class ContactPageController extends PageController { class ContactPageController extends PageController {
private static $allowed_actions = array('Form'); private static $allowed_actions = array('Form');
public function Form() { public function Form() {
@ -85,6 +99,7 @@ Now that we have a contact form, we need some way of collecting the data submitt
); );
} }
} }
```
<div class="hint" markdown="1"> <div class="hint" markdown="1">
Caution: This form is prone to abuse by spammers, Caution: This form is prone to abuse by spammers,
@ -106,12 +121,15 @@ All forms have some basic validation built in email fields will only let the
The framework comes with a predefined validator called [RequiredFields](api:SilverStripe\Forms\RequiredFields), which performs the common task of making sure particular fields are filled out. Below is the code to add validation to a contact form: The framework comes with a predefined validator called [RequiredFields](api:SilverStripe\Forms\RequiredFields), which performs the common task of making sure particular fields are filled out. Below is the code to add validation to a contact form:
:::php
```php
public function Form() { public function Form() {
// ... // ...
$validator = new RequiredFields('Name', 'Message'); $validator = new RequiredFields('Name', 'Message');
return new Form($this, 'Form', $fields, $actions, $validator); 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

@ -27,7 +27,9 @@ be marked `private static` and follow the `lower_case_with_underscores` structur
**mysite/code/MyClass.php** **mysite/code/MyClass.php**
:::php
```php
<?php <?php
class MyClass extends Page { class MyClass extends Page {
@ -44,17 +46,22 @@ be marked `private static` and follow the `lower_case_with_underscores` structur
// .. // ..
} }
```
## Accessing and Setting Configuration Properties ## Accessing and Setting Configuration Properties
This can be done by calling the static method [Config::inst()](api:SilverStripe\Core\Config\Config::inst()), like so: This can be done by calling the static method [Config::inst()](api:SilverStripe\Core\Config\Config::inst()), like so:
:::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
$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()`
if it's necessary to alter class config. This is generally undesirable in most applications, as modification if it's necessary to alter class config. This is generally undesirable in most applications, as modification
@ -77,17 +84,22 @@ To set those configuration options on our previously defined class we can define
**mysite/_config/app.yml** **mysite/_config/app.yml**
:::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;
@ -114,6 +126,7 @@ To use those variables in your application code:
echo implode(', ', MyClass::config()->option_one); echo implode(', ', MyClass::config()->option_one);
// returns 'Qux' // returns 'Qux'
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
There is no way currently to restrict read or write access to any configuration property, or influence/check the values There is no way currently to restrict read or write access to any configuration property, or influence/check the values
@ -189,8 +202,8 @@ 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 Name: adminroutes
After: After:
@ -201,6 +214,7 @@ The structure of each YAML file is a series of headers and values separated by Y
rules: rules:
'admin': 'AdminRootController' 'admin': 'AdminRootController'
--- ---
```
<div class="info"> <div class="info">
If there is only one set of values the header can be omitted. If there is only one set of values the header can be omitted.
@ -237,7 +251,9 @@ before (lower priority than) or after (higher priority than) some other value se
To specify these rules you add an "After" and/or "Before" key to the relevant header section. The value for these To specify these rules you add an "After" and/or "Before" key to the relevant header section. The value for these
keys is a list of reference paths to other value sections. A basic example: keys is a list of reference paths to other value sections. A basic example:
:::yml
```yml
--- ---
Name: adminroutes Name: adminroutes
After: After:
@ -248,6 +264,7 @@ keys is a list of reference paths to other value sections. A basic example:
rules: rules:
'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
out all together. Either has the same affect - that portion will be ignored when checking a value section's reference out all together. Either has the same affect - that portion will be ignored when checking a value section's reference
@ -300,7 +317,9 @@ You then list any of the following rules as sub-keys, with informational values
For instance, to add a property to "foo" when a module exists, and "bar" otherwise, you could do this: For instance, to add a property to "foo" when a module exists, and "bar" otherwise, you could do this:
:::yml
```yml
--- ---
Only: Only:
moduleexists: 'MyFineModule' moduleexists: 'MyFineModule'
@ -314,17 +333,20 @@ For instance, to add a property to "foo" when a module exists, and "bar" otherwi
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">
When you have more than one rule for a nested fragment, they're joined like When you have more than one rule for a nested fragment, they're joined like

View File

@ -10,23 +10,28 @@ throughout the site. Out of the box this includes selecting the current site the
`SiteConfig` options can be accessed from any template by using the $SiteConfig variable. `SiteConfig` options can be accessed from any template by using the $SiteConfig variable.
:::ss
```ss
$SiteConfig.Title $SiteConfig.Title
$SiteConfig.Tagline $SiteConfig.Tagline
<% with $SiteConfig %> <% with $SiteConfig %>
$Title $AnotherField $Title $AnotherField
<% end_with %> <% 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; echo $config->Title;
// returns "Website Name" // returns "Website Name"
```
## Extending SiteConfig ## Extending SiteConfig
@ -34,7 +39,9 @@ To extend the options available in the panel, define your own fields via a [Data
**mysite/code/extensions/CustomSiteConfig.php** **mysite/code/extensions/CustomSiteConfig.php**
:::php
```php
<?php <?php
class CustomSiteConfig extends DataExtension { class CustomSiteConfig extends DataExtension {
@ -49,15 +56,19 @@ To extend the options available in the panel, define your own fields via a [Data
); );
} }
} }
```
Then activate the extension. Then activate the extension.
**mysite/_config/app.yml** **mysite/_config/app.yml**
:::yml
```yml
Silverstripe\SiteConfig\SiteConfig: Silverstripe\SiteConfig\SiteConfig:
extensions: extensions:
- CustomSiteConfig - CustomSiteConfig
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
After adding the class and the YAML change, make sure to rebuild your database by visiting http://yoursite.com/dev/build. After adding the class and the YAML change, make sure to rebuild your database by visiting http://yoursite.com/dev/build.

View File

@ -17,7 +17,9 @@ and `RequestHandler`. You can still apply extensions to descendants of these cla
**mysite/code/extensions/MyMemberExtension.php** **mysite/code/extensions/MyMemberExtension.php**
:::php
```php
<?php <?php
class MyMemberExtension extends DataExtension { class MyMemberExtension extends DataExtension {
@ -31,6 +33,7 @@ and `RequestHandler`. You can still apply extensions to descendants of these cla
return "Hi " . $this->owner->Name; return "Hi " . $this->owner->Name;
} }
} }
```
<div class="info" markdown="1"> <div class="info" markdown="1">
Convention is for extension class names to end in `Extension`. This isn't a requirement but makes it clearer Convention is for extension class names to end in `Extension`. This isn't a requirement but makes it clearer
@ -41,15 +44,21 @@ we want to add the `MyMemberExtension` too. To activate this extension, add the
**mysite/_config/app.yml** **mysite/_config/app.yml**
:::yml
```yml
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'); 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
transformed the original `Member` class in two ways: transformed the original `Member` class in two ways:
@ -70,7 +79,9 @@ $has_one etc.
**mysite/code/extensions/MyMemberExtension.php** **mysite/code/extensions/MyMemberExtension.php**
:::php
```php
<?php <?php
class MyMemberExtension extends DataExtension { class MyMemberExtension extends DataExtension {
@ -88,13 +99,16 @@ $has_one etc.
return "Hi " . $this->owner->Name; 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
@ -102,18 +116,23 @@ Methods that have a unique name will be called as part of the `__call` method on
we added a `SayHi` method which is unique to our extension. 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(); $member = Security::getCurrentUser();
echo $member->SayHi; echo $member->SayHi;
// "Hi Sam" // "Hi Sam"
```
## Modifying Existing Methods ## Modifying Existing Methods
@ -123,7 +142,9 @@ through the [Object::extend()](api:Object::extend()) method.
**framework/security/Member.php** **framework/security/Member.php**
:::php
```php
public function getValidator() { public function getValidator() {
// .. // ..
@ -131,6 +152,7 @@ through the [Object::extend()](api:Object::extend()) method.
// .. // ..
} }
```
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
variables at that given point. In this case, the core function `getValidator` on the `Member` class provides an variables at that given point. In this case, the core function `getValidator` on the `Member` class provides an
@ -139,7 +161,9 @@ validator by defining the `updateValidator` method.
**mysite/code/extensions/MyMemberExtension.php** **mysite/code/extensions/MyMemberExtension.php**
:::php
```php
<?php <?php
class MyMemberExtension extends DataExtension { class MyMemberExtension extends DataExtension {
@ -151,6 +175,7 @@ validator by defining the `updateValidator` method.
$validator->addRequiredField('DateOfBirth'); $validator->addRequiredField('DateOfBirth');
} }
} }
```
<div class="info" markdown="1"> <div class="info" markdown="1">
The `$validator` parameter is passed by reference, as it is an object. The `$validator` parameter is passed by reference, as it is an object.
@ -159,7 +184,9 @@ The `$validator` parameter is passed by reference, as it is an object.
Another common example of when you will want to modify a method is to update the default CMS fields for an object in an Another common example of when you will want to modify a method is to update the default CMS fields for an object in an
extension. The `CMS` provides a `updateCMSFields` Extension Hook to tie into. extension. The `CMS` provides a `updateCMSFields` Extension Hook to tie into.
:::php
```php
<?php <?php
class MyMemberExtension extends DataExtension { class MyMemberExtension extends DataExtension {
@ -178,14 +205,16 @@ extension. The `CMS` provides a `updateCMSFields` Extension Hook to tie into.
$upload->setAllowedFileCategories('image/supported'); $upload->setAllowedFileCategories('image/supported');
} }
} }
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
If you're providing a module or working on code that may need to be extended by other code, it should provide a *hook* If you're providing a module or working on code that may need to be extended by other code, it should provide a *hook*
which allows an Extension to modify the results. which allows an Extension to modify the results.
</div> </div>
:::php
```php
public function Foo() { public function Foo() {
$foo = // .. $foo = // ..
@ -193,6 +222,7 @@ which allows an Extension to modify the results.
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
you need to provide extension hooks at the beginning of the method use `before{..}`. you need to provide extension hooks at the beginning of the method use `before{..}`.
@ -202,7 +232,9 @@ you need to provide extension hooks at the beginning of the method use `before{.
In your [Extension](api:SilverStripe\Core\Extension) class you can only refer to the source object through the `owner` property on the class as In your [Extension](api:SilverStripe\Core\Extension) class you can only refer to the source object through the `owner` property on the class as
`$this` will refer to your `Extension` instance. `$this` will refer to your `Extension` instance.
:::php
```php
<?php <?php
class MyMemberExtension extends DataExtension { class MyMemberExtension extends DataExtension {
@ -212,14 +244,14 @@ In your [Extension](api:SilverStripe\Core\Extension) class you can only refer to
var_dump($this->owner); 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 [Object::getExtensionInstances()](api:Object::getExtensionInstances()) and To see what extensions are currently enabled on an object, use [Object::getExtensionInstances()](api:Object::getExtensionInstances()) and
[Object::hasExtension()](api:Object::hasExtension()) [Object::hasExtension()](api:Object::hasExtension())
```php
:::php
$member = Security::getCurrentUser(); $member = Security::getCurrentUser();
print_r($member->getExtensionInstances()); print_r($member->getExtensionInstances());
@ -227,7 +259,7 @@ To see what extensions are currently enabled on an object, use [Object::getExten
if($member->hasExtension('MyCustomMemberExtension')) { if($member->hasExtension('MyCustomMemberExtension')) {
// .. // ..
} }
```
## Object extension injection points ## Object extension injection points
@ -245,7 +277,9 @@ require that a callback is registered each time, if necessary.
Example: A class that wants to control default values during object initialization. The code needs to assign a value Example: A class that wants to control default values during object initialization. The code needs to assign a value
if not specified in `self::$defaults`, but before extensions have been called: if not specified in `self::$defaults`, but before extensions have been called:
:::php
```php
function __construct() { function __construct() {
$self = $this; $self = $this;
@ -257,6 +291,7 @@ if not specified in `self::$defaults`, but before extensions have been called:
parent::__construct(); 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.
@ -264,7 +299,9 @@ Example 2: User code can intervene in the process of extending cms fields.
This method is preferred to disabling, enabling, and calling field extensions manually. This method is preferred to disabling, enabling, and calling field extensions manually.
</div> </div>
:::php
```php
public function getCMSFields() { public function getCMSFields() {
$this->beforeUpdateCMSFields(function($fields) { $this->beforeUpdateCMSFields(function($fields) {
@ -276,7 +313,7 @@ This method is preferred to disabling, enabling, and calling field extensions ma
// ... additional fields here // ... additional fields here
return $fields; return $fields;
} }
```
## Related Documentaion ## Related Documentaion

View File

@ -11,18 +11,18 @@ In the CMS, authors often want to insert content elements which go beyond standa
in their WYSIWYG editor. Shortcodes are a semi-technical solution for this. A good example would be embedding a 3D file in their WYSIWYG editor. Shortcodes are a semi-technical solution for this. A good example would be embedding a 3D file
viewer or a Google Map at a certain location. 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 /]
@ -30,6 +30,7 @@ Here's some syntax variations:
[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),
when rendered into a template. This means you can use shortcodes on common fields like `SiteTree.Content`, and any when rendered into a template. This means you can use shortcodes on common fields like `SiteTree.Content`, and any
@ -37,9 +38,12 @@ other [DataObject::$db](api:SilverStripe\ORM\DataObject::$db) definitions of the
Other fields can be manually parsed with shortcodes through the `parse` method. Other fields can be manually parsed with shortcodes through the `parse` method.
:::php
```php
$text = "My awesome [my_shortcode] is here."; $text = "My awesome [my_shortcode] is here.";
ShortcodeParser::get_active()->parse($text); ShortcodeParser::get_active()->parse($text);
```
## Defining Custom Shortcodes ## Defining Custom Shortcodes
@ -47,7 +51,9 @@ First we need to define a callback for the shortcode.
**mysite/code/Page.php** **mysite/code/Page.php**
:::php
```php
<?php <?php
class Page extends SiteTree { class Page extends SiteTree {
@ -60,6 +66,7 @@ First we need to define a callback for the shortcode.
return "<em>" . $tagName . "</em> " . $content . "; " . count($arguments) . " arguments."; return "<em>" . $tagName . "</em> " . $content . "; " . count($arguments) . " arguments.";
} }
} }
```
These parameters are passed to the `MyShortCodeMethod` callback: These parameters are passed to the `MyShortCodeMethod` callback:
@ -77,11 +84,13 @@ To register a shortcode you call the following.
**mysite/_config.php** **mysite/_config.php**
:::php
```php
// ShortcodeParser::get('default')->register($shortcode, $callback); // ShortcodeParser::get('default')->register($shortcode, $callback);
ShortcodeParser::get('default')->register('my_shortcode', array('Page', 'MyShortCodeMethod')); ShortcodeParser::get('default')->register('my_shortcode', array('Page', 'MyShortCodeMethod'));
```
## Built-in Shortcodes ## Built-in Shortcodes
@ -93,13 +102,19 @@ Internal page links keep references to their database IDs rather than the URL, i
against moving the target page to a different location in the page tree. This is done through the `[sitetree_link]` against moving the target page to a different location in the page tree. This is done through the `[sitetree_link]`
shortcode, which takes an `id` parameter. shortcode, which takes an `id` parameter.
:::php
```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
@ -135,15 +150,16 @@ 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]> <[paragraph]>Some test</[paragraph]>
<a [titleattribute]>link</a> <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 ```ss
<!-- Good --> <!-- Good -->
<div> <div>
[shortcode] [shortcode]
@ -159,34 +175,35 @@ you need to be careful of nesting to ensure you don't break the output.
<p> <p>
[/shortcode] [/shortcode]
</p> </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> <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> <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> <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> <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) {
// .. // ..
} }
@ -210,6 +227,7 @@ Here is a summary of the callback parameter values based on some example shortco
$enclosedContent => 'content' $enclosedContent => 'content'
$parser => ShortcodeParser instance $parser => ShortcodeParser instance
$tagName => 'my_shortcode' $tagName => 'my_shortcode'
```
## Limitations ## Limitations

View File

@ -18,35 +18,45 @@ Some of the goals of dependency injection are:
The following sums up the simplest usage of the `Injector` it creates a new object of type `MyClassName` through `create` The following sums up the simplest usage of the `Injector` it creates a new object of type `MyClassName` through `create`
:::php
```php
$object = Injector::inst()->create('MyClassName'); $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
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
The `Injector` API can be used for the singleton pattern through `get()`. Subsequent calls to `get` return the same The `Injector` API can be used for the singleton pattern through `get()`. Subsequent calls to `get` return the same
object instance as the first call. 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');
@ -54,12 +64,15 @@ object instance as the first call.
echo ($object === $object2); echo ($object === $object2);
// returns true; // returns true;
```
## Dependencies ## Dependencies
The `Injector` API can be used to define the types of `$dependencies` that an object requires. The `Injector` API can be used to define the types of `$dependencies` that an object requires.
:::php
```php
<?php <?php
class MyController extends Controller { class MyController extends Controller {
@ -76,10 +89,13 @@ The `Injector` API can be used to define the types of `$dependencies` that an ob
'permissions' => '%$PermissionService', '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); echo ($object->permissions instanceof PermissionService);
@ -87,22 +103,27 @@ When creating a new instance of `MyController` the dependencies on that class wi
echo (is_string($object->textProperty)); echo (is_string($object->textProperty));
// returns true; // 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); echo ($object->permissions instanceof MyCustomPermissionService);
@ -110,16 +131,19 @@ Now the dependencies will be replaced with our configuration.
echo ($object->textProperty == 'My Text Value'); echo ($object->textProperty == 'My Text Value');
// returns true; // returns true;
```
As well as properties, method calls can also be specified: As well as properties, method calls can also be specified:
:::yml
```yml
Injector: Injector:
Logger: Logger:
class: Monolog\Logger class: Monolog\Logger
calls: calls:
- [ pushHandler, [ %$DefaultHandler ] ] - [ pushHandler, [ %$DefaultHandler ] ]
```
## Using constants as variables ## Using constants as variables
@ -146,14 +170,19 @@ An example using the `MyFactory` service to create instances of the `MyService`
**mysite/_config/app.yml** **mysite/_config/app.yml**
:::yml
```yml
Injector: Injector:
MyService: MyService:
factory: MyFactory factory: MyFactory
```
**mysite/code/MyFactory.php** **mysite/code/MyFactory.php**
:::php
```php
<?php <?php
class MyFactory implements SilverStripe\Core\Injector\Factory { class MyFactory implements SilverStripe\Core\Injector\Factory {
@ -165,6 +194,7 @@ An example using the `MyFactory` service to create instances of the `MyService`
// 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
@ -187,7 +217,9 @@ runtime.
Assuming a class structure such as Assuming a class structure such as
:::php
```php
<?php <?php
class RestrictivePermissionService { class RestrictivePermissionService {
@ -207,10 +239,13 @@ Assuming a class structure such as
$this->password = $password; $this->password = $password;
} }
} }
```
And the following configuration.. And the following configuration..
:::yml
```yml
name: MyController name: MyController
--- ---
MyController: MyController:
@ -225,12 +260,16 @@ And the following configuration..
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
@ -247,16 +286,15 @@ named services, which may not be actual classes, and thus should not behave as t
Thus if you want an object to have the injected dependencies of a service of another name, you must Thus if you want an object to have the injected dependencies of a service of another name, you must
assign a reference to that service. assign a reference to that service.
```yaml
:::yaml
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
properties. properties.
@ -284,7 +322,9 @@ which may be later discarded, reverting the application to the original state. T
This is useful when writing test cases, as certain services may be necessary to override for a single method call. This is useful when writing test cases, as certain services may be necessary to override for a single method call.
:::php
```php
// Setup default service // Setup default service
Injector::inst()->registerService(new LiveService(), 'ServiceName'); Injector::inst()->registerService(new LiveService(), 'ServiceName');
@ -297,7 +337,7 @@ This is useful when writing test cases, as certain services may be necessary to
// revert changes // revert changes
Injector::unnest(); Injector::unnest();
```
## API Documentation ## API Documentation

View File

@ -45,7 +45,9 @@ used.
**mysite/code/MySQLWriteDbAspect.php** **mysite/code/MySQLWriteDbAspect.php**
:::php
```php
<?php <?php
class MySQLWriteDbAspect implements BeforeCallAspect { class MySQLWriteDbAspect implements BeforeCallAspect {
@ -59,7 +61,6 @@ used.
'insert','update','delete','replace' 'insert','update','delete','replace'
); );
public function beforeCall($proxied, $method, $args, &$alternateReturn) { public function beforeCall($proxied, $method, $args, &$alternateReturn) {
if (isset($args[0])) { if (isset($args[0])) {
$sql = $args[0]; $sql = $args[0];
@ -72,13 +73,16 @@ used.
} }
} }
} }
```
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`
object that's made use of above. object that's made use of above.
**mysite/_config/app.yml** **mysite/_config/app.yml**
:::yml
```yml
WriteMySQLDatabase: WriteMySQLDatabase:
class: MySQLDatabase class: MySQLDatabase
constructor: constructor:
@ -87,6 +91,7 @@ object that's made use of above.
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
of type `MySQLDatabase`, configured to point at the 'write_database'. of type `MySQLDatabase`, configured to point at the 'write_database'.
@ -95,17 +100,19 @@ Next, this should be bound into an instance of the `Aspect` class
**mysite/_config/app.yml** **mysite/_config/app.yml**
:::yml
```yml
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
:::yml
ReadMySQLDatabase: ReadMySQLDatabase:
class: MySQLDatabase class: MySQLDatabase
constructor: constructor:
@ -114,13 +121,14 @@ Next, we need to define the database connection that will be used for all non-wr
username: user username: user
password: pass password: pass
database: read_database database: read_database
```
The final piece that ties everything together is the [AopProxyService](api:SilverStripe\Core\Injector\AopProxyService) instance that will be used as the replacement The final piece that ties everything together is the [AopProxyService](api:SilverStripe\Core\Injector\AopProxyService) instance that will be used as the replacement
object when the framework creates the database connection. object when the framework creates the database connection.
**mysite/_config/app.yml** **mysite/_config/app.yml**
```yml
:::yml
MySQLDatabase: MySQLDatabase:
class: AopProxyService class: AopProxyService
properties: properties:
@ -128,6 +136,7 @@ object when the framework creates the database connection.
beforeCall: beforeCall:
query: query:
- %$MySQLWriteDbAspect - %$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.
@ -138,8 +147,8 @@ 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
:::yml
Injector: Injector:
ReadMySQLDatabase: ReadMySQLDatabase:
class: MySQLDatabase class: MySQLDatabase
@ -167,18 +176,19 @@ Overall configuration for this would look as follows
beforeCall: beforeCall:
query: query:
- %$MySQLWriteDbAspect - %$MySQLWriteDbAspect
```
## Changing what a method returns ## Changing what a method returns
One major feature of an `Aspect` is the ability to modify what is returned from the client's call to the proxied method. 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
:::php
$alternateReturn = $this->writeDb->query($sql, $code); $alternateReturn = $this->writeDb->query($sql, $code);
return false; return false;
```
By returning `false` from the `beforeCall()` method, the wrapping proxy class will_not_ call any additional `beforeCall` By returning `false` from the `beforeCall()` method, the wrapping proxy class will_not_ call any additional `beforeCall`
handlers defined for the called method. Assigning the `$alternateReturn` variable also indicates to return that value handlers defined for the called method. Assigning the `$alternateReturn` variable also indicates to return that value

View File

@ -16,7 +16,9 @@ A basic usage of a module for 3.1 that requires the CMS would look similar to
this: this:
**mycustommodule/composer.json** **mycustommodule/composer.json**
:::js
```js
{ {
"name": "your-vendor-name/module-name", "name": "your-vendor-name/module-name",
"description": "One-liner describing your module", "description": "One-liner describing your module",
@ -42,7 +44,7 @@ this:
] ]
} }
} }
```
Once your module is published online with a service like Github.com or Bitbucket.com, submit the repository to Once your module is published online with a service like Github.com or Bitbucket.com, submit the repository to
[Packagist](https://packagist.org/) to have the module accessible to developers. It'll automatically get picked [Packagist](https://packagist.org/) to have the module accessible to developers. It'll automatically get picked

View File

@ -5,15 +5,18 @@ title: How to Create a Google Maps Shortcode
To demonstrate how easy it is to build custom shortcodes, we'll build one to display a Google Map based on a provided To demonstrate how easy it is to build custom shortcodes, we'll build one to display a Google Map based on a provided
address. We want our CMS authors to be able to embed the map using the following code: address. We want our CMS authors to be able to embed the map using the following code:
:::php
```php
[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.
We'll add defaults to those in our shortcode parser so they're optional. 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) { ShortcodeParser::get('default')->register('googlemap', function($arguments, $address, $parser, $shortcode) {
$iframeUrl = sprintf( $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', 'http://maps.google.com/maps?q=%s&amp;hnear=%s&amp;ie=UTF8&hq=&amp;t=m&amp;z=14&amp;output=embed',
@ -31,3 +34,4 @@ We'll add defaults to those in our shortcode parser so they're optional.
$iframeUrl $iframeUrl
); );
}); });
```

View File

@ -9,7 +9,9 @@ often the member has visited. Or more specifically,
how often he has started a browser session, either through how often he has started a browser session, either through
explicitly logging in or by invoking the "remember me" functionality. explicitly logging in or by invoking the "remember me" functionality.
:::php
```php
<?php <?php
class MyMemberExtension extends DataExtension { class MyMemberExtension extends DataExtension {
private static $db = array( private static $db = array(
@ -42,11 +44,13 @@ explicitly logging in or by invoking the "remember me" functionality.
)); ));
} }
} }
```
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: Member:
extensions: extensions:
- MyMemberExtension - MyMemberExtension
```

View File

@ -8,7 +8,9 @@ to ensure that it works as it should. A simple example would be to test the resu
**mysite/code/Page.php** **mysite/code/Page.php**
:::php
```php
<?php <?php
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
@ -20,10 +22,13 @@ to ensure that it works as it should. A simple example would be to test the resu
return (1 + 1); return (1 + 1);
} }
} }
```
**mysite/tests/PageTest.php** **mysite/tests/PageTest.php**
:::php
```php
<?php <?php
use Page; use Page;
@ -36,6 +41,7 @@ to ensure that it works as it should. A simple example would be to test the resu
$this->assertEquals(2, Page::MyMethod()); $this->assertEquals(2, Page::MyMethod());
} }
} }
```
<div class="info" markdown="1"> <div class="info" markdown="1">
Tests for your application should be stored in the `mysite/tests` directory. Test cases for add-ons should be stored in Tests for your application should be stored in the `mysite/tests` directory. Test cases for add-ons should be stored in
@ -84,7 +90,9 @@ needs.
**phpunit.xml** **phpunit.xml**
:::xml
```xml
<phpunit bootstrap="framework/tests/bootstrap.php" colors="true"> <phpunit bootstrap="framework/tests/bootstrap.php" colors="true">
<testsuite name="Default"> <testsuite name="Default">
<directory>mysite/tests</directory> <directory>mysite/tests</directory>
@ -102,6 +110,7 @@ needs.
</exclude> </exclude>
</groups> </groups>
</phpunit> </phpunit>
```
### setUp() and tearDown() ### setUp() and tearDown()
@ -109,7 +118,9 @@ 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
<?php <?php
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
@ -142,12 +153,15 @@ end of each test.
// .. // ..
} }
} }
```
`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
each individual test case. Remember to class the parent method in each method to ensure the core boot-strapping of tests each individual test case. Remember to class the parent method in each method to ensure the core boot-strapping of tests
takes place. takes place.
:::php
```php
<?php <?php
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
@ -168,6 +182,7 @@ takes place.
// .. // ..
} }
} }
```
### Config and Injector Nesting ### Config and Injector Nesting
@ -179,7 +194,9 @@ If you need to make changes to `Config` (or `Injector`) for each test (or the wh
It's important to remember that the `parent::setUp();` functions will need to be called first to ensure the nesting feature works as expected. It's important to remember that the `parent::setUp();` functions will need to be called first to ensure the nesting feature works as expected.
:::php
```php
public static function setUpBeforeClass() public static function setUpBeforeClass()
{ {
parent::setUpBeforeClass(); parent::setUpBeforeClass();
@ -197,6 +214,7 @@ It's important to remember that the `parent::setUp();` functions will need to be
{ {
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,37 +8,48 @@ 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
of the response. 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
of the response. of the response.
## Submit ## Submit
:::php
```php
$submit = $this->submitForm($formID, $button = null, $data = array()); $submit = $this->submitForm($formID, $button = null, $data = array());
```
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).
## LogInAs ## LogInAs
:::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
@ -46,10 +57,13 @@ The `FunctionalTest` class also provides additional asserts to validate your tes
### assertPartialMatchBySelector ### assertPartialMatchBySelector
:::php
```php
$this->assertPartialMatchBySelector('p.good',array( $this->assertPartialMatchBySelector('p.good',array(
'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
selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The
@ -58,21 +72,25 @@ assertion fails if one of the expectedMatches fails to appear.
### assertExactMatchBySelector ### assertExactMatchBySelector
:::php
```php
$this->assertExactMatchBySelector("#MyForm_ID p.error", array( $this->assertExactMatchBySelector("#MyForm_ID p.error", array(
"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
selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The
assertion fails if one of the expectedMatches fails to appear. assertion fails if one of the expectedMatches fails to appear.
### assertPartialHTMLMatchBySelector ### assertPartialHTMLMatchBySelector
```php
:::php
$this->assertPartialHTMLMatchBySelector("#MyForm_ID p.error", array( $this->assertPartialHTMLMatchBySelector("#MyForm_ID p.error", array(
"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
selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The
@ -83,11 +101,12 @@ assertion fails if one of the expectedMatches fails to appear.
</div> </div>
### assertExactHTMLMatchBySelector ### assertExactHTMLMatchBySelector
```php
:::php
$this->assertExactHTMLMatchBySelector("#MyForm_ID p.error", array( $this->assertExactHTMLMatchBySelector("#MyForm_ID p.error", array(
"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
selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The

View File

@ -14,7 +14,9 @@ To include your fixture file in your tests, you should define it as your `$fixtu
**mysite/tests/MyNewTest.php** **mysite/tests/MyNewTest.php**
:::php
```php
<?php <?php
class MyNewTest extends SapphireTest { class MyNewTest extends SapphireTest {
@ -22,13 +24,16 @@ To include your fixture file in your tests, you should define it as your `$fixtu
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:
**mysite/tests/MyNewTest.php** **mysite/tests/MyNewTest.php**
:::php
```php
<?php <?php
class MyNewTest extends SapphireTest { class MyNewTest extends SapphireTest {
@ -39,13 +44,16 @@ You can also use an array of fixture files, if you want to use parts of multiple
); );
} }
```
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
<?php <?php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
@ -72,12 +80,15 @@ ideal for fixture generation. Say we have the following two DataObjects:
'Players' => 'Player' '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
@ -95,6 +106,7 @@ We can represent multiple instances of them in `YAML` as follows:
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.
@ -102,8 +114,11 @@ indentation, `Player` and `Team`, represent the class names of the objects we wa
The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are **identifiers**. Each identifier you specify 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.
@ -131,7 +146,9 @@ This style of relationship declaration can be used for any type of relationship
We can also declare the relationships conversely. Another way we could write the previous example is: We can also declare the relationships conversely. Another way we could write the previous example is:
:::yml
```yml
Player: Player:
john: john:
Name: John Name: John
@ -148,12 +165,15 @@ We can also declare the relationships conversely. Another way we could write the
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(array( $team = new Team(array(
'Name' => 'Hurricanes', 'Name' => 'Hurricanes',
'Origin' => 'Wellington' 'Origin' => 'Wellington'
@ -162,6 +182,7 @@ writing:
$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
@ -172,7 +193,9 @@ test.
As of SilverStripe 4 you will need to use fully qualfied class names in your YAML fixture files. In the above examples, they belong to the global namespace so there is nothing requires, but if you have a deeper DataObject, or it has a relationship to models that are part of the framework for example, you will need to include their namespaces: As of SilverStripe 4 you will need to use fully qualfied class names in your YAML fixture files. In the above examples, they belong to the global namespace so there is nothing requires, but if you have a deeper DataObject, or it has a relationship to models that are part of the framework for example, you will need to include their namespaces:
:::yml
```yml
MyProject\Model\Player: MyProject\Model\Player:
john: john:
Name: join Name: join
@ -182,6 +205,7 @@ As of SilverStripe 4 you will need to use fully qualfied class names in your YAM
Name: Crusaders Name: Crusaders
Origin: Canterbury Origin: Canterbury
Players: =>MyProject\Model\Player.john Players: =>MyProject\Model\Player.john
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
If your tests are failing and your database has table names that follow the fully qualified class names, you've probably forgotten to implement `private static $table_name = 'Player';` on your namespaced class. This property was introduced in SilverStripe 4 to reduce data migration work. See [DataObject](api:SilverStripe\ORM\DataObject) for an example. If your tests are failing and your database has table names that follow the fully qualified class names, you've probably forgotten to implement `private static $table_name = 'Player';` on your namespaced class. This property was introduced in SilverStripe 4 to reduce data migration work. See [DataObject](api:SilverStripe\ORM\DataObject) for an example.
@ -192,7 +216,9 @@ If your tests are failing and your database has table names that follow the full
`many_many` relations can have additional database fields attached to the relationship. For example we may want to `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 class Player extends DataObject
@ -222,10 +248,13 @@ declare the role each player has in the team.
) )
); );
} }
```
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: Player:
john: john:
Name: John Name: John
@ -247,6 +276,7 @@ To provide the value for the `many_many_extraField` use the YAML list syntax.
Role: Captain Role: Captain
- =>Player.jack: - =>Player.jack:
Role: Winger Role: Winger
```
## Fixture Factories ## Fixture Factories
@ -266,17 +296,23 @@ 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'); $factory = Injector::inst()->create('FixtureFactory');
$obj = $factory->createObject('Team', 'hurricanes'); $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', array( $obj = $factory->createObject('Team', 'hurricanes', array(
'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
@ -285,26 +321,33 @@ mapped to their database identifiers.
After we've created this object in the factory, `getId` is used to retrieve it by the identifier. After we've created this object in the factory, `getId` is used to retrieve it by the identifier.
:::php
$databaseId = $factory->getId('Team', 'hurricanes');
```php
$databaseId = $factory->getId('Team', 'hurricanes');
```
### Default Properties ### Default Properties
Blueprints can be overwritten in order to customise their behavior. For example, if a Fixture does not provide a Team Blueprints can be overwritten in order to customise their behavior. For example, if a Fixture does not provide a Team
name, we can set the default to be `Unknown Team`. name, we can set the default to be `Unknown Team`.
:::php
```php
$factory->define('Team', array( $factory->define('Team', array(
'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', array( $factory->define('Member', array(
'Email' => function($obj, $data, $fixtures) { 'Email' => function($obj, $data, $fixtures) {
if(isset($data['FirstName']) { if(isset($data['FirstName']) {
@ -315,23 +358,29 @@ values based on other fixture data.
$obj->Score = rand(0,10); $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', array( $obj = $factory->createObject('Team', 'hurricanes', array(
'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) {
@ -339,6 +388,7 @@ publish a page, which requires a method call.
}); });
$page = $factory->define('Page', $blueprint); $page = $factory->define('Page', $blueprint);
```
Available callbacks: Available callbacks:
@ -351,7 +401,9 @@ Data of the same type can have variations, for example forum members vs. CMS adm
class, but have completely different properties. This is where named blueprints come in. By default, blueprint names 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');
@ -366,6 +418,7 @@ equal the class names they manage.
$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

View File

@ -7,7 +7,9 @@ how you can load default records into the test database.
**mysite/tests/PageTest.php** **mysite/tests/PageTest.php**
:::php
```php
<?php <?php
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
@ -44,6 +46,7 @@ how you can load default records into the test database.
} }
} }
} }
```
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,
represented as a YAML [Fixture](../fixtures). When our test is run, the data from this file will be loaded into a test represented as a YAML [Fixture](../fixtures). When our test is run, the data from this file will be loaded into a test

View File

@ -9,7 +9,9 @@ response and modify the session within a test.
**mysite/tests/HomePageTest.php** **mysite/tests/HomePageTest.php**
:::php
```php
<?php <?php
class HomePageTest extends FunctionalTest { class HomePageTest extends FunctionalTest {
@ -45,6 +47,7 @@ response and modify the session within a test.
)); ));
} }
} }
```
## Related Documentation ## Related Documentation

View File

@ -8,7 +8,9 @@ see the [Fixtures](../fixtures) documentation.
In this how to we'll use a `FixtureFactory` and a custom blue print for giving us a shortcut for creating new objects In this how to we'll use a `FixtureFactory` and a custom blue print for giving us a shortcut for creating new objects
with information that we need. with information that we need.
:::php
```php
class MyObjectTest extends SapphireTest { class MyObjectTest extends SapphireTest {
protected $factory; protected $factory;
@ -39,6 +41,7 @@ with information that we need.
// returns "My Custom Value" // returns "My Custom Value"
} }
} }
```
## Related Documentation ## Related Documentation

View File

@ -6,7 +6,9 @@ SilverStripe's test system has built-in support for testing emails sent using th
running a [SapphireTest](api:SilverStripe\Dev\SapphireTest) test, then it holds off actually sending the email, and instead lets you assert that an running a [SapphireTest](api:SilverStripe\Dev\SapphireTest) test, then it holds off actually sending the email, and instead lets you assert that an
email was sent using this method. email was sent using this method.
:::php
```php
public function MyMethod() { public function MyMethod() {
$e = new Email(); $e = new Email();
$e->To = "someone@example.com"; $e->To = "someone@example.com";
@ -14,15 +16,18 @@ email was sent using this method.
$e->Body = "I just really wanted to email you and say hi."; $e->Body = "I just really wanted to email you and say hi.";
$e->send(); $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

@ -9,14 +9,17 @@ and behaviors. The environment is managed either through a [YML configuration fi
The definition of setting an environment type in a `mysite/_config/app.yml` looks like The definition of setting an environment type in a `mysite/_config/app.yml` looks like
:::yml
```yml
SilverStripe\Control\Director: SilverStripe\Control\Director:
environment_type: 'dev' environment_type: 'dev'
```
The definition of setting an environment type in a `.env` file looks like The definition of setting an environment type in a `.env` file looks like
```
SS_ENVIRONMENT_TYPE="dev" SS_ENVIRONMENT_TYPE="dev"
```
The three environment types you can set are `dev`, `test` and `live`. The three environment types you can set are `dev`, `test` and `live`.
### Dev ### Dev
@ -38,13 +41,16 @@ Test mode is designed for staging environments or other private collaboration si
In this mode error messages are hidden from the user and SilverStripe includes [BasicAuth](api:SilverStripe\Security\BasicAuth) integration if you In this mode error messages are hidden from the user and SilverStripe includes [BasicAuth](api:SilverStripe\Security\BasicAuth) integration if you
want to password protect the site. You can enable that by adding this to your `mysite/_config/app.yml` file: want to password protect the site. You can enable that by adding this to your `mysite/_config/app.yml` file:
:::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
@ -60,6 +66,7 @@ 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
--- ---
Only: Only:
environment: 'live' environment: 'live'
@ -72,11 +79,11 @@ You can check for the current environment type in [config files](../configuratio
--- ---
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()) { if (Director::isLive()) {
// is in live // is in live
} elseif (Director::isTest()) { } elseif (Director::isTest()) {
@ -84,4 +91,6 @@ certain functionality depending on the environment type.
} elseif (Director::isDev()) { } elseif (Director::isDev()) {
// is in dev mode // is in dev mode
} }
```

View File

@ -10,10 +10,11 @@ to track down a template or two. The template engine can help you along by displ
source code comments indicating which template is responsible for rendering each source code comments indicating which template is responsible for rendering each
block of html on your page. block of html on your page.
:::yaml ```yaml
--- ---
Only: Only:
environment: 'dev' environment: 'dev'
--- ---
SSViewer: SSViewer:
source_file_comments: true source_file_comments: true
```

View File

@ -4,13 +4,13 @@ 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
:::ss
<% cached 'CacheKey' %> <% 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.
Every time the cache key returns a different result, the contents of the block are recalculated. If the cache key is Every time the cache key returns a different result, the contents of the block are recalculated. If the cache key is
@ -21,7 +21,9 @@ will invalidate the cache after a given amount of time has expired (default 10 m
Here are some more complex examples: Here are some more complex examples:
:::ss
```ss
<% cached 'database', $LastEdited %> <% cached 'database', $LastEdited %>
<!-- that updates every time the record changes. --> <!-- that updates every time the record changes. -->
<% end_cached %> <% end_cached %>
@ -33,6 +35,7 @@ Here are some more complex examples:
<% 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
`$CurrentReadingMode, $CurrentUser.ID`. This ensures that the current [Versioned](api:SilverStripe\Versioned\Versioned) state and user ID are used. `$CurrentReadingMode, $CurrentUser.ID`. This ensures that the current [Versioned](api:SilverStripe\Versioned\Versioned) state and user ID are used.
@ -44,10 +47,12 @@ user does not influence your template content, you can update this key as below;
**mysite/_config/app.yml** **mysite/_config/app.yml**
:::yaml
```yaml
SSViewer: SSViewer:
global_key: '$CurrentReadingMode, $Locale' global_key: '$CurrentReadingMode, $Locale'
```
## Aggregates ## Aggregates
@ -58,16 +63,22 @@ on sets of [DataObject](api:SilverStripe\ORM\DataObject)s - the most useful for
For example, if we have a menu, we want that menu to update whenever _any_ page is edited, but would like to cache it For example, if we have a menu, we want that menu to update whenever _any_ page is edited, but would like to cache it
otherwise. By using aggregates, we do that like this: 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.
If we have a block that shows a list of categories, we can make sure the cache updates every time a category is added If we have a block that shows a list of categories, we can make sure the cache updates every time a category is added
or edited 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">
Note the use of both `.max('LastEdited')` and `.count()` - this takes care of both the case where an object has been Note the use of both `.max('LastEdited')` and `.count()` - this takes care of both the case where an object has been
@ -85,7 +96,9 @@ fragment.
For example, a block that shows a collection of rotating slides needs to update whenever the relationship For example, a block that shows a collection of rotating slides needs to update whenever the relationship
`Page::$many_many = array('Slides' => 'Slide')` changes. In `PageController`: `Page::$many_many = array('Slides' => 'Slide')` changes. In `PageController`:
:::php
```php
public function SliderCacheKey() { public function SliderCacheKey() {
$fragments = array( $fragments = array(
@ -97,11 +110,15 @@ For example, a block that shows a collection of rotating slides needs to update
); );
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.
@ -119,24 +136,31 @@ data updates.
For instance, if we show some blog statistics, but are happy having them be slightly stale, we could do For instance, if we show some blog statistics, but are happy having them be slightly stale, we could do
:::ss
<% cached 'blogstatistics', $Blog.ID %>
```ss
<% cached 'blogstatistics', $Blog.ID %>
```
which will invalidate after the cache lifetime expires. If you need more control than that (cache lifetime is which will invalidate after the cache lifetime expires. If you need more control than that (cache lifetime is
configurable only on a site-wide basis), you could add a special function to your controller: configurable only on a site-wide basis), you could add a special function to your controller:
:::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
} }
```
and then use it in the cache key and then use it in the cache key
:::ss
<% cached 'blogstatistics', $Blog.ID, $BlogStatisticsCounter %>
```ss
<% cached 'blogstatistics', $Blog.ID, $BlogStatisticsCounter %>
```
## Cache block conditionals ## Cache block conditionals
@ -147,17 +171,22 @@ value must be true for that block to be cached. Conversely if 'unless' is used,
Following on from the previous example, you might wish to only cache slightly-stale data if the server is experiencing Following on from the previous example, you might wish to only cache slightly-stale data if the server is experiencing
heavy load: heavy load:
:::ss
<% cached 'blogstatistics', $Blog.ID if $HighLoad %>
```ss
<% cached 'blogstatistics', $Blog.ID if $HighLoad %>
```
By adding a `HighLoad` function to your `PageController`, you could enable or disable caching dynamically. By adding a `HighLoad` function to your `PageController`, you could enable or disable caching dynamically.
To cache the contents of a page for all anonymous users, but dynamically calculate the contents for logged in members, To cache the contents of a page for all anonymous users, but dynamically calculate the contents for logged in members,
use something like: use something like:
:::ss
```ss
<% cached unless $CurrentUser %> <% cached unless $CurrentUser %>
```
## Uncached ## Uncached
@ -165,9 +194,11 @@ The template tag 'uncached' can be used - it is the exact equivalent of a cached
returns false. The key and conditionals in an uncached tag are ignored, so you can easily temporarily disable a returns false. The key and conditionals in an uncached tag are ignored, so you can easily temporarily disable a
particular cache block by changing just the tag, leaving the key and conditional intact. particular cache block by changing just the tag, leaving the key and conditional intact.
:::ss
<% uncached %>
```ss
<% uncached %>
```
## Nested cache blocks ## Nested cache blocks
@ -179,7 +210,9 @@ portion dynamic, without having to include any member info in the page's cache k
An example: An example:
:::ss
```ss
<% cached $LastEdited %> <% cached $LastEdited %>
Our wonderful site Our wonderful site
@ -189,7 +222,7 @@ An example:
$ASlowCalculation $ASlowCalculation
<% end_cached %> <% 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
message depending on the logged in member. message depending on the logged in member.
@ -197,7 +230,9 @@ message depending on the logged in member.
Cache conditionals and the uncached tag also work in the same nested manner. Since Member.Name is fast to calculate, you Cache conditionals and the uncached tag also work in the same nested manner. Since Member.Name is fast to calculate, you
could also write the last example as: could also write the last example as:
:::ss
```ss
<% cached $LastEdited %> <% cached $LastEdited %>
Our wonderful site Our wonderful site
@ -207,6 +242,7 @@ could also write the last example as:
$ASlowCalculation $ASlowCalculation
<% end_cached %> <% end_cached %>
```
<div class="warning" markdown="1"> <div class="warning" markdown="1">
Currently a nested cache block can not be contained within an if or loop block. The template engine will throw an error Currently a nested cache block can not be contained within an if or loop block. The template engine will throw an error
@ -215,7 +251,9 @@ letting you know if you've done this. You can often get around this using aggreg
Failing example: Failing example:
:::ss
```ss
<% cached $LastEdited %> <% cached $LastEdited %>
<% loop $Children %> <% loop $Children %>
@ -225,10 +263,13 @@ Failing example:
<% 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') %>
@ -238,10 +279,13 @@ Can be re-written as:
<% end_cached %> <% end_cached %>
<% end_cached %> <% end_cached %>
```
Or: Or:
:::ss
```ss
<% cached $LastEdited %> <% cached $LastEdited %>
(other code) (other code)
<% end_cached %> <% end_cached %>
@ -251,6 +295,7 @@ Or:
$Name $Name
<% end_cached %> <% end_cached %>
<% end_loop %> <% end_loop %>
```
## Cache expiry ## Cache expiry
@ -261,6 +306,11 @@ logic is sound, you could increase the expiry dramatically.
**mysite/_config.php** **mysite/_config.php**
:::php
```php
// Set partial cache expiry to 7 days // Set partial cache expiry to 7 days
SS_Cache::set_cache_lifetime('cacheblock', 60 * 60 * 24 * 7); SS_Cache::set_cache_lifetime('cacheblock', 60 * 60 * 24 * 7);
```

View File

@ -31,20 +31,26 @@ but also exposes caches following the PSR-16 interface.
Cache objects are configured via YAML Cache objects are configured via YAML
and SilverStripe's [dependency injection](/developer-guides/extending/injector) system. and SilverStripe's [dependency injection](/developer-guides/extending/injector) system.
:::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),
which determines which cache adapter is used (see "Adapters" below for details). which determines which cache adapter is used (see "Adapters" below for details).
This factory allows us you to globally define an adapter for all cache instances. 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'); $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.
In our example, the namespace is "myCache", expressed in the service name as In our example, the namespace is "myCache", expressed in the service name as
@ -59,10 +65,13 @@ service doesn't support this. See "Invalidation" for alternative strategies.
Cache objects follow the [PSR-16](http://www.php-fig.org/psr/psr-16/) class interface. Cache objects follow the [PSR-16](http://www.php-fig.org/psr/psr-16/) class interface.
:::php
```php
use Psr\SimpleCache\CacheInterface use Psr\SimpleCache\CacheInterface
$cache = Injector::inst()->get(CacheInterface::class . '.myCache'); $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');
@ -73,6 +82,7 @@ Cache objects follow the [PSR-16](http://www.php-fig.org/psr/psr-16/) class inte
if (!$cache->has('myCacheKey')) { if (!$cache->has('myCacheKey')) {
// ... item does not exists in the cache // ... item does not exists in the cache
} }
```
## Invalidation ## Invalidation
@ -80,30 +90,40 @@ Caches can be invalidated in different ways. The easiest is to actively clear th
entire cache. If the adapter supports namespaced cache clearing, entire cache. If the adapter supports namespaced cache clearing,
this will only affect a subset of cache keys ("myCache" in this example): 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'); $cache = Injector::inst()->get(CacheInterface::class . '.myCache');
// remove all items in this (namespaced) cache // remove all items in this (namespaced) cache
$cache->clear(); $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'); $cache = Injector::inst()->get(CacheInterface::class . '.myCache');
// remove the cache item // remove the cache item
$cache->delete('myCacheKey'); $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'); $cache = Injector::inst()->get(CacheInterface::class . '.myCache');
// remove the cache item // remove the cache item
$cache->set('myCacheKey', 'myValue', 300); // cache for 300 seconds $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.
In order to increase the chance of your cache actually being hit, In order to increase the chance of your cache actually being hit,
@ -112,11 +132,14 @@ You can also set your lifetime to `0`, which means they won't expire.
Since many adapters don't have a way to actively remove expired caches, Since many adapters don't have a way to actively remove expired caches,
you need to be careful with resources here (e.g. filesystem space). 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.
For example, including the `LastEdited` value when caching `DataObject` results For example, including the `LastEdited` value when caching `DataObject` results
@ -125,13 +148,16 @@ The following example caches a member's group names, and automatically
creates a new cache key when any group is edited. Depending on the used adapter, creates a new cache key when any group is edited. Depending on the used adapter,
old cache keys will be garbage collected as the cache fills up. 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'); $cache = Injector::inst()->get(CacheInterface::class . '.myCache');
// Automatically changes when any group is edited // Automatically changes when any group is edited
$cacheKey = implode(['groupNames', $member->ID, Groups::get()->max('LastEdited')]); $cacheKey = implode(['groupNames', $member->ID, Groups::get()->max('LastEdited')]);
$cache->set($cacheKey, $member->Groups()->column('Title')); $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
any classes that implement the [Flushable](/developer_guides/execution_pipeline/flushable/) any classes that implement the [Flushable](/developer_guides/execution_pipeline/flushable/)
@ -163,7 +189,9 @@ Example: Configure core caches to use [memcached](http://www.danga.com/memcached
which requires the [memcached PHP extension](http://php.net/memcached), which requires the [memcached PHP extension](http://php.net/memcached),
and takes a `MemcachedClient` instance as a constructor argument. and takes a `MemcachedClient` instance as a constructor argument.
:::yml
```yml
--- ---
After: After:
- '#corecache' - '#corecache'
@ -177,6 +205,7 @@ and takes a `MemcachedClient` instance as a constructor argument.
class: 'SilverStripe\Core\Cache\MemcachedCacheFactory' class: 'SilverStripe\Core\Cache\MemcachedCacheFactory'
constructor: constructor:
client: '%$MemcachedClient client: '%$MemcachedClient
```
## Additional Caches ## Additional Caches

View File

@ -17,17 +17,21 @@ headers:
## Customizing Cache Headers ## Customizing Cache Headers
### HTTP::set_cache_age ### HTTP::set_cache_age
```php
:::php
HTTP::set_cache_age(0); 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"
clause in `Cache-Control` and `Pragma` will be included. clause in `Cache-Control` and `Pragma` will be included.
### HTTP::register_modification_date ### HTTP::register_modification_date
:::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
[HTTP::register_modification_date(](api:SilverStripe\Control\HTTP::register_modification_date() whenever a record comes from the database ensuring the newest date is present. [HTTP::register_modification_date(](api:SilverStripe\Control\HTTP::register_modification_date() whenever a record comes from the database ensuring the newest date is present.

View File

@ -23,7 +23,8 @@ SilverStripe can request more resources through `Environment::increaseMemoryLimi
`Environment::increaseTimeLimitTo()` functions. `Environment::increaseTimeLimitTo()` functions.
</div> </div>
:::php ```php
public function myBigFunction() { public function myBigFunction() {
Environment::increaseTimeLimitTo(400); Environment::increaseTimeLimitTo(400);
} }
```

View File

@ -15,26 +15,30 @@ The [Member](api:SilverStripe\Security\Member) class comes with 2 static methods
Retrieves the ID (int) of the current logged in member. Returns *0* if user is not logged in. Much lighter than the Retrieves the ID (int) of the current logged in member. Returns *0* if user is not logged in. Much lighter than the
next method for testing if you just need to test. next method for testing if you just need to test.
:::php
```php
// Is a member logged in? // Is a member logged in?
if( Member::currentUserID() ) { if( Member::currentUserID() ) {
// Yes! // Yes!
} else { } else {
// No! // No!
} }
```
**Security::getCurrentUser()** **Security::getCurrentUser()**
Returns the full *Member* Object for the current user, returns *null* if user is not logged in. Returns the full *Member* Object for the current user, returns *null* if user is not logged in.
:::php
```php
if( $member = Security::getCurrentUser() ) { if( $member = Security::getCurrentUser() ) {
// Work with $member // Work with $member
} else { } else {
// Do non-member stuff // Do non-member stuff
} }
```
## Subclassing ## Subclassing
@ -45,20 +49,25 @@ 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
class MyMember extends Member { class MyMember extends Member {
private static $db = array( private static $db = array(
"Age" => "Int", "Age" => "Int",
"Address" => "Text", "Address" => "Text",
); );
} }
```
To ensure that all new members are created using this class, put a call to [Object::useCustomClass()](api:Object::useCustomClass()) in To ensure that all new members are created using this class, put a call to [Object::useCustomClass()](api:Object::useCustomClass()) in
(project)/_config.php: (project)/_config.php:
:::php
```php
Object::useCustomClass("Member", "MyMember"); Object::useCustomClass("Member", "MyMember");
```
Note that if you want to look this class-name up, you can call Object::getCustomClass("Member") Note that if you want to look this class-name up, you can call Object::getCustomClass("Member")
@ -68,7 +77,9 @@ If you overload the built-in public function getCMSFields(), then you can change
details in the newsletter system. This function returns a [FieldList](api:SilverStripe\Forms\FieldList) object. You should generally start by calling details in the newsletter system. This function returns a [FieldList](api:SilverStripe\Forms\FieldList) object. You should generally start by calling
parent::getCMSFields() and manipulate the [FieldList](api:SilverStripe\Forms\FieldList) from there. parent::getCMSFields() and manipulate the [FieldList](api:SilverStripe\Forms\FieldList) from there.
:::php
```php
public function getCMSFields() { public function getCMSFields() {
$fields = parent::getCMSFields(); $fields = parent::getCMSFields();
$fields->insertBefore("HTMLEmail", new TextField("Age")); $fields->insertBefore("HTMLEmail", new TextField("Age"));
@ -76,7 +87,7 @@ parent::getCMSFields() and manipulate the [FieldList](api:SilverStripe\Forms\Fie
$fields->removeByName("Organisation"); $fields->removeByName("Organisation");
return $fields; return $fields;
} }
```
## Extending Member or DataObject? ## Extending Member or DataObject?
@ -93,16 +104,21 @@ Using inheritance to add extra behaviour or data fields to a member is limiting,
class. A better way is to use role extensions to add this behaviour. Add the following to your class. A better way is to use role extensions to add this behaviour. Add the following to your
`[config.yml](/developer_guides/configuration/configuration/#configuration-yaml-syntax-and-rules)`. `[config.yml](/developer_guides/configuration/configuration/#configuration-yaml-syntax-and-rules)`.
:::yml
```yml
Member: Member:
extensions: extensions:
- MyMemberExtension - MyMemberExtension
```
A role extension is simply a subclass of [DataExtension](api:SilverStripe\ORM\DataExtension) that is designed to be used to add behaviour to [Member](api:SilverStripe\Security\Member). A role extension is simply a subclass of [DataExtension](api:SilverStripe\ORM\DataExtension) that is designed to be used to add behaviour to [Member](api:SilverStripe\Security\Member).
The roles affect the entire class - all members will get the additional behaviour. However, if you want to restrict The roles affect the entire class - all members will get the additional behaviour. However, if you want to restrict
things, you should add appropriate [Permission::checkMember()](api:SilverStripe\Security\Permission::checkMember()) calls to the role's methods. things, you should add appropriate [Permission::checkMember()](api:SilverStripe\Security\Permission::checkMember()) calls to the role's methods.
:::php
```php
class MyMemberExtension extends DataExtension { class MyMemberExtension extends DataExtension {
/** /**
@ -126,6 +142,7 @@ things, you should add appropriate [Permission::checkMember()](api:SilverStripe\
// You can add any other methods you like, which you can call directly on the member object. // You can add any other methods you like, which you can call directly on the member object.
} }
} }
```
## Saved User Logins ## ## Saved User Logins ##
@ -153,7 +170,9 @@ reasonably be expected to be allowed to do.
E.g. E.g.
:::php
```php
class CleanRecordsTask extends BuildTask class CleanRecordsTask extends BuildTask
{ {
public function run($request) public function run($request)
@ -166,7 +185,7 @@ E.g.
DataRecord::get()->filter('Dirty', true)->removeAll(); DataRecord::get()->filter('Dirty', true)->removeAll();
}); });
} }
```
## API Documentation ## API Documentation

View File

@ -38,11 +38,11 @@ CMS access for the first time. SilverStripe provides a default admin configurati
and password to be configured for a single special user outside of the normal membership system. 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.

View File

@ -25,8 +25,8 @@ must still be taken when working with literal values or table/column identifiers
come from user input. come from user input.
Example: Example:
```php
:::php
$records = DB::prepared_query('SELECT * FROM "MyClass" WHERE "ID" = ?', array(3)); $records = DB::prepared_query('SELECT * FROM "MyClass" WHERE "ID" = ?', array(3));
$records = MyClass::get()->where(array('"ID" = ?' => 3)); $records = MyClass::get()->where(array('"ID" = ?' => 3));
$records = MyClass::get()->where(array('"ID"' => 3)); $records = MyClass::get()->where(array('"ID"' => 3));
@ -34,10 +34,13 @@ Example:
$records = DataObject::get_one('MyClass', array('"ID" = ?' => 3)); $records = DataObject::get_one('MyClass', array('"ID" = ?' => 3));
$records = MyClass::get()->byID(3); $records = MyClass::get()->byID(3);
$records = SQLSelect::create()->addWhere(array('"ID"' => 3))->execute(); $records = SQLSelect::create()->addWhere(array('"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"') SQLInsert::create('"MyClass"')
->assign('"Name"', 'Daniel') ->assign('"Name"', 'Daniel')
->addAssignments(array( ->addAssignments(array(
@ -52,7 +55,7 @@ Parameterised updates and inserts are also supported, but the syntax is a little
'INSERT INTO "MyClass" ("Name", "Position", "Age", "Created") VALUES(?, ?, GREATEST(0,?,?), NOW())' 'INSERT INTO "MyClass" ("Name", "Position", "Age", "Created") VALUES(?, ?, GREATEST(0,?,?), NOW())'
array('Daniel', 'Accountant', 24, 28) array('Daniel', 'Accountant', 24, 28)
); );
```
### Automatic escaping ### Automatic escaping
@ -76,8 +79,8 @@ 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
:::php
// automatically escaped/quoted // automatically escaped/quoted
$members = Member::get()->filter('Name', $_GET['name']); $members = Member::get()->filter('Name', $_GET['name']);
// automatically escaped/quoted // automatically escaped/quoted
@ -86,6 +89,7 @@ Example:
$members = Member::get()->where(array('"Name" = ?' => $_GET['name'])); $members = Member::get()->where(array('"Name" = ?' => $_GET['name']));
// needs to be escaped and quoted manually (note raw2sql called with the $quote parameter set to true) // 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))); $members = Member::get()->where(sprintf('"Name" = %s', Convert::raw2sql($_GET['name'], true)));
```
<div class="warning" markdown='1'> <div class="warning" markdown='1'>
It is NOT good practice to "be sure" and convert the data passed to the functions above manually. This might It is NOT good practice to "be sure" and convert the data passed to the functions above manually. This might
@ -108,7 +112,9 @@ and [datamodel](/developer_guides/model) for ways to parameterise, cast, and con
Example: Example:
:::php
```php
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 // Pass true as the second parameter of raw2sql to quote the value safely
@ -117,14 +123,16 @@ Example:
// ... // ...
} }
} }
```
* `FormField->Value()` * `FormField->Value()`
* URLParams passed to a Controller-method * URLParams passed to a Controller-method
Example: Example:
:::php
```php
class MyController extends Controller { class MyController extends Controller {
private static $allowed_actions = array('myurlaction'); private static $allowed_actions = array('myurlaction');
public function myurlaction($RAW_urlParams) { public function myurlaction($RAW_urlParams) {
@ -134,13 +142,15 @@ Example:
// ... // ...
} }
} }
```
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**
(or preferably, use parameterised queries). This means if you've got a chain of functions (or preferably, use parameterised queries). This means if you've got a chain of functions
passing data through, escaping should happen at the end of the chain. passing data through, escaping should happen at the end of the chain.
:::php
```php
class MyController extends Controller { class MyController extends Controller {
/** /**
* @param array $RAW_data All names in an indexed array (not SQL-safe) * @param array $RAW_data All names in an indexed array (not SQL-safe)
@ -155,6 +165,7 @@ passing data through, escaping should happen at the end of the chain.
DB::query("UPDATE Player SET Name = {$SQL_name}"); 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
you're passing unescaped data, make sure to be explicit about it by writing *phpdoc*-documentation and *prefixing* your you're passing unescaped data, make sure to be explicit about it by writing *phpdoc*-documentation and *prefixing* your
@ -188,9 +199,9 @@ 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.
@ -218,23 +229,27 @@ object-properties by [casting](/developer_guides/model/data_types_and_casting) i
PHP: PHP:
:::php
```php
class MyObject extends DataObject { class MyObject extends DataObject {
private static $db = array( private static $db = array(
'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>
); );
} }
```
Template: 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
outputting through SSViewer. outputting through SSViewer.
@ -246,7 +261,9 @@ You can force escaping on a casted value/object by using an [escape type](/devel
Template (see above): 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>
@ -254,7 +271,7 @@ Template (see above):
<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
@ -263,7 +280,9 @@ static *$casting* array. Caution: Casting only applies when using values in a te
PHP: PHP:
:::php
```php
class MyObject extends DataObject { class MyObject extends DataObject {
public $Title = '<b>not bold</b>'; // will be escaped due to Text casting public $Title = '<b>not bold</b>'; // will be escaped due to Text casting
@ -277,17 +296,19 @@ PHP:
return $this->Title . '<small>(' . $suffix. ')</small>'; 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
presentation from business logic. presentation from business logic.
@ -302,7 +323,9 @@ also used by *XML* and *ATT* in template code).
PHP: PHP:
:::php
```php
class MyController extends Controller { class MyController extends Controller {
private static $allowed_actions = array('search'); private static $allowed_actions = array('search');
public function search($request) { public function search($request) {
@ -313,13 +336,15 @@ PHP:
)); ));
} }
} }
```
Template: Template:
:::php
<h2 title="Searching for $Query.ATT">$HTMLTitle</h2>
```php
<h2 title="Searching for $Query.ATT">$HTMLTitle</h2>
```
Whenever you insert a variable into an HTML attribute within a template, use $VarName.ATT, no not $VarName. Whenever you insert a variable into an HTML attribute within a template, use $VarName.ATT, no not $VarName.
@ -332,7 +357,9 @@ user data, not *Convert::raw2att()*. Use raw ampersands in your URL, and cast t
PHP: PHP:
:::php
```php
class MyController extends Controller { class MyController extends Controller {
private static $allowed_actions = array('search'); private static $allowed_actions = array('search');
public function search($request) { public function search($request) {
@ -343,13 +370,15 @@ PHP:
)); ));
} }
} }
```
Template: Template:
:::php
<a href="$RSSLink.ATT">RSS feed</a>
```php
<a href="$RSSLink.ATT">RSS feed</a>
```
Some rules of thumb: Some rules of thumb:
@ -395,7 +424,9 @@ passed, such as *mysite.com/home/add/dfsdfdsfd*, then it returns 0.
Below is an example with different ways you would use this casting technique: 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) // cast an ID from URL parameters e.g. (mysite.com/home/action/ID)
@ -410,7 +441,7 @@ Below is an example with different ways you would use this casting technique:
// perform a byID(), which ensures the ID is an integer before querying // perform a byID(), which ensures the ID is an integer before querying
return CaseStudy::get()->byID($categoryID); 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
cast types can be found here: cast types can be found here:
@ -441,13 +472,13 @@ 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> <Files *.yml>
Order allow,deny Order allow,deny
Allow from all Allow from all
</Files> </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,
@ -491,12 +522,15 @@ So in addition to storing the password in a secure fashion,
you can also enforce specific password policies by configuring you can also enforce specific password policies by configuring
a [PasswordValidator](api:SilverStripe\Security\PasswordValidator): a [PasswordValidator](api:SilverStripe\Security\PasswordValidator):
:::php
```php
$validator = new PasswordValidator(); $validator = new PasswordValidator();
$validator->minLength(7); $validator->minLength(7);
$validator->checkHistoricalPasswords(6); $validator->checkHistoricalPasswords(6);
$validator->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation")); $validator->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation"));
Member::set_password_validator($validator); 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:
@ -518,14 +552,16 @@ included in HTML "frame" or "iframe" elements, and thereby prevent the most comm
attack vector. This is done through a HTTP header, which is usually added in your attack vector. This is done through a HTTP header, which is usually added in your
controller's `init()` method: controller's `init()` method:
:::php
```php
class MyController extends Controller { class MyController extends Controller {
public function init() { public function init() {
parent::init(); parent::init();
$this->getResponse()->addHeader('X-Frame-Options', 'SAMEORIGIN'); $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
or submits sensitive user input, and is enabled by default in all CMS controllers, or submits sensitive user input, and is enabled by default in all CMS controllers,
@ -537,9 +573,9 @@ To prevent a forged hostname appearing being used by the application, SilverStri
allows the configure of a whitelist of hosts that are allowed to access the system. By defining 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.
@ -554,23 +590,25 @@ 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: SilverStripe\Control\TrustedProxyMiddleware:
properties: properties:
ProxyHostHeaders: X-Forwarded-Host ProxyHostHeaders: X-Forwarded-Host
ProxySchemeHeaders: X-Forwarded-Protocol ProxySchemeHeaders: X-Forwarded-Protocol
ProxyIPHeaders: X-Forwarded-Ip ProxyIPHeaders: X-Forwarded-Ip
SS_TRUSTED_PROXY_HOST_HEADER="HTTP_X_FORWARDED_HOST" SS_TRUSTED_PROXY_HOST_HEADER="HTTP_X_FORWARDED_HOST"
SS_TRUSTED_PROXY_IP_HEADER="HTTP_X_FORWARDED_FOR" SS_TRUSTED_PROXY_IP_HEADER="HTTP_X_FORWARDED_FOR"
SS_TRUSTED_PROXY_PROTOCOL_HEADER="HTTP_X_FORWARDED_PROTOCOL" 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.
@ -582,14 +620,14 @@ This behaviour is enabled whenever `SS_TRUSTED_PROXY_IPS` is defined, or if the
`BlockUntrustedIPs` environment variable is declared. It is advisable to include the `BlockUntrustedIPs` environment variable is declared. It is advisable to include 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
variable will be no longer necessary, thus it will be necessary to always set variable will be no longer necessary, thus it will be necessary to always set
@ -599,12 +637,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()) { if (!Director::isDev()) {
Director::forceSSL(); 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
[lets encrypt](https://letsencrypt.org/) and configured on your web server. [lets encrypt](https://letsencrypt.org/) and configured on your web server.
@ -612,10 +651,11 @@ Forcing HTTPS so requires a certificate to be purchased or obtained through a ve
We also want to ensure cookies are not shared between secure and non-secure sessions, so we must tell SilverStripe to 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
:::yml
Session: Session:
cookie_secure: true 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
the "Secure" and "HTTPOnly" flags. These flags prevent them from being stolen by an attacker through javascript. the "Secure" and "HTTPOnly" flags. These flags prevent them from being stolen by an attacker through javascript.
@ -627,13 +667,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
Cookie::set('cookie-name', 'chocolate-chip', $expiry = 30, $path = null, $domain = null, $secure = true, Cookie::set('cookie-name', 'chocolate-chip', $expiry = 30, $path = null, $domain = null, $secure = true,
$httpOnly = false $httpOnly = false
); );
```
## Security Headers ## Security Headers
@ -656,10 +696,9 @@ For sensitive pages, such as members areas, or places where sensitive informatio
and `Date: <current date>` will ensure that sensitive content is not stored locally or able to be retrieved by and `Date: <current date>` will ensure that sensitive content is not stored locally or able to be retrieved by
unauthorised local persons. SilverStripe adds the current date for every request, and we can add the other cache unauthorised local persons. SilverStripe adds the current date for every request, and we can add the other cache
headers to the request for our secure controllers: headers to the request for our secure controllers:
```php
:::php
class MySecureController extends Controller { class MySecureController extends Controller {
public function init() { public function init() {
@ -676,6 +715,7 @@ unauthorised local persons. SilverStripe adds the current date for every request
$this->response->addHeader('Strict-Transport-Security', 'max-age=86400; includeSubDomains'); $this->response->addHeader('Strict-Transport-Security', 'max-age=86400; includeSubDomains');
} }
} }
```
## Related ## Related

View File

@ -22,9 +22,12 @@ SilverStripe\Core\Injector\Injector:
### Sending plain text only ### Sending plain text only
:::php
```php
$email = new Email($from, $to, $subject, $body); $email = new Email($from, $to, $subject, $body);
$email->sendPlain(); $email->sendPlain();
```
### Sending combined HTML and plain text ### Sending combined HTML and plain text
@ -32,9 +35,12 @@ By default, emails are sent in both HTML and Plaintext format. A plaintext repre
from the system by stripping HTML markup, or transforming it where possible (e.g. `<strong>text</strong>` is converted from the system by stripping HTML markup, or transforming it where possible (e.g. `<strong>text</strong>` is converted
to `*text*`). 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">
The default HTML template for emails is named `GenericEmail` and is located in `framework/templates/SilverStripe/Email/`. The default HTML template for emails is named `GenericEmail` and is located in `framework/templates/SilverStripe/Email/`.
@ -50,9 +56,12 @@ email object additional information using the `setData` and `addData` methods.
**mysite/templates/Email/MyCustomEmail.ss** **mysite/templates/Email/MyCustomEmail.ss**
:::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..
@ -97,10 +106,13 @@ You can set the default sender address of emails through the `Email.admin_email`
**mysite/_config/app.yml** **mysite/_config/app.yml**
:::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">
Remember, setting a `from` address that doesn't come from your domain (such as the users email) will likely see your Remember, setting a `from` address that doesn't come from your domain (such as the users email) will likely see your
@ -122,12 +134,15 @@ Configuration of those properties looks like the following:
**mysite/_config.php** **mysite/_config.php**
:::php
```php
if(Director::isLive()) { if(Director::isLive()) {
Config::inst()->update('Email', 'bcc_all_emails_to', "client@example.com"); Config::inst()->update('Email', 'bcc_all_emails_to', "client@example.com");
} else { } else {
Config::inst()->update('Email', 'send_all_emails_to', "developer@example.com"); Config::inst()->update('Email', 'send_all_emails_to', "developer@example.com");
} }
```
### Setting custom "Reply To" email address. ### Setting custom "Reply To" email address.
@ -135,20 +150,24 @@ 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
For email headers which do not have getters or setters (like setTo(), setFrom()) you can manipulate the underlying For email headers which do not have getters or setters (like setTo(), setFrom()) you can manipulate the underlying
`Swift_Message` that we provide a wrapper for. `Swift_Message` that we provide a wrapper for.
:::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">
See this [Wikipedia](http://en.wikipedia.org/wiki/E-mail#Message_header) entry for a list of header names. See this [Wikipedia](http://en.wikipedia.org/wiki/E-mail#Message_header) entry for a list of header names.

View File

@ -27,16 +27,19 @@ Feature overview:
You can use the CsvBulkLoader without subclassing or other customizations, if the column names You can use the CsvBulkLoader without subclassing or other customizations, if the column names
in your CSV file match `$db` properties in your dataobject. E.g. a simple import for the 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'); $loader = new CsvBulkLoader('Member');
$result = $loader->load('<my-file-path>'); $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`
interface out of the box. interface out of the box.
@ -45,7 +48,9 @@ interface out of the box.
The simplest way to use [CsvBulkLoader](api:SilverStripe\Dev\CsvBulkLoader) is through a [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) interface - you get an upload form out of the box. The simplest way to use [CsvBulkLoader](api:SilverStripe\Dev\CsvBulkLoader) is through a [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) interface - you get an upload form out of the box.
:::php
```php
<?php <?php
class PlayerAdmin extends ModelAdmin { class PlayerAdmin extends ModelAdmin {
private static $managed_models = array( private static $managed_models = array(
@ -57,6 +62,7 @@ The simplest way to use [CsvBulkLoader](api:SilverStripe\Dev\CsvBulkLoader) is t
private static $url_segment = 'players'; 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
below the search form on the left. below the search form on the left.
@ -68,7 +74,9 @@ Let's create a simple upload form (which is used for `MyDataObject` instances).
You'll need to add a route to your controller to make it accessible via URL You'll need to add a route to your controller to make it accessible via URL
(see [director](/reference/director)). (see [director](/reference/director)).
:::php
```php
<?php <?php
class MyController extends Controller { class MyController extends Controller {
@ -108,6 +116,7 @@ You'll need to add a route to your controller to make it accessible via URL
return $this->redirectBack(); 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
with certain access rights. with certain access rights.
@ -117,16 +126,18 @@ with certain access rights.
We're going to use our knowledge from the previous example to import a more sophisticated CSV file. We're going to use our knowledge from the previous example to import a more sophisticated CSV file.
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
<?php <?php
class Player extends DataObject { class Player extends DataObject {
private static $db = array( private static $db = array(
@ -140,11 +151,13 @@ Datamodel for Player
); );
} }
?> ?>
```
Datamodel for FootballTeam: Datamodel for FootballTeam:
:::php
```php
<?php <?php
class FootballTeam extends DataObject { class FootballTeam extends DataObject {
private static $db = array( private static $db = array(
@ -155,7 +168,7 @@ Datamodel for FootballTeam:
); );
} }
?> ?>
```
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).
@ -163,9 +176,8 @@ 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
<?php <?php
class PlayerCsvBulkLoader extends CsvBulkLoader { class PlayerCsvBulkLoader extends CsvBulkLoader {
public $columnMap = array( public $columnMap = array(
@ -194,10 +206,13 @@ Sample implementation of a custom loader. Assumes a CSV-file in a certain format
} }
} }
?> ?>
```
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
<?php <?php
class PlayerAdmin extends ModelAdmin { class PlayerAdmin extends ModelAdmin {
private static $managed_models = array( private static $managed_models = array(
@ -209,7 +224,7 @@ Building off of the ModelAdmin example up top, use a custom loader instead of th
private static $url_segment = 'players'; private static $url_segment = 'players';
} }
?> ?>
```
## Related ## Related

View File

@ -19,7 +19,9 @@ in the template.
**mysite/code/Page.php** **mysite/code/Page.php**
:::php
```php
public function getWellingtonWeather() { public function getWellingtonWeather() {
$fetch = new RestfulService( $fetch = new RestfulService(
'https://query.yahooapis.com/v1/public/yql' 'https://query.yahooapis.com/v1/public/yql'
@ -48,18 +50,24 @@ in the template.
return $output; return $output;
} }
```
## Features ## Features
### Basic Authenication ### Basic Authenication
:::php
```php
$service = new RestfulService("http://example.harvestapp.com"); $service = new RestfulService("http://example.harvestapp.com");
$service->basicAuth('username', 'password'); $service->basicAuth('username', 'password');
```
### Make multiple requests ### Make multiple requests
:::php
```php
$service = new RestfulService("http://example.harvestapp.com"); $service = new RestfulService("http://example.harvestapp.com");
$peopleXML = $service->request('/people'); $peopleXML = $service->request('/people');
@ -69,44 +77,55 @@ in the template.
$taskXML = $service->request('/tasks'); $taskXML = $service->request('/tasks');
$tasks = $service->getValues($taskXML, 'task'); $tasks = $service->getValues($taskXML, 'task');
```
### Caching ### Caching
To set the cache interval you can pass it as the 2nd argument to constructor. To set the cache interval you can pass it as the 2nd argument to constructor.
:::php
```php
$expiry = 60 * 60; // 1 hour; $expiry = 60 * 60; // 1 hour;
$request = new RestfulService("http://example.harvestapp.com", $expiry ); $request = new RestfulService("http://example.harvestapp.com", $expiry );
```
### Getting Values & Attributes ### Getting Values & Attributes
You can traverse through document tree to get the values or attribute of a particular node using XPath. Take for example You can traverse through document tree to get the values or attribute of a particular node using XPath. Take for example
the following XML that is returned. the following XML that is returned.
:::xml
```xml
<entries> <entries>
<entry id='12'>Sally</entry> <entry id='12'>Sally</entry>
<entry id='15'>Ted</entry> <entry id='15'>Ted</entry>
<entry id='30'>Matt</entry> <entry id='30'>Matt</entry>
<entry id='22'>John</entry> <entry id='22'>John</entry>
</entries> </entries>
```
To extract the id attributes of the entries use: To extract the id attributes of the entries use:
:::php
```php
$this->getAttributes($xml, "entries", "entry"); $this->getAttributes($xml, "entries", "entry");
// array(array('id' => 12), array('id' => '15'), ..) // array(array('id' => 12), array('id' => '15'), ..)
```
To extract the values (the names) of the entries use: To extract the values (the names) of the entries use:
:::php
```php
$this->getValues($xml, "entries", "entry"); $this->getValues($xml, "entries", "entry");
// array('Sally', 'Ted', 'Matt', 'John') // array('Sally', 'Ted', 'Matt', 'John')
```
### Searching for Values & Attributes ### Searching for Values & Attributes
@ -116,18 +135,23 @@ If you don't know the exact position of DOM tree where the node will appear you
This is the recommended method for retrieving values of name spaced nodes. This is the recommended method for retrieving values of name spaced nodes.
</div> </div>
:::xml
```xml
<media:guide> <media:guide>
<media:entry id="2030">video</media:entry> <media:entry id="2030">video</media:entry>
</media:guide> </media:guide>
```
To get the value of entry node with the namespace media, use: To get the value of entry node with the namespace media, use:
:::php
```php
$this->searchValue($response, "//media:guide/media:entry"); $this->searchValue($response, "//media:guide/media:entry");
// array('video'); // array('video');
```
## Best Practices ## Best Practices
@ -137,7 +161,9 @@ If the web service returned an error (for example, API key not available or inad
[RestfulService](api:RestfulService) can delegate the error handling to it's descendant class. To handle the errors, subclass [RestfulService](api:RestfulService) can delegate the error handling to it's descendant class. To handle the errors, subclass
`RestfulService and define a function called errorCatch. `RestfulService and define a function called errorCatch.
:::php
```php
<?php <?php
class MyRestfulService extends RestfulService { class MyRestfulService extends RestfulService {
@ -152,10 +178,13 @@ If the web service returned an error (for example, API key not available or inad
return $response; return $response;
} }
} }
```
If you want to bypass error handling, define `checkErrors` in the constructor for `RestfulService` If you want to bypass error handling, define `checkErrors` in the constructor for `RestfulService`
:::php
```php
<?php <?php
class MyRestfulService extends RestfulService { class MyRestfulService extends RestfulService {
@ -166,7 +195,7 @@ If you want to bypass error handling, define `checkErrors` in the constructor fo
$this->checkErrors = false; $this->checkErrors = false;
} }
} }
```
### Setting cURL options ### Setting cURL options

View File

@ -22,7 +22,9 @@ web pages need to link to the URL to notify users that the RSS feed is available
An outline of step one looks like: An outline of step one looks like:
:::php
```php
$feed = new RSSFeed( $feed = new RSSFeed(
$list, $list,
$link, $link,
@ -36,12 +38,14 @@ An outline of step one looks like:
); );
$feed->outputToBrowser(); $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
@ -52,7 +56,9 @@ You can use [RSSFeed](api:SilverStripe\Control\RSS\RSSFeed) to easily create a f
**mysite/code/Page.php** **mysite/code/Page.php**
:::php
```php
<?php <?php
.. ..
@ -84,6 +90,7 @@ You can use [RSSFeed](api:SilverStripe\Control\RSS\RSSFeed) to easily create a f
return Page::get()->sort("LastEdited", "DESC")->limit(10); return Page::get()->sort("LastEdited", "DESC")->limit(10);
} }
} }
```
### Rendering DataObjects in a RSSFeed ### Rendering DataObjects in a RSSFeed
@ -97,7 +104,9 @@ If the items are all displayed on a single page you may simply hard code the lin
Take an example, we want to create an RSS feed of all the `Players` objects in our site. We make sure the `AbsoluteLink` Take an example, we want to create an RSS feed of all the `Players` objects in our site. We make sure the `AbsoluteLink`
method is defined and returns a string to the full website URL. method is defined and returns a string to the full website URL.
:::php
```php
<?php <?php
class Player extends DataObject { class Player extends DataObject {
@ -112,10 +121,13 @@ method is defined and returns a string to the full website URL.
); );
} }
} }
```
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
<?php <?php
class PageController extends ContentController { class PageController extends ContentController {
@ -140,6 +152,7 @@ Then in our controller, we add a new action which returns a the XML list of `Pla
return $rss->outputToBrowser(); return $rss->outputToBrowser();
} }
} }
```
### Customizing the RSS Feed template ### Customizing the RSS Feed template
@ -150,7 +163,9 @@ 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"?> <?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"> <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel> <channel>
@ -167,12 +182,15 @@ Say from that last example we want to include the Players Team in the XML feed w
<% end_loop %> <% end_loop %>
</channel> </channel>
</rss> </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() { public function players() {
$rss = new RSSFeed( $rss = new RSSFeed(
@ -185,6 +203,7 @@ Say from that last example we want to include the Players Team in the XML feed w
return $rss->outputToBrowser(); return $rss->outputToBrowser();
} }
```
<div class="warning"> <div class="warning">
As we've added a new template (PlayersRss.ss) make sure you clear your SilverStripe cache. As we've added a new template (PlayersRss.ss) make sure you clear your SilverStripe cache.

View File

@ -6,7 +6,9 @@ You can have more customised logic and interface feedback through a custom contr
form (which is used for `MyDataObject` instances). You can access it through form (which is used for `MyDataObject` instances). You can access it through
`http://yoursite.com/MyController/?flush=all`. `http://yoursite.com/MyController/?flush=all`.
:::php
```php
<?php <?php
class MyController extends Controller { class MyController extends Controller {
@ -62,6 +64,7 @@ form (which is used for `MyDataObject` instances). You can access it through
return $this->redirectBack(); return $this->redirectBack();
} }
} }
```
<div class="alert" markdown="1"> <div class="alert" markdown="1">
This interface is not secured, consider using [Permission::check()](api:SilverStripe\Security\Permission::check()) to limit the controller to users with certain This interface is not secured, consider using [Permission::check()](api:SilverStripe\Security\Permission::check()) to limit the controller to users with certain

View File

@ -4,18 +4,20 @@ 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`.
**mysite/code/Player.php**. **mysite/code/Player.php**.
:::php
```php
<?php <?php
class Player extends DataObject { class Player extends DataObject {
@ -31,10 +33,13 @@ information about the individual player and a relation set up for managing the `
'Team' => 'FootballTeam' 'Team' => 'FootballTeam'
); );
} }
```
**mysite/code/FootballTeam.php** **mysite/code/FootballTeam.php**
:::php
```php
<?php <?php
class FootballTeam extends DataObject { class FootballTeam extends DataObject {
@ -47,6 +52,7 @@ information about the individual player and a relation set up for managing the `
'Players' => 'Player' '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
like, so we have to create a sub class of `CsvBulkLoader` to handle the unique file. Things we need to consider with like, so we have to create a sub class of `CsvBulkLoader` to handle the unique file. Things we need to consider with
@ -62,7 +68,9 @@ Our final import looks like this.
**mysite/code/PlayerCsvBulkLoader.php** **mysite/code/PlayerCsvBulkLoader.php**
:::php
```php
<?php <?php
class PlayerCsvBulkLoader extends CsvBulkLoader { class PlayerCsvBulkLoader extends CsvBulkLoader {
@ -96,6 +104,7 @@ Our final import looks like this.
return FootballTeam::get()->filter('Title', $val)->First(); return FootballTeam::get()->filter('Title', $val)->First();
} }
} }
```
## Related ## Related

View File

@ -9,7 +9,9 @@ First, we write the code to query the API feed.
**mysite/code/Page.php** **mysite/code/Page.php**
:::php
```php
public function getWellingtonWeather() { public function getWellingtonWeather() {
$fetch = new RestfulService( $fetch = new RestfulService(
'https://query.yahooapis.com/v1/public/yql' 'https://query.yahooapis.com/v1/public/yql'
@ -38,18 +40,22 @@ First, we write the code to query the API feed.
return $output; return $output;
} }
```
This will provide our `Page` template with a new `WellingtonWeather` variable (an [ArrayList](api:SilverStripe\ORM\ArrayList)). Each item has a This will provide our `Page` template with a new `WellingtonWeather` variable (an [ArrayList](api:SilverStripe\ORM\ArrayList)). Each item has a
single field `Description`. single field `Description`.
**mysite/templates/Page.ss** **mysite/templates/Page.ss**
:::ss
```ss
<% if WellingtonWeather %> <% if WellingtonWeather %>
<% loop WellingtonWeather %> <% loop WellingtonWeather %>
$Description $Description
<% end_loop %> <% end_loop %>
<% end_if %> <% end_if %>
```
## Related ## Related

View File

@ -18,7 +18,9 @@ The default output of a [SearchContext](api:SilverStripe\ORM\Search\SearchContex
Defining search-able fields on your DataObject. Defining search-able fields on your DataObject.
:::php
```php
<?php <?php
class MyDataObject extends DataObject { class MyDataObject extends DataObject {
@ -28,6 +30,7 @@ Defining search-able fields on your DataObject.
'ProductCode' 'ProductCode'
); );
} }
```
## Customizing fields and filters ## Customizing fields and filters
@ -35,7 +38,9 @@ In this example we're defining three attributes on our MyDataObject subclass: `P
and `MyDate`. The attribute `HiddenProperty` should not be searchable, and `MyDate` should only search for dates and `MyDate`. The attribute `HiddenProperty` should not be searchable, and `MyDate` should only search for dates
*after* the search entry (with a `GreaterThanFilter`). *after* the search entry (with a `GreaterThanFilter`).
:::php
```php
<?php <?php
class MyDataObject extends DataObject { class MyDataObject extends DataObject {
@ -63,6 +68,7 @@ and `MyDate`. The attribute `HiddenProperty` should not be searchable, and `MyDa
); );
} }
} }
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
See the [SearchFilter](../model/searchfilters) documentation for more information about filters to use such as the See the [SearchFilter](../model/searchfilters) documentation for more information about filters to use such as the
@ -76,7 +82,9 @@ the `$fields` constructor parameter.
### Generating a search form from the context ### Generating a search form from the context
:::php
```php
<?php <?php
// .. // ..
@ -106,6 +114,7 @@ the `$fields` constructor parameter.
))->renderWith('Page_results'); ))->renderWith('Page_results');
} }
} }
```
### Pagination ### Pagination
@ -114,7 +123,9 @@ For pagination records on multiple pages, you need to wrap the results in a
in order to read page limit information. It is also passed the current in order to read page limit information. It is also passed the current
`HTTPRequest` object so it can read the current page from a GET var. `HTTPRequest` object so it can read the current page from a GET var.
:::php
```php
public function getResults($searchCriteria = array()) { public function getResults($searchCriteria = array()) {
$start = ($this->getRequest()->getVar('start')) ? (int)$this->getRequest()->getVar('start') : 0; $start = ($this->getRequest()->getVar('start')) ? (int)$this->getRequest()->getVar('start') : 0;
$limit = 10; $limit = 10;
@ -132,11 +143,13 @@ in order to read page limit information. It is also passed the current
return $records; 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);
@ -144,7 +157,7 @@ notice that if you want to use this getResults function, you need to change the
'Results' => $results 'Results' => $results
))->renderWith(array('Catalogo_results', 'Page')); ))->renderWith(array('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.
@ -160,9 +173,8 @@ 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
:::ss
<% if $Results %> <% if $Results %>
<ul> <ul>
<% loop $Results %> <% loop $Results %>
@ -200,7 +212,7 @@ Results.PaginationSummary(4) defines how many pages the search will show in the
</p> </p>
</div> </div>
<% end_if %> <% end_if %>
```
## Available SearchFilters ## Available SearchFilters

View File

@ -21,7 +21,9 @@ storage engine.
You can do so by adding this static variable to your class definition: You can do so by adding this static variable to your class definition:
:::php
```php
<?php <?php
class MyDataObject extends DataObject { class MyDataObject extends DataObject {
@ -30,6 +32,7 @@ You can do so by adding this static variable to your class definition:
'MySQLDatabase' => 'ENGINE=MyISAM' 'MySQLDatabase' => '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.
@ -46,7 +49,9 @@ SilverStripe provides a [FulltextFilter](api:SilverStripe\ORM\Filters\FulltextFi
Example DataObject: Example DataObject:
:::php
```php
class SearchableDataObject extends DataObject { class SearchableDataObject extends DataObject {
private static $db = array( private static $db = array(
@ -66,11 +71,15 @@ Example DataObject:
); );
} }
```
Performing the search: 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
field instead of the index. field instead of the index.

View File

@ -27,11 +27,13 @@ The i18n class is enabled by default.
To set the locale you just need to call [i18n::set_locale()](api:SilverStripe\i18n\i18n::set_locale()) passing, as a parameter, the name of the locale that To set the locale you just need to call [i18n::set_locale()](api:SilverStripe\i18n\i18n::set_locale()) passing, as a parameter, the name of the locale that
you want to set. you want to set.
:::php
```php
// mysite/_config.php // mysite/_config.php
i18n::set_locale('de_DE'); // Setting the locale to German (Germany) i18n::set_locale('de_DE'); // Setting the locale to German (Germany)
i18n::set_locale('ca_AD'); // Setting to Catalan (Andorra) 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
these translations are available. See [unicode.org](http://unicode.org/cldr/data/diff/supplemental/languages_and_territories.html) these translations are available. See [unicode.org](http://unicode.org/cldr/data/diff/supplemental/languages_and_territories.html)
@ -51,13 +53,15 @@ As you set the locale you can also get the current value, just by calling [i18n:
To let browsers know which language they're displaying a document in, you can declare a language in your template. To let browsers know which language they're displaying a document in, you can declare a language in your template.
:::html
```html
//'Page.ss' (HTML) //'Page.ss' (HTML)
<html lang="$ContentLocale"> <html lang="$ContentLocale">
//'Page.ss' (XHTML) //'Page.ss' (XHTML)
<html lang="$ContentLocale" xml:lang="$ContentLocale" xmlns="http://www.w3.org/1999/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
(meta tags, HTTP headers), explained in this [w3.org article](http://www.w3.org/International/tutorials/language-decl/). (meta tags, HTTP headers), explained in this [w3.org article](http://www.w3.org/International/tutorials/language-decl/).
@ -66,17 +70,23 @@ You can also set the [script direction](http://www.w3.org/International/question
which is determined by the current locale, in order to indicate the preferred flow of characters which is determined by the current locale, in order to indicate the preferred flow of characters
and default alignment of paragraphs and tables to browsers. 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
Formats can be set globally in the i18n class. Formats can be set globally in the i18n class.
You can use these settings for your own view logic. You can use these settings for your own view logic.
:::php
```php
Config::inst()->update('i18n', 'date_format', 'dd.MM.yyyy'); Config::inst()->update('i18n', 'date_format', 'dd.MM.yyyy');
Config::inst()->update('i18n', 'time_format', 'HH:mm'); Config::inst()->update('i18n', '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).
Formats for it's [IntlDateFormatter](http://php.net/manual/en/class.intldateformatter.php) Formats for it's [IntlDateFormatter](http://php.net/manual/en/class.intldateformatter.php)
@ -96,20 +106,26 @@ They can be accessed via the `i18n.common_languages` and `i18n.common_locales` [
In order to add a value, add the following to your `config.yml`: In order to add a value, add the following to your `config.yml`:
:::yml
```yml
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
i18n: i18n:
common_locales: common_locales:
en_NZ: en_NZ:
native: Niu Zillund native: Niu Zillund
```
### i18n in URLs ### i18n in URLs
@ -139,22 +155,27 @@ in a localised format chosen by the browser and operating system.
Fields can be forced to use a certain locale and date/time format by calling `setHTML5(false)`, Fields can be forced to use a certain locale and date/time format by calling `setHTML5(false)`,
followed by `setLocale()` or `setDateFormat()`/`setTimeFormat()`. followed by `setLocale()` or `setDateFormat()`/`setTimeFormat()`.
:::php
```php
$field = new DateField(); $field = new DateField();
$field->setLocale('de_AT'); // set Austrian/German locale, defaulting format to dd.MM.y $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->setDateFormat('d.M.y'); // set a more specific date format (single digit day/month)
```
## Translating text ## Translating text
Adapting a module to make it localizable is easy with SilverStripe. You just need to avoid hardcoding strings that are Adapting a module to make it localizable is easy with SilverStripe. You just need to avoid hardcoding strings that are
language-dependent and use a translator function call instead. 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.
@ -192,7 +213,9 @@ with both a 'one' and 'other' key (as per the CLDR for the default `en` language
For instance, this is an example of how to correctly declare pluralisations for an object For instance, this is an example of how to correctly declare pluralisations for an object
:::php
```php
class MyObject extends DataObject, implements i18nEntityProvider class MyObject extends DataObject, implements i18nEntityProvider
{ {
public function provideI18nEntities() public function provideI18nEntities()
@ -207,13 +230,15 @@ For instance, this is an example of how to correctly declare pluralisations for
]; ];
} }
} }
```
In YML format this will be expressed as the below. This follows the 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'
@ -221,14 +246,16 @@ In YML format this will be expressed as the below. This follows the
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.
Please ensure that any required plurals are exposed via provideI18nEntities. Please ensure that any required plurals are exposed via provideI18nEntities.
#### Usage in PHP Files #### Usage in PHP Files
:::php
```php
// Simple string translation // Simple string translation
_t('LeftAndMain.FILESIMAGES','Files & Images'); _t('LeftAndMain.FILESIMAGES','Files & Images');
@ -241,7 +268,7 @@ Please ensure that any required plurals are exposed via provideI18nEntities.
// Plurals are invoked via a `|` pipe-delimeter with a {count} argument // Plurals are invoked via a `|` pipe-delimeter with a {count} argument
_t('MyObject.PLURALS', 'An object|{count} objects', [ 'count' => '$count ]); _t('MyObject.PLURALS', 'An object|{count} objects', [ 'count' => '$count ]);
```
#### Usage in Template Files #### Usage in Template Files
@ -256,7 +283,9 @@ the PHP version of the function.
* The original language string and the natural language comment parameters are separated by ` on `. * The original language string and the natural language comment parameters are separated by ` on `.
* The final parameter (which is an array in PHP) is passed as a space separated list of key/value pairs. * The final parameter (which is an array in PHP) is passed as a space separated list of key/value pairs.
:::ss
```ss
// Simple string translation // Simple string translation
<%t Namespace.Entity "String to translate" %> <%t Namespace.Entity "String to translate" %>
@ -265,19 +294,22 @@ the PHP version of the function.
// Plurals follow the same convention, required a `|` and `{count}` in the default string // Plurals follow the same convention, required a `|` and `{count}` in the default string
<%t MyObject.PLURALS 'An item|{count} items' count=$Count %> <%t MyObject.PLURALS 'An item|{count} items' count=$Count %>
```
#### Caching in Template Files with locale switching #### Caching in Template Files with locale switching
When caching a `<% loop %>` or `<% with %>` with `<%t params %>`. It is important to add the Locale to the cache key 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
@ -308,7 +340,7 @@ 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
--- ---
Name: customi18n Name: customi18n
Before: 'defaulti18n' Before: 'defaulti18n'
@ -318,7 +350,7 @@ To create a custom module order, you need to specify a config fragment that inse
- 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.
There are a few special cases: There are a few special cases:
@ -337,21 +369,21 @@ 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
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
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)`.
@ -370,9 +402,11 @@ the browser: The current locale, and the default locale as a fallback.
The `Requirements` class has a special method to determine these includes: The `Requirements` class has a special method to determine these includes:
Just point it to a directory instead of a file, and the class will figure out the includes. Just point it to a directory instead of a file, and the class will figure out the includes.
:::php
Requirements::add_i18n_javascript('<my-module-dir>/javascript/lang');
```php
Requirements::add_i18n_javascript('<my-module-dir>/javascript/lang');
```
### Translation Tables in JavaScript ### Translation Tables in JavaScript
@ -381,7 +415,9 @@ As a fallback for partially translated tables we always include the master table
Master Table (`<my-module-dir>/javascript/lang/en.js`) 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 {
@ -389,14 +425,17 @@ Master Table (`<my-module-dir>/javascript/lang/en.js`)
'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
[build task](https://github.com/silverstripe/silverstripe-buildtools/blob/master/src/GenerateJavascriptI18nTask.php), [build task](https://github.com/silverstripe/silverstripe-buildtools/blob/master/src/GenerateJavascriptI18nTask.php),
@ -405,9 +444,11 @@ format which can be processed more easily by external translation providers (see
### Basic Usage ### Basic Usage
:::js
alert(ss.i18n._t('MYMODULE.MYENTITY'));
```js
alert(ss.i18n._t('MYMODULE.MYENTITY'));
```
### Advanced Use ### Advanced Use
@ -417,7 +458,9 @@ The `ss.i18n` object contain a couple functions to help and replace dynamic vari
`sprintf()` will substitute occurencies of `%s` in the main string with each of the following arguments passed to the function. The substitution is done sequentially. `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'),
@ -425,20 +468,22 @@ The `ss.i18n` object contain a couple functions to help and replace dynamic vari
'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

@ -38,9 +38,12 @@ In order to retain existing file paths in line with framework version 3 you shou
`\SilverStripe\Assets\Flysystem\FlysystemAssetStore.legacy_filenames` config to true. `\SilverStripe\Assets\Flysystem\FlysystemAssetStore.legacy_filenames` config to true.
Note that this will not allow you to utilise certain file versioning features in 4.0. Note that this will not allow you to utilise certain file versioning features in 4.0.
:::yaml
```yaml
\SilverStripe\Assets\Flysystem\FlysystemAssetStore: \SilverStripe\Assets\Flysystem\FlysystemAssetStore:
legacy_filenames: true legacy_filenames: true
```
## Loading content into `DBFile` ## Loading content into `DBFile`
@ -48,9 +51,8 @@ A file can be written to the backend from a file which exists on the local files
within the assets folder). within the assets folder).
For example, to load a temporary file into a DataObject you could use the below: For example, to load a temporary file into a DataObject you could use the below:
```php
:::php
<? <?
class Banner extends DataObject { class Banner extends DataObject {
private static $db = array( private static $db = array(
@ -61,7 +63,7 @@ For example, to load a temporary file into a DataObject you could use the below:
// Image could be assigned in other parts of the code using the below // Image could be assigned in other parts of the code using the below
$banner = new Banner(); $banner = new Banner();
$banner->Image->setFromLocalFile($tempfile['path'], 'uploads/banner-file.jpg'); $banner->Image->setFromLocalFile($tempfile['path'], 'uploads/banner-file.jpg');
```
When uploading a file it's normally necessary to give the file a useful name and directory, otherwise the When uploading a file it's normally necessary to give the file a useful name and directory, otherwise the
asset storage backend will choose one for you. asset storage backend will choose one for you.

View File

@ -35,7 +35,9 @@ images are preserved (meaning images are not stretched).
Here are some examples, assuming the `$Image` object has dimensions of 200x100px: Here are some examples, assuming the `$Image` object has dimensions of 200x100px:
:::ss
```ss
// Scaling functions // Scaling functions
$Image.ScaleWidth(150) // Returns a 150x75px image $Image.ScaleWidth(150) // Returns a 150x75px image
$Image.ScaleMaxWidth(100) // Returns a 100x50px image (like ScaleWidth but prevents up-sampling) $Image.ScaleMaxWidth(100) // Returns a 100x50px image (like ScaleWidth but prevents up-sampling)
@ -66,11 +68,15 @@ Here are some examples, assuming the `$Image` object has dimensions of 200x100px
$Image.FileName // Returns the actual file name including directory path from web root $Image.FileName // Returns the actual file name including directory path from web root
$Image.Link // Returns relative URL path to image $Image.Link // Returns relative URL path to image
$Image.AbsoluteLink // Returns absolute 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
@ -80,10 +86,13 @@ pad any surplus space. You can specify the color of the padding using a hex code
You can also specify a level of transparency to apply to the padding color in a fourth param. This will only effect You can also specify a level of transparency to apply to the padding color in a fourth param. This will only effect
png images. 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
@ -97,7 +106,9 @@ Please refer to the [ImageManipulation](api:SilverStripe\Assets\ImageManipulatio
You can also create your own functions by decorating the `Image` class. You can also create your own functions by decorating the `Image` class.
:::php
```php
class ImageExtension extends \SilverStripe\Core\Extension class ImageExtension extends \SilverStripe\Core\Extension
{ {
@ -134,6 +145,7 @@ You can also create your own functions by decorating the `Image` class.
SilverStripe\Filesystem\Storage\DBFile: SilverStripe\Filesystem\Storage\DBFile:
extensions: extensions:
- ImageExtension - ImageExtension
```
### Form Upload ### Form Upload
@ -164,24 +176,30 @@ place. If you expect the images in your asset store to already have
compression applied and want to serve up the original when no resampling is compression applied and want to serve up the original when no resampling is
necessary, you can add this to your mysite/config/config.yml file: necessary, you can add this to your mysite/config/config.yml file:
:::yml
```yml
# Configure resampling for File dataobject # Configure resampling for File dataobject
File: File:
force_resample: false force_resample: false
# DBFile can be configured independently # DBFile can be configured independently
SilverStripe\Filesystem\Storage\DBFile: SilverStripe\Filesystem\Storage\DBFile:
force_resample: false force_resample: false
```
#### Resampled image quality #### Resampled image quality
To adjust the quality of the generated images when they are resampled, add the To adjust the quality of the generated images when they are resampled, add the
following to your mysite/config/config.yml file: following to your mysite/config/config.yml file:
:::yml
```yml
SilverStripe\Core\Injector\Injector: SilverStripe\Core\Injector\Injector:
SilverStripe\Assets\InterventionBackend: SilverStripe\Assets\InterventionBackend:
properties: properties:
Quality: 90 Quality: 90
```
## Changing the manipulation driver to Imagick ## Changing the manipulation driver to Imagick

View File

@ -18,12 +18,14 @@ For instance, in order to write an asset to a protected location you can use the
config option: config option:
:::php
```php
$store = singleton(AssetStore::class); $store = singleton(AssetStore::class);
$store->setFromString('My protected content', 'Documents/Mydocument.txt', null, null, array( $store->setFromString('My protected content', 'Documents/Mydocument.txt', null, null, array(
'visibility' => AssetStore::VISIBILITY_PROTECTED 'visibility' => AssetStore::VISIBILITY_PROTECTED
)); ));
```
## User access control ## User access control
@ -36,7 +38,9 @@ An automated system will, in most cases, handle this whitelisting for you. Calls
will automatically whitelist access to that file for the current user. Using this as a guide, you can easily will automatically whitelist access to that file for the current user. Using this as a guide, you can easily
control access to embedded assets at a template level. control access to embedded assets at a template level.
:::ss
```ss
<ul class="files"> <ul class="files">
<% loop $File %> <% loop $File %>
<% if $canView %> <% if $canView %>
@ -46,6 +50,7 @@ control access to embedded assets at a template level.
<% end_if %> <% end_if %>
<% end_loop > <% end_loop >
</ul> </ul>
```
Users who are able to guess the value of $URL will not be able to access those urls without being Users who are able to guess the value of $URL will not be able to access those urls without being
authorised by this code. authorised by this code.
@ -58,7 +63,9 @@ authorised users, the following should be considered:
file via PHP for the current user instead, by using the following code to grant access. file via PHP for the current user instead, by using the following code to grant access.
:::php
```php
class PageController extends ContentController { class PageController extends ContentController {
public function init() { public function init() {
parent::init(); parent::init();
@ -70,6 +77,7 @@ authorised users, the following should be considered:
} }
} }
} }
```
* If a user does not have access to a file, you can still generate the URL but suppress the default * If a user does not have access to a file, you can still generate the URL but suppress the default
permission whitelist by invoking the getter as a method, but pass in a falsey value as a parameter. permission whitelist by invoking the getter as a method, but pass in a falsey value as a parameter.
@ -87,7 +95,9 @@ authorised users, the following should be considered:
the `revokeFile` method. the `revokeFile` method.
:::php
```php
class PageController extends ContentController { class PageController extends ContentController {
public function init() { public function init() {
parent::init(); parent::init();
@ -102,7 +112,7 @@ authorised users, the following should be considered:
} }
} }
} }
```
## Controlling asset visibility ## Controlling asset visibility
@ -118,22 +128,26 @@ public facing area.
E.g. E.g.
:::php
```php
$object->MyFile->setFromLocalFile($tmpFile['Path'], $filename, null, null, array( $object->MyFile->setFromLocalFile($tmpFile['Path'], $filename, null, null, array(
'visibility' => AssetStore::VISIBILITY_PROTECTED 'visibility' => AssetStore::VISIBILITY_PROTECTED
)); ));
```
You can also adjust the visibility of any existing file to either public or protected. You can also adjust the visibility of any existing file to either public or protected.
:::
```php
// This will make the file available only when a user calls `->grant()` // This will make the file available only when a user calls `->grant()`
$object->SecretFile->protectFile(); $object->SecretFile->protectFile();
// This file will be available to everyone with the URL // This file will be available to everyone with the URL
$object->PublicFile->publishFile(); $object->PublicFile->publishFile();
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
One thing to note is that all variants of a single file will be treated as One thing to note is that all variants of a single file will be treated as
@ -157,7 +171,7 @@ in the protected store, awaiting publishing.
Internally your folder structure would look something like: Internally your folder structure would look something like:
::: ```
assets/ assets/
.htaccess .htaccess
.protected/ .protected/
@ -166,7 +180,7 @@ Internally your folder structure would look something like:
NewCompanyLogo.gif NewCompanyLogo.gif
33be1b95cb/ 33be1b95cb/
OldCompanyLogo.gif OldCompanyLogo.gif
```
The urls for these two files, however, do not reflect the physical structure directly. The urls for these two files, however, do not reflect the physical structure directly.
@ -181,15 +195,17 @@ 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
$store = singleton(AssetStore::class); $store = singleton(AssetStore::class);
$store->publish('NewCompanyLogo.gif', 'a870de278b475cb75f5d9f451439b2d378e13af1'); $store->publish('NewCompanyLogo.gif', 'a870de278b475cb75f5d9f451439b2d378e13af1');
```
After this the filesystem will now look like below: After this the filesystem will now look like below:
::: ```
assets/ assets/
.htaccess .htaccess
.protected/ .protected/
@ -198,7 +214,7 @@ After this the filesystem will now look like below:
OldCompanyLogo.gif OldCompanyLogo.gif
a870de278b/ a870de278b/
NewCompanyLogo.gif NewCompanyLogo.gif
```
## Performance considerations ## Performance considerations
@ -234,23 +250,22 @@ root altogether.
For instance, given your web root is in the folder `/sites/mysite/www`, you can tell the asset store For instance, given your web root is in the folder `/sites/mysite/www`, you can tell the asset store
to put protected files into `/sites/mysite/protected` with the below `.env` setting: to put protected files into `/sites/mysite/protected` with the below `.env` setting:
```
SS_PROTECTED_ASSETS_PATH="/sites/mysite/protected" SS_PROTECTED_ASSETS_PATH="/sites/mysite/protected"
```
### Configuring: File types ### Configuring: File types
In addition to configuring file locations, it's also important to ensure that you have allowed the In addition to configuring file locations, it's also important to ensure that you have allowed the
appropriate file extensions for your instance. This can be done by setting the `File.allowed_extensions` appropriate file extensions for your instance. This can be done by setting the `File.allowed_extensions`
config. config.
```yaml
:::yaml
File: File:
allowed_extensions: allowed_extensions:
- 7zip - 7zip
- xzip - xzip
```
<div class="warning" markdown="1"> <div class="warning" markdown="1">
Any file not included in this config, or in the default list of extensions, will be blocked from Any file not included in this config, or in the default list of extensions, will be blocked from
@ -268,11 +283,13 @@ When a protected file is served it will also be transmitted with all headers def
You can customise this with the below config: You can customise this with the below config:
:::yaml
```yaml
SilverStripe\Filesystem\Flysystem\FlysystemAssetStore: SilverStripe\Filesystem\Flysystem\FlysystemAssetStore:
file_response_headers: file_response_headers:
Pragma: 'no-cache' Pragma: 'no-cache'
```
### Configuring: Archive behaviour ### Configuring: Archive behaviour
@ -294,22 +311,26 @@ config to true on that class. Note that this feature only works with dataobjects
the `Versioned` extension. the `Versioned` extension.
:::php
```php
class MyVersiondObject extends DataObject { class MyVersiondObject extends DataObject {
/** Ensure assets are archived along with the DataObject */ /** Ensure assets are archived along with the DataObject */
private static $keep_archived_assets = true; private static $keep_archived_assets = true;
/** Versioned */ /** Versioned */
private static $extensions = array('Versioned'); private static $extensions = array('Versioned');
} }
```
The extension can also be globally disabled by removing it at the root level: The extension can also be globally disabled by removing it at the root level:
:::yaml
```yaml
DataObject: DataObject:
AssetControl: null AssetControl: null
```
### Configuring: Web server settings ### Configuring: Web server settings
@ -330,12 +351,12 @@ In order to ensure that public files are served correctly, you should check that
.htaccess bypasses PHP requests for files that do exist. The default template .htaccess bypasses PHP requests for files that do exist. The default template
(declared by `Assets_HTAccess.ss`) has the following section, which may be customised in your project: (declared by `Assets_HTAccess.ss`) has the following section, which may be customised in your project:
```
# Non existant files passed to requesthandler # Non existant files passed to requesthandler
RewriteCond %{REQUEST_URI} ^(.*)$ RewriteCond %{REQUEST_URI} ^(.*)$
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* ../framework/main.php?url=%1 [QSA] RewriteRule .* ../framework/main.php?url=%1 [QSA]
```
You will need to ensure that your core apache configuration has the necessary `AllowOverride` You will need to ensure that your core apache configuration has the necessary `AllowOverride`
settings to support the local .htaccess file. settings to support the local .htaccess file.
@ -348,7 +369,7 @@ while ensuring non-existent files are processed via the Framework.
The default rule for IIS is as below (only partial configuration displayed): The default rule for IIS is as below (only partial configuration displayed):
```
<rule name="Protected and 404 File rewrite" stopProcessing="true"> <rule name="Protected and 404 File rewrite" stopProcessing="true">
<match url="^(.*)$" /> <match url="^(.*)$" />
<conditions> <conditions>
@ -356,7 +377,7 @@ The default rule for IIS is as below (only partial configuration displayed):
</conditions> </conditions>
<action type="Rewrite" url="../framework/main.php?url={R:1}" appendQueryString="true" /> <action type="Rewrite" url="../framework/main.php?url={R:1}" appendQueryString="true" />
</rule> </rule>
```
You will need to make sure that the `allowOverride` property of your root web.config is not set You will need to make sure that the `allowOverride` property of your root web.config is not set
to false, to allow these to take effect. to false, to allow these to take effect.
@ -370,9 +391,9 @@ For instance, this will allow your nginx site to serve files directly, while ens
dynamic requests are processed via the Framework: dynamic requests are processed via the Framework:
::: ```
location ^~ /assets/ { location ^~ /assets/ {
sendfile on; sendfile on;
try_files $uri /framework/main.php?url=$uri&$query_string; try_files $uri /framework/main.php?url=$uri&$query_string;
} }
```

View File

@ -19,7 +19,9 @@ a category.
**mysite/code/Product.php** **mysite/code/Product.php**
:::php
```php
<?php <?php
class Product extends DataObject { class Product extends DataObject {
@ -34,10 +36,13 @@ a category.
'Category' => 'Category' 'Category' => 'Category'
); );
} }
```
**mysite/code/Category.php** **mysite/code/Category.php**
:::php
```php
<?php <?php
class Category extends DataObject { class Category extends DataObject {
@ -50,6 +55,7 @@ a category.
'Products' => 'Product' '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
DataObject's you want to scaffold an interface for. The class can manage multiple models in parallel, if required. DataObject's you want to scaffold an interface for. The class can manage multiple models in parallel, if required.
@ -58,7 +64,9 @@ We'll name it `MyAdmin`, but the class name can be anything you want.
**mysite/code/MyAdmin.php** **mysite/code/MyAdmin.php**
:::php
```php
<?php <?php
class MyAdmin extends ModelAdmin { class MyAdmin extends ModelAdmin {
@ -72,6 +80,7 @@ We'll name it `MyAdmin`, but the class name can be anything you want.
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
users will be able to upload and manage `Product` and `Category` instances through http://yoursite.com/admin/products. users will be able to upload and manage `Product` and `Category` instances through http://yoursite.com/admin/products.
@ -96,7 +105,9 @@ permissions by default. For most cases, less restrictive checks make sense, e.g.
**mysite/code/Category.php** **mysite/code/Category.php**
:::php
```php
<?php <?php
class Category extends DataObject { class Category extends DataObject {
@ -116,6 +127,7 @@ permissions by default. For most cases, less restrictive checks make sense, e.g.
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
@ -129,7 +141,9 @@ class (see [SearchContext](../search/searchcontext) docs for details).
**mysite/code/Product.php** **mysite/code/Product.php**
:::php
```php
<?php <?php
class Product extends DataObject { class Product extends DataObject {
@ -139,6 +153,7 @@ class (see [SearchContext](../search/searchcontext) docs for details).
'ProductCode' 'ProductCode'
); );
} }
```
<div class="hint" markdown="1"> <div class="hint" markdown="1">
[SearchContext](../search/searchcontext) documentation has more information on providing the search functionality. [SearchContext](../search/searchcontext) documentation has more information on providing the search functionality.
@ -152,7 +167,9 @@ model class, where you can add or remove columns. To change the title, use [Data
**mysite/code/Product.php** **mysite/code/Product.php**
:::php
```php
<?php <?php
class Product extends DataObject { class Product extends DataObject {
@ -166,6 +183,7 @@ model class, where you can add or remove columns. To change the title, use [Data
'Price' '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
form. If no search parameters are given, the results will show every record. Results are a [DataList](api:SilverStripe\ORM\DataList) instance, so form. If no search parameters are given, the results will show every record. Results are a [DataList](api:SilverStripe\ORM\DataList) instance, so
@ -175,7 +193,9 @@ For example, we might want to exclude all products without prices in our sample
**mysite/code/MyAdmin.php** **mysite/code/MyAdmin.php**
:::php
```php
<?php <?php
class MyAdmin extends ModelAdmin { class MyAdmin extends ModelAdmin {
@ -191,13 +211,16 @@ For example, we might want to exclude all products without prices in our sample
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
checkbox which limits search results to expensive products (over $100). checkbox which limits search results to expensive products (over $100).
**mysite/code/MyAdmin.php** **mysite/code/MyAdmin.php**
:::php
```php
<?php <?php
class MyAdmin extends ModelAdmin { class MyAdmin extends ModelAdmin {
@ -224,13 +247,16 @@ checkbox which limits search results to expensive products (over $100).
return $list; 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
example, to add a new component. example, to add a new component.
**mysite/code/MyAdmin.php** **mysite/code/MyAdmin.php**
:::php
```php
<?php <?php
class MyAdmin extends ModelAdmin { class MyAdmin extends ModelAdmin {
@ -256,13 +282,16 @@ example, to add a new component.
return $form; 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
to only one specific `GridField`: to only one specific `GridField`:
**mysite/code/MyAdmin.php** **mysite/code/MyAdmin.php**
:::php
```php
<?php <?php
class MyAdmin extends ModelAdmin { class MyAdmin extends ModelAdmin {
@ -285,6 +314,7 @@ to only one specific `GridField`:
return $form; return $form;
} }
} }
```
## Data Import ## Data Import
@ -302,7 +332,9 @@ This is handled through the [GridFieldExportButton](api:SilverStripe\Forms\GridF
To customize the exported columns, create a new method called `getExportFields` in your `ModelAdmin`: To customize the exported columns, create a new method called `getExportFields` in your `ModelAdmin`:
:::php
```php
<?php <?php
class MyAdmin extends ModelAdmin { class MyAdmin extends ModelAdmin {
@ -316,7 +348,7 @@ To customize the exported columns, create a new method called `getExportFields`
); );
} }
} }
```
## Related Documentation ## Related Documentation

View File

@ -54,7 +54,9 @@ coding conventions.
The CMS interface can be accessed by default through the `admin/` URL. You can change this by setting your own [Director routing rule](director#routing-rules) to the `[AdminRootController](api:SilverStripe\Admin\AdminRootController)` and clear the old rule like in the example below. The CMS interface can be accessed by default through the `admin/` URL. You can change this by setting your own [Director routing rule](director#routing-rules) to the `[AdminRootController](api:SilverStripe\Admin\AdminRootController)` and clear the old rule like in the example below.
:::yml
```yml
--- ---
Name: myadmin Name: myadmin
After: After:
@ -65,23 +67,33 @@ The CMS interface can be accessed by default through the `admin/` URL. You can c
'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):
In PHP you should use: In PHP you should use:
:::php
```php
AdminRootController::admin_url() 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
@ -149,7 +161,9 @@ of a `PjaxResponseNegotiator` to handle its display.
Basic example form in a CMS controller subclass: Basic example form in a CMS controller subclass:
:::php
```php
class MyAdmin extends LeftAndMain { class MyAdmin extends LeftAndMain {
function getEditForm() { function getEditForm() {
return CMSForm::create( return CMSForm::create(
@ -176,6 +190,7 @@ Basic example form in a CMS controller subclass:
->setTemplate($this->getTemplatesWithSuffix('_EditForm')); ->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,
and will simply call `parent::getEditForm()` to modify an existing, and will simply call `parent::getEditForm()` to modify an existing,
@ -280,22 +295,20 @@ 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
:::php
public function getClientConfig() { public function getClientConfig() {
return array_merge(parent::getClientConfig(), [ return array_merge(parent::getClientConfig(), [
'reactRouter' => true '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
:::js
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import ConfigHelpers from 'lib/Config'; import ConfigHelpers from 'lib/Config';
import reactRouteRegister from 'lib/ReactRouteRegister'; import reactRouteRegister from 'lib/ReactRouteRegister';
@ -312,19 +325,18 @@ to ensure routes are registered before window.load is invoked.
], ],
}); });
}); });
```
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
:::js
// Register a nested url under `sectionConfig.url` // Register a nested url under `sectionConfig.url`
const sectionConfig = ConfigHelpers.getSection('MyAdmin'); const sectionConfig = ConfigHelpers.getSection('MyAdmin');
reactRouteRegister.add({ reactRouteRegister.add({
path: 'nested', path: 'nested',
component: NestedComponent, component: NestedComponent,
}, [ sectionConfig.url ]); }, [ sectionConfig.url ]);
```
## PJAX: Partial template replacement through Ajax ## PJAX: Partial template replacement through Ajax
@ -349,7 +361,9 @@ Example: Create a bare-bones CMS subclass which shows breadcrumbs (a built-in me
as well as info on the current record. A single link updates both sections independently as well as info on the current record. A single link updates both sections independently
in a single Ajax request. in a single Ajax request.
:::php
```php
// mysite/code/MyAdmin.php // mysite/code/MyAdmin.php
class MyAdmin extends LeftAndMain { class MyAdmin extends LeftAndMain {
private static $url_segment = 'myadmin'; private static $url_segment = 'myadmin';
@ -366,8 +380,9 @@ in a single Ajax request.
return $this->renderWith('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>
@ -381,17 +396,18 @@ in a single Ajax request.
<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,
sending back different `X-Pjax` headers and content. sending back different `X-Pjax` headers and content.
@ -399,9 +415,9 @@ 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
$('.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`.
@ -415,11 +431,11 @@ unrelated to the main flow.
In this case you can use the `loadFragment` call supplied by `LeftAndMain.js`. You can trigger as many of these in 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
$('.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.
@ -435,23 +451,23 @@ has been found on an element (this element will get completely replaced). Afterw
will be triggered. In case of a request error a `loadfragmenterror` will be raised and DOM will not be touched. 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
'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
$('.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,
@ -471,7 +487,9 @@ For example, the currently used controller class might've changed due to a "redi
which affects the currently active menu entry. We're using HTTP response headers to contain this data which affects the currently active menu entry. We're using HTTP response headers to contain this data
without affecting the response body. without affecting the response body.
:::php
```php
class MyController extends LeftAndMain { class MyController extends LeftAndMain {
class myaction() { class myaction() {
// ... // ...
@ -479,6 +497,7 @@ without affecting the response body.
return $html; return $html;
} }
} }
```
Built-in headers are: Built-in headers are:
@ -529,12 +548,15 @@ from "Page" to "Files & Images". To communicate this state change, a controller
response has the option to pass along a special HTTP response header, response has the option to pass along a special HTTP response header,
which is picked up by the menu: 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,
so you don't need to worry about it. The same concept applies for so you don't need to worry about it. The same concept applies for
@ -579,7 +601,9 @@ since all others should render with their tab navigation inline.
Form template with custom tab navigation (trimmed down): 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">
@ -603,10 +627,13 @@ Form template with custom tab navigation (trimmed down):
</div> </div>
</form> </form>
```
Tabset template without tab navigation (e.g. `CMSTabset.ss`) Tabset template without tab navigation (e.g. `CMSTabset.ss`)
:::ss
```ss
<div $AttributesHTML> <div $AttributesHTML>
<% loop Tabs %> <% loop Tabs %>
<% if Tabs %> <% if Tabs %>
@ -620,6 +647,7 @@ Tabset template without tab navigation (e.g. `CMSTabset.ss`)
<% 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.
The base behaviour is applied through adding a class `.cms-tabset` to a container. The base behaviour is applied through adding a class `.cms-tabset` to a container.
@ -629,7 +657,9 @@ This is achieved by template conditionals (see "MyActiveCondition").
The `.cms-panel-link` class will automatically trigger the ajax loading, The `.cms-panel-link` class will automatically trigger the ajax loading,
and load the HTML content into the main view. Example: 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 %>">
@ -644,6 +674,7 @@ and load the HTML content into the main view. Example:
</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`
should return HTML fragments suitable for inserting into the content area, should return HTML fragments suitable for inserting into the content area,

View File

@ -21,8 +21,11 @@ children setting sizes and positions, which in turn requires redrawing of some o
The easiest way to update the layout of the CMS is to call `redraw` on the top-level `.cms-container` element. The easiest way to update the layout of the CMS is to call `redraw` on the top-level `.cms-container` element.
:::js
```js
$('.cms-container').redraw(); $('.cms-container').redraw();
```
This causes the framework to: This causes the framework to:
@ -60,13 +63,16 @@ Call `redraw` on `.cms-container` to re-layout the CMS.
Layout manager will automatically apply algorithms to the children of `.cms-container` by inspecting the Layout manager will automatically apply algorithms to the children of `.cms-container` by inspecting the
`data-layout-type` attribute. Let's take the content toolbar as an example of a second-level layout application: `data-layout-type` attribute. Let's take the content toolbar as an example of a second-level layout application:
:::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
[jLayout algorithms](https://github.com/bramstein/jlayout#layout-algorithms). [jLayout algorithms](https://github.com/bramstein/jlayout#layout-algorithms).
@ -104,8 +110,11 @@ by the algorithm that are initially taken from the `LeftAndMain::LayoutOptions`
Use provided factory method to generate algorithm instances. 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

@ -50,7 +50,9 @@ Note how the configuration happens in different entwine namespaces
("ss.preview" and "ss"), as well as applies to different selectors ("ss.preview" and "ss"), as well as applies to different selectors
(".cms-preview" and ".cms-container"). (".cms-preview" and ".cms-container").
:::js
```js
(function($) { (function($) {
$.entwine('ss.preview', function($){ $.entwine('ss.preview', function($){
$('.cms-preview').entwine({ $('.cms-preview').entwine({
@ -72,14 +74,18 @@ Note how the configuration happens in different entwine namespaces
}); });
}); });
}(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'
to the `LeftAndMain.extra_requirements_javascript` [configuration value](../configuration) to the `LeftAndMain.extra_requirements_javascript` [configuration value](../configuration)
:::yml
```yml
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
is your best reference at the moment - have a look in `framework/admin/javascript/src/LeftAndMain.Preview.js`. is your best reference at the moment - have a look in `framework/admin/javascript/src/LeftAndMain.Preview.js`.

View File

@ -6,23 +6,32 @@ summary: Add custom CSS properties to the rich-text editor.
SilverStripe lets you customise the style of content in the CMS. This is done by setting up a CSS file called SilverStripe lets you customise the style of content in the CMS. This is done by setting up a CSS file called
`editor.css` in either your theme or in your `mysite` folder. This is set through `editor.css` in either your theme or in your `mysite` folder. This is set through
:::php
```php
HtmlEditorConfig::get('cms')->setOption('content_css', project() . '/css/editor.css'); 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.
If using this config option in `mysite/_config.php`, you will have to instead call: If using this config option in `mysite/_config.php`, you will have to instead call:
:::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
add the color 'red' as an option within the `WYSIWYG` add the following to the `editor.css` add the color 'red' as an option within the `WYSIWYG` add the following to the `editor.css`
:::css
```css
.red { .red {
color: red; color: red;
} }
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
After you have defined the `editor.css` make sure you clear your SilverStripe cache for it to take effect. After you have defined the `editor.css` make sure you clear your SilverStripe cache for it to take effect.

View File

@ -44,12 +44,15 @@ plugin. See "[How jQuery Works](http://docs.jquery.com/How_jQuery_Works)" for a
You should write all your custom jQuery code in a closure. You should write all your custom jQuery code in a closure.
:::javascript
```javascript
(function($) { (function($) {
$(document).ready(function(){ $(document).ready(function(){
// your code here. // your code here.
}) })
})(jQuery); })(jQuery);
```
## jQuery Plugins ## jQuery Plugins
@ -70,7 +73,9 @@ development, most importantly:
Example: A plugin to highlight a collection of elements with a configurable foreground and background colour Example: A plugin to highlight a collection of elements with a configurable foreground and background colour
(abbreviated example from [learningjquery.com](http://www.learningjquery.com/2007/10/a-plugin-development-pattern)). (abbreviated example from [learningjquery.com](http://www.learningjquery.com/2007/10/a-plugin-development-pattern)).
:::js
```js
// create closure // create closure
(function($) { (function($) {
// plugin definition // plugin definition
@ -96,11 +101,13 @@ Example: A plugin to highlight a collection of elements with a configurable fore
}; };
// end of closure // end of closure
})(jQuery); })(jQuery);
```
Usage: Usage:
:::js
```js
(function($) { (function($) {
// Highlight all buttons with default colours // Highlight all buttons with default colours
jQuery(':button').highlight(); jQuery(':button').highlight();
@ -111,7 +118,7 @@ Usage:
// Set all further highlight() calls to have a green background // Set all further highlight() calls to have a green background
$.fn.hilight.defaults.background = "green"; $.fn.hilight.defaults.background = "green";
})(jQuery); })(jQuery);
```
## jQuery UI Widgets ## jQuery UI Widgets
@ -130,7 +137,9 @@ 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 () {
@ -156,11 +165,13 @@ Example: Highlighter
blink: false blink: false
}; };
})(jQuery); })(jQuery);
```
Usage: Usage:
:::js
```js
(function($) { (function($) {
// call with default options // call with default options
$(':button').myHighlight(); $(':button').myHighlight();
@ -177,7 +188,7 @@ Usage:
// Get property // Get property
$(':button').myHighlight('getBlink'); $(':button').myHighlight('getBlink');
})(jQuery); })(jQuery);
```
### jQuery.Entwine ### jQuery.Entwine
@ -192,7 +203,9 @@ It is also suited for more complex applications beyond a single-purpose plugin.
Example: Highlighter Example: Highlighter
:::js
```js
(function($) { (function($) {
$(':button').entwine({ $(':button').entwine({
Foreground: 'red', Foreground: 'red',
@ -203,11 +216,13 @@ Example: Highlighter
} }
}); });
})(jQuery); })(jQuery);
```
Usage: Usage:
:::js
```js
(function($) { (function($) {
// call with default options // call with default options
$(':button').entwine().highlight(); $(':button').entwine().highlight();
@ -218,7 +233,7 @@ Usage:
// get property // get property
$(':button').entwine().getBackground(); $(':button').entwine().getBackground();
})(jQuery); })(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
properties, namespacing, as well as its inheritance based on CSS selectors. Please see the [project properties, namespacing, as well as its inheritance based on CSS selectors. Please see the [project
@ -238,13 +253,15 @@ jQuery with a few lines of code. Your jQuery code will normally end up as a ser
Global properties are evil. They are accessible by other scripts, might be overwritten or misused. A popular case is the `$` shortcut in different libraries: in PrototypeJS it stands for `document.getElementByID()`, in jQuery for `jQuery()`. Global properties are evil. They are accessible by other scripts, might be overwritten or misused. A popular case is the `$` shortcut in different libraries: in PrototypeJS it stands for `document.getElementByID()`, in jQuery for `jQuery()`.
:::js
```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.
NoConflict mode is enabled by default in the SilverStripe CMS javascript. NoConflict mode is enabled by default in the SilverStripe CMS javascript.
@ -254,12 +271,14 @@ NoConflict mode is enabled by default in the SilverStripe CMS javascript.
You have to ensure that DOM elements you want to act on are loaded before using them. jQuery provides a wrapper around You have to ensure that DOM elements you want to act on are loaded before using them. jQuery provides a wrapper around
the `window.onload` and `document.ready` events. 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
Ready](http://docs.jquery.com/How_jQuery_Works#Launching_Code_on_Document_Ready). Ready](http://docs.jquery.com/How_jQuery_Works#Launching_Code_on_Document_Ready).
@ -273,7 +292,9 @@ 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 // manual binding, only applies to existing elements
$('input[[type=submit]]').on('click', function() { $('input[[type=submit]]').on('click', function() {
$(this).addClass('loading'); $(this).addClass('loading');
@ -283,7 +304,7 @@ Example: Add a 'loading' classname to all pressed buttons
$('.cms-container').on('click', 'input[[type=submit]]', function() { $('.cms-container').on('click', 'input[[type=submit]]', function() {
$(this).addClass('loading'); $(this).addClass('loading');
}); });
```
### Assume Element Collections ### Assume Element Collections
@ -292,14 +313,16 @@ makes sense). Encapsulate your code by nesting your jQuery commands inside a `jQ
Example: ComplexTableField implements a paginated table with a pop-up for displaying Example: ComplexTableField implements a paginated table with a pop-up for displaying
:::js
```js
$('div.ComplexTableField').each(function() { $('div.ComplexTableField').each(function() {
// This is the over code for the tr elements inside a ComplexTableField. // This is the over code for the tr elements inside a ComplexTableField.
$(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
@ -310,18 +333,22 @@ Example: Simple form change tracking to prevent submission of unchanged data
Through CSS properties 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);
}); });
@ -329,7 +356,7 @@ Through jQuery.data()
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)
@ -339,11 +366,16 @@ 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
@ -353,7 +385,7 @@ Example: Restricted numeric value field
return false; return false;
} }
}); });
```
See [interactive example on jsbin.com](http://jsbin.com/axafa) See [interactive example on jsbin.com](http://jsbin.com/axafa)
@ -375,17 +407,21 @@ Example: Autocomplete input field loading page matches through AJAX
Template: 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 { class MyController {
public function autocomplete($request) { public function autocomplete($request) {
$results = Page::get()->filter("Title", $request->getVar('title')); $results = Page::get()->filter("Title", $request->getVar('title'));
@ -401,11 +437,13 @@ PHP:
))->renderWith('AutoComplete'); ))->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" />
@ -413,11 +451,13 @@ HTML
</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(
@ -435,7 +475,7 @@ JavaScript:
} }
); );
}); });
```
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
have server side code that needs to trigger the update of a couple of elements in the CMS left-hand tree, it would be have server side code that needs to trigger the update of a couple of elements in the CMS left-hand tree, it would be
@ -456,7 +496,9 @@ events](http://docs.jquery.com/Namespaced_Events).
Example: Trigger custom 'validationfailed' event on form submission for each empty element Example: Trigger custom 'validationfailed' event on form submission for each empty element
:::js
```js
$('form').bind('submit', function(e) { $('form').bind('submit', function(e) {
// $(this) refers to form // $(this) refers to form
$(this).find(':input').each(function() { $(this).find(':input').each(function() {
@ -471,7 +513,7 @@ Example: Trigger custom 'validationfailed' event on form submission for each emp
// $(this) refers to input field // $(this) refers to input field
alert($(this).attr('name')); alert($(this).attr('name'));
}); });
```
See [interactive example on jsbin.com](http://jsbin.com/ipeca). See [interactive example on jsbin.com](http://jsbin.com/ipeca).
@ -513,7 +555,9 @@ 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: * Available Custom Events:
@ -560,7 +604,7 @@ Example: jQuery.entwine
} }
}; };
]]); ]]);
```
### Unit Testing ### Unit Testing
@ -572,16 +616,18 @@ 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
@ -594,7 +640,7 @@ Example: JSpec Shopping cart test (from [visionmedia.github.com](http://visionme
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

@ -26,7 +26,9 @@ state already, so you just need to add the alternate state using two data additi
Here is the configuration code for the button: Here is the configuration code for the button:
:::php
```php
public function getCMSActions() { public function getCMSActions() {
$fields = parent::getCMSActions(); $fields = parent::getCMSActions();
@ -41,6 +43,7 @@ Here is the configuration code for the button:
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
simplify our example, let's assume the button state is controlled on the backend only, but you'd usually be better off simplify our example, let's assume the button state is controlled on the backend only, but you'd usually be better off
@ -50,7 +53,9 @@ used for initialisation though.
Here we initialise the button based on the backend check, and assume that the button will only update after page reload Here we initialise the button based on the backend check, and assume that the button will only update after page reload
(or on CMS action). (or on CMS action).
:::php
```php
public function getCMSActions() { public function getCMSActions() {
// ... // ...
if ($this->needsCleaning()) { if ($this->needsCleaning()) {
@ -59,6 +64,7 @@ Here we initialise the button based on the backend check, and assume that the bu
} }
// ... // ...
} }
```
## Frontend support ## Frontend support
@ -72,24 +78,35 @@ frontend. You can affect the state of the button through the jQuery UI calls.
First of all, you can toggle the state of the button - execute this code in the browser's console to see how it works. First of all, you can toggle the state of the button - execute this code in the browser's console to see how it works.
:::js
```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
CMS core that tracks the changes to the input fields and reacts by enabling the *Save* and *Save & publish* buttons CMS core that tracks the changes to the input fields and reacts by enabling the *Save* and *Save & publish* buttons
(changetracker will automatically add `changed` class to the form if a modification is detected). (changetracker will automatically add `changed` class to the form if a modification is detected).
:::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.
@ -107,6 +124,7 @@ CMS core that tracks the changes to the input fields and reacts by enabling the
this._super(e); this._super(e);
} }
}); });
```
## Frontend hooks ## Frontend hooks
@ -136,7 +154,9 @@ disassembled into:
Here is the entire handler put together. You don't need to add any separate initialisation code, this will handle all Here is the entire handler put together. You don't need to add any separate initialisation code, this will handle all
cases. cases.
:::js
```js
(function($) { (function($) {
$.entwine('mysite', function($){ $.entwine('mysite', function($){
@ -157,6 +177,7 @@ cases.
}); });
}(jQuery)); }(jQuery));
```
## Summary ## Summary

View File

@ -9,17 +9,23 @@ shown alongside the field, a tooltip which shows on demand, or toggleable descri
The `FormField->setDescription()` method will add a `<span class="description">` The `FormField->setDescription()` method will add a `<span class="description">`
at the last position within the field, and expects unescaped HTML content. at the last position within the field, and expects unescaped HTML content.
:::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');
```
To show the help text as a tooltip instead of inline, To show the help text as a tooltip instead of inline,
add a `.cms-description-tooltip` class. 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
for native, focusable input elements, which excludes for native, focusable input elements, which excludes
@ -34,19 +40,25 @@ Another option you have available is making the field's description togglable. T
the UI tidy by hiding the description until the user requests more information the UI tidy by hiding the description until the user requests more information
by clicking the 'info' icon displayed alongside the field. 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
by setting an additional `RightTitle`. 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
[Custom form field templates](/developer_guides/forms/form_templates); [Custom form field templates](/developer_guides/forms/form_templates);

View File

@ -20,11 +20,14 @@ First we'll need a custom icon. For this purpose SilverStripe uses 16x16
black-and-transparent PNG graphics. In this case we'll place the icon in black-and-transparent PNG graphics. In this case we'll place the icon in
`mysite/images`, but you are free to use any location. `mysite/images`, but you are free to use any location.
:::php
```php
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
@ -32,11 +35,14 @@ The title of menu entries is configured through the `$menu_title` static.
If its not defined, the CMS falls back to using the class name of the If its not defined, the CMS falls back to using the class name of the
controller, removing the "Admin" bit at the end. controller, removing the "Admin" bit at the end.
:::php
```php
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
`<classname>.MENUTITLE` entity name, which is automatically created when running `<classname>.MENUTITLE` entity name, which is automatically created when running
@ -54,7 +60,9 @@ Google to the menu.
First, we need to define a [LeftAndMainExtension](api:SilverStripe\Admin\LeftAndMainExtension) which will contain our First, we need to define a [LeftAndMainExtension](api:SilverStripe\Admin\LeftAndMainExtension) which will contain our
button configuration. button configuration.
:::php
```php
<?php <?php
class CustomLeftAndMain extends LeftAndMainExtension { class CustomLeftAndMain extends LeftAndMainExtension {
@ -82,14 +90,17 @@ button configuration.
CMSMenu::add_link($id, $title, $link, $priority, $attributes); 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`
class. For more information about configuring extensions see the class. For more information about configuring extensions see the
[extensions reference](/developer_guides/extending/extensions). [extensions reference](/developer_guides/extending/extensions).
:::php
LeftAndMain::add_extension('CustomLeftAndMain')
```php
LeftAndMain::add_extension('CustomLeftAndMain')
```
## Related ## Related

View File

@ -17,7 +17,9 @@ You can use these two classes as a starting point for your customizations.
Here's a brief example on how to add sorting and a new column for a 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.
:::php
```php
// mysite/code/NewsPageHolder.php // mysite/code/NewsPageHolder.php
class NewsPageHolder extends Page { class NewsPageHolder extends Page {
private static $allowed_children = array('NewsPage'); private static $allowed_children = array('NewsPage');
@ -29,6 +31,7 @@ hypothetical `NewsPageHolder` type, which contains `NewsPage` children.
'Author' => 'Member', '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.
This allows us to intercept the list building logic, and alter the `GridField` This allows us to intercept the list building logic, and alter the `GridField`
@ -36,7 +39,9 @@ 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.
:::php
```php
// mysite/code/NewsPageHolderCMSMainExtension.php // mysite/code/NewsPageHolderCMSMainExtension.php
class NewsPageHolderCMSMainExtension extends Extension { class NewsPageHolderCMSMainExtension extends Extension {
function updateListView($listView) { function updateListView($listView) {
@ -61,12 +66,13 @@ or across page types with common characteristics.
} }
} }
} }
```
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
// mysite/_config/config.yml // mysite/_config/config.yml
LeftAndMain: 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

@ -20,7 +20,9 @@ link that wraps around the node title, a node's id which is given as id attribut
tags showing the node status, etc. SilverStripe tree node will be typically rendered into html tags showing the node status, etc. SilverStripe tree node will be typically rendered into html
code like this: code like this:
:::ss
```ss
... ...
<ul> <ul>
... ...
@ -39,6 +41,7 @@ code like this:
... ...
</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:
![Page Node Screenshot](../../../_images/tree_node.png "Page Node") ![Page Node Screenshot](../../../_images/tree_node.png "Page Node")
@ -61,7 +64,9 @@ will be used for the class attribute of &lt;li&gt; tag of the tree node.
### Add new flag ### Add new flag
__Example: using a subclass__ __Example: using a subclass__
:::php
```php
class Page extends SiteTree { class Page extends SiteTree {
public function getScheduledToPublish(){ public function getScheduledToPublish(){
// return either true or false // return either true or false
@ -73,6 +78,7 @@ __Example: using a subclass__
return $flags; 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
__'Scheduled To Publish'__ status. The look of the page node will be changed __'Scheduled To Publish'__ status. The look of the page node will be changed

View File

@ -33,8 +33,8 @@ 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
class CustomSideReport_NameOfReport extends SS_Report { class CustomSideReport_NameOfReport extends SS_Report {
// the name of the report // the name of the report
@ -56,6 +56,7 @@ The following example will create a report to list every page on the current sit
return $fields; 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

@ -28,7 +28,9 @@ Copy the template markup of the base implementation at `framework/admin/template
into `mysite/templates/Includes/LeftAndMain_Menu.ss`. It will automatically be picked up by into `mysite/templates/Includes/LeftAndMain_Menu.ss`. It will automatically be picked up by
the CMS logic. Add a new section into the `<ul class="cms-menu-list">` 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">
<!-- ... --> <!-- ... -->
@ -40,6 +42,7 @@ the CMS logic. Add a new section into the `<ul class="cms-menu-list">`
</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
hardcoded links underneath the left-hand menu. We'll make these dynamic further down. hardcoded links underneath the left-hand menu. We'll make these dynamic further down.
@ -51,16 +54,22 @@ we'll add some CSS, and get it to load
with the CMS interface. Paste the following content into a new file called with the CMS interface. Paste the following content into a new file called
`mysite/css/BookmarkedPages.css`: `mysite/css/BookmarkedPages.css`:
:::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`
[configuration value](../../configuration). [configuration value](../../configuration).
:::yml
```yml
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
@ -69,7 +78,9 @@ the database. For this we need to decorate the page record with a
`DataExtension`. Create a new file called `mysite/code/BookmarkedPageExtension.php` `DataExtension`. Create a new file called `mysite/code/BookmarkedPageExtension.php`
and insert the following code. and insert the following code.
:::php
```php
<?php <?php
class BookmarkedPageExtension extends DataExtension { class BookmarkedPageExtension extends DataExtension {
@ -84,13 +95,17 @@ and insert the following code.
); );
} }
} }
```
Enable the extension in your [configuration file](../../configuration) Enable the extension in your [configuration file](../../configuration)
:::yml
```yml
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`.
Refresh the CMS, open a page for editing and you should see the new checkbox. Refresh the CMS, open a page for editing and you should see the new checkbox.
@ -104,7 +119,9 @@ links)? Again, we extend a core class: The main CMS controller called
Add the following code to a new file `mysite/code/BookmarkedLeftAndMainExtension.php`; Add the following code to a new file `mysite/code/BookmarkedLeftAndMainExtension.php`;
:::php
```php
<?php <?php
class BookmarkedPagesLeftAndMainExtension extends LeftAndMainExtension { class BookmarkedPagesLeftAndMainExtension extends LeftAndMainExtension {
@ -113,19 +130,25 @@ Add the following code to a new file `mysite/code/BookmarkedLeftAndMainExtension
return Page::get()->filter("IsBookmarked", 1); return Page::get()->filter("IsBookmarked", 1);
} }
} }
```
Enable the extension in your [configuration file](../../configuration) Enable the extension in your [configuration file](../../configuration)
:::yml
```yml
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.
Find the `<ul>` you created earlier in `mysite/admin/templates/LeftAndMain.ss` Find the `<ul>` you created earlier in `mysite/admin/templates/LeftAndMain.ss`
and replace it with the following: and replace it with the following:
:::ss
```ss
<ul class="cms-menu-list"> <ul class="cms-menu-list">
<!-- ... --> <!-- ... -->
<% loop $BookmarkedPages %> <% loop $BookmarkedPages %>
@ -134,6 +157,7 @@ and replace it with the following:
</li> </li>
<% end_loop %> <% end_loop %>
</ul> </ul>
```
## Extending the CMS actions ## Extending the CMS actions
@ -165,26 +189,38 @@ First of all we can add a regular standalone button anywhere in the set. Here
we are inserting it in the front of all other actions. We could also add a we are inserting it in the front of all other actions. We could also add a
button group (`CompositeField`) in a similar fashion. 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`
already present in the `FieldList`. 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
infrequently used minor actions. 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
`TabSet`. `TabSet`.
:::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'>
Empty tabs will be automatically removed from the `FieldList` to prevent clutter. Empty tabs will be automatically removed from the `FieldList` to prevent clutter.
@ -205,7 +241,9 @@ Your newly created buttons need handlers to bind to before they will do anything
To implement these handlers, you will need to create a `LeftAndMainExtension` and add To implement these handlers, you will need to create a `LeftAndMainExtension` and add
applicable controller actions to it: applicable controller actions to it:
:::php
```php
class CustomActionsExtension extends LeftAndMainExtension { class CustomActionsExtension extends LeftAndMainExtension {
private static $allowed_actions = array( private static $allowed_actions = array(
@ -218,18 +256,25 @@ applicable controller actions to it:
} }
} }
```
The extension then needs to be registered: The extension then needs to be registered:
:::yaml
```yaml
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

@ -3,20 +3,26 @@
Sometimes you'll work with ModelAdmins from other modules. To customise these interfaces, you can always subclass. But there's Sometimes you'll work with ModelAdmins from other modules. To customise these interfaces, you can always subclass. But there's
also another tool at your disposal: The [Extension](api:SilverStripe\Core\Extension) API. also another tool at your disposal: The [Extension](api:SilverStripe\Core\Extension) API.
:::php
```php
class MyAdminExtension extends Extension { class MyAdminExtension extends Extension {
// ... // ...
public function updateEditForm(&$form) { public function updateEditForm(&$form) {
$form->Fields()->push(/* ... */) $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()`,
`updateSearchForm()`, `updateList()`, `updateImportForm`. `updateSearchForm()`, `updateList()`, `updateImportForm`.

View File

@ -18,7 +18,9 @@ this defines the actions that need to be executed on a flush request.
This example uses [Cache](api:Cache) in some custom code, and the same cache is cleaned on flush: This example uses [Cache](api:Cache) in some custom code, and the same cache is cleaned on flush:
:::php
```php
<?php <?php
class MyClass extends DataObject implements Flushable { class MyClass extends DataObject implements Flushable {
@ -37,6 +39,7 @@ This example uses [Cache](api:Cache) in some custom code, and the same cache is
} }
} }
```
### Using with filesystem ### Using with filesystem
@ -44,7 +47,7 @@ Another example, some temporary files are created in a directory in assets, and
useful in an example like `GD` or `Imagick` generating resampled images, but we want to delete any cached images on useful in an example like `GD` or `Imagick` generating resampled images, but we want to delete any cached images on
flush so they are re-created on demand. flush so they are re-created on demand.
:::php ```php
<?php <?php
class MyClass extends DataObject implements Flushable { class MyClass extends DataObject implements Flushable {
@ -55,4 +58,4 @@ flush so they are re-created on demand.
} }
} }
```

View File

@ -8,9 +8,12 @@ which a SilverStripe application must have available in order for requests to be
This can be accessed in user code via Injector This can be accessed in user code via Injector
:::php
```php
$kernel = Injector::inst()->get(Kernel::class); $kernel = Injector::inst()->get(Kernel::class);
echo "Current environment: " . $kernel->getEnvironment(); echo "Current environment: " . $kernel->getEnvironment();
```
## Kernel services ## Kernel services
@ -35,7 +38,9 @@ As with Config and Injector the Kernel can be nested to safely modify global app
and subsequently restore state. Unlike those classes, however, there is no `::unnest()`. Instead and subsequently restore state. Unlike those classes, however, there is no `::unnest()`. Instead
you should call `->activate()` on the kernel instance you would like to unnest to. you should call `->activate()` on the kernel instance you would like to unnest to.
:::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
@ -46,7 +51,7 @@ you should call `->activate()` on the kernel instance you would like to unnest t
// Any changes to config (or other application state) have now been reverted // Any changes to config (or other application state) have now been reverted
$oldKernel->activate(); $oldKernel->activate();
} }
```
# Application # Application
@ -64,7 +69,9 @@ This class provides basic support for HTTP Middleware, such as [ErrorControlChai
`main.php` contains the default application implementation. `main.php` contains the default application implementation.
:::php
```php
<?php <?php
use SilverStripe\Control\HTTPApplication; use SilverStripe\Control\HTTPApplication;
@ -83,7 +90,7 @@ This class provides basic support for HTTP Middleware, such as [ErrorControlChai
$app->addMiddleware(new ErrorControlChainMiddleware($app)); $app->addMiddleware(new ErrorControlChainMiddleware($app));
$response = $app->handle($request); $response = $app->handle($request);
$response->output(); $response->output();
```
Users can customise their own application by coping the above to a file in `mysite/main.php`, and Users can customise their own application by coping the above to a file in `mysite/main.php`, and
updating their `.htaccess` to point to the new file. updating their `.htaccess` to point to the new file.
@ -102,7 +109,9 @@ Note: This config must also be duplicated in the below template which provide as
`silverstripe-assets/templates/SilverStripe/Assets/Flysystem/PublicAssetAdapter_HTAccess.ss`: `silverstripe-assets/templates/SilverStripe/Assets/Flysystem/PublicAssetAdapter_HTAccess.ss`:
:::ss
```ss
<IfModule mod_rewrite.c> <IfModule mod_rewrite.c>
# ... # ...
# Non existant files passed to requesthandler # Non existant files passed to requesthandler
@ -110,6 +119,7 @@ Note: This config must also be duplicated in the below template which provide as
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* ../mysite/main.php?url=%1 [QSA] RewriteRule .* ../mysite/main.php?url=%1 [QSA]
</IfModule> </IfModule>
```
## Custom application actions ## Custom application actions
@ -124,7 +134,9 @@ For example, the below will setup a request, session, and current controller,
but will leave the application in a "ready" state without performing any but will leave the application in a "ready" state without performing any
routing. 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);
@ -138,6 +150,6 @@ routing.
$controller->pushCurrent(); $controller->pushCurrent();
$controller->doInit(); $controller->doInit();
}, true); }, true);
```

View File

@ -8,10 +8,11 @@ needs to interface over the command line.
The main entry point for any command line execution is `framework/cli-script.php`. For example, to run a database The main entry point for any command line execution is `framework/cli-script.php`. For example, to run a database
rebuild from the command line, use this command: rebuild from the command line, use this command:
```bash
:::bash
cd your-webroot/ cd your-webroot/
php framework/cli-script.php dev/build php framework/cli-script.php dev/build
```
<div class="notice"> <div class="notice">
Your command line php version is likely to use a different configuration as your webserver (run `php -i` to find out Your command line php version is likely to use a different configuration as your webserver (run `php -i` to find out
@ -32,10 +33,10 @@ when running the command php -v, then you may not have php-cli installed so sake
### Installation ### Installation
`sake` can be invoked using `./framework/sake`. For easier access, copy the `sake` file into `/usr/bin/sake`. `sake` can be invoked using `./framework/sake`. For easier access, copy the `sake` file into `/usr/bin/sake`.
```
cd your-webroot/ cd your-webroot/
sudo ./framework/sake installsake sudo ./framework/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>
@ -56,22 +57,27 @@ SS_BASE_URL="http://localhost/base-url"
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 / sake /
# returns the homepage # returns the homepage
sake dev/ sake dev/
# shows a list of development operations # shows a list of development operations
```
Sake is particularly useful for running build tasks. Sake is particularly useful for running build tasks.
```bash
:::bash
sake dev/build "flush=1" 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
:::bash
sake dev/tasks/MyReallyLongTask sake dev/tasks/MyReallyLongTask
```
### Running processes ### Running processes
@ -85,7 +91,9 @@ sleep when the process is in the middle of doing things, and a long sleep when d
This code provides a good template: This code provides a good template:
:::php
```php
<?php <?php
class MyProcess extends Controller { class MyProcess extends Controller {
@ -107,14 +115,16 @@ This code provides a good template:
} }
} }
} }
```
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">
`sake` stores `pid` and log files in the site root directory. `sake` stores `pid` and log files in the site root directory.
@ -124,14 +134,20 @@ Then the process can be managed through `sake`
Parameters can be added to the command. All parameters will be available in `$_GET` array on the server. 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 framework/cli-script.php myurl myparam=1 myotherparam=2 php framework/cli-script.php myurl myparam=1 myotherparam=2
```
Or if you're using `sake` Or if you're using `sake`
:::bash
```bash
sake myurl "myparam=1&myotherparam=2" sake myurl "myparam=1&myotherparam=2"
```
## Running Regular Tasks With Cron ## Running Regular Tasks With Cron
@ -140,5 +156,6 @@ 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/sake dev/tasks/MyTask
```

View File

@ -13,30 +13,38 @@ the [Cookie](api:SilverStripe\Control\Cookie) class. This class mostly follows t
Sets the value of cookie with configuration. Sets the value of cookie with configuration.
:::php
```php
Cookie::set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = false); Cookie::set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = false);
// Cookie::set('MyApplicationPreference', 'Yes'); // Cookie::set('MyApplicationPreference', 'Yes');
```
### get ### get
Returns the value of cookie. 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
Clears a given cookie. 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
@ -47,7 +55,9 @@ that fetches, sets and expires cookies. By default we use a [CookieJar](api:Silv
The [CookieJar](api:SilverStripe\Control\CookieJar) keeps track of cookies that have been set by the current process as well as those that were received The [CookieJar](api:SilverStripe\Control\CookieJar) keeps track of cookies that have been set by the current process as well as those that were received
from the browser. from the browser.
:::php
```php
$myCookies = array( $myCookies = array(
'cookie1' => 'value1', 'cookie1' => 'value1',
); );
@ -57,6 +67,7 @@ from the browser.
Injector::inst()->registerService($newBackend, 'Cookie_Backend'); Injector::inst()->registerService($newBackend, 'Cookie_Backend');
Cookie::get('cookie1'); Cookie::get('cookie1');
```
### Resetting the Cookie_Backend state ### Resetting the Cookie_Backend state
@ -64,28 +75,34 @@ Assuming that your application hasn't messed around with the `$_COOKIE` superglo
`Cookie_Backend` by simply unregistering the `CookieJar` service with `Injector`. Next time you access `Cookie` it'll `Cookie_Backend` by simply unregistering the `CookieJar` service with `Injector`. Next time you access `Cookie` it'll
create a new service for you using the `$_COOKIE` superglobal. 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
the current `CookieJar` service to tell you what it was like when it was registered. the current `CookieJar` service to tell you what it was like when it was registered.
:::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
If you need to implement your own Cookie_Backend you can use the injector system to force a different class to be used. If you need to implement your own Cookie_Backend you can use the injector system to force a different class to be used.
:::yml
```yml
--- ---
Name: mycookie Name: mycookie
After: '#cookie' After: '#cookie'
@ -93,6 +110,7 @@ If you need to implement your own Cookie_Backend you can use the injector system
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.
@ -105,23 +123,28 @@ came from the browser as part of the request.
Using the `Cookie_Backend` we can do this like such: 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
One can also access all of the cookies in one go using the `Cookie_Backend` 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

@ -12,26 +12,35 @@ unit-testing, you can create multiple Controllers, each with their own session.
## set ## set
:::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
size restrictions as to how much you can save). size restrictions as to how much you can save).
:::php
```php
// saves an array // saves an array
Session::set('MyArrayOfValues', array('1','2','3')); Session::set('MyArrayOfValues', array('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));
```
## get ## get
Once you have saved a value to the Session you can access it by using the `get` function. Like the `set` function you Once you have saved a value to the Session you can access it by using the `get` function. Like the `set` function you
can use this anywhere in your PHP files. can use this anywhere in your PHP files.
:::php
```php
echo Session::get('MyValue'); echo Session::get('MyValue');
// returns 6 // returns 6
@ -40,36 +49,43 @@ can use this anywhere in your PHP files.
$object = unserialize(Session::get('MyObject', $object)); $object = unserialize(Session::get('MyObject', $object));
// $object = Object() // $object = Object()
```
## get_all ## get_all
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::get_all(); Session::get_all();
// returns an array of all the session values. // returns an array of all the session values.
```
## clear ## clear
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::clear_all(); Session::clear_all();
```
## Secure Session Cookie ## Secure Session Cookie
In certain circumstances, you may want to use a different `session_name` cookie when using the `https` protocol for security purposes. To do this, you may set the `cookie_secure` parameter to `true` on your `config.yml` In certain circumstances, you may want to use a different `session_name` cookie when using the `https` protocol for security purposes. To do this, you may set the `cookie_secure` parameter to `true` on your `config.yml`
:::yml
```yml
Session: Session:
cookie_secure: true cookie_secure: true
```
This uses the session_name `SECSESSID` for `https` connections instead of the default `PHPSESSID`. Doing so adds an extra layer of security to your session cookie since you no longer share `http` and `https` sessions. This uses the session_name `SECSESSID` for `https` connections instead of the default `PHPSESSID`. Doing so adds an extra layer of security to your session cookie since you no longer share `http` and `https` sessions.

View File

@ -212,14 +212,15 @@ 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 finally fixed this dumb rendering bug that Joe talked about ... LOL
also added another form field for password validation also added another form field for password validation
```
Example: Good commit message Example: Good commit message
```
BUG Formatting through prepValueForDB() BUG Formatting through prepValueForDB()
Added prepValueForDB() which is called on DBField->writeToManipulation() 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). to ensure formatting of value before insertion to DB on a per-DBField type basis (fixes #1234).
Added documentation for DBField->writeToManipulation() (related to a4bd42fd). Added documentation for DBField->writeToManipulation() (related to a4bd42fd).
```

View File

@ -61,7 +61,9 @@ How to deprecate an API:
Here's an example for replacing `Director::isDev()` with a (theoretical) `Env::is_dev()`: Here's an example for replacing `Director::isDev()` with a (theoretical) `Env::is_dev()`:
:::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.
@ -70,6 +72,7 @@ Here's an example for replacing `Director::isDev()` with a (theoretical) `Env::i
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
(e.g. *3.3.0*, *3.4.0*), until a new major release (e.g. *4.0.0*), at which point it gets removed from the codebase. (e.g. *3.3.0*, *3.4.0*), until a new major release (e.g. *4.0.0*), at which point it gets removed from the codebase.
@ -82,9 +85,11 @@ notices are always disabled on both live and test.
`mysite/_config.php` `mysite/_config.php`
:::php
Deprecation::set_enabled(false);
```php
Deprecation::set_enabled(false);
```
`.env` `.env`

View File

@ -39,7 +39,7 @@ As a core contributor it is necessary to have installed the following set of too
* A good `.env` setup in your localhost webroot. * 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"
@ -59,7 +59,7 @@ Example `.env`:
# 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
the [core committers](core_committers), who will assist with setting up your credentials. the [core committers](core_committers), who will assist with setting up your credentials.
@ -168,9 +168,9 @@ doe not make any upstream changes (so it's safe to run without worrying about
any mistakes migrating their way into the public sphere). 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,9 +238,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,31 @@ 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

@ -71,13 +71,14 @@ under the "silverstripe" user, see
Translations need to be reviewed before being committed, which is a process that happens roughly once per month. We're 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
:::bash
tx pull tx pull
# Manually review changes through git diff, then commit # Manually review changes through git diff, then commit
git add lang/* git add lang/*
git commit -m "Updated translations" git commit -m "Updated translations"
```
<div class="notice" markdown="1"> <div class="notice" markdown="1">
You can download your work right from Transifex in order to speed up the process for your desired language. You can download your work right from Transifex in order to speed up the process for your desired language.
@ -88,9 +89,10 @@ You can download your work right from Transifex in order to speed up the process
SilverStripe also supports translating strings in JavaScript (see [i18n](/developer_guides/i18n)), but there's a 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
:::js
ss.i18n.addDictionary('de', {'MyNamespace.MyKey': 'My Translation'}); ss.i18n.addDictionary('de', {'MyNamespace.MyKey': 'My Translation'});
```
But Transifex only accepts structured formats like JSON. But Transifex only accepts structured formats like JSON.
@ -99,8 +101,8 @@ 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] [main]
host = https://www.transifex.com host = https://www.transifex.com
@ -115,16 +117,17 @@ First of all, you need to create those source files in JSON, and store them in `
source_file = javascript/lang/src/en.js source_file = javascript/lang/src/en.js
source_lang = en source_lang = en
type = KEYVALUEJSON 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

@ -25,19 +25,23 @@ except when necessitated by third party conventions (e.g using PHP's `Serializab
SilverStripe's [Config API]() can read its defaults from variables declared as `private static` on classes. SilverStripe's [Config API]() can read its defaults from variables declared as `private static` on classes.
As opposed to other variables, these should be declared as lower case with underscores. As opposed to other variables, these should be declared as lower case with underscores.
:::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 (==)
Where possible, use type-strict identical comparisons instead of loosely typed equality comparisons. Where possible, use type-strict identical comparisons instead of loosely typed equality comparisons.
Read more in the PHP documentation for [comparison operators](http://php.net/manual/en/language.operators.comparison.php) and [object comparison](http://php.net/manual/en/language.oop5.object-comparison.php). Read more in the PHP documentation for [comparison operators](http://php.net/manual/en/language.operators.comparison.php) and [object comparison](http://php.net/manual/en/language.oop5.object-comparison.php).
:::php
```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();
@ -47,13 +51,15 @@ Read more in the PHP documentation for [comparison operators](http://php.net/man
if ($a == 100) { if ($a == 100) {
doThis(); doThis();
} }
```
## Separation of Logic and Presentation ## Separation of Logic and Presentation
Try to avoid using PHP's ability to mix HTML into the code. 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>";
@ -61,10 +67,13 @@ Try to avoid using PHP's ability to mix HTML into the code.
// Template code // Template code
$Title $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";
@ -72,6 +81,7 @@ Better: Keep HTML in template files:
// Template code // Template code
<h2>$Title</h2> <h2>$Title</h2>
```
## Comments ## Comments
@ -87,7 +97,9 @@ and [tag overview](http://manual.phpdoc.org/HTMLSmartyConverter/HandS/phpDocumen
Example: Example:
:::php
```php
/** /**
* My short description for this class. * My short description for this class.
* My longer description with * My longer description with
@ -103,7 +115,6 @@ Example:
*/ */
class MyClass extends Class class MyClass extends Class
{ {
/** /**
* My Method. * My Method.
* This method returns something cool. {@link MyParentMethod} has other cool stuff in it. * This method returns something cool. {@link MyParentMethod} has other cool stuff in it.
@ -117,6 +128,7 @@ Example:
} }
} }
```
## Class Member Ordering ## Class Member Ordering
@ -137,16 +149,22 @@ Put code into the classes in the following order (where applicable).
If you have to use raw SQL, make sure your code works across databases. Make sure you escape your queries like below, If you have to use raw SQL, make sure your code works across databases. Make sure you escape your queries like below,
with the column or table name escaped with double quotes as below. with the column or table name escaped with double quotes as below.
:::php
```php
MyClass::get()->where(array("\"Score\" > ?" => 50)); MyClass::get()->where(array("\"Score\" > ?" => 50));
```
It is preferable to use parameterised queries whenever necessary to provide conditions It is preferable to use parameterised queries whenever necessary to provide conditions
to a SQL query, where values placeholders are each replaced with a single unquoted question mark. to a SQL query, where values placeholders are each replaced with a single unquoted question mark.
If it's absolutely necessary to use literal values in a query make sure that values If it's absolutely necessary to use literal values in a query make sure that values
are single quoted. 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.