Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Value resolver #95

Merged
merged 37 commits into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
da3addf
Import value resolver from document-library
Lustmored Feb 24, 2023
2d260bf
Refactor FilesExtractor to handle PendingFile subclasses in supports()
Lustmored Feb 24, 2023
fb83336
Handle PendingImage in value resolving
Lustmored Feb 24, 2023
1c3464f
Fix Phpstan warnings and run CS fixer
Lustmored Mar 2, 2023
78ffb28
Add missing functional tests
Lustmored Mar 3, 2023
60ae750
Allow File/Image typehint for value resolver
Lustmored Mar 3, 2023
45a5848
Simplify PendingFileValueResolver by introducing a common trait
Lustmored Mar 3, 2023
92fe778
Add simple image injection functional tests
Lustmored Mar 3, 2023
e8dee6c
Utilize UploadedFile::forArgument() for a centralized value resolver …
Lustmored Mar 3, 2023
a0e63ca
Extend UploadedFile to constraints and errorStatus
Lustmored Mar 3, 2023
45dd655
Add file validation logic to pendingfileValueResolver
Lustmored Mar 3, 2023
402f986
Add simple test case for invalid image
Lustmored Mar 3, 2023
669b297
Add valueResolver exception tests
Lustmored Mar 3, 2023
f52409c
CS run
Lustmored Mar 3, 2023
0e8a3cc
Trailing comma is a no-go in PHP8 :)
Lustmored Mar 3, 2023
7a2252a
Make phpstan happy
Lustmored Mar 3, 2023
81e1b15
Trivial suggestions
Lustmored Mar 14, 2023
0636c54
Disable logger on tests
Lustmored Mar 14, 2023
52d320d
Add custom exception for incorrect file
Lustmored Mar 14, 2023
588a906
Revert controller loader to annotation for older Sf versions
Lustmored Mar 14, 2023
eefd7a4
Hide PHP 8.1+ test under if
Lustmored Mar 14, 2023
c5723bc
Move constraints logic to `forArgument`
Lustmored Mar 14, 2023
68bb715
UploadedFile::$multiple is no more
Lustmored Mar 14, 2023
0afd4c9
Move image to the last position of UploadedFile constructor
Lustmored Mar 14, 2023
d9a7902
Introduce UploadedFile as extender of PendingUploadedFile attribute (…
Lustmored Mar 14, 2023
8e793cd
Handle storing files with #[UploadedFile] attribute
Lustmored Mar 14, 2023
d00ee79
Fix typehint
Lustmored Mar 14, 2023
c27ea4a
Update src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php
Lustmored Apr 3, 2023
479cd43
Move IncorrectFileHttpException
Lustmored Apr 3, 2023
70e0203
Remove phpstan ignore
Lustmored Apr 3, 2023
e716dba
Add phpstan-ignore-line and add @readonly to attributes
Lustmored Apr 3, 2023
2ea5cb3
CS fixer run
Lustmored Apr 3, 2023
0dcf1a5
Fix tests
Lustmored Apr 3, 2023
1874fb5
code style again
Lustmored Apr 3, 2023
c4e9731
Add pointless docblock to silence phpstan
Lustmored Apr 3, 2023
9229221
Apply suggestions from code review
Lustmored Apr 7, 2023
37892c5
Mark PendingUploadedFile constructor as consistent for phpstan
Lustmored Apr 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/Filesystem/Attribute/PendingUploadedFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/*
* This file is part of the zenstruck/filesystem package.
*
* (c) Kevin Bond <[email protected]>
*
* 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 <[email protected]>
Lustmored marked this conversation as resolved.
Show resolved Hide resolved
*
* @readonly
*/
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
class PendingUploadedFile
{
public function __construct(
public ?string $path = null,
public ?array $constraints = null,
public ?bool $image = null,
) {
}

public static function forArgument(ArgumentMetadata $argument): self
Lustmored marked this conversation as resolved.
Show resolved Hide resolved
{
$attributes = $argument->getAttributes(self::class, ArgumentMetadata::IS_INSTANCEOF);

if (!empty($attributes)) {
$attribute = $attributes[0];
\assert($attribute instanceof self);
} else {
$attribute = new self();
Lustmored marked this conversation as resolved.
Show resolved Hide resolved
}

$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;
}
}
38 changes: 38 additions & 0 deletions src/Filesystem/Attribute/UploadedFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/*
* This file is part of the zenstruck/filesystem package.
*
* (c) Kevin Bond <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zenstruck\Filesystem\Attribute;

use Zenstruck\Filesystem\Node\Path\Expression;
use Zenstruck\Filesystem\Node\Path\Namer;

/**
* @author Jakub Caban <[email protected]>
Lustmored marked this conversation as resolved.
Show resolved Hide resolved
*
* @readonly
*/
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
final class UploadedFile extends PendingUploadedFile
{
public string|Namer $namer;

public function __construct(
public string $filesystem,
string|Namer|null $namer = null,
?string $path = null,
?array $constraints = null,
?bool $image = null,
) {
parent::__construct($path, $constraints, $image);

$this->namer = $namer ?? new Expression('{checksum}/{name}{ext}');
kbond marked this conversation as resolved.
Show resolved Hide resolved
}
}
2 changes: 1 addition & 1 deletion src/Filesystem/Glide/GlideTransformUrlGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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"
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -45,6 +46,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;
Expand Down Expand Up @@ -155,6 +158,22 @@ 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([
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),
])
)
;

