commerce icon indicating copy to clipboard operation
commerce copied to clipboard

[3.x]: Unable to programmatically create a custom field and assign to Product.

Open msbit opened this issue 2 years ago • 2 comments

What happened?

Description

I am unable to programmatically create a custom field, and subsequently assign it to a Product. It works for User, and for Order, but not for Product. I suspect it's related to Product delegating to ProductType for its field layout under certain circumstances, but I can't work it out. I'd be happy to be told how I'm holding this wrong 🤣

Cross post

Steps to reproduce

From a fresh install of Craft + Commerce, under php craft shell, require('foo.php'); where foo.php is:

<?php

use craft\fields\Number;
use craft\models\FieldLayout;
use craft\commerce\elements\Product;
use craft\commerce\models\ProductType;
use craft\commerce\models\ProductTypeSite;

function random()
{
    return array_reduce(range(0, 9), fn ($x) => $x . range('a', 'z')[rand(0, 25)], '');
}

$salt = random();

$result = Craft::$app->getFields()->saveField($field = new Number([
    'handle' => $salt,
    'name' => $salt,
]));
assert($result === true);
assert($field->hasErrors() === false);

$result = Craft::$app->getFields()->saveLayout($layout = new FieldLayout([
    'fields' => [$field],
    'type' => Product::class,
]));
assert($result === true);
assert($layout->hasErrors() === false);

$result = craft\commerce\Plugin::getInstance()->getProductTypes()->saveProductType($productType = new ProductType([
    'handle' => $salt,
    'hasDimensions' => false,
    'hasVariants' => false,
    'name' => $salt,
    'siteSettings' => [
        Craft::$app->getSites()->getCurrentSite()->id => new ProductTypeSite(),
    ],
]));
assert($result === true);
assert($productType->hasErrors() === false);

$result = Craft::$app->getElements()->saveElement($product = new Product([
    'slug' => $salt,
    'title' => $salt,
    'typeId' => $productType->id,
    'variants' => [
        [
            'hasUnlimitedStock' => false,
            'price' => 0,
            'maxQty' => 0,
            'minQty' => 0,
        ],
    ],
    $salt => 42,
]));
assert($result === true);
assert($product->hasErrors() === false);
assert($product->{$salt} === 42);
assert(Craft::$app->getElements()->getElementById($product->id, Product::class)->{$salt} === 42);

Expected behavior

All assertions to pass.

Actual behavior

WARNING  assert(): assert($product->$salt === 42) failed in foo.php on line 58.
WARNING  assert(): assert(Craft::$app->getElements()->getElementById($product->id, Product::class)->$salt === 42) failed in foo.php on line 59.

Craft CMS version

3.8.16

Craft Commerce version

3.4.22.1

PHP version

7.4.33

Operating system and version

macOS 13.4.1

Database type and version

mysql 8.0.34

Image driver and version

No response

Installed plugins and versions

msbit avatar Aug 10 '23 11:08 msbit

Could you please let us if this persists in Commerce 4 please? We will then take a look. Thanks!

lukeholder avatar Nov 20 '23 12:11 lukeholder

Hey @lukeholder, I'm not going to be able to run this on 4.x for a while, but I've modified my test code to the following:

function createFieldLayout(string $type, Field ...$fields): FieldLayout
{
    Craft::$app->getFields()->saveLayout($layout = new FieldLayout([
        'fields' => $fields,
        'tabs' => [
            [   
                'fields' => $fields,
                'name' => $type,
            ],  
        ],  
        'type' => $type,
    ]));
    return $layout;
}
function createProductType(string $handle, string $name = null): ProductType
{
    CommercePlugin::getInstance()->getProductTypes()->saveProductType($productType = new ProductType([
        'fieldLayoutId' => Craft::$app->getFields()->getLayoutByType(Product::class)->id,
        'handle' => $handle,
        'hasDimensions' => false,
        'hasVariants' => true,
        'name' => $name ?? $handle,
        'siteSettings' => [
            Craft::$app->getSites()->getPrimarySite()->id => new ProductTypeSite(),
        ],  
        'variantFieldLayoutId' => Craft::$app->getFields()->getLayoutByType(\craft\commerce\elements\Variant::class)->id,
    ]));
    return $productType;
}

and

function createProduct(ProductType $type, string $slug, array $attributes = [], array $variantAttributes = []): Product
{
    Craft::$app->getElements()->saveElement($product = new Product(array_merge([
        'slug' => $slug,
        'title' => $slug,
        'typeId' => $type->id,
        'variants' => [
            array_merge([
                'price' => 1,
                'stock' => 1,
                'title' => $slug,
            ], $variantAttributes),
        ],  
    ], $attributes)));
    return $product;
}

Running something like:

    createFieldLayout(
        \craft\commerce\elements\Product::class,
        // some fields
    );  

    createFieldLayout(
        \craft\commerce\elements\Variant::class,
        // some fields
    );

then:

createProduct(
    createProductType("YQu4TZ4M9n6JzqkAGJMLq"),
    "8FX3-dEE2TwnZSfNLgPGd",
);

gets me a properly set up Product with the appropriate fields. I'm guessing the pertinent change was providing the:

  • fieldLayoutId, and
  • variantFieldLayoutId

explicitly when creating the ProductType?

msbit avatar Nov 20 '23 12:11 msbit