diff --git a/src/Bundle/ChillPersonBundle/Actions/PersonCreate/PersonCreateDTO.php b/src/Bundle/ChillPersonBundle/Actions/PersonCreate/PersonCreateDTO.php new file mode 100644 index 000000000..bb8d4f79c --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Actions/PersonCreate/PersonCreateDTO.php @@ -0,0 +1,70 @@ + where the key is the altname's key + */ + public array $altNames = []; + + /** + * @var array + */ + #[Assert\Valid(traverse: true)] + public array $identifiers = []; +} diff --git a/src/Bundle/ChillPersonBundle/Actions/PersonCreate/Service/PersonCreateDTOFactory.php b/src/Bundle/ChillPersonBundle/Actions/PersonCreate/Service/PersonCreateDTOFactory.php new file mode 100644 index 000000000..2259e345a --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Actions/PersonCreate/Service/PersonCreateDTOFactory.php @@ -0,0 +1,96 @@ +firstName = $person->getFirstName(); + $dto->lastName = $person->getLastName(); + $dto->birthdate = $person->getBirthdate(); + $dto->gender = $person->getGender(); + $dto->civility = $person->getCivility(); + $dto->phonenumber = $person->getPhonenumber(); + $dto->mobilenumber = $person->getMobilenumber(); + $dto->email = $person->getEmail(); + $dto->center = $person->getCenter(); + // address/addressForm are not mapped on Person entity; left to defaults + + foreach ($this->configPersonAltNamesHelper->getChoices() as $key => $labels) { + $altName = $person->getAltNames()->findFirst(fn (int $k, PersonAltName $altName) => $altName->getKey() === $key); + if (null === $altName) { + $altName = new PersonAltName(); + $altName->setKey($key); + } + $dto->altNames[$key] = $altName; + } + + foreach ($this->personIdentifierManager->getWorkers() as $worker) { + $identifier = $person + ->getIdentifiers() + ->findFirst(fn (int $key, PersonIdentifier $identifier) => $worker->getDefinition() === $identifier->getDefinition()); + if (null === $identifier) { + $identifier = new PersonIdentifier($worker->getDefinition()); + $person->addIdentifier($identifier); + } + $dto->identifiers['identifier_'.$worker->getDefinition()->getId()] = $identifier; + } + + return $dto; + } + + public function mapPersonCreateDTOtoPerson(PersonCreateDTO $personCreateDTO, Person $person): void + { + $person + ->setFirstName($personCreateDTO->firstName) + ->setLastName($personCreateDTO->lastName) + ->setBirthdate($personCreateDTO->birthdate) + ->setGender($personCreateDTO->gender) + ->setCivility($personCreateDTO->civility) + ->setPhonenumber($personCreateDTO->phonenumber) + ->setMobilenumber($personCreateDTO->mobilenumber) + ->setEmail($personCreateDTO->email) + ->setCenter($personCreateDTO->center); + + foreach ($personCreateDTO->altNames as $altName) { + if ('' === $altName->getLabel()) { + $person->removeAltName($altName); + } else { + $person->addAltName($altName); + } + } + + foreach ($personCreateDTO->identifiers as $identifier) { + $worker = $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($identifier->getDefinition()); + if ($worker->isEmpty($identifier)) { + $person->removeIdentifier($identifier); + } else { + $person->addIdentifier($identifier); + } + } + // Note: address and addressForm are handled by controller/form during creation, not mapped here + } +} diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonController.php b/src/Bundle/ChillPersonBundle/Controller/PersonController.php index f902118ef..3b5cabd95 100644 --- a/src/Bundle/ChillPersonBundle/Controller/PersonController.php +++ b/src/Bundle/ChillPersonBundle/Controller/PersonController.php @@ -11,36 +11,22 @@ declare(strict_types=1); namespace Chill\PersonBundle\Controller; -use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper; -use Chill\PersonBundle\Entity\Household\Household; -use Chill\PersonBundle\Entity\Household\HouseholdMember; use Chill\PersonBundle\Entity\Person; -use Chill\PersonBundle\Form\CreationPersonType; use Chill\PersonBundle\Privacy\PrivacyEvent; use Chill\PersonBundle\Repository\PersonRepository; -use Chill\PersonBundle\Search\SimilarPersonMatcher; -use Chill\PersonBundle\Security\Authorization\PersonVoter; use Doctrine\ORM\EntityManagerInterface; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Form\Extension\Core\Type\SubmitType; -use Symfony\Component\Form\Form; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Validator\Validator\ValidatorInterface; -use Symfony\Contracts\Translation\TranslatorInterface; -use function hash; final class PersonController extends AbstractController { public function __construct( - private readonly AuthorizationHelperInterface $authorizationHelper, - private readonly SimilarPersonMatcher $similarPersonMatcher, - private readonly TranslatorInterface $translator, private readonly EventDispatcherInterface $eventDispatcher, private readonly PersonRepository $personRepository, private readonly ConfigPersonAltNamesHelper $configPersonAltNameHelper, @@ -85,110 +71,6 @@ final class PersonController extends AbstractController ); } - /** - * Method for creating a new person. - * - * The controller register data from a previous post on the form, and - * register it in the session. - * - * The next post compare the data with previous one and, if yes, show a - * review page if there are "alternate persons". - */ - #[Route(path: '/{_locale}/person/new', name: 'chill_person_new')] - public function newAction(Request $request): Response - { - $person = new Person(); - - $authorizedCenters = $this->authorizationHelper->getReachableCenters($this->getUser(), PersonVoter::CREATE); - - if (1 === \count($authorizedCenters)) { - $person->setCenter($authorizedCenters[0]); - } - - $form = $this->createForm(CreationPersonType::class, $person) - ->add('editPerson', SubmitType::class, [ - 'label' => 'Add the person', - ])->add('createPeriod', SubmitType::class, [ - 'label' => 'Add the person and create an accompanying period', - ])->add('createHousehold', SubmitType::class, [ - 'label' => 'Add the person and create a household', - ]); - - $form->handleRequest($request); - - if (Request::METHOD_GET === $request->getMethod()) { - $this->lastPostDataReset(); - } elseif ( - Request::METHOD_POST === $request->getMethod() - && $form->isValid() - ) { - $alternatePersons = $this->similarPersonMatcher - ->matchPerson($person); - - if ( - false === $this->isLastPostDataChanges($form, $request, true) - || 0 === \count($alternatePersons) - ) { - $this->em->persist($person); - - $this->em->flush(); - $this->lastPostDataReset(); - - $address = $form->get('address')->getData(); - $addressForm = (bool) $form->get('addressForm')->getData(); - - if (null !== $address && $addressForm) { - $household = new Household(); - - $member = new HouseholdMember(); - $member->setPerson($person); - $member->setStartDate(new \DateTimeImmutable()); - - $household->addMember($member); - $household->setForceAddress($address); - - $this->em->persist($member); - $this->em->persist($household); - $this->em->flush(); - - if ($form->get('createHousehold')->isClicked()) { - return $this->redirectToRoute('chill_person_household_members_editor', [ - 'persons' => [$person->getId()], - 'household' => $household->getId(), - ]); - } - } - - if ($form->get('createPeriod')->isClicked()) { - return $this->redirectToRoute('chill_person_accompanying_course_new', [ - 'person_id' => [$person->getId()], - ]); - } - - if ($form->get('createHousehold')->isClicked()) { - return $this->redirectToRoute('chill_person_household_members_editor', [ - 'persons' => [$person->getId()], - ]); - } - - return $this->redirectToRoute( - 'chill_person_general_edit', - ['person_id' => $person->getId()] - ); - } - } elseif (Request::METHOD_POST === $request->getMethod() && !$form->isValid()) { - $this->addFlash('error', $this->translator->trans('This form contains errors')); - } - - return $this->render( - '@ChillPerson/Person/create.html.twig', - [ - 'form' => $form->createView(), - 'alternatePersons' => $alternatePersons ?? [], - ] - ); - } - #[Route(path: '/{_locale}/person/{person_id}/general', name: 'chill_person_view')] public function viewAction(int $person_id) { @@ -250,51 +132,4 @@ final class PersonController extends AbstractController return $errors; } - - private function isLastPostDataChanges(Form $form, Request $request, bool $replace = false): bool - { - /** @var SessionInterface $session */ - $session = $this->get('session'); - - if (!$session->has('last_person_data')) { - return true; - } - - $newPost = $this->lastPostDataBuildHash($form, $request); - - $isChanged = $session->get('last_person_data') !== $newPost; - - if ($replace) { - $session->set('last_person_data', $newPost); - } - - return $isChanged; - } - - /** - * build the hash for posted data. - * - * For privacy reasons, the data are hashed using sha512 - */ - private function lastPostDataBuildHash(Form $form, Request $request): string - { - $fields = []; - $ignoredFields = ['form_status', '_token', 'identifiers']; - - foreach ($request->request->all()[$form->getName()] as $field => $value) { - if (\in_array($field, $ignoredFields, true)) { - continue; - } - $fields[$field] = \is_array($value) ? - \implode(',', $value) : $value; - } - ksort($fields); - - return \hash('sha512', \implode('&', $fields)); - } - - private function lastPostDataReset(): void - { - $this->get('session')->set('last_person_data', ''); - } } diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonCreateController.php b/src/Bundle/ChillPersonBundle/Controller/PersonCreateController.php new file mode 100644 index 000000000..7ddd883d7 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Controller/PersonCreateController.php @@ -0,0 +1,205 @@ +authorizationHelper->getReachableCenters($this->getUser(), PersonVoter::CREATE); + + $dto = $this->personCreateDTOFactory->createPersonCreateDTO($person); + + if (1 === \count($authorizedCenters)) { + $dto->center = $authorizedCenters[0]; + } + + $form = $this->createForm(CreationPersonType::class, $dto) + ->add('editPerson', SubmitType::class, [ + 'label' => 'Add the person', + ])->add('createPeriod', SubmitType::class, [ + 'label' => 'Add the person and create an accompanying period', + ])->add('createHousehold', SubmitType::class, [ + 'label' => 'Add the person and create a household', + ]); + + $form->handleRequest($request); + + if (Request::METHOD_GET === $request->getMethod()) { + $this->lastPostDataReset($session); + } elseif ( + Request::METHOD_POST === $request->getMethod() + && $form->isValid() + ) { + $alternatePersons = $this->similarPersonMatcher + ->matchPerson($person); + + $createHouseholdButton = $form->get('createHousehold'); + $createPeriodButton = $form->get('createPeriod'); + $editPersonButton = $form->get('editPerson'); + + if (!$createHouseholdButton instanceof ClickableInterface) { + throw new \UnexpectedValueException(); + } + if (!$createPeriodButton instanceof ClickableInterface) { + throw new \UnexpectedValueException(); + } + if (!$editPersonButton instanceof ClickableInterface) { + throw new \UnexpectedValueException(); + } + + if ( + false === $this->isLastPostDataChanges($form, $request, $session) + || 0 === \count($alternatePersons) + ) { + $this->personCreateDTOFactory->mapPersonCreateDTOtoPerson($dto, $person); + $this->em->persist($person); + + $this->em->flush(); + $this->lastPostDataReset($session); + + $address = $dto->address; + $addressForm = $dto->addressForm; + + if (null !== $address && $addressForm) { + $household = new Household(); + + $member = new HouseholdMember(); + $member->setPerson($person); + $member->setStartDate(new \DateTimeImmutable()); + + $household->addMember($member); + $household->setForceAddress($address); + + $this->em->persist($member); + $this->em->persist($household); + $this->em->flush(); + + if ($createHouseholdButton->isClicked()) { + return $this->redirectToRoute('chill_person_household_members_editor', [ + 'persons' => [$person->getId()], + 'household' => $household->getId(), + ]); + } + } + + if ($createPeriodButton->isClicked()) { + return $this->redirectToRoute('chill_person_accompanying_course_new', [ + 'person_id' => [$person->getId()], + ]); + } + + if ($createHouseholdButton->isClicked()) { + return $this->redirectToRoute('chill_person_household_members_editor', [ + 'persons' => [$person->getId()], + ]); + } + + return $this->redirectToRoute( + 'chill_person_general_edit', + ['person_id' => $person->getId()] + ); + } + } elseif (Request::METHOD_POST === $request->getMethod() && !$form->isValid()) { + $this->addFlash('error', $this->translator->trans('This form contains errors')); + } + + return $this->render( + '@ChillPerson/Person/create.html.twig', + [ + 'form' => $form->createView(), + 'alternatePersons' => $alternatePersons ?? [], + ] + ); + } + + private function isLastPostDataChanges(FormInterface $form, Request $request, SessionInterface $session): bool + { + if (!$session->has('last_person_data')) { + return true; + } + + $newPost = $this->lastPostDataBuildHash($form, $request); + + $isChanged = $session->get('last_person_data') !== $newPost; + $session->set('last_person_data', $newPost); + + return $isChanged; + } + + /** + * build the hash for posted data. + * + * For privacy reasons, the data are hashed using sha512 + */ + private function lastPostDataBuildHash(FormInterface $form, Request $request): string + { + $fields = []; + $ignoredFields = ['form_status', '_token', 'identifiers']; + + foreach ($request->request->all()[$form->getName()] as $field => $value) { + if (\in_array($field, $ignoredFields, true)) { + continue; + } + $fields[$field] = \is_array($value) ? + \implode(',', $value) : $value; + } + ksort($fields); + + return \hash('sha512', \implode('&', $fields)); + } + + private function lastPostDataReset(SessionInterface $session): void + { + $session->set('last_person_data', ''); + } +} diff --git a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php index 83e811eac..7be6a50b1 100644 --- a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php +++ b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php @@ -11,15 +11,14 @@ declare(strict_types=1); namespace Chill\PersonBundle\Form; -use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Form\Event\CustomizeFormEvent; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillPhoneNumberType; use Chill\MainBundle\Form\Type\PickAddressType; use Chill\MainBundle\Form\Type\PickCenterType; use Chill\MainBundle\Form\Type\PickCivilityType; +use Chill\PersonBundle\Actions\PersonCreate\PersonCreateDTO; use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper; -use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Form\Type\PersonAltNameType; use Chill\PersonBundle\Form\Type\PickGenderType; use Chill\PersonBundle\Security\Authorization\PersonVoter; @@ -33,6 +32,7 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; final class CreationPersonType extends AbstractType { @@ -80,12 +80,10 @@ final class CreationPersonType extends AbstractType ->add('addressForm', CheckboxType::class, [ 'label' => 'Create a household and add an address', 'required' => false, - 'mapped' => false, 'help' => 'A new household will be created. The person will be member of this household.', ]) ->add('address', PickAddressType::class, [ 'required' => false, - 'mapped' => false, 'label' => false, ]); @@ -117,7 +115,7 @@ final class CreationPersonType extends AbstractType public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ - 'data_class' => Person::class, + 'data_class' => PersonCreateDTO::class, 'constraints' => [ new Callback($this->validateCheckedAddress(...)), ], @@ -134,10 +132,12 @@ final class CreationPersonType extends AbstractType public function validateCheckedAddress($data, ExecutionContextInterface $context, $payload): void { - /** @var bool $addressFrom */ - $addressFrom = $context->getObject()->get('addressForm')->getData(); - /** @var ?Address $address */ - $address = $context->getObject()->get('address')->getData(); + if (!$data instanceof PersonCreateDTO) { + throw new UnexpectedTypeException($data, PersonCreateDTO::class); + } + + $addressFrom = $data->addressForm; + $address = $data->address; if ($addressFrom && null === $address) { $context->buildViolation('person_creation.If you want to create an household, an address is required') diff --git a/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonIdentifiersDataMapper.php b/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonIdentifiersDataMapper.php index 8516f18ab..bae359e34 100644 --- a/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonIdentifiersDataMapper.php +++ b/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonIdentifiersDataMapper.php @@ -36,7 +36,7 @@ final readonly class PersonIdentifiersDataMapper implements DataMapperInterface return; } foreach ($forms as $key => $form) { - $form->setData($viewData->getValue()[$key]); + $form->setData($viewData->getValue()[$key] ?? $worker->getDefaultValue()[$key] ?? ''); } } diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php index 2f873a8ab..68620d869 100644 --- a/src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php @@ -70,4 +70,9 @@ final readonly class StringIdentifier implements PersonIdentifierEngineInterface return $violations; } + + public function getDefaultValue(PersonIdentifierDefinition $definition): array + { + return ['content' => '']; + } } diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierEngineInterface.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierEngineInterface.php index 84de2968e..a29172958 100644 --- a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierEngineInterface.php +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierEngineInterface.php @@ -42,4 +42,6 @@ interface PersonIdentifierEngineInterface * @return list */ public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array; + + public function getDefaultValue(PersonIdentifierDefinition $definition): array; } diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierWorker.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierWorker.php index 71aa65468..529a98e84 100644 --- a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierWorker.php +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierWorker.php @@ -62,4 +62,9 @@ readonly class PersonIdentifierWorker { return $this->identifierEngine->validate($identifier, $definition); } + + public function getDefaultValue(): array + { + return $this->identifierEngine->getDefaultValue($this->definition); + } } diff --git a/src/Bundle/ChillPersonBundle/config/services/actions.yaml b/src/Bundle/ChillPersonBundle/config/services/actions.yaml index 4ed808605..220a7483e 100644 --- a/src/Bundle/ChillPersonBundle/config/services/actions.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/actions.yaml @@ -14,3 +14,6 @@ services: Chill\PersonBundle\Actions\PersonEdit\Service\: resource: '../../Actions/PersonEdit/Service' + + Chill\PersonBundle\Actions\PersonCreate\Service\: + resource: '../../Actions/PersonCreate/Service'