-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(symfony): add value resolver (#95)
* Import value resolver from document-library * Refactor FilesExtractor to handle PendingFile subclasses in supports() * Handle PendingImage in value resolving * Fix Phpstan warnings and run CS fixer * Add missing functional tests * Allow File/Image typehint for value resolver * Simplify PendingFileValueResolver by introducing a common trait * Add simple image injection functional tests * Utilize UploadedFile::forArgument() for a centralized value resolver configuration * Extend UploadedFile to constraints and errorStatus * Add file validation logic to pendingfileValueResolver * Add simple test case for invalid image * Add valueResolver exception tests * CS run * Trailing comma is a no-go in PHP8 :) * Make phpstan happy * Trivial suggestions * Disable logger on tests * Add custom exception for incorrect file * Revert controller loader to annotation for older Sf versions * Hide PHP 8.1+ test under if * Move constraints logic to `forArgument` * UploadedFile::$multiple is no more * Move image to the last position of UploadedFile constructor * Introduce UploadedFile as extender of PendingUploadedFile attribute (non functional yet!) * Handle storing files with #[UploadedFile] attribute * Fix typehint * Update src/Filesystem/Symfony/HttpKernel/RequestFilesExtractor.php Co-authored-by: Kevin Bond <[email protected]> * Move IncorrectFileHttpException * Remove phpstan ignore * Add phpstan-ignore-line and add @readonly to attributes * CS fixer run * Fix tests * code style again * Add pointless docblock to silence phpstan * Apply suggestions from code review Co-authored-by: Kevin Bond <[email protected]> * Mark PendingUploadedFile constructor as consistent for phpstan --------- Co-authored-by: Kevin Bond <[email protected]>
- Loading branch information
Showing
13 changed files
with
991 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
<?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]> | ||
* | ||
* @phpstan-consistent-constructor | ||
* @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, | ||
) { | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
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 static(); | ||
} | ||
|
||
$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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]> | ||
* | ||
* @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}'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
src/Filesystem/Symfony/Exception/IncorrectFileHttpException.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
54
src/Filesystem/Symfony/HttpKernel/PendingFileValueResolver.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
124
src/Filesystem/Symfony/HttpKernel/PendingFileValueResolverTrait.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.