avris / forms
Simple forms abstraction
Requires
- avris/container: ^1.0
- avris/http: ^4.0
- avris/localisator: ^4.0
- twig/twig: ^2.4
Requires (Dev)
- avris/micrus: ^4.0-dev
- phpunit/phpunit: ^6.4
- squizlabs/php_codesniffer: ^3.2
- symfony/var-dumper: ^4.0
Suggests
- avris/micrus: Web framework that this library was created for
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: optionkeys
, for values: optionlabels
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 oneselect
control or manycheckbox
/radio
ones should be used,keys => callback
(optional, required ifchoices
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 optioncsrf = false
.Multiple
-- an array of values represented as a table of subforms; options:widget
(required) -- classname of the single subformlineStyle
(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 likeTextAddon
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 attributeshelper
readonly
default
-- default dataprototype
-- 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 aForm/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
- Author: Andre Prusinowski (Avris.it)
- Licence: MIT