Magento 2: A simple product feed

With Magento 2 Merchant Beta release date getting closer, I decided to dedicate some time to explore the current Magento 2 application that is on Github. Now that I had the chance to actually work with Magento 2 I have to say that it is actually a huge step forward from Magento 1 in my opinion.

In this article I will try to cover a few concepts by coding a task that probably most of you have done a few times – creating a really simple product feed. The goal is to set a cron job that will run every week and create the product feed file that has all the products that were created during the last week. Not a huge task but I think it does a good job of some explaining some new Magento 2 concepts.

This module’s code is also available on my Github account

Creating the module

Since there are no more code pools, we need to create a separate vendor for our module in the app/code folder.

navigation

Our module definition is in module.xml file now. We also need to declare the modules that our module depends on:

app/code/LDusan/Simple/etc/module.xml

1
2
3
4
5
6
7
8
9
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd">
    <module name="LDusan_Simple" setup_version="0.0.1">
        <sequence>
            <module name="Magento_Config"/>
            <module name="Magento_Catalog"/>
        </sequence>
    </module>
</config>

After that we should add our module name to the modules array in app/etc/config.php:

1
2
3
4
5
6
7
<?php
return array (
  'modules' =>
  array (
/*...*/
    'LDusan_Simple' => 1,
  ),

Cronjob

Let’s define our cronjob. In order for this to work we need to add the Magento 2 cron script to the crontab file, like this (same as in Magento 1):

1
*/5 * * * * /bin/sh /var/www/magento2/dev/shell/cron.sh

After that we need to create our module’s crontab.xml file:

app/code/LDusan/Simple/etc/crontab.xml

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../Cron/etc/crontab.xsd">
    <group id="default">
        <job name="ldusan_test_export" instance="LDusan\Simple\Model\Cron" method="export">
            <schedule>0 12 * * 0</schedule>
        </job>
    </group>
</config>

Cron class

We can now start building the actual Cron class that we will use.

app/code/LDusan/Simple/Model/Cron.php

1
2
3
4
5
6
7
8
<?php
namespace LDusan\Simple\Model;
class Cron
{
}

DEPENDENCY INJECTION

As it has already been mentioned in some articles, in Magento 2 dependencies can be passed through a constructor and they will be automatically resolved using type hints. For this module we need four objects:

  • Product repository to get the products
  • Search Criteria builder (we will need it for using the product repository)
  • Filter Builder
  • Csv class instance from ImportExport module

Our constructor should look like this:

app/code/LDusan/Simple/Model/Cron.php

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
<?php
namespace LDusan\Simple\Model;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Api\FilterBuilder;
use Magento\ImportExport\Model\Export\Adapter\Csv;
class Cron
{
    protected $productRepository;
    protected $searchCriteriaBuilder;
    protected $filterBuilder;
    protected $csv;
    public function __construct(
        ProductRepositoryInterface $productRepository,
        SearchCriteriaBuilder $searchCriteriaBuilder,
        FilterBuilder $filterBuilder,
        Csv $csv
    ) {
        $this->productRepository = $productRepository;
        $this->searchCriteriaBuilder = $searchCriteriaBuilder;
        $this->filterBuilder = $filterBuilder;
        $this->csv = $csv;
    }
/*...*/
}

Since we are using the default classes there is no need to define anything in the xml files yet. If we wanted to use some other product repository class that implements ProductRepositoryInterface we could have set that up in di.xml file.

We should always tend to use interfaces for type hinting rather than concrete classes as with interfaces we can easily switch the actual implementation. By using this principle we also make the elements of our system loosely coupled which is a good programming practice.

EXPORT METHOD

We will divide this process in two parts:

  • Retrieving the products from database
  • Writing data to file

app/code/LDusan/Simple/Model/Cron.php

1
2
3
4
5
public function export()
{
    $items = $this->getProducts();  
    $this->writeToFile($items);
}

PRODUCTS

Fetching multiple products from database is slightly different in Magento 2.

Entities are managed through repositories. Repositories are used for managing entity objects and all the common operations that can be done on them such as get, save, getList, delete are in these classes. These methods are defined in interfaces that repository classes implement. This practice will ensure that all the future versions are compatible with the rest of the application.

app/code/LDusan/Simple/Model/Cron.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function getProducts()
{
    $filters = [];
    $now = new \DateTime();
    $interval = new \DateInterval('P1W');
    $lastWeek = $now->sub($interval);
        
    $filters[] = $this->filterBuilder
        ->setField('created_at')
        ->setConditionType('gt')
        ->setValue($lastWeek->format('Y-m-d H:i:s'))
        ->create();
        
    $this->searchCriteriaBuilder->addFilter($filters);
        
    $searchCriteria = $this->searchCriteriaBuilder->create();
    $searchResults = $this->productRepository->getList($searchCriteria);
    return $searchResults->getItems();
}

CSV FILE

We can easily create our csv file with the ImportExport module’s Csv class:

app/code/LDusan/Simple/Model/Cron.php

1
2
3
4
5
6
7
8
9
10
protected function writeToFile($items)
{
    if (count($items) > 0) {
        $this->csv->setHeaderCols(['id', 'created_at', 'sku']);
            foreach ($items as $item) {
                $this->csv->writeRow(['id'=>$item->getId(), 'created_at' => $item->getCreatedAt(), 'sku' => $item->getSku()]);
            }   
        }
    }
}

PATH ARGUMENT

Here comes the part I like the most about the Magento 2. If you look at the Csv class you’ll see that it extends \Magento\ImportExport\Model\Export\Adapter\AbstractAdapter. Let’s take a look at that class’ constructor:

app/code/Magento/ImportExport/Model/Export/Adapter/AbstractAdapter.php

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
/* ... */
abstract class AbstractAdapter
{
    /**
     * Constructor
     *
     * @param \Magento\Framework\Filesystem $filesystem
     * @param string|null $destination
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function __construct(\Magento\Framework\Filesystem $filesystem, $destination = null)
    {
        $this->_directoryHandle = $filesystem->getDirectoryWrite(DirectoryList::SYS_TMP);
        if (!$destination) {
            $destination = uniqid('importexport_');
            $this->_directoryHandle->touch($destination);
        }
        if (!is_string($destination)) {
            throw new \Magento\Framework\Exception\LocalizedException(__('Destination file path must be a string'));
        }
        if (!$this->_directoryHandle->isWritable()) {
            throw new \Magento\Framework\Exception\LocalizedException(__('Destination directory is not writable'));
        }
        if ($this->_directoryHandle->isFile($destination) && !$this->_directoryHandle->isWritable($destination)) {
            throw new \Magento\Framework\Exception\LocalizedException(__('Destination file is not writable'));
        }
        $this->_destination = $destination;
        $this->_init();
    }
}
/*...*/

We see that it takes with 2 arguments, the second one is the output file destination. If we want to set a specific path we can easily do so with passing the destination argument using the di.xml file:

app/code/LDusan/Simple/etc/di.xml

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <type name="Magento\ImportExport\Model\Export\Adapter\Csv">
        <arguments>
            <argument name="destination" xsi:type="string">feeds/product_feed.csv</argument>
        </arguments>
    </type>
</config>

As a result of this, the feed will be saved in the file that we defined in the xml file. I think this is generally a good approach and that it provides a cleaner way of extending default functionalities.

There is still work to do

If we did everything correctly we should notice the feed in our system’s temporary folder each Monday morning.

Although the task is accomplished, our module is not finished yet. We need to create some tests that will make sure that everything works smoothly. We’ll leave that for the next article.

Source: Magento 2: A simple product feed | Dušan Lukić