avris/forms

Simple forms abstraction

v4.0.1 2018-01-27 23:34 UTC

This package is auto-updated.

Last update: 2021-03-29 00:41:42 UTC


README

Forms are complicated. There are many things you must take into consideration: binding an existing object (if any) to each sparate field of the form, validating them after user has submited the form, if invalid redisplaying it with POST data bound and with validation errors, binding the data back to an object...

Avris Forms add an abstraction layer that handles all of that. You just need to define the list of fields you need and their cofiguration options. You'll get an object that will handle everything for you. Just handle it in the controller and display it in the view.

Installation

composer require avris/forms

Configuration

The dependencies are handled with a DI container. You can either configure it yourself or use a built-in DI builder:

$formsDir = $projectDir . '/vendor/avris/forms';
$locale = 'en';

$builder = (new LocalisatorBuilder())
    ->registerExtension(new LocalisatorExtension($locale, [$formsDir . '/translations']))
    ->registerExtension(new FormsExtension(
        [
           UserForm::class => [],
           PostForm::class => ['@logger'],
        ],
        [CustomWidget::class => []]
    ));

$localisator = $builder->build(LocalisatorInterface::class);

$loader = new FilesystemLoader([$projectDir . '/templates', $formsDir . '/templates']);
self::$twig = new Environment($loader, [
    'cache' => __DIR__ . '/_output/cache',
    'debug' => true,
    'strict_variables' => true,
]);
self::$twig->addExtension(new FormRenderer());
self::$twig->addExtension(new LocalisatorTwig($localisator));

$widgetFactory = $builder->build(WidgetFactory::class);

Take a note that all the forms and widgets are services, so you can inject dependencies to them in a simple way.

Twig is necessary for rendering the form's HTML. It expects has to have a l(string $key, array $replacements) function for translations. You can either provide it yourself, or use Avris Localisator as shown above.

Usage

Definition of a form:

<?php
namespace App\Form;

use App\Entity\User;
use Avris\Forms\Assert as Assert;
use Avris\Forms\Security\CsrfProviderInterface;
use Avris\Forms\Widget as Widget;
use Avris\Forms\Form;
use Avris\Forms\WidgetFactory;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;

class RegisterForm extends Form
{
    /** @var EntityRepository */
    private $repo;

    // inject dependencies as with any other service
    public function __construct(
        WidgetFactory $widgetFactory,
        CsrfProviderInterface $csrfProvider,
        EntityManagerInterface $em
    ) {
        parent::__construct($widgetFactory, $csrfProvider);
        $this->repo = $em->getRepository(User::class);
    }

    public function configure()
    {
        $this
            ->add('username', Widget\Text::class, [
                'placeholder' => 'Username can only contain latin letters and numbers',
            ], [
                new Assert\NotBlank(),
                new Assert\Regexp('^[A-Za-z0-9]+$', 'Username can only contain latin letters and numbers')),
                new Assert\MinLength(5),
                new Assert\MaxLength(25),
                new Assert\Callback(
                    function (string $value) {
                        return !$this->repo->findOneBy(['username' => $value]);
                    },
                    'Username already exists in the database'
                ),
            ])
            ->add('email', Widget\Email::class, [], [
                new Assert\NotBlank(),
                new Assert\Callback(
                    function (string $value) {
                        return !$this->repo->findOneBy(['email' => $value]);
                    },
                    'Email already exists in the database'
                ),
            ])
            ->add('password', Widget\Password::class, [], [
                new Assert\NotBlank(),
                new Assert\MinLength(5),
                new Assert\Callback(
                    function () {
                        return $this->getData()['password'] === $this->getData()['passwordRepeat'];
                    },
                    'Passwords don\'t match' 
                )
            ])
            ->add('passwordRepeat', Widget\Password::class, [], new Assert\NotBlank)
            ->add('source', Widget\Choice::class, [
                'choices' => [
                    'facebook' => 'Facebook',
                    'twitter' => 'Twitter',
                    'friend' => 'Friend',
                ],
                'add_empty' => true,
            ])
            ->add('agree', Widget\Checkbox::class, [
                'label' => '',
                'sublabel' => 'I agree to the terms and conditions')
            ], new Assert\NotBlank)
        ;
    }

    // just a custom helper
    public function getPassword()
    {
        return $this->getData()['password'];
    }

    // if not specified, will fallback to the short class name of the object it's bound to
    // it's useful to specify it, if you have multiple X-related forms on the same page (like Login and Register)
    public function getName(): string
    {
        return 'Register';
    }
}

Using the form:

public function registerAction(WidgetFactory $widgetFactory, EntityManagerInterface $em, RequestInterface $request)
{
    $form = $widgetFactory->build(RegisterForm::class);
    $form->bindObject(new User());
    
    $form->bindRequest($request); 
    if ($form->isValid()) {
        $user = $form->buildObject();
        $em->persist($post);
        $em->flush();

        return $this->redirectToRoute('myaccount');
    }

    return $this->render('User/register.html.twig', ['form' => $form]);
}

And Displayed in the view like that:

