Upgrading from Craft 4
The smoothest way to upgrade to Craft 5 is to start with a fully-updated Craft 4 project. We recommend approaching the upgrade in three phases: preparation, a local upgrade, and rollout to live environments.
# Preparing for the Upgrade
Let’s take a moment to audit and prepare your project.
- Your live site must be running the latest version (opens new window) of Craft 4;
- The most recent Craft 4-compatible versions of all plugins are installed, and Craft 5-compatible versions are available;
- Your project is free of deprecation warnings after thorough testing on the latest version of Craft 4;
- All your environments meet Craft 5’s minimum requirements (the latest version of Craft 4 will run in any environment that meets Craft 5’s requirements, so it’s safe to update PHP and your database ahead of the 5.x upgrade):
- PHP 8.2
- MySQL 8.0.17+ using InnoDB, MariaDB 10.4.6+, or PostgreSQL 13+
- You’ve reviewed the breaking changes in Craft 5 further down this page and understand that additional work and testing may lie ahead, post-upgrade;
Once you’ve completed everything above, you’re ready to start the upgrade process!
If your project uses custom plugins or modules, we have an additional extension upgrade guide.
# Performing the Upgrade
Like any other update, it’s essential that you have a safe place to test the upgrade prior to rolling it out to a live website.
These steps assume you have a local development environment that meets Craft 5’s requirements, and that any changes made in preparation for the upgrade have been deployed to your live site.
DDEV (opens new window) users should take this opportunity to update (opens new window) so that the craftcms
project type reflects our latest recommendations.
In an existing DDEV project, you can change the PHP or database version with the config
command…
ddev config --php-version=8.2
ddev config --database=mysql:8.0
ddev start
…or use ddev config --update
to reapply project type defaults.
Capture a fresh database backup from your live environment and import it.
Make sure you don’t have any pending or active jobs in your queue.
Run
php craft project-config/rebuild
and allow any new background tasks to complete.Capture a database backup of your local environment, just in case things go sideways.
Note your current Temp Uploads Location setting in Settings → Assets → Settings.
MySQL users: Add your database’s current character set and collation to
.env
. If you have always used Craft’s defaults, this will be:CRAFT_DB_CHARSET="utf8mb3" CRAFT_DB_COLLATION="utf8mb3_general_ci"
Read more about this process in the Database Character Set and Collation section, below.
Edit your project’s
composer.json
to require"craftcms/cms": "^5.0.0"
and Craft-5-compatible plugins, all at once.You’ll need to manually edit each plugin version in
composer.json
. If any plugins are still in beta, you may need to change yourminimum-stability
(opens new window) andprefer-stable
(opens new window) settings.While you’re at it, review your project’s other dependencies! You may also need to add
"php": "8.2"
to your platform (opens new window) requirements, or remove it altogether.Run
composer update
.Make any required changes to your configuration.
Run
php craft up
.MySQL users: Remove your database character set and collation settings from
.env
(anddb.php
—even if you didn’t modify it during the upgrade), then runphp craft db/convert-charset
.
Your site is now running Craft 5! If you began this process with no deprecation warnings, you’re nearly done.
Thoroughly review the list of changes on this page, making note of any features you use in templates or modules. Only a fraction of your site’s code is actually evaluated during an upgrade, so it’s your responsibility to check templates and modules for consistency. You may also need to follow any plugin-specific upgrade guides.
# Going Live
The Craft 5 upgrade can be run largely unattended during a routine deployment. Once you’ve performed the upgrade in your local environment and everything is working as expected, commit your updated composer.json
, composer.lock
, and config/project/
directory, as well as any template, configuration, or module files that required updates.
MySQL users must add the correct character set and collation settings to each remote environment for the duration of the upgrade.
Deploy your changes to each environment, then run php craft up
.
If you added temporary character set or collation settings for the upgrade, remove them now, then run php craft db/convert-charset
to apply Craft 5’s new defaults to all tables.
Read more about this part of the upgrade in the Database Character Set and Collation section, below.
# Cleanup + Optional Steps
# Field and Entry Type Consolidation
Craft 5.3 introduced three new commands for consolidating fields and entry types, post-upgrade:
fields/auto-merge
— Automatically discovers functionally identical fields and merges their uses together.fields/merge
— Manually merge one field into another of the same type and update uses of the merged field.entry-types/merge
— Merge one entry type into another and update uses of the merged entry type.
These tools are particularly useful on projects with many Matrix fields containing similar block types, or multiple sections with similar entry types. While the upgrade doesn’t result in more fields or entry types and block types than existed previously, it can result in some ergonomic problems with the new content architecture!
We recommend you approach consolidation in the following order, after getting your project fully upgraded and deployed.
In addition to updating project config, this process generates content migrations. You must commit these files to version control and run craft up
in all other environments to apply the changes!
# Auto-merging Fields
Your first step should be to try auto-merging field definitions:
ddev craft fields/auto-merge
Craft will audit your fields, and propose batches of merge candidates. To accept a merge, type y
and press Enter, then choose the field you want to merge the duplicates into.
The usage of any merged fields in field layouts and templates will remain the same after merging global field definitions. Any overridden names, handles, instructions, and so on will remain. One exception here is if a name or handle override is present but the same as the merged global field’s name: Craft clears the override so that it can be better kept in sync, moving forward.
Don’t like how Craft grouped some of the fields? You can skip any proposed merge and come back to the command later—say, after manually merging a few of the fields.
# Manually Merging Fields
For any additional fields you wish to merge (or merge candidates you skipped in the previous step), run the fields/merge
command as many times as necessary to consolidate them:
ddev craft fields/merge myFirstDropdownField mySecondDropdownField
To merge multiple fields together, choose the field you want to persist, and run the command multiple times, passing each redundant field as the first argument, and the target field as the second, i.e:
ddev craft fields/merge ctaDonate ctaAbout
ddev craft fields/merge ctaStaff ctaAbout
ddev craft fields/merge ctaFoundation ctaAbout
# Merging Entry Types
Once your field definitions have been consolidated, merging entry types is greatly simplified. Here’s how Craft handles fields during a merge:
- Any field instances that map to the same custom field (and have the same handle) will be left alone in the persistent entry type.
- Any single-instance fields that exist in both entry types (but with different handles) will be left alone in the persistent entry type, and the command will note the handle change for fields of the outgoing entry type.
- Any other fields in the outgoing entry type will be added to the persistent entry type under a “Merged Fields” tab. Any handle conflicts will be resolved by incrementing the handle of the merged field, and the handle change will be noted in the command’s output.
There is no “automatic” equivalent for merging entry types—each merge must be run manually:
ddev craft entry-types/merge quote pullQuote
To merge entry types, Craft takes the following actions:
- The persistent entry type’s field layout is updated with any new fields from the outgoing entry type.
- Sections and fields that use the outgoing entry type are updated to use the persistent entry type, instead (if it wasn’t already available in that context).
- The outgoing entry type is removed.
- A content migration is generated an run, remapping entries of the outgoing entry type to the persistent one, and entries’ JSON content is updated with the correct layout element UUIDs.
Merging is concerned only with custom fields. Other field layout elements (like tips, rules, breaks, or templates) are not merged into the persisted entry type.
# Entry Scripts
Older projects may use versions of vlucas/phpdotenv
(opens new window) (the library that loads variables from your .env
file) that depend on features deprecated in PHP 8.1. If you encounter errors during (or after) installation, change your vlucas/phpdotenv
dependency in composer.json
to ^5.6.0
, then run composer update
.
Along with this update, you’ll need to integrate changes from our starter project (opens new window)’s web/index.php
script and craft
executable. If you have done any low-level customization of Craft via bootstrap constants, make sure those values are preserved in the appropriate file(s)—constants shared between the two files can be moved to a common bootstrap.php
file (opens new window).
# Breaking Changes and Deprecations
Features deprecated in Craft 4 may have been fully removed or replaced in Craft 5, and new deprecations have been flagged in Craft 5.
This list focuses on high-traffic, user-facing features. Review the complete changelog (opens new window) for information about changes to specific APIs, including class deprecations, method signatures, and so on.
# Configuration
The following settings have been changed.
# Database Character Set and Collation
MySQL 8.0 deprecated the utf8mb3
character set (opens new window), as well as the use of utf8
as an alias for utf8mb3
(opens new window). MySQL recommends utf8mb4
(opens new window) instead, and it’s expected that utf8
will become an alias for utf8mb4
in MySQL 8.1.
To ease that transition, Craft 5 is proactively treating utf8
as utf8mb4
for MySQL and MariaDB installs.
Explicitly setting the character set and collation ensures compatibility of new tables during the upgrade—afterwards, those settings can be scrubbed and the tables can be converted via Craft’s CLI. Here’s how this will look, in practice.
If you have never customized character set or collation settings for your database connection, set these variables in your .env
file during the upgrade:
CRAFT_DB_CHARSET="utf8mb3"
CRAFT_DB_COLLATION="utf8mb3_general_ci"
If you are using these settings (either from .env
or db.php
), they can be left as-is, for now.
Once the upgrade is complete in an environment, convert your tables to utf8mb4
by removing the CRAFT_DB_CHARSET
and CRAFT_DB_COLLATION
environment variables (and clearing any charset
and collation
settings from db.php
)…
# …or set them to Craft’s defaults…
CRAFT_DB_CHARSET="utf8mb4"
CRAFT_DB_COLLATION="utf8mb4_0900_ai_ci" # MySQL
CRAFT_DB_COLLATION="utf8mb4_unicode_ci" # MariaDB
…then running php craft db/convert-charset
.
# Template Priority
The defaultTemplateExtensions config setting now lists twig
before html
, by default. This means that projects with templates that share a name (i.e. widget.twig
and widget.html
, in the same directory) may behave differently in Craft 5. Audit your templates/
directory for any overlap to ensure consistent rendering. If all your templates use one extension or another, no action is required!
This setting only affects rendering of templates in the front-end; the control panel will always use .twig
files before .html
.
# Volumes & Filesystems
Multiple asset volumes can now share a filesystem! However, they must be carefully arranged such that each volume has a unique and non-overlapping base path.
When selecting a filesystem, options that would result in a collision between two volumes are not shown. This means that you may need to adjust multiple volumes’ configuration in order to consolidate them into a single filesystem.
# Templates
# Variables
With the elimination of Matrix blocks as a discrete element type, we have removed the associated element query factory function from craft\web\twig\variables\CraftVariable (opens new window).
Old | New |
---|---|
craft.matrixBlocks() | craft.entries() |
See the section on Matrix fields for more information about these changes.
# GraphQL
The reusability of entry types means that some GraphQL queries have changed.
GraphQL types corresponding to entry types no longer include their section names. In Craft 4, entries a section named “Ingredients” with the entry type “Ingredient” might have been represented in GraphQL as a type named
ingredients_ingredient_Entry
. That type would now be namedingredient_Entry
.Entries can still be queried by section using the corresponding query type (i.e.
ingredientsEntries
) or thesection
argument for the genericentries
query:query MyIngredients { ingredientsEntries { ... on ingredient_Entry { title } } }
The corresponding queries for entries nested within a Matrix field would look like this:
query DifficultSteps { stepsFieldEntries(difficulty: "hard") { ... on step_Entry { title estimatedTime } } }
The names of mutations targeting entries in a particular section remain unchanged, and still include the section and entry type handles, i.e:
save_ingredients_ingredient_Entry
andsave_ingredients_ingredient_Draft
.Mutations directly targeting nested entries in Matrix fields will use a similar structure, but combining the field handle and entry type handle:
save_stepsField_step_Entry
.When mutating a Matrix field value via GraphQL, the nested entries must be passed under an
entries
key instead of ablocks
key:mutation AddStep { save_recipes_recipe_Entry( title: "Yummy Bits and Bytes" summary: "Chocolate chip cookies that are out of this world!" steps: { entries: [ { step: { difficulty: "hard", instructions: "Preheat oven to 20,000,000°F", estimatedTime: 120, id: "new:1" } } # ... ], sortOrder: [ "new-first" ] } ) { title id } }
# Elements & Content
Craft 5 has an entirely new content storage architecture that underpins many other features.
# Content Table
The content table has been eliminated! Elements now store their content in the elements_sites
table, alongside other localized properties.
Content is stored as a JSON blob, and is dynamically indexed by the database in such a way that all your existing element queries will work without modification.
# Advanced Queries
When using custom fields in advanced where()
conditions, you no longer need to manually assemble a database column prefix/suffix. Instead, Craft can generate the appropriate expression to locate values in the JSON content column:
{# Locate the field layout element that would save to the desired column: #}
{% set entryType = craft.app.entries.getEntryTypeByHandle('post') %}
{% set fieldLayout = entryType.getFieldLayout() %}
{% set sourceField = fieldLayout.getFieldByHandle('sourceMedia') %}
{% set entriesFromPhysicalMedia = craft.entries()
.section('news')
.andWhere([
'or like',
sourceField.getValueSql(),
['print', 'paper', 'press']
])
.all() %}
This ensures that you are querying for the correct instance of a multi-instance field, each of which will store their content under a different UUID in their respective field layouts.
Craft already knows how to query against the appropriate instance of a field when using its handle as a query method, so this is only necessary for complex or compound conditions.
# Case-Sensitivity
MySQL users should review queries against textual custom field values for consistency. The following query using a lower-case name will no longer match a capitalized value:
{% set entries = craft.entries()
.section('departments')
.deptHeadNameFirst('oli')
.all() %}
If you don’t have control over the query value (say, it comes from a third-party feed), consider updating the query to use the new caseInsensitive
option:
{% set entries = craft.entries()
.section('departments')
.deptHeadNameFirst({
value: 'oli',
caseInsensitive: true,
})
.all() %}
You may also want to review any custom sources that use condition rules targeting the affected field types.
# Matrix Fields
During the upgrade, Craft will automatically migrate all your Matrix field content to entries. In doing so, new globally-accessible fields will be created with a unique name:
{Matrix Field Name} - {Block Type Name} - {Field Name}
These fields are then assigned to new entry types that replace your existing Matrix block types.
Field labels and handles are retained, within their field layouts—even if they collide with other fields in the global space!
Queries for nested entries will be largely the same—equivalent methods have been added to craft\elements\db\EntryQuery (opens new window) to enable familiar usage. The .owner()
method still accepts a list of elements that the results must be owned by, and the field()
and fieldId()
methods narrow the results by those belonging to specific fields.
Values passed to the type()
method of entry queries may require updates, as migrated entry types can receive new handles. For example, a Matrix field that contained a block type with the handle gallery
might conflict with a preexisting entry type belonging to a section; in that event, the new entry type for that block would get the handle gallery1
(or potentially gallery2
, gallery3
, and so on, if multiple similar Matrix fields are being migrated).
Visit Settings → Entry Types to check if any handles appear to have collided in this way, then review and update templates as necessary.
# Eager-Loading
Eager-loading has been dramatically simplified for most sites. You no longer need to tell Craft which relational fields to load via the .with()
query method—instead, call .eagerly()
on any query that may result in an N+1 problem to automatically trigger eager-loading:
{% set articles = craft
.entries()
.section('blog')
.with([
['featureImage'],
])
.all() %}
{% for article in articles %}
{% set image = article.featureImage|first %}
{% if image %}
{{ image.getImg() }}
{% endif %}
{% endfor %}
This feature does have some limitations, though. While it will work for all elements connected via a relational or Matrix field, you will still need to explicitly eager-load native attributes like entries’ author
(and now authors
, plural) or assets’ uploader
. Craft can only detect eager-loading opportunities when the original attribute or value is an element query. The two strategies can be safely combined, though:
{% set articles = craft
.entries()
.section('blog')
.with([
['author'],
])
.all() %}
{% for article in articles %}
{# Lazily-eager-loaded relation: #}
{% set image = article.featureImage.eagerly().one() %}
{% if image %}
{{ image.getImg() }}
{% endif %}
{# Explicitly eager-loaded element: #}
<span>{{ article.author.fullName }}</span>
{% endfor %}
# Assets
# Reusable Filesystems
Filesystems can now be shared by multiple asset volumes, so long as each volume has a unique and non-overlapping base path. The assets page has more information on this new behavior.
# Temporary Filesystem
The new tempAssetUploadFs general config setting has replaced the Temp Uploads Location setting in the Settings → Assets → Settings screen. If you noted a custom asset volume in the upgrade process, you will need to follow these steps to set up a distinct filesystem for temporary uploads. Otherwise, Craft will use the legacy behavior and fall back on an instance of craft\fs\Temp (opens new window), which puts temporary uploads in a folder in your local storage/
directory.
Load-balanced or ephemeral environments that rely on a centralized storage solution should define a temporary upload filesystem using the steps below.
The location of temporary uploads is now defined by way of a filesystem instead of a volume:
- Create a new filesystem by visiting Settings → Filesystems, giving it an appropriate Name and Handle (users will not see this, so something like “Temporary” or “Scratch” is fine).
- Set the Base Path to agree with the old volume, keeping in mind that previously, its subpath and its filesystem’s base path were combined.
- Add the handle you chose to your general config file or define a
CRAFT_TEMP_ASSET_UPLOAD_FS
environment override.
A filesystem designated by your tempAssetUploadFs
setting cannot be reused for volumes or image transforms, so it will not appear in filesystem selection menus. If you elect to define the temporary uploads filesystem handle via an environment variable, be sure and add it to your other environments as well!
# Plugins and Modules
Plugin authors (and module maintainers) should refer to our guide on updating Plugins for Craft 5.