├── .github └── FUNDING.yml ├── LICENSE ├── README.md ├── composer.json ├── config └── services.php ├── phpstan.dist.neon └── src ├── Command ├── IndexCreateCommand.php ├── IndexDropCommand.php └── ReindexCommand.php └── SealBundle.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [alexander-schranz] 2 | custom: ["https://paypal.me/L91"] 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alexander Schranz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Note**: 2 | > This is part of the `cmsig/search` project create issues in the [main repository](https://github.com/php-cmsig/search). 3 | 4 | --- 5 | 6 |
7 | SEAL Logo with an abstract seal sitting on a telescope. 8 |
9 | 10 |
Logo created by Meine Wilma
11 | 12 |

SEAL
Symfony Integration

13 | 14 |
15 |
16 | 17 | Integration of the CMS-IG — Search Engine Abstraction Layer (SEAL) into Symfony. 18 | 19 | > **Note**: 20 | > This project is heavily under development and any feedback is greatly appreciated. 21 | 22 | ## Installation 23 | 24 | Use [composer](https://getcomposer.org/) for install the package: 25 | 26 | ```bash 27 | composer require cmsig/seal-symfony-bundle 28 | ``` 29 | 30 | Also install one of the listed adapters. 31 | 32 | ### List of adapters 33 | 34 | The following adapters are available: 35 | 36 | - [MemoryAdapter](../../packages/seal-memory-adapter): `cmsig/seal-memory-adapter` 37 | - [ElasticsearchAdapter](../../packages/seal-elasticsearch-adapter): `cmsig/seal-elasticsearch-adapter` 38 | - [OpensearchAdapter](../../packages/seal-opensearch-adapter): `cmsig/seal-opensearch-adapter` 39 | - [MeilisearchAdapter](../../packages/seal-meilisearch-adapter): `cmsig/seal-meilisearch-adapter` 40 | - [AlgoliaAdapter](../../packages/seal-algolia-adapter): `cmsig/seal-algolia-adapter` 41 | - [LoupeAdapter](../../packages/seal-loupe-adapter): `cmsig/seal-loupe-adapter` 42 | - [SolrAdapter](../../packages/seal-solr-adapter): `cmsig/seal-solr-adapter` 43 | - [RediSearchAdapter](../../packages/seal-redisearch-adapter): `cmsig/seal-redisearch-adapter` 44 | - [TypesenseAdapter](../../packages/seal-typesense-adapter): `cmsig/seal-typesense-adapter` 45 | - ... more coming soon 46 | 47 | Additional Wrapper adapters: 48 | 49 | - [ReadWriteAdapter](../../packages/seal-read-write-adapter): `cmsig/seal-read-write-adapter` 50 | - [MultiAdapter](../../packages/seal-multi-adapter): `cmsig/seal-multi-adapter` 51 | 52 | Creating your own adapter? Add the [`seal-php-adapter`](https://github.com/topics/seal-php-adapter) Topic to your GitHub Repository. 53 | 54 | ## Configuration 55 | 56 | The following code shows how to configure the package: 57 | 58 | ```yaml 59 | # config/packages/cmsig_seal.yaml 60 | 61 | cmsig_seal: 62 | schemas: 63 | app: 64 | dir: '%kernel.project_dir%/config/schemas' 65 | # engine: 'default' 66 | engines: 67 | default: 68 | adapter: '%env(ENGINE_URL)%' 69 | ``` 70 | 71 | A more complex configuration can be here found: 72 | 73 | ```yaml 74 | # config/packages/cmsig_seal.yaml 75 | 76 | cmsig_seal: 77 | schemas: 78 | app: 79 | dir: '%kernel.project_dir%/config/schemas/app' 80 | other: 81 | dir: '%kernel.project_dir%/config/schemas/other' 82 | engine: algolia 83 | engines: 84 | algolia: 85 | adapter: 'algolia://%env(ALGOLIA_APPLICATION_ID)%:%env(ALGOLIA_ADMIN_API_KEY)%' 86 | elasticsearch: 87 | adapter: 'elasticsearch://127.0.0.1:9200' 88 | loupe: 89 | adapter: 'loupe://var/indexes' 90 | meilisearch: 91 | adapter: 'meilisearch://127.0.0.1:7700' 92 | memory: 93 | adapter: 'memory://' 94 | opensearch: 95 | adapter: 'opensearch://127.0.0.1:9200' 96 | redisearch: 97 | adapter: 'redis://supersecure@127.0.0.1:6379' 98 | solr: 99 | adapter: 'solr://127.0.0.1:8983' 100 | typesense: 101 | adapter: 'typesense://S3CR3T@127.0.0.1:8108' 102 | 103 | # ... 104 | multi: 105 | adapter: 'multi://elasticsearch?adapters[]=opensearch' 106 | read-write: 107 | adapter: 'read-write://elasticsearch?write=multi' 108 | index_name_prefix: '' 109 | ``` 110 | 111 | ## Usage 112 | 113 | The default engine is available as `Engine`: 114 | 115 | ```php 116 | class Some { 117 | public function __construct( 118 | private readonly \CmsIg\Seal\EngineInterface $engine, 119 | ) { 120 | } 121 | } 122 | ``` 123 | 124 | A specific engine is available under the config key suffix with `Engine`: 125 | 126 | ```php 127 | class Some { 128 | public function __construct( 129 | private readonly \CmsIg\Seal\EngineInterface $algoliaEngine, 130 | ) { 131 | } 132 | } 133 | ``` 134 | 135 | Multiple engines can be accessed via the `EngineRegistry`: 136 | 137 | ```php 138 | class Some { 139 | private Engine $engine; 140 | 141 | public function __construct( 142 | private readonly \CmsIg\Seal\EngineRegistry $engineRegistry, 143 | ) { 144 | $this->engine = $this->engineRegistry->get('algolia'); 145 | } 146 | } 147 | ``` 148 | 149 | How to create a `Schema` file and use your `Engine` can be found [SEAL Documentation](../../README.md#usage). 150 | 151 | ### Commands 152 | 153 | The bundle provides the following commands: 154 | 155 | **Create configured indexes** 156 | 157 | ```bash 158 | bin/console cmsig:seal:index-create --help 159 | ``` 160 | 161 | **Drop configured indexes** 162 | 163 | ```bash 164 | bin/console cmsig:seal:index-drop --help 165 | ``` 166 | 167 | **Reindex configured indexes** 168 | 169 | ```bash 170 | bin/console cmsig:seal:reindex --help 171 | ``` 172 | 173 | ## Authors 174 | 175 | - [Alexander Schranz](https://github.com/alexander-schranz/) 176 | - [The Community Contributors](https://github.com/php-cmsig/search/graphs/contributors) 177 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cmsig/seal-symfony-bundle", 3 | "description": "An integration of CMS-IG SEAL search abstraction into Symfony Framework.", 4 | "type": "symfony-bundle", 5 | "license": "MIT", 6 | "keywords": [ 7 | "search-client", 8 | "search-abstraction", 9 | "abstraction", 10 | "search", 11 | "search-client", 12 | "search-abstraction", 13 | "cmsig", 14 | "seal", 15 | "elasticsearch", 16 | "opensearch", 17 | "meilisearch", 18 | "typesense", 19 | "solr", 20 | "redisearch", 21 | "algolia", 22 | "loupe", 23 | "integration", 24 | "bridge", 25 | "symfony-bundle", 26 | "bundle", 27 | "symfony" 28 | ], 29 | "autoload": { 30 | "psr-4": { 31 | "CmsIg\\Seal\\Integration\\Symfony\\": "src" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "CmsIg\\Seal\\Integration\\Symfony\\Tests\\": "tests" 37 | } 38 | }, 39 | "authors": [ 40 | { 41 | "name": "Alexander Schranz", 42 | "email": "alexander@sulu.io" 43 | } 44 | ], 45 | "require": { 46 | "php": "^8.1", 47 | "cmsig/seal": "^0.9", 48 | "symfony/config": "^6.1 || ^7.0", 49 | "symfony/console": "^6.1 || ^7.0", 50 | "symfony/dependency-injection": "^6.1 || ^7.0", 51 | "symfony/http-kernel": "^6.1 || ^7.0" 52 | }, 53 | "require-dev": { 54 | "php-cs-fixer/shim": "^3.51", 55 | "phpstan/extension-installer": "^1.2", 56 | "phpstan/phpstan": "^1.10", 57 | "phpstan/phpstan-phpunit": "^1.3", 58 | "phpunit/phpunit": "^10.3", 59 | "rector/rector": "^1.0", 60 | "cmsig/seal-algolia-adapter": "^0.9", 61 | "cmsig/seal-elasticsearch-adapter": "^0.9", 62 | "cmsig/seal-loupe-adapter": "^0.9", 63 | "cmsig/seal-meilisearch-adapter": "^0.9", 64 | "cmsig/seal-memory-adapter": "^0.9", 65 | "cmsig/seal-multi-adapter": "^0.9", 66 | "cmsig/seal-opensearch-adapter": "^0.9", 67 | "cmsig/seal-read-write-adapter": "^0.9", 68 | "cmsig/seal-redisearch-adapter": "^0.9", 69 | "cmsig/seal-solr-adapter": "^0.9", 70 | "cmsig/seal-typesense-adapter": "^0.9" 71 | }, 72 | "replace": { 73 | "schranz-search/symfony-bundle": "self.version" 74 | }, 75 | "conflict": { 76 | "cmsig/seal-algolia-adapter": "<0.9 || >=0.10", 77 | "cmsig/seal-elasticsearch-adapter": "<0.9 || >=0.10", 78 | "cmsig/seal-loupe-adapter": "<0.9 || >=0.10", 79 | "cmsig/seal-meilisearch-adapter": "<0.9 || >=0.10", 80 | "cmsig/seal-memory-adapter": "<0.9 || >=0.10", 81 | "cmsig/seal-multi-adapter": "<0.9 || >=0.10", 82 | "cmsig/seal-opensearch-adapter": "<0.9 || >=0.10", 83 | "cmsig/seal-read-write-adapter": "<0.9 || >=0.10", 84 | "cmsig/seal-redisearch-adapter": "<0.9 || >=0.10", 85 | "cmsig/seal-solr-adapter": "<0.9 || >=0.10", 86 | "cmsig/seal-typesense-adapter": "<0.9 || >=0.10" 87 | }, 88 | "scripts": { 89 | "test": [ 90 | "Composer\\Config::disableProcessTimeout", 91 | "vendor/bin/phpunit" 92 | ], 93 | "phpstan": "@php vendor/bin/phpstan analyze", 94 | "lint-rector": "@php vendor/bin/rector process --dry-run", 95 | "lint-php-cs": "@php vendor/bin/php-cs-fixer fix --verbose --diff --dry-run", 96 | "lint": [ 97 | "@phpstan", 98 | "@lint-php-cs", 99 | "@lint-rector", 100 | "@lint-composer" 101 | ], 102 | "lint-composer": "@composer validate --strict", 103 | "rector": "@php vendor/bin/rector process", 104 | "php-cs-fix": "@php vendor/bin/php-cs-fixer fix", 105 | "fix": [ 106 | "@rector", 107 | "@php-cs-fix" 108 | ] 109 | }, 110 | "repositories": [ 111 | { 112 | "type": "path", 113 | "url": "./../../packages/*", 114 | "options": { 115 | "symlink": true 116 | } 117 | } 118 | ], 119 | "minimum-stability": "dev", 120 | "config": { 121 | "allow-plugins": { 122 | "php-http/discovery": true, 123 | "phpstan/extension-installer": true 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Symfony\Component\DependencyInjection\Loader\Configurator; 15 | 16 | use CmsIg\Seal\Adapter\AdapterFactory; 17 | use CmsIg\Seal\Adapter\Algolia\AlgoliaAdapterFactory; 18 | use CmsIg\Seal\Adapter\Elasticsearch\ElasticsearchAdapterFactory; 19 | use CmsIg\Seal\Adapter\Loupe\LoupeAdapterFactory; 20 | use CmsIg\Seal\Adapter\Meilisearch\MeilisearchAdapterFactory; 21 | use CmsIg\Seal\Adapter\Memory\MemoryAdapterFactory; 22 | use CmsIg\Seal\Adapter\Multi\MultiAdapterFactory; 23 | use CmsIg\Seal\Adapter\Opensearch\OpensearchAdapterFactory; 24 | use CmsIg\Seal\Adapter\ReadWrite\ReadWriteAdapterFactory; 25 | use CmsIg\Seal\Adapter\RediSearch\RediSearchAdapterFactory; 26 | use CmsIg\Seal\Adapter\Solr\SolrAdapterFactory; 27 | use CmsIg\Seal\Adapter\Typesense\TypesenseAdapterFactory; 28 | use CmsIg\Seal\EngineRegistry; 29 | use CmsIg\Seal\Integration\Symfony\Command\IndexCreateCommand; 30 | use CmsIg\Seal\Integration\Symfony\Command\IndexDropCommand; 31 | use CmsIg\Seal\Integration\Symfony\Command\ReindexCommand; 32 | 33 | /* 34 | * @internal 35 | */ 36 | return static function (ContainerConfigurator $container) { 37 | // -------------------------------------------------------------------// 38 | // Commands // 39 | // -------------------------------------------------------------------// 40 | $container->services() 41 | ->set('cmsig_seal.index_create_command', IndexCreateCommand::class) 42 | ->args([ 43 | service('cmsig_seal.engine_registry'), 44 | ]) 45 | ->tag('console.command'); 46 | 47 | $container->services() 48 | ->set('cmsig_seal.index_drop_command', IndexDropCommand::class) 49 | ->args([ 50 | service('cmsig_seal.engine_registry'), 51 | ]) 52 | ->tag('console.command'); 53 | 54 | $container->services() 55 | ->set('cmsig_seal.reindex_command', ReindexCommand::class) 56 | ->args([ 57 | service('cmsig_seal.engine_registry'), 58 | tagged_iterator('cmsig_seal.reindex_provider'), 59 | ]) 60 | ->tag('console.command'); 61 | 62 | // -------------------------------------------------------------------// 63 | // Services // 64 | // -------------------------------------------------------------------// 65 | $container->services() 66 | ->set('cmsig_seal.engine_registry', EngineRegistry::class) 67 | ->args([ 68 | tagged_iterator('cmsig_seal.engine', 'name'), 69 | ]) 70 | ->alias(EngineRegistry::class, 'cmsig_seal.engine_registry'); 71 | 72 | $container->services() 73 | ->set('cmsig_seal.adapter_factory', AdapterFactory::class) 74 | ->args([ 75 | tagged_iterator('cmsig_seal.adapter_factory', null, 'getName'), 76 | ]) 77 | ->alias(AdapterFactory::class, 'cmsig_seal.adapter_factory'); 78 | 79 | if (\class_exists(AlgoliaAdapterFactory::class)) { 80 | $container->services() 81 | ->set('cmsig_seal.algolia.adapter_factory', AlgoliaAdapterFactory::class) 82 | ->args([ 83 | service('service_container'), 84 | ]) 85 | ->tag('cmsig_seal.adapter_factory', ['name' => AlgoliaAdapterFactory::getName()]); 86 | } 87 | 88 | if (\class_exists(ElasticsearchAdapterFactory::class)) { 89 | $container->services() 90 | ->set('cmsig_seal.elasticsearch.adapter_factory', ElasticsearchAdapterFactory::class) 91 | ->args([ 92 | service('service_container'), 93 | ]) 94 | ->tag('cmsig_seal.adapter_factory', ['name' => ElasticsearchAdapterFactory::getName()]); 95 | } 96 | 97 | if (\class_exists(LoupeAdapterFactory::class)) { 98 | $container->services() 99 | ->set('cmsig_seal.loupe.adapter_factory', LoupeAdapterFactory::class) 100 | ->args([ 101 | service('service_container'), 102 | ]) 103 | ->tag('cmsig_seal.adapter_factory', ['name' => LoupeAdapterFactory::getName()]); 104 | } 105 | 106 | if (\class_exists(OpensearchAdapterFactory::class)) { 107 | $container->services() 108 | ->set('cmsig_seal.opensearch.adapter_factory', OpensearchAdapterFactory::class) 109 | ->args([ 110 | service('service_container'), 111 | ]) 112 | ->tag('cmsig_seal.adapter_factory', ['name' => OpensearchAdapterFactory::getName()]); 113 | } 114 | 115 | if (\class_exists(MeilisearchAdapterFactory::class)) { 116 | $container->services() 117 | ->set('cmsig_seal.meilisearch.adapter_factory', MeilisearchAdapterFactory::class) 118 | ->args([ 119 | service('service_container'), 120 | ]) 121 | ->tag('cmsig_seal.adapter_factory', ['name' => MeilisearchAdapterFactory::getName()]); 122 | } 123 | 124 | if (\class_exists(MemoryAdapterFactory::class)) { 125 | $container->services() 126 | ->set('cmsig_seal.memory.adapter_factory', MemoryAdapterFactory::class) 127 | ->args([ 128 | service('service_container'), 129 | ]) 130 | ->tag('cmsig_seal.adapter_factory', ['name' => MemoryAdapterFactory::getName()]); 131 | } 132 | 133 | if (\class_exists(RediSearchAdapterFactory::class)) { 134 | $container->services() 135 | ->set('cmsig_seal.redis.adapter_factory', RediSearchAdapterFactory::class) 136 | ->args([ 137 | service('service_container'), 138 | ]) 139 | ->tag('cmsig_seal.adapter_factory', ['name' => RediSearchAdapterFactory::getName()]); 140 | } 141 | 142 | if (\class_exists(SolrAdapterFactory::class)) { 143 | $container->services() 144 | ->set('cmsig_seal.solr.adapter_factory', SolrAdapterFactory::class) 145 | ->args([ 146 | service('service_container'), 147 | ]) 148 | ->tag('cmsig_seal.adapter_factory', ['name' => SolrAdapterFactory::getName()]); 149 | } 150 | 151 | if (\class_exists(TypesenseAdapterFactory::class)) { 152 | $container->services() 153 | ->set('cmsig_seal.typesense.adapter_factory', TypesenseAdapterFactory::class) 154 | ->args([ 155 | service('service_container'), 156 | ]) 157 | ->tag('cmsig_seal.adapter_factory', ['name' => TypesenseAdapterFactory::getName()]); 158 | } 159 | 160 | // ... 161 | 162 | if (\class_exists(ReadWriteAdapterFactory::class)) { 163 | $container->services() 164 | ->set('cmsig_seal.read_write.adapter_factory', ReadWriteAdapterFactory::class) 165 | ->args([ 166 | service('service_container'), 167 | 'cmsig_seal.adapter.', 168 | ]) 169 | ->tag('cmsig_seal.adapter_factory', ['name' => ReadWriteAdapterFactory::getName()]); 170 | } 171 | 172 | if (\class_exists(MultiAdapterFactory::class)) { 173 | $container->services() 174 | ->set('cmsig_seal.multi.adapter_factory', MultiAdapterFactory::class) 175 | ->args([ 176 | service('service_container'), 177 | 'cmsig_seal.adapter.', 178 | ]) 179 | ->tag('cmsig_seal.adapter_factory', ['name' => MultiAdapterFactory::getName()]); 180 | } 181 | }; 182 | -------------------------------------------------------------------------------- /phpstan.dist.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ../../phpstan.dist.neon 3 | 4 | parameters: 5 | paths: 6 | - src 7 | - config 8 | excludePaths: 9 | - vendor/* 10 | -------------------------------------------------------------------------------- /src/Command/IndexCreateCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace CmsIg\Seal\Integration\Symfony\Command; 15 | 16 | use CmsIg\Seal\EngineRegistry; 17 | use Symfony\Component\Console\Attribute\AsCommand; 18 | use Symfony\Component\Console\Command\Command; 19 | use Symfony\Component\Console\Input\InputInterface; 20 | use Symfony\Component\Console\Input\InputOption; 21 | use Symfony\Component\Console\Output\OutputInterface; 22 | use Symfony\Component\Console\Style\SymfonyStyle; 23 | 24 | /** 25 | * @experimental 26 | */ 27 | #[AsCommand(name: 'cmsig:seal:index-create', description: 'Create configured search indexes.')] 28 | final class IndexCreateCommand extends Command 29 | { 30 | public function __construct(private readonly EngineRegistry $engineRegistry) 31 | { 32 | parent::__construct(); 33 | } 34 | 35 | protected function configure(): void 36 | { 37 | $this->addOption('engine', null, InputOption::VALUE_REQUIRED, 'The name of the engine to create the schema for.'); 38 | $this->addOption('index', null, InputOption::VALUE_REQUIRED, 'The name of the index to create the schema for.'); 39 | } 40 | 41 | protected function execute(InputInterface $input, OutputInterface $output): int 42 | { 43 | $ui = new SymfonyStyle($input, $output); 44 | /** @var string|null $engineName */ 45 | $engineName = $input->getOption('engine'); 46 | /** @var string|null $indexName */ 47 | $indexName = $input->getOption('index'); 48 | 49 | foreach ($this->engineRegistry->getEngines() as $name => $engine) { 50 | if ($engineName && $engineName !== $name) { 51 | continue; 52 | } 53 | 54 | if ($indexName) { 55 | $ui->text('Creating search index "' . $indexName . '" of "' . $name . '" ...'); 56 | $task = $engine->createIndex($indexName, ['return_slow_promise_result' => true]); 57 | $task->wait(); 58 | 59 | continue; 60 | } 61 | 62 | $ui->text('Creating search indexes of "' . $name . '" ...'); 63 | $task = $engine->createSchema(['return_slow_promise_result' => true]); 64 | $task->wait(); 65 | } 66 | 67 | $ui->success('Search indexes created.'); 68 | 69 | return Command::SUCCESS; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Command/IndexDropCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace CmsIg\Seal\Integration\Symfony\Command; 15 | 16 | use CmsIg\Seal\EngineRegistry; 17 | use Symfony\Component\Console\Attribute\AsCommand; 18 | use Symfony\Component\Console\Command\Command; 19 | use Symfony\Component\Console\Input\InputArgument; 20 | use Symfony\Component\Console\Input\InputInterface; 21 | use Symfony\Component\Console\Input\InputOption; 22 | use Symfony\Component\Console\Output\OutputInterface; 23 | use Symfony\Component\Console\Style\SymfonyStyle; 24 | 25 | /** 26 | * @experimental 27 | */ 28 | #[AsCommand(name: 'cmsig:seal:index-drop', description: 'Drop configured search indexes.')] 29 | final class IndexDropCommand extends Command 30 | { 31 | public function __construct(private readonly EngineRegistry $engineRegistry) 32 | { 33 | parent::__construct(); 34 | } 35 | 36 | protected function configure(): void 37 | { 38 | $this->addArgument('engine', InputArgument::OPTIONAL, 'The name of the engine to create the schema for.'); 39 | $this->addArgument('index', InputArgument::OPTIONAL, 'The name of the index to create the schema for.'); 40 | $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Without force nothing will happen in this command.'); 41 | } 42 | 43 | protected function execute(InputInterface $input, OutputInterface $output): int 44 | { 45 | $ui = new SymfonyStyle($input, $output); 46 | /** @var string|null $engineName */ 47 | $engineName = $input->getArgument('engine'); 48 | /** @var string|null $indexName */ 49 | $indexName = $input->getArgument('index'); 50 | $force = $input->getOption('force'); 51 | 52 | if (!$force) { 53 | $ui->error('You need to use the --force option to drop the search indexes.'); 54 | 55 | return Command::FAILURE; 56 | } 57 | 58 | foreach ($this->engineRegistry->getEngines() as $name => $engine) { 59 | if ($engineName && $engineName !== $name) { 60 | continue; 61 | } 62 | 63 | if ($indexName) { 64 | $ui->text('Dropping search index "' . $indexName . '" for "' . $name . '" ...'); 65 | $task = $engine->dropIndex($indexName, ['return_slow_promise_result' => true]); 66 | $task->wait(); 67 | 68 | continue; 69 | } 70 | 71 | $ui->text('Dropping search indexes of "' . $name . '" ...'); 72 | $task = $engine->dropSchema(['return_slow_promise_result' => true]); 73 | $task->wait(); 74 | } 75 | 76 | $ui->success('Search indexes dropped.'); 77 | 78 | return Command::SUCCESS; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Command/ReindexCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace CmsIg\Seal\Integration\Symfony\Command; 15 | 16 | use CmsIg\Seal\EngineRegistry; 17 | use CmsIg\Seal\Reindex\ReindexConfig; 18 | use CmsIg\Seal\Reindex\ReindexProviderInterface; 19 | use Symfony\Component\Console\Attribute\AsCommand; 20 | use Symfony\Component\Console\Command\Command; 21 | use Symfony\Component\Console\Input\InputInterface; 22 | use Symfony\Component\Console\Input\InputOption; 23 | use Symfony\Component\Console\Output\OutputInterface; 24 | use Symfony\Component\Console\Style\SymfonyStyle; 25 | 26 | /** 27 | * @experimental 28 | */ 29 | #[AsCommand(name: 'cmsig:seal:reindex', description: 'Reindex configured search indexes.')] 30 | final class ReindexCommand extends Command 31 | { 32 | /** 33 | * @param iterable $reindexProviders 34 | */ 35 | public function __construct( 36 | private readonly EngineRegistry $engineRegistry, 37 | private readonly iterable $reindexProviders, 38 | ) { 39 | parent::__construct(); 40 | } 41 | 42 | protected function configure(): void 43 | { 44 | $this->addOption('engine', null, InputOption::VALUE_REQUIRED, 'The name of the engine to create the schema for.'); 45 | $this->addOption('index', null, InputOption::VALUE_REQUIRED, 'The name of the index to create the schema for.'); 46 | $this->addOption('drop', null, InputOption::VALUE_NONE, 'Drop the index before reindexing.'); 47 | $this->addOption('bulk-size', null, InputOption::VALUE_REQUIRED, 'The bulk size for reindexing, defaults to 100.', 100); 48 | $this->addOption('datetime-boundary', null, InputOption::VALUE_REQUIRED, 'Do a partial update and limit to only documents that have been changed since a given datetime object.'); 49 | $this->addOption('identifiers', null, InputOption::VALUE_REQUIRED, 'Do a partial update and limit to only a comma-separated list of identifiers.'); 50 | } 51 | 52 | protected function execute(InputInterface $input, OutputInterface $output): int 53 | { 54 | $ui = new SymfonyStyle($input, $output); 55 | /** @var string|null $engineName */ 56 | $engineName = $input->getOption('engine'); 57 | /** @var string|null $indexName */ 58 | $indexName = $input->getOption('index'); 59 | /** @var bool $drop */ 60 | $drop = $input->getOption('drop'); 61 | /** @var int $bulkSize */ 62 | $bulkSize = $input->getOption('bulk-size'); 63 | /** @var \DateTimeImmutable|null $dateTimeBoundary */ 64 | $dateTimeBoundary = $input->getOption('datetime-boundary') ? new \DateTimeImmutable((string) $input->getOption('datetime-boundary')) : null; // @phpstan-ignore-line 65 | /** @var array $identifiers */ 66 | $identifiers = \array_filter(\explode(',', (string) $input->getOption('identifiers'))); // @phpstan-ignore-line 67 | 68 | $reindexConfig = ReindexConfig::create() 69 | ->withIndex($indexName) 70 | ->withBulkSize($bulkSize) 71 | ->withDropIndex($drop) 72 | ->withDateTimeBoundary($dateTimeBoundary) 73 | ->withIdentifiers($identifiers); 74 | 75 | foreach ($this->engineRegistry->getEngines() as $name => $engine) { 76 | if ($engineName && $engineName !== $name) { 77 | continue; 78 | } 79 | 80 | $ui->section('Engine: ' . $name); 81 | 82 | $progressBar = $ui->createProgressBar(); 83 | 84 | $engine->reindex( 85 | $this->reindexProviders, 86 | $reindexConfig, 87 | function (string $index, int $count, int|null $total) use ($progressBar) { 88 | if (null !== $total) { 89 | $progressBar->setMaxSteps($total); 90 | } 91 | 92 | $progressBar->setMessage($index); 93 | $progressBar->setProgress($count); 94 | }, 95 | ); 96 | 97 | $progressBar->finish(); 98 | 99 | $ui->writeln(''); 100 | $ui->writeln(''); 101 | } 102 | 103 | $ui->success('Search indexes reindexed.'); 104 | 105 | return Command::SUCCESS; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/SealBundle.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace CmsIg\Seal\Integration\Symfony; 15 | 16 | use CmsIg\Seal\Adapter\AdapterInterface; 17 | use CmsIg\Seal\Adapter\Multi\MultiAdapterFactory; 18 | use CmsIg\Seal\Adapter\ReadWrite\ReadWriteAdapterFactory; 19 | use CmsIg\Seal\Engine; 20 | use CmsIg\Seal\EngineInterface; 21 | use CmsIg\Seal\Reindex\ReindexProviderInterface; 22 | use CmsIg\Seal\Schema\Loader\PhpFileLoader; 23 | use CmsIg\Seal\Schema\Schema; 24 | use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; 25 | use Symfony\Component\DependencyInjection\ContainerBuilder; 26 | use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; 27 | use Symfony\Component\DependencyInjection\Reference; 28 | use Symfony\Component\HttpKernel\Bundle\AbstractBundle; 29 | 30 | /** 31 | * @experimental 32 | */ 33 | final class SealBundle extends AbstractBundle 34 | { 35 | protected string $extensionAlias = 'cmsig_seal'; 36 | 37 | public function configure(DefinitionConfigurator $definition): void 38 | { 39 | // @phpstan-ignore-next-line 40 | $definition->rootNode() 41 | ->children() 42 | ->scalarNode('index_name_prefix')->defaultValue('')->end() 43 | ->arrayNode('schemas') 44 | ->useAttributeAsKey('name') 45 | ->arrayPrototype() 46 | ->children() 47 | ->scalarNode('dir')->end() 48 | ->scalarNode('engine')->defaultNull()->end() 49 | ->end() 50 | ->end() 51 | ->end() 52 | ->arrayNode('engines') 53 | ->useAttributeAsKey('name') 54 | ->arrayPrototype() 55 | ->children() 56 | ->scalarNode('adapter')->end() 57 | ->end() 58 | ->end() 59 | ->end() 60 | ->end(); 61 | } 62 | 63 | /** 64 | * @param array{ 65 | * index_name_prefix: string, 66 | * engines: array, 67 | * schemas: array, 68 | * } $config 69 | */ 70 | public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void 71 | { 72 | $indexNamePrefix = $config['index_name_prefix']; 73 | $engines = $config['engines']; 74 | $schemas = $config['schemas']; 75 | 76 | $engineSchemaDirs = []; 77 | foreach ($schemas as $options) { 78 | $engineSchemaDirs[$options['engine'] ?? 'default'][] = $options['dir']; 79 | } 80 | 81 | foreach ($engines as $name => $engineConfig) { 82 | $adapterServiceId = 'cmsig_seal.adapter.' . $name; 83 | $engineServiceId = 'cmsig_seal.engine.' . $name; 84 | $schemaLoaderServiceId = 'cmsig_seal.schema_loader.' . $name; 85 | $schemaId = 'cmsig_seal.schema.' . $name; 86 | 87 | $definition = $builder->register($adapterServiceId, AdapterInterface::class) 88 | ->setFactory([new Reference('cmsig_seal.adapter_factory'), 'createAdapter']) 89 | ->setArguments([$engineConfig['adapter']]) 90 | ->addTag('cmsig_seal.adapter', ['name' => $name]); 91 | 92 | if (\class_exists(ReadWriteAdapterFactory::class) || \class_exists(MultiAdapterFactory::class)) { 93 | // the read-write and multi adapter require access all other adapters so they need to be public 94 | $definition->setPublic(true); 95 | } 96 | 97 | $dirs = $engineSchemaDirs[$name] ?? []; 98 | 99 | $builder->register($schemaLoaderServiceId, PhpFileLoader::class) 100 | ->setArguments([$dirs, $indexNamePrefix]); 101 | 102 | $builder->register($schemaId, Schema::class) 103 | ->setFactory([new Reference($schemaLoaderServiceId), 'load']); 104 | 105 | $builder->register($engineServiceId, Engine::class) 106 | ->setArguments([ 107 | new Reference($adapterServiceId), 108 | new Reference($schemaId), 109 | ]) 110 | ->addTag('cmsig_seal.engine', ['name' => $name]); 111 | 112 | if ('default' === $name || (!isset($engines['default']) && !$builder->has(EngineInterface::class))) { 113 | $builder->setAlias(EngineInterface::class, $engineServiceId); 114 | $builder->setAlias(Schema::class, $schemaId); 115 | } 116 | 117 | $builder->registerAliasForArgument( 118 | $engineServiceId, 119 | EngineInterface::class, 120 | $name . 'Engine', 121 | ); 122 | 123 | $builder->registerAliasForArgument( 124 | $schemaId, 125 | Schema::class, 126 | $name . 'Schema', 127 | ); 128 | } 129 | 130 | $builder->registerForAutoconfiguration(ReindexProviderInterface::class) 131 | ->addTag('cmsig_seal.reindex_provider'); 132 | 133 | $container->import(\dirname(__DIR__) . '/config/services.php'); 134 | } 135 | } 136 | --------------------------------------------------------------------------------