mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Model guide documentation
This commit is contained in:
parent
918baf1ca3
commit
699b999f1e
566
docs/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md
Normal file
566
docs/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md
Normal file
@ -0,0 +1,566 @@
|
||||
title: Introduction to the Data Model and ORM
|
||||
summary: Introduction to creating and querying a Data Model through the ORM.
|
||||
|
||||
# Introduction to the Data Model and ORM
|
||||
|
||||
SilverStripe uses an [object-relational model](http://en.wikipedia.org/wiki/Object-relational_model) to represent it's
|
||||
information.
|
||||
|
||||
* Each database-table maps to a PHP class.
|
||||
* Each database-row maps to a PHP object.
|
||||
* Each database-column maps to a property on a PHP object.
|
||||
|
||||
All data tables in SilverStripe are defined as subclasses of [api:DataObject]. The [api:DataObject] class represents a
|
||||
single row in a database table, following the ["Active Record"](http://en.wikipedia.org/wiki/Active_record_pattern)
|
||||
design pattern. Database Columns are is defined as [Data Types](data_types_and_casting) in the static `$db` variable
|
||||
along with any [relationships](../relations) defined as `$has_one`, `$has_many`, `$many_many` properties on the class.
|
||||
|
||||
Let's look at a simple example:
|
||||
|
||||
**mysite/code/Player.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'PlayerNumber' => 'Int',
|
||||
'FirstName' => 'Varchar(255)',
|
||||
'LastName' => 'Text',
|
||||
'Birthday' => 'Date'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
This `Player` class definition will create a database table `Player` with columns for `PlayerNumber`, `FirstName` and
|
||||
so on. After writing this class, we need to regenerate the database schema.
|
||||
|
||||
## Generating the Database Schema
|
||||
|
||||
After adding, modifying or removing `DataObject` classes make sure to rebuild your SilverStripe database. The
|
||||
database-schema is generated automatically by visiting the URL http://www.yoursite.com/dev/build.
|
||||
|
||||
This script will analyze the existing schema, compare it to what's required by your data classes, and alter the schema
|
||||
as required.
|
||||
|
||||
It will perform the following changes:
|
||||
|
||||
* Create any missing tables
|
||||
* Create any missing fields
|
||||
* Create any missing indexes
|
||||
* Alter the field type of any existing fields
|
||||
* Rename any obsolete tables that it previously created to _obsolete_(tablename)
|
||||
|
||||
It **won't** do any of the following
|
||||
|
||||
* Deleting tables
|
||||
* Deleting fields
|
||||
* Rename any tables that it doesn't recognize - so other applications can co-exist in the same database, as long as
|
||||
their table names don't match a SilverStripe data class.
|
||||
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
You need to be logged in as an administrator to perform this command, unless your site is in [dev mode](../debugging),
|
||||
or the command is run through [CLI](../cli).
|
||||
</div>
|
||||
|
||||
When rebuilding the database schema through the [api:SS_ClassLoader] the following additional properties are
|
||||
automatically set on the `DataObject`.
|
||||
|
||||
* ID: Primary Key. When a new record is created, SilverStripe does not use the database's built-in auto-numbering
|
||||
system. Instead, it will generate a new `ID` by adding 1 to the current maximum ID.
|
||||
* ClassName: An enumeration listing this data-class and all of its subclasses.
|
||||
* Created: A date/time field set to the creation date of this record
|
||||
* LastEdited: A date/time field set to the date this record was last edited through `write()`
|
||||
|
||||
**mysite/code/Player.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'PlayerNumber' => 'Int',
|
||||
'FirstName' => 'Varchar(255)',
|
||||
'LastName' => 'Text',
|
||||
'Birthday' => 'Date'
|
||||
);
|
||||
}
|
||||
|
||||
Generates the following `SQL`.
|
||||
|
||||
CREATE TABLE `Player` (
|
||||
`ID` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`ClassName` enum('Player') DEFAULT 'Player',
|
||||
`LastEdited` datetime DEFAULT NULL,
|
||||
`Created` datetime DEFAULT NULL,
|
||||
`PlayerNumber` int(11) NOT NULL DEFAULT '0',
|
||||
`FirstName` varchar(255) DEFAULT NULL,
|
||||
`LastName` mediumtext,
|
||||
`Birthday` datetime DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (`ID`),
|
||||
KEY `ClassName` (`ClassName`)
|
||||
);
|
||||
|
||||
## Creating Data Records
|
||||
|
||||
A new instance of a [api:DataObject] can be created using the `new` syntax.
|
||||
|
||||
:::php
|
||||
$player = new Player();
|
||||
|
||||
Or, a better way is to use the `create` method.
|
||||
|
||||
:::php
|
||||
$player = Player::create();
|
||||
|
||||
Database columns and properties can be set as class properties on the object. The SilverStripe ORM handles the saving
|
||||
of the values through a custom `__set()` method.
|
||||
|
||||
:::php
|
||||
$player->FirstName = "Sam";
|
||||
$player->PlayerNumber = 07;
|
||||
|
||||
To save the `DataObject` to the database use the `write()` method. The first time `write()` is called an `ID` will be
|
||||
set.
|
||||
|
||||
:::php
|
||||
$player->write();
|
||||
|
||||
## Querying Data
|
||||
|
||||
With the `Player` class defined we can query our data using the `ORM` or Object-Relational Model. The `ORM` provides
|
||||
shortcuts and methods for fetching, sorting and filtering data from our database.
|
||||
|
||||
:::php
|
||||
$players = Player::get();
|
||||
// returns a `DataList` containing all the `Player` objects.
|
||||
|
||||
$player = Player::get()->byId(2);
|
||||
// returns a single `Player` object instance that has the ID of 2.
|
||||
|
||||
echo $player->ID;
|
||||
// returns the players 'ID' column value
|
||||
|
||||
echo $player->dbObject('LastEdited')->Ago();
|
||||
// calls the `Ago` method on the `LastEdited` property.
|
||||
|
||||
The `ORM` uses a "fluent" syntax, where you specify a query by chaining together different methods. Two common methods
|
||||
are `filter()` and `sort()`:
|
||||
|
||||
:::php
|
||||
$members = Player::get()->filter(array(
|
||||
'FirstName' => 'Sam'
|
||||
))->sort('Surname');
|
||||
|
||||
// returns a `DataList` containing all the `Player` records that have the `FirstName` of 'Sam'
|
||||
|
||||
<div class="info" markdown="1">
|
||||
Provided `filter` values are automatically escaped and do not require any escaping.
|
||||
</div>
|
||||
|
||||
## Lazy Loading
|
||||
|
||||
The `ORM` doesn't actually execute the [api:SQLQuery] until you iterate on the result with a `foreach()` or `<% loop %>`.
|
||||
|
||||
It's smart enough to generate a single efficient query at the last moment in time without needing to post process the
|
||||
result set in PHP. In `MySQL` the query generated by the ORM may look something like this
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName' => 'Sam'
|
||||
));
|
||||
|
||||
$players = $players->sort('Surname');
|
||||
|
||||
// executes the following single query
|
||||
// SELECT * FROM Player WHERE FirstName = 'Sam' ORDER BY Surname
|
||||
|
||||
|
||||
This also means that getting the count of a list of objects will be done with a single, efficient query.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName' => 'Sam'
|
||||
))->sort('Surname');
|
||||
|
||||
// This will create an single SELECT COUNT query
|
||||
// SELECT COUNT(*) FROM Player WHERE FirstName = 'Sam'
|
||||
echo $players->Count();
|
||||
|
||||
## Looping over a list of objects
|
||||
|
||||
`get()` returns a `DataList` instance. You can loop over `DataList` instances in both PHP and templates.
|
||||
|
||||
:::php
|
||||
$players = Player::get();
|
||||
|
||||
foreach($players as $player) {
|
||||
echo $player->FirstName;
|
||||
}
|
||||
|
||||
See the [Lists](../lists) documentation for more information on dealing with [api:SS_List] instances.
|
||||
|
||||
## Returning a single DataObject
|
||||
|
||||
There are a couple of ways of getting a single DataObject from the ORM. If you know the ID number of the object, you
|
||||
can use `byID($id)`:
|
||||
|
||||
:::php
|
||||
$player = Player::get()->byID(5);
|
||||
|
||||
`get()` returns a [api:DataList] instance. You can use operations on that to get back a single record.
|
||||
|
||||
:::php
|
||||
$players = Player::get();
|
||||
|
||||
$first = $players->first();
|
||||
$last = $players->last();
|
||||
|
||||
## Sorting
|
||||
|
||||
If would like to sort the list by `FirstName` in a ascending way (from A to Z).
|
||||
|
||||
:::php
|
||||
// Sort can either be Ascending (ASC) or Descending (DESC)
|
||||
$players = Player::get()->sort('FirstName', 'ASC');
|
||||
|
||||
// Ascending is implied
|
||||
$players = Player::get()->sort('FirstName');
|
||||
|
||||
To reverse the sort
|
||||
|
||||
:::php
|
||||
$players = Player::get()->sort('FirstName', 'DESC');
|
||||
|
||||
// or..
|
||||
$players = Player::get()->sort('FirstName', 'ASC')->reverse();
|
||||
|
||||
However you might have several entries with the same `FirstName` and would like to sort them by `FirstName` and
|
||||
`LastName`
|
||||
|
||||
:::php
|
||||
$players = Players::get()->sort(array(
|
||||
'FirstName' => 'ASC',
|
||||
'LastName'=>'ASC'
|
||||
));
|
||||
|
||||
You can also sort randomly.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->sort('RAND()')
|
||||
|
||||
|
||||
## Filtering Results
|
||||
|
||||
The `filter()` method filters the list of objects that gets returned.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName' => 'Sam'
|
||||
));
|
||||
|
||||
Each element of the array specifies a filter. You can specify as many filters as you like, and they **all** must be
|
||||
true for the record to be included in the result.
|
||||
|
||||
The key in the filter corresponds to the field that you want to filter and the value in the filter corresponds to the
|
||||
value that you want to filter to.
|
||||
|
||||
So, this would return only those players called "Sam Minnée".
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName' => 'Sam',
|
||||
'LastName' => 'Minnée',
|
||||
));
|
||||
|
||||
// SELECT * FROM Player WHERE FirstName = 'Sam' AND LastName = 'Minnée'
|
||||
|
||||
There is also a short hand way of getting Players with the FirstName of Sam.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter('FirstName', 'Sam');
|
||||
|
||||
Or if you want to find both Sam and Sig.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(
|
||||
'FirstName', array('Sam', 'Sig')
|
||||
);
|
||||
|
||||
// SELECT * FROM Player WHERE FirstName IN ('Sam', 'Sig')
|
||||
|
||||
You can use [SearchFilters](searchfilters) to add additional behavior to your `filter` command rather than an
|
||||
exact match.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName:StartsWith' => 'S'
|
||||
'PlayerNumber:GreaterThan' => '10'
|
||||
));
|
||||
|
||||
### filterAny
|
||||
|
||||
Use the `filterAny()` method to match multiple criteria non-exclusively (with an "OR" disjunctive),
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filterAny(array(
|
||||
'FirstName' => 'Sam',
|
||||
'Age' => 17,
|
||||
));
|
||||
|
||||
// SELECT * FROM Player WHERE ("FirstName" = 'Sam' OR "Age" = '17')
|
||||
|
||||
You can combine both conjunctive ("AND") and disjunctive ("OR") statements.
|
||||
|
||||
:::php
|
||||
$players = Player::get()
|
||||
->filter(array(
|
||||
'LastName' => 'Minnée'
|
||||
))
|
||||
->filterAny(array(
|
||||
'FirstName' => 'Sam',
|
||||
'Age' => 17,
|
||||
));
|
||||
// SELECT * FROM Player WHERE ("LastName" = 'Minnée' AND ("FirstName" = 'Sam' OR "Age" = '17'))
|
||||
|
||||
You can use [SearchFilters](searchfilters) to add additional behavior to your `filterAny` command.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filterAny(array(
|
||||
'FirstName:StartsWith' => 'S'
|
||||
'PlayerNumber:GreaterThan' => '10'
|
||||
));
|
||||
|
||||
|
||||
### filterByCallback
|
||||
|
||||
It is also possible to filter by a PHP callback, this will force the data model to fetch all records and loop them in
|
||||
PHP, thus `filter()` or `filterAny()` are to be preferred over `filterByCallback()`.
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
Because `filterByCallback()` has to run in PHP, it will always return an `ArrayList`
|
||||
</div>
|
||||
|
||||
The first parameter to the callback is the item, the second parameter is the list itself. The callback will run once
|
||||
for each record, if the callback returns true, this record will be added to the list of returned items.
|
||||
|
||||
The below example will get all `Players` aged over 10.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filterByCallback(function($item, $list) {
|
||||
return ($item->Age() > 10);
|
||||
});
|
||||
|
||||
### Exclude
|
||||
|
||||
The `exclude()` method is the opposite to the filter in that it removes entries from a list.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->exclude('FirstName', 'Sam');
|
||||
|
||||
// SELECT * FROM Player WHERE FirstName != 'Sam'
|
||||
|
||||
Remove both Sam and Sig..
|
||||
|
||||
:::php
|
||||
$players = Player::get()->exclude(
|
||||
'FirstName', array('Sam','Sig')
|
||||
);
|
||||
|
||||
`Exclude` follows the same pattern as filter, so for removing only Sam Minnée from the list:
|
||||
|
||||
:::php
|
||||
$players = Player::get()->exclude(array(
|
||||
'FirstName' => 'Sam',
|
||||
'Surname' => 'Minnée',
|
||||
));
|
||||
|
||||
And removing Sig and Sam with that are either age 17 or 74.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->exclude(array(
|
||||
'FirstName' => array('Sam', 'Sig'),
|
||||
'Age' => array(17, 43)
|
||||
));
|
||||
|
||||
// SELECT * FROM Player WHERE ("FirstName" NOT IN ('Sam','Sig) OR "Age" NOT IN ('17', '74));
|
||||
|
||||
You can use [SearchFilters](searchfilters) to add additional behavior to your `exclude` command.
|
||||
|
||||
:::php
|
||||
$players = Player::get()->exclude(array(
|
||||
'FirstName:EndsWith' => 'S'
|
||||
'PlayerNumber:LessThanOrEqual' => '10'
|
||||
));
|
||||
|
||||
### Subtract
|
||||
|
||||
You can subtract entries from a [api:DataList] by passing in another DataList to `subtract()`
|
||||
|
||||
:::php
|
||||
$sam = Player::get()->filter('FirstName', 'Sam');
|
||||
$players = Player::get();
|
||||
|
||||
$noSams = $players->subtract($sam);
|
||||
|
||||
Though for the above example it would probably be easier to use `filter()` and `exclude()`. A better use case could be
|
||||
when you want to find all the members that does not exist in a Group.
|
||||
|
||||
:::php
|
||||
// ... Finding all members that does not belong to $group.
|
||||
$otherMembers = Member::get()->subtract($group->Members());
|
||||
|
||||
### Limit
|
||||
|
||||
You can limit the amount of records returned in a DataList by using the `limit()` method.
|
||||
|
||||
:::php
|
||||
$members = Member::get()->limit(5);
|
||||
|
||||
`limit()` accepts two arguments, the first being the amount of results you want returned, with an optional second
|
||||
parameter to specify the offset, which allows you to tell the system where to start getting the results from. The
|
||||
offset, if not provided as an argument, will default to 0.
|
||||
|
||||
:::php
|
||||
// Return 10 members with an offset of 4 (starting from the 5th result).
|
||||
$members = Member::get()->sort('Surname')->limit(10, 4);
|
||||
|
||||
<div class="alert">
|
||||
Note that the `limit` argument order is different from a MySQL LIMIT clause.
|
||||
</div>
|
||||
|
||||
### Raw SQL
|
||||
|
||||
Occasionally, the system described above won't let you do exactly what you need to do. In these situations, we have
|
||||
methods that manipulate the SQL query at a lower level. When using these, please ensure that all table & field names
|
||||
are escaped with double quotes, otherwise some DB back-ends (e.g. PostgreSQL) won't work.
|
||||
|
||||
Under the hood, query generation is handled by the `[api:DataQuery]` class. This class does provide more direct access
|
||||
to certain SQL features that `DataList` abstracts away from you.
|
||||
|
||||
In general, we advise against using these methods unless it's absolutely necessary. If the ORM doesn't do quite what
|
||||
you need it to, you may also consider extending the ORM with new data types or filter modifiers
|
||||
|
||||
#### Where clauses
|
||||
|
||||
You can specify a WHERE clause fragment (that will be combined with other filters using AND) with the `where()` method:
|
||||
|
||||
:::php
|
||||
$members = Member::get()->where("\"FirstName\" = 'Sam'")
|
||||
|
||||
#### Joining Tables
|
||||
|
||||
You can specify a join with the `innerJoin` and `leftJoin` methods. Both of these methods have the same arguments:
|
||||
|
||||
* The name of the table to join to.
|
||||
* The filter clause for the join.
|
||||
* An optional alias.
|
||||
|
||||
:::php
|
||||
// Without an alias
|
||||
$members = Member::get()
|
||||
->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
|
||||
|
||||
$members = Member::get()
|
||||
->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel");
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
Passing a *$join* statement to will filter results further by the JOINs performed against the foreign table. It will
|
||||
**not** return the additionally joined data.
|
||||
</div>
|
||||
|
||||
### Default Values
|
||||
|
||||
Define the default values for all the `$db` fields. This example sets the "Status"-column on Player to "Active"
|
||||
whenever a new object is created.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $defaults = array(
|
||||
"Status" => 'Active',
|
||||
);
|
||||
}
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
Note: Alternatively you can set defaults directly in the database-schema (rather than the object-model). See
|
||||
[Data Types and Casting](data-types) for details.
|
||||
</div>
|
||||
|
||||
## Subclasses
|
||||
|
||||
|
||||
Inheritance is supported in the data model: separate tables will be linked together, the data spread across these
|
||||
tables. The mapping and saving logic is handled by SilverStripe, you don't need to worry about writing SQL most of the
|
||||
time.
|
||||
|
||||
For example, suppose we have the following set of classes:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Page extends SiteTree {
|
||||
|
||||
}
|
||||
|
||||
class NewsPage extends Page {
|
||||
|
||||
private static $db = array(
|
||||
'Summary' => 'Text'
|
||||
);
|
||||
}
|
||||
|
||||
The data for the following classes would be stored across the following tables:
|
||||
|
||||
:::yml
|
||||
SiteTree:
|
||||
- ID: Int
|
||||
- ClassName: Enum('SiteTree', 'Page', 'NewsPage')
|
||||
- Created: Datetime
|
||||
- LastEdited: Datetime
|
||||
- Title: Varchar
|
||||
- Content: Text
|
||||
NewsArticle:
|
||||
- ID: Int
|
||||
- Summary: Text
|
||||
|
||||
Accessing the data is transparent to the developer.
|
||||
|
||||
:::php
|
||||
$news = NewsPage::get();
|
||||
|
||||
foreach($news as $article) {
|
||||
echo $news->Title;
|
||||
}
|
||||
|
||||
The way the ORM stores the data is this:
|
||||
|
||||
* "Base classes" are direct sub-classes of [api:DataObject]. They are always given a table, whether or not they have
|
||||
special fields. This is called the "base table". In our case, `SiteTree` is the base table.
|
||||
|
||||
* The base table's ClassName field is set to class of the given record. It's an enumeration of all possible
|
||||
sub-classes of the base class (including the base class itself).
|
||||
|
||||
* Each sub-class of the base object will also be given its own table, *as long as it has custom fields*. In the
|
||||
example above, NewsSection didn't have its own data and so an extra table would be redundant.
|
||||
|
||||
* In all the tables, ID is the primary key. A matching ID number is used for all parts of a particular record:
|
||||
record #2 in Page refers to the same object as record #2 in `[api:SiteTree]`.
|
||||
|
||||
To retrieve a news article, SilverStripe joins the [api:SiteTree], [api:Page] and NewsArticle tables by their ID fields.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Data Types and Casting](../data_types_and_casting)
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:DataObject]
|
||||
* [api:DataList]
|
||||
* [api:DataQuery]
|
@ -1,125 +0,0 @@
|
||||
# Page Types
|
||||
|
||||
Hi people
|
||||
|
||||
## Introduction
|
||||
|
||||
Page Types are the basic building blocks of any SilverStripe website. A page type can define:
|
||||
|
||||
* Templates being used to display content
|
||||
* Form fields available to edit content in the CMS
|
||||
* Behaviour specific to a page type. For example a contact form on a ‘Contact Us’ page type, sending an email when the form is submitted
|
||||
|
||||
All the pages on the base installation are of the page type called "Page". See
|
||||
[tutorial:2-extending-a-basic-site](/tutorials/2-extending-a-basic-site) for a good introduction to page-types.
|
||||
|
||||
## Class and Template Inheritance
|
||||
|
||||
Each page type on your website is a sub-class of the `SiteTree` class. Usually, you’ll define a class called `Page`
|
||||
and use this template to lay out the basic design elements that don’t change.
|
||||
|
||||
![](_images/pagetype-inheritance.png)
|
||||
|
||||
Each page type is represented by two classes: a data object and a controller. In the diagrams above and below, the data
|
||||
objects are black and the controllers are blue. The page controllers are only used when the page type is actually
|
||||
visited on the website. In our example above, the search form would become a method on the ‘Page_Controller’ class.
|
||||
Any methods put on the data object will be available wherever we use this page. For example, we put any customizations
|
||||
we want to do to the CMS for this page type in here.
|
||||
|
||||
![](_images/controllers-and-dataobjects.png)
|
||||
|
||||
We put the `Page` class into a file called `Page.php` inside `mysite/code`.
|
||||
As a convention, we also put the `Page_Controller` class in the same file.
|
||||
|
||||
Why do we sub-class `Page` for everything? The easiest way to explain this is to use the example of a search form. If we
|
||||
create a search form on the `Page` class, then any other sub-class can also use it in their templates. This saves us
|
||||
re-defining commonly used forms or controls in every class we use.
|
||||
|
||||
## Templates
|
||||
|
||||
Page type templates work much the same as other [templates](/reference/templates) in SilverStripe
|
||||
(see ). There's some specialized controls and placeholders, as well as built-in inheritance.
|
||||
This is explained in a more in-depth topic at [Page Type Templates](/topics/page-type-templates).
|
||||
|
||||
## Adding Database Fields
|
||||
|
||||
Adding database fields is a simple process. You define them in an array of the static variable `$db`, this array is
|
||||
added on the object class. For example, Page or StaffPage. Every time you run dev/build to recompile the manifest, it
|
||||
checks if any new entries are added to the `$db` array and adds any fields to the database that are missing.
|
||||
|
||||
For example, you may want an additional field on a `StaffPage` class which extends `Page`, called `Author`. `Author` is a
|
||||
standard text field, and can be [casted](/topics/datamodel) as a variable character object in php (`VARCHAR` in SQL). In the
|
||||
following example, our `Author` field is casted as a variable character object with maximum characters of 50. This is
|
||||
especially useful if you know how long your source data needs to be.
|
||||
|
||||
:::php
|
||||
class StaffPage extends Page {
|
||||
private static $db = array(
|
||||
'Author' => 'Varchar(50)'
|
||||
);
|
||||
}
|
||||
class StaffPage_Controller extends Page_Controller {
|
||||
}
|
||||
|
||||
|
||||
See [datamodel](/topics/datamodel) for a more detailed explanation on adding database fields, and how the SilverStripe data
|
||||
model works.
|
||||
|
||||
## Adding Form Fields and Tabs
|
||||
|
||||
See [form](/topics/forms) and [tutorial:2-extending-a-basic-site](/tutorials/2-extending-a-basic-site).
|
||||
Note: To modify fields in the "Settings" tab, you need to use `updateSettingsFields()` instead.
|
||||
|
||||
## Removing inherited form fields and tabs
|
||||
|
||||
### removeFieldFromTab()
|
||||
|
||||
Overloading `getCMSFields()` you can call `removeFieldFromTab()` on a `[api:FieldList]` object. For example, if you don't
|
||||
want the MenuTitle field to show on your page, which is inherited from `[api:SiteTree]`.
|
||||
|
||||
:::php
|
||||
class StaffPage extends Page {
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
$fields->removeFieldFromTab('Root.Content', 'MenuTitle');
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
||||
class StaffPage_Controller extends Page_Controller {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
### removeByName()
|
||||
`removeByName()` for normal form fields is useful for breaking inheritance where you know a field in your form isn't
|
||||
required on a certain page-type.
|
||||
|
||||
:::php
|
||||
class MyForm extends Form {
|
||||
|
||||
public function __construct($controller, $name) {
|
||||
// add a default FieldList of form fields
|
||||
$member = singleton('Member');
|
||||
|
||||
$fields = $member->formFields();
|
||||
|
||||
// We don't want the Country field from our default set of fields, so we remove it.
|
||||
$fields->removeByName('Country');
|
||||
|
||||
$actions = new FieldList(
|
||||
new FormAction('submit', 'Submit')
|
||||
);
|
||||
|
||||
parent::__construct($controller, $name, $fields, $actions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
This will also work if you want to remove a whole tab e.g. $fields->removeByName('Metadata'); will remove the whole
|
||||
Metadata tab.
|
||||
|
||||
For more information on forms, see [form](/topics/forms), [tutorial:2-extending-a-basic-site](/tutorials/2-extending-a-basic-site)
|
||||
and [tutorial:3-forms](/tutorials/3-forms).
|
271
docs/en/02_Developer_Guides/00_Model/02_Relations.md
Normal file
271
docs/en/02_Developer_Guides/00_Model/02_Relations.md
Normal file
@ -0,0 +1,271 @@
|
||||
title: Relations between Records
|
||||
summary: Relate models together using the ORM.
|
||||
|
||||
# Relations between Records
|
||||
|
||||
In most situations you will likely see more than one [api:DataObject] and several classes in your data model may relate
|
||||
to one another. An example of this is a `Player` object may have a relationship to one or more `Team` or `Coach` classes
|
||||
and could take part in many `Games`. Relations are a key part of designing and building a good data model.
|
||||
|
||||
Relations are built through static array definitions on a class, in the format `<relationship-name> => <classname>`.
|
||||
SilverStripe supports a number of relationship types and each relationship type can have any number of relations.
|
||||
|
||||
## has_one
|
||||
|
||||
A 1-to-1 relation creates a database-column called "`<relationship-name>`ID", in the example below this would be
|
||||
"TeamID" on the "Player"-table.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Team extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
}
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $has_one = array(
|
||||
"Team" => "Team",
|
||||
);
|
||||
}
|
||||
|
||||
This defines a relationship called `Team` which links to a `Team` class. The `ORM` handles navigating the relationship
|
||||
and provides a short syntax for accessing the related object.
|
||||
|
||||
:::php
|
||||
$player = Player::get()->byId(1);
|
||||
|
||||
$team = $player->Team();
|
||||
// returns a 'Team' instance.
|
||||
|
||||
echo $player->Team()->Title;
|
||||
// returns the 'Title' column on the 'Team' or `getTitle` if it exists.
|
||||
|
||||
The relationship can also be navigated in [templates](../templates).
|
||||
|
||||
:::ss
|
||||
<% with $Player %>
|
||||
<% if $Team %>
|
||||
Plays for $Team.Title
|
||||
<% end_if %>
|
||||
<% end_with %>
|
||||
|
||||
## has_many
|
||||
|
||||
Defines 1-to-many joins. A database-column named ""`<relationship-name>`ID"" will to be created in the child-class. As
|
||||
you can see from the previous example, `$has_many` goes hand in hand with `$has_one`.
|
||||
|
||||
<div class="alert" markdown='1'>
|
||||
Please specify a $has_one-relationship on the related child-class as well, in order to have the necessary accessors
|
||||
available on both ends.
|
||||
</div>
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Team extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
}
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $has_one = array(
|
||||
"Team" => "Team",
|
||||
);
|
||||
}
|
||||
|
||||
Much like the `has_one` relationship, `has_many` can be navigated through the `ORM` as well. The only difference being
|
||||
you will get an instance of [api:HasManyList] rather than the object.
|
||||
|
||||
:::php
|
||||
$team = Team::get()->first();
|
||||
|
||||
echo $team->Players();
|
||||
// [HasManyList]
|
||||
|
||||
echo $team->Players()->Count();
|
||||
// returns '14';
|
||||
|
||||
foreach($team->Players() as $player) {
|
||||
echo $player->FirstName;
|
||||
}
|
||||
|
||||
To specify multiple $has_manys to the same object you can use dot notation to distinguish them like below:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Person extends DataObject {
|
||||
|
||||
private static $has_many = array(
|
||||
"Managing" => "Company.Manager",
|
||||
"Cleaning" => "Company.Cleaner",
|
||||
);
|
||||
}
|
||||
|
||||
class Company extends DataObject {
|
||||
|
||||
private static $has_one = array(
|
||||
"Manager" => "Person",
|
||||
"Cleaner" => "Person"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Multiple `$has_one` relationships are okay if they aren't linking to the same object type. Otherwise, they have to be
|
||||
named.
|
||||
|
||||
|
||||
## belongs_to
|
||||
|
||||
Defines a 1-to-1 relationship with another object, which declares the other end of the relationship with a
|
||||
corresponding $has_one. A single database column named `<relationship-name>ID` will be created in the object with the
|
||||
`$has_one`, but the $belongs_to by itself will not create a database field. This field will hold the ID of the object
|
||||
declaring the `$belongs_to`.
|
||||
|
||||
Similarly with $has_many, dot notation can be used to explicitly specify the `$has_one` which refers to this relation.
|
||||
This is not mandatory unless the relationship would be otherwise ambiguous.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Team extends DataObject {
|
||||
|
||||
private static $has_one = array(
|
||||
'Coach' => 'Coach'
|
||||
);
|
||||
}
|
||||
|
||||
class Coach extends DataObject {
|
||||
|
||||
private static $belongs_to = array(
|
||||
'Team' => 'Team.Coach'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
## many_many
|
||||
|
||||
Defines many-to-many joins. A new table, (this-class)_(relationship-name), will be created with a pair of ID fields.
|
||||
|
||||
<div class="warning" markdown='1'>
|
||||
Please specify a $belongs_many_many-relationship on the related class as well, in order to have the necessary accessors
|
||||
available on both ends.
|
||||
</div>
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Team extends DataObject {
|
||||
|
||||
private static $many_many = array(
|
||||
"Supporters" => "Supporter",
|
||||
);
|
||||
}
|
||||
|
||||
class Supporter extends DataObject {
|
||||
|
||||
private static $belongs_many_many = array(
|
||||
"Supports" => "Team",
|
||||
);
|
||||
}
|
||||
|
||||
Much like the `has_one` relationship, `mant_many` can be navigated through the `ORM` as well. The only difference being
|
||||
you will get an instance of [api:ManyManyList] rather than the object.
|
||||
|
||||
:::php
|
||||
$team = Team::get()->byId(1);
|
||||
|
||||
$supporters = $team->Supporters();
|
||||
// returns a 'ManyManyList' instance.
|
||||
|
||||
|
||||
The relationship can also be navigated in [templates](../templates).
|
||||
|
||||
:::ss
|
||||
<% with $Supporter %>
|
||||
<% loop $Supports %>
|
||||
Supports $Title
|
||||
<% end_if %>
|
||||
<% end_with %>
|
||||
|
||||
## Adding relations
|
||||
|
||||
Adding new items to a relations works the same, regardless if you're editing a **has_many** or a **many_many**. They are
|
||||
encapsulated by [api:HasManyList] and [api:ManyManyList], both of which provide very similar APIs, e.g. an `add()`
|
||||
and `remove()` method.
|
||||
|
||||
:::php
|
||||
$team = Team::get()->byId(1);
|
||||
|
||||
// create a new supporter
|
||||
$supporter = new Supporter();
|
||||
$supporter->Name = "Foo";
|
||||
$supporter->write();
|
||||
|
||||
// add the supporter.
|
||||
$team->Supporters()->add($supporter);
|
||||
|
||||
|
||||
## Custom Relations
|
||||
|
||||
You can use the ORM to get a filtered result list without writing any SQL. For example, this snippet gets you the
|
||||
"Players"-relation on a team, but only containing active players.
|
||||
|
||||
See `[api:DataObject::$has_many]` for more info on the described relations.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Team extends DataObject {
|
||||
|
||||
private static $has_many = array(
|
||||
"Players" => "Player"
|
||||
);
|
||||
|
||||
public function ActivePlayers() {
|
||||
return $this->Players()->filter('Status', 'Active');
|
||||
}
|
||||
}
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
Adding new records to a filtered `RelationList` like in the example above doesn't automatically set the filtered
|
||||
criteria on the added record.
|
||||
</div>
|
||||
|
||||
## Relations on Unsaved Objects
|
||||
|
||||
You can also set *has_many* and *many_many* relations before the `DataObject` is saved. This behavior uses the
|
||||
[api:UnsavedRelationList] and converts it into the correct `RelationList` when saving the `DataObject` for the first
|
||||
time.
|
||||
|
||||
This unsaved lists will also recursively save any unsaved objects that they contain.
|
||||
|
||||
As these lists are not backed by the database, most of the filtering methods on `DataList` cannot be used on a list of
|
||||
this type. As such, an `UnsavedRelationList` should only be used for setting a relation before saving an object, not
|
||||
for displaying the objects contained in the relation.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Introduction to the Data Model and ORM](data_model_and_orm)
|
||||
* [Lists](lists)
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:HasManyList]
|
||||
* [api:ManyManyList]
|
||||
* [api:DataObject]
|
@ -1,153 +0,0 @@
|
||||
# Sitetree
|
||||
|
||||
## Introduction
|
||||
|
||||
Basic data-object representing all pages within the site tree.
|
||||
The omnipresent *Page* class (located in `mysite/code/Page.php`) is based on this class.
|
||||
|
||||
## Creating, Modifying and Finding Pages
|
||||
|
||||
See the ["datamodel" topic](/topics/datamodel).
|
||||
|
||||
## Linking
|
||||
|
||||
:::php
|
||||
// wrong
|
||||
$mylink = $mypage->URLSegment;
|
||||
// right
|
||||
$mylink = $mypage->Link(); // alternatively: AbsoluteLink(), RelativeLink()
|
||||
|
||||
In a nutshell, the nested URLs feature means that your site URLs now reflect the actual parent/child page structure of
|
||||
your site. The URLs map directly to the chain of parent and child pages. The
|
||||
below table shows a quick summary of what these changes mean for your site:
|
||||
|
||||
![url table](http://silverstripe.org/assets/screenshots/Nested-URLs-Table.png)
|
||||
|
||||
## Querying
|
||||
|
||||
Use *SiteTree::get_by_link()* to correctly retrieve a page by URL, as it taked nested URLs into account (a page URL
|
||||
might consist of more than one *URLSegment*).
|
||||
|
||||
:::php
|
||||
// wrong
|
||||
$mypage = SiteTree::get()->filter("URLSegment", '<mylink>')->First();
|
||||
// right
|
||||
$mypage = SiteTree::get_by_link('<mylink>');
|
||||
|
||||
### Versioning
|
||||
|
||||
The `SiteTree` class automatically has an extension applied to it: `[Versioned](api:Versioned)`.
|
||||
This provides the basis for the CMS to operate on different stages,
|
||||
and allow authors to save their changes without publishing them to
|
||||
website visitors straight away.
|
||||
`Versioned` is a generic extension which can be applied to any `DataObject`,
|
||||
so most of its functionality is explained in the `["versioning" topic](/topics/versioning)`.
|
||||
|
||||
Since `SiteTree` makes heavy use of the extension, it adds some additional
|
||||
functionality and helpers on top of it.
|
||||
|
||||
Permission control:
|
||||
|
||||
:::php
|
||||
class MyPage extends Page {
|
||||
public function canPublish($member = null) {
|
||||
// return boolean from custom logic
|
||||
}
|
||||
public function canDeleteFromLive($member = null) {
|
||||
// return boolean from custom logic
|
||||
}
|
||||
}
|
||||
|
||||
Stage operations:
|
||||
|
||||
* `$page->doUnpublish()`: removes the "Live" record, with additional permission checks,
|
||||
as well as special logic for VirtualPage and RedirectorPage associations
|
||||
* `$page->doPublish()`: Inverse of doUnpublish()
|
||||
* `$page->doRevertToLive()`: Reverts current record to live state (makes sense to save to "draft" stage afterwards)
|
||||
* `$page->doRestoreToStage()`: Restore the content in the active copy of this SiteTree page to the stage site.
|
||||
|
||||
|
||||
Hierarchy operations (defined on `[api:Hierarchy]`:
|
||||
|
||||
* `$page->liveChildren()`: Return results only from live table
|
||||
* `$page->stageChildren()`: Return results from the stage table
|
||||
* `$page->AllHistoricalChildren()`: Return all the children this page had, including pages that were deleted from both stage & live.
|
||||
* `$page->AllChildrenIncludingDeleted()`: Return all children, including those that have been deleted but are still in live.
|
||||
|
||||
## Allowed Children, Default Child and Root-Level
|
||||
|
||||
By default, any page type can be the child of any other page type.
|
||||
However, there are static properties that can be
|
||||
used to set up restrictions that will preserve the integrity of the page hierarchy.
|
||||
|
||||
Example: Restrict blog entry pages to nesting underneath their blog holder
|
||||
|
||||
:::php
|
||||
class BlogHolder extends Page {
|
||||
// Blog holders can only contain blog entries
|
||||
private static $allowed_children = array("BlogEntry");
|
||||
private static $default_child = "BlogEntry";
|
||||
// ...
|
||||
}
|
||||
|
||||
class BlogEntry extends Page {
|
||||
// Blog entries can't contain children
|
||||
private static $allowed_children = "none";
|
||||
private static $can_be_root = false;
|
||||
// ...
|
||||
}
|
||||
|
||||
class Page extends SiteTree {
|
||||
// Don't let BlogEntry pages be underneath Pages. Only underneath Blog holders.
|
||||
private static $allowed_children = array("*Page,", "BlogHolder");
|
||||
}
|
||||
|
||||
|
||||
* **allowed_children:** This can be an array of allowed child classes, or the string "none" - indicating that this page
|
||||
type can't have children. If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no
|
||||
subclasses. Otherwise, the class and all its subclasses are allowed.
|
||||
|
||||
* **default_child:** If a page is allowed more than 1 type of child, you can set a default. This is the value that
|
||||
will be automatically selected in the page type dropdown when you create a page in the CMS.
|
||||
|
||||
* **can_be_root:** This is a boolean variable. It lets you specify whether the given page type can be in the top
|
||||
level.
|
||||
|
||||
Note that there is no allowed_parents` control. To set this, you will need to specify the `allowed_children` of all other page types to exclude the page type in question.
|
||||
|
||||
## Tree Limitations
|
||||
|
||||
SilverStripe limits the amount of initially rendered nodes in order to avoid
|
||||
processing delays, usually to a couple of dozen. The value can be configured
|
||||
through `[api:Hierarchy::$node_threshold_total]`.
|
||||
|
||||
If a website has thousands of pages, the tree UI metaphor can become an inefficient way
|
||||
to manage them. The CMS has an alternative "list view" for this purpose, which allows
|
||||
sorting and paging through large numbers of pages in a tabular view.
|
||||
|
||||
To avoid exceeding performance constraints of both the server and browser,
|
||||
SilverStripe places hard limits on the amount of rendered pages in
|
||||
a specific tree leaf, typically a couple of hundred pages.
|
||||
The value can be configured through `[api:Hierarchy::$node_threshold_leaf]`.
|
||||
|
||||
## Tree Display (Description, Icons and Badges)
|
||||
|
||||
The page tree in the CMS is a central element to manage page hierarchies,
|
||||
hence its display of pages can be customized as well.
|
||||
|
||||
On a most basic level, you can specify a custom page icon
|
||||
to make it easier for CMS authors to identify pages of this type,
|
||||
when navigating the tree or adding a new page:
|
||||
|
||||
:::php
|
||||
class StaffPage extends Page {
|
||||
private static $singular_name = 'Staff Directory';
|
||||
private static $plural_name = 'Staff Directories';
|
||||
private static $description = 'Two-column layout with a list of staff members';
|
||||
private static $icon = 'mysite/images/staff-icon.png';
|
||||
// ...
|
||||
}
|
||||
|
||||
You can also add custom "badges" to each page in the tree,
|
||||
which denote status. Built-in examples are "Draft" and "Deleted" flags.
|
||||
This is detailed in the ["Customize the CMS Tree" howto](/howto/customize-cms-tree).
|
@ -1,302 +0,0 @@
|
||||
# DataObject
|
||||
|
||||
## Introduction
|
||||
|
||||
The `[api:DataObject]` class represents a single row in a database table,
|
||||
following the ["Active Record"](http://en.wikipedia.org/wiki/Active_record_pattern) design pattern.
|
||||
|
||||
## Defining Properties
|
||||
|
||||
Properties defined through `DataObject::$db` map to table columns,
|
||||
and can be declared as different [data-types](/topics/data-types).
|
||||
|
||||
## Loading and Saving Records
|
||||
|
||||
The basic principles around data persistence and querying for objects
|
||||
is explained in the ["datamodel" topic](/topics/datamodel).
|
||||
|
||||
## Defining Form Fields
|
||||
|
||||
In addition to defining how data is persisted, the class can also
|
||||
help with editing it by providing form fields through `DataObject->getCMSFields()`.
|
||||
The resulting `[api:FieldList]` is the centrepiece of many data administration interfaces in SilverStripe.
|
||||
Many customizations of the SilverStripe CMS interface start here,
|
||||
by adding, removing or configuring fields.
|
||||
|
||||
Here is an example getCMSFields implementation:
|
||||
|
||||
:::php
|
||||
class MyDataObject extends DataObject {
|
||||
$db = array(
|
||||
'IsActive' => 'Boolean'
|
||||
);
|
||||
public function getCMSFields() {
|
||||
return new FieldList(
|
||||
new CheckboxField('IsActive')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
There's various [form field types](/references/form-field-types), for editing text, dates,
|
||||
restricting input to numbers, and much more.
|
||||
|
||||
## Scaffolding Form Fields
|
||||
|
||||
The ORM already has a lot of information about the data represented by a `DataObject`
|
||||
through its `$db` property, so why not use it to create form fields as well?
|
||||
If you call the parent implementation, the class will use `[api:FormScaffolder]`
|
||||
to provide reasonable defaults based on the property type (e.g. a checkbox field for booleans).
|
||||
You can then further customize those fields as required.
|
||||
|
||||
:::php
|
||||
class MyDataObject extends DataObject {
|
||||
// ...
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
$fields->fieldByName('IsActive')->setTitle('Is active?');
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
The [ModelAdmin](/reference/modeladmin) class uses this approach to provide
|
||||
data management interfaces with very little custom coding.
|
||||
|
||||
You can also alter the fields of built-in and module `DataObject` classes through
|
||||
your own [DataExtension](/reference/dataextension), and a call to `DataExtension->updateCMSFields()`.
|
||||
`[api::DataObject->beforeUpdateCMSFields()]` can also be used to interact with and add to automatically
|
||||
scaffolded fields prior to being passed to extensions (See [DataExtension](/reference/dataextension)).
|
||||
|
||||
### Searchable Fields
|
||||
|
||||
The `$searchable_fields` property uses a mixed array format that can be used to further customize your generated admin
|
||||
system. The default is a set of array values listing the fields.
|
||||
|
||||
Example: Getting predefined searchable fields
|
||||
|
||||
:::php
|
||||
$fields = singleton('MyDataObject')->searchableFields();
|
||||
|
||||
|
||||
Example: Simple Definition
|
||||
|
||||
:::php
|
||||
class MyDataObject extends DataObject {
|
||||
private static $searchable_fields = array(
|
||||
'Name',
|
||||
'ProductCode'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Searchable fields will be appear in the search interface with a default form field (usually a `[api:TextField]`) and a default
|
||||
search filter assigned (usually an `[api:ExactMatchFilter]`). To override these defaults, you can specify additional information
|
||||
on `$searchable_fields`:
|
||||
|
||||
:::php
|
||||
class MyDataObject extends DataObject {
|
||||
private static $searchable_fields = array(
|
||||
'Name' => 'PartialMatchFilter',
|
||||
'ProductCode' => 'NumericField'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
If you assign a single string value, you can set it to be either a `[api:FormField]` or `[api:SearchFilter]`. To specify both, you can
|
||||
assign an array:
|
||||
|
||||
:::php
|
||||
class MyDataObject extends DataObject {
|
||||
private static $searchable_fields = array(
|
||||
'Name' => array(
|
||||
'field' => 'TextField',
|
||||
'filter' => 'PartialMatchFilter',
|
||||
),
|
||||
'ProductCode' => array(
|
||||
'title' => 'Product code #',
|
||||
'field' => 'NumericField',
|
||||
'filter' => 'PartialMatchFilter',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
To include relations (`$has_one`, `$has_many` and `$many_many`) in your search, you can use a dot-notation.
|
||||
|
||||
:::php
|
||||
class Team extends DataObject {
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
private static $many_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
private static $searchable_fields = array(
|
||||
'Title',
|
||||
'Players.Name',
|
||||
);
|
||||
}
|
||||
class Player extends DataObject {
|
||||
private static $db = array(
|
||||
'Name' => 'Varchar',
|
||||
'Birthday' => 'Date'
|
||||
);
|
||||
private static $belongs_many_many = array(
|
||||
'Teams' => 'Team'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
### Summary Fields
|
||||
|
||||
Summary fields can be used to show a quick overview of the data for a specific `[api:DataObject]` record. Most common use is
|
||||
their display as table columns, e.g. in the search results of a `[api:ModelAdmin]` CMS interface.
|
||||
|
||||
Example: Getting predefined summary fields
|
||||
|
||||
:::php
|
||||
$fields = singleton('MyDataObject')->summaryFields();
|
||||
|
||||
|
||||
Example: Simple Definition
|
||||
|
||||
:::php
|
||||
class MyDataObject extends DataObject {
|
||||
private static $db = array(
|
||||
'Name' => 'Text',
|
||||
'OtherProperty' => 'Text',
|
||||
'ProductCode' => 'Int',
|
||||
);
|
||||
private static $summary_fields = array(
|
||||
'Name',
|
||||
'ProductCode'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
To include relations or field manipulations in your summaries, you can use a dot-notation.
|
||||
|
||||
:::php
|
||||
class OtherObject extends DataObject {
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
}
|
||||
class MyDataObject extends DataObject {
|
||||
private static $db = array(
|
||||
'Name' => 'Text',
|
||||
'Description' => 'HTMLText'
|
||||
);
|
||||
private static $has_one = array(
|
||||
'OtherObject' => 'OtherObject'
|
||||
);
|
||||
private static $summary_fields = array(
|
||||
'Name' => 'Name',
|
||||
'Description.Summary' => 'Description (summary)',
|
||||
'OtherObject.Title' => 'Other Object Title'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Non-textual elements (such as images and their manipulations) can also be used in summaries.
|
||||
|
||||
:::php
|
||||
class MyDataObject extends DataObject {
|
||||
private static $db = array(
|
||||
'Name' => 'Text'
|
||||
);
|
||||
private static $has_one = array(
|
||||
'HeroImage' => 'Image'
|
||||
);
|
||||
private static $summary_fields = array(
|
||||
'Name' => 'Name',
|
||||
'HeroImage.CMSThumbnail' => 'Hero Image'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
## Permissions
|
||||
|
||||
Models can be modified in a variety of controllers and user interfaces,
|
||||
all of which can implement their own security checks. But often it makes
|
||||
sense to centralize those checks on the model, regardless of the used controller.
|
||||
|
||||
The API provides four methods for this purpose:
|
||||
`canEdit()`, `canCreate()`, `canView()` and `canDelete()`.
|
||||
Since they're PHP methods, they can contain arbitrary logic
|
||||
matching your own requirements. They can optionally receive a `$member` argument,
|
||||
and default to the currently logged in member (through `Member::currentUser()`).
|
||||
|
||||
Example: Check for CMS access permissions
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
// ...
|
||||
public function canView($member = null) {
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
public function canEdit($member = null) {
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
public function canDelete($member = null) {
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
public function canCreate($member = null) {
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
}
|
||||
|
||||
**Important**: These checks are not enforced on low-level ORM operations
|
||||
such as `write()` or `delete()`, but rather rely on being checked in the invoking code.
|
||||
The CMS default sections as well as custom interfaces like
|
||||
[ModelAdmin](/reference/modeladmin) or [GridField](/reference/grid-field)
|
||||
already enforce these permissions.
|
||||
|
||||
## Indexes
|
||||
|
||||
It is sometimes desirable to add indexes to your data model, whether to
|
||||
optimize queries or add a uniqueness constraint to a field. This is done
|
||||
through the `DataObject::$indexes` map, which maps index names to descriptor
|
||||
arrays that represent each index. There's several supported notations:
|
||||
|
||||
:::php
|
||||
# Simple
|
||||
private static $indexes = array(
|
||||
'<column-name>' => true
|
||||
);
|
||||
|
||||
# Advanced
|
||||
private static $indexes = array(
|
||||
'<index-name>' => array('type' => '<type>', 'value' => '"<column-name>"')
|
||||
);
|
||||
|
||||
# SQL
|
||||
private static $indexes = array(
|
||||
'<index-name>' => 'unique("<column-name>")'
|
||||
);
|
||||
|
||||
The `<index-name>` can be an an arbitrary identifier in order to allow for more than one
|
||||
index on a specific database column.
|
||||
The "advanced" notation supports more `<type>` notations.
|
||||
These vary between database drivers, but all of them support the following:
|
||||
|
||||
* `index`: Standard index
|
||||
* `unique`: Index plus uniqueness constraint on the value
|
||||
* `fulltext`: Fulltext content index
|
||||
|
||||
In order to use more database specific or complex index notations,
|
||||
we also support raw SQL for as a value in the `$indexes` definition.
|
||||
Keep in mind this will likely make your code less portable between databases.
|
||||
|
||||
Example: A combined index on a two fields.
|
||||
|
||||
:::php
|
||||
private static $db = array(
|
||||
'MyField' => 'Varchar',
|
||||
'MyOtherField' => 'Varchar',
|
||||
);
|
||||
private static $indexes = array(
|
||||
'MyIndexName' => array('type' => 'index', 'value' => '"MyField","MyOtherField"'),
|
||||
);
|
||||
|
||||
## API Documentation
|
||||
|
||||
`[api:DataObject]`
|
94
docs/en/02_Developer_Guides/00_Model/03_Lists.md
Normal file
94
docs/en/02_Developer_Guides/00_Model/03_Lists.md
Normal file
@ -0,0 +1,94 @@
|
||||
title: Managing Lists
|
||||
summary: Learn how to manipulate SS_List objects.
|
||||
|
||||
# Managing Lists
|
||||
|
||||
Whenever using the ORM to fetch records or navigate relationships you will receive an [api:SS_List] instance commonly as
|
||||
either [api:DataList] or [api:RelationList]. This object gives you the ability to iterate over each of the results or
|
||||
modify.
|
||||
|
||||
## Iterating over the list.
|
||||
|
||||
[api:SS_List] implements `IteratorAggregate` allowing you to loop over the instance.
|
||||
|
||||
:::php
|
||||
$members = Member::get();
|
||||
|
||||
foreach($members as $member) {
|
||||
echo $member->Name;
|
||||
}
|
||||
|
||||
Or in the template engine:
|
||||
|
||||
:::ss
|
||||
<% loop $Members %>
|
||||
<!-- -->
|
||||
<% end_loop %>
|
||||
|
||||
## Finding an item by value.
|
||||
|
||||
:::php
|
||||
// $list->find($key, $value);
|
||||
|
||||
//
|
||||
$members = Member::get();
|
||||
|
||||
echo $members->find('ID', 4)->FirstName;
|
||||
// returns 'Sam'
|
||||
|
||||
|
||||
## Maps
|
||||
|
||||
A map is an array where the array indexes contain data as well as the values. You can build a map from any list
|
||||
|
||||
:::php
|
||||
$members = Member::get()->map('ID', 'FirstName');
|
||||
|
||||
// $members = array(
|
||||
// 1 => 'Sam'
|
||||
// 2 => 'Sig'
|
||||
// 3 => 'Will'
|
||||
// );
|
||||
|
||||
This functionality is provided by the [api:SS_Map] class, which can be used to build a map around any `SS_List`.
|
||||
|
||||
:::php
|
||||
$members = Member::get();
|
||||
$map = new SS_Map($members, 'ID', 'FirstName');
|
||||
|
||||
## Column
|
||||
|
||||
:::php
|
||||
$members = Member::get();
|
||||
|
||||
echo $members->column('Email');
|
||||
|
||||
// returns array(
|
||||
// 'sam@silverstripe.com',
|
||||
// 'sig@silverstripe.com',
|
||||
// 'will@silverstripe.com'
|
||||
// );
|
||||
|
||||
## ArrayList
|
||||
|
||||
[api:ArrayList] exists to wrap a standard PHP array in the same API as a database backed list.
|
||||
|
||||
:::php
|
||||
$sam = Member::get()->byId(5);
|
||||
$sig = Member::get()->byId(6);
|
||||
|
||||
$list = new ArrayList();
|
||||
$list->push($sam);
|
||||
$list->push($sig);
|
||||
|
||||
echo $list->Count();
|
||||
// returns '2'
|
||||
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:SS_List]
|
||||
* [api:RelationList]
|
||||
* [api:DataList]
|
||||
* [api:ArrayList]
|
||||
* [api:SS_Map]
|
@ -1,99 +1,182 @@
|
||||
title: Data Types, Overloading and Casting
|
||||
summary: Documentation on how data is stored going in, coming out of the ORM and how to modify it.
|
||||
|
||||
# Data Types and Casting
|
||||
|
||||
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]
|
@ -1,938 +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)
|
||||
|
||||
#### 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
|
||||
|
||||
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
|
||||
|
||||
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");
|
||||
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
### 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;
|
||||
}
|
||||
}
|
||||
|
||||
## Maps
|
||||
|
||||
A map is an array where the array indexes contain data as well as the values.
|
||||
You can build a map from any DataList like this:
|
||||
|
||||
:::php
|
||||
$members = Member::get()->map('ID', 'FirstName');
|
||||
|
||||
This will return a map where the keys are Member IDs, and the values are the
|
||||
corresponding FirstName values. Like everything else in the ORM, these maps are
|
||||
lazy loaded, so the following code will only query a single record from the
|
||||
database:
|
||||
|
||||
:::php
|
||||
$members = Member::get()->map('ID', 'FirstName');
|
||||
echo $member[5];
|
||||
|
||||
This functionality is provided by the `SS_Map` class, which can be used to build
|
||||
a map around any `SS_List`.
|
||||
|
||||
:::php
|
||||
$members = Member::get();
|
||||
$map = new SS_Map($members, 'ID', 'FirstName');
|
||||
|
||||
Note: You can also retrieve a single property from all contained records
|
||||
through [SS_List->column()](api:SS_List#_column).
|
||||
|
||||
## Data Handling
|
||||
|
||||
When saving data through the object model, you don't have to manually escape
|
||||
strings to create SQL-safe commands. You have to make sure though that certain
|
||||
properties are not overwritten, e.g. *ID* or *ClassName*.
|
||||
|
||||
### Creation
|
||||
|
||||
:::php
|
||||
$myPlayer = new Player();
|
||||
$myPlayer->Firstname = "John"; // sets property on object
|
||||
$myPlayer->write(); // writes row to database
|
||||
|
||||
|
||||
### Update
|
||||
|
||||
:::php
|
||||
$myPlayer = Player::get()->byID(99);
|
||||
if($myPlayer) {
|
||||
$myPlayer->Firstname = "John"; // sets property on object
|
||||
$myPlayer->write(); // writes row to database
|
||||
}
|
||||
|
||||
|
||||
### Batch Update
|
||||
|
||||
:::php
|
||||
$myPlayer->update(
|
||||
ArrayLib::filter_keys(
|
||||
$_REQUEST,
|
||||
array('Birthday', 'Firstname')
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
Alternatively you can use *castedUpdate()* to respect the
|
||||
[data-types](/topics/data-types). This is preferred to manually casting data
|
||||
before saving.
|
||||
|
||||
:::php
|
||||
$myPlayer->castedUpdate(
|
||||
ArrayLib::filter_keys(
|
||||
$_REQUEST,
|
||||
array('Birthday', 'Firstname')
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
### onBeforeWrite
|
||||
|
||||
You can customize saving-behaviour for each DataObject, e.g. for adding workflow
|
||||
or data customization. The function is triggered when calling *write()* to save
|
||||
the object to the database. This includes saving a page in the CMS or altering a
|
||||
ModelAdmin record.
|
||||
|
||||
Example: Disallow creation of new players if the currently logged-in player is
|
||||
not a team-manager.
|
||||
|
||||
:::php
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $has_many = array(
|
||||
"Teams"=>"Team"
|
||||
);
|
||||
|
||||
public function onBeforeWrite() {
|
||||
// check on first write action, aka "database row creation"
|
||||
// (ID-property is not set)
|
||||
if(!$this->ID) {
|
||||
$currentPlayer = Member::currentUser();
|
||||
if(!$currentPlayer->IsTeamManager()) {
|
||||
user_error('Player-creation not allowed', E_USER_ERROR);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// check on every write action
|
||||
if(!$this->record['TeamID']) {
|
||||
user_error('Cannot save player without a valid team', E_USER_ERROR);
|
||||
exit();
|
||||
}
|
||||
|
||||
// CAUTION: You are required to call the parent-function, otherwise
|
||||
// SilverStripe will not execute the request.
|
||||
parent::onBeforeWrite();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
Note: There are no separate methods for *onBeforeCreate* and *onBeforeUpdate*.
|
||||
Please check for the existence of $this->ID to toggle these two modes, as shown
|
||||
in the example above.
|
||||
</div>
|
||||
|
||||
### onBeforeDelete
|
||||
|
||||
Triggered before executing *delete()* on an existing object.
|
||||
|
||||
Example: Checking for a specific [permission](/reference/permission) to delete
|
||||
this type of object. It checks if a member is logged in who belongs to a group
|
||||
containing the permission "PLAYER_DELETE".
|
||||
|
||||
:::php
|
||||
class Player extends DataObject {
|
||||
private static $has_many = array(
|
||||
"Teams"=>"Team"
|
||||
);
|
||||
|
||||
public function onBeforeDelete() {
|
||||
if(!Permission::check('PLAYER_DELETE')) {
|
||||
Security::permissionFailure($this);
|
||||
exit();
|
||||
}
|
||||
|
||||
parent::onBeforeDelete();
|
||||
}
|
||||
}
|
||||
|
||||
### Saving data with forms
|
||||
|
||||
See [forms](/topics/forms).
|
||||
|
||||
### Saving data with custom SQL
|
||||
|
||||
See the ["sql queries" topic](/reference/sqlquery) for custom *INSERT*,
|
||||
*UPDATE*, *DELETE* queries.
|
||||
|
||||
## Extending DataObjects
|
||||
|
||||
You can add properties and methods to existing `[api:DataObjects]`s like
|
||||
`[api:Member]` (a core class) without hacking core code or subclassing. See
|
||||
`[api:DataExtension]` for a general description, and `[api:Hierarchy]` for the
|
||||
most popular examples.
|
||||
|
||||
## FAQ
|
||||
|
||||
### What's the difference between DataObject::get() and a relation-getter?
|
||||
|
||||
You can work with both in pretty much the same way, but relationship-getters
|
||||
return a special type of collection:
|
||||
|
||||
A `[api:HasManyList]` or a `[api:ManyManyList]` with relation-specific
|
||||
functionality.
|
||||
|
||||
:::php
|
||||
$myTeams = $myPlayer->Team(); // returns HasManyList
|
||||
$myTeam->add($myOtherPlayer);
|
@ -0,0 +1,84 @@
|
||||
title: Extending DataObjects
|
||||
summary: Modify the data model without using subclasses.
|
||||
|
||||
# Extending DataObjects
|
||||
|
||||
You can add properties and methods to existing [api:DataObjects]s like [api:Member] without hacking core code or sub
|
||||
classing by using [api:DataExtension]. See the [Extending SilverStripe](../extending) guide for more information on
|
||||
[api:DataExtension].
|
||||
|
||||
The following documentation outlines some common hooks that the [api:Extension] API provides specifically for managing
|
||||
data records.
|
||||
|
||||
## onBeforeWrite
|
||||
|
||||
You can customize saving-behaviour for each DataObject, e.g. for adding workflow or data customization. The function is
|
||||
triggered when calling *write()* to save the object to the database. This includes saving a page in the CMS or altering
|
||||
a `ModelAdmin` record.
|
||||
|
||||
Example: Disallow creation of new players if the currently logged-in player is not a team-manager.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $has_many = array(
|
||||
"Teams"=>"Team"
|
||||
);
|
||||
|
||||
public function onBeforeWrite() {
|
||||
// check on first write action, aka "database row creation" (ID-property is not set)
|
||||
if(!$this->isInDb()) {
|
||||
$currentPlayer = Member::currentUser();
|
||||
|
||||
if(!$currentPlayer->IsTeamManager()) {
|
||||
user_error('Player-creation not allowed', E_USER_ERROR);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// check on every write action
|
||||
if(!$this->record['TeamID']) {
|
||||
user_error('Cannot save player without a valid team', E_USER_ERROR);
|
||||
exit();
|
||||
}
|
||||
|
||||
// CAUTION: You are required to call the parent-function, otherwise
|
||||
// SilverStripe will not execute the request.
|
||||
parent::onBeforeWrite();
|
||||
}
|
||||
}
|
||||
|
||||
## onBeforeDelete
|
||||
|
||||
Triggered before executing *delete()* on an existing object.
|
||||
|
||||
Example: Checking for a specific [permission](/reference/permission) to delete this type of object. It checks if a
|
||||
member is logged in who belongs to a group containing the permission "PLAYER_DELETE".
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $has_many = array(
|
||||
"Teams" => "Team"
|
||||
);
|
||||
|
||||
public function onBeforeDelete() {
|
||||
if(!Permission::check('PLAYER_DELETE')) {
|
||||
Security::permissionFailure($this);
|
||||
exit();
|
||||
}
|
||||
|
||||
parent::onBeforeDelete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
Note: There are no separate methods for *onBeforeCreate* and *onBeforeUpdate*. Please check `$this->isInDb()` to toggle
|
||||
these two modes, as shown in the example above.
|
||||
</div>
|
@ -1,114 +0,0 @@
|
||||
# Database Structure
|
||||
|
||||
SilverStripe is currently hard-coded to use a fix mapping between data-objects and the underlying database structure -
|
||||
opting for "convention over configuration". This page details what that database structure is.
|
||||
|
||||
## Base tables
|
||||
|
||||
Each direct sub-class of `[api:DataObject]` will have its own table.
|
||||
|
||||
The following fields are always created.
|
||||
|
||||
* ID: Primary Key
|
||||
* ClassName: An enumeration listing this data-class and all of its subclasses.
|
||||
* Created: A date/time field set to the creation date of this record
|
||||
* LastEdited: A date/time field set to the date this record was last edited
|
||||
|
||||
Every object of this class **or any of its subclasses** will have an entry in this table
|
||||
|
||||
### Extra Fields
|
||||
|
||||
* Every field listed in the data object's **$db** array will be included in this table.
|
||||
* For every relationship listed in the data object's **$has_one** array, there will be an integer field included in the
|
||||
table. This will contain the ID of the data-object being linked to. The database field name will be of the form
|
||||
"(relationship-name)ID", for example, ParentID.
|
||||
|
||||
### 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]`: getNextID() is used when creating new objects; it also handles the mechanics of
|
||||
updating the database to have the required schema.
|
53
docs/en/02_Developer_Guides/00_Model/06_SearchFilters.md
Normal file
53
docs/en/02_Developer_Guides/00_Model/06_SearchFilters.md
Normal file
@ -0,0 +1,53 @@
|
||||
title: SearchFilter Modifiers
|
||||
summary: Use suffixes on your ORM queries.
|
||||
|
||||
# SearchFilter Modifiers
|
||||
|
||||
The `filter` and `exclude` operations specify exact matches by default. However, there are a number of suffixes that
|
||||
you can put on field names to change this behavior. These are represented as `SearchFilter` subclasses and include.
|
||||
|
||||
* [api:StartsWithFilter]
|
||||
* [api:EndsWithFilter]
|
||||
* [api:PartialMatchFilter]
|
||||
* [api:GreaterThanFilter]
|
||||
* [api:GreaterThanOrEqualFilter]
|
||||
* [api:LessThanFilter]
|
||||
* [api:LessThanOrEqualFilter]
|
||||
|
||||
An example of a `SearchFilter` in use:
|
||||
|
||||
:::php
|
||||
// fetch any player that starts with a S
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName:StartsWith' => 'S'
|
||||
'PlayerNumber:GreaterThan' => '10'
|
||||
));
|
||||
|
||||
// to fetch any player that's name contains the letter 'z'
|
||||
$players = Player::get()->filterAny(array(
|
||||
'FirstName:PartialMatch' => 'z'
|
||||
'LastName:PartialMatch' => 'z'
|
||||
));
|
||||
|
||||
Developers can define their own [api:SearchFilter] if needing to extend the ORM filter and exclude behaviors.
|
||||
|
||||
These suffixes can also take modifiers themselves. The modifiers currently supported are `":not"`, `":nocase"` and
|
||||
`":case"`. These negate the filter, make it case-insensitive and make it case-sensitive respectively. The default
|
||||
comparison uses the database's default. For MySQL and MSSQL, this is case-insensitive. For PostgreSQL, this is
|
||||
case-sensitive.
|
||||
|
||||
The following is a query which will return everyone whose first name starts with S either lower or uppercase
|
||||
|
||||
:::php
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName:StartsWith:nocase' => 'S'
|
||||
));
|
||||
|
||||
// use :not to perform a converse operation to filter anything but a 'W'
|
||||
$players = Player::get()->filter(array(
|
||||
'FirstName:StartsWith:not' => 'W'
|
||||
));
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:SearchFilter]
|
50
docs/en/02_Developer_Guides/00_Model/07_Permissions.md
Normal file
50
docs/en/02_Developer_Guides/00_Model/07_Permissions.md
Normal file
@ -0,0 +1,50 @@
|
||||
title: Model-Level Permissions
|
||||
summary: Reduce risk by securing models.
|
||||
|
||||
# Model-Level Permissions
|
||||
|
||||
Models can be modified in a variety of controllers and user interfaces, all of which can implement their own security
|
||||
checks. Often it makes sense to centralize those checks on the model, regardless of the used controller.
|
||||
|
||||
The API provides four methods for this purpose: `canEdit()`, `canCreate()`, `canView()` and `canDelete()`.
|
||||
|
||||
Since they're PHP methods, they can contain arbitrary logic matching your own requirements. They can optionally receive
|
||||
a `$member` argument, and default to the currently logged in member (through `Member::currentUser()`).
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
By default, all `DataObject` subclasses can only be edited, created and viewed by users with the 'ADMIN' permission
|
||||
code.
|
||||
</div>
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
public function canView($member = null) {
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
|
||||
public function canEdit($member = null) {
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
|
||||
public function canDelete($member = null) {
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
|
||||
public function canCreate($member = null) {
|
||||
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
|
||||
}
|
||||
}
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
These checks are not enforced on low-level ORM operations such as `write()` or `delete()`, but rather rely on being
|
||||
checked in the invoking code. The CMS default sections as well as custom interfaces like [api:ModelAdmin] or
|
||||
[api:GridField] already enforce these permissions.
|
||||
</div>
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:DataObject]
|
||||
* [api:Permission]
|
@ -1,143 +0,0 @@
|
||||
# SQL Query
|
||||
|
||||
## Introduction
|
||||
|
||||
An object representing a SQL 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 SQLQuery abstraction layer
|
||||
$query = new SQLQuery();
|
||||
$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 sanitize user input before using it in SQL queries.
|
||||
</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
|
||||
|
||||
Currently not supported through the `SQLQuery` class, please use raw `DB::query()` calls instead.
|
||||
|
||||
:::php
|
||||
DB::query('UPDATE "Player" SET "Status"=\'Active\'');
|
||||
|
||||
### 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
|
||||
$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();
|
||||
|
||||
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
|
||||
$sqlQuery = new SQLQuery();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->setSelect('Birthdate');
|
||||
$sqlQuery->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear');
|
||||
$map = $sqlQuery->execute()->map();
|
||||
$field = new DropdownField('Birthdates', 'Birthdates', $map);
|
||||
|
||||
Note that going through SQLQuery is just necessary here
|
||||
because of the custom SQL value transformation (`YEAR()`).
|
||||
An alternative approach would be a custom getter in the object definition.
|
||||
|
||||
:::php
|
||||
class Player extends DataObject {
|
||||
private static $db = array(
|
||||
'Name' => 'Varchar',
|
||||
'Birthdate' => 'Date'
|
||||
);
|
||||
function getNameWithBirthyear() {
|
||||
return date('y', $this->Birthdate);
|
||||
}
|
||||
}
|
||||
$players = Player::get();
|
||||
$map = $players->map('Name', 'NameWithBirthyear');
|
||||
|
||||
## Related
|
||||
|
||||
* [datamodel](/topics/datamodel)
|
||||
* `[api:DataObject]`
|
||||
* [database-structure](database-structure)
|
114
docs/en/02_Developer_Guides/00_Model/08_SQL_Query.md
Normal file
114
docs/en/02_Developer_Guides/00_Model/08_SQL_Query.md
Normal file
@ -0,0 +1,114 @@
|
||||
title: SQLQuery
|
||||
summary: Write and modify direct database queries through SQLQuery.
|
||||
|
||||
# SQLQuery
|
||||
|
||||
A [api:SQLQuery] object represents a SQL query, which can be serialized into a SQL statement. Dealing with low-level
|
||||
SQL such as `mysql_query()` is not encouraged, since the ORM provides powerful abstraction API's.
|
||||
|
||||
For example, if you want to run a simple `COUNT` SQL statement, the following three statements are functionally
|
||||
equivalent:
|
||||
|
||||
:::php
|
||||
// Through raw SQL.
|
||||
$count = DB::query('SELECT COUNT(*) FROM "Member"')->value();
|
||||
|
||||
// Through SQLQuery abstraction layer.
|
||||
$query = new SQLQuery();
|
||||
$count = $query->setFrom('Member')->setSelect('COUNT(*)')->value();
|
||||
|
||||
// Through the ORM.
|
||||
$count = Member::get()->count();
|
||||
|
||||
|
||||
<div class="info">
|
||||
The SQLQuery object is used by the SilverStripe ORM internally. By understanding SQLQuery, you can modify the SQL that
|
||||
the ORM creates.
|
||||
</div>
|
||||
|
||||
## Usage
|
||||
|
||||
### Select
|
||||
|
||||
:::php
|
||||
$sqlQuery = new SQLQuery();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->selectField('FieldName', 'Name');
|
||||
$sqlQuery->selectField('YEAR("Birthday")', 'Birthyear');
|
||||
$sqlQuery->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"');
|
||||
$sqlQuery->addWhere('YEAR("Birthday") = 1982');
|
||||
|
||||
// $sqlQuery->setOrderBy(...);
|
||||
// $sqlQuery->setGroupBy(...);
|
||||
// $sqlQuery->setHaving(...);
|
||||
// $sqlQuery->setLimit(...);
|
||||
// $sqlQuery->setDistinct(true);
|
||||
|
||||
// Get the raw SQL (optional)
|
||||
$rawSQL = $sqlQuery->sql();
|
||||
|
||||
// Execute and return a Query object
|
||||
$result = $sqlQuery->execute();
|
||||
|
||||
// Iterate over results
|
||||
foreach($result as $row) {
|
||||
echo $row['BirthYear'];
|
||||
}
|
||||
|
||||
The `$result` is an array lightly wrapped in a database-specific subclass of `[api:Query]`. This class implements the
|
||||
*Iterator*-interface, and provides convenience-methods for accessing the data.
|
||||
|
||||
### Delete
|
||||
|
||||
:::php
|
||||
$sqlQuery->setDelete(true);
|
||||
|
||||
### Insert / Update
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
Currently not supported through the `SQLQuery` class, please use raw `DB::query()` calls instead.
|
||||
</div>
|
||||
|
||||
:::php
|
||||
DB::query('UPDATE "Player" SET "Status"=\'Active\'');
|
||||
|
||||
### Joins
|
||||
|
||||
:::php
|
||||
$sqlQuery = new SQLQuery();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->addSelect('COUNT("Player"."ID")');
|
||||
$sqlQuery->addWhere('"Team"."ID" = 99');
|
||||
$sqlQuery->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"');
|
||||
|
||||
$count = $sqlQuery->execute()->value();
|
||||
|
||||
### Mapping
|
||||
|
||||
Creates a map based on the first two columns of the query result.
|
||||
|
||||
:::php
|
||||
$sqlQuery = new SQLQuery();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->setSelect('ID');
|
||||
$sqlQuery->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear');
|
||||
$map = $sqlQuery->execute()->map();
|
||||
|
||||
echo $map;
|
||||
|
||||
// returns array(
|
||||
// 1 => "Foo - 1920",
|
||||
// 2 => "Bar - 1936"
|
||||
// );
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Introduction to the Data Model and ORM](../data_model_and_orm)
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:DataObject]
|
||||
* [api:SQLQuery]
|
||||
* [api:DB]
|
||||
* [api:Query]
|
||||
* [api:Database]
|
@ -1,204 +0,0 @@
|
||||
# Versioning of Database Content
|
||||
|
||||
## Overview
|
||||
|
||||
Database content in SilverStripe can be "staged" before its publication,
|
||||
as well as track all changes through the lifetime of a database record.
|
||||
|
||||
It is most commonly applied to pages in the CMS (the `SiteTree` class).
|
||||
This means that draft content edited in the CMS can be different from published content
|
||||
shown to your website visitors.
|
||||
|
||||
The versioning happens automatically on read and write.
|
||||
If you are using the SilverStripe ORM to perform these operations,
|
||||
you don't need to alter your existing calls.
|
||||
|
||||
Versioning in SilverStripe is handled through the `[api:Versioned]` class.
|
||||
It's a `[api:DataExtension]`, which allow it to be applied to any `[api:DataObject]` subclass.
|
||||
|
||||
## Configuration
|
||||
|
||||
Adding versioned to your `DataObject` subclass works the same as any other extension.
|
||||
It accepts two or more arguments denoting the different "stages",
|
||||
which map to different database tables. Add the following to your [configuration file](/topics/configuration):
|
||||
|
||||
:::yml
|
||||
MyRecord:
|
||||
extensions:
|
||||
- Versioned("Stage","Live")
|
||||
|
||||
Note: The extension is automatically applied to `SiteTree` class.
|
||||
|
||||
## Database Structure
|
||||
|
||||
Depending on how many stages you configured, two or more new tables will be created for your records.
|
||||
Note that the "Stage" naming has a special meaning here, it will leave the original
|
||||
table name unchanged, rather than adding a suffix.
|
||||
|
||||
* `MyRecord` table: Contains staged data
|
||||
* `MyRecord_Live` table: Contains live data
|
||||
* `MyRecord_versions` table: Contains a version history (new record created on each save)
|
||||
|
||||
Similarly, any subclass you create on top of a versioned base
|
||||
will trigger the creation of additional tables, which are automatically joined as required:
|
||||
|
||||
* `MyRecordSubclass` table: Contains only staged data for subclass columns
|
||||
* `MyRecordSubclass_Live` table: Contains only live data for subclass columns
|
||||
* `MyRecordSubclass_versions` table: Contains only version history for subclass columns
|
||||
|
||||
## Usage
|
||||
|
||||
### Reading Versions
|
||||
|
||||
By default, all records are retrieved from the "Draft" stage (so the `MyRecord` table in our example).
|
||||
You can explicitly request a certain stage through various getters on the `Versioned` class.
|
||||
|
||||
:::php
|
||||
// Fetching multiple records
|
||||
$stageRecords = Versioned::get_by_stage('MyRecord', 'Stage');
|
||||
$liveRecords = Versioned::get_by_stage('MyRecord', 'Live');
|
||||
|
||||
// Fetching a single record
|
||||
$stageRecord = Versioned::get_by_stage('MyRecord', 'Stage')->byID(99);
|
||||
$liveRecord = Versioned::get_by_stage('MyRecord', 'Live')->byID(99);
|
||||
|
||||
### Historical Versions
|
||||
|
||||
The above commands will just retrieve the latest version of its respective stage for you,
|
||||
but not older versions stored in the `<class>_versions` tables.
|
||||
|
||||
:::php
|
||||
$historicalRecord = Versioned::get_version('MyRecord', <record-id>, <version-id>);
|
||||
|
||||
Caution: The record is retrieved as a `DataObject`, but saving back modifications
|
||||
via `write()` will create a new version, rather than modifying the existing one.
|
||||
|
||||
In order to get a list of all versions for a specific record,
|
||||
we need to generate specialized `[api:Versioned_Version]` objects,
|
||||
which expose the same database information as a `DataObject`,
|
||||
but also include information about when and how a record was published.
|
||||
|
||||
:::php
|
||||
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
||||
$versions = $record->allVersions();
|
||||
echo $versions->First()->Version; // instance of Versioned_Version
|
||||
|
||||
### Writing Versions and Changing Stages
|
||||
|
||||
The usual call to `DataObject->write()` will write to whatever stage is currently
|
||||
active, as defined by the `Versioned::current_stage()` global setting.
|
||||
Each call will automatically create a new version in the `<class>_versions` table.
|
||||
To avoid this, use `[writeWithoutVersion()](api:Versioned->writeWithoutVersion())` instead.
|
||||
|
||||
To move a saved version from one stage to another,
|
||||
call `[writeToStage(<stage>)](api:Versioned->writeToStage())` on the object.
|
||||
The process of moving a version to a different stage is also called "publishing",
|
||||
so we've created a shortcut for this: `publish(<from-stage>, <to-stage>)`.
|
||||
|
||||
:::php
|
||||
$record = Versioned::get_by_stage('MyRecord', 'Stage')->byID(99);
|
||||
$record->MyField = 'changed';
|
||||
// will update `MyRecord` table (assuming Versioned::current_stage() == 'Stage'),
|
||||
// and write a row to `MyRecord_versions`.
|
||||
$record->write();
|
||||
// will copy the saved record information to the `MyRecord_Live` table
|
||||
$record->publish('Stage', 'Live');
|
||||
|
||||
Similarly, an "unpublish" operation does the reverse, and removes a record
|
||||
from a specific stage.
|
||||
|
||||
:::php
|
||||
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
||||
// will remove the row from the `MyRecord_Live` table
|
||||
$record->deleteFromStage('Live');
|
||||
|
||||
### Forcing the Current Stage
|
||||
|
||||
The current stage is stored as global state on the object.
|
||||
It is usually modified by controllers, e.g. when a preview is initialized.
|
||||
But it can also be set and reset temporarily to force a specific operation
|
||||
to run on a certain stage.
|
||||
|
||||
:::php
|
||||
$origMode = Versioned::get_reading_mode(); // save current mode
|
||||
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Live' records
|
||||
Versioned::set_reading_mode('Stage'); // temporarily overwrite mode
|
||||
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
|
||||
Versioned::set_reading_mode($origMode); // reset current mode
|
||||
|
||||
### Custom SQL
|
||||
|
||||
We generally discourage writing `Versioned` queries from scratch,
|
||||
due to the complexities involved through joining multiple tables
|
||||
across an inherited table scheme (see `[api:Versioned->augmentSQL()]`).
|
||||
If possible, try to stick to smaller modifications of the generated `DataList` objects.
|
||||
|
||||
Example: Get the first 10 live records, filtered by creation date:
|
||||
|
||||
:::php
|
||||
$records = Versioned::get_by_stage('MyRecord', 'Live')->limit(10)->sort('Created', 'ASC');
|
||||
|
||||
### Permissions
|
||||
|
||||
The `Versioned` extension doesn't provide any permissions on its own,
|
||||
but you can have a look at the `SiteTree` class for implementation samples,
|
||||
specifically `canPublish()` and `canDeleteFromStage()`.
|
||||
|
||||
### Page Specific Operations
|
||||
|
||||
Since the `Versioned` extension is primarily used for page objects,
|
||||
the underlying `SiteTree` class has some additional helpers.
|
||||
See the ["sitetree" reference](/reference/sitetree) for details.
|
||||
|
||||
### Templates Variables
|
||||
|
||||
In templates, you don't need to worry about this distinction.
|
||||
The `$Content` variable contain the published content by default,
|
||||
and only preview draft content if explicitly requested (e.g. by the "preview" feature in the CMS).
|
||||
If you want to force a specific stage, we recommend the `Controller->init()` method for this purpose.
|
||||
|
||||
### Controllers
|
||||
|
||||
The current stage for each request is determined by `VersionedRequestFilter` before
|
||||
any controllers initialize, through `Versioned::choose_site_stage()`.
|
||||
It checks for a `Stage` GET parameter, so you can force
|
||||
a draft stage by appending `?stage=Stage` to your request. The setting is "sticky"
|
||||
in the PHP session, so any subsequent requests will also be in draft stage.
|
||||
|
||||
Important: The `choose_site_stage()` call only deals with setting the default stage,
|
||||
and doesn't check if the user is authenticated to view it. As with any other controller logic,
|
||||
please use `DataObject->canView()` to determine permissions, and avoid exposing unpublished
|
||||
content to your users.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
private static $allowed_actions = array('showpage');
|
||||
public function showpage($request) {
|
||||
$page = Page::get()->byID($request->param('ID'));
|
||||
if(!$page->canView()) return $this->httpError(401);
|
||||
// continue with authenticated logic...
|
||||
}
|
||||
}
|
||||
|
||||
The `ContentController` class responsible for page display already has this built in,
|
||||
so your own `canView()` checks are only necessary in controllers extending directly
|
||||
from the `Controller` class.
|
||||
|
||||
## Recipes
|
||||
|
||||
It can be useful to add the variable `$SilverStripeNavigator` somewhere into the template, since it allows you to put a mini "admin" bar on the page which isn't visible to non editors. It shows the current stage and provides a convenient CMS link and version changing link.
|
||||
|
||||
Keep in mind that `$SilverStripeNavigator` is only available on ContentController, so useful when the Versioned extension is applied to SiteTree (default), and not when it's applied to DataObject.
|
||||
|
||||
### Trapping the publication event
|
||||
|
||||
Sometimes, you'll want to do something whenever a particular kind of page is published. This example sends an email
|
||||
whenever a blog entry has been published.
|
||||
|
||||
:::php
|
||||
class Page extends SiteTree {
|
||||
// ...
|
||||
public function onAfterPublish() {
|
||||
mail("sam@silverstripe.com", "Blog published", "The blog has been published");
|
||||
}
|
||||
}
|
@ -1 +1,48 @@
|
||||
* stub, talk validate()
|
||||
title: Model Validation and Constraints
|
||||
summary: Validate your data at the model level
|
||||
|
||||
# Validation and Constraints
|
||||
|
||||
Traditionally, validation in SilverStripe has been mostly handled on the controller through [form validation](../forms).
|
||||
|
||||
While this is a useful approach, it can lead to data inconsistencies if the record is modified outside of the
|
||||
controller and form context.
|
||||
|
||||
Most validation constraints are actually data constraints which belong on the model. SilverStripe provides the
|
||||
[api:DataObject->validate] method for this purpose.
|
||||
|
||||
By default, there is no validation - objects are always valid! However, you can overload this method in your DataObject
|
||||
sub-classes to specify custom validation, or use the `validate` hook through a [api:DataExtension].
|
||||
|
||||
Invalid objects won't be able to be written - a [api:ValidationException] will be thrown and no write will occur.
|
||||
|
||||
It is expected that you call `validate()` in your own application to test that an object is valid before attempting a
|
||||
write, and respond appropriately if it isn't.
|
||||
|
||||
The return value of `validate()` is a [api:ValidationResult] object.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Country' => 'Varchar',
|
||||
'Postcode' => 'Varchar'
|
||||
);
|
||||
|
||||
public function validate() {
|
||||
$result = parent::validate();
|
||||
|
||||
if($this->Country == 'DE' && $this->Postcode && strlen($this->Postcode) != 5) {
|
||||
$result->error('Need five digits for German postcodes');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:DataObject]
|
||||
* [api:ValidationResult];
|
||||
|
169
docs/en/02_Developer_Guides/00_Model/10_Versioning.md
Normal file
169
docs/en/02_Developer_Guides/00_Model/10_Versioning.md
Normal file
@ -0,0 +1,169 @@
|
||||
title: Versioning
|
||||
summary: Add versioning to your database content through the Versioned extension.
|
||||
|
||||
# Versioning
|
||||
|
||||
Database content in SilverStripe can be "staged" before its publication, as well as tracking all changes through the
|
||||
lifetime of a database record.
|
||||
|
||||
It is most commonly applied to pages in the CMS (the `SiteTree` class). Draft content edited in the CMS can be different
|
||||
from published content shown to your website visitors.
|
||||
|
||||
Versioning in SilverStripe is handled through the [api:Versioned] class. As a [api:DataExtension] it is possible to
|
||||
be applied to any `[api:DataObject]` subclass. The extension class will automatically update read and write operations
|
||||
done via the ORM via the `augmentSQL` database hook.
|
||||
|
||||
Adding Versioned to your `DataObject` subclass works the same as any other extension. It accepts two or more arguments
|
||||
denoting the different "stages", which map to different database tables.
|
||||
|
||||
**mysite/_config/app.yml**
|
||||
:::yml
|
||||
MyRecord:
|
||||
extensions:
|
||||
- Versioned("Stage","Live")
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
The extension is automatically applied to `SiteTree` class. For more information on extensions see
|
||||
[Extending](../extending) and the [Configuration](../configuration) documentation.
|
||||
</div>
|
||||
|
||||
## Database Structure
|
||||
|
||||
Depending on how many stages you configured, two or more new tables will be created for your records. In the above, this
|
||||
will create a new `MyClass_Live` table once you've rebuilt the database.
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
Note that the "Stage" naming has a special meaning here, it will leave the original table name unchanged, rather than
|
||||
adding a suffix.
|
||||
</div>
|
||||
|
||||
* `MyRecord` table: Contains staged data
|
||||
* `MyRecord_Live` table: Contains live data
|
||||
* `MyRecord_versions` table: Contains a version history (new record created on each save)
|
||||
|
||||
Similarly, any subclass you create on top of a versioned base will trigger the creation of additional tables, which are
|
||||
automatically joined as required:
|
||||
|
||||
* `MyRecordSubclass` table: Contains only staged data for subclass columns
|
||||
* `MyRecordSubclass_Live` table: Contains only live data for subclass columns
|
||||
* `MyRecordSubclass_versions` table: Contains only version history for subclass columns
|
||||
|
||||
## Usage
|
||||
|
||||
### Reading Versions
|
||||
|
||||
By default, all records are retrieved from the "Draft" stage (so the `MyRecord` table in our example). You can
|
||||
explicitly request a certain stage through various getters on the `Versioned` class.
|
||||
|
||||
:::php
|
||||
// Fetching multiple records
|
||||
$stageRecords = Versioned::get_by_stage('MyRecord', 'Stage');
|
||||
$liveRecords = Versioned::get_by_stage('MyRecord', 'Live');
|
||||
|
||||
// Fetching a single record
|
||||
$stageRecord = Versioned::get_by_stage('MyRecord', 'Stage')->byID(99);
|
||||
$liveRecord = Versioned::get_by_stage('MyRecord', 'Live')->byID(99);
|
||||
|
||||
### Historical Versions
|
||||
|
||||
The above commands will just retrieve the latest version of its respective stage for you, but not older versions stored
|
||||
in the `<class>_versions` tables.
|
||||
|
||||
:::php
|
||||
$historicalRecord = Versioned::get_version('MyRecord', <record-id>, <version-id>);
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
The record is retrieved as a `DataObject`, but saving back modifications via `write()` will create a new version,
|
||||
rather than modifying the existing one.
|
||||
</div>
|
||||
|
||||
In order to get a list of all versions for a specific record, we need to generate specialized `[api:Versioned_Version]`
|
||||
objects, which expose the same database information as a `DataObject`, but also include information about when and how
|
||||
a record was published.
|
||||
|
||||
:::php
|
||||
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
||||
$versions = $record->allVersions();
|
||||
echo $versions->First()->Version; // instance of Versioned_Version
|
||||
|
||||
### Writing Versions and Changing Stages
|
||||
|
||||
The usual call to `DataObject->write()` will write to whatever stage is currently active, as defined by the
|
||||
`Versioned::current_stage()` global setting. Each call will automatically create a new version in the
|
||||
`<class>_versions` table. To avoid this, use `[writeWithoutVersion()](api:Versioned->writeWithoutVersion())` instead.
|
||||
|
||||
To move a saved version from one stage to another, call `[writeToStage(<stage>)](api:Versioned->writeToStage())` on the
|
||||
object. The process of moving a version to a different stage is also called "publishing", so we've created a shortcut
|
||||
for this: `publish(<from-stage>, <to-stage>)`.
|
||||
|
||||
:::php
|
||||
$record = Versioned::get_by_stage('MyRecord', 'Stage')->byID(99);
|
||||
$record->MyField = 'changed';
|
||||
// will update `MyRecord` table (assuming Versioned::current_stage() == 'Stage'),
|
||||
// and write a row to `MyRecord_versions`.
|
||||
$record->write();
|
||||
// will copy the saved record information to the `MyRecord_Live` table
|
||||
$record->publish('Stage', 'Live');
|
||||
|
||||
Similarly, an "unpublish" operation does the reverse, and removes a record from a specific stage.
|
||||
|
||||
:::php
|
||||
$record = MyRecord::get()->byID(99); // stage doesn't matter here
|
||||
// will remove the row from the `MyRecord_Live` table
|
||||
$record->deleteFromStage('Live');
|
||||
|
||||
### Forcing the Current Stage
|
||||
|
||||
The current stage is stored as global state on the object. It is usually modified by controllers, e.g. when a preview
|
||||
is initialized. But it can also be set and reset temporarily to force a specific operation to run on a certain stage.
|
||||
|
||||
:::php
|
||||
$origMode = Versioned::get_reading_mode(); // save current mode
|
||||
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Live' records
|
||||
Versioned::set_reading_mode('Stage'); // temporarily overwrite mode
|
||||
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
|
||||
Versioned::set_reading_mode($origMode); // reset current mode
|
||||
|
||||
### Custom SQL
|
||||
|
||||
We generally discourage writing `Versioned` queries from scratch, due to the complexities involved through joining
|
||||
multiple tables across an inherited table scheme (see `[api:Versioned->augmentSQL()]`). If possible, try to stick to
|
||||
smaller modifications of the generated `DataList` objects.
|
||||
|
||||
Example: Get the first 10 live records, filtered by creation date:
|
||||
|
||||
:::php
|
||||
$records = Versioned::get_by_stage('MyRecord', 'Live')->limit(10)->sort('Created', 'ASC');
|
||||
|
||||
### Permissions
|
||||
|
||||
The `Versioned` extension doesn't provide any permissions on its own, but you can have a look at the `SiteTree` class
|
||||
for implementation samples, specifically `canPublish()` and `canDeleteFromStage()`.
|
||||
|
||||
### Page Specific Operations
|
||||
|
||||
Since the `Versioned` extension is primarily used for page objects, the underlying `SiteTree` class has some additional
|
||||
helpers.
|
||||
|
||||
### Templates Variables
|
||||
|
||||
In templates, you don't need to worry about this distinction. The `$Content` variable contain the published content by
|
||||
default, and only preview draft content if explicitly requested (e.g. by the "preview" feature in the CMS). If you want
|
||||
to force a specific stage, we recommend the `Controller->init()` method for this purpose.
|
||||
|
||||
### Controllers
|
||||
|
||||
The current stage for each request is determined by `VersionedRequestFilter` before any controllers initialize, through
|
||||
`Versioned::choose_site_stage()`. It checks for a `Stage` GET parameter, so you can force a draft stage by appending
|
||||
`?stage=Stage` to your request. The setting is "sticky" in the PHP session, so any subsequent requests will also be in
|
||||
draft stage.
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
The `choose_site_stage()` call only deals with setting the default stage, and doesn't check if the user is
|
||||
authenticated to view it. As with any other controller logic, please use `DataObject->canView()` to determine
|
||||
permissions, and avoid exposing unpublished content to your users.
|
||||
</div>
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:Versioned]
|
208
docs/en/02_Developer_Guides/00_Model/11_Scaffolding.md
Normal file
208
docs/en/02_Developer_Guides/00_Model/11_Scaffolding.md
Normal file
@ -0,0 +1,208 @@
|
||||
title: Building Model and Search Interfaces around Scaffolding
|
||||
summary: Model Driven approach to defining your application UI.
|
||||
|
||||
# Scaffolding
|
||||
|
||||
The ORM already has a lot of information about the data represented by a `DataObject` through its `$db` property, so
|
||||
SilverStripe will use that information to provide scaffold some interfaces. This is done though [api:FormScaffolder]
|
||||
to provide reasonable defaults based on the property type (e.g. a checkbox field for booleans). You can then further
|
||||
customize those fields as required.
|
||||
|
||||
## Form Fields
|
||||
|
||||
An example is `DataObject`, SilverStripe will automatically create your CMS interface so you can modify what you need.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'IsActive' => 'Boolean',
|
||||
'Title' => 'Varchar',
|
||||
'Content' => 'Text'
|
||||
);
|
||||
|
||||
public function getCMSFields() {
|
||||
// parent::getCMSFields() does all the hard work and creates the fields for title, isactive and content.
|
||||
$fields = parent::getCMSFields();
|
||||
$fields->fieldByName('IsActive')->setTitle('Is active?');
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
You can also alter the fields of built-in and module `DataObject` classes through your own
|
||||
[DataExtension](../extensions), and a call to `DataExtension->updateCMSFields`.
|
||||
|
||||
## Searchable Fields
|
||||
|
||||
The `$searchable_fields` property uses a mixed array format that can be used to further customize your generated admin
|
||||
system. The default is a set of array values listing the fields.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'Name',
|
||||
'ProductCode'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Searchable fields will be appear in the search interface with a default form field (usually a [api:TextField]) and a
|
||||
default search filter assigned (usually an [api:ExactMatchFilter]). To override these defaults, you can specify
|
||||
additional information on `$searchable_fields`:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'Name' => 'PartialMatchFilter',
|
||||
'ProductCode' => 'NumericField'
|
||||
);
|
||||
}
|
||||
|
||||
If you assign a single string value, you can set it to be either a [api:FormField] or [api:SearchFilter]. To specify
|
||||
both, you can assign an array:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'Name' => array(
|
||||
'field' => 'TextField',
|
||||
'filter' => 'PartialMatchFilter',
|
||||
),
|
||||
'ProductCode' => array(
|
||||
'title' => 'Product code #',
|
||||
'field' => 'NumericField',
|
||||
'filter' => 'PartialMatchFilter',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
To include relations (`$has_one`, `$has_many` and `$many_many`) in your search, you can use a dot-notation.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Team extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
|
||||
private static $many_many = array(
|
||||
'Players' => 'Player'
|
||||
);
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'Title',
|
||||
'Players.Name',
|
||||
);
|
||||
}
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Varchar',
|
||||
'Birthday' => 'Date'
|
||||
);
|
||||
|
||||
private static $belongs_many_many = array(
|
||||
'Teams' => 'Team'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
### Summary Fields
|
||||
|
||||
Summary fields can be used to show a quick overview of the data for a specific [api:DataObject] record. Most common use
|
||||
is their display as table columns, e.g. in the search results of a `[api:ModelAdmin]` CMS interface.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Text',
|
||||
'OtherProperty' => 'Text',
|
||||
'ProductCode' => 'Int',
|
||||
);
|
||||
|
||||
private static $summary_fields = array(
|
||||
'Name',
|
||||
'ProductCode'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
To include relations or field manipulations in your summaries, you can use a dot-notation.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class OtherObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
}
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Text',
|
||||
'Description' => 'HTMLText'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'OtherObject' => 'OtherObject'
|
||||
);
|
||||
|
||||
private static $summary_fields = array(
|
||||
'Name' => 'Name',
|
||||
'Description.Summary' => 'Description (summary)',
|
||||
'OtherObject.Title' => 'Other Object Title'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Non-textual elements (such as images and their manipulations) can also be used in summaries.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyDataObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Text'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'HeroImage' => 'Image'
|
||||
);
|
||||
|
||||
private static $summary_fields = array(
|
||||
'Name' => 'Name',
|
||||
'HeroImage.CMSThumbnail' => 'Hero Image'
|
||||
);
|
||||
}
|
||||
|
||||
## Related Documenation
|
||||
|
||||
* [SearchFilters](searchfilters)
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:FormScaffolder]
|
||||
* [api:DataObject]
|
55
docs/en/02_Developer_Guides/00_Model/12_Indexes.md
Normal file
55
docs/en/02_Developer_Guides/00_Model/12_Indexes.md
Normal file
@ -0,0 +1,55 @@
|
||||
title: Indexes
|
||||
summary: Add Indexes to your Data Model to optimize database queries.
|
||||
|
||||
# Indexes
|
||||
|
||||
It is sometimes desirable to add indexes to your data model, whether to optimize queries or add a uniqueness constraint
|
||||
to a field. This is done through the `DataObject::$indexes` map, which maps index names to descriptor arrays that
|
||||
represent each index. There's several supported notations:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyObject extends DataObject {
|
||||
|
||||
private static $indexes = array(
|
||||
'<column-name>' => true,
|
||||
'<index-name>' => array('type' => '<type>', 'value' => '"<column-name>"'),
|
||||
'<index-name>' => 'unique("<column-name>")'
|
||||
);
|
||||
}
|
||||
|
||||
The `<index-name>` can be an an arbitrary identifier in order to allow for more than one index on a specific database
|
||||
column. The "advanced" notation supports more `<type>` notations. These vary between database drivers, but all of them
|
||||
support the following:
|
||||
|
||||
* `index`: Standard index
|
||||
* `unique`: Index plus uniqueness constraint on the value
|
||||
* `fulltext`: Fulltext content index
|
||||
|
||||
In order to use more database specific or complex index notations, we also support raw SQL for as a value in the
|
||||
`$indexes` definition. Keep in mind this will likely make your code less portable between databases.
|
||||
|
||||
**mysite/code/MyTestObject.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class MyTestObject extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'MyField' => 'Varchar',
|
||||
'MyOtherField' => 'Varchar',
|
||||
);
|
||||
|
||||
private static $indexes = array(
|
||||
'MyIndexName' => array(
|
||||
'type' => 'index',
|
||||
'value' => '"MyField","MyOtherField"'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:DataObject]
|
@ -1,8 +1,13 @@
|
||||
title: Model and Databases
|
||||
summary: Learn how SilverStripe manages database tables, ways to query your database and how to publish data.
|
||||
introduction: This guide will cover how to create and manipulate data within SilverStripe and how to use the ORM (Object Relational Model) to query data.
|
||||
|
||||
[CHILDREN]
|
||||
In SilverStripe, application data will be represented by a [api:DataObject] class. A `DataObject` subclass defines the
|
||||
data columns, relationships and properties of a particular data record. For example, [api:Member] is a `DataObject`
|
||||
which stores information about a person, CMS user or mail subscriber.
|
||||
|
||||
## How-to
|
||||
[CHILDREN Exclude="How_tos"]
|
||||
|
||||
[CHILDREN How_To]
|
||||
## How to's
|
||||
|
||||
[CHILDREN Folder="How_Tos"]
|
@ -1,398 +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', 'html','htm','xhtml', 'xml','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
|
||||
|
||||
### 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);
|
||||
|
||||
### 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.php:
|
||||
|
||||
:::php
|
||||
Image::add_extension('GalleryImage');
|
||||
|
||||
<div class="notice" markdown='1'>
|
||||
Note: Although you can subclass the Image class instead of using a DataExtension,
|
||||
this is not advisable. For instance: when using a subclass, the 'From files'
|
||||
button will only return files that were uploaded for that subclass, it won't
|
||||
recognize any other images!
|
||||
</div>
|
||||
|
||||
### Edit uploaded images
|
||||
|
||||
By default the UploadField will let you edit the following fields: *Title,
|
||||
Filename, Owner and Folder*. The `fileEditFields` configuration setting allows
|
||||
you you alter these settings. One way to go about this is create a
|
||||
`getCustomFields` function in your GalleryImage object like this:
|
||||
|
||||
:::php
|
||||
class GalleryImage extends DataExtension {
|
||||
...
|
||||
|
||||
function getCustomFields() {
|
||||
$fields = new FieldList();
|
||||
$fields->push(new TextField('Title', 'Title'));
|
||||
$fields->push(new TextareaField('Description', 'Description'));
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
Then, in your GalleryPage, tell the UploadField to use this function:
|
||||
|
||||
:::php
|
||||
$uploadField->setFileEditFields('getCustomFields');
|
||||
|
||||
In a similar fashion you can use 'setFileEditActions' to set the actions for the
|
||||
editform, or 'fileEditValidator' to determine the validator (eg RequiredFields).
|
||||
|
||||
### Configuration Reference
|
||||
|
||||
- `setAllowedMaxFileNumber`: (int) php validation of allowedMaxFileNumber
|
||||
only works when a db relation is available, set to null to allow
|
||||
unlimited if record has a has_one and allowedMaxFileNumber is null, it will be set to 1
|
||||
- `setAllowedFileExtensions`: (array) List of file extensions allowed
|
||||
- `setAllowedFileCategories`: (array|string) List of types of files allowed.
|
||||
May be any of 'image', 'audio', 'mov', 'zip', 'flash', or 'doc'
|
||||
- `setAutoUpload`: (boolean) Should the field automatically trigger an upload once
|
||||
a file is selected?
|
||||
- `setCanAttachExisting`: (boolean|string) Can the user attach existing files from the library.
|
||||
String values are interpreted as permission codes.
|
||||
- `setCanPreviewFolder`: (boolean|string) Can the user preview the folder files will be saved into?
|
||||
String values are interpreted as permission codes.
|
||||
- `setCanUpload`: (boolean|string) Can the user upload new files, or just select from existing files.
|
||||
String values are interpreted as permission codes.
|
||||
- `setDownloadTemplateName`: (string) javascript template used to display already
|
||||
uploaded files, see javascript/UploadField_downloadtemplate.js
|
||||
- `setFileEditFields`: (FieldList|string) FieldList $fields or string $name
|
||||
(of a method on File to provide a fields) for the EditForm (Example: 'getCMSFields')
|
||||
- `setFileEditActions`: (FieldList|string) FieldList $actions or string $name
|
||||
(of a method on File to provide a actions) for the EditForm (Example: 'getCMSActions')
|
||||
- `setFileEditValidator`: (string) Validator (eg RequiredFields) or string $name
|
||||
(of a method on File to provide a Validator) for the EditForm (Example: 'getCMSValidator')
|
||||
- `setOverwriteWarning`: (boolean) Show a warning when overwriting a file.
|
||||
- `setPreviewMaxWidth`: (int)
|
||||
- `setPreviewMaxHeight`: (int)
|
||||
- `setTemplateFileButtons`: (string) Template name to use for the file buttons
|
||||
- `setTemplateFileEdit`: (string) Template name to use for the file edit form
|
||||
- `setUploadTemplateName`: (string) javascript template used to display uploading
|
||||
files, see javascript/UploadField_uploadtemplate.js
|
||||
- `setCanPreviewFolder`: (boolean|string) Is the upload folder visible to uploading users?
|
||||
String values are interpreted as permission codes.
|
||||
|
||||
Certain default values for the above can be configured using the YAML config system.
|
||||
|
||||
:::yaml
|
||||
UploadField:
|
||||
defaultConfig:
|
||||
autoUpload: true
|
||||
allowedMaxFileNumber:
|
||||
canUpload: true
|
||||
canAttachExisting: 'CMS_ACCESS_AssetAdmin'
|
||||
canPreviewFolder: true
|
||||
previewMaxWidth: 80
|
||||
previewMaxHeight: 60
|
||||
uploadTemplateName: 'ss-uploadfield-uploadtemplate'
|
||||
downloadTemplateName: 'ss-uploadfield-downloadtemplate'
|
||||
overwriteWarning: true # Warning before overwriting existing file (only relevant when Upload: replaceFile is true)
|
||||
|
||||
The above settings can also be set on a per-instance basis by using `setConfig` with the appropriate key.
|
||||
|
||||
You can also configure the underlying `[api:Upload]` class, by using the YAML config system.
|
||||
|
||||
:::yaml
|
||||
Upload:
|
||||
# Globally disables automatic renaming of files and displays a warning before overwriting an existing file
|
||||
replaceFile: true
|
||||
uploads_folder: 'Uploads'
|
||||
|
||||
## Using the UploadField in a frontend form
|
||||
|
||||
The UploadField can be used in a frontend form, given that sufficient attention is given
|
||||
to the permissions granted to non-authorised users.
|
||||
|
||||
By default Image::canDelete and Image::canEdit do not require admin privileges, so
|
||||
make sure you override the methods in your Image extension class.
|
||||
|
||||
For instance, to generate an upload form suitable for saving images into a user-defined
|
||||
gallery the below code could be used:
|
||||
|
||||
:::php
|
||||
|
||||
// In GalleryPage.php
|
||||
class GalleryPage extends Page {}
|
||||
class GalleryPage_Controller extends Page_Controller {
|
||||
private static $allowed_actions = array('Form');
|
||||
public function Form() {
|
||||
$fields = new FieldList(
|
||||
new TextField('Title', 'Title', null, 255),
|
||||
$field = new UploadField('Images', 'Upload Images')
|
||||
);
|
||||
$field->setCanAttachExisting(false); // Block access to Silverstripe assets library
|
||||
$field->setCanPreviewFolder(false); // Don't show target filesystem folder on upload field
|
||||
$field->relationAutoSetting = false; // Prevents the form thinking the GalleryPage is the underlying object
|
||||
$actions = new FieldList(new FormAction('submit', 'Save Images'));
|
||||
return new Form($this, 'Form', $fields, $actions, null);
|
||||
}
|
||||
|
||||
public function submit($data, Form $form) {
|
||||
$gallery = new Gallery();
|
||||
$form->saveInto($gallery);
|
||||
$gallery->write();
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
// In Gallery.php
|
||||
class Gallery extends DataObject {
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar(255)'
|
||||
);
|
||||
|
||||
private static $many_many = array(
|
||||
'Images' => 'Image'
|
||||
);
|
||||
}
|
||||
|
||||
// In ImageExtension.php
|
||||
class ImageExtension extends DataExtension {
|
||||
|
||||
private static $belongs_many_many = array(
|
||||
'Gallery' => 'Gallery'
|
||||
);
|
||||
|
||||
function canEdit($member) {
|
||||
// This part is important!
|
||||
return Permission::check('ADMIN');
|
||||
}
|
||||
}
|
||||
Image::add_extension('ImageExtension');
|
@ -1,70 +0,0 @@
|
||||
# Moving from SilverStripe 2 to SilverStripe 3
|
||||
|
||||
These are the main changes to the SiverStripe 3 template language.
|
||||
|
||||
## Control blocks: Loops vs. Scope
|
||||
|
||||
The `<% control $var %>...<% end_control %>` in SilverStripe prior to version 3 has two different meanings. Firstly, if the control variable is a collection (e.g. DataList), then `<% control %>` iterates over that set. If it's a non-iteratable object, however, `<% control %>` introduces a new scope, which is used to render the inner template code. This dual-use is confusing to some people, and doesn't allow a collection of objects to be used as a scope.
|
||||
|
||||
In SilverStripe 3, the first usage (iteration) is replaced by `<% loop $var %>`. The second usage (scoping) is replaced by `<% with $var %>`
|
||||
|
||||
## Literals in Expressions
|
||||
|
||||
Prior to SilverStripe 3, literal values can appear in certain parts of an expression. For example, in the expression `<% if mydinner=kipper %>`, `mydinner` is treated as a property or method on the page or controller, and `kipper` is treated as a literal. This is fairly limited in use.
|
||||
|
||||
Literals can now be quoted, so that both literals and non-literals can be used in contexts where only literals were allowed before. This makes it possible to write the following:
|
||||
|
||||
* `<% if $mydinner=="kipper" %>...` which compares to the literal "kipper"
|
||||
* `<% if $mydinner==$yourdinner %>...` which compares to another property or method on the page called `yourdinner`
|
||||
|
||||
Certain forms that are currently used in SilverStripe 2.x are still supported in SilverStripe 3 for backwards compatibility:
|
||||
|
||||
* `<% if mydinner==yourdinner %>...` is still interpreted as `mydinner` being a property or method, and `yourdinner` being a literal. It is strongly recommended to change to the new syntax in new implementations. The 2.x syntax is likely to be deprecated in the future.
|
||||
|
||||
Similarly, in SilverStripe 2.x, method parameters are treated as literals: `MyMethod(foo)` is now equivalent to `$MyMethod("foo")`. `$MyMethod($foo)` passes a variable to the method, which is only supported in SilverStripe 3.
|
||||
|
||||
## Method Parameters
|
||||
|
||||
Methods can now take an arbitrary number of parameters:
|
||||
|
||||
$MyMethod($foo,"active", $CurrentMember.FirstName)
|
||||
|
||||
Parameter values can be arbitrary expressions, including a mix of literals, variables, and even other method calls.
|
||||
|
||||
## Less sensitivity around spaces
|
||||
|
||||
Within a tag, a single space is equivalent to multiple consequetive spaces. e.g.
|
||||
|
||||
<% if $Foo %>
|
||||
|
||||
is equivalent to
|
||||
|
||||
<% if $Foo %>
|
||||
|
||||
|
||||
## Removed view-specific accessors
|
||||
|
||||
Several methods in ViewableData that were originally added to expose values to the template language were moved,
|
||||
in order to stop polluting the namespace. These were sometimes called by project-specific PHP code too, and that code
|
||||
will need re-working.
|
||||
|
||||
#### Globals
|
||||
|
||||
Some of these methods were wrappers which simply called other static methods. These can simply be replaced with a call
|
||||
to the wrapped method. The list of these methods is:
|
||||
|
||||
- CurrentMember() -> Member::currentUser()
|
||||
- getSecurityID() -> SecurityToken::inst()->getValue()
|
||||
- HasPerm($code) -> Permission::check($code)
|
||||
- BaseHref() -> Director::absoluteBaseURL()
|
||||
- AbsoluteBaseURL() -> Director::absoluteBaseURL()
|
||||
- IsAjax() -> Director::is_ajax()
|
||||
- i18nLocale() -> i18n::get_locale()
|
||||
- CurrentPage() -> Controller::curr()
|
||||
|
||||
#### Scope-exposing
|
||||
|
||||
Some of the removed methods exposed access to the various scopes. These currently have no replacement. The list of
|
||||
these methods is:
|
||||
|
||||
- Top
|
@ -2,3 +2,51 @@ title: Upgrading
|
||||
introduction: Keep your SilverStripe installations up to date with the latest fixes, security patches and new features.
|
||||
|
||||
# Upgrading
|
||||
|
||||
SilverStripe Applications should be kept up to date with the latest security releases. Usually an update or upgrade to
|
||||
your SilverStripe installation means overwriting files, flushing the cache and updating your database-schema.
|
||||
|
||||
<div class="info" markdown="1">
|
||||
See our [upgrade notes and changelogs](/changelogs) for release-specific information.
|
||||
</div>
|
||||
|
||||
## Composer
|
||||
|
||||
For projects managed through Composer, check the version defined in your `composer.json` file. Update the version
|
||||
constraints if required and update composer.
|
||||
|
||||
:::bash
|
||||
composer update
|
||||
|
||||
## Manual
|
||||
|
||||
* Check if any modules (e.g. blog or forum) in your installation are incompatible and need to be upgraded as well
|
||||
* Backup your database content
|
||||
* Backup your webroot files
|
||||
* Download the new release and uncompress it to a temporary folder
|
||||
* Leave custom folders like *mysite* or *themes* in place.
|
||||
* Identify system folders in your webroot (`cms`, `framework` and any additional modules).
|
||||
* Delete existing system folders (or move them outside of your webroot)
|
||||
* Extract and replace system folders from your download (Deleting instead of "copying over" existing folders ensures that files removed from the new SilverStripe release are not persisting in your installation)
|
||||
* Visit http://yoursite.com/dev/build/?flush=1 to rebuild the website database.
|
||||
* Check if you need to adapt your code to changed PHP APIs
|
||||
* Check if you have overwritten any core templates or styles which might need an update.
|
||||
|
||||
<div class="warning" markdown="1">
|
||||
Never update a website on the live server without trying it on a development copy first.
|
||||
</div>
|
||||
|
||||
## Decision Helpers
|
||||
|
||||
How easy will it be to update my project? It's a fair question, and sometimes a difficult one to answer.
|
||||
|
||||
* "Micro" releases (x.y.z) are explicitly backwards compatible, "minor" and "major" releases can deprecate features and change APIs (see our [/misc/release-process](release process) for details)
|
||||
* If you've made custom branches of SilverStripe core, or any thirdparty module, it's going to be harder to upgrade.
|
||||
* The more custom features you have, the harder it will be to upgrade. You will have to re-test all of those features, and adapt to API changes in core.
|
||||
* Customizations of a well defined type - such as custom page types or custom blog widgets - are going to be easier to upgrade than customisations that modify deep system internals like rewriting SQL queries.
|
||||
|
||||
## Related
|
||||
|
||||
* [Release Announcements](http://groups.google.com/group/silverstripe-announce/)
|
||||
* [Blog posts about releases on silverstripe.org](http://silverstripe.org/blog/tag/release)
|
||||
* [Release Process](../contributing/release_process)
|
||||
|
Loading…
Reference in New Issue
Block a user