Upgrade from 4 to 5

A guide for upgrading from 4 to 5. For most sites (those running Laravel > 9), the process will take less than 5 minutes.

Overview

First read through this guide to see if there's anything that you might need to adjust. While there are many items on this page, a majority of them only apply to addons or custom code. We've noted who each item would apply to so you can more easily scan through the changes.

Upgrade using Composer

In your composer.json, change the statamic/cms requirement:

"statamic/cms": "^4.0"
"statamic/cms": "^5.0"

Then run:

composer update statamic/cms --with-dependencies

High impact changes

PHP and Laravel support

Affects apps using PHP < 8.1 or Laravel < 10.

  • The minimum version of PHP is now 8.1.
  • The minimum version of Laravel is now 10.

We highly recommend upgrading all the way to Laravel 11 and PHP 8.3.

Hot Tip!

If you want to (semi-)automate the Laravel upgrade process, we recommend using Laravel Shift (use that link for a special 19.83% discount 🤘).

Site configuration changes

Affects everyone.

Note that Statamic will attempt to migrate this for you automatically during the upgrade.

The site config has been moved from config/statamic/sites.php into resources/sites.yaml. This allows you to manage your sites from the Control Panel.

// config/statamic/sites.php
return [
'sites' => [
'default' => [
'name' => 'First Site',
'url' => config('app.url'),
'locale' => 'en_US',
],
'two' => [
'name' => 'Second Site',
'url' => config('app.url').'/fr/',
'locale' => 'fr_FR',
]
]
];
# resources/sites.yaml
default:
name: First Site
url: '{{ config:app:url }}'
locale: en_US
two:
name: Second Site
url: '{{ config:app:url }}/fr/'
locale: fr_FR

Note: Text direction is now also automatic, based on each site's language. You do not need to migrate direction values to your resources/sites.yaml.

There is now also a new multisite boolean in config/statamic/system.php. Previously, the multi-site feature would be considered "enabled" as soon as you configured a second site. Now there is an explicit option to enable it.

This should be set to true if you previously had 2 or more sites configured.

// config/statamic/system.php
'multisite' => true,

Medium impact changes

Blueprint default value usage

Affects apps that have default defined in their blueprints or fieldsets.

In v4, setting a default value for a field would only make it show up in the respective publish forms. In v5, these values will actually be used where appropriate, such as within front-end templates.

handle: myfield
field:
type: text
default: my default value
title: My Entry
# the "myfield" is missing
{{ if myfield }}yes{{ else }}no{{ /if }}: {{ myfield }}
 
v4 outputs: "no: "
v5 outputs: "yes: my default value"

In most cases, this is what you would have expected to happen anyway.

Site methods

Affects apps or addons using the Site::hasMultiple() method.

Continuing from the multi-site configuration changes above, there are now two separate methods for determining multi-site state.

  • The new Site::multiEnabled() method will return true if the multisite boolean in system.php is enabled.
  • The existing Site::hasMultiple() method will return true if at least two sites are configured.

You should decide whether each existing usage of Site::hasMultiple() should imply that the feature is enabled entirely, or if you need to actually count the number of sites.

Laravel Helpers package has been removed

Affects apps or addons relying on methods from that package in custom code.

The laravel/helpers package provided support for the older global-style string/array/misc functions like array_get, str_contains, snake_case, ends_with, etc. You will now need to either require this package yourself, or update to use the underlying methods.

For example:

namespace App\Example;
 
use Statamic\Support\Arr;
use Statamic\Support\Str;
 
class Example
{
function example()
{
array_get($arr, $key);
Arr::get($arr, $key);
 
str_start($str, $prefix);
Str::start($str, $prefix);
 
ends_with($str, $needle);
Str::endsWith($str, $needle);
}
}

We recommend making the code changes. However, if you just want to require the package:

composer require laravel/helpers

Statamic will now use your app's default pagination view

Affects apps using the auto_links pagination variable in templates.

Previously, Statamic used the pagination::default view to rendering pagination links. In Statamic 5, it will use your app's default pagination view, typically pagination::tailwind.

To avoid making code changes, you may wish to change the default view back to pagination::default:

// app/Providers/AppServiceProvider.php
 
use Illuminate\Pagination\Paginator;
 
public function boot(): void
{
Paginator::defaultView('pagination::default');
}

Updated please file for Laravel 11

Affects apps upgrading to Laravel 11 and the new application skeleton (including all upgrades via Laravel Shift).

