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

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

13 Apr 2026 10:57AM UTC coverage: 27.11%. First build
24339806478

push

github

wol-soft
Merge branch 'refs/heads/master' into jsonSchemaDraft7

# Conflicts:
#	src/Exception/ComposedValue/ConditionalException.php
#	src/Exception/Generic/InvalidConstException.php
#	src/Exception/Generic/InvalidTypeException.php
#	src/Exception/Object/AdditionalPropertiesException.php
#	src/Exception/Object/InvalidInstanceOfException.php
#	src/Traits/SerializableTrait.php

49 of 153 new or added lines in 44 files covered. (32.03%)

167 of 616 relevant lines covered (27.11%)

0.9 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\Traits;
6

7
use PHPModelGenerator\Attributes\Internal;
8
use PHPModelGenerator\Attributes\SchemaName;
9
use PHPModelGenerator\Interfaces\SerializationInterface;
10
use ReflectionClass;
11
use stdClass;
12

13
/**
14
 * Provide methods to serialize generated models
15
 *
16
 * Trait SerializableTrait
17
 *
18
 * @package PHPModelGenerator\Traits
19
 */
20
trait SerializableTrait
21
{
22
    /**
23
     * Maps concrete class names to their php-name => schema-name property maps.
24
     * Populated once per class via reflection.
25
     *
26
     * @var array<class-string, array<string, string>>
27
     */
28
    #[Internal]
29
    private static array $propertySchemaNames = [];
30

31
    /**
32
     * Maps concrete class names to their serialization capability string.
33
     * Populated once per encountered nested-object class.
34
     *
35
     * @var array<class-string, string>
36
     */
37
    #[Internal]
38
    private static array $objectSerializationCapability = [];
39

40
    /** @var array<string, string|false> */
41
    #[Internal]
42
    private static array $customSerializer = [];
43

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

NEW
60
        return json_encode($this->getValues($depth, $except, true), $options, $depth);
×
61
    }
62

63
    /**
64
     * Return a JSON serializable representation of the current state
65
     */
66
    #[\ReturnTypeWillChange]
×
67
    public function jsonSerialize(array $except = [])
68
    {
NEW
69
        return $this->getValues(512, $except, true);
×
70
    }
71

72
    /**
73
     * Get an array representation of the current state
74
     *
75
     * @param array $except provide a list of properties which shouldn't be contained in the resulting JSON.
76
     *                      eg. if you want to return an user model and don't want the password to be included
77
     * @param int $depth    the maximum level of object nesting. Must be greater than 0
78
     *
79
     * @return array|false
80
     */
81
    public function toArray(array $except = [], int $depth = 512)
1✔
82
    {
83
        if ($depth < 1) {
1✔
84
            return false;
×
85
        }
86

87
        return $this->getValues($depth, $except, false);
1✔
88
    }
89

90
    /**
91
     * Get a representation of the current state
92
     *
93
     * @param array $except                provide a list of properties which shouldn't be contained in the resulting
94
     *                                     JSON. eg. if you want to return an user model and don't want the password
95
     *                                     to be included
96
     * @param int $depth                   the maximum level of object nesting. Must be greater than 0
97
     * @param bool $emptyObjectsAsStdClass If set to true, the wrapping data structure for empty objects will be an
98
     *                                     stdClass. Array otherwise
99
     *
100
     * @return array|stdClass
101
     */
102
    private function getValues(int $depth, array $except, bool $emptyObjectsAsStdClass)
