diff --git a/CHANGELOG.md b/CHANGELOG.md index c75b714c5a..e8d37239db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- PHP: generated deserializers with numeric property names now satisfy static return-type analysis. [#7830](https://github.com/microsoft/kiota/issues/7830) - golang: generated code now always uses LF line endings, so `gofmt` no longer reports formatting differences when generating on Windows. - golang: make sure all generated code adheres to golangs coding standards - Fixed non-deterministic model class descriptions when a component schema is referenced from multiple properties with differing reference-level descriptions. [#7927](https://github.com/microsoft/kiota/issues/7927) diff --git a/it/config.json b/it/config.json index 7ea8a7d03f..5d07584cf1 100644 --- a/it/config.json +++ b/it/config.json @@ -162,18 +162,7 @@ "Rationale": "Compilation fails only on linux systems (CI) with api from 2026-06-22 - https://github.com/microsoft/kiota/issues/7828" } ], - "ExcludePatterns": [ - { - "Language": "php", - "Pattern": "/organizations/{organizationId}/apiRequests/overview", - "Rationale": "Fails for php with api dated 2026-06-24 - https://github.com/microsoft/kiota/issues/7830" - }, - { - "Language": "php", - "Pattern": "/networks/{networkId}/clients/{clientId}/splashAuthorizationStatus", - "Rationale": "Fails for php with api dated 2026-06-24 (GET and PUT) - https://github.com/microsoft/kiota/issues/7830" - } - ] + "ExcludePatterns": [] }, "https://raw.githubusercontent.com/docusign/OpenAPI-Specifications/refs/heads/master/esignature.rest.swagger-v2.1.json": { "Suppressions": [ diff --git a/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs index a49d9787f8..7a5ec647fe 100644 --- a/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Kiota.Builder.CodeDOM; using Kiota.Builder.Extensions; @@ -680,23 +681,34 @@ private void WriteDeserializerBody(CodeClass parentClass, LanguageWriter writer, } private void WriteDeserializerBodyForInheritedModel(CodeMethod method, CodeClass parentClass, LanguageWriter writer, bool extendsModelClass = false) { - var codeProperties = parentClass.GetPropertiesOfKind(CodePropertyKind.Custom).ToArray(); + var codeProperties = parentClass.GetPropertiesOfKind(CodePropertyKind.Custom) + .Where(static x => !x.ExistsInBaseType && x.Setter != null) + .OrderBy(static x => x.Name) + .ToArray(); + var hasNumericPropertyName = codeProperties.Any(static x => IsNumericArrayKey(x.WireName)); writer.WriteLine("$o = $this;"); + if (hasNumericPropertyName) + writer.WriteLine("/** @var array $deserializers */"); + var deserializers = extendsModelClass ? + $"{(hasNumericPropertyName ? "array_replace" : "array_merge")}(parent::{method.Name.ToFirstCharacterLowerCase()}(), [" : + " ["; writer.WriteLines( - $"return {((extendsModelClass) ? $"array_merge(parent::{method.Name.ToFirstCharacterLowerCase()}(), [" : " [")}"); + $"{(hasNumericPropertyName ? "$deserializers =" : "return")} {deserializers}"); writer.IncreaseIndent(); if (codeProperties.Length != 0) - { codeProperties - .Where(static x => !x.ExistsInBaseType && x.Setter != null) - .OrderBy(static x => x.Name) .ToList() .ForEach(x => WriteDeserializerPropertyCallback(x, method, writer)); - } writer.DecreaseIndent(); writer.WriteLine(extendsModelClass ? "]);" : "];"); + if (hasNumericPropertyName) + writer.WriteLine("return $deserializers;"); } + private static bool IsNumericArrayKey(string key) => + long.TryParse(key, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && + value.ToString(CultureInfo.InvariantCulture).Equals(key, StringComparison.Ordinal); + private void WriteDeserializerPropertyCallback(CodeProperty property, CodeMethod method, LanguageWriter writer) { if (property.Type.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None diff --git a/tests/Kiota.Builder.Tests/Writers/Php/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Php/CodeMethodWriterTests.cs index 2229faa142..7ed24ca957 100644 --- a/tests/Kiota.Builder.Tests/Writers/Php/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Php/CodeMethodWriterTests.cs @@ -962,6 +962,14 @@ public async Task EscapesIndexerPathParameterNameAsync() "'dOB' => fn(ParseNode $n) => $o->setDOB($n->getDateTimeValue())," }, new object[] + { + new CodeProperty { Name = "fiveHundred", Type = new CodeType { Name = "int32" }, Access = AccessModifier.Private, Kind = CodePropertyKind.Custom, SerializationName = "500" }, + "/** @var array $deserializers */", + "$deserializers =", + "'500' => fn(ParseNode $n) => $o->setFiveHundred($n->getIntegerValue()),", + "return $deserializers;" + }, + new object[] { new CodeProperty { Name = "story", Type = new CodeType { Name = "binary" }, Access = AccessModifier.Private, Kind = CodePropertyKind.Custom }, "'story' => fn(ParseNode $n) => $o->setStory($n->getBinaryContent())," @@ -1218,6 +1226,41 @@ public async Task WriteDeserializerMergeWhenHasParentAsync() Assert.Contains("array_merge(parent::getFieldDeserializers()", result); } + [Fact] + public async Task WriteDeserializerPreservesNumericKeysWhenHasParentAsync() + { + setup(true); + parentClass.Kind = CodeClassKind.Model; + parentClass.AddProperty(new CodeProperty + { + Name = "fiveHundred", + SerializationName = "500", + Access = AccessModifier.Private, + Kind = CodePropertyKind.Custom, + Type = new CodeType { Name = "int32" } + }); + var deserializerMethod = new CodeMethod + { + Name = "getFieldDeserializers", + Kind = CodeMethodKind.Deserializer, + ReturnType = new CodeType + { + IsNullable = false, + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + Name = "array" + } + }; + parentClass.AddMethod(deserializerMethod); + + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.PHP }, root, cancellationToken: TestContext.Current.CancellationToken); + languageWriter.Write(deserializerMethod); + var result = stringWriter.ToString(); + + Assert.Contains("$deserializers = array_replace(parent::getFieldDeserializers(), [", result); + Assert.Contains("'500' => fn(ParseNode $n) => $o->setFiveHundred($n->getIntegerValue()),", result); + Assert.DoesNotContain("array_merge", result); + } + [Fact] public async Task WriteConstructorBodyAsync() {