Basic usage of catalogues in a plugin

This tutorial will introduce you to the catalogue export. We will develop a small plugin that exports variations and you will learn how to create and work with a catalogue to allow your users to map their data to a predefined format. If you are just getting started developing plugins for plentymarkets, we recommend to work through the getting started section first.

At the end of this tutorial, you will have developed a plugin that exports variations in a fictional format. In the UI, the result will look like this:

Result in UI
Figure 1. Result in UI

The complete source code for this tutorial is available here.

Creating the folders

In a first step, the folders are created. The final folder structure will look like this:

Folder structure
Figure 2. Folder structure

What is a catalogue?

Before we can start to develop a plugin that uses the catalogue, we first need to clarify what a catalogue actually is and of which components it consists. A catalogue describes the relation between source data in plentymarkets and a specific export format. The export format is defined by a template. The user will be able to link data sources or static values to each value of the export format. Thus, it provides the foundational configuration for the export.

What is a template?

A template is the core of a catalogue. It defines the export format and therefore what can be present in the final export result. The template is defined by the developer that provides the functionality for a specific export. It includes the following definitions:

  • Available result fields

  • Static template filters

  • Available custom filters

  • Necessary data manipulation

  • Available settings

The workflow is the following: a developer defines a template which contains the knowledge for an export in a specific format. Via this template, a user can then create a catalogue and fill it with data.

Using catalogues in a plugin

In the following section, we will create a template and extend its functionality step by step until we covered all fundamental features.

Basics

To get started, we will create a very simple template with just a single result field and no additional logic like filtering or data manipulation. To do so, we need to create a template provider class. This class needs to extend the abstract base class AbstractGroupedTemplateProvider. It will be used to fill a template with data when it gets booted. A template provider can be used for the booting process of multiple templates, so you don’t need to create multiple classes for similar templates (e.g. you create templates for different product categories of the same marketplace).

For simplicity reasons, we will first provide the example code and afterwards look into what each method does. The example code can be copied from here:

example-template.php
<?php

namespace BasicCatalogExport\Providers;

use Plenty\Modules\Catalog\Containers\Filters\CatalogFilterBuilderContainer;
use Plenty\Modules\Catalog\Containers\TemplateGroupContainer;
use Plenty\Modules\Catalog\Models\SimpleTemplateField;
use Plenty\Modules\Catalog\Models\TemplateGroup;
use Plenty\Modules\Catalog\Templates\Providers\AbstractGroupedTemplateProvider;

/**
* Class ExampleTemplateProvider
* @package BasicCatalogExport\Providers
*/
class ExampleTemplateProvider extends AbstractGroupedTemplateProvider
{
   public function getTemplateGroupContainer(): TemplateGroupContainer
   {
       /** @var TemplateGroupContainer $templateGroupContainer */
       $templateGroupContainer = pluginApp(TemplateGroupContainer::class);

       /** @var TemplateGroup $templateGroup */
       $templateGroup = pluginApp(TemplateGroup::class,
           [
               "identifier" => "groupOne",
               "label" => "fields" // In a productive plugin this should be translated
           ]);

       /** @var SimpleTemplateField $templateField */
       $templateField = pluginApp(SimpleTemplateField::class, [
           'uniqueIdentifier',
           'theResultKey',
           'First example field' // In a productive plugin this should be translated
       ]);

       $templateGroup->addGroupField($templateField);

       $templateGroupContainer->addGroup($templateGroup);

       return $templateGroupContainer;
   }

   public function getFilterContainer(): CatalogFilterBuilderContainer
   {
       return pluginApp(CatalogFilterBuilderContainer::class);
   }

   public function getCustomFilterContainer(): CatalogFilterBuilderContainer
   {
       return pluginApp(CatalogFilterBuilderContainer::class);
   }

   public function isPreviewable(): bool
{
   // If you are not sure what this does, check the guide for DynamicConfig before setting this to true
   // In your productive plugin
   return true;
}

}

We now have this example template provider. However, it needs to be registered before we can create a catalogue through this template. We can do this in the service provider of our plugin. The service provider should now look like this:

service-provider.php
<?php

namespace BasicCatalogExport;

