The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Dancer2::Plugin::FormValidator - neat and easy to start form validation plugin for Dancer2.

VERSION

version 1.02

SYNOPSIS

    ### If you need a simple and easy validation in your project,
    ### This module is what you need.

    use Dancer2;
    use Dancer2::Plugin::FormValidator;

    ### First create form validation profile class.

    package RegisterForm {
        use Moo;
        with 'Dancer2::Plugin::FormValidator::Role::Profile';

        ### Here you need to declare fields => validators.

        sub profile {
            return {
                username     => [ qw(required alpha_num length_min:4 length_max:32) ],
                email        => [ qw(required email length_max:127) ],
                password     => [ qw(required length_max:40) ],
                password_cnf => [ qw(required same:password) ],
                confirm      => [ qw(required accepted) ],
            };
        }
    }

    ### Now you can use it in your Dancer2 project.

    post '/form' => sub {
        if (validate profile => RegisterForm->new) {
            my $valid_hash_ref = validated;

            save_user_input($valid_hash_ref);
            redirect '/success_page';
        }

        redirect '/form';
    };

The html result could be like:

Screenshot register form

DESCRIPTION

This is micro-framework that provides validation in your Dancer2 application. It consists of dsl's keywords: validate, validated, errors. It has a set of built-in validators that can be extended by compatible modules (extensions). Also proved runtime switching between languages, so you can show proper error messages to users.

This module has a minimal set of dependencies and does not require the mandatory use of DBIc or Moose.

Uses simple and declarative approach to validate forms.

Validator

First, you need to create class which will implements at least one main role: Dancer2::Plugin::FormValidator::Role::Profile.

This role requires profile method which should return a HashRef Data::FormValidator accepts:

    package RegisterForm

    use Moo;
    with 'Dancer2::Plugin::FormValidator::Role::Profile';

    sub profile {
        return {
            username     => [ qw(required alpha_num_ascii length_min:4 length_max:32) ],
            email        => [ qw(required email length_max:127) ],
            password     => [ qw(required length_max:40) ],
            password_cnf => [ qw(required same:password) ],
            confirm      => [ qw(required accepted) ],
        };
    };

Profile method

Profile method should always return a HashRef[ArrayRef] where keys are input fields names and values are ArrayRef with list of validators.

Application

Then you need to set basic configuration:

    use Dancer2;

     set plugins => {
            FormValidator => {
                session => {
                    namespace => '_form_validator' # This is required field
                },
            },
        };

Now you can validate POST parameters in your controller:

    use Dancer2;
    use Dancer2::Plugin::FormValidator;
    use RegisterForm;

    post '/register' => sub {
        if (my $valid_hash_ref = validate profile => RegisterForm->new) {
            if (login($valid_hash_ref)) {
                redirect '/success_page';
            }
        }

        redirect '/register';
    };

    get '/register' => sub {
        template 'app/register' => {
            title  => 'Register page',
        };
    };

Template

In you template you have access to: $errors - this is HashRef[ArrayRef] with fields names as keys and error messages values and $old - contains old input values.

Template app/register:

    <div class="w-3/4 max-w-md bg-white shadow-lg py-4 px-6">
        <form method="post" action="/register">
            <div class="py-2">
                <label class="block font-normal text-gray-400" for="name">
                    Name
                </label>
                <input
                        type="text"
                        id="name"
                        name="name"
                        value="<: $old[name] :>"
                        class="border border-2 w-full h-5 px-4 py-5 mt-1 rounded-md
                        hover:outline-none focus:outline-none focus:ring-1 focus:ring-indigo-100"
                >
                <: for $errors[name] -> $error { :>
                    <small class="pl-1 text-red-400"><: $error :></small>
                <: } :>
            </div>
            <div class="py-2">
                <label class="block font-normal text-gray-400" for="email">
                    Email
                </label>
                <input
                        type="text"
                        id="email"
                        name="email"
                        value="<: $old[email] :>"
                        class="border border-2 w-full h-5 px-4 py-5 mt-1 rounded-md
                        hover:outline-none focus:outline-none focus:ring-1 focus:ring-indigo-100"
                >
                <: for $errors[email] -> $error { :>
                    <small class="pl-1 text-red-400"><: $error :></small>
                <: } :>

            <!-- Other fields -->
            ...
            ...
            ...
            <!-- Other fields end -->

            </div>
            <button
                    type="submit"
                    class="mt-4 bg-sky-600 text-white py-2 px-6 rounded-md hover:bg-sky-700"
            >
                Register
            </button>
        </form>
    </div>

CONFIGURATION

    ...
    plugins:
        FormValidator:
            session:
                namespace: '_form_validator'         # this is required
            messages:
                language: en                         # this is default
                ucfirst: 1                           # this is default
                validators:
                    required:
                        en: %s is needed from config # custom en message
                        de: %s ist erforderlich      # custom de message
                    ...
            extensions:
                dbic:
                    provider: Dancer2::Plugin::FormValidator::Extension::DBIC
                    ...
    ...

session

namespace

Session storage key where this module stores data, like: errors or old vars.

messages

language

Default language for error messages.

ucfirst

Apply ucfirst function to messages or not.

validators

Key => values, where key is validator name and value is messages dictionary for different languages.

extensions

Key => values, where key is extension short name and values is its configuration.

DSL KEYWORDS

validate

    validate(Hash %args): HashRef|undef

Accept arguments as hash:

    (
        profile => Object implementing Dancer2::Plugin::FormValidator::Role::Profile # required
        input   => HashRef of values to validate, default is body_parameters->as_hashref_mixed
        lang    => Accepts two-lettered language id, default is 'en'
    )

Profile is required, input and lang is optional.

Returns valid input HashRef if validation succeed, otherwise returns undef.

    ### You can use HashRef returned from validate.

    if (my $valid_hash_ref = validate profile => RegisterForm->new) {
        # Success, data is valid.
    }


    ### Or more declarative approach with validated keyword.

    if (validate profile => RegisterForm->new) {
        # Success, data is valid.
        my $valid_hash_ref = validated;

        # Do some operations...
    }
    else {
        # Error, data is invalid.
        my $errors = errors; # errors keyword returns error messages.

        # Redirect or show errors...
    }

validated

    validated(): HashRef|undef

No arguments. Returns valid input HashRef if validate succeed. Undef value will be returned after first call within one validation process.

    my $valid_hash_ref = validated;

errors

    errors(): HashRef

No arguments. Returns HashRef[ArrayRef] if validation failed.

    my $errors_hash_multi = errors;

Validators

accepted

    accepted(): Bool

Validates that field exists and one of the listed: (yes on 1).

    field => [ qw(accepted) ]

alpha

    alpha(Str $encoding = 'a'): Bool

Validate that string only contain of alphabetic symbols. By default encoding is ascii, i.e /^[[:alpha:]]+$/a.

    field => [ qw(alpha) ]

To set encoding to unicode you need to pass 'u' argument:

    field => [ qw(alpha:u) ]

Then the validation rule will be /^[[:alpha:]]+$/.

alpha_num

    alpha_num(Str $encoding = 'a'): Bool

Validate that string only contain of alphabetic symbols, underscore and numbers 0-9. By default encoding is ascii, i.e. /^\w+$/a.

    field => [ qw(alpha_num) ]

To set encoding to unicode you need to pass 'u' argument:

    field => [ qw(alpha_num:u) ]

Rule will be /^\w+$/.

boolean

    boolean(): Bool

Validate that field is 0 or 1 scalar value.

    field => [ qw(boolean) ]

email

    email(): Bool

Validate that field is valid email(rfc822).

    field => [ qw(email) ]

email_dns

    email_dns(): Bool

Validate that field is valid email(rfc822) and dns exists.

    field => [ qw(email_dns) ]

enum

    enum(Array @values): Bool

Validate that field is one of listed values.

    field => [ qw(enum:value1,value2) ]

integer

    integer(): Bool

Validate that field is integer.

    field => [ qw(integer) ]

length_max

    length_max(Int $num): Bool

Validate that string length <= num.

    field => [ qw(length_max:32) ]

length_min

    length_min(Int $num): Bool

Validate that string length >= num.

    field => [ qw(length_max:4) ]

max

    max(Int $num): Bool

Validate that field is number <= num.

    field => [ qw(max:32) ]

min

    min(Int $num): Bool

Validate that field is number >= num.

    field => [ qw(min:4) ]

numeric

    numeric(): Bool

Validate that field is number.

    field => [ qw(numeric) ]

required

    required(): Bool

