diff --git a/.unused.php b/.unused.php index c43f02f..a323743 100644 --- a/.unused.php +++ b/.unused.php @@ -20,9 +20,22 @@ * Optional params **/ 'skipPackages' => [ + 'phpmd/phpmd',// QA tools + 'insolita/unused-scanner',// QA tools + 'vimeo/psalm',// QA tools + 'friendsofphp/php-cs-fixer',// QA tools + 'rskuipers/php-assumptions',// QA tools + 'phan/phan',// QA tools + 'ergebnis/composer-normalize',// QA tools + 'enlightn/security-checker',// QA tools + 'jakub-onderka/php-parallel-lint',// QA tools + 'phpunit/phpunit',// Unit test + 'sebastian/phpcpd',// QA tools + + 'symfony/polyfill-mbstring',// Prevent Symfony Polyfill requiring PHP 8 ], 'excludeDirectories' => $excludeDirectories, 'scanFiles' => $scanFiles, 'extensions' => ['*.php'], - 'requireDev' => false + 'requireDev' => true ]; \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cb7e73..bfb286a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Facets +- Document prefix in mappings (JSON/XML/Annotation/Attribute) + ## [1.0.0] First version diff --git a/Makefile b/Makefile index 6baa168..63367b5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: analyze fix-code +.PHONY: analyze fix-code test analyze: | vendor $(COMPOSER) install --optimize-autoloader --no-suggest --prefer-dist diff --git a/README.md b/README.md index 3c8c800..4334f28 100644 --- a/README.md +++ b/README.md @@ -235,25 +235,27 @@ The **After** allow you to do more action with results. ### The `Before` group -_In **bold** paramaters that can be changed._ +_In **bold** parameters that can be changed._ Event name | `ObjectManager` method | Available parameters --- | --- | --- `AddingDocumentToSearchEvent` | `addObjectInSearch` and `addObject` | **`data`**, **`documentId`**, `instance` _(r/o)_ `AddingSuggestionEvent` | `addObjectInSuggestion` and `addObject` | **`group`**, **`suggestion`**, **`score`**, **`increment`**, **`payload`**, `instance` _(r/o)_ `CreatingIndexEvent` | `createIndex` | **`builder`**, `classname` _(r/o)_ +`GettingFacetsEvent` | `getFacets` | `classname` _(r/o)_, **`query`**, **`fields`** `GettingSuggestionsEvent` | `getSuggestions` | `classname` _(r/o)_, **`prefix`**, **`fuzzy`**, **`withScores`**, **`withPayloads`**, **`max`**, **`inGroup`** `RemovingDocumentEvent` | `removeObjectFromSearch` | `instance` _(r/o)_, **`documentId`** ### The `After` group -_In **bold** paramaters that can be changed._ +_In **bold** parameters that can be changed._ Event name | `ObjectManager` method | Available parameters --- | --- | --- `AddingDocumentToSearchEvent` | `addObjectInSearch` and `addObject` | `data` _(r/o)_, `documentId` _(r/o)_, `instance` _(r/o)_ `AddingSuggestionEvent` | `addObjectInSuggestion` and `addObject` | `group` _(r/o)_, `suggestion` _(r/o)_, `score` _(r/o)_, `increment` _(r/o)_, `payload` _(r/o)_, `instance` _(r/o)_ `CreatingIndexEvent` | `createIndex` | `succeed` _(r/o)_, `classname` _(r/o)_ +`GettingFacetsEvent` | `getFacets` | `classname` _(r/o)_, `query` _(r/o)_, `fields` _(r/o)_, **`facets`** `GettingSearchBuilderEvent` | `getSearchBuilder` | **`searchBuilder`**, `classname` _(r/o)_ `GettingSuggestionsEvent` | `getSuggestions` | `classname` _(r/o)_, `prefix` _(r/o)_, `fuzzy` _(r/o)_, `withScores` _(r/o)_, `withPayloads` _(r/o)_, `max` _(r/o)_, `inGroup` _(r/o)_, **`suggestions`** `RemovingDocumentEvent` | `removeObjectFromSearch` | `instance` _(r/o)_, `documentId` _(r/o)_, `succeed` _(r/o)_ diff --git a/composer.json b/composer.json index 7a9f357..9658567 100644 --- a/composer.json +++ b/composer.json @@ -18,15 +18,14 @@ ], "require": { "php": "^7.3 || ^8.0", - "macfja/redisearch": "^1.1", + "macfja/redisearch": "^1.3", "predis/predis": "^1.1", "psr/event-dispatcher": "^1.0", "sgh/comparable": "^1.1" }, "require-dev": { - "api-platform/core": "^2.5", "doctrine/annotations": "^1.11", - "doctrine/orm": "^2.8", + "enlightn/security-checker": "^1.5", "ergebnis/composer-normalize": "^2.11", "friendsofphp/php-cs-fixer": "^2.17", "insolita/unused-scanner": "^2.2", @@ -37,7 +36,6 @@ "phpunit/phpunit": "^9.5", "rskuipers/php-assumptions": "^0.8.0", "sebastian/phpcpd": "^6.0", - "sensiolabs/security-checker": "^6.0", "symfony/polyfill-mbstring": "< 1.22.0", "vimeo/psalm": "^4.3" }, diff --git a/src/Annotation/FieldAnnotation.php b/src/Annotation/FieldAnnotation.php index 38d41f5..b7a10f2 100644 --- a/src/Annotation/FieldAnnotation.php +++ b/src/Annotation/FieldAnnotation.php @@ -24,6 +24,9 @@ use Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation; use MacFJA\RediSearch\Index\Builder\Field; +/** + * @phan-suppress PhanDeprecatedInterface + */ interface FieldAnnotation extends NamedArgumentConstructorAnnotation { public function getField(string $name): Field; diff --git a/src/Annotation/Index.php b/src/Annotation/Index.php index 74e5b75..02251fc 100644 --- a/src/Annotation/Index.php +++ b/src/Annotation/Index.php @@ -26,19 +26,29 @@ /** * @Annotation * @Target({"CLASS"}) + * @phan-suppress PhanDeprecatedInterface */ class Index implements NamedArgumentConstructorAnnotation { /** @var string */ private $name; - public function __construct(string $name) + /** @var null|string */ + private $prefix; + + public function __construct(string $name, ?string $prefix = null) { $this->name = $name; + $this->prefix = $prefix; } public function getName(): string { return $this->name; } + + public function getPrefix(): ?string + { + return $this->prefix; + } } diff --git a/src/Annotation/Suggestion.php b/src/Annotation/Suggestion.php index 558eeff..3ac02c9 100644 --- a/src/Annotation/Suggestion.php +++ b/src/Annotation/Suggestion.php @@ -26,6 +26,7 @@ /** * @Annotation * @Target({"PROPERTY", "METHOD"}) + * @phan-suppress PhanDeprecatedInterface */ class Suggestion implements NamedArgumentConstructorAnnotation { @@ -48,9 +49,6 @@ class Suggestion implements NamedArgumentConstructorAnnotation /** @var bool */ private $increment; - /** - * Suggestion constructor. - */ public function __construct(string $group = 'suggestion', float $score = 1.0, string $type = self::TYPE_FULL, bool $increment = false, ?string $payload = null) { $this->group = $group; diff --git a/src/Annotation/TemplateAnnotationMapper.php b/src/Annotation/TemplateAnnotationMapper.php index 8b7ae2a..00fe361 100644 --- a/src/Annotation/TemplateAnnotationMapper.php +++ b/src/Annotation/TemplateAnnotationMapper.php @@ -75,6 +75,21 @@ public static function getRSIndexName(): string return self::getClass(); } + public static function getRSIndexDocumentPrefix(): ?string + { + $reflectionClass = new ReflectionClass(self::getClass()); + $reader = new AnnotationReader(); + $indexName = $reader->getClassAnnotation( + $reflectionClass, + Index::class + ); + if ($indexName instanceof Index) { + return $indexName->getPrefix(); + } + + return null; + } + public function getRSDataArray($instance): array { $classname = self::getClass(); diff --git a/src/Attribute/Index.php b/src/Attribute/Index.php index bcffd64..25787b1 100644 --- a/src/Attribute/Index.php +++ b/src/Attribute/Index.php @@ -30,13 +30,22 @@ class Index implements NamedArgumentConstructorAnnotation /** @var string */ private $name; - public function __construct(string $name) + /** @var null|string */ + private $prefix; + + public function __construct(string $name, ?string $prefix = null) { $this->name = $name; + $this->prefix = $prefix; } public function getName(): string { return $this->name; } + + public function getPrefix(): ?string + { + return $this->prefix; + } } diff --git a/src/Attribute/TemplateAttributeMapper.php b/src/Attribute/TemplateAttributeMapper.php index 0fcd8a6..c4a845b 100644 --- a/src/Attribute/TemplateAttributeMapper.php +++ b/src/Attribute/TemplateAttributeMapper.php @@ -74,6 +74,19 @@ public static function getRSIndexName(): string return self::getClass(); } + public static function getRSIndexDocumentPrefix(): ?string + { + $reflectionClass = new ReflectionClass(self::getClass()); + $indexName = $reflectionClass->getAttributes(Index::class); + if (1 === count($indexName)) { + $indexName = reset($indexName); + /** @var Index $indexName */ + return $indexName->getPrefix(); + } + + return null; + } + public function getRSDataArray($instance): array { $classname = self::getClass(); diff --git a/src/Event/After/GettingFacetsEvent.php b/src/Event/After/GettingFacetsEvent.php new file mode 100644 index 0000000..c993864 --- /dev/null +++ b/src/Event/After/GettingFacetsEvent.php @@ -0,0 +1,85 @@ + */ + private $fields; + + /** @var array> */ + private $facets; + + /** + * @param array $fields + * @param array> $facets + */ + public function __construct(string $classname, string $query, array $fields, array $facets) + { + $this->classname = $classname; + $this->query = $query; + $this->fields = $fields; + $this->facets = $facets; + } + + public function getClassname(): string + { + return $this->classname; + } + + public function getQuery(): string + { + return $this->query; + } + + /** + * @return array + */ + public function getFields(): array + { + return $this->fields; + } + + /** + * @return array> + */ + public function getFacets(): array + { + return $this->facets; + } + + /** + * @param array> $facets + */ + public function setFacets(array $facets): GettingFacetsEvent + { + $this->facets = $facets; + + return $this; + } +} diff --git a/src/Event/Before/GettingFacetsEvent.php b/src/Event/Before/GettingFacetsEvent.php new file mode 100644 index 0000000..ab498d7 --- /dev/null +++ b/src/Event/Before/GettingFacetsEvent.php @@ -0,0 +1,82 @@ + */ + private $fields; + + /** + * @param array $fields + */ + public function __construct(string $classname, string $query, array $fields) + { + $this->classname = $classname; + $this->query = $query; + $this->fields = $fields; + } + + public function getClassname(): string + { + return $this->classname; + } + + public function getQuery(): string + { + return $this->query; + } + + public function setQuery(string $query): GettingFacetsEvent + { + $this->query = $query; + + return $this; + } + + /** + * @return array + */ + public function getFields(): array + { + return $this->fields; + } + + /** + * @param array $fields + */ + public function setFields(array $fields): GettingFacetsEvent + { + DataHelper::assertArrayOf($fields, 'string'); + $this->fields = $fields; + + return $this; + } +} diff --git a/src/IndexObjectFactory.php b/src/IndexObjectFactory.php index 94c1335..a30bd97 100644 --- a/src/IndexObjectFactory.php +++ b/src/IndexObjectFactory.php @@ -21,6 +21,7 @@ namespace MacFJA\RediSearch\Integration; +use MacFJA\RediSearch\Aggregate; use MacFJA\RediSearch\Index; use MacFJA\RediSearch\Search; use MacFJA\RediSearch\Suggestions; @@ -40,4 +41,6 @@ public function getSuggestion(string $name): Suggestions; public function getRedisClient(): Client; public function getEventDispatcher(): EventDispatcherInterface; + + public function getAggregate(): Aggregate; } diff --git a/src/Json/TemplateJsonMapper.php b/src/Json/TemplateJsonMapper.php index c8754ec..55b6f1d 100644 --- a/src/Json/TemplateJsonMapper.php +++ b/src/Json/TemplateJsonMapper.php @@ -66,6 +66,13 @@ public static function getRSIndexName(): string return $data['index'] ?? $data['class']; } + public static function getRSIndexDocumentPrefix(): ?string + { + $data = self::getJsonData(); + + return $data['document-prefix'] ?? null; + } + public function getRSDataArray($instance): array { $data = self::getJsonData(); diff --git a/src/Json/schema.json b/src/Json/schema.json index 2ffc322..2c58630 100644 --- a/src/Json/schema.json +++ b/src/Json/schema.json @@ -113,6 +113,13 @@ "default": "", "pattern": "^.*$" }, + "document-prefix": { + "$id": "#root/items/document-prefix", + "title": "Index Document Prefix", + "type": "string", + "default": "", + "pattern": "^.*$" + }, "stop-words": { "$id": "#root/items/stop-words", "title": "Stop-words", diff --git a/src/MappedClass.php b/src/MappedClass.php index 5136fce..1bf5f28 100644 --- a/src/MappedClass.php +++ b/src/MappedClass.php @@ -32,6 +32,8 @@ public static function getRSSuggestionGroups(): array; public static function getRSIndexName(): string; + public static function getRSIndexDocumentPrefix(): ?string; + /** * @param object $instance * diff --git a/src/ObjectManager.php b/src/ObjectManager.php index 45be9ff..1d269a9 100644 --- a/src/ObjectManager.php +++ b/src/ObjectManager.php @@ -22,16 +22,28 @@ namespace MacFJA\RediSearch\Integration; use function array_filter; +use function array_key_exists; +use function array_reduce; +use function assert; use function count; +use function current; use function get_class; use function in_array; +use function is_array; +use function is_int; +use function is_numeric; use function is_string; +use function key; +use function reset; +use MacFJA\RediSearch\Aggregate\Reducer; +use MacFJA\RediSearch\Aggregate\Result as AggregateResult; use MacFJA\RediSearch\Helper\PaginatedResult; use MacFJA\RediSearch\Index\Builder; use MacFJA\RediSearch\Integration\Event\After; use MacFJA\RediSearch\Integration\Event\Before; use MacFJA\RediSearch\Integration\Exception\NotMappedException; use MacFJA\RediSearch\Integration\Exception\UnidentifiableDocumentException; +use MacFJA\RediSearch\Pipeline; use MacFJA\RediSearch\Search; use MacFJA\RediSearch\Suggestion\Result; use SGH\Comparable\SetFunctions; @@ -69,6 +81,7 @@ public function createIndex(string $className): bool } $indexName = $mapped::getRSIndexName(); + $documentPrefix = $mapped::getRSIndexDocumentPrefix(); $fields = $mapped::getRSFieldsDefinition(); $this->builder->reset(); @@ -76,6 +89,9 @@ public function createIndex(string $className): bool foreach ($fields as $field) { $this->builder->addField($field); } + if (is_string($documentPrefix)) { + $this->builder->withPrefix([$documentPrefix]); + } /** @var Before\CreatingIndexEvent $event */ $event = $this->eventDispatcher->dispatch(new Before\CreatingIndexEvent($this->builder, $className)); @@ -320,4 +336,82 @@ public static function getAllResults(Search $search): PaginatedResult return $search->withResultLimit($preflightResult->getTotalCount())->execute(); } + + /** + * @param array $fields + * + * @return array> + */ + public function getFacets(string $classname, string $query, array $fields): array + { + $mapped = $this->provider->getStaticMappedClass($classname); + + if (null === $mapped) { + throw new NotMappedException($classname); + } + + $indexName = $mapped::getRSIndexName(); + + /** @var Before\GettingFacetsEvent $event */ + $event = $this->eventDispatcher->dispatch(new Before\GettingFacetsEvent( + $classname, + $query, + $fields + )); + + $baseAggregate = $this->objectFactory->getAggregate()->withIndexName($indexName)->withQuery($event->getQuery()); + + $pipeline = new Pipeline($this->objectFactory->getRedisClient()); + + foreach ($event->getFields() as $field) { + $facetAggregate = clone $baseAggregate; + $pipeline->addPipeable( + $facetAggregate->addGroupBy([$field], [Reducer::count('count')]) + ); + } + + $pipelineResult = $pipeline->executePipeline(); + $facets = []; + /** @var PaginatedResult $result */ + foreach ($pipelineResult as $result) { + $items = $result->getItems(); + $facets = array_reduce($items, function ($carry, AggregateResult $item) { + $fields = $item->getFields(); + + $count = $fields['count']; + assert(is_numeric($count)); + $count = (int) $count; + unset($fields['count']); + + reset($fields); + $field = (string) key($fields); + $fieldValue = current($fields); + assert(is_string($fieldValue) || is_int($fieldValue)); + $fieldValue = (string) $fieldValue; + + if (!array_key_exists($field, $carry)) { + $carry[$field] = []; + } + if (!array_key_exists($fieldValue, $carry[$field])) { + $carry[$field][$fieldValue] = []; + } + + $carry[$field][$fieldValue] = $count; + + return $carry; + }, $facets); + } + assert(is_array($facets)); + + /** @var After\GettingFacetsEvent $afterEvent */ + $afterEvent = $this->eventDispatcher->dispatch(new After\GettingFacetsEvent( + $classname, + $event->getQuery(), + $event->getFields(), + // @phan-suppress-next-line PhanPartialTypeMismatchArgument + $facets + )); + + return $afterEvent->getFacets(); + } } diff --git a/src/Xml/TemplateXmlMapper.php b/src/Xml/TemplateXmlMapper.php index 88378cc..c6770e8 100644 --- a/src/Xml/TemplateXmlMapper.php +++ b/src/Xml/TemplateXmlMapper.php @@ -66,6 +66,17 @@ public static function getRSIndexName(): string return (string) ($data['indexname'] ?? $data['name']); } + public static function getRSIndexDocumentPrefix(): ?string + { + $data = self::getXmlData(); + + if (isset($data['documentprefix'])) { + return (string) $data['documentprefix']; + } + + return null; + } + public function getRSDataArray($instance): array { $data = self::getXmlData(); diff --git a/src/Xml/schema.xsd b/src/Xml/schema.xsd index dbf818d..d81a467 100644 --- a/src/Xml/schema.xsd +++ b/src/Xml/schema.xsd @@ -106,6 +106,7 @@ + diff --git a/tests/fixtures/annotation/WithAnnotation.php b/tests/fixtures/annotation/WithAnnotation.php index 255aede..94cfcc7 100644 --- a/tests/fixtures/annotation/WithAnnotation.php +++ b/tests/fixtures/annotation/WithAnnotation.php @@ -30,7 +30,7 @@ use MacFJA\RediSearch\Integration\Annotation\TextField; /** - * @Index(name="tests_annotation") + * @Index(name="tests_annotation", prefix="document-") */ class WithAnnotation { diff --git a/tests/fixtures/attribute/WithAttribute.php b/tests/fixtures/attribute/WithAttribute.php index 738f48c..5f48209 100644 --- a/tests/fixtures/attribute/WithAttribute.php +++ b/tests/fixtures/attribute/WithAttribute.php @@ -26,7 +26,7 @@ use MacFJA\RediSearch\Integration\Attribute\Suggestion; use MacFJA\RediSearch\Integration\Attribute\TextField; -#[Index(name: "tests_attribute")] +#[Index(name: "tests_attribute", prefix: "person-")] class WithAttribute { #[TextField(phonetic: "fr")] diff --git a/tests/fixtures/json/example.json b/tests/fixtures/json/example.json index d9ab690..fc88803 100644 --- a/tests/fixtures/json/example.json +++ b/tests/fixtures/json/example.json @@ -2,6 +2,7 @@ { "class": "MacFJA\\RediSearch\\Integration\\tests\\fixtures\\json\\Person", "index": "person", + "document-prefix": "document", "stop-words": ["the", "redis"], "id": { "name": "getId", "type": "getter" }, "fields": { diff --git a/tests/fixtures/withclass/Person.php b/tests/fixtures/withclass/Person.php index c803a60..520e2a6 100644 --- a/tests/fixtures/withclass/Person.php +++ b/tests/fixtures/withclass/Person.php @@ -66,11 +66,16 @@ public function getRSSuggestions($instance): array public function getRSDocumentId($instance): ?string { - return 'document_'.$this->id; + return self::getRSIndexDocumentPrefix().$this->id; } public static function getRSSuggestionGroups(): array { return ['name']; } + + public static function getRSIndexDocumentPrefix(): ?string + { + return 'document_'; + } } diff --git a/tests/fixtures/xml/example.xml b/tests/fixtures/xml/example.xml index 26da4f7..63c1cd3 100644 --- a/tests/fixtures/xml/example.xml +++ b/tests/fixtures/xml/example.xml @@ -1,6 +1,6 @@ - + id the diff --git a/tests/unit/AnnotationMapperTest.php b/tests/unit/AnnotationMapperTest.php index 621936b..d43dd2f 100644 --- a/tests/unit/AnnotationMapperTest.php +++ b/tests/unit/AnnotationMapperTest.php @@ -64,6 +64,7 @@ public function testNominalCase() self::assertSame($provider->getStaticMappedClass(WithAnnotation::class), get_class($mapped)); self::assertSame('tests_annotation', $mapped::getRSIndexName()); + self::assertSame('document-', $mapped::getRSIndexDocumentPrefix()); self::assertCount(5, $mapped::getRSFieldsDefinition()); self::assertEquals([ new Builder\TextField('firstname', false, null, 'fr'), diff --git a/tests/unit/AttributeMapperTest.php b/tests/unit/AttributeMapperTest.php index 4577dca..886b457 100644 --- a/tests/unit/AttributeMapperTest.php +++ b/tests/unit/AttributeMapperTest.php @@ -50,6 +50,7 @@ public function testNominalCase() self::assertSame($provider->getStaticMappedClass(WithAttribute::class), get_class($mapped)); self::assertSame('tests_annotation', $mapped::getRSIndexName()); + self::assertSame('person-', $mapped::getRSIndexDocumentPrefix()); self::assertCount(2, $mapped::getRSFieldsDefinition()); self::assertEquals([ new Builder\TextField('firstname', false, null, 'fr'), diff --git a/tests/unit/ClassMapperTest.php b/tests/unit/ClassMapperTest.php index c711d69..6417761 100644 --- a/tests/unit/ClassMapperTest.php +++ b/tests/unit/ClassMapperTest.php @@ -51,6 +51,7 @@ public function testNominalCase() self::assertSame($provider->getStaticMappedClass(Person::class), get_class($mapped)); self::assertSame('person', $mapped::getRSIndexName()); + self::assertSame('document_', $mapped::getRSIndexDocumentPrefix()); self::assertCount(3, $mapped::getRSFieldsDefinition()); self::assertEquals([ new Builder\TextField('firstname', true, null, 'fr'), diff --git a/tests/unit/JsonMapperTest.php b/tests/unit/JsonMapperTest.php index 1fbe1db..4a4ede4 100644 --- a/tests/unit/JsonMapperTest.php +++ b/tests/unit/JsonMapperTest.php @@ -59,6 +59,7 @@ public function testNominalCase() self::assertSame($provider->getStaticMappedClass(Person::class), get_class($mapped)); self::assertSame('person', $mapped::getRSIndexName()); + self::assertSame('document', $mapped::getRSIndexDocumentPrefix()); self::assertCount(4, $mapped::getRSFieldsDefinition()); self::assertEquals([ new Builder\TextField('firstname', false, null, 'fr', true), diff --git a/tests/unit/XmlMapperTest.php b/tests/unit/XmlMapperTest.php index f215de0..915e277 100644 --- a/tests/unit/XmlMapperTest.php +++ b/tests/unit/XmlMapperTest.php @@ -54,6 +54,7 @@ public function testCreateIndex() self::assertSame($provider->getStaticMappedClass(Person::class), get_class($mapped)); self::assertSame('person', $mapped::getRSIndexName()); + self::assertSame('document-id-', $mapped::getRSIndexDocumentPrefix()); self::assertCount(4, $mapped::getRSFieldsDefinition()); self::assertEquals([ new Builder\TextField('firstname', false, 1.0, 'fr', true),