Skip to content

Commit e195837

Browse files
authored
refactor: Add array generics support to converters and address some bugs (#30)
1 parent abb1554 commit e195837

17 files changed

Lines changed: 706 additions & 47 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
- 📝 **Multi-Version Support** - Support for JSON Schema Draft-06, Draft-07, Draft 2019-09, and Draft 2020-12
1313
-**Validation** - Validate data against schemas with detailed error messages
1414
- 🤝 **Conditional Schemas** - Support for if/then/else, allOf, anyOf, and not conditions
15-
- 🔄 **Reflection** - Generate schemas from PHP Classes, Enums and Closures
15+
- 🔄 **Reflection** - Generate schemas from PHP classes, enums, and closures — including docblock array generics and constructor property promotion
1616
- 💪 **Type Safety** - Built with PHP 8.3+ features and strict typing
1717
- 🔍 **Version-Aware Features** - Automatic validation of version-specific features with helpful error messages
1818

docs/json-schema/code-generation/from-classes.mdx

Lines changed: 105 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: 'Generate JSON schemas automatically from PHP class definitions'
44
icon: 'code'
55
---
66

7-
Generate JSON schemas automatically from PHP classes using reflection and docblock analysis. This feature extracts property types, validation rules, and documentation from your existing PHP classes.
7+
Generate JSON schemas automatically from PHP classes using reflection and docblock analysis. This feature extracts property types, defaults, documentation, and array element types from your existing PHP classes.
88

99
## Basic Class Schema Generation
1010

@@ -113,7 +113,7 @@ class Product
113113
public bool $in_stock = true;
114114

115115
/**
116-
* @var array Product tags
116+
* @var string[] Product tags
117117
*/
118118
public array $tags = [];
119119
}
@@ -155,6 +155,9 @@ $schema = Schema::fromClass(Product::class);
155155
"tags": {
156156
"type": "array",
157157
"description": "Product tags",
158+
"items": {
159+
"type": "string"
160+
},
158161
"default": []
159162
}
160163
},
@@ -188,12 +191,65 @@ class ModernUser
188191
// Generate with Draft 2019-09 for deprecated support
189192
$schema = Schema::fromClass(
190193
ModernUser::class,
191-
version: SchemaVersion::Draft_2019_09
194+
schemaVersion: SchemaVersion::Draft_2019_09
192195
);
193196

194197
// The generated schema will include "deprecated": true for old_email
195198
```
196199

200+
## Constructor Property Promotion
201+
202+
Promoted constructor properties are supported. Descriptions and array element types are read from the constructor's `@param` tags when the property itself has no docblock:
203+
204+
```php
205+
/**
206+
* User data transfer object
207+
*
208+
* @param string $name The user's full name
209+
* @param int $age The user's age in years
210+
*/
211+
class UserDto
212+
{
213+
public function __construct(
214+
public string $name,
215+
public int $age = 18,
216+
) {}
217+
}
218+
219+
$schema = Schema::fromClass(UserDto::class);
220+
// name: required string with @param description
221+
// age: optional integer with default 18
222+
```
223+
224+
An explicit `@var` tag on a promoted property takes precedence over the matching `@param` description.
225+
226+
## Array Item Types
227+
228+
When a property is typed as `array`, the converter reads element types from docblock generics and adds an `items` schema. Supported syntax includes:
229+
230+
- `string[]` and `(int|string)[]`
231+
- `array<string>`, `array<int|string>`, and `array<string, int>` (value type is used for key-value maps)
232+
- `list<bool>`, `non-empty-array<string>`, and similar list/array generics
233+
234+
```php
235+
class Article
236+
{
237+
/** @var string[] Article tags */
238+
public array $tags;
239+
240+
/** @var array<int|string> Flexible identifiers */
241+
public array $ids;
242+
}
243+
244+
$schema = Schema::fromClass(Article::class);
245+
// tags: { "type": "array", "items": { "type": "string" } }
246+
// ids: { "type": "array", "items": { "type": ["integer", "string"] } }
247+
```
248+
249+
<Note>
250+
Array item types are limited to JSON Schema scalar types (`string`, `integer`, `number`, `boolean`, `null`, `object`, `array`) and unions of those scalars. Class names (e.g. `DateTime[]`), `mixed`, and nested generics (e.g. `array<array<int>>`) are skipped silently, leaving a plain `array` without `items`.
251+
</Note>
252+
197253
## Complex Property Types
198254

199255
Handle complex property types and collections:
@@ -210,7 +266,7 @@ class Order
210266
public string $id;
211267

212268
/**
213-
* @var array List of order items
269+
* @var array<string> List of order item SKUs
214270
*/
215271
public array $items;
216272

@@ -225,26 +281,51 @@ class Order
225281
public ?object $billing_address = null;
226282

227283
/**
228-
* @var array Additional notes
284+
* @var string[] Additional notes
229285
*/
230286
public array $notes = [];
231287

232288
/**
233-
* @var array Metadata key-value pairs
289+
* @var array<string, string> Metadata key-value pairs
234290
*/
235291
public array $metadata = [];
236292
}
237293

238294
$schema = Schema::fromClass(Order::class);
239-
// Custom class types (like Address) are treated as generic 'object' type
240-
// Generic array syntax (e.g., array<OrderItem>) in docblocks is not parsed
241-
// Arrays are treated as generic arrays without item type information
295+
// items, notes, and metadata include string item schemas
296+
// Custom class types (like Address) require ignoreUnknownTypes or separate schema generation
242297
```
243298

244299
<Note>
245-
Custom class types (non-built-in PHP types) are converted to generic `object` schemas. The converter does not recursively analyze nested classes or parse generic array syntax from docblocks. To create schemas for nested objects, generate them separately and combine them using the fluent API.
300+
Custom class types (non-built-in PHP types) are not mapped to JSON Schema types by default and will throw an `UnknownTypeException`. Use `ignoreUnknownTypes: true` to skip unmappable properties, or generate nested object schemas separately and combine them using the fluent API.
246301
</Note>
247302

303+
## Handling Unknown Types
304+
305+
By default, properties typed as custom classes (e.g. `DateTime`, `Address`) cause conversion to fail. Pass `ignoreUnknownTypes: true` to omit those properties from the generated schema:
306+
307+
```php
308+
class Event
309+
{
310+
public string $title;
311+
312+
public DateTime $starts_at;
313+
314+
public ?DateTime $ends_at = null;
315+
}
316+
317+
// Throws UnknownTypeException for DateTime properties
318+
$schema = Schema::fromClass(Event::class);
319+
320+
// Skips DateTime properties, includes title only
321+
$schema = Schema::fromClass(
322+
Event::class,
323+
ignoreUnknownTypes: true,
324+
);
325+
```
326+
327+
This option is also available on `Schema::from()` when passing a class or object instance.
328+
248329
## Enum Integration
249330

250331
Automatically handle backed enums:
@@ -363,9 +444,14 @@ class CompleteUser extends BaseEntity
363444
}
364445

365446
$schema = Schema::fromClass(CompleteUser::class);
366-
// Schema will include properties from base class and traits
447+
// Schema includes properties from the base class and traits
448+
// Static properties are excluded
367449
```
368450

451+
<Note>
452+
Only **public** properties are included by default. Pass `publicOnly: false` to include protected and private properties. Static properties are always excluded.
453+
</Note>
454+
369455
## Validation and Usage
370456

371457
Use the generated schema to validate data:
@@ -418,13 +504,14 @@ if ($userSchema->isValid($userData)) {
418504

419505
The schema generator automatically extracts the following from your docblocks:
420506

421-
- **Description text** - From the docblock summary and description
422-
- **Property types** - From `@var` annotations (combined with native PHP types)
423-
- **Parameter types** - From `@param` annotations
424-
- **Deprecation status** - Using the `@deprecated` tag
507+
- **Description text** - From the class docblock summary and `@var` property descriptions
508+
- **Promoted property descriptions** - From matching constructor `@param` tags when no `@var` is present
509+
- **Property types** - From native PHP type hints (combined with `@var` where present)
510+
- **Array item types** - From generic array syntax in `@var` or `@param` tags (scalar element types only)
511+
- **Deprecation status** - Using the `@deprecated` tag on classes and properties
425512

426513
<Note>
427-
Validation rules (like minLength, pattern, format) are **not** extracted from docblocks. You need to apply them programmatically using the fluent API after generation, or define them in your actual PHP code using native types and enums.
514+
Validation rules (like minLength, pattern, format) are **not** extracted from docblocks. Apply them programmatically using the fluent API after generation, or rely on native PHP types and backed enums for type validation.
428515
</Note>
429516

430517
## Best Practices
@@ -485,15 +572,15 @@ public string $email;
485572
*/
486573
public string $username;
487574

488-
// Good: Document complex array types
575+
// Good: Document array element types with generics
489576
/**
490577
* @var array<string> List of user roles
491578
*/
492579
public array $roles;
493580
```
494581

495582
<Note>
496-
Validation rules like minLength, pattern, format are not extracted from docblocks. Generic array syntax (e.g., `array<string>`, `array<OrderItem>`) in docblocks is not parsed - arrays are treated as generic arrays without item type information. Apply validation rules and array item schemas programmatically using the fluent API after schema generation, or use native PHP types and enums for type validation.
583+
Validation rules like minLength, pattern, and format are not extracted from docblocks. Array item types are inferred from generic syntax for scalar elements only — nested object or array element types (e.g. `array<OrderItem>`) are not resolved recursively. Apply additional validation programmatically using the fluent API after schema generation.
497584
</Note>
498585

499586
## Common Use Cases

