commit 30f737ccb147b970fe0fb4dde781051c7902202c Author: Abel Hoogeveen Date: Wed Dec 28 10:50:19 2022 +0100 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d6966d7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +.gitattributes export-ignore +.gitignore export-ignore +.drone.yml export-ignore +test/ export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1517f7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +composer.lock +composer.phar +.idea/ +build/ +test/temp/ +vendor/ +test/.phpunit.result.cache \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..fae3185 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2023 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. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100755 index 0000000..83ec6fd --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "fuzeworks/forms", + "license": ["MIT"], + "authors": [ + { + "name": "Abel Hoogeveen", + "email": "abel@i15.nl" + } + ], + "require": { + "php": ">=8.1.0", + "fuzeworks/core": "~1.3.0", + "fuzeworks/mvcr": "~1.3.0", + "fuzeworks/objectstorage": "~1.3.0", + "fuzeworks/layout": "~1.3.0", + "fuzeworks/webcomponent": "~1.3.0" + }, + "require-dev": { + "fuzeworks/tracycomponent": "~1.3.0", + "phpunit/phpunit": "^9" + }, + "autoload": { + "psr-4": { + "FuzeWorks\\Forms\\": "src/FuzeWorks/Forms/" + } + } +} \ No newline at end of file diff --git a/layouts/form/layout.bootstrap.latte b/layouts/form/layout.bootstrap.latte new file mode 100755 index 0000000..a35590a --- /dev/null +++ b/layouts/form/layout.bootstrap.latte @@ -0,0 +1,47 @@ +{varType FuzeWorks\Forms\Form $form} +{varType FuzeWorks\Forms\Field $field} +
generateHtmlFormAttributes()|noescape}> + {if $form->isValidated() && $form->isValid()} +
+
Alert!
+ {$form->getName()} was successfully completed. +
+ {else} +
+
Error!
+ {$error|noescape} +
+
+
Warning!
+ {$warning|noescape} +
+ {foreach $form->getFields() as $field} + {switch get_class($field)} + {case "FuzeWorks\Forms\Fields\HiddenField", "FuzeWorks\Forms\Fields\SubmitField", "FuzeWorks\Forms\Fields\ResetField", "FuzeWorks\Forms\Fields\DecoratorField"} + {$field|noescape} + {case "FuzeWorks\Forms\Fields\CheckboxField"} +
+ {$field->addClass("form-check-input")|noescape} + + {if $field->isValidated() && !$field->isValid()} + {$field->getErrors()|implode} + {/if} +
+ {case "FuzeWorks\Forms\Fields\RadioField"} + {$field|noescape} + {default} +
+ + {if $field->isValidated() && !$field->isValid()} + {$field->class(["form-control", "is-invalid"])|noescape} + {$field->getErrors()|implode} + {elseif $field->isValidated() && $field->isValid()} + {$field->class(["form-control", "is-valid"])|noescape} + {else} + {$field->addClass("form-control")|noescape} + {/if} +
+ {/switch} + {/foreach} + {/if} +
\ No newline at end of file diff --git a/src/FuzeWorks/Forms/Exceptions/FormException.php b/src/FuzeWorks/Forms/Exceptions/FormException.php new file mode 100644 index 0000000..8fd394c --- /dev/null +++ b/src/FuzeWorks/Forms/Exceptions/FormException.php @@ -0,0 +1,43 @@ +name = $name; + } + + /** + * Validates this field against its internal and added conditions. + * + * Returns true if all conditions are met, or false if there are errors. + * If the result is false, a list of errors can be retrieved using the getErrors() method. + * @see getErrors + * + * @return bool + */ + public function validate(): bool + { + // Prepare output variables + $this->errors = []; + $this->validated = true; + $this->valid = true; + + // Check if empty or null + if (empty($this->value)) { + // If the value is empty and not optional, turn it into an error + if (!$this->optional) { + $this->valid = false; + if (!is_null($this->emptyErrorString)) + $this->errors[] = $this->emptyErrorString; + else + $this->errors[] = $this->getLabel() . " may not be empty."; + + return false; + } + + // If the value is empty but also optional, that's perfectly fine. Immediately accept without any further tests. + return true; + } + + // Check validateField + if (!$this->validateField()) + $this->valid = false; + + // Check for conditions + foreach ($this->conditions as $condition) + if (!call_user_func($condition, $this)) + $this->valid = false; + + return $this->valid; + } + + /** + * Whether the field has been validated. + * + * @see validate + * @return bool + */ + public function isValidated(): bool + { + return $this->validated; + } + + /** + * Whether the field has met all conditions during validation + * + * @return bool + */ + public function isValid(): bool + { + return $this->valid; + } + + /** + * Change the valid status of the field to false, if so required by post-checks. + * + * @return void + */ + public function invalidate(): void + { + $this->valid = false; + } + + /** + * Returns the name of the field, as was added with the construction of the field. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Returns the currently set value of the field. + * + * @return mixed + */ + public function getValue(): mixed + { + return $this->value; + } + + /** + * Returns the unique identifier of the field. + * + * If an identifier has been set, it shall return 'formName_fieldIdentifier' + * If no identifier has been set, it shall return 'formName_fieldName' + * + * @return string + */ + public function getId(): string + { + if (is_null($this->identifier)) + return $this->formName . "_" . $this->name; + + return $this->formName . "_" . $this->identifier; + } + + /** + * Returns the label of this field, which is the most human-readable. + * + * If a label has been set, returns that label. + * If no label has been set, returns ucfirst(fieldName). + * + * @return string + */ + public function getLabel(): string + { + return is_null($this->label) ? ucfirst($this->name) : $this->label; + } + + /** + * Returns the note of this field. + * + * Notes are there for the developer to categorize fields, if this is required. + * + * @return string|null + */ + public function getNote(): ?string + { + return $this->note; + } + + /** + * Returns an array of strings with all errors found during validation. + * + * @return string[] + */ + public function getErrors(): array + { + return $this->errors; + } + + /** + * Set the current value of this field, which it may be validated against. + * ALWAYS VERIFY AGAINST XSS ATTACKS!!!!!!!!!! + * + * If field is locked the value will not be updated. + * @see lock + * + * @param mixed $value + * @return $this + */ + public function setValue(mixed $value): static + { + if (!$this->lock) + $this->value = $value; + + return $this; + } + + /** + * Sets the identifier of the field. + * + * @param string $identifier + * @return $this + */ + public function setId(string $identifier): static + { + $this->identifier = $identifier; + return $this; + } + + /** + * Sets the label of the field. + * + * @param string $label + * @return $this + */ + public function setLabel(string $label): static + { + $this->label = $label; + return $this; + } + + /** + * Sets the note of the field. + * + * @param string $note + * @return $this + */ + public function setNote(string $note): static + { + $this->note = $note; + return $this; + } + + /** + * When enabled, locks the field, so it may not be edited anymore. + * + * @param bool $lock + * @return $this + */ + public function lock(bool $lock = true): static + { + $this->lock = $lock; + return $this; + } + + /** + * When enabled, marks the field as optional, so that it may be left empty during validation. + * + * @param bool $opt + * @return $this + */ + public function optional(bool $opt = true): static + { + $this->optional = $opt; + return $this; + } + + /** + * Adds a manual condition to the field that it will be validated against. + * + * @param callable $condition + * @return $this + */ + public function condition(callable $condition): static + { + $this->conditions[] = $condition; + return $this; + } + + /** + * Set the string to be displayed when a value is not set and isn't optional. + * + * @param string $errorString + * @return $this + */ + public function emptyErrorString(string $errorString): static + { + $this->emptyErrorString = $errorString; + return $this; + } + + /** + * Add an error to be displayed on this field. + * + * @param string $errorString + * @return $this + */ + public function addError(string $errorString): static + { + $this->errors[] = $errorString; + 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; + } + + /** + * @internal + */ + public function setFormName(string $formName): void + { + $this->formName = $formName; + } + + /** + * Run a validation test against internal conditions. + * + * Gets called by validate() method + * @see validate + * + * @return bool + */ + protected abstract function validateField(): bool; + + /** + * Generates a html string that will be added to the form. + * + * @return string + */ + public abstract function generateHtml(): string; + + public function __toString() + { + return $this->generateHtml(); + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Forms/Fields/ButtonField.php b/src/FuzeWorks/Forms/Fields/ButtonField.php new file mode 100644 index 0000000..071e204 --- /dev/null +++ b/src/FuzeWorks/Forms/Fields/ButtonField.php @@ -0,0 +1,87 @@ +buttonText = $text; + return $this; + } + + public function onClick(string $text): self + { + $this->onClick = $text; + return $this; + } + + /** + * @inheritDoc + */ + public function generateHtml(): string + { + $id = "id='".$this->getId()."'"; + $name = "name='".$this->getName()."'"; + $value = is_null($this->buttonText) ? "value='".ucfirst($this->getName())."'" : "value='".$this->buttonText."'"; + $class = "class='".implode(" ", $this->classNames)."'"; + $onClick = !is_null($this->onClick) ? "onclick='" . $this->onClick . "'" : ""; + + return ""; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Forms/Fields/CheckboxField.php b/src/FuzeWorks/Forms/Fields/CheckboxField.php new file mode 100755 index 0000000..33961e5 --- /dev/null +++ b/src/FuzeWorks/Forms/Fields/CheckboxField.php @@ -0,0 +1,104 @@ +value = true; + return $this; + } + + public function setValue(mixed $value): static + { + if (!is_bool($value) && !is_null($value)) + $value = true; + + return parent::setValue($value); + } + + /** + * Require this field for validation. + * + * If an errorMessage is provided, it shall be displayed instead of the standard 'checkbox is required'. + * + * @param string|null $errorMessage + * @return $this + */ + public function required(string $errorMessage = null): static + { + $this->optional = false; + if (!is_null($errorMessage)) + $this->emptyErrorString = $errorMessage; + + return $this; + } + + /** + * @inheritDoc + */ + protected function validateField(): bool + { + return true; + } + + /** + * @inheritDoc + */ + public function generateHtml(): string + { + $id = "id='".$this->getId()."'"; + $name = "name='".$this->getName()."'"; + $class = "class='".implode(" ", $this->classNames)."'"; + $value = $this->value ? "checked" : ""; + $lock = $this->lock ? "disabled" : ""; + + return ""; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Forms/Fields/ColorField.php b/src/FuzeWorks/Forms/Fields/ColorField.php new file mode 100644 index 0000000..6406340 --- /dev/null +++ b/src/FuzeWorks/Forms/Fields/ColorField.php @@ -0,0 +1,59 @@ +optional = true; + $this->content = $content; + } + + /** + * @inheritDoc + */ + protected function validateField(): bool + { + return true; + } + + /** + * @inheritDoc + */ + public function generateHtml(): string + { + return $this->content; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Forms/Fields/EmailField.php b/src/FuzeWorks/Forms/Fields/EmailField.php new file mode 100644 index 0000000..9b63235 --- /dev/null +++ b/src/FuzeWorks/Forms/Fields/EmailField.php @@ -0,0 +1,69 @@ +value, FILTER_VALIDATE_EMAIL)) + { + $this->valid = false; + $this->errors[] = $this->getLabel() . " is not a valid email address."; + } + + return $this->valid; + } + + public function generateHtml(): string + { + $id = "id='".$this->getId()."'"; + $name = "name='".$this->getName()."'"; + $value = !is_null($this->value) ? "value='".$this->value."'" : ""; + $lock = $this->lock ? "disabled" : ""; + $placeholder = !is_null($this->placeholder) ? "placeholder='".$this->placeholder."'" : ""; + $maxLength = $this->maxLength > 0 ? "maxlength='".$this->maxLength."'" : ""; + $minLength = $this->minLength > 0 ? "minlength='".$this->minLength."'" : ""; + $class = "class='".implode(" ", $this->classNames)."'"; + + return ""; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Forms/Fields/FileField.php b/src/FuzeWorks/Forms/Fields/FileField.php new file mode 100644 index 0000000..2fef7f3 --- /dev/null +++ b/src/FuzeWorks/Forms/Fields/FileField.php @@ -0,0 +1,42 @@ +getId()."'"; + $name = "name='".$this->getName()."'"; + $value = !is_null($this->value) ? "value='".$this->value."'" : ""; + $class = "class='".implode(" ", $this->classNames)."'"; + + return ""; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Forms/Fields/ImageField.php b/src/FuzeWorks/Forms/Fields/ImageField.php new file mode 100644 index 0000000..98379f8 --- /dev/null +++ b/src/FuzeWorks/Forms/Fields/ImageField.php @@ -0,0 +1,42 @@ +getId()."'"; + $name = "name='".$this->getName()."'"; + $placeholder = !is_null($this->placeholder) ? "placeholder='".$this->placeholder."'" : ""; + $maxLength = $this->maxLength > 0 ? "maxlength='".$this->maxLength."'" : ""; + $minLength = $this->minLength > 0 ? "minlength='".$this->minLength."'" : ""; + $class = "class='".implode(" ", $this->classNames)."'"; + + return ""; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Forms/Fields/RadioField.php b/src/FuzeWorks/Forms/Fields/RadioField.php new file mode 100644 index 0000000..4d3f58d --- /dev/null +++ b/src/FuzeWorks/Forms/Fields/RadioField.php @@ -0,0 +1,137 @@ +options[$value])) + throw new InputException("Could not add value to field. Value already exists."); + + // Add to the values + if (is_null($label)) + $this->options[$value] = $value; + else + $this->options[$value] = $label; + + return $this; + } + + /** + * Bulk add options to the RadioField + * + * @param array $options Associative array [name => label, name => label] + * @return $this + * @throws InputException + */ + public function addOptions(array $options): self + { + foreach ($options as $key => $val) + $this->addOption($key, $val); + + return $this; + } + + /** + * Pre-check one of the options + * + * @param string $optionName Name of the option to check + * @return $this + */ + public function check(string $optionName): self + { + $this->setValue($optionName); + return $this; + } + + public function getOptions(): array + { + return $this->options; + } + + protected function validateField(): bool + { + if (isset($this->options[$this->value])) + { + $this->valid = true; + return $this->valid; + } + + $this->valid = false; + $this->errors[] = $this->getLabel() . " was provided an illegal option."; + return $this->valid; + } + + public function generateHtml(): string + { + $name = "name='".$this->getName()."'"; + $lock = $this->lock ? "disabled" : ""; + $class = "class='".implode(" ", $this->classNames)."'"; + + $out = "
"; + foreach ($this->options as $key => $val) + { + $uni_id = $this->getId() . "_" . $key; + $id = "id='" . $uni_id . "'"; + $value = "value='".$key."'"; + $checked = $key === $this->getValue() ? "checked" : ""; + + $out .= "
"; + $out .= ""; + $out .= ""; + $out .= "
"; + } + + $out .= "
"; + return $out; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Forms/Fields/RangeField.php b/src/FuzeWorks/Forms/Fields/RangeField.php new file mode 100644 index 0000000..22e8913 --- /dev/null +++ b/src/FuzeWorks/Forms/Fields/RangeField.php @@ -0,0 +1,42 @@ +cols = $colAmount; + return $this; + } + + /** + * How many rows the field should have. + * + * @param int $rowAmount + * @return $this + */ + public function rows(int $rowAmount): self + { + $this->rows = $rowAmount; + return $this; + } + + /** + * Which way to wrap text in the TextArea. + * + * Can be TextAreaField::WRAP_HARD or TextAreaField::WRAP_SOFT. + * + * @param int $wrapType + * @return $this + */ + public function wrap(int $wrapType): self + { + $this->wrap = $wrapType; + return $this; + } + + /** + * @inheritDoc + */ + public function generateHtml(): string + { + $id = "id='".$this->getId()."'"; + $name = "name='".$this->getName()."'"; + $lock = $this->lock ? "disabled" : ""; + $placeholder = !is_null($this->placeholder) ? "placeholder='".$this->placeholder."'" : ""; + $maxLength = $this->maxLength > 0 ? "maxlength='".$this->maxLength."'" : ""; + $minLength = $this->minLength > 0 ? "minlength='".$this->minLength."'" : ""; + $rows = $this->rows > 0 ? "rows='".$this->rows."'" : ""; + $cols = $this->cols > 0 ? "cols='".$this->cols."'" : ""; + $wrap = $this->wrap == self::WRAP_HARD ? "wrap='hard'" : "wrap='soft'"; + $class = "class='".implode(" ", $this->classNames)."'"; + + return ""; + + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Forms/Fields/TextField.php b/src/FuzeWorks/Forms/Fields/TextField.php new file mode 100755 index 0000000..95f0682 --- /dev/null +++ b/src/FuzeWorks/Forms/Fields/TextField.php @@ -0,0 +1,146 @@ +maxLength = $maxLength; + return $this; + } + + /** + * Set the minimum length this text must be + * + * @param int $minLength + * @return $this + */ + public function minLength(int $minLength): static + { + $this->minLength = $minLength; + return $this; + } + + /** + * Set a placeholder inside the field for the user to see. + * + * @param string $placeholder + * @return $this + */ + public function placeholder(string $placeholder): static + { + $this->placeholder = $placeholder; + return $this; + } + + /** + * Set a regex to match the text against. + * + * E.g: ^[a-z]+$ for testing if there are only lowercase letters. + * + * The $regexError shown be returned to the user when failing the regex. + * + * @param string $regex + * @param string|null $regexError + * @return $this + */ + public function regex(string $regex, string $regexError = null): static + { + $this->regex = $regex; + $this->regexError = $regexError; + return $this; + } + + protected function validateField(): bool + { + $valid = true; + + // Maximum length test + if ($this->maxLength > 0 && strlen($this->value) > $this->maxLength) { + $valid = false; + $this->errors[] = $this->getLabel() . " exceeds maximum length of " . $this->maxLength . " characters."; + } + + // Minimum length test + if ($this->minLength > 0 && strlen($this->value) < $this->minLength) { + $valid = false; + $this->errors[] = $this->getLabel() . " must at least be " . $this->minLength . " characters."; + } + + // Regex test + if (!is_null($this->regex) && !preg_match('/' . $this->regex . '/mu', $this->value)) { + $valid = false; + if (!is_null($this->regexError)) + $this->errors[] = $this->regexError; + else + $this->errors[] = $this->getLabel() . " contains illegal characters."; + } + + $this->valid = $valid; + return $this->valid; + } + + public function generateHtml(): string + { + $id = "id='".$this->getId()."'"; + $name = "name='".$this->getName()."'"; + $value = !is_null($this->value) ? "value='".$this->value."'" : ""; + $lock = $this->lock ? "disabled" : ""; + $placeholder = !is_null($this->placeholder) ? "placeholder='".$this->placeholder."'" : ""; + $maxLength = $this->maxLength > 0 ? "maxlength='".$this->maxLength."'" : ""; + $minLength = $this->minLength > 0 ? "minlength='".$this->minLength."'" : ""; + $class = "class='".implode(" ", $this->classNames)."'"; + + return ""; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Forms/Fields/TimeField.php b/src/FuzeWorks/Forms/Fields/TimeField.php new file mode 100644 index 0000000..c4a8f71 --- /dev/null +++ b/src/FuzeWorks/Forms/Fields/TimeField.php @@ -0,0 +1,42 @@ + ['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
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 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']; + } + +} \ No newline at end of file diff --git a/src/FuzeWorks/Forms/Forms.php b/src/FuzeWorks/Forms/Forms.php new file mode 100755 index 0000000..a705509 --- /dev/null +++ b/src/FuzeWorks/Forms/Forms.php @@ -0,0 +1,124 @@ +libraryPath = dirname(__DIR__, 3); + + // Add layout component path + /** @var Layout $layouts */ + $layouts = Factory::getInstance('layouts'); + $layouts->addComponentPath($this->libraryPath . DS . 'layouts', Priority::LOWEST); + } + + /** + * Retrieve a new Form object with the given $name and $label. + * + * @param string $name + * @param string $label + * @return Form + */ + public function getForm(string $name, string $label = ""): Form + { + return new Form($name, $label); + } + + /** + * Build and retrieve a Form object, and save the result into Cache to save time on next instantiation. + * + * The $callable must return a Form object. + * + * $name and $label will be passed to the Form constructor. + * + * @param callable $callable + * @param string $name + * @param string $label + * @return Form + */ + public function getCachedForm(callable $callable, string $name, string $label = ""): Form + { + // Fetch storage component + /** @var ObjectStorageComponent $storage */ + $storage = Factory::getInstance('storage'); + + // Fetch object storage + /** @var ObjectStorageCache $cache */ + $cache = $storage->getCache(); + + // Find form in storage + $key = "formStorage" . $name; + if ($cache->has($key)) + { + Logger::log("Returning cached Form '".$name."'"); + return $cache->get($key); + } + + // Otherwise, create the form + Logger::log("Generating new Form '".$name."'"); + $form = call_user_func($callable, $this->getForm($name, $label)); + + // Save the form + $cache->set($key, $form, 3600); + + return $form; + } + + + public function getClassesPrefix(): ?string + { + return null; + } + + public function getSourceDirectory(): ?string + { + return null; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Forms/Method.php b/src/FuzeWorks/Forms/Method.php new file mode 100755 index 0000000..0d97f17 --- /dev/null +++ b/src/FuzeWorks/Forms/Method.php @@ -0,0 +1,43 @@ +forms = \FuzeWorks\Factory::getInstance('libraries')->get('forms'); + } + + public function testFoundation() + { + $this->assertInstanceOf(Forms::class, $this->forms); + } + +} \ No newline at end of file diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 0000000..0de88ea --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,62 @@ +setTempDirectory(__DIR__ . '/temp'); +$configurator->setLogDirectory(__DIR__ . '/temp'); + +// Other values +$configurator->setTimeZone('Europe/Amsterdam'); + +// Add components +$configurator->addComponent(new \FuzeWorks\WebComponent()); +$configurator->addComponent(new \FuzeWorks\ObjectStorage\ObjectStorageComponent()); +$configurator->addComponent(new \FuzeWorks\LayoutComponent()); +$configurator->addComponent(new \FuzeWorks\TracyComponent()); + +// Build the container +$container = $configurator->createContainer(); + +// And add the library +$container->libraries->addLibraryClass("forms", Forms::class); +return $container; \ No newline at end of file diff --git a/test/phpunit.xml b/test/phpunit.xml new file mode 100644 index 0000000..789a49f --- /dev/null +++ b/test/phpunit.xml @@ -0,0 +1,17 @@ + + + + + ../ + + + ../vendor/ + ../test/ + + + + + ./ + + + \ No newline at end of file diff --git a/test/temp/.gitignore b/test/temp/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/test/temp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file