use BasicCatalogExport\Providers\ExampleTemplateProvider;
use Plenty\Modules\Catalog\Contracts\TemplateContainerContract;
use Plenty\Plugin\ServiceProvider;

/**
* Class BasicCatalogExportServiceProvider
* @package BasicCatalogExport\Providers
*/
class BasicCatalogExportServiceProvider extends ServiceProvider
{
   const PLUGIN_NAME = "BasicCatalogExport";

   public function register()
   {
       /** @var TemplateContainerContract $templateContainer */
       $templateContainer = pluginApp(TemplateContainerContract::class);

       $templateContainer->register("variationExport", self::PLUGIN_NAME, ExampleTemplateProvider::class);
   }
}

Since our template is now registered and providing a field, the template will be available in the catalogue UI to create a new catalogue.

Create catalogue
Figure 3. Create catalogue
Add data fields
Figure 4. Add data fields

We can now map a source to the field, e.g. the Variation ID, so we have a fully exportable catalogue. We can test this by using the preview function.

Preview of the export
Figure 5. Preview of the export

Now that we have a working example template that implements the most basic functionality, we can go deeper into the specific topics that are definable through a template.

The specific explanations about what each method in our template provider example does are available in the following sections:

  • getTemplateGroupContainer()

  • getFilterContainer()

  • getCustomFilterContainer()

  • isPreviewable()

Fields

The core part of a template are its fields. They are defined through the getTemplateGroupContainer() method in the template provider. The general behaviour is the following: each template contains groups, which themselves contain fields. The groups are generally meant as a tool to order fields in the UI. So if it makes sense for two fields to be displayed together they should be in the same group. The order of the groups and the fields in the groups is identical to the order in which they are added to their respective container.

We will look at the usage by means of our earlier defined example.

template-fields.php
public function getTemplateGroupContainer(): TemplateGroupContainer
{
   /** @var TemplateGroupContainer $templateGroupContainer */
   $templateGroupContainer = pluginApp(TemplateGroupContainer::class);

   /** @var TemplateGroup $templateGroup */
   $templateGroup = pluginApp(TemplateGroup::class,
       [
           "identifier" => "groupOne",
           "label" => "fields" // In a productive plugin this should be translated
       ]);

   /** @var SimpleTemplateField $templateField */
   $templateField = pluginApp(SimpleTemplateField::class, [
       'uniqueIdentifier',
       'theResultKey',
       'First example field' // In a productive plugin this should be translated
   ]);

   $templateGroup->addGroupField($templateField);

   $templateGroupContainer->addGroup($templateGroup);

   return $templateGroupContainer;
}

As we can see here, we first define our container, then we define a group and at last a field. In general, this process will always be the same. That means we will always define groups, and we will always add fields to them. What will change depending on the use case is the type of the field. As we can see in the example, we define an instance of the SimpleTemplateField class. Depending on the specific use case, we would have to use a different one.

Currently available are:
SimpleTemplateField: Defines a result field that can be filled through a source field out of the plentymarkets system or a custom value.
ComplexTemplateField: Defines a result field that will be filled by a value out of a predefined list depending on a condition (e.g. a specific category name depending on a linked category in plentymarkets).
CombinedTemplateField: Defines two result fields - a complex one and a simple one. The complex field will be filled depending on which source fields were used to fill the simple field (e.g. filling a barcode type depends on which source field was used to fill the barcode field).

In general, you want to use a simple field if you expect a non-predefinable value, e.g. an identifier like the name of a variation.

The complex field should be used whenever you have a list of values. A typical example is linking a variation to a category. A marketplace will not let you define new categories on the fly. Instead, you have to choose an existing one and link your variation to it. Therefore, we have a finite amount of valid values which we can provide in a complex field.

The combined field is true to its name. You should use it, if you have a complex field which is logically bound to a simple field. A marketplace might expect a barcode and the specific type of that barcode. Therefore, those two fields should be combined into one field in order to determine the barcode and also its type.

We will now add some fields to our template to use each of these at least once. The following fields will be added in our example:

  • variationName

  • price

  • sku

  • stock

  • category

  • barcode

  • barcode type

Before we start adding fields, we remove the example field we had until now. So currently our method should just create a container with an empty group.

