From d494a9fc880727033a1b3ce36f6e0232322372d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Wed, 21 Aug 2024 21:53:55 +0200 Subject: [PATCH] Improve synonym functionality --- composer.json | 3 + .../SetonoSyliusMeilisearchExtension.php | 65 +++++++++++++++++-- .../Doctrine/SynonymListener.php | 25 ++++++- src/Factory/SynonymFactory.php | 5 +- .../EventSubscriber/NewSynonymSubscriber.php | 2 +- src/Form/Type/IndexChoiceType.php | 37 +++++++++++ src/Form/Type/SynonymType.php | 17 ++++- src/Grid/Filter/IndexFilter.php | 20 ++++++ src/Meilisearch/SynonymResolver.php | 7 +- src/Model/Synonym.php | 64 ++++++++++++++++-- src/Model/SynonymInterface.php | 18 +++-- src/Repository/SynonymRepository.php | 21 ++++-- src/Repository/SynonymRepositoryInterface.php | 3 +- .../config/doctrine/model/Synonym.orm.xml | 17 +++-- src/Resources/config/services.xml | 1 + .../config/services/event_listener.xml | 1 + .../config/services/event_subscriber.xml | 7 -- src/Resources/config/services/form.xml | 16 ++++- src/Resources/config/services/grid.xml | 9 +++ src/Resources/translations/messages.en.yaml | 6 +- .../views/admin/grid/field/_indexes.html.twig | 13 ++++ .../views/admin/grid/filter/indexes.html.twig | 3 + .../views/admin/synonym/_form.html.twig | 6 +- 23 files changed, 317 insertions(+), 49 deletions(-) rename src/{ => Form}/EventSubscriber/NewSynonymSubscriber.php (96%) create mode 100644 src/Form/Type/IndexChoiceType.php create mode 100644 src/Grid/Filter/IndexFilter.php create mode 100644 src/Resources/config/services/grid.xml create mode 100644 src/Resources/views/admin/grid/field/_indexes.html.twig create mode 100644 src/Resources/views/admin/grid/filter/indexes.html.twig diff --git a/composer.json b/composer.json index 3138471..aceabbe 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "require": { "php": ">=8.1", "ext-json": "*", + "doctrine/collections": "^1.8 || ^2.0", "doctrine/event-manager": "^1.2 || ^2.0", "doctrine/orm": "^2.14 || ^3.0", "doctrine/persistence": "^2.5 || ^3.0", @@ -30,6 +31,7 @@ "sylius/core": "^1.0", "sylius/core-bundle": "^1.0", "sylius/currency": "^1.0", + "sylius/grid-bundle": "^1.11", "sylius/locale": "^1.0", "sylius/locale-bundle": "^1.0", "sylius/product": "^1.0", @@ -42,6 +44,7 @@ "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", "symfony/form": "^5.4 || ^6.4 || ^7.0", "symfony/http-foundation": "^5.4 || ^6.4 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.4 || ^7.0", "symfony/messenger": "^5.4 || ^6.4 || ^7.0", "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", "symfony/routing": "^5.4 || ^6.4 || ^7.0", diff --git a/src/DependencyInjection/SetonoSyliusMeilisearchExtension.php b/src/DependencyInjection/SetonoSyliusMeilisearchExtension.php index 29a9d14..0e7bb91 100644 --- a/src/DependencyInjection/SetonoSyliusMeilisearchExtension.php +++ b/src/DependencyInjection/SetonoSyliusMeilisearchExtension.php @@ -90,6 +90,11 @@ public function prepend(ContainerBuilder $container): void ]); $container->prependExtensionConfig('sylius_grid', [ + 'templates' => [ + 'filter' => [ + 'indexes' => '@SetonoSyliusMeilisearchPlugin/admin/grid/filter/indexes.html.twig', + ], + ], 'grids' => [ 'setono_sylius_meilisearch_admin_synonym' => [ 'driver' => [ @@ -108,23 +113,69 @@ public function prepend(ContainerBuilder $container): void 'type' => 'string', 'label' => 'setono_sylius_meilisearch.ui.synonym', ], + 'enabled' => [ + 'type' => 'twig', + 'label' => 'sylius.ui.enabled', + 'options' => [ + 'template' => '@SyliusUi/Grid/Field/enabled.html.twig', + ], + ], + 'indexes' => [ + 'type' => 'twig', + 'label' => 'setono_sylius_meilisearch.ui.indexes', + 'path' => '.', + 'options' => [ + 'template' => '@SetonoSyliusMeilisearchPlugin/admin/grid/field/_indexes.html.twig', + ], + ], 'locale' => [ 'type' => 'string', 'label' => 'sylius.ui.locale', ], - 'channel' => [ - 'type' => 'string', - 'label' => 'sylius.ui.channel', + 'channels' => [ + 'type' => 'twig', + 'label' => 'sylius.ui.channels', + 'options' => [ + 'template' => '@SyliusAdmin/Grid/Field/_channels.html.twig', + ], ], ], 'filters' => [ 'search' => [ 'type' => 'string', - 'label' => 'sylius.ui.search', + 'label' => 'setono_sylius_meilisearch.ui.search_term_and_synonym', 'options' => [ 'fields' => ['term', 'synonym'], ], ], + 'enabled' => [ + 'type' => 'boolean', + 'label' => 'sylius.ui.enabled', + ], + 'indexes' => [ + 'type' => 'indexes', + 'label' => 'setono_sylius_meilisearch.ui.indexes', + 'form_options' => [ + 'placeholder' => 'sylius.ui.all', + ], + ], + 'locale' => [ + 'type' => 'entity', + 'label' => 'sylius.ui.locale', + 'form_options' => [ + 'class' => '%sylius.model.locale.class%', + ], + ], + 'channel' => [ + 'type' => 'entities', + 'label' => 'sylius.ui.channel', + 'form_options' => [ + 'class' => '%sylius.model.channel.class%', + ], + 'options' => [ + 'field' => 'channels.id', + ], + ], ], 'actions' => [ 'main' => [ @@ -140,6 +191,12 @@ public function prepend(ContainerBuilder $container): void 'type' => 'delete', ], ], + // todo in the future it might be a good user experience if we had bulk actions for adding indexes and channels to synonyms + 'bulk' => [ + 'delete' => [ + 'type' => 'delete', + ], + ], ], ], ], diff --git a/src/EventListener/Doctrine/SynonymListener.php b/src/EventListener/Doctrine/SynonymListener.php index 05ddf74..bd96dcc 100644 --- a/src/EventListener/Doctrine/SynonymListener.php +++ b/src/EventListener/Doctrine/SynonymListener.php @@ -7,10 +7,14 @@ use Doctrine\Persistence\Event\LifecycleEventArgs; use Setono\SyliusMeilisearchPlugin\Message\Command\UpdateSynonyms; use Setono\SyliusMeilisearchPlugin\Model\SynonymInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Messenger\MessageBusInterface; -final class SynonymListener +final class SynonymListener implements EventSubscriberInterface { + private bool $update = false; + public function __construct(private readonly MessageBusInterface $commandBus) { } @@ -30,12 +34,31 @@ public function postRemove(LifecycleEventArgs $eventArgs): void $this->handle($eventArgs); } + /** + * This method can be called multiple times in the same request, therefore we set a flag to only dispatch the command once + */ public function handle(LifecycleEventArgs $eventArgs): void { if (!$eventArgs->getObject() instanceof SynonymInterface) { return; } + $this->update = true; + } + + public function dispatch(): void + { + if (!$this->update) { + return; + } + $this->commandBus->dispatch(new UpdateSynonyms()); } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::RESPONSE => ['dispatch', 10], + ]; + } } diff --git a/src/Factory/SynonymFactory.php b/src/Factory/SynonymFactory.php index 8fefc01..82a529b 100644 --- a/src/Factory/SynonymFactory.php +++ b/src/Factory/SynonymFactory.php @@ -30,7 +30,10 @@ public function createInverseFromExisting(SynonymInterface $synonym): SynonymInt $obj->setTerm($synonym->getSynonym()); $obj->setSynonym($synonym->getTerm()); $obj->setLocale($synonym->getLocale()); - $obj->setChannel($synonym->getChannel()); + + foreach ($synonym->getChannels() as $channel) { + $obj->addChannel($channel); + } return $obj; } diff --git a/src/EventSubscriber/NewSynonymSubscriber.php b/src/Form/EventSubscriber/NewSynonymSubscriber.php similarity index 96% rename from src/EventSubscriber/NewSynonymSubscriber.php rename to src/Form/EventSubscriber/NewSynonymSubscriber.php index 7ca56f1..113ec3a 100644 --- a/src/EventSubscriber/NewSynonymSubscriber.php +++ b/src/Form/EventSubscriber/NewSynonymSubscriber.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\SyliusMeilisearchPlugin\EventSubscriber; +namespace Setono\SyliusMeilisearchPlugin\Form\EventSubscriber; use Doctrine\Persistence\ManagerRegistry; use Setono\Doctrine\ORMTrait; diff --git a/src/Form/Type/IndexChoiceType.php b/src/Form/Type/IndexChoiceType.php new file mode 100644 index 0000000..32f511b --- /dev/null +++ b/src/Form/Type/IndexChoiceType.php @@ -0,0 +1,37 @@ +indexRegistry->getNames(); + + $resolver->setDefaults([ + 'choices' => array_combine($names, $names), + 'choice_label' => static fn (string $name): string => ucfirst($name), + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } + + public function getBlockPrefix(): string + { + return 'setono_sylius_meilisearch_index_choice'; + } +} diff --git a/src/Form/Type/SynonymType.php b/src/Form/Type/SynonymType.php index 646fd70..08d28dc 100644 --- a/src/Form/Type/SynonymType.php +++ b/src/Form/Type/SynonymType.php @@ -9,6 +9,7 @@ use Sylius\Bundle\LocaleBundle\Form\Type\LocaleChoiceType; use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -42,8 +43,20 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('locale', LocaleChoiceType::class, [ 'label' => 'sylius.ui.locale', ]) - ->add('channel', ChannelChoiceType::class, [ - 'label' => 'sylius.ui.channel', + ->add('channels', ChannelChoiceType::class, [ + 'multiple' => true, + 'expanded' => true, + 'label' => 'sylius.ui.channels', + 'required' => false, + ]) + ->add('indexes', IndexChoiceType::class, [ + 'multiple' => true, + 'expanded' => true, + 'label' => 'setono_sylius_meilisearch.form.synonym.indexes', + 'required' => false, + ]) + ->add('enabled', CheckboxType::class, [ + 'label' => 'sylius.ui.enabled', 'required' => false, ]) ->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void { diff --git a/src/Grid/Filter/IndexFilter.php b/src/Grid/Filter/IndexFilter.php new file mode 100644 index 0000000..e746771 --- /dev/null +++ b/src/Grid/Filter/IndexFilter.php @@ -0,0 +1,20 @@ +restrict($dataSource->getExpressionBuilder()->like($name, '%"' . $data . '"%')); + } +} diff --git a/src/Meilisearch/SynonymResolver.php b/src/Meilisearch/SynonymResolver.php index 588215d..c0db4ab 100644 --- a/src/Meilisearch/SynonymResolver.php +++ b/src/Meilisearch/SynonymResolver.php @@ -16,16 +16,11 @@ public function __construct(private readonly SynonymRepositoryInterface $synonym public function resolve(IndexScope $indexScope): array { - // it doesn't make sense to resolve synonyms if the locale is not set - if (null === $indexScope->localeCode) { - return []; - } - /** @var list $synonyms */ $synonyms = array_map(static fn (SynonymInterface $synonym): array => [ 'term' => (string) $synonym->getTerm(), 'synonym' => (string) $synonym->getSynonym(), - ], $this->synonymRepository->findByLocaleAndChannel($indexScope->localeCode, $indexScope->channelCode)); + ], $this->synonymRepository->findEnabledByIndexScope($indexScope)); $resolvedSynonyms = []; foreach ($synonyms as $synonym) { diff --git a/src/Model/Synonym.php b/src/Model/Synonym.php index 290717b..d0f96f5 100644 --- a/src/Model/Synonym.php +++ b/src/Model/Synonym.php @@ -4,12 +4,17 @@ namespace Setono\SyliusMeilisearchPlugin\Model; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Sylius\Component\Channel\Model\ChannelInterface; use Sylius\Component\Locale\Model\LocaleInterface; +use Sylius\Component\Resource\Model\ToggleableTrait; use function Symfony\Component\String\u; class Synonym implements SynonymInterface { + use ToggleableTrait; + protected ?int $id = null; protected ?string $term = null; @@ -18,7 +23,16 @@ class Synonym implements SynonymInterface protected ?LocaleInterface $locale = null; - protected ?ChannelInterface $channel = null; + /** @var Collection */ + protected Collection $channels; + + /** @var list|null */ + protected ?array $indexes = null; + + public function __construct() + { + $this->channels = new ArrayCollection(); + } public function getId(): ?int { @@ -55,13 +69,53 @@ public function setLocale(?LocaleInterface $locale): void $this->locale = $locale; } - public function getChannel(): ?ChannelInterface + public function getChannels(): Collection + { + return $this->channels; + } + + public function addChannel(ChannelInterface $channel): void + { + if (!$this->hasChannel($channel)) { + $this->channels->add($channel); + } + } + + public function removeChannel(ChannelInterface $channel): void + { + if ($this->hasChannel($channel)) { + $this->channels->removeElement($channel); + } + } + + public function hasChannel(ChannelInterface $channel): bool + { + return $this->channels->contains($channel); + } + + public function getIndexes(): array + { + return $this->indexes ?? []; + } + + public function addIndex(string $index): void + { + $this->indexes[] = $index; + } + + public function removeIndex(string $index): void { - return $this->channel; + $indexes = $this->getIndexes(); + $key = array_search($index, $indexes, true); + if ($key !== false) { + unset($indexes[$key]); + } + + $this->indexes = [] === $indexes ? null : array_values($indexes); } - public function setChannel(?ChannelInterface $channel): void + public function hasIndex(string $index): bool { - $this->channel = $channel; + return in_array($index, $this->getIndexes(), true); } } diff --git a/src/Model/SynonymInterface.php b/src/Model/SynonymInterface.php index 7135d88..c3fa79c 100644 --- a/src/Model/SynonymInterface.php +++ b/src/Model/SynonymInterface.php @@ -4,12 +4,12 @@ namespace Setono\SyliusMeilisearchPlugin\Model; -use Sylius\Component\Channel\Model\ChannelAwareInterface; -use Sylius\Component\Channel\Model\ChannelInterface; +use Sylius\Component\Channel\Model\ChannelsAwareInterface; use Sylius\Component\Locale\Model\LocaleInterface; use Sylius\Component\Resource\Model\ResourceInterface; +use Sylius\Component\Resource\Model\ToggleableInterface; -interface SynonymInterface extends ResourceInterface, ChannelAwareInterface +interface SynonymInterface extends ResourceInterface, ChannelsAwareInterface, ToggleableInterface { public function getId(): ?int; @@ -25,8 +25,14 @@ public function getLocale(): ?LocaleInterface; public function setLocale(?LocaleInterface $locale): void; - // todo can we think of a scenario where we _really_ need the channel? - public function getChannel(): ?ChannelInterface; + /** + * @return list + */ + public function getIndexes(): array; - public function setChannel(?ChannelInterface $channel): void; + public function addIndex(string $index): void; + + public function removeIndex(string $index): void; + + public function hasIndex(string $index): bool; } diff --git a/src/Repository/SynonymRepository.php b/src/Repository/SynonymRepository.php index d3378a3..a389265 100644 --- a/src/Repository/SynonymRepository.php +++ b/src/Repository/SynonymRepository.php @@ -5,22 +5,29 @@ namespace Setono\SyliusMeilisearchPlugin\Repository; use Setono\SyliusMeilisearchPlugin\Model\SynonymInterface; +use Setono\SyliusMeilisearchPlugin\Provider\IndexScope\IndexScope; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Webmozart\Assert\Assert; class SynonymRepository extends EntityRepository implements SynonymRepositoryInterface { - public function findByLocaleAndChannel(string $localeCode, string $channelCode = null): array + public function findEnabledByIndexScope(IndexScope $indexScope): array { $qb = $this->createQueryBuilder('o') - ->join('o.locale', 'locale', 'WITH', 'locale.code = :localeCode') - ->setParameter('localeCode', $localeCode) + ->andWhere('o.enabled = true') + ->andWhere('o.indexes LIKE :index') + ->setParameter('index', '%"' . $indexScope->index->name . '"%') ; - if (null !== $channelCode) { - $qb->leftJoin('o.channel', 'c') - ->andWhere('o.channel IS NULL OR c.code = :channelCode') - ->setParameter('channelCode', $channelCode) + if (null !== $indexScope->localeCode) { + $qb->join('o.locale', 'locale', 'WITH', 'locale.code = :localeCode') + ->setParameter('localeCode', $indexScope->localeCode) + ; + } + + if (null !== $indexScope->channelCode) { + $qb->join('o.channels', 'c', 'WITH', 'c.code = :channelCode') + ->setParameter('channelCode', $indexScope->channelCode) ; } diff --git a/src/Repository/SynonymRepositoryInterface.php b/src/Repository/SynonymRepositoryInterface.php index d9b7c31..112ad4b 100644 --- a/src/Repository/SynonymRepositoryInterface.php +++ b/src/Repository/SynonymRepositoryInterface.php @@ -5,6 +5,7 @@ namespace Setono\SyliusMeilisearchPlugin\Repository; use Setono\SyliusMeilisearchPlugin\Model\SynonymInterface; +use Setono\SyliusMeilisearchPlugin\Provider\IndexScope\IndexScope; use Sylius\Component\Resource\Repository\RepositoryInterface; /** @@ -15,5 +16,5 @@ interface SynonymRepositoryInterface extends RepositoryInterface /** * @return array */ - public function findByLocaleAndChannel(string $localeCode, string $channelCode = null): array; + public function findEnabledByIndexScope(IndexScope $indexScope): array; } diff --git a/src/Resources/config/doctrine/model/Synonym.orm.xml b/src/Resources/config/doctrine/model/Synonym.orm.xml index 1173cff..8b1b7d2 100644 --- a/src/Resources/config/doctrine/model/Synonym.orm.xml +++ b/src/Resources/config/doctrine/model/Synonym.orm.xml @@ -13,13 +13,22 @@ - - - - + + + + + + + + + + + + + diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 92cd05c..8c4f1a4 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -13,6 +13,7 @@ + diff --git a/src/Resources/config/services/event_listener.xml b/src/Resources/config/services/event_listener.xml index f3c3c6b..d387e29 100644 --- a/src/Resources/config/services/event_listener.xml +++ b/src/Resources/config/services/event_listener.xml @@ -17,6 +17,7 @@ + diff --git a/src/Resources/config/services/event_subscriber.xml b/src/Resources/config/services/event_subscriber.xml index b58d849..d5816e1 100644 --- a/src/Resources/config/services/event_subscriber.xml +++ b/src/Resources/config/services/event_subscriber.xml @@ -5,12 +5,5 @@ - - - - - - - diff --git a/src/Resources/config/services/form.xml b/src/Resources/config/services/form.xml index 67a350e..a572aa4 100644 --- a/src/Resources/config/services/form.xml +++ b/src/Resources/config/services/form.xml @@ -33,6 +33,12 @@ + + + + + + @@ -40,11 +46,19 @@ - + %setono_sylius_meilisearch.model.synonym.class% %setono_sylius_meilisearch.form.type.synonym.validation_groups% + + + + + + + + diff --git a/src/Resources/config/services/grid.xml b/src/Resources/config/services/grid.xml new file mode 100644 index 0000000..5c276a4 --- /dev/null +++ b/src/Resources/config/services/grid.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/src/Resources/translations/messages.en.yaml b/src/Resources/translations/messages.en.yaml index a559b3a..fe421a1 100644 --- a/src/Resources/translations/messages.en.yaml +++ b/src/Resources/translations/messages.en.yaml @@ -16,6 +16,7 @@ setono_sylius_meilisearch: label: Direction one_way: One way two_way: Two way + indexes: Indexes menu: admin: main: @@ -23,9 +24,11 @@ setono_sylius_meilisearch: header: Meilisearch synonyms: Synonyms ui: + indexes: Indexes + no_indexes: No indexes search: Search search_results_for: 'Search results for' - synonyms: Synonyms + search_term_and_synonym: 'Search term and synonym' synonym: Synonym synonym_header: Synonyms synonym_subheader: Manage synonyms @@ -33,4 +36,5 @@ setono_sylius_meilisearch:

Understanding synonyms

Synonyms are words or phrases that have the same or similar meaning. For example, if you add the synonym "jacket" to the term "coat", then when a user searches for "coat", the search results will include results matching "jacket".

If the direction is "one way", then the synonym will only work in one direction. For example, if the term is "coat" and the synonym is "jacket", then when a user searches for "coat", the search results will include results matching "jacket". However, if the direction is "two way", then the synonym will work in both directions. For example when a user searches for "jacket", the search results will include results matching "coat", and when a user searches for "coat", the search results will include results matching "jacket".

+ synonyms: Synonyms term: Term diff --git a/src/Resources/views/admin/grid/field/_indexes.html.twig b/src/Resources/views/admin/grid/field/_indexes.html.twig new file mode 100644 index 0000000..09c48f2 --- /dev/null +++ b/src/Resources/views/admin/grid/field/_indexes.html.twig @@ -0,0 +1,13 @@ +{# @var data \Setono\SyliusMeilisearchPlugin\Model\SynonymInterface #} +{% if data.indexes is empty %} + + + {{ 'setono_sylius_meilisearch.ui.no_indexes'|trans }} + +{% else %} +
    + {% for index in data.indexes %} +
  • {{ index|title }}
  • + {% endfor %} +
+{% endif %} diff --git a/src/Resources/views/admin/grid/filter/indexes.html.twig b/src/Resources/views/admin/grid/filter/indexes.html.twig new file mode 100644 index 0000000..81480fc --- /dev/null +++ b/src/Resources/views/admin/grid/filter/indexes.html.twig @@ -0,0 +1,3 @@ +{% form_theme form '@SyliusUi/Form/theme.html.twig' %} + +{{ form_row(form) }} diff --git a/src/Resources/views/admin/synonym/_form.html.twig b/src/Resources/views/admin/synonym/_form.html.twig index 99ed52d..e8a2f3e 100644 --- a/src/Resources/views/admin/synonym/_form.html.twig +++ b/src/Resources/views/admin/synonym/_form.html.twig @@ -11,9 +11,11 @@ {{ form_row(form.synonym) }} -
+
{{ form_row(form.locale) }} - {{ form_row(form.channel) }} + {{ form_row(form.channels) }} + {{ form_row(form.indexes) }} + {{ form_row(form.enabled) }}
{{ 'setono_sylius_meilisearch.ui.synonym_help'|trans|raw }}