Skip to content

Commit 39720e4

Browse files
authored
Merge pull request #19 from spiral/issue/16
Add Dedicated Constraint Attributes for JSON Schema Validation
2 parents f1c0502 + 0d67ba7 commit 39720e4

24 files changed

+1324
-138
lines changed

README.md

Lines changed: 247 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,184 @@ The generated schema will include a `oneOf` section to reflect the union types:
447447
> All supported types are automatically resolved from native PHP type declarations and reflected in the JSON Schema
448448
> output using oneOf.
449449
450+
## Constraint Attributes
451+
452+
Generator supports dedicated constraint attributes that provide a clean, modular approach to validation rules.
453+
454+
### Available Constraint Attributes
455+
456+
#### String Constraints
457+
458+
- `#[Pattern(regex)]` - Regular expression pattern validation
459+
- `#[Length(min, max)]` - String length constraints
460+
461+
#### Numeric Constraints
462+
463+
- `#[Range(min, max, exclusiveMin, exclusiveMax)]` - Numeric range validation with optional exclusive bounds
464+
- `#[MultipleOf(value)]` - Multiple of validation for numbers
465+
466+
#### Array Constraints
467+
468+
- `#[Items(min, max, unique)]` - Array item constraints with optional uniqueness
469+
- `#[Length(min, max)]` - Array length constraints (same attribute as strings, auto-detects type)
470+
471+
#### General Constraints
472+
473+
- `#[Enum(values)]` - Enumeration validation with array of allowed values
474+
475+
### Usage Examples
476+
477+
#### String Validation
478+
479+
```php
480+
namespace App\DTO;
481+
482+
use Spiral\JsonSchemaGenerator\Attribute\Field;
483+
use Spiral\JsonSchemaGenerator\Attribute\Constraint\Pattern;
484+
use Spiral\JsonSchemaGenerator\Attribute\Constraint\Length;
485+
486+
final readonly class User
487+
{
488+
public function __construct(
489+
#[Field(title: 'Full Name', description: 'User full name in Title Case')]
490+
#[Pattern('^[A-Z][a-z]+(?: [A-Z][a-z]+)*$')]
491+
#[Length(min: 2, max: 100)]
492+
public string $name,
493+
494+
#[Field(title: 'Username')]
495+
#[Pattern('^[a-zA-Z0-9_]{3,20}$')]
496+
#[Length(min: 3, max: 20)]
497+
public string $username,
498+
499+
#[Field(title: 'Email', format: Format::Email)]
500+
#[Pattern('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$')]
501+
public string $email,
502+
) {}
503+
}
504+
```
505+
506+
#### Numeric Validation
507+
508+
```php
509+
namespace App\DTO;
510+
511+
use Spiral\JsonSchemaGenerator\Attribute\Field;
512+
use Spiral\JsonSchemaGenerator\Attribute\Constraint\Range;
513+
use Spiral\JsonSchemaGenerator\Attribute\Constraint\MultipleOf;
514+
515+
final readonly class Product
516+
{
517+
public function __construct(
518+
#[Field(title: 'Price', description: 'Product price in USD')]
519+
#[Range(min: 0.01, max: 99999.99)]
520+
#[MultipleOf(0.01)]
521+
public float $price,
522+
523+
#[Field(title: 'Stock Quantity')]
524+
#[Range(min: 0, max: 10000)]
525+
public int $stock,
526+
527+
#[Field(title: 'Discount Percentage')]
528+
#[Range(min: 0, max: 100, exclusiveMax: true)]
529+
public float $discountPercent,
530+
) {}
531+
}
532+
```
533+
534+
#### Array and Enum Validation
535+
536+
```php
537+
namespace App\DTO;
538+
539+
use Spiral\JsonSchemaGenerator\Attribute\Field;
540+
use Spiral\JsonSchemaGenerator\Attribute\Constraint\Items;
541+
use Spiral\JsonSchemaGenerator\Attribute\Constraint\Length;
542+
use Spiral\JsonSchemaGenerator\Attribute\Constraint\Enum;
543+
544+
final readonly class BlogPost
545+
{
546+
public function __construct(
547+
#[Field(title: 'Tags', description: 'Post tags')]
548+
#[Items(min: 1, max: 10, unique: true)]
549+
public array $tags,
550+
551+
#[Field(title: 'Categories', description: 'Post categories')]
552+
#[Length(min: 1, max: 5)]
553+
public array $categories,
554+
555+
#[Field(title: 'Status')]
556+
#[Enum(['draft', 'published', 'archived', 'pending'])]
557+
public string $status,
558+
559+
#[Field(title: 'Priority')]
560+
#[Enum([1, 2, 3, 4, 5])]
561+
public int $priority,
562+
) {}
563+
}
564+
```
565+
566+
### Generated Schema Output
567+
568+
The constraint attributes generate clean, standards-compliant JSON Schema validation rules:
569+
570+
```json
571+
{
572+
"type": "object",
573+
"properties": {
574+
"name": {
575+
"title": "Full Name",
576+
"description": "User full name in Title Case",
577+
"type": "string",
578+
"pattern": "^[A-Z][a-z]+(?: [A-Z][a-z]+)*$",
579+
"minLength": 2,
580+
"maxLength": 100
581+
},
582+
"price": {
583+
"title": "Price",
584+
"description": "Product price in USD",
585+
"type": "number",
586+
"minimum": 0.01,
587+
"maximum": 99999.99,
588+
"multipleOf": 0.01
589+
},
590+
"tags": {
591+
"title": "Tags",
592+
"description": "Post tags",
593+
"type": "array",
594+
"minItems": 1,
595+
"maxItems": 10,
596+
"uniqueItems": true
597+
},
598+
"status": {
599+
"title": "Status",
600+
"type": "string",
601+
"enum": [
602+
"draft",
603+
"published",
604+
"archived",
605+
"pending"
606+
]
607+
}
608+
},
609+
"required": [
610+
"name",
611+
"price",
612+
"tags",
613+
"status"
614+
]
615+
}
616+
```
617+
618+
### Type Safety
619+
620+
Constraint attributes are automatically validated for type compatibility:
621+
622+
- `Pattern` only applies to string properties
623+
- `Range` and `MultipleOf` only apply to numeric properties (int, float)
624+
- `Items` constraints only apply to array properties
625+
- `Length` adapts behavior: `minLength`/`maxLength` for strings, `minItems`/`maxItems` for arrays
626+
- `Enum` works with any property type
627+
450628
## PHPDoc Validation Constraints
451629

