How to Register a New Mass Edit Action on Products

The Akeneo PIM comes with a number of mass edit actions. It also comes with a flexible way to define your own mass edit actions on selected products.

Prerequisite

The mass edit action uses the BatchBundle in order to run mass edit in the background. Readers and Writers are already created so in this cookbook we will focus on how to create a Mass Edit Action and create a Processor. For more information on how to create Jobs, Readers, Processors, or Writers please see How to Customize Import / Export.

Work with a custom Acme bundle

Your custom Mass Edit actions have to be in a custom Acme bundle which inherits from our EnrichBundle. Once your bundle is created, we must inform Symfony it inherits our Bundle:

# /src/Acme/Bundle/EnrichBundle/AcmeEnrichBundle.php
<?php

namespace Acme\Bundle\EnrichBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeEnrichBundle extends Bundle
{
    public function getParent()
    {
        return 'PimEnrichBundle';
    }
}

Phase 1: Create the Operation

Tip

Operations are designed to build and transport the configuration (eventually via a form) that will be sent to the background job. No item is updated from here!

The first step is to create a new class in the Operation folder that extends AbstractMassEditOperation and declare this new class as a service in the mass_actions.yml file.

The method getBatchJobCode() is very important as it determines which job process to use. In our example we will use the capitalize_values job.

The getItemsName() method is used for the UI, in order to properly show with which items the user is working. In our example, we are working on ‘product’ type.

 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
59
60
// /src/Acme/Bundle/EnrichBundle/MassEditAction/Operation/CapitalizeValues.php
<?php

namespace Acme\Bundle\EnrichBundle\MassEditAction\Operation;

use Pim\Bundle\EnrichBundle\MassEditAction\Operation\AbstractMassEditOperation;

class CapitalizeValues extends AbstractMassEditOperation
{
    /**
     * {@inheritdoc}
     */
    public function getOperationAlias()
    {
        return 'capitalize-values';
    }

    /**
     * {@inheritdoc}
     */
    public function getFormType()
    {
        return 'acme_enrich_operation_capitalize_values';
    }

    /**
     * {@inheritdoc}
     */
    public function getFormOptions()
    {
        return [];
    }

    /**
     * {@inheritdoc}
     */
    public function getItemsName()
    {
        return 'product';
    }

    /**
     * {@inheritdoc}
     */
    public function getActions()
    {
        return [
            'field'   => 'name',
            'options' => ['locale' => null, 'scope' => null]
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function getBatchJobCode()
    {
        return 'capitalize_values';
    }
}
2 things will be sent to the Job:
  • actions : the raw configuration actions, you define what you want here. It will be available within your Job. That’s what getActions() is used for. Here actions are hard-coded, but it could be generated by another method or something else.
  • filters : the selection filter to tell the job which items it will work on. It’s used by the Reader.

Once the Operation is created, you must register it as a service in the DIC with the pim_catalog.mass_edit_action tag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# /src/Acme/Bundle/EnrichBundle/Resources/config/mass_actions.yml
services:
    acme_enrich.mass_edit_action.capitalize_values:
        public: false
        class: Acme\Bundle\EnrichBundle\MassEditAction\Operation\CapitalizeValues
        tags:
            -
                name: pim_enrich.mass_edit_action
                alias: capitalize-values
                acl: acme_enrich_product_capitalize_values
                datagrid: product-grid
                form_type: acme_enrich.form.type.capitalize_values

Note

The alias will be used in the URL (/enrich/mass-edit-action/capitalize-values/configure)

Phase 2: Create the FormType

Tip

As the Operation is used to build configuration, we could need a FormType that will be shown during the configuration step in the UI. Use it to show whatever is useful to you: Select, Input...

For this cookbook, we do not need the user to configure this action, so we’ll use an empty FormType:

 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
// /src/Acme/Bundle/EnrichBundle/Form/Type/MassEditAction/CapitalizeValuesType.php
<?php

namespace Acme\Bundle\EnrichBundle\Form\Type\MassEditAction;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class CapitalizeValuesType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // Build your form here
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(
            array(
                'data_class' => 'Acme\\Bundle\\EnrichBundle\\MassEditAction\\Operation\\CapitalizeValues'
            )
        );
    }

    public function getName()
    {
        return 'acme_enrich_operation_capitalize_values';
    }
}

Don’t forget to register it as a service in the DI:

# /src/Acme/Bundle/EnrichBundle/Resources/config/form_types.yml
services:
    acme_enrich.form.type.capitalize_values:
        class: Acme\Bundle\EnrichBundle\Form\Type\MassEditAction\CapitalizeValuesType
        arguments:
            - Acme\Bundle\EnrichBundle\MassEditAction\Operation\CapitalizeValues
        tags:
            - { name: form.type, alias: acme_enrich_operation_capitalize_values }

The FormType is now linked to the Operation with its name. You need to create a template to render your Mass Edit Action form:

1
2
3
4
5
6
7
#  /src/Acme/Bundle/EnrichBundle/Resources/views/MassEditAction/configure/capitalize-values.html.twig
{% extends 'PimEnrichBundle:MassEditAction:configure/layout.html.twig' %}

{% block formContent %}
    My Form is here !
    But we don't have any field for this case.
{% endblock %}

Note

It will be visible in the configure step of the mass edit.

Phase 3: Create the Processor

Well! Now the user can select the Operation to launch it. The Operation will send its config (filters, and actions) to a background job process. Now we have to write the Processor that will handle product modifications.

