From da3addf949845230e83d349c4b2bb9480174aa77 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 24 Feb 2023 11:39:10 +0100 Subject: [PATCH 01/37] Import value resolver from document-library --- src/Filesystem/Attribute/UploadedFile.php | 24 +++ .../ZenstruckFilesystemExtension.php | 15 ++ .../HttpKernel/PendingFileValueResolver.php | 108 ++++++++++ .../HttpKernel/RequestFilesExtractor.php | 90 +++++++++ .../HttpKernel/RequestFilesExtractorTest.php | 184 ++++++++++++++++++ 5 files changed, 421 insertions(+) create mode 100644 src/Filesystem/Attribute/UploadedFile.php create mode 100644 src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php create mode 100644 src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php create mode 100644 tests/Filesystem/Symfony/HttpKernel/RequestFilesExtractorTest.php diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php new file mode 100644 index 00000000..0ef55465 --- /dev/null +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Filesystem\Attribute; + +/** + * @author Jakub Caban + */ +#[\Attribute(\Attribute::TARGET_PARAMETER)] +final class UploadedFile +{ + public function __construct( + public ?string $path = null + ) { + } +} diff --git a/src/Filesystem/Symfony/DependencyInjection/ZenstruckFilesystemExtension.php b/src/Filesystem/Symfony/DependencyInjection/ZenstruckFilesystemExtension.php index 3b56797b..a2e3dce3 100644 --- a/src/Filesystem/Symfony/DependencyInjection/ZenstruckFilesystemExtension.php +++ b/src/Filesystem/Symfony/DependencyInjection/ZenstruckFilesystemExtension.php @@ -45,6 +45,8 @@ use Zenstruck\Filesystem\Symfony\Command\FilesystemPurgeCommand; use Zenstruck\Filesystem\Symfony\Form\PendingFileType; use Zenstruck\Filesystem\Symfony\HttpKernel\FilesystemDataCollector; +use Zenstruck\Filesystem\Symfony\HttpKernel\PendingFileValueResolver; +use Zenstruck\Filesystem\Symfony\HttpKernel\RequestFilesExtractor; use Zenstruck\Filesystem\Symfony\Routing\RoutePublicUrlGenerator; use Zenstruck\Filesystem\Symfony\Routing\RouteTemporaryUrlGenerator; use Zenstruck\Filesystem\Symfony\Routing\RouteTransformUrlGenerator; @@ -155,6 +157,19 @@ private function registerDoctrine(ContainerBuilder $container, array $config): v $listener->addTag('doctrine.event_listener', ['event' => 'postRemove']); } + // value resolver + $container->register('.zenstruck_document.value_resolver.request_files_extractor', RequestFilesExtractor::class) + ->addArgument(new Reference('property_accessor')) + ; + $container->register('.zenstruck_document.value_resolver.pending_document', PendingFileValueResolver::class) + ->addTag('controller.argument_value_resolver', ['priority' => 110]) + ->addArgument( + new ServiceLocatorArgument([ + RequestFilesExtractor::class => new Reference('.zenstruck_document.value_resolver.request_files_extractor'), + ]) + ) + ; + if (isset($container->getParameter('kernel.bundles')['TwigBundle'])) { $container->register('.zenstruck_filesystem.doctrine.twig_extension', MappingManagerExtension::class) ->addTag('twig.extension') diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php new file mode 100644 index 00000000..a8f2e84f --- /dev/null +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Filesystem\Symfony\HttpKernel; + + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Contracts\Service\ServiceProviderInterface; +use Zenstruck\Filesystem\Attribute\UploadedFile; +use Zenstruck\Filesystem\Node\File\PendingFile; + +/** + * @author Jakub Caban + * + * @internal + */ +if (\interface_exists(ValueResolverInterface::class)) { + class PendingFileValueResolver implements ValueResolverInterface + { + public function __construct( + /** @var ServiceProviderInterface $locator */ + private ServiceProviderInterface $locator + ) { + } + + /** + * @return iterable + */ + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + $attributes = $argument->getAttributes(UploadedFile::class); + + if ( + empty($attributes) + && PendingFile::class !== $argument->getType() + ) { + return []; + } + + $path = $attributes[0]?->path + ?? $argument->getName(); + + return [ + $this->extractor()->extractFilesFromRequest( + $request, + $path, + PendingFile::class !== $argument->getType() + ), + ]; + } + + private function extractor(): RequestFilesExtractor + { + return $this->locator->get(RequestFilesExtractor::class); + } + } +} else { + class PendingFileValueResolver implements ArgumentValueResolverInterface + { + public function __construct( + /** @var ServiceProviderInterface $locator */ + private ServiceProviderInterface $locator + ) { + } + + public function supports(Request $request, ArgumentMetadata $argument): bool + { + return PendingFile::class === $argument->getType() + || !empty($argument->getAttributes(UploadedFile::class)); + } + + /** + * @return iterable + */ + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + $attributes = $argument->getAttributes(UploadedFile::class); + \assert(!empty($attributes)); + + $path = $attributes[0]?->path + ?? $argument->getName(); + + return [ + $this->extractor()->extractFilesFromRequest( + $request, + $path, + PendingFile::class !== $argument->getType() + ), + ]; + } + + private function extractor(): RequestFilesExtractor + { + return $this->locator->get(RequestFilesExtractor::class); + } + } +} diff --git a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php new file mode 100644 index 00000000..d97d8ad6 --- /dev/null +++ b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Filesystem\Symfony\HttpKernel; + + +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\PropertyAccess\PropertyAccessor; +use Zenstruck\Filesystem\Node\File\PendingFile; + +/** + * @author Jakub Caban + */ +class RequestFilesExtractor +{ + public function __construct(private PropertyAccessor $propertyAccessor) + { + } + + public function extractFilesFromRequest( + Request $request, + string $path, + bool $returnArray = false + ): PendingFile|array|null { + $path = $this->canonizePath($path); + + $files = $this->propertyAccessor->getValue($request->files->all(), $path); + + if ($returnArray) { + if (!$files) { + return []; + } + + if (!\is_array($files)) { + $files = [$files]; + } + + return \array_map( + static fn(UploadedFile $file) => new PendingFile($file), + $files + ); + } + + if (\is_array($files)) { + throw new \LogicException(\sprintf('Could not extract files from request for "%s" path: expecting a single file, got %d files.', $path, \count($files))); + } + + if (!$files) { + return null; + } + + return new PendingFile($files); + } + + /** + * Convert HTML paths to PropertyAccessor compatible. + * Examples: "data[file]" -> "[data][file]", "files[]" -> "[files]". + */ + private function canonizePath(string $path): string + { + $path = \preg_replace( + '/\[]$/', + '', + $path + ); + // Correct arguments passed to preg_replace guarantee string return + \assert(\is_string($path)); + + if ('[' !== $path[0]) { + $path = \preg_replace( + '/^([^[]+)/', + '[$1]', + $path + ); + // Correct arguments passed to preg_replace guarantee string return + \assert(\is_string($path)); + } + + return $path; + } +} diff --git a/tests/Filesystem/Symfony/HttpKernel/RequestFilesExtractorTest.php b/tests/Filesystem/Symfony/HttpKernel/RequestFilesExtractorTest.php new file mode 100644 index 00000000..3871d69f --- /dev/null +++ b/tests/Filesystem/Symfony/HttpKernel/RequestFilesExtractorTest.php @@ -0,0 +1,184 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Tests\Filesystem\Symfony\HttpKernel; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\PropertyAccess\PropertyAccessor; +use Zenstruck\Filesystem\Node\File\PendingFile; +use Zenstruck\Filesystem\Symfony\HttpKernel\RequestFilesExtractor; + +/** + * @author Jakub Caban + */ +class RequestFilesExtractorTest extends TestCase +{ + /** + * @test + */ + public function returns_null_for_empty_request(): void + { + $extractor = self::extractor(); + + $request = Request::create(''); + + self::assertNull( + $extractor->extractFilesFromRequest($request, 'file') + ); + } + + /** + * @test + */ + public function returns_null_for_empty_path(): void + { + $extractor = self::extractor(); + + $request = Request::create(''); + $request->files->set('upload', self::uploadedFile()); + + self::assertNull( + $extractor->extractFilesFromRequest($request, 'file') + ); + } + + /** + * @test + */ + public function returns_file_for_correct_path(): void + { + $extractor = self::extractor(); + + $request = Request::create(''); + $request->files->set('file', self::uploadedFile()); + + $document = $extractor->extractFilesFromRequest($request, 'file'); + self::assertNotNull($document); + self::assertInstanceOf(PendingFile::class, $document); + self::assertSame("some content\n", $document->contents()); + } + + /** + * @test + */ + public function returns_file_for_correct_nested_path(): void + { + $extractor = self::extractor(); + + $request = Request::create(''); + $request->files->set('data', ['file' => self::uploadedFile()]); + + $document = $extractor->extractFilesFromRequest($request, 'data[file]'); + self::assertNotNull($document); + self::assertInstanceOf(PendingFile::class, $document); + self::assertSame("some content\n", $document->contents()); + } + + /** + * @test + */ + public function throws_for_single_file_with_array_of_files(): void + { + $extractor = self::extractor(); + + $request = Request::create(''); + $request->files->set('upload', [self::uploadedFile()]); + + $this->expectException(\LogicException::class); + $extractor->extractFilesFromRequest($request, 'upload'); + } + + /** + * @test + */ + public function returns_empty_array_for_empty_request(): void + { + $extractor = self::extractor(); + + $request = Request::create(''); + + self::assertSame( + [], + $extractor->extractFilesFromRequest($request, 'file', true) + ); + } + + /** + * @test + */ + public function returns_empty_array_for_empty_path(): void + { + $extractor = self::extractor(); + + $request = Request::create(''); + $request->files->set('upload', [self::uploadedFile()]); + + self::assertSame( + [], + $extractor->extractFilesFromRequest($request, 'file', true) + ); + } + + /** + * @test + */ + public function returns_array_for_single_file_path(): void + { + $extractor = self::extractor(); + + $request = Request::create(''); + $request->files->set('file', self::uploadedFile()); + + $documents = $extractor->extractFilesFromRequest($request, 'file', true); + self::assertIsArray($documents); + self::assertCount(1, $documents); + self::assertInstanceOf(PendingFile::class, $documents[0]); + self::assertSame("some content\n", $documents[0]->contents()); + } + + /** + * @test + */ + public function returns_array_for_multiple_files_path(): void + { + $extractor = self::extractor(); + + $request = Request::create(''); + $request->files->set('file', [self::uploadedFile(), self::uploadedFile()]); + + $documents = $extractor->extractFilesFromRequest($request, 'file', true); + self::assertIsArray($documents); + self::assertCount(2, $documents); + self::assertInstanceOf(PendingFile::class, $documents[0]); + self::assertSame("some content\n", $documents[0]->contents()); + } + + private static function uploadedFile(): UploadedFile + { + return new UploadedFile( + __DIR__.'/../../../Fixtures/files/textfile.txt', + 'test.txt', + test: true + ); + } + + private static function extractor(): RequestFilesExtractor + { + return new RequestFilesExtractor( + new PropertyAccessor( + PropertyAccessor::DISALLOW_MAGIC_METHODS, + PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH + ) + ); + } +} From 2d260bf89d947638ffe1661d8d8ddbd7365f93d5 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 24 Feb 2023 11:52:05 +0100 Subject: [PATCH 02/37] Refactor FilesExtractor to handle PendingFile subclasses in supports() --- .../HttpKernel/PendingFileValueResolver.php | 12 ++++------- .../HttpKernel/RequestFilesExtractor.php | 20 +++++++++++++++++-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php index a8f2e84f..0275fa73 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php @@ -41,10 +41,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable { $attributes = $argument->getAttributes(UploadedFile::class); - if ( - empty($attributes) - && PendingFile::class !== $argument->getType() - ) { + if (!RequestFilesExtractor::supports($argument)) { return []; } @@ -55,7 +52,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable $this->extractor()->extractFilesFromRequest( $request, $path, - PendingFile::class !== $argument->getType() + $argument->getType() ), ]; } @@ -76,8 +73,7 @@ public function __construct( public function supports(Request $request, ArgumentMetadata $argument): bool { - return PendingFile::class === $argument->getType() - || !empty($argument->getAttributes(UploadedFile::class)); + return RequestFilesExtractor::supports($argument); } /** @@ -95,7 +91,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable $this->extractor()->extractFilesFromRequest( $request, $path, - PendingFile::class !== $argument->getType() + $argument->getType() ), ]; } diff --git a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php index d97d8ad6..fd6ac8a0 100644 --- a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php +++ b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php @@ -14,7 +14,9 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\PropertyAccess\PropertyAccessor; +use Zenstruck\Filesystem\Attribute\UploadedFile as UploadedFileAttribute; use Zenstruck\Filesystem\Node\File\PendingFile; /** @@ -29,13 +31,15 @@ public function __construct(private PropertyAccessor $propertyAccessor) public function extractFilesFromRequest( Request $request, string $path, - bool $returnArray = false + ?string $expectedType = null ): PendingFile|array|null { + $expectedType ??= PendingFile::class; + $path = $this->canonizePath($path); $files = $this->propertyAccessor->getValue($request->files->all(), $path); - if ($returnArray) { + if (!is_a($expectedType, PendingFile::class, true)) { if (!$files) { return []; } @@ -61,6 +65,18 @@ public function extractFilesFromRequest( return new PendingFile($files); } + public static function supports(ArgumentMetadata $argument): bool + { + $attributes = $argument->getAttributes(UploadedFileAttribute::class); + + if (empty($attributes)) { + $type = $argument->getType(); + return $type && is_a($type, PendingFile::class, true); + } + + return true; + } + /** * Convert HTML paths to PropertyAccessor compatible. * Examples: "data[file]" -> "[data][file]", "files[]" -> "[files]". From fb833364d32ffb3234e0fb27d695a28bc4cf1c5f Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 24 Feb 2023 12:21:59 +0100 Subject: [PATCH 03/37] Handle PendingImage in value resolving --- src/Filesystem/Attribute/UploadedFile.php | 3 ++- .../HttpKernel/PendingFileValueResolver.php | 8 +++++- .../HttpKernel/RequestFilesExtractor.php | 26 ++++++++++++++----- .../HttpKernel/RequestFilesExtractorTest.php | 25 ++++++++++++++++++ 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index 0ef55465..791f7137 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -18,7 +18,8 @@ final class UploadedFile { public function __construct( - public ?string $path = null + public ?string $path = null, + public bool $image = false, ) { } } diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php index 0275fa73..dbe97da0 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php @@ -18,6 +18,7 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Contracts\Service\ServiceProviderInterface; use Zenstruck\Filesystem\Attribute\UploadedFile; +use Zenstruck\Filesystem\Node\File\Image\PendingImage; use Zenstruck\Filesystem\Node\File\PendingFile; /** @@ -52,7 +53,12 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable $this->extractor()->extractFilesFromRequest( $request, $path, - $argument->getType() + !is_a( + $argument->getType() ?? PendingFile::class, + PendingFile::class, + true + ), + $attributes[0]?->image || PendingImage::class === $argument->getType() ), ]; } diff --git a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php index fd6ac8a0..a416fcc6 100644 --- a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php +++ b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php @@ -12,11 +12,14 @@ namespace Zenstruck\Filesystem\Symfony\HttpKernel; +use League\Flysystem\FilesystemException; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\PropertyAccess\PropertyAccessor; use Zenstruck\Filesystem\Attribute\UploadedFile as UploadedFileAttribute; +use Zenstruck\Filesystem\Exception\NodeTypeMismatch; +use Zenstruck\Filesystem\Node\File\Image\PendingImage; use Zenstruck\Filesystem\Node\File\PendingFile; /** @@ -31,15 +34,14 @@ public function __construct(private PropertyAccessor $propertyAccessor) public function extractFilesFromRequest( Request $request, string $path, - ?string $expectedType = null + bool $returnArray = false, + bool $returnImage = false, ): PendingFile|array|null { - $expectedType ??= PendingFile::class; - $path = $this->canonizePath($path); $files = $this->propertyAccessor->getValue($request->files->all(), $path); - if (!is_a($expectedType, PendingFile::class, true)) { + if ($returnArray) { if (!$files) { return []; } @@ -49,7 +51,7 @@ public function extractFilesFromRequest( } return \array_map( - static fn(UploadedFile $file) => new PendingFile($file), + static fn(UploadedFile $file) => $returnImage ? new PendingImage($file) : new PendingFile($file), $files ); } @@ -62,7 +64,19 @@ public function extractFilesFromRequest( return null; } - return new PendingFile($files); + $file = new PendingFile($files); + + if ($returnImage) { + try { + return $file->ensureImage(); + } catch (NodeTypeMismatch|FilesystemException) { + // Incorrect images should be skipped + + return null; + } + } + + return $file; } public static function supports(ArgumentMetadata $argument): bool diff --git a/tests/Filesystem/Symfony/HttpKernel/RequestFilesExtractorTest.php b/tests/Filesystem/Symfony/HttpKernel/RequestFilesExtractorTest.php index 3871d69f..524ff1b5 100644 --- a/tests/Filesystem/Symfony/HttpKernel/RequestFilesExtractorTest.php +++ b/tests/Filesystem/Symfony/HttpKernel/RequestFilesExtractorTest.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\PropertyAccess\PropertyAccessor; +use Zenstruck\Filesystem\Node\File\Image\PendingImage; use Zenstruck\Filesystem\Node\File\PendingFile; use Zenstruck\Filesystem\Symfony\HttpKernel\RequestFilesExtractor; @@ -68,6 +69,21 @@ public function returns_file_for_correct_path(): void self::assertSame("some content\n", $document->contents()); } + /** + * @test + */ + public function returns_image_for_correct_path(): void + { + $extractor = self::extractor(); + + $request = Request::create(''); + $request->files->set('file', self::uploadedImage()); + + $document = $extractor->extractFilesFromRequest($request, 'file', returnImage: true); + self::assertNotNull($document); + self::assertInstanceOf(PendingImage::class, $document); + } + /** * @test */ @@ -172,6 +188,15 @@ private static function uploadedFile(): UploadedFile ); } + private static function uploadedImage(): UploadedFile + { + return new UploadedFile( + __DIR__.'/../../../Fixtures/files/symfony.png', + 'symfony.png', + test: true + ); + } + private static function extractor(): RequestFilesExtractor { return new RequestFilesExtractor( From 1c3464fcd2fcf1de2511e0dfa1f9ea7f5a3bc2ee Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Thu, 2 Mar 2023 13:28:33 +0100 Subject: [PATCH 04/37] Fix Phpstan warnings and run CS fixer --- .../Glide/GlideTransformUrlGenerator.php | 2 +- .../HttpKernel/PendingFileValueResolver.php | 22 ++++++++++++++----- .../HttpKernel/RequestFilesExtractor.php | 4 ++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Filesystem/Glide/GlideTransformUrlGenerator.php b/src/Filesystem/Glide/GlideTransformUrlGenerator.php index 0ea30e3e..b9dfc034 100644 --- a/src/Filesystem/Glide/GlideTransformUrlGenerator.php +++ b/src/Filesystem/Glide/GlideTransformUrlGenerator.php @@ -28,7 +28,7 @@ public function transformUrl(string $path, array|string $filter, Config $config) { $filter = match (true) { // @phpstan-ignore-line https://github.com/phpstan/phpstan/issues/8937 \is_string($filter) => ['p' => $filter], // is glide "preset" - \is_array($filter) && !array_is_list($filter) => $filter, // is standard glide parameters + \is_array($filter) && !\array_is_list($filter) => $filter, // is standard glide parameters \is_array($filter) => ['p' => \implode(',', $filter)], // is array of "presets" }; diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php index dbe97da0..ef6d42eb 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php @@ -11,7 +11,6 @@ namespace Zenstruck\Filesystem\Symfony\HttpKernel; - use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; @@ -46,19 +45,22 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable return []; } - $path = $attributes[0]?->path + /** @var UploadedFile|null $attribute */ + $attribute = $attributes[0]; + + $path = $attribute?->path ?? $argument->getName(); return [ $this->extractor()->extractFilesFromRequest( $request, $path, - !is_a( + !\is_a( $argument->getType() ?? PendingFile::class, PendingFile::class, true ), - $attributes[0]?->image || PendingImage::class === $argument->getType() + $attribute?->image || PendingImage::class === $argument->getType() ), ]; } @@ -90,14 +92,22 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable $attributes = $argument->getAttributes(UploadedFile::class); \assert(!empty($attributes)); - $path = $attributes[0]?->path + /** @var UploadedFile|null $attribute */ + $attribute = $attributes[0]; + + $path = $attribute?->path ?? $argument->getName(); return [ $this->extractor()->extractFilesFromRequest( $request, $path, - $argument->getType() + !\is_a( + $argument->getType() ?? PendingFile::class, + PendingFile::class, + true + ), + $attribute?->image || PendingImage::class === $argument->getType() ), ]; } diff --git a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php index a416fcc6..fc56ed84 100644 --- a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php +++ b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php @@ -11,7 +11,6 @@ namespace Zenstruck\Filesystem\Symfony\HttpKernel; - use League\Flysystem\FilesystemException; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; @@ -85,7 +84,8 @@ public static function supports(ArgumentMetadata $argument): bool if (empty($attributes)) { $type = $argument->getType(); - return $type && is_a($type, PendingFile::class, true); + + return $type && \is_a($type, PendingFile::class, true); } return true; From 78ffb282bd9bba47febe0573e61db834e27fbc57 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 3 Mar 2023 11:21:22 +0100 Subject: [PATCH 05/37] Add missing functional tests --- .../HttpKernel/PendingFileValueResolver.php | 4 +- .../PendingDocumentValueResolverTest.php | 131 ++++++++++++++++++ .../Controller/ArgumentResolverController.php | 60 ++++++++ tests/Fixtures/TestKernel.php | 1 + 4 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php create mode 100644 tests/Fixtures/Controller/ArgumentResolverController.php diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php index ef6d42eb..d178681a 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php @@ -46,7 +46,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable } /** @var UploadedFile|null $attribute */ - $attribute = $attributes[0]; + $attribute = $attributes[0] ?? null; $path = $attribute?->path ?? $argument->getName(); @@ -93,7 +93,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable \assert(!empty($attributes)); /** @var UploadedFile|null $attribute */ - $attribute = $attributes[0]; + $attribute = $attributes[0] ?? null; $path = $attribute?->path ?? $argument->getName(); diff --git a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php new file mode 100644 index 00000000..3f4901d0 --- /dev/null +++ b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Tests\Filesystem\Symfony\HttpKernel; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\HttpFoundation\File\UploadedFile; + +/** + * @author Jakub Caban + */ +class PendingDocumentValueResolverTest extends WebTestCase +{ + /** + * @test + */ + public function do_nothing_on_wrong_type(): void + { + $client = self::createClient(); + + $client->request( + 'GET', + 'no-injection', + files: ['file' => self::uploadedFile()] + ); + + self::assertSame('0', $client->getResponse()->getContent()); + } + + /** + * @test + */ + public function inject_on_typed_argument(): void + { + $client = self::createClient(); + + $client->request( + 'GET', + 'single-file', + ); + + self::assertSame('', $client->getResponse()->getContent()); + + $client->request( + 'GET', + 'single-file', + files: ['file' => self::uploadedFile()] + ); + + self::assertSame("some content\n", $client->getResponse()->getContent()); + } + + /** + * @test + */ + public function inject_on_typed_argument_with_path(): void + { + $client = self::createClient(); + + $client->request( + 'GET', + 'single-file-with-path', + files: ['data' => ['file' => self::uploadedFile()]] + ); + + self::assertSame("some content\n", $client->getResponse()->getContent()); + } + + /** + * @test + */ + public function inject_array_on_argument_with_attribute(): void + { + $client = self::createClient(); + + $client->request( + 'GET', + 'multiple-files' + ); + + self::assertSame('0', $client->getResponse()->getContent()); + + $client->request( + 'GET', + 'multiple-files', + files: ['files' => self::uploadedFile()] + ); + + self::assertSame('1', $client->getResponse()->getContent()); + } + + /** + * @test + */ + public function inject_array_on_argument_with_attribute_and_path(): void + { + $client = self::createClient(); + + $client->request( + 'GET', + 'multiple-files-with-path' + ); + + self::assertSame('0', $client->getResponse()->getContent()); + + $client->request( + 'GET', + 'multiple-files-with-path', + files: ['data' => ['files' => self::uploadedFile()]] + ); + + self::assertSame('1', $client->getResponse()->getContent()); + } + + private static function uploadedFile(): UploadedFile + { + return new UploadedFile( + __DIR__.'/../../../Fixtures/files/textfile.txt', + 'test.txt', + test: true + ); + } +} diff --git a/tests/Fixtures/Controller/ArgumentResolverController.php b/tests/Fixtures/Controller/ArgumentResolverController.php new file mode 100644 index 00000000..18ea0fdb --- /dev/null +++ b/tests/Fixtures/Controller/ArgumentResolverController.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Tests\Fixtures\Controller; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; +use Zenstruck\Filesystem\Attribute\UploadedFile; +use Zenstruck\Filesystem\Node\File; +use Zenstruck\Filesystem\Node\File\PendingFile; + +/** + * @author Jakub Caban + */ +class ArgumentResolverController +{ + #[Route('/multiple-files', name: 'multiple-files')] + public function multipleFiles( + #[UploadedFile] + array $files + ): Response { + return new Response((string) \count($files)); + } + + #[Route('/multiple-files-with-path', name: 'multiple-files-with-path')] + public function multipleFilesWithPath( + #[UploadedFile('data[files]')] + array $files + ): Response { + return new Response((string) \count($files)); + } + + #[Route('/no-injection', name: 'no-injection')] + public function noInjection(array $file = []): Response + { + return new Response((string) \count($file)); + } + + #[Route('/single-file', name: 'single-file')] + public function singleFile(?PendingFile $file): Response + { + return new Response($file?->contents() ?? ''); + } + + #[Route('/single-file-with-path', name: 'single-file-with-path')] + public function singleFileWithPath( + #[UploadedFile('data[file]')] + ?PendingFile $file + ): Response { + return new Response($file?->contents() ?? ''); + } +} diff --git a/tests/Fixtures/TestKernel.php b/tests/Fixtures/TestKernel.php index 4b3bbad0..4b251d68 100644 --- a/tests/Fixtures/TestKernel.php +++ b/tests/Fixtures/TestKernel.php @@ -184,5 +184,6 @@ protected function configureRoutes(RoutingConfigurator $routes): void $routes->add('private_public', '/private/{path}') ->requirements(['path' => '.+']) ; + $routes->import(__DIR__.'/Controller', 'annotation'); } } From 60ae750856bfe49410bfa460497320ecfa4be060 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 3 Mar 2023 11:31:04 +0100 Subject: [PATCH 06/37] Allow File/Image typehint for value resolver --- .../HttpKernel/PendingFileValueResolver.php | 26 ++++++++++++++----- .../HttpKernel/RequestFilesExtractor.php | 3 ++- .../Controller/ArgumentResolverController.php | 4 +-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php index d178681a..931d4fea 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php @@ -17,6 +17,8 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Contracts\Service\ServiceProviderInterface; use Zenstruck\Filesystem\Attribute\UploadedFile; +use Zenstruck\Filesystem\Node\File; +use Zenstruck\Filesystem\Node\File\Image; use Zenstruck\Filesystem\Node\File\Image\PendingImage; use Zenstruck\Filesystem\Node\File\PendingFile; @@ -56,11 +58,16 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable $request, $path, !\is_a( - $argument->getType() ?? PendingFile::class, - PendingFile::class, + $argument->getType() ?? File::class, + File::class, + true + ), + $attribute?->image + || is_a( + $argument->getType() ?? File::class, + Image::class, true ), - $attribute?->image || PendingImage::class === $argument->getType() ), ]; } @@ -103,12 +110,17 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable $request, $path, !\is_a( - $argument->getType() ?? PendingFile::class, - PendingFile::class, + $argument->getType() ?? File::class, + File::class, true ), - $attribute?->image || PendingImage::class === $argument->getType() - ), + $attribute?->image + || is_a( + $argument->getType() ?? File::class, + Image::class, + true + ), + ) ]; } diff --git a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php index fc56ed84..95f5a2fb 100644 --- a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php +++ b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php @@ -18,6 +18,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessor; use Zenstruck\Filesystem\Attribute\UploadedFile as UploadedFileAttribute; use Zenstruck\Filesystem\Exception\NodeTypeMismatch; +use Zenstruck\Filesystem\Node\File; use Zenstruck\Filesystem\Node\File\Image\PendingImage; use Zenstruck\Filesystem\Node\File\PendingFile; @@ -85,7 +86,7 @@ public static function supports(ArgumentMetadata $argument): bool if (empty($attributes)) { $type = $argument->getType(); - return $type && \is_a($type, PendingFile::class, true); + return $type && \is_a($type, File::class, true); } return true; diff --git a/tests/Fixtures/Controller/ArgumentResolverController.php b/tests/Fixtures/Controller/ArgumentResolverController.php index 18ea0fdb..73709dac 100644 --- a/tests/Fixtures/Controller/ArgumentResolverController.php +++ b/tests/Fixtures/Controller/ArgumentResolverController.php @@ -45,7 +45,7 @@ public function noInjection(array $file = []): Response } #[Route('/single-file', name: 'single-file')] - public function singleFile(?PendingFile $file): Response + public function singleFile(?File $file): Response { return new Response($file?->contents() ?? ''); } @@ -53,7 +53,7 @@ public function singleFile(?PendingFile $file): Response #[Route('/single-file-with-path', name: 'single-file-with-path')] public function singleFileWithPath( #[UploadedFile('data[file]')] - ?PendingFile $file + ?File $file ): Response { return new Response($file?->contents() ?? ''); } From 45a5848b8bb76caa56d8fea1153321e687674b5b Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 3 Mar 2023 11:38:09 +0100 Subject: [PATCH 07/37] Simplify PendingFileValueResolver by introducing a common trait --- .../HttpKernel/PendingFileValueResolver.php | 86 +------------------ .../PendingFileValueResolverTrait.php | 71 +++++++++++++++ .../Controller/ArgumentResolverController.php | 1 - 3 files changed, 75 insertions(+), 83 deletions(-) create mode 100644 src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php index 931d4fea..559cd529 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php @@ -15,11 +15,6 @@ use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; -use Symfony\Contracts\Service\ServiceProviderInterface; -use Zenstruck\Filesystem\Attribute\UploadedFile; -use Zenstruck\Filesystem\Node\File; -use Zenstruck\Filesystem\Node\File\Image; -use Zenstruck\Filesystem\Node\File\Image\PendingImage; use Zenstruck\Filesystem\Node\File\PendingFile; /** @@ -30,10 +25,8 @@ if (\interface_exists(ValueResolverInterface::class)) { class PendingFileValueResolver implements ValueResolverInterface { - public function __construct( - /** @var ServiceProviderInterface $locator */ - private ServiceProviderInterface $locator - ) { + use PendingFileValueResolverTrait { + resolve as resolveArgument; } /** @@ -41,92 +34,21 @@ public function __construct( */ public function resolve(Request $request, ArgumentMetadata $argument): iterable { - $attributes = $argument->getAttributes(UploadedFile::class); - if (!RequestFilesExtractor::supports($argument)) { return []; } - /** @var UploadedFile|null $attribute */ - $attribute = $attributes[0] ?? null; - - $path = $attribute?->path - ?? $argument->getName(); - - return [ - $this->extractor()->extractFilesFromRequest( - $request, - $path, - !\is_a( - $argument->getType() ?? File::class, - File::class, - true - ), - $attribute?->image - || is_a( - $argument->getType() ?? File::class, - Image::class, - true - ), - ), - ]; - } - - private function extractor(): RequestFilesExtractor - { - return $this->locator->get(RequestFilesExtractor::class); + return $this->resolveArgument($request, $argument); } } } else { class PendingFileValueResolver implements ArgumentValueResolverInterface { - public function __construct( - /** @var ServiceProviderInterface $locator */ - private ServiceProviderInterface $locator - ) { - } + use PendingFileValueResolverTrait; public function supports(Request $request, ArgumentMetadata $argument): bool { return RequestFilesExtractor::supports($argument); } - - /** - * @return iterable - */ - public function resolve(Request $request, ArgumentMetadata $argument): iterable - { - $attributes = $argument->getAttributes(UploadedFile::class); - \assert(!empty($attributes)); - - /** @var UploadedFile|null $attribute */ - $attribute = $attributes[0] ?? null; - - $path = $attribute?->path - ?? $argument->getName(); - - return [ - $this->extractor()->extractFilesFromRequest( - $request, - $path, - !\is_a( - $argument->getType() ?? File::class, - File::class, - true - ), - $attribute?->image - || is_a( - $argument->getType() ?? File::class, - Image::class, - true - ), - ) - ]; - } - - private function extractor(): RequestFilesExtractor - { - return $this->locator->get(RequestFilesExtractor::class); - } } } diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php new file mode 100644 index 00000000..760b9118 --- /dev/null +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Filesystem\Symfony\HttpKernel; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Contracts\Service\ServiceProviderInterface; +use Zenstruck\Filesystem\Attribute\UploadedFile; +use Zenstruck\Filesystem\Node\File; +use Zenstruck\Filesystem\Node\File\Image; +use Zenstruck\Filesystem\Node\File\PendingFile; + +/** + * @author Jakub Caban + * + * @internal + */ +trait PendingFileValueResolverTrait +{ + public function __construct( + /** @var ServiceProviderInterface $locator */ + private ServiceProviderInterface $locator + ) { + } + + /** + * @return iterable + */ + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + $attributes = $argument->getAttributes(UploadedFile::class); + + /** @var UploadedFile|null $attribute */ + $attribute = $attributes[0] ?? null; + + $path = $attribute?->path + ?? $argument->getName(); + + return [ + $this->extractor()->extractFilesFromRequest( + $request, + $path, + !\is_a( + $argument->getType() ?? File::class, + File::class, + true + ), + $attribute?->image + || \is_a( + $argument->getType() ?? File::class, + Image::class, + true + ), + ), + ]; + } + + private function extractor(): RequestFilesExtractor + { + return $this->locator->get(RequestFilesExtractor::class); + } +} diff --git a/tests/Fixtures/Controller/ArgumentResolverController.php b/tests/Fixtures/Controller/ArgumentResolverController.php index 73709dac..c67975f7 100644 --- a/tests/Fixtures/Controller/ArgumentResolverController.php +++ b/tests/Fixtures/Controller/ArgumentResolverController.php @@ -15,7 +15,6 @@ use Symfony\Component\Routing\Annotation\Route; use Zenstruck\Filesystem\Attribute\UploadedFile; use Zenstruck\Filesystem\Node\File; -use Zenstruck\Filesystem\Node\File\PendingFile; /** * @author Jakub Caban From 92fe77842adc8a4f9e1238263fdcf22113258dba Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 3 Mar 2023 11:47:15 +0100 Subject: [PATCH 08/37] Add simple image injection functional tests --- .../PendingDocumentValueResolverTest.php | 25 +++++++++++++++++++ .../Controller/ArgumentResolverController.php | 15 +++++++++++ 2 files changed, 40 insertions(+) diff --git a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php index 3f4901d0..86d75d64 100644 --- a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php +++ b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php @@ -56,6 +56,14 @@ public function inject_on_typed_argument(): void ); self::assertSame("some content\n", $client->getResponse()->getContent()); + + $client->request( + 'GET', + 'single-image', + files: ['image' => self::uploadedImage()] + ); + + self::assertSame("563", $client->getResponse()->getContent()); } /** @@ -95,6 +103,14 @@ public function inject_array_on_argument_with_attribute(): void ); self::assertSame('1', $client->getResponse()->getContent()); + + $client->request( + 'GET', + 'multiple-images', + files: ['images' => self::uploadedImage()] + ); + + self::assertSame('1', $client->getResponse()->getContent()); } /** @@ -128,4 +144,13 @@ private static function uploadedFile(): UploadedFile test: true ); } + + private static function uploadedImage(): UploadedFile + { + return new UploadedFile( + __DIR__.'/../../../Fixtures/files/symfony.png', + 'symfony.png', + test: true + ); + } } diff --git a/tests/Fixtures/Controller/ArgumentResolverController.php b/tests/Fixtures/Controller/ArgumentResolverController.php index c67975f7..93cc7362 100644 --- a/tests/Fixtures/Controller/ArgumentResolverController.php +++ b/tests/Fixtures/Controller/ArgumentResolverController.php @@ -15,6 +15,7 @@ use Symfony\Component\Routing\Annotation\Route; use Zenstruck\Filesystem\Attribute\UploadedFile; use Zenstruck\Filesystem\Node\File; +use Zenstruck\Filesystem\Node\File\Image; /** * @author Jakub Caban @@ -29,6 +30,14 @@ public function multipleFiles( return new Response((string) \count($files)); } + #[Route('/multiple-images', name: 'multiple-images')] + public function multipleImages( + #[UploadedFile(image: true)] + array $images + ): Response { + return new Response((string) \count($images)); + } + #[Route('/multiple-files-with-path', name: 'multiple-files-with-path')] public function multipleFilesWithPath( #[UploadedFile('data[files]')] @@ -49,6 +58,12 @@ public function singleFile(?File $file): Response return new Response($file?->contents() ?? ''); } + #[Route('/single-image', name: 'single-image')] + public function singleImage(Image $image): Response + { + return new Response((string) $image->dimensions()->width()); + } + #[Route('/single-file-with-path', name: 'single-file-with-path')] public function singleFileWithPath( #[UploadedFile('data[file]')] From e8dee6ce76e09873c293c9eff69bb8b98def61ff Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 3 Mar 2023 12:23:46 +0100 Subject: [PATCH 09/37] Utilize UploadedFile::forArgument() for a centralized value resolver configuration --- src/Filesystem/Attribute/UploadedFile.php | 32 ++++++++++++++++++- .../PendingFileValueResolverTrait.php | 24 +++----------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index 791f7137..ecc4ea79 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -11,6 +11,10 @@ namespace Zenstruck\Filesystem\Attribute; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Zenstruck\Filesystem\Node\File; +use Zenstruck\Filesystem\Node\File\Image; + /** * @author Jakub Caban */ @@ -19,7 +23,33 @@ final class UploadedFile { public function __construct( public ?string $path = null, - public bool $image = false, + public ?bool $image = null, + public ?bool $multiple = null, ) { } + + public static function forArgument(ArgumentMetadata $argument): self + { + $attributes = $argument->getAttributes(self::class); + + $attribute = null; + if (!empty($attributes)) { + $attribute = $attributes[0]; + assert($attribute instanceof self); + } + + return new self( + path: $attribute->path ?? $argument->getName(), + image: $attribute->image ?? \is_a( + $argument->getType() ?? File::class, + Image::class, + true + ), + multiple: $attribute->multiple ?? !\is_a( + $argument->getType() ?? File::class, + File::class, + true + ), + ); + } } diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index 760b9118..678445f3 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -16,7 +16,6 @@ use Symfony\Contracts\Service\ServiceProviderInterface; use Zenstruck\Filesystem\Attribute\UploadedFile; use Zenstruck\Filesystem\Node\File; -use Zenstruck\Filesystem\Node\File\Image; use Zenstruck\Filesystem\Node\File\PendingFile; /** @@ -37,29 +36,14 @@ public function __construct( */ public function resolve(Request $request, ArgumentMetadata $argument): iterable { - $attributes = $argument->getAttributes(UploadedFile::class); - - /** @var UploadedFile|null $attribute */ - $attribute = $attributes[0] ?? null; - - $path = $attribute?->path - ?? $argument->getName(); + $attribute = UploadedFile::forArgument($argument); return [ $this->extractor()->extractFilesFromRequest( $request, - $path, - !\is_a( - $argument->getType() ?? File::class, - File::class, - true - ), - $attribute?->image - || \is_a( - $argument->getType() ?? File::class, - Image::class, - true - ), + $attribute->path, + $attribute->multiple, + $attribute->image, ), ]; } From a0e63cac382d117edaf19f19cf74602d4e1b8fc9 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 3 Mar 2023 12:36:08 +0100 Subject: [PATCH 10/37] Extend UploadedFile to constraints and errorStatus --- src/Filesystem/Attribute/UploadedFile.php | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index ecc4ea79..33251ce2 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -12,8 +12,10 @@ namespace Zenstruck\Filesystem\Attribute; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\Validator\Constraints\All; use Zenstruck\Filesystem\Node\File; use Zenstruck\Filesystem\Node\File\Image; +use Zenstruck\Filesystem\Symfony\Validator\PendingImageConstraint; /** * @author Jakub Caban @@ -25,7 +27,18 @@ public function __construct( public ?string $path = null, public ?bool $image = null, public ?bool $multiple = null, + public ?array $constraints = null, + public ?int $errorStatus = null, ) { + if ($this->image && !$this->constraints) { + if ($this->multiple) { + $this->constraints = [ + new All([new PendingImageConstraint()]) + ]; + } else { + $this->constraints = [new PendingImageConstraint()]; + } + } } public static function forArgument(ArgumentMetadata $argument): self @@ -39,17 +52,19 @@ public static function forArgument(ArgumentMetadata $argument): self } return new self( - path: $attribute->path ?? $argument->getName(), - image: $attribute->image ?? \is_a( + path: $attribute?->path ?? $argument->getName(), + image: $attribute?->image ?? \is_a( $argument->getType() ?? File::class, Image::class, true ), - multiple: $attribute->multiple ?? !\is_a( + multiple: $attribute?->multiple ?? !\is_a( $argument->getType() ?? File::class, File::class, true ), + constraints: $attribute?->constraints, + errorStatus: $attribute?->errorStatus ?? 422, ); } } From 45dd655d80e2cf9f9d6ee0687cd90a28255a3c54 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 3 Mar 2023 12:52:26 +0100 Subject: [PATCH 11/37] Add file validation logic to pendingfileValueResolver --- src/Filesystem/Attribute/UploadedFile.php | 4 +-- .../ZenstruckFilesystemExtension.php | 2 ++ .../PendingFileValueResolverTrait.php | 30 ++++++++++++++++--- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index 33251ce2..0c5d0a50 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -30,7 +30,7 @@ public function __construct( public ?array $constraints = null, public ?int $errorStatus = null, ) { - if ($this->image && !$this->constraints) { + if ($this->image && [] === $this->constraints) { if ($this->multiple) { $this->constraints = [ new All([new PendingImageConstraint()]) @@ -63,7 +63,7 @@ public static function forArgument(ArgumentMetadata $argument): self File::class, true ), - constraints: $attribute?->constraints, + constraints: $attribute?->constraints ?? [], errorStatus: $attribute?->errorStatus ?? 422, ); } diff --git a/src/Filesystem/Symfony/DependencyInjection/ZenstruckFilesystemExtension.php b/src/Filesystem/Symfony/DependencyInjection/ZenstruckFilesystemExtension.php index a2e3dce3..989a36a4 100644 --- a/src/Filesystem/Symfony/DependencyInjection/ZenstruckFilesystemExtension.php +++ b/src/Filesystem/Symfony/DependencyInjection/ZenstruckFilesystemExtension.php @@ -25,6 +25,7 @@ use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\String\Slugger\SluggerInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Translation\LocaleAwareInterface; use Zenstruck\Filesystem; use Zenstruck\Filesystem\Doctrine\EventListener\NodeLifecycleListener; @@ -166,6 +167,7 @@ private function registerDoctrine(ContainerBuilder $container, array $config): v ->addArgument( new ServiceLocatorArgument([ RequestFilesExtractor::class => new Reference('.zenstruck_document.value_resolver.request_files_extractor'), + ValidatorInterface::class => new Reference(ValidatorInterface::class), ]) ) ; diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index 678445f3..27ac4483 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -13,6 +13,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Service\ServiceProviderInterface; use Zenstruck\Filesystem\Attribute\UploadedFile; use Zenstruck\Filesystem\Node\File; @@ -38,18 +41,37 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable { $attribute = UploadedFile::forArgument($argument); - return [ - $this->extractor()->extractFilesFromRequest( + $files = $this->extractor()->extractFilesFromRequest( $request, $attribute->path, $attribute->multiple, $attribute->image, - ), - ]; + ); + + if ($files && $attribute->constraints) { + $errors = $this->validator()->validate( + $files, + $attribute->constraints + ); + + if (count($errors)) { + throw new HttpException( + $attribute->errorStatus, + (string) $errors + ); + } + } + + return [$files]; } private function extractor(): RequestFilesExtractor { return $this->locator->get(RequestFilesExtractor::class); } + + private function validator(): ValidatorInterface + { + return $this->locator->get(ValidatorInterface::class); + } } From 402f986d7750a2857382638b5e8a6d69c17e35a6 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 3 Mar 2023 12:55:49 +0100 Subject: [PATCH 12/37] Add simple test case for invalid image --- .../PendingDocumentValueResolverTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php index 86d75d64..cd583066 100644 --- a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php +++ b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php @@ -136,6 +136,24 @@ public function inject_array_on_argument_with_attribute_and_path(): void self::assertSame('1', $client->getResponse()->getContent()); } + + /** + * @test + */ + public function returns_exception_for_invalid_file(): void + { + $client = self::createClient(); + + $client->request( + 'GET', + 'single-image', + files: ['image' => self::uploadedFile()] + ); + $response = $client->getResponse(); + + self::assertSame(422, $response->getStatusCode()); + } + private static function uploadedFile(): UploadedFile { return new UploadedFile( From 669b297ec28a88a99a7ef15a83a7863545f35665 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 3 Mar 2023 13:31:44 +0100 Subject: [PATCH 13/37] Add valueResolver exception tests --- .../HttpKernel/PendingDocumentValueResolverTest.php | 11 ++++++++++- .../Controller/ArgumentResolverController.php | 12 ++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php index cd583066..569d66cb 100644 --- a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php +++ b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php @@ -13,6 +13,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpKernel\Exception\HttpException; /** * @author Jakub Caban @@ -150,8 +151,16 @@ public function returns_exception_for_invalid_file(): void files: ['image' => self::uploadedFile()] ); $response = $client->getResponse(); - self::assertSame(422, $response->getStatusCode()); + + $client->request( + 'GET', + 'validated-file', + files: ['file' => self::uploadedFile()] + ); + $response = $client->getResponse(); + + self::assertSame(500, $response->getStatusCode()); } private static function uploadedFile(): UploadedFile diff --git a/tests/Fixtures/Controller/ArgumentResolverController.php b/tests/Fixtures/Controller/ArgumentResolverController.php index 93cc7362..d02ef590 100644 --- a/tests/Fixtures/Controller/ArgumentResolverController.php +++ b/tests/Fixtures/Controller/ArgumentResolverController.php @@ -16,6 +16,7 @@ use Zenstruck\Filesystem\Attribute\UploadedFile; use Zenstruck\Filesystem\Node\File; use Zenstruck\Filesystem\Node\File\Image; +use Zenstruck\Filesystem\Symfony\Validator\PendingFileConstraint; /** * @author Jakub Caban @@ -71,4 +72,15 @@ public function singleFileWithPath( ): Response { return new Response($file?->contents() ?? ''); } + + #[Route('/validated-file', name: 'validated-file')] + public function validatedFile( + #[UploadedFile( + constraints: [new PendingFileConstraint(mimeTypes: ['application/pdf'])], + errorStatus: 500, + )] + File $file + ): Response { + return new Response($file->contents()); + } } From f52409c37a43bf11c8d5c2a10727a10eb73bbcaf Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 3 Mar 2023 13:33:38 +0100 Subject: [PATCH 14/37] CS run --- src/Filesystem/Attribute/UploadedFile.php | 4 ++-- .../PendingFileValueResolverTrait.php | 19 +++++++------------ .../PendingDocumentValueResolverTest.php | 4 +--- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index 0c5d0a50..bec91daf 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -33,7 +33,7 @@ public function __construct( if ($this->image && [] === $this->constraints) { if ($this->multiple) { $this->constraints = [ - new All([new PendingImageConstraint()]) + new All([new PendingImageConstraint()]), ]; } else { $this->constraints = [new PendingImageConstraint()]; @@ -48,7 +48,7 @@ public static function forArgument(ArgumentMetadata $argument): self $attribute = null; if (!empty($attributes)) { $attribute = $attributes[0]; - assert($attribute instanceof self); + \assert($attribute instanceof self); } return new self( diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index 27ac4483..d39b9390 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -14,11 +14,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\HttpException; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Service\ServiceProviderInterface; use Zenstruck\Filesystem\Attribute\UploadedFile; -use Zenstruck\Filesystem\Node\File; use Zenstruck\Filesystem\Node\File\PendingFile; /** @@ -42,11 +40,11 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable $attribute = UploadedFile::forArgument($argument); $files = $this->extractor()->extractFilesFromRequest( - $request, - $attribute->path, - $attribute->multiple, - $attribute->image, - ); + $request, + $attribute->path, + $attribute->multiple, + $attribute->image, + ); if ($files && $attribute->constraints) { $errors = $this->validator()->validate( @@ -54,11 +52,8 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable $attribute->constraints ); - if (count($errors)) { - throw new HttpException( - $attribute->errorStatus, - (string) $errors - ); + if (\count($errors)) { + throw new HttpException($attribute->errorStatus, (string) $errors); } } diff --git a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php index 569d66cb..e2bd3b56 100644 --- a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php +++ b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php @@ -13,7 +13,6 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\File\UploadedFile; -use Symfony\Component\HttpKernel\Exception\HttpException; /** * @author Jakub Caban @@ -64,7 +63,7 @@ public function inject_on_typed_argument(): void files: ['image' => self::uploadedImage()] ); - self::assertSame("563", $client->getResponse()->getContent()); + self::assertSame('563', $client->getResponse()->getContent()); } /** @@ -137,7 +136,6 @@ public function inject_array_on_argument_with_attribute_and_path(): void self::assertSame('1', $client->getResponse()->getContent()); } - /** * @test */ From 0e8a3cc04e68d4044b151e943f4c53d81da072ca Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 3 Mar 2023 13:36:21 +0100 Subject: [PATCH 15/37] Trailing comma is a no-go in PHP8 :) --- tests/Fixtures/Controller/ArgumentResolverController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Fixtures/Controller/ArgumentResolverController.php b/tests/Fixtures/Controller/ArgumentResolverController.php index d02ef590..6bcc9c4f 100644 --- a/tests/Fixtures/Controller/ArgumentResolverController.php +++ b/tests/Fixtures/Controller/ArgumentResolverController.php @@ -77,7 +77,7 @@ public function singleFileWithPath( public function validatedFile( #[UploadedFile( constraints: [new PendingFileConstraint(mimeTypes: ['application/pdf'])], - errorStatus: 500, + errorStatus: 500 )] File $file ): Response { From 7a2252a76c5e20b0ca7a71a184cf70d398535ac7 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 3 Mar 2023 13:43:17 +0100 Subject: [PATCH 16/37] Make phpstan happy --- phpstan.neon | 1 + .../HttpKernel/PendingFileValueResolverTrait.php | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index bd653e05..164b840d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,6 @@ parameters: level: 8 + checkGenericClassInNonGenericObjectType: false treatPhpDocTypesAsCertain: false paths: - src diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index d39b9390..ace6c9e7 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -14,6 +14,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Service\ServiceProviderInterface; use Zenstruck\Filesystem\Attribute\UploadedFile; @@ -27,7 +28,6 @@ trait PendingFileValueResolverTrait { public function __construct( - /** @var ServiceProviderInterface $locator */ private ServiceProviderInterface $locator ) { } @@ -41,9 +41,9 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable $files = $this->extractor()->extractFilesFromRequest( $request, - $attribute->path, - $attribute->multiple, - $attribute->image, + (string) $attribute->path, + (bool) $attribute->multiple, + (bool) $attribute->image, ); if ($files && $attribute->constraints) { @@ -53,7 +53,9 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable ); if (\count($errors)) { - throw new HttpException($attribute->errorStatus, (string) $errors); + \assert($errors instanceof ConstraintViolationList); + + throw new HttpException((int) $attribute->errorStatus, (string) $errors); } } From 81e1b155fa7bf2cc9d109c34eed0458ddf761d78 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Tue, 14 Mar 2023 11:44:49 +0100 Subject: [PATCH 17/37] Trivial suggestions --- src/Filesystem/Attribute/UploadedFile.php | 2 +- .../Symfony/HttpKernel/PendingDocumentValueResolverTest.php | 4 ++-- .../Symfony/HttpKernel/RequestFilesExtractorTest.php | 4 ++-- tests/Fixtures/TestKernel.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index bec91daf..2de2e6a9 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -28,7 +28,7 @@ public function __construct( public ?bool $image = null, public ?bool $multiple = null, public ?array $constraints = null, - public ?int $errorStatus = null, + public int $errorStatus = 422, ) { if ($this->image && [] === $this->constraints) { if ($this->multiple) { diff --git a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php index e2bd3b56..8ec68acb 100644 --- a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php +++ b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php @@ -164,7 +164,7 @@ public function returns_exception_for_invalid_file(): void private static function uploadedFile(): UploadedFile { return new UploadedFile( - __DIR__.'/../../../Fixtures/files/textfile.txt', + fixture('textfile.txt'), 'test.txt', test: true ); @@ -173,7 +173,7 @@ private static function uploadedFile(): UploadedFile private static function uploadedImage(): UploadedFile { return new UploadedFile( - __DIR__.'/../../../Fixtures/files/symfony.png', + fixture('symfony.png'), 'symfony.png', test: true ); diff --git a/tests/Filesystem/Symfony/HttpKernel/RequestFilesExtractorTest.php b/tests/Filesystem/Symfony/HttpKernel/RequestFilesExtractorTest.php index 524ff1b5..7be66f69 100644 --- a/tests/Filesystem/Symfony/HttpKernel/RequestFilesExtractorTest.php +++ b/tests/Filesystem/Symfony/HttpKernel/RequestFilesExtractorTest.php @@ -182,7 +182,7 @@ public function returns_array_for_multiple_files_path(): void private static function uploadedFile(): UploadedFile { return new UploadedFile( - __DIR__.'/../../../Fixtures/files/textfile.txt', + fixture('textfile.txt'), 'test.txt', test: true ); @@ -191,7 +191,7 @@ private static function uploadedFile(): UploadedFile private static function uploadedImage(): UploadedFile { return new UploadedFile( - __DIR__.'/../../../Fixtures/files/symfony.png', + fixture('symfony.png'), 'symfony.png', test: true ); diff --git a/tests/Fixtures/TestKernel.php b/tests/Fixtures/TestKernel.php index 4b251d68..1108f332 100644 --- a/tests/Fixtures/TestKernel.php +++ b/tests/Fixtures/TestKernel.php @@ -184,6 +184,6 @@ protected function configureRoutes(RoutingConfigurator $routes): void $routes->add('private_public', '/private/{path}') ->requirements(['path' => '.+']) ; - $routes->import(__DIR__.'/Controller', 'annotation'); + $routes->import(__DIR__.'/Controller', 'attribute'); } } From 0636c54935ace839a0e6678b94529e584bb64312 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Tue, 14 Mar 2023 11:46:41 +0100 Subject: [PATCH 18/37] Disable logger on tests --- tests/Fixtures/TestKernel.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Fixtures/TestKernel.php b/tests/Fixtures/TestKernel.php index 1108f332..30920bc6 100644 --- a/tests/Fixtures/TestKernel.php +++ b/tests/Fixtures/TestKernel.php @@ -13,6 +13,7 @@ use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use League\Glide\Urls\UrlBuilder; +use Psr\Log\NullLogger; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Bundle\TwigBundle\TwigBundle; @@ -168,6 +169,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ->setAutowired(true) ->setAutoconfigured(true) ; + $c->register('logger', NullLogger::class); // disable logging } protected function configureRoutes(RoutingConfigurator $routes): void From 52d320d6aca16c9df6ed07b0ac93ccf30d09d61c Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Tue, 14 Mar 2023 11:49:26 +0100 Subject: [PATCH 19/37] Add custom exception for incorrect file --- .../Exception/IncorrectFileHttpException.php | 22 +++++++++++++++++++ .../PendingFileValueResolverTrait.php | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/Filesystem/Exception/IncorrectFileHttpException.php diff --git a/src/Filesystem/Exception/IncorrectFileHttpException.php b/src/Filesystem/Exception/IncorrectFileHttpException.php new file mode 100644 index 00000000..1c85006f --- /dev/null +++ b/src/Filesystem/Exception/IncorrectFileHttpException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Filesystem\Exception; + +use Symfony\Component\HttpKernel\Exception\HttpException; + +/** + * @author Jakub Caban + */ +class IncorrectFileHttpException extends HttpException +{ + +} diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index ace6c9e7..3e9056cc 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -18,6 +18,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Service\ServiceProviderInterface; use Zenstruck\Filesystem\Attribute\UploadedFile; +use Zenstruck\Filesystem\Exception\IncorrectFileHttpException; use Zenstruck\Filesystem\Node\File\PendingFile; /** @@ -55,7 +56,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable if (\count($errors)) { \assert($errors instanceof ConstraintViolationList); - throw new HttpException((int) $attribute->errorStatus, (string) $errors); + throw new IncorrectFileHttpException($attribute->errorStatus, (string) $errors); } } From 588a906a1e73ebeeea36809ae483d8f7e755fd34 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Tue, 14 Mar 2023 11:53:20 +0100 Subject: [PATCH 20/37] Revert controller loader to annotation for older Sf versions --- tests/Fixtures/TestKernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Fixtures/TestKernel.php b/tests/Fixtures/TestKernel.php index 30920bc6..a385bf71 100644 --- a/tests/Fixtures/TestKernel.php +++ b/tests/Fixtures/TestKernel.php @@ -169,7 +169,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ->setAutowired(true) ->setAutoconfigured(true) ; - $c->register('logger', NullLogger::class); // disable logging +// $c->register('logger', NullLogger::class); // disable logging } protected function configureRoutes(RoutingConfigurator $routes): void @@ -186,6 +186,6 @@ protected function configureRoutes(RoutingConfigurator $routes): void $routes->add('private_public', '/private/{path}') ->requirements(['path' => '.+']) ; - $routes->import(__DIR__.'/Controller', 'attribute'); + $routes->import(__DIR__.'/Controller', 'annotation'); } } From eefd7a4366b029854aad0b03d5bcb1f7f84ef24d Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Tue, 14 Mar 2023 12:00:05 +0100 Subject: [PATCH 21/37] Hide PHP 8.1+ test under if --- .../PendingDocumentValueResolverTest.php | 18 +++++----- .../Controller/ArgumentResolverController.php | 11 ------ .../ValidatedArgumentResolverController.php | 36 +++++++++++++++++++ tests/Fixtures/TestKernel.php | 8 +++-- 4 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 tests/Fixtures/Controller/ValidatedArgumentResolverController.php diff --git a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php index 8ec68acb..3f8dc7f3 100644 --- a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php +++ b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php @@ -151,14 +151,16 @@ public function returns_exception_for_invalid_file(): void $response = $client->getResponse(); self::assertSame(422, $response->getStatusCode()); - $client->request( - 'GET', - 'validated-file', - files: ['file' => self::uploadedFile()] - ); - $response = $client->getResponse(); - - self::assertSame(500, $response->getStatusCode()); + if (PHP_VERSION_ID >= 80100) { + $client->request( + 'GET', + 'validated-file', + files: ['file' => self::uploadedFile()] + ); + $response = $client->getResponse(); + + self::assertSame(500, $response->getStatusCode()); + } } private static function uploadedFile(): UploadedFile diff --git a/tests/Fixtures/Controller/ArgumentResolverController.php b/tests/Fixtures/Controller/ArgumentResolverController.php index 6bcc9c4f..e473147b 100644 --- a/tests/Fixtures/Controller/ArgumentResolverController.php +++ b/tests/Fixtures/Controller/ArgumentResolverController.php @@ -72,15 +72,4 @@ public function singleFileWithPath( ): Response { return new Response($file?->contents() ?? ''); } - - #[Route('/validated-file', name: 'validated-file')] - public function validatedFile( - #[UploadedFile( - constraints: [new PendingFileConstraint(mimeTypes: ['application/pdf'])], - errorStatus: 500 - )] - File $file - ): Response { - return new Response($file->contents()); - } } diff --git a/tests/Fixtures/Controller/ValidatedArgumentResolverController.php b/tests/Fixtures/Controller/ValidatedArgumentResolverController.php new file mode 100644 index 00000000..ce67fea0 --- /dev/null +++ b/tests/Fixtures/Controller/ValidatedArgumentResolverController.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + + +namespace Zenstruck\Tests\Fixtures\Controller; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; +use Zenstruck\Filesystem\Attribute\UploadedFile; +use Zenstruck\Filesystem\Node\File; +use Zenstruck\Filesystem\Symfony\Validator\PendingFileConstraint; + +/** + * @author Jakub Caban + */ +class ValidatedArgumentResolverController +{ + #[Route('/validated-file', name: 'validated-file')] + public function validatedFile( + #[UploadedFile( + constraints: [new PendingFileConstraint(mimeTypes: ['application/pdf'])], + errorStatus: 500 + )] + File $file + ): Response { + return new Response($file->contents()); + } +} diff --git a/tests/Fixtures/TestKernel.php b/tests/Fixtures/TestKernel.php index a385bf71..46819e9c 100644 --- a/tests/Fixtures/TestKernel.php +++ b/tests/Fixtures/TestKernel.php @@ -169,7 +169,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ->setAutowired(true) ->setAutoconfigured(true) ; -// $c->register('logger', NullLogger::class); // disable logging + $c->register('logger', NullLogger::class); // disable logging } protected function configureRoutes(RoutingConfigurator $routes): void @@ -186,6 +186,10 @@ protected function configureRoutes(RoutingConfigurator $routes): void $routes->add('private_public', '/private/{path}') ->requirements(['path' => '.+']) ; - $routes->import(__DIR__.'/Controller', 'annotation'); + $routes->import(__DIR__.'/Controller/ArgumentResolverController.php', 'annotation'); + + if (PHP_VERSION_ID >= 80100) { + $routes->import(__DIR__.'/Controller/ValidatedArgumentResolverController.php', 'annotation'); + } } } From c5723bcedaee5f93cd21bc2c96564eeea99e7918 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Tue, 14 Mar 2023 12:20:38 +0100 Subject: [PATCH 22/37] Move constraints logic to `forArgument` --- src/Filesystem/Attribute/UploadedFile.php | 34 +++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index 2de2e6a9..23530896 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -30,15 +30,6 @@ public function __construct( public ?array $constraints = null, public int $errorStatus = 422, ) { - if ($this->image && [] === $this->constraints) { - if ($this->multiple) { - $this->constraints = [ - new All([new PendingImageConstraint()]), - ]; - } else { - $this->constraints = [new PendingImageConstraint()]; - } - } } public static function forArgument(ArgumentMetadata $argument): self @@ -51,19 +42,32 @@ public static function forArgument(ArgumentMetadata $argument): self \assert($attribute instanceof self); } + $constraints = $attribute?->constraints; + $image = $attribute?->image ?? \is_a( + $argument->getType() ?? File::class, + Image::class, + true + ); + + if (null === $constraints && $image) { + if ('array' === $argument->getType()) { + $constraints = [ + new All([new PendingImageConstraint()]), + ]; + } else { + $constraints = [new PendingImageConstraint()]; + } + } + return new self( path: $attribute?->path ?? $argument->getName(), - image: $attribute?->image ?? \is_a( - $argument->getType() ?? File::class, - Image::class, - true - ), + image: $image, multiple: $attribute?->multiple ?? !\is_a( $argument->getType() ?? File::class, File::class, true ), - constraints: $attribute?->constraints ?? [], + constraints: $constraints, errorStatus: $attribute?->errorStatus ?? 422, ); } From 68bb7158f78ac4d609a45affdbfe2de9c2256e05 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Tue, 14 Mar 2023 12:24:28 +0100 Subject: [PATCH 23/37] UploadedFile::$multiple is no more --- src/Filesystem/Attribute/UploadedFile.php | 6 ------ .../Symfony/HttpKernel/PendingFileValueResolverTrait.php | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index 23530896..3c34face 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -26,7 +26,6 @@ final class UploadedFile public function __construct( public ?string $path = null, public ?bool $image = null, - public ?bool $multiple = null, public ?array $constraints = null, public int $errorStatus = 422, ) { @@ -62,11 +61,6 @@ public static function forArgument(ArgumentMetadata $argument): self return new self( path: $attribute?->path ?? $argument->getName(), image: $image, - multiple: $attribute?->multiple ?? !\is_a( - $argument->getType() ?? File::class, - File::class, - true - ), constraints: $constraints, errorStatus: $attribute?->errorStatus ?? 422, ); diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index 3e9056cc..b331fd08 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -43,7 +43,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable $files = $this->extractor()->extractFilesFromRequest( $request, (string) $attribute->path, - (bool) $attribute->multiple, + 'array' === $argument->getType(), (bool) $attribute->image, ); From 0afd4c93d769cb27827ab3070ade3e5f6742fdc6 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Tue, 14 Mar 2023 12:30:30 +0100 Subject: [PATCH 24/37] Move image to the last position of UploadedFile constructor --- src/Filesystem/Attribute/UploadedFile.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index 3c34face..6a0cd61c 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -25,9 +25,9 @@ final class UploadedFile { public function __construct( public ?string $path = null, - public ?bool $image = null, public ?array $constraints = null, public int $errorStatus = 422, + public ?bool $image = null, ) { } @@ -60,9 +60,9 @@ public static function forArgument(ArgumentMetadata $argument): self return new self( path: $attribute?->path ?? $argument->getName(), - image: $image, constraints: $constraints, errorStatus: $attribute?->errorStatus ?? 422, + image: $image, ); } } From d9a7902fff5c11b899b8a0f805aaffa50fe1e8cd Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Tue, 14 Mar 2023 13:16:28 +0100 Subject: [PATCH 25/37] Introduce UploadedFile as extender of PendingUploadedFile attribute (non functional yet!) --- .../Attribute/PendingUploadedFile.php | 65 +++++++++++++++++++ src/Filesystem/Attribute/UploadedFile.php | 56 ++++------------ src/Filesystem/Node/Path/Expression.php | 5 ++ .../PendingFileValueResolverTrait.php | 4 +- .../HttpKernel/RequestFilesExtractor.php | 5 +- .../PendingDocumentValueResolverTest.php | 16 +++++ .../Controller/ArgumentResolverController.php | 17 +++-- .../ValidatedArgumentResolverController.php | 4 +- 8 files changed, 119 insertions(+), 53 deletions(-) create mode 100644 src/Filesystem/Attribute/PendingUploadedFile.php diff --git a/src/Filesystem/Attribute/PendingUploadedFile.php b/src/Filesystem/Attribute/PendingUploadedFile.php new file mode 100644 index 00000000..b43207dd --- /dev/null +++ b/src/Filesystem/Attribute/PendingUploadedFile.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Filesystem\Attribute; + +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\Validator\Constraints\All; +use Zenstruck\Filesystem\Node\File; +use Zenstruck\Filesystem\Node\File\Image; +use Zenstruck\Filesystem\Symfony\Validator\PendingImageConstraint; + +/** + * @author Jakub Caban + */ +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class PendingUploadedFile +{ + public function __construct( + public ?string $path = null, + public ?array $constraints = null, + public int $errorStatus = 422, + public ?bool $image = null, + ) { + } + + public static function forArgument(ArgumentMetadata $argument): self + { + $attributes = $argument->getAttributes(self::class, ArgumentMetadata::IS_INSTANCEOF); + + if (!empty($attributes)) { + $attribute = $attributes[0]; + \assert($attribute instanceof self); + } else { + $attribute = new self(); + } + + $attribute->path ??= $argument->getName(); + + $attribute->image ??= \is_a( + $argument->getType() ?? File::class, + Image::class, + true + ); + + if (null === $attribute->constraints && $attribute->image) { + if ('array' === $argument->getType()) { + $attribute->constraints = [ + new All([new PendingImageConstraint()]), + ]; + } else { + $attribute->constraints = [new PendingImageConstraint()]; + } + } + + return $attribute; + } +} diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index 6a0cd61c..f4753911 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -12,57 +12,27 @@ namespace Zenstruck\Filesystem\Attribute; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; -use Symfony\Component\Validator\Constraints\All; -use Zenstruck\Filesystem\Node\File; -use Zenstruck\Filesystem\Node\File\Image; -use Zenstruck\Filesystem\Symfony\Validator\PendingImageConstraint; +use Zenstruck\Filesystem\Node\Path\Expression; +use Zenstruck\Filesystem\Node\Path\Namer; /** * @author Jakub Caban */ #[\Attribute(\Attribute::TARGET_PARAMETER)] -final class UploadedFile +final class UploadedFile extends PendingUploadedFile { + public string|Namer $namer; + public function __construct( - public ?string $path = null, - public ?array $constraints = null, - public int $errorStatus = 422, - public ?bool $image = null, + public string $filesystem, + string|Namer|null $namer = null, + ?string $path = null, + ?array $constraints = null, + int $errorStatus = 422, + ?bool $image = null, ) { - } - - public static function forArgument(ArgumentMetadata $argument): self - { - $attributes = $argument->getAttributes(self::class); - - $attribute = null; - if (!empty($attributes)) { - $attribute = $attributes[0]; - \assert($attribute instanceof self); - } - - $constraints = $attribute?->constraints; - $image = $attribute?->image ?? \is_a( - $argument->getType() ?? File::class, - Image::class, - true - ); - - if (null === $constraints && $image) { - if ('array' === $argument->getType()) { - $constraints = [ - new All([new PendingImageConstraint()]), - ]; - } else { - $constraints = [new PendingImageConstraint()]; - } - } + parent::__construct($path, $constraints, $errorStatus, $image); - return new self( - path: $attribute?->path ?? $argument->getName(), - constraints: $constraints, - errorStatus: $attribute?->errorStatus ?? 422, - image: $image, - ); + $this->namer = $namer ?? Expression::uniqueSlug(); } } diff --git a/src/Filesystem/Node/Path/Expression.php b/src/Filesystem/Node/Path/Expression.php index ea4dfe70..593e3cd9 100644 --- a/src/Filesystem/Node/Path/Expression.php +++ b/src/Filesystem/Node/Path/Expression.php @@ -33,6 +33,11 @@ public static function slugify(): self return new self('{name}{ext}'); } + public static function uniqueSlug(): self + { + return new self('{checksum}/{name}{ext}'); + } + /** * @param ?positive-int $length */ diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index b331fd08..b7c75ec7 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -17,7 +17,7 @@ use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Service\ServiceProviderInterface; -use Zenstruck\Filesystem\Attribute\UploadedFile; +use Zenstruck\Filesystem\Attribute\PendingUploadedFile; use Zenstruck\Filesystem\Exception\IncorrectFileHttpException; use Zenstruck\Filesystem\Node\File\PendingFile; @@ -38,7 +38,7 @@ public function __construct( */ public function resolve(Request $request, ArgumentMetadata $argument): iterable { - $attribute = UploadedFile::forArgument($argument); + $attribute = PendingUploadedFile::forArgument($argument); $files = $this->extractor()->extractFilesFromRequest( $request, diff --git a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php index 95f5a2fb..4c322a11 100644 --- a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php +++ b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php @@ -16,7 +16,8 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\PropertyAccess\PropertyAccessor; -use Zenstruck\Filesystem\Attribute\UploadedFile as UploadedFileAttribute; +use Zenstruck\Filesystem\Attribute\PendingUploadedFile; +use Zenstruck\Filesystem\Attribute\PendingUploadedFile as UploadedFileAttribute; use Zenstruck\Filesystem\Exception\NodeTypeMismatch; use Zenstruck\Filesystem\Node\File; use Zenstruck\Filesystem\Node\File\Image\PendingImage; @@ -81,7 +82,7 @@ public function extractFilesFromRequest( public static function supports(ArgumentMetadata $argument): bool { - $attributes = $argument->getAttributes(UploadedFileAttribute::class); + $attributes = $argument->getAttributes(PendingUploadedFile::class, ArgumentMetadata::IS_INSTANCEOF); if (empty($attributes)) { $type = $argument->getType(); diff --git a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php index 3f8dc7f3..ee0448cc 100644 --- a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php +++ b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php @@ -66,6 +66,22 @@ public function inject_on_typed_argument(): void self::assertSame('563', $client->getResponse()->getContent()); } + /** + * @test + */ + public function inject_stored(): void + { + $client = self::createClient(); + + $client->request( + 'GET', + 'single-stored-file', + files: ['file' => self::uploadedFile()] + ); + + self::assertSame("some content\n", $client->getResponse()->getContent()); + } + /** * @test */ diff --git a/tests/Fixtures/Controller/ArgumentResolverController.php b/tests/Fixtures/Controller/ArgumentResolverController.php index e473147b..a3370e9e 100644 --- a/tests/Fixtures/Controller/ArgumentResolverController.php +++ b/tests/Fixtures/Controller/ArgumentResolverController.php @@ -13,6 +13,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Zenstruck\Filesystem\Attribute\PendingUploadedFile; use Zenstruck\Filesystem\Attribute\UploadedFile; use Zenstruck\Filesystem\Node\File; use Zenstruck\Filesystem\Node\File\Image; @@ -25,7 +26,7 @@ class ArgumentResolverController { #[Route('/multiple-files', name: 'multiple-files')] public function multipleFiles( - #[UploadedFile] + #[PendingUploadedFile] array $files ): Response { return new Response((string) \count($files)); @@ -33,7 +34,7 @@ public function multipleFiles( #[Route('/multiple-images', name: 'multiple-images')] public function multipleImages( - #[UploadedFile(image: true)] + #[PendingUploadedFile(image: true)] array $images ): Response { return new Response((string) \count($images)); @@ -41,7 +42,7 @@ public function multipleImages( #[Route('/multiple-files-with-path', name: 'multiple-files-with-path')] public function multipleFilesWithPath( - #[UploadedFile('data[files]')] + #[PendingUploadedFile('data[files]')] array $files ): Response { return new Response((string) \count($files)); @@ -59,6 +60,14 @@ public function singleFile(?File $file): Response return new Response($file?->contents() ?? ''); } + #[Route('/single-stored-file', name: 'single-stored-file')] + public function singleStoredFile( + #[UploadedFile('public')] + ?File $file + ): Response{ + return new Response($file?->contents() ?? ''); + } + #[Route('/single-image', name: 'single-image')] public function singleImage(Image $image): Response { @@ -67,7 +76,7 @@ public function singleImage(Image $image): Response #[Route('/single-file-with-path', name: 'single-file-with-path')] public function singleFileWithPath( - #[UploadedFile('data[file]')] + #[PendingUploadedFile('data[file]')] ?File $file ): Response { return new Response($file?->contents() ?? ''); diff --git a/tests/Fixtures/Controller/ValidatedArgumentResolverController.php b/tests/Fixtures/Controller/ValidatedArgumentResolverController.php index ce67fea0..82d58bfb 100644 --- a/tests/Fixtures/Controller/ValidatedArgumentResolverController.php +++ b/tests/Fixtures/Controller/ValidatedArgumentResolverController.php @@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; -use Zenstruck\Filesystem\Attribute\UploadedFile; +use Zenstruck\Filesystem\Attribute\PendingUploadedFile; use Zenstruck\Filesystem\Node\File; use Zenstruck\Filesystem\Symfony\Validator\PendingFileConstraint; @@ -25,7 +25,7 @@ class ValidatedArgumentResolverController { #[Route('/validated-file', name: 'validated-file')] public function validatedFile( - #[UploadedFile( + #[PendingUploadedFile( constraints: [new PendingFileConstraint(mimeTypes: ['application/pdf'])], errorStatus: 500 )] From 8e793cd3973396a374ba97df166785e4bfb8e4c9 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Tue, 14 Mar 2023 13:48:22 +0100 Subject: [PATCH 26/37] Handle storing files with #[UploadedFile] attribute --- .../Attribute/PendingUploadedFile.php | 2 +- src/Filesystem/Attribute/UploadedFile.php | 4 +- src/Filesystem/Node/Path/Expression.php | 5 -- .../ZenstruckFilesystemExtension.php | 2 + .../PendingFileValueResolverTrait.php | 51 ++++++++++++++++++- .../PendingDocumentValueResolverTest.php | 5 +- .../Controller/ArgumentResolverController.php | 10 +++- 7 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/Filesystem/Attribute/PendingUploadedFile.php b/src/Filesystem/Attribute/PendingUploadedFile.php index b43207dd..bb6c3714 100644 --- a/src/Filesystem/Attribute/PendingUploadedFile.php +++ b/src/Filesystem/Attribute/PendingUploadedFile.php @@ -20,7 +20,7 @@ /** * @author Jakub Caban */ -#[\Attribute(\Attribute::TARGET_PARAMETER)] +#[\Attribute(\Attribute::TARGET_PARAMETER|\Attribute::TARGET_PROPERTY)] class PendingUploadedFile { public function __construct( diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index f4753911..6cf34ed5 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -18,7 +18,7 @@ /** * @author Jakub Caban */ -#[\Attribute(\Attribute::TARGET_PARAMETER)] +#[\Attribute(\Attribute::TARGET_PARAMETER|\Attribute::TARGET_PROPERTY)] final class UploadedFile extends PendingUploadedFile { public string|Namer $namer; @@ -33,6 +33,6 @@ public function __construct( ) { parent::__construct($path, $constraints, $errorStatus, $image); - $this->namer = $namer ?? Expression::uniqueSlug(); + $this->namer = $namer ?? new Expression('{checksum}/{name}{ext}'); } } diff --git a/src/Filesystem/Node/Path/Expression.php b/src/Filesystem/Node/Path/Expression.php index 593e3cd9..ea4dfe70 100644 --- a/src/Filesystem/Node/Path/Expression.php +++ b/src/Filesystem/Node/Path/Expression.php @@ -33,11 +33,6 @@ public static function slugify(): self return new self('{name}{ext}'); } - public static function uniqueSlug(): self - { - return new self('{checksum}/{name}{ext}'); - } - /** * @param ?positive-int $length */ diff --git a/src/Filesystem/Symfony/DependencyInjection/ZenstruckFilesystemExtension.php b/src/Filesystem/Symfony/DependencyInjection/ZenstruckFilesystemExtension.php index 989a36a4..e4248fc7 100644 --- a/src/Filesystem/Symfony/DependencyInjection/ZenstruckFilesystemExtension.php +++ b/src/Filesystem/Symfony/DependencyInjection/ZenstruckFilesystemExtension.php @@ -166,6 +166,8 @@ private function registerDoctrine(ContainerBuilder $container, array $config): v ->addTag('controller.argument_value_resolver', ['priority' => 110]) ->addArgument( new ServiceLocatorArgument([ + FilesystemRegistry::class => new Reference(FilesystemRegistry::class), + PathGenerator::class => new Reference(PathGenerator::class), RequestFilesExtractor::class => new Reference('.zenstruck_document.value_resolver.request_files_extractor'), ValidatorInterface::class => new Reference(ValidatorInterface::class), ]) diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index b7c75ec7..561171b5 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -17,9 +17,17 @@ use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Service\ServiceProviderInterface; +use Zenstruck\Filesystem; use Zenstruck\Filesystem\Attribute\PendingUploadedFile; +use Zenstruck\Filesystem\Attribute\UploadedFile; use Zenstruck\Filesystem\Exception\IncorrectFileHttpException; +use Zenstruck\Filesystem\FilesystemRegistry; +use Zenstruck\Filesystem\Node; +use Zenstruck\Filesystem\Node\File; +use Zenstruck\Filesystem\Node\File\LazyFile; use Zenstruck\Filesystem\Node\File\PendingFile; +use Zenstruck\Filesystem\Node\Mapping; +use Zenstruck\Filesystem\Node\PathGenerator; /** * @author Jakub Caban @@ -47,7 +55,11 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable (bool) $attribute->image, ); - if ($files && $attribute->constraints) { + if (!$files) { + return [$files]; + } + + if ($attribute->constraints) { $errors = $this->validator()->validate( $files, $attribute->constraints @@ -60,14 +72,51 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable } } + if ($attribute instanceof UploadedFile) { + if (is_array($files)) { + $files = array_map( + fn (PendingFile $file) => $this->saveFile($attribute, $file), + $files + ); + } else { + $files = $this->saveFile($attribute, $files); + } + } + return [$files]; } + private function saveFile(UploadedFile $uploadedFile, PendingFile $file): File + { + $path = $this->generatePath($uploadedFile, $file); + $file = $this->filesystem($uploadedFile->filesystem) + ->write($path, $file); + + if ($uploadedFile->image) { + return $file->ensureImage(); + } + + return $file; + } + private function extractor(): RequestFilesExtractor { return $this->locator->get(RequestFilesExtractor::class); } + private function filesystem(string $filesystem): Filesystem + { + return $this->locator->get(FilesystemRegistry::class)->get($filesystem); + } + + private function generatePath(UploadedFile $uploadedFile, Node $node): string + { + return $this->locator->get(PathGenerator::class)->generate( + $uploadedFile->namer, + $node + ); + } + private function validator(): ValidatorInterface { return $this->locator->get(ValidatorInterface::class); diff --git a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php index ee0448cc..00df60a6 100644 --- a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php +++ b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php @@ -79,7 +79,10 @@ public function inject_stored(): void files: ['file' => self::uploadedFile()] ); - self::assertSame("some content\n", $client->getResponse()->getContent()); + self::assertSame( + "public://eb9c2bf0eb63f3a7bc0ea37ef18aeba5/test.txt:some content\n", + $client->getResponse()->getContent() + ); } /** diff --git a/tests/Fixtures/Controller/ArgumentResolverController.php b/tests/Fixtures/Controller/ArgumentResolverController.php index a3370e9e..16d529d5 100644 --- a/tests/Fixtures/Controller/ArgumentResolverController.php +++ b/tests/Fixtures/Controller/ArgumentResolverController.php @@ -63,9 +63,15 @@ public function singleFile(?File $file): Response #[Route('/single-stored-file', name: 'single-stored-file')] public function singleStoredFile( #[UploadedFile('public')] - ?File $file + File $file ): Response{ - return new Response($file?->contents() ?? ''); + return new Response( + sprintf( + '%s:%s', + $file->dsn(), + $file->contents() + ) + ); } #[Route('/single-image', name: 'single-image')] From d00ee79b1e253e45798c6b847da523420c44440b Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Tue, 14 Mar 2023 13:51:15 +0100 Subject: [PATCH 27/37] Fix typehint --- .../Symfony/HttpKernel/PendingFileValueResolverTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index 561171b5..0e3b0ee7 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -42,7 +42,7 @@ public function __construct( } /** - * @return iterable + * @return iterable */ public function resolve(Request $request, ArgumentMetadata $argument): iterable { From c27ea4afa0a44bf05db83f7526f5bac56197fb31 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Mon, 3 Apr 2023 11:54:13 +0200 Subject: [PATCH 28/37] Update src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php Co-authored-by: Kevin Bond --- src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php index 4c322a11..1affb47e 100644 --- a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php +++ b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php @@ -25,6 +25,8 @@ /** * @author Jakub Caban + * + * @internal */ class RequestFilesExtractor { From 479cd43ed6ec810012513337245a248951fc3052 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Mon, 3 Apr 2023 11:56:13 +0200 Subject: [PATCH 29/37] Move IncorrectFileHttpException --- .../{ => Symfony}/Exception/IncorrectFileHttpException.php | 2 +- .../Symfony/HttpKernel/PendingFileValueResolverTrait.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/Filesystem/{ => Symfony}/Exception/IncorrectFileHttpException.php (89%) diff --git a/src/Filesystem/Exception/IncorrectFileHttpException.php b/src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php similarity index 89% rename from src/Filesystem/Exception/IncorrectFileHttpException.php rename to src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php index 1c85006f..7cd49b39 100644 --- a/src/Filesystem/Exception/IncorrectFileHttpException.php +++ b/src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Filesystem\Exception; +namespace Zenstruck\Filesystem\Symfony\Exception; use Symfony\Component\HttpKernel\Exception\HttpException; diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index 0e3b0ee7..c9fec278 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -20,7 +20,6 @@ use Zenstruck\Filesystem; use Zenstruck\Filesystem\Attribute\PendingUploadedFile; use Zenstruck\Filesystem\Attribute\UploadedFile; -use Zenstruck\Filesystem\Exception\IncorrectFileHttpException; use Zenstruck\Filesystem\FilesystemRegistry; use Zenstruck\Filesystem\Node; use Zenstruck\Filesystem\Node\File; @@ -28,6 +27,7 @@ use Zenstruck\Filesystem\Node\File\PendingFile; use Zenstruck\Filesystem\Node\Mapping; use Zenstruck\Filesystem\Node\PathGenerator; +use Zenstruck\Filesystem\Symfony\Exception\IncorrectFileHttpException; /** * @author Jakub Caban From 70e0203ff619827b30e19f74954fdd319da003e2 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Mon, 3 Apr 2023 11:56:48 +0200 Subject: [PATCH 30/37] Remove phpstan ignore --- phpstan.neon | 1 - 1 file changed, 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 164b840d..bd653e05 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,5 @@ parameters: level: 8 - checkGenericClassInNonGenericObjectType: false treatPhpDocTypesAsCertain: false paths: - src From e716dba1bd491576c9d3951ec9715fd73c7b43cc Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Mon, 3 Apr 2023 12:00:38 +0200 Subject: [PATCH 31/37] Add phpstan-ignore-line and add @readonly to attributes --- src/Filesystem/Attribute/PendingUploadedFile.php | 2 ++ src/Filesystem/Attribute/UploadedFile.php | 2 ++ .../Symfony/HttpKernel/PendingFileValueResolverTrait.php | 4 +--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Filesystem/Attribute/PendingUploadedFile.php b/src/Filesystem/Attribute/PendingUploadedFile.php index bb6c3714..ca458f92 100644 --- a/src/Filesystem/Attribute/PendingUploadedFile.php +++ b/src/Filesystem/Attribute/PendingUploadedFile.php @@ -19,6 +19,8 @@ /** * @author Jakub Caban + * + * @readonly */ #[\Attribute(\Attribute::TARGET_PARAMETER|\Attribute::TARGET_PROPERTY)] class PendingUploadedFile diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index 6cf34ed5..e2911bc3 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -17,6 +17,8 @@ /** * @author Jakub Caban + * + * @readonly */ #[\Attribute(\Attribute::TARGET_PARAMETER|\Attribute::TARGET_PROPERTY)] final class UploadedFile extends PendingUploadedFile diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index c9fec278..381a41c9 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -36,9 +36,7 @@ */ trait PendingFileValueResolverTrait { - public function __construct( - private ServiceProviderInterface $locator - ) { + /** @phpstan-ignore-line */public function __construct(private ServiceProviderInterface $locator) { } /** From 2ea5cb306612a9ec033b7bb8d6839d1de557a534 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Mon, 3 Apr 2023 12:01:57 +0200 Subject: [PATCH 32/37] CS fixer run --- .../Attribute/PendingUploadedFile.php | 2 +- src/Filesystem/Attribute/UploadedFile.php | 3 +- .../Exception/IncorrectFileHttpException.php | 1 - .../PendingFileValueResolverTrait.php | 132 +++++++++--------- .../HttpKernel/RequestFilesExtractor.php | 1 - .../PendingDocumentValueResolverTest.php | 2 +- .../Controller/ArgumentResolverController.php | 5 +- .../ValidatedArgumentResolverController.php | 1 - tests/Fixtures/TestKernel.php | 2 +- 9 files changed, 72 insertions(+), 77 deletions(-) diff --git a/src/Filesystem/Attribute/PendingUploadedFile.php b/src/Filesystem/Attribute/PendingUploadedFile.php index ca458f92..b512cd63 100644 --- a/src/Filesystem/Attribute/PendingUploadedFile.php +++ b/src/Filesystem/Attribute/PendingUploadedFile.php @@ -22,7 +22,7 @@ * * @readonly */ -#[\Attribute(\Attribute::TARGET_PARAMETER|\Attribute::TARGET_PROPERTY)] +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)] class PendingUploadedFile { public function __construct( diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index e2911bc3..33f34e7d 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -11,7 +11,6 @@ namespace Zenstruck\Filesystem\Attribute; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Zenstruck\Filesystem\Node\Path\Expression; use Zenstruck\Filesystem\Node\Path\Namer; @@ -20,7 +19,7 @@ * * @readonly */ -#[\Attribute(\Attribute::TARGET_PARAMETER|\Attribute::TARGET_PROPERTY)] +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)] final class UploadedFile extends PendingUploadedFile { public string|Namer $namer; diff --git a/src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php b/src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php index 7cd49b39..d8e66284 100644 --- a/src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php +++ b/src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php @@ -18,5 +18,4 @@ */ class IncorrectFileHttpException extends HttpException { - } diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index 381a41c9..6ad07987 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -13,7 +13,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; -use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Service\ServiceProviderInterface; @@ -23,9 +22,7 @@ use Zenstruck\Filesystem\FilesystemRegistry; use Zenstruck\Filesystem\Node; use Zenstruck\Filesystem\Node\File; -use Zenstruck\Filesystem\Node\File\LazyFile; use Zenstruck\Filesystem\Node\File\PendingFile; -use Zenstruck\Filesystem\Node\Mapping; use Zenstruck\Filesystem\Node\PathGenerator; use Zenstruck\Filesystem\Symfony\Exception\IncorrectFileHttpException; @@ -36,87 +33,90 @@ */ trait PendingFileValueResolverTrait { - /** @phpstan-ignore-line */public function __construct(private ServiceProviderInterface $locator) { + /** @phpstan-ignore-line */ + public function __construct(private ServiceProviderInterface $locator) + { } - /** + /** * @return iterable */ - public function resolve(Request $request, ArgumentMetadata $argument): iterable - { - $attribute = PendingUploadedFile::forArgument($argument); - - $files = $this->extractor()->extractFilesFromRequest( - $request, - (string) $attribute->path, - 'array' === $argument->getType(), - (bool) $attribute->image, - ); + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + $attribute = PendingUploadedFile::forArgument($argument); + + $files = $this->extractor()->extractFilesFromRequest( + $request, + (string) $attribute->path, + 'array' === $argument->getType(), + (bool) $attribute->image, + ); - if (!$files) { - return [$files]; - } + if (!$files) { + return [$files]; + } - if ($attribute->constraints) { - $errors = $this->validator()->validate( - $files, - $attribute->constraints - ); + if ($attribute->constraints) { + $errors = $this->validator()->validate( + $files, + $attribute->constraints + ); - if (\count($errors)) { - \assert($errors instanceof ConstraintViolationList); + if (\count($errors)) { + \assert($errors instanceof ConstraintViolationList); - throw new IncorrectFileHttpException($attribute->errorStatus, (string) $errors); + throw new IncorrectFileHttpException($attribute->errorStatus, (string) $errors); + } } - } - if ($attribute instanceof UploadedFile) { - if (is_array($files)) { - $files = array_map( - fn (PendingFile $file) => $this->saveFile($attribute, $file), - $files - ); - } else { - $files = $this->saveFile($attribute, $files); + if ($attribute instanceof UploadedFile) { + if (\is_array($files)) { + $files = \array_map( + fn(PendingFile $file) => $this->saveFile($attribute, $file), + $files + ); + } else { + $files = $this->saveFile($attribute, $files); + } } + + return [$files]; } - return [$files]; - } + private function saveFile(UploadedFile $uploadedFile, PendingFile $file): File + { + $path = $this->generatePath($uploadedFile, $file); + $file = $this->filesystem($uploadedFile->filesystem) + ->write($path, $file) + ; - private function saveFile(UploadedFile $uploadedFile, PendingFile $file): File - { - $path = $this->generatePath($uploadedFile, $file); - $file = $this->filesystem($uploadedFile->filesystem) - ->write($path, $file); + if ($uploadedFile->image) { + return $file->ensureImage(); + } - if ($uploadedFile->image) { - return $file->ensureImage(); + return $file; } - return $file; - } - - private function extractor(): RequestFilesExtractor - { - return $this->locator->get(RequestFilesExtractor::class); - } + private function extractor(): RequestFilesExtractor + { + return $this->locator->get(RequestFilesExtractor::class); + } - private function filesystem(string $filesystem): Filesystem - { - return $this->locator->get(FilesystemRegistry::class)->get($filesystem); - } + private function filesystem(string $filesystem): Filesystem + { + return $this->locator->get(FilesystemRegistry::class)->get($filesystem); + } - private function generatePath(UploadedFile $uploadedFile, Node $node): string - { - return $this->locator->get(PathGenerator::class)->generate( - $uploadedFile->namer, - $node - ); - } + private function generatePath(UploadedFile $uploadedFile, Node $node): string + { + return $this->locator->get(PathGenerator::class)->generate( + $uploadedFile->namer, + $node + ); + } - private function validator(): ValidatorInterface - { - return $this->locator->get(ValidatorInterface::class); - } + private function validator(): ValidatorInterface + { + return $this->locator->get(ValidatorInterface::class); + } } diff --git a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php index 1affb47e..56c9d5f3 100644 --- a/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php +++ b/src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php @@ -17,7 +17,6 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\PropertyAccess\PropertyAccessor; use Zenstruck\Filesystem\Attribute\PendingUploadedFile; -use Zenstruck\Filesystem\Attribute\PendingUploadedFile as UploadedFileAttribute; use Zenstruck\Filesystem\Exception\NodeTypeMismatch; use Zenstruck\Filesystem\Node\File; use Zenstruck\Filesystem\Node\File\Image\PendingImage; diff --git a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php index 00df60a6..53f0b16f 100644 --- a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php +++ b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php @@ -170,7 +170,7 @@ public function returns_exception_for_invalid_file(): void $response = $client->getResponse(); self::assertSame(422, $response->getStatusCode()); - if (PHP_VERSION_ID >= 80100) { + if (\PHP_VERSION_ID >= 80100) { $client->request( 'GET', 'validated-file', diff --git a/tests/Fixtures/Controller/ArgumentResolverController.php b/tests/Fixtures/Controller/ArgumentResolverController.php index 16d529d5..665677b0 100644 --- a/tests/Fixtures/Controller/ArgumentResolverController.php +++ b/tests/Fixtures/Controller/ArgumentResolverController.php @@ -17,7 +17,6 @@ use Zenstruck\Filesystem\Attribute\UploadedFile; use Zenstruck\Filesystem\Node\File; use Zenstruck\Filesystem\Node\File\Image; -use Zenstruck\Filesystem\Symfony\Validator\PendingFileConstraint; /** * @author Jakub Caban @@ -64,9 +63,9 @@ public function singleFile(?File $file): Response public function singleStoredFile( #[UploadedFile('public')] File $file - ): Response{ + ): Response { return new Response( - sprintf( + \sprintf( '%s:%s', $file->dsn(), $file->contents() diff --git a/tests/Fixtures/Controller/ValidatedArgumentResolverController.php b/tests/Fixtures/Controller/ValidatedArgumentResolverController.php index 82d58bfb..f46f0d9f 100644 --- a/tests/Fixtures/Controller/ValidatedArgumentResolverController.php +++ b/tests/Fixtures/Controller/ValidatedArgumentResolverController.php @@ -9,7 +9,6 @@ * file that was distributed with this source code. */ - namespace Zenstruck\Tests\Fixtures\Controller; use Symfony\Component\HttpFoundation\Response; diff --git a/tests/Fixtures/TestKernel.php b/tests/Fixtures/TestKernel.php index 46819e9c..f857c719 100644 --- a/tests/Fixtures/TestKernel.php +++ b/tests/Fixtures/TestKernel.php @@ -188,7 +188,7 @@ protected function configureRoutes(RoutingConfigurator $routes): void ; $routes->import(__DIR__.'/Controller/ArgumentResolverController.php', 'annotation'); - if (PHP_VERSION_ID >= 80100) { + if (\PHP_VERSION_ID >= 80100) { $routes->import(__DIR__.'/Controller/ValidatedArgumentResolverController.php', 'annotation'); } } From 0dcf1a5a8c536a90874720c15b75531902be2d77 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Mon, 3 Apr 2023 12:04:48 +0200 Subject: [PATCH 33/37] Fix tests --- src/Filesystem/Attribute/PendingUploadedFile.php | 1 - src/Filesystem/Attribute/UploadedFile.php | 3 +-- .../Symfony/Exception/IncorrectFileHttpException.php | 4 ++-- .../Symfony/HttpKernel/PendingFileValueResolverTrait.php | 2 +- .../Symfony/HttpKernel/PendingDocumentValueResolverTest.php | 2 +- .../Controller/ValidatedArgumentResolverController.php | 3 +-- 6 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Filesystem/Attribute/PendingUploadedFile.php b/src/Filesystem/Attribute/PendingUploadedFile.php index b512cd63..9a12b917 100644 --- a/src/Filesystem/Attribute/PendingUploadedFile.php +++ b/src/Filesystem/Attribute/PendingUploadedFile.php @@ -28,7 +28,6 @@ class PendingUploadedFile public function __construct( public ?string $path = null, public ?array $constraints = null, - public int $errorStatus = 422, public ?bool $image = null, ) { } diff --git a/src/Filesystem/Attribute/UploadedFile.php b/src/Filesystem/Attribute/UploadedFile.php index 33f34e7d..dd23aa26 100644 --- a/src/Filesystem/Attribute/UploadedFile.php +++ b/src/Filesystem/Attribute/UploadedFile.php @@ -29,10 +29,9 @@ public function __construct( string|Namer|null $namer = null, ?string $path = null, ?array $constraints = null, - int $errorStatus = 422, ?bool $image = null, ) { - parent::__construct($path, $constraints, $errorStatus, $image); + parent::__construct($path, $constraints, $image); $this->namer = $namer ?? new Expression('{checksum}/{name}{ext}'); } diff --git a/src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php b/src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php index d8e66284..ef3d94a2 100644 --- a/src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php +++ b/src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php @@ -11,11 +11,11 @@ namespace Zenstruck\Filesystem\Symfony\Exception; -use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; /** * @author Jakub Caban */ -class IncorrectFileHttpException extends HttpException +class IncorrectFileHttpException extends UnprocessableEntityHttpException { } diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index 6ad07987..2ebd93be 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -65,7 +65,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable if (\count($errors)) { \assert($errors instanceof ConstraintViolationList); - throw new IncorrectFileHttpException($attribute->errorStatus, (string) $errors); + throw new IncorrectFileHttpException((string) $errors); } } diff --git a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php index 53f0b16f..5ebc9db5 100644 --- a/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php +++ b/tests/Filesystem/Symfony/HttpKernel/PendingDocumentValueResolverTest.php @@ -178,7 +178,7 @@ public function returns_exception_for_invalid_file(): void ); $response = $client->getResponse(); - self::assertSame(500, $response->getStatusCode()); + self::assertSame(422, $response->getStatusCode()); } } diff --git a/tests/Fixtures/Controller/ValidatedArgumentResolverController.php b/tests/Fixtures/Controller/ValidatedArgumentResolverController.php index f46f0d9f..83a3dffe 100644 --- a/tests/Fixtures/Controller/ValidatedArgumentResolverController.php +++ b/tests/Fixtures/Controller/ValidatedArgumentResolverController.php @@ -25,8 +25,7 @@ class ValidatedArgumentResolverController #[Route('/validated-file', name: 'validated-file')] public function validatedFile( #[PendingUploadedFile( - constraints: [new PendingFileConstraint(mimeTypes: ['application/pdf'])], - errorStatus: 500 + constraints: [new PendingFileConstraint(mimeTypes: ['application/pdf'])] )] File $file ): Response { From 1874fb5872cbe5db8f47817b06936be899bcc95e Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Mon, 3 Apr 2023 12:05:10 +0200 Subject: [PATCH 34/37] code style again --- .../Symfony/HttpKernel/PendingFileValueResolverTrait.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index 2ebd93be..f7e8d4ea 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -39,8 +39,8 @@ public function __construct(private ServiceProviderInterface $locator) } /** - * @return iterable - */ + * @return iterable + */ public function resolve(Request $request, ArgumentMetadata $argument): iterable { $attribute = PendingUploadedFile::forArgument($argument); From c4e9731ef6e5ec825c10ee0365159282b71786ad Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Mon, 3 Apr 2023 12:14:14 +0200 Subject: [PATCH 35/37] Add pointless docblock to silence phpstan --- .../Symfony/HttpKernel/PendingFileValueResolverTrait.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php index f7e8d4ea..75693748 100644 --- a/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php +++ b/src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php @@ -33,7 +33,9 @@ */ trait PendingFileValueResolverTrait { - /** @phpstan-ignore-line */ + /** + * @param ServiceProviderInterface $locator + */ public function __construct(private ServiceProviderInterface $locator) { } From 9229221c0fc0044915e3e2f4429f1507f9463c61 Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 7 Apr 2023 13:51:30 +0200 Subject: [PATCH 36/37] Apply suggestions from code review Co-authored-by: Kevin Bond --- src/Filesystem/Attribute/PendingUploadedFile.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Filesystem/Attribute/PendingUploadedFile.php b/src/Filesystem/Attribute/PendingUploadedFile.php index 9a12b917..511ed3f4 100644 --- a/src/Filesystem/Attribute/PendingUploadedFile.php +++ b/src/Filesystem/Attribute/PendingUploadedFile.php @@ -32,6 +32,9 @@ public function __construct( ) { } + /** + * @internal + */ public static function forArgument(ArgumentMetadata $argument): self { $attributes = $argument->getAttributes(self::class, ArgumentMetadata::IS_INSTANCEOF); @@ -40,7 +43,7 @@ public static function forArgument(ArgumentMetadata $argument): self $attribute = $attributes[0]; \assert($attribute instanceof self); } else { - $attribute = new self(); + $attribute = new static(); } $attribute->path ??= $argument->getName(); From 37892c50a2827d3b31b67cfa4aa72190028a6a4b Mon Sep 17 00:00:00 2001 From: Jakub Caban Date: Fri, 7 Apr 2023 13:56:56 +0200 Subject: [PATCH 37/37] Mark PendingUploadedFile constructor as consistent for phpstan --- src/Filesystem/Attribute/PendingUploadedFile.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Filesystem/Attribute/PendingUploadedFile.php b/src/Filesystem/Attribute/PendingUploadedFile.php index 511ed3f4..bba1e2dd 100644 --- a/src/Filesystem/Attribute/PendingUploadedFile.php +++ b/src/Filesystem/Attribute/PendingUploadedFile.php @@ -20,6 +20,7 @@ /** * @author Jakub Caban * + * @phpstan-consistent-constructor * @readonly */ #[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]