Prototype Pattern: Extending Abstract Factory
· Reading time: approx. 11 minutes · Category: Magento 2 · Design Patterns
Prototype Pattern:
Extending Abstract Factory
When creating new objects is expensive, the Prototype Pattern copies an existing prototype instead. How Magento uses clone() and Factory::create(), and when you should apply it yourself.
Copying instead of creating anew
The Prototype Pattern (GoF, 1994) solves a specific problem: sometimes creating a new object is expensive, involving heavy initialization, complex dependencies, or database queries in the constructor. In such cases it is more efficient to clone an already existing object and adjust the clone, rather than starting from scratch every time.
In PHP, the primary tool for this is clone $object. Magento uses the Prototype Pattern in several places, sometimes explicitly with clone, more often implicitly through its factory infrastructure.
- 1. The GoF Prototype Pattern
- 2. PHP clone: Shallow vs. Deep Copy
- 3. Magento Factory: Prototype under the hood
- 4. Cloning DataObject: Magento's data carrier
- 5. Shipping rate cloning: a concrete example
- 6. Implementing your own Prototype Pattern
- 7. When the Prototype Pattern makes sense
- 8. Prototype vs. Factory: the difference
- 9. Testing the Prototype Pattern
- 10. Summary
- 11. FAQ
1. The GoF Prototype Pattern
The Prototype Pattern states: create new objects by copying (cloning) a prototype object instead of directly instantiating a class:
<?php
// Classic Prototype Pattern:
interface Prototype
{
public function clone(): static;
}
class ExpensiveObject implements Prototype
{
private array $expensiveData;
public function __construct()
{
// Expensive constructor: loads from DB, initializes complex structures
$this->expensiveData = $this->loadFromDatabase();
}
private function loadFromDatabase(): array
{
sleep(1); // Simulates an expensive DB call
return ['key' => 'value', /* ... 1000 entries ... */];
}
public function clone(): static
{
// PHP clone copies the object without running the constructor
return clone $this;
}
}
// Without Prototype: every new ExpensiveObject() = 1 second wait
$obj1 = new ExpensiveObject(); // 1 second
$obj2 = new ExpensiveObject(); // 1 second
$obj3 = new ExpensiveObject(); // 1 second
// With Prototype: only the first call is expensive
$prototype = new ExpensiveObject(); // 1 second
$obj1 = $prototype->clone(); // Instant (no constructor)
$obj2 = $prototype->clone(); // Instant
$obj3 = $prototype->clone(); // Instant
2. PHP clone: Shallow vs. Deep Copy
PHP's clone performs a shallow copy by default: primitive values and strings are copied, but object references remain the same:
<?php
class Address
{
public function __construct(
public string $street,
public string $city
) {}
}
class Customer
{
public function __construct(
public string $name,
public Address $address // Object reference!
) {}
}
$original = new Customer('Max Sample', new Address('Main Street 1', 'Berlin'));
$clone = clone $original;
// Primitives are copied:
$clone->name = 'Maria Sample';
echo $original->name; // 'Max Sample' ✓ (unchanged)
// Object references are NOT copied:
$clone->address->city = 'Munich';
echo $original->address->city; // 'Munich' ✗ (changed!)
// Shallow copy: $original and $clone share the same Address object
// Solution: __clone() method for a deep copy
class Customer
{
public function __construct(
public string $name,
public Address $address
) {}
public function __clone(): void
{
// Deep copy: clone the Address too
$this->address = clone $this->address;
}
}
$clone2 = clone $original;
$clone2->address->city = 'Hamburg';
echo $original->address->city; // 'Berlin' ✓ (now unchanged)
3. Magento Factory: Prototype under the hood
Magento's generated factory classes are actually a form of the Prototype Pattern. When creating via Factory::create(), the DI container internally uses ObjectManager::create(), which is semantically equivalent to clone $prototype, but with DI container support:
<?php
// Generated 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);
}
}
// Usage: always use the factory instead of new or clone directly
class PostRepository
{
public function __construct(
private readonly PostFactory $postFactory
) {}
public function getById(int $id): Post
{
$post = $this->postFactory->create(); // "Clone" of the prototype via DI
$this->postResource->load($post, $id);
return $post;
}
}
4. Cloning DataObject: Magento's data carrier
<?php
// Magento DataObject is a universal key-value container
// Frequently cloned for "base data plus variations"
use Magento\Framework\DataObject;
// Prototype with base data:
$baseRateData = new DataObject([
'carrier' => 'flatrate',
'carrier_title' => 'Flat Rate',
'method_title' => 'Fixed',
'currency' => 'EUR',
]);
// Variation A: standard delivery
$standard = clone $baseRateData;
$standard->setData('price', 4.99);
$standard->setData('cost', 3.50);
$standard->setData('method', 'standard');
// Variation B: express delivery (without reloading $baseRateData)
$express = clone $baseRateData;
$express->setData('price', 14.99);
$express->setData('cost', 9.00);
$express->setData('method', 'express');
// DataObject has no __clone(), a shallow copy is sufficient here,
// since DataObject only holds scalar values in the array.
5. Shipping rate cloning: a concrete Magento example
In the shipping cost system, Magento clones rate objects for different shipping methods of the same carrier:
<?php
// In Magento\Shipping\Model\Carrier\Flatrate::collectRates() (simplified):
namespace Magento\OfflineShipping\Model\Carrier;
class Flatrate extends AbstractCarrier
{
public function collectRates(RateRequest $request): ?Result
{
$result = $this->_rateResultFactory->create();
// Method object for "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);
// For multi-method carriers: clone base method, modify price:
// $expressMethod = clone $method;
// $expressMethod->setMethod('express');
// $expressMethod->setPrice(14.99);
// $result->append($expressMethod);
return $result;
}
}
6. Implementing your own Prototype Pattern
<?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); // Expensive!
$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; // No template re-parse needed!
$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 and baseHeaders are arrays/strings, no deep copy needed
// If there were object properties: clone them here
}
}
// Usage: initialize once, clone N times
$template = new EmailTemplate($parser);
$template->initialize('newsletter_welcome'); // Expensive once
foreach ($recipients as $recipient) {
$email = $template->createForRecipient(
email: $recipient->getEmail(),
vars: ['name' => $recipient->getName()]
);
$this->mailer->send($email);
}
7. When the Prototype Pattern makes sense
Prototype Pattern makes sense when:
✓ Object creation is expensive (DB queries, template parsing, complex calculations)
✓ Many similar objects with slight differences are needed
✓ The class hierarchy is not known at runtime (dynamic objects)
✓ Objects have many configuration parameters, only a few of which vary
Prototype Pattern does NOT make sense when:
✗ Object creation is cheap (simple value objects)
✗ Magento Factory::create() is sufficient (the DI container handles it correctly)
✗ Objects have complex object graphs (deep copy is error-prone)
✗ The differences between instances are too large
In Magento:
Always prefer Factory::create() over clone directly
Use clone only when the factory is not enough (e.g. rate variations)
Never clone shared objects (singletons), it creates two "singletons"
8. Prototype vs. Factory: the difference
| Criterion | Prototype (clone) | Factory (create) |
|---|---|---|
| Basis | Copies an existing object | Creates a new object via DI |
| Constructor | NOT called | Called (DI) |
| Dependencies | Inherited from the original | Freshly injected by the DI container |
| Performance | Faster (no DI lookup) | Slower (but usually sufficient) |
| In Magento | Rare, specific cases | Standard for non-shared objects |
9. Testing the Prototype Pattern
<?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']);
// Clones are independent of each other
// Changing $email1 does not affect $email2
$this->assertNotSame($email1, $email2);
$this->assertNotSame($email1, $template); // Original unchanged
}
public function testParserCalledOnceNotForEachClone(): void
{
$parser = $this->createMock(TemplateParser::class);
// parse() should only be called ONCE (for initialize())
$parser->expects($this->once())->method('parse')->willReturn('Template');
$parser->expects($this->once())->method('getHeaders')->willReturn([]);
$template = new EmailTemplate($parser);
$template->initialize('test_template');
// Many clones, but parse() is not called again
for ($i = 0; $i < 100; $i++) {
$template->createForRecipient("user{$i}@example.com");
}
}
}
Mironsoft
Magento 2 Performance & Architecture
Performance optimization with design patterns?
We analyze Magento bottlenecks and implement performant solutions with Prototype, Object Pool, and other patterns, measurable, testable, and maintainable.
10. Summary
The Prototype Pattern is less dominant in Magento than Factory or Strategy, but valuable in specific scenarios: when objects are expensive to initialize and many similar instances are needed. PHP's clone operator performs shallow copies; for a deep copy, implement __clone(). In modern Magento code, Factory::create() is almost always preferable.
Prototype Pattern: key points
PHP clone
Shallow copy: primitives copied, object references shared. For a deep copy: implement __clone() and clone nested objects as well.
Magento Factory
Conceptually similar, but with full DI support. Factory::create() is the preferred method for new instances in Magento.
When to use
Useful when: initialization is expensive, many similar instances are needed. Not useful: simple objects, factory is sufficient, complex object graphs.
Caution
Never clone shared objects (violates singleton semantics). Always check whether a shallow copy is enough or whether __clone() is needed for a deep copy.
11. FAQ: Prototype Pattern in Magento 2
1 When clone instead of Factory::create()?
2 Shallow copy vs. deep copy?
__clone().3 Cloning Magento models?
4 Implementing __clone() correctly?
$this->address = clone $this->address;. Arrays with objects: foreach plus clone per element.5 Where does Magento core use clone?
6 Cloning shared objects?
7 DataObject for the Prototype Pattern?
8 Testing prototype code?
9 Performance: clone vs. new?
10 Defining Prototype with an interface?
clone(): static method. Return type static for covariance in PHP 8+. Implementation usually: return clone $this; with optional __clone() logic.