Simple fields

Now we can have a look at all parameters in a simple template field. The following parameters are available:

exportKey: The key that will be present in the export result.
key: A unique identifier, which will be used to work with the field. Therefore, the exportKey can be changed if necessary without users having to redo all the mappings.
label: The string that will be displayed in the UI.
isRequired: Will mark fields with an asterisk () in the UI and display a warning message if the catalogue is saved, but a required field is not filled.
*isLocked
: If this is set to true, the user won’t be able to configure this field. This is useful to define some non-flexible default values which are always necessary in the export.
isArray: Defines which behaviour is applied in the export for this field. If set to true, all mapped sources will be used to fill an array for this field at the end. If set to false, only the first non-null value will be written into the field and all following sources will be skipped.
meta: Not actually used by the export logic. This is useful, if you have any additional information your plugin needs when working with a field directly.
defaultSources: Defines which sources are mapped by default. These might get overwritten by the user if isLocked is false.

Now that we have covered all aspects of a SimpleTemplateField, we can implement some fields into our template.

We can add the fields variationName, price and sku, since these are fields that expect an individual value and therefore should be defined as simple fields. The definition for those will look identical to our first example. The only difference is that these should be required.

Once you’re done, the getTemplateGroupContainer() method should look like this:

simple-fields.php
public function getTemplateGroupContainer(): TemplateGroupContainer
{
   /** @var TemplateGroupContainer $templateGroupContainer */
   $templateGroupContainer = pluginApp(TemplateGroupContainer::class);

   /** @var TemplateGroup $templateGroup */
   $templateGroup = pluginApp(TemplateGroup::class,
       [
           "identifier" => "groupOne",
           "label" => "fields" // In a productive plugin this should be translated
       ]);

   /** @var SimpleTemplateField $name */
   $name = pluginApp(SimpleTemplateField::class, [
       'variationName',
       'name',
       'Variation name', // In a productive plugin this should be translated
       true
   ]);

   /** @var SimpleTemplateField $price */
   $price = pluginApp(SimpleTemplateField::class, [
       'price',
       'price',
       'Sales price', // In a productive plugin this should be translated
       true
   ]);

   /** @var SimpleTemplateField $sku */
   $sku = pluginApp(SimpleTemplateField::class, [
       'sku',
       'sku',
       'SKU', // In a productive plugin this should be translated
       true
   ]);

   /** @var SimpleTemplateField $stock */
   $stock = pluginApp(SimpleTemplateField::class, [
       'stock',
       'stock',
       'Stock', // In a productive plugin this should be translated
       true
   ]);

   $templateGroup->addGroupField($name);
   $templateGroup->addGroupField($price);
   $templateGroup->addGroupField($sku);
   $templateGroup->addGroupField($stock);

   $templateGroupContainer->addGroup($templateGroup);

   return $templateGroupContainer;
}

A catalogue that is created with this template would now look like this:

New catalogue
Figure 6. New catalogue

To make the usage of this catalogue more convenient to the user, we can add the net stock of the variation as default source for the stock field. The source can either be loaded through the FieldGroupRepository or you can just go to the catalogue UI, open the dev tools and map the source yourself.

Mapping sources
Figure 7. Mapping sources

After clicking the save button, you can see the source in the payload of the response. We will just copy that source array and put it into the defaultSources parameter in our field definition (the UUID field is not necessary). The field definition for our stock field will now look like this:

field-definition-stock.php
/** @var SimpleTemplateField $stock */
$stock = pluginApp(SimpleTemplateField::class, [
   'stock',
   'stock',
   'Stock', // In a productive plugin this should be translated
   true,
   false,
   false,
   [],
   [
       [
           'fieldId' => 'stock-0',
           'id' => 0,
           'isCombined' => false,
           'key' => null,
           'type' => "stock",
           'value' => null
       ]
   ]
]);

If the catalogue is created with our current template, it will now look like this:

Catalogue view
Figure 8. Catalogue view

Complex fields

A complex template field is mostly similar to the definition of a simple template field. The most important difference is that a complex template field needs an instance of CatalogMappingValueProviderContract to define the list of mappable values. Another difference is that complex fields do not support default sources. The mapping value provider defines the possible valid values, which are the core of a complex mapping.

Since we only provide you with an interface, you can determine the values in the way that best matches your needs. If it is a short static list of values, you can just return them from a hard-coded array. If it is bigger, you can store and load it from a database. It does not matter where the data comes from.

In our example, the category field should be a complex field. To keep it simple, we will just add a few categories with a maximum nested level of two.

We will have the category men, which contains the categories shoes and jeans. And we will have the category women, which also contains the categories shoes and jeans. Let’s assume that we have to transfer the ID of a category. Our provider could then look like this:

complex-fields.php
<?php

namespace BasicCatalogExport\Providers;

use Plenty\Modules\Catalog\Containers\CatalogMappingValueContainer;
use Plenty\Modules\Catalog\Contracts\CatalogMappingValueProviderContract;
use Plenty\Modules\Catalog\Models\CatalogMappingValue;

class ExampleCategoryMappingValueProvider implements CatalogMappingValueProviderContract
{
   protected $categories = [
       1 => [
           'id' => 1,
           'label' => 'Men',
           'hasChildren' => true,
           'parentId' => null
       ],
       2 => [
           'id' => 2,
           'label' => 'Women',
           'hasChildren' => true,
           'parentId' => null
       ],
       3 => [
           'id' => 3,
           'label' => 'Shoes',
           'parentId' => 1,
           'hasChildren' => false
       ],
       4 => [
           'id' => 4,
           'label' => 'Jeans',
           'parentId' => 1,
           'hasChildren' => false
       ],
       5 => [
           'id' => 5,
           'label' => 'Shoes',
           'parentId' => 2,
           'hasChildren' => false
       ],
       6 => [
           'id' => 6,
           'label' => 'Jeans',
           'parentId' => 2,
           'hasChildren' => false
       ],
   ];

   public function getValueById(string $id): CatalogMappingValue
   {
       if (!isset($this->categories[$id])) {
           throw new \Exception('Category does not exist.', 404);
       }

       return pluginApp(CatalogMappingValue::class, [
           $this->categories[$id]['id'],
           $this->categories[$id]['label'],
           $this->categories[$id]['label'],
           $this->categories[$id]['parentId'],
           $this->categories[$id]['hasChildren']
       ]);
   }

   public function getValuesByParentId(string $parentId = null): CatalogMappingValueContainer
   {
       $mappingValueContainer = pluginApp(CatalogMappingValueContainer::class);

       foreach ($this->categories as $category) {
           if ($category['parentId'] != $parentId) {
               continue;
           }

           $mappingValue = pluginApp(CatalogMappingValue::class, [
               $category['id'],
               $category['label'],
               $category['label'],
               $category['parentId'],
               $category['hasChildren']
           ]);

           $mappingValueContainer->addMappingValue($mappingValue);
       }

       return $mappingValueContainer;
   }

   /**
    * @param array $params
    * @return CatalogMappingValueContainer
    */
   public function getValues(array $params = []): CatalogMappingValueContainer
   {
       return pluginApp(CatalogMappingValueContainer::class);
   }
}

We will now look into what each method does.

The first method getValueById() should be self-explanatory. It will receive an identifier and should return the value that matches the identifier. The value has to be wrapped in an instance of CatalogMappingValue.

The next method getValuesByParentId is called to load a single level of values. If the UI of a catalogue is opened, an initial request is sent that requests all values with parentId = null. If our provider has nested values, it can happen that a user clicks on a parent value in the UI, which triggers a call to this method that includes the clicked value’s ID. In this case, we should return the nested values for that value in an instance of CatalogMappingValueContainer.

The third method can be used to provide filter functionality. Currently, we do not send any search requests. Therefore, this can just return an empty container for now.

Now that we have finished our provider, we can use it to create our complex field. To keep our template tidy, we can create a new group to separate this one from the simple fields. That means that we have to add the following lines to our getTemplateGroupContainer() method:

complex-fields-group.php
// Complex field

/** @var TemplateGroup $complexGroup */
$complexGroup = pluginApp(TemplateGroup::class,
   [
       "identifier" => "groupTwo",
       "label" => "Complex fields" // In a productive plugin this should be translated
   ]);

/** @var ComplexTemplateField $name */
$category = pluginApp(ComplexTemplateField::class, [
   'category',
   'category',
   'Category', // In a productive plugin this should be translated
   pluginApp(ExampleCategoryMappingValueProvider::class),
   true
]);

$complexGroup->addGroupField($category);
$templateGroupContainer->addGroup($complexGroup);

The user will now be able to map categories in the UI:

Mapping categories
Figure 9. Mapping categories

Combined fields

Since we’ve already covered the other two types, this one should be straightforward. It is actually a combination of the previous two types. Just as in the last step, we need an instance of CatalogMappingValueProviderContract. This time, it will provide different types of barcodes. To keep it simple, let’s only use GTIN and ISBN for now. The provider class could look like this:

combined-fields-provider-class.php
<?php

namespace BasicCatalogExport\Providers;

use Plenty\Modules\Catalog\Containers\CatalogMappingValueContainer;
use Plenty\Modules\Catalog\Contracts\CatalogMappingValueProviderContract;
use Plenty\Modules\Catalog\Models\CatalogMappingValue;

class ExampleBarcodeTypeMappingValueProvider implements CatalogMappingValueProviderContract
{
   protected $types = [
       'GTIN' => [
           'id' => 'GTIN',
           'label' => 'GTIN'
       ],
       'ISBN' => [
           'id' => 'ISBN',
           'label' => 'ISBN'
       ]
   ];

   public function getValueById(string $id): CatalogMappingValue
   {
       if (!isset($this->categories[$id])) {
           throw new \Exception('Type does not exist.', 404);
       }

       return pluginApp(CatalogMappingValue::class, [
           $this->types[$id]['id'],
           $this->types[$id]['label'],
           $this->types[$id]['label']
       ]);
   }

   public function getValuesByParentId(string $parentId = null): CatalogMappingValueContainer
   {
       $mappingValueContainer = pluginApp(CatalogMappingValueContainer::class);

       if (!is_null($parentId)) {
           return $mappingValueContainer;
       }
       foreach ($this->types as $type) {

           $mappingValue = pluginApp(CatalogMappingValue::class, [
               $type['id'],
               $type['label'],
               $type['label']
           ]);

           $mappingValueContainer->addMappingValue($mappingValue);
       }

       return $mappingValueContainer;
   }

   /**
    * @param array $params
    * @return CatalogMappingValueContainer
    */
   public function getValues(array $params = []): CatalogMappingValueContainer
   {
       return pluginApp(CatalogMappingValueContainer::class);
   }
}

Additionally, we need an instance of CatalogTemplateFieldContainer which will contain all simple fields of our combined field. The logic is the following: The user can select a value of our value provider, e.g. GTIN. Then, he/she will have the opportunity to map all simple fields that are linked to our combined field. In our case, this will only be the barcode field. If the catalogue can fill the barcode field with the source that was provided by the user, we will also export the value GTIN for the barcode type field.

Note: The behaviour of isArray in the context of CombinedFields is not strictly defined yet. We suggest using false for now until this will have changed.

Thus, our container instance will only contain the barcode field. Let’s again create a new group for this field. The template provider should now include the following lines:

combined-fields-group.php
// Combined field

/** @var TemplateGroup $combinedGroup */
$combinedGroup = pluginApp(TemplateGroup::class,
   [
       "identifier" => "groupThree",
       "label" => "Combined fields" // In a productive plugin this should be translated
   ]);

/** @var CatalogTemplateFieldContainer $simpleContainer */
$simpleContainer = pluginApp(CatalogTemplateFieldContainer::class);

/** @var SimpleTemplateField $name */
$barcode = pluginApp(SimpleTemplateField::class, [
   'barcode',
   'barcode',
   'Barcode',
   true
]);

$simpleContainer->addField($barcode);

/** @var CombinedTemplateField $name */
$barcodeType = pluginApp(CombinedTemplateField::class, [
   'barcodeType',
   'barcodeType',
   'Barcode type', // In a productive plugin this should be translated
   pluginApp(ExampleBarcodeTypeMappingValueProvider::class),
   $simpleContainer
]);

$combinedGroup->addGroupField($barcodeType);
$templateGroupContainer->addGroup($combinedGroup);

