Proxy to wzorzec który stawia pośrednika między klientem a prawdziwym obiektem. Pośrednik implementuje ten sam interfejs co oryginał, więc klient nie widzi różnicy – a Proxy może robić coś dodatkowego: opóźniać inicjalizację, kontrolować dostęp, logować wywołania lub keszować wyniki. W Magento 2 Proxy jest generowane automatycznie dla klas z ciężkimi konstruktorami.
Problem bez Proxy – ciężka inicjalizacja
<?php
// Klasa która przy inicjalizacji robi dużo pracy
class HeavyReportGenerator
{
private array $cachedData = [];
public function __construct(
private DatabaseConnection $db,
private FileSystem $fs,
private CacheInterface $cache
) {
// Ciężki konstruktor - ładuje całą konfigurację raportów przy starcie
$this->cachedData = $this->db->fetchAll('SELECT * FROM report_config');
}
public function generate(string $type): string
{
// ...generowanie raportu
return 'report content';
}
}
// Problem: każda klasa która wstrzykuje HeavyReportGenerator
// ponosi koszt ciężkiego konstruktora nawet jeśli
// nigdy nie wywoła generate()
class OrderController
{
public function __construct(
private HeavyReportGenerator $reportGenerator, // inicjalizowany zawsze
private OrderRepository $orderRepository
) {}
public function index(): void
{
// $this->reportGenerator jest tu nigdy nieużywany
// ale i tak był inicjalizowany przy tworzeniu kontrolera
}
}
Lazy Loading Proxy – implementacja
<?php
declare(strict_types=1);
interface ReportGeneratorInterface
{
public function generate(string $type): string;
}
class HeavyReportGenerator implements ReportGeneratorInterface
{
public function __construct(
private DatabaseConnection $db
) {
// Ciężka inicjalizacja - zapytanie do bazy przy każdym new
sleep(0); // symulacja
}
public function generate(string $type): string
{
return "Raport typu: {$type}";
}
}
// Proxy z leniwą inicjalizacją
class LazyReportGeneratorProxy implements ReportGeneratorInterface
{
private ?HeavyReportGenerator $realGenerator = null;
public function __construct(
private DatabaseConnection $db
) {
// Konstruktor Proxy jest lekki - nic nie inicjalizuje
}
private function getRealGenerator(): HeavyReportGenerator
{
// Prawdziwy obiekt tworzony dopiero przy pierwszym wywołaniu metody
if ($this->realGenerator === null) {
$this->realGenerator = new HeavyReportGenerator($this->db);
}
return $this->realGenerator;
}
public function generate(string $type): string
{
return $this->getRealGenerator()->generate($type);
}
}
Proxy w Magento 2 – automatyczne generowanie
Magento 2 generuje klasy Proxy automatycznie dla klas z ciężkimi konstruktorami. Wystarczy zadeklarować zależność jako KlasaName\Proxy w konstruktorze lub w di.xml:
<?php
namespace Vendor\Module\Model;
// Zamiast wstrzykiwać ciężką klasę bezpośrednio
class OrderExporter
{
public function __construct(
// Magento wygeneruje Proxy który opóźni inicjalizację
private \Magento\Catalog\Model\Product\Action\Proxy $productAction,
private \Magento\Reports\Model\ResourceModel\Report\Collection\Proxy $reportCollection
) {
// Konstruktor jest teraz lekki - Proxy nie inicjalizuje prawdziwych obiektów
}
public function export(array $orderIds): void
{
// Prawdziwe obiekty tworzone dopiero tutaj, przy pierwszym użyciu
$this->productAction->updateAttributes(
$orderIds,
['export_status' => 1],
0
);
}
}
Możesz też wskazać Proxy przez di.xml:
<?xml version="1.0"?>
<config>
<type name="Vendor\Module\Model\OrderExporter">
<arguments>
<argument name="productAction" xsi:type="object">
Magento\Catalog\Model\Product\Action\Proxy
</argument>
</arguments>
</type>
</config>
Protection Proxy – kontrola dostępu
<?php
declare(strict_types=1);
interface CustomerRepositoryInterface
{
public function getById(int $id): CustomerInterface;
public function save(CustomerInterface $customer): CustomerInterface;
public function delete(CustomerInterface $customer): bool;
}
// Proxy kontrolujący dostęp na podstawie uprawnień
class SecureCustomerRepositoryProxy implements CustomerRepositoryInterface
{
public function __construct(
private CustomerRepositoryInterface $repository,
private AuthorizationInterface $authorization
) {}
public function getById(int $id): CustomerInterface
{
// Odczyt - wystarczą podstawowe uprawnienia
$this->checkPermission('Vendor_Module::customer_view');
return $this->repository->getById($id);
}
public function save(CustomerInterface $customer): CustomerInterface
{
// Zapis - wymagane wyższe uprawnienia
$this->checkPermission('Vendor_Module::customer_edit');
return $this->repository->save($customer);
}
public function delete(CustomerInterface $customer): bool
{
// Usunięcie - tylko administrator
$this->checkPermission('Vendor_Module::customer_delete');
return $this->repository->delete($customer);
}
private function checkPermission(string $resource): void
{
if (!$this->authorization->isAllowed($resource)) {
throw new \Magento\Framework\Exception\AuthorizationException(
__('You do not have permission to perform this action.')
);
}
}
}
Caching Proxy – keszowanie wyników
<?php
declare(strict_types=1);
// Proxy keszujący wyniki kosztownych operacji
class CachingProductRepositoryProxy implements ProductRepositoryInterface
{
private array $cache = [];
public function __construct(
private ProductRepositoryInterface $repository,
private \Magento\Framework\Cache\FrontendInterface $cacheFrontend
) {}
public function getById(int $id, bool $editMode = false, ?int $storeId = null, bool $forceReload = false): ProductInterface
{
$cacheKey = "product_{$id}_{$storeId}";
if (!$forceReload && isset($this->cache[$cacheKey])) {
return $this->cache[$cacheKey]; // in-memory cache
}
$cached = $this->cacheFrontend->load($cacheKey);
if (!$forceReload && $cached) {
$product = unserialize($cached);
$this->cache[$cacheKey] = $product;
return $product;
}
$product = $this->repository->getById($id, $editMode, $storeId, $forceReload);
// Zapisz w cache na 1 godzinę
$this->cacheFrontend->save(serialize($product), $cacheKey, [], 3600);
$this->cache[$cacheKey] = $product;
return $product;
}
// Pozostałe metody interfejsu delegują do oryginalnego repozytorium
public function get(string $sku, bool $editMode = false, ?int $storeId = null, bool $forceReload = false): ProductInterface
{
return $this->repository->get($sku, $editMode, $storeId, $forceReload);
}
public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria): \Magento\Framework\Api\SearchResultsInterface
{
return $this->repository->getList($searchCriteria);
}
public function save(ProductInterface $product): ProductInterface
{
$result = $this->repository->save($product);
// Unieważnij cache po zapisie
unset($this->cache["product_{$product->getId()}_"]);
return $result;
}
public function delete(ProductInterface $product): bool
{
$id = $product->getId();
$result = $this->repository->delete($product);
unset($this->cache["product_{$id}_"]);
return $result;
}
public function deleteById(string $sku): bool
{
return $this->repository->deleteById($sku);
}
}
Proxy vs Decorator – gdzie różnica?
Oba wzorce opakowują obiekt i implementują ten sam interfejs. Różnica jest w intencji:
| Aspekt | Proxy | Decorator |
|---|---|---|
| Intencja | Kontrola dostępu do obiektu | Rozszerzenie funkcjonalności |
| Wiedza o oryginale | Sam tworzy lub dostaje oryginał | Zawsze dostaje oryginał z zewnątrz |
| Typowe zastosowania | Lazy loading, cache, auth | Dodawanie warstw logiki |
| W Magento 2 | Generowane klasy Proxy w generated/ | System pluginów (Interceptor) |
Podsumowanie
Proxy to jeden z wzorców który w Magento 2 jest wbudowany w sam framework – generowane klasy Proxy w katalogu generated/ to dokładnie ten wzorzec w akcji. Rozumienie go pomaga świadomie decydować kiedy wstrzyknąć KlasaName\Proxy zamiast samej klasy i kiedy samodzielnie napisać Proxy dla kontroli dostępu lub keszowania.
