diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index d51fcb5..c683cad 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -111,7 +111,7 @@ jobs: - name: 'Run phpunit tests' run: | - vendor/bin/simple-phpunit --coverage-clover=tests/App/build/clover.xml + vendor/bin/phpunit --coverage-clover=tests/App/build/clover.xml - name: Upload coverage results to Coveralls uses: coverallsapp/github-action@v2 diff --git a/.gitignore b/.gitignore index 35c81fc..6491e08 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,6 @@ .phpunit.result.cache .idea /.php-cs-fixer.cache -/.phpstan-cache +/.phpstan-cache/ /.rector-cache/ +/.phpunit.cache/ diff --git a/README.md b/README.md index 938dc6e..3c77273 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ This bundle is licensed under the MIT license. Please, see the complete license ``` composer install docker compose up --detach --wait -vendor/bin/simple-phpunit +vendor/bin/phpunit docker compose down --remove-orphans ``` diff --git a/composer.json b/composer.json index eb48c30..9b0232e 100644 --- a/composer.json +++ b/composer.json @@ -23,23 +23,22 @@ "symfony/http-kernel": "^5.4 || ^6.4", "symfony/event-dispatcher-contracts": "^3.5", - "doctrine/annotations": "^1.2", - "doctrine/cache": "^1.4", "elasticsearch/elasticsearch": "^8.0" }, "require-dev": { - "symfony/stopwatch": "^5.4 || ^6.4", - "symfony/phpunit-bridge": "^5.4 || ^6.4", - "symfony/browser-kit": "^5.4 || ^6.4", "symfony/dotenv": "^5.4 || ^6.4", - "doctrine/orm": "^2.6.3", - "monolog/monolog": "^2.0|^3.0", + "doctrine/orm": "^2.6.3", + "doctrine/annotations": "^1.2", "knplabs/knp-paginator-bundle": "^4.0 || ^5.0", - "friendsofphp/php-cs-fixer": "^3.34", + "monolog/monolog": "^2.0|^3.0", + + "phpunit/phpunit": "^10.5", "php-coveralls/php-coveralls": "^2.1", "jchook/phpunit-assert-throws": "^1.0", - "dms/phpunit-arraysubset-asserts": "^0.2.1", + "dms/phpunit-arraysubset-asserts": "^0.5.0", + + "friendsofphp/php-cs-fixer": "^3.34", "phpstan/phpstan": "^1.12", "phpstan/phpstan-symfony": "^1.4", "phpstan/phpstan-phpunit": "^1.4", @@ -48,7 +47,8 @@ "suggest": { "monolog/monolog": "Allows for client-level logging and tracing", "knplabs/knp-paginator-bundle": "Allows for search results to be paginated", - "doctrine/orm": "Allows for using Doctrine as source for rebuilding indices" + "doctrine/orm": "Allows for using Doctrine as source for rebuilding indices", + "doctrine/annotations": "Allows for using annotations to configure the bundle, which is now deprecated" }, "autoload": { "psr-4": { @@ -69,7 +69,7 @@ }, "scripts": { "run-tests": [ - "XDEBUG_MODE=coverage vendor/bin/simple-phpunit --coverage-text" + "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text" ], "check-code": [ "vendor/bin/phpstan", diff --git a/config/services.yml b/config/services.yml index 1e3a13d..19c0ee3 100644 --- a/config/services.yml +++ b/config/services.yml @@ -73,18 +73,19 @@ services: arguments: - '%sfes.entity_locations%' - Sineflow\ElasticsearchBundle\Mapping\DocumentParser: + Sineflow\ElasticsearchBundle\Mapping\DocumentAttributeParser: arguments: - - '@annotation_reader' - '@Sineflow\ElasticsearchBundle\Mapping\DocumentLocator' - '%sfes.mlproperty.language_separator%' - '%sfes.languages%' Sineflow\ElasticsearchBundle\Mapping\DocumentMetadataCollector: arguments: - - '%sfes.indices%' - - '@Sineflow\ElasticsearchBundle\Mapping\DocumentLocator' - - '@Sineflow\ElasticsearchBundle\Mapping\DocumentParser' + $indexManagers: '%sfes.indices%' + $documentLocator: '@Sineflow\ElasticsearchBundle\Mapping\DocumentLocator' + $annotationParser: '@?Sineflow\ElasticsearchBundle\Mapping\DocumentParser' + $attributeParser: '@Sineflow\ElasticsearchBundle\Mapping\DocumentAttributeParser' + $useAnnotations: '%sfes.use_annotations%' Sineflow\ElasticsearchBundle\Subscriber\KnpPaginateQuerySubscriber: arguments: ['@request_stack'] diff --git a/docs/configuration.md b/docs/configuration.md index 8f96ef9..1f71bf1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -7,6 +7,8 @@ sineflow_elasticsearch: languages: ['en', 'fr'] + use_annotations: false + metadata_cache_pool: sfes.metadata_cache_pool entity_locations: diff --git a/docs/crud.md b/docs/crud.md index aefb623..2cd8e1e 100644 --- a/docs/crud.md +++ b/docs/crud.md @@ -11,15 +11,14 @@ For all steps below we assume that there is an `App` entity location with the `P use Sineflow\ElasticsearchBundle\Annotation as ES; use Sineflow\ElasticsearchBundle\Document\AbstractDocument; -/** - * @ES\Document - */ +#[ES\Document] class Product extends AbstractDocument { - /** - * @ES\Property(type="text", name="title") - */ - public $title; + #[ES\Property( + name: 'title', + type: 'text', + )] + public ?string $title = null; } ``` diff --git a/docs/i18n.md b/docs/i18n.md index 404627e..7558712 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -16,21 +16,19 @@ sineflow_elasticsearch: * Next, you need to declare your multilanguage field as such in your annotation: ``` - /** - * @ES\Property( - * name="title", - * type="text", - * multilanguage=true, - * multilanguageDefaultOptions={ - * "type":"text", - * "index":false - * }, - * options={ - * "analyzer":"{lang}_analyzer", - * } - * ) - */ - public $title; +#[ES\Property( + name: 'title', + type: 'text', + multilanguage: true, + multilanguageDefaultOptions: [ + 'type' => 'text', + 'index' => false, + ], + options: [ + 'analyzer' => '{lang}_analyzer', + ], +)] +public ?MLProperty $title = null; ``` > Note the use of **{lang}** in the analyzer declaration. It is going to be replaced by a respective language code at runtime. diff --git a/docs/mapping-annotations.md b/docs/mapping-annotations.md new file mode 100644 index 0000000..7343035 --- /dev/null +++ b/docs/mapping-annotations.md @@ -0,0 +1,182 @@ +# Mapping + +The Elasticsearch bundle requires document mapping definitions to create the correct index schema and be able to convert data to objects and vice versa - think Doctrine. + +For this to work, you need `doctrine/annotations` installed in your project and `use_annotations` set to `true` in your configuration. + +## Document class annotations + +Elasticsearch index mappings are defined using annotations within document entity classes that implement DocumentInterface: +```php + Make sure your document classes directly implement DocumentInterface or extend AbstractDocument. + + +### Document annotation + +The class representing a document must be annotated as `@ES\Document`. The following properties are supported inside that annotation: + +- `repositoryClass` Allows you to specify a specific repository class for this document. If not specified, the default repository class is used. +``` +repositoryClass="App\Document\Repository\ProductRepository" +``` + +- `providerClass` Allows you to specify a specific data provider that will be used as data source when rebuilding the index. If not specified, the default self-provider is used, i.e the index is rebuilt from itself. +``` +providerClass="App\Document\Provider\ProductProvider" +``` + +- `options` Allows to specify any type option supported by Elasticsearch, such as [\_all](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-all-field.html), [dynamic_templates](https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-templates.html), [dynamic_date_formats](https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-field-mapping.html#date-detection), etc. + +### Property annotation + +Each field within the document is specified using the `@ES\Property` annotation. The following properties are supported inside that annotation: + +- `name` Specifies the name of the field (required). + +- `multilanguage` A flag that specifies whether the field will be multilanguage. For more information, see [declaring multilanguage properties](#mlproperties). +``` +multilanguage=true +``` + +- `objectName` When the field type is `object` or `nested`, this property must be specified, as it specifies which class defines the (nested) object. +``` +objectName="App:ObjAlias" +``` + +- `multiple` Relevant only for `object` and `nested` fields. It specifies whether the field contains a single object or multiple ones. +``` +multiple=true +``` + +- `options` An array of literal options, sent to Elasticsearch as they are. The only exception is with multilanguage properties, where further processing is applied. +``` +options={ + "analyzer":"my_special_analyzer", + "null_value":0 +} +``` + +### Multilanguage properties + +Sometimes, you may have a field that is available in more than one language. This is declared like this: + +``` + /** + * @ES\Property( + * name="name", + * type="text", + * multilanguage=true, + * multilanguageDefaultOptions={ + * "type":"text", + * "index":false + * }, + * options={ + * "analyzer":"{lang}_analyzer", + * } + * ) + */ + public $name; +``` +> Note the use of `{lang}` placeholder in the options. + +When you have a property definition like that, there will not be a field `name` in your index, but instead there will be `name-en`, `name-fr`, `name-de`, etc. where the suffixes are taken from the available languages in your application. +There will also be a field `name-default`, whose default mapping of `type:keyword;ignore_above:256` you can optionally override by specifying alternative `multilanguageDefaultOptions`. + +You may also use the special `{lang}` placeholder in the options array, as often you would need to specify different analyzers, depending on the language. For more information on how that works, see [multilanguage support](i18n.md). + +### Meta property annotations + +#### @ES\Id + +If you need to have access to the `_id` property of an Elasticsearch document you need to have a class property with this annotation. +This way, you can specify the `_id` when you create or update a document and you will also have that value populated in your object when you retrieve an existing document. + +```php +use Sineflow\ElasticsearchBundle\Annotation as ES; + +/** + * @ES\Document + */ +class Product +{ + /** + * @var string + * + * @ES\Id + */ + public $id; +} +``` +> Such property is already defined in `AbstractDocument`, so you can just extend it. + +#### @ES\Score + +You should have a property with this annotation, if you wish the matching `_score` of the document to be populated in it when searching. + +```php +use Sineflow\ElasticsearchBundle\Annotation as ES; + +/** + * @ES\Document + */ +class Product +{ + /** + * @var float + * + * @ES\Score + */ + public $score; +} +``` +> Such property is already defined in `AbstractDocument`, so you can just extend it. + +## DocObject class annotation + +Object classes are almost the same as document classes: + +```php + **Mapping with annotations is still supported, but is deprecated and will be removed in the future. +To see how it works, check [mapping-annotations.md](mapping-annotations.md).** + The Elasticsearch bundle requires document mapping definitions to create the correct index schema and be able to convert data to objects and vice versa - think Doctrine. -## Document class annotations +## Document class attributes -Elasticsearch index mappings are defined using annotations within document entity classes that implement DocumentInterface: +Elasticsearch index mappings are defined using attributes within document entity classes that implement DocumentInterface: ```php Make sure your document classes directly implement DocumentInterface or extend AbstractDocument. +> Make sure your document classes directly implement `DocumentInterface` or extend `AbstractDocument`. -### Document annotation +### Document attribute -The class representing a document must be annotated as `@ES\Document`. The following properties are supported inside that annotation: +The class representing a document must be annotated as `@ES\Document`. The following properties are supported inside that attribute: - `repositoryClass` Allows you to specify a specific repository class for this document. If not specified, the default repository class is used. ``` -repositoryClass="App\Document\Repository\ProductRepository" +repositoryClass: App\Document\Repository\ProductRepository ``` - `providerClass` Allows you to specify a specific data provider that will be used as data source when rebuilding the index. If not specified, the default self-provider is used, i.e the index is rebuilt from itself. ``` -repositoryClass="App\Document\Provider\ProductProvider" +providerClass: App\Document\Provider\ProductProvider ``` - `options` Allows to specify any type option supported by Elasticsearch, such as [\_all](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-all-field.html), [dynamic_templates](https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-templates.html), [dynamic_date_formats](https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-field-mapping.html#date-detection), etc. -### Property annotation +### Property attribute -Each field within the document is specified using the `@ES\Property` annotation. The following properties are supported inside that annotation: +Each field within the document is specified using the `@ES\Property` attribute. The following properties are supported inside that attribute: - `name` Specifies the name of the field (required). - `multilanguage` A flag that specifies whether the field will be multilanguage. For more information, see [declaring multilanguage properties](#mlproperties). ``` -multilanguage=true +multilanguage: true ``` - `objectName` When the field type is `object` or `nested`, this property must be specified, as it specifies which class defines the (nested) object. ``` -objectName="App:ObjAlias" +objectName: App\Document\ObjAlias ``` - `multiple` Relevant only for `object` and `nested` fields. It specifies whether the field contains a single object or multiple ones. ``` -multiple=true +multiple: true ``` - `options` An array of literal options, sent to Elasticsearch as they are. The only exception is with multilanguage properties, where further processing is applied. ``` -options={ - "analyzer":"my_special_analyzer", - "null_value":0 -} +options: [ + 'analyzer': 'my_special_analyzer', + 'fields' => [ + 'raw' => ['type' => 'keyword'], + 'title' => ['type' => 'text'], + ], +], ``` ### Multilanguage properties @@ -79,21 +83,19 @@ options={ Sometimes, you may have a field that is available in more than one language. This is declared like this: ``` - /** - * @ES\Property( - * name="name", - * type="text", - * multilanguage=true, - * multilanguageDefaultOptions={ - * "type":"text", - * "index":false - * }, - * options={ - * "analyzer":"{lang}_analyzer", - * } - * ) - */ - public $name; +#[ES\Property( + name: 'name', + type: 'text', + multilanguage: true, + multilanguageDefaultOptions: [ + 'type' => 'text', + 'index' => false, + ], + options: [ + 'analyzer' => '{lang}_analyzer', + ], +)] +public ?MLProperty $name = null; ``` > Note the use of `{lang}` placeholder in the options. @@ -102,54 +104,42 @@ There will also be a field `name-default`, whose default mapping of `type:keywor You may also use the special `{lang}` placeholder in the options array, as often you would need to specify different analyzers, depending on the language. For more information on how that works, see [multilanguage support](i18n.md). -### Meta property annotations +### Meta property attributes -#### @ES\Id +#### Id -If you need to have access to the `_id` property of an Elasticsearch document you need to have a class property with this annotation. +If you need to have access to the `_id` property of an Elasticsearch document you need to have a class property with this attribute. This way, you can specify the `_id` when you create or update a document and you will also have that value populated in your object when you retrieve an existing document. ```php -use Sineflow\ElasticsearchBundle\Annotation as ES; +use Sineflow\ElasticsearchBundle\attribute as ES; -/** - * @ES\Document - */ +#[ES\Document] class Product { - /** - * @var string - * - * @ES\Id - */ - public $id; + #[ES\Id] + public ?string $id = null; } ``` > Such property is already defined in `AbstractDocument`, so you can just extend it. -#### @ES\Score +#### Score -You should have a property with this annotation, if you wish the matching `_score` of the document to be populated in it when searching. +You should have a property with this attribute, if you wish the matching `_score` of the document to be populated in it when searching. ```php -use Sineflow\ElasticsearchBundle\Annotation as ES; +use Sineflow\ElasticsearchBundle\Attribute as ES; -/** - * @ES\Document - */ +#[ES\Document] class Product { - /** - * @var float - * - * @ES\Score - */ - public $score; + #[ES\Score] + public ?float $score = null; } ``` > Such property is already defined in `AbstractDocument`, so you can just extend it. -## DocObject class annotation +## DocObject class attribute Object classes are almost the same as document classes: @@ -158,23 +148,20 @@ Object classes are almost the same as document classes: namespace App\Document; use Sineflow\ElasticsearchBundle\Document\ObjectInterface; -use Sineflow\ElasticsearchBundle\Annotation as ES; +use Sineflow\ElasticsearchBundle\Attribute as ES; -/** - * @ES\DocObject - */ +#[ES\DocObject] class ObjAlias implements ObjectInterface { - /** - * @var string - * - * @ES\Property(name="title", type="text") - */ - public $title; + #[ES\Property( + name: 'title', + type: 'text', + )] + public ?string $title = null; } ``` -The difference with document classes is that the class must implement `ObjectInterface` and be annotated as `@ES\DocObject`. The mapping of the object properties follows the same rules as the one for the document properties. +The difference with document classes is that the class must implement `ObjectInterface` and have a `DocObject` attribute. The mapping of the object properties follows the same rules as the one for the document properties. More info about mapping is in the [elasticsearch mapping documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html) diff --git a/docs/setup.md b/docs/setup.md index a7337cb..ec3f818 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -65,20 +65,17 @@ The bundle uses `Document` objects to represent Elasticsearch documents. Now let + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnTestsThatTriggerErrors="true" + displayDetailsOnTestsThatTriggerNotices="true" + displayDetailsOnTestsThatTriggerWarnings="true" + bootstrap="tests/tests.bootstrap.php" + cacheDirectory=".phpunit.cache"> @@ -19,20 +17,15 @@ ./tests/Functional/ - - ./tests/ - - - - + ./ @@ -41,6 +34,9 @@ ./vendor ./var + + + diff --git a/rector.php b/rector.php index 3402a2a..4e2914e 100644 --- a/rector.php +++ b/rector.php @@ -5,6 +5,7 @@ use Rector\Caching\ValueObject\Storage\FileCacheStorage; use Rector\Config\RectorConfig; use Rector\Doctrine\Set\DoctrineSetList; +use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Symfony\Set\SymfonySetList; return RectorConfig::configure() @@ -26,6 +27,8 @@ ->withSets([ SymfonySetList::SYMFONY_CODE_QUALITY, DoctrineSetList::DOCTRINE_CODE_QUALITY, + PHPUnitSetList::PHPUNIT_CODE_QUALITY, + PHPUnitSetList::PHPUNIT_100, ]) ->withPhpSets() diff --git a/src/Attribute/DocObject.php b/src/Attribute/DocObject.php new file mode 100644 index 0000000..1423a89 --- /dev/null +++ b/src/Attribute/DocObject.php @@ -0,0 +1,8 @@ +options; + } +} diff --git a/src/Attribute/Id.php b/src/Attribute/Id.php new file mode 100644 index 0000000..bb9e09b --- /dev/null +++ b/src/Attribute/Id.php @@ -0,0 +1,23 @@ +name; + } + + public function getType(): ?string + { + return $this->type; + } + + /** + * Dumps property fields as an array for index mapping + */ + public function dump(array $settings = []): array + { + $result = $this->options; + + // Although it is completely valid syntax to explicitly define objects as such in the mapping definition, ES does not do that by default. + // So, in order to ensure that the mapping for index creation would exactly match the mapping returned from the ES _mapping endpoint, we don't explicitly set 'object' data types + if ('object' !== $this->type) { + $result = \array_merge($result, ['type' => $this->type]); + } + + if (isset($settings['language'])) { + if (!isset($settings['indexAnalyzers'])) { + throw new \InvalidArgumentException('Available index analyzers missing'); + } + + // Recursively replace {lang} in any string option with the respective language + \array_walk_recursive($result, static function (&$value, $key, $settings): void { + if (\is_string($value) && \str_contains($value, self::LANGUAGE_PLACEHOLDER)) { + if (\in_array($key, ['analyzer', 'index_analyzer', 'search_analyzer'])) { + // Replace {lang} in any analyzers with the respective language + // If no analyzer is defined for a certain language, replace {lang} with 'default' + + // Get the names of all available analyzers in the index + $indexAnalyzers = \array_keys($settings['indexAnalyzers']); + + // Make sure a default analyzer is defined, even if we don't need it right now + // because, if a new language is added and we don't have an analyzer for it, ES mapping would fail + $defaultAnalyzer = \str_replace(self::LANGUAGE_PLACEHOLDER, self::DEFAULT_LANG_SUFFIX, $value); + if (!\in_array($defaultAnalyzer, $indexAnalyzers)) { + throw new \LogicException(\sprintf('There must be a default language analyzer "%s" defined for index', $defaultAnalyzer)); + } + + $value = \str_replace(self::LANGUAGE_PLACEHOLDER, $settings['language'], $value); + if (!\in_array($value, $indexAnalyzers)) { + $value = $defaultAnalyzer; + } + } else { + // If it's any other option, just replace with the respective language + $value = \str_replace(self::LANGUAGE_PLACEHOLDER, $settings['language'], $value); + } + } + }, $settings); + } + + return $result; + } +} diff --git a/src/Attribute/PropertyAttributeInterface.php b/src/Attribute/PropertyAttributeInterface.php new file mode 100644 index 0000000..5e7571e --- /dev/null +++ b/src/Attribute/PropertyAttributeInterface.php @@ -0,0 +1,10 @@ +end() ->end() ->scalarNode('metadata_cache_pool')->end() + ->booleanNode('use_annotations') + ->info('Read mapping info from annotations instead of attributes. Used for backward compatibility') + ->defaultFalse() + ->end() ->append($this->getEntityLocationsNode()) ->append($this->getConnectionsNode()) ->append($this->getIndicesNode()) diff --git a/src/DependencyInjection/SineflowElasticsearchExtension.php b/src/DependencyInjection/SineflowElasticsearchExtension.php index a0c083a..aa453f2 100644 --- a/src/DependencyInjection/SineflowElasticsearchExtension.php +++ b/src/DependencyInjection/SineflowElasticsearchExtension.php @@ -2,11 +2,15 @@ namespace Sineflow\ElasticsearchBundle\DependencyInjection; +use Doctrine\Common\Annotations\AnnotationReader; use Sineflow\ElasticsearchBundle\Document\Provider\ProviderInterface; use Sineflow\ElasticsearchBundle\Document\Repository\ServiceRepositoryInterface; +use Sineflow\ElasticsearchBundle\Mapping\DocumentLocator; +use Sineflow\ElasticsearchBundle\Mapping\DocumentParser; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; /** @@ -27,6 +31,21 @@ public function load(array $configs, ContainerBuilder $container): void $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); + // Conditionally register the annotation_reader service, only if doctrine/annotations is installed. + // This will not be needed when support for annotations is removed. + if (class_exists(AnnotationReader::class)) { + $container->register('annotation_reader', AnnotationReader::class) + ->setPublic(true); + + $container->register(DocumentParser::class, DocumentParser::class) + ->setArguments([ + new Reference('annotation_reader'), + new Reference(DocumentLocator::class), + '%sfes.mlproperty.language_separator%', + '%sfes.languages%', + ]); + } + $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../../config')); $loader->load('services.yml'); @@ -35,6 +54,7 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('sfes.indices', $config['indices']); $container->setParameter('sfes.languages', $config['languages']); $container->setParameter('sfes.cache_pool', $config['metadata_cache_pool'] ?? null); + $container->setParameter('sfes.use_annotations', $config['use_annotations']); $container ->registerForAutoconfiguration(ServiceRepositoryInterface::class) diff --git a/src/Document/AbstractDocument.php b/src/Document/AbstractDocument.php index fc8ad44..181acf7 100644 --- a/src/Document/AbstractDocument.php +++ b/src/Document/AbstractDocument.php @@ -3,6 +3,7 @@ namespace Sineflow\ElasticsearchBundle\Document; use Sineflow\ElasticsearchBundle\Annotation as ES; +use Sineflow\ElasticsearchBundle\Attribute as SFES; /** * Document abstraction which introduces mandatory fields for the document. @@ -12,10 +13,12 @@ abstract class AbstractDocument implements DocumentInterface /** * @ES\Id */ + #[SFES\Id] public string|int|null $id = null; /** * @ES\Score */ + #[SFES\Score] public ?float $score = null; } diff --git a/src/Exception/InvalidMappingException.php b/src/Exception/InvalidMappingException.php new file mode 100644 index 0000000..88c91b1 --- /dev/null +++ b/src/Exception/InvalidMappingException.php @@ -0,0 +1,7 @@ +getAttributes(Document::class); + + if (empty($documentAttributes)) { + throw new InvalidMappingException(sprintf('Class "%s" must have the "%s" attribute in order to be used as a Document', $documentReflection->getName(), Document::class)); + } + + /** @var Document $documentAttribute */ + $documentAttribute = $documentAttributes[0]->newInstance(); + + $properties = $this->getProperties($documentReflection, $indexAnalyzers); + + return [ + 'properties' => $properties, + 'fields' => \array_filter($documentAttribute->dump()), + 'propertiesMetadata' => $this->getPropertiesMetadata($documentReflection), + 'repositoryClass' => $documentAttribute->repositoryClass, + 'providerClass' => $documentAttribute->providerClass, + 'className' => $documentReflection->getName(), + ]; + } + + /** + * Finds properties' metadata for every property used in document or inner/nested object + * + * @throws InvalidMappingException + * @throws \ReflectionException + * @throws \LogicException + */ + public function getPropertiesMetadata(\ReflectionClass $documentReflection): array + { + $className = $documentReflection->getName(); + if (\array_key_exists($className, $this->propertiesMetadata)) { + return $this->propertiesMetadata[$className]; + } + + $propertyMetadata = []; + + /** @var \ReflectionProperty $propertyReflection */ + foreach ($this->getDocumentPropertiesReflection($documentReflection) as $propertyName => $propertyReflection) { + $propertyAttributes = $propertyReflection->getAttributes(Property::class); + $propertyAttributes = $propertyAttributes ?: $propertyReflection->getAttributes(Id::class); + $propertyAttributes = $propertyAttributes ?: $propertyReflection->getAttributes(Score::class); + + // Ignore class properties without any recognized attribute + if (empty($propertyAttributes)) { + continue; + } + + /** @var PropertyAttributeInterface $propertyAttribute */ + $propertyAttribute = $propertyAttributes[0]->newInstance(); + + switch ($propertyAttribute::class) { + case Property::class: + $propertyMetadata[$propertyAttribute->name] = [ + 'propertyName' => $propertyName, + 'type' => $propertyAttribute->type, + ]; + if ($propertyAttribute->multilanguage) { + $propertyMetadata[$propertyAttribute->name]['multilanguage'] = true; + } + + // If property is a (nested) object + if (\in_array($propertyAttribute->type, ['object', 'nested'])) { + if (!$propertyAttribute->objectName) { + throw new InvalidMappingException(sprintf('Property "%s" in %s is missing "objectName" setting', $propertyName, $className)); + } + $child = new \ReflectionClass($this->documentLocator->resolveClassName($propertyAttribute->objectName)); + $propertyMetadata[$propertyAttribute->name] = \array_merge( + $propertyMetadata[$propertyAttribute->name], + [ + 'multiple' => $propertyAttribute->multiple, + 'propertiesMetadata' => $this->getPropertiesMetadata($child), + 'className' => $child->getName(), + ], + ); + } else { + if (null !== $propertyAttribute->enumType) { + if (!enum_exists($propertyAttribute->enumType)) { + throw new InvalidMappingException(sprintf('Enum "%s" for property "%s" in %s does not exist', $propertyAttribute->enumType, $propertyName, $className)); + } + $propertyMetadata[$propertyAttribute->name]['enumType'] = $propertyAttribute->enumType; + } + } + break; + + case Score::class: + case Id::class: + $propertyMetadata[$propertyAttribute->getName()] = [ + 'propertyName' => $propertyName, + 'type' => $propertyAttribute->getType(), + ]; + break; + } + + if ($propertyReflection->isPublic()) { + $propertyAccess = DocumentMetadata::PROPERTY_ACCESS_PUBLIC; + } else { + $propertyAccess = DocumentMetadata::PROPERTY_ACCESS_PRIVATE; + $camelCaseName = \ucfirst(Caser::camel($propertyName)); + $setterMethod = 'set'.$camelCaseName; + $getterMethod = 'get'.$camelCaseName; + // Allow issers as getters for boolean properties + if ('boolean' === $propertyAttribute->getType() && !$documentReflection->hasMethod($getterMethod)) { + $getterMethod = 'is'.$camelCaseName; + } + if ($documentReflection->hasMethod($getterMethod) && $documentReflection->hasMethod($setterMethod)) { + $propertyMetadata[$propertyAttribute->getName()]['methods'] = [ + 'getter' => $getterMethod, + 'setter' => $setterMethod, + ]; + } else { + $message = sprintf('Property "%s" either needs to be public or %s() and %s() methods must be defined', $propertyName, $getterMethod, $setterMethod); + throw new \LogicException($message); + } + } + + $propertyMetadata[$propertyAttribute->getName()]['propertyAccess'] = $propertyAccess; + } + + $this->propertiesMetadata[$className] = $propertyMetadata; + + return $this->propertiesMetadata[$className]; + } + + /** + * Returns all defined properties, including the ones from parents. + */ + private function getDocumentPropertiesReflection(\ReflectionClass $documentReflection): array + { + if (\in_array($documentReflection->getName(), $this->properties)) { + return $this->properties[$documentReflection->getName()]; + } + + $properties = []; + + foreach ($documentReflection->getProperties() as $property) { + if (!\in_array($property->getName(), $properties)) { + $properties[$property->getName()] = $property; + } + } + + $parentReflection = $documentReflection->getParentClass(); + if (false !== $parentReflection) { + $properties = \array_merge( + $properties, + \array_diff_key($this->getDocumentPropertiesReflection($parentReflection), $properties), + ); + } + + $this->properties[$documentReflection->getName()] = $properties; + + return $properties; + } + + /** + * Returns properties of reflection class. + * + * @param \ReflectionClass $documentReflection class to read properties from + * + * @throws \ReflectionException + * @throws InvalidMappingException + */ + private function getProperties(\ReflectionClass $documentReflection, array $indexAnalyzers = []): array + { + $mapping = []; + /** @var \ReflectionProperty $propertyReflection */ + foreach ($this->getDocumentPropertiesReflection($documentReflection) as $propertyReflection) { + $propertyAttributes = $propertyReflection->getAttributes(Property::class); + if (empty($propertyAttributes)) { + continue; + } + + /** @var Property $propertyAttribute */ + $propertyAttribute = $propertyAttributes[0]->newInstance(); + + // If it is a multi-language property + if (true === $propertyAttribute->multilanguage) { + if (!\in_array($propertyAttribute->getType(), ['string', 'keyword', 'text'])) { + throw new InvalidMappingException(sprintf('"%s" property in %s is declared as multilanguage, so can only be of type "keyword", "text" or the deprecated "string"', $propertyAttribute->getName(), $documentReflection->getName())); + } + if (!$this->languages) { + throw new InvalidMappingException('There must be at least one language specified in sineflow_elasticsearch.languages in order to use multilanguage properties'); + } + foreach ($this->languages as $language) { + $mapping[$propertyAttribute->getName().$this->languageSeparator.$language] = $this->getPropertyMapping($propertyAttribute, $language, $indexAnalyzers); + } + // TODO: The application should decide whether it wants to use a default field at all and set its mapping on a global base + // The custom mapping from the application should be set here, using perhaps some kind of decorator + $mapping[$propertyAttribute->getName().$this->languageSeparator.Property::DEFAULT_LANG_SUFFIX] = $propertyAttribute->multilanguageDefaultOptions ?: [ + 'type' => 'keyword', + 'ignore_above' => 256, + ]; + } else { + $mapping[$propertyAttribute->getName()] = $this->getPropertyMapping($propertyAttribute, null, $indexAnalyzers); + } + } + + return $mapping; + } + + /** + * @throws \ReflectionException + */ + private function getPropertyMapping(Property $propertyAttribute, ?string $language = null, array $indexAnalyzers = []): array + { + $propertyMapping = $propertyAttribute->dump([ + 'language' => $language, + 'indexAnalyzers' => $indexAnalyzers, + ]); + + // Inner/nested object + if (\in_array($propertyAttribute->type, ['object', 'nested']) && !empty($propertyAttribute->objectName)) { + $propertyMapping = \array_replace_recursive($propertyMapping, $this->getObjectMapping($propertyAttribute->objectName, $indexAnalyzers)); + } + + return $propertyMapping; + } + + /** + * Returns object mapping. + * + * @throws \ReflectionException + */ + private function getObjectMapping(string $objectName, array $indexAnalyzers = []): array + { + $className = $this->documentLocator->resolveClassName($objectName); + + if (\array_key_exists($className, $this->objects)) { + return $this->objects[$className]; + } + + $this->objects[$className] = $this->getRelationMapping(new \ReflectionClass($className), $indexAnalyzers); + + return $this->objects[$className]; + } + + /** + * Returns relation mapping by its reflection. + * + * @throws \ReflectionException + * @throws InvalidMappingException + */ + private function getRelationMapping(\ReflectionClass $objectReflection, array $indexAnalyzers = []): array + { + $docObjectAttributes = $objectReflection->getAttributes(DocObject::class); + if (empty($docObjectAttributes)) { + throw new InvalidMappingException(sprintf('Class "%s" must have the "%s" attribute in order to be used as a nested object inside a Document', $objectReflection->getName(), DocObject::class)); + } + + return ['properties' => $this->getProperties($objectReflection, $indexAnalyzers)]; + } +} diff --git a/src/Mapping/DocumentMetadataCollector.php b/src/Mapping/DocumentMetadataCollector.php index 7d046c2..80cf7d4 100644 --- a/src/Mapping/DocumentMetadataCollector.php +++ b/src/Mapping/DocumentMetadataCollector.php @@ -27,17 +27,32 @@ class DocumentMetadataCollector implements WarmableInterface */ private array $documentClassToIndexManagerNames = []; + private readonly DocumentParser|DocumentAttributeParser $documentParser; + /** - * @param array $indexManagers The list of index managers defined - * @param DocumentParser $parser For reading entity annotations - * @param CacheInterface $cache For caching entity metadata + * @param array $indexManagers The list of index managers defined + * @param DocumentParser|null $annotationParser For reading entity annotations + * @param DocumentAttributeParser $attributeParser For reading entity attributes + * @param CacheInterface $cache For caching entity metadata + * @param bool $useAnnotations Whether to use the attribute parser or the annotation parser */ public function __construct( - private array $indexManagers, + private readonly array $indexManagers, private readonly DocumentLocator $documentLocator, - private readonly DocumentParser $parser, + private readonly ?DocumentParser $annotationParser, + private readonly DocumentAttributeParser $attributeParser, private readonly CacheInterface $cache, + private readonly bool $useAnnotations = false, ) { + if ($this->useAnnotations) { + if (null === $this->annotationParser) { + throw new \LogicException('Annotations are enabled (use_annotations: true), but the "doctrine/annotations" package is not available.'); + } + $this->documentParser = $this->annotationParser; + } else { + $this->documentParser = $this->attributeParser; + } + // Build an internal array with map of document class to index manager name foreach ($this->indexManagers as $indexManagerName => $indexSettings) { $documentClass = $this->documentLocator->resolveClassName($indexSettings['class']); @@ -101,7 +116,7 @@ public function getObjectPropertiesMetadata(string $objectClass): array $cacheKey = self::OBJECTS_CACHE_KEY_PREFIX.\strtr($objectClass, '\\', '.'); - return $this->cache->get($cacheKey, fn (ItemInterface $item): array => $this->parser->getPropertiesMetadata(new \ReflectionClass($objectClass)), 0); + return $this->cache->get($cacheKey, fn (ItemInterface $item): array => $this->documentParser->getPropertiesMetadata(new \ReflectionClass($objectClass)), 0); } /** @@ -130,7 +145,8 @@ private function fetchDocumentMetadata(string $documentClass): DocumentMetadata $documentClass = $this->documentLocator->resolveClassName($documentClass); $indexManagerName = $this->getDocumentClassIndex($documentClass); $indexAnalyzers = $this->indexManagers[$indexManagerName]['settings']['analysis']['analyzer'] ?? []; - $documentMetadataArray = $this->parser->parse(new \ReflectionClass($documentClass), $indexAnalyzers); + + $documentMetadataArray = $this->documentParser->parse(new \ReflectionClass($documentClass), $indexAnalyzers); return new DocumentMetadata($documentMetadataArray); } diff --git a/src/Mapping/DocumentParser.php b/src/Mapping/DocumentParser.php index 0e68d5a..d1ecbcd 100644 --- a/src/Mapping/DocumentParser.php +++ b/src/Mapping/DocumentParser.php @@ -13,6 +13,8 @@ /** * Document parser used for reading document annotations. + * + * @deprecated Use DocumentAttributeParser instead. */ class DocumentParser { diff --git a/tests/AbstractContainerAwareTestCase.php b/tests/AbstractContainerAwareTestCase.php index d79bd98..2d5a949 100644 --- a/tests/AbstractContainerAwareTestCase.php +++ b/tests/AbstractContainerAwareTestCase.php @@ -1,50 +1,9 @@ cachedContainer = null; - } - - /** - * Returns service container. - * - * @param array $kernelOptions Options used passed to kernel if it needs to be initialized. - */ - protected function getContainer(array $kernelOptions = []): ContainerInterface - { - if (!$this->cachedContainer) { - static::bootKernel($kernelOptions); - // gets the special container that allows fetching private services - $this->cachedContainer = static::$container; - } - - return $this->cachedContainer; - } } diff --git a/tests/App/config/config_test.yml b/tests/App/config/config_test.yml index d1d3c2f..8aaee26 100644 --- a/tests/App/config/config_test.yml +++ b/tests/App/config/config_test.yml @@ -14,6 +14,8 @@ framework: sineflow_elasticsearch: + use_annotations: false + languages: ['en', 'fr'] entity_locations: diff --git a/tests/App/fixture/Acme/BarBundle/Document/ObjCategory.php b/tests/App/fixture/Acme/BarBundle/Document/ObjCategory.php index 9d60557..0df3644 100644 --- a/tests/App/fixture/Acme/BarBundle/Document/ObjCategory.php +++ b/tests/App/fixture/Acme/BarBundle/Document/ObjCategory.php @@ -3,38 +3,51 @@ namespace Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\BarBundle\Document; use Sineflow\ElasticsearchBundle\Annotation as ES; +use Sineflow\ElasticsearchBundle\Attribute as SFES; use Sineflow\ElasticsearchBundle\Document\ObjectInterface; +use Sineflow\ElasticsearchBundle\Result\ObjectIterator; /** * Category document for testing. * * @ES\DocObject */ +#[SFES\DocObject] class ObjCategory implements ObjectInterface { /** - * @var string Field without ESB annotation, should not be indexed. + * @var string Field without a SFES attribute or ES annotation - should not be indexed. */ - public $withoutAnnotation; + public string $withoutAnnotation; /** - * @var int - * * @ES\Property(type="integer", name="id") */ - public $id; + #[SFES\Property( + name: 'id', + type: 'integer', + )] + public ?int $id = null; /** - * @var string - * * @ES\Property(type="keyword", name="title") */ - public $title; + #[SFES\Property( + name: 'title', + type: 'keyword', + )] + public ?string $title = null; /** - * @var ObjTag[] + * @var ObjTag[]|ObjectIterator * * @ES\Property(type="object", name="tags", multiple=true, objectName="AcmeBarBundle:ObjTag") */ - public $tags; + #[SFES\Property( + name: 'tags', + type: 'object', + objectName: ObjTag::class, + multiple: true, + )] + public ObjectIterator|array $tags = []; } diff --git a/tests/App/fixture/Acme/BarBundle/Document/ObjTag.php b/tests/App/fixture/Acme/BarBundle/Document/ObjTag.php index 99a6e6f..5d39220 100644 --- a/tests/App/fixture/Acme/BarBundle/Document/ObjTag.php +++ b/tests/App/fixture/Acme/BarBundle/Document/ObjTag.php @@ -3,6 +3,7 @@ namespace Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\BarBundle\Document; use Sineflow\ElasticsearchBundle\Annotation as ES; +use Sineflow\ElasticsearchBundle\Attribute as SFES; use Sineflow\ElasticsearchBundle\Document\ObjectInterface; /** @@ -10,12 +11,15 @@ * * @ES\DocObject */ +#[SFES\DocObject] class ObjTag implements ObjectInterface { /** - * @var string - * * @ES\Property(type="text", name="tagname") */ - public $tagName; + #[SFES\Property( + name: 'tagname', + type: 'text', + )] + public ?string $tagName = null; } diff --git a/tests/App/fixture/Acme/BarBundle/Document/Product.php b/tests/App/fixture/Acme/BarBundle/Document/Product.php index cdaa912..8fb5394 100644 --- a/tests/App/fixture/Acme/BarBundle/Document/Product.php +++ b/tests/App/fixture/Acme/BarBundle/Document/Product.php @@ -3,9 +3,11 @@ namespace Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\BarBundle\Document; use Sineflow\ElasticsearchBundle\Annotation as ES; +use Sineflow\ElasticsearchBundle\Attribute as SFES; use Sineflow\ElasticsearchBundle\Document\AbstractDocument; use Sineflow\ElasticsearchBundle\Document\MLProperty; use Sineflow\ElasticsearchBundle\Result\ObjectIterator; +use Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\BarBundle\Document\Repository\ProductRepository; /** * Product document for testing. @@ -17,11 +19,15 @@ * } * ) */ +#[SFES\Document( + repositoryClass: ProductRepository::class, + options: [ + 'dynamic' => 'strict', + ], +)] class Product extends AbstractDocument { /** - * @var string - * * @ES\Property( * type="text", * name="title", @@ -33,60 +39,87 @@ class Product extends AbstractDocument * } * ) */ - public $title; + #[SFES\Property( + name: 'title', + type: 'text', + options: [ + 'fields' => [ + 'raw' => ['type' => 'keyword'], + 'title' => ['type' => 'text'], + ], + ], + )] + public ?string $title = null; /** - * @var string - * * @ES\Property(type="text", name="description") */ - public $description; + #[SFES\Property( + name: 'description', + type: 'text', + )] + public ?string $description = null; /** - * @var ObjCategory - * * @ES\Property(type="object", name="category", objectName="AcmeBarBundle:ObjCategory") */ - public $category; + #[SFES\Property( + name: 'category', + type: 'object', + objectName: ObjCategory::class, + )] + public ?ObjCategory $category = null; /** * @var ObjCategory[]|ObjectIterator * * @ES\Property(type="object", name="related_categories", multiple=true, objectName="AcmeBarBundle:ObjCategory") */ - public $relatedCategories; + #[SFES\Property( + name: 'related_categories', + type: 'object', + objectName: ObjCategory::class, + multiple: true, + )] + public array|ObjectIterator $relatedCategories = []; /** - * @var int - * * @ES\Property(type="float", name="price") */ - public $price; + #[SFES\Property( + name: 'price', + type: 'float', + )] + public ?int $price = null; /** - * @var string - * * @ES\Property(type="geo_point", name="location") */ - public $location; + #[SFES\Property( + name: 'location', + type: 'geo_point', + )] + public ?string $location = null; /** - * @var string - * * @ES\Property(type="boolean", name="limited") */ - public $limited; + #[SFES\Property( + name: 'limited', + type: 'boolean', + )] + public ?string $limited = null; /** - * @var string - * * @ES\Property(type="date", name="released") */ - public $released; + #[SFES\Property( + name: 'released', + type: 'date', + )] + public ?string $released = null; /** - * @var MLProperty - * * @ES\Property( * name="ml_info", * type="text", @@ -102,11 +135,23 @@ class Product extends AbstractDocument * } * ) */ - public $mlInfo; + #[SFES\Property( + name: 'ml_info', + type: 'text', + multilanguage: true, + options: [ + 'analyzer' => '{lang}_analyzer', + 'fields' => [ + 'ngram' => [ + 'type' => 'text', + 'analyzer' => '{lang}_analyzer', + ], + ], + ], + )] + public ?MLProperty $mlInfo = null; /** - * @var MLProperty - * * @ES\Property( * name="ml_more_info", * type="text", @@ -117,11 +162,18 @@ class Product extends AbstractDocument * } * ) */ - public $mlMoreInfo; + #[SFES\Property( + name: 'ml_more_info', + type: 'text', + multilanguage: true, + multilanguageDefaultOptions: [ + 'type' => 'text', + 'index' => false, + ], + )] + public ?MLProperty $mlMoreInfo = null; /** - * @var int - * * @ES\Property( * type="text", * name="pieces_count", @@ -132,5 +184,17 @@ class Product extends AbstractDocument * } * ) */ - public $tokenPiecesCount; + #[SFES\Property( + name: 'pieces_count', + type: 'text', + options: [ + 'fields' => [ + 'count' => [ + 'type' => 'token_count', + 'analyzer' => 'whitespace', + ], + ], + ], + )] + public ?int $tokenPiecesCount = null; } diff --git a/tests/App/fixture/Acme/FooBundle/Document/Customer.php b/tests/App/fixture/Acme/FooBundle/Document/Customer.php index ecea22a..7aa3830 100644 --- a/tests/App/fixture/Acme/FooBundle/Document/Customer.php +++ b/tests/App/fixture/Acme/FooBundle/Document/Customer.php @@ -3,6 +3,7 @@ namespace Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Document; use Sineflow\ElasticsearchBundle\Annotation as ES; +use Sineflow\ElasticsearchBundle\Attribute as SFES; use Sineflow\ElasticsearchBundle\Document\AbstractDocument; use Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Document\Provider\CustomerProvider; use Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Enum\CustomerTypeEnum; @@ -12,6 +13,9 @@ * providerClass=CustomerProvider::class * ) */ +#[SFES\Document( + providerClass: CustomerProvider::class, +)] class Customer extends AbstractDocument { /** @@ -19,6 +23,10 @@ class Customer extends AbstractDocument * * @ES\Property(name="name", type="keyword") */ + #[SFES\Property( + name: 'name', + type: 'keyword', + )] public string $name; /** @@ -30,25 +38,30 @@ class Customer extends AbstractDocument * enumType=Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Enum\CustomerTypeEnum::class * ) */ + #[SFES\Property( + name: 'customer_type', + type: 'integer', + enumType: CustomerTypeEnum::class, + )] public ?CustomerTypeEnum $customerType = null; /** + * @var bool + * * @ES\Property(name="active", type="boolean") */ + #[SFES\Property( + name: 'active', + type: 'boolean', + )] private $active; - /** - * @return bool - */ public function isActive() { return $this->active; } - /** - * @param bool $active - */ - public function setActive($active) + public function setActive($active): void { $this->active = $active; } diff --git a/tests/App/fixture/Acme/FooBundle/Document/EntityWithInvalidEnum.php b/tests/App/fixture/Acme/FooBundle/Document/EntityWithInvalidEnum.php index 1a0a2af..59ea004 100644 --- a/tests/App/fixture/Acme/FooBundle/Document/EntityWithInvalidEnum.php +++ b/tests/App/fixture/Acme/FooBundle/Document/EntityWithInvalidEnum.php @@ -3,12 +3,14 @@ namespace Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Document; use Sineflow\ElasticsearchBundle\Annotation as ES; +use Sineflow\ElasticsearchBundle\Attribute as SFES; use Sineflow\ElasticsearchBundle\Document\AbstractDocument; use Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Enum\CustomerTypeEnum; /** * @ES\Document */ +#[SFES\Document] class EntityWithInvalidEnum extends AbstractDocument { /** @@ -18,5 +20,10 @@ class EntityWithInvalidEnum extends AbstractDocument * enumType=nonExistingEnumClass * ) */ + #[SFES\Property( + name: 'enum_test', + type: 'string', + enumType: 'nonExistingEnumClass', + )] public ?CustomerTypeEnum $enumTest = null; } diff --git a/tests/App/fixture/Acme/FooBundle/Document/Log.php b/tests/App/fixture/Acme/FooBundle/Document/Log.php index 592b561..79ef879 100644 --- a/tests/App/fixture/Acme/FooBundle/Document/Log.php +++ b/tests/App/fixture/Acme/FooBundle/Document/Log.php @@ -3,17 +3,21 @@ namespace Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Document; use Sineflow\ElasticsearchBundle\Annotation as ES; +use Sineflow\ElasticsearchBundle\Attribute as SFES; use Sineflow\ElasticsearchBundle\Document\AbstractDocument; /** * @ES\Document; */ +#[SFES\Document] class Log extends AbstractDocument { /** - * @var string - * * @ES\Property(name="entry", type="keyword") */ - public $entry; + #[SFES\Property( + name: 'entry', + type: 'keyword', + )] + public ?string $entry = null; } diff --git a/tests/App/fixture/Acme/FooBundle/Document/Order.php b/tests/App/fixture/Acme/FooBundle/Document/Order.php index d27699a..2c90398 100644 --- a/tests/App/fixture/Acme/FooBundle/Document/Order.php +++ b/tests/App/fixture/Acme/FooBundle/Document/Order.php @@ -3,19 +3,26 @@ namespace Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Document; use Sineflow\ElasticsearchBundle\Annotation as ES; +use Sineflow\ElasticsearchBundle\Attribute as SFES; use Sineflow\ElasticsearchBundle\Document\AbstractDocument; +use Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Document\Provider\OrderProvider; /** * @ES\Document( * providerClass="Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Document\Provider\OrderProvider" * ) */ +#[SFES\Document( + providerClass: OrderProvider::class, +)] class Order extends AbstractDocument { /** - * @var int - * * @ES\Property(name="order_time", type="integer") */ - public $orderTime; + #[SFES\Property( + name: 'order_time', + type: 'integer', + )] + public ?int $orderTime = null; } diff --git a/tests/Functional/Document/Provider/ElasticsearchProviderTest.php b/tests/Functional/Document/Provider/ElasticsearchProviderTest.php index 703c5d9..6ac32a6 100644 --- a/tests/Functional/Document/Provider/ElasticsearchProviderTest.php +++ b/tests/Functional/Document/Provider/ElasticsearchProviderTest.php @@ -37,9 +37,9 @@ public function testGetDocument(): void $doc = $esProvider->getDocument(3); - $this->assertEquals([ - '_id' => 3, + $this->assertSame([ 'title' => 'Product 3', + '_id' => '3', ], $doc); } @@ -58,7 +58,7 @@ public function testGetDocuments(): void \sort($ids); // Make sure all and exact documents were returned - $this->assertEquals([1, 2, 3], $ids); + $this->assertSame(['1', '2', '3'], $ids); } private function getProvider() diff --git a/tests/Functional/Document/RepositoryTest.php b/tests/Functional/Document/RepositoryTest.php index 3259cce..80b1c59 100644 --- a/tests/Functional/Document/RepositoryTest.php +++ b/tests/Functional/Document/RepositoryTest.php @@ -6,7 +6,6 @@ use Sineflow\ElasticsearchBundle\Document\Repository\Repository; use Sineflow\ElasticsearchBundle\Finder\Finder; use Sineflow\ElasticsearchBundle\Manager\IndexManager; -use Sineflow\ElasticsearchBundle\Mapping\DocumentMetadataCollector; use Sineflow\ElasticsearchBundle\Tests\AbstractElasticsearchTestCase; class RepositoryTest extends AbstractElasticsearchTestCase @@ -17,10 +16,6 @@ class RepositoryTest extends AbstractElasticsearchTestCase private IndexManager $indexManager; - private Finder $finder; - - private DocumentMetadataCollector $metadataCollector; - /** * {@inheritdoc} */ @@ -48,12 +43,12 @@ protected function setUp(): void { parent::setUp(); - $this->finder = $this->getContainer()->get(Finder::class); + $finder = $this->getContainer()->get(Finder::class); $this->indexManager = $this->getContainer()->get('sfes.index.bar'); $this->indexManager->getConnection()->setAutocommit(true); - $this->repository = new Repository($this->indexManager, $this->finder); + $this->repository = new Repository($this->indexManager, $finder); $this->getIndexManager('bar', !$this->hasCreatedIndexManager('bar')); } @@ -66,9 +61,9 @@ public function testGetIndexManager(): void public function testGetById(): void { $doc = $this->repository->getById('doc1'); - $this->assertEquals('aaa', $doc->title); + $this->assertSame('aaa', $doc->title); $doc = $this->repository->getById(2); - $this->assertEquals('ccc', $doc->title); + $this->assertSame('ccc', $doc->title); } public function testCount(): void @@ -81,17 +76,17 @@ public function testCount(): void ], ]; - $this->assertEquals(2, $this->repository->count($searchBody)); + $this->assertSame(2, $this->repository->count($searchBody)); } public function testReindex(): void { - $this->assertEquals(1, $this->repository->getById('doc1', Finder::RESULTS_RAW)['_version']); + $this->assertSame(1, $this->repository->getById('doc1', Finder::RESULTS_RAW)['_version']); $this->indexManager->reindex('doc1'); $rawDoc = $this->repository->getById('doc1', Finder::RESULTS_RAW); - $this->assertEquals(2, $rawDoc['_version']); - $this->assertEquals('aaa', $rawDoc['_source']['title']); + $this->assertSame(2, $rawDoc['_version']); + $this->assertSame('aaa', $rawDoc['_source']['title']); } } diff --git a/tests/Functional/Finder/Adapter/KnpPaginatorAdapterTest.php b/tests/Functional/Finder/Adapter/KnpPaginatorAdapterTest.php index 1898b82..78862de 100644 --- a/tests/Functional/Finder/Adapter/KnpPaginatorAdapterTest.php +++ b/tests/Functional/Finder/Adapter/KnpPaginatorAdapterTest.php @@ -90,8 +90,8 @@ public function testPagination(): void $this->assertCount(2, $pagination); $this->assertInstanceOf(Product::class, $pagination->offsetGet(0)); - $this->assertEquals(3, $pagination->offsetGet(0)->id); - $this->assertEquals(10, $pagination->getCustomParameter('aggregations')['avg_price']['value']); + $this->assertSame('3', $pagination->offsetGet(0)->id); + $this->assertEqualsWithDelta(10.0, $pagination->getCustomParameter('aggregations')['avg_price']['value'], PHP_FLOAT_EPSILON); $this->assertIsArray($pagination->getCustomParameter('suggestions')); // Test array results @@ -102,8 +102,8 @@ public function testPagination(): void /** @var SlidingPagination $pagination */ $pagination = $paginator->paginate($adapter, 2, 2); - $this->assertEquals(3, $pagination->key()); - $this->assertEquals('3rd Product', $pagination->current()['title']); + $this->assertSame(3, $pagination->key()); + $this->assertSame('3rd Product', $pagination->current()['title']); $this->assertNull($pagination->getCustomParameter('aggregations')); $this->assertNull($pagination->getCustomParameter('suggestions')); @@ -115,9 +115,9 @@ public function testPagination(): void /** @var SlidingPagination $pagination */ $pagination = $paginator->paginate($adapter, 2, 2); - $this->assertEquals(3, $pagination->current()['_id']); - $this->assertEquals('3rd Product', $pagination->current()['_source']['title']); - $this->assertEquals(10, $pagination->getCustomParameter('aggregations')['avg_price']['value']); + $this->assertSame('3', $pagination->current()['_id']); + $this->assertSame('3rd Product', $pagination->current()['_source']['title']); + $this->assertEqualsWithDelta(10.0, $pagination->getCustomParameter('aggregations')['avg_price']['value'], PHP_FLOAT_EPSILON); $this->assertIsArray($pagination->getCustomParameter('suggestions')); } @@ -139,14 +139,14 @@ public function testPaginationSorting(): void // Do not apply default order to KNP, so just use the one in the query /** @var SlidingPagination $pagination */ $pagination = $paginator->paginate($adapter, 1, 3); - $this->assertEquals(3, $pagination->current()->id); + $this->assertSame('3', $pagination->current()->id); // Test setting default order to KNP $pagination = $paginator->paginate($adapter, 1, 3, [ 'defaultSortFieldName' => '_id', 'defaultSortDirection' => 'desc', ]); - $this->assertEquals(54321, $pagination->current()->id); + $this->assertSame('54321', $pagination->current()->id); } public function testInvalidResultsType(): void diff --git a/tests/Functional/Finder/Adapter/ScrollAdapterTest.php b/tests/Functional/Finder/Adapter/ScrollAdapterTest.php index 7ff01ea..9898f9f 100644 --- a/tests/Functional/Finder/Adapter/ScrollAdapterTest.php +++ b/tests/Functional/Finder/Adapter/ScrollAdapterTest.php @@ -75,9 +75,9 @@ public function testScanScroll(): void ++$scrolls; } - $this->assertEquals(6, $i, 'Total matching documents iterated'); - $this->assertEquals(6, $scrollAdapter->getTotalHits(), 'Total hits returned by scroll'); - $this->assertEquals(3, $scrolls, 'Total number of scrolls'); + $this->assertSame(6, $i, 'Total matching documents iterated'); + $this->assertSame(6, $scrollAdapter->getTotalHits(), 'Total hits returned by scroll'); + $this->assertSame(3, $scrolls, 'Total number of scrolls'); // Test array results /** @var ScrollAdapter $scrollAdapter */ @@ -98,9 +98,9 @@ public function testScanScroll(): void } ++$scrolls; } - $this->assertEquals(6, $i, 'Total matching documents iterated'); - $this->assertEquals(6, $scrollAdapter->getTotalHits(), 'Total hits returned by scroll'); - $this->assertEquals(2, $scrolls, 'Total number of scrolls'); + $this->assertSame(6, $i, 'Total matching documents iterated'); + $this->assertSame(6, $scrollAdapter->getTotalHits(), 'Total hits returned by scroll'); + $this->assertSame(2, $scrolls, 'Total number of scrolls'); // Test raw results @@ -126,8 +126,8 @@ public function testScanScroll(): void } ++$scrolls; } - $this->assertEquals(6, $i, 'Total matching documents iterated'); - $this->assertEquals(6, $scrollAdapter->getTotalHits(), 'Total hits returned by scroll'); - $this->assertEquals(2, $scrolls, 'Total number of scrolls'); + $this->assertSame(6, $i, 'Total matching documents iterated'); + $this->assertSame(6, $scrollAdapter->getTotalHits(), 'Total hits returned by scroll'); + $this->assertSame(2, $scrolls, 'Total number of scrolls'); } } diff --git a/tests/Functional/Finder/FinderTest.php b/tests/Functional/Finder/FinderTest.php index 8b5b75d..618a763 100644 --- a/tests/Functional/Finder/FinderTest.php +++ b/tests/Functional/Finder/FinderTest.php @@ -124,10 +124,10 @@ public function testGetById(): void $docAsObject = $finder->get(Product::class, 'doc1'); $this->assertInstanceOf(Product::class, $docAsObject); - $this->assertEquals('aaa', $docAsObject->title); + $this->assertSame('aaa', $docAsObject->title); $docAsArray = $finder->get('AcmeBarBundle:Product', 'doc1', Finder::RESULTS_ARRAY); - $this->assertEquals('aaa', $docAsArray['title']); + $this->assertSame('aaa', $docAsArray['title']); $docAsRaw = $finder->get('AcmeBarBundle:Product', 'doc1', Finder::RESULTS_RAW); $this->assertArraySubset([ @@ -169,18 +169,18 @@ public function testFindInMultipleTypesAndIndices(): void $res = $finder->find(['AcmeBarBundle:Product', 'AcmeFooBundle:Customer'], $searchBody, Finder::RESULTS_OBJECT, [], $totalHits); $this->assertInstanceOf(DocumentIterator::class, $res); $this->assertCount(3, $res); - $this->assertEquals(3, $totalHits); + $this->assertSame(3, $totalHits); $resAsArray = iterator_to_array($res); $this->assertInstanceOf(Customer::class, $resAsArray[0]); $this->assertInstanceOf(Customer::class, $resAsArray[1]); $this->assertInstanceOf(Product::class, $resAsArray[2]); - $this->assertEquals(111, $resAsArray[0]->id); + $this->assertSame('111', $resAsArray[0]->id); $this->assertSame('Jane Doe', $resAsArray[0]->name); $this->assertSame(CustomerTypeEnum::COMPANY, $resAsArray[0]->customerType); - $this->assertEquals(222, $resAsArray[1]->id); + $this->assertSame('222', $resAsArray[1]->id); $this->assertSame('John Doe', $resAsArray[1]->name); $this->assertSame(CustomerTypeEnum::INDIVIDUAL, $resAsArray[1]->customerType); @@ -247,8 +247,8 @@ public function testCount(): void ], ]; - $this->assertEquals(2, $finder->count(['AcmeFooBundle:Customer'], $searchBody)); - $this->assertEquals(3, $finder->count(['AcmeBarBundle:Product', 'AcmeFooBundle:Customer'], $searchBody)); + $this->assertSame(2, $finder->count(['AcmeFooBundle:Customer'], $searchBody)); + $this->assertSame(3, $finder->count(['AcmeBarBundle:Product', 'AcmeFooBundle:Customer'], $searchBody)); } public function testGetTargetIndices(): void @@ -257,7 +257,7 @@ public function testGetTargetIndices(): void $res = $finder->getTargetIndices(['AcmeBarBundle:Product', 'AcmeFooBundle:Customer']); - $this->assertEquals([ + $this->assertSame([ 'sineflow-esb-test-bar', 'sineflow-esb-test-customer', ], $res); diff --git a/tests/Functional/Manager/ConnectionManagerRegistryTest.php b/tests/Functional/Manager/ConnectionManagerRegistryTest.php index 2625a03..ff31716 100644 --- a/tests/Functional/Manager/ConnectionManagerRegistryTest.php +++ b/tests/Functional/Manager/ConnectionManagerRegistryTest.php @@ -30,8 +30,6 @@ public function testGetAll(): void $registry = $this->getContainer()->get(ConnectionManagerRegistry::class); $connections = $registry->getAll(); - foreach ($connections as $connection) { - $this->assertInstanceOf(ConnectionManager::class, $connection); - } + $this->assertContainsOnlyInstancesOf(ConnectionManager::class, $connections); } } diff --git a/tests/Functional/Manager/IndexManagerRegistryTest.php b/tests/Functional/Manager/IndexManagerRegistryTest.php index 765111f..0c567f1 100644 --- a/tests/Functional/Manager/IndexManagerRegistryTest.php +++ b/tests/Functional/Manager/IndexManagerRegistryTest.php @@ -35,7 +35,7 @@ public function testGetByClass(): void $product = new Product(); $im = $registry->getByClass($product::class); $this->assertInstanceOf(IndexManager::class, $im); - $this->assertEquals('bar', $im->getManagerName()); + $this->assertSame('bar', $im->getManagerName()); } public function testGetByEntity(): void @@ -45,6 +45,6 @@ public function testGetByEntity(): void $product = new Product(); $im = $registry->getByEntity($product); $this->assertInstanceOf(IndexManager::class, $im); - $this->assertEquals('bar', $im->getManagerName()); + $this->assertSame('bar', $im->getManagerName()); } } diff --git a/tests/Functional/Manager/IndexManagerTest.php b/tests/Functional/Manager/IndexManagerTest.php index f8e82bb..19d59dd 100644 --- a/tests/Functional/Manager/IndexManagerTest.php +++ b/tests/Functional/Manager/IndexManagerTest.php @@ -67,12 +67,12 @@ protected function getDataArray(): array public function testGetReadAliasAndGetWriteAlias(): void { $imWithAliases = $this->getIndexManager('customer', false); - $this->assertEquals('sineflow-esb-test-customer', $imWithAliases->getReadAlias()); - $this->assertEquals('sineflow-esb-test-customer_write', $imWithAliases->getWriteAlias()); + $this->assertSame('sineflow-esb-test-customer', $imWithAliases->getReadAlias()); + $this->assertSame('sineflow-esb-test-customer_write', $imWithAliases->getWriteAlias()); $imWithoutAliases = $this->getIndexManager('bar', false); - $this->assertEquals('sineflow-esb-test-bar', $imWithoutAliases->getReadAlias()); - $this->assertEquals('sineflow-esb-test-bar', $imWithoutAliases->getWriteAlias()); + $this->assertSame('sineflow-esb-test-bar', $imWithoutAliases->getReadAlias()); + $this->assertSame('sineflow-esb-test-bar', $imWithoutAliases->getWriteAlias()); } /** @@ -157,7 +157,7 @@ public function testGetLiveIndex(): void $imWithoutAliases = $this->getIndexManager('bar'); $liveIndex = $imWithoutAliases->getLiveIndex(); - $this->assertEquals('sineflow-esb-test-bar', $liveIndex); + $this->assertSame('sineflow-esb-test-bar', $liveIndex); } /** @@ -192,7 +192,7 @@ public function testRebuildIndexWithoutDeletingOld(): void $imWithAliases->getConnection()->getClient()->indices()->delete(['index' => $liveIndex]); $newLiveIndex = $imWithAliases->getLiveIndex(); - $this->assertNotEquals($liveIndex, $newLiveIndex); + $this->assertNotSame($liveIndex, $newLiveIndex); } /** @@ -212,7 +212,7 @@ public function testRebuildIndexAndDeleteOld(): void $this->assertFalse($imWithAliases->getConnection()->getClient()->indices()->exists(['index' => $liveIndex])->asBool()); $newLiveIndex = $imWithAliases->getLiveIndex(); - $this->assertNotEquals($liveIndex, $newLiveIndex); + $this->assertNotSame($liveIndex, $newLiveIndex); } /** @@ -236,7 +236,7 @@ public function testPersistForManagerWithoutAliasesWithoutAutocommit(): void $imWithoutAliases->getConnection()->commit(); $doc = $imWithoutAliases->getRepository()->getById(555); $this->assertInstanceOf(Product::class, $doc); - $this->assertEquals('Acme title', $doc->title); + $this->assertSame('Acme title', $doc->title); // Test persisting properties with null values $product->title = null; @@ -280,14 +280,14 @@ public function testPersistForManagerWithAliasesWithoutAutocommit(): void $doc = $imWithAliases->getRepository()->getById(555); $this->assertInstanceOf(Customer::class, $doc); - $this->assertEquals('John Doe', $doc->name); + $this->assertSame('John Doe', $doc->name); // Check that value is set in the additional index for the write alias as well $raw = $imWithAliases->getConnection()->getClient()->get([ 'index' => 'sineflow-esb-test-temp', 'id' => 555, ])->asArray(); - $this->assertEquals('John Doe', $raw['_source']['name']); + $this->assertSame('John Doe', $raw['_source']['name']); $imWithAliases->getConnection()->getClient()->indices()->delete(['index' => 'sineflow-esb-test-temp']); } @@ -309,7 +309,7 @@ public function testPersistRawWithAutocommit(): void ]); $doc = $imWithAliases->getRepository()->getById(444); - $this->assertEquals('Jane', $doc->name); + $this->assertSame('Jane', $doc->name); } /** @@ -328,7 +328,7 @@ public function testPersistStrictMappingDocRetrievedById(): void $im->persist($doc); $doc = $repo->getById('doc1'); - $this->assertEquals('NewName', $doc->title); + $this->assertSame('NewName', $doc->title); } /** @@ -346,7 +346,7 @@ public function testUpdateWithCorrectParams(): void ]); $doc = $imWithAliases->getRepository()->getById(111); - $this->assertEquals('Alicia', $doc->name); + $this->assertSame('Alicia', $doc->name); } /** @@ -416,18 +416,18 @@ public function testReindexWithElasticsearchSelfProvider(): void $im->getConnection()->setAutocommit(false); $rawDoc = $im->getRepository()->getById('abcde', Finder::RESULTS_RAW); - $this->assertEquals(1, $rawDoc['_version']); + $this->assertSame(1, $rawDoc['_version']); $im->reindex('abcde'); $rawDoc = $im->getRepository()->getById('abcde', Finder::RESULTS_RAW); - $this->assertEquals(1, $rawDoc['_version']); + $this->assertSame(1, $rawDoc['_version']); $im->getConnection()->commit(); $rawDoc = $im->getRepository()->getById('abcde', Finder::RESULTS_RAW); - $this->assertEquals(2, $rawDoc['_version']); - $this->assertEquals('log entry', $rawDoc['_source']['entry']); + $this->assertSame(2, $rawDoc['_version']); + $this->assertSame('log entry', $rawDoc['_source']['entry']); } public function testGetDataProvider(): void @@ -463,8 +463,8 @@ public function testGetters(): void $this->assertTrue($imWithAliases->getUseAliases()); $this->assertFalse($imWithoutAliases->getUseAliases()); - $this->assertEquals('customer', $imWithAliases->getManagerName()); - $this->assertEquals('bar', $imWithoutAliases->getManagerName()); + $this->assertSame('customer', $imWithAliases->getManagerName()); + $this->assertSame('bar', $imWithoutAliases->getManagerName()); } /** diff --git a/tests/Functional/Mapping/DocumentAttributeParserTest.php b/tests/Functional/Mapping/DocumentAttributeParserTest.php new file mode 100644 index 0000000..07308a9 --- /dev/null +++ b/tests/Functional/Mapping/DocumentAttributeParserTest.php @@ -0,0 +1,305 @@ +getContainer()->get(DocumentLocator::class); + $separator = $this->getContainer()->getParameter('sfes.mlproperty.language_separator'); + $languages = $this->getContainer()->getParameter('sfes.languages'); + $this->documentAttributeParser = new DocumentAttributeParser($locator, $separator, $languages); + } + + public function testParseNonDocument(): void + { + $this->expectException(InvalidMappingException::class); + $reflection = new \ReflectionClass(ObjCategory::class); + $res = $this->documentAttributeParser->parse($reflection, []); + } + + public function testParseDocumentWithEnumProperty() + { + $reflection = new \ReflectionClass(Customer::class); + $res = $this->documentAttributeParser->parse($reflection, []); + $this->assertSame(CustomerTypeEnum::class, $res['propertiesMetadata']['customer_type']['enumType']); + } + + public function testParseDocumentWithInvalidEnumFieldProperty() + { + $this->expectException(InvalidMappingException::class); + $reflection = new \ReflectionClass(EntityWithInvalidEnum::class); + $this->documentAttributeParser->parse($reflection, []); + } + + public function testParse(): void + { + $reflection = new \ReflectionClass(Product::class); + $indexAnalyzers = [ + 'default_analyzer' => [ + 'type' => 'standard', + ], + 'en_analyzer' => [ + 'type' => 'standard', + ], + ]; + + $res = $this->documentAttributeParser->parse($reflection, $indexAnalyzers); + + $expected = [ + 'properties' => [ + 'title' => [ + 'fields' => [ + 'raw' => [ + 'type' => 'keyword', + ], + 'title' => [ + 'type' => 'text', + ], + ], + 'type' => 'text', + ], + 'description' => [ + 'type' => 'text', + ], + 'category' => [ + 'properties' => [ + 'id' => [ + 'type' => 'integer', + ], + 'title' => [ + 'type' => 'keyword', + ], + 'tags' => [ + 'properties' => [ + 'tagname' => [ + 'type' => 'text', + ], + ], + ], + ], + ], + 'related_categories' => [ + 'properties' => [ + 'id' => [ + 'type' => 'integer', + ], + 'title' => [ + 'type' => 'keyword', + ], + 'tags' => [ + 'properties' => [ + 'tagname' => [ + 'type' => 'text', + ], + ], + ], + ], + ], + 'price' => [ + 'type' => 'float', + ], + 'location' => [ + 'type' => 'geo_point', + ], + 'limited' => [ + 'type' => 'boolean', + ], + 'released' => [ + 'type' => 'date', + ], + 'ml_info-en' => [ + 'analyzer' => 'en_analyzer', + 'fields' => [ + 'ngram' => [ + 'type' => 'text', + 'analyzer' => 'en_analyzer', + ], + ], + 'type' => 'text', + ], + 'ml_info-fr' => [ + 'analyzer' => 'default_analyzer', + 'fields' => [ + 'ngram' => [ + 'type' => 'text', + 'analyzer' => 'default_analyzer', + ], + ], + 'type' => 'text', + ], + 'ml_info-default' => [ + 'type' => 'keyword', + 'ignore_above' => 256, + ], + 'ml_more_info-en' => [ + 'type' => 'text', + ], + 'ml_more_info-fr' => [ + 'type' => 'text', + ], + 'ml_more_info-default' => [ + 'type' => 'text', + 'index' => false, + ], + 'pieces_count' => [ + 'fields' => [ + 'count' => [ + 'type' => 'token_count', + 'analyzer' => 'whitespace', + ], + ], + 'type' => 'text', + ], + ], + 'fields' => [ + 'dynamic' => 'strict', + ], + 'propertiesMetadata' => [ + 'title' => [ + 'propertyName' => 'title', + 'type' => 'text', + 'propertyAccess' => 1, + ], + 'description' => [ + 'propertyName' => 'description', + 'type' => 'text', + 'propertyAccess' => 1, + ], + 'category' => [ + 'propertyName' => 'category', + 'type' => 'object', + 'multiple' => null, + 'propertiesMetadata' => [ + 'id' => [ + 'propertyName' => 'id', + 'type' => 'integer', + 'propertyAccess' => 1, + ], + 'title' => [ + 'propertyName' => 'title', + 'type' => 'keyword', + 'propertyAccess' => 1, + ], + 'tags' => [ + 'propertyName' => 'tags', + 'type' => 'object', + 'multiple' => true, + 'propertiesMetadata' => [ + 'tagname' => [ + 'propertyName' => 'tagName', + 'type' => 'text', + 'propertyAccess' => 1, + ], + ], + 'className' => ObjTag::class, + 'propertyAccess' => 1, + ], + ], + 'className' => ObjCategory::class, + 'propertyAccess' => 1, + ], + 'related_categories' => [ + 'propertyName' => 'relatedCategories', + 'type' => 'object', + 'multiple' => true, + 'propertiesMetadata' => [ + 'id' => [ + 'propertyName' => 'id', + 'type' => 'integer', + 'propertyAccess' => 1, + ], + 'title' => [ + 'propertyName' => 'title', + 'type' => 'keyword', + 'propertyAccess' => 1, + ], + 'tags' => [ + 'propertyName' => 'tags', + 'type' => 'object', + 'multiple' => true, + 'propertiesMetadata' => [ + 'tagname' => [ + 'propertyName' => 'tagName', + 'type' => 'text', + 'propertyAccess' => 1, + ], + ], + 'className' => ObjTag::class, + 'propertyAccess' => 1, + ], + ], + 'className' => ObjCategory::class, + 'propertyAccess' => 1, + ], + 'price' => [ + 'propertyName' => 'price', + 'type' => 'float', + 'propertyAccess' => 1, + ], + 'location' => [ + 'propertyName' => 'location', + 'type' => 'geo_point', + 'propertyAccess' => 1, + ], + 'limited' => [ + 'propertyName' => 'limited', + 'type' => 'boolean', + 'propertyAccess' => 1, + ], + 'released' => [ + 'propertyName' => 'released', + 'type' => 'date', + 'propertyAccess' => 1, + ], + 'ml_info' => [ + 'propertyName' => 'mlInfo', + 'type' => 'text', + 'multilanguage' => true, + 'propertyAccess' => 1, + ], + 'ml_more_info' => [ + 'propertyName' => 'mlMoreInfo', + 'type' => 'text', + 'multilanguage' => true, + 'propertyAccess' => 1, + ], + 'pieces_count' => [ + 'propertyName' => 'tokenPiecesCount', + 'type' => 'text', + 'propertyAccess' => 1, + ], + '_id' => [ + 'propertyName' => 'id', + 'type' => 'keyword', + 'propertyAccess' => 1, + ], + '_score' => [ + 'propertyName' => 'score', + 'type' => 'float', + 'propertyAccess' => 1, + ], + ], + 'repositoryClass' => ProductRepository::class, + 'providerClass' => null, + 'className' => Product::class, + ]; + + $this->assertEquals($expected, $res); + } +} diff --git a/tests/Functional/Mapping/DocumentMetadataCollectorTest.php b/tests/Functional/Mapping/DocumentMetadataCollectorTest.php index db153c3..6c3ca26 100644 --- a/tests/Functional/Mapping/DocumentMetadataCollectorTest.php +++ b/tests/Functional/Mapping/DocumentMetadataCollectorTest.php @@ -3,6 +3,7 @@ namespace Sineflow\ElasticsearchBundle\Tests\Functional\Mapping; use Jchook\AssertThrows\AssertThrows; +use Sineflow\ElasticsearchBundle\Mapping\DocumentAttributeParser; use Sineflow\ElasticsearchBundle\Mapping\DocumentLocator; use Sineflow\ElasticsearchBundle\Mapping\DocumentMetadata; use Sineflow\ElasticsearchBundle\Mapping\DocumentMetadataCollector; @@ -27,8 +28,8 @@ class DocumentMetadataCollectorTest extends AbstractContainerAwareTestCase private DocumentMetadataCollector $metadataCollector; private array $indexManagers; private DocumentLocator $docLocator; - private DocumentParser $docParser; - private CacheInterface $cache; + private ?DocumentParser $docParser; + private DocumentAttributeParser $docAttributeParser; private CacheInterface $nullCache; /** @@ -328,11 +329,18 @@ protected function setUp(): void { $this->indexManagers = $this->getContainer()->getParameter('sfes.indices'); $this->docLocator = $this->getContainer()->get(DocumentLocator::class); - $this->docParser = $this->getContainer()->get(DocumentParser::class); - $this->cache = $this->getContainer()->get('cache.system'); + $this->docParser = $this->getContainer()->has(DocumentParser::class) ? $this->getContainer()->get(DocumentParser::class) : null; + $this->docAttributeParser = $this->getContainer()->get(DocumentAttributeParser::class); + $cache = $this->getContainer()->get('cache.system'); $this->nullCache = $this->getContainer()->get('app.null_cache_adapter'); - $this->metadataCollector = new DocumentMetadataCollector($this->indexManagers, $this->docLocator, $this->docParser, $this->cache); + $this->metadataCollector = new DocumentMetadataCollector( + $this->indexManagers, + $this->docLocator, + $this->docParser, + $this->docAttributeParser, + $cache + ); } public function testGetDocumentMetadata(): void @@ -355,7 +363,13 @@ public function testGetDocumentMetadata(): void public function testMetadataWithCacheVsNoCache(): void { - $metadataCollectorWithCacheDisabled = new DocumentMetadataCollector($this->indexManagers, $this->docLocator, $this->docParser, $this->nullCache); + $metadataCollectorWithCacheDisabled = new DocumentMetadataCollector( + $this->indexManagers, + $this->docLocator, + $this->docParser, + $this->docAttributeParser, + $this->nullCache, + ); $this->assertEquals($this->metadataCollector->getDocumentMetadata('AcmeFooBundle:Customer'), $metadataCollectorWithCacheDisabled->getDocumentMetadata('AcmeFooBundle:Customer')); $this->assertEquals($this->metadataCollector->getObjectPropertiesMetadata('AcmeFooBundle:Customer'), $metadataCollectorWithCacheDisabled->getObjectPropertiesMetadata('AcmeFooBundle:Customer')); } @@ -388,10 +402,10 @@ public function testGetObjectPropertiesMetadataWithValidClasses(): void public function testGetDocumentClassIndex(): void { $docClassIndex = $this->metadataCollector->getDocumentClassIndex('AcmeBarBundle:Product'); - $this->assertEquals('bar', $docClassIndex); + $this->assertSame('bar', $docClassIndex); $docClassIndex = $this->metadataCollector->getDocumentClassIndex(Product::class); - $this->assertEquals('bar', $docClassIndex); + $this->assertSame('bar', $docClassIndex); $this->assertThrows(\InvalidArgumentException::class, function (): void { $this->metadataCollector->getDocumentClassIndex('Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Document\NonExistingClass'); diff --git a/tests/Functional/Mapping/DocumentParserTest.php b/tests/Functional/Mapping/DocumentParserTest.php index 23058ab..c33cc89 100644 --- a/tests/Functional/Mapping/DocumentParserTest.php +++ b/tests/Functional/Mapping/DocumentParserTest.php @@ -11,7 +11,9 @@ use Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\BarBundle\Document\ObjTag; use Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\BarBundle\Document\Product; use Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\BarBundle\Document\Repository\ProductRepository; +use Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Document\Customer; use Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Document\EntityWithInvalidEnum; +use Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\FooBundle\Enum\CustomerTypeEnum; class DocumentParserTest extends AbstractContainerAwareTestCase { @@ -19,6 +21,10 @@ class DocumentParserTest extends AbstractContainerAwareTestCase protected function setUp(): void { + if (!class_exists(AnnotationReader::class)) { + $this->markTestSkipped('doctrine/annotations is not installed, skipping DocumentParser tests.'); + } + $reader = new AnnotationReader(); $locator = $this->getContainer()->get(DocumentLocator::class); $separator = $this->getContainer()->getParameter('sfes.mlproperty.language_separator'); @@ -31,7 +37,14 @@ public function testParseNonDocument(): void $reflection = new \ReflectionClass(ObjCategory::class); $res = $this->documentParser->parse($reflection, []); - $this->assertEquals([], $res); + $this->assertSame([], $res); + } + + public function testParseDocumentWithEnumProperty() + { + $reflection = new \ReflectionClass(Customer::class); + $res = $this->documentParser->parse($reflection, []); + $this->assertSame(CustomerTypeEnum::class, $res['propertiesMetadata']['customer_type']['enumType']); } public function testParseDocumentWithInvalidEnumFieldProperty() diff --git a/tests/Functional/Profiler/ProfilerDataCollectorTest.php b/tests/Functional/Profiler/ProfilerDataCollectorTest.php index 73f744a..02d2b60 100644 --- a/tests/Functional/Profiler/ProfilerDataCollectorTest.php +++ b/tests/Functional/Profiler/ProfilerDataCollectorTest.php @@ -50,7 +50,7 @@ public function testGetQueryCount(): void // 1. DELETE query to remove existing index, // 2. Internal call to GET /_aliases to check if index/alias already exists // 3. PUT request to create index - $this->assertEquals(3, $this->getCollector()->getQueryCount()); + $this->assertSame(3, $this->getCollector()->getQueryCount()); $product = new Product(); $product->title = 'tuna'; @@ -61,7 +61,7 @@ public function testGetQueryCount(): void // 1. GET /sineflow-esb-test-bar/_alias to check if more than one index should be written to // 2. POST bulk request to enter the data // 3. GET /_refresh, because of the $forceRefresh param of ->commit() - $this->assertEquals(6, $this->getCollector()->getQueryCount()); + $this->assertSame(6, $this->getCollector()->getQueryCount()); } /** diff --git a/tests/Functional/Result/DocumentConverterTest.php b/tests/Functional/Result/DocumentConverterTest.php index e501231..c4e55c3 100644 --- a/tests/Functional/Result/DocumentConverterTest.php +++ b/tests/Functional/Result/DocumentConverterTest.php @@ -3,6 +3,7 @@ namespace Sineflow\ElasticsearchBundle\Tests\Functional\Result; use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; +use PHPUnit\Framework\Attributes\Depends; use Sineflow\ElasticsearchBundle\Document\MLProperty; use Sineflow\ElasticsearchBundle\Exception\DocumentConversionException; use Sineflow\ElasticsearchBundle\Mapping\DocumentMetadataCollector; @@ -21,10 +22,12 @@ class DocumentConverterTest extends AbstractContainerAwareTestCase 'title' => 'Foo Product', 'category' => [ 'title' => 'Bar', + 'tags' => [], ], 'related_categories' => [ [ 'title' => 'Acme', + 'tags' => [], ], ], 'ml_info-en' => 'info in English', @@ -55,7 +58,7 @@ public function testAssignArrayToObjectWithNestedSingleValueInsteadOfArray(): vo ); $category = $result->relatedCategories->current(); - $this->assertEquals($category->id, 123); + $this->assertSame(123, $category->id); } public function testAssignArrayToObjectWithNestedSingleValueArrayInsteadOfSingleValue(): void @@ -81,7 +84,7 @@ public function testAssignArrayToObjectWithNestedSingleValueArrayInsteadOfSingle $metadataCollector->getDocumentMetadata('AcmeBarBundle:Product')->getPropertiesMetadata() ); - $this->assertEquals($result->category->id, 123); + $this->assertSame(123, $result->category->id); } public function testAssignArrayToObjectWithNestedMultiValueArrayInsteadOfSingleValue(): void @@ -132,13 +135,13 @@ public function testAssignArrayToObjectWithAllFieldsCorrectlySet() $this->assertSame($product, $result); - $this->assertEquals('Foo Product', $product->title); - $this->assertEquals('doc1', $product->id); + $this->assertSame('Foo Product', $product->title); + $this->assertSame('doc1', $product->id); $this->assertInstanceOf(ObjCategory::class, $product->category); $this->assertContainsOnlyInstancesOf(ObjCategory::class, $product->relatedCategories); $this->assertInstanceOf(MLProperty::class, $product->mlInfo); - $this->assertEquals('info in English', $product->mlInfo->getValue('en')); - $this->assertEquals('info in French', $product->mlInfo->getValue('fr')); + $this->assertSame('info in English', $product->mlInfo->getValue('en')); + $this->assertSame('info in French', $product->mlInfo->getValue('fr')); return $product; } @@ -154,11 +157,11 @@ public function testAssignArrayToObjectWithEmptyFields(): void $converter->assignArrayToObject( $rawDoc, $product, - $metadataCollector->getDocumentMetadata('AcmeBarBundle:Product')->getPropertiesMetadata() + $metadataCollector->getDocumentMetadata(Product::class)->getPropertiesMetadata() ); $this->assertNull($product->title); $this->assertNull($product->category); - $this->assertNull($product->relatedCategories); + $this->assertSame([], $product->relatedCategories); $this->assertNull($product->mlInfo); } @@ -175,15 +178,13 @@ public function testAssignArrayToObjectWithEmptyMultipleNestedField(): void $converter->assignArrayToObject( $rawDoc, $product, - $metadataCollector->getDocumentMetadata('AcmeBarBundle:Product')->getPropertiesMetadata() + $metadataCollector->getDocumentMetadata(Product::class)->getPropertiesMetadata() ); $this->assertInstanceOf(ObjectIterator::class, $product->relatedCategories); $this->assertSame(0, $product->relatedCategories->count()); } - /** - * @depends testAssignArrayToObjectWithAllFieldsCorrectlySet - */ + #[Depends('testAssignArrayToObjectWithAllFieldsCorrectlySet')] public function testConvertToArray(Product $product): void { $converter = $this->getContainer()->get(DocumentConverter::class); @@ -221,13 +222,13 @@ public function testConvertToDocumentWithSource(): void /** @var Product $product */ $product = $converter->convertToDocument($rawFromEs, 'AcmeBarBundle:Product'); - $this->assertEquals('Foo Product', $product->title); - $this->assertEquals('doc1', $product->id); + $this->assertSame('Foo Product', $product->title); + $this->assertSame('doc1', $product->id); $this->assertInstanceOf(ObjCategory::class, $product->category); $this->assertContainsOnlyInstancesOf(ObjCategory::class, $product->relatedCategories); $this->assertInstanceOf(MLProperty::class, $product->mlInfo); - $this->assertEquals('info in English', $product->mlInfo->getValue('en')); - $this->assertEquals('info in French', $product->mlInfo->getValue('fr')); + $this->assertSame('info in English', $product->mlInfo->getValue('en')); + $this->assertSame('info in French', $product->mlInfo->getValue('fr')); } public function testConvertToDocumentWithFields(): void @@ -259,9 +260,9 @@ public function testConvertToDocumentWithFields(): void /** @var Product $product */ $product = $converter->convertToDocument($rawFromEs, 'AcmeBarBundle:Product'); - $this->assertEquals('Foo Product', $product->title); - $this->assertEquals('doc1', $product->id); + $this->assertSame('Foo Product', $product->title); + $this->assertSame('doc1', $product->id); $this->assertInstanceOf(MLProperty::class, $product->mlInfo); - $this->assertEquals('info in English', $product->mlInfo->getValue('en')); + $this->assertSame('info in English', $product->mlInfo->getValue('en')); } } diff --git a/tests/Functional/Result/DocumentIteratorTest.php b/tests/Functional/Result/DocumentIteratorTest.php index 4cf6783..7bc2fc0 100644 --- a/tests/Functional/Result/DocumentIteratorTest.php +++ b/tests/Functional/Result/DocumentIteratorTest.php @@ -89,7 +89,7 @@ public function testIteration(): void $this->assertCount(3, $iterator); - $this->assertEquals(4, $iterator->getTotalCount()); + $this->assertSame(4, $iterator->getTotalCount()); $iteration = 0; /** @var Product $document */ @@ -105,9 +105,7 @@ public function testIteration(): void $this->assertInstanceOf(Product::class, $document); $this->assertInstanceOf(ObjectIterator::class, $categories); - foreach ($categories as $category) { - $this->assertInstanceOf(ObjCategory::class, $category); - } + $this->assertContainsOnlyInstancesOf(ObjCategory::class, $categories); ++$iteration; } @@ -134,13 +132,13 @@ public function testManualIteration(): void '3rd Product', ]; while ($iterator->valid()) { - $this->assertEquals($i, $iterator->key()); + $this->assertSame($i, $iterator->key()); $this->assertEquals($expected[$i], $iterator->current()->title); $iterator->next(); ++$i; } $iterator->rewind(); - $this->assertEquals($expected[0], $iterator->current()->title); + $this->assertSame($expected[0], $iterator->current()->title); } /** @@ -157,7 +155,7 @@ public function testCurrentWithEmptyIterator(): void } /** - * Make sure null is returned when field doesn't exist or is empty and ObjectIterator otherwise + * Make sure the default value is returned when field doesn't exist or is empty and ObjectIterator otherwise */ public function testNestedObjectIterator(): void { @@ -172,7 +170,7 @@ public function testNestedObjectIterator(): void $this->assertContains($product->id, ['1', '2', '3', '54321']); switch ($product->id) { case '54321': - $this->assertNull($product->relatedCategories); + $this->assertSame([], $product->relatedCategories); break; case '3': $this->assertInstanceOf(ObjectIterator::class, $product->relatedCategories); diff --git a/tests/Functional/Subscriber/EntityTrackerSubscriberTest.php b/tests/Functional/Subscriber/EntityTrackerSubscriberTest.php index 9b658bd..5ecbd68 100644 --- a/tests/Functional/Subscriber/EntityTrackerSubscriberTest.php +++ b/tests/Functional/Subscriber/EntityTrackerSubscriberTest.php @@ -114,7 +114,7 @@ public function testPersistWithSeveralBulkOps(): void $this->assertNull($rawCustomer->id); $this->assertNull($customer->id); $this->assertNull($secondRawCustomer->id); - $this->assertEquals('555', $secondCustomer->id); + $this->assertSame('555', $secondCustomer->id); $imWithAliases->getConnection()->commit(); $backupIm->getConnection()->commit(); @@ -122,8 +122,8 @@ public function testPersistWithSeveralBulkOps(): void $this->assertNull($rawCustomer->id, 'id should not have been set'); $this->assertNotNull($customer->id, 'id should have been set'); $this->assertNull($secondRawCustomer->id, 'id should not have been set'); - $this->assertEquals('555', $secondCustomer->id); - $this->assertEquals(123, $log->id); + $this->assertSame('555', $secondCustomer->id); + $this->assertSame('123', $log->id); // Get the customer from ES by name $finder = $this->getContainer()->get(Finder::class); diff --git a/tests/Unit/Annotation/DocumentTest.php b/tests/Unit/Annotation/DocumentTest.php index 51521d5..f74630e 100644 --- a/tests/Unit/Annotation/DocumentTest.php +++ b/tests/Unit/Annotation/DocumentTest.php @@ -23,7 +23,7 @@ public function testDump(): void 'foo' => 'bar', ]; - $this->assertEquals( + $this->assertSame( [ 'dynamic' => 'strict', 'foo' => 'bar', diff --git a/tests/Unit/Annotation/PropertyTest.php b/tests/Unit/Annotation/PropertyTest.php index ae140a1..b25ceaf 100644 --- a/tests/Unit/Annotation/PropertyTest.php +++ b/tests/Unit/Annotation/PropertyTest.php @@ -29,11 +29,11 @@ public function testDump(): void ]; $type->enumType = 'bar'; - $this->assertEquals( + $this->assertSame( [ + 'type' => 'mytype', 'analyzer' => 'standard', 'foo' => 'bar', - 'type' => 'mytype', ], $type->dump(), 'Properties should be filtered' @@ -74,7 +74,7 @@ public function testDumpML(): void ], ]; - $this->assertEquals( + $this->assertSame( [ 'copy_to' => 'en_all', 'analyzer' => 'en_analyzer', diff --git a/tests/Unit/DTO/BulkQueryItemTest.php b/tests/Unit/DTO/BulkQueryItemTest.php index 03f0161..a7d28c2 100644 --- a/tests/Unit/DTO/BulkQueryItemTest.php +++ b/tests/Unit/DTO/BulkQueryItemTest.php @@ -2,6 +2,7 @@ namespace Sineflow\ElasticsearchBundle\Tests\Unit\DTO; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Sineflow\ElasticsearchBundle\DTO\BulkQueryItem; @@ -13,57 +14,52 @@ class BulkQueryItemTest extends TestCase /** * @return array */ - public function getLinesProvider() + public static function getLinesProvider(): \Iterator { - return [ + yield [ + ['index', 'myindex', ['_id' => '3', 'foo' => 'bar'], false], [ - ['index', 'myindex', ['_id' => '3', 'foo' => 'bar'], false], [ - [ - 'index' => [ - '_index' => 'myindex', - '_id' => 3, - ], - ], - [ - 'foo' => 'bar', + 'index' => [ + '_index' => 'myindex', + '_id' => 3, ], ], + [ + 'foo' => 'bar', + ], ], - + ]; + yield [ + ['create', 'myindex', [], false], [ - ['create', 'myindex', [], false], [ - [ - 'create' => [ - '_index' => 'myindex', - ], + 'create' => [ + '_index' => 'myindex', ], - [], ], + [], ], - + ]; + yield [ + ['update', 'myindex', ['_id' => '3'], 'forcedindex'], [ - ['update', 'myindex', ['_id' => '3'], 'forcedindex'], [ - [ - 'update' => [ - '_index' => 'forcedindex', - '_id' => 3, - ], + 'update' => [ + '_index' => 'forcedindex', + '_id' => 3, ], - [], ], + [], ], - + ]; + yield [ + ['delete', 'myindex', ['_id' => '3'], false], [ - ['delete', 'myindex', ['_id' => '3'], false], [ - [ - 'delete' => [ - '_index' => 'myindex', - '_id' => 3, - ], + 'delete' => [ + '_index' => 'myindex', + '_id' => 3, ], ], ], @@ -73,9 +69,8 @@ public function getLinesProvider() /** * @param array $input * @param array $expected - * - * @dataProvider getLinesProvider */ + #[DataProvider('getLinesProvider')] public function testGetLines($input, $expected): void { $bqi = new BulkQueryItem($input[0], $input[1], $input[2]); diff --git a/tests/Unit/DTO/IndicesToDocumentClassesTest.php b/tests/Unit/DTO/IndicesToDocumentClassesTest.php index d8f56c6..cb4af39 100644 --- a/tests/Unit/DTO/IndicesToDocumentClassesTest.php +++ b/tests/Unit/DTO/IndicesToDocumentClassesTest.php @@ -18,7 +18,7 @@ public function testGetSet(): void $obj = new IndicesToDocumentClasses(); $obj->set('my_real_index', 'App:Entity'); - $this->assertEquals('App:Entity', $obj->get('my_real_index')); + $this->assertSame('App:Entity', $obj->get('my_real_index')); $this->assertThrows(\InvalidArgumentException::class, static function () use ($obj): void { $obj->set(null, 'App:Entity'); @@ -31,8 +31,8 @@ public function testGetSet(): void $obj = new IndicesToDocumentClasses(); $obj->set(null, 'App:Entity'); - $this->assertEquals('App:Entity', $obj->get('second_real_index')); - $this->assertEquals('App:Entity', $obj->get('non_existing_index')); + $this->assertSame('App:Entity', $obj->get('second_real_index')); + $this->assertSame('App:Entity', $obj->get('non_existing_index')); $this->assertThrows(\InvalidArgumentException::class, static function () use ($obj): void { $obj->set('my_real_index', 'App:Entity'); diff --git a/tests/Unit/DependencyInjection/Compiler/AddIndexManagersPassTest.php b/tests/Unit/DependencyInjection/Compiler/AddIndexManagersPassTest.php index 3aaa2ba..b7d844c 100644 --- a/tests/Unit/DependencyInjection/Compiler/AddIndexManagersPassTest.php +++ b/tests/Unit/DependencyInjection/Compiler/AddIndexManagersPassTest.php @@ -60,14 +60,20 @@ public function testProcessWithSeveralManagers(): void default => null, } ); + $matcher = $this->exactly(1); $containerMock - ->expects($this->exactly(1)) + ->expects($matcher) ->method('setDefinition') - ->withConsecutive( - [$this->equalTo('sfes.index.test')] - ) - ->willReturn(new Definition()); + ->willReturnCallback( + function (...$parameters) use ($matcher) { + if (1 === $matcher->numberOfInvocations()) { + $this->assertSame('sfes.index.test', $parameters[0]); + } + + return new Definition(); + } + ); $imPrototypeDefinitionMock = $this->getMockBuilder(Definition::class) ->getMock(); diff --git a/tests/Unit/DependencyInjection/ElasticsearchExtensionTest.php b/tests/Unit/DependencyInjection/ElasticsearchExtensionTest.php index 33fd800..9c6079d 100644 --- a/tests/Unit/DependencyInjection/ElasticsearchExtensionTest.php +++ b/tests/Unit/DependencyInjection/ElasticsearchExtensionTest.php @@ -2,12 +2,14 @@ namespace Sineflow\ElasticsearchBundle\Tests\Unit\DependencyInjection; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Sineflow\ElasticsearchBundle\DependencyInjection\SineflowElasticsearchExtension; use Sineflow\ElasticsearchBundle\Document\Provider\ProviderRegistry; use Sineflow\ElasticsearchBundle\Finder\Finder; use Sineflow\ElasticsearchBundle\Manager\ConnectionManagerRegistry; use Sineflow\ElasticsearchBundle\Manager\IndexManagerRegistry; +use Sineflow\ElasticsearchBundle\Mapping\DocumentAttributeParser; use Sineflow\ElasticsearchBundle\Mapping\DocumentLocator; use Sineflow\ElasticsearchBundle\Mapping\DocumentMetadataCollector; use Sineflow\ElasticsearchBundle\Mapping\DocumentParser; @@ -25,10 +27,12 @@ class ElasticsearchExtensionTest extends TestCase /** * @return array */ - public function getData() + public static function getData() { $parameters = [ 'sineflow_elasticsearch' => [ + 'use_annotations' => false, + 'entity_locations' => [ 'AcmeBarBundle' => [ 'directory' => 'tests/App/fixture/Acme/BarBundle/Document', @@ -168,9 +172,8 @@ public function getData() * @param array $expectedEntityLocations * @param array $expectedConnections * @param array $expectedManagers - * - * @dataProvider getData */ + #[DataProvider('getData')] public function testLoad($parameters, $expectedEntityLocations, $expectedConnections, $expectedManagers): void { $container = new ContainerBuilder(); @@ -209,6 +212,7 @@ public function testLoad($parameters, $expectedEntityLocations, $expectedConnect Finder::class, DocumentLocator::class, DocumentParser::class, + DocumentAttributeParser::class, DocumentMetadataCollector::class, ProfilerDataCollector::class, KnpPaginateQuerySubscriber::class, diff --git a/tests/Unit/Document/MLPropertyTest.php b/tests/Unit/Document/MLPropertyTest.php index 96010dc..951b3e0 100644 --- a/tests/Unit/Document/MLPropertyTest.php +++ b/tests/Unit/Document/MLPropertyTest.php @@ -25,19 +25,19 @@ public function testGetSetValue(): void $mlProperty->setValue('test default', 'default'); - $this->assertEquals( + $this->assertSame( 'test en', $mlProperty->getValue('en'), 'MLProperty does not return required language correctly.' ); - $this->assertEquals( + $this->assertSame( 'test default', $mlProperty->getValue('default'), 'MLProperty does not return default language correctly.' ); - $this->assertEquals( + $this->assertSame( 'test default', $mlProperty->getValue('bg'), 'MLProperty does not return default language if required language is missing.' @@ -54,7 +54,7 @@ public function testGetValues(): void $mlProperty->setValue('test en', 'en'); $mlProperty->setValue('test bg', 'bg'); - $this->assertEquals( + $this->assertSame( [ 'default' => 'test default', 'en' => 'test en', @@ -76,7 +76,7 @@ public function testConstruct(): void 'bg' => 'test bg', ]); - $this->assertEquals( + $this->assertSame( [ 'default' => 'test default', 'en' => 'test en', diff --git a/tests/Unit/Mapping/CaserTest.php b/tests/Unit/Mapping/CaserTest.php index c723179..18834ab 100644 --- a/tests/Unit/Mapping/CaserTest.php +++ b/tests/Unit/Mapping/CaserTest.php @@ -2,12 +2,13 @@ namespace Sineflow\ElasticsearchBundle\Tests\Unit\Mapping; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Sineflow\ElasticsearchBundle\Mapping\Caser; class CaserTest extends TestCase { - public function providerForCamel(): array + public static function providerForCamel(): array { $out = [ ['foo_bar', 'fooBar'], @@ -20,7 +21,7 @@ public function providerForCamel(): array return $out; } - public function providerForSnake(): array + public static function providerForSnake(): array { $out = [ ['FooBar', 'foo_bar'], @@ -35,22 +36,20 @@ public function providerForSnake(): array /** * @param string $input * @param string $expected - * - * @dataProvider providerForCamel */ + #[DataProvider('providerForCamel')] public function testCamel($input, $expected): void { - $this->assertEquals($expected, Caser::camel($input)); + $this->assertSame($expected, Caser::camel($input)); } /** * @param string $input * @param string $expected - * - * @dataProvider providerForSnake */ + #[DataProvider('providerForSnake')] public function testSnake($input, $expected): void { - $this->assertEquals($expected, Caser::snake($input)); + $this->assertSame($expected, Caser::snake($input)); } } diff --git a/tests/Unit/Mapping/DocumentLocatorTest.php b/tests/Unit/Mapping/DocumentLocatorTest.php index 5e5ee18..d0f544a 100644 --- a/tests/Unit/Mapping/DocumentLocatorTest.php +++ b/tests/Unit/Mapping/DocumentLocatorTest.php @@ -2,6 +2,7 @@ namespace Sineflow\ElasticsearchBundle\Tests\Unit\Mapping; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Sineflow\ElasticsearchBundle\Mapping\DocumentLocator; use Sineflow\ElasticsearchBundle\Tests\App\Fixture\Acme\BarBundle\Document\Product; @@ -33,7 +34,7 @@ protected function setUp(): void /** * Data provider */ - public function getTestResolveClassNameDataProvider(): array + public static function getTestResolveClassNameDataProvider(): array { $out = [ [ @@ -109,11 +110,10 @@ public function testGetAllDocumentDirs(): void * * @param string $className * @param string $expectedClassName - * - * @dataProvider getTestResolveClassNameDataProvider */ + #[DataProvider('getTestResolveClassNameDataProvider')] public function testResolveClassName($className, $expectedClassName): void { - $this->assertEquals($expectedClassName, $this->locator->resolveClassName($className)); + $this->assertSame($expectedClassName, $this->locator->resolveClassName($className)); } }