# 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:

```json
"statamic/cms": "^4.0" // [tl!--]
"statamic/cms": "^5.0" // [tl!++]
```

Then run:

``` shell
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.

:::tip
If you want to (semi-)automate the Laravel upgrade process, we recommend using [Laravel Shift](https://laravelshift.com/discounts/statamic-1983) (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.

```php
// config/statamic/sites.php
return [ // [tl! --:start]
  'sites' => [
    'default' => [
      'name' => 'First Site',
      'url' => config('app.url'),
      'locale' => 'en_US',
    ],
    'two' => [
      'name' => 'Second Site',
      'url' => config('app.url').'/fr/',
      'locale' => 'fr_FR',
    ]
  ]
]; // [tl! --:end]
```

```yaml
# resources/sites.yaml
default: # [tl! ++:start]
  name: First Site
  url: '{{ config:app:url }}'
  locale: en_US
two:
  name: Second Site
  url: '{{ config:app:url }}/fr/'
  locale: fr_FR # [tl! ++:end]
```

_**Note:** Text direction is now also automatic, [based on each site's language](/multi-site.md#text-direction). 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.

```php
// 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.

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

v4 outputs: "no: " {{# [tl! --] #}}
v5 outputs: "yes: my default value"  {{# [tl! ++] #}}
```

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:

```php
namespace App\Example;

use Statamic\Support\Arr; // [tl! ++,**]
use Statamic\Support\Str; // [tl! ++,**]

class Example
{
  function example()
  {
    array_get($arr, $key); // [tl! --,**]
    Arr::get($arr, $key); // [tl! ++,**]

    str_start($str, $prefix); // [tl! --,**]
    Str::start($str, $prefix); // [tl! ++,**]

    ends_with($str, $needle); // [tl! --,**]
    Str::endsWith($str, $needle); // [tl! ++,**]
  }
}
```

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

```sh
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`:

```php
// app/Providers/AppServiceProvider.php

use Illuminate\Pagination\Paginator; // [tl! ++]

public function boot(): void
{
    Paginator::defaultView('pagination::default'); // [tl! ++]
}
```

### 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:

```php
#!/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](/antlers.md).

### 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`.

```php
// config/statamic/forms.php
'submissions' => 'custom path', // [tl! --]

// config/statamic/stache.php
'stores' => [
    'form-submissions' => [ // [tl! ++:start]
        'directory' => 'custom path',
    ], // [tl! ++:end]
],
```

### 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.

```yaml
validate:
  - 'unique_entry_value:{collection},{id},{site}' #[tl!--]
  - 'new \Statamic\Rules\UniqueEntryValue({collection},{id},{site})' #[tl!++]

  - 'unique_term_value:{taxonomy},{id},{site}' #[tl!--]
  - 'new \Statamic\Rules\UniqueTermValue({taxonomy},{id},{site})' #[tl!++]

  - 'unique_user_value:{id}' #[tl!--]
  - 'new \Statamic\Rules\UniqueUserValue({id})' #[tl!++]
```

### 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 }} {{# [tl! --] #}}
{{ my_array | custom_shuffle_with_seed:123 }} {{# [tl! ++] #}}
```

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.

```antlers
{{ date }} // 1st of may
{{ date | modify_date('+1 day') }} // 2nd of may
{{ date }} // 2nd of may {{# [tl! --] #}}
{{ date }} // 1st of may {{# [tl! ++] #}}
```

### 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.

```antlers
{{ svg
   src="foo.svg"
   sanitize="false" {{# [tl! ++] #}}
}}
```

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

```php
public function boot()
{
    \Statamic\Tags\Svg::disableSanitization(); // [tl! ++]
}
```

### 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.

```js
let bardValue = JSON.parse(getBardValue()); // [tl! --]
let bardValue = getBardValue(); // [tl! ++]
```

### 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.

```php
// To get explicit roles...
$user->roles(); // [tl! --]
$user->explicitRoles(); // [tl! ++]

// To check if a user has a role explicitly defined...
$user->hasRole($role); // [tl! --]
$user->explicitRoles()->has($role->handle()); // [tl! ++]

// To set explicit roles...
$user->roles($roles); // [tl! --]
$user->explicitRoles($roles); // [tl! ++]

// To get all roles, including ones through user groups...
getAllRoles($user); // (It was complicated) [tl! --]
$user->roles(); // [tl! ++]

// To check if a user has a role, including ones through user groups...
getAllRoles($user)->has($role->handle()); // (It was complicated) [tl! --]
$user->hasRole($role); // [tl! ++]
```


### 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:
```antlers
{{ collection:blog status:in="published" }} {{# [tl! --] #}}
{{ collection:blog status:is="published" }} {{# [tl! ++] #}}
```

PHP:
```php
Entry::query()
    ->where('collection', 'blog')
    ->where('status', 'published') // [tl! --]
    ->whereStatus('published') // [tl! ++]
    ->get();
```

REST API:
```php
/api/collections/blog/entries?filter[status:in]=published // [tl! --]
/api/collections/blog/entries?filter[status]=published // [tl! ++]
```

GraphQL API:
```graphql
{
    entries(
        collection: "blog"
        filter: {
            status: { in: "published" }         #[tl! --]
            status: "published"                 #[tl! ++]
        }
    ) {
        data {
            title
        }
    }
}
```

If you need to query entries regardless of status, you can pass `any`.

## 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:

```js
let slug = this.$slugify('Foo Bar'); // foo-bar [tl! --]
let slug = str_slug('Foo Bar'); // foo-bar [tl! ++]

let handle = this.$slugify('Foo Bar', '_'); // foo_bar [tl! --]
let handle = snake_case('Foo Bar'); // foo_bar [tl! ++]
```

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

```js
let slug = getSlug('Foo Bar'); // [tl! --:start]

function getSlug(value) {
    return this.$slugify(value);
} // [tl! --:end]

let slug = await getSlug('Foo Bar'); // [tl! ++:start]

async function getSlug(value) {
  return this.$slug.async().create('Foo Bar');
} // [tl! ++:end]
```


### 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.

```php
use Orchestra\Testbench\TestCase as OrchestraTestCase; // [tl! --]
use Statamic\Testing\AddonTestCase; // [tl! ++]

abstract class TestCase extends OrchestraTestCase // [tl! --]
abstract class TestCase extends AddonTestCase // [tl! ++]
{
    protected string $addonServiceProvider = YourServiceProvider::class; // [tl! ++]

    protected function getPackageProviders($app) // [tl! --:start]
    {
        return [
            GraphQLServiceProvider::class,
            StatamicServiceProvider::class,
            YourServiceProvider::class,
        ];
    }

     // etc... [tl! --:end]
}
```
