mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
0b1f297873
Conflicts: .travis.yml README.md admin/code/LeftAndMain.php admin/css/screen.css admin/scss/screen.scss api/RestfulService.php conf/ConfigureFromEnv.php control/injector/ServiceConfigurationLocator.php control/injector/SilverStripeServiceConfigurationLocator.php core/ClassInfo.php core/Object.php css/AssetUploadField.css css/ComplexTableField_popup.css dev/CSSContentParser.php dev/DevelopmentAdmin.php docs/en/changelogs/index.md docs/en/misc/contributing/code.md docs/en/reference/execution-pipeline.md filesystem/GD.php filesystem/ImagickBackend.php filesystem/Upload.php forms/Form.php forms/FormField.php forms/HtmlEditorConfig.php forms/gridfield/GridFieldDetailForm.php forms/gridfield/GridFieldSortableHeader.php lang/en.yml model/Aggregate.php model/DataList.php model/DataObject.php model/DataQuery.php model/Image.php model/MySQLDatabase.php model/SQLQuery.php model/fieldtypes/HTMLText.php model/fieldtypes/Text.php scss/AssetUploadField.scss search/filters/SearchFilter.php security/Authenticator.php security/LoginForm.php security/Member.php security/MemberAuthenticator.php security/MemberLoginForm.php security/Security.php tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php tests/control/HTTPTest.php tests/control/RequestHandlingTest.php tests/filesystem/UploadTest.php tests/forms/FormTest.php tests/forms/NumericFieldTest.php tests/model/DataListTest.php tests/model/DataObjectTest.php tests/model/TextTest.php tests/security/MemberAuthenticatorTest.php tests/security/SecurityDefaultAdminTest.php tests/view/SSViewerCacheBlockTest.php tests/view/SSViewerTest.php
1118 lines
33 KiB
Markdown
Executable File
1118 lines
33 KiB
Markdown
Executable File
# 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);
|