Commit 7579faa8 authored by Avris's avatar Avris

init

parents
Pipeline #6824165 passed with stage
in 15 seconds
tests/_output/*
vendor/*
.idea/*
**/.DS_Store
phpunit:
script:
- composer install --dev
- vendor/bin/phpunit
## Micrus Forms ##
Without a framework, when you want to create a form, 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...
Micrus 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 use it in the controller and display it in the view.
Micrus Forms were created as a part of [Micrus Framework](https://micrus.avris.it), it can however be used independently from it.
However, in order to handle file uploads, you'll need to include the framework.
### Example (using Doctrine and Twig) ###
Definition of a form:
<?php
namespace App\Form;
use App\Model\User;
use Avris\Micrus\Forms\Form;
use Avris\Micrus\Forms\Assert as Assert;
use Avris\Micrus\Forms\Widget as Widget;
use Avris\Micrus\Tool\Security\Crypt;
class RegisterForm extends Form
{
public function configure() {
$this
->add('username', Widget\Text::class, [
'placeholder' => l('entity.User.custom.usernameRegex'),
], [
new Assert\NotBlank(),
new Assert\Regexp('^[A-Za-z0-9_]+$', l('entity.User.custom.usernameRegex')),
new Assert\MinLength(5),
new Assert\MaxLength(25),
new Assert\Unique(
$this->options->get('orm'), $this->object, 'user', 'username',
l('entity.User.custom.usernameTaken')
),
])
->add('email', Widget\Email::class, [], new Assert\NotBlank())
->add('doPasswordsMatch', 'ObjectValidator')
->add('password', 'Password',
[new Assert\NotBlank(), new Assert\MinLength(5)]
)
->add('passwordRepeat', Widget\Password::class, [], new Assert\NotBlank)
->add('agree', 'Checkbox', [
'label' => '',
'sublabel' => l('entity.User.custom.agreement')
], new Assert\NotBlank)
;
}
public function doPasswordsMatch($user)
{
return $user->password === $user->passwordRepeat
? true
: l('entity.User.custom.passwordMismatch');
}
}
Handled by the controller like this:
public function registerAction()
{
if ($this->getUser()) {
return $this->redirectToRoute('account');
}
$form = new RegisterForm(new User(), $this->container);
$form->bind($this->getData());
if ($form->isValid()) {
$user = $form->getObject();
$user->setPassword($this->getService('crypt')->hash($user->getPassword()));
$this->getEm()->persist($user);
$this->getEm()->flush();
$this->getService('securityManager')->login($user);
return $this->redirectToRoute('account');
}
return $this->render(['form' => $form->setStyle(new Bootstrap2)]);
}
And Displayed in the view like that:
<form method="post" class="form">
{{ form.render('Avris\\Micrus\\Forms\\Style\\Bootstrap')|raw }}
<div class="col-lg-offset-2">
<button type="submit" class="btn btn-primary">Register</button>
</div>
</form>
### Widgets ###
The `add($name, $type = 'Text', $options = [], $asserts = [], $visible = true)` 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)*
* TextAddon *(bootstrap)*
* Number
* Integer
* NumberAddon *(bootstrap)*
* Email
* Url
* Hidden
* Textarea
* Checkbox
* Choice
* ButtonChoice *(bootstrap)*
* IconChoice *(bootstrap)*
* Date
* DateTime
* Time
* File
* Password
* Display *(allows custom HTML)*
`Choice` widget has four remarkable options:
* `choices => [key => value, ...]` -- a list of choices to be presented to the user,
* `model => string` -- if specified will generate `choices` as a list of all entities of current type from the database (with their `id`s as keys and `__toString()`s as values) and bind it back to an entity,
* `multiple => bool` -- if user can select one or many options,
* `expanded => bool` -- if Micrus should generate one `select` control or many `checkbox`/`radio` ones.
There are two special widgets:
* CSRF -- a security token added automatically to every form (to disable CSRF protection, add `csrf => false` to the options in form's constructor),
* ObjectValidator -- not a widget, but a placeholder for a validation that requires checking a database (is username free?)
or checking multiple fields (are passwords equal?). It uses whatever `callback` you specify in its options.
This callback should return `true` if no errors found, and an error message otherwise.
And also three more complex widgets:
* MultipleWidget -- handles a list of widgets of a different type,
* SubForm -- includes another form inside your form,
* MultipleSubForm -- includes a list of other forms inside your form.
Example usage:
->add('emails', Widget\MultipleWidget::class, [
'widget' => Widget\Email::class,
'widgetOptions' => [
'placeholder' => 'user@domain.eu',
],
'widgetAsserts' => [
new Assert\NotBlank,
new NewProjectUser($this->getOptions('project')),
],
'add' => true,
'btnAddText' => '<span class="fa fa-plus-circle"></span>',
'remove' => true,
'btnRemoveText' => '<span class="fa fa-trash"></span>',
], [
new Assert\NotBlank(),
new Assert\MinCount(1),
new Assert\MaxCount(10),
])
->add('servers', Widget\MultipleSubForm::class, [
'form' => ServerForm::class,
'model' => Server::class,
'container' => $this->container,
'add' => true,
'btnAddText' => '<span class="fa fa-plus-circle"></span>',
'remove' => true,
'btnRemoveText' => '<span class="fa fa-trash"></span>',
], [
new Assert\UniqueField('name'),
new Assert\NotBlank(),
new Assert\MinCount(1),
new Assert\MaxCount(20),
])
To handle repeatable widgets, to need a JavaScript to handle adding and removing rows. Feel free to use this one (CoffeeScript):
$('body').on 'click', '.form-multiple-add', ->
$form = $(this).parents('.form-multiple')
newIndices = $form.find('[data-index^=new]').map((i, el) -> el.dataset['index'].substr(3)).get()
newIndex = if newIndices.length then Math.max.apply(null, newIndices) + 1 else 0
$template = $($form.find('.form-multiple-add-template').html().replace(/%i%/g, 'new' + newIndex))
$(this).parents('tr').before($template)
$template.find(':input:enabled:visible:first').focus()
$('body').on 'click', '.form-multiple-remove', ->
return false unless confirm(M.l('crud.delete.multiple.confirm'))
$(this).parents('tr').remove()
### Asserts ###
Available asserts are:
* NotBlank
* Email
* Url
* MaxLength
* MinLength
* Regexp
* Number
* Integer
* Min
* Max
* Step
* 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, how exactly should it be rendered.
For instance with `->render(Bootstrap2::class)` you get each widget wrapped into Bootstrap classes with 2 columns for label and 10 for the widget.
Available styles are:
* `Bootstrap`,
* `Bootstrap1`,
* `Bootstrap2`,
* `Bootstrap3`,
* `BootstrapHalf`,
* `BootstrapMini`,
* `BootstrapInline`,
* `BootstrapNoLabel`,
* `BootstrapInlineNoLabel`.
You can create your own by extending class `Avris\Micrus\Forms\Style\Formstyle` taking the existing ones as an example.
### Iterating ###
It's sometimes useful not to display the whole form at once, but with some chunks. For instance:
<form method="post" class="form row">
<div class="col-lg-4">
{% for widget in form.iterate(null, 'widget3') %}
{{ widget|raw }}
{% endfor %}
</div>
<div class="col-lg-4">
{% for widget in form.iterate('widget4', 'widget6') %}
{{ widget|raw }}
{% endfor %}
</div>
<div class="col-lg-4">
{% for widget in form.iterate('widget7') %}
{{ widget|raw }}
{% endfor %}
</div>
</form>
The `->iterate($start, $stop)` function comes in handy here.
If you omit the `$start` argument, it will start iterating from the beginning,
and if you omit `$stop`, it will go to the very last widget.
### Copyright ###
* **Author:** Andrzej Prusinowski [(Avris.it)](https://avris.it)
* **Licence:** [MIT](https://mit.avris.it)
{
"name": "avris/micrus-forms",
"type": "library",
"description": "Forms module for the Micrus Framework",
"license": "MIT",
"homepage": "https://micrus.avris.it",
"authors": [{
"name": "Avris",
"email": "andre@avris.it",
"homepage": "https://avris.it"
}],
"require": {
"avris/micrus-localizator": "^3.0"
},
"require-dev": {
"avris/micrus": "^3.0",
"phpunit/phpunit": "~4.8",
"symfony/var-dumper": "^3.2"
},
"suggest": {
"avris/micrus": "Web framework that this library was created for, provides abstraction for UploadedFile"
},
"autoload": {
"psr-4": { "Avris\\Micrus\\Forms\\": "src" }
},
"autoload-dev": {
"psr-4": { "Avris\\Micrus\\Forms\\": "tests" }
}
}
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/_bootstrap.php">
<testsuites>
<testsuite name="main">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="tests/_output/coverage"/>
<log type="coverage-clover" target="tests/_output/coverage.xml"/>
<log type="coverage-php" target="tests/_output/coverage.serialized"/>
<log type="coverage-text" target="php://stdout" showOnlySummary="true"/>
</logging>
</phpunit>
<?php
namespace Avris\Micrus\Forms\Assert;
abstract class Assert
{
/** @var string */
protected $message;
public function __construct($message = false)
{
$this->message = $message;
}
/**
* @param $value
* @return true|string (returns true when valid, string with error message otherwise)
*/
abstract public function validate($value);
/**
* @return array
*/
public function getHtmlAttributes()
{
return [];
}
/**
* @param string $value
* @return bool
*/
public static function isEmpty($value)
{
return $value === "" || $value === null || $value === false ||
(is_array($value) && !count($value));
}
/**
* @return string
*/
public function getName()
{
return str_replace('\\', '.', preg_replace('#^.*\\\\Assert\\\\(.*)#U', '$1', get_class($this)));
}
/**
* @return array
*/
public function getReplacements()
{
return [];
}
}
<?php
namespace Avris\Micrus\Forms\Assert;
class Choice extends Assert
{
protected $choices;
protected $multiple;
public function __construct($choices, $multiple, $message = false)
{
$this->choices = $choices;
$this->multiple = $multiple;
parent::__construct($message);
}
public function validate($value)
{
if (!$this->multiple && is_array($value)) {
return $this->message;
}
if ($this->multiple && !is_array($value)) {
return $this->message;
}
if (!is_array($value)) {
$value = [$value];
}
$allowed = array_keys($this->choices);
foreach ($value as $singleValue) {
if (!in_array($singleValue, $allowed)) {
return $this->message;
}
}
return true;
}
}
<?php
namespace Avris\Micrus\Forms\Assert;
class Contains extends Assert
{
/** @var string */
protected $required;
public function __construct($required, $message = false)
{
$this->required = $required;
parent::__construct($message);
}
/**
* @param $value
* @return true|string (returns true when valid, string with error message otherwise)
*/
public function validate($value)
{
return mb_strpos($value, $this->required) === false ? $this->message : true;
}
public function getReplacements()
{
return ['%value%' => $this->required];
}
}
<?php
namespace Avris\Micrus\Forms\Assert;
use Avris\Micrus\Model\User\UserInterface;
use Avris\Micrus\Tool\Security\CryptInterface;
use Avris\Micrus\Tool\Security\SecurityManager;
class CorrectPassword extends Assert implements IsRequired
{
/** @var callable */
protected $callback;
/** @var CryptInterface */
protected $crypt;
public function __construct($callback, CryptInterface $crypt, $message = false)
{
$this->callback = $callback;
$this->crypt = $crypt;
parent::__construct($message);
}
public function validate($value)
{
/** @var UserInterface $dbUser */
$dbUser = call_user_func($this->callback);
if (!$dbUser) {
return $this->message;
}
$auths = $dbUser->getAuthenticators(SecurityManager::AUTHENTICATOR_PASSWORD);
foreach ($auths as $auth) {
if ($this->crypt->validate($value, $auth->getPayload())) {
return true;
}
}
return $this->message;
}
}
<?php
namespace Avris\Micrus\Forms\Assert;
class Csrf extends Assert implements IsRequired
{
public function validate($value)
{
return $value === $_SESSION['_csrf'] ? true : $this->message;
}
}
<?php
namespace Avris\Micrus\Forms\Assert;
class Date extends Assert
{
public function validate($value)
{
try {
$obj = new \DateTime($value);
} catch (\Exception $e) {
return $this->message;
}
if ($obj->format('Y-m-d') !== $value) {
return $this->message;
}
return true;
}
public function getHtmlAttributes()
{
return ['pattern="^\d{4}-\d{2}-\d{2}$"'];
}
}
<?php
namespace Avris\Micrus\Forms\Assert;
class DateTime extends Assert
{
public function validate($value)
{
$value = str_replace('T', ' ', $value);
try {
$obj = new \DateTime($value);
} catch (\Exception $e) {
return $this->message;
}
if ($obj->format('Y-m-d H:i') !== $value && $obj->format('Y-m-d H:i:s') !== $value) {
return $this->message;
}
return true;
}
public function getHtmlAttributes()
{
return [
'pattern="^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$"',
];
}
}
<?php
namespace Avris\Micrus\Forms\Assert;
class Email extends Assert
{
public function validate($value)
{
return filter_var($value, FILTER_VALIDATE_EMAIL) ? true : $this->message;
}
}
<?php
namespace Avris\Micrus\Forms\Assert\File;
use Avris\Micrus\Controller\Http\UploadedFile;
use Avris\Micrus\Forms\Assert\Assert;
class Extension extends Assert
{
protected $extension;
public function __construct($extension, $message = false)
{
$this->extension = is_array($extension) ? $extension : [$extension];
$this->message = $message;
}
public function validate($value)
{
/** @var UploadedFile $value */
return in_array($value->getExtension(), $this->extension)
? true
: $this->message;
}
}
<?php
namespace Avris\Micrus\Forms\Assert\File;
use Avris\Micrus\Controller\Http\UploadedFile;
use Avris\Micrus\Forms\Assert\Assert;
class File extends Assert
{
public function __construct($message = false)
{
$this->message = $message;
}
public function validate($value)
{
return $value instanceof UploadedFile && !$value->getError()
? true
: $this->message;
}
}
<?php
namespace Avris\Micrus\Forms\Assert\File;
use Avris\Micrus\Controller\Http\UploadedFile;
use Avris\Micrus\Forms\Assert\Assert;
class Image extends Assert
{
public function __construct($message = false)
{
$this->message = $message;
}
public function validate($value)
{
/** @var UploadedFile $value */
return is_numeric(exif_imagetype($value->getTmpName()))
? true
: $this->message;
}
}
<?php
namespace Avris\Micrus\Forms\Assert\File;
use Avris\Micrus\Controller\Http\UploadedFile;
use Avris\Micrus\Forms\Assert\Assert;
class MaxHeight extends Assert
{
protected $max;
public function __construct($max, $message = false)
{
$this->max = $max;
$this->message = $message;
}
public function validate($value)
{
/** @var UploadedFile $value */
$imgsize = getimagesize($value->getTmpName());
return $imgsize[1] > $this->max ? $this->message : true;
}
public function getReplacements()
{
return ['%value%' => $this->max];
}
}
<?php
namespace Avris\Micrus\Forms\Assert\File;