Craft 3 Documentation

Changes in Craft 3

Rich Text Fields #

The “Rich Text” field type has been removed from Craft 3, in favor of new Redactor and CKEditor plugins.

If you have any existing Rich Text fields, they will be automatically converted to Redactor fields when you install the Redactor plugin.

Redactor Configs #

If you do install the Redactor plugin, you will need to ensure that your Redactor configs in config/redactor/ are valid JSON. That means:

// Bad:
{
  /* interesting comment */
  buttons: ['bold', 'italic']
}

// Good:
{
  "buttons": ["bold", "italic"]
}

Position Select Fields #

The “Position Select” field type has been removed from Craft 3. If you had any Position Select fields, they will be converted to Dropdown fields, with all the same options.

If you miss Position Select, you can try installing the Position Fieldtype plugin, which brings it back.

Remote Volumes #

Support for Amazon S3, Rackspace Cloud Files, and Google Cloud Storage have been moved into plugins. If you have any asset volumes that were using those services in Craft 2, you will need to install the new plugins:

Configuration #

Config Settings #

The following config settings have been deprecated in Craft 3, and will be completely removed in Craft 4:

File Old Setting New Setting
general.php activateAccountFailurePath invalidUserTokenPath
general.php backupDbOnUpdate backupOnUpdate1
general.php defaultFilePermissions defaultFileMode2
general.php defaultFolderPermissions defaultDirMode
general.php environmentVariables aliases 3
general.php restoreDbOnUpdateFailure restoreOnUpdateFailure
general.php useWriteFileLock useFileLocks
general.php validationKey securityKey4

1 Performance should no longer be a major factor when setting backupOnUpdate to false, since backups aren’t generated by PHP anymore.

2 defaultFileMode is now null by default, meaning it will be determined by the current environment.

*3 Settings that supported Environment Variables in Craft 2 now support Aliases in Craft 3. Site URL and Local volume settings will automatically be converted to the new Alias syntax when updating to Craft 3 (@variable instead of {variable}).

4 securityKey is no longer optional. If you haven’t set it yet, set it to the value in storage/runtime/validation.key (if the file exists). The auto-generated validation.key file fallback will be removed in Craft 4.

The following config settings have been removed entirely:

