credits: BiblioArchives / LibraryArchives Boys diving off a pier, Sarnia, Ontario / Garçons plongeant d’un quai à Sarnia, en Ontario via photopin (license). posted on December 9th, 2016.

A Guide to Object Pools in Magento 2

Let's dive in shall we?

Magento 2 is packed with a lot of cool design patterns. Most of them you might already know, like dependency injection or interceptors, because they are well-known and well- documented. There are however much more design patterns in Magento 2 that are also worth using. One of these patterns that I use regulary in my own modules are Object Pools.

What is an Object Pool?

You might not be aware, but chances are that you already used object pools more than once in Magento 2. Take the CLI for example. If you want to register a command for this, you add something like the following to di.xml:

<type name="Magento\Framework\Console\CommandList">
    <arguments>
        <argument name="commands" xsi:type="array">
            <item name="example_command"
                  xsi:type="object">Vendor\Module\Console\Example</item>
        </argument>
    </arguments>
</type>

There is nothing exotic about the above example. What it states is that: "If an instance of the CommandList is created, add the following entry to the $commands array". Let's take a look at the constructor of Magento\Framework\Console\CommandList:

/**
 * Constructor
 *
 * @param array $commands
 */
public function __construct(array $commands = [])
{
    $this->commands = $commands;
}

That's all of it. Actually, if you take a look at the entire class, you'll note that it's a very small class that does nothing more than store the $commands array.

What makes it powerfull is that the $commands array can be filled by external modules and due to dependency injection we can use these commands anywhere!

A more practical example

Let's say we want to create a module that can export all orders, and any other module is allowed to add a column to this export. This might look like a complex task at first, but it's actually very simple to set up by using an object pool.

The setup

First we setup our interface for a single column in our export. Since we are going to allow other extensions (possibly made by 3rd party developers) to add columns to our export, we must provide an interface so we know what to expect:

Api/Data/ColumnInterface.php

/**
 * Interface ColumnInterface
 */
interface ColumnInterface
{
    /**
     * @param int $orderId
     * @return string
     */
    public function getValue(int $orderId) : string;
}

Next up, we create an interface for our object pool, so we can use it properly with dependency injection:

Api/ColumnPoolInterface.php

/**
 * Interface ColumnPoolInterface
 */
interface ColumnPoolInterface
{
    /**
     * @return string[]
     */
    public function getColumnTypes() : array;

    /**
     * @return \Vendor\Module\Api\Data\ColumnInterface[]
     */
    public function getColumns() : array;

    /**
     * @param string $type
     * @return Data\ColumnInterface
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function getColumn(string $type) : \Vendor\Module\Api\Data\ColumnInterface;
}

And last but not least, we must add the dependency preferences to our di.xml, so we can use our interface in our constructors:

etc/di.xml

<preference for="Vendor\Module\Api\ColumnPoolInterface"
            type="Vendor\Module\Model\ColumnPool"></preference>

The code

Now that our setup is done, it's time to actually write some code. Since we've mapped our interface to a model in di.xml, let's there:

Model/ColumnPool.php

use Vendor\Module\Api\ColumnPoolInterface;
use Vendor\Module\Api\Data\ColumnInterface;
use Magento\Framework\Exception\LocalizedException;

/**
 * Class ColumnPool
 */
class ColumnPool implements ColumnPoolInterface
{
    /**
     * @var array
     */
    protected $columns;

    /**
     * ColumnPool constructor.
     * @param array $columns
     * @throws LocalizedException
     */
    public function __construct(
        array $columns = []
    )
    {
        foreach ($columns as $name => $column) {
            if (!$column instanceof ColumnInterface) {
                throw new LocalizedException(
                    __('Column %1 must be of type Vendor\Module\Api\Data\ColumnPoolInterface', $name));
            }
        }
        $this->columns = $columns;
    }

    /**
     * @return string[]
     */
    public function getColumnTypes() : array
    {
        return array_keys($this->columns);
    }

    /**
     * @return ColumnInterface[]
     */
    public function getColumns() : array
    {
        return $this->columns;
    }

    /**
     * @param string $type
     * @return ColumnInterface
     * @throws LocalizedException
     */
    public function getColumn(string $type) : ColumnInterface
    {
        if (array_key_exists($type, $this->columns)) {
            return $this->columns[$type];
        }
        throw new LocalizedException(__('Column of type %1 is not found.', $type));
    }
}

Now, let's break this class down. First look at the constructor:

/**
 * ColumnPool constructor.
 * @param array $columns
 * @throws LocalizedException
 */
public function __construct(
    array $columns = []
)
{
    foreach ($columns as $name => $column) {
        if (!$column instanceof ColumnInterface) {
            throw new LocalizedException(
                __('Column %1 must be of type Vendor\Module\Api\Data\ColumnPoolInterface', $name));
        }
    }
    $this->columns = $columns;
}

The only thing the constructor does, is iterate through all the columns to make sure that all entries are implementations of ColumnInterface.

The getColumnTypes()-method returns an array with all the used codes of the columns. The getColumns()-method simply returns the entire array.

And last but not least, we have a getColumn()-method that returns a column with a given name, and throws an exception if it's not found.

Responsibility

Please note that this class throws exceptions and not fails silently or returns false. This is intended behaviour. If you ask "why?" you should think about responsibility. Ask yourself the question: "Is it the responsibility of the object pool to handle errors?". The answer is: No. It's the responsibility of the object pool to throw the error, but not to deal with it.

Thinking about responsibility during development leads to cleaner, more SOLID code. But that's a whole different story...

The implementation

Now we have our setup and our code, we can put the object pool to use. Let's start with adding a column to the pool. Let's add the following to our di.xml-file:

<type name="Vendor\Module\Api\ColumnPoolInterface">
    <arguments>
        <argument name="columns" xsi:type="array">
            <item name="increment_id" xsi:type="object">Vendor\Module\Model\Column\IncrementId</item>
        </argument>
    </arguments>
</type>

With the above code we added our first column to the pool: IncrementId. So let's just look at what this class would look like:

Model/Column/IncrementId.php

use Magento\Sales\Api\OrderRepositoryInterface;
use Vendor\Module\Api\Data\ColumnInterface;

/**
 * Class IncrementId
 */
class AbstractColumn implements ColumnInterface
{
    /**
     * @var OrderRepositoryInterface
     */
    protected $orderRepository;

    /**
     * AbstractColumn constructor.
     * @param OrderRepositoryInterface $orderRepository
     */
    public function __construct(
        OrderRepositoryInterface $orderRepository
    )
    {
        $this->orderRepository = $orderRepository;
    }

    /**
     * @param int $orderId
     * @return string
     */
    public function getValue(int $orderId) : string
    {
        $order = $this->orderRepository->get($orderId);
        return '#' . $order->getIncrementId();
    }
}

This is our first column that we added to our column pool. It simply takes an integer $orderId and returns the increment ID that matches the order.

So how can we put this to practical use? Well, up to so far we've only created a pool and added a single column to it. Let's add a manager to handle our order exports. The first thing we need to do for this, is create an interface for our manager:

Api/OrderExportManagementInterface.php

/**
 * Interface OrderExportManagementInterface
 */
interface OrderExportManagementInterface
{
    /**
     * @return array
     */
    public function export() : array;
}

Not much special here. Just a single method. Let's add the dependency preference to our di.xml:

<preference for="Vendor\Module\Api\OrderExportManagementInterface"
            type="Vendor\Module\Model\OrderExportManagement"></preference>

And now for the interesting part: The manager itself:

use Vendor\Module\Api\ColumnPoolInterface;
use Vendor\Module\Api\OrderExportManagementInterface;
use Magento\Framework\Api\Search\SearchCriteriaBuilder;
use Magento\Sales\Api\OrderRepositoryInterface;

/**
 * Class OrderExportManagement
 * @package Happy\OrderExport\Model
 */
class OrderExportManagement implements OrderExportManagementInterface
{
    /**
     * @var ColumnPoolInterface
     */
    protected $columnPool;

