diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4909e5e6..1ddf2b73e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: fail-fast: true matrix: operating-system: [ ubuntu-latest ] - php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] + php: [ '8.2', '8.3', '8.4', '8.5' ] dependencies: [ 'lowest', 'highest' ] name: PHP ${{ matrix.php }} on ${{ matrix.operating-system }} with ${{ matrix.dependencies }} dependencies @@ -38,9 +38,5 @@ jobs: dependency-versions: ${{ matrix.dependencies }} composer-options: "${{ matrix.composer-options }}" - - name: Require type-info-extras - if: ${{ matrix.php >= '8.2' }} - run: composer require radebatz/type-info-extras -W - - name: PHPUnit Tests run: bin/phpunit --configuration phpunit.xml.dist --coverage-text diff --git a/.github/workflows/security-checks.yml b/.github/workflows/security-checks.yml index f5fac7709..710d0d0f6 100644 --- a/.github/workflows/security-checks.yml +++ b/.github/workflows/security-checks.yml @@ -15,7 +15,7 @@ jobs: fail-fast: true matrix: operating-system: [ ubuntu-latest ] - php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] + php: [ '8.2', '8.3', '8.4', '8.5' ] dependencies: [ 'highest' ] name: PHP ${{ matrix.php }} on ${{ matrix.operating-system }} with ${{ matrix.dependencies }} dependencies diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index bb0a5068b..78f958b18 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -64,7 +64,7 @@ 'trim_array_spaces' => true, 'single_space_around_construct' => true, 'single_line_comment_spacing' => true, - 'fully_qualified_strict_types' => true, + 'fully_qualified_strict_types' => ['import_symbols' => true, 'leading_backslash_in_global_namespace' => true], 'global_namespace_import' => ['import_classes' => false, 'import_constants' => null, 'import_functions' => null], 'nullable_type_declaration_for_default_null_value' => true, @@ -89,5 +89,7 @@ 'phpdoc_no_empty_return' => true, 'phpdoc_no_alias_tag' => true, 'phpdoc_param_order' => true, + + 'php_unit_attributes' => true, ]) ->setFinder($finder); diff --git a/README.md b/README.md index 16f0d9733..f7e62125c 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Programmatically, the method `Generator::setVersion()` can be used to change the ## Requirements -`swagger-php` requires at least PHP 7.4 for annotations and PHP 8.1 for using attributes. +`swagger-php` requires at least PHP 8.2. ## Installation (with [Composer](https://getcomposer.org)) @@ -75,6 +75,9 @@ composer require doctrine/annotations Add annotations to your php files. ```php + +use OpenApi\Annotations as OA; + /** * @OA\Info(title="My First API", version="0.1") */ @@ -96,7 +99,7 @@ Generate always-up-to-date documentation. ```php generate(['/path/to/project']); header('Content-Type: application/x-yaml'); echo $openapi->toYaml(); ``` diff --git a/bin/openapi b/bin/openapi index c302320d1..0e6497114 100755 --- a/bin/openapi +++ b/bin/openapi @@ -6,7 +6,7 @@ use OpenApi\Analysers\DocBlockAnnotationFactory; use OpenApi\Analysers\ReflectionAnalyser; use OpenApi\Annotations\OpenApi; use OpenApi\Generator; -use OpenApi\Util; +use OpenApi\SourceFinder; use OpenApi\Loggers\ConsoleLogger; if (class_exists(Generator::class) === false) { @@ -224,7 +224,7 @@ $openapi = $generator ->setVersion($options['version']) ->setConfig($options['config']) ->setAnalyser($analyser) - ->generate(Util::finder($paths, $exclude, $pattern)); + ->generate(new SourceFinder($paths, $exclude, $pattern)); if ($options['output'] === false) { if (strtolower($options['format']) === 'json') { diff --git a/composer.json b/composer.json index f9617a516..0689a71c7 100644 --- a/composer.json +++ b/composer.json @@ -40,15 +40,16 @@ "minimum-stability": "stable", "extra": { "branch-alias": { - "dev-master": "5.x-dev" + "dev-master": "6.x-dev" } }, "require": { - "php": ">=7.4", + "php": ">=8.2", "ext-json": "*", "nikic/php-parser": "^4.19 || ^5.0", "phpstan/phpdoc-parser": "^2.0", "psr/log": "^1.1 || ^2.0 || ^3.0", + "radebatz/type-info-extras": "^1.0.2", "symfony/deprecation-contracts": "^2 || ^3", "symfony/finder": "^5.0 || ^6.0 || ^7.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0" @@ -73,15 +74,13 @@ "doctrine/annotations": "^2.0", "friendsofphp/php-cs-fixer": "^3.62.0", "phpstan/phpstan": "^1.6 || ^2.0", - "phpunit/phpunit": "^9.0", - "rector/rector": "^1.0 || ^2.0", - "vimeo/psalm": "^4.30 || ^5.0" + "phpunit/phpunit": "^10.5", + "rector/rector": "^1.0 || ^2.0" }, "conflict": { "symfony/process": ">=6, <6.4.14" }, "suggest": { - "radebatz/type-info-extras": "^1.0.2", "doctrine/annotations": "^2.0" }, "scripts-descriptions": { @@ -89,7 +88,7 @@ "rector": "Automatic refactoring", "lint": "Test codestyle", "test": "Run all PHP, codestyle and rector tests", - "analyse": "Run static analysis (phpstan/psalm)", + "analyse": "Run static analysis (phpstan)", "spectral-examples": "Run spectral lint over all .yaml files in the docs/examples folder", "spectral-scratch": "Run spectral lint over all .yaml files in the tests/Fixtures/Scratch folder", "spectral": "Run all spectral tests", @@ -112,8 +111,7 @@ "@lint" ], "analyse": [ - "export XDEBUG_MODE=off && phpstan analyse --memory-limit=2G", - "export XDEBUG_MODE=off && psalm --threads=1" + "export XDEBUG_MODE=off && phpstan analyse --memory-limit=3G" ], "spectral-examples": "for ff in `find docs/examples -name '*.yaml'`; do npm run spectral lint $ff; done", "spectral-scratch": "for ff in `find tests/Fixtures/Scratch -name '*.yaml'`; do npm run spectral lint $ff; done", diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 7a2aea223..47b63733e 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -24,6 +24,7 @@ function getGuideSidebar() { { text: 'Upgrading', items: [ + { text: 'Migration from 5.x to 6.x', link: '/guide/migrating-to-v6' }, { text: 'Migration from 4.x to 5.x', link: '/guide/migrating-to-v5' }, { text: 'Migration from 3.x to 4.x', link: '/guide/migrating-to-v4' }, { text: 'Migration from 2.x to 3.x', link: '/guide/migrating-to-v3' }, diff --git a/docs/examples/processors/schema-query-parameter/SchemaQueryParameter.php b/docs/examples/processors/schema-query-parameter/SchemaQueryParameter.php index 4a8525d03..b5e1cd6f4 100644 --- a/docs/examples/processors/schema-query-parameter/SchemaQueryParameter.php +++ b/docs/examples/processors/schema-query-parameter/SchemaQueryParameter.php @@ -25,7 +25,6 @@ class SchemaQueryParameter public function __invoke(Analysis $analysis): void { - /** @var Operation[] $operations */ $operations = $analysis->getAnnotationsOfType(Operation::class); foreach ($operations as $operation) { @@ -34,7 +33,7 @@ public function __invoke(Analysis $analysis): void throw new \InvalidArgumentException('Value of `x.' . self::REF . '` must be a string'); } - $schema = $analysis->getSchemaForSource($operation->x[self::REF]); + $schema = $analysis->getAnnotationForSource($operation->x[self::REF]); if (!$schema instanceof Schema) { throw new \InvalidArgumentException('Value of `x.' . self::REF . "` contains reference to unknown schema: `{$operation->x[self::REF]}`"); } diff --git a/docs/examples/specs/api/annotations/ProductController.php b/docs/examples/specs/api/annotations/ProductController.php index 805d510e8..c44e5aa35 100644 --- a/docs/examples/specs/api/annotations/ProductController.php +++ b/docs/examples/specs/api/annotations/ProductController.php @@ -68,7 +68,6 @@ public function getProduct(?int $product_id) * @OA\MediaType( * mediaType="application/json", * @OA\Schema( - * type="array", * @OA\Items(ref="#/components/schemas/Product") * ) * ) diff --git a/docs/examples/specs/api/attributes/ProductController.php b/docs/examples/specs/api/attributes/ProductController.php index 0b487f345..1080d97c4 100644 --- a/docs/examples/specs/api/attributes/ProductController.php +++ b/docs/examples/specs/api/attributes/ProductController.php @@ -48,7 +48,6 @@ public function getProduct( content: [new OAT\MediaType( mediaType: 'application/json', schema: new OAT\Schema( - type: 'array', items: new OAT\Items(type: Product::class) ) )] diff --git a/docs/examples/specs/api/mixed/ProductController.php b/docs/examples/specs/api/mixed/ProductController.php index c8379aabb..26ba19be8 100644 --- a/docs/examples/specs/api/mixed/ProductController.php +++ b/docs/examples/specs/api/mixed/ProductController.php @@ -57,7 +57,6 @@ public function getProduct(?int $product_id) * description="New product", * required=true, * @OA\JsonContent( - * type="array", * @OA\Items(ref="#/components/schemas/Product") * ) * ) diff --git a/docs/guide/generating-openapi-documents.md b/docs/guide/generating-openapi-documents.md index 0f2d089cf..eca047141 100644 --- a/docs/guide/generating-openapi-documents.md +++ b/docs/guide/generating-openapi-documents.md @@ -61,9 +61,9 @@ In its simplest form this may look something like ```php generate(['/path/to/project']); header('Content-Type: application/x-yaml'); echo $openapi->toYaml(); diff --git a/docs/guide/index.md b/docs/guide/index.md index 62fc740f5..61d354399 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -15,6 +15,5 @@ either adding [`Annotations`](using-annotations.md) or [`Attributes`](using-attr ::: ::: warning Requirements -Using `swagger-php` requires a minimum of **PHP 7.4** for using annotations and -at least **PHP 8.1** to use attributes. +Using `swagger-php` requires a minimum of **PHP 8.2**. ::: diff --git a/docs/guide/migrating-to-v6.md b/docs/guide/migrating-to-v6.md new file mode 100644 index 000000000..49f6b00b6 --- /dev/null +++ b/docs/guide/migrating-to-v6.md @@ -0,0 +1,34 @@ +# Migrating to v6 + +## Overview + +`v6` is mostly a cleanup release, with updated dependencies. The main changes are: + +* The minimum required PHP version is now 8.2 +* `radebatz/type-info-extras` is now a required dependency +* `TypeInfoTypeResolver` now properly handles composite types (unions and intersections) +* Some deprecations have been removed (see below) +* The `MediaType::encoding` property now only accepts `Encoding` objects (BC break) + +For most installations upgrading should not require any changes. + +## Type resolvers +With `radebatz/type-info-extras` now being a required dependency, the `TypeInfoTypeResolver` is not the de-facto default +resolver. + +The `LegacyTypeResolver` can still be used as a drop-in replacement, but is now marked `deprecated` and will be removed +in v7. + +## Removed deprecated elements +### Methods `\Openapi\Generator::getProcessors()` and `\Openapi\Generator::setProcessors()` +Use `getProcessorPipeline()` and `setProcessorPipeline(new Pipeline(...))` methods instead + +### Static method `\Openapi\Generator::scan()` +Main entry point into the `Generator` is now the **non-static** `generate()` method: +```php +(new Generator())->generate(/* ... */); +``` + +### `Utils` helper +Most methods in the class were internal to start with and the `Util::finder()` factory methods is now replaced with +the new `SourceFinder` class. diff --git a/docs/index.md b/docs/index.md index fcc1fce76..59f37bc23 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,7 +27,7 @@ features: Add `swagger-php` attributes (or legacy annotations) to your source code. -⚠️ `doctrine/annotations` is going to be deprecated in the future, so wherever +⚠️ The `doctrine/annotations` library used to parse annotation is going to be deprecated, so wherever possible attributes should be used. diff --git a/docs/reference/generator.md b/docs/reference/generator.md index 87f452bc8..339141332 100644 --- a/docs/reference/generator.md +++ b/docs/reference/generator.md @@ -1,55 +1,17 @@ # Using the `Generator` ## Introduction -The `Generator` class provides an object-oriented way to use `swagger-php` and all its aspects in a single place. - -## The `\OpenApi\scan()` function - -For a long time the `\OpenApi\scan()` function was the main entry point into using swagger-php from PHP code. - -```php -/** - * Scan the filesystem for OpenAPI annotations and build openapi-documentation. - * - * @param array|Finder|string $directory The directory(s) or filename(s) - * @param array $options - * exclude: string|array $exclude The directory(s) or filename(s) to exclude (as absolute or relative paths) - * pattern: string $pattern File pattern(s) to scan (default: *.php) - * analyser: defaults to StaticAnalyser - * analysis: defaults to a new Analysis - * processors: defaults to the registered processors in Analysis - * - * @return OpenApi - */ - function scan($directory, $options = []) { /* ... */ } -``` - -Using it looked typically something like this: -```php -require("vendor/autoload.php"); - -$openapi = \OpenApi\scan(__DIR__, ['exclude' => ['tests'], 'pattern' => '*.php']); -``` - -The two configuration options for the underlying Doctrine doc-block parser `aliases` and `namespaces` -are not part of this function and need to be set separately. - -Being static this means setting them back is the callers responsibility and there is also the fact that -some Doctrine configuration currently can not be reverted easily. - -Therefore, having a single side effect free way of using swagger-php seemed like a good idea... +The `Generator` class is the main entry point into `swagger-php`. ## The `\OpenApi\Generator` class -The `Generator` class can be used in object-oriented (and fluent) style which allows for easy customization -if needed. +The `Generator` class provides an object-oriented (and fluent) API to generate OpenApi specs +and allows for easy customization where needed. -In that case to actually process the given input files the **non-static** method `generate()` is to be used. - -Full example of using the `Generator` class to generate OpenApi specs. +## Full example of using the `Generator` class to generate OpenApi specs ```php -require("vendor/autoload.php"); +require('vendor/autoload.php'); $validate = true; $logger = new \Psr\Log\NullLogger(); @@ -67,99 +29,34 @@ $openapi = (new \OpenApi\Generator($logger)) ->generate(['/path1/to/project', $finder], new \OpenApi\Analysis([], $context), $validate); ``` -`Aliases` and `namespaces` are additional options that allow to customize the parsing of docblocks. +`Aliases` and `namespaces` setting are optional and only required when using annotations. +They allow to customize the parsing of docblocks and are passed through to the underlying `doctrine/annotations` library. Defaults: * **aliases**: `['oa' => 'OpenApi\\Annotations']` - Aliases help the underlying `doctrine annotations library` to parse annotations. Effectively they avoid having - to write `use OpenApi\Annotations as OA;` in your code and make `@OA\property(..)` annotations still work. + Aliases help to parse annotations. Effectively, they avoid having to write `use OpenApi\Annotations as OA;` in your code + and make `@OA\property(..)` annotations still work. * **namespaces**: `['OpenApi\\Annotations\\']` Namespaces control which annotation namespaces can be autoloaded automatically. Under the hood this is handled by registering a custom loader with the `doctrine annotation library`. -Advantages: -* The `Generator` code will handle configuring things as before in a single place -* Static settings will be reverted to the default once finished -* The get/set methods allow for using type hints -* Static configuration is deprecated and can be removed at some point without code changes -* Build in support for PSR logger -* Support for [Symfony Finder](https://symfony.com/doc/current/components/finder.html), `\SplInfo` and file/directory names (`string) as source. - -The minimum code required, using the `generate()` method, looks quite similar to the old `scan()` code: - -```php - /** - * Generate OpenAPI spec by scanning the given source files. - * - * @param iterable $sources PHP source files to scan. - * Supported sources: - * * string - file / directory name - * * \SplFileInfo - * * \Symfony\Component\Finder\Finder - * @param null|Analysis $analysis custom analysis instance - * @param bool $validate flag to enable/disable validation of the returned spec - */ - public function generate(iterable $sources, ?Analysis $analysis = null, bool $validate = true): \OpenApi\OpenApi { /* ... */ } -``` +## Basic example ```php -require("vendor/autoload.php"); +require('"vendor/autoload.php'); $openapi = (new \OpenApi\Generator())->generate(['/path1/to/project']); ``` -For those that want to type even less and keep using a plain array to configure `swagger-php` there is also a static version: - -```php -toYaml(); -``` - -**Note:** While using the same name as the old `scan()` function, the `Generator::scan` method is not -100% backwards compatible. - -```php - /** - * Static wrapper around `Generator::generate()`. - * - * @param iterable $sources PHP source files to scan. - * Supported sources: - * * string - * * \SplFileInfo - * * \Symfony\Component\Finder\Finder - * @param array $options - * aliases: null|array Defaults to `['oa' => 'OpenApi\\Annotations']`. - * namespaces: null|array Defaults to `['OpenApi\\Annotations\\']`. - * analyser: null|AnalyserInterface Defaults to a new `ReflectionAnalyser` supporting both docblocks and attributes. - * analysis: null|Analysis Defaults to a new `Analysis`. - * processors: null|array Defaults to `Analysis::processors()`. - * logger: null|\Psr\Log\LoggerInterface If not set logging will use \OpenApi\Logger as before. - * validate: bool Defaults to `true`. - * version: string Defaults to `\OpenApi\Annotations\OpenApi::VERSION_3_0_0`. Alternatives are: `\OpenApi\Annotations\OpenApi::VERSION_3_1_0`. - */ - public static function scan(iterable $sources, array $options = []): OpenApi { /* ... */ } -``` - -Most notably the `exclude` and `pattern` keys are no longer supported. Instead, a Symfony `Finder` instance can be passed in -as source directly (same as with `Generator::generate()`). - -If needed, the `\OpenApi\Util` class provides a builder method that allows to keep the status-quo +The `generate()` method does not support the CLI `exclude` and `pattern` options directly. +Instead, a custom Symfony `Finder` class (`SourceFinder`) can be used that understands these options. ```php $exclude = ['tests']; $pattern = '*.php'; -$openapi = \OpenApi\Generator::scan(\OpenApi\Util::finder(__DIR__, $exclude, $pattern)); - -// same as - -$openapi = \OpenApi\scan(__DIR__, ['exclude' => $exclude, 'pattern' => $pattern]); +$openapi = (new \OpenApi\Generator())->generate(new \OpenApi\SourceFinder(__DIR__, $exclude, $pattern)); ``` diff --git a/docs/snippets/guide/augmentation/context_awareness_an.php b/docs/snippets/guide/augmentation/context_awareness_an.php index 38a5ba3f7..f26ceb422 100644 --- a/docs/snippets/guide/augmentation/context_awareness_an.php +++ b/docs/snippets/guide/augmentation/context_awareness_an.php @@ -1,5 +1,7 @@ - diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index 0f3330cff..000000000 --- a/psalm.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/rector.php b/rector.php index 7c8480ed0..52b5c1d41 100644 --- a/rector.php +++ b/rector.php @@ -1,5 +1,6 @@ [ __DIR__ . '/src/Serializer.php', ], + ClassPropertyAssignToConstructorPromotionRector::class, + CompleteDynamicPropertiesRector::class => [ + __DIR__ . '/src/Annotations/AbstractAnnotation.php', + ], ]) - ->withPreparedSets(true, true, true, true) - ->withPhpVersion(PhpVersion::PHP_74) + ->withPreparedSets( + deadCode: true, + codeQuality: true, + codingStyle: true, + typeDeclarations: true, + ) + ->withPhpVersion(PhpVersion::PHP_82) ->withPhpSets(); diff --git a/src/Analysers/AttributeAnnotationFactory.php b/src/Analysers/AttributeAnnotationFactory.php index cc5e5cb8c..facbfc41d 100644 --- a/src/Analysers/AttributeAnnotationFactory.php +++ b/src/Analysers/AttributeAnnotationFactory.php @@ -25,7 +25,7 @@ public function __construct(bool $ignoreOtherAttributes = false) public function isSupported(): bool { - return \PHP_VERSION_ID >= 80100; + return true; } public function build(\Reflector $reflector, Context $context): array @@ -106,10 +106,9 @@ public function build(\Reflector $reflector, Context $context): array } // merge backwards into parents... - $isParent = function (OA\AbstractAnnotation $annotation, OA\AbstractAnnotation $possibleParent): bool { + $isParent = static function (OA\AbstractAnnotation $annotation, OA\AbstractAnnotation $possibleParent): bool { // regular annotation hierarchy $explicitParent = null !== $possibleParent->matchNested($annotation) && !$annotation instanceof OA\Attachable; - $isParentAllowed = false; // support Attachable subclasses if ($isAttachable = $annotation instanceof OA\Attachable) { diff --git a/src/Analysers/ComposerAutoloaderScanner.php b/src/Analysers/ComposerAutoloaderScanner.php index b00f743fc..6ee6a3cec 100644 --- a/src/Analysers/ComposerAutoloaderScanner.php +++ b/src/Analysers/ComposerAutoloaderScanner.php @@ -29,7 +29,7 @@ public function scan(array $namespaces): array if ($autoloader = static::getComposerAutoloader()) { foreach (array_keys($autoloader->getClassMap()) as $unit) { foreach ($namespaces as $namespace) { - if (0 === strpos($unit, $namespace)) { + if (str_starts_with($unit, $namespace)) { $units[] = $unit; break; } diff --git a/src/Analysers/DocBlockAnnotationFactory.php b/src/Analysers/DocBlockAnnotationFactory.php index d70ab0d96..923b8ac88 100644 --- a/src/Analysers/DocBlockAnnotationFactory.php +++ b/src/Analysers/DocBlockAnnotationFactory.php @@ -41,16 +41,16 @@ public function build(\Reflector $reflector, Context $context): array $aliases = $this->generator ? $this->generator->getAliases() : []; if (method_exists($reflector, 'getShortName') && method_exists($reflector, 'getName')) { - $aliases[strtolower($reflector->getShortName())] = $reflector->getName(); + $aliases[strtolower((string) $reflector->getShortName())] = $reflector->getName(); } if ($context->with('scanned')) { $details = $context->scanned; foreach ($details['uses'] as $alias => $name) { - $aliasKey = strtolower($alias); + $aliasKey = strtolower((string) $alias); if ($name != $alias && !array_key_exists($aliasKey, $aliases)) { // real aliases only - $aliases[strtolower($alias)] = $name; + $aliases[strtolower((string) $alias)] = $name; } } } diff --git a/src/Analysers/ReflectionAnalyser.php b/src/Analysers/ReflectionAnalyser.php index 92828fb8e..dd3ff6eb4 100644 --- a/src/Analysers/ReflectionAnalyser.php +++ b/src/Analysers/ReflectionAnalyser.php @@ -113,7 +113,7 @@ protected function analyzeFqdn(string $fqdn, Analysis $analysis, array $details) 'methods' => [], 'context' => $context, ]; - $normaliseClass = fn (string $name): string => '\\' . ltrim($name, '\\'); + $normaliseClass = static fn (string $name): string => '\\' . ltrim($name, '\\'); if ($parentClass = $rc->getParentClass()) { $definition['extends'] = $normaliseClass($parentClass->getName()); } diff --git a/src/Analysers/TokenScanner.php b/src/Analysers/TokenScanner.php index bcd9e0f32..890c9fe60 100644 --- a/src/Analysers/TokenScanner.php +++ b/src/Analysers/TokenScanner.php @@ -52,14 +52,14 @@ protected function collect_stmts(array $stmts, string $namespace): array { /** @var array $uses */ $uses = []; - $resolve = function (string $name) use ($namespace, &$uses) { + $resolve = static function (string $name) use ($namespace, &$uses) { if (array_key_exists($name, $uses)) { return $uses[$name]; } return $namespace . '\\' . $name; }; - $details = function () use (&$uses): array { + $details = static function () use (&$uses): array { return [ 'uses' => $uses, 'interfaces' => [], @@ -71,7 +71,7 @@ protected function collect_stmts(array $stmts, string $namespace): array }; $result = []; foreach ($stmts as $stmt) { - switch (get_class($stmt)) { + switch ($stmt::class) { case Use_::class: $uses += $this->collect_uses($stmt); break; diff --git a/src/Analysis.php b/src/Analysis.php index 265eb6141..7021dda57 100644 --- a/src/Analysis.php +++ b/src/Analysis.php @@ -307,15 +307,6 @@ public function getAnnotationsOfType($classes, bool $strict = false): array return $annotations; } - /** - * @param string $fqdn the source class/interface/trait - * @deprecated use getAnnotationForSource() instead - */ - public function getSchemaForSource(string $fqdn): ?OA\Schema - { - return $this->getAnnotationForSource($fqdn, OA\Schema::class); - } - /** * @template T of OA\AbstractAnnotation * @@ -404,23 +395,6 @@ public function split(): \stdClass return $result; } - /** - * Apply the processor(s). - * - * @param callable|array $processors One or more processors - * @deprecated use Generator::withProcessorPipeline() instead - */ - public function process($processors = null): void - { - if (false === is_array($processors) && is_callable($processors)) { - $processors = [$processors]; - } - - foreach ($processors as $processor) { - $processor($this); - } - } - public function validate(): bool { if ($this->openapi instanceof OA\OpenApi) { diff --git a/src/Annotations/AbstractAnnotation.php b/src/Annotations/AbstractAnnotation.php index 34480c68e..3c2325ae7 100644 --- a/src/Annotations/AbstractAnnotation.php +++ b/src/Annotations/AbstractAnnotation.php @@ -10,7 +10,6 @@ use OpenApi\Generator; use OpenApi\Annotations as OA; use OpenApi\OpenApiException; -use OpenApi\Util; use Symfony\Component\Yaml\Yaml; /** @@ -141,30 +140,6 @@ public function __construct(array $properties) } } } - - if ($this instanceof OpenApi) { - if ($this->_context->root()->version) { - // override via `Generator::setVersion()` - $this->openapi = $this->_context->root()->version; - } else { - $this->_context->root()->version = $this->openapi; - } - } - } - - public function __get(string $property) - { - $properties = get_object_vars($this); - $this->_context->logger->warning('Property "' . $property . '" doesn\'t exist in a ' . $this->identity() . ', existing properties: "' . implode('", "', array_keys($properties)) . '" in ' . $this->_context); - } - - public function __set(string $property, $value): void - { - $fields = get_object_vars($this); - foreach (static::$_blacklist as $_property) { - unset($fields[$_property]); - } - $this->_context->logger->warning('Ignoring unexpected property "' . $property . '" for ' . $this->identity() . ', expecting "' . implode('", "', array_keys($fields)) . '" in ' . $this->_context); } /** @@ -239,7 +214,7 @@ public function mergeProperties($object): void if (Generator::isDefault($value)) { continue; } - $identity = method_exists($object, 'identity') ? $object->identity() : get_class($object); + $identity = method_exists($object, 'identity') ? $object->identity() : $object::class; $context1 = $this->_context; $context2 = property_exists($object, '_context') ? $object->_context : 'unknown'; if ($this->{$property} instanceof AbstractAnnotation) { @@ -391,12 +366,19 @@ public function jsonSerialize() if (isset($data->type) && is_array($data->type)) { if (in_array('null', $data->type)) { $data->nullable = true; - $data->type = array_filter($data->type, fn ($t): bool => 'null' !== $t); + $data->type = array_filter($data->type, static fn ($v): bool => $v !== 'null'); if (1 === count($data->type)) { $data->type = array_pop($data->type); } } } + if (isset($data->type) && is_array($data->type)) { + if (1 === count($data->type)) { + $data->type = array_pop($data->type); + } else { + unset($data->type); + } + } } if ($this->_context->isVersion('3.1.x')) { @@ -461,19 +443,18 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', break; } - /** @var class-string $class */ - $class = get_class($annotation); + $class = $annotation::class; if ($details = $this->matchNested($annotation)) { $property = $details->value; if (is_array($property)) { - $this->_context->logger->warning('Only one ' . Util::shorten(get_class($annotation)) . '() allowed for ' . $this->identity() . ' multiple found, skipped: ' . $annotation->_context); + $this->_context->logger->warning('Only one ' . static::shorten($annotation::class) . '() allowed for ' . $this->identity() . ' multiple found, skipped: ' . $annotation->_context); } else { - $this->_context->logger->warning('Only one ' . Util::shorten(get_class($annotation)) . '() allowed for ' . $this->identity() . " multiple found in:\n Using: " . $this->{$property}->_context . "\n Skipped: " . $annotation->_context); + $this->_context->logger->warning('Only one ' . static::shorten($annotation::class) . '() allowed for ' . $this->identity() . " multiple found in:\n Using: " . $this->{$property}->_context . "\n Skipped: " . $annotation->_context); } } elseif ($annotation instanceof AbstractAnnotation) { $message = 'Unexpected ' . $annotation->identity(); if ($class::$_parents) { - $message .= ', expected to be inside ' . implode(', ', Util::shorten($class::$_parents)); + $message .= ', expected to be inside ' . implode(', ', static::shorten($class::$_parents)); } $this->_context->logger->warning($message . ' in ' . $annotation->_context); } @@ -493,7 +474,7 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', $keyField = $nested[1]; foreach ($this->{$property} as $key => $item) { if (is_array($item) && is_numeric($key) === false) { - $this->_context->logger->warning($this->identity() . '->' . $property . ' is an object literal, use nested ' . Util::shorten($annotationClass) . '() annotation(s) in ' . $this->_context); + $this->_context->logger->warning($this->identity() . '->' . $property . ' is an object literal, use nested ' . static::shorten($annotationClass) . '() annotation(s) in ' . $this->_context); $keys[$key] = $item; } elseif (Generator::isDefault($item->{$keyField})) { $this->_context->logger->error($item->identity() . ' is missing key-field: "' . $keyField . '" in ' . $item->_context); @@ -506,7 +487,7 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', } if (property_exists($this, 'ref') && !Generator::isDefault($this->ref) && is_string($this->ref)) { - if (substr($this->ref, 0, 2) === '#/' && $stack !== [] && $stack[0] instanceof OpenApi) { + if (str_starts_with($this->ref, '#/') && $stack !== [] && $stack[0] instanceof OpenApi) { // Internal reference try { $stack[0]->ref($this->ref); @@ -523,11 +504,11 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', $nestedProperty = is_array($nested) ? $nested[0] : $nested; if ($property === $nestedProperty) { if ($this instanceof OpenApi) { - $message = 'Required ' . Util::shorten($class) . '() not found'; + $message = 'Required ' . static::shorten($class) . '() not found'; } elseif (is_array($nested)) { - $message = $this->identity() . ' requires at least one ' . Util::shorten($class) . '() in ' . $this->_context; + $message = $this->identity() . ' requires at least one ' . static::shorten($class) . '() in ' . $this->_context; } else { - $message = $this->identity() . ' requires a ' . Util::shorten($class) . '() in ' . $this->_context; + $message = $this->identity() . ' requires a ' . static::shorten($class) . '() in ' . $this->_context; } break; } @@ -553,7 +534,7 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', $this->_context->logger->warning($this->identity() . '->' . $property . ' "' . $value . '" is invalid, expecting "' . implode('", "', $type) . '" in ' . $this->_context); } } else { - throw new OpenApiException('Invalid ' . get_class($this) . '::$_types[' . $property . ']'); + throw new OpenApiException('Invalid ' . static::class . '::$_types[' . $property . ']'); } } $stack[] = $this; @@ -612,7 +593,7 @@ private static function _validate($fields, array $stack, array $skip, string $ba */ public function identity(): string { - $class = get_class($this); + $class = static::class; $properties = []; /** @var class-string $parent */ foreach (static::$_parents as $parent) { @@ -653,10 +634,10 @@ public function matchNested($other) */ public function getRoot(): string { - $class = get_class($this); + $class = static::class; do { - if (0 === strpos($class, 'OpenApi\\Annotations\\')) { + if (str_starts_with($class, 'OpenApi\\Annotations\\')) { break; } } while ($class = get_parent_class($class)); @@ -671,7 +652,7 @@ public function getRoot(): string */ public function isRoot(string $rootClass): bool { - return get_class($this) === $rootClass || $this->getRoot() === $rootClass; + return static::class === $rootClass || $this->getRoot() === $rootClass; } /** @@ -687,7 +668,7 @@ protected function _identity(array $properties): string } } - return Util::shorten(get_class($this)) . '(' . implode(',', $fields) . ')'; + return static::shorten(static::class) . '(' . implode(',', $fields) . ')'; } /** @@ -696,9 +677,9 @@ protected function _identity(array $properties): string * @param string $type The annotations property type * @param mixed $value The property value */ - private function validateType(string $type, $value): bool + private function validateType(string $type, mixed $value): bool { - if (substr($type, 0, 1) === '[' && substr($type, -1) === ']') { // Array of a specified type? + if (str_starts_with($type, '[') && str_ends_with($type, ']')) { // Array of a specified type? if ($this->validateType('array', $value) === false) { return false; } @@ -725,7 +706,7 @@ private function validateType(string $type, $value): bool * @param string $type The property type * @param mixed $value The value to validate */ - private function validateDefaultTypes(string $type, $value): bool + private function validateDefaultTypes(string $type, mixed $value): bool { if (str_contains($type, '|')) { $types = explode('|', $type); @@ -738,24 +719,16 @@ private function validateDefaultTypes(string $type, $value): bool return false; } - switch ($type) { - case 'string': - return is_string($value); - case 'boolean': - return is_bool($value); - case 'integer': - return is_int($value); - case 'number': - return is_numeric($value); - case 'object': - return is_object($value); - case 'array': - return $this->validateArrayType($value); - case 'scheme': - return in_array($value, ['http', 'https', 'ws', 'wss'], true); - default: - throw new OpenApiException('Invalid type "' . $type . '"'); - } + return match ($type) { + 'string' => is_string($value), + 'boolean' => is_bool($value), + 'integer' => is_int($value), + 'number' => is_numeric($value), + 'object' => is_object($value), + 'array' => $this->validateArrayType($value), + 'scheme' => in_array($value, ['http', 'https', 'ws', 'wss'], true), + default => throw new OpenApiException('Invalid type "' . $type . '"'), + }; } /** @@ -772,7 +745,7 @@ private function validateArrayType($value): bool if ($count !== $i) { return false; } - $count++; + ++$count; } return true; @@ -780,11 +753,8 @@ private function validateArrayType($value): bool /** * Wrap the context with a reference to the annotation it is nested in. - * - * - * @return AbstractAnnotation */ - protected function nested(AbstractAnnotation $annotation, Context $nestedContext) + protected function nested(AbstractAnnotation $annotation, Context $nestedContext): self { if (property_exists($annotation, '_context') && $annotation->_context === $this->_context) { $annotation->_context = $nestedContext; @@ -804,6 +774,26 @@ protected function combine(...$args): array } } - return array_filter($combined, fn ($value): bool => !Generator::isDefault($value) && $value !== null); + return array_filter($combined, static fn ($value): bool => !Generator::isDefault($value) && $value !== null); + } + + /** + * Shorten class name(s). + * + * @param array|object|string $classes Class(es) to shorten + * + * @return string|string[] One or more shortened class names + */ + protected static function shorten($classes) + { + $short = []; + foreach ((array) $classes as $class) { + $short[] = '@' . str_replace([ + 'OpenApi\\Annotations\\', + 'OpenApi\\Attributes\\', + ], 'OA\\', (string) $class); + } + + return is_array($classes) ? $short : array_pop($short); } } diff --git a/src/Annotations/Components.php b/src/Annotations/Components.php index 336d3f255..f53108037 100644 --- a/src/Annotations/Components.php +++ b/src/Annotations/Components.php @@ -7,7 +7,6 @@ namespace OpenApi\Annotations; use OpenApi\Generator; -use OpenApi\Util; /** * Holds a set of reusable objects for different aspects of the OA. @@ -123,7 +122,7 @@ class Components extends AbstractAnnotation */ public static function componentTypes(): array { - return array_filter(array_keys(self::$_nested), fn (string $value): bool => $value !== Attachable::class); + return array_filter(array_keys(self::$_nested), static fn (string $value): bool => $value !== Attachable::class); } /** @@ -151,6 +150,28 @@ public static function ref($component, bool $encode = true): string $name = $component; } - return self::COMPONENTS_PREFIX . $type . '/' . ($encode ? Util::refEncode((string) $name) : $name); + return self::COMPONENTS_PREFIX . $type . '/' . ($encode ? static::refEncode((string) $name) : $name); + } + + /** + * Escapes the special characters "/" and "~". + * + * https://swagger.io/docs/specification/using-ref/ + * https://tools.ietf.org/html/rfc6901#page-3 + */ + public static function refEncode(string $raw): string + { + return str_replace('/', '~1', str_replace('~', '~0', $raw)); + } + + /** + * Converted the escaped characters "~1" and "~" back to "/" and "~". + * + * https://swagger.io/docs/specification/using-ref/ + * https://tools.ietf.org/html/rfc6901#page-3 + */ + public static function refDecode(string $encoded): string + { + return str_replace('~1', '/', str_replace('~0', '~', $encoded)); } } diff --git a/src/Annotations/Items.php b/src/Annotations/Items.php index af125feb1..6cf85d622 100644 --- a/src/Annotations/Items.php +++ b/src/Annotations/Items.php @@ -50,8 +50,7 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', $valid = parent::validate($stack, $skip, $ref, $context); $parent = end($stack); - // type might be array in 3.1.0 - if ($parent instanceof Schema && ($parent->type !== 'array' && !(is_array($parent->type) && in_array('array', $parent->type)))) { + if ($parent instanceof Schema && $parent->type !== 'array') { $this->_context->logger->warning('@OA\\Items() parent type must be "array" in ' . $this->_context); $valid = false; } diff --git a/src/Annotations/MediaType.php b/src/Annotations/MediaType.php index 94248b34e..b4e45d2c6 100644 --- a/src/Annotations/MediaType.php +++ b/src/Annotations/MediaType.php @@ -11,6 +11,8 @@ /** * Each Media Type object provides schema and examples for the media type identified by its key. * + * Parameter encodings can be set either here, or on nested `Property` annotations directly. + * * @see [Media Type Object](https://spec.openapis.org/oas/v3.1.1.html#media-type-object) * * @Annotation @@ -83,17 +85,6 @@ class MediaType extends AbstractAnnotation RequestBody::class, ]; - public function __construct(array $properties) - { - if (array_key_exists('encoding', $properties)) { - $properties['encoding'] = $this->encodingCompat( - $properties['encoding'], - fn (array $args): Encoding => new Encoding($args), - ); - } - parent::__construct($properties); - } - protected function encodingCompat($encoding, callable $factory) { if (!is_array($encoding)) { diff --git a/src/Annotations/OpenApi.php b/src/Annotations/OpenApi.php index acd7ed676..f629f2617 100644 --- a/src/Annotations/OpenApi.php +++ b/src/Annotations/OpenApi.php @@ -9,7 +9,6 @@ use OpenApi\Analysis; use OpenApi\Generator; use OpenApi\OpenApiException; -use OpenApi\Util; /** * This is the root document object for the API specification. @@ -138,6 +137,18 @@ class OpenApi extends AbstractAnnotation */ public static $_types = []; + public function __construct(array $properties) + { + parent::__construct($properties); + + if ($this->_context->root()->version) { + // override via `Generator::setVersion()` + $this->openapi = $this->_context->root()->version; + } else { + $this->_context->root()->version = $this->openapi; + } + } + /** * @inheritdoc */ @@ -180,14 +191,13 @@ public function validate(?array $stack = null, ?array $skip = null, string $ref */ public static function versionMatch(string $version1, string $version2): bool { - $expand = function (string $v): array { + $expand = static function (string $v): array { if (!str_ends_with($v, '.x')) { return [$v]; } - $minor = str_replace('.x', '', $v); - return array_filter(self::SUPPORTED_VERSIONS, fn (string $sv): bool => str_starts_with($sv, $minor)); + return array_filter(self::SUPPORTED_VERSIONS, static fn (string $sv): bool => str_starts_with($sv, $minor)); }; $versions1 = $expand($version1); $versions2 = $expand($version2); @@ -218,7 +228,7 @@ public function saveAs(string $filename, string $format = 'auto'): void */ public function ref(string $ref) { - if (substr($ref, 0, 2) !== '#/') { + if (!str_starts_with($ref, '#/')) { throw new OpenApiException('Unsupported $ref "' . $ref . '", it should start with "#/"'); } @@ -240,12 +250,12 @@ private static function resolveRef(string $ref, string $resolved, $container, ar $slash = strpos($path, '/'); $subpath = $slash === false ? $path : substr($path, 0, $slash); - $property = Util::refDecode($subpath); + $property = Components::refDecode($subpath); $unresolved = $slash === false ? $resolved . $subpath : $resolved . $subpath . '/'; if (is_object($container)) { // support use x-* in ref - $xKey = strpos($property, 'x-') === 0 ? substr($property, 2) : null; + $xKey = str_starts_with($property, 'x-') ? substr($property, 2) : null; if ($xKey) { if (!is_array($container->x) || !array_key_exists($xKey, $container->x)) { $xKey = null; diff --git a/src/Attributes/MediaType.php b/src/Attributes/MediaType.php index a14956ad3..533cbd2b5 100644 --- a/src/Attributes/MediaType.php +++ b/src/Attributes/MediaType.php @@ -33,10 +33,7 @@ public function __construct( 'example' => $example, 'x' => $x ?? Generator::UNDEFINED, 'attachables' => $attachables ?? Generator::UNDEFINED, - 'value' => $this->combine($schema, $examples, $this->encodingCompat( - $encoding, - fn (array $args): Encoding => new Encoding(...$args), - )), + 'value' => $this->combine($schema, $examples, $encoding), ]); } } diff --git a/src/Context.php b/src/Context.php index 04f7f50e5..13a6fd9c5 100644 --- a/src/Context.php +++ b/src/Context.php @@ -32,8 +32,8 @@ * @property string|null $property * @property \Reflector|null $reflector Optional reflection details * @property bool|null $static Indicate a static method - * @property bool|null $generated Indicate the context was generated by a processor or - * the serializer + * @property bool|null $generated Indicate the context was generated by a processor, + * type resolver or serializer * @property OA\AbstractAnnotation|null $nested * @property OA\AbstractAnnotation[]|null $annotations * @property OA\AbstractAnnotation[]|null $other Annotations not related to OpenApi @@ -42,7 +42,7 @@ * @property string|null $version The OpenAPI version in use */ #[\AllowDynamicProperties] -class Context +class Context implements \Stringable { /** * Prototypical inheritance for properties. @@ -183,7 +183,7 @@ public function getDebugLocation(): string public function __serialize(): array { - return array_filter(get_object_vars($this), function ($value): bool { + return array_filter(get_object_vars($this), static function ($value): bool { $rc = is_object($value) ? new \ReflectionClass($value) : null; return (!$rc || !$rc->isAnonymous()) @@ -202,7 +202,7 @@ public function __unserialize(array $data): void /** * Traverse the context tree to get the property value. */ - public function __get(string $property) + public function __get(string $property): mixed { if ($this->parent instanceof Context) { return $this->parent->{$property}; @@ -255,7 +255,7 @@ public function fullyQualifiedName(?string $source): string } elseif ($this->uses) { // Unqualified name (Foo) foreach ($this->uses as $alias => $aliasedNamespace) { - if (strcasecmp($alias, $source) === 0) { + if (strcasecmp((string) $alias, $source) === 0) { return '\\' . $aliasedNamespace; } } diff --git a/src/Generator.php b/src/Generator.php index 87e319482..2770f6484 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -21,9 +21,6 @@ * OpenApi spec generator. * * Scans PHP source code and generates OpenApi specifications from the found OpenApi annotations. - * - * This is an object-oriented alternative to using the now deprecated \OpenApi\scan() function and - * static class properties of the Analyzer and Analysis classes. */ class Generator { @@ -172,7 +169,7 @@ protected function normaliseConfig(array $config): array $normalised = []; foreach ($config as $key => $value) { if (is_numeric($key)) { - $token = explode('=', $value); + $token = explode('=', (string) $value); if (2 === count($token)) { // 'operationId.hash=false' [$key, $value] = $token; @@ -183,10 +180,10 @@ protected function normaliseConfig(array $config): array $value = 'true' == $value; } - if ($isList = ('[]' === substr($key, -2))) { - $key = substr($key, 0, -2); + if ($isList = (str_ends_with((string) $key, '[]'))) { + $key = substr((string) $key, 0, -2); } - $token = explode('.', $key); + $token = explode('.', (string) $key); if (2 === count($token)) { // 'operationId.hash' => false // namespaced / processor @@ -237,6 +234,7 @@ public function getProcessorPipeline(): Pipeline new Processors\BuildPaths(), new Processors\AugmentParameters(), new Processors\AugmentRefs(), + new Processors\AugmentItems(), new Processors\MergeJsonContent(), new Processors\MergeXmlContent(), new Processors\AugmentMediaType(), @@ -300,14 +298,6 @@ public function withProcessorPipeline(callable $with): Generator return $this; } - /** - * @deprecated use `withProcessorPipeline()` instead - */ - public function withProcessor(callable $with): Generator - { - return $this->withProcessorPipeline($with); - } - public function setTypeResolver(?TypeResolverInterface $typeResolver): Generator { $this->typeResolver = $typeResolver; @@ -343,38 +333,6 @@ public function setVersion(?string $version): Generator return $this; } - /** - * @deprecated use non-static `generate()` instead - */ - public static function scan(iterable $sources, array $options = []): ?OA\OpenApi - { - // merge with defaults - $config = $options + [ - 'aliases' => self::DEFAULT_ALIASES, - 'namespaces' => self::DEFAULT_NAMESPACES, - 'analyser' => null, - 'analysis' => null, - 'processor' => null, - 'processors' => null, - 'config' => [], - 'logger' => null, - 'validate' => true, - 'version' => null, - ]; - - $processorPipeline = $config['processor'] ?? - ($config['processors'] ? new Pipeline($config['processors']) : null); - - return (new Generator($config['logger'])) - ->setVersion($config['version']) - ->setAliases($config['aliases']) - ->setNamespaces($config['namespaces']) - ->setAnalyser($config['analyser']) - ->setProcessorPipeline($processorPipeline) - ->setConfig($config['config']) - ->generate($sources, $config['analysis'], $config['validate']); - } - /** * Run code in the context of this generator. * @@ -449,7 +407,7 @@ protected function scanSources(iterable $sources, Analysis $analysis, Context $r continue; } if (is_dir($resolvedSource)) { - $this->scanSources(Util::finder($resolvedSource), $analysis, $rootContext); + $this->scanSources(new SourceFinder($resolvedSource), $analysis, $rootContext); } else { $rootContext->logger->debug(sprintf('Analysing source: %s', $resolvedSource)); $analysis->addAnalysis($analyser->fromFile($resolvedSource, $rootContext)); diff --git a/src/Pipeline.php b/src/Pipeline.php index ffe12e577..4c8b8a2b2 100644 --- a/src/Pipeline.php +++ b/src/Pipeline.php @@ -34,10 +34,10 @@ public function remove($pipe = null, ?callable $matcher = null): Pipeline throw new OpenApiException('pipe or callable must not be empty'); } - // allow matching on class name in $pipe in a string + // allow matching on class name if $pipe in a string if (is_string($pipe) && !$matcher) { $pipeClass = $pipe; - $matcher = (fn ($pipe): bool => !$pipe instanceof $pipeClass); + $matcher = (static fn ($pipe): bool => !$pipe instanceof $pipeClass); } if ($matcher) { @@ -71,7 +71,7 @@ public function insert(callable $pipe, $matcher): Pipeline { if (is_string($matcher)) { $before = $matcher; - $matcher = function (array $pipes) use ($before) { + $matcher = static function (array $pipes) use ($before): int|string|null { foreach ($pipes as $ii => $current) { if ($current instanceof $before) { return $ii; @@ -102,14 +102,11 @@ public function walk(callable $walker): Pipeline } /** - * @param mixed $payload - * * @return mixed */ - public function process($payload) + public function process(mixed $payload) { foreach ($this->pipes as $pipe) { - /** @deprecated null payload returned from pipe */ $payload = $pipe($payload) ?: $payload; } diff --git a/src/Processors/AugmentDiscriminators.php b/src/Processors/AugmentDiscriminators.php index baa713ba5..27dc87c73 100644 --- a/src/Processors/AugmentDiscriminators.php +++ b/src/Processors/AugmentDiscriminators.php @@ -17,13 +17,12 @@ class AugmentDiscriminators { public function __invoke(Analysis $analysis): void { - /** @var OA\Discriminator[] $discriminators */ $discriminators = $analysis->getAnnotationsOfType(OA\Discriminator::class); foreach ($discriminators as $discriminator) { if (!Generator::isDefault($discriminator->mapping)) { foreach ($discriminator->mapping as $value => $type) { - if (is_string($type) && $typeSchema = $analysis->getSchemaForSource($type)) { + if (is_string($type) && $typeSchema = $analysis->getAnnotationForSource($type)) { $discriminator->mapping[$value] = OA\Components::ref($typeSchema); } } diff --git a/src/Processors/AugmentItems.php b/src/Processors/AugmentItems.php new file mode 100644 index 000000000..da15e253d --- /dev/null +++ b/src/Processors/AugmentItems.php @@ -0,0 +1,30 @@ +getAnnotationsOfType(OA\Schema::class); + + foreach ($schemas as $schema) { + if ($schema->items instanceof OA\Items) { + $schema->type = 'array'; + } + } + } +} diff --git a/src/Processors/AugmentMediaType.php b/src/Processors/AugmentMediaType.php index d42b2b78c..53707b163 100644 --- a/src/Processors/AugmentMediaType.php +++ b/src/Processors/AugmentMediaType.php @@ -28,7 +28,7 @@ public function __invoke(Analysis $analysis): void } elseif (!Generator::isDefault($schema->ref)) { try { $refSchema = $analysis->openapi->ref($schema->ref); - } catch (OpenApiException $openApiException) { + } catch (OpenApiException) { // ignore $refSchema = null; } diff --git a/src/Processors/AugmentParameters.php b/src/Processors/AugmentParameters.php index fd59f18f1..9a2458ce0 100644 --- a/src/Processors/AugmentParameters.php +++ b/src/Processors/AugmentParameters.php @@ -65,14 +65,23 @@ protected function augmentParameters(Analysis $analysis): void } if ($context->reflector instanceof \ReflectionParameter) { - $schema = new OA\Schema(['_context' => new Context(['reflector' => $context->reflector], $context)]); + $schema = new OA\Schema([ + '_context' => new Context([ + 'generated' => true, + 'reflector' => $context->reflector, + ], $context), + ]); $this->generator->getTypeResolver()->augmentSchemaType($analysis, $schema); $parameter->merge([new OA\Schema([ 'type' => $schema->type, 'format' => $schema->format, 'ref' => $schema->ref, - '_context' => new Context(['nested' => $this, 'comment' => null, 'reflector' => $context->reflector], $context)]), + '_context' => new Context([ + 'nested' => $this, + 'comment' => null, + 'reflector' => $context->reflector, + ], $context)]), ]); if (Generator::isDefault($parameter->required)) { @@ -113,7 +122,6 @@ protected function augmentSharedParameters(Analysis $analysis): void protected function augmentOperationParameters(Analysis $analysis): void { - /** @var OA\Operation[] $operations */ $operations = $analysis->getAnnotationsOfType(OA\Operation::class); foreach ($operations as $operation) { diff --git a/src/Processors/AugmentProperties.php b/src/Processors/AugmentProperties.php index 9d300e95a..1f5bd8cc5 100644 --- a/src/Processors/AugmentProperties.php +++ b/src/Processors/AugmentProperties.php @@ -24,7 +24,6 @@ class AugmentProperties implements GeneratorAwareInterface public function __invoke(Analysis $analysis): void { - /** @var OA\Property[] $properties */ $properties = $analysis->getAnnotationsOfType(OA\Property::class); foreach ($properties as $property) { @@ -57,7 +56,7 @@ public function __invoke(Analysis $analysis): void $typeAndDescription = $this->parseVarLine((string) $context->comment); if ($typeAndDescription['description']) { - $property->description = $typeAndDescription['description']; + $property->description = trim($typeAndDescription['description']); } elseif ($this->isDocblockRoot($property)) { $property->description = $this->parseDocblock($context->comment); } diff --git a/src/Processors/AugmentRefs.php b/src/Processors/AugmentRefs.php index 8e5158a99..7e3bd07f3 100644 --- a/src/Processors/AugmentRefs.php +++ b/src/Processors/AugmentRefs.php @@ -26,7 +26,6 @@ public function __invoke(Analysis $analysis): void */ protected function resolveAllOfRefs(Analysis $analysis): void { - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class); // ref rewriting @@ -47,8 +46,8 @@ protected function resolveAllOfRefs(Analysis $analysis): void foreach ($analysis->annotations as $annotation) { if (property_exists($annotation, 'ref') && !Generator::isDefault($annotation->ref) && $annotation->ref !== null) { foreach ($updatedRefs as $origRef => $updatedRef) { - if (0 === strpos($annotation->ref, $origRef)) { - $annotation->ref = str_replace($origRef, $updatedRef, $annotation->ref); + if (str_starts_with((string) $annotation->ref, $origRef)) { + $annotation->ref = str_replace($origRef, $updatedRef, (string) $annotation->ref); } } } @@ -58,7 +57,6 @@ protected function resolveAllOfRefs(Analysis $analysis): void protected function resolveFQCNRefs(Analysis $analysis): void { - /** @var OA\AbstractAnnotation[] $annotations */ $annotations = $analysis->getAnnotationsOfType(OA\Components::componentTypes()); foreach ($annotations as $annotation) { @@ -71,7 +69,7 @@ protected function resolveFQCNRefs(Analysis $analysis): void $annotation->ref = OA\Components::ref($refSchema); } } - if (!$resolved && ($refAnnotation = $analysis->getAnnotationForSource($annotation->ref, get_class($annotation)))) { + if (!$resolved && ($refAnnotation = $analysis->getAnnotationForSource($annotation->ref, $annotation::class))) { $annotation->ref = OA\Components::ref($refAnnotation); } } @@ -80,7 +78,6 @@ protected function resolveFQCNRefs(Analysis $analysis): void protected function removeDuplicateRefs(Analysis $analysis): void { - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class); foreach ($schemas as $schema) { diff --git a/src/Processors/AugmentSchemas.php b/src/Processors/AugmentSchemas.php index 7f25d5d3d..93baf2c11 100644 --- a/src/Processors/AugmentSchemas.php +++ b/src/Processors/AugmentSchemas.php @@ -20,7 +20,6 @@ class AugmentSchemas { public function __invoke(Analysis $analysis): void { - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class); $this->augmentSchema($schemas); @@ -103,7 +102,7 @@ protected function augmentType(Analysis $analysis, array $schemas): void $schema->type = 'object'; } } else { - if (is_string($schema->type) && $typeSchema = $analysis->getSchemaForSource($schema->type)) { + if (is_string($schema->type) && $typeSchema = $analysis->getAnnotationForSource($schema->type)) { if (Generator::isDefault($schema->format)) { $schema->ref = OA\Components::ref($typeSchema); $schema->type = Generator::UNDEFINED; diff --git a/src/Processors/AugmentTags.php b/src/Processors/AugmentTags.php index bdbd1c515..78b845d1a 100644 --- a/src/Processors/AugmentTags.php +++ b/src/Processors/AugmentTags.php @@ -35,7 +35,6 @@ public function setWhitelist(array $whitelist): AugmentTags public function __invoke(Analysis $analysis): void { - /** @var OA\Operation[] $operations */ $operations = $analysis->getAnnotationsOfType(OA\Operation::class); $usedTagNames = []; diff --git a/src/Processors/BuildPaths.php b/src/Processors/BuildPaths.php index 87ea8b812..03deea4a2 100644 --- a/src/Processors/BuildPaths.php +++ b/src/Processors/BuildPaths.php @@ -33,7 +33,6 @@ public function __invoke(Analysis $analysis): void } } - /** @var OA\Operation[] $operations */ $operations = $analysis->unmerged()->getAnnotationsOfType(OA\Operation::class); // Merge @OA\Operations into existing @OA\PathItems or create a new one. diff --git a/src/Processors/Concerns/AnnotationTrait.php b/src/Processors/Concerns/AnnotationTrait.php index c2e420896..34db6d6e2 100644 --- a/src/Processors/Concerns/AnnotationTrait.php +++ b/src/Processors/Concerns/AnnotationTrait.php @@ -19,7 +19,7 @@ public function collectAnnotations($root): \SplObjectStorage { $storage = new \SplObjectStorage(); - $this->traverseAnnotations($root, function ($item) use (&$storage): void { + $this->traverseAnnotations($root, static function ($item) use (&$storage): void { if ($item instanceof OA\AbstractAnnotation && !$storage->contains($item)) { $storage->attach($item); } @@ -34,7 +34,7 @@ public function collectAnnotations($root): \SplObjectStorage public function removeAnnotation(iterable $root, OA\AbstractAnnotation $annotation, bool $recurse = true): void { $remove = $this->collectAnnotations($annotation); - $this->traverseAnnotations($root, function ($item) use ($remove): void { + $this->traverseAnnotations($root, static function ($item) use ($remove): void { if ($item instanceof \SplObjectStorage) { foreach ($remove as $annotation) { $item->detach($annotation); diff --git a/src/Processors/Concerns/DocblockTrait.php b/src/Processors/Concerns/DocblockTrait.php index 65fe370eb..6d42019c8 100644 --- a/src/Processors/Concerns/DocblockTrait.php +++ b/src/Processors/Concerns/DocblockTrait.php @@ -41,7 +41,7 @@ public function isDocblockRoot(OA\AbstractAnnotation $annotation): bool foreach ($matchPriorityMap as $className => $strict) { foreach ($annotation->_context->annotations as $contextAnnotation) { if ($strict) { - if ($className === get_class($contextAnnotation)) { + if ($className === $contextAnnotation::class) { return $annotation === $contextAnnotation; } } else { @@ -96,13 +96,13 @@ public function parseDocblock(?string $docblock, ?array &$tags = null): string $comment = preg_split('/(\n|\r\n)/', (string) $docblock); $comment[0] = preg_replace('/[ \t]*\\/\*\*/', '', $comment[0]); // strip '/**' $ii = count($comment) - 1; - $comment[$ii] = preg_replace('/\*\/[ \t]*$/', '', $comment[$ii]); // strip '*/' + $comment[$ii] = preg_replace('/\*\/[ \t]*$/', '', (string) $comment[$ii]); // strip '*/' $lines = []; $append = false; $skip = false; foreach ($comment as $line) { - $line = preg_replace('/^\s+\* ?/', '', $line); - if (substr($tagline = trim($line), 0, 1) === '@') { + $line = preg_replace('/^\s+\* ?/', '', (string) $line); + if (str_starts_with($tagline = trim((string) $line), '@')) { $this->handleTag($tagline, $tags); $skip = true; } @@ -111,11 +111,11 @@ public function parseDocblock(?string $docblock, ?array &$tags = null): string } if ($append) { $ii = count($lines) - 1; - $lines[$ii] = substr($lines[$ii], 0, -1) . $line; + $lines[$ii] = substr((string) $lines[$ii], 0, -1) . $line; } else { $lines[] = $line; } - $append = (substr($line, -1) === '\\'); + $append = (str_ends_with((string) $line, '\\')); } $description = trim(implode("\n", $lines)); @@ -140,7 +140,7 @@ public function extractCommentSummary(string $content): string $summary = ''; foreach ($lines as $line) { $summary .= $line . "\n"; - if ($line === '' || substr($line, -1) === '.') { + if ($line === '' || str_ends_with($line, '.')) { return trim($summary); } } @@ -169,7 +169,7 @@ public function extractCommentDescription(string $content): string } $description = ''; - if (false !== ($substr = substr($content, strlen($summary)))) { + if (false !== ($substr = substr($content, strlen((string) $summary)))) { $description = trim($substr); } @@ -186,14 +186,14 @@ public function parseVarLine(?string $docblock): array $comment = str_replace("\r\n", "\n", (string) $docblock); $comment = preg_replace('/\*\/[ \t]*$/', '', $comment); // strip '*/' - preg_match('/@var\s+(?[^\s]+)([ \t])?(?.+)?+$/im', $comment, $matches); + preg_match('/@var\s+(?[^\s]+)([ \t])?(?.+)?+$/im', (string) $comment, $matches); $result = array_merge( ['type' => null, 'description' => null], - array_filter($matches, fn ($key): bool => in_array($key, ['type', 'description']), ARRAY_FILTER_USE_KEY) + array_filter($matches, static fn ($key): bool => in_array($key, ['type', 'description']), ARRAY_FILTER_USE_KEY) ); - return array_map(fn (?string $value): ?string => null !== $value ? trim($value) : null, $result); + return array_map(static fn (?string $value): ?string => null !== $value ? trim($value) : null, $result); } /** diff --git a/src/Processors/Concerns/RefTrait.php b/src/Processors/Concerns/RefTrait.php index a97b19589..332351944 100644 --- a/src/Processors/Concerns/RefTrait.php +++ b/src/Processors/Concerns/RefTrait.php @@ -19,6 +19,6 @@ protected function toRefKey(Context $context, ?string $name): string protected function isRef(?string $ref): bool { - return $ref && 0 === strpos($ref, '#/'); + return $ref && str_starts_with($ref, '#/'); } } diff --git a/src/Processors/ExpandClasses.php b/src/Processors/ExpandClasses.php index 29770b0c5..2e552c9be 100644 --- a/src/Processors/ExpandClasses.php +++ b/src/Processors/ExpandClasses.php @@ -23,7 +23,6 @@ class ExpandClasses public function __invoke(Analysis $analysis): void { - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true); foreach ($schemas as $schema) { @@ -31,7 +30,7 @@ public function __invoke(Analysis $analysis): void $ancestors = $analysis->getSuperClasses($schema->_context->fullyQualifiedName($schema->_context->class)); $existing = []; foreach ($ancestors as $ancestor) { - $ancestorSchema = $analysis->getSchemaForSource($ancestor['context']->fullyQualifiedName($ancestor['class'])); + $ancestorSchema = $analysis->getAnnotationForSource($ancestor['context']->fullyQualifiedName($ancestor['class'])); if ($ancestorSchema) { $refPath = Generator::isDefault($ancestorSchema->schema) ? $ancestor['class'] : $ancestorSchema->schema; $this->inheritFrom($analysis, $schema, $ancestorSchema, $refPath, $ancestor['context']); diff --git a/src/Processors/ExpandEnums.php b/src/Processors/ExpandEnums.php index 184f77021..1a88759cf 100644 --- a/src/Processors/ExpandEnums.php +++ b/src/Processors/ExpandEnums.php @@ -63,7 +63,6 @@ public function __invoke(Analysis $analysis): void protected function expandContextEnum(Analysis $analysis): void { - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true); foreach ($schemas as $schema) { @@ -74,20 +73,17 @@ protected function expandContextEnum(Analysis $analysis): void $schemaType = $schema->type; $enumType = null; if ($re->isBacked()) { - $backingType = $re->getBackingType(); - if ($backingType instanceof \ReflectionNamedType) { - $enumType = $backingType->getName(); - } + $enumType = $re->getBackingType()->getName(); } // no (or invalid) schema type means name $useName = Generator::isDefault($schemaType) || ($enumType && $this->generator->getTypeResolver()->native2spec($enumType) != $schemaType); - $schema->enum = array_map(fn (\ReflectionEnumUnitCase $case) => ($useName || !($case instanceof \ReflectionEnumBackedCase)) ? $case->name : $case->getBackingValue(), $re->getCases()); + $schema->enum = array_map(static fn (\ReflectionEnumUnitCase $case): int|string => ($useName || !($case instanceof \ReflectionEnumBackedCase)) ? $case->name : $case->getBackingValue(), $re->getCases()); if ($this->enumNames !== null && !$useName) { $schemaX = Generator::isDefault($schema->x) ? [] : $schema->x; - $schemaX[$this->enumNames] = array_map(fn (\ReflectionEnumUnitCase $case): string => $case->name, $re->getCases()); + $schemaX[$this->enumNames] = array_map(static fn (\ReflectionEnumUnitCase $case): string => $case->name, $re->getCases()); $schema->x = $schemaX; } @@ -101,7 +97,6 @@ protected function expandContextEnum(Analysis $analysis): void protected function expandSchemaEnum(Analysis $analysis): void { - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType([OA\Schema::class, OA\ServerVariable::class]); foreach ($schemas as $schema) { diff --git a/src/Processors/ExpandInterfaces.php b/src/Processors/ExpandInterfaces.php index 1df06a526..3ca658026 100644 --- a/src/Processors/ExpandInterfaces.php +++ b/src/Processors/ExpandInterfaces.php @@ -21,7 +21,6 @@ class ExpandInterfaces public function __invoke(Analysis $analysis): void { - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true); foreach ($schemas as $schema) { @@ -32,7 +31,7 @@ public function __invoke(Analysis $analysis): void if (class_exists($className) && ($parent = get_parent_class($className)) && ($inherited = array_keys(class_implements($parent)))) { // strip interfaces we inherit from ancestor foreach (array_keys($interfaces) as $interface) { - if (in_array(ltrim($interface, '\\'), $inherited)) { + if (in_array(ltrim((string) $interface, '\\'), $inherited)) { unset($interfaces[$interface]); } } @@ -41,7 +40,7 @@ public function __invoke(Analysis $analysis): void $existing = []; foreach ($interfaces as $interface) { $interfaceName = $interface['context']->fullyQualifiedName($interface['interface']); - $interfaceSchema = $analysis->getSchemaForSource($interfaceName); + $interfaceSchema = $analysis->getAnnotationForSource($interfaceName); if ($interfaceSchema) { $refPath = Generator::isDefault($interfaceSchema->schema) ? $interface['interface'] : $interfaceSchema->schema; $this->inheritFrom($analysis, $schema, $interfaceSchema, $refPath, $interface['context']); diff --git a/src/Processors/ExpandTraits.php b/src/Processors/ExpandTraits.php index 9875862bb..55db19ff0 100644 --- a/src/Processors/ExpandTraits.php +++ b/src/Processors/ExpandTraits.php @@ -21,7 +21,6 @@ class ExpandTraits public function __invoke(Analysis $analysis): void { - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true); // do regular trait inheritance / merge @@ -30,7 +29,7 @@ public function __invoke(Analysis $analysis): void $traits = $analysis->getTraitsOfClass($schema->_context->fullyQualifiedName($schema->_context->trait), true); $existing = []; foreach ($traits as $trait) { - $traitSchema = $analysis->getSchemaForSource($trait['context']->fullyQualifiedName($trait['trait'])); + $traitSchema = $analysis->getAnnotationForSource($trait['context']->fullyQualifiedName($trait['trait'])); if ($traitSchema) { $refPath = Generator::isDefault($traitSchema->schema) ? $trait['trait'] : $traitSchema->schema; $this->inheritFrom($analysis, $schema, $traitSchema, $refPath, $trait['context']); @@ -48,7 +47,7 @@ public function __invoke(Analysis $analysis): void $traits = $analysis->getTraitsOfClass($schema->_context->fullyQualifiedName($schema->_context->class), true); $existing = []; foreach ($traits as $trait) { - $traitSchema = $analysis->getSchemaForSource($trait['context']->fullyQualifiedName($trait['trait'])); + $traitSchema = $analysis->getAnnotationForSource($trait['context']->fullyQualifiedName($trait['trait'])); if ($traitSchema) { $refPath = Generator::isDefault($traitSchema->schema) ? $trait['trait'] : $traitSchema->schema; $this->inheritFrom($analysis, $schema, $traitSchema, $refPath, $trait['context']); @@ -62,7 +61,7 @@ public function __invoke(Analysis $analysis): void $ancestors = $analysis->getSuperClasses($schema->_context->fullyQualifiedName($schema->_context->class)); $existing = []; foreach ($ancestors as $ancestor) { - $ancestorSchema = $analysis->getSchemaForSource($ancestor['context']->fullyQualifiedName($ancestor['class'])); + $ancestorSchema = $analysis->getAnnotationForSource($ancestor['context']->fullyQualifiedName($ancestor['class'])); if ($ancestorSchema) { // stop here as we inherit everything above break; diff --git a/src/Processors/MergeIntoOpenApi.php b/src/Processors/MergeIntoOpenApi.php index 570418efc..19f76b092 100644 --- a/src/Processors/MergeIntoOpenApi.php +++ b/src/Processors/MergeIntoOpenApi.php @@ -80,7 +80,7 @@ public function __invoke(Analysis $analysis): void if ($this->isMergeComponents()) { // merge Components - $componentsList = array_filter($merge, fn (OA\AbstractAnnotation $annotation): bool => $annotation instanceof OA\Components); + $componentsList = array_filter($merge, static fn (OA\AbstractAnnotation $annotation): bool => $annotation instanceof OA\Components); $firstComponents = $openapi->components; if ((!Generator::isDefault($firstComponents) && $componentsList !== []) || count($merge) > 1) { @@ -101,7 +101,7 @@ public function __invoke(Analysis $analysis): void $analysis->annotations->detach($components); } - $merge = array_filter($merge, fn (OA\AbstractAnnotation $annotation): bool => !$annotation instanceof OA\Components); + $merge = array_filter($merge, static fn (OA\AbstractAnnotation $annotation): bool => !$annotation instanceof OA\Components); $merge[] = $firstComponents; } } diff --git a/src/Processors/MergeJsonContent.php b/src/Processors/MergeJsonContent.php index 68c44ac24..e5ead6119 100644 --- a/src/Processors/MergeJsonContent.php +++ b/src/Processors/MergeJsonContent.php @@ -18,7 +18,6 @@ class MergeJsonContent { public function __invoke(Analysis $analysis): void { - /** @var OA\JsonContent[] $annotations */ $annotations = $analysis->getAnnotationsOfType(OA\JsonContent::class); foreach ($annotations as $jsonContent) { diff --git a/src/Processors/MergeXmlContent.php b/src/Processors/MergeXmlContent.php index 0d4cdd23a..76b88bec3 100644 --- a/src/Processors/MergeXmlContent.php +++ b/src/Processors/MergeXmlContent.php @@ -18,7 +18,6 @@ class MergeXmlContent { public function __invoke(Analysis $analysis): void { - /** @var OA\XmlContent[] $annotations */ $annotations = $analysis->getAnnotationsOfType(OA\XmlContent::class); foreach ($annotations as $xmlContent) { diff --git a/src/Serializer.php b/src/Serializer.php index 56f9f992f..5b63aebb5 100644 --- a/src/Serializer.php +++ b/src/Serializer.php @@ -107,11 +107,11 @@ protected function doDeserialize(\stdClass $c, string $class, Context $context): $property = 'ref'; } - if (substr($property, 0, 2) === 'x-') { + if (str_starts_with((string) $property, 'x-')) { if (Generator::isDefault($annotation->x)) { $annotation->x = []; } - $custom = substr($property, 2); + $custom = substr((string) $property, 2); $annotation->x[$custom] = $value; } else { $annotation->{$property} = $this->doDeserializeProperty($annotation, $property, $value, $context); @@ -182,12 +182,12 @@ protected function doDeserializeProperty(OA\AbstractAnnotation $annotation, stri * * @return array|OA\AbstractAnnotation */ - protected function doDeserializeBaseProperty($type, $value, Context $context) + protected function doDeserializeBaseProperty($type, mixed $value, Context $context) { $isAnnotationClass = is_string($type) && is_subclass_of(trim($type, '[]'), OA\AbstractAnnotation::class); if ($isAnnotationClass) { - $isArray = strpos($type, '[') === 0 && substr($type, -1) === ']'; + $isArray = str_starts_with($type, '[') && str_ends_with($type, ']'); if ($isArray) { $annotationArr = []; diff --git a/src/SourceFinder.php b/src/SourceFinder.php new file mode 100644 index 000000000..b351eecbc --- /dev/null +++ b/src/SourceFinder.php @@ -0,0 +1,63 @@ +sortByName() + ->files() + ->followLinks() + ->name($pattern); + + $directories = (array) $directory; + + foreach ($directories as $path) { + if (is_file($path)) { + $this->append([$path]); + } else { + $this->in($path); + } + } + + foreach ((array) $exclude as $path) { + $this->notPath($this->getRelativePath($path, $directories)); + } + } + + /** + * Turns the given $fullPath into a relative path based on $basePaths, which can either + * be a single string path, or a list of possible paths. If a list is given, the first + * matching basePath in the list will be used to compute the relative path. If no + * relative path could be computed, the original string will be returned because there + * is always a chance it was a valid relative path to begin with. + * + * It should be noted that these are "relative paths" primarily in Finder's sense of them, + * and conform specifically to what is expected by functions like exclude() and notPath(). + * + * In particular, leading and trailing slashes are removed. + */ + private function getRelativePath(string $fullPath, array $directories): string + { + foreach ($directories as $directory) { + if (str_starts_with((string) $directory, $fullPath)) { + $relativePath = substr((string) $directory, strlen($fullPath)); + + if ($relativePath !== '' && $relativePath !== '0') { + return trim($relativePath, '/'); + } + } + } + + return $fullPath; + } +} diff --git a/src/Type/AbstractTypeResolver.php b/src/Type/AbstractTypeResolver.php index 263008113..c5f070dc8 100644 --- a/src/Type/AbstractTypeResolver.php +++ b/src/Type/AbstractTypeResolver.php @@ -8,7 +8,6 @@ use OpenApi\Analysis; use OpenApi\Annotations as OA; -use OpenApi\Context; use OpenApi\Generator; use OpenApi\TypeResolverInterface; @@ -16,7 +15,7 @@ abstract class AbstractTypeResolver implements TypeResolverInterface { protected function type2ref(OA\Schema $schema, Analysis $analysis, string $sourceClass = OA\Schema::class): void { - if (!Generator::isDefault($schema->type)) { + if (!Generator::isDefault($schema->type) && !is_array($schema->type)) { if ($typeSchema = $analysis->getAnnotationForSource($schema->type, $sourceClass)) { $schema->type = Generator::UNDEFINED; $schema->ref = OA\Components::ref($typeSchema); @@ -24,34 +23,6 @@ protected function type2ref(OA\Schema $schema, Analysis $analysis, string $sourc } } - protected function augmentItems(OA\Schema $schema, Analysis $analysis): void - { - if (!Generator::isDefault($schema->type)) { - if (Generator::isDefault($schema->items)) { - $schema->items = new OA\Items([ - 'type' => $schema->type, - '_context' => new Context(['generated' => true], $schema->_context), - ]); - - $this->type2ref($schema->items, $analysis); - - $analysis->addAnnotation($schema->items, $schema->items->_context); - - if (!Generator::isDefault($schema->ref)) { - $schema->items->ref = $schema->ref; - $schema->ref = Generator::UNDEFINED; - } - } elseif (Generator::isDefault($schema->items->type, $schema->items->oneOf, $schema->items->allOf, $schema->items->anyOf)) { - $schema->items->type = $schema->type; - - $this->type2ref($schema->items, $analysis); - } - } - - $this->mapNativeType($schema->items, $schema->items->type); - $schema->type = 'array'; - } - /** * @param string|array $type */ @@ -60,7 +31,7 @@ public function mapNativeType(OA\Schema $schema, $type): bool if (is_array($type)) { $mapped = []; foreach ($type as $t) { - $mapped[] = $this->native2spec(strtolower($t)); + $mapped[] = $this->native2spec(strtolower((string) $t)); } $schema->type = $mapped; diff --git a/src/Type/LegacyTypeResolver.php b/src/Type/LegacyTypeResolver.php index 4d54101b7..b7fb84395 100644 --- a/src/Type/LegacyTypeResolver.php +++ b/src/Type/LegacyTypeResolver.php @@ -12,6 +12,9 @@ use OpenApi\Generator; use OpenApi\TypeResolverInterface; +/** + * @deprecated use `TypeInfoTypeResolver` instead + */ class LegacyTypeResolver extends AbstractTypeResolver { /** @inheritdoc */ @@ -66,9 +69,37 @@ protected function doAugment(Analysis $analysis, OA\Schema $schema, \Reflector $ } } + protected function augmentItems(OA\Schema $schema, Analysis $analysis): void + { + if (!Generator::isDefault($schema->type)) { + if (Generator::isDefault($schema->items)) { + $schema->items = new OA\Items([ + 'type' => $schema->type, + '_context' => new Context(['generated' => true], $schema->_context), + ]); + + $this->type2ref($schema->items, $analysis); + + $analysis->addAnnotation($schema->items, $schema->items->_context); + + if (!Generator::isDefault($schema->ref)) { + $schema->items->ref = $schema->ref; + $schema->ref = Generator::UNDEFINED; + } + } elseif (Generator::isDefault($schema->items->type, $schema->items->oneOf, $schema->items->allOf, $schema->items->anyOf)) { + $schema->items->type = $schema->type; + + $this->type2ref($schema->items, $analysis); + } + } + + $this->mapNativeType($schema->items, $schema->items->type); + $schema->type = 'array'; + } + protected function normaliseTypeResult(?string $explicitType = null, ?array $explicitDetails = null, array $types = [], ?string $name = null, ?bool $nullable = null, ?bool $isArray = null, bool $unsupported = false, ?Context $context = null): \stdClass { - $types = array_filter($types, fn (string $t): bool => !in_array($t, ['null', ''])); + $types = array_filter($types, static fn (string $t): bool => !in_array($t, ['null', ''])); if ($context) { foreach ($types as $ii => $type) { @@ -142,22 +173,15 @@ protected function getReflectionTypeDetails(\Reflector $reflector, ?Context $con */ protected function getDocblockTypeDetails(\Reflector $reflector, ?Context $context): \stdClass { - switch (true) { - case $reflector instanceof \ReflectionProperty: - $docComment = (method_exists($reflector, 'isPromoted') && $reflector->isPromoted()) - && $reflector->getDeclaringClass() && $reflector->getDeclaringClass()->getConstructor() - ? $reflector->getDeclaringClass()->getConstructor()->getDocComment() - : $reflector->getDocComment(); - break; - case $reflector instanceof \ReflectionParameter: - $docComment = $reflector->getDeclaringFunction()->getDocComment(); - break; - case $reflector instanceof \ReflectionFunctionAbstract: - $docComment = $reflector->getDocComment(); - break; - default: - $docComment = null; - } + $docComment = match (true) { + $reflector instanceof \ReflectionProperty => (method_exists($reflector, 'isPromoted') && $reflector->isPromoted()) + && $reflector->getDeclaringClass() && $reflector->getDeclaringClass()->getConstructor() + ? $reflector->getDeclaringClass()->getConstructor()->getDocComment() + : $reflector->getDocComment(), + $reflector instanceof \ReflectionParameter => $reflector->getDeclaringFunction()->getDocComment(), + $reflector instanceof \ReflectionFunctionAbstract => $reflector->getDocComment(), + default => null, + }; // cheat $name = $reflector->getName(); @@ -166,21 +190,14 @@ protected function getDocblockTypeDetails(\Reflector $reflector, ?Context $conte return $this->normaliseTypeResult(null, null, [], $name, null, null, false, $context); } - switch (true) { - case $reflector instanceof \ReflectionProperty: - $tagName = (method_exists($reflector, 'isPromoted') && $reflector->isPromoted()) - ? '@param' - : '@var'; - break; - case $reflector instanceof \ReflectionParameter: - $tagName = '@param'; - break; - case $reflector instanceof \ReflectionFunctionAbstract: - $tagName = '@return'; - break; - default: - $tagName = null; - } + $tagName = match (true) { + $reflector instanceof \ReflectionProperty => (method_exists($reflector, 'isPromoted') && $reflector->isPromoted()) + ? '@param' + : '@var', + $reflector instanceof \ReflectionParameter => '@param', + $reflector instanceof \ReflectionFunctionAbstract => '@return', + default => null, + }; if (!$tagName) { return $this->normaliseTypeResult(null, null, [], $name, null, null, false, $context); @@ -216,7 +233,7 @@ protected function getDocblockTypeDetails(\Reflector $reflector, ?Context $conte if ($result) { $type = $isArray ? $matches[2] : $matches[1]; if ('int' === $type) { - $minMax = array_map(fn (string $s): string => trim($s), explode(',', $matches[2])); + $minMax = array_map(trim(...), explode(',', $matches[2])); if (2 === count($minMax)) { $explicitDetails = [ 'min' => (int) ('min' === $minMax[0] ? \PHP_INT_MIN : $minMax[0]), diff --git a/src/Type/TypeInfoTypeResolver.php b/src/Type/TypeInfoTypeResolver.php index fcfabc50b..9f7da4aa7 100644 --- a/src/Type/TypeInfoTypeResolver.php +++ b/src/Type/TypeInfoTypeResolver.php @@ -8,6 +8,7 @@ use OpenApi\Analysis; use OpenApi\Annotations as OA; +use OpenApi\Context; use OpenApi\Generator; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; @@ -26,8 +27,10 @@ use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\CollectionType; use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; +use Symfony\Component\TypeInfo\Type\IntersectionType; use Symfony\Component\TypeInfo\Type\NullableType; use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\Type\UnionType; use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; use Symfony\Component\TypeInfo\TypeResolver\ReflectionTypeResolver; @@ -51,38 +54,7 @@ protected function doAugment(Analysis $analysis, OA\Schema $schema, \Reflector $ $reflectionType = $reflectionType instanceof NullableType ? $reflectionType->getWrappedType() : $reflectionType; if (Generator::isDefault($schema->type, $schema->oneOf, $schema->allOf, $schema->anyOf) && ($docblockType || $reflectionType)) { - $type = $docblockType ?? $reflectionType; - - $isNonZeroInt = false; - if ($type instanceof CompositeTypeInterface && 2 === count($type->getTypes())) { - $types = $type->getTypes(); - $isNonZeroInt = $types[0] instanceof IntRangeType && $types[1] instanceof IntRangeType; - } - - if (!$type instanceof CompositeTypeInterface || $isNonZeroInt) { - if ($isNonZeroInt) { - $schema->type = 'int'; - $schema->not = $schema->_context->isVersion('3.1.x') - ? ['const' => 0] - : ['enum' => [0]]; - } elseif ($type instanceof BuiltinType || $type instanceof ObjectType) { - $schema->type = (string) $type; - } elseif ($type instanceof IntRangeType) { - $schema->type = $type->getTypeIdentifier()->value; - - $schema->minimum = $type->getFrom(); - $schema->maximum = $type->getTo(); - - } elseif ($type instanceof ExplicitType) { - $schema->type = $type->getTypeIdentifier()->value; - } elseif ($type instanceof CollectionType) { - $schema->type = (string) $type->getCollectionValueType(); - } - } - } - - if ($docblockType instanceof CollectionType || $reflectionType instanceof CollectionType) { - $this->augmentItems($schema, $analysis); + $this->setSchemaType($schema, $docblockType ?? $reflectionType, $analysis, $sourceClass); } $this->type2ref($schema, $analysis, $sourceClass); @@ -99,7 +71,94 @@ protected function doAugment(Analysis $analysis, OA\Schema $schema, \Reflector $ } } - /** + protected function setSchemaType(OA\Schema $schema, Type $type, Analysis $analysis, string $sourceClass = OA\Schema::class): OA\Schema + { + if ($type instanceof CompositeTypeInterface) { + $types = $type->getTypes(); + + $isNonZeroInt = 2 === count($types) && $types[0] instanceof IntRangeType && $types[1] instanceof IntRangeType; + + if ($isNonZeroInt) { + $schema->type = 'int'; + $schema->not = $schema->_context->isVersion('3.1.x') + ? ['const' => 0] + : ['enum' => [0]]; + } else { + $allBuiltin = array_reduce($types, static fn ($carry, $t): bool => $carry && $t instanceof BuiltinType, true); + + if ($type instanceof UnionType) { + if ($allBuiltin) { + $schema->type = array_map(static fn (Type $t): string => (string) $t, $types); + } else { + $builtinTypes = array_filter($types, static fn (Type $t): bool => $t instanceof BuiltinType); + $otherTypes = array_filter($types, static fn (Type $t): bool => !$t instanceof BuiltinType); + + $schema->type = Generator::UNDEFINED; + $schema->oneOf = []; + + if ($builtinTypes) { + $schema->oneOf[] = $builtinSchema = new OA\Schema([ + 'type' => array_values(array_map(static fn (Type $t): string => (string) $t, $builtinTypes)), + '_context' => new Context(['generated' => true], $schema->_context), + ]); + $this->type2ref($builtinSchema, $analysis); + $analysis->addAnnotation($builtinSchema, $builtinSchema->_context); + } + + foreach ($otherTypes as $otherType) { + $otherSchema = new OA\Schema([ + '_context' => new Context(['generated' => true], $schema->_context), + ]); + $schema->oneOf[] = $this->setSchemaType($otherSchema, $otherType, $analysis); + $this->type2ref($otherSchema, $analysis); + $analysis->addAnnotation($otherSchema, $otherSchema->_context); + } + } + } elseif ($type instanceof IntersectionType) { + $schema->type = Generator::UNDEFINED; + $schema->allOf = []; + + foreach ($types as $intersectionType) { + $intersectionSchema = new OA\Schema([ + '_context' => new Context(['generated' => true], $schema->_context), + ]); + $schema->allOf[] = $this->setSchemaType($intersectionSchema, $intersectionType, $analysis); + $this->type2ref($intersectionSchema, $analysis); + $analysis->addAnnotation($intersectionSchema, $intersectionSchema->_context); + } + } + } + } else { + if ($type instanceof BuiltinType || $type instanceof ObjectType) { + $schema->type = (string) $type; + } elseif ($type instanceof IntRangeType) { + $schema->type = $type->getTypeIdentifier()->value; + + $schema->minimum = $type->getFrom(); + $schema->maximum = $type->getTo(); + } elseif ($type instanceof ExplicitType) { + $schema->type = $type->getTypeIdentifier()->value; + } elseif ($type instanceof CollectionType) { + $schema->type = 'array'; + + if (Generator::isDefault($schema->items)) { + $schema->items = new OA\Items(['_context' => new Context(['generated' => true], $schema->_context)]); + $this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis); + $this->type2ref($schema->items, $analysis); + $analysis->addAnnotation($schema->items, $schema->items->_context); + } elseif (Generator::isDefault($schema->items->type, $schema->items->oneOf, $schema->items->allOf, $schema->items->anyOf)) { + $this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis); + $this->type2ref($schema->items, $analysis); + } + + $this->mapNativeType($schema->items, $schema->items->type); + } + } + + return $schema; + } + + /**645 1050272 02 1268 0026220 00 * @param \ReflectionParameter|\ReflectionProperty|\ReflectionMethod $reflector */ protected function getReflectionType(\Reflector $reflector): ?Type @@ -115,7 +174,7 @@ protected function getReflectionType(\Reflector $reflector): ?Type $typeContext = (new TypeContextFactory())->createFromReflection($reflector); return (new ReflectionTypeResolver())->resolve($subject, $typeContext); - } catch (UnsupportedException $unsupportedException) { + } catch (UnsupportedException) { // ignore } @@ -127,22 +186,15 @@ protected function getReflectionType(\Reflector $reflector): ?Type */ public function getDocblockType(\Reflector $reflector): ?Type { - switch (true) { - case $reflector instanceof \ReflectionProperty: - $docComment = (method_exists($reflector, 'isPromoted') && $reflector->isPromoted()) - && $reflector->getDeclaringClass() && $reflector->getDeclaringClass()->getConstructor() - ? $reflector->getDeclaringClass()->getConstructor()->getDocComment() - : $reflector->getDocComment(); - break; - case $reflector instanceof \ReflectionParameter: - $docComment = $reflector->getDeclaringFunction()->getDocComment(); - break; - case $reflector instanceof \ReflectionFunctionAbstract: - $docComment = $reflector->getDocComment(); - break; - default: - $docComment = null; - } + $docComment = match (true) { + $reflector instanceof \ReflectionProperty => (method_exists($reflector, 'isPromoted') && $reflector->isPromoted()) + && $reflector->getDeclaringClass() && $reflector->getDeclaringClass()->getConstructor() + ? $reflector->getDeclaringClass()->getConstructor()->getDocComment() + : $reflector->getDocComment(), + $reflector instanceof \ReflectionParameter => $reflector->getDeclaringFunction()->getDocComment(), + $reflector instanceof \ReflectionFunctionAbstract => $reflector->getDocComment(), + default => null, + }; if (!$docComment) { return null; @@ -150,21 +202,14 @@ public function getDocblockType(\Reflector $reflector): ?Type $typeContext = (new TypeContextFactory())->createFromReflection($reflector); - switch (true) { - case $reflector instanceof \ReflectionProperty: - $tagName = (method_exists($reflector, 'isPromoted') && $reflector->isPromoted()) - ? '@param' - : '@var'; - break; - case $reflector instanceof \ReflectionParameter: - $tagName = '@param'; - break; - case $reflector instanceof \ReflectionFunctionAbstract: - $tagName = '@return'; - break; - default: - $tagName = null; - } + $tagName = match (true) { + $reflector instanceof \ReflectionProperty => (method_exists($reflector, 'isPromoted') && $reflector->isPromoted()) + ? '@param' + : '@var', + $reflector instanceof \ReflectionParameter => '@param', + $reflector instanceof \ReflectionFunctionAbstract => '@return', + default => null, + }; $lexer = new Lexer(new ParserConfig([])); $phpDocParser = new PhpDocParser( @@ -186,7 +231,7 @@ public function getDocblockType(\Reflector $reflector): ?Type ) { try { return (new StringTypeResolver())->resolve((string) $tagValue, $typeContext); - } catch (UnsupportedException $e) { + } catch (UnsupportedException) { // ignore } } diff --git a/src/Util.php b/src/Util.php deleted file mode 100644 index d21abcf64..000000000 --- a/src/Util.php +++ /dev/null @@ -1,156 +0,0 @@ -exclude() and notPath(). - * In particular, leading and trailing slashes are removed. - * - * @param array|string $basePaths - */ - public static function getRelativePath(string $fullPath, $basePaths): string - { - $relativePath = null; - if (is_string($basePaths)) { // just a single path, not an array of possible paths - $relativePath = self::removePrefix($fullPath, $basePaths); - } else { // an array of paths - foreach ($basePaths as $basePath) { - $relativePath = self::removePrefix($fullPath, $basePath); - if (!in_array($relativePath, [null, '', '0'], true)) { - break; - } - } - } - - return in_array($relativePath, [null, '', '0'], true) ? $fullPath : trim($relativePath, '/'); - } - - /** - * Removes a prefix from the start of a string if it exists, or null otherwise. - */ - private static function removePrefix(string $str, string $prefix): ?string - { - if (substr($str, 0, strlen($prefix)) === $prefix) { - return substr($str, strlen($prefix)); - } - - return null; - } - - /** - * Build a Symfony Finder object that scans the given $directory. - * - * @param array|Finder|string $directory The directory(s) or filename(s) - * @param null|array|string $exclude The directory(s) or filename(s) to exclude (as absolute or relative paths) - * @param null|string $pattern The pattern of the files to scan - * - * @throws \InvalidArgumentException - */ - public static function finder($directory, $exclude = null, $pattern = null): Finder - { - if ($directory instanceof Finder) { - // Make sure that the provided Finder only finds files and follows symbolic links. - return $directory->files()->followLinks(); - } else { - $finder = new Finder(); - $finder->sortByName(); - } - if ($pattern === null) { - $pattern = '*.php'; - } - - $finder->files()->followLinks()->name($pattern); - if (is_string($directory)) { - if (is_file($directory)) { // Scan a single file? - $finder->append([$directory]); - } else { // Scan a directory - $finder->in($directory); - } - } elseif (is_array($directory)) { - foreach ($directory as $path) { - if (is_file($path)) { // Scan a file? - $finder->append([$path]); - } else { - $finder->in($path); - } - } - } else { - throw new OpenApiException('Unexpected $directory value:' . gettype($directory)); - } - if ($exclude !== null) { - if (is_string($exclude)) { - $finder->notPath(Util::getRelativePath($exclude, $directory)); - } elseif (is_array($exclude)) { - foreach ($exclude as $path) { - $finder->notPath(Util::getRelativePath($path, $directory)); - } - } else { - throw new OpenApiException('Unexpected $exclude value:' . gettype($exclude)); - } - } - - return $finder; - } - - /** - * Escapes the special characters "/" and "~". - * - * https://swagger.io/docs/specification/using-ref/ - * https://tools.ietf.org/html/rfc6901#page-3 - */ - public static function refEncode(string $raw): string - { - return str_replace('/', '~1', str_replace('~', '~0', $raw)); - } - - /** - * Converted the escaped characters "~1" and "~" back to "/" and "~". - * - * https://swagger.io/docs/specification/using-ref/ - * https://tools.ietf.org/html/rfc6901#page-3 - */ - public static function refDecode(string $encoded): string - { - return str_replace('~1', '/', str_replace('~0', '~', $encoded)); - } - - /** - * Shorten class name(s). - * - * @param array|object|string $classes Class(es) to shorten - * - * @return string|string[] One or more shortened class names - */ - public static function shorten($classes) - { - $short = []; - foreach ((array) $classes as $class) { - $short[] = '@' . str_replace([ - 'OpenApi\\Annotations\\', - 'OpenApi\\Attributes\\', - ], 'OA\\', $class); - } - - return is_array($classes) ? $short : array_pop($short); - } -} diff --git a/tests/Analysers/AttributeAnnotationFactoryTest.php b/tests/Analysers/AttributeAnnotationFactoryTest.php index 20ebec62b..808734f07 100644 --- a/tests/Analysers/AttributeAnnotationFactoryTest.php +++ b/tests/Analysers/AttributeAnnotationFactoryTest.php @@ -11,9 +11,6 @@ use OpenApi\Tests\Fixtures\InvalidPropertyAttribute; use OpenApi\Tests\OpenApiTestCase; -/** - * @requires PHP 8.1 - */ class AttributeAnnotationFactoryTest extends OpenApiTestCase { public function testReturnedAnnotationsCount(): void diff --git a/tests/Analysers/ReflectionAnalyserTest.php b/tests/Analysers/ReflectionAnalyserTest.php index 488320a15..a627dcc1c 100644 --- a/tests/Analysers/ReflectionAnalyserTest.php +++ b/tests/Analysers/ReflectionAnalyserTest.php @@ -79,18 +79,15 @@ public static function analysers(): iterable ]; } - /** - * @requires PHP 8.1 - */ public function testPhp8PromotedProperties(): void { $analysis = $this->analysisFromFixtures(['PHP/Php8PromotedProperties.php']); $schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true); $this->assertCount(1, $schemas); - $analysis->process($this->processors([CleanUnusedComponents::class])); + $this->processorPipeline(strip: [CleanUnusedComponents::class])->process($analysis); + ; - /** @var OA\Property[] $properties */ $properties = $analysis->getAnnotationsOfType(OA\Property::class); [$tags, $id, $labels] = $properties; diff --git a/tests/Analysers/TokenScannerTest.php b/tests/Analysers/TokenScannerTest.php index 58f5f06c1..8a961254c 100644 --- a/tests/Analysers/TokenScannerTest.php +++ b/tests/Analysers/TokenScannerTest.php @@ -8,28 +8,27 @@ use OpenApi\Analysers\TokenScanner; use OpenApi\Tests\OpenApiTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class TokenScannerTest extends OpenApiTestCase { public static function scanCases(): iterable { - if (\PHP_VERSION_ID >= 80100) { - yield 'abstract' => [ - 'PHP/AbstractKeyword.php', - [ - 'OpenApi\Tests\Fixtures\PHP\AbstractKeyword' => [ - 'uses' => [ - 'OAT' => 'OpenApi\Attributes', - ], - 'interfaces' => [], - 'traits' => [], - 'enums' => [], - 'methods' => ['stuff', 'other', 'another'], - 'properties' => [], + yield 'abstract' => [ + 'PHP/AbstractKeyword.php', + [ + 'OpenApi\Tests\Fixtures\PHP\AbstractKeyword' => [ + 'uses' => [ + 'OAT' => 'OpenApi\Attributes', ], + 'interfaces' => [], + 'traits' => [], + 'enums' => [], + 'methods' => ['stuff', 'other', 'another'], + 'properties' => [], ], - ]; - } + ], + ]; yield 'references' => [ 'PHP/References.php', @@ -355,44 +354,40 @@ public static function scanCases(): iterable ], ]; - if (\PHP_VERSION_ID >= 80100) { - yield 'enum' => [ - 'PHP/Enums/StatusEnum.php', - [ - 'OpenApi\\Tests\\Fixtures\\PHP\\Enums\\StatusEnum' => [ - 'uses' => [ - 'OAT' => 'OpenApi\\Attributes', - ], - 'interfaces' => [], - 'enums' => [], - 'traits' => [], - 'methods' => [], - 'properties' => [], + yield 'enum' => [ + 'PHP/Enums/StatusEnum.php', + [ + 'OpenApi\\Tests\\Fixtures\\PHP\\Enums\\StatusEnum' => [ + 'uses' => [ + 'OAT' => 'OpenApi\\Attributes', ], + 'interfaces' => [], + 'enums' => [], + 'traits' => [], + 'methods' => [], + 'properties' => [], ], - ]; + ], + ]; - yield 'enum-backed' => [ - 'PHP/Enums/StatusEnumBacked.php', - [ - 'OpenApi\\Tests\\Fixtures\\PHP\\Enums\\StatusEnumBacked' => [ - 'uses' => [ - 'OAT' => 'OpenApi\\Attributes', - ], - 'interfaces' => [], - 'enums' => [], - 'traits' => [], - 'methods' => [], - 'properties' => [], + yield 'enum-backed' => [ + 'PHP/Enums/StatusEnumBacked.php', + [ + 'OpenApi\\Tests\\Fixtures\\PHP\\Enums\\StatusEnumBacked' => [ + 'uses' => [ + 'OAT' => 'OpenApi\\Attributes', ], + 'interfaces' => [], + 'enums' => [], + 'traits' => [], + 'methods' => [], + 'properties' => [], ], - ]; - } + ], + ]; } - /** - * @dataProvider scanCases - */ + #[DataProvider('scanCases')] public function testScanFile(string $fixture, array $expected): void { $result = (new TokenScanner())->scanFile($this->fixture($fixture)); diff --git a/tests/Annotations/AbstractAnnotationTest.php b/tests/Annotations/AbstractAnnotationTest.php index e048368ea..106ff1cd0 100644 --- a/tests/Annotations/AbstractAnnotationTest.php +++ b/tests/Annotations/AbstractAnnotationTest.php @@ -7,8 +7,8 @@ namespace OpenApi\Tests\Annotations; use OpenApi\Annotations as OA; -use OpenApi\Generator; use OpenApi\Tests\OpenApiTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class AbstractAnnotationTest extends OpenApiTestCase { @@ -20,15 +20,6 @@ public function testVendorFields(): void $this->assertSame(123, $output->$prefixedProperty); } - /** - * @requires PHP < 8.2 - */ - public function testInvalidField(): void - { - $this->assertOpenApiLogEntryContains('Ignoring unexpected property "doesnot" for @OA\Get(), expecting'); - $this->annotationsFromDocBlockParser('@OA\Get(doesnot="exist")'); - } - public function testUmergedAnnotation(): void { $openapi = $this->createOpenApiWithInfo(); @@ -119,9 +110,7 @@ public static function nestedMatches(): iterable ]; } - /** - * @dataProvider nestedMatches - */ + #[DataProvider('nestedMatches')] public function testMatchNested(string $class, $expected): void { $this->assertEquals($expected, (new OA\Get([]))->matchNested(new $class([]))); @@ -129,11 +118,9 @@ public function testMatchNested(string $class, $expected): void public function testDuplicateOperationIdValidation(): void { - $analysis = $this->analysisFromFixtures(['DuplicateOperationId.php']); - (new Generator()) - ->setTypeResolver($this->getTypeResolver()) - ->getProcessorPipeline() - ->process($analysis); + $analysis = $this->analysisFromFixtures([ + 'DuplicateOperationId.php', + ], $this->processorPipeline()); $this->assertOpenApiLogEntryContains('operationId must be unique. Duplicate value found: "getItem"'); $this->assertFalse($analysis->validate()); @@ -146,16 +133,11 @@ public function testIsRoot(): void $this->assertTrue((new SubSchema([]))->isRoot(OA\Schema::class)); } - /** - * @requires PHP 8.1 - */ public function testValidateExamples(): void { - $analysis = $this->analysisFromFixtures(['BadExampleParameter.php']); - (new Generator()) - ->setTypeResolver($this->getTypeResolver()) - ->getProcessorPipeline() - ->process($analysis); + $analysis = $this->analysisFromFixtures([ + 'BadExampleParameter.php', + ], $this->processorPipeline()); $this->assertOpenApiLogEntryContains('Required @OA\PathItem() not found'); $this->assertOpenApiLogEntryContains('Required @OA\Info() not found'); diff --git a/tests/Annotations/AnnotationPropertiesDefinedTest.php b/tests/Annotations/AnnotationPropertiesDefinedTest.php index 08e9eca3f..a72a7a4d9 100644 --- a/tests/Annotations/AnnotationPropertiesDefinedTest.php +++ b/tests/Annotations/AnnotationPropertiesDefinedTest.php @@ -8,12 +8,11 @@ use OpenApi\Annotations as OA; use OpenApi\Tests\OpenApiTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class AnnotationPropertiesDefinedTest extends OpenApiTestCase { - /** - * @dataProvider allAnnotationClasses - */ + #[DataProvider('allAnnotationClasses')] public function testPropertiesAreNotUndefined(string $annotation): void { $properties = get_class_vars($annotation); diff --git a/tests/Annotations/AttributesSyncTest.php b/tests/Annotations/AttributesSyncTest.php index f725585f1..0421c2035 100644 --- a/tests/Annotations/AttributesSyncTest.php +++ b/tests/Annotations/AttributesSyncTest.php @@ -8,10 +8,8 @@ use OpenApi\Annotations as OA; use OpenApi\Tests\OpenApiTestCase; +use PHPUnit\Framework\Attributes\DataProvider; -/** - * @requires PHP 8.1 - */ class AttributesSyncTest extends OpenApiTestCase { public static $SCHEMA_EXCLUSIONS = ['const', 'multipleOf', 'not', 'additionalItems', 'contains', 'patternProperties', 'dependencies', 'propertyNames']; @@ -23,9 +21,7 @@ public function testCounts(): void $this->assertSameSize($this->allAnnotationClasses(), $this->allAttributeClasses()); } - /** - * @dataProvider allAnnotationClasses - */ + #[DataProvider('allAnnotationClasses')] public function testParameterCompleteness(string $annotation): void { $annotationRC = new \ReflectionClass($annotation); @@ -188,9 +184,7 @@ protected function parameterType(string $parameterName, \ReflectionParameter $pa return $var; } - /** - * @dataProvider allAttributeClasses - */ + #[DataProvider('allAttributeClasses')] public function testPropertyCompleteness(string $attribute): void { $attributeRC = new \ReflectionClass($attribute); diff --git a/tests/Annotations/ComponentsTest.php b/tests/Annotations/ComponentsTest.php index ae678d9ac..e0dfee28b 100644 --- a/tests/Annotations/ComponentsTest.php +++ b/tests/Annotations/ComponentsTest.php @@ -17,4 +17,14 @@ public function testRef(): void $this->assertEquals('#/components/schemas/bar', OA\Components::ref(new OA\Schema(['ref' => null, 'schema' => 'bar', '_context' => $this->getContext()]))); $this->assertEquals('#/components/examples/xx', OA\Components::ref(new OA\Examples(['example' => 'xx', '_context' => $this->getContext()]))); } + + public function testRefEncode(): void + { + $this->assertSame('#/paths/~1blogs~1{blog_id}~1new~0posts', '#/paths/' . OA\Components::refEncode('/blogs/{blog_id}/new~posts')); + } + + public function testRefDecode(): void + { + $this->assertSame('/blogs/{blog_id}/new~posts', OA\Components::refDecode('~1blogs~1{blog_id}~1new~0posts')); + } } diff --git a/tests/Annotations/ItemsTest.php b/tests/Annotations/ItemsTest.php index 9172cd474..ce3b0d79e 100644 --- a/tests/Annotations/ItemsTest.php +++ b/tests/Annotations/ItemsTest.php @@ -33,7 +33,9 @@ public function testParentTypeArray(): void public function testRefDefinitionInProperty(): void { - $analysis = $this->analysisFromFixtures(['UsingVar.php'], $this->processors([CleanUnusedComponents::class])); + $analysis = $this->analysisFromFixtures([ + 'UsingVar.php', + ], $this->processorPipeline(strip: [CleanUnusedComponents::class])); $this->assertCount(2, $analysis->openapi->components->schemas); $this->assertEquals('UsingVar', $analysis->openapi->components->schemas[0]->schema); diff --git a/tests/Annotations/NestedPropertyTest.php b/tests/Annotations/NestedPropertyTest.php index 21ad87f5c..695aad0ce 100644 --- a/tests/Annotations/NestedPropertyTest.php +++ b/tests/Annotations/NestedPropertyTest.php @@ -17,8 +17,9 @@ class NestedPropertyTest extends OpenApiTestCase { public function testNestedProperties(): void { - $analysis = $this->analysisFromFixtures(['NestedProperty.php']); - $analysis->process($this->initializeProcessors([ + $analysis = $this->analysisFromFixtures([ + 'NestedProperty.php', + ], $this->processorPipeline([ new MergeIntoOpenApi(), new MergeIntoComponents(), new AugmentSchemas(), diff --git a/tests/Annotations/OpenApiTest.php b/tests/Annotations/OpenApiTest.php index 27ecb74c3..4fd7e5130 100644 --- a/tests/Annotations/OpenApiTest.php +++ b/tests/Annotations/OpenApiTest.php @@ -8,6 +8,7 @@ use OpenApi\Annotations as OA; use OpenApi\Tests\OpenApiTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class OpenApiTest extends OpenApiTestCase { @@ -55,9 +56,7 @@ public static function versionMatchProvider(): iterable yield '3.0.3-3.1.x' => ['3.0.3', '3.1.x', false]; } - /** - * @dataProvider versionMatchProvider - */ + #[DataProvider('versionMatchProvider')] public function testVersionMatch(string $given, string $compare, bool $expected): void { $this->assertEquals($expected, OA\OpenApi::versionMatch($given, $compare)); diff --git a/tests/Annotations/OperationTest.php b/tests/Annotations/OperationTest.php index e5633aef1..6d7516374 100644 --- a/tests/Annotations/OperationTest.php +++ b/tests/Annotations/OperationTest.php @@ -8,6 +8,7 @@ use OpenApi\Annotations as OA; use OpenApi\Tests\OpenApiTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class OperationTest extends OpenApiTestCase { @@ -37,9 +38,7 @@ public static function securityData(): iterable ]; } - /** - * @dataProvider securityData - */ + #[DataProvider('securityData')] public function testSecuritySerialization(array $security, string $docBlock, string $expected): void { // test with Get implementation... diff --git a/tests/Annotations/ValidateRelationsTest.php b/tests/Annotations/ValidateRelationsTest.php index afe6418ea..73fe62c81 100644 --- a/tests/Annotations/ValidateRelationsTest.php +++ b/tests/Annotations/ValidateRelationsTest.php @@ -8,6 +8,7 @@ use OpenApi\Annotations as OA; use OpenApi\Tests\OpenApiTestCase; +use PHPUnit\Framework\Attributes\DataProvider; /** * Test if the annotation class nesting parent/child relations are coherent. @@ -15,10 +16,9 @@ class ValidateRelationsTest extends OpenApiTestCase { /** - * @dataProvider allAnnotationClasses - * * @param string $class */ + #[DataProvider('allAnnotationClasses')] public function testAncestors($class): void { foreach ($class::$_parents as $parent) { @@ -36,10 +36,9 @@ public function testAncestors($class): void } /** - * @dataProvider allAnnotationClasses - * * @param class-string $class */ + #[DataProvider('allAnnotationClasses')] public function testNested($class): void { foreach (array_keys($class::$_nested) as $nestedClass) { diff --git a/tests/CommandlineTest.php b/tests/CommandlineTest.php index 0b2289602..53c580a27 100644 --- a/tests/CommandlineTest.php +++ b/tests/CommandlineTest.php @@ -29,17 +29,17 @@ private function getCommandToExecute(string $cmd, ?string $devNullRedir = null): public function testStdout(): void { - $basePath = $this->examplePath('petstore'); + $basePath = static::examplePath('petstore'); $path = "$basePath/annotations"; exec($this->getCommandToExecute(__DIR__ . '/../bin/openapi --bootstrap ' . __DIR__ . '/cl_bootstrap.php --format yaml ' . escapeshellarg($path), '2>'), $output, $retval); $this->assertSame(0, $retval, implode(PHP_EOL, $output)); $yaml = implode(PHP_EOL, $output); - $this->assertSpecEquals(file_get_contents($this->getSpecFilename('petstore')), $yaml); + $this->assertSpecEquals(file_get_contents(static::getSpecFilename('petstore')), $yaml); } public function testOutputToFile(): void { - $basePath = $this->examplePath('petstore'); + $basePath = static::examplePath('petstore'); $path = "$basePath/annotations"; $filename = sys_get_temp_dir() . '/swagger-php-clitest.yaml'; exec($this->getCommandToExecute(__DIR__ . '/../bin/openapi --bootstrap ' . __DIR__ . '/cl_bootstrap.php --format yaml -o ' . escapeshellarg($filename) . ' ' . escapeshellarg($path), '2>'), $output, $retval); @@ -47,12 +47,12 @@ public function testOutputToFile(): void $this->assertCount(0, $output, 'No output to stdout'); $yaml = file_get_contents($filename); unlink($filename); - $this->assertSpecEquals(file_get_contents($this->getSpecFilename('petstore')), $yaml); + $this->assertSpecEquals(file_get_contents(static::getSpecFilename('petstore')), $yaml); } public function testAddProcessor(): void { - $basePath = $this->examplePath('petstore'); + $basePath = static::examplePath('petstore'); $path = "$basePath/annotations"; $cmd = __DIR__ . '/../bin/openapi --bootstrap ' . __DIR__ . '/cl_bootstrap.php --processor OperationId --format yaml ' . escapeshellarg($path); exec($this->getCommandToExecute($cmd, '2>'), $output, $retval); @@ -61,7 +61,7 @@ public function testAddProcessor(): void public function testExcludeListWarning(): void { - $basePath = $this->examplePath('petstore'); + $basePath = static::examplePath('petstore'); $path = "$basePath/annotations"; exec($this->getCommandToExecute(__DIR__ . '/../bin/openapi -e foo,bar ' . escapeshellarg($path) . ' 2>&1'), $output, $retval); $this->assertSame(1, $retval); @@ -71,7 +71,7 @@ public function testExcludeListWarning(): void public function testMissingArg(): void { - $basePath = $this->examplePath('petstore'); + $basePath = static::examplePath('petstore'); $path = "$basePath/annotations"; exec($this->getCommandToExecute(__DIR__ . '/../bin/openapi ' . escapeshellarg($path) . ' -e 2>&1'), $output, $retval); $this->assertSame(1, $retval); diff --git a/tests/Concerns/UsesExamples.php b/tests/Concerns/UsesExamples.php index bc86ff6df..6b842465f 100644 --- a/tests/Concerns/UsesExamples.php +++ b/tests/Concerns/UsesExamples.php @@ -16,14 +16,14 @@ public static function examplePath(string $name): string return sprintf('%s/docs/examples/specs/%s', dirname(__DIR__, 2), $name); } - public function getSpecFilename(string $name, string $implementation = 'annotations', string $version = OpenApi::VERSION_3_0_0): string + public static function getSpecFilename(string $name, string $implementation = 'annotations', string $version = OpenApi::VERSION_3_0_0): string { $specs = [ "$name-$version.yaml", "$name-$implementation-$version.yaml", ]; - $basePath = $this->examplePath($name); + $basePath = static::examplePath($name); foreach ($specs as $spec) { $specFilename = "$basePath/$spec"; if (file_exists($specFilename)) { @@ -39,7 +39,7 @@ public function registerExampleClassloader(string $name, string $implementation $packageName = str_replace(' ', '', ucwords(str_replace(['-', '.'], ' ', $name))); $packageName = str_replace(' ', '\\', ucwords(str_replace('/', ' ', $packageName))); - $basePath = $this->examplePath($name); + $basePath = static::examplePath($name); $path = "$basePath/$implementation"; $namerspaceBase = sprintf('OpenApi\\Examples\\Specs\\%s\\', $packageName); $implementationNamerspaceBase = sprintf("{$namerspaceBase}%s\\", ucfirst($implementation)); diff --git a/tests/DocSnippetsTest.php b/tests/DocSnippetsTest.php index 94d45c06b..0ea08733c 100644 --- a/tests/DocSnippetsTest.php +++ b/tests/DocSnippetsTest.php @@ -12,15 +12,13 @@ use OpenApi\Processors\OperationId; use OpenApi\Tests\Concerns\UsesExamples; use Symfony\Component\Finder\Finder; +use PHPUnit\Framework\Attributes\DataProvider; -/** - * @requires PHP 8.1 - */ class DocSnippetsTest extends OpenApiTestCase { use UsesExamples; - public function snippetSets(): iterable + public static function snippetSets(): iterable { $finder = (new Finder()) ->in(__DIR__ . '/../docs/snippets/') @@ -32,7 +30,7 @@ public function snippetSets(): iterable if (file_exists($other)) { $key = str_replace('_an', '', $file->getBasename('.php')); foreach ([OpenApi::VERSION_3_0_0, OpenApi::VERSION_3_1_0] as $version) { - yield "$key-$version" => [[$file->getPathname(), $other], $version]; + yield "$key-$version" => [['an' => $file->getPathname(), 'at' => $other], $version]; } } } @@ -40,18 +38,20 @@ public function snippetSets(): iterable } /** - * Compare at/an snippets and ensure they result in the same spec fragment. - * - * @dataProvider snippetSets + * Compare snippets and ensure they result in the same spec fragment. */ + #[DataProvider('snippetSets')] public function testSnippets(array $filenames, string $version): void { $lastSpec = null; - foreach ($filenames as $filename) { + foreach ($filenames as $type => $filename) { + $contents = preg_replace('/(namespace [^;]+);/', "\\1\\$type;", file_get_contents($filename)); $namespace = basename($filename, '.php'); + $tmp = sys_get_temp_dir() . "/$namespace.php"; - file_put_contents($tmp, "" . file_get_contents($filename)); + file_put_contents($tmp, $contents); require_once $tmp; + $openapi = (new Generator($this->getTrackingLogger())) ->setVersion($version) ->setTypeResolver($this->getTypeResolver()) diff --git a/tests/ExamplesTest.php b/tests/ExamplesTest.php index df3ee32ad..cd5228831 100644 --- a/tests/ExamplesTest.php +++ b/tests/ExamplesTest.php @@ -11,15 +11,13 @@ use OpenApi\Generator; use OpenApi\Serializer; use OpenApi\TypeResolverInterface; +use PHPUnit\Framework\Attributes\DataProvider; -/** - * @requires PHP 8.1 - */ class ExamplesTest extends OpenApiTestCase { use UsesExamples; - public function exampleSpecs(): iterable + public static function exampleSpecs(): iterable { $examples = [ 'api', @@ -39,12 +37,12 @@ public function exampleSpecs(): iterable foreach (static::getTypeResolvers() as $resolverName => $typeResolver) { foreach ($examples as $example) { foreach ($implementations as $implementation) { - if (!file_exists($this->examplePath($example) . '/' . $implementation)) { + if (!file_exists(static::examplePath($example) . '/' . $implementation)) { continue; } foreach ($versions as $version) { - if (!file_exists($this->getSpecFilename($example, $implementation, $version))) { + if (!file_exists(static::getSpecFilename($example, $implementation, $version))) { continue; } @@ -62,15 +60,14 @@ public function exampleSpecs(): iterable /** * Validate openapi definitions of the included examples. - * - * @dataProvider exampleSpecs */ + #[DataProvider('exampleSpecs')] public function testExample(TypeResolverInterface $typeResolver, string $name, string $implementation, string $version): void { $this->registerExampleClassloader($name, $implementation); - $path = $this->examplePath("$name/$implementation"); - $specFilename = $this->getSpecFilename($name, $implementation, $version); + $path = static::examplePath("$name/$implementation"); + $specFilename = static::getSpecFilename($name, $implementation, $version); $openapi = (new Generator($this->getTrackingLogger())) ->setVersion($version) @@ -84,12 +81,10 @@ public function testExample(TypeResolverInterface $typeResolver, string $name, s ); } - /** - * @dataProvider exampleSpecs - */ + #[DataProvider('exampleSpecs')] public function testSerializer(TypeResolverInterface $typeResolver, string $name, string $implementation, string $version): void { - $specFilename = $this->getSpecFilename($name, $implementation, $version); + $specFilename = static::getSpecFilename($name, $implementation, $version); $reserialized = (new Serializer())->deserializeFile($specFilename)->toYaml(); diff --git a/tests/Fixtures/PHP/DocblockAndTypehintTypes.php b/tests/Fixtures/PHP/DocblockAndTypehintTypes.php index 268cbbc46..921866abc 100644 --- a/tests/Fixtures/PHP/DocblockAndTypehintTypes.php +++ b/tests/Fixtures/PHP/DocblockAndTypehintTypes.php @@ -178,6 +178,21 @@ public function getString(): string )] public array $oneOfList; + /** + * @var null|DocblockAndTypehintTypes|array + */ + #[OAT\Property()] + public ?array $nullableTypedListUnion; + + /** + * @var null|DocblockAndTypehintTypes|array|array> + */ + #[OAT\Property()] + public ?array $nullableNestedTypedListUnion; + + #[\OpenApi\Attributes\Property(example: true)] + public int|bool|null $reflectionValue; + /** * @param \DateTimeImmutable[] $paramDateTimeList * @param string[] $paramStringList @@ -201,4 +216,7 @@ public function blah( ?array $blah_values, ) { } + + #[OAT\Property] + public FirstInterface&SecondInterface $intersectionVar; } diff --git a/tests/Fixtures/PHP/FirstInterface.php b/tests/Fixtures/PHP/FirstInterface.php new file mode 100644 index 000000000..e35c96884 --- /dev/null +++ b/tests/Fixtures/PHP/FirstInterface.php @@ -0,0 +1,14 @@ + [ - 'contentType' => 'application/json', - ], - ], - ), - ), - responses: [ - new OAT\Response( - response: 200, - description: 'All good', - ), - ] - )] - public function bcEncodingAttr() - { - - } - /** * @OA\Post( - * path="/endpoint/bc-encoding-annot", + * path="/multipart-form-data-annot", * @OA\RequestBody( * @OA\MediaType( * mediaType="multipart-form-data", * @OA\Schema( - * @OA\Property(property="encname") + * @OA\Property(property="encname"), + * @OA\Property( + * property="other", + * @OA\Encoding(contentType="application/xml") + * ) * ), - * encoding={ - * "encname": { - * "contentType": "application/json" - * } - * } + * @OA\Encoding(property="encname", contentType="application/json") * ) * ), * @OA\Response(response="200", description="OK") * ) */ - public function bcEncodingAnnot() + public function multipartFormDataAnnot() { } diff --git a/tests/Fixtures/Scratch/Encoding3.0.0.yaml b/tests/Fixtures/Scratch/Encoding3.0.0.yaml index 72d26d119..1679b7593 100644 --- a/tests/Fixtures/Scratch/Encoding3.0.0.yaml +++ b/tests/Fixtures/Scratch/Encoding3.0.0.yaml @@ -63,36 +63,22 @@ paths: responses: '200': description: 'All good' - /endpoint/bc-encoding-attr: + /multipart-form-data-annot: post: - operationId: 93c969c0f67dc55b9f7f88f6e46d4230 - requestBody: - content: - multipart/form-data: - schema: - properties: - encname: - type: string - type: object - encoding: - encname: - contentType: application/json - responses: - '200': - description: 'All good' - /endpoint/bc-encoding-annot: - post: - operationId: efe7afb9b99c111a0d781e14982abcdf + operationId: 8088dd671fb2ee88b83b2ea51c3458c2 requestBody: content: multipart-form-data: schema: properties: encname: { } + other: { } type: object encoding: encname: contentType: application/json + other: + contentType: application/xml responses: '200': description: OK diff --git a/tests/Fixtures/Scratch/Encoding3.1.0.yaml b/tests/Fixtures/Scratch/Encoding3.1.0.yaml index 3489b87a1..e312afe7a 100644 --- a/tests/Fixtures/Scratch/Encoding3.1.0.yaml +++ b/tests/Fixtures/Scratch/Encoding3.1.0.yaml @@ -63,36 +63,22 @@ paths: responses: '200': description: 'All good' - /endpoint/bc-encoding-attr: + /multipart-form-data-annot: post: - operationId: 93c969c0f67dc55b9f7f88f6e46d4230 - requestBody: - content: - multipart/form-data: - schema: - properties: - encname: - type: string - type: object - encoding: - encname: - contentType: application/json - responses: - '200': - description: 'All good' - /endpoint/bc-encoding-annot: - post: - operationId: efe7afb9b99c111a0d781e14982abcdf + operationId: 8088dd671fb2ee88b83b2ea51c3458c2 requestBody: content: multipart-form-data: schema: properties: encname: { } + other: { } type: object encoding: encname: contentType: application/json + other: + contentType: application/xml responses: '200': description: OK diff --git a/tests/Fixtures/Scratch/MultiTypeProperty3.0.0.yaml b/tests/Fixtures/Scratch/MultiTypeProperty3.0.0-legacy.yaml similarity index 100% rename from tests/Fixtures/Scratch/MultiTypeProperty3.0.0.yaml rename to tests/Fixtures/Scratch/MultiTypeProperty3.0.0-legacy.yaml diff --git a/tests/Fixtures/Scratch/MultiTypeProperty3.0.0-type-info.yaml b/tests/Fixtures/Scratch/MultiTypeProperty3.0.0-type-info.yaml new file mode 100644 index 000000000..6bc2078ef --- /dev/null +++ b/tests/Fixtures/Scratch/MultiTypeProperty3.0.0-type-info.yaml @@ -0,0 +1,38 @@ +openapi: 3.0.0 +info: + title: 'Multi Typed Property Scratch' + version: '1.0' +paths: + /api/endpoint: + get: + description: 'An endpoint' + operationId: 18b884f3d600e07547af2c8e4f1258ea + responses: + '200': + description: OK +components: + schemas: + MultiTypeProperty: + properties: + otherValue: + example: 'My value' + oneOf: + - + type: string + - + type: array + items: + type: string + value: + example: true + nullable: true + mixedUnion: + example: 'My value' + oneOf: + - + type: string + - + type: array + items: + type: string + type: object diff --git a/tests/Fixtures/Scratch/MultiTypeProperty3.1.0.yaml b/tests/Fixtures/Scratch/MultiTypeProperty3.1.0-legacy.yaml similarity index 100% rename from tests/Fixtures/Scratch/MultiTypeProperty3.1.0.yaml rename to tests/Fixtures/Scratch/MultiTypeProperty3.1.0-legacy.yaml diff --git a/tests/Fixtures/Scratch/MultiTypeProperty3.1.0-type-info.yaml b/tests/Fixtures/Scratch/MultiTypeProperty3.1.0-type-info.yaml new file mode 100644 index 000000000..9718ca368 --- /dev/null +++ b/tests/Fixtures/Scratch/MultiTypeProperty3.1.0-type-info.yaml @@ -0,0 +1,43 @@ +openapi: 3.1.0 +info: + title: 'Multi Typed Property Scratch' + version: '1.0' +paths: + /api/endpoint: + get: + description: 'An endpoint' + operationId: 18b884f3d600e07547af2c8e4f1258ea + responses: + '200': + description: OK +components: + schemas: + MultiTypeProperty: + properties: + otherValue: + example: 'My value' + oneOf: + - + type: + - string + - + type: array + items: + type: string + value: + type: + - boolean + - integer + - 'null' + example: true + mixedUnion: + example: 'My value' + oneOf: + - + type: + - string + - + type: array + items: + type: string + type: object diff --git a/tests/GeneratorTest.php b/tests/GeneratorTest.php index 65507c595..08f85a673 100644 --- a/tests/GeneratorTest.php +++ b/tests/GeneratorTest.php @@ -8,8 +8,9 @@ use OpenApi\Generator; use OpenApi\Processors\OperationId; +use OpenApi\SourceFinder; use OpenApi\Tests\Concerns\UsesExamples; -use OpenApi\Util; +use PHPUnit\Framework\Attributes\DataProvider; class GeneratorTest extends OpenApiTestCase { @@ -21,13 +22,11 @@ public static function sourcesProvider(): iterable $sourceDir = static::examplePath("$name/annotations"); yield 'dir-list' => [$name, [$sourceDir]]; - yield 'finder' => [$name, Util::finder($sourceDir)]; - yield 'finder-list' => [$name, [Util::finder($sourceDir)]]; + yield 'finder' => [$name, new SourceFinder($sourceDir)]; + yield 'finder-list' => [$name, [new SourceFinder($sourceDir)]]; } - /** - * @dataProvider sourcesProvider - */ + #[DataProvider('sourcesProvider')] public function testGenerate(string $name, iterable $sources): void { $this->registerExampleClassloader($name); @@ -37,24 +36,7 @@ public function testGenerate(string $name, iterable $sources): void ->setTypeResolver($this->getTypeResolver()) ->generate($sources); - $this->assertSpecEquals(file_get_contents($this->getSpecFilename($name)), $openapi); - } - - /** - * @dataProvider sourcesProvider - */ - public function testScan(string $name, iterable $sources): void - { - $this->registerExampleClassloader($name); - - $analyzer = $this->getAnalyzer(); - $processor = (new Generator()) - ->setTypeResolver($this->getTypeResolver()) - ->getProcessorPipeline(); - - $openapi = Generator::scan($sources, ['processor' => $processor, 'analyser' => $analyzer]); - - $this->assertSpecEquals(file_get_contents($this->getSpecFilename($name)), $openapi); + $this->assertSpecEquals(file_get_contents(static::getSpecFilename($name)), $openapi); } public function testScanInvalidSource(): void @@ -113,9 +95,7 @@ public static function configCases(): iterable ]; } - /** - * @dataProvider configCases - */ + #[DataProvider('configCases')] public function testConfig(array $config, bool $expected): void { $generator = new Generator(); diff --git a/tests/OpenApiTestCase.php b/tests/OpenApiTestCase.php index 62872a39b..509049bd9 100644 --- a/tests/OpenApiTestCase.php +++ b/tests/OpenApiTestCase.php @@ -247,22 +247,20 @@ public static function fixtures(array $files): array }, $files); } - public static function processors(array $strip = []): array + public function processorPipeline(?array $processors = null, array $strip = []): Pipeline { - $processors = []; + $generator = (new Generator()) + ->setTypeResolver($this->getTypeResolver()); - (new Generator()) - ->getProcessorPipeline() - ->walk(function ($processor) use (&$processors, $strip) { - if (!is_object($processor) || !in_array(get_class($processor), $strip)) { - $processors[] = $processor; - } - }); + if ($processors) { + $generator->setProcessorPipeline(new Pipeline($processors)); + } - return $processors; + return $generator->getProcessorPipeline() + ->remove(fn ($processor) => is_object($processor) && in_array(get_class($processor), $strip)); } - public function analysisFromFixtures(array $files, array $processors = [], ?AnalyserInterface $analyzer = null, array $config = []): Analysis + public function analysisFromFixtures(array $files, ?Pipeline $pipeline = null, ?AnalyserInterface $analyzer = null, array $config = []): Analysis { $analysis = new Analysis([], $this->getContext()); @@ -270,7 +268,7 @@ public function analysisFromFixtures(array $files, array $processors = [], ?Anal ->setConfig($config) ->setAnalyser($analyzer ?: $this->getAnalyzer()) ->setTypeResolver($this->getTypeResolver()) - ->setProcessorPipeline(new Pipeline($processors)) + ->setProcessorPipeline($pipeline ?? new Pipeline()) ->generate($this->fixtures($files), $analysis, false); return $analysis; diff --git a/tests/Processors/AugmentParametersTest.php b/tests/Processors/AugmentParametersTest.php index 142c4ab33..e8d9072d3 100644 --- a/tests/Processors/AugmentParametersTest.php +++ b/tests/Processors/AugmentParametersTest.php @@ -16,6 +16,7 @@ use OpenApi\Processors\MergeIntoComponents; use OpenApi\Processors\MergeIntoOpenApi; use OpenApi\Tests\OpenApiTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class AugmentParametersTest extends OpenApiTestCase { @@ -54,9 +55,7 @@ public static function tagCases(): iterable ]; } - /** - * @dataProvider tagCases - */ + #[DataProvider('tagCases')] public function testParseTags(string $params, array $expected): void { $mixed = $this->getContext(['comment' => "/**\n$params\n *"]); @@ -65,13 +64,11 @@ public function testParseTags(string $params, array $expected): void $this->assertEquals($expected, $tags); } - /** - * @requires PHP 8.1 - */ public function testParameterNativeType(): void { - $analysis = $this->analysisFromFixtures(['RequestUsingAttribute.php']); - $analysis->process($this->initializeProcessors([ + $analysis = $this->analysisFromFixtures([ + 'RequestUsingAttribute.php', + ], $this->processorPipeline([ new MergeIntoOpenApi(), new MergeIntoComponents(), new BuildPaths(), diff --git a/tests/Processors/AugmentPropertiesTest.php b/tests/Processors/AugmentPropertiesTest.php index de796eb68..b5271588b 100644 --- a/tests/Processors/AugmentPropertiesTest.php +++ b/tests/Processors/AugmentPropertiesTest.php @@ -6,7 +6,6 @@ namespace OpenApi\Tests\Processors; -use OpenApi\Analysers\ReflectionAnalyser; use OpenApi\Annotations as OA; use OpenApi\Generator; use OpenApi\Processors\AugmentProperties; @@ -14,20 +13,20 @@ use OpenApi\Processors\MergeIntoComponents; use OpenApi\Processors\MergeIntoOpenApi; use OpenApi\Tests\OpenApiTestCase; +use PHPUnit\Framework\Attributes\Group; -/** - * @group Properties - */ +#[Group('Properties')] class AugmentPropertiesTest extends OpenApiTestCase { public function testAugmentProperties(): void { - $analysis = $this->analysisFromFixtures(['Customer.php']); - $analysis->process([ + $analysis = $this->analysisFromFixtures([ + 'Customer.php', + ], $this->processorPipeline([ new MergeIntoOpenApi(), new MergeIntoComponents(), new AugmentSchemas(), - ]); + ])); $customer = $analysis->openapi->components->schemas[0]; @@ -72,7 +71,7 @@ public function testAugmentProperties(): void $this->assertSame(Generator::UNDEFINED, $endorsedFriends->nullable); $this->assertSame(Generator::UNDEFINED, $endorsedFriends->allOf); - $analysis->process($this->initializeProcessors([new AugmentProperties()])); + $this->processorPipeline([new AugmentProperties()])->process($analysis); $expectedValues = [ 'property' => 'firstname', @@ -150,16 +149,14 @@ public function testAugmentProperties(): void public function testTypedProperties(): void { - if ($this->getAnalyzer() instanceof ReflectionAnalyser && PHP_VERSION_ID < 70400) { - $this->markTestSkipped(); - } - - $analysis = $this->analysisFromFixtures(['TypedProperties.php']); - $analysis->process([ + $analysis = $this->analysisFromFixtures([ + 'TypedProperties.php', + ], $this->processorPipeline([ new MergeIntoOpenApi(), new MergeIntoComponents(), new AugmentSchemas(), - ]); + ])); + [ $stringType, $intType, @@ -259,7 +256,7 @@ public function testTypedProperties(): void 'type' => Generator::UNDEFINED, ]); - $analysis->process($this->initializeProcessors([new AugmentProperties()])); + $this->processorPipeline([new AugmentProperties()])->process($analysis); $this->assertName($stringType, [ 'property' => 'stringType', @@ -356,7 +353,7 @@ public function testTypedProperties(): void protected function assertName(OA\Property $property, array $expectedValues): void { foreach ($expectedValues as $key => $val) { - $this->assertSame($val, $property->$key, '@OA\Property()->' . $key); + $this->assertSame($val, $property->$key, '@OA\Property()->property based on propertyname'); } } } diff --git a/tests/Processors/AugmentRefsTest.php b/tests/Processors/AugmentRefsTest.php index bd5ace904..420d20c9c 100644 --- a/tests/Processors/AugmentRefsTest.php +++ b/tests/Processors/AugmentRefsTest.php @@ -20,21 +20,22 @@ class AugmentRefsTest extends OpenApiTestCase public function testAugmentRefsForRequestBody(): void { - $analysis = $this->analysisFromFixtures(['Request.php']); - $analysis->process([ + $analysis = $this->analysisFromFixtures([ + 'Request.php', + ], $this->processorPipeline([ // create openapi->components new MergeIntoOpenApi(), // Merge standalone Scheme's into openapi->components new MergeIntoComponents(), new BuildPaths(), new AugmentRequestBody(), - ]); + ])); $this->assertSame($analysis->openapi->paths[0]->post->requestBody->ref, 'OpenApi\Tests\Fixtures\Request'); - $analysis->process([ + $this->processorPipeline([ new AugmentRefs(), - ]); + ])->process($analysis); $this->assertSame($analysis->openapi->paths[0]->post->requestBody->ref, '#/components/requestBodies/Request'); } diff --git a/tests/Processors/AugmentRequestBodyTest.php b/tests/Processors/AugmentRequestBodyTest.php index a659c9bdd..5c479a2e2 100644 --- a/tests/Processors/AugmentRequestBodyTest.php +++ b/tests/Processors/AugmentRequestBodyTest.php @@ -16,18 +16,20 @@ class AugmentRequestBodyTest extends OpenApiTestCase { public function testAugmentSchemas(): void { - $analysis = $this->analysisFromFixtures(['Request.php']); - $analysis->process([ + $analysis = $this->analysisFromFixtures([ + 'Request.php', + ], $this->processorPipeline([ // create openapi->components new MergeIntoOpenApi(), // Merge standalone Scheme's into openapi->components new MergeIntoComponents(), - ]); + ])); $this->assertCount(1, $analysis->openapi->components->requestBodies); $request = $analysis->openapi->components->requestBodies[0]; $this->assertSame(Generator::UNDEFINED, $request->request, 'Sanity check. No request was defined'); - $analysis->process([new AugmentRequestBody()]); + + $this->processorPipeline([new AugmentRequestBody()])->process($analysis); $this->assertSame('Request', $request->request, '@OA\RequestBody()->request based on classname'); } diff --git a/tests/Processors/AugmentSchemasTest.php b/tests/Processors/AugmentSchemasTest.php index 12ac87609..8cd3d168c 100644 --- a/tests/Processors/AugmentSchemasTest.php +++ b/tests/Processors/AugmentSchemasTest.php @@ -16,19 +16,21 @@ class AugmentSchemasTest extends OpenApiTestCase { public function testAugmentSchemas(): void { - $analysis = $this->analysisFromFixtures(['Customer.php']); - $analysis->process([ + $analysis = $this->analysisFromFixtures([ + 'Customer.php', + ], $this->processorPipeline([ // create openapi->components new MergeIntoOpenApi(), // Merge standalone Scheme's into openapi->components new MergeIntoComponents(), - ]); + ])); $this->assertCount(1, $analysis->openapi->components->schemas); $customer = $analysis->openapi->components->schemas[0]; $this->assertSame(Generator::UNDEFINED, $customer->schema, 'Sanity check. No scheme was defined'); $this->assertSame(Generator::UNDEFINED, $customer->properties, 'Sanity check. @OA\Property\'s not yet merged '); - $analysis->process([new AugmentSchemas()]); + + $this->processorPipeline([new AugmentSchemas()])->process($analysis); $this->assertSame('Customer', $customer->schema, '@OA\Schema()->schema based on classname'); $this->assertIsArray($customer->properties); @@ -37,18 +39,20 @@ public function testAugmentSchemas(): void public function testAugmentSchemasForInterface(): void { - $analysis = $this->analysisFromFixtures(['CustomerInterface.php']); - $analysis->process([ + $analysis = $this->analysisFromFixtures([ + 'CustomerInterface.php', + ], $this->processorPipeline([ // create openapi->components new MergeIntoOpenApi(), // Merge standalone Scheme's into openapi->components new MergeIntoComponents(), - ]); + ])); $this->assertCount(1, $analysis->openapi->components->schemas); $customer = $analysis->openapi->components->schemas[0]; $this->assertSame(Generator::UNDEFINED, $customer->properties, 'Sanity check. @OA\Property\'s not yet merged '); - $analysis->process([new AugmentSchemas()]); + + $this->processorPipeline([new AugmentSchemas()])->process($analysis); $this->assertIsArray($customer->properties); $this->assertCount(9, (array) $customer->properties, '@OA\Property()s are merged into the @OA\Schema of the class'); diff --git a/tests/Processors/AugmentTagsTest.php b/tests/Processors/AugmentTagsTest.php index 43c90bfb3..92951ee03 100644 --- a/tests/Processors/AugmentTagsTest.php +++ b/tests/Processors/AugmentTagsTest.php @@ -10,59 +10,48 @@ class AugmentTagsTest extends OpenApiTestCase { - /** - * @requires PHP 8.1 - */ public function testFilteredAugmentTags(): void { $config = [ 'pathFilter' => ['paths' => ['#^/hello/#']], 'cleanUnusedComponents' => ['enabled' => true], ]; - $analysis = $this->analysisFromFixtures(['SurplusTag.php'], static::processors(), null, $config); + $analysis = $this->analysisFromFixtures( + ['SurplusTag.php'], + $this->processorPipeline(), + config: $config + ); $this->assertCount(1, $analysis->openapi->tags); } - /** - * @requires PHP 8.1 - */ public function testDedupedAugmentTags(): void { - $analysis = $this->analysisFromFixtures(['SurplusTag.php'], static::processors()); + $analysis = $this->analysisFromFixtures( + ['SurplusTag.php'], + $this->processorPipeline() + ); $this->assertCount(3, $analysis->openapi->tags, 'Expecting 3 unique tags'); } - /** - * @requires PHP 8.1 - */ public function testAllowUnusedTags(): void { $analysis = $this->analysisFromFixtures( ['UnusedTags.php'], - static::processors(), - null, - [ - 'augmentTags' => ['whitelist' => ['fancy']], - ] + $this->processorPipeline(), + config: ['augmentTags' => ['whitelist' => ['fancy']]] ); $this->assertCount(2, $analysis->openapi->tags, 'Expecting fancy tag to be preserved'); } - /** - * @requires PHP 8.1 - */ public function testAllowUnusedTagsWildcard(): void { $analysis = $this->analysisFromFixtures( ['UnusedTags.php'], - static::processors(), - null, - [ - 'augmentTags' => ['whitelist' => ['*']], - ] + $this->processorPipeline(), + config: ['augmentTags' => ['whitelist' => ['*']]] ); $this->assertCount(3, $analysis->openapi->tags, 'Expecting all tags to be preserved'); diff --git a/tests/Processors/BuildPathsTest.php b/tests/Processors/BuildPathsTest.php index 34cb5bf6d..90a2fa9c3 100644 --- a/tests/Processors/BuildPathsTest.php +++ b/tests/Processors/BuildPathsTest.php @@ -24,7 +24,7 @@ public function testMergePathsWithSamePath(): void ]; $analysis = new Analysis([$openapi], $this->getContext()); $analysis->openapi = $openapi; - $analysis->process([new BuildPaths()]); + $this->processorPipeline([new BuildPaths()])->process($analysis); $this->assertCount(1, $openapi->paths); $this->assertSame('/comments', $openapi->paths[0]->path); @@ -41,10 +41,11 @@ public function testMergeOperationsWithSamePath(): void ], $this->getContext() ); - $analysis->process([ + $this->processorPipeline([ new MergeIntoOpenApi(), new BuildPaths(), - ]); + ])->process($analysis); + $this->assertCount(1, $openapi->paths); $path = $openapi->paths[0]; $this->assertSame('/comments', $path->path); diff --git a/tests/Processors/CleanUnmergedTest.php b/tests/Processors/CleanUnmergedTest.php index 21dec86f1..fc3f3934a 100644 --- a/tests/Processors/CleanUnmergedTest.php +++ b/tests/Processors/CleanUnmergedTest.php @@ -32,7 +32,7 @@ public function testCleanUnmergedProcessor(): void END; $analysis = new Analysis($this->annotationsFromDocBlockParser($comment), $this->getContext()); $this->assertCount(4, $analysis->annotations); - $analysis->process([new MergeIntoOpenApi()]); + $this->processorPipeline([new MergeIntoOpenApi()])->process($analysis); $this->assertCount(5, $analysis->annotations); $before = $analysis->split(); @@ -42,7 +42,7 @@ public function testCleanUnmergedProcessor(): void $analysis->validate(); // Validation fails to detect the unmerged annotations. // CleanUnmerged should place the unmerged annotions into the swagger->_unmerged array. - $analysis->process([new CleanUnmerged()]); + $this->processorPipeline([new CleanUnmerged()])->process($analysis); $between = $analysis->split(); $this->assertCount(3, $between->merged->annotations, 'Generated @OA\OpenApi, @OA\PathItem and @OA\Info'); @@ -54,11 +54,11 @@ public function testCleanUnmergedProcessor(): void // When a processor places a previously unmerged annotation into the swagger obect. $license = $analysis->getAnnotationsOfType(OA\License::class)[0]; - /** @var OA\Contact $contact */ $contact = $analysis->getAnnotationsOfType(OA\Contact::class)[0]; $analysis->openapi->info->contact = $contact; $this->assertCount(1, $license->_unmerged); - $analysis->process([new CleanUnmerged()]); + + $this->processorPipeline([new CleanUnmerged()])->process($analysis); $this->assertCount(0, $license->_unmerged); $after = $analysis->split(); diff --git a/tests/Processors/CleanUnusedComponentsTest.php b/tests/Processors/CleanUnusedComponentsTest.php index ab3caa6c1..48859dd38 100644 --- a/tests/Processors/CleanUnusedComponentsTest.php +++ b/tests/Processors/CleanUnusedComponentsTest.php @@ -8,6 +8,7 @@ use OpenApi\Generator; use OpenApi\Tests\OpenApiTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class CleanUnusedComponentsTest extends OpenApiTestCase { @@ -23,12 +24,14 @@ public static function countCases(): iterable ]; } - /** - * @dataProvider countCases - */ + #[DataProvider('countCases')] public function testCounts(array $config, string $fixture, int $expectedSchemaCount, int $expectedAnnotationCount): void { - $analysis = $this->analysisFromFixtures([$fixture], static::processors(), null, $config); + $analysis = $this->analysisFromFixtures( + [$fixture], + $this->processorPipeline(), + config: $config + ); if ($expectedSchemaCount === 0) { $this->assertTrue(Generator::isDefault($analysis->openapi->components->schemas)); diff --git a/tests/Processors/DocBlockDescriptionsTest.php b/tests/Processors/DocBlockDescriptionsTest.php index bed4abd97..06625907b 100644 --- a/tests/Processors/DocBlockDescriptionsTest.php +++ b/tests/Processors/DocBlockDescriptionsTest.php @@ -18,11 +18,13 @@ class DocBlockDescriptionsTest extends OpenApiTestCase public function testDocBlockDescription(): void { - $analysis = $this->analysisFromFixtures(['UsingPhpDoc.php']); - $analysis->process([ - new DocBlockDescriptions(), - ]); - /** @var OA\Operation[] $operations */ + $analysis = $this->analysisFromFixtures( + ['UsingPhpDoc.php'], + $this->processorPipeline([ + new DocBlockDescriptions(), + ]) + ); + $operations = $analysis->getAnnotationsOfType(OA\Operation::class); $this->assertSame('api/test1', $operations[0]->path); diff --git a/tests/Processors/ExpandClassesTest.php b/tests/Processors/ExpandClassesTest.php index 87b25b990..0b03550b7 100644 --- a/tests/Processors/ExpandClassesTest.php +++ b/tests/Processors/ExpandClassesTest.php @@ -32,35 +32,31 @@ protected function validate(Analysis $analysis): void public function testExpandClasses(): void { - $analysis = $this->analysisFromFixtures( - [ + $analysis = $this->analysisFromFixtures([ 'AnotherNamespace/Child.php', 'ExpandClasses/GrandAncestor.php', 'ExpandClasses/Ancestor.php', - ] - ); - $analysis->process($this->initializeProcessors([ - new MergeIntoOpenApi(), - new MergeIntoComponents(), - new ExpandInterfaces(), - new ExpandTraits(), - new AugmentSchemas(), - new AugmentProperties(), - new BuildPaths(), - ])); + ], $this->processorPipeline([ + new MergeIntoOpenApi(), + new MergeIntoComponents(), + new ExpandInterfaces(), + new ExpandTraits(), + new AugmentSchemas(), + new AugmentProperties(), + new BuildPaths(), + ])); $this->validate($analysis); - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class); $this->assertCount(4, $schemas); $childSchema = $schemas[0]; $this->assertSame('Child', $childSchema->schema); $this->assertCount(1, $childSchema->properties); - $analysis->process([ + $this->processorPipeline([ new ExpandClasses(), new CleanUnmerged(), - ]); + ])->process($analysis); $this->validate($analysis); $this->assertCount(3, $childSchema->properties); @@ -78,18 +74,19 @@ public function testExpandClassesWithoutDocBlocks(): void // this one doesn't 'ExpandClasses/AncestorWithoutDocBlocks.php', ]); - $analysis->process($this->processors([CleanUnusedComponents::class])); + $this->processorPipeline(strip: [CleanUnusedComponents::class])->process($analysis); $this->validate($analysis); - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class); $this->assertCount(2, $schemas); + $childSchema = $schemas[0]; $this->assertSame('ChildWithDocBlocks', $childSchema->schema); $this->assertCount(1, $childSchema->properties); // no error occurs - $analysis->process([new ExpandClasses()]); + $this->processorPipeline([new ExpandClasses()])->process($analysis); + $this->assertCount(1, $childSchema->properties); } @@ -103,10 +100,9 @@ public function testExpandClassesWithAllOf(): void 'ExpandClasses/Extended.php', 'ExpandClasses/Base.php', ]); - $analysis->process($this->processors([CleanUnusedComponents::class])); + $this->processorPipeline(strip: [CleanUnusedComponents::class])->process($analysis); $this->validate($analysis); - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true); $this->assertCount(4, $schemas); @@ -131,10 +127,9 @@ public function testExpandClassesWithOutAllOf(): void 'ExpandClasses/ExtendedWithoutAllOf.php', 'ExpandClasses/Base.php', ]); - $analysis->process($this->processors([CleanUnusedComponents::class])); + $this->processorPipeline(strip: [CleanUnusedComponents::class])->process($analysis); $this->validate($analysis); - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true); $this->assertCount(4, $schemas); @@ -158,10 +153,9 @@ public function testExpandClassesWithTwoChildSchemas(): void 'ExpandClasses/ExtendedWithTwoSchemas.php', 'ExpandClasses/Base.php', ]); - $analysis->process($this->processors([CleanUnusedComponents::class])); + $this->processorPipeline(strip: [CleanUnusedComponents::class])->process($analysis); $this->validate($analysis); - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true); $this->assertCount(7, $schemas); @@ -191,14 +185,13 @@ public function testPreserveExistingAllOf(): void 'ExpandClasses/BaseThatImplements.php', 'ExpandClasses/TraitUsedByExtendsBaseThatImplements.php', ]); - $analysis->process($this->processors([CleanUnusedComponents::class])); + $this->processorPipeline(strip: [CleanUnusedComponents::class])->process($analysis); $this->validate($analysis); $analysis->openapi->info = new OA\Info(['title' => 'test', 'version' => '1.0.0', '_context' => $this->getContext()]); $analysis->openapi->paths = [new OA\PathItem(['path' => '/test', '_context' => $this->getContext()])]; $analysis->validate(); - /** @var OA\Schema[] $schemas */ $schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true); $this->assertCount(9, $schemas); diff --git a/tests/Processors/ExpandEnumsTest.php b/tests/Processors/ExpandEnumsTest.php index effacdfee..b8658596a 100644 --- a/tests/Processors/ExpandEnumsTest.php +++ b/tests/Processors/ExpandEnumsTest.php @@ -15,17 +15,18 @@ use OpenApi\Tests\Fixtures\PHP\Enums\StatusEnumStringBacked; use OpenApi\Tests\Fixtures\PHP\Enums\TypeEnumStringBacked; use OpenApi\Tests\OpenApiTestCase; +use PHPUnit\Framework\Attributes\DataProvider; -/** - * @requires PHP 8.1 - */ class ExpandEnumsTest extends OpenApiTestCase { public function testExpandUnitEnum(): void { - $analysis = $this->analysisFromFixtures(['PHP/Enums/StatusEnum.php']); - $analysis->process($this->initializeProcessors([new ExpandEnums()])); - $schema = $analysis->getSchemaForSource(StatusEnum::class); + $analysis = $this->analysisFromFixtures( + ['PHP/Enums/StatusEnum.php'], + $this->processorPipeline([new ExpandEnums()]), + ); + + $schema = $analysis->getAnnotationForSource(StatusEnum::class); $this->assertEquals(['DRAFT', 'PUBLISHED', 'ARCHIVED'], $schema->enum); $this->assertEquals('string', $schema->type); @@ -33,9 +34,12 @@ public function testExpandUnitEnum(): void public function testExpandBackedEnum(): void { - $analysis = $this->analysisFromFixtures(['PHP/Enums/StatusEnumBacked.php']); - $analysis->process($this->initializeProcessors([new ExpandEnums()])); - $schema = $analysis->getSchemaForSource(StatusEnumBacked::class); + $analysis = $this->analysisFromFixtures( + ['PHP/Enums/StatusEnumBacked.php'], + $this->processorPipeline([new ExpandEnums()]), + ); + + $schema = $analysis->getAnnotationForSource(StatusEnumBacked::class); $this->assertEquals(['DRAFT', 'PUBLISHED', 'ARCHIVED'], $schema->enum); $this->assertEquals('string', $schema->type); @@ -43,9 +47,12 @@ public function testExpandBackedEnum(): void public function testExpandBackedIntegerEnum(): void { - $analysis = $this->analysisFromFixtures(['PHP/Enums/StatusEnumIntegerBacked.php']); - $analysis->process($this->initializeProcessors([new ExpandEnums()])); - $schema = $analysis->getSchemaForSource(StatusEnumIntegerBacked::class); + $analysis = $this->analysisFromFixtures( + ['PHP/Enums/StatusEnumIntegerBacked.php'], + $this->processorPipeline([new ExpandEnums()]), + ); + + $schema = $analysis->getAnnotationForSource(StatusEnumIntegerBacked::class); $this->assertEquals([1, 2, 3], $schema->enum); $this->assertEquals('integer', $schema->type); @@ -53,9 +60,12 @@ public function testExpandBackedIntegerEnum(): void public function testExpandBackedStringEnum(): void { - $analysis = $this->analysisFromFixtures(['PHP/Enums/StatusEnumStringBacked.php']); - $analysis->process($this->initializeProcessors([new ExpandEnums()])); - $schema = $analysis->getSchemaForSource(StatusEnumStringBacked::class); + $analysis = $this->analysisFromFixtures( + ['PHP/Enums/StatusEnumStringBacked.php'], + $this->processorPipeline([new ExpandEnums()]), + ); + + $schema = $analysis->getAnnotationForSource(StatusEnumStringBacked::class); $this->assertEquals(['draft', 'published', 'archived'], $schema->enum); $this->assertEquals('string', $schema->type); @@ -63,9 +73,12 @@ public function testExpandBackedStringEnum(): void public function testEnumNamesInBackedStringEnum(): void { - $analysis = $this->analysisFromFixtures(['PHP/Enums/StatusEnumStringBacked.php']); - $analysis->process($this->initializeProcessors([new ExpandEnums('enumNames')])); - $schema = $analysis->getSchemaForSource(StatusEnumStringBacked::class); + $analysis = $this->analysisFromFixtures( + ['PHP/Enums/StatusEnumStringBacked.php'], + $this->processorPipeline([new ExpandEnums('enumNames')]), + ); + + $schema = $analysis->getAnnotationForSource(StatusEnumStringBacked::class); $this->assertEquals(['DRAFT', 'PUBLISHED', 'ARCHIVED'], $schema->x['enumNames']); } @@ -152,15 +165,14 @@ public static function expandEnumClassStringFixtures(): iterable ]; } - /** - * @requires PHP 8.1 - * - * @dataProvider expandEnumClassStringFixtures - */ + #[DataProvider('expandEnumClassStringFixtures')] public function testExpandEnumClassString(array $files, string $title, mixed $expected): void { - $analysis = $this->analysisFromFixtures($files); - $analysis->process($this->initializeProcessors([new ExpandEnums()])); + $analysis = $this->analysisFromFixtures( + $files, + $this->processorPipeline([new ExpandEnums()]), + ); + $schemas = $analysis->getAnnotationsOfType([OA\Property::class, OA\Items::class], true); foreach ($schemas as $schema) { diff --git a/tests/Processors/MergeIntoComponentsTest.php b/tests/Processors/MergeIntoComponentsTest.php index 4d7292358..b87048345 100644 --- a/tests/Processors/MergeIntoComponentsTest.php +++ b/tests/Processors/MergeIntoComponentsTest.php @@ -26,7 +26,8 @@ public function testProcessor(): void $this->getContext() ); $this->assertSame(Generator::UNDEFINED, $openapi->components); - $analysis->process([new MergeIntoComponents()]); + + $this->processorPipeline([new MergeIntoComponents()])->process($analysis); $this->assertCount(1, $openapi->components->responses); $this->assertSame($response, $openapi->components->responses[0]); diff --git a/tests/Processors/MergeIntoOpenApiTest.php b/tests/Processors/MergeIntoOpenApiTest.php index 49e8975f5..de0a7e5d5 100644 --- a/tests/Processors/MergeIntoOpenApiTest.php +++ b/tests/Processors/MergeIntoOpenApiTest.php @@ -27,7 +27,8 @@ public function testProcessor(): void ); $this->assertSame($openapi, $analysis->openapi); $this->assertSame(Generator::UNDEFINED, $openapi->info); - $analysis->process([new MergeIntoOpenApi()]); + + $this->processorPipeline([new MergeIntoOpenApi()])->process($analysis); $this->assertSame($openapi, $analysis->openapi); $this->assertSame($info, $openapi->info); diff --git a/tests/Processors/MergeJsonContentTest.php b/tests/Processors/MergeJsonContentTest.php index ff9edc527..0366bed5c 100644 --- a/tests/Processors/MergeJsonContentTest.php +++ b/tests/Processors/MergeJsonContentTest.php @@ -25,11 +25,12 @@ public function testJsonContent(): void END; $analysis = new Analysis($this->annotationsFromDocBlockParser($comment), $this->getContext()); $this->assertCount(3, $analysis->annotations); - /** @var OA\Response $response */ + $response = $analysis->getAnnotationsOfType(OA\Response::class)[0]; $this->assertSame(Generator::UNDEFINED, $response->content); $this->assertCount(1, $response->_unmerged); - $analysis->process([new MergeJsonContent()]); + + $this->processorPipeline([new MergeJsonContent()])->process($analysis); $this->assertIsArray($response->content); $this->assertCount(1, (array) $response->content); @@ -49,10 +50,10 @@ public function testMultipleMediaTypes(): void ) END; $analysis = new Analysis($this->annotationsFromDocBlockParser($comment), $this->getContext()); - /** @var OA\Response $response */ $response = $analysis->getAnnotationsOfType(OA\Response::class)[0]; $this->assertCount(1, $response->content); - $analysis->process([new MergeJsonContent()]); + + $this->processorPipeline([new MergeJsonContent()])->process($analysis); $this->assertCount(2, $response->content); } @@ -67,12 +68,13 @@ public function testParameter(): void END; $analysis = new Analysis($this->annotationsFromDocBlockParser($comment), $this->getContext()); $this->assertCount(4, $analysis->annotations); - /** @var OA\Parameter $parameter */ + $parameter = $analysis->getAnnotationsOfType(OA\Parameter::class)[0]; $this->assertSame(Generator::UNDEFINED, $parameter->content); $this->assertIsArray($parameter->_unmerged); $this->assertCount(1, $parameter->_unmerged); - $analysis->process([new MergeJsonContent()]); + + $this->processorPipeline([new MergeJsonContent()])->process($analysis); $this->assertIsArray($parameter->content); $this->assertCount(1, (array) $parameter->content); @@ -92,7 +94,7 @@ public function testNoParent(): void ) END; $analysis = new Analysis($this->annotationsFromDocBlockParser($comment), $this->getContext()); - $analysis->process([new MergeJsonContent()]); + $this->processorPipeline([new MergeJsonContent()])->process($analysis); } public function testInvalidParent(): void @@ -106,6 +108,7 @@ public function testInvalidParent(): void ) END; $analysis = new Analysis($this->annotationsFromDocBlockParser($comment), $this->getContext()); - $analysis->process([new MergeJsonContent()]); + $this->processorPipeline([new MergeJsonContent()])->process($analysis); + ; } } diff --git a/tests/Processors/MergeXmlContentTest.php b/tests/Processors/MergeXmlContentTest.php index 8670c5c83..98a07d119 100644 --- a/tests/Processors/MergeXmlContentTest.php +++ b/tests/Processors/MergeXmlContentTest.php @@ -25,11 +25,12 @@ public function testXmlContent(): void END; $analysis = new Analysis($this->annotationsFromDocBlockParser($comment), $this->getContext()); $this->assertCount(3, $analysis->annotations); - /** @var OA\Response $response */ + $response = $analysis->getAnnotationsOfType(OA\Response::class)[0]; $this->assertSame(Generator::UNDEFINED, $response->content); $this->assertCount(1, $response->_unmerged); - $analysis->process([new MergeXmlContent()]); + + $this->processorPipeline([new MergeXmlContent()])->process($analysis); $this->assertIsArray($response->content); $this->assertCount(1, (array) $response->content); @@ -49,10 +50,10 @@ public function testMultipleMediaTypes(): void ) END; $analysis = new Analysis($this->annotationsFromDocBlockParser($comment), $this->getContext()); - /** @var OA\Response $response */ $response = $analysis->getAnnotationsOfType(OA\Response::class)[0]; $this->assertCount(1, $response->content); - $analysis->process([new MergeXmlContent()]); + + $this->processorPipeline([new MergeXmlContent()])->process($analysis); $this->assertCount(2, $response->content); } @@ -66,11 +67,12 @@ public function testParameter(): void END; $analysis = new Analysis($this->annotationsFromDocBlockParser($comment), $this->getContext()); $this->assertCount(4, $analysis->annotations); - /** @var OA\Parameter $parameter */ + $parameter = $analysis->getAnnotationsOfType(OA\Parameter::class)[0]; $this->assertSame(Generator::UNDEFINED, $parameter->content); $this->assertCount(1, $parameter->_unmerged); - $analysis->process([new MergeXmlContent()]); + + $this->processorPipeline([new MergeXmlContent()])->process($analysis); $this->assertIsArray($parameter->content); $this->assertCount(1, (array) $parameter->content); @@ -90,7 +92,7 @@ public function testNoParent(): void ) END; $analysis = new Analysis($this->annotationsFromDocBlockParser($comment), $this->getContext()); - $analysis->process([new MergeXmlContent()]); + $this->processorPipeline([new MergeXmlContent()])->process($analysis); } public function testInvalidParent(): void @@ -104,6 +106,6 @@ public function testInvalidParent(): void ) END; $analysis = new Analysis($this->annotationsFromDocBlockParser($comment), $this->getContext()); - $analysis->process([new MergeXmlContent()]); + $this->processorPipeline([new MergeXmlContent()])->process($analysis); } } diff --git a/tests/Processors/OperationIdTest.php b/tests/Processors/OperationIdTest.php index 615503e34..321a5ad8d 100644 --- a/tests/Processors/OperationIdTest.php +++ b/tests/Processors/OperationIdTest.php @@ -7,20 +7,22 @@ namespace OpenApi\Tests\Processors; use OpenApi\Annotations as OA; -use OpenApi\Processors\OperationId; use OpenApi\Tests\OpenApiTestCase; class OperationIdTest extends OpenApiTestCase { public function testGeneratedOperationId(): void { - $analysis = $this->analysisFromFixtures([ - 'Processors/EntityControllerClass.php', - 'Processors/EntityControllerInterface.php', - 'Processors/EntityControllerTrait.php', - ]); - $analysis->process([new OperationId(false)]); - /** @var OA\Operation[] $operations */ + $analysis = $this->analysisFromFixtures( + [ + 'Processors/EntityControllerClass.php', + 'Processors/EntityControllerInterface.php', + 'Processors/EntityControllerTrait.php', + ], + $this->processorPipeline(), + config: ['operationId' => ['hash' => false]] + ); + $operations = $analysis->getAnnotationsOfType(OA\Operation::class); $this->assertCount(3, $operations); diff --git a/tests/RefTest.php b/tests/RefTest.php index 704e39ac1..e3c2c4912 100644 --- a/tests/RefTest.php +++ b/tests/RefTest.php @@ -8,7 +8,6 @@ use OpenApi\Analysis; use OpenApi\Annotations as OA; -use OpenApi\Generator; class RefTest extends OpenApiTestCase { @@ -41,10 +40,7 @@ public function testRef(): void $openapi->merge($this->annotationsFromDocBlockParser($comment)); $analysis = new Analysis([], $this->getContext()); $analysis->addAnnotation($openapi, $this->getContext()); - (new Generator()) - ->setTypeResolver($this->getTypeResolver()) - ->getProcessorPipeline() - ->process($analysis); + $this->processorPipeline()->process($analysis); $analysis->validate(); // escape / as ~1 diff --git a/tests/ScratchTest.php b/tests/ScratchTest.php index a5fc06820..5c6470308 100644 --- a/tests/ScratchTest.php +++ b/tests/ScratchTest.php @@ -9,10 +9,9 @@ use OpenApi\Annotations as OA; use OpenApi\Generator; use OpenApi\TypeResolverInterface; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; -/** - * @requires PHP 8.1 - */ class ScratchTest extends OpenApiTestCase { public static function scratchTestProvider(): iterable @@ -55,11 +54,9 @@ public static function scratchTestProvider(): iterable /** * Test scratch fixtures. - * - * @dataProvider scratchTestProvider - * - * @requires PHP 8.2 */ + #[DataProvider('scratchTestProvider')] + #[RequiresPhp('8.2')] public function testScratch(TypeResolverInterface $typeResolver, string $scratch, string $spec, string $version, array $expectedLogs): void { foreach ($expectedLogs as $logLine) { diff --git a/tests/SerializerTest.php b/tests/SerializerTest.php index 333ec07e3..47d587940 100644 --- a/tests/SerializerTest.php +++ b/tests/SerializerTest.php @@ -10,6 +10,7 @@ use OpenApi\Generator; use OpenApi\Serializer; use OpenApi\Tests\Concerns\UsesExamples; +use PHPUnit\Framework\Attributes\DataProvider; class SerializerTest extends OpenApiTestCase { @@ -151,7 +152,7 @@ public function testDeserializeAnnotation(): void public function testPetstoreExample(): void { $serializer = new Serializer(); - $spec = $this->examplePath('petstore/petstore-3.0.0.yaml'); + $spec = static::examplePath('petstore/petstore-3.0.0.yaml'); $openapi = $serializer->deserializeFile($spec, 'yaml'); $this->assertInstanceOf(OA\OpenApi::class, $openapi); $this->assertSpecEquals(file_get_contents($spec), $openapi->toYaml()); @@ -204,9 +205,7 @@ public function testDeserializeAllOfProperty(): void } } - /** - * @dataProvider allAnnotationClasses - */ + #[DataProvider('allAnnotationClasses')] public function testValidAnnotationsListComplete(string $annotation): void { $staticProperties = (new \ReflectionClass((Serializer::class)))->getStaticProperties(); diff --git a/tests/SourceFinderTest.php b/tests/SourceFinderTest.php new file mode 100644 index 000000000..6b5b8f0b8 --- /dev/null +++ b/tests/SourceFinderTest.php @@ -0,0 +1,24 @@ +assertCount(12, $sources, 'There should be at least a few files and a directory.'); + $this->assertArrayHasKey(static::examplePath('using-traits/annotations/Decoration/Whistles.php'), $sources); + } +} diff --git a/tests/Type/TypeResolverTest.php b/tests/Type/TypeResolverTest.php index fc229695c..df920212f 100644 --- a/tests/Type/TypeResolverTest.php +++ b/tests/Type/TypeResolverTest.php @@ -15,91 +15,144 @@ use OpenApi\Processors\MergeIntoOpenApi; use OpenApi\Tests\Fixtures\PHP\DocblockAndTypehintTypes; use OpenApi\Tests\OpenApiTestCase; -use OpenApi\Type\LegacyTypeResolver; -use OpenApi\Type\TypeInfoTypeResolver; use OpenApi\TypeResolverInterface; -use Radebatz\TypeInfoExtras\TypeResolver\StringTypeResolver; +use PHPUnit\Framework\Attributes\DataProvider; class TypeResolverTest extends OpenApiTestCase { public static function resolverAugmentCases(): iterable { - if (\PHP_VERSION_ID < 80100) { - return []; - } - $expectations = [ - 'nothing' => '{ "property": "nothing" }', - 'string' => '{ "type": "string", "property": "string" }', - 'nullablestring' => '{ "type": "string", "nullable": true, "property": "nullableString" }', - 'nullablestringexplicit' => '{ "type": "string", "nullable": false, "property": "nullableStringExplicit" }', - 'nullablestringdocblock' => '{ "type": "string", "nullable": true, "property": "nullableStringDocblock" }', - 'nullablestringnative' => '{ "type": "string", "nullable": true, "property": "nullableStringNative" }', - 'stringarray' => '{ "type": "array", "items": { "type": "string" }, "property": "stringArray" }', - 'stringlist' => '{ "type": "array", "items": { "type": "string" }, "property": "stringList" }', - 'stringlistexplicit' => '{ "type": "array", "items": { "type": "string", "example": "foo" }, "property": "stringListExplicit" }', - 'nullablestringlist' => '{ "type": "array", "items": { "type": "string" }, "nullable": true, "property": "nullableStringList" }', - 'nullablestringlistunion' => '{ "type": "array", "items": { "type": "string" }, "nullable": true, "property": "nullableStringListUnion" }', - 'class' => '{ "$ref": "#/components/schemas/DocblockAndTypehintTypes" }', - 'nullableclass' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } ], "nullable": true, "property": "nullableClass" }', - 'namespacedglobalclass' => '{ "type": "string", "format": "date-time", "property": "namespacedGlobalClass" }', - 'nullablenamespacedglobalclass' => '{ "type": "string", "format": "date-time", "nullable": true, "property": "nullableNamespacedGlobalClass" }', - 'alsonullablenamespacedglobalclass' => '{ "type": "string", "format": "date-time", "nullable": true, "property": "alsoNullableNamespacedGlobalClass" }', - 'intrange' => '{ "type": "integer", "maximum": 10, "minimum": -9223372036854775808, "property": "intRange" }', - 'positiveint' => '{ "type": "integer", "maximum": 9223372036854775807, "minimum": 1, "property": "positiveInt" }', - 'nonzeroint' => '{ "type": "integer", "not": { "enum": [ 0 ] }, "property": "nonZeroInt" }', - 'arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }', - 'uniontype' => '{ "property": "unionType" }', - 'promotedstring' => '{ "type": "string", "property": "promotedString" }', - 'mixedunion' => '{ "example": "My value", "property": "mixedUnion" }', - 'getstring' => '{ "type": "string", "property": "getString" }', - 'paramdatetimelist' => '{ "type": "array", "items": { "type": "string", "format": "date-time" }, "property": "paramDateTimeList" }', - 'paramstringlist' => '{ "type": "array", "items": { "type": "string" }, "property": "paramStringList" }', - 'blah' => '{ "type": "string", "example": "My blah", "nullable": true, "property": "blah" }', - 'blah_values' => '{ "type": "array", "items": { "type": "string", "example": "hello" }, "nullable": true, "property": "blah_values" }', - 'oneofvar' => '{ "oneOf": [ { "type": "string" }, { "type": "bool" } ], "property": "oneOfVar" }', - 'oneoflist' => '{ "type": "array", "items": { "oneOf": [ { "type": "string" }, { "type": "bool" } ] }, "property": "oneOfList" }', + OA\OpenApi::VERSION_3_0_0 => [ + 'nothing' => '{ "property": "nothing" }', + 'string' => '{ "type": "string", "property": "string" }', + 'nullablestring' => '{ "type": "string", "nullable": true, "property": "nullableString" }', + 'nullablestringexplicit' => '{ "type": "string", "nullable": false, "property": "nullableStringExplicit" }', + 'nullablestringdocblock' => '{ "type": "string", "nullable": true, "property": "nullableStringDocblock" }', + 'nullablestringnative' => '{ "type": "string", "nullable": true, "property": "nullableStringNative" }', + 'stringarray' => '{ "type": "array", "items": { "type": "string" }, "property": "stringArray" }', + 'stringlist' => '{ "type": "array", "items": { "type": "string" }, "property": "stringList" }', + 'stringlistexplicit' => '{ "type": "array", "items": { "type": "string", "example": "foo" }, "property": "stringListExplicit" }', + 'nullablestringlist' => '{ "type": "array", "items": { "type": "string" }, "nullable": true, "property": "nullableStringList" }', + 'nullablestringlistunion' => '{ "type": "array", "items": { "type": "string" }, "nullable": true, "property": "nullableStringListUnion" }', + 'class' => '{ "$ref": "#/components/schemas/DocblockAndTypehintTypes" }', + 'nullableclass' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } ], "nullable": true, "property": "nullableClass" }', + 'namespacedglobalclass' => '{ "type": "string", "format": "date-time", "property": "namespacedGlobalClass" }', + 'nullablenamespacedglobalclass' => '{ "type": "string", "format": "date-time", "nullable": true, "property": "nullableNamespacedGlobalClass" }', + 'alsonullablenamespacedglobalclass' => '{ "type": "string", "format": "date-time", "nullable": true, "property": "alsoNullableNamespacedGlobalClass" }', + 'intrange' => '{ "type": "integer", "maximum": 10, "minimum": -9223372036854775808, "property": "intRange" }', + 'positiveint' => '{ "type": "integer", "maximum": 9223372036854775807, "minimum": 1, "property": "positiveInt" }', + 'nonzeroint' => '{ "type": "integer", "not": { "enum": [ 0 ] }, "property": "nonZeroInt" }', + 'arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }', + 'uniontype' => '{ "property": "unionType" }', + 'promotedstring' => '{ "type": "string", "property": "promotedString" }', + 'legacy:mixedunion' => '{ "example": "My value", "property": "mixedUnion" }', + 'type-info:mixedunion' => '{ "example": "My value", "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "mixed" } } ], "property": "mixedUnion" }', + 'getstring' => '{ "type": "string", "property": "getString" }', + 'paramdatetimelist' => '{ "type": "array", "items": { "type": "string", "format": "date-time" }, "property": "paramDateTimeList" }', + 'paramstringlist' => '{ "type": "array", "items": { "type": "string" }, "property": "paramStringList" }', + 'blah' => '{ "type": "string", "example": "My blah", "nullable": true, "property": "blah" }', + 'blah_values' => '{ "type": "array", "items": { "type": "string", "example": "hello" }, "nullable": true, "property": "blah_values" }', + 'oneofvar' => '{ "oneOf": [ { "type": "string" }, { "type": "bool" } ], "property": "oneOfVar" }', + 'oneoflist' => '{ "type": "array", "items": { "oneOf": [ { "type": "string" }, { "type": "bool" } ] }, "property": "oneOfList" }', + 'legacy:nullabletypedlistunion' => '{ "nullable": true, "property": "nullableTypedListUnion" }', + 'type-info:nullabletypedlistunion' => '{ "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } ], "property": "nullableTypedListUnion" }', + 'legacy:nullablenestedtypedlistunion' => '{ "nullable": true, "property": "nullableNestedTypedListUnion" }', + 'type-info:nullablenestedtypedlistunion' => '{ "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } } ], "property": "nullableNestedTypedListUnion" }', + 'reflectionvalue' => '{ "example": true, "nullable": true, "property": "reflectionValue" }', + 'legacy:intersectionvar' => '{ "property": "intersectionVar" }', + 'type-info:intersectionvar' => '{ "allOf": [ { "$ref": "#/components/schemas/FirstInterface" }, { "$ref": "#/components/schemas/SecondInterface" } ], "property": "intersectionVar" }', + ], + OA\OpenApi::VERSION_3_1_0 => [ + 'nothing' => '{ "property": "nothing" }', + 'string' => '{ "type": "string", "property": "string" }', + 'nullablestring' => '{ "type": [ "string", "null" ], "property": "nullableString" }', + 'nullablestringexplicit' => '{ "type": "string", "property": "nullableStringExplicit" }', + 'nullablestringdocblock' => '{ "type": [ "string", "null" ], "property": "nullableStringDocblock" }', + 'nullablestringnative' => '{ "type": [ "string", "null" ], "property": "nullableStringNative" }', + 'stringarray' => '{ "type": "array", "items": { "type": "string" }, "property": "stringArray" }', + 'stringlist' => '{ "type": "array", "items": { "type": "string" }, "property": "stringList" }', + 'stringlistexplicit' => '{ "type": "array", "items": { "type": "string", "example": "foo" }, "property": "stringListExplicit" }', + 'nullablestringlist' => '{ "type": [ "array", "null" ], "items": { "type": "string" }, "property": "nullableStringList" }', + 'nullablestringlistunion' => '{ "type": [ "array", "null" ], "items": { "type": "string" }, "property": "nullableStringListUnion" }', + 'class' => '{ "$ref": "#/components/schemas/DocblockAndTypehintTypes" }', + 'nullableclass' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "null" } ], "property": "nullableClass" }', + 'namespacedglobalclass' => '{ "type": "string", "format": "date-time", "property": "namespacedGlobalClass" }', + 'nullablenamespacedglobalclass' => '{ "type": [ "string", "null" ], "format": "date-time", "property": "nullableNamespacedGlobalClass" }', + 'alsonullablenamespacedglobalclass' => '{ "type": [ "string", "null" ], "format": "date-time", "property": "alsoNullableNamespacedGlobalClass" }', + 'intrange' => '{ "type": "integer", "maximum": 10, "minimum": -9223372036854775808, "property": "intRange" }', + 'positiveint' => '{ "type": "integer", "maximum": 9223372036854775807, "minimum": 1, "property": "positiveInt" }', + 'nonzeroint' => '{ "type": "integer", "not": { "const": 0 }, "property": "nonZeroInt" } ', + 'arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }', + 'legacy:uniontype' => '{ "property": "unionType" }', + 'type-info:uniontype' => '{ "type": [ "integer", "string" ], "property": "unionType" }', + 'promotedstring' => '{ "type": "string", "property": "promotedString" }', + 'legacy:mixedunion' => '{ "example": "My value", "property": "mixedUnion" }', + 'type-info:mixedunion' => '{ "example": "My value", "oneOf": [ { "type": [ "string" ] }, { "type": "array", "items": { "type": "mixed" } } ], "property": "mixedUnion" }', + 'getstring' => '{ "type": "string", "property": "getString" }', + 'paramdatetimelist' => '{ "type": "array", "items": { "type": "string", "format": "date-time" }, "property": "paramDateTimeList" }', + 'paramstringlist' => '{ "type": "array", "items": { "type": "string" }, "property": "paramStringList" }', + 'blah' => '{ "type": [ "string", "null" ], "example": "My blah", "property": "blah" }', + 'blah_values' => '{ "type": [ "array", "null" ], "items": { "type": "string", "example": "hello" }, "property": "blah_values" }', + 'oneofvar' => '{ "oneOf": [ { "type": "string" }, { "type": "bool" } ], "property": "oneOfVar" }', + 'oneoflist' => '{ "type": "array", "items": { "oneOf": [ { "type": "string" }, { "type": "bool" } ] }, "property": "oneOfList" }', + 'legacy:nullabletypedlistunion' => '{ "property": "nullableTypedListUnion" }', + 'type-info:nullabletypedlistunion' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "null" } ], "property": "nullableTypedListUnion" }', + 'legacy:nullablenestedtypedlistunion' => '{ "property": "nullableNestedTypedListUnion" }', + 'type-info:nullablenestedtypedlistunion' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } }, { "type": "null" } ], "property": "nullableNestedTypedListUnion" }', + 'legacy:reflectionvalue' => '{ "example": true, "property": "reflectionValue" }', + 'type-info:reflectionvalue' => '{ "type": [ "boolean", "integer", "null" ], "example": true, "property": "reflectionValue" }', + 'legacy:intersectionvar' => '{ "property": "intersectionVar" }', + 'type-info:intersectionvar' => '{ "allOf": [ { "$ref": "#/components/schemas/FirstInterface" }, { "$ref": "#/components/schemas/SecondInterface" } ], "property": "intersectionVar" }', + ], ]; $rc = new \ReflectionClass(DocblockAndTypehintTypes::class); + $fixtureFolder = dirname($rc->getFileName()); + $sources = [ + $rc->getFileName(), + "$fixtureFolder/FirstInterface.php", + "$fixtureFolder/SecondInterface.php", + ]; - $typeResolvers = ['legacy' => new LegacyTypeResolver()]; - if (class_exists(StringTypeResolver::class)) { - $typeResolvers['type-info'] = new TypeInfoTypeResolver(); - } + foreach (static::getTypeResolvers() as $key => $typeResolver) { + foreach ([OA\OpenApi::VERSION_3_0_0, OA\OpenApi::VERSION_3_1_0] as $version) { + $analysis = (new Generator()) + ->setVersion($version) + ->setProcessorPipeline(new Pipeline([new MergeIntoOpenApi(), new AugmentSchemas()])) + ->withContext(function (Generator $generator, Analysis $analysis, Context $context) use ($sources) { + $generator->generate($sources, $analysis, false); - foreach ($typeResolvers as $key => $typeResolver) { - $analysis = (new Generator()) - ->setProcessorPipeline(new Pipeline([new MergeIntoOpenApi(), new AugmentSchemas()])) - ->withContext(function (Generator $generator, Analysis $analysis, Context $context) use ($rc) { - $generator->generate([$rc->getFileName()], $analysis, false); + return $analysis; + }); - return $analysis; - }); + $schema = $analysis->getAnnotationForSource(DocblockAndTypehintTypes::class); - $schema = $analysis->getSchemaForSource(DocblockAndTypehintTypes::class); + foreach ($schema->properties as $ii => $property) { + $property->property = $property->_context->property + // promoted properties might not have a name! + ?? $property->_context->method; - foreach ($schema->properties as $ii => $property) { - $property->property = $property->_context->property - // promoted properties might not have a name! - ?? $property->_context->method; + $caseName = strtolower($property->property); + $resolverCaseName = "$key:$caseName"; + $fullCase = "$key:{$version}[$ii]-$caseName"; - $caseName = strtolower($property->property); - $case = "$key-[$ii]-$caseName"; + $json = $expectations[$version][$caseName] ?? $expectations[$version][$resolverCaseName] ?? null; - yield $case => [ - $typeResolver, - $analysis, - $property, - json_decode($expectations[$caseName] ?? $expectations[$case] ?? '{}', true), - ]; + if ($json) { + yield $fullCase => [ + $typeResolver, + $analysis, + $property, + json_decode($json, true), + ]; + } + } } } } - /** - * @dataProvider resolverAugmentCases - */ + #[DataProvider('resolverAugmentCases')] public function testAugmentSchemaType(TypeResolverInterface $typeResolver, Analysis $analysis, OA\Schema $schema, array $expected): void { $typeResolver->augmentSchemaType($analysis, $schema); diff --git a/tests/UtilTest.php b/tests/UtilTest.php deleted file mode 100644 index f8ec669bc..000000000 --- a/tests/UtilTest.php +++ /dev/null @@ -1,70 +0,0 @@ -assertSame('#/paths/~1blogs~1{blog_id}~1new~0posts', '#/paths/' . Util::refEncode('/blogs/{blog_id}/new~posts')); - } - - public function testRefDecode(): void - { - $this->assertSame('/blogs/{blog_id}/new~posts', Util::refDecode('~1blogs~1{blog_id}~1new~0posts')); - } - - public function testFinder(): void - { - // Create a finder for one of the example directories that has a subdirectory. - $finder = (new Finder())->in($this->examplePath('using-traits/annotations')); - $this->assertGreaterThan(0, iterator_count($finder), 'There should be at least a few files and a directory.'); - $finder_array = \iterator_to_array($finder); - $normalize = static function (string $path): string { - return str_replace('\\', '/', $path); - }; - $directory_path = $normalize($this->examplePath('using-traits/annotations/Decoration')); - $normalizePathKeys = static function ($paths) use ($normalize) { - return \array_combine( - \array_map( - $normalize, - \array_keys($paths) - ), - \array_values($paths) - ); - }; - $this->assertArrayHasKey($directory_path, $normalizePathKeys($finder_array), 'The directory should be a path in the finder.'); - // Use the Util method that should set the finder to only find files, since swagger-php only needs files. - $finder_result = Util::finder($finder); - $this->assertGreaterThan(0, iterator_count($finder_result), 'There should be at least a few file paths.'); - $finder_result_array = \iterator_to_array($finder_result); - $this->assertArrayNotHasKey($directory_path, $normalizePathKeys($finder_result_array), 'The directory should not be a path in the finder.'); - } - - public static function shortenFixtures(): iterable - { - return [ - [[OA\Get::class], ['@OA\Get']], - [[OA\Get::class, OA\Post::class], ['@OA\Get', '@OA\Post']], - ]; - } - - /** - * @dataProvider shortenFixtures - */ - public function testShorten(array $classes, array $expected): void - { - $this->assertEquals($expected, Util::shorten($classes)); - } -}