How to Import Products from a XML File

Foundations of connector creation have been covered in the previous chapter (cf How to Create a New Connector). With the following hands-on practice, we will create our own specific connector.

To stay focused on the main concepts, we will implement the simplest connector possible by avoiding to use too many existing elements.

Our use case is to import new products from the following XML file:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<products>
    <product sku="sku-1" name="my name 1" family="camcorders"/>
    <product sku="sku-2" name="my name 2"/>
    <product sku="sku-3" name="my name 3"/>
</products>

We assume that we’re using a standard edition with the icecat_demo_dev data set, sku and name already exist as real attributes of the PIM, family is also an existing property.

Note

The code inside this cookbook entry is available in the src directory, you can clone pim-docs (https://github.com/akeneo/pim-docs) and use a symlink to make the Acme bundle available in the src/.

Create the Connector

Create a new bundle:

1
2
3
4
5
6
7
8
9
<?php

namespace Acme\Bundle\XmlConnectorBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeXmlConnectorBundle extends Bundle
{
}

Register the bundle in AppKernel:

1
2
3
4
5
6
public function registerBundles()
{
    // ...
        new Acme\Bundle\XmlConnectorBundle\AcmeXmlConnectorBundle(),
    // ...
}

Configure the Job

Configure a job in Resources/config/batch_jobs.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
connector:
    name: XML Connector
    jobs:
        xml_product_import:
            title: acme_xml_connector.jobs.xml_product_import.title
            type:  import
            steps:
                import:
                   title: acme_xml_connector.jobs.xml_product_import.import.title
                   services:
                        reader:    acme_xml_connector.reader.file.xml_product
                        processor: pim_connector.processor.denormalization.product.flat
                        writer:    pim_connector.writer.doctrine.product

Here we create an import job which contains a single step: import.

The default step is Akeneo\Bundle\BatchBundle\Step\ItemStep.

An item step is configured with 3 elements, a reader, a processor and a writer.

Here, we’ll use a custom reader acme_xml_connector.reader.file.xml_product but we’ll continue to use default processor and writer.

Important

We strongly advise to always try to re-use most of the existing pieces, especially processor and writer, it ensures that all business rules and validation will be properly applied.

Create the Reader

As we don’t have an existing reader which allows to read this kind of file, we’ll write a new one.

The purpose of the reader is to return each item as an array, in the case of XML file, we can have more work to define what is the item.

 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php

namespace Acme\Bundle\XmlConnectorBundle\Reader\File;

use Akeneo\Bundle\BatchBundle\Item\AbstractConfigurableStepElement;
use Akeneo\Bundle\BatchBundle\Item\ItemReaderInterface;

class XmlProductReader extends AbstractConfigurableStepElement implements ItemReaderInterface
{
    protected $filePath;

    protected $xml;

    public function read()
    {
        if (null === $this->xml) {
            // for example purpose, we should use XML Parser to read line per line
            $this->xml = simplexml_load_file($this->filePath, 'SimpleXMLIterator');
            $this->xml->rewind();
        }

        if ($data = $this->xml->current()) {
            $item = [];
            foreach ($data->attributes() as $attributeName => $attributeValue) {
                $item[$attributeName] = (string) $attributeValue;
            }
            $this->xml->next();

            return $item;
        }

        return null;
    }

    public function getFilePath()
    {
        return $this->filePath;
    }

    public function setFilePath($filePath)
    {
        $this->filePath = $filePath;

        return $this;
    }

    public function getConfigurationFields()
    {
        return array(
            'filePath' => array(
                'options' => array(
                    'label' => 'acme_xml_connector.steps.import.filePath.label',
                    'help'  => 'acme_xml_connector.steps.import.filePath.help'
                )
            ),
        );
    }
}

Our reader reads the file and iterates to return products line by line.

This element must be configured with the path of the XML file (an example file is provided in XmlConnectorBundle\Resources\fixtures\products.xml).

Note

It is recommended to provide a label option (otherwise the attribute name will be used, which can lead to translation collision).

The help option allows you to display a hint next to the field in the job edition form.

Then, we need to define this reader as a service in readers.yml:

1
2
3
4
5
6
parameters:
    acme_xml_connector.reader.file.xml_product.class: Acme\Bundle\XmlConnectorBundle\Reader\File\XmlProductReader

services:
    acme_xml_connector.reader.file.xml_product:
        class: '%acme_xml_connector.reader.file.xml_product.class%'

And we introduce the following extension to load the services files in configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php

namespace Acme\Bundle\XmlConnectorBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\Config\FileLocator;

class AcmeXmlConnectorExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('readers.yml');
    }
}

Use the new Connector

Now if you refresh the cache, the new export can be found under Extract > Import profiles > Create import profile.

You can run the job from the UI or you can use following command:

php app/console akeneo:batch:job my_job_code