Adding Custom Fields to Product, Category, or Customer
Adding Custom Fields to Product, Category, or Customer
~8 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
Chapter 8 introduced the pattern using a product example. This chapter looks at three concrete, particularly common extension points in detail - including the respective pitfalls around $value.
Category: a custom banner field
extend type CategoryInterface {
mironsoft_banner_image: String
@resolver(class: "Mironsoft\\GraphqlDemo\\Model\\Resolver\\Category\\BannerImage")
@doc(description: "URL of a custom marketing banner shown above the category grid")
}Unlike for products, the base type here is deliberately named CategoryInterface, not Category - Magento isn't always consistent in how it names its types. A quick look at introspection (chapter 3) or at vendor/magento/module-catalog-graph-ql/etc/schema.graphqls clears this up quickly, before extend type silently fails.
<?php
declare(strict_types=1);
namespace Mironsoft\GraphqlDemo\Model\Resolver\Category;
use Magento\Catalog\Model\Category;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
* Resolves the custom mironsoft_banner_image field on categories.
*/
class BannerImage implements ResolverInterface
{
/**
* Reads the mironsoft_banner_image attribute off the resolved category model.
*
* @param Field $field Resolved GraphQL field configuration
* @param mixed $context Resolver context
* @param ResolveInfo $info GraphQL resolve tree info
* @param array|null $value Parent CategoryInterface resolver's value
* @param array|null $args Arguments passed to this field
* @return string|null
*/
public function resolve(
Field $field,
$context,
ResolveInfo $info,
?array $value = null,
?array $args = null
): ?string {
/** @var Category|null $category */
$category = $value['model'] ?? null;
$bannerImage = $category?->getData('mironsoft_banner_image');
return is_string($bannerImage) && $bannerImage !== ''
? $bannerImage
: null;
}
}Customer: reading a custom preference
CustomerOutput - the return type of the customer query - is a particularly sensitive extension point, since it practically always carries personal data. The resolver here doesn't automatically get its own customer ID handed to it - it has to read it explicitly from the context, just like the protected favorites mutation later in chapter 18:
extend type CustomerOutput {
mironsoft_newsletter_topic: String
@resolver(class: "Mironsoft\\GraphqlDemo\\Model\\Resolver\\Customer\\NewsletterTopic")
@doc(description: "The customer's preferred newsletter topic")
}Here too, in practice the resolver usually ends up with a value from $value['model'] (the loaded \Magento\Customer\Model\Customer) - Magento's own CustomerOutput resolver already passes the loaded customer model along under this key.
When a new EAV attribute pays off vs. a GraphQL field
For products and categories, it's tempting to add every new field directly as its own EAV attribute via eav_attribute and make it editable through an admin form. That pays off when the value should genuinely be editorially maintained in the admin area (see the Admin Grids & Forms series). For purely computed values - such as mironsoft_badge_text from chapter 8, derived from existing data - a plain GraphQL resolver with no dedicated database attribute is entirely sufficient.
Achtung: extend type on a core type that internally relies on batch resolvers (e.g. price fields on products) can quickly become an N+1 bottleneck if your own field is naive and doesn't batch - a list of 50 products then triggers 50 separate extra database queries just for your custom field. Chapter 20 shows, using the events project, how to avoid this with batch resolvers - the same pattern also works for extended core types.
Chapter 10 wraps up block 3 and takes one more foundational look at each of the four parameters of resolve() - knowledge that's assumed for the custom events API starting in block 4.