Factories — Object Creation Without ObjectManager
Factories provide a clean way to create new object instances without directly depending on ObjectManager.
Why factories matter:
- ObjectManager is discouraged for direct use
- Factories encapsulate the creation pattern
- Allow passing runtime data during creation
- Follow Magento coding standards
How factories work:
// Without factory (bad practice)
use Magento\Framework\ObjectManager\ObjectManager;
class BadExample
{
public function __construct(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
}
public function createProduct($sku)
{
return $this->objectManager->create(Product::class, ['sku' => $sku]);
}
}
// With factory (correct)
class GoodExample
{
public function __construct(
private \Magento\Catalog\Model\ProductFactory $productFactory
) {}
public function createProduct($sku): Product
{
return $this->productFactory->create(['sku' => $sku]);
}
}
Generated factory:
namespace Magento\Catalog\Model\ResourceModel\Product;
class ProductFactory
{
private $objectManager;
public function __construct(\Magento\Framework\ObjectManager\ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
}
public function create(array $data = [])
{
return $this->objectManager->create('Magento\Catalog\Model\Product', $data);
}
}
Factory naming convention:
- Append
Factoryto the class name:Product→ProductFactory - Place in the same namespace as the original class
- Generated automatically when you type-hint the class
Proxies — Lazy Loading Dependencies
Proxies defer loading of heavy dependencies until they're actually needed, reducing memory usage and startup time.
Problem: Some classes have many dependencies, but not all are used in every request.
// This loads ALL dependencies immediately, even if save() is rarely called
class OrderProcessor
{
public function __construct(
private ProductRepository $productRepo,
private CustomerRepository $customerRepo,
private PaymentProcessor $paymentProcessor,
private InventoryService $inventoryService,
private TaxCalculator $taxCalculator,
private EmailService $emailService
) {}
}
Solution — Use a proxy:
// Only the proxy is instantiated; the real object loads on first use
class OrderProcessor
{
public function __construct(
private ProductRepository $productRepo,
private Customer\CustomerRepository\Proxy $customerRepoProxy
) {}
public function process($orderId)
{
// customerRepoProxy only loads CustomerRepository when this is called
$customer = $this->customerRepoProxy->getById($customerId);
}
}
Configuring proxies in di.xml:
<type name="Vendor\Module\Service\OrderProcessor">
<arguments>
<argument name="customerRepoProxy" xsi:type="object">
Magento\Customer\Api\CustomerRepositoryInterface\Proxy
</argument>
</arguments>
</type>
Proxy naming convention:
- Append
\Proxyto the full class path - Generated in the same namespace
- Proxies extend the original class
Generated proxy:
namespace Magento\Customer\Api\CustomerRepositoryInterface;
class Proxy extends \Magento\Customer\Model\ResourceModel\Customer\Repository
{
private $objectManager;
private $realInstance;
public function getById($customerId)
{
if ($this->realInstance === null) {
$this->realInstance = $this->objectManager->get(
\Magento\Customer\Model\ResourceModel\Customer\Repository::class
);
}
return $this->realInstance->getById($customerId);
}
}
Interceptors — Plugin Support
Interceptors (also called plugins) are generated wrapper classes that enable before/after/around method modification.
How interceptors work:
When a class has plugins declared in di.xml, Magento generates an interceptor (Proxy) class that wraps the original method calls.
Plugin declaration:
<type name="Magento\Catalog\Model\ProductRepository">
<plugin name="vendor_product_plugin" type="Vendor\Module\Plugin\ProductPlugin" sortOrder="10"/>
</type>
Generated interceptor structure:
namespace Magento\Catalog\Model\ResourceModel\Product;
class ProductRepository\Interceptor extends ProductRepository
{
private $pluginManager;
public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $options = [])
{
$pluginManager = $this->pluginManager;
// Before plugins
$args = [$product, $options];
$result = $pluginManager->callPlugin('beforeSave', $args);
// Around plugins (if any)
if ($result !== false) {
$result = parent::save(...$args);
}
// After plugins
$result = $pluginManager->callPlugin('afterSave', [$result]);
return $result;
}
}
Plugin execution order:
around(10) → before(10) → before(20) → Method → after(20) → after(10)
Plugin types:
beforeMethod— Executes before the original method, can modify argumentsafterMethod— Executes after, can modify the return valuearoundMethod— Wraps the method, can skip execution entirely
Generated Code Management
Managing generated code effectively is crucial for development and deployment.
Key generated directories:
generated/
├── code/
│ └── Vendor/Module/ # Your module's generated classes
├── di/
│ └── etc/ # Compiled DI configuration
└── metadata/ # Derived database schema
Cleaning generated code:
# Clean everything
rm -rf generated/code/*
rm -rf generated/di/*
rm -rf generated/metadata/*
# Or use Magento command
php bin/magento setup:di:compile --cleanup-generated
When generated code is stale:
- After changing di.xml without recompiling
- After modifying constructor signatures
- After adding/removing plugins
- After changing preferences
Debugging generated code:
# Check if a class is generated
grep -r "class ProductRepository" generated/code/
# Compare generated vs expected
php bin/magento setup:di:compile --dry-run
# Verify plugin registration
cat generated/di/etc/module.xml
Generated code in version control:
- Never commit generated/ to git
- Add to .gitignore
- Regenerate during deployment
- Clean before compilation
Common issues:
# Stale generated code causing errors
rm -rf generated/*
php bin/magento cache:flush
php bin/magento setup:di:compile
# Plugin not working
# 1. Check plugin is declared in di.xml
# 2. Recompile: setup:di:compile
# 3. Clear generated code if needed
Quiz
1. What naming convention do generated factories follow?
2. What is the primary benefit of using proxies?
3. Which generated class enables before/after/around plugin functionality?
4. Should generated/ be committed to version control?
Flashcards
Question
What does a factory do?
Click to reveal answer
Answer
Creates new object instances without direct ObjectManager usage
Question
When is a proxy's real object actually loaded?
Click to reveal answer
Answer
On the first method call to the proxy
Question
What is the naming convention for interceptors?
Click to reveal answer
Answer
ClassName\Interceptor (extends original class)
Question
Where are compiled DI files stored?
Click to reveal answer
Answer
generated/di/etc/
Question
How do you clean generated code?
Click to reveal answer
Answer
rm -rf generated/* or setup:di:compile --cleanup-generated
Revision Notes
Key Takeaways
- 1. Factories create objects without using ObjectManager directly
- 2. Proxies defer loading heavy dependencies until first use
- 3. Interceptors enable plugin (before/after/around) functionality
- 4. All generated code lives in the generated/ directory
- 5. Generated code must be regenerated after any DI changes
- 6. Never commit generated/ to version control
Interview Tips
- • Explain why factories are preferred over direct ObjectManager usage
- • Describe a use case for proxies with a heavy dependency graph
- • Explain how interceptors enable the plugin system
- • Discuss generated code management in CI/CD pipelines
Cheat Sheet
Generated Code Cheat Sheet
Factories:
- Naming:
ClassNameFactory - Purpose: Create objects without ObjectManager
- Usage: Type-hint the factory, call
create()
Proxies:
- Naming:
ClassName\Proxy - Purpose: Lazy-load expensive dependencies
- Usage: Type-hint the proxy class
Interceptors:
- Naming:
ClassName\Interceptor - Purpose: Enable plugin before/after/around
- Auto-generated when plugins are declared
Directory: generated/
Never commit to git