ipl: add ipl\Html\Form prototype

This commit is contained in:
Thomas Gelf 2018-06-07 23:32:39 +02:00
parent 450f40b402
commit 35c2c034fd
15 changed files with 1179 additions and 0 deletions

294
library/vendor/ipl/Html/Form.php vendored Normal file
View File

@ -0,0 +1,294 @@
<?php
namespace dipl\Html;
use dipl\Html\FormElement\BaseFormElement;
use dipl\Html\FormElement\FormElementContainer;
use dipl\Html\FormElement\SubmitElement;
use dipl\Validator\MessageContainer;
use Icinga\Web\Request;
use InvalidArgumentException;
class Form extends BaseHtmlElement
{
use FormElementContainer;
use MessageContainer;
protected $tag = 'form';
protected $action;
protected $method;
/** @var SubmitElement */
protected $submitButton;
/** @var BaseHtmlElement|null */
protected $defaultElementDecorator;
/**
* @var BaseFormElement[]
*/
private $elements = [];
private $populatedValues = [];
/** @var Request TODO: nonono */
private $request;
private $isValid;
/**
* @param \Icinga\Web\Request $request
* @return $this
*/
public function setRequest($request)
{
$this->request = $request;
if ($this->getAction() === null) {
$this->setAction($request->getUrl());
}
return $this;
}
/**
* @return Request|null
*/
public function getRequest()
{
return $this->request;
}
/**
* @param Request $request
* @return $this
*/
public function handleRequest(Request $request)
{
$this->setRequest($request);
if ($this->hasBeenSent()) {
$this->populate($request->getParams());
}
$this->ensureAssembled();
if ($this->hasBeenSubmitted()) {
if ($this->isValid()) {
$this->onSuccess();
} else {
$this->onError();
}
} elseif ($this->hasBeenSent()) {
$this->validatePartial();
}
return $this;
}
public function onSuccess()
{
// $this->redirectOnSuccess();
}
public function onError()
{
/**
$error = Html::tag('p', ['class' => 'error'], 'ERROR: ');
foreach ($this->getElements() as $element) {
foreach ($element->getMessages() as $message) {
$error->add(sprintf('%s: %s', $element->getName(), $message));
}
}
$this->add($error);
*/
}
// TODO: onElementRegistered
public function onRegisteredElement($name, BaseFormElement $element)
{
if ($element instanceof SubmitElement && ! $this->hasSubmitButton()) {
$this->setSubmitButton($element);
}
if (array_key_exists($name, $this->populatedValues)) {
$element->setValue($this->populatedValues[$name]);
}
}
public function isValid()
{
if ($this->isValid === null) {
$this->validate();
}
return $this->isValid;
}
public function validate()
{
$valid = true;
foreach ($this->elements as $element) {
if ($element->isRequired() && ! $element->hasValue()) {
$element->addMessage('This field is required');
$valid = false;
continue;
}
if (! $element->isValid()) {
$valid = false;
}
}
$this->isValid = $valid;
}
public function validatePartial()
{
foreach ($this->getElements() as $element) {
if ($element->hasValue()) {
$element->validate();
}
}
}
public function getValue($name, $default = null)
{
if ($this->hasElement($name)) {
return $this->getElement($name)->getValue();
} else {
return $default;
}
}
public function getValues()
{
$values = [];
foreach ($this->getElements() as $element) {
if (! $element->isIgnored()) {
$values[] = $element->getValue();
}
}
return $values;
}
/**
* @return bool
*/
public function hasBeenSent()
{
if ($this->request === null) {
return false;
}
if ($this->request->getMethod() !== $this->getMethod()) {
return false;
}
// TODO: Check form name element
return true;
}
public function getSuccessUrl()
{
return $this->getAction();
}
public function redirectOnSuccess()
{
$this->request->getResponse()->redirectAndExit($this->getSuccessUrl());
}
/**
* @return bool
*/
public function hasBeenSubmitted()
{
if ($this->hasSubmitButton()) {
return $this->getSubmitButton()->hasBeenPressed();
} else {
return $this->hasBeenSent();
}
}
public function getSubmitButton()
{
return $this->submitButton;
}
public function hasSubmitButton()
{
return $this->submitButton !== null;
}
public function setSubmitButton(SubmitElement $element)
{
$this->submitButton = $element;
return $this;
}
public function populate($values)
{
foreach ($values as $name => $value) {
$this->populatedValues[$name] = $value;
if ($this->hasElement($name)) {
try {
$element = $this->getElement($name);
} catch (InvalidArgumentException $exception) {
// This will not happen, as we checked for hasElement
}
$element->setValue($value);
}
}
}
/**
* @return mixed
*/
public function getMethod()
{
$method = $this->getAttributes()->get('method')->getValue();
if ($method === null) {
// WRONG. Problem:
// right now we get the method in assemble, that's too late.
// TODO: fix this via getMethodAttribute callback
return 'POST';
}
return $method;
}
/**
* @param $method
* @return $this
*/
public function setMethod($method)
{
$this->getAttributes()->set('method', strtoupper($method));
return $this;
}
/**
* @return string
*/
public function getAction()
{
return $this->getAttributes()->get('action')->getValue();
}
/**
* @param $action
* @return $this
*/
public function setAction($action)
{
$this->getAttributes()->set('action', $action);
return $this;
}
}

