diff --git a/composer.json b/composer.json index b2152e1..dfbfd7d 100755 --- a/composer.json +++ b/composer.json @@ -22,14 +22,14 @@ "psr/log": "^3" }, "require-dev": { - "phpunit/phpunit": "^10.0", + "phpunit/phpunit": "^11.0", "mockery/mockery": "^1.5", "infection/infection": "^0.29", "symplify/easy-coding-standard": "^9.0", - "nunomaduro/larastan": "^2.5", - "phpstan/phpstan-mockery": "^1.1", - "orchestra/testbench": "*", - "dg/bypass-finals": "^1.8" + "phpstan/phpstan-mockery": "^2.0", + "orchestra/testbench": "^10.0", + "dg/bypass-finals": "^1.8", + "larastan/larastan": "^3.0" }, "autoload": { "psr-4": { @@ -50,7 +50,8 @@ }, "config": { "allow-plugins": { - "infection/extension-installer": false + "infection/extension-installer": false, + "php-http/discovery": false } } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 166a3b4..fc3ff23 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -9,8 +9,3 @@ parameters: message: "#^PHPDoc tag @var for variable \\$class has invalid type Laravel\\\\Scout\\\\Searchable\\.$#" count: 1 path: src/Infrastructure/Console/ElasticSearch.php - - - - message: "#^Call to an undefined method JeroenG\\\\Explorer\\\\Domain\\\\IndexManagement\\\\IndexConfigurationInterface\\:\\:getAliasConfiguration\\(\\)\\.$#" - count: 2 - path: tests/Unit/IndexManagement/ElasticIndexConfigurationRepositoryTest.php diff --git a/phpstan.neon b/phpstan.neon index f505fba..b734a2a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,10 +1,11 @@ includes: - - ./vendor/nunomaduro/larastan/extension.neon + - ./vendor/larastan/larastan/extension.neon - ./vendor/phpstan/phpstan-mockery/extension.neon - phpstan-baseline.neon parameters: level: 5 + treatPhpDocTypesAsCertain: false paths: - src - tests diff --git a/src/Infrastructure/Scout/ElasticEngine.php b/src/Infrastructure/Scout/ElasticEngine.php index 68e9cfd..867e235 100644 --- a/src/Infrastructure/Scout/ElasticEngine.php +++ b/src/Infrastructure/Scout/ElasticEngine.php @@ -29,7 +29,7 @@ class ElasticEngine extends Engine private ?LoggerInterface $logger; - private static ?array $lastQuery; + private static array $lastQuery; public function __construct( IndexAdapterInterface $indexAdapter, @@ -56,7 +56,7 @@ public function update($models): void return; } - /** @var Explored $firstModel */ + /** @var Model&Explored $firstModel */ $firstModel = $models->first(); $indexConfiguration = $this->indexConfigurationRepository->findForIndex($firstModel->searchableAs()); diff --git a/src/Infrastructure/Scout/ScoutSearchCommandBuilder.php b/src/Infrastructure/Scout/ScoutSearchCommandBuilder.php index c917942..093528a 100644 --- a/src/Infrastructure/Scout/ScoutSearchCommandBuilder.php +++ b/src/Infrastructure/Scout/ScoutSearchCommandBuilder.php @@ -12,6 +12,7 @@ use JeroenG\Explorer\Domain\Syntax\Compound\QueryType; use JeroenG\Explorer\Domain\Syntax\Invert; use JeroenG\Explorer\Domain\Syntax\MultiMatch; +use JeroenG\Explorer\Domain\Syntax\Range; use JeroenG\Explorer\Domain\Syntax\Sort; use JeroenG\Explorer\Domain\Syntax\Term; use JeroenG\Explorer\Domain\Syntax\Terms; @@ -158,7 +159,7 @@ public function getFields(): array public function getBoolQuery(): BoolQuery { - return $this->boolQuery ?? new BoolQuery(); + return $this->boolQuery; } public function setMust(array $must): void @@ -270,8 +271,32 @@ public function buildQuery(): array $compound->add('must', new MultiMatch($this->query, $this->getDefaultSearchFields())); } - foreach ($this->wheres as $field => $value) { - $compound->add('filter', new Term($field, $value)); + foreach ($this->wheres as $field => $where) { + /** + * From Scout 10 to Scout 11 the where method changed from accepting a field and value to accepting an + * array with field, operator and value. This is to support operators other than =. To support both + * versions we check if the where is an array and has a field key. If not we assume it's the old version + * and convert it to the new version. + */ + if (!is_array($where) || !array_key_exists('field', $where)) { + $where = [ + 'field' => $field, + 'operator' => '=', + 'value' => $where, + ]; + } + + $whereQuery = match ($where['operator']) { + '=' => new Term($where['field'], $where['value']), + '!=' => Invert::query(new Term($where['field'], $where['value'])), + '>' => new Range($where['field'], ['gt' => $where['value']]), + '>=' => new Range($where['field'], ['gte' => $where['value']]), + '<' => new Range($where['field'], ['lt' => $where['value']]), + '<=' => new Range($where['field'], ['lte' => $where['value']]), + default => new Term($where['field'], $where['value']), // Default to term query for unknown operators + }; + + $compound->add('filter', $whereQuery); } foreach ($this->whereIns as $field => $values) { diff --git a/tests/Unit/Domain/Query/QueryProperties/SourceFilterTest.php b/tests/Unit/Domain/Query/QueryProperties/SourceFilterTest.php index 328f8a5..3eb4b5e 100644 --- a/tests/Unit/Domain/Query/QueryProperties/SourceFilterTest.php +++ b/tests/Unit/Domain/Query/QueryProperties/SourceFilterTest.php @@ -10,7 +10,7 @@ final class SourceFilterTest extends TestCase { - /** @dataProvider provideSourceFilterValues */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideSourceFilterValues')] public function test_it_builds(array $expectedValue, SourceFilter $subject): void { Assert::assertSame([ '_source' => $expectedValue ], $subject->build()); diff --git a/tests/Unit/Domain/Query/QueryProperties/TrackTotalHitsTest.php b/tests/Unit/Domain/Query/QueryProperties/TrackTotalHitsTest.php index 4b52c00..0c302be 100644 --- a/tests/Unit/Domain/Query/QueryProperties/TrackTotalHitsTest.php +++ b/tests/Unit/Domain/Query/QueryProperties/TrackTotalHitsTest.php @@ -10,7 +10,7 @@ final class TrackTotalHitsTest extends TestCase { - /** @dataProvider provideTrackTotalHitCounts */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideTrackTotalHitCounts')] public function test_it_tracks_count(int $count): void { Assert::assertSame([ 'track_total_hits' => $count ], TrackTotalHits::count($count)->build()); diff --git a/tests/Unit/Domain/Syntax/SortOrderTest.php b/tests/Unit/Domain/Syntax/SortOrderTest.php index b043e90..86e7556 100644 --- a/tests/Unit/Domain/Syntax/SortOrderTest.php +++ b/tests/Unit/Domain/Syntax/SortOrderTest.php @@ -18,18 +18,14 @@ public function test_it_uses_default_missing_when_creating_sort_order(): void ], $sort->build()); } - /** - * @dataProvider provideSortOrderStrings - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideSortOrderStrings')] public function test_sort_order_can_be_created_from_sort_string(string $expectedResult, string $sortString): void { $subject = SortOrder::fromString($sortString); Assert::assertSame($expectedResult, $subject->build()); } - /** - * @dataProvider provideMissingSortOrderStrings - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideMissingSortOrderStrings')] public function test_sort_order_can_be_created_from_sort_string_and_missing(array $expectedResult, string $sortString, string $missing): void { $subject = SortOrder::for($sortString, $missing); diff --git a/tests/Unit/ElasticClientBuilderTest.php b/tests/Unit/ElasticClientBuilderTest.php index 0b11f9d..f765437 100644 --- a/tests/Unit/ElasticClientBuilderTest.php +++ b/tests/Unit/ElasticClientBuilderTest.php @@ -22,7 +22,7 @@ final class ElasticClientBuilderTest extends MockeryTestCase private const CONNECTION_STRING = 'https://example.com:9222'; - /** @dataProvider provideClientConfigs */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideClientConfigs')] public function test_it_creates_client_with_config(array $config, ClientBuilder $expectedBuilder): void { $configRepository = new ConfigRepository([ 'explorer' => $config ]); diff --git a/tests/Unit/FinderTest.php b/tests/Unit/FinderTest.php index 86f9781..fb3cba5 100644 --- a/tests/Unit/FinderTest.php +++ b/tests/Unit/FinderTest.php @@ -21,6 +21,7 @@ use JeroenG\Explorer\Tests\Support\ClientExpectation; use JeroenG\Explorer\Tests\Support\FakeElasticResponse; use Mockery\Adapter\Phpunit\MockeryTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class FinderTest extends MockeryTestCase { @@ -116,7 +117,11 @@ public function test_it_accepts_must_should_filter_and_where_queries(): void $builder->setMust([new Matching('title', 'Lorem Ipsum')]); $builder->setShould([new Matching('text', 'consectetur adipiscing elit')]); $builder->setFilter([new Term('published', true)]); - $builder->setWheres(['subtitle' => 'Dolor sit amet']); + $builder->setWheres([[ + 'field' => 'subtitle', + 'operator' => '=', + 'value' => 'Dolor sit amet', + ]]); $builder->setWhereIns(['tags' => ['t1', 't2']]); $builder->setQuery('fuzzy search'); @@ -167,7 +172,11 @@ public function test_it_accepts_must_should_filter_and_where_not_in_queries(): v $builder->setMust([new Matching('title', 'Lorem Ipsum')]); $builder->setShould([new Matching('text', 'consectetur adipiscing elit')]); $builder->setFilter([new Term('published', true)]); - $builder->setWheres(['subtitle' => 'Dolor sit amet']); + $builder->setWheres([[ + 'field' => 'subtitle', + 'operator' => '=', + 'value' => 'Dolor sit amet', + ]]); $builder->setWhereNotIns(['tags' => ['t1']]); $builder->setQuery('fuzzy search'); @@ -177,6 +186,56 @@ public function test_it_accepts_must_should_filter_and_where_not_in_queries(): v self::assertCount(2, $results); } + #[DataProvider('scoutWhereOperatorProvider')] + public function test_it_accepts_all_scout_where_operators(string $operator, array $expectedFilter): void + { + $client = ClientExpectation::create(); + $client->expectSearch( + [ + 'index' => self::TEST_INDEX, + 'body' => [ + 'query' => [ + 'bool' => [ + 'must' => [], + 'should' => [], + 'filter' => [$expectedFilter], + ], + ], + ], + ], + FakeElasticResponse::array([ + 'hits' => [ + 'total' => ['value' => 0], + 'hits' => [], + ], + ]) + ); + + $builder = new ScoutSearchCommandBuilder(); + $builder->setIndex(self::TEST_INDEX); + $builder->setWheres([[ + 'field' => 'age', + 'operator' => $operator, + 'value' => 18, + ]]); + + $subject = new Finder($client->getMock(), $builder); + + self::assertCount(0, $subject->find()); + } + + public static function scoutWhereOperatorProvider(): array + { + return [ + '=' => ['=', ['term' => ['age' => ['value' => 18, 'boost' => 1.0]]]], + '!=' => ['!=', ['bool' => ['must_not' => ['term' => ['age' => ['value' => 18, 'boost' => 1.0]]]]]], + '>' => ['>', ['range' => ['age' => ['gt' => 18, 'boost' => 1.0]]]], + '>=' => ['>=', ['range' => ['age' => ['gte' => 18, 'boost' => 1.0]]]], + '<' => ['<', ['range' => ['age' => ['lt' => 18, 'boost' => 1.0]]]], + '<=' => ['<=', ['range' => ['age' => ['lte' => 18, 'boost' => 1.0]]]], + ]; + } + public function test_it_accepts_a_query_for_paginated_search(): void { $client = ClientExpectation::create(); diff --git a/tests/Unit/IndexManagement/ElasticIndexConfigurationRepositoryTest.php b/tests/Unit/IndexManagement/ElasticIndexConfigurationRepositoryTest.php index bd00465..5bc61df 100644 --- a/tests/Unit/IndexManagement/ElasticIndexConfigurationRepositoryTest.php +++ b/tests/Unit/IndexManagement/ElasticIndexConfigurationRepositoryTest.php @@ -120,7 +120,7 @@ public function test_it_throws_on_invalid_model(): void iterator_to_array($repository->getConfigurations())[0] ?? null; } - /** @dataProvider invalidIndices */ + #[\PHPUnit\Framework\Attributes\DataProvider('invalidIndices')] public function test_it_errors_on_invalid_indices($indices, string $error): void { $repository = new ElasticIndexConfigurationRepository($indices); @@ -178,7 +178,6 @@ public function test_it_can_find_a_single_index(): void $config = $repository->findForIndex('encyclopedia'); - self::assertNotNull($config); self::assertEquals($indices['encyclopedia']['properties'], $config->getProperties()); self::assertEquals($indices['encyclopedia']['settings'], $config->getSettings()); self::assertEquals('encyclopedia', $config->getName()); @@ -221,6 +220,7 @@ public function test_it_can_turn_off_pruning_for_aliased_indices(): void { $indices = ['encyclopedia' => ['aliased' => true, 'settings' => [], 'properties' => []]]; $repository = new ElasticIndexConfigurationRepository($indices, false); + /** @var AliasedIndexConfiguration $config */ $config = $repository->findForIndex('encyclopedia'); self::assertInstanceOf(IndexAliasConfiguration::class, $config->getAliasConfiguration()); self::assertFalse($config->getAliasConfiguration()->shouldOldAliasesBePruned()); @@ -237,10 +237,8 @@ public function test_it_can_set_default_settings_for_all_indices(): void $configModel = $repository->findForIndex(':searchable_as:'); $configArray = $repository->findForIndex('encyclopedia'); - self::assertNotNull($configModel); self::assertEquals($defaultSettings, $configModel->getSettings()); - self::assertNotNull($configArray); self::assertEquals($defaultSettings, $configArray->getSettings()); } @@ -255,15 +253,13 @@ public function test_it_can_override_default_index_settings_on_a_per_index_level $configModel = $repository->findForIndex(':searchable_as:'); $configArray = $repository->findForIndex('encyclopedia'); - self::assertNotNull($configModel); self::assertNotEquals($defaultSettings, $configModel->getSettings()); self::assertNotEquals([], $configModel->getSettings()); - self::assertNotNull($configArray); self::assertNotEquals($defaultSettings, $configArray->getSettings()); self::assertEquals($indices['encyclopedia']['settings'], $configArray->getSettings()); } - + public function test_it_throws_exception_if_index_settings_method_not_defined_with_analyser(): void { $defaultSettings = ['index' => ['max_result_window' => 100000]]; @@ -288,11 +284,9 @@ public function test_it_does_not_throw_exception_if_analyser_and_index_settings_ $configModel = $repository->findForIndex(':searchable_as:'); $configArray = $repository->findForIndex('encyclopedia'); - self::assertNotNull($configModel); self::assertNotEquals($defaultSettings, $configModel->getSettings()); self::assertNotEquals([], $configModel->getSettings()); - self::assertNotNull($configArray); self::assertNotEquals($defaultSettings, $configArray->getSettings()); self::assertEquals($indices['encyclopedia']['settings'], $configArray->getSettings()); } diff --git a/tests/Unit/IndexManagement/IndexAliasConfigurationTest.php b/tests/Unit/IndexManagement/IndexAliasConfigurationTest.php index 004d239..06e4be9 100644 --- a/tests/Unit/IndexManagement/IndexAliasConfigurationTest.php +++ b/tests/Unit/IndexManagement/IndexAliasConfigurationTest.php @@ -24,7 +24,7 @@ public function test_prune_property(): void self::assertFalse($doNotPrune->shouldOldAliasesBePruned()); } - /** @dataProvider aliasProvider */ + #[\PHPUnit\Framework\Attributes\DataProvider('aliasProvider')] public function test_it_can_get_the_different_aliases(string $alias, string $method): void { $config = IndexAliasConfiguration::create( diff --git a/tests/Unit/IndexManagement/IndexMappingNormalizerTest.php b/tests/Unit/IndexManagement/IndexMappingNormalizerTest.php index 8be931f..9a254be 100644 --- a/tests/Unit/IndexManagement/IndexMappingNormalizerTest.php +++ b/tests/Unit/IndexManagement/IndexMappingNormalizerTest.php @@ -27,7 +27,6 @@ public function test_it_normalizes_mapping(): void $normalizedMapping = $normalizer->normalize($mapping); - self::assertNotNull($normalizedMapping); self::assertEquals($normalizedMapping['fld'], $normalizedMapping['fld']); self::assertEquals([ 'type' => 'integer' ], $normalizedMapping['other']); diff --git a/tests/Unit/ScoutSearchCommandBuilderTest.php b/tests/Unit/ScoutSearchCommandBuilderTest.php index b3b8b0f..0fcedde 100644 --- a/tests/Unit/ScoutSearchCommandBuilderTest.php +++ b/tests/Unit/ScoutSearchCommandBuilderTest.php @@ -71,7 +71,7 @@ public function test_it_gets_searchable_fields(): void self::assertSame(self::TEST_SEARCHABLE_FIELDS, $subject->getDefaultSearchFields()); } - /** @dataProvider buildCommandProvider */ + #[\PHPUnit\Framework\Attributes\DataProvider('buildCommandProvider')] public function test_it_sets_data_based_on_the_scout_builder(string $method, mixed $expected): void { $builder = Mockery::mock(Builder::class); @@ -87,7 +87,7 @@ public function test_it_sets_data_based_on_the_scout_builder(string $method, mix self::assertSame($expected, $subject->$getter()); } - /** @dataProvider buildCommandProvider */ + #[\PHPUnit\Framework\Attributes\DataProvider('buildCommandProvider')] public function test_it_works_with_setters_and_getters(string $method, mixed $expected): void { $command = new ScoutSearchCommandBuilder(); diff --git a/tests/Unit/Syntax/Compound/BoolQueryTest.php b/tests/Unit/Syntax/Compound/BoolQueryTest.php index ead93e2..8c75f04 100644 --- a/tests/Unit/Syntax/Compound/BoolQueryTest.php +++ b/tests/Unit/Syntax/Compound/BoolQueryTest.php @@ -35,10 +35,8 @@ public function test_it_can_build_an_empty_query(): void self::assertSame($expected, $query); } - /** - * @dataProvider queryTypeProvider - * @param string $type - */ + /** @param string $type */ + #[\PHPUnit\Framework\Attributes\DataProvider('queryTypeProvider')] public function test_it_accepts_different_types_of_queries_by_method(string $type): void { $subject = new BoolQuery(); @@ -53,10 +51,8 @@ public function test_it_accepts_different_types_of_queries_by_method(string $typ self::assertSame($expected, $query['bool'][$type]); } - /** - * @dataProvider queryTypeProvider - * @param string $type - */ + /** @param string $type */ + #[\PHPUnit\Framework\Attributes\DataProvider('queryTypeProvider')] public function test_it_accepts_different_types_of_queries_by_add(string $type): void { $subject = new BoolQuery(); @@ -72,10 +68,10 @@ public function test_it_accepts_different_types_of_queries_by_add(string $type): } /** - * @dataProvider syntaxProvider * @param string $className * @param array $args */ + #[\PHPUnit\Framework\Attributes\DataProvider('syntaxProvider')] public function test_it_accepts_different_types_of_syntax(string $className, array $args): void { $subject = new BoolQuery();