Skip to content

Commit d745128

Browse files
Add extension points for custom pricing/data and structure sync
Adds a ProductDocumentsIndexEvent (indexation) and a SearchRequestContextEvent (search request, price_group_id) so a project can plug custom data/pricing logic without decorating the plugin. Also lets several dataproviders contribute to the same structure sync entity (catalog/sourceField/ sourceFieldOption), splitting the built-in attribute/option providers and subscribers accordingly as a first real usage of that mechanism. Refs #33, refs #1391443
1 parent d99f8d6 commit d745128

24 files changed

Lines changed: 629 additions & 267 deletions

src/Command/StructureClean.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
4747
$output->writeln("$message ...");
4848
// @phpstan-ignore method.dynamicName
4949
$this->synchonizer->{$method}(
50-
$this->providers[$entity]->provide(),
50+
$this->provideAll($entity),
5151
true,
5252
$isDryRun
5353
);

src/Command/StructureSync.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
)]
2828
class StructureSync extends Command
2929
{
30-
/** @var ProviderInterface[] */
31-
protected array $providers;
30+
/** @var array<string, ProviderInterface[]> */
31+
protected array $providers = [];
3232

3333
/** @var array<string, string> */
3434
protected array $syncMethod = [
@@ -45,7 +45,9 @@ public function __construct(
4545
iterable $providers,
4646
) {
4747
parent::__construct();
48-
$this->providers = iterator_to_array($providers);
48+
foreach ($providers as $provider) {
49+
$this->providers[$provider->getEntity()][] = $provider;
50+
}
4951
}
5052

5153
protected function execute(InputInterface $input, OutputInterface $output): int
@@ -57,7 +59,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
5759
$time = microtime(true);
5860
$output->writeln("$message ...");
5961
// @phpstan-ignore method.dynamicName
60-
$this->synchonizer->{$method}($this->providers[$entity]->provide());
62+
$this->synchonizer->{$method}($this->provideAll($entity));
6163
$time = number_format(microtime(true) - $time, 2);
6264
$output->writeln("\033[1A$message <info>✔</info> ($time)s");
6365
}
@@ -66,4 +68,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6668

6769
return 0;
6870
}
71+
72+
/**
73+
* Merges the provide() results of every provider registered for the given entity,
74+
* so several namespaces can contribute to it independently.
75+
*/
76+
protected function provideAll(string $entity): iterable
77+
{
78+
foreach ($this->providers[$entity] ?? [] as $provider) {
79+
yield from $provider->provide();
80+
}
81+
}
6982
}

src/Controller/Admin/GallyController.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131

