Forms/src/FuzeWorks/Forms/Form.php

547 lines
14 KiB
PHP
Executable File

<?php
/**
* FuzeWorks Forms Library
*
* The FuzeWorks PHP FrameWork
*
* Copyright (C) 2013-${YEAR} i15
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* @author i15
* @copyright Copyright (c) 2013 - ${YEAR}, i15. (http://i15.nl)
* @license https://opensource.org/licenses/MIT MIT License
*
* @link https://i15.nl
* @since Version 1.3.0
*
* @version Version 1.3.0
*/
namespace FuzeWorks\Forms;
use FuzeWorks\Events;
use FuzeWorks\Factory;
use FuzeWorks\Forms\Fields\HiddenField;
use FuzeWorks\Forms\Fields\SubmitField;
use FuzeWorks\Layout;
use FuzeWorks\Security;
class Form
{
/**
* The name of the form
*
* @var string
*/
protected string $name;
/**
* The name to display to the user
*
* @var string
*/
protected string $label;
/**
* Array of all Fields that this Form possesses.
*
* @var Field[]
*/
protected array $fields = [];
/**
* An associate array of all errors generated by fields of the form.
*
* Saved as 'fieldName' => ['error1', 'error2']
*
* @var array
*/
protected array $fieldErrors = [];
/**
* An iterative array of all errors belonging to the form as a whole.
*
* Saved as ['error1', 'error2']
*
* @var array
*/
protected array $formErrors = [];
/**
* An iterative array of all errors belonging to the form as a whole.
*
* Saved as ['error1', 'error2']
*
* @var array
*/
protected array $formWarnings = [];
/**
* The HTTP method to use to perform this form
*
* @var Method
*/
protected Method $method = Method::POST;
/**
* The url to send this form to. May be null to send to same page.
*
* @var string|null
*/
protected ?string $action = null;
/**
* CSS classnames to add to the <form> element
*
* @var array
*/
protected array $classNames = [];
/**
* Cross site request forgery prevention token. Taken care of internally.
*
* @var string|null
*/
protected ?string $csrfToken = null;
/**
* The source where values of fields are loaded from
*
* @var callable
*/
private $callable = null;
/**
* Whether the form has been validated
*
* @var bool
*/
protected bool $validated = false;
/**
* Whether, after validation, all fields pass their requirements
*
* @var bool
*/
protected bool $valid = false;
/**
* Conditions that verify the entire Form.
*
* Used for customization. Callables must return a bool.
*
* @var callable[]
*/
protected array $conditions = [];
public function __construct(string $name, string $label = "")
{
$this->name = $name;
$this->label = empty($label) ? $name : $label;
// Add CSRF token
/** @var Security $security */
$security = Factory::getInstance("security");
$hash = $security->get_csrf_hash();
if (!is_null($hash)) {
$this->csrfToken = $security->get_csrf_token_name();
$field = new HiddenField($this->csrfToken);
$field->setValue($hash);
$this->field($field);
}
}
/**
* Add a field to the form
*
* @param Field $field
* @return $this
*/
public function field(Field $field): static
{
// Set the form name
$field->setFormName($this->name);
// Add the field
$this->fields[] = $field;
// Return
return $this;
}
/**
* Return all fields
*
* @return Field[]
*/
public function getFields(): array
{
return $this->fields;
}
/**
* Return the field with a specific name.
*
* Returns null if not found.
*
* @param string $name
* @return Field|null
*/
public function getField(string $name): ?Field
{
// Find field by name and return it
foreach ($this->fields as $field)
if ($field->getName() == $name)
return $field;
return null;
}
/**
* Return the csrf token hidden field
*
* @return Field|null
*/
public function getCsrfField(): ?Field
{
if (is_null($this->csrfToken))
return null;
return $this->getField($this->csrfToken);
}
/**
* Set the method for this form.
*
* @param Method $method
* @return $this
*/
public function method(Method $method): static
{
$this->method = $method;
return $this;
}
/**
* Manually set the target of this form
*
* @param string $action
* @return $this
*/
public function action(string $action): static
{
$this->action = $action;
return $this;
}
/**
* Adds the CSS classnames to be printed inside the element.
*
* @param array $classNames
* @return $this
*/
public function class(array $classNames): static
{
foreach ($classNames as $className)
$this->classNames[] = $className;
return $this;
}
/**
* Add one CSS classname to be printed inside the element
*
* @param string $class
* @return $this
*/
public function addClass(string $class): static
{
$this->classNames[] = $class;
return $this;
}
/**
* Sets the CSS classnames to be printed inside the element
*
* @param array $classNames
* @return $this
*/
public function setClasses(array $classNames): static
{
$this->classNames = $classNames;
return $this;
}
/**
* Sets the source where values of fields are loaded from.
*
* The source is a callable which must accept func(string $fieldName) and returns the value or null.
*
* @param callable $callable
* @return $this
*/
public function setSource(callable $callable): static
{
$this->callable = $callable;
return $this;
}
public function validate(): bool
{
// First check if any values have been set
$found = false;
foreach ($this->fields as $field) {
$value = call_user_func($this->getCallable(), $field->getName());
if (!is_null($value))
$found = true;
}
// If not a single value has been set, return false but don't set the form as validated.
if (!$found)
return false;
// Pass over each field
$this->fieldErrors = [];
$this->validated = true;
$this->valid = true;
foreach ($this->fields as $field) {
// First set the value of each field
$value = call_user_func($this->getCallable(), $field->getName());
if (!is_null($value))
$field->setValue($value);
// Then validate it
if (!$field->validate()) {
$this->valid = false;
$this->fieldErrors[$field->getName()] = $field->getErrors();
}
}
// Check for conditions
foreach ($this->conditions as $condition)
if (!call_user_func($condition, $this))
$this->valid = false;
return $this->valid;
}
/**
* Whether the form has been validated.
*
* @see validate
* @return bool
*/
public function isValidated(): bool
{
return $this->validated;
}
/**
* Whether the form has met all conditions during validation
*
* @return bool
*/
public function isValid(): bool
{
return $this->valid;
}
/**
* Change the valid status of the form to false, if so required by post-checks.
*
* @param bool $fields Whether to invalidate all fields as well
* @return void
*/
public function invalidate(bool $fields = false): void
{
$this->valid = false;
if ($fields)
foreach ($this->fields as $field)
$field->invalidate();
}
/**
* Retrieve an associative value array of errors generated by fields, separated by fieldNames.
*
* @see Field::getErrors()
* @return array
*/
public function getFieldErrors(): array
{
return $this->fieldErrors;
}
/**
* Retrieve an iterative array of errors belonging to the form as a whole
*
* @return array
*/
public function getFormErrors(): array
{
return $this->formErrors;
}
/**
* Add an error belonging to the form as a whole
*
* @param string $formError
* @return void
*/
public function addFormError(string $formError): void
{
$this->formErrors[] = $formError;
}
/**
* Retrieve an iterative array of warnings belonging to the form as a whole
*
* @return array
*/
public function getFormWarnings(): array
{
return $this->formWarnings;
}
/**
* Add a warning belonging to the form as a whole
*
* @param string $formWarning
* @return void
*/
public function addFormWarning(string $formWarning): void
{
$this->formWarnings[] = $formWarning;
}
/**
* Retrieve the name of the form, used as an identifier.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Set the name of the form, used as an identifier.
*
* @param string $name
*/
public function setName(string $name): void
{
$this->name = $name;
}
/**
* Get the label of the form, used as the name presented to the user.
*
* @return string
*/
public function getLabel(): string
{
return $this->label;
}
/**
* Set the label of the form, used as the name presented to the user.
*
* @param string $label
*/
public function setLabel(string $label): void
{
$this->label = $label;
}
/**
* Adds a manual condition to the field that it will be validated against.
*
* Callables shall receive the Form as their first argument, and must return a bool.
*
* @param callable $condition
* @return $this
*/
public function condition(callable $condition): static
{
$this->conditions[] = $condition;
return $this;
}
/**
* Generate the HTML for this form. Defaults to the internal 'bootstrap' layout for generation.
*
* @param string $layoutFile
* @return string
*/
public function generate(string $layoutFile): string
{
/** @var Layout $layouts */
$layouts = Factory::cloneInstance("layouts");
$layouts->reset();
$layouts->assign("form", $this);
return $layouts->get($layoutFile);
}
/**
* Generate only the attributes that go into the <form> element.
*
* Useful when manually creating a form layout
*
* @return string
*/
public function generateHtmlFormAttributes(): string
{
$action = !is_null($this->action) ? "action='".$this->action."'" : "";
$method = $this->method == Method::POST ? "method='post'" : "";
$class = "class='".implode(" ", $this->classNames)."'";
return "$class $action $method";
}
public function __toString()
{
return $this->generate("form/bootstrap");
}
public function __get(string $name): Field
{
return $this->getField($name);
}
/**
* Get the current callable for retrieving the values of fields
*
* @return callable
*/
private function getCallable(): callable
{
// If the current callable is manually set, use that one
if (!is_null($this->callable))
return $this->callable;
// If no callable is set, use the callable from web input
$input = Factory::getInstance("input");
if ($this->method == Method::POST)
return [$input, 'post'];
else
return [$input, 'get'];
}
}