From 1ab398713675913a4b52a89afd1daa10214631b9 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Sun, 15 Jan 2023 15:38:47 +0100 Subject: [PATCH 01/22] Add support for Apache Solr --- .github/workflows/test.yml | 8 + config.subsplit-publish.json | 5 + packages/seal-solr-adapter/.gitattributes | 4 + .../seal-solr-adapter/.github/FUNDING.yml | 1 + packages/seal-solr-adapter/.gitignore | 6 + packages/seal-solr-adapter/LICENSE | 21 + packages/seal-solr-adapter/README.md | 43 + packages/seal-solr-adapter/SolrAdapter.php | 34 + packages/seal-solr-adapter/SolrConnection.php | 157 ++ .../seal-solr-adapter/SolrSchemaManager.php | 81 + .../seal-solr-adapter/Tests/ClientHelper.php | 36 + .../Tests/SolrAdapterTest.php | 18 + .../Tests/SolrConnectionTest.php | 27 + .../Tests/SolrSchemaManagerTest.php | 18 + packages/seal-solr-adapter/composer.json | 55 + packages/seal-solr-adapter/composer.lock | 2267 +++++++++++++++++ packages/seal-solr-adapter/docker-compose.yml | 13 + packages/seal-solr-adapter/phpunit.xml.dist | 30 + 18 files changed, 2824 insertions(+) create mode 100644 packages/seal-solr-adapter/.gitattributes create mode 100644 packages/seal-solr-adapter/.github/FUNDING.yml create mode 100644 packages/seal-solr-adapter/.gitignore create mode 100644 packages/seal-solr-adapter/LICENSE create mode 100644 packages/seal-solr-adapter/README.md create mode 100644 packages/seal-solr-adapter/SolrAdapter.php create mode 100644 packages/seal-solr-adapter/SolrConnection.php create mode 100644 packages/seal-solr-adapter/SolrSchemaManager.php create mode 100644 packages/seal-solr-adapter/Tests/ClientHelper.php create mode 100644 packages/seal-solr-adapter/Tests/SolrAdapterTest.php create mode 100644 packages/seal-solr-adapter/Tests/SolrConnectionTest.php create mode 100644 packages/seal-solr-adapter/Tests/SolrSchemaManagerTest.php create mode 100644 packages/seal-solr-adapter/composer.json create mode 100644 packages/seal-solr-adapter/composer.lock create mode 100644 packages/seal-solr-adapter/docker-compose.yml create mode 100644 packages/seal-solr-adapter/phpunit.xml.dist diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7771c98f..dac30084 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,3 +50,11 @@ jobs: directory: 'packages/seal-algolia-adapter' docker: false secrets: inherit + + solr-adapter: + name: Solr Adapter + uses: ./.github/workflows/callable-test.yml + with: + directory: 'packages/seal-solr-adapter' + docker: false + secrets: inherit diff --git a/config.subsplit-publish.json b/config.subsplit-publish.json index 7aa41490..20e47558 100644 --- a/config.subsplit-publish.json +++ b/config.subsplit-publish.json @@ -15,6 +15,11 @@ "directory": "packages/seal-opensearch-adapter", "target": "git@github.com:schranz-search/seal-opensearch-adapter.git" }, + { + "name": "SEALSolrAdapter", + "directory": "packages/seal-solr-adapter", + "target": "git@github.com:schranz-search/seal-solr-adapter.git" + }, { "name": "SEALMeilisearchAdapter", "directory": "packages/seal-meilisearch-adapter", diff --git a/packages/seal-solr-adapter/.gitattributes b/packages/seal-solr-adapter/.gitattributes new file mode 100644 index 00000000..0e920daf --- /dev/null +++ b/packages/seal-solr-adapter/.gitattributes @@ -0,0 +1,4 @@ +.gitattributes export-ignore +.gitignore export-ignore +composer.lock export-ignore +/Tests export-ignore diff --git a/packages/seal-solr-adapter/.github/FUNDING.yml b/packages/seal-solr-adapter/.github/FUNDING.yml new file mode 100644 index 00000000..dccf62e2 --- /dev/null +++ b/packages/seal-solr-adapter/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [alexander-schranz] diff --git a/packages/seal-solr-adapter/.gitignore b/packages/seal-solr-adapter/.gitignore new file mode 100644 index 00000000..a1b63e48 --- /dev/null +++ b/packages/seal-solr-adapter/.gitignore @@ -0,0 +1,6 @@ +/vendor/ +/composer.phar +/phpunit.xml +/.phpunit.result.cache +/Tests/var +/docker-compose.override.yml diff --git a/packages/seal-solr-adapter/LICENSE b/packages/seal-solr-adapter/LICENSE new file mode 100644 index 00000000..12b80bc5 --- /dev/null +++ b/packages/seal-solr-adapter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Alexander Schranz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/seal-solr-adapter/README.md b/packages/seal-solr-adapter/README.md new file mode 100644 index 00000000..6fc9bfc3 --- /dev/null +++ b/packages/seal-solr-adapter/README.md @@ -0,0 +1,43 @@ +
+ Schranz Search Logo with a Seal on it with a magnifying glass +
+ +

Schranz Search SEAL
Solr Adapter

+ +
+
+ +The `SolrAdapter` write the documents into a [Apache Solr](https://github.com/apache/solr) server instance. + +> **Note**: +> This is part of the `schranz-search/schranz-search` project create issues in the [main repository](https://github.com/schranz-search/schranz-search). + +> **Warning**: +> This project is heavily under development and not ready for production. + +## Installation + +Use [composer](https://getcomposer.org/) for install the package: + +```bash +composer require schranz-search/seal schranz-search/seal-solr-adapter +``` + +## Usage. + +The following code shows how to create an Engine using this Adapter: + +```php +connection = $connection ?? new SolrConnection($client); + $this->schemaManager = $schemaManager ?? new SolrSchemaManager($client); + } + + public function getSchemaManager(): SchemaManagerInterface + { + return $this->schemaManager; + } + + public function getConnection(): ConnectionInterface + { + return $this->connection; + } +} diff --git a/packages/seal-solr-adapter/SolrConnection.php b/packages/seal-solr-adapter/SolrConnection.php new file mode 100644 index 00000000..c97195af --- /dev/null +++ b/packages/seal-solr-adapter/SolrConnection.php @@ -0,0 +1,157 @@ +marshaller = new Marshaller(); + } + + public function save(Index $index, array $document, array $options = []): ?TaskInterface + { + $identifierField = $index->getIdentifierField(); + + /** @var string|null $identifier */ + $identifier = ((string) $document[$identifierField->name]) ?? null; + + $indexResponse = $this->client->index($index->name)->addDocuments([ + $this->marshaller->marshall($index->fields, $document), + ], $identifierField->name); + + if ($indexResponse['status'] !== 'enqueued') { + throw new \RuntimeException('Unexpected error while save document with identifier "' . $identifier . '" into Index "' . $index->name . '".'); + } + + if (true !== ($options['return_slow_promise_result'] ?? false)) { + return null; + } + + return new AsyncTask(function() use ($indexResponse) { + $this->client->waitForTask($indexResponse['taskUid']); + }); + } + + public function delete(Index $index, string $identifier, array $options = []): ?TaskInterface + { + $deleteResponse = $this->client->index($index->name)->deleteDocument($identifier); + + if ($deleteResponse['status'] !== 'enqueued') { + throw new \RuntimeException('Unexpected error while delete document with identifier "' . $identifier . '" from Index "' . $index->name . '".'); + } + + if (true !== ($options['return_slow_promise_result'] ?? false)) { + return null; + } + + return new AsyncTask(function() use ($deleteResponse) { + $this->client->waitForTask($deleteResponse['taskUid']); + }); + } + + public function search(Search $search): Result + { + // optimized single document query + if ( + count($search->indexes) === 1 + && count($search->filters) === 1 + && $search->filters[0] instanceof Condition\IdentifierCondition + && $search->offset === 0 + && $search->limit === 1 + ) { + try { + $data = $this->client->index($search->indexes[\array_key_first($search->indexes)]->name)->getDocument($search->filters[0]->identifier); + } catch (ApiException $e) { + if ($e->httpStatus !== 404) { + throw $e; + } + + return new Result( + $this->hitsToDocuments($search->indexes, []), + 0 + ); + } + + return new Result( + $this->hitsToDocuments($search->indexes, [$data]), + 1 + ); + } + + if (count($search->indexes) !== 1) { + throw new \RuntimeException('Solr does not yet support search multiple indexes: https://github.com/schranz-search/schranz-search/issues/28'); + } + + $index = $search->indexes[\array_key_first($search->indexes)]; + $searchIndex = $this->client->index($index->name); + + $query = null; + $filters = []; + foreach ($search->filters as $filter) { + match (true) { + $filter instanceof Condition\IdentifierCondition => $filters[] = $index->getIdentifierField()->name . ' = "' . $filter->identifier . '"', // TODO escape? + $filter instanceof Condition\SearchCondition => $query = $filter->query, + $filter instanceof Condition\EqualCondition => $filters[] = $filter->field . ' = ' . $filter->value, // TODO escape? + $filter instanceof Condition\NotEqualCondition => $filters[] = $filter->field . ' != ' . $filter->value, // TODO escape? + $filter instanceof Condition\GreaterThanCondition => $filters[] = $filter->field . ' > ' . $filter->value, // TODO escape? + $filter instanceof Condition\GreaterThanEqualCondition => $filters[] = $filter->field . ' >= ' . $filter->value, // TODO escape? + $filter instanceof Condition\LessThanCondition => $filters[] = $filter->field . ' < ' . $filter->value, // TODO escape? + $filter instanceof Condition\LessThanEqualCondition => $filters[] = $filter->field . ' <= ' . $filter->value, // TODO escape? + default => throw new \LogicException($filter::class . ' filter not implemented.'), + }; + } + + $searchParams = []; + if (\count($filters) !== 0) { + $searchParams = ['filter' => \implode(' AND ', $filters)]; + } + + if ($search->offset) { + $searchParams['offset'] = $search->offset; + } + + if ($search->limit) { + $searchParams['limit'] = $search->limit; + } + + foreach ($search->sortBys as $field => $direction) { + $searchParams['sort'][] = $field . ':' . $direction; + } + + $data = $searchIndex->search($query, $searchParams)->toArray(); + + return new Result( + $this->hitsToDocuments($search->indexes, $data['hits']), + $data['totalHits'] ?? $data['estimatedTotalHits'] ?? null, + ); + } + + /** + * @param Index[] $indexes + * @param iterable> $hits + * + * @return \Generator> + */ + private function hitsToDocuments(array $indexes, iterable $hits): \Generator + { + $index = $indexes[\array_key_first($indexes)]; + + foreach ($hits as $hit) { + yield $this->marshaller->unmarshall($index->fields, $hit); + } + } +} diff --git a/packages/seal-solr-adapter/SolrSchemaManager.php b/packages/seal-solr-adapter/SolrSchemaManager.php new file mode 100644 index 00000000..af23b68a --- /dev/null +++ b/packages/seal-solr-adapter/SolrSchemaManager.php @@ -0,0 +1,81 @@ +client->createPing([ + 'collection' => $index->name, + 'path' => '/', + 'core' => 'index', + ]); + + try { + $result = $this->client->ping($ping); + } catch (HttpException $e) { + if ($e->getCode() !== 404) { + throw $e; + } + + return false; + } + + return true; + } + + public function dropIndex(Index $index, array $options = []): ?TaskInterface + { + $deleteIndexResponse = $this->client->deleteIndex($index->name); + + if (true !== ($options['return_slow_promise_result'] ?? false)) { + return null; + } + + return new AsyncTask(function() use ($deleteIndexResponse) { + $this->client->waitForTask($deleteIndexResponse['taskUid']); + }); + } + + public function createIndex(Index $index, array $options = []): ?TaskInterface + { + $query = $this->client->createCoreAdmin(); + + $createAction = $query->createCreate(); + $createAction->setCore($index->name); + $query->setAction($createAction); + + // TODO + $attributes = [ + 'searchableAttributes' => $index->searchableFields, + 'filterableAttributes' => $index->filterableFields, + 'sortableAttributes' => $index->sortableFields, + ]; + + $response = $this->client->coreAdmin($query); + $result = $response->getStatusResult(); + + var_dump($result); + exit; + + if (true !== ($options['return_slow_promise_result'] ?? false)) { + return null; + } + + return new SyncTask(null); + } +} diff --git a/packages/seal-solr-adapter/Tests/ClientHelper.php b/packages/seal-solr-adapter/Tests/ClientHelper.php new file mode 100644 index 00000000..deffab5c --- /dev/null +++ b/packages/seal-solr-adapter/Tests/ClientHelper.php @@ -0,0 +1,36 @@ + array( + 'localhost' => array( + 'host' => $host, + 'port' => $port, + 'path' => '/', + 'core' => 'index', + ) + ) + ]; + + self::$client = new Client($adapter, $eventDispatcher, $options); + } + + return self::$client; + } +} diff --git a/packages/seal-solr-adapter/Tests/SolrAdapterTest.php b/packages/seal-solr-adapter/Tests/SolrAdapterTest.php new file mode 100644 index 00000000..85a43349 --- /dev/null +++ b/packages/seal-solr-adapter/Tests/SolrAdapterTest.php @@ -0,0 +1,18 @@ +markTestSkipped('Not supported by Solr: TODO create issue'); + } +} diff --git a/packages/seal-solr-adapter/Tests/SolrSchemaManagerTest.php b/packages/seal-solr-adapter/Tests/SolrSchemaManagerTest.php new file mode 100644 index 00000000..28d7b223 --- /dev/null +++ b/packages/seal-solr-adapter/Tests/SolrSchemaManagerTest.php @@ -0,0 +1,18 @@ +=7.2.0" + }, + "suggest": { + "fig/event-dispatcher-util": "Provides some useful PSR-14 utilities" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "source": "https://github.com/php-fig/event-dispatcher/tree/master" + }, + "time": "2022-06-29T17:22:39+00:00" + }, + { + "name": "psr/http-client", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "22b2ef5687f43679481615605d7a15c557ce85b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/22b2ef5687f43679481615605d7a15c557ce85b1", + "reference": "22b2ef5687f43679481615605d7a15c557ce85b1", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-09-19T09:12:31+00:00" + }, + { + "name": "psr/http-factory", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "5a4f141ac2e5bc35e615134f127e1833158d2944" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/5a4f141ac2e5bc35e615134f127e1833158d2944", + "reference": "5a4f141ac2e5bc35e615134f127e1833158d2944", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2022-07-14T07:21:53+00:00" + }, + { + "name": "psr/http-message", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "efd67d1dc14a7ef4fc4e518e7dee91c271d524e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/efd67d1dc14a7ef4fc4e518e7dee91c271d524e4", + "reference": "efd67d1dc14a7ef4fc4e518e7dee91c271d524e4", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2019-08-29T13:16:46+00:00" + }, + { + "name": "schranz-search/seal", + "version": "0.1.x-dev", + "dist": { + "type": "path", + "url": "./../seal", + "reference": "8661173761656b8526703bc09db6d6bad7cc0528" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Schranz\\Search\\SEAL\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "autoload-dev": { + "psr-4": { + "Schranz\\Search\\SEAL\\Tests\\": "Tests" + } + }, + "scripts": { + "test": [ + "vendor/bin/phpunit" + ] + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexander Schranz", + "email": "alexander@sulu.io" + } + ], + "description": "Search Engine Abstraction Layer", + "keywords": [ + "algolia", + "elasticsearch", + "meilisearch", + "opensearch", + "schranz-search", + "search", + "search-abstraction", + "search-client" + ], + "transport-options": { + "symlink": true, + "relative": true + } + }, + { + "name": "solarium/solarium", + "version": "6.x-dev", + "source": { + "type": "git", + "url": "https://github.com/solariumphp/solarium.git", + "reference": "b785b8bacb4a5a46a4cf18f5080ead41c6c3c1e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/solariumphp/solarium/zipball/b785b8bacb4a5a46a4cf18f5080ead41c6c3c1e2", + "reference": "b785b8bacb4a5a46a4cf18f5080ead41c6c3c1e2", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.0", + "ext-json": "*", + "php": "^7.3 || ^8.0", + "psr/event-dispatcher": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "symfony/event-dispatcher-contracts": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "escapestudios/symfony2-coding-standard": "^3.11", + "ext-iconv": "*", + "guzzlehttp/guzzle": "^7.2", + "nyholm/psr7": "^1.2", + "php-http/guzzle7-adapter": "^0.1", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5", + "roave/security-advisories": "dev-master", + "symfony/event-dispatcher": "^4.3 || ^5.0 || ^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Solarium\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "See GitHub contributors", + "homepage": "https://github.com/solariumphp/solarium/contributors" + } + ], + "description": "PHP Solr client", + "homepage": "http://www.solarium-project.org", + "keywords": [ + "php", + "search", + "solr" + ], + "support": { + "issues": "https://github.com/solariumphp/solarium/issues", + "source": "https://github.com/solariumphp/solarium/tree/6.x" + }, + "time": "2022-12-16T19:18:52+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "0782b0b52a737a05b4383d0df35a474303cabdae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0782b0b52a737a05b4383d0df35a474303cabdae", + "reference": "0782b0b52a737a05b4383d0df35a474303cabdae", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-25T10:21:52+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "d6eef505a6c46e963e54bf73bb9de43cdea70821" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d6eef505a6c46e963e54bf73bb9de43cdea70821", + "reference": "d6eef505a6c46e963e54bf73bb9de43cdea70821", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.x" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2023-01-04T15:42:40+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "default-branch": true, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2022-03-03T13:19:32+00:00" + }, + { + "name": "nikic/php-parser", + "version": "4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "3182d12b55895a2e71ed6684f9bd5cd13527e94e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3182d12b55895a2e71ed6684f9bd5cd13527e94e", + "reference": "3182d12b55895a2e71ed6684f9bd5cd13527e94e", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "default-branch": true, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/4.x" + }, + "time": "2022-12-14T20:51:15+00:00" + }, + { + "name": "phar-io/manifest", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "36d8a21e851a9512db2b086dc5ac2c61308f0138" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/36d8a21e851a9512db2b086dc5ac2c61308f0138", + "reference": "36d8a21e851a9512db2b086dc5ac2c61308f0138", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2022-02-21T19:55:33+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "3bd773131666fd5457a2a89ff790dfd2b2260ae9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/3bd773131666fd5457a2a89ff790dfd2b2260ae9", + "reference": "3bd773131666fd5457a2a89ff790dfd2b2260ae9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.14", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-01-02T08:36:34+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "38b24367e1b340aa78b96d7cab042942d917bb84" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/38b24367e1b340aa78b96d7cab042942d917bb84", + "reference": "38b24367e1b340aa78b96d7cab042942d917bb84", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-11T16:23:04+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "9f9062f50deb249ff1689f9b86741007bcb2a97a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9f9062f50deb249ff1689f9b86741007bcb2a97a", + "reference": "9f9062f50deb249ff1689f9b86741007bcb2a97a", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2023-01-14T12:50:36+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "b247957a1c8dc81a671770f74b479c0a78a818f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b247957a1c8dc81a671770f74b479c0a78a818f1", + "reference": "b247957a1c8dc81a671770f74b479c0a78a818f1", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:46:14+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "3fade0c8462024d0426a00dc1ad0a2fda0df733f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/3fade0c8462024d0426a00dc1ad0a2fda0df733f", + "reference": "3fade0c8462024d0426a00dc1ad0a2fda0df733f", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-04-14T11:24:33+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:03:37+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-14T08:28:10+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e3a614438af7f71eaa6fc8e406be8a3aa5c34595" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e3a614438af7f71eaa6fc8e406be8a3aa5c34595", + "reference": "e3a614438af7f71eaa6fc8e406be8a3aa5c34595", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-07-30T08:13:09+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "b7a390ae3651f7ba3675d8364bff396e87931554" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/b7a390ae3651f7ba3675d8364bff396e87931554", + "reference": "b7a390ae3651f7ba3675d8364bff396e87931554", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/main" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-06-14T05:05:56+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "4d34b23933f255b0822758a44272222cac593eb4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/4d34b23933f255b0822758a44272222cac593eb4", + "reference": "4d34b23933f255b0822758a44272222cac593eb4", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-10-01T05:56:17+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "6.3.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "514f5a1e78cef6dc0964ff360e9b99975f6e2b85" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/514f5a1e78cef6dc0964ff360e9b99975f6e2b85", + "reference": "514f5a1e78cef6dc0964ff360e9b99975f6e2b85", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/error-handler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/6.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-13T09:23:11+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/packages/seal-solr-adapter/docker-compose.yml b/packages/seal-solr-adapter/docker-compose.yml new file mode 100644 index 00000000..1f867509 --- /dev/null +++ b/packages/seal-solr-adapter/docker-compose.yml @@ -0,0 +1,13 @@ +services: + solr: + image: solr:9 + ports: + - "8983:8983" + volumes: + - solr-data:/var/solr + command: + - solr-precreate + - gettingstarted + +volumes: + solr-data: diff --git a/packages/seal-solr-adapter/phpunit.xml.dist b/packages/seal-solr-adapter/phpunit.xml.dist new file mode 100644 index 00000000..3a5ec930 --- /dev/null +++ b/packages/seal-solr-adapter/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + + + + Tests + + + + + + . + + + + ./Tests + + + From 62851bc840a7f7b553c11c352cc056e01f32fb2e Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Fri, 20 Jan 2023 03:04:12 +0100 Subject: [PATCH 02/22] Switch Docker File to Cloud Mode --- packages/seal-solr-adapter/README.md | 4 ++-- packages/seal-solr-adapter/docker-compose.yml | 20 +++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/seal-solr-adapter/README.md b/packages/seal-solr-adapter/README.md index 6fc9bfc3..adec1252 100644 --- a/packages/seal-solr-adapter/README.md +++ b/packages/seal-solr-adapter/README.md @@ -7,7 +7,7 @@

-The `SolrAdapter` write the documents into a [Apache Solr](https://github.com/apache/solr) server instance. +The `SolrAdapter` write the documents into a [Apache Solr](https://github.com/apache/solr) server instance. The Apache Solr server is running in the [`cloud mode`](https://solr.apache.org/guide/solr/latest/getting-started/tutorial-solrcloud.html) as we require to use collections for indexes. > **Note**: > This is part of the `schranz-search/schranz-search` project create issues in the [main repository](https://github.com/schranz-search/schranz-search). @@ -34,7 +34,7 @@ use Solr\Client; use Schranz\Search\SEAL\Adapter\Solr\SolrAdapter; use Schranz\Search\SEAL\Engine; -$client = new Client('http://127.0.0.1:7700'); +$client = new Client('http://127.0.0.1:8983'); $engine = new Engine( new SolrAdapter($client), diff --git a/packages/seal-solr-adapter/docker-compose.yml b/packages/seal-solr-adapter/docker-compose.yml index 1f867509..26856ae4 100644 --- a/packages/seal-solr-adapter/docker-compose.yml +++ b/packages/seal-solr-adapter/docker-compose.yml @@ -1,13 +1,25 @@ +version: '3' services: solr: - image: solr:9 + image: "solr:9" ports: - "8983:8983" + - "9983:9983" + command: solr -f -cloud + healthcheck: + test: ["CMD-SHELL", "curl --silent --fail localhost:8983 || exit 1"] + interval: 5s + timeout: 5s + retries: 10 volumes: - solr-data:/var/solr - command: - - solr-precreate - - gettingstarted + + zookeeper: + image: "solr:9" + depends_on: + - "solr" + network_mode: "service:solr" + command: bash -c "set -x; export; wait-for-solr.sh; solr zk -z localhost:9983 upconfig -n default -d /opt/solr/server/solr/configsets/_default; tail -f /dev/null" volumes: solr-data: From 7c14518c18cde8b0f15b90ca566e6d2ac2a5e99f Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Fri, 20 Jan 2023 03:11:01 +0100 Subject: [PATCH 03/22] Activate Docker Compose for CI Task --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dac30084..dbc2c737 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,5 +56,5 @@ jobs: uses: ./.github/workflows/callable-test.yml with: directory: 'packages/seal-solr-adapter' - docker: false + docker: true secrets: inherit From a04c67d8e9d1069ccedee6a8d06bceffa6d2ac7a Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Fri, 20 Jan 2023 03:34:20 +0100 Subject: [PATCH 04/22] Create Collections via SolrSchemaManager --- .../seal-solr-adapter/SolrSchemaManager.php | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/packages/seal-solr-adapter/SolrSchemaManager.php b/packages/seal-solr-adapter/SolrSchemaManager.php index af23b68a..97c80627 100644 --- a/packages/seal-solr-adapter/SolrSchemaManager.php +++ b/packages/seal-solr-adapter/SolrSchemaManager.php @@ -9,6 +9,9 @@ use Schranz\Search\SEAL\Task\AsyncTask; use Schranz\Search\SEAL\Task\TaskInterface; use Solarium\Exception\HttpException; +use Solarium\QueryType\Server\Collections\Result\ClusterStatusResult; +use Solarium\QueryType\Server\Collections\Result\CreateResult; +use Solarium\QueryType\Server\Collections\Result\DeleteResult; final class SolrSchemaManager implements SchemaManagerInterface { @@ -19,58 +22,53 @@ public function __construct( public function existIndex(Index $index): bool { - $ping = $this->client->createPing([ - 'collection' => $index->name, - 'path' => '/', - 'core' => 'index', - ]); + $collectionQuery = $this->client->createCollections(); - try { - $result = $this->client->ping($ping); - } catch (HttpException $e) { - if ($e->getCode() !== 404) { - throw $e; - } + $action = $collectionQuery->createClusterStatus(['name' => $index->name]); + $collectionQuery->setAction($action); - return false; - } + /** @var ClusterStatusResult $result */ + $result = $this->client->collections($collectionQuery); - return true; + return $result->getClusterState()->collectionExists($index->name); } public function dropIndex(Index $index, array $options = []): ?TaskInterface { - $deleteIndexResponse = $this->client->deleteIndex($index->name); + $collectionQuery = $this->client->createCollections(); + + $action = $collectionQuery->createDelete(['name' => $index->name]); + $collectionQuery->setAction($action); + + $this->client->collections($collectionQuery); if (true !== ($options['return_slow_promise_result'] ?? false)) { return null; } - return new AsyncTask(function() use ($deleteIndexResponse) { - $this->client->waitForTask($deleteIndexResponse['taskUid']); - }); + return new SyncTask(null); } public function createIndex(Index $index, array $options = []): ?TaskInterface { - $query = $this->client->createCoreAdmin(); + $collectionQuery = $this->client->createCollections(); - $createAction = $query->createCreate(); - $createAction->setCore($index->name); - $query->setAction($createAction); + $action = $collectionQuery->createCreate([ + 'name' => $index->name, + 'numShards' => 1, + ]); + $collectionQuery->setAction($action); + + $this->client->collections($collectionQuery); - // TODO + // TODO create schema fields + /* $attributes = [ 'searchableAttributes' => $index->searchableFields, 'filterableAttributes' => $index->filterableFields, 'sortableAttributes' => $index->sortableFields, ]; - - $response = $this->client->coreAdmin($query); - $result = $response->getStatusResult(); - - var_dump($result); - exit; + */ if (true !== ($options['return_slow_promise_result'] ?? false)) { return null; From 760909ea6e133cda7c0fbcdf2f0becadf3e6ee75 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Fri, 20 Jan 2023 22:28:20 +0100 Subject: [PATCH 05/22] Add basic query and document with ID for Documents test --- packages/seal-solr-adapter/SolrConnection.php | 66 ++++++++++++------- .../seal-solr-adapter/Tests/ClientHelper.php | 4 +- packages/seal/composer.json | 3 +- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/packages/seal-solr-adapter/SolrConnection.php b/packages/seal-solr-adapter/SolrConnection.php index c97195af..53f6b4cb 100644 --- a/packages/seal-solr-adapter/SolrConnection.php +++ b/packages/seal-solr-adapter/SolrConnection.php @@ -2,6 +2,7 @@ namespace Schranz\Search\SEAL\Adapter\Solr; +use Schranz\Search\SEAL\Task\SyncTask; use Solarium\Client; use Schranz\Search\SEAL\Adapter\ConnectionInterface; use Schranz\Search\SEAL\Marshaller\Marshaller; @@ -29,38 +30,47 @@ public function save(Index $index, array $document, array $options = []): ?TaskI /** @var string|null $identifier */ $identifier = ((string) $document[$identifierField->name]) ?? null; - $indexResponse = $this->client->index($index->name)->addDocuments([ - $this->marshaller->marshall($index->fields, $document), - ], $identifierField->name); + $marshalledDocument = $this->marshaller->marshall($index->fields, $document); + $marshalledDocument['id'] = $identifier; - if ($indexResponse['status'] !== 'enqueued') { - throw new \RuntimeException('Unexpected error while save document with identifier "' . $identifier . '" into Index "' . $index->name . '".'); - } + $update = $this->client->createUpdate(); + $indexDocument = $update->createDocument(); + + $indexDocument->id = $identifier; + + $update->addDocuments([$indexDocument]); + $update->addCommit(); + + $this->client->getEndpoint() + ->setCollection($index->name); + + $this->client->update($update); if (true !== ($options['return_slow_promise_result'] ?? false)) { return null; } - return new AsyncTask(function() use ($indexResponse) { - $this->client->waitForTask($indexResponse['taskUid']); - }); + return new SyncTask(null); } public function delete(Index $index, string $identifier, array $options = []): ?TaskInterface { - $deleteResponse = $this->client->index($index->name)->deleteDocument($identifier); + $update = $this->client->createUpdate(); + $query = $update->addDeleteById($identifier); - if ($deleteResponse['status'] !== 'enqueued') { - throw new \RuntimeException('Unexpected error while delete document with identifier "' . $identifier . '" from Index "' . $index->name . '".'); - } + $update->addDeleteQuery($query); + $update->addCommit(); + + $this->client->getEndpoint() + ->setCollection($index->name); + + $this->client->update($update); if (true !== ($options['return_slow_promise_result'] ?? false)) { return null; } - return new AsyncTask(function() use ($deleteResponse) { - $this->client->waitForTask($deleteResponse['taskUid']); - }); + return new SyncTask(null); } public function search(Search $search): Result @@ -73,13 +83,14 @@ public function search(Search $search): Result && $search->offset === 0 && $search->limit === 1 ) { - try { - $data = $this->client->index($search->indexes[\array_key_first($search->indexes)]->name)->getDocument($search->filters[0]->identifier); - } catch (ApiException $e) { - if ($e->httpStatus !== 404) { - throw $e; - } + $this->client->getEndpoint() + ->setCollection($search->indexes[\array_key_first($search->indexes)]->name); + + $query = $this->client->createRealtimeGet(); + $query->addId($search->filters[0]->identifier); + $result = $this->client->realtimeGet($query); + if (!$result->getNumFound()) { return new Result( $this->hitsToDocuments($search->indexes, []), 0 @@ -87,7 +98,7 @@ public function search(Search $search): Result } return new Result( - $this->hitsToDocuments($search->indexes, [$data]), + $this->hitsToDocuments($search->indexes, [$result->getDocument()->getFields()]), 1 ); } @@ -151,6 +162,15 @@ private function hitsToDocuments(array $indexes, iterable $hits): \Generator $index = $indexes[\array_key_first($indexes)]; foreach ($hits as $hit) { + unset($hit['_version_']); + + if ($index->getIdentifierField()->name !== 'id') { + $id = $hit['id']; + unset($hit['id']); + + $hit[$index->getIdentifierField()->name] = $id; + } + yield $this->marshaller->unmarshall($index->fields, $hit); } } diff --git a/packages/seal-solr-adapter/Tests/ClientHelper.php b/packages/seal-solr-adapter/Tests/ClientHelper.php index deffab5c..05391d82 100644 --- a/packages/seal-solr-adapter/Tests/ClientHelper.php +++ b/packages/seal-solr-adapter/Tests/ClientHelper.php @@ -22,9 +22,7 @@ public static function getClient(): Client 'localhost' => array( 'host' => $host, 'port' => $port, - 'path' => '/', - 'core' => 'index', - ) + ), ) ]; diff --git a/packages/seal/composer.json b/packages/seal/composer.json index 0b5b46a0..b9c92ef1 100644 --- a/packages/seal/composer.json +++ b/packages/seal/composer.json @@ -4,10 +4,11 @@ "type": "library", "license": "MIT", "keywords": [ - "schranz-search", + "abstraction", "search", "search-client", "search-abstraction", + "schranz-search", "elasticsearch", "opensearch", "meilisearch", From 13216825b862898b0d08c71dff90d81c102fb8b7 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Tue, 24 Jan 2023 23:35:33 +0100 Subject: [PATCH 06/22] Add basic schema for flat fields --- .../seal-solr-adapter/SolrSchemaManager.php | 96 ++++++++++++++++++- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/packages/seal-solr-adapter/SolrSchemaManager.php b/packages/seal-solr-adapter/SolrSchemaManager.php index 97c80627..27602b69 100644 --- a/packages/seal-solr-adapter/SolrSchemaManager.php +++ b/packages/seal-solr-adapter/SolrSchemaManager.php @@ -5,13 +5,11 @@ use Schranz\Search\SEAL\Task\SyncTask; use Solarium\Client; use Schranz\Search\SEAL\Adapter\SchemaManagerInterface; +use Schranz\Search\SEAL\Schema\Field; use Schranz\Search\SEAL\Schema\Index; -use Schranz\Search\SEAL\Task\AsyncTask; use Schranz\Search\SEAL\Task\TaskInterface; -use Solarium\Exception\HttpException; +use Solarium\Core\Client\Request; use Solarium\QueryType\Server\Collections\Result\ClusterStatusResult; -use Solarium\QueryType\Server\Collections\Result\CreateResult; -use Solarium\QueryType\Server\Collections\Result\DeleteResult; final class SolrSchemaManager implements SchemaManagerInterface { @@ -61,6 +59,19 @@ public function createIndex(Index $index, array $options = []): ?TaskInterface $this->client->collections($collectionQuery); + $requests = $this->createRequests($index->fields); + + foreach ($requests as $request) { + $query = $this->client->createApi([ + 'version' => Request::API_V1, + 'handler' => $index->name.'/schema', + 'method' => Request::METHOD_POST, + 'rawdata' => json_encode($request, \JSON_THROW_ON_ERROR), + ]); + + $this->client->execute($query); + } + // TODO create schema fields /* $attributes = [ @@ -76,4 +87,81 @@ public function createIndex(Index $index, array $options = []): ?TaskInterface return new SyncTask(null); } + + /** + * @param Field\AbstractField[] $fields + * + * @return array + */ + private function createRequests(array $fields): array + { + $requests = []; + + foreach ($fields as $name => $field) { + match (true) { + $field instanceof Field\IdentifierField => null, + $field instanceof Field\TextField => $requests[$name] = [ + 'add-field' => [ + 'name' => $name, + 'type' => 'string', + 'indexed' => $field->searchable, + 'docValues' => $field->filterable || $field->sortable, + 'stored' => false, + 'multiValued' => $field->multiple, + ], + ], + $field instanceof Field\BooleanField => $requests[$name] = [ + 'add-field' => [ + 'name' => $name, + 'type' => 'bool', + 'indexed' => $field->searchable, + 'docValues' => $field->filterable || $field->sortable, + 'stored' => false, + 'multiValued' => $field->multiple, + ], + ], + $field instanceof Field\DateTimeField => $requests[$name] = [ + 'add-field' => [ + 'name' => $name, + 'type' => 'pdate', + 'indexed' => $field->searchable, + 'docValues' => $field->filterable || $field->sortable, + 'stored' => false, + 'multiValued' => $field->multiple, + ], + ], + $field instanceof Field\IntegerField => $requests[$name] = [ + 'add-field' => [ + 'name' => $name, + 'type' => 'pint', + 'indexed' => $field->searchable, + 'docValues' => $field->filterable || $field->sortable, + 'stored' => false, + 'multiValued' => $field->multiple, + ], + ], + $field instanceof Field\FloatField => $requests[$name] = [ + 'add-field' => [ + 'name' => $name, + 'type' => 'pfloat', + 'indexed' => $field->searchable, + 'docValues' => $field->filterable || $field->sortable, + 'stored' => false, + 'multiValued' => $field->multiple, + ], + ], + default => null, + /* + $field instanceof Field\ObjectField => $fields[$name] = [ + 'type' => 'object', + 'properties' => $this->createPropertiesMapping($field->fields), + ], + $field instanceof Field\TypedField => $fields = \array_replace($properties, $this->createTypedFieldMapping($name, $field)), + default => throw new \RuntimeException(sprintf('Field type "%s" is not supported.', get_class($field))), + */ + }; + } + + return $requests; + } } From b21441ce37de8acdc80802b2174edc3bd423ded2 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 25 Jan 2023 00:51:56 +0100 Subject: [PATCH 07/22] Create own configset for every collection --- packages/seal-solr-adapter/README.md | 17 ++- .../seal-solr-adapter/SolrSchemaManager.php | 115 ++++++++++-------- .../seal-solr-adapter/Tests/ClientHelper.php | 8 +- packages/seal-solr-adapter/docker-compose.yml | 4 + 4 files changed, 85 insertions(+), 59 deletions(-) diff --git a/packages/seal-solr-adapter/README.md b/packages/seal-solr-adapter/README.md index adec1252..cf82bd3e 100644 --- a/packages/seal-solr-adapter/README.md +++ b/packages/seal-solr-adapter/README.md @@ -31,10 +31,23 @@ The following code shows how to create an Engine using this Adapter: [ + 'localhost' => [ + 'host' => '127.0.0.1', + 'port' => '8983', + // authenticated required for configset api https://solr.apache.org/guide/8_9/configsets-api.html + // alternative set solr.disableConfigSetsCreateAuthChecks=true in your server setup + 'username' => 'solr', + 'password' => 'SolrRocks', + ], + ] +]); $engine = new Engine( new SolrAdapter($client), diff --git a/packages/seal-solr-adapter/SolrSchemaManager.php b/packages/seal-solr-adapter/SolrSchemaManager.php index 27602b69..ca501426 100644 --- a/packages/seal-solr-adapter/SolrSchemaManager.php +++ b/packages/seal-solr-adapter/SolrSchemaManager.php @@ -40,6 +40,13 @@ public function dropIndex(Index $index, array $options = []): ?TaskInterface $this->client->collections($collectionQuery); + $configsetQuery = $this->client->createConfigsets(); + + $action = $configsetQuery->createDelete() + ->setName($index->name); + $configsetQuery->setAction($action); + $this->client->configsets($configsetQuery); + if (true !== ($options['return_slow_promise_result'] ?? false)) { return null; } @@ -49,24 +56,36 @@ public function dropIndex(Index $index, array $options = []): ?TaskInterface public function createIndex(Index $index, array $options = []): ?TaskInterface { + $configsetQuery = $this->client->createConfigsets(); + + $action = $configsetQuery->createCreate() + ->setName($index->name) + ->setBaseConfigSet('_default'); + $configsetQuery->setAction($action); + + $this->client->configsets($configsetQuery); + $collectionQuery = $this->client->createCollections(); $action = $collectionQuery->createCreate([ 'name' => $index->name, 'numShards' => 1, + 'collection.configName' => $index->name, ]); $collectionQuery->setAction($action); $this->client->collections($collectionQuery); - $requests = $this->createRequests($index->fields); + $indexFields = $this->createIndexFields($index->fields); - foreach ($requests as $request) { + foreach ($indexFields as $indexField) { $query = $this->client->createApi([ 'version' => Request::API_V1, 'handler' => $index->name.'/schema', 'method' => Request::METHOD_POST, - 'rawdata' => json_encode($request, \JSON_THROW_ON_ERROR), + 'rawdata' => json_encode([ + 'add-field' => $indexField, + ], \JSON_THROW_ON_ERROR), ]); $this->client->execute($query); @@ -93,65 +112,55 @@ public function createIndex(Index $index, array $options = []): ?TaskInterface * * @return array */ - private function createRequests(array $fields): array + private function createIndexFields(array $fields): array { - $requests = []; + $indexFields = []; foreach ($fields as $name => $field) { match (true) { - $field instanceof Field\IdentifierField => null, - $field instanceof Field\TextField => $requests[$name] = [ - 'add-field' => [ - 'name' => $name, - 'type' => 'string', - 'indexed' => $field->searchable, - 'docValues' => $field->filterable || $field->sortable, - 'stored' => false, - 'multiValued' => $field->multiple, - ], + $field instanceof Field\IdentifierField => null, // TODO define primary field + $field instanceof Field\TextField => $indexFields[$name] = [ + 'name' => $name, + 'type' => 'string', + 'indexed' => $field->searchable, + 'docValues' => $field->filterable || $field->sortable, + 'stored' => false, + 'multiValued' => $field->multiple, ], - $field instanceof Field\BooleanField => $requests[$name] = [ - 'add-field' => [ - 'name' => $name, - 'type' => 'bool', - 'indexed' => $field->searchable, - 'docValues' => $field->filterable || $field->sortable, - 'stored' => false, - 'multiValued' => $field->multiple, - ], + $field instanceof Field\BooleanField => $indexFields[$name] = [ + 'name' => $name, + 'type' => 'bool', + 'indexed' => $field->searchable, + 'docValues' => $field->filterable || $field->sortable, + 'stored' => false, + 'multiValued' => $field->multiple, ], - $field instanceof Field\DateTimeField => $requests[$name] = [ - 'add-field' => [ - 'name' => $name, - 'type' => 'pdate', - 'indexed' => $field->searchable, - 'docValues' => $field->filterable || $field->sortable, - 'stored' => false, - 'multiValued' => $field->multiple, - ], + $field instanceof Field\DateTimeField => $indexFields[$name] = [ + 'name' => $name, + 'type' => 'pdate', + 'indexed' => $field->searchable, + 'docValues' => $field->filterable || $field->sortable, + 'stored' => false, + 'multiValued' => $field->multiple, ], - $field instanceof Field\IntegerField => $requests[$name] = [ - 'add-field' => [ - 'name' => $name, - 'type' => 'pint', - 'indexed' => $field->searchable, - 'docValues' => $field->filterable || $field->sortable, - 'stored' => false, - 'multiValued' => $field->multiple, - ], + $field instanceof Field\IntegerField => $indexFields[$name] = [ + 'name' => $name, + 'type' => 'pint', + 'indexed' => $field->searchable, + 'docValues' => $field->filterable || $field->sortable, + 'stored' => false, + 'multiValued' => $field->multiple, ], - $field instanceof Field\FloatField => $requests[$name] = [ - 'add-field' => [ - 'name' => $name, - 'type' => 'pfloat', - 'indexed' => $field->searchable, - 'docValues' => $field->filterable || $field->sortable, - 'stored' => false, - 'multiValued' => $field->multiple, - ], + $field instanceof Field\FloatField => $indexFields[$name] = [ + 'name' => $name, + 'type' => 'pfloat', + 'indexed' => $field->searchable, + 'docValues' => $field->filterable || $field->sortable, + 'stored' => false, + 'multiValued' => $field->multiple, ], default => null, - /* + /* TODO implement ObjectField and TypedField $field instanceof Field\ObjectField => $fields[$name] = [ 'type' => 'object', 'properties' => $this->createPropertiesMapping($field->fields), @@ -162,6 +171,6 @@ private function createRequests(array $fields): array }; } - return $requests; + return $indexFields; } } diff --git a/packages/seal-solr-adapter/Tests/ClientHelper.php b/packages/seal-solr-adapter/Tests/ClientHelper.php index 05391d82..fda46d81 100644 --- a/packages/seal-solr-adapter/Tests/ClientHelper.php +++ b/packages/seal-solr-adapter/Tests/ClientHelper.php @@ -18,12 +18,12 @@ public static function getClient(): Client $adapter = new Curl(); $eventDispatcher = new EventDispatcher(); $options = [ - 'endpoint' => array( - 'localhost' => array( + 'endpoint' => [ + 'localhost' => [ 'host' => $host, 'port' => $port, - ), - ) + ], + ] ]; self::$client = new Client($adapter, $eventDispatcher, $options); diff --git a/packages/seal-solr-adapter/docker-compose.yml b/packages/seal-solr-adapter/docker-compose.yml index 26856ae4..dab6e3eb 100644 --- a/packages/seal-solr-adapter/docker-compose.yml +++ b/packages/seal-solr-adapter/docker-compose.yml @@ -11,6 +11,8 @@ services: interval: 5s timeout: 5s retries: 10 + environment: + SOLR_OPTS: '-Dsolr.disableConfigSetsCreateAuthChecks=true' volumes: - solr-data:/var/solr @@ -19,6 +21,8 @@ services: depends_on: - "solr" network_mode: "service:solr" + environment: + SOLR_OPTS: '-Dsolr.disableConfigSetsCreateAuthChecks=true' command: bash -c "set -x; export; wait-for-solr.sh; solr zk -z localhost:9983 upconfig -n default -d /opt/solr/server/solr/configsets/_default; tail -f /dev/null" volumes: From 2afad8af82d612f9c04b93ab84dd2f06d69ba861 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Tue, 31 Jan 2023 23:34:23 +0100 Subject: [PATCH 08/22] Create object and typed fields based on FlattenMarshaller --- .../seal-solr-adapter/SolrSchemaManager.php | 31 +++++++++++++------ .../seal/Marshaller/FlattenMarshaller.php | 2 ++ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/seal-solr-adapter/SolrSchemaManager.php b/packages/seal-solr-adapter/SolrSchemaManager.php index ca501426..ab1e719f 100644 --- a/packages/seal-solr-adapter/SolrSchemaManager.php +++ b/packages/seal-solr-adapter/SolrSchemaManager.php @@ -112,11 +112,13 @@ public function createIndex(Index $index, array $options = []): ?TaskInterface * * @return array */ - private function createIndexFields(array $fields): array + private function createIndexFields(array $fields, string $prefix = ''): array { $indexFields = []; foreach ($fields as $name => $field) { + $name = $prefix . $name; + match (true) { $field instanceof Field\IdentifierField => null, // TODO define primary field $field instanceof Field\TextField => $indexFields[$name] = [ @@ -159,18 +161,29 @@ private function createIndexFields(array $fields): array 'stored' => false, 'multiValued' => $field->multiple, ], - default => null, - /* TODO implement ObjectField and TypedField - $field instanceof Field\ObjectField => $fields[$name] = [ - 'type' => 'object', - 'properties' => $this->createPropertiesMapping($field->fields), - ], - $field instanceof Field\TypedField => $fields = \array_replace($properties, $this->createTypedFieldMapping($name, $field)), + $field instanceof Field\ObjectField => $indexFields = \array_replace($indexFields, $this->createIndexFields($field->fields, $name . '.')), + $field instanceof Field\TypedField => array_map(function($fields, $type) use ($name, &$indexFields) { + $indexFields = \array_replace($indexFields, $this->createIndexFields($fields, $name . '.')); + }, $field->types, array_keys($field->types)), default => throw new \RuntimeException(sprintf('Field type "%s" is not supported.', get_class($field))), - */ }; } + if ($prefix === null) { + $indexFields['_rawDocument'] = [ + 'name' => '_rawDocument', + 'type' => 'string', + 'indexed' => false, + 'docValues' => false, + 'stored' => false, + 'multiValued' => false, + ]; + } + return $indexFields; } + + private function createIndexObjectFields(int|string $name, Field\ObjectField $field) + { + } } diff --git a/packages/seal/Marshaller/FlattenMarshaller.php b/packages/seal/Marshaller/FlattenMarshaller.php index eb84c9a6..4a3e7d00 100644 --- a/packages/seal/Marshaller/FlattenMarshaller.php +++ b/packages/seal/Marshaller/FlattenMarshaller.php @@ -8,6 +8,8 @@ * @internal This class currently in discussion to be open for all adapters. * * The FlattenMarshaller will flatten all fields and save original document under a `_rawDocument` field. + * The FlattenMarshaller should only be used when the Search Engine does not support nested objects and so + * the Marshaller should used in many cases instead. */ final class FlattenMarshaller { From b2990644b8029c56f8f55ed5e11c74ee2594878a Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Tue, 31 Jan 2023 23:59:41 +0100 Subject: [PATCH 09/22] Remove unneeded FlattenMarshaller methods --- .../seal/Marshaller/FlattenMarshaller.php | 174 ------------------ 1 file changed, 174 deletions(-) diff --git a/packages/seal/Marshaller/FlattenMarshaller.php b/packages/seal/Marshaller/FlattenMarshaller.php index 4a3e7d00..6cb2ce60 100644 --- a/packages/seal/Marshaller/FlattenMarshaller.php +++ b/packages/seal/Marshaller/FlattenMarshaller.php @@ -182,178 +182,4 @@ private function flattenTyped(string $name, array $raw, Field\TypedField $field, return $keepOrderRaw; } - - private function unflatten(array $fields, array $raw, bool $isParentMultiple = false) - { - foreach ($fields as $name => $field) { - match (true) { - $field instanceof Field\ObjectField => $raw = $this->unflattenObject($name, $raw, $field, $isParentMultiple), - $field instanceof Field\TypedField => $raw = $this->unflattenTyped($name, $raw, $field, $isParentMultiple), - default => null, - }; - } - - return $raw; - } - - - /** - * @param array $raw - * - * @return array - */ - private function unflattenObject(string $name, array $raw, Field\ObjectField $field, bool $rootIsParentMultiple) - { - $rawFields = []; - $firstKey = null; - foreach ($raw as $key => $value) { - if (str_starts_with($key, $name . '.')) { - [, $subName] = \explode('.', $key, 2); - - $rawFields[$subName] = $value; - if ($firstKey === null) { - $firstKey = $key; - - continue; - } - - unset($raw[$key]); - } - } - - $newRawData = []; - if (!$field->multiple) { - $newRawData = $this->unflatten($field->fields, $rawFields, $rootIsParentMultiple); - } else { - foreach ($this->unflattenValue($rawFields) as $key => $value) { - $newRawData[$key] = $this->unflatten($field->fields, $value, true); - } - } - - $keepOrderRaw = []; - foreach ($raw as $key => $value) { - if ($key === $firstKey) { - $keepOrderRaw[$name] = $newRawData; - - continue; - } - - $keepOrderRaw[$key] = $value; - } - - return $keepOrderRaw; - } - - /** - * @param array $raw - * - * @return array - */ - private function unflattenTyped(string $name, array $raw, Field\TypedField $field, bool $rootIsParentMultiple) - { - $rawFields = []; - $firstKey = null; - foreach ($raw as $key => $value) { - if (str_starts_with($key, $name . '.')) { - [, $type, $subName] = \explode('.', $key, 3); - - $rawFields[$type][$subName] = $value; - if ($firstKey === null) { - $firstKey = $key; - - continue; - } - - unset($raw[$key]); - } - } - - $newRawData = []; - foreach ($rawFields as $type => $object) { - $fieldTypes = $field->types[$type]; - - if (!$field->multiple) { - $newRawData[$type] = $this->unflatten($fieldTypes, $object, $rootIsParentMultiple); - - continue; - } - - foreach ($this->unflattenValue($object) as $key => $value) { - $newRawData[$type][$key] = $this->unflatten($fieldTypes, $value, true); - } - } - - $keepOrderRaw = []; - foreach ($raw as $key => $value) { - if ($key === $firstKey) { - $keepOrderRaw[$name] = $newRawData; - - continue; - } - - $keepOrderRaw[$key] = $value; - } - - return $keepOrderRaw; - } - - /** - * @param array $object - * - * @return array - */ - private function unflattenValue(array $object): array - { - $subRawData = []; - foreach ($object as $key => $value) { - if (str_ends_with($key, '._originalLength')) { - continue; - } - - if (isset($object[$key . '._originalLength'])) { - $lengths = []; - $innerOriginalLength = $key . '._originalLength'; - - static $c = 0; - ++$c; - - while (isset($object[$innerOriginalLength . '._originalLength'])) { - array_unshift($lengths, $innerOriginalLength); - $innerOriginalLength .= '._originalLength'; - } - - foreach ($object[$innerOriginalLength] as $subKey => $subValue) { - if ($subValue === 0) { - continue; - } - - foreach ($lengths as $length) { - $counts = array_splice($object[$length], 0, $subValue); - $subRawData[$subKey][$length] = $counts; - - $subValue = array_reduce($counts, function($carry, $item) { - $carry += $item; - return $carry; - }, 0); - } - - $subRawData[$subKey][$key] = array_splice($object[$key], 0, $subValue); - } - - continue; - } - - if (!is_array($value)) { - continue; - } - - foreach ($value as $subKey => $subValue) { - $subRawData[$subKey][$key] = $subValue; - } - } - - $subRawData = array_values($subRawData); - - return $subRawData; - } } From 8be8c4dd702ded9c2c6de308f923a3a364f11fa5 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Tue, 31 Jan 2023 23:59:57 +0100 Subject: [PATCH 10/22] Add Basic Find, Delete and Add Document implementation --- packages/seal-solr-adapter/SolrConnection.php | 13 ++++------- .../seal-solr-adapter/SolrSchemaManager.php | 23 ++++++++++--------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/seal-solr-adapter/SolrConnection.php b/packages/seal-solr-adapter/SolrConnection.php index 53f6b4cb..ba6bb344 100644 --- a/packages/seal-solr-adapter/SolrConnection.php +++ b/packages/seal-solr-adapter/SolrConnection.php @@ -2,6 +2,7 @@ namespace Schranz\Search\SEAL\Adapter\Solr; +use Schranz\Search\SEAL\Marshaller\FlattenMarshaller; use Schranz\Search\SEAL\Task\SyncTask; use Solarium\Client; use Schranz\Search\SEAL\Adapter\ConnectionInterface; @@ -15,12 +16,12 @@ final class SolrConnection implements ConnectionInterface { - private Marshaller $marshaller; + private FlattenMarshaller $marshaller; public function __construct( private readonly Client $client, ) { - $this->marshaller = new Marshaller(); + $this->marshaller = new FlattenMarshaller(); } public function save(Index $index, array $document, array $options = []): ?TaskInterface @@ -34,9 +35,7 @@ public function save(Index $index, array $document, array $options = []): ?TaskI $marshalledDocument['id'] = $identifier; $update = $this->client->createUpdate(); - $indexDocument = $update->createDocument(); - - $indexDocument->id = $identifier; + $indexDocument = $update->createDocument($marshalledDocument); $update->addDocuments([$indexDocument]); $update->addCommit(); @@ -56,9 +55,7 @@ public function save(Index $index, array $document, array $options = []): ?TaskI public function delete(Index $index, string $identifier, array $options = []): ?TaskInterface { $update = $this->client->createUpdate(); - $query = $update->addDeleteById($identifier); - - $update->addDeleteQuery($query); + $update->addDeleteById($identifier); $update->addCommit(); $this->client->getEndpoint() diff --git a/packages/seal-solr-adapter/SolrSchemaManager.php b/packages/seal-solr-adapter/SolrSchemaManager.php index ab1e719f..1dfc3cdd 100644 --- a/packages/seal-solr-adapter/SolrSchemaManager.php +++ b/packages/seal-solr-adapter/SolrSchemaManager.php @@ -112,12 +112,13 @@ public function createIndex(Index $index, array $options = []): ?TaskInterface * * @return array */ - private function createIndexFields(array $fields, string $prefix = ''): array + private function createIndexFields(array $fields, string $prefix = '', bool $isParentMultiple = false): array { $indexFields = []; foreach ($fields as $name => $field) { $name = $prefix . $name; + $isMultiple = $isParentMultiple || $field->multiple; match (true) { $field instanceof Field\IdentifierField => null, // TODO define primary field @@ -127,7 +128,7 @@ private function createIndexFields(array $fields, string $prefix = ''): array 'indexed' => $field->searchable, 'docValues' => $field->filterable || $field->sortable, 'stored' => false, - 'multiValued' => $field->multiple, + 'multiValued' => $isMultiple, ], $field instanceof Field\BooleanField => $indexFields[$name] = [ 'name' => $name, @@ -135,7 +136,7 @@ private function createIndexFields(array $fields, string $prefix = ''): array 'indexed' => $field->searchable, 'docValues' => $field->filterable || $field->sortable, 'stored' => false, - 'multiValued' => $field->multiple, + 'multiValued' => $isMultiple, ], $field instanceof Field\DateTimeField => $indexFields[$name] = [ 'name' => $name, @@ -143,7 +144,7 @@ private function createIndexFields(array $fields, string $prefix = ''): array 'indexed' => $field->searchable, 'docValues' => $field->filterable || $field->sortable, 'stored' => false, - 'multiValued' => $field->multiple, + 'multiValued' => $isMultiple, ], $field instanceof Field\IntegerField => $indexFields[$name] = [ 'name' => $name, @@ -151,7 +152,7 @@ private function createIndexFields(array $fields, string $prefix = ''): array 'indexed' => $field->searchable, 'docValues' => $field->filterable || $field->sortable, 'stored' => false, - 'multiValued' => $field->multiple, + 'multiValued' => $isMultiple, ], $field instanceof Field\FloatField => $indexFields[$name] = [ 'name' => $name, @@ -159,23 +160,23 @@ private function createIndexFields(array $fields, string $prefix = ''): array 'indexed' => $field->searchable, 'docValues' => $field->filterable || $field->sortable, 'stored' => false, - 'multiValued' => $field->multiple, + 'multiValued' => $isMultiple, ], - $field instanceof Field\ObjectField => $indexFields = \array_replace($indexFields, $this->createIndexFields($field->fields, $name . '.')), - $field instanceof Field\TypedField => array_map(function($fields, $type) use ($name, &$indexFields) { - $indexFields = \array_replace($indexFields, $this->createIndexFields($fields, $name . '.')); + $field instanceof Field\ObjectField => $indexFields = \array_replace($indexFields, $this->createIndexFields($field->fields, $name . '.', $isMultiple)), + $field instanceof Field\TypedField => array_map(function($fields, $type) use ($name, &$indexFields, $isMultiple) { + $indexFields = \array_replace($indexFields, $this->createIndexFields($fields, $name . '.' . $type . '.', $isMultiple)); }, $field->types, array_keys($field->types)), default => throw new \RuntimeException(sprintf('Field type "%s" is not supported.', get_class($field))), }; } - if ($prefix === null) { + if ($prefix === '') { $indexFields['_rawDocument'] = [ 'name' => '_rawDocument', 'type' => 'string', 'indexed' => false, 'docValues' => false, - 'stored' => false, + 'stored' => true, 'multiValued' => false, ]; } From 8124f06e9471ac6b4711e289ae1b45ff42454a14 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 1 Feb 2023 00:00:49 +0100 Subject: [PATCH 11/22] Rename _rawDocument to _source to match elasticsearch behaviour --- packages/seal-solr-adapter/SolrSchemaManager.php | 4 ++-- packages/seal/Marshaller/FlattenMarshaller.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/seal-solr-adapter/SolrSchemaManager.php b/packages/seal-solr-adapter/SolrSchemaManager.php index 1dfc3cdd..3b5b4ce5 100644 --- a/packages/seal-solr-adapter/SolrSchemaManager.php +++ b/packages/seal-solr-adapter/SolrSchemaManager.php @@ -171,8 +171,8 @@ private function createIndexFields(array $fields, string $prefix = '', bool $isP } if ($prefix === '') { - $indexFields['_rawDocument'] = [ - 'name' => '_rawDocument', + $indexFields['_source'] = [ + 'name' => '_source', 'type' => 'string', 'indexed' => false, 'docValues' => false, diff --git a/packages/seal/Marshaller/FlattenMarshaller.php b/packages/seal/Marshaller/FlattenMarshaller.php index 6cb2ce60..5f8c5ca0 100644 --- a/packages/seal/Marshaller/FlattenMarshaller.php +++ b/packages/seal/Marshaller/FlattenMarshaller.php @@ -7,7 +7,7 @@ /** * @internal This class currently in discussion to be open for all adapters. * - * The FlattenMarshaller will flatten all fields and save original document under a `_rawDocument` field. + * The FlattenMarshaller will flatten all fields and save original document under a `_source` field. * The FlattenMarshaller should only be used when the Search Engine does not support nested objects and so * the Marshaller should used in many cases instead. */ @@ -30,7 +30,7 @@ public function __construct( public function marshall(array $fields, array $document): array { $flattenDocument = $this->flatten($fields, $document); - $flattenDocument['_rawDocument'] = \json_encode($document, \JSON_THROW_ON_ERROR); + $flattenDocument['_source'] = \json_encode($document, \JSON_THROW_ON_ERROR); return $flattenDocument; } @@ -43,7 +43,7 @@ public function marshall(array $fields, array $document): array */ public function unmarshall(array $fields, array $raw): array { - return \json_decode($raw['_rawDocument'], true, flags: \JSON_THROW_ON_ERROR); + return \json_decode($raw['_source'], true, flags: \JSON_THROW_ON_ERROR); } /** From 0d9eaab9995e6ca0311f62fac102dcc55835c34d Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 1 Feb 2023 00:01:25 +0100 Subject: [PATCH 12/22] Remove not longer required todos --- packages/seal-solr-adapter/SolrSchemaManager.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/seal-solr-adapter/SolrSchemaManager.php b/packages/seal-solr-adapter/SolrSchemaManager.php index 3b5b4ce5..99ff2cf1 100644 --- a/packages/seal-solr-adapter/SolrSchemaManager.php +++ b/packages/seal-solr-adapter/SolrSchemaManager.php @@ -91,15 +91,6 @@ public function createIndex(Index $index, array $options = []): ?TaskInterface $this->client->execute($query); } - // TODO create schema fields - /* - $attributes = [ - 'searchableAttributes' => $index->searchableFields, - 'filterableAttributes' => $index->filterableFields, - 'sortableAttributes' => $index->sortableFields, - ]; - */ - if (true !== ($options['return_slow_promise_result'] ?? false)) { return null; } From ad275126952759ab2af439cd6df9923ad70dda72 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 1 Feb 2023 00:08:49 +0100 Subject: [PATCH 13/22] Fix core tests --- packages/seal/Tests/Marshaller/FlattenMarshallerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/seal/Tests/Marshaller/FlattenMarshallerTest.php b/packages/seal/Tests/Marshaller/FlattenMarshallerTest.php index 7b4dc4e0..8b1b27fe 100644 --- a/packages/seal/Tests/Marshaller/FlattenMarshallerTest.php +++ b/packages/seal/Tests/Marshaller/FlattenMarshallerTest.php @@ -19,7 +19,7 @@ public function testMarshall(array $document, array $flattenDocument, array $fie $marshalledDocument = $marshaller->marshall($fields, $document); - $this->assertSame([...$flattenDocument, '_rawDocument' => \json_encode($document, \JSON_THROW_ON_ERROR)], $marshalledDocument); + $this->assertSame([...$flattenDocument, '_source' => \json_encode($document, \JSON_THROW_ON_ERROR)], $marshalledDocument); } /** @@ -31,7 +31,7 @@ public function testUnmarshall(array $document, array $flattenDocument, array $f { $marshaller = new FlattenMarshaller(); - $flattenDocument['_rawDocument'] = \json_encode($document, \JSON_THROW_ON_ERROR); + $flattenDocument['_source'] = \json_encode($document, \JSON_THROW_ON_ERROR); $unmarshalledDocument = $marshaller->unmarshall($fields, $flattenDocument); $this->assertSame($document, $unmarshalledDocument); From 33b2a37ea2a7ded4f6c58ca0ecb8dc723500b70a Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 1 Feb 2023 01:43:07 +0100 Subject: [PATCH 14/22] Fix that tests are correcty failing --- packages/seal-solr-adapter/SolrConnection.php | 61 ++++++++++++------- .../Testing/AbstractConnectionTestCase.php | 25 +++++++- 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/packages/seal-solr-adapter/SolrConnection.php b/packages/seal-solr-adapter/SolrConnection.php index ba6bb344..01f0ebbd 100644 --- a/packages/seal-solr-adapter/SolrConnection.php +++ b/packages/seal-solr-adapter/SolrConnection.php @@ -6,12 +6,10 @@ use Schranz\Search\SEAL\Task\SyncTask; use Solarium\Client; use Schranz\Search\SEAL\Adapter\ConnectionInterface; -use Schranz\Search\SEAL\Marshaller\Marshaller; use Schranz\Search\SEAL\Schema\Index; use Schranz\Search\SEAL\Search\Condition; use Schranz\Search\SEAL\Search\Result; use Schranz\Search\SEAL\Search\Search; -use Schranz\Search\SEAL\Task\AsyncTask; use Schranz\Search\SEAL\Task\TaskInterface; final class SolrConnection implements ConnectionInterface @@ -95,7 +93,7 @@ public function search(Search $search): Result } return new Result( - $this->hitsToDocuments($search->indexes, [$result->getDocument()->getFields()]), + $this->hitsToDocuments($search->indexes, [$result->getDocument()]), 1 ); } @@ -105,52 +103,69 @@ public function search(Search $search): Result } $index = $search->indexes[\array_key_first($search->indexes)]; - $searchIndex = $this->client->index($index->name); + $this->client->getEndpoint() + ->setCollection($index->name); + + + $query = $this->client->createSelect(); + $helper = $query->getHelper(); + + $queryText = null; - $query = null; $filters = []; foreach ($search->filters as $filter) { match (true) { - $filter instanceof Condition\IdentifierCondition => $filters[] = $index->getIdentifierField()->name . ' = "' . $filter->identifier . '"', // TODO escape? - $filter instanceof Condition\SearchCondition => $query = $filter->query, - $filter instanceof Condition\EqualCondition => $filters[] = $filter->field . ' = ' . $filter->value, // TODO escape? - $filter instanceof Condition\NotEqualCondition => $filters[] = $filter->field . ' != ' . $filter->value, // TODO escape? - $filter instanceof Condition\GreaterThanCondition => $filters[] = $filter->field . ' > ' . $filter->value, // TODO escape? - $filter instanceof Condition\GreaterThanEqualCondition => $filters[] = $filter->field . ' >= ' . $filter->value, // TODO escape? - $filter instanceof Condition\LessThanCondition => $filters[] = $filter->field . ' < ' . $filter->value, // TODO escape? - $filter instanceof Condition\LessThanEqualCondition => $filters[] = $filter->field . ' <= ' . $filter->value, // TODO escape? + $filter instanceof Condition\SearchCondition => $queryText = $filter->query, + $filter instanceof Condition\IdentifierCondition => $filters[] = $index->getIdentifierField()->name . ':' . $filter->identifier . '', // TODO escape? + $filter instanceof Condition\EqualCondition => $filters[] = $filter->field . ':' . $filter->value . '', // TODO escape? + $filter instanceof Condition\NotEqualCondition => $filters[] = '-' . $filter->field . ':' . $filter->value . '', // TODO escape? + $filter instanceof Condition\GreaterThanCondition => $filters[] = $filter->field . ' >= ' . $filter->value . '', // TODO escape? + $filter instanceof Condition\GreaterThanEqualCondition => $filters[] = $filter->field . ' > ' . $filter->value . '', // TODO escape? + $filter instanceof Condition\LessThanCondition => $filters[] = $filter->field . ' <= ' . $filter->value . '', // TODO escape? + $filter instanceof Condition\LessThanEqualCondition => $filters[] = $filter->field . ' < ' . $filter->value . '', // TODO escape? default => throw new \LogicException($filter::class . ' filter not implemented.'), }; } - $searchParams = []; - if (\count($filters) !== 0) { - $searchParams = ['filter' => \implode(' AND ', $filters)]; + if ($queryText !== null) { + $query->setQuery($helper->escapePhrase($queryText)); + } + + foreach ($filters as $key => $filter) { + $query->createFilterQuery('filter_' . $key)->setQuery($filter); } if ($search->offset) { - $searchParams['offset'] = $search->offset; + $query->setStart($search->offset); } if ($search->limit) { - $searchParams['limit'] = $search->limit; + $query->setRows($search->limit); + } + + /* + $searchParams = []; + if (\count($filters) !== 0) { + $searchParams = ['filter' => \implode(' AND ', $filters)]; } + TODO foreach ($search->sortBys as $field => $direction) { $searchParams['sort'][] = $field . ':' . $direction; } + */ - $data = $searchIndex->search($query, $searchParams)->toArray(); + $result = $this->client->select($query); return new Result( - $this->hitsToDocuments($search->indexes, $data['hits']), - $data['totalHits'] ?? $data['estimatedTotalHits'] ?? null, + $this->hitsToDocuments($search->indexes, $result->getDocuments()), + $result->getNumFound() ); } /** * @param Index[] $indexes - * @param iterable> $hits + * @param iterable<\Solarium\QueryType\Select\Result\Document> $hits * * @return \Generator> */ @@ -159,6 +174,8 @@ private function hitsToDocuments(array $indexes, iterable $hits): \Generator $index = $indexes[\array_key_first($indexes)]; foreach ($hits as $hit) { + $hit = $hit->getFields(); + unset($hit['_version_']); if ($index->getIdentifierField()->name !== 'id') { diff --git a/packages/seal/Testing/AbstractConnectionTestCase.php b/packages/seal/Testing/AbstractConnectionTestCase.php index 65037be0..e5b11669 100644 --- a/packages/seal/Testing/AbstractConnectionTestCase.php +++ b/packages/seal/Testing/AbstractConnectionTestCase.php @@ -423,7 +423,7 @@ public function testGreaterThanCondition(): void $search->addFilter(new Condition\GreaterThanCondition('rating', 2.5)); $loadedDocuments = [...$search->getResult()]; - $this->assertCount(1, $loadedDocuments); + $this->assertGreaterThanOrEqual(1, count($loadedDocuments)); foreach ($loadedDocuments as $loadedDocument) { $this->assertGreaterThan(2.5, $loadedDocument['rating']); @@ -458,7 +458,14 @@ public function testGreaterThanEqualCondition(): void $search->addFilter(new Condition\GreaterThanEqualCondition('rating', 2.5)); $loadedDocuments = [...$search->getResult()]; + $this->assertGreaterThan(1, count($loadedDocuments)); + foreach ($loadedDocuments as $loadedDocument) { + $this->assertNotNull( + $loadedDocument['rating'] ?? null, + 'Expected only documents with rating document "' . $document['uuid'] . '" without rating returned.' + ); + $this->assertGreaterThanOrEqual(2.5, $loadedDocument['rating']); } @@ -491,7 +498,14 @@ public function testLessThanCondition(): void $search->addFilter(new Condition\LessThanCondition('rating', 3.5)); $loadedDocuments = [...$search->getResult()]; + $this->assertGreaterThanOrEqual(1, count($loadedDocuments)); + foreach ($loadedDocuments as $loadedDocument) { + $this->assertNotNull( + $loadedDocument['rating'] ?? null, + 'Expected only documents with rating document "' . $document['uuid'] . '" without rating returned.' + ); + $this->assertLessThan(3.5, $loadedDocument['rating']); } @@ -524,7 +538,14 @@ public function testLessThanEqualCondition(): void $search->addFilter(new Condition\LessThanEqualCondition('rating', 3.5)); $loadedDocuments = [...$search->getResult()]; + $this->assertGreaterThan(1, count($loadedDocuments)); + foreach ($loadedDocuments as $loadedDocument) { + $this->assertNotNull( + $loadedDocument['rating'] ?? null, + 'Expected only documents with rating document "' . $document['uuid'] . '" without rating returned.' + ); + $this->assertLessThanOrEqual(3.5, $loadedDocument['rating']); } @@ -558,6 +579,7 @@ public function testSortByAsc(): void $search->addSortBy('rating', 'asc'); $loadedDocuments = [...$search->getResult()]; + $this->assertGreaterThan(1, count($loadedDocuments)); foreach ($documents as $document) { self::$taskHelper->tasks[] = self::$connection->delete( @@ -596,6 +618,7 @@ public function testSortByDesc(): void $search->addSortBy('rating', 'desc'); $loadedDocuments = [...$search->getResult()]; + $this->assertGreaterThan(1, count($loadedDocuments)); foreach ($documents as $document) { self::$taskHelper->tasks[] = self::$connection->delete( From 1382fcde914d4e6c9ad6499a31b1e3ea3a3579e3 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 1 Feb 2023 01:44:37 +0100 Subject: [PATCH 15/22] Add LessThan, LessThanEqual, GreaterThan, GreaterThanEqual Conditions to Solr --- packages/seal-solr-adapter/SolrConnection.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/seal-solr-adapter/SolrConnection.php b/packages/seal-solr-adapter/SolrConnection.php index 01f0ebbd..b4bd024c 100644 --- a/packages/seal-solr-adapter/SolrConnection.php +++ b/packages/seal-solr-adapter/SolrConnection.php @@ -116,13 +116,13 @@ public function search(Search $search): Result foreach ($search->filters as $filter) { match (true) { $filter instanceof Condition\SearchCondition => $queryText = $filter->query, - $filter instanceof Condition\IdentifierCondition => $filters[] = $index->getIdentifierField()->name . ':' . $filter->identifier . '', // TODO escape? - $filter instanceof Condition\EqualCondition => $filters[] = $filter->field . ':' . $filter->value . '', // TODO escape? - $filter instanceof Condition\NotEqualCondition => $filters[] = '-' . $filter->field . ':' . $filter->value . '', // TODO escape? - $filter instanceof Condition\GreaterThanCondition => $filters[] = $filter->field . ' >= ' . $filter->value . '', // TODO escape? - $filter instanceof Condition\GreaterThanEqualCondition => $filters[] = $filter->field . ' > ' . $filter->value . '', // TODO escape? - $filter instanceof Condition\LessThanCondition => $filters[] = $filter->field . ' <= ' . $filter->value . '', // TODO escape? - $filter instanceof Condition\LessThanEqualCondition => $filters[] = $filter->field . ' < ' . $filter->value . '', // TODO escape? + $filter instanceof Condition\IdentifierCondition => $filters[] = $index->getIdentifierField()->name . ':"' . $filter->identifier . '"', // TODO escape? + $filter instanceof Condition\EqualCondition => $filters[] = $filter->field . ':"' . $filter->value . '"', // TODO escape? + $filter instanceof Condition\NotEqualCondition => $filters[] = '-' . $filter->field . ':"' . $filter->value . '"', // TODO escape? + $filter instanceof Condition\GreaterThanCondition => $filters[] = $filter->field . ':{' . $filter->value . ' TO *}', // TODO escape? + $filter instanceof Condition\GreaterThanEqualCondition => $filters[] = $filter->field . ':[' . $filter->value . ' TO *]', // TODO escape? + $filter instanceof Condition\LessThanCondition => $filters[] = $filter->field . ':{* TO ' . $filter->value . '}', // TODO escape? + $filter instanceof Condition\LessThanEqualCondition => $filters[] = $filter->field . ':[* TO ' . $filter->value . ']', // TODO escape? default => throw new \LogicException($filter::class . ' filter not implemented.'), }; } From cf32175b285fd41708fc32c95cc8fef47dbdf0fe Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 1 Feb 2023 01:48:55 +0100 Subject: [PATCH 16/22] Add sorting for Solr --- packages/seal-solr-adapter/SolrConnection.php | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/seal-solr-adapter/SolrConnection.php b/packages/seal-solr-adapter/SolrConnection.php index b4bd024c..e8986cea 100644 --- a/packages/seal-solr-adapter/SolrConnection.php +++ b/packages/seal-solr-adapter/SolrConnection.php @@ -128,6 +128,7 @@ public function search(Search $search): Result } if ($queryText !== null) { + $query->setFields($index->searchableFields); $query->setQuery($helper->escapePhrase($queryText)); } @@ -143,17 +144,9 @@ public function search(Search $search): Result $query->setRows($search->limit); } - /* - $searchParams = []; - if (\count($filters) !== 0) { - $searchParams = ['filter' => \implode(' AND ', $filters)]; - } - - TODO foreach ($search->sortBys as $field => $direction) { - $searchParams['sort'][] = $field . ':' . $direction; + $query->addSort($field, $direction); } - */ $result = $this->client->select($query); From a47e3ea2538ec3b6d6a9c14b73a521aa036aa1c8 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 1 Feb 2023 01:55:13 +0100 Subject: [PATCH 17/22] Escape query fields in Solr --- packages/seal-solr-adapter/SolrConnection.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/seal-solr-adapter/SolrConnection.php b/packages/seal-solr-adapter/SolrConnection.php index e8986cea..93ebdd15 100644 --- a/packages/seal-solr-adapter/SolrConnection.php +++ b/packages/seal-solr-adapter/SolrConnection.php @@ -116,13 +116,13 @@ public function search(Search $search): Result foreach ($search->filters as $filter) { match (true) { $filter instanceof Condition\SearchCondition => $queryText = $filter->query, - $filter instanceof Condition\IdentifierCondition => $filters[] = $index->getIdentifierField()->name . ':"' . $filter->identifier . '"', // TODO escape? - $filter instanceof Condition\EqualCondition => $filters[] = $filter->field . ':"' . $filter->value . '"', // TODO escape? - $filter instanceof Condition\NotEqualCondition => $filters[] = '-' . $filter->field . ':"' . $filter->value . '"', // TODO escape? - $filter instanceof Condition\GreaterThanCondition => $filters[] = $filter->field . ':{' . $filter->value . ' TO *}', // TODO escape? - $filter instanceof Condition\GreaterThanEqualCondition => $filters[] = $filter->field . ':[' . $filter->value . ' TO *]', // TODO escape? - $filter instanceof Condition\LessThanCondition => $filters[] = $filter->field . ':{* TO ' . $filter->value . '}', // TODO escape? - $filter instanceof Condition\LessThanEqualCondition => $filters[] = $filter->field . ':[* TO ' . $filter->value . ']', // TODO escape? + $filter instanceof Condition\IdentifierCondition => $filters[] = $index->getIdentifierField()->name . ':' . $helper->escapeTerm($filter->identifier), + $filter instanceof Condition\EqualCondition => $filters[] = $filter->field . ':' . $helper->escapeTerm($filter->value), + $filter instanceof Condition\NotEqualCondition => $filters[] = '-' . $filter->field . ':' . $helper->escapeTerm($helper->escapeTerm($filter->value)), + $filter instanceof Condition\GreaterThanCondition => $filters[] = $filter->field . ':{' . $helper->escapeTerm($filter->value) . ' TO *}', + $filter instanceof Condition\GreaterThanEqualCondition => $filters[] = $filter->field . ':[' . $helper->escapeTerm($filter->value) . ' TO *]', + $filter instanceof Condition\LessThanCondition => $filters[] = $filter->field . ':{* TO ' . $helper->escapeTerm($filter->value) . '}', + $filter instanceof Condition\LessThanEqualCondition => $filters[] = $filter->field . ':[* TO ' . $helper->escapeTerm($filter->value) . ']', default => throw new \LogicException($filter::class . ' filter not implemented.'), }; } From 5ca27005053140663cd20467cba4467d85ecdb03 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 1 Feb 2023 02:35:51 +0100 Subject: [PATCH 18/22] Add handling of .raw text fields --- packages/seal/Marshaller/FlattenMarshaller.php | 12 +++++------- .../seal/Tests/Marshaller/FlattenMarshallerTest.php | 1 + 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/seal/Marshaller/FlattenMarshaller.php b/packages/seal/Marshaller/FlattenMarshaller.php index 5f8c5ca0..8169cc99 100644 --- a/packages/seal/Marshaller/FlattenMarshaller.php +++ b/packages/seal/Marshaller/FlattenMarshaller.php @@ -13,13 +13,7 @@ */ final class FlattenMarshaller { - private Marshaller $marshaller; - - public function __construct( - bool $dateAsInteger = false, - ) { - $this->marshaller = new Marshaller($dateAsInteger); - } + public function __construct() {} /** * @param Field\AbstractField[] $fields @@ -64,6 +58,10 @@ private function flatten(array $fields, array $raw, bool $rootIsParentMultiple = $field instanceof Field\TypedField => $raw = $this->flattenTyped($name, $raw, $field, $rootIsParentMultiple), default => null, }; + + if ($field instanceof Field\TextField && $field->searchable && ($field->sortable || $field->filterable)) { + $raw[$name. '.raw'] = $raw[$name]; + } } return $raw; diff --git a/packages/seal/Tests/Marshaller/FlattenMarshallerTest.php b/packages/seal/Tests/Marshaller/FlattenMarshallerTest.php index 8b1b27fe..bd9089d0 100644 --- a/packages/seal/Tests/Marshaller/FlattenMarshallerTest.php +++ b/packages/seal/Tests/Marshaller/FlattenMarshallerTest.php @@ -116,6 +116,7 @@ private function provideData(): \Generator 'comments.text' => ['Awesome blog!', 'Like this blog!'], 'tags' => ['Tech', 'UI'], 'categoryIds' => [1, 2], + 'tags.raw' => ['Tech', 'UI'], ], [ 'uuid' => new Field\IdentifierField('uuid'), From b410979d1cdd044d7409bc5f2aa73d8a77b0c22f Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 1 Feb 2023 02:36:11 +0100 Subject: [PATCH 19/22] Add handling for filters on text .raw fields for Solr --- packages/seal-solr-adapter/SolrConnection.php | 36 ++++++++++++++----- .../seal-solr-adapter/SolrSchemaManager.php | 17 ++++++++- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/packages/seal-solr-adapter/SolrConnection.php b/packages/seal-solr-adapter/SolrConnection.php index 93ebdd15..36cc5299 100644 --- a/packages/seal-solr-adapter/SolrConnection.php +++ b/packages/seal-solr-adapter/SolrConnection.php @@ -3,9 +3,11 @@ namespace Schranz\Search\SEAL\Adapter\Solr; use Schranz\Search\SEAL\Marshaller\FlattenMarshaller; +use Schranz\Search\SEAL\Schema\Exception\FieldByPathNotFoundException; use Schranz\Search\SEAL\Task\SyncTask; use Solarium\Client; use Schranz\Search\SEAL\Adapter\ConnectionInterface; +use Schranz\Search\SEAL\Schema\Field; use Schranz\Search\SEAL\Schema\Index; use Schranz\Search\SEAL\Search\Condition; use Schranz\Search\SEAL\Search\Result; @@ -117,19 +119,18 @@ public function search(Search $search): Result match (true) { $filter instanceof Condition\SearchCondition => $queryText = $filter->query, $filter instanceof Condition\IdentifierCondition => $filters[] = $index->getIdentifierField()->name . ':' . $helper->escapeTerm($filter->identifier), - $filter instanceof Condition\EqualCondition => $filters[] = $filter->field . ':' . $helper->escapeTerm($filter->value), - $filter instanceof Condition\NotEqualCondition => $filters[] = '-' . $filter->field . ':' . $helper->escapeTerm($helper->escapeTerm($filter->value)), - $filter instanceof Condition\GreaterThanCondition => $filters[] = $filter->field . ':{' . $helper->escapeTerm($filter->value) . ' TO *}', - $filter instanceof Condition\GreaterThanEqualCondition => $filters[] = $filter->field . ':[' . $helper->escapeTerm($filter->value) . ' TO *]', - $filter instanceof Condition\LessThanCondition => $filters[] = $filter->field . ':{* TO ' . $helper->escapeTerm($filter->value) . '}', - $filter instanceof Condition\LessThanEqualCondition => $filters[] = $filter->field . ':[* TO ' . $helper->escapeTerm($filter->value) . ']', + $filter instanceof Condition\EqualCondition => $filters[] = $this->getFilterField($search->indexes, $filter->field) . ':' . $helper->escapeTerm($filter->value), + $filter instanceof Condition\NotEqualCondition => $filters[] = '-' . $this->getFilterField($search->indexes, $filter->field) . ':' . $helper->escapeTerm($helper->escapeTerm($filter->value)), + $filter instanceof Condition\GreaterThanCondition => $filters[] = $this->getFilterField($search->indexes, $filter->field) . ':{' . $helper->escapeTerm($filter->value) . ' TO *}', + $filter instanceof Condition\GreaterThanEqualCondition => $filters[] = $this->getFilterField($search->indexes, $filter->field) . ':[' . $helper->escapeTerm($filter->value) . ' TO *]', + $filter instanceof Condition\LessThanCondition => $filters[] = $this->getFilterField($search->indexes, $filter->field) . ':{* TO ' . $helper->escapeTerm($filter->value) . '}', + $filter instanceof Condition\LessThanEqualCondition => $filters[] = $this->getFilterField($search->indexes, $filter->field) . ':[* TO ' . $helper->escapeTerm($filter->value) . ']', default => throw new \LogicException($filter::class . ' filter not implemented.'), }; } if ($queryText !== null) { - $query->setFields($index->searchableFields); - $query->setQuery($helper->escapePhrase($queryText)); + $query->setQuery($queryText); } foreach ($filters as $key => $filter) { @@ -181,4 +182,23 @@ private function hitsToDocuments(array $indexes, iterable $hits): \Generator yield $this->marshaller->unmarshall($index->fields, $hit); } } + + private function getFilterField(array $indexes, string $name): string + { + foreach ($indexes as $index) { + try { + $field = $index->getFieldByPath($name); + + if ($field instanceof Field\TextField) { + return $name . '.raw'; + } + + return $name; + } catch (FieldByPathNotFoundException $e) { + // ignore when field is not found and use go to next index instead + } + } + + return $name; + } } diff --git a/packages/seal-solr-adapter/SolrSchemaManager.php b/packages/seal-solr-adapter/SolrSchemaManager.php index 99ff2cf1..74655f45 100644 --- a/packages/seal-solr-adapter/SolrSchemaManager.php +++ b/packages/seal-solr-adapter/SolrSchemaManager.php @@ -115,10 +115,11 @@ private function createIndexFields(array $fields, string $prefix = '', bool $isP $field instanceof Field\IdentifierField => null, // TODO define primary field $field instanceof Field\TextField => $indexFields[$name] = [ 'name' => $name, - 'type' => 'string', + 'type' => $field->searchable ? 'text_general' : 'string', 'indexed' => $field->searchable, 'docValues' => $field->filterable || $field->sortable, 'stored' => false, + 'useDocValuesAsStored' => false, 'multiValued' => $isMultiple, ], $field instanceof Field\BooleanField => $indexFields[$name] = [ @@ -127,6 +128,7 @@ private function createIndexFields(array $fields, string $prefix = '', bool $isP 'indexed' => $field->searchable, 'docValues' => $field->filterable || $field->sortable, 'stored' => false, + 'useDocValuesAsStored' => false, 'multiValued' => $isMultiple, ], $field instanceof Field\DateTimeField => $indexFields[$name] = [ @@ -135,6 +137,7 @@ private function createIndexFields(array $fields, string $prefix = '', bool $isP 'indexed' => $field->searchable, 'docValues' => $field->filterable || $field->sortable, 'stored' => false, + 'useDocValuesAsStored' => false, 'multiValued' => $isMultiple, ], $field instanceof Field\IntegerField => $indexFields[$name] = [ @@ -143,6 +146,7 @@ private function createIndexFields(array $fields, string $prefix = '', bool $isP 'indexed' => $field->searchable, 'docValues' => $field->filterable || $field->sortable, 'stored' => false, + 'useDocValuesAsStored' => false, 'multiValued' => $isMultiple, ], $field instanceof Field\FloatField => $indexFields[$name] = [ @@ -159,6 +163,17 @@ private function createIndexFields(array $fields, string $prefix = '', bool $isP }, $field->types, array_keys($field->types)), default => throw new \RuntimeException(sprintf('Field type "%s" is not supported.', get_class($field))), }; + + if ($field instanceof Field\TextField && $field->searchable && ($field->filterable || $field->sortable)) { + // add additional raw field for field which is filterable/sortable but also searchable + $fieldSettings = $indexFields[$name]; + + $fieldSettings['name'] = $name . '.raw'; + $fieldSettings['type'] = 'string'; + $indexFields[$name . '.raw'] = $fieldSettings; + + $indexFields[$name]['docValues'] = false; + } } if ($prefix === '') { From 4a3e881f0d60c23e12fb39ed0702e84090caf88c Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 1 Feb 2023 02:56:32 +0100 Subject: [PATCH 20/22] Add SearchCondition handling --- packages/seal-solr-adapter/SolrConnection.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/seal-solr-adapter/SolrConnection.php b/packages/seal-solr-adapter/SolrConnection.php index 36cc5299..9127bab0 100644 --- a/packages/seal-solr-adapter/SolrConnection.php +++ b/packages/seal-solr-adapter/SolrConnection.php @@ -130,6 +130,9 @@ public function search(Search $search): Result } if ($queryText !== null) { + $dismax = $query->getDisMax(); + $dismax->setQueryFields(implode(' ', $index->searchableFields)); + $query->setQuery($queryText); } From c5e523d6758e7254daeedc4c3192e3263c442289 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 1 Feb 2023 02:58:43 +0100 Subject: [PATCH 21/22] Add hint for todo multi search support --- packages/seal-solr-adapter/SolrConnection.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/seal-solr-adapter/SolrConnection.php b/packages/seal-solr-adapter/SolrConnection.php index 9127bab0..d334dc78 100644 --- a/packages/seal-solr-adapter/SolrConnection.php +++ b/packages/seal-solr-adapter/SolrConnection.php @@ -101,14 +101,13 @@ public function search(Search $search): Result } if (count($search->indexes) !== 1) { - throw new \RuntimeException('Solr does not yet support search multiple indexes: https://github.com/schranz-search/schranz-search/issues/28'); + throw new \RuntimeException('Solr does not yet support search multiple indexes: https://github.com/schranz-search/schranz-search/issues/86'); } $index = $search->indexes[\array_key_first($search->indexes)]; $this->client->getEndpoint() ->setCollection($index->name); - $query = $this->client->createSelect(); $helper = $query->getHelper(); From 5c75377a3fcae6f3a4118761e3870e0f9ab47f4c Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 1 Feb 2023 03:20:32 +0100 Subject: [PATCH 22/22] Update README for Apache Solr Support --- README.md | 6 +++--- packages/seal-solr-adapter/SolrConnection.php | 3 ++- packages/seal/README.md | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3630115a..357a51b1 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ At current state collect here different search engines which are around and coul - [Opensearch](#opensearch) - [schranz-search/seal-opensearch-adapter](packages/seal-opensearch-adapter) - [Meilisearch](#meilisearch) - [schranz-search/seal-meilisearch-adapter](packages/seal-meilisearch-adapter) - [Algolia](#algolia) - [schranz-search/seal-algolia-adapter](packages/seal-algolia-adapter) - - [Solr](#solr) (help wanted [#73](https://github.com/schranz-search/schranz-search/pull/73)) + - [Solr](#solr) - [schranz-search/seal-solr-adapter](packages/seal-solr-adapter) - [Typesense](#typesense) (work in progress [#76](https://github.com/schranz-search/schranz-search/pull/76)) - [Zinc Labs](#zinc-labs) (work in progress [#79](https://github.com/schranz-search/schranz-search/pull/79)) - [RediSearch](#redisearch) @@ -80,7 +80,7 @@ Fork of Elasticsearch also written in Java. - Server: [Opensearch Server](https://github.com/opensearch-project/OpenSearch) - PHP Client: [Opensearch PHP](https://github.com/opensearch-project/opensearch-php) -Implementation: [schranz-search/seal-elasticsearch-adapter](packages/seal-opensearch-adapter) +Implementation: [schranz-search/seal-opensearch-adapter](packages/seal-opensearch-adapter) ### Meilisearch @@ -107,7 +107,7 @@ A search engine under the Apache Project based on Lucene written in Java: - Server: [Solr Server](https://github.com/apache/solr) - PHP Client: [Solarium PHP](https://github.com/solariumphp/solarium) seems to be a well maintained Client -Implementation: help wanted [#73](https://github.com/schranz-search/schranz-search/pull/73) +Implementation: [schranz-search/seal-solr-adapter](packages/seal-solr-adapter) ### Typesense diff --git a/packages/seal-solr-adapter/SolrConnection.php b/packages/seal-solr-adapter/SolrConnection.php index d334dc78..08332105 100644 --- a/packages/seal-solr-adapter/SolrConnection.php +++ b/packages/seal-solr-adapter/SolrConnection.php @@ -32,7 +32,7 @@ public function save(Index $index, array $document, array $options = []): ?TaskI $identifier = ((string) $document[$identifierField->name]) ?? null; $marshalledDocument = $this->marshaller->marshall($index->fields, $document); - $marshalledDocument['id'] = $identifier; + $marshalledDocument['id'] = $identifier; // Solr currently does not support set another identifier then id: https://github.com/schranz-search/schranz-search/issues/87 $update = $this->client->createUpdate(); $indexDocument = $update->createDocument($marshalledDocument); @@ -175,6 +175,7 @@ private function hitsToDocuments(array $indexes, iterable $hits): \Generator unset($hit['_version_']); if ($index->getIdentifierField()->name !== 'id') { + // Solr currently does not support set another identifier then id: https://github.com/schranz-search/schranz-search/issues/87 $id = $hit['id']; unset($hit['id']); diff --git a/packages/seal/README.md b/packages/seal/README.md index c03bb8f6..556ba599 100644 --- a/packages/seal/README.md +++ b/packages/seal/README.md @@ -41,6 +41,7 @@ The following adapters are available: - [OpensearchAdapter](../seal-opensearch-adapter): `schranz-search/seal-opensearch-adapter` - [MeilisearchAdapter](../seal-meilisearch-adapter): `schranz-search/seal-meilisearch-adapter` - [AlgoliaAdapter](../seal-algolia-adapter): `schranz-search/seal-algolia-adapter` + - [SolrAdapter](../seal-solr-adapter): `schranz-search/seal-solr-adapter` - ... more coming soon Additional Wrapper adapters: