Prototype Pattern in Magento 2: Abstract Factory erweitert | Mironsoft
AI generated

Prototype Pattern: Abstract Factory erweitert

· Lesezeit: ca. 11 Minuten · Kategorie: Magento 2 · Design Patterns

PRO
clone
Magento 2 · Deep Dive · Design Patterns

Prototype Pattern:
Abstract Factory erweitert

Wenn neue Objekte teuer zu initialisieren sind, kopiert das Prototype Pattern einen vorhandenen Prototyp. Wie Magento clone() und Factory::create() nutzt – und wann du es selbst anwenden solltest.

⏱ 11 Min. Deep Dive Design Patterns PHP 8.4

Kopieren statt neu erstellen

Das Prototype Pattern (GoF, 1994) löst ein spezifisches Problem: Manchmal ist das Erstellen eines neuen Objekts teuer – aufwendige Initialisierung, komplexe Abhängigkeiten, Datenbankabfragen im Constructor. In solchen Fällen ist es effizienter, ein bereits vorhandenes Objekt zu klonen und den Klon anzupassen, statt jedes Mal von vorne anzufangen.

In PHP ist das primäre Werkzeug dafür clone $object. Magento nutzt das Prototype Pattern an mehreren Stellen – manchmal explizit mit clone, häufiger implizit durch seine Factory-Infrastruktur.

1. Das GoF Prototype Pattern

Das Prototype Pattern definiert: Erstelle neue Objekte durch Kopieren (Klonen) eines Prototyp-Objekts anstatt durch direktes Instanziieren einer Klasse:


<?php
// Klassisches Prototype Pattern:
interface Prototype
{
    public function clone(): static;
}

class ExpensiveObject implements Prototype
{
    private array $expensiveData;

    public function __construct()
    {
        // Teurer Constructor: lädt aus DB, initialisiert komplexe Strukturen
        $this->expensiveData = $this->loadFromDatabase();
    }

    private function loadFromDatabase(): array
    {
        sleep(1); // Simuliert teuren DB-Aufruf
        return ['key' => 'value', /* ... 1000 Einträge ... */];
    }

    public function clone(): static
    {
        // PHP-clone kopiert das Objekt ohne Constructor auszuführen
        return clone $this;
    }
}

// Ohne Prototype: Jedes new ExpensiveObject() = 1 Sekunde Wartezeit
$obj1 = new ExpensiveObject(); // 1 Sekunde
$obj2 = new ExpensiveObject(); // 1 Sekunde
$obj3 = new ExpensiveObject(); // 1 Sekunde

// Mit Prototype: Nur der erste Aufruf ist teuer
$prototype = new ExpensiveObject(); // 1 Sekunde
$obj1 = $prototype->clone();        // Instant (kein Constructor)
$obj2 = $prototype->clone();        // Instant
$obj3 = $prototype->clone();        // Instant

2. PHP clone: Shallow vs. Deep Copy

PHP's clone macht standardmäßig eine Shallow Copy – primitive Werte und Strings werden kopiert, aber Objekt-Referenzen bleiben dieselben:


<?php
class Address
{
    public function __construct(
        public string $street,
        public string $city
    ) {}
}

class Customer
{
    public function __construct(
        public string $name,
        public Address $address // Objekt-Referenz!
    ) {}
}

$original = new Customer('Max Muster', new Address('Hauptstr. 1', 'Berlin'));
$clone    = clone $original;

// Primitive werden kopiert:
$clone->name = 'Maria Muster';
echo $original->name; // 'Max Muster' ✓ — unverändert

// Objekt-Referenzen werden NICHT kopiert:
$clone->address->city = 'München';
echo $original->address->city; // 'München' ✗ — verändert!
// Shallow Copy: $original und $clone teilen dasselbe Address-Objekt

// Lösung: __clone() Methode für Deep Copy
class Customer
{
    public function __construct(
        public string $name,
        public Address $address
    ) {}

    public function __clone(): void
    {
        // Deep Copy: Address ebenfalls klonen
        $this->address = clone $this->address;
    }
}

$clone2 = clone $original;
$clone2->address->city = 'Hamburg';
echo $original->address->city; // 'Berlin' ✓ — jetzt unverändert

3. Magento Factory: Prototype unter der Haube

