[3.x]: Unable to programmatically create a custom field and assign to Product.
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 🤣
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
Could you please let us if this persists in Commerce 4 please? We will then take a look. Thanks!
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, andvariantFieldLayoutId
explicitly when creating the ProductType?