Skip to content

Commit f1c0502

Browse files
authored
Merge pull request #18 from spiral/feature/phpdoc-support
feat: Add php-doc validation constraints support
2 parents 59dce49 + 1b4628b commit f1c0502

23 files changed

+1375
-115
lines changed

README.md

Lines changed: 425 additions & 7 deletions
Large diffs are not rendered by default.

psalm-baseline.xml

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<files psalm-version="6.10.1@f9fd6bc117e9ce1e854c2ed6777e7135aaa4966b">
3-
<file src="src/Attribute/Field.php">
4-
<ClassMustBeFinal>
5-
<code><![CDATA[Field]]></code>
6-
</ClassMustBeFinal>
7-
</file>
8-
<file src="src/Exception/DefinitionException.php">
9-
<ClassMustBeFinal>
10-
<code><![CDATA[DefinitionException]]></code>
11-
</ClassMustBeFinal>
12-
</file>
13-
<file src="src/Exception/GeneratorException.php">
14-
<ClassMustBeFinal>
15-
<code><![CDATA[GeneratorException]]></code>
16-
</ClassMustBeFinal>
17-
</file>
18-
<file src="src/Exception/InvalidTypeException.php">
19-
<ClassMustBeFinal>
20-
<code><![CDATA[InvalidTypeException]]></code>
21-
</ClassMustBeFinal>
22-
</file>
23-
<file src="src/Generator.php">
24-
<ClassMustBeFinal>
25-
<code><![CDATA[Generator]]></code>
26-
</ClassMustBeFinal>
27-
</file>
3+
<file src="src/Attribute/Field.php">
4+
<ClassMustBeFinal>
5+
<code><![CDATA[Field]]></code>
6+
</ClassMustBeFinal>
7+
</file>
8+
<file src="src/Exception/DefinitionException.php">
9+
<ClassMustBeFinal>
10+
<code><![CDATA[DefinitionException]]></code>
11+
</ClassMustBeFinal>
12+
</file>
13+
<file src="src/Exception/GeneratorException.php">
14+
<ClassMustBeFinal>
15+
<code><![CDATA[GeneratorException]]></code>
16+
</ClassMustBeFinal>
17+
</file>
18+
<file src="src/Exception/InvalidTypeException.php">
19+
<ClassMustBeFinal>
20+
<code><![CDATA[InvalidTypeException]]></code>
21+
</ClassMustBeFinal>
22+
</file>
2823
</files>

src/AbstractDefinition.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
use Spiral\JsonSchemaGenerator\Schema\Property;
88

9+
/**
10+
* @internal
11+
*/
912
abstract class AbstractDefinition implements \JsonSerializable
1013
{
1114
/**

src/Attribute/Field.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
use Spiral\JsonSchemaGenerator\Schema\Format;
88

99
#[\Attribute(\Attribute::TARGET_PROPERTY)]
10-
class Field
10+
readonly class Field
1111
{
1212
public function __construct(
13-
public readonly string $title = '',
14-
public readonly string $description = '',
15-
public readonly mixed $default = null,
16-
public readonly ?Format $format = null,
13+
public string $title = '',
14+
public string $description = '',
15+
public mixed $default = null,
16+
public ?Format $format = null,
1717
) {}
1818
}

src/Generator.php

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,20 @@
1414
use Spiral\JsonSchemaGenerator\Schema\Definition;
1515
use Spiral\JsonSchemaGenerator\Schema\Property;
1616
use Spiral\JsonSchemaGenerator\Schema\PropertyType;
17+
use Spiral\JsonSchemaGenerator\Validation\ValidationConstraintExtractor;
1718

18-
class Generator implements GeneratorInterface
19+
final class Generator implements GeneratorInterface
1920
{
2021
protected array $cache = [];
22+
private readonly ValidationConstraintExtractor $validationExtractor;
2123

2224
public function __construct(
2325
protected readonly ParserInterface $parser = new Parser(),
24-
) {}
26+
?ValidationConstraintExtractor $validationExtractor = null,
27+
protected readonly GeneratorConfig $config = new GeneratorConfig(),
28+
) {
29+
$this->validationExtractor = $validationExtractor ?? new ValidationConstraintExtractor();
30+
}
2531

2632
/**
2733
* @param class-string|\ReflectionClass $class
@@ -120,14 +126,22 @@ protected function generateProperty(PropertyInterface $property): ?Property
120126
}
121127

122128
$type = $property->getType();
129+
$propertyTypes = $this->extractPropertyTypes($type);
130+
131+
// Extract validation constraints from PHPDoc (if enabled)
132+
$validationRules = [];
133+
if ($this->config->enableValidationConstraints) {
134+
$validationRules = $this->extractValidationConstraints($property, $propertyTypes);
135+
}
123136

124137
return new Property(
125-
types: $this->extractPropertyTypes($type),
138+
types: $propertyTypes,
126139
title: $title,
127140
description: $description,
128141
required: $default === null && !$type->allowsNull(),
129142
default: $default,
130143
format: $format,
144+
validationRules: $validationRules,
131145
);
132146
}
133147

@@ -139,10 +153,30 @@ private function extractPropertyTypes(Type $type): array
139153
return \array_map(static fn(SimpleType $simpleType) => new PropertyType(
140154
type: $simpleType->getName(),
141155
enum: $simpleType->getEnumValues(),
142-
collectionTypes: $simpleType->isCollection() ? \array_map(static fn(SimpleType $collectionSimpleType) => new PropertyType(
143-
type: $collectionSimpleType->getName(),
144-
enum: $collectionSimpleType->getEnumValues(),
145-
), $simpleType->getCollectionType()?->types ?? []) : null,
156+
collectionTypes: $simpleType->isCollection() ? \array_map(
157+
static fn(SimpleType $collectionSimpleType) => new PropertyType(
158+
type: $collectionSimpleType->getName(),
159+
enum: $collectionSimpleType->getEnumValues(),
160+
),
161+
$simpleType->getCollectionType()?->types ?? [],
162+
) : null,
146163
), $type->types);
147164
}
165+
166+
/**
167+
* Extract validation constraints from property PHPDoc
168+
*/
169+
private function extractValidationConstraints(PropertyInterface $property, array $propertyTypes): array
170+
{
171+
$allValidationRules = [];
172+
173+
foreach ($propertyTypes as $propertyType) {
174+
if ($propertyType->type instanceof Schema\Type) {
175+
$validationRules = $this->validationExtractor->extractValidationRules($property, $propertyType->type);
176+
$allValidationRules = \array_merge($allValidationRules, $validationRules);
177+
}
178+
}
179+
180+
return $allValidationRules;
181+
}
148182
}

src/GeneratorConfig.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\JsonSchemaGenerator;
6+
7+
final readonly class GeneratorConfig
8+
{
9+
public function __construct(
10+
public bool $enableValidationConstraints = true,
11+
) {}
12+
}

src/Schema.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
use Spiral\JsonSchemaGenerator\Schema\Definition;
88

9+
/**
10+
* @internal
11+
*/
912
final class Schema extends AbstractDefinition
1013
{
1114
private array $definitions = [];

src/Schema/Definition.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
use Spiral\JsonSchemaGenerator\AbstractDefinition;
88
use Spiral\JsonSchemaGenerator\Exception\DefinitionException;
99

10+
/**
11+
* @internal
12+
*/
1013
final class Definition extends AbstractDefinition
1114
{
1215
/**

src/Schema/Property.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
namespace Spiral\JsonSchemaGenerator\Schema;
66

7+
/**
8+
* @internal
9+
*/
710
final readonly class Property implements \JsonSerializable
811
{
912
/**
1013
* @param list<PropertyType> $types
14+
* @param array<string, mixed> $validationRules
1115
*/
1216
public function __construct(
1317
public array $types,
@@ -16,6 +20,7 @@ public function __construct(
1620
public bool $required = false,
1721
public mixed $default = null,
1822
public ?Format $format = null,
23+
public array $validationRules = [], // NEW: Validation rules from PHPDoc
1924
) {}
2025

2126
public function jsonSerialize(): array
@@ -37,6 +42,13 @@ public function jsonSerialize(): array
3742
$property['format'] = $this->format->value;
3843
}
3944

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 overrides normal type processing
48+
$property = \array_merge($property, $this->validationRules);
49+
return $property;
50+
}
51+
4052
$typesCount = \count($this->types);
4153
if ($typesCount > 1) {
4254
foreach ($this->types as $type) {
@@ -46,6 +58,13 @@ public function jsonSerialize(): array
4658
$property = \array_merge($property, $this->propertyTypeToDefinition($this->types[0]));
4759
}
4860

61+
// Apply validation rules from PHPDoc constraints (except type overrides)
62+
$filteredValidationRules = $this->validationRules;
63+
if (isset($filteredValidationRules['type'])) {
64+
unset($filteredValidationRules['type'], $filteredValidationRules['properties'], $filteredValidationRules['required'], $filteredValidationRules['additionalProperties']);
65+
}
66+
$property = \array_merge($property, $filteredValidationRules);
67+
4968
return $property;
5069
}
5170

src/Schema/PropertyType.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
namespace Spiral\JsonSchemaGenerator\Schema;
66

7+
/**
8+
* @internal
9+
*/
710
final readonly class PropertyType
811
{
912
/**

0 commit comments

Comments
 (0)