mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Model guide documentation
This commit is contained in:
parent
19259e0497
commit
30a85be38e
566
docs/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md
Normal file
566
docs/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md
Normal file
@ -0,0 +1,566 @@
|
|||||||
|
title: Introduction to the Data Model and ORM
|
||||||
|
summary: Introduction to creating and querying a Data Model through the ORM.
|
||||||
|
|
||||||
|
# Introduction to the Data Model and ORM
|
||||||
|
|
||||||
|
SilverStripe uses an [object-relational model](http://en.wikipedia.org/wiki/Object-relational_model) to represent it's
|
||||||
|
information.
|
||||||
|
|
||||||
|
* Each database-table maps to a PHP class.
|
||||||
|
* Each database-row maps to a PHP object.
|
||||||
|
* Each database-column maps to a property on a PHP object.
|
||||||
|
|
||||||
|
All data tables in SilverStripe are defined as subclasses of [api:DataObject]. The [api:DataObject] class represents a
|
||||||
|
single row in a database table, following the ["Active Record"](http://en.wikipedia.org/wiki/Active_record_pattern)
|
||||||
|
design pattern. Database Columns are is defined as [Data Types](data_types_and_casting) in the static `$db` variable
|
||||||
|
along with any [relationships](../relations) defined as `$has_one`, `$has_many`, `$many_many` properties on the class.
|
||||||
|
|
||||||
|
Let's look at a simple example:
|
||||||
|
|
||||||
|
**mysite/code/Player.php**
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'PlayerNumber' => 'Int',
|
||||||
|
'FirstName' => 'Varchar(255)',
|
||||||
|
'LastName' => 'Text',
|
||||||
|
'Birthday' => 'Date'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
This `Player` class definition will create a database table `Player` with columns for `PlayerNumber`, `FirstName` and
|
||||||
|
so on. After writing this class, we need to regenerate the database schema.
|
||||||
|
|
||||||
|
## Generating the Database Schema
|
||||||
|
|
||||||
|
After adding, modifying or removing `DataObject` classes make sure to rebuild your SilverStripe database. The
|
||||||
|
database-schema is generated automatically by visiting the URL http://www.yoursite.com/dev/build.
|
||||||
|
|
||||||
|
This script will analyze the existing schema, compare it to what's required by your data classes, and alter the schema
|
||||||
|
as required.
|
||||||
|
|
||||||
|
It will perform the following changes:
|
||||||
|
|
||||||
|
* Create any missing tables
|
||||||
|
* Create any missing fields
|
||||||
|
* Create any missing indexes
|
||||||
|
* Alter the field type of any existing fields
|
||||||
|
* Rename any obsolete tables that it previously created to _obsolete_(tablename)
|
||||||
|
|
||||||
|
It **won't** do any of the following
|
||||||
|
|
||||||
|
* Deleting tables
|
||||||
|
* Deleting fields
|
||||||
|
* Rename any tables that it doesn't recognize - so other applications can co-exist in the same database, as long as
|
||||||
|
their table names don't match a SilverStripe data class.
|
||||||
|
|
||||||
|
|
||||||
|
<div class="notice" markdown='1'>
|
||||||
|
You need to be logged in as an administrator to perform this command, unless your site is in [dev mode](../debugging),
|
||||||
|
or the command is run through [CLI](../cli).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
When rebuilding the database schema through the [api:SS_ClassLoader] the following additional properties are
|
||||||
|
automatically set on the `DataObject`.
|
||||||
|
|
||||||
|
* ID: Primary Key. When a new record is created, SilverStripe does not use the database's built-in auto-numbering
|
||||||
|
system. Instead, it will generate a new `ID` by adding 1 to the current maximum ID.
|
||||||
|
* ClassName: An enumeration listing this data-class and all of its subclasses.
|
||||||
|
* Created: A date/time field set to the creation date of this record
|
||||||
|
* LastEdited: A date/time field set to the date this record was last edited through `write()`
|
||||||
|
|
||||||
|
**mysite/code/Player.php**
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'PlayerNumber' => 'Int',
|
||||||
|
'FirstName' => 'Varchar(255)',
|
||||||
|
'LastName' => 'Text',
|
||||||
|
'Birthday' => 'Date'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Generates the following `SQL`.
|
||||||
|
|
||||||
|
CREATE TABLE `Player` (
|
||||||
|
`ID` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`ClassName` enum('Player') DEFAULT 'Player',
|
||||||
|
`LastEdited` datetime DEFAULT NULL,
|
||||||
|
`Created` datetime DEFAULT NULL,
|
||||||
|
`PlayerNumber` int(11) NOT NULL DEFAULT '0',
|
||||||
|
`FirstName` varchar(255) DEFAULT NULL,
|
||||||
|
`LastName` mediumtext,
|
||||||
|
`Birthday` datetime DEFAULT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`ID`),
|
||||||
|
KEY `ClassName` (`ClassName`)
|
||||||
|
);
|
||||||
|
|
||||||
|
## Creating Data Records
|
||||||
|
|
||||||
|
A new instance of a [api:DataObject] can be created using the `new` syntax.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$player = new Player();
|
||||||
|
|
||||||
|
Or, a better way is to use the `create` method.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$player = Player::create();
|
||||||
|
|
||||||
|
Database columns and properties can be set as class properties on the object. The SilverStripe ORM handles the saving
|
||||||
|
of the values through a custom `__set()` method.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$player->FirstName = "Sam";
|
||||||
|
$player->PlayerNumber = 07;
|
||||||
|
|
||||||
|
To save the `DataObject` to the database use the `write()` method. The first time `write()` is called an `ID` will be
|
||||||
|
set.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$player->write();
|
||||||
|
|
||||||
|
## Querying Data
|
||||||
|
|
||||||
|
With the `Player` class defined we can query our data using the `ORM` or Object-Relational Model. The `ORM` provides
|
||||||
|
shortcuts and methods for fetching, sorting and filtering data from our database.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get();
|
||||||
|
// returns a `DataList` containing all the `Player` objects.
|
||||||
|
|
||||||
|
$player = Player::get()->byId(2);
|
||||||
|
// returns a single `Player` object instance that has the ID of 2.
|
||||||
|
|
||||||
|
echo $player->ID;
|
||||||
|
// returns the players 'ID' column value
|
||||||
|
|
||||||
|
echo $player->dbObject('LastEdited')->Ago();
|
||||||
|
// calls the `Ago` method on the `LastEdited` property.
|
||||||
|
|
||||||
|
The `ORM` uses a "fluent" syntax, where you specify a query by chaining together different methods. Two common methods
|
||||||
|
are `filter()` and `sort()`:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$members = Player::get()->filter(array(
|
||||||
|
'FirstName' => 'Sam'
|
||||||
|
))->sort('Surname');
|
||||||
|
|
||||||
|
// returns a `DataList` containing all the `Player` records that have the `FirstName` of 'Sam'
|
||||||
|
|
||||||
|
<div class="info" markdown="1">
|
||||||
|
Provided `filter` values are automatically escaped and do not require any escaping.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Lazy Loading
|
||||||
|
|
||||||
|
The `ORM` doesn't actually execute the [api:SQLQuery] until you iterate on the result with a `foreach()` or `<% loop %>`.
|
||||||
|
|
||||||
|
It's smart enough to generate a single efficient query at the last moment in time without needing to post process the
|
||||||
|
result set in PHP. In `MySQL` the query generated by the ORM may look something like this
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->filter(array(
|
||||||
|
'FirstName' => 'Sam'
|
||||||
|
));
|
||||||
|
|
||||||
|
$players = $players->sort('Surname');
|
||||||
|
|
||||||
|
// executes the following single query
|
||||||
|
// SELECT * FROM Player WHERE FirstName = 'Sam' ORDER BY Surname
|
||||||
|
|
||||||
|
|
||||||
|
This also means that getting the count of a list of objects will be done with a single, efficient query.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->filter(array(
|
||||||
|
'FirstName' => 'Sam'
|
||||||
|
))->sort('Surname');
|
||||||
|
|
||||||
|
// This will create an single SELECT COUNT query
|
||||||
|
// SELECT COUNT(*) FROM Player WHERE FirstName = 'Sam'
|
||||||
|
echo $players->Count();
|
||||||
|
|
||||||
|
## Looping over a list of objects
|
||||||
|
|
||||||
|
`get()` returns a `DataList` instance. You can loop over `DataList` instances in both PHP and templates.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get();
|
||||||
|
|
||||||
|
foreach($players as $player) {
|
||||||
|
echo $player->FirstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
See the [Lists](../lists) documentation for more information on dealing with [api:SS_List] instances.
|
||||||
|
|
||||||
|
## Returning a single DataObject
|
||||||
|
|
||||||
|
There are a couple of ways of getting a single DataObject from the ORM. If you know the ID number of the object, you
|
||||||
|
can use `byID($id)`:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$player = Player::get()->byID(5);
|
||||||
|
|
||||||
|
`get()` returns a [api:DataList] instance. You can use operations on that to get back a single record.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get();
|
||||||
|
|
||||||
|
$first = $players->first();
|
||||||
|
$last = $players->last();
|
||||||
|
|
||||||
|
## Sorting
|
||||||
|
|
||||||
|
If would like to sort the list by `FirstName` in a ascending way (from A to Z).
|
||||||
|
|
||||||
|
:::php
|
||||||
|
// Sort can either be Ascending (ASC) or Descending (DESC)
|
||||||
|
$players = Player::get()->sort('FirstName', 'ASC');
|
||||||
|
|
||||||
|
// Ascending is implied
|
||||||
|
$players = Player::get()->sort('FirstName');
|
||||||
|
|
||||||
|
To reverse the sort
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->sort('FirstName', 'DESC');
|
||||||
|
|
||||||
|
// or..
|
||||||
|
$players = Player::get()->sort('FirstName', 'ASC')->reverse();
|
||||||
|
|
||||||
|
However you might have several entries with the same `FirstName` and would like to sort them by `FirstName` and
|
||||||
|
`LastName`
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Players::get()->sort(array(
|
||||||
|
'FirstName' => 'ASC',
|
||||||
|
'LastName'=>'ASC'
|
||||||
|
));
|
||||||
|
|
||||||
|
You can also sort randomly.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->sort('RAND()')
|
||||||
|
|
||||||
|
|
||||||
|
## Filtering Results
|
||||||
|
|
||||||
|
The `filter()` method filters the list of objects that gets returned.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->filter(array(
|
||||||
|
'FirstName' => 'Sam'
|
||||||
|
));
|
||||||
|
|
||||||
|
Each element of the array specifies a filter. You can specify as many filters as you like, and they **all** must be
|
||||||
|
true for the record to be included in the result.
|
||||||
|
|
||||||
|
The key in the filter corresponds to the field that you want to filter and the value in the filter corresponds to the
|
||||||
|
value that you want to filter to.
|
||||||
|
|
||||||
|
So, this would return only those players called "Sam Minnée".
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->filter(array(
|
||||||
|
'FirstName' => 'Sam',
|
||||||
|
'LastName' => 'Minnée',
|
||||||
|
));
|
||||||
|
|
||||||
|
// SELECT * FROM Player WHERE FirstName = 'Sam' AND LastName = 'Minnée'
|
||||||
|
|
||||||
|
There is also a short hand way of getting Players with the FirstName of Sam.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->filter('FirstName', 'Sam');
|
||||||
|
|
||||||
|
Or if you want to find both Sam and Sig.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->filter(
|
||||||
|
'FirstName', array('Sam', 'Sig')
|
||||||
|
);
|
||||||
|
|
||||||
|
// SELECT * FROM Player WHERE FirstName IN ('Sam', 'Sig')
|
||||||
|
|
||||||
|
You can use [SearchFilters](searchfilters) to add additional behavior to your `filter` command rather than an
|
||||||
|
exact match.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->filter(array(
|
||||||
|
'FirstName:StartsWith' => 'S'
|
||||||
|
'PlayerNumber:GreaterThan' => '10'
|
||||||
|
));
|
||||||
|
|
||||||
|
### filterAny
|
||||||
|
|
||||||
|
Use the `filterAny()` method to match multiple criteria non-exclusively (with an "OR" disjunctive),
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->filterAny(array(
|
||||||
|
'FirstName' => 'Sam',
|
||||||
|
'Age' => 17,
|
||||||
|
));
|
||||||
|
|
||||||
|
// SELECT * FROM Player WHERE ("FirstName" = 'Sam' OR "Age" = '17')
|
||||||
|
|
||||||
|
You can combine both conjunctive ("AND") and disjunctive ("OR") statements.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()
|
||||||
|
->filter(array(
|
||||||
|
'LastName' => 'Minnée'
|
||||||
|
))
|
||||||
|
->filterAny(array(
|
||||||
|
'FirstName' => 'Sam',
|
||||||
|
'Age' => 17,
|
||||||
|
));
|
||||||
|
// SELECT * FROM Player WHERE ("LastName" = 'Minnée' AND ("FirstName" = 'Sam' OR "Age" = '17'))
|
||||||
|
|
||||||
|
You can use [SearchFilters](searchfilters) to add additional behavior to your `filterAny` command.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->filterAny(array(
|
||||||
|
'FirstName:StartsWith' => 'S'
|
||||||
|
'PlayerNumber:GreaterThan' => '10'
|
||||||
|
));
|
||||||
|
|
||||||
|
|
||||||
|
### filterByCallback
|
||||||
|
|
||||||
|
It is also possible to filter by a PHP callback, this will force the data model to fetch all records and loop them in
|
||||||
|
PHP, thus `filter()` or `filterAny()` are to be preferred over `filterByCallback()`.
|
||||||
|
|
||||||
|
<div class="notice" markdown="1">
|
||||||
|
Because `filterByCallback()` has to run in PHP, it will always return an `ArrayList`
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The first parameter to the callback is the item, the second parameter is the list itself. The callback will run once
|
||||||
|
for each record, if the callback returns true, this record will be added to the list of returned items.
|
||||||
|
|
||||||
|
The below example will get all `Players` aged over 10.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->filterByCallback(function($item, $list) {
|
||||||
|
return ($item->Age() > 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
### Exclude
|
||||||
|
|
||||||
|
The `exclude()` method is the opposite to the filter in that it removes entries from a list.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->exclude('FirstName', 'Sam');
|
||||||
|
|
||||||
|
// SELECT * FROM Player WHERE FirstName != 'Sam'
|
||||||
|
|
||||||
|
Remove both Sam and Sig..
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->exclude(
|
||||||
|
'FirstName', array('Sam','Sig')
|
||||||
|
);
|
||||||
|
|
||||||
|
`Exclude` follows the same pattern as filter, so for removing only Sam Minnée from the list:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->exclude(array(
|
||||||
|
'FirstName' => 'Sam',
|
||||||
|
'Surname' => 'Minnée',
|
||||||
|
));
|
||||||
|
|
||||||
|
And removing Sig and Sam with that are either age 17 or 74.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->exclude(array(
|
||||||
|
'FirstName' => array('Sam', 'Sig'),
|
||||||
|
'Age' => array(17, 43)
|
||||||
|
));
|
||||||
|
|
||||||
|
// SELECT * FROM Player WHERE ("FirstName" NOT IN ('Sam','Sig) OR "Age" NOT IN ('17', '74));
|
||||||
|
|
||||||
|
You can use [SearchFilters](searchfilters) to add additional behavior to your `exclude` command.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->exclude(array(
|
||||||
|
'FirstName:EndsWith' => 'S'
|
||||||
|
'PlayerNumber:LessThanOrEqual' => '10'
|
||||||
|
));
|
||||||
|
|
||||||
|
### Subtract
|
||||||
|
|
||||||
|
You can subtract entries from a [api:DataList] by passing in another DataList to `subtract()`
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$sam = Player::get()->filter('FirstName', 'Sam');
|
||||||
|
$players = Player::get();
|
||||||
|
|
||||||
|
$noSams = $players->subtract($sam);
|
||||||
|
|
||||||
|
Though for the above example it would probably be easier to use `filter()` and `exclude()`. A better use case could be
|
||||||
|
when you want to find all the members that does not exist in a Group.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
// ... Finding all members that does not belong to $group.
|
||||||
|
$otherMembers = Member::get()->subtract($group->Members());
|
||||||
|
|
||||||
|
### Limit
|
||||||
|
|
||||||
|
You can limit the amount of records returned in a DataList by using the `limit()` method.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$members = Member::get()->limit(5);
|
||||||
|
|
||||||
|
`limit()` accepts two arguments, the first being the amount of results you want returned, with an optional second
|
||||||
|
parameter to specify the offset, which allows you to tell the system where to start getting the results from. The
|
||||||
|
offset, if not provided as an argument, will default to 0.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
// Return 10 members with an offset of 4 (starting from the 5th result).
|
||||||
|
$members = Member::get()->sort('Surname')->limit(10, 4);
|
||||||
|
|
||||||
|
<div class="alert">
|
||||||
|
Note that the `limit` argument order is different from a MySQL LIMIT clause.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Raw SQL
|
||||||
|
|
||||||
|
Occasionally, the system described above won't let you do exactly what you need to do. In these situations, we have
|
||||||
|
methods that manipulate the SQL query at a lower level. When using these, please ensure that all table & field names
|
||||||
|
are escaped with double quotes, otherwise some DB back-ends (e.g. PostgreSQL) won't work.
|
||||||
|
|
||||||
|
Under the hood, query generation is handled by the `[api:DataQuery]` class. This class does provide more direct access
|
||||||
|
to certain SQL features that `DataList` abstracts away from you.
|
||||||
|
|
||||||
|
In general, we advise against using these methods unless it's absolutely necessary. If the ORM doesn't do quite what
|
||||||
|
you need it to, you may also consider extending the ORM with new data types or filter modifiers
|
||||||
|
|
||||||
|
#### Where clauses
|
||||||
|
|
||||||
|
You can specify a WHERE clause fragment (that will be combined with other filters using AND) with the `where()` method:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$members = Member::get()->where("\"FirstName\" = 'Sam'")
|
||||||
|
|
||||||
|
#### Joining Tables
|
||||||
|
|
||||||
|
You can specify a join with the `innerJoin` and `leftJoin` methods. Both of these methods have the same arguments:
|
||||||
|
|
||||||
|
* The name of the table to join to.
|
||||||
|
* The filter clause for the join.
|
||||||
|
* An optional alias.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
// Without an alias
|
||||||
|
$members = Member::get()
|
||||||
|
->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
|
||||||
|
|
||||||
|
$members = Member::get()
|
||||||
|
->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel");
|
||||||
|
|
||||||
|
<div class="alert" markdown="1">
|
||||||
|
Passing a *$join* statement to will filter results further by the JOINs performed against the foreign table. It will
|
||||||
|
**not** return the additionally joined data.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Default Values
|
||||||
|
|
||||||
|
Define the default values for all the `$db` fields. This example sets the "Status"-column on Player to "Active"
|
||||||
|
whenever a new object is created.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
private static $defaults = array(
|
||||||
|
"Status" => 'Active',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="notice" markdown='1'>
|
||||||
|
Note: Alternatively you can set defaults directly in the database-schema (rather than the object-model). See
|
||||||
|
[Data Types and Casting](data-types) for details.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Subclasses
|
||||||
|
|
||||||
|
|
||||||
|
Inheritance is supported in the data model: separate tables will be linked together, the data spread across these
|
||||||
|
tables. The mapping and saving logic is handled by SilverStripe, you don't need to worry about writing SQL most of the
|
||||||
|
time.
|
||||||
|
|
||||||
|
For example, suppose we have the following set of classes:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Page extends SiteTree {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class NewsPage extends Page {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Summary' => 'Text'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
The data for the following classes would be stored across the following tables:
|
||||||
|
|
||||||
|
:::yml
|
||||||
|
SiteTree:
|
||||||
|
- ID: Int
|
||||||
|
- ClassName: Enum('SiteTree', 'Page', 'NewsPage')
|
||||||
|
- Created: Datetime
|
||||||
|
- LastEdited: Datetime
|
||||||
|
- Title: Varchar
|
||||||
|
- Content: Text
|
||||||
|
NewsArticle:
|
||||||
|
- ID: Int
|
||||||
|
- Summary: Text
|
||||||
|
|
||||||
|
Accessing the data is transparent to the developer.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$news = NewsPage::get();
|
||||||
|
|
||||||
|
foreach($news as $article) {
|
||||||
|
echo $news->Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
The way the ORM stores the data is this:
|
||||||
|
|
||||||
|
* "Base classes" are direct sub-classes of [api:DataObject]. They are always given a table, whether or not they have
|
||||||
|
special fields. This is called the "base table". In our case, `SiteTree` is the base table.
|
||||||
|
|
||||||
|
* The base table's ClassName field is set to class of the given record. It's an enumeration of all possible
|
||||||
|
sub-classes of the base class (including the base class itself).
|
||||||
|
|
||||||
|
* Each sub-class of the base object will also be given its own table, *as long as it has custom fields*. In the
|
||||||
|
example above, NewsSection didn't have its own data and so an extra table would be redundant.
|
||||||
|
|
||||||
|
* In all the tables, ID is the primary key. A matching ID number is used for all parts of a particular record:
|
||||||
|
record #2 in Page refers to the same object as record #2 in `[api:SiteTree]`.
|
||||||
|
|
||||||
|
To retrieve a news article, SilverStripe joins the [api:SiteTree], [api:Page] and NewsArticle tables by their ID fields.
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
* [Data Types and Casting](../data_types_and_casting)
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
* [api:DataObject]
|
||||||
|
* [api:DataList]
|
||||||
|
* [api:DataQuery]
|
@ -1,125 +0,0 @@
|
|||||||
# Page Types
|
|
||||||
|
|
||||||
Hi people
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
Page Types are the basic building blocks of any SilverStripe website. A page type can define:
|
|
||||||
|
|
||||||
* Templates being used to display content
|
|
||||||
* Form fields available to edit content in the CMS
|
|
||||||
* Behaviour specific to a page type. For example a contact form on a ‘Contact Us’ page type, sending an email when the form is submitted
|
|
||||||
|
|
||||||
All the pages on the base installation are of the page type called "Page". See
|
|
||||||
[tutorial:2-extending-a-basic-site](/tutorials/2-extending-a-basic-site) for a good introduction to page-types.
|
|
||||||
|
|
||||||
## Class and Template Inheritance
|
|
||||||
|
|
||||||
Each page type on your website is a sub-class of the `SiteTree` class. Usually, you’ll define a class called `Page`
|
|
||||||
and use this template to lay out the basic design elements that don’t change.
|
|
||||||
|
|
||||||
![](_images/pagetype-inheritance.png)
|
|
||||||
|
|
||||||
Each page type is represented by two classes: a data object and a controller. In the diagrams above and below, the data
|
|
||||||
objects are black and the controllers are blue. The page controllers are only used when the page type is actually
|
|
||||||
visited on the website. In our example above, the search form would become a method on the ‘Page_Controller’ class.
|
|
||||||
Any methods put on the data object will be available wherever we use this page. For example, we put any customizations
|
|
||||||
we want to do to the CMS for this page type in here.
|
|
||||||
|
|
||||||
![](_images/controllers-and-dataobjects.png)
|
|
||||||
|
|
||||||
We put the `Page` class into a file called `Page.php` inside `mysite/code`.
|
|
||||||
As a convention, we also put the `Page_Controller` class in the same file.
|
|
||||||
|
|
||||||
Why do we sub-class `Page` for everything? The easiest way to explain this is to use the example of a search form. If we
|
|
||||||
create a search form on the `Page` class, then any other sub-class can also use it in their templates. This saves us
|
|
||||||
re-defining commonly used forms or controls in every class we use.
|
|
||||||
|
|
||||||
## Templates
|
|
||||||
|
|
||||||
Page type templates work much the same as other [templates](/reference/templates) in SilverStripe
|
|
||||||
(see ). There's some specialized controls and placeholders, as well as built-in inheritance.
|
|
||||||
This is explained in a more in-depth topic at [Page Type Templates](/topics/page-type-templates).
|
|
||||||
|
|
||||||
## Adding Database Fields
|
|
||||||
|
|
||||||
Adding database fields is a simple process. You define them in an array of the static variable `$db`, this array is
|
|
||||||
added on the object class. For example, Page or StaffPage. Every time you run dev/build to recompile the manifest, it
|
|
||||||
checks if any new entries are added to the `$db` array and adds any fields to the database that are missing.
|
|
||||||
|
|
||||||
For example, you may want an additional field on a `StaffPage` class which extends `Page`, called `Author`. `Author` is a
|
|
||||||
standard text field, and can be [casted](/topics/datamodel) as a variable character object in php (`VARCHAR` in SQL). In the
|
|
||||||
following example, our `Author` field is casted as a variable character object with maximum characters of 50. This is
|
|
||||||
especially useful if you know how long your source data needs to be.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class StaffPage extends Page {
|
|
||||||
private static $db = array(
|
|
||||||
'Author' => 'Varchar(50)'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
class StaffPage_Controller extends Page_Controller {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
See [datamodel](/topics/datamodel) for a more detailed explanation on adding database fields, and how the SilverStripe data
|
|
||||||
model works.
|
|
||||||
|
|
||||||
## Adding Form Fields and Tabs
|
|
||||||
|
|
||||||
See [form](/topics/forms) and [tutorial:2-extending-a-basic-site](/tutorials/2-extending-a-basic-site).
|
|
||||||
Note: To modify fields in the "Settings" tab, you need to use `updateSettingsFields()` instead.
|
|
||||||
|
|
||||||
## Removing inherited form fields and tabs
|
|
||||||
|
|
||||||
### removeFieldFromTab()
|
|
||||||
|
|
||||||
Overloading `getCMSFields()` you can call `removeFieldFromTab()` on a `[api:FieldList]` object. For example, if you don't
|
|
||||||
want the MenuTitle field to show on your page, which is inherited from `[api:SiteTree]`.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class StaffPage extends Page {
|
|
||||||
|
|
||||||
public function getCMSFields() {
|
|
||||||
$fields = parent::getCMSFields();
|
|
||||||
$fields->removeFieldFromTab('Root.Content', 'MenuTitle');
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
class StaffPage_Controller extends Page_Controller {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### removeByName()
|
|
||||||
`removeByName()` for normal form fields is useful for breaking inheritance where you know a field in your form isn't
|
|
||||||
required on a certain page-type.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyForm extends Form {
|
|
||||||
|
|
||||||
public function __construct($controller, $name) {
|
|
||||||
// add a default FieldList of form fields
|
|
||||||
$member = singleton('Member');
|
|
||||||
|
|
||||||
$fields = $member->formFields();
|
|
||||||
|
|
||||||
// We don't want the Country field from our default set of fields, so we remove it.
|
|
||||||
$fields->removeByName('Country');
|
|
||||||
|
|
||||||
$actions = new FieldList(
|
|
||||||
new FormAction('submit', 'Submit')
|
|
||||||
);
|
|
||||||
|
|
||||||
parent::__construct($controller, $name, $fields, $actions);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
This will also work if you want to remove a whole tab e.g. $fields->removeByName('Metadata'); will remove the whole
|
|
||||||
Metadata tab.
|
|
||||||
|
|
||||||
For more information on forms, see [form](/topics/forms), [tutorial:2-extending-a-basic-site](/tutorials/2-extending-a-basic-site)
|
|
||||||
and [tutorial:3-forms](/tutorials/3-forms).
|
|
271
docs/en/02_Developer_Guides/00_Model/02_Relations.md
Normal file
271
docs/en/02_Developer_Guides/00_Model/02_Relations.md
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
title: Relations between Records
|
||||||
|
summary: Relate models together using the ORM.
|
||||||
|
|
||||||
|
# Relations between Records
|
||||||
|
|
||||||
|
In most situations you will likely see more than one [api:DataObject] and several classes in your data model may relate
|
||||||
|
to one another. An example of this is a `Player` object may have a relationship to one or more `Team` or `Coach` classes
|
||||||
|
and could take part in many `Games`. Relations are a key part of designing and building a good data model.
|
||||||
|
|
||||||
|
Relations are built through static array definitions on a class, in the format `<relationship-name> => <classname>`.
|
||||||
|
SilverStripe supports a number of relationship types and each relationship type can have any number of relations.
|
||||||
|
|
||||||
|
## has_one
|
||||||
|
|
||||||
|
A 1-to-1 relation creates a database-column called "`<relationship-name>`ID", in the example below this would be
|
||||||
|
"TeamID" on the "Player"-table.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Team extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Title' => 'Varchar'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_many = array(
|
||||||
|
'Players' => 'Player'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
"Team" => "Team",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
This defines a relationship called `Team` which links to a `Team` class. The `ORM` handles navigating the relationship
|
||||||
|
and provides a short syntax for accessing the related object.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$player = Player::get()->byId(1);
|
||||||
|
|
||||||
|
$team = $player->Team();
|
||||||
|
// returns a 'Team' instance.
|
||||||
|
|
||||||
|
echo $player->Team()->Title;
|
||||||
|
// returns the 'Title' column on the 'Team' or `getTitle` if it exists.
|
||||||
|
|
||||||
|
The relationship can also be navigated in [templates](../templates).
|
||||||
|
|
||||||
|
:::ss
|
||||||
|
<% with $Player %>
|
||||||
|
<% if $Team %>
|
||||||
|
Plays for $Team.Title
|
||||||
|
<% end_if %>
|
||||||
|
<% end_with %>
|
||||||
|
|
||||||
|
## has_many
|
||||||
|
|
||||||
|
Defines 1-to-many joins. A database-column named ""`<relationship-name>`ID"" will to be created in the child-class. As
|
||||||
|
you can see from the previous example, `$has_many` goes hand in hand with `$has_one`.
|
||||||
|
|
||||||
|
<div class="alert" markdown='1'>
|
||||||
|
Please specify a $has_one-relationship on the related child-class as well, in order to have the necessary accessors
|
||||||
|
available on both ends.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Team extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Title' => 'Varchar'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_many = array(
|
||||||
|
'Players' => 'Player'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
"Team" => "Team",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Much like the `has_one` relationship, `has_many` can be navigated through the `ORM` as well. The only difference being
|
||||||
|
you will get an instance of [api:HasManyList] rather than the object.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$team = Team::get()->first();
|
||||||
|
|
||||||
|
echo $team->Players();
|
||||||
|
// [HasManyList]
|
||||||
|
|
||||||
|
echo $team->Players()->Count();
|
||||||
|
// returns '14';
|
||||||
|
|
||||||
|
foreach($team->Players() as $player) {
|
||||||
|
echo $player->FirstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
To specify multiple $has_manys to the same object you can use dot notation to distinguish them like below:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Person extends DataObject {
|
||||||
|
|
||||||
|
private static $has_many = array(
|
||||||
|
"Managing" => "Company.Manager",
|
||||||
|
"Cleaning" => "Company.Cleaner",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Company extends DataObject {
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
"Manager" => "Person",
|
||||||
|
"Cleaner" => "Person"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Multiple `$has_one` relationships are okay if they aren't linking to the same object type. Otherwise, they have to be
|
||||||
|
named.
|
||||||
|
|
||||||
|
|
||||||
|
## belongs_to
|
||||||
|
|
||||||
|
Defines a 1-to-1 relationship with another object, which declares the other end of the relationship with a
|
||||||
|
corresponding $has_one. A single database column named `<relationship-name>ID` will be created in the object with the
|
||||||
|
`$has_one`, but the $belongs_to by itself will not create a database field. This field will hold the ID of the object
|
||||||
|
declaring the `$belongs_to`.
|
||||||
|
|
||||||
|
Similarly with $has_many, dot notation can be used to explicitly specify the `$has_one` which refers to this relation.
|
||||||
|
This is not mandatory unless the relationship would be otherwise ambiguous.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Team extends DataObject {
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
'Coach' => 'Coach'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coach extends DataObject {
|
||||||
|
|
||||||
|
private static $belongs_to = array(
|
||||||
|
'Team' => 'Team.Coach'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## many_many
|
||||||
|
|
||||||
|
Defines many-to-many joins. A new table, (this-class)_(relationship-name), will be created with a pair of ID fields.
|
||||||
|
|
||||||
|
<div class="warning" markdown='1'>
|
||||||
|
Please specify a $belongs_many_many-relationship on the related class as well, in order to have the necessary accessors
|
||||||
|
available on both ends.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Team extends DataObject {
|
||||||
|
|
||||||
|
private static $many_many = array(
|
||||||
|
"Supporters" => "Supporter",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Supporter extends DataObject {
|
||||||
|
|
||||||
|
private static $belongs_many_many = array(
|
||||||
|
"Supports" => "Team",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Much like the `has_one` relationship, `mant_many` can be navigated through the `ORM` as well. The only difference being
|
||||||
|
you will get an instance of [api:ManyManyList] rather than the object.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$team = Team::get()->byId(1);
|
||||||
|
|
||||||
|
$supporters = $team->Supporters();
|
||||||
|
// returns a 'ManyManyList' instance.
|
||||||
|
|
||||||
|
|
||||||
|
The relationship can also be navigated in [templates](../templates).
|
||||||
|
|
||||||
|
:::ss
|
||||||
|
<% with $Supporter %>
|
||||||
|
<% loop $Supports %>
|
||||||
|
Supports $Title
|
||||||
|
<% end_if %>
|
||||||
|
<% end_with %>
|
||||||
|
|
||||||
|
## Adding relations
|
||||||
|
|
||||||
|
Adding new items to a relations works the same, regardless if you're editing a **has_many** or a **many_many**. They are
|
||||||
|
encapsulated by [api:HasManyList] and [api:ManyManyList], both of which provide very similar APIs, e.g. an `add()`
|
||||||
|
and `remove()` method.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$team = Team::get()->byId(1);
|
||||||
|
|
||||||
|
// create a new supporter
|
||||||
|
$supporter = new Supporter();
|
||||||
|
$supporter->Name = "Foo";
|
||||||
|
$supporter->write();
|
||||||
|
|
||||||
|
// add the supporter.
|
||||||
|
$team->Supporters()->add($supporter);
|
||||||
|
|
||||||
|
|
||||||
|
## Custom Relations
|
||||||
|
|
||||||
|
You can use the ORM to get a filtered result list without writing any SQL. For example, this snippet gets you the
|
||||||
|
"Players"-relation on a team, but only containing active players.
|
||||||
|
|
||||||
|
See `[api:DataObject::$has_many]` for more info on the described relations.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Team extends DataObject {
|
||||||
|
|
||||||
|
private static $has_many = array(
|
||||||
|
"Players" => "Player"
|
||||||
|
);
|
||||||
|
|
||||||
|
public function ActivePlayers() {
|
||||||
|
return $this->Players()->filter('Status', 'Active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="notice" markdown="1">
|
||||||
|
Adding new records to a filtered `RelationList` like in the example above doesn't automatically set the filtered
|
||||||
|
criteria on the added record.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Relations on Unsaved Objects
|
||||||
|
|
||||||
|
You can also set *has_many* and *many_many* relations before the `DataObject` is saved. This behavior uses the
|
||||||
|
[api:UnsavedRelationList] and converts it into the correct `RelationList` when saving the `DataObject` for the first
|
||||||
|
time.
|
||||||
|
|
||||||
|
This unsaved lists will also recursively save any unsaved objects that they contain.
|
||||||
|
|
||||||
|
As these lists are not backed by the database, most of the filtering methods on `DataList` cannot be used on a list of
|
||||||
|
this type. As such, an `UnsavedRelationList` should only be used for setting a relation before saving an object, not
|
||||||
|
for displaying the objects contained in the relation.
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
* [Introduction to the Data Model and ORM](data_model_and_orm)
|
||||||
|
* [Lists](lists)
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
* [api:HasManyList]
|
||||||
|
* [api:ManyManyList]
|
||||||
|
* [api:DataObject]
|
@ -1,153 +0,0 @@
|
|||||||
# Sitetree
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
Basic data-object representing all pages within the site tree.
|
|
||||||
The omnipresent *Page* class (located in `mysite/code/Page.php`) is based on this class.
|
|
||||||
|
|
||||||
## Creating, Modifying and Finding Pages
|
|
||||||
|
|
||||||
See the ["datamodel" topic](/topics/datamodel).
|
|
||||||
|
|
||||||
## Linking
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// wrong
|
|
||||||
$mylink = $mypage->URLSegment;
|
|
||||||
// right
|
|
||||||
$mylink = $mypage->Link(); // alternatively: AbsoluteLink(), RelativeLink()
|
|
||||||
|
|
||||||
In a nutshell, the nested URLs feature means that your site URLs now reflect the actual parent/child page structure of
|
|
||||||
your site. The URLs map directly to the chain of parent and child pages. The
|
|
||||||
below table shows a quick summary of what these changes mean for your site:
|
|
||||||
|
|
||||||
![url table](http://silverstripe.org/assets/screenshots/Nested-URLs-Table.png)
|
|
||||||
|
|
||||||
## Querying
|
|
||||||
|
|
||||||
Use *SiteTree::get_by_link()* to correctly retrieve a page by URL, as it taked nested URLs into account (a page URL
|
|
||||||
might consist of more than one *URLSegment*).
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// wrong
|
|
||||||
$mypage = SiteTree::get()->filter("URLSegment", '<mylink>')->First();
|
|
||||||
// right
|
|
||||||
$mypage = SiteTree::get_by_link('<mylink>');
|
|
||||||
|
|
||||||
### Versioning
|
|
||||||
|
|
||||||
The `SiteTree` class automatically has an extension applied to it: `[Versioned](api:Versioned)`.
|
|
||||||
This provides the basis for the CMS to operate on different stages,
|
|
||||||
and allow authors to save their changes without publishing them to
|
|
||||||
website visitors straight away.
|
|
||||||
`Versioned` is a generic extension which can be applied to any `DataObject`,
|
|
||||||
so most of its functionality is explained in the `["versioning" topic](/topics/versioning)`.
|
|
||||||
|
|
||||||
Since `SiteTree` makes heavy use of the extension, it adds some additional
|
|
||||||
functionality and helpers on top of it.
|
|
||||||
|
|
||||||
Permission control:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyPage extends Page {
|
|
||||||
public function canPublish($member = null) {
|
|
||||||
// return boolean from custom logic
|
|
||||||
}
|
|
||||||
public function canDeleteFromLive($member = null) {
|
|
||||||
// return boolean from custom logic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Stage operations:
|
|
||||||
|
|
||||||
* `$page->doUnpublish()`: removes the "Live" record, with additional permission checks,
|
|
||||||
as well as special logic for VirtualPage and RedirectorPage associations
|
|
||||||
* `$page->doPublish()`: Inverse of doUnpublish()
|
|
||||||
* `$page->doRevertToLive()`: Reverts current record to live state (makes sense to save to "draft" stage afterwards)
|
|
||||||
* `$page->doRestoreToStage()`: Restore the content in the active copy of this SiteTree page to the stage site.
|
|
||||||
|
|
||||||
|
|
||||||
Hierarchy operations (defined on `[api:Hierarchy]`:
|
|
||||||
|
|
||||||
* `$page->liveChildren()`: Return results only from live table
|
|
||||||
* `$page->stageChildren()`: Return results from the stage table
|
|
||||||
* `$page->AllHistoricalChildren()`: Return all the children this page had, including pages that were deleted from both stage & live.
|
|
||||||
* `$page->AllChildrenIncludingDeleted()`: Return all children, including those that have been deleted but are still in live.
|
|
||||||
|
|
||||||
## Allowed Children, Default Child and Root-Level
|
|
||||||
|
|
||||||
By default, any page type can be the child of any other page type.
|
|
||||||
However, there are static properties that can be
|
|
||||||
used to set up restrictions that will preserve the integrity of the page hierarchy.
|
|
||||||
|
|
||||||
Example: Restrict blog entry pages to nesting underneath their blog holder
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class BlogHolder extends Page {
|
|
||||||
// Blog holders can only contain blog entries
|
|
||||||
private static $allowed_children = array("BlogEntry");
|
|
||||||
private static $default_child = "BlogEntry";
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
class BlogEntry extends Page {
|
|
||||||
// Blog entries can't contain children
|
|
||||||
private static $allowed_children = "none";
|
|
||||||
private static $can_be_root = false;
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
class Page extends SiteTree {
|
|
||||||
// Don't let BlogEntry pages be underneath Pages. Only underneath Blog holders.
|
|
||||||
private static $allowed_children = array("*Page,", "BlogHolder");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
* **allowed_children:** This can be an array of allowed child classes, or the string "none" - indicating that this page
|
|
||||||
type can't have children. If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no
|
|
||||||
subclasses. Otherwise, the class and all its subclasses are allowed.
|
|
||||||
|
|
||||||
* **default_child:** If a page is allowed more than 1 type of child, you can set a default. This is the value that
|
|
||||||
will be automatically selected in the page type dropdown when you create a page in the CMS.
|
|
||||||
|
|
||||||
* **can_be_root:** This is a boolean variable. It lets you specify whether the given page type can be in the top
|
|
||||||
level.
|
|
||||||
|
|
||||||
Note that there is no allowed_parents` control. To set this, you will need to specify the `allowed_children` of all other page types to exclude the page type in question.
|
|
||||||
|
|
||||||
## Tree Limitations
|
|
||||||
|
|
||||||
SilverStripe limits the amount of initially rendered nodes in order to avoid
|
|
||||||
processing delays, usually to a couple of dozen. The value can be configured
|
|
||||||
through `[api:Hierarchy::$node_threshold_total]`.
|
|
||||||
|
|
||||||
If a website has thousands of pages, the tree UI metaphor can become an inefficient way
|
|
||||||
to manage them. The CMS has an alternative "list view" for this purpose, which allows
|
|
||||||
sorting and paging through large numbers of pages in a tabular view.
|
|
||||||
|
|
||||||
To avoid exceeding performance constraints of both the server and browser,
|
|
||||||
SilverStripe places hard limits on the amount of rendered pages in
|
|
||||||
a specific tree leaf, typically a couple of hundred pages.
|
|
||||||
The value can be configured through `[api:Hierarchy::$node_threshold_leaf]`.
|
|
||||||
|
|
||||||
## Tree Display (Description, Icons and Badges)
|
|
||||||
|
|
||||||
The page tree in the CMS is a central element to manage page hierarchies,
|
|
||||||
hence its display of pages can be customized as well.
|
|
||||||
|
|
||||||
On a most basic level, you can specify a custom page icon
|
|
||||||
to make it easier for CMS authors to identify pages of this type,
|
|
||||||
when navigating the tree or adding a new page:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class StaffPage extends Page {
|
|
||||||
private static $singular_name = 'Staff Directory';
|
|
||||||
private static $plural_name = 'Staff Directories';
|
|
||||||
private static $description = 'Two-column layout with a list of staff members';
|
|
||||||
private static $icon = 'mysite/images/staff-icon.png';
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
You can also add custom "badges" to each page in the tree,
|
|
||||||
which denote status. Built-in examples are "Draft" and "Deleted" flags.
|
|
||||||
This is detailed in the ["Customize the CMS Tree" howto](/howto/customize-cms-tree).
|
|
@ -1,302 +0,0 @@
|
|||||||
# DataObject
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
The `[api:DataObject]` class represents a single row in a database table,
|
|
||||||
following the ["Active Record"](http://en.wikipedia.org/wiki/Active_record_pattern) design pattern.
|
|
||||||
|
|
||||||
## Defining Properties
|
|
||||||
|
|
||||||
Properties defined through `DataObject::$db` map to table columns,
|
|
||||||
and can be declared as different [data-types](/topics/data-types).
|
|
||||||
|
|
||||||
## Loading and Saving Records
|
|
||||||
|
|
||||||
The basic principles around data persistence and querying for objects
|
|
||||||
is explained in the ["datamodel" topic](/topics/datamodel).
|
|
||||||
|
|
||||||
## Defining Form Fields
|
|
||||||
|
|
||||||
In addition to defining how data is persisted, the class can also
|
|
||||||
help with editing it by providing form fields through `DataObject->getCMSFields()`.
|
|
||||||
The resulting `[api:FieldList]` is the centrepiece of many data administration interfaces in SilverStripe.
|
|
||||||
Many customizations of the SilverStripe CMS interface start here,
|
|
||||||
by adding, removing or configuring fields.
|
|
||||||
|
|
||||||
Here is an example getCMSFields implementation:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyDataObject extends DataObject {
|
|
||||||
$db = array(
|
|
||||||
'IsActive' => 'Boolean'
|
|
||||||
);
|
|
||||||
public function getCMSFields() {
|
|
||||||
return new FieldList(
|
|
||||||
new CheckboxField('IsActive')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
There's various [form field types](/references/form-field-types), for editing text, dates,
|
|
||||||
restricting input to numbers, and much more.
|
|
||||||
|
|
||||||
## Scaffolding Form Fields
|
|
||||||
|
|
||||||
The ORM already has a lot of information about the data represented by a `DataObject`
|
|
||||||
through its `$db` property, so why not use it to create form fields as well?
|
|
||||||
If you call the parent implementation, the class will use `[api:FormScaffolder]`
|
|
||||||
to provide reasonable defaults based on the property type (e.g. a checkbox field for booleans).
|
|
||||||
You can then further customize those fields as required.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyDataObject extends DataObject {
|
|
||||||
// ...
|
|
||||||
public function getCMSFields() {
|
|
||||||
$fields = parent::getCMSFields();
|
|
||||||
$fields->fieldByName('IsActive')->setTitle('Is active?');
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
The [ModelAdmin](/reference/modeladmin) class uses this approach to provide
|
|
||||||
data management interfaces with very little custom coding.
|
|
||||||
|
|
||||||
You can also alter the fields of built-in and module `DataObject` classes through
|
|
||||||
your own [DataExtension](/reference/dataextension), and a call to `DataExtension->updateCMSFields()`.
|
|
||||||
`[api::DataObject->beforeUpdateCMSFields()]` can also be used to interact with and add to automatically
|
|
||||||
scaffolded fields prior to being passed to extensions (See [DataExtension](/reference/dataextension)).
|
|
||||||
|
|
||||||
### Searchable Fields
|
|
||||||
|
|
||||||
The `$searchable_fields` property uses a mixed array format that can be used to further customize your generated admin
|
|
||||||
system. The default is a set of array values listing the fields.
|
|
||||||
|
|
||||||
Example: Getting predefined searchable fields
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$fields = singleton('MyDataObject')->searchableFields();
|
|
||||||
|
|
||||||
|
|
||||||
Example: Simple Definition
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyDataObject extends DataObject {
|
|
||||||
private static $searchable_fields = array(
|
|
||||||
'Name',
|
|
||||||
'ProductCode'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Searchable fields will be appear in the search interface with a default form field (usually a `[api:TextField]`) and a default
|
|
||||||
search filter assigned (usually an `[api:ExactMatchFilter]`). To override these defaults, you can specify additional information
|
|
||||||
on `$searchable_fields`:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyDataObject extends DataObject {
|
|
||||||
private static $searchable_fields = array(
|
|
||||||
'Name' => 'PartialMatchFilter',
|
|
||||||
'ProductCode' => 'NumericField'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
If you assign a single string value, you can set it to be either a `[api:FormField]` or `[api:SearchFilter]`. To specify both, you can
|
|
||||||
assign an array:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyDataObject extends DataObject {
|
|
||||||
private static $searchable_fields = array(
|
|
||||||
'Name' => array(
|
|
||||||
'field' => 'TextField',
|
|
||||||
'filter' => 'PartialMatchFilter',
|
|
||||||
),
|
|
||||||
'ProductCode' => array(
|
|
||||||
'title' => 'Product code #',
|
|
||||||
'field' => 'NumericField',
|
|
||||||
'filter' => 'PartialMatchFilter',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
To include relations (`$has_one`, `$has_many` and `$many_many`) in your search, you can use a dot-notation.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Team extends DataObject {
|
|
||||||
private static $db = array(
|
|
||||||
'Title' => 'Varchar'
|
|
||||||
);
|
|
||||||
private static $many_many = array(
|
|
||||||
'Players' => 'Player'
|
|
||||||
);
|
|
||||||
private static $searchable_fields = array(
|
|
||||||
'Title',
|
|
||||||
'Players.Name',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
class Player extends DataObject {
|
|
||||||
private static $db = array(
|
|
||||||
'Name' => 'Varchar',
|
|
||||||
'Birthday' => 'Date'
|
|
||||||
);
|
|
||||||
private static $belongs_many_many = array(
|
|
||||||
'Teams' => 'Team'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
### Summary Fields
|
|
||||||
|
|
||||||
Summary fields can be used to show a quick overview of the data for a specific `[api:DataObject]` record. Most common use is
|
|
||||||
their display as table columns, e.g. in the search results of a `[api:ModelAdmin]` CMS interface.
|
|
||||||
|
|
||||||
Example: Getting predefined summary fields
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$fields = singleton('MyDataObject')->summaryFields();
|
|
||||||
|
|
||||||
|
|
||||||
Example: Simple Definition
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyDataObject extends DataObject {
|
|
||||||
private static $db = array(
|
|
||||||
'Name' => 'Text',
|
|
||||||
'OtherProperty' => 'Text',
|
|
||||||
'ProductCode' => 'Int',
|
|
||||||
);
|
|
||||||
private static $summary_fields = array(
|
|
||||||
'Name',
|
|
||||||
'ProductCode'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
To include relations or field manipulations in your summaries, you can use a dot-notation.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class OtherObject extends DataObject {
|
|
||||||
private static $db = array(
|
|
||||||
'Title' => 'Varchar'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
class MyDataObject extends DataObject {
|
|
||||||
private static $db = array(
|
|
||||||
'Name' => 'Text',
|
|
||||||
'Description' => 'HTMLText'
|
|
||||||
);
|
|
||||||
private static $has_one = array(
|
|
||||||
'OtherObject' => 'OtherObject'
|
|
||||||
);
|
|
||||||
private static $summary_fields = array(
|
|
||||||
'Name' => 'Name',
|
|
||||||
'Description.Summary' => 'Description (summary)',
|
|
||||||
'OtherObject.Title' => 'Other Object Title'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Non-textual elements (such as images and their manipulations) can also be used in summaries.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyDataObject extends DataObject {
|
|
||||||
private static $db = array(
|
|
||||||
'Name' => 'Text'
|
|
||||||
);
|
|
||||||
private static $has_one = array(
|
|
||||||
'HeroImage' => 'Image'
|
|
||||||
);
|
|
||||||
private static $summary_fields = array(
|
|
||||||
'Name' => 'Name',
|
|
||||||
'HeroImage.CMSThumbnail' => 'Hero Image'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
## Permissions
|
|
||||||
|
|
||||||
Models can be modified in a variety of controllers and user interfaces,
|
|
||||||
all of which can implement their own security checks. But often it makes
|
|
||||||
sense to centralize those checks on the model, regardless of the used controller.
|
|
||||||
|
|
||||||
The API provides four methods for this purpose:
|
|
||||||
`canEdit()`, `canCreate()`, `canView()` and `canDelete()`.
|
|
||||||
Since they're PHP methods, they can contain arbitrary logic
|
|
||||||
matching your own requirements. They can optionally receive a `$member` argument,
|
|
||||||
and default to the currently logged in member (through `Member::currentUser()`).
|
|
||||||
|
|
||||||
Example: Check for CMS access permissions
|
|
||||||
|
|
||||||
class MyDataObject extends DataObject {
|
|
||||||
// ...
|
|
||||||
public function canView($member = null) {
|
|
||||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
|
||||||
}
|
|
||||||
public function canEdit($member = null) {
|
|
||||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
|
||||||
}
|
|
||||||
public function canDelete($member = null) {
|
|
||||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
|
||||||
}
|
|
||||||
public function canCreate($member = null) {
|
|
||||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
**Important**: These checks are not enforced on low-level ORM operations
|
|
||||||
such as `write()` or `delete()`, but rather rely on being checked in the invoking code.
|
|
||||||
The CMS default sections as well as custom interfaces like
|
|
||||||
[ModelAdmin](/reference/modeladmin) or [GridField](/reference/grid-field)
|
|
||||||
already enforce these permissions.
|
|
||||||
|
|
||||||
## Indexes
|
|
||||||
|
|
||||||
It is sometimes desirable to add indexes to your data model, whether to
|
|
||||||
optimize queries or add a uniqueness constraint to a field. This is done
|
|
||||||
through the `DataObject::$indexes` map, which maps index names to descriptor
|
|
||||||
arrays that represent each index. There's several supported notations:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
# Simple
|
|
||||||
private static $indexes = array(
|
|
||||||
'<column-name>' => true
|
|
||||||
);
|
|
||||||
|
|
||||||
# Advanced
|
|
||||||
private static $indexes = array(
|
|
||||||
'<index-name>' => array('type' => '<type>', 'value' => '"<column-name>"')
|
|
||||||
);
|
|
||||||
|
|
||||||
# SQL
|
|
||||||
private static $indexes = array(
|
|
||||||
'<index-name>' => 'unique("<column-name>")'
|
|
||||||
);
|
|
||||||
|
|
||||||
The `<index-name>` can be an an arbitrary identifier in order to allow for more than one
|
|
||||||
index on a specific database column.
|
|
||||||
The "advanced" notation supports more `<type>` notations.
|
|
||||||
These vary between database drivers, but all of them support the following:
|
|
||||||
|
|
||||||
* `index`: Standard index
|
|
||||||
* `unique`: Index plus uniqueness constraint on the value
|
|
||||||
* `fulltext`: Fulltext content index
|
|
||||||
|
|
||||||
In order to use more database specific or complex index notations,
|
|
||||||
we also support raw SQL for as a value in the `$indexes` definition.
|
|
||||||
Keep in mind this will likely make your code less portable between databases.
|
|
||||||
|
|
||||||
Example: A combined index on a two fields.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
private static $db = array(
|
|
||||||
'MyField' => 'Varchar',
|
|
||||||
'MyOtherField' => 'Varchar',
|
|
||||||
);
|
|
||||||
private static $indexes = array(
|
|
||||||
'MyIndexName' => array('type' => 'index', 'value' => '"MyField","MyOtherField"'),
|
|
||||||
);
|
|
||||||
|
|
||||||
## API Documentation
|
|
||||||
|
|
||||||
`[api:DataObject]`
|
|
94
docs/en/02_Developer_Guides/00_Model/03_Lists.md
Normal file
94
docs/en/02_Developer_Guides/00_Model/03_Lists.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
title: Managing Lists
|
||||||
|
summary: Learn how to manipulate SS_List objects.
|
||||||
|
|
||||||
|
# Managing Lists
|
||||||
|
|
||||||
|
Whenever using the ORM to fetch records or navigate relationships you will receive an [api:SS_List] instance commonly as
|
||||||
|
either [api:DataList] or [api:RelationList]. This object gives you the ability to iterate over each of the results or
|
||||||
|
modify.
|
||||||
|
|
||||||
|
## Iterating over the list.
|
||||||
|
|
||||||
|
[api:SS_List] implements `IteratorAggregate` allowing you to loop over the instance.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$members = Member::get();
|
||||||
|
|
||||||
|
foreach($members as $member) {
|
||||||
|
echo $member->Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
Or in the template engine:
|
||||||
|
|
||||||
|
:::ss
|
||||||
|
<% loop $Members %>
|
||||||
|
<!-- -->
|
||||||
|
<% end_loop %>
|
||||||
|
|
||||||
|
## Finding an item by value.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
// $list->find($key, $value);
|
||||||
|
|
||||||
|
//
|
||||||
|
$members = Member::get();
|
||||||
|
|
||||||
|
echo $members->find('ID', 4)->FirstName;
|
||||||
|
// returns 'Sam'
|
||||||
|
|
||||||
|
|
||||||
|
## Maps
|
||||||
|
|
||||||
|
A map is an array where the array indexes contain data as well as the values. You can build a map from any list
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$members = Member::get()->map('ID', 'FirstName');
|
||||||
|
|
||||||
|
// $members = array(
|
||||||
|
// 1 => 'Sam'
|
||||||
|
// 2 => 'Sig'
|
||||||
|
// 3 => 'Will'
|
||||||
|
// );
|
||||||
|
|
||||||
|
This functionality is provided by the [api:SS_Map] class, which can be used to build a map around any `SS_List`.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$members = Member::get();
|
||||||
|
$map = new SS_Map($members, 'ID', 'FirstName');
|
||||||
|
|
||||||
|
## Column
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$members = Member::get();
|
||||||
|
|
||||||
|
echo $members->column('Email');
|
||||||
|
|
||||||
|
// returns array(
|
||||||
|
// 'sam@silverstripe.com',
|
||||||
|
// 'sig@silverstripe.com',
|
||||||
|
// 'will@silverstripe.com'
|
||||||
|
// );
|
||||||
|
|
||||||
|
## ArrayList
|
||||||
|
|
||||||
|
[api:ArrayList] exists to wrap a standard PHP array in the same API as a database backed list.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$sam = Member::get()->byId(5);
|
||||||
|
$sig = Member::get()->byId(6);
|
||||||
|
|
||||||
|
$list = new ArrayList();
|
||||||
|
$list->push($sam);
|
||||||
|
$list->push($sig);
|
||||||
|
|
||||||
|
echo $list->Count();
|
||||||
|
// returns '2'
|
||||||
|
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
* [api:SS_List]
|
||||||
|
* [api:RelationList]
|
||||||
|
* [api:DataList]
|
||||||
|
* [api:ArrayList]
|
||||||
|
* [api:SS_Map]
|
@ -1,99 +1,182 @@
|
|||||||
|
title: Data Types, Overloading and Casting
|
||||||
|
summary: Documentation on how data is stored going in, coming out of the ORM and how to modify it.
|
||||||
|
|
||||||
# Data Types and Casting
|
# Data Types and Casting
|
||||||
|
|
||||||
Properties on any SilverStripe object can be type casted automatically,
|
Each model in a SilverStripe [api:DataObject] will handle data at some point. This includes database columns such as
|
||||||
by transforming its scalar value into an instance of the `[api:DBField]` class,
|
the ones defined in a `$db` array or simply a method that returns data for the template.
|
||||||
providing additional helpers. For example, a string can be cast as
|
|
||||||
a `[api:Text]` type, which has a `FirstSentence()` method to retrieve the first
|
A Data Type is represented in SilverStripe by a [api:DBField] subclass. The class is responsible for telling the ORM
|
||||||
sentence in a longer piece of text.
|
about how to store it's data in the database and how to format the information coming out of the database.
|
||||||
|
|
||||||
|
In the `Player` example, we have four database columns each with a different data type (Int, Varchar).
|
||||||
|
|
||||||
|
**mysite/code/Player.php**
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'PlayerNumber' => 'Int',
|
||||||
|
'FirstName' => 'Varchar(255)',
|
||||||
|
'LastName' => 'Text',
|
||||||
|
'Birthday' => 'Date'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
## Available Types
|
## Available Types
|
||||||
|
|
||||||
* `[api:Boolean]`: A boolean field.
|
* [api:Boolean]: A boolean field.
|
||||||
* `[api:Currency]`: A number with 2 decimal points of precision, designed to store currency values.
|
* [api:Currency]: A number with 2 decimal points of precision, designed to store currency values.
|
||||||
* `[api:Date]`: A date field
|
* [api:Date]: A date field
|
||||||
* `[api:Decimal]`: A decimal number.
|
* [api:Decimal]: A decimal number.
|
||||||
* `[api:Enum]`: An enumeration of a set of strings
|
* [api:Enum]: An enumeration of a set of strings
|
||||||
* `[api:HTMLText]`: A variable-length string of up to 2MB, designed to store HTML
|
* [api:HTMLText]: A variable-length string of up to 2MB, designed to store HTML
|
||||||
* `[api:HTMLVarchar]`: A variable-length string of up to 255 characters, designed to store HTML
|
* [api:HTMLVarchar]: A variable-length string of up to 255 characters, designed to store HTML
|
||||||
* `[api:Int]`: An integer field.
|
* [api:Int]: An integer field.
|
||||||
* `[api:Percentage]`: A decimal number between 0 and 1 that represents a percentage.
|
* [api:Percentage]: A decimal number between 0 and 1 that represents a percentage.
|
||||||
* `[api:SS_Datetime]`: A date / time field
|
* [api:SS_Datetime]: A date / time field
|
||||||
* `[api:Text]`: A variable-length string of up to 2MB, designed to store raw text
|
* [api:Text]: A variable-length string of up to 2MB, designed to store raw text
|
||||||
* `[api:Time]`: A time field
|
* [api:Time]: A time field
|
||||||
* `[api:Varchar]`: A variable-length string of up to 255 characters, designed to store raw text
|
* [api:Varchar]: A variable-length string of up to 255 characters, designed to store raw text.
|
||||||
|
|
||||||
## Casting arbitrary values
|
You can define your own [api:DBField] instances if required as well. See the API documentation for a list of all the
|
||||||
|
available subclasses.
|
||||||
|
|
||||||
On the most basic level, the class can be used as simple conversion class
|
## Formatting Output
|
||||||
from one value to another, e.g. to round a number.
|
|
||||||
|
The Data Type does more than setup the correct database schema. They can also define methods and formatting helpers for
|
||||||
|
output. You can manually create instances of a Data Type and pass it through to the template.
|
||||||
|
|
||||||
|
If this case, we'll create a new method for our `Player` that returns the full name. By wrapping this in a [api:Varchar]
|
||||||
|
object we can control the formatting and it allows us to call methods defined from `Varchar` as `LimitCharacters`.
|
||||||
|
|
||||||
|
**mysite/code/Player.php**
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
..
|
||||||
|
|
||||||
|
public function getName() {
|
||||||
|
return DBField::create_field('Varchar', $this->FirstName . ' '. $this->LastName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Then we can refer to a new `Name` column on our `Player` instances. In templates we don't need to use the `get` prefix.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$player = Player::get()->byId(1);
|
||||||
|
|
||||||
|
echo $player->Name;
|
||||||
|
// returns "Sam Minnée"
|
||||||
|
|
||||||
|
echo $player->getName();
|
||||||
|
// returns "Sam Minnée";
|
||||||
|
|
||||||
|
echo $player->getName()->LimitCharacters(2);
|
||||||
|
// returns "Sa.."
|
||||||
|
|
||||||
|
### Casting
|
||||||
|
|
||||||
|
Rather than manually returning objects from your custom functions. You can use the `$casting` property.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
private static $casting = array(
|
||||||
|
"Name" => 'Varchar',
|
||||||
|
);
|
||||||
|
|
||||||
|
public function getName() {
|
||||||
|
return $this->FirstName . ' '. $this->LastName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
The properties on any SilverStripe object can be type casted automatically, by transforming its scalar value into an
|
||||||
|
instance of the [api:DBField] class, providing additional helpers. For example, a string can be cast as a [api:Text]
|
||||||
|
type, which has a `FirstSentence()` method to retrieve the first sentence in a longer piece of text.
|
||||||
|
|
||||||
|
On the most basic level, the class can be used as simple conversion class from one value to another, e.g. to round a
|
||||||
|
number.
|
||||||
|
|
||||||
:::php
|
:::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.
|
Of course that's much more verbose than the equivalent PHP call. The power of [api:DBField] comes with its more
|
||||||
The power of `[api:DBField]` comes with its more sophisticated helpers,
|
sophisticated helpers, like showing the time difference to the current date:
|
||||||
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 `[api:ViewableData]`,
|
Most objects in SilverStripe extend from [api:ViewableData], which means they know how to present themselves in a view
|
||||||
which means they know how to present themselves in a view context.
|
context. Through a `$casting` array, arbitrary properties and getters can be casted:
|
||||||
Through a `$casting` array, arbitrary properties and getters can be casted:
|
|
||||||
|
|
||||||
:::php
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
class MyObject extends ViewableData {
|
class MyObject extends ViewableData {
|
||||||
static $casting = array(
|
|
||||||
|
private static $casting = array(
|
||||||
'MyDate' => 'Date'
|
'MyDate' => 'Date'
|
||||||
);
|
);
|
||||||
|
|
||||||
public function getMyDate() {
|
public function getMyDate() {
|
||||||
return '1982-01-01';
|
return '1982-01-01';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$obj = new MyObject;
|
$obj = new MyObject;
|
||||||
$obj->getMyDate(); // returns string
|
$obj->getMyDate(); // returns string
|
||||||
$obj->MyDate; // returns string
|
$obj->MyDate; // returns string
|
||||||
$obj->obj('MyDate'); // returns object
|
$obj->obj('MyDate'); // returns object
|
||||||
$obj->obj('MyDate')->InPast(); // returns boolean
|
$obj->obj('MyDate')->InPast(); // returns boolean
|
||||||
|
|
||||||
## Casting DataObject
|
|
||||||
|
|
||||||
The `[api:DataObject]` class uses `DBField` to describe the types of its
|
|
||||||
properties which are persisted in database columns, through the `[$db](api:DataObject::$db)` property.
|
|
||||||
In addition to type information, the `DBField` class also knows how to
|
|
||||||
define itself as a database column. See the ["datamodel" topic](/topics/datamodel#casting) for more details.
|
|
||||||
|
|
||||||
<div class="warning" markdown="1">
|
|
||||||
Since we're dealing with a loosely typed language (PHP)
|
|
||||||
as well as varying type support by the different database drivers,
|
|
||||||
type conversions between the two systems are not guaranteed to be lossless.
|
|
||||||
Please take particular care when casting booleans, null values, and on float precisions.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Casting in templates
|
|
||||||
|
|
||||||
In templates, casting helpers are available without the need for an `obj()` call.
|
|
||||||
|
|
||||||
Example: Flagging an object of type `MyObject` (see above) if it's date is in the past.
|
|
||||||
|
|
||||||
:::ss
|
|
||||||
<% if $MyObjectInstance.MyDate.InPast %>Outdated!<% end_if %>
|
|
||||||
|
|
||||||
## Casting HTML Text
|
## Casting HTML Text
|
||||||
|
|
||||||
The database field types `[api:HTMLVarchar]`/`[api:HTMLText]` and `[api:Varchar]`/`[api:Text]`
|
The database field types [api:HTMLVarchar]/[api:HTMLText] and [api:Varchar]/[api:Text] are exactly the same in
|
||||||
are exactly the same in the database. However, the templating engine knows to escape
|
the database. However, the template engine knows to escape fields without the `HTML` prefix automatically in templates,
|
||||||
fields without the `HTML` prefix automatically in templates,
|
to prevent them from rendering HTML interpreted by browsers. This escaping prevents attacks like CSRF or XSS (see
|
||||||
to prevent them from rendering HTML interpreted by browsers.
|
"[security](../security)"), which is important if these fields store user-provided data.
|
||||||
This escaping prevents attacks like CSRF or XSS (see "[security](/topics/security)"),
|
|
||||||
which is important if these fields store user-provided data.
|
|
||||||
|
|
||||||
You can disable this auto-escaping by using the `$MyField.RAW` escaping hints,
|
<div class="hint" markdown="1">
|
||||||
or explicitly request escaping of HTML content via `$MyHtmlField.XML`.
|
You can disable this auto-escaping by using the `$MyField.RAW` escaping hints, or explicitly request escaping of HTML
|
||||||
|
content via `$MyHtmlField.XML`.
|
||||||
|
</div>
|
||||||
|
|
||||||
## Related
|
## Overloading
|
||||||
|
|
||||||
* ["datamodel" topic](/topics/datamodel)
|
"Getters" and "Setters" are functions that help us save fields to our [api:DataObject] instances. By default, the
|
||||||
* ["security" topic](/topics/security)
|
methods `getField()` and `setField()` are used to set column data. They save to the protected array, `$obj->record`.
|
||||||
|
We can overload the default behavior by making a function called "get`<fieldname>`" or "set`<fieldname>`".
|
||||||
|
|
||||||
|
The following example will use the result of `getStatus` instead of the 'Status' database column. We can refer to the
|
||||||
|
database column using `dbObject`.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
"Status" => "Enum(array('Active', 'Injured', 'Retired'))"
|
||||||
|
);
|
||||||
|
|
||||||
|
public function getStatus() {
|
||||||
|
return (!$this->obj("Birthday")->InPast()) ? "Unborn" : $this->dbObject('Status')->Value();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
* [api:DataObject]
|
||||||
|
* [api:DBField]
|
@ -1,1117 +0,0 @@
|
|||||||
# Datamodel
|
|
||||||
|
|
||||||
SilverStripe uses an [object-relational model](http://en.wikipedia.org/wiki/Object-relational_model)
|
|
||||||
that assumes the following connections:
|
|
||||||
|
|
||||||
* Each database-table maps to a PHP class
|
|
||||||
* Each database-row maps to a PHP object
|
|
||||||
* Each database-column maps to a property on a PHP object
|
|
||||||
|
|
||||||
All data tables in SilverStripe are defined as subclasses of `[api:DataObject]`.
|
|
||||||
|
|
||||||
Inheritance is supported in the data model: separate tables will be linked
|
|
||||||
together, the data spread across these tables. The mapping and saving/loading
|
|
||||||
logic is handled by SilverStripe, you don't need to worry about writing SQL most
|
|
||||||
of the time.
|
|
||||||
|
|
||||||
Most of the ORM customizations are possible through [PHP5 Object
|
|
||||||
Overloading](http://www.onlamp.com/pub/a/php/2005/06/16/overloading.html)
|
|
||||||
handled in the `[api:Object]`-class.
|
|
||||||
|
|
||||||
See [database-structure](/reference/database-structure) for in-depth information
|
|
||||||
on the database-schema and the ["sql queries" topic](/reference/sqlquery) in
|
|
||||||
case you need to drop down to the bare metal.
|
|
||||||
|
|
||||||
## Generating the Database Schema
|
|
||||||
|
|
||||||
The SilverStripe database-schema is generated automatically by visiting the URL.
|
|
||||||
`http://localhost/dev/build`
|
|
||||||
|
|
||||||
<div class="notice" markdown='1'>
|
|
||||||
Note: You need to be logged in as an administrator to perform this command,
|
|
||||||
unless your site is in "[dev mode](/topics/debugging)", or the command is run
|
|
||||||
through CLI.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Querying Data
|
|
||||||
|
|
||||||
Every query to data starts with a `DataList::create(<class>)` or `<class>::get()`
|
|
||||||
call. For example, this query would return all of the `Member` objects:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get();
|
|
||||||
|
|
||||||
The ORM uses a "fluent" syntax, where you specify a query by chaining together
|
|
||||||
different methods. Two common methods are `filter()` and `sort()`:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->filter(array(
|
|
||||||
'FirstName' => 'Sam'
|
|
||||||
))->sort('Surname');
|
|
||||||
|
|
||||||
Those of you who know a bit about SQL might be thinking "it looks like you're
|
|
||||||
querying all members, and then filtering to those with a first name of 'Sam'.
|
|
||||||
|
|
||||||
Isn't this very slow?" Is isn't, because the ORM doesn't actually execute the
|
|
||||||
SQL query until you iterate on the result with a `foreach()` or `<% loop %>`.
|
|
||||||
The ORM is 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 for the previous query.
|
|
||||||
|
|
||||||
:::
|
|
||||||
SELECT * FROM Member WHERE FirstName = 'Sam' ORDER BY Surname
|
|
||||||
|
|
||||||
An example of the query process in action:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// The SQL query isn't executed here...
|
|
||||||
$members = Member::get();
|
|
||||||
// ...or here
|
|
||||||
$members = $members->filter(array('FirstName' => 'Sam'));
|
|
||||||
// ...or even here
|
|
||||||
$members = $members->sort('Surname');
|
|
||||||
|
|
||||||
// *This* is where the query is executed
|
|
||||||
foreach($members as $member) {
|
|
||||||
echo "<p>$member->FirstName $member->Surname</p>";
|
|
||||||
}
|
|
||||||
|
|
||||||
This also means that getting the count of a list of objects will be done with a
|
|
||||||
single, efficient query.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->filter(array(
|
|
||||||
'FirstName' => 'Sam'
|
|
||||||
))->sort('Surname');
|
|
||||||
|
|
||||||
// This will create an single SELECT COUNT query similar to -
|
|
||||||
// SELECT COUNT(*) FROM Members WHERE FirstName = 'Sam'
|
|
||||||
echo $members->Count();
|
|
||||||
|
|
||||||
|
|
||||||
### Returning a single DataObject
|
|
||||||
|
|
||||||
There are a couple of ways of getting a single DataObject from the ORM. If you
|
|
||||||
know the ID number of the object, you can use `byID($id)`:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$member = Member::get()->byID(5);
|
|
||||||
|
|
||||||
If you have constructed a query that you know should return a single record, you
|
|
||||||
can call `First()`:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$member = Member::get()->filter(array(
|
|
||||||
'FirstName' => 'Sam', 'Surname' => 'Minnee'
|
|
||||||
))->First();
|
|
||||||
|
|
||||||
|
|
||||||
### Sort
|
|
||||||
|
|
||||||
Quite often you would like to sort a list. Doing this on a list could be done in
|
|
||||||
a few ways.
|
|
||||||
|
|
||||||
If would like to sort the list by `FirstName` in a ascending way (from A to Z).
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->sort('FirstName', 'ASC'); // ASC or DESC
|
|
||||||
$members = Member::get()->sort('FirstName'); // Ascending is implied
|
|
||||||
|
|
||||||
To reverse the sort
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->sort('FirstName', 'DESC');
|
|
||||||
|
|
||||||
// or..
|
|
||||||
$members = Member::get()->sort('FirstName', 'ASC')->reverse();
|
|
||||||
|
|
||||||
However you might have several entries with the same `FirstName` and would like
|
|
||||||
to sort them by `FirstName` and `LastName`
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$member = Member::get()->sort(array(
|
|
||||||
'FirstName' => 'ASC',
|
|
||||||
'LastName'=>'ASC'
|
|
||||||
));
|
|
||||||
|
|
||||||
You can also sort randomly
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$member = Member::get()->sort('RAND()')
|
|
||||||
|
|
||||||
### Filter
|
|
||||||
|
|
||||||
As you might expect, the `filter()` method filters the list of objects that gets
|
|
||||||
returned. The previous example included this filter, which returns all Members
|
|
||||||
with a first name of "Sam".
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->filter(array('FirstName' => 'Sam'));
|
|
||||||
|
|
||||||
In SilverStripe 2, we would have passed `"\"FirstName\" = 'Sam'` to make this
|
|
||||||
query. Now, we pass an array, `array('FirstName' => 'Sam')`, to minimize the
|
|
||||||
risk of SQL injection bugs. The format of this array follows a few rules:
|
|
||||||
|
|
||||||
* Each element of the array specifies a filter. You can specify as many
|
|
||||||
filters as you like, and they **all** must be true.
|
|
||||||
* The key in the filter corresponds to the field that you want to filter by.
|
|
||||||
* The value in the filter corresponds to the value that you want to filter to.
|
|
||||||
|
|
||||||
So, this would return only those members called "Sam Minnée".
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->filter(array(
|
|
||||||
'FirstName' => 'Sam',
|
|
||||||
'Surname' => 'Minnée',
|
|
||||||
));
|
|
||||||
|
|
||||||
There is also a short hand way of getting Members with the FirstName of Sam.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->filter('FirstName', 'Sam');
|
|
||||||
|
|
||||||
Or if you want to find both Sam and Sig.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->filter(
|
|
||||||
'FirstName', array('Sam', 'Sig')
|
|
||||||
);
|
|
||||||
|
|
||||||
Then there is the most complex task when you want to find Sam and Sig that has
|
|
||||||
either Age 17 or 74.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->filter(array(
|
|
||||||
'FirstName' => array('Sam', 'Sig'),
|
|
||||||
'Age' => array(17, 74)
|
|
||||||
));
|
|
||||||
|
|
||||||
// SQL: WHERE ("FirstName" IN ('Sam', 'Sig) AND "Age" IN ('17', '74))
|
|
||||||
|
|
||||||
In case you want to match multiple criteria non-exclusively (with an "OR"
|
|
||||||
disjunctive),use the `filterAny()` method instead:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->filterAny(array(
|
|
||||||
'FirstName' => 'Sam',
|
|
||||||
'Age' => 17,
|
|
||||||
));
|
|
||||||
// SQL: WHERE ("FirstName" = 'Sam' OR "Age" = '17')
|
|
||||||
|
|
||||||
You can also combine both conjunctive ("AND") and disjunctive ("OR") statements.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()
|
|
||||||
->filter(array(
|
|
||||||
'LastName' => 'Minnée'
|
|
||||||
))
|
|
||||||
->filterAny(array(
|
|
||||||
'FirstName' => 'Sam',
|
|
||||||
'Age' => 17,
|
|
||||||
));
|
|
||||||
// WHERE ("LastName" = 'Minnée' AND ("FirstName" = 'Sam' OR "Age" = '17'))
|
|
||||||
|
|
||||||
### Filter with PHP / filterByCallback
|
|
||||||
|
|
||||||
It is also possible to filter by a PHP callback, however this will force the
|
|
||||||
data model to fetch all records and loop them in PHP, thus `filter()` or `filterAny()`
|
|
||||||
are to be preferred over `filterByCallback()`.
|
|
||||||
Please note that because `filterByCallback()` has to run in PHP, it will always return
|
|
||||||
an `ArrayList` (even if called on a `DataList`, this however might change in future).
|
|
||||||
The first parameter to the callback is the item, the second parameter is the list itself.
|
|
||||||
The callback will run once for each record, if the callback returns true, this record
|
|
||||||
will be added to the list of returned items.
|
|
||||||
The below example will get all Members that have an expired or not encrypted password.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$membersWithBadPassword = Member::get()->filterByCallback(function($item, $list) {
|
|
||||||
if ($item->isPasswordExpired() || $item->PasswordEncryption = 'none') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
### Exclude
|
|
||||||
|
|
||||||
The `exclude()` method is the opposite to the filter in that it removes entries
|
|
||||||
from a list.
|
|
||||||
|
|
||||||
If we would like to remove all members from the list with the FirstName of Sam.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->exclude('FirstName', 'Sam');
|
|
||||||
|
|
||||||
Remove both Sam and Sig is as easy as.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->exclude('FirstName', array('Sam','Sig'));
|
|
||||||
|
|
||||||
As you can see it follows the same pattern as filter, so for removing only Sam
|
|
||||||
Minnée from the list:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->exclude(array(
|
|
||||||
'FirstName' => 'Sam',
|
|
||||||
'Surname' => 'Minnée',
|
|
||||||
));
|
|
||||||
|
|
||||||
And removing Sig and Sam with that are either age 17 or 74.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->exclude(array(
|
|
||||||
'FirstName' => array('Sam', 'Sig'),
|
|
||||||
'Age' => array(17, 43)
|
|
||||||
));
|
|
||||||
|
|
||||||
This would be equivalent to a SQL query of
|
|
||||||
|
|
||||||
:::
|
|
||||||
... WHERE ("FirstName" NOT IN ('Sam','Sig) OR "Age" NOT IN ('17', '74));
|
|
||||||
|
|
||||||
### Search Filter Modifiers
|
|
||||||
|
|
||||||
The where clauses showcased in the previous two sections (filter and exclude)
|
|
||||||
specify exact matches by default. However, there are a number of suffixes that
|
|
||||||
you can put on field names to change this behavior such as `":StartsWith"`,
|
|
||||||
`":EndsWith"`, `":PartialMatch"`, `":GreaterThan"`, `":GreaterThanOrEqual"`, `":LessThan"`, `":LessThanOrEqual"`.
|
|
||||||
|
|
||||||
Each of these suffixes is represented in the ORM as a subclass of
|
|
||||||
`[api:SearchFilter]`. Developers can define their own SearchFilters if needing
|
|
||||||
to extend the ORM filter and exclude behaviors.
|
|
||||||
|
|
||||||
These suffixes can also take modifiers themselves. The modifiers currently
|
|
||||||
supported are `":not"`, `":nocase"` and `":case"`. These negate the filter,
|
|
||||||
make it case-insensitive and make it case-sensitive respectively. The default
|
|
||||||
comparison uses the database's default. For MySQL and MSSQL, this is
|
|
||||||
case-insensitive. For PostgreSQL, this is case-sensitive.
|
|
||||||
|
|
||||||
The following is a query which will return everyone whose first name doesn't
|
|
||||||
start with S, who has logged in since 1/1/2011.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->filter(array(
|
|
||||||
'FirstName:StartsWith:Not' => 'S'
|
|
||||||
'LastVisited:GreaterThan' => '2011-01-01'
|
|
||||||
));
|
|
||||||
|
|
||||||
### Subtract
|
|
||||||
|
|
||||||
You can subtract entries from a DataList by passing in another DataList to
|
|
||||||
`subtract()`
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$allSams = Member::get()->filter('FirstName', 'Sam');
|
|
||||||
$allMembers = Member::get();
|
|
||||||
$noSams = $allMembers->subtract($allSams);
|
|
||||||
|
|
||||||
Though for the above example it would probably be easier to use `filter()` and
|
|
||||||
`exclude()`. A better use case could be when you want to find all the members
|
|
||||||
that does not exist in a Group.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// ... Finding all members that does not belong to $group.
|
|
||||||
$otherMembers = Member::get()->subtract($group->Members());
|
|
||||||
|
|
||||||
### Limit
|
|
||||||
|
|
||||||
You can limit the amount of records returned in a DataList by using the
|
|
||||||
`limit()` method.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// Returning the first 5 members, sorted alphabetically by Surname
|
|
||||||
$members = Member::get()->sort('Surname')->limit(5);
|
|
||||||
|
|
||||||
`limit()` accepts two arguments, the first being the amount of results you want
|
|
||||||
returned, with an optional second parameter to specify the offset, which allows
|
|
||||||
you to tell the system where to start getting the results from. The offset, if
|
|
||||||
not provided as an argument, will default to 0.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// Return 10 members with an offset of 4 (starting from the 5th result).
|
|
||||||
// Note that the argument order is different from a MySQL LIMIT clause
|
|
||||||
$members = Member::get()->sort('Surname')->limit(10, 4);
|
|
||||||
|
|
||||||
### Raw SQL options for advanced users
|
|
||||||
|
|
||||||
Occasionally, the system described above won't let you do exactly what you need
|
|
||||||
to do. In these situations, we have methods that manipulate the SQL query at a
|
|
||||||
lower level. When using these, please ensure that all table & field names are
|
|
||||||
escaped with double quotes, otherwise some DB back-ends (e.g. PostgreSQL) won't
|
|
||||||
work.
|
|
||||||
|
|
||||||
Under the hood, query generation is handled by the `[api:DataQuery]` class. This
|
|
||||||
class does provide more direct access to certain SQL features that `DataList`
|
|
||||||
abstracts away from you.
|
|
||||||
|
|
||||||
In general, we advise against using these methods unless it's absolutely
|
|
||||||
necessary. If the ORM doesn't do quite what you need it to, you may also
|
|
||||||
consider extending the ORM with new data types or filter modifiers (that
|
|
||||||
documentation still needs to be written)
|
|
||||||
|
|
||||||
<div class="notice" markdown="1">
|
|
||||||
See [the security topic](/topics/security#parameterised-queries) for details on safe database querying and why parameterised queries
|
|
||||||
are so necessary here.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
#### SQL WHERE Predicates with Parameters
|
|
||||||
|
|
||||||
If using `DataObject::get()` (which returns a `DataList` instance) you can specify a WHERE clause fragment
|
|
||||||
(that will be combined with other filters using AND) with the `where()` method, or `whereAny()` to add a list
|
|
||||||
of clauses combined with OR.
|
|
||||||
|
|
||||||
Placeholders within a predicate are denoted by the question mark symbol, and should not be quoted.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->where(array('"FirstName" = ?' => 'Sam'));
|
|
||||||
|
|
||||||
If using `SQLSelect` you should use `addWhere`, `setWhere`, `addWhereAny`, or `setWhereAny` to modify the query.
|
|
||||||
|
|
||||||
Using the parameterised query syntax you can either provide a single variable as a parameter, an array of parameters
|
|
||||||
if the SQL has multiple value placeholders, or simply pass an indexed array of strings for literal SQL.
|
|
||||||
|
|
||||||
Although parameters can be escaped and directly inserted into the SQL condition (See `Convert::raw2sql()'),
|
|
||||||
the parameterised syntax is the preferred method of declaring conditions on a query.
|
|
||||||
|
|
||||||
Column names must still be double quoted, and for consistency and compatibility with other code, should also
|
|
||||||
be prefixed with the table name.
|
|
||||||
|
|
||||||
E.g.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$query = Table::get();
|
|
||||||
|
|
||||||
// multiple predicates with parameters
|
|
||||||
$query = $query->where(array(
|
|
||||||
'"Table"."Column" = ?' => $column,
|
|
||||||
'"Table"."Name" = ?' => $value
|
|
||||||
));
|
|
||||||
|
|
||||||
// Shorthand for simple column comparison (as above), omitting the '?'
|
|
||||||
// These will each be expanded internally to '"Table"."Column" = ?'
|
|
||||||
$query = $query->where(array(
|
|
||||||
'"Table"."Column"' => $column,
|
|
||||||
'"Table"."Name"' => $value
|
|
||||||
));
|
|
||||||
|
|
||||||
// Multiple predicates, some with multiple parameters.
|
|
||||||
// The parameters should ideally not be an associative array.
|
|
||||||
$query = $query->where(array(
|
|
||||||
'"Table"."ColumnOne" = ? OR "Table"."ColumnTwo" != ?' => array(1, 4),
|
|
||||||
'"Table"."ID" != ?' => $value
|
|
||||||
));
|
|
||||||
|
|
||||||
// Multiple predicates, each with explicitly typed parameters.
|
|
||||||
//
|
|
||||||
// The purpose of this syntax is to provide not only parameter values, but
|
|
||||||
// to also instruct the database connector on how to treat this value
|
|
||||||
// internally (subject to the database API supporting this feature).
|
|
||||||
//
|
|
||||||
// SQLQuery distinguishes these from predicates with multiple parameters
|
|
||||||
// by checking for the 'value' key in any array parameter given
|
|
||||||
$query = $query->whereAny(array(
|
|
||||||
'"Table"."Column"' => array(
|
|
||||||
'value' => $value,
|
|
||||||
'type' => 'string' // or any php type
|
|
||||||
),
|
|
||||||
'"Table"."HasValue"' => array(
|
|
||||||
'value' => 0,
|
|
||||||
'type' => 'boolean'
|
|
||||||
)
|
|
||||||
));
|
|
||||||
|
|
||||||
#### Run-Time Evaluated Conditions with SQLConditionGroup
|
|
||||||
|
|
||||||
Conditional expressions and groups may be encapsulated within a class (implementing
|
|
||||||
the SQLConditionGroup interface) and evaluated at the time of execution.
|
|
||||||
|
|
||||||
This is useful for conditions which may be placed into a query before the details
|
|
||||||
of that condition are fully specified.
|
|
||||||
|
|
||||||
E.g.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
class RandomGroup implements SQLConditionGroup {
|
|
||||||
public $field = null;
|
|
||||||
public function conditionSQL(&$parameters) {
|
|
||||||
$parameters = array();
|
|
||||||
return "{$this->field} < RAND()";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$query = SQLSelect::create()
|
|
||||||
->setFrom('"MyObject"')
|
|
||||||
->setWhere($condition = new RandomCondition());
|
|
||||||
$condition->field = '"Score"';
|
|
||||||
$items = $query->execute();
|
|
||||||
|
|
||||||
#### Direct SQL Predicate
|
|
||||||
|
|
||||||
Conditions can be a literal piece of SQL which doesn't involve any parameters or values
|
|
||||||
at all, or can using safely SQL-encoded values, as it was originally.
|
|
||||||
|
|
||||||
<div class="warning" markdown='1'>
|
|
||||||
In nearly every instance it's preferrable to use the parameterised syntax, especially dealing
|
|
||||||
with variable parameters, even if those values were not submitted by the user.
|
|
||||||
See [the security topic](/topics/security#parameterised-queries) for details.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
For instance, the following are all valid ways of adding SQL conditions directly to a query
|
|
||||||
|
|
||||||
:::php
|
|
||||||
<?php
|
|
||||||
// the entire predicate as a single string
|
|
||||||
$query->addWhere("\"Column\" = 'Value'");
|
|
||||||
|
|
||||||
// multiple predicates as an array
|
|
||||||
$query->addWhere(array("\"Column\" = 'Value'", "\"Column\" != 'Value'"));
|
|
||||||
|
|
||||||
// Shorthand for the above using argument expansion
|
|
||||||
$query->addWhere("\"Column\" = 'Value'", "\"Column\" != 'Value'");
|
|
||||||
|
|
||||||
// Literal SQL condition
|
|
||||||
$query->addWhere('"Created" > NOW()"');
|
|
||||||
|
|
||||||
#### Joining
|
|
||||||
|
|
||||||
You can specify a join with the innerJoin and leftJoin methods. Both of these
|
|
||||||
methods have the same arguments:
|
|
||||||
|
|
||||||
* The name of the table to join to
|
|
||||||
* The filter clause for the join
|
|
||||||
* An optional alias
|
|
||||||
* Priority (to allow you to later sort joins)
|
|
||||||
* An optional list of parameters (in case you wish to use a parameterised subselect).
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// Without an alias
|
|
||||||
$members = Member::get()
|
|
||||||
->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
|
|
||||||
|
|
||||||
$members = Member::get()
|
|
||||||
->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel");
|
|
||||||
|
|
||||||
// With a subselect
|
|
||||||
$members = Member::get()
|
|
||||||
->innerJoin(
|
|
||||||
'(SELECT "MemberID", COUNT("ID") AS "Count" FROM "Member_Likes" GROUP BY "MemberID" HAVING "Count" >= ?)',
|
|
||||||
'"Likes"."MemberID" = "Member"."ID"',
|
|
||||||
"Likes",
|
|
||||||
20,
|
|
||||||
array($threshold)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
Passing a *$join* statement to DataObject::get will filter results further by
|
|
||||||
the JOINs performed against the foreign table. **It will NOT return the
|
|
||||||
additionally joined data.** The returned *$records* will always be a
|
|
||||||
`[api:DataObject]`.
|
|
||||||
|
|
||||||
## Properties
|
|
||||||
|
|
||||||
|
|
||||||
### Definition
|
|
||||||
|
|
||||||
Data is defined in the static variable $db on each class, in the format:
|
|
||||||
`<property-name>` => "data-type"
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Player extends DataObject {
|
|
||||||
private static $db = array(
|
|
||||||
"FirstName" => "Varchar",
|
|
||||||
"Surname" => "Varchar",
|
|
||||||
"Description" => "Text",
|
|
||||||
"Status" => "Enum(array('Active', 'Injured', 'Retired'))",
|
|
||||||
"Birthday" => "Date"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
See [data-types](data-types) for all available types.
|
|
||||||
|
|
||||||
### Overloading
|
|
||||||
|
|
||||||
"Getters" and "Setters" are functions that help us save fields to our data
|
|
||||||
objects. By default, the methods getField() and setField() are used to set data
|
|
||||||
object fields. They save to the protected array, $obj->record. We can overload
|
|
||||||
the default behavior by making a function called "get`<fieldname>`" or
|
|
||||||
"set`<fieldname>`".
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Player extends DataObject {
|
|
||||||
private static $db = array(
|
|
||||||
"Status" => "Enum(array('Active', 'Injured', 'Retired'))"
|
|
||||||
);
|
|
||||||
|
|
||||||
// access through $myPlayer->Status
|
|
||||||
public function getStatus() {
|
|
||||||
// check if the Player is actually... born already!
|
|
||||||
return (!$this->obj("Birthday")->InPast()) ? "Unborn" : $this->Status;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
### Customizing
|
|
||||||
|
|
||||||
We can create new "virtual properties" which are not actually listed in
|
|
||||||
`private static $db` or stored in the database-row.
|
|
||||||
|
|
||||||
Here we combined a Player's first name and surname, accessible through
|
|
||||||
$myPlayer->Title.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Player extends DataObject {
|
|
||||||
|
|
||||||
public function getTitle() {
|
|
||||||
return "{$this->FirstName} {$this->Surname}";
|
|
||||||
}
|
|
||||||
|
|
||||||
// access through $myPlayer->Title = "John Doe";
|
|
||||||
// just saves data on the object, please use $myPlayer->write() to save
|
|
||||||
// the database-row
|
|
||||||
public function setTitle($title) {
|
|
||||||
list($firstName, $surName) = explode(' ', $title);
|
|
||||||
$this->FirstName = $firstName;
|
|
||||||
$this->Surname = $surName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="warning" markdown='1'>
|
|
||||||
**CAUTION:** It is common practice to make sure that pairs of custom
|
|
||||||
getters/setter deal with the same data, in a consistent format.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="warning" markdown='1'>
|
|
||||||
**CAUTION:** Custom setters can be hard to debug: Please double check if you
|
|
||||||
could transform your data in more straight-forward logic embedded to your custom
|
|
||||||
controller or form-saving.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Default Values
|
|
||||||
|
|
||||||
Define the default values for all the $db fields. This example sets the
|
|
||||||
"Status"-column on Player to "Active" whenever a new object is created.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Player extends DataObject {
|
|
||||||
|
|
||||||
private static $defaults = array(
|
|
||||||
"Status" => 'Active',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="notice" markdown='1'>
|
|
||||||
Note: Alternatively you can set defaults directly in the database-schema (rather
|
|
||||||
than the object-model). See [data-types](data-types) for details.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Casting
|
|
||||||
|
|
||||||
Properties defined in *static $db* are automatically casted to their
|
|
||||||
[data-types](data-types) when used in templates.
|
|
||||||
|
|
||||||
You can also cast the return-values of your custom functions (e.g. your "virtual
|
|
||||||
properties"). Calling those functions directly will still return whatever type
|
|
||||||
your PHP code generates, but using the *obj()*-method or accessing through a
|
|
||||||
template will cast the value according to the $casting-definition.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Player extends DataObject {
|
|
||||||
|
|
||||||
private static $casting = array(
|
|
||||||
"MembershipFee" => 'Currency',
|
|
||||||
);
|
|
||||||
|
|
||||||
// $myPlayer->MembershipFee() returns a float (e.g. 123.45)
|
|
||||||
// $myPlayer->obj('MembershipFee') returns a object of type Currency
|
|
||||||
// In a template: <% loop $MyPlayer %>MembershipFee.Nice<% end_loop %>
|
|
||||||
// returns a casted string (e.g. "$123.45")
|
|
||||||
public function getMembershipFee() {
|
|
||||||
return $this->Team()->BaseFee * $this->MembershipYears;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
## Relations
|
|
||||||
|
|
||||||
Relations are built through static array definitions on a class, in the format
|
|
||||||
`<relationship-name> => <classname>`.
|
|
||||||
|
|
||||||
### has_one
|
|
||||||
|
|
||||||
A 1-to-1 relation creates a database-column called "`<relationship-name>`ID", in
|
|
||||||
the example below this would be "TeamID" on the "Player"-table.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// access with $myPlayer->Team()
|
|
||||||
class Player extends DataObject {
|
|
||||||
|
|
||||||
private static $has_one = array(
|
|
||||||
"Team" => "Team",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SilverStripe's `[api:SiteTree]` base-class for content-pages uses a 1-to-1
|
|
||||||
relationship to link to its parent element in the tree:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// access with $mySiteTree->Parent()
|
|
||||||
class SiteTree extends DataObject {
|
|
||||||
private static $has_one = array(
|
|
||||||
"Parent" => "SiteTree",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
A has_one can also be polymorphic, which allows any type of object to be associated.
|
|
||||||
This is useful where there could be many use cases for a particular data structure.
|
|
||||||
|
|
||||||
An additional column is created called "`<relationship-name>`Class", which along
|
|
||||||
with the ID column identifies the object.
|
|
||||||
|
|
||||||
To specify that a has_one relation is polymorphic set the type to 'DataObject'.
|
|
||||||
Ideally, the associated has_many (or belongs_to) should be specified with dot notation.
|
|
||||||
|
|
||||||
::php
|
|
||||||
|
|
||||||
class Player extends DataObject {
|
|
||||||
private static $has_many = array(
|
|
||||||
"Fans" => "Fan.FanOf"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Team extends DataObject {
|
|
||||||
private static $has_many = array(
|
|
||||||
"Fans" => "Fan.FanOf"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type of object returned by $fan->FanOf() will vary
|
|
||||||
class Fan extends DataObject {
|
|
||||||
|
|
||||||
// Generates columns FanOfID and FanOfClass
|
|
||||||
private static $has_one = array(
|
|
||||||
"FanOf" => "DataObject"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="warning" markdown='1'>
|
|
||||||
Note: The use of polymorphic relationships can affect query performance, especially
|
|
||||||
on joins, and also increases the complexity of the database and necessary user code.
|
|
||||||
They should be used sparingly, and only where additional complexity would otherwise
|
|
||||||
be necessary. E.g. Additional parent classes for each respective relationship, or
|
|
||||||
duplication of code.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### has_many
|
|
||||||
|
|
||||||
Defines 1-to-many joins. A database-column named ""`<relationship-name>`ID""
|
|
||||||
will to be created in the child-class.
|
|
||||||
|
|
||||||
<div class="warning" markdown='1'>
|
|
||||||
**CAUTION:** Please specify a $has_one-relationship on the related child-class
|
|
||||||
as well, in order to have the necessary accessors available on both ends.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// access with $myTeam->Players() or $player->Team()
|
|
||||||
class Team extends DataObject {
|
|
||||||
|
|
||||||
private static $has_many = array(
|
|
||||||
"Players" => "Player",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Player extends DataObject {
|
|
||||||
|
|
||||||
private static $has_one = array(
|
|
||||||
"Team" => "Team",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
To specify multiple $has_manys to the same object you can use dot notation to
|
|
||||||
distinguish them like below
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Person extends DataObject {
|
|
||||||
|
|
||||||
private static $has_many = array(
|
|
||||||
"Managing" => "Company.Manager",
|
|
||||||
"Cleaning" => "Company.Cleaner",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Company extends DataObject {
|
|
||||||
|
|
||||||
private static $has_one = array(
|
|
||||||
"Manager" => "Person",
|
|
||||||
"Cleaner" => "Person"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Multiple $has_one relationships are okay if they aren't linking to the same
|
|
||||||
object type.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
/**
|
|
||||||
* THIS IS BAD
|
|
||||||
*/
|
|
||||||
class Team extends DataObject {
|
|
||||||
|
|
||||||
private static $has_many = array(
|
|
||||||
"Players" => "Player",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Player extends DataObject {
|
|
||||||
private static $has_one = array(
|
|
||||||
"Team" => "Team",
|
|
||||||
"AnotherTeam" => "Team",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
### belongs_to
|
|
||||||
|
|
||||||
Defines a 1-to-1 relationship with another object, which declares the other end
|
|
||||||
of the relationship with a corresponding $has_one. A single database column named
|
|
||||||
`<relationship-name>ID` will be created in the object with the $has_one, but
|
|
||||||
the $belongs_to by itself will not create a database field. This field will hold
|
|
||||||
the ID of the object declaring the $belongs_to.
|
|
||||||
|
|
||||||
Similarly with $has_many, dot notation can be used to explicitly specify the $has_one
|
|
||||||
which refers to this relation. This is not mandatory unless the relationship would
|
|
||||||
be otherwise ambiguous.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
|
|
||||||
class Torso extends DataObject {
|
|
||||||
// HeadID will be generated on the Torso table
|
|
||||||
private static $has_one = array(
|
|
||||||
'Head' => 'Head'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Head extends DataObject {
|
|
||||||
// No database field created. The '.Head' suffix could be omitted
|
|
||||||
private static $belongs_to = array(
|
|
||||||
'Torso' => 'Torso.Head'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
### many_many
|
|
||||||
|
|
||||||
Defines many-to-many joins. A new table, (this-class)_(relationship-name), will
|
|
||||||
be created with a pair of ID fields.
|
|
||||||
|
|
||||||
<div class="warning" markdown='1'>
|
|
||||||
**CAUTION:** Please specify a $belongs_many_many-relationship on the related
|
|
||||||
class as well, in order to have the necessary accessors available on both ends.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// access with $myTeam->Categories() or $myCategory->Teams()
|
|
||||||
class Team extends DataObject {
|
|
||||||
|
|
||||||
private static $many_many = array(
|
|
||||||
"Categories" => "Category",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Category extends DataObject {
|
|
||||||
|
|
||||||
private static $belongs_many_many = array(
|
|
||||||
"Teams" => "Team",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
### Adding relations
|
|
||||||
|
|
||||||
Adding new items to a relations works the same, regardless if you're editing a
|
|
||||||
*has_many*- or a *many_many*. They are encapsulated by `[api:HasManyList]` and
|
|
||||||
`[api:ManyManyList]`, both of which provide very similar APIs, e.g. an `add()`
|
|
||||||
and `remove()` method.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Team extends DataObject {
|
|
||||||
|
|
||||||
// see "many_many"-description for a sample definition of class "Category"
|
|
||||||
private static $many_many = array(
|
|
||||||
"Categories" => "Category",
|
|
||||||
);
|
|
||||||
|
|
||||||
public function addCategories(SS_List $cats) {
|
|
||||||
foreach($cats as $cat) $this->Categories()->add($cat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
### Custom Relations
|
|
||||||
|
|
||||||
You can use the flexible datamodel to get a filtered result-list without writing
|
|
||||||
any SQL. For example, this snippet gets you the "Players"-relation on a team,
|
|
||||||
but only containing active players.
|
|
||||||
|
|
||||||
See `[api:DataObject::$has_many]` for more info on the described relations.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Team extends DataObject {
|
|
||||||
private static $has_many = array(
|
|
||||||
"Players" => "Player"
|
|
||||||
);
|
|
||||||
|
|
||||||
// can be accessed by $myTeam->ActivePlayers()
|
|
||||||
public function ActivePlayers() {
|
|
||||||
return $this->Players()->filter('Status', 'Active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Note: Adding new records to a filtered `RelationList` like in the example above
|
|
||||||
doesn't automatically set the filtered criteria on the added record.
|
|
||||||
|
|
||||||
### Relations on Unsaved Objects
|
|
||||||
|
|
||||||
You can also set *has_many* and *many_many* relations before the `DataObject` is
|
|
||||||
saved. This behaviour uses the `[api:UnsavedRelationList]` and converts it into
|
|
||||||
the correct `RelationList` when saving the `DataObject` for the first time.
|
|
||||||
|
|
||||||
This unsaved lists will also recursively save any unsaved objects that they
|
|
||||||
contain.
|
|
||||||
|
|
||||||
As these lists are not backed by the database, most of the filtering methods on
|
|
||||||
`DataList` cannot be used on a list of this type. As such, an
|
|
||||||
`UnsavedRelationList` should only be used for setting a relation before saving
|
|
||||||
an object, not for displaying the objects contained in the relation.
|
|
||||||
|
|
||||||
## Validation and Constraints
|
|
||||||
|
|
||||||
Traditionally, validation in SilverStripe has been mostly handled on the
|
|
||||||
controller through [form validation](/topics/forms#form-validation).
|
|
||||||
|
|
||||||
While this is a useful approach, it can lead to data inconsistencies if the
|
|
||||||
record is modified outside of the controller and form context.
|
|
||||||
|
|
||||||
Most validation constraints are actually data constraints which belong on the
|
|
||||||
model. SilverStripe provides the `[api:DataObject->validate()]` method for this
|
|
||||||
purpose.
|
|
||||||
|
|
||||||
By default, there is no validation - objects are always valid!
|
|
||||||
However, you can overload this method in your
|
|
||||||
DataObject sub-classes to specify custom validation,
|
|
||||||
or use the hook through `[api:DataExtension]`.
|
|
||||||
|
|
||||||
Invalid objects won't be able to be written - a [api:ValidationException]` will
|
|
||||||
be thrown and no write will occur.
|
|
||||||
|
|
||||||
It is expected that you call validate() in your own application to test that an
|
|
||||||
object is valid before attempting a write, and respond appropriately if it isn't.
|
|
||||||
|
|
||||||
The return value of `validate()` is a `[api:ValidationResult]` object.
|
|
||||||
You can append your own errors in there.
|
|
||||||
|
|
||||||
Example: Validate postcodes based on the selected country
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyObject extends DataObject {
|
|
||||||
|
|
||||||
private static $db = array(
|
|
||||||
'Country' => 'Varchar',
|
|
||||||
'Postcode' => 'Varchar'
|
|
||||||
);
|
|
||||||
|
|
||||||
public function validate() {
|
|
||||||
$result = parent::validate();
|
|
||||||
if($this->Country == 'DE' && $this->Postcode && strlen($this->Postcode) != 5) {
|
|
||||||
$result->error('Need five digits for German postcodes');
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="hint" markdown='1'>
|
|
||||||
**Tip:** If you decide to add unique or other indexes to your model via
|
|
||||||
`static $indexes`, see [DataObject](/reference/dataobject) for details.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Maps
|
|
||||||
|
|
||||||
A map is an array where the array indexes contain data as well as the values.
|
|
||||||
You can build a map from any DataList like this:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->map('ID', 'FirstName');
|
|
||||||
|
|
||||||
This will return a map where the keys are Member IDs, and the values are the
|
|
||||||
corresponding FirstName values. Like everything else in the ORM, these maps are
|
|
||||||
lazy loaded, so the following code will only query a single record from the
|
|
||||||
database:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get()->map('ID', 'FirstName');
|
|
||||||
echo $member[5];
|
|
||||||
|
|
||||||
This functionality is provided by the `SS_Map` class, which can be used to build
|
|
||||||
a map around any `SS_List`.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$members = Member::get();
|
|
||||||
$map = new SS_Map($members, 'ID', 'FirstName');
|
|
||||||
|
|
||||||
Note: You can also retrieve a single property from all contained records
|
|
||||||
through [SS_List->column()](api:SS_List#_column).
|
|
||||||
|
|
||||||
## Data Handling
|
|
||||||
|
|
||||||
When saving data through the object model, you don't have to manually escape
|
|
||||||
strings to create SQL-safe commands. You have to make sure though that certain
|
|
||||||
properties are not overwritten, e.g. *ID* or *ClassName*.
|
|
||||||
|
|
||||||
### Creation
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$myPlayer = new Player();
|
|
||||||
$myPlayer->Firstname = "John"; // sets property on object
|
|
||||||
$myPlayer->write(); // writes row to database
|
|
||||||
|
|
||||||
|
|
||||||
### Update
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$myPlayer = Player::get()->byID(99);
|
|
||||||
if($myPlayer) {
|
|
||||||
$myPlayer->Firstname = "John"; // sets property on object
|
|
||||||
$myPlayer->write(); // writes row to database
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
### Batch Update
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$myPlayer->update(
|
|
||||||
ArrayLib::filter_keys(
|
|
||||||
$_REQUEST,
|
|
||||||
array('Birthday', 'Firstname')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
Alternatively you can use *castedUpdate()* to respect the
|
|
||||||
[data-types](/topics/data-types). This is preferred to manually casting data
|
|
||||||
before saving.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$myPlayer->castedUpdate(
|
|
||||||
ArrayLib::filter_keys(
|
|
||||||
$_REQUEST,
|
|
||||||
array('Birthday', 'Firstname')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
### onBeforeWrite
|
|
||||||
|
|
||||||
You can customize saving-behaviour for each DataObject, e.g. for adding workflow
|
|
||||||
or data customization. The function is triggered when calling *write()* to save
|
|
||||||
the object to the database. This includes saving a page in the CMS or altering a
|
|
||||||
ModelAdmin record.
|
|
||||||
|
|
||||||
Example: Disallow creation of new players if the currently logged-in player is
|
|
||||||
not a team-manager.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Player extends DataObject {
|
|
||||||
|
|
||||||
private static $has_many = array(
|
|
||||||
"Teams"=>"Team"
|
|
||||||
);
|
|
||||||
|
|
||||||
public function onBeforeWrite() {
|
|
||||||
// check on first write action, aka "database row creation"
|
|
||||||
// (ID-property is not set)
|
|
||||||
if(!$this->ID) {
|
|
||||||
$currentPlayer = Member::currentUser();
|
|
||||||
if(!$currentPlayer->IsTeamManager()) {
|
|
||||||
user_error('Player-creation not allowed', E_USER_ERROR);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check on every write action
|
|
||||||
if(!$this->record['TeamID']) {
|
|
||||||
user_error('Cannot save player without a valid team', E_USER_ERROR);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// CAUTION: You are required to call the parent-function, otherwise
|
|
||||||
// SilverStripe will not execute the request.
|
|
||||||
parent::onBeforeWrite();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
<div class="notice" markdown='1'>
|
|
||||||
Note: There are no separate methods for *onBeforeCreate* and *onBeforeUpdate*.
|
|
||||||
Please check for the existence of $this->ID to toggle these two modes, as shown
|
|
||||||
in the example above.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### onBeforeDelete
|
|
||||||
|
|
||||||
Triggered before executing *delete()* on an existing object.
|
|
||||||
|
|
||||||
Example: Checking for a specific [permission](/reference/permission) to delete
|
|
||||||
this type of object. It checks if a member is logged in who belongs to a group
|
|
||||||
containing the permission "PLAYER_DELETE".
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Player extends DataObject {
|
|
||||||
private static $has_many = array(
|
|
||||||
"Teams"=>"Team"
|
|
||||||
);
|
|
||||||
|
|
||||||
public function onBeforeDelete() {
|
|
||||||
if(!Permission::check('PLAYER_DELETE')) {
|
|
||||||
Security::permissionFailure($this);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
parent::onBeforeDelete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
### Saving data with forms
|
|
||||||
|
|
||||||
See [forms](/topics/forms).
|
|
||||||
|
|
||||||
### Saving data with custom SQL
|
|
||||||
|
|
||||||
See the ["sql queries" topic](/reference/sqlquery) for custom *INSERT*,
|
|
||||||
*UPDATE*, *DELETE* queries.
|
|
||||||
|
|
||||||
## Extending DataObjects
|
|
||||||
|
|
||||||
You can add properties and methods to existing `[api:DataObjects]`s like
|
|
||||||
`[api:Member]` (a core class) without hacking core code or subclassing. See
|
|
||||||
`[api:DataExtension]` for a general description, and `[api:Hierarchy]` for the
|
|
||||||
most popular examples.
|
|
||||||
|
|
||||||
## FAQ
|
|
||||||
|
|
||||||
### What's the difference between DataObject::get() and a relation-getter?
|
|
||||||
|
|
||||||
You can work with both in pretty much the same way, but relationship-getters
|
|
||||||
return a special type of collection:
|
|
||||||
|
|
||||||
A `[api:HasManyList]` or a `[api:ManyManyList]` with relation-specific
|
|
||||||
functionality.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$myTeams = $myPlayer->Team(); // returns HasManyList
|
|
||||||
$myTeam->add($myOtherPlayer);
|
|
@ -0,0 +1,84 @@
|
|||||||
|
title: Extending DataObjects
|
||||||
|
summary: Modify the data model without using subclasses.
|
||||||
|
|
||||||
|
# Extending DataObjects
|
||||||
|
|
||||||
|
You can add properties and methods to existing [api:DataObjects]s like [api:Member] without hacking core code or sub
|
||||||
|
classing by using [api:DataExtension]. See the [Extending SilverStripe](../extending) guide for more information on
|
||||||
|
[api:DataExtension].
|
||||||
|
|
||||||
|
The following documentation outlines some common hooks that the [api:Extension] API provides specifically for managing
|
||||||
|
data records.
|
||||||
|
|
||||||
|
## onBeforeWrite
|
||||||
|
|
||||||
|
You can customize saving-behaviour for each DataObject, e.g. for adding workflow or data customization. The function is
|
||||||
|
triggered when calling *write()* to save the object to the database. This includes saving a page in the CMS or altering
|
||||||
|
a `ModelAdmin` record.
|
||||||
|
|
||||||
|
Example: Disallow creation of new players if the currently logged-in player is not a team-manager.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
private static $has_many = array(
|
||||||
|
"Teams"=>"Team"
|
||||||
|
);
|
||||||
|
|
||||||
|
public function onBeforeWrite() {
|
||||||
|
// check on first write action, aka "database row creation" (ID-property is not set)
|
||||||
|
if(!$this->isInDb()) {
|
||||||
|
$currentPlayer = Member::currentUser();
|
||||||
|
|
||||||
|
if(!$currentPlayer->IsTeamManager()) {
|
||||||
|
user_error('Player-creation not allowed', E_USER_ERROR);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check on every write action
|
||||||
|
if(!$this->record['TeamID']) {
|
||||||
|
user_error('Cannot save player without a valid team', E_USER_ERROR);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// CAUTION: You are required to call the parent-function, otherwise
|
||||||
|
// SilverStripe will not execute the request.
|
||||||
|
parent::onBeforeWrite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
## onBeforeDelete
|
||||||
|
|
||||||
|
Triggered before executing *delete()* on an existing object.
|
||||||
|
|
||||||
|
Example: Checking for a specific [permission](/reference/permission) to delete this type of object. It checks if a
|
||||||
|
member is logged in who belongs to a group containing the permission "PLAYER_DELETE".
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
private static $has_many = array(
|
||||||
|
"Teams" => "Team"
|
||||||
|
);
|
||||||
|
|
||||||
|
public function onBeforeDelete() {
|
||||||
|
if(!Permission::check('PLAYER_DELETE')) {
|
||||||
|
Security::permissionFailure($this);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::onBeforeDelete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="notice" markdown='1'>
|
||||||
|
Note: There are no separate methods for *onBeforeCreate* and *onBeforeUpdate*. Please check `$this->isInDb()` to toggle
|
||||||
|
these two modes, as shown in the example above.
|
||||||
|
</div>
|
@ -1,114 +0,0 @@
|
|||||||
# Database Structure
|
|
||||||
|
|
||||||
SilverStripe is currently hard-coded to use a fix mapping between data-objects and the underlying database structure -
|
|
||||||
opting for "convention over configuration". This page details what that database structure is.
|
|
||||||
|
|
||||||
## Base tables
|
|
||||||
|
|
||||||
Each direct sub-class of `[api:DataObject]` will have its own table.
|
|
||||||
|
|
||||||
The following fields are always created.
|
|
||||||
|
|
||||||
* ID: Primary Key
|
|
||||||
* ClassName: An enumeration listing this data-class and all of its subclasses.
|
|
||||||
* Created: A date/time field set to the creation date of this record
|
|
||||||
* LastEdited: A date/time field set to the date this record was last edited
|
|
||||||
|
|
||||||
Every object of this class **or any of its subclasses** will have an entry in this table
|
|
||||||
|
|
||||||
### Extra Fields
|
|
||||||
|
|
||||||
* Every field listed in the data object's **$db** array will be included in this table.
|
|
||||||
* For every relationship listed in the data object's **$has_one** array, there will be an integer field included in the
|
|
||||||
table. This will contain the ID of the data-object being linked to. The database field name will be of the form
|
|
||||||
"(relationship-name)ID", for example, ParentID. For polymorphic has_one relationships, there is an additional
|
|
||||||
"(relationship-name)Class" field to identify the class this ID corresponds to. See [datamodel](/topics/datamodel#has_one).
|
|
||||||
|
|
||||||
### ID Generation
|
|
||||||
|
|
||||||
When a new record is created, we don't use the database's built-in auto-numbering system. Instead, we generate a new ID
|
|
||||||
by adding 1 to the current maximum ID.
|
|
||||||
|
|
||||||
## Subclass tables
|
|
||||||
|
|
||||||
At SilverStripe's heart is an object-relational model. And a component of object-oriented data is **inheritance**.
|
|
||||||
Unfortunately, there is no native way of representing inheritance in a relational database. What we do is store the
|
|
||||||
data sub-classed objects across **multiple tables**.
|
|
||||||
|
|
||||||
For example, suppose we have the following set of classes:
|
|
||||||
|
|
||||||
* Class `[api:SiteTree]` extends `[api:DataObject]`: Title, Content fields
|
|
||||||
* Class `[api:Page]` extends `[api:SiteTree]`: Abstract field
|
|
||||||
* Class NewsSection extends `[api:SiteTree]`: *No special fields*
|
|
||||||
* Class NewsArticle extend `[api:Page]`: ArticleDate field
|
|
||||||
|
|
||||||
The data for the following classes would be stored across the following tables:
|
|
||||||
|
|
||||||
* `[api:SiteTree]`
|
|
||||||
* ID: Int
|
|
||||||
* ClassName: Enum('SiteTree', 'Page', 'NewsArticle')
|
|
||||||
* Created: Datetime
|
|
||||||
* LastEdited: Datetime
|
|
||||||
* Title: Varchar
|
|
||||||
* Content: Text
|
|
||||||
* `[api:Page]`
|
|
||||||
* ID: Int
|
|
||||||
* Abstract: Text
|
|
||||||
* NewsArticle
|
|
||||||
* ID: Int
|
|
||||||
* ArticleDate: Date
|
|
||||||
|
|
||||||
The way it works is this:
|
|
||||||
|
|
||||||
* "Base classes" are direct sub-classes of `[api:DataObject]`. They are always given a table, whether or not they have
|
|
||||||
special fields. This is called the "base table"
|
|
||||||
* The base table's ClassName field is set to class of the given record. It's an enumeration of all possible
|
|
||||||
sub-classes of the base class (including the base class itself)
|
|
||||||
* Each sub-class of the base object will also be given its own table, *as long as it has custom fields*. In the
|
|
||||||
example above, NewsSection didn't have its own data and so an extra table would be redundant.
|
|
||||||
* In all the tables, ID is the primary key. A matching ID number is used for all parts of a particular record:
|
|
||||||
record #2 in Page refers to the same object as record #2 in `[api:SiteTree]`.
|
|
||||||
|
|
||||||
To retrieve a news article, SilverStripe joins the `[api:SiteTree]`, `[api:Page]` and NewsArticle tables by their ID fields. We use a
|
|
||||||
left-join for robustness; if there is no matching record in Page, we can return a record with a blank Article field.
|
|
||||||
|
|
||||||
## Staging and versioning
|
|
||||||
|
|
||||||
[todo]
|
|
||||||
|
|
||||||
## Schema auto-generation
|
|
||||||
|
|
||||||
SilverStripe has a powerful tool for automatically building database schemas. We've designed it so that you should never have to build them manually.
|
|
||||||
|
|
||||||
To access it, visit http://localhost/dev/build?flush=1. This script will analyze the existing schema, compare it to what's required by your data classes, and alter the schema as required.
|
|
||||||
|
|
||||||
Put the ?flush=1 on the end if you've added PHP files, so that the rest of the system will find these new classes.
|
|
||||||
|
|
||||||
It will perform the following changes:
|
|
||||||
|
|
||||||
* Create any missing tables
|
|
||||||
* Create any missing fields
|
|
||||||
* Create any missing indexes
|
|
||||||
* Alter the field type of any existing fields
|
|
||||||
* Rename any obsolete tables that it previously created to _obsolete_(tablename)
|
|
||||||
|
|
||||||
It **won't** do any of the following
|
|
||||||
|
|
||||||
* Deleting tables
|
|
||||||
* Deleting fields
|
|
||||||
* Rename any tables that it doesn't recognize - so other applications can co-exist in the same database, as long as their table names don't match a SilverStripe data class.
|
|
||||||
|
|
||||||
|
|
||||||
## Related code
|
|
||||||
|
|
||||||
The information documented in this page is reflected in a few places in the code:
|
|
||||||
|
|
||||||
* `[api:DataObject]`
|
|
||||||
* requireTable() is responsible for specifying the required database schema
|
|
||||||
* instance_get() and instance_get_one() are responsible for generating the database queries for selecting data.
|
|
||||||
* write() is responsible for generating the database queries for writing data.
|
|
||||||
* `[api:Versioned]`
|
|
||||||
* augmentWrite() is responsible for altering the normal database writing operation to handle versions.
|
|
||||||
* augmentQuery() is responsible for altering the normal data selection queries to support versions.
|
|
||||||
* augmentDatabase() is responsible for specifying the altered database schema to support versions.
|
|
||||||
* `[api:MySQLDatabase]`: handles the mechanics of updating the database to have the required schema.
|
|
53
docs/en/02_Developer_Guides/00_Model/06_SearchFilters.md
Normal file
53
docs/en/02_Developer_Guides/00_Model/06_SearchFilters.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
title: SearchFilter Modifiers
|
||||||
|
summary: Use suffixes on your ORM queries.
|
||||||
|
|
||||||
|
# SearchFilter Modifiers
|
||||||
|
|
||||||
|
The `filter` and `exclude` operations specify exact matches by default. However, there are a number of suffixes that
|
||||||
|
you can put on field names to change this behavior. These are represented as `SearchFilter` subclasses and include.
|
||||||
|
|
||||||
|
* [api:StartsWithFilter]
|
||||||
|
* [api:EndsWithFilter]
|
||||||
|
* [api:PartialMatchFilter]
|
||||||
|
* [api:GreaterThanFilter]
|
||||||
|
* [api:GreaterThanOrEqualFilter]
|
||||||
|
* [api:LessThanFilter]
|
||||||
|
* [api:LessThanOrEqualFilter]
|
||||||
|
|
||||||
|
An example of a `SearchFilter` in use:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
// fetch any player that starts with a S
|
||||||
|
$players = Player::get()->filter(array(
|
||||||
|
'FirstName:StartsWith' => 'S'
|
||||||
|
'PlayerNumber:GreaterThan' => '10'
|
||||||
|
));
|
||||||
|
|
||||||
|
// to fetch any player that's name contains the letter 'z'
|
||||||
|
$players = Player::get()->filterAny(array(
|
||||||
|
'FirstName:PartialMatch' => 'z'
|
||||||
|
'LastName:PartialMatch' => 'z'
|
||||||
|
));
|
||||||
|
|
||||||
|
Developers can define their own [api:SearchFilter] if needing to extend the ORM filter and exclude behaviors.
|
||||||
|
|
||||||
|
These suffixes can also take modifiers themselves. The modifiers currently supported are `":not"`, `":nocase"` and
|
||||||
|
`":case"`. These negate the filter, make it case-insensitive and make it case-sensitive respectively. The default
|
||||||
|
comparison uses the database's default. For MySQL and MSSQL, this is case-insensitive. For PostgreSQL, this is
|
||||||
|
case-sensitive.
|
||||||
|
|
||||||
|
The following is a query which will return everyone whose first name starts with S either lower or uppercase
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$players = Player::get()->filter(array(
|
||||||
|
'FirstName:StartsWith:nocase' => 'S'
|
||||||
|
));
|
||||||
|
|
||||||
|
// use :not to perform a converse operation to filter anything but a 'W'
|
||||||
|
$players = Player::get()->filter(array(
|
||||||
|
'FirstName:StartsWith:not' => 'W'
|
||||||
|
));
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
* [api:SearchFilter]
|
50
docs/en/02_Developer_Guides/00_Model/07_Permissions.md
Normal file
50
docs/en/02_Developer_Guides/00_Model/07_Permissions.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
title: Model-Level Permissions
|
||||||
|
summary: Reduce risk by securing models.
|
||||||
|
|
||||||
|
# Model-Level Permissions
|
||||||
|
|
||||||
|
Models can be modified in a variety of controllers and user interfaces, all of which can implement their own security
|
||||||
|
checks. Often it makes sense to centralize those checks on the model, regardless of the used controller.
|
||||||
|
|
||||||
|
The API provides four methods for this purpose: `canEdit()`, `canCreate()`, `canView()` and `canDelete()`.
|
||||||
|
|
||||||
|
Since they're PHP methods, they can contain arbitrary logic matching your own requirements. They can optionally receive
|
||||||
|
a `$member` argument, and default to the currently logged in member (through `Member::currentUser()`).
|
||||||
|
|
||||||
|
<div class="notice" markdown="1">
|
||||||
|
By default, all `DataObject` subclasses can only be edited, created and viewed by users with the 'ADMIN' permission
|
||||||
|
code.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class MyDataObject extends DataObject {
|
||||||
|
|
||||||
|
public function canView($member = null) {
|
||||||
|
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canEdit($member = null) {
|
||||||
|
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canDelete($member = null) {
|
||||||
|
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canCreate($member = null) {
|
||||||
|
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="alert" markdown="1">
|
||||||
|
These checks are not enforced on low-level ORM operations such as `write()` or `delete()`, but rather rely on being
|
||||||
|
checked in the invoking code. The CMS default sections as well as custom interfaces like [api:ModelAdmin] or
|
||||||
|
[api:GridField] already enforce these permissions.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
* [api:DataObject]
|
||||||
|
* [api:Permission]
|
@ -1,271 +0,0 @@
|
|||||||
# SQL Select
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
An object representing a SQL select query, which can be serialized into a SQL statement.
|
|
||||||
It is easier to deal with object-wrappers than string-parsing a raw SQL-query.
|
|
||||||
This object is used by the SilverStripe ORM internally.
|
|
||||||
|
|
||||||
Dealing with low-level SQL is not encouraged, since the ORM provides
|
|
||||||
powerful abstraction APIs (see [datamodel](/topics/datamodel).
|
|
||||||
Starting with SilverStripe 3, records in collections are lazy loaded,
|
|
||||||
and these collections have the ability to run efficient SQL
|
|
||||||
such as counts or returning a single column.
|
|
||||||
|
|
||||||
For example, if you want to run a simple `COUNT` SQL statement,
|
|
||||||
the following three statements are functionally equivalent:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// Through raw SQL
|
|
||||||
$count = DB::query('SELECT COUNT(*) FROM "Member"')->value();
|
|
||||||
// Through SQLSelect abstraction layer
|
|
||||||
$query = new SQLSelect();
|
|
||||||
$count = $query->setFrom('Member')->setSelect('COUNT(*)')->value();
|
|
||||||
// Through the ORM
|
|
||||||
$count = Member::get()->count();
|
|
||||||
|
|
||||||
If you do use raw SQL, you'll run the risk of breaking
|
|
||||||
various assumptions the ORM and code based on it have:
|
|
||||||
|
|
||||||
* Custom getters/setters (object property can differ from database column)
|
|
||||||
* DataObject hooks like onBeforeWrite() and onBeforeDelete()
|
|
||||||
* Automatic casting
|
|
||||||
* Default values set through objects
|
|
||||||
* Database abstraction
|
|
||||||
|
|
||||||
We'll explain some ways to use *SELECT* with the full power of SQL,
|
|
||||||
but still maintain a connection to the ORM where possible.
|
|
||||||
|
|
||||||
<div class="warning" markdown="1">
|
|
||||||
Please read our ["security" topic](/topics/security) to find out
|
|
||||||
how to properly prepare user input and variables for use in queries
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### SELECT
|
|
||||||
|
|
||||||
Selection can be done by creating an instance of `SQLSelect`, which allows
|
|
||||||
management of all elements of a SQL SELECT query, including columns, joined tables,
|
|
||||||
conditional filters, grouping, limiting, and sorting.
|
|
||||||
|
|
||||||
E.g.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$sqlSelect = new SQLSelect();
|
|
||||||
$sqlSelect->setFrom('Player');
|
|
||||||
$sqlSelect->selectField('FieldName', 'Name');
|
|
||||||
$sqlSelect->selectField('YEAR("Birthday")', 'Birthyear');
|
|
||||||
$sqlSelect->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"');
|
|
||||||
$sqlSelect->addWhere(array('YEAR("Birthday") = ?' => 1982));
|
|
||||||
// $sqlSelect->setOrderBy(...);
|
|
||||||
// $sqlSelect->setGroupBy(...);
|
|
||||||
// $sqlSelect->setHaving(...);
|
|
||||||
// $sqlSelect->setLimit(...);
|
|
||||||
// $sqlSelect->setDistinct(true);
|
|
||||||
|
|
||||||
// Get the raw SQL (optional) and parameters
|
|
||||||
$rawSQL = $sqlSelect->sql($parameters);
|
|
||||||
|
|
||||||
// Execute and return a Query object
|
|
||||||
$result = $sqlSelect->execute();
|
|
||||||
|
|
||||||
// Iterate over results
|
|
||||||
foreach($result as $row) {
|
|
||||||
echo $row['BirthYear'];
|
|
||||||
}
|
|
||||||
|
|
||||||
The result of `SQLSelect::execute()` is an array lightly wrapped in a database-specific subclass of `[api:SS_Query]`.
|
|
||||||
This class implements the *Iterator*-interface, and provides convenience-methods for accessing the data.
|
|
||||||
|
|
||||||
### DELETE
|
|
||||||
|
|
||||||
Deletion can be done either by calling `DB::query`/`DB::prepared_query` directly,
|
|
||||||
by creating a `SQLDelete` object, or by transforming a `SQLSelect` into a `SQLDelete`
|
|
||||||
object instead.
|
|
||||||
|
|
||||||
For example, creating a `SQLDelete` object
|
|
||||||
|
|
||||||
:::php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$query = SQLDelete::create()
|
|
||||||
->setFrom('"SiteTree"')
|
|
||||||
->setWhere(array('"SiteTree"."ShowInMenus"' => 0));
|
|
||||||
$query->execute();
|
|
||||||
|
|
||||||
Alternatively, turning an existing `SQLSelect` into a delete
|
|
||||||
|
|
||||||
:::php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$query = SQLSelect::create()
|
|
||||||
->setFrom('"SiteTree"')
|
|
||||||
->setWhere(array('"SiteTree"."ShowInMenus"' => 0))
|
|
||||||
->toDelete();
|
|
||||||
$query->execute();
|
|
||||||
|
|
||||||
Directly querying the database
|
|
||||||
|
|
||||||
:::php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
DB::prepared_query('DELETE FROM "SiteTree" WHERE "SiteTree"."ShowInMenus" = ?', array(0));
|
|
||||||
|
|
||||||
### INSERT/UPDATE
|
|
||||||
|
|
||||||
INSERT and UPDATE can be performed using the `SQLInsert` and `SQLUpdate` classes.
|
|
||||||
These both have similar aspects in that they can modify content in
|
|
||||||
the database, but each are different in the way in which they behave.
|
|
||||||
|
|
||||||
Previously, similar operations could be performed by using the `DB::manipulate`
|
|
||||||
function which would build the INSERT and UPDATE queries on the fly. This method
|
|
||||||
still exists, but internally uses `SQLUpdate` / `SQLInsert`, although the actual
|
|
||||||
query construction is now done by the `DBQueryBuilder` object.
|
|
||||||
|
|
||||||
Each of these classes implements the interface `SQLWriteExpression`, noting that each
|
|
||||||
accepts write key/value pairs in a number of similar ways. These include the following
|
|
||||||
api methods:
|
|
||||||
|
|
||||||
* `addAssignments` - Takes a list of assignments as an associative array of key -> value pairs,
|
|
||||||
but also supports SQL expressions as values if necessary.
|
|
||||||
* `setAssignments` - Replaces all existing assignments with the specified list
|
|
||||||
* `getAssignments` - Returns all currently given assignments, as an associative array
|
|
||||||
in the format `array('Column' => array('SQL' => array('parameters)))`
|
|
||||||
* `assign` - Singular form of addAssignments, but only assigns a single column value.
|
|
||||||
* `assignSQL` - Assigns a column the value of a specified SQL expression without parameters
|
|
||||||
`assignSQL('Column', 'SQL)` is shorthand for `assign('Column', array('SQL' => array()))`
|
|
||||||
|
|
||||||
SQLUpdate also includes the following api methods:
|
|
||||||
|
|
||||||
* `clear` - Clears all assignments
|
|
||||||
* `getTable` - Gets the table to update
|
|
||||||
* `setTable` - Sets the table to update. This should be ANSI quoted.
|
|
||||||
E.g. `$query->setTable('"SiteTree"');`
|
|
||||||
|
|
||||||
SQLInsert also includes the following api methods:
|
|
||||||
* `clear` - Clears all rows
|
|
||||||
* `clearRow` - Clears all assignments on the current row
|
|
||||||
* `addRow` - Adds another row of assignments, and sets the current row to the new row
|
|
||||||
* `addRows` - Adds a number of arrays, each representing a list of assignment rows,
|
|
||||||
and sets the current row to the last one.
|
|
||||||
* `getColumns` - Gets the names of all distinct columns assigned
|
|
||||||
* `getInto` - Gets the table to insert into
|
|
||||||
* `setInto` - Sets the table to insert into. This should be ANSI quoted.
|
|
||||||
E.g. `$query->setInto('"SiteTree"');`
|
|
||||||
|
|
||||||
E.g.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
<?php
|
|
||||||
$update = SQLUpdate::create('"SiteTree"')->where(array('ID' => 3));
|
|
||||||
|
|
||||||
// assigning a list of items
|
|
||||||
$update->addAssignments(array(
|
|
||||||
'"Title"' => 'Our Products',
|
|
||||||
'"MenuTitle"' => 'Products'
|
|
||||||
));
|
|
||||||
|
|
||||||
// Assigning a single value
|
|
||||||
$update->assign('"MenuTitle"', 'Products');
|
|
||||||
|
|
||||||
// Assigning a value using parameterised expression
|
|
||||||
$title = 'Products';
|
|
||||||
$update->assign('"MenuTitle"', array(
|
|
||||||
'CASE WHEN LENGTH("MenuTitle") > LENGTH(?) THEN "MenuTitle" ELSE ? END' =>
|
|
||||||
array($title, $title)
|
|
||||||
));
|
|
||||||
|
|
||||||
// Assigning a value using a pure SQL expression
|
|
||||||
$update->assignSQL('"Date"', 'NOW()');
|
|
||||||
|
|
||||||
// Perform the update
|
|
||||||
$update->execute();
|
|
||||||
|
|
||||||
In addition to assigning values, the SQLInsert object also supports multi-row
|
|
||||||
inserts. For database connectors and API that don't have multi-row insert support
|
|
||||||
these are translated internally as multiple single row inserts.
|
|
||||||
|
|
||||||
For example,
|
|
||||||
|
|
||||||
:::php
|
|
||||||
<?php
|
|
||||||
$insert = SQLInsert::create('"SiteTree"');
|
|
||||||
|
|
||||||
// Add multiple rows in a single call. Note that column names do not need
|
|
||||||
// to be symmetric
|
|
||||||
$insert->addRows(array(
|
|
||||||
array('"Title"' => 'Home', '"Content"' => '<p>This is our home page</p>'),
|
|
||||||
array('"Title"' => 'About Us', '"ClassName"' => 'AboutPage')
|
|
||||||
));
|
|
||||||
|
|
||||||
// Adjust an assignment on the last row
|
|
||||||
$insert->assign('"Content"', '<p>This is about us</p>');
|
|
||||||
|
|
||||||
// Add another row
|
|
||||||
$insert->addRow(array('"Title"' => 'Contact Us'));
|
|
||||||
|
|
||||||
$columns = $insert->getColumns();
|
|
||||||
// $columns will be array('"Title"', '"Content"', '"ClassName"');
|
|
||||||
|
|
||||||
$insert->execute();
|
|
||||||
|
|
||||||
### Value Checks
|
|
||||||
|
|
||||||
Raw SQL is handy for performance-optimized calls,
|
|
||||||
e.g. when you want a single column rather than a full-blown object representation.
|
|
||||||
|
|
||||||
Example: Get the count from a relationship.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$sqlSelect = new SQLSelect();
|
|
||||||
$sqlSelect->setFrom('Player');
|
|
||||||
$sqlSelect->addSelect('COUNT("Player"."ID")');
|
|
||||||
$sqlSelect->addWhere(array('"Team"."ID"' => 99));
|
|
||||||
$sqlSelect->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"');
|
|
||||||
$count = $sqlSelect->execute()->value();
|
|
||||||
|
|
||||||
Note that in the ORM, this call would be executed in an efficient manner as well:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$count = $myTeam->Players()->count();
|
|
||||||
|
|
||||||
### Mapping
|
|
||||||
|
|
||||||
Creates a map based on the first two columns of the query result.
|
|
||||||
This can be useful for creating dropdowns.
|
|
||||||
|
|
||||||
Example: Show player names with their birth year, but set their birth dates as values.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$sqlSelect = new SQLSelect();
|
|
||||||
$sqlSelect->setFrom('Player');
|
|
||||||
$sqlSelect->setSelect('Birthdate');
|
|
||||||
$sqlSelect->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear');
|
|
||||||
$map = $sqlSelect->execute()->map();
|
|
||||||
$field = new DropdownField('Birthdates', 'Birthdates', $map);
|
|
||||||
|
|
||||||
Note that going through SQLSelect is just necessary here
|
|
||||||
because of the custom SQL value transformation (`YEAR()`).
|
|
||||||
An alternative approach would be a custom getter in the object definition.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Player extends DataObject {
|
|
||||||
private static $db = array(
|
|
||||||
'Name' => 'Varchar',
|
|
||||||
'Birthdate' => 'Date'
|
|
||||||
);
|
|
||||||
function getNameWithBirthyear() {
|
|
||||||
return date('y', $this->Birthdate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$players = Player::get();
|
|
||||||
$map = $players->map('Name', 'NameWithBirthyear');
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
* [datamodel](/topics/datamodel)
|
|
||||||
* `[api:DataObject]`
|
|
||||||
* [database-structure](database-structure)
|
|
114
docs/en/02_Developer_Guides/00_Model/08_SQL_Query.md
Normal file
114
docs/en/02_Developer_Guides/00_Model/08_SQL_Query.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
title: SQLQuery
|
||||||
|
summary: Write and modify direct database queries through SQLQuery.
|
||||||
|
|
||||||
|
# SQLQuery
|
||||||
|
|
||||||
|
A [api:SQLQuery] object represents a SQL query, which can be serialized into a SQL statement. Dealing with low-level
|
||||||
|
SQL such as `mysql_query()` is not encouraged, since the ORM provides powerful abstraction API's.
|
||||||
|
|
||||||
|
For example, if you want to run a simple `COUNT` SQL statement, the following three statements are functionally
|
||||||
|
equivalent:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
// Through raw SQL.
|
||||||
|
$count = DB::query('SELECT COUNT(*) FROM "Member"')->value();
|
||||||
|
|
||||||
|
// Through SQLQuery abstraction layer.
|
||||||
|
$query = new SQLQuery();
|
||||||
|
$count = $query->setFrom('Member')->setSelect('COUNT(*)')->value();
|
||||||
|
|
||||||
|
// Through the ORM.
|
||||||
|
$count = Member::get()->count();
|
||||||
|
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
The SQLQuery object is used by the SilverStripe ORM internally. By understanding SQLQuery, you can modify the SQL that
|
||||||
|
the ORM creates.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Select
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$sqlQuery = new SQLQuery();
|
||||||
|
$sqlQuery->setFrom('Player');
|
||||||
|
$sqlQuery->selectField('FieldName', 'Name');
|
||||||
|
$sqlQuery->selectField('YEAR("Birthday")', 'Birthyear');
|
||||||
|
$sqlQuery->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"');
|
||||||
|
$sqlQuery->addWhere('YEAR("Birthday") = 1982');
|
||||||
|
|
||||||
|
// $sqlQuery->setOrderBy(...);
|
||||||
|
// $sqlQuery->setGroupBy(...);
|
||||||
|
// $sqlQuery->setHaving(...);
|
||||||
|
// $sqlQuery->setLimit(...);
|
||||||
|
// $sqlQuery->setDistinct(true);
|
||||||
|
|
||||||
|
// Get the raw SQL (optional)
|
||||||
|
$rawSQL = $sqlQuery->sql();
|
||||||
|
|
||||||
|
// Execute and return a Query object
|
||||||
|
$result = $sqlQuery->execute();
|
||||||
|
|
||||||
|
// Iterate over results
|
||||||
|
foreach($result as $row) {
|
||||||
|
echo $row['BirthYear'];
|
||||||
|
}
|
||||||
|
|
||||||
|
The `$result` is an array lightly wrapped in a database-specific subclass of `[api:Query]`. This class implements the
|
||||||
|
*Iterator*-interface, and provides convenience-methods for accessing the data.
|
||||||
|
|
||||||
|
### Delete
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$sqlQuery->setDelete(true);
|
||||||
|
|
||||||
|
### Insert / Update
|
||||||
|
|
||||||
|
<div class="alert" markdown="1">
|
||||||
|
Currently not supported through the `SQLQuery` class, please use raw `DB::query()` calls instead.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
:::php
|
||||||
|
DB::query('UPDATE "Player" SET "Status"=\'Active\'');
|
||||||
|
|
||||||
|
### Joins
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$sqlQuery = new SQLQuery();
|
||||||
|
$sqlQuery->setFrom('Player');
|
||||||
|
$sqlQuery->addSelect('COUNT("Player"."ID")');
|
||||||
|
$sqlQuery->addWhere('"Team"."ID" = 99');
|
||||||
|
$sqlQuery->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"');
|
||||||
|
|
||||||
|
$count = $sqlQuery->execute()->value();
|
||||||
|
|
||||||
|
### Mapping
|
||||||
|
|
||||||
|
Creates a map based on the first two columns of the query result.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$sqlQuery = new SQLQuery();
|
||||||
|
$sqlQuery->setFrom('Player');
|
||||||
|
$sqlQuery->setSelect('ID');
|
||||||
|
$sqlQuery->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear');
|
||||||
|
$map = $sqlQuery->execute()->map();
|
||||||
|
|
||||||
|
echo $map;
|
||||||
|
|
||||||
|
// returns array(
|
||||||
|
// 1 => "Foo - 1920",
|
||||||
|
// 2 => "Bar - 1936"
|
||||||
|
// );
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
* [Introduction to the Data Model and ORM](../data_model_and_orm)
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
* [api:DataObject]
|
||||||
|
* [api:SQLQuery]
|
||||||
|
* [api:DB]
|
||||||
|
* [api:Query]
|
||||||
|
* [api:Database]
|
@ -1,204 +0,0 @@
|
|||||||
# Versioning of Database Content
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Database content in SilverStripe can be "staged" before its publication,
|
|
||||||
as well as track all changes through the lifetime of a database record.
|
|
||||||
|
|
||||||
It is most commonly applied to pages in the CMS (the `SiteTree` class).
|
|
||||||
This means that draft content edited in the CMS can be different from published content
|
|
||||||
shown to your website visitors.
|
|
||||||
|
|
||||||
The versioning happens automatically on read and write.
|
|
||||||
If you are using the SilverStripe ORM to perform these operations,
|
|
||||||
you don't need to alter your existing calls.
|
|
||||||
|
|
||||||
Versioning in SilverStripe is handled through the `[api:Versioned]` class.
|
|
||||||
It's a `[api:DataExtension]`, which allow it to be applied to any `[api:DataObject]` subclass.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Adding versioned to your `DataObject` subclass works the same as any other extension.
|
|
||||||
It accepts two or more arguments denoting the different "stages",
|
|
||||||
which map to different database tables. Add the following to your [configuration file](/topics/configuration):
|
|
||||||
|
|
||||||
:::yml
|
|
||||||
MyRecord:
|
|
||||||
extensions:
|
|
||||||
- Versioned("Stage","Live")
|
|
||||||
|
|
||||||
Note: The extension is automatically applied to `SiteTree` class.
|
|
||||||
|
|
||||||
## Database Structure
|
|
||||||
|
|
||||||
Depending on how many stages you configured, two or more new tables will be created for your records.
|
|
||||||
Note that the "Stage" naming has a special meaning here, it will leave the original
|
|
||||||
table name unchanged, rather than adding a suffix.
|
|
||||||
|
|
||||||
* `MyRecord` table: Contains staged data
|
|
||||||
* `MyRecord_Live` table: Contains live data
|
|
||||||
* `MyRecord_versions` table: Contains a version history (new record created on each save)
|
|
||||||
|
|
||||||
Similarly, any subclass you create on top of a versioned base
|
|
||||||
will trigger the creation of additional tables, which are automatically joined as required:
|
|
||||||
|
|
||||||
* `MyRecordSubclass` table: Contains only staged data for subclass columns
|
|
||||||
* `MyRecordSubclass_Live` table: Contains only live data for subclass columns
|
|
||||||
* `MyRecordSubclass_versions` table: Contains only version history for subclass columns
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Reading Versions
|
|
||||||
|
|
||||||
By default, all records are retrieved from the "Draft" stage (so the `MyRecord` table in our example).
|
|
||||||
You can explicitly request a certain stage through various getters on the `Versioned` class.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// Fetching multiple records
|
|
||||||
$stageRecords = Versioned::get_by_stage('MyRecord', 'Stage');
|
|
||||||
$liveRecords = Versioned::get_by_stage('MyRecord', 'Live');
|
|
||||||
|
|
||||||
// Fetching a single record
|
|
||||||
$stageRecord = Versioned::get_by_stage('MyRecord', 'Stage')->byID(99);
|
|
||||||
$liveRecord = Versioned::get_by_stage('MyRecord', 'Live')->byID(99);
|
|
||||||
|
|
||||||
### Historical Versions
|
|
||||||
|
|
||||||
The above commands will just retrieve the latest version of its respective stage for you,
|
|
||||||
but not older versions stored in the `<class>_versions` tables.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$historicalRecord = Versioned::get_version('MyRecord', <record-id>, <version-id>);
|
|
||||||
|
|
||||||
Caution: The record is retrieved as a `DataObject`, but saving back modifications
|
|
||||||
via `write()` will create a new version, rather than modifying the existing one.
|
|
||||||
|
|
||||||
In order to get a list of all versions for a specific record,
|
|
||||||
we need to generate specialized `[api:Versioned_Version]` objects,
|
|
||||||
which expose the same database information as a `DataObject`,
|
|
||||||
but also include information about when and how a record was published.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
|
||||||
$versions = $record->allVersions();
|
|
||||||
echo $versions->First()->Version; // instance of Versioned_Version
|
|
||||||
|
|
||||||
### Writing Versions and Changing Stages
|
|
||||||
|
|
||||||
The usual call to `DataObject->write()` will write to whatever stage is currently
|
|
||||||
active, as defined by the `Versioned::current_stage()` global setting.
|
|
||||||
Each call will automatically create a new version in the `<class>_versions` table.
|
|
||||||
To avoid this, use `[writeWithoutVersion()](api:Versioned->writeWithoutVersion())` instead.
|
|
||||||
|
|
||||||
To move a saved version from one stage to another,
|
|
||||||
call `[writeToStage(<stage>)](api:Versioned->writeToStage())` on the object.
|
|
||||||
The process of moving a version to a different stage is also called "publishing",
|
|
||||||
so we've created a shortcut for this: `publish(<from-stage>, <to-stage>)`.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$record = Versioned::get_by_stage('MyRecord', 'Stage')->byID(99);
|
|
||||||
$record->MyField = 'changed';
|
|
||||||
// will update `MyRecord` table (assuming Versioned::current_stage() == 'Stage'),
|
|
||||||
// and write a row to `MyRecord_versions`.
|
|
||||||
$record->write();
|
|
||||||
// will copy the saved record information to the `MyRecord_Live` table
|
|
||||||
$record->publish('Stage', 'Live');
|
|
||||||
|
|
||||||
Similarly, an "unpublish" operation does the reverse, and removes a record
|
|
||||||
from a specific stage.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
|
||||||
// will remove the row from the `MyRecord_Live` table
|
|
||||||
$record->deleteFromStage('Live');
|
|
||||||
|
|
||||||
### Forcing the Current Stage
|
|
||||||
|
|
||||||
The current stage is stored as global state on the object.
|
|
||||||
It is usually modified by controllers, e.g. when a preview is initialized.
|
|
||||||
But it can also be set and reset temporarily to force a specific operation
|
|
||||||
to run on a certain stage.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$origMode = Versioned::get_reading_mode(); // save current mode
|
|
||||||
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Live' records
|
|
||||||
Versioned::set_reading_mode('Stage'); // temporarily overwrite mode
|
|
||||||
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
|
|
||||||
Versioned::set_reading_mode($origMode); // reset current mode
|
|
||||||
|
|
||||||
### Custom SQL
|
|
||||||
|
|
||||||
We generally discourage writing `Versioned` queries from scratch,
|
|
||||||
due to the complexities involved through joining multiple tables
|
|
||||||
across an inherited table scheme (see `[api:Versioned->augmentSQL()]`).
|
|
||||||
If possible, try to stick to smaller modifications of the generated `DataList` objects.
|
|
||||||
|
|
||||||
Example: Get the first 10 live records, filtered by creation date:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$records = Versioned::get_by_stage('MyRecord', 'Live')->limit(10)->sort('Created', 'ASC');
|
|
||||||
|
|
||||||
### Permissions
|
|
||||||
|
|
||||||
The `Versioned` extension doesn't provide any permissions on its own,
|
|
||||||
but you can have a look at the `SiteTree` class for implementation samples,
|
|
||||||
specifically `canPublish()` and `canDeleteFromStage()`.
|
|
||||||
|
|
||||||
### Page Specific Operations
|
|
||||||
|
|
||||||
Since the `Versioned` extension is primarily used for page objects,
|
|
||||||
the underlying `SiteTree` class has some additional helpers.
|
|
||||||
See the ["sitetree" reference](/reference/sitetree) for details.
|
|
||||||
|
|
||||||
### Templates Variables
|
|
||||||
|
|
||||||
In templates, you don't need to worry about this distinction.
|
|
||||||
The `$Content` variable contain the published content by default,
|
|
||||||
and only preview draft content if explicitly requested (e.g. by the "preview" feature in the CMS).
|
|
||||||
If you want to force a specific stage, we recommend the `Controller->init()` method for this purpose.
|
|
||||||
|
|
||||||
### Controllers
|
|
||||||
|
|
||||||
The current stage for each request is determined by `VersionedRequestFilter` before
|
|
||||||
any controllers initialize, through `Versioned::choose_site_stage()`.
|
|
||||||
It checks for a `Stage` GET parameter, so you can force
|
|
||||||
a draft stage by appending `?stage=Stage` to your request. The setting is "sticky"
|
|
||||||
in the PHP session, so any subsequent requests will also be in draft stage.
|
|
||||||
|
|
||||||
Important: The `choose_site_stage()` call only deals with setting the default stage,
|
|
||||||
and doesn't check if the user is authenticated to view it. As with any other controller logic,
|
|
||||||
please use `DataObject->canView()` to determine permissions, and avoid exposing unpublished
|
|
||||||
content to your users.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyController extends Controller {
|
|
||||||
private static $allowed_actions = array('showpage');
|
|
||||||
public function showpage($request) {
|
|
||||||
$page = Page::get()->byID($request->param('ID'));
|
|
||||||
if(!$page->canView()) return $this->httpError(401);
|
|
||||||
// continue with authenticated logic...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
The `ContentController` class responsible for page display already has this built in,
|
|
||||||
so your own `canView()` checks are only necessary in controllers extending directly
|
|
||||||
from the `Controller` class.
|
|
||||||
|
|
||||||
## Recipes
|
|
||||||
|
|
||||||
It can be useful to add the variable `$SilverStripeNavigator` somewhere into the template, since it allows you to put a mini "admin" bar on the page which isn't visible to non editors. It shows the current stage and provides a convenient CMS link and version changing link.
|
|
||||||
|
|
||||||
Keep in mind that `$SilverStripeNavigator` is only available on ContentController, so useful when the Versioned extension is applied to SiteTree (default), and not when it's applied to DataObject.
|
|
||||||
|
|
||||||
### Trapping the publication event
|
|
||||||
|
|
||||||
Sometimes, you'll want to do something whenever a particular kind of page is published. This example sends an email
|
|
||||||
whenever a blog entry has been published.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Page extends SiteTree {
|
|
||||||
// ...
|
|
||||||
public function onAfterPublish() {
|
|
||||||
mail("sam@silverstripe.com", "Blog published", "The blog has been published");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +1,48 @@
|
|||||||
* stub, talk validate()
|
title: Model Validation and Constraints
|
||||||
|
summary: Validate your data at the model level
|
||||||
|
|
||||||
|
# Validation and Constraints
|
||||||
|
|
||||||
|
Traditionally, validation in SilverStripe has been mostly handled on the controller through [form validation](../forms).
|
||||||
|
|
||||||
|
While this is a useful approach, it can lead to data inconsistencies if the record is modified outside of the
|
||||||
|
controller and form context.
|
||||||
|
|
||||||
|
Most validation constraints are actually data constraints which belong on the model. SilverStripe provides the
|
||||||
|
[api:DataObject->validate] method for this purpose.
|
||||||
|
|
||||||
|
By default, there is no validation - objects are always valid! However, you can overload this method in your DataObject
|
||||||
|
sub-classes to specify custom validation, or use the `validate` hook through a [api:DataExtension].
|
||||||
|
|
||||||
|
Invalid objects won't be able to be written - a [api:ValidationException] will be thrown and no write will occur.
|
||||||
|
|
||||||
|
It is expected that you call `validate()` in your own application to test that an object is valid before attempting a
|
||||||
|
write, and respond appropriately if it isn't.
|
||||||
|
|
||||||
|
The return value of `validate()` is a [api:ValidationResult] object.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class MyObject extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Country' => 'Varchar',
|
||||||
|
'Postcode' => 'Varchar'
|
||||||
|
);
|
||||||
|
|
||||||
|
public function validate() {
|
||||||
|
$result = parent::validate();
|
||||||
|
|
||||||
|
if($this->Country == 'DE' && $this->Postcode && strlen($this->Postcode) != 5) {
|
||||||
|
$result->error('Need five digits for German postcodes');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
* [api:DataObject]
|
||||||
|
* [api:ValidationResult];
|
||||||
|
169
docs/en/02_Developer_Guides/00_Model/10_Versioning.md
Normal file
169
docs/en/02_Developer_Guides/00_Model/10_Versioning.md
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
title: Versioning
|
||||||
|
summary: Add versioning to your database content through the Versioned extension.
|
||||||
|
|
||||||
|
# Versioning
|
||||||
|
|
||||||
|
Database content in SilverStripe can be "staged" before its publication, as well as tracking all changes through the
|
||||||
|
lifetime of a database record.
|
||||||
|
|
||||||
|
It is most commonly applied to pages in the CMS (the `SiteTree` class). Draft content edited in the CMS can be different
|
||||||
|
from published content shown to your website visitors.
|
||||||
|
|
||||||
|
Versioning in SilverStripe is handled through the [api:Versioned] class. As a [api:DataExtension] it is possible to
|
||||||
|
be applied to any `[api:DataObject]` subclass. The extension class will automatically update read and write operations
|
||||||
|
done via the ORM via the `augmentSQL` database hook.
|
||||||
|
|
||||||
|
Adding Versioned to your `DataObject` subclass works the same as any other extension. It accepts two or more arguments
|
||||||
|
denoting the different "stages", which map to different database tables.
|
||||||
|
|
||||||
|
**mysite/_config/app.yml**
|
||||||
|
:::yml
|
||||||
|
MyRecord:
|
||||||
|
extensions:
|
||||||
|
- Versioned("Stage","Live")
|
||||||
|
|
||||||
|
<div class="notice" markdown="1">
|
||||||
|
The extension is automatically applied to `SiteTree` class. For more information on extensions see
|
||||||
|
[Extending](../extending) and the [Configuration](../configuration) documentation.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Database Structure
|
||||||
|
|
||||||
|
Depending on how many stages you configured, two or more new tables will be created for your records. In the above, this
|
||||||
|
will create a new `MyClass_Live` table once you've rebuilt the database.
|
||||||
|
|
||||||
|
<div class="notice" markdown="1">
|
||||||
|
Note that the "Stage" naming has a special meaning here, it will leave the original table name unchanged, rather than
|
||||||
|
adding a suffix.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
* `MyRecord` table: Contains staged data
|
||||||
|
* `MyRecord_Live` table: Contains live data
|
||||||
|
* `MyRecord_versions` table: Contains a version history (new record created on each save)
|
||||||
|
|
||||||
|
Similarly, any subclass you create on top of a versioned base will trigger the creation of additional tables, which are
|
||||||
|
automatically joined as required:
|
||||||
|
|
||||||
|
* `MyRecordSubclass` table: Contains only staged data for subclass columns
|
||||||
|
* `MyRecordSubclass_Live` table: Contains only live data for subclass columns
|
||||||
|
* `MyRecordSubclass_versions` table: Contains only version history for subclass columns
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Reading Versions
|
||||||
|
|
||||||
|
By default, all records are retrieved from the "Draft" stage (so the `MyRecord` table in our example). You can
|
||||||
|
explicitly request a certain stage through various getters on the `Versioned` class.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
// Fetching multiple records
|
||||||
|
$stageRecords = Versioned::get_by_stage('MyRecord', 'Stage');
|
||||||
|
$liveRecords = Versioned::get_by_stage('MyRecord', 'Live');
|
||||||
|
|
||||||
|
// Fetching a single record
|
||||||
|
$stageRecord = Versioned::get_by_stage('MyRecord', 'Stage')->byID(99);
|
||||||
|
$liveRecord = Versioned::get_by_stage('MyRecord', 'Live')->byID(99);
|
||||||
|
|
||||||
|
### Historical Versions
|
||||||
|
|
||||||
|
The above commands will just retrieve the latest version of its respective stage for you, but not older versions stored
|
||||||
|
in the `<class>_versions` tables.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$historicalRecord = Versioned::get_version('MyRecord', <record-id>, <version-id>);
|
||||||
|
|
||||||
|
<div class="alert" markdown="1">
|
||||||
|
The record is retrieved as a `DataObject`, but saving back modifications via `write()` will create a new version,
|
||||||
|
rather than modifying the existing one.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
In order to get a list of all versions for a specific record, we need to generate specialized `[api:Versioned_Version]`
|
||||||
|
objects, which expose the same database information as a `DataObject`, but also include information about when and how
|
||||||
|
a record was published.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
||||||
|
$versions = $record->allVersions();
|
||||||
|
echo $versions->First()->Version; // instance of Versioned_Version
|
||||||
|
|
||||||
|
### Writing Versions and Changing Stages
|
||||||
|
|
||||||
|
The usual call to `DataObject->write()` will write to whatever stage is currently active, as defined by the
|
||||||
|
`Versioned::current_stage()` global setting. Each call will automatically create a new version in the
|
||||||
|
`<class>_versions` table. To avoid this, use `[writeWithoutVersion()](api:Versioned->writeWithoutVersion())` instead.
|
||||||
|
|
||||||
|
To move a saved version from one stage to another, call `[writeToStage(<stage>)](api:Versioned->writeToStage())` on the
|
||||||
|
object. The process of moving a version to a different stage is also called "publishing", so we've created a shortcut
|
||||||
|
for this: `publish(<from-stage>, <to-stage>)`.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$record = Versioned::get_by_stage('MyRecord', 'Stage')->byID(99);
|
||||||
|
$record->MyField = 'changed';
|
||||||
|
// will update `MyRecord` table (assuming Versioned::current_stage() == 'Stage'),
|
||||||
|
// and write a row to `MyRecord_versions`.
|
||||||
|
$record->write();
|
||||||
|
// will copy the saved record information to the `MyRecord_Live` table
|
||||||
|
$record->publish('Stage', 'Live');
|
||||||
|
|
||||||
|
Similarly, an "unpublish" operation does the reverse, and removes a record from a specific stage.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
||||||
|
// will remove the row from the `MyRecord_Live` table
|
||||||
|
$record->deleteFromStage('Live');
|
||||||
|
|
||||||
|
### Forcing the Current Stage
|
||||||
|
|
||||||
|
The current stage is stored as global state on the object. It is usually modified by controllers, e.g. when a preview
|
||||||
|
is initialized. But it can also be set and reset temporarily to force a specific operation to run on a certain stage.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$origMode = Versioned::get_reading_mode(); // save current mode
|
||||||
|
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Live' records
|
||||||
|
Versioned::set_reading_mode('Stage'); // temporarily overwrite mode
|
||||||
|
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
|
||||||
|
Versioned::set_reading_mode($origMode); // reset current mode
|
||||||
|
|
||||||
|
### Custom SQL
|
||||||
|
|
||||||
|
We generally discourage writing `Versioned` queries from scratch, due to the complexities involved through joining
|
||||||
|
multiple tables across an inherited table scheme (see `[api:Versioned->augmentSQL()]`). If possible, try to stick to
|
||||||
|
smaller modifications of the generated `DataList` objects.
|
||||||
|
|
||||||
|
Example: Get the first 10 live records, filtered by creation date:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
$records = Versioned::get_by_stage('MyRecord', 'Live')->limit(10)->sort('Created', 'ASC');
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
|
||||||
|
The `Versioned` extension doesn't provide any permissions on its own, but you can have a look at the `SiteTree` class
|
||||||
|
for implementation samples, specifically `canPublish()` and `canDeleteFromStage()`.
|
||||||
|
|
||||||
|
### Page Specific Operations
|
||||||
|
|
||||||
|
Since the `Versioned` extension is primarily used for page objects, the underlying `SiteTree` class has some additional
|
||||||
|
helpers.
|
||||||
|
|
||||||
|
### Templates Variables
|
||||||
|
|
||||||
|
In templates, you don't need to worry about this distinction. The `$Content` variable contain the published content by
|
||||||
|
default, and only preview draft content if explicitly requested (e.g. by the "preview" feature in the CMS). If you want
|
||||||
|
to force a specific stage, we recommend the `Controller->init()` method for this purpose.
|
||||||
|
|
||||||
|
### Controllers
|
||||||
|
|
||||||
|
The current stage for each request is determined by `VersionedRequestFilter` before any controllers initialize, through
|
||||||
|
`Versioned::choose_site_stage()`. It checks for a `Stage` GET parameter, so you can force a draft stage by appending
|
||||||
|
`?stage=Stage` to your request. The setting is "sticky" in the PHP session, so any subsequent requests will also be in
|
||||||
|
draft stage.
|
||||||
|
|
||||||
|
<div class="alert" markdown="1">
|
||||||
|
The `choose_site_stage()` call only deals with setting the default stage, and doesn't check if the user is
|
||||||
|
authenticated to view it. As with any other controller logic, please use `DataObject->canView()` to determine
|
||||||
|
permissions, and avoid exposing unpublished content to your users.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
* [api:Versioned]
|
208
docs/en/02_Developer_Guides/00_Model/11_Scaffolding.md
Normal file
208
docs/en/02_Developer_Guides/00_Model/11_Scaffolding.md
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
title: Building Model and Search Interfaces around Scaffolding
|
||||||
|
summary: Model Driven approach to defining your application UI.
|
||||||
|
|
||||||
|
# Scaffolding
|
||||||
|
|
||||||
|
The ORM already has a lot of information about the data represented by a `DataObject` through its `$db` property, so
|
||||||
|
SilverStripe will use that information to provide scaffold some interfaces. This is done though [api:FormScaffolder]
|
||||||
|
to provide reasonable defaults based on the property type (e.g. a checkbox field for booleans). You can then further
|
||||||
|
customize those fields as required.
|
||||||
|
|
||||||
|
## Form Fields
|
||||||
|
|
||||||
|
An example is `DataObject`, SilverStripe will automatically create your CMS interface so you can modify what you need.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class MyDataObject extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'IsActive' => 'Boolean',
|
||||||
|
'Title' => 'Varchar',
|
||||||
|
'Content' => 'Text'
|
||||||
|
);
|
||||||
|
|
||||||
|
public function getCMSFields() {
|
||||||
|
// parent::getCMSFields() does all the hard work and creates the fields for title, isactive and content.
|
||||||
|
$fields = parent::getCMSFields();
|
||||||
|
$fields->fieldByName('IsActive')->setTitle('Is active?');
|
||||||
|
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
You can also alter the fields of built-in and module `DataObject` classes through your own
|
||||||
|
[DataExtension](../extensions), and a call to `DataExtension->updateCMSFields`.
|
||||||
|
|
||||||
|
## Searchable Fields
|
||||||
|
|
||||||
|
The `$searchable_fields` property uses a mixed array format that can be used to further customize your generated admin
|
||||||
|
system. The default is a set of array values listing the fields.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class MyDataObject extends DataObject {
|
||||||
|
|
||||||
|
private static $searchable_fields = array(
|
||||||
|
'Name',
|
||||||
|
'ProductCode'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Searchable fields will be appear in the search interface with a default form field (usually a [api:TextField]) and a
|
||||||
|
default search filter assigned (usually an [api:ExactMatchFilter]). To override these defaults, you can specify
|
||||||
|
additional information on `$searchable_fields`:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class MyDataObject extends DataObject {
|
||||||
|
|
||||||
|
private static $searchable_fields = array(
|
||||||
|
'Name' => 'PartialMatchFilter',
|
||||||
|
'ProductCode' => 'NumericField'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
If you assign a single string value, you can set it to be either a [api:FormField] or [api:SearchFilter]. To specify
|
||||||
|
both, you can assign an array:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class MyDataObject extends DataObject {
|
||||||
|
|
||||||
|
private static $searchable_fields = array(
|
||||||
|
'Name' => array(
|
||||||
|
'field' => 'TextField',
|
||||||
|
'filter' => 'PartialMatchFilter',
|
||||||
|
),
|
||||||
|
'ProductCode' => array(
|
||||||
|
'title' => 'Product code #',
|
||||||
|
'field' => 'NumericField',
|
||||||
|
'filter' => 'PartialMatchFilter',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
To include relations (`$has_one`, `$has_many` and `$many_many`) in your search, you can use a dot-notation.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Team extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Title' => 'Varchar'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $many_many = array(
|
||||||
|
'Players' => 'Player'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $searchable_fields = array(
|
||||||
|
'Title',
|
||||||
|
'Players.Name',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Name' => 'Varchar',
|
||||||
|
'Birthday' => 'Date'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $belongs_many_many = array(
|
||||||
|
'Teams' => 'Team'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### Summary Fields
|
||||||
|
|
||||||
|
Summary fields can be used to show a quick overview of the data for a specific [api:DataObject] record. Most common use
|
||||||
|
is their display as table columns, e.g. in the search results of a `[api:ModelAdmin]` CMS interface.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class MyDataObject extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Name' => 'Text',
|
||||||
|
'OtherProperty' => 'Text',
|
||||||
|
'ProductCode' => 'Int',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $summary_fields = array(
|
||||||
|
'Name',
|
||||||
|
'ProductCode'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
To include relations or field manipulations in your summaries, you can use a dot-notation.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class OtherObject extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Title' => 'Varchar'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyDataObject extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Name' => 'Text',
|
||||||
|
'Description' => 'HTMLText'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
'OtherObject' => 'OtherObject'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $summary_fields = array(
|
||||||
|
'Name' => 'Name',
|
||||||
|
'Description.Summary' => 'Description (summary)',
|
||||||
|
'OtherObject.Title' => 'Other Object Title'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Non-textual elements (such as images and their manipulations) can also be used in summaries.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class MyDataObject extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Name' => 'Text'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
'HeroImage' => 'Image'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $summary_fields = array(
|
||||||
|
'Name' => 'Name',
|
||||||
|
'HeroImage.CMSThumbnail' => 'Hero Image'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
## Related Documenation
|
||||||
|
|
||||||
|
* [SearchFilters](searchfilters)
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
* [api:FormScaffolder]
|
||||||
|
* [api:DataObject]
|
55
docs/en/02_Developer_Guides/00_Model/12_Indexes.md
Normal file
55
docs/en/02_Developer_Guides/00_Model/12_Indexes.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
title: Indexes
|
||||||
|
summary: Add Indexes to your Data Model to optimize database queries.
|
||||||
|
|
||||||
|
# Indexes
|
||||||
|
|
||||||
|
It is sometimes desirable to add indexes to your data model, whether to optimize queries or add a uniqueness constraint
|
||||||
|
to a field. This is done through the `DataObject::$indexes` map, which maps index names to descriptor arrays that
|
||||||
|
represent each index. There's several supported notations:
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class MyObject extends DataObject {
|
||||||
|
|
||||||
|
private static $indexes = array(
|
||||||
|
'<column-name>' => true,
|
||||||
|
'<index-name>' => array('type' => '<type>', 'value' => '"<column-name>"'),
|
||||||
|
'<index-name>' => 'unique("<column-name>")'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
The `<index-name>` can be an an arbitrary identifier in order to allow for more than one index on a specific database
|
||||||
|
column. The "advanced" notation supports more `<type>` notations. These vary between database drivers, but all of them
|
||||||
|
support the following:
|
||||||
|
|
||||||
|
* `index`: Standard index
|
||||||
|
* `unique`: Index plus uniqueness constraint on the value
|
||||||
|
* `fulltext`: Fulltext content index
|
||||||
|
|
||||||
|
In order to use more database specific or complex index notations, we also support raw SQL for as a value in the
|
||||||
|
`$indexes` definition. Keep in mind this will likely make your code less portable between databases.
|
||||||
|
|
||||||
|
**mysite/code/MyTestObject.php**
|
||||||
|
|
||||||
|
:::php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class MyTestObject extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'MyField' => 'Varchar',
|
||||||
|
'MyOtherField' => 'Varchar',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $indexes = array(
|
||||||
|
'MyIndexName' => array(
|
||||||
|
'type' => 'index',
|
||||||
|
'value' => '"MyField","MyOtherField"'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
* [api:DataObject]
|
@ -1,8 +1,13 @@
|
|||||||
title: Model and Databases
|
title: Model and Databases
|
||||||
summary: Learn how SilverStripe manages database tables, ways to query your database and how to publish data.
|
summary: Learn how SilverStripe manages database tables, ways to query your database and how to publish data.
|
||||||
|
introduction: This guide will cover how to create and manipulate data within SilverStripe and how to use the ORM (Object Relational Model) to query data.
|
||||||
|
|
||||||
[CHILDREN]
|
In SilverStripe, application data will be represented by a [api:DataObject] class. A `DataObject` subclass defines the
|
||||||
|
data columns, relationships and properties of a particular data record. For example, [api:Member] is a `DataObject`
|
||||||
|
which stores information about a person, CMS user or mail subscriber.
|
||||||
|
|
||||||
## How-to
|
[CHILDREN Exclude="How_tos"]
|
||||||
|
|
||||||
[CHILDREN How_To]
|
## How to's
|
||||||
|
|
||||||
|
[CHILDREN Folder="How_Tos"]
|
@ -1,426 +0,0 @@
|
|||||||
title: UploadField
|
|
||||||
summary: How to use the UploadField class for uploading assets.
|
|
||||||
|
|
||||||
# UploadField
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
The UploadField will let you upload one or multiple files of all types,
|
|
||||||
including images. But that's not all it does - it will also link the
|
|
||||||
uploaded file(s) to an existing relation and let you edit the linked files
|
|
||||||
as well. That makes it flexible enough to sometimes even replace the Gridfield,
|
|
||||||
like for instance in creating and managing a simple gallery.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
The field can be used in three ways: To upload a single file into a `has_one` relationship,
|
|
||||||
or allow multiple files into a `has_many` or `many_many` relationship, or to act as a stand
|
|
||||||
alone uploader into a folder with no underlying relation.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
Although images are uploaded and stored on the filesystem immediately after selection,
|
|
||||||
the value (or values) of this field will not be written to any related record until
|
|
||||||
the record is saved and successfully validated. However, any invalid records will still
|
|
||||||
persist across form submissions until explicitly removed or replaced by the user.
|
|
||||||
|
|
||||||
Care should be taken as invalid files may remain within the filesystem until explicitly
|
|
||||||
removed.
|
|
||||||
|
|
||||||
### Single fileupload
|
|
||||||
|
|
||||||
The following example adds an UploadField to a page for single fileupload,
|
|
||||||
based on a has_one relation:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class GalleryPage extends Page {
|
|
||||||
|
|
||||||
private static $has_one = array(
|
|
||||||
'SingleImage' => 'Image'
|
|
||||||
);
|
|
||||||
|
|
||||||
function getCMSFields() {
|
|
||||||
|
|
||||||
$fields = parent::getCMSFields();
|
|
||||||
|
|
||||||
$fields->addFieldToTab(
|
|
||||||
'Root.Upload',
|
|
||||||
$uploadField = new UploadField(
|
|
||||||
$name = 'SingleImage',
|
|
||||||
$title = 'Upload a single image'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
The UploadField will autodetect the relation based on it's `name` property, and
|
|
||||||
save it into the GalleyPages' `SingleImageID` field. Setting the
|
|
||||||
`setAllowedMaxFileNumber` to 1 will make sure that only one image can ever be
|
|
||||||
uploaded and linked to the relation.
|
|
||||||
|
|
||||||
### Multiple fileupload
|
|
||||||
|
|
||||||
Enable multiple fileuploads by using a many_many (or has_many) relation. Again,
|
|
||||||
the `UploadField` will detect the relation based on its $name property value:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class GalleryPage extends Page {
|
|
||||||
|
|
||||||
private static $many_many = array(
|
|
||||||
'GalleryImages' => 'Image'
|
|
||||||
);
|
|
||||||
|
|
||||||
function getCMSFields() {
|
|
||||||
|
|
||||||
$fields = parent::getCMSFields();
|
|
||||||
|
|
||||||
$fields->addFieldToTab(
|
|
||||||
'Root.Upload',
|
|
||||||
$uploadField = new UploadField(
|
|
||||||
$name = 'GalleryImages',
|
|
||||||
$title = 'Upload one or more images (max 10 in total)'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$uploadField->setAllowedMaxFileNumber(10);
|
|
||||||
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class GalleryPage_Controller extends Page_Controller {
|
|
||||||
}
|
|
||||||
|
|
||||||
class GalleryImageExtension extends DataExtension {
|
|
||||||
private static $belongs_many_many = array('Galleries' => 'GalleryPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
Image::add_extension('GalleryImageExtension');
|
|
||||||
|
|
||||||
<div class="notice" markdown='1'>
|
|
||||||
In order to link both ends of the relationship together it's usually advisable to
|
|
||||||
extend Image with the necessary $has_one, $belongs_to, $has_many or $belongs_many_many.
|
|
||||||
In particular, a DataObject with $has_many Images will not work without this specified explicitly.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Overview
|
|
||||||
|
|
||||||
The field can either be configured on an instance level with the various
|
|
||||||
getProperty and setProperty functions, or globally by overriding the YAML defaults.
|
|
||||||
See the [Configuration Reference](uploadfield#configuration-reference) section for possible values.
|
|
||||||
|
|
||||||
Example: mysite/_config/uploadfield.yml
|
|
||||||
|
|
||||||
after: framework#uploadfield
|
|
||||||
---
|
|
||||||
UploadField:
|
|
||||||
defaultConfig:
|
|
||||||
canUpload: false
|
|
||||||
|
|
||||||
### Set a custom folder
|
|
||||||
|
|
||||||
This example will save all uploads in the `/assets/customfolder/` folder. If
|
|
||||||
the folder doesn't exist, it will be created.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$fields->addFieldToTab(
|
|
||||||
'Root.Upload',
|
|
||||||
$uploadField = new UploadField(
|
|
||||||
$name = 'GalleryImages',
|
|
||||||
$title = 'Please upload one or more images' )
|
|
||||||
);
|
|
||||||
$uploadField->setFolderName('customfolder');
|
|
||||||
|
|
||||||
### Limit the allowed filetypes
|
|
||||||
|
|
||||||
`AllowedExtensions` defaults to the `File.allowed_extensions` configuration setting,
|
|
||||||
but can be overwritten for each UploadField:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$uploadField->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif'));
|
|
||||||
|
|
||||||
Entire groups of file extensions can be specified in order to quickly limit types
|
|
||||||
to known file categories.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// This will limit files to the following extensions:
|
|
||||||
// "bmp" ,"gif" ,"jpg" ,"jpeg" ,"pcx" ,"tif" ,"png" ,"alpha","als" ,"cel" ,"icon" ,"ico" ,"ps"
|
|
||||||
// 'doc','docx','txt','rtf','xls','xlsx','pages', 'ppt','pptx','pps','csv','pdf'
|
|
||||||
$uploadField->setAllowedFileCategories('image', 'doc');
|
|
||||||
|
|
||||||
`AllowedExtensions` can also be set globally via the [YAML configuration](/topics/configuration#setting-configuration-via-yaml-files), for example you may add the following into your mysite/_config/config.yml:
|
|
||||||
|
|
||||||
:::yaml
|
|
||||||
File:
|
|
||||||
allowed_extensions:
|
|
||||||
- 7zip
|
|
||||||
- xzip
|
|
||||||
|
|
||||||
<div class="notice" markdown='1'>
|
|
||||||
Note: File types such as SWF, XML and HTML are excluded by default from uploading as these types are common
|
|
||||||
security attack risks. If necessary, these types may be allowed as uploads (at your own risk) by adding each
|
|
||||||
extension to the `File.allowed_extensions` config or setting `File.apply_restrictions_to_admin` to false.
|
|
||||||
See [the security topic](/topics/security#user-uploaded-files) for more information.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Limit the maximum file size
|
|
||||||
|
|
||||||
`AllowedMaxFileSize` is by default set to the lower value of the 2 php.ini configurations: `upload_max_filesize` and `post_max_size`
|
|
||||||
The value is set as bytes.
|
|
||||||
|
|
||||||
NOTE: this only sets the configuration for your UploadField, this does NOT change your server upload settings, so if your server is set to only allow 1 MB and you set the UploadFIeld to 2 MB, uploads will not work.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$sizeMB = 2; // 2 MB
|
|
||||||
$size = $sizeMB * 1024 * 1024; // 2 MB in bytes
|
|
||||||
$this->getValidator()->setAllowedMaxFileSize($size);
|
|
||||||
|
|
||||||
### Overwrite warning
|
|
||||||
|
|
||||||
In order to display a warning before overwriting an existing file, `Upload:replaceFile` must be set to true.
|
|
||||||
|
|
||||||
Via config:
|
|
||||||
|
|
||||||
:::yaml
|
|
||||||
Upload:
|
|
||||||
# Replace an existing file rather than renaming the new one.
|
|
||||||
replaceFile: true
|
|
||||||
UploadField:
|
|
||||||
# Warning before overwriting existing file (only relevant when Upload: replaceFile is true)
|
|
||||||
overwriteWarning: true
|
|
||||||
|
|
||||||
Or per instance:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$uploadField->getUpload()->setReplaceFile(true);
|
|
||||||
$uploadField->setOverwriteWarning(true);
|
|
||||||
|
|
||||||
### Preview dimensions
|
|
||||||
|
|
||||||
Set the dimensions of the image preview. By default the max width is set to 80
|
|
||||||
and the max height is set to 60.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$uploadField->setPreviewMaxWidth(100);
|
|
||||||
$uploadField->setPreviewMaxHeight(100);
|
|
||||||
|
|
||||||
### Disable attachment of existing files
|
|
||||||
|
|
||||||
This can force the user to upload a new file, rather than link to the already
|
|
||||||
existing file librarry
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$uploadField->setCanAttachExisting(false);
|
|
||||||
|
|
||||||
### Disable uploading of new files
|
|
||||||
|
|
||||||
Alternatively, you can force the user to only specify already existing files
|
|
||||||
in the file library
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$uploadField->setCanUpload(false);
|
|
||||||
|
|
||||||
### Automatic or manual upload
|
|
||||||
|
|
||||||
By default, the UploadField will try to automatically upload all selected files.
|
|
||||||
Setting the `autoUpload` property to false, will present you with a list of
|
|
||||||
selected files that you can then upload manually one by one:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$uploadField->setAutoUpload(false);
|
|
||||||
|
|
||||||
### Change Detection
|
|
||||||
|
|
||||||
The CMS interface will automatically notify the form containing
|
|
||||||
an UploadField instance of changes, such as a new upload,
|
|
||||||
or the removal of an existing upload (through a `dirty` event).
|
|
||||||
The UI can then choose an appropriate response (e.g. highlighting the "save" button).
|
|
||||||
If the UploadField doesn't save into a relation, there's
|
|
||||||
technically no saveable change (the upload has already happened),
|
|
||||||
which is why this feature can be disabled on demand.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$uploadField->setConfig('changeDetection', false);
|
|
||||||
|
|
||||||
### Build a simple gallery
|
|
||||||
|
|
||||||
A gallery most times needs more then simple images. You might want to add a
|
|
||||||
description, or maybe some settings to define a transition effect for each slide.
|
|
||||||
First create a
|
|
||||||
[DataExtension](http://doc.silverstripe.org/framework/en/reference/dataextension)
|
|
||||||
like this:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class GalleryImage extends DataExtension {
|
|
||||||
|
|
||||||
private static $db = array(
|
|
||||||
'Description' => 'Text'
|
|
||||||
);
|
|
||||||
|
|
||||||
private static $belongs_many_many = array(
|
|
||||||
'GalleryPage' => 'GalleryPage'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Now register the DataExtension for the Image class in your _config/config.yml:
|
|
||||||
|
|
||||||
Image:
|
|
||||||
extensions:
|
|
||||||
- GalleryImage
|
|
||||||
|
|
||||||
<div class="notice" markdown='1'>
|
|
||||||
Note: Although you can subclass the Image class instead of using a DataExtension,
|
|
||||||
this is not advisable. For instance: when using a subclass, the 'From files'
|
|
||||||
button will only return files that were uploaded for that subclass, it won't
|
|
||||||
recognize any other images!
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Edit uploaded images
|
|
||||||
|
|
||||||
By default the UploadField will let you edit the following fields: *Title,
|
|
||||||
Filename, Owner and Folder*. The `fileEditFields` configuration setting allows
|
|
||||||
you you alter these settings. One way to go about this is create a
|
|
||||||
`getCustomFields` function in your GalleryImage object like this:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class GalleryImage extends DataExtension {
|
|
||||||
...
|
|
||||||
|
|
||||||
function getCustomFields() {
|
|
||||||
$fields = new FieldList();
|
|
||||||
$fields->push(new TextField('Title', 'Title'));
|
|
||||||
$fields->push(new TextareaField('Description', 'Description'));
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Then, in your GalleryPage, tell the UploadField to use this function:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
$uploadField->setFileEditFields('getCustomFields');
|
|
||||||
|
|
||||||
In a similar fashion you can use 'setFileEditActions' to set the actions for the
|
|
||||||
editform, or 'fileEditValidator' to determine the validator (eg RequiredFields).
|
|
||||||
|
|
||||||
### Configuration Reference
|
|
||||||
|
|
||||||
- `setAllowedMaxFileNumber`: (int) php validation of allowedMaxFileNumber
|
|
||||||
only works when a db relation is available, set to null to allow
|
|
||||||
unlimited if record has a has_one and allowedMaxFileNumber is null, it will be set to 1
|
|
||||||
- `setAllowedFileExtensions`: (array) List of file extensions allowed
|
|
||||||
- `setAllowedFileCategories`: (array|string) List of types of files allowed.
|
|
||||||
May be any of 'image', 'audio', 'mov', 'zip', 'flash', or 'doc'
|
|
||||||
- `setAutoUpload`: (boolean) Should the field automatically trigger an upload once
|
|
||||||
a file is selected?
|
|
||||||
- `setCanAttachExisting`: (boolean|string) Can the user attach existing files from the library.
|
|
||||||
String values are interpreted as permission codes.
|
|
||||||
- `setCanPreviewFolder`: (boolean|string) Can the user preview the folder files will be saved into?
|
|
||||||
String values are interpreted as permission codes.
|
|
||||||
- `setCanUpload`: (boolean|string) Can the user upload new files, or just select from existing files.
|
|
||||||
String values are interpreted as permission codes.
|
|
||||||
- `setDownloadTemplateName`: (string) javascript template used to display already
|
|
||||||
uploaded files, see javascript/UploadField_downloadtemplate.js
|
|
||||||
- `setFileEditFields`: (FieldList|string) FieldList $fields or string $name
|
|
||||||
(of a method on File to provide a fields) for the EditForm (Example: 'getCMSFields')
|
|
||||||
- `setFileEditActions`: (FieldList|string) FieldList $actions or string $name
|
|
||||||
(of a method on File to provide a actions) for the EditForm (Example: 'getCMSActions')
|
|
||||||
- `setFileEditValidator`: (string) Validator (eg RequiredFields) or string $name
|
|
||||||
(of a method on File to provide a Validator) for the EditForm (Example: 'getCMSValidator')
|
|
||||||
- `setOverwriteWarning`: (boolean) Show a warning when overwriting a file.
|
|
||||||
- `setPreviewMaxWidth`: (int)
|
|
||||||
- `setPreviewMaxHeight`: (int)
|
|
||||||
- `setTemplateFileButtons`: (string) Template name to use for the file buttons
|
|
||||||
- `setTemplateFileEdit`: (string) Template name to use for the file edit form
|
|
||||||
- `setUploadTemplateName`: (string) javascript template used to display uploading
|
|
||||||
files, see javascript/UploadField_uploadtemplate.js
|
|
||||||
- `setCanPreviewFolder`: (boolean|string) Is the upload folder visible to uploading users?
|
|
||||||
String values are interpreted as permission codes.
|
|
||||||
|
|
||||||
Certain default values for the above can be configured using the YAML config system.
|
|
||||||
|
|
||||||
:::yaml
|
|
||||||
UploadField:
|
|
||||||
defaultConfig:
|
|
||||||
autoUpload: true
|
|
||||||
allowedMaxFileNumber:
|
|
||||||
canUpload: true
|
|
||||||
canAttachExisting: 'CMS_ACCESS_AssetAdmin'
|
|
||||||
canPreviewFolder: true
|
|
||||||
previewMaxWidth: 80
|
|
||||||
previewMaxHeight: 60
|
|
||||||
uploadTemplateName: 'ss-uploadfield-uploadtemplate'
|
|
||||||
downloadTemplateName: 'ss-uploadfield-downloadtemplate'
|
|
||||||
overwriteWarning: true # Warning before overwriting existing file (only relevant when Upload: replaceFile is true)
|
|
||||||
|
|
||||||
The above settings can also be set on a per-instance basis by using `setConfig` with the appropriate key.
|
|
||||||
|
|
||||||
You can also configure the underlying `[api:Upload]` class, by using the YAML config system.
|
|
||||||
|
|
||||||
:::yaml
|
|
||||||
Upload:
|
|
||||||
# Globally disables automatic renaming of files and displays a warning before overwriting an existing file
|
|
||||||
replaceFile: true
|
|
||||||
uploads_folder: 'Uploads'
|
|
||||||
|
|
||||||
## Using the UploadField in a frontend form
|
|
||||||
|
|
||||||
The UploadField can be used in a frontend form, given that sufficient attention is given
|
|
||||||
to the permissions granted to non-authorised users.
|
|
||||||
|
|
||||||
By default Image::canDelete and Image::canEdit do not require admin privileges, so
|
|
||||||
make sure you override the methods in your Image extension class.
|
|
||||||
|
|
||||||
For instance, to generate an upload form suitable for saving images into a user-defined
|
|
||||||
gallery the below code could be used:
|
|
||||||
|
|
||||||
:::php
|
|
||||||
|
|
||||||
// In GalleryPage.php
|
|
||||||
class GalleryPage extends Page {}
|
|
||||||
class GalleryPage_Controller extends Page_Controller {
|
|
||||||
private static $allowed_actions = array('Form');
|
|
||||||
public function Form() {
|
|
||||||
$fields = new FieldList(
|
|
||||||
new TextField('Title', 'Title', null, 255),
|
|
||||||
$field = new UploadField('Images', 'Upload Images')
|
|
||||||
);
|
|
||||||
$field->setCanAttachExisting(false); // Block access to Silverstripe assets library
|
|
||||||
$field->setCanPreviewFolder(false); // Don't show target filesystem folder on upload field
|
|
||||||
$field->relationAutoSetting = false; // Prevents the form thinking the GalleryPage is the underlying object
|
|
||||||
$actions = new FieldList(new FormAction('submit', 'Save Images'));
|
|
||||||
return new Form($this, 'Form', $fields, $actions, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function submit($data, Form $form) {
|
|
||||||
$gallery = new Gallery();
|
|
||||||
$form->saveInto($gallery);
|
|
||||||
$gallery->write();
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In Gallery.php
|
|
||||||
class Gallery extends DataObject {
|
|
||||||
private static $db = array(
|
|
||||||
'Title' => 'Varchar(255)'
|
|
||||||
);
|
|
||||||
|
|
||||||
private static $many_many = array(
|
|
||||||
'Images' => 'Image'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// In ImageExtension.php
|
|
||||||
class ImageExtension extends DataExtension {
|
|
||||||
|
|
||||||
private static $belongs_many_many = array(
|
|
||||||
'Gallery' => 'Gallery'
|
|
||||||
);
|
|
||||||
|
|
||||||
function canEdit($member) {
|
|
||||||
// This part is important!
|
|
||||||
return Permission::check('ADMIN');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Image::add_extension('ImageExtension');
|
|
@ -1,70 +0,0 @@
|
|||||||
# Moving from SilverStripe 2 to SilverStripe 3
|
|
||||||
|
|
||||||
These are the main changes to the SiverStripe 3 template language.
|
|
||||||
|
|
||||||
## Control blocks: Loops vs. Scope
|
|
||||||
|
|
||||||
The `<% control $var %>...<% end_control %>` in SilverStripe prior to version 3 has two different meanings. Firstly, if the control variable is a collection (e.g. DataList), then `<% control %>` iterates over that set. If it's a non-iteratable object, however, `<% control %>` introduces a new scope, which is used to render the inner template code. This dual-use is confusing to some people, and doesn't allow a collection of objects to be used as a scope.
|
|
||||||
|
|
||||||
In SilverStripe 3, the first usage (iteration) is replaced by `<% loop $var %>`. The second usage (scoping) is replaced by `<% with $var %>`
|
|
||||||
|
|
||||||
## Literals in Expressions
|
|
||||||
|
|
||||||
Prior to SilverStripe 3, literal values can appear in certain parts of an expression. For example, in the expression `<% if mydinner=kipper %>`, `mydinner` is treated as a property or method on the page or controller, and `kipper` is treated as a literal. This is fairly limited in use.
|
|
||||||
|
|
||||||
Literals can now be quoted, so that both literals and non-literals can be used in contexts where only literals were allowed before. This makes it possible to write the following:
|
|
||||||
|
|
||||||
* `<% if $mydinner=="kipper" %>...` which compares to the literal "kipper"
|
|
||||||
* `<% if $mydinner==$yourdinner %>...` which compares to another property or method on the page called `yourdinner`
|
|
||||||
|
|
||||||
Certain forms that are currently used in SilverStripe 2.x are still supported in SilverStripe 3 for backwards compatibility:
|
|
||||||
|
|
||||||
* `<% if mydinner==yourdinner %>...` is still interpreted as `mydinner` being a property or method, and `yourdinner` being a literal. It is strongly recommended to change to the new syntax in new implementations. The 2.x syntax is likely to be deprecated in the future.
|
|
||||||
|
|
||||||
Similarly, in SilverStripe 2.x, method parameters are treated as literals: `MyMethod(foo)` is now equivalent to `$MyMethod("foo")`. `$MyMethod($foo)` passes a variable to the method, which is only supported in SilverStripe 3.
|
|
||||||
|
|
||||||
## Method Parameters
|
|
||||||
|
|
||||||
Methods can now take an arbitrary number of parameters:
|
|
||||||
|
|
||||||
$MyMethod($foo,"active", $CurrentMember.FirstName)
|
|
||||||
|
|
||||||
Parameter values can be arbitrary expressions, including a mix of literals, variables, and even other method calls.
|
|
||||||
|
|
||||||
## Less sensitivity around spaces
|
|
||||||
|
|
||||||
Within a tag, a single space is equivalent to multiple consequetive spaces. e.g.
|
|
||||||
|
|
||||||
<% if $Foo %>
|
|
||||||
|
|
||||||
is equivalent to
|
|
||||||
|
|
||||||
<% if $Foo %>
|
|
||||||
|
|
||||||
|
|
||||||
## Removed view-specific accessors
|
|
||||||
|
|
||||||
Several methods in ViewableData that were originally added to expose values to the template language were moved,
|
|
||||||
in order to stop polluting the namespace. These were sometimes called by project-specific PHP code too, and that code
|
|
||||||
will need re-working.
|
|
||||||
|
|
||||||
#### Globals
|
|
||||||
|
|
||||||
Some of these methods were wrappers which simply called other static methods. These can simply be replaced with a call
|
|
||||||
to the wrapped method. The list of these methods is:
|
|
||||||
|
|
||||||
- CurrentMember() -> Member::currentUser()
|
|
||||||
- getSecurityID() -> SecurityToken::inst()->getValue()
|
|
||||||
- HasPerm($code) -> Permission::check($code)
|
|
||||||
- BaseHref() -> Director::absoluteBaseURL()
|
|
||||||
- AbsoluteBaseURL() -> Director::absoluteBaseURL()
|
|
||||||
- IsAjax() -> Director::is_ajax()
|
|
||||||
- i18nLocale() -> i18n::get_locale()
|
|
||||||
- CurrentPage() -> Controller::curr()
|
|
||||||
|
|
||||||
#### Scope-exposing
|
|
||||||
|
|
||||||
Some of the removed methods exposed access to the various scopes. These currently have no replacement. The list of
|
|
||||||
these methods is:
|
|
||||||
|
|
||||||
- Top
|
|
@ -2,3 +2,51 @@ title: Upgrading
|
|||||||
introduction: Keep your SilverStripe installations up to date with the latest fixes, security patches and new features.
|
introduction: Keep your SilverStripe installations up to date with the latest fixes, security patches and new features.
|
||||||
|
|
||||||
# Upgrading
|
# Upgrading
|
||||||
|
|
||||||
|
SilverStripe Applications should be kept up to date with the latest security releases. Usually an update or upgrade to
|
||||||
|
your SilverStripe installation means overwriting files, flushing the cache and updating your database-schema.
|
||||||
|
|
||||||
|
<div class="info" markdown="1">
|
||||||
|
See our [upgrade notes and changelogs](/changelogs) for release-specific information.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Composer
|
||||||
|
|
||||||
|
For projects managed through Composer, check the version defined in your `composer.json` file. Update the version
|
||||||
|
constraints if required and update composer.
|
||||||
|
|
||||||
|
:::bash
|
||||||
|
composer update
|
||||||
|
|
||||||
|
## Manual
|
||||||
|
|
||||||
|
* Check if any modules (e.g. blog or forum) in your installation are incompatible and need to be upgraded as well
|
||||||
|
* Backup your database content
|
||||||
|
* Backup your webroot files
|
||||||
|
* Download the new release and uncompress it to a temporary folder
|
||||||
|
* Leave custom folders like *mysite* or *themes* in place.
|
||||||
|
* Identify system folders in your webroot (`cms`, `framework` and any additional modules).
|
||||||
|
* Delete existing system folders (or move them outside of your webroot)
|
||||||
|
* Extract and replace system folders from your download (Deleting instead of "copying over" existing folders ensures that files removed from the new SilverStripe release are not persisting in your installation)
|
||||||
|
* Visit http://yoursite.com/dev/build/?flush=1 to rebuild the website database.
|
||||||
|
* Check if you need to adapt your code to changed PHP APIs
|
||||||
|
* Check if you have overwritten any core templates or styles which might need an update.
|
||||||
|
|
||||||
|
<div class="warning" markdown="1">
|
||||||
|
Never update a website on the live server without trying it on a development copy first.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Decision Helpers
|
||||||
|
|
||||||
|
How easy will it be to update my project? It's a fair question, and sometimes a difficult one to answer.
|
||||||
|
|
||||||
|
* "Micro" releases (x.y.z) are explicitly backwards compatible, "minor" and "major" releases can deprecate features and change APIs (see our [/misc/release-process](release process) for details)
|
||||||
|
* If you've made custom branches of SilverStripe core, or any thirdparty module, it's going to be harder to upgrade.
|
||||||
|
* The more custom features you have, the harder it will be to upgrade. You will have to re-test all of those features, and adapt to API changes in core.
|
||||||
|
* Customizations of a well defined type - such as custom page types or custom blog widgets - are going to be easier to upgrade than customisations that modify deep system internals like rewriting SQL queries.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
* [Release Announcements](http://groups.google.com/group/silverstripe-announce/)
|
||||||
|
* [Blog posts about releases on silverstripe.org](http://silverstripe.org/blog/tag/release)
|
||||||
|
* [Release Process](../contributing/release_process)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user