Previously, our please command line utility assumed an app/Console/Kernel.php file existed in your application. However, with the introduction of Laravel 11, this file is no longer included with the new application structure.

When upgrading apps to Laravel 11 with the new application skeleton, you'll need to update the please file in your app's root directory:

#!/usr/bin/env php
<?php
 
use Symfony\Component\Console\Input\ArgvInput;
 
define('LARAVEL_START', microtime(true));
 
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
 
// Bootstrap Laravel...
$app = require_once __DIR__.'/bootstrap/app.php';
 
// Rebind the kernel...
Statamic\Console\Please\Application::rebindKernel();
 
// Handle the command...
$status = $app->handleCommand(new ArgvInput);
 
exit($status);

Low impact changes

Regex Antlers Parser has been removed

Affects apps that are still using the regex parser.

The new Antlers parser was introduced in 3.3 and was made the default in 3.4.

You would have been using the old Parser if either of these apply:

  • In config/statamic/antlers.php, the version was set to regex.
  • The antlers.php file doesn't exist at all.

The newer parser should be backwards compatible but if you encounter any errors, you may need to check the docs.

Form submission path config

Affects apps that have customized the submissions path in config/statamic/forms.php.

The place to customize the directory for form submissions has moved from forms.php into stache.php.

// config/statamic/forms.php
'submissions' => 'custom path',
 
// config/statamic/stache.php
'stores' => [
'form-submissions' => [
'directory' => 'custom path',
],
],

Validation rule changes

Affects apps using unique_entry_value, unique_term_value, or unique_user_value rules.

Note that assuming you haven't done anything too unusual, the following changes may have been performed automatically by Statamic during the update process.

We have updated our custom validation rules to use the more modern Laravel syntax. This means dropping the string based aliases in favor of classes.

validate:
- 'unique_entry_value:{collection},{id},{site}'
- 'new \Statamic\Rules\UniqueEntryValue({collection},{id},{site})'
 
- 'unique_term_value:{taxonomy},{id},{site}'
- 'new \Statamic\Rules\UniqueTermValue({taxonomy},{id},{site})'
 
- 'unique_user_value:{id}'
- 'new \Statamic\Rules\UniqueUserValue({id})'

Antlers sanitization

Affects apps or addons using the sanitize modifier or the Html::sanitize method.

The sanitize modifier (and Html::sanitize() method) method has changed under the hood from using htmlentities to htmlspecialchars.

This change allows for things like accent characters (ü, í, etc) to remain unescaped. This is likely what you want to happen anyway, but if you have a reason for them to be converted, you should use entities modifier or Html::entities() method respectively.

Seed removed from shuffle modifier

Affects apps using the shuffle modifier with an argument.

In Laravel 11, the underlying randomization technique was made more secure and no longer supports seeds. If you need to support seeds, you will need to use a custom modifier.

{{ my_array | shuffle:123 }}
{{ my_array | custom_shuffle_with_seed:123 }}

The shuffle modifier without an argument will continue to work without any modification needed.

The modify_date modifier is now immutable

Affects apps using the modify_date modifier.

In Statamic 4, the modify_date modifier would modify date variable which would then be reflected elsewhere in your template.

In Statamic 5, this is fixed, but if you were relying on this incorrect behavior you will need to handle it.

{{ date }} // 1st of may
{{ date | modify_date('+1 day') }} // 2nd of may
{{ date }} // 2nd of may
{{ date }} // 1st of may

The svg tag sanitizes by default

Affects apps that use the svg tag.

The {{ svg }} will now sanitize the output by default. This meant things like JavaScript or other valid but potentially insecure elements will be filtered out.

For most people this won't be a problem but if you rely on this advanced SVG features, you may want to disable it.

{{ svg
src="foo.svg"
sanitize="false"
}}

Alternatively, you can opt out of it completely in a service provider:

public function boot()
{
\Statamic\Tags\Svg::disableSanitization();
}

Bard JS value is now an object

Affects apps or addons that are manually targeting Bard's value in JS

Previously, to prevent issues with how Laravel would trim whitespace on submitted strings, Bard's value would be a JSON stringified version of an object. In Statamic 5, it will just be the object.

let bardValue = JSON.parse(getBardValue());
let bardValue = getBardValue();

User roles inherit from groups

Affects apps or addons using the roles/hasRole methods on the User class, or the user:is/is tags.

In v4, the User class's roles method would only return roles defined explicitly on the user. It would not return roles inherited through any assigned groups.

