Skip to content

Commit

Permalink
Add attributes to make it easy to configure the index
Browse files Browse the repository at this point in the history
  • Loading branch information
loevgaard committed Jul 5, 2024
1 parent c06463d commit 9b3e825
Show file tree
Hide file tree
Showing 15 changed files with 242 additions and 117 deletions.
41 changes: 10 additions & 31 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,34 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.25.0@01a8eb06b9e9cc6cfb6a320bf9fb14331919d505">
<file src="src/Indexer/DefaultIndexer.php">
<MixedArgument>
<code><![CDATA[SearchIndex::class]]></code>
</MixedArgument>
<MixedAssignment>
<code><![CDATA[$index]]></code>
</MixedAssignment>
<MixedInferredReturnType>
<code><![CDATA[SearchIndex]]></code>
</MixedInferredReturnType>
<MixedMethodCall>
<code><![CDATA[exists]]></code>
<code><![CDATA[getSettings]]></code>
</MixedMethodCall>
<MixedReturnStatement>
<code><![CDATA[$index]]></code>
<code><![CDATA[$index]]></code>
</MixedReturnStatement>
<PossiblyInvalidArgument>
<code><![CDATA[$resource]]></code>
</PossiblyInvalidArgument>
<UndefinedClass>
<code><![CDATA[$algoliaIndex]]></code>
<code><![CDATA[$algoliaIndex]]></code>
<code><![CDATA[SearchIndex]]></code>
<code><![CDATA[SearchIndex]]></code>
</UndefinedClass>
<UndefinedMethod>
<code><![CDATA[initIndex]]></code>
</UndefinedMethod>
<files psalm-version="5.23.1@8471a896ccea3526b26d082f4461eeea467f10a4">
<file src="src/Settings/Settings.php">
<PropertyTypeCoercion>
<code><![CDATA[new UniqueList()]]></code>
<code><![CDATA[new UniqueList()]]></code>
<code><![CDATA[new UniqueList()]]></code>
<code><![CDATA[new UniqueList()]]></code>
<code><![CDATA[new UniqueList()]]></code>
<code><![CDATA[new UniqueList()]]></code>
</PropertyTypeCoercion>
</file>
</files>
34 changes: 31 additions & 3 deletions src/Controller/SearchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
use Meilisearch\Client;
use Setono\Doctrine\ORMTrait;
use Setono\SyliusMeilisearchPlugin\Config\IndexRegistryInterface;
use Setono\SyliusMeilisearchPlugin\Document\Attribute\Facet;
use Setono\SyliusMeilisearchPlugin\Document\Document;
use Setono\SyliusMeilisearchPlugin\Form\Type\SearchWidgetType;
use Setono\SyliusMeilisearchPlugin\Model\IndexableInterface;
use Setono\SyliusMeilisearchPlugin\Resolver\IndexName\IndexNameResolverInterface;
use Sylius\Component\Product\Repository\ProductOptionRepositoryInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -26,6 +29,7 @@ public function __construct(
private readonly IndexNameResolverInterface $indexNameResolver,
private readonly IndexRegistryInterface $indexRegistry,
private readonly Client $client,
private readonly ProductOptionRepositoryInterface $productOptionRepository,
/** @var list<string> $searchIndexes */
private readonly array $searchIndexes,
) {
Expand All @@ -34,12 +38,14 @@ public function __construct(

public function search(Request $request): Response
{
$indexNames = array_map(fn (string $searchIndex) => $this->indexNameResolver->resolve($this->indexRegistry->get($searchIndex)), $this->searchIndexes);
$indexes = array_map(fn (string $searchIndex) => $this->indexRegistry->get($searchIndex), $this->searchIndexes);

$items = [];

foreach ($indexNames as $indexName) {
$searchResult = $this->client->index($indexName)->search($request->query->getString('q'));
foreach ($indexes as $index) {
$searchResult = $this->client->index($this->indexNameResolver->resolve($index))->search($request->query->getString('q'), [

Check failure on line 46 in src/Controller/SearchController.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.1 | Deps: highest | SF~5.4.0)

MixedArgument

src/Controller/SearchController.php:46:101: MixedArgument: Argument 1 of Meilisearch\Endpoints\Indexes::search cannot be mixed, expecting null|string (see https://psalm.dev/030)

Check failure on line 46 in src/Controller/SearchController.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.1 | Deps: highest | SF~5.4.0)

UndefinedMethod

src/Controller/SearchController.php:46:118: UndefinedMethod: Method Symfony\Component\HttpFoundation\InputBag::getString does not exist (see https://psalm.dev/022)

Check failure on line 46 in src/Controller/SearchController.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.2 | Deps: lowest | SF~5.4.0)

MixedArgument

src/Controller/SearchController.php:46:101: MixedArgument: Argument 1 of Meilisearch\Endpoints\Indexes::search cannot be mixed, expecting null|string (see https://psalm.dev/030)

Check failure on line 46 in src/Controller/SearchController.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.2 | Deps: lowest | SF~5.4.0)

UndefinedMethod

src/Controller/SearchController.php:46:118: UndefinedMethod: Method Symfony\Component\HttpFoundation\InputBag::getString does not exist (see https://psalm.dev/022)

Check failure on line 46 in src/Controller/SearchController.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.2 | Deps: highest | SF~5.4.0)

MixedArgument

src/Controller/SearchController.php:46:101: MixedArgument: Argument 1 of Meilisearch\Endpoints\Indexes::search cannot be mixed, expecting null|string (see https://psalm.dev/030)

Check failure on line 46 in src/Controller/SearchController.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.2 | Deps: highest | SF~5.4.0)

UndefinedMethod

src/Controller/SearchController.php:46:118: UndefinedMethod: Method Symfony\Component\HttpFoundation\InputBag::getString does not exist (see https://psalm.dev/022)
'facets' => $this->getFacets($index->document),
]);

/** @var array{entityClass: class-string<IndexableInterface>, entityId: mixed} $hit */
foreach ($searchResult->getHits() as $hit) {
Expand All @@ -60,4 +66,26 @@ public function widget(FormFactoryInterface $formFactory): Response
'form' => $form->createView(),
]));
}

/**
* @param class-string<Document> $document
*
* @return list<string>
*/
private function getFacets(string $document): array
{
$facets = [];

$reflectionClass = new \ReflectionClass($document);
foreach ($reflectionClass->getProperties() as $reflectionProperty) {
foreach ($reflectionProperty->getAttributes() as $reflectionAttribute) {
$attribute = $reflectionAttribute->newInstance();
if ($attribute instanceof Facet) {
$facets[] = $reflectionProperty->getName();
}
}
}

return $facets;
}
}
47 changes: 43 additions & 4 deletions src/DataMapper/Product/OptionsDataMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,71 @@
namespace Setono\SyliusMeilisearchPlugin\DataMapper\Product;

use Setono\SyliusMeilisearchPlugin\DataMapper\DataMapperInterface;
use Setono\SyliusMeilisearchPlugin\Document\Attribute\MapProductOption;
use Setono\SyliusMeilisearchPlugin\Document\Document;
use Setono\SyliusMeilisearchPlugin\Document\Product as ProductDocument;
use Setono\SyliusMeilisearchPlugin\Model\IndexableInterface;
use Setono\SyliusMeilisearchPlugin\Provider\IndexScope\IndexScope;
use Sylius\Component\Core\Model\ProductInterface;
use Webmozart\Assert\Assert;

/**
* todo make this prettier
*/
final class OptionsDataMapper implements DataMapperInterface
{
public function map(IndexableInterface $source, Document $target, IndexScope $indexScope, array $context = []): void
{
Assert::true($this->supports($source, $target, $indexScope, $context));

/** @var array<string, list<string>> $options */
$options = [];

foreach ($source->getEnabledVariants() as $variant) {
foreach ($variant->getOptionValues() as $optionValue) {
$option = $optionValue->getOption()?->getCode();
$option = $optionValue->getOptionCode();
if ($option === null) {
continue;
}

$target->options[$option][] = (string) $optionValue->getValue();
$options[$option][] = (string) $optionValue->getValue();
}
}

foreach ($target->options as $option => $values) {
$target->options[$option] = array_values(array_unique($values));
foreach ($options as $option => $values) {
$options[$option] = array_values(array_unique($values));
}

$documentReflection = new \ReflectionClass($target);
foreach ($documentReflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $reflectionProperty) {
$propertyName = $reflectionProperty->getName();

foreach ($reflectionProperty->getAttributes() as $reflectionAttribute) {
$attribute = $reflectionAttribute->newInstance();

if (!$attribute instanceof MapProductOption) {
continue;
}

if (!isset($target->{$propertyName}) || !is_array($target->{$propertyName})) {
continue;
}

$values = [];

foreach ($attribute->codes as $code) {
if (!isset($options[$code])) {
continue;
}

$values[] = $options[$code];
}

$values = array_values(array_unique(array_merge(...$values)));

/** @psalm-suppress MixedArgument */
$target->{$propertyName} = array_merge($target->{$propertyName}, $values);
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/Document/Attribute/Facet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Document\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)]
final class Facet
{
}
2 changes: 1 addition & 1 deletion src/Document/Attribute/Filterable.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)]
final class Filterable
{
}
24 changes: 24 additions & 0 deletions src/Document/Attribute/MapProductOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Document\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
final class MapProductOption
{
/** @var list<string> */
public array $codes = [];

/**
* todo should be nullable to just use the property name as the code
*
* @param list<string>|string $codes
*/
public function __construct(array|string $codes)
{
$this->codes = is_string($codes) ? [$codes] : $codes;
}
}
2 changes: 1 addition & 1 deletion src/Document/Attribute/Searchable.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)]
final class Searchable
{
}
2 changes: 1 addition & 1 deletion src/Document/Attribute/Sortable.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)]
final class Sortable
{
}
13 changes: 8 additions & 5 deletions src/Document/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Setono\SyliusMeilisearchPlugin\Document;

use Setono\SyliusMeilisearchPlugin\Document\Attribute\Facet;
use Setono\SyliusMeilisearchPlugin\Document\Attribute\Filterable;
use Setono\SyliusMeilisearchPlugin\Document\Attribute\Searchable;
use Setono\SyliusMeilisearchPlugin\Document\Attribute\Sortable;
Expand Down Expand Up @@ -37,18 +38,19 @@ class Product extends Document implements UrlAwareInterface, ImageUrlsAwareInter

public ?string $currency = null;

#[Facet]
#[Filterable]
#[Sortable]
public ?float $price = null;

public ?float $originalPrice = null;

/** @var array<string, list<string>> */
public array $options = [];

/**
* This attribute will allow you to create a filter like 'Only show products on sale'
*/
public function onSale(): bool
#[Filterable]
#[Facet]
public function isOnSale(): bool
{
return null !== $this->originalPrice && null !== $this->price && $this->price < $this->originalPrice;
}
Expand All @@ -63,7 +65,8 @@ public function onSale(): bool
* ];
* }
*/
public function discount(): float
#[Sortable]
public function getDiscount(): float
{
if (null === $this->originalPrice || null === $this->price) {
return 0;
Expand Down
59 changes: 0 additions & 59 deletions src/Normalizer/ProductNormalizer.php

This file was deleted.

Loading

0 comments on commit 9b3e825

Please sign in to comment.