Flyweight to wzorzec strukturalny który minimalizuje zużycie pamięci przez współdzielenie jak największej ilości danych między podobnymi obiektami. Zamiast tworzyć tysiące obiektów z powtarzającymi się danymi – tworzysz jeden obiekt i referencje do niego. W Magento 2 ten wzorzec pojawia się w wielu miejscach: tłumaczenia, konfiguracja sklepu, obiekty EAV. Pokażę implementację od podstaw i jak go rozpoznać w istniejącym kodzie.
Problem – tysiące identycznych obiektów
<?php
// Bez Flyweight - każdy produkt tworzy własne obiekty konfiguracji
class ProductWithoutFlyweight
{
private array $taxConfig;
private array $currencyConfig;
private array $storeConfig;
public function __construct(private string $sku, private float $price)
{
// Każdy z 10,000 produktów ładuje te same dane
$this->taxConfig = ['rate' => 0.23, 'type' => 'vat', 'display' => 'gross'];
$this->currencyConfig = ['code' => 'PLN', 'symbol' => 'zł', 'precision' => 2];
$this->storeConfig = ['id' => 1, 'locale' => 'pl_PL', 'timezone' => 'Europe/Warsaw'];
// ~200 bajtów * 10,000 produktów = ~2MB tylko na powtarzające się dane
}
}
// Z Flyweight - współdzielone dane istnieją tylko raz
class ProductFlyweight
{
public function __construct(
private string $sku,
private float $price,
private SharedProductConfig $sharedConfig // ta sama instancja dla wszystkich
) {}
}
Implementacja Flyweight
<?php
declare(strict_types=1);
// Flyweight - tylko niezmienne, współdzielone dane
final class ProductTypeConfig
{
public function __construct(
public readonly string $type, // simple, virtual, bundle
public readonly float $taxRate,
public readonly bool $isShippable,
public readonly bool $isDownloadable,
public readonly array $allowedAttributes,
) {}
}
// Flyweight Factory - gwarantuje unikalność instancji
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]; // zawsze ta sama instancja dla danego 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); }
}
// Kontekst - dane unikalne dla każdego obiektu
class Product
{
public function __construct(
private readonly int $id,
private readonly string $sku,
private float $price,
private readonly ProductTypeConfig $typeConfig, // flyweight - współdzielony
) {}
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);
}
}
// Użycie
$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), // współdzielona instancja
);
}
echo "Produktów: " . count($products) . "\n";
echo "Obiektów config w puli: " . $factory->poolSize() . "\n"; // zawsze 3, nie 10,000
Flyweight z zewnętrznym stanem – tłumaczenia
<?php
declare(strict_types=1);
// Klasyczny przykład Flyweight w Magento: system tłumaczeń
// Zamiast przechowywać cały słownik w każdym obiekcie - jeden współdzielony
class TranslationDictionary
{
private array $translations = [];
public function load(string $locale): void
{
// Ładuj raz, używaj wszędzie
$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];
}
}
// Proxy który "wygląda" jak osobny słownik ale współdzieli dane
class TranslationProxy
{
public function __construct(
private TranslationDictionary $dictionary, // flyweight
private string $locale, // zewnętrzny stan
) {}
public function __($key): string
{
return $this->dictionary->translate($key, $this->locale);
}
}
Flyweight w Magento 2 – gdzie go znajdziesz
<?php
// 1. Magento\Framework\DataObject\Copy - flyweight dla field mapperów
// 2. Magento\Eav\Model\Config - cache atrybutów EAV
// Zamiast ładować te same atrybuty dla każdego produktu - cache w obiekcie Config
/** @var \Magento\Eav\Model\Config $eavConfig */
$attribute = $eavConfig->getAttribute('catalog_product', 'color');
// Drugi raz - zwraca tę samą instancję z cache, nie ładuje z DB
// 3. Magento\Store\Model\StoreManagerInterface
// Jeden obiekt Store współdzielony przez całe żądanie
// 4. Implementacja Flyweight w custom module - cache na żądanie
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]; // ta sama tablica dla każdego wywołania
}
}
Kiedy stosować Flyweight
| Kryterium | Flyweight pasuje | Flyweight zbędny |
|---|---|---|
| Liczba obiektów | Tysiące podobnych | Kilkadziesiąt |
| Powtarzające się dane | Duża część stanu wspólna | Każdy obiekt unikalny |
| Pamięć | Problem z RAM | Pamięć nie jest kwestią |
| Dane niezmienne | Współdzielone dane readonly | Stan często się zmienia |
Podsumowanie
Flyweight rozwiązuje konkretny problem: za dużo obiektów z powtarzającymi się danymi zajmuje za dużo pamięci. Kluczem jest podział na stan wewnętrzny (współdzielony, niezmienny – Flyweight) i zewnętrzny (unikalny dla każdego kontekstu). Factory gwarantuje że każdy unikalny stan wewnętrzny istnieje dokładnie raz. W PHP ten wzorzec jest szczególnie wartościowy w CLI skryptach przetwarzających duże kolekcje – import produktów, generowanie raportów, masowe operacje na zamówieniach.