This affected calling the roles method directly, the hasRole method, or the user:is and is tags.

This was a common point of confusion. So in v5, including the inherited roles is the more "default" behavior.

// To get explicit roles...
$user->roles();
$user->explicitRoles();
 
// To check if a user has a role explicitly defined...
$user->hasRole($role);
$user->explicitRoles()->has($role->handle());
 
// To set explicit roles...
$user->roles($roles);
$user->explicitRoles($roles);
 
// To get all roles, including ones through user groups...
getAllRoles($user); // (It was complicated)
$user->roles();
 
// To check if a user has a role, including ones through user groups...
getAllRoles($user)->has($role->handle()); // (It was complicated)
$user->hasRole($role);

Misc class method changes

The following methods have been removed:

  • Statamic\Entries\Collection::revisions() removed. Use revisionsEnabled().

The following interfaces have added findOrFail() methods:

  • Statamic\Contracts\Assets\AssetContainerRepository
  • Statamic\Contracts\Assets\AssetRepository
  • Statamic\Contracts\Auth\UserRepository
  • Statamic\Contracts\Entries\CollectionRepository
  • Statamic\Contracts\Entries\EntryRepository
  • Statamic\Contracts\Globals\GlobalVariablesRepository
  • Statamic\Contracts\Structures\NavigationRepository
  • Statamic\Contracts\Taxonomies\TermRepository

The following methods have changed:

  • Statamic\StaticCaching\Cacher::getCachedPage() now returns a Statamic\StaticCaching\Page.

Glide filename parameter has been removed

This was an undocumented relic of a feature that let you add a "fake" or "vanity" filename to the end of Glide URLs to be able to improve SEO. This now happening automatically based on the original filename, rendering this feature unnecessary (this feature was broken in certain situations and as mentioned before — undocumented).

If you use this {{ glide filename="" }} parameter, you'll need to remove it, otherwise nothing will be output.

If you are hot-linking to any images rendered with manually set filenames in this way, you'll also need to correct those links.

Entries may now only be queried by a single status

Affects any apps or addons filtering entries by status.

Previously, when querying entries, you could use any condition to filter entries.

However, in v5, as part of improvements to how statuses work behind the scenes, statuses can now only be filtered using the is / equals conditions, and the whereStatus query method. Some examples:

Collection tag:

{{ collection:blog status:in="published" }}
{{ collection:blog status:is="published" }}

PHP:

Entry::query()
->where('collection', 'blog')
->where('status', 'published')
->whereStatus('published')
->get();

REST API:

/api/collections/blog/entries?filter[status:in]=published
/api/collections/blog/entries?filter[status]=published

GraphQL API:

{
entries(
collection: "blog"
filter: {
status: { in: "published" } #
status: "published" #
}
) {
data {
title
}
}
}

Let us know if you need to query by multiple statuses by opening a GitHub issue.

Zero impact changes

These are items that you can completely ignore. They are suggestions on how to improve your code using newly added features.

JS Slug generation

The $slugify JS API has been deprecated. This could be a good time to use the new slug helpers.

If you are generating simple slug/handles, you can use the global helper:

let slug = this.$slugify('Foo Bar'); // foo-bar
let slug = str_slug('Foo Bar'); // foo-bar
 
let handle = this.$slugify('Foo Bar', '_'); // foo_bar
let handle = snake_case('Foo Bar'); // foo_bar

If your slugs need to factor in language logic (like an entry's slug would), then you can use the new server side feature:

let slug = getSlug('Foo Bar');
 
function getSlug(value) {
return this.$slugify(value);
}
 
let slug = await getSlug('Foo Bar');
 
async function getSlug(value) {
return this.$slug.async().create('Foo Bar');
}

Addon test case

If you have an addon, there's a good chance your TestCase is a bit complicated.

You should be able to extend the new AddonTestCase and specify your service provider in favor of manually wiring up all the Testbench bits.

use Orchestra\Testbench\TestCase as OrchestraTestCase;
use Statamic\Testing\AddonTestCase;
 
abstract class TestCase extends OrchestraTestCase
abstract class TestCase extends AddonTestCase
{
protected string $addonServiceProvider = YourServiceProvider::class;
 
protected function getPackageProviders($app)
{
return [
GraphQLServiceProvider::class,
StatamicServiceProvider::class,
YourServiceProvider::class,
];
}
 
// etc...
}
Docs feedback

Submit improvements, related content, or suggestions through Github.

Betterify this page →