diff --git a/README.md b/README.md index 40a1ce5..ed11605 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,18 @@ $builder->add('period', PeriodType::class, [ ]); ``` +#### allow_null + +**type**: `bool` **default**: `true` +Additional options to be used for the *boundaryType* form child. + +```php +$builder->add('period', PeriodType::class, [ + 'allow_null' => false, + // Allow to trigger an error when your Period property is not nullable. +]); +``` + ## Configuration (completely optional) This bundle is build thinking how to save you time and follow best practices as close as possible. diff --git a/src/Form/DataMapper/PeriodDataMapper.php b/src/Form/DataMapper/PeriodDataMapper.php index 0e8cbb9..160fd8f 100644 --- a/src/Form/DataMapper/PeriodDataMapper.php +++ b/src/Form/DataMapper/PeriodDataMapper.php @@ -9,6 +9,7 @@ use League\Period\Exception; use League\Period\Period; use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\FormInterface; @@ -18,6 +19,7 @@ class PeriodDataMapper implements DataMapperInterface private string $startDateChildName; private string $endDateChildName; private string $boundaryTypeChildName; + private bool $allowNull; private const BOUNDARY_TYPES = [ Period::INCLUDE_START_EXCLUDE_END, @@ -30,13 +32,15 @@ public function __construct( string $defaultBoundaryType = Period::INCLUDE_START_EXCLUDE_END, string $startDateChildName = 'startDate', string $endDateChildName = 'endDate', - string $boundaryTypeChildName = 'boundaryType' + string $boundaryTypeChildName = 'boundaryType', + bool $allowNull = true ) { $this->assertValidBoundaryType($defaultBoundaryType); $this->defaultBoundaryType = $defaultBoundaryType; $this->startDateChildName = $startDateChildName; $this->endDateChildName = $endDateChildName; $this->boundaryTypeChildName = $boundaryTypeChildName; + $this->allowNull = $allowNull; } private function assertValidBoundaryType(string $boundaryType): void @@ -95,11 +99,60 @@ public function mapFormsToData(iterable $forms, &$viewData): void $viewData = null; + if (! $startDate instanceof \DateTimeInterface && $endDate instanceof \DateTimeInterface) { + $failure = new TransformationFailedException(\sprintf( + 'Start date should be a %s', + \DateTimeInterface::class + )); + $failure->setInvalidMessage( + 'Start date should be valid. {{ startDate }} is not a valid date.', + ['{{ startDate }}' => json_encode($startDate)] + ); + throw $failure; + } + + if (! $endDate instanceof \DateTimeInterface && $startDate instanceof \DateTimeInterface) { + $failure = new TransformationFailedException(\sprintf( + 'End date should be a %s', + \DateTimeInterface::class + )); + $failure->setInvalidMessage( + 'End date should be valid. {{ endDate }} is not a valid date.', + ['{{ endDate }}' => json_encode($endDate)] + ); + throw $failure; + } + if ($startDate instanceof \DateTimeInterface && $endDate instanceof \DateTimeInterface) { + if ($startDate > $endDate) { + $failure = new TransformationFailedException('Start date should be greater or equals then the end date.'); + $failure->setInvalidMessage('Start date should be greater or equals then the end date', [ + '{{ startDate }}' => json_encode($startDate), + '{{ endDate }}' => json_encode($endDate), + ]); + throw $failure; + } + try { $viewData = Period::fromDatepoint($startDate, $endDate, $boundaryType); } catch (Exception $e) { + $failure = new TransformationFailedException('Invalid Period', 0, $e); + $failure->setInvalidMessage('Invalid Period.', [ + '{{ startDate }}' => json_encode($startDate), + '{{ endDate }}' => json_encode($endDate), + ]); + + throw $failure; } } + + if (!$this->allowNull && $viewData === null) { + $failure = new TransformationFailedException('A valid Period is required'); + $failure->setInvalidMessage('A valid Period is required.', [ + '{{ startDate }}' => json_encode($startDate), + '{{ endDate }}' => json_encode($endDate), + ]); + throw $failure; + } } } diff --git a/src/Form/PeriodType.php b/src/Form/PeriodType.php index a0a57fe..6627681 100644 --- a/src/Form/PeriodType.php +++ b/src/Form/PeriodType.php @@ -21,7 +21,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add($options['start_date_child_name'], $options['start_date_form_type'], \array_merge_recursive( [ - 'label' => 'Start date', + 'label' => 'Start', 'input' => 'datetime_immutable', 'property_path' => 'startDate', ], @@ -29,16 +29,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void )); $builder->add($options['end_date_child_name'], $options['end_date_form_type'], \array_merge_recursive( [ - 'label' => 'End date', + 'label' => 'End', 'input' => 'datetime_immutable', - 'property_path' => 'endDate', - 'constraints' => [ - new GreaterThanOrEqualFormChildren([ - 'child' => $options['end_date_child_name'], - 'gteChild' => $options['start_date_child_name'], - 'useParent' => true, - ]), - ], + 'property_path' => 'endDate' ], $options['end_date_options'] )); @@ -56,7 +49,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $options['default_boundary_type'], $options['start_date_child_name'], $options['end_date_child_name'], - $options['boundary_type_child_name'] + $options['boundary_type_child_name'], + $options['allow_null'] ) ); } @@ -97,6 +91,7 @@ public function configureOptions(OptionsResolver $resolver): void 'end_date_options' => [], 'boundary_type_child_name' => 'boundary', 'boundary_type_options' => [], + 'allow_null' => true ]); $resolver->setAllowedValues('default_boundary_type', [ @@ -113,5 +108,6 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('start_date_child_name', 'string'); $resolver->setAllowedTypes('end_date_child_name', 'string'); $resolver->setAllowedTypes('boundary_type_child_name', 'string'); + $resolver->setAllowedTypes('allow_null', 'bool'); } } diff --git a/src/Form/Validator/GreaterThanOrEqualFormChildren.php b/src/Form/Validator/GreaterThanOrEqualFormChildren.php deleted file mode 100644 index f69adb2..0000000 --- a/src/Form/Validator/GreaterThanOrEqualFormChildren.php +++ /dev/null @@ -1,37 +0,0 @@ -message = $message ?? $this->message; - - if (empty($this->child)) { - throw new ConstraintDefinitionException(sprintf( - 'The "%s" constraint requires "child" option to be set.', - static::class - )); - } - - if (empty($this->gteChild)) { - throw new ConstraintDefinitionException(sprintf( - 'The "%s" constraint requires "$gteChild" option to be set.', - static::class - )); - } - } -} diff --git a/src/Form/Validator/GreaterThanOrEqualFormChildrenValidator.php b/src/Form/Validator/GreaterThanOrEqualFormChildrenValidator.php deleted file mode 100644 index 4f7977e..0000000 --- a/src/Form/Validator/GreaterThanOrEqualFormChildrenValidator.php +++ /dev/null @@ -1,92 +0,0 @@ -context->getObject(); - if (!$object instanceof FormInterface) { - throw new ConstraintDefinitionException(sprintf('"%s" constraint should only be used in Forms ', get_debug_type($constraint))); - } - - $form = $constraint->useParent ? $object->getParent() : $object; - - if (null === $form) { - throw new ConstraintDefinitionException(sprintf('"%s" cannot be used on root forms', get_debug_type($constraint))); - } - - if (!$form->has($constraint->child)) { - throw new ConstraintDefinitionException(sprintf('"%s" cannot find child "%s" on form', get_debug_type($constraint), $constraint->child)); - } - - if (!$form->has($constraint->gteChild)) { - throw new ConstraintDefinitionException(sprintf('"%s" cannot find child "%s" on form', get_debug_type($constraint), $constraint->gteChild)); - } - - $comparedValue = $form->get($constraint->gteChild)->getData(); - $value = $form->get($constraint->child)->getData(); - - if (null === $value || null === $comparedValue) { - return; - } - - // Convert strings to DateTimes if comparing another DateTime - // This allows to compare with any date/time value supported by - // the DateTime constructor: - // https://php.net/datetime.formats - if (\is_string($comparedValue) && $value instanceof \DateTimeInterface) { - // If $value is immutable, convert the compared value to a DateTimeImmutable too, otherwise use DateTime - $dateTimeClass = $value instanceof \DateTimeImmutable ? \DateTimeImmutable::class : \DateTime::class; - - try { - $comparedValue = new $dateTimeClass($comparedValue); - } catch (\Exception $e) { - throw new ConstraintDefinitionException(sprintf('The compared value "%s" could not be converted to a "%s" instance in the "%s" constraint.', $comparedValue, $dateTimeClass, get_debug_type($constraint))); - } - } - - if (!$this->compareValues($value, $comparedValue)) { - $violationBuilder = $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value, self::OBJECT_TO_STRING | self::PRETTY_DATE)) - ->setParameter('{{ compared_value }}', $this->formatValue($comparedValue, self::OBJECT_TO_STRING | self::PRETTY_DATE)) - ->setParameter('{{ compared_value_type }}', $this->formatTypeOf($comparedValue)) - ->setCode($this->getErrorCode()); - - $violationBuilder->addViolation(); - } - } - - /** - * {@inheritdoc} - */ - protected function compareValues($value1, $value2): bool - { - return null === $value2 || $value1 >= $value2; - } - - /** - * {@inheritdoc} - */ - protected function getErrorCode(): string - { - return GreaterThanOrEqual::TOO_LOW_ERROR; - } -} diff --git a/src/Resources/translations/AndantePeriodBundle.en.xlf b/src/Resources/translations/AndantePeriodBundle.en.xlf index a909531..16ec926 100644 --- a/src/Resources/translations/AndantePeriodBundle.en.xlf +++ b/src/Resources/translations/AndantePeriodBundle.en.xlf @@ -19,17 +19,37 @@ Exclude both start and end - Start date - Start date + Start + Start - End date - End date + End + End Boundary type Boundary type + + Start date should be valid. {{ startDate }} is not a valid date. + La data di inizio dovrebbe essere valida. {{ startDate }} non è una data valida. + + + End date should be valid. {{ endDate }} is not a valid date. + La data di fine dovrebbe essere valida. {{ endDate }} non è una data valida. + + + Start date should be greater or equals then the end date. + La data di inizio dovrebbe essere maggiore o uguale della data di fine. + + + Invalid Period. + Periodo non valido. + + + A valid Period is required. + Un periodo è obbligatorio. + diff --git a/src/Resources/translations/AndantePeriodBundle.it.xlf b/src/Resources/translations/AndantePeriodBundle.it.xlf index 93966d6..f9cf56f 100644 --- a/src/Resources/translations/AndantePeriodBundle.it.xlf +++ b/src/Resources/translations/AndantePeriodBundle.it.xlf @@ -19,17 +19,37 @@ Escludi sia inizio che fine - Start date - Data di inizio + Start + Inizio - End date - Data di fine + End + Fine Boundary type Estremità + + Start date should be valid. {{ startDate }} is not a valid date. + Start date should be valid. {{ startDate }} is not a valid date. + + + End date should be valid. {{ endDate }} is not a valid date. + End date should be valid. {{ endDate }} is not a valid date. + + + Start date should be greater or equals then the end date. + Start date should be greater or equals then the end date. + + + Invalid Period. + Invalid Period. + + + A valid Period is required. + A valid Period is required. + diff --git a/tests/Fixtures/Entity/ArticleWithNotNullablePeriod.php b/tests/Fixtures/Entity/ArticleWithNotNullablePeriod.php new file mode 100644 index 0000000..35f576b --- /dev/null +++ b/tests/Fixtures/Entity/ArticleWithNotNullablePeriod.php @@ -0,0 +1,60 @@ +period = $period; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getPeriod(): Period + { + return $this->period; + } + + public function setPeriod(Period $period): self + { + $this->period = $period; + return $this; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + return $this; + } +} diff --git a/tests/Form/ArticleWithNotNullablePeriodType.php b/tests/Form/ArticleWithNotNullablePeriodType.php new file mode 100644 index 0000000..b420845 --- /dev/null +++ b/tests/Form/ArticleWithNotNullablePeriodType.php @@ -0,0 +1,39 @@ +add('period', PeriodType::class, [ + 'start_date_options' => [ + 'widget' => 'single_text', + ], + 'end_date_options' => [ + 'widget' => 'single_text', + ], + 'constraints' => [ + new NotBlank(), + ], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ArticleWithNotNullablePeriod::class, + ]); + } +} diff --git a/tests/Form/PeriodTypeTest.php b/tests/Form/PeriodTypeTest.php index 15dc8a5..a2533cc 100644 --- a/tests/Form/PeriodTypeTest.php +++ b/tests/Form/PeriodTypeTest.php @@ -69,7 +69,7 @@ public function testCreateInvalidPeriod(): void 'end' => '2020-01-01T00:00:00', ]); - $errors = $form->get('end')->getErrors(); + $errors = $form->getErrors(); self::assertCount(1, $errors); } @@ -143,4 +143,103 @@ public function testSetToNull(): void self::assertNull($form->get('end')->getData()); self::assertNull($form->getData()); } + + public function testSetToInvalidPeriod(): void + { + $builder = $this->getFormFactory()->createBuilder(PeriodType::class, null, [ + 'start_date_options' => [ + 'widget' => 'single_text', + ], + 'end_date_options' => [ + 'widget' => 'single_text', + ], + ]); + $form = $builder->getForm(); + + $form->submit([ + 'start' => '2020-01-02T00:00:00', + 'end' => '2020-01-01T00:00:00', + ]); + + $errors = $form->getErrors(); + self::assertCount(1, $errors); + } + + public function testSetToInvalidPeriodWithOnlyStart(): void + { + $builder = $this->getFormFactory()->createBuilder(PeriodType::class, null, [ + 'start_date_options' => [ + 'widget' => 'single_text', + ], + 'end_date_options' => [ + 'widget' => 'single_text', + ], + ]); + $form = $builder->getForm(); + + $form->submit([ + 'start' => '2020-01-02T00:00:00', + ]); + + $errors = $form->getErrors(); + self::assertCount(1, $errors); + } + + public function testSetToInvalidPeriodWithOnlyEnd(): void + { + $builder = $this->getFormFactory()->createBuilder(PeriodType::class, null, [ + 'start_date_options' => [ + 'widget' => 'single_text', + ], + 'end_date_options' => [ + 'widget' => 'single_text', + ], + ]); + $form = $builder->getForm(); + + $form->submit([ + 'end' => '2020-01-02T00:00:00', + ]); + + $errors = $form->getErrors(); + self::assertCount(1, $errors); + } + + public function testSetToInvalidPeriodWithAllowNullOptionSetToFalse(): void + { + $builder = $this->getFormFactory()->createBuilder(PeriodType::class, null, [ + 'start_date_options' => [ + 'widget' => 'single_text', + ], + 'end_date_options' => [ + 'widget' => 'single_text', + ], + 'allow_null' => false, + ]); + $form = $builder->getForm(); + + $form->submit([]); + + $errors = $form->getErrors(); + self::assertCount(1, $errors); + } + + public function testSetToInvalidPeriodWithAllowNullOptionSetToTrue(): void + { + $builder = $this->getFormFactory()->createBuilder(PeriodType::class, null, [ + 'start_date_options' => [ + 'widget' => 'single_text', + ], + 'end_date_options' => [ + 'widget' => 'single_text', + ], + 'allow_null' => true, + ]); + $form = $builder->getForm(); + + $form->submit([]); + + $errors = $form->getErrors(); + self::assertCount(0, $errors); + } }