Now the user is able to define the barcode and its type in the UI.

Defining barcodes
Figure 10. Defining barcodes

Using filters

Since our export now contains all necessary fields, we should have a look at filtering. There are two different types of filters. The first type are the static ones. In the scope of the project, we will simply refer to them as filters. These are defined in the template and cannot be changed by the user. The second type are the custom filters. These are already provided, but the customer decides if and how he/she wants to use them.

Let’s begin with a simple example. We might only want active variations, so we want to add the filter for that. To do so, we need to change our getFilterContainer() method. All we have to do is add a filter to the container before we return it. Each export type can potentially use different kinds of filters. In our case, we are exporting variations. For convenience, there is the VariationFilterBuilderFactory class that has a method for each available variation filter.

So let us create an instance of VariationFilterBuilderFactory and request a VariationIsActive object. In this object, we need to specify whether we want active or inactive variations.

In our case, we want to export active variations, so we call the setShouldBeActive() method with the shouldBeActive param set to true.

Our getFilterContainer() method should now look like this:

filter-method.php
public function getFilterContainer(): CatalogFilterBuilderContainer
{
   /** @var CatalogFilterBuilderContainer $container */
   $container = pluginApp(CatalogFilterBuilderContainer::class);
   /** @var VariationFilterBuilderFactory $filterBuilderFactory */
   $filterBuilderFactory = pluginApp(VariationFilterBuilderFactory::class);

   $variationIsActiveFilter = $filterBuilderFactory->variationIsActive();
   $variationIsActiveFilter->setShouldBeActive(true);
   $container->addFilterBuilder($variationIsActiveFilter);

   return $container;
}

Overall, the getCustomFilter() method is handled identically. The only difference in the definition is that filters do not necessarily need default values in order to work, since they can be manipulated by the user. Apart from that the available filters are identical.

Just to have it covered, let’s also add a custom filter. A good example is the filter ItemHasIds. With this one, the user will be able to provide specific IDs through the UI. Since we provide a custom filter, this time we do not have to provide a default value. However, we can do so if the use case benefits from this. Our getCustomFilterContainer() method should now look like this:

custom-filter.php
public function getCustomFilterContainer(): CatalogFilterBuilderContainer
{
   /** @var CatalogFilterBuilderContainer $container */
   $container = pluginApp(CatalogFilterBuilderContainer::class);
   /** @var VariationFilterBuilderFactory $filterBuilderFactory */
   $filterBuilderFactory = pluginApp(VariationFilterBuilderFactory::class);

   $itemHasIdsFilter = $filterBuilderFactory->itemHasIds();
   $container->addFilterBuilder($itemHasIdsFilter);

   return $container;
}

If we look into the filter tab in our catalogue’s UI, we will now see the static filter for active variations as well as the option to add the itemHasIds filter manually.

Adding filters
Figure 11. Adding filters

Manipulating data

To enable the catalogue to handle all aspects of your export logic, you should try to avoid manipulating data outside the catalogue’s scope. Since you might still need to change some details here and there, we provide multiple tools to manipulate data inside the catalogue.

The first way would be a field-specific callback. You may have noticed that the three types of fields contain the method setCallable(). Via this method, you can inject an instance of CatalogTemplateFieldCallableContract. This interface only provides the method call(). This method will be called each time this field is filled with a non-null value. In the method, you receive the raw item, so all the raw data that was loaded for a specific item in this export. In the variation export, this would be all required data for a specific variation. You also receive the value that was mapped to the field and the type of that value. Whatever you return in this method will then fill the field.

Therefore, you can conditionally manipulate the field’s data directly in the mapping process. A common scenario for this is the generation of an sku. Typically, the customer maps a source from which the sku should be generated, and an actual sku as fallback. So our callback could check for the source type, and if it is not of the type sku, a new sku can be created on the fly at this point. If you want to validate the whole result before generating an sku, you can also store the information about the type somewhere else and handle the generation at a later point.

So let’s add this example to our sku field. First, we need our own implementation of CatalogTemplateFieldCallableContract. For this example, we won’t actually generate an sku but rather set up the basics which would allow us to do this in a productive use case. The class could look like this:

manipulate-data-class.php
<?php

namespace BasicCatalogExport\Callbacks;

use Plenty\Modules\Catalog\Contracts\CatalogTemplateFieldCallableContract;

class ExampleSkuCallback implements CatalogTemplateFieldCallableContract
{
   public function call($item, $value, string $originType)
   {
       if ($originType != 'sku')
       {
           //Something that is not an sku was used as source, we could use this to generate one
       }

       //The returned value will overwrite the previously mapped value in the export result
       //to see a change in the export we can return a hard-coded value in this example
       return "Our callback works!";
   }
}

Obviously, we still need to register this callback on our sku field. To do this, we just change the definition of the sku field in the template provider to look like this:

definition-sku.php
/** @var SimpleTemplateField $sku */
$sku = pluginApp(SimpleTemplateField::class, [
   'sku',
   'sku',
   'SKU', // In a productive plugin this should be translated
   true
]);
$sku->setCallable(pluginApp(ExampleSkuCallback::class));

Another way to manipulate the data are post mutators. You can add post mutators on the template level through your template provider. To do that, all you have to do is overwrite the getPostMutator() method and return your own implementation of CatalogMutatorContract.

Again, this contains only a single method - in this case it is named mutate(). This method is called once per item after the mapping logic is done. This means you receive a fully mapped item and you can change it however you want.

Whatever you return in this method will overwrite the item provided by the catalogue. This means if you want to extract HTML, validate specific entries or other post mapping stuff, this is the right spot to do so.

However, this is not meant to convert the result into a CSV, XML or another file format. If you need to do so, have a look at ResultConverters.

To understand how this works, let’s add a key to the result at the end. This will just be a string that says “We manipulated our result”. Again, we have to create our own implementation - this time of CatalogMutatorContract. Our implementation should look like this:

post-mutators.php
<?php

namespace BasicCatalogExport\Mutators;

use Plenty\Modules\Catalog\Contracts\CatalogMutatorContract;

class ExamplePostMutator implements CatalogMutatorContract
{
   public function mutate($item)
   {
       $item['foo'] = "We manipulated our result";

       return $item;
   }
}

Of course you will have all your template fields’ keys in the item array so we could also manipulate each key independently if we need to.

To use our mutator, we will just add the method getPostMutator() to our template provider like this:

mutator-method.php
public function getPostMutator(): CatalogMutatorContract
{
   return pluginApp(ExamplePostMutator::class);
}

Exporting data

Until now, we used the catalogue UI to look into the data that is exported. However, in our productive environment, we might want to trigger the export from the code. Although there are multiple ways of doing so, in this guide we will only have a look at one of them.

For simplicity reasons, we will trigger our export through a REST route. Therefore, we first need to create a RouteServiceProvider and a controller that will be called by our route. That could look like this:

trigger-export-1.php
<?php

namespace BasicCatalogExport;

use Plenty\Plugin\RouteServiceProvider;
use Plenty\Plugin\Routing\ApiRouter;
use Plenty\Plugin\Routing\Router as WebRouter;

class BasicCatalogExportRouteServiceProvider extends RouteServiceProvider
{
   public function map(ApiRouter $api, WebRouter $webRouter) {
       $api->version(['v1'], ['middleware' => ['oauth']], function ($router) {
           $router->get('example/export', ['uses' => 'BasicCatalogExport\Controllers\VariationExportController@export']);
       });
   }
}
trigger-export-2.php
<?php

namespace BasicCatalogExport\Controllers;

use Plenty\Plugin\Controller;

class VariationExportController extends Controller
{
   public function export()
   {

   }
}

Make sure to register the RouteServiceProvider in the ServiceProvider. The BasicCatalogExportServiceProvider should then look like this:

route-service-provider.php
<?php

namespace BasicCatalogExport;

use BasicCatalogExport\Providers\ExampleTemplateProvider;
use Plenty\Modules\Catalog\Contracts\TemplateContainerContract;
use Plenty\Plugin\ServiceProvider;

/**
* Class BasicCatalogExportServiceProvider
* @package BasicCatalogExport\Providers
*/
class BasicCatalogExportServiceProvider extends ServiceProvider
{
   const PLUGIN_NAME = "BasicCatalogExport";

   public function register()
   {
       $this->getApplication()->register(BasicCatalogExportRouteServiceProvider::class);

       /** @var TemplateContainerContract $templateContainer */
       $templateContainer = pluginApp(TemplateContainerContract::class);

       $templateContainer->register("variationExport", self::PLUGIN_NAME, ExampleTemplateProvider::class);
   }
}

Now we can trigger our method via a REST call to the route GET::/rest/example/export.

In our function, we can now write the logic to export a catalogue. First, we need to retrieve the relevant catalogues. In most cases, this will be all active catalogues that are created through templates of our type. So we request an instance of CatalogRepositoryContract, define the filters in it and load the catalogues. Thus, our method now looks like this:

export-catalogue.php
/** @var CatalogRepositoryContract $catalogRepository */
$catalogRepository = pluginApp(CatalogRepositoryContract::class);
$catalogRepository->setFilters(
   [
       'type' => BasicCatalogExportServiceProvider::PLUGIN_NAME,
       'active' => true
   ]
);

$page = 1;

do {
   $paginatedResult = $catalogRepository->all($page);
   foreach ($paginatedResult->getResult() as $catalog) {

   }
} while (!$paginatedResult->isLastPage());

Inside of the foreach, we will always have a catalogue that is matching our filters. Since we just want to have an example, we can add a return statement at the end of the foreach, so that we only export a single one for now. Additionally, we now need an instance of CatalogExportRepositoryContract which we can request before we enter our do while loop. With this repository we can trigger the export for a specific catalogue. A basic export can look like this:

basic-export.php
$page = 1;
$resultArray = [];

/** @var CatalogExportRepositoryContract $catalogExportRepository */
$catalogExportRepository = pluginApp(CatalogExportRepositoryContract::class);

do {
   $paginatedResult = $catalogRepository->all($page);
   foreach ($paginatedResult->getResult() as $catalog) {
       $exportService = $catalogExportRepository->exportById($catalog->id);

       //$exportService->applyDynamicConfig(); Will run the dynamic config logic. This should be used in most scenarios

       // These can be used to only trigger a partial export of a catalogue. A good example is a stock export,
       // which does not need the item-specific data apart from stock and sku
       //$exportService->allowExportKeys();
       //$exportService->forbidExportKeys();

       $catalogExportResult = $exportService->getResult();

       foreach ($catalogExportResult as $page) {
           $resultArray = array_merge($resultArray, $page);
       }

       return $resultArray;
   }
} while (!$paginatedResult->isLastPage());

Let’s go through what happens here line by line. At the beginning of the do while, we load all our catalogues. We then iterate through them and request an instance of CatalogExportServiceContract through the exportById() method of our CatalogExportRepositoryContract. The comments show some examples of working with this service before triggering the actual export. What is important is that all the logic that is applied at this point and has to be done to get a valid export should be moved into the DynamicConfig. This is necessary to enable exports directly out of the catalogue scope (e.g. preview or download through the UI). If you don’t provide all that logic in the DynamicConfig, you should set the isPreviewable attribute of your template to false. An in-depth explanation of DynamicConfig can be found here.

Through the getResult() method, we receive an instance of CatalogExportResult. This will actually run the mapping logic and return our results in an array. You can receive the result in chunks by iterating over the CatalogExportResult, since it implements the iterable interface.

We then append each chunk to a result array. Keep in mind that this is done here to provide a simple example. In your application, you should try to work with chunks whenever possible to keep memory consumption low.

To make the overview of our example easier, let’s add the Variation ID as source of our Variation name field as well as any source for our sku field in the catalogue. So in the UI our catalogue now looks like this:

Adding sources
Figure 12. Adding sources

In postman, we get a response like this if we call our route:

Response in postman
Figure 13. Response in postman

As you can see, our data manipulation is applied and we receive the mapped values for the non-manipulated fields.

You now know the fundamentals to develop your export plugin by using catalogues. You know how to create and register a template, how to define fields, filter and manipulate data, and trigger exports. With this, you are already able to cover most use cases.

Further reading

For further specific use cases we provide different guides which will help you to optimise your usage of the catalogue. Some examples are: