Aller au contenu principal

Akeneo PIM et Dropbox : importer des ressources depuis Dropbox

Akeneo PIM est une puissante solution de gestion d’information produit (Product Information Management) aujourd’hui plébiscitée par nombre d’acteurs du e-commerce pour sa simplicité d’utilisation et les services rendus. Akeneo PIM permet de collecter, gérer, enrichir l’information produit, créer des catalogues, afin de les distribuer sur différents canaux de diffusion (web, print, etc…).

Logo Dropbox

 

La collecte des données est un point crucial dans la réussite des projets PIM. Elles peuvent avoir différentes sources dont des solutions de stockage et de partage de fichiers sur le cloud comme Dropbox. Dropbox est une solution très utilisée qui fournit des clients pour la plupart des systèmes d’exploitation (Windows, Linux, Mac OS, Android, iOS, etc…) permettant de synchroniser des ressources entre un système de fichier local et le cloud. Les fichiers déposés dans Dropbox sont également accessibles via un navigateur Web.

Il nous est donc arrivé qu’une entreprise utilisant Akeneo PIM doive importer des ressources (images, pdf, etc…) stockés sur Dropbox et les rattacher aux bons attributs des bons produits. Pour ce faire, nous avons mis en place une méthodologie spécifique.

1 – Convention de nommage et affectation

En premier lieu, nous établissons une convention de nommage des fichiers sur Dropbox, qui nous permettra d’identifier quel produit et quel attribut mettre à jour.

Les attributs susceptibles de se voir affecter des ressources sont de type “collection de ressources”. Supposons que nous ayons les 3 attributs suivants :

  • images_produit
  • documents_legaux
  • documents_commerciaux

Nous mettons en place les règles suivantes de nommage de fichiers sur Dropbox :

  • doivent être affectés à l’attribut “images_produit” les fichiers dont le nom correspond aux patterns suivants :
    • [SKU_produit]_base.jpg
    • [SKU_produit]_small.jpg
    • [SKU_produit]_thumbnail.jpg
  • doivent être affectés à l’attribut “documents_legaux” les fichiers dont le nom correspond au pattern suivant :
    • [SKU_produit]_LEG_[texte_libre].pdf
  • doivent être affectés à l’attribut “documents_commerciaux” les fichiers dont le nom correspond au pattern suivant :
    • [SKU_produit]_COM_[texte_libre].pdf

Cette convention de nommage des fichiers nous permettra d’affecter correctement les ressources aux bons produits et aux bon attributs.

S’ajoute à ceci la possibilité d’ignorer certains fichiers si leur nom est préfixé d’un _ (underscore).

Enfin, le téléchargement ne doit pas supprimer les fichiers de Dropbox, mais nous ne voulons pas non plus les télécharger s’ils l’ont déjà été.

2 – Mise en place d’un job d’import

Afin de mener à bien cette tâche, on peut mettre en place un job d’import constitué de 3 étapes :

  1. téléchargement des fichiers de Dropbox dans un dossier temporaire du système de fichiers local
  2. import de masse des fichiers fraîchement téléchargés dans le PAM de Akeneo PIM
  3. rattachement des ressources aux bons produits et aux bons attributs

La déclaration du job pourra ressembler à ceci :

parameters:
  acme.dropbox.connector: 'Dropbox Connector'
  acme.job.import_dropbox_assets.name: 'import_dropbox_assets'

services:
  acme.job.import_dropbox_assets:
    class: '%pim_connector.job.simple_job.class%'
    arguments:
      - '%acme.job.import_dropbox_assets.name%'
      - '@event_dispatcher'
      - '@akeneo_batch.job_repository'
      -
        - '@acme.step.download_dropbox_assets'
        - '@acme.step.mass_import_assets'
        - '@acme.step.process_dropbox_assets'
    tags:
      - { name: 'akeneo_batch.job', connector: '%acme.dropbox.connector%', type: 'import' }

3 – Préparation de Dropbox

Nous devons dans un premier temps préparer Dropbox à servir des fichiers à Akeneo PIM. Cela nécessite de créer une application sur Dropbox, à laquelle le PIM aura accès grâce à un jeton d’identification (token). Tout ceci se fait à partir de la console Dropbox à l’adresse : https://www.dropbox.com/developers/apps.

