Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import attribute and option translations from Akeneo (#192) #207

Merged
merged 1 commit into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions config/services/attribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Webgriffe\SyliusAkeneoPlugin\Attribute\Importer;

return static function (ContainerConfigurator $containerConfigurator) {
$services = $containerConfigurator->services();

$services->set('webgriffe_sylius_akeneo.attribute.importer', Importer::class)
->args([
service('event_dispatcher'),
service('webgriffe_sylius_akeneo.api_client'),
service('sylius.repository.product_option'),
service('sylius.factory.product_option_translation'),
service('sylius.repository.product_attribute'),
service('sylius.factory.product_attribute_translation'),
])
->tag('webgriffe_sylius_akeneo.importer')
;
};
17 changes: 13 additions & 4 deletions docs/architecture_and_customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,21 @@ importer** (`Webgriffe\SyliusAkeneoPlugin\ProductAssociations\Importer`). This i
associations to the corresponding Sylius products associations. The association types must already exist on Sylius with
the same code they have on Akeneo.

### Attribute importer

Another provided importer is the **attribute importer** (`\Webgriffe\SyliusAkeneoPlugin\Attribute\Importer`). This
importer imports the Akeneo attribute translations into Sylius attribute and option translations.
The attributes and options must already exist on Sylius with the same code they have on Akeneo to be imported.

### Attribute options importer

Another provided importer is the **attribute options
importer** (`\Webgriffe\SyliusAkeneoPlugin\AttributeOptions\Importer`). This importer imports the Akeneo simple select
and multi select attributes options into Sylius select attributes. The select attributes must already exist on Sylius
with the same code they have on Akeneo.
Another provided importer is the **attribute options importer**
(`\Webgriffe\SyliusAkeneoPlugin\AttributeOptions\Importer`). This importer imports the Akeneo simple select
and multi select attributes options into Sylius select attributes. It imports also all attribute options that are used
on Sylius as product options. If the attribute has metrical type the values will not be imported because they could be
any value, it will be created by the ProductOptionValueResolver during product variant import.
The select attributes and the product options must already exist on Sylius
with the same code they have on Akeneo to be imported.

## Customize which Akeneo products to import

Expand Down
12 changes: 11 additions & 1 deletion docs/upgrade/upgrade-2.*.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ nav_order: 0
parent: Upgrade
---

# Upgrade from `v2.7.0` to `v2.8.0`

The v2.8.0 version introduces the Attribute importer.
If you want to import attribute and options translations from Akeneo you have to add the `--importer="Attribute"` option to the command that imports once a hour:

```diff
- 0 * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --all --importer="AttributeOptions"
+ 0 * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --all --importer="Attribute" --importer="AttributeOptions"
```

# Upgrade from `v2.4.0` to `v2.5.0`

The v2.5.0 version now allow you to choose which product and product model to import through webhook entry point.
Expand All @@ -15,7 +25,7 @@ Take a look at the [customization documentation](../architecture_and_customizati
The v2.4.0 version introduces the Product Model importer. If you are using the webhook no changes are requested as it will be automatically enqueued on every update.
If you are using the cronjob, you have to add the `--importer="ProductModel"` option to the command that imports every minute:

```git
```diff
- * * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --since-file=/path/to/sylius/var/storage/akeneo-import-sincefile.txt --importer="Product" --importer="ProductAssociations"
+ * * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --since-file=/path/to/sylius/var/storage/akeneo-import-sincefile.txt --importer="Product" --importer="ProductModel" --importer="ProductAssociations"
```
Expand Down
4 changes: 2 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,15 +459,15 @@ It could be useful to add also this command to your scheduler to run automatical
To make all importers and other plugin features work automatically the following is the suggested crontab:

```
0 * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --all --importer="AttributeOptions"
0 * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --all --importer="Attribute" --importer="AttributeOptions"
* * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --since-file=/path/to/sylius/var/storage/akeneo-import-sincefile.txt --importer="Product" --importer="ProductModel" --importer="ProductAssociations"
0 */6 * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:reconcile
0 0 * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:cleanup-item-import-results
```

This will:

* Import the update of all attribute options every hour
* Import the attribute/option translations and import all attribute/option values every hour
* Import, every minute, all products and product models that have been modified since the last execution, along with their associations
* Reconcile Akeneo deleted products every 6 hours

Expand Down
154 changes: 154 additions & 0 deletions src/Attribute/Importer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php

declare(strict_types=1);

namespace Webgriffe\SyliusAkeneoPlugin\Attribute;

use Akeneo\Pim\ApiClient\AkeneoPimClientInterface;
use Akeneo\Pim\ApiClient\Pagination\ResourceCursorInterface;
use Akeneo\Pim\ApiClient\Search\SearchBuilder;
use DateTime;
use Sylius\Component\Product\Model\ProductAttributeInterface;
use Sylius\Component\Product\Model\ProductAttributeTranslationInterface;
use Sylius\Component\Product\Model\ProductOptionInterface;
use Sylius\Component\Product\Model\ProductOptionTranslationInterface;
use Sylius\Component\Product\Repository\ProductOptionRepositoryInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Webgriffe\SyliusAkeneoPlugin\Event\IdentifiersModifiedSinceSearchBuilderBuiltEvent;
use Webgriffe\SyliusAkeneoPlugin\ImporterInterface;
use Webgriffe\SyliusAkeneoPlugin\ProductOptionHelperTrait;
use Webgriffe\SyliusAkeneoPlugin\SyliusProductAttributeHelperTrait;

/**
* @psalm-type AkeneoAttribute array{code: string, type: string, labels: array<string, ?string>}
*/
final class Importer implements ImporterInterface
{
public const SIMPLESELECT_TYPE = 'pim_catalog_simpleselect';

public const MULTISELECT_TYPE = 'pim_catalog_multiselect';

public const BOOLEAN_TYPE = 'pim_catalog_boolean';

public const METRIC_TYPE = 'pim_catalog_metric';

public const AKENEO_ENTITY = 'Attribute';

use ProductOptionHelperTrait, SyliusProductAttributeHelperTrait;

/**
* @param FactoryInterface<ProductOptionTranslationInterface> $productOptionTranslationFactory
* @param RepositoryInterface<ProductAttributeInterface> $productAttributeRepository
* @param FactoryInterface<ProductAttributeTranslationInterface> $productAttributeTranslationFactory
*/
public function __construct(
private EventDispatcherInterface $eventDispatcher,
private AkeneoPimClientInterface $apiClient,
private ProductOptionRepositoryInterface $productOptionRepository,
private FactoryInterface $productOptionTranslationFactory,
private RepositoryInterface $productAttributeRepository,
private FactoryInterface $productAttributeTranslationFactory,
) {
}

public function getAkeneoEntity(): string
{
return self::AKENEO_ENTITY;
}

public function getIdentifiersModifiedSince(DateTime $sinceDate): array
{
$searchBuilder = new SearchBuilder();
$this->eventDispatcher->dispatch(
new IdentifiersModifiedSinceSearchBuilderBuiltEvent($this, $searchBuilder, $sinceDate),
);
/**
* @psalm-suppress TooManyTemplateParams
*
* @var ResourceCursorInterface<array-key, AkeneoAttribute> $akeneoAttributes
*/
$akeneoAttributes = $this->apiClient->getAttributeApi()->all(50, ['search' => $searchBuilder->getFilters()]);

return array_merge(
$this->filterBySyliusAttributeCodes($akeneoAttributes),
$this->filterSyliusOptionCodes($akeneoAttributes),
);
}

public function import(string $identifier): void
{
/** @var AkeneoAttribute $akeneoAttribute */
$akeneoAttribute = $this->apiClient->getAttributeApi()->get($identifier);

$syliusProductAttribute = $this->productAttributeRepository->findOneBy(['code' => $identifier]);
if ($syliusProductAttribute instanceof ProductAttributeInterface) {
$this->importAttributeData($akeneoAttribute, $syliusProductAttribute);
}

$syliusProductOption = $this->productOptionRepository->findOneBy(['code' => $identifier]);
if ($syliusProductOption instanceof ProductOptionInterface) {
$this->importOptionData($akeneoAttribute, $syliusProductOption);
}
}

/**
* @return FactoryInterface<ProductOptionTranslationInterface>
*/
private function getProductOptionTranslationFactory(): FactoryInterface
{
return $this->productOptionTranslationFactory;
}

private function getProductOptionRepository(): ProductOptionRepositoryInterface
{
return $this->productOptionRepository;
}

/**
* @return RepositoryInterface<ProductAttributeInterface>
*/
private function getProductAttributeRepository(): RepositoryInterface
{
return $this->productAttributeRepository;
}

/**
* @param AkeneoAttribute $akeneoAttribute
*/
private function importAttributeData(array $akeneoAttribute, ProductAttributeInterface $syliusProductAttribute): void
{
$this->importProductAttributeTranslations($akeneoAttribute, $syliusProductAttribute);
$this->productAttributeRepository->add($syliusProductAttribute);
}

/**
* @param AkeneoAttribute $akeneoAttribute
*/
private function importOptionData(array $akeneoAttribute, ProductOptionInterface $syliusProductOption): void
{
$this->importProductOptionTranslations($akeneoAttribute, $syliusProductOption);
$this->productOptionRepository->add($syliusProductOption);
// TODO: Update also the position of the option? The problem is that this position is on family variant entity!
}

/**
* @param AkeneoAttribute $akeneoAttribute
*/
private function importProductAttributeTranslations(array $akeneoAttribute, ProductAttributeInterface $syliusProductAttribute): void
{
foreach ($akeneoAttribute['labels'] as $locale => $label) {
$productAttributeTranslation = $syliusProductAttribute->getTranslation($locale);
if ($productAttributeTranslation->getLocale() === $locale) {
$productAttributeTranslation->setName($label);

continue;
}
$newProductAttributeTranslation = $this->productAttributeTranslationFactory->createNew();
$newProductAttributeTranslation->setLocale($locale);
$newProductAttributeTranslation->setName($label);
$syliusProductAttribute->addTranslation($newProductAttributeTranslation);
}
}
}
Loading
Loading