Skip to content

Commit 97dfcf8

Browse files
symfonyamlnicolas-grekas
authored andcommitted
[Validator] Add the Video constraint for validating video files
1 parent 83c2aec commit 97dfcf8

File tree

14 files changed

+897
-3
lines changed

14 files changed

+897
-3
lines changed

.github/workflows/unit-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
if: matrix.php == '8.4'
6161
run: |
6262
sudo apt-get update
63-
sudo apt-get install zopfli
63+
sudo apt-get install zopfli ffmpeg
6464
6565
- name: Configure environment
6666
run: |

.github/workflows/windows.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,9 @@ jobs:
172172
173173
php phpunit install
174174
175-
- name: Install memurai-developer
175+
- name: Install memurai-developer, ffmpeg
176176
run: |
177-
choco install --no-progress memurai-developer
177+
choco install --no-progress memurai-developer ffmpeg
178178
179179
- name: Run tests
180180
run: |

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
7.4
55
---
66

7+
* Add the `Video` constraint for validating video files
78
* Deprecate implementing `__sleep/wakeup()` on `GenericMetadata` implementations; use `__(un)serialize()` instead
89
* Deprecate passing a list of choices to the first argument of the `Choice` constraint. Use the `choices` option instead
910
* Add the `min` and `max` parameter to the `Length` constraint violation
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Process\ExecutableFinder;
15+
use Symfony\Component\Process\Process;
16+
use Symfony\Component\Validator\Attribute\HasNamedArguments;
17+
use Symfony\Component\Validator\Exception\LogicException;
18+
19+
/**
20+
* @author Kev <https://github.com/symfonyaml>
21+
* @author Nicolas Grekas <p@tchwork.com>
22+
*/
23+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
24+
class Video extends File
25+
{
26+
public const SIZE_NOT_DETECTED_ERROR = '5dab98df-43c8-481b-94f9-46a3c958285c';
27+
public const TOO_WIDE_ERROR = '9e18d6a4-aeda-4644-be8e-9e29dbfd6c4a';
28+
public const TOO_NARROW_ERROR = 'b267f54b-d994-46d4-9ca6-338fc4f7962f';
29+
public const TOO_HIGH_ERROR = '44f4c411-0199-48c2-b597-df1f5944ccde';
30+
public const TOO_LOW_ERROR = '0b6bc3ce-df90-40f9-90aa-5bbb840cb481';
31+
public const TOO_FEW_PIXEL_ERROR = '510ddf98-2eda-436e-be7e-b6f107bc0e22';
32+
public const TOO_MANY_PIXEL_ERROR = 'ff0a8ee8-951d-4c97-afe2-03c0d61a2a02';
33+
public const RATIO_TOO_BIG_ERROR = '5e6b9c21-d4d8-444d-9f4c-e3ff1e25a9a6';
34+
public const RATIO_TOO_SMALL_ERROR = '26985857-7447-49dc-b271-1477a76cc63c';
35+
public const SQUARE_NOT_ALLOWED_ERROR = '18500335-b868-4056-b2a2-aa2aeeb0cbdf';
36+
public const LANDSCAPE_NOT_ALLOWED_ERROR = 'cbf38fbc-04c0-457a-8c29-a6f3080e415a';
37+
public const PORTRAIT_NOT_ALLOWED_ERROR = '6c3e34a8-94d5-4434-9f20-fb9c0f3ab531';
38+
public const CORRUPTED_VIDEO_ERROR = '591b9c4d-d357-425f-8672-6b187816550e';
39+
public const MULTIPLE_VIDEO_STREAMS_ERROR = '2d1b2b2e-3f37-4fdd-9a2a-8b6b77b2a6a3';
40+
public const UNSUPPORTED_VIDEO_CODEC_ERROR = 'a9f2f6f7-2b5a-4f3c-b746-d3e2e9d1b2a1';
41+
public const UNSUPPORTED_VIDEO_CONTAINER_ERROR = 'b7c9d2a4-5e1f-4aa0-8f9d-1c3e2b4a6d7e';
42+
43+
// Include the mapping from the base class
44+
45+
protected const ERROR_NAMES = [
46+
self::NOT_FOUND_ERROR => 'NOT_FOUND_ERROR',
47+
self::NOT_READABLE_ERROR => 'NOT_READABLE_ERROR',
48+
self::EMPTY_ERROR => 'EMPTY_ERROR',
49+
self::TOO_LARGE_ERROR => 'TOO_LARGE_ERROR',
50+
self::INVALID_MIME_TYPE_ERROR => 'INVALID_MIME_TYPE_ERROR',
51+
self::FILENAME_TOO_LONG => 'FILENAME_TOO_LONG',
52+
self::SIZE_NOT_DETECTED_ERROR => 'SIZE_NOT_DETECTED_ERROR',
53+
self::TOO_WIDE_ERROR => 'TOO_WIDE_ERROR',
54+
self::TOO_NARROW_ERROR => 'TOO_NARROW_ERROR',
55+
self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR',
56+
self::TOO_LOW_ERROR => 'TOO_LOW_ERROR',
57+
self::TOO_FEW_PIXEL_ERROR => 'TOO_FEW_PIXEL_ERROR',
58+
self::TOO_MANY_PIXEL_ERROR => 'TOO_MANY_PIXEL_ERROR',
59+
self::RATIO_TOO_BIG_ERROR => 'RATIO_TOO_BIG_ERROR',
60+
self::RATIO_TOO_SMALL_ERROR => 'RATIO_TOO_SMALL_ERROR',
61+
self::SQUARE_NOT_ALLOWED_ERROR => 'SQUARE_NOT_ALLOWED_ERROR',
62+
self::LANDSCAPE_NOT_ALLOWED_ERROR => 'LANDSCAPE_NOT_ALLOWED_ERROR',
63+
self::PORTRAIT_NOT_ALLOWED_ERROR => 'PORTRAIT_NOT_ALLOWED_ERROR',
64+
self::CORRUPTED_VIDEO_ERROR => 'CORRUPTED_VIDEO_ERROR',
65+
self::MULTIPLE_VIDEO_STREAMS_ERROR => 'MULTIPLE_VIDEO_STREAMS_ERROR',
66+
self::UNSUPPORTED_VIDEO_CODEC_ERROR => 'UNSUPPORTED_VIDEO_CODEC_ERROR',
67+
self::UNSUPPORTED_VIDEO_CONTAINER_ERROR => 'UNSUPPORTED_VIDEO_CONTAINER_ERROR',
68+
];
69+
70+
public array|string $mimeTypes = 'video/*';
71+
public ?int $minWidth = null;
72+
public ?int $maxWidth = null;
73+
public ?int $maxHeight = null;
74+
public ?int $minHeight = null;
75+
public int|float|null $maxRatio = null;
76+
public int|float|null $minRatio = null;
77+
public int|float|null $minPixels = null;
78+
public int|float|null $maxPixels = null;
79+
public ?bool $allowSquare = true;
80+
public ?bool $allowLandscape = true;
81+
public ?bool $allowPortrait = true;
82+
public array $allowedCodecs = ['h264', 'hevc', 'h265', 'vp9', 'av1', 'mpeg4', 'mpeg2video'];
83+
public array $allowedContainers = ['mp4', 'mov', 'mkv', 'webm', 'avi'];
84+
85+
// The constant for a wrong MIME type is taken from the parent class.
86+
public string $mimeTypesMessage = 'This file is not a valid video.';
87+
public string $sizeNotDetectedMessage = 'The size of the video could not be detected.';
88+
public string $maxWidthMessage = 'The video width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.';
89+
public string $minWidthMessage = 'The video width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.';
90+
public string $maxHeightMessage = 'The video height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.';
91+
public string $minHeightMessage = 'The video height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.';
92+
public string $minPixelsMessage = 'The video has too few pixels ({{ pixels }} pixels). Minimum amount expected is {{ min_pixels }} pixels.';
93+
public string $maxPixelsMessage = 'The video has too many pixels ({{ pixels }} pixels). Maximum amount expected is {{ max_pixels }} pixels.';
94+
public string $maxRatioMessage = 'The video ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.';
95+
public string $minRatioMessage = 'The video ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.';
96+
public string $allowSquareMessage = 'The video is square ({{ width }}x{{ height }}px). Square videos are not allowed.';
97+
public string $allowLandscapeMessage = 'The video is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented videos are not allowed.';
98+
public string $allowPortraitMessage = 'The video is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented videos are not allowed.';
99+
public string $corruptedMessage = 'The video file is corrupted.';
100+
public string $multipleVideoStreamsMessage = 'The video contains multiple streams. Only one stream is allowed.';
101+
public string $unsupportedCodecMessage = 'Unsupported video codec "{{ codec }}".';
102+
public string $unsupportedContainerMessage = 'Unsupported video container "{{ container }}".';
103+
104+
/**
105+
* @param positive-int|string|null $maxSize The max size of the underlying file
106+
* @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)
107+
* @param non-empty-string[]|null $mimeTypes Acceptable media types
108+
* @param positive-int|null $filenameMaxLength Maximum length of the file name
109+
* @param string|null $disallowEmptyMessage Enable empty upload validation with this message in case of error
110+
* @param string|null $uploadIniSizeErrorMessage Message if the file size exceeds the max size configured in php.ini
111+
* @param string|null $uploadFormSizeErrorMessage Message if the file size exceeds the max size configured in the HTML input field
112+
* @param string|null $uploadPartialErrorMessage Message if the file is only partially uploaded
113+
* @param string|null $uploadNoTmpDirErrorMessage Message if there is no upload_tmp_dir in php.ini
114+
* @param string|null $uploadCantWriteErrorMessage Message if the uploaded file can not be stored in the temporary directory
115+
* @param string|null $uploadErrorMessage Message if an unknown error occurred on upload
116+
* @param string[]|null $groups
117+
* @param int<0, int>|null $minWidth Minimum video width
118+
* @param positive-int|null $maxWidth Maximum video width
119+
* @param positive-int|null $maxHeight Maximum video height
120+
* @param int<0, int>|null $minHeight Minimum video weight
121+
* @param positive-int|float|null $maxRatio Maximum video ratio
122+
* @param int<0, max>|float|null $minRatio Minimum video ratio
123+
* @param int<0, max>|float|null $minPixels Minimum amount of pixels
124+
* @param positive-int|float|null $maxPixels Maximum amount of pixels
125+
* @param bool|null $allowSquare Whether to allow a square video (defaults to true)
126+
* @param bool|null $allowLandscape Whether to allow a landscape video (defaults to true)
127+
* @param bool|null $allowPortrait Whether to allow a portrait video (defaults to true)
128+
* @param string|null $sizeNotDetectedMessage Message if the system can not determine video size and there is a size constraint to validate
129+
* @param string[]|null $allowedCodecs Allowed codec names
130+
* @param string[]|null $allowedContainers Allowed container names
131+
*
132+
* @see https://www.iana.org/assignments/media-types/media-types.xhtml Existing media types
133+
*/
134+
#[HasNamedArguments]
135+
public function __construct(
136+
int|string|null $maxSize = null,
137+
?bool $binaryFormat = null,
138+
array|string|null $mimeTypes = null,
139+
?int $filenameMaxLength = null,
140+
?int $minWidth = null,
141+
?int $maxWidth = null,
142+
?int $maxHeight = null,
143+
?int $minHeight = null,
144+
int|float|null $maxRatio = null,
145+
int|float|null $minRatio = null,
146+
int|float|null $minPixels = null,
147+
int|float|null $maxPixels = null,
148+
?bool $allowSquare = null,
149+
?bool $allowLandscape = null,
150+
?bool $allowPortrait = null,
151+
?array $allowedCodecs = null,
152+
?array $allowedContainers = null,
153+
?string $notFoundMessage = null,
154+
?string $notReadableMessage = null,
155+
?string $maxSizeMessage = null,
156+
?string $mimeTypesMessage = null,
157+
?string $disallowEmptyMessage = null,
158+
?string $filenameTooLongMessage = null,
159+
?string $uploadIniSizeErrorMessage = null,
160+
?string $uploadFormSizeErrorMessage = null,
161+
?string $uploadPartialErrorMessage = null,
162+
?string $uploadNoFileErrorMessage = null,
163+
?string $uploadNoTmpDirErrorMessage = null,
164+
?string $uploadCantWriteErrorMessage = null,
165+
?string $uploadExtensionErrorMessage = null,
166+
?string $uploadErrorMessage = null,
167+
?string $sizeNotDetectedMessage = null,
168+
?string $maxWidthMessage = null,
169+
?string $minWidthMessage = null,
170+
?string $maxHeightMessage = null,
171+
?string $minHeightMessage = null,
172+
?string $minPixelsMessage = null,
173+
?string $maxPixelsMessage = null,
174+
?string $maxRatioMessage = null,
175+
?string $minRatioMessage = null,
176+
?string $allowSquareMessage = null,
177+
?string $allowLandscapeMessage = null,
178+
?string $allowPortraitMessage = null,
179+
?string $corruptedMessage = null,
180+
?string $multipleVideoStreamsMessage = null,
181+
?string $unsupportedCodecMessage = null,
182+
?string $unsupportedContainerMessage = null,
183+
?array $groups = null,
184+
mixed $payload = null,
185+
) {
186+
static $hasFfprobe;
187+
if (!$hasFfprobe) {
188+
if (!class_exists(Process::class)) {
189+
throw new LogicException('The Process component is required to use the Video constraint. Try running "composer require symfony/process".');
190+
}
191+
if (!$hasFfprobe ??= (new ExecutableFinder())->find('ffprobe')) {
192+
throw new LogicException('The ffprobe binary is required to use the Video constraint.');
193+
}
194+
}
195+
196+
parent::__construct(
197+
null,
198+
$maxSize,
199+
$binaryFormat,
200+
$mimeTypes,
201+
$filenameMaxLength,
202+
$notFoundMessage,
203+
$notReadableMessage,
204+
$maxSizeMessage,
205+
$mimeTypesMessage,
206+
$disallowEmptyMessage,
207+
$filenameTooLongMessage,
208+
$uploadIniSizeErrorMessage,
209+
$uploadFormSizeErrorMessage,
210+
$uploadPartialErrorMessage,
211+
$uploadNoFileErrorMessage,
212+
$uploadNoTmpDirErrorMessage,
213+
$uploadCantWriteErrorMessage,
214+
$uploadExtensionErrorMessage,
215+
$uploadErrorMessage,
216+
$groups,
217+
$payload
218+
);
219+
220+
$this->minWidth = $minWidth ?? $this->minWidth;
221+
$this->maxWidth = $maxWidth ?? $this->maxWidth;
222+
$this->maxHeight = $maxHeight ?? $this->maxHeight;
223+
$this->minHeight = $minHeight ?? $this->minHeight;
224+
$this->maxRatio = $maxRatio ?? $this->maxRatio;
225+
$this->minRatio = $minRatio ?? $this->minRatio;
226+
$this->minPixels = $minPixels ?? $this->minPixels;
227+
$this->maxPixels = $maxPixels ?? $this->maxPixels;
228+
$this->allowSquare = $allowSquare ?? $this->allowSquare;
229+
$this->allowLandscape = $allowLandscape ?? $this->allowLandscape;
230+
$this->allowPortrait = $allowPortrait ?? $this->allowPortrait;
231+
$this->allowedCodecs = $allowedCodecs ?? $this->allowedCodecs;
232+
$this->allowedContainers = $allowedContainers ?? $this->allowedContainers;
233+
$this->sizeNotDetectedMessage = $sizeNotDetectedMessage ?? $this->sizeNotDetectedMessage;
234+
$this->maxWidthMessage = $maxWidthMessage ?? $this->maxWidthMessage;
235+
$this->minWidthMessage = $minWidthMessage ?? $this->minWidthMessage;
236+
$this->maxHeightMessage = $maxHeightMessage ?? $this->maxHeightMessage;
237+
$this->minHeightMessage = $minHeightMessage ?? $this->minHeightMessage;
238+
$this->minPixelsMessage = $minPixelsMessage ?? $this->minPixelsMessage;
239+
$this->maxPixelsMessage = $maxPixelsMessage ?? $this->maxPixelsMessage;
240+
$this->maxRatioMessage = $maxRatioMessage ?? $this->maxRatioMessage;
241+
$this->minRatioMessage = $minRatioMessage ?? $this->minRatioMessage;
242+
$this->allowSquareMessage = $allowSquareMessage ?? $this->allowSquareMessage;
243+
$this->allowLandscapeMessage = $allowLandscapeMessage ?? $this->allowLandscapeMessage;
244+
$this->allowPortraitMessage = $allowPortraitMessage ?? $this->allowPortraitMessage;
245+
$this->corruptedMessage = $corruptedMessage ?? $this->corruptedMessage;
246+
$this->multipleVideoStreamsMessage = $multipleVideoStreamsMessage ?? $this->multipleVideoStreamsMessage;
247+
$this->unsupportedCodecMessage = $unsupportedCodecMessage ?? $this->unsupportedCodecMessage;
248+
$this->unsupportedContainerMessage = $unsupportedContainerMessage ?? $this->unsupportedContainerMessage;
249+
250+
if (!\in_array('video/*', (array) $this->mimeTypes, true) && null === $mimeTypesMessage) {
251+
$this->mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.';
252+
}
253+
}
254+
}

0 commit comments

Comments
 (0)