├── tests
├── Application
│ ├── var
│ │ └── .gitkeep
│ ├── .gitignore
│ ├── app
│ │ ├── config
│ │ │ ├── config_test.yml
│ │ │ ├── config_dev.yml
│ │ │ ├── routing_dev.yml
│ │ │ ├── routing.yml
│ │ │ ├── config.yml
│ │ │ └── security.yml
│ │ └── AppKernel.php
│ ├── package.json
│ ├── Gulpfile.js
│ └── bin
│ │ └── console
├── Behat
│ └── Resources
│ │ ├── suites.yml
│ │ └── services.xml
├── Responses
│ └── Expected
│ │ └── WEB_GB
│ │ ├── en_GB
│ │ ├── product_list_page_filtered_by_production_year_2015_attribute.json
│ │ ├── product_list_page_filtered_by_production_year_2015_or_2020_and_mug_material_wood_attribute.json
│ │ ├── product_list_page_filtered_by_partial_phrase.json
│ │ ├── product_list_page_filtered_by_production_year_2015_or_2020_attribute.json
│ │ ├── product_list_page_filtered_by_phrase.json
│ │ ├── second_product_list_page_limited_to_two.json
│ │ ├── first_product_list_page_limited_to_two.json
│ │ ├── product_list_page_filtered_by_price_range.json
│ │ └── product_list_page_filtered_by_mug_material_wood_attribute.json
│ │ └── de_DE
│ │ ├── product_list_page_filtered_by_production_year_2015_attribute.json
│ │ ├── product_list_page_filtered_by_phrase.json
│ │ └── product_list_page_filtered_by_mug_material_wood_attribute.json
├── Event
│ └── ProductCreatedTest.php
├── DependencyInjection
│ ├── ConfigurationTest.php
│ └── SyliusElasticSearchExtensionTest.php
└── Factory
│ └── ProductDocumentFactoryTest.php
├── .gitignore
├── easy-coding-standard.neon
├── phpspec.yml.dist
├── src
├── Controller
│ ├── ImageView.php
│ ├── TaxonView.php
│ ├── PriceView.php
│ ├── AttributeView.php
│ ├── ProductListView.php
│ ├── VariantView.php
│ ├── ProductView.php
│ └── SearchController.php
├── Factory
│ ├── Document
│ │ ├── ImageDocumentFactoryInterface.php
│ │ ├── TaxonDocumentFactoryInterface.php
│ │ ├── PriceDocumentFactoryInterface.php
│ │ ├── OptionDocumentFactoryInterface.php
│ │ ├── VariantDocumentFactoryInterface.php
│ │ ├── AttributeDocumentFactoryInterface.php
│ │ ├── ProductDocumentFactoryInterface.php
│ │ ├── ImageDocumentFactory.php
│ │ ├── PriceDocumentFactory.php
│ │ ├── OptionDocumentFactory.php
│ │ ├── AttributeDocumentFactory.php
│ │ ├── TaxonDocumentFactory.php
│ │ ├── VariantDocumentFactory.php
│ │ └── ProductDocumentFactory.php
│ └── View
│ │ ├── ProductListViewFactoryInterface.php
│ │ └── ProductListViewFactory.php
├── Filter
│ ├── ViewData
│ │ ├── EmptyViewData.php
│ │ └── PagerAwareViewData.php
│ └── Widget
│ │ ├── MultiDynamicAggregateWithoutView.php
│ │ ├── SingleNestedTermChoice.php
│ │ ├── Pager.php
│ │ ├── MultiDynamicAggregateOverride.php
│ │ ├── InStock.php
│ │ ├── Sort.php
│ │ └── OptionMultiDynamicAggregate.php
├── Exception
│ └── UnsupportedFactoryMethodException.php
├── SyliusElasticSearchPlugin.php
├── Resources
│ └── config
│ │ ├── routing.yml
│ │ ├── services
│ │ ├── controller.xml
│ │ ├── command.xml
│ │ └── factories.xml
│ │ ├── app
│ │ └── config.yml
│ │ └── services.xml
├── Event
│ ├── ProductCreated.php
│ ├── ProductDeleted.php
│ └── ProductUpdated.php
├── Document
│ ├── ImageDocument.php
│ ├── PriceDocument.php
│ ├── AttributeDocument.php
│ ├── OptionDocument.php
│ ├── TaxonDocument.php
│ ├── VariantDocument.php
│ └── ProductDocument.php
├── DependencyInjection
│ ├── SyliusElasticSearchExtension.php
│ └── Configuration.php
├── Projection
│ └── ProductProjector.php
├── Command
│ ├── ResetProductIndexCommand.php
│ └── UpdateProductIndexCommand.php
└── EventListener
│ └── ProductPublisher.php
├── behat.yml.dist
├── README.md
├── phpunit.xml.dist
├── spec
├── Document
│ ├── ImageDocumentSpec.php
│ ├── PriceDocumentSpec.php
│ ├── AttributeDocumentSpec.php
│ ├── TaxonDocumentSpec.php
│ └── ProductDocumentSpec.php
└── Projection
│ └── ProductProjectorSpec.php
├── .travis.yml
└── composer.json
/tests/Application/var/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/Behat/Resources/suites.yml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /composer.lock
3 | /bin/
4 |
--------------------------------------------------------------------------------
/tests/Application/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 |
3 | /var/
4 | !/var/.gitkeep
5 |
6 | /web/
7 |
--------------------------------------------------------------------------------
/tests/Application/app/config/config_test.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: "config.yml" }
3 |
--------------------------------------------------------------------------------
/easy-coding-standard.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - vendor/sylius-labs/coding-standard/easy-coding-standard.neon
3 |
--------------------------------------------------------------------------------
/phpspec.yml.dist:
--------------------------------------------------------------------------------
1 | suites:
2 | default:
3 | namespace: Sylius\ElasticSearchPlugin
4 | psr4_prefix: Sylius\ElasticSearchPlugin
5 |
--------------------------------------------------------------------------------
/src/Controller/ImageView.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/tests/Application/app/config/config_dev.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: "config.yml" }
3 |
4 | framework:
5 | router: { resource: "%kernel.root_dir%/config/routing_dev.yml" }
6 | profiler: { only_exceptions: false }
7 |
8 | swiftmailer:
9 | disable_delivery: true
10 |
11 | web_profiler:
12 | toolbar: true
13 | intercept_redirects: false
14 |
--------------------------------------------------------------------------------
/src/Controller/PriceView.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | tests/
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/Application/app/config/routing.yml:
--------------------------------------------------------------------------------
1 | sylius_admin_dashboard_redirect:
2 | path: /admin
3 | defaults:
4 | _controller: FrameworkBundle:Redirect:redirect
5 | route: sylius_admin_dashboard
6 | permanent: true
7 |
8 | sylius_shop:
9 | resource: "@SyliusShopBundle/Resources/config/routing.yml"
10 |
11 | sylius_admin:
12 | resource: "@SyliusAdminBundle/Resources/config/routing.yml"
13 | prefix: /admin
14 |
15 | sylius_api:
16 | resource: "@SyliusAdminApiBundle/Resources/config/routing/main.yml"
17 | prefix: /api
18 |
19 | sylius_search:
20 | resource: "@SyliusElasticSearchPlugin/Resources/config/routing.yml"
21 | prefix: /
22 |
--------------------------------------------------------------------------------
/tests/Responses/Expected/WEB_GB/en_GB/product_list_page_filtered_by_production_year_2015_attribute.json:
--------------------------------------------------------------------------------
1 | {
2 | "page": 1,
3 | "limit": 9,
4 | "total": 1,
5 | "pages": 1,
6 | "items": [
7 | {
8 | "id": 1,
9 | "code": "LOGAN_MUG_CODE",
10 | "name": "Logan Mug",
11 | "slug": "@string@",
12 | "taxons": "@array@",
13 | "variants": "@array@",
14 | "attributes": "@array@",
15 | "images": "@array@",
16 | "price": "@array@",
17 | "channelCode": "WEB_GB",
18 | "localeCode": "en_GB"
19 | }
20 | ],
21 | "filters": "@array@"
22 | }
23 |
--------------------------------------------------------------------------------
/tests/Responses/Expected/WEB_GB/de_DE/product_list_page_filtered_by_production_year_2015_attribute.json:
--------------------------------------------------------------------------------
1 | {
2 | "page": 1,
3 | "limit": 9,
4 | "total": 1,
5 | "pages": 1,
6 | "items": [
7 | {
8 | "id": 1,
9 | "code": "LOGAN_MUG_CODE",
10 | "name": "Logan Becher",
11 | "slug": "@string@",
12 | "taxons": "@array@",
13 | "variants": "@array@",
14 | "attributes": "@array@",
15 | "images": "@array@",
16 | "price": "@array@",
17 | "channelCode": "WEB_GB",
18 | "localeCode": "de_DE"
19 | }
20 | ],
21 | "filters": "@array@"
22 | }
23 |
--------------------------------------------------------------------------------
/src/Controller/ProductListView.php:
--------------------------------------------------------------------------------
1 | prophesize(ProductInterface::class);
19 | $event = ProductCreated::occur($product->reveal());
20 |
21 | $this->assertEquals($product->reveal(), $event->product());
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/spec/Document/ImageDocumentSpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType(ImageDocument::class);
15 | }
16 |
17 | function it_has_code()
18 | {
19 | $this->setCode('abc');
20 |
21 | $this->getCode()->shouldReturn('abc');
22 | }
23 |
24 | function it_has_path()
25 | {
26 | $this->setPath('/abc');
27 |
28 | $this->getPath()->shouldReturn('/abc');
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/spec/Document/PriceDocumentSpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType(PriceDocument::class);
15 | }
16 |
17 | function it_has_amount()
18 | {
19 | $this->setAmount(1000);
20 |
21 | $this->getAmount()->shouldReturn(1000);
22 | }
23 |
24 | function it_has_currency()
25 | {
26 | $this->setCurrency('EUR');
27 |
28 | $this->getCurrency()->shouldReturn('EUR');
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Factory/Document/AttributeDocumentFactoryInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/spec/Document/AttributeDocumentSpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType(AttributeDocument::class);
15 | }
16 |
17 | function it_has_code()
18 | {
19 | $this->setCode('color');
20 |
21 | $this->getCode()->shouldReturn('color');
22 | }
23 |
24 | function it_has_name()
25 | {
26 | $this->setName('color');
27 |
28 | $this->getName()->shouldReturn('color');
29 | }
30 |
31 | function it_has_value()
32 | {
33 | $this->setValue('red');
34 |
35 | $this->getValue()->shouldReturn('red');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/Application/Gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var chug = require('gulp-chug');
3 | var argv = require('yargs').argv;
4 |
5 | config = [
6 | '--rootPath',
7 | argv.rootPath || '../../../../../../../tests/Application/web/assets/',
8 | '--nodeModulesPath',
9 | argv.nodeModulesPath || '../../../../../../../tests/Application/node_modules/',
10 | '--vendorPath',
11 | argv.vendorPath || '../../../../../../../vendor/'
12 | ];
13 |
14 | gulp.task('admin', function() {
15 | gulp.src('../../vendor/sylius/sylius/src/Sylius/Bundle/AdminBundle/Gulpfile.js', { read: false })
16 | .pipe(chug({ args: config }))
17 | ;
18 | });
19 |
20 | gulp.task('shop', function() {
21 | gulp.src('../../vendor/sylius/sylius/src/Sylius/Bundle/ShopBundle/Gulpfile.js', { read: false })
22 | .pipe(chug({ args: config }))
23 | ;
24 | });
25 |
26 | gulp.task('default', ['admin', 'shop']);
27 |
--------------------------------------------------------------------------------
/src/Event/ProductCreated.php:
--------------------------------------------------------------------------------
1 | product = $product;
22 | }
23 |
24 | /**
25 | * @param ProductInterface $product
26 | *
27 | * @return self
28 | */
29 | public static function occur(ProductInterface $product)
30 | {
31 | return new self($product);
32 | }
33 |
34 | /**
35 | * @return ProductInterface
36 | */
37 | public function product(): ProductInterface
38 | {
39 | return $this->product;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Event/ProductDeleted.php:
--------------------------------------------------------------------------------
1 | product = $product;
22 | }
23 |
24 | /**
25 | * @param ProductInterface $product
26 | *
27 | * @return self
28 | */
29 | public static function occur(ProductInterface $product)
30 | {
31 | return new self($product);
32 | }
33 |
34 | /**
35 | * @return ProductInterface
36 | */
37 | public function product(): ProductInterface
38 | {
39 | return $this->product;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Event/ProductUpdated.php:
--------------------------------------------------------------------------------
1 | product = $product;
22 | }
23 |
24 | /**
25 | * @param ProductInterface $product
26 | *
27 | * @return self
28 | */
29 | public static function occur(ProductInterface $product)
30 | {
31 | return new self($product);
32 | }
33 |
34 | /**
35 | * @return ProductInterface
36 | */
37 | public function product(): ProductInterface
38 | {
39 | return $this->product;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Factory/Document/ImageDocumentFactory.php:
--------------------------------------------------------------------------------
1 | imageDocumentClass = $imageDocumentClass;
18 | }
19 |
20 | public function create(ImageInterface $image): ImageDocument
21 | {
22 | /** @var ImageDocument $imageDocument */
23 | $imageDocument = new $this->imageDocumentClass();
24 | $imageDocument->setCode($image->getType());
25 | $imageDocument->setPath($image->getPath());
26 |
27 | return $imageDocument;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/Application/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev');
19 | $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(['--no-debug', '']) && $env !== 'prod';
20 |
21 | if ($debug) {
22 | Debug::enable();
23 | }
24 |
25 | $kernel = new AppKernel($env, $debug);
26 | $application = new Application($kernel);
27 | $application->run($input);
28 |
--------------------------------------------------------------------------------
/src/Filter/ViewData/PagerAwareViewData.php:
--------------------------------------------------------------------------------
1 | limit = $itemsPerPage;
27 | }
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | public function getSerializableData()
33 | {
34 | $data = parent::getSerializableData();
35 |
36 | $data['pager']['limit'] = $this->limit;
37 |
38 | return $data;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/Responses/Expected/WEB_GB/en_GB/product_list_page_filtered_by_partial_phrase.json:
--------------------------------------------------------------------------------
1 | {
2 | "page": 1,
3 | "limit": 9,
4 | "total": 2,
5 | "pages": 1,
6 | "items": [
7 | {
8 | "id": 1,
9 | "code": "LOGAN_MUG_CODE",
10 | "name": "Logan Mug",
11 | "slug": "@string@",
12 | "taxons": "@array@",
13 | "variants": "@array@",
14 | "attributes": "@array@",
15 | "images": "@array@",
16 | "price": "@array@",
17 | "channelCode": "WEB_GB",
18 | "localeCode": "en_GB"
19 | },
20 | {
21 | "id": 2,
22 | "code": "LOGAN_HAT_CODE",
23 | "name": "Logan Hat",
24 | "slug": "@string@",
25 | "taxons": "@array@",
26 | "variants": "@array@",
27 | "attributes": "@array@",
28 | "images": "@array@",
29 | "price": "@array@",
30 | "channelCode": "WEB_GB",
31 | "localeCode": "en_GB"
32 | }
33 | ],
34 | "filters": "@array@"
35 | }
36 |
--------------------------------------------------------------------------------
/src/Resources/config/services/command.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/tests/Responses/Expected/WEB_GB/en_GB/product_list_page_filtered_by_production_year_2015_or_2020_attribute.json:
--------------------------------------------------------------------------------
1 | {
2 | "page": 1,
3 | "limit": 9,
4 | "total": 2,
5 | "pages": 1,
6 | "items": [
7 | {
8 | "id": 1,
9 | "code": "LOGAN_MUG_CODE",
10 | "name": "Logan Mug",
11 | "slug": "@string@",
12 | "taxons": "@array@",
13 | "variants": "@array@",
14 | "attributes": "@array@",
15 | "images": "@array@",
16 | "price": "@array@",
17 | "channelCode": "WEB_GB",
18 | "localeCode": "en_GB"
19 | },
20 | {
21 | "id": 3,
22 | "code": "LOGAN_T_SHIRT_CODE",
23 | "name": "Logan T-Shirt",
24 | "slug": "@string@",
25 | "taxons": "@array@",
26 | "variants": "@array@",
27 | "attributes": "@array@",
28 | "images": "@array@",
29 | "price": "@array@",
30 | "channelCode": "WEB_GB",
31 | "localeCode": "en_GB"
32 | }
33 | ],
34 | "filters": "@array@"
35 | }
36 |
--------------------------------------------------------------------------------
/src/Filter/Widget/SingleNestedTermChoice.php:
--------------------------------------------------------------------------------
1 | ', $this->getDocumentField());
25 |
26 | if ($state && $state->isActive()) {
27 | $search->addPostFilter(
28 | new NestedQuery(
29 | $path,
30 | new TermQuery($field, $state->getValue())
31 | )
32 | );
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/spec/Document/TaxonDocumentSpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType(TaxonDocument::class);
16 | }
17 |
18 | function it_has_code()
19 | {
20 | $this->setCode('mug');
21 |
22 | $this->getCode()->shouldReturn('mug');
23 | }
24 |
25 | function it_has_slug()
26 | {
27 | $this->setSlug('/mug');
28 |
29 | $this->getSlug()->shouldReturn('/mug');
30 | }
31 |
32 | function it_has_images()
33 | {
34 | $images = new Collection();
35 | $this->setImages($images);
36 |
37 | $this->getImages()->shouldReturn($images);
38 | }
39 |
40 | function it_has_description()
41 | {
42 | $this->setDescription('Lorem ipsum');
43 |
44 | $this->getDescription()->shouldReturn('Lorem ipsum');
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Document/ImageDocument.php:
--------------------------------------------------------------------------------
1 | code;
34 | }
35 |
36 | /**
37 | * @param string $code
38 | */
39 | public function setCode(?string $code): void
40 | {
41 | $this->code = $code;
42 | }
43 |
44 | /**
45 | * @return string
46 | */
47 | public function getPath(): string
48 | {
49 | return $this->path;
50 | }
51 |
52 | /**
53 | * @param string $path
54 | */
55 | public function setPath(string $path): void
56 | {
57 | $this->path = $path;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Controller/ProductView.php:
--------------------------------------------------------------------------------
1 | priceDocumentClass = $priceDocumentClass;
21 | }
22 |
23 | public function create(
24 | ChannelPricingInterface $channelPricing,
25 | CurrencyInterface $currency
26 | ): PriceDocument {
27 | /** @var PriceDocument $price */
28 | $price = new $this->priceDocumentClass();
29 | $originalAmount = $channelPricing->getOriginalPrice();
30 |
31 | $price->setAmount($channelPricing->getPrice());
32 | $price->setCurrency($currency->getCode());
33 | $price->setOriginalAmount(null !== $originalAmount && $originalAmount > 0 ? $originalAmount : 0);
34 |
35 | return $price;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.1
5 |
6 | jdk:
7 | - oraclejdk8
8 |
9 | addons:
10 | apt:
11 | sources:
12 | - elasticsearch-5.x
13 | packages:
14 | - elasticsearch
15 | - oracle-java8-set-default
16 |
17 | services:
18 | - elasticsearch
19 |
20 | cache:
21 | directories:
22 | - vendor
23 |
24 | before_install:
25 | - phpenv config-rm xdebug.ini || true
26 | - echo "memory_limit=4096M" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
27 |
28 | - composer self-update
29 | - composer validate
30 |
31 | install:
32 | - composer update --prefer-dist
33 | - tests/Application/bin/console doctrine:schema:create --env=test -vvv --no-interaction
34 | - tests/Application/bin/console ongr:es:index:create -vvv --no-interaction
35 |
36 | script:
37 | - bin/phpspec run --no-interaction
38 | - bin/phpunit
39 | - tests/Application/bin/console sylius:elastic-search:update-product-index --env=test -vvv && tests/Application/bin/console sylius:elastic-search:update-product-index --env=test -vvv
40 | - tests/Application/bin/console sylius:elastic-search:reset-product-index --env=test --force -vvv && tests/Application/bin/console sylius:elastic-search:reset-product-index --env=test --force -vvv
41 | #- bin/behat --strict -vvv --no-interaction
42 |
--------------------------------------------------------------------------------
/src/DependencyInjection/SyliusElasticSearchExtension.php:
--------------------------------------------------------------------------------
1 | processConfiguration($this->getConfiguration([], $container), $configs);
20 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
21 |
22 | $container->setParameter('sylius_elastic_search.attribute_whitelist', $config['attribute_whitelist']);
23 |
24 | foreach ($config['document_classes'] as $document => $class) {
25 | $container->setParameter(sprintf('sylius_elastic_search.document.%s.class', $document), $class);
26 | }
27 |
28 | foreach ($config['view_classes'] as $view => $class) {
29 | $container->setParameter(sprintf('sylius_elastic_search.view.%s.class', $view), $class);
30 | }
31 |
32 | $loader->load('services.xml');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Factory/Document/OptionDocumentFactory.php:
--------------------------------------------------------------------------------
1 | optionDocumentClass = $optionDocumentClass;
21 | }
22 |
23 | public function create(
24 | ProductOptionValueInterface $optionValue,
25 | LocaleInterface $locale
26 | ): OptionDocument {
27 | /** @var ProductOptionValueTranslationInterface $optionValueTranslation */
28 | $optionValueTranslation = $optionValue->getTranslation($locale->getCode());
29 |
30 | /** @var ProductOptionTranslationInterface $productOptionTranslation */
31 | $productOptionTranslation = $optionValue->getOption()->getTranslation($locale->getCode());
32 |
33 | /** @var OptionDocument $option */
34 | $option = new $this->optionDocumentClass();
35 | $option->setCode($optionValue->getOptionCode());
36 | $option->setName($productOptionTranslation->getName());
37 | $option->setValue($optionValueTranslation->getValue());
38 |
39 | return $option;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Document/PriceDocument.php:
--------------------------------------------------------------------------------
1 | amount;
41 | }
42 |
43 | /**
44 | * @param int $amount
45 | */
46 | public function setAmount(int $amount): void
47 | {
48 | $this->amount = $amount;
49 | }
50 |
51 | /**
52 | * @return int
53 | */
54 | public function getOriginalAmount(): int
55 | {
56 | return $this->originalAmount;
57 | }
58 |
59 | /**
60 | * @param int $originalAmount
61 | */
62 | public function setOriginalAmount(int $originalAmount = 0): void
63 | {
64 | $this->originalAmount = $originalAmount;
65 | }
66 |
67 | /**
68 | * @return string
69 | */
70 | public function getCurrency(): string
71 | {
72 | return $this->currency;
73 | }
74 |
75 | /**
76 | * @param string $currency
77 | */
78 | public function setCurrency(string $currency): void
79 | {
80 | $this->currency = $currency;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tests/Application/app/AppKernel.php:
--------------------------------------------------------------------------------
1 | load($this->getRootDir() . '/config/config_' . $this->getEnvironment() . '.yml');
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | public function getCacheDir(): string
42 | {
43 | return sprintf('%s/%s/cache', sys_get_temp_dir(), md5(__DIR__));
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public function getLogDir(): string
50 | {
51 | return sprintf('%s/%s/logs', sys_get_temp_dir(), md5(__DIR__));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/Responses/Expected/WEB_GB/de_DE/product_list_page_filtered_by_phrase.json:
--------------------------------------------------------------------------------
1 | {
2 | "page": 1,
3 | "limit": 9,
4 | "total": 1,
5 | "pages": 1,
6 | "items": [
7 | {
8 | "id": 2,
9 | "code": "LOGAN_HAT_CODE",
10 | "name": "Logan Hut",
11 | "slug": "@string@",
12 | "taxons": "@array@",
13 | "variants": "@array@",
14 | "attributes": "@array@",
15 | "images": "@array@",
16 | "price": "@array@",
17 | "channelCode": "WEB_GB",
18 | "localeCode": "de_DE"
19 | }
20 | ],
21 | "filters": {
22 | "channel": "@array@",
23 | "enabled": "@array@",
24 | "taxonCode": "@array@",
25 | "taxonSlug": "@array@",
26 | "priceRange": "@array@",
27 | "locale": "@array@",
28 | "inStock": "@array@",
29 | "paginator": "@array@",
30 | "search": {
31 | "state": {
32 | "active": true,
33 | "value": "hut",
34 | "urlParameters": {
35 | "search": "hut"
36 | },
37 | "name": "search",
38 | "options": []
39 | },
40 | "tags": [],
41 | "urlParameters": {
42 | "channel": "WEB_GB",
43 | "enabled": true,
44 | "locale": "de_DE",
45 | "search": "hut"
46 | },
47 | "resetUrlParameters": {
48 | "channel": "WEB_GB",
49 | "enabled": true,
50 | "locale": "de_DE"
51 | },
52 | "name": "search"
53 | },
54 | "attributes": "@array@",
55 | "attributesByCode": "@array@",
56 | "options": "@array@",
57 | "sort": "@array@"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/Responses/Expected/WEB_GB/en_GB/product_list_page_filtered_by_phrase.json:
--------------------------------------------------------------------------------
1 | {
2 | "page": 1,
3 | "limit": 9,
4 | "total": 1,
5 | "pages": 1,
6 | "items": [
7 | {
8 | "id": 2,
9 | "code": "LOGAN_HAT_CODE",
10 | "name": "Logan Hat",
11 | "slug": "@string@",
12 | "taxons": "@array@",
13 | "variants": "@array@",
14 | "attributes": "@array@",
15 | "images": "@array@",
16 | "price": "@array@",
17 | "channelCode": "WEB_GB",
18 | "localeCode": "en_GB"
19 | }
20 | ],
21 | "filters": {
22 | "channel": "@array@",
23 | "enabled": "@array@",
24 | "taxonCode": "@array@",
25 | "taxonSlug": "@array@",
26 | "priceRange": "@array@",
27 | "locale": "@array@",
28 | "inStock": "@array@",
29 | "paginator": "@array@",
30 | "search": {
31 | "state": {
32 | "active": true,
33 | "value": "hat",
34 | "urlParameters": {
35 | "search": "hat"
36 | },
37 | "name": "search",
38 | "options": []
39 | },
40 | "tags": [],
41 | "urlParameters": {
42 | "channel": "WEB_GB",
43 | "enabled": true,
44 | "locale": "en_GB",
45 | "search": "hat"
46 | },
47 | "resetUrlParameters": {
48 | "channel": "WEB_GB",
49 | "enabled": true,
50 | "locale": "en_GB"
51 | },
52 | "name": "search"
53 | },
54 | "attributes": "@array@",
55 | "attributesByCode": "@array@",
56 | "options": "@array@",
57 | "sort": "@array@"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/Responses/Expected/WEB_GB/en_GB/second_product_list_page_limited_to_two.json:
--------------------------------------------------------------------------------
1 | {
2 | "page": 2,
3 | "limit": 2,
4 | "total": 3,
5 | "pages": 2,
6 | "items": [
7 | {
8 | "id": 3,
9 | "code": "LOGAN_T_SHIRT_CODE",
10 | "name": "Logan T-Shirt",
11 | "slug": "@string@",
12 | "taxons": "@array@",
13 | "variants": "@array@",
14 | "attributes": "@array@",
15 | "images": "@array@",
16 | "price": "@array@",
17 | "channelCode": "WEB_GB",
18 | "localeCode": "en_GB"
19 | }
20 | ],
21 | "filters": {
22 | "channel": "@array@",
23 | "enabled": "@array@",
24 | "taxonCode": "@array@",
25 | "taxonSlug": "@array@",
26 | "priceRange": "@array@",
27 | "locale": "@array@",
28 | "inStock": "@array@",
29 | "paginator": {
30 | "state": {
31 | "active": true,
32 | "value": 2,
33 | "urlParameters": [],
34 | "name": "paginator",
35 | "options": {
36 | "limit": 2
37 | }
38 | },
39 | "tags": [],
40 | "urlParameters": {
41 | "channel": "WEB_GB",
42 | "enabled": true,
43 | "locale": "en_GB"
44 | },
45 | "resetUrlParameters": {
46 | "channel": "WEB_GB",
47 | "enabled": true,
48 | "locale": "en_GB"
49 | },
50 | "name": "paginator",
51 | "currentPage": 2,
52 | "totalItems": 3,
53 | "maxPages": 20,
54 | "itemsPerPage": 2,
55 | "numPages": 2,
56 | "options": [],
57 | "limit": 2
58 | },
59 | "search": "@array@",
60 | "attributes": "@array@",
61 | "attributesByCode": "@array@",
62 | "options": "@array@",
63 | "sort": "@array@"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/Application/app/config/config.yml:
--------------------------------------------------------------------------------
1 | parameters:
2 | locale: "en_GB"
3 | secret: "Three can keep a secret, if two of them are dead."
4 |
5 | imports:
6 | - { resource: "@SyliusCoreBundle/Resources/config/app/config.yml" }
7 | - { resource: "@SyliusAdminBundle/Resources/config/app/config.yml" }
8 | - { resource: "@SyliusShopBundle/Resources/config/app/config.yml" }
9 | - { resource: "@SyliusAdminApiBundle/Resources/config/app/config.yml" }
10 | - { resource: "@SyliusElasticSearchPlugin/Resources/config/app/config.yml" }
11 |
12 | - { resource: "security.yml" }
13 |
14 | framework:
15 | translator: { fallbacks: ["%locale%"] }
16 | secret: "%secret"
17 | router:
18 | resource: "%kernel.root_dir%/config/routing.yml"
19 | strict_requirements: "%kernel.debug%"
20 | form: true
21 | csrf_protection: true
22 | validation: { enable_annotations: true }
23 | templating: { engines: ["twig"] }
24 | default_locale: "%locale%"
25 | trusted_proxies: ~
26 | session:
27 | storage_id: session.storage.mock_file
28 | handler_id: ~
29 | test: ~
30 |
31 | doctrine:
32 | dbal:
33 | driver: "pdo_sqlite"
34 | path: "%kernel.root_dir%/../var/db.sql"
35 | charset: UTF8
36 |
37 | fos_rest:
38 | exception: ~
39 | view:
40 | formats:
41 | json: true
42 | xml: true
43 | empty_content: 204
44 | format_listener:
45 | rules:
46 | - { path: '^/api', priorities: ['json', 'xml'], fallback_format: json, prefer_extension: true }
47 | - { path: '^/', stop: true }
48 |
49 | sylius_resource:
50 | drivers:
51 | - doctrine/orm
52 |
53 | sylius_grid:
54 | drivers:
55 | - doctrine/orm
56 |
57 | ongr_elasticsearch:
58 | managers:
59 | default:
60 | index:
61 | index_name: sylius
62 | mappings:
63 | SyliusElasticSearchPlugin: {}
64 |
65 | sylius_elastic_search:
66 | attribute_whitelist: ['MUG_COLLECTION_CODE', 'MUG_MATERIAL_CODE', 'PRODUCTION_YEAR', 'AVAILABLE_FROM']
67 |
--------------------------------------------------------------------------------
/src/Factory/Document/AttributeDocumentFactory.php:
--------------------------------------------------------------------------------
1 | attributeDocumentClass = $attributeDocumentClass;
20 | }
21 |
22 | public function create(
23 | $data,
24 | LocaleInterface $locale,
25 | ProductAttributeValueInterface $productAttributeValue
26 | ): array {
27 | $productAttributes = [];
28 |
29 | if (is_array($data)) {
30 | foreach ($data as $value) {
31 | $productAttributes = array_merge(
32 | $productAttributes,
33 | $this->create(
34 | $value,
35 | $locale,
36 | $productAttributeValue
37 | )
38 | );
39 | }
40 | } else {
41 | /** @var AttributeDocument $productAttribute */
42 | $productAttribute = new $this->attributeDocumentClass();
43 | $productAttribute->setCode($productAttributeValue->getCode());
44 | $productAttribute->setValue($data);
45 | /** @var ProductAttributeTranslationInterface $productAttributeTranslation */
46 | $productAttributeTranslation = $productAttributeValue->getAttribute()->getTranslation(
47 | $locale->getCode()
48 | );
49 | $productAttribute->setName($productAttributeTranslation->getName());
50 | $productAttributes = [$productAttribute];
51 | }
52 |
53 | return $productAttributes;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sylius/elastic-search-plugin",
3 | "type": "sylius-bundle",
4 | "description": "Elastic search integration for Sylius.",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Arkadiusz Krakowiak",
9 | "email": "arkadiusz.k.e@gmail.com",
10 | "homepage": "http://github.com/arminek"
11 | }
12 | ],
13 | "require": {
14 | "php": "^7.1",
15 | "sylius/sylius": "^1.0",
16 | "ongr/elasticsearch-dsl": "^5.0",
17 | "ongr/elasticsearch-bundle": "^5.0",
18 | "simple-bus/symfony-bridge": "^4.1",
19 | "ongr/filter-manager-bundle": "2.1.x-dev as v2.1.1",
20 | "ramsey/uuid": "^3.7"
21 | },
22 | "require-dev": {
23 | "behat/behat": "^3.2",
24 | "behat/mink": "^1.7",
25 | "behat/mink-browserkit-driver": "^1.3",
26 | "behat/mink-extension": "^2.2",
27 | "friends-of-behat/context-service-extension": "^0.3",
28 | "friends-of-behat/cross-container-extension": "^0.2",
29 | "friends-of-behat/performance-extension": "^1.0",
30 | "friends-of-behat/service-container-extension": "^0.3",
31 | "friends-of-behat/symfony-extension": "^0.2.1",
32 | "friends-of-behat/variadic-extension": "^0.1",
33 | "lakion/mink-debug-extension": "^1.2.3",
34 | "matthiasnoback/symfony-config-test": "^2.1",
35 | "matthiasnoback/symfony-dependency-injection-test": "^1.1",
36 | "php-http/guzzle6-adapter": "^1.1",
37 | "phpspec/phpspec": "^3.2",
38 | "phpunit/phpunit": "^5.6",
39 | "behat/mink-selenium2-driver": "^1.3",
40 | "lakion/api-test-case": "^1.1",
41 | "doctrine/common": "<2.8",
42 | "symplify/easy-coding-standard": "^2.4",
43 | "sylius-labs/coding-standard": "^1.0"
44 | },
45 | "config": {
46 | "bin-dir": "bin"
47 | },
48 | "minimum-stability": "stable",
49 | "prefer-stable": true,
50 | "autoload": {
51 | "psr-4": {
52 | "Sylius\\ElasticSearchPlugin\\": "src/",
53 | "Tests\\Sylius\\ElasticSearchPlugin\\": "tests/"
54 | }
55 | },
56 | "autoload-dev": {
57 | "classmap": ["tests/Application/app/AppKernel.php"]
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Document/AttributeDocument.php:
--------------------------------------------------------------------------------
1 | code;
61 | }
62 |
63 | /**
64 | * @param string $code
65 | */
66 | public function setCode(string $code): void
67 | {
68 | $this->code = $code;
69 | }
70 |
71 | /**
72 | * @return string
73 | */
74 | public function getName(): ?string
75 | {
76 | return $this->name;
77 | }
78 |
79 | /**
80 | * @param string $name
81 | */
82 | public function setName(?string $name): void
83 | {
84 | $this->name = $name;
85 | }
86 |
87 | /**
88 | * @return mixed
89 | */
90 | public function getValue()
91 | {
92 | return $this->value;
93 | }
94 |
95 | /**
96 | * @param mixed $value
97 | */
98 | public function setValue($value): void
99 | {
100 | $this->value = $value;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/Document/OptionDocument.php:
--------------------------------------------------------------------------------
1 | code;
61 | }
62 |
63 | /**
64 | * @param string $code
65 | */
66 | public function setCode(string $code): void
67 | {
68 | $this->code = $code;
69 | }
70 |
71 | /**
72 | * @return string
73 | */
74 | public function getName(): string
75 | {
76 | return $this->name;
77 | }
78 |
79 | /**
80 | * @param string $name
81 | */
82 | public function setName(string $name): void
83 | {
84 | $this->name = $name;
85 | }
86 |
87 | /**
88 | * @return string
89 | */
90 | public function getValue(): string
91 | {
92 | return $this->value;
93 | }
94 |
95 | /**
96 | * @param string $value
97 | */
98 | public function setValue(string $value): void
99 | {
100 | $this->value = $value;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/Factory/Document/TaxonDocumentFactory.php:
--------------------------------------------------------------------------------
1 | taxonDocumentClass = $taxonDocumentClass;
25 | $this->imageDocumentFactory = $imageDocumentFactory;
26 | }
27 |
28 | /**
29 | * @param TaxonInterface $taxon Sylius taxon model
30 | * @param LocaleInterface $localeCode
31 | * @param int|null $position Override the position in the Taxon model by passing your own
32 | *
33 | * @return TaxonDocument
34 | */
35 | public function create(TaxonInterface $taxon, LocaleInterface $localeCode, ?int $position = null): TaxonDocument
36 | {
37 | /** @var TaxonTranslationInterface $taxonTranslation */
38 | $taxonTranslation = $taxon->getTranslation($localeCode->getCode());
39 |
40 | /** @var TaxonDocument $taxonDocument */
41 | $taxonDocument = new $this->taxonDocumentClass();
42 | $taxonDocument->setCode($taxon->getCode());
43 | $taxonDocument->setSlug($taxonTranslation->getSlug());
44 | if (is_int($position)) {
45 | $taxonDocument->setPosition($position);
46 | } else {
47 | $taxonDocument->setPosition($taxon->getPosition());
48 | }
49 |
50 | $taxonDocument->setDescription($taxonTranslation->getDescription());
51 |
52 | /** @var ImageDocument[] $images */
53 | $images = [];
54 | foreach ($taxon->getImages() as $image) {
55 | $images[] = $this->imageDocumentFactory->create($image);
56 | }
57 | $taxonDocument->setImages(new Collection($images));
58 |
59 | return $taxonDocument;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/Responses/Expected/WEB_GB/en_GB/first_product_list_page_limited_to_two.json:
--------------------------------------------------------------------------------
1 | {
2 | "page": 1,
3 | "limit": 2,
4 | "total": 3,
5 | "pages": 2,
6 | "items": [
7 | {
8 | "id": 1,
9 | "code": "LOGAN_MUG_CODE",
10 | "name": "Logan Mug",
11 | "slug": "@string@",
12 | "taxons": "@array@",
13 | "variants": "@array@",
14 | "attributes": "@array@",
15 | "images": "@array@",
16 | "price": "@array@",
17 | "channelCode": "WEB_GB",
18 | "localeCode": "en_GB"
19 | },
20 | {
21 | "id": 2,
22 | "code": "LOGAN_HAT_CODE",
23 | "name": "Logan Hat",
24 | "slug": "@string@",
25 | "taxons": "@array@",
26 | "variants": "@array@",
27 | "attributes": "@array@",
28 | "images": "@array@",
29 | "price": "@array@",
30 | "channelCode": "WEB_GB",
31 | "localeCode": "en_GB"
32 | }
33 | ],
34 | "filters": {
35 | "channel": "@array@",
36 | "enabled": "@array@",
37 | "taxonCode": "@array@",
38 | "taxonSlug": "@array@",
39 | "priceRange": "@array@",
40 | "locale": "@array@",
41 | "inStock": "@array@",
42 | "paginator": {
43 | "state": {
44 | "active": true,
45 | "value": 1,
46 | "urlParameters": [],
47 | "name": "paginator",
48 | "options": {
49 | "limit": 2
50 | }
51 | },
52 | "tags": [],
53 | "urlParameters": {
54 | "channel": "WEB_GB",
55 | "enabled": true,
56 | "locale": "en_GB"
57 | },
58 | "resetUrlParameters": {
59 | "channel": "WEB_GB",
60 | "enabled": true,
61 | "locale": "en_GB"
62 | },
63 | "name": "paginator",
64 | "currentPage": 1,
65 | "totalItems": 3,
66 | "maxPages": 20,
67 | "itemsPerPage": 2,
68 | "numPages": 2,
69 | "options": [],
70 | "limit": 2
71 | },
72 | "search": "@array@",
73 | "attributes": "@array@",
74 | "attributesByCode": "@array@",
75 | "options": "@array@",
76 | "sort": "@array@"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Resources/config/services/factories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | %sylius_elastic_search.document.product.class%
6 |
7 |
8 |
9 |
10 |
11 | %sylius_elastic_search.attribute_whitelist%
12 |
13 |
14 |
15 | %sylius_elastic_search.document.taxon.class%
16 |
17 |
18 |
19 |
20 | %sylius_elastic_search.document.attribute.class%
21 |
22 |
23 |
24 | %sylius_elastic_search.document.price.class%
25 |
26 |
27 |
28 | %sylius_elastic_search.document.image.class%
29 |
30 |
31 |
32 | %sylius_elastic_search.document.option.class%
33 |
34 |
35 |
36 | %sylius_elastic_search.document.variant.class%
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/spec/Document/ProductDocumentSpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType(ProductDocument::class);
18 | }
19 |
20 | function it_has_code()
21 | {
22 | $this->setCode('Mug');
23 |
24 | $this->getCode()->shouldReturn('Mug');
25 | }
26 |
27 | function it_has_name()
28 | {
29 | $this->setName('Big Mug');
30 |
31 | $this->getName()->shouldReturn('Big Mug');
32 | }
33 |
34 | function it_has_channel_code()
35 | {
36 | $this->setChannelCode('WEB');
37 |
38 | $this->getChannelCode()->shouldReturn('WEB');
39 | }
40 |
41 | function it_has_locale_code()
42 | {
43 | $this->setLocaleCode('en');
44 |
45 | $this->getLocaleCode()->shouldReturn('en');
46 | }
47 |
48 | function it_has_description()
49 | {
50 | $this->setDescription('Lorem ipsum');
51 |
52 | $this->getDescription()->shouldReturn('Lorem ipsum');
53 | }
54 |
55 | function it_has_price()
56 | {
57 | $price = new PriceDocument();
58 | $this->setPrice($price);
59 |
60 | $this->getPrice()->shouldReturn($price);
61 | }
62 |
63 | function it_has_main_taxon()
64 | {
65 | $taxon = new TaxonDocument();
66 | $this->setMainTaxon($taxon);
67 |
68 | $this->getMainTaxon()->shouldReturn($taxon);
69 | }
70 |
71 | function it_has_taxons()
72 | {
73 | $taxons = new Collection();
74 | $this->setTaxons($taxons);
75 |
76 | $this->getTaxons()->shouldReturn($taxons);
77 | }
78 |
79 | function it_has_attributes()
80 | {
81 | $attributeValues = new Collection();
82 | $this->setAttributes($attributeValues);
83 |
84 | $this->getAttributes()->shouldReturn($attributeValues);
85 | }
86 |
87 | function it_has_slug()
88 | {
89 | $this->setSlug('/mug');
90 |
91 | $this->getSlug()->shouldReturn('/mug');
92 | }
93 |
94 | function it_has_images()
95 | {
96 | $images = new Collection();
97 | $this->setImages($images);
98 |
99 | $this->getImages()->shouldReturn($images);
100 | }
101 |
102 | function it_has_average_review_rating()
103 | {
104 | $this->setAverageReviewRating(2.4);
105 |
106 | $this->getAverageReviewRating()->shouldReturn(2.4);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/Document/TaxonDocument.php:
--------------------------------------------------------------------------------
1 | images = new Collection();
53 | }
54 |
55 | /**
56 | * @return string
57 | */
58 | public function getCode(): string
59 | {
60 | return $this->code;
61 | }
62 |
63 | /**
64 | * @param string $code
65 | */
66 | public function setCode(string $code): void
67 | {
68 | $this->code = $code;
69 | }
70 |
71 | /**
72 | * @return string
73 | */
74 | public function getSlug(): string
75 | {
76 | return $this->slug;
77 | }
78 |
79 | /**
80 | * @param string $slug
81 | */
82 | public function setSlug(string $slug): void
83 | {
84 | $this->slug = $slug;
85 | }
86 |
87 | /**
88 | * @return int
89 | */
90 | public function getPosition(): int
91 | {
92 | return $this->position;
93 | }
94 |
95 | /**
96 | * @param int $position
97 | */
98 | public function setPosition(int $position): void
99 | {
100 | $this->position = $position;
101 | }
102 |
103 | /**
104 | * @return Collection
105 | */
106 | public function getImages(): Collection
107 | {
108 | return $this->images;
109 | }
110 |
111 | /**
112 | * @param Collection $images
113 | */
114 | public function setImages(Collection $images): void
115 | {
116 | $this->images = $images;
117 | }
118 |
119 | /**
120 | * @return string|null
121 | */
122 | public function getDescription(): ?string
123 | {
124 | return $this->description;
125 | }
126 |
127 | /**
128 | * @param string|null $description
129 | */
130 | public function setDescription(?string $description): void
131 | {
132 | $this->description = $description;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/Filter/Widget/Pager.php:
--------------------------------------------------------------------------------
1 | getOption('limit', 10);
28 | }
29 |
30 | return (int) $this->getOption('limit', 10);
31 | }
32 |
33 | /**
34 | * {@inheritdoc}
35 | */
36 | public function getState(Request $request): FilterState
37 | {
38 | $state = parent::getState($request);
39 | // Reset pager with any filter.
40 | $state->setUrlParameters([]);
41 | $page = (int) $state->getValue();
42 | $state->setValue($page < 1 ? 1 : $page);
43 | $state->addOption('limit', (int) $request->get('limit', (int) $this->getOption('limit', 10)));
44 |
45 | return $state;
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | public function modifySearch(Search $search, ?FilterState $state = null, ?SearchRequest $request = null): void
52 | {
53 | if ($state && $state->isActive()) {
54 | $search->setFrom($this->getCountPerPage($state) * ($state->getValue() - 1));
55 | }
56 |
57 | $search->setSize($this->getCountPerPage($state));
58 | }
59 |
60 | /**
61 | * {@inheritdoc}
62 | */
63 | public function preProcessSearch(Search $search, Search $relatedSearch, ?FilterState $state = null)
64 | {
65 | // Nothing to do here.
66 | }
67 |
68 | /**
69 | * {@inheritdoc}
70 | */
71 | public function createViewData(): PagerAwareViewData
72 | {
73 | return new PagerAwareViewData();
74 | }
75 |
76 | /**
77 | * {@inheritdoc}
78 | */
79 | public function getViewData(DocumentIterator $result, ViewData $data): ViewData
80 | {
81 | /** @var ViewData\PagerAwareViewData $data */
82 | $data->setData(
83 | $result->count(),
84 | $data->getState()->getValue(),
85 | $data->getState()->getOption('limit'),
86 | $this->getOption('max_pages', 10)
87 | );
88 |
89 | return $data;
90 | }
91 |
92 | /**
93 | * {@inheritdoc}
94 | */
95 | public function isRelated(): bool
96 | {
97 | return false;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tests/Application/app/config/security.yml:
--------------------------------------------------------------------------------
1 | security:
2 | providers:
3 | sylius_admin_user_provider:
4 | id: sylius.admin_user_provider.email_or_name_based
5 | sylius_shop_user_provider:
6 | id: sylius.shop_user_provider.email_or_name_based
7 | encoders:
8 | Sylius\Component\User\Model\UserInterface: sha512
9 | firewalls:
10 | admin:
11 | switch_user: true
12 | context: admin
13 | pattern: /admin(?:/.*)?$
14 | form_login:
15 | provider: sylius_admin_user_provider
16 | login_path: sylius_admin_login
17 | check_path: sylius_admin_login_check
18 | failure_path: sylius_admin_login
19 | default_target_path: sylius_admin_dashboard
20 | use_forward: false
21 | use_referer: true
22 | logout:
23 | path: sylius_admin_logout
24 | target: sylius_admin_login
25 | anonymous: true
26 |
27 | oauth_token:
28 | pattern: ^/api/oauth/v2/token
29 | security: false
30 |
31 | api:
32 | pattern: ^/api
33 | fos_oauth: true
34 | stateless: true
35 | anonymous: true
36 |
37 | shop:
38 | switch_user: { role: ROLE_ALLOWED_TO_SWITCH }
39 | context: shop
40 | pattern: /.*
41 | form_login:
42 | success_handler: sylius.authentication.success_handler
43 | failure_handler: sylius.authentication.failure_handler
44 | provider: sylius_shop_user_provider
45 | login_path: sylius_shop_login
46 | check_path: sylius_shop_login_check
47 | failure_path: sylius_shop_login
48 | default_target_path: sylius_shop_homepage
49 | use_forward: false
50 | use_referer: true
51 | remember_me:
52 | secret: "%secret%"
53 | name: APP_REMEMBER_ME
54 | lifetime: 31536000
55 | always_remember_me: true
56 | remember_me_parameter: _remember_me
57 | logout:
58 | path: sylius_shop_logout
59 | target: sylius_shop_login
60 | invalidate_session: false
61 | success_handler: sylius.handler.shop_user_logout
62 | anonymous: true
63 |
64 | dev:
65 | pattern: ^/(_(profiler|wdt)|css|images|js)/
66 | security: false
67 |
68 | access_control:
69 | - { path: "^/_partial.*", ip: 127.0.0.1 }
70 |
71 | - { path: ^/login.*, role: IS_AUTHENTICATED_ANONYMOUSLY }
72 | - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
73 | - { path: ^/verify, role: IS_AUTHENTICATED_ANONYMOUSLY }
74 | - { path: "/account.*", role: ROLE_USER }
75 |
76 | - { path: ^/admin/login, role: IS_AUTHENTICATED_ANONYMOUSLY }
77 | - { path: ^/admin/login-check, role: IS_AUTHENTICATED_ANONYMOUSLY }
78 | - { path: "^/admin.*", role: ROLE_ADMINISTRATION_ACCESS }
79 |
80 | - { path: ^/api, role: ROLE_API_ACCESS }
81 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/ConfigurationTest.php:
--------------------------------------------------------------------------------
1 |
34 | */
35 | final class ConfigurationTest extends \PHPUnit_Framework_TestCase
36 | {
37 | use ConfigurationTestCaseTrait;
38 |
39 | /**
40 | * @test
41 | */
42 | public function it_has_document_classes()
43 | {
44 | $this->assertProcessedConfigurationEquals([], ['document_classes' => [
45 | 'product' => ProductDocument::class,
46 | 'attribute' => AttributeDocument::class,
47 | 'image' => ImageDocument::class,
48 | 'price' => PriceDocument::class,
49 | 'taxon' => TaxonDocument::class,
50 | 'variant' => VariantDocument::class,
51 | 'option' => OptionDocument::class,
52 | ]], 'document_classes');
53 | }
54 |
55 | /**
56 | * @test
57 | */
58 | public function it_has_view_classes()
59 | {
60 | $this->assertProcessedConfigurationEquals([], ['view_classes' => [
61 | 'product_list' => ProductListView::class,
62 | 'product' => ProductView::class,
63 | 'product_variant' => VariantView::class,
64 | 'attribute' => AttributeView::class,
65 | 'image' => ImageView::class,
66 | 'price' => PriceView::class,
67 | 'taxon' => TaxonView::class,
68 | ]], 'view_classes');
69 | }
70 |
71 | /**
72 | * @test
73 | */
74 | public function it_has_attribute_white_list()
75 | {
76 | $this->assertProcessedConfigurationEquals(
77 | ['sylius_elastic_search' => ['attribute_whitelist' => ['color']]],
78 | ['attribute_whitelist' => ['color']],
79 | 'attribute_whitelist'
80 | );
81 | }
82 |
83 | /**
84 | * {@inheritdoc}
85 | */
86 | protected function getConfiguration()
87 | {
88 | return new Configuration();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Filter/Widget/MultiDynamicAggregateOverride.php:
--------------------------------------------------------------------------------
1 | ', $this->getDocumentField());
30 | $filter = !empty($filter = $relatedSearch->getPostFilters()) ? $filter : new MatchAllQuery();
31 | $aggregation = new NestedAggregation($state->getName(), $path);
32 | $nameAggregation = new TermsAggregation('name', $this->getNameField());
33 | $valueAggregation = new TermsAggregation('value', $field);
34 | $filterAggregation = new FilterAggregation($state->getName() . '-filter');
35 | $nameAggregation->addAggregation($valueAggregation);
36 | $aggregation->addAggregation($nameAggregation);
37 | $filterAggregation->setFilter($filter);
38 |
39 | if ($this->hasOption('names_sort_type')) {
40 | $valueAggregation->addParameter('order', [$this->getOption('names_sort_type') => $this->getOption('names_sort_order')]);
41 | }
42 |
43 | if ($this->hasOption('names_size')) {
44 | $nameAggregation->addParameter('size', $this->getOption('names_size'));
45 | }
46 |
47 | if ($this->hasOption('values_sort_type')) {
48 | $valueAggregation->addParameter('order', [$this->getOption('values_sort_type') => $this->getOption('values_sort_order')]);
49 | }
50 |
51 | if ($this->getOption('values_size')) {
52 | $valueAggregation->addParameter('size', $this->getOption('values_size'));
53 | }
54 |
55 | if ($state->isActive()) {
56 | foreach ($state->getValue() as $key => $term) {
57 | $terms = $state->getValue();
58 | unset($terms[$key]);
59 |
60 | $this->addSubFilterAggregation(
61 | $filterAggregation,
62 | $aggregation,
63 | $terms,
64 | $key
65 | );
66 | }
67 | }
68 |
69 | $this->addSubFilterAggregation(
70 | $filterAggregation,
71 | $aggregation,
72 | $state->getValue() ? $state->getValue() : [],
73 | 'all-selected'
74 | );
75 |
76 | $search->addAggregation($filterAggregation);
77 |
78 | if ($this->getShowZeroChoices()) {
79 | $search->addAggregation($aggregation);
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Factory/Document/VariantDocumentFactory.php:
--------------------------------------------------------------------------------
1 | variantDocumentClass = $variantDocumentClass;
37 | $this->priceDocumentFactory = $priceDocumentFactory;
38 | $this->imageDocumentFactory = $imageDocumentFactory;
39 | $this->optionDocumentFactory = $optionDocumentFactory;
40 | }
41 |
42 | public function create(
43 | ProductVariantInterface $productVariant,
44 | ChannelInterface $channel,
45 | LocaleInterface $locale
46 | ): VariantDocument {
47 | $options = [];
48 | foreach ($productVariant->getOptionValues() as $optionValue) {
49 | $options[] = $this->optionDocumentFactory->create($optionValue, $locale);
50 | }
51 |
52 | /** @var ChannelPricingInterface $channelPricing */
53 | $channelPricing = $productVariant->getChannelPricingForChannel($channel);
54 |
55 | $price = $this->priceDocumentFactory->create(
56 | $channelPricing,
57 | $channel->getBaseCurrency()
58 | );
59 |
60 | /** @var ProductVariantTranslationInterface $productVariantTranslation */
61 | $productVariantTranslation = $productVariant->getTranslation($locale->getCode());
62 |
63 | /** @var VariantDocument $variant */
64 | $variant = new $this->variantDocumentClass();
65 | $variant->setId($productVariant->getId());
66 | $variant->setCode($productVariant->getCode());
67 |
68 | if (!$productVariantTranslation->getName()) {
69 | $variant->setName($productVariant->getProduct()->getTranslation($locale->getCode())->getName());
70 | } else {
71 | $variant->setName($productVariantTranslation->getName());
72 | }
73 |
74 | $variant->setPrice($price);
75 | $variant->setStock($productVariant->getOnHand() - $productVariant->getOnHold());
76 | $variant->setIsTracked($productVariant->isTracked());
77 | $variant->setOptions(new Collection($options));
78 | if ($productVariant->getImages()->count() > 0) {
79 | /** @var ImageDocument[] $images */
80 | $images = [];
81 | foreach ($productVariant->getImages() as $image) {
82 | $images[] = $this->imageDocumentFactory->create($image);
83 | }
84 | $variant->setImages(new Collection($images));
85 | }
86 |
87 | return $variant;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/SyliusElasticSearchExtensionTest.php:
--------------------------------------------------------------------------------
1 | load([]);
40 |
41 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.document.product.class', ProductDocument::class);
42 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.document.attribute.class', AttributeDocument::class);
43 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.document.image.class', ImageDocument::class);
44 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.document.price.class', PriceDocument::class);
45 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.document.taxon.class', TaxonDocument::class);
46 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.document.variant.class', VariantDocument::class);
47 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.document.option.class', OptionDocument::class);
48 | }
49 |
50 | /**
51 | * @test
52 | */
53 | public function it_defines_view_classes_parameters()
54 | {
55 | $this->load([]);
56 |
57 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.view.product_list.class', ProductListView::class);
58 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.view.product.class', ProductView::class);
59 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.view.product_variant.class', VariantView::class);
60 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.view.attribute.class', AttributeView::class);
61 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.view.image.class', ImageView::class);
62 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.view.price.class', PriceView::class);
63 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.view.taxon.class', TaxonView::class);
64 | }
65 |
66 | /**
67 | * @test
68 | */
69 | public function it_defines_attribute_whitelist_parameter()
70 | {
71 | $this->load([]);
72 |
73 | $this->assertContainerBuilderHasParameter('sylius_elastic_search.attribute_whitelist', []);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Controller/SearchController.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | final class SearchController
20 | {
21 | /**
22 | * @var ViewHandlerInterface
23 | */
24 | private $restViewHandler;
25 |
26 | /**
27 | * @var \Sylius\ElasticSearchPlugin\Factory\View\ProductListViewFactoryInterface
28 | */
29 | private $productListViewFactory;
30 |
31 | /**
32 | * @var FilterManagerInterface
33 | */
34 | private $filterManager;
35 |
36 | /**
37 | * @var RepositoryInterface
38 | */
39 | private $channelRepository;
40 |
41 | /**
42 | * @param ViewHandlerInterface $restViewHandler
43 | * @param \Sylius\ElasticSearchPlugin\Factory\View\ProductListViewFactoryInterface $productListViewFactory
44 | * @param FilterManagerInterface $filterManager
45 | * @param RepositoryInterface $channelRepository
46 | */
47 | public function __construct(
48 | ViewHandlerInterface $restViewHandler,
49 | ProductListViewFactoryInterface $productListViewFactory,
50 | FilterManagerInterface $filterManager,
51 | RepositoryInterface $channelRepository
52 | ) {
53 | $this->restViewHandler = $restViewHandler;
54 | $this->productListViewFactory = $productListViewFactory;
55 | $this->filterManager = $filterManager;
56 | $this->channelRepository = $channelRepository;
57 | }
58 |
59 | /**
60 | * @param Request $request
61 | *
62 | * @return Response
63 | */
64 | public function __invoke(Request $request)
65 | {
66 | if (!$request->query->has('channel')) {
67 | throw new NotFoundHttpException('Cannot find products without channel provided!');
68 | }
69 |
70 | if (!$request->query->has('locale')) {
71 | $channelCode = $request->query->get('channel');
72 | $channel = $this->channelRepository->findOneBy(['code' => $channelCode]);
73 |
74 | if (null === $channel) {
75 | throw new NotFoundHttpException(sprintf('Channel with code "%s" cannot be found!', $channelCode));
76 | }
77 |
78 | $request->query->set('locale', $channel->getDefaultLocale()->getCode());
79 | }
80 |
81 | if (!$request->query->has('stock')) {
82 | $request->query->set('stock', '1');
83 | }
84 |
85 | if (!$request->query->has('sort')) {
86 | if (null !== $request->get('taxonCode')) {
87 | $request->query->set('sort', ['taxonPositionByCode' => [$request->get('taxonCode') => 'asc']]);
88 | }
89 |
90 | if (null !== $request->get('taxonSlug')) {
91 | $request->query->set('sort', ['taxonPositionBySlug' => [$request->get('taxonSlug') => 'asc']]);
92 | }
93 | }
94 |
95 | $request->query->set('enabled', true);
96 |
97 | $response = $this->filterManager->handleRequest($request);
98 |
99 | return $this->restViewHandler->handle(
100 | View::create(
101 | $this->productListViewFactory->createFromSearchResponse($response),
102 | Response::HTTP_OK
103 | )
104 | );
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/Filter/Widget/InStock.php:
--------------------------------------------------------------------------------
1 | get($this->getRequestField());
33 |
34 | if (isset($value) && $value !== '') {
35 | $value = is_array($value) ? array_values($value) : $value;
36 | $state->setActive(true);
37 | $state->setValue($value);
38 | }
39 |
40 | if (!$state->isActive()) {
41 | return $state;
42 | }
43 |
44 | $values = explode(';', $state->getValue(), 2);
45 |
46 | $argumentCount = count($values);
47 | if ($argumentCount === 0) {
48 | $state->setActive(false);
49 |
50 | return $state;
51 | }
52 |
53 | $gt = $this->isInclusive() ? 'gte' : 'gt';
54 | $lt = $this->isInclusive() ? 'lte' : 'lt';
55 |
56 | $normalized = [];
57 |
58 | if ($argumentCount === 1) {
59 | $normalized[$gt] = $values[0];
60 | }
61 | if ($argumentCount === 2) {
62 | $normalized[$gt] = $values[0];
63 | $normalized[$lt] = $values[1];
64 | }
65 |
66 | $state->setValue($normalized);
67 |
68 | return $state;
69 | }
70 |
71 | /**
72 | * {@inheritdoc}
73 | */
74 | public function modifySearch(Search $search, FilterState $state = null, SearchRequest $request = null)
75 | {
76 | if ($state && $state->isActive()) {
77 | $boolQuery = new BoolQuery();
78 | $boolQuery->add(new RangeQuery($this->getDocumentField(), $state->getValue()), BoolQuery::SHOULD);
79 | $boolQuery->add(new TermQuery('variants.is_tracked', false), BoolQuery::SHOULD);
80 | $nestedQuery = new NestedQuery('variants', $boolQuery);
81 | $search->addPostFilter($nestedQuery);
82 | }
83 | }
84 |
85 | /**
86 | * {@inheritdoc}
87 | */
88 | public function preProcessSearch(Search $search, Search $relatedSearch, FilterState $state = null)
89 | {
90 | $stateAgg = new StatsAggregation($state->getName());
91 | $stateAgg->setField($this->getDocumentField());
92 | $filters = $relatedSearch->getPostFilters();
93 | if (!empty($filters)) {
94 | $search->addPostFilter($filters);
95 | }
96 | $search->addAggregation($stateAgg);
97 | }
98 |
99 | /**
100 | * {@inheritdoc}
101 | */
102 | public function getViewData(DocumentIterator $result, ViewData $data)
103 | {
104 | $name = $data->getState()->getName();
105 | /** @var $data ViewData\RangeAwareViewData */
106 | $data->setMinBounds($result->getAggregation($name)['min']);
107 | $data->setMaxBounds($result->getAggregation($name)['max']);
108 |
109 | return $data;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/Projection/ProductProjector.php:
--------------------------------------------------------------------------------
1 | elasticsearchManager = $elasticsearchManager;
45 | $this->productDocumentRepository = $elasticsearchManager->getRepository(ProductDocument::class);
46 | $this->productDocumentFactory = $productDocumentFactory;
47 | }
48 |
49 | /**
50 | * @param ProductCreated $event
51 | */
52 | public function handleProductCreated(ProductCreated $event): void
53 | {
54 | $this->scheduleCreatingNewProductDocuments($event->product());
55 | $this->scheduleRemovingOldProductDocuments($event->product());
56 |
57 | $this->elasticsearchManager->commit();
58 | }
59 |
60 | /**
61 | * We create a new product documents with updated data and remove old once
62 | *
63 | * @param ProductUpdated $event
64 | */
65 | public function handleProductUpdated(ProductUpdated $event): void
66 | {
67 | $product = $event->product();
68 |
69 | $this->scheduleCreatingNewProductDocuments($product);
70 | $this->scheduleRemovingOldProductDocuments($product);
71 |
72 | $this->elasticsearchManager->commit();
73 | }
74 |
75 | /**
76 | * We remove deleted product
77 | *
78 | * @param ProductDeleted $event
79 | */
80 | public function handleProductDeleted(ProductDeleted $event): void
81 | {
82 | $product = $event->product();
83 |
84 | $this->scheduleRemovingOldProductDocuments($product);
85 |
86 | $this->elasticsearchManager->commit();
87 | }
88 |
89 | private function scheduleCreatingNewProductDocuments(ProductInterface $product): void
90 | {
91 | /** @var ChannelInterface[] $channels */
92 | $channels = $product->getChannels();
93 | foreach ($channels as $channel) {
94 | /** @var LocaleInterface[] $locales */
95 | $locales = $channel->getLocales();
96 | foreach ($locales as $locale) {
97 | $this->elasticsearchManager->persist(
98 | $this->productDocumentFactory->create(
99 | $product,
100 | $locale,
101 | $channel
102 | )
103 | );
104 | }
105 | }
106 | }
107 |
108 | private function scheduleRemovingOldProductDocuments(ProductInterface $product): void
109 | {
110 | /** @var DocumentIterator|ProductDocument[] $currentProductDocuments */
111 | $currentProductDocuments = $this->productDocumentRepository->findBy(['code' => $product->getCode()]);
112 |
113 | foreach ($currentProductDocuments as $sameCodeProductDocument) {
114 | $this->elasticsearchManager->remove($sameCodeProductDocument);
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/tests/Responses/Expected/WEB_GB/en_GB/product_list_page_filtered_by_price_range.json:
--------------------------------------------------------------------------------
1 | {
2 | "page": 1,
3 | "limit": 9,
4 | "total": 2,
5 | "pages": 1,
6 | "items": [
7 | {
8 | "id": 1,
9 | "code": "LOGAN_MUG_CODE",
10 | "name": "Logan Mug",
11 | "slug": "@string@",
12 | "taxons": "@array@",
13 | "variants": [
14 | {
15 | "id": 2,
16 | "code": "LOGAN_MUG_SMALL_CODE",
17 | "name": "Gnome Mug",
18 | "price": {
19 | "current": 1000,
20 | "currency": "GBP",
21 | "original": 0
22 | },
23 | "stock": 0,
24 | "isTracked": true,
25 | "images": []
26 | },
27 | {
28 | "id": 1,
29 | "code": "LOGAN_MUG_BIG_CODE",
30 | "name": "Logan Mug",
31 | "price": {
32 | "current": 1000,
33 | "currency": "GBP",
34 | "original": 0
35 | },
36 | "stock": 0,
37 | "isTracked": false,
38 | "images": []
39 | }
40 | ],
41 | "attributes": "@array@",
42 | "images": "@array@",
43 | "price": {
44 | "current": 1000,
45 | "currency": "GBP",
46 | "original": 0
47 | },
48 | "channelCode": "WEB_GB",
49 | "localeCode": "en_GB"
50 | },
51 | {
52 | "id": 2,
53 | "code": "LOGAN_HAT_CODE",
54 | "name": "Logan Hat",
55 | "slug": "@string@",
56 | "taxons": "@array@",
57 | "variants": [
58 | {
59 | "id": 3,
60 | "code": "LOGAN_HAT_CODE",
61 | "name": "Logan Hat",
62 | "price": {
63 | "current": 1500,
64 | "currency": "GBP",
65 | "original": 0
66 | },
67 | "stock": 2,
68 | "isTracked": true,
69 | "images": []
70 | }
71 | ],
72 | "attributes": "@array@",
73 | "images": "@array@",
74 | "price": {
75 | "current": 1500,
76 | "currency": "GBP",
77 | "original": 0
78 | },
79 | "channelCode": "WEB_GB",
80 | "localeCode": "en_GB"
81 | }
82 | ],
83 | "filters": {
84 | "channel": "@array@",
85 | "enabled": "@array@",
86 | "taxonCode": "@array@",
87 | "taxonSlug": "@array@",
88 | "priceRange": {
89 | "state": {
90 | "active": true,
91 | "value": {
92 | "gte": "1000",
93 | "lte": "1500"
94 | },
95 | "urlParameters": {
96 | "price": "1000;1500"
97 | },
98 | "name": "priceRange",
99 | "options": []
100 | },
101 | "tags": [],
102 | "urlParameters": {
103 | "channel": "WEB_GB",
104 | "enabled": true,
105 | "price": "1000;1500",
106 | "locale": "en_GB"
107 | },
108 | "resetUrlParameters": {
109 | "channel": "WEB_GB",
110 | "enabled": true,
111 | "locale": "en_GB"
112 | },
113 | "name": "priceRange",
114 | "minBounds": 1000,
115 | "maxBounds": 2000
116 | },
117 | "locale": "@array@",
118 | "inStock": "@array@",
119 | "paginator": "@array@",
120 | "search": "@array@",
121 | "attributes": "@array@",
122 | "attributesByCode": "@array@",
123 | "options": "@array@",
124 | "sort": "@array@"
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/Command/ResetProductIndexCommand.php:
--------------------------------------------------------------------------------
1 | productRepository = $productRepository;
45 | $this->elasticsearchManager = $manager;
46 | $this->productDocumentFactory = $productDocumentFactory;
47 |
48 | parent::__construct('sylius:elastic-search:reset-product-index');
49 | }
50 |
51 | /**
52 | * {@inheritdoc}
53 | */
54 | protected function configure(): void
55 | {
56 | $this
57 | ->addOption('force', 'f', null, 'To confirm running this command')
58 | ;
59 | }
60 |
61 | /**
62 | * {@inheritdoc}
63 | */
64 | protected function execute(InputInterface $input, OutputInterface $output)
65 | {
66 | $lockHandler = new LockHandler('sylius-elastic-index-update');
67 | if ($lockHandler->lock()) {
68 | if (!$input->getOption('force')) {
69 | $output->writeln('WARNING! This command will drop the existing index and rebuild it from scratch. To proceed, run with "--force" option.');
70 |
71 | return;
72 | }
73 |
74 | $output->writeln(sprintf('Dropping and creating "%s" ElasticSearch index', $this->elasticsearchManager->getIndexName()));
75 | $this->elasticsearchManager->dropAndCreateIndex();
76 |
77 | $productDocumentsCreated = 0;
78 |
79 | /** @var ProductInterface[] $products */
80 | $products = $this->productRepository->findAll();
81 |
82 | $output->writeln(sprintf('Loading %d products into ElasticSearch', count($products)));
83 |
84 | foreach ($products as $product) {
85 | $channels = $product->getChannels();
86 |
87 | /** @var ChannelInterface $channel */
88 | foreach ($channels as $channel) {
89 | $locales = $channel->getLocales();
90 | foreach ($locales as $locale) {
91 | $productDocument = $this->productDocumentFactory->create(
92 | $product,
93 | $locale,
94 | $channel
95 | );
96 |
97 | $this->elasticsearchManager->persist($productDocument);
98 |
99 | ++$productDocumentsCreated;
100 | if (($productDocumentsCreated % 100) === 0) {
101 | $this->elasticsearchManager->commit();
102 | }
103 | }
104 | }
105 | }
106 |
107 | $this->elasticsearchManager->commit();
108 | $lockHandler->release();
109 | $output->writeln('Product index was rebuilt!');
110 | } else {
111 | $output->writeln(sprintf('Command is already running'));
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/Resources/config/app/config.yml:
--------------------------------------------------------------------------------
1 | ongr_elasticsearch:
2 | analysis:
3 | analyzer:
4 | keywordAnalyzer:
5 | type: custom
6 | tokenizer: keyword
7 | filter: [lowercase]
8 | incrementalAnalyzer:
9 | type: custom
10 | tokenizer: standard
11 | filter:
12 | - lowercase
13 | - asciifolding
14 |
15 | ongr_filter_manager:
16 | managers:
17 | search_list:
18 | filters:
19 | - channel
20 | - enabled
21 | - taxonCode
22 | - taxonSlug
23 | - priceRange
24 | - inStock
25 | - locale
26 | - paginator
27 | - search
28 | - attributes
29 | - attributesByCode
30 | - options
31 | - sort
32 | repository: es.manager.default.product
33 | filters:
34 | channel:
35 | type: choice
36 | request_field: channel
37 | document_field: channel_code
38 | enabled:
39 | type: choice
40 | request_field: enabled
41 | document_field: enabled
42 | taxonCode:
43 | type: sylius_elastic_search.choice_nested
44 | request_field: taxonCode
45 | document_field: taxons>taxons.code
46 | taxonSlug:
47 | type: sylius_elastic_search.choice_nested
48 | request_field: taxonSlug
49 | document_field: taxons>taxons.slug
50 | priceRange:
51 | type: range
52 | request_field: price
53 | document_field: price.amount
54 | options:
55 | inclusive: true
56 | inStock:
57 | type: sylius_elastic_search.in_stock
58 | request_field: stock
59 | document_field: variants.stock
60 | options:
61 | inclusive: true
62 | locale:
63 | type: choice
64 | request_field: locale
65 | document_field: locale_code
66 | paginator:
67 | type: sylius_elastic_search.custom_pager
68 | document_field: ~
69 | request_field: page
70 | options:
71 | limit: 9
72 | max_pages: 20
73 | attributes:
74 | type: sylius_elastic_search.multi_dynamic_aggregate
75 | request_field: attributes
76 | document_field: attributes>attributes.value.raw
77 | options:
78 | name_field: attributes.name.raw
79 | names_sort_type: _term
80 | names_sort_order: asc
81 | names_size: 100
82 | values_sort_type: _count
83 | values_sort_order: desc
84 | values_size: 100
85 | attributesByCode:
86 | type: sylius_elastic_search.multi_dynamic_aggregate_without_view
87 | request_field: attributesByCode
88 | document_field: attributes>attributes.value.raw
89 | options:
90 | name_field: attributes.code
91 | options:
92 | type: sylius_elastic_search.multi_dynamic_aggregate_options
93 | request_field: options
94 | document_field: variants>variants.options>variants.options.value.raw
95 | options:
96 | name_field: variants.options.name.raw
97 | sort_type: _term
98 | sort_order: asc
99 | size: 100
100 | search:
101 | type: match
102 | request_field: search
103 | document_field: name^3,description,attributes>attributes.name.standard,attributes>attributes.value.standard
104 | options:
105 | operator: and
106 | sort:
107 | type: sylius_elastic_search.sort
108 | request_field: sort
109 | document_field: ~
110 | options:
111 | aliases:
112 | name: name
113 | slug: slug
114 | code: code
115 | price: price.amount
116 | date: created_at
117 |
--------------------------------------------------------------------------------
/src/Resources/config/services.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | %sylius_elastic_search.view.product_list.class%
28 | %sylius_elastic_search.view.product.class%
29 | %sylius_elastic_search.view.product_variant.class%
30 | %sylius_elastic_search.view.attribute.class%
31 | %sylius_elastic_search.view.image.class%
32 | %sylius_elastic_search.view.price.class%
33 | %sylius_elastic_search.view.taxon.class%
34 |
35 |
36 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/Filter/Widget/Sort.php:
--------------------------------------------------------------------------------
1 | get($this->getRequestField());
28 |
29 | if (null !== $value && '' !== $value) {
30 | $state->setActive(true);
31 | $state->setValue($value);
32 | $state->setUrlParameters([$this->getRequestField() => $value]);
33 | }
34 |
35 | return $state;
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | public function modifySearch(Search $search, ?FilterState $state = null, ?SearchRequest $request = null)
42 | {
43 | if ($state && $state->isActive()) {
44 | $stateValue = $state->getValue();
45 |
46 | if (!is_array($stateValue)) {
47 | return;
48 | }
49 |
50 | $aliases = $this->getOption('aliases') ?? [];
51 |
52 | foreach ($stateValue as $field => $data) {
53 | if ('attributes' === $field) {
54 | $this->addAttributeFieldToSort($search, $data);
55 |
56 | continue;
57 | }
58 |
59 | if ('taxonPositionByCode' === $field) {
60 | $this->addPositionFieldToSort($search, 'code', $data);
61 |
62 | continue;
63 | }
64 |
65 | if ('taxonPositionBySlug' === $field) {
66 | $this->addPositionFieldToSort($search, 'slug', $data);
67 |
68 | continue;
69 | }
70 |
71 | if (array_key_exists($field, $aliases)) {
72 | $this->addRegularFieldToSort($search, $aliases[$field], $data);
73 |
74 | continue;
75 | }
76 | }
77 | }
78 | }
79 |
80 | /**
81 | * {@inheritdoc}
82 | */
83 | public function preProcessSearch(Search $search, Search $relatedSearch, ?FilterState $state = null)
84 | {
85 | // Nothing to do here.
86 | }
87 |
88 | /**
89 | * {@inheritdoc}
90 | */
91 | public function createViewData(): EmptyViewData
92 | {
93 | return new EmptyViewData();
94 | }
95 |
96 | /**
97 | * {@inheritdoc}
98 | */
99 | public function getViewData(DocumentIterator $result, ViewData $data): ViewData
100 | {
101 | return $data;
102 | }
103 |
104 | private function addAttributeFieldToSort(Search $search, array $settings): void
105 | {
106 | foreach ($settings as $attributeCode => $sortingOrder) {
107 | $fieldSort = new FieldSort('attributes.value.raw', $sortingOrder, ['nested_path' => 'attributes', 'mode' => 'max']);
108 | $fieldSort->setNestedFilter(new TermQuery('attributes.code', $attributeCode));
109 |
110 | $search->addSort($fieldSort);
111 | }
112 | }
113 |
114 | private function addPositionFieldToSort(Search $search, string $identifier, array $settings): void
115 | {
116 | foreach ($settings as $taxonIdentifier => $sortingOrder) {
117 | $fieldSort = new FieldSort('taxons.position', $sortingOrder, ['nested_path' => 'taxons']);
118 | $fieldSort->setNestedFilter(new TermQuery(sprintf('taxons.%s', $identifier), $taxonIdentifier));
119 |
120 | $search->addSort($fieldSort);
121 | }
122 | }
123 |
124 | private function addRegularFieldToSort(Search $search, string $field, string $order, array $options = []): void
125 | {
126 | $search->addSort(new FieldSort($field, $order, $options));
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Document/VariantDocument.php:
--------------------------------------------------------------------------------
1 | images = new Collection();
74 | $this->options = new Collection();
75 | }
76 |
77 | /**
78 | * @return mixed
79 | */
80 | public function getId()
81 | {
82 | return $this->id;
83 | }
84 |
85 | /**
86 | * @param mixed $id
87 | */
88 | public function setId($id): void
89 | {
90 | $this->id = $id;
91 | }
92 |
93 | /**
94 | * @return Collection
95 | */
96 | public function getImages(): Collection
97 | {
98 | return $this->images;
99 | }
100 |
101 | /**
102 | * @param Collection $images
103 | */
104 | public function setImages(Collection $images): void
105 | {
106 | $this->images = $images;
107 | }
108 |
109 | /**
110 | * @return PriceDocument
111 | */
112 | public function getPrice(): PriceDocument
113 | {
114 | return $this->price;
115 | }
116 |
117 | /**
118 | * @param PriceDocument $price
119 | */
120 | public function setPrice(PriceDocument $price): void
121 | {
122 | $this->price = $price;
123 | }
124 |
125 | /**
126 | * @return string
127 | */
128 | public function getCode(): string
129 | {
130 | return $this->code;
131 | }
132 |
133 | /**
134 | * @param string $code
135 | */
136 | public function setCode(string $code): void
137 | {
138 | $this->code = $code;
139 | }
140 |
141 | /**
142 | * @return string
143 | */
144 | public function getName(): string
145 | {
146 | return $this->name;
147 | }
148 |
149 | /**
150 | * @param string $name
151 | */
152 | public function setName(string $name): void
153 | {
154 | $this->name = $name;
155 | }
156 |
157 | /**
158 | * @return int
159 | */
160 | public function getStock(): int
161 | {
162 | return $this->stock;
163 | }
164 |
165 | /**
166 | * @param int $stock
167 | */
168 | public function setStock(int $stock): void
169 | {
170 | $this->stock = $stock;
171 | }
172 |
173 | public function getIsTracked(): bool
174 | {
175 | return $this->isTracked;
176 | }
177 |
178 | /**
179 | * @return bool
180 | */
181 | public function isTracked(): bool
182 | {
183 | return $this->getIsTracked();
184 | }
185 |
186 | /**
187 | * @param bool $isTracked
188 | */
189 | public function setIsTracked(bool $isTracked): void
190 | {
191 | $this->isTracked = $isTracked;
192 | }
193 |
194 | /**
195 | * @return Collection
196 | */
197 | public function getOptions(): Collection
198 | {
199 | return $this->options;
200 | }
201 |
202 | /**
203 | * @param Collection $options
204 | */
205 | public function setOptions(Collection $options): void
206 | {
207 | $this->options = $options;
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/spec/Projection/ProductProjectorSpec.php:
--------------------------------------------------------------------------------
1 | getRepository(ProductDocument::class)->willReturn($productDocumentRepository);
28 |
29 | $this->beConstructedWith($elasticsearchManager, $productDocumentFactory);
30 | }
31 |
32 | function it_is_initializable()
33 | {
34 | $this->shouldHaveType(ProductProjector::class);
35 | }
36 |
37 | function it_saves_product_documents_and_removes_the_current_ones(
38 | Manager $elasticsearchManager,
39 | ProductDocumentFactoryInterface $productDocumentFactory,
40 | Repository $productDocumentRepository,
41 | ProductInterface $product,
42 | LocaleInterface $locale,
43 | ChannelInterface $channel
44 | ) {
45 | $product->getCode()->willReturn('FOO');
46 | $product->getChannels()->willReturn(new ArrayCollection([$channel->getWrappedObject()]));
47 | $channel->getLocales()->willReturn(new ArrayCollection([$locale->getWrappedObject()]));
48 |
49 | $existingProductDocument = new ProductDocument();
50 | $productDocumentRepository->findBy(['code' => 'FOO'])->willReturn(new \ArrayIterator([$existingProductDocument]));
51 |
52 | $newProductDocument = new ProductDocument();
53 | $productDocumentFactory->create($product, $locale, $channel)->willReturn($newProductDocument);
54 |
55 | $elasticsearchManager->persist($newProductDocument)->shouldBeCalled();
56 | $elasticsearchManager->remove($existingProductDocument)->shouldBeCalled();
57 | $elasticsearchManager->commit()->shouldBeCalled();
58 |
59 | $this->handleProductCreated(ProductCreated::occur($product->getWrappedObject()));
60 | }
61 |
62 | function it_does_not_save_product_document_if_product_does_not_have_channel_defined(
63 | Manager $elasticsearchManager,
64 | ProductDocumentFactoryInterface $productDocumentFactory,
65 | Repository $productDocumentRepository,
66 | ProductInterface $product
67 | ) {
68 | $product->getCode()->willReturn('FOO');
69 | $product->getChannels()->willReturn(new ArrayCollection([]));
70 |
71 | $productDocumentRepository->findBy(['code' => 'FOO'])->willReturn(new \ArrayIterator([]));
72 |
73 | $productDocumentFactory->create(Argument::any(), Argument::any(), Argument::any())->shouldNotBeCalled();
74 |
75 | $elasticsearchManager->persist(Argument::any())->shouldNotBeCalled();
76 | $elasticsearchManager->commit()->shouldBeCalled();
77 |
78 | $this->handleProductCreated(ProductCreated::occur($product->getWrappedObject()));
79 | }
80 |
81 | function it_does_not_save_product_document_if_channel_has_not_locales_defined(
82 | Manager $elasticsearchManager,
83 | ProductDocumentFactoryInterface $productDocumentFactory,
84 | Repository $productDocumentRepository,
85 | ProductInterface $product,
86 | ChannelInterface $channel
87 | ) {
88 | $product->getCode()->willReturn('FOO');
89 | $channel->getLocales()->willReturn(new ArrayCollection([]));
90 | $product->getChannels()->willReturn(new ArrayCollection([$channel->getWrappedObject()]));
91 |
92 | $productDocumentRepository->findBy(['code' => 'FOO'])->willReturn(new \ArrayIterator([]));
93 |
94 | $productDocumentFactory->create(Argument::any(), Argument::any(), Argument::any())->shouldNotBeCalled();
95 |
96 | $elasticsearchManager->persist(Argument::any())->shouldNotBeCalled();
97 | $elasticsearchManager->commit()->shouldBeCalled();
98 |
99 | $this->handleProductCreated(ProductCreated::occur($product->getWrappedObject()));
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 |
36 | */
37 | final class Configuration implements ConfigurationInterface
38 | {
39 | /**
40 | * {@inheritdoc}
41 | */
42 | public function getConfigTreeBuilder()
43 | {
44 | $treeBuilder = new TreeBuilder();
45 | $rootNode = $treeBuilder->root('sylius_elastic_search');
46 |
47 | $this->buildAttributeWhitelistNode($rootNode);
48 | $this->buildDocumentClassesNode($rootNode);
49 | $this->buildViewClassesNode($rootNode);
50 |
51 | return $treeBuilder;
52 | }
53 |
54 | /**
55 | * @param ArrayNodeDefinition $rootNode
56 | */
57 | private function buildDocumentClassesNode(ArrayNodeDefinition $rootNode)
58 | {
59 | $rootNode
60 | ->addDefaultsIfNotSet()
61 | ->children()
62 | ->arrayNode('document_classes')
63 | ->addDefaultsIfNotSet()
64 | ->children()
65 | ->scalarNode('product')->defaultValue(ProductDocument::class)->end()
66 | ->scalarNode('attribute')->defaultValue(AttributeDocument::class)->end()
67 | ->scalarNode('image')->defaultValue(ImageDocument::class)->end()
68 | ->scalarNode('price')->defaultValue(PriceDocument::class)->end()
69 | ->scalarNode('taxon')->defaultValue(TaxonDocument::class)->end()
70 | ->scalarNode('variant')->defaultValue(VariantDocument::class)->end()
71 | ->scalarNode('option')->defaultValue(OptionDocument::class)->end()
72 | ->end()
73 | ->end()
74 | ->end()
75 | ;
76 | }
77 |
78 | /**
79 | * @param ArrayNodeDefinition $rootNode
80 | */
81 | private function buildViewClassesNode(ArrayNodeDefinition $rootNode)
82 | {
83 | $rootNode
84 | ->addDefaultsIfNotSet()
85 | ->children()
86 | ->arrayNode('view_classes')
87 | ->addDefaultsIfNotSet()
88 | ->children()
89 | ->scalarNode('product_list')->defaultValue(ProductListView::class)->end()
90 | ->scalarNode('product')->defaultValue(ProductView::class)->end()
91 | ->scalarNode('product_variant')->defaultValue(VariantView::class)->end()
92 | ->scalarNode('attribute')->defaultValue(AttributeView::class)->end()
93 | ->scalarNode('image')->defaultValue(ImageView::class)->end()
94 | ->scalarNode('price')->defaultValue(PriceView::class)->end()
95 | ->scalarNode('taxon')->defaultValue(TaxonView::class)->end()
96 | ->end()
97 | ->end()
98 | ->end()
99 | ;
100 | }
101 |
102 | /**
103 | * @param ArrayNodeDefinition $rootNode
104 | */
105 | private function buildAttributeWhitelistNode(ArrayNodeDefinition $rootNode)
106 | {
107 | $rootNode
108 | ->addDefaultsIfNotSet()
109 | ->children()
110 | ->arrayNode('attribute_whitelist')
111 | ->prototype('scalar')->end()
112 | ->end()
113 | ->end()
114 | ;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/Command/UpdateProductIndexCommand.php:
--------------------------------------------------------------------------------
1 | productRepository = $productRepository;
55 | $this->elasticsearchManager = $elasticsearchManager;
56 | $this->productDocumentRepository = $elasticsearchManager->getRepository(ProductDocument::class);
57 | $this->productDocumentFactory = $productDocumentFactory;
58 |
59 | parent::__construct('sylius:elastic-search:update-product-index');
60 | }
61 |
62 | /**
63 | * {@inheritdoc}
64 | */
65 | protected function configure(): void
66 | {
67 | $this->setDescription('Update products in Elasticsearch index.');
68 | }
69 |
70 | /**
71 | * {@inheritdoc}
72 | */
73 | protected function execute(InputInterface $input, OutputInterface $output): void
74 | {
75 | $lockHandler = new LockHandler('sylius-elastic-index-update');
76 | if ($lockHandler->lock()) {
77 | $processedProductsCodes = [];
78 | $productDocumentsWaitingForCommit = 0;
79 |
80 | $search = $this->productDocumentRepository->createSearch();
81 | $search->setScroll('10m');
82 | $search->addSort(new FieldSort('synchronised_at', 'asc'));
83 |
84 | /** @var DocumentIterator|ProductDocument[] $productDocuments */
85 | $productDocuments = $this->productDocumentRepository->findDocuments($search);
86 |
87 | foreach ($productDocuments as $productDocument) {
88 | $productCode = $productDocument->getCode();
89 |
90 | if (isset($processedProductsCodes[$productCode])) {
91 | continue;
92 | }
93 |
94 | $output->writeln(sprintf('Updating product with code "%s"', $productCode));
95 |
96 | $this->scheduleCreatingNewProductDocuments($productCode);
97 | $this->scheduleRemovingOldProductDocuments($productCode);
98 |
99 | ++$productDocumentsWaitingForCommit;
100 | if (($productDocumentsWaitingForCommit % 100) === 0) {
101 | $this->elasticsearchManager->commit();
102 | $productDocumentsWaitingForCommit = 0;
103 | }
104 |
105 | $processedProductsCodes[$productCode] = 1;
106 | }
107 |
108 | if ($productDocumentsWaitingForCommit > 0) {
109 | $this->elasticsearchManager->commit();
110 | }
111 | $lockHandler->release();
112 | $output->writeln('Updates done');
113 | } else {
114 | $output->writeln(sprintf('Command is already running'));
115 | }
116 | }
117 |
118 | private function scheduleCreatingNewProductDocuments(string $productCode): void
119 | {
120 | /** @var ProductInterface|null $product */
121 | $product = $this->productRepository->findOneBy(['code' => $productCode]);
122 |
123 | if (null === $product) {
124 | return;
125 | }
126 |
127 | /** @var ChannelInterface[] $channels */
128 | $channels = $product->getChannels();
129 |
130 | foreach ($channels as $channel) {
131 | /** @var LocaleInterface[] $locales */
132 | $locales = $channel->getLocales();
133 |
134 | foreach ($locales as $locale) {
135 | $this->elasticsearchManager->persist($this->productDocumentFactory->create(
136 | $product,
137 | $locale,
138 | $channel
139 | ));
140 | }
141 | }
142 | }
143 |
144 | private function scheduleRemovingOldProductDocuments(string $productCode): void
145 | {
146 | /** @var DocumentIterator|ProductDocument[] $currentProductDocuments */
147 | $currentProductDocuments = $this->productDocumentRepository->findBy(['code' => $productCode]);
148 |
149 | foreach ($currentProductDocuments as $sameCodeProductDocument) {
150 | $this->elasticsearchManager->remove($sameCodeProductDocument);
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/EventListener/ProductPublisher.php:
--------------------------------------------------------------------------------
1 | eventBus = $eventBus;
50 | }
51 |
52 | /**
53 | * @param OnFlushEventArgs $event
54 | */
55 | public function onFlush(OnFlushEventArgs $event): void
56 | {
57 | $scheduledInsertions = $event->getEntityManager()->getUnitOfWork()->getScheduledEntityInsertions();
58 |
59 | foreach ($scheduledInsertions as $entity) {
60 | if ($entity instanceof ProductInterface && !isset($this->scheduledInsertions[$entity->getCode()])) {
61 | $this->scheduledInsertions[$entity->getCode()] = $entity;
62 |
63 | continue;
64 | }
65 |
66 | $entity = $this->getProductFromEntity($entity);
67 | if ($entity instanceof ProductInterface && !isset($this->scheduledUpdates[$entity->getCode()])) {
68 | $this->scheduledUpdates[$entity->getCode()] = $entity;
69 | }
70 | }
71 |
72 | $scheduledUpdates = $event->getEntityManager()->getUnitOfWork()->getScheduledEntityUpdates();
73 | foreach ($scheduledUpdates as $entity) {
74 | $entity = $this->getProductFromEntity($entity);
75 | if ($entity instanceof ProductInterface && !isset($this->scheduledUpdates[$entity->getCode()])) {
76 | $this->scheduledUpdates[$entity->getCode()] = $entity;
77 | }
78 | }
79 |
80 | $scheduledDeletions = $event->getEntityManager()->getUnitOfWork()->getScheduledEntityDeletions();
81 | foreach ($scheduledDeletions as $entity) {
82 | if ($entity instanceof ProductInterface && !isset($this->scheduledDeletions[$entity->getCode()])) {
83 | $this->scheduledDeletions[$entity->getCode()] = $entity;
84 |
85 | continue;
86 | }
87 |
88 | $entity = $this->getProductFromEntity($entity);
89 | if ($entity instanceof ProductInterface && !isset($this->scheduledUpdates[$entity->getCode()])) {
90 | $this->scheduledUpdates[$entity->getCode()] = $entity;
91 | }
92 | }
93 | }
94 |
95 | /**
96 | * @param PostFlushEventArgs $event
97 | */
98 | public function postFlush(PostFlushEventArgs $event): void
99 | {
100 | foreach ($this->scheduledInsertions as $product) {
101 | $this->eventBus->handle(ProductCreated::occur($product));
102 | }
103 |
104 | $scheduledUpdates = array_diff_key(
105 | $this->scheduledUpdates,
106 | $this->scheduledInsertions,
107 | $this->scheduledDeletions
108 | );
109 | foreach ($scheduledUpdates as $product) {
110 | $this->eventBus->handle(ProductUpdated::occur($product));
111 | }
112 |
113 | foreach ($this->scheduledDeletions as $product) {
114 | $this->eventBus->handle(ProductDeleted::occur($product));
115 | }
116 |
117 | $this->scheduledInsertions = [];
118 | $this->scheduledUpdates = [];
119 | $this->scheduledDeletions = [];
120 | }
121 |
122 | /**
123 | * @param object $entity
124 | *
125 | * @return ProductInterface|null
126 | */
127 | private function getProductFromEntity($entity): ?ProductInterface
128 | {
129 | if ($entity instanceof ProductInterface) {
130 | return $entity;
131 | }
132 |
133 | if ($entity instanceof ProductTranslationInterface) {
134 | return $this->getProductFromEntity($entity->getTranslatable());
135 | }
136 |
137 | if ($entity instanceof ProductVariantInterface) {
138 | return $entity->getProduct();
139 | }
140 |
141 | if ($entity instanceof ProductVariantTranslationInterface) {
142 | return $this->getProductFromEntity($entity->getTranslatable());
143 | }
144 |
145 | if ($entity instanceof ChannelPricingInterface) {
146 | return $this->getProductFromEntity($entity->getProductVariant());
147 | }
148 |
149 | if ($entity instanceof ProductTaxonInterface) {
150 | return $entity->getProduct();
151 | }
152 |
153 | if ($entity instanceof ProductAttributeValueInterface) {
154 | return $entity->getProduct();
155 | }
156 |
157 | if ($entity instanceof ProductImageInterface) {
158 | return $this->getProductFromEntity($entity->getOwner());
159 | }
160 |
161 | return null;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/tests/Responses/Expected/WEB_GB/en_GB/product_list_page_filtered_by_mug_material_wood_attribute.json:
--------------------------------------------------------------------------------
1 | {
2 | "page": 1,
3 | "limit": 9,
4 | "total": 1,
5 | "pages": 1,
6 | "items": [
7 | {
8 | "id": 1,
9 | "code": "LOGAN_MUG_CODE",
10 | "name": "Logan Mug",
11 | "slug": "@string@",
12 | "taxons": "@array@",
13 | "variants": "@array@",
14 | "attributes": "@array@",
15 | "images": "@array@",
16 | "price": "@array@",
17 | "channelCode": "WEB_GB",
18 | "localeCode": "en_GB"
19 | }
20 | ],
21 | "filters": {
22 | "channel": "@array@",
23 | "enabled": "@array@",
24 | "taxonCode": "@array@",
25 | "taxonSlug": "@array@",
26 | "priceRange": "@array@",
27 | "locale": "@array@",
28 | "inStock": "@array@",
29 | "paginator": "@array@",
30 | "search": "@array@",
31 | "attributes": {
32 | "state": {
33 | "active": true,
34 | "value": {
35 | "Mug material": [
36 | "Wood"
37 | ]
38 | },
39 | "urlParameters": {
40 | "attributes": {
41 | "Mug material": [
42 | "Wood"
43 | ]
44 | }
45 | },
46 | "name": "attributes",
47 | "options": []
48 | },
49 | "tags": [],
50 | "urlParameters": {
51 | "channel": "WEB_GB",
52 | "enabled": true,
53 | "locale": "en_GB",
54 | "attributes": {
55 | "Mug material": [
56 | "Wood"
57 | ]
58 | }
59 | },
60 | "resetUrlParameters": {
61 | "channel": "WEB_GB",
62 | "enabled": true,
63 | "locale": "en_GB"
64 | },
65 | "name": "attributes",
66 | "items": [
67 | {
68 | "tags": [],
69 | "urlParameters": [],
70 | "resetUrlParameters": {
71 | "channel": "WEB_GB",
72 | "enabled": true,
73 | "locale": "en_GB"
74 | },
75 | "name": "Mug collection",
76 | "choices": {
77 | "HOLIDAY COLLECTION": {
78 | "active": false,
79 | "default": false,
80 | "urlParameters": {
81 | "channel": "WEB_GB",
82 | "enabled": true,
83 | "locale": "en_GB",
84 | "attributes": {
85 | "Mug material": [
86 | "Wood"
87 | ],
88 | "Mug collection": [
89 | "HOLIDAY COLLECTION"
90 | ]
91 | }
92 | },
93 | "label": "HOLIDAY COLLECTION",
94 | "count": 1
95 | }
96 | }
97 | },
98 | {
99 | "tags": [],
100 | "urlParameters": [],
101 | "resetUrlParameters": {
102 | "channel": "WEB_GB",
103 | "enabled": true,
104 | "locale": "en_GB"
105 | },
106 | "name": "Mug material",
107 | "choices": {
108 | "Wood": {
109 | "active": true,
110 | "default": false,
111 | "urlParameters": {
112 | "channel": "WEB_GB",
113 | "enabled": true,
114 | "locale": "en_GB",
115 | "attributes": {
116 | "Mug material": []
117 | }
118 | },
119 | "label": "Wood",
120 | "count": 1
121 | }
122 | }
123 | },
124 | {
125 | "tags": [],
126 | "urlParameters": [],
127 | "resetUrlParameters": {
128 | "channel": "WEB_GB",
129 | "enabled": true,
130 | "locale": "en_GB"
131 | },
132 | "name": "Production year",
133 | "choices": {
134 | "2015": {
135 | "active": false,
136 | "default": false,
137 | "urlParameters": {
138 | "channel": "WEB_GB",
139 | "enabled": true,
140 | "locale": "en_GB",
141 | "attributes": {
142 | "Mug material": [
143 | "Wood"
144 | ],
145 | "Production year": [
146 | "2015"
147 | ]
148 | }
149 | },
150 | "label": "2015",
151 | "count": 1
152 | }
153 | }
154 | }
155 | ]
156 | },
157 | "attributesByCode": "@array@",
158 | "options": "@array@",
159 | "sort": "@array@"
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/tests/Responses/Expected/WEB_GB/de_DE/product_list_page_filtered_by_mug_material_wood_attribute.json:
--------------------------------------------------------------------------------
1 | {
2 | "page": 1,
3 | "limit": 9,
4 | "total": 1,
5 | "pages": 1,
6 | "items": [
7 | {
8 | "id": 1,
9 | "code": "LOGAN_MUG_CODE",
10 | "name": "Logan Becher",
11 | "slug": "@string@",
12 | "taxons": "@array@",
13 | "variants": "@array@",
14 | "attributes": "@array@",
15 | "images": "@array@",
16 | "price": "@array@",
17 | "channelCode": "WEB_GB",
18 | "localeCode": "de_DE"
19 | }
20 | ],
21 | "filters": {
22 | "channel": "@array@",
23 | "enabled": "@array@",
24 | "taxonCode": "@array@",
25 | "taxonSlug": "@array@",
26 | "priceRange": "@array@",
27 | "locale": "@array@",
28 | "inStock": "@array@",
29 | "paginator": "@array@",
30 | "search": "@array@",
31 | "attributes": {
32 | "state": {
33 | "active": true,
34 | "value": {
35 | "Becher Material": [
36 | "Holz"
37 | ]
38 | },
39 | "urlParameters": {
40 | "attributes": {
41 | "Becher Material": [
42 | "Holz"
43 | ]
44 | }
45 | },
46 | "name": "attributes",
47 | "options": []
48 | },
49 | "tags": [],
50 | "urlParameters": {
51 | "channel": "WEB_GB",
52 | "enabled": true,
53 | "locale": "de_DE",
54 | "attributes": {
55 | "Becher Material": [
56 | "Holz"
57 | ]
58 | }
59 | },
60 | "resetUrlParameters": {
61 | "channel": "WEB_GB",
62 | "enabled": true,
63 | "locale": "de_DE"
64 | },
65 | "name": "attributes",
66 | "items": [
67 | {
68 | "tags": [],
69 | "urlParameters": [],
70 | "resetUrlParameters": {
71 | "channel": "WEB_GB",
72 | "enabled": true,
73 | "locale": "de_DE"
74 | },
75 | "name": "Becher Material",
76 | "choices": {
77 | "Holz": {
78 | "active": true,
79 | "default": false,
80 | "urlParameters": {
81 | "channel": "WEB_GB",
82 | "enabled": true,
83 | "locale": "de_DE",
84 | "attributes": {
85 | "Becher Material": []
86 | }
87 | },
88 | "label": "Holz",
89 | "count": 1
90 | }
91 | }
92 | },
93 | {
94 | "tags": [],
95 | "urlParameters": [],
96 | "resetUrlParameters": {
97 | "channel": "WEB_GB",
98 | "enabled": true,
99 | "locale": "de_DE"
100 | },
101 | "name": "Becher Sammlung",
102 | "choices": {
103 | "FEIERTAGSKOLLEKTION": {
104 | "active": false,
105 | "default": false,
106 | "urlParameters": {
107 | "channel": "WEB_GB",
108 | "enabled": true,
109 | "locale": "de_DE",
110 | "attributes": {
111 | "Becher Material": [
112 | "Holz"
113 | ],
114 | "Becher Sammlung": [
115 | "FEIERTAGSKOLLEKTION"
116 | ]
117 | }
118 | },
119 | "label": "FEIERTAGSKOLLEKTION",
120 | "count": 1
121 | }
122 | }
123 | },
124 | {
125 | "tags": [],
126 | "urlParameters": [],
127 | "resetUrlParameters": {
128 | "channel": "WEB_GB",
129 | "enabled": true,
130 | "locale": "de_DE"
131 | },
132 | "name": "Produktionsjahr",
133 | "choices": {
134 | "2015": {
135 | "active": false,
136 | "default": false,
137 | "urlParameters": {
138 | "channel": "WEB_GB",
139 | "enabled": true,
140 | "locale": "de_DE",
141 | "attributes": {
142 | "Becher Material": [
143 | "Holz"
144 | ],
145 | "Produktionsjahr": [
146 | "2015"
147 | ]
148 | }
149 | },
150 | "label": "2015",
151 | "count": 1
152 | }
153 | }
154 | }
155 | ]
156 | },
157 | "attributesByCode": "@array@",
158 | "options": "@array@",
159 | "sort": "@array@"
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/Factory/View/ProductListViewFactory.php:
--------------------------------------------------------------------------------
1 | productListViewClass = $productListViewClass;
56 | $this->productViewClass = $productViewClass;
57 | $this->productVariantViewClass = $productVariantViewClass;
58 | $this->attributeViewClass = $attributeViewClass;
59 | $this->imageViewClass = $imageViewClass;
60 | $this->priceViewClass = $priceViewClass;
61 | $this->taxonViewClass = $taxonViewClass;
62 | }
63 |
64 | /**
65 | * {@inheritdoc}
66 | */
67 | public function createFromSearchResponse(SearchResponse $response): ProductListView
68 | {
69 | $result = $response->getResult();
70 | $filters = $response->getFilters();
71 |
72 | /** @var ProductListView $productListView */
73 | $productListView = new $this->productListViewClass();
74 | $productListView->filters = $filters;
75 |
76 | $pager = $filters['paginator']->getSerializableData()['pager'];
77 | $productListView->page = $pager['current_page'];
78 | $productListView->total = $pager['total_items'];
79 | $productListView->pages = $pager['num_pages'];
80 | $productListView->limit = $pager['limit'];
81 |
82 | /** @var ProductDocument $product */
83 | foreach ($result as $product) {
84 | $productListView->items[] = $this->getProductView($product);
85 | }
86 |
87 | return $productListView;
88 | }
89 |
90 | /**
91 | * @param Collection|ImageDocument[] $images
92 | *
93 | * @return ImageView[]
94 | */
95 | private function getImageViews(Collection $images): array
96 | {
97 | $imageViews = [];
98 | foreach ($images as $image) {
99 | /** @var ImageView $imageView */
100 | $imageView = new $this->imageViewClass();
101 | $imageView->code = $image->getCode();
102 | $imageView->path = $image->getPath();
103 |
104 | $imageViews[] = $imageView;
105 | }
106 |
107 | return $imageViews;
108 | }
109 |
110 | /**
111 | * @param Collection|TaxonDocument[] $taxons
112 | * @param TaxonDocument|null $mainTaxonDocument
113 | *
114 | * @return TaxonView
115 | */
116 | private function getTaxonView(Collection $taxons, ?TaxonDocument $mainTaxonDocument): TaxonView
117 | {
118 | /** @var TaxonView $taxonView */
119 | $taxonView = new $this->taxonViewClass();
120 |
121 | $taxonView->main = null === $mainTaxonDocument ? null : $mainTaxonDocument->getCode();
122 | foreach ($taxons as $taxon) {
123 | $taxonView->others[] = $taxon->getCode();
124 | }
125 |
126 | return $taxonView;
127 | }
128 |
129 | /**
130 | * @param Collection|AttributeDocument[] $attributes
131 | *
132 | * @return AttributeView[]
133 | */
134 | private function getAttributeViews(Collection $attributes): array
135 | {
136 | $attributeValueViews = [];
137 | foreach ($attributes as $attribute) {
138 | /** @var AttributeView $attributeView */
139 | $attributeView = new $this->attributeViewClass();
140 | $attributeView->code = $attribute->getCode();
141 | $attributeView->value = $attribute->getValue();
142 | $attributeView->name = $attribute->getName();
143 |
144 | $attributeValueViews[$attribute->getCode()] = $attributeView;
145 | }
146 |
147 | return $attributeValueViews;
148 | }
149 |
150 | /**
151 | * @param PriceDocument $price
152 | *
153 | * @return PriceView
154 | */
155 | private function getPriceView(PriceDocument $price): PriceView
156 | {
157 | /** @var PriceView $priceView */
158 | $priceView = new $this->priceViewClass();
159 | $priceView->current = $price->getAmount();
160 | $priceView->currency = $price->getCurrency();
161 | $priceView->original = $price->getOriginalAmount();
162 |
163 | return $priceView;
164 | }
165 |
166 | /**
167 | * @param VariantDocument[]|Collection $variants
168 | *
169 | * @return array
170 | */
171 | private function getVariantViews(Collection $variants): array
172 | {
173 | $variantViews = [];
174 | foreach ($variants as $variant) {
175 | /** @var VariantView $variantView */
176 | $variantView = new $this->productVariantViewClass();
177 | $variantView->id = $variant->getId();
178 | $variantView->price = $this->getPriceView($variant->getPrice());
179 | $variantView->code = $variant->getCode();
180 | $variantView->name = $variant->getName();
181 | $variantView->stock = $variant->getStock();
182 | $variantView->isTracked = $variant->getIsTracked();
183 |
184 | if ($variant->getImages()->count() > 0) {
185 | $variantView->images = $this->getImageViews($variant->getImages());
186 | }
187 | $variantViews[] = $variantView;
188 | }
189 |
190 | return $variantViews;
191 | }
192 |
193 | /**
194 | * @param ProductDocument $product
195 | *
196 | * @return ProductView
197 | */
198 | private function getProductView(ProductDocument $product): ProductView
199 | {
200 | /** @var ProductView $productView */
201 | $productView = new $this->productViewClass();
202 | $productView->id = $product->getId();
203 | $productView->slug = $product->getSlug();
204 | $productView->name = $product->getName();
205 | $productView->code = $product->getCode();
206 | $productView->rating = $product->getAverageReviewRating();
207 | $productView->localeCode = $product->getLocaleCode();
208 | $productView->channelCode = $product->getChannelCode();
209 | if ($product->getImages()->count() > 0) {
210 | $productView->images = $this->getImageViews($product->getImages());
211 | }
212 | $productView->taxons = $this->getTaxonView($product->getTaxons(), $product->getMainTaxon());
213 | $productView->attributes = $this->getAttributeViews($product->getAttributes());
214 | $productView->variants = $this->getVariantViews($product->getVariants());
215 | $productView->price = $this->getPriceView($product->getPrice());
216 |
217 | return $productView;
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/src/Filter/Widget/OptionMultiDynamicAggregate.php:
--------------------------------------------------------------------------------
1 | getAggregation(sprintf('%s-filter', $filterName));
38 |
39 | foreach ($values as $name => $value) {
40 | $data[$name] = $aggregation->find(sprintf('%s.%s.name', $name, $filterName));
41 | }
42 |
43 | $data['all-selected'] = $aggregation->find(sprintf('all-selected.%s.%s.name', $filterName, $filterName));
44 |
45 | return $data;
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | public function modifySearch(Search $search, FilterState $state = null, SearchRequest $request = null)
52 | {
53 | if ($state && $state->isActive()) {
54 | $search->addPostFilter($this->getFilterQuery($state->getValue()));
55 | }
56 | }
57 |
58 | /**
59 | * Forms $unsortedChoices array with all possible choices.
60 | * 0 is assigned to the document count of the choices.
61 | *
62 | * @param DocumentIterator $result
63 | * @param ViewData $data
64 | *
65 | * @return array
66 | */
67 | protected function formInitialUnsortedChoices($result, $data)
68 | {
69 | $unsortedChoices = [];
70 | $urlParameters = array_merge(
71 | $data->getResetUrlParameters(),
72 | $data->getState()->getUrlParameters()
73 | );
74 |
75 | foreach ($result->getAggregation($data->getName())->getAggregation($data->getName())->getAggregation('name') as $nameBucket) {
76 | $groupName = $nameBucket['key'];
77 |
78 | foreach ($nameBucket->getAggregation('value') as $bucket) {
79 | $bucketArray = ['key' => $bucket['key'], 'doc_count' => 0];
80 | $choice = $this->createChoice($data, $bucket['key'], '', $bucketArray, $urlParameters);
81 | $unsortedChoices[$groupName][$bucket['key']] = $choice;
82 | }
83 | }
84 |
85 | return $unsortedChoices;
86 | }
87 |
88 | /**
89 | * {@inheritdoc}
90 | */
91 | public function getViewData(DocumentIterator $result, ViewData $data)
92 | {
93 | $unsortedChoices = [];
94 | $activeNames = $data->getState()->isActive() ? array_keys($data->getState()->getValue()) : [];
95 | $filterAggregations = $this->fetchAggregation($result, $data->getName(), $data->getState()->getValue());
96 |
97 | if ($this->getShowZeroChoices()) {
98 | $unsortedChoices = $this->formInitialUnsortedChoices($result, $data);
99 | }
100 |
101 | /** @var AggregationValue $bucket */
102 | foreach ($filterAggregations as $activeName => $filterAggregation) {
103 | foreach ($filterAggregation as $nameAggregation) {
104 | $name = $nameAggregation['key'];
105 |
106 | if (($name != $activeName && $activeName != 'all-selected') ||
107 | ($activeName == 'all-selected' && in_array($name, $activeNames))) {
108 | continue;
109 | }
110 |
111 | foreach ($nameAggregation['value']['buckets'] as $bucket) {
112 | $choice = $this->createChoice($data, $name, $activeName, $bucket);
113 | $unsortedChoices[$name][$bucket['key']] = $choice;
114 | }
115 |
116 | $this->addViewDataItem($data, $name, $unsortedChoices[$name]);
117 | unset($unsortedChoices[$name]);
118 | }
119 | }
120 |
121 | /** @var ViewData\AggregateViewData $data */
122 | $data->sortItems();
123 |
124 | return $data;
125 | }
126 |
127 | /**
128 | * {@inheritdoc}
129 | */
130 | public function preProcessSearch(Search $search, Search $relatedSearch, FilterState $state = null)
131 | {
132 | [$parent, $child, $field] = explode('>', $this->getDocumentField());
133 | $filter = !empty($filter = $relatedSearch->getPostFilters()) ? $filter : new MatchAllQuery();
134 | $parentAggregation = new NestedAggregation($state->getName(), $parent);
135 | $childAggregation = new NestedAggregation($state->getName(), $child);
136 | $nameAggregation = new TermsAggregation('name', $this->getNameField());
137 | $valueAggregation = new TermsAggregation('value', $field);
138 | $filterAggregation = new FilterAggregation($state->getName() . '-filter');
139 | $nameAggregation->addAggregation($valueAggregation);
140 | $childAggregation->addAggregation($nameAggregation);
141 | $parentAggregation->addAggregation($childAggregation);
142 | $filterAggregation->setFilter($filter);
143 |
144 | if ($this->getSortType()) {
145 | $valueAggregation->addParameter('order', [$this->getSortType() => $this->getSortOrder()]);
146 | }
147 |
148 | if ($this->getOption('size')) {
149 | $valueAggregation->addParameter('size', $this->getOption('size'));
150 | }
151 |
152 | if ($state->isActive()) {
153 | foreach ($state->getValue() as $key => $term) {
154 | $terms = $state->getValue();
155 | unset($terms[$key]);
156 |
157 | $this->addSubFilterAggregation(
158 | $filterAggregation,
159 | $childAggregation,
160 | $terms,
161 | $key
162 | );
163 | }
164 | }
165 |
166 | $this->addSubFilterAggregation(
167 | $filterAggregation,
168 | $parentAggregation,
169 | $state->getValue() ? $state->getValue() : [],
170 | 'all-selected'
171 | );
172 |
173 | $search->addAggregation($filterAggregation);
174 |
175 | if ($this->getShowZeroChoices()) {
176 | $search->addAggregation($parentAggregation);
177 | }
178 | }
179 |
180 | /**
181 | * @param array $terms
182 | *
183 | * @return BoolQuery
184 | */
185 | private function getFilterQuery($terms)
186 | {
187 | [$parent, $child, $field] = explode('>', $this->getDocumentField());
188 | $boolQuery = new BoolQuery();
189 | foreach ($terms as $groupName => $values) {
190 | $innerBoolQuery = new BoolQuery();
191 |
192 | foreach ($values as $value) {
193 | $nestedBoolQuery = new BoolQuery();
194 | $nestedBoolQuery->add(new TermQuery($field, $value));
195 | $nestedBoolQuery->add(new TermQuery($this->getNameField(), $groupName));
196 |
197 | $inStockBoolQuery = new BoolQuery();
198 |
199 | $inStockBoolQuery->add(new RangeQuery('variants.stock', ['gte' => 1]), BoolQuery::SHOULD);
200 | $inStockBoolQuery->add(new TermQuery('variants.is_tracked', false), BoolQuery::SHOULD);
201 |
202 | $childBoolQuery = new BoolQuery();
203 | $childBoolQuery->add($inStockBoolQuery, BoolQuery::MUST);
204 | $childBoolQuery->add(new NestedQuery($child, $nestedBoolQuery), BoolQuery::MUST);
205 |
206 | $nestedQuery = new NestedQuery($parent, $childBoolQuery);
207 |
208 | $innerBoolQuery->add(
209 | $nestedQuery,
210 |
211 | BoolQuery::SHOULD
212 | );
213 | }
214 |
215 | $boolQuery->add($innerBoolQuery);
216 | }
217 |
218 | return $boolQuery;
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/src/Factory/Document/ProductDocumentFactory.php:
--------------------------------------------------------------------------------
1 | assertClassExtends($productDocumentClass, ProductDocument::class);
64 | $this->productDocumentClass = $productDocumentClass;
65 |
66 | $this->attributeDocumentFactory = $attributeDocumentFactory;
67 | $this->imageDocumentFactory = $imageDocumentFactory;
68 | $this->priceDocumentFactory = $priceDocumentFactory;
69 | $this->taxonDocumentFactory = $taxonDocumentFactory;
70 | $this->variantDocumentFactory = $variantDocumentFactory;
71 | $this->attributeWhitelist = $attributeWhitelist;
72 | }
73 |
74 | /**
75 | * Create a product document from the product object with all it's related documents
76 | *
77 | * @param ProductInterface $product
78 | * @param LocaleInterface $locale
79 | * @param ChannelInterface $channel
80 | *
81 | * @return ProductDocument
82 | */
83 | public function create(
84 | ProductInterface $product,
85 | LocaleInterface $locale,
86 | ChannelInterface $channel
87 | ): ProductDocument {
88 | /** @var ProductVariantInterface[] $productVariants */
89 | $productVariants = $product->getVariants()->filter(function (ProductVariantInterface $productVariant) use ($channel): bool {
90 | return $productVariant->hasChannelPricingForChannel($channel);
91 | });
92 |
93 | /**
94 | * @var ArrayObject
95 | */
96 | $iterator = $productVariants->getIterator();
97 | $iterator->uasort(
98 | function (ProductVariantInterface $a, ProductVariantInterface $b) {
99 | return $a->getName() <=> $b->getName();
100 | }
101 | );
102 | $variantDocuments = [];
103 | foreach ($iterator as $variant) {
104 | $variantDocuments[] = $this->variantDocumentFactory->create($variant, $channel, $locale);
105 | }
106 |
107 | /** @var ImageDocument[] $imageDocuments */
108 | $imageDocuments = [];
109 | foreach ($product->getImages() as $productImage) {
110 | foreach ($productVariants as $variant) {
111 | if ($variant->hasImage($productImage)) {
112 | continue 2;
113 | }
114 | }
115 |
116 | $imageDocuments[] = $this->imageDocumentFactory->create($productImage);
117 | }
118 |
119 | /** @var TaxonDocument[] $taxonDocuments */
120 | $taxonDocuments = [];
121 | foreach ($product->getProductTaxons() as $syliusProductTaxon) {
122 | $taxonDocuments[] = $this->taxonDocumentFactory->create(
123 | $syliusProductTaxon->getTaxon(),
124 | $locale,
125 | $syliusProductTaxon->getPosition()
126 | );
127 | }
128 |
129 | /** @var ProductTranslationInterface|TranslationInterface $productTranslation */
130 | $productTranslation = $product->getTranslation($locale->getCode());
131 |
132 | $attributeDocuments = $this->getAttributeDocuments($product, $locale, $channel);
133 |
134 | /** @var ProductDocument $productDocument */
135 | $productDocument = new $this->productDocumentClass();
136 | $productDocument->setUuid(Uuid::uuid4()->toString());
137 | $productDocument->setId($product->getId());
138 | $productDocument->setEnabled($product->isEnabled());
139 | $productDocument->setLocaleCode($locale->getCode());
140 | $productDocument->setSlug($productTranslation->getSlug());
141 | $productDocument->setName($productTranslation->getName());
142 | $productDocument->setDescription($productTranslation->getDescription());
143 | $productDocument->setChannelCode($channel->getCode());
144 | $productDocument->setCode($product->getCode());
145 | $productDocument->setCreatedAt($product->getCreatedAt());
146 | $productDocument->setSynchronisedAt(new \DateTime('now'));
147 | $productDocument->setAverageReviewRating($product->getAverageRating());
148 | $productDocument->setVariants(new Collection($variantDocuments));
149 | $productDocument->setImages(new Collection($imageDocuments));
150 | $productDocument->setTaxons(new Collection($taxonDocuments));
151 | $productDocument->setAttributes(new Collection($attributeDocuments));
152 |
153 | /**
154 | * Set smallest product variant price, used for search by price
155 | */
156 | $productDocument->setPrice(
157 | $this->priceDocumentFactory->create(
158 | $this->getMinimalPriceFromVariants($productVariants, $channel),
159 | $channel->getBaseCurrency()
160 | )
161 | );
162 |
163 | if (null !== $product->getMainTaxon()) {
164 | $productDocument->setMainTaxon(
165 | $this->taxonDocumentFactory->create($product->getMainTaxon(), $locale)
166 | );
167 | }
168 |
169 | return $productDocument;
170 | }
171 |
172 | /**
173 | * @param ProductVariantInterface[]|DoctrineCollection $variants
174 | * @param ChannelInterface $channel
175 | *
176 | * @return ChannelPricingInterface
177 | */
178 | private function getMinimalPriceFromVariants($variants, ChannelInterface $channel): ChannelPricingInterface
179 | {
180 | /** @var ChannelPricingInterface $minProductChannelPrice */
181 | $minProductChannelPrice = $variants->first()->getChannelPricingForChannel($channel);
182 |
183 | foreach ($variants as $variant) {
184 | $channelPrice = $variant->getChannelPricingForChannel($channel);
185 | if (
186 | ($variant->isTracked() && $variant->getOnHold() - $variant->getOnHold(
187 | ) > 0 && $minProductChannelPrice->getPrice() < $channelPrice->getPrice())
188 | || (!$variant->isTracked() && $minProductChannelPrice->getPrice() < $channelPrice->getPrice())
189 | ) {
190 | $minProductChannelPrice = $channelPrice;
191 | }
192 | }
193 |
194 | return $minProductChannelPrice;
195 | }
196 |
197 | /**
198 | * @param string $class
199 | * @param string $parentClass
200 | *
201 | * @throws \InvalidArgumentException
202 | */
203 | private function assertClassExtends(string $class, string $parentClass)
204 | {
205 | if ($class !== $parentClass && !in_array($parentClass, class_parents($class), true)) {
206 | throw new \InvalidArgumentException(sprintf('Class %s MUST extend class %s!', $class, $parentClass));
207 | }
208 | }
209 |
210 | /**
211 | * @param ProductInterface $product
212 | * @param LocaleInterface $locale
213 | * @param ChannelInterface $channel
214 | *
215 | * @return array
216 | */
217 | private function getAttributeDocuments(
218 | ProductInterface $product,
219 | LocaleInterface $locale,
220 | ChannelInterface $channel
221 | ): array {
222 | $productAttributes = $product->getAttributesByLocale(
223 | $locale->getCode(),
224 | $channel->getDefaultLocale()->getCode()
225 | );
226 |
227 | $attributeDocuments = [];
228 | foreach ($productAttributes as $syliusProductAttributeValue) {
229 | if (in_array($syliusProductAttributeValue->getCode(), $this->attributeWhitelist, true) || empty($this->attributeWhitelist)) {
230 | $attributeDocuments = array_merge(
231 | $attributeDocuments,
232 | $this->attributeDocumentFactory->create(
233 | $syliusProductAttributeValue->getValue(),
234 | $locale,
235 | $syliusProductAttributeValue
236 | )
237 | );
238 | }
239 | }
240 |
241 | return $attributeDocuments;
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/src/Document/ProductDocument.php:
--------------------------------------------------------------------------------
1 | attributes = new Collection();
153 | $this->taxons = new Collection();
154 | $this->images = new Collection();
155 | $this->variants = new Collection();
156 | }
157 |
158 | /**
159 | * @return string
160 | */
161 | public function getUuid(): string
162 | {
163 | return $this->uuid;
164 | }
165 |
166 | /**
167 | * @param string $uuid
168 | */
169 | public function setUuid(string $uuid): void
170 | {
171 | $this->uuid = $uuid;
172 | }
173 |
174 | /**
175 | * @return mixed
176 | */
177 | public function getId()
178 | {
179 | return $this->id;
180 | }
181 |
182 | /**
183 | * @param mixed $id
184 | */
185 | public function setId($id): void
186 | {
187 | $this->id = $id;
188 | }
189 |
190 | /**
191 | * @return string
192 | */
193 | public function getCode(): string
194 | {
195 | return $this->code;
196 | }
197 |
198 | /**
199 | * @param string $code
200 | */
201 | public function setCode(string $code): void
202 | {
203 | $this->code = $code;
204 | }
205 |
206 | /**
207 | * @return string
208 | */
209 | public function getName(): string
210 | {
211 | return $this->name;
212 | }
213 |
214 | /**
215 | * @param string $name
216 | */
217 | public function setName(string $name): void
218 | {
219 | $this->name = $name;
220 | }
221 |
222 | /**
223 | * @return bool
224 | */
225 | public function isEnabled(): bool
226 | {
227 | return $this->enabled;
228 | }
229 |
230 | /**
231 | * @param bool $enabled
232 | */
233 | public function setEnabled(bool $enabled): void
234 | {
235 | $this->enabled = $enabled;
236 | }
237 |
238 | /**
239 | * @return string
240 | */
241 | public function getSlug(): string
242 | {
243 | return $this->slug;
244 | }
245 |
246 | /**
247 | * @param string $slug
248 | */
249 | public function setSlug(string $slug): void
250 | {
251 | $this->slug = $slug;
252 | }
253 |
254 | /**
255 | * @return string
256 | */
257 | public function getChannelCode(): string
258 | {
259 | return $this->channelCode;
260 | }
261 |
262 | /**
263 | * @param string $channelCode
264 | */
265 | public function setChannelCode(string $channelCode): void
266 | {
267 | $this->channelCode = $channelCode;
268 | }
269 |
270 | /**
271 | * @return string
272 | */
273 | public function getLocaleCode(): string
274 | {
275 | return $this->localeCode;
276 | }
277 |
278 | /**
279 | * @param string $localeCode
280 | */
281 | public function setLocaleCode(string $localeCode): void
282 | {
283 | $this->localeCode = $localeCode;
284 | }
285 |
286 | /**
287 | * @return string
288 | */
289 | public function getDescription(): ?string
290 | {
291 | return $this->description;
292 | }
293 |
294 | /**
295 | * @param string $description
296 | */
297 | public function setDescription(?string $description): void
298 | {
299 | $this->description = $description;
300 | }
301 |
302 | /**
303 | * @return PriceDocument
304 | */
305 | public function getPrice(): PriceDocument
306 | {
307 | return $this->price;
308 | }
309 |
310 | /**
311 | * @param PriceDocument $price
312 | */
313 | public function setPrice(PriceDocument $price): void
314 | {
315 | $this->price = $price;
316 | }
317 |
318 | /**
319 | * @return TaxonDocument
320 | */
321 | public function getMainTaxon(): ?TaxonDocument
322 | {
323 | return $this->mainTaxon;
324 | }
325 |
326 | /**
327 | * @param TaxonDocument $mainTaxon
328 | */
329 | public function setMainTaxon(TaxonDocument $mainTaxon): void
330 | {
331 | $this->mainTaxon = $mainTaxon;
332 | }
333 |
334 | /**
335 | * @return Collection|TaxonDocument[]
336 | */
337 | public function getTaxons(): Collection
338 | {
339 | return $this->taxons;
340 | }
341 |
342 | /**
343 | * @param Collection|TaxonDocument[] $taxons
344 | */
345 | public function setTaxons($taxons): void
346 | {
347 | $this->taxons = $taxons;
348 | }
349 |
350 | /**
351 | * @return Collection
352 | */
353 | public function getAttributes(): Collection
354 | {
355 | return $this->attributes;
356 | }
357 |
358 | /**
359 | * @param Collection $attributes
360 | */
361 | public function setAttributes(Collection $attributes): void
362 | {
363 | $this->attributes = $attributes;
364 | }
365 |
366 | /**
367 | * @return Collection
368 | */
369 | public function getImages(): Collection
370 | {
371 | return $this->images;
372 | }
373 |
374 | /**
375 | * @param Collection $images
376 | */
377 | public function setImages(Collection $images): void
378 | {
379 | $this->images = $images;
380 | }
381 |
382 | /**
383 | * @return float
384 | */
385 | public function getAverageReviewRating(): ?float
386 | {
387 | return $this->averageReviewRating;
388 | }
389 |
390 | /**
391 | * @param float $averageReviewRating
392 | */
393 | public function setAverageReviewRating(float $averageReviewRating): void
394 | {
395 | $this->averageReviewRating = $averageReviewRating;
396 | }
397 |
398 | /**
399 | * @return \DateTimeInterface
400 | */
401 | public function getCreatedAt(): \DateTimeInterface
402 | {
403 | return $this->createdAt;
404 | }
405 |
406 | /**
407 | * @param \DateTimeInterface $createdAt
408 | */
409 | public function setCreatedAt(\DateTimeInterface $createdAt): void
410 | {
411 | $this->createdAt = $createdAt;
412 | }
413 |
414 | /**
415 | * @return \DateTimeInterface
416 | */
417 | public function getSynchronisedAt(): \DateTimeInterface
418 | {
419 | return $this->synchronisedAt;
420 | }
421 |
422 | /**
423 | * @param \DateTimeInterface $synchronisedAt
424 | */
425 | public function setSynchronisedAt(\DateTimeInterface $synchronisedAt): void
426 | {
427 | $this->synchronisedAt = $synchronisedAt;
428 | }
429 |
430 | /**
431 | * @return Collection
432 | */
433 | public function getVariants(): Collection
434 | {
435 | return $this->variants;
436 | }
437 |
438 | /**
439 | * @param Collection $variants
440 | */
441 | public function setVariants(Collection $variants): void
442 | {
443 | $this->variants = $variants;
444 | }
445 | }
446 |
--------------------------------------------------------------------------------
/tests/Factory/ProductDocumentFactoryTest.php:
--------------------------------------------------------------------------------
1 | productRepository = static::$kernel->getContainer()->get('doctrine.orm.entity_manager')->getRepository(
47 | Product::class
48 | )
49 | ;
50 | $this->localeRepository = static::$kernel->getContainer()->get('doctrine.orm.entity_manager')->getRepository(
51 | Locale::class
52 | )
53 | ;
54 | $this->channelRepository = static::$kernel->getContainer()->get('doctrine.orm.entity_manager')->getRepository(
55 | Channel::class
56 | )
57 | ;
58 | }
59 |
60 | /**
61 | * @test
62 | */
63 | public function it_creates_product_document_from_sylius_product_model()
64 | {
65 | /** @var ChannelInterface $syliusChannel */
66 | $syliusChannel = $this->channelRepository->findOneByCode('WEB_GB');
67 | /** @var Locale $syliusLocale */
68 | $syliusLocale = $this->localeRepository->findOneBy(['code' => 'en_GB']);
69 | $createdAt = \DateTime::createFromFormat(\DateTime::W3C, '2017-04-18T16:12:55+02:00');
70 |
71 | /** @var Product $syliusProduct */
72 | $syliusProduct = $this->productRepository->findOneByChannelAndSlug(
73 | $syliusChannel,
74 | $syliusLocale->getCode(),
75 | 'logan-mug'
76 | );
77 | $syliusProduct->setCreatedAt($createdAt);
78 |
79 | $factory = new ProductDocumentFactory(
80 | ProductDocument::class,
81 | new AttributeDocumentFactory(AttributeDocument::class),
82 | new ImageDocumentFactory(ImageDocument::class),
83 | new PriceDocumentFactory(PriceDocument::class),
84 | new TaxonDocumentFactory(TaxonDocument::class, new ImageDocumentFactory(ImageDocument::class)),
85 | new VariantDocumentFactory(
86 | VariantDocument::class,
87 | new PriceDocumentFactory(PriceDocument::class),
88 | new ImageDocumentFactory(ImageDocument::class),
89 | new OptionDocumentFactory(OptionDocument::class)
90 | ),
91 | ['MUG_COLLECTION_CODE', 'MUG_MATERIAL_CODE', 'PRODUCTION_YEAR']
92 | );
93 | /** @var ProductDocument $product */
94 | $product = $factory->create(
95 | $syliusProduct,
96 | $syliusLocale,
97 | $syliusChannel
98 | );
99 |
100 | $taxon = $this->makeMainTaxon();
101 |
102 | $productTaxons = $this->makeProductTaxons();
103 |
104 | $productAttributes = $this->makeProductAttributes();
105 |
106 | $this->assertEquals($product->getCode(), $product->getCode());
107 | $this->assertEquals($product->getName(), $product->getName());
108 | $this->assertEquals('en_GB', $product->getLocaleCode());
109 | $this->assertEquals(new Collection($productAttributes), $product->getAttributes());
110 | $this->assertEquals(1000, $product->getPrice()->getAmount());
111 | $this->assertEquals('GBP', $product->getPrice()->getCurrency());
112 | $this->assertEquals('en_GB', $product->getLocaleCode());
113 | $this->assertEquals('WEB_GB', $product->getChannelCode());
114 | $this->assertEquals('logan-mug', $product->getSlug());
115 | $this->assertEquals('Logan Mug', $product->getName());
116 | $this->assertEquals($createdAt, $product->getCreatedAt());
117 | $this->assertEquals('Logan Mug', $product->getDescription());
118 | $this->assertEquals($taxon, $product->getMainTaxon());
119 | $this->assertEquals(new Collection($productTaxons), $product->getTaxons());
120 | $this->assertEquals(0.0, $product->getAverageReviewRating());
121 | }
122 |
123 | /**
124 | * @test
125 | */
126 | public function it_creates_product_document_only_with_whitelisted_attributes()
127 | {
128 | /** @var ChannelInterface $syliusChannel */
129 | $syliusChannel = $this->channelRepository->findOneByCode('WEB_GB');
130 | /** @var Locale $syliusLocale */
131 | $syliusLocale = $this->localeRepository->findOneBy(['code' => 'en_GB']);
132 | $createdAt = \DateTime::createFromFormat(\DateTime::W3C, '2017-04-18T16:12:55+02:00');
133 |
134 | /** @var Product $syliusProduct */
135 | $syliusProduct = $this->productRepository->findOneByChannelAndSlug(
136 | $syliusChannel,
137 | $syliusLocale->getCode(),
138 | 'logan-mug'
139 | );
140 | $syliusProduct->setCreatedAt($createdAt);
141 |
142 | $factory = new ProductDocumentFactory(
143 | ProductDocument::class,
144 | new AttributeDocumentFactory(AttributeDocument::class),
145 | new ImageDocumentFactory(ImageDocument::class),
146 | new PriceDocumentFactory(PriceDocument::class),
147 | new TaxonDocumentFactory(TaxonDocument::class, new ImageDocumentFactory(ImageDocument::class)),
148 | new VariantDocumentFactory(
149 | VariantDocument::class,
150 | new PriceDocumentFactory(PriceDocument::class),
151 | new ImageDocumentFactory(ImageDocument::class),
152 | new OptionDocumentFactory(OptionDocument::class)
153 | ),
154 | ['PRODUCTION_YEAR']
155 | );
156 | /** @var ProductDocument $product */
157 | $product = $factory->create(
158 | $syliusProduct,
159 | $syliusLocale,
160 | $syliusChannel
161 | );
162 |
163 | $taxon = $this->makeMainTaxon();
164 |
165 | $productTaxons = $this->makeProductTaxons();
166 |
167 | $productAttribute = $this->makeProductionYearAttribute();
168 |
169 | $this->assertEquals($product->getCode(), $product->getCode());
170 | $this->assertEquals($product->getName(), $product->getName());
171 | $this->assertEquals('en_GB', $product->getLocaleCode());
172 | $this->assertEquals(
173 | new Collection([$productAttribute]),
174 | $product->getAttributes()
175 | );
176 | $this->assertEquals(1000, $product->getPrice()->getAmount());
177 | $this->assertEquals('GBP', $product->getPrice()->getCurrency());
178 | $this->assertEquals('en_GB', $product->getLocaleCode());
179 | $this->assertEquals('WEB_GB', $product->getChannelCode());
180 | $this->assertEquals('logan-mug', $product->getSlug());
181 | $this->assertEquals('Logan Mug', $product->getName());
182 | $this->assertEquals($createdAt, $product->getCreatedAt());
183 | $this->assertEquals('Logan Mug', $product->getDescription());
184 | $this->assertEquals($taxon, $product->getMainTaxon());
185 | $this->assertEquals(new Collection($productTaxons), $product->getTaxons());
186 | $this->assertEquals(0.0, $product->getAverageReviewRating());
187 | }
188 |
189 | /**
190 | * @return TaxonDocument
191 | */
192 | private function makeMainTaxon(): TaxonDocument
193 | {
194 | $taxon = new TaxonDocument();
195 | $taxon->setCode('MUG');
196 | $taxon->setPosition(0);
197 | $taxon->setSlug('categories/mugs');
198 | $taxon->setDescription('Lorem ipsum');
199 |
200 | return $taxon;
201 | }
202 |
203 | /**
204 | * @return array
205 | */
206 | private function makeProductTaxons(): array
207 | {
208 | $productTaxons = [];
209 | $productTaxon = new TaxonDocument();
210 | $productTaxon->setCode('MUG');
211 | $productTaxon->setSlug('categories/mugs');
212 | $productTaxon->setPosition(0);
213 | $productTaxon->setDescription('Lorem ipsum');
214 | $productTaxons[] = $productTaxon;
215 | $productTaxon = new TaxonDocument();
216 | $productTaxon->setCode('BRAND');
217 | $productTaxon->setSlug('brands');
218 | $productTaxon->setPosition(3);
219 | $productTaxon->setDescription('Lorem ipsum');
220 | $productTaxons[] = $productTaxon;
221 |
222 | return $productTaxons;
223 | }
224 |
225 | /**
226 | * @return AttributeDocument
227 | */
228 | private function makeProductionYearAttribute(): AttributeDocument
229 | {
230 | $productAttribute = new AttributeDocument();
231 | $productAttribute->setCode('PRODUCTION_YEAR');
232 | $productAttribute->setName('Production year');
233 | $productAttribute->setValue('2015');
234 |
235 | return $productAttribute;
236 | }
237 |
238 | private function makeProductAttributes(): array
239 | {
240 | $productAttributes = [];
241 |
242 | $productAttribute = new AttributeDocument();
243 | $productAttribute->setCode('MUG_COLLECTION_CODE');
244 | $productAttribute->setName('Mug collection');
245 | $productAttribute->setValue('HOLIDAY COLLECTION');
246 | $productAttributes[] = $productAttribute;
247 |
248 | $productAttribute = new AttributeDocument();
249 | $productAttribute->setCode('MUG_MATERIAL_CODE');
250 | $productAttribute->setName('Mug material');
251 | $productAttribute->setValue('Wood');
252 | $productAttributes[] = $productAttribute;
253 |
254 | $productAttributes[] = $this->makeProductionYearAttribute();
255 |
256 | return $productAttributes;
257 | }
258 | }
259 |
--------------------------------------------------------------------------------