Upgrading from Commerce 4

Commerce 5 introduces powerful multi-store functionality and transforms the product management experience.

If you’re currently running Commerce 3, you’ll need to follow the Commerce 4 upgrade guide, first.

# Preparing for the Upgrade

Before you begin, make sure that you have…

  • …updated to the latest version of Craft 4 and Commerce 4;
  • …familiarized yourself with the Craft 5 upgrade process;
  • …reviewed the changelog (opens new window) and changes further down this page;
  • …resolved all deprecation warnings from Commerce 4 that need fixing;
  • …backed up your database and files, in case anything goes awry;

Once you’ve completed these steps, you’re ready to begin the upgrade process.

If you are coming from Commerce 3 in two stages, run the Commerce 4 upgrade command to migrate customer data before continuing to Commerce 5!

# Performing the Upgrade

The Commerce 5 happens at the same time you upgrade to Craft 5. As you alter version constraints in composer.json, set craftcms/commerce to ^5.0.0-beta.1.

Upgrading from Craft 4
Take a peek at what’s new in Craft 5.

Commerce migrations will run just after Craft’s, and content for your products and variants will be moved into the new storage architecture. Once you’re running the latest version of Craft Commerce, you’ll need to update your templates and any custom code relevant to the topics detailed below.

# Products + Variants

The way you interact with products and variants has changed in some subtle, non-breaking ways.

# Nested Elements

Variants take advantage of Craft 5’s new nested elements system, meaning they are managed via an embedded element index as a table or cards. Products with many variants benefit from search, filtering, pagination, sorting, and customizable previews.

During the upgrade, variants will be automatically migrated to this new structure. To clarify this relationship, we are deprecating craft\commerce\elements\Variant::getProduct() (opens new window) in favor of the underlying element class’s getOwner() (opens new window) method:

{% set smallThings = craft.variants()
  .weight('< 1')
  .with(['owner'])
  .all() %}

<ul>
  {% for smallThing in smallThings %}
    <li>{{ smallThing.getOwner().title }}: {{ smallThing.title }}</li>
  {% endfor %}
</ul>

# Product Types

Product types now have a Maximum Variants setting that replaces the hasVariants setting. During the upgrade, product types that had hasVariants off were given a maxVariants of 1. Logic based on a product type’s configuration may need to be updated:

 










{% if product.type.hasVariants %}
  <select name="purchasableId">
    {% for variant in product.variants %}
      <option value="{{ variant.id }}">{{ variant.title }}</option>
    {% endfor %}
  </select>
  {# ... #}
{% else %}
  <button name="purchasableId" value="{{ product.defaultVariantId }}">Add to Cart</button>
{% endif %}

# Field Layouts

The interface for single-variant products is the same as multi-variant products, so you may now use product and variant field layouts at the same time. This also means that all purchasable information is transparently edited via a variant (rather than it appearing within the sidebar of the product edit screen).

To give you more flexibility over how built-in variant attributes are managed, fields that were previously in the product’s sidebar (or a split view of the inline variant editor) are now discrete field layout elements. Use the Variant Fields tab in any product type edit screen to arrange these in a way that makes sense for your store.

# Multi-Store

Commerce now supports multiple storefronts, each with unique configuration.

Stores
Set up your sales channels to match your customers’ needs and locations.

A number of settings have moved from the “global” space into the context of individual stores:

  • autoSetNewCartAddresses
  • autoSetCartShippingMethodOption
  • autoSetPaymentSource
  • allowEmptyCartOnCheckout
  • allowCheckoutWithoutPayment
  • allowPartialPaymentOnCheckout
  • requireShippingAddressAtCheckout
  • requireBillingAddressAtCheckout
  • requireShippingMethodSelectionAtCheckout
  • useBillingAddressForTax
  • validateOrganizationTaxIdAsVatId
  • orderReferenceFormat
  • freeOrderPaymentStrategy
  • minimumTotalPriceStrategy

If you had previously used these in your config/commerce.php file, they will be ignored. During the upgrade, Commerce copies each setting onto the new default/primary store, and saves them in project config. To review or update these settings, visit

  1. Commerce
  2. System Settings
  3. Stores
  4. Primary
.

The singular store service has been deprecated. In its place, the stores service provides access to information about all the configured stores. Calls to craft.commerce.store.store should be replaced with the new global currentStore variable, or an explicit call via the new service:

{# Get the store’s address: #}
{% set address = craft.commerce.store.store.getLocationAddress() %}

Non-project config settings (like the order condition formula and markets) are now accessible via a dedicated StoreSettings (opens new window) model:

{% set allowedCountries = currentStore.settings.countryList %}

{% if cart.shippingAddress.countryCode not in countries|keys %}
  <p>Sorry, we don’t fulfill orders to or from {{ countries[cart.shippingAddress.countryCode].name }}!</p>
{% endif %}

# Prices

Commerce 5 changes how pricing, sales, and discounts work. Variants now define a Price and a Promotional Price, with variations handled via catalog pricing rules.

This differs from the previous sales engine in a couple of fundamental ways:

  • Prices are pre-computed for each possible combination of purchasable (and customer, if applicable);
  • The final price and promotional price (previously the “sale price”) can be used for filtering and sorting element queries;

A pricing rule either sets or reduces the price (or promotional price) of a purchasable based on its original price (or promotional price). You can even configure rules that cross-populate prices and promotional prices—like setting the normal price to the promotional price for customers in certain user group, then taking an additional percentage off the original promotional price.

Commerce will try and keep your pricing up-to-date, but some events (like a user being removed from a group) can cause brief inconsistencies. To ensure the pricing catalog remains accurate, add a daily CRON task:

0 0 * * * /usr/bin/env php /path/to/craft commerce/catalog-pricing/generate
Catalog Pricing
Read more about the new pricing engine.

# Sales

Upgraded projects that had at least one sale configured will retain access to the sales feature. New projects, however, should use the new catalog pricing engine.

# Templates

We expect minimal impact to front-end templates, for most projects. Single-store installations will function almost identically, before and after the upgrade.

# Global Variables

A new currentStore variable is available in all templates, containing a reference to the craft\commerce\models\Store (opens new window) for the current site.

# Properties & Methods

Some properties and methods have been moved from products to variants (along with their corresponding query methods):

  • craft\commerce\elements\Product::hasUnlimitedStock() is deprecated. Check each variant’s inventoryTracked property, instead:
    {% set hasVariantWithUntrackedStock = product.variants.contains('inventoryTracked', true) %}
    
  • craft\commerce\elements\Variant::getProduct() has been deprecated in favor of the nested element interface’s getOwner() method. getProduct() will continue to work throughout the 5.x lifecycle.
  • craft\commerce\elements\Product::getVariants() now returns a special type of collection. See the hasUnlimitedStock replacement, above, for an example of how this can be used.

# Queries

Some product query params have been moved to variant queries to agree with their shift to the base craft\commerce\base\Purchasable (opens new window) class, but their accepted arguments remain the same:

  • shippingCategory()
  • shippingCategoryId()
  • taxCategory()
  • taxCategoryId()
  • availableForPurchase()

If you are using any of these parameters in product queries, you may need to replace them with hasVariant(), and pass a variant query with those params:




 


{% set perishableShippingCategory = craft.commerce.shippingCategories.getShippingCategoryByHandle('perishable') %}

{% set perishableGoods = craft.products()
  .shippingCategory(perishableShippingCategory)
  .all() %}

Variant queries’ hasUnlimitedStock() param (inherited from PurchasableQuery (opens new window)) has also been deprecated—use the new inventoryTracked() param, instead. Note that the meaning of the boolean param has inverted, as well: when querying for variants with “unlimited” or untracked inventory, hasUnlimitedStock(true) translates to inventoryTracked(false).

# Console Commands

Commerce 5 adds one new console command, to help keep catalog pricing up-to-date:

commerce/pricing-catalog/generate
Fully regenerates pricing data for all purchasables.

# API Changes

To support multi-store functionality and the new pricing catalog, there are a number of breaking API changes. Review these and other deprecations in the changelog (opens new window).

# Hooks

With the adoption of the universal element editor, some template hooks have been removed:

  • cp.commerce.product.edit.content
  • cp.commerce.product.edit.details