├── .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 |

8 |
9 |
10 |
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 |
--------------------------------------------------------------------------------