Skip to content

New PSR for standardizing Validator (ValidatorInterface) #1337

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

LeTraceurSnork
Copy link

Here's a proposal of new PSR for standardizing Validators to one interface
Continuation of long-lasting discussions on official Discord channel

Goal

To unite earth and water many separated validator libraries under one interface so the switching and DI would be easier, protect from potential vendor-locks and reduce coupling.

@LeTraceurSnork LeTraceurSnork requested a review from a team as a code owner July 16, 2025 14:53
@LeTraceurSnork
Copy link
Author

@Crell @KorvinSzanto I guess you would want to at least look at it
@fabpot and I would appreciate your opinion as well, given that I based this on Symfony's validators xD

Testability and integration with dependency injection containers.
Consistent error format for user feedback, translation, or error mapping.
Clean separation of simple (bool) and extended (structured, multi-error) validation.
This pattern avoids having to deeply couple code to a specific validator ecosystem (as in Symfony) and dramatically reduces refactoring when switching stacks or updating validation strategies.
Copy link
Contributor

Choose a reason for hiding this comment

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

It's not just about specific applications switching stacks. It's about me being able to use a validation routine in a stand-alone library and not care if it's going to be used with Symfony, Slim, or Laravel. Eg, in Serde I can just tell people "if you want fancier validation, go use PSR-Whatever and leave me alone." (Or integrate a PSR-Whatever bridge into Serde if I feel like it.)

Copy link
Author

Choose a reason for hiding this comment

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

Oh, yeah, I see your point. Tho, I'm struggling with exact words to put that point in them 😕
Feel free to rephrase that in a suggestion)))


Every violation contains:

- Machine-readable code (for programmatic matching, i18n, etc.).
Copy link
Contributor

Choose a reason for hiding this comment

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

Disagree here. The class name is the machine-readable code. We don't need more opaque inconsistent strings.

Copy link
Member

Choose a reason for hiding this comment

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

@Crell class name doesn't work well for exposing it in APIs i.e. JavaScript or Android/iOS clients should not be dealing with PHP fully qualified class names.

Copy link
Author

Choose a reason for hiding this comment

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

Actually, I thought of some flag-like codes like those ones in json_encode() function (second argument)
But, yeah, class names are not good enough - for example, if you want to pass those violations to frontend


### 3.5. Swappability & DI

Core purpose — to allow hot-swapping validator engines and compositions via DI or configuration, zero code refactor.
Copy link
Contributor

Choose a reason for hiding this comment

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

See above.

### 3.6. Scope

Only single-value validation in scope; not object/collection/nested.
Contextual validation may be layered by implementors using extensions.
Copy link
Contributor

Choose a reason for hiding this comment

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

We should think through what compound validation would look like. That should be included.

Copy link
Author

Choose a reason for hiding this comment

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

That will probably require some analogue of Constraints (like in Symfony), which may be just too much for this PSR. Not quite sure

Comment on lines 10 to 12
The final implementations MAY decorate the objects with more
functionality than the one proposed but they MUST implement the indicated
interfaces/functionality first.
Copy link
Contributor

Choose a reason for hiding this comment

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

This block is unnecessary.

*
* @return ValidatorResponseInterface
*/
public function validate(mixed $value): ValidatorResponseInterface;
Copy link
Contributor

Choose a reason for hiding this comment

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

If the validation passed, we don't need detailed information. We can likely short-circuit that to something like true|ValidatorErrorInterface or similar.

Copy link
Member

Choose a reason for hiding this comment

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

@Crell that would create more code handling it. Having a single return value works better in this case.

Copy link
Author

Choose a reason for hiding this comment

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

Totally agree with @samdark here. I faced this problem in WordPress once - a core function used to return true|WP_Error and if you checked that via if ($response) you always got true condition. So, eventually, they changed call from

if ( retrieve_password( $user->user_login ) )

to

if ( true === retrieve_password( $user->user_login ) )

Regarding your thread - just... Why? Why do we need to complicate output instead of simplifying it?

p.s. I'm not even speaking about that we might want to read some details that Validator might provide on succesful validate

*
* @return bool
*/
public function isValid(): bool;
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be a property now, not a method. 😄

Copy link
Member

Choose a reason for hiding this comment

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

There are zero benefits making it a property. Just drawbacks.

Ie. If it is a property you are forcing the implementation to be a value object.

Choose a reason for hiding this comment

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

Those drawbacks should be gone with PHP 8.4 though. Not sure if we want to target PHP 8.4 for this standard just to gain access to hooked properties. That would as of today be rather strict and hurt the adoption of the standard.

Copy link
Member

Choose a reason for hiding this comment

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

But there are still no benefits... =)

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. No, it does not force it to be a value object. It just changes the spelling to be simpler and more concise.
  2. The benefit is conceptual integrity. See the talk I gave at PHPTek this year on the new world properties open up for us.
  3. We should target 8.5, frankly. This wouldn't be ready before 8.5 is released anyway, and we've learned our lesson in the past that targeting an older-than-latest release always backfires and just makes more work for all involved.

Copy link
Contributor

Choose a reason for hiding this comment

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

Putting on my Core Committee hat for a moment, I will never vote for a spec that goes out of its way to support 10 year old versions of PHP. A spec will always be used far more in the future than in the past. Anyone on PHP 5 still (god help them) isn't going to be able to use any 3rd party validator libraries anyway, because those will all be written for more modern PHP versions.

We learned our lesson from PSR-6, and PSR-7 (if my memory of the timing is right). And did it right in PSR-14 by using object. Use whatever language features are current and useful. If that makes the PHP version requirement high, so be it. The future is longer than the past.

Choose a reason for hiding this comment

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

You are absolutely right @Crell about supporting ancient PHP releases. Most validation libraries that could adopt the new standard don't deliver new feature releases with PHP 7 support, so why bother about PHP 7.

But not supporting 10 year old PHP does not mean we have to exclude last year's PHP. As far as I can tell, requiring PHP 8.5 does not unlock any features that we could make use of in this contract, does it?

If hooked properties is what we want for this contract, let's target PHP 8.4? For Symfony at least, that would make adopting the standard way easier because we'll bump our PHP requirement to 8.4 in November.

Copy link
Contributor

Choose a reason for hiding this comment

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

If there's no 8.5 features that would be useful, then yes, a version target of 8.4 is fine. But if we do find there are 8.5 features that would be useful, we should be very open to then requiring 8.5 to use them.

Off hand, I can't think of any new 8.5 features that would be useful here, but interface properties from 8.4, definitely are useful.

Copy link
Author

@LeTraceurSnork LeTraceurSnork Jul 22, 2025

Choose a reason for hiding this comment

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

I would've understand if you'd say smth like we can't support PHP 5.6 because we need to use object type as param (which we don't...yet). But drop older versions support just because 8.5 is fancy? Why? It's not like we need to do something overbearing - just not use new syntax in an interface (that may be used, btw, in libraries that extends that interface). It's not like interface suggests usage of money_format() or smth 😁

Copy link
Member

Choose a reason for hiding this comment

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

I think this long discussion proves my point.

There are zero benefits making it a property. Just drawbacks.

I understand we have property hooks and interface properties in PHP 8.4. But these two features did not bring any value to PHP other then "it is nicer to write".

Since a PSR should be a standardized way to do something, I think we should consider adoption rather than try to move libraries to use language features that does not give any value.

My strongest recommendation is to keep these as methods.

*
* @return string
*/
public function getMessage(): string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Could also be a property.

Copy link
Author

Choose a reason for hiding this comment

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

I suggest we decide that in one thread and then apply to all
#1337 (comment)

*
* @return string
*/
public function getCode(): string;
Copy link
Contributor

Choose a reason for hiding this comment

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

As specified, this is extraneous and unhelpful.

@Crell
Copy link
Contributor

Crell commented Jul 16, 2025

As I have said many times, get the WG together first, design details second. 😄

@Crell
Copy link
Contributor

Crell commented Jul 16, 2025

Also: The current proposal doesn't seem to go far enough. How do I link the validator to metadata? Would, for instance, an attribute implement this interface? If so, what does it do if it needs a service to do its validation? (Eg, "does this record exist in the db?") Then the validation metdata (attribute) and the validation logic (a service) need to be separate.

We don't have to mandate how that is handled, but we do need to ensure that it can be handled in a clean and portable fashion.

@derrabus
Copy link

derrabus commented Jul 17, 2025

Hello and thank you for working on this proposal. I believe and common interface for validators can be useful, especially for interoperability with libraries and even external systems like JS frontends. A few remarks from my side. I'll might add some more later when I have more time.

Source of violation

Why I validate a complex object, a violation with a human-readable message is often not enough. Imagine, the object represents a form submitted by the user. I want to validate the whole object and receive a list of violations. If the form is invalid, I want to render the message next to the field with the invalid input. In order to facilitate that, I need a machine readable representation of the property path. This is how Symfony does it.

https://github.com/symfony/symfony/blob/5015234dd9fe86dee561eb31a0de524f68a052a0/src/Symfony/Component/Validator/ConstraintViolationInterface.php#L86-L98

I'm not saying you should copy the way Symfony does it, but please take that use-case into consideration.

Translation

Regarding the error message, you're documenting it as…

Error message (human-readable, MAY be locale-dependent).

This basically means that the validator has to be aware of the target locale when conducting the validation. This also means that I can only target a single locale which I have to determine before calling the validator. It would be great if the violation object would be translatable in some way. Unfortunately, we don't have a PSR standard for translators or translatable objects.

Copy link
Member

@samdark samdark left a comment

Choose a reason for hiding this comment

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

Overall, I see it an interesting thing to standardize. Here's what we have in Yii3 about the topic:

https://github.com/yiisoft/validator


To satisfy both lightweight and complex needs, two interfaces are provided:

- `SimpleValidatorInterface` — exposes only `isValid(mixed $value): bool`. Suitable for basic needs (is it a valid phone? email? int in range?...) and maximizing performance.
Copy link
Member

Choose a reason for hiding this comment

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

Is that running a single validation rule against the value? If so, I'd name it differently... like RuleInterface or something.

Copy link
Author

Choose a reason for hiding this comment

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

Well... That probably IS a Constraint (like Symfony's) 🤔
Shall it contain methods like getCondition() or smth like that then?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe. That would be very useful if you want to replicate the same validation on the client side (mobile, SPA frontend) based on the rules used.

To satisfy both lightweight and complex needs, two interfaces are provided:

- `SimpleValidatorInterface` — exposes only `isValid(mixed $value): bool`. Suitable for basic needs (is it a valid phone? email? int in range?...) and maximizing performance.
- `ExtendedValidatorInterface` — exposes `validate(mixed $value): ValidatorResponseInterface`, for structured results (validation status, errors, error codes/messages/etc.).
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
- `ExtendedValidatorInterface` — exposes `validate(mixed $value): ValidatorResponseInterface`, for structured results (validation status, errors, error codes/messages/etc.).
- `ValidatorInterface` — exposes `validate(mixed $value): ValidatorResponseInterface`, for structured results (validation status, errors, error codes/messages/etc.).

for the reason in https://github.com/php-fig/fig-standards/pull/1337/files#r2213153862


Every violation contains:

- Machine-readable code (for programmatic matching, i18n, etc.).
Copy link
Member

Choose a reason for hiding this comment

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

@Crell class name doesn't work well for exposing it in APIs i.e. JavaScript or Android/iOS clients should not be dealing with PHP fully qualified class names.


### 3.6. Scope

Only single-value validation in scope; not object/collection/nested.
Copy link
Member

Choose a reason for hiding this comment

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

I'd ensure that these fall into the given interfaces well.

Copy link
Member

Choose a reason for hiding this comment

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

Also, in reality, single value validation without context is not enough.

*
* @return ValidatorResponseInterface
*/
public function validate(mixed $value): ValidatorResponseInterface;
Copy link
Member

Choose a reason for hiding this comment

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

Usually there's a context that matters, such as in the case when you need a password to be repeated exactly.

Copy link
Author

Choose a reason for hiding this comment

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

Suggested change
public function validate(mixed $value): ValidatorResponseInterface;
public function validate(mixed $value, array $context = []): ValidatorResponseInterface;

Probably?

Copy link
Member

@samdark samdark Jul 23, 2025

Choose a reason for hiding this comment

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

Yes. Either that or a DTO.

* MUST return true if $value passes.
* MUST return false if $value fails.
* MUST NOT throw exceptions if $value fails.
* SHOULD throw ValidatorException if the Validator itself could not tell if the value passes or not (e.g. Validator misconfigured)
Copy link
Member

Choose a reason for hiding this comment

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

Should we use generic \RuntimeException for this case?

Copy link
Author

Choose a reason for hiding this comment

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

No, because this one is more specific. RuntimeException is basically anything, while package exceptions is described specifically to show that error occures somewhere inside this package functionality

Copy link
Member

Choose a reason for hiding this comment

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

If you're not going to handle it specifically, it doesn't make sense to make it specific.

*
* @return ValidatorResponseInterface
*/
public function validate(mixed $value): ValidatorResponseInterface;
Copy link
Member

Choose a reason for hiding this comment

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

@Crell that would create more code handling it. Having a single return value works better in this case.

/**
* A single validation violation.
*/
interface ViolationInterface
Copy link
Member

Choose a reason for hiding this comment

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

For intl, there is a need for:

 /**
     * Returns parameters used for {@see $message} translation.
     *
     * @return array A mapping between parameter names and values.
     */
    public function getParameters(): array

@Crell
Copy link
Contributor

Crell commented Jul 17, 2025

@samdark Please don't use "Reviews". They break threading badly. 😄

Having a single ValidatorResponseInterface on which you have to check isValid is also more code. There will be code that determines which validators passed and which failed either way. The rest is just spelling. And for spelling, I (almost) always favor using the type system.

Re using the class name as the ID: The ID is an opaque string currently. It wouldn't be useful to some other system anyway. Honestly I'm not even sure what purpose it serves to exist at all, so unless that can be demonstrated I would object to having it. (Not something that has to be resolved before a WG forms, of course. The WG vote should likely include a lot fewer details than what is listed here.)

@samdark
Copy link
Member

samdark commented Jul 23, 2025

@Crell That's what I've used to... and these give a very good context cause comments are attached to certain code.

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.

5 participants