View File

@ -0,0 +1,134 @@
<?php
namespace dipl\Html\FormDecorator;
use dipl\Html\BaseHtmlElement;
use dipl\Html\FormElement\BaseFormElement;
use dipl\Html\Html;
use dipl\Html\HtmlDocument;
class DdDtDecorator extends BaseHtmlElement
{
protected $tag = 'dl';
protected $dt;
protected $dd;
/** @var HtmlDocument */
protected $wrapped;
protected $ready = false;
/**
* @param HtmlDocument $document
* @return static
*/
public function wrap(HtmlDocument $document)
{
// TODO: ignore hidden
$newWrapper = clone($this);
$newWrapper->wrapped = $document;
$document->addWrapper($newWrapper);
return $newWrapper;
}
protected function renderLabel()
{
if ($this->wrapped instanceof BaseFormElement) {
$label = $this->wrapped->getLabel();
if (strlen($label)) {
return Html::tag('label', null, $label);
}
}
return null;
}
public function XXrenderAttributes()
{
// TODO: only when sent?!
if ($this->wrapped instanceof BaseFormElement) {
if (! $this->wrapped->isValid()) {
$this->getAttributes()->add('class', 'errors');
}
}
return parent::renderAttributes();
}
protected function renderDescription()
{
if ($this->wrapped instanceof BaseFormElement) {
$description = $this->wrapped->getDescription();
if (strlen($description)) {
return Html::tag('p', ['class' => 'description'], $description);
}
}
return null;
}
protected function renderErrors()
{
if ($this->wrapped instanceof BaseFormElement) {
$errors = [];
foreach ($this->wrapped->getMessages() as $message) {
$errors[] = Html::tag('p', ['class' => 'error'], $message);
}
if (! empty($errors)) {
return $errors;
}
}
return null;
}
public function add($content)
{
if ($content !== $this->wrapped) {
parent::add($content);
}
return $this;
}
/**
* @throws \Icinga\Exception\IcingaException
*/
protected function assemble()
{
$this->add([$this->dt(), $this->dd()]);
$this->ready = true;
}
public function dt()
{
if ($this->dt === null) {
$this->dt = Html::tag('dt', null, $this->renderLabel());
}
return $this->dt;
}
/**
* @return \dipl\Html\HtmlElement
* @throws \Icinga\Exception\IcingaException
* @throws \Icinga\Exception\ProgrammingError
*/
public function dd()
{
if ($this->dd === null) {
$this->dd = Html::tag('dd', null, [
$this->wrapped,
$this->renderErrors(),
$this->renderDescription()
]);
}
return $this->dd;
}
}

