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 = '';
+ protected $labelWrapper = '';
/**
* 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