452630
Generator supports extracting validation constraints from PHPDoc comments, providing rich validation
@@ -615,33 +793,89 @@ final class ContactInfo
615793

616794
## Configuration Options
617795

618-
You can configure the generator behavior using the `GeneratorConfig` class:
796+
You can configure the generator behavior using the `GeneratorConfig` class and custom property data extractors:
619797

620798
```php
621799
use Spiral\JsonSchemaGenerator\Generator;
622800
use Spiral\JsonSchemaGenerator\GeneratorConfig;
801+
use Spiral\JsonSchemaGenerator\Validation\AttributeConstraintExtractor;
802+
use Spiral\JsonSchemaGenerator\Validation\PhpDocValidationConstraintExtractor;
803+
use Spiral\JsonSchemaGenerator\Validation\CompositePropertyDataExtractor;
623804

624-
// Enable validation constraints (default: true)
805+
// Basic configuration - enable/disable validation constraints
625806
$config = new GeneratorConfig(enableValidationConstraints: true);
626807
$generator = new Generator(config: $config);
627808

628-
// Disable validation constraints for performance
629-
$config = new GeneratorConfig(enableValidationConstraints: false);
630-
$generator = new Generator(config: $config);
809+
// Advanced configuration - custom property data extractors
810+
$compositeExtractor = new CompositePropertyDataExtractor([
811+
new PhpDocValidationConstraintExtractor(),
812+
new AttributeConstraintExtractor(),
813+
]);
814+
815+
$generator = new Generator(propertyDataExtractor: $compositeExtractor);
816+
817+
// Use default extractors (recommended for most cases)
818+
$generator = new Generator(propertyDataExtractor: CompositePropertyDataExtractor::createDefault());
819+
```
820+
821+
#### Property Data Extractors
822+
823+
The generator uses a modular property data extractor system that allows you to customize how validation constraints are extracted from properties:
824+
825+
**Available Extractors:**
826+
827+
- `PhpDocValidationConstraintExtractor` - Extracts constraints from PHPDoc comments
828+
- `AttributeConstraintExtractor` - Extracts constraints from PHP attributes
829+
- `CompositePropertyDataExtractor` - Combines multiple extractors
830+
831+
**Usage Examples:**
832+
833+
```php
834+
// Use only PHPDoc constraints
835+
$generator = new Generator(propertyDataExtractor: new CompositePropertyDataExtractor([
836+
new PhpDocValidationConstraintExtractor(),
837+
]));
838+
839+
// Use only attribute constraints
840+
$generator = new Generator(propertyDataExtractor: new CompositePropertyDataExtractor([
841+
new AttributeConstraintExtractor(),
842+
]));
843+
844+
// Use both (default behavior)
845+
$generator = new Generator(propertyDataExtractor: CompositePropertyDataExtractor::createDefault());
846+
847+
// Disable all validation constraints for performance
848+
$generator = new Generator(propertyDataExtractor: new CompositePropertyDataExtractor([]));
631849
```
632850

