• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wol-soft / php-json-schema-model-generator-production / 23464452873

23 Mar 2026 10:23PM UTC coverage: 19.859% (+0.1%) from 19.718%
23464452873

push

github

Enno Woortmann
Fix serialization skipping nested properties with same name as outer optional property

Per-object skip-not-provided entries were merged into the $except parameter
which was then passed to child objects. Any child property with the same name
as an outer optional property that was not provided would be incorrectly
excluded from the serialized output.

Fix: use a separate $localExcept for filtering the current object's properties.
The original $except (user-supplied global excludes only) is what propagates
to child objects.

Fixes wol-soft/php-json-schema-model-generator#117

2 of 4 new or added lines in 1 file covered. (50.0%)

113 of 569 relevant lines covered (19.86%)

0.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.33
/src/Traits/SerializableTrait.php
1
<?php
2

3
declare(strict_types = 1);
4

5
namespace PHPModelGenerator\Traits;
6

7
use stdClass;
8

9
/**
10
 * Provide methods to serialize generated models
11
 *
12
 * Trait SerializableTrait
13
 *
14
 * @package PHPModelGenerator\Traits
15
 */
16
trait SerializableTrait
17
{
18
    private static $_customSerializer = [];
19

20
    /**
21
     * Get a JSON representation of the current state
22
     *
23
     * @param array $except provide a list of properties which shouldn't be contained in the resulting JSON.
24
     *                      eg. if you want to return an user model and don't want the password to be included
25
     * @param int $options  Bitmask for json_encode
26
     * @param int $depth    the maximum level of object nesting. Must be greater than 0
27
     *
28
     * @return string|false
29
     */
30
    public function toJSON(array $except = [], int $options = 0, int $depth = 512)
×
31
    {
32
        if ($depth < 1) {
×
33
            return false;
×
34
        }
35

36
        return json_encode($this->_getValues($depth, $except, true), $options, $depth);
×
37
    }
38

39
    /**
40
     * Return a JSON serializable representation of the current state
41
     */
42
    #[\ReturnTypeWillChange]
×
43
    public function jsonSerialize(array $except = [])
44
    {
45
        return $this->_getValues(512, $except, true);
×
46
    }
47

48
    /**
49
     * Get an array representation of the current state
50
     *
51
     * @param array $except provide a list of properties which shouldn't be contained in the resulting JSON.
52
     *                      eg. if you want to return an user model and don't want the password to be included
53
     * @param int $depth    the maximum level of object nesting. Must be greater than 0
54
     *
55
     * @return array|false
56
     */
57
    public function toArray(array $except = [], int $depth = 512)
1✔
58
    {
59
        if ($depth < 1) {
1✔
60
            return false;
×
61
        }
62

63
        return $this->_getValues($depth, $except, false);
1✔
64
    }
65

66
    /**
67
     * Get a representation of the current state
68
     *
69
     * @param array $except                provide a list of properties which shouldn't be contained in the resulting JSON.
70
     *                                     eg. if you want to return an user model and don't want the password to be included
71
     * @param int $depth                   the maximum level of object nesting. Must be greater than 0
72
     * @param bool $emptyObjectsAsStdClass If set to true, the wrapping data structure for empty objects will be an stdClass. Array otherwise
73
     *
74
     * @return array|stdClass
75
     */
76
    private function _getValues(int $depth, array $except, bool $emptyObjectsAsStdClass)
1✔
77
    {
78
        $depth--;
1✔
79
        $modelData = [];
1✔
80

81
        $localExcept = $except;
1✔
82
        if (isset($this->_skipNotProvidedPropertiesMap, $this->_rawModelDataInput)) {
1✔
NEW
83
            $localExcept = array_merge(
×
NEW
84
                $localExcept,
×
85
                array_diff($this->_skipNotProvidedPropertiesMap, array_keys($this->_rawModelDataInput))
×
86
            );
×
87
        }
88

89
        foreach (get_class_vars(get_class($this)) as $key => $value) {
1✔
90
            if (in_array($key, $localExcept) || str_starts_with($key, '_') !== false) {
1✔
91
                continue;
1✔
92
            }
93

94
            if ($customSerializer = $this->_getCustomSerializerMethod($key)) {
1✔
95
                $modelData[$key] = $this->_getSerializedValue($this->{$customSerializer}(), $depth, $except, $emptyObjectsAsStdClass);
×
96
                continue;
×
97
            }
98

99
            $modelData[$key] = $this->_getSerializedValue($this->$key, $depth, $except, $emptyObjectsAsStdClass);
1✔
100
        }
101

102
        $data = $this->resolveSerializationHook($modelData, $depth, $except);
1✔
103

104
        if ($emptyObjectsAsStdClass && empty($data)) {
1✔
105
            $data = new stdClass();
×
106
        }
107

108
        return $data;
1✔
109
    }
110

111
    /**
112
     * Function can be overwritten by classes using the trait to hook into serialization
113
     */
114
    protected function resolveSerializationHook(array $data, int $depth, array $except): array
1✔
115
    {
116
        return $data;
1✔
117
    }
118

119
    private function _getSerializedValue($value, int $depth, array $except, bool $emptyObjectsAsStdClass = false) {
1✔
120
        if (is_array($value)) {
1✔
121
            $subData = [];
1✔
122
            foreach ($value as $subKey => $element) {
1✔
123
                $subData[$subKey] = $this->_getSerializedValue($element, $depth - 1, $except, $emptyObjectsAsStdClass);
1✔
124
            }
125
            return $subData;
1✔
126
        }
127

128
        return $this->evaluateAttribute($value, $depth, $except, $emptyObjectsAsStdClass);
1✔
129
    }
130

131
    private function evaluateAttribute($attribute, int $depth, array $except, bool $emptyObjectsAsStdClass)
1✔
132
    {
133
        if (!is_object($attribute)) {
1✔
134
            return $attribute;
1✔
135
        }
136

137
        if ($depth === 0 && method_exists($attribute, '__toString')) {
1✔
138
            return (string) $attribute;
×
139
        }
140

141
        $data = match (true) {
1✔
142
            0 >= $depth                                                           => null,
1✔
143
            $emptyObjectsAsStdClass && method_exists($attribute, 'jsonSerialize') => $attribute->jsonSerialize($except),
1✔
144
            method_exists($attribute, 'toArray')                                  => $attribute->toArray($except),
1✔
145
            default                                                               => get_object_vars($attribute),
1✔
146
        };
1✔
147

148
        if ($data === [] && $emptyObjectsAsStdClass) {
1✔
149
            $data = new stdClass();
×
150
        }
151

152
        return $data;
1✔
153
    }
154

155
    private function _getCustomSerializerMethod(string $property) {
1✔
156
        if (isset(self::$_customSerializer[$property])) {
1✔
157
            return self::$_customSerializer[$property];
1✔
158
        }
159

160
        $customSerializer = 'serialize' . ucfirst($property);
1✔
161
        if (!method_exists($this, $customSerializer)) {
1✔
162
            $customSerializer = false;
1✔
163
        }
164

165
        return self::$_customSerializer[$property] = $customSerializer;
1✔
166
    }
167
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc