diff --git a/.github/workflows/composer.yml b/.github/workflows/composer.yml deleted file mode 100644 index f6a269e0..00000000 --- a/.github/workflows/composer.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: PHP Composer - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - schedule: - - cron: '0 0 * * *' - -jobs: - build: - - runs-on: ubuntu-latest - - strategy: - matrix: - php: [7.2, 7.3, 7.4, 8.0] - - steps: - - uses: actions/checkout@v2 - - - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - - - name: Validate composer.json and composer.lock - run: composer validate - - - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest diff --git a/.github/workflows/recipe.yaml b/.github/workflows/recipe.yaml new file mode 100644 index 00000000..fcc2d0e1 --- /dev/null +++ b/.github/workflows/recipe.yaml @@ -0,0 +1,83 @@ +name: Flex Recipe + +on: + push: + branches: [ master ] + pull_request: + +jobs: + + recipe: + + runs-on: ubuntu-latest + + env: + SYMFONY_ENDPOINT: http://127.0.0.1/ + + strategy: + fail-fast: false + matrix: + php: ['7.4' ,'8.0'] + sylius: ["~1.9.0", "~1.10.0"] + exclude: + - php: 8.0 + sylius: "~1.9.0" + + steps: + - name: Setup PHP + run: | + sudo update-alternatives --set php /usr/bin/php${{ matrix.php }} + echo "date.timezone=UTC" >> /tmp/timezone.ini + sudo mv /tmp/timezone.ini /etc/php/${{ matrix.php }}/cli/conf.d/timezone.ini + echo ${{ matrix.php }} > .php-version + + - uses: actions/checkout@v2 + with: + path: plugin + + # Run the server at the start so it can download the recipes! + - name: Run standalone symfony flex server + run: | + echo ${{ github.token }} | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin + docker run --rm --name flex -d -v $PWD/plugin/recipes:/var/www/flex/var/repo/private/monsieurbiz/sylius-search-plugin -p 80:80 docker.pkg.github.com/monsieurbiz/docker/symfony-flex-server:latest contrib official + docker ps + + - run: mkdir -p /home/runner/{.composer/cache,.config/composer} + + - uses: actions/cache@v1 + id: cache-composer + with: + path: /home/runner/.composer/cache + key: composer2-php:${{ matrix.php }}-sylius:${{ matrix.sylius }}-${{ github.sha }} + restore-keys: composer2-php:${{ matrix.php }}-sylius:${{ matrix.sylius }}- + + - name: Composer v2 + run: sudo composer self-update --2 + + - name: Composer Github Auth + run: composer config -g github-oauth.github.com ${{ github.token }} + + - name: Create Sylius-Standard project without install + run: | + composer create-project --prefer-dist --no-scripts --no-progress --no-install sylius/sylius-standard sylius "${{ matrix.sylius }}" + + - name: Setup some requirements + working-directory: ./sylius + run: | + composer config repositories.plugin '{"type": "path", "url": "../plugin/"}' + composer config extra.symfony.allow-contrib true + composer config secure-http false + composer config --unset platform.php + + - name: Require plugin without install + working-directory: ./sylius + run: | + composer require --no-install --no-update monsieurbiz/sylius-search-plugin="*@dev" + + - name: Composer install + working-directory: ./sylius + run: | + composer install + + - name: Show flex server logs + run: docker logs --tail 100 flex diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml new file mode 100644 index 00000000..1cf94aa4 --- /dev/null +++ b/.github/workflows/security.yaml @@ -0,0 +1,48 @@ +name: Security + +on: + push: + pull_request: + +jobs: + + security: + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0'] + + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + run: | + sudo update-alternatives --set php /usr/bin/php${{ matrix.php }} + echo "date.timezone=UTC" | sudo tee /etc/php/${{ matrix.php }}/cli/conf.d/timezone.ini + echo "${{ matrix.php }}" > .php-version + + - uses: actions/cache@v1 + id: cache-composer + with: + path: /home/runner/.composer/cache + key: composer2-php:${{ matrix.php }}-${{ github.sha }} + restore-keys: composer2-php:${{ matrix.php }}- + + - run: mkdir -p /home/runner/{.composer/cache,.config/composer} + if: steps.cache-composer.outputs.cache-hit != 'true' + + - name: Composer v2 + run: sudo composer self-update --2 + + - name: Composer Github Auth + run: composer config -g github-oauth.github.com ${{ github.token }} + + - uses: actions/checkout@v2 + + - name: Install PHP dependencies + run: composer update --prefer-dist + + - uses: symfonycorp/security-checker-action@v2 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 00000000..46a6a2e3 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,77 @@ +name: Tests + +on: + push: + branches: [ master ] + pull_request: + +jobs: + + php: + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0'] + + env: + SYMFONY_ARGS: --no-tls + COMPOSER_ARGS: --prefer-dist + DOCKER_INTERACTIVE_ARGS: -t + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: Setup PHP + run: | + sudo update-alternatives --set php /usr/bin/php${{ matrix.php }} + echo "date.timezone=UTC" | sudo tee /etc/php/${{ matrix.php }}/cli/conf.d/timezone.ini + echo "${{ matrix.php }}" > .php-version + + - name: Install symfony CLI + run: | + curl https://get.symfony.com/cli/installer | bash + echo "${HOME}/.symfony/bin" >> $GITHUB_PATH + + - uses: actions/cache@v1 + id: cache-composer + with: + path: /home/runner/.composer/cache + key: composer2-php:${{ matrix.php }}-${{ github.sha }} + restore-keys: composer2-php:${{ matrix.php }}- + + - run: mkdir -p /home/runner/{.composer/cache,.config/composer} + if: steps.cache-composer.outputs.cache-hit != 'true' + + - name: Composer v2 + run: sudo composer self-update --2 + + - name: Composer Github Auth + run: composer config -g github-oauth.github.com ${{ github.token }} + + - run: make install + + - run: make test.composer + + - run: make test.phpcs + + - run: make test.phpunit + + - run: make test.phpstan + + - run: make test.phpmd + + - run: make test.phpspec + + - run: make test.yaml + + - run: make test.twig + + - run: make test.schema + + - run: make test.container diff --git a/.gitignore b/.gitignore index 1760897e..e184ceaa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,19 @@ /vendor/ -/node_modules/ /composer.lock +/symfony.lock /etc/build/* !/etc/build/.gitignore -/tests/Application/yarn.lock +/tests/Application /behat.yml /phpspec.yml /package-lock.json -/.php_cs.cache /.php-version +/php.ini /.phpunit.result.cache /node_modules - +/yarn.lock diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 00000000..a0b260d9 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,265 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +$header = <<<'HEADER' +This file is part of Monsieur Biz' Search plugin for Sylius. + +(c) Monsieur Biz + +For the full copyright and license information, please view the LICENSE.txt +file that was distributed with this source code. +HEADER; + +$finder = PhpCsFixer\Finder::create() + ->in(__DIR__) + ->exclude( + [ + 'tests/Application', + ] + ) +; + +$config = new PhpCsFixer\Config(); +$config + ->setRiskyAllowed(true) + ->setRules([ + '@DoctrineAnnotation' => true, + '@PHP71Migration' => true, + '@PHP71Migration:risky' => true, + '@PHPUnit60Migration:risky' => true, + '@Symfony' => true, + '@Symfony:risky' => true, + 'align_multiline_comment' => [ + 'comment_type' => 'phpdocs_like', + ], + 'array_indentation' => true, + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'binary_operator_spaces' => true, + 'blank_line_after_opening_tag' => true, + 'blank_line_after_namespace' => true, + 'blank_line_before_statement' => true, + 'braces' => [ + 'allow_single_line_closure' => true, + ], + 'cast_spaces' => true, + 'class_attributes_separation' => true, + 'class_definition' => [ + 'single_item_single_line' => true, + 'multi_line_extends_each_single_line' => true, + ], + 'combine_consecutive_issets' => true, + 'combine_consecutive_unsets' => true, + 'comment_to_phpdoc' => true, + 'compact_nullable_typehint' => true, + 'concat_space' => [ + 'spacing' => 'one', + ], + 'constant_case' => [ + 'case' => 'lower', + ], + 'declare_equal_normalize' => true, + 'dir_constant' => true, + 'declare_strict_types' => true, + 'doctrine_annotation_array_assignment' => [ + 'operator' => '=', + ], + 'doctrine_annotation_spaces' => [ + 'after_array_assignments_equals' => false, + 'before_array_assignments_equals' => false, + ], + 'elseif' => true, + 'encoding' => true, + 'ereg_to_preg' => true, + 'error_suppression' => true, + 'explicit_indirect_variable' => true, + 'full_opening_tag' => true, + 'fully_qualified_strict_types' => true, + 'function_declaration' => true, + 'function_to_constant' => true, + 'function_typehint_space' => true, + 'general_phpdoc_tag_rename' => true, + 'header_comment' => [ + 'header' => $header, + 'location' => 'after_open', + ], + 'include' => true, + 'increment_style' => [ + 'style' => 'pre', + ], + 'indentation_type' => true, + 'is_null' => true, + 'line_ending' => true, + 'list_syntax' => [ + 'syntax' => 'short', + ], + 'logical_operators' => true, + 'lowercase_cast' => true, + 'lowercase_keywords' => true, + 'lowercase_static_reference' => true, + 'magic_constant_casing' => true, + 'method_argument_space' => true, + 'modernize_types_casting' => true, + 'multiline_comment_opening_closing' => true, + 'multiline_whitespace_before_semicolons' => [ + 'strategy' => 'new_line_for_chained_calls', + ], + 'native_constant_invocation' => true, + 'native_function_casing' => true, + 'new_with_braces' => true, + 'no_alias_functions' => true, + 'no_alternative_syntax' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_break_comment' => true, + 'no_closing_tag' => true, + 'no_empty_comment' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_extra_blank_lines' => [ + 'tokens' => [ + 'break', + 'case', + 'continue', + 'curly_brace_block', + 'default', + 'extra', + 'parenthesis_brace_block', + 'return', + 'square_brace_block', + 'switch', + 'throw', + 'use', + ], + ], + 'no_homoglyph_names' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_mixed_echo_print' => [ + 'use' => 'echo', + ], + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_null_property_initialization' => true, + 'no_php4_constructor' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_short_bool_cast' => true, + 'no_spaces_after_function_name' => true, + 'no_spaces_around_offset' => true, + 'no_spaces_inside_parenthesis' => true, + 'no_superfluous_elseif' => true, + 'no_superfluous_phpdoc_tags' => [ + 'allow_mixed' => true, + ], + 'no_unset_cast' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unneeded_curly_braces' => true, + 'no_unneeded_final_method' => true, + 'no_unset_on_property' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'no_trailing_comma_in_list_call' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'non_printable_character' => true, + 'normalize_index_brace' => true, + 'object_operator_without_whitespace' => true, + 'ordered_imports' => [ + 'imports_order' => [ + 'class', + 'function', + 'const', + ], + 'sort_algorithm' => 'alpha', + ], + 'php_unit_dedicate_assert' => true, + 'php_unit_fqcn_annotation' => true, + 'php_unit_method_casing' => [ + 'case' => 'camel_case', + ], + 'php_unit_set_up_tear_down_visibility' => true, + 'php_unit_test_annotation' => [ + 'style' => 'prefix', + ], + 'phpdoc_align' => [ + 'align' => 'left', + ], + 'phpdoc_add_missing_param_annotation' => [ + 'only_untyped' => true, + ], + 'phpdoc_indent' => true, + 'phpdoc_inline_tag_normalizer' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_alias_tag' => true, + 'phpdoc_no_empty_return' => true, + 'phpdoc_no_package' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_order' => true, + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_tag_type' => true, + 'phpdoc_to_comment' => false, + 'phpdoc_trim' => true, + 'phpdoc_trim_consecutive_blank_line_separation' => true, + 'phpdoc_types' => true, + 'phpdoc_types_order' => [ + 'null_adjustment' => 'always_last', + 'sort_algorithm' => 'none', + ], + 'phpdoc_var_annotation_correct_order' => true, + 'phpdoc_var_without_name' => true, + 'pow_to_exponentiation' => true, + 'protected_to_private' => true, + 'return_assignment' => true, + 'return_type_declaration' => true, + 'self_accessor' => true, + 'short_scalar_cast' => true, + 'single_blank_line_at_eof' => true, + 'single_blank_line_before_namespace' => true, + 'single_class_element_per_statement' => true, + 'single_import_per_statement' => true, + 'single_line_after_imports' => true, + 'single_line_comment_style' => true, + 'single_quote' => true, + 'space_after_semicolon' => true, + 'standardize_not_equals' => true, + 'strict_param' => true, + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'ternary_operator_spaces' => true, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline' => [ + 'elements' => ['arrays'], + ], + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'visibility_required' => [ + 'elements' => [ + 'const', + 'property', + 'method', + ], + ], + 'void_return' => true, + 'whitespace_after_comma_in_array' => true, // alerady in symfony set + ]) + ->setFinder($finder) +; + +return $config; diff --git a/.php-version.dist b/.php-version.dist new file mode 100644 index 00000000..cc40bca6 --- /dev/null +++ b/.php-version.dist @@ -0,0 +1 @@ +8.0 diff --git a/.php_cs.dist b/.php_cs.dist deleted file mode 100644 index b46f7ed1..00000000 --- a/.php_cs.dist +++ /dev/null @@ -1,122 +0,0 @@ - - -For the full copyright and license information, please view the LICENSE -file that was distributed with this source code. -HEADER; - -$finder = PhpCsFixer\Finder::create() - ->in(__DIR__) - ->exclude('tests/Application/var') - ->exclude('tests/Application/src/Migrations') - ->exclude('src/generated') - ->append([ - 'tests/Application/bin/console', - ]); - -return PhpCsFixer\Config::create() - ->setRiskyAllowed(true) - ->setRules([ - '@DoctrineAnnotation' => true, - '@PHP71Migration' => true, - '@PHP71Migration:risky' => true, - '@PHPUnit60Migration:risky' => true, - '@Symfony' => true, - '@Symfony:risky' => true, - 'align_multiline_comment' => [ - 'comment_type' => 'phpdocs_like', - ], - 'array_indentation' => true, - 'array_syntax' => [ - 'syntax' => 'short', - ], - 'comment_to_phpdoc' => true, - 'compact_nullable_typehint' => true, - 'concat_space' => [ - 'spacing' => 'one', - ], - 'doctrine_annotation_array_assignment' => [ - 'operator' => '=', - ], - 'doctrine_annotation_spaces' => [ - 'after_array_assignments_equals' => false, - 'before_array_assignments_equals' => false, - ], - 'explicit_indirect_variable' => true, - 'fully_qualified_strict_types' => true, - 'function_declaration' => [ - 'closure_function_spacing' => 'none', - ], - 'header_comment' => [ - 'header' => $header, - 'location' => 'after_open', - ], - 'logical_operators' => true, - 'multiline_comment_opening_closing' => true, - 'multiline_whitespace_before_semicolons' => [ - 'strategy' => 'new_line_for_chained_calls', - ], - 'no_alternative_syntax' => true, - 'no_extra_blank_lines' => [ - 'tokens' => [ - 'break', - 'continue', - 'curly_brace_block', - 'extra', - 'parenthesis_brace_block', - 'return', - 'square_brace_block', - 'throw', - 'use', - ], - ], - 'no_superfluous_elseif' => true, - 'no_superfluous_phpdoc_tags' => false, - 'no_unset_cast' => true, - 'no_unset_on_property' => true, - 'no_useless_else' => true, - 'no_useless_return' => true, - 'ordered_imports' => [ - 'imports_order' => [ - 'class', - 'function', - 'const', - ], - 'sort_algorithm' => 'alpha', - ], - 'php_unit_method_casing' => [ - 'case' => 'camel_case', - ], - 'php_unit_set_up_tear_down_visibility' => true, - 'php_unit_test_annotation' => [ - 'style' => 'prefix', - ], - 'phpdoc_align' => [ - 'align' => 'left', - ], - 'phpdoc_add_missing_param_annotation' => [ - 'only_untyped' => true, - ], - 'phpdoc_order' => true, - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_to_comment' => false, - 'phpdoc_trim_consecutive_blank_line_separation' => true, - 'phpdoc_var_annotation_correct_order' => true, - 'return_assignment' => true, - 'strict_param' => true, - 'visibility_required' => [ - 'elements' => [ - 'const', - 'method', - 'property', - ], - ], - 'void_return' => true, - ]) - ->setFinder($finder); diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 00000000..9971d62e --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,15 @@ +## Node + +Be sure you have node 14 on your machine. You can use NVM to easily switch versions. + +# Docker + +Be sure you have docker on your machine. + +# Symfony + +Be sure you have the Symfony binary on your machine. + +``` + curl -sS https://get.symfony.com/cli/installer | bash + ``` diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 06c219d8..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Monsieur Biz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..1b1809b8 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright 2021 Monsieur Biz + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..b0cb7cc3 --- /dev/null +++ b/Makefile @@ -0,0 +1,203 @@ +.DEFAULT_GOAL := help +SHELL=/bin/bash +APP_DIR=tests/Application +SYLIUS_VERSION=1.10 +SYMFONY=cd ${APP_DIR} && symfony +COMPOSER=symfony composer +CONSOLE=${SYMFONY} console +export COMPOSE_PROJECT_NAME=search +PLUGIN_NAME=sylius-${COMPOSE_PROJECT_NAME}-plugin +COMPOSE=docker-compose +YARN=yarn + +### +### DEVELOPMENT +### ¯¯¯¯¯¯¯¯¯¯¯ + +install: application platform sylius es.reindex ## Install the plugin +.PHONY: install + +up: docker.up server.start ## Up the project (start docker, start symfony server) +stop: server.stop docker.stop ## Stop the project (stop docker, stop symfony server) +down: server.stop docker.down ## Down the project (removes docker containers, stop symfony server) + +reset: ## Stop docker and remove dependencies + ${MAKE} docker.down || true + rm -rf ${APP_DIR}/node_modules ${APP_DIR}/yarn.lock + rm -rf ${APP_DIR} + rm -rf vendor composer.lock +.PHONY: reset + +dependencies: composer.lock node_modules ## Setup the dependencies +.PHONY: dependencies + +.php-version: .php-version.dist + cp .php-version.dist .php-version + +php.ini: php.ini.dist + cp php.ini.dist php.ini + +composer.lock: composer.json + ${COMPOSER} install --no-scripts --no-plugins + +yarn.install: ${APP_DIR}/yarn.lock + +${APP_DIR}/yarn.lock: + ln -sf ${APP_DIR}/node_modules node_modules + cd ${APP_DIR} && ${YARN} install && ${YARN} build + ${YARN} install + ${YARN} encore prod + +node_modules: ${APP_DIR}/node_modules ## Install the Node dependencies using yarn + +${APP_DIR}/node_modules: yarn.install + +### +### TEST APPLICATION +### ¯¯¯¯¯ + +application: .php-version php.ini ${APP_DIR} setup_application ${APP_DIR}/docker-compose.yaml + +${APP_DIR}: + (${COMPOSER} create-project --prefer-dist --no-scripts --no-progress --no-install sylius/sylius-standard="${SYLIUS_VERSION}" ${APP_DIR}) + +setup_application: + rm -f ${APP_DIR}/yarn.lock + (cd ${APP_DIR} && ${COMPOSER} config repositories.plugin '{"type": "path", "url": "../../"}') + (cd ${APP_DIR} && ${COMPOSER} config extra.symfony.allow-contrib true) + (cd ${APP_DIR} && ${COMPOSER} config minimum-stability dev) + (cd ${APP_DIR} && ${COMPOSER} require --no-scripts --no-progress --no-install --no-update monsieurbiz/${PLUGIN_NAME}="*@dev") + $(MAKE) apply_dist ${APP_DIR}/.php-version ${APP_DIR}/php.ini + (cd ${APP_DIR} && ${COMPOSER} install) + +${APP_DIR}/docker-compose.yaml: + rm -f ${APP_DIR}/docker-compose.yml + rm -f ${APP_DIR}/docker-compose.yaml + cp docker-compose.yaml.dist ${APP_DIR}/docker-compose.yaml +.PHONY: ${APP_DIR}/docker-compose.yaml + +${APP_DIR}/.php-version: .php-version + (cd ${APP_DIR} && ln -sf ../../.php-version) + +${APP_DIR}/php.ini: php.ini + (cd ${APP_DIR} && ln -sf ../../php.ini) + +apply_dist: + ROOT_DIR=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))); \ + for i in `cd dist && find . -type f`; do \ + FILE_PATH=`echo $$i | sed 's|./||'`; \ + FOLDER_PATH=`dirname $$FILE_PATH`; \ + echo $$FILE_PATH; \ + (cd ${APP_DIR} && rm -f $$FILE_PATH); \ + (cd ${APP_DIR} && mkdir -p $$FOLDER_PATH); \ + (cd ${APP_DIR} && ln -s $$ROOT_DIR/dist/$$FILE_PATH $$FILE_PATH); \ + done +## Specific because symlink is not used correctly in Github Actions + rm ${APP_DIR}/docker/elasticsearch/Dockerfile + cp dist/docker/elasticsearch/Dockerfile ${APP_DIR}/docker/elasticsearch/ + +### +### TESTS +### ¯¯¯¯¯ + +test.all: test.composer test.phpstan test.phpmd test.phpunit test.phpspec test.phpcs test.yaml test.schema test.twig test.container ## Run all tests in once + +test.composer: ## Validate composer.json + ${COMPOSER} validate --strict + +test.phpstan: ## Run PHPStan + ${COMPOSER} phpstan || true + +test.phpmd: ## Run PHPMD + ${COMPOSER} phpmd || true + +test.phpunit: ## Run PHPUnit + ${COMPOSER} phpunit + +test.phpspec: ## Run PHPSpec + ${COMPOSER} phpspec + +test.phpcs: ## Run PHP CS Fixer in dry-run + ${COMPOSER} run -- phpcs --dry-run -v + +test.phpcs.fix: ## Run PHP CS Fixer and fix issues if possible + ${COMPOSER} run -- phpcs -v + +test.container: ## Lint the symfony container + ${CONSOLE} lint:container + +test.yaml: ## Lint the symfony Yaml files + ${CONSOLE} lint:yaml ../../recipes ../../src/Resources/config --parse-tags + +test.schema: ## Validate MySQL Schema + ${CONSOLE} doctrine:schema:validate + +test.twig: ## Validate Twig templates + ${CONSOLE} lint:twig --no-debug templates/ ../../src/Resources/views/ + +### +### SYLIUS +### ¯¯¯¯¯¯ + +sylius: dependencies sylius.database sylius.fixtures sylius.assets ## Install Sylius +.PHONY: sylius + +sylius.database: ## Setup the database + ${CONSOLE} doctrine:database:drop --if-exists --force + ${CONSOLE} doctrine:database:create --if-not-exists + ${CONSOLE} doctrine:migration:migrate -n + +sylius.fixtures: ## Run the fixtures + ${CONSOLE} sylius:fixtures:load -n default + +sylius.assets: ## Install all assets with symlinks + ${CONSOLE} assets:install --symlink + ${CONSOLE} sylius:install:assets + ${CONSOLE} sylius:theme:assets:install --symlink + +### +### PLATFORM +### ¯¯¯¯¯¯¯¯ + +platform: .php-version up ## Setup the platform tools +.PHONY: platform + +docker.pull: ## Pull the docker images + cd ${APP_DIR} && ${COMPOSE} pull + +docker.up: ## Start the docker containers + cd ${APP_DIR} && ${COMPOSE} up -d +.PHONY: docker.up + +docker.stop: ## Stop the docker containers + cd ${APP_DIR} && ${COMPOSE} stop +.PHONY: docker.stop + +docker.down: ## Stop and remove the docker containers + cd ${APP_DIR} && ${COMPOSE} down +.PHONY: docker.down + +docker.logs: ## Logs the docker containers + cd ${APP_DIR} && ${COMPOSE} logs -f +.PHONY: docker.logs + +server.start: ## Run the local webserver using Symfony + ${SYMFONY} local:server:start -d + +server.stop: ## Stop the local webserver + ${SYMFONY} local:server:stop + +es.reindex: ## Reindex elasticsearch + ${CONSOLE} monsieurbiz:search:populate + +### +### HELP +### ¯¯¯¯ + +help: SHELL=/bin/bash +help: ## Dislay this help + @IFS=$$'\n'; for line in `grep -h -E '^[a-zA-Z_#-]+:?.*?##.*$$' $(MAKEFILE_LIST)`; do if [ "$${line:0:2}" = "##" ]; then \ + echo $$line | awk 'BEGIN {FS = "## "}; {printf "\033[33m %s\033[0m\n", $$2}'; else \ + echo $$line | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m%s\n", $$1, $$2}'; fi; \ + done; unset IFS; +.PHONY: help diff --git a/README.md b/README.md index 86c5d253..d6e0eb4a 100644 --- a/README.md +++ b/README.md @@ -15,501 +15,116 @@ [![Search Plugin license](https://img.shields.io/github/license/monsieurbiz/SyliusSearchPlugin?public)](https://github.com/monsieurbiz/SyliusSearchPlugin/blob/master/LICENSE.txt) [![Build Status](https://img.shields.io/github/workflow/status/monsieurbiz/SyliusSearchPlugin/PHP%20Composer)](https://github.com/monsieurbiz/SyliusSearchPlugin/actions?query=workflow%3A%22PHP+Composer%22) -A search plugin for Sylius using [Jane](https://github.com/janephp/janephp) and [Elastically](https://github.com/jolicode/elastically). - -## Features - -### Search with filters, sort and limits - -![Search results](screenshot_search.jpg) - -### Taxon view with filters, sort and limits - -![Taxon view](screenshot_taxon.jpg) - - -### Instant search while you're typing - -![Instant search](screenshot_instant.jpg) +A search plugin for Sylius using [Elastically](https://github.com/jolicode/elastically) and [Jane](https://github.com/janephp/janephp). ## Installation -Require the plugin : -`composer require monsieurbiz/sylius-search-plugin="^1.0@RC"` - -> If you are using Symfony Flex, the recipe will automatically do the actions below. - -Then create the config file in `config/packages/monsieurbiz_search_plugin.yaml` with the [default configuration](#configuration). - -Import routes in `config/routes.yaml` : -```yaml -monsieurbiz_search_plugin: - resource: "@MonsieurBizSyliusSearchPlugin/Resources/config/routing.yaml" -``` - -Modify `config/bundles.php` to add this line at the end : +Require the plugin : ``` - MonsieurBiz\SyliusSearchPlugin\MonsieurBizSyliusSearchPlugin::class => ['all' => true], +composer require monsieurbiz/sylius-search-plugin="2.0.x-dev@dev" ``` -Finally configure plugin in your `.env` file by adding these lines at the end : -``` -###> MonsieurBizSearchPlugin ### -MONSIEURBIZ_SEARCHPLUGIN_ES_HOST=localhost -MONSIEURBIZ_SEARCHPLUGIN_ES_PORT=9200 -###< MonsieurBizSearchPlugin ### -``` - -## Installation - -1. Install Elasticsearch 💪. See [Infrastructure](#infrastructure) below. -2. Your `Product` entity needs to implement the [DocumentableInterface](#documentable-objects) interface and use the `\MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableProductTrait` trait. - -2. Your `ProductAttribute` and `ProductOption` entities need to implement the `\MonsieurBiz\SyliusSearchPlugin\Entity\Product\FilterableInterface` interface and use the `\MonsieurBiz\SyliusSearchPlugin\Model\Product\FilterableTrait` trait. - -3. You need to run a diff of your doctrine's migrations: `console doctrine:migrations:diff`. Don't forget to run it! (`console doctrine:migrations:migrate`) - -4. Copy the templates: (we update the `ProductAttribute` and `ProductOption` forms) - - ```bash - mkdir -p templates/bundles/SyliusAdminBundle - cp -Rv vendor/monsieurbiz/sylius-search-plugin/src/Resources/SyliusAdminBundle/views/ templates/bundles/SyliusAdminBundle/ - ``` +If you are using Symfony Flex, the recipe will automatically do some actions. -5. Run the [populate command](#Command). +
+For the installation without flex, follow these additional steps +

-## Infrastructure - -The plugin was developed for Elasticsearch 7.2.x versions. -You need to have `analysis-icu` and `analysis-phonetic` elasticsearch plugin installed. - -### Development - -Elasticsearch is available on `9200` port : http://127.0.0.1:9200/ -Cerebro on port `9000` : http://127.0.0.1:9000/#/overview?host=http:%2F%2Felasticsearch:9200 -Kibana on port `5601` : http://127.0.0.1:5601/ - -On your machine, Elasticsearch is available at http://127.0.0.1:9200/ -In docker, Elasticsearch is available at http://elasticsearch:9200/ -This is the second URL you have to put on Cerebro, Kibana and Elasticsearch if you want to connect to the cluster. - -For a development infrastructure with docker, you can check the [Monsieur Biz Sylius infra](https://github.com/monsieurbiz/sylius-infra/) +Change your `config/bundles.php` file to add this line for the plugin declaration: +```php + ['all' => true], + Jane\Bundle\AutoMapperBundle\JaneAutoMapperBundle::class => ['all' => true], +]; +``` -The default module configuration is : +Create the config file in `config/packages/monsieurbiz_sylius_search_plugin.yaml`: ```yaml imports: - - { resource: "@MonsieurBizSyliusSearchPlugin/Resources/config/config.yaml" } - -monsieur_biz_sylius_search: - files: - search: '%kernel.project_dir%/vendor/monsieurbiz/sylius-search-plugin/src/Resources/config/elasticsearch/queries/search.yaml' - instant: '%kernel.project_dir%/vendor/monsieurbiz/sylius-search-plugin/src/Resources/config/elasticsearch/queries/instant.yaml' - taxon: '%kernel.project_dir%/vendor/monsieurbiz/sylius-search-plugin/src/Resources/config/elasticsearch/queries/taxon.yaml' - documentable_classes : - - 'App\Entity\Product\Product' - grid: - limits: - taxon: [9, 18, 27] - search: [9, 18, 27] - default_limit: - taxon: 9 - search: 9 - instant: 10 - sorting: - taxon: ['name', 'price', 'created_at'] - search: ['name', 'price', 'created_at'] - filters: - apply_manually: false # Will refresh the filters depending on applied filters after you apply it manually - use_main_taxon: true # Use main taxon for the taxon filter, else use the taxons - + - { resource: "@MonsieurBizSyliusSearchPlugin/Resources/config/config.yaml" } ``` -You can customize it in `config/packages/monsieurbiz_sylius_search_plugin.yaml`. - -`monsieur_biz_sylius_search.files.search` is the query used to perform the search. -`monsieur_biz_sylius_search.files.instant` is the query used to perform the instant search. -`monsieur_biz_sylius_search.files.taxon` is the query used to perform the taxon view. - -The `{{QUERY}}` string inside is replaced in PHP by the query typed by the user. - -`documentable_classes` is an array of entities which can be indexed in Elasticsearch. +Create the route config file in `config/routes/monsieurbiz_sylius_search_plugin.yaml`: -You can also change available sortings and limits. - -You can decide to load filters before their application or after : ```yaml -monsieur_biz_sylius_search: - grid: - filters: - apply_manually: false # Will refresh the filters depending on applied filters after you apply it manually +monsieurbiz_search_plugin: + resource: "@MonsieurBizSyliusSearchPlugin/Resources/config/routing.yaml" ``` -For example, if you choose a `L` size and the config is `true`, you will have only filters available for this size. -You will also have a button to apply the desired filters : - -![Submit filters button](submit_filters.png) - +Copy the override templates: -If it's set to false, you will have all available filters for the products and they will be applied on each change -automatically. - - -You can decide to use the `Categories` filter with main taxon or taxons : -```yaml -monsieur_biz_sylius_search: - grid: - filters: - use_main_taxon: true # Use main taxon for the taxon filter, else use the taxons +```shell +cp -Rv vendor/monsieurbiz/sylius-search-plugin/src/Resources/templates/* templates/ ``` -## Documentable objects +Finally configure plugin in your .env file by adding these lines at the end : -If you want to index an object in the search index, your entity have to implements `MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface` interface : - -```php -interface DocumentableInterface -{ - public function getDocumentType(): string; - public function convertToDocument(string $locale): Result; -} +``` +###> MonsieurBizSearchPlugin ### +MONSIEURBIZ_SEARCHPLUGIN_MESSENGER_TRANSPORT_DSN=doctrine://default +MONSIEURBIZ_SEARCHPLUGIN_ES_HOST=${ELASTICSEARCH_HOST:-localhost} +MONSIEURBIZ_SEARCHPLUGIN_ES_PORT=${ELASTICSEARCH_PORT:-9200} +###< MonsieurBizSearchPlugin ### ``` -Here is an example for the product conversion using the plugin Trait to convert products : +

+
-```php -baseConvertion($locale); - - /* Adding additional attributes */ -// $document->addAttribute('my_attribute', 'My attribute', [$this->getMyAttribute()], $locale, 75); - - return $document; - } -} -``` - -Then, replace `MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableProductTrait` in your Product with your new Trait and done! - - -```php -customProperty; - } - - /** - * @param null|string $customString - * - * @return ResultInterface - */ - public function setCustomerProperty($customString): ResultInterface - { - $this->customProperty = $customString; - return $this; - } -} -``` - -Then, create a new Trait. This is the same as in "Adding additional attributes" explained. Except, we also overwrite the createResult method to use our own Result we just created. - -```php -baseConvertion($locale); - - /* Setting additonal properties */ -// $document->setCustomProperty('myCustomValue'); - - return $document; - } -} -``` - -As a final step, overwrite the `JoliCode\Elastically\Client` config in your `config/services.yaml` to use your new Result entity. - -```yaml -services: - ... - - # Change monsieurbiz/sylius-search-plugin services - JoliCode\Elastically\Client: - arguments: - $config: - host: '%env(MONSIEURBIZ_SEARCHPLUGIN_ES_HOST)%' - port: '%env(MONSIEURBIZ_SEARCHPLUGIN_ES_PORT)%' - elastically_mappings_directory: '%kernel.project_dir%/vendor/monsieurbiz/sylius-search-plugin/src/Resources/config/elasticsearch/mappings' - elastically_index_class_mapping: - documents-it_it: App\SearchPlugin\Model\Document\Result - documents-fr_fr: App\SearchPlugin\Model\Document\Result - documents-fr: App\SearchPlugin\Model\Document\Result - documents-en: App\SearchPlugin\Model\Document\Result - documents-en_us: App\SearchPlugin\Model\Document\Result - elastically_bulk_size: 100 -``` - -## Score by attribute - -Each document attribute can have a `score`. It means it can be more important than another. -For example, the product name in the exemple above has a score of `50`, and the description a score of `10` : -```php -$document->addAttribute('name', 'Name', [$this->getTranslation($locale)->getName()], $locale, 50); -$document->addAttribute('description', 'Description', [$this->getTranslation($locale)->getDescription()], $locale, 10); -``` - -## Improve search accuracy - -You can customize the search with your custom query files and modifying : - -```yaml -monsieur_biz_sylius_search: - files: - search: '%kernel.project_dir%/vendor/monsieurbiz/sylius-search-plugin/src/Resources/config/elasticsearch/queries/search.yaml' - instant: '%kernel.project_dir%/vendor/monsieurbiz/sylius-search-plugin/src/Resources/config/elasticsearch/queries/instant.yaml' - taxon: '%kernel.project_dir%/vendor/monsieurbiz/sylius-search-plugin/src/Resources/config/elasticsearch/queries/taxon.yaml' -``` - -## Indexed Documents - -Indexed documents are all entities defined in `monsieur_biz_search.documentable_classes` which implement `DocumentableInterface`. - -```yaml -monsieur_biz_sylius_search: - documentable_classes : - - 'App\Entity\Product\Product' -``` - -## Command - -A symfony command is available to populate index : `console monsieurbiz:search:populate` - -## Index on save - -For product entity, we have a listener to add / update / delete document on save. -It is the `MonsieurBiz\SyliusSearchPlugin\EventListener\DocumentListener` class which : -- `saveDocument` on `post_create` dans `post_update` -- `removeDocument` on `pre_delete` - -If your entity implements `DocumentableInterface`, you can add listeners to manage entities modifications (Replace `` with your) : -```yaml - app.event_listener.document_listener: - class: MonsieurBiz\SyliusSearchPlugin\EventListener\DocumentListener - arguments: - - '@MonsieurBiz\SyliusSearchPlugin\Model\Document\Index\Indexer' - tags: - - { name: kernel.event_listener, event: sylius..post_create, method: saveDocument } - - { name: kernel.event_listener, event: sylius..post_update, method: saveDocument } - - { name: kernel.event_listener, event: sylius..pre_delete, method: deleteDocument } -``` - -## Url Params - -If you add a new entity in search index. You have to be able to generate an URL when you display it. -In order to do that, you can customize the `RenderDocumentUrl` twig extension : -```php -public function getUrlParams(Result $document): UrlParamsProvider { - switch ($document->getType()) { - case "product" : - return new UrlParamsProvider('sylius_shop_product_show', ['slug' => $document->getSlug(), '_locale' => $document->getLocale()]); - break; - - // Add new case ! - } - - throw new NotSupportedTypeException(sprintf('Object type "%s" not supported to get URL', $this->getType())); -} -``` - -## Display search form in front - -A Twig method is available to display the form : `search_form()`. You can pass a parameter to specify a custom template. -By default, the form is displayed on `sonata.block.event.sylius.shop.layout.header` event. +3. You need to run a diff of your doctrine's migrations: `console doctrine:migrations:diff`. Don't forget to run it! (`console doctrine:migrations:migrate`) -## Front customization +4. Run the populate command. -You can override all templates in your theme. +## Infrastructure -The bundle's templates are : -- Search results display page (`src/MonsieurBizSearchPlugin/Resources/views/Search/`) -- Instant search display block (`src/MonsieurBizSearchPlugin/Resources/views/Instant/`) -- Taxon results display page (`src/MonsieurBizSearchPlugin/Resources/views/Taxon/`) -- Smaller components (`src/MonsieurBizSearchPlugin/Resources/views/Common/`) -- JS parameters (`src/MonsieurBizSearchPlugin/Resources/views/js.html.twig`) +The plugin was developed for Elasticsearch 7.16.x versions. You need to have analysis-icu and analysis-phonetic elasticsearch plugin installed. -Sylius documentation to customize these templates is available [here](https://docs.sylius.com/en/latest/customization/template.html). +## Other information -## Jane +### Jane We are using [Jane](https://github.com/janephp/janephp) to create a DTO (Data-transfer object). -Generated classes are on `src/MonsieurBizSearchPlugin/generated` folder. -Jane configuration and JSON Schema are on `src/MonsieurBizSearchPlugin/Resources/config/jane` folder. +Generated classes are on `generated` folder. +Jane configuration and JSON Schema are on `src/Resources/config/jane` folder. To rebuild generated class during plugin development, we are using : ```bash -symfony php vendor/bin/jane generate --config-file=src/Resources/config/jane/dto-config.php +symfony php vendor/bin/jane generate --config-file=src/Resources/config/jane/jane-configuration.php ``` -## Elastically - -The [Elastically](https://github.com/jolicode/elastically) Client is configured in `src/MonsieurBizSearchPlugin/Resources/config/services.yaml` file. -You can customize it if you want in `config/services.yaml`. -Analyzers and YAML mappings are on `src/MonsieurBizSearchPlugin/Resources/config/elasticsearch/mappings` folder. - -You can also find YAML used by plugin to perform the search on Elasticsearch : -- `src/MonsieurBizSearchPlugin/Resources/config/elasticsearch/queries/search.yaml` -- `src/MonsieurBizSearchPlugin/Resources/config/elasticsearch/queries/instant.yaml` -- `src/MonsieurBizSearchPlugin/Resources/config/elasticsearch/queries/taxon.yaml` - -These queries can be customized in another folder if you change the plugin config. - -## Fixtures +### Elastically -You can use fixtures to define filterable and non-filterable options and attributes : - -```yaml - -sylius_fixtures: - suites: - default: - fixtures: - monsieurbiz_sylius_search_filterable: - options: - custom: - cap_collection: - attribute: 'cap_collection' - filterable: false - - dress_collection: - attribute: 'dress_collection' - filterable: false - - dress_height: - option: 'dress_height' - filterable: false - - dress_size: - option: 'dress_size' - filterable: true -``` +The [Elastically](https://github.com/jolicode/elastically) Client is configured in `src/Resources/config/services.yaml` file. +You can customize it in your `.env` file or if you want in `config/services.yaml`. +Analyzers and YAML mappings are on `src/Resources/config/elasticsearch` folder. diff --git a/TESTING.md b/TESTING.md index 80d8b411..dd1a5fcf 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,78 +1,58 @@ # Testing -Be sure you've run the `composer install` before reading this file. -With Symfony binary, you should run `symfony composer install` +## Requirements -## Installation +You'll need: -1. To be able to run yarn build correctly, create symlink for `node_modules` : +- PHP 7.4 minimum. +- docker, for the database. +- symfony CLI, to run the local server. +- composer, to install PHP dependencies. +- npm and yarn, to install ui dependencies and build the JS/CSS files. - ```bash - ln -s tests/Application/node_modules node_modules - ``` +## Installation -2. From the plugin root directory, run the following commands: +```bash +make install +``` - ```bash - $ (cd tests/Application && yarn install) - $ (cd tests/Application && yarn build) - $ (cd tests/Application && bin/console assets:install public -e test) - $ (cd tests/Application && bin/console doctrine:database:drop --force -e test --if-exists) - $ (cd tests/Application && bin/console doctrine:database:create -e test) - $ (cd tests/Application && bin/console doctrine:schema:create -e test) - ``` - -To be able to setup the plugin's database, remember to configure your database credentials in `tests/Application/.env` -and `tests/Application/.env.test`. You can also add custom configuration in `tests/Application/.env.test.local`. +This will run a Sylius app (the one in `tests/Application/`) with the plugin +installed and all Sylius' sample data. It uses the symfony binary. ## Usage -### Running plugin tests +### List all available commands - - PHPUnit +```bash +make help +``` - ```bash - $ vendor/bin/phpunit - ``` +### Running minimum plugin tests - - PHPSpec +- PHPUnit ```bash - $ vendor/bin/phpspec run + make test.phpunit ``` - - - PHPStan - - ```bash - $ vendor/bin/phpstan analyse src - ``` - -### Opening Sylius with the plugin - -- Using `test` environment: - ```bash - $ (cd tests/Application && bin/console sylius:fixtures:load -e test) - $ (cd tests/Application && bin/console server:run -d public -e test) - ``` - -- Using `dev` environment: +- PHP CS fixer ```bash - $ (cd tests/Application && bin/console sylius:fixtures:load -e dev) - $ (cd tests/Application && bin/console server:run -d public -e dev) + make test.phpcs ``` -### Reindex Elasticsearch + > Tip: You can fix your code with `make test.phpcs.fix`! -- Using `test` environment: +- PHPSpec ```bash - $ (cd tests/Application && bin/console monsieurbiz:search:populate -e test) + make test.phpspec ``` - -- Using `dev` environment: + +- PHPStan ```bash - $ (cd tests/Application && bin/console monsieurbiz:search:populate -e dev) + make test.phpstan ``` + +> Tip: You can run all tests with `make test.all`! diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 00000000..1570bf44 --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,61 @@ +global.MonsieurBizInstantSearch = class { + constructor( + instantUrl, + searchInputSelector, + resultClosestSelector, + resultFindSelector, + keyUpTimeOut, + minQueryLength + ) { + // Init a timeout variable to be used below + var instantSearchTimeout = null; + document.querySelector(searchInputSelector).addEventListener('keyup', function (e) { + clearTimeout(instantSearchTimeout); + var query = e.currentTarget.value; + var resultElement = e.currentTarget.closest(resultClosestSelector).querySelector(resultFindSelector); + instantSearchTimeout = setTimeout(function () { + if (query.length >= minQueryLength) { + var httpRequest = new XMLHttpRequest(); + httpRequest.onload = function() { + if (this.status === 200) { + resultElement.innerHTML = this.responseText; + resultElement.style.display = 'block'; + } + }; + httpRequest.open("POST", instantUrl); + httpRequest.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + httpRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + httpRequest.send(new URLSearchParams({query: query}).toString()); + } + }, keyUpTimeOut); + }); + + // Hide results when user leave the autocomplete form + const searchForm = document.querySelector(searchInputSelector).closest(resultClosestSelector); + searchForm.addEventListener('focusout', function (e) { + if (e.relatedTarget === null || !searchForm.contains(e.relatedTarget)) { + const resultElement = searchForm.querySelector(resultFindSelector); + resultElement.style.display = 'none'; + } + }); + + document.querySelector(searchInputSelector).addEventListener('focus', function (e) { + var query = e.currentTarget.value; + if (query !== '') { + const resultElement = searchForm.querySelector(resultFindSelector); + resultElement.style.display = 'block'; + } + }); + } +} + +document.addEventListener("DOMContentLoaded", function() { + new MonsieurBizInstantSearch( + monsieurbizSearchPlugin.instantUrl, + monsieurbizSearchPlugin.searchInputSelector, + monsieurbizSearchPlugin.resultClosestSelector, + monsieurbizSearchPlugin.resultFindSelector, + monsieurbizSearchPlugin.keyUpTimeOut, + monsieurbizSearchPlugin.minQueryLength + ); +}); diff --git a/composer.json b/composer.json index a35b577e..ff1b5dee 100644 --- a/composer.json +++ b/composer.json @@ -1,63 +1,82 @@ { "name": "monsieurbiz/sylius-search-plugin", "type": "sylius-plugin", - "keywords": ["sylius", "sylius-plugin", "elasticsearch"], + "keywords": ["sylius", "sylius-plugin", "monsieurbiz"], "description": "A search plugin using Elasticsearch for Sylius.", "license": "MIT", "require": { - "php": "^7.2 || ^8.0", - "sylius/sylius": "^1.4", - "jolicode/elastically": "^1.0.0" + "php": "~7.4|~8.0", + "babdev/pagerfanta-bundle": "^2.5", + "jacquesbh/eater": "^2.0", + "jane-php/automapper-bundle": "^7.1", + "jolicode/elastically": "^1.4.0", + "monsieurbiz/sylius-settings-plugin": "^1.0", + "sylius/sylius": ">=1.9 <1.11", + "symfony/messenger": "^4.4 || ^5.2" }, "require-dev": { - "behat/behat": "^3.4", - "behat/mink": "^1.7@dev", - "behat/mink-browserkit-driver": "^1.3", - "behat/mink-extension": "^2.2", - "behat/mink-selenium2-driver": "^1.3", + "behat/behat": "^3.6.1", + "behat/mink-selenium2-driver": "^1.4", + "dmore/behat-chrome-extension": "^1.3", + "dmore/chrome-mink-driver": "^2.7", + "doctrine/data-fixtures": "^1.4", + "ergebnis/composer-normalize": "^2.5", + "friends-of-behat/mink": "^1.8", + "friends-of-behat/mink-browserkit-driver": "^1.4", + "friends-of-behat/mink-extension": "^2.4", "friends-of-behat/page-object-extension": "^0.3", - "friends-of-behat/suite-settings-extension": "^1.0", - "friends-of-behat/symfony-extension": "^2.0", - "friends-of-behat/variadic-extension": "^1.1", - "friends-of-behat/mink-debug-extension": "^2.0", - "phpspec/phpspec": "^6.3", - "phpstan/phpstan-doctrine": "0.12.33", - "phpstan/phpstan-webmozart-assert": "^0.12.8", - "phpunit/phpunit": "^8.5.12", - "sensiolabs/security-checker": "^5.0", - "sylius-labs/coding-standard": "^3.2.2", - "symfony/browser-kit": "^3.4|^4.1", - "symfony/debug-bundle": "^3.4|^4.1", - "symfony/dotenv": "^4.2", - "symfony/intl": "^3.4|^4.1", - "symfony/web-profiler-bundle": "^3.4|^4.1", - "symfony/web-server-bundle": "^3.4|^4.1", - "jane-php/json-schema": "^5.2" + "friends-of-behat/symfony-extension": "^2.1", + "friends-of-behat/variadic-extension": "^1.3", + "hwi/oauth-bundle": "^1.1", + "lchrusciel/api-test-case": "^5.0", + "matthiasnoback/symfony-config-test": "^4.2", + "matthiasnoback/symfony-dependency-injection-test": "^4.1", + "mikey179/vfsstream": "^1.6", + "mockery/mockery": "^1.4", + "pamil/prophecy-common": "^0.1", + "phpspec/phpspec": "^6.1", + "phpstan/phpstan": "^0.12.57", + "phpstan/phpstan-doctrine": "^0.12.19", + "phpstan/phpstan-webmozart-assert": "^0.12.7", + "phpunit/phpunit": "^8.5", + "psalm/plugin-mockery": "^0.3", + "psr/event-dispatcher": "^1.0", + "sylius-labs/coding-standard": "^3.1", + "symfony/browser-kit": "^4.4", + "symfony/debug-bundle": "^4.4", + "symfony/dotenv": "^4.4", + "symfony/flex": "^1.7", + "symfony/web-profiler-bundle": "^4.4", + "phpmd/phpmd": "@stable" }, "conflict": { - "symfony/symfony": "4.1.8", - "symfony/browser-kit": "4.1.8", - "symfony/dependency-injection": "4.1.8", - "symfony/dom-crawler": "4.1.8", - "symfony/routing": "4.1.8" + "doctrine/dbal": "^3" }, "prefer-stable": true, "autoload": { "psr-4": { "MonsieurBiz\\SyliusSearchPlugin\\": "src/", - "Tests\\MonsieurBiz\\SyliusSearchPlugin\\": "tests/", - "Tests\\MonsieurBiz\\SyliusSearchPlugin\\App\\": "tests/Application/src" + "MonsieurBiz\\SyliusSearchPlugin\\Generated\\": "generated/" } }, - "autoload-dev": { - "classmap": ["tests/Application/Kernel.php"] - }, "scripts": { - "phpcs": "php-cs-fixer fix --using-cache=false" + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "phpcs": "php-cs-fixer fix --using-cache=false", + "jane-generate": "jane generate --config-file=src/Resources/config/jane/jane-configuration.php", + "phpstan": "phpstan analyse -c phpstan.neon src/", + "phpmd": "phpmd --exclude Migrations/* src/ ansi phpmd.xml", + "phpunit": "phpunit", + "phpspec": "phpspec run" }, "extra": { + "symfony": { + "require": "^4.4" + }, "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0-dev" } } } diff --git a/dist/.env.local b/dist/.env.local new file mode 100644 index 00000000..d0f2d4df --- /dev/null +++ b/dist/.env.local @@ -0,0 +1,3 @@ +MONSIEURBIZ_SEARCHPLUGIN_MESSENGER_TRANSPORT_DSN=doctrine://default +MONSIEURBIZ_SEARCHPLUGIN_ES_HOST=${ELASTICSEARCH_HOST:-localhost} +MONSIEURBIZ_SEARCHPLUGIN_ES_PORT=${ELASTICSEARCH_PORT:-9200} diff --git a/dist/config/packages/monsieurbiz_sylius_search_plugin.yaml b/dist/config/packages/monsieurbiz_sylius_search_plugin.yaml new file mode 100644 index 00000000..d39a7f98 --- /dev/null +++ b/dist/config/packages/monsieurbiz_sylius_search_plugin.yaml @@ -0,0 +1,2 @@ +imports: + - { resource: "@MonsieurBizSyliusSearchPlugin/Resources/config/config.yaml" } diff --git a/recipe/dev/config/routes/monsieurbiz_sylius_search_plugin.yaml b/dist/config/routes/monsieurbiz_sylius_search_plugin.yaml similarity index 100% rename from recipe/dev/config/routes/monsieurbiz_sylius_search_plugin.yaml rename to dist/config/routes/monsieurbiz_sylius_search_plugin.yaml diff --git a/dist/docker-compose.override.yaml b/dist/docker-compose.override.yaml new file mode 100644 index 00000000..c52250be --- /dev/null +++ b/dist/docker-compose.override.yaml @@ -0,0 +1,48 @@ +version: '3.8' +services: + elasticsearch: + build: + context: ./docker/elasticsearch/ + args: + USER_UID: ${USER_UID} + volumes: + - esdata:/usr/share/elasticsearch/data:rw + environment: + - node.name=elasticsearch + - cluster.initial_master_nodes=elasticsearch + - cluster.name=docker-cluster + - bootstrap.memory_lock=true + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + - "xpack.security.enabled=false" + ulimits: + memlock: + soft: -1 + hard: -1 + ports: + - "9200:9200" + - "9300:9300" + + cerebro: + image: lmenezes/cerebro + ports: + - "9000:9000" + links: + - elasticsearch + + kibana: + image: kibana:7.4.0 + ports: + - "5601:5601" + environment: + - "SERVER_NAME=localhost" + - "ELASTICSEARCH_HOSTS=http://elasticsearch:9200" + - "XPACK_GRAPH_ENABLED=false" + - "XPACK_ML_ENABLED=false" + - "XPACK_REPORTING_ENABLED=false" + - "XPACK_SECURITY_ENABLED=false" + - "XPACK_WATCHER_ENABLED=false" + links: + - elasticsearch + +volumes: + esdata: {} diff --git a/dist/docker/elasticsearch/Dockerfile b/dist/docker/elasticsearch/Dockerfile new file mode 100644 index 00000000..c9843113 --- /dev/null +++ b/dist/docker/elasticsearch/Dockerfile @@ -0,0 +1,5 @@ +FROM docker.elastic.co/elasticsearch/elasticsearch:7.16.3 + +# Install ES plugins +RUN bin/elasticsearch-plugin install analysis-phonetic +RUN bin/elasticsearch-plugin install analysis-icu diff --git a/tests/Application/src/Entity/Product/ProductAttribute.php b/dist/src/Entity/Product/ProductAttribute.php similarity index 64% rename from tests/Application/src/Entity/Product/ProductAttribute.php rename to dist/src/Entity/Product/ProductAttribute.php index ce6b2391..eaff27e7 100644 --- a/tests/Application/src/Entity/Product/ProductAttribute.php +++ b/dist/src/Entity/Product/ProductAttribute.php @@ -5,28 +5,27 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * 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 Tests\MonsieurBiz\SyliusSearchPlugin\App\Entity\Product; +namespace App\Entity\Product; use Doctrine\ORM\Mapping as ORM; -use MonsieurBiz\SyliusSearchPlugin\Entity\Product\FilterableInterface; -use MonsieurBiz\SyliusSearchPlugin\Model\Product\FilterableTrait; +use MonsieurBiz\SyliusSearchPlugin\Entity\Product\SearchableInterface; +use MonsieurBiz\SyliusSearchPlugin\Model\Product\SearchableTrait; use Sylius\Component\Attribute\Model\AttributeTranslationInterface; use Sylius\Component\Product\Model\ProductAttribute as BaseProductAttribute; -use Sylius\Component\Product\Model\ProductAttributeTranslation; /** * @ORM\Entity * @ORM\Table(name="sylius_product_attribute") */ -class ProductAttribute extends BaseProductAttribute implements FilterableInterface +class ProductAttribute extends BaseProductAttribute implements SearchableInterface { - use FilterableTrait; + use SearchableTrait; protected function createTranslation(): AttributeTranslationInterface { diff --git a/tests/Application/src/Entity/Product/ProductOption.php b/dist/src/Entity/Product/ProductOption.php similarity index 65% rename from tests/Application/src/Entity/Product/ProductOption.php rename to dist/src/Entity/Product/ProductOption.php index 3f468f74..2adc02e8 100644 --- a/tests/Application/src/Entity/Product/ProductOption.php +++ b/dist/src/Entity/Product/ProductOption.php @@ -5,28 +5,27 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * 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 Tests\MonsieurBiz\SyliusSearchPlugin\App\Entity\Product; +namespace App\Entity\Product; use Doctrine\ORM\Mapping as ORM; -use MonsieurBiz\SyliusSearchPlugin\Entity\Product\FilterableInterface; -use MonsieurBiz\SyliusSearchPlugin\Model\Product\FilterableTrait; +use MonsieurBiz\SyliusSearchPlugin\Entity\Product\SearchableInterface; +use MonsieurBiz\SyliusSearchPlugin\Model\Product\SearchableTrait; use Sylius\Component\Product\Model\ProductOption as BaseProductOption; -use Sylius\Component\Product\Model\ProductOptionTranslation; use Sylius\Component\Product\Model\ProductOptionTranslationInterface; /** * @ORM\Entity * @ORM\Table(name="sylius_product_option") */ -class ProductOption extends BaseProductOption implements FilterableInterface +class ProductOption extends BaseProductOption implements SearchableInterface { - use FilterableTrait; + use SearchableTrait; protected function createTranslation(): ProductOptionTranslationInterface { diff --git a/dist/src/Migrations/Version20211012092353.php b/dist/src/Migrations/Version20211012092353.php new file mode 100644 index 00000000..60e21270 --- /dev/null +++ b/dist/src/Migrations/Version20211012092353.php @@ -0,0 +1,42 @@ + + * + * 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\Migrations; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\Migrations\AbstractMigration; + +/** + * Auto-generated Migration: Please modify to your needs! + */ +final class Version20211012092353 extends AbstractMigration +{ + public function getDescription(): string + { + return 'Add search columns on product attribute and product option'; + } + + public function up(Schema $schema): void + { + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE sylius_product_attribute ADD searchable TINYINT(1) DEFAULT \'1\' NOT NULL, ADD filterable TINYINT(1) DEFAULT \'0\' NOT NULL, ADD search_weight SMALLINT UNSIGNED DEFAULT 1 NOT NULL'); + $this->addSql('ALTER TABLE sylius_product_option ADD searchable TINYINT(1) DEFAULT \'1\' NOT NULL, ADD filterable TINYINT(1) DEFAULT \'0\' NOT NULL, ADD search_weight SMALLINT UNSIGNED DEFAULT 1 NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE sylius_product_attribute DROP searchable, DROP filterable, DROP search_weight'); + $this->addSql('ALTER TABLE sylius_product_option DROP searchable, DROP filterable, DROP search_weight'); + } +} diff --git a/dist/src/Migrations/Version20220128112640.php b/dist/src/Migrations/Version20220128112640.php new file mode 100644 index 00000000..20cf9652 --- /dev/null +++ b/dist/src/Migrations/Version20220128112640.php @@ -0,0 +1,40 @@ + + * + * 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\Migrations; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\Migrations\AbstractMigration; + +/** + * Auto-generated Migration: Please modify to your needs! + */ +final class Version20220128112640 extends AbstractMigration +{ + public function getDescription(): string + { + return ''; + } + + public function up(Schema $schema): void + { + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TABLE messenger_messages (id BIGINT AUTO_INCREMENT NOT NULL, body LONGTEXT NOT NULL, headers LONGTEXT NOT NULL, queue_name VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, available_at DATETIME NOT NULL, delivered_at DATETIME DEFAULT NULL, INDEX IDX_75EA56E016BA31DB (delivered_at), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE messenger_messages'); + } +} diff --git a/src/Resources/SyliusAdminBundle/views/ProductAttribute/_form.html.twig b/dist/templates/bundles/SyliusAdminBundle/ProductAttribute/_form.html.twig similarity index 83% rename from src/Resources/SyliusAdminBundle/views/ProductAttribute/_form.html.twig rename to dist/templates/bundles/SyliusAdminBundle/ProductAttribute/_form.html.twig index 6d75a0d8..3130f269 100644 --- a/src/Resources/SyliusAdminBundle/views/ProductAttribute/_form.html.twig +++ b/dist/templates/bundles/SyliusAdminBundle/ProductAttribute/_form.html.twig @@ -8,11 +8,14 @@ {{ form_row(form.position) }} {{ form_row(form.type) }} + {{ form_row(form.translatable) }}

{{ 'monsieurbiz_searchplugin.admin.product_attribute.form.title'|trans }}

-
+
+ {{ form_row(form.searchable) }} {{ form_row(form.filterable) }} + {{ form_row(form.search_weight) }}
{% if form.configuration is defined %} @@ -24,3 +27,4 @@
{% endif %} {{ translationForm(form.translations) }} + diff --git a/src/Resources/SyliusAdminBundle/views/ProductOption/_form.html.twig b/dist/templates/bundles/SyliusAdminBundle/ProductOption/_form.html.twig similarity index 83% rename from src/Resources/SyliusAdminBundle/views/ProductOption/_form.html.twig rename to dist/templates/bundles/SyliusAdminBundle/ProductOption/_form.html.twig index bcc8013f..661788ae 100644 --- a/src/Resources/SyliusAdminBundle/views/ProductOption/_form.html.twig +++ b/dist/templates/bundles/SyliusAdminBundle/ProductOption/_form.html.twig @@ -10,8 +10,10 @@

{{ 'monsieurbiz_searchplugin.admin.product_option.form.title'|trans }}

-
+
+ {{ form_row(form.searchable) }} {{ form_row(form.filterable) }} + {{ form_row(form.search_weight) }}

{{ 'sylius.ui.values'|trans }}

diff --git a/doc/ProductAttributeValueReader.md b/doc/ProductAttributeValueReader.md new file mode 100644 index 00000000..1aaddf7b --- /dev/null +++ b/doc/ProductAttributeValueReader.md @@ -0,0 +1,47 @@ +# Product attribute value reader + +## What is it? + +A product attribute value reader is used to transform the value of an attribute into an indexable value for the elasticsearch document. + +We have defined a reader for the native Sylius types: + +- checkbox +- date +- datetime +- integer +- percent +- select +- textarea +- text + +## Add or replace a reader + +You have added a new attribute type, and you want to index its value. +Or you want to change an existing reader. + +In your `service.yaml`, you can add or replace a product attribute value reader : + +**Create a Product Attribute Value Reader class** that implements the `\MonsieurBiz\SyliusSearchPlugin\AutoMapper\ProductAttributeValueReader\ReaderInterface` interface, and define the two methods: + +- `getReaderCode`: this code matches the reader with the attribute type +- `getValue`: return the indexable value + +```php +use \MonsieurBiz\SyliusSearchPlugin\AutoMapper\ProductAttributeValueReader\ReaderInterface; + +class MyCustomReader implements ReaderInterface +{ + // ... +} +``` + +And add the `monsieurbiz.search.automapper.product_attribute_value_reader` tag on your custom reader : + +```yaml +App\...\MyCustomReader: + tags: [ 'monsieurbiz.search.automapper.product_attribute_value_reader' ] + +``` + +To **replace** an existing product attribute value reader, your `getReaderCode` returns the attribute type code of the existing reader. diff --git a/docker-compose.yaml.dist b/docker-compose.yaml.dist new file mode 100644 index 00000000..1dab6ee0 --- /dev/null +++ b/docker-compose.yaml.dist @@ -0,0 +1,22 @@ +version: '3.8' +services: + database: + platform: linux/x86_64 + image: mysql:8.0 + command: --default-authentication-plugin=mysql_native_password + ports: + - 3306 + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: 1 + MYSQL_DATABASE: sylius + volumes: + - database:/var/lib/mysql + + mailer: + image: monsieurbiz/mailcatcher + ports: + - 1025 + - 1080 + +volumes: + database: {} diff --git a/generated/Model/ChannelDTO.php b/generated/Model/ChannelDTO.php new file mode 100644 index 00000000..dfa0250c --- /dev/null +++ b/generated/Model/ChannelDTO.php @@ -0,0 +1,34 @@ + + * + * 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\Generated\Model; + +class ChannelDTO +{ + /** + * @var string + */ + protected $code; + + public function getCode(): string + { + return $this->code; + } + + public function setCode(string $code): self + { + $this->code = $code; + + return $this; + } +} diff --git a/generated/Model/ImageDTO.php b/generated/Model/ImageDTO.php new file mode 100644 index 00000000..e19a2d5c --- /dev/null +++ b/generated/Model/ImageDTO.php @@ -0,0 +1,34 @@ + + * + * 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\Generated\Model; + +class ImageDTO +{ + /** + * @var string|null + */ + protected $path; + + public function getPath(): ?string + { + return $this->path; + } + + public function setPath(?string $path): self + { + $this->path = $path; + + return $this; + } +} diff --git a/generated/Model/PricingDTO.php b/generated/Model/PricingDTO.php new file mode 100644 index 00000000..a79f0d5f --- /dev/null +++ b/generated/Model/PricingDTO.php @@ -0,0 +1,85 @@ + + * + * 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\Generated\Model; + +class PricingDTO +{ + /** + * @var string + */ + protected $channelCode; + + /** + * @var int|null + */ + protected $price; + + /** + * @var int|null + */ + protected $originalPrice; + + /** + * @var bool + */ + protected $priceReduced; + + public function getChannelCode(): string + { + return $this->channelCode; + } + + public function setChannelCode(string $channelCode): self + { + $this->channelCode = $channelCode; + + return $this; + } + + public function getPrice(): ?int + { + return $this->price; + } + + public function setPrice(?int $price): self + { + $this->price = $price; + + return $this; + } + + public function getOriginalPrice(): ?int + { + return $this->originalPrice; + } + + public function setOriginalPrice(?int $originalPrice): self + { + $this->originalPrice = $originalPrice; + + return $this; + } + + public function getPriceReduced(): bool + { + return $this->priceReduced; + } + + public function setPriceReduced(bool $priceReduced): self + { + $this->priceReduced = $priceReduced; + + return $this; + } +} diff --git a/generated/Model/ProductAttributeDTO.php b/generated/Model/ProductAttributeDTO.php new file mode 100644 index 00000000..c4225abf --- /dev/null +++ b/generated/Model/ProductAttributeDTO.php @@ -0,0 +1,74 @@ + + * + * 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\Generated\Model; + +class ProductAttributeDTO +{ + /** + * @var string + */ + protected $code; + + /** + * @var string + */ + protected $name; + + /** + * @var mixed|null + */ + protected $value; + + public function getCode(): string + { + return $this->code; + } + + public function setCode(string $code): self + { + $this->code = $code; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + /** + * @return mixed|null + */ + public function getValue() + { + return $this->value; + } + + /** + * @param mixed|null $value + */ + public function setValue($value): self + { + $this->value = $value; + + return $this; + } +} diff --git a/generated/Model/ProductTaxonDTO.php b/generated/Model/ProductTaxonDTO.php new file mode 100644 index 00000000..915a3dd6 --- /dev/null +++ b/generated/Model/ProductTaxonDTO.php @@ -0,0 +1,51 @@ + + * + * 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\Generated\Model; + +class ProductTaxonDTO +{ + /** + * @var TaxonDTO + */ + protected $taxon; + + /** + * @var int|null + */ + protected $position; + + public function getTaxon(): TaxonDTO + { + return $this->taxon; + } + + public function setTaxon(TaxonDTO $taxon): self + { + $this->taxon = $taxon; + + return $this; + } + + public function getPosition(): ?int + { + return $this->position; + } + + public function setPosition(?int $position): self + { + $this->position = $position; + + return $this; + } +} diff --git a/generated/Model/TaxonDTO.php b/generated/Model/TaxonDTO.php new file mode 100644 index 00000000..ed788d8a --- /dev/null +++ b/generated/Model/TaxonDTO.php @@ -0,0 +1,85 @@ + + * + * 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\Generated\Model; + +class TaxonDTO +{ + /** + * @var string + */ + protected $name; + + /** + * @var string + */ + protected $code; + + /** + * @var int + */ + protected $position; + + /** + * @var int + */ + protected $level; + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getCode(): string + { + return $this->code; + } + + public function setCode(string $code): self + { + $this->code = $code; + + return $this; + } + + public function getPosition(): int + { + return $this->position; + } + + public function setPosition(int $position): self + { + $this->position = $position; + + return $this; + } + + public function getLevel(): int + { + return $this->level; + } + + public function setLevel(int $level): self + { + $this->level = $level; + + return $this; + } +} diff --git a/generated/Normalizer/ChannelDTONormalizer.php b/generated/Normalizer/ChannelDTONormalizer.php new file mode 100644 index 00000000..c5b96b76 --- /dev/null +++ b/generated/Normalizer/ChannelDTONormalizer.php @@ -0,0 +1,71 @@ + + * + * 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\Generated\Normalizer; + +use Jane\Component\JsonSchemaRuntime\Reference; +use MonsieurBiz\SyliusSearchPlugin\Generated\Runtime\Normalizer\CheckArray; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +class ChannelDTONormalizer implements DenormalizerInterface, NormalizerInterface, DenormalizerAwareInterface, NormalizerAwareInterface +{ + use CheckArray; + + use DenormalizerAwareTrait; + + use NormalizerAwareTrait; + + public function supportsDenormalization($data, $type, $format = null) + { + return 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\ChannelDTO' === $type; + } + + public function supportsNormalization($data, $format = null) + { + return $data instanceof \MonsieurBiz\SyliusSearchPlugin\Generated\Model\ChannelDTO; + } + + public function denormalize($data, $class, $format = null, array $context = []) + { + if (isset($data['$ref'])) { + return new Reference($data['$ref'], $context['document-origin']); + } + if (isset($data['$recursiveRef'])) { + return new Reference($data['$recursiveRef'], $context['document-origin']); + } + $object = new \MonsieurBiz\SyliusSearchPlugin\Generated\Model\ChannelDTO(); + if (null === $data || false === \is_array($data)) { + return $object; + } + if (\array_key_exists('code', $data)) { + $object->setCode($data['code']); + } + + return $object; + } + + public function normalize($object, $format = null, array $context = []) + { + $data = []; + if (null !== $object->getCode()) { + $data['code'] = $object->getCode(); + } + + return $data; + } +} diff --git a/generated/Normalizer/ImageDTONormalizer.php b/generated/Normalizer/ImageDTONormalizer.php new file mode 100644 index 00000000..6b2acde5 --- /dev/null +++ b/generated/Normalizer/ImageDTONormalizer.php @@ -0,0 +1,85 @@ + + * + * 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\Generated\Normalizer; + +use Jane\Component\JsonSchemaRuntime\Reference; +use MonsieurBiz\SyliusSearchPlugin\Generated\Runtime\Normalizer\CheckArray; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +class ImageDTONormalizer implements DenormalizerInterface, NormalizerInterface, DenormalizerAwareInterface, NormalizerAwareInterface +{ + use CheckArray; + + use DenormalizerAwareTrait; + + use NormalizerAwareTrait; + + public function supportsDenormalization($data, $type, $format = null) + { + return 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\ImageDTO' === $type; + } + + public function supportsNormalization($data, $format = null) + { + return $data instanceof \MonsieurBiz\SyliusSearchPlugin\Generated\Model\ImageDTO; + } + + public function denormalize($data, $class, $format = null, array $context = []) + { + if (isset($data['$ref'])) { + return new Reference($data['$ref'], $context['document-origin']); + } + if (isset($data['$recursiveRef'])) { + return new Reference($data['$recursiveRef'], $context['document-origin']); + } + $object = new \MonsieurBiz\SyliusSearchPlugin\Generated\Model\ImageDTO(); + if (null === $data || false === \is_array($data)) { + return $object; + } + if (\array_key_exists('path', $data) && null !== $data['path']) { + $value = $data['path']; + if (null === $data['path']) { + $value = $data['path']; + } elseif (\is_string($data['path'])) { + $value = $data['path']; + } + $object->setPath($value); + } elseif (\array_key_exists('path', $data) && null === $data['path']) { + $object->setPath(null); + } + + return $object; + } + + public function normalize($object, $format = null, array $context = []) + { + $data = []; + if (null !== $object->getPath()) { + $value = $object->getPath(); + if (null === $object->getPath()) { + $value = $object->getPath(); + } elseif (\is_string($object->getPath())) { + $value = $object->getPath(); + } + $data['path'] = $value; + } + + return $data; + } +} diff --git a/generated/Normalizer/JaneObjectNormalizer.php b/generated/Normalizer/JaneObjectNormalizer.php new file mode 100644 index 00000000..48f7047c --- /dev/null +++ b/generated/Normalizer/JaneObjectNormalizer.php @@ -0,0 +1,76 @@ + + * + * 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\Generated\Normalizer; + +use MonsieurBiz\SyliusSearchPlugin\Generated\Runtime\Normalizer\CheckArray; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +class JaneObjectNormalizer implements DenormalizerInterface, NormalizerInterface, DenormalizerAwareInterface, NormalizerAwareInterface +{ + use CheckArray; + + use DenormalizerAwareTrait; + + use NormalizerAwareTrait; + + protected $normalizers = ['MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\ImageDTO' => 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Normalizer\\ImageDTONormalizer', 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\ChannelDTO' => 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Normalizer\\ChannelDTONormalizer', 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\ProductTaxonDTO' => 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Normalizer\\ProductTaxonDTONormalizer', 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\TaxonDTO' => 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Normalizer\\TaxonDTONormalizer', 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\ProductAttributeDTO' => 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Normalizer\\ProductAttributeDTONormalizer', 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\PricingDTO' => 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Normalizer\\PricingDTONormalizer', '\\Jane\\Component\\JsonSchemaRuntime\\Reference' => '\\MonsieurBiz\\SyliusSearchPlugin\\Generated\\Runtime\\Normalizer\\ReferenceNormalizer']; + + protected $normalizersCache = []; + + public function supportsDenormalization($data, $type, $format = null) + { + return \array_key_exists($type, $this->normalizers); + } + + public function supportsNormalization($data, $format = null) + { + return \is_object($data) && \array_key_exists(\get_class($data), $this->normalizers); + } + + public function normalize($object, $format = null, array $context = []) + { + $normalizerClass = $this->normalizers[\get_class($object)]; + $normalizer = $this->getNormalizer($normalizerClass); + + return $normalizer->normalize($object, $format, $context); + } + + public function denormalize($data, $class, $format = null, array $context = []) + { + $denormalizerClass = $this->normalizers[$class]; + $denormalizer = $this->getNormalizer($denormalizerClass); + + return $denormalizer->denormalize($data, $class, $format, $context); + } + + private function getNormalizer(string $normalizerClass) + { + return $this->normalizersCache[$normalizerClass] ?? $this->initNormalizer($normalizerClass); + } + + private function initNormalizer(string $normalizerClass) + { + $normalizer = new $normalizerClass(); + $normalizer->setNormalizer($this->normalizer); + $normalizer->setDenormalizer($this->denormalizer); + $this->normalizersCache[$normalizerClass] = $normalizer; + + return $normalizer; + } +} diff --git a/generated/Normalizer/PricingDTONormalizer.php b/generated/Normalizer/PricingDTONormalizer.php new file mode 100644 index 00000000..b556b93b --- /dev/null +++ b/generated/Normalizer/PricingDTONormalizer.php @@ -0,0 +1,125 @@ + + * + * 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\Generated\Normalizer; + +use Jane\Component\JsonSchemaRuntime\Reference; +use MonsieurBiz\SyliusSearchPlugin\Generated\Runtime\Normalizer\CheckArray; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +class PricingDTONormalizer implements DenormalizerInterface, NormalizerInterface, DenormalizerAwareInterface, NormalizerAwareInterface +{ + use CheckArray; + + use DenormalizerAwareTrait; + + use NormalizerAwareTrait; + + public function supportsDenormalization($data, $type, $format = null) + { + return 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\PricingDTO' === $type; + } + + public function supportsNormalization($data, $format = null) + { + return $data instanceof \MonsieurBiz\SyliusSearchPlugin\Generated\Model\PricingDTO; + } + + public function denormalize($data, $class, $format = null, array $context = []) + { + if (isset($data['$ref'])) { + return new Reference($data['$ref'], $context['document-origin']); + } + if (isset($data['$recursiveRef'])) { + return new Reference($data['$recursiveRef'], $context['document-origin']); + } + $object = new \MonsieurBiz\SyliusSearchPlugin\Generated\Model\PricingDTO(); + if (null === $data || false === \is_array($data)) { + return $object; + } + if (\array_key_exists('channel_code', $data)) { + $object->setChannelCode($data['channel_code']); + } + if (\array_key_exists('price', $data) && null !== $data['price']) { + $value = $data['price']; + if (null === $data['price']) { + $value = $data['price']; + } elseif (\is_int($data['price'])) { + $value = $data['price']; + } + $object->setPrice($value); + } elseif (\array_key_exists('price', $data) && null === $data['price']) { + $object->setPrice(null); + } + if (\array_key_exists('original_price', $data) && null !== $data['original_price']) { + $value_1 = $data['original_price']; + if (null === $data['original_price']) { + $value_1 = $data['original_price']; + } elseif (\is_int($data['original_price'])) { + $value_1 = $data['original_price']; + } + $object->setOriginalPrice($value_1); + } elseif (\array_key_exists('original_price', $data) && null === $data['original_price']) { + $object->setOriginalPrice(null); + } + if (\array_key_exists('price_reduced', $data)) { + $value_2 = $data['price_reduced']; + if (\is_bool($data['price_reduced'])) { + $value_2 = $data['price_reduced']; + } + $object->setPriceReduced($value_2); + } + + return $object; + } + + public function normalize($object, $format = null, array $context = []) + { + $data = []; + if (null !== $object->getChannelCode()) { + $data['channel_code'] = $object->getChannelCode(); + } + if (null !== $object->getPrice()) { + $value = $object->getPrice(); + if (null === $object->getPrice()) { + $value = $object->getPrice(); + } elseif (\is_int($object->getPrice())) { + $value = $object->getPrice(); + } + $data['price'] = $value; + } + if (null !== $object->getOriginalPrice()) { + $value_1 = $object->getOriginalPrice(); + if (null === $object->getOriginalPrice()) { + $value_1 = $object->getOriginalPrice(); + } elseif (\is_int($object->getOriginalPrice())) { + $value_1 = $object->getOriginalPrice(); + } + $data['original_price'] = $value_1; + } + if (null !== $object->getPriceReduced()) { + $value_2 = $object->getPriceReduced(); + if (\is_bool($object->getPriceReduced())) { + $value_2 = $object->getPriceReduced(); + } + $data['price_reduced'] = $value_2; + } + + return $data; + } +} diff --git a/generated/Normalizer/ProductAttributeDTONormalizer.php b/generated/Normalizer/ProductAttributeDTONormalizer.php new file mode 100644 index 00000000..7936b408 --- /dev/null +++ b/generated/Normalizer/ProductAttributeDTONormalizer.php @@ -0,0 +1,97 @@ + + * + * 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\Generated\Normalizer; + +use Jane\Component\JsonSchemaRuntime\Reference; +use MonsieurBiz\SyliusSearchPlugin\Generated\Runtime\Normalizer\CheckArray; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +class ProductAttributeDTONormalizer implements DenormalizerInterface, NormalizerInterface, DenormalizerAwareInterface, NormalizerAwareInterface +{ + use CheckArray; + + use DenormalizerAwareTrait; + + use NormalizerAwareTrait; + + public function supportsDenormalization($data, $type, $format = null) + { + return 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\ProductAttributeDTO' === $type; + } + + public function supportsNormalization($data, $format = null) + { + return $data instanceof \MonsieurBiz\SyliusSearchPlugin\Generated\Model\ProductAttributeDTO; + } + + public function denormalize($data, $class, $format = null, array $context = []) + { + if (isset($data['$ref'])) { + return new Reference($data['$ref'], $context['document-origin']); + } + if (isset($data['$recursiveRef'])) { + return new Reference($data['$recursiveRef'], $context['document-origin']); + } + $object = new \MonsieurBiz\SyliusSearchPlugin\Generated\Model\ProductAttributeDTO(); + if (null === $data || false === \is_array($data)) { + return $object; + } + if (\array_key_exists('code', $data)) { + $object->setCode($data['code']); + } + if (\array_key_exists('name', $data)) { + $object->setName($data['name']); + } + if (\array_key_exists('value', $data) && null !== $data['value']) { + $value = $data['value']; + if (null === $data['value']) { + $value = $data['value']; + } elseif (isset($data['value'])) { + $value = $data['value']; + } + $object->setValue($value); + } elseif (\array_key_exists('value', $data) && null === $data['value']) { + $object->setValue(null); + } + + return $object; + } + + public function normalize($object, $format = null, array $context = []) + { + $data = []; + if (null !== $object->getCode()) { + $data['code'] = $object->getCode(); + } + if (null !== $object->getName()) { + $data['name'] = $object->getName(); + } + if (null !== $object->getValue()) { + $value = $object->getValue(); + if (null === $object->getValue()) { + $value = $object->getValue(); + } elseif (null !== $object->getValue()) { + $value = $object->getValue(); + } + $data['value'] = $value; + } + + return $data; + } +} diff --git a/generated/Normalizer/ProductTaxonDTONormalizer.php b/generated/Normalizer/ProductTaxonDTONormalizer.php new file mode 100644 index 00000000..c9831949 --- /dev/null +++ b/generated/Normalizer/ProductTaxonDTONormalizer.php @@ -0,0 +1,91 @@ + + * + * 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\Generated\Normalizer; + +use Jane\Component\JsonSchemaRuntime\Reference; +use MonsieurBiz\SyliusSearchPlugin\Generated\Runtime\Normalizer\CheckArray; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +class ProductTaxonDTONormalizer implements DenormalizerInterface, NormalizerInterface, DenormalizerAwareInterface, NormalizerAwareInterface +{ + use CheckArray; + + use DenormalizerAwareTrait; + + use NormalizerAwareTrait; + + public function supportsDenormalization($data, $type, $format = null) + { + return 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\ProductTaxonDTO' === $type; + } + + public function supportsNormalization($data, $format = null) + { + return $data instanceof \MonsieurBiz\SyliusSearchPlugin\Generated\Model\ProductTaxonDTO; + } + + public function denormalize($data, $class, $format = null, array $context = []) + { + if (isset($data['$ref'])) { + return new Reference($data['$ref'], $context['document-origin']); + } + if (isset($data['$recursiveRef'])) { + return new Reference($data['$recursiveRef'], $context['document-origin']); + } + $object = new \MonsieurBiz\SyliusSearchPlugin\Generated\Model\ProductTaxonDTO(); + if (null === $data || false === \is_array($data)) { + return $object; + } + if (\array_key_exists('taxon', $data)) { + $object->setTaxon($this->denormalizer->denormalize($data['taxon'], 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\TaxonDTO', 'json', $context)); + } + if (\array_key_exists('position', $data) && null !== $data['position']) { + $value = $data['position']; + if (null === $data['position']) { + $value = $data['position']; + } elseif (\is_int($data['position'])) { + $value = $data['position']; + } + $object->setPosition($value); + } elseif (\array_key_exists('position', $data) && null === $data['position']) { + $object->setPosition(null); + } + + return $object; + } + + public function normalize($object, $format = null, array $context = []) + { + $data = []; + if (null !== $object->getTaxon()) { + $data['taxon'] = $this->normalizer->normalize($object->getTaxon(), 'json', $context); + } + if (null !== $object->getPosition()) { + $value = $object->getPosition(); + if (null === $object->getPosition()) { + $value = $object->getPosition(); + } elseif (\is_int($object->getPosition())) { + $value = $object->getPosition(); + } + $data['position'] = $value; + } + + return $data; + } +} diff --git a/generated/Normalizer/TaxonDTONormalizer.php b/generated/Normalizer/TaxonDTONormalizer.php new file mode 100644 index 00000000..c7ac195c --- /dev/null +++ b/generated/Normalizer/TaxonDTONormalizer.php @@ -0,0 +1,89 @@ + + * + * 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\Generated\Normalizer; + +use Jane\Component\JsonSchemaRuntime\Reference; +use MonsieurBiz\SyliusSearchPlugin\Generated\Runtime\Normalizer\CheckArray; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +class TaxonDTONormalizer implements DenormalizerInterface, NormalizerInterface, DenormalizerAwareInterface, NormalizerAwareInterface +{ + use CheckArray; + + use DenormalizerAwareTrait; + + use NormalizerAwareTrait; + + public function supportsDenormalization($data, $type, $format = null) + { + return 'MonsieurBiz\\SyliusSearchPlugin\\Generated\\Model\\TaxonDTO' === $type; + } + + public function supportsNormalization($data, $format = null) + { + return $data instanceof \MonsieurBiz\SyliusSearchPlugin\Generated\Model\TaxonDTO; + } + + public function denormalize($data, $class, $format = null, array $context = []) + { + if (isset($data['$ref'])) { + return new Reference($data['$ref'], $context['document-origin']); + } + if (isset($data['$recursiveRef'])) { + return new Reference($data['$recursiveRef'], $context['document-origin']); + } + $object = new \MonsieurBiz\SyliusSearchPlugin\Generated\Model\TaxonDTO(); + if (null === $data || false === \is_array($data)) { + return $object; + } + if (\array_key_exists('name', $data)) { + $object->setName($data['name']); + } + if (\array_key_exists('code', $data)) { + $object->setCode($data['code']); + } + if (\array_key_exists('position', $data)) { + $object->setPosition($data['position']); + } + if (\array_key_exists('level', $data)) { + $object->setLevel($data['level']); + } + + return $object; + } + + public function normalize($object, $format = null, array $context = []) + { + $data = []; + if (null !== $object->getName()) { + $data['name'] = $object->getName(); + } + if (null !== $object->getCode()) { + $data['code'] = $object->getCode(); + } + if (null !== $object->getPosition()) { + $data['position'] = $object->getPosition(); + } + if (null !== $object->getLevel()) { + $data['level'] = $object->getLevel(); + } + + return $data; + } +} diff --git a/generated/Runtime/Normalizer/CheckArray.php b/generated/Runtime/Normalizer/CheckArray.php new file mode 100644 index 00000000..db91ddff --- /dev/null +++ b/generated/Runtime/Normalizer/CheckArray.php @@ -0,0 +1,24 @@ + + * + * 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\Generated\Runtime\Normalizer; + +trait CheckArray +{ + public function isOnlyNumericKeys(array $array): bool + { + return \count(array_filter($array, function ($key) { + return is_numeric($key); + }, \ARRAY_FILTER_USE_KEY)) === \count($array); + } +} diff --git a/generated/Runtime/Normalizer/ReferenceNormalizer.php b/generated/Runtime/Normalizer/ReferenceNormalizer.php new file mode 100644 index 00000000..e7d978e6 --- /dev/null +++ b/generated/Runtime/Normalizer/ReferenceNormalizer.php @@ -0,0 +1,39 @@ + + * + * 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\Generated\Runtime\Normalizer; + +use Jane\Component\JsonSchemaRuntime\Reference; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +class ReferenceNormalizer implements NormalizerInterface +{ + /** + * @inheritdoc + */ + public function normalize($object, $format = null, array $context = []) + { + $ref = []; + $ref['$ref'] = (string) $object->getReferenceUri(); + + return $ref; + } + + /** + * @inheritdoc + */ + public function supportsNormalization($data, $format = null) + { + return $data instanceof Reference; + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..9b4d6622 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "monsieurbiz-sylius-search-plugin", + "version": "0.0.1", + "description": "Add instant search on the search form", + "main": "webpack.config.js", + "scripts": { + "build": "encore production", + "watch": "encore dev --watch" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/monsieurbiz/SyliusSearchPlugin.git" + }, + "author": "Monsieur Biz ", + "license": "MIT", + "bugs": { + "url": "https://github.com/monsieurbiz/SyliusSearchPlugin/issues" + }, + "homepage": "https://github.com/monsieurbiz/SyliusSearchPlugin#readme", + "devDependencies": { + "@symfony/webpack-encore": "^0.28.1" + }, + "dependencies": { + } +} diff --git a/php.ini.dist b/php.ini.dist new file mode 100644 index 00000000..b0fe7fef --- /dev/null +++ b/php.ini.dist @@ -0,0 +1 @@ +memory_limit=-1 diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 00000000..d0c34236 --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpspec.yml.dist b/phpspec.yml.dist index dab4879d..2632afde 100644 --- a/phpspec.yml.dist +++ b/phpspec.yml.dist @@ -2,3 +2,5 @@ suites: main: namespace: MonsieurBiz\SyliusSearchPlugin psr4_prefix: MonsieurBiz\SyliusSearchPlugin + src_path: src + spec_path: tests diff --git a/phpstan.neon b/phpstan.neon index ea8e49e3..708fba1b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,17 +1,17 @@ -includes: - - vendor/phpstan/phpstan-doctrine/extension.neon - - vendor/phpstan/phpstan-webmozart-assert/extension.neon - parameters: - reportUnmatchedIgnoredErrors: false + level: max + paths: + - %rootDir%/src/ + + checkMissingIterableValueType: false excludes_analyse: # Makes PHPStan crash - 'src/DependencyInjection/Configuration.php' + - 'src/DependencyInjection/MonsieurBizSyliusSearchExtension.php' # Test dependencies - - 'tests/Application/app/**.php' - - 'tests/Application/src/**.php' - - ignoreErrors: - - '/Parameter #1 $configuration of method Symfony\Component\DependencyInjection\Extension\Extension::processConfiguration() expects Symfony\Component\Config\Definition\ConfigurationInterface, Symfony\Component\Config\Definition\ConfigurationInterface|null given./' + - 'tests/Application/**/*' + + # Generated files + - 'generated/**/*' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ab87647a..9700364d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,55 @@ - tests + tests/Unit - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/recipe/dev/config/packages/monsieurbiz_sylius_search_plugin.yaml b/recipes/1.0/config/packages/monsieurbiz_sylius_search_plugin.yaml similarity index 100% rename from recipe/dev/config/packages/monsieurbiz_sylius_search_plugin.yaml rename to recipes/1.0/config/packages/monsieurbiz_sylius_search_plugin.yaml diff --git a/recipes/1.0/config/routes/monsieurbiz_sylius_search_plugin.yaml b/recipes/1.0/config/routes/monsieurbiz_sylius_search_plugin.yaml new file mode 100644 index 00000000..6ad0857e --- /dev/null +++ b/recipes/1.0/config/routes/monsieurbiz_sylius_search_plugin.yaml @@ -0,0 +1,2 @@ +monsieurbiz_search_plugin: + resource: "@MonsieurBizSyliusSearchPlugin/Resources/config/routing.yaml" diff --git a/recipe/dev/manifest.json b/recipes/1.0/manifest.json similarity index 76% rename from recipe/dev/manifest.json rename to recipes/1.0/manifest.json index 3166429d..acb378dd 100644 --- a/recipe/dev/manifest.json +++ b/recipes/1.0/manifest.json @@ -1,12 +1,15 @@ { "bundles": { - "MonsieurBiz\\SyliusSearchPlugin\\MonsieurBizSyliusSearchPlugin::class": [ + "MonsieurBiz\\SyliusSearchPlugin\\MonsieurBizSyliusSearchPlugin": [ "all" ] }, "copy-from-recipe": { "config/": "%CONFIG_DIR%/" }, + "copy-from-package": { + "src/Resources/templates/": "templates/" + }, "env": { "MONSIEURBIZ_SEARCHPLUGIN_ES_HOST": "localhost", "MONSIEURBIZ_SEARCHPLUGIN_ES_PORT": "9200" diff --git a/recipes/2.0-dev b/recipes/2.0-dev new file mode 120000 index 00000000..c3318381 --- /dev/null +++ b/recipes/2.0-dev @@ -0,0 +1 @@ +2.0/ \ No newline at end of file diff --git a/recipes/2.0/config/packages/monsieurbiz_sylius_search_plugin.yaml b/recipes/2.0/config/packages/monsieurbiz_sylius_search_plugin.yaml new file mode 100644 index 00000000..759ac0d9 --- /dev/null +++ b/recipes/2.0/config/packages/monsieurbiz_sylius_search_plugin.yaml @@ -0,0 +1,2 @@ +imports: + - { resource: "@MonsieurBizSyliusSearchPlugin/Resources/config/config.yaml" } diff --git a/recipes/2.0/config/routes/monsieurbiz_sylius_search_plugin.yaml b/recipes/2.0/config/routes/monsieurbiz_sylius_search_plugin.yaml new file mode 100644 index 00000000..ea7b6f92 --- /dev/null +++ b/recipes/2.0/config/routes/monsieurbiz_sylius_search_plugin.yaml @@ -0,0 +1,2 @@ +monsieurbiz_search_plugin: + resource: "@MonsieurBizSyliusSearchPlugin/Resources/config/routing.yaml" diff --git a/recipes/2.0/manifest.json b/recipes/2.0/manifest.json new file mode 100644 index 00000000..769b861f --- /dev/null +++ b/recipes/2.0/manifest.json @@ -0,0 +1,18 @@ +{ + "bundles": { + "MonsieurBiz\\SyliusSearchPlugin\\MonsieurBizSyliusSearchPlugin": [ + "all" + ] + }, + "copy-from-recipe": { + "config/": "%CONFIG_DIR%/" + }, + "copy-from-package": { + "src/Resources/templates/": "templates/" + }, + "env": { + "MONSIEURBIZ_SEARCHPLUGIN_ES_HOST": "${ELASTICSEARCH_HOST:-localhost}", + "MONSIEURBIZ_SEARCHPLUGIN_ES_PORT": "${ELASTICSEARCH_PORT:-9200}", + "MONSIEURBIZ_SEARCHPLUGIN_MESSENGER_TRANSPORT_DSN": "doctrine://default" + } +} diff --git a/screenshot_instant.jpg b/screenshot_instant.jpg deleted file mode 100644 index 295722e3..00000000 Binary files a/screenshot_instant.jpg and /dev/null differ diff --git a/screenshot_search.jpg b/screenshot_search.jpg deleted file mode 100644 index 2d24a7f1..00000000 Binary files a/screenshot_search.jpg and /dev/null differ diff --git a/screenshot_taxon.jpg b/screenshot_taxon.jpg deleted file mode 100644 index 42d74632..00000000 Binary files a/screenshot_taxon.jpg and /dev/null differ diff --git a/src/Adapter/ResultSetAdapter.php b/src/Adapter/ResultSetAdapter.php deleted file mode 100644 index dfc1f528..00000000 --- a/src/Adapter/ResultSetAdapter.php +++ /dev/null @@ -1,59 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Adapter; - -use MonsieurBiz\SyliusSearchPlugin\Model\Document\ResultSet; -use Pagerfanta\Adapter\AdapterInterface; - -class ResultSetAdapter implements AdapterInterface -{ - /** @var ResultSet */ - private $resultSet; - - /** - * Constructor. - * - * @param ResultSet $resultSet - */ - public function __construct(ResultSet $resultSet) - { - $this->resultSet = $resultSet; - } - - /** - * Returns the array. - * - * @return ResultSet - */ - public function getResultSet() - { - return $this->resultSet; - } - - /** - * {@inheritdoc} - */ - public function getNbResults() - { - return $this->resultSet->getTotalHits(); - } - - /** - * {@inheritdoc} - */ - public function getSlice($offset, $length) - { - return \array_slice($this->resultSet->getResults(), $offset, $length); - } -} diff --git a/src/AutoMapper/Configuration.php b/src/AutoMapper/Configuration.php new file mode 100644 index 00000000..e7ae44cd --- /dev/null +++ b/src/AutoMapper/Configuration.php @@ -0,0 +1,51 @@ + + * + * 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\AutoMapper; + +use RuntimeException; + +final class Configuration +{ + private array $sourceClasses = []; + + private array $targetClasses = []; + + public function addSourceClass(string $identifier, string $className): void + { + $this->sourceClasses[$identifier] = $className; + } + + public function getSourceClass(string $identifier): string + { + if (!\array_key_exists($identifier, $this->sourceClasses)) { + throw new RuntimeException('Unknown source class for: ' . $identifier); + } + + return $this->sourceClasses[$identifier]; + } + + public function addTargetClass(string $identifier, string $className): void + { + $this->targetClasses[$identifier] = $className; + } + + public function getTargetClass(string $identifier): string + { + if (!\array_key_exists($identifier, $this->targetClasses)) { + throw new RuntimeException('Unknown target class for: ' . $identifier); + } + + return $this->targetClasses[$identifier]; + } +} diff --git a/src/AutoMapper/ProductAttributeValueConfiguration.php b/src/AutoMapper/ProductAttributeValueConfiguration.php new file mode 100644 index 00000000..5811559b --- /dev/null +++ b/src/AutoMapper/ProductAttributeValueConfiguration.php @@ -0,0 +1,79 @@ + + * + * 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\AutoMapper; + +use Jane\Bundle\AutoMapperBundle\Configuration\MapperConfigurationInterface; +use Jane\Component\AutoMapper\MapperGeneratorMetadataInterface; +use Jane\Component\AutoMapper\MapperMetadata; +use MonsieurBiz\SyliusSearchPlugin\AutoMapper\ProductAttributeValueReader\ReaderInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; +use RuntimeException; +use Sylius\Component\Product\Model\ProductAttributeValueInterface; + +final class ProductAttributeValueConfiguration implements MapperConfigurationInterface, LoggerAwareInterface +{ + use LoggerAwareTrait; + + private Configuration $configuration; + + /** + * @var ReaderInterface[] + */ + private array $productAttributeValueReaders; + + public function __construct(Configuration $configuration, iterable $productAttributeValueReaders) + { + $this->logger = new NullLogger(); + $this->configuration = $configuration; + $this->productAttributeValueReaders = $productAttributeValueReaders instanceof \Traversable + ? iterator_to_array($productAttributeValueReaders) + : $productAttributeValueReaders; + } + + public function process(MapperGeneratorMetadataInterface $metadata): void + { + if (!$metadata instanceof MapperMetadata) { + return; + } + if (0 === \count($this->productAttributeValueReaders)) { + 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); + }); + } + + public function getSource(): string + { + return $this->configuration->getSourceClass('product_attribute_value'); + } + + public function getTarget(): string + { + return $this->configuration->getTargetClass('product_attribute'); + } +} diff --git a/src/AutoMapper/ProductAttributeValueReader/CheckboxReader.php b/src/AutoMapper/ProductAttributeValueReader/CheckboxReader.php new file mode 100644 index 00000000..705799da --- /dev/null +++ b/src/AutoMapper/ProductAttributeValueReader/CheckboxReader.php @@ -0,0 +1,22 @@ + + * + * 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\AutoMapper\ProductAttributeValueReader; + +class CheckboxReader extends DefaultReader +{ + public static function getReaderCode(): string + { + return 'checkbox'; + } +} diff --git a/src/AutoMapper/ProductAttributeValueReader/DateReader.php b/src/AutoMapper/ProductAttributeValueReader/DateReader.php new file mode 100644 index 00000000..1ebd7c4e --- /dev/null +++ b/src/AutoMapper/ProductAttributeValueReader/DateReader.php @@ -0,0 +1,24 @@ + + * + * 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\AutoMapper\ProductAttributeValueReader; + +class DateReader extends DateTimeReader implements ReaderInterface +{ + protected string $defaultFormat = 'Y-m-d'; + + public static function getReaderCode(): string + { + return 'date'; + } +} diff --git a/src/AutoMapper/ProductAttributeValueReader/DateTimeReader.php b/src/AutoMapper/ProductAttributeValueReader/DateTimeReader.php new file mode 100644 index 00000000..bb6e6786 --- /dev/null +++ b/src/AutoMapper/ProductAttributeValueReader/DateTimeReader.php @@ -0,0 +1,40 @@ + + * + * 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\AutoMapper\ProductAttributeValueReader; + +use Sylius\Component\Product\Model\ProductAttributeValueInterface; + +class DateTimeReader implements ReaderInterface +{ + protected string $defaultFormat = 'Y-m-d H:i:s'; + + public function getValue(ProductAttributeValueInterface $productAttribute) + { + if (null === $productAttribute->getAttribute()) { + return ''; + } + + $productAttributeValue = $productAttribute->getValue(); + if ($productAttributeValue instanceof \DateTime) { + $productAttributeValue = $productAttributeValue->format($this->defaultFormat); + } + + return $productAttributeValue; + } + + public static function getReaderCode(): string + { + return 'datetime'; + } +} diff --git a/src/AutoMapper/ProductAttributeValueReader/DefaultReader.php b/src/AutoMapper/ProductAttributeValueReader/DefaultReader.php new file mode 100644 index 00000000..77bc57af --- /dev/null +++ b/src/AutoMapper/ProductAttributeValueReader/DefaultReader.php @@ -0,0 +1,26 @@ + + * + * 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\AutoMapper\ProductAttributeValueReader; + +use Sylius\Component\Product\Model\ProductAttributeValueInterface; + +abstract class DefaultReader implements ReaderInterface +{ + public function getValue(ProductAttributeValueInterface $productAttribute) + { + return (string) $productAttribute->getValue(); + } + + abstract public static function getReaderCode(): string; +} diff --git a/src/AutoMapper/ProductAttributeValueReader/IntegerReader.php b/src/AutoMapper/ProductAttributeValueReader/IntegerReader.php new file mode 100644 index 00000000..d3fde12c --- /dev/null +++ b/src/AutoMapper/ProductAttributeValueReader/IntegerReader.php @@ -0,0 +1,22 @@ + + * + * 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\AutoMapper\ProductAttributeValueReader; + +class IntegerReader extends DefaultReader +{ + public static function getReaderCode(): string + { + return 'integer'; + } +} diff --git a/src/AutoMapper/ProductAttributeValueReader/PercentReader.php b/src/AutoMapper/ProductAttributeValueReader/PercentReader.php new file mode 100644 index 00000000..09dd8e62 --- /dev/null +++ b/src/AutoMapper/ProductAttributeValueReader/PercentReader.php @@ -0,0 +1,22 @@ + + * + * 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\AutoMapper\ProductAttributeValueReader; + +class PercentReader extends DefaultReader +{ + public static function getReaderCode(): string + { + return 'percent'; + } +} diff --git a/src/AutoMapper/ProductAttributeValueReader/ReaderInterface.php b/src/AutoMapper/ProductAttributeValueReader/ReaderInterface.php new file mode 100644 index 00000000..dc135260 --- /dev/null +++ b/src/AutoMapper/ProductAttributeValueReader/ReaderInterface.php @@ -0,0 +1,26 @@ + + * + * 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\AutoMapper\ProductAttributeValueReader; + +use Sylius\Component\Product\Model\ProductAttributeValueInterface; + +interface ReaderInterface +{ + public static function getReaderCode(): string; + + /** + * @return string|array + */ + public function getValue(ProductAttributeValueInterface $productAttribute); +} diff --git a/src/AutoMapper/ProductAttributeValueReader/SelectReader.php b/src/AutoMapper/ProductAttributeValueReader/SelectReader.php new file mode 100644 index 00000000..1c30340a --- /dev/null +++ b/src/AutoMapper/ProductAttributeValueReader/SelectReader.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 MonsieurBiz\SyliusSearchPlugin\AutoMapper\ProductAttributeValueReader; + +use Sylius\Component\Product\Model\ProductAttributeValueInterface; +use Sylius\Component\Resource\Translation\Provider\TranslationLocaleProviderInterface; + +class SelectReader implements ReaderInterface +{ + private string $defaultLocaleCode; + + public function __construct(TranslationLocaleProviderInterface $localeProvider) + { + $this->defaultLocaleCode = $localeProvider->getDefaultLocaleCode(); + } + + public function getValue(ProductAttributeValueInterface $productAttribute) + { + if (null === $productAttribute->getAttribute()) { + return ''; + } + + $currentLocale = $productAttribute->getLocaleCode(); + $choices = $productAttribute->getAttribute()->getConfiguration()['choices'] ?? []; + $productAttributeValue = $productAttribute->getValue(); + if (!is_iterable($productAttributeValue)) { + $productAttributeValue = [$productAttributeValue]; + } + + $result = []; + foreach ($productAttributeValue as $value) { + $locale = $currentLocale; + if (!isset($choices[$value][$locale])) { + $locale = $this->defaultLocaleCode; + } + $result[] = $choices[$value][$locale]; + } + + return $result; + } + + public static function getReaderCode(): string + { + return 'select'; + } +} diff --git a/src/Model/ArrayObject.php b/src/AutoMapper/ProductAttributeValueReader/TextReader.php similarity index 58% rename from src/Model/ArrayObject.php rename to src/AutoMapper/ProductAttributeValueReader/TextReader.php index 418ff6d8..b28c8f04 100644 --- a/src/Model/ArrayObject.php +++ b/src/AutoMapper/ProductAttributeValueReader/TextReader.php @@ -5,18 +5,18 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * 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\Model; +namespace MonsieurBiz\SyliusSearchPlugin\AutoMapper\ProductAttributeValueReader; -class ArrayObject extends \ArrayObject +class TextReader extends DefaultReader { - public function toArray(): array + public static function getReaderCode(): string { - return $this->getArrayCopy(); + return 'text'; } } diff --git a/src/AutoMapper/ProductAttributeValueReader/TextareaReader.php b/src/AutoMapper/ProductAttributeValueReader/TextareaReader.php new file mode 100644 index 00000000..8a4cde49 --- /dev/null +++ b/src/AutoMapper/ProductAttributeValueReader/TextareaReader.php @@ -0,0 +1,22 @@ + + * + * 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\AutoMapper\ProductAttributeValueReader; + +class TextareaReader extends DefaultReader +{ + public static function getReaderCode(): string + { + return 'textarea'; + } +} diff --git a/src/AutoMapper/ProductMapperConfiguration.php b/src/AutoMapper/ProductMapperConfiguration.php new file mode 100644 index 00000000..97d8ee69 --- /dev/null +++ b/src/AutoMapper/ProductMapperConfiguration.php @@ -0,0 +1,231 @@ + + * + * 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\AutoMapper; + +use DateTimeInterface; +use Jane\Bundle\AutoMapperBundle\Configuration\MapperConfigurationInterface; +use Jane\Component\AutoMapper\AutoMapperInterface; +use Jane\Component\AutoMapper\MapperGeneratorMetadataInterface; +use Jane\Component\AutoMapper\MapperMetadata; +use MonsieurBiz\SyliusSearchPlugin\Entity\Product\SearchableInterface; +use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\ProductInterface; +use Sylius\Component\Core\Model\ProductTaxonInterface; +use Sylius\Component\Core\Model\ProductVariantInterface as ModelProductVariantInterface; +use Sylius\Component\Inventory\Checker\AvailabilityCheckerInterface; +use Sylius\Component\Inventory\Model\StockableInterface; +use Sylius\Component\Product\Model\ProductVariantInterface; +use Sylius\Component\Product\Resolver\ProductVariantResolverInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; + +final class ProductMapperConfiguration implements MapperConfigurationInterface +{ + private Configuration $configuration; + + private AutoMapperInterface $autoMapper; + + private ProductVariantResolverInterface $productVariantResolver; + + private RequestStack $requestStack; + + private AvailabilityCheckerInterface $availabilityChecker; + + public function __construct( + Configuration $configuration, + AutoMapperInterface $autoMapper, + ProductVariantResolverInterface $productVariantResolver, + RequestStack $requestStack, + AvailabilityCheckerInterface $availabilityChecker + ) { + $this->configuration = $configuration; + $this->autoMapper = $autoMapper; + $this->productVariantResolver = $productVariantResolver; + $this->requestStack = $requestStack; + $this->availabilityChecker = $availabilityChecker; + } + + public function process(MapperGeneratorMetadataInterface $metadata): void + { + if (!$metadata instanceof MapperMetadata) { + return; + } + + $metadata->forMember('id', function (ProductInterface $product): int { + return $product->getId(); + }); + + $metadata->forMember('code', function (ProductInterface $product): ?string { + return $product->getCode(); + }); + + $metadata->forMember('enabled', function (ProductInterface $product): bool { + return $product->isEnabled(); + }); + + $metadata->forMember('slug', function (ProductInterface $product): ?string { + return $product->getSlug(); + }); + + $metadata->forMember('name', function (ProductInterface $product): ?string { + return $product->getName(); + }); + + $metadata->forMember('description', function (ProductInterface $product): ?string { + return $product->getDescription(); + }); + + $metadata->forMember('created_at', function (ProductInterface $product): ?DateTimeInterface { + return $product->getCreatedAt(); + }); + + $metadata->forMember('images', function (ProductInterface $product): array { + $images = []; + $imageDTOClass = $this->configuration->getTargetClass('image'); + foreach ($product->getImages() as $image) { + $images[] = $this->autoMapper->map($image, $imageDTOClass); + } + + return $images; + }); + + $metadata->forMember('mainTaxon', function (ProductInterface $product) { + return null !== $product->getMainTaxon() + ? $this->autoMapper->map($product->getMainTaxon(), $this->configuration->getTargetClass('taxon')) + : null; + }); + + $metadata->forMember('product_taxons', function (ProductInterface $product): array { + return array_map(function (ProductTaxonInterface $productTaxon) { + // todo add parent taxon in Taxon object with automapper + return $this->autoMapper->map($productTaxon, $this->configuration->getTargetClass('product_taxon')); + }, $product->getProductTaxons()->toArray()); + }); + + $metadata->forMember('channels', function (ProductInterface $product): array { + return array_map(function (ChannelInterface $channel) { + return $this->autoMapper->map($channel, $this->configuration->getTargetClass('channel')); + }, $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); + } + + return $attributes; + }); + + $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, + ]; + } + } + + foreach ($options as $optionCode => $optionValues) { + $options[$optionCode]['values'] = array_values($optionValues['values']); + } + + return $options; + }); + + $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); + } + + return $variants; + }); + + $metadata->forMember('prices', function (ProductInterface $product): array { + $prices = []; + foreach ($product->getChannels() as $channel) { + /** @var ChannelInterface $channel */ + $request = new Request(['_channel_code' => $channel->getCode()]); + $this->requestStack->push($request); + if ( + null === ($variant = $this->productVariantResolver->getVariant($product)) + || !$variant instanceof ModelProductVariantInterface + || null === ($channelPricing = $variant->getChannelPricingForChannel($channel)) + ) { + $this->requestStack->pop(); + + continue; + } + $this->requestStack->pop(); + $prices[] = $this->autoMapper->map( + $channelPricing, + $this->configuration->getTargetClass('pricing') + ); + } + + return $prices; + }); + } + + public function getSource(): string + { + return $this->configuration->getSourceClass('product'); + } + + public function getTarget(): string + { + return $this->configuration->getTargetClass('product'); + } + + private function isProductVariantInStock(ProductVariantInterface $productVariant): bool + { + if (!$productVariant instanceof StockableInterface) { + return true; + } + + return $this->availabilityChecker->isStockAvailable($productVariant); + } +} diff --git a/src/AutoMapper/VariantMapperConfiguration.php b/src/AutoMapper/VariantMapperConfiguration.php new file mode 100644 index 00000000..9388a210 --- /dev/null +++ b/src/AutoMapper/VariantMapperConfiguration.php @@ -0,0 +1,59 @@ + + * + * 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\AutoMapper; + +use Jane\Bundle\AutoMapperBundle\Configuration\MapperConfigurationInterface; +use Jane\Component\AutoMapper\MapperGeneratorMetadataInterface; +use Jane\Component\AutoMapper\MapperMetadata; +use Sylius\Component\Inventory\Checker\AvailabilityCheckerInterface; +use Sylius\Component\Inventory\Model\StockableInterface; +use Sylius\Component\Product\Model\ProductVariantInterface; + +final class VariantMapperConfiguration implements MapperConfigurationInterface +{ + private Configuration $configuration; + + private AvailabilityCheckerInterface $availabilityChecker; + + public function __construct(Configuration $configuration, AvailabilityCheckerInterface $availabilityChecker) + { + $this->configuration = $configuration; + $this->availabilityChecker = $availabilityChecker; + } + + public function process(MapperGeneratorMetadataInterface $metadata): void + { + if (!$metadata instanceof MapperMetadata) { + return; + } + + $metadata->forMember('is_in_stock', function (ProductVariantInterface $productVariant): bool { + if (!$productVariant instanceof StockableInterface) { + return true; + } + + return $this->availabilityChecker->isStockAvailable($productVariant); + }); + } + + public function getSource(): string + { + return $this->configuration->getSourceClass('product_variant'); + } + + public function getTarget(): string + { + return $this->configuration->getTargetClass('product_variant'); + } +} diff --git a/src/Command/PopulateCommand.php b/src/Command/PopulateCommand.php index a213368b..b0af5ba2 100644 --- a/src/Command/PopulateCommand.php +++ b/src/Command/PopulateCommand.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -13,55 +13,33 @@ namespace MonsieurBiz\SyliusSearchPlugin\Command; -use MonsieurBiz\SyliusSearchPlugin\Exception\ReadOnlyIndexException; -use MonsieurBiz\SyliusSearchPlugin\Model\Document\Index\Indexer; +use MonsieurBiz\SyliusSearchPlugin\Index\Indexer; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class PopulateCommand extends Command { - /** - * @var string - */ protected static $defaultName = 'monsieurbiz:search:populate'; - /** - * @var Indexer - */ - protected $documentIndexer; + private Indexer $indexer; - /** - * PopulateCommand constructor. - * - * @param Indexer $documentIndexer - */ - public function __construct(Indexer $documentIndexer) + public function __construct(Indexer $indexer, $name = null) { - $this->documentIndexer = $documentIndexer; - parent::__construct(static::$defaultName); + parent::__construct($name); + $this->indexer = $indexer; + } + + protected function configure(): void + { + parent::configure(); } - /** - * Populate ES. - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int 0 if everything went fine, or an exit code - */ protected function execute(InputInterface $input, OutputInterface $output) { - $output->writeln(sprintf('Generating index')); - try { - $this->documentIndexer->indexAll(); - } catch (ReadOnlyIndexException $exception) { - $output->writeln('Cannot purge old index. Please to do it manually if needed.'); - // it's better to use return Command::FAILURE; in Symfony 5 - return 1; - } - $output->writeln(sprintf('Generated index')); - // it's better to use return Command::SUCCESS; in Symfony 5 - return 0; + $this->indexer->indexAll(); + $output->writeln('ok'); + + return Command::SUCCESS; } } diff --git a/src/Command/SearchCommand.php b/src/Command/SearchCommand.php new file mode 100644 index 00000000..aa026d72 --- /dev/null +++ b/src/Command/SearchCommand.php @@ -0,0 +1,108 @@ + + * + * 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\Command; + +use Elastica\Exception\Connection\HttpException; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Model\Product\ProductDTO; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Search; +use MonsieurBiz\SyliusSettingsPlugin\Settings\SettingsInterface; +use Sylius\Component\Channel\Context\ChannelContextInterface; +use Sylius\Component\Registry\ServiceRegistryInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; + +class SearchCommand extends Command +{ + protected static $defaultName = 'monsieurbiz:search:search'; + + private Search $search; + + private RequestStack $requestStack; + + private ChannelContextInterface $channelContext; + + private SettingsInterface $searchSettings; + + private ServiceRegistryInterface $documentableRegistry; + + public function __construct( + Search $search, + RequestStack $requestStack, + ChannelContextInterface $channelContext, + SettingsInterface $searchSettings, + ServiceRegistryInterface $documentableRegistry, + $name = null + ) { + parent::__construct($name); + $this->search = $search; + $this->requestStack = $requestStack; + $this->channelContext = $channelContext; + $this->searchSettings = $searchSettings; + $this->documentableRegistry = $documentableRegistry; + } + + protected function configure(): void + { + parent::configure(); + $this->addArgument('query', InputArgument::REQUIRED, 'Search query'); + $this->addOption('channel', 'c', InputOption::VALUE_OPTIONAL, 'Channel code', 'FASHION_WEB'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $query = $input->getArgument('query'); + $request = new Request(['query' => $query, '_channel_code' => $input->getOption('channel')]); + $this->requestStack->push($request); + /** @var DocumentableInterface $documentable */ + $documentable = $this->documentableRegistry->get('search.documentable.monsieurbiz_product'); + $requestConfiguration = new RequestConfiguration( + $request, + RequestInterface::SEARCH_TYPE, + $documentable, + $this->searchSettings, + $this->channelContext + ); + + try { + $result = $this->search->search($requestConfiguration); + } catch (HttpException $exception) { + $io->error('Error with the HTTP request: ' . $exception->getMessage()); + + return Command::FAILURE; + } + + $io->title('Search result for: ' . $query); + $io->section('Nb results: ' . $result->count()); + $documents = []; + foreach ($result->getIterator() as $resultItem) { + /** @var ProductDTO $productDTO */ + $productDTO = $resultItem->getModel(); + $documents[] = [$resultItem->getScore(), $productDTO->getData('id')]; + } + $io->table(['Score', 'Document ID'], $documents); + + return Command::SUCCESS; + } +} diff --git a/src/Context/RequestTaxonContext.php b/src/Context/RequestTaxonContext.php deleted file mode 100644 index b0ce4138..00000000 --- a/src/Context/RequestTaxonContext.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Context; - -use MonsieurBiz\SyliusSearchPlugin\Exception\TaxonNotFoundException; -use Sylius\Component\Core\Model\TaxonInterface; -use Sylius\Component\Locale\Context\LocaleContextInterface; -use Sylius\Component\Taxonomy\Repository\TaxonRepositoryInterface; -use Symfony\Component\HttpFoundation\RequestStack; - -final class RequestTaxonContext implements TaxonContextInterface -{ - /** @var RequestStack */ - private $requestStack; - - /** @var TaxonRepositoryInterface */ - private $taxonRepository; - - /** @var LocaleContextInterface */ - private $localeContext; - - public function __construct( - RequestStack $requestStack, - TaxonRepositoryInterface $taxonRepository, - LocaleContextInterface $localeContext - ) { - $this->requestStack = $requestStack; - $this->taxonRepository = $taxonRepository; - $this->localeContext = $localeContext; - } - - public function getTaxon(): TaxonInterface - { - $slug = htmlspecialchars($this->requestStack->getCurrentRequest()->get('slug')); - $localeCode = $this->localeContext->getLocaleCode(); - - /** @var TaxonInterface $taxon */ - $taxon = $this->taxonRepository->findOneBySlug($slug, $localeCode); - - if (null === $slug || null === $taxon) { - throw new TaxonNotFoundException(); - } - - return $taxon; - } -} diff --git a/src/Controller/SearchController.php b/src/Controller/SearchController.php index 883f82eb..f0b3d41c 100644 --- a/src/Controller/SearchController.php +++ b/src/Controller/SearchController.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -13,186 +13,147 @@ namespace MonsieurBiz\SyliusSearchPlugin\Controller; -use MonsieurBiz\SyliusSearchPlugin\Context\TaxonContextInterface; -use MonsieurBiz\SyliusSearchPlugin\Exception\MissingLocaleException; -use MonsieurBiz\SyliusSearchPlugin\Exception\NotSupportedTypeException; -use MonsieurBiz\SyliusSearchPlugin\Helper\RenderDocumentUrlHelper; -use MonsieurBiz\SyliusSearchPlugin\Model\Config\GridConfig; -use MonsieurBiz\SyliusSearchPlugin\Model\Document\Index\Search; -use MonsieurBiz\SyliusSearchPlugin\Model\Document\Result; -use MonsieurBiz\SyliusSearchPlugin\Model\Document\ResultSet; +use MonsieurBiz\SyliusSearchPlugin\Exception\UnknownRequestTypeException; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Search; +use MonsieurBiz\SyliusSettingsPlugin\Settings\SettingsInterface; +use Sylius\Bundle\ResourceBundle\Controller\Parameters; +use Sylius\Bundle\ResourceBundle\Controller\ParametersParserInterface; use Sylius\Component\Channel\Context\ChannelContextInterface; use Sylius\Component\Currency\Context\CurrencyContextInterface; +use Sylius\Component\Locale\Context\LocaleContextInterface; +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 Twig\Environment; +use Symfony\Component\Intl\Currencies; class SearchController extends AbstractController { - public const SORT_ASC = 'asc'; - public const SORT_DESC = 'desc'; + private Search $search; - /** @var Environment */ - private $templatingEngine; + private CurrencyContextInterface $currencyContext; - /** @var Search */ - private $documentSearch; + private LocaleContextInterface $localeContext; - /** @var ChannelContextInterface */ - private $channelContext; + private ChannelContextInterface $channelContext; - /** @var CurrencyContextInterface */ - private $currencyContext; + private SettingsInterface $searchSettings; - /** @var TaxonContextInterface */ - private $taxonContext; + private ServiceRegistryInterface $documentableRegistry; - /** @var GridConfig */ - private $gridConfig; - - /** @var RenderDocumentUrlHelper */ - private $renderDocumentUrlHelper; + private ParametersParserInterface $parametersParser; public function __construct( - Environment $templatingEngine, - Search $documentSearch, - ChannelContextInterface $channelContext, + Search $search, CurrencyContextInterface $currencyContext, - TaxonContextInterface $taxonContext, - GridConfig $gridConfig, - RenderDocumentUrlHelper $renderDocumentUrlHelper + LocaleContextInterface $localeContext, + ChannelContextInterface $channelContext, + SettingsInterface $searchSettings, + ServiceRegistryInterface $documentableRegistry, + ParametersParserInterface $parametersParser ) { - $this->templatingEngine = $templatingEngine; - $this->documentSearch = $documentSearch; - $this->channelContext = $channelContext; + $this->search = $search; $this->currencyContext = $currencyContext; - $this->taxonContext = $taxonContext; - $this->gridConfig = $gridConfig; - $this->renderDocumentUrlHelper = $renderDocumentUrlHelper; + $this->localeContext = $localeContext; + $this->channelContext = $channelContext; + $this->searchSettings = $searchSettings; + $this->documentableRegistry = $documentableRegistry; + $this->parametersParser = $parametersParser; } - /** - * Post search. - * - * @param Request $request - * - * @return RedirectResponse - */ - public function postAction(Request $request) + // TODO add an optional parameter $documentType (nullable => get the default document type) + public function searchAction(Request $request, string $query): Response { - $query = $request->request->get('monsieurbiz_searchplugin_search')['query'] ?? null; - - return new RedirectResponse( - $this->generateUrl('monsieurbiz_sylius_search_search', - ['query' => urlencode($query)]) + /** @var DocumentableInterface $documentable */ + $documentable = $this->documentableRegistry->get('search.documentable.monsieurbiz_product'); + $requestConfiguration = new RequestConfiguration( + $request, + RequestInterface::SEARCH_TYPE, + $documentable, + $this->searchSettings, + $this->channelContext ); + $result = $this->search->search($requestConfiguration); + + return $this->render('@MonsieurBizSyliusSearchPlugin/Search/result.html.twig', [ + 'documentable' => $result->getDocumentable(), + 'requestConfiguration' => $requestConfiguration, + 'query' => urldecode($query), + 'result' => $result, + 'currencySymbol' => Currencies::getSymbol($this->currencyContext->getCurrencyCode(), $this->localeContext->getLocaleCode()), + ]); } /** - * Perform the search action & display results. User can add page, limit or sorting. - * - * @param Request $request - * - * @return Response + * Post search. */ - public function searchAction(Request $request): Response + public function postAction(Request $request): RedirectResponse { - // Init grid config depending on request - $this->gridConfig->init(GridConfig::SEARCH_TYPE, $request); - - // Perform search - /** @var ResultSet $resultSet */ - $resultSet = $this->documentSearch->search($this->gridConfig); - - // Redirect to document if only one result and no filter applied - $appliedFilters = $this->gridConfig->getAppliedFilters(); - if (1 === $resultSet->getTotalHits() && empty($appliedFilters)) { - /** @var Result $document */ - $document = current($resultSet->getResults()); - try { - $urlParams = $this->renderDocumentUrlHelper->getUrlParams($document); - - return new RedirectResponse($this->generateUrl($urlParams->getPath(), $urlParams->getParams())); - } catch (NotSupportedTypeException $e) { - // Return list of results if cannot redirect, so ignore Exception - } catch (MissingLocaleException $e) { - // Return list of results if locale is missing - } - } - - // Get number formatter for currency - $currencyCode = $this->currencyContext->getCurrencyCode(); - $formatter = new \NumberFormatter($request->getLocale() . '@currency=' . $currencyCode, \NumberFormatter::CURRENCY); - - // Display result list - return new Response($this->templatingEngine->render('@MonsieurBizSyliusSearchPlugin/Search/result.html.twig', [ - 'query' => $this->gridConfig->getQuery(), - 'limits' => $this->gridConfig->getLimits(), - 'resultSet' => $resultSet, - 'channel' => $this->channelContext->getChannel(), - 'currencyCode' => $this->currencyContext->getCurrencyCode(), - 'moneySymbol' => $formatter->getSymbol(\NumberFormatter::CURRENCY_SYMBOL), - 'gridConfig' => $this->gridConfig, - ])); + $query = (array) $request->request->get('monsieurbiz_searchplugin_search') ?? []; + $query = $query['query'] ?? ''; + + return $this->redirect( + $this->generateUrl( + 'monsieurbiz_search_search', + ['query' => urlencode($query)] + ) + ); } /** * Perform the instant search action & display results. - * - * @param Request $request - * - * @return Response */ public function instantAction(Request $request): Response { - // Init grid config depending on request - $this->gridConfig->init(GridConfig::INSTANT_TYPE, $request); - - // Perform instant search - /** @var ResultSet $resultSet */ - $resultSet = $this->documentSearch->instant($this->gridConfig); - - // Display instant result list - return new Response($this->templatingEngine->render('@MonsieurBizSyliusSearchPlugin/Instant/result.html.twig', [ - 'query' => $this->gridConfig->getQuery(), - 'resultSet' => $resultSet, - 'channel' => $this->channelContext->getChannel(), - 'currencyCode' => $this->currencyContext->getCurrencyCode(), - 'gridConfig' => $this->gridConfig, - ])); + $results = []; + /** @var DocumentableInterface $documentable */ + foreach ($this->documentableRegistry->all() as $documentable) { + if (!(bool) $this->searchSettings->getCurrentValue($this->channelContext->getChannel(), null, 'instant_search_enabled__' . $documentable->getIndexCode())) { + continue; + } + + $requestConfiguration = new RequestConfiguration( + $request, + RequestInterface::INSTANT_TYPE, + $documentable, + $this->searchSettings, + $this->channelContext + ); + + try { + $results[] = $this->search->search($requestConfiguration); + } catch (UnknownRequestTypeException $e) { + continue; + } + } + + return $this->render('@MonsieurBizSyliusSearchPlugin/Instant/result.html.twig', [ + 'results' => $results, + ]); } - /** - * Perform the taxon action & display results. - * - * @param Request $request - * - * @return Response - */ public function taxonAction(Request $request): Response { - // Init grid config depending on request - $this->gridConfig->init(GridConfig::TAXON_TYPE, $request, $this->taxonContext->getTaxon()); - - // Perform search - /** @var ResultSet $resultSet */ - $resultSet = $this->documentSearch->taxon($this->gridConfig); - - // Get number formatter for currency - $currencyCode = $this->currencyContext->getCurrencyCode(); - $formatter = new \NumberFormatter($request->getLocale() . '@currency=' . $currencyCode, \NumberFormatter::CURRENCY); - - // Display result list - return new Response($this->templatingEngine->render('@MonsieurBizSyliusSearchPlugin/Taxon/result.html.twig', [ - 'taxon' => $this->gridConfig->getTaxon(), - 'limits' => $this->gridConfig->getLimits(), - 'resultSet' => $resultSet, - 'channel' => $this->channelContext->getChannel(), - 'currencyCode' => $this->currencyContext->getCurrencyCode(), - 'moneySymbol' => $formatter->getSymbol(\NumberFormatter::CURRENCY_SYMBOL), - 'gridConfig' => $this->gridConfig, - ])); + /** @var DocumentableInterface $documentable */ + $documentable = $this->documentableRegistry->get('search.documentable.monsieurbiz_product'); + $requestConfiguration = new RequestConfiguration( + $request, + RequestInterface::TAXON_TYPE, + $documentable, + $this->searchSettings, + $this->channelContext, + new Parameters($this->parametersParser->parseRequestValues($request->attributes->get('_sylius', []), $request)) + ); + $result = $this->search->search($requestConfiguration); + + return $this->render('@MonsieurBizSyliusSearchPlugin/Taxon/result.html.twig', [ + 'requestConfiguration' => $requestConfiguration, + 'result' => $result, + 'currencySymbol' => Currencies::getSymbol($this->currencyContext->getCurrencyCode(), $this->localeContext->getLocaleCode()), + ]); } } diff --git a/src/DependencyInjection/AutomapperConfigurationRegistryPass.php b/src/DependencyInjection/AutomapperConfigurationRegistryPass.php new file mode 100644 index 00000000..2de4b20d --- /dev/null +++ b/src/DependencyInjection/AutomapperConfigurationRegistryPass.php @@ -0,0 +1,32 @@ + + * + * 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\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +final class AutomapperConfigurationRegistryPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + $automapperConfig = $container->getDefinition(\MonsieurBiz\SyliusSearchPlugin\AutoMapper\Configuration::class); + $automapperClasses = (array) $container->getParameter('monsieurbiz.search.config.automapper_classes'); + foreach ($automapperClasses['sources'] as $identifier => $sourceClass) { + $automapperConfig->addMethodCall('addSourceClass', [$identifier, $sourceClass]); + } + foreach ($automapperClasses['targets'] as $identifier => $sourceClass) { + $automapperConfig->addMethodCall('addTargetClass', [$identifier, $sourceClass]); + } + } +} diff --git a/src/DependencyInjection/AutowireMappingProviderParameterPass.php b/src/DependencyInjection/AutowireMappingProviderParameterPass.php new file mode 100644 index 00000000..173d6187 --- /dev/null +++ b/src/DependencyInjection/AutowireMappingProviderParameterPass.php @@ -0,0 +1,42 @@ + + * + * 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\DependencyInjection; + +use JoliCode\Elastically\Mapping\YamlProvider; +use MonsieurBiz\SyliusSearchPlugin\Mapping\YamlWithLocaleProvider; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException; + +class AutowireMappingProviderParameterPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition(YamlProvider::class) || !$container->hasDefinition(YamlWithLocaleProvider::class)) { + return; + } + + $yamlMappingProvider = $container->getDefinition(YamlProvider::class); + $decoratedYamlMappingProvider = $container->getDefinition(YamlWithLocaleProvider::class); + + try { + $decoratedYamlMappingProvider->setArgument( + '$configurationDirectory', + $yamlMappingProvider->getArgument('$configurationDirectory') + ); + } catch (OutOfBoundsException $exception) { + // yaml provider service has no configuration directory argument + } + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 72e2c0ce..20dccdd7 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -13,95 +13,78 @@ namespace MonsieurBiz\SyliusSearchPlugin\DependencyInjection; +use MonsieurBiz\SyliusSearchPlugin\Mapping\YamlWithLocaleProvider; +use MonsieurBiz\SyliusSearchPlugin\Model\Datasource\RepositoryDatasource; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\Documentable; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; final class Configuration implements ConfigurationInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('monsieur_biz_sylius_search'); - if (method_exists($treeBuilder, 'getRootNode')) { - $rootNode = $treeBuilder->getRootNode(); - } else { - // BC layer for symfony/config 4.1 and older - $rootNode = $treeBuilder->root('monsieur_biz_sylius_search'); - } - + $rootNode = $treeBuilder->getRootNode(); $rootNode ->children() - // Files - ->arrayNode('files') - ->children() - ->scalarNode('search')->isRequired()->end() - ->scalarNode('taxon')->isRequired()->end() - ->scalarNode('instant')->isRequired()->end() - ->end() - ->end() - - // Documentable classes - ->variableNode('documentable_classes')->end() - - // Grid - ->arrayNode('grid') - ->children() - - // Limits - ->arrayNode('limits') + ->arrayNode('documents') + ->useAttributeAsKey('code', false) + ->defaultValue([]) + ->arrayPrototype() ->children() - ->arrayNode('taxon') - ->performNoDeepMerging() - ->integerPrototype()->end() - ->isRequired() - ->defaultValue([10, 25, 50]) - ->end() - ->arrayNode('search') - ->performNoDeepMerging() - ->integerPrototype()->end() - ->isRequired() - ->defaultValue([10, 25, 50]) + ->scalarNode('document_class')->defaultValue(Documentable::class)->end() + ->scalarNode('instant_search_enabled')->defaultValue(false)->end() + ->scalarNode('source')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('target')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('mapping_provider')->defaultValue(YamlWithLocaleProvider::class)->end() + ->scalarNode('datasource')->defaultValue(RepositoryDatasource::class)->end() + ->arrayNode('templates') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('item')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('instant')->isRequired()->cannotBeEmpty()->end() + ->end() ->end() - ->end() - ->end() - - // Default limit - ->arrayNode('default_limit') - ->children() - ->integerNode('taxon')->isRequired()->defaultValue(10)->end() - ->integerNode('search')->isRequired()->defaultValue(10)->end() - ->integerNode('instant')->isRequired()->defaultValue(10)->end() - ->end() - ->end() - // Sorting - ->arrayNode('sorting') - ->children() - ->arrayNode('taxon') - ->performNoDeepMerging() - ->scalarPrototype()->end() - ->isRequired() - ->defaultValue(['name']) - ->end() - ->arrayNode('search') - ->performNoDeepMerging() - ->scalarPrototype()->end() - ->isRequired() - ->defaultValue(['name']) + // Limits + ->arrayNode('limits') + ->children() + ->arrayNode('search') + ->performNoDeepMerging() + ->integerPrototype()->end() + ->defaultValue([9, 18, 27]) + ->end() + ->arrayNode('taxon') + ->performNoDeepMerging() + ->integerPrototype()->end() + ->defaultValue([9, 18, 27]) + ->end() + ->arrayNode('instant_search') + ->performNoDeepMerging() + ->integerPrototype()->end() + ->defaultValue([10]) + ->end() + ->end() ->end() ->end() ->end() - - // Filters - ->arrayNode('filters') - ->children() - ->booleanNode('apply_manually')->isRequired()->defaultValue(false)->end() - ->booleanNode('use_main_taxon')->isRequired()->defaultValue(false)->end() + ->end() + ->arrayNode('automapper_classes') + ->children() + ->arrayNode('sources') + ->useAttributeAsKey('code', false) + ->defaultValue([]) + ->prototype('scalar')->end() + ->end() + ->arrayNode('targets') + ->useAttributeAsKey('code', false) + ->defaultValue([]) + ->prototype('scalar')->end() ->end() ->end() - ->end() ->end() ; diff --git a/src/DependencyInjection/DocumentableRegistryPass.php b/src/DependencyInjection/DocumentableRegistryPass.php new file mode 100644 index 00000000..c97b5933 --- /dev/null +++ b/src/DependencyInjection/DocumentableRegistryPass.php @@ -0,0 +1,84 @@ + + * + * 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\DependencyInjection; + +use InvalidArgumentException; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class DocumentableRegistryPass implements CompilerPassInterface +{ + 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'])]); + + // 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'], + 'limits__' . $indexCode => $documentableConfiguration['limits'], + ]; + } + + $container->setParameter('monsieurbiz.settings.config.plugins', $searchSettings); + } + + /** + * @throws InvalidArgumentException + */ + private function validateDocumentableResource(string $class): void + { + $interfaces = (array) (class_implements($class) ?? []); + + if (!\in_array(DocumentableInterface::class, $interfaces, true)) { + throw new InvalidArgumentException(sprintf('Class "%s" must implement "%s" to be registered as a Documentable.', $class, DocumentableInterface::class)); + } + } +} diff --git a/src/DependencyInjection/MonsieurBizSyliusSearchExtension.php b/src/DependencyInjection/MonsieurBizSyliusSearchExtension.php index 2b3602ff..cc7eaa34 100644 --- a/src/DependencyInjection/MonsieurBizSyliusSearchExtension.php +++ b/src/DependencyInjection/MonsieurBizSyliusSearchExtension.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -13,19 +13,53 @@ namespace MonsieurBiz\SyliusSearchPlugin\DependencyInjection; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface; +use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; final class MonsieurBizSyliusSearchExtension extends Extension { - public const EXTENSION_CONFIG_NAME = 'monsieurbiz_sylius_search'; + public const EXTENSION_CONFIG_NAME = 'monsieurbiz.search.config'; public function load(array $configs, ContainerBuilder $container): void { - $configuration = new Configuration(); - $config = $this->processConfiguration($configuration, $configs); + $config = $this->processConfiguration($this->getConfiguration([], $container), $configs); foreach ($config as $name => $value) { $container->setParameter(self::EXTENSION_CONFIG_NAME . '.' . $name, $value); + if ('documents' === $name) { + $this->addDocumentsConfiguration(self::EXTENSION_CONFIG_NAME . '.' . $name, $value, $container); + } + } + + $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('services.yaml'); + + $container->registerForAutoconfiguration(RequestInterface::class) + ->addTag('monsieurbiz.search.request') + ; + } + + /** + * @inheritdoc + */ + public function getAlias() + { + return str_replace(['monsieur_biz'], ['monsieurbiz'], parent::getAlias()); + } + + private function addDocumentsConfiguration(string $name, array $values, ContainerBuilder $container): void + { + foreach ($values as $documentIndexName => $documentValues) { + $this->addDocumentConfiguration($name . '.' . $documentIndexName, $documentValues, $container); + } + } + + private function addDocumentConfiguration(string $name, array $values, ContainerBuilder $container): void + { + foreach ($values as $configName => $configValue) { + $container->setParameter($name . '.' . $configName, $configValue); } } } diff --git a/src/DependencyInjection/RegisterSearchRequestPass.php b/src/DependencyInjection/RegisterSearchRequestPass.php new file mode 100644 index 00000000..fa1f1366 --- /dev/null +++ b/src/DependencyInjection/RegisterSearchRequestPass.php @@ -0,0 +1,35 @@ + + * + * 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\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +class RegisterSearchRequestPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition('monsieurbiz.search.registry.search_request')) { + return; + } + + $registry = $container->getDefinition('monsieurbiz.search.registry.search_request'); + foreach ($container->findTaggedServiceIds('monsieurbiz.search.request') as $serviceId => $tags) { + foreach ($tags as $tag) { + $registry->addMethodCall('register', [$tag['id'] ?? $serviceId, new Reference($serviceId)]); + } + } + } +} diff --git a/src/Entity/Product/FilterableInterface.php b/src/Entity/Product/SearchableInterface.php similarity index 63% rename from src/Entity/Product/FilterableInterface.php rename to src/Entity/Product/SearchableInterface.php index a57cb315..56071ad5 100644 --- a/src/Entity/Product/FilterableInterface.php +++ b/src/Entity/Product/SearchableInterface.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -13,9 +13,17 @@ namespace MonsieurBiz\SyliusSearchPlugin\Entity\Product; -interface FilterableInterface +interface SearchableInterface { + public function isSearchable(): bool; + + public function setSearchable(bool $searchable): void; + public function isFilterable(): bool; public function setFilterable(bool $filterable): void; + + public function getSearchWeight(): int; + + public function setSearchWeight(int $searchWeight): void; } diff --git a/src/Event/MappingProviderEvent.php b/src/Event/MappingProviderEvent.php new file mode 100644 index 00000000..35b077d1 --- /dev/null +++ b/src/Event/MappingProviderEvent.php @@ -0,0 +1,51 @@ + + * + * 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\Event; + +use ArrayObject; +use Symfony\Contracts\EventDispatcher\Event; + +class MappingProviderEvent extends Event +{ + public const EVENT_NAME = 'monsieurbiz.search.mapping.provider'; + + private string $indexCode; + + /** + * @var ArrayObject|null + */ + private ?ArrayObject $mapping; + + /** + * @param ArrayObject|null $mapping + */ + public function __construct(string $indexCode, ?ArrayObject $mapping) + { + $this->indexCode = $indexCode; + $this->mapping = $mapping; + } + + public function getIndexCode(): string + { + return $this->indexCode; + } + + /** + * @return ArrayObject|null + */ + public function getMapping(): ?ArrayObject + { + return $this->mapping; + } +} diff --git a/src/EventListener/DocumentListener.php b/src/EventListener/DocumentListener.php deleted file mode 100644 index 950ba4d0..00000000 --- a/src/EventListener/DocumentListener.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\EventListener; - -use MonsieurBiz\SyliusSearchPlugin\Model\Document\Index\Indexer; -use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; -use Symfony\Component\EventDispatcher\GenericEvent; -use Webmozart\Assert\Assert; - -final class DocumentListener -{ - /** @var Indexer */ - private $documentIndexer; - - /** - * DocumentListener constructor. - * - * @param Indexer $documentIndexer - */ - public function __construct(Indexer $documentIndexer) - { - $this->documentIndexer = $documentIndexer; - } - - /** - * Save document to search index, update if exists. - * - * @param GenericEvent $event - * - * @throws \Exception - */ - public function saveDocument(GenericEvent $event): void - { - $subject = $event->getSubject(); - Assert::isInstanceOf($subject, DocumentableInterface::class); - - $this->documentIndexer->indexOne($subject); - } - - /** - * Delete document in search index. - * - * @param GenericEvent $event - * - * @throws \Exception - */ - public function deleteDocument(GenericEvent $event): void - { - $subject = $event->getSubject(); - Assert::isInstanceOf($subject, DocumentableInterface::class); - - $this->documentIndexer->removeOne($subject); - } -} diff --git a/src/EventSubscriber/AppendProductAttributeMappingSubscriber.php b/src/EventSubscriber/AppendProductAttributeMappingSubscriber.php new file mode 100644 index 00000000..12b7f8ef --- /dev/null +++ b/src/EventSubscriber/AppendProductAttributeMappingSubscriber.php @@ -0,0 +1,137 @@ + + * + * 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\EventSubscriber; + +use MonsieurBiz\SyliusSearchPlugin\Entity\Product\SearchableInterface; +use MonsieurBiz\SyliusSearchPlugin\Event\MappingProviderEvent; +use MonsieurBiz\SyliusSearchPlugin\Repository\ProductAttributeRepositoryInterface; +use MonsieurBiz\SyliusSearchPlugin\Repository\ProductOptionRepositoryInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class AppendProductAttributeMappingSubscriber implements EventSubscriberInterface +{ + private ProductAttributeRepositoryInterface $productAttributeRepository; + + private ProductOptionRepositoryInterface $productOptionRepository; + + private string $fieldAnalyzer; + + public function __construct( + ProductAttributeRepositoryInterface $productAttributeRepository, + ProductOptionRepositoryInterface $productOptionRepository, + string $fieldAnalyzer + ) { + $this->productAttributeRepository = $productAttributeRepository; + $this->productOptionRepository = $productOptionRepository; + $this->fieldAnalyzer = $fieldAnalyzer; + } + + public static function getSubscribedEvents() + { + return [ + MappingProviderEvent::EVENT_NAME => 'omMappingProvider', + ]; + } + + public function omMappingProvider(MappingProviderEvent $event): void + { + if ('monsieurbiz_product' !== $event->getIndexCode()) { + return; + } + $mapping = $event->getMapping(); + if (null === $mapping || !$mapping->offsetExists('mappings')) { + return; + } + /** @var array $mappings */ + $mappings = $mapping->offsetGet('mappings'); + $attributesMapping = []; + foreach ($this->productAttributeRepository->findIsSearchableOrFilterable() as $productAttribute) { + $attributesMapping[$productAttribute->getCode()] = $this->getProductAttributeProperties($productAttribute); + } + if (0 < \count($attributesMapping)) { + $mappings['properties']['attributes'] = [ + 'type' => 'nested', + 'properties' => $attributesMapping, + ]; + } + + $optionsMapping = []; + foreach ($this->productOptionRepository->findIsSearchableOrFilterable() as $productOption) { + $optionsMapping[$productOption->getCode()] = $this->getProductOptionProperties($productOption); + } + if (0 < \count($optionsMapping)) { + $mappings['properties']['options'] = [ + 'type' => 'nested', + 'properties' => $optionsMapping, + ]; + } + + $mapping->offsetSet('mappings', $mappings); + } + + private function getProductAttributeProperties(SearchableInterface $productAttribute): array + { + $properties = [ + 'type' => 'nested', + 'properties' => [ + 'code' => ['type' => 'keyword'], + 'name' => ['type' => 'keyword'], + 'value' => ['type' => 'text'], + ], + ]; + + if ($productAttribute->isFilterable()) { + $properties['properties']['value']['fields'] = [ + 'keyword' => ['type' => 'keyword'], + ]; + } + + if ($productAttribute->isSearchable()) { + $properties['properties']['value']['analyzer'] = $this->fieldAnalyzer; + } + + return $properties; + } + + private function getProductOptionProperties(SearchableInterface $productOption): array + { + $properties = [ + 'type' => 'nested', + 'properties' => [ + 'code' => ['type' => 'keyword'], + 'name' => ['type' => 'keyword'], + 'values' => [ + 'type' => 'nested', + 'properties' => [ + 'value' => ['type' => 'text'], + 'enabled' => ['type' => 'boolean'], + 'is_in_stock' => ['type' => 'boolean'], + ], + ], + ], + ]; + + if ($productOption->isFilterable()) { + $properties['properties']['values']['properties']['value']['fields'] = [ + 'keyword' => ['type' => 'keyword'], + ]; + } + + if ($productOption->isSearchable()) { + $properties['properties']['values']['properties']['value']['analyzer'] = $this->fieldAnalyzer; + } + + return $properties; + } +} diff --git a/src/EventSubscriber/ReindexProductEventSubscriber.php b/src/EventSubscriber/ReindexProductEventSubscriber.php new file mode 100644 index 00000000..8f2f9bd3 --- /dev/null +++ b/src/EventSubscriber/ReindexProductEventSubscriber.php @@ -0,0 +1,166 @@ + + * + * 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\EventSubscriber; + +use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface; +use Doctrine\ORM\Event\OnFlushEventArgs; +use Doctrine\ORM\Events; +use MonsieurBiz\SyliusSearchPlugin\Message\ProductReindexFromIds; +use MonsieurBiz\SyliusSearchPlugin\Message\ProductReindexFromTaxon; +use MonsieurBiz\SyliusSearchPlugin\Message\ProductToDeleteFromIds; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Sylius\Component\Core\Model\ChannelPricingInterface; +use Sylius\Component\Core\Model\ProductImageInterface; +use Sylius\Component\Core\Model\ProductInterface; +use Sylius\Component\Core\Model\ProductTaxonInterface; +use Sylius\Component\Core\Model\ProductTranslationInterface; +use Sylius\Component\Product\Model\ProductAttributeValueInterface; +use Sylius\Component\Product\Model\ProductInterface as ModelProductInterface; +use Sylius\Component\Product\Model\ProductVariantInterface; +use Sylius\Component\Product\Model\ProductVariantTranslationInterface; +use Symfony\Component\Messenger\MessageBusInterface; + +class ReindexProductEventSubscriber implements EventSubscriberInterface, LoggerAwareInterface +{ + use LoggerAwareTrait; + + /** + * @var ModelProductInterface[] + */ + private array $productsToReindex = []; + + private array $productsToBeDelete = []; + + private MessageBusInterface $messageBus; + + public function __construct(MessageBusInterface $messageBus) + { + $this->messageBus = $messageBus; + } + + public function getSubscribedEvents() + { + return [ + Events::onFlush => 'onFlush', + Events::postFlush => 'postFlush', + ]; + } + + public function onFlush(OnFlushEventArgs $eventArgs): void + { + $eventArgs->getEntityManager()->getEventManager()->removeEventListener(Events::onFlush, $this); + $unitOfWork = $eventArgs->getEntityManager()->getUnitOfWork(); + + $collections = array_merge($unitOfWork->getScheduledCollectionUpdates(), $unitOfWork->getScheduledCollectionDeletions()); + foreach ($collections as $collection) { + if (method_exists($collection, 'getOwner') && $collection->getOwner() instanceof ProductInterface) { + $this->productsToReindex[] = $collection->getOwner(); + } + } + + $entities = array_merge($unitOfWork->getScheduledEntityInsertions(), $unitOfWork->getScheduledEntityUpdates()); + $this->onFlushEntities($entities); + $this->onFlushEntities($unitOfWork->getScheduledEntityDeletions(), 'deletions'); + + if (0 !== \count($this->productsToBeDelete)) { + $productToDeleteMessage = new ProductToDeleteFromIds(); + array_map(function (ProductInterface $product) use ($productToDeleteMessage): void { + foreach ($this->productsToReindex as $key => $productsToReindex) { + if ($productsToReindex->getId() === $product->getId()) { + unset($this->productsToReindex[$key]); + } + } + $productToDeleteMessage->addProductId($product->getId()); + }, $this->productsToBeDelete); + $this->messageBus->dispatch($productToDeleteMessage); + } + + // in other event subscriber ... + // todo reindex all data when: change/create/remove attribute/option, add/remove channel, add/remove locale + } + + public function postFlush(): void + { + $productReindexFormIdsMessage = new ProductReindexFromIds(); + + foreach ($this->productsToReindex as $productsToReindex) { + if (null === $productsToReindex->getId()) { + continue; + } + $productReindexFormIdsMessage->addProductId($productsToReindex->getId()); + } + $this->productsToReindex = []; + + if (0 !== \count($productReindexFormIdsMessage->getProductIds())) { + $this->messageBus->dispatch($productReindexFormIdsMessage); + } + } + + private function onFlushEntities(array $entities, string $type = 'insertionsOrUpdate'): void + { + foreach ($entities as $entity) { + if ($entity instanceof ProductInterface && 'deletions' === $type) { + $this->productsToBeDelete[] = $entity; + + continue; + } + if ($entity instanceof ProductTaxonInterface && null !== $entity->getTaxon()) { + $this->messageBus->dispatch(new ProductReindexFromTaxon($entity->getTaxon()->getId())); + + continue; + } + $product = $this->getProduct($entity); + if (null !== $product) { + $this->productsToReindex[] = $product; + } + } + } + + private function getProduct(object $entity): ?ModelProductInterface + { + switch (true) { + case $entity instanceof ProductInterface: + return $entity; + case $entity instanceof ProductVariantInterface: + return $entity->getProduct(); + case $entity instanceof ProductTaxonInterface: + return $entity->getProduct(); + case $entity instanceof ProductTranslationInterface && $entity->getTranslatable() instanceof ProductInterface: + /** @var ProductInterface $product */ + $product = $entity->getTranslatable(); + + return $product; + case $entity instanceof ProductAttributeValueInterface: + return $entity->getProduct(); + case $entity instanceof ProductImageInterface && $entity->getOwner() instanceof ProductInterface: + /** @var ProductInterface $product */ + $product = $entity->getOwner(); + + return $product; + case $entity instanceof ChannelPricingInterface && $entity->getProductVariant() instanceof ProductVariantInterface: + /** @var ProductVariantInterface $productVariant */ + $productVariant = $entity->getProductVariant(); + + return $productVariant->getProduct(); + case $entity instanceof ProductVariantTranslationInterface && $entity->getTranslatable() instanceof ProductVariantInterface: + /** @var ProductVariantInterface $productVariant */ + $productVariant = $entity->getTranslatable(); + + return $productVariant->getProduct(); + } + + return null; + } +} diff --git a/src/Exception/MissingPriceException.php b/src/Exception/MissingPriceException.php deleted file mode 100644 index 9912d06a..00000000 --- a/src/Exception/MissingPriceException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Exception; - -use Exception; - -class MissingPriceException extends Exception -{ -} diff --git a/src/Exception/NotSupportedTypeException.php b/src/Exception/NotSupportedTypeException.php deleted file mode 100644 index f1f174c2..00000000 --- a/src/Exception/NotSupportedTypeException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Exception; - -use Exception; - -class NotSupportedTypeException extends Exception -{ -} diff --git a/src/Exception/TaxonNotFoundException.php b/src/Exception/ObjectNotInstanceOfClassException.php similarity index 55% rename from src/Exception/TaxonNotFoundException.php rename to src/Exception/ObjectNotInstanceOfClassException.php index 6c651d37..a288030a 100644 --- a/src/Exception/TaxonNotFoundException.php +++ b/src/Exception/ObjectNotInstanceOfClassException.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -13,12 +13,12 @@ namespace MonsieurBiz\SyliusSearchPlugin\Exception; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use InvalidArgumentException; -final class TaxonNotFoundException extends NotFoundHttpException +class ObjectNotInstanceOfClassException extends InvalidArgumentException { - public function __construct() + public static function fromClassName(string $className): self { - parent::__construct('Taxon cannot be found.'); + return new self(sprintf('Object is not instance of class "%s"', $className)); } } diff --git a/src/Exception/ReadOnlyIndexException.php b/src/Exception/ReadOnlyIndexException.php deleted file mode 100644 index e24c964d..00000000 --- a/src/Exception/ReadOnlyIndexException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Exception; - -use Exception; - -class ReadOnlyIndexException extends Exception -{ -} diff --git a/src/Exception/UnknownGridConfigType.php b/src/Exception/UnknownGridConfigType.php deleted file mode 100644 index f6aa90a4..00000000 --- a/src/Exception/UnknownGridConfigType.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Exception; - -use MonsieurBiz\SyliusSearchPlugin\Model\Config\GridConfig; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - -final class UnknownGridConfigType extends NotFoundHttpException -{ - public function __construct() - { - parent::__construct(sprintf( - 'Unknown GridConfig type, available are "%s", "%s" and "%s"', - GridConfig::SEARCH_TYPE, GridConfig::TAXON_TYPE, GridConfig::INSTANT_TYPE - )); - } -} diff --git a/src/Exception/MissingAttributeException.php b/src/Exception/UnknownRequestTypeException.php similarity index 83% rename from src/Exception/MissingAttributeException.php rename to src/Exception/UnknownRequestTypeException.php index 93eb4d7d..5b8c3741 100644 --- a/src/Exception/MissingAttributeException.php +++ b/src/Exception/UnknownRequestTypeException.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -15,6 +15,6 @@ use Exception; -class MissingAttributeException extends Exception +class UnknownRequestTypeException extends Exception { } diff --git a/src/Fixture/Factory/FilterableFixtureFactory.php b/src/Fixture/Factory/FilterableFixtureFactory.php deleted file mode 100644 index 045c70d8..00000000 --- a/src/Fixture/Factory/FilterableFixtureFactory.php +++ /dev/null @@ -1,101 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Fixture\Factory; - -use MonsieurBiz\SyliusSearchPlugin\Entity\Product\FilterableInterface; -use Sylius\Bundle\CoreBundle\Fixture\Factory\AbstractExampleFactory; -use Sylius\Bundle\CoreBundle\Fixture\OptionsResolver\LazyOption; -use Sylius\Component\Product\Model\ProductAttributeInterface; -use Sylius\Component\Product\Model\ProductOptionInterface; -use Sylius\Component\Resource\Repository\RepositoryInterface; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class FilterableFixtureFactory extends AbstractExampleFactory implements FilterableFixtureFactoryInterface -{ - /** - * @var RepositoryInterface - */ - protected $productAttributeRepository; - - /** - * @var RepositoryInterface - */ - protected $productOptionRepository; - - /** - * @var OptionsResolver - */ - private $optionsResolver; - - /** - * FilterableFixtureFactory constructor. - * - * @param RepositoryInterface $productAttributeRepository - * @param RepositoryInterface $productOptionRepository - */ - public function __construct( - RepositoryInterface $productAttributeRepository, - RepositoryInterface $productOptionRepository - ) { - $this->productAttributeRepository = $productAttributeRepository; - $this->productOptionRepository = $productOptionRepository; - $this->optionsResolver = new OptionsResolver(); - $this->configureOptions($this->optionsResolver); - } - - /** - * {@inheritdoc} - */ - protected function configureOptions(OptionsResolver $resolver): void - { - $resolver - ->setDefault('attribute', null) - ->setAllowedTypes('attribute', ['null', 'string', ProductAttributeInterface::class]) - ->setNormalizer('attribute', LazyOption::findOneBy($this->productAttributeRepository, 'code')) - ->setDefault('option', null) - ->setAllowedTypes('option', ['null', 'string', ProductOptionInterface::class]) - ->setNormalizer('option', LazyOption::findOneBy($this->productOptionRepository, 'code')) - ->setDefault('filterable', true) - ; - } - - /** - * @param array $options - * - * @throws \Exception - * - * @return object - */ - public function create(array $options = []) - { - $options = $this->optionsResolver->resolve($options); - - if (isset($options['attribute']) && !empty($options['attribute'])) { - $object = $options['attribute']; - } elseif (isset($options['option']) && !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 FilterableInterface) { - throw new \Exception(sprintf('Your class "%s" is not an instance of %s', \get_class($object), FilterableInterface::class)); - } - - /** @var FilterableInterface $object */ - $object->setFilterable(((bool) $options['filterable']) ?? false); - - return $object; - } -} diff --git a/src/Fixture/Factory/FilterableFixtureFactoryInterface.php b/src/Fixture/Factory/FilterableFixtureFactoryInterface.php deleted file mode 100644 index 21c64c8a..00000000 --- a/src/Fixture/Factory/FilterableFixtureFactoryInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Fixture\Factory; - -use Sylius\Bundle\CoreBundle\Fixture\Factory\ExampleFactoryInterface; - -interface FilterableFixtureFactoryInterface extends ExampleFactoryInterface -{ -} diff --git a/src/Fixture/FilterableFixture.php b/src/Fixture/FilterableFixture.php deleted file mode 100644 index e3315cfe..00000000 --- a/src/Fixture/FilterableFixture.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Fixture; - -use Doctrine\ORM\EntityManagerInterface; -use MonsieurBiz\SyliusSearchPlugin\Fixture\Factory\FilterableFixtureFactoryInterface; -use Sylius\Bundle\CoreBundle\Fixture\AbstractResourceFixture; -use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; - -class FilterableFixture extends AbstractResourceFixture implements FilterableFixtureInterface -{ - /** - * FilterableFixture constructor. - * - * @param EntityManagerInterface $productManager - * @param FilterableFixtureFactoryInterface $exampleFactory - */ - public function __construct( - EntityManagerInterface $productManager, - FilterableFixtureFactoryInterface $exampleFactory - ) { - parent::__construct($productManager, $exampleFactory); - } - - /** - * {@inheritdoc} - */ - public function getName(): string - { - return 'monsieurbiz_sylius_search_filterable'; - } - - /** - * {@inheritdoc} - */ - protected function configureResourceNode(ArrayNodeDefinition $resourceNode): void - { - $resourceNode - ->children() - ->scalarNode('attribute')->end() - ->scalarNode('option')->end() - ->booleanNode('filterable')->defaultValue(true)->end() - ; - } -} diff --git a/src/Fixture/FilterableFixtureInterface.php b/src/Fixture/FilterableFixtureInterface.php deleted file mode 100644 index 7218e7a2..00000000 --- a/src/Fixture/FilterableFixtureInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Fixture; - -use Sylius\Bundle\FixturesBundle\Fixture\FixtureInterface; - -interface FilterableFixtureInterface extends FixtureInterface -{ -} diff --git a/src/Form/Extension/ProductAttributeTypeExtension.php b/src/Form/Extension/ProductAttributeTypeExtension.php index b83b30e2..67bccb4a 100644 --- a/src/Form/Extension/ProductAttributeTypeExtension.php +++ b/src/Form/Extension/ProductAttributeTypeExtension.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -16,17 +16,29 @@ use Sylius\Bundle\ProductBundle\Form\Type\ProductAttributeType; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; final class ProductAttributeTypeExtension extends AbstractTypeExtension { public function buildForm(FormBuilderInterface $builder, array $options): void { + $searchWeightValues = range(1, 10); + $builder + ->add('searchable', CheckboxType::class, [ + 'label' => 'monsieurbiz_searchplugin.admin.product_attribute.form.searchable', + 'required' => true, + ]) ->add('filterable', CheckboxType::class, [ 'label' => 'monsieurbiz_searchplugin.admin.product_attribute.form.filterable', 'required' => true, ]) + ->add('search_weight', ChoiceType::class, [ + 'label' => 'monsieurbiz_searchplugin.admin.product_attribute.form.search_weight', + 'required' => true, + 'choices' => array_combine($searchWeightValues, $searchWeightValues), + ]) ; } diff --git a/src/Form/Extension/ProductOptionTypeExtension.php b/src/Form/Extension/ProductOptionTypeExtension.php index 2d7ec32b..71c2abda 100644 --- a/src/Form/Extension/ProductOptionTypeExtension.php +++ b/src/Form/Extension/ProductOptionTypeExtension.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -16,16 +16,28 @@ use Sylius\Bundle\ProductBundle\Form\Type\ProductOptionType; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; final class ProductOptionTypeExtension extends AbstractTypeExtension { public function buildForm(FormBuilderInterface $builder, array $options): void { + $searchWeightValues = range(1, 10); + $builder + ->add('searchable', CheckboxType::class, [ + 'label' => 'monsieurbiz_searchplugin.admin.product_attribute.form.searchable', + 'required' => true, + ]) ->add('filterable', CheckboxType::class, [ - 'label' => 'monsieurbiz_searchplugin.admin.product_option.form.filterable', + 'label' => 'monsieurbiz_searchplugin.admin.product_attribute.form.filterable', + 'required' => true, + ]) + ->add('search_weight', ChoiceType::class, [ + 'label' => 'monsieurbiz_searchplugin.admin.product_attribute.form.search_weight', 'required' => true, + 'choices' => array_combine($searchWeightValues, $searchWeightValues), ]) ; } diff --git a/src/Form/Type/SearchType.php b/src/Form/Type/SearchType.php index c04ba720..883e117b 100644 --- a/src/Form/Type/SearchType.php +++ b/src/Form/Type/SearchType.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -14,24 +14,23 @@ namespace MonsieurBiz\SyliusSearchPlugin\Form\Type; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\SearchType as SymfonySearchType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; -use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Required; class SearchType extends AbstractType { - /** - * @param FormBuilderInterface $builder - * @param array $options - */ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('query', TextType::class, [ + ->add('query', SymfonySearchType::class, [ 'required' => true, 'label' => 'monsieurbiz_searchplugin.form.query', + 'attr' => [ + 'placeholder' => 'monsieurbiz_searchplugin.form.query_placeholder', + ], 'constraints' => [ new NotBlank(), new Required(), @@ -45,7 +44,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void } /** - * {@inheritdoc} + * @inheritdoc */ public function getBlockPrefix() { diff --git a/src/Form/Type/Settings/LimitsSearchType.php b/src/Form/Type/Settings/LimitsSearchType.php new file mode 100644 index 00000000..65d5c4ba --- /dev/null +++ b/src/Form/Type/Settings/LimitsSearchType.php @@ -0,0 +1,61 @@ + + * + * 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\Form\Type\Settings; + +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSettingsPlugin\Form\AbstractSettingsType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class LimitsSearchType extends AbstractSettingsType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + /** @var DocumentableInterface $documentable */ + $documentable = $options['documentable']; + + $this->addWithDefaultCheckbox( + $builder, + 'search', + CollectionType::class, + [ + 'entry_type' => NumberType::class, + 'label' => 'monsieurbiz_searchplugin.admin.setting_form.limit_search_' . $documentable->getIndexCode(), + 'required' => true, + 'allow_add' => true, + 'allow_delete' => true, + ] + ); + $this->addWithDefaultCheckbox( + $builder, + 'instant_search', + CollectionType::class, + [ + 'entry_type' => NumberType::class, + 'label' => 'monsieurbiz_searchplugin.admin.setting_form.limit_instant_search_' . $documentable->getIndexCode(), + 'required' => true, + 'allow_add' => true, + 'allow_delete' => true, + ] + ); + } + + public function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + $resolver->setRequired('documentable'); + } +} diff --git a/src/Form/Type/Settings/SettingsSearchType.php b/src/Form/Type/Settings/SettingsSearchType.php new file mode 100644 index 00000000..d58ff765 --- /dev/null +++ b/src/Form/Type/Settings/SettingsSearchType.php @@ -0,0 +1,54 @@ + + * + * 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\Form\Type\Settings; + +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSettingsPlugin\Form\AbstractSettingsType; +use Sylius\Component\Registry\ServiceRegistryInterface; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\FormBuilderInterface; + +class SettingsSearchType extends AbstractSettingsType +{ + private ServiceRegistryInterface $documentableRegistry; + + public function __construct(ServiceRegistryInterface $documentableRegistry) + { + $this->documentableRegistry = $documentableRegistry; + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + /** @var DocumentableInterface $documentable */ + foreach ($this->documentableRegistry->all() as $documentable) { + $this->addWithDefaultCheckbox( + $builder, + 'instant_search_enabled__' . $documentable->getIndexCode(), + CheckboxType::class, + [ + 'required' => false, + 'label' => 'monsieurbiz_searchplugin.admin.setting_form.instant_search_enabled_' . $documentable->getIndexCode(), + ] + ); + $subOptions = $options; + $subOptions['data'] = $subOptions['data']['limits__' . $documentable->getIndexCode()] ?? []; + $subOptions['documentable'] = $documentable; + $builder->add( + 'limits__' . $documentable->getIndexCode(), + LimitsSearchType::class, + $subOptions + ); + } + } +} diff --git a/src/Helper/AggregationHelper.php b/src/Helper/AggregationHelper.php deleted file mode 100644 index bf13d1ab..00000000 --- a/src/Helper/AggregationHelper.php +++ /dev/null @@ -1,135 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Helper; - -class AggregationHelper -{ - public const MAX_AGGREGATED_ATTRIBUTES_INFO = 100; - public const MAX_AGGREGATED_TAXON_INFO = 500; - - /** - * Build sort array to add in query. - * - * @param string $field - * - * @return array - */ - public static function buildAggregation(string $field): array - { - return [ - 'filter' => [ - 'bool' => [ - 'must' => [ - [ - 'term' => ['attributes.code' => $field], - ], - ], - ], - ], - 'aggs' => [ - 'values' => [ - 'terms' => ['field' => 'attributes.value.keyword', 'size' => self::MAX_AGGREGATED_ATTRIBUTES_INFO], // Retrieve all attributes info - ], - ], - ]; - } - - /** - * Build sort array to add in query. - * - * @param array $filters - * - * @return array - */ - public static function buildAggregations(array $filters): array - { - $attributeAggregations = []; - foreach ($filters as $field) { - $attributeAggregations[$field] = self::buildAggregation($field); - } - - $aggregations = [ - 'attributes' => [ - 'nested' => ['path' => 'attributes'], - 'aggs' => [ - 'codes' => [ - 'terms' => ['field' => 'attributes.code', 'size' => self::MAX_AGGREGATED_ATTRIBUTES_INFO] // Retrieve all attributes info - , - 'aggs' => [ - 'names' => [ - 'terms' => ['field' => 'attributes.name.keyword'], - ], - ], - ], - ], - ], - // Get taxon info to be able to retrieve the attribute name from code, we also need the level - 'taxons' => [ - 'nested' => ['path' => 'taxon'], - 'aggs' => [ - 'codes' => [ - 'terms' => ['field' => 'taxon.code', 'size' => self::MAX_AGGREGATED_TAXON_INFO], // Retrieve all taxon info - 'aggs' => [ - 'levels' => [ - 'terms' => ['field' => 'taxon.level'], - 'aggs' => [ - 'names' => [ - 'terms' => ['field' => 'taxon.name'], - ], - ], - ], - ], - ], - ], - ], - // Get main taxon info to be able to retrieve the attribute name from code, we also need the level - 'mainTaxon' => [ - 'nested' => ['path' => 'mainTaxon'], - 'aggs' => [ - 'codes' => [ - 'terms' => ['field' => 'mainTaxon.code', 'size' => self::MAX_AGGREGATED_TAXON_INFO], // Retrieve all taxon info - 'aggs' => [ - 'levels' => [ - 'terms' => ['field' => 'mainTaxon.level'], - 'aggs' => [ - 'names' => [ - 'terms' => ['field' => 'mainTaxon.name'], - ], - ], - ], - ], - ], - ], - ], - // Get attributes info to be able to retrieve the attribute name from code - 'price' => [ - 'nested' => ['path' => 'price'], - 'aggs' => [ - 'values' => [ - 'stats' => ['field' => 'price.value'], - ], - ], - ], - ]; - - if (!empty($attributeAggregations)) { - $aggregations['filters'] = [ - 'nested' => ['path' => 'attributes'], - 'aggs' => $attributeAggregations, - ]; - } - - return $aggregations; - } -} diff --git a/src/Helper/FilterHelper.php b/src/Helper/FilterHelper.php deleted file mode 100644 index 25f8ef39..00000000 --- a/src/Helper/FilterHelper.php +++ /dev/null @@ -1,218 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Helper; - -class FilterHelper -{ - public const MAIN_TAXON_FILTER = 'main_taxon'; - public const TAXON_FILTER = 'taxon'; - public const PRICE_FILTER = 'price'; - - /** - * Return an array with filters for query. - * - * @param array $appliedFilters - * - * @return array - */ - public static function buildFilters(array $appliedFilters): array - { - if (empty($appliedFilters)) { - return []; - } - - $filters = []; - foreach ($appliedFilters as $field => $values) { - if (self::TAXON_FILTER === $field) { - $filters[] = self::buildTaxonFilter($values); - } elseif (self::MAIN_TAXON_FILTER === $field) { - $filters[] = self::buildMainTaxonFilter($values); - } elseif (self::PRICE_FILTER === $field) { - if (isset($values['min']) && isset($values['max'])) { - $filters[] = self::buildPriceFilter((int) $values['min'], (int) $values['max']); - } - } else { - $filters[] = self::buildFilter($field, $values); - } - } - - return [ - 'bool' => [ - 'filter' => $filters, - ], - ]; - } - - /** - * Build filter array to add in query. - * - * @param string $field - * @param array $values - * - * @return array - */ - public static function buildFilter(string $field, array $values): array - { - $filterValues = []; - foreach ($values as $value) { - $filterValues[] = self::buildFilterValue($value); - } - - return [ - 'nested' => [ - 'path' => 'attributes', - 'query' => [ - 'bool' => [ - 'must' => [ - 'match' => [ - 'attributes.code' => $field, - ], - ], - 'should' => $filterValues, - 'minimum_should_match' => 1, - ], - ], - ], - ]; - } - - /** - * Build filter array for taxon to add in query. - * - * @param array $values - * - * @return array - */ - public static function buildTaxonFilter(array $values): array - { - $filterValues = []; - foreach ($values as $value) { - $filterValues[] = self::buildTaxonFilterValue($value); - } - - return [ - 'nested' => [ - 'path' => 'taxon', - 'query' => [ - 'bool' => [ - 'should' => $filterValues, - 'minimum_should_match' => 1, - ], - ], - ], - ]; - } - - /** - * Build filter array for main taxon to add in query. - * - * @param array $values - * - * @return array - */ - public static function buildMainTaxonFilter(array $values): array - { - $filterValues = []; - foreach ($values as $value) { - $filterValues[] = self::buildMainTaxonFilterValue($value); - } - - return [ - 'nested' => [ - 'path' => 'mainTaxon', - 'query' => [ - 'bool' => [ - 'should' => $filterValues, - 'minimum_should_match' => 1, - ], - ], - ], - ]; - } - - /** - * Build filter array for price to add in query. - * - * @param int $min - * @param int $max - * - * @return array - */ - public static function buildPriceFilter(int $min, int $max): array - { - return [ - 'nested' => [ - 'path' => 'price', - 'query' => [ - 'range' => [ - 'price.value' => [ - [ - 'gte' => $min * 100, - 'lte' => $max * 100, - ], - ], - ], - ], - ], - ]; - } - - /** - * Build filter value array to add in query. - * - * @param string $value - * - * @return array - */ - public static function buildFilterValue(string $value): array - { - return [ - 'term' => [ - 'attributes.value.keyword' => SlugHelper::toLabel($value), - ], - ]; - } - - /** - * Build filter value array to add in query. - * - * @param string $value - * - * @return array - */ - public static function buildTaxonFilterValue(string $value): array - { - return [ - 'term' => [ - 'taxon.name' => SlugHelper::toLabel($value), - ], - ]; - } - - /** - * Build filter value array to add in query. - * - * @param string $value - * - * @return array - */ - public static function buildMainTaxonFilterValue(string $value): array - { - return [ - 'term' => [ - 'mainTaxon.name' => SlugHelper::toLabel($value), - ], - ]; - } -} diff --git a/src/Helper/RenderDocumentUrlHelper.php b/src/Helper/RenderDocumentUrlHelper.php deleted file mode 100644 index f3524c92..00000000 --- a/src/Helper/RenderDocumentUrlHelper.php +++ /dev/null @@ -1,32 +0,0 @@ -getType()) { - case 'product': - return new UrlParamsProvider('sylius_shop_product_show', ['slug' => $document->getSlug(), '_locale' => $document->getLocale()]); - break; - } - - throw new NotSupportedTypeException(sprintf('Object type "%s" not supported to get URL', $document->getType())); - } -} diff --git a/src/Helper/SlugHelper.php b/src/Helper/SlugHelper.php index b3bb6205..c9e8fa64 100644 --- a/src/Helper/SlugHelper.php +++ b/src/Helper/SlugHelper.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -15,12 +15,12 @@ class SlugHelper { - public static function toSlug($label): string + public static function toSlug(string $label): string { return urlencode($label); } - public static function toLabel($slug): string + public static function toLabel(string $slug): string { return urldecode($slug); } diff --git a/src/Helper/SortHelper.php b/src/Helper/SortHelper.php deleted file mode 100644 index 17060905..00000000 --- a/src/Helper/SortHelper.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Helper; - -class SortHelper -{ - /** - * Get query's sort array depending on sorted field. - * - * @param string $field - * @param string $channel - * @param string $order - * @param string $taxon - * - * @return array - */ - public static function getSortParamByField(string $field, string $channel, string $order = 'asc', $taxon = ''): array - { - switch ($field) { - case 'name': - return self::buildSort('attributes.value.keyword', $order, 'attributes', 'attributes.code', $field); - case 'created_at': - return self::buildSort('attributes.value.keyword', $order, 'attributes', 'attributes.code', $field); - case 'price': - return self::buildSort('price.value', $order, 'price', 'price.channel', $channel); - case 'position': - return self::buildSort('taxon.productPosition', $order, 'taxon', 'taxon.code', $taxon); - default: - // Dummy value to have null sorting in ES and keep ES results sorting - return self::buildSort('attributes.value.keyword', $order, 'attributes', 'attributes.code', 'dummy'); - } - } - - /** - * Build sort array to add in query. - * - * @param string $field - * @param string $order - * @param string $nestedPath - * @param string $sortFilterField - * @param string $sortFilterValue - * - * @return array - */ - public static function buildSort( - string $field, - string $order, - string $nestedPath, - string $sortFilterField, - string $sortFilterValue - ): array { - return [ - $field => [ - 'order' => $order, - 'nested' => [ - 'path' => $nestedPath, - 'filter' => [ - 'term' => [$sortFilterField => $sortFilterValue], - ], - ], - ], - ]; - } -} diff --git a/src/Index/Indexer.php b/src/Index/Indexer.php new file mode 100644 index 00000000..04969b2b --- /dev/null +++ b/src/Index/Indexer.php @@ -0,0 +1,170 @@ + + * + * 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\Index; + +use Doctrine\ORM\EntityManagerInterface; +use Elastica\Document; +use Jane\Component\AutoMapper\AutoMapperInterface; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\ClientFactory; +use Sylius\Component\Locale\Model\LocaleInterface; +use Sylius\Component\Registry\ServiceRegistryInterface; +use Sylius\Component\Resource\Model\TranslatableInterface; +use Sylius\Component\Resource\Repository\RepositoryInterface; + +final class Indexer +{ + private ServiceRegistryInterface $documentableRegistry; + + private RepositoryInterface $localeRepository; + + private array $locales = []; + + private EntityManagerInterface $entityManager; + + private AutoMapperInterface $autoMapper; + + private ClientFactory $clientFactory; + + public function __construct( + ServiceRegistryInterface $documentableRegistry, + RepositoryInterface $localeRepository, + EntityManagerInterface $entityManager, + AutoMapperInterface $autoMapper, + ClientFactory $clientFactory + ) { + $this->documentableRegistry = $documentableRegistry; + $this->localeRepository = $localeRepository; + $this->entityManager = $entityManager; + $this->autoMapper = $autoMapper; + $this->clientFactory = $clientFactory; + } + + /** + * Retrieve all available locales. + */ + public function getLocales(): array + { + if (0 === \count($this->locales)) { + $locales = $this->localeRepository->findAll(); + $this->locales = array_filter(array_map( + function (LocaleInterface $locale): string { + return $locale->getCode() ?? ''; + }, + $locales + )); + } + + return $this->locales; + } + + /** + * Index all documentable object. + */ + public function indexAll(): void + { + /** @var DocumentableInterface $documentable */ + foreach ($this->documentableRegistry->all() as $documentable) { + $this->indexDocumentable($documentable); + } + } + + public function indexByDocuments(DocumentableInterface $documentable, array $documents, ?string $locale = null, ?\JoliCode\Elastically\Indexer $indexer = null): void + { + if (null === $indexer) { + $indexer = $this->clientFactory->getIndexer($documentable, $locale); + } + + if (null === $locale && $documentable->isTranslatable()) { + foreach ($this->getLocales() as $localeCode) { + $this->indexByDocuments($documentable, $documents, $localeCode, $indexer); + } + + $indexer->flush(); + + return; + } + $indexName = $this->getIndexName($documentable, $locale); + foreach ($documents as $document) { + if (null !== $locale && $document instanceof TranslatableInterface) { + $document->setCurrentLocale($locale); + } + $dto = $this->autoMapper->map($document, $documentable->getTargetClass()); + // @phpstan-ignore-next-line + $indexer->scheduleIndex($indexName, new Document((string) $document->getId(), $dto)); + } + } + + public function deleteByDocuments(DocumentableInterface $documentable, array $documents, ?string $locale = null, ?\JoliCode\Elastically\Indexer $indexer = null): void + { + if (null === $indexer) { + $indexer = $this->clientFactory->getIndexer($documentable, $locale); + } + + if (null === $locale && $documentable->isTranslatable()) { + foreach ($this->getLocales() as $localeCode) { + $this->deleteByDocuments($documentable, $documents, $localeCode, $indexer); + } + + $indexer->flush(); + + return; + } + + $indexName = $this->getIndexName($documentable, $locale); + foreach ($documents as $document) { + if (null !== $locale && $document instanceof TranslatableInterface) { + $document->setCurrentLocale($locale); + } + $indexer->scheduleDelete($indexName, (string) $document->getId()); + } + } + + private function indexDocumentable(DocumentableInterface $documentable, ?string $locale = null): void + { + if (null === $locale && $documentable->isTranslatable()) { + foreach ($this->getLocales() as $localeCode) { + $this->indexDocumentable($documentable, $localeCode); + } + + return; + } + $indexName = $this->clientFactory->getIndexName($documentable, $locale); + $indexBuilder = $this->clientFactory->getIndexBuilder($documentable, $locale); + $newIndex = $indexBuilder->createIndex($indexName, [ + 'index_code' => $documentable->getIndexCode(), + 'locale' => null !== $locale ? strtolower($locale) : null, + ]); + + $indexer = $this->clientFactory->getIndexer($documentable, $locale); + foreach ($documentable->getDatasource()->getItems($documentable->getSourceClass()) as $item) { + if (null !== $locale && $item instanceof TranslatableInterface) { + $item->setCurrentLocale($locale); + } + $dto = $this->autoMapper->map($item, $documentable->getTargetClass()); + // @phpstan-ignore-next-line + $indexer->scheduleIndex($newIndex, new Document((string) $item->getId(), $dto)); + } + $indexer->flush(); + + $indexBuilder->markAsLive($newIndex, $indexName); + $indexBuilder->speedUpRefresh($newIndex); + $indexBuilder->purgeOldIndices($indexName); + } + + private function getIndexName(DocumentableInterface $documentable, ?string $locale = null): string + { + return $documentable->getIndexCode() . strtolower(null !== $locale ? '_' . $locale : ''); + } +} diff --git a/src/Mapping/YamlWithLocaleProvider.php b/src/Mapping/YamlWithLocaleProvider.php new file mode 100644 index 00000000..7d51bfde --- /dev/null +++ b/src/Mapping/YamlWithLocaleProvider.php @@ -0,0 +1,92 @@ + + * + * 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\Mapping; + +use ArrayObject; +use JoliCode\Elastically\Mapping\MappingProviderInterface; +use JoliCode\Elastically\Mapping\YamlProvider; +use MonsieurBiz\SyliusSearchPlugin\Event\MappingProviderEvent; +use MonsieurBiz\SyliusSearchPlugin\Repository\ProductAttributeRepositoryInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Parser; + +class YamlWithLocaleProvider implements MappingProviderInterface +{ + private YamlProvider $decorated; + + private string $configurationDirectory; + + private Parser $parser; + + private ProductAttributeRepositoryInterface $attributeRepository; + + private EventDispatcherInterface $eventDispatcher; + + public function __construct( + YamlProvider $decorated, + string $configurationDirectory, + EventDispatcherInterface $eventDispatcher, + ProductAttributeRepositoryInterface $attributeRepository, + ?Parser $parser = null + ) { + $this->decorated = $decorated; + $this->configurationDirectory = $configurationDirectory; + $this->parser = $parser ?? new Parser(); + $this->attributeRepository = $attributeRepository; + $this->eventDispatcher = $eventDispatcher; + } + + public function provideMapping(string $indexName, array $context = []): ?array + { + $mapping = $this->decorated->provideMapping($context['index_code'] ?? $indexName, $context) ?? []; + + $locale = $context['locale'] ?? null; + if (null !== $locale) { + $mapping = $this->appendLocaleAnalyzers($mapping, $locale); + } + + $mappingProviderEvent = new MappingProviderEvent($context['index_code'] ?? $indexName, new ArrayObject($mapping)); + $this->eventDispatcher->dispatch( + $mappingProviderEvent, + MappingProviderEvent::EVENT_NAME + ); + + return (array) $mappingProviderEvent->getMapping(); + } + + private function appendLocaleAnalyzers(array $mapping, string $locale): array + { + foreach ($this->getLocaleCode($locale) as $localeCode) { + $analyzerFilePath = $this->configurationDirectory . \DIRECTORY_SEPARATOR . 'analyzers_' . $localeCode . '.yaml'; + + 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 getLocaleCode(string $locale): array + { + return array_unique([ + current(explode('_', $locale)), + $locale, + ]); + } +} diff --git a/src/Message/ProductReindexFromIds.php b/src/Message/ProductReindexFromIds.php new file mode 100644 index 00000000..8627d13d --- /dev/null +++ b/src/Message/ProductReindexFromIds.php @@ -0,0 +1,34 @@ + + * + * 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\Message; + +class ProductReindexFromIds +{ + private array $productIds; + + public function __construct(array $productIds = []) + { + $this->productIds = $productIds; + } + + public function getProductIds(): array + { + return array_unique($this->productIds); + } + + public function addProductId(int $productIds): void + { + $this->productIds[] = $productIds; + } +} diff --git a/src/Message/ProductReindexFromTaxon.php b/src/Message/ProductReindexFromTaxon.php new file mode 100644 index 00000000..7b2af22f --- /dev/null +++ b/src/Message/ProductReindexFromTaxon.php @@ -0,0 +1,31 @@ + + * + * 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\Message; + +class ProductReindexFromTaxon +{ + //todo rename to ProductReindexFromTaxonId + + private int $taxonId; + + public function __construct(int $taxonId) + { + $this->taxonId = $taxonId; + } + + public function getTaxonId(): int + { + return $this->taxonId; + } +} diff --git a/src/Message/ProductToDeleteFromIds.php b/src/Message/ProductToDeleteFromIds.php new file mode 100644 index 00000000..3f58da2f --- /dev/null +++ b/src/Message/ProductToDeleteFromIds.php @@ -0,0 +1,34 @@ + + * + * 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\Message; + +class ProductToDeleteFromIds +{ + private array $productIds; + + public function __construct(array $productIds = []) + { + $this->productIds = $productIds; + } + + public function getProductIds(): array + { + return array_unique($this->productIds); + } + + public function addProductId(int $productIds): void + { + $this->productIds[] = $productIds; + } +} diff --git a/src/MessageHandler/ProductReindexFromIdsHandler.php b/src/MessageHandler/ProductReindexFromIdsHandler.php new file mode 100644 index 00000000..7777afc0 --- /dev/null +++ b/src/MessageHandler/ProductReindexFromIdsHandler.php @@ -0,0 +1,52 @@ + + * + * 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\MessageHandler; + +use MonsieurBiz\SyliusSearchPlugin\Index\Indexer; +use MonsieurBiz\SyliusSearchPlugin\Message\ProductReindexFromIds; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use Sylius\Component\Core\Repository\ProductRepositoryInterface; +use Sylius\Component\Registry\ServiceRegistryInterface; +use Symfony\Component\Messenger\Handler\MessageHandlerInterface; + +class ProductReindexFromIdsHandler implements MessageHandlerInterface +{ + private ProductRepositoryInterface $productRepository; + + private Indexer $indexer; + + private ServiceRegistryInterface $documentableRegistry; + + public function __construct( + ProductRepositoryInterface $productRepository, + Indexer $indexer, + ServiceRegistryInterface $documentableRegistry + ) { + $this->productRepository = $productRepository; + $this->indexer = $indexer; + $this->documentableRegistry = $documentableRegistry; + } + + public function __invoke(ProductReindexFromIds $message): void + { + /** @var DocumentableInterface $documentable */ + $documentable = $this->documentableRegistry->get('search.documentable.monsieurbiz_product'); + $products = $this->productRepository->findBy(['id' => $message->getProductIds()]); + + $this->indexer->indexByDocuments( + $documentable, + $products + ); + } +} diff --git a/src/MessageHandler/ProductReindexFromTaxonHandler.php b/src/MessageHandler/ProductReindexFromTaxonHandler.php new file mode 100644 index 00000000..8f8194da --- /dev/null +++ b/src/MessageHandler/ProductReindexFromTaxonHandler.php @@ -0,0 +1,60 @@ + + * + * 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\MessageHandler; + +use Doctrine\ORM\EntityRepository; +use MonsieurBiz\SyliusSearchPlugin\Index\Indexer; +use MonsieurBiz\SyliusSearchPlugin\Message\ProductReindexFromTaxon; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use Sylius\Component\Core\Repository\ProductRepositoryInterface; +use Sylius\Component\Registry\ServiceRegistryInterface; +use Symfony\Component\Messenger\Handler\MessageHandlerInterface; + +class ProductReindexFromTaxonHandler implements MessageHandlerInterface +{ + private ProductRepositoryInterface $productRepository; + + private Indexer $indexer; + + private ServiceRegistryInterface $documentableRegistry; + + public function __construct( + ProductRepositoryInterface $productRepository, + Indexer $indexer, + ServiceRegistryInterface $documentableRegistry + ) { + $this->productRepository = $productRepository; + $this->indexer = $indexer; + $this->documentableRegistry = $documentableRegistry; + } + + public function __invoke(ProductReindexFromTaxon $message): void + { + /** @var DocumentableInterface $documentable */ + $documentable = $this->documentableRegistry->get('search.documentable.monsieurbiz_product'); + if (!$this->productRepository instanceof EntityRepository) { + return; + } + $products = $this->productRepository->createQueryBuilder('o') + ->innerJoin('o.productTaxons', 'productTaxon') + ->andWhere('productTaxon.taxon = :taxonId') + ->setParameter('taxonId', $message->getTaxonId())->getQuery()->getResult() + ; + + $this->indexer->indexByDocuments( + $documentable, + $products + ); + } +} diff --git a/src/MessageHandler/ProductToDeleteFromIdsHandler.php b/src/MessageHandler/ProductToDeleteFromIdsHandler.php new file mode 100644 index 00000000..9ed3b90a --- /dev/null +++ b/src/MessageHandler/ProductToDeleteFromIdsHandler.php @@ -0,0 +1,52 @@ + + * + * 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\MessageHandler; + +use MonsieurBiz\SyliusSearchPlugin\Index\Indexer; +use MonsieurBiz\SyliusSearchPlugin\Message\ProductToDeleteFromIds; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use Sylius\Component\Core\Repository\ProductRepositoryInterface; +use Sylius\Component\Registry\ServiceRegistryInterface; +use Symfony\Component\Messenger\Handler\MessageHandlerInterface; + +class ProductToDeleteFromIdsHandler implements MessageHandlerInterface +{ + private ProductRepositoryInterface $productRepository; + + private Indexer $indexer; + + private ServiceRegistryInterface $documentableRegistry; + + public function __construct( + ProductRepositoryInterface $productRepository, + Indexer $indexer, + ServiceRegistryInterface $documentableRegistry + ) { + $this->productRepository = $productRepository; + $this->indexer = $indexer; + $this->documentableRegistry = $documentableRegistry; + } + + public function __invoke(ProductToDeleteFromIds $message): void + { + /** @var DocumentableInterface $documentable */ + $documentable = $this->documentableRegistry->get('search.documentable.monsieurbiz_product'); + $products = $this->productRepository->findBy(['id' => $message->getProductIds()]); + + $this->indexer->deleteByDocuments( + $documentable, + $products + ); + } +} diff --git a/src/Model/Config/FilesConfig.php b/src/Model/Config/FilesConfig.php deleted file mode 100644 index 73c4aa32..00000000 --- a/src/Model/Config/FilesConfig.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Model\Config; - -use MonsieurBiz\SyliusSearchPlugin\Exception\MissingConfigFileException; - -class FilesConfig -{ - /** @var string */ - private $searchPath; - - /** @var string */ - private $instantPath; - - /** @var string */ - private $taxonPath; - - public function __construct(array $files) - { - if (!isset($files['search']) || !isset($files['instant']) || !isset($files['taxon'])) { - throw new MissingConfigFileException('You need to have 3 config files : search, instant and taxon'); - } - $this->searchPath = $files['search']; - $this->instantPath = $files['instant']; - $this->taxonPath = $files['taxon']; - } - - /** - * @return string - */ - public function getSearchPath(): string - { - return $this->searchPath; - } - - /** - * @return string - */ - public function getInstantPath(): string - { - return $this->instantPath; - } - - /** - * @return string - */ - public function getTaxonPath(): string - { - return $this->taxonPath; - } -} diff --git a/src/Model/Config/GridConfig.php b/src/Model/Config/GridConfig.php deleted file mode 100644 index 30c8b551..00000000 --- a/src/Model/Config/GridConfig.php +++ /dev/null @@ -1,339 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Model\Config; - -use MonsieurBiz\SyliusSearchPlugin\Exception\UnknownGridConfigType; -use Sylius\Component\Core\Model\TaxonInterface; -use Sylius\Component\Product\Model\ProductAttribute; -use Sylius\Component\Product\Model\ProductOption; -use Sylius\Component\Resource\Repository\RepositoryInterface; -use Symfony\Component\HttpFoundation\Request; - -class GridConfig -{ - public const SEARCH_TYPE = 'search'; - public const TAXON_TYPE = 'taxon'; - public const INSTANT_TYPE = 'instant'; - - public const SORT_ASC = 'asc'; - public const SORT_DESC = 'desc'; - - public const FALLBACK_LIMIT = 10; - - /** @var array */ - private $config; - - /** @var string[] */ - private $isInitialized = false; - - /** @var string */ - private $type; - - /** @var string */ - private $locale; - - /** @var string */ - private $query; - - /** @var int */ - private $page; - - /** @var int[] */ - private $limits; - - /** @var int */ - private $limit; - - /** @var string[] */ - private $sorting; - - /** @var TaxonInterface|null */ - private $taxon; - - /** @var array */ - private $appliedFilters; - - /** - * @var array|null - */ - private $filterableAttributes; - - /** - * @var array|null - */ - private $filterableOptions; - - /** - * @var RepositoryInterface - */ - private $productAttributeRepository; - - /** - * @var RepositoryInterface - */ - private $productOptionRepository; - - /** - * GridConfig constructor. - * - * @param array $config - * @param RepositoryInterface $productAttributeRepository - * @param RepositoryInterface $productOptionRepository - */ - public function __construct(array $config, RepositoryInterface $productAttributeRepository, RepositoryInterface $productOptionRepository) - { - $this->config = $config; - $this->productAttributeRepository = $productAttributeRepository; - $this->productOptionRepository = $productOptionRepository; - } - - /** - * @param string $type - * @param Request $request - * @param TaxonInterface|null $taxon - */ - public function init(string $type, Request $request, ?TaxonInterface $taxon = null): void - { - if ($this->isInitialized) { - return; - } - - switch ($type) { - case self::SEARCH_TYPE: - // Set type, locale, page and query - $this->type = $type; - $this->locale = $request->getLocale(); - $this->page = max(1, (int) $request->get('page')); - $this->query = htmlspecialchars(urldecode($request->get('query'))); - - // Set sorting - $availableSorting = $this->config['sorting']['search'] ?? []; - $this->sorting = $this->cleanSorting($request->get('sorting'), $availableSorting); - - // Set limit - $this->limit = max(1, (int) $request->get('limit')); - $this->limits = $this->config['limits']['search'] ?? []; - if (!\in_array($this->limit, $this->limits, true)) { - $this->limit = $this->config['default_limit']['search'] ?? self::FALLBACK_LIMIT; - } - - // Set applied filters - $this->appliedFilters = $request->get('attribute') ?? []; - if ($priceFilter = $request->get('price')) { - $this->appliedFilters['price'] = $priceFilter; - } - - $this->isInitialized = true; - break; - case self::TAXON_TYPE: - // Set type, locale, page and taxon - $this->type = $type; - $this->locale = $request->getLocale(); - $this->page = max(1, (int) $request->get('page')); - $this->taxon = $taxon; - - // Set sorting - $availableSorting = $this->config['sorting']['taxon'] ?? []; - $this->sorting = $this->cleanSorting($request->get('sorting'), $availableSorting); - if (!\is_array($this->sorting) || empty($this->sorting)) { - $this->sorting['position'] = self::SORT_ASC; - } - - // Set applied filters - $this->appliedFilters = $request->get('attribute') ?? []; - if ($priceFilter = $request->get('price')) { - $this->appliedFilters['price'] = $priceFilter; - } - - // Set limit - $this->limit = max(1, (int) $request->get('limit')); - $this->limits = $this->config['limits']['taxon'] ?? []; - if (!\in_array($this->limit, $this->limits, true)) { - $this->limit = $this->config['default_limit']['taxon'] ?? self::FALLBACK_LIMIT; - } - $this->isInitialized = true; - break; - case self::INSTANT_TYPE: - // Set type, locale, page and query - $this->type = $type; - $this->locale = $request->getLocale(); - $this->page = 1; - $this->query = htmlspecialchars(urldecode($request->get('query'))); - - // Set limit - $this->limit = $this->config['default_limit']['instant'] ?? self::FALLBACK_LIMIT; - $this->isInitialized = true; - break; - default: - throw new UnknownGridConfigType(); - } - } - - /** - * @return string - */ - public function getType(): string - { - return $this->type; - } - - /** - * @return string - */ - public function getLocale(): string - { - return $this->locale; - } - - /** - * @return string - */ - public function getQuery(): string - { - return $this->query; - } - - /** - * @return int - */ - public function getPage(): int - { - return $this->page; - } - - /** - * @return int[] - */ - public function getLimits(): array - { - return $this->limits; - } - - /** - * @return int - */ - public function getLimit(): int - { - return $this->limit; - } - - /** - * @return string[] - */ - public function getSorting(): array - { - return $this->sorting; - } - - /** - * @return string[] - */ - public function getAttributeFilters(): array - { - if (null === $this->filterableAttributes) { - $attributes = $this->productAttributeRepository->findBy([ - 'filterable' => true, - ]); - $this->filterableAttributes = []; - /** @var ProductAttribute $attribute */ - foreach ($attributes as $attribute) { - $this->filterableAttributes[] = $attribute->getCode(); - } - } - - return $this->filterableAttributes; - } - - /** - * @return string[] - */ - public function getOptionFilters(): array - { - if (null === $this->filterableOptions) { - $options = $this->productOptionRepository->findBy([ - 'filterable' => true, - ]); - $this->filterableOptions = []; - /** @var ProductOption $option */ - foreach ($options as $option) { - $this->filterableOptions[] = $option->getCode(); - } - } - - return $this->filterableOptions; - } - - /** - * @return bool - */ - public function haveToApplyManuallyFilters(): bool - { - return $this->config['filters']['apply_manually'] ?? false; - } - - /** - * @return bool - */ - public function useMainTaxonForFilter(): bool - { - return $this->config['filters']['use_main_taxon'] ?? false; - } - - /** - * @return string[] - */ - public function getFilters(): array - { - return array_merge($this->getAttributeFilters(), $this->getOptionFilters()); - } - - /** - * @return array - */ - public function getAppliedFilters(): array - { - return $this->appliedFilters; - } - - /** - * @return TaxonInterface|null - */ - public function getTaxon(): ?TaxonInterface - { - return $this->taxon; - } - - /** - * Be sure given sort in available. - * - * @param $sorting - * @param $availableSorting - * - * @return array - */ - private function cleanSorting(?array $sorting, array $availableSorting): array - { - if (!\is_array($sorting)) { - return []; - } - - foreach ($sorting as $field => $order) { - if (!\in_array($field, $availableSorting, true) || !\in_array($order, [self::SORT_ASC, self::SORT_DESC], true)) { - unset($sorting[$field]); - } - } - - return $sorting; - } -} diff --git a/src/Exception/MissingLocaleException.php b/src/Model/Datasource/DatasourceInterface.php similarity index 62% rename from src/Exception/MissingLocaleException.php rename to src/Model/Datasource/DatasourceInterface.php index f0cd340b..da80d29a 100644 --- a/src/Exception/MissingLocaleException.php +++ b/src/Model/Datasource/DatasourceInterface.php @@ -5,16 +5,15 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * 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\Exception; +namespace MonsieurBiz\SyliusSearchPlugin\Model\Datasource; -use Exception; - -class MissingLocaleException extends Exception +interface DatasourceInterface { + public function getItems(string $sourceClass): iterable; } diff --git a/src/Model/Datasource/RepositoryDatasource.php b/src/Model/Datasource/RepositoryDatasource.php new file mode 100644 index 00000000..3f7b93b3 --- /dev/null +++ b/src/Model/Datasource/RepositoryDatasource.php @@ -0,0 +1,51 @@ + + * + * 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\Model\Datasource; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; +use Pagerfanta\Pagerfanta; +use Sylius\Component\Resource\Repository\RepositoryInterface; + +class RepositoryDatasource implements DatasourceInterface +{ + private EntityManagerInterface $entityManager; + + public function __construct(EntityManagerInterface $entityManager) + { + $this->entityManager = $entityManager; + } + + public function getItems(string $sourceClass): iterable + { + /** @phpstan-ignore-next-line */ + $repository = $this->entityManager->getRepository($sourceClass); + if ($repository instanceof RepositoryInterface && ($paginator = $repository->createPaginator()) instanceof Pagerfanta) { + $page = 1; + while ($paginator->hasNextPage()) { + $paginator->setCurrentPage($page); + foreach ($paginator as $item) { + yield $item; + } + if ($paginator->hasNextPage()) { + $page = $paginator->getNextPage(); + } + } + + return null; + } + + return $repository instanceof EntityRepository ? $repository->createQueryBuilder('o')->getQuery()->toIterable() : null; + } +} diff --git a/src/Model/Document/Filter.php b/src/Model/Document/Filter.php deleted file mode 100644 index 86bd2a55..00000000 --- a/src/Model/Document/Filter.php +++ /dev/null @@ -1,92 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Model\Document; - -class Filter -{ - /** - * @var string - */ - private $code; - - /** - * @var string - */ - private $label; - - /** - * @var FilterValue[] - */ - private $values = []; - - /** - * @var int - */ - private $count; - - /** - * Filter constructor. - * - * @param string $code - * @param string $label - * @param int $count - */ - public function __construct(string $code, string $label, int $count) - { - $this->code = $code; - $this->label = $label; - $this->count = $count; - } - - /** - * @return string - */ - public function getCode(): string - { - return $this->code; - } - - /** - * @return string - */ - public function getLabel(): string - { - return $this->label; - } - - /** - * @return FilterValue[] - */ - public function getValues(): array - { - return $this->values; - } - - /** - * @param $value - * @param $count - */ - public function addValue($value, $count): void - { - $this->values[] = new FilterValue($value, $count); - } - - /** - * @return int - */ - public function getCount(): int - { - return $this->count; - } -} diff --git a/src/Model/Document/Index/AbstractIndex.php b/src/Model/Document/Index/AbstractIndex.php deleted file mode 100644 index 53c9d75d..00000000 --- a/src/Model/Document/Index/AbstractIndex.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Model\Document\Index; - -use JoliCode\Elastically\Client; -use JoliCode\Elastically\IndexBuilder; -use JoliCode\Elastically\Indexer; -use MonsieurBiz\SyliusSearchPlugin\Provider\DocumentRepositoryProvider; - -abstract class AbstractIndex -{ - public const DOCUMENT_INDEX_NAME = 'documents'; - - /** - * @var DocumentRepositoryProvider - */ - protected $documentRepositoryProvider; - - /** @var Client */ - private $client; - - /** - * PopulateCommand constructor. - * - * @param Client $client - */ - public function __construct( - Client $client - ) { - $this->client = $client; - } - - /** - * Get the client. - * - * @return Client - */ - protected function getClient(): Client - { - return $this->client; - } - - /** - * Retrieve the index name. - * - * @param string $locale - * - * @return string - */ - protected function getIndexName(string $locale): string - { - return self::DOCUMENT_INDEX_NAME . '-' . strtolower($locale); - } - - /** - * @return IndexBuilder - */ - protected function getIndexBuilder(): IndexBuilder - { - return $this->client->getIndexBuilder(); - } - - /** - * @return Indexer - */ - protected function getIndexer(): Indexer - { - return $this->client->getIndexer(); - } -} diff --git a/src/Model/Document/Index/Indexer.php b/src/Model/Document/Index/Indexer.php index 64779821..58963949 100644 --- a/src/Model/Document/Index/Indexer.php +++ b/src/Model/Document/Index/Indexer.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -21,8 +21,6 @@ use MonsieurBiz\SyliusSearchPlugin\Model\Document\Result; use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; use MonsieurBiz\SyliusSearchPlugin\Provider\DocumentRepositoryProvider; -use MonsieurBiz\SyliusSearchPlugin\Provider\SearchQueryProvider; -use Psr\Log\LoggerInterface; use Sylius\Component\Locale\Model\LocaleInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Webmozart\Assert\Assert; @@ -42,12 +40,6 @@ class Indexer extends AbstractIndex /** * PopulateCommand constructor. - * - * @param Client $client - * @param DocumentRepositoryProvider $documentRepositoryProvider - * @param RepositoryInterface $localeRepository - * @param SearchQueryProvider $searchQueryProvider - * @param LoggerInterface $logger */ public function __construct( Client $client, @@ -61,15 +53,13 @@ public function __construct( /** * Retrieve all available locales. - * - * @return array */ public function getLocales(): array { if (empty($this->locales)) { $locales = $this->localeRepository->findAll(); $this->locales = array_map( - function(LocaleInterface $locale) { + function (LocaleInterface $locale) { return $locale->getCode(); }, $locales @@ -94,8 +84,6 @@ public function indexAll(): void /** * Index all document for a locale. * - * @param string $locale - * * @throws \Exception */ public function indexAllByLocale(string $locale): void @@ -122,6 +110,7 @@ public function indexAllByLocale(string $locale): void $this->getIndexer()->flush(); $this->getIndexer()->refresh($indexName); + try { $this->getIndexBuilder()->purgeOldIndices($indexName); } catch (ResponseException $exception) { @@ -132,8 +121,6 @@ public function indexAllByLocale(string $locale): void /** * Index a document for all locales. * - * @param DocumentableInterface $subject - * * @throws \Exception */ public function indexOne(DocumentableInterface $subject): void @@ -147,9 +134,6 @@ public function indexOne(DocumentableInterface $subject): void /** * Index a document for one locale. * - * @param Result $document - * @param string $locale - * * @throws MissingParamException */ public function indexOneByLocale(Result $document, string $locale): void @@ -163,8 +147,6 @@ public function indexOneByLocale(Result $document, string $locale): void /** * Remove a document for all locales. * - * @param DocumentableInterface $subject - * * @throws \Exception */ public function removeOne(DocumentableInterface $subject): void @@ -178,9 +160,6 @@ public function removeOne(DocumentableInterface $subject): void /** * Remove a document for all locales. * - * @param Result $document - * @param string $locale - * * @throws MissingParamException */ public function removeOneByLocale(Result $document, string $locale): void diff --git a/src/Model/Document/Index/Search.php b/src/Model/Document/Index/Search.php deleted file mode 100644 index 6db1ee29..00000000 --- a/src/Model/Document/Index/Search.php +++ /dev/null @@ -1,280 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Model\Document\Index; - -use Elastica\Exception\Connection\HttpException; -use Elastica\Exception\ResponseException; -use Elastica\ResultSet as ElasticaResultSet; -use JoliCode\Elastically\Client; -use MonsieurBiz\SyliusSearchPlugin\Exception\ReadFileException; -use MonsieurBiz\SyliusSearchPlugin\Helper\AggregationHelper; -use MonsieurBiz\SyliusSearchPlugin\Helper\FilterHelper; -use MonsieurBiz\SyliusSearchPlugin\Helper\SortHelper; -use MonsieurBiz\SyliusSearchPlugin\Model\ArrayObject; -use MonsieurBiz\SyliusSearchPlugin\Model\Config\GridConfig; -use MonsieurBiz\SyliusSearchPlugin\Model\Document\ResultSet; -use MonsieurBiz\SyliusSearchPlugin\Provider\SearchQueryProvider; -use Psr\Log\LoggerInterface; -use Sylius\Component\Channel\Context\ChannelContextInterface; -use Symfony\Component\Yaml\Yaml; - -class Search extends AbstractIndex -{ - /** @var SearchQueryProvider */ - private $searchQueryProvider; - - /** @var LoggerInterface */ - private $logger; - - /** @var ChannelContextInterface */ - private $channelContext; - - /** - * PopulateCommand constructor. - * - * @param Client $client - * @param SearchQueryProvider $searchQueryProvider - * @param ChannelContextInterface $channelContext - * @param LoggerInterface $logger - */ - public function __construct( - Client $client, - SearchQueryProvider $searchQueryProvider, - ChannelContextInterface $channelContext, - LoggerInterface $logger - ) { - parent::__construct($client); - $this->searchQueryProvider = $searchQueryProvider; - $this->channelContext = $channelContext; - $this->logger = $logger; - } - - /** - * Search documents for a given locale, search terms, max number items and page. - * - * @param GridConfig $gridConfig - * - * @return ResultSet - */ - public function search(GridConfig $gridConfig): ResultSet - { - try { - return $this->query($gridConfig, $this->getSearchQuery($gridConfig)); - } catch (ReadFileException $exception) { - $this->logger->critical($exception->getMessage()); - - return new ResultSet($gridConfig->getLimit(), $gridConfig->getPage()); - } - } - - /** - * Instant search documents for a given locale, query and a max number items. - * - * @param GridConfig $gridConfig - * - * @return ResultSet - */ - public function instant(GridConfig $gridConfig): ResultSet - { - try { - return $this->query($gridConfig, $this->getInstantQuery($gridConfig)); - } catch (ReadFileException $exception) { - $this->logger->critical($exception->getMessage()); - - return new ResultSet($gridConfig->getLimit(), $gridConfig->getPage()); - } - } - - /** - * Taxon search documents for a given locale, taxon code, max number items and page. - * - * @param GridConfig $gridConfig - * - * @return ResultSet - */ - public function taxon(GridConfig $gridConfig): ResultSet - { - try { - return $this->query($gridConfig, $this->getTaxonQuery($gridConfig)); - } catch (ReadFileException $exception) { - $this->logger->critical($exception->getMessage()); - - return new ResultSet($gridConfig->getLimit(), $gridConfig->getPage()); - } - } - - /** - * Perform search for a given query. - * - * @param GridConfig $gridConfig - * @param array $query - * - * @return ResultSet - */ - private function query(GridConfig $gridConfig, array $query) - { - try { - /** @var ElasticaResultSet $results */ - $results = $this->getClient()->getIndex($this->getIndexName($gridConfig->getLocale()))->search( - $query, $gridConfig->getLimit() - ); - } catch (HttpException $exception) { - $this->logger->critical($exception->getMessage()); - - return new ResultSet($gridConfig->getLimit(), $gridConfig->getPage()); - } catch (ResponseException $exception) { - $this->logger->critical($exception->getMessage()); - - return new ResultSet($gridConfig->getLimit(), $gridConfig->getPage()); - } - - return new ResultSet($gridConfig->getLimit(), $gridConfig->getPage(), $results, $gridConfig->getTaxon()); - } - - /** - * Retrieve the query to send to Elasticsearch for search. - * - * @param GridConfig $gridConfig - * - * @throws ReadFileException - * - * @return array - */ - private function getSearchQuery(GridConfig $gridConfig): array - { - $query = $this->searchQueryProvider->getSearchQuery(); - - // Replace params - $query = str_replace('{{QUERY}}', $gridConfig->getQuery(), $query); - $query = str_replace('{{CHANNEL}}', $this->channelContext->getChannel()->getCode(), $query); - - // Convert query to array - $query = $this->parseQuery($query); - - $appliedFilters = FilterHelper::buildFilters($gridConfig->getAppliedFilters()); - if ($gridConfig->haveToApplyManuallyFilters() && isset($appliedFilters['bool']['filter'])) { - // Will retrieve filters after we applied the current ones - $query['query']['bool']['filter'] = array_merge( - $query['query']['bool']['filter'], $appliedFilters['bool']['filter'] - ); - } elseif (!empty($appliedFilters)) { - // Will retrieve filters before we applied the current ones - $query['post_filter'] = new ArrayObject($appliedFilters); // Use custom ArrayObject because Elastica make `toArray` on it. - } - - // Manage limits - $from = ($gridConfig->getPage() - 1) * $gridConfig->getLimit(); - $query['from'] = max(0, $from); - $query['size'] = max(1, $gridConfig->getLimit()); - - // Manage sorting - $channelCode = $this->channelContext->getChannel()->getCode(); - foreach ($gridConfig->getSorting() as $field => $order) { - $query['sort'][] = SortHelper::getSortParamByField($field, $channelCode, $order); - break; // only 1 - } - - // Manage filters - $aggs = AggregationHelper::buildAggregations($gridConfig->getFilters()); - if (!empty($aggs)) { - $query['aggs'] = AggregationHelper::buildAggregations($gridConfig->getFilters()); - } - - return $query; - } - - /** - * Retrieve the query to send to Elasticsearch for instant search. - * - * @param GridConfig $gridConfig - * - * @throws ReadFileException - * - * @return array - */ - private function getInstantQuery(GridConfig $gridConfig): array - { - $query = $this->searchQueryProvider->getInstantQuery(); - - // Replace params - $query = str_replace('{{QUERY}}', $gridConfig->getQuery(), $query); - $query = str_replace('{{CHANNEL}}', $this->channelContext->getChannel()->getCode(), $query); - - // Convert query to array - return $this->parseQuery($query); - } - - /** - * Retrieve the query to send to Elasticsearch for taxon search. - * - * @param GridConfig $gridConfig - * - * @throws ReadFileException - * - * @return array - */ - private function getTaxonQuery(GridConfig $gridConfig): array - { - $query = $this->searchQueryProvider->getTaxonQuery(); - - // Replace params - $query = str_replace('{{TAXON}}', $gridConfig->getTaxon()->getCode(), $query); - $query = str_replace('{{CHANNEL}}', $this->channelContext->getChannel()->getCode(), $query); - - // Convert query to array - $query = $this->parseQuery($query); - - // Apply filters - $appliedFilters = FilterHelper::buildFilters($gridConfig->getAppliedFilters()); - if ($gridConfig->haveToApplyManuallyFilters() && isset($appliedFilters['bool']['filter'])) { - // Will retrieve filters after we applied the current ones - $query['query']['bool']['filter'] = array_merge( - $query['query']['bool']['filter'], $appliedFilters['bool']['filter'] - ); - } elseif (!empty($appliedFilters)) { - // Will retrieve filters before we applied the current ones - $query['post_filter'] = new ArrayObject($appliedFilters); // Use custom ArrayObject because Elastica make `toArray` on it. - } - - // Manage limits - $from = ($gridConfig->getPage() - 1) * $gridConfig->getLimit(); - $query['from'] = max(0, $from); - $query['size'] = max(1, $gridConfig->getLimit()); - - // Manage sorting - $channelCode = $this->channelContext->getChannel()->getCode(); - foreach ($gridConfig->getSorting() as $field => $order) { - $query['sort'][] = SortHelper::getSortParamByField($field, $channelCode, $order, $gridConfig->getTaxon()->getCode()); - break; // only 1 - } - - // Manage filters - $aggs = AggregationHelper::buildAggregations($gridConfig->getFilters()); - if (!empty($aggs)) { - $query['aggs'] = AggregationHelper::buildAggregations($gridConfig->getFilters()); - } - - return $query; - } - - /** - * @param string $query - * - * @return array - */ - private function parseQuery(string $query): array - { - return Yaml::parse($query); - } -} diff --git a/src/Model/Document/RangeFilter.php b/src/Model/Document/RangeFilter.php deleted file mode 100644 index 2b58e956..00000000 --- a/src/Model/Document/RangeFilter.php +++ /dev/null @@ -1,115 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Model\Document; - -class RangeFilter -{ - /** - * @var string - */ - private $code; - - /** - * @var string - */ - private $label; - - /** - * @var string - */ - private $minLabel; - - /** - * @var string - */ - private $maxLabel; - - /** - * @var int - */ - private $min; - - /** - * @var int - */ - private $max; - - /** - * Filter constructor. - * - * @param string $code - * @param string $label - * @param string $minLabel - * @param string $maxLabel - * @param int $min - * @param int $max - */ - public function __construct(string $code, string $label, string $minLabel, string $maxLabel, int $min, int $max) - { - $this->code = $code; - $this->label = $label; - $this->minLabel = $minLabel; - $this->maxLabel = $maxLabel; - $this->min = $min; - $this->max = $max; - } - - /** - * @return string - */ - public function getCode(): string - { - return $this->code; - } - - /** - * @return string - */ - public function getLabel(): string - { - return $this->label; - } - - /** - * @return string - */ - public function getMinLabel(): string - { - return $this->minLabel; - } - - /** - * @return string - */ - public function getMaxLabel(): string - { - return $this->maxLabel; - } - - /** - * @return int - */ - public function getMin(): int - { - return $this->min; - } - - /** - * @return int - */ - public function getMax(): int - { - return $this->max; - } -} diff --git a/src/Model/Document/Result.php b/src/Model/Document/Result.php deleted file mode 100644 index 64034517..00000000 --- a/src/Model/Document/Result.php +++ /dev/null @@ -1,217 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Model\Document; - -use MonsieurBiz\SyliusSearchPlugin\Exception\MissingLocaleException; -use MonsieurBiz\SyliusSearchPlugin\Exception\MissingParamException; -use MonsieurBiz\SyliusSearchPlugin\Exception\MissingPriceException; -use MonsieurBiz\SyliusSearchPlugin\Exception\NotSupportedTypeException; -use MonsieurBiz\SyliusSearchPlugin\generated\Model\Attributes; -use MonsieurBiz\SyliusSearchPlugin\generated\Model\Document; -use MonsieurBiz\SyliusSearchPlugin\generated\Model\Price; -use MonsieurBiz\SyliusSearchPlugin\generated\Model\Taxon; -use MonsieurBiz\SyliusSearchPlugin\Provider\UrlParamsProvider; - -class Result extends Document implements ResultInterface -{ - /** - * Document ID in elasticsearch. - * - * @throws MissingParamException - * - * @return string - */ - public function getUniqId(): string - { - if (!$this->getType()) { - throw new MissingParamException('Missing "type" for document'); - } - if (!$this->getId()) { - throw new MissingParamException('Missing "ID" for document'); - } - - return sprintf('%s-%d', $this->getType(), $this->getId()); - } - - /** - * @param string $code - * - * @return Attributes - */ - public function getAttribute(string $code): ?Attributes - { - foreach ($this->getAttributes() as $attribute) { - if ($attribute->getCode() === $code) { - return $attribute; - } - } - - return null; - } - - /** - * @param string $channelCode - * @param string $currencyCode - * - * @throws MissingPriceException - * - * @return Price|null - */ - public function getPriceByChannelAndCurrency(string $channelCode, string $currencyCode): ?Price - { - if (null === $this->getPrice()) { - return null; - } - foreach ($this->getPrice() as $price) { - if ($price->getChannel() === $channelCode && $price->getCurrency() === $currencyCode) { - return $price; - } - } - throw new MissingPriceException(sprintf('Price not found for channel "%s" and currency "%s"', $channelCode, $currencyCode)); - } - - /** - * @param string $channelCode - * @param string $currencyCode - * - * @return Price|null - */ - public function getOriginalPriceByChannelAndCurrency(string $channelCode, string $currencyCode): ?Price - { - if (null === $this->getOriginalPrice()) { - return null; - } - - foreach ($this->getOriginalPrice() as $price) { - if ($price->getChannel() === $channelCode && $price->getCurrency() === $currencyCode) { - return $price; - } - } - - return null; - } - - /** - * @throws MissingLocaleException - * - * @return string - */ - public function getLocale(): string - { - foreach ($this->getAttributes() as $attribute) { - if ($attribute->getLocale()) { - return $attribute->getLocale(); - } - } - - throw new MissingLocaleException('Locale not found in document'); - } - - /** - * @throws MissingLocaleException - * @throws NotSupportedTypeException - * - * @return UrlParamsProvider - */ - public function getUrlParams(): UrlParamsProvider - { - switch ($this->getType()) { - case 'product': - return new UrlParamsProvider('sylius_shop_product_show', ['slug' => $this->getSlug(), '_locale' => $this->getLocale()]); - break; - } - - throw new NotSupportedTypeException(sprintf('Object type "%s" not supported to get URL', $this->getType())); - } - - /** - * @param string $channel - * - * @return Result - */ - public function addChannel(string $channel): self - { - $this->setChannel($this->getChannel() ? array_unique(array_merge($this->getChannel(), [$channel])) : [$channel]); - - return $this; - } - - /** - * @param string $code - * @param string $name - * @param int $position - * @param int $level - * @param int $productPosition - * - * @return Result - */ - public function addTaxon(string $code, string $name, int $position, int $level, int $productPosition): ResultInterface - { - $taxon = new Taxon(); - $taxon->setCode($code)->setPosition($position)->setName($name)->setLevel($level)->setProductPosition($productPosition); - $this->setTaxon($this->getTaxon() ? array_merge($this->getTaxon(), [$taxon]) : [$taxon]); - - return $this; - } - - /** - * @param string $channel - * @param string $currency - * @param int $value - * - * @return Result - */ - public function addPrice(string $channel, string $currency, int $value): ResultInterface - { - $price = new Price(); - $price->setChannel($channel)->setCurrency($currency)->setValue($value); - $this->setPrice($this->getPrice() ? array_merge($this->getPrice(), [$price]) : [$price]); - - return $this; - } - - /** - * @param string $channel - * @param string $currency - * @param int $value - * - * @return Result - */ - public function addOriginalPrice(string $channel, string $currency, int $value): ResultInterface - { - $price = new Price(); - $price->setChannel($channel)->setCurrency($currency)->setValue($value); - $this->setOriginalPrice($this->getOriginalPrice() ? array_merge($this->getOriginalPrice(), [$price]) : [$price]); - - return $this; - } - - /** - * @param string $code - * @param string $name - * @param array $value - * @param string $locale - * @param int $score - * - * @return Result - */ - public function addAttribute(string $code, string $name, array $value, string $locale, int $score): ResultInterface - { - $attribute = new Attributes(); - $attribute->setCode($code)->setName($name)->setValue($value)->setLocale($locale)->setScore($score); - $this->setAttributes($this->getAttributes() ? array_merge($this->getAttributes(), [$attribute]) : [$attribute]); - - return $this; - } -} diff --git a/src/Model/Document/ResultInterface.php b/src/Model/Document/ResultInterface.php deleted file mode 100644 index a6ec4f4f..00000000 --- a/src/Model/Document/ResultInterface.php +++ /dev/null @@ -1,126 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Model\Document; - -use MonsieurBiz\SyliusSearchPlugin\Exception\MissingLocaleException; -use MonsieurBiz\SyliusSearchPlugin\Exception\MissingParamException; -use MonsieurBiz\SyliusSearchPlugin\Exception\MissingPriceException; -use MonsieurBiz\SyliusSearchPlugin\Exception\NotSupportedTypeException; -use MonsieurBiz\SyliusSearchPlugin\generated\Model\Attributes; -use MonsieurBiz\SyliusSearchPlugin\generated\Model\Document; -use MonsieurBiz\SyliusSearchPlugin\generated\Model\Price; -use MonsieurBiz\SyliusSearchPlugin\generated\Model\Taxon; -use MonsieurBiz\SyliusSearchPlugin\Provider\UrlParamsProvider; - -interface ResultInterface -{ - /** - * Document ID in elasticsearch. - * - * @throws MissingParamException - * - * @return string - */ - public function getUniqId(): string; - - - /** - * @param string $code - * - * @return Attributes - */ - public function getAttribute(string $code): ?Attributes; - - /** - * @param string $channelCode - * @param string $currencyCode - * - * @throws MissingPriceException - * - * @return Price|null - */ - public function getPriceByChannelAndCurrency(string $channelCode, string $currencyCode): ?Price; - - /** - * @param string $channelCode - * @param string $currencyCode - * - * @throws MissingPriceException - * - * @return Price|null - */ - public function getOriginalPriceByChannelAndCurrency(string $channelCode, string $currencyCode): ?Price; - - /** - * @throws MissingLocaleException - * - * @return string - */ - public function getLocale(): string; - - /** - * @throws MissingLocaleException - * @throws NotSupportedTypeException - * - * @return UrlParamsProvider - */ - public function getUrlParams(): UrlParamsProvider; - - /** - * @param string $channel - * - * @return ResultInterface - */ - public function addChannel(string $channel): self; - - /** - * @param string $code - * @param string $name - * @param int $position - * @param int $level - * @param int $productPosition - * - * @return ResultInterface - */ - public function addTaxon(string $code, string $name, int $position, int $level, int $productPosition): self; - - /** - * @param string $channel - * @param string $currency - * @param int $value - * - * @return ResultInterface - */ - public function addPrice(string $channel, string $currency, int $value): self; - - /** - * @param string $channel - * @param string $currency - * @param int $value - * - * @return ResultInterface - */ - public function addOriginalPrice(string $channel, string $currency, int $value): self; - - /** - * @param string $code - * @param string $name - * @param array $value - * @param string $locale - * @param int $score - * - * @return ResultInterface - */ - public function addAttribute(string $code, string $name, array $value, string $locale, int $score): self; -} diff --git a/src/Model/Document/ResultSet.php b/src/Model/Document/ResultSet.php deleted file mode 100644 index 3cb19121..00000000 --- a/src/Model/Document/ResultSet.php +++ /dev/null @@ -1,335 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Model\Document; - -use Elastica\ResultSet as ElasticaResultSet; -use JoliCode\Elastically\Result; -use MonsieurBiz\SyliusSearchPlugin\Adapter\ResultSetAdapter; -use MonsieurBiz\SyliusSearchPlugin\generated\Model\Taxon; -use Pagerfanta\Pagerfanta; -use Sylius\Component\Core\Model\TaxonInterface; - -class ResultSet -{ - /** @var Result[] */ - private $results = []; - - /** @var int */ - private $totalHits; - - /** @var int */ - private $maxItems; - - /** @var int */ - private $page; - - /** @var Filter[] */ - private $filters = []; - - /** @var RangeFilter|null */ - private $priceFilter; - - /** @var Filter|null */ - private $taxonFilter; - - /** @var Filter|null */ - private $mainTaxonFilter; - - /** @var Pagerfanta */ - private $pager; - - /** - * SearchResults constructor. - * - * @param int $maxItems - * @param int $page - * @param ElasticaResultSet|null $resultSet - * @param TaxonInterface|null $taxon - */ - public function __construct(int $maxItems, int $page, ?ElasticaResultSet $resultSet = null, ?TaxonInterface $taxon = null) - { - $this->maxItems = $maxItems; - $this->page = $page; - - // Empty result set - if (null === $resultSet) { - $this->totalHits = 0; - $this->results = []; - $this->filters = []; - } else { - /** @var Result $result */ - foreach ($resultSet as $result) { - $this->results[] = $result->getModel(); - } - $this->totalHits = $resultSet->getTotalHits(); - $this->initFilters($resultSet, $taxon); - } - - $this->initPager(); - } - - /** - * Init pager with Pager Fanta. - */ - private function initPager(): void - { - $adapter = new ResultSetAdapter($this); - $this->pager = new Pagerfanta($adapter); - $this->pager->setMaxPerPage($this->maxItems); - $this->pager->setCurrentPage($this->page); - } - - /** - * Init filters array depending on result aggregations. - * - * @param ElasticaResultSet $resultSet - * @param TaxonInterface|null $taxon - */ - private function initFilters(ElasticaResultSet $resultSet, ?TaxonInterface $taxon = null): void - { - $aggregations = $resultSet->getAggregations(); - // No aggregation so don't perform filters - if (empty($aggregations)) { - return; - } - - // Retrieve filters labels in aggregations - $attributes = []; - $attributeAggregations = $aggregations['attributes'] ?? []; - unset($attributeAggregations['doc_count']); - $attributeCodeBuckets = $attributeAggregations['codes']['buckets'] ?? []; - foreach ($attributeCodeBuckets as $attributeCodeBucket) { - $attributeCode = $attributeCodeBucket['key']; - $attributeNameBuckets = $attributeCodeBucket['names']['buckets'] ?? []; - foreach ($attributeNameBuckets as $attributeNameBucket) { - $attributeName = $attributeNameBucket['key']; - $attributes[$attributeCode] = $attributeName; - break; - } - } - - // Retrieve filters values in aggregations - $filterAggregations = $aggregations['filters'] ?? []; - unset($filterAggregations['doc_count']); - foreach ($filterAggregations as $field => $aggregation) { - if (0 === $aggregation['doc_count']) { - continue; - } - $filter = new Filter($field, $attributes[$field] ?? $field, $aggregation['doc_count']); - $buckets = $aggregation['values']['buckets'] ?? []; - foreach ($buckets as $bucket) { - if (isset($bucket['key']) && isset($bucket['doc_count'])) { - $filter->addValue($bucket['key'], $bucket['doc_count']); - } - } - $this->filters[] = $filter; - } - $this->sortFilters(); - - $this->addTaxonFilter($aggregations, $taxon); - $this->addMainTaxonFilter($aggregations, $taxon); - - $this->addPriceFilter($aggregations); - } - - /** - * @return Result[] - */ - public function getResults(): array - { - return $this->results; - } - - /** - * @return Filter[] - */ - public function getFilters(): array - { - return $this->filters; - } - - /** - * @return int - */ - public function getTotalHits(): int - { - return $this->totalHits; - } - - /** - * @return Pagerfanta - */ - public function getPager(): Pagerfanta - { - return $this->pager; - } - - /** - * @return Filter|null - */ - public function getTaxonFilter(): ?Filter - { - return $this->taxonFilter; - } - - /** - * @return Filter|null - */ - public function getMainTaxonFilter(): ?Filter - { - return $this->mainTaxonFilter; - } - - /** - * @return RangeFilter|null - */ - public function getPriceFilter(): ?RangeFilter - { - return $this->priceFilter; - } - - /** - * Sort filters. - */ - protected function sortFilters(): void - { - usort($this->filters, function($filter1, $filter2) { - /** @var Filter $filter1 */ - /** @var Filter $filter2 */ - - // If same count we display the filters with more values before - if ($filter1->getCount() === $filter2->getCount()) { - return \count($filter2->getValues()) > \count($filter1->getValues()); - } - - return $filter2->getCount() > $filter1->getCount(); - }); - } - - /** - * Add taxon filter depending on aggregations. - * - * @param array $aggregations - * @param TaxonInterface|null $taxon - */ - protected function addTaxonFilter(array $aggregations, ?TaxonInterface $taxon): void - { - $taxonAggregation = $aggregations['taxons'] ?? null; - if ($taxonAggregation && $taxonAggregation['doc_count'] > 0) { - // Get current taxon level to retrieve only greater levels, in search we will take only the first level - $currentTaxonLevel = $taxon ? $taxon->getLevel() : 0; - - // Get children taxon if we have current taxon - $childrenTaxon = []; - if ($taxon) { - foreach ($taxon->getChildren() as $child) { - $childrenTaxon[$child->getCode()] = $child->getLevel(); - } - } - - $filter = new Filter('taxon', 'monsieurbiz_searchplugin.filters.taxon_filter', $taxonAggregation['doc_count']); - - // Get taxon code in aggregation - $taxonCodeBuckets = $taxonAggregation['codes']['buckets'] ?? []; - foreach ($taxonCodeBuckets as $taxonCodeBucket) { - if (0 === $taxonCodeBucket['doc_count']) { - continue; - } - $taxonCode = $taxonCodeBucket['key']; - $taxonName = null; - - // Get taxon level in aggregation - $taxonLevelBuckets = $taxonCodeBucket['levels']['buckets'] ?? []; - foreach ($taxonLevelBuckets as $taxonLevelBucket) { - $level = $taxonLevelBucket['key']; - if ($level === ($currentTaxonLevel + 1) && (!$taxon || isset($childrenTaxon[$taxonCode]))) { - // Get taxon name in aggregation - $taxonNameBuckets = $taxonLevelBucket['names']['buckets'] ?? []; - foreach ($taxonNameBuckets as $taxonNameBucket) { - $taxonName = $taxonNameBucket['key']; - $filter->addValue($taxonName ?? $taxonCode, $taxonCodeBucket['doc_count']); - break 2; - } - } - } - } - - // Put taxon filter in first if contains value - if (\count($filter->getValues())) { - $this->taxonFilter = $filter; - } - } - } - - /** - * Add main taxon filter depending on aggregations. - * - * @param array $aggregations - * @param TaxonInterface|null $taxon - */ - protected function addMainTaxonFilter(array $aggregations, ?TaxonInterface $taxon): void - { - $taxonAggregation = $aggregations['mainTaxon'] ?? null; - if ($taxonAggregation && $taxonAggregation['doc_count'] > 0) { - $filter = new Filter('main_taxon', 'monsieurbiz_searchplugin.filters.taxon_filter', $taxonAggregation['doc_count']); - - // Get main taxon code in aggregation - $taxonCodeBuckets = $taxonAggregation['codes']['buckets'] ?? []; - foreach ($taxonCodeBuckets as $taxonCodeBucket) { - if (0 === $taxonCodeBucket['doc_count']) { - continue; - } - $taxonCode = $taxonCodeBucket['key']; - $taxonName = null; - - // Get main taxon level in aggregation - $taxonLevelBuckets = $taxonCodeBucket['levels']['buckets'] ?? []; - foreach ($taxonLevelBuckets as $taxonLevelBucket) { - // Get main taxon name in aggregation - $taxonNameBuckets = $taxonLevelBucket['names']['buckets'] ?? []; - foreach ($taxonNameBuckets as $taxonNameBucket) { - $taxonName = $taxonNameBucket['key']; - $filter->addValue($taxonName ?? $taxonCode, $taxonCodeBucket['doc_count']); - break 2; - } - } - } - - // Put taxon filter in first if contains value - if (\count($filter->getValues())) { - $this->mainTaxonFilter = $filter; - } - } - } - - /** - * Add price filter depending on aggregations. - * - * @param array $aggregations - */ - protected function addPriceFilter(array $aggregations): void - { - $priceAggregation = $aggregations['price'] ?? null; - if ($priceAggregation && $priceAggregation['doc_count'] > 0) { - $this->priceFilter = new RangeFilter( - 'price', - 'monsieurbiz_searchplugin.filters.price_filter', - 'monsieurbiz_searchplugin.filters.price_min', - 'monsieurbiz_searchplugin.filters.price_max', - (int) floor(($priceAggregation['values']['min'] ?? 0) / 100), - (int) ceil(($priceAggregation['values']['max'] ?? 0) / 100) - ); - } - } -} diff --git a/src/Model/Documentable/Documentable.php b/src/Model/Documentable/Documentable.php new file mode 100644 index 00000000..cf2484a3 --- /dev/null +++ b/src/Model/Documentable/Documentable.php @@ -0,0 +1,86 @@ + + * + * 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\Model\Documentable; + +use Sylius\Component\Resource\Model\TranslatableInterface; + +class Documentable implements DocumentableInterface +{ + use DocumentableDatasourceTrait; + + use DocumentableMappingProviderTrait; + + private string $indexCode; + + private string $sourceClass; + + private string $targetClass; + + /** + * @var array + */ + private array $templates; + + private array $limits; + + public function __construct( + string $indexCode, + string $sourceClass, + string $targetClass, + array $templates, + array $limits + ) { + $this->indexCode = $indexCode; + $this->sourceClass = $sourceClass; + $this->targetClass = $targetClass; + $this->templates = $templates; + $this->limits = $limits; + } + + public function getIndexCode(): string + { + return $this->indexCode; + } + + public function getSourceClass(): string + { + return $this->sourceClass; + } + + public function getTargetClass(): string + { + return $this->targetClass; + } + + public function isTranslatable(): bool + { + $interface = (array) (class_implements($this->getSourceClass()) ?? []); + + return \in_array(TranslatableInterface::class, $interface, true); + } + + public function getTemplate(string $type): ?string + { + return $this->templates[$type] ?? null; + } + + public function getLimits(?string $queryType = null): array + { + if (null == $queryType) { + return $this->limits; + } + + return $this->limits[$queryType] ?? []; + } +} diff --git a/src/Model/Documentable/DocumentableDatasourceTrait.php b/src/Model/Documentable/DocumentableDatasourceTrait.php new file mode 100644 index 00000000..2a90842c --- /dev/null +++ b/src/Model/Documentable/DocumentableDatasourceTrait.php @@ -0,0 +1,31 @@ + + * + * 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\Model\Documentable; + +use MonsieurBiz\SyliusSearchPlugin\Model\Datasource\DatasourceInterface; + +trait DocumentableDatasourceTrait +{ + protected DatasourceInterface $datasource; + + public function setDatasource(DatasourceInterface $datasource): void + { + $this->datasource = $datasource; + } + + public function getDatasource(): DatasourceInterface + { + return $this->datasource; + } +} diff --git a/src/Model/Documentable/DocumentableInterface.php b/src/Model/Documentable/DocumentableInterface.php index 4824d2ce..fc17d217 100644 --- a/src/Model/Documentable/DocumentableInterface.php +++ b/src/Model/Documentable/DocumentableInterface.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -13,11 +13,29 @@ namespace MonsieurBiz\SyliusSearchPlugin\Model\Documentable; -use MonsieurBiz\SyliusSearchPlugin\Model\Document\ResultInterface; +use JoliCode\Elastically\Mapping\MappingProviderInterface; +use MonsieurBiz\SyliusSearchPlugin\Model\Datasource\DatasourceInterface; interface DocumentableInterface { - public function getDocumentType(): string; + public function getIndexCode(): string; - public function convertToDocument(string $locale): ResultInterface; + // TODO move it in CustomMappingProviderInterface + public function setMappingProvider(MappingProviderInterface $mapping): void; + + public function getMappingProvider(): MappingProviderInterface; + + public function getSourceClass(): string; + + public function getTargetClass(): string; + + public function setDatasource(DatasourceInterface $datasource): void; + + public function getDatasource(): DatasourceInterface; + + public function isTranslatable(): bool; + + public function getTemplate(string $type): ?string; + + public function getLimits(?string $queryType): array; } diff --git a/src/Model/Documentable/DocumentableMappingProviderTrait.php b/src/Model/Documentable/DocumentableMappingProviderTrait.php new file mode 100644 index 00000000..55b85d2b --- /dev/null +++ b/src/Model/Documentable/DocumentableMappingProviderTrait.php @@ -0,0 +1,31 @@ + + * + * 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\Model\Documentable; + +use JoliCode\Elastically\Mapping\MappingProviderInterface; + +trait DocumentableMappingProviderTrait +{ + protected MappingProviderInterface $mappingProvider; + + public function setMappingProvider(MappingProviderInterface $mapping): void + { + $this->mappingProvider = $mapping; + } + + public function getMappingProvider(): MappingProviderInterface + { + return $this->mappingProvider; + } +} diff --git a/src/Model/Documentable/DocumentableProductTrait.php b/src/Model/Documentable/DocumentableProductTrait.php index 9cead518..b73abef9 100644 --- a/src/Model/Documentable/DocumentableProductTrait.php +++ b/src/Model/Documentable/DocumentableProductTrait.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -21,33 +21,22 @@ use Sylius\Component\Core\Model\Image; use Sylius\Component\Core\Model\ProductTaxonInterface; use Sylius\Component\Core\Model\ProductVariant; +use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Currency\Model\CurrencyInterface; -use Sylius\Component\Core\Model\ProductVariantInterface; trait DocumentableProductTrait { - /** - * @return string - */ public function getDocumentType(): string { return 'product'; } - /** - * @return ResultInterface - */ public function createResult(): ResultInterface { return new Result(); } - /** - * @param string $locale - * - * @return ResultInterface - */ public function convertToDocument(string $locale): ResultInterface { $document = $this->createResult(); @@ -75,11 +64,6 @@ public function convertToDocument(string $locale): ResultInterface return $this->addOptionsInDocument($document, $locale); } - /** - * @param ResultInterface $document - * - * @return ResultInterface - */ protected function addImagesInDocument(ResultInterface $document): ResultInterface { /** @var Image $image */ @@ -90,11 +74,6 @@ protected function addImagesInDocument(ResultInterface $document): ResultInterfa return $document; } - /** - * @param ResultInterface $document - * - * @return ResultInterface - */ protected function addChannelsInDocument(ResultInterface $document): ResultInterface { /** @var Channel $channel */ @@ -105,11 +84,6 @@ protected function addChannelsInDocument(ResultInterface $document): ResultInter return $document; } - /** - * @param ResultInterface $document - * - * @return ResultInterface - */ protected function addPricesInDocument(ResultInterface $document): ResultInterface { /** @var Channel $channel */ @@ -131,12 +105,6 @@ protected function addPricesInDocument(ResultInterface $document): ResultInterfa return $document; } - /** - * @param ResultInterface $document - * @param string $locale - * - * @return ResultInterface - */ protected function addTaxonsInDocument(ResultInterface $document, string $locale): ResultInterface { /** @var TaxonInterface $mainTaxon */ @@ -165,12 +133,6 @@ protected function addTaxonsInDocument(ResultInterface $document, string $locale return $document; } - /** - * @param ResultInterface $document - * @param string $locale - * - * @return ResultInterface - */ protected function addAttributesInDocument(ResultInterface $document, string $locale): ResultInterface { /** @var AttributeValueInterface $attribute */ @@ -191,7 +153,6 @@ protected function addAttributesInDocument(ResultInterface $document, string $lo /** * @param Result $document - * @param string $locale * * @return Result */ @@ -220,8 +181,6 @@ protected function addOptionsInDocument(ResultInterface $document, string $local /** * @param $channel - * - * @return null */ private function getCheapestVariantForChannel($channel) { @@ -248,6 +207,7 @@ private function getProductHasVariantInStock() return true; } } + return false; } } diff --git a/src/Model/Product/FilterableTrait.php b/src/Model/Product/FilterableTrait.php deleted file mode 100644 index 2f1d51c1..00000000 --- a/src/Model/Product/FilterableTrait.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Model\Product; - -use Doctrine\ORM\Mapping as ORM; - -trait FilterableTrait -{ - /** - * @var bool - * @ORM\Column(name="filterable", type="boolean", nullable=false, options={"default"=true}) - */ - protected $filterable = true; - - /** - * @return bool - */ - public function isFilterable(): bool - { - return $this->filterable; - } - - /** - * @param bool $filterable - */ - public function setFilterable(bool $filterable): void - { - $this->filterable = $filterable; - } -} diff --git a/src/Exception/MissingParamException.php b/src/Model/Product/ProductDTO.php similarity index 68% rename from src/Exception/MissingParamException.php rename to src/Model/Product/ProductDTO.php index f0126db3..a5bad0ef 100644 --- a/src/Exception/MissingParamException.php +++ b/src/Model/Product/ProductDTO.php @@ -5,16 +5,16 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * 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\Exception; +namespace MonsieurBiz\SyliusSearchPlugin\Model\Product; -use Exception; +use Jacquesbh\Eater\Eater; -class MissingParamException extends Exception +class ProductDTO extends Eater { } diff --git a/src/Model/Product/SearchableTrait.php b/src/Model/Product/SearchableTrait.php new file mode 100644 index 00000000..2cd919dd --- /dev/null +++ b/src/Model/Product/SearchableTrait.php @@ -0,0 +1,64 @@ + + * + * 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\Model\Product; + +use Doctrine\ORM\Mapping as ORM; + +trait SearchableTrait +{ + /** + * @ORM\Column(name="searchable", type="boolean", nullable=false, options={"default"=true}) + */ + protected bool $searchable = true; + + /** + * @ORM\Column(name="filterable", type="boolean", nullable=false, options={"default"=false}) + */ + protected bool $filterable = false; + + /** + * @ORM\Column(name="search_weight", type="smallint", nullable=false, options={"default"=1, "unsigned"=true}) + */ + protected int $searchWeight = 1; + + public function isSearchable(): bool + { + return $this->searchable; + } + + public function setSearchable(bool $searchable): void + { + $this->searchable = $searchable; + } + + public function isFilterable(): bool + { + return $this->filterable; + } + + public function setFilterable(bool $filterable): void + { + $this->filterable = $filterable; + } + + public function getSearchWeight(): int + { + return $this->searchWeight; + } + + public function setSearchWeight(int $searchWeight): void + { + $this->searchWeight = $searchWeight; + } +} diff --git a/src/Model/Product/VariantDTO.php b/src/Model/Product/VariantDTO.php new file mode 100644 index 00000000..9231f3a0 --- /dev/null +++ b/src/Model/Product/VariantDTO.php @@ -0,0 +1,49 @@ + + * + * 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\Model\Product; + +use Jacquesbh\Eater\Eater; + +final class VariantDTO extends Eater +{ + public function getCode(): ?string + { + return $this->getData('code'); + } + + public function setCode(string $code): void + { + $this->setData('code', $code); + } + + public function isEnabled(): bool + { + return (bool) $this->getData('enabled'); + } + + public function setEnabled(bool $enabled): void + { + $this->setData('enabled', $enabled); + } + + public function isInStock(): bool + { + return (bool) $this->getData('is_in_stock'); + } + + public function setIsInStock(bool $isInStock): void + { + $this->setData('is_in_stock', $isInStock); + } +} diff --git a/src/MonsieurBizSyliusSearchPlugin.php b/src/MonsieurBizSyliusSearchPlugin.php index c7293b4f..d72df69e 100644 --- a/src/MonsieurBizSyliusSearchPlugin.php +++ b/src/MonsieurBizSyliusSearchPlugin.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -13,10 +13,41 @@ namespace MonsieurBiz\SyliusSearchPlugin; +use MonsieurBiz\SyliusSearchPlugin\DependencyInjection\AutomapperConfigurationRegistryPass; +use MonsieurBiz\SyliusSearchPlugin\DependencyInjection\AutowireMappingProviderParameterPass; +use MonsieurBiz\SyliusSearchPlugin\DependencyInjection\DocumentableRegistryPass; +use MonsieurBiz\SyliusSearchPlugin\DependencyInjection\RegisterSearchRequestPass; use Sylius\Bundle\CoreBundle\Application\SyliusPluginTrait; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\HttpKernel\Bundle\Bundle; final class MonsieurBizSyliusSearchPlugin extends Bundle { use SyliusPluginTrait; + + public function getContainerExtension(): ?ExtensionInterface + { + if (null === $this->containerExtension) { + $this->containerExtension = false; + $extension = $this->createContainerExtension(); + if (null !== $extension) { + $this->containerExtension = $extension; + } + } + + return $this->containerExtension instanceof ExtensionInterface + ? $this->containerExtension + : null; + } + + public function build(ContainerBuilder $container): void + { + parent::build($container); + $container->addCompilerPass(new DocumentableRegistryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 50); // Run the compiler pass before \MonsieurBiz\SyliusSettingsPlugin\DependencyInjection\InstantiateSettingsPass + $container->addCompilerPass(new RegisterSearchRequestPass()); + $container->addCompilerPass(new AutomapperConfigurationRegistryPass()); + $container->addCompilerPass(new AutowireMappingProviderParameterPass()); + } } diff --git a/src/Normalizer/Product/ProductDTONormalizer.php b/src/Normalizer/Product/ProductDTONormalizer.php new file mode 100644 index 00000000..479379d7 --- /dev/null +++ b/src/Normalizer/Product/ProductDTONormalizer.php @@ -0,0 +1,134 @@ + + * + * 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\Normalizer\Product; + +use Jacquesbh\Eater\EaterInterface; +use MonsieurBiz\SyliusSearchPlugin\AutoMapper\Configuration; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + +final class ProductDTONormalizer extends ObjectNormalizer implements DenormalizerInterface, NormalizerInterface, DenormalizerAwareInterface, NormalizerAwareInterface +{ + use DenormalizerAwareTrait; + + use NormalizerAwareTrait; + + private Configuration $automapperConfiguration; + + public function __construct( + Configuration $automapperConfiguration, + ClassMetadataFactoryInterface $classMetadataFactory = null, + NameConverterInterface $nameConverter = null, + PropertyAccessorInterface $propertyAccessor = null, + PropertyTypeExtractorInterface $propertyTypeExtractor = null, + ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, + callable $objectClassResolver = null, + array $defaultContext = [] + ) { + parent::__construct( + $classMetadataFactory, + $nameConverter, + $propertyAccessor, + $propertyTypeExtractor, + $classDiscriminatorResolver, + $objectClassResolver, + $defaultContext + ); + $this->automapperConfiguration = $automapperConfiguration; + } + + public function denormalize($data, string $type, string $format = null, array $context = []) + { + /** @var EaterInterface $object */ + $object = parent::denormalize($data, $type, $format, $context); + + if (\array_key_exists('main_taxon', $data)) { + $taxonDTOClass = $this->automapperConfiguration->getTargetClass('taxon'); + $object->setData('main_taxon', $this->denormalizer->denormalize($data['main_taxon'], $taxonDTOClass, 'json', $context)); + unset($data['main_taxon']); + } + + if (\array_key_exists('product_taxons', $data)) { + $values = []; + $productTaxonDTOClass = $this->automapperConfiguration->getTargetClass('product_taxon'); + foreach ($data['product_taxons'] as $value) { + $values[] = $this->denormalizer->denormalize($value, $productTaxonDTOClass, 'json', $context); + } + $object->setData('product_taxons', $values); + unset($data['product_taxons']); + } + + if (\array_key_exists('images', $data) && null !== $data['images']) { + $values = []; + $imageDTOClass = $this->automapperConfiguration->getTargetClass('image'); + foreach ($data['images'] as $value) { + $values[] = $this->denormalizer->denormalize($value, $imageDTOClass, 'json', $context); + } + $object->setData('images', $values); + unset($data['product_taxons']); + } + + if (\array_key_exists('channels', $data)) { + $values = []; + $channelDTOClass = $this->automapperConfiguration->getTargetClass('channel'); + foreach ($data['channels'] as $value) { + $values[] = $this->denormalizer->denormalize($value, $channelDTOClass, 'json', $context); + } + $object->setData('channels', $values); + unset($data['channels']); + } + + if (\array_key_exists('attributes', $data)) { + $values = []; + $productAttributeDTOClass = $this->automapperConfiguration->getTargetClass('product_attribute'); + foreach ($data['attributes'] as $key => $value) { + $values[$key] = $this->denormalizer->denormalize($value, $productAttributeDTOClass, 'json', $context); + } + $object->setData('attributes', $values); + unset($data['channels']); + } + + if (\array_key_exists('prices', $data)) { + $values = []; + $pricingDTOClass = $this->automapperConfiguration->getTargetClass('pricing'); + foreach ($data['prices'] as $key => $value) { + $values[$key] = $this->denormalizer->denormalize($value, $pricingDTOClass, 'json', $context); + } + $object->setData('prices', $values); + unset($data['channels']); + } + + return $object; + } + + public function supportsDenormalization($data, string $type, string $format = null): bool + { + return $this->automapperConfiguration->getTargetClass('product') === $type; + } + + public function supportsNormalization($data, string $format = null): bool + { + return false; + } +} diff --git a/src/Provider/DocumentRepositoryProvider.php b/src/Provider/DocumentRepositoryProvider.php deleted file mode 100644 index 4e5f5b60..00000000 --- a/src/Provider/DocumentRepositoryProvider.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Provider; - -use Doctrine\ORM\EntityManagerInterface; - -class DocumentRepositoryProvider -{ - /** @var EntityManagerInterface */ - private $entityManager; - - /** @var string */ - private $documentableClasses; - - /** - * SearchQueryProvider constructor. - * - * @param EntityManagerInterface $entityManager - * @param array $documentableClasses - */ - public function __construct(EntityManagerInterface $entityManager, array $documentableClasses) - { - $this->entityManager = $entityManager; - $this->documentableClasses = $documentableClasses; - } - - public function getRepositories() - { - $repositories = []; - foreach ($this->documentableClasses as $class) { - $repositories[] = $this->entityManager->getRepository($class); - } - - return $repositories; - } -} diff --git a/src/Provider/SearchQueryProvider.php b/src/Provider/SearchQueryProvider.php deleted file mode 100644 index 71094673..00000000 --- a/src/Provider/SearchQueryProvider.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Provider; - -use MonsieurBiz\SyliusSearchPlugin\Exception\MissingConfigFileException; -use MonsieurBiz\SyliusSearchPlugin\Exception\ReadFileException; -use MonsieurBiz\SyliusSearchPlugin\Model\Config\FilesConfig; - -class SearchQueryProvider -{ - /** @var FilesConfig */ - private $filesConfig; - - /** - * SearchQueryProvider constructor. - * - * @param array $files - * - * @throws MissingConfigFileException - */ - public function __construct(array $files) - { - $this->filesConfig = new FilesConfig($files); - } - - /** - * Get search query. - * - * @throws ReadFileException - * - * @return string - */ - public function getSearchQuery() - { - return $this->getQuery($this->filesConfig->getSearchPath()); - } - - /** - * Get instant query. - * - * @throws ReadFileException - * - * @return false|string - */ - public function getInstantQuery() - { - return $this->getQuery($this->filesConfig->getInstantPath()); - } - - /** - * Get taxon query. - * - * @throws ReadFileException - * - * @return false|string - */ - public function getTaxonQuery() - { - return $this->getQuery($this->filesConfig->getTaxonPath()); - } - - /** - * Get content from file. - * - * @param $path - * - * @throws ReadFileException - * - * @return false|string - */ - private function getQuery($path) - { - $query = @file_get_contents($path); - if (false === $query) { - throw new ReadFileException(sprintf('Error while opening file "%s".', $path)); - } - - return $query; - } -} diff --git a/src/Provider/UrlParamsProvider.php b/src/Provider/UrlParamsProvider.php deleted file mode 100644 index 1df1d80c..00000000 --- a/src/Provider/UrlParamsProvider.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Provider; - -class UrlParamsProvider -{ - /** @var string */ - private $path; - - /** @var array */ - private $params; - - /** - * UrlParamsProvider constructor. - * - * @param string $path - * @param array $params - */ - public function __construct(string $path, array $params) - { - $this->path = $path; - $this->params = $params; - } - - /** - * @return string - */ - public function getPath(): string - { - return $this->path; - } - - /** - * @param string $path - */ - public function setPath(string $path): void - { - $this->path = $path; - } - - /** - * @return array - */ - public function getParams(): array - { - return $this->params; - } - - /** - * @param array $params - */ - public function setParams(array $params): void - { - $this->params = $params; - } -} diff --git a/src/Repository/ProductAttributeRepository.php b/src/Repository/ProductAttributeRepository.php new file mode 100644 index 00000000..814bbabb --- /dev/null +++ b/src/Repository/ProductAttributeRepository.php @@ -0,0 +1,37 @@ + + * + * 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\Repository; + +use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; + +class ProductAttributeRepository implements ProductAttributeRepositoryInterface +{ + private EntityRepository $attributeRepository; + + public function __construct(EntityRepository $attributeRepository) + { + $this->attributeRepository = $attributeRepository; + } + + public function findIsSearchableOrFilterable(): array + { + return $this->attributeRepository->createQueryBuilder('o') + ->innerJoin('o.translations', 'translation') + ->andWhere('o.searchable = true') + ->orWhere('o.filterable = true') + ->getQuery() + ->getResult() + ; + } +} diff --git a/src/Repository/ProductAttributeRepositoryInterface.php b/src/Repository/ProductAttributeRepositoryInterface.php new file mode 100644 index 00000000..79477992 --- /dev/null +++ b/src/Repository/ProductAttributeRepositoryInterface.php @@ -0,0 +1,25 @@ + + * + * 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\Repository; + +use MonsieurBiz\SyliusSearchPlugin\Entity\Product\SearchableInterface; +use Sylius\Component\Product\Model\ProductAttributeInterface; + +interface ProductAttributeRepositoryInterface +{ + /** + * @return ProductAttributeInterface[]&SearchableInterface[] + */ + public function findIsSearchableOrFilterable(): array; +} diff --git a/src/Repository/ProductOptionRepository.php b/src/Repository/ProductOptionRepository.php new file mode 100644 index 00000000..07aa25e5 --- /dev/null +++ b/src/Repository/ProductOptionRepository.php @@ -0,0 +1,37 @@ + + * + * 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\Repository; + +use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; + +class ProductOptionRepository implements ProductOptionRepositoryInterface +{ + private EntityRepository $productOptionRepository; + + public function __construct(EntityRepository $productOptionRepository) + { + $this->productOptionRepository = $productOptionRepository; + } + + public function findIsSearchableOrFilterable(): array + { + return $this->productOptionRepository->createQueryBuilder('o') + ->innerJoin('o.translations', 'translation') + ->andWhere('o.searchable = true') + ->orWhere('o.filterable = true') + ->getQuery() + ->getResult() + ; + } +} diff --git a/src/Repository/ProductOptionRepositoryInterface.php b/src/Repository/ProductOptionRepositoryInterface.php new file mode 100644 index 00000000..62ead329 --- /dev/null +++ b/src/Repository/ProductOptionRepositoryInterface.php @@ -0,0 +1,25 @@ + + * + * 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\Repository; + +use MonsieurBiz\SyliusSearchPlugin\Entity\Product\SearchableInterface; +use Sylius\Component\Product\Model\ProductOptionInterface; + +interface ProductOptionRepositoryInterface +{ + /** + * @return ProductOptionInterface[]&SearchableInterface[] + */ + public function findIsSearchableOrFilterable(): array; +} diff --git a/src/Resolver/CheapestProductVariantResolver.php b/src/Resolver/CheapestProductVariantResolver.php new file mode 100644 index 00000000..e1c41021 --- /dev/null +++ b/src/Resolver/CheapestProductVariantResolver.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 MonsieurBiz\SyliusSearchPlugin\Resolver; + +use Sylius\Component\Channel\Context\ChannelContextInterface; +use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\ProductVariantInterface as ModelProductVariantInterface; +use Sylius\Component\Product\Model\ProductInterface; +use Sylius\Component\Product\Model\ProductVariantInterface; +use Sylius\Component\Product\Resolver\ProductVariantResolverInterface; + +class CheapestProductVariantResolver implements ProductVariantResolverInterface +{ + private ChannelContextInterface $channelContext; + + public function __construct(ChannelContextInterface $channelContext) + { + $this->channelContext = $channelContext; + } + + public function getVariant(ProductInterface $subject): ?ProductVariantInterface + { + $channel = $this->channelContext->getChannel(); + if ($subject->getEnabledVariants()->isEmpty() || !$channel instanceof ChannelInterface) { + return null; + } + + $cheapestVariant = null; + $cheapestPrice = null; + $variants = $subject->getEnabledVariants(); + foreach ($variants as $variant) { + if (!$variant instanceof ModelProductVariantInterface) { + continue; + } + if (null === ($channelPrice = $variant->getChannelPricingForChannel($channel))) { + continue; + } + if (null === $cheapestPrice || $channelPrice->getPrice() < $cheapestPrice) { + $cheapestPrice = $channelPrice->getPrice(); + $cheapestVariant = $variant; + } + } + + return $cheapestVariant; + } +} diff --git a/src/Resources/config/config.yaml b/src/Resources/config/config.yaml index ca0f321e..e128d434 100644 --- a/src/Resources/config/config.yaml +++ b/src/Resources/config/config.yaml @@ -1,4 +1,6 @@ imports: - - { resource: "twig.yaml" } - - { resource: "sylius.yaml" } - { resource: "services.yaml" } + - { resource: "monsieurbiz_search.yaml" } + - { resource: "sylius/ui.yaml" } + - { resource: "monsieurbiz/settings.yaml" } + - { resource: "messenger.yaml" } diff --git a/src/Resources/config/elasticsearch/mappings/analyzers.yaml b/src/Resources/config/elasticsearch/analyzers.yaml similarity index 100% rename from src/Resources/config/elasticsearch/mappings/analyzers.yaml rename to src/Resources/config/elasticsearch/analyzers.yaml diff --git a/src/Resources/config/elasticsearch/analyzers_en.yaml b/src/Resources/config/elasticsearch/analyzers_en.yaml new file mode 100644 index 00000000..1c8efaf0 --- /dev/null +++ b/src/Resources/config/elasticsearch/analyzers_en.yaml @@ -0,0 +1,7 @@ +filter: + stemmer: + type: stemmer + language: english +analyzer: + search_standard: + filter: [ 'stemmer' ] diff --git a/src/Resources/config/elasticsearch/analyzers_fr.yaml b/src/Resources/config/elasticsearch/analyzers_fr.yaml new file mode 100644 index 00000000..4209d640 --- /dev/null +++ b/src/Resources/config/elasticsearch/analyzers_fr.yaml @@ -0,0 +1,7 @@ +filter: + stemmer: + type: stemmer + language: french +analyzer: + search_standard: + filter: [ 'stemmer' ] diff --git a/src/Resources/config/elasticsearch/mappings/documents-es_mapping.yaml b/src/Resources/config/elasticsearch/documents-es_mapping.yaml similarity index 100% rename from src/Resources/config/elasticsearch/mappings/documents-es_mapping.yaml rename to src/Resources/config/elasticsearch/documents-es_mapping.yaml diff --git a/src/Resources/config/elasticsearch/monsieurbiz_product_mapping.yaml b/src/Resources/config/elasticsearch/monsieurbiz_product_mapping.yaml new file mode 100644 index 00000000..0162f285 --- /dev/null +++ b/src/Resources/config/elasticsearch/monsieurbiz_product_mapping.yaml @@ -0,0 +1,79 @@ +mappings: + dynamic: false + properties: + # attributes mapping is managed dynamically + # options mapping is managed dynamically + code: + type: keyword + enabled: + type: boolean + channels: + type: nested + properties: + code: + type: keyword + name: + type: text + fields: + keyword: + type: keyword + autocomplete: + type: text + analyzer: search_autocomplete + search_analyzer: standard + created_at: + type: date + description: + type: text + images: + type: nested + properties: + path: + type: keyword + main_taxon: + type: nested + properties: + code: + type: keyword + name: + type: keyword + position: + type: integer + level: + type: integer + product_taxons: + type: nested + properties: + taxon: + type: nested + properties: + code: + type: keyword + name: + type: keyword + position: + type: integer + level: + type: integer + position: + type: integer + prices: + type: nested + properties: + channel_code: + type: keyword + price: + type: integer + original_price: + type: integer + is_price_reduced: + type: boolean + variants: + type: nested + properties: + code: + type: keyword + enabled: + type: boolean + is_in_stock: + type: boolean diff --git a/src/Resources/config/jane/dto-config.php b/src/Resources/config/jane/jane-configuration.php similarity index 60% rename from src/Resources/config/jane/dto-config.php rename to src/Resources/config/jane/jane-configuration.php index ecd1112c..1f0deb4d 100644 --- a/src/Resources/config/jane/dto-config.php +++ b/src/Resources/config/jane/jane-configuration.php @@ -5,16 +5,15 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ declare(strict_types=1); return [ - 'json-schema-file' => 'src/Resources/config/jane/document.json', + 'json-schema-file' => __DIR__ . '/json-schema.json', 'root-class' => 'Model', - 'namespace' => 'MonsieurBiz\SyliusSearchPlugin\generated', - 'directory' => 'src/generated', - 'strict' => false, + 'namespace' => 'MonsieurBiz\SyliusSearchPlugin\Generated', + 'directory' => __DIR__ . '/../../../../generated', ]; diff --git a/src/Resources/config/jane/json-schema.json b/src/Resources/config/jane/json-schema.json new file mode 100644 index 00000000..dc30a8c9 --- /dev/null +++ b/src/Resources/config/jane/json-schema.json @@ -0,0 +1,86 @@ +{ + "$schema": "http://json-schema.org/2019-09/schema#", + "definitions": { + "ImageDTO": { + "type": "object", + "properties": { + "path": { + "type": [ + "null", + "string" + ] + } + } + }, + "ChannelDTO": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + } + }, + "ProductTaxonDTO": { + "type": "object", + "properties": { + "taxon": { + "$ref": "#/definitions/TaxonDTO" + }, + "position": { + "type": [ + "null", + "integer" + ] + } + } + }, + "TaxonDTO": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "code": { + "type": "string" + }, + "position": { + "type": "integer" + }, + "level": { + "type": "integer" + } + } + }, + "ProductAttributeDTO": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "value": { + "type": ["null", "mixed"] + } + } + }, + "PricingDTO": { + "type": "object", + "properties": { + "channel_code": { + "type": "string" + }, + "price": { + "type": ["null", "integer"] + }, + "original_price": { + "type": ["null", "integer"] + }, + "price_reduced": { + "type": ["boolean"] + } + } + } + } +} diff --git a/src/Resources/config/messenger.yaml b/src/Resources/config/messenger.yaml new file mode 100644 index 00000000..b82225f4 --- /dev/null +++ b/src/Resources/config/messenger.yaml @@ -0,0 +1,11 @@ +framework: + messenger: + transports: + async_search: + dsn: '%env(MONSIEURBIZ_SEARCHPLUGIN_MESSENGER_TRANSPORT_DSN)%' + options: + queue_name: 'monsieurbiz_search' + + routing: + MonsieurBiz\SyliusSearchPlugin\Message\ProductReindexFromTaxon: async_search + MonsieurBiz\SyliusSearchPlugin\Message\ProductReindexFromIds: async_search diff --git a/src/Resources/config/monsieurbiz/settings.yaml b/src/Resources/config/monsieurbiz/settings.yaml new file mode 100644 index 00000000..d8397f61 --- /dev/null +++ b/src/Resources/config/monsieurbiz/settings.yaml @@ -0,0 +1,12 @@ +monsieurbiz_sylius_settings: + plugins: + monsieurbiz.search: + vendor_name: Monsieur Biz + vendor_url: + plugin_name: Search + description: Search configuration + icon: search + use_locales: false + classes: + form: MonsieurBiz\SyliusSearchPlugin\Form\Type\Settings\SettingsSearchType + default_values: # default values are defined in MonsieurBiz\SyliusSearchPlugin\DependencyInjection\DocumentableRegistryPass diff --git a/src/Resources/config/monsieurbiz_search.yaml b/src/Resources/config/monsieurbiz_search.yaml new file mode 100644 index 00000000..398c49a1 --- /dev/null +++ b/src/Resources/config/monsieurbiz_search.yaml @@ -0,0 +1,31 @@ +monsieurbiz_sylius_search: + documents: + monsieurbiz_product: + #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\ProductInterface' + target: 'MonsieurBiz\SyliusSearchPlugin\Model\Product\ProductDTO' + templates: + item: '@MonsieurBizSyliusSearchPlugin/Search/product/_box.html.twig' + instant: '@MonsieurBizSyliusSearchPlugin/Instant/Product/_box.html.twig' + #mapping_provider: '...' # by default monsieurbiz.search.mapper_provider + #dataprovider: '...' # by default MonsieurBiz\SyliusSearchPlugin\Model\Datasource\RepositoryDatasource + automapper_classes: + sources: + product: '%sylius.model.product.class%' + product_variant: '%sylius.model.product_variant.class%' + product_attribute_value: '%sylius.model.product_attribute_value.class%' + targets: + product: 'MonsieurBiz\SyliusSearchPlugin\Model\Product\ProductDTO' + image: 'MonsieurBiz\SyliusSearchPlugin\Generated\Model\ImageDTO' + taxon: 'MonsieurBiz\SyliusSearchPlugin\Generated\Model\TaxonDTO' + product_taxon: 'MonsieurBiz\SyliusSearchPlugin\Generated\Model\ProductTaxonDTO' + channel: 'MonsieurBiz\SyliusSearchPlugin\Generated\Model\ChannelDTO' + product_attribute: 'MonsieurBiz\SyliusSearchPlugin\Generated\Model\ProductAttributeDTO' + product_variant: 'MonsieurBiz\SyliusSearchPlugin\Model\Product\VariantDTO' + pricing: 'MonsieurBiz\SyliusSearchPlugin\Generated\Model\PricingDTO' + diff --git a/src/Resources/config/routing.yaml b/src/Resources/config/routing.yaml index f2a3d58b..03d57fd0 100644 --- a/src/Resources/config/routing.yaml +++ b/src/Resources/config/routing.yaml @@ -1,5 +1,5 @@ -monsieurbiz_sylius_search_shop: - resource: "routing/shop.yaml" +monsieurbiz_search_shop: + resource: "@MonsieurBizSyliusSearchPlugin/Resources/config/routing/shop.yaml" prefix: /{_locale} requirements: - _locale: ^[a-z]{2}(?:_[A-Z]{2})?$ + _locale: "^[A-Za-z]{2,4}(_([A-Za-z]{4}|[0-9]{3}))?(_([A-Za-z]{2}|[0-9]{3}))?$" diff --git a/src/Resources/config/routing/shop.yaml b/src/Resources/config/routing/shop.yaml index 7d645600..10b90786 100644 --- a/src/Resources/config/routing/shop.yaml +++ b/src/Resources/config/routing/shop.yaml @@ -1,27 +1,29 @@ -monsieurbiz_sylius_search_search: - path: /search/{query} - methods: [GET] - defaults: - _controller: MonsieurBiz\SyliusSearchPlugin\Controller\SearchController:searchAction - requirements: - query: .+ +monsieurbiz_search_search: + path: /search/{query} + methods: [GET] + defaults: + _controller: MonsieurBiz\SyliusSearchPlugin\Controller\SearchController:searchAction + requirements: + query: .+ -monsieurbiz_sylius_search_post: - path: /search - methods: [POST] - defaults: - _controller: MonsieurBiz\SyliusSearchPlugin\Controller\SearchController:postAction +monsieurbiz_search_post: + path: /search + methods: [POST] + defaults: + _controller: MonsieurBiz\SyliusSearchPlugin\Controller\SearchController:postAction -monsieurbiz_sylius_search_instant: - path: /instant - methods: [POST] - defaults: - _controller: MonsieurBiz\SyliusSearchPlugin\Controller\SearchController:instantAction +monsieurbiz_search_instant: + path: /instant + methods: [POST] + defaults: + _controller: MonsieurBiz\SyliusSearchPlugin\Controller\SearchController:instantAction monsieurbiz_sylius_search_taxon: - path: /taxons/{slug} - methods: [GET] - defaults: - _controller: MonsieurBiz\SyliusSearchPlugin\Controller\SearchController:taxonAction - requirements: - slug: .+ + path: /taxons/{slug} + methods: [GET] + defaults: + _controller: MonsieurBiz\SyliusSearchPlugin\Controller\SearchController:taxonAction + _sylius: + taxon: "expr:notFoundOnNull(service('sylius.repository.taxon').findOneBySlug($slug, service('sylius.context.locale').getLocaleCode()))" + requirements: + slug: .+ diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 81895441..045101df 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -1,88 +1,207 @@ +parameters: + monsieurbiz.search.model.documentable.interface: MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface + monsieurbiz.search.request.interface: MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface + monsieurbiz.search.product_attribute_analyzer: 'search_standard' + monsieurbiz.search.product.enable_stock_filter: false + monsieurbiz.search.product.is_in_stock_scoring_boost: 0 # The value is used to multiply the document score (0 to disable the scoring boost) + monsieurbiz.search.product.apply_is_in_stock_scoring_boost_on: + - !php/const MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface::SEARCH_TYPE + - !php/const MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface::TAXON_TYPE + - !php/const MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface::INSTANT_TYPE + services: _defaults: autowire: true autoconfigure: true public: false + bind: + Sylius\Bundle\ResourceBundle\Controller\ParametersParserInterface: '@sylius.resource_controller.parameters_parser' + $localeProvider: '@sylius.translation_locale_provider' + $productVariantResolver: '@MonsieurBiz\SyliusSearchPlugin\Resolver\CheapestProductVariantResolver' + $documentableRegistry: '@monsieurbiz.search.registry.documentable' + $searchRequestsRegistry: '@monsieurbiz.search.registry.search_request' + $enableStockFilter: '%monsieurbiz.search.product.enable_stock_filter%' _instanceof: - MonsieurBiz\SyliusSearchPlugin\Fixture\FilterableFixtureInterface: - tags: ['sylius_fixtures.fixture'] + MonsieurBiz\SyliusSearchPlugin\AutoMapper\ProductAttributeValueReader\ReaderInterface: + tags: [ 'monsieurbiz.search.automapper.product_attribute_value_reader' ] MonsieurBiz\SyliusSearchPlugin\: resource: '../../*' exclude: '../../{Entity,Migrations,Tests,Kernel.php}' - MonsieurBiz\SyliusSearchPlugin\Controller\: - resource: '../../Controller' - tags: ['controller.service_arguments'] - MonsieurBiz\SyliusSearchPlugin\Form\Extension\: resource: '../../Form/Extension' tags: - { name: form.type_extension } - # Client configuration. - JoliCode\Elastically\Client: + MonsieurBiz\SyliusSearchPlugin\Generated\: + resource: '../../../generated' + + # ES Client configuration + MonsieurBiz\SyliusSearchPlugin\Search\ClientFactory: arguments: $config: host: '%env(MONSIEURBIZ_SEARCHPLUGIN_ES_HOST)%' port: '%env(MONSIEURBIZ_SEARCHPLUGIN_ES_PORT)%' - elastically_mappings_directory: '%kernel.project_dir%/vendor/monsieurbiz/sylius-search-plugin/src/Resources/config/elasticsearch/mappings' - elastically_index_class_mapping: - # @TODO Add it in config - documents-it_it: \MonsieurBiz\SyliusSearchPlugin\Model\Document\Result - documents-fr_fr: \MonsieurBiz\SyliusSearchPlugin\Model\Document\Result - documents-fr: \MonsieurBiz\SyliusSearchPlugin\Model\Document\Result - documents-en: \MonsieurBiz\SyliusSearchPlugin\Model\Document\Result - documents-en_us: \MonsieurBiz\SyliusSearchPlugin\Model\Document\Result - elastically_bulk_size: 100 - - # Add JS for plugin - monsieurbiz_sylius_search.block_event_listener.layout.javascripts: - class: Sylius\Bundle\UiBundle\Block\BlockEventListener - arguments: - - '@@MonsieurBizSyliusSearchPlugin/js.html.twig' - tags: - - { name: kernel.event_listener, event: sonata.block.event.sylius.shop.layout.javascripts, method: onBlockEvent } - # Add form search in header - monsieurbiz_sylius_search.block_event_listener.layout.header: - class: Sylius\Bundle\UiBundle\Block\BlockEventListener + MonsieurBiz\SyliusSearchPlugin\Repository\ProductAttributeRepository: arguments: - - '@@MonsieurBizSyliusSearchPlugin/Header/form.html.twig' - tags: - - { name: kernel.event_listener, event: sonata.block.event.sylius.shop.layout.header, method: onBlockEvent } + $attributeRepository: '@sylius.repository.product_attribute' - # Event when a product is added / modified / deleted - monsieurbiz_sylius_search.event_listener.document_listener: - class: MonsieurBiz\SyliusSearchPlugin\EventListener\DocumentListener + # Define our mapping provider + JoliCode\Elastically\Mapping\YamlProvider: arguments: - - '@MonsieurBiz\SyliusSearchPlugin\Model\Document\Index\Indexer' - tags: - - { name: kernel.event_listener, event: sylius.product.post_create, method: saveDocument } - - { name: kernel.event_listener, event: sylius.product.post_update, method: saveDocument } - - { name: kernel.event_listener, event: sylius.product.pre_delete, method: deleteDocument } + $configurationDirectory: '@=service("file_locator").locate("@MonsieurBizSyliusSearchPlugin/Resources/config/elasticsearch")' + MonsieurBiz\SyliusSearchPlugin\Mapping\YamlWithLocaleProvider: + decorates: JoliCode\Elastically\Mapping\YamlProvider + arguments: + $decorated: '@.inner' + + # Defines our registries + monsieurbiz.search.registry.documentable: + class: Sylius\Component\Registry\ServiceRegistry + arguments: + $className: '%monsieurbiz.search.model.documentable.interface%' + $context: 'documentable' + + monsieurbiz.search.registry.search_request: + class: Sylius\Component\Registry\ServiceRegistry + arguments: + $className: '%monsieurbiz.search.request.interface%' + $context: 'documentable' - MonsieurBiz\SyliusSearchPlugin\Provider\SearchQueryProvider: + # Define product attribute value readers + MonsieurBiz\SyliusSearchPlugin\AutoMapper\ProductAttributeValueConfiguration: arguments: - $files: '%monsieurbiz_sylius_search.files%' + $productAttributeValueReaders: !tagged_iterator { tag: 'monsieurbiz.search.automapper.product_attribute_value_reader', default_index_method: 'getReaderCode' } - # Provider to retrieve repositories to index - MonsieurBiz\SyliusSearchPlugin\Provider\DocumentRepositoryProvider: + # + MonsieurBiz\SyliusSearchPlugin\EventSubscriber\AppendProductAttributeMappingSubscriber: arguments: - $documentableClasses: '%monsieurbiz_sylius_search.documentable_classes%' + $fieldAnalyzer: '%monsieurbiz.search.product_attribute_analyzer%' + + # Define aggregation builders + MonsieurBiz\SyliusSearchPlugin\Search\Request\Aggregation\MainTaxonAggregation: + tags: { name: 'monsieurbiz.search.aggregation_builder' } + + MonsieurBiz\SyliusSearchPlugin\Search\Request\Aggregation\TaxonsAggregation: + tags: { name: 'monsieurbiz.search.aggregation_builder' } + + MonsieurBiz\SyliusSearchPlugin\Search\Request\Aggregation\PriceAggregation: + tags: { name: 'monsieurbiz.search.aggregation_builder' } + + MonsieurBiz\SyliusSearchPlugin\Search\Request\Aggregation\ProductAttributesAggregation: + tags: { name: 'monsieurbiz.search.aggregation_builder' } - MonsieurBiz\SyliusSearchPlugin\Model\Config\GridConfig: + MonsieurBiz\SyliusSearchPlugin\Search\Request\Aggregation\ProductOptionsAggregation: + tags: { name: 'monsieurbiz.search.aggregation_builder' } + + MonsieurBiz\SyliusSearchPlugin\Search\Request\AggregationBuilder: + arguments: [ !tagged_iterator { tag: 'monsieurbiz.search.aggregation_builder' } ] + + # Define query filters + monsieurbiz.search.request.query_filter.product_search.search_term_filter: + class: MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\SearchTermFilter arguments: - $config: '%monsieurbiz_sylius_search.grid%' + $fieldsToSearch: + - 'name^5' + - 'description' - # Helpers - MonsieurBiz\SyliusSearchPlugin\Helper\RenderDocumentUrlHelper: ~ + monsieurbiz.search.request.query_filter.product_instant_search.search_term_filter: + class: MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\SearchTermFilter + arguments: + $fieldsToSearch: + - 'name^5' + - 'description' + - 'name.autocomplete' - # Twig extensions - MonsieurBiz\SyliusSearchPlugin\Twig\Extension\RenderDocumentUrl: + MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\ProductSearchRegistry: arguments: - $helper: '@MonsieurBiz\SyliusSearchPlugin\Helper\RenderDocumentUrlHelper' - tags: - - { name: twig.extension } + - [ + '@monsieurbiz.search.request.query_filter.product_search.search_term_filter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\ChannelFilter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\EnabledFilter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\IsInStockFilter' + ] + + monsieurbiz.search.request.query_filter.product_instant_search_registry: + class: MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\ProductSearchRegistry + arguments: + - [ + '@monsieurbiz.search.request.query_filter.product_instant_search.search_term_filter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\ChannelFilter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\EnabledFilter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\IsInStockFilter' + ] + + MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\ProductTaxonRegistry: + arguments: + - [ + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\ChannelFilter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\EnabledFilter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\TaxonFilter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\Product\IsInStockFilter' + ] + + # Define post filters + MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\ProductTaxonRegistry: + arguments: + - [ + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\Product\AttributesPostFilter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\Product\MainTaxonPostFilter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\Product\OptionsPostFilter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\Product\PricePostFilter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\Product\ProductTaxonPostFilter', + ] + + # Define sorters + MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\ProductSorterRegistry: + arguments: + - [ + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\Product\PositionSorter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\Product\PriceSorter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\Product\NameSorter', + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\Product\CreatedAtSorter', + ] + + # Functions score + MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore\Product\InStockWeightFunction: + arguments: + $inStockWeight: '%monsieurbiz.search.product.is_in_stock_scoring_boost%' + $applyOnRequestTypes: '%monsieurbiz.search.product.apply_is_in_stock_scoring_boost_on%' + + MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore\ProductFunctionScoreRegistry: + arguments: + - [ + '@MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore\Product\InStockWeightFunction' + ] + + # Define the product queries + MonsieurBiz\SyliusSearchPlugin\Search\Request\ProductRequest\Search: + arguments: + $queryFilterRegistry: '@MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\ProductSearchRegistry' + $postFilterRegistry: '@MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\ProductTaxonRegistry' + $sorterRegistry: '@MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\ProductSorterRegistry' + + MonsieurBiz\SyliusSearchPlugin\Search\Request\ProductRequest\InstantSearch: + arguments: + $queryFilterRegistry: '@monsieurbiz.search.request.query_filter.product_instant_search_registry' + + MonsieurBiz\SyliusSearchPlugin\Search\Request\ProductRequest\Taxon: + arguments: + $queryFilterRegistry: '@MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\ProductTaxonRegistry' + $postFilterRegistry: '@MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\ProductTaxonRegistry' + $sorterRegistry: '@MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\ProductSorterRegistry' + + # Define the filter builders + MonsieurBiz\SyliusSearchPlugin\Search\ResponseFactory: + arguments: + $filterBuilders: { + product_main_taxon: '@MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterBuilders\Product\MainTaxonFilterBuilder', + product_taxons: '@MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterBuilders\Product\TaxonsFilterBuilder', + product_price: '@MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterBuilders\Product\PriceFilterBuilder', + product_attribute: '@MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterBuilders\Product\AttributeFilterBuilder', + product_option: '@MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterBuilders\Product\OptionFilterBuilder' + } diff --git a/src/Resources/config/sylius.yaml b/src/Resources/config/sylius.yaml deleted file mode 100644 index b98da91d..00000000 --- a/src/Resources/config/sylius.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Add JS for plugin for Sylius 2.0 -#sylius_ui: -# events: -# sylius.shop.layout.javascripts: -# blocks: -# monsieur_biz_sylius_search_js: -# template: "@MonsieurBizSyliusSearchPlugin/js.html.twig" -# priority: 0 diff --git a/src/Resources/config/sylius/ui.yaml b/src/Resources/config/sylius/ui.yaml new file mode 100644 index 00000000..7642e9a0 --- /dev/null +++ b/src/Resources/config/sylius/ui.yaml @@ -0,0 +1,11 @@ +sylius_ui: + events: + sylius.shop.layout.javascripts: + blocks: + monsieurbiz_search_javascripts: + template: "@MonsieurBizSyliusSearchPlugin/_scripts.html.twig" + sylius.shop.layout.header.content: + blocks: + search: + template: "@MonsieurBizSyliusSearchPlugin/Header/form.html.twig" + priority: 20 diff --git a/src/Resources/config/twig.yaml b/src/Resources/config/twig.yaml deleted file mode 100644 index fc4605fc..00000000 --- a/src/Resources/config/twig.yaml +++ /dev/null @@ -1,3 +0,0 @@ -twig: - globals: - monsieur_biz_sylius_search_grid_config: '@MonsieurBiz\SyliusSearchPlugin\Model\Config\GridConfig' diff --git a/src/Resources/public/entrypoints.json b/src/Resources/public/entrypoints.json new file mode 100644 index 00000000..78b0e007 --- /dev/null +++ b/src/Resources/public/entrypoints.json @@ -0,0 +1,9 @@ +{ + "entrypoints": { + "monsieurbiz-search": { + "js": [ + "/public/js/monsieurbiz-search.js" + ] + } + } +} \ No newline at end of file diff --git a/src/Resources/public/js/app.js b/src/Resources/public/js/app.js deleted file mode 100644 index d03720fc..00000000 --- a/src/Resources/public/js/app.js +++ /dev/null @@ -1,74 +0,0 @@ -/** global: monsieurbizSearchPlugin */ -(function ($) { - 'use strict'; - $.fn.extend({ - instantSearch: function () { - // No instant if disabled - if (!monsieurbizSearchPlugin.instantEnabled) { - return; - } - $(monsieurbizSearchPlugin.searchInputSelector).prop('autocomplete', 'off'); - // Init a timeout variable to be used below - var instantSearchTimeout = null; - $(monsieurbizSearchPlugin.searchInputSelector).keyup(function() { - clearTimeout(instantSearchTimeout); - var query = $(this).val(); - var resultElement = $(this).closest(monsieurbizSearchPlugin.resultClosestSelector).find(monsieurbizSearchPlugin.resultFindSelector); - instantSearchTimeout = setTimeout(function () { - if (query.length >= monsieurbizSearchPlugin.minQueryLength) { - $.post(monsieurbizSearchPlugin.instantUrl, { query: query }) - .done(function( data ) { - resultElement.html(data); - resultElement.show(); - }); - } - }, monsieurbizSearchPlugin.keyUpTimeOut); - }); - - // Hide results when user leave the search field - $(monsieurbizSearchPlugin.searchInputSelector).focusout(function () { - var resultElement = $(this).closest(monsieurbizSearchPlugin.resultClosestSelector).find(monsieurbizSearchPlugin.resultFindSelector); - setTimeout(function () { - resultElement.hide(); - }, 100); // Add timeout to keep the click on the result - }); - }, - filterSearch: function () { - $(monsieurbizSearchPlugin.priceFilterSelector).prop('autocomplete', 'off'); - - // If only a button can submit filters - if (monsieurbizSearchPlugin.refreshWithButton) { - $(monsieurbizSearchPlugin.filterForm).submit(function(event) { - $(monsieurbizSearchPlugin.loaderSelector).dimmer('show'); - }); - return; - } - - // Init a timeout variable when typing a price - var priceFilterTimeout = null; - $(monsieurbizSearchPlugin.priceFilterSelector).keyup(function() { - clearTimeout(priceFilterTimeout); - var input = $(this); - priceFilterTimeout = setTimeout(function () { - $(this).applyFilter(input.attr('name'), input.val()); - }, monsieurbizSearchPlugin.keyUpTimeOut); - }); - - $(monsieurbizSearchPlugin.attributeFilterSelector).change(function() { - $(this).applyFilter($(this).attr('name'), $(this).val()); - }); - }, - applyFilter: function (field, value) { - // Changed field and value are available in case we need it - $(monsieurbizSearchPlugin.loaderSelector).dimmer('show'); - $(monsieurbizSearchPlugin.filterForm).submit(); - } - }); -})(jQuery); - -(function($) { - $(document).ready(function () { - $(this).instantSearch(); - $(this).filterSearch(); - }); -})(jQuery); diff --git a/src/Resources/public/js/monsieurbiz-search.js b/src/Resources/public/js/monsieurbiz-search.js new file mode 100644 index 00000000..f79fd97b --- /dev/null +++ b/src/Resources/public/js/monsieurbiz-search.js @@ -0,0 +1 @@ +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/public/",n(n.s="ng4s")}({ng4s:function(e,t,n){(function(e){function t(e,t){for(var n=0;n=i){var e=new XMLHttpRequest;e.onload=function(){200===this.status&&(a.innerHTML=this.responseText,a.style.display="block")},e.open("POST",t),e.setRequestHeader("X-Requested-With","XMLHttpRequest"),e.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),e.send(new URLSearchParams({query:n}).toString())}}),u)}));var a=document.querySelector(n).closest(r);a.addEventListener("focusout",(function(e){null!==e.relatedTarget&&a.contains(e.relatedTarget)||(a.querySelector(o).style.display="none")})),document.querySelector(n).addEventListener("focus",(function(e){""!==e.currentTarget.value&&(a.querySelector(o).style.display="block")}))},n&&t(e.prototype,n),r&&t(e,r),Object.defineProperty(e,"prototype",{writable:!1}),e;var e,n,r}(),document.addEventListener("DOMContentLoaded",(function(){new MonsieurBizInstantSearch(monsieurbizSearchPlugin.instantUrl,monsieurbizSearchPlugin.searchInputSelector,monsieurbizSearchPlugin.resultClosestSelector,monsieurbizSearchPlugin.resultFindSelector,monsieurbizSearchPlugin.keyUpTimeOut,monsieurbizSearchPlugin.minQueryLength)}))}).call(this,n("yLpj"))},yLpj:function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n}}); \ No newline at end of file diff --git a/src/Resources/public/manifest.json b/src/Resources/public/manifest.json new file mode 100644 index 00000000..0462ffdf --- /dev/null +++ b/src/Resources/public/manifest.json @@ -0,0 +1,3 @@ +{ + "public/monsieurbiz-search.js": "/public/js/monsieurbiz-search.js" +} \ No newline at end of file diff --git a/src/Resources/templates/bundles/SyliusAdminBundle/ProductAttribute/_form.html.twig b/src/Resources/templates/bundles/SyliusAdminBundle/ProductAttribute/_form.html.twig new file mode 100644 index 00000000..3130f269 --- /dev/null +++ b/src/Resources/templates/bundles/SyliusAdminBundle/ProductAttribute/_form.html.twig @@ -0,0 +1,30 @@ +{% from '@SyliusAdmin/Macro/translationForm.html.twig' import translationForm %} + +{{ form_errors(form) }} + +
+
+ {{ form_row(form.code) }} + {{ form_row(form.position) }} + {{ form_row(form.type) }} +
+ {{ form_row(form.translatable) }} +
+
+

{{ 'monsieurbiz_searchplugin.admin.product_attribute.form.title'|trans }}

+
+ {{ form_row(form.searchable) }} + {{ form_row(form.filterable) }} + {{ form_row(form.search_weight) }} +
+
+{% if form.configuration is defined %} +
+

{{ 'sylius.ui.configuration'|trans }}

+ {% for field in form.configuration %} + {{ form_row(field) }} + {% endfor %} +
+{% endif %} +{{ translationForm(form.translations) }} + diff --git a/src/Resources/templates/bundles/SyliusAdminBundle/ProductOption/_form.html.twig b/src/Resources/templates/bundles/SyliusAdminBundle/ProductOption/_form.html.twig new file mode 100644 index 00000000..661788ae --- /dev/null +++ b/src/Resources/templates/bundles/SyliusAdminBundle/ProductOption/_form.html.twig @@ -0,0 +1,20 @@ +{% from '@SyliusAdmin/Macro/translationForm.html.twig' import translationForm %} + +
+ {{ form_errors(form) }} +
+ {{ form_row(form.code) }} + {{ form_row(form.position) }} +
+ {{ translationForm(form.translations) }} +
+
+

{{ 'monsieurbiz_searchplugin.admin.product_option.form.title'|trans }}

+
+ {{ form_row(form.searchable) }} + {{ form_row(form.filterable) }} + {{ form_row(form.search_weight) }} +
+
+

{{ 'sylius.ui.values'|trans }}

+{{ form_row(form.values) }} diff --git a/src/Resources/translations/messages.en.yml b/src/Resources/translations/messages.en.yml index e772090b..ec17a2ae 100644 --- a/src/Resources/translations/messages.en.yml +++ b/src/Resources/translations/messages.en.yml @@ -4,12 +4,19 @@ monsieurbiz_searchplugin: form: title: Search filterable: Filterable + searchable: Searchable + search_weight: Search Weight product_option: form: title: Search filterable: Filterable + setting_form: + instant_search_enabled_monsieurbiz_product: 'Product: Instant search enabled' + limit_search_monsieurbiz_product: 'Product: Available limits for search' + limit_instant_search_monsieurbiz_product: 'Product: Available limits for instant search' form: query: 'Search' + query_placeholder: 'Search…' submit: 'Submit search' search: result: @@ -19,6 +26,7 @@ monsieurbiz_searchplugin: result: search_result: 'Search results for "%query%" (%count%)' no_result: 'No result.' + monsieurbiz_product: 'Products' filters: filter_results: 'Filter results' apply_filters: 'Apply filters' diff --git a/src/Resources/translations/messages.fr.yml b/src/Resources/translations/messages.fr.yml index 112acfaa..829fe97e 100644 --- a/src/Resources/translations/messages.fr.yml +++ b/src/Resources/translations/messages.fr.yml @@ -4,12 +4,19 @@ monsieurbiz_searchplugin: form: title: Recherche filterable: Filtrable + searchable: Recherchable + search_weight: Pondération dans la recherche product_option: form: title: Recherche filterable: Filtrable + setting_form: + instant_search_enabled_monsieurbiz_product: 'Produit: Activer la recherche instantanée' + limit_search_monsieurbiz_product: 'Produit: Limites disponibles pour la recherche' + limit_instant_search_monsieurbiz_product: 'Produit: Limites disponibles pour la recherche instantanée' form: query: 'Recherche' + query_placeholder: 'Rechercher…' submit: 'Lancer la recherche' search: result: diff --git a/src/Resources/translations/messages.it.yml b/src/Resources/translations/messages.it.yml index 95e7c38f..fb1bf7a6 100644 --- a/src/Resources/translations/messages.it.yml +++ b/src/Resources/translations/messages.it.yml @@ -10,6 +10,7 @@ monsieurbiz_searchplugin: filterable: Filtrabile form: query: 'Cerca' + query_placeholder: 'Cerca…' submit: 'Cerca' search: result: diff --git a/src/Resources/views/Common/_box.html.twig b/src/Resources/views/Common/_box.html.twig deleted file mode 100644 index 09111e04..00000000 --- a/src/Resources/views/Common/_box.html.twig +++ /dev/null @@ -1,27 +0,0 @@ -{% import "@SyliusShop/Common/Macro/money.html.twig" as money %} - - diff --git a/src/Resources/views/Common/_filter.html.twig b/src/Resources/views/Common/_filter.html.twig deleted file mode 100644 index edff2c97..00000000 --- a/src/Resources/views/Common/_filter.html.twig +++ /dev/null @@ -1,28 +0,0 @@ -{% set appliedAttributes = app.request.query.get('attribute') %} -{% set currentValues = (appliedAttributes[filter.code] is defined) ? appliedAttributes[filter.code] : [] %} -{% if filter.values|length > 1 or currentValues is not empty %} -
-
-
{{ filter.label | trans }}
-
- {% for value in filter.values %} - {% set valueIsApplied = (value.slug in currentValues) %} -
-
- -
-
- {% endfor %} -
-
-
-{% endif %} diff --git a/src/Resources/views/Common/_filters.html.twig b/src/Resources/views/Common/_filters.html.twig deleted file mode 100644 index 53b78708..00000000 --- a/src/Resources/views/Common/_filters.html.twig +++ /dev/null @@ -1,50 +0,0 @@ -{% set taxonFilter = gridConfig.useMainTaxonForFilter() ? resultSet.mainTaxonFilter : resultSet.taxonFilter %} - - diff --git a/src/Resources/views/Common/_pagination.html.twig b/src/Resources/views/Common/_pagination.html.twig deleted file mode 100644 index c2a1f838..00000000 --- a/src/Resources/views/Common/_pagination.html.twig +++ /dev/null @@ -1,16 +0,0 @@ -{% set paginationLimits = (limits) %} - -
-
- -
-
diff --git a/src/Resources/views/Common/_rangeFilter.html.twig b/src/Resources/views/Common/_rangeFilter.html.twig deleted file mode 100644 index c6f312bf..00000000 --- a/src/Resources/views/Common/_rangeFilter.html.twig +++ /dev/null @@ -1,25 +0,0 @@ -
- {% set currentValue = app.request.query.get( filter.code ) %} - {% set minValue = (currentValue['min'] is defined) ? currentValue['min'] : filter.min %} - {% set maxValue = (currentValue['max'] is defined) ? currentValue['max'] : filter.max %} -
-
{{ filter.label | trans }}
-
- -
- -
-
{{ moneySymbol }}
-
-
- -
- -
-
{{ moneySymbol }}
-
-
- -
-
-
diff --git a/src/Resources/views/Common/_sorting.html.twig b/src/Resources/views/Common/_sorting.html.twig deleted file mode 100644 index 2d2da374..00000000 --- a/src/Resources/views/Common/_sorting.html.twig +++ /dev/null @@ -1,52 +0,0 @@ -{% if resultSet.totalHits > 0 %} - - {% set route = app.request.attributes.get('_route') %} - {% set route_parameters = app.request.attributes.get('_route_params')|merge(app.request.query.all) %} - - {% set criteria = app.request.query.get('criteria', {}) %} - - {% set default_path = path(route, route_parameters|merge({'sorting': null, 'criteria': criteria})) %} - {% set from_a_to_z_path = path(route, route_parameters|merge({'sorting': {'name': 'asc'}, 'criteria': criteria})) %} - {% set from_z_to_a_path = path(route, route_parameters|merge({'sorting': {'name': 'desc'}, 'criteria': criteria})) %} - {% set oldest_first_path = path(route, route_parameters|merge({'sorting': {'created_at': 'asc'}, 'criteria': criteria})) %} - {% set newest_first_path = path(route, route_parameters|merge({'sorting': {'created_at': 'desc'}, 'criteria': criteria})) %} - {% set cheapest_first_path = path(route, route_parameters|merge({'sorting': {'price': 'asc'}, 'criteria': criteria})) %} - {% set most_expensive_first_path = path(route, route_parameters|merge({'sorting': {'price': 'desc'}, 'criteria': criteria})) %} - - {% if app.request.query.get('sorting') is empty %} - {% set current_sorting_label = 'sylius.ui.by_position'|trans|lower %} - {% elseif app.request.query.get('sorting').name is defined and app.request.query.get('sorting').name == 'asc'%} - {% set current_sorting_label = 'sylius.ui.from_a_to_z'|trans|lower %} - {% elseif app.request.query.get('sorting').name is defined and app.request.query.get('sorting').name == 'desc'%} - {% set current_sorting_label = 'sylius.ui.from_z_to_a'|trans|lower %} - {% elseif app.request.query.get('sorting').created_at is defined and app.request.query.get('sorting').created_at == 'desc'%} - {% set current_sorting_label = 'sylius.ui.newest_first'|trans|lower %} - {% elseif app.request.query.get('sorting').created_at is defined and app.request.query.get('sorting').created_at == 'asc'%} - {% set current_sorting_label = 'sylius.ui.oldest_first'|trans|lower %} - {% elseif app.request.query.get('sorting').price is defined and app.request.query.get('sorting').price == 'asc'%} - {% set current_sorting_label = 'sylius.ui.cheapest_first'|trans|lower %} - {% elseif app.request.query.get('sorting').price is defined and app.request.query.get('sorting').price == 'desc' %} - {% set current_sorting_label = 'sylius.ui.most_expensive_first'|trans|lower %} - {% else %} - {% set current_sorting_label = 'sylius.ui.by_position'|trans|lower %} - {% endif %} - - -{% endif %} diff --git a/src/Resources/views/Event/_sonata.html.twig b/src/Resources/views/Event/_sonata.html.twig deleted file mode 100644 index f4308e68..00000000 --- a/src/Resources/views/Event/_sonata.html.twig +++ /dev/null @@ -1 +0,0 @@ -{{ sonata_block_render_event(eventName, eventData) }} diff --git a/src/Resources/views/Event/_sylius.html.twig b/src/Resources/views/Event/_sylius.html.twig deleted file mode 100644 index ab3779aa..00000000 --- a/src/Resources/views/Event/_sylius.html.twig +++ /dev/null @@ -1 +0,0 @@ -{{ sylius_template_event(eventName, eventData) }} diff --git a/src/Resources/views/Event/event.html.twig b/src/Resources/views/Event/event.html.twig deleted file mode 100644 index 55a25779..00000000 --- a/src/Resources/views/Event/event.html.twig +++ /dev/null @@ -1,5 +0,0 @@ -{% if bundle_exists('SonataCoreBundle') %} - {% include '@MonsieurBizSyliusSearchPlugin/Event/_sonata.html.twig' with {'eventName': eventName, 'eventData': eventData} %} -{% else %} - {% include '@MonsieurBizSyliusSearchPlugin/Event/_sylius.html.twig' with {'eventName': eventName, 'eventData': eventData} %} -{% endif %} diff --git a/src/Resources/views/Instant/Product/_box.html.twig b/src/Resources/views/Instant/Product/_box.html.twig new file mode 100644 index 00000000..dda699eb --- /dev/null +++ b/src/Resources/views/Instant/Product/_box.html.twig @@ -0,0 +1,21 @@ +{% import "@SyliusShop/Common/Macro/money.html.twig" as money %} + +
+ + {% if item.images|first %} + {% set path = item.images|first.path|imagine_filter(filter|default('sylius_shop_product_thumbnail')) %} + {% else %} + {% set path = '//placehold.it/200x200' %} + {% endif %} + + {{ item.name }} + +
+ {{ item.name }} + + {% if item.prices is not empty %} + {% set pricing = item.prices|filter(price => price.channelCode == sylius.channel.code)|first %} +
{{ money.convertAndFormat(pricing.price) }}
+ {% endif %} +
+
diff --git a/src/Resources/views/Instant/_box.html.twig b/src/Resources/views/Instant/_box.html.twig deleted file mode 100644 index 5b2d76e5..00000000 --- a/src/Resources/views/Instant/_box.html.twig +++ /dev/null @@ -1,27 +0,0 @@ -{% import "@SyliusShop/Common/Macro/money.html.twig" as money %} - - diff --git a/src/Resources/views/Instant/result.html.twig b/src/Resources/views/Instant/result.html.twig index 2410c3f6..b91b310b 100644 --- a/src/Resources/views/Instant/result.html.twig +++ b/src/Resources/views/Instant/result.html.twig @@ -1,9 +1,21 @@ {% block content %} - {% if resultSet.totalHits == 0 %} - {{ 'monsieurbiz_searchplugin.instant.result.no_result'|trans }} - {% else %} - {% for result in resultSet.results %} - {% include '@MonsieurBizSyliusSearchPlugin/Instant/_box.html.twig' with {'result': result} %} - {% endfor %} + {% set noResult = true %} + {% for result in results %} + {% if result.count != 0 %} + {% set noResult = false %} +
+
{{ ('monsieurbiz_searchplugin.instant.result.' ~ result.documentable.indexCode)|trans }}
+
+ {% for item in result %} + {% include result.documentable.template('instant') with {'item': item.model} %} + {% endfor %} +
+
+ {% endif %} + {% endfor %} + {% if noResult %} +
+
{{ 'monsieurbiz_searchplugin.instant.result.no_result'|trans }}
+
{% endif %} {% endblock %} diff --git a/src/Resources/views/Search/Filter/default.html.twig b/src/Resources/views/Search/Filter/default.html.twig new file mode 100644 index 00000000..33222ab4 --- /dev/null +++ b/src/Resources/views/Search/Filter/default.html.twig @@ -0,0 +1,30 @@ +{% set inputName = filter.code %} +{% if filter.type is not empty %} + {% set inputName = filter.type ~ '[' ~ inputName ~ ']' %} +{% endif %} + +{% if filter.values|length %} +
+
+
{{ filter.label | trans }}
+
+ {% for value in filter.values %} +
+
+ +
+
+ {% endfor %} +
+
+
+{% endif %} diff --git a/src/Resources/views/Search/Filter/range.html.twig b/src/Resources/views/Search/Filter/range.html.twig new file mode 100644 index 00000000..a4f7e287 --- /dev/null +++ b/src/Resources/views/Search/Filter/range.html.twig @@ -0,0 +1,20 @@ +
+
+
{{ filter.label | trans }}
+
+ + {% for value in filter.values %} + {% set valueType = filter.valueType(value.label) %} + {% set inputValue = value.isApplied ? value.value : '' %} + {% set placeholderValue = value.isApplied ? filter.defaultValue(valueType) : value.value %} +
+ +
+
{{ currencySymbol }}
+ +
+
+ {% endfor %} +
+
+
diff --git a/src/Resources/views/Search/_box.html.twig b/src/Resources/views/Search/_box.html.twig deleted file mode 100644 index 01dd1460..00000000 --- a/src/Resources/views/Search/_box.html.twig +++ /dev/null @@ -1 +0,0 @@ -{% include '@MonsieurBizSyliusSearchPlugin/Common/_box.html.twig' with {'result': result} %} diff --git a/src/Resources/views/Search/_filter.html.twig b/src/Resources/views/Search/_filter.html.twig new file mode 100644 index 00000000..e1cba8b2 --- /dev/null +++ b/src/Resources/views/Search/_filter.html.twig @@ -0,0 +1,7 @@ +{% include [ + '@MonsieurBizSyliusSearchPlugin/Search/Filter/'~filter.type~'.html.twig', + '@MonsieurBizSyliusSearchPlugin/Search/Filter/default.html.twig' + ] with { + 'filter': filter, + 'currencySymbol': currencySymbol +} %} diff --git a/src/Resources/views/Search/_filters.html.twig b/src/Resources/views/Search/_filters.html.twig index 0b4d2940..4f44e6c7 100644 --- a/src/Resources/views/Search/_filters.html.twig +++ b/src/Resources/views/Search/_filters.html.twig @@ -1 +1,41 @@ -{% include '@MonsieurBizSyliusSearchPlugin/Common/_filters.html.twig' %} + diff --git a/src/Resources/views/form.html.twig b/src/Resources/views/Search/_form.html.twig similarity index 67% rename from src/Resources/views/form.html.twig rename to src/Resources/views/Search/_form.html.twig index 3b861152..3d5e5754 100644 --- a/src/Resources/views/form.html.twig +++ b/src/Resources/views/Search/_form.html.twig @@ -1,6 +1,6 @@ {% form_theme form '@SyliusShop/Form/theme.html.twig' %}
- {{ form_start(form, {'action': path('monsieurbiz_sylius_search_post'), 'method': 'POST', 'attr': {'class': 'ui search item autocomplete-search'}}) }} + {{ form_start(form, {'action': path('monsieurbiz_search_post'), 'method': 'POST', 'attr': {'class': 'ui search item category autocomplete-search', 'tabindex': '-1'}}) }} {{ form_errors(form) }} {{ form_row(form.query, {'value': query, 'label': false}) }} {{ form_row(form.submit, {'attr': {'class': 'ui primary button'}}) }} diff --git a/src/Resources/views/Search/_header.html.twig b/src/Resources/views/Search/_header.html.twig index 90131d43..23771156 100644 --- a/src/Resources/views/Search/_header.html.twig +++ b/src/Resources/views/Search/_header.html.twig @@ -1,7 +1,5 @@ -{% include '@MonsieurBizSyliusSearchPlugin/Event/event.html.twig' with {'eventName': 'sylius.shop.search.header.before', 'eventData': {'query': query}} %} - +{{ sylius_template_event('sylius.shop.search.header.before', _context) }}

- {{ 'monsieurbiz_searchplugin.search.result.search_result'|trans({'%query%': query, '%count%': resultSet.totalHits}) }} + {{ 'monsieurbiz_searchplugin.search.result.search_result'|trans({'%query%': query, '%count%': result.count}) }}

- -{% include '@MonsieurBizSyliusSearchPlugin/Event/event.html.twig' with {'eventName': 'sylius.shop.search.header.after', 'eventData': {'query': query}} %} +{{ sylius_template_event('sylius.shop.search.header.after', _context) }} diff --git a/src/Resources/views/Search/_pagination.html.twig b/src/Resources/views/Search/_pagination.html.twig index bd29502e..21f7d365 100644 --- a/src/Resources/views/Search/_pagination.html.twig +++ b/src/Resources/views/Search/_pagination.html.twig @@ -1 +1,16 @@ -{% include '@MonsieurBizSyliusSearchPlugin/Common/_pagination.html.twig' %} +{% set paginationLimits = requestConfiguration.availableLimits %} + +
+
+ +
+
diff --git a/src/Resources/views/Search/_sorting.html.twig b/src/Resources/views/Search/_sorting.html.twig index a91f09ff..7103c174 100644 --- a/src/Resources/views/Search/_sorting.html.twig +++ b/src/Resources/views/Search/_sorting.html.twig @@ -1 +1,51 @@ -{% include '@MonsieurBizSyliusSearchPlugin/Common/_sorting.html.twig' %} +{% if result.count > 0 %} + {% set route = app.request.attributes.get('_route') %} + {% set route_parameters = app.request.attributes.get('_route_params')|merge(app.request.query.all) %} + + {% set criteria = app.request.query.get('criteria', {}) %} + + {% set default_path = path(route, route_parameters|merge({'sorting': null, 'criteria': criteria})) %} + {% set from_a_to_z_path = path(route, route_parameters|merge({'sorting': {'name': 'asc'}, 'criteria': criteria})) %} + {% set from_z_to_a_path = path(route, route_parameters|merge({'sorting': {'name': 'desc'}, 'criteria': criteria})) %} + {% set oldest_first_path = path(route, route_parameters|merge({'sorting': {'created_at': 'asc'}, 'criteria': criteria})) %} + {% set newest_first_path = path(route, route_parameters|merge({'sorting': {'created_at': 'desc'}, 'criteria': criteria})) %} + {% set cheapest_first_path = path(route, route_parameters|merge({'sorting': {'price': 'asc'}, 'criteria': criteria})) %} + {% set most_expensive_first_path = path(route, route_parameters|merge({'sorting': {'price': 'desc'}, 'criteria': criteria})) %} + + {% if app.request.query.get('sorting') is empty %} + {% set current_sorting_label = 'sylius.ui.by_position'|trans|lower %} + {% elseif app.request.query.get('sorting').name is defined and app.request.query.get('sorting').name == 'asc'%} + {% set current_sorting_label = 'sylius.ui.from_a_to_z'|trans|lower %} + {% elseif app.request.query.get('sorting').name is defined and app.request.query.get('sorting').name == 'desc'%} + {% set current_sorting_label = 'sylius.ui.from_z_to_a'|trans|lower %} + {% elseif app.request.query.get('sorting').created_at is defined and app.request.query.get('sorting').created_at == 'desc'%} + {% set current_sorting_label = 'sylius.ui.newest_first'|trans|lower %} + {% elseif app.request.query.get('sorting').created_at is defined and app.request.query.get('sorting').created_at == 'asc'%} + {% set current_sorting_label = 'sylius.ui.oldest_first'|trans|lower %} + {% elseif app.request.query.get('sorting').price is defined and app.request.query.get('sorting').price == 'asc'%} + {% set current_sorting_label = 'sylius.ui.cheapest_first'|trans|lower %} + {% elseif app.request.query.get('sorting').price is defined and app.request.query.get('sorting').price == 'desc' %} + {% set current_sorting_label = 'sylius.ui.most_expensive_first'|trans|lower %} + {% else %} + {% set current_sorting_label = 'sylius.ui.by_position'|trans|lower %} + {% endif %} + + +{% endif %} diff --git a/src/Resources/views/Search/product/_box.html.twig b/src/Resources/views/Search/product/_box.html.twig new file mode 100644 index 00000000..8b359128 --- /dev/null +++ b/src/Resources/views/Search/product/_box.html.twig @@ -0,0 +1,28 @@ +{% import "@SyliusShop/Common/Macro/money.html.twig" as money %} + +
+ +
+
+
+
{{ 'sylius.ui.view_more'|trans }}
+
+
+
+ {% if item.images|first %} + {% set path = item.images|first.path|imagine_filter(filter|default('sylius_shop_product_thumbnail')) %} + {% else %} + {% set path = '//placehold.it/200x200' %} + {% endif %} + + {{ item.name }} +
+
+ {{ item.name }} + + {% if item.prices is not empty %} + {% set pricing = item.prices|filter(price => price.channelCode == sylius.channel.code)|first %} +
{{ money.convertAndFormat(pricing.price) }}
+ {% endif %} +
+
diff --git a/src/Resources/views/Search/result.html.twig b/src/Resources/views/Search/result.html.twig index be69a02d..b9d802f9 100644 --- a/src/Resources/views/Search/result.html.twig +++ b/src/Resources/views/Search/result.html.twig @@ -4,41 +4,41 @@ {% block content %} {% include '@MonsieurBizSyliusSearchPlugin/Search/_header.html.twig' %} -
-
-
-

{{ 'monsieurbiz_searchplugin.filters.loading' | trans }}

-
-
-
- {% include '@MonsieurBizSyliusSearchPlugin/Search/_sidebar.html.twig' %} +
+
+
+

{{ 'monsieurbiz_searchplugin.filters.loading' | trans }}

-
- {% if resultSet.totalHits == 0 %} -
-
-
-

- {{ 'monsieurbiz_searchplugin.search.result.no_result'|trans }} -

-
+
+
+ {% include '@MonsieurBizSyliusSearchPlugin/Search/_sidebar.html.twig' %} +
+
+ {% if result.count == 0 %} +
+
+
+

+ {{ 'monsieurbiz_searchplugin.search.result.no_result'|trans }} +

- {% else %} - {% include '@MonsieurBizSyliusSearchPlugin/Search/_pagination.html.twig' %} - {% include '@MonsieurBizSyliusSearchPlugin/Search/_sorting.html.twig' %} - +
+ {% else %} + {% include '@MonsieurBizSyliusSearchPlugin/Search/_pagination.html.twig' %} + {% include '@MonsieurBizSyliusSearchPlugin/Search/_sorting.html.twig' %} + -
- {% for result in resultSet.results %} - {% include '@MonsieurBizSyliusSearchPlugin/Search/_box.html.twig' with {'result': result} %} - {% endfor %} -
+
+ {% for item in result %} + {% include documentable.template('item') with {'item': item.model} %} + {% endfor %} +
- + - {{ pagination.simple(resultSet.pager) }} - {% endif %} -
+ {{ pagination.simple(result.paginator) }} + {% endif %}
+
{% endblock %} diff --git a/src/Resources/views/Taxon/_box.html.twig b/src/Resources/views/Taxon/_box.html.twig deleted file mode 100644 index 01dd1460..00000000 --- a/src/Resources/views/Taxon/_box.html.twig +++ /dev/null @@ -1 +0,0 @@ -{% include '@MonsieurBizSyliusSearchPlugin/Common/_box.html.twig' with {'result': result} %} diff --git a/src/Resources/views/Taxon/_filters.html.twig b/src/Resources/views/Taxon/_filters.html.twig deleted file mode 100644 index 0b4d2940..00000000 --- a/src/Resources/views/Taxon/_filters.html.twig +++ /dev/null @@ -1 +0,0 @@ -{% include '@MonsieurBizSyliusSearchPlugin/Common/_filters.html.twig' %} diff --git a/src/Resources/views/Taxon/_pagination.html.twig b/src/Resources/views/Taxon/_pagination.html.twig deleted file mode 100644 index bd29502e..00000000 --- a/src/Resources/views/Taxon/_pagination.html.twig +++ /dev/null @@ -1 +0,0 @@ -{% include '@MonsieurBizSyliusSearchPlugin/Common/_pagination.html.twig' %} diff --git a/src/Resources/views/Taxon/_sidebar.html.twig b/src/Resources/views/Taxon/_sidebar.html.twig deleted file mode 100644 index 23d1eabe..00000000 --- a/src/Resources/views/Taxon/_sidebar.html.twig +++ /dev/null @@ -1,6 +0,0 @@ -{% include '@MonsieurBizSyliusSearchPlugin/Event/event.html.twig' with {'eventName': 'sylius.shop.product.index.before_vertical_menu', 'eventData': {'taxon': taxon}} %} - -{#{% include '@MonsieurBizSyliusSearchPlugin/Taxon/_tree.html.twig' %}#} -{% include '@MonsieurBizSyliusSearchPlugin/Taxon/_filters.html.twig' %} - -{% include '@MonsieurBizSyliusSearchPlugin/Event/event.html.twig' with {'eventName': 'sylius.shop.product.index.after_vertical_menu', 'eventData': {'taxon': taxon}} %} diff --git a/src/Resources/views/Taxon/_sorting.html.twig b/src/Resources/views/Taxon/_sorting.html.twig deleted file mode 100644 index a91f09ff..00000000 --- a/src/Resources/views/Taxon/_sorting.html.twig +++ /dev/null @@ -1 +0,0 @@ -{% include '@MonsieurBizSyliusSearchPlugin/Common/_sorting.html.twig' %} diff --git a/src/Resources/views/Taxon/_tree.html.twig b/src/Resources/views/Taxon/_tree.html.twig deleted file mode 100644 index e7ab2956..00000000 --- a/src/Resources/views/Taxon/_tree.html.twig +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/src/Resources/views/Taxon/result.html.twig b/src/Resources/views/Taxon/result.html.twig index ad745724..f677c0eb 100644 --- a/src/Resources/views/Taxon/result.html.twig +++ b/src/Resources/views/Taxon/result.html.twig @@ -4,41 +4,41 @@ {% block content %} {% include '@SyliusShop/Product/Index/_header.html.twig' %} -
-
-
-

{{ 'monsieurbiz_searchplugin.filters.loading' | trans }}

-
-
-
- {% include '@MonsieurBizSyliusSearchPlugin/Taxon/_sidebar.html.twig' %} +
+
+
+

{{ 'monsieurbiz_searchplugin.filters.loading' | trans }}

-
- {% if resultSet.totalHits == 0 %} -
-
-
-

- {{ 'monsieurbiz_searchplugin.search.result.no_result'|trans }} -

-
+
+
+ {% include '@MonsieurBizSyliusSearchPlugin/Search/_sidebar.html.twig' %} +
+
+ {% if result.count == 0 %} +
+
+
+

+ {{ 'monsieurbiz_searchplugin.search.result.no_result'|trans }} +

- {% else %} - {% include '@MonsieurBizSyliusSearchPlugin/Taxon/_pagination.html.twig' %} - {% include '@MonsieurBizSyliusSearchPlugin/Taxon/_sorting.html.twig' %} - +
+ {% else %} + {% include '@MonsieurBizSyliusSearchPlugin/Search/_pagination.html.twig' %} + {% include '@MonsieurBizSyliusSearchPlugin/Search/_sorting.html.twig' %} + -
- {% for result in resultSet.results %} - {% include '@MonsieurBizSyliusSearchPlugin/Taxon/_box.html.twig' with {'result': result} %} - {% endfor %} -
+
+ {% for item in result %} + {% include result.documentable.template('item') with {'item': item.model} %} + {% endfor %} +
- + - {{ pagination.simple(resultSet.pager) }} - {% endif %} -
+ {{ pagination.simple(result.paginator) }} + {% endif %}
+
{% endblock %} diff --git a/src/Resources/views/_scripts.html.twig b/src/Resources/views/_scripts.html.twig new file mode 100644 index 00000000..ed1d3b06 --- /dev/null +++ b/src/Resources/views/_scripts.html.twig @@ -0,0 +1,12 @@ + +{% include '@SyliusUi/_javascripts.html.twig' with {'path': 'bundles/monsieurbizsyliussearchplugin/js/monsieurbiz-search.js'} %} diff --git a/src/Resources/views/js.html.twig b/src/Resources/views/js.html.twig deleted file mode 100644 index c14f2620..00000000 --- a/src/Resources/views/js.html.twig +++ /dev/null @@ -1,17 +0,0 @@ - -{% include '@SyliusUi/_javascripts.html.twig' with {'path': 'bundles/monsieurbizsyliussearchplugin/js/app.js'} %} diff --git a/src/Search/ClientFactory.php b/src/Search/ClientFactory.php new file mode 100644 index 00000000..ed517b89 --- /dev/null +++ b/src/Search/ClientFactory.php @@ -0,0 +1,74 @@ + + * + * 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; + +use JoliCode\Elastically\Client; +use JoliCode\Elastically\Factory; +use JoliCode\Elastically\IndexBuilder; +use JoliCode\Elastically\Indexer; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use Symfony\Component\Serializer\SerializerInterface; + +class ClientFactory +{ + private array $config; + + private SerializerInterface $serializer; + + public function __construct(SerializerInterface $serializer, array $config = []) + { + $this->config = $config; + $this->serializer = $serializer; + } + + public function getClient(DocumentableInterface $documentable, ?string $localeCode = null): Client + { + $factory = new Factory($this->getConfig($documentable, $localeCode)); + + return $factory->buildClient(); + } + + public function getIndexBuilder(DocumentableInterface $documentable, ?string $localeCode = null): IndexBuilder + { + $factory = new Factory($this->getConfig($documentable, $localeCode)); + + return $factory->buildIndexBuilder(); + } + + public function getIndexer(DocumentableInterface $documentable, ?string $localeCode = null): Indexer + { + $factory = new Factory($this->getConfig($documentable, $localeCode)); + + return $factory->buildIndexer(); + } + + public function getIndexName(DocumentableInterface $documentable, ?string $locale): string + { + return $documentable->getIndexCode() . strtolower(null !== $locale ? '_' . $locale : ''); + } + + private function getConfig(DocumentableInterface $documentable, ?string $localeCode): array + { + $indexName = $this->getIndexName($documentable, $localeCode); + $additionalConfig = [ + Factory::CONFIG_INDEX_CLASS_MAPPING => [ + $indexName => $documentable->getTargetClass(), + ], + Factory::CONFIG_MAPPINGS_PROVIDER => $documentable->getMappingProvider(), + Factory::CONFIG_SERIALIZER => $this->serializer, + ]; + + return array_merge($this->config, $additionalConfig); + } +} diff --git a/src/Search/Filter/Filter.php b/src/Search/Filter/Filter.php new file mode 100644 index 00000000..1b365cca --- /dev/null +++ b/src/Search/Filter/Filter.php @@ -0,0 +1,114 @@ + + * + * 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\Filter; + +use MonsieurBiz\SyliusSearchPlugin\Helper\SlugHelper; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterInterface; + +class Filter implements FilterInterface +{ + /** + * @var string + */ + private $code; + + /** + * @var string + */ + private $label; + + /** + * @var FilterValue[] + */ + private $values = []; + + /** + * @var int + */ + private $count; + + private string $type; + + private RequestConfiguration $requestConfiguration; + + /** + * Filter constructor. + */ + public function __construct(RequestConfiguration $requestConfiguration, string $code, string $label, int $count, string $type = '') + { + $this->code = $code; + $this->label = $label; + $this->count = $count; + $this->type = $type; + $this->requestConfiguration = $requestConfiguration; + } + + public function getCode(): string + { + return $this->code; + } + + public function getLabel(): string + { + return $this->label; + } + + /** + * @return FilterValue[] + */ + public function getValues(): array + { + return $this->values; + } + + public function addValue(string $label, int $count, ?string $value = null): void + { + $this->values[] = new FilterValue( + $label, + $count, + $value, + \in_array(SlugHelper::toSlug($value ?? $label), $this->getCurrentValues(), true) + ); + } + + public function getCount(): int + { + return $this->count; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): void + { + $this->type = $type; + } + + public function getAppliedValues(): array + { + return array_filter($this->getValues(), function (FilterValue $filterValue): bool { + return $filterValue->isApplied(); + }); + } + + protected function getCurrentValues(): array + { + $appliedFilters = $this->requestConfiguration->getAppliedFilters(); + + return $appliedFilters[$this->getType()][$this->getCode()] ?? $appliedFilters[$this->getCode()] ?? []; + } +} diff --git a/src/Model/Document/FilterValue.php b/src/Search/Filter/FilterValue.php similarity index 50% rename from src/Model/Document/FilterValue.php rename to src/Search/Filter/FilterValue.php index 3192a5ce..6ae666b4 100644 --- a/src/Model/Document/FilterValue.php +++ b/src/Search/Filter/FilterValue.php @@ -5,67 +5,64 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * 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\Model\Document; +namespace MonsieurBiz\SyliusSearchPlugin\Search\Filter; use MonsieurBiz\SyliusSearchPlugin\Helper\SlugHelper; class FilterValue { - /** - * @var string - */ - private $slug; + private string $label; - /** - * @var string - */ - private $label; + private int $count; - /** - * @var int - */ - private $count; + private string $value; + + private bool $isApplied; /** * Filter constructor. - * - * @param string $label - * @param int $count */ - public function __construct(string $label, int $count) + public function __construct(string $label, int $count, string $value = null, bool $isApplied = false) { - $this->slug = SlugHelper::toSlug($label); + $this->value = $value ?? $label; $this->label = $label; $this->count = $count; + $this->isApplied = $isApplied; } - /** - * @return string - */ public function getSlug(): string { - return $this->slug; + return SlugHelper::toSlug($this->value); } - /** - * @return string - */ public function getLabel(): string { return $this->label; } - /** - * @return int - */ public function getCount(): int { return $this->count; } + + public function getValue(): string + { + return $this->value; + } + + public function setValue(string $value): void + { + $this->value = $value; + } + + public function isApplied(): bool + { + return $this->isApplied; + } } diff --git a/src/Search/Filter/RangeFilter.php b/src/Search/Filter/RangeFilter.php new file mode 100644 index 00000000..59a92fb2 --- /dev/null +++ b/src/Search/Filter/RangeFilter.php @@ -0,0 +1,127 @@ + + * + * 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\Filter; + +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterInterface; + +class RangeFilter implements FilterInterface +{ + /** + * @var string + */ + private $code; + + /** + * @var string + */ + private $label; + + /** + * @var string + */ + private $minLabel; + + /** + * @var int + */ + private $min; + + /** + * @var int + */ + private $max; + + private array $values = []; + + private RequestConfiguration $requestConfiguration; + + /** + * Filter constructor. + */ + public function __construct(RequestConfiguration $requestConfiguration, string $code, string $label, string $minLabel, string $maxLabel, int $min, int $max) + { + $this->requestConfiguration = $requestConfiguration; + $this->code = $code; + $this->label = $label; + $this->minLabel = $minLabel; + $this->min = $min; + $this->max = $max; + + $this->addValue($minLabel, 0, (string) $min); + $this->addValue($maxLabel, 0, (string) $max); + } + + public function getCode(): string + { + return $this->code; + } + + public function getLabel(): string + { + return $this->label; + } + + public function addValue(string $label, int $count, ?string $value = null): void + { + $currentValueType = $this->getValueType($label); + $currentValues = $this->getCurrentValues(); + $isApplied = \array_key_exists($currentValueType, $currentValues); + $value = $isApplied ? $currentValues[$currentValueType] : $value; + + $this->values[] = new FilterValue($label, $count, $value, $isApplied); + } + + public function getValues(): array + { + return $this->values; + } + + public function getType(): string + { + return 'range'; + } + + public function getAppliedValues(): array + { + return array_filter($this->values, function (FilterValue $filterValue): bool { + return $filterValue->isApplied(); + }); + } + + public function getDefaultValue(string $type): int + { + if ('min' == $type) { + return $this->min; + } + + return $this->max; + } + + public function getValueType(string $valueLabel): string + { + if ($valueLabel == $this->minLabel) { + return 'min'; + } + + return 'max'; + } + + protected function getCurrentValues(): array + { + $appliedFilters = $this->requestConfiguration->getAppliedFilters(); + + return $appliedFilters[$this->getType()] ?? $appliedFilters[$this->getCode()] ?? $appliedFilters; + } +} diff --git a/src/Search/Request/Aggregation/AggregationBuilderInterface.php b/src/Search/Request/Aggregation/AggregationBuilderInterface.php new file mode 100644 index 00000000..f4501024 --- /dev/null +++ b/src/Search/Request/Aggregation/AggregationBuilderInterface.php @@ -0,0 +1,26 @@ + + * + * 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\Aggregation; + +use Elastica\Aggregation\AbstractAggregation; + +interface AggregationBuilderInterface +{ + /** + * @param string|array|object $aggregation + * + * @return AbstractAggregation|false|null + */ + public function build($aggregation, array $filters); +} diff --git a/src/Search/Request/Aggregation/MainTaxonAggregation.php b/src/Search/Request/Aggregation/MainTaxonAggregation.php new file mode 100644 index 00000000..c8750d97 --- /dev/null +++ b/src/Search/Request/Aggregation/MainTaxonAggregation.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\Aggregation; + +use Elastica\QueryBuilder; + +final class MainTaxonAggregation implements AggregationBuilderInterface +{ + public function build($aggregation, array $filters) + { + if (!$this->isSupported($aggregation)) { + return null; + } + + $qb = new QueryBuilder(); + $filters = array_filter($filters, function ($filter): bool { + return !$filter->hasParam('path') || 'main_taxon' !== $filter->getParam('path'); + }); + $filterQuery = $qb->query()->bool(); + foreach ($filters as $filter) { + $filterQuery->addMust($filter); + } + + return $qb->aggregation() + ->filter('main_taxon') + ->setFilter($filterQuery) + ->addAggregation( + $qb->aggregation() + ->nested('main_taxon', 'main_taxon') + ->addAggregation( + $qb->aggregation() + ->terms('codes') + ->setField('main_taxon.code') + ->addAggregation( + $qb->aggregation() + ->terms('levels') + ->setField('main_taxon.level') + ->addAggregation( + $qb->aggregation() + ->terms('names') + ->setField('main_taxon.name') + ) + ) + ) + ) + ; + } + + /** + * @param string|array|object $aggregation + */ + private function isSupported($aggregation): bool + { + return 'main_taxon' === $aggregation; + } +} diff --git a/src/Search/Request/Aggregation/PriceAggregation.php b/src/Search/Request/Aggregation/PriceAggregation.php new file mode 100644 index 00000000..4df477a9 --- /dev/null +++ b/src/Search/Request/Aggregation/PriceAggregation.php @@ -0,0 +1,76 @@ + + * + * 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\Aggregation; + +use Elastica\QueryBuilder; +use Sylius\Component\Channel\Context\ChannelContextInterface; + +final class PriceAggregation implements AggregationBuilderInterface +{ + private ChannelContextInterface $channelContext; + + public function __construct(ChannelContextInterface $channelContext) + { + $this->channelContext = $channelContext; + } + + public function build($aggregation, array $filters) + { + if (!$this->isSupported($aggregation)) { + return null; + } + + $qb = new QueryBuilder(); + $channelCode = $this->channelContext->getChannel()->getCode() ?? ''; + + $filters = array_filter($filters, function ($filter): bool { + return !$filter->hasParam('path') || 'prices' !== $filter->getParam('path'); + }); + + $filterQuery = $qb->query()->bool(); + foreach ($filters as $filter) { + $filterQuery->addMust($filter); + } + + return $qb->aggregation() + ->filter('prices') + ->setFilter($filterQuery) + ->addAggregation( + $qb->aggregation() + ->nested('prices', 'prices') + ->addAggregation( + $qb->aggregation() + ->filter('prices') + ->setFilter( + $qb->query()->term() + ->setTerm('prices.channel_code', $channelCode) + ) + ->addAggregation( + $qb->aggregation() + ->stats('prices_stats') + ->setField('prices.price') + ) + ) + ) + ; + } + + /** + * @param string|array|object $aggregation + */ + private function isSupported($aggregation): bool + { + return 'price' === $aggregation && null !== $this->channelContext->getChannel()->getCode(); + } +} diff --git a/src/Search/Request/Aggregation/ProductAttributeAggregation.php b/src/Search/Request/Aggregation/ProductAttributeAggregation.php new file mode 100644 index 00000000..b18fbed3 --- /dev/null +++ b/src/Search/Request/Aggregation/ProductAttributeAggregation.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\Aggregation; + +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Entity\Product\SearchableInterface; +use Sylius\Component\Product\Model\ProductAttributeInterface; + +final class ProductAttributeAggregation implements AggregationBuilderInterface +{ + public function build($aggregation, array $filters) + { + /** @var ProductAttributeInterface&SearchableInterface $aggregation */ + if (!$this->isSupport($aggregation) || !$aggregation->isFilterable()) { + return null; + } + + $qb = new QueryBuilder(); + $filters = array_filter($filters, function ($filter) use ($aggregation): bool { + return !$filter->hasParam('path') || ( + false !== strpos($filter->getParam('path'), 'attributes.') + && 'attributes.' . $aggregation->getCode() !== $filter->getParam('path') + ); + }); + + $filterQuery = $qb->query()->bool(); + foreach ($filters as $filter) { + $filterQuery->addMust($filter); + } + + /** @phpstan-ignore-next-line */ + return $qb->aggregation()->filter($aggregation->getCode()) + ->setFilter($filterQuery) + ->addAggregation( + /** @phpstan-ignore-next-line */ + $qb->aggregation()->nested($aggregation->getCode(), sprintf('attributes.%s', $aggregation->getCode())) + ->addAggregation( + $qb->aggregation()->terms('names') + ->setField(sprintf('attributes.%s.name', $aggregation->getCode())) + ->addAggregation( + $qb->aggregation()->terms('values') + ->setField(sprintf('attributes.%s.value.keyword', $aggregation->getCode())) + ) + ) + ) + ; + } + + /** + * @param string|array|object $aggregation + */ + private function isSupport($aggregation): bool + { + return $aggregation instanceof ProductAttributeInterface && null !== $aggregation->getCode(); + } +} diff --git a/src/Search/Request/Aggregation/ProductAttributesAggregation.php b/src/Search/Request/Aggregation/ProductAttributesAggregation.php new file mode 100644 index 00000000..760ef334 --- /dev/null +++ b/src/Search/Request/Aggregation/ProductAttributesAggregation.php @@ -0,0 +1,82 @@ + + * + * 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\Aggregation; + +use Elastica\Query\AbstractQuery; +use Elastica\QueryBuilder; +use Sylius\Component\Product\Model\ProductAttributeInterface; + +final class ProductAttributesAggregation implements AggregationBuilderInterface +{ + private ProductAttributeAggregation $productAttributeAggregationBuilder; + + public function __construct() + { + $this->productAttributeAggregationBuilder = new ProductAttributeAggregation(); + } + + public function build($aggregation, array $filters) + { + if (!$this->isSupport($aggregation)) { + return null; + } + + $qb = new QueryBuilder(); + + $currentFilters = array_filter($filters, function (AbstractQuery $filter): bool { + return !$filter->hasParam('path') || false === strpos($filter->getParam('path'), 'attributes.'); + }); + + $filterQuery = $qb->query()->bool(); + foreach ($currentFilters as $filter) { + $filterQuery->addMust($filter); + } + + $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) { + continue; + } + $attributesAggregation->addAggregation($subAggregationObject); + } + + if (0 == \count($attributesAggregation->getAggs())) { + return false; + } + + return $qb->aggregation()->filter('attributes') + ->setFilter($filterQuery) + ->addAggregation($attributesAggregation) + ; + } + + /** + * @param string|array|object $aggregation + */ + private function isSupport($aggregation): bool + { + if (!\is_array($aggregation)) { + return false; + } + foreach ($aggregation as $subAggregation) { + if ($subAggregation instanceof ProductAttributeInterface) { + return true; + } + } + + return false; + } +} diff --git a/src/Search/Request/Aggregation/ProductOptionAggregation.php b/src/Search/Request/Aggregation/ProductOptionAggregation.php new file mode 100644 index 00000000..9c4f2e85 --- /dev/null +++ b/src/Search/Request/Aggregation/ProductOptionAggregation.php @@ -0,0 +1,92 @@ + + * + * 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\Aggregation; + +use Elastica\Query\AbstractQuery; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Entity\Product\SearchableInterface; +use Sylius\Component\Product\Model\ProductOptionInterface; + +final class ProductOptionAggregation implements AggregationBuilderInterface +{ + private bool $enableStockFilter; + + public function __construct(bool $enableStockFilter) + { + $this->enableStockFilter = $enableStockFilter; + } + + public function build($aggregation, array $filters) + { + /** @var ProductOptionInterface&SearchableInterface $aggregation */ + if (!$this->isSupport($aggregation) || !$aggregation->isFilterable()) { + return null; + } + + $qb = new QueryBuilder(); + + $filters = array_filter($filters, function (AbstractQuery $filter) use ($aggregation): bool { + return !$filter->hasParam('path') || ( + false !== strpos($filter->getParam('path'), 'options.') + && 'options.' . $aggregation->getCode() . '.values' !== $filter->getParam('path') + ); + }); + + $filterQuery = $qb->query()->bool(); + foreach ($filters as $filter) { + $filterQuery->addMust($filter); + } + + $qb = new QueryBuilder(); + $optionBoolConditions = $qb->query()->bool() + ->addMust($qb->query()->term([sprintf('options.%s.values.enabled', $aggregation->getCode()) => ['value' => true]])) + ; + if ($this->enableStockFilter) { + $optionBoolConditions->addMust($qb->query()->term([sprintf('options.%s.values.is_in_stock', $aggregation->getCode()) => ['value' => true]])); + } + $valuesAggregation = $qb->aggregation()->filter('values', $optionBoolConditions) + ->addAggregation( + $qb->aggregation()->terms('values') + ->setField(sprintf('options.%s.values.value.keyword', $aggregation->getCode())) + ) + ; + + /** @phpstan-ignore-next-line */ + return $qb->aggregation()->filter($aggregation->getCode()) + ->setFilter($filterQuery) + ->addAggregation( + /** @phpstan-ignore-next-line */ + $qb->aggregation()->nested($aggregation->getCode(), sprintf('options.%s', $aggregation->getCode())) + ->addAggregation( + $qb->aggregation()->terms('names') + ->setField(sprintf('options.%s.name', $aggregation->getCode())) + ->addAggregation( + $qb->aggregation()->nested('values', sprintf('options.%s.values', $aggregation->getCode())) + ->addAggregation( + $valuesAggregation + ) + ) + ) + ) + ; + } + + /** + * @param string|array|object $aggregation + */ + private function isSupport($aggregation): bool + { + return $aggregation instanceof ProductOptionInterface && null !== $aggregation->getCode(); + } +} diff --git a/src/Search/Request/Aggregation/ProductOptionsAggregation.php b/src/Search/Request/Aggregation/ProductOptionsAggregation.php new file mode 100644 index 00000000..7c5971e7 --- /dev/null +++ b/src/Search/Request/Aggregation/ProductOptionsAggregation.php @@ -0,0 +1,81 @@ + + * + * 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\Aggregation; + +use Elastica\Query\AbstractQuery; +use Elastica\QueryBuilder; +use Sylius\Component\Product\Model\ProductOptionInterface; + +final class ProductOptionsAggregation implements AggregationBuilderInterface +{ + private ProductOptionAggregation $productOptionAggregationBuilder; + + public function __construct(ProductOptionAggregation $productOptionAggregationBuilder) + { + $this->productOptionAggregationBuilder = $productOptionAggregationBuilder; + } + + public function build($aggregation, array $filters) + { + if (!$this->isSupport($aggregation)) { + return null; + } + + $qb = new QueryBuilder(); + $currentFilters = array_filter($filters, function (AbstractQuery $filter): bool { + return !$filter->hasParam('path') || false === strpos($filter->getParam('path'), 'options.'); + }); + $filterQuery = $qb->query()->bool(); + foreach ($currentFilters as $filter) { + $filterQuery->addMust($filter); + } + + $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) { + continue; + } + $optionsAggregation->addAggregation($subAggregationObject); + } + + if (0 == \count($optionsAggregation->getAggs())) { + return false; + } + + return $qb->aggregation()->filter('options') + ->setFilter($filterQuery) + ->addAggregation($optionsAggregation) + ; + } + + /** + * @param string|array|object $aggregation + */ + private function isSupport($aggregation): bool + { + if (!\is_array($aggregation)) { + return false; + } + + foreach ($aggregation as $subAggregation) { + if ($subAggregation instanceof ProductOptionInterface) { + return true; + } + } + + return false; + } +} diff --git a/src/Search/Request/Aggregation/TaxonsAggregation.php b/src/Search/Request/Aggregation/TaxonsAggregation.php new file mode 100644 index 00000000..79acc0b9 --- /dev/null +++ b/src/Search/Request/Aggregation/TaxonsAggregation.php @@ -0,0 +1,74 @@ + + * + * 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\Aggregation; + +use Elastica\QueryBuilder; +use Sylius\Component\Core\Model\TaxonInterface; + +final class TaxonsAggregation implements AggregationBuilderInterface +{ + public function build($aggregation, array $filters) + { + if (!$this->isSupported($aggregation)) { + return null; + } + /** @var TaxonInterface $currentTaxon */ + $currentTaxon = $aggregation['taxons']; /** @phpstan-ignore-line */ + $qb = new QueryBuilder(); + + $filters = array_filter($filters, function ($filter): bool { + return !$filter->hasParam('path') || 'product_taxons' !== $filter->getParam('path'); + }); + + $filterQuery = $qb->query()->bool(); + foreach ($filters as $filter) { + $filterQuery->addMust($filter); + } + $taxonLevel = $currentTaxon->getLevel() ?? 0; + + return $qb->aggregation() + ->filter('taxons') + ->setFilter($filterQuery) + ->addAggregation( + $qb->aggregation() + ->nested('taxons', 'product_taxons') + ->addAggregation( + $qb->aggregation() + ->nested('taxons', 'product_taxons.taxon') + ->addAggregation( + $qb->aggregation() + ->filter('taxons', $qb->query()->term(['product_taxons.taxon.level' => ['value' => $taxonLevel + 1]])) + ->addAggregation( + $qb->aggregation() + ->terms('codes') + ->setField('product_taxons.taxon.code') + ->addAggregation( + $qb->aggregation()->terms('names') + ->setField('product_taxons.taxon.name') + ) + ) + ) + ) + ) + ; + } + + /** + * @param string|array|object $aggregation + */ + private function isSupported($aggregation): bool + { + return \is_array($aggregation) && \array_key_exists('taxons', $aggregation); + } +} diff --git a/src/Search/Request/AggregationBuilder.php b/src/Search/Request/AggregationBuilder.php new file mode 100644 index 00000000..79cafbbb --- /dev/null +++ b/src/Search/Request/AggregationBuilder.php @@ -0,0 +1,63 @@ + + * + * 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\Aggregation\AbstractAggregation; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Aggregation\AggregationBuilderInterface; +use RuntimeException; + +class AggregationBuilder +{ + /** + * @var iterable + */ + private iterable $aggregationBuilders; + + public function __construct(iterable $aggregationBuilders) + { + $this->aggregationBuilders = $aggregationBuilders; + } + + public function buildAggregations(array $aggregations, array $filters): array + { + $buckets = []; + + foreach ($aggregations as $aggregation) { + $aggregationQuery = $this->buildAggregation($aggregation, $filters); + if (false === $aggregationQuery) { + continue; + } + $buckets[] = $aggregationQuery; + } + + return array_filter($buckets); + } + + /** + * @param string|array $aggregation + * + * @return AbstractAggregation|bool + */ + private function buildAggregation($aggregation, array $filters) + { + foreach ($this->aggregationBuilders as $aggregationBuilder) { + $aggregationQuery = $aggregationBuilder->build($aggregation, $filters); + if (null !== $aggregationQuery) { + return $aggregationQuery; + } + } + + throw new RuntimeException('Aggregation can be build'); // it's throw an exception if we have not filtreable attribute + } +} diff --git a/src/Search/Request/FunctionScore/FunctionScoreInterface.php b/src/Search/Request/FunctionScore/FunctionScoreInterface.php new file mode 100644 index 00000000..46ad3a00 --- /dev/null +++ b/src/Search/Request/FunctionScore/FunctionScoreInterface.php @@ -0,0 +1,22 @@ + + * + * 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\FunctionScore; + +use Elastica\Query\FunctionScore; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +interface FunctionScoreInterface +{ + public function addFunctionScore(FunctionScore $functionScore, RequestConfiguration $requestConfiguration): void; +} diff --git a/src/Search/Request/FunctionScore/FunctionScoreRegistryInterface.php b/src/Search/Request/FunctionScore/FunctionScoreRegistryInterface.php new file mode 100644 index 00000000..8330bc7a --- /dev/null +++ b/src/Search/Request/FunctionScore/FunctionScoreRegistryInterface.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 MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore; + +use Sylius\Component\Registry\ServiceRegistryInterface; + +interface FunctionScoreRegistryInterface extends ServiceRegistryInterface +{ +} diff --git a/src/Search/Request/FunctionScore/Product/InStockWeightFunction.php b/src/Search/Request/FunctionScore/Product/InStockWeightFunction.php new file mode 100644 index 00000000..fd7058e9 --- /dev/null +++ b/src/Search/Request/FunctionScore/Product/InStockWeightFunction.php @@ -0,0 +1,56 @@ + + * + * 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\FunctionScore\Product; + +use Elastica\Query\FunctionScore; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore\FunctionScoreInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +class InStockWeightFunction implements FunctionScoreInterface +{ + private bool $enableStockFilter; + + private int $inStockWeight; + + private array $applyOnRequestTypes; + + public function __construct( + bool $enableStockFilter, + int $inStockWeight, + array $applyOnRequestTypes + ) { + $this->enableStockFilter = $enableStockFilter; + $this->inStockWeight = $inStockWeight; + $this->applyOnRequestTypes = $applyOnRequestTypes; + } + + public function addFunctionScore(FunctionScore $functionScore, RequestConfiguration $requestConfiguration): void + { + if ( + $this->enableStockFilter + || 1 > $this->inStockWeight + || !\in_array($requestConfiguration->getType(), $this->applyOnRequestTypes, true) + ) { + return; + } + + $qb = new QueryBuilder(); + $functionScore->addWeightFunction( + $this->inStockWeight, + $qb->query()->nested()->setPath('variants') + ->setQuery($qb->query()->term(['variants.is_in_stock' => true])) + ); + } +} diff --git a/src/Search/Request/FunctionScore/ProductFunctionScoreRegistry.php b/src/Search/Request/FunctionScore/ProductFunctionScoreRegistry.php new file mode 100644 index 00000000..fc25bc65 --- /dev/null +++ b/src/Search/Request/FunctionScore/ProductFunctionScoreRegistry.php @@ -0,0 +1,28 @@ + + * + * 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\FunctionScore; + +use Sylius\Component\Registry\ServiceRegistry; + +final class ProductFunctionScoreRegistry extends ServiceRegistry implements FunctionScoreRegistryInterface +{ + public function __construct(array $functionsScore = []) + { + parent::__construct(FunctionScoreInterface::class, 'monsieurbiz.search'); + + foreach ($functionsScore as $functionScore) { + $this->register(\get_class($functionScore), $functionScore); + } + } +} diff --git a/src/Search/Request/PostFilter/PostFilterInterface.php b/src/Search/Request/PostFilter/PostFilterInterface.php new file mode 100644 index 00000000..c3dd4d63 --- /dev/null +++ b/src/Search/Request/PostFilter/PostFilterInterface.php @@ -0,0 +1,22 @@ + + * + * 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\PostFilter; + +use Elastica\Query\BoolQuery; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +interface PostFilterInterface +{ + public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void; +} diff --git a/src/Context/TaxonContextInterface.php b/src/Search/Request/PostFilter/PostFilterRegistryInterface.php similarity index 56% rename from src/Context/TaxonContextInterface.php rename to src/Search/Request/PostFilter/PostFilterRegistryInterface.php index 237733af..74037c55 100644 --- a/src/Context/TaxonContextInterface.php +++ b/src/Search/Request/PostFilter/PostFilterRegistryInterface.php @@ -5,17 +5,16 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * 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\Context; +namespace MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter; -use Sylius\Component\Core\Model\TaxonInterface; +use Sylius\Component\Registry\ServiceRegistryInterface; -interface TaxonContextInterface +interface PostFilterRegistryInterface extends ServiceRegistryInterface { - public function getTaxon(): TaxonInterface; } diff --git a/src/Search/Request/PostFilter/Product/AttributesPostFilter.php b/src/Search/Request/PostFilter/Product/AttributesPostFilter.php new file mode 100644 index 00000000..c7209a7a --- /dev/null +++ b/src/Search/Request/PostFilter/Product/AttributesPostFilter.php @@ -0,0 +1,41 @@ + + * + * 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\PostFilter\Product; + +use Elastica\Query\BoolQuery; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Helper\SlugHelper; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\PostFilterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +final class AttributesPostFilter implements PostFilterInterface +{ + public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void + { + $qb = new QueryBuilder(); + foreach ($requestConfiguration->getAppliedFilters('attributes') as $field => $values) { + $attributeValueQuery = $qb->query()->bool(); + + foreach ($values as $value) { + $termQuery = $qb->query()->term([sprintf('attributes.%s.value.keyword', $field) => SlugHelper::toLabel($value)]); + $attributeValueQuery->addShould($termQuery); // todo configure the "and" or "or" + } + + $attributeQuery = $qb->query()->nested(); + $attributeQuery->setPath(sprintf('attributes.%s', $field))->setQuery($attributeValueQuery); + + $boolQuery->addMust($attributeQuery); + } + } +} diff --git a/src/Search/Request/PostFilter/Product/MainTaxonPostFilter.php b/src/Search/Request/PostFilter/Product/MainTaxonPostFilter.php new file mode 100644 index 00000000..c8ced4c8 --- /dev/null +++ b/src/Search/Request/PostFilter/Product/MainTaxonPostFilter.php @@ -0,0 +1,49 @@ + + * + * 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\PostFilter\Product; + +use Elastica\Query\BoolQuery; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Helper\SlugHelper; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\PostFilterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +final class MainTaxonPostFilter implements PostFilterInterface +{ + public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void + { + $qb = new QueryBuilder(); + foreach ($requestConfiguration->getAppliedFilters('taxon') as $field => $values) { + $mainTaxonQuery = $qb->query() + ->bool() + ; + $values = array_filter($values) ?? []; + foreach ($values as $value) { + $mainTaxonQuery->addShould( + $qb->query() + ->term() + ->setTerm(sprintf('%s.code', $field), SlugHelper::toLabel($value)) + ); + } + $boolQuery->addMust( + $qb->query() + ->nested() + ->setPath($field) + ->setQuery( + $mainTaxonQuery + ) + ); + } + } +} diff --git a/src/Search/Request/PostFilter/Product/OptionsPostFilter.php b/src/Search/Request/PostFilter/Product/OptionsPostFilter.php new file mode 100644 index 00000000..56217bc0 --- /dev/null +++ b/src/Search/Request/PostFilter/Product/OptionsPostFilter.php @@ -0,0 +1,54 @@ + + * + * 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\PostFilter\Product; + +use Elastica\Query\BoolQuery; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Helper\SlugHelper; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\PostFilterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +final class OptionsPostFilter implements PostFilterInterface +{ + private bool $enableStockFilter; + + public function __construct(bool $enableStockFilter) + { + $this->enableStockFilter = $enableStockFilter; + } + + public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void + { + $qb = new QueryBuilder(); + foreach ($requestConfiguration->getAppliedFilters('options') as $field => $values) { + $optionValueQuery = $qb->query()->bool(); + foreach ($values as $value) { + $termQuery = $qb->query()->term([sprintf('options.%s.values.value.keyword', $field) => SlugHelper::toLabel($value)]); + $optionValueQuery->addShould($termQuery); // todo configure the "and" or "or" + } + + $optionQuery = $qb->query()->nested(); + $condition = $qb->query()->bool() + ->addMust($qb->query()->term([sprintf('options.%s.values.enabled', $field) => true])) + ; + if ($this->enableStockFilter) { + $condition->addMust($qb->query()->term([sprintf('options.%s.values.is_in_stock', $field) => true])); + } + $condition->addMust($optionValueQuery); + $optionQuery->setPath(sprintf('options.%s.values', $field))->setQuery($condition); + + $boolQuery->addMust($optionQuery); + } + } +} diff --git a/src/Search/Request/PostFilter/Product/PricePostFilter.php b/src/Search/Request/PostFilter/Product/PricePostFilter.php new file mode 100644 index 00000000..d4b4514c --- /dev/null +++ b/src/Search/Request/PostFilter/Product/PricePostFilter.php @@ -0,0 +1,62 @@ + + * + * 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\PostFilter\Product; + +use Elastica\Query\BoolQuery; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\PostFilterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use Sylius\Component\Channel\Context\ChannelContextInterface; + +final class PricePostFilter implements PostFilterInterface +{ + private ChannelContextInterface $channelContext; + + public function __construct(ChannelContextInterface $channelContext) + { + $this->channelContext = $channelContext; + } + + public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void + { + $qb = new QueryBuilder(); + $priceValue = $requestConfiguration->getAppliedFilters('price'); + if (0 !== \count($priceValue)) { + $channelPriceFilter = $qb->query() + ->term(['prices.channel_code' => $this->channelContext->getChannel()->getCode()]) + ; + $conditions = []; + if (\array_key_exists('min', $priceValue)) { + $conditions['gte'] = $priceValue['min'] * 100; + } + if (\array_key_exists('max', $priceValue)) { + $conditions['lte'] = $priceValue['max'] * 100; + } + $priceQuery = $qb->query() + ->range('prices.price', $conditions) + ; + + $boolQuery->addMust( + $qb->query() + ->nested() + ->setPath('prices') + ->setQuery( + $qb->query()->bool() + ->addMust($channelPriceFilter) + ->addMust($priceQuery) + ) + ); + } + } +} diff --git a/src/Search/Request/PostFilter/Product/ProductTaxonPostFilter.php b/src/Search/Request/PostFilter/Product/ProductTaxonPostFilter.php new file mode 100644 index 00000000..656c96eb --- /dev/null +++ b/src/Search/Request/PostFilter/Product/ProductTaxonPostFilter.php @@ -0,0 +1,52 @@ + + * + * 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\PostFilter\Product; + +use Elastica\Query\BoolQuery; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Helper\SlugHelper; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\PostFilterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +final class ProductTaxonPostFilter implements PostFilterInterface +{ + public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void + { + $qb = new QueryBuilder(); + $taxonsSelected = $requestConfiguration->getAppliedFilters('taxons'); + if (0 !== \count($taxonsSelected)) { + $taxonQuery = $qb->query() + ->bool() + ; + foreach ($taxonsSelected as $value) { + $taxonQuery->addShould( + $qb->query() + ->term() + ->setTerm('product_taxons.taxon.code', SlugHelper::toLabel($value)) + ); + } + + $boolQuery->addMust( + $qb->query() + ->nested() + ->setPath('product_taxons') + ->setQuery( + $qb->query()->nested() + ->setPath('product_taxons.taxon') + ->setQuery($taxonQuery) + ) + ); + } + } +} diff --git a/src/Search/Request/PostFilter/ProductTaxonRegistry.php b/src/Search/Request/PostFilter/ProductTaxonRegistry.php new file mode 100644 index 00000000..8cdc1b25 --- /dev/null +++ b/src/Search/Request/PostFilter/ProductTaxonRegistry.php @@ -0,0 +1,28 @@ + + * + * 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\PostFilter; + +use Sylius\Component\Registry\ServiceRegistry; + +final class ProductTaxonRegistry extends ServiceRegistry implements PostFilterRegistryInterface +{ + public function __construct(array $queryFilters = []) + { + parent::__construct(PostFilterInterface::class, 'monsieurbiz.search'); + + foreach ($queryFilters as $queryFilter) { + $this->register(\get_class($queryFilter), $queryFilter); + } + } +} diff --git a/src/Search/Request/ProductRequest/InstantSearch.php b/src/Search/Request/ProductRequest/InstantSearch.php new file mode 100644 index 00000000..0cfebcd3 --- /dev/null +++ b/src/Search/Request/ProductRequest/InstantSearch.php @@ -0,0 +1,97 @@ + + * + * 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\ProductRequest; + +use Elastica\Query; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\FunctionScore\FunctionScoreRegistryInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterRegistryInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface; +use RuntimeException; +use Sylius\Component\Registry\ServiceRegistryInterface; + +final class InstantSearch implements RequestInterface +{ + private DocumentableInterface $documentable; + + private ?RequestConfiguration $configuration; + + private QueryFilterRegistryInterface $queryFilterRegistry; + + private FunctionScoreRegistryInterface $functionScoreRegistry; + + public function __construct( + ServiceRegistryInterface $documentableRegistry, + QueryFilterRegistryInterface $queryFilterRegistry, + FunctionScoreRegistryInterface $functionScoreRegistry + ) { + /** @var DocumentableInterface $documentable */ + $documentable = $documentableRegistry->get('search.documentable.monsieurbiz_product'); + $this->documentable = $documentable; + $this->queryFilterRegistry = $queryFilterRegistry; + $this->functionScoreRegistry = $functionScoreRegistry; + } + + public function getType(): string + { + return RequestInterface::INSTANT_TYPE; + } + + public function getDocumentable(): DocumentableInterface + { + return $this->documentable; + } + + public function getQuery(): Query + { + if (null === $this->configuration || '' === $this->configuration->getQueryText()) { + throw new RuntimeException('missing query text'); + } + + $qb = new QueryBuilder(); + $boolQuery = $qb->query()->bool(); + foreach ($this->queryFilterRegistry->all() as $queryFilter) { + $queryFilter->apply($boolQuery, $this->configuration); + } + + $query = Query::create($boolQuery); + + /** @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->functionScoreRegistry->all() as $functionScoreClass) { + $functionScoreClass->addFunctionScore($functionScore, $this->configuration); + } + + $query->setQuery($functionScore); + + return $query; + } + + public function supports(string $type, string $documentableCode): bool + { + return RequestInterface::INSTANT_TYPE == $type && $this->getDocumentable()->getIndexCode() == $documentableCode; + } + + public function setConfiguration(RequestConfiguration $configuration): void + { + $this->configuration = $configuration; + } +} diff --git a/src/Search/Request/ProductRequest/Search.php b/src/Search/Request/ProductRequest/Search.php new file mode 100644 index 00000000..bec028a7 --- /dev/null +++ b/src/Search/Request/ProductRequest/Search.php @@ -0,0 +1,151 @@ + + * + * 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\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\FunctionScoreRegistryInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\PostFilterRegistryInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterRegistryInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterRegistryInterface; +use RuntimeException; +use Sylius\Component\Registry\ServiceRegistryInterface; + +final class Search implements RequestInterface +{ + private DocumentableInterface $documentable; + + private RequestConfiguration $configuration; + + private ProductAttributeRepositoryInterface $productAttributeRepository; + + private ProductOptionRepositoryInterface $productOptionRepository; + + private AggregationBuilder $aggregationBuilder; + + private QueryFilterRegistryInterface $queryFilterRegistry; + + private PostFilterRegistryInterface $postFilterRegistry; + + private SorterRegistryInterface $sorterRegistry; + + private FunctionScoreRegistryInterface $functionScoreRegistry; + + public function __construct( + ServiceRegistryInterface $documentableRegistry, + ProductAttributeRepositoryInterface $productAttributeRepository, + ProductOptionRepositoryInterface $productOptionRepository, + AggregationBuilder $aggregationBuilder, + QueryFilterRegistryInterface $queryFilterRegistry, + PostFilterRegistryInterface $postFilterRegistry, + SorterRegistryInterface $sorterRegistry, + FunctionScoreRegistryInterface $functionScoreRegistry + ) { + /** @var DocumentableInterface $documentable */ + $documentable = $documentableRegistry->get('search.documentable.monsieurbiz_product'); + $this->documentable = $documentable; + $this->productAttributeRepository = $productAttributeRepository; + $this->productOptionRepository = $productOptionRepository; + $this->aggregationBuilder = $aggregationBuilder; + $this->queryFilterRegistry = $queryFilterRegistry; + $this->postFilterRegistry = $postFilterRegistry; + $this->sorterRegistry = $sorterRegistry; + $this->functionScoreRegistry = $functionScoreRegistry; + } + + 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 + { + if ('' === $this->configuration->getQueryText()) { + throw new RuntimeException('missing query text'); + } + + $qb = new QueryBuilder(); + + $boolQuery = $qb->query()->bool(); + foreach ($this->queryFilterRegistry->all() as $queryFilter) { + $queryFilter->apply($boolQuery, $this->configuration); + } + + $query = Query::create($boolQuery); + $postFilter = new Query\BoolQuery(); + foreach ($this->postFilterRegistry->all() as $postFilterApplier) { + $postFilterApplier->apply($postFilter, $this->configuration); + } + $query->setPostFilter($postFilter); + + $this->addAggregations($query, $postFilter); + + foreach ($this->sorterRegistry->all() 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->functionScoreRegistry->all() 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 + { + $aggregations = $this->aggregationBuilder->buildAggregations( + [ + 'main_taxon', + 'price', + $this->productAttributeRepository->findIsSearchableOrFilterable(), + $this->productOptionRepository->findIsSearchableOrFilterable(), + ], + $postFilter->hasParam('must') ? $postFilter->getParam('must') : [] + ); + + foreach ($aggregations as $aggregation) { + $query->addAggregation($aggregation); + } + } +} diff --git a/src/Search/Request/ProductRequest/Taxon.php b/src/Search/Request/ProductRequest/Taxon.php new file mode 100644 index 00000000..014f460c --- /dev/null +++ b/src/Search/Request/ProductRequest/Taxon.php @@ -0,0 +1,155 @@ + + * + * 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\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\FunctionScoreRegistryInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\PostFilter\PostFilterRegistryInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterRegistryInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterRegistryInterface; +use RuntimeException; +use Sylius\Component\Channel\Context\ChannelContextInterface; +use Sylius\Component\Registry\ServiceRegistryInterface; + +final class Taxon implements RequestInterface +{ + private DocumentableInterface $documentable; + + private ProductAttributeRepositoryInterface $productAttributeRepository; + + private ProductOptionRepositoryInterface $productOptionRepository; + + private ChannelContextInterface $channelContext; + + private AggregationBuilder $aggregationBuilder; + + private ?RequestConfiguration $configuration; + + private QueryFilterRegistryInterface $queryFilterRegistry; + + private PostFilterRegistryInterface $postFilterRegistry; + + private SorterRegistryInterface $sorterRegistry; + + private FunctionScoreRegistryInterface $functionScoreRegistry; + + public function __construct( + ServiceRegistryInterface $documentableRegistry, + ProductAttributeRepositoryInterface $productAttributeRepository, + ProductOptionRepositoryInterface $productOptionRepository, + ChannelContextInterface $channelContext, + AggregationBuilder $aggregationBuilder, + QueryFilterRegistryInterface $queryFilterRegistry, + PostFilterRegistryInterface $postFilterRegistry, + SorterRegistryInterface $sorterRegistry, + FunctionScoreRegistryInterface $functionScoreRegistry + ) { + /** @var DocumentableInterface $documentable */ + $documentable = $documentableRegistry->get('search.documentable.monsieurbiz_product'); + $this->documentable = $documentable; + $this->productAttributeRepository = $productAttributeRepository; + $this->productOptionRepository = $productOptionRepository; + $this->channelContext = $channelContext; + $this->aggregationBuilder = $aggregationBuilder; + $this->queryFilterRegistry = $queryFilterRegistry; + $this->postFilterRegistry = $postFilterRegistry; + $this->sorterRegistry = $sorterRegistry; + $this->functionScoreRegistry = $functionScoreRegistry; + } + + 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->queryFilterRegistry->all() as $queryFilter) { + $queryFilter->apply($boolQuery, $this->configuration); + } + $query = Query::create($boolQuery); + $postFilter = new Query\BoolQuery(); + + foreach ($this->postFilterRegistry->all() as $postFilterApplier) { + $postFilterApplier->apply($postFilter, $this->configuration); + } + $query->setPostFilter($postFilter); + + $this->addAggregations($query, $postFilter); + + foreach ($this->sorterRegistry->all() 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->functionScoreRegistry->all() 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 + { + if (null === $this->configuration) { + throw new RuntimeException('Missing request configuration'); + } + $aggregations = $this->aggregationBuilder->buildAggregations( + [ + ['taxons' => $this->configuration->getTaxon()], + 'price', + $this->productAttributeRepository->findIsSearchableOrFilterable(), + $this->productOptionRepository->findIsSearchableOrFilterable(), + ], + $postFilter->hasParam('must') ? $postFilter->getParam('must') : [] + ); + + foreach ($aggregations as $aggregation) { + $query->addAggregation($aggregation); + } + } +} diff --git a/src/Search/Request/QueryFilter/Product/ChannelFilter.php b/src/Search/Request/QueryFilter/Product/ChannelFilter.php new file mode 100644 index 00000000..55c53792 --- /dev/null +++ b/src/Search/Request/QueryFilter/Product/ChannelFilter.php @@ -0,0 +1,43 @@ + + * + * 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\Product; + +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; + +final class ChannelFilter implements QueryFilterInterface +{ + private ChannelContextInterface $channelContext; + + public function __construct(ChannelContextInterface $channelContext) + { + $this->channelContext = $channelContext; + } + + public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void + { + $qb = new QueryBuilder(); + + $boolQuery->addFilter( + $qb->query()->nested() + ->setPath('channels') + ->setQuery( + $qb->query()->term(['channels.code' => ['value' => $this->channelContext->getChannel()->getCode()]]) + ) + ); + } +} diff --git a/src/Search/Request/QueryFilter/Product/EnabledFilter.php b/src/Search/Request/QueryFilter/Product/EnabledFilter.php new file mode 100644 index 00000000..3c5783da --- /dev/null +++ b/src/Search/Request/QueryFilter/Product/EnabledFilter.php @@ -0,0 +1,31 @@ + + * + * 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\Product; + +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 +{ + public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void + { + $qb = new QueryBuilder(); + + $boolQuery->addFilter( + $qb->query()->term(['enabled' => ['value' => true]]) + ); + } +} diff --git a/src/Search/Request/QueryFilter/Product/IsInStockFilter.php b/src/Search/Request/QueryFilter/Product/IsInStockFilter.php new file mode 100644 index 00000000..c023d5ad --- /dev/null +++ b/src/Search/Request/QueryFilter/Product/IsInStockFilter.php @@ -0,0 +1,45 @@ + + * + * 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\Product; + +use Elastica\Query\BoolQuery; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +class IsInStockFilter implements QueryFilterInterface +{ + private bool $enableStockFilter; + + public function __construct(bool $enableStockFilter) + { + $this->enableStockFilter = $enableStockFilter; + } + + public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void + { + if (!$this->enableStockFilter) { + return; + } + + $qb = new QueryBuilder(); + $boolQuery->addFilter( + $qb->query()->nested() + ->setPath('variants') + ->setQuery( + $qb->query()->term(['variants.is_in_stock' => ['value' => true]]) + ) + ); + } +} diff --git a/src/Search/Request/QueryFilter/Product/SearchTermFilter.php b/src/Search/Request/QueryFilter/Product/SearchTermFilter.php new file mode 100644 index 00000000..512d3f0b --- /dev/null +++ b/src/Search/Request/QueryFilter/Product/SearchTermFilter.php @@ -0,0 +1,115 @@ + + * + * 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\Product; + +use Elastica\Query\BoolQuery; +use Elastica\Query\MultiMatch; +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\RequestConfiguration; + +final class SearchTermFilter implements QueryFilterInterface +{ + private ProductAttributeRepositoryInterface $productAttributeRepository; + + private ProductOptionRepositoryInterface $productOptionRepository; + + private array $fieldsToSearch; + + public function __construct( + ProductAttributeRepositoryInterface $productAttributeRepository, + ProductOptionRepositoryInterface $productOptionRepository, + array $fieldsToSearch + ) { + $this->productAttributeRepository = $productAttributeRepository; + $this->productOptionRepository = $productOptionRepository; + $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->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 + { + $qb = new QueryBuilder(); + foreach ($this->productAttributeRepository->findIsSearchableOrFilterable() as $productAttribute) { + if (!$productAttribute->isSearchable()) { + continue; + } + + $attributeValueQuery = $qb->query()->multi_match(); + $attributeValueQuery->setFields([ + sprintf('attributes.%s.value^%d', $productAttribute->getCode(), $productAttribute->getSearchWeight()), + ]); + $attributeValueQuery->setQuery($requestConfiguration->getQueryText()); + $attributeValueQuery->setFuzziness(MultiMatch::FUZZINESS_AUTO); + + $attributeQuery = $qb->query()->nested(); + $attributeQuery->setPath(sprintf('attributes.%s', $productAttribute->getCode()))->setQuery($attributeValueQuery); + + $searchQuery->addShould($attributeQuery); + } + } + + private function addOptionsQueries(BoolQuery $searchQuery, RequestConfiguration $requestConfiguration): void + { + $qb = new QueryBuilder(); + foreach ($this->productOptionRepository->findIsSearchableOrFilterable() as $productOption) { + if (!$productOption->isSearchable()) { + continue; + } + + $attributeValueQuery = $qb->query()->multi_match(); + $attributeValueQuery->setFields([ + sprintf('options.%s.values.value^%d', $productOption->getCode(), $productOption->getSearchWeight()), + ]); + $attributeValueQuery->setQuery($requestConfiguration->getQueryText()); + $attributeValueQuery->setFuzziness(MultiMatch::FUZZINESS_AUTO); + + $attributeQuery = $qb->query()->nested(); + $attributeQuery->setPath(sprintf('options.%s.values', $productOption->getCode()))->setQuery($attributeValueQuery); + + $searchQuery->addShould($attributeQuery); + } + } +} diff --git a/src/Search/Request/QueryFilter/Product/TaxonFilter.php b/src/Search/Request/QueryFilter/Product/TaxonFilter.php new file mode 100644 index 00000000..bd83599c --- /dev/null +++ b/src/Search/Request/QueryFilter/Product/TaxonFilter.php @@ -0,0 +1,42 @@ + + * + * 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\Product; + +use Elastica\Query\BoolQuery; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter\QueryFilterInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +final class TaxonFilter implements QueryFilterInterface +{ + public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void + { + $qb = new QueryBuilder(); + $searchQuery = $qb->query()->nested() + ->setPath('product_taxons') + ->setQuery( + $qb->query()->nested() + ->setPath('product_taxons.taxon') + ->setQuery( + $qb->query()->term(['product_taxons.taxon.code' => ['value' => $requestConfiguration->getTaxon()->getCode()]]) + ) + ) + ; + if ($requestConfiguration->getTaxon()->isRoot()) { + $searchQuery = $qb->query()->bool(); + } + + $boolQuery->addMust($searchQuery); + } +} diff --git a/src/Search/Request/QueryFilter/ProductSearchRegistry.php b/src/Search/Request/QueryFilter/ProductSearchRegistry.php new file mode 100644 index 00000000..ce7f01e7 --- /dev/null +++ b/src/Search/Request/QueryFilter/ProductSearchRegistry.php @@ -0,0 +1,28 @@ + + * + * 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 Sylius\Component\Registry\ServiceRegistry; + +final class ProductSearchRegistry extends ServiceRegistry implements QueryFilterRegistryInterface +{ + public function __construct(array $queryFilters = []) + { + parent::__construct(QueryFilterInterface::class, 'monsieurbiz.search'); + + foreach ($queryFilters as $queryFilter) { + $this->register(\get_class($queryFilter), $queryFilter); + } + } +} diff --git a/src/Search/Request/QueryFilter/ProductTaxonRegistry.php b/src/Search/Request/QueryFilter/ProductTaxonRegistry.php new file mode 100644 index 00000000..cf770a39 --- /dev/null +++ b/src/Search/Request/QueryFilter/ProductTaxonRegistry.php @@ -0,0 +1,28 @@ + + * + * 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 Sylius\Component\Registry\ServiceRegistry; + +final class ProductTaxonRegistry extends ServiceRegistry implements QueryFilterRegistryInterface +{ + public function __construct(array $queryFilters = []) + { + parent::__construct(QueryFilterInterface::class, 'monsieurbiz.search'); + + foreach ($queryFilters as $queryFilter) { + $this->register(\get_class($queryFilter), $queryFilter); + } + } +} diff --git a/src/Search/Request/QueryFilter/QueryFilterInterface.php b/src/Search/Request/QueryFilter/QueryFilterInterface.php new file mode 100644 index 00000000..6912403e --- /dev/null +++ b/src/Search/Request/QueryFilter/QueryFilterInterface.php @@ -0,0 +1,22 @@ + + * + * 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 MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +interface QueryFilterInterface +{ + public function apply(BoolQuery $boolQuery, RequestConfiguration $requestConfiguration): void; +} diff --git a/src/Search/Request/QueryFilter/QueryFilterRegistryInterface.php b/src/Search/Request/QueryFilter/QueryFilterRegistryInterface.php new file mode 100644 index 00000000..28b0a01f --- /dev/null +++ b/src/Search/Request/QueryFilter/QueryFilterRegistryInterface.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 MonsieurBiz\SyliusSearchPlugin\Search\Request\QueryFilter; + +use Sylius\Component\Registry\ServiceRegistryInterface; + +interface QueryFilterRegistryInterface extends ServiceRegistryInterface +{ +} diff --git a/src/Search/Request/RequestConfiguration.php b/src/Search/Request/RequestConfiguration.php new file mode 100644 index 00000000..486ac8c9 --- /dev/null +++ b/src/Search/Request/RequestConfiguration.php @@ -0,0 +1,127 @@ + + * + * 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 MonsieurBiz\SyliusSearchPlugin\Exception\ObjectNotInstanceOfClassException; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSettingsPlugin\Settings\SettingsInterface; +use Sylius\Bundle\ResourceBundle\Controller\Parameters; +use Sylius\Component\Channel\Context\ChannelContextInterface; +use Sylius\Component\Core\Model\TaxonInterface; +use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; +use Symfony\Component\HttpFoundation\Request; + +final class RequestConfiguration +{ + public const FALLBACK_LIMIT = 9; + + private Request $request; + + private string $type; + + private DocumentableInterface $documentable; + + private SettingsInterface $searchSettings; + + private ChannelContextInterface $channelContext; + + private Parameters $parameters; + + public function __construct( + Request $request, + string $type, + DocumentableInterface $documentable, + SettingsInterface $searchSettings, + ChannelContextInterface $channelContext, + ?Parameters $parameters = null + ) { + $this->request = $request; + $this->type = $type; + $this->documentable = $documentable; + $this->searchSettings = $searchSettings; + $this->channelContext = $channelContext; + $this->parameters = $parameters ?? new Parameters(); + } + + public function getQueryText(): string + { + return $this->request->get('query', ''); + } + + public function getAppliedFilters(string $type = null): array + { + $requestQuery = $this->request->query->all(); + $requestQuery = array_map(function ($query) { + return \is_array($query) ? array_filter($query) : $query; + }, $requestQuery); + + return null !== $type ? ($requestQuery[$type] ?? []) : $requestQuery; + } + + public function getSorting(): array + { + return $this->request->get('sorting', []); + } + + public function getPage(): int + { + return (int) $this->request->get('page', 1); + } + + public function getLimit(): int + { + $limit = (int) $this->request->get('limit', self::FALLBACK_LIMIT); + $availableLimits = $this->getAvailableLimits(); + + if (!\in_array($limit, $availableLimits, true)) { + $limit = reset($availableLimits); + } + + return $limit; + } + + public function getAvailableLimits(): array + { + $configLimits = $this->searchSettings->getCurrentValue( + $this->channelContext->getChannel(), + null, + 'limits__' . $this->getDocumentType() + ); + + return $configLimits[$this->getType()] ?? $this->documentable->getLimits($this->getType()); + } + + public function getType(): string + { + return $this->type; + } + + public function getDocumentType(): string + { + return $this->documentable->getIndexCode(); + } + + public function getTaxon(): TaxonInterface + { + if (!$this->parameters->has('taxon')) { + throw new ParameterNotFoundException('taxon'); + } + $taxon = $this->parameters->get('taxon'); + if (!$taxon instanceof TaxonInterface) { + throw ObjectNotInstanceOfClassException::fromClassName(TaxonInterface::class); + } + + return $this->parameters->get('taxon'); + } +} diff --git a/src/Search/Request/RequestHandler.php b/src/Search/Request/RequestHandler.php new file mode 100644 index 00000000..47ee4d95 --- /dev/null +++ b/src/Search/Request/RequestHandler.php @@ -0,0 +1,44 @@ + + * + * 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 MonsieurBiz\SyliusSearchPlugin\Exception\UnknownRequestTypeException; +use Sylius\Component\Registry\ServiceRegistryInterface; + +class RequestHandler +{ + private ServiceRegistryInterface $searchRequestsRegistry; + + public function __construct(ServiceRegistryInterface $searchRequestsRegistry) + { + $this->searchRequestsRegistry = $searchRequestsRegistry; + } + + /** + * @throws UnknownRequestTypeException + */ + public function getRequest(RequestConfiguration $requestConfiguration): RequestInterface + { + /** @var RequestInterface $request */ + foreach ($this->searchRequestsRegistry->all() as $request) { + if ($request->supports($requestConfiguration->getType(), $requestConfiguration->getDocumentType())) { + $request->setConfiguration($requestConfiguration); + + return $request; + } + } + + throw new UnknownRequestTypeException(); + } +} diff --git a/src/Search/Request/RequestInterface.php b/src/Search/Request/RequestInterface.php new file mode 100644 index 00000000..8e869959 --- /dev/null +++ b/src/Search/Request/RequestInterface.php @@ -0,0 +1,36 @@ + + * + * 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 MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; + +interface RequestInterface +{ + public const SEARCH_TYPE = 'search'; + + public const TAXON_TYPE = 'taxon'; + + public const INSTANT_TYPE = 'instant_search'; + + public function getType(): string; + + public function getDocumentable(): DocumentableInterface; + + public function getQuery(): Query; + + public function supports(string $type, string $documentableCode): bool; + + public function setConfiguration(RequestConfiguration $configuration): void; +} diff --git a/src/Search/Request/Sorting/Product/CreatedAtSorter.php b/src/Search/Request/Sorting/Product/CreatedAtSorter.php new file mode 100644 index 00000000..dcc791a8 --- /dev/null +++ b/src/Search/Request/Sorting/Product/CreatedAtSorter.php @@ -0,0 +1,34 @@ + + * + * 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\Sorting\Product; + +use Elastica\Query; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterBuilderTrait; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterInterface; + +final class CreatedAtSorter implements SorterInterface +{ + use SorterBuilderTrait; + + public function apply(Query $query, RequestConfiguration $requestConfiguration): void + { + $sorting = $requestConfiguration->getSorting(); + if (!\array_key_exists('created_at', $sorting)) { + return; + } + + $query->addSort($this->buildSort('created_at', $sorting['created_at'])); + } +} diff --git a/src/Search/Request/Sorting/Product/NameSorter.php b/src/Search/Request/Sorting/Product/NameSorter.php new file mode 100644 index 00000000..91a87a88 --- /dev/null +++ b/src/Search/Request/Sorting/Product/NameSorter.php @@ -0,0 +1,34 @@ + + * + * 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\Sorting\Product; + +use Elastica\Query; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterBuilderTrait; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterInterface; + +final class NameSorter implements SorterInterface +{ + use SorterBuilderTrait; + + public function apply(Query $query, RequestConfiguration $requestConfiguration): void + { + $sorting = $requestConfiguration->getSorting(); + if (!\array_key_exists('name', $sorting)) { + return; + } + + $query->addSort($this->buildSort('name.keyword', $sorting['name'])); + } +} diff --git a/src/Search/Request/Sorting/Product/PositionSorter.php b/src/Search/Request/Sorting/Product/PositionSorter.php new file mode 100644 index 00000000..7c1a1ddd --- /dev/null +++ b/src/Search/Request/Sorting/Product/PositionSorter.php @@ -0,0 +1,46 @@ + + * + * 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\Sorting\Product; + +use Elastica\Query; +use Elastica\QueryBuilder; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterBuilderTrait; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterInterface; + +final class PositionSorter implements SorterInterface +{ + use SorterBuilderTrait; + + public function apply(Query $query, RequestConfiguration $requestConfiguration): void + { + $sorting = $requestConfiguration->getSorting(); + if (!\array_key_exists('position', $sorting) && 0 !== \count($sorting)) { + return; + } + + $query->addSort($this->buildSort('_score', 'desc')); + if (RequestInterface::TAXON_TYPE == $requestConfiguration->getType()) { + $qb = new QueryBuilder(); + $filter = $qb->query()->nested() + ->setPath('product_taxons.taxon') + ->setQuery( + $qb->query()->term(['product_taxons.taxon.code' => ['value' => $requestConfiguration->getTaxon()->getCode()]]) + ) + ; + $query->addSort($this->buildSort('product_taxons.position', 'asc', 'product_taxons', null, $filter)); + } + } +} diff --git a/src/Search/Request/Sorting/Product/PriceSorter.php b/src/Search/Request/Sorting/Product/PriceSorter.php new file mode 100644 index 00000000..dcd5ecb2 --- /dev/null +++ b/src/Search/Request/Sorting/Product/PriceSorter.php @@ -0,0 +1,50 @@ + + * + * 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\Sorting\Product; + +use Elastica\Query; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterBuilderTrait; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting\SorterInterface; +use Sylius\Component\Channel\Context\ChannelContextInterface; + +final class PriceSorter implements SorterInterface +{ + use SorterBuilderTrait; + + private ChannelContextInterface $channelContext; + + public function __construct(ChannelContextInterface $channelContext) + { + $this->channelContext = $channelContext; + } + + public function apply(Query $query, RequestConfiguration $requestConfiguration): void + { + $sorting = $requestConfiguration->getSorting(); + if (!\array_key_exists('price', $sorting)) { + return; + } + + $query->addSort( + $this->buildSort( + 'prices.price', + $sorting['price'], + 'prices', + 'prices.channel_code', + $this->channelContext->getChannel()->getCode() + ) + ); + } +} diff --git a/src/Search/Request/Sorting/ProductSorterRegistry.php b/src/Search/Request/Sorting/ProductSorterRegistry.php new file mode 100644 index 00000000..f06efd3e --- /dev/null +++ b/src/Search/Request/Sorting/ProductSorterRegistry.php @@ -0,0 +1,28 @@ + + * + * 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\Sorting; + +use Sylius\Component\Registry\ServiceRegistry; + +final class ProductSorterRegistry extends ServiceRegistry implements SorterRegistryInterface +{ + public function __construct(array $queryFilters = []) + { + parent::__construct(SorterInterface::class, 'monsieurbiz.search'); + + foreach ($queryFilters as $queryFilter) { + $this->register(\get_class($queryFilter), $queryFilter); + } + } +} diff --git a/src/Search/Request/Sorting/SorterBuilderTrait.php b/src/Search/Request/Sorting/SorterBuilderTrait.php new file mode 100644 index 00000000..e78c5a87 --- /dev/null +++ b/src/Search/Request/Sorting/SorterBuilderTrait.php @@ -0,0 +1,46 @@ + + * + * 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\Sorting; + +use Elastica\Query\AbstractQuery; + +trait SorterBuilderTrait +{ + /** + * @param string|AbstractQuery|null $sortFilterValue + */ + protected function buildSort( + string $field, + string $order, + ?string $nestedPath = null, + ?string $sortFilterField = null, + $sortFilterValue = null + ): array { + $sort = [$field => ['order' => $order]]; + if (null !== $nestedPath) { + $sort[$field]['nested']['path'] = $nestedPath; + $filter = [ + 'term' => [ + $sortFilterField => $sortFilterValue, + ], + ]; + if ($sortFilterValue instanceof AbstractQuery) { + $filter = $sortFilterValue->toArray(); + } + $sort[$field]['nested']['filter'] = $filter; + } + + return $sort; + } +} diff --git a/src/Search/Request/Sorting/SorterInterface.php b/src/Search/Request/Sorting/SorterInterface.php new file mode 100644 index 00000000..a7f85d4f --- /dev/null +++ b/src/Search/Request/Sorting/SorterInterface.php @@ -0,0 +1,22 @@ + + * + * 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\Sorting; + +use Elastica\Query; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +interface SorterInterface +{ + public function apply(Query $query, RequestConfiguration $requestConfiguration): void; +} diff --git a/src/Exception/MissingConfigFileException.php b/src/Search/Request/Sorting/SorterRegistryInterface.php similarity index 57% rename from src/Exception/MissingConfigFileException.php rename to src/Search/Request/Sorting/SorterRegistryInterface.php index e40285d4..05dd08cf 100644 --- a/src/Exception/MissingConfigFileException.php +++ b/src/Search/Request/Sorting/SorterRegistryInterface.php @@ -5,16 +5,16 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * 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\Exception; +namespace MonsieurBiz\SyliusSearchPlugin\Search\Request\Sorting; -use Exception; +use Sylius\Component\Registry\ServiceRegistryInterface; -class MissingConfigFileException extends Exception +interface SorterRegistryInterface extends ServiceRegistryInterface { } diff --git a/src/Search/Response.php b/src/Search/Response.php new file mode 100644 index 00000000..df030207 --- /dev/null +++ b/src/Search/Response.php @@ -0,0 +1,111 @@ + + * + * 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; + +use Elastica\ResultSet; +use JoliCode\Elastically\Result; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use Pagerfanta\Adapter\AdapterInterface; +use Pagerfanta\Pagerfanta; + +class Response implements ResponseInterface +{ + private RequestConfiguration $requestConfiguration; + + private AdapterInterface $adapter; + + private DocumentableInterface $documentable; + + /** + * @var Pagerfanta|null + */ + private ?Pagerfanta $paginator = null; + + private array $filters = []; + + private iterable $filterBuilders; + + public function __construct( + RequestConfiguration $requestConfiguration, + AdapterInterface $adapter, + DocumentableInterface $documentable, + iterable $filterBuilders + ) { + $this->requestConfiguration = $requestConfiguration; + $this->adapter = $adapter; + $this->documentable = $documentable; + $this->filterBuilders = $filterBuilders; + $this->buildFilters(); + } + + public function getIterator() + { + return $this->getPaginator(); + } + + public function count() + { + return $this->getPaginator()->getNbResults(); + } + + public function getFilters(): array + { + return $this->filters; + } + + public function getPaginator(): Pagerfanta + { + if (null === $this->paginator) { + $this->paginator = new Pagerfanta($this->adapter); + $this->paginator->setCurrentPage($this->requestConfiguration->getPage()); + $this->paginator->setMaxPerPage($this->requestConfiguration->getLimit()); + } + + return $this->paginator; + } + + public function getDocumentable(): DocumentableInterface + { + return $this->documentable; + } + + private function buildFilters(): void + { + /** @var ResultSet $results */ + $results = $this->getPaginator()->getCurrentPageResults(); + $aggregations = $results->getAggregations(); + // No aggregation so don't perform filters + if (0 === \count($aggregations)) { + return; + } + + array_map(function ($aggregationCode, $aggregationData): void { + foreach ($this->filterBuilders as $filterBuilder) { + if (null !== $filter = $filterBuilder->build($this->getDocumentable(), $this->requestConfiguration, $aggregationCode, $aggregationData)) { + $this->filters[$filterBuilder->getPosition()][] = $filter; + } + } + }, array_keys($aggregations), $aggregations); + + $result = []; + ksort($this->filters); + foreach ($this->filters as $filters) { + foreach ($filters as $filter) { + $result[] = $filter; + } + } + $this->filters = array_merge(...$result); + } +} diff --git a/src/Search/Response/FilterBuilders/FilterBuilderInterface.php b/src/Search/Response/FilterBuilders/FilterBuilderInterface.php new file mode 100644 index 00000000..23780ccc --- /dev/null +++ b/src/Search/Response/FilterBuilders/FilterBuilderInterface.php @@ -0,0 +1,33 @@ + + * + * 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\Response\FilterBuilders; + +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterInterface; + +interface FilterBuilderInterface +{ + /** + * @return FilterInterface[]|null + */ + public function build( + DocumentableInterface $documentable, + RequestConfiguration $requestConfiguration, + string $aggregationCode, + array $aggregationData + ): ?array; + + public function getPosition(): int; +} diff --git a/src/Search/Response/FilterBuilders/Product/AttributeFilterBuilder.php b/src/Search/Response/FilterBuilders/Product/AttributeFilterBuilder.php new file mode 100644 index 00000000..555518d8 --- /dev/null +++ b/src/Search/Response/FilterBuilders/Product/AttributeFilterBuilder.php @@ -0,0 +1,73 @@ + + * + * 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\Response\FilterBuilders\Product; + +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Filter\Filter; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterBuilders\FilterBuilderInterface; + +class AttributeFilterBuilder implements FilterBuilderInterface +{ + public function build( + DocumentableInterface $documentable, + RequestConfiguration $requestConfiguration, + string $aggregationCode, + array $aggregationData + ): ?array { + if ('monsieurbiz_product' !== $documentable->getIndexCode() || 'attributes' !== $aggregationCode) { + return null; + } + + $attributeAggregations = $aggregationData[$aggregationCode] ?? []; + $attributeAggregations = $attributeAggregations[$aggregationCode] ?? $attributeAggregations; + unset($attributeAggregations['doc_count']); + $filters = []; + foreach ($attributeAggregations as $attributeCode => $attributeAggregation) { + if (isset($attributeAggregation[$attributeCode])) { + $attributeAggregation = $attributeAggregation[$attributeCode]; + } + $attributeNameBuckets = $attributeAggregation['names']['buckets'] ?? []; + foreach ($attributeNameBuckets as $attributeNameBucket) { + $attributeValueBuckets = $attributeNameBucket['values']['buckets'] ?? []; + $filter = new Filter( + $requestConfiguration, + $attributeCode, + $attributeNameBucket['key'], + $attributeNameBucket['doc_count'], + $aggregationCode + ); + foreach ($attributeValueBuckets as $attributeValueBucket) { + if (0 === $attributeValueBucket['doc_count']) { + continue; + } + if (isset($attributeValueBucket['key'], $attributeValueBucket['doc_count'])) { + $filter->addValue($attributeValueBucket['key'], $attributeValueBucket['doc_count']); + } + } + + if (0 !== \count($filter->getValues())) { + $filters[] = $filter; + } + } + } + + return 0 !== \count($filters) ? $filters : null; + } + + public function getPosition(): int + { + return 20; + } +} diff --git a/src/Search/Response/FilterBuilders/Product/MainTaxonFilterBuilder.php b/src/Search/Response/FilterBuilders/Product/MainTaxonFilterBuilder.php new file mode 100644 index 00000000..b6a739ab --- /dev/null +++ b/src/Search/Response/FilterBuilders/Product/MainTaxonFilterBuilder.php @@ -0,0 +1,79 @@ + + * + * 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\Response\FilterBuilders\Product; + +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Filter\Filter; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterBuilders\FilterBuilderInterface; + +class MainTaxonFilterBuilder implements FilterBuilderInterface +{ + public function build( + DocumentableInterface $documentable, + RequestConfiguration $requestConfiguration, + string $aggregationCode, + array $aggregationData + ): ?array { + if ('monsieurbiz_product' !== $documentable->getIndexCode() || 'main_taxon' !== $aggregationCode) { + return null; + } + + $taxonAggregation = $aggregationData['main_taxon'] ?? null; + if ($taxonAggregation && $taxonAggregation['doc_count'] > 0) { + $filter = new Filter( + $requestConfiguration, + 'main_taxon', + 'monsieurbiz_searchplugin.filters.taxon_filter', + $taxonAggregation['doc_count'], + 'taxon' + ); + + // Get main taxon code in aggregation + $taxonCodeBuckets = $taxonAggregation['codes']['buckets'] ?? []; + foreach ($taxonCodeBuckets as $taxonCodeBucket) { + if (0 === $taxonCodeBucket['doc_count']) { + continue; + } + $taxonCode = $taxonCodeBucket['key']; + $taxonName = null; + + // Get main taxon level in aggregation + $taxonLevelBuckets = $taxonCodeBucket['levels']['buckets'] ?? []; + foreach ($taxonLevelBuckets as $taxonLevelBucket) { + // Get main taxon name in aggregation + $taxonNameBuckets = $taxonLevelBucket['names']['buckets'] ?? []; + foreach ($taxonNameBuckets as $taxonNameBucket) { + $taxonName = $taxonNameBucket['key']; + $filter->addValue($taxonName ?? $taxonCode, $taxonCodeBucket['doc_count'], $taxonCode); + + break 2; + } + } + } + + // Put taxon filter in first if contains value + if (0 !== \count($filter->getValues())) { + return [$filter]; + } + } + + return null; + } + + public function getPosition(): int + { + return 1; + } +} diff --git a/src/Search/Response/FilterBuilders/Product/OptionFilterBuilder.php b/src/Search/Response/FilterBuilders/Product/OptionFilterBuilder.php new file mode 100644 index 00000000..cf0de594 --- /dev/null +++ b/src/Search/Response/FilterBuilders/Product/OptionFilterBuilder.php @@ -0,0 +1,73 @@ + + * + * 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\Response\FilterBuilders\Product; + +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Filter\Filter; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterBuilders\FilterBuilderInterface; + +class OptionFilterBuilder implements FilterBuilderInterface +{ + public function build( + DocumentableInterface $documentable, + RequestConfiguration $requestConfiguration, + string $aggregationCode, + array $aggregationData + ): ?array { + if ('monsieurbiz_product' !== $documentable->getIndexCode() || 'options' !== $aggregationCode) { + return null; + } + + $attributeAggregations = $aggregationData[$aggregationCode] ?? []; + $attributeAggregations = $attributeAggregations[$aggregationCode] ?? $attributeAggregations; + unset($attributeAggregations['doc_count']); + $filters = []; + foreach ($attributeAggregations as $attributeCode => $attributeAggregation) { + if (isset($attributeAggregation[$attributeCode])) { + $attributeAggregation = $attributeAggregation[$attributeCode]; + } + $attributeNameBuckets = $attributeAggregation['names']['buckets'] ?? []; + foreach ($attributeNameBuckets as $attributeNameBucket) { + $attributeValueBuckets = $attributeNameBucket['values']['values']['values']['buckets'] ?? []; + $filter = new Filter( + $requestConfiguration, + $attributeCode, + $attributeNameBucket['key'], + $attributeNameBucket['doc_count'], + $aggregationCode + ); + foreach ($attributeValueBuckets as $attributeValueBucket) { + if (0 === $attributeValueBucket['doc_count']) { + continue; + } + if (isset($attributeValueBucket['key'], $attributeValueBucket['doc_count'])) { + $filter->addValue($attributeValueBucket['key'], $attributeValueBucket['doc_count']); + } + } + + if (0 !== \count($filter->getValues())) { + $filters[] = $filter; + } + } + } + + return 0 !== \count($filters) ? $filters : null; + } + + public function getPosition(): int + { + return 20; + } +} diff --git a/src/Search/Response/FilterBuilders/Product/PriceFilterBuilder.php b/src/Search/Response/FilterBuilders/Product/PriceFilterBuilder.php new file mode 100644 index 00000000..38ddc5c6 --- /dev/null +++ b/src/Search/Response/FilterBuilders/Product/PriceFilterBuilder.php @@ -0,0 +1,56 @@ + + * + * 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\Response\FilterBuilders\Product; + +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Filter\RangeFilter; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterBuilders\FilterBuilderInterface; + +class PriceFilterBuilder implements FilterBuilderInterface +{ + public function build( + DocumentableInterface $documentable, + RequestConfiguration $requestConfiguration, + string $aggregationCode, + array $aggregationData + ): ?array { + if ('monsieurbiz_product' !== $documentable->getIndexCode() || 'prices' !== $aggregationCode) { + return null; + } + + $filter = null; + $priceAggregation = $aggregationData['prices']['prices'] ?? null; + if ($priceAggregation && $priceAggregation['doc_count'] > 0) { + $filter = [ + new RangeFilter( + $requestConfiguration, + 'price', + 'monsieurbiz_searchplugin.filters.price_filter', + 'monsieurbiz_searchplugin.filters.price_min', + 'monsieurbiz_searchplugin.filters.price_max', + (int) floor(($priceAggregation['prices_stats']['min'] ?? 0) / 100), + (int) ceil(($priceAggregation['prices_stats']['max'] ?? 0) / 100) + ), + ]; + } + + return $filter; + } + + public function getPosition(): int + { + return 10; + } +} diff --git a/src/Search/Response/FilterBuilders/Product/TaxonsFilterBuilder.php b/src/Search/Response/FilterBuilders/Product/TaxonsFilterBuilder.php new file mode 100644 index 00000000..55a7753c --- /dev/null +++ b/src/Search/Response/FilterBuilders/Product/TaxonsFilterBuilder.php @@ -0,0 +1,64 @@ + + * + * 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\Response\FilterBuilders\Product; + +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Filter\Filter; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +class TaxonsFilterBuilder +{ + public function build( + DocumentableInterface $documentable, + RequestConfiguration $requestConfiguration, + string $aggregationCode, + array $aggregationData + ): ?array { + if ('monsieurbiz_product' !== $documentable->getIndexCode() || 'taxons' !== $aggregationCode) { + return null; + } + + $taxonAggregation = $aggregationData['taxons']['taxons']['taxons'] ?? null; + if ($taxonAggregation && $taxonAggregation['doc_count'] > 0) { + $filter = new Filter($requestConfiguration, 'taxons', 'monsieurbiz_searchplugin.filters.taxon_filter', $taxonAggregation['doc_count']); + + // Get main taxon code in aggregation + $taxonCodeBuckets = $taxonAggregation['codes']['buckets'] ?? []; + foreach ($taxonCodeBuckets as $taxonCodeBucket) { + if (0 === $taxonCodeBucket['doc_count']) { + continue; + } + $taxonCode = $taxonCodeBucket['key']; + $taxonName = null; + $taxonNameBuckets = $taxonCodeBucket['names']['buckets'] ?? []; + foreach ($taxonNameBuckets as $taxonNameBucket) { + $taxonName = $taxonNameBucket['key']; + $filter->addValue($taxonName ?? $taxonCode, $taxonCodeBucket['doc_count'], $taxonCode); + } + } + + // Put taxon filter in first if contains value + if (0 !== \count($filter->getValues())) { + return [$filter]; + } + } + + return null; + } + + public function getPosition(): int + { + return 2; + } +} diff --git a/src/Exception/ReadFileException.php b/src/Search/Response/FilterInterface.php similarity index 69% rename from src/Exception/ReadFileException.php rename to src/Search/Response/FilterInterface.php index 8d931002..45bb79b5 100644 --- a/src/Exception/ReadFileException.php +++ b/src/Search/Response/FilterInterface.php @@ -5,16 +5,14 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * 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\Exception; +namespace MonsieurBiz\SyliusSearchPlugin\Search\Response; -use Exception; - -class ReadFileException extends Exception +interface FilterInterface { } diff --git a/src/Search/ResponseFactory.php b/src/Search/ResponseFactory.php new file mode 100644 index 00000000..b0c929b6 --- /dev/null +++ b/src/Search/ResponseFactory.php @@ -0,0 +1,38 @@ + + * + * 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; + +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use Pagerfanta\Adapter\AdapterInterface; + +class ResponseFactory +{ + private iterable $filterBuilders; + + public function __construct(iterable $filterBuilders) + { + $this->filterBuilders = $filterBuilders; + } + + public function build(RequestConfiguration $requestConfiguration, AdapterInterface $adapter, DocumentableInterface $documentable): ResponseInterface + { + return new Response( + $requestConfiguration, + $adapter, + $documentable, + $this->filterBuilders + ); + } +} diff --git a/src/Search/ResponseInterface.php b/src/Search/ResponseInterface.php new file mode 100644 index 00000000..0a7ecfe3 --- /dev/null +++ b/src/Search/ResponseInterface.php @@ -0,0 +1,39 @@ + + * + * 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; + +use Countable; +use IteratorAggregate; +use JoliCode\Elastically\Result; +use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; +use MonsieurBiz\SyliusSearchPlugin\Search\Response\FilterInterface; +use Pagerfanta\Pagerfanta; + +/** + * @extends IteratorAggregate + */ +interface ResponseInterface extends IteratorAggregate, Countable +{ + /** + * @return FilterInterface[] + */ + public function getFilters(): array; + + /** + * @return Pagerfanta + */ + public function getPaginator(): Pagerfanta; + + public function getDocumentable(): DocumentableInterface; +} diff --git a/src/Search/Search.php b/src/Search/Search.php new file mode 100644 index 00000000..3674bc15 --- /dev/null +++ b/src/Search/Search.php @@ -0,0 +1,60 @@ + + * + * 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; + +use MonsieurBiz\SyliusSearchPlugin\Exception\UnknownRequestTypeException; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestHandler; +use Pagerfanta\Elastica\ElasticaAdapter; +use Sylius\Component\Locale\Context\LocaleContextInterface; + +class Search implements SearchInterface +{ + private LocaleContextInterface $localeContext; + + private RequestHandler $requestHandler; + + private ClientFactory $clientFactory; + + private ResponseFactory $responseFactory; + + public function __construct( + ClientFactory $clientFactory, + LocaleContextInterface $localeContext, + RequestHandler $requestHandler, + ResponseFactory $responseFactory + ) { + $this->localeContext = $localeContext; + $this->requestHandler = $requestHandler; + $this->clientFactory = $clientFactory; + $this->responseFactory = $responseFactory; + } + + /** + * @throws UnknownRequestTypeException + */ + public function search(RequestConfiguration $requestConfiguration): ResponseInterface + { + $request = $this->requestHandler->getRequest($requestConfiguration); + + $indexName = $this->clientFactory->getIndexName($request->getDocumentable(), $this->localeContext->getLocaleCode()); + $client = $this->clientFactory->getClient($request->getDocumentable(), $this->localeContext->getLocaleCode()); + + return $this->responseFactory->build( + $requestConfiguration, + new ElasticaAdapter($client->getIndex($indexName), $request->getQuery()), + $request->getDocumentable() + ); + } +} diff --git a/src/Search/SearchInterface.php b/src/Search/SearchInterface.php new file mode 100644 index 00000000..5f1d54fa --- /dev/null +++ b/src/Search/SearchInterface.php @@ -0,0 +1,21 @@ + + * + * 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; + +use MonsieurBiz\SyliusSearchPlugin\Search\Request\RequestConfiguration; + +interface SearchInterface +{ + public function search(RequestConfiguration $requestConfiguration): ResponseInterface; +} diff --git a/src/Twig/Extension/CheckMethodExists.php b/src/Twig/Extension/CheckMethodExists.php deleted file mode 100644 index f9dcfc2a..00000000 --- a/src/Twig/Extension/CheckMethodExists.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Twig\Extension; - -use Symfony\Component\DependencyInjection\ContainerInterface; -use Twig\Extension\AbstractExtension; -use Twig\TwigFunction; - -class CheckMethodExists extends AbstractExtension -{ - private $container; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; - } - - public function getFunctions() - { - return [ - new TwigFunction('bundle_exists', [$this, 'bundleExists']), - ]; - } - - public function bundleExists($bundle) - { - return \array_key_exists( - $bundle, - $this->container->getParameter('kernel.bundles') - ); - } -} diff --git a/src/Twig/Extension/RenderDocumentUrl.php b/src/Twig/Extension/RenderDocumentUrl.php deleted file mode 100644 index 2829e59b..00000000 --- a/src/Twig/Extension/RenderDocumentUrl.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace MonsieurBiz\SyliusSearchPlugin\Twig\Extension; - -use MonsieurBiz\SyliusSearchPlugin\Helper\RenderDocumentUrlHelper; -use Twig\Extension\AbstractExtension; -use Twig\TwigFunction; - -class RenderDocumentUrl extends AbstractExtension -{ - /** - * @var RenderDocumentUrlHelper - */ - private $helper; - - /** - * RenderDocumentUrl constructor. - * - * @param RenderDocumentUrlHelper $helper - */ - public function __construct( - RenderDocumentUrlHelper $helper - ) { - $this->helper = $helper; - } - - public function getFunctions() - { - return [ - new TwigFunction('search_result_url_param', [$this->helper, 'getUrlParams']), - ]; - } -} diff --git a/src/Twig/Extension/RenderSearchForm.php b/src/Twig/Extension/RenderSearchForm.php index 8bd60636..bf93806c 100644 --- a/src/Twig/Extension/RenderSearchForm.php +++ b/src/Twig/Extension/RenderSearchForm.php @@ -5,7 +5,7 @@ * * (c) Monsieur Biz * - * For the full copyright and license information, please view the LICENSE + * For the full copyright and license information, please view the LICENSE.txt * file that was distributed with this source code. */ @@ -23,14 +23,11 @@ class RenderSearchForm extends AbstractExtension { - /** @var FormFactoryInterface */ - private $formFactory; + private FormFactoryInterface $formFactory; - /** @var Environment */ - private $templatingEngine; + private Environment $templatingEngine; - /** @var RequestStack */ - private $requestStack; + private RequestStack $requestStack; public function __construct( FormFactoryInterface $formFactory, @@ -49,13 +46,15 @@ public function getFunctions() ]; } - public function createForm($template = null) + public function createForm(?string $template = null): Markup { - $template = $template ?? '@MonsieurBizSyliusSearchPlugin/form.html.twig'; + $request = $this->requestStack->getCurrentRequest(); + $template = $template ?? '@MonsieurBizSyliusSearchPlugin/Search/_form.html.twig'; + $query = null !== $request ? $request->get('query', '') : ''; return new Markup($this->templatingEngine->render($template, [ 'form' => $this->formFactory->create(SearchType::class)->createView(), - 'query' => urldecode($this->requestStack->getCurrentRequest()->get('query') ?? ''), + 'query' => urldecode($query), ]), 'UTF-8'); } } diff --git a/src/generated/Model/Attributes.php b/src/generated/Model/Attributes.php deleted file mode 100644 index 49eb9ecc..00000000 --- a/src/generated/Model/Attributes.php +++ /dev/null @@ -1,142 +0,0 @@ -code; - } - /** - * - * - * @param string|null $code - * - * @return self - */ - public function setCode(?string $code) : self - { - $this->code = $code; - return $this; - } - /** - * - * - * @return string|null - */ - public function getName() : ?string - { - return $this->name; - } - /** - * - * - * @param string|null $name - * - * @return self - */ - public function setName(?string $name) : self - { - $this->name = $name; - return $this; - } - /** - * - * - * @return string[]|null - */ - public function getValue() : ?array - { - return $this->value; - } - /** - * - * - * @param string[]|null $value - * - * @return self - */ - public function setValue(?array $value) : self - { - $this->value = $value; - return $this; - } - /** - * - * - * @return string|null - */ - public function getLocale() : ?string - { - return $this->locale; - } - /** - * - * - * @param string|null $locale - * - * @return self - */ - public function setLocale(?string $locale) : self - { - $this->locale = $locale; - return $this; - } - /** - * - * - * @return int|null - */ - public function getScore() : ?int - { - return $this->score; - } - /** - * - * - * @param int|null $score - * - * @return self - */ - public function setScore(?int $score) : self - { - $this->score = $score; - return $this; - } -} \ No newline at end of file diff --git a/src/generated/Model/Document.php b/src/generated/Model/Document.php deleted file mode 100644 index f5c0856f..00000000 --- a/src/generated/Model/Document.php +++ /dev/null @@ -1,358 +0,0 @@ -type; - } - /** - * - * - * @param string|null $type - * - * @return self - */ - public function setType(?string $type) : self - { - $this->type = $type; - return $this; - } - /** - * - * - * @return string|null - */ - public function getCode() : ?string - { - return $this->code; - } - /** - * - * - * @param string|null $code - * - * @return self - */ - public function setCode(?string $code) : self - { - $this->code = $code; - return $this; - } - /** - * - * - * @return int|null - */ - public function getId() : ?int - { - return $this->id; - } - /** - * - * - * @param int|null $id - * - * @return self - */ - public function setId(?int $id) : self - { - $this->id = $id; - return $this; - } - /** - * - * - * @return bool|null - */ - public function getEnabled() : ?bool - { - return $this->enabled; - } - /** - * - * - * @param bool|null $enabled - * - * @return self - */ - public function setEnabled(?bool $enabled) : self - { - $this->enabled = $enabled; - return $this; - } - /** - * - * - * @return bool|null - */ - public function getInStock() : ?bool - { - return $this->inStock; - } - /** - * - * - * @param bool|null $inStock - * - * @return self - */ - public function setInStock(?bool $inStock) : self - { - $this->inStock = $inStock; - return $this; - } - /** - * - * - * @return string|null - */ - public function getSlug() : ?string - { - return $this->slug; - } - /** - * - * - * @param string|null $slug - * - * @return self - */ - public function setSlug(?string $slug) : self - { - $this->slug = $slug; - return $this; - } - /** - * - * - * @return string|null - */ - public function getImage() : ?string - { - return $this->image; - } - /** - * - * - * @param string|null $image - * - * @return self - */ - public function setImage(?string $image) : self - { - $this->image = $image; - return $this; - } - /** - * - * - * @return string[]|null - */ - public function getChannel() : ?array - { - return $this->channel; - } - /** - * - * - * @param string[]|null $channel - * - * @return self - */ - public function setChannel(?array $channel) : self - { - $this->channel = $channel; - return $this; - } - /** - * - * - * @return Taxon|null - */ - public function getMainTaxon() : ?Taxon - { - return $this->mainTaxon; - } - /** - * - * - * @param Taxon|null $mainTaxon - * - * @return self - */ - public function setMainTaxon(?Taxon $mainTaxon) : self - { - $this->mainTaxon = $mainTaxon; - return $this; - } - /** - * - * - * @return Taxon[]|null - */ - public function getTaxon() : ?array - { - return $this->taxon; - } - /** - * - * - * @param Taxon[]|null $taxon - * - * @return self - */ - public function setTaxon(?array $taxon) : self - { - $this->taxon = $taxon; - return $this; - } - /** - * - * - * @return Attributes[]|null - */ - public function getAttributes() : ?array - { - return $this->attributes; - } - /** - * - * - * @param Attributes[]|null $attributes - * - * @return self - */ - public function setAttributes(?array $attributes) : self - { - $this->attributes = $attributes; - return $this; - } - /** - * - * - * @return Price[]|null - */ - public function getPrice() : ?array - { - return $this->price; - } - /** - * - * - * @param Price[]|null $price - * - * @return self - */ - public function setPrice(?array $price) : self - { - $this->price = $price; - return $this; - } - /** - * - * - * @return Price[]|null - */ - public function getOriginalPrice() : ?array - { - return $this->originalPrice; - } - /** - * - * - * @param Price[]|null $originalPrice - * - * @return self - */ - public function setOriginalPrice(?array $originalPrice) : self - { - $this->originalPrice = $originalPrice; - return $this; - } -} diff --git a/src/generated/Model/Price.php b/src/generated/Model/Price.php deleted file mode 100644 index afaaf63b..00000000 --- a/src/generated/Model/Price.php +++ /dev/null @@ -1,88 +0,0 @@ -channel; - } - /** - * - * - * @param string|null $channel - * - * @return self - */ - public function setChannel(?string $channel) : self - { - $this->channel = $channel; - return $this; - } - /** - * - * - * @return string|null - */ - public function getCurrency() : ?string - { - return $this->currency; - } - /** - * - * - * @param string|null $currency - * - * @return self - */ - public function setCurrency(?string $currency) : self - { - $this->currency = $currency; - return $this; - } - /** - * - * - * @return int|null - */ - public function getValue() : ?int - { - return $this->value; - } - /** - * - * - * @param int|null $value - * - * @return self - */ - public function setValue(?int $value) : self - { - $this->value = $value; - return $this; - } -} \ No newline at end of file diff --git a/src/generated/Model/Taxon.php b/src/generated/Model/Taxon.php deleted file mode 100644 index a9166a8c..00000000 --- a/src/generated/Model/Taxon.php +++ /dev/null @@ -1,142 +0,0 @@ -name; - } - /** - * - * - * @param string|null $name - * - * @return self - */ - public function setName(?string $name) : self - { - $this->name = $name; - return $this; - } - /** - * - * - * @return string|null - */ - public function getCode() : ?string - { - return $this->code; - } - /** - * - * - * @param string|null $code - * - * @return self - */ - public function setCode(?string $code) : self - { - $this->code = $code; - return $this; - } - /** - * - * - * @return int|null - */ - public function getPosition() : ?int - { - return $this->position; - } - /** - * - * - * @param int|null $position - * - * @return self - */ - public function setPosition(?int $position) : self - { - $this->position = $position; - return $this; - } - /** - * - * - * @return int|null - */ - public function getLevel() : ?int - { - return $this->level; - } - /** - * - * - * @param int|null $level - * - * @return self - */ - public function setLevel(?int $level) : self - { - $this->level = $level; - return $this; - } - /** - * - * - * @return int|null - */ - public function getProductPosition() : ?int - { - return $this->productPosition; - } - /** - * - * - * @param int|null $productPosition - * - * @return self - */ - public function setProductPosition(?int $productPosition) : self - { - $this->productPosition = $productPosition; - return $this; - } -} \ No newline at end of file diff --git a/src/generated/Normalizer/AttributesNormalizer.php b/src/generated/Normalizer/AttributesNormalizer.php deleted file mode 100644 index 5eebd74d..00000000 --- a/src/generated/Normalizer/AttributesNormalizer.php +++ /dev/null @@ -1,112 +0,0 @@ -{'$ref'})) { - return new Reference($data->{'$ref'}, $context['document-origin']); - } - if (isset($data->{'$recursiveRef'})) { - return new Reference($data->{'$recursiveRef'}, $context['document-origin']); - } - $object = new \MonsieurBiz\SyliusSearchPlugin\generated\Model\Attributes(); - if (property_exists($data, 'code') && $data->{'code'} !== null) { - $object->setCode($data->{'code'}); - } - elseif (property_exists($data, 'code') && $data->{'code'} === null) { - $object->setCode(null); - } - if (property_exists($data, 'name') && $data->{'name'} !== null) { - $object->setName($data->{'name'}); - } - elseif (property_exists($data, 'name') && $data->{'name'} === null) { - $object->setName(null); - } - if (property_exists($data, 'value') && $data->{'value'} !== null) { - $values = array(); - foreach ($data->{'value'} as $value) { - $values[] = $value; - } - $object->setValue($values); - } - elseif (property_exists($data, 'value') && $data->{'value'} === null) { - $object->setValue(null); - } - if (property_exists($data, 'locale') && $data->{'locale'} !== null) { - $object->setLocale($data->{'locale'}); - } - elseif (property_exists($data, 'locale') && $data->{'locale'} === null) { - $object->setLocale(null); - } - if (property_exists($data, 'score') && $data->{'score'} !== null) { - $object->setScore($data->{'score'}); - } - elseif (property_exists($data, 'score') && $data->{'score'} === null) { - $object->setScore(null); - } - return $object; - } - public function normalize($object, $format = null, array $context = array()) - { - $data = new \stdClass(); - if (null !== $object->getCode()) { - $data->{'code'} = $object->getCode(); - } - else { - $data->{'code'} = null; - } - if (null !== $object->getName()) { - $data->{'name'} = $object->getName(); - } - else { - $data->{'name'} = null; - } - if (null !== $object->getValue()) { - $values = array(); - foreach ($object->getValue() as $value) { - $values[] = $value; - } - $data->{'value'} = $values; - } - else { - $data->{'value'} = null; - } - if (null !== $object->getLocale()) { - $data->{'locale'} = $object->getLocale(); - } - else { - $data->{'locale'} = null; - } - if (null !== $object->getScore()) { - $data->{'score'} = $object->getScore(); - } - else { - $data->{'score'} = null; - } - return $data; - } -} \ No newline at end of file diff --git a/src/generated/Normalizer/DocumentNormalizer.php b/src/generated/Normalizer/DocumentNormalizer.php deleted file mode 100644 index 937e0a09..00000000 --- a/src/generated/Normalizer/DocumentNormalizer.php +++ /dev/null @@ -1,240 +0,0 @@ -{'$ref'})) { - return new Reference($data->{'$ref'}, $context['document-origin']); - } - if (isset($data->{'$recursiveRef'})) { - return new Reference($data->{'$recursiveRef'}, $context['document-origin']); - } - $object = new \MonsieurBiz\SyliusSearchPlugin\generated\Model\Document(); - if (property_exists($data, 'type') && $data->{'type'} !== null) { - $object->setType($data->{'type'}); - } - elseif (property_exists($data, 'type') && $data->{'type'} === null) { - $object->setType(null); - } - if (property_exists($data, 'code') && $data->{'code'} !== null) { - $object->setCode($data->{'code'}); - } - elseif (property_exists($data, 'code') && $data->{'code'} === null) { - $object->setCode(null); - } - if (property_exists($data, 'id') && $data->{'id'} !== null) { - $object->setId($data->{'id'}); - } - elseif (property_exists($data, 'id') && $data->{'id'} === null) { - $object->setId(null); - } - if (property_exists($data, 'enabled') && $data->{'enabled'} !== null) { - $object->setEnabled($data->{'enabled'}); - } - elseif (property_exists($data, 'enabled') && $data->{'enabled'} === null) { - $object->setEnabled(null); - } - if (property_exists($data, 'inStock') && $data->{'inStock'} !== null) { - $object->setInStock($data->{'inStock'}); - } - elseif (property_exists($data, 'inStock') && $data->{'inStock'} === null) { - $object->setInStock(null); - } - if (property_exists($data, 'slug') && $data->{'slug'} !== null) { - $object->setSlug($data->{'slug'}); - } - elseif (property_exists($data, 'slug') && $data->{'slug'} === null) { - $object->setSlug(null); - } - if (property_exists($data, 'image') && $data->{'image'} !== null) { - $object->setImage($data->{'image'}); - } - elseif (property_exists($data, 'image') && $data->{'image'} === null) { - $object->setImage(null); - } - if (property_exists($data, 'channel') && $data->{'channel'} !== null) { - $values = array(); - foreach ($data->{'channel'} as $value) { - $values[] = $value; - } - $object->setChannel($values); - } - elseif (property_exists($data, 'channel') && $data->{'channel'} === null) { - $object->setChannel(null); - } - if (property_exists($data, 'main_taxon') && $data->{'main_taxon'} !== null) { - $object->setMainTaxon($this->denormalizer->denormalize($data->{'main_taxon'}, 'MonsieurBiz\\SyliusSearchPlugin\\generated\\Model\\Taxon', 'json', $context)); - } - elseif (property_exists($data, 'main_taxon') && $data->{'main_taxon'} === null) { - $object->setMainTaxon(null); - } - if (property_exists($data, 'taxon') && $data->{'taxon'} !== null) { - $values_1 = array(); - foreach ($data->{'taxon'} as $value_1) { - $values_1[] = $this->denormalizer->denormalize($value_1, 'MonsieurBiz\\SyliusSearchPlugin\\generated\\Model\\Taxon', 'json', $context); - } - $object->setTaxon($values_1); - } - elseif (property_exists($data, 'taxon') && $data->{'taxon'} === null) { - $object->setTaxon(null); - } - if (property_exists($data, 'attributes') && $data->{'attributes'} !== null) { - $values_2 = array(); - foreach ($data->{'attributes'} as $value_2) { - $values_2[] = $this->denormalizer->denormalize($value_2, 'MonsieurBiz\\SyliusSearchPlugin\\generated\\Model\\Attributes', 'json', $context); - } - $object->setAttributes($values_2); - } - elseif (property_exists($data, 'attributes') && $data->{'attributes'} === null) { - $object->setAttributes(null); - } - if (property_exists($data, 'price') && $data->{'price'} !== null) { - $values_3 = array(); - foreach ($data->{'price'} as $value_3) { - $values_3[] = $this->denormalizer->denormalize($value_3, 'MonsieurBiz\\SyliusSearchPlugin\\generated\\Model\\Price', 'json', $context); - } - $object->setPrice($values_3); - } - elseif (property_exists($data, 'price') && $data->{'price'} === null) { - $object->setPrice(null); - } - if (property_exists($data, 'original_price') && $data->{'original_price'} !== null) { - $values_4 = array(); - foreach ($data->{'original_price'} as $value_4) { - $values_4[] = $this->denormalizer->denormalize($value_4, 'MonsieurBiz\\SyliusSearchPlugin\\generated\\Model\\Price', 'json', $context); - } - $object->setOriginalPrice($values_4); - } - elseif (property_exists($data, 'original_price') && $data->{'original_price'} === null) { - $object->setOriginalPrice(null); - } - return $object; - } - public function normalize($object, $format = null, array $context = array()) - { - $data = new \stdClass(); - if (null !== $object->getType()) { - $data->{'type'} = $object->getType(); - } - else { - $data->{'type'} = null; - } - if (null !== $object->getCode()) { - $data->{'code'} = $object->getCode(); - } - else { - $data->{'code'} = null; - } - if (null !== $object->getId()) { - $data->{'id'} = $object->getId(); - } - else { - $data->{'id'} = null; - } - if (null !== $object->getEnabled()) { - $data->{'enabled'} = $object->getEnabled(); - } - else { - $data->{'enabled'} = null; - } - if (null !== $object->getInStock()) { - $data->{'inStock'} = $object->getInStock(); - } - else { - $data->{'inStock'} = null; - } - if (null !== $object->getSlug()) { - $data->{'slug'} = $object->getSlug(); - } - else { - $data->{'slug'} = null; - } - if (null !== $object->getImage()) { - $data->{'image'} = $object->getImage(); - } - else { - $data->{'image'} = null; - } - if (null !== $object->getChannel()) { - $values = array(); - foreach ($object->getChannel() as $value) { - $values[] = $value; - } - $data->{'channel'} = $values; - } - else { - $data->{'channel'} = null; - } - if (null !== $object->getMainTaxon()) { - $data->{'main_taxon'} = $this->normalizer->normalize($object->getMainTaxon(), 'json', $context); - } - else { - $data->{'main_taxon'} = null; - } - if (null !== $object->getTaxon()) { - $values_1 = array(); - foreach ($object->getTaxon() as $value_1) { - $values_1[] = $this->normalizer->normalize($value_1, 'json', $context); - } - $data->{'taxon'} = $values_1; - } - else { - $data->{'taxon'} = null; - } - if (null !== $object->getAttributes()) { - $values_2 = array(); - foreach ($object->getAttributes() as $value_2) { - $values_2[] = $this->normalizer->normalize($value_2, 'json', $context); - } - $data->{'attributes'} = $values_2; - } - else { - $data->{'attributes'} = null; - } - if (null !== $object->getPrice()) { - $values_3 = array(); - foreach ($object->getPrice() as $value_3) { - $values_3[] = $this->normalizer->normalize($value_3, 'json', $context); - } - $data->{'price'} = $values_3; - } - else { - $data->{'price'} = null; - } - if (null !== $object->getOriginalPrice()) { - $values_4 = array(); - foreach ($object->getOriginalPrice() as $value_4) { - $values_4[] = $this->normalizer->normalize($value_4, 'json', $context); - } - $data->{'original_price'} = $values_4; - } - else { - $data->{'original_price'} = null; - } - return $data; - } -} diff --git a/src/generated/Normalizer/JaneObjectNormalizer.php b/src/generated/Normalizer/JaneObjectNormalizer.php deleted file mode 100644 index 0f6c5f1d..00000000 --- a/src/generated/Normalizer/JaneObjectNormalizer.php +++ /dev/null @@ -1,48 +0,0 @@ - 'MonsieurBiz\\SyliusSearchPlugin\\generated\\Normalizer\\DocumentNormalizer', 'MonsieurBiz\\SyliusSearchPlugin\\generated\\Model\\Price' => 'MonsieurBiz\\SyliusSearchPlugin\\generated\\Normalizer\\PriceNormalizer', 'MonsieurBiz\\SyliusSearchPlugin\\generated\\Model\\Attributes' => 'MonsieurBiz\\SyliusSearchPlugin\\generated\\Normalizer\\AttributesNormalizer', 'MonsieurBiz\\SyliusSearchPlugin\\generated\\Model\\Taxon' => 'MonsieurBiz\\SyliusSearchPlugin\\generated\\Normalizer\\TaxonNormalizer', '\\Jane\\JsonSchemaRuntime\\Reference' => '\\Jane\\JsonSchemaRuntime\\Normalizer\\ReferenceNormalizer'), $normalizersCache = array(); - public function supportsDenormalization($data, $type, $format = null) - { - return array_key_exists($type, $this->normalizers); - } - public function supportsNormalization($data, $format = null) - { - return is_object($data) && array_key_exists(get_class($data), $this->normalizers); - } - public function normalize($object, $format = null, array $context = array()) - { - $normalizerClass = $this->normalizers[get_class($object)]; - $normalizer = $this->getNormalizer($normalizerClass); - return $normalizer->normalize($object, $format, $context); - } - public function denormalize($data, $class, $format = null, array $context = array()) - { - $denormalizerClass = $this->normalizers[$class]; - $denormalizer = $this->getNormalizer($denormalizerClass); - return $denormalizer->denormalize($data, $class, $format, $context); - } - private function getNormalizer(string $normalizerClass) - { - return $this->normalizersCache[$normalizerClass] ?? $this->initNormalizer($normalizerClass); - } - private function initNormalizer(string $normalizerClass) - { - $normalizer = new $normalizerClass(); - $normalizer->setNormalizer($this->normalizer); - $normalizer->setDenormalizer($this->denormalizer); - $this->normalizersCache[$normalizerClass] = $normalizer; - return $normalizer; - } -} \ No newline at end of file diff --git a/src/generated/Normalizer/NormalizerFactory.php b/src/generated/Normalizer/NormalizerFactory.php deleted file mode 100644 index 3373c88b..00000000 --- a/src/generated/Normalizer/NormalizerFactory.php +++ /dev/null @@ -1,18 +0,0 @@ -{'$ref'})) { - return new Reference($data->{'$ref'}, $context['document-origin']); - } - if (isset($data->{'$recursiveRef'})) { - return new Reference($data->{'$recursiveRef'}, $context['document-origin']); - } - $object = new \MonsieurBiz\SyliusSearchPlugin\generated\Model\Price(); - if (property_exists($data, 'channel') && $data->{'channel'} !== null) { - $object->setChannel($data->{'channel'}); - } - elseif (property_exists($data, 'channel') && $data->{'channel'} === null) { - $object->setChannel(null); - } - if (property_exists($data, 'currency') && $data->{'currency'} !== null) { - $object->setCurrency($data->{'currency'}); - } - elseif (property_exists($data, 'currency') && $data->{'currency'} === null) { - $object->setCurrency(null); - } - if (property_exists($data, 'value') && $data->{'value'} !== null) { - $object->setValue($data->{'value'}); - } - elseif (property_exists($data, 'value') && $data->{'value'} === null) { - $object->setValue(null); - } - return $object; - } - public function normalize($object, $format = null, array $context = array()) - { - $data = new \stdClass(); - if (null !== $object->getChannel()) { - $data->{'channel'} = $object->getChannel(); - } - else { - $data->{'channel'} = null; - } - if (null !== $object->getCurrency()) { - $data->{'currency'} = $object->getCurrency(); - } - else { - $data->{'currency'} = null; - } - if (null !== $object->getValue()) { - $data->{'value'} = $object->getValue(); - } - else { - $data->{'value'} = null; - } - return $data; - } -} \ No newline at end of file diff --git a/src/generated/Normalizer/TaxonNormalizer.php b/src/generated/Normalizer/TaxonNormalizer.php deleted file mode 100644 index 5d7bf413..00000000 --- a/src/generated/Normalizer/TaxonNormalizer.php +++ /dev/null @@ -1,104 +0,0 @@ -{'$ref'})) { - return new Reference($data->{'$ref'}, $context['document-origin']); - } - if (isset($data->{'$recursiveRef'})) { - return new Reference($data->{'$recursiveRef'}, $context['document-origin']); - } - $object = new \MonsieurBiz\SyliusSearchPlugin\generated\Model\Taxon(); - if (property_exists($data, 'name') && $data->{'name'} !== null) { - $object->setName($data->{'name'}); - } - elseif (property_exists($data, 'name') && $data->{'name'} === null) { - $object->setName(null); - } - if (property_exists($data, 'code') && $data->{'code'} !== null) { - $object->setCode($data->{'code'}); - } - elseif (property_exists($data, 'code') && $data->{'code'} === null) { - $object->setCode(null); - } - if (property_exists($data, 'position') && $data->{'position'} !== null) { - $object->setPosition($data->{'position'}); - } - elseif (property_exists($data, 'position') && $data->{'position'} === null) { - $object->setPosition(null); - } - if (property_exists($data, 'level') && $data->{'level'} !== null) { - $object->setLevel($data->{'level'}); - } - elseif (property_exists($data, 'level') && $data->{'level'} === null) { - $object->setLevel(null); - } - if (property_exists($data, 'product_position') && $data->{'product_position'} !== null) { - $object->setProductPosition($data->{'product_position'}); - } - elseif (property_exists($data, 'product_position') && $data->{'product_position'} === null) { - $object->setProductPosition(null); - } - return $object; - } - public function normalize($object, $format = null, array $context = array()) - { - $data = new \stdClass(); - if (null !== $object->getName()) { - $data->{'name'} = $object->getName(); - } - else { - $data->{'name'} = null; - } - if (null !== $object->getCode()) { - $data->{'code'} = $object->getCode(); - } - else { - $data->{'code'} = null; - } - if (null !== $object->getPosition()) { - $data->{'position'} = $object->getPosition(); - } - else { - $data->{'position'} = null; - } - if (null !== $object->getLevel()) { - $data->{'level'} = $object->getLevel(); - } - else { - $data->{'level'} = null; - } - if (null !== $object->getProductPosition()) { - $data->{'product_position'} = $object->getProductPosition(); - } - else { - $data->{'product_position'} = null; - } - return $data; - } -} \ No newline at end of file diff --git a/submit_filters.png b/submit_filters.png deleted file mode 100644 index 080e16fe..00000000 Binary files a/submit_filters.png and /dev/null differ diff --git a/tests/Application/.babelrc b/tests/Application/.babelrc deleted file mode 100644 index e563a62e..00000000 --- a/tests/Application/.babelrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "presets": [ - ["env", { - "targets": { - "node": "6" - }, - "useBuiltIns": true - }] - ], - "plugins": [ - ["transform-object-rest-spread", { - "useBuiltIns": true - }] - ] -} diff --git a/tests/Application/.env b/tests/Application/.env deleted file mode 100644 index c6f7517a..00000000 --- a/tests/Application/.env +++ /dev/null @@ -1,28 +0,0 @@ -# This file is a "template" of which env vars needs to be defined in your configuration or in an .env file -# Set variables here that may be different on each deployment target of the app, e.g. development, staging, production. -# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration - -###> symfony/framework-bundle ### -APP_ENV=dev -APP_DEBUG=1 -APP_SECRET=EDITME -###< symfony/framework-bundle ### - -###> doctrine/doctrine-bundle ### -# Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url -# For a sqlite database, use: "sqlite:///%kernel.project_dir%/var/data.db" -# Set "serverVersion" to your server version to avoid edge-case exceptions and extra database calls -DATABASE_URL=mysql://root@127.0.0.1/sylius_%kernel.environment%?serverVersion=5.5 -###< doctrine/doctrine-bundle ### - -###> symfony/swiftmailer-bundle ### -# For Gmail as a transport, use: "gmail://username:password@localhost" -# For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode=" -# Delivery is disabled by default via "null://localhost" -MAILER_URL=smtp://localhost -###< symfony/swiftmailer-bundle ### - -###> monsieurbiz/sylius-search-plugin ### -MONSIEURBIZ_SEARCHPLUGIN_ES_HOST=localhost -MONSIEURBIZ_SEARCHPLUGIN_ES_PORT=9200 -###< monsieurbiz/sylius-search-plugin ### diff --git a/tests/Application/.env.test b/tests/Application/.env.test deleted file mode 100644 index eb87f36f..00000000 --- a/tests/Application/.env.test +++ /dev/null @@ -1,3 +0,0 @@ -APP_SECRET='ch4mb3r0f5ecr3ts' - -KERNEL_CLASS='Tests\MonsieurBiz\SyliusSearchPlugin\Application\Kernel' diff --git a/tests/Application/.eslintrc.js b/tests/Application/.eslintrc.js deleted file mode 100644 index 92c4cee3..00000000 --- a/tests/Application/.eslintrc.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - extends: 'airbnb-base', - env: { - node: true, - }, - rules: { - 'object-shorthand': ['error', 'always', { - avoidQuotes: true, - avoidExplicitReturnArrows: true, - }], - 'function-paren-newline': ['error', 'consistent'], - 'max-len': ['warn', 120, 2, { - ignoreUrls: true, - ignoreComments: false, - ignoreRegExpLiterals: true, - ignoreStrings: true, - ignoreTemplateLiterals: true, - }], - }, -}; diff --git a/tests/Application/.gitignore b/tests/Application/.gitignore deleted file mode 100644 index 8ad1225e..00000000 --- a/tests/Application/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -/public/assets -/public/css -/public/js -/public/media/* -!/public/media/image/ -/public/media/image/* -!/public/media/image/.gitignore - -/node_modules - -###> symfony/framework-bundle ### -/.env.*.local -/.env.local -/.env.local.php -/public/bundles -/var/ -/vendor/ -###< symfony/framework-bundle ### - -###> symfony/web-server-bundle ### -/.web-server-pid -###< symfony/web-server-bundle ### diff --git a/tests/Application/.php-version b/tests/Application/.php-version deleted file mode 100644 index 37722ebb..00000000 --- a/tests/Application/.php-version +++ /dev/null @@ -1 +0,0 @@ -7.4 diff --git a/tests/Application/Kernel.php b/tests/Application/Kernel.php deleted file mode 100644 index 132b0ad4..00000000 --- a/tests/Application/Kernel.php +++ /dev/null @@ -1,115 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Tests\MonsieurBiz\SyliusSearchPlugin\Application; - -use PSS\SymfonyMockerContainer\DependencyInjection\MockerContainer; -use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; -use Symfony\Component\Config\Loader\DelegatingLoader; -use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\Config\Loader\LoaderResolver; -use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\DependencyInjection\Loader\ClosureLoader; -use Symfony\Component\DependencyInjection\Loader\DirectoryLoader; -use Symfony\Component\DependencyInjection\Loader\GlobFileLoader; -use Symfony\Component\DependencyInjection\Loader\IniFileLoader; -use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; -use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; -use Symfony\Component\HttpKernel\Config\FileLocator; -use Symfony\Component\HttpKernel\Kernel as BaseKernel; -use Symfony\Component\Routing\RouteCollectionBuilder; -use Webmozart\Assert\Assert; - -final class Kernel extends BaseKernel -{ - use MicroKernelTrait; - - private const CONFIG_EXTS = '.{php,xml,yaml,yml}'; - - public function getCacheDir(): string - { - return $this->getProjectDir() . '/var/cache/' . $this->environment; - } - - public function getLogDir(): string - { - return $this->getProjectDir() . '/var/log'; - } - - public function registerBundles(): iterable - { - $contents = require $this->getProjectDir() . '/config/bundles.php'; - foreach ($contents as $class => $envs) { - if (isset($envs['all']) || isset($envs[$this->environment])) { - yield new $class(); - } - } - } - - protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void - { - $container->addResource(new FileResource($this->getProjectDir() . '/config/bundles.php')); - $container->setParameter('container.dumper.inline_class_loader', true); - $confDir = $this->getProjectDir() . '/config'; - - $loader->load($confDir . '/{packages}/*' . self::CONFIG_EXTS, 'glob'); - $loader->load($confDir . '/{packages}/' . $this->environment . '/**/*' . self::CONFIG_EXTS, 'glob'); - $loader->load($confDir . '/{services}' . self::CONFIG_EXTS, 'glob'); - $loader->load($confDir . '/{services}_' . $this->environment . self::CONFIG_EXTS, 'glob'); - } - - protected function configureRoutes(RouteCollectionBuilder $routes): void - { - $confDir = $this->getProjectDir() . '/config'; - - $routes->import($confDir . '/{routes}/*' . self::CONFIG_EXTS, '/', 'glob'); - $routes->import($confDir . '/{routes}/' . $this->environment . '/**/*' . self::CONFIG_EXTS, '/', 'glob'); - $routes->import($confDir . '/{routes}' . self::CONFIG_EXTS, '/', 'glob'); - } - - protected function getContainerBaseClass(): string - { - if ($this->isTestEnvironment()) { - return MockerContainer::class; - } - - return parent::getContainerBaseClass(); - } - - protected function getContainerLoader(ContainerInterface $container): LoaderInterface - { - /** @var ContainerBuilder $container */ - Assert::isInstanceOf($container, ContainerBuilder::class); - - $locator = new FileLocator($this, $this->getRootDir() . '/Resources'); - $resolver = new LoaderResolver([ - new XmlFileLoader($container, $locator), - new YamlFileLoader($container, $locator), - new IniFileLoader($container, $locator), - new PhpFileLoader($container, $locator), - new GlobFileLoader($container, $locator), - new DirectoryLoader($container, $locator), - new ClosureLoader($container), - ]); - - return new DelegatingLoader($resolver); - } - - private function isTestEnvironment(): bool - { - return 0 === strpos($this->getEnvironment(), 'test'); - } -} diff --git a/tests/Application/bin/console b/tests/Application/bin/console deleted file mode 100755 index e4474639..00000000 --- a/tests/Application/bin/console +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env php -getParameterOption(['--env', '-e'], null, true)) { - putenv('APP_ENV=' . $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); -} - -if ($input->hasParameterOption('--no-debug', true)) { - putenv('APP_DEBUG=' . $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); -} - -require dirname(__DIR__) . '/config/bootstrap.php'; - -if ($_SERVER['APP_DEBUG']) { - umask(0000); - - if (class_exists(Debug::class)) { - Debug::enable(); - } -} - -$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); -$application = new Application($kernel); -$application->run($input); diff --git a/tests/Application/composer.json b/tests/Application/composer.json deleted file mode 100644 index 326735f5..00000000 --- a/tests/Application/composer.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "sylius/plugin-skeleton-test-application", - "description": "Sylius application for plugin testing purposes (composer.json needed for project dir resolving)", - "license": "MIT" -} diff --git a/tests/Application/config/bootstrap.php b/tests/Application/config/bootstrap.php deleted file mode 100644 index 88c382a0..00000000 --- a/tests/Application/config/bootstrap.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use Symfony\Component\Dotenv\Dotenv; - -require dirname(__DIR__) . '../../../vendor/autoload.php'; - -// Load cached env vars if the .env.local.php file exists -// Run "composer dump-env prod" to create it (requires symfony/flex >=1.2) -if (is_array($env = @include dirname(__DIR__) . '/.env.local.php')) { - $_SERVER += $env; - $_ENV += $env; -} elseif (!class_exists(Dotenv::class)) { - throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); -} else { - // load all the .env files - (new Dotenv(true))->loadEnv(dirname(__DIR__) . '/.env'); -} - -$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; -$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; -$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; diff --git a/tests/Application/config/bundles.php b/tests/Application/config/bundles.php deleted file mode 100644 index edc93d5f..00000000 --- a/tests/Application/config/bundles.php +++ /dev/null @@ -1,68 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -return [ - Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], - Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], - Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], - Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], - Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], - Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], - Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true], - Sylius\Bundle\OrderBundle\SyliusOrderBundle::class => ['all' => true], - Sylius\Bundle\MoneyBundle\SyliusMoneyBundle::class => ['all' => true], - Sylius\Bundle\CurrencyBundle\SyliusCurrencyBundle::class => ['all' => true], - Sylius\Bundle\LocaleBundle\SyliusLocaleBundle::class => ['all' => true], - Sylius\Bundle\ProductBundle\SyliusProductBundle::class => ['all' => true], - Sylius\Bundle\ChannelBundle\SyliusChannelBundle::class => ['all' => true], - Sylius\Bundle\AttributeBundle\SyliusAttributeBundle::class => ['all' => true], - Sylius\Bundle\TaxationBundle\SyliusTaxationBundle::class => ['all' => true], - Sylius\Bundle\ShippingBundle\SyliusShippingBundle::class => ['all' => true], - Sylius\Bundle\PaymentBundle\SyliusPaymentBundle::class => ['all' => true], - Sylius\Bundle\MailerBundle\SyliusMailerBundle::class => ['all' => true], - Sylius\Bundle\PromotionBundle\SyliusPromotionBundle::class => ['all' => true], - Sylius\Bundle\AddressingBundle\SyliusAddressingBundle::class => ['all' => true], - Sylius\Bundle\InventoryBundle\SyliusInventoryBundle::class => ['all' => true], - Sylius\Bundle\TaxonomyBundle\SyliusTaxonomyBundle::class => ['all' => true], - Sylius\Bundle\UserBundle\SyliusUserBundle::class => ['all' => true], - Sylius\Bundle\CustomerBundle\SyliusCustomerBundle::class => ['all' => true], - Sylius\Bundle\UiBundle\SyliusUiBundle::class => ['all' => true], - Sylius\Bundle\ReviewBundle\SyliusReviewBundle::class => ['all' => true], - Sylius\Bundle\CoreBundle\SyliusCoreBundle::class => ['all' => true], - Sylius\Bundle\ResourceBundle\SyliusResourceBundle::class => ['all' => true], - Sylius\Bundle\GridBundle\SyliusGridBundle::class => ['all' => true], - winzou\Bundle\StateMachineBundle\winzouStateMachineBundle::class => ['all' => true], - Sonata\BlockBundle\SonataBlockBundle::class => ['all' => true], - Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle::class => ['all' => true], - JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true], - FOS\RestBundle\FOSRestBundle::class => ['all' => true], - Knp\Bundle\GaufretteBundle\KnpGaufretteBundle::class => ['all' => true], - Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true], - Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true], - Payum\Bundle\PayumBundle\PayumBundle::class => ['all' => true], - Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], - WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle::class => ['all' => true], - Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], - Sylius\Bundle\FixturesBundle\SyliusFixturesBundle::class => ['all' => true], - Sylius\Bundle\PayumBundle\SyliusPayumBundle::class => ['all' => true], - Sylius\Bundle\ThemeBundle\SyliusThemeBundle::class => ['all' => true], - Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['all' => true], - Sylius\Bundle\AdminBundle\SyliusAdminBundle::class => ['all' => true], - Sylius\Bundle\ShopBundle\SyliusShopBundle::class => ['all' => true], - FOS\OAuthServerBundle\FOSOAuthServerBundle::class => ['all' => true], - Sylius\Bundle\AdminApiBundle\SyliusAdminApiBundle::class => ['all' => true], - MonsieurBiz\SyliusSearchPlugin\MonsieurBizSyliusSearchPlugin::class => ['all' => true], - Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true], - Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true], - FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle::class => ['test' => true, 'test_cached' => true], -]; diff --git a/tests/Application/config/packages/_sylius.yaml b/tests/Application/config/packages/_sylius.yaml deleted file mode 100644 index dfd932f1..00000000 --- a/tests/Application/config/packages/_sylius.yaml +++ /dev/null @@ -1,30 +0,0 @@ -imports: - - { resource: "@SyliusCoreBundle/Resources/config/app/config.yml" } - - - { resource: "@SyliusAdminBundle/Resources/config/app/config.yml" } - - { resource: "@SyliusAdminApiBundle/Resources/config/app/config.yml" } - - - { resource: "@SyliusShopBundle/Resources/config/app/config.yml" } - -parameters: - sylius_core.public_dir: '%kernel.project_dir%/public' - -sylius_shop: - product_grid: - include_all_descendants: true - -sylius_product: - resources: - product: - classes: - model: Tests\MonsieurBiz\SyliusSearchPlugin\App\Entity\Product\Product - product_option: - classes: - model: Tests\MonsieurBiz\SyliusSearchPlugin\App\Entity\Product\ProductOption - -sylius_attribute: - resources: - product: - attribute: - classes: - model: Tests\MonsieurBiz\SyliusSearchPlugin\App\Entity\Product\ProductAttribute diff --git a/tests/Application/config/packages/dev/framework.yaml b/tests/Application/config/packages/dev/framework.yaml deleted file mode 100644 index 5dd13a03..00000000 --- a/tests/Application/config/packages/dev/framework.yaml +++ /dev/null @@ -1,3 +0,0 @@ -framework: - profiler: { only_exceptions: false } - ide: phpstorm diff --git a/tests/Application/config/packages/dev/jms_serializer.yaml b/tests/Application/config/packages/dev/jms_serializer.yaml deleted file mode 100644 index 353e4602..00000000 --- a/tests/Application/config/packages/dev/jms_serializer.yaml +++ /dev/null @@ -1,7 +0,0 @@ -jms_serializer: - visitors: - json: - options: - - JSON_PRETTY_PRINT - - JSON_UNESCAPED_SLASHES - - JSON_PRESERVE_ZERO_FRACTION diff --git a/tests/Application/config/packages/dev/monolog.yaml b/tests/Application/config/packages/dev/monolog.yaml deleted file mode 100644 index da2b092d..00000000 --- a/tests/Application/config/packages/dev/monolog.yaml +++ /dev/null @@ -1,9 +0,0 @@ -monolog: - handlers: - main: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug - firephp: - type: firephp - level: info diff --git a/tests/Application/config/packages/dev/monsieurbiz_sylius_search_plugin.yaml b/tests/Application/config/packages/dev/monsieurbiz_sylius_search_plugin.yaml deleted file mode 100644 index fc978376..00000000 --- a/tests/Application/config/packages/dev/monsieurbiz_sylius_search_plugin.yaml +++ /dev/null @@ -1,10 +0,0 @@ -monsieur_biz_sylius_search: - files: - search: '%kernel.project_dir%/../../src/Resources/config/elasticsearch/queries/search.yaml' - instant: '%kernel.project_dir%/../../src/Resources/config/elasticsearch/queries/instant.yaml' - taxon: '%kernel.project_dir%/../../src/Resources/config/elasticsearch/queries/taxon.yaml' - documentable_classes : - - 'Tests\MonsieurBiz\SyliusSearchPlugin\App\Entity\Product\Product' - grid: - filters: - apply_manually: false # Will refresh the filters depending on applied filters after you apply it manually diff --git a/tests/Application/config/packages/dev/routing.yaml b/tests/Application/config/packages/dev/routing.yaml deleted file mode 100644 index 4116679a..00000000 --- a/tests/Application/config/packages/dev/routing.yaml +++ /dev/null @@ -1,3 +0,0 @@ -framework: - router: - strict_requirements: true diff --git a/tests/Application/config/packages/dev/swiftmailer.yaml b/tests/Application/config/packages/dev/swiftmailer.yaml deleted file mode 100644 index f4380780..00000000 --- a/tests/Application/config/packages/dev/swiftmailer.yaml +++ /dev/null @@ -1,2 +0,0 @@ -swiftmailer: - disable_delivery: true diff --git a/tests/Application/config/packages/dev/web_profiler.yaml b/tests/Application/config/packages/dev/web_profiler.yaml deleted file mode 100644 index 1f1cb2bb..00000000 --- a/tests/Application/config/packages/dev/web_profiler.yaml +++ /dev/null @@ -1,3 +0,0 @@ -web_profiler: - toolbar: true - intercept_redirects: false diff --git a/tests/Application/config/packages/doctrine.yaml b/tests/Application/config/packages/doctrine.yaml deleted file mode 100644 index d854a0c2..00000000 --- a/tests/Application/config/packages/doctrine.yaml +++ /dev/null @@ -1,24 +0,0 @@ -parameters: - # Adds a fallback DATABASE_URL if the env var is not set. - # This allows you to run cache:warmup even if your - # environment variables are not available yet. - # You should not need to change this value. - env(DATABASE_URL): '' - -doctrine: - dbal: - driver: 'pdo_mysql' - server_version: '5.7' - charset: UTF8 - - url: '%env(resolve:DATABASE_URL)%' - orm: - auto_generate_proxy_classes: '%kernel.debug%' - auto_mapping: true - mappings: - Tests\MonsieurBiz\SyliusSearchPlugin\App: - is_bundle: false - type: annotation - dir: '%kernel.project_dir%/src/Entity' - prefix: 'Tests\MonsieurBiz\SyliusSearchPlugin\App\Entity' - alias: Tests\MonsieurBiz\SyliusSearchPlugin\App diff --git a/tests/Application/config/packages/doctrine_migrations.yaml b/tests/Application/config/packages/doctrine_migrations.yaml deleted file mode 100644 index c0a12026..00000000 --- a/tests/Application/config/packages/doctrine_migrations.yaml +++ /dev/null @@ -1,5 +0,0 @@ -doctrine_migrations: - dir_name: "%kernel.project_dir%/src/Migrations" - - # Namespace is arbitrary but should be different from App\Migrations as migrations classes should NOT be autoloaded - namespace: DoctrineMigrations diff --git a/tests/Application/config/packages/fos_rest.yaml b/tests/Application/config/packages/fos_rest.yaml deleted file mode 100644 index a72eef7c..00000000 --- a/tests/Application/config/packages/fos_rest.yaml +++ /dev/null @@ -1,11 +0,0 @@ -fos_rest: - exception: true - view: - formats: - json: true - xml: true - empty_content: 204 - format_listener: - rules: - - { path: '^/api/.*', priorities: ['json', 'xml'], fallback_format: json, prefer_extension: true } - - { path: '^/', stop: true } diff --git a/tests/Application/config/packages/framework.yaml b/tests/Application/config/packages/framework.yaml deleted file mode 100644 index e74ed811..00000000 --- a/tests/Application/config/packages/framework.yaml +++ /dev/null @@ -1,7 +0,0 @@ -framework: - secret: '%env(APP_SECRET)%' - form: true - csrf_protection: true - templating: { engines: ["twig"] } - session: - handler_id: ~ diff --git a/tests/Application/config/packages/jms_serializer.yaml b/tests/Application/config/packages/jms_serializer.yaml deleted file mode 100644 index 64dd8d10..00000000 --- a/tests/Application/config/packages/jms_serializer.yaml +++ /dev/null @@ -1,4 +0,0 @@ -jms_serializer: - visitors: - xml: - format_output: '%kernel.debug%' diff --git a/tests/Application/config/packages/liip_imagine.yaml b/tests/Application/config/packages/liip_imagine.yaml deleted file mode 100644 index bb2e7ceb..00000000 --- a/tests/Application/config/packages/liip_imagine.yaml +++ /dev/null @@ -1,6 +0,0 @@ -liip_imagine: - resolvers: - default: - web_path: - web_root: "%kernel.project_dir%/public" - cache_prefix: "media/cache" diff --git a/tests/Application/config/packages/monsieurbiz_sylius_search_plugin.yaml b/tests/Application/config/packages/monsieurbiz_sylius_search_plugin.yaml deleted file mode 120000 index 5bc94855..00000000 --- a/tests/Application/config/packages/monsieurbiz_sylius_search_plugin.yaml +++ /dev/null @@ -1 +0,0 @@ -../../../../recipe/dev/config/packages/monsieurbiz_sylius_search_plugin.yaml \ No newline at end of file diff --git a/tests/Application/config/packages/prod/doctrine.yaml b/tests/Application/config/packages/prod/doctrine.yaml deleted file mode 100644 index 2f16f0fd..00000000 --- a/tests/Application/config/packages/prod/doctrine.yaml +++ /dev/null @@ -1,31 +0,0 @@ -doctrine: - orm: - metadata_cache_driver: - type: service - id: doctrine.system_cache_provider - query_cache_driver: - type: service - id: doctrine.system_cache_provider - result_cache_driver: - type: service - id: doctrine.result_cache_provider - -services: - doctrine.result_cache_provider: - class: Symfony\Component\Cache\DoctrineProvider - public: false - arguments: - - '@doctrine.result_cache_pool' - doctrine.system_cache_provider: - class: Symfony\Component\Cache\DoctrineProvider - public: false - arguments: - - '@doctrine.system_cache_pool' - -framework: - cache: - pools: - doctrine.result_cache_pool: - adapter: cache.app - doctrine.system_cache_pool: - adapter: cache.system diff --git a/tests/Application/config/packages/prod/jms_serializer.yaml b/tests/Application/config/packages/prod/jms_serializer.yaml deleted file mode 100644 index bc97faf1..00000000 --- a/tests/Application/config/packages/prod/jms_serializer.yaml +++ /dev/null @@ -1,6 +0,0 @@ -jms_serializer: - visitors: - json: - options: - - JSON_UNESCAPED_SLASHES - - JSON_PRESERVE_ZERO_FRACTION diff --git a/tests/Application/config/packages/prod/monolog.yaml b/tests/Application/config/packages/prod/monolog.yaml deleted file mode 100644 index 64612114..00000000 --- a/tests/Application/config/packages/prod/monolog.yaml +++ /dev/null @@ -1,10 +0,0 @@ -monolog: - handlers: - main: - type: fingers_crossed - action_level: error - handler: nested - nested: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug diff --git a/tests/Application/config/packages/routing.yaml b/tests/Application/config/packages/routing.yaml deleted file mode 100644 index 368bc7f4..00000000 --- a/tests/Application/config/packages/routing.yaml +++ /dev/null @@ -1,3 +0,0 @@ -framework: - router: - strict_requirements: ~ diff --git a/tests/Application/config/packages/security.yaml b/tests/Application/config/packages/security.yaml deleted file mode 100644 index a6689955..00000000 --- a/tests/Application/config/packages/security.yaml +++ /dev/null @@ -1,103 +0,0 @@ -parameters: - sylius.security.admin_regex: "^/admin" - sylius.security.api_regex: "^/api" - sylius.security.shop_regex: "^/(?!admin|api/.*|api$|media/.*)[^/]++" - -security: - always_authenticate_before_granting: true - providers: - sylius_admin_user_provider: - id: sylius.admin_user_provider.email_or_name_based - sylius_shop_user_provider: - id: sylius.shop_user_provider.email_or_name_based - encoders: - Sylius\Component\User\Model\UserInterface: argon2i - firewalls: - admin: - switch_user: true - context: admin - pattern: "%sylius.security.admin_regex%" - provider: sylius_admin_user_provider - form_login: - provider: sylius_admin_user_provider - login_path: sylius_admin_login - check_path: sylius_admin_login_check - failure_path: sylius_admin_login - default_target_path: sylius_admin_dashboard - use_forward: false - use_referer: true - csrf_token_generator: security.csrf.token_manager - csrf_parameter: _csrf_admin_security_token - csrf_token_id: admin_authenticate - remember_me: - secret: "%env(APP_SECRET)%" - path: /admin - name: APP_ADMIN_REMEMBER_ME - lifetime: 31536000 - remember_me_parameter: _remember_me - logout: - path: sylius_admin_logout - target: sylius_admin_login - anonymous: true - - oauth_token: - pattern: "%sylius.security.api_regex%/oauth/v2/token" - security: false - - api: - pattern: "%sylius.security.api_regex%/.*" - provider: sylius_admin_user_provider - fos_oauth: true - stateless: true - anonymous: true - - shop: - switch_user: { role: ROLE_ALLOWED_TO_SWITCH } - context: shop - pattern: "%sylius.security.shop_regex%" - provider: sylius_shop_user_provider - form_login: - success_handler: sylius.authentication.success_handler - failure_handler: sylius.authentication.failure_handler - provider: sylius_shop_user_provider - login_path: sylius_shop_login - check_path: sylius_shop_login_check - failure_path: sylius_shop_login - default_target_path: sylius_shop_homepage - use_forward: false - use_referer: true - csrf_token_generator: security.csrf.token_manager - csrf_parameter: _csrf_shop_security_token - csrf_token_id: shop_authenticate - remember_me: - secret: "%env(APP_SECRET)%" - name: APP_SHOP_REMEMBER_ME - lifetime: 31536000 - remember_me_parameter: _remember_me - logout: - path: sylius_shop_logout - target: sylius_shop_login - invalidate_session: false - success_handler: sylius.handler.shop_user_logout - anonymous: true - - dev: - pattern: ^/(_(profiler|wdt)|css|images|js)/ - security: false - - access_control: - - { path: "%sylius.security.admin_regex%/_partial", role: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] } - - { path: "%sylius.security.admin_regex%/_partial", role: ROLE_NO_ACCESS } - - { path: "%sylius.security.shop_regex%/_partial", role: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] } - - { path: "%sylius.security.shop_regex%/_partial", role: ROLE_NO_ACCESS } - - - { path: "%sylius.security.admin_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: "%sylius.security.api_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: "%sylius.security.shop_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY } - - - { path: "%sylius.security.shop_regex%/register", role: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: "%sylius.security.shop_regex%/verify", role: IS_AUTHENTICATED_ANONYMOUSLY } - - - { path: "%sylius.security.admin_regex%", role: ROLE_ADMINISTRATION_ACCESS } - - { path: "%sylius.security.api_regex%/.*", role: ROLE_API_ACCESS } - - { path: "%sylius.security.shop_regex%/account", role: ROLE_USER } diff --git a/tests/Application/config/packages/security_checker.yaml b/tests/Application/config/packages/security_checker.yaml deleted file mode 100644 index 0f9cf00f..00000000 --- a/tests/Application/config/packages/security_checker.yaml +++ /dev/null @@ -1,9 +0,0 @@ -services: - SensioLabs\Security\SecurityChecker: - public: false - - SensioLabs\Security\Command\SecurityCheckerCommand: - arguments: ['@SensioLabs\Security\SecurityChecker'] - public: false - tags: - - { name: console.command, command: 'security:check' } diff --git a/tests/Application/config/packages/staging/monolog.yaml b/tests/Application/config/packages/staging/monolog.yaml deleted file mode 100644 index 64612114..00000000 --- a/tests/Application/config/packages/staging/monolog.yaml +++ /dev/null @@ -1,10 +0,0 @@ -monolog: - handlers: - main: - type: fingers_crossed - action_level: error - handler: nested - nested: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug diff --git a/tests/Application/config/packages/staging/swiftmailer.yaml b/tests/Application/config/packages/staging/swiftmailer.yaml deleted file mode 100644 index f4380780..00000000 --- a/tests/Application/config/packages/staging/swiftmailer.yaml +++ /dev/null @@ -1,2 +0,0 @@ -swiftmailer: - disable_delivery: true diff --git a/tests/Application/config/packages/stof_doctrine_extensions.yaml b/tests/Application/config/packages/stof_doctrine_extensions.yaml deleted file mode 100644 index 7770f74e..00000000 --- a/tests/Application/config/packages/stof_doctrine_extensions.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html -# See the official DoctrineExtensions documentation for more details: https://github.com/Atlantic18/DoctrineExtensions/tree/master/doc/ -stof_doctrine_extensions: - default_locale: '%locale%' diff --git a/tests/Application/config/packages/swiftmailer.yaml b/tests/Application/config/packages/swiftmailer.yaml deleted file mode 100644 index 3bab0d32..00000000 --- a/tests/Application/config/packages/swiftmailer.yaml +++ /dev/null @@ -1,2 +0,0 @@ -swiftmailer: - url: '%env(MAILER_URL)%' diff --git a/tests/Application/config/packages/test/framework.yaml b/tests/Application/config/packages/test/framework.yaml deleted file mode 100644 index 76d7e5e1..00000000 --- a/tests/Application/config/packages/test/framework.yaml +++ /dev/null @@ -1,4 +0,0 @@ -framework: - test: ~ - session: - storage_id: session.storage.mock_file diff --git a/tests/Application/config/packages/test/monolog.yaml b/tests/Application/config/packages/test/monolog.yaml deleted file mode 100644 index 7e2b9e3a..00000000 --- a/tests/Application/config/packages/test/monolog.yaml +++ /dev/null @@ -1,6 +0,0 @@ -monolog: - handlers: - main: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: error diff --git a/tests/Application/config/packages/test/monsieurbiz_sylius_search_plugin.yaml b/tests/Application/config/packages/test/monsieurbiz_sylius_search_plugin.yaml deleted file mode 100644 index fc978376..00000000 --- a/tests/Application/config/packages/test/monsieurbiz_sylius_search_plugin.yaml +++ /dev/null @@ -1,10 +0,0 @@ -monsieur_biz_sylius_search: - files: - search: '%kernel.project_dir%/../../src/Resources/config/elasticsearch/queries/search.yaml' - instant: '%kernel.project_dir%/../../src/Resources/config/elasticsearch/queries/instant.yaml' - taxon: '%kernel.project_dir%/../../src/Resources/config/elasticsearch/queries/taxon.yaml' - documentable_classes : - - 'Tests\MonsieurBiz\SyliusSearchPlugin\App\Entity\Product\Product' - grid: - filters: - apply_manually: false # Will refresh the filters depending on applied filters after you apply it manually diff --git a/tests/Application/config/packages/test/swiftmailer.yaml b/tests/Application/config/packages/test/swiftmailer.yaml deleted file mode 100644 index c438f4b2..00000000 --- a/tests/Application/config/packages/test/swiftmailer.yaml +++ /dev/null @@ -1,6 +0,0 @@ -swiftmailer: - disable_delivery: true - logging: true - spool: - type: file - path: "%kernel.cache_dir%/spool" diff --git a/tests/Application/config/packages/test/sylius_theme.yaml b/tests/Application/config/packages/test/sylius_theme.yaml deleted file mode 100644 index 4d34199f..00000000 --- a/tests/Application/config/packages/test/sylius_theme.yaml +++ /dev/null @@ -1,3 +0,0 @@ -sylius_theme: - sources: - test: ~ diff --git a/tests/Application/config/packages/test/web_profiler.yaml b/tests/Application/config/packages/test/web_profiler.yaml deleted file mode 100644 index 03752de2..00000000 --- a/tests/Application/config/packages/test/web_profiler.yaml +++ /dev/null @@ -1,6 +0,0 @@ -web_profiler: - toolbar: false - intercept_redirects: false - -framework: - profiler: { collect: false } diff --git a/tests/Application/config/packages/test_cached/doctrine.yaml b/tests/Application/config/packages/test_cached/doctrine.yaml deleted file mode 100644 index 49528606..00000000 --- a/tests/Application/config/packages/test_cached/doctrine.yaml +++ /dev/null @@ -1,16 +0,0 @@ -doctrine: - orm: - entity_managers: - default: - result_cache_driver: - type: memcached - host: localhost - port: 11211 - query_cache_driver: - type: memcached - host: localhost - port: 11211 - metadata_cache_driver: - type: memcached - host: localhost - port: 11211 diff --git a/tests/Application/config/packages/test_cached/fos_rest.yaml b/tests/Application/config/packages/test_cached/fos_rest.yaml deleted file mode 100644 index 2b4189da..00000000 --- a/tests/Application/config/packages/test_cached/fos_rest.yaml +++ /dev/null @@ -1,3 +0,0 @@ -fos_rest: - exception: - debug: true diff --git a/tests/Application/config/packages/test_cached/framework.yaml b/tests/Application/config/packages/test_cached/framework.yaml deleted file mode 100644 index 76d7e5e1..00000000 --- a/tests/Application/config/packages/test_cached/framework.yaml +++ /dev/null @@ -1,4 +0,0 @@ -framework: - test: ~ - session: - storage_id: session.storage.mock_file diff --git a/tests/Application/config/packages/test_cached/monolog.yaml b/tests/Application/config/packages/test_cached/monolog.yaml deleted file mode 100644 index 7e2b9e3a..00000000 --- a/tests/Application/config/packages/test_cached/monolog.yaml +++ /dev/null @@ -1,6 +0,0 @@ -monolog: - handlers: - main: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: error diff --git a/tests/Application/config/packages/test_cached/swiftmailer.yaml b/tests/Application/config/packages/test_cached/swiftmailer.yaml deleted file mode 100644 index c438f4b2..00000000 --- a/tests/Application/config/packages/test_cached/swiftmailer.yaml +++ /dev/null @@ -1,6 +0,0 @@ -swiftmailer: - disable_delivery: true - logging: true - spool: - type: file - path: "%kernel.cache_dir%/spool" diff --git a/tests/Application/config/packages/test_cached/sylius_channel.yaml b/tests/Application/config/packages/test_cached/sylius_channel.yaml deleted file mode 100644 index bab83ef2..00000000 --- a/tests/Application/config/packages/test_cached/sylius_channel.yaml +++ /dev/null @@ -1,2 +0,0 @@ -sylius_channel: - debug: true diff --git a/tests/Application/config/packages/test_cached/sylius_theme.yaml b/tests/Application/config/packages/test_cached/sylius_theme.yaml deleted file mode 100644 index 4d34199f..00000000 --- a/tests/Application/config/packages/test_cached/sylius_theme.yaml +++ /dev/null @@ -1,3 +0,0 @@ -sylius_theme: - sources: - test: ~ diff --git a/tests/Application/config/packages/test_cached/twig.yaml b/tests/Application/config/packages/test_cached/twig.yaml deleted file mode 100644 index 8c6e0b40..00000000 --- a/tests/Application/config/packages/test_cached/twig.yaml +++ /dev/null @@ -1,2 +0,0 @@ -twig: - strict_variables: true diff --git a/tests/Application/config/packages/translation.yaml b/tests/Application/config/packages/translation.yaml deleted file mode 100644 index 1f4f9664..00000000 --- a/tests/Application/config/packages/translation.yaml +++ /dev/null @@ -1,8 +0,0 @@ -framework: - default_locale: '%locale%' - translator: - paths: - - '%kernel.project_dir%/translations' - fallbacks: - - '%locale%' - - 'en' diff --git a/tests/Application/config/packages/twig.yaml b/tests/Application/config/packages/twig.yaml deleted file mode 100644 index 8545473d..00000000 --- a/tests/Application/config/packages/twig.yaml +++ /dev/null @@ -1,12 +0,0 @@ -twig: - paths: ['%kernel.project_dir%/templates'] - debug: '%kernel.debug%' - strict_variables: '%kernel.debug%' - -services: - _defaults: - public: false - autowire: true - autoconfigure: true - - Twig\Extra\Intl\IntlExtension: ~ diff --git a/tests/Application/config/packages/validator.yaml b/tests/Application/config/packages/validator.yaml deleted file mode 100644 index 61807db6..00000000 --- a/tests/Application/config/packages/validator.yaml +++ /dev/null @@ -1,3 +0,0 @@ -framework: - validation: - enable_annotations: true diff --git a/tests/Application/config/routes.yaml b/tests/Application/config/routes.yaml deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/Application/config/routes/dev/twig.yaml b/tests/Application/config/routes/dev/twig.yaml deleted file mode 100644 index f4ee8396..00000000 --- a/tests/Application/config/routes/dev/twig.yaml +++ /dev/null @@ -1,3 +0,0 @@ -_errors: - resource: '@TwigBundle/Resources/config/routing/errors.xml' - prefix: /_error diff --git a/tests/Application/config/routes/dev/web_profiler.yaml b/tests/Application/config/routes/dev/web_profiler.yaml deleted file mode 100644 index 3e79dc21..00000000 --- a/tests/Application/config/routes/dev/web_profiler.yaml +++ /dev/null @@ -1,7 +0,0 @@ -_wdt: - resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml" - prefix: /_wdt - -_profiler: - resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml" - prefix: /_profiler diff --git a/tests/Application/config/routes/liip_imagine.yaml b/tests/Application/config/routes/liip_imagine.yaml deleted file mode 100644 index 201cbd5d..00000000 --- a/tests/Application/config/routes/liip_imagine.yaml +++ /dev/null @@ -1,2 +0,0 @@ -_liip_imagine: - resource: "@LiipImagineBundle/Resources/config/routing.yaml" diff --git a/tests/Application/config/routes/monsieurbiz_sylius_search_plugin.yaml b/tests/Application/config/routes/monsieurbiz_sylius_search_plugin.yaml deleted file mode 120000 index 14ac27dc..00000000 --- a/tests/Application/config/routes/monsieurbiz_sylius_search_plugin.yaml +++ /dev/null @@ -1 +0,0 @@ -../../../../recipe/dev/config/routes/monsieurbiz_sylius_search_plugin.yaml \ No newline at end of file diff --git a/tests/Application/config/routes/sylius_admin.yaml b/tests/Application/config/routes/sylius_admin.yaml deleted file mode 100644 index 1ba48d6c..00000000 --- a/tests/Application/config/routes/sylius_admin.yaml +++ /dev/null @@ -1,3 +0,0 @@ -sylius_admin: - resource: "@SyliusAdminBundle/Resources/config/routing.yml" - prefix: /admin diff --git a/tests/Application/config/routes/sylius_admin_api.yaml b/tests/Application/config/routes/sylius_admin_api.yaml deleted file mode 100644 index 80aed457..00000000 --- a/tests/Application/config/routes/sylius_admin_api.yaml +++ /dev/null @@ -1,3 +0,0 @@ -sylius_admin_api: - resource: "@SyliusAdminApiBundle/Resources/config/routing.yml" - prefix: /api diff --git a/tests/Application/config/routes/sylius_shop.yaml b/tests/Application/config/routes/sylius_shop.yaml deleted file mode 100644 index 8818568b..00000000 --- a/tests/Application/config/routes/sylius_shop.yaml +++ /dev/null @@ -1,14 +0,0 @@ -sylius_shop: - resource: "@SyliusShopBundle/Resources/config/routing.yml" - prefix: /{_locale} - requirements: - _locale: ^[a-z]{2}(?:_[A-Z]{2})?$ - -sylius_shop_payum: - resource: "@SyliusShopBundle/Resources/config/routing/payum.yml" - -sylius_shop_default_locale: - path: / - methods: [GET] - defaults: - _controller: sylius.controller.shop.locale_switch:switchAction diff --git a/tests/Application/config/services.yaml b/tests/Application/config/services.yaml deleted file mode 100644 index d15eba83..00000000 --- a/tests/Application/config/services.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Put parameters here that don't need to change on each machine where the app is deployed -# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration -parameters: - locale: en_US - -services: - # Client configuration. - JoliCode\Elastically\Client: - arguments: - $config: - host: '%env(MONSIEURBIZ_SEARCHPLUGIN_ES_HOST)%' - port: '%env(MONSIEURBIZ_SEARCHPLUGIN_ES_PORT)%' - elastically_mappings_directory: '%kernel.project_dir%/../../src/Resources/config/elasticsearch/mappings' - elastically_index_class_mapping: - documents-fr: \MonsieurBiz\SyliusSearchPlugin\Model\Document\Result - documents-en: \MonsieurBiz\SyliusSearchPlugin\Model\Document\Result - documents-en_us: \MonsieurBiz\SyliusSearchPlugin\Model\Document\Result - elastically_bulk_size: 100 - -sylius_fixtures: - suites: - default: - fixtures: - monsieurbiz_sylius_search_filterable: - options: - custom: - cap_collection: - attribute: 'cap_collection' - filterable: false - - dress_collection: - attribute: 'dress_collection' - filterable: false - - dress_height: - option: 'dress_height' - filterable: false - - dress_size: - option: 'dress_size' - filterable: true diff --git a/tests/Application/config/services_test.yaml b/tests/Application/config/services_test.yaml deleted file mode 100644 index b0c2d6a9..00000000 --- a/tests/Application/config/services_test.yaml +++ /dev/null @@ -1,3 +0,0 @@ -imports: -# - { resource: "../../Behat/Resources/services.xml" } -# - { resource: "../../../vendor/sylius/sylius/src/Sylius/Behat/Resources/config/services.xml" } diff --git a/tests/Application/gulpfile.babel.js b/tests/Application/gulpfile.babel.js deleted file mode 100644 index 5920316f..00000000 --- a/tests/Application/gulpfile.babel.js +++ /dev/null @@ -1,60 +0,0 @@ -import chug from 'gulp-chug'; -import gulp from 'gulp'; -import yargs from 'yargs'; - -const { argv } = yargs - .options({ - rootPath: { - description: ' path to public assets directory', - type: 'string', - requiresArg: true, - required: false, - }, - nodeModulesPath: { - description: ' path to node_modules directory', - type: 'string', - requiresArg: true, - required: false, - }, - }); - -const config = [ - '--rootPath', - argv.rootPath || '../../../../../../../tests/Application/public/assets', - '--nodeModulesPath', - argv.nodeModulesPath || '../../../../../../../tests/Application/node_modules', -]; - -export const buildAdmin = function buildAdmin() { - return gulp.src('../../vendor/sylius/sylius/src/Sylius/Bundle/AdminBundle/gulpfile.babel.js', { read: false }) - .pipe(chug({ args: config, tasks: 'build' })); -}; -buildAdmin.description = 'Build admin assets.'; - -export const watchAdmin = function watchAdmin() { - return gulp.src('../../vendor/sylius/sylius/src/Sylius/Bundle/AdminBundle/gulpfile.babel.js', { read: false }) - .pipe(chug({ args: config, tasks: 'watch' })); -}; -watchAdmin.description = 'Watch admin asset sources and rebuild on changes.'; - -export const buildShop = function buildShop() { - return gulp.src('../../vendor/sylius/sylius/src/Sylius/Bundle/ShopBundle/gulpfile.babel.js', { read: false }) - .pipe(chug({ args: config, tasks: 'build' })); -}; -buildShop.description = 'Build shop assets.'; - -export const watchShop = function watchShop() { - return gulp.src('../../vendor/sylius/sylius/src/Sylius/Bundle/ShopBundle/gulpfile.babel.js', { read: false }) - .pipe(chug({ args: config, tasks: 'watch' })); -}; -watchShop.description = 'Watch shop asset sources and rebuild on changes.'; - -export const build = gulp.parallel(buildAdmin, buildShop); -build.description = 'Build assets.'; - -gulp.task('admin', buildAdmin); -gulp.task('admin-watch', watchAdmin); -gulp.task('shop', buildShop); -gulp.task('shop-watch', watchShop); - -export default build; diff --git a/tests/Application/package.json b/tests/Application/package.json deleted file mode 100644 index 2f72522b..00000000 --- a/tests/Application/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "dependencies": { - "babel-polyfill": "^6.26.0", - "chart.js": "^2.9.3", - "jquery": "^3.2.0", - "jquery.dirtyforms": "^2.0.0", - "lightbox2": "^2.9.0", - "semantic-ui-css": "^2.2.0", - "slick-carousel": "^1.8.1" - }, - "devDependencies": { - "@symfony/webpack-encore": "^0.28.0", - "babel-core": "^6.26.3", - "babel-plugin-external-helpers": "^6.22.0", - "babel-plugin-module-resolver": "^3.1.1", - "babel-plugin-transform-object-rest-spread": "^6.26.0", - "babel-preset-env": "^1.7.0", - "babel-register": "^6.26.0", - "dedent": "^0.7.0", - "eslint": "^4.19.1", - "eslint-config-airbnb-base": "^12.1.0", - "eslint-import-resolver-babel-module": "^4.0.0", - "eslint-plugin-import": "^2.12.0", - "fast-async": "^6.3.7", - "gulp": "^4.0.0", - "gulp-chug": "^0.5", - "gulp-concat": "^2.6.0", - "gulp-debug": "^2.1.2", - "gulp-if": "^2.0.0", - "gulp-livereload": "^3.8.1", - "gulp-order": "^1.1.1", - "gulp-sass": "^4.0.1", - "gulp-sourcemaps": "^1.6.0", - "gulp-uglifycss": "^1.0.5", - "merge-stream": "^1.0.0", - "rollup": "^0.60.7", - "rollup-plugin-babel": "^3.0.4", - "rollup-plugin-commonjs": "^9.1.3", - "rollup-plugin-inject": "^2.0.0", - "rollup-plugin-node-resolve": "^3.3.0", - "rollup-plugin-uglify": "^4.0.0", - "sass-loader": "^7.0.1", - "upath": "^1.1.0", - "yargs": "^6.4.0" - }, - "scripts": { - "build": "gulp build", - "gulp": "gulp build", - "lint": "yarn lint:js", - "lint:js": "eslint gulpfile.babel.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/Sylius/Sylius.git" - }, - "author": "Paweł Jędrzejewski", - "license": "MIT" -} diff --git a/tests/Application/php.ini b/tests/Application/php.ini deleted file mode 100644 index 8519987f..00000000 --- a/tests/Application/php.ini +++ /dev/null @@ -1 +0,0 @@ -date.timezone=Greenwich diff --git a/tests/Application/public/.htaccess b/tests/Application/public/.htaccess deleted file mode 100644 index 99ed00df..00000000 --- a/tests/Application/public/.htaccess +++ /dev/null @@ -1,25 +0,0 @@ -DirectoryIndex app.php - - - RewriteEngine On - - RewriteCond %{HTTP:Authorization} ^(.*) - RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] - - RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ - RewriteRule ^(.*) - [E=BASE:%1] - - RewriteCond %{ENV:REDIRECT_STATUS} ^$ - RewriteRule ^index\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L] - - RewriteCond %{REQUEST_FILENAME} -f - RewriteRule .? - [L] - - RewriteRule .? %{ENV:BASE}/index.php [L] - - - - - RedirectMatch 302 ^/$ /index.php/ - - diff --git a/tests/Application/public/favicon.ico b/tests/Application/public/favicon.ico deleted file mode 100644 index 592f7a8e..00000000 Binary files a/tests/Application/public/favicon.ico and /dev/null differ diff --git a/tests/Application/public/index.php b/tests/Application/public/index.php deleted file mode 100644 index 9b21af88..00000000 --- a/tests/Application/public/index.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use Symfony\Component\Debug\Debug; -use Symfony\Component\HttpFoundation\Request; -use Tests\MonsieurBiz\SyliusSearchPlugin\Application\Kernel; - -require dirname(__DIR__) . '/config/bootstrap.php'; - -if ($_SERVER['APP_DEBUG']) { - umask(0000); - - Debug::enable(); -} - -if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) { - Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); -} - -if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) { - Request::setTrustedHosts([$trustedHosts]); -} - -$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); -$request = Request::createFromGlobals(); -$response = $kernel->handle($request); -$response->send(); -$kernel->terminate($request, $response); diff --git a/tests/Application/public/media/image/.gitignore b/tests/Application/public/media/image/.gitignore deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/Application/public/robots.txt b/tests/Application/public/robots.txt deleted file mode 100644 index 214e4119..00000000 --- a/tests/Application/public/robots.txt +++ /dev/null @@ -1,4 +0,0 @@ -# www.robotstxt.org/ -# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449 - -User-agent: * diff --git a/tests/Application/src/Entity/Product/Product.php b/tests/Application/src/Entity/Product/Product.php deleted file mode 100644 index 888c4a19..00000000 --- a/tests/Application/src/Entity/Product/Product.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Tests\MonsieurBiz\SyliusSearchPlugin\App\Entity\Product; - -use Doctrine\ORM\Mapping as ORM; -use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableInterface; -use MonsieurBiz\SyliusSearchPlugin\Model\Documentable\DocumentableProductTrait; -use Sylius\Component\Core\Model\Product as BaseProduct; -use Sylius\Component\Core\Model\ProductTranslation; -use Sylius\Component\Product\Model\ProductTranslationInterface; - -/** - * @ORM\MappedSuperclass - * @ORM\Table(name="sylius_product") - */ -class Product extends BaseProduct implements DocumentableInterface -{ - use DocumentableProductTrait; - - protected function createTranslation(): ProductTranslationInterface - { - return new ProductTranslation(); - } -} diff --git a/tests/Application/src/Migrations/Version20201016105958.php b/tests/Application/src/Migrations/Version20201016105958.php deleted file mode 100644 index bf4fa206..00000000 --- a/tests/Application/src/Migrations/Version20201016105958.php +++ /dev/null @@ -1,37 +0,0 @@ -abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); - - $this->addSql('ALTER TABLE sylius_product_option ADD filterable TINYINT(1) DEFAULT \'1\' NOT NULL'); - $this->addSql('ALTER TABLE sylius_product_attribute ADD filterable TINYINT(1) DEFAULT \'1\' NOT NULL'); - } - - public function down(Schema $schema) : void - { - // this down() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); - - $this->addSql('ALTER TABLE sylius_product_attribute DROP filterable'); - $this->addSql('ALTER TABLE sylius_product_option DROP filterable'); - } -} diff --git a/tests/Application/templates/.gitkeep b/tests/Application/templates/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/Application/templates/bundles/SyliusAdminBundle b/tests/Application/templates/bundles/SyliusAdminBundle deleted file mode 120000 index 98a3226d..00000000 --- a/tests/Application/templates/bundles/SyliusAdminBundle +++ /dev/null @@ -1 +0,0 @@ -../../../../src/Resources/SyliusAdminBundle/views \ No newline at end of file diff --git a/tests/Application/translations/.gitignore b/tests/Application/translations/.gitignore deleted file mode 100644 index e69de29b..00000000 diff --git a/src/Resources/views/.gitkeep b/tests/Unit/.gitkeep similarity index 100% rename from src/Resources/views/.gitkeep rename to tests/Unit/.gitkeep diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..33a53433 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +use Symfony\Component\Dotenv\Dotenv; + +require dirname(__DIR__) . '/vendor/autoload.php'; + +if (file_exists(dirname(__DIR__) . '/config/bootstrap.php')) { + require dirname(__DIR__) . '/config/bootstrap.php'; +} elseif (method_exists(Dotenv::class, 'bootEnv')) { + (new Dotenv())->bootEnv(dirname(__DIR__) . '/.env'); +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..1236737e --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,24 @@ +const Encore = require('@symfony/webpack-encore'); + +Encore + // directory where compiled assets will be stored + .setOutputPath('src/Resources/public') + // public path used by the web server to access the output path + .setPublicPath('/public') + + // entries + .addEntry('monsieurbiz-search', './assets/js/app.js') + + // configuration + .disableSingleRuntimeChunk() + .cleanupOutputBeforeBuild() + .enableSourceMaps(!Encore.isProduction()) + .enableVersioning(Encore.isProduction()) + + // organise files + .configureFilenames({ + js: 'js/[name].js' + }) +; + +module.exports = Encore.getWebpackConfig();