Skip to content

Commit a742e56

Browse files
committed
Merge remote-tracking branch 'refs/remotes/origin/drafts' into attributes
2 parents c010bb9 + 0d6e126 commit a742e56

111 files changed

Lines changed: 3959 additions & 2941 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.local.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
"Bash(composer show:*)",
3232
"Bash(php -r \":*)",
3333
"Bash(gh pr:*)",
34-
"Bash(xargs wc:*)"
34+
"Bash(xargs wc:*)",
35+
"Bash(tee /tmp/phpunit-output.txt)",
36+
"Read(//tmp/**)"
3537
]
3638
}
3739
}

CLAUDE.md

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ user to resolve it.
99

1010
Rules:
1111

12-
- Ask all foreseeable clarifying questions upfront in a single batch before work begins.
12+
- When there are multiple clarifying questions to ask, ask them **one at a time**, in order of
13+
dependency (earlier answers may resolve later questions). Wait for the answer before asking the
14+
next question. This allows the user to discuss each point in depth without being overwhelmed by
15+
a wall of questions.
1316
- If new ambiguities emerge during execution that were not foreseeable upfront, pause and ask
1417
follow-up questions before proceeding past that decision point.
1518
- For high-stakes decisions (architecture, scope, data model, API shape, behaviour changes) always
@@ -19,6 +22,9 @@ Rules:
1922
visible so the user can correct it.
2023
- There must be no silent interpretation or interpolation of under-specified tasks. If something is
2124
unclear, ask. Do not guess and proceed.
25+
- For multi-phase implementations, **never start the next phase without an explicit go-ahead from
26+
the user**. After completing a phase, summarise what was done and wait for confirmation before
27+
proceeding.
2228

2329
When generating a new CLAUDE.md for a repository, include this clarification policy verbatim as a
2430
preamble before all other content.
@@ -48,6 +54,20 @@ composer update
4854

4955
Tests write generated PHP classes to `sys_get_temp_dir()/PHPModelGeneratorTest/Models/` and dump failed classes to `./failed-classes/` (auto-cleaned on bootstrap).
5056

57+
### Running the full test suite
58+
59+
When running the full test suite, always save output to a file so the complete
60+
output is available for analysis without re-running. Use `--display-warnings` to capture warning
61+
details and `--no-coverage` to skip slow coverage collection:
62+
63+
```bash
64+
php -d memory_limit=128M ./vendor/bin/phpunit --no-coverage --display-warnings 2>&1 | sed 's/\x1b\[[0-9;]*m//g' > /tmp/phpunit-output.txt; tail -5 /tmp/phpunit-output.txt
65+
```
66+
67+
Then analyse with: `grep -E "FAIL|ERROR|WARN|Tests:" /tmp/phpunit-output.txt`
68+
69+
After analysis is complete, delete the file: `rm /tmp/phpunit-output.txt`
70+
5171
## Architecture
5272

5373
This library generates PHP model classes from JSON Schema files. The process is a 4-step pipeline:
@@ -72,15 +92,25 @@ This library generates PHP model classes from JSON Schema files. The process is
7292
### Schema Processing
7393

7494
`SchemaProcessor` (`src/SchemaProcessor/SchemaProcessor.php`) orchestrates property parsing:
75-
- Uses `PropertyProcessorFactory` to instantiate the correct processor by JSON type (String, Integer, Number, Boolean, Array, Object, Null, Const, Any, Reference)
76-
- Convention: processor class name is `PHPModelGenerator\PropertyProcessor\Property\{Type}Processor`
95+
- Uses `PropertyFactory` (`src/PropertyProcessor/PropertyFactory.php`) to create and configure each property
96+
- `PropertyFactory` resolves `$ref` references, delegates `object` types to `processSchema`, and for all other types constructs a `Property` directly and applies Draft modifiers
7797
- `ComposedValueProcessorFactory` handles `allOf`, `anyOf`, `oneOf`, `if/then/else`, `not`
7898
- `SchemaDefinitionDictionary` tracks `$ref` definitions to avoid duplicate processing
7999

100+
### Draft System (`src/Draft/`)
101+
102+
The Draft system defines per-type modifier and validator registrations:
103+
- **`DraftInterface`** / **`DraftBuilder`** / **`Draft`** — Draft definition, builder, and built (immutable) registry
104+
- **`Draft_07.php`** — The JSON Schema Draft 7 definition; registers all types, modifiers, and validator factories
105+
- **`Element/Type`** — One entry per JSON Schema type; holds an ordered list of `ModifierInterface` instances
106+
- **`Modifier/`**`TypeCheckModifier`, `ConstModifier`, `NumberModifier`, `NullModifier`, `ObjectType/ObjectModifier`, `DefaultValueModifier`, `DefaultArrayToEmptyArrayModifier`; each implements `ModifierInterface::modify()`
107+
- **`Model/Validator/Factory/`**`AbstractValidatorFactory` subclasses keyed to schema keywords (e.g. `MinLengthPropertyValidatorFactory` for `minLength`); run as modifiers when a matching key exists in the schema
108+
109+
`PropertyFactory::applyDraftModifiers` resolves `getCoveredTypes($type)` (which always includes `'any'`) and runs every modifier for each covered type in order.
110+
80111
### Property Processors (`src/PropertyProcessor/`)
81112

82-
- `Property/` — One processor per JSON Schema type
83-
- `ComposedValue/` — Processors for composition keywords
113+
- `ComposedValue/` — Processors for composition keywords (`allOf`, `anyOf`, `oneOf`, `if/then/else`, `not`)
84114
- `Filter/` — Custom filter processing
85115
- `Decorator/` — Property decorators (ObjectInstantiation, PropertyTransfer, IntToFloatCast, etc.)
86116

@@ -131,6 +161,10 @@ property, duplicate property names with unresolvable type conflicts, and any oth
131161
that cannot produce a correct PHP model. Fail loudly at generation time so the developer sees the
132162
problem immediately rather than receiving silently incorrect generated code.
133163

164+
### Reading files
165+
166+
Always use the dedicated `Read` tool to read file contents. Never use `sed`, `head`, `tail`, `cat`, or `awk` to read or extract portions of files. The `Read` tool supports `offset` and `limit` parameters for reading partial files when needed.
167+
134168
### PHP import style
135169

136170
Always add `use` imports for every class referenced in a file, including global PHP classes such as

docs/requirements.txt

-1004 Bytes
Binary file not shown.

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
#
5959
# This is also used if you do content translation via gettext catalogs.
6060
# Usually you set "language" from the command line for these cases.
61-
language = None
61+
language = 'en'
6262

6363
# List of patterns, relative to source directory, that match files and
6464
# directories to ignore when looking for source files.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
Custom Drafts
2+
=============
3+
4+
The *Draft* system defines which JSON Schema keywords are recognised during model generation and
5+
how each keyword is translated into PHP code. Every ``GeneratorConfiguration`` has a
6+
*draft* — by default ``AutoDetectionDraft``, which inspects the ``$schema`` keyword in each
7+
schema file and picks the appropriate draft automatically.
8+
9+
You can replace the default draft with a concrete ``DraftInterface`` implementation to pin all
10+
schemas to a single set of rules, or supply a ``DraftFactoryInterface`` implementation to choose
11+
the draft dynamically per schema file.
12+
13+
Concepts
14+
--------
15+
16+
Type
17+
A named JSON Schema type (``"object"``, ``"string"``, ``"integer"``, ``"number"``,
18+
``"boolean"``, ``"array"``, ``"null"``, or the virtual ``"any"`` type that applies to every
19+
property regardless of its declared type).
20+
21+
Each type entry holds an ordered list of *modifiers*.
22+
23+
Modifier (``ModifierInterface``)
24+
A unit of work that reads the raw JSON Schema for a property and modifies the in-memory
25+
``PropertyInterface`` object — adding validators, decorators, type hints, or any other
26+
enrichment. Modifiers are executed in registration order, and the ``"any"`` modifiers run
27+
for every property.
28+
29+
Validator factory (``AbstractValidatorFactory``)
30+
A special modifier that is keyed to a single JSON Schema keyword (e.g. ``"minLength"``).
31+
It checks whether that keyword is present in the schema and, if so, adds the corresponding
32+
validator to the property. Validator factories are registered via ``Type::addValidator()``.
33+
34+
Implementing a custom draft
35+
---------------------------
36+
37+
The simplest approach is to extend an existing draft (e.g. ``Draft_07``) via the builder API and
38+
override the parts you need.
39+
40+
**Example: add a custom keyword modifier to Draft 7**
41+
42+
.. code-block:: php
43+
44+
use PHPModelGenerator\Draft\Draft_07;
45+
use PHPModelGenerator\Draft\DraftBuilder;
46+
use PHPModelGenerator\Draft\DraftInterface;
47+
use PHPModelGenerator\Draft\Modifier\ModifierInterface;
48+
use PHPModelGenerator\Model\Property\PropertyInterface;
49+
use PHPModelGenerator\Model\Schema;
50+
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
51+
use PHPModelGenerator\SchemaProcessor\SchemaProcessor;
52+
53+
class DeprecatedModifier implements ModifierInterface
54+
{
55+
public function modify(
56+
SchemaProcessor $schemaProcessor,
57+
Schema $schema,
58+
PropertyInterface $property,
59+
JsonSchema $propertySchema,
60+
): void {
61+
if (!($propertySchema->getJson()['deprecated'] ?? false)) {
62+
return;
63+
}
64+
65+
// Add a PHPDoc annotation or any other enrichment here.
66+
}
67+
}
68+
69+
class MyDraft implements DraftInterface
70+
{
71+
public function getDefinition(): DraftBuilder
72+
{
73+
// Obtain the standard Draft 7 builder and append a modifier to the 'any' type.
74+
$builder = (new Draft_07())->getDefinition();
75+
$builder->getType('any')->addModifier(new DeprecatedModifier());
76+
77+
return $builder;
78+
}
79+
}
80+
81+
Register the custom draft in your generator configuration:
82+
83+
.. code-block:: php
84+
85+
use PHPModelGenerator\Model\GeneratorConfiguration;
86+
87+
$configuration = (new GeneratorConfiguration())
88+
->setDraft(new MyDraft());
89+
90+
Implementing a draft factory
91+
-----------------------------
92+
93+
If you need to select the draft dynamically per schema file, implement
94+
``DraftFactoryInterface``:
95+
96+
.. code-block:: php
97+
98+
use PHPModelGenerator\Draft\AutoDetectionDraft;
99+
use PHPModelGenerator\Draft\DraftFactoryInterface;
100+
use PHPModelGenerator\Draft\DraftInterface;
101+
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
102+
103+
class MyDraftFactory implements DraftFactoryInterface
104+
{
105+
private AutoDetectionDraft $autoDetect;
106+
private MyDraft $myDraft;
107+
108+
public function __construct()
109+
{
110+
$this->autoDetect = new AutoDetectionDraft();
111+
$this->myDraft = new MyDraft();
112+
}
113+
114+
public function getDraftForSchema(JsonSchema $schema): DraftInterface
115+
{
116+
// Use the custom draft only for schemas that opt in via a custom flag.
117+
if ($schema->getJson()['x-use-my-draft'] ?? false) {
118+
return $this->myDraft;
119+
}
120+
121+
return $this->autoDetect->getDraftForSchema($schema);
122+
}
123+
}
124+
125+
$configuration = (new GeneratorConfiguration())
126+
->setDraft(new MyDraftFactory());

docs/source/generator/postProcessor.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ All added post processors will be executed after a schema was processed and befo
2424
builtin/patternPropertiesAccessorPostProcessor
2525

2626
.. toctree::
27-
:caption: Custom Post Processors
27+
:caption: Custom Extensions
2828
:maxdepth: 1
2929

3030
custom/customPostProcessor
31+
custom/customDraft

docs/source/gettingStarted.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,34 @@ The output of a generation process may look like:
309309
Rendered class MyApp\User\Response\Login
310310
Rendered class MyApp\User\Response\Register
311311
312+
JSON Schema draft version
313+
^^^^^^^^^^^^^^^^^^^^^^^^^
314+
315+
.. code-block:: php
316+
317+
setDraft(DraftInterface|DraftFactoryInterface $draft);
318+
319+
Controls which JSON Schema draft version is used during generation. Accepts either a concrete
320+
draft instance (``DraftInterface``) to pin all schemas to one draft, or a factory
321+
(``DraftFactoryInterface``) to select the draft per schema file.
322+
323+
By default ``AutoDetectionDraft`` is used. It implements ``DraftFactoryInterface`` and inspects
324+
the ``$schema`` keyword of each schema file to select the appropriate draft automatically. When
325+
the keyword is absent or unrecognised, it falls back to JSON Schema Draft 7 behaviour, so schemas
326+
with different ``$schema`` declarations in the same generation run can use different drafts.
327+
328+
Available draft classes:
329+
330+
============= ================================
331+
Draft class Description
332+
============= ================================
333+
``Draft_07`` JSON Schema Draft 7 (default)
334+
============= ================================
335+
336+
.. seealso::
337+
338+
:doc:`generator/custom/customDraft` — how to implement a custom draft or modifier.
339+
312340
Custom filter
313341
^^^^^^^^^^^^^
314342

docs/source/toc-generator.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
:maxdepth: 2
44

55
generator/postProcessor
6+
generator/custom/customDraft

phpcs.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@
1212
<exclude name="PSR2.Classes.ClassDeclaration.ImplementsLine"/>
1313
<!-- Allow snake_case for internal utility methods -->
1414
<exclude name="PSR1.Methods.CamelCapsMethodName.NotCamelCaps"/>
15+
<!-- Allow underscore-separated class names (e.g. Draft_07, Draft_2020_12) -->
16+
<exclude name="Squiz.Classes.ValidClassName.NotPascalCase"/>
1517
</rule>
1618
</ruleset>

src/Draft/AutoDetectionDraft.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPModelGenerator\Draft;
6+
7+
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
8+
9+
class AutoDetectionDraft implements DraftFactoryInterface
10+
{
11+
/** @var DraftInterface[] Keyed by draft class name; reused across schemas */
12+
private array $draftInstances = [];
13+
14+
public function getDraftForSchema(JsonSchema $jsonSchema): DraftInterface
15+
{
16+
// Only Draft_07 is currently supported; all schemas (including unrecognised
17+
// or absent $schema keywords) fall back to it. Additional drafts will be
18+
// detected here in later phases (e.g. draft-04, draft 2020-12).
19+
return $this->draftInstances[Draft_07::class] ??= new Draft_07();
20+
}
21+
}

0 commit comments

Comments
 (0)