File Setting
db.php collation
db.php initSQLs
general.php appId
general.php cacheMethod (see Configuration → Data Caching Config

omitScriptNameInUrls and usePathInfo #

The omitScriptNameInUrls setting can no longer be set to 'auto', as it was by default in Craft 2. Which means you will need to explicitly set it to true in config/general.php if you’ve configured your server to route HTTP requests to index.php.

Similarly, the usePathInfo setting can no longer be set to 'auto' either. If your server is configured to support PATH_INFO, you can set this to true. This is only necessary if you can’t set omitScriptNameInUrls to true, though.

URL Rules #

If you have any URL rules saved in config/routes.php, you will need to update them to Yii 2’s pattern-route syntax.

// Old:
'dashboard' => ['action' => 'dashboard/index'],
'settings/fields/new' => 'settings/fields/_edit',
'settings/fields/edit/(?P<fieldId>\d+)' => 'settings/fields/_edit',
'blog/type/([^\/]+)' => 'blog/_type',

// New:
'dashboard' => 'dashboard/index',
'settings/fields/new' => ['template' => 'settings/fields/_edit'],
'settings/fields/edit/<fieldId:\d+>' => ['template' => 'settings/fields/_edit'],
'blog/type/<type:[^\/]+>' => ['template' => 'blog/_type'],

PHP Constants #

The following PHP constants have been deprecated in Craft 3, and will no longer work in Craft 4:

Old New
CRAFT_LOCALE CRAFT_SITE
CRAFT_SITE_URL Use the siteUrl config setting instead

Static Translation Files #

Craft 3 still supports static translations, but the directory structure has changed. Now within your translations/ folder, you should create subdirectories for each locale, and within them, PHP files for each translation category.

The acceptable translation categories are:

Category Description
app Craft’s translation messages
yii Yii’s translation messages
site custom site-specific translation messages
plugin-handle Plugins’ translation messages

In Craft 3, your translations/ folder might look something like this:

translations/
  en-US/
    app.php
    site.php

User Photos #

User photos are stored as assets now. When upgrading to Craft 3, Craft will automatically create a new asset volume called “User Photos”, set to the storage/userphotos/ folder, where Craft previously stored all user photos. However this folder is above your web root and inaccessible to HTTP requests, so until you make this volume publicly accessible, user photos will not work on the front end.

Here’s how you can resolve this:

  1. Move the storage/userphotos/ folder somewhere below your web root (e.g. public_html/userphotos/)
  2. Go to Settings → Assets → Volumes → User Photos and configure the volume based on the new folder location:
    • Update the File System Path setting to point to the new folder location
    • Enable the “Assets in this volume have public URLs” setting
    • Set the correct URL setting for the folder
    • Save the volume

Twig 2 #

Craft 3 uses Twig 2, which has its own breaking changes for templates:

Macros #

Twig 2 requires that you explicitly import macros in each template where you are using them. They are no longer automatically available if a parent template is including them, or even if they were defined in the same template file.

Old:
{% macro foo %}...{% endmacro %}
{{ _self.foo() }}

New:
{% macro foo %}...{% endmacro %}
{% import _self as macros %}
{{ macros.foo() }}

Undefined Blocks #

Twig 1 let you call block() even for blocks that didn’t exist:

{% if block('foo') is not empty %}
    {{ block('foo') }}
{% endif %}

Twig 2 will throw an error unless it’s a defined test:

{% if block('foo') is defined %}
    {{ block('foo') }}
{% endif %}

Template Tags #

The following Twig template tags have been removed:

Old New
{% endpaginate %} (no replacement needed; just delete it)

The following Twig template tags have been deprecated in Craft 3, and will be completely removed in Craft 4:

Old New
{% includecss %} {% css %}
{% includehirescss %} {% css %} (write your own media selector)
{% includejs %} {% js %}
{% includecssfile url %} {% do view.registerCssFile(url) %}
{% includejsfile url %} {% do view.registerJsFile(url) %}
{% includecssresource path %} See Asset Bundles
{% includejsresource path %} See Asset Bundles

Template Functions #

The following template functions have been removed:

Old New
craft.hasPackage() (n/a)
craft.entryRevisions.getDraftByOffset() (n/a)
craft.entryRevisions.getVersionByOffset() (n/a)
craft.fields.getFieldType(type) craft.app.fields.createField(type)
craft.fields.populateFieldType() (n/a)
craft.tasks.areTasksPending() craft.app.queue.getHasWaitingJobs()1
craft.tasks.getRunningTask() (n/a)
craft.tasks.getTotalTasks() (n/a)
craft.tasks.haveTasksFailed() (n/a)
craft.tasks.isTaskRunning() craft.app.queue.getHasReservedJobs()1

1 Only available if the queue component implements craft\queue\QueueInterface.

The following template functions have been deprecated in Craft 3, and will be completely removed in Craft 4:

Old New
round(num) num|round
getCsrfInput() csrfInput()
getHeadHtml() head()
getFootHtml() endBody()
getTranslations() view.getTranslations()|json_encode|raw
craft.categoryGroups.getAllGroupIds() craft.app.categoryGroups.allGroupIds
craft.categoryGroups.getEditableGroupIds() craft.app.categories.editableGroupIds
craft.categoryGroups.getAllGroups() craft.app.categoryGroups.allGroups
craft.categoryGroups.getEditableGroups() craft.app.categories.editableGroups
craft.categoryGroups.getTotalGroups() craft.app.categories.totalGroups
craft.categoryGroups.getGroupById(id) craft.app.categories.getGroupById(id)
craft.categoryGroups.getGroupByHandle(handle) craft.app.categories.getGroupByHandle(handle)
craft.config.[setting] (magic getter) craft.app.config.general.[setting]
craft.config.get(setting) craft.app.config.general.[setting]
craft.config.usePathInfo() craft.app.config.general.usePathInfo
craft.config.omitScriptNameInUrls() craft.app.config.general.omitScriptNameInUrls
craft.config.getResourceTrigger() craft.app.config.general.resourceTrigger
craft.locale() craft.app.language
craft.isLocalized() craft.app.isMultiSite
craft.deprecator.getTotalLogs() craft.app.deprecator.totalLogs
craft.elementIndexes.getSources() craft.app.elementIndexes.sources
craft.emailMessages.getAllMessages() craft.emailMessages.allMessages
craft.emailMessages.getMessage(key) craft.app.emailMessages.getMessage(key)
craft.entryRevisions.getDraftsByEntryId(id) craft.app.entryRevisions.getDraftsByEntryId(id)
craft.entryRevisions.getEditableDraftsByEntryId(id) craft.entryRevisions.getEditableDraftsByEntryId(id)
craft.entryRevisions.getDraftById(id) craft.app.entryRevisions.getDraftById(id)
craft.entryRevisions.getVersionsByEntryId(id) craft.app.entryRevisions.getVersionsByEntryId(id)
craft.entryRevisions.getVersionById(id) craft.app.entryRevisions.getVersionById(id)
craft.feeds.getFeedItems(url) craft.app.feeds.getFeedItems(url)
craft.fields.getAllGroups() craft.app.fields.allGroups
craft.fields.getGroupById(id) craft.app.fields.getGroupById(id)
craft.fields.getFieldById(id) craft.app.fields.getFieldById(id)
craft.fields.getFieldByHandle(handle) craft.app.fields.getFieldByHandle(handle)
craft.fields.getAllFields() craft.app.fields.allFields
craft.fields.getFieldsByGroupId(id) craft.app.fields.getFieldsByGroupId(id)
craft.fields.getLayoutById(id) craft.app.fields.getLayoutById(id)
craft.fields.getLayoutByType(type) craft.app.fields.getLayoutByType(type)
craft.fields.getAllFieldTypes() craft.app.fields.allFieldTypes
craft.globals.getAllSets() craft.app.globals.allSets
craft.globals.getEditableSets() craft.app.globals.editableSets
craft.globals.getTotalSets() craft.app.globals.totalSets
craft.globals.getTotalEditableSets() craft.app.globals.totalEditableSets
craft.globals.getSetById(id) craft.app.globals.getSetById(id)
craft.globals.getSetByHandle(handle) craft.app.globals.getSetByHandle(handle)
craft.i18n.getAllLocales() craft.app.i18n.allLocales
craft.i18n.getAppLocales() craft.app.i18n.appLocales
craft.i18n.getCurrentLocale() craft.app.locale
craft.i18n.getLocaleById(id) craft.app.i18n.getLocaleById(id)
craft.i18n.getSiteLocales() craft.app.i18n.siteLocales
craft.i18n.getSiteLocaleIds() craft.app.i18n.siteLocaleIds
craft.i18n.getPrimarySiteLocale() craft.app.i18n.primarySiteLocale
craft.i18n.getEditableLocales() craft.app.i18n.editableLocales
craft.i18n.getEditableLocaleIds() craft.app.i18n.editableLocaleIds
craft.i18n.getLocaleData() craft.app.i18n.getLocaleById(id)
craft.i18n.getDatepickerJsFormat() craft.app.locale.getDateFormat('short', 'jui')
craft.i18n.getTimepickerJsFormat() craft.app.locale.getTimeFormat('short', 'php')
craft.request.isGet() craft.app.request.isGet
craft.request.isPost() craft.app.request.isPost
craft.request.isDelete() craft.app.request.isDelete
craft.request.isPut() craft.app.request.isPut
craft.request.isAjax() craft.app.request.isAjax
craft.request.isSecure() craft.app.request.isSecureConnection
craft.request.isLivePreview() craft.app.request.isLivePreview
craft.request.getScriptName() craft.app.request.scriptFilename
craft.request.getPath() craft.app.request.pathInfo
craft.request.getUrl() url(craft.app.request.pathInfo)
craft.request.getSegments() craft.app.request.segments
craft.request.getSegment(num) craft.app.request.getSegment(num)
craft.request.getFirstSegment() craft.app.request.segments|first
craft.request.getLastSegment() craft.app.request.segments|last
craft.request.getParam(name) craft.app.request.getParam(name)
craft.request.getQuery(name) craft.app.request.getQueryParam(name)
craft.request.getPost(name) craft.app.request.getBodyParam(name)
craft.request.getCookie(name) craft.app.request.cookies.get(name)
craft.request.getServerName() craft.app.request.serverName
craft.request.getUrlFormat() craft.app.config.general.usePathInfo
craft.request.isMobileBrowser() craft.app.request.isMobileBrowser()
craft.request.getPageNum() craft.app.request.pageNum
craft.request.getHostInfo() craft.app.request.hostInfo
craft.request.getScriptUrl() craft.app.request.scriptUrl
craft.request.getPathInfo() craft.app.request.getPathInfo(true)
craft.request.getRequestUri() craft.app.request.url
craft.request.getServerPort() craft.app.request.serverPort
craft.request.getUrlReferrer() craft.app.request.referrer
craft.request.getUserAgent() craft.app.request.userAgent
craft.request.getUserHostAddress() craft.app.request.userIP
craft.request.getUserHost() craft.app.request.userHost
craft.request.getPort() craft.app.request.port
craft.request.getCsrfToken() craft.app.request.csrfToken
craft.request.getQueryString() craft.app.request.queryString
craft.request.getQueryStringWithoutPath() craft.app.request.queryStringWithoutPath
craft.request.getIpAddress() craft.app.request.userIP
craft.request.getClientOs() craft.app.request.clientOs
craft.sections.getAllSections() craft.app.sections.allSections
craft.sections.getEditableSections() craft.app.sections.editableSections
craft.sections.getTotalSections() craft.app.sections.totalSections
craft.sections.getTotalEditableSections() craft.app.sections.totalEditableSections
craft.sections.getSectionById(id) craft.app.sections.getSectionById(id)
craft.sections.getSectionByHandle(handle) craft.app.sections.getSectionByHandle(handle)
craft.systemSettings.[category] (magic getter) craft.app.systemSettings.getSettings('category')
craft.userGroups.getAllGroups() craft.app.userGroups.allGroups
craft.userGroups.getGroupById(id) craft.app.userGroups.getGroupById(id)
craft.userGroups.getGroupByHandle(handle) craft.app.userGroups.getGroupByHandle(handle)
craft.userPermissions.getAllPermissions() craft.app.userPermissions.allPermissions
craft.userPermissions.getGroupPermissionsByUserId(id) craft.app.userPermissions.getGroupPermissionsByUserId(id)
craft.session.isLoggedIn() not craft.app.user.isGuest
craft.session.getUser() currentUser
craft.session.getRemainingSessionTime() craft.app.user.remainingSessionTime
craft.session.getRememberedUsername() craft.app.user.rememberedUsername
craft.session.getReturnUrl() craft.app.user.getReturnUrl()
craft.session.getFlashes() craft.app.session.getAllFlashes()
craft.session.getFlash() craft.app.session.getFlash()
craft.session.hasFlash() craft.app.session.hasFlash()

Date Formatting #

Craft’s extended DateTime class has been removed in Craft 3. Here’s a list of things you used to be able to do in your templates, and what the Craft 3 equivalent is. (The DateTime object is represented by the d variable. In reality it could be entry.postDate, now, etc.)

Old New
{{ d }} (treated as a string) {{ d|date('Y-m-d') }}
{{ d.atom() }} {{ d|atom }}
{{ d.cookie() }} {{ d|date('l, d-M-y H:i:s T')}}
{{ d.day() }} {{ d|date('j') }}
{{ d.iso8601() }} {{ d|date('c') }}
{{ d.localeDate() }} {{ d|date('short') }}
{{ d.localeTime() }} {{ d|time('short') }}
{{ d.month() }} {{ d|date('n') }}
{{ d.mySqlDateTime() }} {{ d|date('Y-m-d H:i:s') }}
{{ d.nice() }} {{ d|datetime('short') }}
{{ d.rfc1036() }} {{ d|date('D, d M y H:i:s O') }}
{{ d.rfc1123() }} {{ d|date('r') }}
{{ d.rfc2822() }} {{ d|date('r') }}
{{ d.rfc3339() }} {{ d|date('Y-m-d\TH:i:sP') }}
{{ d.rfc822() }} {{ d|date('D, d M y H:i:s O') }}
{{ d.rfc850() }} {{ d|date('l, d-M-y H:i:s T') }}
{{ d.rss() }} {{ d|rss }}
{{ d.uiTimestamp() }} {{ d|timestamp('short') }}
{{ d.w3c() }} {{ d|date('Y-m-d\TH:i:sP') }}
{{ d.w3cDate() }} {{ d|date('Y-m-d') }}
{{ d.year() }} {{ d|date('Y') }}

Currency Formatting #

The |currency filter now maps to craft\i18n\Formatter::asCurrency(). It still works the same, but the stripZeroCents argument has been renamed to stripZeros, and pushed back a couple notches, so you will need to update your templates if you were setting that argument.

Old:
{{ num|currency('USD', true) }}
{{ num|currency('USD', stripZeroCents = true) }}

New:
{{ num|currency('USD', stripZeros = true) }}

Element Queries #

Query Params #

The following params have been removed:

Element Type Old Param New Param
All of them childOf relatedTo.sourceElement
All of them childField relatedTo.field
All of them parentOf relatedTo.targetElement
All of them parentField relatedTo.field
All of them depth level
Tag name title
Tag setId groupId
Tag set group
Tag orderBy:"name" orderBy:"title"

The following params are now deprecated in Craft 3, and will be completely removed in Craft 4:

Element Type Old Param New Param
All of them order orderBy
All of them locale siteId or site
All of them localeEnabled enabledForSite
All of them relatedTo.sourceLocale relatedTo.sourceSite
Asset source volume
Asset sourceId volumeId
Matrix Block ownerLocale ownerSite or ownerSiteId

limit Param #

The limit param is now set to null (no limit) by default, rather than 100.

Setting Params to Arrays #

If you want to set a param value to an array, you now must type out the array brackets.

Old:
{% set query = craft.entries()
    .relatedTo('and', 1, 2, 3) %}

New:
{% set query = craft.entries()
    .relatedTo(['and', 1, 2, 3]) %}

Cloning Element Queries #

In Craft 2, each time you call a parameter-setter method (e.g. .type('article')), the method would:

  1. clone the ElementCriteriaModel object
  2. set the parameter value on the cloned object
  3. return the cloned object

That made it possible to execute variations of an element query, without affecting subsequent queries. For example:

{% set query = craft.entries.section('news') %}
{% set articleEntries = query.type('article').find() %}
{% set totalEntries = query.total() %}

Here .type() is applying the type parameter to a clone of query, so it had no effect on query.total(), which will still return the total number of News entries, regardless of their entry types.

This behavior has changed in Craft 3, though. Now any time you call a parameter-setter method, the method will:

  1. set the parameter value on the current element query
  2. return the element query

Which means in the above code example, totalEntries will be set to the total Article entries, as the type parameter will still be applied.

If you have any templates that count on the Craft 2 behavior, you can fix them using the clone() function.

{% set query = craft.entries.section('news') %}
{% set articleEntries = clone(query).type('article').all() %}
{% set totalEntries = query.count() %}

Query Methods #

The following methods have been removed:

Old New
findElementAtOffset(offset) nth(offset)

The following methods are now deprecated in Craft 3, and will be completely removed in Craft 4:

Old New
ids(criteria) ids() (setting criteria params here is now deprecated)
find() all()
first() one()
last() inReverse().one() (see last())
total() count()

Treating Queries as Arrays #

Support for treating element queries as if they’re arrays has been deprecated in Craft 3, and will be completely removed in Craft 4.

When you need to loop over an element query, you should start explicitly calling .all(), which will execute the database query and return the array of results:

Old:
{% for entry in craft.entries.section('news') %}...{% endfor %}
{% for asset in entry.myAssetsField %}...{% endfor %}

New:
{% for entry in craft.entries.section('news').all() %}...{% endfor %}
{% for asset in entry.myAssetsField.all() %}...{% endfor %}

When you need to to get the total number of results from an element query, you should call the .count() method:

Old:
{% set total = craft.entries.section('news')|length %}

New:
{% set total = craft.entries.section('news').count() %}

Alternatively, if you already needed to fetch the actual query results, and you didn’t set the offset or limit params, you can use the |length filter to find the total size of the results array without the need for an extra database query.

{% set entries = craft.entries()
    .section('news')
    .all() %}
{% set total = entries|length %}

last() #

last() was deprecated in Craft 3 because it isn’t clear that it needs to run two database queries behind the scenes (the equivalent of query.nth(query.count() - 1)).

In most cases you can replace calls to .last() with .inReverse().one() and get the same result, without the extra database query. (inReverse() will reverse the sort direction of all of the ORDER BY columns in the generated SQL.)

{# Channel entries are ordered by `postDate DESC` by default, so this will swap
   it to `postDate ASC`, returning the oldest News entry: #} 

{% set oldest = craft.entries()
    .section('news')
    .inReverse()
    .one() %}

There are two cases where inReverse() won’t work as expected, though:

In those cases, you can just replace the .last() call with what it’s already doing internally:

{% set query = craft.entries()
    .section('news') %}
{% set total = query.count() %}
{% set last = query.nth(total - 1) %}

Elements #

The following element properties have been removed:

Element Type Old Property New Property
Tag name title

The following element properties have been deprecated in Craft 3, and will be completely removed in Craft 4:

Old New
locale siteId, site.handle, or site.language

Models #

The following model methods have been deprecated in Craft 3, and will be completely removed in Craft 4:

Old New
getError('field') getFirstError('field')

Locales #

The following locale methods have been deprecated in Craft 3, and will be completely removed in Craft 4:

Old New
getId() id
getName() getDisplayName(craft.app.language)
getNativeName() getDisplayName()

Request Params #

Your front-end <form>s and JS scripts that submit to a controller action will need to be updated with the following changes.

action Params #

action params must be rewritten in in kebab-case rather than camelCase.

Old:
<input type="hidden" name="action" value="entries/saveEntry">

New:
<input type="hidden" name="action" value="entries/save-entry">

The following controller actions have been removed:

Old New
categories/create-category categories/save-category
users/validate users/verify-email
users/save-profile users/save-user

redirect Params #

redirect params must be hashed now.

Old:
<input type="hidden" name="redirect" value="foo/bar">

New:
<input type="hidden" name="redirect" value="{{ 'foo/bar'|hash }}">

The redirectInput() function is provided as a shortcut.

{{ redirectInput('foo/bar') }}

The following redirect param tokens are no longer supported:

Controller Action Old Token New Token
entries/save-entry {entryId} {id}
entry-revisions/save-draft {entryId} {id}
entry-revisions/publish-draft {entryId} {id}
fields/save-field {fieldId} {id}
globals/save-set {setId} {id}
sections/save-section {sectionId} {id}
users/save-user {userId} {id}

CSRF Token Params #

CSRF protection is enabled by default in Craft 3. If you didn’t already have it enabled (via the enableCsrfProtection config setting), each of your front-end <form>s and JS scripts that submit to a controller action will need to be updated with a new CSRF token param, named after your csrfTokenName config setting value (set to 'CRAFT_CSRF_TOKEN' by default).

{% set csrfTokenName = craft.app.config.general.csrfTokenName %}
{% set csrfToken = craft.app.request.csrfToken %}
<input type="hidden" name="{{ csrfTokenName }}" value="{{ csrfToken }}">

The csrfInput() function is provided as a shortcut.

{{ csrfInput() }}

Memcache #

If you are using memcache for your cacheMethod config setting and you did not have useMemcached set to true in your craft/config/memcache.php config file, you’ll need to install memcached on your server. Craft 3 will only use it because there is not a PHP 7 compatible version of memcache available.

DbCache #

If you are using db for your cacheMethod config setting, you’ll need to manually execute some SQL before attempting the Craft 3 update.

MySQL:

DROP TABLE IF EXISTS craft_cache;

CREATE TABLE craft_cache (
    id char(128) NOT NULL PRIMARY KEY,
    expire int(11),
    data BLOB,
    dateCreated datetime NOT NULL,
    dateUpdated datetime NOT NULL,
    uid char(36) NOT NULL DEFAULT 0
);

PostgreSQL:

DROP TABLE IF EXISTS craft_cache;

CREATE TABLE craft_cache (
    id char(128) NOT NULL PRIMARY KEY,
    expire int4,
    data BYTEA,
    dateCreated timestamp NOT NULL,
    dateUpdated timestamp NOT NULL,
    uid char(36) NOT NULL DEFAULT '0'::bpchar
);

Note that these examples use the default craft/config/db.php config setting of craft.

If you have changed that config setting, you will want to adjust the examples accordingly.

Plugins #

See Updating Plugins for Craft 3.