├── LICENSE
├── composer.json
├── context.yaml
├── psalm-baseline.xml
├── rector.php
└── src
├── AbstractDefinition.php
├── Attribute
├── AdditionalProperties.php
├── Constraint
│ ├── Enum.php
│ ├── Items.php
│ ├── Length.php
│ ├── MultipleOf.php
│ ├── Pattern.php
│ └── Range.php
└── Field.php
├── Exception
├── DefinitionException.php
├── GeneratorException.php
└── InvalidTypeException.php
├── Generator.php
├── GeneratorInterface.php
├── Parser
├── ClassParser.php
├── ClassParserInterface.php
├── Parser.php
├── ParserInterface.php
├── Property.php
├── PropertyInterface.php
├── SimpleType.php
├── Type.php
└── TypeInterface.php
├── Schema.php
├── Schema
├── Definition.php
├── Format.php
├── Property.php
├── PropertyOption.php
├── PropertyOptions.php
├── PropertyType.php
├── Reference.php
└── Type.php
└── Validation
├── AdditionalPropertiesExtractor.php
├── AttributeConstraintExtractor.php
├── CompositePropertyDataExtractor.php
├── Constraint
├── AbstractConstraint.php
├── ArrayConstraint.php
├── NumericConstraint.php
└── StringConstraint.php
├── ConstraintMapper.php
├── DocBlockParser.php
├── PhpDocValidationConstraintExtractor.php
└── PropertyDataExtractorInterface.php
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 Spiral Scout
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spiral/json-schema-generator",
3 | "description": "Provides the ability to generate JSON schemas from Data Transfer Object (DTO) classes",
4 | "keywords": [],
5 | "homepage": "https://github.com/spiral/json-schema-generator",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Anton Titov (wolfy-j)",
10 | "email": "wolfy-j@spiralscout.com"
11 | },
12 | {
13 | "name": "Pavel Buchnev (butschster)",
14 | "email": "pavel.buchnev@spiralscout.com"
15 | },
16 | {
17 | "name": "Aleksei Gagarin (roxblnfk)",
18 | "email": "alexey.gagarin@spiralscout.com"
19 | },
20 | {
21 | "name": "Maksim Smakouz (msmakouz)",
22 | "email": "maksim.smakouz@spiralscout.com"
23 | }
24 | ],
25 | "require": {
26 | "php": ">=8.3",
27 | "symfony/property-info": "^7.2.0 || ^8.0.0",
28 | "phpstan/phpdoc-parser": "^1.33 | ^2.1",
29 | "phpdocumentor/reflection-docblock": "^5.3"
30 | },
31 | "require-dev": {
32 | "phpunit/phpunit": "^10.5.45",
33 | "spiral/code-style": "^2.2.2",
34 | "vimeo/psalm": "^6.10",
35 | "rector/rector": "^2.0"
36 | },
37 | "autoload": {
38 | "psr-4": {
39 | "Spiral\\JsonSchemaGenerator\\": "src"
40 | }
41 | },
42 | "autoload-dev": {
43 | "psr-4": {
44 | "Spiral\\JsonSchemaGenerator\\Tests\\": "tests"
45 | }
46 | },
47 | "scripts": {
48 | "cs:fix": "php-cs-fixer fix -v",
49 | "psalm": "psalm",
50 | "refactor": "rector process --config=rector.php",
51 | "refactor:ci": "rector process --config=rector.php --dry-run --ansi",
52 | "test": "phpunit",
53 | "test-coverage": "phpunit --coverage",
54 | "psalm:baseline": "psalm --set-baseline=psalm-baseline.xml"
55 | },
56 | "config": {
57 | "sort-packages": true
58 | },
59 | "minimum-stability": "dev",
60 | "prefer-stable": true
61 | }
62 |
--------------------------------------------------------------------------------
/context.yaml:
--------------------------------------------------------------------------------
1 | $schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json'
2 |
3 | tools:
4 | - id: run-tests
5 | description: 'Run tests'
6 | type: run
7 | commands:
8 | - cmd: vendor/bin/phpunit
9 |
10 | documents:
11 | - description: 'Project structure overview'
12 | outputPath: project-structure.md
13 | sources:
14 | - type: tree
15 | sourcePaths:
16 | - src
17 | showCharCount: true
18 |
19 | - description: Source code
20 | outputPath: source-code.md
21 | sources:
22 | - type: file
23 | sourcePaths:
24 | - src
25 |
--------------------------------------------------------------------------------
/psalm-baseline.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | paths([
12 | __DIR__ . '/src',
13 | __DIR__ . '/tests',
14 | ]);
15 |
16 | // Register rules for PHP 8.4 migration
17 | $rectorConfig->sets([
18 | SetList::PHP_83,
19 | LevelSetList::UP_TO_PHP_83,
20 | ]);
21 |
22 | // Skip vendor directories
23 | $rectorConfig->skip([
24 | __DIR__ . '/vendor',
25 | AddOverrideAttributeToOverriddenMethodsRector::class,
26 | ]);
27 | };
28 |
--------------------------------------------------------------------------------
/src/AbstractDefinition.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | protected array $properties = [];
18 |
19 | /**
20 | * @param non-empty-string $name
21 | */
22 | public function addProperty(string $name, Property $property): self
23 | {
24 | $this->properties[$name] = $property;
25 |
26 | return $this;
27 | }
28 |
29 | protected function renderProperties(array $schema): array
30 | {
31 | if ($this->properties === []) {
32 | return $schema;
33 | }
34 |
35 | $schema['properties'] = [];
36 |
37 | // Building properties
38 | $required = [];
39 | foreach ($this->properties as $name => $property) {
40 | $schema['properties'][$name] = $property->jsonSerialize();
41 |
42 | if ($property->required) {
43 | $required[] = $name;
44 | }
45 | }
46 |
47 | if ($required !== []) {
48 | $schema['required'] = $required;
49 | }
50 |
51 | return $schema;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Attribute/AdditionalProperties.php:
--------------------------------------------------------------------------------
1 | propertyDataExtractor = $propertyDataExtractor ?? CompositePropertyDataExtractor::createDefault();
31 | }
32 |
33 | /**
34 | * @param class-string|\ReflectionClass $class
35 | */
36 | public function generate(string|\ReflectionClass $class): Schema
37 | {
38 | $class = $this->parser->parse($class);
39 |
40 | // check cached
41 | if (isset($this->cache[$class->getName()])) {
42 | return $this->cache[$class->getName()];
43 | }
44 |
45 | $schema = new Schema();
46 |
47 | $dependencies = [];
48 | // Generating properties
49 | foreach ($class->getProperties() as $property) {
50 | $psc = $this->generateProperty($property);
51 | if ($psc === null) {
52 | continue;
53 | }
54 |
55 | // does it refer to any other classes
56 | $dependencies = [...$dependencies, ...$psc->getDependencies()];
57 |
58 | $schema->addProperty($property->getName(), $psc);
59 | }
60 |
61 | // Generating dependencies
62 | $dependencies = \array_unique($dependencies);
63 | $rollingDependencies = [];
64 | $doneDependencies = [];
65 |
66 | do {
67 | foreach ($dependencies as $dependency) {
68 | $dependency = $this->parser->parse($dependency);
69 | $definition = $this->generateDefinition($dependency, $rollingDependencies);
70 | if ($definition === null) {
71 | continue;
72 | }
73 |
74 | $schema->addDefinition($dependency->getShortName(), $definition);
75 | }
76 |
77 | $doneDependencies = [...$doneDependencies, ...$dependencies];
78 | $rollingDependencies = \array_diff($rollingDependencies, $doneDependencies);
79 | if ($rollingDependencies === []) {
80 | break;
81 | }
82 |
83 | $dependencies = $rollingDependencies;
84 | } while (true);
85 |
86 | // caching
87 | $this->cache[$class->getName()] = $schema;
88 |
89 | return $schema;
90 | }
91 |
92 | protected function generateDefinition(ClassParserInterface $class, array &$dependencies = []): ?Definition
93 | {
94 | $properties = [];
95 | // class properties
96 | foreach ($class->getProperties() as $property) {
97 | $psc = $this->generateProperty($property);
98 | if ($psc === null) {
99 | continue;
100 | }
101 |
102 | $dependencies = [...$dependencies, ...$psc->getDependencies()];
103 | $properties[$property->getName()] = $psc;
104 | }
105 |
106 | return new Definition(type: $class->getName(), title: $class->getShortName(), properties: $properties);
107 | }
108 |
109 | protected function generateProperty(PropertyInterface $property): ?Property
110 | {
111 | // Looking for Field attribute
112 | $title = '';
113 | $description = '';
114 | $default = null;
115 | $format = null;
116 |
117 | $attribute = $property->findAttribute(Field::class);
118 | if ($attribute !== null) {
119 | $title = $attribute->title;
120 | $description = $attribute->description;
121 | $default = $attribute->default;
122 | $format = $attribute->format;
123 | }
124 |
125 | if ($default === null && $property->hasDefaultValue()) {
126 | $default = $property->getDefaultValue();
127 | }
128 |
129 | $type = $property->getType();
130 | $propertyTypes = $this->extractPropertyTypes($type);
131 |
132 | // Extract validation constraints using the configurable extractor system
133 | $validationRules = $this->extractValidationConstraints($property, $propertyTypes);
134 |
135 | return new Property(
136 | types: $propertyTypes,
137 | title: $title,
138 | description: $description,
139 | required: $default === null && !$type->allowsNull(),
140 | default: $default,
141 | format: $format,
142 | validationRules: $validationRules,
143 | );
144 | }
145 |
146 | /**
147 | * @return list
148 | */
149 | private function extractPropertyTypes(Type $type): array
150 | {
151 | return \array_map(static fn(SimpleType $simpleType) => new PropertyType(
152 | type: $simpleType->getName(),
153 | enum: $simpleType->getEnumValues(),
154 | collectionTypes: $simpleType->isCollection() ? \array_map(
155 | static fn(SimpleType $collectionSimpleType) => new PropertyType(
156 | type: $collectionSimpleType->getName(),
157 | enum: $collectionSimpleType->getEnumValues(),
158 | ),
159 | $simpleType->getCollectionType()?->types ?? [],
160 | ) : null,
161 | ), $type->types);
162 | }
163 |
164 | /**
165 | * Extract validation constraints from property using the configured extractors.
166 | */
167 | private function extractValidationConstraints(PropertyInterface $property, array $propertyTypes): array
168 | {
169 | $allValidationRules = [];
170 |
171 | foreach ($propertyTypes as $propertyType) {
172 | if ($propertyType->type instanceof Schema\Type) {
173 | $validationRules = $this->propertyDataExtractor->extractValidationRules($property, $propertyType->type);
174 | $allValidationRules = \array_merge($allValidationRules, $validationRules);
175 | }
176 | }
177 |
178 | return $allValidationRules;
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/src/GeneratorInterface.php:
--------------------------------------------------------------------------------
1 |
31 | */
32 | private array $constructorParameters = [];
33 |
34 | private readonly PropertyTypeExtractorInterface $propertyInfo;
35 |
36 | /**
37 | * @param \ReflectionClass|class-string $class
38 | */
39 | public function __construct(\ReflectionClass|string $class)
40 | {
41 | if (\is_string($class)) {
42 | try {
43 | $class = new \ReflectionClass($class);
44 | } catch (\ReflectionException $e) {
45 | throw new GeneratorException($e->getMessage(), $e->getCode(), $e);
46 | }
47 | }
48 |
49 | $this->class = $class;
50 | $this->propertyInfo = $this->createPropertyInfo();
51 |
52 | $constructor = $this->class->getConstructor();
53 | if ($constructor !== null) {
54 | foreach ($constructor->getParameters() as $parameter) {
55 | if ($parameter->isPromoted()) {
56 | $this->constructorParameters[$parameter->getName()] = $parameter;
57 | }
58 | }
59 | }
60 | }
61 |
62 | /**
63 | * @return class-string
64 | */
65 | public function getName(): string
66 | {
67 | return $this->class->getName();
68 | }
69 |
70 | /**
71 | * @return non-empty-string
72 | */
73 | public function getShortName(): string
74 | {
75 | return $this->class->getShortName();
76 | }
77 |
78 | /**
79 | * @return array
80 | */
81 | public function getProperties(): array
82 | {
83 | $properties = [];
84 | foreach ($this->class->getProperties() as $property) {
85 | // skipping private, protected, static properties, properties without type
86 | if ($property->isPrivate() || $property->isProtected() || $property->isStatic() || !$property->hasType()) {
87 | continue;
88 | }
89 |
90 | /**
91 | * @var \ReflectionNamedType|\ReflectionUnionType|null $type
92 | */
93 | $type = $property->getType();
94 | if (!$type instanceof \ReflectionNamedType && !$type instanceof \ReflectionUnionType) {
95 | continue;
96 | }
97 |
98 | $properties[] = new Property(
99 | property: $property,
100 | type: $this->getPropertyType($property),
101 | hasDefaultValue: $this->hasPropertyDefaultValue($property),
102 | defaultValue: $this->getPropertyDefaultValue($property),
103 | );
104 | }
105 |
106 | return $properties;
107 | }
108 |
109 | public function isEnum(): bool
110 | {
111 | return $this->class->isEnum();
112 | }
113 |
114 | /**
115 | * @param non-empty-string|class-string $typeName
116 | *
117 | * @return list|null
118 | */
119 | private function getEnumValues(string $typeName): ?array
120 | {
121 | if (!\is_subclass_of($typeName, \BackedEnum::class)) {
122 | return null;
123 | }
124 |
125 | $reflectionEnum = new \ReflectionEnum($typeName);
126 |
127 | return \array_map(
128 | static fn(\ReflectionEnumUnitCase $case): int|string => $case->getValue()->value,
129 | $reflectionEnum->getCases(),
130 | );
131 | }
132 |
133 | private function getPropertyType(\ReflectionProperty $property): Type
134 | {
135 | $type = $this->propertyInfo->getType($property->class, $property->getName());
136 |
137 | if ($type === null) {
138 | throw new InvalidTypeException();
139 | }
140 |
141 | return $this->createType($type);
142 | }
143 |
144 | private function createType(TypeInfoType $type): Type
145 | {
146 | $simpleTypes = [];
147 | if ($type instanceof UnionType) {
148 | foreach ($type->getTypes() as $subType) {
149 | $simpleType = $this->createSimpleType($subType);
150 | if ($simpleType !== null) {
151 | $simpleTypes[] = $simpleType;
152 | }
153 | }
154 | } else {
155 | $simpleType = $this->createSimpleType($type);
156 | if ($simpleType !== null) {
157 | $simpleTypes[] = $simpleType;
158 | }
159 | }
160 |
161 | return new Type(types: $simpleTypes);
162 | }
163 |
164 | private function createSimpleType(TypeInfoType $type): ?SimpleType
165 | {
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;
183 | }
184 |
185 | if ($type instanceof BackedEnumType) {
186 | $enum = $this->getEnumValues($type->getClassName());
187 | $typeName = $type->getBackingType()->getTypeIdentifier()->value;
188 | $builtin = true;
189 | }
190 |
191 | if ($typeName === '') {
192 | throw new InvalidTypeException();
193 | }
194 |
195 | return new SimpleType(
196 | name: $typeName,
197 | builtin: $builtin,
198 | collectionType: $collectionType,
199 | enum: $enum,
200 | );
201 | }
202 |
203 | private function hasPropertyDefaultValue(\ReflectionProperty $property): bool
204 | {
205 | $parameter = $this->constructorParameters[$property->getName()] ?? null;
206 |
207 | return $property->hasDefaultValue() || ($parameter !== null && $parameter->isDefaultValueAvailable());
208 | }
209 |
210 | private function getPropertyDefaultValue(\ReflectionProperty $property): mixed
211 | {
212 | if ($property->hasDefaultValue()) {
213 | $default = $property->getDefaultValue();
214 | }
215 |
216 | $parameter = $this->constructorParameters[$property->getName()] ?? null;
217 | if ($parameter !== null && $property->isPromoted() && $parameter->isDefaultValueAvailable()) {
218 | $default = $parameter->getDefaultValue();
219 | }
220 |
221 | return $default ?? null;
222 | }
223 |
224 | private function createPropertyInfo(): PropertyTypeExtractorInterface
225 | {
226 | return new PropertyInfoExtractor(typeExtractors: [
227 | new PhpStanExtractor(),
228 | new PhpDocExtractor(),
229 | new ReflectionExtractor(),
230 | ]);
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/src/Parser/ClassParserInterface.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | public function getProperties(): array;
23 | }
24 |
--------------------------------------------------------------------------------
/src/Parser/Parser.php:
--------------------------------------------------------------------------------
1 | property->getName();
25 | }
26 |
27 | /**
28 | * @template T
29 | *
30 | * @param class-string $name The class name of the attribute.
31 | *
32 | * @return T|null The attribute or {@see null}, if the requested attribute does not exist.
33 | */
34 | public function findAttribute(string $name): ?object
35 | {
36 | $name = $this->property->getAttributes($name);
37 | if ($name !== []) {
38 | return $name[0]->newInstance();
39 | }
40 |
41 | return null;
42 | }
43 |
44 | public function hasDefaultValue(): bool
45 | {
46 | return $this->hasDefaultValue;
47 | }
48 |
49 | public function getDefaultValue(): mixed
50 | {
51 | return $this->defaultValue;
52 | }
53 |
54 | public function getType(): Type
55 | {
56 | return $this->type;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Parser/PropertyInterface.php:
--------------------------------------------------------------------------------
1 | $name The class name of the attribute.
18 | *
19 | * @return T|null The attribute or {@see null}, if the requested attribute does not exist.
20 | */
21 | public function findAttribute(string $name): ?object;
22 |
23 | public function hasDefaultValue(): bool;
24 |
25 | public function getDefaultValue(): mixed;
26 |
27 | public function getType(): Type;
28 | }
29 |
--------------------------------------------------------------------------------
/src/Parser/SimpleType.php:
--------------------------------------------------------------------------------
1 | |null $enum
22 | */
23 | public function __construct(
24 | string $name,
25 | private bool $builtin,
26 | private ?Type $collectionType = null,
27 | private ?array $enum = null,
28 | ) {
29 | /** @psalm-suppress PropertyTypeCoercion */
30 | $this->name = $this->builtin ? SchemaType::fromBuiltIn($name) : $name;
31 | }
32 |
33 | /**
34 | * @return class-string|SchemaType
35 | */
36 | public function getName(): string|SchemaType
37 | {
38 | return $this->name;
39 | }
40 |
41 | public function isBuiltin(): bool
42 | {
43 | return $this->builtin;
44 | }
45 |
46 | public function isEnum(): bool
47 | {
48 | return $this->enum !== null;
49 | }
50 |
51 | /**
52 | * @return list|null
53 | */
54 | public function getEnumValues(): ?array
55 | {
56 | return $this->enum;
57 | }
58 |
59 | public function isCollection(): bool
60 | {
61 | return $this->collectionType !== null;
62 | }
63 |
64 | public function getCollectionType(): ?Type
65 | {
66 | return $this->collectionType;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Parser/Type.php:
--------------------------------------------------------------------------------
1 | $types
16 | */
17 | public function __construct(
18 | public array $types,
19 | ) {}
20 |
21 | public function allowsNull(): bool
22 | {
23 | return \count(\array_filter($this->types, static fn(SimpleType $type): bool => $type->getName() === SchemaType::Null)) !== 0;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Parser/TypeInterface.php:
--------------------------------------------------------------------------------
1 | definitions[$name] = $definition;
19 | return $this;
20 | }
21 |
22 | public function jsonSerialize(): array
23 | {
24 | $schema = $this->renderProperties(['type' => 'object']);
25 |
26 | if ($this->definitions !== []) {
27 | $schema['definitions'] = [];
28 |
29 | foreach ($this->definitions as $name => $definition) {
30 | $schema['definitions'][$name] = $definition->jsonSerialize();
31 | }
32 | }
33 |
34 | return $schema;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Schema/Definition.php:
--------------------------------------------------------------------------------
1 | $property) {
26 | if (!$property instanceof Property) {
27 | throw new DefinitionException(\sprintf(
28 | 'Property `%s` is not an instance of `%s`.',
29 | // type name or class name
30 | \get_debug_type($property),
31 | Property::class,
32 | ));
33 | }
34 |
35 | $this->addProperty($name, $property);
36 | }
37 | }
38 |
39 | public function jsonSerialize(): array
40 | {
41 | $schema = [];
42 | if ($this->title !== '') {
43 | $schema['title'] = $this->title;
44 | }
45 |
46 | if ($this->description !== '') {
47 | $schema['description'] = $this->description;
48 | }
49 |
50 | if ($this->type instanceof Type) {
51 | $schema['type'] = $this->type->value;
52 | } else {
53 | $schema = $this->renderType($schema);
54 | }
55 |
56 | return $this->renderProperties($schema);
57 | }
58 |
59 | private function renderType(array $schema): array
60 | {
61 | if ($this->properties !== []) {
62 | $schema['type'] = 'object';
63 | return $this->renderProperties($schema);
64 | }
65 |
66 | $rf = new \ReflectionClass($this->type);
67 | if (!$rf->isEnum()) {
68 | throw new DefinitionException(\sprintf(
69 | 'SimpleType `%s` must be a backed enum or class with properties.',
70 | $this->type instanceof Type ? $this->type->value : $this->type,
71 | ));
72 | }
73 |
74 | $rf = new \ReflectionEnum($this->type);
75 |
76 | /** @var \ReflectionEnum $rf */
77 | if (!$rf->isBacked()) {
78 | throw new DefinitionException(\sprintf(
79 | 'SimpleType `%s` is not a backed enum.',
80 | $this->type instanceof Type ? $this->type->value : $this->type,
81 | ));
82 | }
83 |
84 | /**
85 | * @var \ReflectionNamedType $type
86 | */
87 | $type = $rf->getBackingType();
88 |
89 | // mapping to json schema type
90 | $schema['type'] = match ($type->getName()) {
91 | 'int', 'float' => 'number',
92 | 'string' => 'string',
93 | 'bool' => 'boolean',
94 | default => throw new DefinitionException(\sprintf(
95 | 'SimpleType `%s` is not a backed enum.',
96 | $this->type instanceof Type ? $this->type->value : $this->type,
97 | )),
98 | };
99 |
100 | // options are scalar values at this point
101 | $schema['enum'] = $this->options;
102 |
103 | return $schema;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/Schema/Format.php:
--------------------------------------------------------------------------------
1 | $types
14 | * @param array $validationRules
15 | */
16 | public function __construct(
17 | public array $types,
18 | public string $title = '',
19 | public string $description = '',
20 | public bool $required = false,
21 | public mixed $default = null,
22 | public ?Format $format = null,
23 | public array $validationRules = [], // NEW: Validation rules from PHPDoc
24 | ) {}
25 |
26 | public function jsonSerialize(): array
27 | {
28 | $property = [];
29 | if ($this->title !== '') {
30 | $property['title'] = $this->title;
31 | }
32 |
33 | if ($this->description !== '') {
34 | $property['description'] = $this->description;
35 | }
36 |
37 | if ($this->default !== null) {
38 | $property['default'] = $this->default;
39 | }
40 |
41 | if ($this->format instanceof Format) {
42 | $property['format'] = $this->format->value;
43 | }
44 |
45 | // Check if we have an array shape constraint that should override the type
46 | if (isset($this->validationRules['type']) && $this->validationRules['type'] === 'object') {
47 | // Array shape or additional properties overrides normal type processing
48 | $property = \array_merge($property, $this->validationRules);
49 |
50 | // Clean up internal metadata keys
51 | unset($property['_additionalPropertiesClass']);
52 |
53 | return $property;
54 | }
55 |
56 | $typesCount = \count($this->types);
57 | if ($typesCount > 1) {
58 | foreach ($this->types as $type) {
59 | $property['oneOf'][] = $this->propertyTypeToDefinition($type);
60 | }
61 | } elseif ($typesCount === 1) {
62 | $property = \array_merge($property, $this->propertyTypeToDefinition($this->types[0]));
63 | }
64 |
65 | // Apply validation rules from PHPDoc constraints (except type overrides)
66 | $filteredValidationRules = $this->validationRules;
67 | if (isset($filteredValidationRules['type'])) {
68 | unset($filteredValidationRules['type'], $filteredValidationRules['properties'], $filteredValidationRules['required'], $filteredValidationRules['additionalProperties'], $filteredValidationRules['_additionalPropertiesClass']);
69 | }
70 | $property = \array_merge($property, $filteredValidationRules);
71 |
72 | return $property;
73 | }
74 |
75 | public function getDependencies(): array
76 | {
77 | $dependencies = [];
78 | foreach ($this->types as $type) {
79 | if (\is_string($type->type)) {
80 | $dependencies[] = $type->type;
81 | }
82 | if ($type->type === Type::Array && $type->collectionTypes !== null && $type->collectionTypes !== []) {
83 | foreach ($type->collectionTypes as $collectionType) {
84 | if (\is_string($collectionType->type)) {
85 | $dependencies[] = $collectionType->type;
86 | }
87 | }
88 | }
89 | }
90 |
91 | // Extract dependencies from additional properties references
92 | if (isset($this->validationRules['_additionalPropertiesClass'])) {
93 | $dependencies[] = $this->validationRules['_additionalPropertiesClass'];
94 | }
95 |
96 | return $dependencies;
97 | }
98 |
99 | protected function propertyTypeToDefinition(PropertyType $propertyType): array
100 | {
101 | $property = [];
102 |
103 | if ($propertyType->type instanceof Type) {
104 | $property['type'] = $propertyType->type->value;
105 | if ($propertyType->enum !== null) {
106 | $property['enum'] = $propertyType->enum;
107 | }
108 | if ($propertyType->type === Type::Array && $propertyType->collectionTypes !== null && $propertyType->collectionTypes !== []) {
109 | $collectionTypeCount = \count($propertyType->collectionTypes);
110 | if ($collectionTypeCount > 1) {
111 | foreach ($propertyType->collectionTypes as $collectionType) {
112 | if ($collectionType->type instanceof Type) {
113 | $schemaType = ['type' => $collectionType->type->value];
114 | if ($collectionType->enum !== null) {
115 | $schemaType['enum'] = $collectionType->enum;
116 | }
117 | $property['items']['anyOf'][] = $schemaType;
118 | } else {
119 | $property['items']['anyOf'][] = [
120 | '$ref' => (new Reference(
121 | $collectionType->type,
122 | ))->jsonSerialize(),
123 | ];
124 | }
125 | }
126 | } elseif ($collectionTypeCount === 1) {
127 | $collectionType = $propertyType->collectionTypes[0];
128 | if ($collectionType->type instanceof Type) {
129 | $property['items'] = ['type' => $collectionType->type->value];
130 | if ($collectionType->enum !== null) {
131 | $property['items']['enum'] = $collectionType->enum;
132 | }
133 | } else {
134 | $property['items'] = ['$ref' => (new Reference($collectionType->type))->jsonSerialize()];
135 | }
136 | }
137 | }
138 | } else {
139 | $property['$ref'] = (new Reference($propertyType->type))->jsonSerialize();
140 | }
141 |
142 | return $property;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/Schema/PropertyOption.php:
--------------------------------------------------------------------------------
1 | value) && !\class_exists($this->value)) {
21 | throw new InvalidTypeException('Invalid property option definition.');
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Schema/PropertyOptions.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | final class PropertyOptions implements \Countable, \ArrayAccess, \JsonSerializable
12 | {
13 | /**
14 | * @var array
15 | */
16 | private array $options = [];
17 |
18 | /**
19 | * @param array $options
20 | */
21 | public function __construct(array $options = [])
22 | {
23 | foreach ($options as $option) {
24 | $this->options[] = new PropertyOption($option);
25 | }
26 | }
27 |
28 | /**
29 | * @return array
30 | */
31 | public function getOptions(): array
32 | {
33 | return $this->options;
34 | }
35 |
36 | public function count(): int
37 | {
38 | return \count($this->options);
39 | }
40 |
41 | public function offsetExists(mixed $offset): bool
42 | {
43 | return isset($this->options[$offset]);
44 | }
45 |
46 | public function offsetGet(mixed $offset): PropertyOption
47 | {
48 | return $this->options[$offset];
49 | }
50 |
51 | /**
52 | * @param int $offset
53 | * @param PropertyOption|class-string|Type $value
54 | */
55 | public function offsetSet(mixed $offset, mixed $value): void
56 | {
57 | $this->options[$offset] = $value instanceof PropertyOption ? $value : new PropertyOption($value);
58 | }
59 |
60 | public function offsetUnset(mixed $offset): void
61 | {
62 | unset($this->options[$offset]);
63 | }
64 |
65 | public function jsonSerialize(): array
66 | {
67 | $types = [];
68 | foreach ($this->options as $option) {
69 | if (\is_string($option->value)) {
70 | // reference to class
71 | $types[] = ['$ref' => (new Reference($option->value))->jsonSerialize()];
72 | continue;
73 | }
74 |
75 | $types[] = ['type' => $option->value->value];
76 | }
77 | return $types;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Schema/PropertyType.php:
--------------------------------------------------------------------------------
1 | |null $enum
15 | * @param list|null $collectionTypes
16 | */
17 | public function __construct(
18 | public string|Type $type,
19 | public ?array $enum = null,
20 | public ?array $collectionTypes = null,
21 | ) {}
22 | }
23 |
--------------------------------------------------------------------------------
/src/Schema/Reference.php:
--------------------------------------------------------------------------------
1 | className, '\\');
22 |
23 | return '#/definitions/' . ($pos === false ? $this->className : \substr($this->className, $pos + 1));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Schema/Type.php:
--------------------------------------------------------------------------------
1 | self::String,
22 | 'integer', 'int' => self::Integer,
23 | 'float', 'double', 'number' => self::Number,
24 | 'boolean', 'bool' => self::Boolean,
25 | 'object' => self::Object,
26 | 'array' => self::Array,
27 | 'null' => self::Null,
28 | default => throw new \InvalidArgumentException(\sprintf('Invalid type `%s`.', $type)),
29 | };
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Validation/AdditionalPropertiesExtractor.php:
--------------------------------------------------------------------------------
1 | findAttribute(AdditionalProperties::class);
24 | if (!$additionalProperties instanceof AdditionalProperties) {
25 | return $validationRules;
26 | }
27 |
28 | // Override array type to object type for additional properties
29 | $validationRules['type'] = 'object';
30 |
31 | // Process the additional properties value type
32 | $additionalPropertiesSchema = $this->processValueType(
33 | $additionalProperties->valueType,
34 | $additionalProperties->valueClass,
35 | );
36 |
37 | $validationRules['additionalProperties'] = $additionalPropertiesSchema;
38 |
39 | // Store class dependencies for later extraction
40 | if ($additionalProperties->valueType === 'object' && $additionalProperties->valueClass !== null) {
41 | $validationRules['_additionalPropertiesClass'] = $additionalProperties->valueClass;
42 | }
43 |
44 | return $validationRules;
45 | }
46 |
47 | /**
48 | * Process the value type and return appropriate JSON schema structure.
49 | *
50 | * @param class-string|null $valueClass
51 | */
52 | private function processValueType(string $valueType, ?string $valueClass): array|bool
53 | {
54 | return match ($valueType) {
55 | 'int', 'integer' => ['type' => 'integer'],
56 | 'number', 'float' => ['type' => 'number'],
57 | 'boolean', 'bool' => ['type' => 'boolean'],
58 | 'mixed' => true, // Allow any type
59 | 'object' => $this->processObjectType($valueClass),
60 | default => ['type' => 'string'], // fallback to string
61 | };
62 | }
63 |
64 | /**
65 | * Process object type with class reference.
66 | *
67 | * @param class-string|null $valueClass
68 | */
69 | private function processObjectType(?string $valueClass): array
70 | {
71 | if ($valueClass === null) {
72 | return ['type' => 'object'];
73 | }
74 |
75 | // Create reference to the class definition
76 | return ['$ref' => (new Reference($valueClass))->jsonSerialize()];
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Validation/AttributeConstraintExtractor.php:
--------------------------------------------------------------------------------
1 | findAttribute(Pattern::class);
24 | if ($pattern instanceof Pattern) {
25 | $validationRules['pattern'] = $pattern->pattern;
26 | }
27 |
28 | // Extract Length constraint
29 | $length = $property->findAttribute(Length::class);
30 | if ($length instanceof Length) {
31 | if ($length->min !== null) {
32 | $validationRules[$this->getLengthMinKey($jsonSchemaType)] = $length->min;
33 | }
34 | if ($length->max !== null) {
35 | $validationRules[$this->getLengthMaxKey($jsonSchemaType)] = $length->max;
36 | }
37 | }
38 |
39 | // Extract Range constraint
40 | $range = $property->findAttribute(Range::class);
41 | if ($range instanceof Range) {
42 | if ($range->min !== null) {
43 | $key = $range->exclusiveMin === true ? 'exclusiveMinimum' : 'minimum';
44 | $validationRules[$key] = $range->min;
45 | }
46 | if ($range->max !== null) {
47 | $key = $range->exclusiveMax === true ? 'exclusiveMaximum' : 'maximum';
48 | $validationRules[$key] = $range->max;
49 | }
50 | }
51 |
52 | // Extract MultipleOf constraint
53 | $multipleOf = $property->findAttribute(MultipleOf::class);
54 | if ($multipleOf instanceof MultipleOf && $this->isNumericType($jsonSchemaType)) {
55 | $validationRules['multipleOf'] = $multipleOf->value;
56 | }
57 |
58 | // Extract Items constraint
59 | $items = $property->findAttribute(Items::class);
60 | if ($items instanceof Items && $jsonSchemaType === Type::Array) {
61 | if ($items->min !== null) {
62 | $validationRules['minItems'] = $items->min;
63 | }
64 | if ($items->max !== null) {
65 | $validationRules['maxItems'] = $items->max;
66 | }
67 | if ($items->unique === true) {
68 | $validationRules['uniqueItems'] = true;
69 | }
70 | }
71 |
72 | // Extract Enum constraint
73 | $enum = $property->findAttribute(Enum::class);
74 | if ($enum instanceof Enum) {
75 | $validationRules['enum'] = $enum->values;
76 | }
77 |
78 | return $validationRules;
79 | }
80 |
81 | private function getLengthMinKey(Type $jsonSchemaType): string
82 | {
83 | return match ($jsonSchemaType) {
84 | Type::Array => 'minItems',
85 | default => 'minLength', // fallback
86 | };
87 | }
88 |
89 | private function getLengthMaxKey(Type $jsonSchemaType): string
90 | {
91 | return match ($jsonSchemaType) {
92 | Type::Array => 'maxItems',
93 | default => 'maxLength', // fallback
94 | };
95 | }
96 |
97 | private function isNumericType(Type $jsonSchemaType): bool
98 | {
99 | return $jsonSchemaType === Type::Integer || $jsonSchemaType === Type::Number;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Validation/CompositePropertyDataExtractor.php:
--------------------------------------------------------------------------------
1 | $extractors
14 | */
15 | public function __construct(
16 | private array $extractors = [],
17 | ) {}
18 |
19 | /**
20 | * Create a default instance with commonly used extractors.
21 | */
22 | public static function createDefault(): self
23 | {
24 | return new self([
25 | new PhpDocValidationConstraintExtractor(),
26 | new AttributeConstraintExtractor(),
27 | new AdditionalPropertiesExtractor(), // Add the new extractor
28 | ]);
29 | }
30 |
31 | /**
32 | * Add an extractor to the composite.
33 | */
34 | public function withExtractor(PropertyDataExtractorInterface $extractor): self
35 | {
36 | return new self([...$this->extractors, $extractor]);
37 | }
38 |
39 | public function extractValidationRules(PropertyInterface $property, Type $jsonSchemaType): array
40 | {
41 | $allValidationRules = [];
42 |
43 | foreach ($this->extractors as $extractor) {
44 | $validationRules = $extractor->extractValidationRules($property, $jsonSchemaType);
45 | $allValidationRules = \array_merge($allValidationRules, $validationRules);
46 | }
47 |
48 | return $allValidationRules;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Validation/Constraint/AbstractConstraint.php:
--------------------------------------------------------------------------------
1 | type;
22 | }
23 |
24 | public function getValue(): mixed
25 | {
26 | return $this->value;
27 | }
28 |
29 | protected function isApplicable(string $jsonSchemaType): bool
30 | {
31 | return match ($this->type) {
32 | 'positive-int', 'negative-int', 'non-positive-int', 'non-negative-int' => $jsonSchemaType === 'integer',
33 | 'int-range' => $jsonSchemaType === 'integer',
34 | 'non-empty-string', 'numeric-string', 'class-string' => $jsonSchemaType === 'string',
35 | 'non-empty-array', 'array-shape' => $jsonSchemaType === 'array',
36 | default => false,
37 | };
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Validation/Constraint/ArrayConstraint.php:
--------------------------------------------------------------------------------
1 | type) {
15 | 'non-empty-array', 'non-empty-list' => ['minItems' => 1],
16 | 'array-shape' => $this->parseArrayShape(),
17 | default => [],
18 | };
19 | }
20 |
21 | private function parseArrayShape(): array
22 | {
23 | if (!\is_array($this->value)) {
24 | return [];
25 | }
26 |
27 | $properties = [];
28 | $required = [];
29 |
30 | foreach ($this->value as $key => $spec) {
31 | $isOptional = \str_ends_with($key, '?');
32 | $cleanKey = $isOptional ? \rtrim($key, '?') : $key;
33 |
34 | if (!$isOptional) {
35 | $required[] = $cleanKey;
36 | }
37 |
38 | // Basic type mapping for shaped array elements
39 | $properties[$cleanKey] = match ($spec) {
40 | 'string' => ['type' => 'string'],
41 | 'int', 'integer' => ['type' => 'integer'],
42 | 'float', 'number' => ['type' => 'number'],
43 | 'bool', 'boolean' => ['type' => 'boolean'],
44 | default => ['type' => 'string'], // fallback
45 | };
46 | }
47 |
48 | $schema = [
49 | 'type' => 'object',
50 | 'properties' => $properties,
51 | 'additionalProperties' => false, // Shaped arrays are strict
52 | ];
53 |
54 | if ($required !== []) {
55 | $schema['required'] = $required;
56 | }
57 |
58 | return $schema;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Validation/Constraint/NumericConstraint.php:
--------------------------------------------------------------------------------
1 | type) {
15 | 'positive-int' => ['minimum' => 1],
16 | 'negative-int' => ['maximum' => -1],
17 | 'non-positive-int' => ['maximum' => 0],
18 | 'non-negative-int' => ['minimum' => 0],
19 | 'int-range' => $this->parseIntRange(),
20 | default => [],
21 | };
22 | }
23 |
24 | private function parseIntRange(): array
25 | {
26 | if (!\is_array($this->value) || \count($this->value) !== 2) {
27 | return [];
28 | }
29 |
30 | [$min, $max] = $this->value;
31 | $schema = [];
32 |
33 | if ($min !== 'min' && \is_numeric($min)) {
34 | $schema['minimum'] = (int) $min;
35 | }
36 |
37 | if ($max !== 'max' && \is_numeric($max)) {
38 | $schema['maximum'] = (int) $max;
39 | }
40 |
41 | return $schema;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Validation/Constraint/StringConstraint.php:
--------------------------------------------------------------------------------
1 | type) {
15 | 'non-empty-string' => ['minLength' => 1],
16 | 'numeric-string' => ['pattern' => '^[0-9]*\.?[0-9]+$'],
17 | 'class-string' => [
18 | 'pattern' => '^[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff\\\\]*$',
19 | 'description' => 'Must be a valid PHP class name',
20 | ],
21 | default => [],
22 | };
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Validation/ConstraintMapper.php:
--------------------------------------------------------------------------------
1 | createConstraintObject($constraint);
25 | } elseif (\is_array($constraint)) {
26 | $constraintObj = $this->createConstraintObjectFromArray($constraint);
27 | } else {
28 | continue;
29 | }
30 |
31 | if ($constraintObj && $this->isConstraintApplicable($constraintObj, $jsonSchemaType)) {
32 | $rules = $constraintObj->toJsonSchema();
33 | $validationRules = \array_merge($validationRules, $rules);
34 | }
35 | }
36 |
37 | return $validationRules;
38 | }
39 |
40 | private function createConstraintObject(string $constraint): ?AbstractConstraint
41 | {
42 | return match (true) {
43 | \in_array($constraint, ['positive-int', 'negative-int', 'non-positive-int', 'non-negative-int'], true) =>
44 | new NumericConstraint($constraint),
45 | \in_array($constraint, ['non-empty-string', 'numeric-string', 'class-string'], true) =>
46 | new StringConstraint($constraint),
47 | \in_array($constraint, ['non-empty-array', 'non-empty-list'], true) =>
48 | new ArrayConstraint($constraint),
49 | default => null,
50 | };
51 | }
52 |
53 | private function createConstraintObjectFromArray(array $constraint): ?AbstractConstraint
54 | {
55 | $type = \array_key_first($constraint);
56 | $value = $type !== null ? $constraint[$type] : null;
57 |
58 | return match ($type) {
59 | 'int-range' => new NumericConstraint($type, $value),
60 | 'array-shape' => new ArrayConstraint($type, $value),
61 | default => null,
62 | };
63 | }
64 |
65 | private function isConstraintApplicable(AbstractConstraint $constraint, Type $jsonSchemaType): bool
66 | {
67 | return match ($constraint->getType()) {
68 | 'positive-int', 'negative-int', 'non-positive-int', 'non-negative-int', 'int-range' =>
69 | $jsonSchemaType === Type::Integer,
70 | 'non-empty-string', 'numeric-string', 'class-string' =>
71 | $jsonSchemaType === Type::String,
72 | 'non-empty-array', 'non-empty-list', 'array-shape' =>
73 | $jsonSchemaType === Type::Array,
74 | default => false,
75 | };
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Validation/DocBlockParser.php:
--------------------------------------------------------------------------------
1 | ]+)>/';
14 | private const string ARRAY_SHAPE_PATTERN = '/array\{([^}]+)\}/';
15 |
16 | public function parseDocComment(string $docComment): array
17 | {
18 | $constraints = [];
19 |
20 | // Extract @var annotation - now captures everything until */ or newline
21 | if (\preg_match(self::VAR_PATTERN, $docComment, $matches)) {
22 | $typeAnnotation = \trim($matches[1]);
23 | $typeAnnotation = \rtrim($typeAnnotation, ' */'); // Clean up trailing */
24 | $constraints = \array_merge($constraints, $this->parseTypeAnnotation($typeAnnotation));
25 | }
26 |
27 | return $constraints;
28 | }
29 |
30 | private function parseTypeAnnotation(string $type): array
31 | {
32 | $constraints = [];
33 |
34 | // Handle simple constraints
35 | $simpleConstraints = [
36 | 'positive-int',
37 | 'negative-int',
38 | 'non-positive-int',
39 | 'non-negative-int',
40 | 'non-empty-string',
41 | 'numeric-string',
42 | 'class-string',
43 | 'non-empty-array',
44 | 'non-empty-list',
45 | ];
46 |
47 | foreach ($simpleConstraints as $constraint) {
48 | if (\str_contains($type, $constraint)) {
49 | $constraints[] = $constraint;
50 | }
51 | }
52 |
53 | // Handle int ranges like int<-42, 1337>
54 | if (\preg_match(self::INT_RANGE_PATTERN, $type, $matches)) {
55 | $range = $this->parseIntRange($matches[1]);
56 | if ($range !== null) {
57 | $constraints[] = ['int-range' => $range];
58 | }
59 | }
60 |
61 | // Handle array shapes like array{foo: string, bar: int}
62 | if (\preg_match(self::ARRAY_SHAPE_PATTERN, $type, $matches)) {
63 | $shape = $this->parseArrayShape($matches[1]);
64 | if ($shape !== []) {
65 | $constraints[] = ['array-shape' => $shape];
66 | }
67 | }
68 |
69 | return $constraints;
70 | }
71 |
72 | private function parseIntRange(string $rangeStr): ?array
73 | {
74 | $parts = \array_map('trim', \explode(',', $rangeStr));
75 |
76 | if (\count($parts) !== 2) {
77 | return null;
78 | }
79 |
80 | return [$parts[0], $parts[1]];
81 | }
82 |
83 | private function parseArrayShape(string $shapeStr): array
84 | {
85 | $shape = [];
86 | $elements = \array_map(\trim(...), \explode(',', $shapeStr));
87 |
88 | foreach ($elements as $element) {
89 | if (\str_contains($element, ':')) {
90 | /** @psalm-suppress PossiblyUndefinedArrayOffset */
91 | [$key, $valueType] = \array_map(\trim(...), \explode(':', $element, 2));
92 | $shape[$key] = $valueType;
93 | }
94 | }
95 |
96 | return $shape;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Validation/PhpDocValidationConstraintExtractor.php:
--------------------------------------------------------------------------------
1 | getPropertyDocComment($property);
20 |
21 | if ($docComment === null) {
22 | return [];
23 | }
24 |
25 | $constraints = $this->docBlockParser->parseDocComment($docComment);
26 |
27 | return $this->constraintMapper->mapConstraintsToJsonSchema($constraints, $jsonSchemaType);
28 | }
29 |
30 | private function getPropertyDocComment(PropertyInterface $property): ?string
31 | {
32 | // We need to access the ReflectionProperty to get doc comment
33 | // This requires extending PropertyInterface or accessing it differently
34 |
35 | // For now, we'll use reflection to get the property's doc comment
36 | // This is a bit hacky but necessary without changing the existing interface
37 |
38 | $reflection = new \ReflectionClass($property);
39 | $propertyReflection = null;
40 |
41 | // Try to get the ReflectionProperty from the Property object
42 | try {
43 | $propertyField = $reflection->getProperty('property');
44 | $propertyReflection = $propertyField->getValue($property);
45 | } catch (\ReflectionException) {
46 | return null;
47 | }
48 |
49 | if (!$propertyReflection instanceof \ReflectionProperty) {
50 | return null;
51 | }
52 |
53 | $docComment = $propertyReflection->getDocComment();
54 | return $docComment === false ? null : $docComment;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Validation/PropertyDataExtractorInterface.php:
--------------------------------------------------------------------------------
1 | Array of validation rules
18 | */
19 | public function extractValidationRules(PropertyInterface $property, Type $jsonSchemaType): array;
20 | }
21 |
--------------------------------------------------------------------------------