<form method="post" class="form">
    {{ widget(form, form.name, 'Avris\\Forms\\Style\\Bootstrap') }}
    <div class="form-group">
        <button type="submit" class="btn btn-block btn-primary">Log in</button>
    </div>
</form>

For more examples, see the Micrus Demo Project.

Widgets

The add(s tring$name, string $type = Widget\Text::class, array $options = [], array $asserts = []) method lets you add a field to your form. $name parameter must be unique because it will be the name of object's property. $type is a string that defines which widget should be used:

  • Text (default)
  • Number
  • Integer
  • Email
  • Url
  • Hidden
  • Password
  • Textarea
  • Checkbox, options:
    • sublabel -- text to display next to the checkbox
  • Date
  • DateTime
  • Time
  • Choice -- options:
    • choices => [key => value, ...] (required) -- a list of choices to be presented to the user; it can be either a simple key-value list, or an array of objects -- in the second case, Avris Forms will preserve those objects, but you need to provide a way to map them onto a key-value list (for keys: option keys, for values: option labels or method __toString)
    • add_empty => bool (default: false)
    • multiple => bool (default: false) -- if user can select one or many options,
    • expanded => bool (default: false) -- if one select control or many checkbox/radio ones should be used,
    • keys => callback (optional, required if choices is an array of objects) -- mapping an object to a unique string identifier, for instance: function (User $user) { return $user->getId(); }
    • labels => callback (optional) -- mapping an object to its text representation (if not given, __toString will be used).
  • File
  • Custom -- allows custom HTML, doesn't bind anything to the object; options:
    • template (required) - the name of a Twig template to be displayed
  • Csrf -- CSRF protection, added automatically to every form, unless it's created with option csrf = false.
  • Multiple -- an array of values represented as a table of subforms; options:
    • widget (required) -- classname of the single subform
    • lineStyle (default: Avris\Forms\Style\BootstrapInlineNoLabel)
    • add => bool (default: true)
    • remove => bool|callback (default: true)
    • element_prototype
    • element_options
    • element_asserts
  • TextAddon (bootstrap), options:
    • before (optional)
    • after (optional)
  • NumberAddon (bootstrap), options like TextAddon

Note that since forms as widgets as well, you can easily nest form inside a different one (either directly for 1-to-1 relations, or using Multiple widget for 1-to-many relations).

Options for all widgets:

  • label
  • class
  • attr -- array of HTML attributes
  • helper
  • readonly
  • default -- default data
  • prototype -- default object to be bound

Custom widgets

To create your own widget:

  • create a class extending Avris\Forms\Widget\Widget that contains all the logic (data transformations)
  • register it in the DI
  • create a corresponding Twig template, for instance MyApp\Widget\Foo class should have a Form/MyApp/Widget/Foo.html.twig template.

Asserts

Available asserts are:

  • NotBlank
  • Email
  • Url
  • MaxLength
  • MinLength
  • Regexp
  • Number
  • Integer
  • Min
  • Max
  • Step
  • MinCount
  • MaxCount
  • Date
  • DateTime
  • Time
  • MinDate
  • MaxDate
  • ObjectValidator
  • CorrectPassword
  • Choice
  • Csrf
  • MinCount
  • MaxCount
  • File\File
  • File\Image
  • File\Extension
  • File\Type
  • File\MaxHeight
  • File\MinHeight
  • File\MaxWidth
  • File\MinWidth
  • File\MaxSize
  • File\MaxRatio

Many widgets automatically add a relevant assert, so you don't have to.

Styles

When rendering a form to HTML, you can specify a style. For instance with `{{ widget(form, form.name, 'Avris\Forms\Style\Bootstrap2') }} you get each widget wrapped into Bootstrap classes with 2 columns for label and 10 for the widget.

Built-in styles are:

  • DefaultStyle
  • Bootstrap
  • Bootstrap1
  • Bootstrap2
  • Bootstrap3
  • BootstrapHalf
  • BootstrapInline
  • BootstrapInlineNoLabel
  • BootstrapMini
  • BootstrapNoLabel

You can create your own by implementing Avris\Forms\Style\FormStyleInterface.

Framework integration

Micrus

Although Avris Forms can be used independently from it, they were originally created as a part of Micrus Framework. Therefore the integration with this framework is really simple. Just register the module in your App\App:registerModules:

yield new \Avris\Forms\FormsModule;

You can use helpers in your controller:

/**
 * @M\Route("/add", name="postAdd")
 * @M\Route("/{uuid:id}/edit", name="postEdit")
 */
public function formAction(EntityManagerInterface $em, RequestInterface $request, Post $post = null)
{
    if (!$post) {
        $post = new Post($this->getUser());
    }

    $form = $this->form(PostForm::class, $post, $request); // creates a form and binds the object and the request 

    if ($this->handleForm($form, $post)) { // validates the form and if valid, binds the data back to $post
        $post->handleFileUpload($this->getProjectDir());
        $em->persist($post);
        $em->flush();

        return $this->redirectToRoute('postRead', ['id' => $post->getId()]);
    }

    return $this->render(['form' => $form], 'Post/form');
}

Copyright