Skip to content

Commit 2ad3cf0

Browse files
committed
Merge remote-tracking branch 'origin/master' into jsonSchemaDraft2019
# Conflicts: # tests/AbstractPHPModelGeneratorTestCase.php # tests/Basic/PhpAttributeTest.php # tests/PostProcessor/AdditionalPropertiesAccessorPostProcessorTest.php # tests/PostProcessor/EnumPostProcessorTest.php # tests/PostProcessor/PatternPropertiesAccessorPostProcessorTest.php
2 parents baf39a0 + 4212f98 commit 2ad3cf0

158 files changed

Lines changed: 5754 additions & 810 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.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# CLAUDE.md
22

3+
## Responding to review notes
4+
5+
When working through a list of review notes, critically evaluate each note before acting on it:
6+
7+
- **Is the note correct?** The reviewer may be mistaken about what the code does, or may be
8+
operating on a false assumption. If the note is factually wrong, explain why and skip it.
9+
- **Is the proposed fix better than the current approach?** The reviewer's suggestion is a
10+
starting point, not a mandate. If a different solution is clearly superior, propose it.
11+
- **Is there an even better solution?** Think beyond the note. If the reviewer flags a smell,
12+
consider whether the right fix is the one they suggest or a deeper redesign.
13+
- **Document the reasoning.** For each note, produce a summary of what action was taken and why
14+
— including why any note was rejected or handled differently than suggested.
15+
16+
After tackling all notes, provide a summary table: one row per note, action taken, and brief
17+
reasoning.
18+
19+
---
20+
321
## Learning from reviews
422

523
After completing a task that involved responding to code review feedback, scan the reviewer's
@@ -73,7 +91,7 @@ composer update
7391
./vendor/bin/phpunit --testdox
7492
```
7593

76-
Tests write generated PHP classes to `sys_get_temp_dir()/PHPModelGeneratorTest/Models/` and dump failed classes to `./failed-classes/` (auto-cleaned on bootstrap).
94+
Tests write generated PHP classes to a session-unique directory `sys_get_temp_dir()/PHPModelGeneratorTest_<id>/Models/` (defined as `MODEL_TEMP_PATH`; the base is `TEST_BASE_DIR`) and dump failed classes to `./failed-classes/` (auto-cleaned on bootstrap). The session directory is cleaned up automatically via a shutdown function when the PHP process exits.
7795

7896
### Running the full test suite
7997

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ After generating a class with this JSON-Schema our class with the name `Person`
104104
// the constructor takes an array with data which is validated and applied to the model
105105
public function __construct(array $modelData);
106106

107-
// the method getRawModelDataInput always delivers the raw input which was provided on instantiation
108-
public function getRawModelDataInput(): array;
107+
// meta()->rawInput() always delivers the raw input which was provided on instantiation
108+
public function meta(): Meta;
109109

110110
// getters to fetch the validated properties. Age is nullable as it's not required
111111
public function getName(): string;
@@ -134,7 +134,7 @@ $person = new Person(['name' => 'Albert', 'age' => -1]);
134134
$person = new Person(['name' => 'Albert']);
135135
$person->getName(); // returns 'Albert'
136136
$person->getAge(); // returns NULL
137-
$person->getRawModelDataInput(); // returns ['name' => 'Albert']
137+
$person->meta()->rawInput(); // returns ['name' => 'Albert']
138138

139139
// If setters are generated the setters also perform validations.
140140
// Exception: 'Value for age must not be smaller than 0'
@@ -168,7 +168,7 @@ The library is tested via [PHPUnit](https://phpunit.de/).
168168

169169
After installing the dependencies of the library via `composer update` you can execute the tests with `./vendor/bin/phpunit` (Linux) or `vendor\bin\phpunit.bat` (Windows). The test names are optimized for the usage of the `--testdox` output. Most tests are atomic integration tests which will set up a JSON-Schema file and generate a class from the schema and test the behaviour of the generated class afterwards.
170170

171-
During the execution the tests will create a directory PHPModelGeneratorTest in tmp where JSON-Schema files and PHP classes will be written to.
171+
During the execution the tests will create a session-unique directory `PHPModelGeneratorTest_<id>` in tmp where JSON-Schema files and PHP classes will be written to. The directory is removed automatically when the test process exits, so concurrent test sessions do not interfere with each other.
172172

173173
If a test which creates a PHP class from a JSON-Schema fails the JSON-Schema and the generated class(es) will be dumped to the directory `./failed-classes`
174174

docs/requirements.txt

-62 Bytes
Binary file not shown.

docs/source/combinedSchemas/allOf.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,13 @@ The thrown exception will be a *PHPModelGenerator\\Exception\\ComposedValue\\All
9898
**any** branch, the generator promotes that property to non-nullable in the generated class. All
9999
``allOf`` branches must hold simultaneously, so any branch's ``required`` constraint is effectively
100100
global. See `Cross-typed compositions <crossTypedComposition.html>`__ for the full promotion rules.
101+
102+
.. note::
103+
104+
Properties in object-level ``allOf`` branches may carry a ``"default"`` value. Because all
105+
branches apply simultaneously, defaults from every branch are combined. When multiple branches
106+
define a default for the same property, those defaults must agree; the generator throws a
107+
``SchemaException`` at generation time if they differ.
108+
109+
See `Default values <../generic/default.html#branch-defaults-in-compositions>`__ for the full
110+
explanation.

