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 603a08e94..000000000 --- a/docs/en/02_Developer_Guides/00_Model/05_Data_Model_and_ORM.md +++ /dev/null @@ -1,938 +0,0 @@ -# Datamodel - -SilverStripe uses an [object-relational model](http://en.wikipedia.org/wiki/Object-relational_model) -that assumes the following connections: - -* Each database-table maps to a PHP class -* Each database-row maps to a PHP object -* Each database-column maps to a property on a PHP object - -All data tables in SilverStripe are defined as subclasses of `[api:DataObject]`. - -Inheritance is supported in the data model: separate tables will be linked -together, the data spread across these tables. The mapping and saving/loading -logic is handled by SilverStripe, you don't need to worry about writing SQL most -of the time. - -Most of the ORM customizations are possible through [PHP5 Object -Overloading](http://www.onlamp.com/pub/a/php/2005/06/16/overloading.html) -handled in the `[api:Object]`-class. - -See [database-structure](/reference/database-structure) for in-depth information -on the database-schema and the ["sql queries" topic](/reference/sqlquery) in -case you need to drop down to the bare metal. - -## Generating the Database Schema - -The SilverStripe database-schema is generated automatically by visiting the URL. -`http://localhost/dev/build` - -
-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) - -#### Where clauses - -You can specify a WHERE clause fragment (that will be combined with other -filters using AND) with the `where()` method: - - :::php - $members = Member::get()->where("\"FirstName\" = 'Sam'") - -#### Joining - -You can specify a join with the innerJoin and leftJoin methods. Both of these -methods have the same arguments: - - * The name of the table to join to - * The filter clause for the join - * An optional alias - -For example: - - :::php - // Without an alias - $members = Member::get() - ->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\""); - - $members = Member::get() - ->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel"); - -Passing a *$join* statement to DataObject::get will filter results further by -the JOINs performed against the foreign table. **It will NOT return the -additionally joined data.** The returned *$records* will always be a -`[api:DataObject]`. - -## Properties - - -### Definition - -Data is defined in the static variable $db on each class, in the format: -`` => "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", - ); - } - -### 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; - } - } - -## 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 dd71f539d..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. - -### ID Generation - -When a new record is created, we don't use the database's built-in auto-numbering system. Instead, we generate a new ID -by adding 1 to the current maximum ID. - -## Subclass tables - -At SilverStripe's heart is an object-relational model. And a component of object-oriented data is **inheritance**. -Unfortunately, there is no native way of representing inheritance in a relational database. What we do is store the -data sub-classed objects across **multiple tables**. - -For example, suppose we have the following set of classes: - -* Class `[api:SiteTree]` extends `[api:DataObject]`: Title, Content fields -* Class `[api:Page]` extends `[api:SiteTree]`: Abstract field -* Class NewsSection extends `[api:SiteTree]`: *No special fields* -* Class NewsArticle extend `[api:Page]`: ArticleDate field - -The data for the following classes would be stored across the following tables: - -* `[api:SiteTree]` - * ID: Int - * ClassName: Enum('SiteTree', 'Page', 'NewsArticle') - * Created: Datetime - * LastEdited: Datetime - * Title: Varchar - * Content: Text -* `[api:Page]` - * ID: Int - * Abstract: Text -* NewsArticle - * ID: Int - * ArticleDate: Date - -The way it works is this: - -* "Base classes" are direct sub-classes of `[api:DataObject]`. They are always given a table, whether or not they have -special fields. This is called the "base table" -* The base table's ClassName field is set to class of the given record. It's an enumeration of all possible -sub-classes of the base class (including the base class itself) -* Each sub-class of the base object will also be given its own table, *as long as it has custom fields*. In the -example above, NewsSection didn't have its own data and so an extra table would be redundant. -* In all the tables, ID is the primary key. A matching ID number is used for all parts of a particular record: -record #2 in Page refers to the same object as record #2 in `[api:SiteTree]`. - -To retrieve a news article, SilverStripe joins the `[api:SiteTree]`, `[api:Page]` and NewsArticle tables by their ID fields. We use a -left-join for robustness; if there is no matching record in Page, we can return a record with a blank Article field. - -## Staging and versioning - -[todo] - -## Schema auto-generation - -SilverStripe has a powerful tool for automatically building database schemas. We've designed it so that you should never have to build them manually. - -To access it, visit http://localhost/dev/build?flush=1. This script will analyze the existing schema, compare it to what's required by your data classes, and alter the schema as required. - -Put the ?flush=1 on the end if you've added PHP files, so that the rest of the system will find these new classes. - -It will perform the following changes: - - * Create any missing tables - * Create any missing fields - * Create any missing indexes - * Alter the field type of any existing fields - * Rename any obsolete tables that it previously created to _obsolete_(tablename) - -It **won't** do any of the following - - * Deleting tables - * Deleting fields - * Rename any tables that it doesn't recognize - so other applications can co-exist in the same database, as long as their table names don't match a SilverStripe data class. - - -## Related code - -The information documented in this page is reflected in a few places in the code: - -* `[api:DataObject]` - * requireTable() is responsible for specifying the required database schema - * instance_get() and instance_get_one() are responsible for generating the database queries for selecting data. - * write() is responsible for generating the database queries for writing data. -* `[api:Versioned]` - * augmentWrite() is responsible for altering the normal database writing operation to handle versions. - * augmentQuery() is responsible for altering the normal data selection queries to support versions. - * augmentDatabase() is responsible for specifying the altered database schema to support versions. -* `[api:MySQLDatabase]`: getNextID() is used when creating new objects; it also handles the mechanics of -updating the database to have the required schema. \ No newline at end of file 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 423f77b81..000000000 --- a/docs/en/02_Developer_Guides/00_Model/07_SQL_Query.md +++ /dev/null @@ -1,143 +0,0 @@ -# SQL Query - -## Introduction - -An object representing a SQL query, which can be serialized into a SQL statement. -It is easier to deal with object-wrappers than string-parsing a raw SQL-query. -This object is used by the SilverStripe ORM internally. - -Dealing with low-level SQL is not encouraged, since the ORM provides -powerful abstraction APIs (see [datamodel](/topics/datamodel). -Starting with SilverStripe 3, records in collections are lazy loaded, -and these collections have the ability to run efficient SQL -such as counts or returning a single column. - -For example, if you want to run a simple `COUNT` SQL statement, -the following three statements are functionally equivalent: - - :::php - // Through raw SQL - $count = DB::query('SELECT COUNT(*) FROM "Member"')->value(); - // Through SQLQuery abstraction layer - $query = new SQLQuery(); - $count = $query->setFrom('Member')->setSelect('COUNT(*)')->value(); - // Through the ORM - $count = Member::get()->count(); - -If you do use raw SQL, you'll run the risk of breaking -various assumptions the ORM and code based on it have: - -* Custom getters/setters (object property can differ from database column) -* DataObject hooks like onBeforeWrite() and onBeforeDelete() -* Automatic casting -* Default values set through objects -* Database abstraction - -We'll explain some ways to use *SELECT* with the full power of SQL, -but still maintain a connection to the ORM where possible. - -
-Please read our ["security" topic](/topics/security) to find out -how to sanitize user input before using it in SQL queries. -
- -## Usage - -### SELECT - - :::php - $sqlQuery = new SQLQuery(); - $sqlQuery->setFrom('Player'); - $sqlQuery->selectField('FieldName', 'Name'); - $sqlQuery->selectField('YEAR("Birthday")', 'Birthyear'); - $sqlQuery->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"'); - $sqlQuery->addWhere('YEAR("Birthday") = 1982'); - // $sqlQuery->setOrderBy(...); - // $sqlQuery->setGroupBy(...); - // $sqlQuery->setHaving(...); - // $sqlQuery->setLimit(...); - // $sqlQuery->setDistinct(true); - - // Get the raw SQL (optional) - $rawSQL = $sqlQuery->sql(); - - // Execute and return a Query object - $result = $sqlQuery->execute(); - - // Iterate over results - foreach($result as $row) { - echo $row['BirthYear']; - } - -The result is an array lightly wrapped in a database-specific subclass of `[api:Query]`. -This class implements the *Iterator*-interface, and provides convenience-methods for accessing the data. - -### DELETE - - :::php - $sqlQuery->setDelete(true); - -### INSERT/UPDATE - -Currently not supported through the `SQLQuery` class, please use raw `DB::query()` calls instead. - - :::php - DB::query('UPDATE "Player" SET "Status"=\'Active\''); - -### Value Checks - -Raw SQL is handy for performance-optimized calls, -e.g. when you want a single column rather than a full-blown object representation. - -Example: Get the count from a relationship. - - :::php - $sqlQuery = new SQLQuery(); - $sqlQuery->setFrom('Player'); - $sqlQuery->addSelect('COUNT("Player"."ID")'); - $sqlQuery->addWhere('"Team"."ID" = 99'); - $sqlQuery->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"'); - $count = $sqlQuery->execute()->value(); - -Note that in the ORM, this call would be executed in an efficient manner as well: - - :::php - $count = $myTeam->Players()->count(); - -### Mapping - -Creates a map based on the first two columns of the query result. -This can be useful for creating dropdowns. - -Example: Show player names with their birth year, but set their birth dates as values. - - :::php - $sqlQuery = new SQLQuery(); - $sqlQuery->setFrom('Player'); - $sqlQuery->setSelect('Birthdate'); - $sqlQuery->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear'); - $map = $sqlQuery->execute()->map(); - $field = new DropdownField('Birthdates', 'Birthdates', $map); - -Note that going through SQLQuery is just necessary here -because of the custom SQL value transformation (`YEAR()`). -An alternative approach would be a custom getter in the object definition. - - :::php - class Player extends DataObject { - private static $db = array( - 'Name' => 'Varchar', - 'Birthdate' => 'Date' - ); - function getNameWithBirthyear() { - return date('y', $this->Birthdate); - } - } - $players = Player::get(); - $map = $players->map('Name', 'NameWithBirthyear'); - -## Related - -* [datamodel](/topics/datamodel) -* `[api:DataObject]` -* [database-structure](database-structure) 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 bffc11b1a..000000000 --- a/docs/en/02_Developer_Guides/UploadField.md +++ /dev/null @@ -1,398 +0,0 @@ -title: UploadField -summary: How to use the UploadField class for uploading assets. - -# UploadField - -## Introduction - -The UploadField will let you upload one or multiple files of all types, -including images. But that's not all it does - it will also link the -uploaded file(s) to an existing relation and let you edit the linked files -as well. That makes it flexible enough to sometimes even replace the Gridfield, -like for instance in creating and managing a simple gallery. - -## Usage - -The field can be used in three ways: To upload a single file into a `has_one` relationship, -or allow multiple files into a `has_many` or `many_many` relationship, or to act as a stand -alone uploader into a folder with no underlying relation. - -## Validation - -Although images are uploaded and stored on the filesystem immediately after selection, -the value (or values) of this field will not be written to any related record until -the record is saved and successfully validated. However, any invalid records will still -persist across form submissions until explicitly removed or replaced by the user. - -Care should be taken as invalid files may remain within the filesystem until explicitly -removed. - -### Single fileupload - -The following example adds an UploadField to a page for single fileupload, -based on a has_one relation: - - :::php - class GalleryPage extends Page { - - private static $has_one = array( - 'SingleImage' => 'Image' - ); - - function getCMSFields() { - - $fields = parent::getCMSFields(); - - $fields->addFieldToTab( - 'Root.Upload', - $uploadField = new UploadField( - $name = 'SingleImage', - $title = 'Upload a single image' - ) - ); - return $fields; - } - } - -The UploadField will autodetect the relation based on it's `name` property, and -save it into the GalleyPages' `SingleImageID` field. Setting the -`setAllowedMaxFileNumber` to 1 will make sure that only one image can ever be -uploaded and linked to the relation. - -### Multiple fileupload - -Enable multiple fileuploads by using a many_many (or has_many) relation. Again, -the `UploadField` will detect the relation based on its $name property value: - - :::php - class GalleryPage extends Page { - - private static $many_many = array( - 'GalleryImages' => 'Image' - ); - - function getCMSFields() { - - $fields = parent::getCMSFields(); - - $fields->addFieldToTab( - 'Root.Upload', - $uploadField = new UploadField( - $name = 'GalleryImages', - $title = 'Upload one or more images (max 10 in total)' - ) - ); - $uploadField->setAllowedMaxFileNumber(10); - - return $fields; - } - } - class GalleryPage_Controller extends Page_Controller { - } - - class GalleryImageExtension extends DataExtension { - private static $belongs_many_many = array('Galleries' => 'GalleryPage); - } - - Image::add_extension('GalleryImageExtension'); - -
-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', 'html','htm','xhtml', 'xml','pdf' - $uploadField->setAllowedFileCategories('image', 'doc'); - -`AllowedExtensions` can also be set globally via the [YAML configuration](/topics/configuration#setting-configuration-via-yaml-files), for example you may add the following into your mysite/_config/config.yml: - - :::yaml - File: - allowed_extensions: - - 7zip - - xzip - -### Limit the maximum file size - -`AllowedMaxFileSize` is by default set to the lower value of the 2 php.ini configurations: `upload_max_filesize` and `post_max_size` -The value is set as bytes. - -NOTE: this only sets the configuration for your UploadField, this does NOT change your server upload settings, so if your server is set to only allow 1 MB and you set the UploadFIeld to 2 MB, uploads will not work. - - :::php - $sizeMB = 2; // 2 MB - $size = $sizeMB * 1024 * 1024; // 2 MB in bytes - $this->getValidator()->setAllowedMaxFileSize($size); - -### Preview dimensions - -Set the dimensions of the image preview. By default the max width is set to 80 -and the max height is set to 60. - - :::php - $uploadField->setPreviewMaxWidth(100); - $uploadField->setPreviewMaxHeight(100); - -### Disable attachment of existing files - -This can force the user to upload a new file, rather than link to the already -existing file librarry - - :::php - $uploadField->setCanAttachExisting(false); - -### Disable uploading of new files - -Alternatively, you can force the user to only specify already existing files -in the file library - - :::php - $uploadField->setCanUpload(false); - -### Automatic or manual upload - -By default, the UploadField will try to automatically upload all selected files. -Setting the `autoUpload` property to false, will present you with a list of -selected files that you can then upload manually one by one: - - :::php - $uploadField->setAutoUpload(false); - -### Change Detection - -The CMS interface will automatically notify the form containing -an UploadField instance of changes, such as a new upload, -or the removal of an existing upload (through a `dirty` event). -The UI can then choose an appropriate response (e.g. highlighting the "save" button). -If the UploadField doesn't save into a relation, there's -technically no saveable change (the upload has already happened), -which is why this feature can be disabled on demand. - - :::php - $uploadField->setConfig('changeDetection', false); - -### Build a simple gallery - -A gallery most times needs more then simple images. You might want to add a -description, or maybe some settings to define a transition effect for each slide. -First create a -[DataExtension](http://doc.silverstripe.org/framework/en/reference/dataextension) -like this: - - :::php - class GalleryImage extends DataExtension { - - private static $db = array( - 'Description' => 'Text' - ); - - private static $belongs_many_many = array( - 'GalleryPage' => 'GalleryPage' - ); - } - -Now register the DataExtension for the Image class in your _config.php: - - :::php - Image::add_extension('GalleryImage'); - -
-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)