Flyweight is a structural pattern that minimises memory usage by sharing as much data as possible between similar objects. Instead of creating thousands of objects with repeated data – you create one object and references to it. In Magento 2 this pattern appears in many places: translations, store configuration, EAV objects. I show an implementation from scratch and how to recognise it in existing code.
The problem – thousands of identical objects
<?php
// Without Flyweight - each product creates its own config objects
class ProductWithoutFlyweight
{
private array $taxConfig;
private array $currencyConfig;
private array $storeConfig;
public function __construct(private string $sku, private float $price)
{
// Each of 10,000 products loads the same data
$this->taxConfig = ['rate' => 0.23, 'type' => 'vat', 'display' => 'gross'];
$this->currencyConfig = ['code' => 'PLN', 'symbol' => 'zl', 'precision' => 2];
$this->storeConfig = ['id' => 1, 'locale' => 'pl_PL', 'timezone' => 'Europe/Warsaw'];
// ~200 bytes * 10,000 products = ~2MB just for repeated data
}
}
// With Flyweight - shared data exists only once
class ProductFlyweight
{
public function __construct(
private string $sku,
private float $price,
private SharedProductConfig $sharedConfig // same instance for all
) {}
}
Flyweight implementation
<?php
declare(strict_types=1);
// Flyweight - only immutable, shared data
final class ProductTypeConfig
{
public function __construct(
public readonly string $type,
public readonly float $taxRate,
public readonly bool $isShippable,
public readonly bool $isDownloadable,
public readonly array $allowedAttributes,
) {}
}
// Flyweight Factory - guarantees instance uniqueness
class ProductTypeConfigFactory
{
/** @var ProductTypeConfig[] */
private array $pool = [];
public function get(string $type): ProductTypeConfig
{
if (!isset($this->pool[$type])) {
$this->pool[$type] = $this->create($type);
}
return $this->pool[$type]; // always the same instance for a given type
}
private function create(string $type): ProductTypeConfig
{
return match($type) {
'simple' => new ProductTypeConfig(
type: 'simple',
taxRate: 0.23,
isShippable: true,
isDownloadable: false,
allowedAttributes: ['weight', 'dimensions', 'color', 'size'],
),
'virtual' => new ProductTypeConfig(
type: 'virtual',
taxRate: 0.23,
isShippable: false,
isDownloadable: false,
allowedAttributes: ['duration', 'access_period'],
),
'downloadable' => new ProductTypeConfig(
type: 'downloadable',
taxRate: 0.23,
isShippable: false,
isDownloadable: true,
allowedAttributes: ['file_size', 'file_type', 'max_downloads'],
),
default => throw new \InvalidArgumentException("Unknown product type: {$type}"),
};
}
public function poolSize(): int { return count($this->pool); }
}
// Context - data unique to each object
class Product
{
public function __construct(
private readonly int $id,
private readonly string $sku,
private float $price,
private readonly ProductTypeConfig $typeConfig, // flyweight - shared
) {}
public function getTaxAmount(): float
{
return $this->price * $this->typeConfig->taxRate;
}
public function canShip(): bool
{
return $this->typeConfig->isShippable;
}
public function getGrossPrice(): float
{
return $this->price * (1 + $this->typeConfig->taxRate);
}
}
// Usage
$factory = new ProductTypeConfigFactory();
$products = [];
for ($i = 1; $i <= 10000; $i++) {
$type = ['simple', 'virtual', 'downloadable'][random_int(0, 2)];
$products[] = new Product(
id: $i,
sku: "SKU-{$i}",
price: mt_rand(999, 99999) / 100,
typeConfig: $factory->get($type), // shared instance
);
}
echo "Products: " . count($products) . "\n";
echo "Config objects in pool: " . $factory->poolSize() . "\n"; // always 3, not 10,000
Flyweight with extrinsic state – translations
<?php
declare(strict_types=1);
class TranslationDictionary
{
private array $translations = [];
public function load(string $locale): void
{
$file = __DIR__ . "/i18n/{$locale}.csv";
if (!file_exists($file)) return;
foreach (file($file, FILE_IGNORE_NEW_LINES) as $line) {
[$key, $value] = str_getcsv($line);
$this->translations[$locale][$key] = $value;
}
}
public function translate(string $key, string $locale): string
{
return $this->translations[$locale][$key] ?? $key;
}
}
class TranslationDictionaryFactory
{
private array $dictionaries = [];
public function __construct(private TranslationDictionary $dictionary) {}
public function getForLocale(string $locale): TranslationProxy
{
if (!isset($this->dictionaries[$locale])) {
$this->dictionary->load($locale);
$this->dictionaries[$locale] = new TranslationProxy($this->dictionary, $locale);
}
return $this->dictionaries[$locale];
}
}
class TranslationProxy
{
public function __construct(
private TranslationDictionary $dictionary, // flyweight
private string $locale, // extrinsic state
) {}
public function __($key): string
{
return $this->dictionary->translate($key, $this->locale);
}
}
Flyweight in Magento 2 – where to find it
<?php
// 1. Magento\Eav\Model\Config - EAV attribute cache
// Instead of loading the same attributes for each product - cache in Config object
/** @var \Magento\Eav\Model\Config $eavConfig */
$attribute = $eavConfig->getAttribute('catalog_product', 'color');
// Second call - returns the same instance from cache, does not load from DB
// 2. Magento\Store\Model\StoreManagerInterface
// One Store object shared throughout the request
// 3. Flyweight implementation in custom module - on-demand cache
class AttributeOptionCache
{
private array $cache = [];
public function __construct(
private \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory $factory
) {}
public function getOptions(int $attributeId): array
{
if (!isset($this->cache[$attributeId])) {
$collection = $this->factory->create();
$collection->setAttributeFilter($attributeId);
$collection->setStoreFilter(0);
$this->cache[$attributeId] = $collection->toOptionArray();
}
return $this->cache[$attributeId]; // same array for every call
}
}
When to use Flyweight
| Criterion | Flyweight fits | Flyweight unnecessary |
|---|---|---|
| Number of objects | Thousands of similar objects | A few dozen |
| Repeated data | Large portion of state is shared | Every object is unique |
| Memory | RAM is a problem | Memory is not an issue |
| Immutable data | Shared data is readonly | State changes frequently |
Summary
Flyweight solves a specific problem: too many objects with repeated data consuming too much memory. The key is splitting into intrinsic state (shared, immutable – the Flyweight) and extrinsic state (unique to each context). The Factory guarantees that each unique intrinsic state exists exactly once. In PHP this pattern is particularly valuable in CLI scripts processing large collections – product imports, report generation, bulk order operations.
