From 81547b7b2b3fdda494ef023a16ef0890b4a9d4b2 Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 23 Mar 2026 23:27:31 +0100 Subject: [PATCH] Improve docs coverage and accuracy --- README.md | 5 +- docs/.vitepress/config.ts | 3 + docs/guide/circular-dependencies.md | 118 ++++++++++++ docs/guide/examples.md | 9 +- docs/guide/framework-integration.md | 45 +++-- docs/guide/index.md | 7 +- docs/guide/runtime-api.md | 267 ++++++++++++++++++++++++++++ docs/guide/validation.md | 13 +- docs/reference/cli.md | 2 + docs/reference/importer.md | 4 +- docs/reference/jsonschema.md | 209 ++++++++++++++++++++++ docs/reference/typescript.md | 32 ++++ 12 files changed, 690 insertions(+), 24 deletions(-) create mode 100644 docs/guide/circular-dependencies.md create mode 100644 docs/guide/runtime-api.md create mode 100644 docs/reference/jsonschema.md diff --git a/README.md b/README.md index e3d836f..ffc4bbd 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ return Schema::create() Field::string('content'), Field::dto('author', 'Author')->required(), Field::collection('tags', 'Tag')->singular('tag'), - Field::bool('published')->defaultValue(false), + Field::bool('published')->default(false), Field::string('publishedAt'), )) ->dto(Dto::immutable('Author')->fields( @@ -201,8 +201,11 @@ Full documentation available at **[php-collective.github.io/dto](https://php-col - [Getting Started](https://php-collective.github.io/dto/guide/) - Quick start guide with examples - [Configuration Builder](https://php-collective.github.io/dto/guide/config-builder) - Fluent API for defining DTOs +- [Runtime API](https://php-collective.github.io/dto/guide/runtime-api) - Core DTO methods and global runtime options - [Examples](https://php-collective.github.io/dto/guide/examples) - Practical usage patterns +- [Circular Dependencies](https://php-collective.github.io/dto/guide/circular-dependencies) - How generation-time cycle detection works - [TypeScript Generation](https://php-collective.github.io/dto/reference/typescript) - Generate TypeScript interfaces +- [JSON Schema Generation](https://php-collective.github.io/dto/reference/jsonschema) - Generate JSON Schema for DTO definitions - [Schema Importer](https://php-collective.github.io/dto/reference/importer) - Bootstrap DTOs from JSON data/schema - [Performance](https://php-collective.github.io/dto/guide/performance) - Benchmarks and optimization tips diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 3ca04f9..db997d2 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -54,6 +54,7 @@ export default defineConfig({ text: 'Configuration', items: [ { text: 'Config Builder', link: '/guide/config-builder' }, + { text: 'Runtime API', link: '/guide/runtime-api' }, { text: 'Validation', link: '/guide/validation' }, { text: 'Advanced Types', link: '/guide/advanced-types' }, { text: 'Shaped Arrays', link: '/guide/shaped-arrays' }, @@ -65,6 +66,7 @@ export default defineConfig({ text: 'Advanced', items: [ { text: 'Advanced Patterns', link: '/guide/advanced-patterns' }, + { text: 'Circular Dependencies', link: '/guide/circular-dependencies' }, { text: 'Framework Integration', link: '/guide/framework-integration' }, { text: 'Separating Generated Code', link: '/guide/separating-generated-code' }, { text: 'Design Decisions', link: '/guide/design-decisions' }, @@ -86,6 +88,7 @@ export default defineConfig({ items: [ { text: 'CLI Reference', link: '/reference/cli' }, { text: 'TypeScript Generation', link: '/reference/typescript' }, + { text: 'JSON Schema Generation', link: '/reference/jsonschema' }, { text: 'Schema Importer', link: '/reference/importer' }, ] } diff --git a/docs/guide/circular-dependencies.md b/docs/guide/circular-dependencies.md new file mode 100644 index 0000000..82f591d --- /dev/null +++ b/docs/guide/circular-dependencies.md @@ -0,0 +1,118 @@ +--- +title: Circular Dependencies +--- + +# Circular Dependencies + +The generator analyzes DTO relationships before rendering classes and throws early when it finds an eager dependency cycle. + +This is a generation-time check, not a runtime hydration check. + +## When Cycle Detection Runs + +Cycle detection happens during definition building, before DTO files are written. + +That means a cycle fails generation even if you have not instantiated the DTOs yet. + +## What Counts as a Dependency + +The dependency analyzer currently looks at: + +- DTO field types such as `User` +- array and collection element types such as `User[]` +- nullable and namespaced types such as `?User[]` or `\App\Dto\UserDto` +- union and intersection members such as `Foo|Bar`, `Foo&Bar`, and parenthesized DNF shapes +- collection `singularType` +- explicit `dto` field metadata +- DTO inheritance via `extends` + +## What Breaks a Cycle + +Lazy fields are excluded from the dependency graph: + +```php +Dto::create('User')->fields( + Field::dto('manager', 'User')->asLazy(), +) +``` + +This lets you model recursive graphs without blocking generation. + +### Important + +Nullable fields alone do **not** currently remove a dependency from the analyzer. If you need to break a generation-time cycle, use `lazy`. + +## Basic Example + +### Eager Cycle That Fails + +```php +return Schema::create() + ->dto(Dto::create('User')->fields( + Field::dto('team', 'Team'), + )) + ->dto(Dto::create('Team')->fields( + Field::dto('owner', 'User'), + )) + ->toArray(); +``` + +Generation fails because `User -> Team -> User` is an eager cycle. + +### Lazy Edge That Passes + +```php +return Schema::create() + ->dto(Dto::create('User')->fields( + Field::dto('team', 'Team')->asLazy(), + )) + ->dto(Dto::create('Team')->fields( + Field::dto('owner', 'User'), + )) + ->toArray(); +``` + +This works because the lazy `team` field is skipped during cycle analysis. + +## Collections and Advanced Types + +Cycle detection also applies to: + +- collections using `singularType` +- unions such as `User|Team` +- intersections such as `Foo&Bar` +- parenthesized DNF shapes such as `(Foo|Bar)&Baz` + +If any eager branch introduces a cycle, generation fails. Making the field lazy skips the whole field from the analyzer. + +## Self-References + +Direct self-references do not count as a cycle in the analyzer: + +```php +Dto::create('Category')->fields( + Field::dto('parent', 'Category'), +) +``` + +This is allowed by the generator. In practice, recursive structures are still usually better modeled as lazy fields when large subtrees are involved. + +## Inheritance + +`extends` relationships are part of the dependency graph. A DTO that extends another DTO depends on that parent during analysis. + +## Troubleshooting + +When generation fails, the exception message shows the discovered cycle path. + +Typical fixes: + +1. Mark one edge in the cycle as lazy. +2. Reconsider whether two DTOs need to reference each other directly. +3. Move one side of the relationship to an identifier field instead of a nested DTO. + +## Related Guides + +- [Config Builder](./config-builder) for `asLazy()` +- [Performance](./performance) for lazy hydration tradeoffs +- [Troubleshooting](./troubleshooting) for generator error messages diff --git a/docs/guide/examples.md b/docs/guide/examples.md index 1e25a63..8719115 100644 --- a/docs/guide/examples.md +++ b/docs/guide/examples.md @@ -649,7 +649,7 @@ $profile = new ProfileDto(['bio' => null, 'followers' => null]); // OK // Non-null values are validated $profile = new ProfileDto(['bio' => str_repeat('x', 501)]); -// Exception: Field 'bio' must be at most 500 characters +// Exception: Validation failed in App\Dto\ProfileDto: bio must be at most 500 characters ``` ### Extracting Validation Rules @@ -667,8 +667,11 @@ $rules = $dto->validationRules(); // 'age' => ['min' => 0, 'max' => 150], // ] -// Use with framework validators -$validator = new FrameworkValidator($rules); +// Map to framework-native validation rules or constraints +$frameworkRules = [ + 'username' => ['required', 'string', 'min:' . $rules['username']['minLength']], + 'email' => ['required', 'email'], +]; ``` ## Lazy Loading diff --git a/docs/guide/framework-integration.md b/docs/guide/framework-integration.md index 9d93a4e..2c4d968 100644 --- a/docs/guide/framework-integration.md +++ b/docs/guide/framework-integration.md @@ -6,10 +6,10 @@ title: Framework Integration This library is **framework-agnostic** by design. It works with any PHP framework without requiring wrapper packages, while still offering deep integration possibilities. -Do We Need Wrapper Libraries? +## Do We Need Wrapper Libraries? | Feature | Without Wrapper | With Wrapper | - |--------------------|--------------------|-----------------------------| +|--------------------|--------------------|-----------------------------| | Basic usage | ✅ Works | ✅ Works | | Collection factory | 1 line setup | Auto-configured | | Code generation | bin/dto generate | artisan dto:generate | @@ -193,18 +193,39 @@ class UsersController extends AppController ## Validation -The library doesn't include validation - use your framework's validator. +The library includes built-in DTO validation for: + +- required fields +- PHP-native type enforcement +- common field rules such as `minLength`, `maxLength`, `min`, `max`, and `pattern` + +Framework validators still matter for request validation, business rules, custom messages, and cross-field constraints. A practical setup is: + +1. Let the DTO handle structural validation and type-safe hydration. +2. Use your framework validator for request-specific or domain-specific rules. +3. Optionally map `validationRules()` into framework-native rules if you want to reuse DTO metadata. ### Laravel ```php use Illuminate\Support\Facades\Validator; -$dto = new UserDto($request->all(), ignoreMissing: true); +$dto = UserDto::createFromArray($request->all(), ignoreMissing: true); + +$dtoRules = $dto->validationRules(); $validator = Validator::make($dto->toArray(), [ - 'name' => 'required|string|max:255', - 'email' => 'required|email|unique:users', + 'name' => [ + 'required', + 'string', + 'min:' . ($dtoRules['name']['minLength'] ?? 0), + 'max:' . ($dtoRules['name']['maxLength'] ?? 255), + ], + 'email' => [ + 'required', + 'email', + 'unique:users', + ], ]); if ($validator->fails()) { @@ -212,9 +233,12 @@ if ($validator->fails()) { } ``` +`validationRules()` returns framework-agnostic metadata such as `minLength` and `pattern`. Laravel does not consume that structure directly, so map it into Laravel rule strings or objects first. + ### Symfony ```php +use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Validator\ValidatorInterface; class UserController extends AbstractController @@ -223,7 +247,8 @@ class UserController extends AbstractController { $dto = new UserDto(json_decode($request->getContent(), true)); - // Validate the DTO array representation + // DTO construction already applies built-in field validation. + // Use Symfony Validator for richer request or business rules. $violations = $validator->validate($dto->toArray(), new Assert\Collection([ 'name' => [new Assert\NotBlank(), new Assert\Length(['max' => 255])], 'email' => [new Assert\NotBlank(), new Assert\Email()], @@ -246,10 +271,10 @@ The CLI tool works the same way in any framework: ```bash # Generate DTOs from config -bin/dto generate config/dto.php src/Dto App\\Dto +vendor/bin/dto generate --config-path=config/ --src-path=src/ --namespace=App # Or with XML -bin/dto generate config/dto.xml src/Dto App\\Dto +vendor/bin/dto generate --config-path=config/ --src-path=src/ --namespace=App --format=xml ``` ### Framework-Specific Paths @@ -267,7 +292,7 @@ Add to your `composer.json` for convenience: ```json { "scripts": { - "dto:generate": "bin/dto generate config/dto.php src/Dto App\\Dto" + "dto:generate": "vendor/bin/dto generate --config-path=config/ --src-path=src/ --namespace=App" } } ``` diff --git a/docs/guide/index.md b/docs/guide/index.md index 7233695..94f6952 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -29,7 +29,7 @@ Create `config/dto.xml`: ```xml - + @@ -87,7 +87,7 @@ Choose your preferred format—all generate identical DTOs: ```xml [XML] - + @@ -271,5 +271,8 @@ parameters: - [Examples](./examples) — Real-world usage patterns - [Config Builder](./config-builder) — Fluent PHP API reference +- [Runtime API](./runtime-api) — Core DTO methods, serialization, and global options - [Validation](./validation) — Field constraints and rules +- [Circular Dependencies](./circular-dependencies) — How to model recursive DTO graphs safely - [TypeScript Generation](../reference/typescript) — Frontend type sync +- [JSON Schema Generation](../reference/jsonschema) — Export JSON Schema from DTO definitions diff --git a/docs/guide/runtime-api.md b/docs/guide/runtime-api.md new file mode 100644 index 0000000..ace3c49 --- /dev/null +++ b/docs/guide/runtime-api.md @@ -0,0 +1,267 @@ +--- +title: Runtime API +--- + +# Runtime API + +This guide collects the core runtime methods exposed by generated DTOs and the shared base `Dto` class. + +Most day-to-day usage happens through generated getters, setters, and `with*()` methods, but the base API is useful when you need dynamic access, key-type conversion, or framework-level helpers. + +## Construction + +### Constructor + +```php +$dto = new UserDto($data, ignoreMissing: false, type: null); +``` + +- `data`: initial array payload +- `ignoreMissing`: ignore unknown input keys instead of throwing +- `type`: input key style such as underscored or dashed + +### `create()` + +Convenience wrapper around `new`: + +```php +$dto = UserDto::create(['name' => 'Jane']); +``` + +### `createFromArray()` + +Generated DTOs expose a typed static constructor: + +```php +$dto = UserDto::createFromArray([ + 'id' => 1, + 'email' => 'jane@example.com', +]); +``` + +This is usually the most readable entry point when you want shaped-array type information in static analysis. + +### `fromArray()` + +Mutable DTOs also expose an instance-level hydrator: + +```php +$dto = new UserDto(); +$dto->fromArray(['name' => 'Jane']); +``` + +This mutates the current instance. Immutable DTOs use `create()`, `createFromArray()`, or generated `with*()` methods instead. + +### `fromUnserialized()` + +Create a DTO from a JSON string produced by `serialize()`: + +```php +$dto = UserDto::fromUnserialized($json); +``` + +Mutable DTOs also expose an instance-level `unserialize()` method when you want to rehydrate an existing object in place. + +## Reading Data + +### Generated Getters + +Generated DTOs expose typed getters such as: + +```php +$dto->getName(); +$dto->getAddress(); +$dto->getAddressOrFail(); +``` + +### Dynamic `get()` and `has()` + +Use these when the field name is only known at runtime: + +```php +$value = $dto->get('name'); +$present = $dto->has('name'); +``` + +Both methods also accept an optional key type. + +`has()` checks whether the field currently has a value, not whether the field exists in the DTO definition. + +### `read()` + +Safely traverse nested DTOs, arrays, and `ArrayAccess`-backed structures: + +```php +$city = $order->read(['customer', 'address', 'city']); +$firstEmail = $company->read(['departments', 0, 'members', 0, 'email'], 'unknown'); +``` + +Path segments can be field names or collection indexes. When any segment is missing, `read()` returns the provided default value. + +### `fields()` and `touchedFields()` + +```php +$allFields = $dto->fields(); +$changedFields = $dto->touchedFields(); +``` + +- `fields()` returns all DTO field names +- `touchedFields()` returns only fields that were set or mutated + +### `validationRules()` + +Returns built-in validation metadata in a framework-agnostic format: + +```php +$rules = $dto->validationRules(); +// ['email' => ['pattern' => '/.../'], 'age' => ['min' => 0]] +``` + +## Serialization + +### `toArray()` + +Convert the DTO to an array: + +```php +$data = $dto->toArray(); +$snake = $dto->toArray(UserDto::TYPE_UNDERSCORED); +$subset = $dto->toArray(fields: ['id', 'email']); +``` + +The optional arguments are: + +- `type`: output key style +- `fields`: only serialize a subset of fields +- `touched`: internal flag used by `touchedToArray()` + +### `touchedToArray()` + +Serialize only fields that were touched: + +```php +$changes = $dto->touchedToArray(); +``` + +This is useful for PATCH payloads, change tracking, and emitting only modified state. + +### `serialize()` and `__toString()` + +`serialize()` returns a JSON string of touched fields: + +```php +$json = $dto->serialize(); +echo $dto; // same touched-field JSON representation +``` + +This is different from PHP's native `serialize($dto)`. + +### Native `serialize()` / `unserialize()` + +DTOs implement `__serialize()` and `__unserialize()` for PHP's native serialization: + +```php +$serialized = serialize($dto); +$restored = unserialize($serialized); +``` + +Native serialization also works on touched fields, not the full DTO state. + +### `jsonSerialize()` + +DTOs implement `JsonSerializable`, so `json_encode($dto)` uses `toArray()`. + +## Mutation + +### Mutable DTOs + +Mutable DTOs support generated setters and dynamic `set()`: + +```php +$dto->setName('Jane'); +$dto->set('name', 'Jane'); +``` + +### Immutable DTOs + +Immutable DTOs support generated `with*()` methods and dynamic `with()`: + +```php +$updated = $dto->withEmail('new@example.com'); +$updated = $dto->with('email', 'new@example.com'); +``` + +These return a new instance. + +### `clone()` + +Creates a deep clone of the DTO, including nested DTOs, arrays, and collections: + +```php +$copy = $dto->clone(); +``` + +Lazy field payloads are preserved in the clone as well. + +## Key Types + +The base `Dto` class supports multiple key styles for input and output: + +- `TYPE_DEFAULT` +- `TYPE_CAMEL` +- `TYPE_UNDERSCORED` +- `TYPE_DASHED` + +Examples: + +```php +$dto->fromArray($request->getData(), false, UserDto::TYPE_UNDERSCORED); +$query = $dto->toArray(UserDto::TYPE_DASHED); +``` + +### Global Default Key Type + +You can set a global default for all DTOs: + +```php +use PhpCollective\Dto\Dto\Dto; + +Dto::setDefaultKeyType(Dto::TYPE_UNDERSCORED); +``` + +This affects calls where no explicit key type is passed. + +## Collections + +### `setCollectionFactory()` + +Override collection instantiation globally: + +```php +use PhpCollective\Dto\Dto\Dto; + +Dto::setCollectionFactory(fn (array $items) => collect($items)); +``` + +This is useful for framework-native collection classes in Laravel, CakePHP, or Symfony integrations. + +### Resetting Global Runtime State + +Because collection factories and default key types are static global settings, tests should reset them explicitly: + +```php +Dto::setCollectionFactory(null); +Dto::setDefaultKeyType(null); +``` + +## Runtime Exceptions Worth Knowing + +Common runtime failures include: + +- missing required fields +- unknown dynamic field access in `get()`, `set()`, or `has()` +- invalid regex patterns in built-in validation rules +- incompatible factory return types +- unknown fields during native `unserialize()` + +See [Troubleshooting](./troubleshooting) for the full exception guide. diff --git a/docs/guide/validation.md b/docs/guide/validation.md index a1cd2cb..18194bc 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -124,11 +124,12 @@ use Illuminate\Translation\Translator; $loader = new ArrayLoader(); $translator = new Translator($loader, 'en'); $factory = new Factory($translator); +$dtoRules = $userDto->validationRules(); $validator = $factory->make($userDto->toArray(), [ 'email' => 'required|email', 'age' => 'required|integer|min:18|max:120', - 'name' => 'required|string|min:2|max:100', + 'name' => 'required|string|min:' . ($dtoRules['name']['minLength'] ?? 2) . '|max:' . ($dtoRules['name']['maxLength'] ?? 100), ]); if ($validator->fails()) { @@ -136,6 +137,8 @@ if ($validator->fails()) { } ``` +`validationRules()` returns framework-agnostic metadata, not Laravel-ready rule strings. Read from it and translate it into Laravel's rule format when you want to reuse DTO constraints. + ### Respect/Validation ```php @@ -258,10 +261,6 @@ $rules = $dto->validationRules(); // ] ``` -Only fields with at least one active rule are included. The returned keys match the config rule names: `required`, `minLength`, `maxLength`, `min`, `max`, `pattern`. - -The framework integration plugins provide ready-made bridges: +The returned structure is intentionally simple. Frameworks like Laravel or Symfony still need a small adapter layer if you want to turn this metadata into native validator rules or constraints. -- **CakePHP:** `DtoValidator::fromDto($dto)` → `Cake\Validation\Validator` -- **Laravel:** `DtoValidationRules::fromDto($dto)` → Laravel rule arrays -- **Symfony:** `DtoConstraintBuilder::fromDto($dto)` → `Symfony\Component\Validator\Constraints\Collection` +Only fields with at least one active rule are included. The returned keys match the config rule names: `required`, `minLength`, `maxLength`, `min`, `max`, `pattern`. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 752d336..b652e16 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -44,6 +44,8 @@ Generate JSON Schema from configuration files. vendor/bin/dto jsonschema [options] ``` +See [JSON Schema Generation](/reference/jsonschema) for schema structure, type mapping, and programmatic options. + ## Common Options Options available for all commands: diff --git a/docs/reference/importer.md b/docs/reference/importer.md index b23fe72..432a5c0 100644 --- a/docs/reference/importer.md +++ b/docs/reference/importer.md @@ -227,7 +227,7 @@ $importer->import($json, ['format' => 'xml']); ```xml - + @@ -235,6 +235,8 @@ $importer->import($json, ['format' => 'xml']); ``` +The XML importer currently emits the historical `cakephp-dto` namespace. This reflects the current implementation output. + ### YAML ```php diff --git a/docs/reference/jsonschema.md b/docs/reference/jsonschema.md new file mode 100644 index 0000000..6c7a3a2 --- /dev/null +++ b/docs/reference/jsonschema.md @@ -0,0 +1,209 @@ +--- +title: JSON Schema Generation +--- + +# JSON Schema Generation + +Generate JSON Schema directly from your DTO configuration files. + +This exporter is useful for API contracts, consumer documentation, and validation layers that need a schema artifact without reflecting over runtime DTO classes. + +## Quick Start + +```bash +# Single file output with $defs +vendor/bin/dto jsonschema --output=schemas/ + +# Multi-file output with external $ref values +vendor/bin/dto jsonschema --multi-file --output=schemas/ +``` + +## CLI Options + +``` +JSON Schema Options: + --output=PATH Path for JSON Schema output (default: schemas/) + --single-file Generate all schemas in one file with $defs (default) + --multi-file Generate each schema in separate file + --no-refs Inline nested DTOs instead of using $ref + --date-format=FMT Date format: date-time, date, string (default: date-time) +``` + +See [CLI Reference](/reference/cli) for the full command overview and shared options. + +## Output Modes + +### Single File Output + +```bash +vendor/bin/dto jsonschema --output=schemas/ +``` + +Creates `schemas/dto-schemas.json` with a shared `$defs` section: + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dto-schemas.json", + "title": "DTO Schemas", + "$defs": { + "UserDto": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "email": { "type": "string" } + }, + "required": ["id"], + "additionalProperties": false + } + } +} +``` + +Nested DTOs use in-document references such as `#/$defs/UserDto`. + +### Multi-File Output + +```bash +vendor/bin/dto jsonschema --multi-file --output=schemas/ +``` + +Creates one file per DTO: + +- `schemas/UserDto.json` +- `schemas/OrderDto.json` + +Nested DTOs use external `$ref` values such as `UserDto.json`. + +### Inline Nested DTOs + +```bash +vendor/bin/dto jsonschema --no-refs --output=schemas/ +``` + +When `--no-refs` is used, nested DTOs are expanded inline instead of emitted as `$ref`. + +## Schema Characteristics + +The generated schemas currently have these traits: + +- Draft 2020-12 schema format by default +- `additionalProperties: false` on generated object schemas +- `required` only for fields marked as required in the DTO definition +- Nullable fields represented via `oneOf` with `{"type":"null"}` +- Union types represented via `oneOf` +- `mixed` represented as an empty schema (`{}`) + +## Type Mapping + +| DTO Type | JSON Schema | +|---------|-------------| +| `int`, `integer` | `{"type":"integer"}` | +| `float`, `double` | `{"type":"number"}` | +| `string` | `{"type":"string"}` | +| `bool`, `boolean` | `{"type":"boolean"}` | +| `array` | `{"type":"array"}` | +| `mixed` | `{}` | +| `object` | `{"type":"object"}` | +| `string[]` | `{"type":"array","items":{"type":"string"}}` | +| `int[]` | `{"type":"array","items":{"type":"integer"}}` | +| Other DTO | `$ref` or inline object schema | +| `DateTime*` | `{"type":"string","format":"date-time"}` by default | + +Unknown non-DTO class types currently fall back to a generic object schema. + +## Date Formats + +Date-like PHP types (`DateTime`, `DateTimeImmutable`, `DateTimeInterface`) are emitted as JSON strings with a configurable format: + +```bash +vendor/bin/dto jsonschema --date-format=date +vendor/bin/dto jsonschema --date-format=string +``` + +- `date-time`: RFC 3339 style timestamp string +- `date`: calendar date string +- `string`: plain string without a format hint + +## Unions and Nullability + +### Union Types + +```php +Field::union('value', 'int', 'string')->required() +``` + +Becomes: + +```json +{ + "oneOf": [ + { "type": "integer" }, + { "type": "string" } + ] +} +``` + +### Nullable Fields + +Optional nullable fields are wrapped in `oneOf` with `null`: + +```json +{ + "oneOf": [ + { "type": "string" }, + { "type": "null" } + ] +} +``` + +## Programmatic Usage + +```php +use PhpCollective\Dto\Engine\XmlEngine; +use PhpCollective\Dto\Generator\ArrayConfig; +use PhpCollective\Dto\Generator\Builder; +use PhpCollective\Dto\Generator\ConsoleIo; +use PhpCollective\Dto\Generator\JsonSchemaGenerator; + +$config = new ArrayConfig(['namespace' => 'App']); +$engine = new XmlEngine(); +$builder = new Builder($engine, $config); +$io = new ConsoleIo(); + +$definitions = $builder->build('config/', []); + +$generator = new JsonSchemaGenerator($io, [ + 'singleFile' => false, + 'schemaVersion' => 'https://json-schema.org/draft/2020-12/schema', + 'suffix' => 'Dto', + 'dateFormat' => 'date-time', + 'useRefs' => true, +]); + +$generator->generate($definitions, 'schemas/'); +``` + +### Programmatic Options + +| Option | Default | Description | +|--------|---------|-------------| +| `singleFile` | `true` | Generate one `dto-schemas.json` file with `$defs` | +| `schemaVersion` | Draft 2020-12 URL | Value for the top-level `$schema` keyword | +| `suffix` | `'Dto'` | Custom suffix for generated schema names | +| `dateFormat` | `'date-time'` | Format for date-like PHP types: `date-time`, `date`, `string` | +| `useRefs` | `true` | Emit `$ref` for nested DTOs instead of inlining | + +The CLI currently exposes `singleFile`, `useRefs` via `--no-refs`, and `dateFormat`. + +## CI/CD Integration + +```yaml +- name: Generate JSON Schema + run: vendor/bin/dto jsonschema --output=schemas/ + +- name: Validate generated artifacts + run: test -f schemas/dto-schemas.json +``` + +The generator fails with a descriptive error if it cannot create the output directory or write a schema file. diff --git a/docs/reference/typescript.md b/docs/reference/typescript.md index 7368984..20ca8ca 100644 --- a/docs/reference/typescript.md +++ b/docs/reference/typescript.md @@ -28,6 +28,8 @@ TypeScript Options: --file-case=CASE File naming: pascal, dashed, snake (default: pascal) ``` +See [CLI Reference](/reference/cli) for the full command overview and shared options. + ## Examples ### Single File Output (Default) @@ -126,6 +128,20 @@ With `--strict-nulls`: email: string | null; // explicit null union ``` +### Union Types and Imports + +PHP unions are converted to TypeScript unions: + +```php +Field::union('result', 'User', 'Address')->required() +``` + +```typescript +result: UserDto | AddressDto; +``` + +In multi-file mode, referenced DTOs inside unions get the necessary `import type` statements automatically. + ## Type Mapping | PHP Type | TypeScript Type | @@ -208,3 +224,19 @@ $tsGenerator = new TypeScriptGenerator($io, [ $tsGenerator->generate($definitions, 'frontend/src/types/'); ``` + +### Programmatic Options + +`TypeScriptGenerator` supports a few advanced options beyond the CLI flags: + +| Option | Default | Description | +|--------|---------|-------------| +| `singleFile` | `true` | Generate `dto.ts` instead of per-type files | +| `fileNameCase` | `'pascal'` | File naming for multi-file mode: `pascal`, `dashed`, `snake` | +| `readonly` | `false` | Mark all fields readonly | +| `strictNulls` | `false` | Emit `| null` instead of optional `?` syntax | +| `exportStyle` | `'interface'` | Emit `interface` or `type` declarations | +| `dateType` | `'string'` | Emit date-like PHP types as `string` or `Date` | +| `suffix` | `'Dto'` | Custom suffix for generated interface names | + +The CLI currently exposes `singleFile`, `readonly`, `strictNulls`, and `fileNameCase`. The remaining options are available through programmatic usage.