From 9f14ed6edf548de02022226110195a49506143ee Mon Sep 17 00:00:00 2001 From: Guy Sartorelli <36352093+GuySartorelli@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:54:06 +1300 Subject: [PATCH] DOC Document changes to template layer (#591) --- .../04_Directory_Structure.md | 2 +- .../01_Templates/01_Syntax.md | 61 ++++---- .../01_Templates/02_Common_Variables.md | 10 +- .../01_Templates/03_Requirements.md | 4 +- .../01_Templates/04_Rendering_Templates.md | 92 ++++++++++-- .../01_Templates/05_Template_Inheritance.md | 15 -- .../01_Templates/07_Caching.md | 13 +- .../01_Templates/09_Casting.md | 23 +-- .../11_Partial_Template_Caching.md | 10 +- .../01_Templates/12_Swap_Template_Engines.md | 133 ++++++++++++++++++ .../How_Tos/01_Navigation_Menu.md | 2 +- en/02_Developer_Guides/01_Templates/index.md | 4 +- .../02_Controllers/01_Introduction.md | 6 +- .../03_Forms/03_Form_Templates.md | 10 +- .../03_Forms/How_Tos/02_Lightweight_Form.md | 2 +- .../How_Tos/05_Simple_Contact_Form.md | 6 +- .../05_Extending/00_Modules.md | 2 +- .../08_Performance/01_Caching.md | 2 +- .../09_Security/05_Secure_Coding.md | 8 +- en/02_Developer_Guides/10_Email/index.md | 4 +- .../11_Integration/02_RSSFeed.md | 8 +- en/02_Developer_Guides/14_Files/02_Images.md | 8 +- .../14_Files/03_File_Security.md | 10 +- .../02_CMS_Architecture.md | 16 +-- .../04_Preview.md | 24 +--- .../How_Tos/Extend_CMS_Interface.md | 12 +- en/08_Changelogs/6.0.0.md | 58 +++++++- 27 files changed, 380 insertions(+), 165 deletions(-) create mode 100644 en/02_Developer_Guides/01_Templates/12_Swap_Template_Engines.md diff --git a/en/00_Getting_Started/04_Directory_Structure.md b/en/00_Getting_Started/04_Directory_Structure.md index 77aa71df8..bae022b00 100644 --- a/en/00_Getting_Started/04_Directory_Structure.md +++ b/en/00_Getting_Started/04_Directory_Structure.md @@ -32,7 +32,7 @@ We use `app/` as the default folder. | `app/_config` | YAML configuration specific to your application | | `app/src` | PHP code specific to your application (subdirectories are optional) | | `app/tests` | PHP unit/functional/end-to-end tests | -| `app/templates` | HTML [templates](/developer_guides/templates) with `*.ss-extension` for the `$default` theme | +| `app/templates` | [templates](/developer_guides/templates) for the `$default` theme | | `app/client/src` | Conventional directory for source resources (images/CSS/JavaScript) for your CMS customisations | | `app/client/dist` | Conventional directory for transpiled resources (images/CSS/JavaScript) for your CMS customisations | | `app/client/lang` | Conventional directory for [JavaScript translation tables](/developer_guides/i18n/#translation-tables-in-javascript) | diff --git a/en/02_Developer_Guides/01_Templates/01_Syntax.md b/en/02_Developer_Guides/01_Templates/01_Syntax.md index 9125c308d..2d8a30c63 100644 --- a/en/02_Developer_Guides/01_Templates/01_Syntax.md +++ b/en/02_Developer_Guides/01_Templates/01_Syntax.md @@ -6,8 +6,7 @@ icon: code # Template syntax -A template can contain any markup language (e.g HTML, CSV, JSON, etc) and before being rendered to the user, they're -processed through [SSViewer](api:SilverStripe\View\SSViewer). This process replaces placeholders such as `$Var` with real content from your +A template can contain any markup language (e.g HTML, CSV, JSON, etc). The rendering process replaces placeholders such as `$Var` with real content from your model (see [Model and Databases](../model)) and allows you to use logic like `<% if $Var %>` in your templates. An example of a Silverstripe CMS template is below: @@ -69,32 +68,33 @@ This will result in trying to fetch a `Title` property from the data being displ Variables can be chained together, and include arguments. ```ss -$Foo(param) +$Foo("param") $Foo.Bar ``` These variables will call a method / field on the object and insert the returned value as a string into the template. +Arguments don't have to be literals - you can pass template variables as arguments as well. + > [!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). +> Arguments cannot be literal arrays (e.g `$myMethod(["val1", "val2"])`). The behaviour for this is currently undefined. - `$Foo("param")` will call `$obj->Foo("param")` - `$Foo.Bar` will call `$obj->Foo()->Bar` +- `$Foo($Bar)` will call `$obj->Bar` and then pass the result into `$obj->Foo($result)` > [!NOTE] -> Arguments passed into methods can be any non-array literal type (not just strings), e.g: +> Arguments passed into methods can be almost any 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 +> - `$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 and for future-proofing. 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. +been defined, the system will attempt to cast the object to a string. > [!NOTE] > For more details around how variables are inserted and formatted into a template see @@ -111,8 +111,7 @@ use SilverStripe\ORM\DataObject; class MyObject extends DataObject { // ... - - public function UsersIpAddress() + public function usersIpAddress() { return $this->getRequest()->getIP(); } @@ -125,10 +124,11 @@ class MyObject extends DataObject ``` > [!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()`. +> Method names that begin with `get` will automatically be resolved when their prefix is excluded. For example, if the method `usersIpAddress()` didn't exist, the above reference to `$UsersIpAddress` would invoke a method named `getUsersIpAddress()`. +> Note that a method with the exact name you're referencing will always be used if it exists. 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) +object the methods get called on. For the standard `Page.ss` template the scope is usually the current [`ContentController`](api:SilverStripe\CMS\Controllers\ContentController) object. This object provides access to all the public methods on that controller, as well as the public methods, relations, and database fields for its corresponding [`SiteTree`](api:SilverStripe\CMS\Model\SiteTree) record. ```ss @@ -141,6 +141,12 @@ $SilverStripeNavigator $Content ``` +### Case sensitivity + +Just like in PHP, method names are case insensitive, but property names are case sensitive. + +Using `$MyValue` in a template will successfully call a `myValue()` method, or a `getMyValue()` method even though the case doesn't match what you used in the template. But it *will not* match a `myValue` property or database field because the case doesn't match. + ## Conditional logic The simplest conditional block is to check for the presence of a value. This effectively works the same as [`isset()`](https://www.php.net/manual/en/function.isset.php) in PHP - i.e. if there is no variable available with that name, or the variable's value is `0`, `false`, or `null`, the condition will be false. @@ -206,7 +212,7 @@ For more nuanced conditions you can use the `!=` operator. ### Boolean logic -Multiple checks can be done using `||`/`or`, or `&&`/ `and`. +Multiple checks can be done using `||`/`or`, or `&&`/`and`. > [!NOTE] > `or` is functionally equivalent to `||` in template conditions, and `and` is functionally equivalent to `&&`. @@ -564,27 +570,12 @@ refer directly to properties and methods of the [`Member`](api:SilverStripe\Secu ### `fortemplate()` and `$Me` {#fortemplate} -If you reference some `ModelData` object directly in a template, the `forTemplate()` method on that object will be called. -This can be used to provide a default template for an object. - -```php -// app/src/PageType/HomePage.php -namespace App\PageType; - -use Page; - -class HomePage extends Page -{ - public function forTemplate(): string - { - // We can also render a template here using $this->renderWith() - return 'Page: ' . $this->Title; - } -} -``` +If you reference some object directly in a template, the `forTemplate()` method on that object will be called. This can be used to provide a default template for an object. +If the `forTemplate()` method isn't implemented, the system will attempt to cast your object to a string. +The default implementation of this method on `ModelData` renders the model using templates named after the class or its superclasses. ```ss -<%-- calls forTemplate() on the first page in the list and prints Page: Home --%> +<%-- calls forTemplate() on the first page in the list --%> $Pages->First ``` @@ -592,11 +583,11 @@ You can also use the `$Me` variable, which outputs the current object in scope b This is especially helpful when you want to directly render items in a list you're looping over. > [!WARNING] -> If the object does not have a `forTemplate()` method implemented, this will throw an error. +> If the object does not have an appropriate template, implement the `forTemplate()` method, or implement [`__toString()`](https://www.php.net/manual/en/language.oop5.magic.php#object.tostring), this will throw an error. ```ss <% loop $Pages %> - <%-- calls forTemplate() on the current object in scope and prints Page: Home --%> + <%-- calls forTemplate() on the current object in scope --%> $Me <% end_loop %> ``` 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 f87d8196e..7c5c41f66 100644 --- a/en/02_Developer_Guides/01_Templates/02_Common_Variables.md +++ b/en/02_Developer_Guides/01_Templates/02_Common_Variables.md @@ -36,11 +36,11 @@ have a read of the [Formatting, Modifying and Casting Variables](casting) docume ``` -The `<% base_tag %>` placeholder is replaced with the HTML base element. Relative links within a document (such as +The `<% base_tag %>` placeholder is replaced with the HTML `` element. Relative links within a document (such as ``) will become relative to the URI specified in the base tag. This ensures the browser knows where to locate your site’s images and CSS files. -It renders in the template as `` +It renders in the template as `` > [!CAUTION] > A `<% base_tag %>` is nearly always required or assumed by Silverstripe CMS to exist. @@ -117,7 +117,7 @@ on a per-page basis. By default `$MetaTags` renders (assuming 5.1.0 is the current version of `silverstripe/framework`): -```ss +```html Title of the Page @@ -125,12 +125,12 @@ By default `$MetaTags` renders (assuming 5.1.0 is the current version of `silver `$MetaTags(false)` will render -```ss +```html ``` -If using `$MetaTags(false)` we can provide a more custom `title`. +If using `$MetaTags(false)` we can provide a custom ` tag`. ```ss $MetaTags(false) diff --git a/en/02_Developer_Guides/01_Templates/03_Requirements.md b/en/02_Developer_Guides/01_Templates/03_Requirements.md index 61880c7f1..3bc37374d 100644 --- a/en/02_Developer_Guides/01_Templates/03_Requirements.md +++ b/en/02_Developer_Guides/01_Templates/03_Requirements.md @@ -94,7 +94,7 @@ Also see [Direct resource urls](#direct-resource-urls) below if you need to incl ## PHP requirements API It is common practice to include most Requirements either in the `init()` method of your [controller](../controllers/), or -as close to rendering as possible (e.g. in [FormField](api:SilverStripe\Forms\FormField)). +as close to rendering as possible (e.g. in [`FormField::Field()`](api:SilverStripe\Forms\FormField::Field())). ```php namespace App\Control; @@ -451,7 +451,7 @@ If you want to get a resource for a *specific* theme or from somewhere that is n ``` > [!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()`. +> 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 b0c0fd048..e0cc29aba 100644 --- a/en/02_Developer_Guides/01_Templates/04_Rendering_Templates.md +++ b/en/02_Developer_Guides/01_Templates/04_Rendering_Templates.md @@ -6,6 +6,9 @@ icon: code # Rendering data to a template +> [!NOTE] +> The template syntax, file extensions, and specifics about which templates are chosen from a set as described on this page are specific to the default [`SSTemplateEngine`](api:SilverStripe\View\SSTemplateEngine) - but many of the concepts here (especially the PHP code) should work with any template engine you choose to use. + Templates do nothing on their own. Rather, they are used to generate markup - most typically they are used to generate HTML markup, using variables from some `ModelData` object. All of the `<% if %>`, `<% loop %>` and other variables are methods or parameters that are called on the current object in scope (see [scope](syntax#scope) in the syntax section). @@ -19,6 +22,9 @@ The following will render the given data into a template. Given the template: Our application code can render into that view using the [`renderWith()`](api:SilverStripe\Model\ModelData) method provided by `ModelData`. Call this method on any instance of `ModelData` or its subclasses, passing in a template name or an array of templates to render. +> [!IMPORTANT] +> Don't include the `.ss` file extension when referencing templates. + ```php namespace App\Model; @@ -44,6 +50,11 @@ class MyModel extends DataObject If you want to render an arbitrary template into the `$Layout` section of a page, you need to render your layout template and pass that as the `Layout` parameter to the Page template. +These examples assume you have moved the `templates/Coach_Message.ss` template file to `templates/Layout/Coach_Message.ss` + +> [!WARNING] +> While a lot of the concepts on this page apply for any template engine, the `$Layout` functionality is specific to the default [`SSTemplateEngine`](api:SilverStripe\View\SSTemplateEngine). + ```php namespace App\Model; @@ -71,7 +82,6 @@ class MyModel extends DataObject In this case it may be better to use an *implicit* `Layout` type template, and rely on template inheritance to figure out which templates to use. ```php -// This assumes you have moved the Coach_Message template to `templates/Layout/Coach_Message.ss` $this->customise($data)->renderWith(['Coach_Message', 'Page']); ``` @@ -157,16 +167,15 @@ class MyPageController extends PageController ## Rendering arbitrary data in templates -Any data you want to render into the template that does not extend `ModelData` should be wrapped in an object that -does, such as `ArrayData` or `ArrayList`. +While `ModelData` has some methods on it you may find useful for reprensenting complex data, you should be able to use just about anything as a model in a template. + +To actually render the data, you can use the `customise()` method to add your arbitrary data on top of an existing model: ```php namespace App\PageType; use PageController; use SilverStripe\Control\Director; -use SilverStripe\Model\ArrayData; -use SilverStripe\Model\List\ArrayList; class MyPageController extends PageController { @@ -178,14 +187,14 @@ class MyPageController extends PageController return $this->customise([ 'Name' => 'John', 'Role' => 'Head Coach', - 'Experience' => ArrayList::create([ - ArrayData::create([ + 'Experience' => [ + [ 'Title' => 'First Job', - ]) - ArrayData::create([ + ], + [ 'Title' => 'Second Job', - ]), - ]), + ], + ], ])->renderWith('AjaxTemplate'); } else { return $this->httpError(400); @@ -194,5 +203,62 @@ class MyPageController extends PageController } ``` -> [!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 `ModelData` instance as mentioned above. +Or wrap the data in a `ModelData` subclass such as `ArrayList`: + +```php +namespace App\PageType; + +use PageController; +use SilverStripe\Model\ArrayData; + +class MyPageController extends PageController +{ + // ... + + public function getMyRenderedData() + { + return ArrayData::create([ + 'Name' => 'John', + 'Role' => 'Head Coach', + 'Experience' => [ + [ + 'Title' => 'First Job', + ], + [ + 'Title' => 'Second Job', + ], + ], + ])->renderWith('MyTemplate'); + } +} +``` + +Or you can hand the data to `SSViewer` directly: + +```php +namespace App\PageType; + +use PageController; +use SilverStripe\View\SSViewer; + +class MyPageController extends PageController +{ + // ... + + public function getMyRenderedData() + { + return SSViewer::create('MyTemplate')->process([ + 'Name' => 'John', + 'Role' => 'Head Coach', + 'Experience' => [ + [ + 'Title' => 'First Job', + ], + [ + 'Title' => 'Second Job', + ], + ], + ]); + } +} +``` diff --git a/en/02_Developer_Guides/01_Templates/05_Template_Inheritance.md b/en/02_Developer_Guides/01_Templates/05_Template_Inheritance.md index 392339f9e..777ddb45e 100644 --- a/en/02_Developer_Guides/01_Templates/05_Template_Inheritance.md +++ b/en/02_Developer_Guides/01_Templates/05_Template_Inheritance.md @@ -103,21 +103,6 @@ to determine resolution priority. This search is based on the following three co the module author, and does not normally need to be customised. This includes the `$project` and `$other_modules` placeholder values. -### ThemeResourceLoader - -The resolution of themes is performed by a [ThemeResourceLoader](api:SilverStripe\View\ThemeResourceLoader) -instance, which resolves a template (or list of templates) and a set of themes to a system template path. - -For each path the loader will search in this order: - -- Loop through each theme which is configured. -- If a theme is a set (declared with the `$` prefix, e.g. `$default`) it will perform a nested search within - that set. -- When searching the `$default` set, all modules will be searched in the order declared via the `module_priority` - config, interpolating keys `$project` and `$other_modules` as necessary. By default, your project's templates should - be checked before any vendor modules. -- When the first template is found, it will be immediately returned, and will not continue to search. - ### Declaring themes All themes can be enabled and sorted via the `SilverStripe\View\SSViewer.themes` config value. For reference diff --git a/en/02_Developer_Guides/01_Templates/07_Caching.md b/en/02_Developer_Guides/01_Templates/07_Caching.md index 4eb1fd195..38d5c5f02 100644 --- a/en/02_Developer_Guides/01_Templates/07_Caching.md +++ b/en/02_Developer_Guides/01_Templates/07_Caching.md @@ -8,8 +8,8 @@ icon: rocket ## Object caching -All functions that provide data to templates must have no side effects, as the value is cached after first access. For -example, this controller method will not behave as you might imagine. +Any values accessed from a template on a `ModelData` object are cached after first access. Because of this, methods that provide data to templates should ideally have no side effects. For +example, this `getCounter()` method will not behave as you might imagine when invoked from a template. ```php namespace App\Model; @@ -33,8 +33,7 @@ class MyObject extends DataObject ```ss $Counter, $Counter, $Counter - -// returns 1, 1, 1 +// renders as 1, 1, 1 ``` When we render `$Counter` to the template we would expect the value to increase and output `1, 2, 3`. However, as @@ -51,3 +50,9 @@ Example: $CacheableContent <% end_cached %> ``` + +## Template caching + +Every time a raw template file is processed, some PHP code is generated from it which is then executed to produce the final rendered result. + +That PHP code is cached in the filesystem so that the raw template file doesn't need to be processed again until either the cache is flushed or the raw template file is updated. diff --git a/en/02_Developer_Guides/01_Templates/09_Casting.md b/en/02_Developer_Guides/01_Templates/09_Casting.md index d39604bd6..bb6ac9473 100644 --- a/en/02_Developer_Guides/01_Templates/09_Casting.md +++ b/en/02_Developer_Guides/01_Templates/09_Casting.md @@ -6,13 +6,17 @@ icon: code # Formatting, casting, and escaping variable content -All objects that are being rendered in a template should be a [ModelData](api:SilverStripe\Model\ModelData) instance such as `DataObject`, -`DBField` or `Controller`. From these objects, the template can include any method from the object in [scope](syntax#scope). +Classes that subclass [`ModelData`](api:SilverStripe\Model\ModelData) (such as `DataObject`, +`DBField` or `Controller`) can define how their values should be cast or escaped when used in a template. + +Most of the details on this page refer to casting values using the configuration on `ModelData`. ## Casting -The templating system casts variables and the result of method calls into one of the various [`DBField`](api:SilverStripe\ORM\FieldType\DBField) -classes before outputting them in the final rendered markup. Those `DBField` classes provide methods that developers can use to format data in +The templating system casts variables and the result of method calls into objects (usually one of the various [`DBField`](api:SilverStripe\ORM\FieldType\DBField) +classes) before outputting them in the final rendered markup. This casting functionality lives in the [`CastingService`](api:SilverStripe\View\CastingService) class. + +`DBField` classes provide methods that developers can use to format data in the template (e.g. choosing how to format a date), as well as defining whether HTML markup in that data will be escaped, stripped, or included directly in the rendered result. @@ -22,6 +26,8 @@ content that method sends back, or provide a type (using the same format as the to a template, Silverstripe CMS will ensure that the object is wrapped in the correct type. This provides you with all of the methods available in that class, and ensures and values are safely escaped. +Note that if an object is returned from the method, that object will be retained instead of using the `$casting` configuration. + ```php namespace App\Data; @@ -43,13 +49,8 @@ class MyTemplatedObject extends ModelData When calling `$MyCustomMethod` on the above object from a template, Silverstripe CMS now has the context that this method contains HTML, so it won't escape the value. -For every field used in templates, a casting helper will be applied. This will first check for any -`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 `ModelData.default_cast` configuration. By default, -> that configuration is set to `Text`, so HTML characters are escaped. +> By default, all non-object values without a type explicitly defined in a `$casting` configuration will be cast to an appropriate `DBField` implementation (e.g. booleans are cast to [`DBBoolean`](api:SilverStripe\ORM\FieldType\DBBoolean)), except array which are wrapped in either [`ArrayList`](api:SilverStripe\Model\List\ArrayList) (for indexed arrays) or [`ArrayData`](api:SilverStripe\Model\ArrayData) (for associative arrays). ### Common casting types @@ -127,7 +128,7 @@ All `DBField` instances share the following useful methods for formatting their The concept of escaping values in templates is ultimately just a combination of formatting and casting. Values are typically escaped (i.e. the special HTML characters are encoded) in templates by either not -declaring a casting type, or by defaulting to the `Text` casting type defined on `ModelData`. +declaring a casting type, or by defaulting to the `Text` casting type. 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 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 ad0bf924a..8eab59ea6 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 @@ -49,9 +49,9 @@ different templates. Here is how it works in detail: -1. `SilverStripe\View\SSViewer::$global_key` hash +1. `SilverStripe\View\SSTemplateEngine.global_key` hash - With the current template context, value of the `$global_key` variable is rendered into a string and hashed. + With the current template context, value of the `$global_key` configuration property is rendered into a string and hashed. `$global_key` content is inserted into the template "as is" at the compilation stage. Changing its value won't have any effect until template recompilation (e.g. on cache flush). @@ -64,7 +64,7 @@ Here is how it works in detail: ```yml # app/_config/view.yml - SilverStripe\View\SSViewer: + SilverStripe\View\SSTemplateEngine: global_key: '$CurrentReadingMode, $CurrentUser.ID, $CurrentLocale' ``` @@ -86,7 +86,7 @@ Here is how it works in detail: A string produced by concatenation of all the values mentioned above is used as the final value. - Even if `$CacheKey` is omitted, `SilverStripe\View\SSViewer::$global_key` and `Block hash` values are still + Even if `$CacheKey` is omitted, `SilverStripe\View\SSTemplateEngine.global_key` and `Block hash` values are still getting used to generate cache key for the caching backend storage. #### Cache key calculated in controller @@ -340,7 +340,7 @@ The two following forms produce the same result <%-- Hash of this content block is also included into the final Cache Key value along with - SilverStripe\View\SSViewer::$global_key + SilverStripe\View\SSTemplateEngine::$global_key --%> <% uncached %> This text is always dynamic (never cached) diff --git a/en/02_Developer_Guides/01_Templates/12_Swap_Template_Engines.md b/en/02_Developer_Guides/01_Templates/12_Swap_Template_Engines.md new file mode 100644 index 000000000..63c2dde94 --- /dev/null +++ b/en/02_Developer_Guides/01_Templates/12_Swap_Template_Engines.md @@ -0,0 +1,133 @@ +--- +title: Swap Template Engines +summary: Using a third-party template rendering engine with Silverstripe CMS +icon: hdd +--- + +# Swap template engines + +All other sections in the documentation on this website assume you're using the default template rendering engine. That said, Silverstripe CMS does allow you to use other template rendering engines if you want to. + +## How to swap out a template engine + +Depending on what your end goal is, you can swap out the template engine at a few different points. + +### Globally override the default engine via dependency injection {#swap-globally} + +This is the lowest effort in terms of setting the engine, but it means *every single template* used in your project (including in the CMS backend) must be in the syntax your template engine understands. If you don't provide copies of all templates in your syntax of choice, things will fail. + +```yml +--- +After: '#view-config' +--- +SilverStripe\Core\Injector\Injector: + SilverStripe\View\TemplateEngine: + class: 'MyModule\MyTemplateEngine' +``` + +> [!WARNING] +> There are a few places in core code that take a string template from configuration and render it using the default template engine. You'll need to find and update the configuration to provide a string template in your chosen syntax. + +### Define the template engine for a specific controller or model {#swap-controller-or-model} + +#### Controllers {#swap-controller} + +If you want all templates rendered by a given controller to use a different template engine, you need to override the [`getTemplateEngine()`](api:SilverStripe\Control\Controller::getTemplateEngine()) method on your controller. + +```php +namespace App\Controller; + +use MyModule\MyTemplateEngine; +use SilverStripe\Control\Controller; +use SilverStripe\View\TemplateEngine; + +class MyController extends Controller +{ + // ... + protected function getTemplateEngine(): TemplateEngine + { + if (!$this->templateEngine) { + $this->templateEngine = MyTemplateEngine::create(); + } + return $this->templateEngine; + } +} +``` + +When this controller is rendered (e.g. via [`handleAction()`](api:SilverStripe\Control\Controller::handleAction()) or directly calling [`render()`](api:SilverStripe\Control\Controller::handleAction())), your template engine will be used to render it. You must make sure you have an appropriate template available. + +Note that if a different controller is invoked (e.g. rendering an elemental area from inside a page template) or a model gets rendered directly (e.g. using `$Me` in a template) the default template engine will be used for that if you haven't changed it there as well. + +#### Models {#swap-model} + +There's no `getTemplateEngine()` method in [`ModelData`](api:SilverStripe\Model\ModelData). Instead, you'll need to override the [`renderWith()`](api:SilverStripe\Model\ModelData::renderWith()) method like so: + +```php +namespace App\Model; + +use MyModule\MyTemplateEngine; +use SilverStripe\Model\ModelData; +use SilverStripe\ORM\FieldType\DBHTMLText; +use SilverStripe\View\SSViewer; + +class MyModel extends ModelData +{ + // ... + public function renderWith($template, ModelData|array $customFields = []): DBHTMLText + { + // In this case if an SSViewer has been explicitly instantiated, that will be used instead. + // You can choose to override that as well, if it suits your use case to do so. + if (!($template instanceof SSViewer)) { + $template = SSViewer::create($template, MyTemplateEngine::create()); + } + return parent::renderWith($template, $customFields); + } +} +``` + +### For a given instance of `SSViewer` {#swap-ssviewer} + +If you're instantiating [`SSViewer`](api:SilverStripe\View\SSViewer) instances directly, you can pass the template engine as the second constructor argument. + +```php +use MyModule\MyTemplateEngine; +use SilverStripe\View\SSViewer; + +// ... +$viewer = SSViewer::create($templates, MyTemplateEngine::create()); +``` + +## Implementing a template engine + +If you want to use a specific third-party template rendering solution in Silverstripe CMS and there's no module for it, you may need to create your own class that implements the [`TemplateEngine`](api:SilverStripe\View\TemplateEngine) interface. + +When a template needs to be rendered, usually `SSViewer` will be told which template candidates to choose from and what data to inject into the template. This will be passed through from `SSViewer` to your `TemplateEngine` class, which will need to: + +- Determine which actual template file to use +- Render the template, using the given data +- Output the rendered markup as a string + +`SSViewer` is then responsible for normalising the markup (e.g. by rewriting anchor links and injecting resources from the Requirements API). + +For consistency, your template engine should follow these guidelines: + +1. Respect the `SSViewer` cascading themes list and the template priority order. + This ensures that projects and modules can refer to themes and templates generically, and don't need to know the specifics of your template engine for things to work correctly. +1. Respect [`TemplateGlobalProvider`](api:SilverStripe\View\TemplateGlobalProvider) data. + Some modules and project code will provide data that should be available in all templates. Ensuring that data is available will mean you don't have to re-implement ways to access that data when you need it. +1. Respect casting as defined in the [casting](/developer_guides/model/data_types_and_casting/#casting) docs. + This may mean turning off default casting or escaping functionality provided by the third-party template rendering solution. It ensures that the data can be displayed in ways intended by whoever set up the data model. +1. Respect Requirements API. + As mentioned above, `SSViewer` will inject resources from the Requirements API into your final rendered result for you. These can come from PHP code, but you should ideally provide a way to interact with the Requirements API from your templates as well. +1. Respect the base tag generated with [`SSViewer::getBaseTag()`](api:SilverStripe\View\SSViewer::getBaseTag()). + As mentioned in the [common variables](/developer_guides/templates/common_variables/#base-tag) section, there are assumptions in the Silverstripe CMS codebase about this base tag being present. +1. Respect the i18n system. + The localisation system used in Silverstripe CMS is designed to allow you to use the exact same localisation files in PHP code *and* in templates. Ideally your template engine should hook into this same system to avoid duplicating localisation strings. + +Note that when `SSViewer` provides data to your template engine, it will be wrapped in an instance of [`ViewLayerData`](api:SilverStripe\View\ViewLayerData). `ViewLayerData` makes sure data is cast appropriately according to the model that provides it (where possible). When you request values from it, they also come wrapped in `ViewLayerData` unless they're null, or you specifically call the [`getRawDataValue()`](api:SilverStripe\View\ViewLayerData::getRawDataValue()) method. + +### Gotchas + +- You will likely need to use `ViewLayerData::getRawDataValue()` to get values that can be used in conditional statements, since for example an object will always be truthy even if it represents a boolean `false`. + Note that in some cases a `DBField` will be returned from a method on your model or controller directly, in which case `ViewLayerData::getRawDataValue()` will return the `DBField` instance instead of the truly *raw* value. +- Values in the `$overlay` argument for the [`TemplateEngine::render()`](api:SilverStripe\View\TemplateEngine::render()) and [`TemplateEngine::renderString()`](api:SilverStripe\View\TemplateEngine::renderString()) methods, and values returned from `TemplateGlobalProvider`, will not be wrapped in `ViewLayerData`. You should wrap those yourself before using them. diff --git a/en/02_Developer_Guides/01_Templates/How_Tos/01_Navigation_Menu.md b/en/02_Developer_Guides/01_Templates/How_Tos/01_Navigation_Menu.md index 2c2f34eda..f99d9d6be 100644 --- a/en/02_Developer_Guides/01_Templates/How_Tos/01_Navigation_Menu.md +++ b/en/02_Developer_Guides/01_Templates/How_Tos/01_Navigation_Menu.md @@ -8,7 +8,7 @@ summary: Build a multi-tiered navigation UI. In this how-to, we'll create a simple menu which you can use as the primary navigation for your website. This outputs a top level menu with a nested second level using the `Menu` loop and a `Children` loop. -This template relies on the [`Menu()`](api:SilverStripe\Controllers\ContentController::Menu()) method available via the [`ContentController`](api:SilverStripe\Controllers\ContentController) on all [`SiteTree`](api:SilverStripe\CMS\Model\SiteTree) records. The number passed in is the level to start the menu from, where `1` is the root level pages. +This template relies on the [`getMenu()`](api:SilverStripe\Controllers\ContentController::getMenu()) method available via the [`ContentController`](api:SilverStripe\Controllers\ContentController) on all [`SiteTree`](api:SilverStripe\CMS\Model\SiteTree) records. The number passed in is the level to start the menu from, where `1` is the root level pages. ```ss <%-- app/templates/Page.ss --%> diff --git a/en/02_Developer_Guides/01_Templates/index.md b/en/02_Developer_Guides/01_Templates/index.md index 70a6aa72e..c8f69f23d 100644 --- a/en/02_Developer_Guides/01_Templates/index.md +++ b/en/02_Developer_Guides/01_Templates/index.md @@ -12,8 +12,8 @@ core framework, the modules or themes you install, and your own custom templates Silverstripe CMS templates are simple text files that have an `.ss` extension. They can contain any markup language (e.g HTML, XML, JSON..) and can include features such as variables and logic controls such as conditionals. -In this guide we'll look at the syntax of the custom template engine [`SSViewer`](api:SilverStripe\View\SSViewer) and how to render -templates from your controllers. + +In this section we'll look at the syntax of the default template engine and how to render templates from your controllers. Note that anything referencing `SSViewer` should apply regardless of which template engine you use. [CHILDREN Exclude=How_Tos] diff --git a/en/02_Developer_Guides/02_Controllers/01_Introduction.md b/en/02_Developer_Guides/02_Controllers/01_Introduction.md index b7e2e38b5..a2da212dc 100644 --- a/en/02_Developer_Guides/02_Controllers/01_Introduction.md +++ b/en/02_Developer_Guides/02_Controllers/01_Introduction.md @@ -172,13 +172,13 @@ The template to use for a given action is determined in the following order: 1. If a template has been explicitly declared for the "index" action in the `templates` property, it will be used (regardless of what action is being rendered). 1. If the `template` property has been set at all, its value will be used. 1. If a template exists with the name of this class or any of its ancestors, suffixed with the name of the action name, it will be used. - - e.g. for the `App\Control\TeamController` example, the "showPlayers" action would look for `templates/App/Control/TeamController_showPlayers.ss` and `templates/SilverStripe/Control/Controller_showPlayers.ss`. + - e.g. for the `App\Control\TeamController` example, the "showPlayers" action would look for templates named `templates/App/Control/TeamController_showPlayers` and `templates/SilverStripe/Control/Controller_showPlayers` with the relevant file extension. - Note that the "index" action skips this step. 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`. + - e.g. for the `App\Control\TeamController` example, it would look for templates named `templates/App/Control/TeamController` and `templates/SilverStripe/Control/Controller` with the relevant file extension. > [!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`. +> 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 a `HomePage_{action}` template after checking `HomePageController_{action}`. 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. 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 05feb4677..42192760c 100644 --- a/en/02_Developer_Guides/03_Forms/03_Form_Templates.md +++ b/en/02_Developer_Guides/03_Forms/03_Form_Templates.md @@ -22,17 +22,17 @@ To override the template for CMS forms, the custom templates should be located i > [!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 +> you want to create a `MyCustomFormTemplate` copy the contents of the `Form` template to a `MyCustomFormTemplate` template file and > modify as you need. > -> *The default Form.ss can be found at `/vendor/silverstripe/framework/templates/SilverStripe/Forms/Includes/Form.ss`* +> *The default Form template file 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`). +class name (i.e.`EmailField` will attempt to render into a template called `EmailField` and if that isn't found, `TextField` or +finally `FormField`). > [!CAUTION] -> While you can override all templates using normal view inheritance (i.e.defining a `Form.ss`) other modules may rely on +> While you can override all templates using normal view inheritance (i.e.defining a `Form` template) 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/How_Tos/02_Lightweight_Form.md b/en/02_Developer_Guides/03_Forms/How_Tos/02_Lightweight_Form.md index 0e931e6d4..1e589a240 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 @@ -60,7 +60,7 @@ class SearchPage extends Page </form> ``` -`SearchForm.ss` will be executed within the scope of the `Form` object so has access to any of the methods and +The `SearchForm` template 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`. > [!WARNING] 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 e5118ae74..017e82621 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 @@ -81,15 +81,15 @@ return Form::create($this, 'Form', $fields, $actions); Finally we create the `Form` object and return it. The first argument is the controller that the form is on – this is almost always $this. The second argument is the name of the form – this has to be the same as the name of the function that creates the form, so we've used 'Form'. The third and fourth arguments are the fields and actions we created earlier. -To show the form on the page, we need to render it in our template. We do this by appending $ to the name of the form – so for the form we just created we need to add $Form. Add $Form to the themes/currenttheme/Layout/Page.ss template, below $Content. +To show the form on the page, we need to render it in our template. We do this by appending `$` to the name of the form – so for the form we just created we need to add `$Form`. Add `$Form` to the `themes/currenttheme/Layout/Page` template, below `$Content`. -The reason it's standard practice to name the form function 'Form' is so that we don't have to create a separate template for each page with a form. By adding $Form to the generic Page.ss template, all pages with a form named 'Form' will have their forms shown. +The reason it's standard practice to name the form function 'Form' is so that we don't have to create a separate template for each page with a form. By adding $Form to the generic `Page` template, all pages with a form named 'Form' will have their forms shown. If you now create a ContactPage in the CMS (making sure you have rebuilt the database and flushed the templates e.g. `sake db:build --flush`) and visit the page, you will now see a contact form. ![a form with three text fields ("name", "email", and "message") and a submit button](../../../_images/howto_contactForm.jpg) -Now that we have a contact form, we need some way of collecting the data submitted. We do this by creating a function on the controller with the same name as the form action. In this case, we create the function 'submit' on the ContactPage_Controller class. +Now that we have a contact form, we need some way of collecting the data submitted. We do this by creating a function on the controller with the same name as the form action. In this case, we create the function 'submit' on the `ContactPage_Controller` class. ```php namespace App\PageType; diff --git a/en/02_Developer_Guides/05_Extending/00_Modules.md b/en/02_Developer_Guides/05_Extending/00_Modules.md index 66fd36f3c..6bb824114 100644 --- a/en/02_Developer_Guides/05_Extending/00_Modules.md +++ b/en/02_Developer_Guides/05_Extending/00_Modules.md @@ -165,7 +165,7 @@ which the Silverstripe CMS project applies to the modules it creates and maintai ### Coding guidelines - Complies to a well defined module directory structure and coding standards: - - `templates/` (for `.ss` templates) + - `templates/` (for templates e.g. `*.ss` files) - `src/` (for `.php` files) - `tests/` (for `*Test.php` test files), and; - `_config/` (for `.yml` config files) diff --git a/en/02_Developer_Guides/08_Performance/01_Caching.md b/en/02_Developer_Guides/08_Performance/01_Caching.md index db18e787d..df51bc7df 100644 --- a/en/02_Developer_Guides/08_Performance/01_Caching.md +++ b/en/02_Developer_Guides/08_Performance/01_Caching.md @@ -293,7 +293,7 @@ SilverStripe\Core\Injector\Injector: Unfortunately not all caches are configurable via cache adapters. -- [SSViewer](api:SilverStripe\View\SSViewer) writes compiled templates as PHP files to the filesystem +- [`SSTemplateEngine`](api:SilverStripe\View\SSTemplateEngine) writes compiled templates as PHP files to the filesystem (in order to achieve opcode caching on `include()` calls) - [i18n](api:SilverStripe\i18n\i18n) uses `Symfony\Component\Config\ConfigCacheFactoryInterface` (filesystem-based) 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 24e98121a..6d3967435 100644 --- a/en/02_Developer_Guides/09_Security/05_Secure_Coding.md +++ b/en/02_Developer_Guides/09_Security/05_Secure_Coding.md @@ -254,8 +254,8 @@ We recommend configuring [shortcodes](/developer_guides/extending/shortcodes) th ### Escaping model properties -[SSViewer](api:SilverStripe\View\SSViewer) (the Silverstripe CMS template engine) automatically takes care of escaping HTML tags from specific -object-properties by [casting](/developer_guides/model/data_types_and_casting) its string value into a [DBField](api:SilverStripe\ORM\FieldType\DBField) object. +Before outputting values to the template layer, [`ViewLayerData`](api:SilverStripe\View\ViewLayerData) automatically takes care of escaping HTML tags from specific +object-properties by [casting](/developer_guides/model/data_types_and_casting) its string value into a [`DBField`](api:SilverStripe\ORM\FieldType\DBField) object. PHP: @@ -285,7 +285,7 @@ Template: ``` The example below assumes that data wasn't properly filtered when saving to the database, but are escaped before -outputting through SSViewer. +outputting rendered template results through `SSViewer`. ### Overriding default escaping in templates @@ -307,7 +307,7 @@ Template (see above): ### Escaping custom attributes and getters Every object attribute or getter method used for template purposes should have its escape type defined through the -static *$casting* array. Caution: Casting only applies when using values in a template, not in PHP. +`casting` configuration property (assuming you're using a `ModelData` subclass). Caution: Casting only applies when using values in a template, not in PHP. PHP: diff --git a/en/02_Developer_Guides/10_Email/index.md b/en/02_Developer_Guides/10_Email/index.md index b4fecd35b..5620b9080 100644 --- a/en/02_Developer_Guides/10_Email/index.md +++ b/en/02_Developer_Guides/10_Email/index.md @@ -116,8 +116,8 @@ $email->send(); ``` > [!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 `<project-root>/themes/<my-theme>/SilverStripe/Control/Email/Email.ss`. Alternatively, copy it to a different location and use `setHTMLTemplate` when you create the +> The default HTML template for emails is `vendor/silverstripe/framework/templates/SilverStripe/Control/Email/Email`. +> To customise this template, first copy it to a `<project-root>/themes/<my-theme>/SilverStripe/Control/Email/Email` template file. 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 diff --git a/en/02_Developer_Guides/11_Integration/02_RSSFeed.md b/en/02_Developer_Guides/11_Integration/02_RSSFeed.md index ae3f273e5..5c0500d09 100644 --- a/en/02_Developer_Guides/11_Integration/02_RSSFeed.md +++ b/en/02_Developer_Guides/11_Integration/02_RSSFeed.md @@ -164,13 +164,13 @@ class HomePageController extends PageController ### Customizing the RSS feed template -The default template used for XML view is `vendor/silverstripe/framework/templates/RSSFeed.ss`. This template displays titles and links to +The default template used for XML view is `vendor/silverstripe/framework/templates/RSSFeed`. This template displays titles and links to the object. To customise the XML produced use `setTemplate`. Say from that last example we want to include the Players Team in the XML feed we might create the following XML file. -```xml -<!-- app/templates/PlayersRss.ss --> +```ss +<%-- app/templates/PlayersRss.ss --%> <?xml version="1.0"?> <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"> <channel> @@ -216,7 +216,7 @@ class HomePage extends Page ``` > [!WARNING] -> As we've added a new template (PlayersRss.ss) make sure you clear your Silverstripe CMS cache. +> As we've added a new template (`PlayersRss`) make sure you clear your Silverstripe CMS cache. ## API documentation diff --git a/en/02_Developer_Guides/14_Files/02_Images.md b/en/02_Developer_Guides/14_Files/02_Images.md index 8568e1a4c..e152603d3 100644 --- a/en/02_Developer_Guides/14_Files/02_Images.md +++ b/en/02_Developer_Guides/14_Files/02_Images.md @@ -406,16 +406,16 @@ Manipulated images are stored as "file variants" in the same folder structure as ## Controlling how images are rendered -Developers can customise how `Image` instances are rendered on their website by overriding the `templates/SilverStripe/Assets/Storage/DBFile_Image.ss` template file. +Developers can customise how `Image` instances are rendered on their website by overriding the `templates/SilverStripe/Assets/Storage/DBFile_Image` template. This will apply to images added to an `HTMLEditorField` and images invoked in templates. You can also choose to have different rendering logic for `HTMLEditorField` images and for images invoked in templates by overriding different templates. -- Add a `SilverStripe/Assets/Shortcodes/ImageShortcodeProvider_Image.ss` to your theme to control images added to an HTMLEditorField. -- Add a `DBFile_Image.ss` file to the root of your theme to control only images invoked in templates. +- Add a `SilverStripe/Assets/Shortcodes/ImageShortcodeProvider_Image` template to your theme to control images added to an `HTMLEditorField`. +- Add a `DBFile_Image` template to the root of your theme to control only images invoked in templates. -Look at [Template inheritance](../templates/Template_Inheritance) for more information on how to override SS templates. +Look at [Template inheritance](../templates/Template_Inheritance) for more information on how to override templates. ## API documentation 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 d75727bd1..a2f5e5be9 100644 --- a/en/02_Developer_Guides/14_Files/03_File_Security.md +++ b/en/02_Developer_Guides/14_Files/03_File_Security.md @@ -457,10 +457,10 @@ see [Server Requirements: Secure Assets](/getting_started/server_requirements#se If the default server configuration is not appropriate for your specific environment, then you can further customise the `.htaccess` or `web.config` by editing one or more of the below: -- `PublicAssetAdapter_HTAccess.ss`: Template for public permissions on the Apache server. -- `PublicAssetAdapter_WebConfig.ss`: Template for public permissions on the IIS server. -- `ProtectedAssetAdapter_HTAccess.ss`: Template for the protected store on the Apache server (should deny all requests). -- `ProtectedAssetAdapter_WebConfig.ss`: Template for the protected store on the IIS server (should deny all requests). +- `PublicAssetAdapter_HTAccess`: Template for public permissions on the Apache server. +- `PublicAssetAdapter_WebConfig`: Template for public permissions on the IIS server. +- `ProtectedAssetAdapter_HTAccess`: Template for the protected store on the Apache server (should deny all requests). +- `ProtectedAssetAdapter_WebConfig`: Template for the protected store on the IIS server (should deny all requests). Each of these files will be regenerated on `?flush`, so it is important to ensure that these files are overridden at the template level, not via manually generated configuration files. @@ -469,7 +469,7 @@ overridden at the template level, not via manually generated configuration files In order to ensure that public files are served correctly, you should check that your `assets/.htaccess` bypasses PHP requests for files that do exist. The default template -(declared by `PublicAssetAdapter_HTAccess.ss`) has the following section, which may be customised in your project: +(declared by the `PublicAssetAdapter_HTAccess` template) has the following section, which may be customised in your project: ```text # Non existent files passed to requesthandler 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 9b3c4176c..294e1602b 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 @@ -134,15 +134,15 @@ If you have the `cms` module installed, have a look at [`CMSMain::getEditForm()` example on how to extend the base functionality (e.g. by adding page versioning hints to the form). CMS templates are inherited based on their controllers, similar to subclasses of -the common `Page` object (a new PHP class `MyPage` will look for a `MyPage.ss` template). -We can use this to create a different base template with `LeftAndMain.ss` +the common `Page` object (a new PHP class `MyPage` will look for a `MyPage` template). +We can use this to create a different base template called `LeftAndMain` (which corresponds to the `LeftAndMain` PHP controller class). In case you want to retain the main CMS structure (which is recommended), -just create your own "Content" template (e.g. `MyCMSController_Content.ss`), +just create your own "Content" template (e.g. `MyCMSController_Content`), which is in charge of rendering the main content area apart from the CMS menu. Depending on the complexity of your layout, you'll also need to override the -"EditForm" template (e.g. `MyCMSController_EditForm.ss`), e.g. to implement +"EditForm" template (e.g. `MyCMSController_EditForm`), e.g. to implement a tabbed form which only scrolls the main tab areas, while keeping the buttons at the bottom of the frame. This requires manual assignment of the template to your form instance, see [`CMSMain::getEditForm()`](api:SilverStripe\CMS\Controllers\CMSMain::getEditForm()) for details. @@ -152,7 +152,7 @@ In this case, you can either override the full base template as described above. To avoid duplicating all this template code, you can also use the special [`LeftAndMain::Tools()`](api:SilverStripe\Admin\LeftAndMain::Tools()) and [`LeftAndMain::EditFormTools()`](api:SilverStripe\Admin\LeftAndMain::EditFormTools()) methods available in `LeftAndMain`. These placeholders are populated by auto-detected templates, -with the naming convention of `<controller classname>_Tools.ss` and `<controller classname>_EditFormTools.ss`. +with the naming convention of `<controller classname>_Tools` and `<controller classname>_EditFormTools`. So to add or "subclass" a tools panel, simply create this file and it's automatically picked up. ## Layout and panels @@ -176,7 +176,7 @@ For example, the "EditForm" has specific view and logic JavaScript behaviour which can be enabled via adding the "CMS-edit-form" class. In order to set the correct layout classes, we also need a custom template. To obey the inheritance chain, we use `$this->getTemplatesWithSuffix('_EditForm')` for -selecting the most specific template (so `MyAdmin_EditForm.ss`, if it exists). +selecting the most specific template (so a template named `MyAdmin_EditForm`, if it exists). The form should use a `LeftAndMainFormRequestHandler`, since it allows the use of a `PjaxResponseNegotiator` to handle its display. @@ -690,7 +690,7 @@ content is occupied by the main content area. jQuery assumes a common parent in the DOM for both the tab navigation and its target DOM elements. In order to achieve this level of flexibility, most tabsets in the CMS use a custom template which leaves rendering the tab navigation to -a separate template: `CMSMain.ss`. See the "Forms" section above +a separate template named `CMSMain`. See the "Forms" section above for an example form. Here's how you would apply this template to your own tabsets used in the CMS. @@ -725,7 +725,7 @@ Form template with custom tab navigation (trimmed down): </form> ``` -Tabset template without tab navigation (e.g. `CMSTabset.ss`) +Tabset template without tab navigation (e.g. a template named `CMSTabset`) ```ss <div $AttributesHTML> 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 183b7d4c2..ebff2cc64 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 @@ -272,27 +272,6 @@ list of items and indirectly calling `forTemplate` using the [`$Me` template var This method will be used by the `cmsPreview` action in the `MyAdmin` class to tell the CMS what to display in the preview panel. -The `forTemplate` method will probably look something like this: - -```php -namespace App\Model; - -use SilverStripe\ORM\CMSPreviewable; -use SilverStripe\ORM\DataObject; - -class Product extends DataObject implements CMSPreviewable -{ - // ... - - public function forTemplate(): string - { - // If the template for this DataObject is not an "Include" template, use the appropriate type here - // e.g. "Layout". - return $this->renderWith(['type' => 'Includes', self::class]); - } -} -``` - #### The `ModelAdmin` implementation We need to add the `cmsPreview` action to the `MyAdmin` class, which will output the @@ -388,7 +367,6 @@ class MyAdmin extends ModelAdmin > namespace App\Admin; > > use SilverStripe\Admin\ModelAdmin; -> use SilverStripe\Model\ArrayData; > use SilverStripe\View\Requirements; > use SilverStripe\View\SSViewer; > @@ -405,7 +383,7 @@ class MyAdmin extends ModelAdmin > // 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])); +> $preview = SSViewer::create('PreviewBase')->process(['Preview' => $preview]); > > // ... ommitted for brevity > } 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 4b716595b..75c44fc58 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 @@ -25,13 +25,13 @@ directory (usually `app/templates/`), it'll take priority over the built-in one. CMS templates are inherited based on their controllers, similar to subclasses of -the common `Page` object (a new PHP class `MyPage` will look for a `MyPage.ss` template). -We can use this to create a different base template with `LeftAndMain.ss` +the common `Page` object (a new PHP class `MyPage` will look for a template named `MyPage`). +We can use this to create a different base template named `LeftAndMain` (which corresponds to the `LeftAndMain` PHP controller class). -Copy the template markup of the base implementation at `templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList.ss` -from the `silverstripe/admin` module -into `app/templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList.ss`. It will automatically be picked up by +Copy the template markup of the base implementation in the `templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList` +template from the `silverstripe/admin` module +into a new `app/templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList` template. It will automatically be picked up by the CMS logic. Add a new section into the `<ul class="cms-menu__list">` ```ss @@ -173,7 +173,7 @@ SilverStripe\Admin\LeftAndMain: ``` As the last step, replace the hardcoded links with our list from the database. -Find the `<ul>` you created earlier in `app/templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList.ss` +Find the `<ul>` you created earlier in the `app/templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList` template and replace it with the following: ```ss diff --git a/en/08_Changelogs/6.0.0.md b/en/08_Changelogs/6.0.0.md index bf512feef..10ade37e1 100644 --- a/en/08_Changelogs/6.0.0.md +++ b/en/08_Changelogs/6.0.0.md @@ -462,6 +462,62 @@ Note that the `SilverStripe\View\ViewableData` class has been renamed to [`Silve See [many renamed classes](#renamed-classes) for more information about this change. +#### Improved separation between the view and model layers {#view-layer-separation} + +Historically the `ModelData` class did double-duty as being the base class for most models as well as being the presumed class wrapping data for the template layer. Part of this included methods like `XML_val()` being called on any object in the template layer, despite being methods very specifically implemented on `ModelData`. + +Any data that wasn't wrapped in `ModelData` was hit-and-miss as to whether it would work in the template layer, and whether the *way* you can use it is consistent. It also meant the `ModelData` class had some complexity it didn't necessarily need to represent a model. + +To improve the separation between the view and model layers (and in some cases as quality-of-life improvements), we've made the following changes: + +- Added a new [`ViewLayerData`](api:SilverStripe\View\ViewLayerData) class which sits between the template layer and the model layer. All data that gets used in the template layer gets wrapped in a `ViewLayerData` instance first. This class provides a consistent API and value lookup logic so that all data gets treated the same way once it's in the template layer. +- Move casting logic into a new [`CastingService`](api:SilverStripe\View\CastingService) class. This class is responsible for casting data to the correct model (e.g. by default strings get cast to [`DBText`](api:SilverStripe\ORM\FieldType\DBText) and booleans get cast to [`DBBoolean`](api:SilverStripe\ORM\FieldType\DBBoolean)). If the source of the data is known and is an instance of `ModelData`, the casting service calls [`ModelData::castingHelper()`](api:SilverStripe\Model\ModelData:castingHelper()) to ensure the [`ModelData.casting`](api:SilverStripe\Model\ModelData->casting) configuration and (in the case of `DataObject`) the db schema are taken into account. + - Native indexed PHP arrays can now be passed into templates and iterated over with `<% loop $MyArray %>`. Under the hood they are wrapped in [`ArrayList`](api:SilverStripe\Model\List\ArrayList), so you can get the count using `$Count` and use `<% if $MyArray %>` as a shortcut for `<% if $MyArray.Count %>`. Other functionality from `ArrayList` such as filtering and sorting cannot be used on these arrays since they don't have keys to filter or sort against. +- Implemented a default [`ModelData::forTemplate()`](api:SilverStripe\Model\ModelData::forTemplate()) method which will attempt to render the model using templates named after it and its superclasses. See [`forTemplate` and `$Me`](/developer_guides/templates/syntax/#fortemplate) for information about this method's usage. + - [`ModelDataCustomised::forTemplate()`](SilverStripe\Model\ModelDataCustomised::forTemplate()) explicitly uses the `forTemplate()` method of the class being customised, not from the class providing the customisation. +- The `ModelData::XML_val()` method has been removed as it is no longer needed to get values for usage in templates. +- Arguments are now passed into getter methods when invoked in templates. For example, if a model has a `getMyField(..$args)` method and `$MyField(1,2,3)` is used in a template, the args `1, 2, 3` will be passed in to the `getMyField()` method. + - For parity, the [`ModelData::obj()`](api:SilverStripe\Model\ModelData::obj()) method now also passes arguments into getter methods. Note however that this method is no longer used to get values in the template layer. +- Values from template variables are passed into functions when used as arguments + - For example, `$doSomething($Title)` will pass the value of the `Title` property into the `doSomething()` method. See [template syntax](/developer_guides/templates/syntax/#variables) documentation for more details. +- The [`ModelData::objCacheSet()`](api:SilverStripe\Model\ModelData::objCacheSet()) and [`ModelData::objCacheGet()`](api:SilverStripe\Model\ModelData::objCacheGet()) methods now deal with raw values prior to being cast. This is so that `ViewLayerData` can use the cache reliably. +- Nothing in core or supported modules (except for the template engine itself) relies on absolute file paths for templates - instead, template names and relative paths (without the `.ss` extension) are used. + - [`Email::getHTMLTemplate()`](SilverStripe\Control\Email\Email::getHTMLTemplate()) now returns an array of template candidates, unless a specific template was set using `setHTMLTemplate()`. + - `ThemeResourceLoader::findTemplate()` has been removed without a replacement. + - `SSViewer::chooseTemplate()` has been removed without a replacement. +- `TemplateEngine` classes will throw a [`MissingTemplateException`](api:SilverStripe\View\Exception\MissingTemplateException) if there is no file mapping to any of the template candidates passed to them. +- The [`Email::setHTMLTemplate()`](api:SilverStripe\Control\Email\Email::setHTMLTemplate()) and [`Email::setPlainTemplate()`](api:SilverStripe\Control\Email\Email::setPlainTemplate()) methods used to strip the `.ss` extension off strings passed into them. They no longer do this. You should double check any calls to those methods and remove the `.ss` extension from any strings you're passing in, unless those strings represent full absolute file paths. + +> [!WARNING] +> If you were overriding `ModelData::XML_val()` or `ModelData::obj()` to influence values used in the template layer, you will need to try an alternative way to alter those values. +> Best practice is to implement getter methods in most cases - but as a last resort you could implement a subclass of `ViewLayerData` and replace it using the injector. + +If you have set the [`ModelData.default_cast`](api:SilverStripe\Model\ModelData->default_cast) configuration property for some model, consider unsetting this so that the relevant `DBField` instance is chosen based on the type of the value, and use `ModelData.casting` if some specific fields need to be cast to non-default classes. + +#### Abstraction of template rendering {#view-layer-abstraction} + +The [`SSViewer`](api:SilverStripe\View\SSViewer) class previously had two duties: + +1. Act as the barrier between the template layer and the model layer +1. Actually process and render templates + +This made that class difficult to maintain. More importantly, it made it difficult to use other template rendering solutions with Silverstripe CMS since the barrier between the two layers was tightly coupled to the ss template rendering solution. + +The template rendering functionality has now been abstracted. `SSViewer` still acts as the barrier between the model and template layers, but it now delegates rendering templates to an injectable [`TemplateEngine`](api:SilverStripe\View\TemplateEngine). + +`TemplateEngine` is an interface with all of the methods required for `SSViewer` and the rest of Silverstripe CMS to interact reliably with your template rendering solution of choice. For now, all of the templates provided in core and supported modules will use the familiar ss template syntax and the default template engine will be [`SSTemplateEngine`](api:SilverStripe\View\SSTemplateEngine) - but the door is now open for you to experiment with alternative template rendering solutions if you want to. + +There are various ways to declare which rendering engine to use, which are explained in detail in the [swap template engines](/developer_guides/templates/swap_template_engines/) documentation. + +Some API which used to be on `SSViewer` is now on `SSTemplateEngine`, and some has been outright removed. The common ones are listed here, but see [full list of removed and changed API](#api-removed-and-changed) below for the full list. + +- The `SSViewer.global_key` configuration property is now [`SSTemplateEngine.global_key`](api:SilverStripe\View\SSTemplateEngine->global_key). +- `SSViewer::chooseTemplate()` has been removed without a replacement. +- `SSViewer::hasTemplate()` is now [`TemplateEngine::hasTemplate()`](api:SilverStripe\View\TemplateEngine::hasTemplate()). +- `SSViewer::fromString()` and the `SSViewer_FromString` class have been replaced with [`TemplateEngine::renderString()`](api:SilverStripe\View\TemplateEngine::renderString()). + +If you want to just keep using the ss template syntax you're familiar with, you shouldn't need to change anything (except as specified in other sections or if you were using API that has moved or been removed). + #### Strong typing for `ModelData` and `DBField` Many of the properties and methods in `ModelData`, [`DBField`](api:SilverStripe\ORM\FieldType\DBField), and their immediate subclasses have been given strong typehints. Previously, these only had typehints in the PHPDoc which meant that any arbitrary values could be assigned or returned. @@ -527,12 +583,12 @@ You can change the level of entropy required by passing one of the valid [minSco ### Other new features -- Native indexed PHP arrays can now be passed into templates and iterated over with `<% loop $MyArray %>`. Under the hood they are wrapped in [`ArrayList`](api:SilverStripe\Model\List\ArrayList), so you can get the count using `$Count` and use `<% if $ArrayList %>` as a shortcut for `<% if $ArrayList.Count %>`. Other functionality from `ArrayList` such as filtering and sorting cannot be used on arrays since they don't have keys to filter or sort against. - Modules no longer need to have a root level `_config.php` or `_config` directory to be recognised as a Silverstripe CMS module. They will now be recognised as a module if they have a `composer.json` file with a `type` of `silverstripe-vendormodule` or `silverstripe-theme`. - A new [`DataObject::getCMSEditLink()`](api:SilverStripe\ORM\DataObject::getCMSEditLink()) method has been added, which returns `null` by default. This provides more consistency for that method which has previously been inconsistently applied to various subclasses of `DataObject`. See [managing records](/developer_guides/model/managing_records/#getting-an-edit-link) for more details about providing sane values for this method in your own subclasses. - The `CMSEditLink()` method on many `DataObject` subclasses has been renamed to `getCMSEditLink()`. - The [`UrlField`](api:SilverStripe\Forms\UrlField) class has some new API for setting which protocols are allowed for valid URLs. - The [`EmailField`](api:SilverStripe\Forms\EmailField) class now uses `symfony/validator` to handle its validation logic, where previously this was validated with a custom regex. +- [`ArrayData`](api:SilverStripe\Model\ArrayData) can now be serialised using [`json_encode()`](https://www.php.net/manual/en/function.json-encode.php). ## Dependency changes