Un bouton permet de créer une nouvelle application, à laquelle on donnera des permissions de type “App folder”. Sur cette page, dans la section OAuth2, on trouvera un bouton permettant de générer un Access token. C’est ce token qui permettra au PIM de s’identifier auprès de Dropbox. Il faut donc le récupérer et le stocker précieusement car il ne sera ensuite plus visible.

Une fois l’application créée, un dossier portant le nom de l’application est créé sur Dropbox. C’est dans ce dossier qu’il faut copier ou déplacer tous les ressources que le PIM doit pouvoir télécharger.

4 – Téléchargement des ressources depuis Dropbox

La première étape consiste à récupérer les fichiers depuis Dropbox. La récupération se fait assez simplement grâce à l’excellente librairie PHP Flysystem (https://flysystem.thephpleague.com/docs/) qui propose une couche d’abstraction unifiée pour lire et écrire sur différents types de systèmes de fichiers. Flysystem est installé par défaut sur le PIM, mais pas l’adaptateur (https://fr.wikipedia.org/wiki/Adaptateur_(patron_de_conception)) Dropbox qu’il faut ajouter aux dépendances via la commande suivante :

$ composer require spatie/flysystem-dropbox

Une fois les dépendances installées, nous pouvons configurer un service pour communiquer avec Dropbox :

# src/AcmeBundle/Resources/config/dropbox.yml
services:
  flysystem.dropbox.client:
    class: Spatie\Dropbox\Client
    arguments:
      - '%dropbox.access_token%'

  flysystem.dropbox.adapter:
    class: Spatie\FlysystemDropbox\DropboxAdapter
    arguments:
      - '@flysystem.dropbox.client'

  flysystem.dropbox.filesystem:
    class: League\Flysystem\Filesystem
    arguments:
      - '@flysystem.dropbox.adapter'
      -
        case_sensitive: false

Ce service requiert qu’on lui fournisse le jeton d’accès généré lors de la configuration de l’application Dropbox. Nous pouvons l’ajouter aux paramètres du PIM :

# app/config/parameters.yml
parameters:
  # ajouter cette clé à la suite des paramètres déjà présents dans le fichier
  dropbox.access_token: fdskjnq89SDhdqksdjqksd78QSDbkjbqsdqsd3DQSD

Nous sommes désormais en mesure de parcourir l’arborescence du système de fichier Dropbox grâce au service flysystem.dropbox.filesystem.

Nous pouvons dès à présent ajouter le code de l’étape de téléchargement des ressources :

<?php


namespace AcmeBundle\Connector\Step;


use Akeneo\Tool\Component\Batch\Item\InvalidItemException;
use Akeneo\Tool\Component\Batch\Model\StepExecution;
use Akeneo\Tool\Component\Batch\Model\Warning;
use Akeneo\Tool\Component\Batch\Step\AbstractStep;

abstract class BasicStep extends AbstractStep
{
    /**
     * Handle step execution warning
     *
     * @param StepExecution        $stepExecution
     * @param mixed                $element
     * @param InvalidItemException $e
     */
    protected function handleStepExecutionWarning(
        StepExecution $stepExecution,
        $element,
        InvalidItemException $e
    ) {
        $warning = new Warning(
            $stepExecution,
            $e->getMessage(),
            $e->getMessageParameters(),
            $e->getItem()->getInvalidData()
        );

        $this->jobRepository->addWarning($warning);

        $this->dispatchInvalidItemEvent(
            get_class($element),
            $e->getMessage(),
            $e->getMessageParameters(),
            $e->getItem()
        );
    }
}
<?php


namespace AcmeBundle\Connector\Step;


use Akeneo\Asset\Component\Upload\UploadContext;
use Akeneo\Tool\Component\Batch\Item\DataInvalidItem;
use Akeneo\Tool\Component\Batch\Item\InvalidItemException;
use Akeneo\Tool\Component\Batch\Job\JobRepositoryInterface;
use Akeneo\Tool\Component\Batch\Model\StepExecution;
use AcmeBundle\Entity\DropboxAsset;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Doctrine\RegistryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class DownloadDropboxAssetsStep extends BasicStep
{
    /** @var string */
    private $tmpStorageDir;
    /** @var Filesystem */
    private $dropboxFs;
    /** @var LoggerInterface */
    private $logger;
    /** @var RegistryInterface */
    private $doctrine;

    /**
     * @param string $name
     * @param EventDispatcherInterface $eventDispatcher
     * @param JobRepositoryInterface $jobRepository
     * @param string $tmpStorageDir
     * @param Filesystem $dropboxFs
     * @param LoggerInterface $logger
     * @param RegistryInterface $doctrine
     */
    public function __construct(
        string $name,
        EventDispatcherInterface $eventDispatcher,
        JobRepositoryInterface $jobRepository,
        string $tmpStorageDir,
        Filesystem $dropboxFs,
        LoggerInterface $logger,
        RegistryInterface $doctrine
    ) {
        parent::__construct($name, $eventDispatcher, $jobRepository);

        $this->tmpStorageDir = $tmpStorageDir;
        $this->dropboxFs = $dropboxFs;
        $this->logger = $logger;
        $this->doctrine = $doctrine;
    }
    /**
     * @return string The name of this step
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @param StepExecution $stepExecution an entity representing the step to be executed
     * @throws \League\Flysystem\FileExistsException
     * @throws \League\Flysystem\FileNotFoundException
     */
    public function doExecute(StepExecution $stepExecution)
    {
        $user = $stepExecution->getJobParameters()->get('user');
        $uploadContext = new UploadContext($this->tmpStorageDir, $user);
        $dropboxBoxRepository = $this->doctrine->getRepository(DropboxAsset::class);
        $registeredDownloads = $dropboxBoxRepository->flatArray();
        $em = $this->doctrine->getManager();
        $count = 0;

        foreach ($this->dropboxFs->listContents('', true) as $file) {
            if (!$this->supports($file)) {
                continue;
            }
            $stepExecution->incrementSummaryInfo('dropbox_asset_read');

            try {
                $matches = $this->extractFromPattern($file, $stepExecution);
            } catch (InvalidItemException $e) {
                $this->handleStepExecutionWarning($stepExecution, $this, $e);
                continue;
            }

            $fileKey = sprintf('%s_%s_%s', $matches['identifier'], $matches['attribute'], $file['timestamp']);
            if (in_array($fileKey, $registeredDownloads)) {
                $stepExecution->incrementSummaryInfo('dropbox_asset_skipped_no_diff');
                continue;
            }

            $path = sprintf('%s/%s', $uploadContext->getTemporaryUploadDirectory(), $file['basename']);

            $fs = new Filesystem(new Local('/'));
            if (!$fs->putStream($path, $this->dropboxFs->readStream($file['path']))) {
                throw new \Exception(
                    'Something wrong happened while trying to download a file from Dropbox (%s)'
                );
                continue;
            }

            $dropboxAsset = (new DropboxAsset())
                ->setValue($fileKey)
                ->setAssetCode($file['filename'])
            ;
            $em->persist($dropboxAsset);
            $count++;
        }

        $em->flush();
        $stepExecution->addSummaryInfo('dropbox_asset_downloaded', $count);
    }

    /**
     * @param array $file
     * @param StepExecution $stepExecution
     * @return array|null
     * @throws InvalidItemException
     */
    private function extractFromPattern(array $file, StepExecution $stepExecution): ?array
    {
        foreach ($this->getSupportedPatterns() as $pattern) {
            if (preg_match($pattern, $file['basename'], $matches)) {
                return $matches;
            }
        }

        $stepExecution->incrementSummaryInfo('dropbox_asset_number_unsupported_files');
        throw new InvalidItemException(
            sprintf('Unsupported file (the filename "%s" doesn\'t match any supported pattern).', $file['path']),
            new DataInvalidItem($file)
        );
    }

    /**
     * @return array
     */
    private function getSupportedPatterns(): array
    {
        return [
            '/^(?P<identifier>[a-zA-Z0-9]+)_(?P<attribute>[a-zA-Z0-9]+)\.jpg$/',
            '/^(?P<identifier>[a-zA-Z0-9]+)_(?P<attribute>LEG|COM)_.+\.pdf$/',
        ];
    }

    /**
     * @param array $file
     * @return bool
     */
    private function supports(array $file): bool
    {
        if ($this->isDir($file)) {
            return false;
        }

        $parts = explode('/', $file['dirname']);
        foreach ($parts as $part) {
            if (preg_match('/^_.*/', $part)) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param array $file
     * @return bool
     */
    private function isDir(array $file): bool
    {
        return isset($file['type']) && $file['type'] === 'dir';
    }
}
services:

  # Other service definitions

  acme.step.download_dropbox_assets:
    class: 'AcmeBundle\Connector\Step\DownloadDropboxAssetsStep'
    arguments:
      - 'download_dropbox_assets_step'
      - '@event_dispatcher'
      - '@akeneo_batch.job_repository'
      - '%tmp_storage_dir%'
      - '@flysystem.dropbox.filesystem'
      - '@logger'
      - '@doctrine'

Pour qu’à l’étape suivante l’import de masse puisse fonctionner, les fichiers doivent être téléchargés dans un sous-dossier du répertoire temporaire définit par le paramètre tmp_storage_dir, dans le fichier app/config/pim_parameters.yml.

Ce sous-dossier porte par convention le nom d’un utilisateur. Ce peut être, dans notre cas, une chaîne de caractères arbitraire, que nous pouvons passer en paramètre du job (ci-dessous dans le paramètre user – pour les généralités sur la création et le paramétrage de jobs dans Akeneo, je vous renvoie vers la documentation officielle (https://docs.akeneo.com/latest/import_and_export_data/guides/create-connector.html).

$user = $stepExecution->getJobParameters()->get('user');
$uploadContext = new UploadContext($this->tmpStorageDir, $user);

Nous ne devons pas télécharger les ressources qui ont déjà été récupérées lors de précédentes exécutions du job. Pour garder une trace des fichiers téléchargés, nous créons donc une entité Doctrine destinée à stocker cette information en base de données :

AcmeBundle\Entity\DropboxAsset:
  type: entity
  table: acme_dropbox_asset
  repositoryClass: AcmeBundle\Repository\DropboxAssetRepository
  id:
    id:
      type: integer
      generator: { strategy: AUTO }
  fields:
    value:
      type: string
      nullable: false
    processed:
      type: boolean
      nullable: false
    assetCode:
      type: string
      nullable: false
<?php


namespace AcmeBundle\Entity;


class DropboxAsset
{
    /** @var int */
    private $id;
    /** @var string */
    private $value;
    /** @var string */
    private $assetCode;
    /** @var bool */
    private $processed;

    public function __construct()
    {
        $this->processed = false;
    }

    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getValue(): string
    {
        return $this->value;
    }

    /**
     * @param string $value
     * @return DropboxAsset
     */
    public function setValue(string $value): DropboxAsset
    {
        $this->value = $value;

        return $this;
    }

    /**
     * @return string
     */
    public function getAssetCode(): string
    {
        return $this->assetCode;
    }

    /**
     * @param string $assetCode
     * @return DropboxAsset
     */
    public function setAssetCode(string $assetCode): DropboxAsset
    {
        $this->assetCode = $assetCode;
      
        return $this;
    }

    /**
     * @return bool
     */
    public function isProcessed(): bool
    {
        return $this->processed;
    }

    /**
     * @param bool $processed
     * @return DropboxAsset
     */
    public function setProcessed(bool $processed): DropboxAsset
    {
        $this->processed = $processed;

        return $this;
    }
}
<?php


namespace AcmeBundle\Repository;


use Doctrine\ORM\EntityRepository;

class DropboxAssetRepository extends EntityRepository
{
    /**
     * Return a flat array of DropboxAsset values
     *
     * @return array
     */
    public function flatArray(): array
    {
        $qb = $this
            ->createQueryBuilder('q')
            ->select(['q.value'])
        ;

        return array_map(function($item) {
            return $item['value'];
        }, $qb->getQuery()->getArrayResult());
    }

    /**
     * @return mixed
     */
    public function findUnprocessedAssets()
    {
        $qb = $this
            ->createQueryBuilder('q')
            ->where('q.processed = :bool')
            ->setParameter('bool', false)
        ;

        return $qb->getQuery()->execute();
    }

}

Nous pouvons dès lors récupérer cette liste depuis le dépôt :

$dropboxBoxRepository = this->doctrine->getRepository(DropboxAsset::class);
$registeredDownloads = $dropboxBoxRepository->flatArray();

Nous demandons alors à Dropbox de nous renvoyer tout le contenu du dossier racine de l’application que nous avons configurée au début de ce tutoriel :

foreach ($this->dropboxFs->listContents('', true) as $file) {

listContents est une méthode de l’API Flysystem qui renvoie une collection d’éléments contenant les métadonnées des fichiers sur Dropbox (chemin, nom et type de fichier, et des métadonnées spécifiques à Dropbox). Le premier argument de la méthode indique le chemin à partir duquel nous souhaitons lister les fichiers (ici la chaîne de caractères vide signifie “à partir de la racine”) et le deuxième argument est un booléen permettant de lister récursivement ou non.

Nous vérifions ensuite si le fichier est bien pris en charge. S’il ne s’agit pas d’un dossier, et que son nom correspond à une expression régulière connue (cf. conventions de nommage présentées plus haut), le fichier peut être téléchargé dans le dossier temporaire :

$fs->putStream($path, $this->dropboxFs->readStream($file['path'])

Le fichier obtenu, nous pouvons stocker en base de données une nouvelle instance de l’entité DropboxAsset, à laquelle nous donnons une valeur unique nous permettant de facilement la retrouver ultérieurement. Cette valeur unique est construite à partir de l’identifiant et de l’attribut du produit auquel appartient la ressource et un timestamp. Ces informations nous seront utiles pour rattacher les ressources à leurs produits respectifs.

Une fois tous les fichiers téléchargés, nous pouvons passer à l’étape suivante : l’import de ces fichiers dans le gestionnaire de ressources du PIM.

5 – Import en masse des fichiers dans le gestionnaire de ressources du PIM

Le PIM Akeneo propose déjà nativement une commande capable d’importer tous les fichiers ressources, s’ils sont contenus dans un sous-dossier du stockage temporaire (cf. section précédente). Pour plus de détails sur la commande elle-même, je vous renvoie à la documentation officielle : https://docs.akeneo.com/latest/manipulate_pim_data/product_asset/mass_import.html.

Ce qui nous intéresse ici, c’est l’automatisation de l’exécution de cette commande une fois le téléchargement des ressources depuis Dropbox effectué. Le framework Symfony, sur lequel repose Akeneo, permet de le faire très simplement :

services:
  acme.step.mass_import_assets:
    class: 'AcmeBundle\Connector\Step\MassImportAssetsStep'
    arguments:
      - 'mass_import_assets'
      - '@event_dispatcher'
      - '@akeneo_batch.job_repository'
      - '@kernel'
<?php


namespace AcmeBundle\Connector\Step;


use Akeneo\Tool\Component\Batch\Job\JobRepositoryInterface;
use Akeneo\Tool\Component\Batch\Model\StepExecution;
use Akeneo\Tool\Component\Batch\Step\AbstractStep;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\KernelInterface;

class MassImportAssetsStep extends BasicStep
{
    private $kernel;

    public function __construct(
        $name,
        EventDispatcherInterface $eventDispatcher,
        JobRepositoryInterface $jobRepository,
        KernelInterface $kernel
    ) {
        parent::__construct($name, $eventDispatcher, $jobRepository);

        $this->kernel = $kernel;
    }

    /**
     * @param StepExecution $stepExecution the current step context
     *
     * @throws \Exception
     */
    protected function doExecute(StepExecution $stepExecution)
    {
        $user = $stepExecution->getJobParameters()->get('user');

        $application = new Application($this->kernel);
        $application->setAutoExit(false);

        $input = new ArrayInput([
            'command' => 'pim:product-asset:mass-upload',
            '--user' => $user
        ]);

        $output = new BufferedOutput();
        $application->run($input, $output);

        $stepExecution->addSummaryInfo('dropbox_asset_mass_import_result', $output->fetch());
    }
}

Je vous renvoie vers la documentation officielle de Symfony pour de plus amples détails : https://symfony.com/doc/3.4/console/command_in_controller.html.

6 – Rattachement des ressources aux bons produits et aux bons attributs

La dernière étape, une fois les fichiers téléchargés et importés dans le gestionnaire de ressources de Akeneo, consiste à les rattacher aux bons produits et aux bons attributs.

Le code suivant récupère de la base de données les informations sur les nouveaux ressources qui n’ont pas encore été traités. Ces informations nous permettent d’identifier le produit auquel relier la ressource et, via une table de correspondance, l’attribut qui doit être mis à jour.

services:
  acme.step.process_dropbox_assets:
    class: 'AcmeBundle\Connector\Step\ProcessDropboxAssetsStep'
    arguments:
      - 'process_dropbox_assets_step'
      - '@event_dispatcher'
      - '@akeneo_batch.job_repository'
      - '@doctrine'
      - '@pim_catalog.query.product_and_product_model_query_builder_factory'
      - '@pim_catalog.updater.property_adder'
      - '@pim_catalog.saver.product'
      - 50
<?php


namespace AcmeBundle\Connector\Step;


use Akeneo\Pim\Enrichment\Bundle\Elasticsearch\ProductAndProductModelQueryBuilderFactory;
use Akeneo\Pim\Enrichment\Component\Product\Query\Filter\Operators;
use Akeneo\Tool\Component\Batch\Item\DataInvalidItem;
use Akeneo\Tool\Component\Batch\Item\InvalidItemException;
use Akeneo\Tool\Component\Batch\Job\JobRepositoryInterface;
use Akeneo\Tool\Component\Batch\Model\StepExecution;
use Akeneo\Tool\Component\StorageUtils\Saver\BulkSaverInterface;
use Akeneo\Tool\Component\StorageUtils\Updater\PropertyAdderInterface;
use AcmeBundle\Entity\DropboxAsset;
use Symfony\Bridge\Doctrine\RegistryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class ProcessDropboxAssetsStep extends BasicStep
{
    const ATTRIBUTE_MAP = [
        'default' => 'images_produit',
        'LEG' => 'documents_legaux',
        'COM' => 'documents_commerciaux',
    ];


    /** @var RegistryInterface */
    private $doctrine;
    /** @var \Akeneo\Pim\Enrichment\Component\Product\Query\ProductQueryBuilderInterface */
    private $productQb;
    /** @var PropertyAdderInterface */
    private $propertyAdder;
    /** @var BulkSaverInterface */
    private $productSaver;
    /** @var int */
    private $batchSize;

    /**
     * @param $name
     * @param EventDispatcherInterface $eventDispatcher
     * @param JobRepositoryInterface $jobRepository
     * @param RegistryInterface $doctrine
     * @param ProductAndProductModelQueryBuilderFactory $productAndProductModelQueryBuilderFactory
     * @param PropertyAdderInterface $propertyAdder
     * @param BulkSaverInterface $productSaver
     * @param int $batchSize
     */
    public function __construct(
        $name,
        EventDispatcherInterface $eventDispatcher,
        JobRepositoryInterface $jobRepository,
        RegistryInterface $doctrine,
        ProductAndProductModelQueryBuilderFactory $productAndProductModelQueryBuilderFactory,
        PropertyAdderInterface $propertyAdder,
        BulkSaverInterface $productSaver,
        int $batchSize = 100
    )
    {
        parent::__construct($name, $eventDispatcher, $jobRepository);

        $this->doctrine = $doctrine;
        $this->productQb = $productAndProductModelQueryBuilderFactory->create();
        $this->propertyAdder = $propertyAdder;
        $this->productSaver = $productSaver;
        $this->batchSize = $batchSize;
    }

    /**
     * @param StepExecution $stepExecution the current step context
     *
     * @throws \Exception
     */
    protected function doExecute(StepExecution $stepExecution)
    {
        $rawAssets = $this->doctrine
            ->getRepository(DropboxAsset::class)
            ->findUnprocessedAssets()
        ;
        $stepExecution->addSummaryInfo('dropbox_asset_number_to_process', count($rawAssets));
        $stepExecution->addSummaryInfo('dropbox_asset_number_processed', 0);
        $mapping = $this->buildProductAssetMapping($rawAssets);

        $offset = 0;
        do {
            $map = array_slice($mapping, $offset, $this->batchSize);
            if (empty($map)) {
                break;
            }
            $this->processMap($map, $stepExecution);
            $offset += $this->batchSize;
        } while (count($map) === $this->batchSize);
    }

    /**
     * @param array $map
     * @param StepExecution $stepExecution
     */
    private function processMap(array $map, StepExecution $stepExecution)
    {
        $cursor = $this->productQb
            ->addFilter('identifier', Operators::IN_LIST, array_keys($map))
            ->execute()
        ;

        $productsToWrite = [];
        $assetsToWrite = [];
        foreach ($cursor as $product) {
            $assets = $map[$product->getIdentifier()];
            $pattern = '/^[a-zA-Z0-9]+_(?P<id>[a-zA-Z0-9]+)/';
            foreach ($assets as $asset) {
                preg_match($pattern, $asset->getValue(), $matches);
                $key = $matches['id'];
                $property = self::ATTRIBUTE_MAP['default'];
                if (in_array($key, array_keys(self::ATTRIBUTE_MAP))) {
                    $property = self::ATTRIBUTE_MAP[$key];
                }
                try {
                    $this->propertyAdder->addData(
                        $product,
                        $property,
                        [$asset->getAssetCode()],
                        ['locale' => null, 'scope' => null]
                    );
                    $assetsToWrite[] = $asset;
                    if (!in_array($product, $productsToWrite)) {
                        $productsToWrite[] = $product;
                    }
                } catch (\Exception $e) {
                    $this->handleStepExecutionWarning($stepExecution, $this, new InvalidItemException(
                        $e->getMessage(),
                        new DataInvalidItem([$asset->getAssetCode()])
                    ));
                }
            }
        }

        $this->productSaver->saveAll($productsToWrite);
        $stepExecution->incrementSummaryInfo('dropbox_asset_number_affected_products', count($productsToWrite));

        $this->updateProcessedAssets($assetsToWrite, $stepExecution);
    }

    /**
     * @param array $assets
     * @param StepExecution $stepExecution
     */
    private function updateProcessedAssets(array $assets, StepExecution $stepExecution)
    {
        foreach ($assets as $asset) {
            $asset->setProcessed(true);
        }
        $stepExecution->incrementSummaryInfo('dropbox_asset_number_processed', count($assets));
        $this->doctrine->getManager()->flush();
    }

    /**
     * @param DropboxAsset[] $assets
     * @return array
     * @throws InvalidItemException
     */
    private function buildProductAssetMapping(array $assets): array
    {
        $mapping = [];
        $pattern = "/^(?P<product_code>[a-zA-Z0-9]+)_[a-zA-Z0-9]+/";
        foreach ($assets as $asset) {
            if (!preg_match($pattern, $asset->getValue(), $matches)) {
                throw new InvalidItemException(
                    'Could not extract info from recorded DropboxAsset',
                    new DataInvalidItem($asset)
                );
            }
            $mapping[$matches['product_code']][] = $asset;
        }

        return $mapping;
    }
}

Il est donc relativement simple de récupérer des ressources depuis Dropbox. Cela peut se faire en trois étapes :

  1. télécharger les ressources localement,
  2. importer les ressources dans le PIM,
  3. rattacher les ressources aux produits.

Nous avons pris ici l’exemple de Dropbox, mais grâce à Flysystem, on pourrait tout aussi bien faire la même chose depuis AWS S3, Azure, SFTP, Google Cloud Storage, WebDAV et bien d’autres, sans avoir beaucoup de code à changer.

Si vous souhaitez être accompagné pour la mise en place de votre PIM, n’hésitez pas à demander conseil à nos experts certifiés Akeneo.

Martin Meunier - Business Developer Kaliop Digital Commerce