Skip to content

Instantly share code, notes, and snippets.

@dadamssg
Last active August 29, 2015 14:17
Show Gist options
  • Save dadamssg/90d4f3c2151862e68c27 to your computer and use it in GitHub Desktop.
Save dadamssg/90d4f3c2151862e68c27 to your computer and use it in GitHub Desktop.

Handling validation with a command bus

I've been struggling with coming up with a way to handle validation in my command bus architecture.

Throwing validation exceptions in middleware felt wrong. Those aren't really exceptional occurences. Validating data outside of the bus(in controllers/console commands) felt wrong. Validation could be easily side-stepped. Validating in handlers felt cluttered and seemed to violate the SRP.

I think I've finally come up with something I'm fairly happy with though.

What I tried first

I was/am using symfony forms to map incoming data to command objects. I was doing the validation there. I'd define the validation rules in a bundle's validation.yml file so my controllers looked something like this.

class UserController extends ApiController
{
    public function registerAction(Request $request)
    {
        $form = $this->createForm('register_user');

        $form->handleRequest($request);

        if ($form->isValid()) {

            $command = $form->getData();
            $this->getCommandBus()->handle($command);

            return $this
                ->setData(['message' => "Registration successful."])
                ->setStatusCode(Response::HTTP_CREATED)
                ->respond();
        }

        return $this->respondWithForm($form);
    }
}

The problem

The problem was that validation is now kind of coupled to a web request. If I create a command object in a console command and pass it to the bus I skip validation...not exactly what I want.

The solution

I decided I needed/wanted that validation check to happen somewhere in the depths of the ->handle($command) call. This way the validation couldn't ever be side-stepped.

I decided to use a middleware and a trait.

class CommandValidatorMiddleware implements MessageBusMiddleware
{
    /**
     * @var Validator
     */
    private $validator;

    /**
     * @param Validator $validator
     */
    public function __construct(Validator $validator)
    {
        $this->validator = $validator;
    }

    /**
     * {@inheritdoc}
     */
    public function handle(Message $command, callable $next)
    {
        $traits = class_uses($command);

        // Command isn't worried about validation, continue on
        if ($traits === false || !in_array(HasErrorsTrait::CLASS, $traits)) {
            $next($command);
            return;
        }

        // Validate the command and bail if errors
        $errors = $this->validator->validate($command);
        if (count($errors) > 0) {
            $command->addErrors($errors);
            return;
        }

        // No errors, continue on
        $next($command);
    }
}

And now my controller will look like this.

class UserController extends ApiController
{
    public function registerAction(Request $request)
    {
        $form = $this->createForm('register_user');

        $form->handleRequest($request);

        if ($form->isValid()) {

            $command = $form->getData();
            $this->getCommandBus()->handle($command);

            if ($command->hasErrors()) {
            	return $this->respondWithErrors($command->getErrors());
            }

            return $this
                ->setData(['message' => "Registration successful."])
                ->setStatusCode(Response::HTTP_CREATED)
                ->respond();
        }

        return $this->respondWithForm($form);
    }
}

Perfect...except now my command is being validated twice. Once by symfony's form component and once by the middleware I just introduced. That's not very efficient, especially if validation requires hitting the database. The solution to that is simple though. You can actually disable some of the form components validation by setting the validation_groups setting to false.

class RegisterUserCommandType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $notMapped = ['mapped' => false];

        $builder
            ->add('email', null, $notMapped)
            ->add('password', null, $notMapped);
    }

    /**
     * {@inheritdoc}
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(
            [
                'validation_groups' => false, // <-- disable validation check
                'empty_data' => function (FormInterface $form) {
                    return new RegisterUserCommand(
                        Uuid::generate(),
                        $form->get('email')->getData(),
                        $form->get('password')->getData()
                    );
                }
            ]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'register_user';
    }
}

Now the form will only have errors if basic integrity checks fail, ex. missing fields. This allows the form to be sure it has just enough information to build up the command but doesn't care if the data is any good which is exactly what I want. That's my middleware's job now.

I don't think this is perfect but I like it better than anything I've come up with so far. Handling validation in a different way? I'd love to see how. Comment or tweet at me!

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