diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 244474298..d1100ffe5 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -3,8 +3,7 @@ name: docs-build on: release: types: [published] - repository_dispatch: - types: docs-build + workflow_dispatch: jobs: build-deploy: @@ -13,5 +12,5 @@ jobs: - name: Build Docs uses: laminas/documentation-theme/github-actions/docs@master env: - "DOCS_DEPLOY_KEY": ${{ secrets.DOCS_DEPLOY_KEY }} - "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} + DEPLOY_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 966d560a9..23a3f3570 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -241,6 +241,9 @@ Element\DateTime::class Element\DateTime::class + + parent::get($name, $options) + $aliases $factories @@ -292,6 +295,9 @@ translate translate + + (string) $label + diff --git a/src/FormElementManager.php b/src/FormElementManager.php index 41f75aa67..cc7a99a22 100644 --- a/src/FormElementManager.php +++ b/src/FormElementManager.php @@ -321,9 +321,10 @@ public function configure(array $config) * createFromInvokable() will use these and pass them to the instance * constructor if not null and a non-empty array. * - * @param class-string|string $name Service name of plugin to retrieve. + * @template T of ElementInterface + * @param class-string|string $name Service name of plugin to retrieve. * @param null|array $options Options to use when creating the instance. - * @psalm-return ($name is class-string ? ElementInterface : mixed) + * @return ($name is class-string ? T : ElementInterface) */ public function get($name, ?array $options = null): mixed { diff --git a/src/View/Helper/AbstractFormDateSelect.php b/src/View/Helper/AbstractFormDateSelect.php index 62e56ac9d..54a92ff7a 100644 --- a/src/View/Helper/AbstractFormDateSelect.php +++ b/src/View/Helper/AbstractFormDateSelect.php @@ -203,13 +203,13 @@ protected function getMonthsOptions(string $pattern): array * NOTE: we don't use a pattern for years, as years written as two digits can lead to hard to * read date for users, so we only use four digits years * - * @return array + * @return array */ protected function getYearsOptions(int $minYear, int $maxYear): array { $result = []; for ($i = $maxYear; $i >= $minYear; --$i) { - $result[$i] = $i; + $result[$i] = (string) $i; } return $result; diff --git a/src/View/Helper/AbstractHelper.php b/src/View/Helper/AbstractHelper.php index 2576e6156..f50ee9b30 100644 --- a/src/View/Helper/AbstractHelper.php +++ b/src/View/Helper/AbstractHelper.php @@ -561,12 +561,17 @@ protected function hasAllowedPrefix(string $attribute): bool } /** - * translate the label + * Translate the label * * @internal + * + * @todo Reduce argument to only string in the next major + * @param string $label */ - protected function translateLabel(string|int $label): string|int + protected function translateLabel(int|string|float|bool $label): string { + $label = (string) $label; + return $this->getTranslator()?->translate($label, $this->getTranslatorTextDomain()) ?? $label; } diff --git a/src/View/Helper/FormCheckbox.php b/src/View/Helper/FormCheckbox.php index 64dfe68c9..95f8cd014 100644 --- a/src/View/Helper/FormCheckbox.php +++ b/src/View/Helper/FormCheckbox.php @@ -22,8 +22,9 @@ public function render(ElementInterface $element): string { if (! $element instanceof CheckboxElement) { throw new Exception\InvalidArgumentException(sprintf( - '%s requires that the element is of type Laminas\Form\Element\Checkbox', - __METHOD__ + '%s requires that the element is of type %s', + __METHOD__, + CheckboxElement::class )); } diff --git a/src/View/Helper/FormCollection.php b/src/View/Helper/FormCollection.php index ea605d7da..8de5d8088 100644 --- a/src/View/Helper/FormCollection.php +++ b/src/View/Helper/FormCollection.php @@ -4,9 +4,12 @@ namespace Laminas\Form\View\Helper; +use Laminas\Form\Fieldset as FieldsetElement; use Laminas\Form\Element\Collection as CollectionElement; use Laminas\Form\ElementInterface; +use Laminas\Form\Exception; use Laminas\Form\FieldsetInterface; +use Laminas\Form\LabelAwareInterface; use Laminas\View\Helper\HelperInterface; use RuntimeException; @@ -47,7 +50,7 @@ class FormCollection extends AbstractHelper * * @var string */ - protected $labelWrapper = '%s'; + protected $labelWrapper = '%2$s'; /** * Where shall the template-data be inserted into @@ -77,6 +80,13 @@ class FormCollection extends AbstractHelper */ protected $fieldsetHelper; + /** + * Form label helper instance + * + * @var null|FormLabel + */ + protected $labelHelper; + /** * Invoke helper as function * @@ -103,6 +113,14 @@ public function __invoke(?ElementInterface $element = null, bool $wrap = true) */ public function render(ElementInterface $element): string { + if (! $element instanceof FieldsetElement) { + throw new Exception\InvalidArgumentException(sprintf( + '%s requires that the element is of type %s', + __METHOD__, + FieldsetElement::class + )); + } + $renderer = $this->getView(); if ($renderer !== null && ! method_exists($renderer, 'plugin')) { // Bail early if renderer is not pluggable @@ -113,6 +131,7 @@ public function render(ElementInterface $element): string $templateMarkup = ''; $elementHelper = $this->getElementHelper(); assert(is_callable($elementHelper)); + $fieldsetHelper = $this->getFieldsetHelper(); assert(is_callable($fieldsetHelper)); @@ -128,43 +147,33 @@ public function render(ElementInterface $element): string } } + if (! $this->shouldWrap) { + return $markup . $templateMarkup; + } + // Every collection is wrapped by a fieldset if needed - if ($this->shouldWrap) { - $attributes = $element->getAttributes(); - if (! $this->getDoctypeHelper()->isHtml5()) { - unset( - $attributes['name'], - $attributes['disabled'], - $attributes['form'] - ); - } - $attributesString = $attributes !== [] ? ' ' . $this->createAttributesString($attributes) : ''; + $attributes = $element->getAttributes(); + if (! $this->getDoctypeHelper()->isHtml5()) { + unset( + $attributes['name'], + $attributes['disabled'], + $attributes['form'] + ); + } - $label = $element->getLabel(); - $legend = ''; + $label = $element->getLabel() ?? ''; + $labelAttributes = []; - if (! empty($label)) { - $label = $this->translateLabel($label); - $label = $this->escapeLabel($element, $label); + if ($label !== '') { + $label = $this->translateLabel($label); + $label = $this->escapeLabel($element, $label); - $legend = sprintf( - $this->labelWrapper, - $label - ); + if ($element instanceof LabelAwareInterface) { + $labelAttributes = $element->getLabelAttributes(); } - - $markup = sprintf( - $this->wrapper, - $markup, - $legend, - $templateMarkup, - $attributesString - ); - } else { - $markup .= $templateMarkup; } - return $markup; + return $this->wrapElement($markup, $templateMarkup, $label, $attributes, $labelAttributes); } /** @@ -371,4 +380,66 @@ public function setTemplateWrapper(string $templateWrapper) return $this; } + + /** + * Retrieve the FormLabel helper + */ + protected function getLabelHelper(): FormLabel + { + if ($this->labelHelper) { + return $this->labelHelper; + } + + if ($this->view !== null && method_exists($this->view, 'plugin')) { + $this->labelHelper = $this->view->plugin('form_label'); + } + + if (! $this->labelHelper instanceof FormLabel) { + $this->labelHelper = new FormLabel(); + } + + if ($this->hasTranslator()) { + $this->labelHelper->setTranslator( + $this->getTranslator(), + $this->getTranslatorTextDomain() + ); + } + + return $this->labelHelper; + } + + public function wrapLabel(string $label, array $labelAttributes = []): string + { + $labelHelper = $this->getLabelHelper(); + $labelAttributesString = ''; + + if (is_array($labelAttributes) && $labelAttributes !== []) { + $labelAttributesString = ' ' . $labelHelper->createAttributesString($labelAttributes); + } + + return sprintf( + $this->getLabelWrapper(), + $labelAttributesString, + $label + ); + } + + public function wrapElement(string $markup, string $templateMarkup, string $label, array $attributes = [], array $labelAttributes = []): string + { + $legend = ''; + + if ($label !== '') { + $legend = $this->wrapLabel($label, $labelAttributes); + } + + $attributesString = $attributes !== [] ? ' ' . $this->createAttributesString($attributes) : ''; + + return sprintf( + $this->getWrapper(), + $markup, + $legend, + $templateMarkup, + $attributesString + ); + } } diff --git a/src/View/Helper/FormDateSelect.php b/src/View/Helper/FormDateSelect.php index d37cb0f65..d8cee03a8 100644 --- a/src/View/Helper/FormDateSelect.php +++ b/src/View/Helper/FormDateSelect.php @@ -53,8 +53,9 @@ public function render(ElementInterface $element): string { if (! $element instanceof DateSelectElement) { throw new Exception\InvalidArgumentException(sprintf( - '%s requires that the element is of type Laminas\Form\Element\DateSelect', - __METHOD__ + '%s requires that the element is of type %s', + __METHOD__, + DateSelectElement::class )); } diff --git a/src/View/Helper/FormDateTimeSelect.php b/src/View/Helper/FormDateTimeSelect.php index 74e70e870..0d04ab180 100644 --- a/src/View/Helper/FormDateTimeSelect.php +++ b/src/View/Helper/FormDateTimeSelect.php @@ -84,8 +84,9 @@ public function render(ElementInterface $element): string { if (! $element instanceof DateTimeSelectElement) { throw new Exception\InvalidArgumentException(sprintf( - '%s requires that the element is of type Laminas\Form\Element\DateTimeSelect', - __METHOD__ + '%s requires that the element is of type %s', + __METHOD__, + DateTimeSelectElement::class )); } diff --git a/src/View/Helper/FormMonthSelect.php b/src/View/Helper/FormMonthSelect.php index dd19135c1..259ddfdc4 100644 --- a/src/View/Helper/FormMonthSelect.php +++ b/src/View/Helper/FormMonthSelect.php @@ -52,8 +52,9 @@ public function render(ElementInterface $element): string { if (! $element instanceof MonthSelectElement) { throw new Exception\InvalidArgumentException(sprintf( - '%s requires that the element is of type Laminas\Form\Element\MonthSelect', - __METHOD__ + '%s requires that the element is of type %s', + __METHOD__, + MonthSelectElement::class )); } diff --git a/src/View/Helper/FormMultiCheckbox.php b/src/View/Helper/FormMultiCheckbox.php index 1bd3da21b..03c41593a 100644 --- a/src/View/Helper/FormMultiCheckbox.php +++ b/src/View/Helper/FormMultiCheckbox.php @@ -104,8 +104,9 @@ public function render(ElementInterface $element): string { if (! $element instanceof MultiCheckboxElement) { throw new Exception\InvalidArgumentException(sprintf( - '%s requires that the element is of type Laminas\Form\Element\MultiCheckbox', - __METHOD__ + '%s requires that the element is of type %s', + __METHOD__, + MultiCheckboxElement::class )); } diff --git a/src/View/Helper/FormRow.php b/src/View/Helper/FormRow.php index d6c67c0fe..b9d25f613 100644 --- a/src/View/Helper/FormRow.php +++ b/src/View/Helper/FormRow.php @@ -6,12 +6,16 @@ use Laminas\Form\Element\Button; use Laminas\Form\Element\Captcha; +use Laminas\Form\Element\Collection; use Laminas\Form\Element\MonthSelect; use Laminas\Form\ElementInterface; use Laminas\Form\Exception; use Laminas\Form\LabelAwareInterface; +use Laminas\View\Helper\HelperInterface; +use RuntimeException; use function in_array; +use function is_array; use function method_exists; use function sprintf; use function strtolower; @@ -63,6 +67,20 @@ class FormRow extends AbstractHelper */ protected $elementHelper; + /** + * The view helper used to render sub fieldsets. + * + * @var null|HelperInterface + */ + protected $fieldsetHelper; + + /** + * The name of the default view helper that is used to render sub elements. + * + * @var string + */ + protected $defaultFieldsetHelper = 'formCollection'; + /** * Form element errors helper instance * @@ -119,14 +137,14 @@ public function render(ElementInterface $element, ?string $labelPosition = null) $elementHelper = $this->getElementHelper(); $elementErrorsHelper = $this->getElementErrorsHelper(); - $label = $element->getLabel(); + $label = $element->getLabel() ?? ''; $inputErrorClass = $this->getInputErrorClass(); if ($labelPosition === null) { $labelPosition = $this->labelPosition; } - if (isset($label) && '' !== $label) { + if ('' !== $label) { // Translate the label $label = $this->translateLabel($label); } @@ -160,81 +178,100 @@ public function render(ElementInterface $element, ?string $labelPosition = null) // hidden elements do not need a