Skip to content

Commit 8414e3e

Browse files
author
Sergey Lavrienya
committed
Prepare codebase for 2.0.0 with modern PHP type system and Symfony 7.2+ support
1 parent 1008a24 commit 8414e3e

File tree

6 files changed

+140
-100
lines changed

6 files changed

+140
-100
lines changed

CHANGELOG.md

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,66 @@
11
# Changelog
22

3-
## 1.0.0 - Unreleased
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6+
and this project adheres to [Semantic Versioning](https://semver.org/).
7+
8+
---
9+
10+
## [1.3.0] – 2025-07-21
11+
12+
### Added
13+
- Support for nullable enums.
14+
- Better property definition accuracy.
15+
16+
### Fixed
17+
- Psalm warnings for Symfony 7.3.
18+
19+
---
20+
21+
## [1.2.0] – 2025-06-02
22+
23+
### Added
24+
- `format` parameter added to the `Field` attribute.
25+
26+
---
27+
28+
## [1.1.1] – 2025-04-29
29+
30+
### Changed
31+
- Upgraded `phpdoc-parser` dependency for improved PHPDoc handling.
32+
33+
---
34+
35+
## [1.1.0] – 2023-12-06
36+
37+
### Added
38+
- Compatibility with `symfony/property-info` v7.0.
39+
40+
### Fixed
41+
- `.gitattributes` configuration.
42+
43+
---
44+
45+
## [1.0.0] – 2023-11-26
46+
47+
### Added
48+
- Initial release.
49+
- JSON Schema generator for PHP.
50+
- Primary use case: structured output generation for LLM-based systems.
51+
52+
### Features
53+
- PHP native type support.
54+
- Nested objects and list (array) support.
55+
- Psalm type annotations support.
56+
- Custom metadata via PHP attributes.
57+
- Enum support.
58+
59+
## [2.0.0] – Unreleased
60+
61+
### Added
62+
- Compatibility with Symfony 7.2 and newer.
63+
64+
> **Note**
65+
> This version takes advantage of updated type system features and is intended for use with modern Symfony applications.
466
5-
- initial release

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Main use case - structured output definition for LLMs.
1616

1717
Make sure that your server is configured with the following PHP versions and extensions:
1818

19-
- PHP >=8.1
19+
- PHP >=8.3
2020

2121
## Installation
2222

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
}
2424
],
2525
"require": {
26-
"php": ">=8.1",
27-
"symfony/property-info": "^6.4.18 || ^7.2.0",
26+
"php": ">=8.3",
27+
"symfony/property-info": "^7.2.0 || ^8.0.0",
2828
"phpstan/phpdoc-parser": "^1.33 | ^2.1",
2929
"phpdocumentor/reflection-docblock": "^5.3"
3030
},

src/Parser/ClassParser.php

Lines changed: 57 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@
66

77
use Spiral\JsonSchemaGenerator\Exception\GeneratorException;
88
use Spiral\JsonSchemaGenerator\Exception\InvalidTypeException;
9-
use Spiral\JsonSchemaGenerator\Schema\Type as SchemaType;
109
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
1110
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
1211
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
1312
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
14-
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
15-
use Symfony\Component\PropertyInfo\Type as PropertyInfoType;
13+
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
14+
use Symfony\Component\TypeInfo\Type\BackedEnumType;
15+
use Symfony\Component\TypeInfo\Type\BuiltinType;
16+
use Symfony\Component\TypeInfo\Type\CollectionType;
17+
use Symfony\Component\TypeInfo\Type\ObjectType;
18+
use Symfony\Component\TypeInfo\Type\UnionType;
19+
use Symfony\Component\TypeInfo\TypeIdentifier;
20+
use Symfony\Component\TypeInfo\Type as TypeInfoType;
1621

1722
/**
1823
* @internal
@@ -26,7 +31,7 @@ final class ClassParser implements ClassParserInterface
2631
*/
2732
private array $constructorParameters = [];
2833

29-
private readonly PropertyInfoExtractorInterface $propertyInfo;
34+
private readonly PropertyTypeExtractorInterface $propertyInfo;
3035

