diff --git a/composer.json b/composer.json index 7bff322..3138471 100644 --- a/composer.json +++ b/composer.json @@ -67,6 +67,7 @@ "psalm/plugin-phpunit": "^0.18.4", "setono/code-quality-pack": "^2.8.2", "sylius/sylius": "~1.12.18", + "symfony/browser-kit": "^5.4 || ^6.4 || ^7.0", "symfony/debug-bundle": "^5.4 || ^6.4 || ^7.0", "symfony/dotenv": "^5.4 || ^6.4 || ^7.0", "symfony/http-client": "^5.4 || ^6.4 || ^7.0", @@ -107,7 +108,7 @@ "analyse": "psalm", "check-style": "ecs check", "fix-style": "ecs check --fix", - "phpunit": "phpunit", + "phpunit": "phpunit --exclude-group=functional", "rector": "rector" } } diff --git a/src/Controller/Action/SearchAction.php b/src/Controller/Action/SearchAction.php index 26d506b..d1437ee 100644 --- a/src/Controller/Action/SearchAction.php +++ b/src/Controller/Action/SearchAction.php @@ -5,15 +5,10 @@ namespace Setono\SyliusMeilisearchPlugin\Controller\Action; use Doctrine\Persistence\ManagerRegistry; -use Meilisearch\Client; use Setono\Doctrine\ORMTrait; -use Setono\SyliusMeilisearchPlugin\Config\Index; -use Setono\SyliusMeilisearchPlugin\Document\Metadata\Facet; -use Setono\SyliusMeilisearchPlugin\Document\Metadata\MetadataFactoryInterface; +use Setono\SyliusMeilisearchPlugin\Engine\SearchEngineInterface; use Setono\SyliusMeilisearchPlugin\Form\Builder\SearchFormBuilderInterface; -use Setono\SyliusMeilisearchPlugin\Meilisearch\Builder\FilterBuilderInterface; use Setono\SyliusMeilisearchPlugin\Model\IndexableInterface; -use Setono\SyliusMeilisearchPlugin\Resolver\IndexName\IndexNameResolverInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Twig\Environment; @@ -26,40 +21,23 @@ final class SearchAction public function __construct( ManagerRegistry $managerRegistry, private readonly Environment $twig, - private readonly IndexNameResolverInterface $indexNameResolver, - private readonly Client $client, - private readonly MetadataFactoryInterface $metadataFactory, private readonly SearchFormBuilderInterface $searchFormBuilder, - private readonly FilterBuilderInterface $filterBuilder, - private readonly Index $index, - private readonly int $hitsPerPage, + private readonly SearchEngineInterface $searchEngine, ) { $this->managerRegistry = $managerRegistry; } public function __invoke(Request $request): Response { - $q = $request->query->get('q'); - Assert::nullOrString($q); + $query = $request->query->get('q'); + Assert::nullOrString($query); - $page = (int) $request->query->get('p', 1); - $page = max(1, $page); - - $metadata = $this->metadataFactory->getMetadataFor($this->index->document); - - $searchResult = $this->client->index($this->indexNameResolver->resolve($this->index))->search($q, [ - 'facets' => array_map(static fn (Facet $facet) => $facet->name, $metadata->getFacets()), - 'filter' => $this->filterBuilder->build($request), - 'sort' => ['price:asc'], - 'hitsPerPage' => $this->hitsPerPage, - 'page' => $page, - ]); + $searchResult = $this->searchEngine->execute($query, $request->query->all()); $searchForm = $this->searchFormBuilder->build($searchResult); $searchForm->handleRequest($request); $items = []; - /** @var array{entityClass: class-string, entityId: mixed} $hit */ foreach ($searchResult->getHits() as $hit) { $items[] = $this->getManager($hit['entityClass'])->find($hit['entityClass'], $hit['entityId']); diff --git a/src/Engine/SearchEngine.php b/src/Engine/SearchEngine.php new file mode 100644 index 0000000..e291060 --- /dev/null +++ b/src/Engine/SearchEngine.php @@ -0,0 +1,46 @@ +metadataFactory->getMetadataFor($this->index->document); + + $searchParams = [ + 'facets' => array_map(static fn (Facet $facet) => $facet->name, $metadata->getFacets()), + 'filter' => $this->filterBuilder->build($parameters), + 'hitsPerPage' => $this->hitsPerPage, + 'page' => $page, + ]; + if ('' !== $sort) { + $searchParams['sort'] = [$sort]; + } + + return $this->client->index($this->indexNameResolver->resolve($this->index))->search($query, $searchParams); + } +} diff --git a/src/Engine/SearchEngineInterface.php b/src/Engine/SearchEngineInterface.php new file mode 100644 index 0000000..142fea4 --- /dev/null +++ b/src/Engine/SearchEngineInterface.php @@ -0,0 +1,12 @@ +add('sort', ChoiceType::class, [ 'choices' => [ - 'Price: Low to High' => 'price:asc', - 'Price: High to Low' => 'price:desc', + 'Cheapest first' => 'price:asc', + 'Biggest discount' => 'discount:desc', + 'Newest first' => 'createdAt:desc', + 'Relevance' => '', ], 'required' => false, 'placeholder' => 'Sort by', diff --git a/src/Meilisearch/Builder/FilterBuilder.php b/src/Meilisearch/Builder/FilterBuilder.php index a029e4c..cb35039 100644 --- a/src/Meilisearch/Builder/FilterBuilder.php +++ b/src/Meilisearch/Builder/FilterBuilder.php @@ -4,16 +4,14 @@ namespace Setono\SyliusMeilisearchPlugin\Meilisearch\Builder; -use Symfony\Component\HttpFoundation\Request; - // todo this should be refactored final class FilterBuilder implements FilterBuilderInterface { - public function build(Request $request): array + public function build(array $parameters): array { $filters = []; - $query = $request->query->has('facets') ? $request->query->all('facets') : $request->query->all(); + $query = (array) ($parameters['facets'] ?? $parameters); if (isset($query['onSale'])) { $filters[] = 'onSale = true'; diff --git a/src/Meilisearch/Builder/FilterBuilderInterface.php b/src/Meilisearch/Builder/FilterBuilderInterface.php index a5109ab..989ac77 100644 --- a/src/Meilisearch/Builder/FilterBuilderInterface.php +++ b/src/Meilisearch/Builder/FilterBuilderInterface.php @@ -4,12 +4,7 @@ namespace Setono\SyliusMeilisearchPlugin\Meilisearch\Builder; -use Symfony\Component\HttpFoundation\Request; - interface FilterBuilderInterface { - /** - * Takes a Symfony request and returns a filter ready for the Meilisearch client - */ - public function build(Request $request): array; + public function build(array $parameters): array; } diff --git a/src/Resources/config/services/conditional/search.xml b/src/Resources/config/services/conditional/search.xml index 26f49f3..ff57dfc 100644 --- a/src/Resources/config/services/conditional/search.xml +++ b/src/Resources/config/services/conditional/search.xml @@ -10,13 +10,19 @@ - - - + + + + + + + %setono_sylius_meilisearch.search.hits_per_page% + + diff --git a/tests/Functional/SearchTest.php b/tests/Functional/SearchTest.php new file mode 100644 index 0000000..6474bd8 --- /dev/null +++ b/tests/Functional/SearchTest.php @@ -0,0 +1,76 @@ + 'test', 'debug' => true]); + } + + public function testItProvidesSearchResults(): void + { + /** @var SearchEngine $searchEngine */ + $searchEngine = self::getContainer()->get(SearchEngine::class); + $result = $searchEngine->execute('jeans'); + + self::assertSame(8, $result->getHitsCount()); + } + + public function testItSortsSearchResultsByLowestPrice(): void + { + /** @var SearchEngine $searchEngine */ + $searchEngine = self::getContainer()->get(SearchEngine::class); + $result = $searchEngine->execute('jeans', ['sort' => 'price:asc']); + + self::assertSame(8, $result->getHitsCount()); + + $previousKey = null; + foreach ($result->getHits() as $key => $hit) { + if ($previousKey === null) { + $previousKey = $key; + + continue; + } + + $previousHit = (array) $result->getHit($previousKey); + self::assertGreaterThanOrEqual($previousHit['price'], $hit['price']); + $previousKey = $key; + } + } + + public function testItSortsSearchResultsByNewestDate(): void + { + /** @var SearchEngine $searchEngine */ + $searchEngine = self::getContainer()->get(SearchEngine::class); + $result = $searchEngine->execute('jeans', ['sort' => 'createdAt:desc']); + + self::assertSame(8, $result->getHitsCount()); + + $previousKey = null; + foreach ($result->getHits() as $key => $hit) { + if ($previousKey === null) { + $previousKey = $key; + + continue; + } + + $previousHit = (array) $result->getHit($previousKey); + self::assertLessThanOrEqual($previousHit['createdAt'], $hit['createdAt']); + $previousKey = $key; + } + } +} diff --git a/tests/DataMapper/Product/OptionsDataMapperTest.php b/tests/Unit/DataMapper/Product/OptionsDataMapperTest.php similarity index 97% rename from tests/DataMapper/Product/OptionsDataMapperTest.php rename to tests/Unit/DataMapper/Product/OptionsDataMapperTest.php index 2571907..f87305b 100644 --- a/tests/DataMapper/Product/OptionsDataMapperTest.php +++ b/tests/Unit/DataMapper/Product/OptionsDataMapperTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\SyliusMeilisearchPlugin\Tests\DataMapper\Product; +namespace Setono\SyliusMeilisearchPlugin\Tests\Unit\DataMapper\Product; use Doctrine\Common\Collections\ArrayCollection; use PHPUnit\Framework\TestCase; diff --git a/tests/DataMapper/Product/Provider/ProductPricesProviderTest.php b/tests/Unit/DataMapper/Product/Provider/ProductPricesProviderTest.php similarity index 100% rename from tests/DataMapper/Product/Provider/ProductPricesProviderTest.php rename to tests/Unit/DataMapper/Product/Provider/ProductPricesProviderTest.php diff --git a/tests/DependencyInjection/SetonoSyliusMeilisearchExtensionTest.php b/tests/Unit/DependencyInjection/SetonoSyliusMeilisearchExtensionTest.php similarity index 97% rename from tests/DependencyInjection/SetonoSyliusMeilisearchExtensionTest.php rename to tests/Unit/DependencyInjection/SetonoSyliusMeilisearchExtensionTest.php index ccab261..f652961 100644 --- a/tests/DependencyInjection/SetonoSyliusMeilisearchExtensionTest.php +++ b/tests/Unit/DependencyInjection/SetonoSyliusMeilisearchExtensionTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\SyliusMeilisearchPlugin\Tests\DependencyInjection; +namespace Setono\SyliusMeilisearchPlugin\Tests\Unit\DependencyInjection; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; use Setono\SyliusMeilisearchPlugin\Controller\Action\SearchAction; diff --git a/tests/Document/Metadata/MetadataTest.php b/tests/Unit/Document/Metadata/MetadataTest.php similarity index 90% rename from tests/Document/Metadata/MetadataTest.php rename to tests/Unit/Document/Metadata/MetadataTest.php index e2c6b17..b304ee2 100644 --- a/tests/Document/Metadata/MetadataTest.php +++ b/tests/Unit/Document/Metadata/MetadataTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\SyliusMeilisearchPlugin\Tests\Document\Metadata; +namespace Setono\SyliusMeilisearchPlugin\Tests\Unit\Document\Metadata; use PHPUnit\Framework\TestCase; use Setono\SyliusMeilisearchPlugin\Document\Attribute\Facet; diff --git a/tests/Resolver/IndexName/IndexNameResolverTest.php b/tests/Unit/Resolver/IndexName/IndexNameResolverTest.php similarity index 94% rename from tests/Resolver/IndexName/IndexNameResolverTest.php rename to tests/Unit/Resolver/IndexName/IndexNameResolverTest.php index 3ed0d46..8ff06d7 100644 --- a/tests/Resolver/IndexName/IndexNameResolverTest.php +++ b/tests/Unit/Resolver/IndexName/IndexNameResolverTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\SyliusMeilisearchPlugin\Tests\Resolver\IndexName; +namespace Setono\SyliusMeilisearchPlugin\Tests\Unit\Resolver\IndexName; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait;