diff --git a/en/00_Getting_Started/02_Composer.md b/en/00_Getting_Started/02_Composer.md index ed7f425f2..a7d1c2aa7 100644 --- a/en/00_Getting_Started/02_Composer.md +++ b/en/00_Getting_Started/02_Composer.md @@ -22,9 +22,8 @@ installed globally. You should now be able to run the command: composer help ``` -[info] -If you already have Composer installed, make sure it is composer 2 by running `composer --version`. If you're running Composer 1, run [`composer self-update`](https://getcomposer.org/doc/03-cli.md#self-update-selfupdate). You may also want to check out the [upgrade guide for Composer 1.x to 2.0](https://getcomposer.org/upgrade/UPGRADE-2.0.md). -[/info] +> [!NOTE] +> If you already have Composer installed, make sure it is composer 2 by running `composer --version`. If you're running Composer 1, run [`composer self-update`](https://getcomposer.org/doc/03-cli.md#self-update-selfupdate). You may also want to check out the [upgrade guide for Composer 1.x to 2.0](https://getcomposer.org/upgrade/UPGRADE-2.0.md). ## Create a new site @@ -35,14 +34,13 @@ version: composer create-project silverstripe/installer my-project ``` -[hint] -With the above command, `my-project` is the path (relative to your current working directory) where Composer will create the project. - -For example, on OSX, you might want to create a project as a sub-directory of `~/Sites`. You could do that by running `cd ~/Sites` before -the above command. - -If that directory doesn't exist, Composer will create it for you. -[/hint] +> [!TIP] +> With the above command, `my-project` is the path (relative to your current working directory) where Composer will create the project. +> +> For example, on OSX, you might want to create a project as a sub-directory of `~/Sites`. You could do that by running `cd ~/Sites` before +> the above command. +> +> If that directory doesn't exist, Composer will create it for you. If you want to get additional fixtures for testing, such as behat and phpunit configuration, an example `.env.example` file, and all documentation, then it's recommended to use the `--prefer-source` option @@ -87,11 +85,10 @@ a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing- composer require silverstripe/blog ^2 ``` -[warning] -**Version constraints:** `master` or `main` is not a legal version string - it's a branch name. These are different things. The -version string that would get you the branch is `dev-main`. The version string that would get you a numeric branch is -a little different. The version string for the `5` branch is `5.x-dev`. -[/warning] +> [!WARNING] +> **Version constraints:** `master` or `main` is not a legal version string - it's a branch name. These are different things. The +> version string that would get you the branch is `dev-main`. The version string that would get you a numeric branch is +> a little different. The version string for the `5` branch is `5.x-dev`. ## Updating dependencies @@ -107,14 +104,13 @@ composer update Updates to the required modules will be installed, and the `composer.lock` file will get updated with the specific commits and version constraints for each of them. -[hint] -The update command can also be used to *downgrade* dependencies - if you edit your `composer.json` file and set a version -constraint that will require a lower version to be installed, running `composer update` will "update" your installed -dependencies to match your constraints, which in this case would install lower versions than what you had previously. - -You may occasionally need to use the `--with-all-dependencies` option when downgrading dependencies to avoid conflicting -version constraints. -[/hint] +> [!TIP] +> The update command can also be used to *downgrade* dependencies - if you edit your `composer.json` file and set a version +> constraint that will require a lower version to be installed, running `composer update` will "update" your installed +> dependencies to match your constraints, which in this case would install lower versions than what you had previously. +> +> You may occasionally need to use the `--with-all-dependencies` option when downgrading dependencies to avoid conflicting +> version constraints. ## Deploying projects with Composer @@ -229,10 +225,9 @@ file. It will appear in your project root, and by default, it will look somethin To add modules, you should add more entries into the `"require"` section. For example, we might add the blog and forum modules. -[notice] -Be careful with the commas at the end of the lines! You can run `composer validate` to be sure your `composer.json` -file is formatted correctly. -[/notice] +> [!WARNING] +> Be careful with the commas at the end of the lines! You can run `composer validate` to be sure your `composer.json` +> file is formatted correctly. Save your file, and then run the following command to update the installed packages: @@ -303,16 +298,15 @@ This is how you do it: composer require silverstripe/cms ``` -[notice] -In most cases, you will probably have a specific branch of the fork you want to install. You should use the appropriate -version constraint to install that branch. For example, to install a branch called `fix/issue-1990` your version constraint -should be `dev-fix/issue-1990`. - -Depending on what other installed modules have that package as a dependency, you may also need to declare an -[inline alias](https://getcomposer.org/doc/articles/aliases.md#require-inline-alias). - -See more about this in [Forks and branch names](#forks-and-branch-names) below. -[/notice] +> [!WARNING] +> In most cases, you will probably have a specific branch of the fork you want to install. You should use the appropriate +> version constraint to install that branch. For example, to install a branch called `fix/issue-1990` your version constraint +> should be `dev-fix/issue-1990`. +> +> Depending on what other installed modules have that package as a dependency, you may also need to declare an +> [inline alias](https://getcomposer.org/doc/articles/aliases.md#require-inline-alias). +> +> See more about this in [Forks and branch names](#forks-and-branch-names) below. Composer will scan all of the repositories you list, collect meta-data about the packages within them, and use them in favour of the packages listed on packagist. To switch back to using the mainline version of the package, just remove diff --git a/en/00_Getting_Started/03_Environment_Management.md b/en/00_Getting_Started/03_Environment_Management.md index d9677051a..ad6bc346c 100644 --- a/en/00_Getting_Started/03_Environment_Management.md +++ b/en/00_Getting_Started/03_Environment_Management.md @@ -54,11 +54,10 @@ use SilverStripe\Core\Environment; Environment::getEnv('SS_DATABASE_CLASS'); ``` -[hint] -The `Environment::getEnv()` method will return `false` both if there was no value set for a variable or if -the variable was explicitly set as `false`. You can determine whether a variable has been set by calling -[`Environment::hasEnv()`](api:SilverStripe\Core\Environment::hasEnv()). -[/hint] +> [!TIP] +> The `Environment::getEnv()` method will return `false` both if there was no value set for a variable or if +> the variable was explicitly set as `false`. You can determine whether a variable has been set by calling +> [`Environment::hasEnv()`](api:SilverStripe\Core\Environment::hasEnv()). Individual settings can be assigned via [`Environment::setEnv()`](api:SilverStripe\Core\Environment::setEnv()) or [`Environment::putEnv()`](api:SilverStripe\Core\Environment::putEnv()) methods. @@ -67,9 +66,8 @@ use SilverStripe\Core\Environment; Environment::setEnv('API_KEY', 'AABBCCDDEEFF012345'); ``` -[warning] -`Environment::getEnv()` will return `false` whether the variable was explicitly set as `false` or simply wasn't set at all. You can use [`Environment::hasEnv()`](api:SilverStripe\Core\Environment::hasEnv()) to check whether an environment variable was set or not. -[/warning] +> [!WARNING] +> `Environment::getEnv()` will return `false` whether the variable was explicitly set as `false` or simply wasn't set at all. You can use [`Environment::hasEnv()`](api:SilverStripe\Core\Environment::hasEnv()) to check whether an environment variable was set or not. ### Using environment variables in config @@ -91,9 +89,8 @@ $loader = new EnvironmentLoader(); $loader->loadFile($env); ``` -[warning] -Note that because `_config.php` is processed after YAML configuration, variables set in these extra `.env` files cannot be used inside YAML config. -[/warning] +> [!WARNING] +> Note that because `_config.php` is processed after YAML configuration, variables set in these extra `.env` files cannot be used inside YAML config. ## Core environment variables diff --git a/en/00_Getting_Started/index.md b/en/00_Getting_Started/index.md index c3fa616ef..073ab82f0 100644 --- a/en/00_Getting_Started/index.md +++ b/en/00_Getting_Started/index.md @@ -29,9 +29,8 @@ Within the newly created `my-project` folder, point your webserver at the `publi Now create a `.env` file in your project root (not the `public/` folder). -[hint] -If you used `silverstripe/installer` to create your project, you can rename the `.env.example` file to `.env`. It includes the minimum required [environment variables](environment_management). -[/hint] +> [!TIP] +> If you used `silverstripe/installer` to create your project, you can rename the `.env.example` file to `.env`. It includes the minimum required [environment variables](environment_management). Replace the placeholders as required: diff --git a/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md b/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md index bad5efee3..ef4855dad 100644 --- a/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md +++ b/en/02_Developer_Guides/00_Model/01_Data_Model_and_ORM.md @@ -43,11 +43,10 @@ class Player extends DataObject 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. -[hint] -You can technically omit the `table_name` property, and a default table name will be created based on the fully qualified class name - but this can result in table names that are too long for the database engine to handle. We recommend that you *always* explicitly declare a table name for your models. - -See more in [Mapping classes to tables with `DataObjectSchema`](#mapping-classes-to-tables) below. -[/hint] +> [!TIP] +> You can technically omit the `table_name` property, and a default table name will be created based on the fully qualified class name - but this can result in table names that are too long for the database engine to handle. We recommend that you *always* explicitly declare a table name for your models. +> +> See more in [Mapping classes to tables with `DataObjectSchema`](#mapping-classes-to-tables) below. ## Generating the database schema @@ -55,9 +54,8 @@ After adding, modifying or removing `DataObject` subclasses, make sure to rebuil database schema is generated automatically by visiting `/dev/build` (e.g. `https://www.example.com/dev/build`) in your browser while authenticated as an administrator, or by running `sake dev/build` on the command line (see [Command Line Interface](/developer_guides/cli/) to learn more about `sake`). -[info] -In "dev" mode, you do not need to be authenticated to run `/dev/build`. See [Environment Types](/developer_guides/debugging/environment_types) for more information. -[/info] +> [!NOTE] +> In "dev" mode, you do not need to be authenticated to run `/dev/build`. See [Environment Types](/developer_guides/debugging/environment_types) for more information. This script will analyze the existing schema, compare it to what's required by your data classes, and alter the schema as required. @@ -119,11 +117,10 @@ However, a better way is to use the `create()` method. $player = Player::create(); ``` -[hint] -Using the `create()` method provides chainability (known as a "fluent API" or "[fluent interface](https://en.wikipedia.org/wiki/Fluent_interface)"), which can add elegance and brevity to your code, e.g. `Player::create(['FirstName' => 'Sam'])->write()`. - -More importantly, however, it will look up the class in the [`Injector`](api:SilverStripe\Core\Injector\Injector) so that the class can be overridden by [dependency injection](../extending/injector). For this reason, instantiating records using the `new` keyword is considered bad practice. -[/hint] +> [!TIP] +> Using the `create()` method provides chainability (known as a "fluent API" or "[fluent interface](https://en.wikipedia.org/wiki/Fluent_interface)"), which can add elegance and brevity to your code, e.g. `Player::create(['FirstName' => 'Sam'])->write()`. +> +> More importantly, however, it will look up the class in the [`Injector`](api:SilverStripe\Core\Injector\Injector) so that the class can be overridden by [dependency injection](../extending/injector). For this reason, instantiating records using the `new` keyword is considered bad practice. Database columns (aka fields) can be set as class properties on the object. The Silverstripe CMS ORM handles the saving of the values through a custom `__set()` method. @@ -152,6 +149,9 @@ $id = $player->write(); With the `Player` class defined we can query our data using the ORM. The ORM provides shortcuts and methods for fetching, sorting and filtering data from our database. +> [!TIP] +> All of the below methods to get a single record will return `null` if there is no record to return. + ```php // returns a `DataList` containing all the `Player` objects. $players = Player::get(); @@ -165,13 +165,8 @@ $player = Player::get()->byID(2); $player = Player::get_by_id(2); ``` -[hint] -All of the above methods to get a single record will return `null` if there is no record to return. -[/hint] - -[info] -`DataObject::get()->byID()` and `DataObject::get_by_id()` achieve similar results, though the object returned by `DataObject::get_by_id()` is cached against a `static` property within `DataObject`. -[/info] +> [!NOTE] +> `DataObject::get()->byID()` and `DataObject::get_by_id()` achieve similar results, though the object returned by `DataObject::get_by_id()` is cached against a `static` property within `DataObject`. The ORM uses a "fluent" syntax, where you specify a query by chaining together different methods. Two common methods are `filter()` and `sort()`: @@ -185,17 +180,15 @@ $members = Player::get()->filter([ There's a lot more to filtering and sorting, so make sure to keep reading. -[info] -Values passed in to the `filter()` and `sort()` methods are automatically escaped and do not require any additional escaping. This makes it easy to safely filter/sort records by user input. -[/info] +> [!NOTE] +> Values passed in to the `filter()` and `sort()` methods are automatically escaped and do not require any additional escaping. This makes it easy to safely filter/sort records by user input. ### Querying data when you have a record When you have a `DataObject` record, it already has all of its database field data attached to it. You can get those values without triggering further database queries. -[notice] -This does not apply to relations, which are always lazy loaded. See the [Relations between Records](relations) documentation for more information. -[/notice] +> [!WARNING] +> This does not apply to relations, which are always lazy loaded. See the [Relations between Records](relations) documentation for more information. ```php $player = Player::get()->byID(2); @@ -215,9 +208,8 @@ All database fields have a default value format which you can retrieve by treati The ORM doesn't actually execute the [SQLSelect](api:SilverStripe\ORM\Queries\SQLSelect) query until you iterate on the result (e.g. with a `foreach()` or `<% loop %>`). -[hint] -Some convenience methods (e.g. [`column()`](api:SilverStripe\ORM\DataList::column()) or aggregator methods like [`min()`](api:SilverStripe\ORM\DataList::min())) will also execute the query. -[/hint] +> [!TIP] +> Some convenience methods (e.g. [`column()`](api:SilverStripe\ORM\DataList::column()) or aggregator methods like [`min()`](api:SilverStripe\ORM\DataList::min())) will also execute the query. 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 @@ -267,9 +259,8 @@ if ($players->exists()) { } ``` -[hint] -While you could use `if ($players->Count() > 0)` for this condition, the `exists()` method uses an `EXISTS` SQL query, which is more performant. -[/hint] +> [!TIP] +> While you could use `if ($players->Count() > 0)` for this condition, the `exists()` method uses an `EXISTS` SQL query, which is more performant. See the [Lists](lists) documentation for more information on dealing with [SS_List](api:SilverStripe\ORM\SS_List) instances. @@ -454,20 +445,18 @@ $teams = Team::get()->filter('Players.Avg(PointsScored):GreaterThan', 15); $teams = Team::get()->filter('Players.Sum(PointsScored):LessThan', 300); ``` -[hint] -The above examples are using "dot notation" to get the aggregations of the `Players` relation on the `Teams` model. See [Relations between Records](relations) to learn more. -[/hint] +> [!TIP] +> The above examples are using "dot notation" to get the aggregations of the `Players` relation on the `Teams` model. See [Relations between Records](relations) to learn more. ### `filterByCallback` It is possible to filter by a PHP callback using the [`filterByCallback()`](api:SilverStripe\ORM\DataList::filterByCallback()) method. This will force the data model to fetch all records and loop them in PHP which will be much worse for performance, thus `filter()` or `filterAny()` are to be preferred over `filterByCallback()`. -[notice] -Because `filterByCallback()` has to run in PHP, it has a significant performance tradeoff, and should not be used on large recordsets. - -`filterByCallback()` will always return an `ArrayList`. -[/notice] +> [!WARNING] +> Because `filterByCallback()` has to run in PHP, it has a significant performance tradeoff, and should not be used on large recordsets. +> +> `filterByCallback()` will always return an `ArrayList`. The first parameter to the callback is the record, the second parameter is the list itself. The callback will run once for each record. The callback must return a boolean value. If the callback returns true, the current record will be included in the list of returned items. @@ -757,11 +746,10 @@ $members = Member::get() ->innerJoin("Group_Members", '"Rel"."MemberID" = "Member"."ID"', "Rel"); ``` -[alert] -Using a join will *filter* results further by the JOINs performed against the foreign table. It will -**not return** the additionally joined data. For the examples above, we're still only selecting values for the fields -on the `Member` class table. -[/alert] +> [!CAUTION] +> Using a join will *filter* results further by the JOINs performed against the foreign table. It will +> **not return** the additionally joined data. For the examples above, we're still only selecting values for the fields +> on the `Member` class table. ### Default values @@ -843,9 +831,8 @@ Product_Digital_Computer: IsPreBuilt: 'Boolean' ``` -[hint] -Note that because `DigitalProduct` doesn't define any new fields it doesn't need its own table. We should still declare a `$table_name` though - who knows if this model might have its own table created in the future (e.g. if we add fields to it later on). -[/hint] +> [!TIP] +> Note that because `DigitalProduct` doesn't define any new fields it doesn't need its own table. We should still declare a `$table_name` though - who knows if this model might have its own table created in the future (e.g. if we add fields to it later on). Accessing the data is transparent to the developer. diff --git a/en/02_Developer_Guides/00_Model/02_Relations.md b/en/02_Developer_Guides/00_Model/02_Relations.md index 053b846f3..0d72b4119 100644 --- a/en/02_Developer_Guides/00_Model/02_Relations.md +++ b/en/02_Developer_Guides/00_Model/02_Relations.md @@ -52,27 +52,25 @@ class Team extends DataObject This defines a one-to-many relationship called `Team` which links any number of `Player` records to a single `Team` record. The ORM handles navigating the relationship and provides a short syntax for accessing the related object. -[hint] -Relations don't only apply to your own `DataObject` models - you can make relations to core models such as `File` and `Image` as well: - -```php -namespace App\Model; - -use SilverStripe\Assets\File; -use SilverStripe\Assets\Image; -use SilverStripe\ORM\DataObject; - -class Team extends DataObject -{ - private static $has_one = [ - 'Teamphoto' => Image::class, - 'Lineup' => File::class, - ]; - // ... -} -``` - -[/hint] +> [!TIP] +> Relations don't only apply to your own `DataObject` models - you can make relations to core models such as `File` and `Image` as well: +> +> ```php +> namespace App\Model; +> +> use SilverStripe\Assets\File; +> use SilverStripe\Assets\Image; +> use SilverStripe\ORM\DataObject; +> +> class Team extends DataObject +> { +> private static $has_one = [ +> 'Teamphoto' => Image::class, +> 'Lineup' => File::class, +> ]; +> // ... +> } +> ``` At the database level, the `has_one` from our example above creates a `TeamID` field on the `Player` table. A `has_many` field does not impose any database changes. It merely injects a new method into the class to access the related records (in this case, `Players()`) @@ -86,9 +84,8 @@ echo $player->Team()->Title; // returns the 'Title' column on the 'Team' or `getTitle` if it exists. ``` -[info] -Even if the `$player` record doesn't have any team record saved in its `Team` relation, `$player->Team()` will return a `Team` object. In that case, it will be an *empty* record, with only default values applied. You can validate if that is the case by calling [`exists()`](api:SilverStripe\ORM\DataObject::exists()) on the record (e.g. `$player->Team()->exists()`). -[/info] +> [!NOTE] +> Even if the `$player` record doesn't have any team record saved in its `Team` relation, `$player->Team()` will return a `Team` object. In that case, it will be an *empty* record, with only default values applied. You can validate if that is the case by calling [`exists()`](api:SilverStripe\ORM\DataObject::exists()) on the record (e.g. `$player->Team()->exists()`). The relationship can also be navigated in [templates](../templates). @@ -155,13 +152,12 @@ class Fan extends DataObject } ``` -[warning] -Note: The use of polymorphic relationships can affect query performance, especially -on joins, and also increases the complexity of the database and necessary user code. -They should be used sparingly, and only where additional complexity would otherwise -be necessary. For example additional parent classes for each respective relationship, or -duplication of code. -[/warning] +> [!WARNING] +> Note: The use of polymorphic relationships can affect query performance, especially +> on joins, and also increases the complexity of the database and necessary user code. +> They should be used sparingly, and only where additional complexity would otherwise +> be necessary. For example additional parent classes for each respective relationship, or +> duplication of code. ### Multi-relational `has_one` {#multi-relational-has-one} @@ -171,13 +167,12 @@ in situations like adding multiple lists of links to a [`SiteConfig`](api:Silver An additional column is created called `Relation`, along with the `Class` and `ID` columns of a normal polymorphic `has_one` relation. -[warning] -If you save records into the `has_one` relation programatically, you must set the relation in the -`Relation` field, or it won't be included when you fetch the `has_many` relation list. - -Generally it is better to instead add the record with the `has_one` relation into its corresponding `has_many` relation -directly - see [adding relations](#adding-relations). -[/warning] +> [!WARNING] +> If you save records into the `has_one` relation programatically, you must set the relation in the +> `Relation` field, or it won't be included when you fetch the `has_many` relation list. +> +> Generally it is better to instead add the record with the `has_one` relation into its corresponding `has_many` relation +> directly - see [adding relations](#adding-relations). To specify that a `has_one` relation is multi-relational define the relation like so: @@ -202,9 +197,8 @@ class Fan extends DataObject } ``` -[hint] -Multi-relational `has_one` relations *must* be [polymorphic](#polymorphic-has-one). -[/hint] +> [!TIP] +> Multi-relational `has_one` relations *must* be [polymorphic](#polymorphic-has-one). It is best practice for your `has_many` relations to indicate which relation they're pointing at using dot notation. For example: @@ -236,9 +230,8 @@ corresponding `has_one`. A single database column named `ID` Similarly with `has_many` below, [dot notation](#dot-notation) can (and for best practice *should*) be used to explicitly specify the `has_one` which refers to this relation. This is not mandatory unless the relationship would be otherwise ambiguous. -[hint] -You can use `RelationValidationService` for validation of relationships. This tool will point out the relationships which may need a review. See [Validating relations](#validating-relations) for more information. -[/hint] +> [!TIP] +> You can use `RelationValidationService` for validation of relationships. This tool will point out the relationships which may need a review. See [Validating relations](#validating-relations) for more information. ```php namespace App\Model; @@ -270,19 +263,18 @@ class Coach extends DataObject Defines one-to-many joins. As you can see from the previous example, `$has_many` goes hand in hand with `$has_one`. -[alert] -When defining a `has_many` relation, you *must* specify a `has_one` relationship on the related class as well. To add a `has_one` relation on core classes, yml config settings can be used: - -```yml -SilverStripe\Assets\Image: - has_one: - Team: App\Model\Team -``` - -You can point multiple `has_many` relations at a single `has_one` relation if you use a [multi-relational `has_one`](#multi-relational-has-one). - -Note that in some cases you may be better off using a `many_many` relation instead. Carefully consider whether you are defining a "one-to-many" or a "many-to-many" relationship. -[/alert] +> [!CAUTION] +> When defining a `has_many` relation, you *must* specify a `has_one` relationship on the related class as well. To add a `has_one` relation on core classes, yml config settings can be used: +> +> ```yml +> SilverStripe\Assets\Image: +> has_one: +> Team: App\Model\Team +> ``` +> +> You can point multiple `has_many` relations at a single `has_one` relation if you use a [multi-relational `has_one`](#multi-relational-has-one). +> +> Note that in some cases you may be better off using a `many_many` relation instead. Carefully consider whether you are defining a "one-to-many" or a "many-to-many" relationship. ```php namespace App\Model; @@ -389,23 +381,20 @@ class Company extends DataObject Multiple `has_many` or `belongs_to` relationships are okay *without* dot notation if they aren't linking to the same model class. Otherwise, using dot notation is required. With that said, dot notation is recommended in all cases as it makes your code more resilient to change. Adding new relationships is easier when you don't need to review and update existing ones. -[hint] -You can use `RelationValidationService` for validation of relationships. This tool will point out the relationships which may need a review. See [Validating relations](#validating-relations) for more information. -[/hint] +> [!TIP] +> You can use `RelationValidationService` for validation of relationships. This tool will point out the relationships which may need a review. See [Validating relations](#validating-relations) for more information. ## `many_many` relationships {#many-many} -[warning] -Please specify a `belongs_many_many` relationship on the related class as well in order to have the necessary accessors available on both ends. See [`belongs_many_many`](#belongs-many-many) for more information. -[/warning] +> [!WARNING] +> Please specify a `belongs_many_many` relationship on the related class as well in order to have the necessary accessors available on both ends. See [`belongs_many_many`](#belongs-many-many) for more information. Defines many-to-many relationships. This type of relationship requires a new table which has an `ID` column to represent the relationship itself and a column for each of the IDs of the related records. There are two ways this relationship can be declared which are (described below) depending on how the developer wishes to manage this join table. -[hint] -You can use `RelationValidationService` for validation of relationships. This tool will point out the relationships which may need a review. See [Validating relations](#validating-relations) for more information. -[/hint] +> [!TIP] +> You can use `RelationValidationService` for validation of relationships. This tool will point out the relationships which may need a review. See [Validating relations](#validating-relations) for more information. ### Automatic `many_many` table {#automatic-many-many-table} @@ -545,9 +534,8 @@ $team = Team::get()->byId(1); $supporters = $team->Supporters()->filter(['Ranking' => 1]); ``` -[hint] -For records accessed in a [`ManyManyThroughList`](api:SilverStripe\ORM\ManyManyThroughList), you can access the join record (e.g. for our example above a `TeamSupporter` instance) by calling [`getJoin()`](api:SilverStripe\ORM\DataObject::getJoin()) or as the `$Join` property in templates. -[/hint] +> [!TIP] +> For records accessed in a [`ManyManyThroughList`](api:SilverStripe\ORM\ManyManyThroughList), you can access the join record (e.g. for our example above a `TeamSupporter` instance) by calling [`getJoin()`](api:SilverStripe\ORM\DataObject::getJoin()) or as the `$Join` property in templates. #### Polymorphic `many_many` @@ -688,22 +676,20 @@ The relationship can also be navigated in [templates](../templates). <% end_with %> ``` -[hint] -For many_many through relations, the join record can be accessed via [`$Join`](api:SilverStripe\ORM\DataObject::getJoin()) or the actual relation name (e.g. `$TeamSupporter`). This is useful if your template is class-agnostic and doesn't know specifically what relation names are used. - -This also provides three ways to access the extra fields on a many_many through relation: - -```ss -<% with $Supporter %> - <% loop $Supports %> - Access extrafields directly: $Ranking - Access extrafields using getJoin: $Join.Ranking - Access extrafields using the somewhat-magic join-class selector: $TeamSupporter.Ranking - <% end_loop %> -<% end_with %> -``` - -[/hint] +> [!TIP] +> For many_many through relations, the join record can be accessed via [`$Join`](api:SilverStripe\ORM\DataObject::getJoin()) or the actual relation name (e.g. `$TeamSupporter`). This is useful if your template is class-agnostic and doesn't know specifically what relation names are used. +> +> This also provides three ways to access the extra fields on a many_many through relation: +> +> ```ss +> <% with $Supporter %> +> <% loop $Supports %> +> Access extrafields directly: $Ranking +> Access extrafields using getJoin: $Join.Ranking +> Access extrafields using the somewhat-magic join-class selector: $TeamSupporter.Ranking +> <% end_loop %> +> <% end_with %> +> ``` ## `belongs_many_many` {#belongs-many-many} @@ -903,23 +889,21 @@ Eager loading can be used in templates. The following example assumes that `$MyT Eager loading supports all relationship types. -[notice] -Eager loading is only intended to be used in read-only scenarios such as when outputting data on the front-end of a website. When using default lazy-loading, relationship methods will return a subclass of [`DataList`](api:SilverStripe\ORM\DataList) such as [`HasManyList`](api:SilverStripe\ORM\HasManyList). However when using eager-loading, an [`EagerLoadedList`](api:SilverStripe\ORM\EagerLoadedList) will be returned instead. `EagerLoadedList` has common methods such as `filter()`, `sort()`, `limit()` and `reverse()` available to manipulate its data, as well as some methods you'd expect on the various relation list implementations such as [`getExtraData()`](api:SilverStripe\ORM\EagerLoadedList::getExtraData()). - -Note that filtering or sorting an `EagerLoadedList` will be done in PHP rather than as part of the database query, since we have already loaded all its relevant data into memory. - -Note also that `EagerLoadedList` can't filter or sort by fields on relations using dot notation (e.g. `sort('MySubRelation.Title')` won't work). -[/notice] +> [!WARNING] +> Eager loading is only intended to be used in read-only scenarios such as when outputting data on the front-end of a website. When using default lazy-loading, relationship methods will return a subclass of [`DataList`](api:SilverStripe\ORM\DataList) such as [`HasManyList`](api:SilverStripe\ORM\HasManyList). However when using eager-loading, an [`EagerLoadedList`](api:SilverStripe\ORM\EagerLoadedList) will be returned instead. `EagerLoadedList` has common methods such as `filter()`, `sort()`, `limit()` and `reverse()` available to manipulate its data, as well as some methods you'd expect on the various relation list implementations such as [`getExtraData()`](api:SilverStripe\ORM\EagerLoadedList::getExtraData()). +> +> Note that filtering or sorting an `EagerLoadedList` will be done in PHP rather than as part of the database query, since we have already loaded all its relevant data into memory. +> +> Note also that `EagerLoadedList` can't filter or sort by fields on relations using dot notation (e.g. `sort('MySubRelation.Title')` won't work). ## Cascading deletions Relationships between objects can cause cascading deletions, if necessary, through configuration of the `cascade_deletes` config on the class that declares the relationship. -[alert] -Declaring `cascade_deletes` implies delete permissions on the listed objects. -Built-in controllers using delete operations check `canDelete()` on the owner, but not on the owned object. -[/alert] +> [!CAUTION] +> Declaring `cascade_deletes` implies delete permissions on the listed objects. +> Built-in controllers using delete operations check `canDelete()` on the owner, but not on the owned object. ```php namespace App\Model; @@ -947,9 +931,8 @@ If your object is versioned, `cascade_deletes` will also act as "cascade unpubli on a parent object will trigger unpublish on the child, similarly to how `owns` causes triggered publishing. See the [versioning docs](/developer_guides/model/versioning) for more information on ownership. -[alert] -If the child model is not versioned, `cascade_deletes` will result in the child record being *deleted* if the parent is unpublished! Be sure to check whether both sides of the relationship are versioned before declaring `cascade_deletes`. -[/alert] +> [!CAUTION] +> If the child model is not versioned, `cascade_deletes` will result in the child record being *deleted* if the parent is unpublished! Be sure to check whether both sides of the relationship are versioned before declaring `cascade_deletes`. ## Cascading duplications @@ -1014,9 +997,8 @@ $dupe = $parent->duplicate(relations: false); $dupe = $parent->duplicate(relations: ['Children']); ``` -[info] -The first parameter in `duplicate()` is `$doWrite` and determines whether the new duplicate record(s) will be written to the database. The second parameter is `$relations`, and it works as described above. -[/info] +> [!NOTE] +> The first parameter in `duplicate()` is `$doWrite` and determines whether the new duplicate record(s) will be written to the database. The second parameter is `$relations`, and it works as described above. ## Adding relations @@ -1038,11 +1020,10 @@ $team->Supporters()->add($supporter); Note that `add()` and `remove()` happen instantaneously. You don't have to call `write()` on anything after using those methods. -[hint] -To set what record is in a `has_one` relation, just set the `ID` field - e.g: `$player->TeamID = $team->ID;` - -Don't forget to write the record (`$player->write();`)! -[/hint] +> [!TIP] +> To set what record is in a `has_one` relation, just set the `ID` field - e.g: `$player->TeamID = $team->ID;` +> +> Don't forget to write the record (`$player->write();`)! ## Custom relations @@ -1070,21 +1051,19 @@ class Team extends DataObject } ``` -[notice] -Adding new records to a filtered `RelationList` like in the example above doesn't automatically set the filtered -criteria on the added record - the record is added to the relation but is otherwise unaltered. - -```php -$newPlayer = Player::create(['Status' => 'Inactive']); -$newPlayer->write(); - -$playersList = Team()->get_one()->Players()->filter('Status', 'Active'); -$playersList->add($newPlayer); -// Still returns 'Inactive' -$status = $newPlayer->Status; -``` - -[/notice] +> [!WARNING] +> Adding new records to a filtered `RelationList` like in the example above doesn't automatically set the filtered +> criteria on the added record - the record is added to the relation but is otherwise unaltered. +> +> ```php +> $newPlayer = Player::create(['Status' => 'Inactive']); +> $newPlayer->write(); +> +> $playersList = Team()->get_one()->Players()->filter('Status', 'Active'); +> $playersList->add($newPlayer); +> // Still returns 'Inactive' +> $status = $newPlayer->Status; +> ``` ## Relations on unsaved objects diff --git a/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md b/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md index 7542da3f2..54c7680aa 100644 --- a/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md +++ b/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md @@ -92,9 +92,8 @@ class Car extends DataObject } ``` -[info] -`Enum` fields will use the first defined value as the default if you don't explicitly declare one. In the example above, the default value would be "New" if it hadn't been declared. -[/info] +> [!NOTE] +> `Enum` fields will use the first defined value as the default if you don't explicitly declare one. In the example above, the default value would be "New" if it hadn't been declared. ## Formatting output @@ -137,38 +136,36 @@ $name = $player->getName(); $name = $player->getName()->LimitCharacters(2); ``` -[hint] -For `DBField` types that represent strings, you can just treat the instance like a string. - -```php -$player = Player::get()->byId(1); -// returns the string "Name: Sam Minnée" -$string = 'Name: ' . $player->Name; -``` - -For other types, we need to make sure we get the value from the `DBField` instance first: - -```php -$player = Player::get()->byId(1); -// where `getAge()` returns a `DBInt` field: -// this will throw a "TypeError: Unsupported operand types: SilverStripe\ORM\FieldType\DBInt + int" -$player->Age + 5; -// returns the correct int value as a result -$player->Age->value + 5; -``` - -That doesn't apply to templates, where we can just treat all `DBField` instances as though they are primitives *or* call methods on them: - -```ss -<% with $player %> - <%-- prints out the name, e.g. Sam Minnée --%> - $Name - <%-- prints out the name in all caps, e.g. SAM MINNÉE --%> - $Name.UpperCase -<% end_with %> -``` - -[/hint] +> [!TIP] +> For `DBField` types that represent strings, you can just treat the instance like a string. +> +> ```php +> $player = Player::get()->byId(1); +> // returns the string "Name: Sam Minnée" +> $string = 'Name: ' . $player->Name; +> ``` +> +> For other types, we need to make sure we get the value from the `DBField` instance first: +> +> ```php +> $player = Player::get()->byId(1); +> // where `getAge()` returns a `DBInt` field: +> // this will throw a "TypeError: Unsupported operand types: SilverStripe\ORM\FieldType\DBInt + int" +> $player->Age + 5; +> // returns the correct int value as a result +> $player->Age->value + 5; +> ``` +> +> That doesn't apply to templates, where we can just treat all `DBField` instances as though they are primitives *or* call methods on them: +> +> ```ss +> <% with $player %> +> <%-- prints out the name, e.g. Sam Minnée --%> +> $Name +> <%-- prints out the name in all caps, e.g. SAM MINNÉE --%> +> $Name.UpperCase +> <% end_with %> +> ``` On the most basic level, the `DBField` classes can be used for simple conversions from one value to another, e.g. to round a number. @@ -191,9 +188,8 @@ DBField::create_field('Date', '1982-01-01')->TimeDiff(); Most objects in Silverstripe CMS extend from [ViewableData](api:SilverStripe\View\ViewableData), which means they know how to present themselves in a view context. Rather than manually returning objects from your custom functions. You can use the `$casting` configuration property. This casting only happens when you get the values in a template, so calling the method in your PHP code will always return the raw value. -[hint] -While these examples are using `DataObject` subclasses, you can use the `$casting` configuration property on *any* `ViewableData` subclass. -[/hint] +> [!TIP] +> While these examples are using `DataObject` subclasses, you can use the `$casting` configuration property on *any* `ViewableData` subclass. ```php namespace App\Model; @@ -299,9 +295,8 @@ class Product extends DataObject } ``` -[hint] -Note that in the example above we've used a PHPDoc comment to indicate that the `$Cost` property is a `float`, even though the database field type is `Int`. This is because the `getCost()` getter method will automatically be used when trying to access `Cost` as a property (i.e. `$product->Cost` will return the result of `$product->getCost()`). -[/hint] +> [!TIP] +> Note that in the example above we've used a PHPDoc comment to indicate that the `$Cost` property is a `float`, even though the database field type is `Int`. This is because the `getCost()` getter method will automatically be used when trying to access `Cost` as a property (i.e. `$product->Cost` will return the result of `$product->getCost()`). ## API documentation diff --git a/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md b/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md index e195a4f63..6546da580 100644 --- a/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md +++ b/en/02_Developer_Guides/00_Model/05_Extending_DataObjects.md @@ -10,9 +10,8 @@ You can add properties and methods to existing [`DataObject`](api:SilverStripe\O The following documentation outlines some common hooks that the [`Extension`](api:SilverStripe\Core\Extension) API provides specifically for managing data records. Note that this is *not* an exhaustive list - we encourage you to look at the source code to see what other extension hooks are available. -[warning] -Avoid using the hooks shown here for checking permissions or validating data - there are specific mechanisms for handling those scenarios. See [permissions](permissions) and [validation](validation) respectively. -[/warning] +> [!WARNING] +> Avoid using the hooks shown here for checking permissions or validating data - there are specific mechanisms for handling those scenarios. See [permissions](permissions) and [validation](validation) respectively. ## `onBeforeWrite` @@ -85,10 +84,9 @@ class Player extends DataObject } ``` -[notice] -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. -[/notice] +> [!WARNING] +> 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. ## Related lessons diff --git a/en/02_Developer_Guides/00_Model/06_SearchFilters.md b/en/02_Developer_Guides/00_Model/06_SearchFilters.md index 8b84255f2..561a1150f 100644 --- a/en/02_Developer_Guides/00_Model/06_SearchFilters.md +++ b/en/02_Developer_Guides/00_Model/06_SearchFilters.md @@ -36,9 +36,8 @@ $players = Player::get()->filterAny([ ]); ``` -[hint] -Notice the syntax - to invoke a `SearchFilter` in the `filter()`/`filterAny()`/`find()` or `exclude()`/`excludeAny()` methods, you add a colon after the field name, followed by the name of the filter (excluding the actual word "filter"). e.g. for a `StartsWithFilter`: `'FieldName:StartsWith'` -[/hint] +> [!TIP] +> Notice the syntax - to invoke a `SearchFilter` in the `filter()`/`filterAny()`/`find()` or `exclude()`/`excludeAny()` methods, you add a colon after the field name, followed by the name of the filter (excluding the actual word "filter"). e.g. for a `StartsWithFilter`: `'FieldName:StartsWith'` Developers can define their own [SearchFilter](api:SilverStripe\ORM\Filters\SearchFilter) if needing to extend the ORM filter and exclude behaviors. @@ -47,23 +46,21 @@ Developers can define their own [SearchFilter](api:SilverStripe\ORM\Filters\Sear `SearchFilter`s can also take modifiers. The modifiers currently supported are `":not"`, `":nocase"`, and `":case"` (though you can implement custom modifiers on your own `SearchFilter` implementations). These negate the filter, make it case-insensitive and make it case-sensitive, respectively. -[info] -The default comparison uses the database's default case sensitivity. For MySQL and MSSQL, this is case-insensitive. For PostgreSQL, this is case-sensitive. But you can declare the default -case sensitivity for your project by setting the `default_case_sensitive` configuration property on `SearchFilter` like so: - -```yml -SilverStripe\ORM\Filters\SearchFilter: - default_case_sensitive: false -``` - -Though note that for backwards compatibility reasons, `ArrayList` is explicitly case sensitive by default. To change that, you must set `ArrayList.default_case_sensitive` to false. - -```yml -SilverStripe\ORM\ArrayList: - default_case_sensitive: false -``` - -[/info] +> [!NOTE] +> The default comparison uses the database's default case sensitivity. For MySQL and MSSQL, this is case-insensitive. For PostgreSQL, this is case-sensitive. But you can declare the default +> case sensitivity for your project by setting the `default_case_sensitive` configuration property on `SearchFilter` like so: +> +> ```yml +> SilverStripe\ORM\Filters\SearchFilter: +> default_case_sensitive: false +> ``` +> +> Though note that for backwards compatibility reasons, `ArrayList` is explicitly case sensitive by default. To change that, you must set `ArrayList.default_case_sensitive` to false. +> +> ```yml +> SilverStripe\ORM\ArrayList: +> default_case_sensitive: false +> ``` ```php // Fetch players that their FirstName is exactly 'Sam' @@ -117,19 +114,17 @@ $players = Player::get()->filter([ ]); ``` -[hint] -You can combine `:not` and either `:nocase` or `:case`. Note that the order doesn't matter - these two calls are equivalent: - -```php -$players = Player::get()->filter([ - 'FirstName:StartsWith:nocase:not' => 'S', -]); -$players = Player::get()->filter([ - 'FirstName:StartsWith:not:nocase' => 'S', -]); -``` - -[/hint] +> [!TIP] +> You can combine `:not` and either `:nocase` or `:case`. Note that the order doesn't matter - these two calls are equivalent: +> +> ```php +> $players = Player::get()->filter([ +> 'FirstName:StartsWith:nocase:not' => 'S', +> ]); +> $players = Player::get()->filter([ +> 'FirstName:StartsWith:not:nocase' => 'S', +> ]); +> ``` ## Related lessons diff --git a/en/02_Developer_Guides/00_Model/07_Permissions.md b/en/02_Developer_Guides/00_Model/07_Permissions.md index d33a7abad..728333a19 100644 --- a/en/02_Developer_Guides/00_Model/07_Permissions.md +++ b/en/02_Developer_Guides/00_Model/07_Permissions.md @@ -11,17 +11,15 @@ checks. Often it makes sense to centralize those checks on the model, regardless The API provides four methods for this purpose: `canEdit()`, `canCreate()`, `canView()` and `canDelete()`. -[hint] -Versioned models have additional permission methods - see [Version specific `can` methods](versioning#permission-methods). -[/hint] +> [!TIP] +> Versioned models have additional permission methods - see [Version specific `can` methods](versioning#permission-methods). 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 `Security::getCurrentUser()`). -[notice] -By default, all `DataObject` subclasses can only be edited, created and viewed by users with the 'ADMIN' permission code. -Make sure you implement these methods for models which should be editable by members with more restrictive permission models. -[/notice] +> [!WARNING] +> By default, all `DataObject` subclasses can only be edited, created and viewed by users with the 'ADMIN' permission code. +> Make sure you implement these methods for models which should be editable by members with more restrictive permission models. In this example, the `MyDataObject` model can be viewed, edited, deleted, and created by any user with the `CMS_ACCESS_CMSMain` permission code, aka "Access to 'Pages' section". @@ -99,11 +97,10 @@ class MyDataObject extends SomeParentObject See the [User Permissions](/developer_guides/security/permissions/) section for more information about defining permissions. -[alert] -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`](api:SilverStripe\Admin\ModelAdmin) or -[`GridField`](api:SilverStripe\Forms\GridField\GridField) already enforce these permissions. -[/alert] +> [!CAUTION] +> 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`](api:SilverStripe\Admin\ModelAdmin) or +> [`GridField`](api:SilverStripe\Forms\GridField\GridField) already enforce these permissions. ## Defining permissions in extensions diff --git a/en/02_Developer_Guides/00_Model/08_SQL_Select.md b/en/02_Developer_Guides/00_Model/08_SQL_Select.md index c494e4e47..7bcca3fb1 100644 --- a/en/02_Developer_Guides/00_Model/08_SQL_Select.md +++ b/en/02_Developer_Guides/00_Model/08_SQL_Select.md @@ -54,15 +54,17 @@ various assumptions the ORM and code based on it have: We'll explain some ways to use the low-level APIs with the full power of SQL, but still maintain a connection to the ORM where possible. -[warning] -Please read our [security topic](/developer_guides/security) to find out -how to properly prepare user input and variables for use in queries -[/warning] +> [!WARNING] +> Please read our [security topic](/developer_guides/security) to find out +> how to properly prepare user input and variables for use in queries ## Usage ### Getting table names +> [!WARNING] +> Because of the way the ORM interacts with class inheritance, some models will spread their data across multiple tables. See [Joining tables for a DataObject inheritance chain](#joins-for-inheritance) below for information about how to handle that scenario. + While you could hardcode table names into your SQL queries, that invites human error and means you have to make sure you know exactly what table stores which data for every class in the class hierarchy of the model you're interested in. Luckily, the [`DataObjectSchema`](api:SilverStripe\ORM\DataObjectSchema) class knows all about the database schema for your `DataObject` models. The following methods in particular may be useful to you: - [`baseDataTable()`](api:SilverStripe\ORM\DataObjectSchema::baseDataTable()): Get the name of the database table which holds the base data (i.e. `ID`, `ClassName`, `Created`, etc) for a given `DataObject` class @@ -71,15 +73,10 @@ While you could hardcode table names into your SQL queries, that invites human e - [`tableForField()`](api:SilverStripe\ORM\DataObjectSchema::tableForField()): Get the table name in the class hierarchy which contains a given field column. - [`tableName()`](api:SilverStripe\ORM\DataObjectSchema::tableName()): Get table name for the given class. Note that this does not confirm a table actually exists (or should exist), but returns the name that would be used if this table did exist. Make sure to call `classHasTable()` before using this table name in a query. -[hint] -While the default database connector will work fine without explicitly ANSI-quoting table names in queries, it is good practice to make sure they are quoted (especially if you're writing these queries in a module that will be publicly shared) to ensure your queries will work on other database connectors such as [`PostgreSQLDatabase`](https://github.com/silverstripe/silverstripe-postgresql) which explicitly require ANSI quoted table names. - -You can do that by passing the raw table name into [`DB::get_conn()->escapeIdentifier()`](api:SilverStripe\ORM\Connect\Database::escapeIdentifier()), which will ensure it is correctly escaped according to the rules of the currently active database connector. -[/hint] - -[notice] -Because of the way the ORM interacts with class inheritance, some models will spread their data across multiple tables. See [Joining tables for a DataObject inheritance chain](#joins-for-inheritance) below for information about how to handle that scenario. -[/notice] +> [!TIP] +> While the default database connector will work fine without explicitly ANSI-quoting table names in queries, it is good practice to make sure they are quoted (especially if you're writing these queries in a module that will be publicly shared) to ensure your queries will work on other database connectors such as [`PostgreSQLDatabase`](https://github.com/silverstripe/silverstripe-postgresql) which explicitly require ANSI quoted table names. +> +> You can do that by passing the raw table name into [`DB::get_conn()->escapeIdentifier()`](api:SilverStripe\ORM\Connect\Database::escapeIdentifier()), which will ensure it is correctly escaped according to the rules of the currently active database connector. ### SELECT @@ -130,9 +127,8 @@ foreach ($result as $row) { } ``` -[info] -There's a lot to this API - we highly recommend that you check out the PHPDoc comments on the methods in this class to learn more about the specific usages of each - for example, the [`addWhere()`](api:SilverStripe\ORM\Queries\SQLSelect::addWhere()) method's PHPDoc includes multiple examples of different syntaxes that can be passed into it. -[/info] +> [!NOTE] +> There's a lot to this API - we highly recommend that you check out the PHPDoc comments on the methods in this class to learn more about the specific usages of each - for example, the [`addWhere()`](api:SilverStripe\ORM\Queries\SQLSelect::addWhere()) method's PHPDoc includes multiple examples of different syntaxes that can be passed into it. The result of [`SQLSelect::execute()`](api:SilverStripe\ORM\Queries\SQLSelect::execute()) is an array lightly wrapped in a database-specific subclass of [`Query`](api:SilverStripe\ORM\Connect\Query). This class implements the [`IteratorAggregate`](https://www.php.net/manual/en/class.iteratoraggregate.php) interface, and provides convenience methods for accessing the data. @@ -328,11 +324,10 @@ With some queries you'll know ahead of time how many values you're including in In those cases, you can use the [`DB::placeholders()`](api:SilverStripe\ORM\DB::placeholders()) method, which prepares these placeholders for you. -[info] -If you need this for some thing other than inclusion in an `IN` SQL operation, you can pass a custom delimiter as the second argument to `DB::placeholders()`. - -Also note that you can pass an integer in as the first argument rather than an array of values, if you want. -[/info] +> [!NOTE] +> If you need this for some thing other than inclusion in an `IN` SQL operation, you can pass a custom delimiter as the second argument to `DB::placeholders()`. +> +> Also note that you can pass an integer in as the first argument rather than an array of values, if you want. Example: Get the fields for all players in a team which has more than 15 wins. @@ -354,14 +349,12 @@ $sqlQuery->setFrom($playerTableName)->where([ $results = $sqlQuery->execute(); ``` -[info] -This is obviously a contrived example - this could easily (and more efficiently) be done using the ORM: - -```php -$players = Player::get()->filter('Teams.Wins:GreaterThan', 15); -``` - -[/info] +> [!NOTE] +> This is obviously a contrived example - this could easily (and more efficiently) be done using the ORM: +> +> ```php +> $players = Player::get()->filter('Teams.Wins:GreaterThan', 15); +> ``` ### Joining tables for a `DataObject` inheritance chain {#joins-for-inheritance} @@ -401,24 +394,22 @@ foreach ($columns as $alias => $ansiQuotedColumn) { } ``` -[hint] -If we want all of the fields for *all* models in the class hierarchy (mimicking `Product::get()` where `Product` is the first subclass of `DataObject` - see the example in the [Introduction to the Data Model and ORM](data_model_and_orm/#subclasses)), we can do this by using a `LEFT JOIN` (like above), ommitting the `WHERE` clause on the `ClassName` field, and making sure we join *all* tables for the inheritance chain regardless of the fields being selected. To do that, make sure you're using the first `DataObject` class as your first main query class (replace `Computer` above with `Product`, in this example), remove the call to `$select->addWhere()`, and add the following code to the end of the above example: - -```php -// Make sure we join all the tables for the model inheritance chain -foreach (ClassInfo::subclassesFor(Product::class, includeBaseClass: false) as $class) { - if ($schema->classHasTable($class)) { - $classTable = $schema->tableName($class); - if (!$select->isJoinedTo($classTable)) { - $quotedClassTable = DB::get_conn()->escapeIdentifier($classTable); - $joinOnClause = $schema->sqlColumnForField(Product::class, 'ID') . ' = ' . $quotedClassTable . '."ID"'; - $select->addLeftJoin($quotedClassTable, $joinOnClause); - } - } -} -``` - -[/hint] +> [!TIP] +> If we want all of the fields for *all* models in the class hierarchy (mimicking `Product::get()` where `Product` is the first subclass of `DataObject` - see the example in the [Introduction to the Data Model and ORM](data_model_and_orm/#subclasses)), we can do this by using a `LEFT JOIN` (like above), ommitting the `WHERE` clause on the `ClassName` field, and making sure we join *all* tables for the inheritance chain regardless of the fields being selected. To do that, make sure you're using the first `DataObject` class as your first main query class (replace `Computer` above with `Product`, in this example), remove the call to `$select->addWhere()`, and add the following code to the end of the above example: +> +> ```php +> // Make sure we join all the tables for the model inheritance chain +> foreach (ClassInfo::subclassesFor(Product::class, includeBaseClass: false) as $class) { +> if ($schema->classHasTable($class)) { +> $classTable = $schema->tableName($class); +> if (!$select->isJoinedTo($classTable)) { +> $quotedClassTable = DB::get_conn()->escapeIdentifier($classTable); +> $joinOnClause = $schema->sqlColumnForField(Product::class, 'ID') . ' = ' . $quotedClassTable . '."ID"'; +> $select->addLeftJoin($quotedClassTable, $joinOnClause); +> } +> } +> } +> ``` ### Common table expressions (CTE aka the `WITH` clause) {#cte} @@ -579,9 +570,8 @@ foreach ($results as $row) { } ``` -[hint] -Note that you do *not* have to call `execute()` with these methods, unlike the abstraction layer in the other examples. This is because you're passing the entire query into the method - you can't change the query after it's passed in, so it gets executed right away. The return type for these methods is the same as the return type for the [`execute()`](api::SilverStripe\ORM\Queries\SQLExpression::execute()) methods on the `SQLExpression` classes. -[/hint] +> [!TIP] +> Note that you do *not* have to call `execute()` with these methods, unlike the abstraction layer in the other examples. This is because you're passing the entire query into the method - you can't change the query after it's passed in, so it gets executed right away. The return type for these methods is the same as the return type for the [`execute()`](api::SilverStripe\ORM\Queries\SQLExpression::execute()) methods on the `SQLExpression` classes. ### Data types diff --git a/en/02_Developer_Guides/00_Model/10_Versioning.md b/en/02_Developer_Guides/00_Model/10_Versioning.md index e1ce7292c..5b2a72e60 100644 --- a/en/02_Developer_Guides/00_Model/10_Versioning.md +++ b/en/02_Developer_Guides/00_Model/10_Versioning.md @@ -13,21 +13,20 @@ Versioning in Silverstripe CMS is handled through the [`Versioned`](api:SilverSt The `Versioned` extension is applied to pages in the CMS (the [`SiteTree`](api:SilverStripe\CMS\Model\SiteTree) class) - along with some other core `DataObject` models such as files - by default. Draft content edited in the CMS can be different from published content shown to your website visitors. -[notice] -There are two complementary modules that improve content editor experience around "owned" nested objects (e.g. elemental blocks). -Those are in experimental status right now, but we would appreciate any feedback and contributions. - -You can check them out on GitHub: - -- -- - -The first one adds extra metadata to versions about object parents at the moment of version creation. -The second module extends CMS History UI adding control over nested objects. - -Here is an example screenshot from `silverstripe/versioned-snapshot-admin`: -![a screenshot of the versioned-snapshot-admin module's "history" interface showing version history of data relations](../../_images/snapshot-admin.png) -[/notice] +> [!WARNING] +> There are two complementary modules that improve content editor experience around "owned" nested objects (e.g. elemental blocks). +> Those are in experimental status right now, but we would appreciate any feedback and contributions. +> +> You can check them out on GitHub: +> +> - +> - +> +> The first one adds extra metadata to versions about object parents at the moment of version creation. +> The second module extends CMS History UI adding control over nested objects. +> +> Here is an example screenshot from `silverstripe/versioned-snapshot-admin`: +> ![a screenshot of the versioned-snapshot-admin module's "history" interface showing version history of data relations](../../_images/snapshot-admin.png) ## Understanding versioning concepts @@ -42,17 +41,15 @@ By default, adding the `Versioned` extension to a `DataObject` will create 2 sta - "Stage" for tracking draft content (aka "draft") - "Live" for tracking content publicly visible (aka "published"). -[info] -Yes, the draft stage is called the "Stage" stage. In this documentation we'll try to differentiate between the stage named "Stage" and the concept of a stage by giving the named stage a capital S and putting quotes around it - but in some cases we'll just refer to it as "draft" because often that's the more intuitive way to think of it. -[/info] +> [!NOTE] +> Yes, the draft stage is called the "Stage" stage. In this documentation we'll try to differentiate between the stage named "Stage" and the concept of a stage by giving the named stage a capital S and putting quotes around it - but in some cases we'll just refer to it as "draft" because often that's the more intuitive way to think of it. Publishing a versioned `DataObject` is equivalent to copying the version from the "Stage" stage to the "Live" stage. If you just want to keep track of the version history of a model's records but you don't need to separate draft and published versions, you can apply the `Versioned` extension to your `DataObject` without stages. This will allow you to keep track of all changes that have been applied to a DataObject and who made them. -[hint] -The `Versioned` class has a `Versioned::DRAFT` constant to refer to the "Stage" stage, and `Versioned::LIVE` to refer to the "Live" stage. It can be useful to use those in your PHP code when you need to refer to the stages. -[/hint] +> [!TIP] +> The `Versioned` class has a `Versioned::DRAFT` constant to refer to the "Stage" stage, and `Versioned::LIVE` to refer to the "Live" stage. It can be useful to use those in your PHP code when you need to refer to the stages. ### Ownership and relations between `DataObject` models {#ownership} @@ -73,11 +70,10 @@ Silverstripe CMS makes this possible by using the concept of *cascade publishing A non-recursive publish operation is also available if you want to publish a new version of a object without cascade publishing all its children. -[alert] -Declaring ownership implies publish permissions on owned objects. -Built-in controllers using cascading publish operations check `canPublish()` -on the owner, but not on the owned object. -[/alert] +> [!CAUTION] +> Declaring ownership implies publish permissions on owned objects. +> Built-in controllers using cascading publish operations check `canPublish()` +> on the owner, but not on the owned object. #### Ownership of unversioned object @@ -104,11 +100,10 @@ Changes to many objects can be grouped together using the [`ChangeSet`](api:Silv Records can be added to a changeset in the CMS by using the "Add to campaign" button that is available on the edit forms of all pages and files. Programmatically, this is done by creating a `ChangeSet` object and invoking its [`addObject(DataObject $record)`](api:SilverStripe\Versioning\ChangeSet::addObject()) method. -[info] -DataObjects can be added to more than one ChangeSet. -Most of the time, these objects contain changes. -A ChangeSet can contain unchanged objects as well. -[/info] +> [!NOTE] +> DataObjects can be added to more than one ChangeSet. +> Most of the time, these objects contain changes. +> A ChangeSet can contain unchanged objects as well. #### Implicit vs. Explicit inclusions @@ -126,6 +121,10 @@ This section explains how to take a regular `DataObject` and add versioning to i ### Applying the `Versioned` extension to your `DataObject` +> [!WARNING] +> Versioning only works if you are adding the extension to the base class. That is, the first subclass +> of `DataObject`. Adding this extension to children of the base class will have unpredictable behaviour. + Adding versioning to a `DataObject` model is as easy as applying the [`Versioned`](api:SilverStripe\Versioned\Versioned) extension to it, either via PHP or YAML configuration. This will apply versioning *with stages*, meaning you can have a draft and a published version of your records. ```php @@ -172,15 +171,9 @@ App\Model\MyStagedModel: - SilverStripe\Versioned\Versioned.versioned ``` -[notice] -The `Versioned` extension is automatically applied to the `SiteTree` class. For more information on extensions see -[extending](/developer_guides/extending/) and the [Configuration](/developer_guides/configuration/) documentation. -[/notice] - -[warning] -Versioning only works if you are adding the extension to the base class. That is, the first subclass -of `DataObject`. Adding this extension to children of the base class will have unpredictable behaviour. -[/warning] +> [!WARNING] +> The `Versioned` extension is automatically applied to the `SiteTree` class. For more information on extensions see +> [extending](/developer_guides/extending/) and the [Configuration](/developer_guides/configuration/) documentation. #### Versioning a `many_many` relation @@ -235,12 +228,11 @@ class ProductCategory extends DataObject By default, `Versioned` will come out of the box with security extensions which restrict the visibility of objects in Draft ("Stage") or Archive viewing mode. -[alert] -As is standard practice, user code should always invoke `canView()` on any object before -rendering it. DataLists do not filter on `canView()` automatically, so this must be -done via user code. This can be achieved either by wrapping `<% if $canView %>;` in -your template, or by implementing your visibility check in PHP. -[/alert] +> [!CAUTION] +> As is standard practice, user code should always invoke `canView()` on any object before +> rendering it. DataLists do not filter on `canView()` automatically, so this must be +> done via user code. This can be achieved either by wrapping `<% if $canView %>;` in +> your template, or by implementing your visibility check in PHP. #### Version specific *can* methods {#permission-methods} @@ -254,9 +246,8 @@ Versioned DataObjects get additional permission check methods to verify what ope These methods accept an optional `Member` argument. If not provided, they will assume you want to check the permission against the current `Member`. When performing a version operation on behalf of a `Member`, you'll probably want to use these methods to confirm they are authorised. -[warning] -Like with the base `can` permission checks, these checks are *not* performed automatically when invoking the associated action via PHP. i.e. if you call `publishSingle()` on a record in your own code, Silverstripe CMS will *not* check if the currently authenticated user has permission to publish the record. Make sure you are performing permission checks by calling these `can` methods before invoking the associated actions. -[/warning] +> [!WARNING] +> Like with the base `can` permission checks, these checks are *not* performed automatically when invoking the associated action via PHP. i.e. if you call `publishSingle()` on a record in your own code, Silverstripe CMS will *not* check if the currently authenticated user has permission to publish the record. Make sure you are performing permission checks by calling these `can` methods before invoking the associated actions. ```php $record = MyRecord::get()->byID(99); @@ -448,9 +439,8 @@ class MyPage extends Page You must declare both `owns` and `cascade_deletes` if you want all publish, unpublish, and archive actions to carry through. -[info] -Note that ownership cannot be used with polymorphic relations (i.e. `has_one` to non-type specific `DataObject`). -[/info] +> [!NOTE] +> Note that ownership cannot be used with polymorphic relations (i.e. `has_one` to non-type specific `DataObject`). #### Unversioned `DataObject` ownership @@ -599,6 +589,9 @@ By default, all records are retrieved from the "Stage" (aka draft) stage, which You can explicitly request a specific stage through various static methods on the `Versioned` class. +> [!TIP] +> Note that in the below examples we just return the `DataList` without executing it. We don't need to execute the query, the reading mode is attached to the `DataList` as soon as it's created via the [`augmentDataQueryCreation()`](api:SilverStripe\Versioned\Versioned::augmentDataQueryCreation()) extension hook implementation. + ```php use SilverStripe\Versioned\Versioned; @@ -640,13 +633,8 @@ $liveRecords = MyRecord::get(); Versioned::set_reading_mode($oldMode); ``` -[info] -`Versioned::set_stage(Versioned::LIVE)` is the equivalent of `Versioned::set_reading_mode('Stage.' . Versioned::LIVE)`. -[/info] - -[hint] -Note that in the above examples we just return the `DataList` without executing it. We don't need to execute the query, the reading mode is attached to the `DataList` as soon as it's created via the [`augmentDataQueryCreation()`](api:SilverStripe\Versioned\Versioned::augmentDataQueryCreation()) extension hook implementation. -[/hint] +> [!NOTE] +> `Versioned::set_stage(Versioned::LIVE)` is the equivalent of `Versioned::set_reading_mode('Stage.' . Versioned::LIVE)`. ### Reading historical versions @@ -662,9 +650,8 @@ $historicalRecord = Versioned::get_version(MyRecord::class, id: 5, version: 6); The record is retrieved as a regular `DataObject` record with its values set to the values it had when that version was originally saved. -[alert] -Saving modifications via `write()` will create a *new* version, rather than modifying the existing one. -[/alert] +> [!CAUTION] +> Saving 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 get the record version data as specialized [`Versioned_Version`](api:SilverStripe\Versioned\Versioned_Version) objects, which expose the same database information as a `DataObject`, but also include information about when and how @@ -841,9 +828,8 @@ Depending on whether staging is enabled, one or more new tables will be created is always created to track historic versions for your model. If staging is enabled this will also create a new `_Live` table once you've rebuilt the database. -[notice] -Note that the "Stage" stage doesn't get its own table - instead, the original table represents the "Stage" stage. -[/notice] +> [!WARNING] +> Note that the "Stage" stage doesn't get its own table - instead, the original table represents the "Stage" stage. - `MyRecord` table: Contains "Stage" (draft) data - `MyRecord_Live` table: Contains "Live" (published) data @@ -883,10 +869,9 @@ these are presented in might still contain dependent objects that are versioned. You can opt for a session base stage setting through the `Versioned.use_session` configuration property. -[warning] -Settin `Versioned.use_session` can lead to leaking unpublished information, e.g. if a live URL is viewed in draft mode, -and the result is cached due to aggressive cache settings (not varying on cookie values). -[/warning] +> [!WARNING] +> Settin `Versioned.use_session` can lead to leaking unpublished information, e.g. if a live URL is viewed in draft mode, +> and the result is cached due to aggressive cache settings (not varying on cookie values). ```php // app/src/Model/MyObject.php @@ -978,11 +963,10 @@ SilverStripe\Control\Director: 'my-objects/$ID': 'App\Control\MyObjectController' ``` -[alert] -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. -[/alert] +> [!CAUTION] +> 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. #### Templates variables @@ -1039,11 +1023,10 @@ You can use the React and GraphQL driven history viewer UI to display historic c comparisons for a versioned DataObject. This is automatically enabled for SiteTree objects and content blocks in [dnadesign/silverstripe-elemental](https://github.com/dnadesign/silverstripe-elemental). -[warning] -Because of the lack of specificity in the `HistoryViewer.Form_ItemEditForm` scope used when injecting the history viewer to the DOM, only one model can have a working history panel at a time, with exception to `SiteTree` which has its own history viewer scope. For example, if you already have `dnadesign/silverstripe-elemental` installed, the custom history viewer instance injected as a part of this documentation will *break* the one provided by the elemental module. - -There are ways you can get around this limitation. You may wish to put some conditional logic in `app/client/src/boot/index.js` below to only perform the transformations if the current location is within a specific model admin, for example. -[/warning] +> [!WARNING] +> Because of the lack of specificity in the `HistoryViewer.Form_ItemEditForm` scope used when injecting the history viewer to the DOM, only one model can have a working history panel at a time, with exception to `SiteTree` which has its own history viewer scope. For example, if you already have `dnadesign/silverstripe-elemental` installed, the custom history viewer instance injected as a part of this documentation will *break* the one provided by the elemental module. +> +> There are ways you can get around this limitation. You may wish to put some conditional logic in `app/client/src/boot/index.js` below to only perform the transformations if the current location is within a specific model admin, for example. If you want to enable the history viewer for a custom versioned DataObject, you will need to: @@ -1052,10 +1035,9 @@ If you want to enable the history viewer for a custom versioned DataObject, you - Register your GraphQL queries and mutations with Injector - Add a HistoryViewerField to the DataObject's `getCMSFields` -[notice] -**Please note:** these examples are given in the context of project-level customisation. You may need to adjust -the webpack configuration slightly for use in a module. -[/notice] +> [!WARNING] +> **Please note:** these examples are given in the context of project-level customisation. You may need to adjust +> the webpack configuration slightly for use in a module. ### Setup {#history-viewer-setup} @@ -1089,14 +1071,9 @@ class MyVersionedObject extends DataObject If you haven't already configured frontend asset (JavaScript/CSS) building for your project, you will need to configure some basic packages to be built in order to enable history viewer functionality. This section includes a very basic webpack configuration which uses [@silverstripe/webpack-config](https://www.npmjs.com/package/@silverstripe/webpack-config). -[hint] -If you have this configured for your project already, ensure you have the `@apollo/client` and `graphql-tag` libraries in your `package.json` -requirements (with the appropriate version constraints from below), and skip this section. -[/hint] - -[notice] -Using `@silverstripe/webpack-config` will keep your transpiled bundle size smaller and ensure you are using the correct versions of `@apollo/client` and `graphql-tag`, as these will automatically be added as [webpack externals](https://webpack.js.org/configuration/externals/). If you are not using that npm package, it is very important you use the correct versions of those dependencies. -[/notice] +> [!TIP] +> If you have this configured for your project already, ensure you have the `@apollo/client` and `graphql-tag` libraries in your `package.json` +> requirements (with the appropriate version constraints from below), and skip this section. You can configure your directory structure like so: @@ -1123,6 +1100,9 @@ You can configure your directory structure like so: } ``` +> [!WARNING] +> Using `@silverstripe/webpack-config` will keep your transpiled bundle size smaller and ensure you are using the correct versions of `@apollo/client` and `graphql-tag`, as these will automatically be added as [webpack externals](https://webpack.js.org/configuration/externals/). If you are not using that npm package, it is very important you use the correct versions of those dependencies. + ```js // webpack.config.js const Path = require('path'); @@ -1151,9 +1131,8 @@ module.exports = [ At this stage, running `yarn build` should correctly build `app/client/dist/js/bundle.js`. -[notice] -Don't forget to [configure your project's "exposed" folders](/developer_guides/templates/requirements/#configuring-your-project-exposed-folders) and run `composer vendor-expose` on the command line so that the browser has access to your new dist JS file. -[/notice] +> [!WARNING] +> Don't forget to [configure your project's "exposed" folders](/developer_guides/templates/requirements/#configuring-your-project-exposed-folders) and run `composer vendor-expose` on the command line so that the browser has access to your new dist JS file. ### Create and use GraphQL schema {#history-viewer-gql} diff --git a/en/02_Developer_Guides/00_Model/11_Scaffolding.md b/en/02_Developer_Guides/00_Model/11_Scaffolding.md index 686d2d79f..3b62e5bfb 100644 --- a/en/02_Developer_Guides/00_Model/11_Scaffolding.md +++ b/en/02_Developer_Guides/00_Model/11_Scaffolding.md @@ -39,9 +39,8 @@ class MyDataObject extends DataObject } ``` -[hint] -It is typically considered a good practice to wrap your modifications in a call to [`beforeUpdateCMSFields()`](api:SilverStripe\ORM\DataObject::beforeUpdateCMSFields()) - the `updateCMSFields()` extension hook is already triggered by `parent::getCMSFields()`, so this is how you ensure any new fields are added before extensions update your fieldlist. -[/hint] +> [!TIP] +> It is typically considered a good practice to wrap your modifications in a call to [`beforeUpdateCMSFields()`](api:SilverStripe\ORM\DataObject::beforeUpdateCMSFields()) - the `updateCMSFields()` extension hook is already triggered by `parent::getCMSFields()`, so this is how you ensure any new fields are added before extensions update your fieldlist. To fully customise your form fields, start with an empty FieldList. @@ -76,28 +75,21 @@ class MyDataObject extends DataObject } ``` -[hint] -It is good practice to invoke the `updateCMSFields()` extension hook afterward, so that extensions in modules can apply their functionality to your field list. -[/hint] +> [!TIP] +> It is good practice to invoke the `updateCMSFields()` extension hook afterward, so that extensions in modules can apply their functionality to your field list. You can also alter the fields of built-in and module `DataObject` classes by implementing `updateCMSFields()` in [your own Extension](/developer_guides/extending/extensions). -[info] -`FormField` scaffolding takes [`$field_labels` config](#field-labels) into account as well. -[/info] +> [!NOTE] +> `FormField` scaffolding takes [`$field_labels` config](#field-labels) into account as well. ## Searchable fields The `$searchable_fields` property uses a mixed array format that can be used to further customise your generated admin system. The default is a set of array values listing the fields. -[info] -`$searchable_fields` will default to use the [`$summary_fields` config](#summary-fields), excluding anything that isn't a database field (such as method calls) if not explicitly defined. -[/info] - -[warning] -If you define a `searchable_fields` configuration, *do not* specify fields that are not stored in the database (such as methods), as this will cause an error. -[/warning] +> [!NOTE] +> `$searchable_fields` will default to use the [`$summary_fields` config](#summary-fields), excluding anything that isn't a database field (such as method calls) if not explicitly defined. ```php namespace App\Model; @@ -113,6 +105,9 @@ class MyDataObject extends DataObject } ``` +> [!WARNING] +> If you define a `searchable_fields` configuration, *do not* specify fields that are not stored in the database (such as methods), as this will cause an error. + ### General search field Tabular views such as `GridField` or `ModelAdmin` include a search bar. The search bar will search across all of your searchable fields by default. It will return a match if the search terms appear in any of the searchable fields. @@ -141,9 +136,8 @@ class MyDataObject extends DataObject By default the general search field uses the name "q". If you already use that field name or search query in your [SearchContext](/developer_guides/search/searchcontext), you can change this to whatever name you prefer either globally or per class: -[hint] -If you set `general_search_field_name` to any empty string, general search will be disabled entirely. Instead, the first field in your searchable fields configuration will be used. -[/hint] +> [!TIP] +> If you set `general_search_field_name` to any empty string, general search will be disabled entirely. Instead, the first field in your searchable fields configuration will be used. ##### Globally change the general search field name via YAML config {#general-field-name-yaml} @@ -199,9 +193,8 @@ class MyDataObject extends DataObject } ``` -[warning] -You may get unexpected results using some filters if you don't disable splitting the query into terms - for example if you use an [ExactMatchFilter](api:SilverStripe\ORM\Filters\ExactMatchFilter), each term in the query *must* exactly match the value in at least one field to get a match. If you disable splitting terms, the whole query must exactly match a field value instead. -[/warning] +> [!WARNING] +> You may get unexpected results using some filters if you don't disable splitting the query into terms - for example if you use an [ExactMatchFilter](api:SilverStripe\ORM\Filters\ExactMatchFilter), each term in the query *must* exactly match the value in at least one field to get a match. If you disable splitting terms, the whole query must exactly match a field value instead. #### Splitting search queries into individual terms @@ -393,9 +386,8 @@ class Player extends DataObject Use a single search field that matches on multiple database fields with `'match_any'`. This also supports specifying a `FormField` and a filter, though it is not necessary to do so. -[alert] -If you don't specify a `FormField`, you must use the name of a real database field as the array key instead of a custom name so that a default field class can be determined. -[/alert] +> [!CAUTION] +> If you don't specify a `FormField`, you must use the name of a real database field as the array key instead of a custom name so that a default field class can be determined. ```php namespace App\Model; @@ -556,9 +548,8 @@ class MyDataObject extends DataObject For any fields *not* defined in `$field_labels`, labels can be localised by defining the name prefixed by the type of field (e.g `db_`, `has_one_`, etc) in your localisation YAML files: -[info] -The class name should be the class that defined the field or relationship. -[/info] +> [!NOTE] +> The class name should be the class that defined the field or relationship. ```yml # app/lang/en.yml @@ -568,9 +559,8 @@ en: has_one_HeroImage: "Hero Image" ``` -[notice] -For relations (such as `has_one_HeroImage` above), this field label applies to the scaffolded form field (an `UploadField` for files, a tab for `has_many`/`many_many`, etc). It does *not* apply to summary or searchable fields with dot notation. -[/notice] +> [!WARNING] +> For relations (such as `has_one_HeroImage` above), this field label applies to the scaffolded form field (an `UploadField` for files, a tab for `has_many`/`many_many`, etc). It does *not* apply to summary or searchable fields with dot notation. Labels you define in `$field_labels` *won't* be overridden by localisation strings. To make those localisable, you will need to override the [`fieldLabels()`](api:SilverStripe\ORM\DataObject) method and explicitly localise those labels yourself: diff --git a/en/02_Developer_Guides/00_Model/13_Managing_Records.md b/en/02_Developer_Guides/00_Model/13_Managing_Records.md index 286b79683..8fff6378c 100644 --- a/en/02_Developer_Guides/00_Model/13_Managing_Records.md +++ b/en/02_Developer_Guides/00_Model/13_Managing_Records.md @@ -69,10 +69,9 @@ class MyParentModel extends DataObject } ``` -[hint] -If the `cms_edit_owner` is in some vendor dependency that you don't control, you can always apply `CMSEditLinkExtension` -and the `cms_edit_owner` via YAML. -[/hint] +> [!TIP] +> If the `cms_edit_owner` is in some vendor dependency that you don't control, you can always apply `CMSEditLinkExtension` +> and the `cms_edit_owner` via YAML. With the above code examples, you can call `CMSEditLink()` on any instance of `MyModel` or `MyParentModel` and it will produce an appropriate edit link for that record (assuming the relations are set up). This can be used, for example, in email reminders @@ -81,8 +80,7 @@ to update content, or as a link (available to admins) on the front-end to go str It is also useful when [making a previewable `DataObject`](../customising_the_admin_interface/preview/), as `CMSEditLink()` is one of the methods in the [CMSPreviewable](api:SilverStripe\ORM\CMSPreviewable) interface. -[info] -`SiteTree` already has `CMSEditLinkExtension` applied, which means any `cms_edit_owner` pointing to a `has_one` relation of -a `SiteTree` will work, assuming the page has a `GridField` for its reciprocal `has_many` relation with a `GridFieldDetailForm` -in it. -[/info] +> [!NOTE] +> `SiteTree` already has `CMSEditLinkExtension` applied, which means any `cms_edit_owner` pointing to a `has_one` relation of +> a `SiteTree` will work, assuming the page has a `GridField` for its reciprocal `has_many` relation with a `GridFieldDetailForm` +> in it. diff --git a/en/02_Developer_Guides/00_Model/How_Tos/Dynamic_Default_Fields.md b/en/02_Developer_Guides/00_Model/How_Tos/Dynamic_Default_Fields.md index bbab5633a..089acc416 100644 --- a/en/02_Developer_Guides/00_Model/How_Tos/Dynamic_Default_Fields.md +++ b/en/02_Developer_Guides/00_Model/How_Tos/Dynamic_Default_Fields.md @@ -5,10 +5,9 @@ summary: Learn how to add default values to your models # Default values and records -[hint] -This page is about defining default values and records in your model class, which only affects *new* records. You can set defaults directly in the database-schema, which affects *existing* records as well. See -[Data Types and Casting](/developer_guides/model/data_types_and_casting/#default-values) for details. -[/hint] +> [!TIP] +> This page is about defining default values and records in your model class, which only affects *new* records. You can set defaults directly in the database-schema, which affects *existing* records as well. See +> [Data Types and Casting](/developer_guides/model/data_types_and_casting/#default-values) for details. ## Static default values @@ -63,47 +62,45 @@ class Dog extends DataObject } ``` -[hint] -This method is called very early in the process of instantiating a new record, before any relations are set for it. If you want to set values based on, for example, a `has_one` relation called `Parent`, you can do that by implementing [`onBeforeWrite()`](/developer_guides/model/extending_dataobjects/#onbeforewrite) or a [setter method](/developer_guides/model/data_types_and_casting/#overriding) - for example: - -```php -namespace App\Model; - -use SilverStripe\ORM\DataObject; - -class Dog extends DataObject -{ - // ... - - public function onBeforeWrite() - { - // Only do this if the record hasn't been written to the database yet (optional) - if (!$this->isInDb()) { - $parent = $this->Parent(); - // Set the FullTitle based on the parent, if one exists - if ($parent->exists()) { - $this->FullTitle = $parent->Title . ': ' . $this->Title; - } else { - $this->FullTitle = $this->Title; - } - } - } - - // or - - public function setFullTitle($value): static - { - $parent = $this->Parent(); - // Set the FullTitle based on the parent, if one exists - if ($parent->exists()) { - $value = $parent->Title . ': ' . $value; - } - return $this->setField('FullTitle', $value); - } -} -``` - -[/hint] +> [!TIP] +> This method is called very early in the process of instantiating a new record, before any relations are set for it. If you want to set values based on, for example, a `has_one` relation called `Parent`, you can do that by implementing [`onBeforeWrite()`](/developer_guides/model/extending_dataobjects/#onbeforewrite) or a [setter method](/developer_guides/model/data_types_and_casting/#overriding) - for example: +> +> ```php +> namespace App\Model; +> +> use SilverStripe\ORM\DataObject; +> +> class Dog extends DataObject +> { +> // ... +> +> public function onBeforeWrite() +> { +> // Only do this if the record hasn't been written to the database yet (optional) +> if (!$this->isInDb()) { +> $parent = $this->Parent(); +> // Set the FullTitle based on the parent, if one exists +> if ($parent->exists()) { +> $this->FullTitle = $parent->Title . ': ' . $this->Title; +> } else { +> $this->FullTitle = $this->Title; +> } +> } +> } +> +> // or +> +> public function setFullTitle($value): static +> { +> $parent = $this->Parent(); +> // Set the FullTitle based on the parent, if one exists +> if ($parent->exists()) { +> $value = $parent->Title . ': ' . $value; +> } +> return $this->setField('FullTitle', $value); +> } +> } +> ``` ## Static default records diff --git a/en/02_Developer_Guides/00_Model/How_Tos/Grouping_DataObject_Sets.md b/en/02_Developer_Guides/00_Model/How_Tos/Grouping_DataObject_Sets.md index 3ebb4d249..828ae5119 100644 --- a/en/02_Developer_Guides/00_Model/How_Tos/Grouping_DataObject_Sets.md +++ b/en/02_Developer_Guides/00_Model/How_Tos/Grouping_DataObject_Sets.md @@ -86,9 +86,8 @@ class ModulePage extends Page } ``` -[notice] -Notice that we're sorting as part of the ORM call. While `GroupedList` does have a `sort()` method, it doesn't work how you might expect, as it returns a sorted copy of the underlying list rather than sorting the list in place. -[/notice] +> [!WARNING] +> Notice that we're sorting as part of the ORM call. While `GroupedList` does have a `sort()` method, it doesn't work how you might expect, as it returns a sorted copy of the underlying list rather than sorting the list in place. The final step is to render this into a template. The `GroupedBy()` method breaks up the set into a number of sets, grouped by the field that is passed as the parameter. diff --git a/en/02_Developer_Guides/01_Templates/01_Syntax.md b/en/02_Developer_Guides/01_Templates/01_Syntax.md index 64a16b83f..5fd8e4900 100644 --- a/en/02_Developer_Guides/01_Templates/01_Syntax.md +++ b/en/02_Developer_Guides/01_Templates/01_Syntax.md @@ -43,10 +43,9 @@ An example of a Silverstripe CMS template is below: ``` -[note] -Templates can be used for more than HTML output. You can use them to output your data as JSON, XML, CSV or any other -text-based format. -[/note] +> [!NOTE] +> Templates can be used for more than HTML output. You can use them to output your data as JSON, XML, CSV or any other +> text-based format. ## Template file location @@ -76,33 +75,30 @@ $Foo.Bar These variables will call a method / field on the object and insert the returned value as a string into the template. +> [!WARNING] +> If you wish to pass arguments to getter functions, you must use the full method name. e.g. `$Thing` will try to access `Thing` as a property, which will ultimately result in `getThing()` being called with no arguments To pass arguments you must use `$getThing('param')`. +> +> Also, arguments must be literals, and cannot be other template variables (`$getThing($variable)` will pass the literal string `'$variable'` to the `getThing()` method). + - `$Foo("param")` will call `$obj->Foo("param")` - `$Foo.Bar` will call `$obj->Foo()->Bar` -[info] -Arguments passed into methods can be any non-array literal type (not just strings), e.g: - -- `$Foo(1)` will pass `1` as an int -- `$Foo(0.5)` will pass `0.5` as a float -- `$Foo(true)` will pass `true` as a boolean -- `$Foo(null)` will pass `null` as a null primitive -- `$Foo("param")`, `$Foo('param')`, and `$Foo(param)` will all pass `'param'` as a string. It is recommended that you always use quotes when passing a string for clarity -[/info] - -[notice] -If you wish to pass arguments to getter functions, you must use the full method name. e.g. `$Thing` will try to access `Thing` as a property, which will ultimately result in `getThing()` being called with no arguments To pass arguments you must use `$getThing('param')`. - -Also, arguments must be literals, and cannot be other template variables (`$getThing($variable)` will pass the literal string `'$variable'` to the `getThing()` method). -[/notice] +> [!NOTE] +> Arguments passed into methods can be any non-array literal type (not just strings), e.g: +> +> - `$Foo(1)` will pass `1` as an int +> - `$Foo(0.5)` will pass `0.5` as a float +> - `$Foo(true)` will pass `true` as a boolean +> - `$Foo(null)` will pass `null` as a null primitive +> - `$Foo("param")`, `$Foo('param')`, and `$Foo(param)` will all pass `'param'` as a string. It is recommended that you always use quotes when passing a string for clarity If a variable returns a string, that string will be inserted into the template. If the variable returns an object, then the system will attempt to render the object through its `forTemplate()` method. If the `forTemplate()` method has not been defined, the system will return an error. -[note] -For more details around how variables are inserted and formatted into a template see -[Formatting, Modifying and Casting Variables](/developer_guides/templates/casting/) -[/note] +> [!NOTE] +> For more details around how variables are inserted and formatted into a template see +> [Formatting, Modifying and Casting Variables](/developer_guides/templates/casting/) Variables can come from your database fields, or custom methods you define on your objects. @@ -128,9 +124,8 @@ class MyObject extends DataObject

You are coming from $UsersIpAddress.

``` -[note] -Method names that begin with `get` will automatically be resolved when their prefix is excluded. For example, the above method call `$UsersIpAddress` would also invoke a method named `getUsersIpAddress()`. -[/note] +> [!NOTE] +> Method names that begin with `get` will automatically be resolved when their prefix is excluded. For example, the above method call `$UsersIpAddress` would also invoke a method named `getUsersIpAddress()`. The variables that can be used in a template vary based on the object currently in scope (see [scope](#scope) below). Scope defines what object the methods get called on. For the standard `Page.ss` template the scope is the current [`ContentController`](api:SilverStripe\CMS\Controllers\ContentController) @@ -164,9 +159,8 @@ A conditional can also use comparisons. <% end_if %> ``` -[notice] -You can technically omit the `$` prefix for variables inside template tags, but this is a legacy behaviour and can result in unexpected behaviour. Variables should have a `$` prefix, and string literals should have quotes. -[/notice] +> [!WARNING] +> You can technically omit the `$` prefix for variables inside template tags, but this is a legacy behaviour and can result in unexpected behaviour. Variables should have a `$` prefix, and string literals should have quotes. Conditionals can also provide the `else` case. @@ -214,9 +208,8 @@ For more nuanced conditions you can use the `!=` operator. Multiple checks can be done using `||`/`or`, or `&&`/ `and`. -[info] -`or` is functionally equivalent to `||` in template conditions, and `and` is functionally equivalent to `&&`. -[/info] +> [!NOTE] +> `or` is functionally equivalent to `||` in template conditions, and `and` is functionally equivalent to `&&`. If *either* of the conditions is true. @@ -273,9 +266,8 @@ include. <% end_with %> ``` -[hint] -Unlike when passing arguments to a function call in templates, arguments passed to a template include can be literals *or* variables. -[/hint] +> [!TIP] +> Unlike when passing arguments to a function call in templates, arguments passed to a template include can be literals *or* variables. ## Looping over lists @@ -294,13 +286,12 @@ collection. This snippet loops over the children of a page, and generates an unordered list showing the `Title` property from each page. -[notice] -The `$Title` inside the loop refers to the Title property on each object that is looped over, not the current page like -the reference of `$Title` outside the loop. - -This demonstrates the concept of scope ([see scope below](#scope)). When inside a `<% loop %>` the scope of the template has changed to the -object that is being looped over. -[/notice] +> [!WARNING] +> The `$Title` inside the loop refers to the Title property on each object that is looped over, not the current page like +> the reference of `$Title` outside the loop. +> +> This demonstrates the concept of scope ([see scope below](#scope)). When inside a `<% loop %>` the scope of the template has changed to the +> object that is being looped over. ### Altering the list @@ -385,10 +376,9 @@ iteration. These are provided by [`SSViewer_BasicIteratorSupport::get_template_i ``` -[info] -A common task is to paginate your lists. See the [Pagination](how_tos/pagination) how to for a tutorial on adding -pagination. -[/info] +> [!NOTE] +> A common task is to paginate your lists. See the [Pagination](how_tos/pagination) how to for a tutorial on adding +> pagination. ### `Modulus` and `MultipleOf` @@ -396,10 +386,9 @@ pagination. `$Modulus` returns the modulus of the numerical position of the item in the data set. You must pass in the number to perform modulus operations to and an optional offset to start from. It returns an integer. -[hint] -`$Modulus` is useful for floated grid CSS layouts. If you want 3 rows across, put `column-$Modulus(3)` as a class and add a -`clear: both` to `.column-1`. -[/hint] +> [!TIP] +> `$Modulus` is useful for floated grid CSS layouts. If you want 3 rows across, put `column-$Modulus(3)` as a class and add a +> `clear: both` to `.column-1`. ```ss <% loop $Children %> @@ -448,9 +437,8 @@ $Foo <%-- returns "3" --%> \$Foo <%-- returns "$Foo" --%> ``` -[hint] -For more information on formatting and casting variables see [Formatting, Modifying and Casting Variables](casting) -[/hint] +> [!TIP] +> For more information on formatting and casting variables see [Formatting, Modifying and Casting Variables](casting) ## Scope @@ -511,9 +499,8 @@ It will create this markup:

Page 'Child 2' is a child of 'MyPage'

``` -[notice] -Each `<% loop %>` or `<% with %>` block results in a change of scope, regardless of how the objects are traversed in the opening statement. See the example below: -[/notice] +> [!WARNING] +> Each `<% loop %>` or `<% with %>` block results in a change of scope, regardless of how the objects are traversed in the opening statement. See the example below: ```ss {$Title} <%-- Page title --%> @@ -598,9 +585,8 @@ You can also use the `$Me` variable, which outputs the current object in scope b $Me ``` -[notice] -If the object does not have a `forTemplate()` method implemented, this will throw an error. -[/notice] +> [!WARNING] +> If the object does not have a `forTemplate()` method implemented, this will throw an error. ## Comments diff --git a/en/02_Developer_Guides/01_Templates/02_Common_Variables.md b/en/02_Developer_Guides/01_Templates/02_Common_Variables.md index 27c28775f..7a59143d7 100644 --- a/en/02_Developer_Guides/01_Templates/02_Common_Variables.md +++ b/en/02_Developer_Guides/01_Templates/02_Common_Variables.md @@ -14,20 +14,18 @@ explained in more detail on the [syntax](syntax#scope) page. Many of the methods scope, and you can specify additional static methods to be available globally in templates by implementing the [TemplateGlobalProvider](api:SilverStripe\View\TemplateGlobalProvider) interface. -[notice] -Want a quick way of knowing what scope you're in? Try putting `$ClassName` in your template. You should see a string -such as `Page` of the object that's in scope. The methods you can call on that object then are any functions, database -properties or relations on the `Page` class, `PageController` class as well as anything from their parent classes and -any extensions applied to them. -[/notice] +> [!WARNING] +> Want a quick way of knowing what scope you're in? Try putting `$ClassName` in your template. You should see a string +> such as `Page` of the object that's in scope. The methods you can call on that object then are any functions, database +> properties or relations on the `Page` class, `PageController` class as well as anything from their parent classes and +> any extensions applied to them. Outputting these variables is only the start, if you want to format or manipulate them before adding them to the template have a read of the [Formatting, Modifying and Casting Variables](casting) documentation. -[alert] -Some of the following only apply when you have the `silverstripe/cms` module installed. If you're using `silverstripe/framework` alone, this -functionality may not be included. -[/alert] +> [!CAUTION] +> Some of the following only apply when you have the `silverstripe/cms` module installed. If you're using `silverstripe/framework` alone, this +> functionality may not be included. ## Base tag @@ -44,9 +42,8 @@ browser knows where to locate your site’s images and CSS files. It renders in the template as `` -[alert] -A `<% base_tag %>` is nearly always required or assumed by Silverstripe CMS to exist. -[/alert] +> [!CAUTION] +> A `<% base_tag %>` is nearly always required or assumed by Silverstripe CMS to exist. ## `CurrentMember` @@ -71,9 +68,8 @@ Most objects within Silverstripe CMS will respond to `$Title` (i.e. they should The CMS module in particular provides two fields to label a page: `Title` and `MenuTitle`. `Title` is the title displayed on the web page, while `MenuTitle` can be a shorter version suitable for size-constrained menus. -[notice] -If `MenuTitle` is left blank by the CMS author, it'll just default to the value in `Title`. -[/notice] +> [!WARNING] +> If `MenuTitle` is left blank by the CMS author, it'll just default to the value in `Title`. ## Page content @@ -84,21 +80,19 @@ $Content It returns the database content of the `Content` field. For subclasses of [`SiteTree`](api:SilverStripe\CMS\Model\SiteTree), this is the value of the WYSIWYG editor but it is also the standard for any object that has a body of content to output. -[info] -Please note that this database content can be "versioned", meaning that draft content edited in the CMS can be different -from published content shown to your website visitors. In templates, you don't need to worry about this distinction. - -The `$Content` variable contains the published content by default, and only preview draft content if explicitly -requested (e.g. by the "preview" feature in the CMS) (see the [versioning documentation](/../model/versioning) for -more details). -[/info] +> [!NOTE] +> Please note that this database content can be "versioned", meaning that draft content edited in the CMS can be different +> from published content shown to your website visitors. In templates, you don't need to worry about this distinction. +> +> The `$Content` variable contains the published content by default, and only preview draft content if explicitly +> requested (e.g. by the "preview" feature in the CMS) (see the [versioning documentation](/../model/versioning) for +> more details). ### `SiteConfig`: global settings -[notice] -`SiteConfig` comes from an optional module that is bundled with the CMS. If you wish to include `SiteConfig` in your framework only -web pages, you'll need to install `silverstripe/siteconfig` via composer. -[/notice] +> [!WARNING] +> `SiteConfig` comes from an optional module that is bundled with the CMS. If you wish to include `SiteConfig` in your framework only +> web pages, you'll need to install `silverstripe/siteconfig` via composer. ```ss $SiteConfig.Title @@ -118,9 +112,8 @@ The `$MetaTags` placeholder in a template returns a segment of HTML appropriate will set up title, keywords and description meta-tags, based on the CMS content and is editable in the 'Meta-data' tab on a per-page basis. -[notice] -If you don’t want to include the title tag use `$MetaTags(false)`. -[/notice] +> [!WARNING] +> If you don’t want to include the title tag use `$MetaTags(false)`. By default `$MetaTags` renders (assuming 5.1.0 is the current version of `silverstripe/framework`): @@ -290,10 +283,9 @@ the `CMS` or a custom list of data. This originates in the `Versioned` extension See [Looping Over Lists](syntax#looping-over-lists) for more information about looping in general. -[alert] -For doing your website navigation most likely you'll want to use `$Menu` since its independent of the page -context. -[/alert] +> [!CAUTION] +> For doing your website navigation most likely you'll want to use `$Menu` since its independent of the page +> context. ### `ChildrenOf` @@ -307,11 +299,10 @@ Will create a list of the children of the given page, as identified by its `URLS because it's not dependent on the context of the current page. For example, it would allow you to list all staff member pages underneath a "staff" holder on any page, regardless if its on the top level or elsewhere. -[notice] -Because variables can't be passed into method calls from templates (see [Syntax > Variables](syntax#variables)), this requires you to hardcode some value into your template - which means you must ensure you have a page added in the CMS with that URL segment - -A more robust way to implement this would be to add a helper method in your page controller which dynamically gets the appropriate page (if one exists). -[/notice] +> [!WARNING] +> Because variables can't be passed into method calls from templates (see [Syntax > Variables](syntax#variables)), this requires you to hardcode some value into your template - which means you must ensure you have a page added in the CMS with that URL segment +> +> A more robust way to implement this would be to add a helper method in your page controller which dynamically gets the appropriate page (if one exists). ### `AllChildren` @@ -335,9 +326,8 @@ preference, `AllChildren` does not filter by `ShowInMenus`. `$Menu(1)` returns the top-level menu of the website. You can also create a sub-menu using `$Menu(2)`, and so forth. -[notice] -Pages with the `ShowInMenus` property set to `false` will be filtered out. -[/notice] +> [!WARNING] +> Pages with the `ShowInMenus` property set to `false` will be filtered out. ## Access to a specific page @@ -401,19 +391,17 @@ There are a number of arguments that can be passed in - see [SiteTree::Breadcrum By default, it uses the template defined in `templates/BreadcrumbsTemplate.ss` of the `silverstripe/cms` module. -[info] -To customise the markup that `$Breadcrumbs` generates, copy `templates/BreadcrumbsTemplate.ss` -from the `silverstripe/cms` module to your theme (e.g.: `themes/your-theme/templates/BreadcrumbsTemplate.ss`). -Modify the newly copied template and flush your Silverstripe CMS cache. -[/info] +> [!NOTE] +> To customise the markup that `$Breadcrumbs` generates, copy `templates/BreadcrumbsTemplate.ss` +> from the `silverstripe/cms` module to your theme (e.g.: `themes/your-theme/templates/BreadcrumbsTemplate.ss`). +> Modify the newly copied template and flush your Silverstripe CMS cache. ## `SilverStripeNavigator` The [SilverStripeNavigator](api:SilverStripe\Admin\Navigator\SilverStripeNavigator) can be used on the front end for any page using a [ContentController](api:SilverStripe\CMS\Controllers\ContentController). It provides useful functionality for content authors such as showing whether the page being viewed is in published or draft mode, giving links to swap viewing modes, and a link to the CMS edit form for that page. -[warning] -It's recommended to only display this for logged on users who have access to the CMS. -[/warning] +> [!WARNING] +> It's recommended to only display this for logged on users who have access to the CMS. ```ss <% if $HasPerm('CMS_ACCESS') %>$SilverStripeNavigator<% end_if %> diff --git a/en/02_Developer_Guides/01_Templates/03_Requirements.md b/en/02_Developer_Guides/01_Templates/03_Requirements.md index 3b4537d6a..24fe9ecfe 100644 --- a/en/02_Developer_Guides/01_Templates/03_Requirements.md +++ b/en/02_Developer_Guides/01_Templates/03_Requirements.md @@ -88,9 +88,8 @@ You can require resources using the `require` template statement. Also see [Direct resource urls](#direct-resource-urls) below if you need to include the resource URL directly in your template. -[alert] -Requiring resources from the template is restricted compared to the PHP API. -[/alert] +> [!CAUTION] +> Requiring resources from the template is restricted compared to the PHP API. ## PHP requirements API @@ -224,18 +223,16 @@ Requirements::combine_files( ); ``` -[alert] -To make debugging easier in your local environment, combined files is disabled when running your application in `dev` -mode. You can re-enable dev combination by setting `Requirements_Backend.combine_in_dev` to true. -[/alert] +> [!CAUTION] +> To make debugging easier in your local environment, combined files is disabled when running your application in `dev` +> mode. You can re-enable dev combination by setting `Requirements_Backend.combine_in_dev` to true. ### Configuring combined file storage Silverstripe CMS provides an API for combining multiple resource files together into a single file to reduce the number of network calls required. -[notice] -It is generally accepted that if your webserver supports HTTP/2, multiple smaller resource files are better than a larger combined file. If you are using Apache, you will need to use php-fpm to support HTTP/2. -[/notice] +> [!WARNING] +> It is generally accepted that if your webserver supports HTTP/2, multiple smaller resource files are better than a larger combined file. If you are using Apache, you will need to use php-fpm to support HTTP/2. In some situations or server configurations, it may be necessary to customise the behaviour of generated JavaScript files in order to ensure that current files are served in requests. @@ -245,9 +242,8 @@ where `` represents the hash of the source files used to generate that con as used by the [`AssetStore`](api:SilverStripe\Assets\Storage\AssetStore) backend, is used for this storage, but it can be substituted for any other backend. -[info] -Note that these combined files are stored as assets (by default in the `public/assets` directory), rather than being stored with other resources in your `public/_resources` directory. -[/info] +> [!NOTE] +> Note that these combined files are stored as assets (by default in the `public/assets` directory), rather than being stored with other resources in your `public/_resources` directory. You can also use any of the below options in order to tweak this behaviour: @@ -340,11 +336,10 @@ Requirements::combine_files('print.css', $printStylesheets, 'print'); By default, all requirements files are flushed (deleted) when manifests are flushed (see [Flushing](/developer_guides/execution_pipeline/manifests/#flushing)). This can be disabled by setting the `Requirements.disable_flush_combined` config to `true`. -[alert] -When combining CSS files, take care of relative urls, as these will not be re-written to match -the destination location of the resulting combined CSS unless you have set the -`Requirements_Backend.resolve_relative_css_refs` configuration property to `true`. -[/alert] +> [!CAUTION] +> When combining CSS files, take care of relative urls, as these will not be re-written to match +> the destination location of the resulting combined CSS unless you have set the +> `Requirements_Backend.resolve_relative_css_refs` configuration property to `true`. ### Combined JS files @@ -383,9 +378,8 @@ use SilverStripe\View\Requirements; Requirements::clear('modulename/javascript/some-lib.js'); ``` -[alert] -Depending on where you call this command, a Requirement might be *re-included* afterwards. -[/alert] +> [!CAUTION] +> Depending on where you call this command, a Requirement might be *re-included* afterwards. ## Blocking @@ -399,20 +393,18 @@ One common example is to block `jquery.js` which would otherwise be added to the Requirements::block('some/module:client/dist/jquery.js'); ``` -[alert] -The CMS also uses the `Requirements` system, and its operation can be affected by `block()` calls. Avoid this by -limiting the scope of your blocking operations, e.g. in `init()` of your controller. -[/alert] +> [!CAUTION] +> The CMS also uses the `Requirements` system, and its operation can be affected by `block()` calls. Avoid this by +> limiting the scope of your blocking operations, e.g. in `init()` of your controller. ## Inclusion order Requirements acts like a stack, where everything is rendered sequentially in the order it was included. There is no way to change inclusion-order, other than using *Requirements::clear* and rebuilding the whole set of requirements. -[alert] -Inclusion order is both relevant for CSS and JavaScript files in terms of dependencies, inheritance and overlays - be -careful when messing with the order of requirements. -[/alert] +> [!CAUTION] +> Inclusion order is both relevant for CSS and JavaScript files in terms of dependencies, inheritance and overlays - be +> careful when messing with the order of requirements. ## JavaScript placement @@ -458,9 +450,8 @@ If you want to get a resource for a *specific* theme or from somewhere that is n ``` -[hint] -Notice the `vendor/module:some/path/to/file.jpg` syntax (used to get a resource from a specific module) is only valid for the `$resourceURL()` helper method. It won't work for `themedResourceURL()`. -[/hint] +> [!TIP] +> Notice the `vendor/module:some/path/to/file.jpg` syntax (used to get a resource from a specific module) is only valid for the `$resourceURL()` helper method. It won't work for `themedResourceURL()`. ### Resource URLs or filepaths from a PHP context diff --git a/en/02_Developer_Guides/01_Templates/04_Rendering_Templates.md b/en/02_Developer_Guides/01_Templates/04_Rendering_Templates.md index a7e79c4dd..5bf29fbbe 100644 --- a/en/02_Developer_Guides/01_Templates/04_Rendering_Templates.md +++ b/en/02_Developer_Guides/01_Templates/04_Rendering_Templates.md @@ -77,38 +77,34 @@ $this->customise($data)->renderWith(['Coach_Message', 'Page']); This will look for a global `templates/Coach_Message.ss` template, and if it doesn't find one, will use `templates/Page.ss` as the main template. Then, when it encounters `$Layout` in that template, it will find and use the `templates/Layout/Coach_Message.ss` file to substitute that variable. -[hint] -You will often have templates named after specific classes, as discussed in [template types and locations](template_inheritance/#template-types-and-locations). In that case, you can simply use `MyClass::class` syntax here. e.g: - -```php -use App\Model\Coach; - -$this->customise($data)->renderWith([Coach::class . '_Message', Page::class]); -``` - -This will search for the following templates: - -- `templates/App/Model/Coach_Message.ss` -- `templates/Page.ss` -- `templates/App/Model/Layout/Coach_Message.ss` -- `templates/Layout/Page.ss` - -[/hint] +> [!TIP] +> You will often have templates named after specific classes, as discussed in [template types and locations](template_inheritance/#template-types-and-locations). In that case, you can simply use `MyClass::class` syntax here. e.g: +> +> ```php +> use App\Model\Coach; +> +> $this->customise($data)->renderWith([Coach::class . '_Message', Page::class]); +> ``` +> +> This will search for the following templates: +> +> - `templates/App/Model/Coach_Message.ss` +> - `templates/Page.ss` +> - `templates/App/Model/Layout/Coach_Message.ss` +> - `templates/Layout/Page.ss` See [template types and locations](template_inheritance/#template-types-and-locations) for more information. -[info] -Most classes in Silverstripe CMS you want in your template extend `ViewableData` and allow you to call `renderWith`. This -includes [Controller](api:SilverStripe\Control\Controller), [FormField](api:SilverStripe\Forms\FormField) and [DataObject](api:SilverStripe\ORM\DataObject) instances. - -```php -$controller->renderWith([MyController::class, MyBaseController::class]); - -use SilverStripe\Security\Security; -Security::getCurrentUser()->renderWith('Member_Profile'); -``` - -[/info] +> [!NOTE] +> Most classes in Silverstripe CMS you want in your template extend `ViewableData` and allow you to call `renderWith`. This +> includes [Controller](api:SilverStripe\Control\Controller), [FormField](api:SilverStripe\Forms\FormField) and [DataObject](api:SilverStripe\ORM\DataObject) instances. +> +> ```php +> $controller->renderWith([MyController::class, MyBaseController::class]); +> +> use SilverStripe\Security\Security; +> Security::getCurrentUser()->renderWith('Member_Profile'); +> ``` ## Advanced use cases @@ -138,27 +134,26 @@ class MyPageController extends PageController } ``` -[hint] -`Controller` already has a shortcut for the above scenario. Instead of explicitly calling `renderWith()` above, you can declare a template with the following naming convension: `[modelOrControllerClass]_[action].ss` e.g. `Page_iwantmyajax.ss`, `HomePage_iwantmyajax.ss`, or `PageController_iwantmyajax.ss`. - -With a template that follows that naming convention in place, the PHP for the `iwantmyajax()` becomes: - -```php -public function iwantmyajax() -{ - if (!Director::is_ajax()) { - return $this->httpError(400); - } - // will feed $this into $this->prepareResponse(), which will render $this using templates defined - // in $this->getViewer() - return $this; -} -``` - -This ultimately uses [`SSViewer::get_templates_by_class()`](api::SilverStripe\View\SSViewer::get_templates_by_class()) to find the templates for the class or its parent classes with the action as a suffix. - -If you don't have any logic to add in the action, you can forego implementing a method altogether - all you need is to add the action name in the `$allowed_actions` configuration array and make sure you have an appropriately named template. -[/hint] +> [!TIP] +> `Controller` already has a shortcut for the above scenario. Instead of explicitly calling `renderWith()` above, you can declare a template with the following naming convension: `[modelOrControllerClass]_[action].ss` e.g. `Page_iwantmyajax.ss`, `HomePage_iwantmyajax.ss`, or `PageController_iwantmyajax.ss`. +> +> With a template that follows that naming convention in place, the PHP for the `iwantmyajax()` becomes: +> +> ```php +> public function iwantmyajax() +> { +> if (!Director::is_ajax()) { +> return $this->httpError(400); +> } +> // will feed $this into $this->prepareResponse(), which will render $this using templates defined +> // in $this->getViewer() +> return $this; +> } +> ``` +> +> This ultimately uses [`SSViewer::get_templates_by_class()`](api::SilverStripe\View\SSViewer::get_templates_by_class()) to find the templates for the class or its parent classes with the action as a suffix. +> +> If you don't have any logic to add in the action, you can forego implementing a method altogether - all you need is to add the action name in the `$allowed_actions` configuration array and make sure you have an appropriately named template. ## Rendering arbitrary data in templates @@ -199,9 +194,8 @@ class MyPageController extends PageController } ``` -[notice] -A common mistake is trying to loop over an array directly in a template - this won't work. You'll need to wrap the array in some `ViewableData` instance as mentioned above. -[/notice] +> [!WARNING] +> A common mistake is trying to loop over an array directly in a template - this won't work. You'll need to wrap the array in some `ViewableData` instance as mentioned above. ## Related lessons diff --git a/en/02_Developer_Guides/01_Templates/06_Themes.md b/en/02_Developer_Guides/01_Templates/06_Themes.md index 7408097a9..982a4673e 100644 --- a/en/02_Developer_Guides/01_Templates/06_Themes.md +++ b/en/02_Developer_Guides/01_Templates/06_Themes.md @@ -25,10 +25,9 @@ composer require my_vendor/my_theme [version] *Note:* `[version]` should be replaced with a version constraint if you know it, otherwise leave it blank to pull the latest version compatible with your project. -[alert] -As you've added new files to your Silverstripe CMS installation, make sure you clear the Silverstripe CMS cache by appending -`?flush=1` to your website URL (e.g `https://www.example.com/?flush=1`). -[/alert] +> [!CAUTION] +> As you've added new files to your Silverstripe CMS installation, make sure you clear the Silverstripe CMS cache by appending +> `?flush=1` to your website URL (e.g `https://www.example.com/?flush=1`). ### Configuring themes @@ -81,9 +80,8 @@ In the above configuration, `$public` and `$default` are special placeholders. Any resources you put in that directory can be accessed the same way you access regular theme resources, such as via [the requirements API](requirements) or the [`ThemeResourceLoader`](api:SilverStripe\View\ThemeResourceLoader). -[warning] -We recommend you don't include any templates in the public directory, as doing so could expose sensitive information such as information about your database schema. -[/warning] +> [!WARNING] +> We recommend you don't include any templates in the public directory, as doing so could expose sensitive information such as information about your database schema. `$default` refers to all modules which have a `template/` directory (including your project). Typically this goes at the end of the themes configuration, so that your themes take precedence over any templates provided by modules. diff --git a/en/02_Developer_Guides/01_Templates/09_Casting.md b/en/02_Developer_Guides/01_Templates/09_Casting.md index ca8a71c79..52d32dd94 100644 --- a/en/02_Developer_Guides/01_Templates/09_Casting.md +++ b/en/02_Developer_Guides/01_Templates/09_Casting.md @@ -47,10 +47,9 @@ For every field used in templates, a casting helper will be applied. This will f `casting` helper on your model specific to that field, and will fall back to the `default_cast` config in case none are specified. -[note] -By default, all content without a type explicitly defined in a `$casting` array will use the `ViewableData.default_cast` configuration. By default, -that configuration is set to `Text`, so HTML characters are escaped. -[/note] +> [!NOTE] +> By default, all content without a type explicitly defined in a `$casting` array will use the `ViewableData.default_cast` configuration. By default, +> that configuration is set to `Text`, so HTML characters are escaped. ### Common casting types @@ -116,16 +115,14 @@ All `DBField` instances share the following useful methods for formatting their - [`URLATT()`](api:SilverStripe\ORM\FieldType\DBField::URLATT()) - encodes strings for use in URLs via [`urlencode()`](https://www.php.net/manual/en/function.urlencode.php) - [`JSON()`](api:SilverStripe\ORM\FieldType\DBField::JSON()) - encodes the value as a JSON string via [`json_encode()`](https://www.php.net/manual/en/function.json-encode.php) -[info] -See [the API documentation](api:SilverStripe\ORM\FieldType) for all the formatting methods available to you for the various field types. -[/info] +> [!NOTE] +> See [the API documentation](api:SilverStripe\ORM\FieldType) for all the formatting methods available to you for the various field types. ## Escaping HTML values in templates {#escaping} -[notice] -For specific security advice related to escaping values, see the [Security](/developer_guides/security/secure_coding/#xss-cross-site-scripting) -documentation. -[/notice] +> [!WARNING] +> For specific security advice related to escaping values, see the [Security](/developer_guides/security/secure_coding/#xss-cross-site-scripting) +> documentation. The concept of escaping values in templates is ultimately just a combination of formatting and casting. @@ -136,12 +133,11 @@ See the [casting](#casting) section above for instructions on configuring your model to declare casting types for fields, and how some of the more common casting types affect escaping. -[info] -In addition to escaping via casting, `DBField` instances have an `escape_type` configuration property which is -either set to `"xml"` or `"raw"`. This configuration tells you whether XML content will be escaped or not, but does -*not* actually directly affect the casting of the value in templates. That is determined by what is returned from -the `forTemplate()` method (or any method explicitly called from within the template). -[/info] +> [!NOTE] +> In addition to escaping via casting, `DBField` instances have an `escape_type` configuration property which is +> either set to `"xml"` or `"raw"`. This configuration tells you whether XML content will be escaped or not, but does +> *not* actually directly affect the casting of the value in templates. That is determined by what is returned from +> the `forTemplate()` method (or any method explicitly called from within the template). ### Escape methods in templates @@ -150,15 +146,12 @@ Within the template, fields can have their encoding customised at a certain leve See the [formatting](#formatting) section above for some of the more common formatting methods available and how they affect escaping. -[hint] -If you are unsure of whether the field has been cast to `HTMLText` but you know -it contains safe HTML content, you can use `.RAW` to ensure the HTML is not escaped. -[/hint] - -[warning] -Be careful using `.RAW` on non HTML field types - if the value being formatted includes content provided -by the user you could be introducing attack vectors for cross-site scripting attacks. -[/warning] +> [!TIP] +> If you are unsure of whether the field has been cast to `HTMLText` but you know +> it contains safe HTML content, you can use `.RAW` to ensure the HTML is not escaped. +> +> But be careful using `.RAW` on non HTML field types - if the value being formatted includes content provided +> by the user you could be introducing attack vectors for cross-site scripting attacks. ## Cast summary methods diff --git a/en/02_Developer_Guides/01_Templates/11_Partial_Template_Caching.md b/en/02_Developer_Guides/01_Templates/11_Partial_Template_Caching.md index beccd1bbd..9b6777ec0 100644 --- a/en/02_Developer_Guides/01_Templates/11_Partial_Template_Caching.md +++ b/en/02_Developer_Guides/01_Templates/11_Partial_Template_Caching.md @@ -19,9 +19,8 @@ is fetched from a [cache backend](../performance/caching), instead of being rege This is not a definitive example of the syntax, but it shows the most common use case. -[note] -See also the [Complete syntax definition](#complete-syntax-definition) section -[/note] +> [!NOTE] +> See also the [Complete syntax definition](#complete-syntax-definition) section The key parts are `$CacheKey` and `$CacheCondition`. The following sections explain every one of them in more detail. @@ -32,9 +31,8 @@ The following sections explain every one of them in more detail. Defines a unique key for the cache storage. -[warning] -Avoid heavy computations in `$CacheKey` as it is evaluated for every template render. -[/warning] +> [!WARNING] +> Avoid heavy computations in `$CacheKey` as it is evaluated for every template render. The syntax is an optional list of template expressions delimited by commas: @@ -152,10 +150,9 @@ Without a condition: - your cache backend will always be queried for cache (for every template render) - your cache backend may be cluttered with redundant and useless data -[warning] -The `$CacheCondition` value is evaluated on every template render and should be as lightweight as possible. -If you need a complex condition, it may be sensible to calculate the condition in `onBeforeWrite()` for your model and store the result in the database. -[/warning] +> [!WARNING] +> The `$CacheCondition` value is evaluated on every template render and should be as lightweight as possible. +> If you need a complex condition, it may be sensible to calculate the condition in `onBeforeWrite()` for your model and store the result in the database. ## Cache storage @@ -165,11 +162,10 @@ By default, it is initialised by `SilverStripe\Core\Cache\DefaultCacheFactory` w - `namespace: "cacheblock"` - `defaultLifetime: 600` -[note] -The defaultLifetime is in seconds, so a value of `600` means every cache record expires in 10 minutes. -If you have good `$CacheKey` and `$CacheCondition` implementations, you may want to tune these settings to -improve performance. -[/note] +> [!NOTE] +> The defaultLifetime is in seconds, so a value of `600` means every cache record expires in 10 minutes. +> If you have good `$CacheKey` and `$CacheCondition` implementations, you may want to tune these settings to +> improve performance. Example below shows how to set partial cache expiry to one hour. @@ -214,10 +210,9 @@ The template processor will transparently flatten the structure into something s <% cached $PageKey %><%-- Footer goes here --%><% end_cached %> ``` -[note] -`$PageKey` is used twice, but evaluated only once per render because of [template object caching](caching/#object-caching). -If the body section should also be cached with the same requirements as the header and footer sections, it may make sense to use `<% cached $PageKey, $BodyKey %>`. -[/note] +> [!NOTE] +> `$PageKey` is used twice, but evaluated only once per render because of [template object caching](caching/#object-caching). +> If the body section should also be cached with the same requirements as the header and footer sections, it may make sense to use `<% cached $PageKey, $BodyKey %>`. ## Uncached @@ -238,45 +233,42 @@ In this example, the body content is not cached, but the header and footer secti Because of the nested block flattening (see above), it works seamlessly on any level of depth. -[warning] -The `uncached` block only works on the lexical level. -If you have a template that caches content rendering another template with included uncached blocks, -those will not have any effect on the parent template caching blocks. -[/warning] +> [!WARNING] +> The `uncached` block only works on the lexical level. +> If you have a template that caches content rendering another template with included uncached blocks, +> those will not have any effect on the parent template caching blocks. ## Nesting in LOOP and IF blocks Currently, a cache block cannot be included in `if` and `loop` blocks. The template engine will throw an error letting you know if you've done this. -[note] -You may often get around this using aggregates or by un-nesting the block. - -For example: - -```ss -<% cached $LastEdited %> - <% loop $Children %> - <% cached $Up.LastEdited, $LastEdited %> - $Name - <% end_cached %> - <% end_loop %> -<% end_cached %> -``` - -Might be re-written (and more efficient) as something like this: - -```ss -<% cached $LastEdited %> - <% cached $LastEdited, $AllChildren.max('LastEdited') %> - <% loop $Children %> - $Name - <% end_loop %> - <% end_cached %> -<% end_cached %> -``` - -[/note] +> [!NOTE] +> You may often get around this using aggregates or by un-nesting the block. +> +> For example: +> +> ```ss +> <% cached $LastEdited %> +> <% loop $Children %> +> <% cached $Up.LastEdited, $LastEdited %> +> $Name +> <% end_cached %> +> <% end_loop %> +> <% end_cached %> +> ``` +> +> Might be re-written (and more efficient) as something like this: +> +> ```ss +> <% cached $LastEdited %> +> <% cached $LastEdited, $AllChildren.max('LastEdited') %> +> <% loop $Children %> +> $Name +> <% end_loop %> +> <% end_cached %> +> <% end_cached %> +> ``` ## Unless (syntax sugar) diff --git a/en/02_Developer_Guides/01_Templates/How_Tos/02_Pagination.md b/en/02_Developer_Guides/01_Templates/How_Tos/02_Pagination.md index 1023a85dd..9cb8e635b 100644 --- a/en/02_Developer_Guides/01_Templates/How_Tos/02_Pagination.md +++ b/en/02_Developer_Guides/01_Templates/How_Tos/02_Pagination.md @@ -35,10 +35,9 @@ class MyPageController extends PageController } ``` -[notice] -Note that the concept of "pages" used in pagination does not necessarily mean that we're dealing with `Page` classes, -it's just a term to describe a sub-collection of the list. -[/notice] +> [!WARNING] +> Note that the concept of "pages" used in pagination does not necessarily mean that we're dealing with `Page` classes, +> it's just a term to describe a sub-collection of the list. There are two ways to generate pagination controls: [PaginatedList::Pages()](api:SilverStripe\ORM\PaginatedList::Pages()) and [PaginatedList::PaginationSummary()](api:SilverStripe\ORM\PaginatedList::PaginationSummary()). In this example we will use `PaginationSummary()`. diff --git a/en/02_Developer_Guides/02_Controllers/01_Introduction.md b/en/02_Developer_Guides/02_Controllers/01_Introduction.md index 24d52773b..a9447c2a7 100644 --- a/en/02_Developer_Guides/02_Controllers/01_Introduction.md +++ b/en/02_Developer_Guides/02_Controllers/01_Introduction.md @@ -8,10 +8,9 @@ summary: A brief look at the definition of a Controller, creating actions and ho The following example is for a simple [`Controller`](api:SilverStripe\Control\Controller) class. When building off the Silverstripe CMS you will subclass the base `Controller` class. -[info] -If you're using the `cms` module and dealing with [`SiteTree`](api:SilverStripe\CMS\Model\SiteTree) records then for your custom page controllers you -would extend [`ContentController`](api:SilverStripe\CMS\Controllers\ContentController) or `PageController`. -[/info] +> [!NOTE] +> If you're using the `cms` module and dealing with [`SiteTree`](api:SilverStripe\CMS\Model\SiteTree) records then for your custom page controllers you +> would extend [`ContentController`](api:SilverStripe\CMS\Controllers\ContentController) or `PageController`. ```php // app/src/Control/TeamController.php @@ -34,21 +33,19 @@ class TeamController extends Controller } ``` -[warning] -When choosing names for actions, avoid using the same name you've used for relations on the model the controller represents. If you have relations with the same name as controller actions, templates rendered for that controller which refer to the relation won't render as expected - they will attempt to render the action where you expect to be using the relation. - -For example if the controller above was for a `Team` model which had a `Players` relation, the action should not also be named `players`. Something like `showPlayers` would be more appropriate. -[/warning] +> [!WARNING] +> When choosing names for actions, avoid using the same name you've used for relations on the model the controller represents. If you have relations with the same name as controller actions, templates rendered for that controller which refer to the relation won't render as expected - they will attempt to render the action where you expect to be using the relation. +> +> For example if the controller above was for a `Team` model which had a `Players` relation, the action should not also be named `players`. Something like `showPlayers` would be more appropriate. ## Routing We need to define the URL that this controller can be accessed on. In our case, the `TeamsController` should be visible at `https://www.example.com/teams/` and the `players` custom action is at `https://www.example.com/team/players/`. -[info] -If you're extending `ContentController` or `PageController` for your `SiteTree` records you don't need to define the routes value as the `cms` handles -routing for those. -[/info] +> [!NOTE] +> If you're extending `ContentController` or `PageController` for your `SiteTree` records you don't need to define the routes value as the `cms` handles +> routing for those. ```yml # app/_config/routes.yml @@ -61,9 +58,8 @@ SilverStripe\Control\Director: 'teams//$Action/$ID/$Name': 'App\Control\TeamController' ``` -[alert] -Make sure that after modifying the `routes.yml` file you clear your Silverstripe CMS caches using `?flush=1`. -[/alert] +> [!CAUTION] +> Make sure that after modifying the `routes.yml` file you clear your Silverstripe CMS caches using `?flush=1`. For more information about creating custom routes, see the [Routing](routing) documentation. @@ -84,12 +80,11 @@ Action methods can return one of four things: 1. an `HTTPResponse`. This can either be a new response or `$this->getResponse()`. 1. `$this` or `$this->customise()`. This will render the controller using the appropriate template and set the rendered result as the body for the current `HTTPResponse`. -[hint] - -- returning `$this` is the equivalent of returning an empty array. -- returning `$this->customise()` is the equivalent of returning an array with data. - -[/hint] +> [!TIP] +> There are a couple of things to note here: +> +> - returning `$this` is the equivalent of returning an empty array. +> - returning `$this->customise()` is the equivalent of returning an array with data. See [templates](#templates) below for information about declaring what template to use in the above scenarios. @@ -182,9 +177,8 @@ The template to use for a given action is determined in the following order: 1. If a template exists with the name of this class or any of its ancestors (with no suffix), it will be used. - e.g. for the `App\Control\TeamController` example, it would look for `templates/App/Control/TeamController.ss` and `templates/SilverStripe/Control/Controller.ss`. -[note] -Subclasses of `ContentController` additionally check for templates named similarly to the model the controller represents - for example a `HomePageController` class which represents a `HomePage` model will look for `HomePage_{action}.ss` after checking `HomePageController_{action}.ss`. -[/note] +> [!NOTE] +> Subclasses of `ContentController` additionally check for templates named similarly to the model the controller represents - for example a `HomePageController` class which represents a `HomePage` model will look for `HomePage_{action}.ss` after checking `HomePageController_{action}.ss`. You can declare templates to be used for an action by setting the `templates` array. The key should be the name of the action, and the value should be a template name, or array of template names in cascading precedence. @@ -206,27 +200,25 @@ class TeamController extends Controller } ``` -[warning] -The `templates` property is *not* a configuration property, so if you declare it directly as in the above example you will -override any templates declared in parent classes. If you want to keep template declarations from parent classes, you could -apply new templates in a constructor like so: - -```php -namespace App\Control; - -class TeamController extends SomeParentController -{ - // ... - - public function __construct() - { - parent::__construct(); - $this->templates['showPlayers'] => 'TemplateForPlayers'; - } -} -``` - -[/warning] +> [!WARNING] +> The `templates` property is *not* a configuration property, so if you declare it directly as in the above example you will +> override any templates declared in parent classes. If you want to keep template declarations from parent classes, you could +> apply new templates in a constructor like so: +> +> ```php +> namespace App\Control; +> +> class TeamController extends SomeParentController +> { +> // ... +> +> public function __construct() +> { +> parent::__construct(); +> $this->templates['showPlayers'] => 'TemplateForPlayers'; +> } +> } +> ``` As mentioned in [Actions](#actions) above, controller actions can return a string or `HTTPResponse` to bypass this template selection process. @@ -258,10 +250,9 @@ $playersActionLink = $teamController::Link('players'); You can of course also use `$Link` in a template. -[notice] -If you have more complex logic for determining the link for your controller, you can override the `Link()` method - in that case you should -be sure to invoke the `updateLink` extension method so that extensions can make changes as necessary: `$this->extend('updateLink', $link, $action);` -[/notice] +> [!WARNING] +> If you have more complex logic for determining the link for your controller, you can override the `Link()` method - in that case you should +> be sure to invoke the `updateLink` extension method so that extensions can make changes as necessary: `$this->extend('updateLink', $link, $action);` ## Connecting pages to controllers diff --git a/en/02_Developer_Guides/02_Controllers/02_Routing.md b/en/02_Developer_Guides/02_Controllers/02_Routing.md index 09ce6b251..f41cfbb44 100644 --- a/en/02_Developer_Guides/02_Controllers/02_Routing.md +++ b/en/02_Developer_Guides/02_Controllers/02_Routing.md @@ -5,16 +5,14 @@ summary: A more in depth look at how to map requests to particular controllers a # Routing -[info] -If you're extending [`ContentController`](api:SilverStripe\CMS\Controllers\ContentController) or `PageController` for your [`SiteTree`](api:SilverStripe\CMS\Model\SiteTree) records you don't need to define the routing rules as the `cms` handles routing for those. You may still need to define [url_handlers](#url-handlers) in some cases though. -[/info] +> [!NOTE] +> If you're extending [`ContentController`](api:SilverStripe\CMS\Controllers\ContentController) or `PageController` for your [`SiteTree`](api:SilverStripe\CMS\Model\SiteTree) records you don't need to define the routing rules as the `cms` handles routing for those. You may still need to define [url_handlers](#url-handlers) in some cases though. Routing is the process of mapping URL's to [Controller](api:SilverStripe\Control\Controller) and actions. -[hint] -Getting routing rules right can be tricky. Add `?debug_request` to the end of your URL in your browser (while in dev mode) to see debug information about how your controller is matching actions against your URL pattern. -See [URL Variable Tools](/developer_guides/debugging/url_variable_tools) for more useful URL variables for debugging. -[/hint] +> [!TIP] +> Getting routing rules right can be tricky. Add `?debug_request` to the end of your URL in your browser (while in dev mode) to see debug information about how your controller is matching actions against your URL pattern. +> See [URL Variable Tools](/developer_guides/debugging/url_variable_tools) for more useful URL variables for debugging. Routes are defined by setting the `rules` configuration array on [`Director`](api:SilverStripe\Control\Director). Typically you will add this configuration in a `routes.yml` file in your application or module's `_config` folder alongside your other configuration files. @@ -33,9 +31,8 @@ SilverStripe\Control\Director: '': 'App\Control\HomeController' ``` -[hint] -The `//` before `$Action` in the above routing pattern is important! Without this, the appropriate action will not be matched. See [URL patterns](#url-patterns) below for more information about this. -[/hint] +> [!TIP] +> The `//` before `$Action` in the above routing pattern is important! Without this, the appropriate action will not be matched. See [URL patterns](#url-patterns) below for more information about this. The above declarations will instantiate a new controller with the given class name. If your controller needs some additional setup (e.g. it has constructor parameters or needs some method to be called before handling certain requests) you can set up a service with the injector and tell the `Director` to use that specific service. @@ -47,9 +44,8 @@ SilverStripe\Control\Director: See [Dependency Injection](/developer_guides/extending/injector) for more information about the injector configuration syntax and how to define services. -[hint] -You can also define redirections in your routing rules! See [Redirection](redirection#redirections-in-routing-rules) for more information. -[/hint] +> [!TIP] +> You can also define redirections in your routing rules! See [Redirection](redirection#redirections-in-routing-rules) for more information. Read the [Configuration](../configuration) documentation for more information about the configuration API and syntax in general. @@ -70,6 +66,22 @@ SilverStripe\Control\Director: ## Parameters +> [!CAUTION] +> Be aware that if your action doesn't follow the default URL handler pattern `$Action//$ID/$OtherID`, you *must* declare the appropriate url_handler pattern for your action. +> This is because the `Director.rules` configuration is *only* used to indentify which *controller* should handle the request, and how to handle parameters. It does *not* +> provide enough information on its own for the controller to know which *action* should be used. +> +> For example, the following two routing rules *must* have an appropriate `url_handlers` declaration: +> +> - `teams//$Action/$ID/$AnotherID/$Name` - the `$Action/$ID/$AnotherID/$Name` portion needs to be declared in `url_handlers` +> - `teams//$@` - the `$@` portion needs to be declared in `url_handlers` +> +> In both cases, having any more than 3 path segments after `teams/` in the URL will result in the error "I can't handle sub-URLs on class `App\Control\TeamController`". This happens because there are more path segments than the default URL handler pattern knows how to deal with. +> +> Note also that in both cases the first path segment after `teams/` will try to match against an action on the controller. You can also use `url_handlers` to declare a specific action that should handle these patterns regardless of what the parameter values resolve to. +> +> See [URL Handlers](#url-handlers) below for more information about the `url_handlers` configuration array. + ```yml SilverStripe\Control\Director: rules: @@ -82,33 +94,14 @@ It also contains 3 `parameters` (or `params` for short). `$Action`, `$ID` and `$ which will be filled when the user makes their request. Request parameters are available on the `HTTPRequest` object and can be pulled out from a controller using `$this->getRequest()->param($name)`. -[hint] -The base `Controller` class already defines `$Action//$ID/$OtherID` in the `url_handlers` configuration array - so you can omit that part of the routing rule if you want, simplifying the above rule to: - -```yml -SilverStripe\Control\Director: - rules: - 'teams': 'App\Control\TeamController' -``` - -[/hint] - -[alert] -Be aware that if your action doesn't follow the default URL handler pattern `$Action//$ID/$OtherID`, you *must* declare the appropriate url_handler pattern for your action. -This is because the `Director.rules` configuration is *only* used to indentify which *controller* should handle the request, and how to handle parameters. It does *not* -provide enough information on its own for the controller to know which *action* should be used. - -For example, the following two routing rules *must* have an appropriate `url_handlers` declaration: - -- `teams//$Action/$ID/$AnotherID/$Name` - the `$Action/$ID/$AnotherID/$Name` portion needs to be declared in `url_handlers` -- `teams//$@` - the `$@` portion needs to be declared in `url_handlers` - -In both cases, having any more than 3 path segments after `teams/` in the URL will result in the error "I can't handle sub-URLs on class `App\Control\TeamController`". This happens because there are more path segments than the default URL handler pattern knows how to deal with. - -Note also that in both cases the first path segment after `teams/` will try to match against an action on the controller. You can also use `url_handlers` to declare a specific action that should handle these patterns regardless of what the parameter values resolve to. - -See [URL Handlers](#url-handlers) below for more information about the `url_handlers` configuration array. -[/alert] +> [!TIP] +> The base `Controller` class already defines `$Action//$ID/$OtherID` in the `url_handlers` configuration array - so you can omit that part of the routing rule if you want, simplifying the above rule to: +> +> ```yml +> SilverStripe\Control\Director: +> rules: +> 'teams': 'App\Control\TeamController' +> ``` Here is what those parameters would look like for certain requests @@ -154,19 +147,17 @@ $params = [ $id = $this->getRequest()->param('ID'); ``` -[info] -All Controllers have access to `$this->getRequest()` for the request object and `$this->getResponse()` for the response. -Controller actions also accept the current `HTTPRequest` as their first argument. -[/info] +> [!NOTE] +> All Controllers have access to `$this->getRequest()` for the request object and `$this->getResponse()` for the response. +> Controller actions also accept the current `HTTPRequest` as their first argument. ## URL patterns The [`RequestHandler`](api:SilverStripe\Control\RequestHandler) (of which `Controller` is a subclass) will parse all rules you specify against the following patterns. The most specific rule will be the one followed for the response. -[alert] -A rule must always start with alphabetical (`[A-Za-z]`) characters or a $Variable declaration -[/alert] +> [!CAUTION] +> A rule must always start with alphabetical (`[A-Za-z]`) characters or a $Variable declaration | Pattern | Description | | ----------- | --------------- | @@ -174,11 +165,10 @@ A rule must always start with alphabetical (`[A-Za-z]`) characters or a $Variabl | `!` | **Require Variable** - Placing this after a parameter variable requires data to be present for the rule to match | | `//` | **Shift Point** - Declares that variables denoted with a $ are only parsed into the $params AFTER this point in the regex | -[notice] -The shift point is an important part of the routing pattern and should immediately follow the hard-coded portion of the URL segment. -This ensures that the request handler knows to only pass through items *after* that point as variable parameters for the controller to check against its `url_handler` -patterns. -[/notice] +> [!WARNING] +> The shift point is an important part of the routing pattern and should immediately follow the hard-coded portion of the URL segment. +> This ensures that the request handler knows to only pass through items *after* that point as variable parameters for the controller to check against its `url_handler` +> patterns. The following is a very common URL handler syntax. For any URL that contains 'teams' this rule will match and hand over execution to the matching controller. The `TeamsController` is passed an optional action, id, and other id parameters to do any more @@ -292,11 +282,10 @@ In previous examples the URLs were configured using the [`Director`](api:SilverS Alternatively you can use this to provide just enough information for the `Director` to select your controller to handle the request, and specify the rest of the routing rules for your actions directly in your Controller class. -[alert] -Don't forget to set your actions in the `allowed_actions` configuration array, or you won't be able to access them via HTTP requests. - -See the [Access Control](access_control) documentation for more information. -[/alert] +> [!CAUTION] +> Don't forget to set your actions in the `allowed_actions` configuration array, or you won't be able to access them via HTTP requests. +> +> See the [Access Control](access_control) documentation for more information. In this case, the routing rule only needs to provide enough information for the framework to choose the desired controller. diff --git a/en/02_Developer_Guides/02_Controllers/03_Access_Control.md b/en/02_Developer_Guides/02_Controllers/03_Access_Control.md index 5d2c363f8..ef87fddaf 100644 --- a/en/02_Developer_Guides/02_Controllers/03_Access_Control.md +++ b/en/02_Developer_Guides/02_Controllers/03_Access_Control.md @@ -14,6 +14,9 @@ actions on the website they shouldn't be able to. Any action you define on a controller must be defined in a `$allowed_actions` configuration array. This prevents users from directly calling methods that they shouldn't. +> [!NOTE] +> If the permission check fails, Silverstripe CMS will return a `403` Forbidden HTTP status. + ```php namespace App\Control; @@ -45,33 +48,27 @@ class MyController extends Controller } ``` -[notice] -If you want to add access checks in a subclass for an action which is declared in a parent class, and the parent class *doesn't* declare an access check for that action, your subclass will have to redeclare the action method. - -The declaration of the method can be as simple as: - -```php -namespace App\Control; - -use SilverStripe\Control\Controller; -use SilverStripe\Control\HTTPRequest; - -class MyController extends Controller -{ - // ... - - public function someAction(HTTPRequest $request) - { - return parent::someAction($request); - } -} -``` - -[/notice] - -[info] -If the permission check fails, Silverstripe CMS will return a `403` Forbidden HTTP status. -[/info] +> [!WARNING] +> If you want to add access checks in a subclass for an action which is declared in a parent class, and the parent class *doesn't* declare an access check for that action, your subclass will have to redeclare the action method. +> +> The declaration of the method can be as simple as: +> +> ```php +> namespace App\Control; +> +> use SilverStripe\Control\Controller; +> use SilverStripe\Control\HTTPRequest; +> +> class MyController extends Controller +> { +> // ... +> +> public function someAction(HTTPRequest $request) +> { +> return parent::someAction($request); +> } +> } +> ``` An action named "index" is allowed by default, unless `allowed_actions` is defined as an empty array, or the action is specifically restricted. @@ -254,10 +251,9 @@ class MyController extends Controller } ``` -[notice] -This is recommended as an addition to access checks defined in `$allowed_actions`, in order to handle more complex checks, rather than a -replacement. -[/notice] +> [!WARNING] +> This is recommended as an addition to access checks defined in `$allowed_actions`, in order to handle more complex checks, rather than a +> replacement. ## Controller level checks @@ -265,9 +261,8 @@ After checking for allowed_actions, each controller invokes its [`init()`](api:S common state. If an `init()` method returns a `HTTPResponse` with either a 3xx or 4xx HTTP status code, it'll abort execution. This behavior can be used to implement permission checks. -[info] -`init()` is called regardless of the action that will ultimately handle the request, and executes before the action does. -[/info] +> [!NOTE] +> `init()` is called regardless of the action that will ultimately handle the request, and executes before the action does. ```php namespace App\Control; diff --git a/en/02_Developer_Guides/02_Controllers/05_Middlewares.md b/en/02_Developer_Guides/02_Controllers/05_Middlewares.md index 5389b4210..6e9c98fbf 100644 --- a/en/02_Developer_Guides/02_Controllers/05_Middlewares.md +++ b/en/02_Developer_Guides/02_Controllers/05_Middlewares.md @@ -147,11 +147,10 @@ $app->addMiddleware(new MyApplicationMiddleware()); // ... ``` -[info] -It's pretty rare to need to modify the `index.php` file directly like this - if you need to do it, make sure you clearly document this change -somewhere, e.g. in your project's README.md file if you have one, both for your own reference and for any other developers working on the project -with you. -[/info] +> [!NOTE] +> It's pretty rare to need to modify the `index.php` file directly like this - if you need to do it, make sure you clearly document this change +> somewhere, e.g. in your project's README.md file if you have one, both for your own reference and for any other developers working on the project +> with you. Beware that by this point, the Silverstripe framework features you normally rely on (e.g.: ORM, Injector, services configured by Injector, the config API) won't be available in your middleware or in `index.php` because they won't have been initialised yet. diff --git a/en/02_Developer_Guides/02_Controllers/07_CMS_JSON_APIs.md b/en/02_Developer_Guides/02_Controllers/07_CMS_JSON_APIs.md index f5ebeec89..db91cbff2 100644 --- a/en/02_Developer_Guides/02_Controllers/07_CMS_JSON_APIs.md +++ b/en/02_Developer_Guides/02_Controllers/07_CMS_JSON_APIs.md @@ -23,9 +23,8 @@ Because of this you should generally avoid updating large parts of a DataObject Create a subclass of [`LeftAndMain`](api:SilverStripe\Admin\LeftAndMain). This ensures that users must be logged in to the admin interface to access the endpoint. Additionally, it provides access to the methods [`LeftAndMain::jsonSuccess()`](api:SilverStripe\Admin\LeftAndMain::jsonSuccess()) and [`LeftAndMain::jsonError()`](api:SilverStripe\Admin\LeftAndMain::jsonError()). -[warning] -To enhance security, do not create a direct subclass of [`Controller`](api:SilverStripe\Control\Controller) routed using YAML on the `/admin` route. This practice is strongly discouraged as it circumvents the requirement to log in to the CMS to access the endpoints. At best you'd be re-implementing logic that already exists. -[/warning] +> [!WARNING] +> To enhance security, do not create a direct subclass of [`Controller`](api:SilverStripe\Control\Controller) routed using YAML on the `/admin` route. This practice is strongly discouraged as it circumvents the requirement to log in to the CMS to access the endpoints. At best you'd be re-implementing logic that already exists. When naming this class, it's best practice to add a "Controller" suffix to this class, for instance name it `MySomethingController`. @@ -114,9 +113,8 @@ Generally you should not include a message outlining the nature of the error whe If you do include a message, remember that error messages are only intended for developers so do not use the `_t()` function to make them translatable. Do not use any returned messages on the frontend for things like toast notifications, instead those messages should be added directly in JavaScript. -[info] -Despite the slightly convoluted JSON format returned by `jsonError()` with multiple nodes, its usage remains consistent with `FormSchema`. It's better to use this method for uniformity rather than introducing separate methods for `FormSchema` and non-FormSchema failures. -[/info] +> [!NOTE] +> Despite the slightly convoluted JSON format returned by `jsonError()` with multiple nodes, its usage remains consistent with `FormSchema`. It's better to use this method for uniformity rather than introducing separate methods for `FormSchema` and non-FormSchema failures. ## CSRF token @@ -130,9 +128,8 @@ import Config from 'lib/Config'; const securityID = Config.get('SecurityID'); ``` -[warning] -The `lib/Config` import is provided by the `silverstripe/admin` module via [`@silverstripe/webpack-config`](https://www.npmjs.com/package/@silverstripe/webpack-config). -[/warning] +> [!WARNING] +> The `lib/Config` import is provided by the `silverstripe/admin` module via [`@silverstripe/webpack-config`](https://www.npmjs.com/package/@silverstripe/webpack-config). Ensure the security of your endpoints by validating the security token on relevant endpoints. @@ -144,9 +141,8 @@ if (!SecurityToken::inst()->checkRequest($this->getRequest())) { } ``` -[info] -The `400` HTTP status code used here is consistent with the code used when the CSRF check fails when submitting data using `FormSchema`. -[/info] +> [!NOTE] +> The `400` HTTP status code used here is consistent with the code used when the CSRF check fails when submitting data using `FormSchema`. ## Passing values from PHP to global JavaScript @@ -211,9 +207,8 @@ backend.post(endpoint, data, headers) }); ``` -[warning] -The `lib/Config` and `lib/Backend` imports are provided by the `silverstripe/admin` module via [`@silverstripe/webpack-config`](https://www.npmjs.com/package/@silverstripe/webpack-config). -[/warning] +> [!WARNING] +> The `lib/Config` and `lib/Backend` imports are provided by the `silverstripe/admin` module via [`@silverstripe/webpack-config`](https://www.npmjs.com/package/@silverstripe/webpack-config). On the controller's endpoint method, retrieve the POST data using `$json = json_decode($this->getRequest()->getBody());`. diff --git a/en/02_Developer_Guides/03_Forms/00_Introduction.md b/en/02_Developer_Guides/03_Forms/00_Introduction.md index cc3b90e7e..36d11abae 100644 --- a/en/02_Developer_Guides/03_Forms/00_Introduction.md +++ b/en/02_Developer_Guides/03_Forms/00_Introduction.md @@ -84,13 +84,12 @@ class MyFormPageController extends PageController $HelloForm ``` -[info] -The examples above use `FormField::create()` instead of the `new` operator (`new FormField()`). This is best practice, -as it allows you to use [dependency injection](/developer_guides/extending/injector/) to replace the actual class being used at runtime. - -As an extra incentive, it also allows you to chain operations like `setTitle()` without assigning the field instance to a temporary -variable. -[/info] +> [!NOTE] +> The examples above use `FormField::create()` instead of the `new` operator (`new FormField()`). This is best practice, +> as it allows you to use [dependency injection](/developer_guides/extending/injector/) to replace the actual class being used at runtime. +> +> As an extra incentive, it also allows you to chain operations like `setTitle()` without assigning the field instance to a temporary +> variable. When constructing the `Form` instance (`Form::create($controller, $name)`) both controller and name are required. The `$controller` and `$name` are used to allow Silverstripe CMS to calculate the origin of the `Form object`. When a user @@ -125,10 +124,9 @@ class MyFormPageController extends PageController See [URL Handlers](/developer_guides/controllers/routing/#url-handlers) for more information about handling controller actions. -[notice] -Form actions (`doSayHello()`), on the other hand, should *not* be included in `$allowed_actions`; these are handled -separately through [Form::httpSubmission()](api:SilverStripe\Forms\Form::httpSubmission()). -[/notice] +> [!WARNING] +> Form actions (`doSayHello()`), on the other hand, should *not* be included in `$allowed_actions`; these are handled +> separately through [Form::httpSubmission()](api:SilverStripe\Forms\Form::httpSubmission()). ## Adding formFields @@ -140,9 +138,8 @@ use SilverStripe\Forms\TextField; TextField::create($name, $title, $value); ``` -[info] -A list of the common FormField subclasses is available on the [Common Subclasses](field_types/common_subclasses/) page. -[/info] +> [!NOTE] +> A list of the common FormField subclasses is available on the [Common Subclasses](field_types/common_subclasses/) page. The fields are added to the [FieldList](api:SilverStripe\Forms\FieldList) `fields` property on the `Form` and can be modified at up to the point the `Form` is rendered. @@ -202,20 +199,18 @@ Fields can be removed from the form. $form->getFields()->removeByName('Email'); ``` -[alert] -Forms can be tabbed (such as the CMS interface). In these cases, there are additional functions such as `addFieldToTab` -and `removeFieldByTab` to ensure the fields are on the correct interface. See [Tabbed Forms](tabbed_forms) for more -information on the CMS interface. -[/alert] +> [!CAUTION] +> Forms can be tabbed (such as the CMS interface). In these cases, there are additional functions such as `addFieldToTab` +> and `removeFieldByTab` to ensure the fields are on the correct interface. See [Tabbed Forms](tabbed_forms) for more +> information on the CMS interface. ## Modifying formFields Each [FormField](api:SilverStripe\Forms\FormField) subclass has a number of methods you can call on it to customise its' behavior or HTML markup. The default `FormField` object has several methods for doing common operations. -[notice] -Most of the `set` operations will return the object back so methods can be chained. -[/notice] +> [!WARNING] +> Most of the `set` operations will return the object back so methods can be chained. ```php use SilverStripe\Forms\TextField; @@ -341,10 +336,9 @@ with the particular button. In the previous example, clicking the 'Another Butto - The `Form` instance. - The `Controller` instance. -[notice] -If the `$action` method cannot be found on any of those or is marked as `private` or `protected`, an error will be -thrown. -[/notice] +> [!WARNING] +> If the `$action` method cannot be found on any of those or is marked as `private` or `protected`, an error will be +> thrown. The `$action` method takes two arguments: diff --git a/en/02_Developer_Guides/03_Forms/01_Validation.md b/en/02_Developer_Guides/03_Forms/01_Validation.md index 531de11fc..ce300d294 100644 --- a/en/02_Developer_Guides/03_Forms/01_Validation.md +++ b/en/02_Developer_Guides/03_Forms/01_Validation.md @@ -65,11 +65,10 @@ class MyFormPageController extends PageController In this example we will be required to input a value for `Name` and a valid email address for `Email` before the `doSubmitForm` method is called. -[info] -Each individual [FormField](api:SilverStripe\Forms\FormField) instance is responsible for validating the submitted content through the -[FormField::validate()](api:SilverStripe\Forms\FormField::validate()) method. By default, this just checks the value exists. Fields like `EmailField` override -`validate` to check for a specific format. -[/info] +> [!NOTE] +> Each individual [FormField](api:SilverStripe\Forms\FormField) instance is responsible for validating the submitted content through the +> [FormField::validate()](api:SilverStripe\Forms\FormField::validate()) method. By default, this just checks the value exists. Fields like `EmailField` override +> `validate` to check for a specific format. ## Extensions @@ -94,11 +93,10 @@ class FormFieldValidationExtension extends Extension } ``` -[notice] -This extension hook will not work without the ampersand (`&`) in the `&$result` argument. This is because the return -value of the function is ignored, so the validation result has to be updated by changing the value of the `$result` -variable. This is known as [passing by reference](https://www.php.net/manual/en/language.references.pass.php). -[/notice] +> [!WARNING] +> This extension hook will not work without the ampersand (`&`) in the `&$result` argument. This is because the return +> value of the function is ignored, so the validation result has to be updated by changing the value of the `$result` +> variable. This is known as [passing by reference](https://www.php.net/manual/en/language.references.pass.php). ## Validation in `FormField` subclasses @@ -132,9 +130,8 @@ should trigger a validation error on the page) and pass this to the `extendValid to hook into the validation logic. In addition, in the event of failed validation, a useful error message must be set on the given validator. -[notice] -You can also override the entire `Form` validation by subclassing `Form` and defining a `validate` method on the form. -[/notice] +> [!WARNING] +> You can also override the entire `Form` validation by subclassing `Form` and defining a `validate` method on the form. ## Form action validation @@ -351,9 +348,8 @@ call `setValidator` easily. However, a `DataObject` can provide its own `Validat [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) and [GridField](api:SilverStripe\Forms\GridField\GridField) will respect the provided `Validator`/s and handle displaying error and success responses to the user. -[info] -Again, custom error messages can be provided through the `FormField` -[/info] +> [!NOTE] +> Again, custom error messages can be provided through the `FormField` ```php namespace App\PageType; @@ -390,10 +386,9 @@ class MyPage extends Page } ``` -[hint] -You can also update the `CompositeValidator` by creating an `Extension` and implementing the -`updateCMSCompositeValidator()` method. -[/hint] +> [!TIP] +> You can also update the `CompositeValidator` by creating an `Extension` and implementing the +> `updateCMSCompositeValidator()` method. ## Related lessons diff --git a/en/02_Developer_Guides/03_Forms/03_Form_Templates.md b/en/02_Developer_Guides/03_Forms/03_Form_Templates.md index eb0c4d0ab..05feb4677 100644 --- a/en/02_Developer_Guides/03_Forms/03_Form_Templates.md +++ b/en/02_Developer_Guides/03_Forms/03_Form_Templates.md @@ -20,22 +20,20 @@ $field->setTemplate('MyCustomTextField'); To override the template for CMS forms, the custom templates should be located in `app/templates/`. Front-end form templates can be located in `app/templates/` or in the active theme's `templates/` directory. -[notice] -It's recommended to copy the contents of the template you're going to replace and use that as a start. For instance, if -you want to create a `MyCustomFormTemplate` copy the contents of `Form.ss` to a `MyCustomFormTemplate.ss` file and -modify as you need. - -*The default Form.ss can be found at `/vendor/silverstripe/framework/templates/SilverStripe/Forms/Includes/Form.ss`* -[/notice] +> [!WARNING] +> It's recommended to copy the contents of the template you're going to replace and use that as a start. For instance, if +> you want to create a `MyCustomFormTemplate` copy the contents of `Form.ss` to a `MyCustomFormTemplate.ss` file and +> modify as you need. +> +> *The default Form.ss can be found at `/vendor/silverstripe/framework/templates/SilverStripe/Forms/Includes/Form.ss`* By default, `Form` and the various `FormField` subclasses follow the Silverstripe CMS Template convention and are rendered into templates of the same class name (i.e.`EmailField` will attempt to render into `EmailField.ss` and if that isn't found, `TextField.ss` or finally `FormField.ss`). -[alert] -While you can override all templates using normal view inheritance (i.e.defining a `Form.ss`) other modules may rely on -the core template structure. It is recommended to use `setTemplate` and unique templates for specific forms. -[/alert] +> [!CAUTION] +> While you can override all templates using normal view inheritance (i.e.defining a `Form.ss`) other modules may rely on +> the core template structure. It is recommended to use `setTemplate` and unique templates for specific forms. For [`FormField`](api:SilverStripe\Forms\FormField) instances, there are several other templates that are used on top of the main `setTemplate()`. diff --git a/en/02_Developer_Guides/03_Forms/04_Form_Security.md b/en/02_Developer_Guides/03_Forms/04_Form_Security.md index 06216f9e5..e5e548766 100644 --- a/en/02_Developer_Guides/03_Forms/04_Form_Security.md +++ b/en/02_Developer_Guides/03_Forms/04_Form_Security.md @@ -16,10 +16,9 @@ Silverstripe CMS protects users against [Cross-Site Request Forgery](https://owa random string generated by [SecurityToken](api:SilverStripe\Security\SecurityToken) to identify the particular user request vs a third-party forging fake requests. -[info] -For more information on Cross-Site Request Forgery, consult the [OWASP](https://owasp.org/www-community/attacks/csrf) -website. -[/info] +> [!NOTE] +> For more information on Cross-Site Request Forgery, consult the [OWASP](https://owasp.org/www-community/attacks/csrf) +> website. The `SecurityToken` automatically added looks something like: @@ -48,10 +47,9 @@ $form = Form::create(/* ... */); $form->disableSecurityToken(); ``` -[alert] -Do not disable the `SecurityID` for forms that perform some modification to the user's session. This will open your -application up to `CSRF` security holes. -[/alert] +> [!CAUTION] +> Do not disable the `SecurityID` for forms that perform some modification to the user's session. This will open your +> application up to `CSRF` security holes. ## Strict form submission diff --git a/en/02_Developer_Guides/03_Forms/06_Tabbed_Forms.md b/en/02_Developer_Guides/03_Forms/06_Tabbed_Forms.md index de9780f34..a5dc3147d 100644 --- a/en/02_Developer_Guides/03_Forms/06_Tabbed_Forms.md +++ b/en/02_Developer_Guides/03_Forms/06_Tabbed_Forms.md @@ -9,17 +9,15 @@ Silverstripe CMS's [FormScaffolder](api:SilverStripe\Forms\FormScaffolder) can a CMS and other scaffolded interfaces, it will output [TabSet](api:SilverStripe\Forms\TabSet) and [Tab](api:SilverStripe\Forms\Tab) objects and use jQuery Tabs to split parts of the data model. -[info] -All interfaces within the CMS such as [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) and [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) use tabbed interfaces by default. -[/info] +> [!NOTE] +> All interfaces within the CMS such as [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) and [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) use tabbed interfaces by default. When dealing with tabbed forms, modifying the fields in the form has a few differences. Each [Tab](api:SilverStripe\Forms\Tab) will be given a name, and normally they all exist under the `Root` [TabSet](api:SilverStripe\Forms\TabSet). -[notice] -[TabSet](api:SilverStripe\Forms\TabSet) instances can contain child [Tab](api:SilverStripe\Forms\Tab) and further [TabSet](api:SilverStripe\Forms\TabSet) instances, however the CMS UI will only -display up to two levels of tabs in the interface. -[/notice] +> [!WARNING] +> [TabSet](api:SilverStripe\Forms\TabSet) instances can contain child [Tab](api:SilverStripe\Forms\Tab) and further [TabSet](api:SilverStripe\Forms\TabSet) instances, however the CMS UI will only +> display up to two levels of tabs in the interface. ## Adding a field to a tab diff --git a/en/02_Developer_Guides/03_Forms/07_Using_GridField_With_Arbitrary_Data.md b/en/02_Developer_Guides/03_Forms/07_Using_GridField_With_Arbitrary_Data.md index 44bae2569..2279167e4 100644 --- a/en/02_Developer_Guides/03_Forms/07_Using_GridField_With_Arbitrary_Data.md +++ b/en/02_Developer_Guides/03_Forms/07_Using_GridField_With_Arbitrary_Data.md @@ -8,9 +8,8 @@ icon: table [`GridField`](api:SilverStripe\Forms\GridField\GridField) is often used for displaying and editing `DataObject` records - but it can be used with other data as well. You might have data that is pulled from an API for example, which you want to display in the admin area of your Silverstripe CMS project. -[info] -This document assumes you're familiar with `GridField` - see the [`GridField` documentation](/developer_guides/forms/field_types/gridfield/) for information about using `GridField`. -[/info] +> [!NOTE] +> This document assumes you're familiar with `GridField` - see the [`GridField` documentation](/developer_guides/forms/field_types/gridfield/) for information about using `GridField`. Data which isn't represented by `DataObject` records can come in two forms: @@ -25,11 +24,10 @@ Some grid field components may require specific information, such as which colum Regardless of how you get your data, whether it's from a web API or some other source, you'll need to store it in an `ArrayList`. For best results, each record should also be explicitly instantiated as an `ArrayData` object in the list. -[hint] -The `ID` field shown here isn't necessary if you only want to view the records as rows in the `GridField`, but if you want to be able to view *each* record in a read-only form view, the `ID` field is mandatory. - -See [viewing data in a form](#arraydata-view) for more information. -[/hint] +> [!TIP] +> The `ID` field shown here isn't necessary if you only want to view the records as rows in the `GridField`, but if you want to be able to view *each* record in a read-only form view, the `ID` field is mandatory. +> +> See [viewing data in a form](#arraydata-view) for more information. ```php use SilverStripe\ORM\ArrayList; @@ -116,9 +114,8 @@ For data to be viewed in a read-only form, each record in the list must have an You'll need to add a `GridFieldDetailForm` component to the `GridField` and tell it how to represent your data by passing a [`FieldList`](api:SilverStripe\Forms\FieldList) into [`GridFieldDetailForm::setFields()`](api:SilverStripe\Forms\GridField\GridFieldDetailForm::setFields()). -[hint] -Because `ArrayData` doesn't implement a `canEdit()` method, the form will be implicitly turned into a read-only form for you. You don't need to worry about passing in read-only form fields. -[/hint] +> [!TIP] +> Because `ArrayData` doesn't implement a `canEdit()` method, the form will be implicitly turned into a read-only form for you. You don't need to worry about passing in read-only form fields. ```php use SilverStripe\Forms\FieldList; @@ -151,11 +148,10 @@ However, if you omit these method implementations, you must instead pass the req To represent your data as rows in a `GridField`, you can rely on the default `GridFieldConfig` object that the field will build for itself. If you implement a `summaryFields()` method in your data class, the `GridField` will call that method to find out what fields it should display. -[hint] -The `ID` field shown here isn't necessary if you only want to view/edit the records as rows in the `GridField`, but if you want to be able to view *each* record in a read-only form view, the `ID` field is mandatory. - -See [viewing data in a form](#custom-view) for more information. -[/hint] +> [!TIP] +> The `ID` field shown here isn't necessary if you only want to view/edit the records as rows in the `GridField`, but if you want to be able to view *each* record in a read-only form view, the `ID` field is mandatory. +> +> See [viewing data in a form](#custom-view) for more information. ```php namespace App\Data; @@ -221,9 +217,8 @@ If you want to be able to filter your `GridField`, you will need to tell the `Gr What's more, we don't have to pass the search fields into the `BasicSearchContext` instance either if we implement a `scaffoldSearchFields()` method. -[hint] -You can optionally implement the `i18n_singular_name()` method to return a localised string to represent the plural name of this model. This is used in the filter header as the placeholder text for the general search field. -[/hint] +> [!TIP] +> You can optionally implement the `i18n_singular_name()` method to return a localised string to represent the plural name of this model. This is used in the filter header as the placeholder text for the general search field. ```php namespace App\Data; @@ -257,9 +252,8 @@ class DataRepresentation extends ViewableData No changes are required to the `GridField` components, assuming you didn't remove the `GridFieldFilterHeader` component. -[hint] -The `BasicSearchContext` respects some (*but not all*) [`$searchable_fields` configuration options](/developer_guides/model/scaffolding/#searchable-fields), so you can implement a `searchableFields()` method in your class to further customise the `GridField` filtering experience. -[/hint] +> [!TIP] +> The `BasicSearchContext` respects some (*but not all*) [`$searchable_fields` configuration options](/developer_guides/model/scaffolding/#searchable-fields), so you can implement a `searchableFields()` method in your class to further customise the `GridField` filtering experience. ### Exporting data {#custom-export} @@ -282,9 +276,8 @@ If the class representing your data has a `getCMSFields()` method, the return va If your class doesn't implement a `canEdit()` method, or it does and the method returns `false`, the form will be read-only. -[hint] -You can optionally implement the `i18n_plural_name()` method to return a localised string to represent the singular name of this model. This is used in the add button, breadcrumbs, and toasts. -[/hint] +> [!TIP] +> You can optionally implement the `i18n_plural_name()` method to return a localised string to represent the singular name of this model. This is used in the add button, breadcrumbs, and toasts. ```php namespace App\Data; @@ -332,9 +325,8 @@ For new records, the `write()` method *must* set the `ID` field on the record, s Records with no `ID` field or which have a non-numeric value for their `ID` field are considered new (unsaved) records. -[hint] -If you have specific validation rules you want to apply, you can also implement a `getCMSCompositeValidator()` method as described in [validation in the CMS](/developer_guides/forms/validation/#validation-in-the-cms). -[/hint] +> [!TIP] +> If you have specific validation rules you want to apply, you can also implement a `getCMSCompositeValidator()` method as described in [validation in the CMS](/developer_guides/forms/validation/#validation-in-the-cms). ```php namespace App\Data; @@ -421,13 +413,11 @@ $gridField->getConfig()->addComponent(GridFieldEditButton::create()); You can also enable creating new records and deleting records by adding the [`GridFieldAddNewButton`](api:SilverStripe\Forms\GridField\GridFieldAddNewButton) and [`GridFieldDeleteAction`](api:SilverStripe\Forms\GridField\GridFieldDeleteAction) components to your `GridField` config. -[hint] -At this point your `GridField` config is essentially the same as a [`GridFieldConfig_RecordEditor`](api:SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor) - so you could set up your `GridField` like so: - -```php -use SilverStripe\Forms\GridField\GridField; -use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor; -$gridField = GridField::create('MyData', 'My data', $list, GridFieldConfig_RecordEditor::create()); -``` - -[/hint] +> [!TIP] +> At this point your `GridField` config is essentially the same as a [`GridFieldConfig_RecordEditor`](api:SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor) - so you could set up your `GridField` like so: +> +> ```php +> use SilverStripe\Forms\GridField\GridField; +> use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor; +> $gridField = GridField::create('MyData', 'My data', $list, GridFieldConfig_RecordEditor::create()); +> ``` diff --git a/en/02_Developer_Guides/03_Forms/Field_types/02_DateField.md b/en/02_Developer_Guides/03_Forms/Field_types/02_DateField.md index 4b8534e83..7740d397c 100644 --- a/en/02_Developer_Guides/03_Forms/Field_types/02_DateField.md +++ b/en/02_Developer_Guides/03_Forms/Field_types/02_DateField.md @@ -55,9 +55,8 @@ DateField::create('MyDate') ->setDateFormat('dd/MM/yyyy'); ``` -[info] -The formats are based on [ICU format](https://unicode-org.github.io/icu/userguide/format_parse/datetime/#simpledateformat). -[/info] +> [!NOTE] +> The formats are based on [ICU format](https://unicode-org.github.io/icu/userguide/format_parse/datetime/#simpledateformat). ## Min and max dates diff --git a/en/02_Developer_Guides/03_Forms/Field_types/03_HTMLEditorField.md b/en/02_Developer_Guides/03_Forms/Field_types/03_HTMLEditorField.md index ec1eb74d7..76e225cc4 100644 --- a/en/02_Developer_Guides/03_Forms/Field_types/03_HTMLEditorField.md +++ b/en/02_Developer_Guides/03_Forms/Field_types/03_HTMLEditorField.md @@ -85,11 +85,10 @@ in the framework (and the `cms` module in case you've got that installed). There can be multiple configs, which should always be created / accessed using [HTMLEditorConfig::get()](api:SilverStripe\Forms\HTMLEditor\HTMLEditorConfig::get()). You can then set the currently active config using `HTMLEditorConfig::set_active()`. -[notice] -The order in which the `_config.php` files are executed depends on the module names. Execution -order is alphabetical, so if you set a TinyMCE option in the `aardvark/_config.php` (i.e. the module name is simply `aardvark`), -this will be overridden in `vendor/silverstripe/admin/_config.php` (because the module name is `silverstripe/admin`) and your modification will disappear. -[/notice] +> [!WARNING] +> The order in which the `_config.php` files are executed depends on the module names. Execution +> order is alphabetical, so if you set a TinyMCE option in the `aardvark/_config.php` (i.e. the module name is simply `aardvark`), +> this will be overridden in `vendor/silverstripe/admin/_config.php` (because the module name is `silverstripe/admin`) and your modification will disappear. ## Adding and removing capabilities @@ -98,9 +97,8 @@ In its simplest form, the configuration of the editor includes adding and removi You can add plugins to the editor using the Framework's [TinyMCEConfig::enablePlugins()](api:SilverStripe\Forms\HTMLEditor\TinyMCEConfig::enablePlugins()) method. This will transparently generate the relevant underlying TinyMCE code. -[hint] -The `enablePlugins()` method is implemented on `TinyCMEConfig`, which is a subclass of `HTMLEditorConfig`. This is true of most of the configuration methods used in this documentation. We've done an explicit `instanceof` check here for correctness, but in reality unless your project introduces an alternative WYSIWYG editor, you can safely omit that check. The remaining examples in this documentation will omit the check. -[/hint] +> [!TIP] +> The `enablePlugins()` method is implemented on `TinyCMEConfig`, which is a subclass of `HTMLEditorConfig`. This is true of most of the configuration methods used in this documentation. We've done an explicit `instanceof` check here for correctness, but in reality unless your project introduces an alternative WYSIWYG editor, you can safely omit that check. The remaining examples in this documentation will omit the check. ```php // app/_config.php @@ -113,10 +111,9 @@ if ($editorConfig instanceof TinyMCEConfig) { } ``` -[notice] -This utilities the TinyMCE's [external_plugins](https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#external_plugins) -option under the hood. -[/notice] +> [!WARNING] +> This utilities the TinyMCE's [external_plugins](https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#external_plugins) +> option under the hood. Plugins and advanced themes can provide additional buttons that can be added (or removed) through the configuration. Here is an example of adding a `ssmacron` button after the `charmap` button: @@ -135,11 +132,10 @@ use SilverStripe\Forms\HTMLEditor\TinyMCEConfig; TinyMCEConfig::get('cms')->removeButtons('tablecontrols', 'blockquote', 'hr'); ``` -[notice] -Internally `HTMLEditorConfig` uses the TinyMCE's `toolbar` option to configure these. See the -[TinyMCE documentation of this option](https://www.tiny.cloud/docs/tinymce/6/toolbar-configuration-options/#toolbar) -for more details. -[/notice] +> [!WARNING] +> Internally `HTMLEditorConfig` uses the TinyMCE's `toolbar` option to configure these. See the +> [TinyMCE documentation of this option](https://www.tiny.cloud/docs/tinymce/6/toolbar-configuration-options/#toolbar) +> for more details. ### Setting options @@ -184,10 +180,9 @@ $validElements = str_replace('iframe[', 'iframe[data-*|'); $editor->setOption('extended_valid_elements', $validElements); ``` -[notice] -The default setting for the CMS's `extended_valid_elements` we are overriding here can be found in -`vendor/silverstripe/admin/_config.php`. -[/notice] +> [!WARNING] +> The default setting for the CMS's `extended_valid_elements` we are overriding here can be found in +> `vendor/silverstripe/admin/_config.php`. ## Enabling custom plugins @@ -200,14 +195,13 @@ use SilverStripe\Forms\HTMLEditor\TinyMCEConfig; TinyMCEConfig::get('cms')->enablePlugins(['myplugin' => 'app/javascript/myplugin/editor_plugin.js']); ``` -[hint] -The path for the plugin file must be one of the following: - -- `null` (if the plugin being enabled is a built-in plugin) -- a path, relative to your `_resources/` directory, to the plugin file -- a `ModuleResource` instance representing the plugin JavaScript file (see `silverstripe/admin`'s `_config.php` file for examples) -- an absolute URL (e.g. for a third-party plugin to be fetched from a CDN). -[/hint] +> [!TIP] +> The path for the plugin file must be one of the following: +> +> - `null` (if the plugin being enabled is a built-in plugin) +> - a path, relative to your `_resources/` directory, to the plugin file +> - a `ModuleResource` instance representing the plugin JavaScript file (see `silverstripe/admin`'s `_config.php` file for examples) +> - an absolute URL (e.g. for a third-party plugin to be fetched from a CDN). You can learn how to [create a plugin](https://www.tiny.cloud/docs/tinymce/6/creating-a-plugin/) from the TinyMCE documentation. diff --git a/en/02_Developer_Guides/03_Forms/Field_types/04_GridField.md b/en/02_Developer_Guides/03_Forms/Field_types/04_GridField.md index 8b68b65f8..1e79b45c5 100644 --- a/en/02_Developer_Guides/03_Forms/Field_types/04_GridField.md +++ b/en/02_Developer_Guides/03_Forms/Field_types/04_GridField.md @@ -13,10 +13,9 @@ Usually `GridField` is used with `DataObject` records - but it can be used with See [using `GridField` with arbitrary data](/developer_guides/forms/using_gridfield_with_arbitrary_data/) for more information. -[info] -`GridField` powers the automated data UI of [ModelAdmin](api:SilverStripe\Admin\ModelAdmin). For more information about `ModelAdmin` see the -[Customizing the CMS](/developer_guides/customising_the_admin_interface) guide. -[/info] +> [!NOTE] +> `GridField` powers the automated data UI of [ModelAdmin](api:SilverStripe\Admin\ModelAdmin). For more information about `ModelAdmin` see the +> [Customizing the CMS](/developer_guides/customising_the_admin_interface) guide. ```php use SilverStripe\Forms\GridField\GridField; @@ -25,9 +24,8 @@ use SilverStripe\Forms\GridField\GridField; $field = GridField::create($name, $title, $list); ``` -[hint] -GridField can only be used with `$list` data sets that are of the type `SS_List` such as `DataList` or `ArrayList`. -[/hint] +> [!TIP] +> GridField can only be used with `$list` data sets that are of the type `SS_List` such as `DataList` or `ArrayList`. Each `GridField` is built from a number of components grouped into the [GridFieldConfig](api:SilverStripe\Forms\GridField\GridFieldConfig). Without any components, a `GridField` has almost no functionality. The `GridFieldConfig` instance and the attached [GridFieldComponent](api:SilverStripe\Forms\GridField\GridFieldComponent) are @@ -222,12 +220,11 @@ $gridField->setConfig($config); Similar to `GridFieldConfig_Base` with the addition support of the ability to view a [`GridFieldDetailForm`](api:SilverStripe\Forms\GridField\GridFieldDetailForm) containing a read-only view of the data record. -[info] -Each record in the list must have an `ID` field, and the value of that field must be a positive integer. - -If the class representing your data has a `getCMSFields()` method, the return value of that method will be used for the fields displayed in the read-only view. -Otherwise, you'll need to pass in a [`FieldList`](api:SilverStripe\Forms\FieldList) to [`GridFieldDetailForm::setFields()`](api:SilverStripe\Forms\GridField\GridFieldDetailForm::setFields()). -[/info] +> [!NOTE] +> Each record in the list must have an `ID` field, and the value of that field must be a positive integer. +> +> If the class representing your data has a `getCMSFields()` method, the return value of that method will be used for the fields displayed in the read-only view. +> Otherwise, you'll need to pass in a [`FieldList`](api:SilverStripe\Forms\FieldList) to [`GridFieldDetailForm::setFields()`](api:SilverStripe\Forms\GridField\GridFieldDetailForm::setFields()). ```php use SilverStripe\Forms\GridField\GridFieldConfig_RecordViewer; @@ -242,24 +239,20 @@ $gridField->setConfig($config); ### `GridFieldConfig_RecordEditor` +> [!CAUTION] +> Permission control for editing and deleting the record uses the `canEdit()` and `canDelete()` methods on the class that represents your data. + Similar to `GridFieldConfig_RecordViewer` with the addition support to edit or delete each of the records. -[info] Each record in the list must have an `ID` field, and the value of that field must be a positive integer. If the class representing your data has a `getCMSFields()` method, the return value of that method will be used for the fields displayed in the read-only view. Otherwise, you'll need to pass in a [`FieldList`](api:SilverStripe\Forms\FieldList) to [`GridFieldDetailForm::setFields()`](api:SilverStripe\Forms\GridField\GridFieldDetailForm::setFields()). -[/info] - -[warning] -The class representing your data *must* implement [`DataObjectInterface`](api:SilverStripe\ORM\DataObjectInterface) so that your records can be edited. - -See [using `GridField` with arbitrary data](/developer_guides/forms/using_gridfield_with_arbitrary_data/) for more information. -[/warning] -[alert] -Permission control for editing and deleting the record uses the `canEdit()` and `canDelete()` methods on the class that represents your data. -[/alert] +> [!WARNING] +> The class representing your data *must* implement [`DataObjectInterface`](api:SilverStripe\ORM\DataObjectInterface) so that your records can be edited. +> +> See [using `GridField` with arbitrary data](/developer_guides/forms/using_gridfield_with_arbitrary_data/) for more information. ```php use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor; @@ -467,10 +460,9 @@ Fragments are designated areas within a `GridField` which can be shared between your own fragments by using a `\$DefineFragment` placeholder in your component's template. This example will simply create an area rendered before the table wrapped in a simple `
`. -[notice] -Please note that in templates, you'll need to escape the dollar sign on `\$DefineFragment`. These are specially -processed placeholders as opposed to native template syntax. -[/notice] +> [!WARNING] +> Please note that in templates, you'll need to escape the dollar sign on `\$DefineFragment`. These are specially +> processed placeholders as opposed to native template syntax. ```php namespace App\Form\GridField; diff --git a/en/02_Developer_Guides/03_Forms/How_Tos/02_Lightweight_Form.md b/en/02_Developer_Guides/03_Forms/How_Tos/02_Lightweight_Form.md index afb86774f..0e931e6d4 100644 --- a/en/02_Developer_Guides/03_Forms/How_Tos/02_Lightweight_Form.md +++ b/en/02_Developer_Guides/03_Forms/How_Tos/02_Lightweight_Form.md @@ -13,9 +13,8 @@ For example, a basic search form. We want to use the [Form](api:SilverStripe\For totally custom template to meet our needs. To do this, we'll provide the class with a unique template through [`setTemplate()`](api:SilverStripe\Forms\Form::setTemplate()). -[info] -If you just want to change the template for a given form field instead, you can call [`setTemplate()`](api:SilverStripe\Forms\FormField::setTemplate()) on the individual field. -[/info] +> [!NOTE] +> If you just want to change the template for a given form field instead, you can call [`setTemplate()`](api:SilverStripe\Forms\FormField::setTemplate()) on the individual field. ```php // app/src/PageType/SearchPage.php @@ -64,6 +63,5 @@ class SearchPage extends Page `SearchForm.ss` will be executed within the scope of the `Form` object so has access to any of the methods and properties on [Form](api:SilverStripe\Forms\Form) such as `$Fields` and `$Actions`. -[notice] -To understand more about Scope or the syntax for custom templates, read the [Templates](../../templates) guide. -[/notice] +> [!WARNING] +> To understand more about Scope or the syntax for custom templates, read the [Templates](../../templates) guide. diff --git a/en/02_Developer_Guides/03_Forms/How_Tos/04_Create_a_GridField_ActionProvider.md b/en/02_Developer_Guides/03_Forms/How_Tos/04_Create_a_GridField_ActionProvider.md index 84f9bba6c..e0bf96142 100644 --- a/en/02_Developer_Guides/03_Forms/How_Tos/04_Create_a_GridField_ActionProvider.md +++ b/en/02_Developer_Guides/03_Forms/How_Tos/04_Create_a_GridField_ActionProvider.md @@ -21,12 +21,11 @@ perform custom operations on a row: 1. Create a custom action 1. [Add your custom action to the `GridFieldConfig`](#add-action-to-config) -[info] -To create a custom action follow the [Basic GridField custom action boilerplate](#custom-action-boilerplate) below. - -If you would like to create a custom action in the GridField action menu follow the -[Add a GridField custom action to the `GridField_ActionMenu`](#implement-gridfield-actionmenuitem) -[/info] +> [!NOTE] +> To create a custom action follow the [Basic GridField custom action boilerplate](#custom-action-boilerplate) below. +> +> If you would like to create a custom action in the GridField action menu follow the +> [Add a GridField custom action to the `GridField_ActionMenu`](#implement-gridfield-actionmenuitem) ## Basic `GridField` custom action boilerplate {#custom-action-boilerplate} diff --git a/en/02_Developer_Guides/03_Forms/How_Tos/05_Simple_Contact_Form.md b/en/02_Developer_Guides/03_Forms/How_Tos/05_Simple_Contact_Form.md index 2bbbfb29c..b701a438b 100644 --- a/en/02_Developer_Guides/03_Forms/How_Tos/05_Simple_Contact_Form.md +++ b/en/02_Developer_Guides/03_Forms/How_Tos/05_Simple_Contact_Form.md @@ -135,12 +135,11 @@ class ContactPageController extends PageController } ``` -[hint] -Caution: This form is prone to abuse by spammers, -since it doesn't enforce rate limitation and doesn't implement any checks for bots. -We recommend to use a validation service like the ["recaptcha" module](https://github.com/silverstripe/silverstripe-spamprotection) -for better security. -[/hint] +> [!CAUTION] +> Caution: This form is prone to abuse by spammers, +> since it doesn't enforce rate limitation and doesn't implement any checks for bots. +> We recommend to use a validation service like the ["recaptcha" module](https://github.com/silverstripe/silverstripe-spamprotection) +> for better security. Any function that receives a form submission takes two arguments: the data passed to the form as an indexed array, and the form itself. In order to extract the data, you can either use functions on the form object to get the fields and query their values, or just use the raw data in the array. In the example above, we used the array, as it's the easiest way to get data without requiring the form fields to perform any special transformations. diff --git a/en/02_Developer_Guides/04_Configuration/00_Configuration.md b/en/02_Developer_Guides/04_Configuration/00_Configuration.md index 3b613d073..e5fda31d5 100644 --- a/en/02_Developer_Guides/04_Configuration/00_Configuration.md +++ b/en/02_Developer_Guides/04_Configuration/00_Configuration.md @@ -18,9 +18,8 @@ properties API: - Configuration is normally set once during initialization and then not changed. - Configuration is normally set by a knowledgeable technical user, such as a developer, not the end user. -[notice] -For providing content editors or CMS users a place to manage configuration see the [SiteConfig](siteconfig) module. -[/notice] +> [!WARNING] +> For providing content editors or CMS users a place to manage configuration see the [SiteConfig](siteconfig) module. ## Configuration properties @@ -44,28 +43,26 @@ class MyClass } ``` -[info] -Generally speaking, a private static property in Silverstripe CMS means a configuration property. -If you want a private static property that has no interactions with the configuration API, you can -mark it `@internal` in the property's PHPdoc. - -```php -namespace App; - -use SilverStripe\Core\Config\Configurable; - -class MyClass -{ - use Configurable; - - /** - * @internal - */ - private static $not_config; -} -``` - -[/info] +> [!NOTE] +> Generally speaking, a private static property in Silverstripe CMS means a configuration property. +> If you want a private static property that has no interactions with the configuration API, you can +> mark it `@internal` in the property's PHPdoc. +> +> ```php +> namespace App; +> +> use SilverStripe\Core\Config\Configurable; +> +> class MyClass +> { +> use Configurable; +> +> /** +> * @internal +> */ +> private static $not_config; +> } +> ``` ## Accessing configuration properties @@ -123,9 +120,8 @@ App\MyClass: - Baz ``` -[hint] -See [Configuration YAML Syntax and Rules](#configuration-yaml-syntax-and-rules) below for more information about the YAML configuration syntax. -[/hint] +> [!TIP] +> See [Configuration YAML Syntax and Rules](#configuration-yaml-syntax-and-rules) below for more information about the YAML configuration syntax. The values we've defined in YAML are *merged* with the existing configuration (see [Configuration Values](#configuration-values) below): @@ -139,10 +135,9 @@ echo MyClass::config()->get('option_one'); echo implode(', ', MyClass::config()->get('option_two')); ``` -[notice] -There is no way currently to restrict read or write access to any configuration property, or influence/validate the values -being read or written. -[/notice] +> [!WARNING] +> There is no way currently to restrict read or write access to any configuration property, or influence/validate the values +> being read or written. ## Configuration values @@ -174,11 +169,10 @@ rules: ``` - If the value is not an array, the highest priority value is used without any attempt to merge -[alert] -The exception to this is "falsey" values - empty arrays, empty strings, etc. When merging a truthy value with -a falsey value, the result will be the truthy value regardless of priority. When merging two falsey values -the result will be the higher priority falsey value. -[/alert] +> [!CAUTION] +> The exception to this is "falsey" values - empty arrays, empty strings, etc. When merging a truthy value with +> a falsey value, the result will be the truthy value regardless of priority. When merging two falsey values +> the result will be the higher priority falsey value. The locations that configuration values are taken from in highest to lowest priority order are: @@ -189,10 +183,9 @@ The locations that configuration values are taken from in highest to lowest prio - The composite configuration value of the parent class of this class - Any static set on an "additional static source" class (such as an extension) named the same as the name of the property -[notice] -It is incorrect to have mixed types of the same named property in different locations - but an error will not necessarily -be raised due to optimizations in the lookup code. -[/notice] +> [!WARNING] +> It is incorrect to have mixed types of the same named property in different locations - but an error will not necessarily +> be raised due to optimizations in the lookup code. ## Configuration masks @@ -218,18 +211,16 @@ bitwise `|` operator. ## Configuration YAML syntax and rules -[alert] -YAML files can not be placed any deeper than 2 directories deep. This will only affect you if you nest your modules deeper than the top level of your project. -[/alert] +> [!CAUTION] +> YAML files can not be placed any deeper than 2 directories deep. This will only affect you if you nest your modules deeper than the top level of your project. Each module can have a directory immediately underneath the main module directory called `_config/`. Inside this directory you can add YAML files that contain values for the configuration system. -[info] -The name of the files within the project's `_config/` directly are arbitrary. Our examples use -`app/_config/app.yml` but you can break this file down into smaller files, or clearer patterns like `extensions.yml`, -`email.yml` if you want. -[/info] +> [!NOTE] +> The name of the files within the project's `_config/` directly are arbitrary. Our examples use +> `app/_config/app.yml` but you can break this file down into smaller files, or clearer patterns like `extensions.yml`, +> `email.yml` if you want. ### Syntax @@ -251,9 +242,8 @@ SilverStripe\Control\Director: The header typically includes the name of this value set and some rules which apply to it - e.g. to evaluate this value set before or after some other named set. -[info] -If there is only one set of values and you don't want any rules to apply to the value set, the header can be omitted. -[/info] +> [!NOTE] +> If there is only one set of values and you don't want any rules to apply to the value set, the header can be omitted. Each value set of a YAML file implicitly has a reference path which is made up of the module name, the config file name, and a fragment identifier. Reference paths look like this: `module/file#fragment` - e.g `admin/routes#adminroutes`. @@ -303,9 +293,8 @@ You do not have to specify all portions of a reference path. Any portion may be out all together. Either has the same affect - that portion will be ignored when checking a value section's reference path, and will always match. You may even specify just `'*'`, which means "all value sections". -[notice] -Be careful when using wildcards, as this can result in circular dependencies. An error will be thrown if that happens. -[/notice] +> [!WARNING] +> Be careful when using wildcards, as this can result in circular dependencies. An error will be thrown if that happens. When a particular value section matches both a `Before` *and* an `After` rule, this may be a problem. Clearly one value section can not be both before *and* after another. However when you have used wildcards, if there @@ -322,10 +311,9 @@ after value sections with a name of `rootroutes`. However because `'*'` implicit (it is the equivalent of `'*/*#*'`) but `#rootroutes` only has two (it is the equivalent of `'*/*#rootroutes'`), the `Before` rule ultimately gets evaluated as meaning "every value section *except* ones that have a fragment name of rootroutes". -[alert] -It is possible to create chains that are unsolvable. For instance, A must be before B, B must be before C, C must be -before A. In this case you will get an error when accessing your site. -[/alert] +> [!CAUTION] +> It is possible to create chains that are unsolvable. For instance, A must be before B, B must be before C, C must be +> before A. In this case you will get an error when accessing your site. #### Exclusionary rules @@ -393,11 +381,10 @@ Only: --- ``` -[alert] -When you have more than one rule for a nested fragment, they're joined like -`FRAGMENT_INCLUDED = (ONLY && ONLY) && !(EXCEPT && EXCEPT)`. -That is, the fragment will be included if all Only rules match, except if all Except rules match. -[/alert] +> [!CAUTION] +> When you have more than one rule for a nested fragment, they're joined like +> `FRAGMENT_INCLUDED = (ONLY && ONLY) && !(EXCEPT && EXCEPT)`. +> That is, the fragment will be included if all Only rules match, except if all Except rules match. ## Unit tests diff --git a/en/02_Developer_Guides/04_Configuration/01_SiteConfig.md b/en/02_Developer_Guides/04_Configuration/01_SiteConfig.md index 2133c153a..79e16040f 100644 --- a/en/02_Developer_Guides/04_Configuration/01_SiteConfig.md +++ b/en/02_Developer_Guides/04_Configuration/01_SiteConfig.md @@ -67,10 +67,9 @@ Silverstripe\SiteConfig\SiteConfig: - App\Extension\CustomSiteConfig ``` -[notice] -After adding the class and the YAML change, make sure to rebuild your database by visiting `https://www.example.com/dev/build`. -You may also need to reload the screen with a `?flush=1` i.e.`https://www.example.com/admin/settings?flush=1`. -[/notice] +> [!WARNING] +> After adding the class and the YAML change, make sure to rebuild your database by visiting `https://www.example.com/dev/build`. +> You may also need to reload the screen with a `?flush=1` i.e.`https://www.example.com/admin/settings?flush=1`. You can define as many extensions for `SiteConfig` as you need. For example, if you're developing a module and want to provide the users a place to configure site-wide settings then the `SiteConfig` panel is the place to go it. diff --git a/en/02_Developer_Guides/05_Extending/00_Modules.md b/en/02_Developer_Guides/05_Extending/00_Modules.md index b32225491..5cc1f66ff 100644 --- a/en/02_Developer_Guides/05_Extending/00_Modules.md +++ b/en/02_Developer_Guides/05_Extending/00_Modules.md @@ -48,9 +48,8 @@ on your project's stability configuration. To lock down to a specific version, branch or commit, read up on ["lock" files](https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control). -[notice] -After you add or remove modules, make sure you rebuild the database, class and configuration manifests by going to `https://www.example.com/dev/build?flush=1` -[/notice] +> [!WARNING] +> After you add or remove modules, make sure you rebuild the database, class and configuration manifests by going to `https://www.example.com/dev/build?flush=1` ## Creating a module {#create} @@ -101,10 +100,9 @@ In most cases we recommended using a new project, at least at first. If this is a module you intend to be available publicly, it might make sense to submit the repository to [Packagist](https://packagist.org/) at this stage. -[note] -If you want your module to be private or for some reason don't want to publish it in packagist just yet, -see [Including a private module in your project](#including-a-private-module-in-your-project) below. -[/note] +> [!NOTE] +> If you want your module to be private or for some reason don't want to publish it in packagist just yet, +> see [Including a private module in your project](#including-a-private-module-in-your-project) below. Once you've done that, you can simply install it like you would any other dependency - just make sure you use the `--prefer-source` option, which will ensure @@ -114,11 +112,10 @@ Composer installs the module directly from GitHub and keeps the initialised loca composer require my_vendor/module_name:dev-main --prefer-source ``` -[hint] -The `dev-main` portion of the above command above is a version constraint which tells Composer to install your module from the `main` branch. -If you are using a different branch name, you should use the correct branch here instead (e.g. if the branch name is `development`, the constraint -will be `dev-development`). -[/hint] +> [!TIP] +> The `dev-main` portion of the above command above is a version constraint which tells Composer to install your module from the `main` branch. +> If you are using a different branch name, you should use the correct branch here instead (e.g. if the branch name is `development`, the constraint +> will be `dev-development`). Once Composer has installed the module, you can develop your module in the `vendor/my_vendor/module_name` directory, and commit/push changes from there to the remote repository in GitHub. @@ -130,9 +127,8 @@ Including public or private repositories that are not indexed on **Packagist** i For our example module you can add the following lines to your `composer.json` file in the root directory of your main project. -[notice] -This goes into the `composer.json` for the Silverstripe CMS project where you're installing your module, *not* into the `composer.json` of your module itself. -[/notice] +> [!WARNING] +> This goes into the `composer.json` for the Silverstripe CMS project where you're installing your module, *not* into the `composer.json` of your module itself. ```json { diff --git a/en/02_Developer_Guides/05_Extending/01_Extensions.md b/en/02_Developer_Guides/05_Extending/01_Extensions.md index 721e73424..f269e06d2 100644 --- a/en/02_Developer_Guides/05_Extending/01_Extensions.md +++ b/en/02_Developer_Guides/05_Extending/01_Extensions.md @@ -13,10 +13,9 @@ trait applied within core, modules or even their own code to make it more reusab Extensions are defined as subclasses of the [`Extension`](api:SilverStripe\Core\Extension) class. Typically, subclasses of the [`DataExtension`](api:SilverStripe\ORM\DataExtension) class are used for extending a [`DataObject`](api:SilverStripe\ORM\DataObject) subclass. -[info] -For performance reasons a few classes are excluded from receiving extensions, including `ViewableData` -and `RequestHandler`. You can still apply extensions to descendants of these classes. -[/info] +> [!NOTE] +> For performance reasons a few classes are excluded from receiving extensions, including `ViewableData` +> and `RequestHandler`. You can still apply extensions to descendants of these classes. ```php // app/src/Extension/MyMemberExtension.php @@ -38,9 +37,8 @@ class MyMemberExtension extends DataExtension } ``` -[info] -Convention is for extension class names to end in `Extension`. This isn't a requirement but makes it clearer -[/info] +> [!NOTE] +> Convention is for extension class names to end in `Extension`. This isn't a requirement but makes it clearer After this class has been created, it does not yet apply it to any object. We need to tell Silverstripe CMS what classes we want to add the `MyMemberExtension` to. To activate this extension, add the following via the [Configuration API](../configuration). @@ -151,18 +149,16 @@ App\Extension\MyDataClassConfigExtension: new_config_property: true ``` -[notice] -Note that the value for `key1` in the `my_configuration_property` array was *not* overridden by the extension class. Configuration declared in an extension class is merged into the base class as a lower priority than the base class itself. Where there is any collision between the configuration declared on the base class and on the extension class, the base class configuration is used. - -If you need to override values, you should do so using the [yml configuration API](/developer_guides/configuration/configuration). - -```yml -App\Data\MyDataClass: - my_configuration_property: - key1: 'is overridden' -``` - -[/notice] +> [!WARNING] +> Note that the value for `key1` in the `my_configuration_property` array was *not* overridden by the extension class. Configuration declared in an extension class is merged into the base class as a lower priority than the base class itself. Where there is any collision between the configuration declared on the base class and on the extension class, the base class configuration is used. +> +> If you need to override values, you should do so using the [yml configuration API](/developer_guides/configuration/configuration). +> +> ```yml +> App\Data\MyDataClass: +> my_configuration_property: +> key1: 'is overridden' +> ``` See [Configuration API](/developer_guides/configuration/configuration/) for more information about configuration properties. @@ -218,9 +214,8 @@ $member = Security::getCurrentUser(); echo $member->getGreeting(); ``` -[notice] -Note that `protected`, `private`, and `static` methods *are not* accessible from the extended object/class. -[/notice] +> [!WARNING] +> Note that `protected`, `private`, and `static` methods *are not* accessible from the extended object/class. ## Modifying existing methods @@ -269,9 +264,8 @@ class MyMemberExtension extends DataExtension } ``` -[info] -In this case the `$validator` argument can be modified directly, as it is an object. To modify literals, you will need to [explicitly pass by reference](https://www.php.net/manual/en/language.references.pass.php). -[/info] +> [!NOTE] +> In this case the `$validator` argument can be modified directly, as it is an object. To modify literals, you will need to [explicitly pass by reference](https://www.php.net/manual/en/language.references.pass.php). Another common example of when you will want to modify a method is to update the default CMS fields for an object in an extension. The `CMS` provides a `updateCMSFields` Extension Hook to tie into. @@ -303,10 +297,9 @@ class MyMemberExtension extends DataExtension } ``` -[notice] -If you're providing a module or working on code that may need to be extended by other code, it should provide a *hook* -which allows an Extension to modify the results. -[/notice] +> [!WARNING] +> If you're providing a module or working on code that may need to be extended by other code, it should provide a *hook* +> which allows an Extension to modify the results. ```php namespace App\Model; @@ -356,10 +349,9 @@ callback to be executed immediately before and after `extend()` is called on ext This is useful in many cases where working with modules such as `tractorcow/silverstripe-fluent` which operate on `DataObject` fields that must exist in the `FieldList` at the time that `$this->extend('UpdateCMSFields')` is called. -[notice] -Please note that each callback is only ever called once, and then cleared, so multiple extensions to the same function -require that a callback is registered each time, if necessary. -[/notice] +> [!WARNING] +> Please note that each callback is only ever called once, and then cleared, so multiple extensions to the same function +> require that a callback is registered each time, if necessary. Example: A class that wants to control default values during object initialization. The code needs to assign a value if not specified in `self::$defaults`, but before extensions have been called: @@ -390,9 +382,8 @@ class MyModel extends DataObject Example 2: User code can intervene in the process of extending CMS fields. -[notice] -This method is preferred to disabling, enabling, and calling field extensions manually. -[/notice] +> [!WARNING] +> This method is preferred to disabling, enabling, and calling field extensions manually. ```php namespace App\Model; @@ -446,12 +437,11 @@ class CustomisedSomeExtension extends SomeExtension } ``` -[notice] -Please note that overriding the extension like this should be done in YAML configuration using the injector only. It is not recommended -to use `Config::modify()->set()` to adjust the implementation class name of an extension after the configuration -manifest has been loaded, which may not work consistently due to the "extra methods" cache having already been -populated. -[/notice] +> [!WARNING] +> Please note that overriding the extension like this should be done in YAML configuration using the injector only. It is not recommended +> to use `Config::modify()->set()` to adjust the implementation class name of an extension after the configuration +> manifest has been loaded, which may not work consistently due to the "extra methods" cache having already been +> populated. ## Related lessons diff --git a/en/02_Developer_Guides/05_Extending/04_Shortcodes.md b/en/02_Developer_Guides/05_Extending/04_Shortcodes.md index 3a217179b..fe3365172 100644 --- a/en/02_Developer_Guides/05_Extending/04_Shortcodes.md +++ b/en/02_Developer_Guides/05_Extending/04_Shortcodes.md @@ -85,9 +85,8 @@ public function getMyHtmlVarcharField() } ``` -[info] -See [Data types and Casting](/developer_guides/model/data_types_and_casting/#overriding) for more information about getter methods. -[/info] +> [!NOTE] +> See [Data types and Casting](/developer_guides/model/data_types_and_casting/#overriding) for more information about getter methods. ## Defining custom shortcodes @@ -108,15 +107,14 @@ class MyShortCodeProvider Note that the `$casting` configuration here is optional - it's used in this case to allow directly calling this method from a template. It doesn't affect the actual shortcode functionality at all. -[warning] -Note that the `$arguments` parameter potentially contains any arbitrary key/value pairs the user has chosen to include. -It is strongly recommended that you don't directly convert this array into a list of attributes for your final HTML markup -as that could lead to XSS vulnerabilities in your project. - -If you want to use the `$arguments` parameter as a list of attributes for your final HTML markup, it is strongly recommended that you -pass the array through a filter of allowed arguments using [array_filter()](https://www.php.net/manual/en/function.array-filter.php) -or similar. -[/warning] +> [!WARNING] +> Note that the `$arguments` parameter potentially contains any arbitrary key/value pairs the user has chosen to include. +> It is strongly recommended that you don't directly convert this array into a list of attributes for your final HTML markup +> as that could lead to XSS vulnerabilities in your project. +> +> If you want to use the `$arguments` parameter as a list of attributes for your final HTML markup, it is strongly recommended that you +> pass the array through a filter of allowed arguments using [array_filter()](https://www.php.net/manual/en/function.array-filter.php) +> or similar. These parameters are passed to the `parseMyShortCode` callback: @@ -139,9 +137,8 @@ use SilverStripe\View\Parsers\ShortcodeParser; ShortcodeParser::get('default')->register('my_shortcode', [MyShortCodeProvider::class, 'parseMyShortCode']); ``` -[info] -Note that `my_shortcode` is an arbitrary name which can be made up of alphanumeric characters and the underscore (`_`) character. If you try to register a shortcode with a name using any other characters, it will not work. -[/info] +> [!NOTE] +> Note that `my_shortcode` is an arbitrary name which can be made up of alphanumeric characters and the underscore (`_`) character. If you try to register a shortcode with a name using any other characters, it will not work. ## Built-in shortcodes diff --git a/en/02_Developer_Guides/05_Extending/05_Injector.md b/en/02_Developer_Guides/05_Extending/05_Injector.md index 7f12ad1b5..bab12f39c 100644 --- a/en/02_Developer_Guides/05_Extending/05_Injector.md +++ b/en/02_Developer_Guides/05_Extending/05_Injector.md @@ -81,9 +81,8 @@ $object === $object2; As with `create()`, you can pass as many arguments as you need to the instantiated singleton by passing them into `get()` - but you'll need to pass them in as an array to the third argument or as a named `$constructorArgs` argument, since `get()`'s second argument is a boolean to determine whether the instantiated object is a singleton or not. -[info] -The arguments passed in for the singleton's constructor will only take effect the first time the singleton is instantiated - after that, because it is a singleton and has therefore already been instantiated, the constructor arguments will be ignored. -[/info] +> [!NOTE] +> The arguments passed in for the singleton's constructor will only take effect the first time the singleton is instantiated - after that, because it is a singleton and has therefore already been instantiated, the constructor arguments will be ignored. ```php use App\MyClient; @@ -137,9 +136,8 @@ $client = Injector::inst()->get(MyClient::class); // $client is now an instance of WriteClient ``` -[info] -Note that `App\MyClient` [does not have to be an existing class](#service-inheritance) - you can use abitrary strings to identify singleton services. That said, using existing classes can be easier to reason about and can be refactored by automatic tools/IDEs - along with providing a valid default class to use if one is not explicitly registered. -[/info] +> [!NOTE] +> Note that `App\MyClient` [does not have to be an existing class](#service-inheritance) - you can use abitrary strings to identify singleton services. That said, using existing classes can be easier to reason about and can be refactored by automatic tools/IDEs - along with providing a valid default class to use if one is not explicitly registered. Using Injector imperatively like this is most common [in testing](#testing-with-injector). Usually, the configuration API is used instead. @@ -166,9 +164,8 @@ $object = Injector::inst()->get(MyClient::class); This allows you to concisely override classes in Silverstripe core or other third-party Silverstripe code. -[info] -When overriding other configuration beware the [order that configuration is applied](../configuration/#configuration-values). You may have to use the [Before/After](../configuration/#before-after-rules) syntax to apply your override. -[/info] +> [!NOTE] +> When overriding other configuration beware the [order that configuration is applied](../configuration/#configuration-values). You may have to use the [Before/After](../configuration/#before-after-rules) syntax to apply your override. ### Special YAML syntax @@ -218,9 +215,8 @@ SilverStripe\Core\Injector\Injector: secret: '`SS_API_CLIENT_SECRET`' ``` -[info] -Note: undefined variables will be replaced with null. -[/info] +> [!NOTE] +> Note: undefined variables will be replaced with null. You can have multiple environment variables within a single value, though the overall value must start and end with backticks. @@ -237,9 +233,8 @@ SilverStripe\Core\Injector\Injector: Silverstripe classes can declare a special `$dependencies` array which can quickly configure dependencies when used with the injector API. The `Injector` will evaluate the array values and assign the appropriate value to a property that matches the array key. For example: -[info] -Just like the YAML syntax discussed above, constants and environment variables can be substitutes in dependency values using backticks. -[/info] +> [!NOTE] +> Just like the YAML syntax discussed above, constants and environment variables can be substitutes in dependency values using backticks. ```php namespace App\Control; @@ -283,9 +278,8 @@ class MyController extends Controller } ``` -[info] -Note the properties set by `Injector` must be public properties, or have a public setter method. -[/info] +> [!NOTE] +> Note the properties set by `Injector` must be public properties, or have a public setter method. When creating a new instance of `App\Control\MyController` via Injector the permissions property will contain an instance of the `ThirdParty\PermissionService` that was resolved by Injector, and the `defaultText` property will contain the string defined in the `$dependencies` array. @@ -487,13 +481,12 @@ use SilverStripe\Core\Injector\Injector; $instance = Injector::inst()->get(MyService::class); ``` -[note] -For simplicity, the above example doesn't use the `$service` parameter, though it needs to be declared to match the method signature from the `Factory` interface. - -The `$service` parameter will hold the name of the service being requested, which allows you to use the same factory class for multiple different services if you want to. In the above example, the value would be `'App\MyService'`. - -The `$params` parameter will hold any constructor arguments that are passed into the `get()` method, as an array. In the above example it is simply an empty array. -[note] +> [!NOTE] +> For simplicity, the above example doesn't use the `$service` parameter, though it needs to be declared to match the method signature from the `Factory` interface. +> +> The `$service` parameter will hold the name of the service being requested, which allows you to use the same factory class for multiple different services if you want to. In the above example, the value would be `'App\MyService'`. +> +> The `$params` parameter will hold any constructor arguments that are passed into the `get()` method, as an array. In the above example it is simply an empty array. ### Factory method diff --git a/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md b/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md index ba9920c48..405c5194c 100644 --- a/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md +++ b/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md @@ -39,44 +39,40 @@ class PageTest extends SapphireTest } ``` -[info] -Tests for your application should be stored in the `app/tests` directory. Test cases for add-ons should be stored in -the `(modulename)/tests` directory. - -Test case classes should end with `Test` (e.g. `PageTest`) and test methods must start with `test` (e.g. `testMyMethod`). - -Ensure you [import](https://php.net/manual/en/language.namespaces.importing.php#example-252) any classes you need for the test, including `SilverStripe\Dev\SapphireTest` or `SilverStripe\Dev\FunctionalTest`. -[/info] +> [!NOTE] +> Tests for your application should be stored in the `app/tests` directory. Test cases for add-ons should be stored in +> the `(modulename)/tests` directory. +> +> Test case classes should end with `Test` (e.g. `PageTest`) and test methods must start with `test` (e.g. `testMyMethod`). +> +> Ensure you [import](https://php.net/manual/en/language.namespaces.importing.php#example-252) any classes you need for the test, including `SilverStripe\Dev\SapphireTest` or `SilverStripe\Dev\FunctionalTest`. A Silverstripe CMS unit test is created by extending one of two classes, [SapphireTest](api:SilverStripe\Dev\SapphireTest) or [FunctionalTest](api:SilverStripe\Dev\FunctionalTest). [SapphireTest](api:SilverStripe\Dev\SapphireTest) is used to test your model logic (such as a `DataObject`), and [FunctionalTest](api:SilverStripe\Dev\FunctionalTest) is used when you want to test a `Controller`, `Form` or anything that requires a web page. -[info] -`FunctionalTest` is a subclass of `SapphireTest` so will inherit all of the behaviors. By subclassing `FunctionalTest` -you gain the ability to load and test web pages on the site. - -`SapphireTest` in turn, extends `PHPUnit\Framework\TestCase`. For more information on `PHPUnit\Framework\TestCase` see -the [PHPUnit](https://www.phpunit.de) documentation. It provides a lot of fundamental concepts that we build on in this -documentation. -[/info] +> [!NOTE] +> `FunctionalTest` is a subclass of `SapphireTest` so will inherit all of the behaviors. By subclassing `FunctionalTest` +> you gain the ability to load and test web pages on the site. +> +> `SapphireTest` in turn, extends `PHPUnit\Framework\TestCase`. For more information on `PHPUnit\Framework\TestCase` see +> the [PHPUnit](https://www.phpunit.de) documentation. It provides a lot of fundamental concepts that we build on in this +> documentation. ## Test databases and fixtures +> [!CAUTION] +> As the test runner will create new databases for the tests to run, the database user should have the appropriate +> permissions to create new databases on your server. + Silverstripe CMS tests create their own database when the test starts and fixture files are specified. New `ss_tmp` databases are created using the same connection details you provide for the main website. The new `ss_tmp` database does not copy what is currently in your application database. To provide seed data use a [Fixture](fixtures) file. -[alert] -As the test runner will create new databases for the tests to run, the database user should have the appropriate -permissions to create new databases on your server. -[/alert] - -[notice] -The test database is rebuilt every time one of the test methods is run and is removed afterwards. If the test is interrupted, the database will not be removed. Over time, you may have several hundred test -databases on your machine. To get rid of them, run `sake dev/tasks/CleanupTestDatabasesTask`. -[/notice] +> [!WARNING] +> The test database is rebuilt every time one of the test methods is run and is removed afterwards. If the test is interrupted, the database will not be removed. Over time, you may have several hundred test +> databases on your machine. To get rid of them, run `sake dev/tasks/CleanupTestDatabasesTask`. ## Custom PHPUnit configuration diff --git a/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md b/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md index 71e65c000..215a5c2b9 100644 --- a/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md +++ b/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md @@ -101,9 +101,8 @@ Assert that the most recently queried page contains a number of content tags spe selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The assertion fails if one of the expectedMatches fails to appear. -[notice] -` ` characters are stripped from the content; make sure that your assertions take this into account. -[/notice] +> [!WARNING] +> ` ` characters are stripped from the content; make sure that your assertions take this into account. ### `assertExactHTMLMatchBySelector` @@ -117,9 +116,8 @@ Assert that the most recently queried page contains a number of content tags spe selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The assertion fails if one of the expectedMatches fails to appear. -[notice] -` ` characters are stripped from the content; make sure that your assertions take this into account. -[/notice] +> [!WARNING] +> ` ` characters are stripped from the content; make sure that your assertions take this into account. ## Related documentation diff --git a/en/02_Developer_Guides/06_Testing/04_Fixtures.md b/en/02_Developer_Guides/06_Testing/04_Fixtures.md index 9a4ceee64..a1a62b328 100644 --- a/en/02_Developer_Guides/06_Testing/04_Fixtures.md +++ b/en/02_Developer_Guides/06_Testing/04_Fixtures.md @@ -139,19 +139,12 @@ seen by the fields prefixed with `=>`. Each one of our Players has a relationship to a Team, this is shown with the `Team` field for each `Player` being set to `=>App\Test\Team.` followed by a team name. -[info] Take the player John in our example YAML, his team is the Hurricanes which is represented by `=>App\Test\Team.hurricanes`. This sets the `has_one` relationship for John with with the `Team` object `hurricanes`. -[/info] -[hint] -Note that we use the name of the relationship (Team), and not the name of the -database field (TeamID). -[/hint] - -[hint] -Also be aware the target of a relationship must be defined before it is referenced, for example the `hurricanes` team must appear in the fixture file before the line `Team: =>App\Test\Team.hurricanes`. -[/hint] +> [!TIP] +> Note that we use the name of the relationship (Team), and not the name of the +> database field (TeamID). This style of relationship declaration can be used for any type of relationship (i.e. `has_one`, `has_many`, `many_many`). @@ -176,6 +169,9 @@ App\Test\Team: Players: =>App\Test\Player.joe,=>App\Test\Player.jack ``` +> [!WARNING] +> Be aware the target of a relationship must be defined before it is referenced, for example the `hurricanes` team must appear in the fixture file before the line `Team: =>App\Test\Team.hurricanes`. + The database is populated by instantiating `DataObject` objects and setting the fields declared in the `YAML`, then calling `write()` on those objects. Take for instance the `hurricances` record in the `YAML`. It is equivalent to writing: @@ -193,10 +189,9 @@ $team->write(); $team->Players()->add($john); ``` -[notice] -As the YAML fixtures will call `write`, any `onBeforeWrite()` or default value logic will be executed as part of the -test. -[/notice] +> [!WARNING] +> As the YAML fixtures will call `write`, any `onBeforeWrite()` or default value logic will be executed as part of the +> test. ## Fixtures for namespaced classes @@ -213,9 +208,8 @@ App\Test\Team: Players: =>App\Test\Player.john ``` -[notice] -If your tests are failing and your database has table names that follow the fully qualified class names, you've probably forgotten to implement `private static $table_name = 'Player';` on your namespaced class. See [DataObject](api:SilverStripe\ORM\DataObject) for an example. -[/notice] +> [!WARNING] +> If your tests are failing and your database has table names that follow the fully qualified class names, you've probably forgotten to implement `private static $table_name = 'Player';` on your namespaced class. See [DataObject](api:SilverStripe\ORM\DataObject) for an example. ## Defining many_many_extraFields @@ -295,9 +289,8 @@ While manually defined fixtures provide full flexibility, they offer very little Alternatively, you can use the [FixtureFactory](api:SilverStripe\Dev\FixtureFactory) class, which allows you to set default values, callbacks on object creation, and dynamic/lazy value setting. -[hint] -`SapphireTest` uses `FixtureFactory` under the hood when it is provided with YAML based fixtures. -[/hint] +> [!TIP] +> `SapphireTest` uses `FixtureFactory` under the hood when it is provided with YAML based fixtures. The idea is that rather than instantiating objects directly, we'll have a factory class for them. This factory can have *blueprints* defined on it, which tells the factory how to instantiate an object of a specific type. Blueprints need a @@ -325,10 +318,9 @@ $obj = $factory->createObject(Team::class, 'hurricanes', [ ]); ``` -[warning] -It is important to remember that fixtures are referenced by arbitrary identifiers ('hurricanes'). These are internally -mapped to their database identifiers. -[/warning] +> [!WARNING] +> It is important to remember that fixtures are referenced by arbitrary identifiers ('hurricanes'). These are internally +> mapped to their database identifiers. After we've created this object in the factory, `getId` is used to retrieve it by the identifier. diff --git a/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md b/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md index 89b89610e..2d2b2ac3b 100644 --- a/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md +++ b/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md @@ -85,17 +85,15 @@ Firstly we define a static `$fixture_file`, this should point to a file that rep represented as a YAML [Fixture](../fixtures). When our test is run, the data from this file will be loaded into a test database and discarded at the end of the test. -[notice] -The `fixture_file` property can be path to a file, or an array of strings pointing to many files. The path must be -absolute from your project root folder. -[/notice] +> [!WARNING] +> The `fixture_file` property can be path to a file, or an array of strings pointing to many files. The path must be +> absolute from your project root folder. The second part of our class is the `testURLGeneration` method. This method is our test. When the test is executed, methods prefixed with the word `test` will be run. -[notice] -The test database is rebuilt every time one of these methods is run. -[/notice] +> [!WARNING] +> The test database is rebuilt every time one of these methods is run. Inside our test method is the `objFromFixture` method that will generate an object for us based on data from our fixture file. To identify to the object, we provide a class name and an identifier. The identifier is specified in the YAML file @@ -116,9 +114,8 @@ Just like on web requests, Silverstripe CMS caches metadata about the execution vendor/bin/phpunit app/tests/PageTest.php '' flush=1 ``` -[info] -For more information on PHPUnit's assertions see the [PHPUnit manual](https://docs.phpunit.de/en/9.6/assertions.html). -[/info] +> [!NOTE] +> For more information on PHPUnit's assertions see the [PHPUnit manual](https://docs.phpunit.de/en/9.6/assertions.html). ## Related documentation diff --git a/en/02_Developer_Guides/07_Debugging/00_Environment_Types.md b/en/02_Developer_Guides/07_Debugging/00_Environment_Types.md index becdbe130..d86f8b135 100644 --- a/en/02_Developer_Guides/07_Debugging/00_Environment_Types.md +++ b/en/02_Developer_Guides/07_Debugging/00_Environment_Types.md @@ -17,11 +17,10 @@ When developing your websites, adding page types or installing modules you shoul you will see full error back traces and view the development tools without having to be logged in as an administrator user. -[alert] -**dev mode should not be enabled long term on live sites for security reasons**. In dev mode by outputting back traces -of function calls a hacker can gain information about your environment (including passwords) so you should use dev mode -on a public server very carefully. -[/alert] +> [!CAUTION] +> **dev mode should not be enabled long term on live sites for security reasons**. In dev mode by outputting back traces +> of function calls a hacker can gain information about your environment (including passwords) so you should use dev mode +> on a public server very carefully. ## Test mode @@ -50,9 +49,8 @@ When using CGI/FastCGI with Apache, you will have to add the `RewriteRule .* - [ All error messages are suppressed from the user and the application is in it's most *secure* state. -[alert] -Live sites should always run in live mode. You should not run production websites in dev mode. -[/alert] +> [!CAUTION] +> Live sites should always run in live mode. You should not run production websites in dev mode. ## Checking environment type diff --git a/en/02_Developer_Guides/07_Debugging/01_Error_Handling.md b/en/02_Developer_Guides/07_Debugging/01_Error_Handling.md index 187c03daf..2e69766e8 100644 --- a/en/02_Developer_Guides/07_Debugging/01_Error_Handling.md +++ b/en/02_Developer_Guides/07_Debugging/01_Error_Handling.md @@ -12,10 +12,9 @@ configurations: one for development environments, and another for test or live e environments, Silverstripe CMS will deal harshly with any warnings or errors: a full call-stack is shown and execution stops for anything, giving you early warning of a potential issue to handle. -[info] -There are a range of monolog handlers available, both in the core package and in add-ons. See the -[Monolog documentation](https://github.com/Seldaek/monolog/blob/main/doc/01-usage.md) for more information. -[/info] +> [!NOTE] +> There are a range of monolog handlers available, both in the core package and in add-ons. See the +> [Monolog documentation](https://github.com/Seldaek/monolog/blob/main/doc/01-usage.md) for more information. ## Raising errors and logging diagnostic information @@ -169,6 +168,9 @@ The calls key, `MailHandler`, can be anything you like: its main purpose is to l ### Logging to a file +> [!WARNING] +> You will need to make sure the user running the PHP process has write access to the log file, wherever you choose to put it. + To log to a file, you can use Monolog's `StreamHandler`, like this: ```yml @@ -183,19 +185,14 @@ SilverStripe\Core\Injector\Injector: - "info" ``` -[warning] -The log file path must be an absolute file path, as relative paths may behave differently between CLI and HTTP requests. If you want to use a *relative* path, you can use the `SS_ERROR_LOG` environment variable to declare a file path that is relative to your project root: - -```bash -SS_ERROR_LOG="./silverstripe.log" -``` - -You don't need any of the YAML configuration above if you are using the `SS_ERROR_LOG` environment variable - but you can use a combination of the environment variable and YAML configuration if you want to configure multiple error log files. -[/warning] - -[notice] -You will need to make sure the user running the PHP process has write access to the log file, wherever you choose to put it. -[/notice] +> [!WARNING] +> The log file path must be an absolute file path, as relative paths may behave differently between CLI and HTTP requests. If you want to use a *relative* path, you can use the `SS_ERROR_LOG` environment variable to declare a file path that is relative to your project root: +> +> ```bash +> SS_ERROR_LOG="./silverstripe.log" +> ``` +> +> You don't need any of the YAML configuration above if you are using the `SS_ERROR_LOG` environment variable - but you can use a combination of the environment variable and YAML configuration if you want to configure multiple error log files. The `info` argument provides the minimum level to start logging at. @@ -280,11 +277,10 @@ SilverStripe\Core\Injector\Injector: Body: "The website server has not been able to respond to your request" ``` -[info] -In addition to Silverstripe CMS integrated logging, it is advisable to fall back to PHP's native logging functionality. A -script might terminate before it reaches the Silverstripe CMS error handling, for example in the case of a fatal error. Make -sure `log_errors` and `error_log` in your PHP ini file are configured. -[/info] +> [!NOTE] +> In addition to Silverstripe CMS integrated logging, it is advisable to fall back to PHP's native logging functionality. A +> script might terminate before it reaches the Silverstripe CMS error handling, for example in the case of a fatal error. Make +> sure `log_errors` and `error_log` in your PHP ini file are configured. ## Replacing default implementations diff --git a/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md b/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md index 557b719af..6b9f3d9c3 100644 --- a/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md +++ b/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md @@ -13,9 +13,8 @@ icon: tachometer-alt Use conditions whenever possible. The cache tag supports defining conditions via either `if` or `unless` keyword. Those are optional, however is highly recommended. -[warning] -Avoid performing heavy computations in conditionals, as they are evaluated for every template rendering. -[/warning] +> [!WARNING] +> Avoid performing heavy computations in conditionals, as they are evaluated for every template rendering. If you cache without conditions: @@ -62,57 +61,50 @@ otherwise. By using aggregates, we do that like this: %> ``` -The cache for this will update whenever a page is added, removed or edited. - -[note] -The use of the fully qualified classname is necessary. -[/note] - -[note] -The use of both `.max('LastEdited')` and `.count()` makes sure we check for any object -edited or deleted since the cache was last built. -[/note] - -[warning] -Be careful using aggregates. Remember that the database is usually one of the performance bottlenecks. -Keep in mind that every key of every cached block is recalculated for every template render, regardless of caching -result. Aggregating SQL queries are usually produce more load on the database than simple select queries, -especially if you query records by Primary Key or join tables using database indices properly. - -Sometimes it may be cheaper to not cache altogether, rather than cache a block using a bunch of heavy aggregating SQL -queries. - -Let us consider two versions: - -```ss -# Version 1 (bad) - -<% cached - $List('SilverStripe\CMS\Model\SiteTree').max('LastEdited'), - $List('SilverStripe\CMS\Model\SiteTree').count() -%> - Parent title is: $Me.Parent.Title -<% end_cached %> -``` - -```ss -# Version 2 (better performance than Version 1) - -Parent title is: $Me.Parent.Title -``` - -`Version 1` always generates two heavy aggregating SQL queries for the database on every -template render. -`Version 2` always generates a single and more performant SQL query fetching the record by its Primary Key. +> [!NOTE] +> The use of the fully qualified classname is necessary. +> +> The use of both `.max('LastEdited')` and `.count()` makes sure we check for any object +> edited or deleted since the cache was last built. -[/warning] +The cache for this will update whenever a page is added, removed or edited. -[warning] -If you use the same aggregate in a template more than once, it will be recalculated every time -unless you move it out into a separate -[controller method](../templates/partial_template_caching/#cache-key-calculated-in-controller). -[Object Caching](../templates/caching/#object-caching) only works for single variables and not for chained expressions. -[/warning] +> [!WARNING] +> Be careful using aggregates. Remember that the database is usually one of the performance bottlenecks. +> Keep in mind that every key of every cached block is recalculated for every template render, regardless of caching +> result. Aggregating SQL queries are usually produce more load on the database than simple select queries, +> especially if you query records by Primary Key or join tables using database indices properly. +> +> Sometimes it may be cheaper to not cache altogether, rather than cache a block using a bunch of heavy aggregating SQL +> queries. +> +> Let us consider two versions: +> +> ```ss +> # Version 1 (bad) +> +> <% cached +> $List('SilverStripe\CMS\Model\SiteTree').max('LastEdited'), +> $List('SilverStripe\CMS\Model\SiteTree').count() +> %> +> Parent title is: $Me.Parent.Title +> <% end_cached %> +> ``` +> +> ```ss +> # Version 2 (better performance than Version 1) +> +> Parent title is: $Me.Parent.Title +> ``` +> +> `Version 1` always generates two heavy aggregating SQL queries for the database on every +> template render. +> `Version 2` always generates a single and more performant SQL query fetching the record by its Primary Key. +> +> Note also that if you use the same aggregate in a template more than once, it will be recalculated every time +> unless you move it out into a separate +> [controller method](../templates/partial_template_caching/#cache-key-calculated-in-controller). +> [Object Caching](../templates/caching/#object-caching) only works for single variables and not for chained expressions. ## Purposely stale data @@ -163,6 +155,9 @@ All you need to do to swap the cache backend for partial template cache blocks i Here's an example of how it could be done: +> [!NOTE] +> For the below example to work it is necessary to have the Injector service `App\Cache\Service.memcached` defined somewhere in the configs. + ```yml # app/_config/cache.yml --- @@ -174,12 +169,7 @@ SilverStripe\Core\Injector\Injector: Psr\SimpleCache\CacheInterface.cacheblock: '%$App\Cache\Service.memcached' ``` -[note] -For the above example to work it is necessary to have the Injector service `App\Cache\Service.memcached` defined somewhere in the configs. -[/note] - -[warning] -The default filesystem cache backend does not support auto cleanup of the residual files with expired cache records. -If your project relies on Template Caching heavily (e.g. thousands of cache records daily), you may want to keep en eye on the -filesystem storage. Sooner or later its capacity may be exhausted. -[/warning] +> [!WARNING] +> The default filesystem cache backend does not support auto cleanup of the residual files with expired cache records. +> If your project relies on Template Caching heavily (e.g. thousands of cache records daily), you may want to keep en eye on the +> filesystem storage. Sooner or later its capacity may be exhausted. diff --git a/en/02_Developer_Guides/08_Performance/01_Caching.md b/en/02_Developer_Guides/08_Performance/01_Caching.md index 3325a8078..2c55ad956 100644 --- a/en/02_Developer_Guides/08_Performance/01_Caching.md +++ b/en/02_Developer_Guides/08_Performance/01_Caching.md @@ -49,12 +49,11 @@ SilverStripe\Core\Injector\Injector: namespace: "myCache" ``` -[alert] -Please note that if you have the `silverstripe/versioned` module installed (automatically installed by the -`silverstripe/cms` module), caches will automatically be segmented by current “stage”. This ensures that -any content written to the cache in the *draft* reading mode isn’t accidentally exposed in the *live* reading mode. -Please read the [versioned cache segmentation](#versioned-cache-segmentation) section for more information. -[/alert] +> [!CAUTION] +> Please note that if you have the `silverstripe/versioned` module installed (automatically installed by the +> `silverstripe/cms` module), caches will automatically be segmented by current “stage”. This ensures that +> any content written to the cache in the *draft* reading mode isn’t accidentally exposed in the *live* reading mode. +> Please read the [versioned cache segmentation](#versioned-cache-segmentation) section for more information. Cache objects are instantiated through a [CacheFactory](api:SilverStripe\Core\Cache\CacheFactory), which determines which cache adapter is used (see "Adapters" below for details). diff --git a/en/02_Developer_Guides/08_Performance/04_Static_Publishing.md b/en/02_Developer_Guides/08_Performance/04_Static_Publishing.md index 114206fd0..c85e33eaf 100644 --- a/en/02_Developer_Guides/08_Performance/04_Static_Publishing.md +++ b/en/02_Developer_Guides/08_Performance/04_Static_Publishing.md @@ -9,10 +9,9 @@ One of the best ways to get the top performance out of Silverstripe CMS is to by time, connecting to the database and formatting your templates. This is only appropriate approach on web pages that have completely static content. -[info] -If you want to cache part of a page, or your site has interactive elements such as forms, then -[Partial Caching](partial_caching) is more suitable. -[/info] +> [!NOTE] +> If you want to cache part of a page, or your site has interactive elements such as forms, then +> [Partial Caching](partial_caching) is more suitable. By publishing the page as HTML it's possible to run Silverstripe CMS from behind a corporate firewall, on a low performance server or serve millions of hits an hour without expensive hardware. diff --git a/en/02_Developer_Guides/08_Performance/05_Resource_Usage.md b/en/02_Developer_Guides/08_Performance/05_Resource_Usage.md index c493df40c..9dcba155e 100644 --- a/en/02_Developer_Guides/08_Performance/05_Resource_Usage.md +++ b/en/02_Developer_Guides/08_Performance/05_Resource_Usage.md @@ -12,18 +12,16 @@ Silverstripe CMS tries to keep its resource usage within the documented limits These limits are defined through `memory_limit` and `max_execution_time` in the PHP configuration. They can be overwritten through `ini_set()`. -[alert] -Most shared hosting providers will have maximum values that can't be altered. -[/alert] +> [!CAUTION] +> Most shared hosting providers will have maximum values that can't be altered. For certain tasks like synchronizing a large `assets/` folder with all file and folder entries in the database, more resources are required temporarily. In general, we recommend running resource intensive tasks through the [command line](../cli), where configuration defaults for these settings are higher or even unlimited. -[info] -Silverstripe CMS can request more resources through `Environment::increaseMemoryLimitTo()` and -`Environment::increaseTimeLimitTo()` functions. -[/info] +> [!NOTE] +> Silverstripe CMS can request more resources through `Environment::increaseMemoryLimitTo()` and +> `Environment::increaseTimeLimitTo()` functions. ```php use SilverStripe\Core\Environment; diff --git a/en/02_Developer_Guides/08_Performance/06_ORM.md b/en/02_Developer_Guides/08_Performance/06_ORM.md index d190234de..09ba16850 100644 --- a/en/02_Developer_Guides/08_Performance/06_ORM.md +++ b/en/02_Developer_Guides/08_Performance/06_ORM.md @@ -20,9 +20,8 @@ SilverStripe\Forms\TreeDropdownField: search_filter: 'StartsWith' ``` -[hint] -A very common use of `TreeDropdownField` is the "Insert Link" feature in the TinyMCE WYSIWYG. Setting this configuration to use another filter and adding an index on `Title` and `MenuTitle` for [`SiteTree`](api:SilverStripe\CMS\Model\SiteTree) can significantly improve performance. -[/hint] +> [!TIP] +> A very common use of `TreeDropdownField` is the "Insert Link" feature in the TinyMCE WYSIWYG. Setting this configuration to use another filter and adding an index on `Title` and `MenuTitle` for [`SiteTree`](api:SilverStripe\CMS\Model\SiteTree) can significantly improve performance. See [SearchFilter Modifiers](/developer_guides/model/searchfilters/) for more information about search filters. @@ -39,9 +38,8 @@ SilverStripe\ORM\Connect\DBSchemaManager: - App\Model\ModelWithManyRecords ``` -[info] -Note: The entire inheritance chain (both ancestors and descendents) of models in that configuration array will be excluded from the check and repair step. -[/info] +> [!NOTE] +> Note: The entire inheritance chain (both ancestors and descendents) of models in that configuration array will be excluded from the check and repair step. You can also disable these checks entirely: diff --git a/en/02_Developer_Guides/09_Security/00_Member.md b/en/02_Developer_Guides/09_Security/00_Member.md index 90370f3e4..e90f9adf0 100644 --- a/en/02_Developer_Guides/09_Security/00_Member.md +++ b/en/02_Developer_Guides/09_Security/00_Member.md @@ -29,10 +29,9 @@ if ($member) { ## Subclassing -[warning] -This is the least desirable way of extending the [Member](api:SilverStripe\Security\Member) class. It's better to use [DataExtension](api:SilverStripe\ORM\DataExtension) -(see below). -[/warning] +> [!WARNING] +> This is the least desirable way of extending the [Member](api:SilverStripe\Security\Member) class. It's better to use [DataExtension](api:SilverStripe\ORM\DataExtension) +> (see below). You can define subclasses of [Member](api:SilverStripe\Security\Member) to add extra fields or functionality to the built-in membership system. diff --git a/en/02_Developer_Guides/09_Security/04_Sudo_Mode.md b/en/02_Developer_Guides/09_Security/04_Sudo_Mode.md index 5dccf1453..e307a9db1 100644 --- a/en/02_Developer_Guides/09_Security/04_Sudo_Mode.md +++ b/en/02_Developer_Guides/09_Security/04_Sudo_Mode.md @@ -66,9 +66,8 @@ be applied to React components in your module or code to intercept component ren information and log in screen, which will validate, activate sudo mode, and re-render the wrapped component afterwards on success. -[notice] -The `WithSudoMode` HOC is exposed via [Webpack's expose-loader plugin](https://webpack.js.org/loaders/expose-loader/). You will need to add it as a [webpack external](https://webpack.js.org/configuration/externals/) to use it. The recommended way to do this is via the [@silverstripe/webpack-config npm package](https://www.npmjs.com/package/@silverstripe/webpack-config) which handles all the external configuration for you. -[/notice] +> [!WARNING] +> The `WithSudoMode` HOC is exposed via [Webpack's expose-loader plugin](https://webpack.js.org/loaders/expose-loader/). You will need to add it as a [webpack external](https://webpack.js.org/configuration/externals/) to use it. The recommended way to do this is via the [@silverstripe/webpack-config npm package](https://www.npmjs.com/package/@silverstripe/webpack-config) which handles all the external configuration for you. You can get the injector to apply the HOC to your component automatically using [injector transformations](/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/#transforming-services-using-middleware): diff --git a/en/02_Developer_Guides/09_Security/05_Secure_Coding.md b/en/02_Developer_Guides/09_Security/05_Secure_Coding.md index 9da685489..35c91b951 100644 --- a/en/02_Developer_Guides/09_Security/05_Secure_Coding.md +++ b/en/02_Developer_Guides/09_Security/05_Secure_Coding.md @@ -104,10 +104,9 @@ $members = Member::get()->where(['"Name" = ?' => $_GET['name']]); $members = Member::get()->where(sprintf('"Name" = %s', Convert::raw2sql($_GET['name'], true))); ``` -[warning] -It is NOT good practice to "be sure" and convert the data passed to the functions above manually. This might -result in *double escaping* and alters the actually saved data (e.g. by adding slashes to your content). -[/warning] +> [!WARNING] +> It is NOT good practice to "be sure" and convert the data passed to the functions above manually. This might +> result in *double escaping* and alters the actually saved data (e.g. by adding slashes to your content). ### Manual escaping @@ -215,10 +214,9 @@ XSS (Cross-Site-Scripting). With some basic guidelines, you can ensure your outp displaying a blog post in HTML from a trusted author, or escaping a search parameter from an untrusted visitor before redisplaying it). -[notice] -Note: Silverstripe CMS templates do not remove tags, please use [strip_tags()](https://php.net/strip_tags) for this purpose -or [sanitize](https://htmlpurifier.org/) it correctly. -[/notice] +> [!WARNING] +> Note: Silverstripe CMS templates do not remove tags, please use [strip_tags()](https://php.net/strip_tags) for this purpose +> or [sanitize](https://htmlpurifier.org/) it correctly. See the [OWASP article on XSS](https://owasp.org/www-community/attacks/xss/) for more information. @@ -343,10 +341,9 @@ class MyObject extends DataObject } ``` -[info] -If `$title` was a public property called `$Title`, it would also be casted the same way that the result of -`$getTitle()` is casted. -[/info] +> [!NOTE] +> If `$title` was a public property called `$Title`, it would also be casted the same way that the result of +> `$getTitle()` is casted. Template: @@ -369,23 +366,21 @@ template, you'll need to take care of casting and escaping yourself in PHP. The [Convert](api:SilverStripe\Core\Convert) class has utilities for this, mainly *Convert::raw2xml()* and *Convert::raw2att()* (which is also used by *XML* and *ATT* in template code). -[warning] -Most of the `Convert::raw2` methods accept arrays and do not affect array keys. -If you serialize your data, make sure to do that before you pass it to `Convert::raw2` methods. - -For example: - -```php -use SilverStripe\Core\Convert; - -// WRONG! -json_encode(Convert::raw2sql($request->getVar('multiselect'))); - -// Correct! -Convert::raw2sql(json_encode($request->getVar('multiselect'))); -``` - -[/warning] +> [!WARNING] +> Most of the `Convert::raw2` methods accept arrays and do not affect array keys. +> If you serialize your data, make sure to do that before you pass it to `Convert::raw2` methods. +> +> For example: +> +> ```php +> use SilverStripe\Core\Convert; +> +> // WRONG! +> json_encode(Convert::raw2sql($request->getVar('multiselect'))); +> +> // Correct! +> Convert::raw2sql(json_encode($request->getVar('multiselect'))); +> ``` PHP: @@ -801,10 +796,9 @@ SilverStripe\Core\Injector\Injector: TokenCookieSecure: true ``` -[info] -There is not currently an easy way to pass a `samesite` attribute value for setting this cookie - but you can set the -default value for the attribute for all cookies. See [the main cookies documentation](/developer_guides/cookies_and_sessions/cookies#samesite-attribute) for more information. -[/info] +> [!NOTE] +> There is not currently an easy way to pass a `samesite` attribute value for setting this cookie - but you can set the +> default value for the attribute for all cookies. See [the main cookies documentation](/developer_guides/cookies_and_sessions/cookies#samesite-attribute) for more information. For other cookies set by your application we should also ensure the users are provided with secure cookies by setting the "Secure" and "HTTPOnly" flags. These flags prevent them from being stolen by an attacker through JavaScript. diff --git a/en/02_Developer_Guides/10_Email/index.md b/en/02_Developer_Guides/10_Email/index.md index 6df181f85..b4fecd35b 100644 --- a/en/02_Developer_Guides/10_Email/index.md +++ b/en/02_Developer_Guides/10_Email/index.md @@ -18,9 +18,8 @@ The `Sendmail` transport is the most common one and is used by default in Silver Alternatively you can provide a different `DSN` to select any of the Transport classes provided natively by `symfony/mailer` or other compatible third-party transports. For more information and to see what other transports are available see the [symfony/mailer transport types](https://symfony.com/doc/current/mailer.html#using-a-3rd-party-transport). -[hint] -The format for the DSN is exactly as defined in the symfony docs linked above. Some common examples are listed below. -[/hint] +> [!TIP] +> The format for the DSN is exactly as defined in the symfony docs linked above. Some common examples are listed below. To set the DSN string in an environment variable (recommended): @@ -116,17 +115,19 @@ $email->text('My plain text email content'); $email->send(); ``` -[info] -The default HTML template for emails is `vendor/silverstripe/framework/templates/SilverStripe/Control/Email/Email.ss`. -To customise this template, first copy it to `/themes//SilverStripe/Control/Email/Email.ss`. Alternatively, copy it to a different location and use `setHTMLTemplate` when you create the -`Email` instance. Note - by default the `$EmailContent` variable will escape HTML tags for security reasons. If you feel confident allowing this variable to be rendered as HTML, then update your custom email template to `$EmailContent.RAW` -[/info] +> [!NOTE] +> The default HTML template for emails is `vendor/silverstripe/framework/templates/SilverStripe/Control/Email/Email.ss`. +> To customise this template, first copy it to `/themes//SilverStripe/Control/Email/Email.ss`. Alternatively, copy it to a different location and use `setHTMLTemplate` when you create the +> `Email` instance. Note - by default the `$EmailContent` variable will escape HTML tags for security reasons. If you feel confident allowing this variable to be rendered as HTML, then update your custom email template to `$EmailContent.RAW` ### Templates HTML emails can use custom templates using the same template language as your website template. You can also pass the email object additional information using the `setData` and `addData` methods. +> [!NOTE] +> Calling `setData()` or `addData()` once or more will cause the email to be rendered using Silverstripe templates. This will override any email content set directly via methods such as `setBody()`, `html()`, or `text()`. + ```ss <%-- app/templates/Email/MyCustomEmail.ss --%>

Hi $Member.FirstName

@@ -152,14 +153,9 @@ $email = Email::create() $email->send(); ``` -[info] -Calling `setData()` or `addData()` once or more will cause the email to be rendered using Silverstripe templates. This will override any email content set directly via methods such as `setBody()`, `html()`, or `text()`. -[/info] - -[alert] -As we've added a new template file (`MyCustomEmail`) make sure you clear the Silverstripe CMS cache for your changes to -take affect. -[/alert] +> [!CAUTION] +> As we've added a new template file (`MyCustomEmail`) make sure you clear the Silverstripe CMS cache for your changes to +> take affect. #### Custom plain templates @@ -209,10 +205,9 @@ $to = [ $email = Email::create($from, $to, $subject, $body); ``` -[alert] -Remember, setting a `from` address that doesn't come from your domain (such as the users email) will likely see your -email marked as spam. If you want to send from another address think about using the `setReplyTo` method. -[/alert] +> [!CAUTION] +> Remember, setting a `from` address that doesn't come from your domain (such as the users email) will likely see your +> email marked as spam. If you want to send from another address think about using the `setReplyTo` method. You will also have to remove the `SS_SEND_ALL_EMAILS_FROM` environment variable if it is present. @@ -262,10 +257,9 @@ $email->replyTo('reply@example.com'); If you wish to handle email send failures then you can wrap `$email->send()` with a try/catch block that catches the Symfony Mailer `TransportExceptionInterface`. -[hint] -You might get a Symfony Mailer `RfcComplianceException` when instantiating the `Email` object if the email address you're trying to send to or from is invalid. -In some cases you'll want to catch and handle that exception as well. -[/hint] +> [!TIP] +> You might get a Symfony Mailer `RfcComplianceException` when instantiating the `Email` object if the email address you're trying to send to or from is invalid. +> In some cases you'll want to catch and handle that exception as well. ```php use SilverStripe\Control\Email\Email; diff --git a/en/02_Developer_Guides/11_Integration/00_CSV_Import.md b/en/02_Developer_Guides/11_Integration/00_CSV_Import.md index 45b0d5ca6..7a91280cf 100644 --- a/en/02_Developer_Guides/11_Integration/00_CSV_Import.md +++ b/en/02_Developer_Guides/11_Integration/00_CSV_Import.md @@ -91,9 +91,8 @@ Let's create a simple upload form (which is used for `MyDataObject` instances). You'll need to add a route to your controller to make it accessible via URL (see [Routing](../../controllers/routing/)). -[warning] -Don't forget to perform [permission checks](#permission-checks) if the data is provided by users. -[/warning] +> [!WARNING] +> Don't forget to perform [permission checks](#permission-checks) if the data is provided by users. ```php namespace App\Control; diff --git a/en/02_Developer_Guides/11_Integration/02_RSSFeed.md b/en/02_Developer_Guides/11_Integration/02_RSSFeed.md index 0426ec105..164c8ae5c 100644 --- a/en/02_Developer_Guides/11_Integration/02_RSSFeed.md +++ b/en/02_Developer_Guides/11_Integration/02_RSSFeed.md @@ -13,10 +13,9 @@ your current staff members, comments or any other custom [DataObject](api:Silver logical limitation here is that every item in the RSS-feed should be accessible through a URL on your website, so it's advisable to just create feeds from subclasses of [SiteTree](api:SilverStripe\CMS\Model\SiteTree). -[warning] -If you wish to generate an RSS feed that contains a [DataObject](api:SilverStripe\ORM\DataObject), ensure you define a `AbsoluteLink` method on -the object. -[/warning] +> [!WARNING] +> If you wish to generate an RSS feed that contains a [DataObject](api:SilverStripe\ORM\DataObject), ensure you define a `AbsoluteLink` method on +> the object. ## Usage @@ -101,9 +100,8 @@ class HomePageController extends PageController DataObjects can be rendered in the feed as well, however, since they aren't explicitly [SiteTree](api:SilverStripe\CMS\Model\SiteTree) subclasses we need to include a function `AbsoluteLink` to allow the RSS feed to link through to the item. -[info] -If the items are all displayed on a single page you may simply hard code the link to point to a particular page. -[/info] +> [!NOTE] +> If the items are all displayed on a single page you may simply hard code the link to point to a particular page. Take an example, we want to create an RSS feed of all the `Players` objects in our site. We make sure the `AbsoluteLink` method is defined and returns a string to the full website URL. @@ -217,9 +215,8 @@ class HomePage extends Page } ``` -[warning] -As we've added a new template (PlayersRss.ss) make sure you clear your Silverstripe CMS cache. -[/warning] +> [!WARNING] +> As we've added a new template (PlayersRss.ss) make sure you clear your Silverstripe CMS cache. ## API documentation diff --git a/en/02_Developer_Guides/11_Integration/How_Tos/Import_CSV_through_a_Controller.md b/en/02_Developer_Guides/11_Integration/How_Tos/Import_CSV_through_a_Controller.md index 64e6a740b..0a3c16323 100644 --- a/en/02_Developer_Guides/11_Integration/How_Tos/Import_CSV_through_a_Controller.md +++ b/en/02_Developer_Guides/11_Integration/How_Tos/Import_CSV_through_a_Controller.md @@ -81,6 +81,5 @@ class MyController extends Controller } ``` -[alert] -If the file you're loading data from is uploaded by a user, you should pass `true` to the [`CsvBulkLoader::setCheckPermissions()`](api:SilverStripe\Dev\CsvBulkLoader::setCheckPermissions()) method. Otherwise, permissions will not be respected and the user may alter data in ways they would otherwise not be permitted to. -[/alert] +> [!CAUTION] +> If the file you're loading data from is uploaded by a user, you should pass `true` to the [`CsvBulkLoader::setCheckPermissions()`](api:SilverStripe\Dev\CsvBulkLoader::setCheckPermissions()) method. Otherwise, permissions will not be respected and the user may alter data in ways they would otherwise not be permitted to. diff --git a/en/02_Developer_Guides/12_Search/01_Searchcontext.md b/en/02_Developer_Guides/12_Search/01_Searchcontext.md index 062727845..74e3289b3 100644 --- a/en/02_Developer_Guides/12_Search/01_Searchcontext.md +++ b/en/02_Developer_Guides/12_Search/01_Searchcontext.md @@ -13,9 +13,8 @@ search parameters and an object class it acts on. The default output of a [SearchContext](api:SilverStripe\ORM\Search\SearchContext) is either a [SQLSelect](api:SilverStripe\ORM\Queries\SQLSelect) object for further refinement, or a [DataObject](api:SilverStripe\ORM\DataObject) instance. -[notice] -[SearchContext](api:SilverStripe\ORM\Search\SearchContext) is mainly used by [ModelAdmin](/developer_guides/customising_the_admin_interface/modeladmin), as it powers the [`DataObject::$searchable_fields` configuration](/developer_guides/model/scaffolding#searchable-fields). -[/notice] +> [!WARNING] +> [SearchContext](api:SilverStripe\ORM\Search\SearchContext) is mainly used by [ModelAdmin](/developer_guides/customising_the_admin_interface/modeladmin), as it powers the [`DataObject::$searchable_fields` configuration](/developer_guides/model/scaffolding#searchable-fields). ## Usage @@ -77,15 +76,12 @@ class MyDataObject extends DataObject } ``` -[notice] +> [!WARNING] +> In case you need multiple contexts, consider name-spacing your request parameters by using `FieldList->namespace()` on +> the `$fields` constructor parameter. + See the [SearchFilter](../model/searchfilters) documentation for more information about filters to use such as the `GreaterThanFilter`. -[/notice] - -[notice] -In case you need multiple contexts, consider name-spacing your request parameters by using `FieldList->namespace()` on -the `$fields` constructor parameter. -[/notice] ### Customising the general search field diff --git a/en/02_Developer_Guides/12_Search/02_FulltextSearch.md b/en/02_Developer_Guides/12_Search/02_FulltextSearch.md index 63256995e..aa33293e8 100644 --- a/en/02_Developer_Guides/12_Search/02_FulltextSearch.md +++ b/en/02_Developer_Guides/12_Search/02_FulltextSearch.md @@ -10,11 +10,10 @@ Fulltext search allows advanced search criteria for searching words within a tex Fulltext search can be achieved using the built-in [MySQLDatabase](api:SilverStripe\ORM\Connect\MySQLDatabase) class a more powerful wrapper for Fulltext search is provided through a module. -[notice] -See the [FulltextSearch Module](https://github.com/silverstripe-labs/silverstripe-fulltextsearch/). This module provides -a high level wrapper for running advanced search services such as Solr, Lucene or Sphinx in the backend rather than -`MySQL` search. -[/notice] +> [!WARNING] +> See the [FulltextSearch Module](https://github.com/silverstripe-labs/silverstripe-fulltextsearch/). This module provides +> a high level wrapper for running advanced search services such as Solr, Lucene or Sphinx in the backend rather than +> `MySQL` search. ## Adding fulltext support to `MySQLDatabase` @@ -40,11 +39,10 @@ class MyDataObject extends DataObject The [FulltextSearchable](api:SilverStripe\ORM\Search\FulltextSearchable) extension will add the correct `Fulltext` indexes to the data model. -[alert] -The [SearchForm](api:SilverStripe\CMS\Search\SearchForm) and [FulltextSearchable](api:SilverStripe\ORM\Search\FulltextSearchable) API's are currently hard coded to be specific to `Page` and `File` -records and cannot easily be adapted to include custom `DataObject` instances. To include your custom objects in the -default site search, have a look at those extensions and modify as required. -[/alert] +> [!CAUTION] +> The [SearchForm](api:SilverStripe\CMS\Search\SearchForm) and [FulltextSearchable](api:SilverStripe\ORM\Search\FulltextSearchable) API's are currently hard coded to be specific to `Page` and `File` +> records and cannot easily be adapted to include custom `DataObject` instances. To include your custom objects in the +> default site search, have a look at those extensions and modify as required. ### Fulltext filter diff --git a/en/02_Developer_Guides/13_i18n/index.md b/en/02_Developer_Guides/13_i18n/index.md index 3a8d8ca0b..24da44f0f 100644 --- a/en/02_Developer_Guides/13_i18n/index.md +++ b/en/02_Developer_Guides/13_i18n/index.md @@ -305,23 +305,21 @@ If you want to run the text collector for just one module you can use the 'modul You can also run the text collector against multiple specific modules by separating the module names with a comma: `https://www.example.com/dev/tasks/i18nTextCollectorTask/?module=silverstripe%2Fcms,silverstripe%2Fframework` -[info] -The `%2F` in `silverstripe%2Fcms` is a `/` which has been encoded for use in a URL in a non-ambiguous way. -[/info] +> [!NOTE] +> The `%2F` in `silverstripe%2Fcms` is a `/` which has been encoded for use in a URL in a non-ambiguous way. The text collector also collects text for themes - if you want to run text collection on a specific theme, reference the theme with prefix `themes:`, e.g: `https://www.example.com/dev/tasks/i18nTextCollectorTask/?module=themes:my-theme` -[hint] -You can also run this task via the command line using sake, e.g: - -```bash -sake dev/tasks/i18nTextCollectorTask module=themes:my-theme,silverstripe/framework -``` - -See [the sake documentation](/developer_guides/cli/) for details about using sake. -[/hint] +> [!TIP] +> You can also run this task via the command line using sake, e.g: +> +> ```bash +> sake dev/tasks/i18nTextCollectorTask module=themes:my-theme,silverstripe/framework +> ``` +> +> See [the sake documentation](/developer_guides/cli/) for details about using sake. ## Module priority diff --git a/en/02_Developer_Guides/14_Files/03_File_Security.md b/en/02_Developer_Guides/14_Files/03_File_Security.md index 065f77d18..e55491f66 100644 --- a/en/02_Developer_Guides/14_Files/03_File_Security.md +++ b/en/02_Developer_Guides/14_Files/03_File_Security.md @@ -77,12 +77,11 @@ Most commonly this is through the "Access to Files section" permission. Custom implementations (e.g. APIs or custom file viewers) can have further restrictions in your project. -[warning] -When implementing your own `canView()` logic through [extensions](/developer_guides/extending/extensions), -existing unprotected files are not retroactively moved to the protected asset store. -While those new permissions are honoured in the CMS, protected files through custom `canView()` -can still be downloaded through a public URL until a `write()` operation is triggered on them. -[/warning] +> [!WARNING] +> When implementing your own `canView()` logic through [extensions](/developer_guides/extending/extensions), +> existing unprotected files are not retroactively moved to the protected asset store. +> While those new permissions are honoured in the CMS, protected files through custom `canView()` +> can still be downloaded through a public URL until a `write()` operation is triggered on them. ## Asset stores @@ -222,10 +221,9 @@ $object->SecretFile->protectFile(); $object->PublicFile->publishFile(); ``` -[notice] -One thing to note is that all variants of a single file will be treated as -a single entity for access control, so specific variants cannot be individually controlled. -[/notice] +> [!WARNING] +> One thing to note is that all variants of a single file will be treated as +> a single entity for access control, so specific variants cannot be individually controlled. ## How file access is protected diff --git a/en/02_Developer_Guides/14_Files/06_Allowed_file_types.md b/en/02_Developer_Guides/14_Files/06_Allowed_file_types.md index 214455488..b0060d5d6 100644 --- a/en/02_Developer_Guides/14_Files/06_Allowed_file_types.md +++ b/en/02_Developer_Guides/14_Files/06_Allowed_file_types.md @@ -32,9 +32,8 @@ Any file not included in this config, or in the default list of extensions, will any requests to the assets directory. Invalid files will be blocked regardless of whether they exist or not, and will not invoke any PHP processes. -[warning] -While SVG images are a popular format to display images on the web, they are not included in the file extension whitelist because they can contain arbitrary scripts that will be executed when the image is rendered in a browser. Allowing CMS users to upload SVG images would be a significant XSS risk. We strongly advise developers against whitelisting the `svg` file extension. -[/warning] +> [!WARNING] +> While SVG images are a popular format to display images on the web, they are not included in the file extension whitelist because they can contain arbitrary scripts that will be executed when the image is rendered in a browser. Allowing CMS users to upload SVG images would be a significant XSS risk. We strongly advise developers against whitelisting the `svg` file extension. You can also remove pre-existing entries from the whitelist by setting them to `false`. diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/01_ModelAdmin.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/01_ModelAdmin.md index 895e1cbfa..1c1c80393 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/01_ModelAdmin.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/01_ModelAdmin.md @@ -11,10 +11,9 @@ searchables list and edit views of [DataObject](api:SilverStripe\ORM\DataObject) It uses the framework's knowledge about the model to provide sensible defaults, allowing you to get started in a couple of lines of code, while still providing a solid base for customization. -[info] -The interface is mainly powered by the [GridField](api:SilverStripe\Forms\GridField\GridField) class ([documentation](../forms/field_types/gridfield)), which can -also be used in other areas of your application. -[/info] +> [!NOTE] +> The interface is mainly powered by the [GridField](api:SilverStripe\Forms\GridField\GridField) class ([documentation](../forms/field_types/gridfield)), which can +> also be used in other areas of your application. Let's assume we want to manage a simple product listing as a sample data model: A product can have a name, price, and a category. @@ -86,9 +85,8 @@ class MyAdmin extends ModelAdmin This will automatically add a new menu entry to the Silverstripe CMS UI entitled `My Product Admin` and logged in users will be able to upload and manage `Product` and `Category` instances through `https://www.example.com/admin/products`. -[alert] -After defining these classes, make sure you have rebuilt your Silverstripe CMS database and flushed your cache. -[/alert] +> [!CAUTION] +> After defining these classes, make sure you have rebuilt your Silverstripe CMS database and flushed your cache. ## Defining the `ModelAdmin` models @@ -146,6 +144,12 @@ class MyAdmin extends ModelAdmin It is trivial to get links to the edit form for managed records. +> [!NOTE] +> The [getLinkForModelClass()](api:SilverStripe\Admin\ModelAdmin::getLinkForModelClass()) method returns a link +> for the first tab defined for that class. If you have multiple tabs for a given class (as in the example above) +> it is better to use [getLinkForModelTab()](api:SilverStripe\Admin\ModelAdmin::getLinkForModelTab()) which will +> give you a link for the specific tab you pass in. + ```php $admin = MyAdmin::singleton(); if ($admin->isManagedModel(Product::class)) { @@ -158,18 +162,10 @@ if ($admin->isManagedModel(Product::class)) { $tabLink = $admin->getLinkForModelTab('product-category'); ``` -[info] -The [getLinkForModelClass()](api:SilverStripe\Admin\ModelAdmin::getLinkForModelClass()) method returns a link -for the first tab defined for that class. If you have multiple tabs for a given class (as in the example above) -it is better to use [getLinkForModelTab()](api:SilverStripe\Admin\ModelAdmin::getLinkForModelTab()) which will -give you a link for the specific tab you pass in. -[/info] - -[hint] -If you want `getLinkForModelClass()` to return the link for a specific tab, you can override the -[getModelTabForModelClass()](api:SilverStripe\Admin\ModelAdmin::getModelTabForModelClass()) method -for your `ModelAdmin` subclass. -[/hint] +> [!TIP] +> If you want `getLinkForModelClass()` to return the link for a specific tab, you can override the +> [getModelTabForModelClass()](api:SilverStripe\Admin\ModelAdmin::getModelTabForModelClass()) method +> for your `ModelAdmin` subclass. You can also use the new [CMSEditLinkExtension](api:SilverStripe\Admin\CMSEditLinkExtension) to provide a `CMSEditLink()` method on the record - see [Managing Records](../model/managing_records#getting-an-edit-link). @@ -179,9 +175,8 @@ Each new `ModelAdmin` subclass creates its' own [permission code](../security), `CMS_ACCESS_MyAdmin`. Users with access to the Admin UI will need to have this permission assigned through `admin/security/` or have the `ADMIN` permission code in order to gain access to the controller. -[notice] -For more information on the security and permission system see the [Security Documentation](../security) -[/notice] +> [!WARNING] +> For more information on the security and permission system see the [Security Documentation](../security) The [DataObject](api:SilverStripe\ORM\DataObject) API has more granular permission control, which is enforced in [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) by default. Available checks are `canEdit()`, `canCreate()`, `canView()` and `canDelete()`. Models check for administrator @@ -265,9 +260,8 @@ class Product extends DataObject } ``` -[hint] -[SearchContext](../search/searchcontext) documentation has more information on providing the search functionality. -[/hint] +> [!TIP] +> [SearchContext](../search/searchcontext) documentation has more information on providing the search functionality. ## Displaying results diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md index fa4f05631..6bef283cb 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md @@ -77,17 +77,16 @@ SilverStripe\Admin\AdminRootController: When extending the CMS or creating modules, you can take advantage of various functions that will return the configured admin URL (by default 'admin' is returned): -[warning] -Depending on your configuration, the returned value *may or may not* include a trailing slash. The default is to not include one, but you should take care to not -explicitly expect one scenario or the other. - -In PHP you can use [Controller::join_links()](api:SilverStripe\Control\Controller::join_links()) or pass an argument to -[AdminRootController::admin_url()](api:SilverStripe\Admin\AdminRootController::admin_url()) to ensure only one `/` character separates the admin URL from the rest of -your path. - -In JavaScript, if you are using [@silverstripe/webpack-config](https://www.npmjs.com/package/@silverstripe/webpack-config), you can use the `joinUrlPaths()` utility -function. -[/warning] +> [!WARNING] +> Depending on your configuration, the returned value *may or may not* include a trailing slash. The default is to not include one, but you should take care to not +> explicitly expect one scenario or the other. +> +> In PHP you can use [Controller::join_links()](api:SilverStripe\Control\Controller::join_links()) or pass an argument to +> [AdminRootController::admin_url()](api:SilverStripe\Admin\AdminRootController::admin_url()) to ensure only one `/` character separates the admin URL from the rest of +> your path. +> +> In JavaScript, if you are using [@silverstripe/webpack-config](https://www.npmjs.com/package/@silverstripe/webpack-config), you can use the `joinUrlPaths()` utility +> function. In PHP you should use: @@ -234,11 +233,10 @@ correctly configured form. ## JavaScript through jQuery.Entwine -[notice] -The following documentation regarding Entwine does not apply to React components or sections powered by React. -If you're developing new functionality in React powered sections please refer to -[React, Redux, and GraphQL](/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/). -[/notice] +> [!WARNING] +> The following documentation regarding Entwine does not apply to React components or sections powered by React. +> If you're developing new functionality in React powered sections please refer to +> [React, Redux, and GraphQL](/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/). jQuery.entwine is a library which allows us to attach behaviour to DOM elements in a flexible and structured manner. diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/03_CMS_Layout.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/03_CMS_Layout.md index 6677c0c32..c60c9c7d2 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/03_CMS_Layout.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/03_CMS_Layout.md @@ -5,11 +5,10 @@ summary: Add interactivity enhancements to the admin with Javascript # CMS layout -[notice] -The following documentation regarding JavaScript layouts does not apply to React components or sections powered by React. -If you're developing new functionality in React powered sections please refer to -[ReactJS in Silverstripe CMS](./how_tos/extend_cms_interface/#react-rendered-ui). -[/notice] +> [!WARNING] +> The following documentation regarding JavaScript layouts does not apply to React components or sections powered by React. +> If you're developing new functionality in React powered sections please refer to +> [ReactJS in Silverstripe CMS](./how_tos/extend_cms_interface/#react-rendered-ui). The CMS markup is structured into "panels", which are the base units containing interface components (or other panels), as declared by the class `cms-panel`. Some panels can be made collapsible. @@ -35,17 +34,16 @@ This causes the framework to: to the layout manager) - trigger `redraw` on children which also cascades deeper into the hierarchy (this is framework activity) -[notice] -Caveat: `layout` is also triggered when a DOM element is replaced with AJAX in `LeftAndMain::handleAjaxResponse`. In -this case it is triggered on the parent of the element being replaced. -Calling the top level `layout` is not enough as it will wrongly descend down the detached element's hierarchy. -[/notice] - -[notice] -Caveat: invocation order of the `redraws` is crucial here, generally going from innermost to outermost elements. For -example, the tab panels have be applied in the CMS form before the form itself is layouted with its sibling panels to -avoid incorrect dimensions. -[/notice] +> [!WARNING] +> There are some caveats to this: +> +> `layout` is also triggered when a DOM element is replaced with AJAX in `LeftAndMain::handleAjaxResponse`. In +> this case it is triggered on the parent of the element being replaced. +> Calling the top level `layout` is not enough as it will wrongly descend down the detached element's hierarchy. +> +> The invocation order of the `redraws` is crucial here, generally going from innermost to outermost elements. For +> example, the tab panels have be applied in the CMS form before the form itself is layouted with its sibling panels to +> avoid incorrect dimensions. ![Layout variations](../../_images/cms-architecture.png) diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/04_Preview.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/04_Preview.md index d270ae3dc..631230965 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/04_Preview.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/04_Preview.md @@ -62,38 +62,36 @@ nesting `GridField`s. For the below examples it is assumed you aren't using nest `GridField`s - though [CMSEditLinkExtension](/developer_guides/model/managing_records#getting-an-edit-link) will handle those situations for you if you use it. -[hint] -The easiest way to implement `CMSEditLink()` is by -[using CMSEditLinkExtension](/developer_guides/model/managing_records#getting-an-edit-link). -But for completeness, the other examples below show alternative implementations. - -```php -namespace App\Model; - -use App\Admin\MyModelAdmin; -use SilverStripe\Admin\CMSEditLinkExtension; -use SilverStripe\ORM\CMSPreviewable; -use SilverStripe\ORM\DataObject; - -class MyParentModel extends DataObject implements CMSPreviewable -{ - private static string $cms_edit_owner = MyModelAdmin::class; - - private static $extensions = [ - CMSEditLinkExtension::class, - ]; - - public function CMSEditLink() - { - // Get the value returned by the extension - return $this->extend('CMSEditLink')[0]; - } - - // ... -} -``` - -[/hint] +> [!TIP] +> The easiest way to implement `CMSEditLink()` is by +> [using CMSEditLinkExtension](/developer_guides/model/managing_records#getting-an-edit-link). +> But for completeness, the other examples below show alternative implementations. +> +> ```php +> namespace App\Model; +> +> use App\Admin\MyModelAdmin; +> use SilverStripe\Admin\CMSEditLinkExtension; +> use SilverStripe\ORM\CMSPreviewable; +> use SilverStripe\ORM\DataObject; +> +> class MyParentModel extends DataObject implements CMSPreviewable +> { +> private static string $cms_edit_owner = MyModelAdmin::class; +> +> private static $extensions = [ +> CMSEditLinkExtension::class, +> ]; +> +> public function CMSEditLink() +> { +> // Get the value returned by the extension +> return $this->extend('CMSEditLink')[0]; +> } +> +> // ... +> } +> ``` #### `GetMimeType` @@ -214,9 +212,8 @@ Note that if you had set up this model to [act like a page](https://www.silverst you could simply `return $this->Link($action)`. In that case the new action would not need to be added to your `ModelAdmin`. -[warning] -Note: The `if (!$this->isInDB())` check below is important! Without this, the preview panel will redirect you to a 404 page when creating a new object. -[/warning] +> [!WARNING] +> Note: The `if (!$this->isInDB())` check below is important! Without this, the preview panel will redirect you to a 404 page when creating a new object. `ModelAdmin` provides methods for generating a link for the correct model: @@ -274,10 +271,9 @@ class Product extends DataObject implements CMSPreviewable } ``` -[hint] -Remember, if you're implementing this [in an extension](/developer_guides/extending/extensions/), you'll -need to replace any `$this->` with `$this->owner->` to get the values from the actual record. -[/hint] +> [!TIP] +> Remember, if you're implementing this [in an extension](/developer_guides/extending/extensions/), you'll +> need to replace any `$this->` with `$this->owner->` to get the values from the actual record. Let's assume when you display this object on the front end you're just looping through a list of items and indirectly calling `forTemplate` using the [`$Me` template variable](../templates/syntax#fortemplate). @@ -314,6 +310,12 @@ Because this is a public method called on a `ModelAdmin`, which will often be ex in a back-end context using admin themes, it pays to ensure we're loading the front-end themes whilst rendering out the preview content. +> [!TIP] +> If the `ModelAdmin` you want to do this on is in some vendor module, you can apply +> this action in an extension as well! Just remember to use the public methods where +> protected properties are used below (e.g. `$this->urlParams['ID']` would become +> `$this->owner->getUrlParams()['ID']`). + ```php namespace App\Admin; @@ -367,65 +369,56 @@ class MyAdmin extends ModelAdmin } ``` -[hint] -If the `ModelAdmin` you want to do this on is in some vendor module, you can apply -this action in an extension as well! Just remember to use the public methods where -protected properties are used above (e.g. `$this->urlParams['ID']` would become -`$this->owner->getUrlParams()['ID']`). -[/hint] - -[hint] -If the CSS or JS you have added via [the Requirements API](/developer_guides/templates/requirements/#php-requirements-api) -aren't coming through, you may need to add `` and `` tags to the markup. It may not be appropriate to do this in -your main template (you don't want two `` tags on a page that includes the template), so you might need a preview wrapper -template, like so: - -```ss -<%-- themes/mytheme/templates/PreviewBase.ss --%> - - -<%-- head tag is needed for css to be injected --%> - -<%-- body tag is needed for javascript to be injected --%> - - <%-- these two divs are just here to comply with styling from the simple theme, replace them with your own theme markup --%> -
- $Preview -
- - -``` - -```php -// app/src/Admin/MyAdmin.php -namespace App\Admin; - -use SilverStripe\Admin\ModelAdmin; -use SilverStripe\View\ArrayData; -use SilverStripe\View\Requirements; -use SilverStripe\View\SSViewer; - -class MyAdmin extends ModelAdmin -{ - // ... - - public function cmsPreview() - { - // ... ommitted for brevity - - // Add in global css/js that would normally be added in the page base template (as needed) - Requirements::themedCSS('client/dist/css/style.css'); - // Render the preview content - $preview = $obj->forTemplate(); - // Wrap preview in proper html, body, etc so Requirements are used - $preview = SSViewer::create('PreviewBase')->process(ArrayData::create(['Preview' => $preview])); - - // ... ommitted for brevity - } -} -``` - -[/hint] +> [!TIP] +> If the CSS or JS you have added via [the Requirements API](/developer_guides/templates/requirements/#php-requirements-api) +> aren't coming through, you may need to add `` and `` tags to the markup. It may not be appropriate to do this in +> your main template (you don't want two `` tags on a page that includes the template), so you might need a preview wrapper +> template, like so: +> +> ```ss +> <%-- themes/mytheme/templates/PreviewBase.ss --%> +> +> +> <%-- head tag is needed for css to be injected --%> +> +> <%-- body tag is needed for javascript to be injected --%> +> +> <%-- these two divs are just here to comply with styling from the simple theme, replace them with your own theme markup --%> +>
+> $Preview +>
+> +> +> ``` +> +> ```php +> // app/src/Admin/MyAdmin.php +> namespace App\Admin; +> +> use SilverStripe\Admin\ModelAdmin; +> use SilverStripe\View\ArrayData; +> use SilverStripe\View\Requirements; +> use SilverStripe\View\SSViewer; +> +> class MyAdmin extends ModelAdmin +> { +> // ... +> +> public function cmsPreview() +> { +> // ... ommitted for brevity +> +> // Add in global css/js that would normally be added in the page base template (as needed) +> Requirements::themedCSS('client/dist/css/style.css'); +> // Render the preview content +> $preview = $obj->forTemplate(); +> // Wrap preview in proper html, body, etc so Requirements are used +> $preview = SSViewer::create('PreviewBase')->process(ArrayData::create(['Preview' => $preview])); +> +> // ... ommitted for brevity +> } +> } +> ``` ### Enabling preview for `DataObject` models which belong to a page @@ -757,13 +750,12 @@ internal states of the layout. You can reach it by calling: $('.cms-container').entwine('.ss').getLayoutOptions().mode; ``` -[notice] -Caveat: the `.preview-mode-selector` appears twice, once in the preview and -second time in the CMS actions area as `#preview-mode-dropdown-in-cms`. This is -done because the user should still have access to the mode selector even if -preview is not visible. Currently CMS Actions are a separate area to the preview -option selectors, even if they try to appear as one horizontal bar. -[/notice] +> [!WARNING] +> Caveat: the `.preview-mode-selector` appears twice, once in the preview and +> second time in the CMS actions area as `#preview-mode-dropdown-in-cms`. This is +> done because the user should still have access to the mode selector even if +> preview is not visible. Currently CMS Actions are a separate area to the preview +> option selectors, even if they try to appear as one horizontal bar. ### Preview API diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/05_Typography.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/05_Typography.md index bbea283a2..75a452ef9 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/05_Typography.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/05_Typography.md @@ -27,13 +27,12 @@ $config = new TinyMCEConfig(); $config->setContentCSS([ '/app/client/css/editor.css' ]); ``` -[notice] -`silverstripe/admin` adds a small CSS file to `editor_css` which highlights broken links - you'll -probably want to include that in the array you pass to `setContentCSS()`, either by first calling -`getContentCSS()` and merging that array with your new one (and passing the result to `setContentCSS()`) -or by adding `'/_resources/vendor/silverstripe/admin/client/dist/styles/editor.css'` to the array you pass -to `setContentCSS()` -[/notice] +> [!WARNING] +> `silverstripe/admin` adds a small CSS file to `editor_css` which highlights broken links - you'll +> probably want to include that in the array you pass to `setContentCSS()`, either by first calling +> `getContentCSS()` and merging that array with your new one (and passing the result to `setContentCSS()`) +> or by adding `'/_resources/vendor/silverstripe/admin/client/dist/styles/editor.css'` to the array you pass +> to `setContentCSS()` ## Custom style dropdown diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/06_Javascript_Development.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/06_Javascript_Development.md index a145fac1e..8da3c194c 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/06_Javascript_Development.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/06_Javascript_Development.md @@ -21,11 +21,10 @@ There are many ways to solve the problem of transpiling. The toolchain we use in ## jQuery, jQuery UI and jQuery.Entwine: our libraries of choice -[notice] -The following documentation regarding jQuery, jQueryUI and Entwine does not apply to React components or sections powered by React. -If you're developing new functionality in React powered sections please refer to -[ReactJS, Redux, and GraphQL](./reactjs_redux_and_graphql). -[/notice] +> [!WARNING] +> The following documentation regarding jQuery, jQueryUI and Entwine does not apply to React components or sections powered by React. +> If you're developing new functionality in React powered sections please refer to +> [ReactJS, Redux, and GraphQL](./reactjs_redux_and_graphql). We predominantly use [jQuery](https://jquery.com) as our abstraction library for DOM related programming, within the Silverstripe CMS and certain framework aspects. @@ -140,9 +139,8 @@ NoConflict mode is enabled by default in the Silverstripe CMS JavaScript. You have to ensure that DOM elements you want to act on are loaded before using them. jQuery provides a wrapper around the `window.onload` and `document.ready` events. -[info] -This doesn't apply to jQuery entwine declarations, which will apply to elements matching your selectors as they get added to the DOM, even if that happens before or after your code is executed. See [the entwine documentation](jquery_entwine) for more details about this. -[/info] +> [!NOTE] +> This doesn't apply to jQuery entwine declarations, which will apply to elements matching your selectors as they get added to the DOM, even if that happens before or after your code is executed. See [the entwine documentation](jquery_entwine) for more details about this. ```js (function ($) { @@ -176,9 +174,8 @@ $('.cms-container').on('click', 'input[[type=submit]]', function () { }); ``` -[hint] -You can do this using entwine as well, which has the added benefit of not requiring your original selector to match a DOM element initially (e.g. for the above example if there are no `.cms-container` elements, or those elements are removed and re-added to the DOM, your native binding won't work but an entwine one will). -[/hint] +> [!TIP] +> You can do this using entwine as well, which has the added benefit of not requiring your original selector to match a DOM element initially (e.g. for the above example if there are no `.cms-container` elements, or those elements are removed and re-added to the DOM, your native binding won't work but an entwine one will). ### Assume element collections diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/07_jQuery_Entwine.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/07_jQuery_Entwine.md index ea41afb0c..19df1c8bb 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/07_jQuery_Entwine.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/07_jQuery_Entwine.md @@ -5,11 +5,10 @@ iconBrand: js # jQuery entwine -[notice] -The following documentation regarding jQuery and Entwine does not apply to React components or sections powered by React. -If you're developing new functionality in React powered sections please refer to -[React, Redux, and GraphQL](/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/). -[/notice] +> [!WARNING] +> The following documentation regarding jQuery and Entwine does not apply to React components or sections powered by React. +> If you're developing new functionality in React powered sections please refer to +> [React, Redux, and GraphQL](/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/). jQuery Entwine was originally written by [Hamish Friedlander](https://github.com/hafriedlander/jquery.entwine). @@ -27,9 +26,8 @@ $('div').entwine({ }); ``` -[info] -The definitions you provide are *not* bound to the elements that match at definition time. You can declare behaviour prior to the DOM existing in any form (i.e. prior to DOMReady) and later calls and event handlers will function correctly. -[/info] +> [!NOTE] +> The definitions you provide are *not* bound to the elements that match at definition time. You can declare behaviour prior to the DOM existing in any form (i.e. prior to DOMReady) and later calls and event handlers will function correctly. ### Selector specifity and "inheritance" {#specificity} @@ -91,9 +89,8 @@ Attribute text Nonsense ``` -[notice] -For selectors with *the same* level of specificity, the definition which is declared first takes precedence. -[/notice] +> [!WARNING] +> For selectors with *the same* level of specificity, the definition which is declared first takes precedence. #### Calling less-specific logic from a definition with higher-specificity @@ -235,9 +232,8 @@ $('.green').entwine({ }); ``` -[hint] -Remember, if you wanted the background colour to change for the div with class `green` as well, you can simply call `this._super()` in the click event handler declared for that selector. See [Selector specifity and "inheritance"](#specificity) for more information about how this works. -[/hint] +> [!TIP] +> Remember, if you wanted the background colour to change for the div with class `green` as well, you can simply call `this._super()` in the click event handler declared for that selector. See [Selector specifity and "inheritance"](#specificity) for more information about how this works. ### Handling events from other elements @@ -269,11 +265,10 @@ Note that an onunmatch block must be paired with an onmatch block - an onunmatch You can also declare a function with the name `onadd` which is similar to `onmatch` but is explicitly triggered by the element being added to the DOM. This means if the element already exists when you declare this function, your function will not be called (but `onmatch` would be). Similarly, if you delcare a function called `onremove`, it will be called when an element is *removed* from the DOM. This does not need an `onadd` function to be declared, unlike `onunmatch`. -[warning] -The `onmatch` and `onadd` events are triggered `asynchronously` - this means that after you add an element to the DOM, it is not guaranteed that functionality in your `onmatch` or `onadd` function for that element will be processed immediately. This is handled using a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver). - -The `onunmatch` and `onremove` events are triggered synchronously however, so you can rely on the element still existing when these functions are called. The element will not be removed from the DOM until the `onunmatch` and `onremove` functions for the element have been called and finished executing. -[/warning] +> [!WARNING] +> The `onmatch` and `onadd` events are triggered `asynchronously` - this means that after you add an element to the DOM, it is not guaranteed that functionality in your `onmatch` or `onadd` function for that element will be processed immediately. This is handled using a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver). +> +> The `onunmatch` and `onremove` events are triggered synchronously however, so you can rely on the element still existing when these functions are called. The element will not be removed from the DOM until the `onunmatch` and `onremove` functions for the element have been called and finished executing. ## Properties @@ -295,9 +290,8 @@ $('div').getMyProperty(); // returns 32 ## Namespaces -[hint] -Most entwine logic defined in core Silverstripe CMS modules uses the `ss` namespace. -[/hint] +> [!TIP] +> Most entwine logic defined in core Silverstripe CMS modules uses the `ss` namespace. To avoid name clashes, to allow multiple bindings to the same event, and to generally seperate a set of functions from other code, you can use namespaces. These are declared by calling the `jQuery.entwine()` function and passing in both the namespace name and a callback, which contains all entwine declarations which belong to that namespace: @@ -317,18 +311,16 @@ You can then call these functions like this: $('div').entwine('foo.bar').baz(); ``` -[info] -Notice that `$` is passed in as an argument to the callback function. This is a *different object* than the `$` which the `entwine()` function is being called on, which contains information about the namespace that you have defined. Another way to write the namespace closure, which illustrates this point, would be like so: - -```js -jQuery.entwine('foo.bar', ($) => { - $('div').entwine({ - // declarations here - }); -}); -``` - -[/info] +> [!NOTE] +> Notice that `$` is passed in as an argument to the callback function. This is a *different object* than the `$` which the `entwine()` function is being called on, which contains information about the namespace that you have defined. Another way to write the namespace closure, which illustrates this point, would be like so: +> +> ```js +> jQuery.entwine('foo.bar', ($) => { +> $('div').entwine({ +> // declarations here +> }); +> }); +> ``` Namespaced functions, properties, and event handlers work just like regular functions (`this` is still set to a matching DOM Node). However, specifity is calculated per namespace. This is particularly useful for events, because given this: @@ -383,6 +375,9 @@ $('div').entwine({ With the above entwine declarations, calling +> [!NOTE] +> Note that trying to call `$('div').bar();` would throw an uncaught `TypeError` saying something like "$(...).bar is not a function", because the `bar()` function was defined in a namespace, but we are trying to call that function from *outside* of that namespace. + ```js $('div').entwine('foo').bar(); ``` @@ -394,45 +389,40 @@ baz qux ``` -[info] -Note that trying to call `$('div').bar();` would throw an uncaught `TypeError` saying something like "$(...).bar is not a function", because the `bar()` function was defined in a namespace, but we are trying to call that function from *outside* of that namespace. -[/info] - -[warning] -Note that 'exists' means that a function is declared in this namespace for *any* selector, not just a matching one. Given the dom - -```html -
Internal text
-``` - -And the entwine definitions - -```js -$.entwine('foo', ($) => { - $('div').entwine({ - bar() { - this.baz(); - }, - }); - - $('span').entwine({ - baz() { - // eslint-disable-next-line no-console - console.log('a'); - }, - }); -}); - -$('div').entwine({ - baz() { - // eslint-disable-next-line no-console - console.log('b'); - }, -}); -``` - -Then calling `$('div')entwine('foo').bar();` will *not* display "b". Even though the `span` rule could never match a `div`, because `baz()` is defined for some rule in the `foo` namespace, the base namespace will never be checked. -[/warning] +> [!WARNING] +> Note that 'exists' means that a function is declared in this namespace for *any* selector, not just a matching one. Given the dom +> +> ```html +>
Internal text
+> ``` +> +> And the entwine definitions +> +> ```js +> $.entwine('foo', ($) => { +> $('div').entwine({ +> bar() { +> this.baz(); +> }, +> }); +> +> $('span').entwine({ +> baz() { +> // eslint-disable-next-line no-console +> console.log('a'); +> }, +> }); +> }); +> +> $('div').entwine({ +> baz() { +> // eslint-disable-next-line no-console +> console.log('b'); +> }, +> }); +> ``` +> +> Then calling `$('div')entwine('foo').bar();` will *not* display "b". Even though the `span` rule could never match a `div`, because `baz()` is defined for some rule in the `foo` namespace, the base namespace will never be checked. ### Calling to another namespace (and forcing base) diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/08_ReactJS_Redux_and_GraphQL.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/08_ReactJS_Redux_and_GraphQL.md index b162e19d5..2c32996f5 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/08_ReactJS_Redux_and_GraphQL.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/08_ReactJS_Redux_and_GraphQL.md @@ -21,12 +21,11 @@ There are some several members of this ecosystem that all work together to provi All of these pillars of the frontend application can be customised, giving you more control over how the admin interface looks, feels, and behaves. -[alert] -These technologies underpin the future of Silverstripe CMS development, but their current implementation is -*experimental*. Our APIs are not expected to change drastically between releases, but they are excluded from -our [semantic versioning](https://semver.org) commitments for the time being. Any breaking changes will be -clearly signalled in release notes. -[/alert] +> [!CAUTION] +> These technologies underpin the future of Silverstripe CMS development, but their current implementation is +> *experimental*. Our APIs are not expected to change drastically between releases, but they are excluded from +> our [semantic versioning](https://semver.org) commitments for the time being. Any breaking changes will be +> clearly signalled in release notes. First, a brief summary of what each of these are: @@ -255,6 +254,9 @@ const addLoggingMiddleware = (next) => (error) => { If you've created a module using React, it's a good idea to afford other developers an API to enhance those components, forms, and state. To do that, simply register them with `Injector`. +> [!WARNING] +> Because of the unique structure of the `form` middleware, you cannot register new services to `Injector.form`. + ```js // my-public-module/js/main.js import Injector from 'lib/Injector'; @@ -269,15 +271,10 @@ Services can then be fetched using their respective `.get()` methods. const MyComponent = Injector.component.get('MyComponent'); ``` -[notice] -Because of the unique structure of the `form` middleware, you cannot register new services to `Injector.form`. -[/notice] - -[alert] -Overwriting components by calling `register()` multiple times for the same -service name is discouraged, and will throw an error. Should you really need to do this, -you can pass `{ force: true }` as the third argument to the `register()` function. -[/alert] +> [!CAUTION] +> Overwriting components by calling `register()` multiple times for the same +> service name is discouraged, and will throw an error. Should you really need to do this, +> you can pass `{ force: true }` as the third argument to the `register()` function. ### Transforming services using middleware @@ -358,14 +355,12 @@ Injector.transform( ); ``` -[info] -This flag can only be used once per transformation. -The following are not allowed: - -- `{ before: ['*', 'something-else'] }` -- `{ after: '*', before: 'something-else' }` - -[/info] +> [!NOTE] +> This flag can only be used once per transformation. +> The following are not allowed: +> +> - `{ before: ['*', 'something-else'] }` +> - `{ after: '*', before: 'something-else' }` ### Injector context @@ -617,6 +612,9 @@ Injector.transform( ); ``` +> [!WARNING] +> It is critical that you end series of mutation calls with `getState()`. + The `alterSchema()` function takes a callback, with an instance of `FormStateManager` (`form` in the above example) as a parameter. `FormStateMangaer` allows you to declaratively update the form schema API using several helper methods, including: @@ -629,14 +627,9 @@ API using several helper methods, including: - `addFieldClass(fieldName:string, cssClassName:string)` - `removeFieldClass(fieldName:string, cssClassName:string)` -[info] -For a complete list of props that are available to update on a `Field` object, -see -[/info] - -[notice] -It is critical that you end series of mutation calls with `getState()`. -[/notice] +> [!NOTE] +> For a complete list of props that are available to update on a `Field` object, +> see In addition to mutation methods, several readonly methods are available on `FormSchemaManager` to read the current form state, including: @@ -978,9 +971,8 @@ Injector.ready(() => { }); ``` -[info] -`this[0]` is how we get the underlying DOM element that the jQuery object is wrapping. We can't pass `this` directly into the `createRoot()` function because react doesn't know how to deal with a jQuery object wrapper. See [the jQuery documentation](https://api.jquery.com/Types/#jQuery) for more information about that syntax. -[/info] +> [!NOTE] +> `this[0]` is how we get the underlying DOM element that the jQuery object is wrapping. We can't pass `this` directly into the `createRoot()` function because react doesn't know how to deal with a jQuery object wrapper. See [the jQuery documentation](https://api.jquery.com/Types/#jQuery) for more information about that syntax. The `silverstripe/admin` module provides `apolloClient` and `store` objects in the global namespace to be shared by other modules. We'll make use of those, and create our own app wrapped in ``. @@ -1065,22 +1057,21 @@ Dynamic GraphQL queries are generated by populating pre-baked templates with spe In this example, we're using the `READ` template, which needs to know the plural name of the object (e.g. `READ` with `Notes` makes a `readNotes` query), whether pagination is activated, and which fields you want to query. -[hint] -For simplicity, we're not querying any relations or otherwise nested data here. If we had, for example, a `foo` relation with a `title` field and this was exposed in the schema, we would need to add it to the fields array like this: - -```js -const query = { - // ... - fields: [ - 'foo', [ - 'title', - ] - ], -}; -``` - -You might instinctively try to use JSON object notation for this instead, but that won't work. -[/hint] +> [!TIP] +> For simplicity, we're not querying any relations or otherwise nested data here. If we had, for example, a `foo` relation with a `title` field and this was exposed in the schema, we would need to add it to the fields array like this: +> +> ```js +> const query = { +> // ... +> fields: [ +> 'foo', [ +> 'title', +> ] +> ], +> }; +> ``` +> +> You might instinctively try to use JSON object notation for this instead, but that won't work. #### Register all the things @@ -1102,18 +1093,16 @@ const registerDependencies = () => { export default registerDependencies; ``` -[hint] -If you have a lot of components or queries to add, you can use `registerMany` instead: - -```js -Injector.component.registerMany({ - NotesList, - NotesListItem, - // ...etc -}); -``` - -[/hint] +> [!TIP] +> If you have a lot of components or queries to add, you can use `registerMany` instead: +> +> ```js +> Injector.component.registerMany({ +> NotesList, +> NotesListItem, +> // ...etc +> }); +> ``` We use `Injector.query.register()` to register our `readNotes` query so that other projects can extend it. @@ -1250,9 +1239,8 @@ App\Model\Note: Let's first update the `NotesListItem` to contain our new field. -[notice] -Note that we're overriding the entire `NotesListItem` component. This is the main reason we broke the original list up into smaller components. -[/notice] +> [!WARNING] +> Note that we're overriding the entire `NotesListItem` component. This is the main reason we broke the original list up into smaller components. ```js // app/client/src/transformNotesListItem.js @@ -1351,10 +1339,9 @@ Injector.transform( ); ``` -[hint] -This transformation could either be transpiled as-is, or if you have other JavaScript to include in this module you might want to export it as a function and call it from some entry point. -Don't forget to add the transpiled result to the CMS e.g. via the `SilverStripe\Admin\LeftAndMain.extra_requirements_javascript` configuration property. -[/hint] +> [!TIP] +> This transformation could either be transpiled as-is, or if you have other JavaScript to include in this module you might want to export it as a function and call it from some entry point. +> Don't forget to add the transpiled result to the CMS e.g. via the `SilverStripe\Admin\LeftAndMain.extra_requirements_javascript` configuration property. ### Creating extensible mutations @@ -1382,9 +1369,8 @@ const AddForm = ({ onAdd }) => { export default AddForm; ``` -[info] -Because this isn't a full react tutorial, we've avoided the complexity of ensuring the list gets updated when we add an item to the form. You'll have to refresh the page to see your note after adding it. -[/info] +> [!NOTE] +> Because this isn't a full react tutorial, we've avoided the complexity of ensuring the list gets updated when we add an item to the form. You'll have to refresh the page to see your note after adding it. And we'll inject that component into our `App` container. diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/CMS_Alternating_Button.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/CMS_Alternating_Button.md index 9bc3ebf60..07fb57b3f 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/CMS_Alternating_Button.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/CMS_Alternating_Button.md @@ -90,11 +90,10 @@ class MyObject extends DataObject ## Frontend support -[notice] -The following documentation regarding jQuery, jQueryUI and Entwine does not apply to React components or sections powered by React. -If you're developing new functionality in React powered sections please refer to -[React, Redux, and GraphQL](/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/). -[/notice] +> [!WARNING] +> The following documentation regarding jQuery, jQueryUI and Entwine does not apply to React components or sections powered by React. +> If you're developing new functionality in React powered sections please refer to +> [React, Redux, and GraphQL](/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/). As with the *Save* and *Save & publish* buttons, you might want to add some scripted reactions to user actions on the frontend. You can affect the state of the button through the jQuery UI calls. diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Customise_Site_Reports.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Customise_Site_Reports.md index 55d1f3738..f7c2e5ca7 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Customise_Site_Reports.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Customise_Site_Reports.md @@ -23,9 +23,8 @@ SilverStripe\Reports\Report: limit_count_in_overview: 500 ``` -[notice] -Note that some reports may have overridden the `getCount` method, and for those reports this may not apply. -[/notice] +> [!WARNING] +> Note that some reports may have overridden the `getCount` method, and for those reports this may not apply. ## Default reports diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md index 3b4b2002b..70f2b094a 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md @@ -227,9 +227,8 @@ We can also easily create new drop-up menus by defining new tabs within the $fields->addFieldToTab('ActionMenus.MyDropUp', FormAction::create('minor', 'Minor action in a new drop-up')); ``` -[hint] -Empty tabs will be automatically removed from the `FieldList` to prevent clutter. -[/hint] +> [!TIP] +> Empty tabs will be automatically removed from the `FieldList` to prevent clutter. To make the actions more user-friendly you can also use alternating buttons as detailed in the [CMS Alternating Button](cms_alternating_button) diff --git a/en/02_Developer_Guides/16_Execution_Pipeline/01_Flushable.md b/en/02_Developer_Guides/16_Execution_Pipeline/01_Flushable.md index 0a96df5ba..7423b5aa8 100644 --- a/en/02_Developer_Guides/16_Execution_Pipeline/01_Flushable.md +++ b/en/02_Developer_Guides/16_Execution_Pipeline/01_Flushable.md @@ -11,11 +11,10 @@ Allows a class to define it's own flush functionality, which is triggered when ` [FlushMiddleware](api:SilverStripe\Control\Middleware\FlushMiddleware) is run before a request is made, calling `flush()` statically on all implementors of [Flushable](api:SilverStripe\Core\Flushable). -[notice] -Flushable implementers might also be triggered automatically on deploy if you have `SS_FLUSH_ON_DEPLOY` [environment -variable](../configuration/environment_variables) defined. In that case even if you don't manually pass `flush=1` parameter, the first request after deploy -will still be calling `Flushable::flush` on those entities. -[/notice] +> [!WARNING] +> Flushable implementers might also be triggered automatically on deploy if you have `SS_FLUSH_ON_DEPLOY` [environment +> variable](../configuration/environment_variables) defined. In that case even if you don't manually pass `flush=1` parameter, the first request after deploy +> will still be calling `Flushable::flush` on those entities. ## Usage diff --git a/en/02_Developer_Guides/17_CLI/index.md b/en/02_Developer_Guides/17_CLI/index.md index 6a8737b42..016dba788 100644 --- a/en/02_Developer_Guides/17_CLI/index.md +++ b/en/02_Developer_Guides/17_CLI/index.md @@ -19,21 +19,19 @@ cd your-project-root/ php vendor/silverstripe/framework/cli-script.php dev/build ``` -[notice] -Your command line PHP version is likely to use a different configuration as your webserver (run `php -i` to find out -more). This can be a good thing, your CLI can be configured to use higher memory limits than you would want your website -to have. -[/notice] +> [!WARNING] +> Your command line PHP version is likely to use a different configuration as your webserver (run `php -i` to find out +> more). This can be a good thing, your CLI can be configured to use higher memory limits than you would want your website +> to have. ## Sake - Silverstripe CMS make Sake is a simple wrapper around `cli-script.php`. It also tries to detect which `php` executable to use if more than one are available. It is accessible via `vendor/bin/sake`. -[info] -If you are using a Debian server: Check you have the php-cli package installed for sake to work. If you get an error -when running the command PHP -v, then you may not have php-cli installed so sake won't work. -[/info] +> [!NOTE] +> If you are using a Debian server: Check you have the php-cli package installed for sake to work. If you get an error +> when running the command PHP -v, then you may not have php-cli installed so sake won't work. ### Installation @@ -44,9 +42,8 @@ cd your-project-root/ sudo ./vendor/bin/sake installsake ``` -[warning] -This currently only works on UNIX like systems, not on Windows. -[/warning] +> [!WARNING] +> This currently only works on UNIX like systems, not on Windows. ### Configuration @@ -78,10 +75,9 @@ sake dev/ sake dev/build "flush=1" ``` -[alert] -You have to run "sake" with the same system user that runs your web server, -otherwise "flush" won't be able to clean the cache properly. -[/alert] +> [!CAUTION] +> You have to run "sake" with the same system user that runs your web server, +> otherwise "flush" won't be able to clean the cache properly. It can also be handy if you have a long running script.. @@ -146,9 +142,8 @@ sake -start my_process sake -stop my_process ``` -[notice] -`sake` stores `pid` and log files in the project root directory. -[/notice] +> [!WARNING] +> `sake` stores `pid` and log files in the project root directory. ## Arguments diff --git a/en/02_Developer_Guides/18_Cookies_And_Sessions/01_Cookies.md b/en/02_Developer_Guides/18_Cookies_And_Sessions/01_Cookies.md index fa6585bca..6a09f18fd 100644 --- a/en/02_Developer_Guides/18_Cookies_And_Sessions/01_Cookies.md +++ b/en/02_Developer_Guides/18_Cookies_And_Sessions/01_Cookies.md @@ -28,9 +28,8 @@ Cookie::set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = // Cookie::set('MyApplicationPreference', 'Yes'); ``` -[info] -To set a cookie for less than 1 day, you can assign an `$expiry` value that is lower than 1. e.g. `Cookie::set('name', 'value', $expiry = 0.5);` will set a cookie for 12 hours. -[/info] +> [!NOTE] +> To set a cookie for less than 1 day, you can assign an `$expiry` value that is lower than 1. e.g. `Cookie::set('name', 'value', $expiry = 0.5);` will set a cookie for 12 hours. ### Get @@ -63,9 +62,8 @@ SilverStripe\Control\Cookie: default_samesite: 'Strict' ``` -[info] -Note that this *doesn't* apply for the session cookie, which is handled separately. See [Sessions](/developer_guides/cookies_and_sessions/sessions#samesite-attribute). -[/info] +> [!NOTE] +> Note that this *doesn't* apply for the session cookie, which is handled separately. See [Sessions](/developer_guides/cookies_and_sessions/sessions#samesite-attribute). ## Cookie_Backend diff --git a/en/02_Developer_Guides/19_GraphQL/01_getting_started/01_activating_the_server.md b/en/02_Developer_Guides/19_GraphQL/01_getting_started/01_activating_the_server.md index 95faa2cf8..6ebdfefe8 100644 --- a/en/02_Developer_Guides/19_GraphQL/01_getting_started/01_activating_the_server.md +++ b/en/02_Developer_Guides/19_GraphQL/01_getting_started/01_activating_the_server.md @@ -21,10 +21,9 @@ in the user space (e.g. `/graphql`) - i.e. your custom schema, while `admin` ref GraphQL server used by CMS modules (`admin/graphql`). You can also [set up a new schema server](#setting-up-a-custom-graphql-server) if you wish. -[info] -The word "server" here refers to a route with its own isolated GraphQL schema. It does -not refer to a web server. -[/info] +> [!NOTE] +> The word "server" here refers to a route with its own isolated GraphQL schema. It does +> not refer to a web server. By default, `silverstripe/graphql` does not route any GraphQL servers. To activate the default, public-facing GraphQL server that ships with the module, just add a rule to [`Director`](api:SilverStripe\Control\Director). diff --git a/en/02_Developer_Guides/19_GraphQL/01_getting_started/02_configuring_your_schema.md b/en/02_Developer_Guides/19_GraphQL/01_getting_started/02_configuring_your_schema.md index ca605a170..026351eae 100644 --- a/en/02_Developer_Guides/19_GraphQL/01_getting_started/02_configuring_your_schema.md +++ b/en/02_Developer_Guides/19_GraphQL/01_getting_started/02_configuring_your_schema.md @@ -65,11 +65,10 @@ have to `flush=1` every time you make a schema update, which will slow down your It is recommended that you store your schema YAML **outside of the _config directory** to increase performance and remove the need for flushing when you [build your schema](building_the_schema). -[notice] -This doesn't mean there is never a need to `flush=1` when building your schema. If you were to add a new -schema, make a change to the value of this `src` attribute, or create new PHP classes, those are still -standard config changes which won't take effect without a flush. -[/notice] +> [!WARNING] +> This doesn't mean there is never a need to `flush=1` when building your schema. If you were to add a new +> schema, make a change to the value of this `src` attribute, or create new PHP classes, those are still +> standard config changes which won't take effect without a flush. We can do this by adding a `src` key to our `app/_config/graphql.yml` schema definition that maps to a directory relative to the project root. @@ -86,22 +85,20 @@ SilverStripe\GraphQL\Schema\Schema: Your `src` must be an array. This allows further source files to be merged into your schema. This feature can be use to extend the schema of third party modules. -[info] -Your directory can also be relative to a module reference, e.g. `somevendor/somemodule: _graphql`: - -```yml -# app/_config/graphql.yml -SilverStripe\GraphQL\Schema\Schema: - schemas: - default: - src: - - app/_graphql - - module/_graphql - # The next line would map to `vendor/somevendor/somemodule/_graphql` - - 'somevendor/somemodule: _graphql' -``` - -[/info] +> [!NOTE] +> Your directory can also be relative to a module reference, e.g. `somevendor/somemodule: _graphql`: +> +> ```yml +> # app/_config/graphql.yml +> SilverStripe\GraphQL\Schema\Schema: +> schemas: +> default: +> src: +> - app/_graphql +> - module/_graphql +> # The next line would map to `vendor/somevendor/somemodule/_graphql` +> - 'somevendor/somemodule: _graphql' +> ``` Now, in the new `app/_graphql` folder, we can create YAML file definitions. @@ -183,10 +180,9 @@ be implicitly placed in the corresponding section of the schema - e.g. any confi added to a `.yml` file in `app/_graphql/config/` will be implicitly added to `SilverStripe\GraphQL\Schema\Schema.schemas.default.config`. -[hint] -The names of the actual files here do not matter. You could for example have a separate file -for each of your types, e.g. `app/_graphql/types/my-first-type.yml`. -[/hint] +> [!TIP] +> The names of the actual files here do not matter. You could for example have a separate file +> for each of your types, e.g. `app/_graphql/types/my-first-type.yml`. ```yml # app/_graphql/config/config.yml @@ -263,10 +259,9 @@ and [resolver discovery](../working_with_generic_types/resolver_discovery) secti Let's define a generic type for our GraphQL schema. -[info] -Generic types don't map to `DataObject` classes - they're useful for querying more 'generic' data (hence the name). -You'll learn more about adding DataObjects in [working with DataObjects](../working_with_DataObjects). -[/info] +> [!NOTE] +> Generic types don't map to `DataObject` classes - they're useful for querying more 'generic' data (hence the name). +> You'll learn more about adding DataObjects in [working with DataObjects](../working_with_DataObjects). ```yml # app/_graphql/types.yml @@ -296,10 +291,9 @@ To define a type as required (non-null), you add an exclamation mark: `String!` Often times, you may want to do both: `[String!]!` -[notice] -Look out for the footgun, here. Make sure your bracketed type is in quotes -(i.e. `'[String]'`, not `[String]`), otherwise it's valid YAML that will get parsed as an array! -[/notice] +> [!WARNING] +> Look out for the footgun, here. Make sure your bracketed type is in quotes +> (i.e. `'[String]'`, not `[String]`), otherwise it's valid YAML that will get parsed as an array! That's all there is to it! To learn how we can take this further, check out the [working with generic types](../working_with_generic_types) documentation. Otherwise, diff --git a/en/02_Developer_Guides/19_GraphQL/01_getting_started/03_building_the_schema.md b/en/02_Developer_Guides/19_GraphQL/01_getting_started/03_building_the_schema.md index d08aa4f7a..f2912618a 100644 --- a/en/02_Developer_Guides/19_GraphQL/01_getting_started/03_building_the_schema.md +++ b/en/02_Developer_Guides/19_GraphQL/01_getting_started/03_building_the_schema.md @@ -26,10 +26,9 @@ whenever the schema definition changes, or a new schema definition is added. - Any time you run the `dev/build` command on your project. - `silverstripe/graphql` will attempt to generate your schema "on-demand" on the first GraphQL request *only* if it wasn’t already generated. -[warning] -Relying on the "on-demand" schema generation on the first GraphQL request requires some additional consideration. -See [deploying the schema](deploying_the_schema#on-demand). -[/warning] +> [!WARNING] +> Relying on the "on-demand" schema generation on the first GraphQL request requires some additional consideration. +> See [deploying the schema](deploying_the_schema#on-demand). #### Running `dev/graphql/build` @@ -41,16 +40,14 @@ This command takes an optional `schema` parameter. If you only want to generate (e.g. generate your custom schema, but not the CMS schema), you should pass in the name of the schema you want to build. -[info] -If you do not provide a `schema` parameter, the command will build all schemas. -[/info] +> [!NOTE] +> If you do not provide a `schema` parameter, the command will build all schemas. `vendor/bin/sake dev/graphql/build schema=default` -[info] -Most of the time, the name of your custom schema is `default`. If you're editing DataObjects -that are accessed with GraphQL in the CMS, you may have to rebuild the `admin` schema as well. -[/info] +> [!NOTE] +> Most of the time, the name of your custom schema is `default`. If you're editing DataObjects +> that are accessed with GraphQL in the CMS, you may have to rebuild the `admin` schema as well. Keep in mind that some of your changes will be in YAML in the `_config/` directory, which also requires a flush. @@ -107,10 +104,9 @@ those *are* meant to be accessible through your webserver. See [Tips and Tricks: Schema Introspection](tips_and_tricks#schema-introspection) to find out how to generate these files for your own schema. -[alert] -While it is safe for you to view these files, you should not manually alter them. If you need to make a change -to your GraphQL schema, you should [update the schema definition](configuring_your_schema) and rebuild your schema. -[/alert] +> [!CAUTION] +> While it is safe for you to view these files, you should not manually alter them. If you need to make a change +> to your GraphQL schema, you should [update the schema definition](configuring_your_schema) and rebuild your schema. ### Further reading diff --git a/en/02_Developer_Guides/19_GraphQL/01_getting_started/04_using_procedural_code.md b/en/02_Developer_Guides/19_GraphQL/01_getting_started/04_using_procedural_code.md index 95e26954b..1e356f252 100644 --- a/en/02_Developer_Guides/19_GraphQL/01_getting_started/04_using_procedural_code.md +++ b/en/02_Developer_Guides/19_GraphQL/01_getting_started/04_using_procedural_code.md @@ -21,11 +21,10 @@ types is that they're dynamic. So the procedural API for schemas has to be prett Lastly, if you just prefer writing PHP to writing YAML, this is a good option, too. -[notice] -One thing you cannot do with the procedural API, though it may be tempting, is define resolvers -on the fly as closures. Resolvers must be static methods on a class, and are evaluated during -the schema build. -[/notice] +> [!WARNING] +> One thing you cannot do with the procedural API, though it may be tempting, is define resolvers +> on the fly as closures. Resolvers must be static methods on a class, and are evaluated during +> the schema build. ### Adding executable code diff --git a/en/02_Developer_Guides/19_GraphQL/01_getting_started/05_deploying_the_schema.md b/en/02_Developer_Guides/19_GraphQL/01_getting_started/05_deploying_the_schema.md index 89fa0738e..5c56acae9 100644 --- a/en/02_Developer_Guides/19_GraphQL/01_getting_started/05_deploying_the_schema.md +++ b/en/02_Developer_Guides/19_GraphQL/01_getting_started/05_deploying_the_schema.md @@ -24,9 +24,8 @@ A simplistic approach is to build the schema in your local development environme This approach has the advantage of being very simple, but it will pollute your commits with massive diffs for the generated code. -[notice] -Make sure you set your site to `live` mode and remove any `DEBUG_SCHEMA=1` from your `.env` file if it is there before generating the schema to be committed. -[/notice] +> [!WARNING] +> Make sure you set your site to `live` mode and remove any `DEBUG_SCHEMA=1` from your `.env` file if it is there before generating the schema to be committed. #### Explicitly build the schema during each deployment {#build-during-deployment} @@ -54,9 +53,8 @@ While benchmarking schema generation performance, we measured that a schema expo Our expectation is that on-demand schema generation will be performant for most projects with small or medium schemas. -[warning] -Note that with this approach you will need to remove or empty the `.graphql-generated/` and `public/_graphql/` folders on each server for each deployment that includes a change to the schema definition, or you risk having an outdated GraphQL schema. The "on-demand" schema generation does not detect changes to the schema definition. -[/warning] +> [!WARNING] +> Note that with this approach you will need to remove or empty the `.graphql-generated/` and `public/_graphql/` folders on each server for each deployment that includes a change to the schema definition, or you risk having an outdated GraphQL schema. The "on-demand" schema generation does not detect changes to the schema definition. #### Build the schema during/before deployment and share it across your servers {#multi-server-shared-dirs} diff --git a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/01_adding_dataobjects_to_the_schema.md b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/01_adding_dataobjects_to_the_schema.md index da217f3b5..342976883 100644 --- a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/01_adding_dataobjects_to_the_schema.md +++ b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/01_adding_dataobjects_to_the_schema.md @@ -43,10 +43,9 @@ Case in point, by supplying a value of `*` for `fields` , we're saying that we w on the `Page` class. This includes the first level of relationships, as defined on `has_one`, `has_many`, or `many_many`. -[notice] -Fields on relationships will not inherit the `*` fields selector, and will only expose their ID by default. -To add additional fields for those relationships you will need to add the corresponding `DataObject` model types. -[/notice] +> [!WARNING] +> Fields on relationships will not inherit the `*` fields selector, and will only expose their ID by default. +> To add additional fields for those relationships you will need to add the corresponding `DataObject` model types. The `*` value on `operations` tells the schema to create all available queries and mutations for the DataObject, including: @@ -65,6 +64,11 @@ Now we can access our schema on the default GraphQL endpoint, `/graphql`. Test it out! +> [!NOTE] +> Note the use of the default arguments on `date`. Fields created from `DBFields` +> generate their own default sets of arguments. For more information, see +> [DBFieldArgs](query_plugins#dbfieldargs). + **A query:** ```graphql @@ -89,16 +93,9 @@ query { } ``` -[info] -Note the use of the default arguments on `date`. Fields created from `DBFields` -generate their own default sets of arguments. For more information, see -[DBFieldArgs](query_plugins#dbfieldargs). -[/info] - -[info] -The `... on BlogPage` syntax is called an [inline fragment](https://graphql.org/learn/queries/#inline-fragments). -You can learn more about this syntax in the [Inheritance](../inheritance) section. -[/info] +> [!NOTE] +> The `... on BlogPage` syntax is called an [inline fragment](https://graphql.org/learn/queries/#inline-fragments). +> You can learn more about this syntax in the [Inheritance](../inheritance) section. **A mutation:** @@ -113,9 +110,8 @@ mutation { } ``` -[hint] -Did you get a permissions error? Make sure you're authenticated as someone with appropriate access. -[/hint] +> [!TIP] +> Did you get a permissions error? Make sure you're authenticated as someone with appropriate access. ### Configuring operations @@ -193,22 +189,20 @@ App\Model\ProductCategory: featured: true ``` -[notice] -A couple things to note here: - -- By assigning a value of `true` to the field, we defer to the model to infer the type for the field. To override that, we can always add a `type` property: - - ```yml - App\Model\Product: - fields: - onSale: - type: Boolean - ``` - -- The mapping of our field names to the `DataObject` property is case-insensitive. It is a -convention in GraphQL APIs to use lowerCamelCase fields, so this is given by default. - -[/notice] +> [!WARNING] +> A couple things to note here: +> +> - By assigning a value of `true` to the field, we defer to the model to infer the type for the field. To override that, we can always add a `type` property: +> +> ```yml +> App\Model\Product: +> fields: +> onSale: +> type: Boolean +> ``` +> +> - The mapping of our field names to the `DataObject` property is case-insensitive. It is a +> convention in GraphQL APIs to use lowerCamelCase fields, so this is given by default. ### Bulk loading models @@ -293,17 +287,15 @@ By default, four loaders are provided to you to help gather specific classnames: - `include: [ 'src/Model/*.model.php' ]` - `include: [ 'somevendor/somemodule: src/Model/*.php' ]` +> [!NOTE] +> `exclude` directives will always supersede `include` directives. + Each block starts with a collection of all classes that gets filtered as each loader runs. The primary job of a loader is to *remove* classes from the entire collection, not add them in. -[info] -`exclude` directives will always supersede `include` directives. -[/info] - -[info] -If you find that this paints with too big a brush, you can always override individual models explicitly in `models.yml`. -The bulk loaders run *before* the `models.yml` config is loaded. -[/info] +> [!NOTE] +> If you find that this paints with too big a brush, you can always override individual models explicitly in `models.yml`. +> The bulk loaders run *before* the `models.yml` config is loaded. #### `DataObject` subclasses are the default starting point @@ -448,10 +440,9 @@ modelConfig: type_formatter: ['App\GraphQL\Formatter', 'formatType'] ``` -[info] -In the above example, `DataObject` is the result of [`DataObjectModel::getIdentifier()`](api:SilverStripe\GraphQL\Schema\DataObject::getIdentifier()). -Each model class must declare one of these. -[/info] +> [!NOTE] +> In the above example, `DataObject` is the result of [`DataObjectModel::getIdentifier()`](api:SilverStripe\GraphQL\Schema\DataObject::getIdentifier()). +> Each model class must declare one of these. The formatting function in your `App\GraphQL\Formatter` class could look something like: diff --git a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/02_query_plugins.md b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/02_query_plugins.md index 626639e33..b07b2d8ec 100644 --- a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/02_query_plugins.md +++ b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/02_query_plugins.md @@ -56,13 +56,12 @@ query { } ``` -[notice] -If you're not familiar with the jargon of `edges` and `node`, don't worry too much about it -for now. It's just a pretty well-established convention for pagination in GraphQL, mostly owing -to its frequent use with [cursor-based pagination](https://graphql.org/learn/pagination/), which -isn't something we do in Silverstripe CMS. You can ignore `edges.node` and just use `nodes` if -you want to. -[/notice] +> [!WARNING] +> If you're not familiar with the jargon of `edges` and `node`, don't worry too much about it +> for now. It's just a pretty well-established convention for pagination in GraphQL, mostly owing +> to its frequent use with [cursor-based pagination](https://graphql.org/learn/pagination/), which +> isn't something we do in Silverstripe CMS. You can ignore `edges.node` and just use `nodes` if +> you want to. #### Limiting pagination @@ -85,9 +84,8 @@ SilverStripe\GraphQL\Schema\Plugin\PaginationPlugin: max_limit: 10 ``` -[notice] -If you want to *increase* the limit beyond the default value, you will also need to set a new `default_limit` configuration value on the `PaginationPlugin`. -[/notice] +> [!WARNING] +> If you want to *increase* the limit beyond the default value, you will also need to set a new `default_limit` configuration value on the `PaginationPlugin`. #### Disabling pagination @@ -164,10 +162,9 @@ query { } ``` -[notice] -While it is possible to filter using multiple comparators, segmenting them into -disjunctive groups (e.g. "OR" and "AND" clauses) is not yet supported. -[/notice] +> [!WARNING] +> While it is possible to filter using multiple comparators, segmenting them into +> disjunctive groups (e.g. "OR" and "AND" clauses) is not yet supported. Nested fields are supported by default: @@ -213,9 +210,8 @@ App\Model\ProductCategory: title: true ``` -[info] -You can also add all fields with `'*': true`, just like with standard model definitions. -[/info] +> [!NOTE] +> You can also add all fields with `'*': true`, just like with standard model definitions. ##### Adding non-native filter fields @@ -285,10 +281,9 @@ class ProductResolver } ``` -[info] -Custom filter fields are also a good opportunity to implement something like `filterByCallback` on your list for -particularly complex computations that cannot be done at the database level. -[/info] +> [!NOTE] +> Custom filter fields are also a good opportunity to implement something like `filterByCallback` on your list for +> particularly complex computations that cannot be done at the database level. #### Disabling the filter plugin diff --git a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/03_permissions.md b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/03_permissions.md index 40bec7c03..2f9e2ba81 100644 --- a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/03_permissions.md +++ b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/03_permissions.md @@ -15,9 +15,8 @@ Please see [Model-Level Permissions](/developer_guides/model/permissions/#model- ### Mutation permssions -[info] -When mutations fail due to permission checks, they throw a [`PermissionsException`](api:SilverStripe\GraphQL\Schema\Exception\PermissionsException). -[/info] +> [!NOTE] +> When mutations fail due to permission checks, they throw a [`PermissionsException`](api:SilverStripe\GraphQL\Schema\Exception\PermissionsException). For `create`, if a singleton instance of the record being created doesn't pass a `canCreate($member)` check, the mutation will throw. @@ -32,19 +31,17 @@ For `delete`, if any of the given IDs don't pass a `canDelete($member)` check, t Query permissions are a bit more complicated, because they can either be in list form, (paginated or not), or a single item. Rather than throw, these permission checks work as filters. -[notice] -It is critical that you have a `canView()` method defined on your DataObjects. Without this, only admins are -assumed to have permission to view a record. -[/notice] +> [!WARNING] +> It is critical that you have a `canView()` method defined on your DataObjects. Without this, only admins are +> assumed to have permission to view a record. For `read` and `readOne` a plugin called `canView` will filter the result set by the `canView($member)` check. -[notice] -When paginated items fail a `canView()` check, the `pageInfo` field is not affected. -Limits and pages are determined through database queries. It would be too inefficient to perform in-memory checks on large data sets. -This can result in pages showing a smaller number of items than what the page should contain, but keeps the pagination calls consistent -for `limit` and `offset` parameters. -[/notice] +> [!WARNING] +> When paginated items fail a `canView()` check, the `pageInfo` field is not affected. +> Limits and pages are determined through database queries. It would be too inefficient to perform in-memory checks on large data sets. +> This can result in pages showing a smaller number of items than what the page should contain, but keeps the pagination calls consistent +> for `limit` and `offset` parameters. ### Disabling query permissions diff --git a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/04_inheritance.md b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/04_inheritance.md index ca21a3f91..07ac51ba2 100644 --- a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/04_inheritance.md +++ b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/04_inheritance.md @@ -136,11 +136,10 @@ query { You can think of interfaces in this context as abstractions of *parent classes* - and the best part is they're generated automatically. We don't need to manually define or implement the interfaces. -[info] -A good way to determine whether you need an inline fragment is to ask -"can this field appear on any other types in the query?" If the answer is yes, you want to use an interface, -which is usually the parent class with the "Interface" suffix. -[/info] +> [!NOTE] +> A good way to determine whether you need an inline fragment is to ask +> "can this field appear on any other types in the query?" If the answer is yes, you want to use an interface, +> which is usually the parent class with the "Interface" suffix. ### Inheritance: a deep dive @@ -336,10 +335,9 @@ query { } ``` -[info] -The above example shows a query for elements on all elemental pages - but for most situations you will -probably only want to query the elements on one page at a time. -[/info] +> [!NOTE] +> The above example shows a query for elements on all elemental pages - but for most situations you will +> probably only want to query the elements on one page at a time. ### Optional: use unions instead of interfaces diff --git a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/05_versioning.md b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/05_versioning.md index 70674395b..e213976c7 100644 --- a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/05_versioning.md +++ b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/05_versioning.md @@ -41,11 +41,10 @@ The `version` field on your `DataObject` will include the following fields: - `liveVersion`: Boolean (True if the version is the one that is currently live) - `latestDraftVersion`: Boolean (True if the version is the latest draft version) -[info] -Note that `author` and `publisher` are in relation to the given *version* of the object - these are -not necessarily the same as the author and publisher of the *original* record (i.e. the author may not -be the person who created the object, they're the person who saved a specific version of it). -[/info] +> [!NOTE] +> Note that `author` and `publisher` are in relation to the given *version* of the object - these are +> not necessarily the same as the author and publisher of the *original* record (i.e. the author may not +> be the person who created the object, they're the person who saved a specific version of it). Let's look at it in context: diff --git a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/06_property_mapping.md b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/06_property_mapping.md index 46cae7883..1f190668f 100644 --- a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/06_property_mapping.md +++ b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/06_property_mapping.md @@ -24,10 +24,9 @@ Page: property: Content ``` -[notice] -When using explicit property mapping, you must also define an explicit type, as it can -no longer be inferred. -[/notice] +> [!WARNING] +> When using explicit property mapping, you must also define an explicit type, as it can +> no longer be inferred. ### Dot-separated accessors diff --git a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/07_nested_definitions.md b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/07_nested_definitions.md index dfc4daf52..49e292282 100644 --- a/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/07_nested_definitions.md +++ b/en/02_Developer_Guides/19_GraphQL/02_working_with_dataobjects/07_nested_definitions.md @@ -44,9 +44,8 @@ App\Model\BlogCategory: fields: '*' ``` -[info] -You cannot define operations on nested types. They only accept fields. -[/info] +> [!NOTE] +> You cannot define operations on nested types. They only accept fields. ### Further reading diff --git a/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/02_building_a_custom_query.md b/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/02_building_a_custom_query.md index 5d6f3f71d..2cda24610 100644 --- a/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/02_building_a_custom_query.md +++ b/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/02_building_a_custom_query.md @@ -59,12 +59,11 @@ queries: resolver: [ 'App\GraphQL\Resolver\MyResolver', 'resolveCountries' ] ``` -[notice] -Note the difference in syntax here between the `type` and the `resolver` - the type declaration -*must* have quotes around it, because we are saying "this is a list of `Country` objects". The value -of this must be a YAML *string*. But the resolver must *not* be surrounded in quotes. It is explicitly -a YAML array, so that PHP recognises it as a `callable`. -[/notice] +> [!WARNING] +> Note the difference in syntax here between the `type` and the `resolver` - the type declaration +> *must* have quotes around it, because we are saying "this is a list of `Country` objects". The value +> of this must be a YAML *string*. But the resolver must *not* be surrounded in quotes. It is explicitly +> a YAML array, so that PHP recognises it as a `callable`. Now, we just have to build the schema: @@ -108,13 +107,12 @@ And the expected response: } ``` -[notice] -Keep in mind that [plugins](../working_with_DataObjects/query_plugins) -don't apply in this context - at least without updating the resolver -to account for them. Most importantly this means you need to -implement your own `canView()` checks. It also means you need -to add your own filter functionality, such as [pagination](adding_pagination). -[/notice] +> [!WARNING] +> Keep in mind that [plugins](../working_with_DataObjects/query_plugins) +> don't apply in this context - at least without updating the resolver +> to account for them. Most importantly this means you need to +> implement your own `canView()` checks. It also means you need +> to add your own filter functionality, such as [pagination](adding_pagination). ## Resolver method arguments diff --git a/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/04_adding_arguments.md b/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/04_adding_arguments.md index 79a84f637..62b13734a 100644 --- a/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/04_adding_arguments.md +++ b/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/04_adding_arguments.md @@ -18,11 +18,10 @@ queries: 'readCountries(limit: Int!)': '[Country]' ``` -[hint] -In the above example, the `limit` argument is *required* by making it non-nullable. If you want to be able -to get an un-filtered list, you can instead allow the argument to be nullable by removing the `!`: -`'readCountries(limit: Int)': '[Country]'` -[/hint] +> [!TIP] +> In the above example, the `limit` argument is *required* by making it non-nullable. If you want to be able +> to get an un-filtered list, you can instead allow the argument to be nullable by removing the `!`: +> `'readCountries(limit: Int)': '[Country]'` We've provided the required argument `limit` to the query, which will allow us to truncate the results. Let's update the resolver accordingly. diff --git a/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/05_adding_pagination.md b/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/05_adding_pagination.md index 2a2524a03..d207e783d 100644 --- a/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/05_adding_pagination.md +++ b/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/05_adding_pagination.md @@ -27,12 +27,11 @@ Let's take this a step further and paginate it using a plugin. Since pagination is a fairly common task, we can take advantage of some reusable code here and just add a generic plugin for paginating. -[notice] -If you're paginating a `DataList`, you might want to consider using models with read operations (instead of declaring -them as generic types with generic queries), which paginate by default using the `paginateList` plugin. -You can use generic typing and follow the below instructions too but it requires code that, for `DataObject` models, -you get for free. -[/notice] +> [!WARNING] +> If you're paginating a `DataList`, you might want to consider using models with read operations (instead of declaring +> them as generic types with generic queries), which paginate by default using the `paginateList` plugin. +> You can use generic typing and follow the below instructions too but it requires code that, for `DataObject` models, +> you get for free. Let's add the plugin to our query: diff --git a/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/index.md b/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/index.md index 26d4f8367..c6af3d5ea 100644 --- a/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/index.md +++ b/en/02_Developer_Guides/19_GraphQL/03_working_with_generic_types/index.md @@ -13,10 +13,9 @@ write some custom queries from the ground up. This is useful for situations where your data doesn't come from a `DataObject`, or where you have very specific requirements for your GraphQL API that don't easily map to the schema of your `DataObject` classes. -[info] -Just because we won't be using DataObjects in this example doesn't mean you can't do it - you can absolutely -declare `DataObject` classes as generic types. You would lose a lot of the benefits of the `DataObject` model -in doing so, but this lower level API may suit your needs for very specific use cases. -[/info] +> [!NOTE] +> Just because we won't be using DataObjects in this example doesn't mean you can't do it - you can absolutely +> declare `DataObject` classes as generic types. You would lose a lot of the benefits of the `DataObject` model +> in doing so, but this lower level API may suit your needs for very specific use cases. [CHILDREN] diff --git a/en/02_Developer_Guides/19_GraphQL/04_security_and_best_practices/01_authentication.md b/en/02_Developer_Guides/19_GraphQL/04_security_and_best_practices/01_authentication.md index 28a087a74..888d48324 100644 --- a/en/02_Developer_Guides/19_GraphQL/04_security_and_best_practices/01_authentication.md +++ b/en/02_Developer_Guides/19_GraphQL/04_security_and_best_practices/01_authentication.md @@ -18,11 +18,10 @@ the same `Member` session is used to authenticate GraphQL requests, however if y are performing requests from an anonymous/external application you may need to authenticate before you can complete a request. -[notice] -Please note that when implementing GraphQL resources it is the developer's -responsibility to ensure that permission checks are implemented wherever -resources are accessed. -[/notice] +> [!WARNING] +> Please note that when implementing GraphQL resources it is the developer's +> responsibility to ensure that permission checks are implemented wherever +> resources are accessed. ### Default authentication @@ -93,9 +92,8 @@ is applicable in the current request context (provided as an argument). Here's an example for implementing HTTP basic authentication: -[notice] -Note that basic authentication for GraphQL will bypass Multi-Factor Authentication (MFA) if that's enabled. Using basic authentication for GraphQL is considered insecure if you are using MFA. -[/notice] +> [!WARNING] +> Note that basic authentication for GraphQL will bypass Multi-Factor Authentication (MFA) if that's enabled. Using basic authentication for GraphQL is considered insecure if you are using MFA. ```yml SilverStripe\GraphQL\Auth\Handler: diff --git a/en/02_Developer_Guides/19_GraphQL/04_security_and_best_practices/02_cors.md b/en/02_Developer_Guides/19_GraphQL/04_security_and_best_practices/02_cors.md index 5fb31daa9..5ef567f25 100644 --- a/en/02_Developer_Guides/19_GraphQL/04_security_and_best_practices/02_cors.md +++ b/en/02_Developer_Guides/19_GraphQL/04_security_and_best_practices/02_cors.md @@ -61,10 +61,9 @@ Once you have enabled CORS you can then control four new headers in the HTTP Res Allow-Headers: 'Authorization, Content-Type, Content-Language' ``` - [notice] - If you add extra headers to your GraphQL server, you will need to write a - custom resolver function to handle the response. - [/notice] + > [!WARNING] + > If you add extra headers to your GraphQL server, you will need to write a + > custom resolver function to handle the response. 1. **Access-Control-Allow-Methods.** diff --git a/en/02_Developer_Guides/19_GraphQL/04_security_and_best_practices/05_recursive_or_complex_queries.md b/en/02_Developer_Guides/19_GraphQL/04_security_and_best_practices/05_recursive_or_complex_queries.md index ea03dd461..63cfb3f9d 100644 --- a/en/02_Developer_Guides/19_GraphQL/04_security_and_best_practices/05_recursive_or_complex_queries.md +++ b/en/02_Developer_Guides/19_GraphQL/04_security_and_best_practices/05_recursive_or_complex_queries.md @@ -30,9 +30,8 @@ SilverStripe\GraphQL\Schema\Schema: max_query_complexity: 100 # default unlimited ``` -[info] -For calculating the query complexity, every field in the query gets a default score 1 (including ObjectType nodes). Total complexity of the query is the sum of all field scores. -[/info] +> [!NOTE] +> For calculating the query complexity, every field in the query gets a default score 1 (including ObjectType nodes). Total complexity of the query is the sum of all field scores. You can also configure these settings for individual schemas. This allows you to fine-tune the security of your custom public-facing schema without affecting the security of the schema used in the CMS. To do so, either replace `'*'` with the name of your schema in the YAML configuration above, or set the values under the `config` key for your schema using preferred file structure as defined in [configuring your schema](../getting_started/configuring_your_schema/). For example: diff --git a/en/02_Developer_Guides/19_GraphQL/05_plugins/03_writing_a_complex_plugin.md b/en/02_Developer_Guides/19_GraphQL/05_plugins/03_writing_a_complex_plugin.md index 17f94c10e..f81f38992 100644 --- a/en/02_Developer_Guides/19_GraphQL/05_plugins/03_writing_a_complex_plugin.md +++ b/en/02_Developer_Guides/19_GraphQL/05_plugins/03_writing_a_complex_plugin.md @@ -277,11 +277,10 @@ we could filter the result set by a list of `IDs` early on, which would allow us a `DataList` throughout the whole cycle, but this would force us to loop over an unlimited result set, and that's never a good idea. -[notice] -If you've picked up on the inconsistency that the `pageInfo` property is now inaccurate, this is a long-standing -issue with doing post-query filters. Ideally, any middleware that filters a `DataList` should do it at the query level, -but that's not always possible. -[/notice] +> [!WARNING] +> If you've picked up on the inconsistency that the `pageInfo` property is now inaccurate, this is a long-standing +> issue with doing post-query filters. Ideally, any middleware that filters a `DataList` should do it at the query level, +> but that's not always possible. ### Step 5: apply the plugins diff --git a/en/02_Developer_Guides/19_GraphQL/06_extending/adding_middleware.md b/en/02_Developer_Guides/19_GraphQL/06_extending/adding_middleware.md index fad05d381..b7b1104c3 100644 --- a/en/02_Developer_Guides/19_GraphQL/06_extending/adding_middleware.md +++ b/en/02_Developer_Guides/19_GraphQL/06_extending/adding_middleware.md @@ -17,10 +17,9 @@ In `silverstripe/graphql`, middleware is used for query execution, but could ostensibly be used elsewhere too if the API ever accomodates such an expansion. -[notice] -The middleware API in the `silverstripe/graphql` module is separate from other common middleware -APIs in Silverstripe CMS, such as `HTTPMiddleware`. The two are not interchangable. -[/notice] +> [!WARNING] +> The middleware API in the `silverstripe/graphql` module is separate from other common middleware +> APIs in Silverstripe CMS, such as `HTTPMiddleware`. The two are not interchangable. The signature for middleware (defined in [`QueryMiddleware`](api:SilverStripe\GraphQL\Middleware\QueryMiddleware)) looks like this: diff --git a/en/02_Developer_Guides/19_GraphQL/07_tips_and_tricks.md b/en/02_Developer_Guides/19_GraphQL/07_tips_and_tricks.md index ba613187d..19438aeb8 100644 --- a/en/02_Developer_Guides/19_GraphQL/07_tips_and_tricks.md +++ b/en/02_Developer_Guides/19_GraphQL/07_tips_and_tricks.md @@ -15,11 +15,10 @@ of differentiating between the two. When debugging, however, it's much easier if these classnames are human-readable. To turn on debug mode, add `DEBUG_SCHEMA=1` to your environment file. The classnames and filenames in the generated code directory will then match their type names. -[warning] -Take care not to use `DEBUG_SCHEMA=1` as an inline environment variable to your build command, e.g. -`DEBUG_SCHEMA=1 vendor/bin/sake dev/graphql/build` because any activity that happens at run time, e.g. querying the schema -will fail, since the environment variable is no longer set. -[/warning] +> [!WARNING] +> Take care not to use `DEBUG_SCHEMA=1` as an inline environment variable to your build command, e.g. +> `DEBUG_SCHEMA=1 vendor/bin/sake dev/graphql/build` because any activity that happens at run time, e.g. querying the schema +> will fail, since the environment variable is no longer set. In live mode, full obfuscation kicks in and the filenames become unreadable. You can only determine the type they map to by looking at the generated classes and finding the `// @type:` inline comment, e.g. `// @type:Page`. @@ -66,9 +65,8 @@ By default three are provided, which cover most use cases: All of these implementations can be configured through `Injector`. -[notice] -Note that each schema gets its own set of persisted queries. In these examples, we're using the `default` schema. -[/notice] +> [!WARNING] +> Note that each schema gets its own set of persisted queries. In these examples, we're using the `default` schema. #### FileProvider @@ -87,9 +85,8 @@ A flat file in the path `/var/www/project/query-mapping.json` should contain som {"someUniqueID":"query{validateToken{Valid Message Code}}"} ``` -[notice] -The file path must be absolute. -[/notice] +> [!WARNING] +> The file path must be absolute. #### HTTPProvider @@ -127,9 +124,8 @@ To access a persisted query, simply pass an `id` parameter in the request in lie `GET https://www.example.com/graphql?id=someID` -[notice] -Note that if you pass `query` along with `id`, an exception will be thrown. -[/notice] +> [!WARNING] +> Note that if you pass `query` along with `id`, an exception will be thrown. ## Query caching (caution: EXPERIMENTAL) @@ -157,9 +153,8 @@ SilverStripe\ORM\DataObject: - SilverStripe\GraphQL\Extensions\QueryRecorderExtension ``` -[warning] -This feature is experimental, and has not been thoroughly evaluated for security. Use at your own risk. -[/warning] +> [!WARNING] +> This feature is experimental, and has not been thoroughly evaluated for security. Use at your own risk. ## Schema introspection {#schema-introspection} diff --git a/en/02_Developer_Guides/19_GraphQL/08_architecture_diagrams.md b/en/02_Developer_Guides/19_GraphQL/08_architecture_diagrams.md index 1d6942d91..69b0989ef 100644 --- a/en/02_Developer_Guides/19_GraphQL/08_architecture_diagrams.md +++ b/en/02_Developer_Guides/19_GraphQL/08_architecture_diagrams.md @@ -36,9 +36,8 @@ There are two key processes that happen at request time. Although they're run in The controller receives the query as a request parameter and persists it as state. It then fetches the schema from the schema storage service (generated code). Then, the query is passed to a query handler service that runs the query through the generated schema code, into a stack of resolvers that execute in serial, much like a stack of middlewares, until finally the response is generated and sent down the wire. -[info] -The concept of the "resolver stack" is illustrated later in this document. -[/info] +> [!NOTE] +> The concept of the "resolver stack" is illustrated later in this document. ## Schema composition @@ -101,9 +100,8 @@ Sometimes, a resolver needs to be used in multiple contexts, for instance, a gen To solve this problem, we can use "resolver context". -[info] -The word "context" is a bit overloaded here. This section has nothing to do with the `$context` parameter that is passed to all resolvers. -[/info] +> [!NOTE] +> The word "context" is a bit overloaded here. This section has nothing to do with the `$context` parameter that is passed to all resolvers. When resolvers have context, they must be factories, or functions that return functions, using the following pattern: diff --git a/en/03_Upgrading/07_Deprecations.md b/en/03_Upgrading/07_Deprecations.md index 9418ef74c..31a6e9d0b 100644 --- a/en/03_Upgrading/07_Deprecations.md +++ b/en/03_Upgrading/07_Deprecations.md @@ -15,9 +15,8 @@ Major releases of Silverstripe CMS introduce many API changes. They include a de To enable deprecation warnings, set the `SS_DEPRECATION_ENABLED` environment variable in your project's `.env` file. -[info] -If the `SS_DEPRECATION_ENABLED` environment variable is set, this takes precedence over use of the `Deprecation::enable()` static method. -[/info] +> [!NOTE] +> If the `SS_DEPRECATION_ENABLED` environment variable is set, this takes precedence over use of the `Deprecation::enable()` static method. ```bash SS_DEPRECATION_ENABLED=true @@ -29,9 +28,8 @@ Alternatively, add the following line to your project's `app/_config.php`. Deprecation::enable(); ``` -[info] -Deprecation warnings will only ever show if your `SS_ENVIRONMENT_TYPE` is set to `dev`. -[/info] +> [!NOTE] +> Deprecation warnings will only ever show if your `SS_ENVIRONMENT_TYPE` is set to `dev`. Once you have resolved all of the deprecation warnings you can, it is recommended to turn off deprecation warnings again. @@ -63,17 +61,16 @@ SilverStripe\Core\Injector\Injector: ErrorLogFileHandler: [ pushHandler, [ '%$ErrorLogFileHandler' ] ] ``` -[notice] -The log file path must be an absolute file path, as relative paths may behave differently between CLI and HTTP requests. If you want to use a *relative* path, you can use the `SS_ERROR_LOG` environment variable to declare a file path that is relative to your project root: - -```bash -SS_ERROR_LOG="./silverstripe.log" -``` - -You don't need any of the YAML configuration above if you are using the `SS_ERROR_LOG` environment variable - but you can use a combination of the environment variable and YAML configuration if you want to configure multiple error log files. - -You will also need to make sure the user running the PHP process has write access to the log file, wherever you choose to put it. -[/notice] +> [!WARNING] +> The log file path must be an absolute file path, as relative paths may behave differently between CLI and HTTP requests. If you want to use a *relative* path, you can use the `SS_ERROR_LOG` environment variable to declare a file path that is relative to your project root: +> +> ```bash +> SS_ERROR_LOG="./silverstripe.log" +> ``` +> +> You don't need any of the YAML configuration above if you are using the `SS_ERROR_LOG` environment variable - but you can use a combination of the environment variable and YAML configuration if you want to configure multiple error log files. +> +> You will also need to make sure the user running the PHP process has write access to the log file, wherever you choose to put it. See [Configuring error logging](/developer_guides/debugging/error_handling/#configuring-error-logging) to learn about other ways you can handle error logs. @@ -83,9 +80,8 @@ Deprecation warnings won't be output to HTTP responses by default because it can Deprecation warnings can be set to output in HTTP responses by setting `SS_DEPRECATION_SHOW_HTTP` to a truthy value in your .env file. -[info] -If the `SS_DEPRECATION_SHOW_HTTP` environment variable is set, this takes precedence over use of the `Deprecation::setShouldShowForHttp()` static method. -[/info] +> [!NOTE] +> If the `SS_DEPRECATION_SHOW_HTTP` environment variable is set, this takes precedence over use of the `Deprecation::setShouldShowForHttp()` static method. ```bash SS_DEPRECATION_SHOW_HTTP=true @@ -99,9 +95,8 @@ Deprecation::setShouldShowForHttp(true); Note that the output for this won't be very easy to read. You might prefer instead to install [lekoala/silverstripe-debugbar](https://github.com/lekoala/silverstripe-debugbar) as a dev dependency. Deprecation warnings will be logged in the "messages" tab of the debugbar. -[warning] -The debugbar will *not* show you any deprecation warnings that are triggered from XHR/AJAX requests or which are triggered after the middleware has finished generating the debugbar for the response. -[/warning] +> [!WARNING] +> The debugbar will *not* show you any deprecation warnings that are triggered from XHR/AJAX requests or which are triggered after the middleware has finished generating the debugbar for the response. ## Deprecation warnings in the CLI @@ -109,9 +104,8 @@ Deprecation warnings are output for CLI responses by default (assuming they're e You can suppress deprecation warnings from CLI output by setting `SS_DEPRECATION_SHOW_CLI` to a falsy value in your .env file. -[info] -If the `SS_DEPRECATION_SHOW_CLI` environment variable is set, this takes precedence over use of the `Deprecation::SS_DEPRECATION_SHOW_CLI()` static method. -[/info] +> [!NOTE] +> If the `SS_DEPRECATION_SHOW_CLI` environment variable is set, this takes precedence over use of the `Deprecation::SS_DEPRECATION_SHOW_CLI()` static method. ```bash SS_DEPRECATION_SHOW_CLI=false diff --git a/en/04_Changelogs/5.0.0.md b/en/04_Changelogs/5.0.0.md index d8ef2cc53..2d1055c90 100644 --- a/en/04_Changelogs/5.0.0.md +++ b/en/04_Changelogs/5.0.0.md @@ -240,9 +240,8 @@ There were a lot more changes than just those, so you may want to also check out ## Front-end build stack upgrades {#front-end} -[info] -"Front-end" in this section refers to the JavaScript and CSS in the CMS. It doesn't have any impact on your website's public-facing front-end. -[/info] +> [!NOTE] +> "Front-end" in this section refers to the JavaScript and CSS in the CMS. It doesn't have any impact on your website's public-facing front-end. We've upgraded the front-end build stack for the CMS, along with most of the JavaScript dependencies. @@ -469,15 +468,14 @@ SilverStripe\Control\Controller: add_trailing_slash: true ``` -[warning] -Because this can be controlled with configuration, it is best practice to avoid explicitly expecting a trailing slash to either be present or be omitted. - -In PHP, you can use methods like [`Controller::join_links()`](api:SilverStripe\Control\Controller::join_links()) or [`Controller::normaliseTrailingSlash()`](api:SilverStripe\Control\Controller::normaliseTrailingSlash()). - -For JavaScript in your CMS customisations, we recommend using the `joinUrlPaths()` utility function from `silverstripe/admin`. You can access this method with `import { joinUrlPaths } from 'lib/urls;` if your project uses [@silverstripe/webpack-config](https://www.npmjs.com/package/@silverstripe/webpack-config). - -In your templates, you should use appropriate methods from your controller or model such as [`SiteTree::Link()`](api:SilverStripe\CMS\Model\SiteTree::Link()) which uses `Controller::join_links()` under the hood. -[/warning] +> [!WARNING] +> Because this can be controlled with configuration, it is best practice to avoid explicitly expecting a trailing slash to either be present or be omitted. +> +> In PHP, you can use methods like [`Controller::join_links()`](api:SilverStripe\Control\Controller::join_links()) or [`Controller::normaliseTrailingSlash()`](api:SilverStripe\Control\Controller::normaliseTrailingSlash()). +> +> For JavaScript in your CMS customisations, we recommend using the `joinUrlPaths()` utility function from `silverstripe/admin`. You can access this method with `import { joinUrlPaths } from 'lib/urls;` if your project uses [@silverstripe/webpack-config](https://www.npmjs.com/package/@silverstripe/webpack-config). +> +> In your templates, you should use appropriate methods from your controller or model such as [`SiteTree::Link()`](api:SilverStripe\CMS\Model\SiteTree::Link()) which uses `Controller::join_links()` under the hood. By default, the [`CanonicalURLMiddleware`](api:SilverStripe\Control\Middleware\CanonicalURLMiddleware) redirects traffic to include or omit the trailing slash according to the above configuration. This means that traffic directed to `/about-us/` will be redirected to `/about-us`. You can disable this behaviour with the following YML configuration: @@ -488,20 +486,18 @@ SilverStripe\Core\Injector\Injector: enforceTrailingSlashConfig: false ``` -[info] -Redirects will not be performed for any route starting with `admin/` or `dev/` by default. You can configure this, as well as exclude specific user agents from being redirected, with the following YML configuration: - -```yml -SilverStripe\Core\Injector\Injector: - SilverStripe\Control\Middleware\CanonicalURLMiddleware: - properties: - enforceTrailingSlashConfigIgnorePaths: - - 'my-ajax-controller/' - enforceTrailingSlashConfigIgnoreUserAgents: - - 'my-dev-user-agent' -``` - -[/info] +> [!NOTE] +> Redirects will not be performed for any route starting with `admin/` or `dev/` by default. You can configure this, as well as exclude specific user agents from being redirected, with the following YML configuration: +> +> ```yml +> SilverStripe\Core\Injector\Injector: +> SilverStripe\Control\Middleware\CanonicalURLMiddleware: +> properties: +> enforceTrailingSlashConfigIgnorePaths: +> - 'my-ajax-controller/' +> enforceTrailingSlashConfigIgnoreUserAgents: +> - 'my-dev-user-agent' +> ``` ### CWP agency extensions diff --git a/en/04_Changelogs/5.2.0.md b/en/04_Changelogs/5.2.0.md index 46ba83e42..84734adf4 100644 --- a/en/04_Changelogs/5.2.0.md +++ b/en/04_Changelogs/5.2.0.md @@ -45,9 +45,8 @@ Traditionally, if you wanted to have multiple `has_many` relations for the same This release includes a new `has_one` syntax to declare that your `has_one` should be allowed to handle multiple reciprocal `has_many` relations. The syntax for that is as follows: -[hint] -Multi-relational `has_one` relations *must* be polymorphic. -[/hint] +> [!TIP] +> Multi-relational `has_one` relations *must* be polymorphic. ```php namespace App\Model; @@ -72,11 +71,10 @@ class MyExample extends DataObject Multiple `has_many` relations on a single class can point to the above `has_one` relation, and they will be correctly saved and resolved when you get the relation list. -[warning] -This new feature means sometimes the value in the associative `has_one` configuration array will be an array, rather than a class name. -If you are relying on fetching this configuration to find the class names of `has_one` relations, consider using -[`DataObject::hasOne()`](api:SilverStripe\ORM\DataObject::hasOne()) or [`DataObjectSchema::hasOneComponent()`](api:SilverStripe\ORM\DataObjectSchema::hasOneComponent()) instead. -[/warning] +> [!WARNING] +> This new feature means sometimes the value in the associative `has_one` configuration array will be an array, rather than a class name. +> If you are relying on fetching this configuration to find the class names of `has_one` relations, consider using +> [`DataObject::hasOne()`](api:SilverStripe\ORM\DataObject::hasOne()) or [`DataObjectSchema::hasOneComponent()`](api:SilverStripe\ORM\DataObjectSchema::hasOneComponent()) instead. See [multi-relational `has_one` in the relations docs](/developer_guides/model/relations/#multi-relational-has-one) for more details about this relation type. @@ -212,9 +210,8 @@ You can also now optionally implement a `canView()` method on your `BuildTask` i Typehints using PHPStan-style generic types have been added to PHPDocs in many areas of the codebase of supported modules. The primary goal of this is to improve the developer experience by correctly reporting to your IDE what types it should expect, for example when looping through a `DataList`. In many cases your IDE will now know what types to expect without needing you to prompt it with [`@var` annotation comments](https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/var.html). -[info] -There are some cases where this goal conflicts with having types that are correctly identified by PHPStan itself (or other static analysis tools). For example conditional return types aren't supported as widely in IDEs as generics themselves are, so we opted to not use conditional return types even when those would result in a more accurate type for static analysis tools. -[/info] +> [!NOTE] +> There are some cases where this goal conflicts with having types that are correctly identified by PHPStan itself (or other static analysis tools). For example conditional return types aren't supported as widely in IDEs as generics themselves are, so we opted to not use conditional return types even when those would result in a more accurate type for static analysis tools. While you should see some improvements immediately after updating, there are some changes you can make to your own codebase to best use the new generic type hints. @@ -243,9 +240,8 @@ The generic typing on the `Extension` class can be used to tell your IDE what ty For this to be useful, you need to tell your IDE that your subclass `@extends` the `Extension` class, and tell it what type the owner should be. -[warning] -Don't forget to include a `use` statement, even if you're not explicitly referencing the type anywhere in your actual code. Your IDE needs the `use` statement to resolve the FQCN for the class you're referencing in the typehint. -[/warning] +> [!WARNING] +> Don't forget to include a `use` statement, even if you're not explicitly referencing the type anywhere in your actual code. Your IDE needs the `use` statement to resolve the FQCN for the class you're referencing in the typehint. ```php namespace App\Extension; @@ -306,11 +302,10 @@ class HomepageController extends PageController A [`LogicException`](https://www.php.net/manual/en/class.logicexception.php) is now thrown by [`FormSchema::getSchema()`](api:SilverStripe\Forms\Schema::getSchema()) if a react component was not found for a field type. -[warning] -If your project or a module you're using is currently trying to include a field which doesn't have a react component into a react-rendered form, it will have been silently failing. The form will have been rendering everything except for the field(s) which have no react component. - -This will now fail by throwing an exception, which means your form won't render at all until you remove or replace the field(s). -[/warning] +> [!WARNING] +> If your project or a module you're using is currently trying to include a field which doesn't have a react component into a react-rendered form, it will have been silently failing. The form will have been rendering everything except for the field(s) which have no react component. +> +> This will now fail by throwing an exception, which means your form won't render at all until you remove or replace the field(s). ### Other new features diff --git a/en/04_Changelogs/beta/5.0.0-beta1.md b/en/04_Changelogs/beta/5.0.0-beta1.md index 1363f578f..9b83d1c3a 100644 --- a/en/04_Changelogs/beta/5.0.0-beta1.md +++ b/en/04_Changelogs/beta/5.0.0-beta1.md @@ -229,9 +229,8 @@ For example, let's assume your CMS 4 branch is called `main` and the current sta - `main` will now be used to release a CMS 5 version of your module. All CMS 5 upgrade commits should be merged in `main`. - To patch bugs against both the CMS 4 and CMS 5 versions of your module, merge the fix in the `3` branch. Then merge the `3` branch into `main`. -[info] -If the name of your branch is a number, composer will allow it to be installed as an unstable tag. e.g.: If your module has a `7` branch, composer will alias the `HEAD` of that branch as `7.x-dev`. This will satisfy a constraint like `^7`. This is especially useful for some development pipelines and for running tests in a continuous integration environment like GitHub Actions. -[/info] +> [!NOTE] +> If the name of your branch is a number, composer will allow it to be installed as an unstable tag. e.g.: If your module has a `7` branch, composer will alias the `HEAD` of that branch as `7.x-dev`. This will satisfy a constraint like `^7`. This is especially useful for some development pipelines and for running tests in a continuous integration environment like GitHub Actions. ### Stop using deprecated code {#stop-using-deprecated-code} @@ -344,9 +343,8 @@ There were a lot more changes than just those, so you may want to also check out ## Front-end build stack upgrades {#front-end} -[info] -"Front-end" in this section refers to the JavaScript and CSS in the CMS. It doesn't have any impact on your website's public-facing front-end. -[/info] +> [!NOTE] +> "Front-end" in this section refers to the JavaScript and CSS in the CMS. It doesn't have any impact on your website's public-facing front-end. We've upgraded the front-end build stack for the CMS, along with most of the JavaScript dependencies. @@ -571,11 +569,10 @@ SilverStripe\Control\Controller: add_trailing_slash: true ``` -[warning] -Because this can be controlled with configuration, it is best practice to avoid explicitly expecting a trailing slash to either be present or be omitted. - -In PHP, you can use methods like [`Controller::join_links()`](api:SilverStripe\Control\Controller::join_links()) or [`Controller::normaliseTrailingSlash()`](api:SilverStripe\Control\Controller::normaliseTrailingSlash()). In JavaScript in your CMS customisations, we recommend using using the `joinUrlPaths()` utility function `silverstripe/admin` which you can access with `import { joinUrlPaths } from 'lib/urls;` if your project uses [@silverstripe/webpack-config](https://www.npmjs.com/package/@silverstripe/webpack-config). In your templates, you should use appropriate methods from your controller or model such as [`SiteTree::Link()`](api:SilverStripe\CMS\Model\SiteTree::Link()) which uses `Controller::join_links()` under the hood. -[/warning] +> [!WARNING] +> Because this can be controlled with configuration, it is best practice to avoid explicitly expecting a trailing slash to either be present or be omitted. +> +> In PHP, you can use methods like [`Controller::join_links()`](api:SilverStripe\Control\Controller::join_links()) or [`Controller::normaliseTrailingSlash()`](api:SilverStripe\Control\Controller::normaliseTrailingSlash()). In JavaScript in your CMS customisations, we recommend using using the `joinUrlPaths()` utility function `silverstripe/admin` which you can access with `import { joinUrlPaths } from 'lib/urls;` if your project uses [@silverstripe/webpack-config](https://www.npmjs.com/package/@silverstripe/webpack-config). In your templates, you should use appropriate methods from your controller or model such as [`SiteTree::Link()`](api:SilverStripe\CMS\Model\SiteTree::Link()) which uses `Controller::join_links()` under the hood. The [`CanonicalURLMiddleware`](api:SilverStripe\Control\Middleware\CanonicalURLMiddleware) will also, by default, redirect traffic to include or omit the trailing slash according to the above configuration. By default, this means that traffic directed to `/about-us/` will be redirected to `/about-us`. You can disable this behaviour with the following YML configuration: @@ -586,20 +583,18 @@ SilverStripe\Core\Injector\Injector: enforceTrailingSlashConfig: false ``` -[info] -Redirects will not be performed for any route starting with `admin/` or `dev/` by default. You can configure this, as well as exclude specific user agents from being redirected, with the following YML configuration: - -```yml -SilverStripe\Core\Injector\Injector: - SilverStripe\Control\Middleware\CanonicalURLMiddleware: - properties: - enforceTrailingSlashConfigIgnorePaths: - - 'my-ajax-controller/' - enforceTrailingSlashConfigIgnoreUserAgents: - - 'my-dev-user-agent' -``` - -[/info] +> [!NOTE] +> Redirects will not be performed for any route starting with `admin/` or `dev/` by default. You can configure this, as well as exclude specific user agents from being redirected, with the following YML configuration: +> +> ```yml +> SilverStripe\Core\Injector\Injector: +> SilverStripe\Control\Middleware\CanonicalURLMiddleware: +> properties: +> enforceTrailingSlashConfigIgnorePaths: +> - 'my-ajax-controller/' +> enforceTrailingSlashConfigIgnoreUserAgents: +> - 'my-dev-user-agent' +> ``` ### CWP agency extensions {#cwp-agency-extensions} diff --git a/en/04_Changelogs/rc/5.0.0-rc1.md b/en/04_Changelogs/rc/5.0.0-rc1.md index 18f6e10c3..81fb5b9d2 100644 --- a/en/04_Changelogs/rc/5.0.0-rc1.md +++ b/en/04_Changelogs/rc/5.0.0-rc1.md @@ -229,9 +229,8 @@ There were a lot more changes than just those, so you may want to also check out ## Front-end build stack upgrades {#front-end} -[info] -"Front-end" in this section refers to the JavaScript and CSS in the CMS. It doesn't have any impact on your website's public-facing front-end. -[/info] +> [!NOTE] +> "Front-end" in this section refers to the JavaScript and CSS in the CMS. It doesn't have any impact on your website's public-facing front-end. We've upgraded the front-end build stack for the CMS, along with most of the JavaScript dependencies. @@ -456,11 +455,10 @@ SilverStripe\Control\Controller: add_trailing_slash: true ``` -[warning] -Because this can be controlled with configuration, it is best practice to avoid explicitly expecting a trailing slash to either be present or be omitted. - -In PHP, you can use methods like [`Controller::join_links()`](api:SilverStripe\Control\Controller::join_links()) or [`Controller::normaliseTrailingSlash()`](api:SilverStripe\Control\Controller::normaliseTrailingSlash()). In JavaScript in your CMS customisations, we recommend using using the `joinUrlPaths()` utility function `silverstripe/admin` which you can access with `import { joinUrlPaths } from 'lib/urls;` if your project uses [@silverstripe/webpack-config](https://www.npmjs.com/package/@silverstripe/webpack-config). In your templates, you should use appropriate methods from your controller or model such as [`SiteTree::Link()`](api:SilverStripe\CMS\Model\SiteTree::Link()) which uses `Controller::join_links()` under the hood. -[/warning] +> [!WARNING] +> Because this can be controlled with configuration, it is best practice to avoid explicitly expecting a trailing slash to either be present or be omitted. +> +> In PHP, you can use methods like [`Controller::join_links()`](api:SilverStripe\Control\Controller::join_links()) or [`Controller::normaliseTrailingSlash()`](api:SilverStripe\Control\Controller::normaliseTrailingSlash()). In JavaScript in your CMS customisations, we recommend using using the `joinUrlPaths()` utility function `silverstripe/admin` which you can access with `import { joinUrlPaths } from 'lib/urls;` if your project uses [@silverstripe/webpack-config](https://www.npmjs.com/package/@silverstripe/webpack-config). In your templates, you should use appropriate methods from your controller or model such as [`SiteTree::Link()`](api:SilverStripe\CMS\Model\SiteTree::Link()) which uses `Controller::join_links()` under the hood. The [`CanonicalURLMiddleware`](api:SilverStripe\Control\Middleware\CanonicalURLMiddleware) will also, by default, redirect traffic to include or omit the trailing slash according to the above configuration. By default, this means that traffic directed to `/about-us/` will be redirected to `/about-us`. You can disable this behaviour with the following YML configuration: @@ -471,20 +469,18 @@ SilverStripe\Core\Injector\Injector: enforceTrailingSlashConfig: false ``` -[info] -Redirects will not be performed for any route starting with `admin/` or `dev/` by default. You can configure this, as well as exclude specific user agents from being redirected, with the following YML configuration: - -```yml -SilverStripe\Core\Injector\Injector: - SilverStripe\Control\Middleware\CanonicalURLMiddleware: - properties: - enforceTrailingSlashConfigIgnorePaths: - - 'my-ajax-controller/' - enforceTrailingSlashConfigIgnoreUserAgents: - - 'my-dev-user-agent' -``` - -[/info] +> [!NOTE] +> Redirects will not be performed for any route starting with `admin/` or `dev/` by default. You can configure this, as well as exclude specific user agents from being redirected, with the following YML configuration: +> +> ```yml +> SilverStripe\Core\Injector\Injector: +> SilverStripe\Control\Middleware\CanonicalURLMiddleware: +> properties: +> enforceTrailingSlashConfigIgnorePaths: +> - 'my-ajax-controller/' +> enforceTrailingSlashConfigIgnoreUserAgents: +> - 'my-dev-user-agent' +> ``` ### CWP agency extensions diff --git a/en/05_Contributing/00_Issues_and_Bugs.md b/en/05_Contributing/00_Issues_and_Bugs.md index b4e2ef2c6..8cb176c08 100644 --- a/en/05_Contributing/00_Issues_and_Bugs.md +++ b/en/05_Contributing/00_Issues_and_Bugs.md @@ -8,9 +8,8 @@ icon: bug ## Reporting bugs -[alert] -If you think you've found a security issue, please use [the specific process](#reporting-security-issues) for those. Do *not* raise a security issue in GitHub. -[/alert] +> [!CAUTION] +> If you think you've found a security issue, please use [the specific process](#reporting-security-issues) for those. Do *not* raise a security issue in GitHub. If you have discovered a bug in Silverstripe CMS, we'd be glad to hear about it - well written bug reports can be half of the solution already! @@ -19,9 +18,8 @@ Silverstripe CMS uses [GitHub](https://github.com/) to manage bug reports. If yo want to report a bug, you will need to [create a GitHub account](https://docs.github.com/en/get-started/onboarding/getting-started-with-your-github-account) and log in. -[warning] -We don't provide support through GitHub issues. If you're having trouble using or developing with Silverstripe CMS but you don't think your problem is a bug, please ask for assistance in our [community channels](https://www.silverstripe.org/community). -[/warning] +> [!WARNING] +> We don't provide support through GitHub issues. If you're having trouble using or developing with Silverstripe CMS but you don't think your problem is a bug, please ask for assistance in our [community channels](https://www.silverstripe.org/community). ### Before submitting a bug @@ -54,22 +52,20 @@ is the best way to ensure it gets fixed. ## Feature requests -[warning] -Please don't file feature requests as GitHub issues. If there's a new feature -you'd like to see in Silverstripe CMS, you either need to write it yourself (and -[submit a pull request](/contributing/code/#step-by-step-from-forking-to-sending-the-pull-request)) or convince somebody else to -write it for you. Any "wishlist" type issues without code attached can be -expected to be closed as soon as they're reviewed. -[/warning] +> [!WARNING] +> Please don't file feature requests as GitHub issues. If there's a new feature +> you'd like to see in Silverstripe CMS, you either need to write it yourself (and +> [submit a pull request](/contributing/code/#step-by-step-from-forking-to-sending-the-pull-request)) or convince somebody else to +> write it for you. Any "wishlist" type issues without code attached can be +> expected to be closed as soon as they're reviewed. In order to gain interest and feedback in your feature, we encourage you to present it to the community through the [community channels](https://www.silverstripe.org/community). ## Reporting security issues -[warning] -If you think a bug may have security implications, do not create a GitHub issue for it. This may lead to a zero-day vulnerability. -[/warning] +> [!WARNING] +> If you think a bug may have security implications, do not create a GitHub issue for it. This may lead to a zero-day vulnerability. Report potential security issues to [security@silverstripe.org](mailto:security@silverstripe.org). Emails sent to that address are forwarded to a private mailing list and kick off a specific security process. diff --git a/en/05_Contributing/01_Code.md b/en/05_Contributing/01_Code.md index a1df4a2a7..57a75ea2c 100644 --- a/en/05_Contributing/01_Code.md +++ b/en/05_Contributing/01_Code.md @@ -10,19 +10,17 @@ The Silverstripe CMS core and supported modules are hosted on [GitHub](https://g This documentation assumes you are fairly confident with git and GitHub. If that isn't the case, you may want to read some guides for [GitHub](https://docs.github.com/en/get-started/quickstart), [git](https://docs.github.com/en/get-started/using-git), and [pull requests](https://docs.github.com/en/pull-requests). -[hint] -Note: By supplying code to the Silverstripe CMS core team in issues and pull requests, you agree to assign copyright of that code to Silverstripe Limited, on the condition that Silverstripe Limited releases that code under the BSD license. - -We ask for this so that the ownership in the license is clear and unambiguous, and so that community involvement doesn't stop us from being able to continue supporting these projects. By releasing this code under a permissive license, this copyright assignment won't prevent you from using the code in any way you see fit. -[/hint] +> [!TIP] +> Note: By supplying code to the Silverstripe CMS core team in issues and pull requests, you agree to assign copyright of that code to Silverstripe Limited, on the condition that Silverstripe Limited releases that code under the BSD license. +> +> We ask for this so that the ownership in the license is clear and unambiguous, and so that community involvement doesn't stop us from being able to continue supporting these projects. By releasing this code under a permissive license, this copyright assignment won't prevent you from using the code in any way you see fit. ## Before you start working {#before-you-start} There are a few things that you should do before you start working on a fix: -[info] -If you want to contribute changes to documentation, please read through the [contributing documentation](./documentation) page. -[/info] +> [!NOTE] +> If you want to contribute changes to documentation, please read through the [contributing documentation](./documentation) page. ### Consider if your change should be its own module @@ -46,11 +44,10 @@ Refer to [Contributing Issues](./issues_and_bugs/) for more information about fi ## Step-by-step: how to contribute code {#step-by-step} -[notice] -The examples below assume you are making a change that applies to the `5.1` branch. - -Please adjust the commands as appropriate for the version of Silverstripe CMS that you're targeting. See [picking the right version](#picking-the-right-version). -[/notice] +> [!WARNING] +> The examples below assume you are making a change that applies to the `5.1` branch. +> +> Please adjust the commands as appropriate for the version of Silverstripe CMS that you're targeting. See [picking the right version](#picking-the-right-version). ### Editing files directly on GitHub @@ -71,9 +68,8 @@ As we follow semantic versioning, we name the branches in repositories according If after reading this section you are still unsure what branch your pull request should go to, consider asking either in the GitHub issue that you address with your PR or in one of the various [community channels](https://www.silverstripe.org/community/). -[hint] -Refer to our [definition of public API](/project_governance/public_api/) for the following sections. -[/hint] +> [!TIP] +> Refer to our [definition of public API](/project_governance/public_api/) for the following sections. Any updates to third party dependencies in `composer.json` should aim to target the default branch for a minor release if possible. Targeting a patch release branch is acceptable if updating dependencies is required to fix a high impact or critical bug and is unlikely to result in regressions. @@ -129,9 +125,8 @@ composer reinstall / --prefer-source git checkout -b ``` -[hint] -Use a descriptive name for your branch. For example if you are fixing a bug related to swapping preview modes targetting the `5.1` branch: `pulls/5.1/fix-preview-modes` -[/hint] +> [!TIP] +> Use a descriptive name for your branch. For example if you are fixing a bug related to swapping preview modes targetting the `5.1` branch: `pulls/5.1/fix-preview-modes` ### Step 4: work on your pull request {#work-on-your-pr} @@ -226,9 +221,8 @@ The core team will review the pull request as time permits. They will most likel If other changes are merged in before yours, your pull request may end up with merge conflicts. You'll need to resolve those by rebasing your branch on top of the target branch, and then manually resolving the merge conflicts. -[warning] -Using `--force-with-lease` is necessary after a rebase, because otherwise GitHub will reject your push. This is because your commit hashes will have changed. But beware that you are explicitly telling GitHub that you are intentionally overriding data. Make sure you have the correct branch name when doing that step to avoid accidentally overriding other branches in your forked repository. -[/warning] +> [!WARNING] +> Using `--force-with-lease` is necessary after a rebase, because otherwise GitHub will reject your push. This is because your commit hashes will have changed. But beware that you are explicitly telling GitHub that you are intentionally overriding data. Make sure you have the correct branch name when doing that step to avoid accidentally overriding other branches in your forked repository. ```bash cd vendor// diff --git a/en/05_Contributing/02_Documentation.md b/en/05_Contributing/02_Documentation.md index 3e2a2afeb..0987de22b 100644 --- a/en/05_Contributing/02_Documentation.md +++ b/en/05_Contributing/02_Documentation.md @@ -30,9 +30,8 @@ If you find a problem related to how the documentation is displayed which can't - If you are fixing incorrect or incomplete information, you should create a PR that targets the most recent patch release branch branch for the relevant major release line (e.g. `5.1`). - If you are adding documentation for functionality that has not yet been released, you should target the most recent minor release branch branch (e.g. `5`). -[warning] -You should make your changes in the lowest major branch they apply to. For instance, if you fix a spelling issue that you found in the CMS 4 documentation, submit your fix to the `4.13` branch in GitHub and it'll be copied to the most recent major version of the documentation automatically. *Don't submit multiple pull requests for the same change*. -[/warning] +> [!WARNING] +> You should make your changes in the lowest major branch they apply to. For instance, if you fix a spelling issue that you found in the CMS 4 documentation, submit your fix to the `4.13` branch in GitHub and it'll be copied to the most recent major version of the documentation automatically. *Don't submit multiple pull requests for the same change*. ## Editing online @@ -129,52 +128,44 @@ Silverstripe CMS documentation has a few special syntax extensions that normal m There are several built-in block styles for calling out a paragraph of text. Please use these graphical elements sparingly. -[info] -"Info box": An info box is useful for adding, deepening or accenting information in the main text. They can be used for background knowledge, or to provide links to further information (ie, a "see also" link). -[/info] +> [!NOTE] +> "Info box": An info box is useful for adding, deepening or accenting information in the main text. They can be used for background knowledge, or to provide links to further information (ie, a "see also" link). Code for an Info box: ```text -[info] -... -[/info] +> [!NOTE] +> ... ``` -[hint] -"Hint box": A hint box is great for pointing out extra use cases or hints about how to use a feature. -[/hint] +> [!TIP] +> "Hint box": A hint box is great for pointing out extra use cases or hints about how to use a feature. Code for a Hint box: ```text -[hint] -... -[/hint] +> [!TIP] +> ... ``` -[warning] -"Warning box": A warning box is useful for pointing out gotchas or technical notifications relating to the main text. For example, notifying users about a deprecated feature. -[/warning] +> [!WARNING] +> "Warning box": A warning box is useful for pointing out gotchas or technical notifications relating to the main text. For example, notifying users about a deprecated feature. Code for a Warning box: ```text -[warning] -... -[/warning] +> [!WARNING] +> ... ``` -[alert] -"Alert box": An alert box is good for for calling out a severe bug or a technical issue requiring a user's attention. For example, suppose a rare edge case sometimes leads to a variable being overwritten incorrectly. An alert box can be used to alert the user to this case so they can write their own code to handle it. -[/alert] +> [!CAUTION] +> "Alert box": An alert box is good for for calling out a severe bug or a technical issue requiring a user's attention. For example, suppose a rare edge case sometimes leads to a variable being overwritten incorrectly. An alert box can be used to alert the user to this case so they can write their own code to handle it. Code for an Alert box: ```text -[alert] -... -[/alert] +> [!CAUTION] +> ... ``` ### Links to documentation diff --git a/en/05_Contributing/06_Build_Tooling.md b/en/05_Contributing/06_Build_Tooling.md index 381a6d75a..0de53855c 100644 --- a/en/05_Contributing/06_Build_Tooling.md +++ b/en/05_Contributing/06_Build_Tooling.md @@ -44,14 +44,13 @@ Once you've installed Node.js and yarn, run the following command once in the `s yarn install ``` -[notice] -The `silverstripe/admin` repository includes some components and dependencies that other modules -need to work. Make sure that in addition to the module(s) who's code you're touching, you also run -`yarn install` in the directory for `silverstripe/admin`. - -You may need to first run `composer reinstall silverstripe/admin --prefer-source` if you installed -that module without `--prefer-source` originally. -[/notice] +> [!WARNING] +> The `silverstripe/admin` repository includes some components and dependencies that other modules +> need to work. Make sure that in addition to the module(s) who's code you're touching, you also run +> `yarn install` in the directory for `silverstripe/admin`. +> +> You may need to first run `composer reinstall silverstripe/admin --prefer-source` if you installed +> that module without `--prefer-source` originally. ## Build commands diff --git a/en/05_Contributing/08_Triage_Resources.md b/en/05_Contributing/08_Triage_Resources.md index cb73dc60d..9a7f99112 100644 --- a/en/05_Contributing/08_Triage_Resources.md +++ b/en/05_Contributing/08_Triage_Resources.md @@ -12,9 +12,8 @@ If you are involved with triage of Silverstripe CMS core and supported modules, You can also optionally subscribe to the repository via [GitHub watch functionality](https://docs.github.com/en/account-and-profile/managing-subscriptions-and-notifications-on-github/setting-up-notifications/configuring-notifications#configuring-your-watch-settings-for-an-individual-repository). -[hint] -When performing these tasks, make sure to adhere to the [code of conduct](/project_governance/code_of_conduct/) and [maintainer guidelines](/project_governance/maintainer_guidelines/#guidelines). -[/hint] +> [!TIP] +> When performing these tasks, make sure to adhere to the [code of conduct](/project_governance/code_of_conduct/) and [maintainer guidelines](/project_governance/maintainer_guidelines/#guidelines). ## How to triage diff --git a/en/05_Contributing/10_Managing_Security_Issues.md b/en/05_Contributing/10_Managing_Security_Issues.md index 34e5c3199..856553944 100644 --- a/en/05_Contributing/10_Managing_Security_Issues.md +++ b/en/05_Contributing/10_Managing_Security_Issues.md @@ -28,9 +28,8 @@ This process is usually started once someone [reports a security issue](issues_a ### When receiving a report -[hint] -Ensure the reporter is given a justification for all issues we conclude are not security vulnerabilities. -[/hint] +> [!TIP] +> Ensure the reporter is given a justification for all issues we conclude are not security vulnerabilities. 1. An automated response is sent back to the reporter to acknowledge receipt of their vulnerability report. 1. Perform an initial assessment of the report. If the report does not qualify as a security issue, reply to the reporter thanking them for their efforts and explaining that we won't be handling this as a security issue. @@ -55,9 +54,8 @@ Ensure the reporter is given a justification for all issues we conclude are not 1. Once a CVE has been assigned, add it to the GitHub issue and respond to the issue reporter. - Ask the reporter if they want to be credited for the disclosure and under what name. -[warning] -Make sure you read the [special circumstances](#special-circumstances) section below in case there are more or different steps you need to follow. -[/warning] +> [!WARNING] +> Make sure you read the [special circumstances](#special-circumstances) section below in case there are more or different steps you need to follow. ### Developing a fix diff --git a/en/06_Project_Governance/03_Maintainer_Guidelines.md b/en/06_Project_Governance/03_Maintainer_Guidelines.md index 6bfc96791..5a00a4598 100644 --- a/en/06_Project_Governance/03_Maintainer_Guidelines.md +++ b/en/06_Project_Governance/03_Maintainer_Guidelines.md @@ -10,10 +10,9 @@ The maintainer guidelines apply to [supported modules](./supported_modules/). Un This document outlines expectations on maintainers of Silverstripe CMS. It also forms the default expectations for maintainers of [supported modules](./supported_modules/), unless more specific contribution guidelines are available for a module. -[note] -A lot of extra information is available in the [Contributing](/contributing/) documentation. -All maintainers should be familiar with those docs as they explain many details about how we work. -[/note] +> [!NOTE] +> A lot of extra information is available in the [Contributing](/contributing/) documentation. +> All maintainers should be familiar with those docs as they explain many details about how we work. Refer to the [triage and peer review](/contributing/triage_resources/) for information about how those tasks are performed. @@ -52,9 +51,8 @@ CMS Squad members have write access to core repositories in order to work effect ### Peer reviewers -[info] -This is a new role, and is currently in a trial period. It is likely to change over time as we learn what works and what doesn't. -[/info] +> [!NOTE] +> This is a new role, and is currently in a trial period. It is likely to change over time as we learn what works and what doesn't. This role is made up of community members who have demonstrated a willingness and ability to improve Silverstripe CMS through their contributions to the open source supported modules. It empowers community members to have a more active role in ensuring pull requests get merged, and builds more momentum for community-created pull requests. @@ -106,9 +104,8 @@ There will be a low-touch background check before inviting the contributor to th ### Contribution refiners -[info] -This is a new role, and is currently in a trial period. It is likely to change over time as we learn what works and what doesn't. -[/info] +> [!NOTE] +> This is a new role, and is currently in a trial period. It is likely to change over time as we learn what works and what doesn't. This role is made up of community members who have demonstrated a willingness and ability to improve Silverstripe CMS through their contributions to the open source supported modules. It empowers community members to have a more active role in identifying and refining bug reports and pull requests with high value potential so that they can be resolved more quickly. diff --git a/en/06_Project_Governance/05_Major_release_policy.md b/en/06_Project_Governance/05_Major_release_policy.md index f6d92d4bf..a2fbf6cd1 100644 --- a/en/06_Project_Governance/05_Major_release_policy.md +++ b/en/06_Project_Governance/05_Major_release_policy.md @@ -12,9 +12,8 @@ This policy applies to all [Silverstripe CMS commercially supported modules](/pr Community modules are not covered by this policy. Modules in the `silverstripe` GitHub organisation that are not commercially supported are updated on a best effort basis. -[info] -Refer to our [definition of public API](/project_governance/public_api/). -[/info] +> [!NOTE] +> Refer to our [definition of public API](/project_governance/public_api/). ## General approach to major releases diff --git a/en/06_Project_Governance/06_Minor_release_policy.md b/en/06_Project_Governance/06_Minor_release_policy.md index 2434109d8..169bed327 100644 --- a/en/06_Project_Governance/06_Minor_release_policy.md +++ b/en/06_Project_Governance/06_Minor_release_policy.md @@ -12,9 +12,8 @@ Note that the release cadence and pre-release time frame are indicative only. We Our minor release policy is more flexible because minor release upgrades are more straightforward to manage than major release upgrades. -[info] -Refer to our [definition of public API](/project_governance/public_api/). -[/info] +> [!NOTE] +> Refer to our [definition of public API](/project_governance/public_api/). ## Scope of the policy