3136
/**
3237
* @param \ReflectionClass|class-string $class
@@ -125,101 +130,74 @@ private function getEnumValues(string $typeName): ?array
125130
);
126131
}
127132

128-
/**
129-
* @param non-empty-string $typeName
130-
*/
131-
private function getTypeBuildIn(string $typeName): bool
132-
{
133-
if ((\class_exists($typeName))) {
134-
return \is_subclass_of($typeName, \BackedEnum::class);
135-
}
136-
137-
return $typeName !== SchemaType::Object->value;
138-
}
139-
140-
/**
141-
* @param non-empty-string|class-string $typeName
142-
*
143-
* @return non-empty-string|class-string
144-
*/
145-
private function getEnumTypeName(string $typeName): string
133+
private function getPropertyType(\ReflectionProperty $property): Type
146134
{
147-
if (!\is_subclass_of($typeName, \BackedEnum::class)) {
148-
return $typeName;
149-
}
135+
$type = $this->propertyInfo->getType($property->class, $property->getName());
150136

151-
$reflection = new \ReflectionEnum($typeName);
152-
$backingType = $reflection->getBackingType();
153-
154-
if (!$backingType instanceof \ReflectionNamedType) {
155-
return $typeName;
137+
if ($type === null) {
138+
throw new InvalidTypeException();
156139
}
157140

158-
return $backingType->getName();
141+
return $this->createType($type);
159142
}
160143

161-
private function getPropertyType(\ReflectionProperty $property): Type
144+
private function createType(TypeInfoType $type): Type
162145
{
163-
/** @psalm-suppress DeprecatedMethod, DeprecatedClass */
164-
$types = $this->propertyInfo->getTypes($property->class, $property->getName());
165-
166146
$simpleTypes = [];
167-
$isNullable = false;
168-
foreach ($types ?? [] as $type) {
169-
$typeName = $type->getBuiltinType() === SchemaType::Object->value && $type->getClassName() !== null
170-
? $type->getClassName()
171-
: $type->getBuiltinType();
172-
173-
if ($typeName === '') {
174-
throw new InvalidTypeException();
147+
if ($type instanceof UnionType) {
148+
foreach ($type->getTypes() as $subType) {
149+
$simpleType = $this->createSimpleType($subType);
150+
if ($simpleType !== null) {
151+
$simpleTypes[] = $simpleType;
152+
}
175153
}
176-
if ($type->isNullable() && $isNullable === false) {
177-
$simpleTypes[] = new SimpleType(
178-
name: SchemaType::Null->value,
179-
builtin: true,
180-
);
181-
$isNullable = true;
154+
} else {
155+
$simpleType = $this->createSimpleType($type);
156+
if ($simpleType !== null) {
157+
$simpleTypes[] = $simpleType;
182158
}
183-
184-
$simpleTypes[] = new SimpleType(
185-
name: $this->getEnumTypeName($typeName),
186-
builtin: $this->getTypeBuildIn($typeName),
187-
collectionType: $this->getCollectionValueType($type),
188-
enum: $this->getEnumValues($typeName),
189-
);
190159
}
191160

192161
return new Type(types: $simpleTypes);
193162
}
194163

195-
/**
196-
* @psalm-suppress DeprecatedClass
197-
*/
198-
private function getCollectionValueType(PropertyInfoType $propertyInfoType): ?Type
164+
private function createSimpleType(TypeInfoType $type): ?SimpleType
199165
{
200-
if (!$propertyInfoType->isCollection()) {
201-
return null;
166+
$typeName = '';
167+
$builtin = true;
168+
$enum = null;
169+
$collectionType = null;
170+
if ($type instanceof BuiltinType) {
171+
if ($type->getTypeIdentifier() === TypeIdentifier::MIXED) {
172+
return null;
173+
}
174+
$typeName = $type->getTypeIdentifier()->value;
175+
}
176+
if ($type instanceof CollectionType) {
177+
$typeName = TypeIdentifier::ARRAY->value;
178+
$collectionType = $this->createType($type->getCollectionValueType());
179+
}
180+
if ($type instanceof ObjectType) {
181+
$typeName = $type->getClassName();
182+
$builtin = false;
202183
}
203184

204-
$simpleTypes = [];
205-
/** @psalm-suppress DeprecatedClass */
206-
foreach ($propertyInfoType->getCollectionValueTypes() as $collectionValueType) {
207-
$typeName = $collectionValueType->getBuiltinType() === SchemaType::Object->value && $collectionValueType->getClassName() !== null
208-
? $collectionValueType->getClassName()
209-
: $collectionValueType->getBuiltinType();
210-
211-
if ($typeName === '') {
212-
throw new InvalidTypeException();
213-
}
185+
if ($type instanceof BackedEnumType) {
186+
$enum = $this->getEnumValues($type->getClassName());
187+
$typeName = $type->getBackingType()->getTypeIdentifier()->value;
188+
$builtin = true;
189+
}
214190

215-
$simpleTypes[] = new SimpleType(
216-
name: $this->getEnumTypeName($typeName),
217-
builtin: $this->getTypeBuildIn($typeName),
218-
enum: $this->getEnumValues($typeName),
219-
);
191+
if ($typeName === '') {
192+
throw new InvalidTypeException();
220193
}
221194

222-
return new Type(types: $simpleTypes);
195+
return new SimpleType(
196+
name: $typeName,
197+
builtin: $builtin,
198+
collectionType: $collectionType,
199+
enum: $enum,
200+
);
223201
}
224202

225203
private function hasPropertyDefaultValue(\ReflectionProperty $property): bool
@@ -243,7 +221,7 @@ private function getPropertyDefaultValue(\ReflectionProperty $property): mixed
243221
return $default ?? null;
244222
}
245223

246-
private function createPropertyInfo(): PropertyInfoExtractorInterface
224+
private function createPropertyInfo(): PropertyTypeExtractorInterface
247225
{
248226
return new PropertyInfoExtractor(typeExtractors: [
249227
new PhpStanExtractor(),

tests/Unit/GeneratorTest.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ public function testGenerateMovie(): void
4949
'title' => 'Release Status',
5050
'description' => 'The release status of the movie',
5151
'oneOf' => [
52-
[
53-
'type' => 'null',
54-
],
5552
[
5653
'type' => 'string',
5754
'enum' => ['Released', 'Rumored', 'Post Production', 'In Production', 'Planned', 'Canceled'],
5855
],
56+
[
57+
'type' => 'null',
58+
],
5959
],
6060
],
6161
'releaseDate' => [
@@ -108,7 +108,6 @@ public function testGenerateActor(): void
108108
'title' => 'Filmography',
109109
'description' => 'List of movies and series featuring the actor',
110110
'oneOf' => [
111-
['type' => 'null'],
112111
[
113112
'type' => 'array',
114113
'items' => [
@@ -118,22 +117,23 @@ public function testGenerateActor(): void
118117
],
119118
],
120119
],
120+
['type' => 'null'],
121121
],
122122
],
123123
'bestMovie' => [
124124
'title' => 'Best Movie',
125125
'description' => 'The best movie of the actor',
126126
'oneOf' => [
127-
['type' => 'null'],
128127
['$ref' => '#/definitions/Movie'],
128+
['type' => 'null'],
129129
],
130130
],
131131
'bestSeries' => [
132132
'title' => 'Best Series',
133133
'description' => 'The most prominent series of the actor',
134134
'oneOf' => [
135-
['type' => 'null'],
136135
['$ref' => '#/definitions/Series'],
136+
['type' => 'null'],
137137
],
138138
],
139139
],
@@ -183,7 +183,6 @@ public function testGenerateActor(): void
183183
'title' => 'Release Status',
184184
'description' => 'The release status of the movie',
185185
'oneOf' => [
186-
['type' => 'null'],
187186
[
188187
'type' => 'string',
189188
'enum' => [
@@ -195,6 +194,7 @@ public function testGenerateActor(): void
195194
'Canceled',
196195
],
197196
],
197+
['type' => 'null'],
198198
],
199199
],
200200
],
@@ -234,7 +234,6 @@ public function testGenerateActor(): void
234234
'title' => 'Series Status',
235235
'description' => 'The current status of the series',
236236
'oneOf' => [
237-
['type' => 'null'],
238237
[
239238
'type' => 'string',
240239
'enum' => [
@@ -245,6 +244,7 @@ public function testGenerateActor(): void
245244
'Hiatus',
246245
],
247246
],
247+
['type' => 'null'],
248248
],
249249
],
250250
'firstAirDate' => [
@@ -269,8 +269,8 @@ public function testGenerateActor(): void
269269
'title' => 'Seasons',
270270
'description' => 'Number of seasons released',
271271
'oneOf' => [
272-
['type' => 'null'],
273272
['type' => 'integer'],
273+
['type' => 'null'],
274274
],
275275
],
276276
],
@@ -295,25 +295,25 @@ public function testGenerateFlexibleValue(): void
295295
'title' => 'Value',
296296
'description' => 'Can be either string or integer',
297297
'oneOf' => [
298-
['type' => 'string'],
299298
['type' => 'integer'],
299+
['type' => 'string'],
300300
],
301301
],
302302
'flag' => [
303303
'title' => 'Optional Flag',
304304
'description' => 'Boolean or null',
305305
'oneOf' => [
306-
['type' => 'null'],
307306
['type' => 'boolean'],
307+
['type' => 'null'],
308308
],
309309
],
310310
'flex' => [
311311
'title' => 'Flexible Field',
312312
'description' => 'Can be string, int, or null',
313313
'oneOf' => [
314+
['type' => 'integer'],
314315
['type' => 'null'],
315316
['type' => 'string'],
316-
['type' => 'integer'],
317317
],
318318
],
319319
],

0 commit comments

Comments
 (0)