    /**
     * @var OrderRepositoryInterface
     */
    protected $orderRepository;

    /**
     * @var SearchCriteriaBuilder
     */
    protected $searchCriteriaBuilder;

    /**
     * OrderExportManagement constructor.
     * @param ColumnPoolInterface $columnPool
     * @param OrderRepositoryInterface $orderRepository
     * @param SearchCriteriaBuilder $searchCriteriaBuilder
     */
    public function __construct(
        ColumnPoolInterface $columnPool,
        OrderRepositoryInterface $orderRepository,
        SearchCriteriaBuilder $searchCriteriaBuilder
    )
    {
        $this->columnPool = $columnPool;
        $this->orderRepository = $orderRepository;
        $this->searchCriteriaBuilder = $searchCriteriaBuilder;
    }

    /**
     * @return array
     */
    public function export() : array
    {
        $export = [];

        // Fetch all orders:
        $searchCriteria = $this->searchCriteriaBuilder
            ->addSortOrder('created_at', 'ASC')
            ->create();
        $orders = $this->orderRepository->getList($searchCriteria);

        // Run them through the pool of columns:
        foreach ($orders->getItems() as $order) {
            $data = [];
            foreach ($this->columnPool->getColumns() as $type => $column) {
                $value = $column->getValue($order->getEntityId());
                $data[$type] = $value;
            }
            $export[] = $data;
        }

        // Export it:
        return $export;
    }
}

Once again, let's break this class a bit down. The most important part in this class is in the constructor:

public function __construct(
    ColumnPoolInterface $columnPool,
    OrderRepositoryInterface $orderRepository,
    SearchCriteriaBuilder $searchCriteriaBuilder
)
{
    $this->columnPool = $columnPool;
    $this->orderRepository = $orderRepository;
    $this->searchCriteriaBuilder = $searchCriteriaBuilder;
}

Notice how we use the column pool in our constructor. This is where it all comes together. The ColumnPoolInterface will be mapped to the ColumnPool-model due to di.xml. The same di.xml that filled the $columns argument of the constructor of ColumnPool.

So what happens in the export()-method on our ColumnPoolManagement:

// Run them through the pool of columns:
foreach ($orders->getItems() as $order) {
    $data = [];
    foreach ($this->columnPool->getColumns() as $type => $column) {
        $value = $column->getValue($order->getEntityId());
        $data[$type] = $value;
    }
    $export[] = $data;
}

As you can see, we iterate through the orders, and for each order we iterate through all the columns provided by the column pool, effectively allowing the values of our export to be determined purely by configuration and external modules.

The beauty of it

The beauty of this method, by using the object pool-design pattern, is that we completely separated the various responsibilities of our order export, and made it modular, flexible and easy to extend as well. Just think about it:

  • The only responsibility of the pool is to keep track of what columns to export.
  • The only responsibility of the actual exporter is to load a bunch of orders and iterate through them.
  • The responsibility of fetching and validating data and such is not in this module, but in each individual column.
  • We're not even saving to CSV of XLS or whatever, since that's not the responsibility of the exporter.

Compare this to an export method that works like this:

/**
 * @return array
 */
public function exportOrders()
{
    $orders = $this->getOrders();
    $export = [];
    foreach ($orders as $order) {
        $export[] = [
            'increment_id' => $order->getIncrementId(),
            'invoice_id' => $this->getInvoiceId($order),
            'date' => explode(' ', (string) $order->getCreatedAt())[0],
            'customer_name' => implode(' ', array_filter([
                $order->getCustomerFirstname(), 
                $order->getCustomerMiddlename(), 
                $order->getCustomerLastname()]))
        ];
    }
    return $export;
}

The above example is very sensitive for errors and makes it very hard, or even almost impossible to add, remove or mutate specific columns in the order export. You're most likely have to create a complete rewrite if you want to make adjustments to the above.

In conclusion

I hope this article have shown you some insight in what object pools are in Magento 2, but most importantly: how you can utilize them to the fullest in your own extensions.

Magento 2 Undocumented Design Patterns Object Pools