diff --git a/src/Element/ChoiceElement.php b/src/Element/ChoiceElement.php index 4e9af0e..d046c15 100644 --- a/src/Element/ChoiceElement.php +++ b/src/Element/ChoiceElement.php @@ -15,19 +15,18 @@ class ChoiceElement extends Element implements ChoiceElementInterface protected ?ChoiceListInterface $list = null; /** - * @param ChoiceListInterface $list - * - * @return static + * {@inheritDoc} */ public function withChoices(ChoiceListInterface $list): static { + $this->assertNotSubmitted(__METHOD__); $this->list = $list; return $this; } /** - * @return ChoiceListInterface + * {@inheritDoc} */ public function choices(): ChoiceListInterface { diff --git a/src/Element/CollectionElement.php b/src/Element/CollectionElement.php index 2418618..47b73ec 100644 --- a/src/Element/CollectionElement.php +++ b/src/Element/CollectionElement.php @@ -31,16 +31,17 @@ class CollectionElement extends Element implements CollectionElementInterface private array $allErrors = []; /** - * @param ElementInterface[] $elements - * - * @return static + * {@inheritDoc} */ public function withElement(ElementInterface ...$elements): static { + $this->assertNotSubmitted(__METHOD__); + array_walk( $elements, function (ElementInterface $element): void { $this->elements[$element->name()] = $element; + $element->withParent($this); } ); @@ -48,11 +49,7 @@ function (ElementInterface $element): void { } /** - * @param string $name - * - * @return ElementInterface - * @throws ElementNotFoundException - * + * {@inheritDoc} */ public function element(string $name): ElementInterface { @@ -66,9 +63,7 @@ public function element(string $name): ElementInterface } /** - * @param string $name - * - * @return bool + * {@inheritDoc} */ public function elementExists(string $name): bool { @@ -78,18 +73,24 @@ public function elementExists(string $name): bool /** * If the key is "value" and the $value an array, we assign all values to the children. * - * @param string $key - * @param bool|int|string $value - * - * @return static + * {@inheritDoc} */ public function withAttribute(string $key, $value): static { + $this->assertNotSubmitted(__METHOD__); + if ($key === 'value' && is_array($value)) { - foreach ($this->elements as $name => $element) { - $this->elements[$name]->withValue($value[$name] ?? ''); + $assignedValues = []; + foreach ($value as $elementName => $elementValue) { + if (!$this->elementExists($elementName)) { + continue; + } + $this->element($elementName)->withValue($elementValue); + $assignedValues[$elementName] = $elementValue; } + $this->attributes['value'] = $assignedValues; + return $this; } @@ -101,9 +102,7 @@ public function withAttribute(string $key, $value): static /** * Returns a list of values for each element inside the collection. * - * @param string $key - * - * @return array + * {@inheritDoc} */ public function attribute(string $key) { @@ -118,7 +117,7 @@ public function attribute(string $key) } /** - * @return array + * {@inheritDoc} */ public function elements(): array { @@ -128,16 +127,13 @@ public function elements(): array /** * Delegate errors down to the children. * - * @param array $errors - * - * @return static + * {@inheritDoc} */ public function withErrors(array $errors = []): static { $this->allErrors = $errors; - foreach ($this->elements as $element) { - $name = $element->name(); + foreach ($this->elements as $name => $element) { if (isset($errors[$name]) && $element instanceof ErrorAwareInterface) { $element->withErrors((array) $errors[$name]); unset($errors[$name]); @@ -151,7 +147,7 @@ public function withErrors(array $errors = []): static } /** - * @return bool + * {@inheritDoc} */ public function hasErrors(): bool { @@ -167,6 +163,9 @@ public function hasErrors(): bool return false; } + /** + * {@inheritDoc} + */ public function validate(): bool { $isValid = parent::validate(); diff --git a/src/Element/Element.php b/src/Element/Element.php index af039ce..94cb885 100644 --- a/src/Element/Element.php +++ b/src/Element/Element.php @@ -2,6 +2,8 @@ namespace ChriCo\Fields\Element; +use ChriCo\Fields\Exception\LogicException; + /** * Class Element * @@ -31,6 +33,8 @@ class Element implements */ protected $filter = null; + protected ?CollectionElement $parent = null; + /** * @param string $name */ @@ -41,52 +45,57 @@ public function __construct(string $name) } /** - * @param string $key - * @param bool|int|string $value - * - * @return static + * {@inheritDoc} */ public function withAttribute(string $key, $value): static { + $this->assertNotSubmitted(__METHOD__); $this->attributes[$key] = $value; return $this; } /** - * @return string + * {@inheritDoc} */ - public function id(): string + public function attribute(string $key) { - return (string) $this->attribute('id'); + return $this->attributes()[$key] ?? ''; } /** - * @param string $key + * An Element itself can be disabled but also can be disabled through the parent. * - * @return bool|int|mixed|string + * {@inheritDoc} */ - public function attribute(string $key) + public function isDisabled(): bool { - if (!isset($this->attributes[$key])) { - return ''; - } + $disabled = $this->parent()?->isDisabled() ?? $this->attribute('disabled'); - return $this->attributes[$key]; + return is_bool($disabled) && $disabled; } /** - * @return bool + * The Element itself cannot be submitted. It is always submitted through + * the parent which is Form::isSubmitted(). + * + * {@inheritDoc} */ - public function isDisabled(): bool + public function isSubmitted(): bool { - $disabled = $this->attribute('disabled'); + return $this->parent()?->isSubmitted() ?? false; + } - return is_bool($disabled) && $disabled; + /** + * {@inheritDoc} + */ + public function id(): string + { + return (string) $this->attribute('id'); } /** - * @return string + * {@inheritDoc} */ public function name(): string { @@ -94,7 +103,7 @@ public function name(): string } /** - * @return string + * {@inheritDoc} */ public function type(): string { @@ -102,7 +111,7 @@ public function type(): string } /** - * @return bool|int|mixed|string + * {@inheritDoc} */ public function value() { @@ -112,19 +121,18 @@ public function value() } /** - * @param string $value - * - * @return static + * {@inheritDoc} */ public function withValue($value): static { + $this->assertNotSubmitted(__METHOD__); $this->withAttribute('value', $value); return $this; } /** - * @return array + * {@inheritDoc} */ public function attributes(): array { @@ -132,12 +140,29 @@ public function attributes(): array } /** - * @param array $attributes - * - * @return static + * {@inheritDoc} + */ + public function attributesForView(): array + { + $attributes = $this->attributes; + if ($this->parent() !== null) { + $parentAttributes = $this->parent()->attributesForView(); + $id = $parentAttributes['id']; + $name = $parentAttributes['name']; + + $attributes['id'] = $id . '_' . $attributes['id']; + $attributes['name'] = $name . '[' . $attributes['name'] . ']'; + } + + return $attributes; + } + + /** + * {@inheritDoc} */ public function withAttributes(array $attributes = []): static { + $this->assertNotSubmitted(__METHOD__); foreach ($attributes as $key => $value) { $this->withAttribute($key, $value); } @@ -146,7 +171,7 @@ public function withAttributes(array $attributes = []): static } /** - * @return array + * {@inheritDoc} */ public function options(): array { @@ -154,13 +179,12 @@ public function options(): array } /** - * @param array $options - * - * @return static + * {@inheritDoc} */ public function withOptions(array $options = []): static { - foreach($options as $key => $value){ + $this->assertNotSubmitted(__METHOD__); + foreach ($options as $key => $value) { $this->withOption($key, $value); } @@ -168,22 +192,18 @@ public function withOptions(array $options = []): static } /** - * @param string $key - * @param int|string $value - * - * @return static + * {@inheritDoc} */ public function withOption(string $key, $value): static { + $this->assertNotSubmitted(__METHOD__); $this->options[$key] = $value; return $this; } /** - * @param string $key - * - * @return int|mixed|string + * {@inheritDoc} */ public function option(string $key) { @@ -195,17 +215,19 @@ public function option(string $key) } /** - * @param callable $callable - * - * @return static + * {@inheritDoc} */ public function withFilter(callable $callable): static { + $this->assertNotSubmitted(__METHOD__); $this->filter = $callable; return $this; } + /** + * {@inheritDoc} + */ public function filter($value) { if ($this->filter) { @@ -216,17 +238,19 @@ public function filter($value) } /** - * @param callable $callable - * - * @return static + * {@inheritDoc} */ public function withValidator(callable $callable): static { + $this->assertNotSubmitted(__METHOD__); $this->validator = $callable; return $this; } + /** + * {@inheritDoc} + */ public function validate(): bool { $value = $this->value(); @@ -242,4 +266,37 @@ public function validate(): bool return $valid; } + + /** + * {@inheritDoc} + */ + public function withParent(CollectionElement $element): static + { + $this->parent = $element; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function parent(): ?CollectionElement + { + return $this->parent; + } + + /** + * Internal helper function for with*()-methods to ensure + * that data is only set when the Element (and parent) is not yet submitted. + * + * @param string $caller + * + * @return void + */ + protected function assertNotSubmitted(string $caller): void + { + if ($this->isSubmitted()) { + throw new LogicException(sprintf('You cannot call %s after submission.', $caller)); + } + } } diff --git a/src/Element/ElementInterface.php b/src/Element/ElementInterface.php index 88d43d4..505e339 100644 --- a/src/Element/ElementInterface.php +++ b/src/Element/ElementInterface.php @@ -16,6 +16,13 @@ interface ElementInterface */ public function isDisabled(): bool; + /** + * In case the element itself (or parent) is submitted. + * + * @return bool + */ + public function isSubmitted(): bool; + /** * Proxy to get the "id" in field attributes. * @@ -40,7 +47,7 @@ public function name(): string; /** * Proxy to get the value in field attributes. * - * @return mixed + * @return bool|int|mixed|string|array */ public function value(); @@ -51,6 +58,15 @@ public function value(); */ public function withValue($value); + /** + * Returns attributes prepared for the View with taking + * the parent into consideration for building correct + * "id" and "name" attributes. + * + * @return array + */ + public function attributesForView(): array; + /** * Get all field attributes for this element. * @@ -72,7 +88,7 @@ public function withAttribute(string $key, $value); /** * @param string $key * - * @return int|string|bool $value + * @return bool|int|mixed|string|array $value */ public function attribute(string $key); @@ -124,4 +140,17 @@ public function validate(): bool; * @param callable $callable */ public function withValidator(callable $callable); + + /** + * Setting a parent which can be either a Collection or Form itself + * to reuse internally to detect if the Element is disabled or submitted. + * + * @param CollectionElement $element + */ + public function withParent(CollectionElement $element); + + /** + * @return CollectionElement|null + */ + public function parent(): ?CollectionElement; } diff --git a/src/Element/Form.php b/src/Element/Form.php index 6e3004e..6b60b02 100644 --- a/src/Element/Form.php +++ b/src/Element/Form.php @@ -21,63 +21,23 @@ class Form extends CollectionElement implements FormInterface protected bool $isSubmitted = false; /** - * Contains the raw data assigned by Form::bind_data + * Contains the raw data assigned by Form::submit() */ protected array $rawData = []; /** - * Contains the filtered data. - */ - protected array $data = []; - - /** - * @param string $key - * @param string|array $value - * - * @return static - * @throws LogicException - * - */ - public function withAttribute(string $key, $value): static - { - if ($key === 'value' && is_array($value)) { - $this->withData($value); - } - - parent::withAttribute($key, $value); - - return $this; - } - - /** - * @param array $data - * - * @return static - * @throws LogicException - * + * {@inheritDoc} */ public function withData(array $data = []): static { - if ($this->isSubmitted) { - throw new LogicException('You cannot change data of a submitted form.'); - } - - foreach ($data as $name => $value) { - if (!$this->elementExists($name)) { - continue; - } - $element = $this->element($name); - $element->withValue($value); - $this->data[$name] = $element->value(); - } + $this->assertNotSubmitted(__METHOD__); + $this->withAttribute('value', $data); return $this; } /** - * @param array $inputData - * - * @throws ElementNotFoundException + * {@inheritDoc} */ public function submit(array $inputData = []) { @@ -94,8 +54,6 @@ public function submit(array $inputData = []) $this->rawData[$name] = $value; $element->withValue($value); - $this->data[$name] = $element->value(); - if (!$element->validate()) { $this->isValid = false; } @@ -103,17 +61,15 @@ public function submit(array $inputData = []) } /** - * @return array + * {@inheritDoc} */ public function data(): array { - return $this->data; + return $this->value(); } /** - * @return bool - * @throws LogicException - * + * {@inheritDoc} */ public function isValid(): bool { @@ -133,7 +89,7 @@ public function isValid(): bool } /** - * @return bool + * {@inheritDoc} */ public function isSubmitted(): bool { diff --git a/src/Element/FormInterface.php b/src/Element/FormInterface.php index b5cb55c..415c2a5 100644 --- a/src/Element/FormInterface.php +++ b/src/Element/FormInterface.php @@ -9,7 +9,6 @@ */ interface FormInterface { - /** * Submits data to the form, filter and validates it. * @@ -18,14 +17,12 @@ interface FormInterface public function submit(array $inputData = []); /** - * @return bool - */ - public function isSubmitted(): bool; - - /** - * Set data without re-validating and filtering it. + * Pre-assign data (values) to all Elements when the Form is not submitted. + * This method is a shorthand to Form::withValue($data) or Form::withAttribute('value', $data); * * @param array $data + * + * @deprecated ElementInterface::setValue() */ public function withData(array $data = []); @@ -33,6 +30,8 @@ public function withData(array $data = []); * Returns the assigned data. * * @return array + * + * @deprecated ElementInterface::value() */ public function data(): array; diff --git a/src/ElementFactory.php b/src/ElementFactory.php index 3974b9b..6820478 100644 --- a/src/ElementFactory.php +++ b/src/ElementFactory.php @@ -231,7 +231,10 @@ protected static function configureDescription( */ protected static function configureElement(Element\ElementInterface $element, array $specs = []) { - $element->withAttributes($specs['attributes']); + $attributes = $specs['attributes']; + // Don't set name again. + unset($attributes['name']); + $element->withAttributes($attributes); if (!empty($specs['options'])) { $element->withOptions($specs['options']); diff --git a/src/View/Button.php b/src/View/Button.php index bc682ef..c85e2dd 100644 --- a/src/View/Button.php +++ b/src/View/Button.php @@ -25,7 +25,7 @@ public function render(ElementInterface $element): string // Every button should have a label (text). $this->assertElementIsInstanceOf($element, LabelAwareInterface::class); - $attributes = $element->attributes(); + $attributes = $element->attributesForView(); $attributes = $this->buildCssClasses($attributes, 'element', $element); return sprintf( diff --git a/src/View/Checkbox.php b/src/View/Checkbox.php index dd0c620..276af6b 100644 --- a/src/View/Checkbox.php +++ b/src/View/Checkbox.php @@ -28,7 +28,7 @@ public function render(ElementInterface $element): string $this->assertElementIsInstanceOf($element, ChoiceElementInterface::class); $list = $element->choices(); - $attributes = $element->attributes(); + $attributes = $element->attributesForView(); $choices = $list->choices(); $selected = $list->choicesForValue((array) $element->value()); @@ -40,7 +40,7 @@ public function render(ElementInterface $element): string $isMultiChoice = false; if (count($choices) > 1) { - $attributes['name'] = $element->name() . '[]'; + $attributes['name'] = $attributes['name'] . '[]'; $isMultiChoice = true; } @@ -50,8 +50,8 @@ public function render(ElementInterface $element): string $elementAttr['checked'] = isset($selected[$key]); $elementAttr['disabled'] = $choice['disabled']; $elementAttr['id'] = $isMultiChoice - ? $element->id() . '_' . $key - : $element->id(); + ? $attributes['id'] . '_' . $key + : $attributes['id']; $elementAttr = $this->buildCssClasses($elementAttr, 'element', $element); $label = sprintf( diff --git a/src/View/Collection.php b/src/View/Collection.php index 8524506..18ca973 100644 --- a/src/View/Collection.php +++ b/src/View/Collection.php @@ -48,10 +48,6 @@ public function render(ElementInterface $element): string $html = array_reduce( $element->elements(), function ($html, ElementInterface $next) use ($element, $row): string { - // Adding the CollectionElement name to the Element name and ID as prefix. - $next->withAttribute('id', $element->id() . '_' . $next->id()); - $next->withAttribute('name', $element->name() . '[' . $next->name() . ']'); - // In case we have nested CollectionElement, then // we don't want to nest those when rendering. if ($next instanceof Element\CollectionElement) { @@ -65,7 +61,7 @@ function ($html, ElementInterface $next) use ($element, $row): string { '' ); - $attributes = $element->attributes(); + $attributes = $element->attributesForView(); $attributes = $this->buildCssClasses($attributes, 'collection', $element); // we do not want to get the "name" and "type" rendered as attribute on wrapper. unset($attributes['name'], $attributes['type']); diff --git a/src/View/Form.php b/src/View/Form.php index ca4755f..17fd259 100644 --- a/src/View/Form.php +++ b/src/View/Form.php @@ -56,7 +56,7 @@ static function ($html, ElementInterface $next) use ($element, $row): string { '' ); - $attributes = $element->attributes(); + $attributes = $element->attributesForView(); // Don't re-use the "type" as attribute on