633-
### Configuration Options
851+
### Custom Property Data Extractors
634852

635-
- `enableValidationConstraints` (bool, default: true) - Enable/disable PHPDoc validation constraint extraction
853+
You can create custom property data extractors by implementing the `PropertyDataExtractorInterface`:
636854

637-
When `enableValidationConstraints` is disabled, the generator will skip parsing PHPDoc comments for validation rules,
638-
which can improve performance for large schemas where validation constraints are not needed.
855+
```php
856+
use Spiral\JsonSchemaGenerator\Validation\PropertyDataExtractorInterface;
857+
use Spiral\JsonSchemaGenerator\Parser\PropertyInterface;
858+
use Spiral\JsonSchemaGenerator\Schema\Type;
639859

640-
## Integration with Valinor
860+
class CustomConstraintExtractor implements PropertyDataExtractorInterface
861+
{
862+
public function extractValidationRules(PropertyInterface $property, Type $jsonSchemaType): array
863+
{
864+
$rules = [];
865+
866+
// Your custom constraint extraction logic here
867+
// For example, extract constraints from custom attributes or naming conventions
868+
869+
return $rules;
870+
}
871+
}
872+
873+
// Use your custom extractor
874+
$generator = new Generator(
875+
propertyDataExtractor: CompositePropertyDataExtractor::createDefault()
876+
->withExtractor(new CustomConstraintExtractor())
877+
);
641878

642-
The JSON Schema Generator works perfectly with the [Valinor PHP package](https://github.com/CuyZ/Valinor) for complete
643-
data mapping and validation workflows. Valinor can validate incoming data based on the same PHPDoc constraints that the
644-
generator uses to create JSON schemas.
645879

646880
### Installation
647881

src/Attribute/Constraint/Enum.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\JsonSchemaGenerator\Attribute\Constraint;
6+
7+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
8+
final readonly class Enum
9+
{
10+
public function __construct(
11+
public array $values,
12+
) {}
13+
}

src/Attribute/Constraint/Items.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\JsonSchemaGenerator\Attribute\Constraint;
6+
7+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
8+
final readonly class Items
9+
{
10+
public function __construct(
11+
public ?int $min = null,
12+
public ?int $max = null,
13+
public ?bool $unique = null,
14+
) {}
15+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\JsonSchemaGenerator\Attribute\Constraint;
6+
7+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
8+
final readonly class Length
9+
{
10+
public function __construct(
11+
public ?int $min = null,
12+
public ?int $max = null,
13+
) {}
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\JsonSchemaGenerator\Attribute\Constraint;
6+
7+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
8+
final readonly class MultipleOf
9+
{
10+
public function __construct(
11+
public int|float $value,
12+
) {}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\JsonSchemaGenerator\Attribute\Constraint;
6+
7+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
8+
final readonly class Pattern
9+
{
10+
public function __construct(
11+
public string $pattern,
12+
) {}
13+
}

src/Attribute/Constraint/Range.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\JsonSchemaGenerator\Attribute\Constraint;
6+
7+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
8+
final readonly class Range
9+
{
10+
public function __construct(
11+
public int|float|null $min = null,
12+
public int|float|null $max = null,
13+
public ?bool $exclusiveMin = null,
14+
public ?bool $exclusiveMax = null,
15+
) {}
16+
}

0 commit comments

Comments
 (0)