This article is part of a series on design patterns for framework independent code, especially to write extensions for Magento 1 and Magento 2 alike.
All Parts:
- Introduction: Write Extensions For Magento 1 And Magento 2
- Part 1: Accessing Configuration Data
- Part 2: Using Dependency Injection
- Part 3: Building Bridges
- Part 4: Preparing Data For Output
- Part 5: Using Advanced Autoloading
- Part 6: Leveraging PSR Interfaces
- Part 7: Iterating Iterators
So we want to decouple business logic (our library) from the framework, making the Magento module depend on the library, not the other way around.
But somehow we still need to be able to interact with existing Magento models.
The Bridge design pattern (a simplified variation of it, to be exact) addresses this problem:
Intent: Decouple an abstraction from its implementation so that the two can vary independently
Just like our Configuration Value Objects, this is again an implementation of the Dependency Inversion principle.
-
High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
Here, „Details“ are the concrete classes in the library as well as in the Magento module. And as an abstraction we create a set of interfaces, the Implementors. These tell the module, what exactly we need from Magento (or, generally speaking, from the application) and constitute a well-defined interface between the two.
Remember that the end goal is a structure like this:
So we cannot use any class of the modules directly from the library. The modules have to implement interfaces instead, which were defined in the library, and pass these implementations to the library, wherever it expects the interfaces.
The UML diagram below shows the participants of the bridge pattern in its original sense. What’s important for us, is that the library provides interfaces (“Implementor”), which are implemented by the module (“ConcreteImplementor”). I am going to call these concrete implementations Bridge Classes. The client code in the library only depends on the interfaces, never on the concrete implementations. The “RefinedAbstraction” allows us to have different variations on the client as well because the bridge pattern is meant to solve parallel inheritance hierarchies.
In my examples I don’t use the additonal abstraction within the library (“Abstraction” and “RefinedAbstraction”), but use the interfaces directly. Why? Because it solves a problem that I don’t have: being able to have different implementations on the client side (the library). I will not introduce another layer of abstraction if I don’t need it.
Example
Let’s say we need to be able to get name and URL for one or several products by SKU. Then these are our implementor interfaces:
Implementor Interfaces
1 2 3 4 5 6 7 8 9 10 11 12 13 |
interface Product { public function getSku(); public function getName(); public function getUrl(); } interface ProductRepository { /** * @return Product[] */ public function findBySkus(array $skus); } |
Our library then requires a ProductRepository, but leaves the implementation up to the Magento module. For unit tests we will create a stub implementation or mock the interface.
The implementation then can act as an adapter for the Magento models:
Magento 1 Bridge Classes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
final class IntegerNet_Example_Model_Bridge_Product implements Product { protected $_product; public function __construct(Mage_Catalog_Model_Product $product) { $this->_product = $product; } public function getSku() { return $this->_product->getSku(); } ´public function getName() { return $this->_product->getName(); } public function getUrl() { return $this->_product->getUrl(); } } final class IntegerNet_Example_Model_Bridge_ProductRepository implements ProductRepository { public function findBySkus(array $skus) { $result = []; $collection = Mage::getResourceModel('catalog/product_collection') ->addAttributeToSelect('name') ->addUrlRewrite() ->addAttributeToFilter('sku', array('in' => $skus)); foreach ($collection as $product) { $result[] = Mage::getModel('integernet_example/bridge_product', $product); } return $result; } } |
I used Mage::getModel('integernet_example/bridge_product', $product)
here, which follows the common best practice in Magento module development, not to use „new“, but unfortunately getModel() is limited to one constructor parameter.
So I ended up intentionally breaking the rule for bridge classes. To keep the flexibility of the Magento rewrite system and follow the same principles as in the library, I introduce a factory. This can be a single helper (which can be rewritten using the Magento rewrite system):
1 2 3 4 5 6 7 8 9 10 11 |
class IntegerNet_Example_Helper_Factory { public function createProductRepository() { return new IntegerNet_Example_Model_Bridge_ProductRepository(); } public function createProduct(Mage_Catalog_Model_Product $product) { return new IntegerNet_Example_Model_Bridge_Product($product); } } |
Now the findBySku method in the ProductRepository implementation above looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
public function findBySkus(array $skus) { $result = []; $collection = Mage::getResourceModel('catalog/product_collection') ->addAttributeToSelect('name') ->addUrlRewrite() ->addAttributeToFilter('sku', array('in' => $skus)); foreach ($collection as $product) { $result[] = Mage::helper('integernet_example/factory')->createProduct($product); } return $result; } |
Library Code
A class in the library that uses these interfaces could look like this (just a CSV export of selected products, using a writer such as League\Csv):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
interface ProductExport { public function exportProducts(array $skus); } final class ProductUrlExport { private $productRepository; private $writer; public function __construct(ProductRepository $productRepository, Writer $writer) { $this->productRepository = $productRepository; $this->writer = $writer; } public function exportProducts(array $skus) { $products = $this->productRepository->findBySkus($skus); foreach ($products as $product) { $this->writer->insertOne([$product->getName(), $product->getUrl()]); } } } |
It receives a ProductRepository
instance, eventually created by the Magento module. As promised in the last part, Dependency Injection, I’ll give an example how to connect the parts as well:
To create our super complex product URL export class, we’ll have a ProductExport factory:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class ProductExportFactory { private $productRepository; public function __construct(ProductRepository $productRepository, Writer $writer) { $this->productRepository = $productRepository; } public function createUrlCsvExport($filename) { return new ProductUrlExport($this->productRepository, \League\Csv\Writer::createFromPath($filename)); } } |
This also instantiates the internal dependencies (here: the CSV writer), which we don’t need to expose to the Magento module to keep the interface between library and module small and keep as much program logic as possible in the library.
Using the Library in the Module
This is how you could use the code from above in an admin controller action:
1 2 3 4 5 6 7 8 |
public function exportCsvAction() { $repository = Mage::helper('integernet_example/factory')->getProductRepository(); $factory = new ProductExportFactory($repository); $filename = Mage::getBaseDir('var') . DS . 'export' . DS . 'urls-' . time() . '.csv'; $export = $factory->createUrlCsvExport($filename); $export->exportProducts($this->getRequest()->getParam('skus')); } |
I would move the instantiation of the ProductExportFactory to our existing factory helper, to be able to reuse or rewrite it:
1 2 3 4 5 6 7 8 9 10 11 |
class IntegerNet_Example_Helper_Factory { ... public function createProductExportFactory() { $repository = $this->createProductRepository(); return new ProductExportFactory($repository); } } |
… which leaves us with a controller action like this:
1 2 3 4 5 6 7 |
public function exportCsvAction() { $factory = Mage::helper('integernet_example/factory')->createProductExportFactory(); $filename = Mage::getBaseDir('var') . DS . 'export' . DS . 'urls-' . time() . '.csv'; $export = $factory->createUrlCsvExport($filename); $export->exportProducts($this->getRequest()->getParam('skus')); } |
Depending on your actual use case, you also might want to create the product exporter immediately from the helper to avoid factories that create factories (I know, it’s a common joke about Java and overengineered class design).
Pattern Definition
As stated above, this is not following the Bridge pattern definition to the point. You could also say, it’s a variation of the Adapter pattern, but where the “Adaptor” implements an interface and is exchangable:
- Bridge class => Adaptor
- Magento models => Adaptee
Defining Interfaces
The interfaces should only contain the methods we really need. If an interface still ends up with lots of methods, consider splitting it into multiple interfaces, because it’s unlikely that they are used all at once in a class. The concrete implementation can still be one class, implementing multiple interfaces, like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
interface LinkableResource { public function getUrl(); public function getLinkText(); public function getLinkTitle(); } interface HasMediaGallery { public function getImages(); } interface HasCategories { public function getCategories(); public function isInCategory(Category $category); } final class IntegerNet_Example_Model_Bridge_Product implements LinkableResource, HasMediaGallery, HasCategories { ... } |
Interface Segregation Principle – Clients should not be forced to depend upon interfaces that they don’t use.
This makes it not only easier to mock the dependencies for unit tests, but also we are more flexible when it comes to different implementations (i.e. Magento 1 vs. Magento 2 vs. OroCommerce …)
But what about the presentation layer? In the next part, we will discuss, which parts of the layout you can decouple, and introduce view models.

Author: Fabian Schmengler
Fabian Schmengler is Magento developer and trainer at integer_net. His focus lies in backend development, conceptual design and test automation.
Fabian was repeatedly selected as a Magento Master in 2017 and 2018 based on his engagements, active participation on StackExchange and contributions to the Magento 2 core.
Trackbacks/Pingbacks