├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── Annotations
├── CustomIndex.php
└── CustomIndexes.php
├── Command
└── IndexUpdateCommand.php
├── DBAL
├── ExtendedPlatform.php
└── QueryExecutor.php
├── DTO
└── CustomIndex.php
├── DependencyInjection
├── Configuration.php
└── IntaroCustomIndexExtension.php
├── Dockerfile
├── IntaroCustomIndexBundle.php
├── LICENSE
├── Makefile
├── Metadata
├── Attribute
│ └── CustomIndex.php
├── Reader.php
└── ReaderInterface.php
├── README.md
├── UPGRADE.md
├── Validator
└── Constraints
│ ├── AllowedIndexType.php
│ └── AllowedIndexTypeValidator.php
├── composer.json
├── config
└── di.php
├── docker-compose.yml
└── phpstan.neon.dist
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | tests:
6 | name: PHPUnit PHP ${{ matrix.php }} ${{ matrix.dependency }} (Symfony ${{ matrix.symfony }})
7 | runs-on: ubuntu-22.04
8 | strategy:
9 | matrix:
10 | php:
11 | - '8.1'
12 | - '8.2'
13 | - '8.3'
14 | symfony:
15 | - '5.4.*'
16 | - '6.4.*'
17 | fail-fast: true
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v4
21 |
22 | - name: Setup PHP
23 | uses: shivammathur/setup-php@v2
24 | with:
25 | php-version: ${{ matrix.php }}
26 |
27 | - name: Configure Symfony
28 | run: composer config extra.symfony.require "${{ matrix.symfony }}"
29 |
30 | - name: Update project dependencies
31 | run: composer update --no-progress --ansi --prefer-stable
32 |
33 | - name: Validate composer
34 | run: composer validate --strict --no-check-lock
35 |
36 | - name: PHP-CS-Fixer
37 | run: vendor/bin/php-cs-fixer check -vv
38 |
39 | - name: PHPStan
40 | run: vendor/bin/phpstan analyse
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.lock
2 | composer.phar
3 | .php_cs.cache
4 | /vendor
5 |
6 | # Other
7 | .DS_Store
8 | *~
9 | /nbproject/*
10 | /*tags*
11 | .idea
12 | .idea/*
13 | /.idea
14 | /.idea/*
15 | .travis.yml
16 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | in(__DIR__)
7 | ;
8 |
9 | return Retailcrm\PhpCsFixer\Defaults::rules([
10 | 'no_trailing_whitespace_in_string' => false,
11 | ])
12 | ->setFinder($finder)
13 | ->setCacheFile(__DIR__ . '/.php_cs.cache/results')
14 | ;
15 |
--------------------------------------------------------------------------------
/Annotations/CustomIndex.php:
--------------------------------------------------------------------------------
1 | ON
[USING <$method>] ( <$columns> ) [WHERE <$where>]
14 | */
15 | class CustomIndex extends Annotation
16 | {
17 | /**
18 | * Index name
19 | *
20 | * @var string
21 | */
22 | public $name;
23 |
24 | /**
25 | * Index is unique
26 | *
27 | * @var bool
28 | */
29 | public $unique = false;
30 |
31 | /**
32 | * Using index structure (btree, hash, gist, or gin)
33 | *
34 | * @var string
35 | */
36 | public $using;
37 |
38 | /**
39 | * For partial index
40 | *
41 | * @var string
42 | */
43 | public $where;
44 |
45 | /**
46 | * Index columns
47 | *
48 | * @var array
49 | */
50 | public $columns;
51 | }
52 |
--------------------------------------------------------------------------------
/Annotations/CustomIndexes.php:
--------------------------------------------------------------------------------
1 | addOption(self::DUMP_SQL_OPTION, null, InputOption::VALUE_NONE, 'Dump sql instead creating index');
40 | }
41 |
42 | protected function execute(InputInterface $input, OutputInterface $output): int
43 | {
44 | $this->input = $input;
45 | $this->output = $output;
46 |
47 | $connection = $this->em->getConnection();
48 | $platform = $this->createExtendedPlatform($connection->getDatabasePlatform());
49 | $indexesNames = $this->queryExecutor->getIndexesNames($platform, $this->searchInAllSchemas);
50 | $currentSchema = $this->queryExecutor->getCurrentSchema($platform);
51 | $customIndexes = $this->reader->getIndexes($currentSchema, $this->searchInAllSchemas);
52 |
53 | $this->dropIndexes($indexesNames, $customIndexes, $platform);
54 | $this->createIndexes($indexesNames, $customIndexes, $platform);
55 |
56 | return Command::SUCCESS;
57 | }
58 |
59 | /**
60 | * @param array $indexesNames
61 | * @param array $customIndexes
62 | */
63 | private function createIndexes(array $indexesNames, array $customIndexes, ExtendedPlatform $platform): void
64 | {
65 | $createFlag = false;
66 | foreach ($customIndexes as $name => $index) {
67 | if (!in_array($name, $indexesNames, true)) {
68 | $this->createIndex($platform, $index);
69 | $createFlag = true;
70 | }
71 | }
72 | if (!$createFlag) {
73 | $this->output->writeln('No index was created');
74 | }
75 | }
76 |
77 | /**
78 | * @param array $indexesNames
79 | * @param array $customIndexes
80 | */
81 | private function dropIndexes(array $indexesNames, array $customIndexes, ExtendedPlatform $platform): void
82 | {
83 | $dropFlag = false;
84 | foreach ($indexesNames as $indexName) {
85 | if (!array_key_exists($indexName, $customIndexes)) {
86 | $this->dropIndex($platform, $this->quoteSchema($indexName));
87 | $dropFlag = true;
88 | }
89 | }
90 |
91 | if (!$dropFlag) {
92 | $this->output->writeln('No index was dropped.');
93 | }
94 | }
95 |
96 | private function dropIndex(ExtendedPlatform $platform, string $indexName): void
97 | {
98 | if ($this->input->getOption(self::DUMP_SQL_OPTION)) {
99 | $this->output->writeln($platform->getDropIndexSQL($indexName, '') . ';');
100 |
101 | return;
102 | }
103 |
104 | $this->queryExecutor->dropIndex($platform, $indexName);
105 | $this->output->writeln('Index ' . $indexName . ' was dropped.');
106 | }
107 |
108 | private function createIndex(ExtendedPlatform $platform, CustomIndex $index): void
109 | {
110 | $errors = $this->validator->validate($index);
111 | if (!count($errors)) {
112 | if ($this->input->getOption(self::DUMP_SQL_OPTION)) {
113 | $this->output->writeln($platform->createIndexSQL($index) . ';');
114 |
115 | return;
116 | }
117 |
118 | $this->queryExecutor->createIndex($platform, $index);
119 | $this->output->writeln('Index ' . $index->getName() . ' was created.');
120 |
121 | return;
122 | }
123 |
124 | $this->output->writeln('Index ' . $index->getName() . ' was not created.');
125 |
126 | foreach ($errors as $error) {
127 | $this->output->writeln('' . $error->getMessage() . '');
128 | }
129 | }
130 |
131 | private function quoteSchema(string $name): string
132 | {
133 | $parts = explode('.', $name);
134 | $parts[0] = '"' . $parts[0] . '"';
135 |
136 | return implode('.', $parts);
137 | }
138 |
139 | private function createExtendedPlatform(AbstractPlatform $platform): ExtendedPlatform
140 | {
141 | return match (true) {
142 | $platform instanceof PostgreSQLPlatform => new ExtendedPlatform(),
143 | default => throw new \LogicException(sprintf('Platform %s does not support', $platform::class)),
144 | };
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/DBAL/ExtendedPlatform.php:
--------------------------------------------------------------------------------
1 | getUnique()) {
14 | $sql .= ' UNIQUE';
15 | }
16 |
17 | $sql .= ' INDEX ' . $index->getName();
18 | $sql .= ' ON ' . $index->getTableName();
19 |
20 | if ($index->getUsing()) {
21 | $sql .= ' USING ' . $index->getUsing();
22 | }
23 |
24 | $sql .= ' (' . implode(', ', $index->getColumns()) . ')';
25 |
26 | if ($index->getWhere()) {
27 | $sql .= ' WHERE ' . $index->getWhere();
28 | }
29 |
30 | return $sql;
31 | }
32 |
33 | public function dropIndexSQL(string $indexName): string
34 | {
35 | return "DROP index $indexName";
36 | }
37 |
38 | public function currentSchemaSelectSQL(): string
39 | {
40 | return 'SELECT current_schema()';
41 | }
42 |
43 | public function indexesNamesSelectSQL(bool $searchInAllSchemas): string
44 | {
45 | $sql = "
46 | SELECT schemaname || '.' || indexname as relname
47 | FROM pg_indexes
48 | WHERE indexname LIKE :indexName
49 | AND indexname NOT LIKE '%_ccnew'
50 | ";
51 | if (!$searchInAllSchemas) {
52 | $sql .= ' AND schemaname = current_schema()';
53 | }
54 |
55 | return $sql;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/DBAL/QueryExecutor.php:
--------------------------------------------------------------------------------
1 | createIndexSQL($customIndex);
17 | $statement = $this->connection->prepare($sql);
18 | $statement->executeStatement();
19 | }
20 |
21 | public function dropIndex(ExtendedPlatform $platform, string $indexName): void
22 | {
23 | $sql = $platform->dropIndexSQL($indexName);
24 | $statement = $this->connection->prepare($sql);
25 | $statement->executeStatement();
26 | }
27 |
28 | /** @return array */
29 | public function getIndexesNames(ExtendedPlatform $platform, bool $searchInAllSchemas): array
30 | {
31 | $sql = $platform->indexesNamesSelectSQL($searchInAllSchemas);
32 | $statement = $this->connection->prepare($sql);
33 | $statement->bindValue('indexName', CustomIndex::PREFIX . '%');
34 | $result = $statement->executeQuery();
35 |
36 | $indexesNames = [];
37 | $data = $result->fetchAllAssociative();
38 | foreach ($data as $row) {
39 | $indexesNames[] = $row['relname'];
40 | }
41 |
42 | return $indexesNames;
43 | }
44 |
45 | public function getCurrentSchema(ExtendedPlatform $platform): string
46 | {
47 | $sql = $platform->currentSchemaSelectSQL();
48 | $statement = $this->connection->prepare($sql);
49 | $result = $statement->executeQuery();
50 | $data = $result->fetchAssociative();
51 | $currentSchema = $data['current_schema'] ?? null;
52 | if (null === $currentSchema) {
53 | throw new \LogicException('Current schema not found');
54 | }
55 |
56 | return $currentSchema;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/DTO/CustomIndex.php:
--------------------------------------------------------------------------------
1 | 'string',
21 | 'message' => 'Column should be type of string',
22 | ]),
23 | ])]
24 | private array $columns = [];
25 |
26 | private bool $unique;
27 | #[AllowedIndexType]
28 | private ?string $using;
29 | private ?string $where;
30 | #[Assert\Length(min: 1, max: 63, minMessage: 'TableName must be set', maxMessage: 'TableName is too long')]
31 | private string $tableName;
32 | private string $schema;
33 | private string $currentSchema;
34 |
35 | /**
36 | * @param string[]|string $columns
37 | */
38 | public function __construct(
39 | string $tableName,
40 | string $schema,
41 | string $currentSchema,
42 | array|string $columns,
43 | ?string $name = null,
44 | bool $unique = false,
45 | ?string $using = null,
46 | ?string $where = null,
47 | ) {
48 | $this->tableName = $tableName;
49 | $this->setColumns($columns);
50 | $this->unique = $unique;
51 | $this->using = $using;
52 | $this->where = (string) $where;
53 | $this->schema = $schema;
54 | $this->currentSchema = $currentSchema;
55 |
56 | if (!empty($name)) {
57 | $this->setName($name);
58 |
59 | return;
60 | }
61 |
62 | $this->generateName();
63 | }
64 |
65 | public function getTableName(): string
66 | {
67 | if ($this->schema !== $this->currentSchema) {
68 | return $this->schema . '.' . $this->tableName;
69 | }
70 |
71 | return $this->tableName;
72 | }
73 |
74 | public function getSchema(): ?string
75 | {
76 | return $this->schema;
77 | }
78 |
79 | public function getName(): ?string
80 | {
81 | return $this->name;
82 | }
83 |
84 | /**
85 | * @return string[]
86 | */
87 | public function getColumns(): array
88 | {
89 | return $this->columns;
90 | }
91 |
92 | public function getUnique(): bool
93 | {
94 | return $this->unique;
95 | }
96 |
97 | public function getUsing(): ?string
98 | {
99 | return $this->using;
100 | }
101 |
102 | public function getWhere(): ?string
103 | {
104 | return $this->where;
105 | }
106 |
107 | private function generateName(): void
108 | {
109 | $columns = $this->getColumns();
110 | $strToMd5 = $this->getTableName();
111 | foreach ($columns as $column) {
112 | $strToMd5 .= $column;
113 | }
114 |
115 | $strToMd5 .= $this->getUsing() . ($this->getWhere() ? '_' . $this->getWhere() : '');
116 | $name = self::PREFIX . ($this->getUnique() ? self::UNIQUE . '_' : '') . md5($strToMd5);
117 | $this->setName($name);
118 | }
119 |
120 | /**
121 | * @param string[]|string $columns
122 | */
123 | private function setColumns(array|string $columns): void
124 | {
125 | if (is_string($columns)) {
126 | $columns = [$columns];
127 | }
128 |
129 | $this->columns = [];
130 | foreach ($columns as $column) {
131 | if (!empty($column)) {
132 | $this->columns[] = $column;
133 | }
134 | }
135 | }
136 |
137 | private function setName(string $name): void
138 | {
139 | if (!str_starts_with($name, self::PREFIX)) {
140 | $name = self::PREFIX . $name;
141 | }
142 |
143 | $this->name = $name;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | getRootNode();
16 | $rootNode
17 | ->children()
18 | // if true update indexes in all db schemas
19 | // else update only in current schema
20 | ->booleanNode('search_in_all_schemas')
21 | ->defaultTrue()
22 | ->end()
23 | ->arrayNode('allowed_index_types')
24 | ->prototype('scalar')
25 | ->validate()
26 | ->ifNotInArray(self::AVAILABLE_INDEX_TYPES)
27 | ->thenInvalid('Unknown index type. Allowed types: ' . implode(', ', self::AVAILABLE_INDEX_TYPES) . '.')
28 | ->end()
29 | ->end()
30 | ->cannotBeEmpty()
31 | ->defaultValue(self::AVAILABLE_INDEX_TYPES)
32 | ->end()
33 | ->end()
34 | ;
35 |
36 | return $treeBuilder;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/DependencyInjection/IntaroCustomIndexExtension.php:
--------------------------------------------------------------------------------
1 | load('di.php');
16 |
17 | $configuration = new Configuration();
18 | $config = $this->processConfiguration($configuration, $configs);
19 | $container->setParameter('intaro.custom_index.search_in_all_schemas', $config['search_in_all_schemas']);
20 | $container->setParameter('intaro.custom.index.allowed_index_types', $config['allowed_index_types']);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG PHP_IMAGE_TAG
2 | FROM php:${PHP_IMAGE_TAG}-cli-alpine
3 |
4 | COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
5 |
6 | WORKDIR /opt/test
7 |
--------------------------------------------------------------------------------
/IntaroCustomIndexBundle.php:
--------------------------------------------------------------------------------
1 | $columns */
9 | public function __construct(
10 | public ?string $name = null,
11 | public array $columns = [],
12 | public ?string $where = null,
13 | public ?string $using = null,
14 | public bool $unique = false,
15 | ) {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Metadata/Reader.php:
--------------------------------------------------------------------------------
1 | em->getMetadataFactory()->getAllMetadata();
21 | $indexNamesToCustomIndexes = [];
22 | $abstractClassesInfo = $this->getAbstractClassesInfo($metadata);
23 | foreach ($metadata as $meta) {
24 | if ($this->isAbstract($meta)) {
25 | continue;
26 | }
27 |
28 | $this->collect($indexNamesToCustomIndexes, $meta, $meta->getTableName(), $currentSchema, $searchInAllSchemas);
29 | $parentsMeta = $this->searchParentsWithIndex($meta, $abstractClassesInfo);
30 | foreach ($parentsMeta as $parentMeta) {
31 | $tableName = $this->getTableNameFromMetadata($meta, $parentMeta);
32 | $this->collect($indexNamesToCustomIndexes, $parentMeta, $tableName, $currentSchema, $searchInAllSchemas, true);
33 | }
34 | }
35 |
36 | return $indexNamesToCustomIndexes;
37 | }
38 |
39 | /**
40 | * @param array $indexNamesToCustomIndexes
41 | * @param ClassMetadata $metadata
42 | */
43 | private function collect(
44 | array &$indexNamesToCustomIndexes,
45 | ClassMetadata $metadata,
46 | string $tableName,
47 | string $currentSchema,
48 | bool $searchInAllSchemas,
49 | bool $tablePostfix = false,
50 | ): void {
51 | $reflectionAttributes = $this->getCustomIndexesAttributes($metadata);
52 | if (empty($reflectionAttributes)) {
53 | return;
54 | }
55 |
56 | foreach ($reflectionAttributes as $attribute) {
57 | $schema = $metadata->getSchemaName() ?: $currentSchema;
58 | // skip index from side schema in single schema mode
59 | if (!$searchInAllSchemas && $schema !== $currentSchema) {
60 | continue;
61 | }
62 |
63 | $attributeArguments = $attribute->getArguments();
64 | $name = $attributeArguments['name'] ?? '';
65 | $index = new CustomIndex(
66 | $tableName,
67 | $schema,
68 | $currentSchema,
69 | $attributeArguments['columns'] ?? [],
70 | $name . ($name && $tablePostfix ? '_' . $tableName : ''),
71 | $attributeArguments['unique'] ?? false,
72 | $attributeArguments['using'] ?? null,
73 | $attributeArguments['where'] ?? null,
74 | );
75 |
76 | $key = $schema . '.' . $index->getName();
77 | $indexNamesToCustomIndexes[$key] = $index;
78 | }
79 | }
80 |
81 | /**
82 | * @param ClassMetadata $metadata
83 | * @param ClassMetadata $parentMetadata
84 | */
85 | private function getTableNameFromMetadata(ClassMetadata $metadata, ClassMetadata $parentMetadata): string
86 | {
87 | if (ClassMetadata::INHERITANCE_TYPE_JOINED === $metadata->inheritanceType) {
88 | return $parentMetadata->getTableName();
89 | }
90 |
91 | return $metadata->getTableName();
92 | }
93 |
94 | /**
95 | * @param ClassMetadata $meta
96 | *
97 | * @return array<\ReflectionAttribute>
98 | */
99 | private function getCustomIndexesAttributes(ClassMetadata $meta): array
100 | {
101 | return $this->getMetaReflectionClass($meta)->getAttributes(Attribute\CustomIndex::class);
102 | }
103 |
104 | /**
105 | * @param ClassMetadata $meta
106 | */
107 | private function isAbstract(ClassMetadata $meta): bool
108 | {
109 | return $this->getMetaReflectionClass($meta)->isAbstract();
110 | }
111 |
112 | /**
113 | * @param array> $metadata
114 | *
115 | * @return array
116 | */
117 | private function getAbstractClassesInfo(array $metadata): array
118 | {
119 | $abstractClasses = [];
120 | foreach ($metadata as $meta) {
121 | if ($this->isAbstract($meta)) {
122 | $abstractClasses[$meta->getName()] = $meta;
123 | }
124 | }
125 |
126 | return $abstractClasses;
127 | }
128 |
129 | /**
130 | * @param ClassMetadata $meta
131 | * @param array $abstractClasses
132 | *
133 | * @return array
134 | */
135 | private function searchParentsWithIndex(ClassMetadata $meta, array $abstractClasses): array
136 | {
137 | $reflectionClass = $this->getMetaReflectionClass($meta);
138 | $parentMeta = [];
139 | foreach ($abstractClasses as $entityName => $entityMeta) {
140 | if ($reflectionClass->isSubclassOf($entityName)) {
141 | $parentMeta[$entityName] = $entityMeta;
142 | }
143 | }
144 |
145 | return $parentMeta;
146 | }
147 |
148 | /**
149 | * @param ClassMetadata $meta
150 | *
151 | * @return \ReflectionClass
152 | */
153 | private function getMetaReflectionClass(ClassMetadata $meta): \ReflectionClass
154 | {
155 | $reflectionClass = $meta->getReflectionClass();
156 | if (null === $reflectionClass) {
157 | throw new \RuntimeException('Reflection class is not found for ' . $meta->getName());
158 | }
159 |
160 | return $reflectionClass;
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/Metadata/ReaderInterface.php:
--------------------------------------------------------------------------------
1 | */
10 | public function getIndexes(string $currentSchema, bool $searchInAllSchemas): array;
11 | }
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CustomIndexBundle
2 |
3 | The CustomIndexBundle allows create index for doctrine entities using attribute with entity definition and console command.
4 |
5 | ## Installation
6 |
7 | CustomIndexBundle requires Symfony 5 or higher. Works only with PostgreSQL.
8 |
9 | Run into your project directory:
10 | ```
11 | $ composer require intaro/custom-index-bundle
12 | ```
13 |
14 | Register the bundle in `config/bundles.php`:
15 |
16 | ```php
17 |
18 | ['all' => true],
23 | ];
24 | ```
25 |
26 | If your project have many schemas in single database and command must generate custom indexes only for one schema then add in your `config.yml`:
27 |
28 | ```yaml
29 | intaro_custom_index:
30 | search_in_all_schemas: false
31 | allowed_index_types: ['gin', 'gist', 'btree', 'hash']
32 |
33 | ```
34 |
35 | Default value of `search_in_all_schemas` is `true`.
36 | If you have different entities in different schemas and you need to update custom indexes in all schemas at once then you must set `search_in_all_schemas` to `true` or omit this config.
37 | If you have database with only public schema then `search_in_all_schemas` value doesn't matter.
38 |
39 | Parameter `allowed_index_types` helps to exclude some types of indexes. If someone will try to use excluded type, command `intaro:doctrine:index:update` will return an error.
40 | Default value is `['gin', 'gist', 'btree', 'hash']`.
41 |
42 | ## Usage
43 |
44 | 1) Add attributes in your entity
45 |
46 | ```php
47 | '`).
71 | * `unique` - index is unique (default = false).
72 | * `using` - corresponds to `USING` directive in PostgreSQL `CREATE INDEX` command.
73 | * `where` - corresponds to `WHERE` directive in PostgreSQL `CREATE INDEX` command.
74 |
75 | Required only `columns` property.
76 |
77 | 2) Use `intaro:doctrine:index:update` command for update db.
78 |
79 | ```
80 | php app/console intaro:doctrine:index:update
81 | ```
82 |
83 | You may use `dump-sql` parameter for dump sql with `DROP/CREATE INDEX` commands
84 |
85 | ```
86 | php app/console intaro:doctrine:index:update --dump-sql
87 | ```
88 |
89 | ## Examples
90 |
91 | Create index using `pg_trgm` extension:
92 | ```php
93 |
5 | Minimum php version raised to 8.1
6 | Removed support for symfony < 5
7 | Now you must always pass an array in a property `columns` (previously it was possible to pass a string/array)
8 | Replaced the use of annotations with attributes
9 |
10 |
11 | ### Automatic transition with Rector
12 |
13 | You may use rector to automatically convert your code to the new version.
14 |
15 | ```php
16 | paths(['src']);
27 |
28 | $rectorConfig->ruleWithConfiguration(NestedAnnotationToAttributeRector::class, [
29 | new NestedAnnotationToAttribute(CustomIndexes::class, [
30 | new AnnotationPropertyToAttributeClass(CustomIndex::class, 'indexes'),
31 | ], true),
32 | ]);
33 | };
34 | ```
35 |
--------------------------------------------------------------------------------
/Validator/Constraints/AllowedIndexType.php:
--------------------------------------------------------------------------------
1 | $allowedIndexTypes
13 | */
14 | public function __construct(private readonly array $allowedIndexTypes)
15 | {
16 | }
17 |
18 | public function validate(mixed $value, Constraint $constraint): void
19 | {
20 | if (!$constraint instanceof AllowedIndexType) {
21 | throw new UnexpectedTypeException($constraint, AllowedIndexType::class);
22 | }
23 |
24 | if (null === $value || '' === $value) {
25 | return;
26 | }
27 |
28 | if (!is_string($value)) {
29 | throw new UnexpectedTypeException($value, 'string');
30 | }
31 |
32 | if (!in_array($value, $this->allowedIndexTypes, true)) {
33 | $this->context->addViolation(
34 | $constraint->message,
35 | [
36 | '{{ type }}' => $value,
37 | '{{ allowed_types }}' => implode(', ', $this->allowedIndexTypes),
38 | ]
39 | );
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "intaro/custom-index-bundle",
3 | "description": "Annotation and command for control entity custom indexes",
4 | "keywords": ["symfony", "index", "postgresql-index"],
5 | "type": "symfony-bundle",
6 | "license": "MIT",
7 | "authors": [{
8 | "name": "Chernyavtsev Ivan",
9 | "email": "chernyavtsev@intaro.ru",
10 | "role": "Developer"
11 | }],
12 | "require": {
13 | "php": "^8.1",
14 | "doctrine/orm": "^2.2.3 || ^3.0",
15 | "symfony/config": "^5.0 || ^6.0",
16 | "symfony/console": "^5.0 || ^6.0",
17 | "symfony/dependency-injection": "^5.0 || ^6.0",
18 | "symfony/http-kernel": "^5.0 || ^6.0",
19 | "symfony/validator": "^5.0 || ^6.0"
20 | },
21 | "require-dev": {
22 | "friendsofphp/php-cs-fixer": "^3.59",
23 | "phpstan/phpstan": "^1.11",
24 | "retailcrm/php-code-style": "^1.0"
25 | },
26 | "autoload": {
27 | "psr-4": {
28 | "Intaro\\CustomIndexBundle\\": ""
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/config/di.php:
--------------------------------------------------------------------------------
1 | services()
13 | ->defaults()
14 | ->autowire()
15 | ->autoconfigure()
16 | ;
17 |
18 | $services
19 | ->set('intaro_custom_index.allowed_index_type_validator', AllowedIndexTypeValidator::class)
20 | ->arg('$allowedIndexTypes', '%intaro.custom.index.allowed_index_types%')
21 | ->tag('validator.constraint_validator')
22 |
23 | ->set('intaro_custom_index.index_annotation_reader', Reader::class)
24 |
25 | ->set('intaro_custom_index.query_executor', QueryExecutor::class)
26 |
27 | ->set('intaro_custom_index.command.index_update_command', IndexUpdateCommand::class)
28 | ->arg('$reader', service('intaro_custom_index.index_annotation_reader'))
29 | ->arg('$queryExecutor', service('intaro_custom_index.query_executor'))
30 | ->arg('$searchInAllSchemas', '%intaro.custom_index.search_in_all_schemas%')
31 | ;
32 | };
33 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | php:
3 | build:
4 | context: .
5 | args:
6 | PHP_IMAGE_TAG: ${PHP_IMAGE_TAG:-8.1}
7 | volumes:
8 | - "./:/opt/test"
9 |
--------------------------------------------------------------------------------
/phpstan.neon.dist:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 8
3 | paths:
4 | - Command/
5 | - config/
6 | - DBAL/
7 | - DTO/
8 | - Metadata/
9 | - Validator/
10 | - IntaroCustomIndexBundle.php
11 |
--------------------------------------------------------------------------------