Magentos generierte Factory-Klassen sind tatsächlich eine Form des Prototype Patterns. Beim Erstellen über Factory::create() nutzt der DI-Container intern ObjectManager::create() – das ist semantisch äquivalent zu clone $prototype, aber mit DI-Container-Unterstützung:


<?php
// Generierte Factory (generated/code/Mironsoft/Blog/Model/PostFactory.php)
namespace Mironsoft\Blog\Model;

class PostFactory
{
    public function __construct(
        private readonly \Magento\Framework\ObjectManagerInterface $objectManager,
        private readonly string $instanceName = Post::class
    ) {}

    /**
     * Create new Post instance.
     * Internally: ObjectManager::create() — non-shared, new object.
     * Conceptually similar to: clone $prototype (but with full DI resolution)
     */
    public function create(array $data = []): Post
    {
        return $this->objectManager->create($this->instanceName, $data);
    }
}

// Nutzung: immer Factory statt new oder clone direkt
class PostRepository
{
    public function __construct(
        private readonly PostFactory $postFactory
    ) {}

    public function getById(int $id): Post
    {
        $post = $this->postFactory->create(); // "Klon" des Prototyps via DI
        $this->postResource->load($post, $id);
        return $post;
    }
}

4. DataObject klonen: Magento-Datentransporter


<?php
// Magento DataObject ist ein universeller Key-Value-Container
// Häufig geklont für "Basisdaten + Variationen"

use Magento\Framework\DataObject;

// Prototyp mit Basisdaten:
$baseRateData = new DataObject([
    'carrier'       => 'flatrate',
    'carrier_title' => 'Flat Rate',
    'method_title'  => 'Fixed',
    'currency'      => 'EUR',
]);

// Variation A: Standardlieferung
$standard = clone $baseRateData;
$standard->setData('price', 4.99);
$standard->setData('cost', 3.50);
$standard->setData('method', 'standard');

// Variation B: Expresslieferung (ohne $baseRateData neu laden)
$express = clone $baseRateData;
$express->setData('price', 14.99);
$express->setData('cost', 9.00);
$express->setData('method', 'express');

// DataObject hat kein __clone() — Shallow Copy reicht hier,
// da DataObject nur skalare Werte im Array hält.

5. Shipping Rate Cloning: Konkretes Magento-Beispiel

Im Versandkosten-System klont Magento Rate-Objekte für verschiedene Shipping-Methoden desselben Carriers:


<?php
// In Magento\Shipping\Model\Carrier\Flatrate::collectRates() (vereinfacht):
namespace Magento\OfflineShipping\Model\Carrier;

class Flatrate extends AbstractCarrier
{
    public function collectRates(RateRequest $request): ?Result
    {
        $result = $this->_rateResultFactory->create();

        // Methoden-Objekt für "flatrate_flatrate":
        /** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */
        $method = $this->_rateMethodFactory->create();
        $method->setCarrier($this->_code);
        $method->setCarrierTitle($this->getConfigData('title'));
        $method->setMethod($this->_code);
        $method->setMethodTitle($this->getConfigData('name'));
        $method->setPrice($this->getFinalPriceWithHandlingFee((float)$this->getConfigData('price')));
        $method->setCost((float)$this->getConfigData('price'));

        $result->append($method);

        // Für Multi-Method-Carrier: clone base method, modify price:
        // $expressMethod = clone $method;
        // $expressMethod->setMethod('express');
        // $expressMethod->setPrice(14.99);
        // $result->append($expressMethod);

        return $result;
    }
}

6. Eigenes Prototype Pattern implementieren


<?php
declare(strict_types=1);

namespace Mironsoft\Email\Model;

use Magento\Framework\Mail\MessageInterface;

/**
 * Email template prototype: base template that can be cloned
 * for different recipients without re-parsing the template.
 */
class EmailTemplate
{
    private string $parsedBody = '';
    private array $baseHeaders = [];

    public function __construct(
        private readonly TemplateParser $parser
    ) {}

    /**
     * Initialize the prototype with base template data.
     * Expensive: parses template, loads translations, etc.
     */
    public function initialize(string $templateId): void
    {
        $this->parsedBody   = $this->parser->parse($templateId); // Teuer!
        $this->baseHeaders  = $this->parser->getHeaders($templateId);
    }

