Skip to content

Introduce Draft-based architecture: eliminate legacy processors, centralize modifier/validator registration#123

Open
wol-soft wants to merge 10 commits intomasterfrom
drafts
Open

Introduce Draft-based architecture: eliminate legacy processors, centralize modifier/validator registration#123
wol-soft wants to merge 10 commits intomasterfrom
drafts

Conversation

@wol-soft
Copy link
Copy Markdown
Owner

Summary

  • Introduces a Draft system (src/Draft/) that centralises per-type modifier and validator-factory registrations for JSON Schema Draft 7, replacing the scattered processor class hierarchy
  • Eliminates ~2 900 lines of legacy processor classes (AbstractPropertyProcessor, AbstractValueProcessor, AbstractTypedValueProcessor, AbstractNumericProcessor, BaseProcessor, all per-type processors, PropertyProcessorFactory, ComposedValueProcessorFactory, MultiTypeProcessor, PropertyMetaDataCollection) in favour of a single PropertyFactory that applies ordered modifiers
  • All scalar/array/object/composition keyword validators are now registered as AbstractValidatorFactory subclasses in Draft_07, making it straightforward to add or override behaviour per draft version
  • Adds ConstModifier, IntToFloatModifier, NullModifier, TypeCheckModifier, DefaultValueModifier, DefaultArrayToEmptyArrayModifier, and ObjectModifier as first-class modifier classes
  • Adds AutoDetectionDraft for backwards-compatible draft auto-selection
  • New tests: DraftTest, DefaultValueModifierTest, TypeCheckModifierTest
  • Docs: new customDraft.rst page, updated gettingStarted.rst and postProcessor.rst

Test plan

  • ./vendor/bin/phpunit --no-coverage passes green
  • ./vendor/bin/phpcs --standard=phpcs.xml src/ tests/Draft/ reports no issues
  • New draft/modifier tests exercise happy-path and error cases for Draft, DefaultValueModifier, and TypeCheckModifier
  • Existing regression tests (Basic, Objects, Issues, etc.) continue to pass unchanged

Enno Woortmann added 10 commits March 26, 2026 01:35
Add DraftInterface/DraftFactoryInterface abstraction and wire it into
GeneratorConfiguration. The pipeline does not yet use the draft — this
phase establishes the structural foundation only.

New classes:
- DraftInterface: getDefinition(): DraftBuilder
- DraftFactoryInterface: getDraftForSchema(JsonSchema): DraftInterface
- DraftBuilder: fluent builder for Draft instances
- Draft: built result with getCoveredTypes(string|array) — always includes 'any'
- Element/Type: holds a type name and its ModifierInterface list
- Modifier/ModifierInterface: modify() contract for future phases
- Draft_07: registers all 7 JSON Schema types + 'any' (empty modifier lists)
- AutoDetectionDraft: DraftFactoryInterface that detects $schema keyword,
  falls back to Draft_07 for absent/unrecognised values

GeneratorConfiguration gains getDraft()/setDraft() accepting
DraftInterface|DraftFactoryInterface; defaults to AutoDetectionDraft.

phpcs.xml: exclude Squiz.Classes.ValidClassName.NotPascalCase to allow
underscore-separated draft class names (Draft_07, Draft_2020_12, etc.).
…raft architecture

Phase 2 — Eliminate PropertyMetaDataCollection:
- Add bool $required parameter to PropertyFactory::create,
  ProcessorFactoryInterface::getProcessor and all processor constructors
- Replace isAttributeRequired() lookups with $this->required throughout
- Move addDependencyValidator to BaseProcessor::addPropertiesToSchema,
  reading directly from $json['dependencies'][$propertyName]
- Update SchemaDefinition::resolveReference to accept bool $required
  instead of PropertyMetaDataCollection; simplify cache key accordingly
- Remove PMC save/restore from AdditionalPropertiesValidator,
  PatternPropertiesValidator, ArrayTupleValidator,
  AbstractComposedValueProcessor, and IfProcessor
