diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..432db3f --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,12 @@ +{ + "enabledPlugins": { + "code-review@claude-plugins-official": true, + "github@claude-plugins-official": true, + "feature-dev@claude-plugins-official": true, + "code-simplifier@claude-plugins-official": true, + "ralph-loop@claude-plugins-official": true, + "pr-review-toolkit@claude-plugins-official": true, + "claude-md-management@claude-plugins-official": true, + "php-lsp@claude-plugins-official": true + } +} diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 79c9265..a98fdfa 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -12,35 +12,30 @@ $finder = (new PhpCsFixer\Finder()) ->in(__DIR__.'/src') ->in(__DIR__.'/tests') + ->notPath('Resources/config/') ; return (new PhpCsFixer\Config()) ->setRules([ '@PER-CS' => true, '@Symfony' => true, + '@Symfony:risky' => true, + '@PHP85Migration' => true, + '@PHP8x5Migration:risky' => true, 'header_comment' => [ 'header' => <<<'EOF' - This file is part of the ChamberOrchestra package. +This file is part of the ChamberOrchestra package. - For the full copyright and license information, please view the LICENSE - file that was distributed with this source code. - EOF, +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. +EOF, + 'location' => 'after_declare_strict', + 'separate' => 'both', ], - 'declare_strict_types' => true, 'strict_param' => true, - 'array_syntax' => ['syntax' => 'short'], - 'ordered_imports' => ['sort_algorithm' => 'alpha'], - 'no_unused_imports' => true, - 'trailing_comma_in_multiline' => true, - 'single_quote' => true, - 'global_namespace_import' => [ - 'import_classes' => false, - 'import_constants' => false, - 'import_functions' => false, - ], 'native_function_invocation' => [ 'include' => ['@all'], - 'scope' => 'all', + 'scope' => 'namespaced', 'strict' => true, ], ]) diff --git a/src/Exception/TransformationFailedException.php b/src/Exception/TransformationFailedException.php index 48f43fb..d81a45a 100644 --- a/src/Exception/TransformationFailedException.php +++ b/src/Exception/TransformationFailedException.php @@ -14,13 +14,13 @@ class TransformationFailedException extends \Symfony\Component\Form\Exception\TransformationFailedException implements ExceptionInterface { /** @param list $allowedTypes */ - public static function notAllowedType(mixed $id, array $allowedTypes): TransformationFailedException + public static function notAllowedType(mixed $id, array $allowedTypes): self { return new self( \sprintf( "Passed value is not one of allowed types, allowed types '%s', passed '%s'.", \implode(',', $allowedTypes), - \is_object($id) ? \get_class($id) : \gettype($id) + \is_object($id) ? $id::class : \gettype($id) ) ); } diff --git a/src/Extension/TelExtension.php b/src/Extension/TelExtension.php index 3f3f0b7..24258fc 100644 --- a/src/Extension/TelExtension.php +++ b/src/Extension/TelExtension.php @@ -28,15 +28,13 @@ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->addViewTransformer( new CallbackTransformer( - function (?string $value): ?string { - return $value; - }, - function (?string $value): ?string { + static fn (?string $value): ?string => $value, + static function (?string $value): ?string { if (null === $value || '' === $value) { return null; } - return \preg_replace('/[^\d]/', '', $value) ?? ''; + return \preg_replace('/[^\d+]/', '', $value) ?? ''; } ) ); diff --git a/src/Transformer/ArrayToStringTransformer.php b/src/Transformer/ArrayToStringTransformer.php index a3a752c..b4ce11b 100644 --- a/src/Transformer/ArrayToStringTransformer.php +++ b/src/Transformer/ArrayToStringTransformer.php @@ -38,7 +38,7 @@ public function reverseTransform(mixed $value): array } return \array_map( - static fn (string $value): string => \preg_replace('/[^\d]/', '', $value) ?? '', + static fn (string $value): string => \trim($value), \explode(',', $value) ); } diff --git a/src/Transformer/JsonStringToArrayTransformer.php b/src/Transformer/JsonStringToArrayTransformer.php index 7bd5050..2e8332e 100644 --- a/src/Transformer/JsonStringToArrayTransformer.php +++ b/src/Transformer/JsonStringToArrayTransformer.php @@ -24,9 +24,9 @@ public function transform(mixed $value): ?string } try { - $value = \json_encode($value, JSON_THROW_ON_ERROR); + $value = \json_encode($value, \JSON_THROW_ON_ERROR); } catch (\JsonException $e) { - throw new TransformationFailedException(\sprintf('Could not encode array into json.'), $e->getCode(), $e); + throw new TransformationFailedException('Could not encode array into json.', $e->getCode(), $e); } return $value; @@ -41,9 +41,9 @@ public function reverseTransform(mixed $value): ?array try { /** @var array $decoded */ - $decoded = \json_decode($value, true, 512, JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR); + $decoded = \json_decode($value, true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR); } catch (\JsonException $e) { - throw new TransformationFailedException(\sprintf('Could not parse JSON into array.'), $e->getCode(), $e); + throw new TransformationFailedException('Could not parse JSON into array.', $e->getCode(), $e); } return $decoded; diff --git a/src/Type/HiddenEntityType.php b/src/Type/HiddenEntityType.php index 093f88b..4d439f9 100644 --- a/src/Type/HiddenEntityType.php +++ b/src/Type/HiddenEntityType.php @@ -43,7 +43,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder->addViewTransformer( new CallbackTransformer( - function (?object $value) use ($entityClass, $choiceValue, $em): string|null { + static function (?object $value) use ($entityClass, $choiceValue, $em): string|null { if (null === $value) { return null; } @@ -99,7 +99,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver ->setRequired('class') ->setAllowedTypes('class', 'string') - ->setAllowedValues('class', function (string $value) use ($em): bool { + ->setAllowedValues('class', static function (string $value) use ($em): bool { if (!\class_exists($value)) { return false; } @@ -115,7 +115,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver ->setAllowedTypes('query_builder', ['null', 'callable', QueryBuilder::class]) - ->setNormalizer('query_builder', function (Options $options, mixed $value) use ($em): ?QueryBuilder { + ->setNormalizer('query_builder', static function (Options $options, mixed $value) use ($em): ?QueryBuilder { if (null === $value || $value instanceof QueryBuilder) { return $value; } @@ -139,7 +139,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver ->setAllowedTypes('choice_value', ['null', 'string']) - ->setNormalizer('choice_value', function (Options $options, mixed $value) use ($em): string { + ->setNormalizer('choice_value', static function (Options $options, mixed $value) use ($em): string { /** @var class-string $entityClass */ $entityClass = $options['class']; $class = $em->getClassMetadata($entityClass); diff --git a/src/Type/TimestampType.php b/src/Type/TimestampType.php index a0ce1c0..d167fd4 100644 --- a/src/Type/TimestampType.php +++ b/src/Type/TimestampType.php @@ -24,15 +24,9 @@ class TimestampType extends AbstractType public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'input' => 'datetime_immutable', 'grouping' => false, 'scale' => 0, ]); - - $resolver->setAllowedValues('input', [ - 'datetime', - 'datetime_immutable', - ]); } /** @param array $options */ diff --git a/src/Validator/Constraints/UniqueFieldValidator.php b/src/Validator/Constraints/UniqueFieldValidator.php index c8594c2..0e39d27 100644 --- a/src/Validator/Constraints/UniqueFieldValidator.php +++ b/src/Validator/Constraints/UniqueFieldValidator.php @@ -73,7 +73,7 @@ private function addViolation(UniqueField $constraint, mixed $value): void && (!\is_object($value) || $value instanceof \DateTimeInterface || \method_exists($value, '__toString'))) { $builder->setParameter( '{{ value }}', - $this->formatValue($value, self::PRETTY_DATE & self::OBJECT_TO_STRING) + $this->formatValue($value, self::PRETTY_DATE | self::OBJECT_TO_STRING) ); } $builder diff --git a/src/View/FailureView.php b/src/View/FailureView.php index 60d2f97..7e7fe05 100644 --- a/src/View/FailureView.php +++ b/src/View/FailureView.php @@ -13,6 +13,7 @@ use ChamberOrchestra\ViewBundle\View\ResponseView; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; class FailureView extends ResponseView @@ -20,9 +21,9 @@ class FailureView extends ResponseView protected string $type = 'https://datatracker.ietf.org/doc/html/rfc9110#section-15'; protected readonly string $title; - public function __construct(int $status = JsonResponse::HTTP_BAD_REQUEST, string $message = 'Validation Failed') + public function __construct(int $status = JsonResponse::HTTP_BAD_REQUEST, ?string $message = null) { - $this->title = $message; + $this->title = $message ?? Response::$statusTexts[$status] ?? 'Error'; parent::__construct($status, ['Content-Type' => 'application/problem+json']); } diff --git a/src/View/ValidationFailedView.php b/src/View/ValidationFailedView.php index 5524c8c..0558db3 100644 --- a/src/View/ValidationFailedView.php +++ b/src/View/ValidationFailedView.php @@ -24,7 +24,7 @@ class ValidationFailedView extends FailureView /** @param list $violations */ public function __construct(array $violations = [], string $message = 'Validation Failed') { - $this->detail = \implode("\n", \array_map(fn (ViolationView $error): string => $error->title, $violations)); + $this->detail = \implode("\n", \array_map(static fn (ViolationView $error): string => $error->title, $violations)); $this->violations = $violations; parent::__construct(JsonResponse::HTTP_UNPROCESSABLE_ENTITY, $message); diff --git a/tests/Integrational/HiddenEntityTypeQueryBuilderIntegrationTest.php b/tests/Integrational/HiddenEntityTypeQueryBuilderIntegrationTest.php index 61710b5..7b52898 100644 --- a/tests/Integrational/HiddenEntityTypeQueryBuilderIntegrationTest.php +++ b/tests/Integrational/HiddenEntityTypeQueryBuilderIntegrationTest.php @@ -47,9 +47,7 @@ public function testQueryBuilderAndChoiceValueAreUsed(): void 'class' => TestUser::class, 'choice_value' => 'email', 'data_class' => null, - 'query_builder' => static function (EntityRepository $repository) { - return $repository->createQueryBuilder('u'); - }, + 'query_builder' => static fn (EntityRepository $repository) => $repository->createQueryBuilder('u'), ]); $form->submit('user@example.com'); diff --git a/tests/Unit/ApiFormTraitTest.php b/tests/Unit/ApiFormTraitTest.php index 9be70b7..774d10e 100644 --- a/tests/Unit/ApiFormTraitTest.php +++ b/tests/Unit/ApiFormTraitTest.php @@ -39,7 +39,7 @@ public function exposeConvertRequestToArray(Request $request): array [], [], ['CONTENT_TYPE' => 'application/json'], - \json_encode(['payload' => ['id' => 1]], JSON_THROW_ON_ERROR) + \json_encode(['payload' => ['id' => 1]], \JSON_THROW_ON_ERROR) ); $request->files->set('file', ['name' => 'upload.txt']); @@ -98,7 +98,7 @@ public function testHandleApiCallThrowsOnNullRequest(): void $stack = new RequestStack(); $container = $this->createStub(ContainerInterface::class); - $container->method('get')->willReturnCallback(fn (string $id) => match ($id) { + $container->method('get')->willReturnCallback(static fn (string $id) => match ($id) { 'request_stack' => $stack, }); diff --git a/tests/Unit/Extension/TelExtensionTest.php b/tests/Unit/Extension/TelExtensionTest.php index 6fd9fa0..c7a8463 100644 --- a/tests/Unit/Extension/TelExtensionTest.php +++ b/tests/Unit/Extension/TelExtensionTest.php @@ -42,6 +42,7 @@ public function testBuildFormAddsTransformer(): void $extension->buildForm($builder, []); self::assertSame('123', $captured->reverseTransform('1 (2)3')); + self::assertSame('+15551234567', $captured->reverseTransform('+1 (555) 123-4567')); self::assertNull($captured->reverseTransform('')); } } diff --git a/tests/Unit/FormTraitTest.php b/tests/Unit/FormTraitTest.php index 24b8975..9c88d57 100644 --- a/tests/Unit/FormTraitTest.php +++ b/tests/Unit/FormTraitTest.php @@ -101,7 +101,7 @@ public function exposeOnFormSubmitted(FormInterface $form, ?callable $callable = $form->method('isValid')->willReturn(true); $form->method('getData')->willReturn(['id' => 1]); - $response = $host->exposeOnFormSubmitted($form, fn () => ['ok' => true]); + $response = $host->exposeOnFormSubmitted($form, static fn () => ['ok' => true]); self::assertInstanceOf(DataView::class, $response); } @@ -202,7 +202,7 @@ public function testCreateRedirectResponseReturnsViewForXmlHttpRequest(): void $stack->push($request); $container = $this->createStub(ContainerInterface::class); - $container->method('get')->willReturnCallback(fn (string $id) => match ($id) { + $container->method('get')->willReturnCallback(static fn (string $id) => match ($id) { 'request_stack' => $stack, }); @@ -243,7 +243,7 @@ public function testCreateRedirectResponseReturnsResponseForNonXmlHttpRequest(): $stack->push($request); $container = $this->createStub(ContainerInterface::class); - $container->method('get')->willReturnCallback(fn (string $id) => match ($id) { + $container->method('get')->willReturnCallback(static fn (string $id) => match ($id) { 'request_stack' => $stack, }); @@ -284,7 +284,7 @@ public function testCreateSuccessHtmlResponseHandlesXmlHttpRequest(): void $stack->push($request); $container = $this->createStub(ContainerInterface::class); - $container->method('get')->willReturnCallback(fn (string $id) => match ($id) { + $container->method('get')->willReturnCallback(static fn (string $id) => match ($id) { 'request_stack' => $stack, }); @@ -329,7 +329,7 @@ public function testCreateSuccessHtmlResponseHandlesNonXmlHttpRequest(): void $stack->push($request); $container = $this->createStub(ContainerInterface::class); - $container->method('get')->willReturnCallback(fn (string $id) => match ($id) { + $container->method('get')->willReturnCallback(static fn (string $id) => match ($id) { 'request_stack' => $stack, }); @@ -370,7 +370,7 @@ public function testHandleFormCallThrowsOnNullRequest(): void $stack = new RequestStack(); $container = $this->createStub(ContainerInterface::class); - $container->method('get')->willReturnCallback(function (string $id) use ($stack) { + $container->method('get')->willReturnCallback(static function (string $id) use ($stack) { return match ($id) { 'request_stack' => $stack, 'form.factory' => Forms::createFormFactory(), @@ -405,7 +405,7 @@ public function testCreateRedirectResponseFallsBackWhenNoRequest(): void $stack = new RequestStack(); $container = $this->createStub(ContainerInterface::class); - $container->method('get')->willReturnCallback(fn (string $id) => match ($id) { + $container->method('get')->willReturnCallback(static fn (string $id) => match ($id) { 'request_stack' => $stack, }); diff --git a/tests/Unit/Transformer/ArrayToStringTransformerTest.php b/tests/Unit/Transformer/ArrayToStringTransformerTest.php index 50cd06c..8b365d9 100644 --- a/tests/Unit/Transformer/ArrayToStringTransformerTest.php +++ b/tests/Unit/Transformer/ArrayToStringTransformerTest.php @@ -26,6 +26,13 @@ public function testTransformAndReverseTransform(): void self::assertSame(['123', '456'], $transformer->reverseTransform('123, 456')); } + public function testReverseTransformTrimsWhitespace(): void + { + $transformer = new ArrayToStringTransformer(); + + self::assertSame(['foo', 'bar', 'baz'], $transformer->reverseTransform('foo, bar, baz')); + } + public function testTransformRejectsInvalidType(): void { $transformer = new ArrayToStringTransformer(); diff --git a/tests/Unit/Type/TimestampTypeTest.php b/tests/Unit/Type/TimestampTypeTest.php index 9a64af7..73409f7 100644 --- a/tests/Unit/Type/TimestampTypeTest.php +++ b/tests/Unit/Type/TimestampTypeTest.php @@ -14,6 +14,7 @@ use ChamberOrchestra\FormBundle\Transformer\DateTimeToNumberTransformer; use ChamberOrchestra\FormBundle\Type\TimestampType; use PHPUnit\Framework\TestCase; +use Symfony\Component\Clock\DatePoint; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -28,12 +29,11 @@ public function testConfigureOptionsSetsDefaults(): void $type->configureOptions($resolver); $options = $resolver->resolve(); - self::assertSame('datetime_immutable', $options['input']); self::assertFalse($options['grouping']); self::assertSame(0, $options['scale']); } - public function testBuildFormAddsTransformer(): void + public function testBuildFormAddsDatePointTransformer(): void { $type = new TimestampType(); $builder = $this->createMock(FormBuilderInterface::class); @@ -41,9 +41,17 @@ public function testBuildFormAddsTransformer(): void $builder ->expects($this->once()) ->method('addModelTransformer') - ->with($this->callback(static fn ($transformer) => $transformer instanceof DateTimeToNumberTransformer)); + ->with($this->callback(static function ($transformer): bool { + if (!$transformer instanceof DateTimeToNumberTransformer) { + return false; + } - $type->buildForm($builder, ['input' => 'datetime_immutable']); + $result = $transformer->reverseTransform(0); + + return $result instanceof DatePoint; + })); + + $type->buildForm($builder, []); } public function testParentIsNumberType(): void diff --git a/tests/Unit/View/FailureViewTest.php b/tests/Unit/View/FailureViewTest.php index 2a0183b..7a2cb14 100644 --- a/tests/Unit/View/FailureViewTest.php +++ b/tests/Unit/View/FailureViewTest.php @@ -41,4 +41,16 @@ public function testNormalizeUsesNormalizer(): void self::assertSame('Bad', $data['title']); self::assertSame('https://datatracker.ietf.org/doc/html/rfc9110#section-15', $data['type']); } + + public function testDefaultTitleDerivedFromStatusCode(): void + { + $normalizer = $this->createMock(NormalizerInterface::class); + $normalizer->method('normalize')->willReturnCallback(static fn (array $data) => $data); + + $badRequest = new FailureView(JsonResponse::HTTP_BAD_REQUEST); + self::assertSame('Bad Request', $badRequest->normalize($normalizer)['title']); + + $serverError = new FailureView(JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + self::assertSame('Internal Server Error', $serverError->normalize($normalizer)['title']); + } }