    /**
     * Create a new email for a specific recipient.
     * Clones the prototype and customizes recipient-specific data.
     * Much cheaper than calling initialize() again.
     */
    public function createForRecipient(string $email, array $vars = []): static
    {
        $copy = clone $this; // Kein erneuter Template-Parse!
        $copy->applyVariables($vars);
        $copy->setRecipient($email);
        return $copy;
    }

    private function applyVariables(array $vars): void
    {
        foreach ($vars as $key => $value) {
            $this->parsedBody = str_replace('{ {' . $key . '} }', $value, $this->parsedBody);
        }
    }

    private function setRecipient(string $email): void
    {
        $this->baseHeaders['To'] = $email;
    }

    public function __clone(): void
    {
        // parsedBody und baseHeaders sind Arrays/Strings — keine Deep Copy nötig
        // Falls Objekt-Properties: hier klonen
    }
}

// Nutzung: 1x initialisieren, N mal klonen
$template = new EmailTemplate($parser);
$template->initialize('newsletter_welcome'); // 1x teuer

foreach ($recipients as $recipient) {
    $email = $template->createForRecipient(
        email: $recipient->getEmail(),
        vars: ['name' => $recipient->getName()]
    );
    $this->mailer->send($email);
}

7. Wann Prototype Pattern sinnvoll ist


Prototype Pattern sinnvoll wenn:

✓ Objekt-Erstellung ist teuer (DB-Queries, Template-Parsing, komplexe Berechnungen)
✓ Viele ähnliche Objekte mit leichten Unterschieden benötigt werden
✓ Klassen-Hierarchie zur Laufzeit nicht bekannt (dynamische Objekte)
✓ Objekte haben viele Konfigurationsparameter, von denen nur wenige variieren

Prototype Pattern NICHT sinnvoll wenn:

✗ Objekt-Erstellung ist billig (einfache Value Objects)
✗ Magento Factory::create() reicht (DI-Container macht es richtig)
✗ Objekte haben komplexe Objekt-Graphen (Deep Copy ist fehleranfällig)
✗ Unterschiede zwischen Instanzen sind zu groß

In Magento:
  Immer Factory::create() bevorzugen statt clone direkt
  clone nur wenn Factory nicht ausreicht (z.B. Rate-Variationen)
  Nie clone auf Shared Objects (Singletons) — gibt zwei "Singletons"

8. Prototype vs. Factory: Der Unterschied

KriteriumPrototype (clone)Factory (create)
BasisVorhandenes Objekt kopierenNeues Objekt via DI erstellen
ConstructorWird NICHT aufgerufenWird aufgerufen (DI)
AbhängigkeitenÜbernommen vom OriginalFrisch injiziert vom DI-Container
PerformanceSchneller (kein DI-Lookup)Langsamer (aber meist ausreichend)
In MagentoSelten, spezifische FälleStandard für Non-shared Objects

9. Prototype Pattern testen


<?php
class EmailTemplateTest extends \PHPUnit\Framework\TestCase
{
    public function testCloneIsIndependent(): void
    {
        $parser = $this->createMock(TemplateParser::class);
        $parser->method('parse')->willReturn('Hello { {name} }!');
        $parser->method('getHeaders')->willReturn(['From' => 'noreply@shop.de']);

        $template = new EmailTemplate($parser);
        $template->initialize('test_template');

        $email1 = $template->createForRecipient('alice@example.com', ['name' => 'Alice']);
        $email2 = $template->createForRecipient('bob@example.com', ['name' => 'Bob']);

        // Klone sind unabhängig voneinander
        // Änderung an $email1 beeinflusst $email2 nicht
        $this->assertNotSame($email1, $email2);
        $this->assertNotSame($email1, $template); // Original unverändert
    }

    public function testParserCalledOnceNotForEachClone(): void
    {
        $parser = $this->createMock(TemplateParser::class);
        // parse() soll nur EINMAL aufgerufen werden (für initialize())
        $parser->expects($this->once())->method('parse')->willReturn('Template');
        $parser->expects($this->once())->method('getHeaders')->willReturn([]);

        $template = new EmailTemplate($parser);
        $template->initialize('test_template');

        // Viele Klone — parse() wird nicht erneut aufgerufen
        for ($i = 0; $i < 100; $i++) {
            $template->createForRecipient("user{$i}@example.com");
        }
    }
}

Mironsoft

Magento 2 Performance & Architektur

Performance-Optimierung mit Design Patterns?

Wir analysieren Magento-Bottlenecks und implementieren performante Lösungen mit Prototype, Object Pool und anderen Patterns – messbar, testbar, wartbar.

Performance-Audit
Bottlenecks in Objekt-Erstellung, Template-Parsing und DB-Zugriffen identifizieren.
Pattern-Implementierung
Prototype, Object Pool, Factory – das richtige Pattern für den jeweiligen Anwendungsfall.
Benchmarking
Vor/nach Vergleich mit Blackfire.io – messbare Performance-Verbesserung dokumentieren.

10. Zusammenfassung

Das Prototype Pattern ist in Magento weniger dominant als Factory oder Strategy, aber in spezifischen Szenarien wertvoll: wenn Objekte teuer zu initialisieren sind und viele ähnliche Instanzen benötigt werden. PHPs clone-Operator macht Shallow Copies – für Deep Copy implementiere __clone(). In modernem Magento-Code ist Factory::create() fast immer vorzuziehen.

Prototype Pattern – Kernpunkte

PHP clone

Shallow Copy: Primitives kopiert, Objekt-Referenzen geteilt. Für Deep Copy: __clone() implementieren und verschachtelte Objekte ebenfalls klonen.

Magento Factory

Conceptuell ähnlich, aber mit vollständiger DI-Unterstützung. Factory::create() ist die bevorzugte Methode für neue Instanzen in Magento.

Einsatz

Sinnvoll wenn: Initialisierung teuer, viele ähnliche Instanzen nötig. Nicht sinnvoll: einfache Objekte, Factory reicht aus, komplexe Objekt-Graphen.

Vorsicht

Nie Shared Objects klonen (verletzt Singleton-Semantik). Immer prüfen ob Shallow Copy ausreicht oder __clone() für tiefe Kopie nötig ist.

11. FAQ: Prototype Pattern in Magento 2

1 Wann clone statt Factory::create()?
Factory::create() fast immer besser. clone direkt wenn: Factory nicht verfügbar, viele Variationen teurer Prototypen nötig, exakter Objekt-Zustand kopiert werden soll.
2 Shallow Copy vs. Deep Copy?
Shallow: Primitives kopiert, Objekt-Referenzen geteilt. Deep: alle Objekte ebenfalls geklont — via __clone() implementieren.
3 Magento-Models klonen?
Technisch möglich aber nicht empfohlen — inkonsistente Zustände möglich. Stattdessen: Factory::create() für neue Instanz, Daten dann manuell kopieren.
4 __clone() korrekt implementieren?
Wird nach clone-Operator aufgerufen. Alle Objekt-Properties ebenfalls klonen: $this->address = clone $this->address;. Arrays mit Objekten: foreach + clone je Element.
5 Wo nutzt Magento Core clone?
RateResult-Objekte im Shipping, DataObject-Variationen, Block-Variationen im Template-System. Indirekt: jede Factory::create() ist konzeptuell Prototype.
6 Shared Objects klonen?
Niemals! Klon wird nicht in DI-Container zurückregistriert → zwei Instanzen die eigentlich Singleton sein sollten → inkonsistenter Zustand.
7 DataObject für Prototype Pattern?
Ja — DataObject hält nur skalare Werte → Shallow Copy ausreichend. Basis-DataObject erstellen, klonen, spezifische Werte setzen. Typischer Einsatz im Shipping-System.
8 Prototype-Code testen?
Test-Schwerpunkte: Klon unabhängig vom Original (assertNotSame). Änderungen am Klon beeinflussen Original nicht. Teure Initialisierung nur einmal aufgerufen (expects()->once()).
9 Performance: clone vs. new?
clone meist schneller als new + DI-Lookup, aber Unterschied in Mikrosekunden. Echter Gewinn: wenn Constructor teure Ops enthält (DB, Template-Parse) — clone spart diese komplett.
10 Prototype mit Interface definieren?
Interface mit clone(): static Methode. Rückgabetyp static für Kovarianz in PHP 8+. Implementierung meist: return clone $this; mit optionaler __clone()-Logik.