Model guide documentation

This commit is contained in:
Will Rossiter 2014-10-27 16:40:02 +13:00 committed by Cam Findlay
parent 19259e0497
commit 30a85be38e
25 changed files with 1912 additions and 2847 deletions

View 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]

View File

@ -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, youll define a class called `Page`
and use this template to lay out the basic design elements that dont 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).

View 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]

View File

@ -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).

View File

@ -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]`

View 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]

View File

@ -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
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.
Each model in a SilverStripe [api:DataObject] will handle data at some point. This includes database columns such as
the ones defined in a `$db` array or simply a method that returns data for the template.
A Data Type is represented in SilverStripe by a [api:DBField] subclass. The class is responsible for telling the ORM
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
* `[api:Boolean]`: A boolean field.
* `[api:Currency]`: A number with 2 decimal points of precision, designed to store currency values.
* `[api:Date]`: A date field
* `[api:Decimal]`: A decimal number.
* `[api:Enum]`: An enumeration of a set of strings
* `[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:Int]`: An integer field.
* `[api:Percentage]`: A decimal number between 0 and 1 that represents a percentage.
* `[api:SS_Datetime]`: A date / time field
* `[api:Text]`: A variable-length string of up to 2MB, designed to store raw text
* `[api:Time]`: A time field
* `[api:Varchar]`: A variable-length string of up to 255 characters, designed to store raw text
* [api:Boolean]: A boolean field.
* [api:Currency]: A number with 2 decimal points of precision, designed to store currency values.
* [api:Date]: A date field
* [api:Decimal]: A decimal number.
* [api:Enum]: An enumeration of a set of strings
* [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:Int]: An integer field.
* [api:Percentage]: A decimal number between 0 and 1 that represents a percentage.
* [api:SS_Datetime]: A date / time field
* [api:Text]: A variable-length string of up to 2MB, designed to store raw text
* [api:Time]: A time field
* [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
from one value to another, e.g. to round a number.
## Formatting Output
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
DBField::create_field('Double', 1.23456)->Round(2); // results in 1.23
Of course that's much more verbose than the equivalent PHP call.
The power of `[api:DBField]` comes with its more sophisticated helpers,
like showing the time difference to the current date:
Of course that's much more verbose than the equivalent PHP call. The power of [api:DBField] comes with its more
sophisticated helpers, like showing the time difference to the current date:
:::php
DBField::create_field('Date', '1982-01-01')->TimeDiff(); // shows "30 years ago"
## Casting ViewableData
Most objects in SilverStripe extend from `[api:ViewableData]`,
which means they know how to present themselves in a view context.
Through a `$casting` array, arbitrary properties and getters can be casted:
Most objects in SilverStripe extend from [api:ViewableData], which means they know how to present themselves in a view
context. Through a `$casting` array, arbitrary properties and getters can be casted:
:::php
<?php
class MyObject extends ViewableData {
static $casting = array(
private static $casting = array(
'MyDate' => 'Date'
);
public function getMyDate() {
return '1982-01-01';
}
}
$obj = new MyObject;
$obj->getMyDate(); // returns string
$obj->MyDate; // returns string
$obj->obj('MyDate'); // returns object
$obj->obj('MyDate')->InPast(); // returns boolean
## 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
The database field types `[api:HTMLVarchar]`/`[api:HTMLText]` and `[api:Varchar]`/`[api:Text]`
are exactly the same in the database. However, the templating engine knows to escape
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 "[security](/topics/security)"),
which is important if these fields store user-provided data.
The database field types [api:HTMLVarchar]/[api:HTMLText] and [api:Varchar]/[api:Text] are exactly the same in
the database. However, the template engine knows to escape 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
"[security](../security)"), which is important if these fields store user-provided data.
You can disable this auto-escaping by using the `$MyField.RAW` escaping hints,
or explicitly request escaping of HTML content via `$MyHtmlField.XML`.
<div class="hint" markdown="1">
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)
* ["security" topic](/topics/security)
"Getters" and "Setters" are functions that help us save fields to our [api:DataObject] instances. By default, the
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]

View File

@ -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);

View File

@ -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>

View File

@ -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.

View 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]

View 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]

View File

@ -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)

View 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]

View File

@ -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");
}
}

View File

@ -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];

View 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]

View 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]

View 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]

View File

@ -1,8 +1,13 @@
title: Model and Databases
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"]

View File

@ -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');

View File

@ -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

View File

@ -2,3 +2,51 @@ title: Upgrading
introduction: Keep your SilverStripe installations up to date with the latest fixes, security patches and new features.
# 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)