Refactoring Magento 2 Model Code to the Repository Design Pattern

Refactoring Magento 2 Model Code to the Repository Design Pattern

A step-by-step guide to refactoring Magento 2 code to adhere to the recommended design patterns, with a focus on implementing the repository pattern to improve overall maintainability.

When developing in Magento 2, following best practices and coding standards is crucial to make sure your code is maintainable and easy to understand. I’d like to explore a Magento 2 code design issue by focusing on a simple model, and discuss its refactoring to align with the recommended design pattern.

Looking at the Code Issue

Let's start by examining the initial code snippet for our Contact model, which contains a design issue related to the loadByEmail method:

app/code/Vendor/Module/Model/Contact.php
<?php
 
declare(strict_types=1);
 
namespace Vendor\Module\Model;
 
use Magento\Framework\Model\AbstractModel;
 
class Contact extends AbstractModel
{
    protected function _construct()
    {
        $this->_init('Vendor\Module\Model\ResourceModel\Contact');
    }
 
    public function loadByEmail($email)
    {
        $this->_getResource()->load($this, $email, 'email');
        return $this;
    }
}

This code is intended to simply return a specific contact record by an email address. While it looks simple, there is a major design flaw in this code.

To gain a deeper understanding of the design issue that is going on here, we must first grasp the fundamentals of the Magento 2 design principles, and also identify which general design pattern is being violated.

In this code, the loadByEmail method is located directly in the model. This approach may have easy-to-understand benefits; however, it violates several critical design principles in Magento 2.

Lets break down some of the most significant violations.

Violating the Single Responsibility Principle

The Single Responsibility Principle is one of the five object-oriented programming design principles known as SOLID. It states that a class should have a single responsibility and should not perform multiple tasks.

In our initial code, the Contact model takes on multiple responsibilities. This includes both loading data and performing email-based lookups. By having multiple responsibilities, this model becomes more challenging to maintain, modify, and extend. Implementing a more suitable design pattern can solve this issue, allowing each class to focus on a single responsibility.

Violating Magento 2's Service Contract Design Pattern

Service Contracts are a crucial design pattern in Magento 2. They represent a set of public interfaces (in the form of PHP interfaces) for all of a module's functionalities. Using Service Contracts allows a module to encapsulate all of the related business logic, and ensures that class implementations can be easily replaced or re-used without affecting other parts of the system.

In our initial code, the loadByEmail method is a part of the model, which violates the Service Contract design pattern.

Violating the Separation of Concerns

Separation of Concerns is a fundamental principle in software engineering that promotes developing modular components, each having a distinct responsibility. It significantly contributes to making code more maintainable and flexible by isolating specific concerns and reducing the coupling between components.

The loadByEmail method in the Contact model combines data access logic with the responsibility of representing the Contact entity. Resolving this with a more ideal design pattern would make the code more modular and maintainable.

Violating the Don't Repeat Yourself (DRY) Principle

The Don't Repeat Yourself (DRY) principle is another essential software design guideline which aims to avoid duplicating code by abstracting it into a single, central location.

While not immediately apparent in the example provided, having the loadByEmail method directly in the model sets a bad precedent. It risks having more methods with similar data access logic in the model. This approach could lead to duplicated and scattered logic throughout the module, making future code maintenance and refactoring more challenging.

The Repository Design Pattern: Solving the Design Violations and Embracing the Benefits

Overall, the design problem stems from having the loadByEmail method directly in the model. We can refactor this code to follow both the Service Contract and Repository patterns, which enhances its compatibility with Magento 2 coding standards, as well as fixing the underlying design foo-pah.

Let’s first explore the rationale behind selecting the Repository pattern over other available options, such as using Collection classes, and explain how it solves the initial code violations and unlocks significant advantages within the context of Magento 2.

Why Choose the Repository Pattern over Collection Classes?

Collection classes offer a simple implementation fix to this issue, however the Repository design pattern is favored over using Collection classes for several reasons:

  1. Abstraction: Repositories act as an abstraction layer between the data access logic and the rest of the application. This abstraction allows for easier modification or replacement of the data storage logic without affecting other parts of the system.
  2. Improved Testability: With a repository, you can leverage dependency injection to replace actual data access with mock objects during testing. This approach enables more efficient unit testing and promotes test-driven development.
  3. Consistency and Readability: Implementing the Repository pattern enforces consistency in data access logic throughout your module, leading to improved readability, which is particularly useful when collaborating with other developers.
  4. Easier API Implementation: When developing APIs for your module, having a repository in place simplifies the process by providing a unified access point for reading and modifying data. This setup ensures that the API's business logic remains consistent with the module's internal logic.

Choosing the Repository pattern over Collection classes aligns better with Magento 2 best practices and provides a more consistent, maintainable, and testable solution.

How the Repository Pattern Solves the Design Violations

The implementation of the Repository pattern addresses the design violations present in the initial code, offering several key benefits:

  1. Single Responsibility Principle: By moving the loadByEmail method to a separate repository class, we ensure that the Contact model now focuses solely on representing the contact entity, adhering to the Single Responsibility Principle.
  2. Service Contract Design Pattern: With the Repository pattern in place, we respect Magento 2's Service Contract design pattern by providing a defined interface (the ContactRepositoryInterface), which represents our module's functionality. This interface promotes better encapsulation and makes it easier to replace or reuse the implementation without affecting other components.
  3. Separation of Concerns: The Repository pattern enforces the Separation of Concerns principle by dividing responsibilities between the model and the repository, making our code more modular and maintainable.
  4. Don't Repeat Yourself (DRY) Principle: As the Repository pattern centralizes data access logic, it guards against the risk of duplicated code, ensuring that any changes to data access only occur in one place, improving maintainability.

Benefits of Using Repositories in Magento 2

Beyond addressing design violations, employing the Repository pattern in a Magento 2 context offers some specific advantages:

  1. Easier Caching and Performance Optimization: With a central location for managing data access, implementing caching and performance optimizations becomes more straightforward, as the changes only affect the repository.
  2. Simplified Extensibility: Adhering to the Repository pattern and Service Contracts allows developers to more easily integrate the code with other extensions or customizations. The repository interface can be replaced by a preference, plugin or proxy without disrupting the rest of the module's functionality.
  3. Future-proofing: As Magento 2 continuously evolves, adhering to its recommended design patterns ensures that your code is better prepared to accommodate future updates and upgrades, reducing the chance of compatibility issues.

So to reiterate, implementing the Repository pattern in Magento 2 not only solves the design violations present in the initial code, but provides a myriad of additional advantages that promote consistency, maintainability, and scalability. By embracing the Repository pattern, module developers can create clean, organized, and efficient code aligned with the best practices set by Magento 2.

Refactoring Code to the Repository Pattern

Now that we have identified the design issue in the initial code, it's time to refactor and realign it with Magento 2 design patterns by implementing the Repository pattern. This approach not only resolves the design violations, but also yields significant advantages in maintainability, flexibility, and scalability.

There following are a few simple steps which updates our initial code into a more robust and well-architected solution.

Step 1: Create a Contact Repository Interface

The first step is to create an interface for the Contact repository. This interface defines the methods needed for working with the Contact entity in our custom module:

app/code/Vendor/Module/Api/ContactRepositoryInterface.php
<?php
 
declare(strict_types=1);
 
namespace Vendor\Module\Api;
 
use Vendor\Module\Api\Data\ContactInterface;
 
interface ContactRepositoryInterface
{
    public function getById($contactId);
 
    public function save(ContactInterface $contact);
 
    public function delete(ContactInterface $contact);
 
    public function loadByEmail($email);
}

A Contact Repository Interface defines the public methods for working with the Contact entity, establishing a clear contract of what the module's functionality will be. This interface is crucial for enforcing consistency, readability, and testability.

Step 2: Implement the Interface with a Concrete Repository Class

Now, we need to implement the interface by creating a concrete repository class. This class will handle the actual business logic for working with our Contact entity:

app/code/Vendor/Module/Model/ContactRepository.php
<?php
 
declare(strict_types=1);
 
namespace Vendor\Module\Model;
 
use Vendor\Module\Api\ContactRepositoryInterface;
use Vendor\Module\Api\Data\ContactInterface;
use Vendor\Module\Model\ResourceModel\Contact as ContactResource;
 
class ContactRepository implements ContactRepositoryInterface
{
    public function __construct(
        protected ContactResource $contactResource,
        protected ContactFactory $contactFactory
    ) {}
 
    public function getById($contactId)
    {
        $contact = $this->contactFactory->create();
        $this->contactResource->load($contact, $contactId);
        return $contact;
    }
 
    public function save(ContactInterface $contact)
    {
        $this->contactResource->save($contact);
        return $contact;
    }
 
    public function delete(ContactInterface $contact)
    {
        $this->contactResource->delete($contact);
    }
 
    public function loadByEmail($email)
    {
        $contact = $this->contactFactory->create();
        $this->contactResource->load($contact, $email, 'email');
        return $contact;
    }
}

The Concrete Repository Class is the actual implementation of the Contact Repository Interface. It contains the specific business logic for managing the Contact entity, ensuring consistent data access, and providing an abstraction layer between the data storage and the rest of the system.

Step 3: Remove the loadByEmail Method from the Contact Model

The final step in our refactoring process is to remove the loadByEmail method from the Contact model. As our goal is to use the repository for working with entities, we no longer need this method inside the model:

app/code/Vendor/Module/Model/Contact.php
<?php
 
declare(strict_types=1);
 
namespace Vendor\Module\Model;
 
use Magento\Framework\Model\AbstractModel;
 
class Contact extends AbstractModel
{
    protected function _construct()
    {
        $this->_init('Vendor\Module\Model\ResourceModel\Contact');
    }
}

By removing the loadByEmail method from the Contact model, we are simplifying the model's responsibility to just representing the Contact entity. This change adheres to the Single Responsibility Principle and Separation of Concerns, resulting in more maintainable and better-structured code.

Now that we've completed our refactoring, we can use the Vendor\Module\Api\ContactRepositoryInterface to work with the Contact entity in our module, making our code more compatible with Magento 2 recommended design patterns.

