├── bin └── openapi-client-generator.source ├── composer.json ├── example ├── openapi-client-miele.yaml ├── openapi-client-one.yaml ├── openapi-client-subsplit.yaml └── templates │ └── composer.json ├── src ├── ClassString.php ├── Configuration.php ├── Configuration │ ├── Destination.php │ ├── EntryPoints.php │ ├── Namespace_.php │ ├── QA.php │ ├── QA │ │ └── Tool.php │ ├── Schemas.php │ ├── State.php │ ├── SubSplit.php │ ├── SubSplit │ │ ├── RootPackage.php │ │ └── SectionPackage.php │ ├── Templates.php │ └── Voter.php ├── ContentType │ ├── Json.php │ └── Raw.php ├── Contract │ ├── ContentType.php │ ├── SectionGenerator.php │ └── Voter │ │ ├── AbstractListOperation.php │ │ ├── ListOperation.php │ │ └── StreamOperation.php ├── File.php ├── Gatherer │ ├── Client.php │ ├── CompositSchema.php │ ├── ExampleData.php │ ├── Hydrator.php │ ├── HydratorUtils.php │ ├── IntegerReturnerPretendingToBeARandomNumberGenerator.php │ ├── IntersectionSchema.php │ ├── Operation.php │ ├── OperationHydrator.php │ ├── Path.php │ ├── Property.php │ ├── Schema.php │ ├── Type.php │ ├── WebHook.php │ └── WebHookHydrator.php ├── Generator.php ├── Generator │ ├── Client.php │ ├── Client │ │ ├── Methods │ │ │ └── ChunkCount.php │ │ ├── PHPStan │ │ │ ├── ClientCallReturnTypes.php │ │ │ └── ClientCallReturnTypesTest.php │ │ ├── Routers.php │ │ └── Routers │ │ │ ├── Router.php │ │ │ ├── RouterClass.php │ │ │ └── RouterClassMethod.php │ ├── ClientInterface.php │ ├── Contract.php │ ├── Error.php │ ├── Helper │ │ ├── Operation.php │ │ ├── OperationArray.php │ │ ├── ReflectionTypes.php │ │ ├── ResultConverter.php │ │ └── Types.php │ ├── Hydrator.php │ ├── Hydrators.php │ ├── Operation.php │ ├── OperationTest.php │ ├── Operations.php │ ├── OperationsInterface.php │ ├── Operator.php │ ├── Operators.php │ ├── Routers.php │ ├── Schema.php │ ├── Schema │ │ ├── MultipleCastUnionToType.php │ │ └── SingleCastUnionToType.php │ ├── WebHook.php │ └── WebHooks.php ├── Output │ ├── Error.php │ ├── Status.php │ └── Status │ │ ├── ANSI.php │ │ ├── OverWritingOutPut.php │ │ ├── Simple.php │ │ └── Step.php ├── PrivatePromotedPropertyAsParam.php ├── PromotedPropertyAsParam.php ├── Registry │ ├── CompositSchema.php │ ├── Contract.php │ ├── Schema.php │ ├── ThrowableSchema.php │ └── UnknownSchema.php ├── Representation │ ├── Client.php │ ├── Contract.php │ ├── ExampleData.php │ ├── Header.php │ ├── Hydrator.php │ ├── Operation.php │ ├── OperationEmptyResponse.php │ ├── OperationRequestBody.php │ ├── OperationResponse.php │ ├── Parameter.php │ ├── Path.php │ ├── Property.php │ ├── PropertyType.php │ ├── Schema.php │ └── WebHook.php ├── SectionGenerator │ ├── OperationIdSlash.php │ └── WebHooks.php ├── State.php ├── State │ ├── File.php │ └── Files.php ├── Utils.php ├── Voter │ ├── ListOperation │ │ └── PageAndPerPageInQuery.php │ └── StreamOperation │ │ ├── DownloadInOperationId.php │ │ └── DownloadInPath.php └── phpstan-assertType-mock.php └── test-.php /bin/openapi-client-generator.source: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | hydrateObject(Configuration::class, Yaml::parseFile($configurationFile)); 25 | (new Generator( 26 | $configuration, 27 | dirname($configurationFile) . DIRECTORY_SEPARATOR, 28 | ))->generate( 29 | $configuration->namespace->source . '\\', 30 | $configuration->namespace->test . '\\', 31 | dirname($configurationFile) . DIRECTORY_SEPARATOR, 32 | ); 33 | 34 | return 0; 35 | })($configuration); 36 | } catch (Throwable $throwable) { 37 | Error::display($throwable); 38 | } finally { 39 | exit ($exitCode); 40 | } 41 | })($argv[1]); 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-clients/openapi-client-generator", 3 | "description": "Generate a client based on an OpenAPI spec", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Cees-Jan Kiewiet", 8 | "email": "ceesjank@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": "^8.2", 13 | "api-clients/contracts": "^0.1", 14 | "api-clients/github": "^0.2@dev", 15 | "api-clients/openapi-client-utils": "dev-main", 16 | "ckr/arraymerger": "^3.0", 17 | "codeinc/http-reason-phrase-lookup": "^1.0", 18 | "delight-im/random": "^1.0", 19 | "devizzent/cebe-php-openapi": "^1", 20 | "eventsauce/object-hydrator": "^1.2", 21 | "jawira/case-converter": "^3.5", 22 | "kwn/number-to-words": "^2.6", 23 | "league/openapi-psr7-validator": "^0.21", 24 | "league/uri": "^6.8 || ^7.3", 25 | "nikic/php-parser": "^4.15", 26 | "nunomaduro/termwind": "^1.15", 27 | "ondram/ci-detector": "^4.1", 28 | "phpstan/phpdoc-parser": "^1.22", 29 | "pointybeard/reverse-regex": "1.0.0.3", 30 | "psr/http-message": "^1.1 || ^2 || ^3", 31 | "react/async": "^4.0", 32 | "react/http": "^1.8", 33 | "reactivex/rxphp": "^2.0", 34 | "ringcentral/psr7": "^1.3", 35 | "symfony/yaml": "^6.0", 36 | "twig/twig": "^3.5", 37 | "wyrihaximus/async-test-utilities": "^7.0", 38 | "wyrihaximus/composer-update-bin-autoload-path": "^1", 39 | "wyrihaximus/react-awaitable-observable": "^1.0", 40 | "wyrihaximus/simple-twig": "^2.1", 41 | "wyrihaximus/subsplit-tools": "dev-main" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "ApiClients\\Client\\Github\\": "generated/", 46 | "ApiClients\\Tools\\OpenApiClientGenerator\\": "src/" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "ApiClients\\Tests\\Tools\\OpenApiClientGenerator\\": "tests/unit/" 52 | } 53 | }, 54 | "bin": [ 55 | "bin/openapi-client-generator" 56 | ], 57 | "config": { 58 | "allow-plugins": { 59 | "dealerdirect/phpcodesniffer-composer-installer": true, 60 | "ergebnis/composer-normalize": true, 61 | "infection/extension-installer": true, 62 | "wyrihaximus/composer-update-bin-autoload-path": true 63 | }, 64 | "platform": { 65 | "php": "8.2.13" 66 | } 67 | }, 68 | "extra": { 69 | "wyrihaximus": { 70 | "bin-autoload-path-update": [ 71 | "bin/openapi-client-generator" 72 | ] 73 | } 74 | }, 75 | "scripts": { 76 | "post-install-cmd": [ 77 | "composer normalize" 78 | ], 79 | "post-update-cmd": [ 80 | "composer normalize" 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /example/openapi-client-miele.yaml: -------------------------------------------------------------------------------- 1 | state: 2 | file: etc/openapi-client-generator.state 3 | additionalFiles: 4 | - composer.json 5 | - composer.lock 6 | spec: https://www.miele.com/developer/swagger-ui/m3rdapi.yaml 7 | #spec: m3rdapi.yaml 8 | namespace: 9 | source: ApiClients\Client\Miele 10 | test: ApiClients\Tests\Client\Miele 11 | entryPoints: 12 | call: true 13 | operations: true 14 | webHooks: false 15 | destination: 16 | root: generated-miele 17 | source: src 18 | test: tests 19 | templates: 20 | dir: templates 21 | variables: 22 | fullname: Miele 23 | packageName: miele 24 | schemas: 25 | allowDuplication: true 26 | useAliasesForDuplication: true 27 | contentType: 28 | - ApiClients\Tools\OpenApiClientGenerator\ContentType\Json 29 | - ApiClients\Tools\OpenApiClientGenerator\ContentType\Raw 30 | voter: 31 | listOperation: 32 | - ApiClients\Tools\OpenApiClientGenerator\Voter\ListOperation\PageAndPerPageInQuery 33 | streamOperation: 34 | - ApiClients\Tools\OpenApiClientGenerator\Voter\StreamOperation\DownloadInOperationId 35 | qa: 36 | phpcs: 37 | enabled: true 38 | phpstan: 39 | enabled: true 40 | configFilePath: etc/phpstan-extension.neon 41 | -------------------------------------------------------------------------------- /example/openapi-client-one.yaml: -------------------------------------------------------------------------------- 1 | state: 2 | file: etc/openapi-client-generator.state 3 | additionalFiles: 4 | - composer.json 5 | - composer.lock 6 | #spec: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions-next/api.github.com/api.github.com.yaml 7 | spec: api.github.com.yaml 8 | entryPoints: 9 | call: true 10 | operations: true 11 | webHooks: true 12 | namespace: 13 | source: ApiClients\Client\Github 14 | test: ApiClients\Tests\Client\Github 15 | destination: 16 | root: generated-github 17 | source: src 18 | test: tests 19 | templates: 20 | dir: templates 21 | variables: 22 | fullname: GitHub 23 | packageName: github 24 | schemas: 25 | allowDuplication: true 26 | useAliasesForDuplication: true 27 | contentType: 28 | - ApiClients\Tools\OpenApiClientGenerator\ContentType\Json 29 | - ApiClients\Tools\OpenApiClientGenerator\ContentType\Raw 30 | voter: 31 | listOperation: 32 | - ApiClients\Tools\OpenApiClientGenerator\Voter\ListOperation\PageAndPerPageInQuery 33 | streamOperation: 34 | - ApiClients\Tools\OpenApiClientGenerator\Voter\StreamOperation\DownloadInOperationId 35 | qa: 36 | phpcs: 37 | enabled: true 38 | phpstan: 39 | enabled: true 40 | configFilePath: etc/phpstan-extension.neon 41 | -------------------------------------------------------------------------------- /example/openapi-client-subsplit.yaml: -------------------------------------------------------------------------------- 1 | state: 2 | file: etc/openapi-client-generator.state 3 | additionalFiles: 4 | - composer.json 5 | - composer.lock 6 | #spec: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions-next/api.github.com/api.github.com.yaml 7 | spec: api.github.com.yaml 8 | entryPoints: 9 | call: true 10 | operations: true 11 | webHooks: true 12 | namespace: 13 | source: ApiClients\Client\Github 14 | test: ApiClients\Tests\Client\Github 15 | destination: 16 | root: generated-github-subsplit 17 | source: src 18 | test: tests 19 | templates: 20 | dir: templates 21 | variables: 22 | foo: bar 23 | schemas: 24 | allowDuplication: true 25 | useAliasesForDuplication: true 26 | contentType: 27 | - ApiClients\Tools\OpenApiClientGenerator\ContentType\Json 28 | - ApiClients\Tools\OpenApiClientGenerator\ContentType\Raw 29 | voter: 30 | listOperation: 31 | - ApiClients\Tools\OpenApiClientGenerator\Voter\ListOperation\PageAndPerPageInQuery 32 | streamOperation: 33 | - ApiClients\Tools\OpenApiClientGenerator\Voter\StreamOperation\DownloadInOperationId 34 | subSplit: 35 | subSplitsDestination: clients 36 | branch: v0.3.x 37 | targetVersion: ^v0.3@dev 38 | subSplitConfiguration: etc/config.subsplit-publish.json 39 | fullName: GitHub {{ section }} 40 | vendor: api-clients 41 | sectionGenerator: 42 | - ApiClients\Tools\OpenApiClientGenerator\SectionGenerator\OperationIdSlash 43 | - ApiClients\Tools\OpenApiClientGenerator\SectionGenerator\WebHooks 44 | rootPackage: 45 | name: github 46 | repository: git@github.com:php-api-clients/github.git 47 | sectionPackage: 48 | name: github-{{ section }} 49 | repository: git@github.com:php-api-clients/github-{{ section }}.git 50 | qa: 51 | phpcs: 52 | enabled: true 53 | phpstan: 54 | enabled: true 55 | configFilePath: etc/phpstan-extension.neon 56 | -------------------------------------------------------------------------------- /example/templates/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-clients/{{ packageName }}", 3 | "description": "Non-Blocking first {{ fullName }} client", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Cees-Jan Kiewiet", 8 | "email": "ceesjank@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": "^8.2", 13 | {% if requires is iterable %} 14 | {% for require in requires %} 15 | "{{ require.name }}": "{{ require.version }}", 16 | {% endfor %} 17 | {% endif %} 18 | "api-clients/contracts": "^0.1", 19 | "api-clients/openapi-client-utils": "dev-main", 20 | "devizzent/cebe-php-openapi": "^1", 21 | "eventsauce/object-hydrator": "^1.1", 22 | "league/openapi-psr7-validator": "^0.21", 23 | "league/uri": "^7.3 || ^6.8", 24 | "psr/http-message": "^1.0", 25 | "react/http": "^1.8", 26 | "react/async": "^4.0", 27 | "wyrihaximus/react-awaitable-observable": "^1.0" 28 | }, 29 | "require-dev": { 30 | {% if require-dev is iterable %} 31 | {% for require in requires-dev %} 32 | "{{ require.name }}": "{{ require.version }}", 33 | {% endfor %} 34 | {% endif %} 35 | "wyrihaximus/async-test-utilities": "^7" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "{{ namespace|trim('\\', 'left')|replace({'\\': '\\\\'}) }}": "src/" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "{{ namespace|trim('\\', 'left')|replace({'\\': '\\\\'}) }}": "src/" 45 | } 46 | }, 47 | {% if suggests is iterable and suggests|length > 0 %} 48 | "suggest": { 49 | {% for suggest in suggests %} 50 | "api-clients/{{ suggest.name }}": "{{ suggest.reason }}"{% if not loop.last %},{% endif %} 51 | {% endfor %} 52 | }, 53 | {% endif %} 54 | {% if qa.phpstan.enabled is constant('true') and qa.phpstan.configFilePath is not constant('null') %} 55 | "extra": { 56 | "phpstan": { 57 | "includes": [ 58 | "{{ qa.phpstan.configFilePath }}" 59 | ] 60 | } 61 | }, 62 | {% endif %} 63 | "config": { 64 | "sort-packages": true, 65 | "platform": { 66 | "php": "8.2.13" 67 | }, 68 | "allow-plugins": { 69 | "dealerdirect/phpcodesniffer-composer-installer": true, 70 | "composer/package-versions-deprecated": true, 71 | "ergebnis/composer-normalize": true, 72 | "icanhazstring/composer-unused": true, 73 | "wyrihaximus/composer-update-bin-autoload-path": true, 74 | "infection/extension-installer": true 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/ClassString.php: -------------------------------------------------------------------------------- 1 | source, '\\') . '\\' . $relative), 18 | Utils::cleanUpNamespace(trim($namespace->test, '\\') . '\\' . $relative), 19 | ); 20 | 21 | return new self( 22 | $namespace, 23 | new Namespace_( 24 | Utils::dirname($fullyQualified->source), 25 | Utils::dirname($fullyQualified->test), 26 | ), 27 | $fullyQualified, 28 | $relative, 29 | Utils::basename($relative), 30 | ); 31 | } 32 | 33 | private function __construct( 34 | public Namespace_ $baseNamespaces, 35 | public Namespace_ $namespace, 36 | public Namespace_ $fullyQualified, 37 | public string $relative, 38 | public string $className, 39 | ) { 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Configuration.php: -------------------------------------------------------------------------------- 1 | >|null $contentType */ 22 | public function __construct( 23 | public State $state, 24 | public string $spec, 25 | #[MapFrom('entryPoints')] 26 | public EntryPoints $entryPoints, 27 | public Templates|null $templates, 28 | public Namespace_ $namespace, 29 | public Destination $destination, 30 | #[MapFrom('contentType')] 31 | public array|null $contentType, 32 | #[MapFrom('subSplit')] 33 | public SubSplit|null $subSplit, 34 | public Schemas|null $schemas, 35 | public Voter|null $voter, 36 | public QA|null $qa, 37 | ) { 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Configuration/Destination.php: -------------------------------------------------------------------------------- 1 | $additionalFiles */ 13 | public function __construct( 14 | public string $file, 15 | #[MapFrom('additionalFiles')] 16 | #[CastListToType('string')] 17 | public array|null $additionalFiles, 18 | ) { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Configuration/SubSplit.php: -------------------------------------------------------------------------------- 1 | >|null $sectionGenerator */ 15 | public function __construct( 16 | #[MapFrom('subSplitsDestination')] 17 | public string $subSplitsDestination, 18 | public string $branch, 19 | #[MapFrom('targetVersion')] 20 | public string $targetVersion, 21 | #[MapFrom('subSplitConfiguration')] 22 | public string $subSplitConfiguration, 23 | #[MapFrom('fullName')] 24 | public string $fullName, 25 | public string $vendor, 26 | #[MapFrom('sectionGenerator')] 27 | public array|null $sectionGenerator, 28 | #[MapFrom('rootPackage')] 29 | public RootPackage $rootPackage, 30 | #[MapFrom('sectionPackage')] 31 | public SectionPackage $sectionPackage, 32 | ) { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Configuration/SubSplit/RootPackage.php: -------------------------------------------------------------------------------- 1 | |null $variables */ 10 | public function __construct( 11 | public string $dir, 12 | public array|null $variables, 13 | ) { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Configuration/Voter.php: -------------------------------------------------------------------------------- 1 | >|null $listOperation 14 | * @param array>|null $streamOperation 15 | */ 16 | public function __construct( 17 | #[MapFrom('listOperation')] 18 | public array|null $listOperation, 19 | #[MapFrom('streamOperation')] 20 | public array|null $streamOperation, 21 | ) { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ContentType/Json.php: -------------------------------------------------------------------------------- 1 | */ 15 | public static function contentType(): iterable 16 | { 17 | yield 'application/json'; 18 | yield 'application/scim+json'; 19 | } 20 | 21 | public static function parse(Expr $expr): Expr 22 | { 23 | return new Node\Expr\FuncCall( 24 | new Node\Name('json_decode'), 25 | [ 26 | new Arg( 27 | $expr, 28 | ), 29 | new Arg( 30 | new Node\Expr\ConstFetch( 31 | new Node\Name('true'), 32 | ), 33 | ), 34 | ], 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ContentType/Raw.php: -------------------------------------------------------------------------------- 1 | */ 13 | public static function contentType(): iterable 14 | { 15 | yield 'text/plain'; 16 | yield 'text/x-markdown'; 17 | yield 'text/html'; 18 | yield 'application/pdf'; 19 | } 20 | 21 | public static function parse(Expr $expr): Expr 22 | { 23 | return $expr; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Contract/ContentType.php: -------------------------------------------------------------------------------- 1 | */ 12 | public static function contentType(): iterable; 13 | 14 | public static function parse(Expr $expr): Expr; 15 | } 16 | -------------------------------------------------------------------------------- /src/Contract/SectionGenerator.php: -------------------------------------------------------------------------------- 1 | response as $response) { 18 | if ($response->code === 200 && $response->content instanceof Schema) { 19 | return false; 20 | } 21 | 22 | if ($response->code === 200 && $response->content instanceof PropertyType && $response->content->type !== 'array') { 23 | return false; 24 | } 25 | } 26 | 27 | $match = []; 28 | foreach (static::keys() as $key) { 29 | $match[$key] = false; 30 | } 31 | 32 | foreach ($operation->parameters as $parameter) { 33 | if (! array_key_exists($parameter->name, $match)) { 34 | continue; 35 | } 36 | 37 | if ($parameter->location !== 'query') { 38 | continue; 39 | } 40 | 41 | $match[$parameter->name] = true; 42 | } 43 | 44 | foreach ($match as $matched) { 45 | if ($matched === false) { 46 | return false; 47 | } 48 | } 49 | 50 | return true; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Contract/Voter/ListOperation.php: -------------------------------------------------------------------------------- 1 | */ 16 | public static function keys(): array; 17 | 18 | public static function list(Operation $operation): bool; 19 | } 20 | -------------------------------------------------------------------------------- /src/Contract/Voter/StreamOperation.php: -------------------------------------------------------------------------------- 1 | servers ?? [] as $server) { 20 | if (strlen($server->url) === 0) { 21 | continue; 22 | } 23 | 24 | $baseUrl = $server->url; 25 | break; 26 | } 27 | 28 | return new \ApiClients\Tools\OpenApiClientGenerator\Representation\Client( 29 | $baseUrl, 30 | $paths, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Gatherer/CompositSchema.php: -------------------------------------------------------------------------------- 1 | type === 'array'; 33 | $properties = []; 34 | $example = []; 35 | 36 | if ($isArray) { 37 | $schema = $schema->items; 38 | } 39 | 40 | foreach ($schema->properties as $propertyName => $property) { 41 | $gatheredProperty = Property::gather( 42 | $baseNamespace, 43 | $className, 44 | (string) $propertyName, 45 | in_array( 46 | (string) $propertyName, 47 | $schema->required ?? [], 48 | false, 49 | ), 50 | $property, 51 | $schemaRegistry, 52 | $contractRegistry, 53 | $compositSchemaRegistry, 54 | ); 55 | $properties[] = $gatheredProperty; 56 | 57 | $example[$gatheredProperty->sourceName] = $gatheredProperty->example->raw; 58 | 59 | foreach (['examples', 'example'] as $examplePropertyName) { 60 | if (array_key_exists($gatheredProperty->sourceName, $example)) { 61 | break; 62 | } 63 | 64 | if (! property_exists($schema, $examplePropertyName) || ! is_array($schema->$examplePropertyName) || ! array_key_exists($gatheredProperty->sourceName, $schema->$examplePropertyName)) { 65 | continue; 66 | } 67 | 68 | $example[$gatheredProperty->sourceName] = $schema->$examplePropertyName[$gatheredProperty->sourceName]; 69 | } 70 | 71 | foreach ($property->enum ?? [] as $value) { 72 | $example[$gatheredProperty->sourceName] = $value; 73 | break; 74 | } 75 | 76 | if ($example[$gatheredProperty->sourceName] !== null || $schema->required) { 77 | continue; 78 | } 79 | 80 | unset($example[$gatheredProperty->sourceName]); 81 | } 82 | 83 | return new Schema( 84 | ClassString::factory($baseNamespace, 'Schema\\' . $className), 85 | ClassString::factory($baseNamespace, 'Contract\\' . $className), 86 | ClassString::factory($baseNamespace, 'Error\\' . $className), 87 | ClassString::factory($baseNamespace, 'ErrorSchemas\\' . $className), 88 | $schema->title ?? '', 89 | $schema->description ?? '', 90 | $example, 91 | $properties, 92 | $schema, 93 | $isArray, 94 | ($schema->type === null ? ['object'] : (is_array($schema->type) ? $schema->type : [$schema->type])), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Gatherer/ExampleData.php: -------------------------------------------------------------------------------- 1 | type === 'array' || $type->type === 'union') { 35 | if ($type->payload instanceof Schema) { 36 | $exampleData = ArrayMerger::doMerge( 37 | $type->payload->example, 38 | is_array($exampleData) ? $exampleData : [], 39 | ArrayMerger::FLAG_OVERWRITE_NUMERIC_KEY | ArrayMerger::FLAG_ALLOW_SCALAR_TO_ARRAY_CONVERSION, 40 | ); 41 | } elseif ($type->payload instanceof PropertyType) { 42 | return self::gather($exampleData, $type->payload, $propertyName); 43 | } 44 | 45 | return new Representation\ExampleData($exampleData, $exampleData instanceof Node\Expr ? $exampleData : self::turnArrayIntoNode((array) $exampleData)); 46 | } 47 | 48 | if ($type->payload instanceof Schema) { 49 | $exampleData = ArrayMerger::doMerge($type->payload->example, is_array($exampleData) ? $exampleData : [], ArrayMerger::FLAG_OVERWRITE_NUMERIC_KEY | ArrayMerger::FLAG_ALLOW_SCALAR_TO_ARRAY_CONVERSION); 50 | 51 | return new Representation\ExampleData($exampleData, self::turnArrayIntoNode($exampleData)); 52 | } 53 | 54 | if ($exampleData === null && $type->type === 'scalar' && is_string($type->payload)) { 55 | return self::scalarData(strlen($propertyName), $type->payload, $type->format, $type->pattern); 56 | } 57 | 58 | return self::determiteType($exampleData); 59 | } 60 | 61 | public static function determiteType(mixed $exampleData): Representation\ExampleData 62 | { 63 | return match (gettype($exampleData)) { 64 | 'boolean' => new Representation\ExampleData( 65 | $exampleData, 66 | new Node\Expr\ConstFetch( 67 | new Node\Name( 68 | $exampleData ? 'true' : 'false', 69 | ), 70 | ), 71 | ), 72 | 'integer' => new Representation\ExampleData( 73 | $exampleData, 74 | new Node\Scalar\LNumber($exampleData), 75 | ), 76 | 'double' => new Representation\ExampleData( 77 | $exampleData, 78 | new Node\Scalar\DNumber($exampleData), 79 | ), 80 | 'string' => new Representation\ExampleData( 81 | $exampleData, 82 | new Node\Scalar\String_($exampleData), 83 | ), 84 | 'array' => new Representation\ExampleData($exampleData, self::turnArrayIntoNode($exampleData)), 85 | default => new Representation\ExampleData( 86 | null, 87 | new Node\Expr\ConstFetch( 88 | new Node\Name( 89 | 'null', 90 | ), 91 | ), 92 | ), 93 | }; 94 | } 95 | 96 | /** @phpstan-ignore-next-line */ 97 | public static function scalarData(int $seed, string $type, string|null $format, string|null $pattern = null): Representation\ExampleData 98 | { 99 | if (strpos($type, '|') !== false) { 100 | [$firstType] = explode('|', $type); 101 | 102 | return self::scalarData($seed, $firstType, $format, $pattern); 103 | } 104 | 105 | if ($type === 'int' || $type === '?int') { 106 | return new Representation\ExampleData($seed, new Node\Scalar\LNumber($seed)); 107 | } 108 | 109 | if ($type === 'float' || $type === '?float' || $type === 'int|float' || $type === 'null|int|float') { 110 | return new Representation\ExampleData($seed / 10, new Node\Scalar\DNumber($seed / 10)); 111 | } 112 | 113 | if ($type === 'bool' || $type === '?bool') { 114 | return new Representation\ExampleData( 115 | false, 116 | new Node\Expr\ConstFetch( 117 | new Node\Name( 118 | 'false', 119 | ), 120 | ), 121 | ); 122 | } 123 | 124 | if ($type === 'string' || $type === '?string') { 125 | if ($pattern !== null) { 126 | $result = ''; 127 | 128 | /** @phpstan-ignore-next-line */ 129 | @(new Parser(new Lexer($pattern), new Scope(), new Scope()))->parse()->getResult()->generate( 130 | $result, 131 | new IntegerReturnerPretendingToBeARandomNumberGenerator(strlen($pattern)), 132 | ); 133 | 134 | return new Representation\ExampleData($result, new Node\Scalar\String_($result)); 135 | } 136 | 137 | if ($format === 'uri') { 138 | return new Representation\ExampleData('https://example.com/', new Node\Scalar\String_('https://example.com/')); 139 | } 140 | 141 | if ($format === 'email') { 142 | return new Representation\ExampleData('hi@example.com', new Node\Scalar\String_('hi@example.com')); 143 | } 144 | 145 | if ($format === 'date-time') { 146 | return new Representation\ExampleData(date(DateTimeInterface::RFC3339, 0), new Node\Scalar\String_(date(DateTimeInterface::RFC3339, 0))); 147 | } 148 | 149 | if ($format === 'uuid') { 150 | return new Representation\ExampleData('4ccda740-74c3-4cfa-8571-ebf83c8f300a', new Node\Scalar\String_('4ccda740-74c3-4cfa-8571-ebf83c8f300a')); 151 | } 152 | 153 | if ($format === 'ipv4') { 154 | return new Representation\ExampleData('127.0.0.1', new Node\Scalar\String_('127.0.0.1')); 155 | } 156 | 157 | if ($format === 'ipv6') { 158 | return new Representation\ExampleData('::1', new Node\Scalar\String_('::1')); 159 | } 160 | 161 | return new Representation\ExampleData('generated', new Node\Scalar\String_('generated')); 162 | } 163 | 164 | if ($type === 'array' || $type === '?array') { 165 | $string = self::scalarData($seed, 'string', $format, $pattern); 166 | 167 | return new Representation\ExampleData( 168 | [ 169 | $string->raw, 170 | ], 171 | new Node\Expr\Array_( 172 | [ 173 | new Node\Expr\ArrayItem( 174 | $string->node, 175 | ), 176 | ], 177 | ), 178 | ); 179 | } 180 | 181 | return new Representation\ExampleData( 182 | null, 183 | new Node\Expr\ConstFetch( 184 | new Node\Name( 185 | 'null', 186 | ), 187 | ), 188 | ); 189 | } 190 | 191 | /** @param array $array */ 192 | private static function turnArrayIntoNode(array $array): Node\Expr 193 | { 194 | return new Node\Expr\FuncCall( 195 | new Node\Name('\json_decode'), 196 | [ 197 | new Node\Arg( 198 | new Node\Scalar\String_( 199 | json_encode([...self::arrayToRaw($array)]), 200 | ), 201 | ), 202 | new Node\Arg( 203 | new Node\Expr\ConstFetch( 204 | new Node\Name( 205 | 'false', 206 | ), 207 | ), 208 | ), 209 | ], 210 | ); 211 | } 212 | 213 | /** 214 | * @param array $exampleData 215 | * 216 | * @return iterable 217 | */ 218 | private static function arrayToRaw(array $exampleData): iterable 219 | { 220 | foreach ($exampleData as $key => $value) { 221 | if ($value instanceof Representation\ExampleData) { 222 | $value = $value->raw; 223 | } 224 | 225 | if (is_array($value)) { 226 | $value = [...self::arrayToRaw($value)]; 227 | } 228 | 229 | yield $key => $value; 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/Gatherer/Hydrator.php: -------------------------------------------------------------------------------- 1 | */ 13 | public static function listSchemas(Schema $schema): iterable 14 | { 15 | yield $schema; 16 | 17 | foreach ($schema->properties as $property) { 18 | yield from self::listSchemasFromPropertyType($property->type); 19 | } 20 | } 21 | 22 | /** @return iterable */ 23 | private static function listSchemasFromPropertyType(PropertyType $propertyType): iterable 24 | { 25 | if ($propertyType->payload instanceof Schema) { 26 | yield from self::listSchemas($propertyType->payload); 27 | } elseif ($propertyType->payload instanceof PropertyType) { 28 | yield from self::listSchemasFromPropertyType($propertyType->payload); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Gatherer/IntegerReturnerPretendingToBeARandomNumberGenerator.php: -------------------------------------------------------------------------------- 1 | randomNumber > $max ? $max : $this->randomNumber; 23 | } 24 | 25 | /** 26 | * @phpstan-ignore-next-line 27 | */ 28 | public function seed($seed = null) 29 | { 30 | return $this->randomNumber; 31 | } 32 | 33 | public function max() 34 | { 35 | return $this->randomNumber; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Gatherer/IntersectionSchema.php: -------------------------------------------------------------------------------- 1 | allOf as $schema) { 38 | $gatheredProperties = []; 39 | foreach ($schema->properties as $propertyName => $property) { 40 | $gatheredProperty = $gatheredProperties[(string) $propertyName] = Property::gather( 41 | $baseNamespace, 42 | $className, 43 | (string) $propertyName, 44 | in_array( 45 | (string) $propertyName, 46 | $schema->required ?? [], 47 | false, 48 | ), 49 | $property, 50 | $schemaRegistry, 51 | $contractRegistry, 52 | $compositSchemaRegistry, 53 | ); 54 | 55 | $example[$gatheredProperty->sourceName] = $gatheredProperty->example->raw; 56 | 57 | foreach (['examples', 'example'] as $examplePropertyName) { 58 | if (array_key_exists($gatheredProperty->sourceName, $example)) { 59 | break; 60 | } 61 | 62 | if (! property_exists($schema, $examplePropertyName) || ! is_array($schema->$examplePropertyName) || ! array_key_exists($gatheredProperty->sourceName, $schema->$examplePropertyName)) { 63 | continue; 64 | } 65 | 66 | $example[$gatheredProperty->sourceName] = $schema->$examplePropertyName[$gatheredProperty->sourceName]; 67 | } 68 | 69 | foreach ($property->enum ?? [] as $value) { 70 | $example[$gatheredProperty->sourceName] = $value; 71 | break; 72 | } 73 | 74 | if ($example[$gatheredProperty->sourceName] !== null || $property->required || $baseProperty->required) { 75 | continue; 76 | } 77 | 78 | unset($example[$gatheredProperty->sourceName]); 79 | } 80 | 81 | $contracts[] = new Contract( 82 | ClassString::factory( 83 | $baseNamespace, 84 | $contractRegistry->get($schema, 'Contract\\' . $className . '\\' . $schema->title), 85 | ), 86 | $gatheredProperties, 87 | ); 88 | 89 | $properties = [...$properties, ...$gatheredProperties]; 90 | } 91 | 92 | return new Schema( 93 | ClassString::factory($baseNamespace, 'Schema\\' . $className), 94 | $contracts, 95 | ClassString::factory($baseNamespace, 'Error\\' . $className), 96 | ClassString::factory($baseNamespace, 'ErrorSchemas\\' . $className), 97 | $baseProperty->title ?? '', 98 | $baseProperty->description ?? '', 99 | $example, 100 | $properties, 101 | $baseProperty, 102 | false, 103 | ($baseProperty->type === null ? ['object'] : (is_array($baseProperty->type) ? $baseProperty->type : [$baseProperty->type])), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Gatherer/Operation.php: -------------------------------------------------------------------------------- 1 | $metaData */ 40 | public static function gather( 41 | Namespace_ $baseNamespace, 42 | string $className, 43 | string $matchMethod, 44 | string $method, 45 | string $path, 46 | array $metaData, 47 | openAPIOperation $operation, 48 | ThrowableSchema $throwableSchemaRegistry, 49 | SchemaRegistry $schemaRegistry, 50 | ContractRegistry $contractRegistry, 51 | CompositSchemaRegistry $compositSchemaRegistry, 52 | ): \ApiClients\Tools\OpenApiClientGenerator\Representation\Operation { 53 | $returnType = []; 54 | $parameters = []; 55 | $empties = []; 56 | foreach ($operation->parameters as $parameter) { 57 | $types = is_array($parameter->schema->type) ? $parameter->schema->type : [$parameter->schema->type]; 58 | if (count($parameter->schema->oneOf ?? []) > 0) { 59 | $types = []; 60 | foreach ($parameter->schema->oneOf as $oneOfSchema) { 61 | $types[] = $oneOfSchema->type; 62 | } 63 | } 64 | 65 | $parameterType = str_replace([ 66 | 'integer', 67 | 'any', 68 | 'boolean', 69 | ], [ 70 | 'int', 71 | 'string|object', 72 | 'bool', 73 | ], implode('|', $types)); 74 | 75 | $parameters[] = new Parameter( 76 | (new Convert($parameter->name))->toCamel(), 77 | $parameter->name, 78 | $parameter->description ?? '', 79 | $parameterType, 80 | $parameter->schema->format, 81 | $parameter->in, 82 | $parameter->schema->default, 83 | ExampleData::scalarData($parameter->name === 'page' ? 1 : strlen($parameter->name), $parameterType, $parameter->schema->format), 84 | ); 85 | } 86 | 87 | $classNameSanitized = str_replace('/', '\\', Utils::className($className)); 88 | $requestBody = []; 89 | if ($operation->requestBody !== null) { 90 | foreach ($operation->requestBody->content as $contentType => $requestBodyDetails) { 91 | $requestBodyClassname = $schemaRegistry->get( 92 | $requestBodyDetails->schema, 93 | $classNameSanitized . '\\Request\\' . Utils::className(str_replace('/', '_', $contentType)), 94 | ); 95 | $requestBody[] = new OperationRequestBody( 96 | $contentType, 97 | Schema::gather($baseNamespace, $requestBodyClassname, $requestBodyDetails->schema, $schemaRegistry, $contractRegistry, $compositSchemaRegistry), 98 | ); 99 | } 100 | } 101 | 102 | $response = []; 103 | foreach ($operation->responses ?? [] as $code => $spec) { 104 | $isError = $code === 'default' || $code >= 400; 105 | $contentCount = 0; 106 | foreach ($spec->content as $contentType => $contentTypeMediaType) { 107 | $contentCount++; 108 | $responseClassname = $schemaRegistry->get( 109 | $contentTypeMediaType->schema, 110 | 'Operations\\' . $classNameSanitized . '\\Response\\' . Utils::className( 111 | str_replace( 112 | '/', 113 | '_', 114 | $contentType, 115 | ) . '\\' . ($code === 'default' ? 'Default' : (HttpReasonPhraseLookup::getReasonPhrase($code) ?? 'Unknown')), 116 | ), 117 | ); 118 | 119 | $response[] = new OperationResponse( 120 | $code, 121 | $contentType, 122 | $spec->description, 123 | Type::gather( 124 | $baseNamespace, 125 | $responseClassname, 126 | $contentType, 127 | $contentTypeMediaType->schema, 128 | true, 129 | $schemaRegistry, 130 | $contractRegistry, 131 | $compositSchemaRegistry, 132 | ), 133 | ); 134 | if ($isError) { 135 | $throwableSchemaRegistry->add('Schema\\' . $responseClassname); 136 | continue; 137 | } 138 | 139 | $returnType[] = $responseClassname; 140 | } 141 | 142 | if ($contentCount !== 0) { 143 | continue; 144 | } 145 | 146 | $headers = []; 147 | foreach ($spec->headers as $headerName => $headerSpec) { 148 | $headers[$headerName] = new Header($headerName, Schema::gather( 149 | $baseNamespace, 150 | $schemaRegistry->get( 151 | $headerSpec->schema, 152 | 'WebHookHeader\\' . ucfirst(preg_replace('/\PL/u', '', $headerName)), 153 | ), 154 | $headerSpec->schema, 155 | $schemaRegistry, 156 | $contractRegistry, 157 | $compositSchemaRegistry, 158 | ), ExampleData::determiteType($headerSpec->example)); 159 | } 160 | 161 | $empties[] = new OperationEmptyResponse($code, $spec->description, $headers); 162 | } 163 | 164 | if (count($returnType) === 0) { 165 | $returnType[] = '\\' . ResponseInterface::class; 166 | } 167 | 168 | $name = lcfirst(trim(Utils::basename($className), '\\')); 169 | $group = strlen(trim(trim(Utils::dirname($className), '\\'), '.')) > 0 ? trim(str_replace('\\', '', Utils::dirname($className)), '\\') : null; 170 | 171 | return new \ApiClients\Tools\OpenApiClientGenerator\Representation\Operation( 172 | ClassString::factory($baseNamespace, 'Internal\\Operation\\' . Utils::fixKeyword($className)), 173 | ClassString::factory($baseNamespace, $classNameSanitized), 174 | ClassString::factory($baseNamespace, 'Internal\\Operator\\' . Utils::fixKeyword($className)), 175 | lcfirst( 176 | str_replace( 177 | ['\\'], 178 | ['👷'], 179 | ClassString::factory($baseNamespace, Utils::fixKeyword($className))->relative, 180 | ), 181 | ), 182 | $name, 183 | (new Convert($name))->toCamel(), 184 | $group, 185 | $group === null ? null : (new Convert($group))->toCamel(), 186 | $operation->operationId, 187 | strtoupper($matchMethod), 188 | strtoupper($method), 189 | $operation->summary, 190 | $operation->externalDocs, 191 | $path, 192 | $metaData, 193 | array_unique($returnType), 194 | [ 195 | ...array_filter($parameters, static fn (Parameter $parameter): bool => $parameter->default === null), 196 | ...array_filter($parameters, static fn (Parameter $parameter): bool => $parameter->default !== null), 197 | ], 198 | $requestBody, 199 | $response, 200 | $empties, 201 | ); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Gatherer/OperationHydrator.php: -------------------------------------------------------------------------------- 1 | response as $response) { 22 | if (! ($response->content->payload instanceof Schema)) { 23 | continue; 24 | } 25 | 26 | foreach (HydratorUtils::listSchemas($response->content->payload) as $schema) { 27 | $schemaClasses[] = $schema; 28 | } 29 | } 30 | } 31 | 32 | return Hydrator::gather( 33 | $baseNamespace, 34 | 'Operation\\' . $className, 35 | '🌀', 36 | ...$schemaClasses, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Gatherer/Path.php: -------------------------------------------------------------------------------- 1 | getOperations() as $method => $operation) { 37 | $operationClassName = Utils::className($operation->operationId); 38 | if (strlen($operationClassName) === 0) { 39 | continue; 40 | } 41 | 42 | $operations[] = $opp = Operation::gather( 43 | $baseNamespace, 44 | $operationClassName, 45 | $method, 46 | $method, 47 | $path, 48 | [], 49 | $operation, 50 | $throwableSchemaRegistry, 51 | $schemaRegistry, 52 | $contractRegistry, 53 | $compositSchemaRegistry, 54 | ); 55 | 56 | if ($voters !== null && is_array($voters->listOperation)) { 57 | $shouldStream = false; 58 | $voter = null; 59 | /** @phpstan-ignore-next-line */ 60 | foreach ($voters->listOperation as $voter) { 61 | if ($voter::list($opp)) { 62 | $shouldStream = true; 63 | break; 64 | } 65 | } 66 | 67 | if ($voter !== null && $shouldStream) { 68 | $operations[] = Operation::gather( 69 | $baseNamespace, 70 | $operationClassName . 'Listing', 71 | 'LIST', 72 | $method, 73 | $path, 74 | [ 75 | 'listOperation' => [ 76 | 'key' => $voter::incrementorKey(), 77 | 'initialValue' => $voter::incrementorInitialValue(), 78 | 'keys' => $voter::keys(), 79 | ], 80 | ], 81 | $operation, 82 | $throwableSchemaRegistry, 83 | $schemaRegistry, 84 | $contractRegistry, 85 | $compositSchemaRegistry, 86 | ); 87 | } 88 | } 89 | 90 | if ($voters === null || ! is_array($voters->streamOperation)) { 91 | continue; 92 | } 93 | 94 | $shouldStream = false; 95 | foreach ($voters->streamOperation as $voter) { 96 | if ($voter::stream($opp)) { 97 | $shouldStream = true; 98 | break; 99 | } 100 | } 101 | 102 | if (! $shouldStream) { 103 | continue; 104 | } 105 | 106 | $operations[] = Operation::gather( 107 | $baseNamespace, 108 | $operationClassName . 'Streaming', 109 | 'STREAM', 110 | $method, 111 | $path, 112 | [], 113 | $operation, 114 | $throwableSchemaRegistry, 115 | $schemaRegistry, 116 | $contractRegistry, 117 | $compositSchemaRegistry, 118 | ); 119 | } 120 | 121 | return new \ApiClients\Tools\OpenApiClientGenerator\Representation\Path( 122 | ClassString::factory($baseNamespace, $className), 123 | OperationHydrator::gather( 124 | $baseNamespace, 125 | $className, 126 | ...$operations, 127 | ), 128 | $operations, 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Gatherer/Property.php: -------------------------------------------------------------------------------- 1 | examples ?? []) > 0) { 44 | $examples = array_values(array_filter($property->examples, static fn (mixed $value): bool => $value !== null)); 45 | // Main reason we're doing this is so we cause more variety in the example data when a list of examples is provided, but also consistently pick the same item so we do don't cause code churn 46 | /** @phpstan-ignore-next-line */ 47 | $exampleData = $examples[strlen($sourcePropertyName) % 2 ? 0 : count($examples) - 1]; 48 | } 49 | 50 | if ($exampleData === null && $property->example !== null) { 51 | $exampleData = $property->example; 52 | } 53 | 54 | if ($exampleData === null && count($property->enum ?? []) > 0) { 55 | $enum = $property->enum; 56 | $enums = array_values(array_filter($property->enum, static fn (mixed $value): bool => $value !== null)); 57 | // Main reason we're doing this is so we cause more variety in the enum based example data, but also consistently pick the same item so we do don't cause code churn 58 | /** @phpstan-ignore-next-line */ 59 | $exampleData = $enums[strlen($sourcePropertyName) % 2 ? 0 : count($enums) - 1]; 60 | } 61 | 62 | $propertyName = str_replace([ 63 | '@', 64 | '+', 65 | '-', 66 | '$', 67 | ], [ 68 | '_AT_', 69 | '_PLUS_', 70 | '_MIN_', 71 | '_DOLLAR_', 72 | ], $sourcePropertyName); 73 | $propertyName = preg_replace_callback( 74 | '/[0-9]+/', 75 | static function ($matches) { 76 | return '_' . str_replace(['-', ' '], '_', NumberToWords::transformNumber('en', (int) $matches[0])) . '_'; 77 | }, 78 | $propertyName, 79 | ); 80 | 81 | $type = Type::gather( 82 | $baseNamespace, 83 | $className, 84 | $propertyName, 85 | $property, 86 | $required, 87 | $schemaRegistry, 88 | $contractRegistry, 89 | $compositSchemaRegistry, 90 | ); 91 | 92 | if ($property->type === 'array' && is_array($type->payload)) { 93 | $arrayItemsRaw = []; 94 | $arrayItemsNode = []; 95 | 96 | foreach ($type->payload as $index => $arrayItem) { 97 | $arrayItemExampleData = ExampleData::gather( 98 | $exampleData, 99 | $arrayItem->type === 'union' ? $arrayItem->payload[(array_key_exists($index, $arrayItem->payload) ? $index : 0)] : $arrayItem, 100 | $propertyName . str_pad('', $index + 1, '_'), 101 | ); 102 | $arrayItemsRaw[] = $arrayItemExampleData->raw; 103 | $arrayItemsNode[] = new Node\Expr\ArrayItem($arrayItemExampleData->node); 104 | } 105 | 106 | $exampleData = new Representation\ExampleData($arrayItemsRaw, new Node\Expr\Array_($arrayItemsNode)); 107 | } elseif ($type->type === 'union') { 108 | foreach ($type->payload as $index => $arrayItem) { 109 | $exampleData = ExampleData::gather( 110 | $arrayItem->payload instanceof Representation\PropertyType ? $exampleData : null, 111 | $arrayItem->payload instanceof Representation\PropertyType ? $arrayItem->payload : $arrayItem, 112 | $propertyName . str_pad('', $index + 1, '_'), 113 | ); 114 | } 115 | } else { 116 | $exampleData = ExampleData::gather($exampleData, $type, $propertyName); 117 | } 118 | 119 | return new Representation\Property( 120 | (new Convert($propertyName))->toCamel(), 121 | $sourcePropertyName, 122 | $property->description ?? '', 123 | $exampleData, 124 | $type, 125 | $type->nullable, 126 | $enum, 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Gatherer/Schema.php: -------------------------------------------------------------------------------- 1 | allOf) && count($schema->allOf) > 0) { 33 | return IntersectionSchema::gather( 34 | $baseNamespace, 35 | $className, 36 | $schema, 37 | $schemaRegistry, 38 | $contractRegistry, 39 | $compositSchemaRegistry, 40 | ); 41 | } 42 | 43 | $className = Utils::className($className); 44 | $isArray = $schema->type === 'array'; 45 | $properties = []; 46 | $example = []; 47 | 48 | if ($isArray) { 49 | $schema = $schema->items; 50 | } 51 | 52 | foreach ($schema->properties as $propertyName => $property) { 53 | $gatheredProperty = $properties[] = Property::gather( 54 | $baseNamespace, 55 | $className, 56 | (string) $propertyName, 57 | in_array( 58 | (string) $propertyName, 59 | $schema->required ?? [], 60 | false, 61 | ), 62 | $property, 63 | $schemaRegistry, 64 | $contractRegistry, 65 | $compositSchemaRegistry, 66 | ); 67 | 68 | $example[$gatheredProperty->sourceName] = $gatheredProperty->example->raw; 69 | 70 | foreach (['examples', 'example'] as $examplePropertyName) { 71 | if (array_key_exists($gatheredProperty->sourceName, $example)) { 72 | break; 73 | } 74 | 75 | if (! property_exists($schema, $examplePropertyName) || ! is_array($schema->$examplePropertyName) || ! array_key_exists($gatheredProperty->sourceName, $schema->$examplePropertyName)) { 76 | continue; 77 | } 78 | 79 | $example[$gatheredProperty->sourceName] = $schema->$examplePropertyName[$gatheredProperty->sourceName]; 80 | } 81 | 82 | foreach ($property->enum ?? [] as $value) { 83 | $example[$gatheredProperty->sourceName] = $value; 84 | break; 85 | } 86 | 87 | if ($example[$gatheredProperty->sourceName] !== null || $schema->required) { 88 | continue; 89 | } 90 | 91 | unset($example[$gatheredProperty->sourceName]); 92 | } 93 | 94 | return new \ApiClients\Tools\OpenApiClientGenerator\Representation\Schema( 95 | ClassString::factory($baseNamespace, 'Schema\\' . $className), 96 | [ 97 | new Contract( 98 | ClassString::factory( 99 | $baseNamespace, 100 | $contractRegistry->get($schema, 'Contract\\' . $className), 101 | ), 102 | $properties, 103 | ), 104 | ], 105 | ClassString::factory($baseNamespace, 'Error\\' . $className), 106 | ClassString::factory($baseNamespace, 'ErrorSchemas\\' . $className), 107 | $schema->title ?? '', 108 | $schema->description ?? '', 109 | $example, 110 | $properties, 111 | $schema, 112 | $isArray, 113 | ($schema->type === null ? ['object'] : (is_array($schema->type) ? $schema->type : [$schema->type])), 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Gatherer/Type.php: -------------------------------------------------------------------------------- 1 | type; 38 | $nullable = ! $required; 39 | 40 | if (is_array($property->allOf) && count($property->allOf) > 0) { 41 | return new PropertyType( 42 | 'object', 43 | null, 44 | null, 45 | IntersectionSchema::gather( 46 | $baseNamespace, 47 | $schemaRegistry->get( 48 | $property, 49 | Utils::className($className . '\\' . $propertyName), 50 | ), 51 | $property, 52 | $schemaRegistry, 53 | $contractRegistry, 54 | $compositSchemaRegistry, 55 | ), 56 | $nullable, 57 | ); 58 | } 59 | 60 | if (is_array($property->oneOf) && count($property->oneOf) > 0) { 61 | // Check if nullable 62 | if ( 63 | count($property->oneOf) === 2 && 64 | count(array_filter($property->oneOf, static fn (BaseSchema $schema): bool => $schema->type === 'null')) === 1 65 | ) { 66 | return self::gather( 67 | $baseNamespace, 68 | $className, 69 | $propertyName, 70 | current(array_filter($property->oneOf, static fn (BaseSchema $schema): bool => $schema->type !== 'null')), 71 | false, 72 | $schemaRegistry, 73 | $contractRegistry, 74 | $compositSchemaRegistry, 75 | ); 76 | } 77 | 78 | return new PropertyType( 79 | 'union', 80 | null, 81 | null, 82 | [ 83 | ...(static function ( 84 | Namespace_ $baseNamespace, 85 | string $className, 86 | string $propertyName, 87 | array $properties, 88 | bool $required, 89 | SchemaRegistry $schemaRegistry, 90 | ContractRegistry $contractRegistry, 91 | CompositSchemaRegistry $compositSchemaRegistry, 92 | ): iterable { 93 | foreach ($properties as $index => $property) { 94 | yield self::gather( 95 | $baseNamespace, 96 | $className, 97 | $propertyName . '\\' . NumberToWords::transformNumber('en', $index), 98 | $property, 99 | $required, 100 | $schemaRegistry, 101 | $contractRegistry, 102 | $compositSchemaRegistry, 103 | ); 104 | } 105 | })( 106 | $baseNamespace, 107 | $className, 108 | $propertyName, 109 | $property->oneOf, 110 | $required, 111 | $schemaRegistry, 112 | $contractRegistry, 113 | $compositSchemaRegistry, 114 | ), 115 | ], 116 | $nullable, 117 | ); 118 | } 119 | 120 | if (is_array($property->anyOf) && count($property->anyOf) > 0) { 121 | // Check if nullable 122 | if ( 123 | count($property->anyOf) === 2 && 124 | count(array_filter($property->anyOf, static fn (BaseSchema $schema): bool => $schema->type === 'null')) === 1 125 | ) { 126 | return self::gather( 127 | $baseNamespace, 128 | $className, 129 | $propertyName, 130 | current(array_filter($property->anyOf, static fn (BaseSchema $schema): bool => $schema->type !== 'null')), 131 | false, 132 | $schemaRegistry, 133 | $contractRegistry, 134 | $compositSchemaRegistry, 135 | ); 136 | } 137 | 138 | return new PropertyType( 139 | 'union', 140 | null, 141 | null, 142 | [ 143 | ...(static function ( 144 | Namespace_ $baseNamespace, 145 | string $className, 146 | string $propertyName, 147 | array $properties, 148 | bool $required, 149 | SchemaRegistry $schemaRegistry, 150 | ContractRegistry $contractRegistry, 151 | CompositSchemaRegistry $compositSchemaRegistry, 152 | ): iterable { 153 | foreach ($properties as $index => $property) { 154 | yield self::gather( 155 | $baseNamespace, 156 | $className, 157 | $propertyName . '\\' . NumberToWords::transformNumber('en', $index), 158 | $property, 159 | $required, 160 | $schemaRegistry, 161 | $contractRegistry, 162 | $compositSchemaRegistry, 163 | ); 164 | } 165 | })( 166 | $baseNamespace, 167 | $className, 168 | $propertyName, 169 | $property->anyOf, 170 | $required, 171 | $schemaRegistry, 172 | $contractRegistry, 173 | $compositSchemaRegistry, 174 | ), 175 | ], 176 | $nullable, 177 | ); 178 | } 179 | 180 | if ( 181 | is_array($type) && 182 | count($type) === 2 && 183 | ( 184 | in_array(null, $type, false) || 185 | in_array('null', $type, false) 186 | ) 187 | ) { 188 | foreach ($type as $pt) { 189 | /** @phpstan-ignore-next-line */ 190 | if ($pt !== null && $pt !== 'null') { 191 | $type = $pt; 192 | break; 193 | } 194 | } 195 | 196 | $nullable = true; 197 | } 198 | 199 | if ($type === 'array') { 200 | $arrayItems = []; 201 | 202 | foreach (range(0, ($property->maxItems ?? $property->minItems ?? 2) - 1) as $index) { 203 | $arrayItems[] = self::gather( 204 | $baseNamespace, 205 | $className, 206 | $propertyName, 207 | $property->items, 208 | $required, 209 | $schemaRegistry, 210 | $contractRegistry, 211 | $compositSchemaRegistry, 212 | ); 213 | } 214 | 215 | return new PropertyType( 216 | 'array', 217 | null, 218 | null, 219 | $arrayItems, 220 | $nullable, 221 | ); 222 | } 223 | 224 | if (is_string($type)) { 225 | $type = str_replace([ 226 | 'integer', 227 | 'number', 228 | 'any', 229 | 'null', 230 | 'boolean', 231 | ], [ 232 | 'int', 233 | 'int|float', 234 | '', 235 | '', 236 | 'bool', 237 | ], $type); 238 | } else { 239 | $type = ''; 240 | } 241 | 242 | if ($type === '') { 243 | return new PropertyType( 244 | 'scalar', 245 | null, 246 | null, 247 | 'string', 248 | false, 249 | ); 250 | } 251 | 252 | if ($type === 'object') { 253 | return new PropertyType( 254 | 'object', 255 | null, 256 | null, 257 | Schema::gather( 258 | $baseNamespace, 259 | $schemaRegistry->get( 260 | $property, 261 | Utils::className($className . '\\' . $propertyName), 262 | ), 263 | $property, 264 | $schemaRegistry, 265 | $contractRegistry, 266 | $compositSchemaRegistry, 267 | ), 268 | $nullable, 269 | ); 270 | } 271 | 272 | return new PropertyType( 273 | 'scalar', 274 | $property->format ?? null, 275 | $property->pattern ?? null, 276 | $type, 277 | $nullable, 278 | ); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/Gatherer/WebHook.php: -------------------------------------------------------------------------------- 1 | post?->requestBody === null && ! property_exists($webhook->post->requestBody, 'content')) { 32 | throw new RuntimeException('Missing request body content to deal with'); 33 | } 34 | 35 | [$event] = explode('/', $webhook->post->operationId); 36 | 37 | $headers = []; 38 | foreach ($webhook->post->parameters ?? [] as $header) { 39 | if ($header->in !== 'header') { 40 | continue; 41 | } 42 | 43 | $headers[] = new Header($header->name, Schema::gather( 44 | $baseNamespace, 45 | $schemaRegistry->get( 46 | $header->schema, 47 | 'WebHookHeader\\' . ucfirst(preg_replace('/\PL/u', '', $header->name)), 48 | ), 49 | $header->schema, 50 | $schemaRegistry, 51 | $contractRegistry, 52 | $compositSchemaRegistry, 53 | ), ExampleData::determiteType($header->example)); 54 | } 55 | 56 | return new \ApiClients\Tools\OpenApiClientGenerator\Representation\WebHook( 57 | $event, 58 | $webhook->post->summary ?? '', 59 | $webhook->post->description ?? '', 60 | $webhook->post->operationId, 61 | $webhook->post->externalDocs->url ?? '', 62 | $headers, 63 | iterator_to_array((static function (array $content, SchemaRegistry $schemaRegistry, ContractRegistry $contractRegistry, CompositSchemaRegistry $compositSchemaRegistry, Namespace_ $baseNamespace): iterable { 64 | foreach ($content as $type => $schema) { 65 | yield $type => Schema::gather( 66 | $baseNamespace, 67 | $schemaRegistry->get($schema->schema, 'T' . time()), 68 | $schema->schema, 69 | $schemaRegistry, 70 | $contractRegistry, 71 | $compositSchemaRegistry, 72 | ); 73 | } 74 | })($webhook->post->requestBody->content, $schemaRegistry, $contractRegistry, $compositSchemaRegistry, $baseNamespace)), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Gatherer/WebHookHydrator.php: -------------------------------------------------------------------------------- 1 | schema as $webHookSchema) { 21 | foreach (HydratorUtils::listSchemas($webHookSchema) as $schema) { 22 | $schemaClasses[] = $schema; 23 | } 24 | } 25 | } 26 | 27 | return Hydrator::gather( 28 | $baseNamespace, 29 | 'WebHook\\' . Utils::className($event), 30 | '🪝', 31 | ...$schemaClasses, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Generator/Client/Methods/ChunkCount.php: -------------------------------------------------------------------------------- 1 | $nodes */ 12 | public function __construct( 13 | public string $className, 14 | public string $returnType, 15 | public string $docBlockReturnType, 16 | public array $nodes, 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Generator/Client/PHPStan/ClientCallReturnTypesTest.php: -------------------------------------------------------------------------------- 1 | */ 24 | public static function generate(Configuration $configuration, string $pathPrefix, Representation\Client $client): iterable 25 | { 26 | $operations = []; 27 | foreach ($client->paths as $path) { 28 | $operations = [...$operations, ...$path->operations]; 29 | } 30 | 31 | $factory = new BuilderFactory(); 32 | $stmt = $factory->namespace(new Node\Name(trim($configuration->namespace->test . '\\Types', '\\'))); 33 | 34 | $stmt->addStmt( 35 | new Node\Stmt\Expression( 36 | new Expr\Assign( 37 | new Expr\Variable( 38 | new Node\Name( 39 | 'client', 40 | ), 41 | ), 42 | new Expr\New_( 43 | new Node\Name( 44 | '\\' . $configuration->namespace->source . '\\Client', 45 | ), 46 | [ 47 | new Arg( 48 | new Expr\New_( 49 | new Node\Stmt\Class_( 50 | null, 51 | [ 52 | 'implements' => [ 53 | new Node\Name('\\' . AuthenticationInterface::class), 54 | ], 55 | 'stmts' => [ 56 | $factory->method('authHeader')->setReturnType( 57 | new Node\Name('string'), 58 | )->addStmt( 59 | new Node\Stmt\Return_( 60 | new Node\Scalar\String_('Saturn V'), 61 | ), 62 | )->getNode(), 63 | ], 64 | ], 65 | ), 66 | ), 67 | ), 68 | new Arg( 69 | new Expr\New_( 70 | new Node\Name( 71 | '\\' . Browser::class, 72 | ), 73 | ), 74 | ), 75 | ], 76 | ), 77 | ), 78 | ), 79 | ); 80 | 81 | foreach ($operations as $operation) { 82 | $stmt->addStmt( 83 | new Node\Stmt\Expression( 84 | new Expr\FuncCall( 85 | new Node\Name( 86 | '\PHPStan\Testing\assertType', 87 | ), 88 | [ 89 | new Arg( 90 | new Node\Scalar\String_( 91 | str_replace(',', ', ', Operation::getDocBlockResultTypeFromOperation($operation)), 92 | ), 93 | ), 94 | new Arg( 95 | new Expr\MethodCall( 96 | new Expr\Variable( 97 | new Node\Name( 98 | 'client', 99 | ), 100 | ), 101 | new Node\Name( 102 | 'call', 103 | ), 104 | [ 105 | new Arg( 106 | new Node\Scalar\String_($operation->matchMethod . ' ' . $operation->path), 107 | ), 108 | ], 109 | ), 110 | ), 111 | ], 112 | ), 113 | ), 114 | ); 115 | } 116 | 117 | yield new File($pathPrefix, 'Types\ClientCallReturnTypes', $stmt->getNode()); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Generator/Client/Routers.php: -------------------------------------------------------------------------------- 1 | , returnType: string, docBlockReturnType: string}>>> $operations */ 20 | private array $operations = []; 21 | 22 | /** @param array $nodes */ 23 | public function add( 24 | string $method, 25 | string|null $group, 26 | string $name, 27 | string $returnType, 28 | string $docBlockReturnType, 29 | array $nodes, 30 | ): Router { 31 | $this->operations[$method][$group ?? ''][$name] = [ 32 | 'nodes' => $nodes, 33 | 'returnType' => $returnType, 34 | 'docBlockReturnType' => $docBlockReturnType, 35 | ]; 36 | 37 | return $this->createClassName($method, $group, $name); 38 | } 39 | 40 | /** @return iterable */ 41 | public function get(): iterable 42 | { 43 | foreach ($this->operations as $method => $groups) { 44 | foreach ($groups as $group => $methods) { 45 | $classMethods = []; 46 | foreach ($methods as $name => $op) { 47 | $classMethods[] = new RouterClassMethod($name, $op['returnType'], $op['docBlockReturnType'], $op['nodes']); 48 | } 49 | 50 | yield new RouterClass( 51 | $method, 52 | $group, 53 | $classMethods, 54 | ); 55 | } 56 | } 57 | } 58 | 59 | public function createClassName( 60 | string $method, 61 | string|null $group, 62 | string $name, 63 | ): Router { 64 | $className = rtrim('Internal\\Router\\' . (new Convert($method))->toPascal() . ($group === null ? '' : '\\' . (new Convert($group))->toPascal()), '\\'); 65 | 66 | return new Router( 67 | $className, 68 | (new Convert($name))->toCamel(), 69 | str_replace( 70 | '\\', 71 | '🔀', 72 | lcfirst( 73 | $className, 74 | ), 75 | ), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Generator/Client/Routers/Router.php: -------------------------------------------------------------------------------- 1 | $methods */ 10 | public function __construct( 11 | public string $method, 12 | public string $group, 13 | public array $methods, 14 | ) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Generator/Client/Routers/RouterClassMethod.php: -------------------------------------------------------------------------------- 1 | $nodes */ 12 | public function __construct( 13 | public string $name, 14 | public string $returnType, 15 | public string $docBlockReturnType, 16 | public array $nodes, 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Generator/ClientInterface.php: -------------------------------------------------------------------------------- 1 | $operations 30 | * 31 | * @return iterable 32 | */ 33 | public static function generate(Configuration $configuration, string $pathPrefix, array $operations): iterable 34 | { 35 | $factory = new BuilderFactory(); 36 | $stmt = $factory->namespace(trim($configuration->namespace->source, '\\')); 37 | 38 | $class = $factory->interface('ClientInterface'); 39 | 40 | if ($configuration->entryPoints->call) { 41 | $class->addStmt( 42 | $factory->method('call')->makePublic()->setDocComment( 43 | new Doc(implode(PHP_EOL, [ 44 | ...($configuration->qa?->phpcs ? ['// phpcs:disable'] : []), 45 | '/**', 46 | // ' * @return ' . (static function (array $operations): string { 47 | // $count = count($operations); 48 | // $lastItem = $count - 1; 49 | // $left = ''; 50 | // $right = ''; 51 | // for ($i = 0; $i < $count; $i++) { 52 | // $returnType = \ApiClients\Tools\OpenApiClientGenerator\Generator\Helper\Operation::getDocBlockResultTypeFromOperation($operations[$i]); 53 | // if ($i !== $lastItem) { 54 | // $left .= '($call is "' . $operations[$i]->matchMethod . ' ' . $operations[$i]->path . '" ? ' . $returnType . ' : '; 55 | // } else { 56 | // $left .= $returnType; 57 | // } 58 | // 59 | // $right .= ')'; 60 | // } 61 | // 62 | // return $left . $right; 63 | // })($operations), 64 | ' */', 65 | ...($configuration->qa?->phpcs ? ['// phpcs:enabled'] : []), 66 | ])), 67 | )->addParam((new Param('call'))->setType('string'))->addParam((new Param('params'))->setType('array')->setDefault([]))->setReturnType( 68 | new UnionType( 69 | array_map( 70 | static fn (string $type): Name => new Name($type), 71 | array_unique( 72 | [ 73 | ...Types::filterDuplicatesAndIncompatibleRawTypes(...(static function (array $operations): iterable { 74 | foreach ($operations as $operation) { 75 | yield from explode('|', \ApiClients\Tools\OpenApiClientGenerator\Generator\Helper\Operation::getResultTypeFromOperation($operation)); 76 | } 77 | })($operations)), 78 | ], 79 | ), 80 | ), 81 | ), 82 | ), 83 | ); 84 | } 85 | 86 | if ($configuration->entryPoints->operations) { 87 | $class->addStmt( 88 | $factory->method('operations')->setReturnType('OperationsInterface')->makePublic(), 89 | ); 90 | } 91 | 92 | if ($configuration->entryPoints->webHooks) { 93 | $class->addStmt( 94 | $factory->method('webHooks')->setReturnType('\\' . WebHooksInterface::class)->makePublic(), 95 | ); 96 | } 97 | 98 | yield new File($pathPrefix, 'ClientInterface', $stmt->addStmt($class)->getNode()); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Generator/Contract.php: -------------------------------------------------------------------------------- 1 | $aliases 30 | * 31 | * @return iterable 32 | */ 33 | public static function generate(string $pathPrefix, Representation\Contract $contract): iterable 34 | { 35 | $factory = new BuilderFactory(); 36 | 37 | $interface = $factory->interface($contract->className->className); 38 | $contractProperties = []; 39 | foreach ($contract->properties as $property) { 40 | $types = []; 41 | if ($property->type->type === 'union' && is_array($property->type->payload)) { 42 | $types[] = self::buildUnionType($property->type); 43 | } 44 | 45 | if ($property->type->type === 'array' && ! is_string($property->type->payload)) { 46 | if ($property->type->payload instanceof Representation\PropertyType) { 47 | if (! $property->type->payload->payload instanceof Representation\PropertyType) { 48 | $iterableType = $property->type->payload; 49 | if ($iterableType->payload instanceof Representation\Schema) { 50 | $iterableType = $iterableType->payload->className->fullyQualified->source; 51 | } 52 | 53 | if ($iterableType instanceof Representation\PropertyType && (($iterableType->payload instanceof Representation\PropertyType && $iterableType->payload->type === 'union') || is_array($iterableType->payload))) { 54 | $iterableType = self::buildUnionType($iterableType); 55 | } 56 | 57 | if ($iterableType instanceof Representation\PropertyType) { 58 | $iterableType = $iterableType->payload; 59 | } 60 | 61 | $compiledTYpe = ($property->nullable ? '?' : '') . 'array<' . $iterableType . '>'; 62 | $contractProperties[$property->name] = '@property ' . $compiledTYpe . ' $' . $property->name; 63 | } 64 | } elseif (is_array($property->type->payload)) { 65 | $schemaClasses = []; 66 | foreach ($property->type->payload as $payloadType) { 67 | $schemaClasses = [...$schemaClasses, ...self::getUnionTypeSchemas($payloadType)]; 68 | } 69 | 70 | if (count($schemaClasses) > 0) { 71 | $compiledTYpe = ($property->nullable ? '?' : '') . 'array<' . implode('|', array_unique([ 72 | ...(static function (Representation\Schema ...$schemas): iterable { 73 | foreach ($schemas as $schema) { 74 | yield $schema->className->fullyQualified->source; 75 | } 76 | })(...$schemaClasses), 77 | ])) . '>'; 78 | $contractProperties[$property->name] = '@property ' . $compiledTYpe . ' $' . $property->name; 79 | } 80 | } 81 | 82 | $types[] = 'array'; 83 | } elseif ($property->type->payload instanceof Representation\Schema) { 84 | $types[] = $property->type->payload->className->relative; 85 | } elseif (is_string($property->type->payload)) { 86 | $types[] = $property->type->payload; 87 | } 88 | 89 | $types = array_unique($types); 90 | 91 | $nullable = ''; 92 | if ($property->nullable) { 93 | $nullable = count($types) > 1 || count(explode('|', implode('|', $types))) > 1 ? 'null|' : '?'; 94 | } 95 | 96 | if (count($types) > 0) { 97 | if (! array_key_exists($property->name, $contractProperties)) { 98 | $contractProperties[$property->name] = '@property ' . $nullable . implode('|', $types) . ' $' . $property->name; 99 | } 100 | } else { 101 | if (! array_key_exists($property->name, $contractProperties)) { 102 | $contractProperties[$property->name] = '@property $' . $property->name; 103 | } 104 | } 105 | } 106 | 107 | if (count($contractProperties) > 0) { 108 | $interface->setDocComment('/**' . PHP_EOL . ' * ' . implode(PHP_EOL . ' * ', $contractProperties) . PHP_EOL . ' */'); 109 | } 110 | 111 | yield new File($pathPrefix, $contract->className->relative, $factory->namespace($contract->className->namespace->source)->addStmt($interface)->getNode()); 112 | } 113 | 114 | private static function buildUnionType(Representation\PropertyType $type): string 115 | { 116 | $typeList = []; 117 | if (is_array($type->payload)) { 118 | foreach ($type->payload as $typeInUnion) { 119 | $typeList[] = match (gettype($typeInUnion->payload)) { 120 | 'string' => $typeInUnion->payload, 121 | 'array' => 'array', 122 | 'object' => match ($typeInUnion->payload::class) { 123 | Representation\Schema::class => $typeInUnion->payload->className->relative, 124 | Representation\PropertyType::class => self::buildUnionType($typeInUnion->payload), 125 | }, 126 | }; 127 | } 128 | } else { 129 | $typeList[] = $type->payload; 130 | } 131 | 132 | return implode( 133 | '|', 134 | array_unique( 135 | array_filter( 136 | $typeList, 137 | static fn (string $item): bool => strlen(trim($item)) > 0, 138 | ), 139 | ), 140 | ); 141 | } 142 | 143 | /** @return iterable */ 144 | private static function getUnionTypeSchemas(Representation\PropertyType $type): iterable 145 | { 146 | if (! is_array($type->payload)) { 147 | return; 148 | } 149 | 150 | foreach ($type->payload as $typeInUnion) { 151 | if ($typeInUnion->payload instanceof Representation\Schema) { 152 | yield $typeInUnion->payload; 153 | } 154 | 155 | if (! ($typeInUnion->payload instanceof Representation\PropertyType)) { 156 | continue; 157 | } 158 | 159 | yield from self::getUnionTypeSchemas($typeInUnion->payload); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Generator/Error.php: -------------------------------------------------------------------------------- 1 | */ 15 | public static function generate(string $pathPrefix, Schema $schema): iterable 16 | { 17 | $factory = new BuilderFactory(); 18 | $stmt = $factory->namespace($schema->errorClassName->namespace->source); 19 | 20 | $class = $factory->class($schema->errorClassName->className)->extend('\\' . \Error::class)->makeFinal(); 21 | 22 | $class->addStmt((new BuilderFactory())->method('__construct')->makePublic()->addParam( 23 | (new PromotedPropertyAsParam('status'))->setType('int'), 24 | )->addParam( 25 | (new PromotedPropertyAsParam('error'))->setType($schema->className->relative), 26 | )); 27 | 28 | yield new File($pathPrefix, $schema->errorClassName->relative, $stmt->addStmt($class)->getNode()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Generator/Helper/Operation.php: -------------------------------------------------------------------------------- 1 | addParams([ 42 | ...(static function (array $params): iterable { 43 | foreach ($params as $param) { 44 | yield (new Builder\Param($param->name))->setType($param->type === '' ? 'mixed' : $param->type); 45 | } 46 | })($operation->parameters), 47 | ...(count($operation->requestBody) > 0 ? [ 48 | (new Builder\Param('params'))->setType('array'), 49 | ] : []), 50 | ]); 51 | } 52 | 53 | public static function methodReturnType(Builder\Method $method, Representation\Operation $operation): Builder\Method 54 | { 55 | $docComment = ReflectionTypes::copyDocBlock($operation->operatorClassName->fullyQualified->source, 'call'); 56 | 57 | if ($docComment !== null) { 58 | $method = $method->setDocComment($docComment); 59 | } 60 | 61 | return $method->setReturnType( 62 | ReflectionTypes::copyReturnType($operation->operatorClassName->fullyQualified->source, 'call'), 63 | ); 64 | } 65 | 66 | public static function methodCallOperation(Representation\Operation $operation): Node\Stmt\Return_ 67 | { 68 | return new Node\Stmt\Return_( 69 | new Expr\MethodCall( 70 | new Expr\MethodCall( 71 | new Node\Expr\PropertyFetch( 72 | new Node\Expr\Variable('this'), 73 | 'operators', 74 | ), 75 | $operation->operatorLookUpMethod, 76 | ), 77 | 'call', 78 | [ 79 | ...(static function (array $params): iterable { 80 | foreach ($params as $param) { 81 | yield new Arg(new Node\Expr\Variable($param->name)); 82 | } 83 | })($operation->parameters), 84 | ...(count($operation->requestBody) > 0 ? [new Arg(new Node\Expr\Variable('params'))] : []), 85 | ], 86 | ), 87 | ); 88 | } 89 | 90 | public static function getResultTypeFromOperation(Representation\Operation $operation): string 91 | { 92 | /** @phpstan-ignore-next-line */ 93 | $returnType = (new ReflectionClass($operation->className->fullyQualified->source))->getMethod('createResponse')->getReturnType(); 94 | if ($returnType === null) { 95 | return 'void'; 96 | } 97 | 98 | if ((string) $returnType === 'void') { 99 | return (string) $returnType; 100 | } 101 | 102 | return self::convertObservableIntoIterable( 103 | implode( 104 | '|', 105 | array_map( 106 | static fn (string $object): Node\Name => new Node\Name((strpos($object, '\\') > 0 ? '\\' : '') . $object), 107 | explode('|', (string) $returnType), 108 | ), 109 | ), 110 | ); 111 | } 112 | 113 | public static function getDocBlockFromOperation(Representation\Operation $operation): Doc 114 | { 115 | return new Doc( 116 | implode( 117 | PHP_EOL, 118 | [ 119 | '/**', 120 | ' * @return ' . self::getDocBlockResultTypeFromOperation($operation), 121 | ' */', 122 | ], 123 | ), 124 | ); 125 | } 126 | 127 | public static function getDocBlockResultTypeFromOperation(Representation\Operation $operation): string 128 | { 129 | /** @phpstan-ignore-next-line */ 130 | $docComment = (new ReflectionClass($operation->className->fullyQualified->source))->getMethod('createResponse')->getDocComment(); 131 | if (! is_string($docComment)) { 132 | return ''; 133 | } 134 | 135 | // basic setup 136 | 137 | $lexer = new Lexer(); 138 | $constExprParser = new ConstExprParser(); 139 | $typeParser = new TypeParser($constExprParser); 140 | $phpDocParser = new PhpDocParser($typeParser, $constExprParser); 141 | 142 | // parsing and reading a PHPDoc string 143 | $tokens = new TokenIterator($lexer->tokenize($docComment)); 144 | $phpDocNode = $phpDocParser->parse($tokens); // PhpDocNode 145 | 146 | return self::convertObservableIntoIterable( 147 | implode( 148 | '|', 149 | array_map( 150 | static fn (ReturnTagValueNode $returnTagValueNode): string => (string) $returnTagValueNode->type, 151 | $phpDocNode->getReturnTagValues(), 152 | ), 153 | ), 154 | ); 155 | } 156 | 157 | private static function convertObservableIntoIterable(string $string): string 158 | { 159 | return str_replace( 160 | [ 161 | '\\' . Observable::class . '<', 162 | '\\' . Observable::class, 163 | '(', 164 | ' ', 165 | ')', 166 | ], 167 | [ 168 | 'iterable */ 61 | public static function uniqueSchemas(string|Schema|PropertyType ...$propertyTypes): iterable 62 | { 63 | $schemas = []; 64 | 65 | foreach ($propertyTypes as $propertyType) { 66 | if (is_string($propertyType)) { 67 | $schemas[$propertyType] = $propertyType; 68 | continue; 69 | } 70 | 71 | if ($propertyType instanceof Schema) { 72 | $schemas[$propertyType->className->fullyQualified->source] = $propertyType; 73 | continue; 74 | } 75 | 76 | foreach ( 77 | static::uniqueSchemas(...is_array($propertyType->payload) ? $propertyType->payload : [$propertyType->payload]) as $nestedPropertyType 78 | ) { 79 | $schemas[$nestedPropertyType instanceof Schema ? $nestedPropertyType->className->fullyQualified->source : $nestedPropertyType] = $nestedPropertyType; 80 | } 81 | } 82 | 83 | yield from $schemas; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Generator/Helper/ReflectionTypes.php: -------------------------------------------------------------------------------- 1 | getMethod($method)->getReturnType(); 24 | switch ($reflection::class) { 25 | //ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType 26 | case ReflectionNamedType::class: 27 | return new Name(str_replace( 28 | 'Traversable', 29 | 'iterable', 30 | (strpos((string) $reflection, '\\') !== false ? '\\' : '') . $reflection, 31 | )); 32 | 33 | break; 34 | case ReflectionUnionType::class: 35 | return new Node\UnionType( 36 | [ 37 | ...(static function (string ...$types): iterable { 38 | foreach ($types as $type) { 39 | if ($type === 'array') { 40 | continue; 41 | } 42 | 43 | yield new Name(str_replace( 44 | 'Traversable', 45 | 'iterable', 46 | (strpos($type, '\\') !== false ? '\\' : '') . $type, 47 | )); 48 | } 49 | })(...[ 50 | ...Types::filterDuplicatesAndIncompatibleRawTypes(...array_map( 51 | static fn (ReflectionType $type): string => (string) $type, 52 | $reflection->getTypes(), 53 | )), 54 | ]), 55 | ], 56 | ); 57 | 58 | break; 59 | default: 60 | return ''; 61 | } 62 | } 63 | 64 | public static function copyDocBlock(string $class, string $method): Doc|null 65 | { 66 | $comment = (new ReflectionClass($class))->getMethod($method)->getDocComment(); 67 | if ($comment !== null) { 68 | return new Doc($comment); 69 | } 70 | 71 | return null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Generator/Helper/ResultConverter.php: -------------------------------------------------------------------------------- 1 | */ 14 | public static function convert(Node\Expr $expr): iterable 15 | { 16 | yield new Node\Stmt\Expression( 17 | new Node\Expr\Assign( 18 | new Node\Expr\Variable('result'), 19 | new Node\Expr\FuncCall( 20 | new Node\Name('\React\Async\await'), 21 | [ 22 | new Node\Arg($expr), 23 | ], 24 | ), 25 | ), 26 | ); 27 | 28 | yield new Node\Stmt\If_( 29 | new Node\Expr\Instanceof_( 30 | new Node\Expr\Variable('result'), 31 | new Node\Name('\\' . Observable::class), 32 | ), 33 | [ 34 | 'stmts' => [ 35 | new Node\Stmt\Expression( 36 | new Node\Expr\Assign( 37 | new Node\Expr\Variable('result'), 38 | new Node\Expr\FuncCall( 39 | new Node\Name('\WyriHaximus\React\awaitObservable'), 40 | [ 41 | new Arg(new Node\Expr\Variable('result')), 42 | ], 43 | ), 44 | ), 45 | ), 46 | ], 47 | ], 48 | ); 49 | 50 | yield new Node\Stmt\Return_( 51 | new Node\Expr\Variable('result'), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Generator/Helper/Types.php: -------------------------------------------------------------------------------- 1 | */ 24 | public static function normalizeDocBlock(string ...$types): array 25 | { 26 | return array_map( 27 | static fn (string $type): string => in_array($type, self::SCALARS) || substr($type, 0, 1) === '\\' || substr($type, 0, 5) === 'array' ? $type : 'Schema\\' . $type, 28 | $types, 29 | ); 30 | } 31 | 32 | /** @return array */ 33 | public static function normalizeRaw(string ...$types): array 34 | { 35 | return array_map( 36 | static function (string $type): string { 37 | if (in_array($type, self::SCALARS) || substr($type, 0, 1) === '\\') { 38 | return $type; 39 | } 40 | 41 | return 'Schema\\' . $type; 42 | }, 43 | $types, 44 | ); 45 | } 46 | 47 | /** @return array */ 48 | public static function normalizeNodeName(string ...$types): array 49 | { 50 | return array_map( 51 | static fn (string $type): Node\Name => new Node\Name($type), 52 | self::normalizeRaw(...$types), 53 | ); 54 | } 55 | 56 | /** @return iterable */ 57 | public static function filterDuplicatesAndIncompatibleRawTypes(string ...$types): iterable 58 | { 59 | $keepVoid = true; 60 | $keepArray = false; 61 | $duplicates = []; 62 | 63 | foreach ($types as $type) { 64 | if ($type !== 'array') { 65 | continue; 66 | } 67 | 68 | $keepArray = true; 69 | } 70 | 71 | foreach ($types as $type) { 72 | if ($type !== 'void') { 73 | $keepVoid = false; 74 | } 75 | 76 | if ($type === 'iterable') { 77 | $keepArray = false; 78 | } 79 | 80 | if ($type === 'void' || $type === 'array') { 81 | continue; 82 | } 83 | 84 | if (array_key_exists($type, $duplicates)) { 85 | continue; 86 | } 87 | 88 | yield $type; 89 | 90 | $duplicates[$type] = true; 91 | } 92 | 93 | if ($keepArray) { 94 | yield 'array'; 95 | 96 | return; 97 | } 98 | 99 | if (! $keepVoid) { 100 | return; 101 | } 102 | 103 | yield 'void'; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Generator/Hydrator.php: -------------------------------------------------------------------------------- 1 | */ 19 | public static function generate(string $pathPrefix, \ApiClients\Tools\OpenApiClientGenerator\Representation\Hydrator $hydrator): iterable 20 | { 21 | $schemaClasses = []; 22 | 23 | foreach ($hydrator->schemas as $schema) { 24 | $schemaClasses[] = trim($schema->className->fullyQualified->source, '\\'); 25 | } 26 | 27 | if (count($schemaClasses) <= 0) { 28 | return; 29 | } 30 | 31 | yield new File( 32 | $pathPrefix, 33 | $hydrator->className->relative, 34 | (new ObjectMapperCodeGenerator())->dump( 35 | array_unique( 36 | array_filter( 37 | $schemaClasses, 38 | static fn (string $className): bool => count((new ReflectionMethod($className, '__construct'))->getParameters()) > 0, 39 | ), 40 | ), 41 | trim($hydrator->className->fullyQualified->source, '\\'), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Generator/Operations.php: -------------------------------------------------------------------------------- 1 | $paths 24 | * @param array $operations 25 | * 26 | * @return iterable 27 | */ 28 | public static function generate(Configuration $configuration, string $pathPrefix, array $paths, array $operations): iterable 29 | { 30 | $operationHydratorMap = []; 31 | foreach ($paths as $path) { 32 | foreach ($path->operations as $pathOperation) { 33 | $operationHydratorMap[$pathOperation->operationId] = $path->hydrator; 34 | } 35 | } 36 | 37 | $groups = []; 38 | foreach ($operations as $operation) { 39 | $groups[$operation->group][] = $operation; 40 | } 41 | 42 | $factory = new BuilderFactory(); 43 | $stmt = $factory->namespace($configuration->namespace->source); 44 | 45 | $class = $factory->class('Operations')->makeFinal()->implement(new Name('OperationsInterface'))->makeReadonly(); 46 | 47 | $class->addStmt( 48 | $factory->method('__construct')->makePublic()->addParam( 49 | (new PrivatePromotedPropertyAsParam('operators'))->setType('Internal\\Operators'), 50 | ), 51 | ); 52 | 53 | foreach ($groups as $group => $groupsOperations) { 54 | if ($group === '') { 55 | foreach ($groupsOperations as $groupsOperation) { 56 | $class->addStmt( 57 | Helper\Operation::methodSignature( 58 | $factory->method((new Convert($groupsOperation->name))->toCamel())->makePublic(), 59 | $groupsOperation, 60 | )->addStmt(Helper\Operation::methodCallOperation($groupsOperation)), 61 | ); 62 | } 63 | 64 | continue; 65 | } 66 | 67 | $class->addStmt( 68 | $factory->method((new Convert($group))->toCamel())->makePublic()->setReturnType('Operation\\' . $group)->addStmts([ 69 | new Node\Stmt\Return_( 70 | new Expr\New_( 71 | new Name( 72 | 'Operation\\' . $group, 73 | ), 74 | [ 75 | new Arg( 76 | new Expr\PropertyFetch( 77 | new Expr\Variable('this'), 78 | 'operators', 79 | ), 80 | ), 81 | ], 82 | ), 83 | ), 84 | ]), 85 | ); 86 | 87 | yield from self::generateOperationsGroup( 88 | $pathPrefix, 89 | $configuration->namespace, 90 | 'Operation\\' . $group, 91 | $groupsOperations, 92 | $group, 93 | ); 94 | } 95 | 96 | yield from Operators::generate($configuration, $pathPrefix, $operations, $operationHydratorMap); 97 | yield new File($pathPrefix, 'Operations', $stmt->addStmt($class)->getNode()); 98 | } 99 | 100 | /** 101 | * @param array $operations 102 | * @param array $operationHydratorMap 103 | * 104 | * @return iterable 105 | */ 106 | private static function generateOperationsGroup(string $pathPrefix, Configuration\Namespace_ $namespace, string $className, array $operations, string $group): iterable 107 | { 108 | $factory = new BuilderFactory(); 109 | $stmt = $factory->namespace(Utils::dirname($namespace->source . '\\' . $className)); 110 | 111 | $class = $factory->class(Utils::basename($className))->makeFinal()->addStmt( 112 | $factory->method('__construct')->makePublic()->addParam( 113 | (new PrivatePromotedPropertyAsParam('operators'))->setType('Internal\Operators'), 114 | ), 115 | ); 116 | 117 | foreach ($operations as $operation) { 118 | if ($operation->group !== $group) { 119 | continue; 120 | } 121 | 122 | $class->addStmt( 123 | Helper\Operation::methodSignature( 124 | $factory->method((new Convert($operation->name))->toCamel())->makePublic(), 125 | $operation, 126 | )->addStmt(Helper\Operation::methodCallOperation($operation)), 127 | ); 128 | } 129 | 130 | yield new File($pathPrefix, $className, $stmt->addStmt($class)->getNode()); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Generator/OperationsInterface.php: -------------------------------------------------------------------------------- 1 | $operations 19 | * 20 | * @return iterable 21 | */ 22 | public static function generate(Configuration $configuration, string $pathPrefix, array $operations): iterable 23 | { 24 | $factory = new BuilderFactory(); 25 | $stmt = $factory->namespace($configuration->namespace->source); 26 | $class = $factory->interface('OperationsInterface'); 27 | 28 | /** @var array> $groups */ 29 | $groups = []; 30 | foreach ($operations as $operation) { 31 | $groups[$operation->group][] = $operation; 32 | } 33 | 34 | foreach ($groups as $group => $groupOperations) { 35 | if (strlen($group) > 0) { 36 | $class->addStmt( 37 | $factory->method((new Convert($group))->toCamel())->makePublic()->setReturnType('Operation\\' . $group), 38 | ); 39 | continue; 40 | } 41 | 42 | foreach ($groupOperations as $groupOperation) { 43 | $class->addStmt( 44 | Helper\Operation::methodSignature( 45 | $factory->method($groupOperation->nameCamel)->makePublic(), 46 | $groupOperation, 47 | ), 48 | ); 49 | } 50 | } 51 | 52 | yield new File($pathPrefix, 'OperationsInterface', $stmt->addStmt($class)->getNode()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Generator/Operators.php: -------------------------------------------------------------------------------- 1 | $operations 25 | * @param array $operationHydratorMap 26 | * 27 | * @return iterable 28 | */ 29 | public static function generate(Configuration $configuration, string $pathPrefix, array $operations, array $operationHydratorMap): iterable 30 | { 31 | $factory = new BuilderFactory(); 32 | $stmt = $factory->namespace(trim($configuration->namespace->source, '\\') . '\\Internal'); 33 | 34 | $class = $factory->class('Operators')->makeFinal()->addStmt( 35 | $factory->method('__construct')->makePublic()->addParam( 36 | (new PrivatePromotedPropertyAsParam('authentication'))->setType('\\' . AuthenticationInterface::class)->makeReadonly(), 37 | )->addParam( 38 | (new PrivatePromotedPropertyAsParam('browser'))->setType('\\' . Browser::class)->makeReadonly(), 39 | )->addParam( 40 | (new PrivatePromotedPropertyAsParam('requestSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly(), 41 | )->addParam( 42 | (new PrivatePromotedPropertyAsParam('responseSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly(), 43 | )->addParam( 44 | (new PrivatePromotedPropertyAsParam('hydrators'))->setType('Internal\\Hydrators')->makeReadonly(), 45 | ), 46 | ); 47 | 48 | foreach ($operations as $operation) { 49 | $class->addStmts([ 50 | $factory->property($operation->operatorLookUpMethod)->setType('?' . $operation->operatorClassName->relative)->setDefault(null)->makePrivate(), 51 | $factory->method($operation->operatorLookUpMethod)->setReturnType($operation->operatorClassName->relative)->makePublic()->addStmts([ 52 | new Node\Stmt\If_( 53 | new Node\Expr\BinaryOp\Identical( 54 | new Node\Expr\Instanceof_( 55 | new Node\Expr\PropertyFetch( 56 | new Node\Expr\Variable('this'), 57 | $operation->operatorLookUpMethod, 58 | ), 59 | new Node\Name($operation->operatorClassName->relative), 60 | ), 61 | new Node\Expr\ConstFetch(new Node\Name('false')), 62 | ), 63 | [ 64 | 'stmts' => [ 65 | new Node\Stmt\Expression( 66 | new Node\Expr\Assign( 67 | new Node\Expr\PropertyFetch( 68 | new Node\Expr\Variable('this'), 69 | $operation->operatorLookUpMethod, 70 | ), 71 | new Node\Expr\New_( 72 | new Node\Name($operation->operatorClassName->relative), 73 | [ 74 | ...(static function (Operation $operation, array $operationHydratorMap): iterable { 75 | foreach ((new ReflectionClass($operation->operatorClassName->fullyQualified->source))->getConstructor()->getParameters() as $parameter) { 76 | if ($parameter->name === 'hydrator') { 77 | yield new Arg( 78 | new Node\Expr\MethodCall( 79 | new Node\Expr\PropertyFetch( 80 | new Node\Expr\Variable('this'), 81 | 'hydrators', 82 | ), 83 | 'getObjectMapper' . ucfirst($operationHydratorMap[$operation->operationId]->methodName), 84 | ), 85 | ); 86 | continue; 87 | } 88 | 89 | yield new Arg( 90 | new Node\Expr\PropertyFetch( 91 | new Node\Expr\Variable('this'), 92 | $parameter->name, 93 | ), 94 | ); 95 | } 96 | })($operation, $operationHydratorMap), 97 | ], 98 | ), 99 | ), 100 | ), 101 | ], 102 | ], 103 | ), 104 | new Node\Stmt\Return_( 105 | new Node\Expr\PropertyFetch( 106 | new Node\Expr\Variable('this'), 107 | $operation->operatorLookUpMethod, 108 | ), 109 | ), 110 | ]), 111 | ]); 112 | } 113 | 114 | yield new File($pathPrefix, 'Internal\\Operators', $stmt->addStmt($class)->getNode()); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Generator/Routers.php: -------------------------------------------------------------------------------- 1 | */ 22 | public static function generate(Configuration $configuration, string $pathPrefix, ClientRouters $routers): iterable 23 | { 24 | $factory = new BuilderFactory(); 25 | $stmt = $factory->namespace(trim($configuration->namespace->source, '\\') . '\\Internal'); 26 | 27 | $class = $factory->class('Routers')->makeFinal()->addStmt( 28 | $factory->method('__construct')->makePublic()->addParam( 29 | (new PrivatePromotedPropertyAsParam('authentication'))->setType('\\' . AuthenticationInterface::class)->makeReadonly(), 30 | )->addParam( 31 | (new PrivatePromotedPropertyAsParam('browser'))->setType('\\' . Browser::class)->makeReadonly(), 32 | )->addParam( 33 | (new PrivatePromotedPropertyAsParam('requestSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly(), 34 | )->addParam( 35 | (new PrivatePromotedPropertyAsParam('responseSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly(), 36 | )->addParam( 37 | (new PrivatePromotedPropertyAsParam('hydrators'))->setType('Internal\\Hydrators')->makeReadonly(), 38 | ), 39 | ); 40 | 41 | foreach ($routers->get() as $group) { 42 | $router = $routers->createClassName($group->method, $group->group, ''); 43 | $class->addStmts([ 44 | $factory->property($router->loopUpMethod)->setType('?' . $router->class)->setDefault(null)->makePrivate(), 45 | $factory->method($router->loopUpMethod)->setReturnType($router->class)->makePublic()->addStmts([ 46 | new Node\Stmt\If_( 47 | new Node\Expr\BinaryOp\Identical( 48 | new Node\Expr\Instanceof_( 49 | new Node\Expr\PropertyFetch( 50 | new Node\Expr\Variable('this'), 51 | $router->loopUpMethod, 52 | ), 53 | new Node\Name($router->class), 54 | ), 55 | new Node\Expr\ConstFetch(new Node\Name('false')), 56 | ), 57 | [ 58 | 'stmts' => [ 59 | new Node\Stmt\Expression( 60 | new Node\Expr\Assign( 61 | new Node\Expr\PropertyFetch( 62 | new Node\Expr\Variable('this'), 63 | $router->loopUpMethod, 64 | ), 65 | new Node\Expr\New_( 66 | new Node\Name($router->class), 67 | [ 68 | new Arg( 69 | new Node\Expr\PropertyFetch( 70 | new Node\Expr\Variable('this'), 71 | 'browser', 72 | ), 73 | false, 74 | false, 75 | [], 76 | new Node\Identifier('browser'), 77 | ), 78 | new Arg( 79 | new Node\Expr\PropertyFetch( 80 | new Node\Expr\Variable('this'), 81 | 'authentication', 82 | ), 83 | false, 84 | false, 85 | [], 86 | new Node\Identifier('authentication'), 87 | ), 88 | new Arg( 89 | new Node\Expr\PropertyFetch( 90 | new Node\Expr\Variable('this'), 91 | 'requestSchemaValidator', 92 | ), 93 | false, 94 | false, 95 | [], 96 | new Node\Identifier('requestSchemaValidator'), 97 | ), 98 | new Arg( 99 | new Node\Expr\PropertyFetch( 100 | new Node\Expr\Variable('this'), 101 | 'responseSchemaValidator', 102 | ), 103 | false, 104 | false, 105 | [], 106 | new Node\Identifier('responseSchemaValidator'), 107 | ), 108 | new Arg( 109 | new Node\Expr\PropertyFetch( 110 | new Node\Expr\Variable('this'), 111 | 'hydrators', 112 | ), 113 | false, 114 | false, 115 | [], 116 | new Node\Identifier('hydrators'), 117 | ), 118 | ], 119 | ), 120 | ), 121 | ), 122 | ], 123 | ], 124 | ), 125 | new Node\Stmt\Return_( 126 | new Node\Expr\PropertyFetch( 127 | new Node\Expr\Variable('this'), 128 | $router->loopUpMethod, 129 | ), 130 | ), 131 | ]), 132 | ]); 133 | } 134 | 135 | yield new File($pathPrefix, 'Internal\\Routers', $stmt->addStmt($class)->getNode()); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Generator/Schema/MultipleCastUnionToType.php: -------------------------------------------------------------------------------- 1 | */ 20 | public static function generate(string $pathPrefix, ClassString $classString, ClassString $wrappingClassString, Schema ...$schemas): iterable 21 | { 22 | $factory = new BuilderFactory(); 23 | $stmt = $factory->namespace($classString->namespace->source); 24 | 25 | $class = $factory->class($classString->className)->makeFinal()->makeReadonly()->addAttribute( 26 | new Node\Attribute( 27 | new Node\Name('\\' . Attribute::class), 28 | [ 29 | new Node\Arg( 30 | new Node\Expr\ClassConstFetch( 31 | new Node\Name('\\' . Attribute::class), 32 | 'TARGET_PARAMETER', 33 | ), 34 | ), 35 | ], 36 | ), 37 | )->implement('\\' . PropertyCaster::class)->addStmt( 38 | $factory->property('wrappedCaster')->makePrivate()->setType($wrappingClassString->fullyQualified->source), 39 | )->addStmt( 40 | $factory->method('__construct')->makePublic()->addStmts([ 41 | new Node\Stmt\Expression( 42 | new Node\Expr\Assign( 43 | new Node\Expr\PropertyFetch( 44 | new Node\Expr\Variable( 45 | new Node\Name('this'), 46 | ), 47 | new Node\Name('wrappedCaster'), 48 | ), 49 | new Node\Expr\New_( 50 | new Node\Name( 51 | $wrappingClassString->fullyQualified->source, 52 | ), 53 | ), 54 | ), 55 | ), 56 | ]), 57 | )->addStmt( 58 | $factory->method('cast')->makePublic()->addParams([ 59 | (new Param('value'))->setType('mixed'), 60 | (new Param('hydrator'))->setType('\\' . ObjectMapper::class), 61 | ])->setReturnType('mixed')->addStmts([ 62 | new Node\Stmt\Expression( 63 | new Node\Expr\Assign( 64 | new Node\Expr\Variable( 65 | new Node\Name('data'), 66 | ), 67 | new Node\Expr\Array_(), 68 | ), 69 | ), 70 | new Node\Stmt\Expression( 71 | new Node\Expr\Assign( 72 | new Node\Expr\Variable( 73 | new Node\Name('values'), 74 | ), 75 | new Node\Expr\Variable( 76 | new Node\Name('value'), 77 | ), 78 | ), 79 | ), 80 | new Node\Expr\FuncCall( 81 | new Node\Name('unset'), 82 | [ 83 | new Node\Arg( 84 | new Node\Expr\Variable( 85 | new Node\Name('value'), 86 | ), 87 | ), 88 | ], 89 | ), 90 | new Node\Stmt\Foreach_( 91 | new Node\Expr\Variable( 92 | new Node\Name('values'), 93 | ), 94 | new Node\Expr\Variable( 95 | new Node\Name('value'), 96 | ), 97 | [ 98 | 'stmts' => [ 99 | new Node\Stmt\Expression( 100 | new Node\Expr\Assign( 101 | new Node\Expr\ArrayDimFetch( 102 | new Node\Expr\Variable( 103 | new Node\Name('values'), 104 | ), 105 | ), 106 | new Node\Expr\MethodCall( 107 | new Node\Expr\PropertyFetch( 108 | new Node\Expr\Variable( 109 | new Node\Name('this'), 110 | ), 111 | new Node\Name('wrappedCaster'), 112 | ), 113 | new Node\Name('cast'), 114 | [ 115 | new Node\Arg( 116 | new Node\Expr\Variable( 117 | new Node\Name('value'), 118 | ), 119 | ), 120 | new Node\Arg( 121 | new Node\Expr\Variable( 122 | new Node\Name('hydrator'), 123 | ), 124 | ), 125 | ], 126 | ), 127 | ), 128 | ), 129 | ], 130 | ], 131 | ), 132 | new Node\Stmt\Return_( 133 | new Node\Expr\Variable('data'), 134 | ), 135 | ]), 136 | ); 137 | 138 | yield new File($pathPrefix, $classString->relative, $stmt->addStmt($class)->getNode()); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Generator/Schema/SingleCastUnionToType.php: -------------------------------------------------------------------------------- 1 | */ 27 | public static function generate(string $pathPrefix, ClassString $classString, Schema ...$schemas): iterable 28 | { 29 | $factory = new BuilderFactory(); 30 | $stmt = $factory->namespace($classString->namespace->source); 31 | 32 | $class = $factory->class($classString->className)->makeFinal()->addAttribute( 33 | new Node\Attribute( 34 | new Node\Name('\\' . Attribute::class), 35 | [ 36 | new Node\Arg( 37 | new Node\Expr\ClassConstFetch( 38 | new Node\Name('\\' . Attribute::class), 39 | 'TARGET_PARAMETER', 40 | ), 41 | ), 42 | ], 43 | ), 44 | )->implement('\\' . PropertyCaster::class)->addStmt( 45 | (new BuilderFactory())->method('cast')->makePublic()->addParams([ 46 | (new Param('value'))->setType('mixed'), 47 | (new Param('hydrator'))->setType('\\' . ObjectMapper::class), 48 | ])->setReturnType('mixed')->addStmts([ 49 | new Node\Stmt\If_( 50 | new Node\Expr\FuncCall( 51 | new Node\Name('\is_array'), 52 | [ 53 | new Node\Arg( 54 | new Node\Expr\Variable('value'), 55 | ), 56 | ], 57 | ), 58 | [ 59 | 'stmts' => [ 60 | new Node\Stmt\Expression( 61 | new Node\Expr\Assign( 62 | new Node\Expr\Variable('signatureChunks'), 63 | new Node\Expr\FuncCall( 64 | new Node\Name('\array_unique'), 65 | [ 66 | new Node\Arg( 67 | new Node\Expr\FuncCall( 68 | new Node\Name('\array_keys'), 69 | [ 70 | new Node\Arg( 71 | new Node\Expr\Variable('value'), 72 | ), 73 | ], 74 | ), 75 | ), 76 | ], 77 | ), 78 | ), 79 | ), 80 | new Node\Stmt\Expression( 81 | new Node\Expr\FuncCall( 82 | new Node\Name('\sort'), 83 | [ 84 | new Node\Arg( 85 | new Node\Expr\Variable('signatureChunks'), 86 | ), 87 | ], 88 | ), 89 | ), 90 | new Node\Stmt\Expression( 91 | new Node\Expr\Assign( 92 | new Node\Expr\Variable('signature'), 93 | new Node\Expr\FuncCall( 94 | new Node\Name('\implode'), 95 | [ 96 | new Node\Arg( 97 | new Node\Scalar\String_('|'), 98 | ), 99 | new Node\Arg( 100 | new Node\Expr\Variable('signatureChunks'), 101 | ), 102 | ], 103 | ), 104 | ), 105 | ), 106 | ...(static function (Schema ...$schemas): iterable { 107 | foreach ($schemas as $schema) { 108 | $condition = new Node\Expr\BinaryOp\Identical( 109 | new Node\Expr\Variable('signature'), 110 | new Node\Scalar\String_( 111 | implode( 112 | '|', 113 | [ 114 | ...(static function (Property ...$properties): iterable { 115 | $names = []; 116 | foreach ($properties as $property) { 117 | $names[] = $property->sourceName; 118 | } 119 | 120 | sort($names); 121 | 122 | return $names; 123 | })(...$schema->properties), 124 | ], 125 | ), 126 | ), 127 | ); 128 | foreach ($schema->properties as $property) { 129 | $enumConditionals = []; 130 | foreach ($property->enum as $enumPossibility) { 131 | $enumConditionals[] = new Node\Expr\BinaryOp\Identical( 132 | new Node\Expr\ArrayDimFetch( 133 | new Node\Expr\Variable('value'), 134 | new Node\Scalar\String_($property->sourceName), 135 | ), 136 | new Node\Scalar\String_($enumPossibility), 137 | ); 138 | } 139 | 140 | if (count($enumConditionals) <= 0) { 141 | continue; 142 | } 143 | 144 | $enumCondition = array_shift($enumConditionals); 145 | foreach ($enumConditionals as $enumConditional) { 146 | $enumCondition = new Node\Expr\BinaryOp\BooleanOr( 147 | $enumCondition, 148 | $enumConditional, 149 | ); 150 | } 151 | 152 | $condition = new Node\Expr\BinaryOp\BooleanAnd( 153 | $condition, 154 | $enumCondition, 155 | ); 156 | } 157 | 158 | yield new Node\Stmt\If_( 159 | $condition, 160 | [ 161 | 'stmts' => [ 162 | new Node\Stmt\TryCatch([ 163 | new Node\Stmt\Return_( 164 | new Node\Expr\MethodCall( 165 | new Node\Expr\Variable('hydrator'), 166 | 'hydrateObject', 167 | [ 168 | new Node\Arg( 169 | new Node\Expr\ClassConstFetch( 170 | new Node\Name($schema->className->relative), 171 | 'class', 172 | ), 173 | ), 174 | new Node\Arg( 175 | new Node\Expr\Variable('value'), 176 | ), 177 | ], 178 | ), 179 | ), 180 | ], [ 181 | new Node\Stmt\Catch_( 182 | [new Node\Name('\\' . Throwable::class)], 183 | ), 184 | ]), 185 | ], 186 | ], 187 | ); 188 | } 189 | })(...$schemas), 190 | ], 191 | ], 192 | ), 193 | new Node\Stmt\Return_( 194 | new Node\Expr\Variable('value'), 195 | ), 196 | ]), 197 | ); 198 | 199 | yield new File($pathPrefix, $classString->relative, $stmt->addStmt($class)->getNode()); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Generator/WebHook.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public static function generate( 36 | string $pathPrefix, 37 | string $namespace, 38 | string $event, 39 | SchemaRegistry $schemaRegistry, 40 | \ApiClients\Tools\OpenApiClientGenerator\Representation\WebHook ...$webHooks, 41 | ): iterable { 42 | $className = Utils::className($event); 43 | 44 | $factory = new BuilderFactory(); 45 | $stmt = $factory->namespace(ltrim($namespace . 'Internal\WebHook', '\\')); 46 | 47 | $class = $factory->class(ltrim($className, '\\'))->makeFinal()->implement('\\' . WebHookInterface::class)->setDocComment(new Doc(implode(PHP_EOL, [ 48 | '/**', 49 | ' * @internal', 50 | ' */', 51 | ]))); 52 | $class->addStmt($factory->property('requestSchemaValidator')->setType('\\' . SchemaValidator::class)->makeReadonly()->makePrivate()); 53 | $class->addStmt($factory->property('hydrator')->setType('Internal\\Hydrator\\WebHook\\' . $className)->makeReadonly()->makePrivate()); 54 | 55 | $constructor = $factory->method('__construct')->makePublic()->addParam( 56 | (new Param('requestSchemaValidator'))->setType('\\' . SchemaValidator::class), 57 | )->addParam( 58 | (new Param('hydrator'))->setType('Internal\\Hydrator\\WebHook\\' . $className), 59 | )->addStmt( 60 | new Node\Expr\Assign( 61 | new Node\Expr\PropertyFetch( 62 | new Node\Expr\Variable('this'), 63 | 'requestSchemaValidator', 64 | ), 65 | new Node\Expr\Variable('requestSchemaValidator'), 66 | ), 67 | )->addStmt( 68 | new Node\Expr\Assign( 69 | new Node\Expr\PropertyFetch( 70 | new Node\Expr\Variable('this'), 71 | 'hydrator', 72 | ), 73 | new Node\Expr\Variable('hydrator'), 74 | ), 75 | ); 76 | $class->addStmt($constructor); 77 | 78 | $resolveReturnTypes = []; 79 | $method = $factory->method('resolve')->makePublic()->setReturnType('object')->addParam( 80 | (new Param('headers'))->setType('array'), 81 | )->addParam( 82 | (new Param('data'))->setType('array'), 83 | ); 84 | $gotoLabels = 'actions_aaaaa'; 85 | $tmts = []; 86 | $tmts[] = new Node\Expr\Assign( 87 | new Node\Expr\Variable('error'), 88 | new Node\Expr\New_( 89 | new Node\Name('\\' . RuntimeException::class), 90 | [ 91 | new Arg(new Node\Scalar\String_('No action matching given headers and data')), 92 | ], 93 | ), 94 | ); 95 | 96 | foreach ($webHooks as $webHook) { 97 | $headers = []; 98 | foreach ($webHook->headers as $header) { 99 | $headers[] = new Node\Stmt\Expression(new Node\Expr\MethodCall( 100 | new Node\Expr\PropertyFetch( 101 | new Node\Expr\Variable('this'), 102 | 'requestSchemaValidator', 103 | ), 104 | 'validate', 105 | [ 106 | new Node\Arg(new Node\Expr\ArrayDimFetch( 107 | new Node\Expr\Variable('headers'), 108 | new Node\Scalar\String_(strtolower($header->name)), 109 | )), 110 | new Node\Arg(new Node\Expr\StaticCall(new Node\Name('\cebe\openapi\Reader'), 'readFromJson', [ 111 | new Node\Arg(new Node\Expr\ClassConstFetch( 112 | new Node\Name($header->schema->className->relative), 113 | 'SCHEMA_JSON', 114 | )), 115 | new Node\Arg(new Node\Scalar\String_('\cebe\openapi\spec\Schema')), 116 | ])), 117 | ], 118 | )); 119 | } 120 | 121 | foreach ($webHook->schema as $contentTYpe => $schema) { 122 | $resolveReturnTypes[] = $schema->className->relative; 123 | $tmts[] = new Node\Stmt\If_( 124 | new Node\Expr\BinaryOp\Equal( 125 | new Node\Expr\ArrayDimFetch(new Node\Expr\Variable('headers'), new Node\Scalar\String_('content-type')), 126 | new Node\Scalar\String_($contentTYpe), 127 | ), 128 | [ 129 | 'stmts' => [ 130 | new Node\Stmt\TryCatch([ 131 | ...$headers, 132 | new Node\Stmt\Expression(new Node\Expr\MethodCall( 133 | new Node\Expr\PropertyFetch( 134 | new Node\Expr\Variable('this'), 135 | 'requestSchemaValidator', 136 | ), 137 | 'validate', 138 | [ 139 | new Node\Arg(new Node\Expr\Variable('data')), 140 | new Node\Arg(new Node\Expr\StaticCall(new Node\Name('\cebe\openapi\Reader'), 'readFromJson', [ 141 | new Arg(new Node\Expr\ClassConstFetch( 142 | new Node\Name($schema->className->relative), 143 | 'SCHEMA_JSON', 144 | )), 145 | new Arg(new Node\Scalar\String_('\cebe\openapi\spec\Schema')), 146 | ])), 147 | ], 148 | )), 149 | new Node\Stmt\Return_(new Node\Expr\MethodCall( 150 | new Node\Expr\PropertyFetch( 151 | new Node\Expr\Variable('this'), 152 | 'hydrator', 153 | ), 154 | 'hydrateObject', 155 | [ 156 | new Node\Arg(new Node\Expr\ClassConstFetch( 157 | new Node\Name($schema->className->relative), 158 | 'class', 159 | )), 160 | new Node\Arg(new Node\Expr\Variable('data')), 161 | ], 162 | )), 163 | ], [ 164 | new Node\Stmt\Catch_( 165 | [new Node\Name('\\' . Throwable::class)], 166 | new Node\Expr\Variable('error'), 167 | [ 168 | new Node\Stmt\Goto_($gotoLabels), 169 | ], 170 | ), 171 | ]), 172 | ], 173 | ], 174 | ); 175 | } 176 | 177 | $tmts[] = new Node\Stmt\Label($gotoLabels); 178 | $gotoLabels++; 179 | } 180 | 181 | $tmts[] = new Node\Stmt\Throw_(new Node\Expr\Variable('error')); 182 | 183 | if (count($resolveReturnTypes) > 0) { 184 | $method->setReturnType(implode('|', array_unique($resolveReturnTypes))); 185 | } 186 | 187 | $method->addStmts($tmts); 188 | $class->addStmt($method); 189 | 190 | yield new File($pathPrefix, 'Internal\\WebHook\\' . $className, $stmt->addStmt($class)->getNode()); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Output/Error.php: -------------------------------------------------------------------------------- 1 | 21 |
ERROR
22 | 23 | ' . $throwable . ' 24 | 25 | '); 26 | 27 | if ((new CiDetector())->detect()->getCiName() !== CiDetector::CI_GITHUB_ACTIONS) { 28 | return; 29 | } 30 | 31 | file_put_contents(getenv('GITHUB_STEP_SUMMARY'), "### ⚠️ Error ⚠️\n```" . $throwable->getMessage() . "```\n", FILE_APPEND); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Output/Status.php: -------------------------------------------------------------------------------- 1 | output = new ANSI(...$steps); 19 | } else { 20 | $this->output = new Simple(...$steps); 21 | } 22 | } 23 | 24 | public function markStepBusy(string $key): void 25 | { 26 | $this->output->markStepBusy($key); 27 | } 28 | 29 | public function markStepDone(string $key): void 30 | { 31 | $this->output->markStepDone($key); 32 | } 33 | 34 | public function markStepWontDo(string ...$keys): void 35 | { 36 | $this->output->markStepWontDo(...$keys); 37 | } 38 | 39 | public function itemForStep(string $key, int $count): void 40 | { 41 | $this->output->itemForStep($key, $count); 42 | } 43 | 44 | public function advanceStep(string $key): void 45 | { 46 | $this->output->advanceStep($key); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Output/Status/ANSI.php: -------------------------------------------------------------------------------- 1 | */ 16 | private readonly array $steps; 17 | 18 | /** @var array */ 19 | private array $stepsStatus = []; 20 | 21 | /** @var array */ 22 | private array $itemsCountForStep = []; 23 | 24 | /** @var array */ 25 | private array $stepProgress = []; 26 | 27 | private int $lastPaint = 0; 28 | 29 | public function __construct(Step ...$steps) 30 | { 31 | $this->steps = $steps; 32 | foreach ($this->steps as $step) { 33 | $this->stepsStatus[$step->key] = '🌀'; 34 | if (! $step->progressBer) { 35 | continue; 36 | } 37 | 38 | $this->itemsCountForStep[$step->key] = 0; 39 | $this->stepProgress[$step->key] = 0; 40 | } 41 | 42 | renderUsing(new OverWritingOutPut(new ConsoleOutput())); 43 | } 44 | 45 | private function render(): void 46 | { 47 | $html = ''; 48 | $html .= ''; 49 | $html .= ''; 50 | $html .= ''; 51 | $html .= ''; 52 | $html .= ''; 53 | $html .= ''; 54 | $html .= ''; 55 | foreach ($this->steps as $step) { 56 | $progress = ''; 57 | if ($step->progressBer && $this->itemsCountForStep[$step->key] > 0) { 58 | $progress = $this->stepProgress[$step->key] . '/' . $this->itemsCountForStep[$step->key]; 59 | } 60 | 61 | $html .= ''; 62 | $html .= ''; 63 | $html .= ''; 64 | $html .= ''; 65 | $html .= ''; 66 | } 67 | 68 | $html .= '
StatusStepProgress
' . $this->stepsStatus[$step->key] . '' . $step->name . '' . $progress . '
'; 69 | 70 | render($html); 71 | $this->lastPaint = time(); 72 | } 73 | 74 | public function maybeRender(): void 75 | { 76 | if ($this->lastPaint === time()) { 77 | return; 78 | } 79 | 80 | $this->render(); 81 | } 82 | 83 | public function markStepBusy(string $key): void 84 | { 85 | $this->stepsStatus[$key] = '🌻'; 86 | $this->render(); 87 | } 88 | 89 | public function markStepDone(string $key): void 90 | { 91 | $this->stepsStatus[$key] = '✅'; 92 | $this->render(); 93 | } 94 | 95 | public function markStepWontDo(string ...$keys): void 96 | { 97 | foreach ($keys as $key) { 98 | $this->stepsStatus[$key] = '🚫'; 99 | } 100 | 101 | $this->render(); 102 | } 103 | 104 | public function itemForStep(string $key, int $count): void 105 | { 106 | $this->itemsCountForStep[$key] = $count; 107 | if ($this->stepProgress[$key] === 0) { 108 | $this->stepsStatus[$key] = '🌑'; 109 | } 110 | 111 | $this->render(); 112 | } 113 | 114 | public function advanceStep(string $key): void 115 | { 116 | $this->stepProgress[$key]++; 117 | $percentage = 100 / $this->itemsCountForStep[$key] * $this->stepProgress[$key]; 118 | /** @phpstan-ignore-next-line */ 119 | switch (true) { 120 | case $percentage <= 12.5: 121 | $this->stepsStatus[$key] = '🌑'; 122 | break; 123 | case $percentage > 12.5 && $percentage <= 25: 124 | $this->stepsStatus[$key] = '🌒'; 125 | break; 126 | case $percentage > 25 && $percentage <= 37.5: 127 | $this->stepsStatus[$key] = '🌓'; 128 | break; 129 | case $percentage > 37.5 && $percentage <= 50: 130 | $this->stepsStatus[$key] = '🌔'; 131 | break; 132 | case $percentage > 50 && $percentage <= 62.5: 133 | $this->stepsStatus[$key] = '🌕'; 134 | break; 135 | case $percentage > 62.5 && $percentage <= 75: 136 | $this->stepsStatus[$key] = '🌖'; 137 | break; 138 | case $percentage > 75 && $percentage <= 87.5: 139 | $this->stepsStatus[$key] = '🌗'; 140 | break; 141 | case $percentage > 87.5: 142 | $this->stepsStatus[$key] = '🌘'; 143 | break; 144 | } 145 | 146 | $this->maybeRender(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Output/Status/OverWritingOutPut.php: -------------------------------------------------------------------------------- 1 | |string $messages 29 | * 30 | * @phpstan-ignore-next-line 31 | */ 32 | public function write(iterable|string $messages, bool $newline = false, int $options = 0): void 33 | { 34 | $this->output->write($messages, $newline, $options); 35 | } 36 | 37 | /** 38 | * @param iterable|string $messages 39 | * 40 | * @phpstan-ignore-next-line 41 | */ 42 | public function writeln(iterable|string $messages, int $options = 0): void 43 | { 44 | if (! is_string($messages)) { 45 | $messages = implode(PHP_EOL, [...$messages]); 46 | } 47 | 48 | if ($this->previousLinecount > 0) { 49 | $this->output->write(sprintf("\x1b[%dA", $this->previousLinecount)); 50 | $this->output->write("\x1b[0J"); 51 | } 52 | 53 | $this->previousLinecount = count(explode(PHP_EOL, $messages)); 54 | 55 | $this->output->writeln($messages, $options); 56 | } 57 | 58 | public function setVerbosity(int $level): void 59 | { 60 | $this->output->setVerbosity($level); 61 | } 62 | 63 | public function getVerbosity(): int 64 | { 65 | return $this->output->getVerbosity(); 66 | } 67 | 68 | public function isQuiet(): bool 69 | { 70 | return $this->output->isQuiet(); 71 | } 72 | 73 | public function isVerbose(): bool 74 | { 75 | return $this->output->isVerbose(); 76 | } 77 | 78 | public function isVeryVerbose(): bool 79 | { 80 | return $this->output->isVeryVerbose(); 81 | } 82 | 83 | public function isDebug(): bool 84 | { 85 | return $this->output->isDebug(); 86 | } 87 | 88 | public function setDecorated(bool $decorated): void 89 | { 90 | $this->output->setDecorated($decorated); 91 | } 92 | 93 | public function isDecorated(): bool 94 | { 95 | return $this->output->isDecorated(); 96 | } 97 | 98 | public function setFormatter(OutputFormatterInterface $formatter): void 99 | { 100 | $this->output->setFormatter($formatter); 101 | } 102 | 103 | public function getFormatter(): OutputFormatterInterface 104 | { 105 | return $this->output->getFormatter(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Output/Status/Simple.php: -------------------------------------------------------------------------------- 1 | */ 15 | private readonly array $steps; 16 | 17 | public function __construct(Step ...$steps) 18 | { 19 | $this->steps = array_combine( 20 | array_map( 21 | static fn (Step $step): string => $step->key, 22 | $steps, 23 | ), 24 | $steps, 25 | ); 26 | } 27 | 28 | public function markStepBusy(string $key): void 29 | { 30 | } 31 | 32 | public function markStepDone(string $key): void 33 | { 34 | echo $this->steps[$key]->name, PHP_EOL; 35 | } 36 | 37 | public function markStepWontDo(string ...$keys): void 38 | { 39 | } 40 | 41 | public function itemForStep(string $key, int $count): void 42 | { 43 | } 44 | 45 | public function advanceStep(string $key): void 46 | { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Output/Status/Step.php: -------------------------------------------------------------------------------- 1 | name), 21 | $this->default, 22 | $this->type, 23 | $this->byRef, 24 | $this->variadic, 25 | [], 26 | Node\Stmt\Class_::MODIFIER_PRIVATE, 27 | $this->attributeGroups, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/PromotedPropertyAsParam.php: -------------------------------------------------------------------------------- 1 | name), 21 | $this->default, 22 | $this->type, 23 | $this->byRef, 24 | $this->variadic, 25 | [], 26 | Node\Stmt\Class_::MODIFIER_PUBLIC, 27 | $this->attributeGroups, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Registry/CompositSchema.php: -------------------------------------------------------------------------------- 1 | */ 13 | private array $splHash = []; 14 | 15 | public function __construct( 16 | private readonly Namespace_ $baseNamespaces, 17 | ) { 18 | } 19 | 20 | public function get(PropertyType $propertyType): void 21 | { 22 | } 23 | 24 | /** @return iterable */ 25 | public function list(): iterable 26 | { 27 | $unknownSchemas = $this->unknownSchemas; 28 | $this->unknownSchemas = []; 29 | 30 | yield from $unknownSchemas; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Registry/Contract.php: -------------------------------------------------------------------------------- 1 | */ 20 | private array $splHash = []; 21 | 22 | /** @var array */ 23 | private array $unknownSchemas = []; 24 | 25 | /** @throws JsonException */ 26 | public function get(openAPISchema $schema, string $fallbackName): string 27 | { 28 | if ($schema->type === 'array') { 29 | $schema = $schema->items; 30 | } 31 | 32 | if (! $schema instanceof openAPISchema) { 33 | throw new RuntimeException('Schemas has to be instance of: ' . openAPISchema::class); 34 | } 35 | 36 | $hash = spl_object_hash($schema); 37 | if (array_key_exists($hash, $this->splHash)) { 38 | return $this->splHash[$hash]; 39 | } 40 | 41 | $className = Utils::fixKeyword($fallbackName); 42 | 43 | $suffix = 'a'; 44 | while (array_key_exists($className, $this->unknownSchemas)) { 45 | $className = Utils::fixKeyword($fallbackName . strtoupper($suffix++)); 46 | } 47 | 48 | $this->splHash[spl_object_hash($schema)] = $className; 49 | $this->unknownSchemas[$className] = new UnknownSchema($fallbackName, $className, $schema); 50 | 51 | return $className; 52 | } 53 | 54 | public function hasContracts(): bool 55 | { 56 | return count($this->unknownSchemas) > 0; 57 | } 58 | 59 | /** @return iterable */ 60 | public function contracts(): iterable 61 | { 62 | $unknownSchemas = $this->unknownSchemas; 63 | $this->unknownSchemas = []; 64 | 65 | yield from $unknownSchemas; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Registry/Schema.php: -------------------------------------------------------------------------------- 1 | */ 23 | private array $splHash = []; 24 | /** @var array */ 25 | private array $json = []; 26 | 27 | /** @var array */ 28 | private array $unknownSchemas = []; 29 | 30 | /** @var array */ 31 | private array $unknownSchemasJson = []; 32 | /** @var array> */ 33 | private array $aliasses = []; 34 | 35 | public function __construct( 36 | private readonly Namespace_ $baseNamespaces, 37 | private readonly bool $allowDuplicatedSchemas, 38 | private readonly bool $useAliasesForDuplication, 39 | ) { 40 | } 41 | 42 | public function addClassName(string $className, openAPISchema $schema): void 43 | { 44 | if ($schema->type === 'array') { 45 | $schema = $schema->items; 46 | } 47 | 48 | if (! $schema instanceof openAPISchema) { 49 | throw new RuntimeException('Schemas has to be instance of: ' . openAPISchema::class); 50 | } 51 | 52 | $className = Utils::className($className); 53 | $this->splHash[spl_object_hash($schema)] = $className; 54 | $this->json[json_encode($schema->getSerializableData())] = $className; 55 | } 56 | 57 | /** @throws JsonException */ 58 | public function get(openAPISchema $schema, string $fallbackName): string 59 | { 60 | if ($schema->type === 'array') { 61 | $schema = $schema->items; 62 | } 63 | 64 | if (! $schema instanceof openAPISchema) { 65 | throw new RuntimeException('Schemas has to be instance of: ' . openAPISchema::class); 66 | } 67 | 68 | $hash = spl_object_hash($schema); 69 | if (array_key_exists($hash, $this->splHash)) { 70 | return $this->splHash[$hash]; 71 | } 72 | 73 | $json = json_encode($schema->getSerializableData()); 74 | if (! $this->allowDuplicatedSchemas && array_key_exists($json, $this->json)) { 75 | return $this->json[$json]; 76 | } 77 | 78 | if (! $this->allowDuplicatedSchemas && array_key_exists($json, $this->unknownSchemasJson)) { 79 | return $this->unknownSchemasJson[$json]; 80 | } 81 | 82 | $className = Utils::fixKeyword($fallbackName); 83 | 84 | if ($this->allowDuplicatedSchemas && $this->useAliasesForDuplication && array_key_exists($json, $this->json)) { 85 | $this->aliasses['Schema\\' . $this->json[$json]][] = ClassString::factory($this->baseNamespaces, 'Schema\\' . $className); 86 | 87 | return $className; 88 | } 89 | 90 | if ($this->allowDuplicatedSchemas && $this->useAliasesForDuplication && array_key_exists($json, $this->unknownSchemasJson)) { 91 | $this->aliasses['Schema\\' . $this->unknownSchemasJson[$json]][] = ClassString::factory($this->baseNamespaces, 'Schema\\' . $className); 92 | 93 | return $className; 94 | } 95 | 96 | $suffix = 'a'; 97 | while (array_key_exists($className, $this->unknownSchemas)) { 98 | $className = Utils::fixKeyword($fallbackName . strtoupper($suffix++)); 99 | } 100 | 101 | $this->splHash[spl_object_hash($schema)] = $className; 102 | $this->unknownSchemasJson[$json] = $className; 103 | $this->unknownSchemas[$className] = new UnknownSchema($fallbackName, $className, $schema); 104 | 105 | return $className; 106 | } 107 | 108 | public function hasUnknownSchemas(): bool 109 | { 110 | return count($this->unknownSchemas) > 0; 111 | } 112 | 113 | /** @return iterable */ 114 | public function unknownSchemas(): iterable 115 | { 116 | $unknownSchemas = $this->unknownSchemas; 117 | $this->unknownSchemas = []; 118 | 119 | yield from $unknownSchemas; 120 | } 121 | 122 | /** @return iterable */ 123 | public function aliasesForClassName(string $classname): iterable 124 | { 125 | if (! array_key_exists($classname, $this->aliasses)) { 126 | return; 127 | } 128 | 129 | yield from $this->aliasses[$classname]; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Registry/ThrowableSchema.php: -------------------------------------------------------------------------------- 1 | */ 12 | private array $throwables = []; 13 | 14 | public function add(string $class): void 15 | { 16 | $this->throwables[] = $class; 17 | } 18 | 19 | public function has(string $class): bool 20 | { 21 | return in_array($class, $this->throwables, true); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Registry/UnknownSchema.php: -------------------------------------------------------------------------------- 1 | $paths */ 10 | public function __construct( 11 | public readonly string|null $baseUrl, 12 | /** @var array $paths */ 13 | public readonly array $paths, 14 | ) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Representation/Contract.php: -------------------------------------------------------------------------------- 1 | $properties */ 12 | public function __construct( 13 | public readonly ClassString $className, 14 | /** @var array $properties */ 15 | public readonly array $properties, 16 | ) { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Representation/ExampleData.php: -------------------------------------------------------------------------------- 1 | $schemas */ 12 | public function __construct( 13 | public readonly ClassString $className, 14 | public readonly string $methodName, 15 | /** @var array $schemas */ 16 | public readonly array $schemas, 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Representation/Operation.php: -------------------------------------------------------------------------------- 1 | $metaData 14 | * @param array $returnType 15 | * @param array $parameters 16 | * @param array $requestBody 17 | * @param array $response 18 | * @param array $empty 19 | */ 20 | public function __construct( 21 | public ClassString $className, 22 | public ClassString $classNameSanitized, 23 | public ClassString $operatorClassName, 24 | public string $operatorLookUpMethod, 25 | public string $name, 26 | public string $nameCamel, 27 | public string|null $group, 28 | public string|null $groupCamel, 29 | public string $operationId, 30 | public string $matchMethod, 31 | public string $method, 32 | public string $summary, 33 | public ExternalDocumentation|null $externalDocs, 34 | public string $path, 35 | /** @var array $metaData */ 36 | public array $metaData, 37 | /** @var array $returnType */ 38 | public array $returnType, 39 | /** @var array $parameters */ 40 | public array $parameters, 41 | /** @var array $requestBody */ 42 | public array $requestBody, 43 | /** @var array $response */ 44 | public array $response, 45 | /** @var array $empty */ 46 | public array $empty, 47 | ) { 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Representation/OperationEmptyResponse.php: -------------------------------------------------------------------------------- 1 | $headers */ 10 | public function __construct( 11 | public int $code, 12 | public string $description, 13 | /** @var array
$headers */ 14 | public array $headers, 15 | ) { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Representation/OperationRequestBody.php: -------------------------------------------------------------------------------- 1 | $operations */ 12 | public function __construct( 13 | public readonly ClassString $className, 14 | public readonly Hydrator $hydrator, 15 | /** @var array $operations */ 16 | public readonly array $operations, 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Representation/Property.php: -------------------------------------------------------------------------------- 1 | $enum */ 10 | public function __construct( 11 | public readonly string $name, 12 | public readonly string $sourceName, 13 | public readonly string $description, 14 | public readonly ExampleData $example, 15 | public readonly PropertyType $type, 16 | public readonly bool $nullable, 17 | public readonly array $enum, 18 | ) { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Representation/PropertyType.php: -------------------------------------------------------------------------------- 1 | $payload */ 10 | public function __construct( 11 | public readonly string $type, 12 | public readonly string|null $format, 13 | public readonly string|null $pattern, 14 | public readonly string|Schema|PropertyType|array $payload, 15 | public readonly bool $nullable, 16 | ) { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Representation/Schema.php: -------------------------------------------------------------------------------- 1 | $contracts 14 | * @param array $example 15 | * @param array $properties 16 | * @param array $type 17 | */ 18 | public function __construct( 19 | public readonly ClassString $className, 20 | /** @var array $contracts */ 21 | public readonly array $contracts, 22 | public readonly ClassString $errorClassName, 23 | public readonly ClassString $errorClassNameAliased, 24 | public readonly string $title, 25 | public readonly string $description, 26 | /** @var array $example */ 27 | public readonly array $example, 28 | /** @var array $properties */ 29 | public readonly array $properties, 30 | public readonly baseSchema $schema, 31 | public readonly bool $isArray, 32 | public readonly array $type, 33 | ) { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Representation/WebHook.php: -------------------------------------------------------------------------------- 1 | $headers 11 | * @param array $schema 12 | */ 13 | public function __construct( 14 | public readonly string $event, 15 | public readonly string $summary, 16 | public readonly string $description, 17 | public readonly string $operationId, 18 | public readonly string $documentationUrl, 19 | /** @var array
*/ 20 | public readonly array $headers, 21 | /** @var array */ 22 | public readonly array $schema, 23 | ) { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/SectionGenerator/OperationIdSlash.php: -------------------------------------------------------------------------------- 1 | operations[0]->operationId); 20 | array_pop($chunks); 21 | 22 | return implode('-', $chunks); 23 | } 24 | 25 | public static function webHook(WebHook ...$webHooks): string|false 26 | { 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SectionGenerator/WebHooks.php: -------------------------------------------------------------------------------- 1 | $files */ 13 | private array $files = []; 14 | 15 | /** @param array $files */ 16 | public function __construct( 17 | array $files, 18 | ) { 19 | foreach ($files as $file) { 20 | $this->files[$file->name] = $file; 21 | } 22 | } 23 | 24 | public function upsert(string $fileName, string $hash): void 25 | { 26 | $this->files[$fileName] = new File($fileName, $hash); 27 | } 28 | 29 | public function has(string $fileName): bool 30 | { 31 | return array_key_exists($fileName, $this->files); 32 | } 33 | 34 | public function get(string $fileName): File 35 | { 36 | return $this->files[$fileName]; 37 | } 38 | 39 | public function remove(string $fileName): void 40 | { 41 | unset($this->files[$fileName]); 42 | } 43 | 44 | /** @return array */ 45 | public function files(): array 46 | { 47 | return array_values($this->files); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Utils.php: -------------------------------------------------------------------------------- 1 | self::fixKeyword( 45 | (new Convert($chunk))->toPascal(), 46 | ), 47 | explode( 48 | '\\', 49 | $className, 50 | ), 51 | ), 52 | ); 53 | 54 | return trim(self::cleanUpNamespace(self::fixKeyword($className)), '\\'); 55 | } 56 | 57 | public static function cleanUpNamespace(string $namespace): string 58 | { 59 | do { 60 | $previousNamespace = $namespace; 61 | $namespace = str_replace('/', '\\', $namespace); 62 | $namespace = str_replace('\\\\', '\\', $namespace); 63 | } while ($previousNamespace !== $namespace); 64 | 65 | $namespace = trim($namespace, '\\'); 66 | 67 | return '\\' . $namespace; 68 | } 69 | 70 | public static function fqcn(string $fqcn): string 71 | { 72 | return str_replace('/', '\\', $fqcn); 73 | } 74 | 75 | public static function dirname(string $fqcn): string 76 | { 77 | $fqcn = str_replace('\\', '/', $fqcn); 78 | 79 | return trim(self::cleanUpNamespace(dirname($fqcn)), '\\'); 80 | } 81 | 82 | public static function basename(string $fqcn): string 83 | { 84 | $fqcn = str_replace('\\', '/', $fqcn); 85 | 86 | return trim(self::cleanUpNamespace(basename($fqcn)), '\\'); 87 | } 88 | 89 | public static function fixKeyword(string $name): string 90 | { 91 | $name = self::fqcn($name); 92 | $nameBoom = explode('\\', $name); 93 | 94 | /** @phpstan-ignore-next-line */ 95 | return $name . (in_array( 96 | strtolower($nameBoom[count($nameBoom) - 1]), 97 | ['__halt_compiler', 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor', 'self', 'parent', 'object'], 98 | false, 99 | ) ? '_' : ''); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Voter/ListOperation/PageAndPerPageInQuery.php: -------------------------------------------------------------------------------- 1 | */ 23 | final public static function keys(): array 24 | { 25 | return ['perPage', 'page']; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Voter/StreamOperation/DownloadInOperationId.php: -------------------------------------------------------------------------------- 1 | operationId, 'download') !== false; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Voter/StreamOperation/DownloadInPath.php: -------------------------------------------------------------------------------- 1 | path, 'download') !== false; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/phpstan-assertType-mock.php: -------------------------------------------------------------------------------- 1 | dump( 9 | [ 10 | WebhookPing::class, 11 | ], 12 | ':poop-emoji:', 13 | ); 14 | --------------------------------------------------------------------------------