How do you validate input when you have different cases?

I could really use a helping hand here

I tend to create projects with a controller/action where I need validation. I have a number of possibilities

  1. Create a DTO object (not a entity) with validation annotations
  2. Use my Model object, where each property has validation rules annotated (I can even add ValidationGroups)
  3. Annotate each action argument with it’s own validation and create my final entity with the arguments

I’ve tried all three.

Number 1 is very explicit according to the current context and usecase, so that is great. But leads to many small DTO classes and tend to get a mess for me (I would love to see how other organizes it, if you do this - PLEASE SHARE )

Lately I’ve been going away from number 2 (use the model) since all validation rules might not apply.

And number 3 can end up in a large docblock or custom (/combined) validators for a explicit argument (ValidPasswordValidator, ValidAccountIdentifierValidator). Just so we are on the same pace, this is how I’ved used it

<?php
namespace Vendor\Core\Validation\Validator;
use Neos\Flow\Validation\Validator\ConjunctionValidator;

class ValidAccountIdentifierValidator extends ConjunctionValidator
{
    public function validate($value)
    {
        $this->addValidator(new NotEmptyValidator());
        $this->addValidator(new EmailAddressValidator());
        $this->addValidator(new UniqueEmailAddressValidator());

        return parent::validate($value);
    }
}

Where as in a API context (one action = one endpoint) it makes sense to go for some kind of DTO approach. But then more validation concepts get’s mixed. And I tend to get in doubt what is really the best way

I would love to hear how you do, usescases, best practice, experiences and such.

Al in the name of getting better at using Flow and learn from others.

I hope you will share

Søren

Thanks for bringing up this discussion! I think a framework like Flow lives by challenging it’s concepts and rethinking things every now and then.

And oh boy, I do have a record track with Flow and validation and I do think there the framework is indeed missing something. And I do think, what you are feeling here is related to what I also discovered some years ago.

Quoting myself from RFC: Optimized Validation of Entities from way back in 2016 (yes, that long):

So the actual issue we face is, that Flow currently does not separate user input validation and enforcing business rules . The only thing that comes close is writing individual model validators, but it’s not easy to distinguish the two types of validators in order to skip the one type depending on context.

From a pure DDD viewpoint, the latter should actually happen inside the aggregates, ie. the aggregate is responsible for asserting it’s invariants. Now Flow currently doesn’t provide any nice flow (no pun intended) or best practice for doing such validations and notifying the user on the outcome, e.g. return validation messages (It could maybe be solved with Views.yaml configuration for a specific DomainInvariantException class).

From my viewpoint today, I would maybe say that Flow does not have any solution for “business rule enforcing” at all.

So with that being said, I would highly suggest to avoid doing “string”,“float”, etc. validations on any entity property alltogether (under some circumstances, property validators can act as a safe-guard to prevent running into wrongly persisted data, e.g. string length limits on a column). The only valid validations you should have on a domain model are business rules, which for Flow as mentioned probably translate best as custom Model validators. My gripe with that approach is the detachedness of the two concerns then. You have business rules in a class (Acme\Foo\Domain\Validator\MyDomainModelValidator) that is not physically close to the actual domain model it belongs to. This goes against a very valid heuristic to “keep things close together that change together”.

Hence, optimally, I’d say do 1) with a bit of encoding business rules into the domain model itself.
But of course in reality a lot of projects just deal with simple CRUD operations and in those cases, go for the easy way that Flows Validation framework handles well: annotate the entity directly, i.e. 2)
You will notice that this would be the right thing, when you start to just duplicate your entities into DTOs with added validation annotations and there is no logic involved between the input and what you persist to the database.

I haven’t done 3) really yet - I guess it could work well for cases where the endpoint decides the validation that needs to happen. Though maybe there it’d also be better to have that switch encoded in the domain model itself, because conditional validation sounds like a business rule.