Resolvers

For arlanix/product-feed 1.0.0

Resolvers

This page is for the PHP developer who needs a mapping source that no attribute holds and no template can build: a value worked out per product, such as a weight with its unit or a price in another form. A resolver is that source. Once registered, it is listed in the Computed group of the mapping's Value picker, beside the module's own: Product URL, Image URL, Price with currency and the rest.

A resolver runs once per product row and field. What it returns goes through the row's dictionary and modifiers like any other value; see Where each one runs.

Write a resolver

The following resolver gives a product's weight with its unit, as Google's shipping_weight takes it, with the unit as an argument the store chooses:

<?php
declare(strict_types=1);

namespace Vendor\Module\Model\Resolver;

use Arlanix\Feed\Model\Mapping\MappingException;
use Arlanix\Feed\Model\Mapping\Resolver\ReadsAttributesInterface;
use Arlanix\Feed\Model\Mapping\Resolver\ResolverInterface;
use Arlanix\Feed\Model\Mapping\Resolver\SuggestsArgumentsInterface;
use Arlanix\Feed\Model\Mapping\Resolver\ValidatesArgumentsInterface;
use Arlanix\Feed\Model\Mapping\RowContext;
use Magento\Framework\Phrase;

class ShippingWeight implements
    ResolverInterface,
    ReadsAttributesInterface,
    SuggestsArgumentsInterface,
    ValidatesArgumentsInterface
{
    private const UNITS = ['kg', 'lb'];

    public function resolve(RowContext $context, array $arguments = []): mixed
    {
        $weight = $context->getValue('weight');
        if ($weight === null || $weight === '') {
            return null;
        }

        return sprintf('%s %s', (float)$weight, $arguments['unit'] ?? 'kg');
    }

    public function getLabel(): Phrase
    {
        return __('Shipping weight — the weight with its unit');
    }

    public function attributesFor(array $arguments): array
    {
        return ['weight'];
    }

    public function argumentSuggestions(): array
    {
        return [[
            'name' => 'unit',
            'label' => (string)__('Unit'),
            'items' => [
                ['value' => 'kg', 'label' => 'kg'],
                ['value' => 'lb', 'label' => 'lb'],
            ],
        ]];
    }

    public function validateArguments(array $arguments): void
    {
        $unit = $arguments['unit'] ?? 'kg';
        if (!in_array($unit, self::UNITS, true)) {
            throw new MappingException(new Phrase('Unknown unit "%1".', [$unit]));
        }
    }
}

All the interfaces are in Arlanix\Feed\Model\Mapping\Resolver. Only ResolverInterface is required:

Method Does
resolve(RowContext $context, array $arguments): mixed Returns the field's value for one product row. Return null or '' when the product has none: a row marked Required then leaves the product out, and the store sees why. $arguments holds the store's choices, keyed by the names argumentSuggestions() declares, as strings.
getLabel(): Phrase The name in the Value picker, which shows it with the alias: Shipping weight — the weight with its unit (shipping_weight).

RowContext holds the row: getValue('weight') reads a field, getValue('parent.sku') a field of a configurable parent, getProductId() and getStoreId() say whose row it is. For every method, see What a row holds.

An exception from resolve() stops the whole run. Throw only for a fault the store has to fix; for a product with no value, return null.

Declare the attributes it reads

A row holds only the product attributes some feed of its store view reads, so the catalogue stays small. A resolver that reads an attribute declares it with ReadsAttributesInterface:

Method Does
attributesFor(array $arguments): array Returns the attribute codes the resolver reads with these arguments, such as ['weight'].

When a feed is saved with a row that declares an attribute the catalogue was not built with, the catalogue is built again, and the feeds wait until it is; see The indexers. An attribute that is read but not declared is null, unless another feed of the store view happens to read it.

These fields are in every row and need no declaration: entity_id, sku, type_id, name, status, visibility, price, final_price, minimal_price, has_discount, currency, qty, is_in_stock, stock_status and url. Declare anything else, the image URLs included: image_url loads the image attribute it is made from.

Offer arguments

An argument lets one resolver serve several rows: the image resolver's size, the unit of the example. With SuggestsArgumentsInterface, the mapping row shows a control for each argument:

Method Does
argumentSuggestions(): array Returns one entry per argument: name, the key in $arguments; label, the control's label; items, the choices as value and label. An empty items gives a text box.

Copy from never fills an argument in: the store chooses it on the row.

Check the arguments

With ValidatesArgumentsInterface, wrong arguments are refused when the feed is saved, and checked again before Preview and before every run:

Method Does
validateArguments(array $arguments): void Throws Arlanix\Feed\Model\Mapping\MappingException naming what is wrong. The store sees the message after the field's name: Field "shipping_weight": Unknown unit "oz".

Read the feed

A resolver whose answer depends on the feed, such as the feed's own category assignments, implements FeedAwareInterface:

Method Does
forFeed(Feed $feed): void Called with the feed before its rows are mapped. Keep what you need of it for the calls to resolve() that follow.

Register it

In your module's etc/di.xml, add the resolver to ResolverPool under its alias:

<type name="Arlanix\Feed\Model\Mapping\Resolver\ResolverPool">
    <arguments>
        <argument name="resolvers" xsi:type="array">
            <item name="shipping_weight" xsi:type="object">Vendor\Module\Model\Resolver\ShippingWeight</item>
        </argument>
    </arguments>
</type>

The alias is what a mapping row stores, and what a field catalog's default_resolver names; see Field catalogs. The aliases the module registers are attribute, static, image, additional_images, product_url, price, sale_price, sale_window, product_type and google_product_category; an item under one of those names replaces the module's resolver.

Then run bin/magento setup:upgrade and bin/magento cache:clean config. Open a feed, choose Shipping weight in the Value picker of a row, choose a unit, save, and check the value with Preview.