if (isset($container->getParameter('kernel.bundles')['TwigBundle'])) {
$container->register('.zenstruck_filesystem.doctrine.twig_extension', MappingManagerExtension::class)
->addTag('twig.extension')
Expand Down
21 changes: 21 additions & 0 deletions src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the zenstruck/filesystem package.
*
* (c) Kevin Bond <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zenstruck\Filesystem\Symfony\Exception;

use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;

/**
* @author Jakub Caban <[email protected]>
*/
class IncorrectFileHttpException extends UnprocessableEntityHttpException
{
}
54 changes: 54 additions & 0 deletions src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

/*
* This file is part of the zenstruck/filesystem package.
*
* (c) Kevin Bond <[email protected]>
*
* 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 Zenstruck\Filesystem\Node\File\PendingFile;

/**
* @author Jakub Caban <[email protected]>
*
* @internal
*/
if (\interface_exists(ValueResolverInterface::class)) {
class PendingFileValueResolver implements ValueResolverInterface
{
use PendingFileValueResolverTrait {
resolve as resolveArgument;
}

/**
* @return iterable<PendingFile|array|null>
*/
public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
if (!RequestFilesExtractor::supports($argument)) {
return [];
}

return $this->resolveArgument($request, $argument);
}
}
} else {
class PendingFileValueResolver implements ArgumentValueResolverInterface
{
use PendingFileValueResolverTrait;

public function supports(Request $request, ArgumentMetadata $argument): bool
{
return RequestFilesExtractor::supports($argument);
}
}
}
124 changes: 124 additions & 0 deletions src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

/*
* This file is part of the zenstruck/filesystem package.
*
* (c) Kevin Bond <[email protected]>
*
* 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\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\FilesystemRegistry;
use Zenstruck\Filesystem\Node;
use Zenstruck\Filesystem\Node\File;
use Zenstruck\Filesystem\Node\File\PendingFile;
use Zenstruck\Filesystem\Node\PathGenerator;
use Zenstruck\Filesystem\Symfony\Exception\IncorrectFileHttpException;

/**
* @author Jakub Caban <[email protected]>
*
* @internal
*/
trait PendingFileValueResolverTrait
{
/**
* @param ServiceProviderInterface<mixed> $locator
*/
public function __construct(private ServiceProviderInterface $locator)
{
}

/**
* @return iterable<File|array|null>
*/
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 ($attribute->constraints) {
$errors = $this->validator()->validate(
$files,
$attribute->constraints
);

if (\count($errors)) {
\assert($errors instanceof ConstraintViolationList);

throw new IncorrectFileHttpException((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);
}
}

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);
}
}
Loading