diff --git a/.github/workflows/samples-php8.yaml b/.github/workflows/samples-php8.yaml
index 82ead5c953a..0d2941f6e3c 100644
--- a/.github/workflows/samples-php8.yaml
+++ b/.github/workflows/samples-php8.yaml
@@ -4,9 +4,11 @@ on:
push:
paths:
- samples/server/petstore/php-symfony/SymfonyBundle-php/**
+ - samples/server/petstore/php-flight/**
pull_request:
paths:
- samples/server/petstore/php-symfony/SymfonyBundle-php/**
+ - samples/server/petstore/php-flight/**
jobs:
build:
name: Build PHP projects
@@ -17,6 +19,7 @@ jobs:
sample:
# servers
- samples/server/petstore/php-symfony/SymfonyBundle-php/
+ - samples/server/petstore/php-flight/
steps:
- uses: actions/checkout@v4
- name: Setup PHP with tools
diff --git a/bin/configs/php-flight.yaml b/bin/configs/php-flight.yaml
new file mode 100644
index 00000000000..2c3dedc1911
--- /dev/null
+++ b/bin/configs/php-flight.yaml
@@ -0,0 +1,6 @@
+generatorName: php-flight
+outputDir: samples/server/petstore/php-flight
+inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
+templateDir: modules/openapi-generator/src/main/resources/php-flight
+additionalProperties:
+ hideGenerationTimestamp: "true"
diff --git a/docs/generators.md b/docs/generators.md
index 749f43f15db..03c75077a3a 100644
--- a/docs/generators.md
+++ b/docs/generators.md
@@ -120,6 +120,7 @@ The following generators are available:
* [kotlin-spring](generators/kotlin-spring.md)
* [kotlin-vertx (beta)](generators/kotlin-vertx.md)
* [nodejs-express-server (beta)](generators/nodejs-express-server.md)
+* [php-flight (experimental)](generators/php-flight.md)
* [php-laravel](generators/php-laravel.md)
* [php-lumen](generators/php-lumen.md)
* [php-mezzio-ph](generators/php-mezzio-ph.md)
diff --git a/docs/generators/php-flight.md b/docs/generators/php-flight.md
new file mode 100644
index 00000000000..8b7c90a069b
--- /dev/null
+++ b/docs/generators/php-flight.md
@@ -0,0 +1,270 @@
+---
+title: Documentation for the php-flight Generator
+---
+
+## METADATA
+
+| Property | Value | Notes |
+| -------- | ----- | ----- |
+| generator name | php-flight | pass this to the generate command after -g |
+| generator stability | EXPERIMENTAL | |
+| generator type | SERVER | |
+| generator language | PHP | |
+| generator default templating engine | mustache | |
+| helpTxt | Generates a PHP Flight Framework server library. | |
+
+## CONFIG OPTIONS
+These options may be applied as additional-properties (cli) or configOptions (plugins). Refer to [configuration docs](https://openapi-generator.tech/docs/configuration) for more details.
+
+| Option | Description | Values | Default |
+| ------ | ----------- | ------ | ------- |
+|allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false|
+|apiPackage|package for generated api classes| |null|
+|artifactUrl|artifact URL in generated pom.xml| |null|
+|artifactVersion|The version to use in the composer package version field. e.g. 1.2.3| |null|
+|composerPackageName|The name to use in the composer package name field. e.g. `vendor/project` (must be lowercase and consist of words separated by `-`, `.` or `_`).| |null|
+|developerOrganization|developer organization in generated pom.xml| |null|
+|developerOrganizationUrl|developer organization URL in generated pom.xml| |null|
+|disallowAdditionalPropertiesIfNotPresent|If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.|
- **false**
- The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.
- **true**
- Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.
|true|
+|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true|
+|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|- **false**
- No changes to the enum's are made, this is the default option.
- **true**
- With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.
|false|
+|hideGenerationTimestamp|Hides the generation timestamp when files are generated.| |true|
+|invokerPackage|The main namespace to use for all classes. e.g. Yay\Pets| |null|
+|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|- **true**
- The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.
- **false**
- The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.
|true|
+|licenseName|The name of the license| |null|
+|modelPackage|package for generated models| |null|
+|packageName|The main package name for classes. e.g. GeneratedPetstore| |null|
+|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false|
+|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true|
+|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
+|srcBasePath|The directory to serve as source root.| |null|
+|variableNamingConvention|naming convention of variable name, e.g. camelCase.| |camelCase|
+
+## IMPORT MAPPING
+
+| Type/Alias | Imports |
+| ---------- | ------- |
+
+
+## INSTANTIATION TYPES
+
+| Type/Alias | Instantiated By |
+| ---------- | --------------- |
+|array|array|
+|map|array|
+
+
+## LANGUAGE PRIMITIVES
+
+
+- \DateTime
+- \SplFileObject
+- array
+- bool
+- boolean
+- byte
+- float
+- int
+- integer
+- mixed
+- number
+- object
+- string
+- void
+
+
+## RESERVED WORDS
+
+
+- __halt_compiler
+- _header_accept
+- _tempbody
+- 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
+- formparams
+- function
+- global
+- goto
+- headerparams
+- httpbody
+- if
+- implements
+- include
+- include_once
+- instanceof
+- insteadof
+- interface
+- isset
+- list
+- namespace
+- new
+- or
+- print
+- private
+- protected
+- public
+- queryparams
+- require
+- require_once
+- resourcepath
+- return
+- static
+- switch
+- throw
+- trait
+- try
+- unset
+- use
+- var
+- while
+- xor
+
+
+## FEATURE SET
+
+
+### Client Modification Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|BasePath|✗|ToolingExtension
+|Authorizations|✗|ToolingExtension
+|UserAgent|✗|ToolingExtension
+|MockServer|✗|ToolingExtension
+
+### Data Type Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Custom|✗|OAS2,OAS3
+|Int32|✓|OAS2,OAS3
+|Int64|✓|OAS2,OAS3
+|Float|✓|OAS2,OAS3
+|Double|✓|OAS2,OAS3
+|Decimal|✓|ToolingExtension
+|String|✓|OAS2,OAS3
+|Byte|✓|OAS2,OAS3
+|Binary|✓|OAS2,OAS3
+|Boolean|✓|OAS2,OAS3
+|Date|✓|OAS2,OAS3
+|DateTime|✓|OAS2,OAS3
+|Password|✓|OAS2,OAS3
+|File|✓|OAS2
+|Uuid|✗|
+|Array|✓|OAS2,OAS3
+|Null|✗|OAS3
+|AnyType|✗|OAS2,OAS3
+|Object|✓|OAS2,OAS3
+|Maps|✓|ToolingExtension
+|CollectionFormat|✓|OAS2
+|CollectionFormatMulti|✓|OAS2
+|Enum|✓|OAS2,OAS3
+|ArrayOfEnum|✓|ToolingExtension
+|ArrayOfModel|✓|ToolingExtension
+|ArrayOfCollectionOfPrimitives|✓|ToolingExtension
+|ArrayOfCollectionOfModel|✓|ToolingExtension
+|ArrayOfCollectionOfEnum|✓|ToolingExtension
+|MapOfEnum|✗|ToolingExtension
+|MapOfModel|✗|ToolingExtension
+|MapOfCollectionOfPrimitives|✓|ToolingExtension
+|MapOfCollectionOfModel|✗|ToolingExtension
+|MapOfCollectionOfEnum|✗|ToolingExtension
+
+### Documentation Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Readme|✓|ToolingExtension
+|Model|✓|ToolingExtension
+|Api|✓|ToolingExtension
+
+### Global Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Host|✓|OAS2,OAS3
+|BasePath|✓|OAS2,OAS3
+|Info|✓|OAS2,OAS3
+|Schemes|✗|OAS2,OAS3
+|PartialSchemes|✓|OAS2,OAS3
+|Consumes|✓|OAS2
+|Produces|✓|OAS2
+|ExternalDocumentation|✓|OAS2,OAS3
+|Examples|✓|OAS2,OAS3
+|XMLStructureDefinitions|✗|OAS2,OAS3
+|MultiServer|✗|OAS3
+|ParameterizedServer|✗|OAS3
+|ParameterStyling|✗|OAS3
+|Callbacks|✗|OAS3
+|LinkObjects|✗|OAS3
+
+### Parameter Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Path|✓|OAS2,OAS3
+|Query|✓|OAS2,OAS3
+|Header|✓|OAS2,OAS3
+|Body|✓|OAS2
+|FormUnencoded|✗|OAS2
+|FormMultipart|✗|OAS2
+|Cookie|✗|OAS3
+
+### Schema Support Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Simple|✓|OAS2,OAS3
+|Composite|✓|OAS2,OAS3
+|Polymorphism|✗|OAS2,OAS3
+|Union|✗|OAS3
+|allOf|✗|OAS2,OAS3
+|anyOf|✗|OAS3
+|oneOf|✗|OAS3
+|not|✗|OAS3
+
+### Security Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|BasicAuth|✓|OAS2,OAS3
+|ApiKey|✓|OAS2,OAS3
+|OpenIDConnect|✗|OAS3
+|BearerToken|✓|OAS3
+|OAuth2_Implicit|✓|OAS2,OAS3
+|OAuth2_Password|✗|OAS2,OAS3
+|OAuth2_ClientCredentials|✗|OAS2,OAS3
+|OAuth2_AuthorizationCode|✗|OAS2,OAS3
+|SignatureAuth|✗|OAS3
+|AWSV4Signature|✗|ToolingExtension
+
+### Wire Format Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|JSON|✓|OAS2,OAS3
+|XML|✗|OAS2,OAS3
+|PROTOBUF|✗|ToolingExtension
+|Custom|✗|OAS2,OAS3
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpFlightServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpFlightServerCodegen.java
new file mode 100644
index 00000000000..9a33a4c80cf
--- /dev/null
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpFlightServerCodegen.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openapitools.codegen.languages;
+
+import static org.openapitools.codegen.utils.StringUtils.camelize;
+
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.servers.Server;
+import java.io.File;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import org.apache.commons.lang3.StringUtils;
+import org.openapitools.codegen.CliOption;
+import org.openapitools.codegen.CodegenConstants;
+import org.openapitools.codegen.CodegenModel;
+import org.openapitools.codegen.CodegenOperation;
+import org.openapitools.codegen.CodegenProperty;
+import org.openapitools.codegen.CodegenType;
+import org.openapitools.codegen.SupportingFile;
+import org.openapitools.codegen.meta.GeneratorMetadata;
+import org.openapitools.codegen.meta.Stability;
+import org.openapitools.codegen.meta.features.DataTypeFeature;
+import org.openapitools.codegen.meta.features.DocumentationFeature;
+import org.openapitools.codegen.meta.features.GlobalFeature;
+import org.openapitools.codegen.meta.features.ParameterFeature;
+import org.openapitools.codegen.meta.features.SchemaSupportFeature;
+import org.openapitools.codegen.meta.features.SecurityFeature;
+import org.openapitools.codegen.meta.features.WireFormatFeature;
+import org.openapitools.codegen.model.ModelMap;
+import org.openapitools.codegen.model.ModelsMap;
+import org.openapitools.codegen.model.OperationMap;
+import org.openapitools.codegen.model.OperationsMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PhpFlightServerCodegen extends AbstractPhpCodegen {
+
+ private final Logger LOGGER = LoggerFactory.getLogger(PhpFlightServerCodegen.class);
+
+ // Type-hintable primitive types
+ // ref: http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration
+ protected HashSet typeHintable = new HashSet<>(
+ Arrays.asList(
+ "array",
+ "bool",
+ "float",
+ "int",
+ "string"
+ )
+ );
+
+ public PhpFlightServerCodegen() {
+ super();
+
+ modifyFeatureSet(features -> features
+ .includeDocumentationFeatures(DocumentationFeature.Readme)
+ .wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON))
+ .securityFeatures(EnumSet.of(
+ SecurityFeature.BasicAuth,
+ SecurityFeature.BearerToken,
+ SecurityFeature.ApiKey,
+ SecurityFeature.OAuth2_Implicit))
+ .excludeDataTypeFeatures(
+ DataTypeFeature.MapOfCollectionOfEnum,
+ DataTypeFeature.MapOfEnum,
+ DataTypeFeature.MapOfCollectionOfModel,
+ DataTypeFeature.MapOfModel)
+ .excludeParameterFeatures(
+ ParameterFeature.FormMultipart,
+ ParameterFeature.FormUnencoded,
+ ParameterFeature.Cookie)
+ .excludeGlobalFeatures(
+ GlobalFeature.XMLStructureDefinitions,
+ GlobalFeature.Callbacks,
+ GlobalFeature.LinkObjects,
+ GlobalFeature.ParameterStyling
+ )
+ .excludeSchemaSupportFeatures(
+ SchemaSupportFeature.Polymorphism
+ )
+ );
+
+ generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata)
+ .stability(Stability.EXPERIMENTAL)
+ .build();
+
+ embeddedTemplateDir = templateDir = "php-flight";
+
+ // clear import mapping (from default generator) as slim does not use it
+ // at the moment
+ importMapping.clear();
+
+ srcBasePath = "";
+
+ defaultIncludes = new HashSet<>(
+ Arrays.asList(
+ "\\DateTime"
+ )
+ );
+
+ variableNamingConvention = "camelCase";
+ artifactVersion = "1.0.0";
+ setInvokerPackage("OpenAPIServer");
+ testPackage = invokerPackage + "\\Test";
+ apiPackage = invokerPackage + "\\" + apiDirName;
+ modelPackage = invokerPackage + "\\" + modelDirName;
+ outputFolder = "generated-code" + File.separator + "php-flight";
+
+ // no doc files
+ modelDocTemplateFiles.clear();
+ apiDocTemplateFiles.clear();
+ apiTestTemplateFiles.clear();
+
+ embeddedTemplateDir = templateDir = "php-flight";
+
+ cliOptions.add(new CliOption(CodegenConstants.HIDE_GENERATION_TIMESTAMP, CodegenConstants.HIDE_GENERATION_TIMESTAMP_DESC)
+ .defaultValue(Boolean.TRUE.toString()));
+ cliOptions.stream().filter(o -> Objects.equals(o.getOpt(), VARIABLE_NAMING_CONVENTION)).findFirst().ifPresent(o -> o.defaultValue("camelCase"));
+ }
+
+ @Override
+ public CodegenType getTag() {
+ return CodegenType.SERVER;
+ }
+
+ @Override
+ public String getName() {
+ return "php-flight";
+ }
+
+ @Override
+ public String getHelp() {
+ return "Generates a PHP Flight Framework server library.";
+ }
+
+ @Override
+ public String apiFileFolder() {
+ if (apiPackage.startsWith(invokerPackage + "\\")) {
+ // need to strip out invokerPackage from path
+ return (outputFolder + File.separator + toSrcPath(StringUtils.removeStart(apiPackage, invokerPackage + "\\"), srcBasePath));
+ }
+ return (outputFolder + File.separator + toSrcPath(apiPackage, srcBasePath));
+ }
+
+ @Override
+ public String modelFileFolder() {
+ if (modelPackage.startsWith(invokerPackage + "\\")) {
+ // need to strip out invokerPackage from path
+ return (outputFolder + File.separator + toSrcPath(StringUtils.removeStart(modelPackage, invokerPackage + "\\"), srcBasePath));
+ }
+ return (outputFolder + File.separator + toSrcPath(modelPackage, srcBasePath));
+ }
+
+ @Override
+ public void processOpts() {
+ super.processOpts();
+
+ inlineSchemaOption.put("RESOLVE_INLINE_ENUMS", "true");
+
+ // add trailing slash for mustache templates
+ additionalProperties.put("relativeSrcBasePath", srcBasePath.isEmpty() ? "" : srcBasePath + "/");
+ additionalProperties.put("modelSrcPath", "." + "/" + toSrcPath(modelPackage, srcBasePath));
+ additionalProperties.put("apiSrcPath", "." + "/" + toSrcPath(apiPackage, srcBasePath));
+ additionalProperties.put("testSrcPath", "." + "/" + toSrcPath(testPackage, srcBasePath));
+ additionalProperties.put("escapedModelPackage", modelPackage.replace("\\", "\\\\"));
+
+ if (additionalProperties.containsKey("testPackage")) {
+ // Update model package to contain the specified model package name and the invoker package
+ testPackage = invokerPackage + "\\" + (String) additionalProperties.get("testPackage");
+ }
+ additionalProperties.put("testPackage", testPackage);
+
+ supportingFiles.add(new SupportingFile("composer.mustache", "", "composer.json"));
+ supportingFiles.add(new SupportingFile("phpunit.mustache", "", "phpunit.xml.dist"));
+ supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
+ supportingFiles.add(new SupportingFile("register_routes.mustache", toSrcPath(invokerPackage, srcBasePath), "RegisterRoutes.php"));
+ supportingFiles.add(new SupportingFile("register_routes_test.mustache", toSrcPath(testPackage, srcBasePath), "RegisterRoutesTest.php"));
+ }
+
+ @Override
+ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) {
+ OperationMap operations = objs.getOperations();
+ List operationList = operations.getOperation();
+ operationList.forEach(operation -> {
+ operation.vendorExtensions.put("x-path", mapToFlightPath(operation.path));
+ String returnType = operation.responses.stream().filter(r -> r.is2xx && r.dataType != null).map(r -> this.getTypeHint(r.dataType, false, false)).filter(t -> !t.isEmpty()).map(t -> t + "|null").findFirst().orElse("void");
+ operation.vendorExtensions.put("x-return-type", returnType);
+ operation.vendorExtensions.put("x-return-type-is-void", returnType.equals("void"));
+ operation.vendorExtensions.put("x-return-type-comment",
+ operation.responses.stream().filter(r -> r.is2xx && r.dataType != null).map(r -> this.getTypeHint(r.dataType, true, false)).filter(t -> !t.isEmpty()).map(t -> t+"|null").findFirst().orElse("void"));
+ operation.vendorExtensions.put("x-nonFormParams", operation.allParams.stream().filter(p -> !p.isFormParam).toArray());
+
+ operation.allParams.forEach(param -> {
+ param.vendorExtensions.put("x-parameter-type", param.required ? getTypeHint(param.dataType, false, false) : getTypeHintNullable(param.dataType, false));
+ String commentType = param.required ? getTypeHint(param.dataType, true, false) : getTypeHintNullable(param.dataType, false);
+ param.vendorExtensions.put("x-comment-type", commentType);
+ param.vendorExtensions.put("x-comment-type-escaped", commentType.replace("\\", "\\\\"));
+ });
+ });
+ escapeMediaType(operationList);
+ return objs;
+ }
+
+ private String mapToFlightPath(String path) {
+ return path.replaceAll("\\{([^}]+)}", "@$1");
+ }
+
+ @Override
+ public ModelsMap postProcessModels(ModelsMap objs) {
+ objs = super.postProcessModels(objs);
+
+ ModelMap models = objs.getModels().get(0);
+ CodegenModel model = models.getModel();
+
+ // Simplify model var type
+ for (CodegenProperty var : model.vars) {
+ if (var.dataType != null) {
+ // Determine if the parameter type is supported as a type hint and make it available
+ // to the templating engine
+ var.vendorExtensions.put("x-parameter-type", var.required ? getTypeHint(var.dataType, false, true) : getTypeHintNullable(var.dataType, true));
+ var.vendorExtensions.put("x-comment-type", var.required ? getTypeHint(var.dataType, true, true) : getTypeHintNullableForComments(var.dataType, true));
+ }
+ }
+
+ return objs;
+ }
+
+ protected String getTypeHintNullable(String type, boolean modelContext) {
+ String typeHint = getTypeHint(type, false, modelContext);
+ if (!typeHint.equals("")) {
+ return "?" + typeHint;
+ }
+
+ return typeHint;
+ }
+
+ protected String getTypeHintNullableForComments(String type, boolean modelContext) {
+ String typeHint = getTypeHint(type, true, modelContext);
+ if (!typeHint.equals("")) {
+ return typeHint + "|null";
+ }
+
+ return typeHint;
+ }
+
+ protected String getTypeHint(String type, boolean forComments, boolean modelContext) {
+ // Type hint array types
+ if (type.endsWith("[]")) {
+ if (forComments) {
+ //Make type hints for array in comments. Call getTypeHint recursive for extractSimpleName for models
+ String typeWithoutArray = type.substring(0, type.length() - 2);
+ return this.getTypeHint(typeWithoutArray, true, modelContext) + "[]";
+ } else {
+ return "array";
+ }
+ }
+
+ // Check if the type is a native type that is type hintable in PHP
+ if (typeHintable.contains(type)) {
+ return type;
+ }
+
+ // Default includes are referenced by their fully-qualified class name (including namespace)
+ if (defaultIncludes.contains(type)) {
+ return type;
+ }
+
+ // Model classes are assumed to be imported and we reference them by their class name
+ if (isModelClass(type)) {
+ // This parameter is an instance of a model
+ return modelContext ? extractSimpleName(type) : type;
+ }
+
+ // PHP does not support type hinting for this parameter data type
+ return "";
+ }
+
+ protected Boolean isModelClass(String type) {
+ return Boolean.valueOf(type.contains(modelPackage()));
+ }
+
+ @Override
+ public String toApiName(String name) {
+ if (name.length() == 0) {
+ return toAbstractName("DefaultApi");
+ }
+ return toAbstractName(camelize(name) + "Api");
+ }
+
+ @Override
+ public String toApiTestFilename(String name) {
+ if (name.length() == 0) {
+ return "DefaultApiTest";
+ }
+ return camelize(name) + "ApiTest";
+ }
+
+ @Override
+ public CodegenOperation fromOperation(String path,
+ String httpMethod,
+ Operation operation,
+ List servers) {
+ CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
+ op.path = encodePath(path);
+ return op;
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
index 9849aa702f3..3521954fc48 100644
--- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
+++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
@@ -91,6 +91,7 @@ org.openapitools.codegen.languages.OpenAPIYamlGenerator
org.openapitools.codegen.languages.PlantumlDocumentationCodegen
org.openapitools.codegen.languages.PerlClientCodegen
org.openapitools.codegen.languages.PhpClientCodegen
+org.openapitools.codegen.languages.PhpFlightServerCodegen
org.openapitools.codegen.languages.PhpNextgenClientCodegen
org.openapitools.codegen.languages.PhpLaravelServerCodegen
org.openapitools.codegen.languages.PhpLumenServerCodegen
diff --git a/modules/openapi-generator/src/main/resources/php-flight/README.mustache b/modules/openapi-generator/src/main/resources/php-flight/README.mustache
new file mode 100644
index 00000000000..d510c3a5535
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/README.mustache
@@ -0,0 +1,10 @@
+## Requirements
+
+PHP 8.1 and later
+
+## Installation & Usage
+
+1. Set up flight as usual - see [Flight documentation](https://docs.flightphp.com/install)
+2. Generate using the OpenAPI generator
+3. Subclass some/all generated `Abstract*Api` and overwrite the methods you'd like handled. When implementing the `*Stream` methods, make sure to stream the response and not implement the non-stream method.
+4. Register routes for your subclassed apis: `RegisterRoutes::registerRoutes(new MyApiHandler());`
diff --git a/modules/openapi-generator/src/main/resources/php-flight/api.mustache b/modules/openapi-generator/src/main/resources/php-flight/api.mustache
new file mode 100644
index 00000000000..a0b146bd07d
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/api.mustache
@@ -0,0 +1,57 @@
+licenseInfo}}
+
+namespace {{apiPackage}};
+
+
+{{#operations}}abstract class {{classname}}
+{
+
+ {{#operation}}
+ /**
+ * Operation {{{operationId}}}
+ * Path: {{{path}}}
+ *
+ {{#summary}}
+ * {{{summary}}}
+ *
+ {{/summary}}
+ {{#vendorExtensions.x-nonFormParams}}
+ * @param {{vendorExtensions.x-parameter-type}} ${{paramName}} {{description}} {{#required}}(required){{/required}}{{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}}
+ {{/vendorExtensions.x-nonFormParams}}
+ *
+ * @return {{vendorExtensions.x-return-type-comment}}
+ {{#isDeprecated}}
+ * @deprecated
+ {{/isDeprecated}}
+ */
+ public function {{operationId}}({{#vendorExtensions.x-nonFormParams}}{{vendorExtensions.x-parameter-type}} ${{paramName}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-nonFormParams}}): {{vendorExtensions.x-return-type}}
+ {
+ throw new \Exception('Not implemented');
+ }
+
+ {{#returnContainer}}
+ /**
+ * Operation {{{operationId}}} (stream)
+ *
+ {{#summary}}
+ * {{{summary}}}
+ *
+ {{/summary}}
+ {{#vendorExtensions.x-nonFormParams}}
+ * @param {{vendorExtensions.x-parameter-type}} ${{paramName}} {{description}} {{#required}}(required){{/required}}{{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}}
+{{/vendorExtensions.x-nonFormParams}}
+ *
+ {{#isDeprecated}}
+ * @deprecated
+ {{/isDeprecated}}
+ */
+ public function {{operationId}}Stream({{#vendorExtensions.x-nonFormParams}}{{vendorExtensions.x-parameter-type}} ${{paramName}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-nonFormParams}}): void
+ {
+ throw new \Exception('Not implemented');
+ }
+ {{/returnContainer}}
+{{/operation}}
+}
+{{/operations}}
diff --git a/modules/openapi-generator/src/main/resources/php-flight/composer.mustache b/modules/openapi-generator/src/main/resources/php-flight/composer.mustache
new file mode 100644
index 00000000000..d922daab923
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/composer.mustache
@@ -0,0 +1,39 @@
+{
+ {{#composerPackageName}}
+ "name": "{{{.}}}",
+ {{/composerPackageName}}
+ "description": "{{appDescription}}.",
+ "keywords": ["openapi", "php", "framework", "flightphp"],
+ {{#artifactVersion}}
+ "version": "{{.}}",
+ {{/artifactVersion}}
+ "homepage": "{{{artifactUrl}}}",
+ "license": "{{{licenseName}}}",
+ "authors": [
+ {
+ "name": "{{{developerOrganization}}}",
+ "homepage": "{{{developerOrganizationUrl}}}"
+ }
+ ],
+ "type": "library",
+ "require": {
+ "php": "^8.1.0",
+ "flightphp/core": "^3.5.1",
+ "ext-json": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "config": {
+ "optimize-autoloader": true,
+ "preferred-install": "dist",
+ "sort-packages": true
+ },
+ "autoload": {
+ "psr-4": {
+ "{{escapedInvokerPackage}}\\": ""
+ }
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
+}
diff --git a/modules/openapi-generator/src/main/resources/php-flight/gitignore b/modules/openapi-generator/src/main/resources/php-flight/gitignore
new file mode 100644
index 00000000000..931a98c71bd
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/gitignore
@@ -0,0 +1,28 @@
+# ref: https://github.com/github/gitignore/blob/master/Composer.gitignore
+
+composer.phar
+/vendor/
+
+# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
+# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
+# composer.lock
+
+# phplint tool creates cache file which is not necessary in a codebase
+/.phplint-cache
+
+# Do not commit local PHPUnit config
+/phpunit.xml
+/.phpunit.result.cache
+
+# Do not commit local PHP_CodeSniffer config
+/phpcs.xml
+
+# Application config may contain sensitive data
+/config/**/*.*
+!/config/.htaccess
+!/config/dev/default.inc.php
+!/config/prod/default.inc.php
+
+# Logs folder
+/logs/**/*.*
+!/logs/.htaccess
diff --git a/modules/openapi-generator/src/main/resources/php-flight/licenseInfo.mustache b/modules/openapi-generator/src/main/resources/php-flight/licenseInfo.mustache
new file mode 100644
index 00000000000..c10b59eaab8
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/licenseInfo.mustache
@@ -0,0 +1,23 @@
+/**
+ {{#appName}}
+ * {{{.}}}
+ {{/appName}}
+ * PHP version 8.x
+ *
+ * @package {{invokerPackage}}
+ * @author OpenAPI Generator team
+ * @link https://github.com/openapitools/openapi-generator
+ */
+
+/**
+ {{#appDescription}}
+ * {{{.}}}
+ {{/appDescription}}
+ {{#version}}
+ * The version of the OpenAPI document: {{{.}}}
+ {{/version}}
+ {{#infoEmail}}
+ * Contact: {{{.}}}
+ {{/infoEmail}}
+ * Generated by: https://github.com/openapitools/openapi-generator.git
+ */
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/php-flight/model.mustache b/modules/openapi-generator/src/main/resources/php-flight/model.mustache
new file mode 100644
index 00000000000..c0fd08c25c5
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/model.mustache
@@ -0,0 +1,31 @@
+partial_header}}
+
+namespace {{modelPackage}};
+
+/**
+ * Class representing the {{classname}} model.
+ *
+{{#description}}
+ * {{.}}
+ *
+{{/description}}
+ * @package {{modelPackage}}
+ * @author OpenAPI Generator team
+ */
+{{#isEnum}}{{>model_enum}}{{/isEnum}}
+{{^isEnum}}{{>model_generic}}{{/isEnum}}
+{{/model}}{{/models}}
diff --git a/modules/openapi-generator/src/main/resources/php-flight/model_enum.mustache b/modules/openapi-generator/src/main/resources/php-flight/model_enum.mustache
new file mode 100644
index 00000000000..b12428b10e2
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/model_enum.mustache
@@ -0,0 +1,8 @@
+enum {{classname}}: {{dataType}}
+{
+{{#allowableValues}}
+ {{#enumVars}}
+ case {{{name}}} = {{{value}}};
+ {{/enumVars}}
+{{/allowableValues}}
+}
diff --git a/modules/openapi-generator/src/main/resources/php-flight/model_generic.mustache b/modules/openapi-generator/src/main/resources/php-flight/model_generic.mustache
new file mode 100644
index 00000000000..9748119501d
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/model_generic.mustache
@@ -0,0 +1,90 @@
+class {{classname}} {{#parentSchema}}extends {{{parent}}} {{/parentSchema}} implements \JsonSerializable
+{
+ {{#vars}}{{>model_variables}}
+ {{/vars}}
+ /**
+ * Constructor
+ *
+{{#vars}}
+ * @param {{vendorExtensions.x-comment-type}} ${{name}}
+{{/vars}}
+ */
+ public function __construct({{#vars}}{{vendorExtensions.x-parameter-type}} ${{name}}{{^-last}}, {{/-last}}{{/vars}})
+ {
+ {{#vars}}
+ $this->{{name}} = ${{name}};
+ {{/vars}}
+ }
+
+ public static function fromArray(array $data): self
+ {
+ return new self(
+ {{#vars}}
+ {{#isDateTime}}
+ isset($data['{{baseName}}']) ? new \DateTime($data['{{baseName}}']) : null{{^last}}, {{/last}}
+ {{/isDateTime}}
+ {{#isEnum}}
+ isset($data['{{baseName}}']) ? {{enumName}}::tryFrom($data['{{baseName}}']) : null{{^last}}, {{/last}}
+ {{/isEnum}}
+ {{#isEnumRef}}
+ isset($data['{{baseName}}']) ? {{complexType}}::tryFrom($data['{{baseName}}']) : null{{^last}}, {{/last}}
+ {{/isEnumRef}}
+ {{#isModel}}
+ isset($data['{{baseName}}']) ? {{complexType}}::fromArray($data['{{baseName}}']) : null{{^last}}, {{/last}}
+ {{/isModel}}
+ {{#isArray}}
+ {{#items.isEnumRef}}
+ isset($data['{{baseName}}']) ? array_map(fn($item) => {{items.complexType}}::tryFrom($item), $data['{{baseName}}']) : null{{^last}}, {{/last}}
+ {{/items.isEnumRef}}
+ {{#items.isModel}}
+ isset($data['{{baseName}}']) ? array_map(fn($item) => {{items.complexType}}::fromArray($item), $data['{{baseName}}']) : null{{^last}}, {{/last}}
+ {{/items.isModel}}
+ {{#items.isDateTime}}
+ isset($data['{{baseName}}']) ? array_map(fn($item) => new \DateTime($item), $data['{{baseName}}']) : null{{^last}}, {{/last}}
+ {{/items.isDateTime}}
+ {{^items.isEnumRef}}
+ {{^items.isModel}}
+ {{^items.isDateTime}}
+ $data['{{baseName}}'] ?? null{{^last}}, {{/last}}
+ {{/items.isDateTime}}
+ {{/items.isModel}}
+ {{/items.isEnumRef}}
+ {{/isArray}}
+ {{^isEnumRef}}
+ {{^isEnum}}
+ {{^isModel}}
+ {{^isArray}}
+ {{^isDateTime}}
+ $data['{{baseName}}'] ?? null{{^last}}, {{/last}}
+ {{/isDateTime}}
+ {{/isArray}}
+ {{/isModel}}
+ {{/isEnum}}
+ {{/isEnumRef}}
+ {{/vars}}
+ );
+ }
+
+ public function jsonSerialize(): mixed {
+ return [
+ {{#vars}}
+ {{#isDateTime}}
+ '{{baseName}}' => $this->{{name}}?->format('c'){{^last}}, {{/last}}
+ {{/isDateTime}}
+ {{#isArray}}
+ {{#items.isDateTime}}
+ '{{baseName}}' => $this->{{name}} ? array_map(fn($item) => $item->format('c'), $this->{{name}}) : null{{^last}}, {{/last}}
+ {{/items.isDateTime}}
+ {{^items.isDateTime}}
+ '{{baseName}}' => $this->{{name}}{{^last}}, {{/last}}
+ {{/items.isDateTime}}
+ {{/isArray}}
+ {{^isDateTime}}
+ {{^isArray}}
+ '{{baseName}}' => $this->{{name}}{{^last}}, {{/last}}
+ {{/isArray}}
+ {{/isDateTime}}
+ {{/vars}}
+ ];
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/php-flight/model_variables.mustache b/modules/openapi-generator/src/main/resources/php-flight/model_variables.mustache
new file mode 100644
index 00000000000..259ce295ea7
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/model_variables.mustache
@@ -0,0 +1,113 @@
+ /**
+ {{#description}}
+ * {{.}}
+ *
+ {{/description}}
+ * @var {{{vendorExtensions.x-comment-type}}}
+ * @SerializedName("{{baseName}}")
+{{#required}}
+ * @Assert\NotNull()
+ {{^isPrimitiveType}}
+ * @Assert\Valid()
+ {{/isPrimitiveType}}
+{{/required}}
+{{#isEnum}}
+ {{#isContainer}}
+ * @Assert\All({
+ {{#items}}
+ * @Assert\Choice({ {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}} })
+ {{/items}}
+ * })
+ {{/isContainer}}
+ {{^isContainer}}
+ * @Assert\Choice({ {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}} })
+ {{/isContainer}}
+{{/isEnum}}
+{{#isContainer}}
+ * @Assert\All({
+ {{#items}}
+ * @Assert\Type("{{dataType}}")
+ {{/items}}
+ * })
+ {{#isMap}}
+ {{#items}}
+ * @Type("array")
+ {{/items}}
+ {{/isMap}}
+ {{^isMap}}
+ {{#items}}
+ {{#isEnumRef}}
+ * @Accessor(getter="getSerialized{{nameInPascalCase}}")
+ * @Type("array")
+ {{/isEnumRef}}
+ {{^isEnumRef}}
+ * @Type("array<{{dataType}}>")
+ {{/isEnumRef}}
+ {{/items}}
+ {{/isMap}}
+{{/isContainer}}
+{{^isContainer}}
+ {{#isDate}}
+ * @Assert\Type("\Date")
+ * @Type("DateTime<'Y-m-d'>")
+ {{/isDate}}
+ {{#isDateTime}}
+ * @Assert\Type("\DateTime"))
+ * @Type("DateTime")
+ {{/isDateTime}}
+ {{#isEnumRef}}
+ * @Accessor(getter="getSerialized{{nameInPascalCase}}")
+ * @Type("string")
+ {{/isEnumRef}}
+ {{^isDate}}
+ {{^isDateTime}}
+ {{^isEnumRef}}
+ * @Assert\Type("{{dataType}}")
+ * @Type("{{dataType}}")
+ {{/isEnumRef}}
+ {{/isDateTime}}
+ {{/isDate}}
+{{/isContainer}}
+{{#hasValidation}}
+ {{#maxLength}}
+ * @Assert\Length(
+ * max = {{.}}
+ * )
+ {{/maxLength}}
+ {{#minLength}}
+ * @Assert\Length(
+ * min = {{.}}
+ * )
+ {{/minLength}}
+ {{#minimum}}
+ {{#exclusiveMinimum}}
+ * @Assert\GreaterThan({{minimum}})
+ {{/exclusiveMinimum}}
+ {{^exclusiveMinimum}}
+ * @Assert\GreaterThanOrEqual({{minimum}})
+ {{/exclusiveMinimum}}
+ {{/minimum}}
+ {{#maximum}}
+ {{#exclusiveMaximum}}
+ * @Assert\LessThan({{maximum}})
+ {{/exclusiveMaximum}}
+ {{^exclusiveMaximum}}
+ * @Assert\LessThanOrEqual({{maximum}})
+ {{/exclusiveMaximum}}
+ {{/maximum}}
+ {{#pattern}}
+ * @Assert\Regex("/{{.}}/")
+ {{/pattern}}
+ {{#maxItems}}
+ * @Assert\Count(
+ * max = {{.}}
+ * )
+ {{/maxItems}}
+ {{#minItems}}
+ * @Assert\Count(
+ * min = {{.}}
+ * )
+ {{/minItems}}
+{{/hasValidation}}
+ */
+ public {{{vendorExtensions.x-parameter-type}}} ${{name}}{{#defaultValue}} = {{{defaultValue}}}{{/defaultValue}};
diff --git a/modules/openapi-generator/src/main/resources/php-flight/partial_header.mustache b/modules/openapi-generator/src/main/resources/php-flight/partial_header.mustache
new file mode 100644
index 00000000000..01bb4a845c9
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/partial_header.mustache
@@ -0,0 +1,14 @@
+/**
+ {{#appName}}
+ * {{{.}}}
+ *
+ {{/appName}}
+ {{#appDescription}}
+ * {{{.}}}
+ *
+ {{/appDescription}}
+ * {{#version}}The version of the OpenAPI document: {{{.}}}{{/version}}
+ * {{#infoEmail}}Contact: {{{.}}}{{/infoEmail}}
+ * Generated by: https://github.com/openapitools/openapi-generator.git
+ *
+ */
diff --git a/modules/openapi-generator/src/main/resources/php-flight/phpunit.mustache b/modules/openapi-generator/src/main/resources/php-flight/phpunit.mustache
new file mode 100644
index 00000000000..f7df0c08377
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/phpunit.mustache
@@ -0,0 +1,28 @@
+
+
+
+
+ {{#lambda.forwardslash}}{{apiSrcPath}}{{/lambda.forwardslash}}
+ {{#lambda.forwardslash}}{{modelSrcPath}}{{/lambda.forwardslash}}
+ .
+
+
+ {{#lambda.forwardslash}}{{testSrcPath}}{{/lambda.forwardslash}}
+
+
+
+
+ {{#lambda.forwardslash}}{{testSrcPath}}{{/lambda.forwardslash}}
+
+
+
+
+
+
diff --git a/modules/openapi-generator/src/main/resources/php-flight/register_routes.mustache b/modules/openapi-generator/src/main/resources/php-flight/register_routes.mustache
new file mode 100644
index 00000000000..c66e90b42b9
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/register_routes.mustache
@@ -0,0 +1,85 @@
+licenseInfo}}
+
+namespace {{invokerPackage}};
+{{#apiInfo}}
+
+class RegisterRoutes {
+
+ static public function registerRoutes({{#apis}}\{{apiPackage}}\{{classname}}{{^-last}}|{{/-last}}{{/apis}} $handler): void
+ {
+ $reflectionClass = new \ReflectionClass($handler);
+
+ {{#apis}}
+ {{#operations}}
+ {{#operation}}
+ if (declaresMethod($reflectionClass, '{{operationId}}') && declaresMethod($reflectionClass, '{{operationId}}Stream')) {
+ throw new \Exception('Operation {{operationId}} cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, '{{operationId}}')) {
+ \Flight::route('{{httpMethod}} {{vendorExtensions.x-path}}', function ({{#pathParams}}string ${{paramName}}{{^-last}}, {{/-last}}{{/pathParams}}) use ($handler) {
+ $r = \Flight::request();
+ {{^vendorExtensions.x-return-type-is-void}}$result = {{/vendorExtensions.x-return-type-is-void}}$handler->{{operationId}}(
+ {{#vendorExtensions.x-nonFormParams}}
+ parseParam({{#isBodyParam}}json_decode($r->getBody(), true){{/isBodyParam}}{{#isQueryParam}}$r->query['{{baseName}}'] ?? null{{/isQueryParam}}{{#isPathParam}}${{paramName}}{{/isPathParam}}{{#isHeaderParam}}$r->getHeader('{{baseName}}'){{/isHeaderParam}}, '{{vendorExtensions.x-comment-type-escaped}}'){{^-last}}, {{/-last}}
+ {{/vendorExtensions.x-nonFormParams}}
+ );
+ {{^vendorExtensions.x-return-type-is-void}}
+ if ($result === null) {
+ \Flight::halt(204);
+ } else {
+ \Flight::json($result);
+ }
+ {{/vendorExtensions.x-return-type-is-void}}
+ {{#vendorExtensions.x-return-type-is-void}}
+ \Flight::halt(204);
+ {{/vendorExtensions.x-return-type-is-void}}
+ });
+ }
+ {{#returnContainer}}
+ if (declaresMethod($reflectionClass, '{{operationId}}Stream')) {
+ \Flight::route('{{httpMethod}} {{vendorExtensions.x-path}}', function ({{#pathParams}}string ${{paramName}}{{^-last}}, {{/-last}}{{/pathParams}}) use ($handler) {
+ $r = \Flight::request();
+ $handler->{{operationId}}Stream(
+ {{#vendorExtensions.x-nonFormParams}}
+ parseParam({{#isBodyParam}}json_decode($r->getBody(), true){{/isBodyParam}}{{#isQueryParam}}$r->query['{{baseName}}'] ?? null{{/isQueryParam}}{{#isPathParam}}${{paramName}}{{/isPathParam}}{{#isHeaderParam}}$r->getHeader('{{baseName}}'){{/isHeaderParam}}, '{{vendorExtensions.x-comment-type-escaped}}'){{^-last}}, {{/-last}}
+ {{/vendorExtensions.x-nonFormParams}}
+ );
+ // ignore return value: streaming expected
+ })->streamWithHeaders(['Content-Type' => 'application/json']);
+ }
+ {{/returnContainer}}
+
+ {{/operation}}
+ {{/operations}}
+ {{/apis}}
+ }
+}
+
+{{/apiInfo}}
+
+function parseParam(mixed $param, string $type)
+{
+ $nonNullType = str_replace('?', '', $type);
+ if ($param === null) {
+ return null;
+ } elseif ($nonNullType === 'int') {
+ return intval($param);
+ } elseif ($nonNullType === 'float') {
+ return floatval($param);
+ } elseif ($nonNullType === 'bool') {
+ return filter_var($param, FILTER_VALIDATE_BOOLEAN);
+ } elseif (str_ends_with($type, '[]')) {
+ return array_map(fn($el) => parseParam($el, substr($type, 0, -2)), $param);
+ } elseif (str_starts_with($nonNullType, '\\{{escapedModelPackage}}')) {
+ return new $nonNullType($param);
+ } else {
+ return $param;
+ }
+}
+
+function declaresMethod(\ReflectionClass $reflectionClass, string $methodName): bool
+{
+ return $reflectionClass->hasMethod($methodName) && $reflectionClass->getMethod($methodName)->getDeclaringClass()->getName() === $reflectionClass->getName();
+}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/php-flight/register_routes_test.mustache b/modules/openapi-generator/src/main/resources/php-flight/register_routes_test.mustache
new file mode 100644
index 00000000000..c50fbade9c5
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-flight/register_routes_test.mustache
@@ -0,0 +1,31 @@
+licenseInfo}}
+
+{{#apiInfo}}
+namespace {{testPackage}};
+
+class RegisterRoutesTest extends \PHPUnit\Framework\TestCase {
+{{#apis}}
+ public function testRegisterRoutes{{classname}}(): void
+ {
+ $handler = new class extends \{{apiPackage}}\{{classname}} {
+ {{#operations}}
+ {{#operation}}
+ {{#-first}}
+ public function {{operationId}}({{#vendorExtensions.x-nonFormParams}}{{^isFormParam}}{{vendorExtensions.x-parameter-type}} ${{paramName}}{{^-last}}, {{/-last}}{{/isFormParam}}{{/vendorExtensions.x-nonFormParams}}): {{vendorExtensions.x-return-type}}
+ {
+ {{^vendorExtensions.x-return-type-is-void}}
+ return null;
+ {{/vendorExtensions.x-return-type-is-void}}
+ }
+ {{/-first}}
+ {{/operation}}
+ {{/operations}}
+ };
+ \{{invokerPackage}}\RegisterRoutes::registerRoutes($handler);
+ $this->assertTrue(true);
+ }
+{{/apis}}
+}
+{{/apiInfo}}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/flight/PhpFlightServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/flight/PhpFlightServerCodegenTest.java
new file mode 100644
index 00000000000..cbba0a00ed8
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/flight/PhpFlightServerCodegenTest.java
@@ -0,0 +1,74 @@
+package org.openapitools.codegen.php.flight;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.openapitools.codegen.ClientOptInput;
+import org.openapitools.codegen.DefaultGenerator;
+import org.openapitools.codegen.TestUtils;
+import org.openapitools.codegen.config.CodegenConfigurator;
+import org.testng.annotations.Test;
+
+public class PhpFlightServerCodegenTest {
+
+ @Test
+ public void shouldGenerateModel() throws Exception {
+ Map properties = new HashMap<>();
+
+ File output = Files.createTempDirectory("test").toFile();
+
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("php-flight")
+ .setAdditionalProperties(properties)
+ .setInputSpec("src/test/resources/3_0/petstore-php-flight.yaml")
+ .setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ final ClientOptInput clientOptInput = configurator.toClientOptInput();
+ DefaultGenerator generator = new DefaultGenerator();
+ List files = generator.opts(clientOptInput).generate();
+
+ TestUtils.ensureContainsFile(files, output, "Api/AbstractPetApi.php");
+ TestUtils.ensureContainsFile(files, output, "Model/Pet.php");
+ TestUtils.ensureContainsFile(files, output, "Model/StandaloneEnum.php");
+ TestUtils.ensureContainsFile(files, output, "Model/PetStatus.php"); // inline enum
+ TestUtils.ensureContainsFile(files, output, "README.md");
+ TestUtils.ensureContainsFile(files, output, "RegisterRoutes.php");
+
+ java.nio.file.Path petModelFile = files.stream().filter(f -> f.getName().contains("Pet.php")).findFirst().orElseThrow().toPath();
+ TestUtils.assertFileContains(petModelFile, "namespace OpenAPIServer\\Model;");
+ TestUtils.assertFileContains(petModelFile, "public int $id;");
+ TestUtils.assertFileContains(petModelFile, "public ?string $name;");
+ TestUtils.assertFileContains(petModelFile, "public ?\\DateTime $dateTimeAttribute;");
+ TestUtils.assertFileContains(petModelFile, "@var Order[]|null");
+ TestUtils.assertFileContains(petModelFile, "public ?array $objectList;");
+ TestUtils.assertFileContains(petModelFile, "$data['photo_urls'] ?? null,");
+ TestUtils.assertFileContains(petModelFile, "'photo_urls' => $this->photoUrls");
+
+ TestUtils.assertFileContains(petModelFile, "isset($data['category']) ? Category::fromArray($data['category']) : null,");
+ TestUtils.assertFileContains(petModelFile, "isset($data['status']) ? PetStatus::tryFrom($data['status']) : null");
+ TestUtils.assertFileContains(petModelFile, "isset($data['refEnum']) ? StandaloneEnum::tryFrom($data['refEnum']) : null");
+ TestUtils.assertFileContains(petModelFile, "isset($data['dateTimeAttribute']) ? new \\DateTime($data['dateTimeAttribute']) : null");
+
+ java.nio.file.Path petApiFile = files.stream().filter(f -> f.getName().contains("AbstractPetApi.php")).findFirst().orElseThrow().toPath();
+ TestUtils.assertFileContains(petApiFile, "namespace OpenAPIServer\\Api;");
+ TestUtils.assertFileContains(petApiFile, "public function getPetById(int $petId)");
+ TestUtils.assertFileContains(petApiFile, "public function updatePet(\\OpenAPIServer\\Model\\Pet $pet): \\OpenAPIServer\\Model\\Pet|null");
+
+ java.nio.file.Path registerRoutesFile = files.stream().filter(f -> f.getName().contains("RegisterRoutes.php")).findFirst().orElseThrow().toPath();
+ TestUtils.assertFileContains(registerRoutesFile, "function registerRoutes(\\OpenAPIServer\\Api\\AbstractPetApi|\\OpenAPIServer\\Api\\AbstractUserApi $handler): void");
+ TestUtils.assertFileContains(registerRoutesFile,
+ "Flight::route('POST /user/createWithArray/@pathParamInt/@pathParamString', function (string $pathParamInt, string $pathParamString) use ($handler) {");
+ TestUtils.assertFileContains(registerRoutesFile, "parseParam($pathParamInt, 'int')");
+ TestUtils.assertFileContains(registerRoutesFile, "parseParam($pathParamString, 'string')");
+ TestUtils.assertFileContains(registerRoutesFile, "parseParam(json_decode($r->getBody(), true), '\\\\OpenAPIServer\\\\Model\\\\User[]')");
+ TestUtils.assertFileContains(registerRoutesFile, "parseParam($r->getHeader('api_key'), '?string')");
+
+ Files.readAllLines(files.stream().filter(f -> f.getName().contains("RegisterRoutesTest.php")).findFirst().orElseThrow().toPath()).forEach(System.out::println);
+ java.nio.file.Path registerRoutesTestFile = files.stream().filter(f -> f.getName().contains("RegisterRoutesTest.php")).findFirst().orElseThrow().toPath();
+ TestUtils.assertFileContains(registerRoutesTestFile, "namespace OpenAPIServer\\Test;");
+
+ output.deleteOnExit();
+ }
+}
diff --git a/modules/openapi-generator/src/test/resources/3_0/petstore-php-flight.yaml b/modules/openapi-generator/src/test/resources/3_0/petstore-php-flight.yaml
new file mode 100644
index 00000000000..ded453c4dd3
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/petstore-php-flight.yaml
@@ -0,0 +1,411 @@
+openapi: 3.0.0
+servers:
+ - url: 'http://petstore.swagger.io/v2'
+info:
+ description: >-
+ This is a sample server Petstore server. For this sample, you can use the api key
+ `special-key` to test the authorization filters.
+ version: 1.0.0
+ title: OpenAPI Petstore
+ license:
+ name: Apache-2.0
+ url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
+tags:
+ - name: pet
+ description: Everything about your Pets
+ - name: store
+ description: Access to Petstore orders
+ - name: user
+ description: Operations about user
+paths:
+ /pet:
+ post:
+ tags:
+ - pet
+ summary: Add a new pet to the store
+ description: ''
+ operationId: addPet
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ '405':
+ description: Invalid input
+ requestBody:
+ $ref: '#/components/requestBodies/Pet'
+ put:
+ tags:
+ - pet
+ summary: Update an existing pet
+ description: ''
+ operationId: updatePet
+ externalDocs:
+ url: "http://petstore.swagger.io/v2/doc/updatePet"
+ description: "API documentation for the updatePet operation"
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ '400':
+ description: Invalid ID supplied
+ '404':
+ description: Pet not found
+ '405':
+ description: Validation exception
+ requestBody:
+ $ref: '#/components/requestBodies/Pet'
+ '/pet/{petId}':
+ get:
+ tags:
+ - pet
+ summary: Find pet by ID
+ description: Returns a single pet
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ description: ID of pet to return
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ '400':
+ description: Invalid ID supplied
+ '404':
+ description: Pet not found
+ security:
+ - api_key: []
+ post:
+ tags:
+ - pet
+ summary: Updates a pet in the store with form data
+ description: ''
+ operationId: updatePetWithForm
+ parameters:
+ - name: petId
+ in: path
+ description: ID of pet that needs to be updated
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '405':
+ description: Invalid input
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ description: Updated name of the pet
+ type: string
+ status:
+ description: Updated status of the pet
+ type: string
+ delete:
+ tags:
+ - pet
+ summary: Deletes a pet
+ description: ''
+ operationId: deletePet
+ parameters:
+ - name: api_key
+ in: header
+ required: false
+ schema:
+ type: string
+ - name: petId
+ in: path
+ description: Pet id to delete
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '400':
+ description: Invalid pet value
+ /user/createWithArray/{pathParamInt}/{pathParamString}:
+ post:
+ tags:
+ - user
+ summary: Creates list of users with given input array
+ description: ''
+ operationId: createUsersWithArrayInput
+ parameters:
+ - name: pathParamInt
+ in: path
+ required: true
+ schema:
+ type: integer
+ format: int64
+ - name: pathParamString
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ default:
+ description: successful operation
+ requestBody:
+ $ref: '#/components/requestBodies/UserArray'
+ /user/login:
+ get:
+ tags:
+ - user
+ summary: Logs user into the system
+ description: ''
+ operationId: loginUser
+ parameters:
+ - name: username
+ in: query
+ description: The user name for login
+ required: true
+ schema:
+ type: string
+ pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$'
+ - name: password
+ in: query
+ description: The password for login in clear text
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers:
+ Set-Cookie:
+ description: >-
+ Cookie authentication key for use with the `api_key`
+ apiKey authentication.
+ schema:
+ type: string
+ example: AUTH_KEY=abcde12345; Path=/; HttpOnly
+ X-Rate-Limit:
+ description: calls per hour allowed by the user
+ schema:
+ type: integer
+ format: int32
+ X-Expires-After:
+ description: date in UTC when token expires
+ schema:
+ type: string
+ format: date-time
+ content:
+ application/json:
+ schema:
+ type: string
+ '400':
+ description: Invalid username/password supplied
+externalDocs:
+ description: Find out more about Swagger
+ url: 'http://swagger.io'
+components:
+ requestBodies:
+ UserArray:
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/User'
+ description: List of user object
+ required: true
+ Pet:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ description: Pet object that needs to be added to the store
+ required: true
+ securitySchemes:
+ petstore_auth:
+ type: oauth2
+ flows:
+ implicit:
+ authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog'
+ scopes:
+ 'write:pets': modify pets in your account
+ 'read:pets': read your pets
+ api_key:
+ type: apiKey
+ name: api_key
+ in: header
+ schemas:
+ StandaloneEnum:
+ type: string
+ enum:
+ - firstValue
+ - secondValue
+ Order:
+ title: Pet Order
+ description: An order for a pets from the pet store
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ petId:
+ type: integer
+ format: int64
+ quantity:
+ type: integer
+ format: int32
+ shipDate:
+ type: string
+ format: date-time
+ status:
+ type: string
+ description: Order Status
+ enum:
+ - placed
+ - approved
+ - delivered
+ complete:
+ type: boolean
+ default: false
+ xml:
+ name: Order
+ Category:
+ title: Pet category
+ description: A category for a pet
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$'
+ xml:
+ name: Category
+ User:
+ title: a User
+ description: A User who is purchasing from the pet store
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ username:
+ type: string
+ firstName:
+ type: string
+ lastName:
+ type: string
+ email:
+ type: string
+ password:
+ type: string
+ phone:
+ type: string
+ userStatus:
+ type: integer
+ format: int32
+ description: User Status
+ xml:
+ name: User
+ Tag:
+ title: Pet Tag
+ description: A tag for a pet
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ xml:
+ name: Tag
+ Pet:
+ title: a Pet
+ description: A pet for sale in the pet store
+ type: object
+ required:
+ - id
+ - photoUrls
+ properties:
+ id:
+ type: integer
+ format: int64
+ category:
+ $ref: '#/components/schemas/Category'
+ name:
+ type: string
+ example: doggie
+ photo_urls:
+ type: array
+ items:
+ type: string
+ tags:
+ type: array
+ items:
+ $ref: '#/components/schemas/Tag'
+ refEnum:
+ $ref: '#/components/schemas/StandaloneEnum'
+ status:
+ type: string
+ description: pet status in the store
+ deprecated: true
+ enum:
+ - available
+ - pending
+ - sold
+ dateTimeAttribute:
+ type: string
+ format: date-time
+ primitiveList:
+ type: array
+ items:
+ type: integer
+ objectList:
+ type: array
+ items:
+ $ref: '#/components/schemas/Order'
+ dateTimeList:
+ type: array
+ items:
+ type: string
+ format: date-time
+ inlineEnumList:
+ type: array
+ items:
+ type: string
+ enum:
+ - firstValue
+ - secondValue
+ refEnumList:
+ type: array
+ items:
+ $ref: '#/components/schemas/StandaloneEnum'
+ xml:
+ name: Pet
+ ApiResponse:
+ title: An uploaded response
+ description: Describes the result of uploading an image resource
+ type: object
+ properties:
+ code:
+ type: integer
+ format: int32
+ type:
+ type: string
+ message:
+ type: string
diff --git a/samples/server/petstore/php-flight/.gitignore b/samples/server/petstore/php-flight/.gitignore
new file mode 100644
index 00000000000..931a98c71bd
--- /dev/null
+++ b/samples/server/petstore/php-flight/.gitignore
@@ -0,0 +1,28 @@
+# ref: https://github.com/github/gitignore/blob/master/Composer.gitignore
+
+composer.phar
+/vendor/
+
+# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
+# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
+# composer.lock
+
+# phplint tool creates cache file which is not necessary in a codebase
+/.phplint-cache
+
+# Do not commit local PHPUnit config
+/phpunit.xml
+/.phpunit.result.cache
+
+# Do not commit local PHP_CodeSniffer config
+/phpcs.xml
+
+# Application config may contain sensitive data
+/config/**/*.*
+!/config/.htaccess
+!/config/dev/default.inc.php
+!/config/prod/default.inc.php
+
+# Logs folder
+/logs/**/*.*
+!/logs/.htaccess
diff --git a/samples/server/petstore/php-flight/.openapi-generator-ignore b/samples/server/petstore/php-flight/.openapi-generator-ignore
new file mode 100644
index 00000000000..7484ee590a3
--- /dev/null
+++ b/samples/server/petstore/php-flight/.openapi-generator-ignore
@@ -0,0 +1,23 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
diff --git a/samples/server/petstore/php-flight/.openapi-generator/FILES b/samples/server/petstore/php-flight/.openapi-generator/FILES
new file mode 100644
index 00000000000..248dfdf733d
--- /dev/null
+++ b/samples/server/petstore/php-flight/.openapi-generator/FILES
@@ -0,0 +1,18 @@
+.gitignore
+Api/AbstractPetApi.php
+Api/AbstractStoreApi.php
+Api/AbstractUserApi.php
+Model/ApiResponse.php
+Model/Category.php
+Model/FindPetsByStatusStatusParameterInner.php
+Model/Order.php
+Model/OrderStatus.php
+Model/Pet.php
+Model/PetStatus.php
+Model/Tag.php
+Model/User.php
+README.md
+RegisterRoutes.php
+Test/RegisterRoutesTest.php
+composer.json
+phpunit.xml.dist
diff --git a/samples/server/petstore/php-flight/.openapi-generator/VERSION b/samples/server/petstore/php-flight/.openapi-generator/VERSION
new file mode 100644
index 00000000000..ecb21862b1e
--- /dev/null
+++ b/samples/server/petstore/php-flight/.openapi-generator/VERSION
@@ -0,0 +1 @@
+7.6.0-SNAPSHOT
diff --git a/samples/server/petstore/php-flight/Api/AbstractPetApi.php b/samples/server/petstore/php-flight/Api/AbstractPetApi.php
new file mode 100644
index 00000000000..f895ed6e273
--- /dev/null
+++ b/samples/server/petstore/php-flight/Api/AbstractPetApi.php
@@ -0,0 +1,171 @@
+code = $code;
+ $this->type = $type;
+ $this->message = $message;
+ }
+
+ public static function fromArray(array $data): self
+ {
+ return new self(
+ $data['code'] ?? null,
+ $data['type'] ?? null,
+ $data['message'] ?? null,
+ );
+ }
+
+ public function jsonSerialize(): mixed {
+ return [
+ 'code' => $this->code,
+ 'type' => $this->type,
+ 'message' => $this->message,
+ ];
+ }
+}
+
+
diff --git a/samples/server/petstore/php-flight/Model/Category.php b/samples/server/petstore/php-flight/Model/Category.php
new file mode 100644
index 00000000000..4f3f6e9d81b
--- /dev/null
+++ b/samples/server/petstore/php-flight/Model/Category.php
@@ -0,0 +1,83 @@
+id = $id;
+ $this->name = $name;
+ }
+
+ public static function fromArray(array $data): self
+ {
+ return new self(
+ $data['id'] ?? null,
+ $data['name'] ?? null,
+ );
+ }
+
+ public function jsonSerialize(): mixed {
+ return [
+ 'id' => $this->id,
+ 'name' => $this->name,
+ ];
+ }
+}
+
+
diff --git a/samples/server/petstore/php-flight/Model/FindPetsByStatusStatusParameterInner.php b/samples/server/petstore/php-flight/Model/FindPetsByStatusStatusParameterInner.php
new file mode 100644
index 00000000000..752121f877d
--- /dev/null
+++ b/samples/server/petstore/php-flight/Model/FindPetsByStatusStatusParameterInner.php
@@ -0,0 +1,41 @@
+id = $id;
+ $this->petId = $petId;
+ $this->quantity = $quantity;
+ $this->shipDate = $shipDate;
+ $this->status = $status;
+ $this->complete = $complete;
+ }
+
+ public static function fromArray(array $data): self
+ {
+ return new self(
+ $data['id'] ?? null,
+ $data['petId'] ?? null,
+ $data['quantity'] ?? null,
+ isset($data['shipDate']) ? new \DateTime($data['shipDate']) : null,
+ isset($data['status']) ? OrderStatus::tryFrom($data['status']) : null,
+ $data['complete'] ?? null,
+ );
+ }
+
+ public function jsonSerialize(): mixed {
+ return [
+ 'id' => $this->id,
+ 'petId' => $this->petId,
+ 'quantity' => $this->quantity,
+ 'shipDate' => $this->shipDate?->format('c'),
+ 'status' => $this->status,
+ 'complete' => $this->complete,
+ ];
+ }
+}
+
+
diff --git a/samples/server/petstore/php-flight/Model/OrderStatus.php b/samples/server/petstore/php-flight/Model/OrderStatus.php
new file mode 100644
index 00000000000..92a8d69c54b
--- /dev/null
+++ b/samples/server/petstore/php-flight/Model/OrderStatus.php
@@ -0,0 +1,43 @@
+")
+ */
+ public array $photoUrls;
+
+ /**
+ * @var Tag[]|null
+ * @SerializedName("tags")
+ * @Assert\All({
+ * @Assert\Type("\OpenAPIServer\Model\Tag")
+ * })
+ * @Type("array<\OpenAPIServer\Model\Tag>")
+ */
+ public ?array $tags;
+
+ /**
+ * @var PetStatus|null
+ * @SerializedName("status")
+ * @Accessor(getter="getSerializedStatus")
+ * @Type("string")
+ */
+ public ?PetStatus $status;
+
+ /**
+ * Constructor
+ *
+ * @param int|null $id
+ * @param Category|null $category
+ * @param string $name
+ * @param string[] $photoUrls
+ * @param Tag[]|null $tags
+ * @param PetStatus|null $status
+ */
+ public function __construct(?int $id, ?Category $category, string $name, array $photoUrls, ?array $tags, ?PetStatus $status)
+ {
+ $this->id = $id;
+ $this->category = $category;
+ $this->name = $name;
+ $this->photoUrls = $photoUrls;
+ $this->tags = $tags;
+ $this->status = $status;
+ }
+
+ public static function fromArray(array $data): self
+ {
+ return new self(
+ $data['id'] ?? null,
+ isset($data['category']) ? Category::fromArray($data['category']) : null,
+ $data['name'] ?? null,
+ $data['photoUrls'] ?? null,
+ isset($data['tags']) ? array_map(fn($item) => Tag::fromArray($item), $data['tags']) : null,
+ isset($data['status']) ? PetStatus::tryFrom($data['status']) : null,
+ );
+ }
+
+ public function jsonSerialize(): mixed {
+ return [
+ 'id' => $this->id,
+ 'category' => $this->category,
+ 'name' => $this->name,
+ 'photoUrls' => $this->photoUrls,
+ 'tags' => $this->tags,
+ 'status' => $this->status,
+ ];
+ }
+}
+
+
diff --git a/samples/server/petstore/php-flight/Model/PetStatus.php b/samples/server/petstore/php-flight/Model/PetStatus.php
new file mode 100644
index 00000000000..b0c5fedb730
--- /dev/null
+++ b/samples/server/petstore/php-flight/Model/PetStatus.php
@@ -0,0 +1,43 @@
+id = $id;
+ $this->name = $name;
+ }
+
+ public static function fromArray(array $data): self
+ {
+ return new self(
+ $data['id'] ?? null,
+ $data['name'] ?? null,
+ );
+ }
+
+ public function jsonSerialize(): mixed {
+ return [
+ 'id' => $this->id,
+ 'name' => $this->name,
+ ];
+ }
+}
+
+
diff --git a/samples/server/petstore/php-flight/Model/User.php b/samples/server/petstore/php-flight/Model/User.php
new file mode 100644
index 00000000000..d5d4fbd5871
--- /dev/null
+++ b/samples/server/petstore/php-flight/Model/User.php
@@ -0,0 +1,156 @@
+id = $id;
+ $this->username = $username;
+ $this->firstName = $firstName;
+ $this->lastName = $lastName;
+ $this->email = $email;
+ $this->password = $password;
+ $this->phone = $phone;
+ $this->userStatus = $userStatus;
+ }
+
+ public static function fromArray(array $data): self
+ {
+ return new self(
+ $data['id'] ?? null,
+ $data['username'] ?? null,
+ $data['firstName'] ?? null,
+ $data['lastName'] ?? null,
+ $data['email'] ?? null,
+ $data['password'] ?? null,
+ $data['phone'] ?? null,
+ $data['userStatus'] ?? null,
+ );
+ }
+
+ public function jsonSerialize(): mixed {
+ return [
+ 'id' => $this->id,
+ 'username' => $this->username,
+ 'firstName' => $this->firstName,
+ 'lastName' => $this->lastName,
+ 'email' => $this->email,
+ 'password' => $this->password,
+ 'phone' => $this->phone,
+ 'userStatus' => $this->userStatus,
+ ];
+ }
+}
+
+
diff --git a/samples/server/petstore/php-flight/README.md b/samples/server/petstore/php-flight/README.md
new file mode 100644
index 00000000000..d510c3a5535
--- /dev/null
+++ b/samples/server/petstore/php-flight/README.md
@@ -0,0 +1,10 @@
+## Requirements
+
+PHP 8.1 and later
+
+## Installation & Usage
+
+1. Set up flight as usual - see [Flight documentation](https://docs.flightphp.com/install)
+2. Generate using the OpenAPI generator
+3. Subclass some/all generated `Abstract*Api` and overwrite the methods you'd like handled. When implementing the `*Stream` methods, make sure to stream the response and not implement the non-stream method.
+4. Register routes for your subclassed apis: `RegisterRoutes::registerRoutes(new MyApiHandler());`
diff --git a/samples/server/petstore/php-flight/RegisterRoutes.php b/samples/server/petstore/php-flight/RegisterRoutes.php
new file mode 100644
index 00000000000..367e0df8daf
--- /dev/null
+++ b/samples/server/petstore/php-flight/RegisterRoutes.php
@@ -0,0 +1,380 @@
+addPet(
+ parseParam(json_decode($r->getBody(), true), '\\OpenAPIServer\\Model\\Pet')
+ );
+ if ($result === null) {
+ \Flight::halt(204);
+ } else {
+ \Flight::json($result);
+ }
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'deletePet') && declaresMethod($reflectionClass, 'deletePetStream')) {
+ throw new \Exception('Operation deletePet cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'deletePet')) {
+ \Flight::route('DELETE /pet/@petId', function (string $petId) use ($handler) {
+ $r = \Flight::request();
+ $handler->deletePet(
+ parseParam($petId, 'int'),
+ parseParam($r->getHeader('api_key'), '?string')
+ );
+ \Flight::halt(204);
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'findPetsByStatus') && declaresMethod($reflectionClass, 'findPetsByStatusStream')) {
+ throw new \Exception('Operation findPetsByStatus cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'findPetsByStatus')) {
+ \Flight::route('GET /pet/findByStatus', function () use ($handler) {
+ $r = \Flight::request();
+ $result = $handler->findPetsByStatus(
+ parseParam($r->query['status'] ?? null, '\\OpenAPIServer\\Model\\FindPetsByStatusStatusParameterInner[]')
+ );
+ if ($result === null) {
+ \Flight::halt(204);
+ } else {
+ \Flight::json($result);
+ }
+ });
+ }
+ if (declaresMethod($reflectionClass, 'findPetsByStatusStream')) {
+ \Flight::route('GET /pet/findByStatus', function () use ($handler) {
+ $r = \Flight::request();
+ $handler->findPetsByStatusStream(
+ parseParam($r->query['status'] ?? null, '\\OpenAPIServer\\Model\\FindPetsByStatusStatusParameterInner[]')
+ );
+ // ignore return value: streaming expected
+ })->streamWithHeaders(['Content-Type' => 'application/json']);
+ }
+
+ if (declaresMethod($reflectionClass, 'findPetsByTags') && declaresMethod($reflectionClass, 'findPetsByTagsStream')) {
+ throw new \Exception('Operation findPetsByTags cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'findPetsByTags')) {
+ \Flight::route('GET /pet/findByTags', function () use ($handler) {
+ $r = \Flight::request();
+ $result = $handler->findPetsByTags(
+ parseParam($r->query['tags'] ?? null, 'string[]')
+ );
+ if ($result === null) {
+ \Flight::halt(204);
+ } else {
+ \Flight::json($result);
+ }
+ });
+ }
+ if (declaresMethod($reflectionClass, 'findPetsByTagsStream')) {
+ \Flight::route('GET /pet/findByTags', function () use ($handler) {
+ $r = \Flight::request();
+ $handler->findPetsByTagsStream(
+ parseParam($r->query['tags'] ?? null, 'string[]')
+ );
+ // ignore return value: streaming expected
+ })->streamWithHeaders(['Content-Type' => 'application/json']);
+ }
+
+ if (declaresMethod($reflectionClass, 'getPetById') && declaresMethod($reflectionClass, 'getPetByIdStream')) {
+ throw new \Exception('Operation getPetById cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'getPetById')) {
+ \Flight::route('GET /pet/@petId', function (string $petId) use ($handler) {
+ $r = \Flight::request();
+ $result = $handler->getPetById(
+ parseParam($petId, 'int')
+ );
+ if ($result === null) {
+ \Flight::halt(204);
+ } else {
+ \Flight::json($result);
+ }
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'updatePet') && declaresMethod($reflectionClass, 'updatePetStream')) {
+ throw new \Exception('Operation updatePet cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'updatePet')) {
+ \Flight::route('PUT /pet', function () use ($handler) {
+ $r = \Flight::request();
+ $result = $handler->updatePet(
+ parseParam(json_decode($r->getBody(), true), '\\OpenAPIServer\\Model\\Pet')
+ );
+ if ($result === null) {
+ \Flight::halt(204);
+ } else {
+ \Flight::json($result);
+ }
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'updatePetWithForm') && declaresMethod($reflectionClass, 'updatePetWithFormStream')) {
+ throw new \Exception('Operation updatePetWithForm cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'updatePetWithForm')) {
+ \Flight::route('POST /pet/@petId', function (string $petId) use ($handler) {
+ $r = \Flight::request();
+ $handler->updatePetWithForm(
+ parseParam($petId, 'int')
+ );
+ \Flight::halt(204);
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'uploadFile') && declaresMethod($reflectionClass, 'uploadFileStream')) {
+ throw new \Exception('Operation uploadFile cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'uploadFile')) {
+ \Flight::route('POST /pet/@petId/uploadImage', function (string $petId) use ($handler) {
+ $r = \Flight::request();
+ $result = $handler->uploadFile(
+ parseParam($petId, 'int')
+ );
+ if ($result === null) {
+ \Flight::halt(204);
+ } else {
+ \Flight::json($result);
+ }
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'deleteOrder') && declaresMethod($reflectionClass, 'deleteOrderStream')) {
+ throw new \Exception('Operation deleteOrder cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'deleteOrder')) {
+ \Flight::route('DELETE /store/order/@orderId', function (string $orderId) use ($handler) {
+ $r = \Flight::request();
+ $handler->deleteOrder(
+ parseParam($orderId, 'string')
+ );
+ \Flight::halt(204);
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'getInventory') && declaresMethod($reflectionClass, 'getInventoryStream')) {
+ throw new \Exception('Operation getInventory cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'getInventory')) {
+ \Flight::route('GET /store/inventory', function () use ($handler) {
+ $r = \Flight::request();
+ $handler->getInventory(
+ );
+ \Flight::halt(204);
+ });
+ }
+ if (declaresMethod($reflectionClass, 'getInventoryStream')) {
+ \Flight::route('GET /store/inventory', function () use ($handler) {
+ $r = \Flight::request();
+ $handler->getInventoryStream(
+ );
+ // ignore return value: streaming expected
+ })->streamWithHeaders(['Content-Type' => 'application/json']);
+ }
+
+ if (declaresMethod($reflectionClass, 'getOrderById') && declaresMethod($reflectionClass, 'getOrderByIdStream')) {
+ throw new \Exception('Operation getOrderById cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'getOrderById')) {
+ \Flight::route('GET /store/order/@orderId', function (string $orderId) use ($handler) {
+ $r = \Flight::request();
+ $result = $handler->getOrderById(
+ parseParam($orderId, 'int')
+ );
+ if ($result === null) {
+ \Flight::halt(204);
+ } else {
+ \Flight::json($result);
+ }
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'placeOrder') && declaresMethod($reflectionClass, 'placeOrderStream')) {
+ throw new \Exception('Operation placeOrder cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'placeOrder')) {
+ \Flight::route('POST /store/order', function () use ($handler) {
+ $r = \Flight::request();
+ $result = $handler->placeOrder(
+ parseParam(json_decode($r->getBody(), true), '\\OpenAPIServer\\Model\\Order')
+ );
+ if ($result === null) {
+ \Flight::halt(204);
+ } else {
+ \Flight::json($result);
+ }
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'createUser') && declaresMethod($reflectionClass, 'createUserStream')) {
+ throw new \Exception('Operation createUser cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'createUser')) {
+ \Flight::route('POST /user', function () use ($handler) {
+ $r = \Flight::request();
+ $handler->createUser(
+ parseParam(json_decode($r->getBody(), true), '\\OpenAPIServer\\Model\\User')
+ );
+ \Flight::halt(204);
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'createUsersWithArrayInput') && declaresMethod($reflectionClass, 'createUsersWithArrayInputStream')) {
+ throw new \Exception('Operation createUsersWithArrayInput cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'createUsersWithArrayInput')) {
+ \Flight::route('POST /user/createWithArray', function () use ($handler) {
+ $r = \Flight::request();
+ $handler->createUsersWithArrayInput(
+ parseParam(json_decode($r->getBody(), true), '\\OpenAPIServer\\Model\\User[]')
+ );
+ \Flight::halt(204);
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'createUsersWithListInput') && declaresMethod($reflectionClass, 'createUsersWithListInputStream')) {
+ throw new \Exception('Operation createUsersWithListInput cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'createUsersWithListInput')) {
+ \Flight::route('POST /user/createWithList', function () use ($handler) {
+ $r = \Flight::request();
+ $handler->createUsersWithListInput(
+ parseParam(json_decode($r->getBody(), true), '\\OpenAPIServer\\Model\\User[]')
+ );
+ \Flight::halt(204);
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'deleteUser') && declaresMethod($reflectionClass, 'deleteUserStream')) {
+ throw new \Exception('Operation deleteUser cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'deleteUser')) {
+ \Flight::route('DELETE /user/@username', function (string $username) use ($handler) {
+ $r = \Flight::request();
+ $handler->deleteUser(
+ parseParam($username, 'string')
+ );
+ \Flight::halt(204);
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'getUserByName') && declaresMethod($reflectionClass, 'getUserByNameStream')) {
+ throw new \Exception('Operation getUserByName cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'getUserByName')) {
+ \Flight::route('GET /user/@username', function (string $username) use ($handler) {
+ $r = \Flight::request();
+ $result = $handler->getUserByName(
+ parseParam($username, 'string')
+ );
+ if ($result === null) {
+ \Flight::halt(204);
+ } else {
+ \Flight::json($result);
+ }
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'loginUser') && declaresMethod($reflectionClass, 'loginUserStream')) {
+ throw new \Exception('Operation loginUser cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'loginUser')) {
+ \Flight::route('GET /user/login', function () use ($handler) {
+ $r = \Flight::request();
+ $result = $handler->loginUser(
+ parseParam($r->query['username'] ?? null, 'string'),
+ parseParam($r->query['password'] ?? null, 'string')
+ );
+ if ($result === null) {
+ \Flight::halt(204);
+ } else {
+ \Flight::json($result);
+ }
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'logoutUser') && declaresMethod($reflectionClass, 'logoutUserStream')) {
+ throw new \Exception('Operation logoutUser cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'logoutUser')) {
+ \Flight::route('GET /user/logout', function () use ($handler) {
+ $r = \Flight::request();
+ $handler->logoutUser(
+ );
+ \Flight::halt(204);
+ });
+ }
+
+ if (declaresMethod($reflectionClass, 'updateUser') && declaresMethod($reflectionClass, 'updateUserStream')) {
+ throw new \Exception('Operation updateUser cannot be both streaming and non-streaming');
+ }
+ if (declaresMethod($reflectionClass, 'updateUser')) {
+ \Flight::route('PUT /user/@username', function (string $username) use ($handler) {
+ $r = \Flight::request();
+ $handler->updateUser(
+ parseParam($username, 'string'),
+ parseParam(json_decode($r->getBody(), true), '\\OpenAPIServer\\Model\\User')
+ );
+ \Flight::halt(204);
+ });
+ }
+
+ }
+}
+
+
+function parseParam(mixed $param, string $type)
+{
+ $nonNullType = str_replace('?', '', $type);
+ if ($param === null) {
+ return null;
+ } elseif ($nonNullType === 'int') {
+ return intval($param);
+ } elseif ($nonNullType === 'float') {
+ return floatval($param);
+ } elseif ($nonNullType === 'bool') {
+ return filter_var($param, FILTER_VALIDATE_BOOLEAN);
+ } elseif (str_ends_with($type, '[]')) {
+ return array_map(fn($el) => parseParam($el, substr($type, 0, -2)), $param);
+ } elseif (str_starts_with($nonNullType, '\\OpenAPIServer\\Model')) {
+ return new $nonNullType($param);
+ } else {
+ return $param;
+ }
+}
+
+function declaresMethod(\ReflectionClass $reflectionClass, string $methodName): bool
+{
+ return $reflectionClass->hasMethod($methodName) && $reflectionClass->getMethod($methodName)->getDeclaringClass()->getName() === $reflectionClass->getName();
+}
\ No newline at end of file
diff --git a/samples/server/petstore/php-flight/Test/RegisterRoutesTest.php b/samples/server/petstore/php-flight/Test/RegisterRoutesTest.php
new file mode 100644
index 00000000000..3e7dcfefb5e
--- /dev/null
+++ b/samples/server/petstore/php-flight/Test/RegisterRoutesTest.php
@@ -0,0 +1,52 @@
+assertTrue(true);
+ }
+ public function testRegisterRoutesAbstractStoreApi(): void
+ {
+ $handler = new class extends \OpenAPIServer\Api\AbstractStoreApi {
+ public function deleteOrder(string $orderId): void
+ {
+ }
+ };
+ \OpenAPIServer\RegisterRoutes::registerRoutes($handler);
+ $this->assertTrue(true);
+ }
+ public function testRegisterRoutesAbstractUserApi(): void
+ {
+ $handler = new class extends \OpenAPIServer\Api\AbstractUserApi {
+ public function createUser(\OpenAPIServer\Model\User $user): void
+ {
+ }
+ };
+ \OpenAPIServer\RegisterRoutes::registerRoutes($handler);
+ $this->assertTrue(true);
+ }
+}
diff --git a/samples/server/petstore/php-flight/composer.json b/samples/server/petstore/php-flight/composer.json
new file mode 100644
index 00000000000..782987a751a
--- /dev/null
+++ b/samples/server/petstore/php-flight/composer.json
@@ -0,0 +1,34 @@
+{
+ "description": "This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters..",
+ "keywords": ["openapi", "php", "framework", "flightphp"],
+ "version": "1.0.0",
+ "homepage": "https://openapi-generator.tech",
+ "license": "unlicense",
+ "authors": [
+ {
+ "name": "OpenAPI",
+ "homepage": "https://openapi-generator.tech"
+ }
+ ],
+ "type": "library",
+ "require": {
+ "php": "^8.1.0",
+ "flightphp/core": "^3.5.1",
+ "ext-json": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "config": {
+ "optimize-autoloader": true,
+ "preferred-install": "dist",
+ "sort-packages": true
+ },
+ "autoload": {
+ "psr-4": {
+ "OpenAPIServer\\": ""
+ }
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
+}
diff --git a/samples/server/petstore/php-flight/phpunit.xml.dist b/samples/server/petstore/php-flight/phpunit.xml.dist
new file mode 100644
index 00000000000..0a03f35e357
--- /dev/null
+++ b/samples/server/petstore/php-flight/phpunit.xml.dist
@@ -0,0 +1,28 @@
+
+
+
+
+ ./Api
+ ./Model
+ .
+
+
+ ./Test
+
+
+
+
+ ./Test
+
+
+
+
+
+