The Processor receives products one by one, given by the Reader:

  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
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
// /src/Acme/Bundle/EnrichBundle/Connector/Processor/MassEdit/Product/CapitalizeValuesProcessor.php
<?php

namespace Acme\Bundle\EnrichBundle\Connector\Processor\MassEdit\Product;

use Akeneo\Component\StorageUtils\Updater\PropertySetterInterface;
use Pim\Bundle\CatalogBundle\Exception\InvalidArgumentException;
use Pim\Bundle\CatalogBundle\Model\ProductInterface;
use Pim\Bundle\EnrichBundle\Connector\Processor\AbstractProcessor;
use Pim\Component\Connector\Repository\JobConfigurationRepositoryInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class CapitalizeValuesProcessor extends AbstractProcessor
{
    /** @var PropertySetterInterface */
    protected $propertySetter;

    /** @var ValidatorInterface */
    protected $validator;

    /**
     * @param JobConfigurationRepositoryInterface $jobConfigRepository
     * @param PropertySetterInterface             $propertySetter
     * @param ValidatorInterface                  $validator
     */
    public function __construct(
        JobConfigurationRepositoryInterface $jobConfigRepository,
        PropertySetterInterface $propertySetter,
        ValidatorInterface $validator
    ) {
        parent::__construct($jobConfigRepository);

        $this->propertySetter = $propertySetter;
        $this->validator      = $validator;
    }

    /**
     * {@inheritdoc}
     */
    public function process($product)
    {
        /** @var ProductInterface $product */

        // This is where you put your custom logic. Here we work on a
        // $product the Reader gave us.

        // This is the configuration we receive from our Operation
        $configuration = $this->getJobConfiguration();

        if (!array_key_exists('actions', $configuration)) {
            throw new InvalidArgumentException('Missing configuration for \'actions\'.');
        }

        $actions = $configuration['actions'];

        // Retrieve custom config from the action
        $field = $actions['field'];
        $options = $actions['options'];

        // Capitalize the attribute value of the product
        $originalValue    = $product->getValue($field)->getData();
        $capitalizedValue = strtoupper($originalValue);

        // Use the property setter to update the product
        $newData = ['field' => $field, 'value' => $capitalizedValue, 'options' => $options];
        $this->setData($product, [$newData]);

        // Validate the product
        if (null === $product || (null !== $product && !$this->isProductValid($product))) {
            $this->stepExecution->incrementSummaryInfo('skipped_products');

            return null; // By returning null, the product won't be saved by the Writer
        }

        // Used on the Reporting Screen to have a summary on the Mass Edit execution
        $this->stepExecution->incrementSummaryInfo('mass_edited');

        return $product; // Send the product to the Writer to be saved
    }

    /**
     * Validate the product and raise a warning if not
     *
     * @param ProductInterface $product
     *
     * @return bool
     */
    protected function isProductValid(ProductInterface $product)
    {
        $violations = $this->validator->validate($product);
        $this->addWarningMessage($violations, $product);

        return 0 === $violations->count();
    }

    /**
     * Set data from $actions to the given $product
     *
     * @param ProductInterface $product
     * @param array            $actions
     *
     * @return CapitalizeValuesProcessor
     */
    protected function setData(ProductInterface $product, array $actions)
    {
        foreach ($actions as $action) {
            $this->propertySetter->setData($product, $action['field'], $action['value'], $action['options']);
        }

        return $this;
    }
}

Again, register the newly created class:

1
2
3
4
5
6
7
8
# /src/Acme/Bundle/EnrichBundle/Resources/config/processors.yml
services:
    acme_enrich.mass_edit.capitalize_values.processor:
        class: Acme\Bundle\EnrichBundle\Connector\Processor\MassEdit\Product\CapitalizeValuesProcessor
        arguments:
            - '@pim_connector.repository.job_configuration'
            - '@pim_catalog.updater.product_property_setter'
            - '@pim_validator'

Phase 4: Create the background Job

Tip

The Job will run 3 steps: Read, Process & Write. In this cookbook, we use existing Reader and Writer.

We just wrote the Processor in the previous phase, so let’s tell the Job which services to use!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# /src/Acme/Bundle/EnrichBundle/Resources/config/batch_jobs.yml
connector:
    name: Akeneo Mass Edit Connector
    jobs:
        capitalize_values:
            title: capitalize_values
            type:  mass_edit
            steps:
                perform:
                    title: capitalize_values
                    services:
                        reader:    pim_enrich.connector.reader.mass_edit.product
                        processor: acme_enrich.mass_edit.capitalize_values.processor
                        writer:    pim_connector.writer.doctrine.product

The Job has to be in your database, so add it to your fixtures:

1
2
3
4
5
6
7
# /src/Acme/Bundle/EnrichBundle/Resources/fixtures/jobs.yml
jobs:
    capitalize_values:
        connector: Akeneo Mass Edit Connector
        alias:     capitalize_values
        label:     Mass capitalize products value
        type:      mass_edit

Note

To better understand how to handle this, you can read this chapter: Add your own Data

Phase 5: Translating the Mass Edit Action Choice

Once you have realized the previous operations (and eventually cleared your cache), you should see a new option on the /enrich/mass-edit-action/choose page.

Akeneo will generate for you a translation key following this pattern: pim_enrich.mass_edit_action.%alias%.label.

You may now define some translation keys (label, description and success_flash) in your translations catalog(s).