diff --git a/Makefile b/Makefile
index 2c36f65e..9b5054ca 100644
--- a/Makefile
+++ b/Makefile
@@ -114,10 +114,10 @@ test.composer: ## Validate composer.json
${COMPOSER} validate --strict
test.phpstan: ## Run PHPStan
- ${COMPOSER} phpstan || true
+ ${COMPOSER} phpstan
test.phpmd: ## Run PHPMD
- ${COMPOSER} phpmd || true
+ ${COMPOSER} phpmd
test.phpunit: ## Run PHPUnit
${COMPOSER} phpunit
diff --git a/README.md b/README.md
index 38012435..b8b0e340 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,10 @@ use Sylius\Component\Product\Model\ProductAttribute as BaseProductAttribute;
4. Run the populate command.
+## Documentation
+
+[Documentation is available in the *docs* folder.](docs/index.md)
+
## Infrastructure
The plugin was developed for Elasticsearch 7.16.x versions. You need to have analysis-icu and analysis-phonetic elasticsearch plugin installed.
diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md
index 8b4b0ba3..a55e3154 100644
--- a/UPGRADE-2.0.md
+++ b/UPGRADE-2.0.md
@@ -6,10 +6,11 @@
- Remove `MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\ProductTaxonRegistry` service to use an iterator of services tagged `monsieurbiz.search.request.product_post_filter`
- Remove `MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\ProductSorterRegistry` service to use an iterator of services tagged `monsieurbiz.search.request.product_sorter`
- Remove `\MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore\ProductFunctionScoreRegistry` service to use an iterator of services tagged `monsieurbiz.search.request.product_function_score`
-- The `MonsieurBiz\SyliusSearchPlugin\Mapping\YamlWithLocaleProvider` is no longer a decorator. Some constructor parameters are removed : `$decorated`, `$configurationDirectory` and `$attributeRepository`, and we have `$yamlProviderFactory`, `$fileLocator` and `$configurationDirectories`.
+- The `MonsieurBiz\SyliusSearchPlugin\Mapping\YamlWithLocaleProvider` is no longer a decorator. Some constructor parameters are removed : `$decorated`, `$configurationDirectory` and `$attributeRepository`, and we add `$fileLocator` and `$configurationDirectories`.
- New setting `monsieurbiz_sylius_search.elastically_configuration_paths` to define paths of elasticsearch mapping files. By default it's `['@MonsieurBizSyliusSearchPlugin/Resources/config/elasticsearch']`.
- New method `deleteByDocumentIds` in the `MonsieurBiz\SyliusSearchPlugin\Index\IndexerInterface` interface
- Deprecated the method `deleteByDocuments` in the `MonsieurBiz\SyliusSearchPlugin\Index\IndexerInterface` interface. Use `deleteByDocumentIds` instead.
+- `ChannelFilter` and `EnabledFilter` in `MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product` were moved to `MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter`
# UPGRADE FROM v1.X.X TO v2.0.x
diff --git a/dist/src/Resources/config/config.yaml b/dist/src/Resources/config/config.yaml
index c430b5e3..650833cb 100644
--- a/dist/src/Resources/config/config.yaml
+++ b/dist/src/Resources/config/config.yaml
@@ -3,3 +3,4 @@ parameters:
imports:
- { resource: "services.yaml" }
+ - { resource: "search/taxons.yaml" }
diff --git a/dist/src/Resources/config/elasticsearch/app_taxon_mapping.yaml b/dist/src/Resources/config/elasticsearch/app_taxon_mapping.yaml
new file mode 100644
index 00000000..a506015c
--- /dev/null
+++ b/dist/src/Resources/config/elasticsearch/app_taxon_mapping.yaml
@@ -0,0 +1,51 @@
+mappings:
+ dynamic: false
+ properties:
+ code:
+ type: keyword
+ enabled:
+ type: boolean
+ name:
+ type: text
+ fields:
+ keyword:
+ type: keyword
+ autocomplete:
+ type: text
+ analyzer: search_autocomplete
+ search_analyzer: standard
+ description:
+ type: text
+ created_at:
+ type: date
+ format: yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_second
+ position:
+ type: integer
+ level:
+ type: integer
+ left:
+ type: integer
+ right:
+ type: integer
+ parent_taxon:
+ type: nested
+ properties:
+ code:
+ type: keyword
+ enabled:
+ type: boolean
+ name:
+ type: keyword
+ description:
+ type: text
+ created_at:
+ type: date
+ format: yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_second
+ position:
+ type: integer
+ level:
+ type: integer
+ left:
+ type: integer
+ right:
+ type: integer
diff --git a/dist/src/Resources/config/elasticsearch/monsieurbiz_product_mapping.yaml b/dist/src/Resources/config/elasticsearch/monsieurbiz_product_mapping.yaml
index a47dc704..e3a1e806 100644
--- a/dist/src/Resources/config/elasticsearch/monsieurbiz_product_mapping.yaml
+++ b/dist/src/Resources/config/elasticsearch/monsieurbiz_product_mapping.yaml
@@ -5,3 +5,7 @@ mappings:
fields:
keyword: # add keyword field for sorting
type: keyword
+settings:
+ mapping:
+ nested_fields:
+ limit: 1000
diff --git a/dist/src/Resources/config/search/taxons.yaml b/dist/src/Resources/config/search/taxons.yaml
new file mode 100644
index 00000000..d4e44060
--- /dev/null
+++ b/dist/src/Resources/config/search/taxons.yaml
@@ -0,0 +1,23 @@
+monsieurbiz_sylius_search:
+ documents:
+ app_taxon:
+ #prefix: '…' # define a custom index prefix on index names and aliases
+ #document_class: '…' # by default MonsieurBiz\SyliusSearchPlugin\Model\Documentable\Documentable
+ instant_search_enabled: true # by default false
+ limits:
+ search: [9, 18, 27]
+ taxon: [9, 18, 27]
+ instant_search: [5]
+ source: 'Sylius\Component\Core\Model\TaxonInterface'
+ target: 'App\Search\Model\Taxon\TaxonDTO'
+ templates:
+ item: '@MonsieurBizSyliusSearchPlugin/Search/Taxon/_box.html.twig'
+ instant: '@MonsieurBizSyliusSearchPlugin/Instant/Taxon/_box.html.twig'
+ #mapping_provider: '...' # by default monsieurbiz.search.mapper_provider
+ datasource: 'App\Search\Model\Datasource\TaxonDatasource' # by default MonsieurBiz\SyliusSearchPlugin\Model\Datasource\RepositoryDatasource
+ position: -1
+ automapper_classes:
+ sources:
+ taxon: '%sylius.model.taxon.class%'
+ targets:
+ app_taxon: 'App\Search\Model\Taxon\TaxonDTO'
diff --git a/dist/src/Resources/config/services.yaml b/dist/src/Resources/config/services.yaml
index 1d621a2f..4a3d4be5 100644
--- a/dist/src/Resources/config/services.yaml
+++ b/dist/src/Resources/config/services.yaml
@@ -3,6 +3,8 @@ services:
autowire: true
autoconfigure: true
public: false
+ bind:
+ $documentableRegistry: '@monsieurbiz.search.registry.documentable'
# Makes classes in src/ available to be used as services;
# this creates a service per class whose id is the fully-qualified class name
@@ -53,3 +55,40 @@ services:
App\Search\Request\FunctionScore\Product\BoostExpensiveProductFunction:
tags:
- { name: monsieurbiz.search.request.product_function_score }
+
+ # Define the taxon requests
+ app.search.request.taxon_instant_search:
+ class: MonsieurBiz\SyliusSearchPlugin\Search\Request\InstantSearch
+ arguments:
+ $documentType: app_taxon
+ $queryFilters: !tagged_iterator { tag: 'app.search.request.taxon_instant_search_filter' }
+ $functionScores: !tagged_iterator { tag: 'app.search.request.taxon_function_score' }
+
+ app.search.request.taxon_search:
+ class: MonsieurBiz\SyliusSearchPlugin\Search\Request\Search
+ arguments:
+ $documentType: app_taxon
+ $queryFilters: !tagged_iterator { tag: 'app.search.request.taxon_search_filter' }
+ $postFilters: !tagged_iterator { tag: 'app.search.request.taxon_post_filter' }
+ $sorters: !tagged_iterator { tag: 'app.search.request.taxon_sorter' }
+ $functionScores: !tagged_iterator { tag: 'app.search.request.taxon_function_score' }
+
+ # Define the taxon query filters
+ app.search.request.query_filter.taxon_instant_search.search_term_filter:
+ class: MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\SearchTermFilter
+ arguments:
+ $fieldsToSearch:
+ - 'name^5'
+ - 'description'
+ - 'name.autocomplete'
+ tags:
+ - { name: app.search.request.taxon_instant_search_filter }
+
+ app.search.request.query_filter.taxon_search.search_term_filter:
+ class: MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\SearchTermFilter
+ arguments:
+ $fieldsToSearch:
+ - 'name^5'
+ - 'description'
+ tags:
+ - { name: app.search.request.taxon_search_filter }
diff --git a/dist/src/Search/Automapper/TaxonMapperConfiguration.php b/dist/src/Search/Automapper/TaxonMapperConfiguration.php
new file mode 100644
index 00000000..a9647679
--- /dev/null
+++ b/dist/src/Search/Automapper/TaxonMapperConfiguration.php
@@ -0,0 +1,126 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace App\Search\Automapper;
+
+use App\Search\Model\Taxon\TaxonDTO;
+use DateTimeInterface;
+use Doctrine\Common\Proxy\Proxy;
+use Doctrine\Common\Util\ClassUtils;
+use Doctrine\ORM\EntityManagerInterface;
+use Jane\Bundle\AutoMapperBundle\Configuration\MapperConfigurationInterface;
+use Jane\Component\AutoMapper\AutoMapperInterface;
+use Jane\Component\AutoMapper\MapperGeneratorMetadataInterface;
+use Jane\Component\AutoMapper\MapperMetadata;
+use MonsieurBiz\SyliusSearchPlugin\AutoMapper\ConfigurationInterface;
+use Sylius\Component\Core\Model\TaxonInterface;
+
+final class TaxonMapperConfiguration implements MapperConfigurationInterface
+{
+ private ConfigurationInterface $configuration;
+
+ private AutoMapperInterface $autoMapper;
+
+ public function __construct(
+ ConfigurationInterface $configuration,
+ AutoMapperInterface $autoMapper,
+ private EntityManagerInterface $entityManager
+ ) {
+ $this->configuration = $configuration;
+ $this->autoMapper = $autoMapper;
+ }
+
+ public function process(MapperGeneratorMetadataInterface $metadata): void
+ {
+ if (!$metadata instanceof MapperMetadata) {
+ return;
+ }
+
+ $metadata->forMember('id', function (TaxonInterface $taxon): int {
+ return $taxon->getId();
+ });
+
+ $metadata->forMember('code', function (TaxonInterface $taxon): ?string {
+ return $taxon->getCode();
+ });
+
+ $metadata->forMember('enabled', function (TaxonInterface $taxon): bool {
+ return $taxon->isEnabled();
+ });
+
+ $metadata->forMember('slug', function (TaxonInterface $taxon): ?string {
+ return $taxon->getSlug();
+ });
+
+ $metadata->forMember('name', function (TaxonInterface $taxon): ?string {
+ return $taxon->getName();
+ });
+
+ $metadata->forMember('description', function (TaxonInterface $taxon): ?string {
+ return $taxon->getDescription();
+ });
+
+ $metadata->forMember('created_at', function (TaxonInterface $taxon): ?DateTimeInterface {
+ return $taxon->getCreatedAt();
+ });
+
+ $metadata->forMember('position', function (TaxonInterface $taxon): ?int {
+ return $taxon->getPosition();
+ });
+
+ $metadata->forMember('level', function (TaxonInterface $taxon): ?int {
+ return $taxon->getLevel();
+ });
+
+ $metadata->forMember('left', function (TaxonInterface $taxon): ?int {
+ return $taxon->getLeft();
+ });
+
+ $metadata->forMember('right', function (TaxonInterface $taxon): ?int {
+ return $taxon->getRight();
+ });
+
+ /** @phpstan-ignore-next-line */
+ $metadata->forMember('parent_taxon', function (TaxonInterface $taxon): ?TaxonDTO {
+ return ($parent = $taxon->getParent()) ? $this->autoMapper->map(
+ $this->getRealTaxonEntity($parent),
+ $this->configuration->getTargetClass('app_taxon')
+ ) : null;
+ });
+ }
+
+ public function getSource(): string
+ {
+ return $this->configuration->getSourceClass('taxon');
+ }
+
+ public function getTarget(): string
+ {
+ return $this->configuration->getTargetClass('app_taxon');
+ }
+
+ private function getRealTaxonEntity(TaxonInterface $taxon): TaxonInterface
+ {
+ if ($taxon instanceof Proxy) {
+ // Clear the entity manager to detach the proxy object
+ $this->entityManager->clear(\get_class($taxon));
+ // Retrieve the original class name
+ $entityClassName = ClassUtils::getRealClass(\get_class($taxon));
+ // Find the object in repository from the ID
+ /** @var ?TaxonInterface $taxon */
+ $taxon = $this->entityManager->find($entityClassName, $taxon->getId());
+ }
+
+ return $taxon;
+ }
+}
diff --git a/dist/src/Search/Model/Datasource/TaxonDatasource.php b/dist/src/Search/Model/Datasource/TaxonDatasource.php
new file mode 100644
index 00000000..ca4c0ada
--- /dev/null
+++ b/dist/src/Search/Model/Datasource/TaxonDatasource.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace App\Search\Model\Datasource;
+
+use Doctrine\ORM\EntityManagerInterface;
+use MonsieurBiz\SyliusSearchPlugin\Model\Datasource\DatasourceInterface;
+use Pagerfanta\Doctrine\ORM\QueryAdapter;
+use Pagerfanta\Pagerfanta;
+use Sylius\Component\Taxonomy\Repository\TaxonRepositoryInterface;
+use Webmozart\Assert\Assert;
+
+class TaxonDatasource implements DatasourceInterface
+{
+ private EntityManagerInterface $entityManager;
+
+ public function __construct(EntityManagerInterface $entityManager)
+ {
+ $this->entityManager = $entityManager;
+ }
+
+ public function getItems(string $sourceClass): iterable
+ {
+ $repository = $this->entityManager->getRepository($sourceClass);
+ /** @var TaxonRepositoryInterface $repository */
+ Assert::isInstanceOf($repository, TaxonRepositoryInterface::class);
+
+ $queryBuilder = $repository->createQueryBuilder('o')
+ ->andWhere('o.enabled = :enabled')
+ ->setParameter('enabled', true)
+ ;
+
+ $paginator = new Pagerfanta(new QueryAdapter($queryBuilder, false, false));
+ $paginator->setMaxPerPage(self::DEFAULT_MAX_PER_PAGE);
+ $page = 1;
+ do {
+ $paginator->setCurrentPage($page);
+
+ foreach ($paginator->getIterator() as $item) {
+ yield $item;
+ }
+ $page = $paginator->hasNextPage() ? $paginator->getNextPage() : 1;
+ } while ($paginator->hasNextPage());
+
+ return null;
+ }
+}
diff --git a/dist/src/Search/Model/Taxon/TaxonDTO.php b/dist/src/Search/Model/Taxon/TaxonDTO.php
new file mode 100644
index 00000000..d1d8fc7d
--- /dev/null
+++ b/dist/src/Search/Model/Taxon/TaxonDTO.php
@@ -0,0 +1,20 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace App\Search\Model\Taxon;
+
+use Jacquesbh\Eater\Eater;
+
+class TaxonDTO extends Eater
+{
+}
diff --git a/dist/templates/bundles/MonsieurBizSyliusSearchPlugin/Instant/Taxon/_box.html.twig b/dist/templates/bundles/MonsieurBizSyliusSearchPlugin/Instant/Taxon/_box.html.twig
new file mode 100644
index 00000000..7f28ac1e
--- /dev/null
+++ b/dist/templates/bundles/MonsieurBizSyliusSearchPlugin/Instant/Taxon/_box.html.twig
@@ -0,0 +1,7 @@
+{% import "@SyliusShop/Common/Macro/money.html.twig" as money %}
+
+
diff --git a/dist/templates/bundles/MonsieurBizSyliusSearchPlugin/Search/product/_box.html.twig b/dist/templates/bundles/MonsieurBizSyliusSearchPlugin/Search/Product/_box.html.twig
similarity index 100%
rename from dist/templates/bundles/MonsieurBizSyliusSearchPlugin/Search/product/_box.html.twig
rename to dist/templates/bundles/MonsieurBizSyliusSearchPlugin/Search/Product/_box.html.twig
diff --git a/dist/templates/bundles/MonsieurBizSyliusSearchPlugin/Search/Taxon/_box.html.twig b/dist/templates/bundles/MonsieurBizSyliusSearchPlugin/Search/Taxon/_box.html.twig
new file mode 100644
index 00000000..77478d3a
--- /dev/null
+++ b/dist/templates/bundles/MonsieurBizSyliusSearchPlugin/Search/Taxon/_box.html.twig
@@ -0,0 +1,7 @@
+{% import "@SyliusShop/Common/Macro/money.html.twig" as money %}
+
+
diff --git a/dist/translations/messages.en.yaml b/dist/translations/messages.en.yaml
index 7cfd41ab..da2ca1d7 100644
--- a/dist/translations/messages.en.yaml
+++ b/dist/translations/messages.en.yaml
@@ -2,3 +2,10 @@ app:
ui:
short_desc_a_to_z_path: From short description A to Z
short_desc_z_to_a_path: From short description Z to A
+monsieurbiz_searchplugin:
+ instant:
+ result:
+ app_taxon: Taxons
+ search:
+ result:
+ app_taxon: Taxons
diff --git a/docs/add_custom_boosts.md b/docs/add_custom_boosts.md
index b9b4f6e0..28d139a6 100644
--- a/docs/add_custom_boosts.md
+++ b/docs/add_custom_boosts.md
@@ -21,6 +21,6 @@ The boost is enabled when the value of `monsieurbiz.search.product.is_in_stock_s
To create a new boost, you must
- [Create a new class that implements `MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore\FunctionScoreInterface`](../dist/src/Search/Request/FunctionScore/Product/BoostExpensiveProductFunction.php)
-- [Tag it with `monsieurbiz.search.request.product_function_score`](../dist/src/Resources/config/services.yaml#L53)
+- [Tag it with `monsieurbiz.search.request.product_function_score`](../dist/src/Resources/config/services.yaml#L54)
In our example we will boost, in the search, the product with a price greater than 50.
diff --git a/docs/add_custom_entities.md b/docs/add_custom_entities.md
new file mode 100644
index 00000000..f6c79fbc
--- /dev/null
+++ b/docs/add_custom_entities.md
@@ -0,0 +1,98 @@
+# Add custom entities
+
+In our example, we will add taxons to the search results.
+
+In the instant search :
+
+![Taxons displayed in the instant search results](img/taxon-instant.jpg)
+
+In the search results, tabs will be displayed if you have many type of documents :
+
+![Tabs displayed in the search results](img/taxon-search.jpg)
+
+By clicking on the tab you will switch to the results of the selected type of document :
+
+![Taxons displayed in the search results](img/taxon-search-2.jpg)
+
+## Index your new entity in Elasticsearch
+
+### Add your new entity as a type of document
+
+[Declare your entity as a type of document for search](../dist/src/Resources/config/search/taxons.yaml).
+
+- Use `instant_search_enabled` config to define if your entity should be displayed in the instant search.
+- Use `position` config to change the order of the entity compared to each others.
+- The `source` config is used to define the source of the data.
+- The `target` config is used to define the target of the data, you can put a different sources in the same target for example if you want to mix some objects in the same page.
+- The `templates` config node will define the templates used for the display of your document in the instant search and in the search page.
+- The `datasource` allows you to change the way you retrieve the list of the documents to be indexed. In our example with taxons, we want only enabled taxons.
+
+In the node `automapper_classes`, you have to define the source and the target classes of the data.
+For our example, the source is the Sylius' taxon model, and the target in a custom DTO.
+We will create the custom DTO later in the documentation.
+
+### Declare the elastic search mapping
+
+[Declare the mapping of your entity for Elasticsearch](../dist/src/Resources/config/elasticsearch/app_taxon_mapping.yaml).
+
+### Create the Datasource class if you defined a custom one
+
+By default the [`MonsieurBiz\SyliusSearchPlugin\Model\Datasource\RepositoryDatasource`](/src/Model/Datasource/RepositoryDatasource.php) will be used and retrieve all results.
+In our example we will retrieve all enabled taxons.
+
+[Create the TaxonDatasource class to retrieve enabled taxons](../dist/src/Search/Model/Datasource/TaxonDatasource.php).
+
+### Create the Taxon DTO
+
+This is the class used in `targets` of your `automapper_classes` configuration.
+
+[Create the Taxon DTO](../dist/src/Search/Model/Taxon/TaxonDTO.php).
+
+In our example, we use an Eater class which will allow us the `get` and `set` any value we want.
+You can use a custom DTO with custom methods if you want.
+
+### Define a MapperConfiguration (optional)
+
+We have to define how to populate the data from the model to the DTO object because we use a dynamic DTO which used Eater class.
+You can use another automapper if you want and avoid this part.
+
+[Create the TaxonMapperConfiguration](../dist/src/Search/Automapper/TaxonMapperConfiguration.php).
+
+Be careful, the `public function getSource(): string` method must return the value of one of the `sources` defined in the `automapper_classes` configuration.
+Also, the `public function getTarget(): string` method must return the value of one of the `targets` defined in the `automapper_classes` configuration.
+
+## Display your new entity in the search results
+
+## Define your Instant Search request
+
+If you want to display your entity in the instant search (`instant_search_enabled` is `true` in configuration).
+
+[Declare your instant search request service](../dist/src/Resources/config/services.yaml#L60).
+
+[Don't forget to bind the parameter for the service](../dist/src/Resources/config/services.yaml#L6).
+
+## Define your Search request
+
+[Declare your search request service](../dist/src/Resources/config/services.yaml#L67).
+
+[Don't forget to bind the parameter for the service](../dist/src/Resources/config/services.yaml#L6).
+
+You can extends the `MonsieurBiz\SyliusSearchPlugin\Search\Request\Search` class to manage your aggregations like in [products](../src/Search/Request/ProductRequest/Search.php).
+
+## Define your Search query filter
+
+[Declare your search query filter for instant search](../dist/src/Resources/config/services.yaml#L77).
+
+[Declare your search query filter for search](../dist/src/Resources/config/services.yaml#L87).
+
+You can extends the `MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\SearchTermFilter` class to manage your custom behaviour like in [products](../src/Search/Request/QueryFilter/Product/SearchTermFilter.php).
+
+## Add the templates for display
+
+[Declare your templates for instant search](../dist/templates/bundles/MonsieurBizSyliusSearchPlugin/Instant/Taxon/_box.html.twig).
+
+[Declare your templates for search](../dist/templates/bundles/MonsieurBizSyliusSearchPlugin/Search/Taxon/_box.html.twig).
+
+## Add your document translation
+
+[Declare your translations for your entity](../dist/translations/messages.en.yaml#L5).
diff --git a/docs/add_custom_filters.md b/docs/add_custom_filters.md
new file mode 100644
index 00000000..edd62725
--- /dev/null
+++ b/docs/add_custom_filters.md
@@ -0,0 +1,34 @@
+# Add custom filters in products requests
+
+## Create your own filter
+
+1. You can create your own filter service by implementing the `\MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterInterface` interface.
+
+```php
+ 'onMappingProvider',
- ];
- }
-
- public function onMappingProvider(MappingProviderEvent $event): void
- {
- if ('monsieurbiz_product' !== $event->getIndexCode()) {
- return;
- }
-
- $mapping = $event->getMapping();
- if (null === $mapping || !$mapping->offsetExists('mappings')) {
- return;
- }
-
- $settings = $mapping->offsetGet('settings') ?? [];
- $mapping->offsetSet('settings', array_merge($settings, ['mapping.nested_fields.limit' => 100])); // Increase the limit of nested fields
- }
-}
-```
+To change that you can [change the mapping to apply a new setting](../dist/src/Resources/config/elasticsearch/monsieurbiz_product_mapping.yaml#L11).
diff --git a/phpmd.xml b/phpmd.xml
index d0c34236..ecdbc45c 100644
--- a/phpmd.xml
+++ b/phpmd.xml
@@ -44,7 +44,7 @@
-
+
diff --git a/phpstan.neon b/phpstan.neon
index 708fba1b..39be1cd4 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -4,6 +4,7 @@ parameters:
- %rootDir%/src/
checkMissingIterableValueType: false
+ checkGenericClassInNonGenericObjectType: false
excludes_analyse:
# Makes PHPStan crash
@@ -12,6 +13,6 @@ parameters:
# Test dependencies
- 'tests/Application/**/*'
-
+
# Generated files
- 'generated/**/*'
diff --git a/src/AutoMapper/ProductAttributeValueConfiguration.php b/src/AutoMapper/ProductAttributeValueConfiguration.php
index c8b33eae..e9030893 100644
--- a/src/AutoMapper/ProductAttributeValueConfiguration.php
+++ b/src/AutoMapper/ProductAttributeValueConfiguration.php
@@ -52,19 +52,7 @@ public function process(MapperGeneratorMetadataInterface $metadata): void
throw new RuntimeException('Undefined product attribute value reader');
}
- $metadata->forMember('value', function (ProductAttributeValueInterface $productAttributeValue) {
- if (null === $productAttributeValue->getType()) {
- return null;
- }
- if (!\array_key_exists($productAttributeValue->getType(), $this->productAttributeValueReaders)) {
- $this->logger->alert(sprintf('Missing product attribute value reader for "%s" type', $productAttributeValue->getType()));
-
- return null;
- }
- $reader = $this->productAttributeValueReaders[$productAttributeValue->getType()];
-
- return $reader->getValue($productAttributeValue);
- });
+ $metadata->forMember('value', [$this, 'getProductAttributeValue']);
}
public function getSource(): string
@@ -76,4 +64,23 @@ public function getTarget(): string
{
return $this->configuration->getTargetClass('product_attribute');
}
+
+ /**
+ * @return array|string|null
+ */
+ public function getProductAttributeValue(ProductAttributeValueInterface $productAttributeValue)
+ {
+ if (null === $productAttributeValue->getType()) {
+ return null;
+ }
+ if (!\array_key_exists($productAttributeValue->getType(), $this->productAttributeValueReaders)) {
+ // @phpstan-ignore-next-line The logger can't be null here
+ $this->logger->alert(sprintf('Missing product attribute value reader for "%s" type', $productAttributeValue->getType()));
+
+ return null;
+ }
+ $reader = $this->productAttributeValueReaders[$productAttributeValue->getType()];
+
+ return $reader->getValue($productAttributeValue);
+ }
}
diff --git a/src/AutoMapper/ProductAttributeValueReader/SelectReader.php b/src/AutoMapper/ProductAttributeValueReader/SelectReader.php
index 1c30340a..a28f2e15 100644
--- a/src/AutoMapper/ProductAttributeValueReader/SelectReader.php
+++ b/src/AutoMapper/ProductAttributeValueReader/SelectReader.php
@@ -25,6 +25,9 @@ public function __construct(TranslationLocaleProviderInterface $localeProvider)
$this->defaultLocaleCode = $localeProvider->getDefaultLocaleCode();
}
+ /**
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
public function getValue(ProductAttributeValueInterface $productAttribute)
{
if (null === $productAttribute->getAttribute()) {
diff --git a/src/AutoMapper/ProductMapperConfiguration.php b/src/AutoMapper/ProductMapperConfiguration.php
index a2ca738b..33ff80f2 100644
--- a/src/AutoMapper/ProductMapperConfiguration.php
+++ b/src/AutoMapper/ProductMapperConfiguration.php
@@ -118,104 +118,125 @@ public function process(MapperGeneratorMetadataInterface $metadata): void
}, $product->getChannels()->toArray());
});
- $metadata->forMember('attributes', function (ProductInterface $product): array {
- $attributes = [];
- $currentLocale = $product->getTranslation()->getLocale();
- if (null === $currentLocale) {
- return $attributes;
- }
- $productAttributeDTOClass = $this->configuration->getTargetClass('product_attribute');
- foreach ($product->getAttributesByLocale($currentLocale, $currentLocale) as $attributeValue) {
- if (null === $attributeValue->getName() || null === $attributeValue->getValue()) {
- continue;
- }
- $attribute = $attributeValue->getAttribute();
- if (!$attribute instanceof SearchableInterface || (!$attribute->isSearchable() && !$attribute->isFilterable())) {
- continue;
- }
- $attributes[$attributeValue->getCode()] = $this->autoMapper->map($attributeValue, $productAttributeDTOClass);
- }
+ $metadata->forMember('attributes', [$this, 'getAttributes']);
- return $attributes;
- });
+ $metadata->forMember('options', [$this, 'getOptions']);
- $metadata->forMember('options', function (ProductInterface $product): array {
- $options = [];
- $currentLocale = $product->getTranslation()->getLocale();
- foreach ($product->getVariants() as $variant) {
- foreach ($variant->getOptionValues() as $optionValue) {
- if (null === $optionValue->getOption()) {
- continue;
- }
- if (!isset($options[$optionValue->getOptionCode()])) {
- $options[$optionValue->getOptionCode()] = [
- 'name' => $optionValue->getOption()->getTranslation($currentLocale)->getName(),
- 'values' => [],
- ];
- }
- $isEnabled = ($options[$optionValue->getOptionCode()]['values'][$optionValue->getCode()]['enabled'] ?? false)
- || $variant->isEnabled();
- // A variant option is considered to be in stock if the current option is enabled and is in stock
- $isInStock = ($options[$optionValue->getOptionCode()]['values'][$optionValue->getCode()]['is_in_stock'] ?? false)
- || ($variant->isEnabled() && $this->isProductVariantInStock($variant));
- $options[$optionValue->getOptionCode()]['values'][$optionValue->getCode()] = [
- 'value' => $optionValue->getTranslation($currentLocale)->getValue(),
- 'enabled' => $isEnabled,
- 'is_in_stock' => $isInStock,
- ];
- }
- }
+ $metadata->forMember('variants', [$this, 'getVariants']);
- foreach ($options as $optionCode => $optionValues) {
- $options[$optionCode]['values'] = array_values($optionValues['values']);
- }
+ $metadata->forMember('prices', [$this, 'getPrices']);
+ }
- return $options;
- });
+ public function getSource(): string
+ {
+ return $this->configuration->getSourceClass('product');
+ }
- $metadata->forMember('variants', function (ProductInterface $product): array {
- $variants = [];
- $productVariantDTOClass = $this->configuration->getTargetClass('product_variant');
- foreach ($product->getEnabledVariants() as $variant) {
- $variants[] = $this->autoMapper->map($variant, $productVariantDTOClass);
- }
+ public function getTarget(): string
+ {
+ return $this->configuration->getTargetClass('product');
+ }
- return $variants;
- });
+ /**
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
+ public function getAttributes(ProductInterface $product): array
+ {
+ $attributes = [];
+ $currentLocale = $product->getTranslation()->getLocale();
+ if (null === $currentLocale) {
+ return $attributes;
+ }
+ $productAttributeDTOClass = $this->configuration->getTargetClass('product_attribute');
+ foreach ($product->getAttributesByLocale($currentLocale, $currentLocale) as $attributeValue) {
+ if (null === $attributeValue->getName() || null === $attributeValue->getValue()) {
+ continue;
+ }
+ $attribute = $attributeValue->getAttribute();
+ if (!$attribute instanceof SearchableInterface || (!$attribute->isSearchable() && !$attribute->isFilterable())) {
+ continue;
+ }
+ $attributes[$attributeValue->getCode()] = $this->autoMapper->map($attributeValue, $productAttributeDTOClass);
+ }
- $metadata->forMember('prices', function (ProductInterface $product): array {
- $prices = [];
- foreach ($product->getChannels() as $channel) {
- /** @var ChannelInterface $channel */
- $this->channelSimulationContext->setChannel($channel);
- if (
- null === ($variant = $this->productVariantResolver->getVariant($product))
- || !$variant instanceof ModelProductVariantInterface
- || null === ($channelPricing = $variant->getChannelPricingForChannel($channel))
- ) {
- $this->channelSimulationContext->setChannel(null);
+ return $attributes;
+ }
+ /**
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
+ public function getOptions(ProductInterface $product): array
+ {
+ $options = [];
+ $currentLocale = $product->getTranslation()->getLocale();
+ foreach ($product->getVariants() as $variant) {
+ foreach ($variant->getOptionValues() as $optionValue) {
+ if (null === $optionValue->getOption()) {
continue;
}
- $this->channelSimulationContext->setChannel(null);
- $prices[] = $this->autoMapper->map(
- $channelPricing,
- $this->configuration->getTargetClass('pricing')
- );
+ if (!isset($options[$optionValue->getOptionCode()])) {
+ $options[$optionValue->getOptionCode()] = [
+ 'name' => $optionValue->getOption()->getTranslation($currentLocale)->getName(),
+ 'values' => [],
+ ];
+ }
+ $isEnabled = ($options[$optionValue->getOptionCode()]['values'][$optionValue->getCode()]['enabled'] ?? false)
+ || $variant->isEnabled();
+ // A variant option is considered to be in stock if the current option is enabled and is in stock
+ $isInStock = ($options[$optionValue->getOptionCode()]['values'][$optionValue->getCode()]['is_in_stock'] ?? false)
+ || ($variant->isEnabled() && $this->isProductVariantInStock($variant));
+ $options[$optionValue->getOptionCode()]['values'][$optionValue->getCode()] = [
+ 'value' => $optionValue->getTranslation($currentLocale)->getValue(),
+ 'enabled' => $isEnabled,
+ 'is_in_stock' => $isInStock,
+ ];
}
+ }
- return $prices;
- });
+ foreach ($options as $optionCode => $optionValues) {
+ $options[$optionCode]['values'] = array_values($optionValues['values']);
+ }
+
+ return $options;
}
- public function getSource(): string
+ public function getVariants(ProductInterface $product): array
{
- return $this->configuration->getSourceClass('product');
+ $variants = [];
+ $productVariantDTOClass = $this->configuration->getTargetClass('product_variant');
+ foreach ($product->getEnabledVariants() as $variant) {
+ $variants[] = $this->autoMapper->map($variant, $productVariantDTOClass);
+ }
+
+ return $variants;
}
- public function getTarget(): string
+ /**
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
+ public function getPrices(ProductInterface $product): array
{
- return $this->configuration->getTargetClass('product');
+ $prices = [];
+ foreach ($product->getChannels() as $channel) {
+ /** @var ChannelInterface $channel */
+ $this->channelSimulationContext->setChannel($channel);
+ if (
+ null === ($variant = $this->productVariantResolver->getVariant($product))
+ || !$variant instanceof ModelProductVariantInterface
+ || null === ($channelPricing = $variant->getChannelPricingForChannel($channel))
+ ) {
+ $this->channelSimulationContext->setChannel(null);
+
+ continue;
+ }
+ $this->channelSimulationContext->setChannel(null);
+ $prices[] = $this->autoMapper->map(
+ $channelPricing,
+ $this->configuration->getTargetClass('pricing')
+ );
+ }
+
+ return $prices;
}
private function isProductVariantInStock(ProductVariantInterface $productVariant): bool
diff --git a/src/Command/PopulateCommand.php b/src/Command/PopulateCommand.php
index 4fcf7a36..057dcfff 100644
--- a/src/Command/PopulateCommand.php
+++ b/src/Command/PopulateCommand.php
@@ -17,6 +17,7 @@
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
class PopulateCommand extends Command
{
@@ -37,8 +38,9 @@ protected function configure(): void
protected function execute(InputInterface $input, OutputInterface $output)
{
- $this->indexer->indexAll();
- $output->writeln('ok');
+ $io = new SymfonyStyle($input, $output);
+ $this->indexer->indexAll($io);
+ $io->success('Done');
return Command::SUCCESS;
}
diff --git a/src/Controller/SearchController.php b/src/Controller/SearchController.php
index 2ea42b7e..abe932d7 100644
--- a/src/Controller/SearchController.php
+++ b/src/Controller/SearchController.php
@@ -24,11 +24,13 @@
use Sylius\Component\Channel\Context\ChannelContextInterface;
use Sylius\Component\Currency\Context\CurrencyContextInterface;
use Sylius\Component\Locale\Context\LocaleContextInterface;
+use Sylius\Component\Registry\NonExistingServiceException;
use Sylius\Component\Registry\ServiceRegistryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Intl\Currencies;
class SearchController extends AbstractController
@@ -65,11 +67,12 @@ public function __construct(
$this->parametersParser = $parametersParser;
}
- // TODO add an optional parameter $documentType (nullable => get the default document type)
- public function searchAction(Request $request, string $query): Response
- {
- /** @var DocumentableInterface $documentable */
- $documentable = $this->documentableRegistry->get('search.documentable.monsieurbiz_product');
+ public function searchAction(
+ Request $request,
+ string $query
+ ): Response {
+ $documentType = ((string) $request->query->get('document_type')) ?: null;
+ $documentable = $this->getDocumentable($documentType);
$requestConfiguration = new RequestConfiguration(
$request,
RequestInterface::SEARCH_TYPE,
@@ -80,6 +83,7 @@ public function searchAction(Request $request, string $query): Response
$result = $this->search->search($requestConfiguration);
return $this->render('@MonsieurBizSyliusSearchPlugin/Search/result.html.twig', [
+ 'documentableRegistries' => $this->documentableRegistry->all(),
'documentable' => $result->getDocumentable(),
'requestConfiguration' => $requestConfiguration,
'query' => urldecode($query),
@@ -126,7 +130,7 @@ public function instantAction(Request $request): Response
);
try {
- $results[] = $this->search->search($requestConfiguration);
+ $results[$documentable->getIndexCode()] = $this->search->search($requestConfiguration);
} catch (UnknownRequestTypeException $e) {
continue;
}
@@ -137,10 +141,11 @@ public function instantAction(Request $request): Response
]);
}
- public function taxonAction(Request $request): Response
- {
- /** @var DocumentableInterface $documentable */
- $documentable = $this->documentableRegistry->get('search.documentable.monsieurbiz_product');
+ public function taxonAction(
+ Request $request,
+ string $documentType = 'monsieurbiz_product'
+ ): Response {
+ $documentable = $this->getDocumentable($documentType);
$requestConfiguration = new RequestConfiguration(
$request,
RequestInterface::TAXON_TYPE,
@@ -157,4 +162,20 @@ public function taxonAction(Request $request): Response
'currencySymbol' => Currencies::getSymbol($this->currencyContext->getCurrencyCode(), $this->localeContext->getLocaleCode()),
]);
}
+
+ private function getDocumentable(?string $documentType): DocumentableInterface
+ {
+ if (null === $documentType) {
+ $documentables = $this->documentableRegistry->all();
+
+ return reset($documentables);
+ }
+
+ try {
+ /** @phpstan-ignore-next-line */
+ return $this->documentableRegistry->get('search.documentable.' . $documentType);
+ } catch (NonExistingServiceException $exception) {
+ throw new NotFoundHttpException(sprintf('Documentable "%s" not found', $documentType));
+ }
+ }
}
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index a4edd50d..241dc546 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -59,6 +59,9 @@ public function getConfigTreeBuilder(): TreeBuilder
->prototype('scalar')->end()
->end()
->end()
+
+ // Position
+ ->integerNode('position')->defaultValue(0)->end()
->end()
->end()
->end()
diff --git a/src/DependencyInjection/DocumentableRegistryPass.php b/src/DependencyInjection/DocumentableRegistryPass.php
index d00a8de0..dda4d546 100644
--- a/src/DependencyInjection/DocumentableRegistryPass.php
+++ b/src/DependencyInjection/DocumentableRegistryPass.php
@@ -28,48 +28,17 @@ public function process(ContainerBuilder $container): void
if (!$container->hasDefinition('monsieurbiz.search.registry.documentable')) {
return;
}
-
- $registry = $container->getDefinition('monsieurbiz.search.registry.documentable');
$documentables = $container->getParameter('monsieurbiz.search.config.documents');
if (!\is_array($documentables)) {
return;
}
- $searchSettings = [];
- if ($container->hasParameter('monsieurbiz.settings.config.plugins')) {
- $searchSettings = $container->getParameter('monsieurbiz.settings.config.plugins');
- }
-
- foreach ($documentables as $indexCode => $documentableConfiguration) {
- $documentableServiceId = 'search.documentable.' . $indexCode;
- $documentableClass = $documentableConfiguration['document_class'];
- $this->validateDocumentableResource($documentableClass);
- $documentableDefinition = (new Definition($documentableClass))
- ->setAutowired(true)
- ->setArguments([
- '$indexCode' => $indexCode,
- '$sourceClass' => $documentableConfiguration['source'],
- '$targetClass' => $documentableConfiguration['target'],
- '$templates' => $documentableConfiguration['templates'],
- '$limits' => $documentableConfiguration['limits'],
- ])
- ;
- $documentableDefinition = $container->setDefinition($documentableServiceId, $documentableDefinition);
- $documentableDefinition->addTag('monsieurbiz.search.documentable');
- $documentableDefinition->addMethodCall('setMappingProvider', [new Reference($documentableConfiguration['mapping_provider'])]);
- $documentableDefinition->addMethodCall('setDatasource', [new Reference($documentableConfiguration['datasource'])]);
- if ($this->isPrefixedDocumentableClass($documentableClass) && isset($documentableConfiguration['prefix'])) {
- $documentableDefinition->addMethodCall('setPrefix', [$documentableConfiguration['prefix']]);
- }
-
- // Add documentable into registry
- $registry->addMethodCall('register', [$documentableServiceId, new Reference($documentableServiceId)]);
- // Add the default settings value of documentable
- $searchSettings['monsieurbiz.search']['default_values']['instant_search_enabled__' . $indexCode] = $documentableConfiguration['instant_search_enabled'];
- $searchSettings['monsieurbiz.search']['default_values']['limits__' . $indexCode] = $documentableConfiguration['limits'];
- }
+ // Sort documentables by position
+ uasort($documentables, function ($documentableA, $documentableB) {
+ return $documentableA['position'] <=> $documentableB['position'];
+ });
- $container->setParameter('monsieurbiz.settings.config.plugins', $searchSettings);
+ $this->addDocumentableServices($container, $documentables);
}
/**
@@ -90,4 +59,53 @@ private function isPrefixedDocumentableClass(string $class): bool
return \in_array(PrefixedDocumentableInterface::class, $interfaces, true);
}
+
+ private function addDocumentableServices(ContainerBuilder $container, array $documentables): void
+ {
+ $registry = $container->getDefinition('monsieurbiz.search.registry.documentable');
+
+ $searchSettings = [];
+ if ($container->hasParameter('monsieurbiz.settings.config.plugins')) {
+ $searchSettings = $container->getParameter('monsieurbiz.settings.config.plugins');
+ }
+
+ foreach ($documentables as $indexCode => $documentableConfiguration) {
+ $documentableServiceId = 'search.documentable.' . $indexCode;
+
+ // Create documentable service
+ $this->createDocumentable($container, $documentableServiceId, $indexCode, $documentableConfiguration);
+
+ // Add documentable into registry
+ $registry->addMethodCall('register', [$documentableServiceId, new Reference($documentableServiceId)]);
+
+ // Add the default settings value of documentable
+ $searchSettings['monsieurbiz.search']['default_values']['instant_search_enabled__' . $indexCode] = $documentableConfiguration['instant_search_enabled'];
+ $searchSettings['monsieurbiz.search']['default_values']['limits__' . $indexCode] = $documentableConfiguration['limits'];
+ }
+
+ $container->setParameter('monsieurbiz.settings.config.plugins', $searchSettings);
+ }
+
+ private function createDocumentable(ContainerBuilder $container, string $documentableServiceId, string $indexCode, array $documentableConfiguration): void
+ {
+ $documentableClass = $documentableConfiguration['document_class'];
+ $this->validateDocumentableResource($documentableClass);
+ $documentableDefinition = (new Definition($documentableClass))
+ ->setAutowired(true)
+ ->setArguments([
+ '$indexCode' => $indexCode,
+ '$sourceClass' => $documentableConfiguration['source'],
+ '$targetClass' => $documentableConfiguration['target'],
+ '$templates' => $documentableConfiguration['templates'],
+ '$limits' => $documentableConfiguration['limits'],
+ ])
+ ;
+ $documentableDefinition = $container->setDefinition($documentableServiceId, $documentableDefinition);
+ $documentableDefinition->addTag('monsieurbiz.search.documentable');
+ $documentableDefinition->addMethodCall('setMappingProvider', [new Reference($documentableConfiguration['mapping_provider'])]);
+ $documentableDefinition->addMethodCall('setDatasource', [new Reference($documentableConfiguration['datasource'])]);
+ if ($this->isPrefixedDocumentableClass($documentableClass) && isset($documentableConfiguration['prefix'])) {
+ $documentableDefinition->addMethodCall('setPrefix', [$documentableConfiguration['prefix']]);
+ }
+ }
}
diff --git a/src/EventSubscriber/AppendProductAttributeMappingSubscriber.php b/src/EventSubscriber/AppendProductAttributeMappingSubscriber.php
index e5c154f2..06df06e4 100644
--- a/src/EventSubscriber/AppendProductAttributeMappingSubscriber.php
+++ b/src/EventSubscriber/AppendProductAttributeMappingSubscriber.php
@@ -55,6 +55,14 @@ public function onMappingProvider(MappingProviderEvent $event): void
}
/** @var array $mappings */
$mappings = $mapping->offsetGet('mappings');
+ $mappings = $this->appendAttributesMapping($mappings);
+ $mappings = $this->appendOptionsMapping($mappings);
+
+ $mapping->offsetSet('mappings', $mappings);
+ }
+
+ private function appendAttributesMapping(array $mappings): array
+ {
$attributesMapping = [];
foreach ($this->productAttributeRepository->findIsSearchableOrFilterable() as $productAttribute) {
$attributesMapping[$productAttribute->getCode()] = $this->getProductAttributeProperties($productAttribute);
@@ -66,6 +74,11 @@ public function onMappingProvider(MappingProviderEvent $event): void
];
}
+ return $mappings;
+ }
+
+ private function appendOptionsMapping(array $mappings): array
+ {
$optionsMapping = [];
foreach ($this->productOptionRepository->findIsSearchableOrFilterable() as $productOption) {
$optionsMapping[$productOption->getCode()] = $this->getProductOptionProperties($productOption);
@@ -77,7 +90,7 @@ public function onMappingProvider(MappingProviderEvent $event): void
];
}
- $mapping->offsetSet('mappings', $mappings);
+ return $mappings;
}
private function getProductAttributeProperties(SearchableInterface $productAttribute): array
diff --git a/src/Factory/YamlProviderFactory.php b/src/Factory/YamlProviderFactory.php
deleted file mode 100644
index d8b6219b..00000000
--- a/src/Factory/YamlProviderFactory.php
+++ /dev/null
@@ -1,25 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE.txt
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace MonsieurBiz\SyliusSearchPlugin\Factory;
-
-use JoliCode\Elastically\Mapping\YamlProvider;
-use Symfony\Component\Yaml\Parser;
-
-class YamlProviderFactory
-{
- public function create(string $configurationDirectory, Parser $parser): YamlProvider
- {
- return new YamlProvider($configurationDirectory, $parser);
- }
-}
diff --git a/src/Fixture/Factory/SearchableFixtureFactory.php b/src/Fixture/Factory/SearchableFixtureFactory.php
index acf24b02..e7f4de3c 100644
--- a/src/Fixture/Factory/SearchableFixtureFactory.php
+++ b/src/Fixture/Factory/SearchableFixtureFactory.php
@@ -13,6 +13,7 @@
namespace MonsieurBiz\SyliusSearchPlugin\Fixture\Factory;
+use Exception;
use MonsieurBiz\SyliusSearchPlugin\Entity\Product\SearchableInterface;
use Sylius\Bundle\CoreBundle\Fixture\Factory\AbstractExampleFactory;
use Sylius\Bundle\CoreBundle\Fixture\OptionsResolver\LazyOption;
@@ -20,6 +21,7 @@
use Sylius\Component\Product\Model\ProductOptionInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
+use Webmozart\Assert\Assert;
class SearchableFixtureFactory extends AbstractExampleFactory implements SearchableFixtureFactoryInterface
{
@@ -69,29 +71,29 @@ protected function configureOptions(OptionsResolver $resolver): void
}
/**
- * @throws \Exception
- *
- * @return object
+ * @throws Exception
*/
- public function create(array $options = [])
+ public function create(array $options = []): SearchableInterface
{
$options = $this->optionsResolver->resolve($options);
+ $object = $this->getSearchableObject($options);
+ $object->setFilterable(((bool) $options['filterable']) ?? false);
+ $object->setSearchable(((bool) $options['searchable']) ?? false);
+
+ return $object;
+ }
- if (isset($options['attribute']) && !empty($options['attribute'])) {
+ private function getSearchableObject(array $options): SearchableInterface
+ {
+ $object = null;
+ if (!empty($options['attribute'])) {
$object = $options['attribute'];
- } elseif (isset($options['option']) && !empty($options['option'])) {
+ } elseif (!empty($options['option'])) {
$object = $options['option'];
- } else {
- throw new \Exception('You need to specify an attribute or an option to be filterable.');
- }
-
- if (!$object instanceof SearchableInterface) {
- throw new \Exception(sprintf('Your class "%s" is not an instance of %s', \get_class($object), SearchableInterface::class));
}
/** @var SearchableInterface $object */
- $object->setFilterable(((bool) $options['filterable']) ?? false);
- $object->setSearchable(((bool) $options['searchable']) ?? false);
+ Assert::isInstanceOf($object, SearchableInterface::class);
return $object;
}
diff --git a/src/Fixture/SearchableFixture.php b/src/Fixture/SearchableFixture.php
index 32665f37..027d8b1c 100644
--- a/src/Fixture/SearchableFixture.php
+++ b/src/Fixture/SearchableFixture.php
@@ -43,6 +43,7 @@ public function getName(): string
*/
protected function configureResourceNode(ArrayNodeDefinition $resourceNode): void
{
+ /** @phpstan-ignore-next-line */
$resourceNode
->children()
->scalarNode('attribute')->end()
diff --git a/src/Form/Extension/ProductAttributeTypeExtension.php b/src/Form/Extension/ProductAttributeTypeExtension.php
index 67bccb4a..a231f939 100644
--- a/src/Form/Extension/ProductAttributeTypeExtension.php
+++ b/src/Form/Extension/ProductAttributeTypeExtension.php
@@ -21,6 +21,9 @@
final class ProductAttributeTypeExtension extends AbstractTypeExtension
{
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$searchWeightValues = range(1, 10);
diff --git a/src/Form/Extension/ProductOptionTypeExtension.php b/src/Form/Extension/ProductOptionTypeExtension.php
index 71c2abda..a4425452 100644
--- a/src/Form/Extension/ProductOptionTypeExtension.php
+++ b/src/Form/Extension/ProductOptionTypeExtension.php
@@ -21,6 +21,9 @@
final class ProductOptionTypeExtension extends AbstractTypeExtension
{
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$searchWeightValues = range(1, 10);
diff --git a/src/Form/Type/SearchType.php b/src/Form/Type/SearchType.php
index 883e117b..4c6c6412 100644
--- a/src/Form/Type/SearchType.php
+++ b/src/Form/Type/SearchType.php
@@ -22,6 +22,9 @@
class SearchType extends AbstractType
{
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
diff --git a/src/Index/Indexer.php b/src/Index/Indexer.php
index 2bf8dd59..d5343d99 100644
--- a/src/Index/Indexer.php
+++ b/src/Index/Indexer.php
@@ -25,6 +25,8 @@
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\Registry\ServiceRegistryInterface;
use Sylius\Component\Resource\Model\TranslatableInterface;
+use Symfony\Component\Console\Output\NullOutput;
+use Symfony\Component\Console\Output\OutputInterface;
final class Indexer implements IndexerInterface
{
@@ -57,14 +59,19 @@ public function __construct(
/**
* Index all documentable object.
*/
- public function indexAll(): void
+ public function indexAll(?OutputInterface $output = null): void
{
+ $output = $output ?? new NullOutput();
/** @var DocumentableInterface $documentable */
foreach ($this->documentableRegistry->all() as $documentable) {
- $this->indexDocumentable($documentable);
+ $output->writeln(sprintf('Indexing %s', $documentable->getIndexCode()));
+ $this->indexDocumentable($output, $documentable);
}
}
+ /**
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
public function indexByDocuments(DocumentableInterface $documentable, array $documents, ?string $locale = null, ?ElasticallyIndexer $indexer = null): void
{
if (null === $indexer) {
@@ -101,6 +108,9 @@ public function deleteByDocuments(DocumentableInterface $documentable, array $do
$this->deleteByDocumentIds($documentable, $documentIds, $locale, $indexer);
}
+ /**
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
public function deleteByDocumentIds(DocumentableInterface $documentable, array $documentsIds, ?string $locale = null, ?ElasticallyIndexer $indexer = null): void
{
if (null === $indexer) {
@@ -143,11 +153,18 @@ private function getLocales(): array
return $this->locales;
}
- private function indexDocumentable(DocumentableInterface $documentable, ?string $locale = null): void
+ /**
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
+ private function indexDocumentable(OutputInterface $output, DocumentableInterface $documentable, ?string $locale = null): void
{
if (null === $locale && $documentable->isTranslatable()) {
foreach ($this->getLocales() as $localeCode) {
- $this->indexDocumentable($documentable, $localeCode);
+ $output->writeln(
+ sprintf('Indexing %s for locale %s', $documentable->getIndexCode(), $localeCode),
+ OutputInterface::VERBOSITY_VERBOSE
+ );
+ $this->indexDocumentable($output, $documentable, $localeCode);
}
return;
@@ -172,8 +189,10 @@ private function indexDocumentable(DocumentableInterface $documentable, ?string
$indexer->flush();
$indexBuilder->markAsLive($newIndex, $indexName);
+ $output->writeln(sprintf('Index %s is now live', $indexName), OutputInterface::VERBOSITY_VERBOSE);
$indexBuilder->speedUpRefresh($newIndex);
$indexBuilder->purgeOldIndices($indexName);
+ $output->writeln(sprintf('Old indices for %s are now purged', $indexName), OutputInterface::VERBOSITY_VERBOSE);
}
/**
diff --git a/src/Index/IndexerInterface.php b/src/Index/IndexerInterface.php
index 40073476..5454c056 100644
--- a/src/Index/IndexerInterface.php
+++ b/src/Index/IndexerInterface.php
@@ -15,10 +15,11 @@
use JoliCode\Elastically\Indexer as ElasticallyIndexer;
use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface;
+use Symfony\Component\Console\Output\OutputInterface;
interface IndexerInterface
{
- public function indexAll(): void;
+ public function indexAll(?OutputInterface $output = null): void;
public function indexByDocuments(DocumentableInterface $documentable, array $documents, ?string $locale = null, ?ElasticallyIndexer $indexer = null): void;
diff --git a/src/Mapping/YamlWithLocaleProvider.php b/src/Mapping/YamlWithLocaleProvider.php
index ecf2d412..dfdee2a8 100644
--- a/src/Mapping/YamlWithLocaleProvider.php
+++ b/src/Mapping/YamlWithLocaleProvider.php
@@ -17,7 +17,6 @@
use Elastica\Exception\InvalidException;
use JoliCode\Elastically\Mapping\MappingProviderInterface;
use MonsieurBiz\SyliusSearchPlugin\Event\MappingProviderEvent;
-use MonsieurBiz\SyliusSearchPlugin\Factory\YamlProviderFactory;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Yaml\Exception\ParseException;
@@ -27,8 +26,6 @@ class YamlWithLocaleProvider implements MappingProviderInterface
{
private EventDispatcherInterface $eventDispatcher;
- private YamlProviderFactory $yamlProviderFactory;
-
private FileLocatorInterface $fileLocator;
/**
@@ -40,13 +37,11 @@ class YamlWithLocaleProvider implements MappingProviderInterface
public function __construct(
EventDispatcherInterface $eventDispatcher,
- YamlProviderFactory $yamlProviderFactory,
FileLocatorInterface $fileLocator,
iterable $configurationDirectories = [],
?Parser $parser = null
) {
$this->eventDispatcher = $eventDispatcher;
- $this->yamlProviderFactory = $yamlProviderFactory;
$this->fileLocator = $fileLocator;
$this->configurationDirectories = $configurationDirectories;
$this->parser = $parser ?? new Parser();
@@ -81,11 +76,13 @@ public function provideMapping(string $indexName, array $context = []): ?array
private function appendMapping(string $configurationDirectory, array $mapping, string $indexName, array $context): array
{
- $yamlProvider = $this->yamlProviderFactory->create($configurationDirectory, $this->parser);
-
try {
- $mapping = array_merge_recursive($mapping, $yamlProvider->provideMapping($context['index_code'] ?? $indexName, $context) ?? []);
- } catch (InvalidException $exception) {
+ $indexName = $context['index_code'] ?? $indexName;
+ $fileName = $context['filename'] ?? ($indexName . '_mapping.yaml');
+ $mappingFilePath = $configurationDirectory . \DIRECTORY_SEPARATOR . $fileName;
+
+ $mapping = array_merge_recursive($mapping, $this->parser->parseFile($mappingFilePath));
+ } catch (ParseException $exception) {
// the mapping yaml file does not exist.
}
@@ -98,15 +95,22 @@ private function appendLocaleAnalyzers(string $configurationDirectory, array $ma
return $mapping;
}
+ $mapping = $this->appendAnalyzers($configurationDirectory . \DIRECTORY_SEPARATOR . 'analyzers.yaml', $mapping);
+
foreach ($this->getLocaleCode($locale) as $localeCode) {
- $analyzerFilePath = $configurationDirectory . \DIRECTORY_SEPARATOR . 'analyzers_' . $localeCode . '.yaml';
+ $mapping = $this->appendAnalyzers($configurationDirectory . \DIRECTORY_SEPARATOR . 'analyzers_' . $localeCode . '.yaml', $mapping);
+ }
- try {
- $analyzer = $this->parser->parseFile($analyzerFilePath) ?? [];
- $mapping['settings']['analysis'] = array_merge_recursive($mapping['settings']['analysis'] ?? [], $analyzer);
- } catch (ParseException $exception) {
- // the yaml file does not exist or does not exist.
- }
+ return $mapping;
+ }
+
+ private function appendAnalyzers(string $analyzerFilePath, array $mapping): array
+ {
+ try {
+ $analyzer = $this->parser->parseFile($analyzerFilePath) ?? [];
+ $mapping['settings']['analysis'] = array_merge_recursive($mapping['settings']['analysis'] ?? [], $analyzer);
+ } catch (ParseException $exception) {
+ // the yaml file does not exist or does not exist.
}
return $mapping;
diff --git a/src/Model/Datasource/ProductDatasource.php b/src/Model/Datasource/ProductDatasource.php
index 491fbdf3..ce996d79 100644
--- a/src/Model/Datasource/ProductDatasource.php
+++ b/src/Model/Datasource/ProductDatasource.php
@@ -14,6 +14,7 @@
namespace MonsieurBiz\SyliusSearchPlugin\Model\Datasource;
use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\EntityRepository;
use Pagerfanta\Doctrine\ORM\QueryAdapter;
use Pagerfanta\Pagerfanta;
use Sylius\Component\Core\Repository\ProductRepositoryInterface;
@@ -30,8 +31,9 @@ public function __construct(EntityManagerInterface $entityManager)
public function getItems(string $sourceClass): iterable
{
+ /** @phpstan-ignore-next-line */
$repository = $this->entityManager->getRepository($sourceClass);
- /** @var ProductRepositoryInterface $repository */
+ /** @var ProductRepositoryInterface&EntityRepository $repository */
Assert::isInstanceOf($repository, ProductRepositoryInterface::class);
$queryBuilder = $repository->createQueryBuilder('o')
diff --git a/src/Normalizer/Product/ProductDTONormalizer.php b/src/Normalizer/Product/ProductDTONormalizer.php
index 24532387..394f5569 100644
--- a/src/Normalizer/Product/ProductDTONormalizer.php
+++ b/src/Normalizer/Product/ProductDTONormalizer.php
@@ -13,8 +13,8 @@
namespace MonsieurBiz\SyliusSearchPlugin\Normalizer\Product;
-use Jacquesbh\Eater\EaterInterface;
use MonsieurBiz\SyliusSearchPlugin\AutoMapper\Configuration;
+use MonsieurBiz\SyliusSearchPlugin\Model\Product\ProductDTO;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
@@ -58,9 +58,15 @@ public function __construct(
$this->automapperConfiguration = $automapperConfiguration;
}
- public function denormalize($data, string $type, string $format = null, array $context = [])
+ /**
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
+ *
+ * @param mixed $data
+ */
+ public function denormalize($data, string $type, string $format = null, array $context = []): ProductDTO
{
- /** @var EaterInterface $object */
+ /** @var ProductDTO $object */
$object = parent::denormalize($data, $type, $format, $context);
if (\array_key_exists('main_taxon', $data) && null !== $data['main_taxon']) {
@@ -122,11 +128,21 @@ public function denormalize($data, string $type, string $format = null, array $c
return $object;
}
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ *
+ * @param mixed $data
+ */
public function supportsDenormalization($data, string $type, string $format = null): bool
{
return $this->automapperConfiguration->getTargetClass('product') === $type;
}
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ *
+ * @param mixed $data
+ */
public function supportsNormalization($data, string $format = null): bool
{
return false;
diff --git a/src/Resolver/CheapestProductVariantResolver.php b/src/Resolver/CheapestProductVariantResolver.php
index e1c41021..f88c4c50 100644
--- a/src/Resolver/CheapestProductVariantResolver.php
+++ b/src/Resolver/CheapestProductVariantResolver.php
@@ -29,6 +29,9 @@ public function __construct(ChannelContextInterface $channelContext)
$this->channelContext = $channelContext;
}
+ /**
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
public function getVariant(ProductInterface $subject): ?ProductVariantInterface
{
$channel = $this->channelContext->getChannel();
diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml
index 1af9646c..e8dffe1e 100644
--- a/src/Resources/config/services.yaml
+++ b/src/Resources/config/services.yaml
@@ -44,6 +44,16 @@ services:
MonsieurBiz\SyliusSearchPlugin\Generated\:
resource: '../../../generated'
+ # Do not autoconfigure requests
+ MonsieurBiz\SyliusSearchPlugin\Search\Request\InstantSearch:
+ autoconfigure: false
+
+ MonsieurBiz\SyliusSearchPlugin\Search\Request\Search:
+ autoconfigure: false
+
+ MonsieurBiz\SyliusSearchPlugin\Search\Request\Taxon:
+ autoconfigure: false
+
# ES Client configuration
MonsieurBiz\SyliusSearchPlugin\Search\ClientFactory:
arguments:
@@ -126,13 +136,13 @@ services:
tags:
- { name: monsieurbiz.search.request.product_instant_search_filter }
- MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\ChannelFilter:
+ MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\ChannelFilter:
tags:
- { name: monsieurbiz.search.request.product_search_filter }
- { name: monsieurbiz.search.request.product_instant_search_filter }
- { name: monsieurbiz.search.request.product_taxon_filter }
- MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\EnabledFilter:
+ MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\EnabledFilter:
tags:
- { name: monsieurbiz.search.request.product_search_filter }
- { name: monsieurbiz.search.request.product_instant_search_filter }
@@ -197,18 +207,22 @@ services:
# Define the product queries
MonsieurBiz\SyliusSearchPlugin\Search\Request\ProductRequest\Search:
arguments:
+ $documentType: monsieurbiz_product
$queryFilters: !tagged_iterator { tag: 'monsieurbiz.search.request.product_search_filter' }
$postFilters: !tagged_iterator { tag: 'monsieurbiz.search.request.product_post_filter' }
$sorters: !tagged_iterator { tag: 'monsieurbiz.search.request.product_sorter' }
$functionScores: !tagged_iterator { tag: 'monsieurbiz.search.request.product_function_score' }
- MonsieurBiz\SyliusSearchPlugin\Search\Request\ProductRequest\InstantSearch:
+ monsieurbiz.search.request.product_instant_search:
+ class: MonsieurBiz\SyliusSearchPlugin\Search\Request\InstantSearch
arguments:
+ $documentType: monsieurbiz_product
$queryFilters: !tagged_iterator { tag: 'monsieurbiz.search.request.product_instant_search_filter' }
$functionScores: !tagged_iterator { tag: 'monsieurbiz.search.request.product_function_score' }
MonsieurBiz\SyliusSearchPlugin\Search\Request\ProductRequest\Taxon:
arguments:
+ $documentType: monsieurbiz_product
$queryFilters: !tagged_iterator { tag: 'monsieurbiz.search.request.product_taxon_filter' }
$postFilters: !tagged_iterator { tag: 'monsieurbiz.search.request.product_post_filter' }
$sorters: !tagged_iterator { tag: 'monsieurbiz.search.request.product_sorter' }
diff --git a/src/Resources/translations/messages.en.yml b/src/Resources/translations/messages.en.yml
index ec17a2ae..3eed8484 100644
--- a/src/Resources/translations/messages.en.yml
+++ b/src/Resources/translations/messages.en.yml
@@ -22,6 +22,7 @@ monsieurbiz_searchplugin:
result:
search_result: 'Search results for "%query%" (%count%)'
no_result: 'No result.'
+ monsieurbiz_product: 'Products'
instant:
result:
search_result: 'Search results for "%query%" (%count%)'
diff --git a/src/Resources/translations/messages.fr.yml b/src/Resources/translations/messages.fr.yml
index 829fe97e..305eab2b 100644
--- a/src/Resources/translations/messages.fr.yml
+++ b/src/Resources/translations/messages.fr.yml
@@ -22,10 +22,12 @@ monsieurbiz_searchplugin:
result:
search_result: 'Résultats de recherche pour "%query%" (%count%)'
no_result: 'Pas de résultat.'
+ monsieurbiz_product: 'Produits'
instant:
result:
search_result: 'Résultats de recherche pour "%query%" (%count%)'
no_result: 'Pas de résultat.'
+ monsieurbiz_product: 'Produits'
filters:
filter_results: 'Filtrer les résultats'
apply_filters: 'Appliquer les filtres'
diff --git a/src/Resources/translations/messages.it.yml b/src/Resources/translations/messages.it.yml
index fb1bf7a6..b3ed5081 100644
--- a/src/Resources/translations/messages.it.yml
+++ b/src/Resources/translations/messages.it.yml
@@ -16,10 +16,12 @@ monsieurbiz_searchplugin:
result:
search_result: 'Risultati per la ricerca "%query%" (%count%)'
no_result: 'Nessun risultato.'
+ monsieurbiz_product: 'Prodotti'
instant:
result:
search_result: 'Risultati per la ricerca "%query%" (%count%)'
no_result: 'Nessun risultato.'
+ monsieurbiz_product: 'Prodotti'
filters:
filter_results: 'Filtra i risultati'
apply_filters: 'Applica i filtri'
diff --git a/src/Resources/views/Search/_filters.html.twig b/src/Resources/views/Search/_filters.html.twig
index 8fa35cf0..b183f236 100644
--- a/src/Resources/views/Search/_filters.html.twig
+++ b/src/Resources/views/Search/_filters.html.twig
@@ -8,25 +8,32 @@
{{ 'monsieurbiz_searchplugin.filters.no_filter'|trans }}
{% else %}
- {% set path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')|merge(app.request.query.all)) %}
+ {% set encodedQuery = app.request.attributes.get('_route_params').query|default('')|url_encode %}
+ {% set path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')|merge(app.request.query.all)|merge({query: encodedQuery})) %}