Skip to content
Merged

8.0 #16

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -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
}
}
27 changes: 11 additions & 16 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
])
Expand Down
4 changes: 2 additions & 2 deletions src/Exception/TransformationFailedException.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
class TransformationFailedException extends \Symfony\Component\Form\Exception\TransformationFailedException implements ExceptionInterface
{
/** @param list<string> $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)
)
);
}
Expand Down
8 changes: 3 additions & 5 deletions src/Extension/TelExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) ?? '';
}
)
);
Expand Down
2 changes: 1 addition & 1 deletion src/Transformer/ArrayToStringTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
Expand Down
8 changes: 4 additions & 4 deletions src/Transformer/JsonStringToArrayTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,9 +41,9 @@ public function reverseTransform(mixed $value): ?array

try {
/** @var array<mixed> $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;
Expand Down
8 changes: 4 additions & 4 deletions src/Type/HiddenEntityType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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);
Expand Down
6 changes: 0 additions & 6 deletions src/Type/TimestampType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, mixed> $options */
Expand Down
2 changes: 1 addition & 1 deletion src/Validator/Constraints/UniqueFieldValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/View/FailureView.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@

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
{
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']);
}

Expand Down
2 changes: 1 addition & 1 deletion src/View/ValidationFailedView.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class ValidationFailedView extends FailureView
/** @param list<ViolationView> $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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
4 changes: 2 additions & 2 deletions tests/Unit/ApiFormTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);

Expand Down Expand Up @@ -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,
});

Expand Down
1 change: 1 addition & 0 deletions tests/Unit/Extension/TelExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(''));
}
}
14 changes: 7 additions & 7 deletions tests/Unit/FormTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
});

Expand Down Expand Up @@ -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,
});

Expand Down Expand Up @@ -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,
});

Expand Down Expand Up @@ -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,
});

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
});

Expand Down
7 changes: 7 additions & 0 deletions tests/Unit/Transformer/ArrayToStringTransformerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
16 changes: 12 additions & 4 deletions tests/Unit/Type/TimestampTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,22 +29,29 @@ 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);

$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
Expand Down
12 changes: 12 additions & 0 deletions tests/Unit/View/FailureViewTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}
}