Advantages and Disadvantages of the Previous Method and the Refactor

It's essential to analyze both the advantages and disadvantages of our initial code design and the refactored solution to understand the implications of the changes we've made.

Let’s dive into the strengths and weaknesses of each approach, demonstrating the rationale behind our decision to refactor the code, and how it benefits the overall design and functionality of the module.

Previous Method

Advantages

  • Simplicity: The initial method of directly using the model was simpler and quicker to implement, making it easier to understand for developers new to Magento 2.
  • Less code: The initial method required less code for the implementation, which may have been less intimidating for less experienced developers.

Disadvantages

  • Maintainability: The previous method violates the recommended design patterns in Magento 2, potentially leading to maintainability issues.
  • Scalability: Directly working with models is less scalable than using the Service Contract and Repository pattern, limiting future adaptability.
  • Potential for code duplication: Having multiple methods directly in the model may result in duplicated logic, which could have been better abstracted in a centralized repository class.

The Refactor

Advantages

  • Maintainability: By following Magento 2 recommended design patterns, the refactored code becomes more maintainable, easier to understand, and compatible with the ecosystem.
  • Scalability: The implementation of the Repository pattern provides a systematic and structured approach to manipulating entities in Magento 2, greatly enhancing scalability.
  • Readability: The separation of responsibilities between the model and the repository improves the readability of the code, promoting a better understanding of the module's functionality.
  • Future-proofing: Staying aligned with Magento 2 coding standards ensures a smoother upgrade process when new versions of Magento 2 are released.

Disadvantages

  • Complexity: The overall refactor requires more code and additional classes, which might seem overwhelming initially.
  • Learning curve: Developers new to Magento 2 may need more time to grasp the logic of the Repository pattern before becoming comfortable with its implementation and usage.

Recognizing the Problem & Finding a Solution

Before we wrap up, let’s discuss how to be more proactive in recognizing improperly architected Magento 2 code, and provide guidance on selecting the most suitable design pattern for various scenarios.

Noticing Improperly Architected Code

To effectively identify potential issues in your code, keep these points in mind:

  1. Pay attention to design principle violations: Keep a watchful eye on code violating widely accepted design principles such as SOLID, DRY, KISS, and YAGNI. A breach of these principles often signals potential issues that need addressing.
  2. Adhere to Magento 2 best practices: Familiarize yourself with the recommended design patterns and best practices specific to Magento 2. If you need familiarly with these concepts, the Magento 2 Coding Kickstart course covers Service Contracts, Repositories, and Factories in great detail. Ensuring alignment with these guidelines helps maintain compliance and increase overall code quality.
  3. Regularly Review and Refactor Code: Periodically review and refactor your code to identify any inconsistencies, deviations or redundancies. Code reviews involving different team members can provide fresh perspectives and insights into potential issues.
  4. Take note of complex or hard-to-understand code: If a particular piece of code seems overly complex or difficult to understand, it may be an indication that a more suitable design pattern or refactoring is needed.

Selecting the Right Design Pattern

When faced with a decision on which design pattern to choose or when in doubt about your code's architecture, consider the following steps:

  1. Understand the problem at hand: Before selecting a design pattern, it's crucial to grasp the exact nature of the problem you're trying to solve or the requirements of the particular feature you're implementing.
  2. Research design patterns: Familiarize yourself with common design patterns such as Creational, Structural, and Behavioral, as well as Magento-specific patterns like Repositories, Factories, and Proxies (we also have a full premium course on Proxy Classes!). Understanding when and how these patterns are best utilized will enable you to make informed decisions about the most suitable solutions for your code.
  3. Discuss with colleagues or consult the community: Reach out to fellow developers, join forums or read articles and blog posts relating to your specific problem. If you are a University student, you will also get access to Campus, a community of Magento developers which you can consult. This approach can provide valuable insights and help you decide on the most appropriate design pattern to be used.
  4. Evaluate the trade-offs: Consider the advantages and disadvantages of using a specific design pattern. Take into account factors like complexity, maintainability, scalability, and testability, rather than blindly trusting a specific design pattern.
  5. Iterate and refine: Remember, choosing a design pattern isn't set in stone. Be open to revising your decision and adapting your code based on feedback and new insights to continually improve your code quality.

By proactively identifying poorly architected code and systematically evaluating the available design patterns, you can mitigate potential issues and develop high-quality Magento 2 modules and customizations that adhere to best practices.

Refactoring Magento 2 code to align with coding standards and best practices is essential for developers looking to create scalable, maintainable, and easy-to-understand solutions. By following the step-by-step process process above, you can efficiently refactor your code, implementing the Repository pattern and ensuring your code is compatible with Magento 2 design patterns.

Embracing these best practices will ultimately lead to increased code quality and a better overall experience for both developers and end-users.

Do you have an itch to learn more about Magento? Consider these 3 valuable resources I have for you:

  1. Explore Magento 2 design patterns & best practices course (1,000+ students)
  2. Grow your Magento expertise with all courses & lessons (700+ students)
  3. Learn visually with blocks of code & inline comments (3,000+ students)