docs/json-schema/code-generation/from-closures.mdx

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Handle optional parameters with default values:
6868
* @param float $price Product price in USD
6969
* @param string $description Product description
7070
* @param bool $active Whether the product is active
71-
* @param array $tags Product tags
71+
* @param string[] $tags Product tags
7272
*/
7373
$createProductClosure = function (
7474
string $name,
@@ -111,6 +111,9 @@ $schema = Schema::fromClosure($createProductClosure);
111111
"tags": {
112112
"type": "array",
113113
"description": "Product tags",
114+
"items": {
115+
"type": "string"
116+
},
114117
"default": []
115118
}
116119
},
@@ -184,6 +187,41 @@ The generated schema will include nullable types:
184187
```
185188
</Accordion>
186189

190+
## Array Item Types
191+
192+
When a parameter is typed as `array`, element types from generic docblock syntax are added as an `items` schema:
193+
194+
```php
195+
/**
196+
* Tag a resource with labels
197+
*
198+
* @param string[] $tags Resource tags
199+
* @param array<int|string> $ids Associated identifiers
200+
*/
201+
$tagResourceClosure = function (array $tags, array $ids): void {};
202+
203+
$schema = Schema::fromClosure($tagResourceClosure);
204+
// tags: { "type": "array", "items": { "type": "string" } }
205+
// ids: { "type": "array", "items": { "type": ["integer", "string"] } }
206+
```
207+
208+
Supported syntax matches class property docblocks: `T[]`, `array<T>`, `list<T>`, `array<K,V>`, and unions of scalar element types. Class names, `mixed`, and nested generics are skipped silently.
209+
210+
## Handling Unknown Types
211+
212+
Parameters typed as custom classes throw an `UnknownTypeException` by default. Pass `ignoreUnknownTypes: true` to skip unmappable parameters:
213+
214+
```php
215+
/**
216+
* @param string $title Event title
217+
* @param DateTime $starts_at Start time
218+
*/
219+
$createEvent = function (string $title, DateTime $starts_at): void {};
220+
221+
$schema = Schema::fromClosure($createEvent, ignoreUnknownTypes: true);
222+
// Only includes title
223+
```
224+
187225
## Advanced Parameter Documentation
188226

189227
Use detailed docblock annotations for validation rules:
@@ -261,7 +299,7 @@ Real-world example for API endpoint validation:
261299
* @param ?string $email Email filter
262300
* @param ?int $age_min Minimum age filter
263301
* @param ?int $age_max Maximum age filter
264-
* @param array $roles Role filter
302+
* @param array<string> $roles Role filter
265303
* @param int $page Page number for pagination
266304
* @param int $per_page Items per page
267305
* @param string $sort_by Sort field
@@ -319,7 +357,7 @@ $modernApiClosure = function (
319357
// Generate with Draft 2019-09 for deprecated support
320358
$modernSchema = Schema::fromClosure(
321359
$modernApiClosure,
322-
version: SchemaVersion::Draft_2019_09
360+
schemaVersion: SchemaVersion::Draft_2019_09
323361
);
324362

325363
// Apply validation rules programmatically after generation
@@ -429,7 +467,7 @@ function processData($name, $age, $tags, $date) {}
429467
```
430468

431469
<Note>
432-
Validation rules like format, minLength, pattern are not extracted from docblocks. Generic array syntax (e.g., `array<string>`, `array<OrderItem>`) in docblocks is not parsed - arrays are treated as generic arrays without item type information. Only parameter types and descriptions are extracted. Apply validation rules and array item schemas programmatically using the fluent API after schema generation.
470+
Validation rules like format, minLength, and pattern are not extracted from docblocks. Array item types are inferred from generic syntax for scalar elements only. Apply additional validation programmatically using the fluent API after schema generation.
433471
</Note>
434472

435473
### 4. Design Functions for Schema Generation
@@ -451,7 +489,7 @@ function handleUserStuff($data, $action, $options = []) {}
451489
| `int` | `integer` | Integer numbers |
452490
| `float` | `number` | Floating-point numbers |
453491
| `bool` | `boolean` | Boolean true/false |
454-
| `array` | `array` | Generic arrays |
492+
| `array` | `array` | Generic arrays; `items` added when docblock specifies element types |
455493
| `?string` | `["string", "null"]` | Nullable string |
456494
| `mixed` | No type constraint | Accepts any type |
457495
| `object` | `object` | Generic object |

docs/json-schema/introduction.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ All schema types support common properties like title, description, examples, de
147147

148148
### Code Generation
149149
Generate schemas from existing PHP code:
150-
- **PHP Classes** - Extract schemas from class properties and docblocks
151-
- **Closures** - Generate from function signatures and parameter types
150+
- **PHP Classes** - Extract schemas from class properties, constructor promotion, and docblocks (including array item types)
151+
- **Closures** - Generate from function signatures, parameter types, and docblock generics
152152
- **Backed Enums** - Create enum validation schemas
153153
- **JSON Import** - Convert existing JSON Schema definitions
154154

phpunit.dist.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@
99
<directory suffix="Test.php">./tests</directory>
1010
</testsuite>
1111
</testsuites>
12-
<coverage>
13-
<report>
14-
<html outputDirectory="coverage"/>
15-
</report>
16-
</coverage>
1712
<source>
1813
<include>
1914
<directory suffix=".php">./src</directory>

0 commit comments

Comments
 (0)