From f6b91689a3ed5685dabaaf266a7b0bb4b40bf225 Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Fri, 29 Nov 2024 15:21:44 +0100 Subject: [PATCH 01/16] [Validator] Add the Video constraint for validating video files --- .../Component/Validator/Constraints/Video.php | 194 ++++++++ .../Validator/Constraints/VideoValidator.php | 223 +++++++++ .../Tests/Constraints/Fixtures/test.mp4 | Bin 0 -> 1556 bytes .../Tests/Constraints/Fixtures/test_16by9.mp4 | Bin 0 -> 1563 bytes .../Tests/Constraints/Fixtures/test_4by3.mp4 | Bin 0 -> 1582 bytes .../Constraints/Fixtures/test_corrupted.mp4 | Bin 0 -> 999 bytes .../Constraints/Fixtures/test_landscape.mp4 | Bin 0 -> 1557 bytes .../Constraints/Fixtures/test_portrait.mp4 | Bin 0 -> 1556 bytes .../Validator/Tests/Constraints/VideoTest.php | 48 ++ .../Tests/Constraints/VideoValidatorTest.php | 470 ++++++++++++++++++ src/Symfony/Component/Validator/composer.json | 3 +- 11 files changed, 937 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Validator/Constraints/Video.php create mode 100644 src/Symfony/Component/Validator/Constraints/VideoValidator.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test.mp4 create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_16by9.mp4 create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_4by3.mp4 create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_corrupted.mp4 create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_landscape.mp4 create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_portrait.mp4 create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/VideoTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php diff --git a/src/Symfony/Component/Validator/Constraints/Video.php b/src/Symfony/Component/Validator/Constraints/Video.php new file mode 100644 index 000000000000..c7610695080e --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/Video.php @@ -0,0 +1,194 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraints\File; + +/** + * @author Kev + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class Video extends File +{ + public const SIZE_NOT_DETECTED_ERROR = '6d55c3f4-e58e-4fe3-91ee-74b492199956'; + public const TOO_WIDE_ERROR = '7f87163d-878f-47f5-99ba-a8eb723a1ab2'; + public const TOO_NARROW_ERROR = '9afbd561-4f90-4a27-be62-1780fc43604a'; + public const TOO_HIGH_ERROR = '7efae81c-4877-47ba-aa65-d01ccb0d4645'; + public const TOO_LOW_ERROR = 'aef0cb6a-c07f-4894-bc08-1781420d7b4c'; + public const TOO_FEW_PIXEL_ERROR = '1b06b97d-ae48-474e-978f-038a74854c43'; + public const TOO_MANY_PIXEL_ERROR = 'ee0804e8-44db-4eac-9775-be91aaf72ce1'; + public const RATIO_TOO_BIG_ERROR = '70cafca6-168f-41c9-8c8c-4e47a52be643'; + public const RATIO_TOO_SMALL_ERROR = '59b8c6ef-bcf2-4ceb-afff-4642ed92f12e'; + public const SQUARE_NOT_ALLOWED_ERROR = '5d41425b-facb-47f7-a55a-de9fbe45cb46'; + public const LANDSCAPE_NOT_ALLOWED_ERROR = '6f895685-7cf2-4d65-b3da-9029c5581d88'; + public const PORTRAIT_NOT_ALLOWED_ERROR = '65608156-77da-4c79-a88c-02ef6d18c782'; + public const CORRUPTED_VIDEO_ERROR = '5d4163f3-648f-4e39-87fd-cc5ea7aad2d1'; + + // Include the mapping from the base class + + protected const ERROR_NAMES = [ + self::NOT_FOUND_ERROR => 'NOT_FOUND_ERROR', + self::NOT_READABLE_ERROR => 'NOT_READABLE_ERROR', + self::EMPTY_ERROR => 'EMPTY_ERROR', + self::TOO_LARGE_ERROR => 'TOO_LARGE_ERROR', + self::INVALID_MIME_TYPE_ERROR => 'INVALID_MIME_TYPE_ERROR', + self::FILENAME_TOO_LONG => 'FILENAME_TOO_LONG', + self::SIZE_NOT_DETECTED_ERROR => 'SIZE_NOT_DETECTED_ERROR', + self::TOO_WIDE_ERROR => 'TOO_WIDE_ERROR', + self::TOO_NARROW_ERROR => 'TOO_NARROW_ERROR', + self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR', + self::TOO_LOW_ERROR => 'TOO_LOW_ERROR', + self::TOO_FEW_PIXEL_ERROR => 'TOO_FEW_PIXEL_ERROR', + self::TOO_MANY_PIXEL_ERROR => 'TOO_MANY_PIXEL_ERROR', + self::RATIO_TOO_BIG_ERROR => 'RATIO_TOO_BIG_ERROR', + self::RATIO_TOO_SMALL_ERROR => 'RATIO_TOO_SMALL_ERROR', + self::SQUARE_NOT_ALLOWED_ERROR => 'SQUARE_NOT_ALLOWED_ERROR', + self::LANDSCAPE_NOT_ALLOWED_ERROR => 'LANDSCAPE_NOT_ALLOWED_ERROR', + self::PORTRAIT_NOT_ALLOWED_ERROR => 'PORTRAIT_NOT_ALLOWED_ERROR', + self::CORRUPTED_VIDEO_ERROR => 'CORRUPTED_VIDEO_ERROR', + ]; + + /** + * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + */ + protected static $errorNames = self::ERROR_NAMES; + + public array|string $mimeTypes = 'video/*'; + public ?int $minWidth = null; + public ?int $maxWidth = null; + public ?int $maxHeight = null; + public ?int $minHeight = null; + public int|float|null $maxRatio = null; + public int|float|null $minRatio = null; + public int|float|null $minPixels = null; + public int|float|null $maxPixels = null; + public ?bool $allowSquare = true; + public ?bool $allowLandscape = true; + public ?bool $allowPortrait = true; + + // The constant for a wrong MIME type is taken from the parent class. + public string $mimeTypesMessage = 'This file is not a valid video.'; + public string $sizeNotDetectedMessage = 'The size of the video could not be detected.'; + public string $maxWidthMessage = 'The video width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.'; + public string $minWidthMessage = 'The video width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.'; + public string $maxHeightMessage = 'The video height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.'; + public string $minHeightMessage = 'The video height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.'; + public string $minPixelsMessage = 'The video has too few pixels ({{ pixels }} pixels). Minimum amount expected is {{ min_pixels }} pixels.'; + public string $maxPixelsMessage = 'The video has too many pixels ({{ pixels }} pixels). Maximum amount expected is {{ max_pixels }} pixels.'; + public string $maxRatioMessage = 'The video ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.'; + public string $minRatioMessage = 'The video ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.'; + public string $allowSquareMessage = 'The video is square ({{ width }}x{{ height }}px). Square videos are not allowed.'; + public string $allowLandscapeMessage = 'The video is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented videos are not allowed.'; + public string $allowPortraitMessage = 'The video is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented videos are not allowed.'; + public string $corruptedMessage = 'The video file is corrupted.'; + + public function __construct( + ?array $options = null, + int|string|null $maxSize = null, + ?bool $binaryFormat = null, + array|string|null $mimeTypes = null, + ?int $filenameMaxLength = null, + ?int $minWidth = null, + ?int $maxWidth = null, + ?int $maxHeight = null, + ?int $minHeight = null, + int|float|null $maxRatio = null, + int|float|null $minRatio = null, + int|float|null $minPixels = null, + int|float|null $maxPixels = null, + ?bool $allowSquare = null, + ?bool $allowLandscape = null, + ?bool $allowPortrait = null, + ?string $notFoundMessage = null, + ?string $notReadableMessage = null, + ?string $maxSizeMessage = null, + ?string $mimeTypesMessage = null, + ?string $disallowEmptyMessage = null, + ?string $filenameTooLongMessage = null, + ?string $uploadIniSizeErrorMessage = null, + ?string $uploadFormSizeErrorMessage = null, + ?string $uploadPartialErrorMessage = null, + ?string $uploadNoFileErrorMessage = null, + ?string $uploadNoTmpDirErrorMessage = null, + ?string $uploadCantWriteErrorMessage = null, + ?string $uploadExtensionErrorMessage = null, + ?string $uploadErrorMessage = null, + ?string $sizeNotDetectedMessage = null, + ?string $maxWidthMessage = null, + ?string $minWidthMessage = null, + ?string $maxHeightMessage = null, + ?string $minHeightMessage = null, + ?string $minPixelsMessage = null, + ?string $maxPixelsMessage = null, + ?string $maxRatioMessage = null, + ?string $minRatioMessage = null, + ?string $allowSquareMessage = null, + ?string $allowLandscapeMessage = null, + ?string $allowPortraitMessage = null, + ?string $corruptedMessage = null, + ?array $groups = null, + mixed $payload = null + ) { + parent::__construct( + $options, + $maxSize, + $binaryFormat, + $mimeTypes, + $filenameMaxLength, + $notFoundMessage, + $notReadableMessage, + $maxSizeMessage, + $mimeTypesMessage, + $disallowEmptyMessage, + $filenameTooLongMessage, + $uploadIniSizeErrorMessage, + $uploadFormSizeErrorMessage, + $uploadPartialErrorMessage, + $uploadNoFileErrorMessage, + $uploadNoTmpDirErrorMessage, + $uploadCantWriteErrorMessage, + $uploadExtensionErrorMessage, + $uploadErrorMessage, + $groups, + $payload + ); + + $this->minWidth = $minWidth ?? $this->minWidth; + $this->maxWidth = $maxWidth ?? $this->maxWidth; + $this->maxHeight = $maxHeight ?? $this->maxHeight; + $this->minHeight = $minHeight ?? $this->minHeight; + $this->maxRatio = $maxRatio ?? $this->maxRatio; + $this->minRatio = $minRatio ?? $this->minRatio; + $this->minPixels = $minPixels ?? $this->minPixels; + $this->maxPixels = $maxPixels ?? $this->maxPixels; + $this->allowSquare = $allowSquare ?? $this->allowSquare; + $this->allowLandscape = $allowLandscape ?? $this->allowLandscape; + $this->allowPortrait = $allowPortrait ?? $this->allowPortrait; + $this->sizeNotDetectedMessage = $sizeNotDetectedMessage ?? $this->sizeNotDetectedMessage; + $this->maxWidthMessage = $maxWidthMessage ?? $this->maxWidthMessage; + $this->minWidthMessage = $minWidthMessage ?? $this->minWidthMessage; + $this->maxHeightMessage = $maxHeightMessage ?? $this->maxHeightMessage; + $this->minHeightMessage = $minHeightMessage ?? $this->minHeightMessage; + $this->minPixelsMessage = $minPixelsMessage ?? $this->minPixelsMessage; + $this->maxPixelsMessage = $maxPixelsMessage ?? $this->maxPixelsMessage; + $this->maxRatioMessage = $maxRatioMessage ?? $this->maxRatioMessage; + $this->minRatioMessage = $minRatioMessage ?? $this->minRatioMessage; + $this->allowSquareMessage = $allowSquareMessage ?? $this->allowSquareMessage; + $this->allowLandscapeMessage = $allowLandscapeMessage ?? $this->allowLandscapeMessage; + $this->allowPortraitMessage = $allowPortraitMessage ?? $this->allowPortraitMessage; + $this->corruptedMessage = $corruptedMessage ?? $this->corruptedMessage; + + if (!\in_array('video/*', (array) $this->mimeTypes, true) && !\array_key_exists('mimeTypesMessage', $options ?? []) && null === $mimeTypesMessage) { + $this->mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.'; + } + } +} diff --git a/src/Symfony/Component/Validator/Constraints/VideoValidator.php b/src/Symfony/Component/Validator/Constraints/VideoValidator.php new file mode 100644 index 000000000000..830706742903 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/VideoValidator.php @@ -0,0 +1,223 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; + +/** + * @author Kev + */ +class VideoValidator extends FileValidator +{ + /** + * @return void + */ + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof Video) { + throw new UnexpectedTypeException($constraint, Video::class); + } + + if (!\class_exists('FFMpeg\FFProbe')) { + throw new \LogicException('The "php-ffmpeg/php-ffmpeg" library must be installed to use the Video constraint. Try running "composer require php-ffmpeg/php-ffmpeg".'); + } + + $violations = \count($this->context->getViolations()); + + parent::validate($value, $constraint); + + $failed = \count($this->context->getViolations()) !== $violations; + + if ($failed || null === $value || '' === $value) { + return; + } + + if (null === $constraint->minWidth && null === $constraint->maxWidth + && null === $constraint->minHeight && null === $constraint->maxHeight + && null === $constraint->minPixels && null === $constraint->maxPixels + && null === $constraint->minRatio && null === $constraint->maxRatio + && $constraint->allowSquare && $constraint->allowLandscape && $constraint->allowPortrait) { + return; + } + + try { + $ffprobe = \FFMpeg\FFProbe::create(); + $stream = $ffprobe->streams($value)->videos()->first(); + if (!$stream) { + throw new \Exception('Unexpected FFMpeg error'); + } + $dimensions = $stream->getDimensions(); + } catch (\Exception $e) { + $this->context->buildViolation($constraint->corruptedMessage) + ->setParameter('{{ message }}', $e->getMessage()) + ->setCode(Video::CORRUPTED_VIDEO_ERROR) + ->addViolation(); + + return; + } + + $width = $dimensions->getWidth(); + $height = $dimensions->getHeight(); + + if ($constraint->minWidth) { + if (!ctype_digit((string) $constraint->minWidth)) { + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum width.', $constraint->minWidth)); + } + + if ($width < $constraint->minWidth) { + $this->context->buildViolation($constraint->minWidthMessage) + ->setParameter('{{ width }}', $width) + ->setParameter('{{ min_width }}', $constraint->minWidth) + ->setCode(Video::TOO_NARROW_ERROR) + ->addViolation(); + + return; + } + } + + if ($constraint->maxWidth) { + if (!ctype_digit((string) $constraint->maxWidth)) { + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum width.', $constraint->maxWidth)); + } + + if ($width > $constraint->maxWidth) { + $this->context->buildViolation($constraint->maxWidthMessage) + ->setParameter('{{ width }}', $width) + ->setParameter('{{ max_width }}', $constraint->maxWidth) + ->setCode(Video::TOO_WIDE_ERROR) + ->addViolation(); + + return; + } + } + + if ($constraint->minHeight) { + if (!ctype_digit((string) $constraint->minHeight)) { + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum height.', $constraint->minHeight)); + } + + if ($height < $constraint->minHeight) { + $this->context->buildViolation($constraint->minHeightMessage) + ->setParameter('{{ height }}', $height) + ->setParameter('{{ min_height }}', $constraint->minHeight) + ->setCode(Video::TOO_LOW_ERROR) + ->addViolation(); + + return; + } + } + + if ($constraint->maxHeight) { + if (!ctype_digit((string) $constraint->maxHeight)) { + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum height.', $constraint->maxHeight)); + } + + if ($height > $constraint->maxHeight) { + $this->context->buildViolation($constraint->maxHeightMessage) + ->setParameter('{{ height }}', $height) + ->setParameter('{{ max_height }}', $constraint->maxHeight) + ->setCode(Video::TOO_HIGH_ERROR) + ->addViolation(); + } + } + + $pixels = $width * $height; + + if (null !== $constraint->minPixels) { + if (!ctype_digit((string) $constraint->minPixels)) { + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum amount of pixels.', $constraint->minPixels)); + } + + if ($pixels < $constraint->minPixels) { + $this->context->buildViolation($constraint->minPixelsMessage) + ->setParameter('{{ pixels }}', $pixels) + ->setParameter('{{ min_pixels }}', $constraint->minPixels) + ->setParameter('{{ height }}', $height) + ->setParameter('{{ width }}', $width) + ->setCode(Video::TOO_FEW_PIXEL_ERROR) + ->addViolation(); + } + } + + if (null !== $constraint->maxPixels) { + if (!ctype_digit((string) $constraint->maxPixels)) { + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum amount of pixels.', $constraint->maxPixels)); + } + + if ($pixels > $constraint->maxPixels) { + $this->context->buildViolation($constraint->maxPixelsMessage) + ->setParameter('{{ pixels }}', $pixels) + ->setParameter('{{ max_pixels }}', $constraint->maxPixels) + ->setParameter('{{ height }}', $height) + ->setParameter('{{ width }}', $width) + ->setCode(Video::TOO_MANY_PIXEL_ERROR) + ->addViolation(); + } + } + + $ratio = round($dimensions->getRatio()->getValue(), 2); + + if (null !== $constraint->minRatio) { + if (!is_numeric((string) $constraint->minRatio)) { + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum ratio.', $constraint->minRatio)); + } + + if ($ratio < round($constraint->minRatio, 2)) { + $this->context->buildViolation($constraint->minRatioMessage) + ->setParameter('{{ ratio }}', $ratio) + ->setParameter('{{ min_ratio }}', round($constraint->minRatio, 2)) + ->setCode(Video::RATIO_TOO_SMALL_ERROR) + ->addViolation(); + } + } + + if (null !== $constraint->maxRatio) { + if (!is_numeric((string) $constraint->maxRatio)) { + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum ratio.', $constraint->maxRatio)); + } + + if ($ratio > round($constraint->maxRatio, 2)) { + $this->context->buildViolation($constraint->maxRatioMessage) + ->setParameter('{{ ratio }}', $ratio) + ->setParameter('{{ max_ratio }}', round($constraint->maxRatio, 2)) + ->setCode(Video::RATIO_TOO_BIG_ERROR) + ->addViolation(); + } + } + + if (!$constraint->allowSquare && $width == $height) { + $this->context->buildViolation($constraint->allowSquareMessage) + ->setParameter('{{ width }}', $width) + ->setParameter('{{ height }}', $height) + ->setCode(Video::SQUARE_NOT_ALLOWED_ERROR) + ->addViolation(); + } + + if (!$constraint->allowLandscape && $width > $height) { + $this->context->buildViolation($constraint->allowLandscapeMessage) + ->setParameter('{{ width }}', $width) + ->setParameter('{{ height }}', $height) + ->setCode(Video::LANDSCAPE_NOT_ALLOWED_ERROR) + ->addViolation(); + } + + if (!$constraint->allowPortrait && $width < $height) { + $this->context->buildViolation($constraint->allowPortraitMessage) + ->setParameter('{{ width }}', $width) + ->setParameter('{{ height }}', $height) + ->setCode(Video::PORTRAIT_NOT_ALLOWED_ERROR) + ->addViolation(); + } + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test.mp4 b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..d251e5def93d168fcf46d04c43f060b16b32348d GIT binary patch literal 1556 zcmZux&5ImG6tCGOF$511R!v|dC9Ws4v)wbhlg&WGhBY_`1uy0xLQ~yUJ>5)y)Kt~X z&g39Dc+jH=Ui=3X6ugNC!9#A6qhP>`2oez@0l`Hi*7U6I45(k`n?+vTx{L>&0ss3d4} zsiM4!2Z7)9x*iv$$fs5X-lX02CcfY1g$hzN;dT^3y2`bkA{1+AWZ*YF1YzMtHc>Jp zL?02M5oKQmeuu}YsR|Jy)aTBqJkKno{mH(J9fWvT1Rjnc zO&)WAp$@AgvC0Lz+@%Hqgp*g*K&04&Cpw#FF^;lSc)T=qia0BTqhn;HQzjR1fj6)k;?k1 zi(sp?D#L!IVVx^ALI-LJ?4Y&hEyS?MN;unCl`0;gMvqQu1U1wsnso-IF;Zlw0es}cS28W zpgy%fIPrX%w1x+C9i14!_?{coIoDSl7ig6G6rIZR)Vx5)8z$OP(pm6Lm0LX8nt-st zt+5*47BUmC@Is!h{Q&9%_RgI;ZabB^sU1%t@`?8xMNE;qKUi*m@{TW6O8A)l}59|@dj6?3jxL)kru^b!5vW3E40 zFV1Gzbbxx;_=}yN{lYHw!9V`&wTBOn&)r6==<=*Lp84p)4-IxU{o~g=z1L^Q$As{l z7FL5(7Wy`DX%|g?A!Up{cG0?e01%gc2PAKzu9yVSGJeT7V7oOpt=(PNp^K}zuJ-pJ zMgx1u@$F!ZzZ^ElzxQYNTC^>smjFY|n=G9Kq_q_iA D5(Q-P literal 0 HcmV?d00001 diff --git a/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_16by9.mp4 b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_16by9.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..deb4587f8e6398b40fd93aa1a56e29210ae0d400 GIT binary patch literal 1563 zcmZuxPiS047@tkBsEFneq+kjoRuJrN-rG$!ap6Hq8X82Q7kd!lF>hwxzQ?}zW->Fo z+3m$11bgt}#fx`QC>})cR1gVx6Hn5jAgNF)f*$lxLAw6tZPKi!kYn~iu_>a~y%@?>hHgzk1Jm;=9g=85U_)1P*Jy>;)-?pGgOxJ~c8 z^!p#2Fj#%_%FgBH8ohXR8gxXferuEqsR~xLl_a#h(L;=7mvWgWGE2CiC=>_93?9ft1nH*C)On^41wiCEwkBx}JRsbh#w9Lm+#6DsO zkJzxPFQc$ad1h3}Vz`FX8Cev$1=<{M3hn^-UKs^Qfr)FXl2JRn5Qa2m*2UTma_uc< z0rqsP($q>9tx=a5IQTe)QUjLZ5TD3wk#nSSEC^_2>MM9&GUwCCD<@6CU<7$m3=Nya zT$P$RoH;t=49hEofT3aDCN->-7QIShJps-|)COjPXJhZuQxp6CZ%P9 zuKKEQh}xZ2K=*ttQP9F`+S5L6&Dl8Wz6ODHQlo{r#)$A7FhRs(dzikTCAp6GyCQGA zN0z4Feg5O0KOcN><&%xYA720Vrn>j{;G1_330ZuhRB8mfq8w!ce2y0HLw}z%i2t3? ze-03zI&Ym=J`OG58`?oL22j4^2KAkbbB0IJ{N1PM)IE>A_qoHn4ycEs?t;oXS zyR8Wkj^fs_8n;T3GiYcjPS?H#@fLaOp!REKqA)eW=D)~e?DLS6w=!?GE6B2 zSRR#i(D}5_iymGzFdE#w*q(1@M7&_IM!P=1c#l1ny}*=1j7LvUdUf z^7Ma;nfzowJDuXtL)7iYU*zfOAvxCv|M25i|GE6I@oZy>qFbciF5bQL#n%mTCj0G| zgWXHh2M>IdCC#h`e;?}x)-vAiQKO%~PKaORtabGe0PBp9)t1LAsEvC7@|n)BW$3Qe z8nW>Hkq$DhX0qBg08WO&G4QubS#z&`aeh!3u3=jHUOu8`K6?!>f98}Acn*K+K8R0% Qyun84`etjR+X{o=U%5;q{YGt)h@J6Q)BHms|AP*5Z}h}u?nRZln5A2n4q zUlC*v9u!Xs3Zj32f=EQcQx3v{2T$U~fDj}gk$@t}VNZ^~>Y2@sYaUbeUcHZBy^rc4 zgwV*0Wul9WkOg8nR)Y;fH{r@2?5)!j57oN<(boy$>+cB{&Ds9YkS{)y#5Ql z_T-;`jT)^DTBTv31a(_Ys+vyAp_@EtG1h`%uLg(ui!bdyU)`V=F73dRix7sLLXJ}r z8QO51X0_ooTqt8>WV>D;4u`ct!bOp?yjG}A-QH4*O_su^kR~Z|-KHT6Sm?P_iO6eG zE`qcOyPn&2+74woOUGJx&ZyaTMy}hWnebvUqPibII!~n@BNV%@xaZa!1Yx6DG7{Xk zh%O?0#qy5u+!hUERbsD{p7e_-(!zKf)Wiw_7AGx=E{k#Sk99gtLZot}aA>Z|E5amW#-@?vMyQm*3Hl)I zD>nASB9qME9HK);u_Ols92K)Zkz$$9=vCm$G311M4akf$X^RN_AYs~Gk|Y4(-`mJg0`&)RPvs{rN57W1^B>Q;3 zb@A>y#Q$gcZSns8+Bes~I(Pe{J3l{m`ryOr?hg+wzViAf7k+x)8r9GRl632rnHRz<4uDVGF<8E%PHxtj5W!9p{Bvi5DLx5>qWGOil) zRFx7o`;i{WmS3_T?H#^53dOM|Qgolkp>Pi?Clufqm|to^~hO6gRtJf%&L zLO#$&9}UF%d~5%wz%9*VPBo`c_8vsLEdAeOsz2UWCKDXmK;5a_ArDM`BWF9{AAI`s z-(O!pti+W!_t7q5x5|C*J^EvXoQZGUIM{o3a(HM_9+6B-@G90i))JoctkKIWgxEz^ zv?+%GvCIfrZCiO2wS`sy*-Yox5^R@C3t6}^vq2XYQ(funKo0ucFz{D1QF3d3Vt!B< ou3}Q!Uf!i@GJOr_zA%LaJd6K%3t|%>FR($hxm7#gs=1EyFE)*DwEzGB literal 0 HcmV?d00001 diff --git a/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_corrupted.mp4 b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_corrupted.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..a03d940035dad9a62831a6b566cdd4c36a0a1ebd GIT binary patch literal 999 zcmZuw&ubG=5S~r2l;RuFLA<==y)>b_*=6^2(|W4V zLl0hx;KhGHK|xP~;6dooKfr<)5u_rh2f?1Y&g^DQj2-s9`QCir&g{+WBccT;i+?DtUL{6up`%$&o_{~r#9l6*$Lq&8`Yk&sXC|DB~7AR zltKk~DoQ)RDa3}%C~vIM-Z-MqkztOQe)k538*t|!zOU31wjU$hsJ|&DzefGG_(0f_ zG@5k!gD!%6=)_5r#~~%aqDZ)+@Fi);h(EjO+bqG_QlQ#4;&*`x(t(iX6@6mp$tH8A z5e+Paw8;KfB=PZHh$1|!V%Dbi3+AFvaA6(&{*y~TcNcc{uvIWiTW)KA`K1Oky>Fin ztn1P7F{Q|PJn+y{8CvT>db*kJu9~Wv z*_j-~gNWCFc<~=l(1TtCK~Tt14;};qUR02X_yvMFXePhvnaPf89@F(+y^r6k_g>Wy zLTGAdDmStuq)8md7qAKUOSS70LLN!A5U_nyCd`6=aQ4i6{@!=je*WyI+x<`8x%4f) z{mgH_PlN6*ZBZ_@pnkVQb;s*^bT5g!jCEn?x8bn!;!D?_Z|%}6*N@kdX%%6@zAGQq+y39 zA}%B!hJMfMd6ZSGm>Cgz(@xKu`hJI&BFw~;nsE$ipDHs$DAq_Pq2KlpgoT#*R3wo@ z^brwhRt-excPY=bELnt5pIR-7A~%o@rUwbP5csGJJsg20H)JKkz`x}CG-bv{$_#Vm zJQe^W70EO;!iKxlW*Pwwr;u{UGHjv~ohfpTqpTDjt#qA&=Owf574W=USh{e42b z^#*xte(&!O?}@jA=|AU+U%!6sO!v!wZxXVxRZ2O5Q&CQ`1bmG)?!bPBG>H41(0?{i zpV}Xucs@*O;sHH}P7Gmu)eh^N8*7eBG?)7noyzmjyg*;-FNnk%?6yT=@MvoS!VTzUSyJd)!s4FeN_PCZd!Bt(C53Q44S^A*5H=@e4Z42p z3(3N^o!}n0V34z)AM1iHbd-205Gk^5d=k)%o4K4&qV8 zi~@IQ+_#BayX^Ag86oZ?TgKKCfVlEIbn-R|<4ypr;#YhVwi^pm^Q)yDIxZKw+TVj5 zj}yzlUnxb+o%!kYDPgjjDdigZfLi(DDqMWuN(cB9{>~kUOHN*3lXUN(z29y7o_8N} C-)0~H literal 0 HcmV?d00001 diff --git a/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_portrait.mp4 b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_portrait.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..d4528785dea2dbaea6fe5fc098a2eed42ea0c574 GIT binary patch literal 1556 zcmZuxO^72!6s}}&5%J(*ltD%)aTgD6GTq5cW(+jUI=h&If*|fegtoe>I^CqZt5Q|T zBzq7KD|q$d#fv9V(1TtCL0DlA9z1$*!Gk{_D}w7ma8Db*>LfekxQ}$bSMPoAy?XCe zg%CnhH`lpUB_U1XIlc~?^03sqAtB`9)JO^2_oZMC{L^y}EfydDc;)x6f4epK^8JfH z&|A;^d1uz??a~(I%19day3}-oUO@Lm(qpU#!=Meo&daY{d9k%iFI_zXQb-QNk<#-* zrjB-kpxf#Moe;{*Io;pcIX*saPjewv!K$`0!yTWcow>3AMroI;%JwN|3FA>njZC91 z6*4IlA4Oq5=m(Tlte9IF1+#8Hn1x}NmNLrZjM_;8X`gC4hbz|Dh$w6aaKb{%d?rQg zBZlyZ4XcJS3VW1iMwKjvYe=1uMUh)b2eSjg9RxluqX0)>;+m>t)Cn(!Ax)Wev9_aJ zd&DxpSjQ?&t#r{Yb(w*Kk5ec$Vi`8^iOv=|$5GabfL5kX!Sj+ie~i3x(i99J=#ye% z*gWQ{)XZVe(IID8ULgd4hIveCSSc-fmBe}uITuj}G80VuB9b`Cne|C>BkQt`Wj@SY z0-MsZiib+Wx>Rboj^rG%qt0Hi65}$j0JdCKl21@$;7@6oZ>W(rgSX-F{c644@Q9m~ zmI=D*tHLJgblU+P`&y!)joUQVK5os~Eb2WEg>_P+g}KIva2+r~#A0Ji-_Me~jr+SL z-@Q&AU)=lWvwQNL@Xn2ozWwmopI#j*-06+2QmH90MLEp`_$k`B4f}1O=C{&DyV(iNe$p7XKR8vgcR*>m0C36w+fF5)(=xY);EMXr0Of zLFdKNwk>cCma4jpM<=GB*aO?RU|mu`{>D1H9*B?ex&K1~_jEeuTGJ#0i2n2Rf5cLM zvTiLF*z64lXb5-t{3K z)68mcpT>WixU{P-Kk+6Z{v%t~)f0fY#vOWj8-;N%fY$zNz6sm4rK$Pd$_^b@OI?i* zASV;y82Ia@thqNoeR@imtYKRFM&74ZzPt(-K5@zidOV literal 0 HcmV?d00001 diff --git a/src/Symfony/Component/Validator/Tests/Constraints/VideoTest.php b/src/Symfony/Component/Validator/Tests/Constraints/VideoTest.php new file mode 100644 index 000000000000..5f71a58363a9 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/VideoTest.php @@ -0,0 +1,48 @@ +loadClassMetadata($metadata)); + + [$aConstraint] = $metadata->properties['a']->getConstraints(); + self::assertNull($aConstraint->minWidth); + self::assertNull($aConstraint->maxWidth); + self::assertNull($aConstraint->minHeight); + self::assertNull($aConstraint->maxHeight); + + [$bConstraint] = $metadata->properties['b']->getConstraints(); + self::assertSame(50, $bConstraint->minWidth); + self::assertSame(200, $bConstraint->maxWidth); + self::assertSame(50, $bConstraint->minHeight); + self::assertSame(200, $bConstraint->maxHeight); + self::assertSame(['Default', 'VideoDummy'], $bConstraint->groups); + + [$cConstraint] = $metadata->properties['c']->getConstraints(); + self::assertSame(100000, $cConstraint->maxSize); + self::assertSame(['my_group'], $cConstraint->groups); + self::assertSame('some attached data', $cConstraint->payload); + } +} + +class VideoDummy +{ + #[Video] + private $a; + + #[Video(minWidth: 50, maxWidth: 200, minHeight: 50, maxHeight: 200)] + private $b; + + #[Video(maxSize: '100K', groups: ['my_group'], payload: 'some attached data')] + private $c; +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php new file mode 100644 index 000000000000..ed7869408c29 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php @@ -0,0 +1,470 @@ +video = __DIR__.'/Fixtures/test.mp4'; + $this->videoLandscape = __DIR__.'/Fixtures/test_landscape.mp4'; + $this->videoPortrait = __DIR__.'/Fixtures/test_portrait.mp4'; + $this->video4By3 = __DIR__.'/Fixtures/test_4by3.mp4'; + $this->video16By9 = __DIR__.'/Fixtures/test_16by9.mp4'; + $this->videoCorrupted = __DIR__.'/Fixtures/test_corrupted.mp4'; + $this->notAnVideo = __DIR__.'/Fixtures/ccc.txt'; + } + + public function testNullIsValid() + { + $this->validator->validate(null, new Video()); + + $this->assertNoViolation(); + } + + public function testEmptyStringIsValid() + { + $this->validator->validate('', new Video()); + + $this->assertNoViolation(); + } + + public function testValidVideo() + { + $this->validator->validate($this->video, new Video()); + + $this->assertNoViolation(); + } + + /** + * Checks that the logic from FileValidator still works. + * + * @dataProvider provideConstraintsWithNotFoundMessage + */ + public function testFileNotFound(Video $constraint) + { + $this->validator->validate('foobar', $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ file }}', '"foobar"') + ->setCode(Video::NOT_FOUND_ERROR) + ->assertRaised(); + } + + public static function provideConstraintsWithNotFoundMessage(): iterable + { + yield 'Doctrine style' => [new Video([ + 'notFoundMessage' => 'myMessage', + ])]; + yield 'Named arguments' => [ + new Video(notFoundMessage: 'myMessage'), + ]; + } + + public function testValidSize(): void + { + $constraint = new Video([ + 'minWidth' => 1, + 'maxWidth' => 2, + 'minHeight' => 1, + 'maxHeight' => 2, + ]); + + $this->validator->validate($this->video, $constraint); + + $this->assertNoViolation(); + } + + /** + * @dataProvider provideMinWidthConstraints + */ + public function testWidthTooSmall(Video $constraint) + { + $this->validator->validate($this->video, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', '2') + ->setParameter('{{ min_width }}', '3') + ->setCode(Video::TOO_NARROW_ERROR) + ->assertRaised(); + } + + public static function provideMinWidthConstraints(): iterable + { + yield 'Doctrine style' => [new Video([ + 'minWidth' => 3, + 'minWidthMessage' => 'myMessage', + ])]; + yield 'Named arguments' => [ + new Video(minWidth: 3, minWidthMessage: 'myMessage'), + ]; + } + + /** + * @dataProvider provideMaxWidthConstraints + */ + public function testWidthTooBig(Video $constraint) + { + $this->validator->validate($this->video, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', '2') + ->setParameter('{{ max_width }}', '1') + ->setCode(Video::TOO_WIDE_ERROR) + ->assertRaised(); + } + + public static function provideMaxWidthConstraints(): iterable + { + yield 'Doctrine style' => [new Video([ + 'maxWidth' => 1, + 'maxWidthMessage' => 'myMessage', + ])]; + yield 'Named arguments' => [ + new Video(maxWidth: 1, maxWidthMessage: 'myMessage'), + ]; + } + + /** + * @dataProvider provideMinHeightConstraints + */ + public function testHeightTooSmall(Video $constraint) + { + $this->validator->validate($this->video, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ height }}', '2') + ->setParameter('{{ min_height }}', '3') + ->setCode(Video::TOO_LOW_ERROR) + ->assertRaised(); + } + + public static function provideMinHeightConstraints(): iterable + { + yield 'Doctrine style' => [new Video([ + 'minHeight' => 3, + 'minHeightMessage' => 'myMessage', + ])]; + yield 'Named arguments' => [ + new Video(minHeight: 3, minHeightMessage: 'myMessage'), + ]; + } + + /** + * @dataProvider provideMaxHeightConstraints + */ + public function testHeightTooBig(Video $constraint) + { + $this->validator->validate($this->video, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ height }}', '2') + ->setParameter('{{ max_height }}', '1') + ->setCode(Video::TOO_HIGH_ERROR) + ->assertRaised(); + } + + public static function provideMaxHeightConstraints(): iterable + { + yield 'Doctrine style' => [new Video([ + 'maxHeight' => 1, + 'maxHeightMessage' => 'myMessage', + ])]; + yield 'Named arguments' => [ + new Video(maxHeight: 1, maxHeightMessage: 'myMessage'), + ]; + } + + /** + * @dataProvider provideMinPixelsConstraints + */ + public function testPixelsTooFew(Video $constraint) + { + $this->validator->validate($this->video, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ pixels }}', '4') + ->setParameter('{{ min_pixels }}', '5') + ->setParameter('{{ height }}', '2') + ->setParameter('{{ width }}', '2') + ->setCode(Video::TOO_FEW_PIXEL_ERROR) + ->assertRaised(); + } + + public static function provideMinPixelsConstraints(): iterable + { + yield 'Doctrine style' => [new Video([ + 'minPixels' => 5, + 'minPixelsMessage' => 'myMessage', + ])]; + yield 'Named arguments' => [ + new Video(minPixels: 5, minPixelsMessage: 'myMessage'), + ]; + } + + /** + * @dataProvider provideMaxPixelsConstraints + */ + public function testPixelsTooMany(Video $constraint) + { + $this->validator->validate($this->video, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ pixels }}', '4') + ->setParameter('{{ max_pixels }}', '3') + ->setParameter('{{ height }}', '2') + ->setParameter('{{ width }}', '2') + ->setCode(Video::TOO_MANY_PIXEL_ERROR) + ->assertRaised(); + } + + public static function provideMaxPixelsConstraints(): iterable + { + yield 'Doctrine style' => [new Video([ + 'maxPixels' => 3, + 'maxPixelsMessage' => 'myMessage', + ])]; + yield 'Named arguments' => [ + new Video(maxPixels: 3, maxPixelsMessage: 'myMessage'), + ]; + } + + /** + * @dataProvider provideMinRatioConstraints + */ + public function testRatioTooSmall(Video $constraint) + { + $this->validator->validate($this->video, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ ratio }}', 1) + ->setParameter('{{ min_ratio }}', 2) + ->setCode(Video::RATIO_TOO_SMALL_ERROR) + ->assertRaised(); + } + + public static function provideMinRatioConstraints(): iterable + { + yield 'Doctrine style' => [new Video([ + 'minRatio' => 2, + 'minRatioMessage' => 'myMessage', + ])]; + yield 'Named arguments' => [ + new Video(minRatio: 2, minRatioMessage: 'myMessage'), + ]; + } + + /** + * @dataProvider provideMaxRatioConstraints + */ + public function testRatioTooBig(Video $constraint) + { + $this->validator->validate($this->video, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ ratio }}', 1) + ->setParameter('{{ max_ratio }}', 0.5) + ->setCode(Video::RATIO_TOO_BIG_ERROR) + ->assertRaised(); + } + + public static function provideMaxRatioConstraints(): iterable + { + yield 'Doctrine style' => [new Video([ + 'maxRatio' => 0.5, + 'maxRatioMessage' => 'myMessage', + ])]; + yield 'Named arguments' => [ + new Video(maxRatio: 0.5, maxRatioMessage: 'myMessage'), + ]; + } + + public function testMaxRatioUsesTwoDecimalsOnly() + { + $constraint = new Video([ + 'maxRatio' => 1.33, + ]); + + $this->validator->validate($this->video4By3, $constraint); + + $this->assertNoViolation(); + } + + public function testMinRatioUsesInputMoreDecimals() + { + $constraint = new Video([ + 'minRatio' => 4 / 3, + ]); + + $this->validator->validate($this->video4By3, $constraint); + + $this->assertNoViolation(); + } + + public function testMaxRatioUsesInputMoreDecimals() + { + $constraint = new Video([ + 'maxRatio' => 16 / 9, + ]); + + $this->validator->validate($this->video16By9, $constraint); + + $this->assertNoViolation(); + } + + /** + * @dataProvider provideAllowSquareConstraints + */ + public function testSquareNotAllowed(Video $constraint) + { + $this->validator->validate($this->video, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', 2) + ->setParameter('{{ height }}', 2) + ->setCode(Video::SQUARE_NOT_ALLOWED_ERROR) + ->assertRaised(); + } + + public static function provideAllowSquareConstraints(): iterable + { + yield 'Doctrine style' => [new Video([ + 'allowSquare' => false, + 'allowSquareMessage' => 'myMessage', + ])]; + yield 'Named arguments' => [ + new Video(allowSquare: false, allowSquareMessage: 'myMessage'), + ]; + } + + /** + * @dataProvider provideAllowLandscapeConstraints + */ + public function testLandscapeNotAllowed(Video $constraint) + { + $this->validator->validate($this->videoLandscape, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', 2) + ->setParameter('{{ height }}', 1) + ->setCode(Video::LANDSCAPE_NOT_ALLOWED_ERROR) + ->assertRaised(); + } + + public static function provideAllowLandscapeConstraints(): iterable + { + yield 'Doctrine style' => [new Video([ + 'allowLandscape' => false, + 'allowLandscapeMessage' => 'myMessage', + ])]; + yield 'Named arguments' => [ + new Video(allowLandscape: false, allowLandscapeMessage: 'myMessage'), + ]; + } + + /** + * @dataProvider provideAllowPortraitConstraints + */ + public function testPortraitNotAllowed(Video $constraint) + { + $this->validator->validate($this->videoPortrait, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', 1) + ->setParameter('{{ height }}', 2) + ->setCode(Video::PORTRAIT_NOT_ALLOWED_ERROR) + ->assertRaised(); + } + + public static function provideAllowPortraitConstraints(): iterable + { + yield 'Doctrine style' => [new Video([ + 'allowPortrait' => false, + 'allowPortraitMessage' => 'myMessage', + ])]; + yield 'Named arguments' => [ + new Video(allowPortrait: false, allowPortraitMessage: 'myMessage'), + ]; + } + + public function testCorrupted() + { + $constraint = new Video([ + 'maxRatio' => 1, + ]); + + $this->validator->validate($this->videoCorrupted, $constraint); + + $this->buildViolation('The video file is corrupted.') + ->setParameter('{{ message }}', \sprintf('Unable to probe %s', $this->videoCorrupted)) + ->setCode(Video::CORRUPTED_VIDEO_ERROR) + ->assertRaised(); + } + + public function testInvalidMimeType() + { + $this->validator->validate($this->notAnVideo, $constraint = new Video()); + + $this->assertSame('video/*', $constraint->mimeTypes); + + $this->buildViolation('This file is not a valid video.') + ->setParameter('{{ file }}', \sprintf('"%s"', $this->notAnVideo)) + ->setParameter('{{ type }}', '"text/plain"') + ->setParameter('{{ types }}', '"video/*"') + ->setParameter('{{ name }}', '"ccc.txt"') + ->setCode(Video::INVALID_MIME_TYPE_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider provideInvalidMimeTypeWithNarrowedSet + */ + public function testInvalidMimeTypeWithNarrowedSet(Video $constraint) + { + $this->validator->validate($this->video, $constraint); + + $this->buildViolation('The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.') + ->setParameter('{{ file }}', \sprintf('"%s"', $this->video)) + ->setParameter('{{ type }}', '"video/mp4"') + ->setParameter('{{ types }}', '"video/mkv", "video/mov"') + ->setParameter('{{ name }}', '"test.mp4"') + ->setCode(Video::INVALID_MIME_TYPE_ERROR) + ->assertRaised(); + } + + public static function provideInvalidMimeTypeWithNarrowedSet() + { + yield 'Doctrine style' => [new Video([ + 'mimeTypes' => [ + 'video/mkv', + 'video/mov', + ], + ])]; + yield 'Named arguments' => [ + new Video(mimeTypes: [ + 'video/mkv', + 'video/mov', + ]), + ]; + } +} diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 5177d37d2955..79d5d875cc37 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -40,7 +40,8 @@ "symfony/property-info": "^6.4|^7.0", "symfony/translation": "^6.4.3|^7.0.3", "symfony/type-info": "^7.1", - "egulias/email-validator": "^2.1.10|^3|^4" + "egulias/email-validator": "^2.1.10|^3|^4", + "php-ffmpeg/php-ffmpeg": "^1.3" }, "conflict": { "doctrine/lexer": "<1.1", From c670298220ada38b43c7251522c823ccbcb53bd9 Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Fri, 29 Nov 2024 15:24:49 +0100 Subject: [PATCH 02/16] Fix conflicts in CHANGELOG --- src/Symfony/Component/Validator/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 70468d4d3fdb..f9fbfc901740 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add support for ratio checks for SVG files to the `Image` constraint * Add the `Slug` constraint + * Add the `Video` constraint for validating video files 7.2 --- From 5b6f30a704e670a7cead338cd468cf90d72b7968 Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Fri, 29 Nov 2024 15:36:59 +0100 Subject: [PATCH 03/16] Update uuid and add comments --- .../Component/Validator/Constraints/Video.php | 26 +++++++++---------- .../Validator/Tests/Constraints/VideoTest.php | 3 +++ .../Tests/Constraints/VideoValidatorTest.php | 3 +++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/Video.php b/src/Symfony/Component/Validator/Constraints/Video.php index c7610695080e..8c86b38db91b 100644 --- a/src/Symfony/Component/Validator/Constraints/Video.php +++ b/src/Symfony/Component/Validator/Constraints/Video.php @@ -19,19 +19,19 @@ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Video extends File { - public const SIZE_NOT_DETECTED_ERROR = '6d55c3f4-e58e-4fe3-91ee-74b492199956'; - public const TOO_WIDE_ERROR = '7f87163d-878f-47f5-99ba-a8eb723a1ab2'; - public const TOO_NARROW_ERROR = '9afbd561-4f90-4a27-be62-1780fc43604a'; - public const TOO_HIGH_ERROR = '7efae81c-4877-47ba-aa65-d01ccb0d4645'; - public const TOO_LOW_ERROR = 'aef0cb6a-c07f-4894-bc08-1781420d7b4c'; - public const TOO_FEW_PIXEL_ERROR = '1b06b97d-ae48-474e-978f-038a74854c43'; - public const TOO_MANY_PIXEL_ERROR = 'ee0804e8-44db-4eac-9775-be91aaf72ce1'; - public const RATIO_TOO_BIG_ERROR = '70cafca6-168f-41c9-8c8c-4e47a52be643'; - public const RATIO_TOO_SMALL_ERROR = '59b8c6ef-bcf2-4ceb-afff-4642ed92f12e'; - public const SQUARE_NOT_ALLOWED_ERROR = '5d41425b-facb-47f7-a55a-de9fbe45cb46'; - public const LANDSCAPE_NOT_ALLOWED_ERROR = '6f895685-7cf2-4d65-b3da-9029c5581d88'; - public const PORTRAIT_NOT_ALLOWED_ERROR = '65608156-77da-4c79-a88c-02ef6d18c782'; - public const CORRUPTED_VIDEO_ERROR = '5d4163f3-648f-4e39-87fd-cc5ea7aad2d1'; + public const SIZE_NOT_DETECTED_ERROR = '5dab98df-43c8-481b-94f9-46a3c958285c'; + public const TOO_WIDE_ERROR = '9e18d6a4-aeda-4644-be8e-9e29dbfd6c4a'; + public const TOO_NARROW_ERROR = 'b267f54b-d994-46d4-9ca6-338fc4f7962f'; + public const TOO_HIGH_ERROR = '44f4c411-0199-48c2-b597-df1f5944ccde'; + public const TOO_LOW_ERROR = '0b6bc3ce-df90-40f9-90aa-5bbb840cb481'; + public const TOO_FEW_PIXEL_ERROR = '510ddf98-2eda-436e-be7e-b6f107bc0e22'; + public const TOO_MANY_PIXEL_ERROR = 'ff0a8ee8-951d-4c97-afe2-03c0d61a2a02'; + public const RATIO_TOO_BIG_ERROR = '5e6b9c21-d4d8-444d-9f4c-e3ff1e25a9a6'; + public const RATIO_TOO_SMALL_ERROR = '26985857-7447-49dc-b271-1477a76cc63c'; + public const SQUARE_NOT_ALLOWED_ERROR = '18500335-b868-4056-b2a2-aa2aeeb0cbdf'; + public const LANDSCAPE_NOT_ALLOWED_ERROR = 'cbf38fbc-04c0-457a-8c29-a6f3080e415a'; + public const PORTRAIT_NOT_ALLOWED_ERROR = '6c3e34a8-94d5-4434-9f20-fb9c0f3ab531'; + public const CORRUPTED_VIDEO_ERROR = '591b9c4d-d357-425f-8672-6b187816550e'; // Include the mapping from the base class diff --git a/src/Symfony/Component/Validator/Tests/Constraints/VideoTest.php b/src/Symfony/Component/Validator/Tests/Constraints/VideoTest.php index 5f71a58363a9..e18ee370a875 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/VideoTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/VideoTest.php @@ -7,6 +7,9 @@ use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; +/** + * @author Kev + */ class VideoTest extends TestCase { public function testAttributes() diff --git a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php index ed7869408c29..60c68dd8f783 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php @@ -6,6 +6,9 @@ use Symfony\Component\Validator\Constraints\VideoValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +/** + * @author Kev + */ class VideoValidatorTest extends ConstraintValidatorTestCase { protected string $path; From 9b9928f62d258092490cbab2083b210411fae943 Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Fri, 29 Nov 2024 15:44:26 +0100 Subject: [PATCH 04/16] Apply coding standards suggested by fabbot.io --- .../Component/Validator/Constraints/Video.php | 4 +--- .../Validator/Constraints/VideoValidator.php | 23 ++++++++----------- .../Validator/Tests/Constraints/VideoTest.php | 9 ++++++++ .../Tests/Constraints/VideoValidatorTest.php | 11 ++++++++- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/Video.php b/src/Symfony/Component/Validator/Constraints/Video.php index 8c86b38db91b..1ccb8c983c1f 100644 --- a/src/Symfony/Component/Validator/Constraints/Video.php +++ b/src/Symfony/Component/Validator/Constraints/Video.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Validator\Constraints; -use Symfony\Component\Validator\Constraints\File; - /** * @author Kev */ @@ -136,7 +134,7 @@ public function __construct( ?string $allowPortraitMessage = null, ?string $corruptedMessage = null, ?array $groups = null, - mixed $payload = null + mixed $payload = null, ) { parent::__construct( $options, diff --git a/src/Symfony/Component/Validator/Constraints/VideoValidator.php b/src/Symfony/Component/Validator/Constraints/VideoValidator.php index 830706742903..95993822d8d3 100644 --- a/src/Symfony/Component/Validator/Constraints/VideoValidator.php +++ b/src/Symfony/Component/Validator/Constraints/VideoValidator.php @@ -20,16 +20,13 @@ */ class VideoValidator extends FileValidator { - /** - * @return void - */ public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Video) { throw new UnexpectedTypeException($constraint, Video::class); } - if (!\class_exists('FFMpeg\FFProbe')) { + if (!class_exists('FFMpeg\FFProbe')) { throw new \LogicException('The "php-ffmpeg/php-ffmpeg" library must be installed to use the Video constraint. Try running "composer require php-ffmpeg/php-ffmpeg".'); } @@ -55,7 +52,7 @@ public function validate(mixed $value, Constraint $constraint): void $ffprobe = \FFMpeg\FFProbe::create(); $stream = $ffprobe->streams($value)->videos()->first(); if (!$stream) { - throw new \Exception('Unexpected FFMpeg error'); + throw new \Exception('Unexpected FFMpeg error.'); } $dimensions = $stream->getDimensions(); } catch (\Exception $e) { @@ -72,7 +69,7 @@ public function validate(mixed $value, Constraint $constraint): void if ($constraint->minWidth) { if (!ctype_digit((string) $constraint->minWidth)) { - throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum width.', $constraint->minWidth)); + throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum width.', $constraint->minWidth)); } if ($width < $constraint->minWidth) { @@ -88,7 +85,7 @@ public function validate(mixed $value, Constraint $constraint): void if ($constraint->maxWidth) { if (!ctype_digit((string) $constraint->maxWidth)) { - throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum width.', $constraint->maxWidth)); + throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum width.', $constraint->maxWidth)); } if ($width > $constraint->maxWidth) { @@ -104,7 +101,7 @@ public function validate(mixed $value, Constraint $constraint): void if ($constraint->minHeight) { if (!ctype_digit((string) $constraint->minHeight)) { - throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum height.', $constraint->minHeight)); + throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum height.', $constraint->minHeight)); } if ($height < $constraint->minHeight) { @@ -120,7 +117,7 @@ public function validate(mixed $value, Constraint $constraint): void if ($constraint->maxHeight) { if (!ctype_digit((string) $constraint->maxHeight)) { - throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum height.', $constraint->maxHeight)); + throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum height.', $constraint->maxHeight)); } if ($height > $constraint->maxHeight) { @@ -136,7 +133,7 @@ public function validate(mixed $value, Constraint $constraint): void if (null !== $constraint->minPixels) { if (!ctype_digit((string) $constraint->minPixels)) { - throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum amount of pixels.', $constraint->minPixels)); + throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum amount of pixels.', $constraint->minPixels)); } if ($pixels < $constraint->minPixels) { @@ -152,7 +149,7 @@ public function validate(mixed $value, Constraint $constraint): void if (null !== $constraint->maxPixels) { if (!ctype_digit((string) $constraint->maxPixels)) { - throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum amount of pixels.', $constraint->maxPixels)); + throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum amount of pixels.', $constraint->maxPixels)); } if ($pixels > $constraint->maxPixels) { @@ -170,7 +167,7 @@ public function validate(mixed $value, Constraint $constraint): void if (null !== $constraint->minRatio) { if (!is_numeric((string) $constraint->minRatio)) { - throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum ratio.', $constraint->minRatio)); + throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum ratio.', $constraint->minRatio)); } if ($ratio < round($constraint->minRatio, 2)) { @@ -184,7 +181,7 @@ public function validate(mixed $value, Constraint $constraint): void if (null !== $constraint->maxRatio) { if (!is_numeric((string) $constraint->maxRatio)) { - throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum ratio.', $constraint->maxRatio)); + throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum ratio.', $constraint->maxRatio)); } if ($ratio > round($constraint->maxRatio, 2)) { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/VideoTest.php b/src/Symfony/Component/Validator/Tests/Constraints/VideoTest.php index e18ee370a875..5c6b11ac053c 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/VideoTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/VideoTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\Validator\Tests\Constraints; use PHPUnit\Framework\TestCase; diff --git a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php index 60c68dd8f783..a29f2b2a5840 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\Validator\Tests\Constraints; use Symfony\Component\Validator\Constraints\Video; @@ -84,7 +93,7 @@ public static function provideConstraintsWithNotFoundMessage(): iterable ]; } - public function testValidSize(): void + public function testValidSize() { $constraint = new Video([ 'minWidth' => 1, From ed02046e2d40f3acf6e33f34035bd72441f182be Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Fri, 29 Nov 2024 15:47:06 +0100 Subject: [PATCH 05/16] Use ::class whenever possible from fabbot.io --- src/Symfony/Component/Validator/Constraints/VideoValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Constraints/VideoValidator.php b/src/Symfony/Component/Validator/Constraints/VideoValidator.php index 95993822d8d3..60b2c54be89e 100644 --- a/src/Symfony/Component/Validator/Constraints/VideoValidator.php +++ b/src/Symfony/Component/Validator/Constraints/VideoValidator.php @@ -26,7 +26,7 @@ public function validate(mixed $value, Constraint $constraint): void throw new UnexpectedTypeException($constraint, Video::class); } - if (!class_exists('FFMpeg\FFProbe')) { + if (!class_exists(\FFMpeg\FFProbe::class)) { throw new \LogicException('The "php-ffmpeg/php-ffmpeg" library must be installed to use the Video constraint. Try running "composer require php-ffmpeg/php-ffmpeg".'); } From 21e0c7ac1f3853f6fea689aa5019a20e3402ec5c Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Fri, 29 Nov 2024 15:54:49 +0100 Subject: [PATCH 06/16] php-ffmpeg in main composer file for dev + check php-ffmpeg is installed in constraint class --- composer.json | 3 ++- src/Symfony/Component/Validator/Constraints/Video.php | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b6099e895494..ec46a75bb885 100644 --- a/composer.json +++ b/composer.json @@ -160,7 +160,8 @@ "twig/cssinliner-extra": "^2.12|^3", "twig/inky-extra": "^2.12|^3", "twig/markdown-extra": "^2.12|^3", - "web-token/jwt-library": "^3.3.2|^4.0" + "web-token/jwt-library": "^3.3.2|^4.0", + "php-ffmpeg/php-ffmpeg": "^1.3" }, "conflict": { "ext-psr": "<1.1|>=2", diff --git a/src/Symfony/Component/Validator/Constraints/Video.php b/src/Symfony/Component/Validator/Constraints/Video.php index 1ccb8c983c1f..0211a502daa1 100644 --- a/src/Symfony/Component/Validator/Constraints/Video.php +++ b/src/Symfony/Component/Validator/Constraints/Video.php @@ -185,6 +185,10 @@ public function __construct( $this->allowPortraitMessage = $allowPortraitMessage ?? $this->allowPortraitMessage; $this->corruptedMessage = $corruptedMessage ?? $this->corruptedMessage; + if (!class_exists(\FFMpeg\FFProbe::class)) { + throw new \LogicException('The "php-ffmpeg/php-ffmpeg" library must be installed to use the Video constraint. Try running "composer require php-ffmpeg/php-ffmpeg".'); + } + if (!\in_array('video/*', (array) $this->mimeTypes, true) && !\array_key_exists('mimeTypesMessage', $options ?? []) && null === $mimeTypesMessage) { $this->mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.'; } From 37a138adcd503dbd3fdb8086a5675b75b9ca0dde Mon Sep 17 00:00:00 2001 From: Kev Date: Tue, 7 Jan 2025 18:31:22 +0100 Subject: [PATCH 07/16] Update src/Symfony/Component/Validator/Constraints/VideoValidator.php Co-authored-by: Nicolas Grekas --- src/Symfony/Component/Validator/Constraints/VideoValidator.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Constraints/VideoValidator.php b/src/Symfony/Component/Validator/Constraints/VideoValidator.php index 60b2c54be89e..9bef6b3bfcc6 100644 --- a/src/Symfony/Component/Validator/Constraints/VideoValidator.php +++ b/src/Symfony/Component/Validator/Constraints/VideoValidator.php @@ -44,7 +44,8 @@ public function validate(mixed $value, Constraint $constraint): void && null === $constraint->minHeight && null === $constraint->maxHeight && null === $constraint->minPixels && null === $constraint->maxPixels && null === $constraint->minRatio && null === $constraint->maxRatio - && $constraint->allowSquare && $constraint->allowLandscape && $constraint->allowPortrait) { + && $constraint->allowSquare && $constraint->allowLandscape && $constraint->allowPortrait + ) { return; } From 6d607a1b8cf7a1d9ee9b9daa5434efe66469e26a Mon Sep 17 00:00:00 2001 From: Kev Date: Tue, 7 Jan 2025 18:48:34 +0100 Subject: [PATCH 08/16] Use named arguments as Nicolas Grekas suggested --- .../Component/Validator/Constraints/Video.php | 19 ++- .../Tests/Constraints/VideoValidatorTest.php | 126 ++++++++---------- 2 files changed, 66 insertions(+), 79 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/Video.php b/src/Symfony/Component/Validator/Constraints/Video.php index 0211a502daa1..ba732aaba5fa 100644 --- a/src/Symfony/Component/Validator/Constraints/Video.php +++ b/src/Symfony/Component/Validator/Constraints/Video.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; + /** * @author Kev */ @@ -55,11 +57,6 @@ class Video extends File self::CORRUPTED_VIDEO_ERROR => 'CORRUPTED_VIDEO_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - public array|string $mimeTypes = 'video/*'; public ?int $minWidth = null; public ?int $maxWidth = null; @@ -89,8 +86,8 @@ class Video extends File public string $allowPortraitMessage = 'The video is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented videos are not allowed.'; public string $corruptedMessage = 'The video file is corrupted.'; + #[HasNamedArguments] public function __construct( - ?array $options = null, int|string|null $maxSize = null, ?bool $binaryFormat = null, array|string|null $mimeTypes = null, @@ -136,8 +133,12 @@ public function __construct( ?array $groups = null, mixed $payload = null, ) { + if (!class_exists(\FFMpeg\FFProbe::class)) { + throw new \LogicException('The "php-ffmpeg/php-ffmpeg" library must be installed to use the Video constraint. Try running "composer require php-ffmpeg/php-ffmpeg".'); + } + parent::__construct( - $options, + [], // File constraint do not use named arguments yet $maxSize, $binaryFormat, $mimeTypes, @@ -185,10 +186,6 @@ public function __construct( $this->allowPortraitMessage = $allowPortraitMessage ?? $this->allowPortraitMessage; $this->corruptedMessage = $corruptedMessage ?? $this->corruptedMessage; - if (!class_exists(\FFMpeg\FFProbe::class)) { - throw new \LogicException('The "php-ffmpeg/php-ffmpeg" library must be installed to use the Video constraint. Try running "composer require php-ffmpeg/php-ffmpeg".'); - } - if (!\in_array('video/*', (array) $this->mimeTypes, true) && !\array_key_exists('mimeTypesMessage', $options ?? []) && null === $mimeTypesMessage) { $this->mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.'; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php index a29f2b2a5840..3b1e50fb24a0 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php @@ -85,9 +85,7 @@ public function testFileNotFound(Video $constraint) public static function provideConstraintsWithNotFoundMessage(): iterable { - yield 'Doctrine style' => [new Video([ - 'notFoundMessage' => 'myMessage', - ])]; + yield 'Doctrine style' => [new Video(notFoundMessage: 'myMessage')]; yield 'Named arguments' => [ new Video(notFoundMessage: 'myMessage'), ]; @@ -95,12 +93,12 @@ public static function provideConstraintsWithNotFoundMessage(): iterable public function testValidSize() { - $constraint = new Video([ - 'minWidth' => 1, - 'maxWidth' => 2, - 'minHeight' => 1, - 'maxHeight' => 2, - ]); + $constraint = new Video( + minWidth: 1, + maxWidth: 2, + minHeight: 1, + maxHeight: 2, + ); $this->validator->validate($this->video, $constraint); @@ -123,10 +121,10 @@ public function testWidthTooSmall(Video $constraint) public static function provideMinWidthConstraints(): iterable { - yield 'Doctrine style' => [new Video([ - 'minWidth' => 3, - 'minWidthMessage' => 'myMessage', - ])]; + yield 'Doctrine style' => [new Video( + minWidth: 3, + minWidthMessage: 'myMessage', + )]; yield 'Named arguments' => [ new Video(minWidth: 3, minWidthMessage: 'myMessage'), ]; @@ -148,10 +146,10 @@ public function testWidthTooBig(Video $constraint) public static function provideMaxWidthConstraints(): iterable { - yield 'Doctrine style' => [new Video([ - 'maxWidth' => 1, - 'maxWidthMessage' => 'myMessage', - ])]; + yield 'Doctrine style' => [new Video( + maxWidth: 1, + maxWidthMessage: 'myMessage', + )]; yield 'Named arguments' => [ new Video(maxWidth: 1, maxWidthMessage: 'myMessage'), ]; @@ -173,10 +171,10 @@ public function testHeightTooSmall(Video $constraint) public static function provideMinHeightConstraints(): iterable { - yield 'Doctrine style' => [new Video([ - 'minHeight' => 3, - 'minHeightMessage' => 'myMessage', - ])]; + yield 'Doctrine style' => [new Video( + minHeight: 3, + minHeightMessage: 'myMessage', + )]; yield 'Named arguments' => [ new Video(minHeight: 3, minHeightMessage: 'myMessage'), ]; @@ -198,10 +196,10 @@ public function testHeightTooBig(Video $constraint) public static function provideMaxHeightConstraints(): iterable { - yield 'Doctrine style' => [new Video([ - 'maxHeight' => 1, - 'maxHeightMessage' => 'myMessage', - ])]; + yield 'Doctrine style' => [new Video( + maxHeight: 1, + maxHeightMessage: 'myMessage', + )]; yield 'Named arguments' => [ new Video(maxHeight: 1, maxHeightMessage: 'myMessage'), ]; @@ -225,10 +223,10 @@ public function testPixelsTooFew(Video $constraint) public static function provideMinPixelsConstraints(): iterable { - yield 'Doctrine style' => [new Video([ - 'minPixels' => 5, - 'minPixelsMessage' => 'myMessage', - ])]; + yield 'Doctrine style' => [new Video( + minPixels: 5, + minPixelsMessage: 'myMessage', + )]; yield 'Named arguments' => [ new Video(minPixels: 5, minPixelsMessage: 'myMessage'), ]; @@ -252,10 +250,10 @@ public function testPixelsTooMany(Video $constraint) public static function provideMaxPixelsConstraints(): iterable { - yield 'Doctrine style' => [new Video([ - 'maxPixels' => 3, - 'maxPixelsMessage' => 'myMessage', - ])]; + yield 'Doctrine style' => [new Video( + maxPixels: 3, + maxPixelsMessage: 'myMessage', + )]; yield 'Named arguments' => [ new Video(maxPixels: 3, maxPixelsMessage: 'myMessage'), ]; @@ -277,10 +275,10 @@ public function testRatioTooSmall(Video $constraint) public static function provideMinRatioConstraints(): iterable { - yield 'Doctrine style' => [new Video([ - 'minRatio' => 2, - 'minRatioMessage' => 'myMessage', - ])]; + yield 'Doctrine style' => [new Video( + minRatio: 2, + minRatioMessage: 'myMessage', + )]; yield 'Named arguments' => [ new Video(minRatio: 2, minRatioMessage: 'myMessage'), ]; @@ -302,10 +300,10 @@ public function testRatioTooBig(Video $constraint) public static function provideMaxRatioConstraints(): iterable { - yield 'Doctrine style' => [new Video([ - 'maxRatio' => 0.5, - 'maxRatioMessage' => 'myMessage', - ])]; + yield 'Doctrine style' => [new Video( + maxRatio: 0.5, + maxRatioMessage: 'myMessage', + )]; yield 'Named arguments' => [ new Video(maxRatio: 0.5, maxRatioMessage: 'myMessage'), ]; @@ -313,9 +311,7 @@ public static function provideMaxRatioConstraints(): iterable public function testMaxRatioUsesTwoDecimalsOnly() { - $constraint = new Video([ - 'maxRatio' => 1.33, - ]); + $constraint = new Video(maxRatio: 1.33); $this->validator->validate($this->video4By3, $constraint); @@ -324,9 +320,7 @@ public function testMaxRatioUsesTwoDecimalsOnly() public function testMinRatioUsesInputMoreDecimals() { - $constraint = new Video([ - 'minRatio' => 4 / 3, - ]); + $constraint = new Video(minRatio: 4 / 3); $this->validator->validate($this->video4By3, $constraint); @@ -335,9 +329,7 @@ public function testMinRatioUsesInputMoreDecimals() public function testMaxRatioUsesInputMoreDecimals() { - $constraint = new Video([ - 'maxRatio' => 16 / 9, - ]); + $constraint = new Video(maxRatio: 16 / 9); $this->validator->validate($this->video16By9, $constraint); @@ -360,10 +352,10 @@ public function testSquareNotAllowed(Video $constraint) public static function provideAllowSquareConstraints(): iterable { - yield 'Doctrine style' => [new Video([ - 'allowSquare' => false, - 'allowSquareMessage' => 'myMessage', - ])]; + yield 'Doctrine style' => [new Video( + allowSquare: false, + allowSquareMessage: 'myMessage', + )]; yield 'Named arguments' => [ new Video(allowSquare: false, allowSquareMessage: 'myMessage'), ]; @@ -385,10 +377,10 @@ public function testLandscapeNotAllowed(Video $constraint) public static function provideAllowLandscapeConstraints(): iterable { - yield 'Doctrine style' => [new Video([ - 'allowLandscape' => false, - 'allowLandscapeMessage' => 'myMessage', - ])]; + yield 'Doctrine style' => [new Video( + allowLandscape: false, + allowLandscapeMessage: 'myMessage', + )]; yield 'Named arguments' => [ new Video(allowLandscape: false, allowLandscapeMessage: 'myMessage'), ]; @@ -410,10 +402,10 @@ public function testPortraitNotAllowed(Video $constraint) public static function provideAllowPortraitConstraints(): iterable { - yield 'Doctrine style' => [new Video([ - 'allowPortrait' => false, - 'allowPortraitMessage' => 'myMessage', - ])]; + yield 'Doctrine style' => [new Video( + allowPortrait: false, + allowPortraitMessage: 'myMessage', + )]; yield 'Named arguments' => [ new Video(allowPortrait: false, allowPortraitMessage: 'myMessage'), ]; @@ -421,9 +413,7 @@ public static function provideAllowPortraitConstraints(): iterable public function testCorrupted() { - $constraint = new Video([ - 'maxRatio' => 1, - ]); + $constraint = new Video(maxRatio: 1); $this->validator->validate($this->videoCorrupted, $constraint); @@ -466,12 +456,12 @@ public function testInvalidMimeTypeWithNarrowedSet(Video $constraint) public static function provideInvalidMimeTypeWithNarrowedSet() { - yield 'Doctrine style' => [new Video([ - 'mimeTypes' => [ + yield 'Doctrine style' => [new Video( + mimeTypes: [ 'video/mkv', 'video/mov', ], - ])]; + )]; yield 'Named arguments' => [ new Video(mimeTypes: [ 'video/mkv', From 93c8747e7b143d1814e02a6c385b5898730e2f78 Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Tue, 7 Jan 2025 19:03:27 +0100 Subject: [PATCH 09/16] Fix Psalm issue : Cannot find referenced variable $options in Video.php --- src/Symfony/Component/Validator/Constraints/Video.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Constraints/Video.php b/src/Symfony/Component/Validator/Constraints/Video.php index ba732aaba5fa..f88e01debebf 100644 --- a/src/Symfony/Component/Validator/Constraints/Video.php +++ b/src/Symfony/Component/Validator/Constraints/Video.php @@ -186,7 +186,7 @@ public function __construct( $this->allowPortraitMessage = $allowPortraitMessage ?? $this->allowPortraitMessage; $this->corruptedMessage = $corruptedMessage ?? $this->corruptedMessage; - if (!\in_array('video/*', (array) $this->mimeTypes, true) && !\array_key_exists('mimeTypesMessage', $options ?? []) && null === $mimeTypesMessage) { + if (!\in_array('video/*', (array) $this->mimeTypes, true) && null === $mimeTypesMessage) { $this->mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.'; } } From 980eeb8393b2092f226f4ebbd744b9e6d079769c Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Tue, 7 Jan 2025 19:15:44 +0100 Subject: [PATCH 10/16] CI Install ffmpeg in system dependencies for linux and windows tests --- .github/workflows/unit-tests.yml | 6 ++++++ .github/workflows/windows.yml | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index d8d36c4e7e03..37ee7180bf81 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -63,6 +63,12 @@ jobs: sudo apt-get update sudo apt-get install zopfli + - name: Install system dependencies + run: | + echo "::group::install tools & libraries" + sudo apt-get install ffmpeg + echo "::endgroup::" + - name: Configure environment run: | git config --global user.email "" diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 62ab3e5e6a3a..653187db4fb8 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -96,9 +96,9 @@ jobs: php phpunit install - - name: Install memurai-developer + - name: Install memurai-developer, ffmpeg run: | - choco install --no-progress memurai-developer + choco install --no-progress memurai-developer ffmpeg - name: Run tests (minimal extensions) if: always() && steps.setup.outcome == 'success' From e9ebfa29f9182e9ffd281d01818dc0cebeefff3c Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Thu, 9 Jan 2025 20:25:57 +0100 Subject: [PATCH 11/16] Video validator test requires extension fileinfo --- .../Component/Validator/Tests/Constraints/VideoValidatorTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php index 3b1e50fb24a0..7a6fd724c5f5 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php @@ -17,6 +17,7 @@ /** * @author Kev + * @requires extension fileinfo */ class VideoValidatorTest extends ConstraintValidatorTestCase { From 9b48473d23fde9aacba00f501fa187d8c9f6a819 Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Thu, 9 Jan 2025 20:26:55 +0100 Subject: [PATCH 12/16] Fix coding standard from fabbot.io --- .../Component/Validator/Tests/Constraints/VideoValidatorTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php index 7a6fd724c5f5..475f33268ef9 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php @@ -17,6 +17,7 @@ /** * @author Kev + * * @requires extension fileinfo */ class VideoValidatorTest extends ConstraintValidatorTestCase From ab74399464531542a2844ba44da8957b76d9856c Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Fri, 10 Jan 2025 16:55:52 +0100 Subject: [PATCH 13/16] Remove group in CI + added phpdoc on video constraints --- .github/workflows/unit-tests.yml | 2 -- .../Component/Validator/Constraints/Video.php | 29 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 37ee7180bf81..4a4528d757bd 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -65,9 +65,7 @@ jobs: - name: Install system dependencies run: | - echo "::group::install tools & libraries" sudo apt-get install ffmpeg - echo "::endgroup::" - name: Configure environment run: | diff --git a/src/Symfony/Component/Validator/Constraints/Video.php b/src/Symfony/Component/Validator/Constraints/Video.php index f88e01debebf..03a66410e47a 100644 --- a/src/Symfony/Component/Validator/Constraints/Video.php +++ b/src/Symfony/Component/Validator/Constraints/Video.php @@ -86,6 +86,35 @@ class Video extends File public string $allowPortraitMessage = 'The video is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented videos are not allowed.'; public string $corruptedMessage = 'The video file is corrupted.'; + /** + * @param positive-int|string|null $maxSize The max size of the underlying file + * @param bool|null $binaryFormat Pass true to use binary-prefixed units (KiB, MiB, etc.) or false to use SI-prefixed units (kB, MB) in displayed messages. Pass null to guess the format from the maxSize option. (defaults to null) + * @param non-empty-string[]|null $mimeTypes Acceptable media types + * @param positive-int|null $filenameMaxLength Maximum length of the file name + * @param string|null $disallowEmptyMessage Enable empty upload validation with this message in case of error + * @param string|null $uploadIniSizeErrorMessage Message if the file size exceeds the max size configured in php.ini + * @param string|null $uploadFormSizeErrorMessage Message if the file size exceeds the max size configured in the HTML input field + * @param string|null $uploadPartialErrorMessage Message if the file is only partially uploaded + * @param string|null $uploadNoTmpDirErrorMessage Message if there is no upload_tmp_dir in php.ini + * @param string|null $uploadCantWriteErrorMessage Message if the uploaded file can not be stored in the temporary directory + * @param string|null $uploadErrorMessage Message if an unknown error occurred on upload + * @param string[]|null $groups + * @param int<0, int>|null $minWidth Minimum video width + * @param positive-int|null $maxWidth Maximum video width + * @param positive-int|null $maxHeight Maximum video height + * @param int<0, int>|null $minHeight Minimum video weight + * @param positive-int|float|null $maxRatio Maximum video ratio + * @param int<0, max>|float|null $minRatio Minimum video ratio + * @param int<0, max>|float|null $minPixels Minimum amount of pixels + * @param positive-int|float|null $maxPixels Maximum amount of pixels + * @param bool|null $allowSquare Whether to allow a square video (defaults to true) + * @param bool|null $allowLandscape Whether to allow a landscape video (defaults to true) + * @param bool|null $allowPortrait Whether to allow a portrait video (defaults to true) + * @param bool|null $detectCorrupted Whether to validate the video is not corrupted (defaults to false) + * @param string|null $sizeNotDetectedMessage Message if the system can not determine video size and there is a size constraint to validate + * + * @see https://www.iana.org/assignments/media-types/media-types.xhtml Existing media types + */ #[HasNamedArguments] public function __construct( int|string|null $maxSize = null, From a083f661a13961984e0b967bc2c2b78db3476e20 Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Fri, 10 Jan 2025 17:09:57 +0100 Subject: [PATCH 14/16] VideoValidatorTest should be skipped if ffmpeg is not available on the system --- .../Validator/Tests/Constraints/VideoValidatorTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php index 475f33268ef9..aee4c56bc93a 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/VideoValidatorTest.php @@ -38,6 +38,13 @@ protected function createValidator(): VideoValidator protected function setUp(): void { + // This test should be skipped if ffmpeg is not available on the system + exec('which ffmpeg', $output, $status); + + if (0 !== $status) { + $this->markTestSkipped('FFMpeg is not available.'); + } + parent::setUp(); $this->video = __DIR__.'/Fixtures/test.mp4'; From cf56118a9f2d31a5b4168241cd806be6db64c5d6 Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Fri, 10 Jan 2025 17:11:42 +0100 Subject: [PATCH 15/16] Applied coding standard suggested by fabbot.io --- src/Symfony/Component/Validator/Constraints/Video.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Constraints/Video.php b/src/Symfony/Component/Validator/Constraints/Video.php index 03a66410e47a..a1a513574629 100644 --- a/src/Symfony/Component/Validator/Constraints/Video.php +++ b/src/Symfony/Component/Validator/Constraints/Video.php @@ -110,7 +110,6 @@ class Video extends File * @param bool|null $allowSquare Whether to allow a square video (defaults to true) * @param bool|null $allowLandscape Whether to allow a landscape video (defaults to true) * @param bool|null $allowPortrait Whether to allow a portrait video (defaults to true) - * @param bool|null $detectCorrupted Whether to validate the video is not corrupted (defaults to false) * @param string|null $sizeNotDetectedMessage Message if the system can not determine video size and there is a size constraint to validate * * @see https://www.iana.org/assignments/media-types/media-types.xhtml Existing media types From 1b26cf23fb262e1155c9efbc6871e35e0bf9aa3f Mon Sep 17 00:00:00 2001 From: symfonyaml <> Date: Sun, 12 Jan 2025 18:59:00 +0100 Subject: [PATCH 16/16] Replace ctype_digit check with just a lower than comparison --- .../Validator/Constraints/VideoValidator.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/VideoValidator.php b/src/Symfony/Component/Validator/Constraints/VideoValidator.php index 9bef6b3bfcc6..e2f293773a1d 100644 --- a/src/Symfony/Component/Validator/Constraints/VideoValidator.php +++ b/src/Symfony/Component/Validator/Constraints/VideoValidator.php @@ -69,7 +69,7 @@ public function validate(mixed $value, Constraint $constraint): void $height = $dimensions->getHeight(); if ($constraint->minWidth) { - if (!ctype_digit((string) $constraint->minWidth)) { + if ($constraint->minWidth < 0) { throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum width.', $constraint->minWidth)); } @@ -85,7 +85,7 @@ public function validate(mixed $value, Constraint $constraint): void } if ($constraint->maxWidth) { - if (!ctype_digit((string) $constraint->maxWidth)) { + if ($constraint->maxWidth < 0) { throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum width.', $constraint->maxWidth)); } @@ -101,7 +101,7 @@ public function validate(mixed $value, Constraint $constraint): void } if ($constraint->minHeight) { - if (!ctype_digit((string) $constraint->minHeight)) { + if ($constraint->minHeight < 0) { throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum height.', $constraint->minHeight)); } @@ -117,7 +117,7 @@ public function validate(mixed $value, Constraint $constraint): void } if ($constraint->maxHeight) { - if (!ctype_digit((string) $constraint->maxHeight)) { + if ($constraint->maxHeight < 0) { throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum height.', $constraint->maxHeight)); } @@ -133,7 +133,7 @@ public function validate(mixed $value, Constraint $constraint): void $pixels = $width * $height; if (null !== $constraint->minPixels) { - if (!ctype_digit((string) $constraint->minPixels)) { + if ($constraint->minPixels < 0) { throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum amount of pixels.', $constraint->minPixels)); } @@ -149,7 +149,7 @@ public function validate(mixed $value, Constraint $constraint): void } if (null !== $constraint->maxPixels) { - if (!ctype_digit((string) $constraint->maxPixels)) { + if ($constraint->maxPixels < 0) { throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum amount of pixels.', $constraint->maxPixels)); }