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