Manually Sorting Commerce Products

Craft Commerce adds the powerful product element type, bearing many similarities to the native entry. One way they differ, however, is that products cannot be managed in a manually-sorted, hierarchical structure.

You can, however, create a custom field that allows store managers to hand-sort products.

Add a Commerce Products field to a Global Set #

1. Create a Commerce Products field. #

Navigate to SettingsFields and press New field to create a new field, selecting Commerce Products for its Field Type.

The name, handle, and sources can be whatever you’d like, just remember the handle (orderedProducts, in the screenshot below) because we’ll need it again in a moment.

When you’re finished, press Save.

Ordered Products Field

2. Add your field to a Global Set. #

Next, we’ll add our newly-created field to a global set (or single). You can add the field to an existing global set if you’d like, but we’ll create a new one called “Store Customization.”

Navigate to SettingsGlobals and press New global set, choosing a name and handle and dragging the field from step #1 into the Field Layout for the Global Set.

Press Save. Don’t forget to grant permissions to the new global set or section to any store managers who will need to control product sorting!

Store Customization Global Set

Now you can navigate to Globals and see this new field in the global set we just created.

Press Add a product and select all your products.

Store Customization Global Set Fields

Drag the products into whatever order you’d like and press Save.

3. Use your field in templates. #

Anywhere you’d like to use this custom sorting on the front end, replace craft.products() queries with your global set handle and field handle:

{# Fetch products from our custom field #}
{% set products = storeCustomization.sortedProducts.all() %}

<ol>
  {% for product in products %}
    <li><a href="{{ product.url }}">{{ product.title }}</a></li>
  {% endfor %}
</ol>

Craft returns the related elements in exactly the order they are set in the control panel.

Instead of immediately calling .all() on the custom field, we can use [any other ProductQuery method](/docs/commerce/5.x/products-variants.html#product-query-parameters to hone the results:

{# Filter by product type: #}
{% set sortedWidgets = storeCustomization.sortedProducts
  .type('widget')
  .all() %}

{# Filter by category: #}
{% set sortedWidgets = storeCustomization.sortedProducts
  .relatedTo({
    targetElement: category,
    field: 'primaryCategory',
   })
  .all() %}

Automatically Add New Products #

If we stopped here, store managers would still need to update this field any time they added a new product.

We can use a custom module to listen for new Commerce products and append them to that field, automatically.

See Using Events in a Custom Module for an example of setting up a custom module for the first time.

In your module’s init() method, add the following code to listen for Product::EVENT_AFTER_SAVE and update the Global Set field with a newly-saved product:

use craft\commerce\elements\Product;
use craft\events\ModelEvent;
use yii\base\Event;
use Craft;

Event::on(
    Product::class,
    Product::EVENT_AFTER_SAVE,
    static function(ModelEvent $event) {
        $globalSet = Craft::$app
            ->getGlobals()
            ->getSetByHandle('storeCustomization');

        // Make sure the global set exists before we try and manipulate it:
        if (!$globalSet) {
            return;
        }

        // Is this a *new* product, or just a routine save?
        if (!$event->isNew) {
            return;
        }

        // Get the “Sorted Products” field’s existing product IDs:
        $productIds = $globalSet->sortedProducts->ids();

        // Append the new product ID:
        $productIds[] = $event->sender->id;

        // Set “Sorted Products” field value:
        $globalSet->setFieldValue('sortedProducts', $productIds);

        // Save the global set with the updated field value:
        Craft::$app->getElements()->saveElement($globalSet);
        }
    }
);

Navigate to CommerceProducts and press New product. Enter required fields and press Save.

That new product should now be added to the product list under GlobalsStore CustomizationSorted Products, ready to be sorted into whatever position you’d prefer.

Deleted products will automatically be removed from the custom field, but products explicitly removed from the field will not be re-added!

To ensure a product is always represented in the field, you can adjust the logic like this:

$globalSet = Craft::$app
    ->getGlobals()
    ->getSetByHandle('storeCustomization');

if (!$globalSet) {
    return;
}

$productIds = $globalSet->sortedProducts->ids();

// Check if the product is already here:
if (in_array($productIds, $event->sender->id)) {
    // Yep, it is! Nothing more to do here.
    return;
}

// Ok, it’s missing! Append its ID, and continue as we did before:
$productIds[] = $event->sender->id;

$globalSet->setFieldValue('sortedProducts', $productIds);
Craft::$app->getElements()->saveElement($globalSet);

This skips the check for whether the product is “new” and uses its presence in the relational field to decide whether to act or not.

Applies to Craft CMS 5, Craft CMS 4, Craft CMS 3, Craft Commerce 5, Craft Commerce 4, and Craft Commerce 3.