Skip to content

Integrate Fluent, FluentGen and FluentAnalysis#1730

Draft
alganet wants to merge 1 commit intoRespect:mainfrom
alganet:fluent-analysis
Draft

Integrate Fluent, FluentGen and FluentAnalysis#1730
alganet wants to merge 1 commit intoRespect:mainfrom
alganet:fluent-analysis

Conversation

@alganet
Copy link
Copy Markdown
Member

@alganet alganet commented Mar 25, 2026

Replace the internal code generation and builder infrastructure with Respect/Fluent for runtime method resolution, Respect/FluentGen for mixin interface generation, and Respect/FluentAnalysis for PHPStan type narrowing.

ValidatorBuilder now extends Fluent's Append builder with ComposableMap for prefix composition at runtime.

Each validator is annotated with:

  • #[Assurance] declaring the type it narrows to (100+ validators)
  • #[AssuranceParameter] on constructor params used for dynamic type resolution (Instance)
  • #[Composable] with class-string references for prefix composition constraints (Not, All, Key, Property, NullOr, UndefOr, Min, Max, Length, and 29 validators with with/without constraints)
  • #[ComposableParameter] on promoted prefix parameters (Key, Property)

Removes the internal CodeGen infrastructure (MethodBuilder, MixinGenerator, PrefixMapGenerator, etc.) in favor of FluentGen.

Adds type inference tests validating PHPStan narrowing for type validators, val variants, composites (allOf, anyOf, oneOf, noneOf, when), modifiers (not, nullOr, undefOr), element narrowing (each, all), value/member narrowing (identical, in), parameter narrowing (instance), and chain intersection.

@alganet
Copy link
Copy Markdown
Member Author

alganet commented Mar 25, 2026

This is a draft, which includes BC breaks and FluentAnalysis integration, mentioned in #1729

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.40%. Comparing base (ab992b4) to head (fbc2247).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #1730      +/-   ##
============================================
- Coverage     99.41%   99.40%   -0.02%     
+ Complexity     1020      998      -22     
============================================
  Files           194      191       -3     
  Lines          2387     2334      -53     
============================================
- Hits           2373     2320      -53     
  Misses           14       14              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@alganet alganet force-pushed the fluent-analysis branch 2 times, most recently from 8827c6b to 10d894a Compare March 25, 2026 06:17
@alganet alganet force-pushed the fluent-analysis branch 4 times, most recently from da4a13c to a9f8dcc Compare March 26, 2026 05:09
Replace the internal code generation and builder infrastructure with
Respect/Fluent for runtime method resolution, Respect/FluentGen for
mixin interface generation, and Respect/FluentAnalysis for PHPStan
type narrowing.

ValidatorBuilder now extends Fluent's Append builder with
ComposableMap for prefix composition at runtime.

Each validator is annotated with:
- #[Assurance] declaring the type it narrows to (100+ validators)
- #[AssuranceParameter] on constructor params used for dynamic type
  resolution (Instance)
- #[Composable] with class-string references for prefix composition
  constraints (Not, All, Key, Property, NullOr, UndefOr, Min, Max,
  Length, and 29 validators with with/without constraints)
- #[ComposableParameter] on promoted prefix parameters (Key, Property)

Removes the internal CodeGen infrastructure (MethodBuilder,
MixinGenerator, PrefixMapGenerator, etc.) in favor of FluentGen.

Adds type inference tests validating PHPStan narrowing for type
validators, val variants, composites (allOf, anyOf, oneOf, noneOf,
when), modifiers (not, nullOr, undefOr), element narrowing (each,
all), value/member narrowing (identical, in), parameter narrowing
(instance), and chain intersection.
Copy link
Copy Markdown
Member

@henriquemoody henriquemoody left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great!

'{{subject}} must be a boolean',
'{{subject}} must not be a boolean',
)]
#[Assurance(type: 'bool')]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is true, though. I think the assurance should be strict

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I need to review all narrowings I declared. There might be more than this one innacurate.

'{{subject}} must not be sorted in descending order',
self::TEMPLATE_DESCENDING,
)]
#[Assurance(type: 'array|string')]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between type: 'array|string' and type: ['array', 'string']? I've seen both

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For plain types, it makes no difference at all during runtime.

In runtime code, zero difference. This is imploded by | when it is an array. It's an array to allow users to use ::class when the target is a class, which enhances refactoring tools and IDE assistance.

This is a part of the design I'm not entirely sure about.

Copy link
Copy Markdown
Member

@henriquemoody henriquemoody Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if type would accept an AssuranceType object (or a string that creates a SingleType). That way we could allow union types as well:

interface AssuranceType {}
class SimpleType implements AssuranceType {}
class IntersectionType implements AssuranceType {}

You could have a from() in all those classes.

new SimpleType('string');
new UnionType('string', 'int');
new IntersectionType(Countable::class, Iterable::class);

The attribute declaration could remain the similar (with a factory method under the hood):

#[Assurance(type: 'string')]

But it would also allow more explicit declarations:

#[Assurance(type: new UnionType('string', 'int'))]

But, to be honest, I wouldn't mind always passing an object to type anyways

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it is today, one could pass generics too: array<FileInfo> and that should work. The type API gets hairy very quickly with the edge cases, and in the end we're inventing a new type declaration API. If going that way, probably sebastian/type is a better choice.

I personally like pure string (friendly to search&replace: you change a phpdoc and the attribute in one go), but I recognize IDEs fare better when classes use ::class class constants ("Refactor class..." option in some IDEs). Also future-proof (later we can add more support for other ways).

Another avenue could be trying to break phpstan to allow using mixed $input on ->evaluate(), but a different phpdoc for it (which today is not allowed), and the attribute would instead be on the evaluate method. That would require some changes to Simple validators (basically, all validators would need explicit evaluate), and it's an untested approach to extensions that I never tried.

public static function __callStatic(string $ruleName, array $arguments): self
public static function __callStatic(string $ruleName, array $arguments): static
{
return self::init()->__call($ruleName, $arguments);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd still try/catch to throw an exception from Validation

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way it is, it would throw FluentException instead, which might be more descriptive than ComponentException. There are tests for this behavior!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could pass the message through, though. I'm more concerned about the type. I think it's useful for the user to expect a Respect\Validation\Exceptions\Exception from this library, and this would break that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right! I'll do it.

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