- Delete PropertyMetaDataCollection entirely

Phase 3 refinements — Draft architecture:
- DraftInterface.getDefinition() returns DraftBuilder (not Draft),
  enabling draft extension by consumers; PropertyFactory builds and caches
- Type constructor auto-installs TypeCheckModifier via
  TypeConverter::jsonSchemaToPhp, removing per-type boilerplate from Draft_07
- DefaultValueModifier is self-contained: reads type from schema, resolves
  is_* check and int->float coercion internally; no callable constructor API
- Draft_07 is now a clean declarative list with no implementation detail
- Replace DRAFT_BYPASS_TYPES hardcoded list with Draft::hasType() check
- Add TypeConverter::jsonSchemaToPhp() to centralise JSON->PHP type mapping
…to validator factories

Introduce AbstractValidatorFactory hierarchy (SimplePropertyValidatorFactory,
SimpleBaseValidatorFactory) and migrate all keyword-specific validator generation
out of the legacy processors into dedicated factory classes registered on Draft_07.

Migrated types: string (pattern, minLength, maxLength, format), integer/number
(minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf), array
(items, minItems, maxItems, uniqueItems, contains), universal (enum, filter).
Also adds DefaultArrayToEmptyArrayModifier for the array default-empty behaviour.

Legacy processor generateValidators overrides for string, numeric, and array
types are removed entirely. AbstractNumericProcessor is now empty (flattened into
IntegerProcessor/NumberProcessor directly extending AbstractTypedValueProcessor).

PropertyFactory exposes applyTypeModifiers/applyUniversalModifiers as public
methods; MultiTypeProcessor uses these directly instead of a skipUniversalModifiers
flag, eliminating the 'type-only' sentinel.

FilterValidator.validateFilterCompatibilityWithBaseType now derives the effective
type from getNestedSchema() for object properties instead of relying on a
set/restore workaround in FilterValidatorFactory.
Multi-type properties ("type": [...]) are now handled directly in
PropertyFactory::createMultiTypeProperty instead of delegating to
MultiTypeProcessor. Each type is processed through its legacy single-type
processor and Draft type modifiers; type-check validators are collected and
consolidated into a single MultiTypeCheckValidator; decorators are forwarded
via PropertyTransferDecorator; universal modifiers run once on the main
property after all sub-properties resolve.

- Delete MultiTypeProcessor
- PropertyProcessorFactory::getProcessor now only handles string types;
  the private getSingleTypePropertyProcessor wrapper is inlined
- ProcessorFactoryInterface::getProcessor parameter narrowed to string
- Type validity is checked via PropertyFactory::checkType (shared between
  scalar and multi-type paths)
- Three invalidRecursiveMultiType test expectations updated: outer
  InvalidItemException property name changes from "item of array property"
  to "property" due to different extracted method registration order
…r architecture

- Convert MinProperties, MaxProperties, PropertyNames, PatternProperties,
  AdditionalProperties, and Properties processing from BaseProcessor methods
  into validator factories (Factory/Object/) registered via addValidator() in
  Draft_07, consistent with the scalar/array/number/string factory pattern
- ObjectModifier remains a proper modifier (structural, not keyword-driven)
- PropertyFactory: add type=object path that calls processSchema directly,
  wires the outer property via TypeCheckModifier + ObjectModifier, and runs
  universal modifiers (filter/enum/default) on the outer property
- PropertyFactory: add RequiredPropertyValidator for required type=object
  properties; strip property-level keywords before passing schema to
  processSchema to prevent double-application on the nested class root
- SchemaProcessor: add transferComposedPropertiesToSchema (migrated from
  BaseProcessor) with correct use imports so allOf/anyOf/oneOf branch
  properties are transferred and conflict detection fires correctly
- BaseProcessor: remove all methods now handled by Draft modifiers/factories
…dValueProcessorFactory

Introduce Model\Validator\Factory\Composition\AbstractCompositionValidatorFactory
(extends AbstractValidatorFactory) with shared composition helpers, plus five
concrete factories: AllOfValidatorFactory, AnyOfValidatorFactory,
OneOfValidatorFactory, NotValidatorFactory, IfValidatorFactory. A marker
interface ComposedPropertiesValidatorFactoryInterface replaces
ComposedPropertiesInterface for the property-transfer guard.

Register all five on the 'any' type in Draft_07 via addValidator(), consistent
with the keyword-keyed pattern established in Phase 4.

Delete ComposedValueProcessorFactory and all legacy ComposedValue processor
classes (AbstractComposedValueProcessor, AllOfProcessor, AnyOfProcessor,
OneOfProcessor, NotProcessor, IfProcessor and their interfaces).
AbstractPropertyProcessor is slimmed to only the RequiredPropertyValidator and
isImplicitNullAllowed helpers still needed by the bridge.

Update all is_a() checks and use-imports in Schema, BaseProcessor,
SchemaProcessor, ConditionalPropertyValidator, and
CompositionRequiredPromotionPostProcessor to reference the new factory classes.
…ConstModifier, IntToFloatModifier, NullModifier

- Delete all PropertyProcessor/Property/* classes (AbstractPropertyProcessor,
  AbstractValueProcessor, AbstractTypedValueProcessor, AbstractNumericProcessor,
  StringProcessor, IntegerProcessor, NumberProcessor, BooleanProcessor,
  ArrayProcessor, ObjectProcessor, NullProcessor, AnyProcessor, ConstProcessor,
  ReferenceProcessor, BasereferenceProcessor, BaseProcessor)
- Delete PropertyProcessorFactory and ProcessorFactoryInterface
- Remove ProcessorFactoryInterface constructor parameter from PropertyFactory
- Inline $ref / baseReference routing as private methods on PropertyFactory
- Refactor PropertyFactory::create into focused dispatch + four extracted methods
  (createObjectProperty, createBaseProperty, createTypedProperty, buildProperty)
  with a single unified applyModifiers helper replacing three separate methods
- Add ConstModifier (registered on 'any' type): sets PropertyType from const value
  type and adds InvalidConstException validator; immutability is fully respected
- Add IntToFloatModifier (registered on 'number' type): adds IntToFloatCastDecorator
- Add NullModifier (registered on 'null' type): clears PropertyType and adds TypeHintDecorator
- Register object type in Draft_07 with typeCheck=false (PropertyFactory handles
  object type-check via wireObjectProperty, not via Draft modifier dispatch)
- Remove stale PropertyProcessorFactory imports from validator/schema files
- Add ConstPropertyTest::testConstPropertyHasNoSetterWhenImmutable
- Add docs/source/generator/custom/customDraft.rst: custom draft / modifier guide
- Update setDraft documentation in gettingStarted.rst with seealso link
- Update CLAUDE.md architecture section to document the final Draft modifier system
@coveralls
Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 23736910562

Details

  • 1374 of 1399 (98.21%) changed or added relevant lines in 60 files are covered.
  • 3 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.4%) to 98.331%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/Draft/Modifier/ObjectType/ObjectModifier.php 27 28 96.43%
src/Model/Validator/FilterValidator.php 8 9 88.89%
src/Model/Validator/PropertyNamesValidator.php 6 7 85.71%
src/Draft/DraftBuilder.php 5 7 71.43%
src/Model/Validator/Factory/Composition/AbstractCompositionValidatorFactory.php 90 92 97.83%
src/PropertyProcessor/PropertyFactory.php 254 256 99.22%
src/SchemaProcessor/SchemaProcessor.php 106 109 97.25%
src/Draft/Draft.php 17 21 80.95%
src/Model/Validator/Factory/SimplePropertyValidatorFactory.php 12 21 57.14%
Files with Coverage Reduction New Missed Lines %
src/Model/Validator/FilterValidator.php 1 98.0%
src/Model/Validator/PassThroughTypeCheckValidator.php 2 85.71%
Totals Coverage Status
Change from base Build 23736867000: -0.4%
Covered Lines: 4243
Relevant Lines: 4315

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants