@@ -69,9 +69,6 @@ public PhpNextgenClientCodegen() {
6969 GlobalFeature .LinkObjects ,
7070 GlobalFeature .ParameterStyling
7171 )
72- .excludeSchemaSupportFeatures (
73- SchemaSupportFeature .Polymorphism
74- )
7572 );
7673
7774 // clear import mapping (from default generator) as php does not use it
@@ -127,6 +124,7 @@ public void processOpts() {
127124 supportingFiles .add (new SupportingFile ("FormDataProcessor.mustache" , toSrcPath (invokerPackage , srcBasePath ), "FormDataProcessor.php" ));
128125 supportingFiles .add (new SupportingFile ("ObjectSerializer.mustache" , toSrcPath (invokerPackage , srcBasePath ), "ObjectSerializer.php" ));
129126 supportingFiles .add (new SupportingFile ("ModelInterface.mustache" , toSrcPath (modelPackage , srcBasePath ), "ModelInterface.php" ));
127+ supportingFiles .add (new SupportingFile ("OneOfInterface.mustache" , toSrcPath (modelPackage , srcBasePath ), "OneOfInterface.php" ));
130128 supportingFiles .add (new SupportingFile ("HeaderSelector.mustache" , toSrcPath (invokerPackage , srcBasePath ), "HeaderSelector.php" ));
131129 supportingFiles .add (new SupportingFile ("composer.mustache" , "" , "composer.json" ));
132130 supportingFiles .add (new SupportingFile ("README.mustache" , "" , "README.md" ));
@@ -145,30 +143,168 @@ public void processOpts() {
145143 public Map <String , ModelsMap > postProcessAllModels (Map <String , ModelsMap > objs ) {
146144 final Map <String , ModelsMap > processed = super .postProcessAllModels (objs );
147145
146+ Map <String , String > oneOfTypeHints = new HashMap <>();
147+ for (ModelsMap modelsMap : processed .values ()) {
148+ for (ModelMap m : modelsMap .getModels ()) {
149+ collectOneOfTypeHint (m .getModel (), oneOfTypeHints );
150+ }
151+ }
152+
148153 for (Map .Entry <String , ModelsMap > entry : processed .entrySet ()) {
149- entry .setValue (postProcessModelsMap (entry .getValue ()));
154+ entry .setValue (postProcessModelsMap (entry .getValue (), oneOfTypeHints ));
150155 }
151156
152157 return processed ;
153158 }
154159
155- private ModelsMap postProcessModelsMap (ModelsMap objs ) {
160+ /**
161+ * If the given model is a oneOf composition, record the PHP union type that should be used
162+ * wherever the model is referenced.
163+ */
164+ private void collectOneOfTypeHint (CodegenModel model , Map <String , String > oneOfTypeHints ) {
165+ if (model == null || model .getComposedSchemas () == null ) {
166+ return ;
167+ }
168+
169+ List <CodegenProperty > oneOf = model .getComposedSchemas ().getOneOf ();
170+ if (oneOf == null || oneOf .isEmpty ()) {
171+ return ;
172+ }
173+
174+ Set <String > memberTypes = new LinkedHashSet <>();
175+ for (CodegenProperty member : oneOf ) {
176+ memberTypes .add ((member .isArray || member .isMap ) ? "array" : member .dataType );
177+ }
178+
179+ oneOfTypeHints .put ("\\ " + modelPackage + "\\ " + model .classname , String .join ("|" , memberTypes ));
180+ }
181+
182+ /**
183+ * PHP forbids the nullable shorthand ({@code ?T}) on union types, so a union must instead
184+ * gain an explicit {@code |null} member.
185+ */
186+ private static String makeNullable (String phpType ) {
187+ return phpType .contains ("|" ) ? phpType + "|null" : "?" + phpType ;
188+ }
189+
190+ /**
191+ * The base PHP type hint for a single element: a container collapses to {@code array} (PHP
192+ * cannot type-hint {@code Foo[]}), a oneOf alias expands to the union of its members, and
193+ * everything else stays its {@code dataType}.
194+ */
195+ private String phpBaseType (String dataType , boolean isContainer , Map <String , String > oneOfTypeHints ) {
196+ return isContainer ? "array" : oneOfTypeHints .getOrDefault (dataType , dataType );
197+ }
198+
199+ /**
200+ * The PHP signature type hint: the {@link #phpBaseType base type}, made nullable when the
201+ * element is optional or nullable - except {@code mixed}, which already admits null.
202+ */
203+ private String phpSignatureType (String dataType , boolean isContainer , boolean nullable , Map <String , String > oneOfTypeHints ) {
204+ String base = phpBaseType (dataType , isContainer , oneOfTypeHints );
205+ return (nullable && !base .equals ("mixed" )) ? makeNullable (base ) : base ;
206+ }
207+
208+ /**
209+ * Wrap an expanded inner union back into container phpdoc notation: {@code (Apple|Banana)[]}
210+ * for arrays (parenthesised so {@code []} binds to the whole union, not just its last member)
211+ * and {@code array<string,Apple|Banana>} for maps. A {@code null} inner propagates, signalling
212+ * "no oneOf in this type".
213+ */
214+ private static String wrapContainerDoc (boolean isArray , String inner ) {
215+ if (inner == null ) {
216+ return null ;
217+ }
218+ return isArray ? (inner .contains ("|" ) ? "(" + inner + ")[]" : inner + "[]" )
219+ : "array<string," + inner + ">" ;
220+ }
221+
222+ /**
223+ * The phpdoc type with any reference to a oneOf model expanded to the union of its members.
224+ * A oneOf model is only a deserialization dispatcher, so its members do not inherit from it
225+ * and {@code @param Fruit} would be a lie — {@code @param Apple|Banana} is the truth.
226+ * Returns {@code null} when no oneOf is involved, so the caller can leave the original
227+ * {@code dataType} phpdoc untouched.
228+ */
229+ private String oneOfDocType (CodegenProperty prop , Map <String , String > oneOfTypeHints ) {
230+ return docTypeOf (prop .isArray , prop .isMap , prop .items , prop .dataType , oneOfTypeHints );
231+ }
232+
233+ /** @see #oneOfDocType(CodegenProperty, Map) */
234+ private String oneOfDocType (CodegenParameter param , Map <String , String > oneOfTypeHints ) {
235+ return docTypeOf (param .isArray , param .isMap , param .items , param .dataType , oneOfTypeHints );
236+ }
237+
238+ /** @see #oneOfDocType(CodegenProperty, Map) */
239+ private String oneOfDocType (CodegenResponse response , Map <String , String > oneOfTypeHints ) {
240+ return docTypeOf (response .isArray , response .isMap , response .items , response .dataType , oneOfTypeHints );
241+ }
242+
243+ /**
244+ * The shared core of the {@code oneOfDocType} overloads: expands a oneOf {@code dataType} to the
245+ * union of its members (recursing through array/map items so the expansion reaches nested oneOfs),
246+ * or returns {@code null} when no oneOf is involved. See {@link #oneOfDocType(CodegenProperty, Map)}.
247+ */
248+ private String docTypeOf (boolean isArray , boolean isMap , CodegenProperty items , String dataType , Map <String , String > oneOfTypeHints ) {
249+ if ((isArray || isMap ) && items != null ) {
250+ return wrapContainerDoc (isArray , oneOfDocType (items , oneOfTypeHints ));
251+ }
252+ return oneOfTypeHints .get (dataType );
253+ }
254+
255+ /**
256+ * The final phpdoc type, ready for the template to emit verbatim: the oneOf-expanded type (or the
257+ * unchanged {@code dataType} when no oneOf is involved), with a {@code |null} member appended
258+ * when the element is optional or nullable. phpdoc unions always spell out {@code |null}
259+ * rather than using the {@code ?T} shorthand.
260+ */
261+ private String phpDocType (CodegenProperty prop , Map <String , String > oneOfTypeHints ) {
262+ return bakeDocType (oneOfDocType (prop , oneOfTypeHints ), prop .dataType , prop .notRequiredOrIsNullable ());
263+ }
264+
265+ private String phpDocType (CodegenParameter param , Map <String , String > oneOfTypeHints ) {
266+ return bakeDocType (oneOfDocType (param , oneOfTypeHints ), param .dataType , param .notRequiredOrIsNullable ());
267+ }
268+
269+ /**
270+ * The shared core of the {@code phpDocType} overloads: uses {@code expandedType}, falling back to
271+ * {@code dataType} when it is {@code null}, and appends {@code |null} when {@code nullable}.
272+ * See {@link #phpDocType(CodegenProperty, Map)}.
273+ */
274+ private static String bakeDocType (String expandedType , String dataType , boolean nullable ) {
275+ String docType = expandedType != null ? expandedType : dataType ;
276+ return nullable ? docType + "|null" : docType ;
277+ }
278+
279+ /**
280+ * A oneOf model is an abstract dispatcher, so the default doc example ({@code new Mammal()})
281+ * instantiates a type that cannot be used. Rewrite the example to instantiate the first member
282+ * of the union instead ({@code new Whale()}). Handles a oneOf parameter directly as well as a
283+ * container whose items are a oneOf.
284+ */
285+ private void useFirstOneOfMemberInExample (CodegenParameter param , Map <String , String > oneOfTypeHints ) {
286+ if (param .example == null ) {
287+ return ;
288+ }
289+ String alias = oneOfTypeHints .containsKey (param .dataType ) ? param .dataType
290+ : (param .items != null && oneOfTypeHints .containsKey (param .items .dataType ) ? param .items .dataType : null );
291+ if (alias == null ) {
292+ return ;
293+ }
294+ String firstMember = oneOfTypeHints .get (alias ).split ("\\ |" , 2 )[0 ];
295+ if (firstMember .startsWith ("\\ " )) { // a concrete class we can instantiate
296+ param .example = param .example .replace (alias , firstMember );
297+ }
298+ }
299+
300+ private ModelsMap postProcessModelsMap (ModelsMap objs , Map <String , String > oneOfTypeHints ) {
156301 for (ModelMap m : objs .getModels ()) {
157302 CodegenModel model = m .getModel ();
158303
159304 for (CodegenProperty prop : model .vars ) {
160- String propType ;
161- if (prop .isArray || prop .isMap ) {
162- propType = "array" ;
163- } else {
164- propType = prop .dataType ;
165- }
166-
167- if ((!prop .required || prop .isNullable ) && !propType .equals ("mixed" )) { // optional or nullable but not mixed
168- propType = "?" + propType ;
169- }
170-
171- prop .vendorExtensions .putIfAbsent ("x-php-prop-type" , propType );
305+ prop .vendorExtensions .putIfAbsent ("x-php-prop-type" ,
306+ phpSignatureType (prop .dataType , prop .isArray || prop .isMap , prop .notRequiredOrIsNullable (), oneOfTypeHints ));
307+ prop .vendorExtensions .putIfAbsent ("x-php-prop-doc-type" , phpDocType (prop , oneOfTypeHints ));
172308 }
173309 }
174310 return objs ;
@@ -177,6 +313,12 @@ private ModelsMap postProcessModelsMap(ModelsMap objs) {
177313 @ Override
178314 public OperationsMap postProcessOperationsWithModels (OperationsMap objs , List <ModelMap > allModels ) {
179315 objs = super .postProcessOperationsWithModels (objs , allModels );
316+
317+ Map <String , String > oneOfTypeHints = new HashMap <>();
318+ for (ModelMap m : allModels ) {
319+ collectOneOfTypeHint (m .getModel (), oneOfTypeHints );
320+ }
321+
180322 OperationMap operations = objs .getOperations ();
181323 for (CodegenOperation operation : operations .getOperation ()) {
182324 Set <String > phpReturnTypeOptions = new LinkedHashSet <>();
@@ -185,16 +327,15 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
185327
186328 for (CodegenResponse response : operation .responses ) {
187329 if (response .dataType != null ) {
188- String returnType = response .dataType ;
189- if (response .isArray || response .isMap ) {
190- // PHP does not understand array type hinting so we strip it
191- // The phpdoc will still contain the array type hinting
192- returnType = "array" ;
193- }
194-
195- phpReturnTypeOptions .add (returnType );
196- docReturnTypeOptions .add (response .dataType );
197- } else {
330+ // The signature collapses a container to `array` (PHP cannot type-hint Foo[]);
331+ // the phpdoc keeps the full notation, with any oneOf alias expanded to its union.
332+ phpReturnTypeOptions .add (phpBaseType (response .dataType , response .isArray || response .isMap , oneOfTypeHints ));
333+ String responseDocType = oneOfDocType (response , oneOfTypeHints );
334+ docReturnTypeOptions .add (responseDocType != null ? responseDocType : response .dataType );
335+ } else if (response .is2xx ) {
336+ // Only a body-less *success* response makes the method return null. A body-less
337+ // error response throws an ApiException instead, so it must not make the return
338+ // type nullable.
198339 hasEmptyResponse = true ;
199340 }
200341 }
@@ -207,11 +348,7 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
207348 String phpReturnType = String .join ("|" , phpReturnTypeOptions );
208349 String docReturnType = String .join ("|" , docReturnTypeOptions );
209350 if (hasEmptyResponse ) {
210- if (phpReturnTypeOptions .size () > 1 ) {
211- phpReturnType = phpReturnType + "|null" ;
212- } else {
213- phpReturnType = "?" + phpReturnType ;
214- }
351+ phpReturnType = makeNullable (phpReturnType );
215352 docReturnType = docReturnType + "|null" ;
216353 }
217354
@@ -221,16 +358,10 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
221358 }
222359
223360 for (CodegenParameter param : operation .allParams ) {
224- String paramType ;
225- if (param .isArray || param .isMap ) {
226- paramType = "array" ;
227- } else {
228- paramType = param .dataType ;
229- }
230- if ((!param .required || param .isNullable ) && !paramType .equals ("mixed" )) { // optional or nullable but not mixed
231- paramType = "?" + paramType ;
232- }
233- param .vendorExtensions .putIfAbsent ("x-php-param-type" , paramType );
361+ param .vendorExtensions .putIfAbsent ("x-php-param-type" ,
362+ phpSignatureType (param .dataType , param .isArray || param .isMap , param .notRequiredOrIsNullable (), oneOfTypeHints ));
363+ param .vendorExtensions .putIfAbsent ("x-php-param-doc-type" , phpDocType (param , oneOfTypeHints ));
364+ useFirstOneOfMemberInExample (param , oneOfTypeHints );
234365 }
235366 }
236367
0 commit comments