├── .gitignore ├── rector.php ├── tests └── ApipMigrateOpenApiRector │ ├── config │ └── configured_rule.php │ ├── Fixture │ ├── api-resource-no-changes.php.inc │ ├── api-resource-double-props.php.inc │ ├── api-property.php.inc │ └── api-resource.php.inc │ └── ApipMigrateOpenApiRectorTest.php ├── src ├── MyBetterStandardPrinter.php └── ApipMigrateOpenApiRector.php ├── composer.json ├── phpunit.xml ├── .php-cs-fixer.php ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.php-cs-fixer.cache 2 | /.phpunit.cache 3 | /composer.lock 4 | /vendor/ 5 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 9 | __DIR__ . '/src', 10 | __DIR__ . '/tests', 11 | ]) 12 | ->withPhpSets(php83: true) 13 | ->withTypeCoverageLevel(\PHP_INT_MAX) 14 | ->withDeadCodeLevel(\PHP_INT_MAX) 15 | ->withCodeQualityLevel(\PHP_INT_MAX) 16 | ->withRules([ 17 | // add your custom rules here 18 | ]) 19 | ; 20 | -------------------------------------------------------------------------------- /tests/ApipMigrateOpenApiRector/config/configured_rule.php: -------------------------------------------------------------------------------- 1 | rule(App\ApipMigrateOpenApiRector::class); 10 | $rectorConfig->importNames(); 11 | }; 12 | 13 | return static function (RectorConfig $config) use ($builder): void { 14 | $config->singleton(BetterStandardPrinter::class, fn ($app): \App\MyBetterStandardPrinter => new MyBetterStandardPrinter($app->get(ExprAnalyzer::class))); 15 | 16 | $builder($config); 17 | }; 18 | -------------------------------------------------------------------------------- /tests/ApipMigrateOpenApiRector/Fixture/api-resource-no-changes.php.inc: -------------------------------------------------------------------------------- 1 | onMultiline($nodes)) { 13 | return $this->pCommaSeparatedMultiline($nodes, true) . $this->nl; 14 | } 15 | if (!$this->hasNodeWithComments($nodes)) { 16 | return $this->pCommaSeparated($nodes); 17 | } 18 | 19 | return $this->pCommaSeparatedMultiline($nodes, $trailingComma) . $this->nl; 20 | } 21 | 22 | private function onMultiline(array $nodes): bool 23 | { 24 | foreach ($nodes as $node) { 25 | if ($node->getAttribute('multiline')) { 26 | return true; 27 | } 28 | } 29 | 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "require": { 4 | "rector/rector": "dev-main as 2.0.10@dev", 5 | "stevebauman/unfinalize": "^2.1.1" 6 | }, 7 | "require-dev": { 8 | "api-platform/core": "^4.0.18", 9 | "phpunit/phpunit": "^11.5.10", 10 | "symfony/var-dumper": "^7.2.3" 11 | }, 12 | "autoload": { 13 | "psr-4": { 14 | "App\\": "src/" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "App\\Tests\\": "tests/" 20 | } 21 | }, 22 | "config": { 23 | "bump-after-update": true, 24 | "sort-packages": true 25 | }, 26 | "scripts": { 27 | "post-update-cmd": [ 28 | "@php vendor/bin/unfinalize run vendor/rector/rector/src/PhpParser/Printer/BetterStandardPrinter.php" 29 | ], 30 | "post-install-cmd": [ 31 | "@php vendor/bin/unfinalize run vendor/rector/rector/src/PhpParser/Printer/BetterStandardPrinter.php" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | tests 20 | 21 | 22 | 23 | 24 | 25 | src 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ->notPath('Fixture') 6 | ; 7 | 8 | return (new PhpCsFixer\Config()) 9 | ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) 10 | ->setRiskyAllowed(true) 11 | ->setRules([ 12 | '@PHP83Migration' => true, 13 | '@PhpCsFixer' => true, 14 | '@Symfony' => true, 15 | '@Symfony:risky' => true, 16 | 'php_unit_internal_class' => false, // From @PhpCsFixer but we don't want it 17 | 'php_unit_test_class_requires_covers' => false, // From @PhpCsFixer but we don't want it 18 | 'phpdoc_add_missing_param_annotation' => false, // From @PhpCsFixer but we don't want it 19 | 'method_chaining_indentation' => false, // Do not work well with our ES Repository 20 | 'concat_space' => ['spacing' => 'one'], 21 | 'trailing_comma_in_multiline' => ['elements' => ['arrays', 'match', 'parameters']], 22 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 23 | ]) 24 | ->setFinder($finder) 25 | ; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Grégoire Pineau 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 | -------------------------------------------------------------------------------- /tests/ApipMigrateOpenApiRector/Fixture/api-resource-double-props.php.inc: -------------------------------------------------------------------------------- 1 | 'Publish a version', 'parameters' => [['name' => 'id', 'type' => 'string', 'in' => 'path', 'required' => true, 'description' => 'The id of the version']]], 13 | openapi: new Operation(operationId: 'publishVersion'), 14 | ), 15 | ], 16 | )] 17 | class Draft 18 | { 19 | } 20 | ----- 21 | 'id', 'type' => 'string', 'in' => 'path', 'required' => true, 'description' => 'The id of the version']], 34 | operationId: 'publishVersion', 35 | )), 36 | ], 37 | )] 38 | class Draft 39 | { 40 | } 41 | -------------------------------------------------------------------------------- /tests/ApipMigrateOpenApiRector/Fixture/api-property.php.inc: -------------------------------------------------------------------------------- 1 | 'object', 15 | 'properties' => [ 16 | 'allowed' => ['type' => 'boolean'], 17 | 'providers' => ['type' => 'object', 'additionalProperties' => ['type' => 'boolean']], 18 | ], 19 | ])] 20 | public ?array $oAuthRestriction = null, 21 | ) { 22 | } 23 | } 24 | ----- 25 | 'object', 39 | 'properties' => [ 40 | 'allowed' => ['type' => 'boolean'], 41 | 'providers' => ['type' => 'object', 'additionalProperties' => ['type' => 'boolean']], 42 | ], 43 | ])] 44 | public ?array $oAuthRestriction = null, 45 | ) { 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/ApipMigrateOpenApiRector/ApipMigrateOpenApiRectorTest.php: -------------------------------------------------------------------------------- 1 | function (NodeAbstract $node, array $a) { 20 | $key = Caster::PREFIX_PROTECTED . 'attributes'; 21 | if (\array_key_exists($key, $a)) { 22 | $a[$key]['scope'] = new CutStub($a[$key]['scope']); 23 | } 24 | if (\array_key_exists($key, $a)) { 25 | $a[$key]['origNode'] = new CutStub($a[$key]['origNode']); 26 | } 27 | // Bourin mode! 28 | $a[$key] = new CutStub($a[$key]); 29 | 30 | return $a; 31 | }, 32 | ]; 33 | } 34 | 35 | #[DataProvider('provideData')] 36 | public function test(string $filePath): void 37 | { 38 | $this->doTestFile($filePath); 39 | } 40 | 41 | public static function provideData(): \Iterator 42 | { 43 | return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); 44 | } 45 | 46 | public function provideConfigFilePath(): string 47 | { 48 | return __DIR__ . '/config/configured_rule.php'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rector rule for migration API-Platform OpenApiContext 2 | 3 | Provide this kind of diff: 4 | ```diff 5 | new Get( 6 | // ... 7 | - openapiContext: [ 8 | - 'description' => 'Use this endpoint to retrieve the details of a draft rule.', 9 | - 'operationId' => 'getDraft', 10 | - ] 11 | + openapi: new Operation( 12 | + description: 'Use this endpoint to retrieve the details of a draft rule.', 13 | + operationId: 'getDraft', 14 | + ) 15 | ), 16 | ``` 17 | 18 | ## Installation 19 | 20 | Copy the content of `ApipMigrateOpenApiRector` in your project, and add it as a rules: 21 | 22 | ```php 23 | rule(App\ApipMigrateOpenApiRector::class); 27 | // .... 28 | ``` 29 | 30 | ## Optional, beautiful output 31 | 32 | >[!NOTE] 33 | > Rector has no extension point to change the printer. And the default printer is final. 34 | > So we need to hack a bit to change it. See this [locked issue](https://github.com/rectorphp/rector/issues/9033)) 35 | 36 | If you want a better output, you'll need to copy also the `MyBetterStandardPrinter` class in your project, and add it to the `rector.php`: 37 | 38 | ```php 39 | rule(App\ApipMigrateOpenApiRector::class); 49 | $rectorConfig->importNames(); 50 | }; 51 | 52 | return static function (RectorConfig $config) use ($builder): void { 53 | $config->singleton(BetterStandardPrinter::class, fn ($app): \App\MyBetterStandardPrinter => new MyBetterStandardPrinter($app->get(ExprAnalyzer::class))); 54 | 55 | $builder($config); 56 | }; 57 | ``` 58 | -------------------------------------------------------------------------------- /src/ApipMigrateOpenApiRector.php: -------------------------------------------------------------------------------- 1 | getName($node->class), HttpOperation::class, true)) { 29 | return $this->convert($node); 30 | } 31 | 32 | return null; 33 | } 34 | 35 | private function convert(Node $node): ?Node 36 | { 37 | $openApiArgs = []; 38 | $toDelete = []; 39 | $changed = false; 40 | 41 | foreach ($node->args as $k => $arg) { 42 | if ('openapiContext' === $this->getName($arg)) { 43 | $changed = true; 44 | $toDelete[] = $k; 45 | 46 | $argValue = $arg->value; 47 | 48 | if (!$argValue instanceof Node\Expr\Array_) { 49 | // It should be an array! If it's not, we can't do anything. 50 | continue; 51 | } 52 | 53 | foreach ($argValue->items as $item) { 54 | $openApiArgs[] = new Node\Arg( 55 | $item->value, 56 | name: new Node\Identifier($this->valueResolver->getValue($item->key)), 57 | attributes: [ 58 | ...$item->getAttributes(), 59 | 'multiline' => true, 60 | ], 61 | ); 62 | } 63 | 64 | continue; 65 | } 66 | 67 | if ('openapi' === $this->getName($arg)) { 68 | $toDelete[] = $k; 69 | 70 | foreach ($arg->value->args ?? [] as $item) { 71 | $openApiArgs[] = new Node\Arg( 72 | $item->value, 73 | name: $item->name, 74 | attributes: [ 75 | ...$item->getAttributes(), 76 | 'multiline' => true, 77 | ], 78 | ); 79 | } 80 | 81 | continue; 82 | } 83 | } 84 | 85 | if (!$openApiArgs || !$changed) { 86 | return null; 87 | } 88 | 89 | foreach ($toDelete as $k) { 90 | unset($node->args[$k]); 91 | } 92 | 93 | $openApi = new Arg( 94 | new New_( 95 | new Node\Name\FullyQualified(Operation::class), 96 | $openApiArgs, 97 | ), 98 | name: new Node\Identifier('openapi'), 99 | ); 100 | $node->args[] = $openApi; 101 | 102 | 103 | return $node; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/ApipMigrateOpenApiRector/Fixture/api-resource.php.inc: -------------------------------------------------------------------------------- 1 | 'Use this endpoint to retrieve the details of a draft rule.', 19 | 'operationId' => 'getDraft', 20 | ] 21 | ), 22 | new GetCollection( 23 | provider: DraftCollectionProvider::class, 24 | openapiContext: [ 25 | 'summary' => 'Retrieves the collection of Draft rules', 26 | 'description' => <<<'EOFTXT' 27 | This endpoint allows to retrieve the collection of draft rules of the project. 28 | Draft rules represent changes that have been saved in the project but not yet published. 29 | Each draft comes with a `status` property, which can be on of: 30 | * `add`: this is a new rule that is going to be added when publishing the ruleset 31 | * `update`: this is an already existing redirection rule that is going to be changed when publishing the ruleset 32 | * `delete`: this is an existing redirection rule that is going to be removed when publishing the ruleset 33 | EOFTXT, 34 | 'operationId' => 'getDrafts', 35 | 'parameters' => [ 36 | [ 37 | 'name' => 'projectId', 38 | 'in' => 'query', 39 | 'description' => 'The id of the project.', 40 | 'required' => true, 41 | 'schema' => [ 42 | 'type' => 'string', 43 | ], 44 | ], 45 | ], 46 | ] 47 | ), 48 | ], 49 | formats: ['json', 'jsonld'], 50 | )] 51 | class Draft 52 | { 53 | } 54 | ----- 55 | 'projectId', 93 | 'in' => 'query', 94 | 'description' => 'The id of the project.', 95 | 'required' => true, 96 | 'schema' => [ 97 | 'type' => 'string', 98 | ], 99 | ], 100 | ], 101 | ) 102 | ), 103 | ], 104 | formats: ['json', 'jsonld'], 105 | )] 106 | class Draft 107 | { 108 | } 109 | --------------------------------------------------------------------------------