PHP programming community has evolved a lot since Magento’s first release in 2008. I think that Magento 2 came just in time to catch up with the ongoing programming trends.
Magento 1 way
One of the common Magento 1 principles that was used in the core, as well as in most of the extensions was to put some common methods in an abstract class and then override a few of them in child classes.
This approach has obviously served well for some time, however in order to achieve better separation of concerns we need to embrace the new philosophy of Magento 2.
Magento 2 approach
As you may have noticed the entire Magento 2 architecture is strongly influenced by service contracts.
Service contracts
We could say that service contracts represent certain sets of interfaces that aim to provide reliable and consistent public API for the classes that implement them. With this approach we can easily utilize some of the well known object oriented programming principles:
Composition over inheritance
Is a well known design principle. I think this Wikipedia article describes it very well:
Classes should achieve polymorphic behavior and code reuse by their composition (by containing instances of other classes that implement the desired functionality) rather than inheritance from a base or parent class.
In other words, passing small objects to our classes (through constructor in M2 case) is often preferred over creating a chain of hierarchy that most of the times doesn’t represent our problem domain well.
Note: if we would instantiate the objects inside our class instead of passing them that would also be considered composition. In Magento 2 we want to utilize the dependency injection by passing objects.
Relationship type difference
The core issue that this principle resolves is in the type of relationship that these approaches impose.
Inheritance is an “is-a” relationship.
Composition represents “has-a” relationship.
By using inheritance we are saying that our child classes are subtypes of parents class, where with composition our class consists of small objects that have their own behavior
Code example
class Product extends \Magento\Catalog\Model\AbstractModel implements IdentityInterface, SaleableInterface, ProductInterface { /*..*/ }
As you can see, in this Magento 2 piece of code the product class now implements a few interfaces, one of which is salable interface. I think this is a great start, as we are no longer tied to the concrete implementation.
We can separate our stock logic in a separate class and use it in our product with composition. That way we are not tight coupling our stock logic. It could look like this:
class Product extends \Magento\Catalog\Model\AbstractModel implements IdentityInterface, SaleableInterface, ProductInterface { public function __construct( \Magento\CatalogInventory\Api\StockManagementInterface $stockManagement, /***/ ) { $this->stockManagement = $stockManagement; /***/ } /***/ }
More work to do
These things are great, however I feel that the codebase can still be improved.
It’s definitely not very easy to effectively refactor a project of this scale without losing backwards compatibility.
Data integrity
Let’s take a look at the official dev docs service contracts page:
A service contract includes data interfaces, which preserve data integrity.
In an extensible piece of software such as Magento it is important to define boundaries between objects and how are they meant to be used.
Let’s get back to our product example:
class Product extends \Magento\Catalog\Model\AbstractModel implements IdentityInterface, SaleableInterface, ProductInterface { /*..*/ }
So at the moment the Product class still:
extends \Magento\Catalog\Model\AbstractModel
that extends abstract class AbstractExtensibleModel
that extends AbstractModel
And we ended up with a non semantic hierarchy chain that we wanted to avoid.
One of the main problems is that those parent classes have a lot of public methods such as:
public function setData($key, $value = null)
This particular method is especially problematic to me as it actually means that we can set anything that we want on our objects. If all our classes in Magento 2 extend from the class that contains this method I believe that there won’t be much of data integrity left.
Enforcing design principles by code
I believe that programmatically enforcing correct programming principles is of high importance in Magento and in open source software in general.
Step by step solution
This case is the ideal candidate for applying composition over inheritance principle mentioned above. We need to go through the refactoring process step by step.
Remove one layer
If you look at the \Magento\Catalog\Model\AbstractModel it’s really just a bunch of attribute related methods. It’s more or less just the implementation for the custom attributes so it doesn’t need be so tightly coupled with our Product class:
abstract class AbstractModel extends \Magento\Framework\Model\AbstractExtensibleModel { /*..*/ /** * Retrieve locked attributes * * @return array */ public function getLockedAttributes() { return array_keys($this->_lockedAttributes); } /** * Checks that model have locked attributes * * @return boolean */ public function hasLockedAttributes() { return !empty($this->_lockedAttributes); } /** * Retrieve locked attributes * * @param mixed $attributeCode * @return boolean */ public function isLockedAttribute($attributeCode) { return isset($this->_lockedAttributes[$attributeCode]); } /*..*/ }
To me it seems that custom attributes are more of “feature” that product has rather than something that’s necessarily built into products. With this implementation we are stuck with tight coupling our product with custom attributes implementation.
We can move the entire attributes implementation into our constructor:
class Product implements IdentityInterface, SaleableInterface { /*..*/ public function __construct( ProductAttributeManagementInterface $productAttributeManagementInterface, /*..*/ ) { $this->productAttributeManagementInterface = $productAttributeManagementInterface } }
class ProductAttributeManagement implements ProductAttributeManagementInterface { /*..*/ /** * Retrieve locked attributes * * @return array */ public function getLockedAttributes() { return array_keys($this->_lockedAttributes); } /** * Checks that model have locked attributes * * @return boolean */ public function hasLockedAttributes() { return !empty($this->_lockedAttributes); } /**/ }
interface ProductAttributeManagementInterface { public function getCustomAttributes() public function getCustomAttribute($attributeCode) }
I think it shouldn’t be too hard to do this and we can apply the same process to the entire codebase.
Community
I see that there are some initiatives that have a goal of involving more people into helping out with refactoring. I liked this one as I think that it brings real value to the product and that it’s easy enough for everyone to participate. If we would have a refactoring roadmap or join efforts to improve one module per month that would be great.
We are all in this together after all so improving Magento should be important for all of us.
Would you like to recommend this article to your friends or colleagues?
Feel free to share.