6.0.0 (unreleased)
Overview
-
- Validation added to DBFields
- Changes to
sake
,BuildTask
, CLI interaction in general - Read-only replica database support
- Run
CanonicalURLMiddleware
in all environments by default - Changes to default cache adapters
- Changes to scaffolded form fields
SiteTree
uses form field scaffolding- Filter within a range with
searchable_fields
- Changes to the templating/view layer
- Changes to
LeftAndMain
and its subclasses - Changes to password validation
- Status flags in the CMS
- New
Versioned
methods - New default front-end theme
- Other new features
- Bug fixes
-
- Many renamed classes
- GraphQL removed from the CMS
FormField
classes now useFieldValidator
for validationCMSMain
search filter changes- Most extension hook methods are now protected
- Changes to some extension hook names
FormField::Value()
split into two methodsController::has_curr()
removed- Strict typing for
Factory
implementations - Elemental
TopPage
class names changed - List interface changes
- General changes
-
- PHP version support
- MySQL 5 no longer supported
- MySQL now defaults to utf8mb4
- Session and authentication cookie changes
- TinyMCE is in a separate module now
DBDecimal
default valueRedirectorPage
validation- Update JS MIME type, remove
type
in<script>
tags getSchemaDataDefaults()
now includes attributes- Remember me token rotation
- Validation exceptions shows additional info in some contexts
silverstripe/campaign-admin
no longer part ofsilverstripe/recipe-cms
$CurrentPageURL
template support removed
- Full list of removed and changed API (by module, alphabetically)
Change to commercially supported modules
Some Silverstripe CMS modules are commercially supported. Silverstripe commits to looking after those modules for the duration of the Silverstripe CMS 6 lifecycle.
Review the list of Commercially Supported Modules for Silverstripe CMS 6.
Modules losing commercial support
Some modules that were commercially supported in Silverstripe CMS 5 are not supported in Silverstripe CMS 6. Some of those modules provide CMS6-compatible versions. Others have been dropped altogether.
Just because a module is not "commercially supported", doesn't mean that you shouldn't be using it. Community supported modules are maintained on a "best-effort" basis. You should take this into consideration when choosing to install a community supported module in your project.
Email community@silverstripe.org if you are keen to maintain some of the modules that are no longer commercially supported.
The code in silverstripe/externallinks
, silverstripe/security-report
, and silverstripe/sitewidecontent-report
has been copied into silverstripe/reports
and will be maintained there going forward. The namespaces for classes in those modules has been updated to SilverStripe\Reports
. Note that any code that related to silverstripe/subsite
or silverstripe/contentreview
integration has been removed.
CMS 6 compatible versions of silverstripe/blog
, silverstripe/subsites
, and silverstripe/crontask
have been released with CMS 6.0.0, though they are not commercially supported.
New supported modules
Some new modules have been created and are now covered by our supported modules policy. These modules will be supported for the full lifetime of Silverstripe CMS 6.
To continue using TinyMCE as an HTML editor in your existing project, you must add silverstripe/htmleditor-tinymce
as an explicit dependency.
silverstripe/htmleditor-tinymce
. See TinyMCE is in a separate module now for more details.silverstripe/startup-theme
. See new default front-end theme below for more details.silverstripe/template-engine
module. See abstraction of template rendering for more details.
Features and enhancements
Validation added to DBField
s
DBField
is the base class for all database fields in Silverstripe CMS. For instance when you defined 'MyField' => 'Varchar(255)'
in your DataObject
subclass, the MyField
property would be an instance of DBVarchar
.
Validation has been added to most DBField
subclasses. This means that when a value is set on a DBField
subclass, it will be validated against the constraints of that field. This field validation is called as part of DataObject::validate()
which itself is called as part of DataObject::write()
. If a value is invalid then a ValidationException
will be thrown.
For example, if you have a Varchar(64)
, and you try to set a value longer than 64 characters, an exception will now be thrown. Previously, the value would be truncated to 64 characters and saved to the database.
The validation is added through subclasses of the new FieldValidator
abstract class, for instance the StringFieldValidator
is used to validate DBVarchar
.
Note that this new DBField
validation is independent of the existing CMS form field validation that uses methods such as FormField::validate()
and DataObject::getCMSCompositeValidator()
.
Updates have been made to some setValue()
methods of DBField
subclasses to convert the value to the correct type before validating it.
DBBoolean
uses thetinyint
data type and retains the legacy behaviour of convertingtrue/'true'/'t'/'1'
to1
,false/'false'/'f'/'0'
to0
.DBDecimal
will convert numeric strings as well as integers to floats.DBForeignKey
will convert a blank string to 0.DBInt
will convert integer like strings to integers.DBYear
will convert integer like strings to integers. Also shorthand years are converted to full years (e.g. "24" becomes "2024").
In most cases though, the correct scalar type must now be used. For instance it is no longer possible to set an integer value on a DBVarchar
field. You must use a string.
Some new DBField subclasses have been added which will provide validation for specific types of data:
To use these new field types, simply define them in a DataObject subclass:
// app/src/Pages/MyPage.php
namespace App\Pages;
use SilverStripe\CMS\Model\SiteTree;
class MyPage extends SiteTree
{
private static array $db = [
// Values will be validated as an email address
'MyEmail' => 'Email',
// Values will be validated as an IP address
'MyIP' => 'IP',
// Values will be validated as a URL
'MyURL' => 'URL',
];
}
If you have an existing project that uses Varchar fields for email addresses, IP addresses, or URLs, and you switch to using one of the new DBField types, be aware that some of the values in the database may fail validation the next time they are saved which could cause issues for CMS editors.
You may wish to create a BuildTask
that calls DataObject::validate()
on all affected records.
While we have tried to match the validation rules up to what would already have been stored in the database, there is a chance you'll find yourself with pre-existing data which doesn't meet the validation rules, and which therefore causes validation exceptions to be thrown the next time you try to save those records.
If that happens, you may be able to resolve it with one of the following solutions:
- If there is a form field for that database column, update the value in the form to a valid value before saving the record.
- Write a
BuildTask
that updates any invalid values in your database. - While it's generally not recommended, you have the option of disabling validation via the
DataObject.validation_enabled
configuration property.
In addition, the following DBField
subclasses have also had the following changes made to them:
DBBoolean
will no longer convert non-boolean like values to abool
when callingsetValue()
. For instance"Amazing"
will no longer be converted totrue
, instead its value will remain as"Amazing"
and it will throw a validation exception if attempted to be written to the database. There are some boolean-like values, such as"t"
, that will continue to be converted to their boolean equivalents for backwards compatibility.DBCurrency
will no longer parse numeric values contained in a string when callingsetValue()
. For instance"this is 50.29 dollars"
will no longer be converted to"$50.29"
, instead its value will remain as"this is 50.29 dollars"
and it will throw a validation exception if attempted to be written to the database.DBDecimal
will no longer parse numeric values contained in a string when callingsetValue()
unless the string is entirely numeric. For example"This is -123.45"
will no longer be converted to"-123.45"
, instead its value will remain as"This is -123.45"
and it will throw a validation exception if attempted to be written to the database. Fully numeric strings like"-123.45"
will still be cast and not throw validation exceptions.DBPercentage
stores values internally as a float. It will no longer convert values above1.0
such as2.3
to1.0
when callingsetValue()
. Instead, it will keep the value as2.3
. If the value exceeds the value ofDBPercentage::getMaxValue()
, which has a default value of1.0
though can be overriden in a subclass, then it will throw a validation exception if attempted to be written to the database.
Changes to sake
, BuildTask
, and CLI interaction in general
Until now, running sake
on the command line has executed a simulated HTTP request to your Silverstripe CMS project, using the routing and controllers that your web application uses to handle HTTP requests. This resulted in both a non-standard CLI experience and added confusion about when an HTTPRequest
actually represented an HTTP request.
We've rebuilt Sake using symfony/console
- the same package that powers Composer.
Here are some common commands you can run with Sake:
# list available commands
sake # or `sake list`
# list available tasks
sake tasks
# build the database
sake db:build
# flush the cache
sake flush # or use the `--flush` flag with any other command
# get help info about a command (including tasks)
sake <command> --help # e.g. `sake db:build --help`
To reduce upgrade pains we've retained backwards compatability with the legacy syntax for dev/*
routed actions (e.g. sake dev/build flush=1
will still work). This allows you to continue using existing scripts and cron jobs.
This legacy syntax is deprecated, and will stop working in a future major release.
If for some reason you specifically need to access an HTTP route in your project using Sake, you can use the new sake navigate
command, e.g. sake navigate about-us/teams
.
See sake for more information about using and configuring sake
, including how to register your own custom commands.
There is also a new PolyCommand
class which provides a standardised API for code that needs to be accessible from both an HTTP request and CLI. This is used for BuildTask
and other code accessed via /dev/*
as mentioned below.
See PolyCommand
for more details about the PolyCommand
API.
Changes to BuildTask
Change to API
The BuildTask
class is now a subclass of PolyCommand
.
As a result of this, any BuildTask
implementations in your project or module will need to be updated. The upgrade will likely look like this in most cases:
namespace App\Tasks;
-use SilverStripe\Control\Director;
-use SilverStripe\Control\HTTPRequest;
use SilverStripe\Dev\BuildTask;
+use SilverStripe\PolyExecution\PolyOutput;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
class MyCustomTask extends BuildTask;
{
- private static $segment = 'my-custom-task';
+ protected static string $commandName = 'my-custom-task';
- protected $title = 'My custom task';
+ protected string $title = 'My custom task';
- protected $description = 'A task that does something custom';
+ protected static string $description = 'A task that does something custom';
- public function run(HTTPRequest $request)
+ protected function execute(InputInterface $input, PolyOutput $output): int
{
+ if ($input->getOption('do-action')) {
- if ($request->getVar('do-action')) {
- if (Director::is_cli()) {
- echo "Doing something...\n"
- } else {
- echo "Doing something...<br>\n";
- }
+ $output->writeln('Doing something...');
}
- echo "Done\n";
+ return Command::SUCCESS;
}
+
+ public function getOptions(): array
+ {
+ return [
+ new InputOption('do-action', null, InputOption::VALUE_NONE, 'do something specific'),
+ ];
+ }
}
Note that you should no longer output "done" or some equivalent message at the end of the task. Any time a task finishes executing, the output will include a message about whether the task completed successfully or failed (based on the return value) and how long it took.
See PolyCommand
for more details about the BuildTask
API.
Change to names
Some tasks were relying on the FQCN instead of having an explicit name. This means the name used for both the URL and the CLI command were excessively long.
The way these are displayed in the new sake tasks
list doesn't suit long names, so we have given explicit names to these tasks in order to make them shorter. If you have scripts or cron jobs that reference these you will need to update them to use the new name.
For example, you used to navigate to /dev/tasks/<OLD_NAME>
or use sake dev/tasks/<OLD_NAME>
. Now you will need to navigate to /dev/tasks/<NEW_NAME>
or use sake tasks:<NEW_NAME>
.
Class | Old name | New name |
---|---|---|
ContentReviewEmails | SilverStripe-ContentReview-Tasks-ContentReviewEmails | content-review-emails |
DeleteAllJobsTask | Symbiote-QueuedJobs-Tasks-DeleteAllJobsTask | delete-queued-jobs |
MigrateContentToElement | DNADesign-Elemental-Tasks-MigrateContentToElement | elemental-migrate-content |
StaticCacheFullBuildTask | SilverStripe-StaticPublishQueue-Task-StaticCacheFullBuildTask | static-cache-full-build |
Changes to /dev/*
actions
With the changes to sake
come changes to the way dev/*
actions are handled. Most of these are now subclasses of the new DevCommand
class which is itself a subclass of PolyCommand
.
One small change as a result of this is the dont_populate
parameter for dev/build
and for the new db:build
CLI command has been deprecated. Use no-populate
instead. For example use https://example.com/dev/build/?no-populate=1
and sake db:build --no-populate
.
Registering dev/*
commands
If you have custom actions registered under DevelopmentAdmin.registered_controllers
you'll need to update the YAML configuration for these. If you want them to be accessible via CLI, you'll also have to update the PHP code.
With the below example, there are two custom actions displayed in the list at /dev
:
/dev/my-http-only-action
: intended for use in the browser only, but you'd have to add custom logic ininit()
to disallow its use in CLI until now/dev/my-http-and-cli-action
: intended for use both in CLI and in the browser.
For actions that should only be accessible in the browser, you only need to change how these are registered. Move them from DevelopmentAdmin.registered_controllers
to the new DevelopmentAdmin.controllers
configuration property.
Controllers added to DevelopmentAdmin.controllers
can only be accessed via HTTP requests, so you can remove any logic around CLI usage.
For actions that should be accessible in the browser and via CLI, you will need to change these from being a Controller
to subclassing DevCommand
. These get registered to the new DevelopmentAdmin.commands
configuration property
SilverStripe\Dev\DevelopmentAdmin:
- registered_controllers:
- my-http-only-action:
- controller: 'App\Dev\MyHttpOnlyActionController'
- links:
- my-http-only-action: 'Perform my custom action in dev/my-http-only-action (do not run in CLI)'
- my-http-and-cli-action:
- controller: 'App\Dev\MyHttpAndCliActionController'
- links:
- my-http-and-cli-action: 'Perform my custom action in dev/my-http-and-cli-action'
+ controllers:
+ my-http-only-action:
+ class: 'App\Dev\MyHttpOnlyActionController'
+ description: 'Perform my custom action in dev/my-http-only-action'
+ commands:
+ my-http-and-cli-action: 'App\Dev\MyHttpAndCliActionCommand'
namespace App\Dev;
-use SilverStripe\Control\Controller;
-use SilverStripe\Control\Director;
-use SilverStripe\Control\HTTPRequest;
+use SilverStripe\Dev\Command\DevCommand;
-use SilverStripe\Dev\DevelopmentAdmin;
+use SilverStripe\PolyExecution\PolyOutput;
-use SilverStripe\Security\Permission;
use SilverStripe\Security\PermissionProvider;
-use SilverStripe\Security\Security;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
-class MyHttpAndCliActionController extends Controller implements PermissionProvider
+class MyHttpAndCliActionController extends DevCommand implements PermissionProvider
{
+ protected static string $commandName = 'app:my-http-and-cli-action';
+
+ protected static string $description = 'Perform my custom action in dev/my-http-and-cli-action or via sake app:my-http-and-cli-action';
+
+ private static array $permissions_for_browser_execution = [
+ 'MY_CUSTOM_PERMISSION',
+ ];
+
+ public function getTitle(): string
+ {
+ return 'My other action';
+ }
+
- protected function init(): void
- {
- parent::init();
-
- if (!$this->canInit()) {
- Security::permissionFailure($this);
- }
- }
-
- public function index(HTTPRequest $request)
+ protected function execute(InputInterface $input, PolyOutput $output): int
{
- $someVar = $request->getVar('some-var');
+ $input->getOption('some-var');
- if (Director::is_cli()) {
- $body = "some output\n";
- } else {
- $body = "some output<br>\n";
- }
+ $output->writeln('some output');
-
- return $this->getResponse()->setBody($body);
+ return Command::SUCCESS;
}
+ public function getOptions(): array
+ {
+ return [
+ new InputOption('some-var', null, InputOption::VALUE_NONE, 'some get variable'),
+ ];
+ }
- public function canInit(): bool
- {
- return (
- Director::isDev()
- || (Director::is_cli() && DevelopmentAdmin::config()->get('allow_all_cli'))
- || Permission::check('MY_CUSTOM_PERMISSION')
- );
- }
// ...
}
You would now access the /dev/my-http-only-action
action via an HTTP request only. The /dev/my-http-and-cli-action
action can be access via an HTTP request, or by using sake app:my-http-and-cli-action
on the command line.
The some-var
option can be used in a query string when running the action via an HTTP request, or as a flag (e.g. sake app:my-http-and-cli-action --some-var
) in CLI.
See PolyCommand
for more details about the DevCommand
API.
sake -start
and sake -stop
have been removed
Sake used to have functionality to make daemon processes for your application. This functionality was managed with sake -start my-process
and sake -stop my-process
.
We've removed this functionality. Please use an appropriate daemon tool such as systemctl
to manage these instead.
Read-only replica database support
Read-only replicas are additional databases that are used to offload read queries from the primary database, which can improve performance by reducing the load on the primary database.
Read-only replicas are configured by adding environment variables that match the primary environment variable and suffixing _REPLICA_<replica-number>
to the variable name, where <replica_number>
is the replica number padding by a zero if it's less than 10, for example SS_DATABASE_SERVER
becomes SS_DATABASE_SERVER_REPLICA_01
for the first replica, or SS_DATABASE_SERVER_REPLICA_12
for the 12th replica. Replicas must be numbered sequentially starting from 01
.
Replicas cannot define different configuration values for SS_DATABASE_CLASS
, SS_DATABASE_NAME
, or SS_DATABASE_CHOOSE_NAME
. They are restricted to prevent strange issues that could arise from having inconsistent database configurations across replicas.
If one or more read-only replicas have been configured, then for each request one of the read-only replicas will be randomly selected from the pool of available replicas to handle queries for the rest of the request cycle, unless criteria has been met to use the primary database instead, for example a write operation.
See read-only database replicas for more details.
When replicas are configured, calling the method DB::get_conn()
will now give a replica by default if one is able to be used. To get the primary database connection, call DB::get_conn(DB::CONN_PRIMARY)
instead.
Note that the DB::CONN_PRIMARY
constant, which has a value of "primary", is used to specify the primary database used. Prior to CMS 6 when there was no DB replica support, the primary database was referred to as "default". If you have code that uses the string "default" to refer to the primary database, you should update it to use the DB::CONN_PRIMARY
constant instead.
Note that some DataQuery
methods such as DataQuery::execute()
now work slightly differently as they will use the replica database if the queried DataObject
has the DataObject.must_use_primary_db
configuration set to true
. However calling the equivalent SQLSelect
method via a DataQuery
e.g. $dataQuery->query()->execute()
will not respect the DataObject.must_use_primary_db
configuration.
Run CanonicalURLMiddleware
in all environments by default
In Silverstripe CMS 5 CanonicalURLMiddleware
only runs in production by default. This lead to issues with fetch
and APIs behaving differently in production environments to development. Silverstripe 6.0 changes this default to run the rules in dev
, test
, and live
by default.
To opt out of this change include the following in your _config.php
use SilverStripe\Control\Middleware\CanonicalURLMiddleware;
use SilverStripe\Core\CoreKernel;
CanonicalURLMiddleware::singleton()->setEnabledEnvs([
CoreKernel::LIVE,
]);
Changes to default cache adapters
The DefaultCacheFactory
used to use APCu cache by default for your webserver - but we found this cache isn't actually shared with CLI. This means flushing cache from the CLI didn't actually flush the cache your webserver was using.
What's more, the PHPFilesAdapter
used as a fallback wasn't enabled for CLI, which resulted in having a different filesystem cache for CLI than is used for the webserver.
We've made the following changes to resolve this problem:
- The
PHPFilesAdapter
will only be used if it's available for both the webserver and CLI. Otherwise,FilesystemAdapter
will be used for both. - There is no default in-memory cache used by
DefaultCacheFactory
. - Cache factories have been implemented for Redis, Memcached, and APCu, with a new
InMemoryCacheFactory
interface available for your own implementations.
We strongly encourage you to configure an appropriate in-memory cache for your use-case. See adding an in-memory cache adapter for details.
Changes to scaffolded form fields
Some of the form fields have changed when scaffolding form fields for relations.
Previously, File
, Image
, and Folder
were all scaffolded using UploadField
for has_one
relations, and GridField
for has_many
and many_many
relations.
All other models used SearchableDropdownField
for has_one
relations and GridField
for has_many
and many_many
relations.
Class | has_one | has_many | many_many |
---|---|---|---|
SiteTree | TreeDropdownField | TreeMultiselectField | TreeMultiselectField |
Group | TreeDropdownField | TreeMultiselectField | TreeMultiselectField |
Member | No change | SearchableMultiDropdownField | SearchableMultiDropdownField |
File | No change | UploadField | UploadField |
Image | No change | UploadField | UploadField |
Folder | TreeDropdownField | TreeMultiselectField | TreeMultiselectField |
TaxonomyTerm | No change | SearchableMultiDropdownField | SearchableMultiDropdownField |
Link | LinkField | MultiLinkField | No change |
BlogCategory | No change | TagField | TagField |
BlogTag | No change | TagField | TagField |
Recipient | No change | Changed which GridfieldComponent classes are used | Changed which GridfieldComponent classes are used |
SiteTree
uses form field scaffolding
SiteTree::getCMSFields()
used to create its form fields from scratch, without calling parent::getCMSFields()
. This meant that all subclasses of SiteTree
(i.e. all of your Page
classes) had to explicitly define all form fields.
SiteTree::getCMSFields()
now uses the same form field scaffolding that all other DataObject
subclasses use.
Note that this means when you initially upgrade to Silverstripe CMS 6 you may have form fields being added to your CMS edit forms that you don't want to include, or tabs from relations that you don't want. You can use the scaffold_cms_fields_settings
configuration property to change which fields are being scaffolded.
For example, if you have a database column for which you don't want content authors to see or edit the value, you can use the ignoreFields
option to stop the form field for that column from being scaffolded:
namespace App\PageTypes;
use Page;
class MyCustomPage extends Page
{
// ...
private static array $db = [
'SecretToken' => 'Varchar',
];
private static array $scaffold_cms_fields_settings = [
'ignoreFields' => [
'SecretToken',
],
];
}
You might currently have form fields that are only added if some condition is true, for example if the user has certain permissions. If you change nothing, those fields will likely display for all users editing the record. You can make the fields only appear conditionally by doing one of the following:
- Reverse the condition - if you are currently adding a field if some condition is true, change your logic so you instead remove the scaffolded form field if that condition is false.
- Use the
scaffold_cms_fields_settings
configuration to ensure those form fields are not scaffolded. You can then leave your logic as is for those fields.
See the scaffolding section for more details about using these options.
As part of your CMS 6 upgrade, you should check all of the page types in your project and in any modules you maintain to ensure the correct form fields are available in the appropriate tabs. You should also check Extension
subclasses that you know get applied to pages to ensure fields aren't being scaffolded from those that you want to keep hidden.
What if I don't have time to upgrade all of my page types?
If you have a lot of complex page types and extensions, upgrading all of them to account for the new scaffolding might be a large task. If you want to avoid upgrading your getCMSFields()
and updateCMSFields()
implementations initially, you can use the restrictRelations
and restrictFields
scaffolding options in the scaffold_cms_fields_settings
configuration property for your pages. You can then declare that only the fields introduced in parent classes should be scaffolded.
The below YAML configuration can be used as a base for this workaround. It will work for all page types available in commercially supported modules. If you use page types provided in third-party modules, you may need to add configuration for those as well.
Note that this is explicitly intended as a temporary workaround, so that you can focus on other areas of the upgrade first, and come back to your page form fields later.
As more community modules are upgraded to account for form field scaffolding in their page types and extension classes, you may
need to add more fields to this list. To avoid having to continuously update these lists it's recommended that you take the time
to update your getCMSFields()
and updateCMSFields()
implementations as soon as you have time to do so.
Click to see the YAML configuration snippet
SilverStripe\CMS\Model\SiteTree:
scaffold_cms_fields_settings:
restrictRelations:
# This will stop all has_many and many_many relations from being
# scaffolded except for new relations which are added to this list
- 'ThisRelationDoesntExist'
restrictFields:
# These fields are scaffolded from SiteTree, and are the bare minimum
# fields that we need to be scaffolded for all page types
- 'Title'
- 'MenuTitle'
- 'URLSegment'
- 'Content'
SilverStripe\CMS\Model\VirtualPage:
scaffold_cms_fields_settings:
restrictFields:
- 'CopyContentFrom'
SilverStripe\CMS\Model\RedirectorPage:
scaffold_cms_fields_settings:
restrictFields:
- 'ExternalURL'
- 'LinkTo'
- 'LinkToFile'
SilverStripe\Blog\Model\BlogPost:
scaffold_cms_fields_settings:
restrictRelations:
- 'Categories'
- 'Tags'
restrictFields:
- 'Summary'
- 'FeaturedImage'
- 'PublishDate'
SilverStripe\IFrame\IFramePage:
scaffold_cms_fields_settings:
restrictFields:
- 'ForceProtocol'
- 'IFrameURL'
- 'IFrameTitle'
- 'AutoHeight'
- 'AutoWidth'
- 'FixedHeight'
- 'FixedWidth'
- 'BottomContent'
- 'AlternateContent'
SilverStripe\UserForms\Model\UserDefinedForm:
scaffold_cms_fields_settings:
restrictRelations:
- 'EmailRecipients'
Filter within a range with searchable_fields
Using the searchable_fields
configuration, you can now declare that a field should be filtered using a range. This works out of the box with the numeric, date, datetime, and time fields that come in the Silverstripe framework.
You can also provide ranges for other field types, but it requires some additional configuration.
namespace App\Model;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Filters\WithinRangeFilter;
class MyDataObject extends DataObject
{
// ...
private static array $db = [
'Price' => 'Currency',
];
private static array $searchable_fields = [
'Price' => WithinRangeFilter::class,
];
}
This configuration will duplicate the form field, providing one form field for the "from" value, and another for the "to" value. Users can then filter within a range using the filters in the CMS.
If a user fills in only the "from" or "to" field, the other will be populated with the minimum or maximum value defined by the relevant DBField
class in getMinValue()
or getMaxValue()
See the searchable fields documentation for more details.
Changes to the templating/view layer
Note that the SilverStripe\View\ViewableData
class has been renamed to SilverStripe\Model\ModelData
. We will refer to it as ModelData
in the rest of these change logs.
See many renamed classes for more information about this change.
Improved separation between the view and model layers
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
class which sits between the template layer and the model layer. All data that gets used in the template layer gets wrapped in aViewLayerData
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
class. This class is responsible for casting data to the correct model (e.g. by default strings get cast toDBText
and booleans get cast toDBBoolean
). If the source of the data is known and is an instance ofModelData
, the casting service callsModelData::castingHelper()
to ensure theModelData.casting
configuration and (in the case ofDataObject
) the db schema are taken into account. -
Implemented a default
ModelData::forTemplate()
method which will attempt to render the model using templates named after it and its superclasses. SeeforTemplate
and$Me
for information about this method's usage.ModelDataCustomised::forTemplate()
explicitly uses theforTemplate()
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. - The
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. - The
ModelData::objCacheSet()
andModelData::objCacheGet()
methods now deal with raw values prior to being cast. This is so thatViewLayerData
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()
now returns an array of template candidates, unless a specific template was set usingsetHTMLTemplate()
.ThemeResourceLoader::findTemplate()
has been removed without a replacement.SSViewer::chooseTemplate()
has been removed without a replacement.
TemplateEngine
classes will throw aMissingTemplateException
if there is no file mapping to any of the template candidates passed to them.- The
Email::setHTMLTemplate()
andEmail::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.
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
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
The SSViewer
class previously had two duties:
- Act as the barrier between the template layer and the model layer
- 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
.
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
. This template engine lives in the new silverstripe/template-engine
module.
Along with making the default template engine easier to maintain, these changes also open the door 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 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 below for the full list.
- The
SSViewer.global_key
configuration property is nowSSTemplateEngine.global_key
. SSViewer::chooseTemplate()
has been removed without a replacement.SSViewer::hasTemplate()
is nowTemplateEngine::hasTemplate()
.SSViewer::fromString()
and theSSViewer_FromString
class have been replaced withTemplateEngine::renderString()
.
Along with those API changes, the following classes and interfaces were moved into the new module:
Old class | New class |
---|---|
SilverStripe\View\SSViewer_BasicIteratorSupport | SilverStripe\TemplateEngine\BasicIteratorSupport |
SilverStripe\View\SSTemplateParseException | SilverStripe\TemplateEngine\Exception\SSTemplateParseException |
SilverStripe\View\SSTemplateParser | SilverStripe\TemplateEngine\SSTemplateParser |
SilverStripe\View\SSViewer_Scope | SilverStripe\TemplateEngine\ScopeManager |
SilverStripe\View\SSViewer_DataPresenter | SilverStripe\TemplateEngine\ScopeManager |
SilverStripe\View\TemplateIteratorProvider | SilverStripe\TemplateEngine\TemplateIteratorProvider |
SilverStripe\View\TemplateParser | SilverStripe\TemplateEngine\TemplateParser |
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).
Changes to template functionality
The following changes have been made to the way templates work.
- Native indexed PHP arrays can now be passed into templates and iterated over with
<% loop $MyArray %>
. Under the hood they are wrapped inArrayList
, so you can get the count using$MyArray.Count
and use<% if $MyArray %>
as a shortcut for<% if $MyArray.Count %>
. Other functionality fromArrayList
such as filtering and sorting cannot be used on these arrays since they don't have keys to filter or sort against. - 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 args1, 2, 3
will be passed in to thegetMyField()
method. -
Values from template variables are passed into functions when used as arguments
- For example,
$doSomething($Title)
will pass the value of theTitle
property into thedoSomething()
method. See template syntax documentation for more details.
- For example,
- The old
<% _t("My_KEY", "Default text") %>
and<% sprintf(_t("My_KEY", "Default text with %s"), "replacement") %>
i18n syntaxes have been removed. Use the syntax described in the i18n documentation instead.
Strong typing for ModelData
and DBField
Many of the properties and methods in ModelData
, 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.
Most of the strong types are either identical to the old PHPDoc types, or match what was already actually being assigned to, passed into, and returned from those APIs.
In some cases, where a string was expected but sometimes null
was being used, we have explicitly strongly typed the API to string
. This matches similar changes PHP made in PHP 8.1 and will help avoid passing null
values in to functions that expect a string.
The one change we specifically want to call out is for ModelData::obj()
. This method will now explicitly return null
if there is no field, property, method, etc to represent the field you're trying to get an object for. This is a change from the old behaviour, where an empty DBField
instance would be returned even though there's no way for any non-null value to be available for that field.
See the full list of removed and changed API to see all of the API with updated typing.
Changes to LeftAndMain
and its subclasses
LeftAndMain
has historically been the superclass for all controllers routed in /admin/*
(i.e. all controllers used in the CMS). It's also the superclass for admin-routed controllers which manage modal forms. That class includes a lot of boilerplate functionality for setting up a menu, edit form, etc which a lot of controllers don't need.
A new AdminController
has been created which provides the /admin/*
routing functionality and permission checks that LeftAndMain
used to be responsible for. If you have a controller which needs to be routed through /admin/*
with the relevant CMS permission checks, but which does not need a menu item on the left or an edit form, you should update that class to be a subclass of AdminController
instead.
The new FormSchemaController
class (which is a subclass of AdminController
) now owns the logic required for injecting and managing forms inside modals.
As a result of these changes, the following classes have had their class hierarchy updated:
Class | Old superclass | New superclass |
---|---|---|
LeftAndMain | Controller | FormSchemaController |
SudoModeController | LeftAndMain | AdminController |
ElementalAreaController | CMSMain | FormSchemaController |
HistoryViewerController | LeftAndMain | FormSchemaController |
UserDefinedFormAdmin | LeftAndMain | FormSchemaController |
AdminRegistrationController | LeftAndMain | AdminController |
LinkFieldController | LeftAndMain | FormSchemaController |
SubsiteXHRController | LeftAndMain | AdminController |
CMSExternalLinksController | Controller | AdminController |
The tree_class
configuration property on LeftAndMain
and its subclasses has be renamed to model_class
, and a new getModelClass()
method has been implemented to return it. This is used in methods like getRecord()
to get a record of the correct class.
The getModelClass()
method is the same method used in ModelAdmin
to get the class for the currently accessed tab. This parity means you can predictably call getModelClass()
on any initialised subclass of LeftAndMain
and get an appropriate base class to work with.
Some localisation keys have changed as a result of the refactor, so if you are using the localisation API to update localised text in a LeftAndMain
controller, you may need to update the key for your customisation.
Changes toCMSMain
CMSMain
is a subclass of LeftAndMain
which provides the main way of managing page content in the CMS. It had a lot of hardcoded references to SiteTree
and Page
, despite most of its functionality being compatible with any class that uses the Hierarchy
extension.
The hardcoded references have been removed, along with other assumptions that the model class for CMSMain
was always a SiteTree
. This opens up the way for a future enhancement to introduce a new generalised version of CMSMain
that works with any model using the Hierarchy
extension, which could be implemented in projects similar to how ModelAdmin
is used.
As a result of this, a lot of references to "page" have been removed from both API and UI elements. A full list of API changes can be found at the bottom of the changelog.
Effects of these refactors in other classes
Some classes outside of the LeftAndMain
class hierarchy have also been affected by the refactoring:
- The
SiteTree.need_permission
configuration property has been removed. This wasn't used in permission checks anyway, so these permissions would have had to be separately checked incanCreate()
to have the intended effect. If you were using this configuration property, implement a change tocanCreate()
in yourPage
class instead. - The
SiteTree.description
configuration property has been renamed toclass_description
. This configuration has been added toDataObject
along with the correspondingclassDescription()
andi18n_classDescription()
methods. - The
BaseElement::getTypeNice()
method now callsi18n_classDescription()
to get the text it will display. - The
Hierarchy
extension now has a bunch of configuration and methods which used to be exclusive toSiteTree
.
New SingleRecordAdmin
class and changes to SiteConfig
A new SingleRecordAdmin
class has been created which makes it easier to create an admin section for editing a single record.
This is the new super class for SiteConfigLeftAndMain
and CMSProfileController
.
As part of this change, we have removed the updateCurrentSiteConfig
extension hook on SiteConfig
and updated the canDelete()
permissions on SiteConfig
to explicitly return false
by default, even for administrators.
The getCMSActions()
method of SiteConfig
also no longer returns the save action, as that is handled by the controller which instantiates the edit form. Other actions added through getCMSActions()
(e.g. if you added them through an extension) will still be included.
Changes to some CSS selectors
The jstree-pageicon
CSS class is now jstree-recordicon
, and the page-icon
CSS class is now record-icon
.
Some of the CSS selectors that had been added to the edit forms in SiteConfigLeftAndMain
and CMSProfileController
are no longer available - if you were using CSS selectors in those admin sections, you may need to change the way you're handling that.
Changes to password validation
PasswordValidator
changes
The deprecated SilverStripe\Security\PasswordValidator
class has been renamed to RulesPasswordValidator
and is now optional.
The default password validator is now EntropyPasswordValidator
which is powered by the PasswordStrength
constraint in symfony/validator
. This constraint determines if a password is strong enough based on its entropy, rather than on arbitrary rules about what characters it contains.
You can change the required strength of valid passwords by setting the EntropyPasswordValidator.password_strength
configuration property to one of the valid minScore values:
SilverStripe\Security\Validation\EntropyPasswordValidator:
password_strength: 4
EntropyPasswordValidator
also has the same options for avoiding repeat uses of the same password that RulesPasswordValidator
has.
This does not retroactively affect existing passwords, but will affect any new passwords (e.g. new members or changing the password of an existing member).
If you want to revert to the validator that was used in CMS 5, you can do so with this YAML configuration:
---
After: '#corepasswords'
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Security\Validation\PasswordValidator:
class: 'SilverStripe\Security\Validation\RulesPasswordValidator'
See passwords for more information about password validation.
New sha512
password hasher
The cwp/cwp-core
module provided a password hasher for sha512
specifically for NZISM (New Zealand Information Security Manual) compliance (see HMAC algorithms in the NZISM document).
cwp/cwp-core
is no longer a commercially supported module, however the password hasher has been moved into silverstripe/framework
as PasswordEncryptorPBKDF2
.
This means users whose passwords were hashed using this algorithm will still be able to log into their account after you have upgraded to CMS 6.
cwp/cwp-core
also changed the default hasher, but for CMS 6 the hashing algorithm is blowfish
. If you wish to change the default hashing algorithm to sha512
, you can do so with this YAML snippet:
SilverStripe\Security\Security:
password_encryption_algorithm: 'pbkdf2_sha512'
ConfirmedPasswordField
changes
If ConfirmedPasswordField->requireStrongPassword is set to true, the old behaviour was to validate that at least one digit and one alphanumeric character was included. This meant that you could have a password like "a1" and it would be considered "strong".
This has been changed to use the PasswordStrength
constraint in symfony/validator
instead. Now a password is considered "strong" based on its level of entropy.
You can change the level of entropy required by passing one of the valid minScore values into ConfirmedPasswordField::setMinPasswordStrength()
.
Status flags in the CMS
In CMS 5 the CMS showed status flags for the SiteTree
class inside the /admin/pages
section, and occasionally for other models in grid fields - but this was inconsistent and was done in a variety of different ways depending on what was adding the flags and where they were displayed.
We've standardised this in the new ModelData::getStatusFlags()
method to define the flags, and ModelData::getStatusFlagMarkup()
to build the HTML markup for them. This means that status flags can be displayed for any model in the CMS.
This is already used to show what locale the data is in for models localised using tractorcow/silverstripe-fluent
, and what versioned stage it's in for models using the Versioned
extension.
Status flags are displayed in breadcrumbs at the top of edit forms in the CMS, in the site tree for CMSMain
, for each row in a grid field, and in dnadesign/silverstripe-elemental
and silverstripe/linkfield
.
See status flags for more information.
New Versioned
methods
The following new methods have been added to Versioned
to make it easier to get records in the various versioning states:
updateListToAlsoIncludeStage()
- Gives the same results asget_by_stage()
, but you can pass it a list to modify instead of it giving you a new list.updateListToAlsoIncludeDeleted()
- Gives the same results asget_including_deleted()
, but you can pass it a list to modify instead of it giving you a new list.getRemovedFromDraft()
- Returns a list of records (both published and archived) which have been removed from draft.updateListToOnlyIncludeRemovedFromDraft()
- Gives the same results asgetRemovedFromDraft()
, but you can pass it a list to modify instead of it giving you a new list.getArchivedOnly()
- Returns a list of only records which have been archived.updateListToOnlyIncludeArchived()
- Gives the same results asgetArchivedOnly()
, but you can pass it a list to modify instead of it giving you a new list.
New default front-end theme
New projects created using silverstripe/installer
will notice a new theme silverstripe/startup-theme
which replaces silverstripe-themes/simple
.
The new theme is visually very similar to the old one, but has been built from the ground up with cascading themes and customisability in mind. For example you can customise a lot of the styling by changing values of CSS variables.
The new theme also outputs the $ElementalArea
by default if you have dnadesign/silverstripe-elemental
installed, and exposes the $SilverStripeNavigator
for logged in users.
Most projects will still benefit from a custom built theme, but this new theme should make it easier to test and validate PHP code changes in the early stages of a project and for module development.
Other new features
- 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 acomposer.json
file with atype
ofsilverstripe-vendormodule
orsilverstripe-theme
. - A new
DataObject::getCMSEditLink()
method has been added, which returnsnull
by default. This provides more consistency for that method which has previously been inconsistently applied to various subclasses ofDataObject
. See managing records for more details about providing sane values for this method in your own subclasses. - The
CMSEditLink()
method on manyDataObject
subclasses has been renamed togetCMSEditLink()
. - The
UrlField
class has some new API for setting which protocols are allowed for valid URLs. - The
EmailField
class now usessymfony/validator
to handle its validation logic, where previously this was validated with a custom regex. ArrayData
can now be serialised usingjson_encode()
.- Cloning a
Form
will now recursively clone its validator, fields list, and actions list. - You can now set the
samesite
attribute for all cookies individually. See cookies samesite attribute for details.
Dependency changes
intervention/image
has been upgraded from v2 to v3
We've upgraded from intervention/image
v2 to v3. One of the main improvements included in this upgrade is full support for animated GIFs.
If you are directly interacting with APIs from intervention/image
in your project or module you should check out their upgrade guide.
Animated vs still images
Manipulating animated images takes longer, and results in a larger filesize.
Because of this, the ThumbnailGenerator
will provide still images as thumbnails for animated gifs by default. You can change that for a given instance of ThumbnailGenerator
by passing true
to the setAllowsAnimation()
method. For example, to allow animated thumbnails for UploadField
:
---
After: '#assetadminthumbnails'
---
SilverStripe\Core\Injector\Injector:
SilverStripe\AssetAdmin\Model\ThumbnailGenerator.assetadmin:
properties:
AllowsAnimation: true
The Image::PreviewLink()
method also doesn't allow an animated result by default. This is used in the "Files" admin section, and anywhere you can choose an existing image such as UploadField
and the WYSIWYG file modals.
You can allow animated previews by setting Image.allow_animated_preview
configuration property to true
:
SilverStripe\Assets\Image:
allow_animated_preview: true
You can disable the ability to create animated variants globally by setting decodeAnimation
to false
in the Intervention\Image\ImageManager
's constructor:
SilverStripe\Core\Injector\Injector:
Intervention\Image\ImageManager:
constructor:
decodeAnimation: false
You can also toggle that configuration setting on and off for a given image instance, or create a variant from your image which uses a specific frame of animation - see animated images for details.
Using GD or Imagick
One of the changes that comes as a result of this upgrade is a change in how you configure which manipulation driver (GD or Imagick) to use.
To facilitate upgrades and to ensure we are providing optimal defaults out of the box, if you have the imagick PHP extension installed, it will be used as the driver for intervention/image
by default. If you don't, the assumption is that you have the GD PHP extension installed, and it will be used instead.
See changing the manipulation driver for the new configuration for swapping the driver used by intervention/image
.
New API
The following new methods have been added to facilitate this upgrade:
Method name | Where the method was added |
---|---|
getIsAnimated() | AssetContainer::getIsAnimated() , ImageManipulation::getIsAnimated() (and therefore DBFile , File , and their subclasses), Image_Backend::getIsAnimated() , InterventionBackend::getIsAnimated() |
RemoveAnimation() | ImageManipulation::RemoveAnimation() (and therefore DBFile , File , and their subclasses), Image_Backend::removeAnimation() , InterventionBackend::removeAnimation() |
getAllowsAnimationInManipulations() | Image_Backend::getAllowsAnimationInManipulations() , InterventionBackend::getAllowsAnimationInManipulations() |
setAllowsAnimationInManipulations() | Image_Backend::setAllowsAnimationInManipulations() , InterventionBackend::setAllowsAnimationInManipulations() |
getAllowsAnimation() | ThumbnailGenerator::getAllowsAnimation() |
setAllowsAnimation() | ThumbnailGenerator::setAllowsAnimation() |
Symfony dependencies have been upgraded from v6 to v7
We've upgraded the various Symfony dependencies from v6 to v7.
JavaScript dependencies
Several JavaScript dependencies have been updated, and a few have been replaced.
Notably, react-dnd
has been replaced with dnd-kit
. If you were using react-dnd
and relying on the webpack externals provided by silverstripe/admin
, these are no longer available. We recommend refactoring your code to use dnd-kit
so that your UX is consistent with other drag-and-drop in the CMS.
Bootstrap upgrade
We have upgraded from bootstrap 4 to bootstrap 5. Part of this upgrade also included upgrading reactstrap from 8 to 9 and upgrading popperjs from 1 to 2.
Refer to the following upgrade guides for details about changes you may need to make to styling and JavaScript in your CMS customisations:
Note that while the bootstrap guide mentions removing the styling for the .form-group
, .form-row
, or .form-inline
CSS classes, that styling has been included in the CMS to reduce upgrade pain.
Bug fixes
This release includes a number of bug fixes to improve a broad range of areas. Check the change logs for full details of these fixes split by module. Thank you to the community members that helped contribute these fixes as part of the release!
PaginatedList::count()
used to give the total number of items in the underlying unpaginated list. This wasn't consistent with the number of items in the current page, which is what you get when iterating over the list. Thecount()
method now gives you the number of items in the current page only. If you want to total number of items in the unpaginated list, usePaginatedList::getTotalItems()
instead.
API changes
Many renamed classes
There are a lot of classes which were in the SilverStripe\ORM
namespace and a few in the SilverStripe\View
namespace that simply don't belong there.
We've moved many of these into more suitable namespaces, and in a few cases have taken the opportunity to rename the class to something more appropriate. You will need to update any references to the old Fully Qualified Class Names to use the new names and namespaces instead.
Note that the change from ViewableData
to ModelData
specifically was made to improve the separation between the model layer and the view layer. It didn't make much sense for a class called ViewableData
to be the superclass for all of our model types. The new name better reflects the purpose of this class as the base class for models.
The following classes have been changed when code was moved to the silverstripe/reports
module from some of the modules that lost commercial support:
Validator
and its subclasses have been renamed:
Old FQCN | New FQCN |
---|---|
SilverStripe\Forms\Validator | SilverStripe\Forms\Validation\Validator |
SilverStripe\Forms\RequiredFields | SilverStripe\Forms\Validation\RequiredFieldsValidator |
SilverStripe\Forms\CompositeValidator | SilverStripe\Forms\Validation\CompositeValidator |
SilverStripe\UserForms\Form\UserFormsRequiredFields | SilverStripe\UserForms\Form\UserFormsRequiredFieldsValidator |
Symbiote\AdvancedWorkflow\Forms\AWRequiredFields | Symbiote\AdvancedWorkflow\Forms\AWRequiredFieldsValidator |
GraphQL removed from the CMS
If you need to use GraphQL in your project for public-facing frontend schemas, you can still install and use the silverstripe/graphql
module.
GraphQL has been removed from the admin section of the CMS and is no longer installed when creating a new project using silverstripe/installer
, or an existing project that uses silverstripe/recipe-cms
. All existing functionality in the CMS that previously relied on GraphQL has been migrated to use regular Silverstripe CMS controllers instead.
Any customisations made to the admin
GraphQL schema will no longer work. There are extension hooks available on the new controller endpoints for read operations, for example AssetAdminOpen::apiRead()
that allow you to customise the JSON data returned.
PHP code such as resolvers that were in silverstripe/asset-admin
, silverstripe/cms
and silverstripe/versioned
have been move to the silverstripe/graphql
module and have had their namespace updated. The GraphQL yml config for the versioned module has also been copied over as that was previously enabled by default on all schemas. The GraphQL YAML configuration for the silverstripe/asset-admin
and silverstripe/cms
modules has not been moved as as that was only enabled on the admin schema.
If your project does not have any custom GraphQL, after upgrading you may still have the old .graphql-generated
and public/_graphql
folders in your project. You can safely remove these folders.
FormField
classes now use FieldValidator
for validation
Many of FormField
subclasses in the SilverStripe\Forms
namespace now use FieldValidator
classes for validation, which are also used for DBField
validation. This has meant that much of the old validate()
logic on FormField
subclasses has been removed as it was duplicated in the FieldValidator
classes. Some custom in validate()
methods not found in FieldValidator
classes methods has been retained.
As part of this change, the FormField::validate()
now returns a ValidationResult
object where it used to return a boolean. The $validator
parameter has also been removed. If you have implemented a custom validate()
method in a FormField
subclass, you will need to update it to return a ValidationResult
object instead and remove the $validator
parameter.
The extendValidationResult()
method and the updateValidationResult
extension hook on FormField
have both been removed and replaced with an updateValidate
hook instead, which has a single ValidationResult $result
parameter. This matches the updateValidate
extension hook on DataObject
.
As part of this change method signature of FormField::validate()
changed so that it no longer accepts a parameter, and now returns a ValidationResult
object instead of a boolean.
getCMSValidator
method no longer supported
In a DataObject
subclass, you used to be able to implement a getCMSValidator()
method and return a Validator
for validating edit form submissions in the CMS.
This is no longer supported. Instead, you should override the DataObject::getCMSCompositeValidator()
method as described in validation in the CMS.
CMSMain
search filter changes
The search filter in the "Pages" admin section (aka CMSMain
) is visually similar to the search filters in other sections of the CMS, but has until now had a completely different code path and therefore slightly different functionality.
In CMS 6 that functionality now follows the same code paths used by GridFieldFilterHeader
which powers the search filters in most other sections of the CMS.
This means CMSMain
search filters now rely on SearchContext
to perform the actual filtering, and get their configuration from the searchable_fields
configuration property. Since CMSMain
uses SiteTree
as its model class by default, you'll need to update the searchable_fields
configuration for SiteTree
via YAML configuration if you want to update the fields users can filter against.
Shared functionality for building the search filter form has been moved from CMSMain
and GridFieldFilterHeader
into a new SearchContextForm
class. Some of the HTML markup for the search filters has also been updated so it can be shared across all CMS sections.
One consequence of this is that CMSSiteTreeFilter
and its subclasses are no longer primarily responsible for filtering in the "Pages" admin section. They are still used for filtering by the status (e.g. draft vs modified) of pages. If you had implemented a custom CMSSiteTreeFilter
subclass, you will need to either modify it (if it is intended to provide functionality for filtering by a status) or look into refactoring it into a replacement or extension for the SiteTreeSearchContext
class.
The DNADesign\Elemental\Controllers\ElementSiteTreeFilterSearch
class has also changed - it is now ElementalSiteTreeSearchContext
. All of the configuration that applied to the old class applies to the new one, so you may need to update that name in your configuration. Refer to searching blocks for more information about this class and the performance impacts it brings with it.
Most extension hook methods are now protected
Core implementations of most extension hooks such as updateCMSFields()
now have protected visibility. Formerly they had public visibility which meant they could be called directly which was not how they were intended to be used. Extension hook implementations are still able to be declared public in project code, though it is recommended that all extension hook methods are declared protected in project code to follow best practice.
Changes to some extension hook names
In order to better align the codebase in Silverstripe CMS with best practices, the following extension hook methods have changed name:
Class that defined the hook | Old name | New name |
---|---|---|
Member | afterMemberLoggedIn | onAfterMemberLoggedIn |
Member | afterMemberLoggedOut | onAfterMemberLoggedOut |
Member | authenticationFailed | onAuthenticationFailed |
Member | authenticationFailedUnknownUser | onAuthenticationFailedUnknownUser |
Member | authenticationSucceeded | onAuthenticationSucceeded |
Member | beforeMemberLoggedIn | onBeforeMemberLoggedIn |
Member | beforeMemberLoggedOut | onBeforeMemberLoggedOut |
LeftAndMain | init | onInit |
DataObject | flushCache | onFlushCache |
LostPasswordHandler | forgotPassword | onForgotPassword |
ErrorPage | getDefaultRecords | updateDefaultRecords |
SiteTree | MetaComponents | updateMetaComponents |
SiteTree | MetaTags | updateMetaTags |
DataObject | populateDefaults | onAfterPopulateDefaults |
Member | registerFailedLogin | onRegisterFailedLogin |
DataObject | requireDefaultRecords | onRequireDefaultRecords |
DataObject | validate | updateValidate |
The following were additionally changed to match the renamed methods which invoke them:
Class that defined the hook | Old name | New name |
---|---|---|
CMSMain | updateLinkPageAdd | updateLinkRecordAdd |
CMSMain | updateSiteTreeAsUL | updateTreeAsUL |
CMSMain | updateSiteTreeHints | updateTreeHints |
CMSMain | updateCurrentPageID | updateCurrentRecordID |
The updateCurrentRecordID
extension hook method is now invoked from LeftAndMain
, which is a superclass of CMSMain
.
If you have implemented any of those methods in an Extension
subclass, you will need to rename them for them to continue working.
FormField::Value()
split into two methods
SilverStripe\Forms\FormField::Value()
has been split into to FormField::getValue()
which usually returns an unmodified version of the value, and FormField::getFormattedValue()
which is intended to be modified with things like localisation formatting and will be displayed to users.
If you have overridden the Value()
method in a subclass, you will need to update the method name to one of these, depending on the purpose of the custom functionality. If you use $Value
in a template to get the value of a form field, you will need to update it to $FormattedValue
.
Controller::has_curr()
removed
The SilverStripe\Control\Controller::has_curr()
method has been removed. If you were using this method in your project then you should replace it with a call to Controller::curr()
and checking if it returned null
, meaning if there is no current controller. This is equivalent to checking if has_curr()
had returned false
. Controller::curr()
no longer creates an exception if there is no current controller.
Strict typing for Factory
implementations
The Factory::create()
method now has strict typehinting. The first argument must be a string, and either null
or an object must be returned.
One consequence of this is that you can no longer directly pass an instantiated anonymous class object into Injector::load()
. Instead, you need to get the class name using get_class()
and pass that in as the class.
use App\ClassToReplace;
use SilverStripe\Core\Injector\Injector;
// Use `get_class()` to get the class name for your anonymous class
$replacementClass = get_class(new class () {
private string $property;
public function __construct(string $value = null)
{
$this->property = $value;
}
});
Injector::inst()->load([
ClassToReplace::class => [
'class' => $replacementClass,
],
]);
Elemental TopPage
class names changed
The class names for the TopPage
feature in dnadesign/silverstripe-elemental
did not follow the correct naming convention for Silverstripe CMS. The class names have been changed as follows:
Old name | New name |
---|---|
DNADesign\Elemental\TopPage\DataExtension | DNADesign\Elemental\Extensions\TopPageElementExtension |
DNADesign\Elemental\TopPage\FluentExtension | DNADesign\Elemental\Extensions\TopPageElementFluentExtension |
DNADesign\Elemental\TopPage\SiteTreeExtension | DNADesign\Elemental\Extensions\TopPageSiteTreeExtension |
If you reference any of these classes in your project or module, most likely in config if you have tractorcow/silverstripe-fluent
installed, then you will need to update the references to the new class names.
List interface changes
The SS_List
interface now includes the methods from the Filterable
, Limitable
, and Sortable
interfaces, which have now been removed. This means that any class that implements SS_List
must now also implement the methods from those interfaces.
Many of the methods on SS_List
and the classes that implement it are now strongly typed. This means that you will need to ensure that any custom classes that implement SS_List
have the correct types for the methods that they implement.
As part of these changes ArrayList::find()
will no longer accept an int argument for the $key
param, it will now only accept a string argument.
General changes
DataObject::validate()
now has an explicitValidationResult
return type.DataObject::write()
has a new boolean$skipValidation
parameter. This can be useful for scenarios where you want to automatically create a new record with no data initially without restricting how developers can set up their validation rules.FieldList
is now strongly typed. Methods that previously allowed any iterable variables to be passed, namelyFieldList::addFieldsToTab()
andFieldList::removeFieldsFromTab()
, now require an array to be passed instead.DNADesign\Elemental\Models\BaseElement::getDescription()
and the correspondingDNADesign\Elemental\Models\BaseElement.description
configuration property have been removed. If you were using either of these in your custom elemental blocks, either set the newclass_description
configuration property or override one of thei18n_classDescription()
orgetTypeNice()
methods instead.SilverStripe\ORM\DataExtension
,SilverStripe\CMS\Model\SiteTreeExtension
, andSilverStripe\Admin\LeftAndMainExtension
have been removed. If you subclass any of these classes, you must now subclassExtension
directly instead.- The
SilverStripe\Model\List\ArrayList.default_case_sensitive
configuration property has been removed. This means the default case sensitivity ofArrayList
is now the same as any other list which uses search filters. If you were using that configuration property, or you were relying onArrayList
being case sensitive by default, you should double check that your list filters are working the way you expect. See search filters for details about case sensitivity in search filters. - The execution flow for
ChangePasswordHandler::changepassword()
has changed slightly. The session isn't updated until after the redirect now. If you overrode that method expecting the session to be updated prior to the redirect, you probably want to override the new protectedcreateChangePasswordResponse()
method instead.
Other changes
PHP version support
Silverstripe CMS 6 requires either PHP 8.3 or PHP 8.4. PHP 8.1 and PHP 8.2 which were supported in Silverstripe CMS 5 are no longer supported.
MySQL 5 no longer supported
MySQL 5.6 and 5.7 are no longer supported. The minimum supported version is MySQL 8.0. We support and test against the latest LTS releases of MySQL and MariaDB.
MySQL now defaults to utf8mb4
MySQL will now use utf8mb4
by default rather than plain utf8
. This provides better support for emojis and other special characters.
Depending on when you created your Silverstripe CMS project, you may already be using utf8mb4
as the default encoding. The silverstripe/recipe-core
recipe has included a configuration file setting your database settings to utf8mb4
for a few years.
When upgrading your Silverstripe CMS project, review the app/_config/mysite.yml
file and remove the following lines if they exist:
# UTF8MB4 has limited support in older MySQL versions.
# Remove this configuration if you experience issues.
---
Name: myproject-database
---
SilverStripe\ORM\Connect\MySQLDatabase:
connection_charset: utf8mb4
connection_collation: utf8mb4_unicode_ci
charset: utf8mb4
collation: utf8mb4_unicode_ci
Session and authentication cookie changes
Session and authentication cookies have been updated to be more secure. Regular cookies have not been changed.
The default value of Session.cookie_secure
has been changed from false
to true
, and the default value of CookieAuthenticationHandler->TokenCookieSecure
has been changed from false
to true
. This value corresponds with the cookie Secure
attribute. When this is set to true, the session cookie name will be SECSESSID
for secure HTTPs connections as opposed to the regular session cookie PHPSESSID
which is only used for insecure HTTP connections.
The default value of Session.cookie_samesite
has been changed from Lax
to Strict
, and CookieAuthenticationHandler->tokenCookieSameSite
has been added with a default value of Strict
. This value corresponds to the cookie SameSite
attribute. This has been done as a security measure to help prevent cross-site attacks.
While it's generally not recommended, if you have a requirement to change these values you can do so via YAML:
SilverStripe\Control\Session:
cookie_secure: false
cookie_samesite: 'Lax'
SilverStripe\Core\Injector\Injector:
SilverStripe\Security\MemberAuthenticator\CookieAuthenticationHandler:
properties:
TokenCookieSecure: false
TokenCookieSameSite: 'Lax'
TinyMCE is in a separate module now
To continue using TinyMCE as an HTML editor in your existing project, you must add silverstripe/htmleditor-tinymce
as an explicit dependency.
Silverstripe CMS 6 continues to use TinyMCE 6, the same version used in CMS 5. We chose not to upgrade to TinyMCE 7 because that version of TinyMCE has GPLv2+
license which is incompatible with our BSD-3
license.
Instead, we have moved all TinyMCE functionality used by Silverstripe CMS into a new module - silverstripe/htmleditor-tinymce
. To continue using TinyMCE in your project, you must add that dependency in your composer.json
file.
We plan to replace TinyMCE with a new default HTML editor in a future minor release of Silverstripe CMS. Existing projects will be able to opt-in when it becomes available. Both new and existing projects can continue using TinyMCE for the lifetime of CMS 6.
To facilitate swapping to a new editor, we have abstracted the HTMLEditorConfig
class and related functionality. This change makes it easier to replace TinyMCE with a different HTML editor in the future.
Part of this change includes a new way to define rules such as which elements and attributes are allowed in HTML content. You can continue to set the valid_elements
and extended_valid_elements
configuration if you want to, but the new API is generic and will work for any HTMLEditorConfig
implementation and can be defined in YAML configuration.
See defining HTML editor configurations for details about the new generalised API, and optional features > TinyMCE HTML editor for any TinyMCE-specific documentation.
If you want TinyMCE 7 anyway
If you want to use TinyMCE 7 in your own projects, and have done your due diligence to validate that its new license won't cause you any legal problems, you can theoretically do that.
Note that using TinyMCE 7 with Silverstripe CMS isn't supported and has not been tested. The plugins, skin, and configuration that come with silverstripe/htmleditor-tinymce
are not guaranteed to work with TinyMCE 7.
Configure the TinyMCEConfig
class to use your own copy of TinyMCE by setting the base_dir
configuration property to the directory which holds your TinyMCE JavaScript distribution files. Then define the URL for the silverstripe
skin if you want to use it:
// app/_config.php
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\TinyMCE\TinyMCEConfig;
$module = ModuleLoader::getModule('silverstripe/htmleditor-tinymce');
$skinUrl = $module->getResource('client/dist/tinymce/skins/ui/silverstripe')->getURL();
TinyMCEConfig::get('cms')->setOption('skin_url', $skinUrl);
DBDecimal
default value
Previously if an invalid default value was provided for a DBDecimal
database column, it would silently set the default value to 0
. This will now throw an exception instead, so that you're aware your configured value is invalid and can correct it.
RedirectorPage
validation
RedirectorPage
now uses the Url
constraint from symfony/validator
to validate the ExternalURL
field. It will no longer add http://
to the start of URLs for you if you're missing a protocol - instead, a validation error message will be displayed.
Update JS MIME type, remove type
in <script>
tags
We've updated the MIME type for JavaScript from "application/javascript"
to "text/javascript"
. Additionally, the type
attribute has been omitted from any <script>
tags generated by Silverstripe CMS itself (e.g. in the Requirements API). The most up-to-date RFC says to use "text/javascript"
in HTML5. Since modern browsers will default to that type when one isn't explicitly declared, it is generally encouraged to omit it instead of redundantly setting it.
- Before:
<script type="application/javascript" src="..."></script>
- After:
<script src="..."></script>
This change is generally backward-compatible and should not affect existing functionality. However, if your project explicitly relies on the type
attribute for <script>
tags, you may need to adjust accordingly.
getSchemaDataDefaults()
now includes attributes
The FormField::getSchemaDataDefaults()
method (and by extension the getSchemaData()
method) now calls getAttributes()
. This was done so that attributes such as placeholder
can be used in the react components that render form fields.
In the past it was common to call getSchemaData()
inside the getAttributes()
method, so that a form field rendered in an entwine admin context had the data necessary to bootstrap a react component for that field. Doing that now would result in an infinite recursion loop.
If you were calling getSchemaData()
in your getAttributes()
method in a FormField
subclass include $SchemaAttributesHtml
in your template instead. For example:
-public function getAttributes()
-{
- $attributes = parent::getAttributes();
- $attributes['data-schema'] = json_encode($this->getSchemaData());
- return $attributes;
-}
-<div $AttributesHTML></div>
+<div $AttributesHTML $SchemaAttributesHtml></div>
Remember me token rotation
RememberLoginHash
no longer rotates its token during session renewal, and related config to control this has been removed. This has no functional impact on the Remember Me feature, and resolves some edgecases that could trigger an unexpected logout.
The related onAfterRenewToken
extension hook has been renamed to onAfterRenewSession
, and is triggered at the same logical step in the session renewal process.
Validation exceptions shows additional info in some contexts
ValidationResult
objects created by DataObject::validate()
now includes additional information about validation exceptions including the field name that failed validation, and if applicable the model class and record ID. This additional information will be included in the validation message by ValidationException
if the validation exception happened in a CLI context, or in an HTTP context if the current controller is an instance contained in the ValidationException.show_additional_info_non_cli_controllers
configuration or a subclass. By default only DevelopmentAdmin
and subclasses are configured to show additonal information.
This is done to help developers debug validation issues that happen outside of a UX context more easily, such as validation exceptions that occur during a dev/build
.
The value of the field that failed validation is intentially not included in the output, as it could contain sensitive information e.g. an API token.
silverstripe/campaign-admin
no longer part of silverstripe/recipe-cms
The silverstripe/campaign-admin
module is no longer included in silverstripe/recipe-cms
. If your composer.json
requires silverstripe/recipe-cms
and you wish to continue using silverstripe/campaign-admin
, then you will need to add manually add to your project's composer.json
.
$CurrentPageURL
template support removed
Support for the $CurrentPageURL
template variable, which was previously used to populate email templates with the current page URL, has been removed. This variable was unreliable and is no longer supported.