diff --git a/docs/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md b/docs/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md
new file mode 100644
index 000000000..d07b0e282
--- /dev/null
+++ b/docs/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md
@@ -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
+ '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.
+
+
+
+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).
+
+
+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
+ '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'
+
+
+Provided `filter` values are automatically escaped and do not require any escaping.
+
+
+## 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()`.
+
+
+Because `filterByCallback()` has to run in PHP, it will always return an `ArrayList`
+
+
+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);
+
+
+Note that the `limit` argument order is different from a MySQL LIMIT clause.
+
+
+### 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");
+
+
+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.
+
+
+### 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
+ 'Active',
+ );
+ }
+
+
+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.
+
+
+## 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
+ '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]
diff --git a/docs/en/02_Developer_Guides/00_Model/01_Page_Types.md b/docs/en/02_Developer_Guides/00_Model/01_Page_Types.md
deleted file mode 100644
index ed391f5f1..000000000
--- a/docs/en/02_Developer_Guides/00_Model/01_Page_Types.md
+++ /dev/null
@@ -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).
diff --git a/docs/en/02_Developer_Guides/00_Model/02_Relations.md b/docs/en/02_Developer_Guides/00_Model/02_Relations.md
new file mode 100644
index 000000000..687e99402
--- /dev/null
+++ b/docs/en/02_Developer_Guides/00_Model/02_Relations.md
@@ -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 ` => `.
+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 "``ID", in the example below this would be
+"TeamID" on the "Player"-table.
+
+ :::php
+ '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 ""``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`.
+
+
+Please specify a $has_one-relationship on the related child-class as well, in order to have the necessary accessors
+available on both ends.
+
+
+ :::php
+ '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
+ "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 `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
+ '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.
+
+
+Please specify a $belongs_many_many-relationship on the related class as well, in order to have the necessary accessors
+available on both ends.
+
+
+ :::php
+ "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
+ "Player"
+ );
+
+ public function ActivePlayers() {
+ return $this->Players()->filter('Status', 'Active');
+ }
+ }
+
+
+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 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]
diff --git a/docs/en/02_Developer_Guides/00_Model/02_SiteTree.md b/docs/en/02_Developer_Guides/00_Model/02_SiteTree.md
deleted file mode 100644
index a8046ea9d..000000000
--- a/docs/en/02_Developer_Guides/00_Model/02_SiteTree.md
+++ /dev/null
@@ -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", '')->First();
- // right
- $mypage = SiteTree::get_by_link('');
-
-### 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).
diff --git a/docs/en/02_Developer_Guides/00_Model/03_DataObject.md b/docs/en/02_Developer_Guides/00_Model/03_DataObject.md
deleted file mode 100644
index 24aeb2d74..000000000
--- a/docs/en/02_Developer_Guides/00_Model/03_DataObject.md
+++ /dev/null
@@ -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(
- '' => true
- );
-
- # Advanced
- private static $indexes = array(
- '' => array('type' => '', 'value' => '""')
- );
-
- # SQL
- private static $indexes = array(
- '' => 'unique("")'
- );
-
-The `` 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 `` 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]`
diff --git a/docs/en/02_Developer_Guides/00_Model/03_Lists.md b/docs/en/02_Developer_Guides/00_Model/03_Lists.md
new file mode 100644
index 000000000..b87a86bce
--- /dev/null
+++ b/docs/en/02_Developer_Guides/00_Model/03_Lists.md
@@ -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]
\ No newline at end of file
diff --git a/docs/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md b/docs/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md
index 280284f46..b077740a4 100644
--- a/docs/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md
+++ b/docs/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md
@@ -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
+ '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
+ 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
+ '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
+ '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.
-
-
-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.
-
-
-## 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`.
+
+You can disable this auto-escaping by using the `$MyField.RAW` escaping hints, or explicitly request escaping of HTML
+content via `$MyHtmlField.XML`.
+
-## Related
+## Overloading
- * ["datamodel" topic](/topics/datamodel)
- * ["security" topic](/topics/security)
\ No newline at end of file
+"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``" or "set``".
+
+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
+ "Enum(array('Active', 'Injured', 'Retired'))"
+ );
+
+ public function getStatus() {
+ return (!$this->obj("Birthday")->InPast()) ? "Unborn" : $this->dbObject('Status')->Value();
+ }
+
+
+## API Documentation
+
+* [api:DataObject]
+* [api:DBField]
\ No newline at end of file
diff --git a/docs/en/02_Developer_Guides/00_Model/05_Data_Model_and_ORM.md b/docs/en/02_Developer_Guides/00_Model/05_Data_Model_and_ORM.md
deleted file mode 100644
index 012facf2a..000000000
--- a/docs/en/02_Developer_Guides/00_Model/05_Data_Model_and_ORM.md
+++ /dev/null
@@ -1,1117 +0,0 @@
-# Datamodel
-
-SilverStripe uses an [object-relational model](http://en.wikipedia.org/wiki/Object-relational_model)
-that assumes the following connections:
-
-* Each database-table maps to a PHP class
-* Each database-row maps to a PHP object
-* Each database-column maps to a property on a PHP object
-
-All data tables in SilverStripe are defined as subclasses of `[api:DataObject]`.
-
-Inheritance is supported in the data model: separate tables will be linked
-together, the data spread across these tables. The mapping and saving/loading
-logic is handled by SilverStripe, you don't need to worry about writing SQL most
-of the time.
-
-Most of the ORM customizations are possible through [PHP5 Object
-Overloading](http://www.onlamp.com/pub/a/php/2005/06/16/overloading.html)
-handled in the `[api:Object]`-class.
-
-See [database-structure](/reference/database-structure) for in-depth information
-on the database-schema and the ["sql queries" topic](/reference/sqlquery) in
-case you need to drop down to the bare metal.
-
-## Generating the Database Schema
-
-The SilverStripe database-schema is generated automatically by visiting the URL.
-`http://localhost/dev/build`
-
-
-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.
-
-
-## Querying Data
-
-Every query to data starts with a `DataList::create()` or `::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 "$member->FirstName $member->Surname
";
- }
-
-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)
-
-
-See [the security topic](/topics/security#parameterised-queries) for details on safe database querying and why parameterised queries
-are so necessary here.
-
-
-#### SQL WHERE Predicates with Parameters
-
-If using `DataObject::get()` (which returns a `DataList` instance) you can specify a WHERE clause fragment
-(that will be combined with other filters using AND) with the `where()` method, or `whereAny()` to add a list
-of clauses combined with OR.
-
-Placeholders within a predicate are denoted by the question mark symbol, and should not be quoted.
-
-For example:
-
- :::php
- $members = Member::get()->where(array('"FirstName" = ?' => 'Sam'));
-
-If using `SQLSelect` you should use `addWhere`, `setWhere`, `addWhereAny`, or `setWhereAny` to modify the query.
-
-Using the parameterised query syntax you can either provide a single variable as a parameter, an array of parameters
-if the SQL has multiple value placeholders, or simply pass an indexed array of strings for literal SQL.
-
-Although parameters can be escaped and directly inserted into the SQL condition (See `Convert::raw2sql()'),
-the parameterised syntax is the preferred method of declaring conditions on a query.
-
-Column names must still be double quoted, and for consistency and compatibility with other code, should also
-be prefixed with the table name.
-
-E.g.
-
- :::php
- where(array(
- '"Table"."Column" = ?' => $column,
- '"Table"."Name" = ?' => $value
- ));
-
- // Shorthand for simple column comparison (as above), omitting the '?'
- // These will each be expanded internally to '"Table"."Column" = ?'
- $query = $query->where(array(
- '"Table"."Column"' => $column,
- '"Table"."Name"' => $value
- ));
-
- // Multiple predicates, some with multiple parameters.
- // The parameters should ideally not be an associative array.
- $query = $query->where(array(
- '"Table"."ColumnOne" = ? OR "Table"."ColumnTwo" != ?' => array(1, 4),
- '"Table"."ID" != ?' => $value
- ));
-
- // Multiple predicates, each with explicitly typed parameters.
- //
- // The purpose of this syntax is to provide not only parameter values, but
- // to also instruct the database connector on how to treat this value
- // internally (subject to the database API supporting this feature).
- //
- // SQLQuery distinguishes these from predicates with multiple parameters
- // by checking for the 'value' key in any array parameter given
- $query = $query->whereAny(array(
- '"Table"."Column"' => array(
- 'value' => $value,
- 'type' => 'string' // or any php type
- ),
- '"Table"."HasValue"' => array(
- 'value' => 0,
- 'type' => 'boolean'
- )
- ));
-
-#### Run-Time Evaluated Conditions with SQLConditionGroup
-
-Conditional expressions and groups may be encapsulated within a class (implementing
-the SQLConditionGroup interface) and evaluated at the time of execution.
-
-This is useful for conditions which may be placed into a query before the details
-of that condition are fully specified.
-
-E.g.
-
- :::php
- field} < RAND()";
- }
- }
-
- $query = SQLSelect::create()
- ->setFrom('"MyObject"')
- ->setWhere($condition = new RandomCondition());
- $condition->field = '"Score"';
- $items = $query->execute();
-
-#### Direct SQL Predicate
-
-Conditions can be a literal piece of SQL which doesn't involve any parameters or values
-at all, or can using safely SQL-encoded values, as it was originally.
-
-
-In nearly every instance it's preferrable to use the parameterised syntax, especially dealing
-with variable parameters, even if those values were not submitted by the user.
-See [the security topic](/topics/security#parameterised-queries) for details.
-
-
-For instance, the following are all valid ways of adding SQL conditions directly to a query
-
- :::php
- addWhere("\"Column\" = 'Value'");
-
- // multiple predicates as an array
- $query->addWhere(array("\"Column\" = 'Value'", "\"Column\" != 'Value'"));
-
- // Shorthand for the above using argument expansion
- $query->addWhere("\"Column\" = 'Value'", "\"Column\" != 'Value'");
-
- // Literal SQL condition
- $query->addWhere('"Created" > NOW()"');
-
-#### Joining
-
-You can specify a join with the innerJoin and leftJoin methods. Both of these
-methods have the same arguments:
-
- * The name of the table to join to
- * The filter clause for the join
- * An optional alias
- * Priority (to allow you to later sort joins)
- * An optional list of parameters (in case you wish to use a parameterised subselect).
-
-For example:
-
- :::php
- // Without an alias
- $members = Member::get()
- ->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
-
- $members = Member::get()
- ->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel");
-
- // With a subselect
- $members = Member::get()
- ->innerJoin(
- '(SELECT "MemberID", COUNT("ID") AS "Count" FROM "Member_Likes" GROUP BY "MemberID" HAVING "Count" >= ?)',
- '"Likes"."MemberID" = "Member"."ID"',
- "Likes",
- 20,
- array($threshold)
- );
-
-
-Passing a *$join* statement to DataObject::get will filter results further by
-the JOINs performed against the foreign table. **It will NOT return the
-additionally joined data.** The returned *$records* will always be a
-`[api:DataObject]`.
-
-## Properties
-
-
-### Definition
-
-Data is defined in the static variable $db on each class, in the format:
-`` => "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``" or
-"set``".
-
- :::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;
- }
- }
-
-
-**CAUTION:** It is common practice to make sure that pairs of custom
-getters/setter deal with the same data, in a consistent format.
-
-
-
-**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.
-
-
-### 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',
- );
- }
-
-
-Note: Alternatively you can set defaults directly in the database-schema (rather
-than the object-model). See [data-types](data-types) for details.
-
-
-### 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
-` => `.
-
-### has_one
-
-A 1-to-1 relation creates a database-column called "``ID", in
-the example below this would be "TeamID" on the "Player"-table.
-
- :::php
- // access with $myPlayer->Team()
- class Player extends DataObject {
-
- private static $has_one = array(
- "Team" => "Team",
- );
- }
-
-SilverStripe's `[api:SiteTree]` base-class for content-pages uses a 1-to-1
-relationship to link to its parent element in the tree:
-
- :::php
- // access with $mySiteTree->Parent()
- class SiteTree extends DataObject {
- private static $has_one = array(
- "Parent" => "SiteTree",
- );
- }
-
-A has_one can also be polymorphic, which allows any type of object to be associated.
-This is useful where there could be many use cases for a particular data structure.
-
-An additional column is created called "``Class", which along
-with the ID column identifies the object.
-
-To specify that a has_one relation is polymorphic set the type to 'DataObject'.
-Ideally, the associated has_many (or belongs_to) should be specified with dot notation.
-
- ::php
-
- class Player extends DataObject {
- private static $has_many = array(
- "Fans" => "Fan.FanOf"
- );
- }
-
- class Team extends DataObject {
- private static $has_many = array(
- "Fans" => "Fan.FanOf"
- );
- }
-
- // Type of object returned by $fan->FanOf() will vary
- class Fan extends DataObject {
-
- // Generates columns FanOfID and FanOfClass
- private static $has_one = array(
- "FanOf" => "DataObject"
- );
- }
-
-
-Note: The use of polymorphic relationships can affect query performance, especially
-on joins, and also increases the complexity of the database and necessary user code.
-They should be used sparingly, and only where additional complexity would otherwise
-be necessary. E.g. Additional parent classes for each respective relationship, or
-duplication of code.
-
-
-### has_many
-
-Defines 1-to-many joins. A database-column named ""``ID""
-will to be created in the child-class.
-
-
-**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.
-
-
- :::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
-`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.
-
-
-**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.
-
-
- :::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;
- }
- }
-
-
-**Tip:** If you decide to add unique or other indexes to your model via
-`static $indexes`, see [DataObject](/reference/dataobject) for details.
-
-
-## 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();
- }
- }
-
-
-
-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.
-
-
-### 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);
diff --git a/docs/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md b/docs/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md
new file mode 100644
index 000000000..db8f0f61f
--- /dev/null
+++ b/docs/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md
@@ -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
+ "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
+ "Team"
+ );
+
+ public function onBeforeDelete() {
+ if(!Permission::check('PLAYER_DELETE')) {
+ Security::permissionFailure($this);
+ exit();
+ }
+
+ parent::onBeforeDelete();
+ }
+ }
+
+
+
+
+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.
+
diff --git a/docs/en/02_Developer_Guides/00_Model/06_Database_Structure.md b/docs/en/02_Developer_Guides/00_Model/06_Database_Structure.md
deleted file mode 100644
index 61d7a37b2..000000000
--- a/docs/en/02_Developer_Guides/00_Model/06_Database_Structure.md
+++ /dev/null
@@ -1,114 +0,0 @@
-# Database Structure
-
-SilverStripe is currently hard-coded to use a fix mapping between data-objects and the underlying database structure -
-opting for "convention over configuration". This page details what that database structure is.
-
-## Base tables
-
-Each direct sub-class of `[api:DataObject]` will have its own table.
-
-The following fields are always created.
-
-* ID: Primary Key
-* ClassName: An enumeration listing this data-class and all of its subclasses.
-* Created: A date/time field set to the creation date of this record
-* LastEdited: A date/time field set to the date this record was last edited
-
-Every object of this class **or any of its subclasses** will have an entry in this table
-
-### Extra Fields
-
-* Every field listed in the data object's **$db** array will be included in this table.
-* For every relationship listed in the data object's **$has_one** array, there will be an integer field included in the
-table. This will contain the ID of the data-object being linked to. The database field name will be of the form
-"(relationship-name)ID", for example, ParentID. For polymorphic has_one relationships, there is an additional
-"(relationship-name)Class" field to identify the class this ID corresponds to. See [datamodel](/topics/datamodel#has_one).
-
-### ID Generation
-
-When a new record is created, we don't use the database's built-in auto-numbering system. Instead, we generate a new ID
-by adding 1 to the current maximum ID.
-
-## Subclass tables
-
-At SilverStripe's heart is an object-relational model. And a component of object-oriented data is **inheritance**.
-Unfortunately, there is no native way of representing inheritance in a relational database. What we do is store the
-data sub-classed objects across **multiple tables**.
-
-For example, suppose we have the following set of classes:
-
-* Class `[api:SiteTree]` extends `[api:DataObject]`: Title, Content fields
-* Class `[api:Page]` extends `[api:SiteTree]`: Abstract field
-* Class NewsSection extends `[api:SiteTree]`: *No special fields*
-* Class NewsArticle extend `[api:Page]`: ArticleDate field
-
-The data for the following classes would be stored across the following tables:
-
-* `[api:SiteTree]`
- * ID: Int
- * ClassName: Enum('SiteTree', 'Page', 'NewsArticle')
- * Created: Datetime
- * LastEdited: Datetime
- * Title: Varchar
- * Content: Text
-* `[api:Page]`
- * ID: Int
- * Abstract: Text
-* NewsArticle
- * ID: Int
- * ArticleDate: Date
-
-The way it works is this:
-
-* "Base classes" are direct sub-classes of `[api:DataObject]`. They are always given a table, whether or not they have
-special fields. This is called the "base table"
-* The base table's ClassName field is set to class of the given record. It's an enumeration of all possible
-sub-classes of the base class (including the base class itself)
-* Each sub-class of the base object will also be given its own table, *as long as it has custom fields*. In the
-example above, NewsSection didn't have its own data and so an extra table would be redundant.
-* In all the tables, ID is the primary key. A matching ID number is used for all parts of a particular record:
-record #2 in Page refers to the same object as record #2 in `[api:SiteTree]`.
-
-To retrieve a news article, SilverStripe joins the `[api:SiteTree]`, `[api:Page]` and NewsArticle tables by their ID fields. We use a
-left-join for robustness; if there is no matching record in Page, we can return a record with a blank Article field.
-
-## Staging and versioning
-
-[todo]
-
-## Schema auto-generation
-
-SilverStripe has a powerful tool for automatically building database schemas. We've designed it so that you should never have to build them manually.
-
-To access it, visit http://localhost/dev/build?flush=1. This script will analyze the existing schema, compare it to what's required by your data classes, and alter the schema as required.
-
-Put the ?flush=1 on the end if you've added PHP files, so that the rest of the system will find these new classes.
-
-It will perform the following changes:
-
- * Create any missing tables
- * Create any missing fields
- * Create any missing indexes
- * Alter the field type of any existing fields
- * Rename any obsolete tables that it previously created to _obsolete_(tablename)
-
-It **won't** do any of the following
-
- * Deleting tables
- * Deleting fields
- * Rename any tables that it doesn't recognize - so other applications can co-exist in the same database, as long as their table names don't match a SilverStripe data class.
-
-
-## Related code
-
-The information documented in this page is reflected in a few places in the code:
-
-* `[api:DataObject]`
- * requireTable() is responsible for specifying the required database schema
- * instance_get() and instance_get_one() are responsible for generating the database queries for selecting data.
- * write() is responsible for generating the database queries for writing data.
-* `[api:Versioned]`
- * augmentWrite() is responsible for altering the normal database writing operation to handle versions.
- * augmentQuery() is responsible for altering the normal data selection queries to support versions.
- * augmentDatabase() is responsible for specifying the altered database schema to support versions.
-* `[api:MySQLDatabase]`: handles the mechanics of updating the database to have the required schema.
diff --git a/docs/en/02_Developer_Guides/00_Model/06_SearchFilters.md b/docs/en/02_Developer_Guides/00_Model/06_SearchFilters.md
new file mode 100644
index 000000000..41724d67e
--- /dev/null
+++ b/docs/en/02_Developer_Guides/00_Model/06_SearchFilters.md
@@ -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]
\ No newline at end of file
diff --git a/docs/en/02_Developer_Guides/00_Model/07_Permissions.md b/docs/en/02_Developer_Guides/00_Model/07_Permissions.md
new file mode 100644
index 000000000..49f8d4082
--- /dev/null
+++ b/docs/en/02_Developer_Guides/00_Model/07_Permissions.md
@@ -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()`).
+
+
+By default, all `DataObject` subclasses can only be edited, created and viewed by users with the 'ADMIN' permission
+code.
+
+
+ :::php
+
+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.
+
+
+## API Documentation
+
+* [api:DataObject]
+* [api:Permission]
\ No newline at end of file
diff --git a/docs/en/02_Developer_Guides/00_Model/07_SQL_Query.md b/docs/en/02_Developer_Guides/00_Model/07_SQL_Query.md
deleted file mode 100644
index 9d00c4fcb..000000000
--- a/docs/en/02_Developer_Guides/00_Model/07_SQL_Query.md
+++ /dev/null
@@ -1,271 +0,0 @@
-# SQL Select
-
-## Introduction
-
-An object representing a SQL select query, which can be serialized into a SQL statement.
-It is easier to deal with object-wrappers than string-parsing a raw SQL-query.
-This object is used by the SilverStripe ORM internally.
-
-Dealing with low-level SQL is not encouraged, since the ORM provides
-powerful abstraction APIs (see [datamodel](/topics/datamodel).
-Starting with SilverStripe 3, records in collections are lazy loaded,
-and these collections have the ability to run efficient SQL
-such as counts or returning a single column.
-
-For example, if you want to run a simple `COUNT` SQL statement,
-the following three statements are functionally equivalent:
-
- :::php
- // Through raw SQL
- $count = DB::query('SELECT COUNT(*) FROM "Member"')->value();
- // Through SQLSelect abstraction layer
- $query = new SQLSelect();
- $count = $query->setFrom('Member')->setSelect('COUNT(*)')->value();
- // Through the ORM
- $count = Member::get()->count();
-
-If you do use raw SQL, you'll run the risk of breaking
-various assumptions the ORM and code based on it have:
-
-* Custom getters/setters (object property can differ from database column)
-* DataObject hooks like onBeforeWrite() and onBeforeDelete()
-* Automatic casting
-* Default values set through objects
-* Database abstraction
-
-We'll explain some ways to use *SELECT* with the full power of SQL,
-but still maintain a connection to the ORM where possible.
-
-
-Please read our ["security" topic](/topics/security) to find out
-how to properly prepare user input and variables for use in queries
-
-
-## Usage
-
-### SELECT
-
-Selection can be done by creating an instance of `SQLSelect`, which allows
-management of all elements of a SQL SELECT query, including columns, joined tables,
-conditional filters, grouping, limiting, and sorting.
-
-E.g.
-
- :::php
- setFrom('Player');
- $sqlSelect->selectField('FieldName', 'Name');
- $sqlSelect->selectField('YEAR("Birthday")', 'Birthyear');
- $sqlSelect->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"');
- $sqlSelect->addWhere(array('YEAR("Birthday") = ?' => 1982));
- // $sqlSelect->setOrderBy(...);
- // $sqlSelect->setGroupBy(...);
- // $sqlSelect->setHaving(...);
- // $sqlSelect->setLimit(...);
- // $sqlSelect->setDistinct(true);
-
- // Get the raw SQL (optional) and parameters
- $rawSQL = $sqlSelect->sql($parameters);
-
- // Execute and return a Query object
- $result = $sqlSelect->execute();
-
- // Iterate over results
- foreach($result as $row) {
- echo $row['BirthYear'];
- }
-
-The result of `SQLSelect::execute()` is an array lightly wrapped in a database-specific subclass of `[api:SS_Query]`.
-This class implements the *Iterator*-interface, and provides convenience-methods for accessing the data.
-
-### DELETE
-
-Deletion can be done either by calling `DB::query`/`DB::prepared_query` directly,
-by creating a `SQLDelete` object, or by transforming a `SQLSelect` into a `SQLDelete`
-object instead.
-
-For example, creating a `SQLDelete` object
-
- :::php
- setFrom('"SiteTree"')
- ->setWhere(array('"SiteTree"."ShowInMenus"' => 0));
- $query->execute();
-
-Alternatively, turning an existing `SQLSelect` into a delete
-
- :::php
- setFrom('"SiteTree"')
- ->setWhere(array('"SiteTree"."ShowInMenus"' => 0))
- ->toDelete();
- $query->execute();
-
-Directly querying the database
-
- :::php
- value pairs,
- but also supports SQL expressions as values if necessary.
- * `setAssignments` - Replaces all existing assignments with the specified list
- * `getAssignments` - Returns all currently given assignments, as an associative array
- in the format `array('Column' => array('SQL' => array('parameters)))`
- * `assign` - Singular form of addAssignments, but only assigns a single column value.
- * `assignSQL` - Assigns a column the value of a specified SQL expression without parameters
- `assignSQL('Column', 'SQL)` is shorthand for `assign('Column', array('SQL' => array()))`
-
-SQLUpdate also includes the following api methods:
-
- * `clear` - Clears all assignments
- * `getTable` - Gets the table to update
- * `setTable` - Sets the table to update. This should be ANSI quoted.
- E.g. `$query->setTable('"SiteTree"');`
-
-SQLInsert also includes the following api methods:
- * `clear` - Clears all rows
- * `clearRow` - Clears all assignments on the current row
- * `addRow` - Adds another row of assignments, and sets the current row to the new row
- * `addRows` - Adds a number of arrays, each representing a list of assignment rows,
- and sets the current row to the last one.
- * `getColumns` - Gets the names of all distinct columns assigned
- * `getInto` - Gets the table to insert into
- * `setInto` - Sets the table to insert into. This should be ANSI quoted.
- E.g. `$query->setInto('"SiteTree"');`
-
-E.g.
-
- :::php
- where(array('ID' => 3));
-
- // assigning a list of items
- $update->addAssignments(array(
- '"Title"' => 'Our Products',
- '"MenuTitle"' => 'Products'
- ));
-
- // Assigning a single value
- $update->assign('"MenuTitle"', 'Products');
-
- // Assigning a value using parameterised expression
- $title = 'Products';
- $update->assign('"MenuTitle"', array(
- 'CASE WHEN LENGTH("MenuTitle") > LENGTH(?) THEN "MenuTitle" ELSE ? END' =>
- array($title, $title)
- ));
-
- // Assigning a value using a pure SQL expression
- $update->assignSQL('"Date"', 'NOW()');
-
- // Perform the update
- $update->execute();
-
-In addition to assigning values, the SQLInsert object also supports multi-row
-inserts. For database connectors and API that don't have multi-row insert support
-these are translated internally as multiple single row inserts.
-
-For example,
-
- :::php
- addRows(array(
- array('"Title"' => 'Home', '"Content"' => 'This is our home page
'),
- array('"Title"' => 'About Us', '"ClassName"' => 'AboutPage')
- ));
-
- // Adjust an assignment on the last row
- $insert->assign('"Content"', 'This is about us
');
-
- // Add another row
- $insert->addRow(array('"Title"' => 'Contact Us'));
-
- $columns = $insert->getColumns();
- // $columns will be array('"Title"', '"Content"', '"ClassName"');
-
- $insert->execute();
-
-### Value Checks
-
-Raw SQL is handy for performance-optimized calls,
-e.g. when you want a single column rather than a full-blown object representation.
-
-Example: Get the count from a relationship.
-
- :::php
- $sqlSelect = new SQLSelect();
- $sqlSelect->setFrom('Player');
- $sqlSelect->addSelect('COUNT("Player"."ID")');
- $sqlSelect->addWhere(array('"Team"."ID"' => 99));
- $sqlSelect->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"');
- $count = $sqlSelect->execute()->value();
-
-Note that in the ORM, this call would be executed in an efficient manner as well:
-
- :::php
- $count = $myTeam->Players()->count();
-
-### Mapping
-
-Creates a map based on the first two columns of the query result.
-This can be useful for creating dropdowns.
-
-Example: Show player names with their birth year, but set their birth dates as values.
-
- :::php
- $sqlSelect = new SQLSelect();
- $sqlSelect->setFrom('Player');
- $sqlSelect->setSelect('Birthdate');
- $sqlSelect->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear');
- $map = $sqlSelect->execute()->map();
- $field = new DropdownField('Birthdates', 'Birthdates', $map);
-
-Note that going through SQLSelect is just necessary here
-because of the custom SQL value transformation (`YEAR()`).
-An alternative approach would be a custom getter in the object definition.
-
- :::php
- class Player extends DataObject {
- private static $db = array(
- 'Name' => 'Varchar',
- 'Birthdate' => 'Date'
- );
- function getNameWithBirthyear() {
- return date('y', $this->Birthdate);
- }
- }
- $players = Player::get();
- $map = $players->map('Name', 'NameWithBirthyear');
-
-## Related
-
-* [datamodel](/topics/datamodel)
-* `[api:DataObject]`
-* [database-structure](database-structure)
diff --git a/docs/en/02_Developer_Guides/00_Model/08_SQL_Query.md b/docs/en/02_Developer_Guides/00_Model/08_SQL_Query.md
new file mode 100644
index 000000000..1a92c0839
--- /dev/null
+++ b/docs/en/02_Developer_Guides/00_Model/08_SQL_Query.md
@@ -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();
+
+
+
+The SQLQuery object is used by the SilverStripe ORM internally. By understanding SQLQuery, you can modify the SQL that
+the ORM creates.
+
+
+## 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\'');
+
+### 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]
\ No newline at end of file
diff --git a/docs/en/02_Developer_Guides/00_Model/08_Versioning.md b/docs/en/02_Developer_Guides/00_Model/08_Versioning.md
deleted file mode 100644
index c6741ab9d..000000000
--- a/docs/en/02_Developer_Guides/00_Model/08_Versioning.md
+++ /dev/null
@@ -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 `_versions` tables.
-
- :::php
- $historicalRecord = Versioned::get_version('MyRecord', , );
-
-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 `_versions` table.
-To avoid this, use `[writeWithoutVersion()](api:Versioned->writeWithoutVersion())` instead.
-
-To move a saved version from one stage to another,
-call `[writeToStage()](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(, )`.
-
- :::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");
- }
- }
diff --git a/docs/en/02_Developer_Guides/00_Model/09_Validation.md b/docs/en/02_Developer_Guides/00_Model/09_Validation.md
index 4a80e2d28..045fd0d6d 100644
--- a/docs/en/02_Developer_Guides/00_Model/09_Validation.md
+++ b/docs/en/02_Developer_Guides/00_Model/09_Validation.md
@@ -1 +1,48 @@
-* stub, talk validate()
\ No newline at end of file
+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
+ '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];
diff --git a/docs/en/02_Developer_Guides/00_Model/10_Versioning.md b/docs/en/02_Developer_Guides/00_Model/10_Versioning.md
new file mode 100644
index 000000000..db0b43439
--- /dev/null
+++ b/docs/en/02_Developer_Guides/00_Model/10_Versioning.md
@@ -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")
+
+
+The extension is automatically applied to `SiteTree` class. For more information on extensions see
+[Extending](../extending) and the [Configuration](../configuration) documentation.
+
+
+## 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.
+
+
+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 `_versions` tables.
+
+ :::php
+ $historicalRecord = Versioned::get_version('MyRecord', , );
+
+
+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
+`_versions` table. To avoid this, use `[writeWithoutVersion()](api:Versioned->writeWithoutVersion())` instead.
+
+To move a saved version from one stage to another, call `[writeToStage()](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(, )`.
+
+ :::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.
+
+
+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.
+
+
+## API Documentation
+
+* [api:Versioned]
\ No newline at end of file
diff --git a/docs/en/02_Developer_Guides/00_Model/11_Scaffolding.md b/docs/en/02_Developer_Guides/00_Model/11_Scaffolding.md
new file mode 100644
index 000000000..2a014169f
--- /dev/null
+++ b/docs/en/02_Developer_Guides/00_Model/11_Scaffolding.md
@@ -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
+ '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
+ '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
+ 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
+ '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
+ '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
+ '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
+ '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]
\ No newline at end of file
diff --git a/docs/en/02_Developer_Guides/00_Model/12_Indexes.md b/docs/en/02_Developer_Guides/00_Model/12_Indexes.md
new file mode 100644
index 000000000..145fdcb6b
--- /dev/null
+++ b/docs/en/02_Developer_Guides/00_Model/12_Indexes.md
@@ -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
+ ' => true,
+ '' => array('type' => '', 'value' => '""'),
+ '' => 'unique("")'
+ );
+ }
+
+The `` 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 `` 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
+ 'Varchar',
+ 'MyOtherField' => 'Varchar',
+ );
+
+ private static $indexes = array(
+ 'MyIndexName' => array(
+ 'type' => 'index',
+ 'value' => '"MyField","MyOtherField"'
+ )
+ );
+ }
+
+## API Documentation
+
+* [api:DataObject]
\ No newline at end of file
diff --git a/docs/en/02_Developer_Guides/00_Model/How_To/Dynamic_Default_Fields.md b/docs/en/02_Developer_Guides/00_Model/How_Tos/Dynamic_Default_Fields.md
similarity index 100%
rename from docs/en/02_Developer_Guides/00_Model/How_To/Dynamic_Default_Fields.md
rename to docs/en/02_Developer_Guides/00_Model/How_Tos/Dynamic_Default_Fields.md
diff --git a/docs/en/02_Developer_Guides/00_Model/How_To/Grouping_DataObject_Sets.md b/docs/en/02_Developer_Guides/00_Model/How_Tos/Grouping_DataObject_Sets.md
similarity index 100%
rename from docs/en/02_Developer_Guides/00_Model/How_To/Grouping_DataObject_Sets.md
rename to docs/en/02_Developer_Guides/00_Model/How_Tos/Grouping_DataObject_Sets.md
diff --git a/docs/en/02_Developer_Guides/00_Model/index.md b/docs/en/02_Developer_Guides/00_Model/index.md
index 92412ffb1..475503c6b 100644
--- a/docs/en/02_Developer_Guides/00_Model/index.md
+++ b/docs/en/02_Developer_Guides/00_Model/index.md
@@ -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]
\ No newline at end of file
+## How to's
+
+[CHILDREN Folder="How_Tos"]
\ No newline at end of file
diff --git a/docs/en/02_Developer_Guides/UploadField.md b/docs/en/02_Developer_Guides/UploadField.md
deleted file mode 100644
index 1e44bf4a3..000000000
--- a/docs/en/02_Developer_Guides/UploadField.md
+++ /dev/null
@@ -1,426 +0,0 @@
-title: UploadField
-summary: How to use the UploadField class for uploading assets.
-
-# UploadField
-
-## Introduction
-
-The UploadField will let you upload one or multiple files of all types,
-including images. But that's not all it does - it will also link the
-uploaded file(s) to an existing relation and let you edit the linked files
-as well. That makes it flexible enough to sometimes even replace the Gridfield,
-like for instance in creating and managing a simple gallery.
-
-## Usage
-
-The field can be used in three ways: To upload a single file into a `has_one` relationship,
-or allow multiple files into a `has_many` or `many_many` relationship, or to act as a stand
-alone uploader into a folder with no underlying relation.
-
-## Validation
-
-Although images are uploaded and stored on the filesystem immediately after selection,
-the value (or values) of this field will not be written to any related record until
-the record is saved and successfully validated. However, any invalid records will still
-persist across form submissions until explicitly removed or replaced by the user.
-
-Care should be taken as invalid files may remain within the filesystem until explicitly
-removed.
-
-### Single fileupload
-
-The following example adds an UploadField to a page for single fileupload,
-based on a has_one relation:
-
- :::php
- class GalleryPage extends Page {
-
- private static $has_one = array(
- 'SingleImage' => 'Image'
- );
-
- function getCMSFields() {
-
- $fields = parent::getCMSFields();
-
- $fields->addFieldToTab(
- 'Root.Upload',
- $uploadField = new UploadField(
- $name = 'SingleImage',
- $title = 'Upload a single image'
- )
- );
- return $fields;
- }
- }
-
-The UploadField will autodetect the relation based on it's `name` property, and
-save it into the GalleyPages' `SingleImageID` field. Setting the
-`setAllowedMaxFileNumber` to 1 will make sure that only one image can ever be
-uploaded and linked to the relation.
-
-### Multiple fileupload
-
-Enable multiple fileuploads by using a many_many (or has_many) relation. Again,
-the `UploadField` will detect the relation based on its $name property value:
-
- :::php
- class GalleryPage extends Page {
-
- private static $many_many = array(
- 'GalleryImages' => 'Image'
- );
-
- function getCMSFields() {
-
- $fields = parent::getCMSFields();
-
- $fields->addFieldToTab(
- 'Root.Upload',
- $uploadField = new UploadField(
- $name = 'GalleryImages',
- $title = 'Upload one or more images (max 10 in total)'
- )
- );
- $uploadField->setAllowedMaxFileNumber(10);
-
- return $fields;
- }
- }
- class GalleryPage_Controller extends Page_Controller {
- }
-
- class GalleryImageExtension extends DataExtension {
- private static $belongs_many_many = array('Galleries' => 'GalleryPage);
- }
-
- Image::add_extension('GalleryImageExtension');
-
-
-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.
-
-
-## Configuration
-
-### Overview
-
-The field can either be configured on an instance level with the various
-getProperty and setProperty functions, or globally by overriding the YAML defaults.
-See the [Configuration Reference](uploadfield#configuration-reference) section for possible values.
-
-Example: mysite/_config/uploadfield.yml
-
- after: framework#uploadfield
- ---
- UploadField:
- defaultConfig:
- canUpload: false
-
-### Set a custom folder
-
-This example will save all uploads in the `/assets/customfolder/` folder. If
-the folder doesn't exist, it will be created.
-
- :::php
- $fields->addFieldToTab(
- 'Root.Upload',
- $uploadField = new UploadField(
- $name = 'GalleryImages',
- $title = 'Please upload one or more images' )
- );
- $uploadField->setFolderName('customfolder');
-
-### Limit the allowed filetypes
-
-`AllowedExtensions` defaults to the `File.allowed_extensions` configuration setting,
-but can be overwritten for each UploadField:
-
- :::php
- $uploadField->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif'));
-
-Entire groups of file extensions can be specified in order to quickly limit types
-to known file categories.
-
- :::php
- // This will limit files to the following extensions:
- // "bmp" ,"gif" ,"jpg" ,"jpeg" ,"pcx" ,"tif" ,"png" ,"alpha","als" ,"cel" ,"icon" ,"ico" ,"ps"
- // 'doc','docx','txt','rtf','xls','xlsx','pages', 'ppt','pptx','pps','csv','pdf'
- $uploadField->setAllowedFileCategories('image', 'doc');
-
-`AllowedExtensions` can also be set globally via the [YAML configuration](/topics/configuration#setting-configuration-via-yaml-files), for example you may add the following into your mysite/_config/config.yml:
-
- :::yaml
- File:
- allowed_extensions:
- - 7zip
- - xzip
-
-
-Note: File types such as SWF, XML and HTML are excluded by default from uploading as these types are common
-security attack risks. If necessary, these types may be allowed as uploads (at your own risk) by adding each
-extension to the `File.allowed_extensions` config or setting `File.apply_restrictions_to_admin` to false.
-See [the security topic](/topics/security#user-uploaded-files) for more information.
-
-
-### Limit the maximum file size
-
-`AllowedMaxFileSize` is by default set to the lower value of the 2 php.ini configurations: `upload_max_filesize` and `post_max_size`
-The value is set as bytes.
-
-NOTE: this only sets the configuration for your UploadField, this does NOT change your server upload settings, so if your server is set to only allow 1 MB and you set the UploadFIeld to 2 MB, uploads will not work.
-
- :::php
- $sizeMB = 2; // 2 MB
- $size = $sizeMB * 1024 * 1024; // 2 MB in bytes
- $this->getValidator()->setAllowedMaxFileSize($size);
-
-### Overwrite warning
-
-In order to display a warning before overwriting an existing file, `Upload:replaceFile` must be set to true.
-
-Via config:
-
- :::yaml
- Upload:
- # Replace an existing file rather than renaming the new one.
- replaceFile: true
- UploadField:
- # Warning before overwriting existing file (only relevant when Upload: replaceFile is true)
- overwriteWarning: true
-
-Or per instance:
-
- :::php
- $uploadField->getUpload()->setReplaceFile(true);
- $uploadField->setOverwriteWarning(true);
-
-### Preview dimensions
-
-Set the dimensions of the image preview. By default the max width is set to 80
-and the max height is set to 60.
-
- :::php
- $uploadField->setPreviewMaxWidth(100);
- $uploadField->setPreviewMaxHeight(100);
-
-### Disable attachment of existing files
-
-This can force the user to upload a new file, rather than link to the already
-existing file librarry
-
- :::php
- $uploadField->setCanAttachExisting(false);
-
-### Disable uploading of new files
-
-Alternatively, you can force the user to only specify already existing files
-in the file library
-
- :::php
- $uploadField->setCanUpload(false);
-
-### Automatic or manual upload
-
-By default, the UploadField will try to automatically upload all selected files.
-Setting the `autoUpload` property to false, will present you with a list of
-selected files that you can then upload manually one by one:
-
- :::php
- $uploadField->setAutoUpload(false);
-
-### Change Detection
-
-The CMS interface will automatically notify the form containing
-an UploadField instance of changes, such as a new upload,
-or the removal of an existing upload (through a `dirty` event).
-The UI can then choose an appropriate response (e.g. highlighting the "save" button).
-If the UploadField doesn't save into a relation, there's
-technically no saveable change (the upload has already happened),
-which is why this feature can be disabled on demand.
-
- :::php
- $uploadField->setConfig('changeDetection', false);
-
-### Build a simple gallery
-
-A gallery most times needs more then simple images. You might want to add a
-description, or maybe some settings to define a transition effect for each slide.
-First create a
-[DataExtension](http://doc.silverstripe.org/framework/en/reference/dataextension)
-like this:
-
- :::php
- class GalleryImage extends DataExtension {
-
- private static $db = array(
- 'Description' => 'Text'
- );
-
- private static $belongs_many_many = array(
- 'GalleryPage' => 'GalleryPage'
- );
- }
-
-Now register the DataExtension for the Image class in your _config/config.yml:
-
- Image:
- extensions:
- - GalleryImage
-
-
-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!
-
-
-### 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');
diff --git a/docs/en/03_Upgrading/01_Templates_Upgrading_Guide.md b/docs/en/03_Upgrading/01_Templates_Upgrading_Guide.md
deleted file mode 100644
index 5a7440c58..000000000
--- a/docs/en/03_Upgrading/01_Templates_Upgrading_Guide.md
+++ /dev/null
@@ -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
diff --git a/docs/en/03_Upgrading/index.md b/docs/en/03_Upgrading/index.md
index 080bf7b9f..e051ff8ae 100644
--- a/docs/en/03_Upgrading/index.md
+++ b/docs/en/03_Upgrading/index.md
@@ -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.
+
+
+See our [upgrade notes and changelogs](/changelogs) for release-specific information.
+
+
+## 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.
+
+
+Never update a website on the live server without trying it on a development copy first.
+
+
+## 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)