Skip to content

[Validator] Add Video constraint for validating video files #59042

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/unit-tests.yml
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolas-grekas Thank you for your first review.
I will need some help for CI, to define ffmpeg as a system dependency, so the tests can pass.

Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ jobs:
sudo apt-get update
sudo apt-get install zopfli

- name: Install system dependencies
run: |
sudo apt-get install ffmpeg

- name: Configure environment
run: |
git config --global user.email ""
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

@Spomky Spomky Mar 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about Add the Video constraint to validate video files and their properties (e.g., dimensions and pixel density).


7.2
---
Expand Down
221 changes: 221 additions & 0 deletions src/Symfony/Component/Validator/Constraints/Video.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Attribute\HasNamedArguments;

/**
* @author Kev <https://github.com/symfonyaml>
*/
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class Video extends File
{
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

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',
];

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.';

/**
* @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 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,
?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,
) {
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(
[], // File constraint do not use named arguments yet
$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) && null === $mimeTypesMessage) {
$this->mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.';
}
}
}
Loading
Loading