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 %} + +
+
+ {{ item.name }} +
+
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 %} + +
+
+ {{ item.name }} +
+
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})) %}
{% for filter in result.filters %} {% include '@MonsieurBizSyliusSearchPlugin/Search/_filter.html.twig' with {'filter': filter} %} {% endfor %} + {# Don't loose current document_type #} + {% set documentType = app.request.query.all()['document_type']|default() %} + {% if documentType is not empty %} + + {% endif %} + {# Don't loose current sorting #} {% set sorting = app.request.query.all()['sorting']|default() %} {% if sorting is not empty %} {% for sort, order in sorting %} - + {% endfor %} {% endif %} {# Don't loose current limit #} {% set limit = app.request.query.get('limit') %} {% if limit is not empty %} - + {% endif %} {# {% if gridConfig.haveToApplyManuallyFilters() %}#}
diff --git a/src/Resources/views/Search/_tabs.html.twig b/src/Resources/views/Search/_tabs.html.twig new file mode 100644 index 00000000..21a6c47f --- /dev/null +++ b/src/Resources/views/Search/_tabs.html.twig @@ -0,0 +1,20 @@ +{% if documentableRegistries|length > 1%} + +{% endif %} diff --git a/src/Resources/views/Search/result.html.twig b/src/Resources/views/Search/result.html.twig index b9d802f9..f887a4d5 100644 --- a/src/Resources/views/Search/result.html.twig +++ b/src/Resources/views/Search/result.html.twig @@ -4,6 +4,8 @@ {% block content %} {% include '@MonsieurBizSyliusSearchPlugin/Search/_header.html.twig' %} + {% include '@MonsieurBizSyliusSearchPlugin/Search/_tabs.html.twig' %} +
diff --git a/src/Search/Filter/FilterValue.php b/src/Search/Filter/FilterValue.php index 6ae666b4..d990855f 100644 --- a/src/Search/Filter/FilterValue.php +++ b/src/Search/Filter/FilterValue.php @@ -27,6 +27,8 @@ class FilterValue /** * Filter constructor. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ public function __construct(string $label, int $count, string $value = null, bool $isApplied = false) { diff --git a/src/Search/Request/Aggregation/ProductAttributeAggregation.php b/src/Search/Request/Aggregation/ProductAttributeAggregation.php index b18fbed3..6a9fe6bc 100644 --- a/src/Search/Request/Aggregation/ProductAttributeAggregation.php +++ b/src/Search/Request/Aggregation/ProductAttributeAggregation.php @@ -19,6 +19,11 @@ final class ProductAttributeAggregation implements AggregationBuilderInterface { + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @param mixed $aggregation + */ public function build($aggregation, array $filters) { /** @var ProductAttributeInterface&SearchableInterface $aggregation */ diff --git a/src/Search/Request/Aggregation/ProductAttributesAggregation.php b/src/Search/Request/Aggregation/ProductAttributesAggregation.php index 760ef334..68be0354 100644 --- a/src/Search/Request/Aggregation/ProductAttributesAggregation.php +++ b/src/Search/Request/Aggregation/ProductAttributesAggregation.php @@ -26,6 +26,11 @@ public function __construct() $this->productAttributeAggregationBuilder = new ProductAttributeAggregation(); } + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @param mixed $aggregation + */ public function build($aggregation, array $filters) { if (!$this->isSupport($aggregation)) { @@ -44,7 +49,6 @@ public function build($aggregation, array $filters) } $attributesAggregation = $qb->aggregation()->nested('attributes', 'attributes'); - /** @phpstan-ignore-next-line */ foreach ($aggregation as $subAggregation) { $subAggregationObject = $this->productAttributeAggregationBuilder->build($subAggregation, $filters); if (null === $subAggregationObject || false === $subAggregationObject) { diff --git a/src/Search/Request/Aggregation/ProductOptionAggregation.php b/src/Search/Request/Aggregation/ProductOptionAggregation.php index 9c4f2e85..70684f12 100644 --- a/src/Search/Request/Aggregation/ProductOptionAggregation.php +++ b/src/Search/Request/Aggregation/ProductOptionAggregation.php @@ -27,6 +27,11 @@ public function __construct(bool $enableStockFilter) $this->enableStockFilter = $enableStockFilter; } + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @param mixed $aggregation + */ public function build($aggregation, array $filters) { /** @var ProductOptionInterface&SearchableInterface $aggregation */ diff --git a/src/Search/Request/Aggregation/ProductOptionsAggregation.php b/src/Search/Request/Aggregation/ProductOptionsAggregation.php index 7c5971e7..fed8f293 100644 --- a/src/Search/Request/Aggregation/ProductOptionsAggregation.php +++ b/src/Search/Request/Aggregation/ProductOptionsAggregation.php @@ -26,6 +26,11 @@ public function __construct(ProductOptionAggregation $productOptionAggregationBu $this->productOptionAggregationBuilder = $productOptionAggregationBuilder; } + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @param mixed $aggregation + */ public function build($aggregation, array $filters) { if (!$this->isSupport($aggregation)) { @@ -42,7 +47,6 @@ public function build($aggregation, array $filters) } $optionsAggregation = $qb->aggregation()->nested('options', 'options'); - /** @phpstan-ignore-next-line */ foreach ($aggregation as $subAggregation) { $subAggregationObject = $this->productOptionAggregationBuilder->build($subAggregation, $filters); if (null === $subAggregationObject || false === $subAggregationObject) { diff --git a/src/Search/Request/ProductRequest/InstantSearch.php b/src/Search/Request/InstantSearch.php similarity index 79% rename from src/Search/Request/ProductRequest/InstantSearch.php rename to src/Search/Request/InstantSearch.php index a64a4d87..b90bd936 100644 --- a/src/Search/Request/ProductRequest/InstantSearch.php +++ b/src/Search/Request/InstantSearch.php @@ -11,42 +11,42 @@ declare(strict_types=1); -namespace MonsieurBiz\SyliusSearchPlugin\Search\Request\ProductRequest; +namespace MonsieurBiz\SyliusSearchPlugin\Search\Request; use Elastica\Query; use Elastica\QueryBuilder; use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; use MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore\FunctionScoreInterface; use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterInterface; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface; use RuntimeException; use Sylius\Component\Registry\ServiceRegistryInterface; -final class InstantSearch implements RequestInterface +class InstantSearch implements InstantSearchInterface { - private DocumentableInterface $documentable; + protected ServiceRegistryInterface $documentableRegistry; - private ?RequestConfiguration $configuration; + protected ?RequestConfiguration $configuration; + + protected string $documentType; /** * @var iterable */ - private iterable $queryFilters; + protected iterable $queryFilters; /** * @var iterable */ - private iterable $functionScores; + protected iterable $functionScores; public function __construct( ServiceRegistryInterface $documentableRegistry, + string $documentType, iterable $queryFilters, iterable $functionScores ) { - /** @var DocumentableInterface $documentable */ - $documentable = $documentableRegistry->get('search.documentable.monsieurbiz_product'); - $this->documentable = $documentable; + $this->documentableRegistry = $documentableRegistry; + $this->documentType = $documentType; $this->queryFilters = $queryFilters; $this->functionScores = $functionScores; } @@ -58,7 +58,8 @@ public function getType(): string public function getDocumentable(): DocumentableInterface { - return $this->documentable; + /** @phpstan-ignore-next-line */ + return $this->documentableRegistry->get('search.documentable.' . $this->documentType); } public function getQuery(): Query diff --git a/src/Search/Request/InstantSearchInterface.php b/src/Search/Request/InstantSearchInterface.php new file mode 100644 index 00000000..afff24c3 --- /dev/null +++ b/src/Search/Request/InstantSearchInterface.php @@ -0,0 +1,18 @@ + + * + * 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\Search\Request; + +interface InstantSearchInterface extends RequestInterface +{ +} diff --git a/src/Search/Request/ProductRequest/Search.php b/src/Search/Request/ProductRequest/Search.php index dd98acb9..0a432d3c 100644 --- a/src/Search/Request/ProductRequest/Search.php +++ b/src/Search/Request/ProductRequest/Search.php @@ -14,132 +14,45 @@ namespace MonsieurBiz\SyliusSearchPlugin\Search\Request\ProductRequest; use Elastica\Query; -use Elastica\QueryBuilder; -use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use Elastica\Query\BoolQuery; use MonsieurBiz\SyliusSearchPlugin\Repository\ProductAttributeRepositoryInterface; use MonsieurBiz\SyliusSearchPlugin\Repository\ProductOptionRepositoryInterface; use MonsieurBiz\SyliusSearchPlugin\Search\Request\AggregationBuilder; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore\FunctionScoreInterface; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\PostFilterInterface; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterInterface; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Search as SearchRequest; use Sylius\Component\Registry\ServiceRegistryInterface; -final class Search implements RequestInterface +final class Search extends SearchRequest { - private DocumentableInterface $documentable; - - private RequestConfiguration $configuration; - private ProductAttributeRepositoryInterface $productAttributeRepository; private ProductOptionRepositoryInterface $productOptionRepository; - private AggregationBuilder $aggregationBuilder; - - /** - * @var iterable - */ - private iterable $queryFilters; - - /** - * @var iterable - */ - private iterable $postFilters; - - /** - * @var iterable - */ - private iterable $sorters; - - /** - * @var iterable - */ - private iterable $functionScores; - public function __construct( ServiceRegistryInterface $documentableRegistry, - ProductAttributeRepositoryInterface $productAttributeRepository, - ProductOptionRepositoryInterface $productOptionRepository, AggregationBuilder $aggregationBuilder, + string $documentType, iterable $queryFilters, iterable $postFilters, iterable $sorters, - iterable $functionScores + iterable $functionScores, + ProductAttributeRepositoryInterface $productAttributeRepository, + ProductOptionRepositoryInterface $productOptionRepository, ) { - /** @var DocumentableInterface $documentable */ - $documentable = $documentableRegistry->get('search.documentable.monsieurbiz_product'); - $this->documentable = $documentable; + parent::__construct( + $documentableRegistry, + $aggregationBuilder, + $documentType, + $queryFilters, + $postFilters, + $sorters, + $functionScores + ); + $this->productAttributeRepository = $productAttributeRepository; $this->productOptionRepository = $productOptionRepository; - $this->aggregationBuilder = $aggregationBuilder; - $this->queryFilters = $queryFilters; - $this->postFilters = $postFilters; - $this->sorters = $sorters; - $this->functionScores = $functionScores; - } - - public function getType(): string - { - return RequestInterface::SEARCH_TYPE; - } - - public function getDocumentable(): DocumentableInterface - { - return $this->documentable; - } - - public function setConfiguration(RequestConfiguration $configuration): void - { - $this->configuration = $configuration; - } - - public function getQuery(): Query - { - $qb = new QueryBuilder(); - - $boolQuery = $qb->query()->bool(); - foreach ($this->queryFilters as $queryFilter) { - $queryFilter->apply($boolQuery, $this->configuration); - } - - $query = Query::create($boolQuery); - $postFilter = new Query\BoolQuery(); - foreach ($this->postFilters as $postFilterApplier) { - $postFilterApplier->apply($postFilter, $this->configuration); - } - $query->setPostFilter($postFilter); - - $this->addAggregations($query, $postFilter); - - foreach ($this->sorters as $sorter) { - $sorter->apply($query, $this->configuration); - } - - /** @var Query\AbstractQuery $queryObject */ - $queryObject = $query->getQuery(); - $functionScore = $qb->query()->function_score() - ->setQuery($queryObject) - ->setBoostMode(Query\FunctionScore::BOOST_MODE_MULTIPLY) - ->setScoreMode(Query\FunctionScore::SCORE_MODE_MULTIPLY) - ; - foreach ($this->functionScores as $functionScoreClass) { - $functionScoreClass->addFunctionScore($functionScore, $this->configuration); - } - - $query->setQuery($functionScore); - - return $query; - } - - public function supports(string $type, string $documentableCode): bool - { - return $type == $this->getType() && $this->getDocumentable()->getIndexCode() == $documentableCode; } - private function addAggregations(Query $query, Query\BoolQuery $postFilter): void + protected function addAggregations(Query $query, BoolQuery $postFilter): void { $aggregations = $this->aggregationBuilder->buildAggregations( [ diff --git a/src/Search/Request/ProductRequest/Taxon.php b/src/Search/Request/ProductRequest/Taxon.php index d2c62226..b495e537 100644 --- a/src/Search/Request/ProductRequest/Taxon.php +++ b/src/Search/Request/ProductRequest/Taxon.php @@ -14,138 +14,51 @@ namespace MonsieurBiz\SyliusSearchPlugin\Search\Request\ProductRequest; use Elastica\Query; -use Elastica\QueryBuilder; -use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; use MonsieurBiz\SyliusSearchPlugin\Repository\ProductAttributeRepositoryInterface; use MonsieurBiz\SyliusSearchPlugin\Repository\ProductOptionRepositoryInterface; use MonsieurBiz\SyliusSearchPlugin\Search\Request\AggregationBuilder; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore\FunctionScoreInterface; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\PostFilterInterface; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterInterface; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Taxon as TaxonRequest; use RuntimeException; use Sylius\Component\Channel\Context\ChannelContextInterface; use Sylius\Component\Registry\ServiceRegistryInterface; -final class Taxon implements RequestInterface +final class Taxon extends TaxonRequest { - private DocumentableInterface $documentable; - private ProductAttributeRepositoryInterface $productAttributeRepository; private ProductOptionRepositoryInterface $productOptionRepository; - private ChannelContextInterface $channelContext; - - private AggregationBuilder $aggregationBuilder; - - private ?RequestConfiguration $configuration; - - /** - * @var iterable - */ - private iterable $queryFilters; - - /** - * @var iterable - */ - private iterable $postFilters; - - /** - * @var iterable - */ - private iterable $sorters; - /** - * @var iterable + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ - private iterable $functionScores; - public function __construct( ServiceRegistryInterface $documentableRegistry, - ProductAttributeRepositoryInterface $productAttributeRepository, - ProductOptionRepositoryInterface $productOptionRepository, ChannelContextInterface $channelContext, AggregationBuilder $aggregationBuilder, + string $documentType, iterable $queryFilters, iterable $postFilters, iterable $sorters, - iterable $functionScores + iterable $functionScores, + ProductAttributeRepositoryInterface $productAttributeRepository, + ProductOptionRepositoryInterface $productOptionRepository ) { - /** @var DocumentableInterface $documentable */ - $documentable = $documentableRegistry->get('search.documentable.monsieurbiz_product'); - $this->documentable = $documentable; + parent::__construct( + $documentableRegistry, + $channelContext, + $aggregationBuilder, + $documentType, + $queryFilters, + $postFilters, + $sorters, + $functionScores + ); + $this->productAttributeRepository = $productAttributeRepository; $this->productOptionRepository = $productOptionRepository; - $this->channelContext = $channelContext; - $this->aggregationBuilder = $aggregationBuilder; - $this->queryFilters = $queryFilters; - $this->postFilters = $postFilters; - $this->sorters = $sorters; - $this->functionScores = $functionScores; - } - - public function getType(): string - { - return RequestInterface::TAXON_TYPE; - } - - public function getDocumentable(): DocumentableInterface - { - return $this->documentable; - } - - public function getQuery(): Query - { - $qb = new QueryBuilder(); - - $boolQuery = $qb->query()->bool(); - foreach ($this->queryFilters as $queryFilter) { - $queryFilter->apply($boolQuery, $this->configuration); - } - - $query = Query::create($boolQuery); - $postFilter = new Query\BoolQuery(); - foreach ($this->postFilters as $postFilterApplier) { - $postFilterApplier->apply($postFilter, $this->configuration); - } - $query->setPostFilter($postFilter); - - $this->addAggregations($query, $postFilter); - - foreach ($this->sorters as $sorter) { - $sorter->apply($query, $this->configuration); - } - - /** @var Query\AbstractQuery $queryObject */ - $queryObject = $query->getQuery(); - $functionScore = $qb->query()->function_score() - ->setQuery($queryObject) - ->setBoostMode(Query\FunctionScore::BOOST_MODE_MULTIPLY) - ->setScoreMode(Query\FunctionScore::SCORE_MODE_MULTIPLY) - ; - foreach ($this->functionScores as $functionScoreClass) { - $functionScoreClass->addFunctionScore($functionScore, $this->configuration); - } - - $query->setQuery($functionScore); - - return $query; - } - - public function supports(string $type, string $documentableCode): bool - { - return RequestInterface::TAXON_TYPE === $type && $documentableCode === $this->getDocumentable()->getIndexCode(); - } - - public function setConfiguration(RequestConfiguration $configuration): void - { - $this->configuration = $configuration; } - private function addAggregations(Query $query, Query\BoolQuery $postFilter): void + protected function addAggregations(Query $query, Query\BoolQuery $postFilter): void { if (null === $this->configuration) { throw new RuntimeException('Missing request configuration'); diff --git a/src/Search/Request/QueryFilter/Product/ChannelFilter.php b/src/Search/Request/QueryFilter/ChannelFilter.php similarity index 92% rename from src/Search/Request/QueryFilter/Product/ChannelFilter.php rename to src/Search/Request/QueryFilter/ChannelFilter.php index 55c53792..1b4877f9 100644 --- a/src/Search/Request/QueryFilter/Product/ChannelFilter.php +++ b/src/Search/Request/QueryFilter/ChannelFilter.php @@ -11,11 +11,10 @@ declare(strict_types=1); -namespace MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product; +namespace MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter; use Elastica\Query\BoolQuery; use Elastica\QueryBuilder; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterInterface; use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; use Sylius\Component\Channel\Context\ChannelContextInterface; diff --git a/src/Search/Request/QueryFilter/Product/EnabledFilter.php b/src/Search/Request/QueryFilter/EnabledFilter.php similarity index 89% rename from src/Search/Request/QueryFilter/Product/EnabledFilter.php rename to src/Search/Request/QueryFilter/EnabledFilter.php index 3c5783da..8c590137 100644 --- a/src/Search/Request/QueryFilter/Product/EnabledFilter.php +++ b/src/Search/Request/QueryFilter/EnabledFilter.php @@ -11,11 +11,10 @@ declare(strict_types=1); -namespace MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product; +namespace MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter; use Elastica\Query\BoolQuery; use Elastica\QueryBuilder; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterInterface; use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; final class EnabledFilter implements QueryFilterInterface diff --git a/src/Search/Request/QueryFilter/Product/SearchTermFilter.php b/src/Search/Request/QueryFilter/Product/SearchTermFilter.php index 512d3f0b..2db2a8f8 100644 --- a/src/Search/Request/QueryFilter/Product/SearchTermFilter.php +++ b/src/Search/Request/QueryFilter/Product/SearchTermFilter.php @@ -18,55 +18,29 @@ use Elastica\QueryBuilder; use MonsieurBiz\SyliusSearchPlugin\Repository\ProductAttributeRepositoryInterface; use MonsieurBiz\SyliusSearchPlugin\Repository\ProductOptionRepositoryInterface; -use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\SearchTermFilter as BaseSearchTermFilter; use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; -final class SearchTermFilter implements QueryFilterInterface +final class SearchTermFilter extends BaseSearchTermFilter { private ProductAttributeRepositoryInterface $productAttributeRepository; private ProductOptionRepositoryInterface $productOptionRepository; - private array $fieldsToSearch; - public function __construct( ProductAttributeRepositoryInterface $productAttributeRepository, ProductOptionRepositoryInterface $productOptionRepository, array $fieldsToSearch ) { + parent::__construct($fieldsToSearch); $this->productAttributeRepository = $productAttributeRepository; $this->productOptionRepository = $productOptionRepository; - $this->fieldsToSearch = $fieldsToSearch; } - public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void + protected function addCustomFilters(BoolQuery $searchQuery, RequestConfiguration $requestConfiguration): void { - $qb = new QueryBuilder(); - - $searchCode = $qb->query()->term(['code' => $requestConfiguration->getQueryText()]); - - $searchQuery = $qb->query()->bool(); - $searchQuery->addShould($searchCode); - $this->addFieldsToSearchCondition($searchQuery, $requestConfiguration); - $this->addAttributesQueries($searchQuery, $requestConfiguration); $this->addOptionsQueries($searchQuery, $requestConfiguration); - - $boolQuery->addMust($searchQuery); - } - - private function addFieldsToSearchCondition(BoolQuery $searchQuery, RequestConfiguration $requestConfiguration): void - { - if (0 === \count($this->fieldsToSearch)) { - return; - } - $qb = new QueryBuilder(); - $nameAndDescriptionQuery = $qb->query()->multi_match(); - $nameAndDescriptionQuery->setFields($this->fieldsToSearch); - $nameAndDescriptionQuery->setQuery($requestConfiguration->getQueryText()); - $nameAndDescriptionQuery->setType(MultiMatch::TYPE_MOST_FIELDS); - $nameAndDescriptionQuery->setFuzziness(MultiMatch::FUZZINESS_AUTO); - $searchQuery->addShould($nameAndDescriptionQuery); } private function addAttributesQueries(BoolQuery $searchQuery, RequestConfiguration $requestConfiguration): void diff --git a/src/Search/Request/QueryFilter/SearchTermFilter.php b/src/Search/Request/QueryFilter/SearchTermFilter.php new file mode 100644 index 00000000..c3ef5344 --- /dev/null +++ b/src/Search/Request/QueryFilter/SearchTermFilter.php @@ -0,0 +1,67 @@ + + * + * 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\Search\Request\QueryFilter; + +use Elastica\Query\BoolQuery; +use Elastica\Query\MultiMatch; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +class SearchTermFilter implements QueryFilterInterface +{ + protected array $fieldsToSearch; + + public function __construct( + array $fieldsToSearch + ) { + $this->fieldsToSearch = $fieldsToSearch; + } + + public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void + { + $qb = new QueryBuilder(); + + $searchCode = $qb->query()->term(['code' => $requestConfiguration->getQueryText()]); + + $searchQuery = $qb->query()->bool(); + $searchQuery->addShould($searchCode); + $this->addFieldsToSearchCondition($searchQuery, $requestConfiguration); + + $this->addCustomFilters($searchQuery, $requestConfiguration); + + $boolQuery->addMust($searchQuery); + } + + protected function addFieldsToSearchCondition(BoolQuery $searchQuery, RequestConfiguration $requestConfiguration): void + { + if (0 === \count($this->fieldsToSearch)) { + return; + } + $qb = new QueryBuilder(); + $nameAndDescriptionQuery = $qb->query()->multi_match(); + $nameAndDescriptionQuery->setFields($this->fieldsToSearch); + $nameAndDescriptionQuery->setQuery($requestConfiguration->getQueryText()); + $nameAndDescriptionQuery->setType(MultiMatch::TYPE_MOST_FIELDS); + $nameAndDescriptionQuery->setFuzziness(MultiMatch::FUZZINESS_AUTO); + $searchQuery->addShould($nameAndDescriptionQuery); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function addCustomFilters(BoolQuery $searchQuery, RequestConfiguration $requestConfiguration): void + { + // Used by children classes + } +} diff --git a/src/Search/Request/Search.php b/src/Search/Request/Search.php new file mode 100644 index 00000000..773f0031 --- /dev/null +++ b/src/Search/Request/Search.php @@ -0,0 +1,142 @@ + + * + * 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\Search\Request; + +use Elastica\Query; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore\FunctionScoreInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\PostFilterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterInterface; +use Sylius\Component\Registry\ServiceRegistryInterface; + +class Search implements SearchInterface +{ + protected ServiceRegistryInterface $documentableRegistry; + + protected RequestConfiguration $configuration; + + protected AggregationBuilder $aggregationBuilder; + + protected string $documentType; + + /** + * @var iterable + */ + protected iterable $queryFilters; + + /** + * @var iterable + */ + protected iterable $postFilters; + + /** + * @var iterable + */ + protected iterable $sorters; + + /** + * @var iterable + */ + protected iterable $functionScores; + + public function __construct( + ServiceRegistryInterface $documentableRegistry, + AggregationBuilder $aggregationBuilder, + string $documentType, + iterable $queryFilters, + iterable $postFilters, + iterable $sorters, + iterable $functionScores + ) { + $this->documentableRegistry = $documentableRegistry; + $this->aggregationBuilder = $aggregationBuilder; + $this->documentType = $documentType; + $this->queryFilters = $queryFilters; + $this->postFilters = $postFilters; + $this->sorters = $sorters; + $this->functionScores = $functionScores; + } + + public function getType(): string + { + return RequestInterface::SEARCH_TYPE; + } + + public function getDocumentable(): DocumentableInterface + { + /** @phpstan-ignore-next-line */ + return $this->documentableRegistry->get('search.documentable.' . $this->documentType); + } + + public function setConfiguration(RequestConfiguration $configuration): void + { + $this->configuration = $configuration; + } + + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function getQuery(): Query + { + $qb = new QueryBuilder(); + + $boolQuery = $qb->query()->bool(); + foreach ($this->queryFilters as $queryFilter) { + $queryFilter->apply($boolQuery, $this->configuration); + } + + $query = Query::create($boolQuery); + $postFilter = new Query\BoolQuery(); + foreach ($this->postFilters as $postFilterApplier) { + $postFilterApplier->apply($postFilter, $this->configuration); + } + $query->setPostFilter($postFilter); + + $this->addAggregations($query, $postFilter); + + foreach ($this->sorters as $sorter) { + $sorter->apply($query, $this->configuration); + } + + /** @var Query\AbstractQuery $queryObject */ + $queryObject = $query->getQuery(); + $functionScore = $qb->query()->function_score() + ->setQuery($queryObject) + ->setBoostMode(Query\FunctionScore::BOOST_MODE_MULTIPLY) + ->setScoreMode(Query\FunctionScore::SCORE_MODE_MULTIPLY) + ; + foreach ($this->functionScores as $functionScoreClass) { + $functionScoreClass->addFunctionScore($functionScore, $this->configuration); + } + + $query->setQuery($functionScore); + + return $query; + } + + public function supports(string $type, string $documentableCode): bool + { + return $type == $this->getType() && $this->getDocumentable()->getIndexCode() == $documentableCode; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function addAggregations(Query $query, Query\BoolQuery $postFilter): void + { + // Used by chidlren classes + } +} diff --git a/src/Search/Request/SearchInterface.php b/src/Search/Request/SearchInterface.php new file mode 100644 index 00000000..78ae60d0 --- /dev/null +++ b/src/Search/Request/SearchInterface.php @@ -0,0 +1,18 @@ + + * + * 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\Search\Request; + +interface SearchInterface extends RequestInterface +{ +} diff --git a/src/Search/Request/Taxon.php b/src/Search/Request/Taxon.php new file mode 100644 index 00000000..86b70ae9 --- /dev/null +++ b/src/Search/Request/Taxon.php @@ -0,0 +1,152 @@ + + * + * 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\Search\Request; + +use Elastica\Query; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore\FunctionScoreInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\PostFilterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterInterface; +use RuntimeException; +use Sylius\Component\Channel\Context\ChannelContextInterface; +use Sylius\Component\Registry\ServiceRegistryInterface; + +class Taxon implements RequestInterface +{ + protected ServiceRegistryInterface $documentableRegistry; + + protected ChannelContextInterface $channelContext; + + protected string $documentType; + + protected AggregationBuilder $aggregationBuilder; + + protected ?RequestConfiguration $configuration; + + /** + * @var iterable + */ + protected iterable $queryFilters; + + /** + * @var iterable + */ + protected iterable $postFilters; + + /** + * @var iterable + */ + protected iterable $sorters; + + /** + * @var iterable + */ + protected iterable $functionScores; + + public function __construct( + ServiceRegistryInterface $documentableRegistry, + ChannelContextInterface $channelContext, + AggregationBuilder $aggregationBuilder, + string $documentType, + iterable $queryFilters, + iterable $postFilters, + iterable $sorters, + iterable $functionScores + ) { + $this->documentableRegistry = $documentableRegistry; + $this->channelContext = $channelContext; + $this->aggregationBuilder = $aggregationBuilder; + $this->documentType = $documentType; + $this->queryFilters = $queryFilters; + $this->postFilters = $postFilters; + $this->sorters = $sorters; + $this->functionScores = $functionScores; + } + + public function getType(): string + { + return RequestInterface::TAXON_TYPE; + } + + public function getDocumentable(): DocumentableInterface + { + /** @phpstan-ignore-next-line */ + return $this->documentableRegistry->get('search.documentable.' . $this->documentType); + } + + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function getQuery(): Query + { + if (!($configuration = $this->configuration)) { + throw new RuntimeException('Configuration is not set'); + } + + $qb = new QueryBuilder(); + + $boolQuery = $qb->query()->bool(); + foreach ($this->queryFilters as $queryFilter) { + $queryFilter->apply($boolQuery, $configuration); + } + + $query = Query::create($boolQuery); + $postFilter = new Query\BoolQuery(); + foreach ($this->postFilters as $postFilterApplier) { + $postFilterApplier->apply($postFilter, $configuration); + } + $query->setPostFilter($postFilter); + + $this->addAggregations($query, $postFilter); + + foreach ($this->sorters as $sorter) { + $sorter->apply($query, $configuration); + } + + /** @var Query\AbstractQuery $queryObject */ + $queryObject = $query->getQuery(); + $functionScore = $qb->query()->function_score() + ->setQuery($queryObject) + ->setBoostMode(Query\FunctionScore::BOOST_MODE_MULTIPLY) + ->setScoreMode(Query\FunctionScore::SCORE_MODE_MULTIPLY) + ; + foreach ($this->functionScores as $functionScoreClass) { + $functionScoreClass->addFunctionScore($functionScore, $configuration); + } + + $query->setQuery($functionScore); + + return $query; + } + + public function supports(string $type, string $documentableCode): bool + { + return $type == $this->getType() && $this->getDocumentable()->getIndexCode() == $documentableCode; + } + + public function setConfiguration(RequestConfiguration $configuration): void + { + $this->configuration = $configuration; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function addAggregations(Query $query, Query\BoolQuery $postFilter): void + { + // Used by children classes + } +} diff --git a/src/Search/Request/TaxonInterface.php b/src/Search/Request/TaxonInterface.php new file mode 100644 index 00000000..e6d082bd --- /dev/null +++ b/src/Search/Request/TaxonInterface.php @@ -0,0 +1,18 @@ + + * + * 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\Search\Request; + +interface TaxonInterface extends RequestInterface +{ +} diff --git a/src/Search/Response.php b/src/Search/Response.php index 78072012..399b7fdc 100644 --- a/src/Search/Response.php +++ b/src/Search/Response.php @@ -82,6 +82,9 @@ public function getDocumentable(): DocumentableInterface return $this->documentable; } + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ private function buildFilters(): void { /** @var ResultSet $results */ diff --git a/src/Search/Response/FilterBuilders/Product/AttributeFilterBuilder.php b/src/Search/Response/FilterBuilders/Product/AttributeFilterBuilder.php index 6f91952e..be3957c3 100644 --- a/src/Search/Response/FilterBuilders/Product/AttributeFilterBuilder.php +++ b/src/Search/Response/FilterBuilders/Product/AttributeFilterBuilder.php @@ -20,6 +20,9 @@ class AttributeFilterBuilder implements FilterBuilderInterface { + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ public function build( DocumentableInterface $documentable, RequestConfiguration $requestConfiguration, diff --git a/src/Search/Response/FilterBuilders/Product/MainTaxonFilterBuilder.php b/src/Search/Response/FilterBuilders/Product/MainTaxonFilterBuilder.php index d3c43cb2..71c5710c 100644 --- a/src/Search/Response/FilterBuilders/Product/MainTaxonFilterBuilder.php +++ b/src/Search/Response/FilterBuilders/Product/MainTaxonFilterBuilder.php @@ -20,6 +20,9 @@ class MainTaxonFilterBuilder implements FilterBuilderInterface { + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ public function build( DocumentableInterface $documentable, RequestConfiguration $requestConfiguration, diff --git a/src/Search/Response/FilterBuilders/Product/OptionFilterBuilder.php b/src/Search/Response/FilterBuilders/Product/OptionFilterBuilder.php index 1d12c43c..252a3e2f 100644 --- a/src/Search/Response/FilterBuilders/Product/OptionFilterBuilder.php +++ b/src/Search/Response/FilterBuilders/Product/OptionFilterBuilder.php @@ -20,6 +20,9 @@ class OptionFilterBuilder implements FilterBuilderInterface { + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ public function build( DocumentableInterface $documentable, RequestConfiguration $requestConfiguration, diff --git a/src/Search/Response/FilterBuilders/Product/PriceFilterBuilder.php b/src/Search/Response/FilterBuilders/Product/PriceFilterBuilder.php index edb34c4f..f1a47828 100644 --- a/src/Search/Response/FilterBuilders/Product/PriceFilterBuilder.php +++ b/src/Search/Response/FilterBuilders/Product/PriceFilterBuilder.php @@ -20,6 +20,9 @@ class PriceFilterBuilder implements FilterBuilderInterface { + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ public function build( DocumentableInterface $documentable, RequestConfiguration $requestConfiguration,