1✔
103
    {
104
        $depth--;
1✔
105
        $modelData = [];
1✔
106

107
        $localExcept = $except;
1✔
108
        if (isset($this->skipNotProvidedPropertiesMap, $this->rawModelDataInput)) {
1✔
109
            $localExcept = array_merge(
×
110
                $localExcept,
×
NEW
111
                array_diff($this->skipNotProvidedPropertiesMap, array_keys($this->rawModelDataInput))
×
112
            );
×
113
        }
114

115
        foreach ($this->getPropertySchemaNames() as $phpName => $schemaName) {
1✔
116
            if (in_array($schemaName, $localExcept, true)) {
1✔
117
                continue;
1✔
118
            }
119

120
            if ($customSerializer = $this->getCustomSerializerMethod($phpName)) {
1✔
NEW
121
                $modelData[$schemaName] = $this->getSerializedValue(
×
122
                    $this->{$customSerializer}(),
×
123
                    $depth,
×
124
                    $except,
×
125
                    $emptyObjectsAsStdClass,
×
126
                );
×
127
                continue;
×
128
            }
129

130
            $modelData[$schemaName] = $this->getSerializedValue(
1✔
131
                $this->{$phpName},
1✔
132
                $depth,
1✔
133
                $except,
1✔
134
                $emptyObjectsAsStdClass,
1✔
135
            );
1✔
136
        }
137

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

140
        if ($emptyObjectsAsStdClass && empty($data)) {
1✔
NEW
141
            return new stdClass();
×
142
        }
143

144
        return $data;
1✔
145
    }
146

147
    /**
148
     * Build and cache the php-name => schema-name map for this concrete class in a single
149
     * reflection pass.
150
     *
151
     * For generated model classes every schema-derived property carries a #[SchemaName]
152
     * attribute; only those properties are included, keyed by their original JSON Schema name.
153
     *
154
     * For hand-written classes that include this trait (e.g. the exception hierarchy) no
155
     * #[SchemaName] attributes are present. In that case the method falls back to the legacy
156
     * behaviour and uses the PHP property name as both the key and the output key, preserving
157
     * backward-compatible serialization for those classes.
158
     *
159
     * Static properties and properties marked with #[Internal] are never serialized.
160
     *
161
     * @return array<string, string>
162
     */
163
    private function getPropertySchemaNames(): array
1✔
164
    {
165
        if (isset(self::$propertySchemaNames[static::class])) {
1✔
166
            return self::$propertySchemaNames[static::class];
1✔
167
        }
168

169
        $map = [];
1✔
170
        $hasSchemaNames = false;
1✔
171
        $fallbackMap = [];
1✔
172

173
        foreach ((new ReflectionClass($this))->getProperties() as $property) {
1✔
174
            // Static properties are never part of an instance's serialized state.
175
            if ($property->isStatic()) {
1✔
NEW
176
                continue;
×
177
            }
178

179
            $phpName = $property->getName();
1✔
180
            $schemaNameAttributes = $property->getAttributes(SchemaName::class);
1✔
181

182
            if ($schemaNameAttributes !== []) {
1✔
NEW
183
                $map[$phpName] = $schemaNameAttributes[0]->newInstance()->name;
×
NEW
184
                $hasSchemaNames = true;
×
185
            } elseif (!$hasSchemaNames) {
1✔
186
                // Accumulate fallback entries for non-generated classes. Exclude any property
187
                // explicitly marked with #[Internal].
188
                if ($property->getAttributes(Internal::class) === []) {
1✔
189
                    $fallbackMap[$phpName] = $phpName;
1✔
190
                }
191
            }
192
        }
193

194
        return self::$propertySchemaNames[static::class] = $hasSchemaNames ? $map : $fallbackMap;
1✔
195
    }
196

197
    /**
198
     * Function can be overwritten by classes using the trait to hook into serialization
199
     */
200
    protected function resolveSerializationHook(array $data, int $depth, array $except): array
1✔
201
    {
202
        return $data;
1✔
203
    }
204

205
    private function getSerializedValue($value, int $depth, array $except, bool $emptyObjectsAsStdClass = false)