View File

@ -0,0 +1,279 @@
<?php
namespace dipl\Html\FormElement;
use dipl\Html\Attribute;
use dipl\Html\BaseHtmlElement;
use dipl\Validator\MessageContainer;
use dipl\Validator\ValidatorInterface;
use InvalidArgumentException;
abstract class BaseFormElement extends BaseHtmlElement
{
use MessageContainer;
/** @var string */
protected $name;
/** @var mixed */
protected $value;
/** @var string */
protected $description;
/** @var string */
protected $label;
/** @var null|bool */
protected $isValid;
/** @var bool */
protected $required = false;
/** @var bool */
protected $ignored = false;
/** @var ValidatorInterface[] */
protected $validators = [];
// TODO: Validators, errors, errorMessages()
/**
* Link constructor.
* @param $name
* @param $value
* @param \dipl\Html\Attributes|array|null $attributes
*/
public function __construct($name, $attributes = null)
{
$this->getAttributes()
->registerAttributeCallback('label', [$this, 'getNoAttribute'], [$this, 'setLabel'])
->registerAttributeCallback('name', [$this, 'getNameAttribute'], [$this, 'setName'])
->registerAttributeCallback('value', [$this, 'getValueAttribute'], [$this, 'setValue'])
->registerAttributeCallback('description', [$this, 'getNoAttribute'], [$this, 'setDescription'])
->registerAttributeCallback('validators', null, [$this, 'setValidators'])
->registerAttributeCallback('required', [$this, 'getRequiredAttribute'], [$this, 'setRequired']);
if ($attributes !== null) {
$this->addAttributes($attributes);
}
$this->setName($name);
}
/**
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* @param string $description
* @return $this
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* @return string
*/
public function getLabel()
{
return $this->label;
}
/**
* @param string $label
* @return $this
*/
public function setLabel($label)
{
$this->label = $label;
return $this;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $name
* @return $this
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* @param $value
* @return $this
*/
public function setValue($value)
{
$this->value = $value;
$this->isValid = null;
return $this;
}
public function isRequired()
{
return $this->required;
}
public function setRequired($required = true)
{
$this->required = (bool) $required;
return $this;
}
public function isIgnored()
{
return $this->ignored;
}
public function setIgnored($ignored = true)
{
$this->required = (bool) $ignored;
return $this;
}
/**
* @return Attribute|string
*/
public function getNameAttribute()
{
return $this->getName();
}
/**
* @return mixed
*/
public function getValueAttribute()
{
return $this->getValue();
}
/**
* @return null
*/
public function getNoAttribute()
{
return null;
}
/**
* @return null|Attribute
*/
public function getRequiredAttribute()
{
if ($this->isRequired()) {
return new Attribute('required', true);
}
return null;
}
/**
* @return ValidatorInterface[]
*/
public function getValidators()
{
return $this->validators;
}
/**
* @param array $validators
*/
public function setValidators(array $validators)
{
$this->validators = [];
foreach ($validators as $validator => $options) {
if (! $validator instanceof ValidatorInterface) {
$validator = $this->createValidator($validator, $options);
}
$this->validators[] = $validator;
}
}
/**
* @param $name
* @param $options
* @return ValidatorInterface
*/
public function createValidator($name, $options)
{
$class = 'ipl\\Validator\\' . ucfirst($name) . 'Validator';
if (class_exists($class)) {
return new $class($options);
} else {
throw new InvalidArgumentException(
'Unable to create Validator: %s',
$name
);
}
}
public function hasValue()
{
$value = $this->getValue();
return $value !== null && $value !== '' && $value !== [];
}
/**
* @return bool
*/
public function isValid()
{
if ($this->isValid === null) {
$this->validate();
}
return $this->isValid;
}
/**
* @return $this
*/
public function validate()
{
$isValid = true;
foreach ($this->getValidators() as $validator) {
if (! $validator->isValid($this->getValue())) {
$isValid = false;
foreach ($validator->getMessages() as $message) {
$this->addMessage($message);
}
}
}
$this->isValid = $isValid;
return $this;
}
}

View File

@ -0,0 +1,157 @@
<?php
namespace dipl\Html\FormElement;
use dipl\Html\BaseHtmlElement;
use InvalidArgumentException;
trait FormElementContainer
{
/** @var BaseFormElement[] */
private $elements = [];
/**
* @return BaseFormElement[]
*/
public function getElements()
{
return $this->elements;
}
/**
* @param $name
* @return BaseFormElement
*/
public function getElement($name)
{
if (! array_key_exists($name, $this->elements)) {
throw new InvalidArgumentException(sprintf(
'Trying to get inexistant element "%s"',
$name
));
}
return $this->elements[$name];
}
/**
* @param string|BaseFormElement $element
* @return bool
*/
public function hasElement($element)
{
if (is_string($element)) {
return array_key_exists($element, $this->elements);
} elseif ($element instanceof BaseFormElement) {
return in_array($element, $this->elements, true);
} else {
return false;
}
}
/**
* @param string $name
* @param string|BaseFormElement $type
* @param array|null $options
* @return $this
*/
public function addElement($name, $type = null, $options = null)
{
$this->registerElement($name, $type, $options);
if ($this instanceof BaseHtmlElement) {
$element = $this->decorate($this->getElement($name));
}
//...
$this->add($element);
return $this;
}
protected function decorate(BaseFormElement $element)
{
if ($this->hasDefaultElementDecorator()) {
$this->getDefaultElementDecorator()->wrap($element);
}
return $element;
}
/**
* @param string $name
* @param string|BaseFormElement $type
* @param array|null $options
* @return $this
*/
public function registerElement($name, $type = null, $options = null)
{
if (is_string($type)) {
$type = $this->createElement($name, $type, $options);
}
$this->elements[$name] = $type;
if (method_exists($this, 'onRegisteredElement')) {
$this->onRegisteredElement($name, $type);
}
return $this;
}
/**
* TODO: Add PluginLoader
*
* @param $name
* @param $type
* @param $options
* @return BaseFormElement
*/
public function createElement($name, $type, $attributes = null)
{
$class = __NAMESPACE__ . '\\' . ucfirst($type) . 'Element';
if (class_exists($class)) {
/** @var BaseFormElement $element */
$element = new $class($name);
if ($attributes !== null) {
$element->addAttributes($attributes);
}
return $element;
} else {
throw new InvalidArgumentException(sprintf(
'Unable to create Form Element, no such type: %s',
$type
));
}
}
/**
* @param FormElementContainer $form
*/
public function addElementsFrom(FormElementContainer $form)
{
foreach ($form->getElements() as $name => $element) {
$this->addElement($element);
}
}
public function setDefaultElementDecorator(BaseHtmlElement $decorator)
{
$this->defaultElementDecorator = $decorator;
return $this;
}
public function hasDefaultElementDecorator()
{
return $this->defaultElementDecorator !== null;
}
/**
* @return BaseHtmlElement
*/
public function getDefaultElementDecorator()
{
return $this->defaultElementDecorator;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace dipl\Html\FormElement;
class HiddenElement extends InputElement
{
protected $type = 'hidden';
}

View File

@ -0,0 +1,35 @@
<?php
namespace dipl\Html\FormElement;
use dipl\Html\Attribute;
abstract class InputElement extends BaseFormElement
{
protected $tag = 'input';
/** @var string */
protected $type;
public function __construct($name, $attributes = null)
{
parent::__construct($name, $attributes);
$this->getAttributes()->registerAttributeCallback('type', [$this, 'getTypeAttribute']);
}
/**
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* @return Attribute
*/
public function getTypeAttribute()
{
return new Attribute('type', $this->getType());
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace dipl\Html\FormElement;
class SelectElement extends BaseFormElement
{
protected $tag = 'select';
/** @var SelectOption[] */
protected $options = [];
public function __construct($name, $attributes = null)
{
parent::__construct($name, $attributes);
$this->getAttributes()->registerAttributeCallback(
'options',
null,
[$this, 'setOptions']
);
}
/**
* @param array $options
* @return $this
*/
public function setOptions(array $options)
{
foreach ($options as $value => $label) {
$this->options[$value] = new SelectOption($value, $label);
}
return $this;
}
protected function assemble()
{
$currentValue = $this->getValue();
foreach ($this->options as $value => $option) {
if ($value == $currentValue) {
$option->getAttributes()->set('selected', true);
}
$this->add($option);
}
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace dipl\Html\FormElement;
use dipl\Html\BaseHtmlElement;
class SelectOption extends BaseHtmlElement
{
protected $tag = 'option';
/** @var mixed */
protected $value;
/**
* SelectOption constructor.
* @param string|null $value
* @param string|null $label
*/
public function __construct($value = null, $label = null)
{
$this->add($label);
$this->getAttributes()->add('value', $value);
}
/**
* @param $label
* @return $this
*/
public function setLabel($label)
{
$this->setContent($label);
return $this;
}
/**
* @return string
*/
public function getValue()
{
return $this->value;
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace dipl\Html\FormElement;
use dipl\Html\Attribute;
class SubmitElement extends InputElement
{
protected $type = 'submit';
protected $buttonLabel;
public function setLabel($label)
{
$this->buttonLabel = $label;
return $this;
}
/**
* @return string
*/
public function getButtonLabel()
{
if ($this->buttonLabel === null) {
return $this->getName();
} else {
return $this->buttonLabel;
}
}
/**
* @return mixed|static
*/
public function getValueAttribute()
{
return new Attribute('value', $this->getButtonLabel());
}
/**
* @return bool
*/
public function hasBeenPressed()
{
return $this->getButtonLabel() === $this->getValue();
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace dipl\Html\FormElement;
class TextElement extends InputElement
{
protected $type = 'text';
}

View File

@ -0,0 +1,10 @@
<?php
namespace dipl\Html\FormElement;
use dipl\Html\BaseHtmlElement;
class TextareaElement extends BaseHtmlElement
{
protected $tag = 'textarea';
}

View File

@ -0,0 +1,38 @@
<?php
namespace dipl\Validator;
use dipl\Translation\TranslationHelper;
class GreaterThanValidator extends SimpleValidator
{
use TranslationHelper;
public function __construct($max)
{
if (is_array($max)) {
parent::__construct($max);
} else {
$this->settings = [
'max' => $max
];
}
}
public function isValid($value)
{
if ($value > $this->getSetting('max', PHP_INT_MAX)) {
$this->clearMessages();
return true;
} else {
$this->addMessage(
$this->translate('%s is not greater than %d'),
$value,
$this->getSetting('max', PHP_INT_MAX)
);
return false;
}
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace dipl\Validator;
trait MessageContainer
{
protected $messages = [];
public function getMessages()
{
return $this->messages;
}
public function addMessage($message)
{
$args = func_get_args();
array_shift($args);
if (empty($args)) {
$this->messages[] = $message;
} else {
$this->messages[] = vsprintf($message, $args);
}
return $this;
}
public function setMessages(array $messages)
{
$this->messages = $messages;
return $this;
}
public function clearMessages()
{
return $this->setMessages([]);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace dipl\Validator;
abstract class SimpleValidator implements ValidatorInterface
{
use MessageContainer;
protected $settings = [];
public function __construct(array $settings = [])
{
$this->settings = $settings;
}
public function getSetting($name, $default = null)
{
if (array_key_exists($name, $this->settings)) {
return $this->settings[$name];
} else {
return $default;
}
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace dipl\Validator;
interface ValidatorInterface
{
/**
* // TODO: @throws \RuntimeException
* @param mixed $value
* @return bool
*/
public function isValid($value);
/**
* @return array
*/
public function getMessages();
}