docs/source/combinedSchemas/anyOf.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,15 @@ The thrown exception will be a *PHPModelGenerator\\Exception\\ComposedValue\\Any
8888
Because at least one branch must apply and all branches guarantee the property's presence, the
8989
getter can safely be non-nullable. See `Cross-typed compositions <crossTypedComposition.html>`__
9090
for the full promotion rules.
91+
92+
.. note::
93+
94+
Properties in object-level ``anyOf`` branches may carry a ``"default"`` value. The generator
95+
applies the branch default only when that branch is among the active ones — determined at
96+
construction time by which branches the provided data satisfies. When multiple matching branches
97+
define a default for the same property, those defaults must agree; the generator throws a
98+
``SchemaException`` at generation time if they differ. Branch defaults are **not** included in
99+
``getRawModelDataInput()``.
100+
101+
See `Default values <../generic/default.html#branch-defaults-in-compositions>`__ for the full
102+
explanation.

docs/source/combinedSchemas/if.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,18 @@ When only a ``then`` block is present (no ``else``), the branch may not apply at
194194
property's presence. If there is no ``else`` block, the property is never promoted — the schema
195195
is silent when the condition fails, so the property may be absent. See
196196
`Cross-typed compositions <crossTypedComposition.html>`__ for the full promotion rules.
197+
198+
.. note::
199+
200+
Properties in object-level ``then`` or ``else`` branches may carry a ``"default"`` value. The
201+
generator applies the branch default only when the relevant branch is active — the ``then``
202+
default applies when the ``if`` condition is satisfied, and the ``else`` default applies when it
203+
is not. A user-supplied value always overrides the branch default. Branch defaults are **not**
204+
included in ``getRawModelDataInput()``.
205+
206+
When a ``then`` or ``else`` branch default conflicts with a root ``properties`` default or a
207+
``patternProperties`` default for the same property, the generator throws a ``SchemaException``
208+
at generation time.
209+
210+
See `Default values <../generic/default.html#branch-defaults-in-compositions>`__ for the full
211+
explanation.

docs/source/combinedSchemas/mergedProperty.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,34 @@ This schema will generate three classes as no merged property is created. The ma
102102
public function setName(string $name): static
103103
public function getAge(): ?int
104104
public function setAge(int $age): static
105+
106+
Branch-constraint isolation
107+
---------------------------
108+
109+
When the same property name appears in multiple composition branches with different constraints
110+
(e.g. one ``oneOf`` branch requires an ``enum`` value while another allows a free-form string),
111+
the outer merged class does **not** inherit branch-specific constraints from any individual
112+
branch. Validation constraints such as ``enum``, ``minLength``, or ``pattern`` remain scoped to
113+
their respective branch and are enforced only when that branch is being validated.
114+
115+
This means that the ``EnumPostProcessor`` and similar post-processors will correctly operate on
116+
the branch-level property — not on the outer merged property — so the generated merged class
117+
setter accepts the full union of allowed values from all branches rather than being narrowed to
118+
the constraints of a single branch.
119+
120+
Attributes for shared properties
121+
---------------------------------
122+
123+
When ``JSON_POINTER`` or ``JSON_SCHEMA`` attributes are enabled (see
124+
`Attributes <../gettingStarted.html#attributes>`__), properties that appear in more than one
125+
composition branch receive synthesised attributes:
126+
127+
- **JSON_POINTER**: one ``#[JsonPointer]`` attribute is emitted per branch that defines the
128+
property. When the property is also declared in the root ``properties`` block, the root
129+
pointer is prepended.
130+
131+
- **JSON_SCHEMA**: a single ``#[JsonSchema]`` attribute is emitted whose value is a synthesised
132+
JSON object containing the composition keyword (``allOf``, ``anyOf``, ``oneOf``, or
133+
``if``/``then``/``else`` sub-objects) with only those branch sub-schemas that involve this
134+
property. Root-level constraints are merged into the top-level object when the property is
135+
also root-registered.

docs/source/combinedSchemas/oneOf.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,17 @@ The thrown exception will be a *PHPModelGenerator\\Exception\\ComposedValue\\One
9898
Exactly one branch applies at runtime; because all branches guarantee the property's presence,
9999
the getter can safely be non-nullable. See `Cross-typed compositions <crossTypedComposition.html>`__
100100
for the full promotion rules.
101+
102+
.. note::
103+
104+
Properties in object-level ``oneOf`` branches may carry a ``"default"`` value. The generator
105+
applies the branch default only when that branch is the active one — determined at construction
106+
time by which branch the provided data satisfies. A user-supplied value always overrides the
107+
branch default. Branch defaults are **not** included in ``getRawModelDataInput()``.
108+
109+
When two ``oneOf`` branches define a default for the same property, or when a branch default
110+
conflicts with a root ``properties`` default or a ``patternProperties`` default, the generator
111+
throws a ``SchemaException`` at generation time.
112+
113+
See `Default values <../generic/default.html#branch-defaults-in-compositions>`__ for the full
114+
explanation and examples.

docs/source/generator/builtin/additionalPropertiesAccessorPostProcessor.rst

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,27 +34,34 @@ Generated interface with the **AdditionalPropertiesAccessorPostProcessor**:
3434

3535
.. code-block:: php
3636
37-
public function getRawModelDataInput(): array;
38-
3937
public function setExample(float $example): static;
4038
public function getExample(): float;
4139
42-
public function getAdditionalProperties(): array;
43-
public function getAdditionalProperty(string $property): ?string;
44-
public function setAdditionalProperty(string $property, string $value): static;
45-
public function removeAdditionalProperty(string $property): bool;
40+
public function meta(): Meta;
41+
public function additionalProperties(): AdditionalPropertiesAccessor;
42+
43+
The ``additionalProperties()`` method returns an accessor object with the following interface:
44+
45+
.. code-block:: php
46+
47+
public function getAll(): array;
48+
public function get(string $key): mixed;
49+
public function set(string $key, mixed $value): void;
50+
public function remove(string $key): bool;
4651
4752
.. note::
4853

49-
The methods **setAdditionalProperty** and **removeAdditionalProperty** are only added if the `immutable setting <../../gettingStarted.html#immutable-classes>`__ is set to false.
54+
The methods **set** and **remove** on the accessor are only available if the `immutable setting <../../gettingStarted.html#immutable-classes>`__ is set to false.
55+
56+
When the ``additionalProperties`` keyword provides a schema that constrains the value type, a typed companion class ``{ModelName}AdditionalProperties`` is generated that narrows the return and parameter types of the accessor methods accordingly.
5057

51-
**getAdditionalProperties**: This method returns all additional properties which are currently part of the model as key-value pairs where the key is the property name and the value the current value stored in the model. All other properties which are part of the object (in this case the property *example*) will not be included. In opposite to the *getRawModelDataInput* the values provided via this method are the processed values. This means if the schema provides an object-schema for additional properties an array of object instances will be returned. If the additional properties schema contains `filter <../../nonStandardExtensions/filter.html>`__ the filtered (and in case of transforming filter transformed) values will be returned.
58+
**getAll**: Returns all additional properties currently part of the model as key-value pairs. Properties defined in the schema (in this case *example*) are not included. Unlike ``meta()->rawInput()``, the values returned here are the processed valuesif the schema defines an object schema for additional properties, an array of object instances is returned; if a `filter <../../nonStandardExtensions/filter.html>`__ is applied, the filtered (and for transforming filters, transformed) values are returned.
5259

53-
**getAdditionalProperty**: Returns the current value of a single additional property. If the requested property doesn't exist null will be returned. Returns as well as *getAdditionalProperties* the processed values.
60+
**get**: Returns the current value of a single additional property. Returns null if the requested property does not exist. Like ``getAll``, returns the processed value.
5461

55-
**setAdditionalProperty**: Adds or updates an additional property. Performs all necessary validations like property names or min and max properties validations. If the additional properties are processed via a transforming filter an already transformed value will be accepted. If a property which is regularly defined in the schema a *RegularPropertyAsAdditionalPropertyException* will be thrown. If the change is valid and performed also the output of *getRawModelDataInput* will be updated.
62+
**set**: Adds or updates an additional property. Performs all necessary validations including property name constraints and min/max properties limits. If the additional properties are processed via a transforming filter an already transformed value will be accepted. Throws *RegularPropertyAsAdditionalPropertyException* if the key conflicts with a regularly-defined schema property.
5663

57-
**removeAdditionalProperty**: Removes an existing additional property from the model. Returns true if the additional property has been removed, false otherwise (if no additional property with the requested key exists). May throw a *MinPropertiesException* if the change would result in an invalid model state. If the change is valid and performed also the output of *getRawModelDataInput* will be updated.
64+
**remove**: Removes an existing additional property from the model. Returns true if the property was removed, false if it did not exist. May throw a *MinPropertiesException* if removal would produce an invalid model state.
5865

5966
Serialization
6067
~~~~~~~~~~~~~

docs/source/generator/builtin/enumPostProcessor.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,45 @@ If an enum which requires a mapping is found but no mapping is provided a **Sche
8888
.. note::
8989

9090
By enabling the *$skipNonMappedEnums* option of the **EnumPostProcessor** you can skip enums which require a mapping but don't provide a mapping. Those enums will provide the default `enum <../../complexTypes/enum.html>`__ behaviour.
91+
92+
Enums inside compositions
93+
~~~~~~~~~~~~~~~~~~~~~~~~~
94+
95+
Enum schemas are also discovered inside composition keywords (**oneOf**, **anyOf**, **allOf**, **if** / **then** / **else**) and inside array **items** schemas, including nested compositions. The generated enum class is added to the parent property's PHP type hint so the setter accepts the enum instance directly.
96+
97+
Consider a property that accepts either a single status or a list of statuses:
98+
99+
.. code-block:: json
100+
101+
{
102+
"type": "object",
103+
"properties": {
104+
"status_filter": {
105+
"oneOf": [
106+
{ "$ref": "#/definitions/Status" },
107+
{
108+
"type": "array",
109+
"items": { "$ref": "#/definitions/Status" }
110+
}
111+
]
112+
}
113+
},
114+
"definitions": {
115+
"Status": {
116+
"type": "string",
117+
"title": "Status",
118+
"enum": ["active", "paused", "completed"]
119+
}
120+
}
121+
}
122+
123+
The generated setter exposes the enum class in both the PHPDoc and the native type hint:
124+
125+
.. code-block:: php
126+
127+
/**
128+
* @param string|Status|string[]|Status[]|null $statusFilter
129+
*/
130+
public function setStatusFilter(string|Status|array|null $statusFilter): static;
131+
132+
Branches under **not** are intentionally skipped — a value that fails an inner schema is not itself enum-typed and contributes no useful type information.

0 commit comments

Comments
 (0)