1✔
206
    {
207
        if (is_array($value)) {
1✔
208
            $subData = [];
1✔
209
            foreach ($value as $subKey => $element) {
1✔
210
                $subData[$subKey] = $this->getSerializedValue(
1✔
211
                    $element,
1✔
212
                    $depth - 1,
1✔
213
                    $except,
1✔
214
                    $emptyObjectsAsStdClass,
1✔
215
                );
1✔
216
            }
217
            return $subData;
1✔
218
        }
219

220
        return $this->evaluateAttribute($value, $depth, $except, $emptyObjectsAsStdClass);
1✔
221
    }
222

223
    private function evaluateAttribute($attribute, int $depth, array $except, bool $emptyObjectsAsStdClass)
1✔
224
    {
225
        if (!is_object($attribute)) {
1✔
226
            return $attribute;
1✔
227
        }
228

229
        // Determine and cache the serialization capability of this concrete class.
230
        // The cache key is the class name only — capability is class-intrinsic.
231
        // Context-dependent decisions (depth, emptyObjectsAsStdClass) are handled
232
        // after the cache lookup so they never pollute the cached value.
233
        self::$objectSerializationCapability[$attribute::class] ??= match (true) {
1✔
234
            $attribute instanceof SerializationInterface => 'protocol',
1✔
NEW
235
            method_exists($attribute, 'jsonSerialize')   => 'jsonSerializable',
×
NEW
236
            method_exists($attribute, 'toArray')         => 'toArray',
×
NEW
237
            method_exists($attribute, '__toString')      => 'stringable',
×
NEW
238
            default                                      => 'plain',
×
239
        };
1✔
240

241
        // Depth-exhausted: return a string representation if possible, null otherwise.
242
        if ($depth <= 0) {
1✔
NEW
243
            return method_exists($attribute, '__toString') ? (string) $attribute : null;
×
244
        }
245

246
        $data = match (self::$objectSerializationCapability[$attribute::class]) {
1✔
247
            // Objects speaking our serialization protocol: propagate $except and $depth,
248
            // and choose the method that preserves the emptyObjectsAsStdClass semantics.
249
            'protocol' => $emptyObjectsAsStdClass
1✔
NEW
250
                ? $attribute->jsonSerialize($except)
×
251
                : $attribute->toArray($except, $depth),
1✔
252
            // Objects implementing JsonSerializable but not our protocol: we can pass
253
            // $except for the jsonSerialize branch only; no depth or $except for toArray
254
            // because we don't know the callee's signature.
NEW
255
            'jsonSerializable' => $emptyObjectsAsStdClass
×
NEW
256
                ? $attribute->jsonSerialize()
×
NEW
257
                : get_object_vars($attribute),
×
NEW
258
            'toArray' => $attribute->toArray(),
×
259
            // 'stringable' and 'plain' both fall through to get_object_vars; stringable
260
            // was handled by the depth-exhausted early return above.
NEW
261
            default => get_object_vars($attribute),
×
262
        };
1✔
263

264
        if ($data === [] && $emptyObjectsAsStdClass) {
1✔
NEW
265
            return new stdClass();
×
266
        }
267

268
        return $data;
1✔
269
    }
270

271
    private function getCustomSerializerMethod(string $property)
1✔
272
    {
273
        // Cache key includes the concrete class so that subclasses with additional
274
        // serialize*() methods are discovered independently of their parent class.
275
        // array_key_exists is used (not isset) because the cached "no serializer"
276
        // sentinel is false, and isset() returns false for null but true for false.
277
        $cacheKey = static::class . '::' . $property;
1✔
278
        if (array_key_exists($cacheKey, self::$customSerializer)) {
1✔
279
            return self::$customSerializer[$cacheKey];
1✔
280
        }
281

282
        $customSerializer = 'serialize' . ucfirst($property);
1✔
283
        if (!method_exists($this, $customSerializer)) {
1✔
284
            $customSerializer = false;
1✔
285
        }
286

287
        return self::$customSerializer[$cacheKey] = $customSerializer;
1✔
288
    }
289
}
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