Validate that field exists and not empty string.

    field => [ qw(required) ]

required_with

    required_with(Str $field_name): Bool

Validate that field exists and not empty string if another field is exists and not empty.

    field_1 => [ qw(required) ]
    field_2 => [ qw(required_with:field_1) ]

same

    same(Str $field_name): Bool

Validate that field is exact value as another.

    field_1 => [ qw(required) ]
    field_2 => [ qw(required same:field_1) ]

CUSTOM MESSAGES

To define custom error messages for fields/validators your Validator should implement Role: Dancer2::Plugin::FormValidator::Role::ProfileHasMessages.

    package Validator {
        use Moo;
        with 'Dancer2::Plugin::FormValidator::Role::ProfileHasMessages';

        sub profile {
            return {
                name  => [qw(required)],
                email => [qw(required email)],
            };
        }

        sub messages {
            return {
                name => {
                    required => {
                        en => 'Specify your %s',
                    },
                },
                email => {
                    required => {
                        en => '%s is needed',
                    },
                    email => {
                        en => '%s please use valid email',
                    }
                }
            };
        }
    }

HOOKS

There is hook_before method available, which allows your Profile object to make decisions depending on the input data. You could use it with Moo around modifier:

    around hook_before => sub {
        my ($orig, $self, $profile, $input) = @_;

        # If there is specific input value.
        if ($input->{name} eq 'Secret') {
            # Delete all validators for field 'surname'.
            delete $profile->{surname};
        }

        return $orig->($self, $profile, $input);
    };

EXTENSIONS

Writing custom extensions

You can extend the set of validators by writing extensions:

    package Extension {
        use Moo;
        with 'Dancer2::Plugin::FormValidator::Role::Extension';

        sub validators {
            return {
                is_true  => 'IsTrue',   # Full class name
                email    => 'Email',    # Full class name
                restrict => 'Restrict', # Full class name
            }
        }
    }

Extension should implement Role: Dancer2::Plugin::FormValidator::Role::Extension.

Hint: you could reassign built-in validator with your custom one.

Custom validators:

    package IsTrue {
        use Moo;
        with 'Dancer2::Plugin::FormValidator::Role::Validator';

        sub message {
            return {
                en => '%s is not a true value',
            };
        }

        sub validate {
            my ($self, $field, $input) = @_;

            if (exists $input->{$field}) {
                if ($input->{$field} == 1) {
                    return 1;
                }
                else {
                    return 0;
                }
            }

            return 1;
        }
    }

Validator should implement Role: Dancer2::Plugin::FormValidator::Role::Validator.

Config:

    set plugins => {
        FormValidator => {
            session    => {
                namespace => '_form_validator'
            },
            extensions => {
                extension => {
                    provider => 'Extension',
                }
            }
        },
    };

Extensions modules

There is a set of ready-made extensions available on cpan:

ROLES

  • Dancer2::Plugin::FormValidator::Role::Profile - for profile classes.

  • Dancer2::Plugin::FormValidator::Role::HasMessages - for classes, that implements custom error messages.

  • Dancer2::Plugin::FormValidator::Role::ProfileHasMessages - brings together Profile and HasMassages.

  • Dancer2::Plugin::FormValidator::Role::Extension - for extension classes.

  • Dancer2::Plugin::FormValidator::Role::Validator - for custom validators.

HINTS

If you don't want to create separated classes for your validation logic, you could create one base class and reuse it in your project.

    ### Validator class

    package Validator {
        use Moo;
        with 'Dancer2::Plugin::FormValidator::Role::Profile';

        has profile_hash => (
            is       => 'ro',
            required => 1,
        );

        sub profile {
            return $_[0]->profile_hash;
        }
    }

    ### Application

    use Dancer2

    my $validator = Validator->new(profile_hash =>
        {
            email => [qw(required email)],
        }
    );

    post '/subscribe' => sub {
        if (not validate profile => $validator) {
            to_json errors;
        }
    };

BUGS AND LIMITATIONS

If you find one, please let me know.

SOURCE CODE REPOSITORY

https://github.com/AlexP007/dancer2-plugin-formvalidator.

AUTHOR

Alexander Panteleev <alexpan at cpan dot org>.

LICENSE AND COPYRIGHT

This software is copyright (c) 2022 by Alexander Panteleev. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.