PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Wzorzec Flyweight – współdzielenie obiektów, cache instancji, Magento 2

by Henryk Tews / wtorek, 26 maja 2026 / Opublikowano w Wzorce projektowe

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.

About Henryk Tews

Co możesz przeczytać następne

Wzorzec Template Method – szkielet algorytmu, hooks, abstract vs hook, porównanie ze Strategy
Interpreter w PHP – własna gramatyka reguł rabatowych, parser, drzewo wyrażeń
Adapter i Facade w PHP – wzorce strukturalne
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}