From 43bf6af6db2dc026f5ea706fa19ef09c805559ae Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Wed, 5 Aug 2026 15:09:49 +0200 Subject: [PATCH] Auto-assign the sole visible category in StartRegistration when no category field is shown When only one newsletter category existed, TypeHasCategoriesElementTrait skipped adding the categories field to the form. As a result, the model transformer created a PendingOptIn with no categories, and the confirmed Recipient ended up stored with no subscriptions. StartRegistration\Type now detects this case inside the model transformer's reverseTransform and injects the single visible category into the form data before passing it to the PendingOptInFactory. As part of this, addCategoriesElementToForm() was refactored: the trait no longer calls findVisible() itself or owns the categoryRepository property. Instead, it receives the pre-fetched choices as a parameter and unconditionally adds the field. The decision of whether to call addCategoriesElementToForm() at all now rests with the calling types (StartRegistration\Type and EditRegistration\Type), which avoids a second findVisible() call and separates the concerns of fetching categories and rendering the form field. --- src/EditRegistration/Type.php | 7 ++++- .../TypeHasCategoriesElementTrait.php | 11 +------- src/StartRegistration/Type.php | 16 +++++++++-- tests/StartRegistration/TypeTest.php | 28 +++++++++++++++++++ 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/EditRegistration/Type.php b/src/EditRegistration/Type.php index ef36e6f..c7940f8 100644 --- a/src/EditRegistration/Type.php +++ b/src/EditRegistration/Type.php @@ -12,6 +12,7 @@ class Type extends AbstractType use TypeHasCategoriesElementTrait; public const ELEMENT_CATEGORIES = 'categories'; + protected CategoryRepositoryInterface $categoryRepository; public function __construct(CategoryRepositoryInterface $categoryRepository) { @@ -20,7 +21,11 @@ public function __construct(CategoryRepositoryInterface $categoryRepository) public function buildForm(FormBuilderInterface $builder, array $options): void { - $this->addCategoriesElementToForm($builder, false); + $choices = $this->categoryRepository->findVisible(); + + if (\count($choices) > 1) { + $this->addCategoriesElementToForm($builder, $choices, false); + } // We need at least one element in addition to the categories above, so that Symfony recognizes the form being // submitted even if no categories where chosen. diff --git a/src/EditRegistration/TypeHasCategoriesElementTrait.php b/src/EditRegistration/TypeHasCategoriesElementTrait.php index cc061aa..4f7a204 100644 --- a/src/EditRegistration/TypeHasCategoriesElementTrait.php +++ b/src/EditRegistration/TypeHasCategoriesElementTrait.php @@ -5,20 +5,11 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Validator\Constraints\Choice; -use Webfactory\NewsletterRegistrationBundle\Entity\CategoryRepositoryInterface; trait TypeHasCategoriesElementTrait { - protected CategoryRepositoryInterface $categoryRepository; - - protected function addCategoriesElementToForm(FormBuilderInterface $builder, bool $recipientHasToChooseAtLeastOne) + protected function addCategoriesElementToForm(FormBuilderInterface $builder, array $choices, bool $recipientHasToChooseAtLeastOne): void { - // add category choices, if there is more than one - $choices = $this->categoryRepository->findVisible(); - if (\count($choices) < 2) { - return; - } - $constraints = []; if (true === $recipientHasToChooseAtLeastOne) { $constraints[] = new Choice(choices: $choices, multiple: true, min: 1); diff --git a/src/StartRegistration/Type.php b/src/StartRegistration/Type.php index 4efaa33..74a40d3 100644 --- a/src/StartRegistration/Type.php +++ b/src/StartRegistration/Type.php @@ -19,6 +19,7 @@ class Type extends AbstractType public const ELEMENT_HONEYPOT = 'url'; protected PendingOptInFactoryInterface $pendingOptInFactory; + protected CategoryRepositoryInterface $categoryRepository; public function __construct(CategoryRepositoryInterface $categoryRepository, PendingOptInFactoryInterface $pendingOptInFactory) { @@ -30,7 +31,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add(static::ELEMENT_EMAIL_ADDRESS, EmailAddressType::class); - $this->addCategoriesElementToForm($builder, true); + $choices = $this->categoryRepository->findVisible(); + + if (\count($choices) > 1) { + $this->addCategoriesElementToForm($builder, $choices, true); + } // fake field for spam protection $builder->add(static::ELEMENT_HONEYPOT, HoneypotType::class); @@ -47,7 +52,14 @@ function (?PendingOptInInterface $pendingOptIn): array { static::ELEMENT_CATEGORIES => $pendingOptIn->getCategories(), ]; }, - function (array $formData) use ($that): ?PendingOptInInterface { + function (array $formData) use ($that, $choices): ?PendingOptInInterface { + if (!isset($formData[self::ELEMENT_CATEGORIES]) && 1 === \count($choices)) { + // if the field 'categories' is not in the form because you could choose only one anyway, we need to + // set that one category here. + $singleCategory = $choices[0]; + $formData[self::ELEMENT_CATEGORIES] = [$singleCategory]; + } + return $that->pendingOptInFactory->fromRegistrationFormData($formData); } )); diff --git a/tests/StartRegistration/TypeTest.php b/tests/StartRegistration/TypeTest.php index 02dad28..9bc6d2b 100644 --- a/tests/StartRegistration/TypeTest.php +++ b/tests/StartRegistration/TypeTest.php @@ -240,6 +240,34 @@ function (array $formData) { $this->assertEquals($pendingOptIn, $form->getData()); } + #[Test] + public function provides_PendingOptIn_with_single_category_auto_assigned_when_only_one_exists(): void + { + $this->setUpOneCategory(); + + $pendingOptIn = new PendingOptIn(null, new EmailAddress('webfactory@example.com', 'secret'), [$this->category1]); + $this->pendingOptInFactory + ->method('fromRegistrationFormData') + ->with( + $this->callback( + function (array $formData) { + return \array_key_exists(StartRegistrationType::ELEMENT_CATEGORIES, $formData) + && $formData[StartRegistrationType::ELEMENT_CATEGORIES] === [$this->category1]; + } + ) + ) + ->willReturn($pendingOptIn); + + $form = $this->factory->create(StartRegistrationType::class); + $form->submit([ + StartRegistrationType::ELEMENT_EMAIL_ADDRESS => 'webfactory@example.com', + StartRegistrationType::ELEMENT_HONEYPOT => '', + ]); + + $this->assertTrue($form->isValid()); + $this->assertEquals($pendingOptIn, $form->getData()); + } + #[Test] public function provides_PendingOptIn_if_submitted_with_valid_data_and_category_choices() {