3232
final class GallyController extends AbstractController
3333
{
34-
/** @var ProviderInterface[] */
35-
protected array $providers;
34+
/** @var array<string, ProviderInterface[]> */
35+
protected array $providers = [];
3636

3737
/** @var array<string, string> */
3838
protected array $syncMethod = [
@@ -49,9 +49,21 @@ public function __construct(
4949
private TranslatorInterface $translator,
5050
private CacheManager $cacheManager,
5151
) {
52-
/** @var ProviderInterface[] $providersArray */
53-
$providersArray = iterator_to_array($providers);
54-
$this->providers = $providersArray;
52+
/** @var ProviderInterface $provider */
53+
foreach ($providers as $provider) {
54+
$this->providers[$provider->getEntity()][] = $provider;
55+
}
56+
}
57+
58+
/**
59+
* Merges the provide() results of every provider registered for the given entity,
60+
* so several namespaces can contribute to it independently.
61+
*/
62+
private function provideAll(string $entity): iterable
63+
{
64+
foreach ($this->providers[$entity] ?? [] as $provider) {
65+
yield from $provider->provide();
66+
}
5567
}
5668

5769
public function renderGallyConfigForm(Request $request): Response
@@ -122,7 +134,7 @@ public function renderSyncFieldsForm(Request $request): Response
122134
if ($validConnection) {
123135
foreach ($this->syncMethod as $entity => $method) {
124136
// @phpstan-ignore method.dynamicName
125-
$this->synchonizer->{$method}($this->providers[$entity]->provide());
137+
$this->synchonizer->{$method}($this->provideAll($entity));
126138
}
127139
$this->addFlash('success', $this->translator->trans('gally_sylius.ui.sync_success'));
128140
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gally\SyliusPlugin\Event;
6+
7+
use Sylius\Component\Core\Model\ProductInterface;
8+
use Symfony\Contracts\EventDispatcher\Event;
9+
10+
/**
11+
* Dispatched once per channel/locale batch during product indexation, so a project
12+
* can add custom fields or override existing ones (e.g. price) in the documents
13+
* about to be sent to Gally, using the already-loaded product collection.
14+
*/
15+
final class ProductDocumentsIndexEvent extends Event
16+
{
17+
/** @var array<int|string, array> */
18+
private array $documents;
19+
20+
/**
21+
* @param iterable<ProductInterface> $products
22+
* @param array<int|string, array> $documents keyed by product id, mutable in place
23+
*/
24+
public function __construct(
25+
private iterable $products,
26+
array &$documents,
27+
) {
28+
$this->documents = &$documents;
29+
}
30+
31+
/**
32+
* @return iterable<ProductInterface>
33+
*/
34+
public function getProducts(): iterable
35+
{
36+
return $this->products;
37+
}
38+
39+
/**
40+
* @return array<int|string, array>
41+
*/
42+
public function getDocuments(): array
43+
{
44+
return $this->documents;
45+
}
46+
47+
public function setDocument(int|string $productId, array $document): void
48+
{
49+
$this->documents[$productId] = $document;
50+
}
51+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gally\SyliusPlugin\Event;
6+
7+
use Symfony\Contracts\EventDispatcher\Event;
8+
9+
/**
10+
* Dispatched right before building a Gally search Request, so a project can
11+
* override contextual values (e.g. the price group id) that the connector
12+
* has no reliable way to infer on its own.
13+
*/
14+
final class SearchRequestContextEvent extends Event
15+
{
16+
private ?string $priceGroupId = null;
17+
18+
public function getPriceGroupId(): ?string
19+
{
20+
return $this->priceGroupId;
21+
}
22+
23+
public function setPriceGroupId(?string $priceGroupId): void
24+
{
25+
$this->priceGroupId = $priceGroupId;
26+
}
27+
}

src/Grid/Gally/PagerfantaGallyAdapter.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Gally\Sdk\GraphQl\Request;
2121
use Gally\Sdk\Service\SearchManager;
2222
use Gally\SyliusPlugin\Event\GridFilterUpdateEvent;
23+
use Gally\SyliusPlugin\Event\SearchRequestContextEvent;
2324
use Gally\SyliusPlugin\Search\Aggregation\AggregationBuilder;
2425
use Gally\SyliusPlugin\Search\Result;
2526
use Pagerfanta\Adapter\AdapterInterface;
@@ -81,6 +82,9 @@ public function getSlice(int $offset, int $length): iterable
8182
/** @var int|string $page */
8283
$page = $this->parameters->get('page', 1);
8384

85+
$context = new SearchRequestContextEvent();
86+
$this->eventDispatcher->dispatch($context, 'gally.search.build_request');
87+
8488
$request = new Request(
8589
$this->currentLocalizedCatalog,
8690
new Metadata('product'),
@@ -92,7 +96,8 @@ public function getSlice(int $offset, int $length): iterable
9296
$search,
9397
$this->filters,
9498
(string) $sortField,
95-
$sortDirection
99+
$sortDirection,
100+
$context->getPriceGroupId()
96101
);
97102
$response = $this->searchManager->search($request);
98103
$productNumbers = [];

src/Grid/Gally/Search/SearchAdapter.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Gally\Sdk\GraphQl\Request;
2020
use Gally\Sdk\Service\SearchManager;
2121
use Gally\SyliusPlugin\Event\GridFilterUpdateEvent;
22+
use Gally\SyliusPlugin\Event\SearchRequestContextEvent;
2223
use Gally\SyliusPlugin\Grid\Gally\GallyAdapterInterface;
2324
use Gally\SyliusPlugin\Indexer\Provider\CatalogProvider;
2425
use Gally\SyliusPlugin\Search\Aggregation\AggregationBuilder;
@@ -73,6 +74,9 @@ public function getSlice(int $offset, int $length): iterable
7374
$sortDirection = $sorting[$sortField] ?? null;
7475
$page = (int) $page;
7576

77+
$context = new SearchRequestContextEvent();
78+
$this->eventDispatcher->dispatch($context, 'gally.search.build_request');
79+
7680
$request = new Request(
7781
$this->catalogProvider->getLocalizedCatalog(),
7882
new Metadata('product'),
@@ -84,7 +88,8 @@ public function getSlice(int $offset, int $length): iterable
8488
$search,
8589
$this->filters,
8690
(string) $sortField,
87-
$sortDirection
91+
$sortDirection,
92+
$context->getPriceGroupId()
8893
);
8994
$response = $this->searchManager->search($request);
9095

src/Indexer/ProductIndexer.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
namespace Gally\SyliusPlugin\Indexer;
1616

1717
use Gally\Sdk\Service\IndexOperation;
18+
use Gally\SyliusPlugin\Event\ProductDocumentsIndexEvent;
1819
use Gally\SyliusPlugin\Indexer\Provider\CatalogProvider;
1920
use Sylius\Component\Attribute\Model\AttributeValueInterface;
2021
use Sylius\Component\Core\Calculator\ProductVariantPricesCalculatorInterface;
@@ -26,6 +27,7 @@
2627
use Sylius\Component\Locale\Model\LocaleInterface;
2728
use Sylius\Component\Product\Model\ProductOptionValueInterface;
2829
use Sylius\Component\Resource\Repository\RepositoryInterface;
30+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
2931

3032
/**
3133
* Class ProductIndexer.
@@ -45,6 +47,7 @@ public function __construct(
4547
IndexOperation $indexOperation,
4648
private ProductRepositoryInterface $productRepository,
4749
private ProductVariantPricesCalculatorInterface $productVariantPriceCalculator,
50+
private EventDispatcherInterface $eventDispatcher,
4851
) {
4952
parent::__construct($channelRepository, $catalogProvider, $indexOperation);
5053
}
@@ -76,15 +79,25 @@ public function getDocumentsToIndex(
7679
);
7780
$products = $queryBuilder->getQuery()->execute();
7881
}
79-
/** @var iterable $products */
82+
$documents = [];
83+
/** @var iterable<ProductInterface> $products */
8084
foreach ($products as $product) {
8185
/** @var ProductInterface $product */
8286
if (!$product->isEnabled()) {
8387
continue;
8488
}
8589

86-
yield $this->formatProduct($product, $channel, $locale);
90+
/** @var int|string $productId */
91+
$productId = $product->getId();
92+
$documents[(string) $productId] = $this->formatProduct($product, $channel, $locale);
8793
}
94+
95+
$this->eventDispatcher->dispatch(
96+
new ProductDocumentsIndexEvent($products, $documents),
97+
'gally.indexer.product.documents'
98+
);
99+
100+
yield from $documents;
88101
}
89102

90103
private function formatProduct(ProductInterface $product, ChannelInterface $channel, LocaleInterface $locale): array
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
/**
3+
* DISCLAIMER
4+
*
5+
* Do not edit or add to this file if you wish to upgrade Gally to newer versions in the future.
6+
*
7+
* @package Gally
8+
* @author Stephan Hochdörfer <S.Hochdoerfer@bitexpert.de>, Gally Team <elasticsuite@smile.fr>
9+
* @copyright 2022-present Smile
10+
* @license Open Software License v. 3.0 (OSL-3.0)
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace Gally\SyliusPlugin\Indexer\Provider;
16+
17+
use Doctrine\Common\Collections\Collection;
18+
use Gally\Sdk\Entity\Label;
19+
use Gally\Sdk\Entity\LocalizedCatalog;
20+
use Gally\Sdk\Entity\SourceField;
21+
use Gally\Sdk\Entity\SourceFieldOption;
22+
use Sylius\Component\Product\Model\ProductOptionValueTranslation;
23+
24+
/**
25+
* Shared source field option building logic for the product attribute and product option providers.
26+
*/
27+
abstract class AbstractSourceFieldOptionProvider implements ProviderInterface
28+
{
29+
/** @var LocalizedCatalog[] */
30+
private array $localizedCatalogs = [];
31+
32+
public function __construct(protected CatalogProvider $catalogProvider)
33+
{
34+
foreach ($this->catalogProvider->provide() as $localizedCatalog) {
35+
$this->localizedCatalogs[] = $localizedCatalog;
36+
}
37+
}
38+
39+
public function getEntity(): string
40+
{
41+
return 'sourceFieldOption';
42+
}
43+
44+
/**
45+
* @param Collection<int, ProductOptionValueTranslation>|list<array<string, string>> $translations
46+
*/
47+
public function buildSourceFieldOption(
48+
SourceField $sourceField,
49+
string $code,
50+
string $defaultLabel,
51+
Collection|array $translations,
52+
int $position,
53+
): SourceFieldOption {
54+
/** @var Label[] $labels */
55+
$labels = $this->getLabels($translations, $defaultLabel);
56+
57+
return new SourceFieldOption(
58+
$sourceField,
59+
$code,
60+
$position,
61+
$defaultLabel,
62+
$labels,
63+
);
64+
}
65+
66+
/**
67+
* @param Collection<int, ProductOptionValueTranslation>|list<array<string, string>> $translations
68+
*/
69+
protected function getLabels(Collection|array $translations, string $defaultLabel): array
70+
{
71+
$labelsByLocal = [];
72+
foreach ($translations as $translation) {
73+
$locale = str_replace(
74+
'-',
75+
'_',
76+
$translation instanceof ProductOptionValueTranslation
77+
? (string) $translation->getLocale()
78+
: $translation['locale']
79+
);
80+
$labelsByLocal[$locale] = $translation instanceof ProductOptionValueTranslation
81+
? $translation->getValue()
82+
: $translation['translation'];
83+
}
84+
85+
$labels = [];
86+
foreach ($this->localizedCatalogs as $localizedCatalog) {
87+
$label = $labelsByLocal[$localizedCatalog->getLocale()] ?? null;
88+
if (null !== $label && '' !== $label && $label !== $defaultLabel) {
89+
$labels[] = new Label($localizedCatalog, $label);
90+
}
91+
}
92+
93+
return $labels;
94+
}
95+
}

0 commit comments

Comments
 (0)