Upgrading from Craft 3
The smoothest way to upgrade to Craft 4 is to start with a fully-updated Craft 3 project.
# Preparing for the Upgrade
Let’s take a moment to audit the current state of your site, and make sure:
- your live site is running the latest version of Craft 3 (opens new window)
- you have the most recent Craft 3-compatible version of all plugins installed, and you’ve verified that Craft 4-compatible versions are available
- there are no deprecation warnings (opens new window) to be fixed
- all your environments meet Craft 4’s minimum requirements
- PHP 8.0.2+ and MySQL 5.7.8+, MariaDB 10.2.7+, or PostgreSQL 10+ (opens new window)
- newly-required PHP extensions: BCMath (opens new window) and Intl (opens new window)
- you’ve reviewed the changes in Craft 4 further down this page and understand what work 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.
These steps assume you have a local development environment that meets Craft 4’s requirements, and that any changes made in preparation for the upgrade have been deployed to your live site.
- Capture a fresh database backup from your live environment and import it.
- If your database has
entrydrafts
andentryversions
tables, check them for any meaningful data. Craft 3.2 stopped using these tables when drafts and revisions became elements, and the tables will be removed as part of the Craft 4 install process. - 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.
- Edit your project’s
composer.json
to require"craftcms/cms": "^4.0.0"
and Craft-4-compatible plugins all at once.You may also need to addYou’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."php": "8.0.2"
to your platform (opens new window) requirements. - Run
composer update
. - Make any required changes to your configuration.
- Run
php craft migrate/all
.
Your site is now running Craft 4! 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, like Upgrading to Commerce 4.
Once you’ve verified everything’s in order, commit your updated composer.json
, composer.lock
, and config/project/
directory (along with any template, configuration, or module files that required updates) and deploy those changes normally in each additional environment.
# Optional Steps
These steps aren’t required to use Craft 4, but it’s the perfect time to do some housekeeping.
# MySQL Character Sets
If you’re using MySQL, we recommend running php craft db/convert-charset
along with the upgrade process to ensure optimal database performance.
# Entry Script
The Craft starter project (opens new window) is kept up-to-date with new Craft features, and provides official recommendations for your entry scripts (index.php
, the craft
CLI executable, and a shared bootstrap.php
file), configuration structure, etc. It’s a good idea to look this over as a means of keeping your upgraded projects as similar as possible to fresh ones. Be mindful of any customizations you’ve made to the scripts, over time—this is where you would have set any additional PHP constants.
Incorporating updated entry script(s) into your project may also involve:
- Changing the required version of DotEnv (opens new window) (
vlucas/phpdotenv
) incomposer.json
to match the starter project; - Reviewing how your environment is determined;
Craft 3 projects would automatically assign (opens new window) the special CRAFT_ENVIRONMENT
constant to the value of an environment variable named ENVIRONMENT
—but the Craft 4 starter kit requires that you directly set CRAFT_ENVIRONMENT
from your .env
file.
# Breaking Changes and Deprecations
Features deprecated in Craft 3 may have been fully removed or replaced in Craft 4, and new deprecations have been flagged in Craft 4
# Configuration
# Config Settings
Some config settings have been removed in Craft 4:
File | Setting | Note |
---|---|---|
config/general.php | customAsciiCharMappings | Deprecated in 3.0.10. Submit corrections to Stringy (opens new window). |
config/general.php | siteName | Set in the control panel, optionally using environment variables. (See example (opens new window).) |
config/general.php | siteUrl | Set in the control panel, optionally using environment variables. (See example (opens new window).) |
config/general.php | suppressTemplateErrors | |
config/general.php | useCompressedJs | Craft always serves compressed JavaScript files now. |
config/general.php | useProjectConfigFile | Project config always writes YAML now, but you can manually control when. |
Leaving legacy settings in general.php
will throw an error.
You can now set your own config settings—as opposed to those Craft supports—from config/custom.php
. Any of your custom config settings will be accessible via Craft::$app->config->custom->myCustomSetting
, or {{ craft.app.config.custom.myCustomSetting }}
.
# Volumes
Volumes have changed a bit in Craft 4.
In Craft 3, volumes were for storing custom files and defining their associated field layouts. In Craft 4, the field layouts work exactly the same but URLs and storage settings are moved to a new concept called a Filesystem.
Craft 4 Volume settings:
Craft 4 Filesystem settings:
You can create any number of filesystems, giving each one a handle, and you designate one filesystem for each volume. Since this can be set to an environment variable, you can define all the filesystems you need in different environments and easily swap them out depending on the actual environment you’re in.
You’ll want to create one filesystem per volume, which should be fairly quick since filesystems can be created in slideouts without leaving the volume settings page.
The migration process will take care of volume migrations for you, but there are two cases that may require your attention:
volumes.php
files are no longer supported—so you’ll need to use filesystems accordingly if you’re swapping storage methods in different environments.- Any filesystems without public URLs should designate a transform filesystem in order to have control panel thumbnails. Craft used to store generated thumbnails separately for the control panel—but it will now create them alongside your assets just like front-end transforms.
# Logging
The logging system in Craft 4 has undergone some significant changes.
- 404s are no longer logged by default. You can customize this in
config/app.php
viacomponents.log.monologTargetConfig.except
:'components' => [ 'log' => [ 'monologTargetConfig' => [ // Override exclusions: 'except' => [ \yii\i18n\PhpMessageSource::class . ':*', // This pattern is a default, commented out here for illustration: // \yii\web\HttpException::class . ':404', ], ], ], ],
- Query logging and query profiling are no longer enabled by default, except when Dev Mode is on. These can be manually adjusted by setting yii\db\Connection::$enableLogging (opens new window) and yii\db\Connection::$enableProfiling (opens new window) from
config/app.php
:return [ 'components' => [ 'db' => function() { $config = craft\helpers\App::dbConfig(); // Always enable DB logging and profiling $config['enableLogging'] => true; $config['enableProfiling'] => true; return Craft::createObject($config); }, ], ];
- When CRAFT_STREAM_LOG is set to
true
, file logging will be completely disabled.
See craft\log\MonologTarget (opens new window) for a look at Craft’s default log configuration.
Any existing custom log components defined in config/app.php
, config/web.php
, or config/console.php
may require changes noted below.
The following PHP classes have been removed:
Class | What to do instead |
---|---|
\craft\log\FileTarget | Configure Monolog targets via components.log.monologTargetConfig . |
\craft\log\StreamLogTarget | Configure Monolog targets via components.log.monologTargetConfig . |
\craft\helpers\App::getDefaultLogTargets() | Add additional log targets via components.log.targets . |
\craft\helpers\App::logConfig | Define your own log component using yii\log\Dispatcher . |
The following PHP methods have been removed:
Method | What to do instead |
---|---|
\craft\helpers\App::getDefaultLogTargets() | Add additional log targets via components.log.targets . |
\craft\helpers\App::logConfig | Define your own log component using yii\log\Dispatcher (opens new window). |
# PHP Constants
Some PHP constants have been deprecated in Craft 4, and will no longer work in Craft 5:
Old PHP Constant | What to do instead |
---|---|
CRAFT_SITE_URL | Environment-specific site URLs can be defined via environment variables (opens new window). |
CRAFT_LOCALE | CRAFT_SITE |
# Templating
Craft 4 comes with Twig 3 (opens new window), meaning some tags, functions, global variables, and operators have changed. You may also need to review templates for the following:
- Due to changes in craft\elements\db\ElementQuery (opens new window), the special
loop
variable (opens new window) in{% for %}
tags may be missing some properties when using an element query, directly. Call a query execution method like.all()
or.collect()
to ensure you are dealing with an array or object that implements theCountable
interface.
# Tags
The following template tags have been removed:
Old Tag | What to do instead |
---|---|
{% spaceless %} | {% apply spaceless %} |
{% filter %} | {% apply %} |
The if
param in {% for %}
tags has also been removed, but |filter
can be used in its place:
{# Craft 3 #}
{% for item in items if item is not null %}
{# ... #}
{% endfor %}
{# Craft 4 #}
{% for item in items|filter(item => item is not null) %}
{# ... #}
{% endfor %}
The {% cache %}
tag now stores any external references from {% css %}
and {% js %}
tags now, in addition to any inline content.
# Functions
Some template functions have been removed completely:
Old Template Function | What to do instead |
---|---|
getCsrfInput() | csrfInput() |
getFootHtml() | endBody() |
getHeadHtml() | head() |
round() | |round |
atom() | |atom |
cookie() | |date(constant('DATE_COOKIE')) |
iso8601() | |date(constant('DATE_ISO8601')) |
rfc822() | |date(constant('DATE_RFC822')) |
rfc850() | |date(constant('DATE_RFC850')) |
rfc1036() | |date(constant('DATE_RFC1036')) |
rfc1123() | |date(constant('DATE_RFC1123')) |
rfc2822() | |date(constant('DATE_RFC7231')) |
rfc3339() | |date(constant('DATE_RFC2822')) |
rss() | |rss |
w3c() | |date(constant('DATE_W3C')) |
w3cDate() | |date('Y-m-d') |
mySqlDateTime() | |date('Y-m-d H:i:s') |
localeDate() | |date('short') |
localeTime() | |time('short') |
year() | |date('Y') |
month() | |date('n') |
day() | |date('j') |
nice() | |dateTime('short') |
uiTimestamp() | |timestamp('short') |
# Variables
Certain services were exposed via the craft
variable in Twig (an instance of craft\web\twig\variables\CraftVariable (opens new window)) for greater compatibility with Craft 2 templates, but those have been removed in favor of accessing them via the craft.app
variable:
Old Template Variable | What to do instead |
---|---|
craft.categoryGroups | craft.app.categories |
craft.config | craft.app.config |
craft.deprecator | craft.app.deprecator |
craft.elementIndexes | craft.app.elementIndexes |
craft.emailMessages | craft.app.systemMessages |
craft.feeds | n/a (see below) |
craft.fields | craft.app.fields |
craft.globals | craft.app.globals |
craft.i18n | craft.app.i18n |
craft.isLocalized | craft.app.isMultiSite |
craft.locale | craft.app.locale |
craft.request | craft.app.request |
craft.sections | craft.app.sections |
craft.session | craft.app.session |
craft.systemSettings | System settings are stored in Project Config
3.1.0+. Use craft.app.projectConfig.get('...') to access settings by their keys in config/project/project.yml . If a value you need is set to an alias or environment variable, pass it to the parseEnv() function. |
craft.userGroups | craft.app.userGroups |
craft.userPermissions | craft.app.userPermissions |
Reading Feeds?
You can use dodecastudio/craft-feedreader (opens new window) as a drop-in replacement for Craft’s removed feed service:
{# Craft 3 #}
{% set feed = craft.app.feeds.getFeed('https://craftcms.com/blog.rss') %}
{# Craft 4 #}
{% set feed = craft.feedreader.getFeed('https://craftcms.com/blog.rss') %}
# Operators
Twig 3’s operators (in
, <
, >
, <=
, >=
, ==
, !=
) are more strict comparing strings to integers and floats. Make sure this doesn’t have any unintended consequences!
# Elements
# Custom Field Data
Craft elements now clone array and object field values before returning them.
This means that every time you call a field handle like element.myCustomField
, you’ll get a fresh copy of that field’s value. As a result, you may no longer need to use clone()
to avoid inadvertently changing data or element queries used elsewhere.
The change in behavior has the potential to break templates relying on Craft 3’s behavior, so be sure to check any templates or custom code that modifies and re-uses custom field values:
{# Field values are mutated in-place... #}
{% set importantPosts = entry.relatedNews
.type('breakingNews')
.orderBy('dateUpdated DESC')
.all() %}
{# -> Expected results. #}
{# ...so subsequent uses of the field are tainted: #}
{% set allUpdates = entry.relatedNews
.orderBy('title ASC')
.all() %}
{# -> Still filtered by `breakingNews`! #}
If you do need to operate on the same value multiple times, you can store it in a temporary variable—the cloning only happens when accessing the field data directly from an element.
# Query Params
Some element query params have been removed:
Element Type | Old Param | What to do instead |
---|---|---|
all | locale | site or siteId |
all | localeEnabled | status |
all | order | orderBy |
Asset | source | volume |
Asset | sourceId | volumeId |
Matrix block | ownerLocale | site or siteId |
Matrix block | ownerSite | site |
Matrix block | ownerSiteId | siteId |
Some element query params have been renamed in Craft 4. The old params have been deprecated, but will continue to work until Craft 5.
Element Type | Old Param | New Param |
---|---|---|
all | anyStatus | status(null) |
# Query Methods
Some element query methods have been removed in Craft 4.
Old Method | What to do instead |
---|---|
find() | all() |
first() | one() |
last() | inReverse().one() |
total() | count() |
# User Queries
User queries now return all users by default in Craft 4, instead of only active users. Any user queries relying on this default behavior may need to be updated:
{# Craft 3 returned all *active* users by default #}
{% set activeUsers = craft.users().all() %}
{# Craft 4 returns *all* users by default; specify status for the same behavior #}
{% set activeUsers = craft.users()
.status('active')
.all() %}
# Reserved Field Names
The following field handles are no longer allowed due to conflicts with native properties or methods:
isNewForSite
isProvisionalDraft
newSiteIds
In addition, [User] field layouts can no longer contain fields with the following handles 4.5.0+:
active
addresses
admin
email
friendlyName
locked
name
password
pending
suspended
username
# Collections
Craft 4 adds the Collections (opens new window) package, which offers a more convenient and consistent way of working with arrays and collections of things.
Element queries now include a collect()
method that returns query results as one of these collections instead of an array:
{# Array #}
{% set posts = craft.entries()
.section('blog')
.all() %}
{# Collection #}
{% set posts = craft.entries()
.section('blog')
.collect() %}
There’s also a new collect() function you can use in Twig templates.
Be careful with any conditionals that rely on an implicit count! An empty array evaluates as false
, while an empty collection evaluates as true
:
{# 👍 #}
{% if myArray %}
{# Do stuff #}
{% else %}
{# No items to do stuff with; do something else #}
{% endif %}
{# ❌ #}
{% if myCollection %}
{# Do stuff #}
{% else %}
{# !! We’ll never end up here !! #}
{% endif %}
Use the |length
filter or the collection’s .count()
method instead:
{# 👍 #}
{% if myCollection.count() %}
{# Do stuff #}
{% else %}
{# No items to do stuff with; do something else #}
{% endif %}
{# 👍 #}
{% if myCollection|length %}
{# Do stuff #}
{% else %}
{# No items to do stuff with; do something else #}
{% endif %}
# GraphQL
GraphQL Argument | What to do instead |
---|---|
immediately | all GraphQL transforms are now processed immediately |
enabledForSite | status |
# Console Commands
Old Command | What to do instead |
---|---|
--type option for migrate/* commands | --track or --plugin option |
# Alternative Text Fields
Craft 4’s Assets have the option of including a native alt
field for alternative text. You can use this to query assets with or without alternative text, and Craft will use it generating image tags both in the control panel and on the front end.
If you’re already using your own custom field for this, you can use Craft’s resave command(s) (opens new window) to migrate to the new alt
field:
- In the control panel, drag Craft’s
alt
field into each relevant field layout. - From your terminal, use the
resave/assets
command to populate the newalt
field text from your old field’s content:php craft resave/assets --set alt --to myAltTextField --if-empty
- If everything looks good, you can optionally empty the content out of your original field:
php craft resave/assets --set myAltTextField --to :empty:
- Remove the old field from each relevant field layout.
For deployment, you’ll probably want to take a phased approach: upgrade your site, migrate off your existing field to the native one, then remove the existing field after you’ve migrated the data in all your environments.
Don’t forget to update your templates and GraphQL queries!
alt
is now a reserved word for Asset Volume field layouts. If you have an existing, custom alt
field, you’ll need to change it.
# User Permissions
A few user permissions have been removed in Craft 4:
assignUserGroups
previously let authorized users assign other users to their own groups. Authorization must now be explicitly granted for each group.customizeSources
had made it possible for authorized users to customize element sources. Only admins can customize element sources now, and only from an environment that allows admin changes.publishPeerEntryDrafts:<uid>
permissions wouldn’t have stopped users from viewing, copying, and saving draft content themselves.
# Queue Drivers
If you’re overriding Craft’s queue
component in config/app.php
, you may want to override the proxyQueue
(opens new window) property of Craft’s built-in queue driver, instead. This way, you’ll regain visibility into the queue’s state from the control panel.
<?php
return [
'components' => [
'queue' => [
'proxyQueue' => [
// custom queue config goes here
],
],
],
];
# Forms + Actions
# users/save-user
When registering or updating users, passing a fullName
param is preferred to firstName
and lastName
, as the former ensures values are saved exactly as provided. Users now store names in a single field, and will parse them back out (opens new window) into first and last names for backwards-compatibility.
<form method="post">
{{ csrfInput() }}
{{ actionInput('users/save-user') }}
{# Separate first and last name inputs: #}
{{ input('text', 'firstName', user.firstName) }}
{{ input('text', 'lastName', user.lastName) }}
{# ... #}
</form>
Read more about the users/save-user
controller action.
# Plugins and Modules
Plugin authors (and module maintainers) should refer to our guide on updating Plugins for Craft 4.