diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 4694cf69713..ca95ffc2e85 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -160,6 +160,14 @@ langcode: Choice: callback: 'Drupal\Core\TypedData\Plugin\DataType\LanguageReference::getAllValidLangcodes' +# A number of bytes; either a plain number or with a size indication such as "MB". +# @see \Drupal\Component\Utility\Bytes +bytes: + type: string + label: 'Bytes' + constraints: + Callback: ['\Drupal\Component\Utility\Bytes', 'validateConstraint'] + # Complex extended data types: # Root of a configuration object. diff --git a/core/lib/Drupal/Component/Utility/Bytes.php b/core/lib/Drupal/Component/Utility/Bytes.php index 8b8de6d4071..429b6b428b8 100644 --- a/core/lib/Drupal/Component/Utility/Bytes.php +++ b/core/lib/Drupal/Component/Utility/Bytes.php @@ -2,6 +2,8 @@ namespace Drupal\Component\Utility; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + /** * Provides helper methods for byte conversions. */ @@ -109,4 +111,30 @@ class Bytes { return in_array(strtolower($string), self::ALLOWED_SUFFIXES); } + /** + * Validates a string is a representation of a number of bytes. + * + * To be used with the `Callback` constraint. + * + * @param string|int|float|null $value + * The string, integer or float to validate. + * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context + * The validation execution context. + * + * @see \Symfony\Component\Validator\Constraints\CallbackValidator + * @see core/config/schema/core.data_types.schema.yml + */ + public static function validateConstraint(string|int|float|null $value, ExecutionContextInterface $context): void { + // Ignore NULL values (i.e. support `nullable: true`). + if ($value === NULL) { + return; + } + + if (!self::validate((string) $value)) { + $context->addViolation('This value must be a number of bytes, optionally with a unit such as "MB" or "megabytes". %value does not represent a number of bytes.', [ + '%value' => $value, + ]); + } + } + } diff --git a/core/tests/Drupal/Tests/Component/Utility/BytesTest.php b/core/tests/Drupal/Tests/Component/Utility/BytesTest.php index 3ec6e091316..d8c9c5ce222 100644 --- a/core/tests/Drupal/Tests/Component/Utility/BytesTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/BytesTest.php @@ -6,7 +6,10 @@ namespace Drupal\Tests\Component\Utility; use Drupal\Component\Utility\Bytes; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * Tests bytes size parsing helper methods. @@ -18,6 +21,7 @@ use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; class BytesTest extends TestCase { use ExpectDeprecationTrait; + use ProphecyTrait; /** * Tests \Drupal\Component\Utility\Bytes::toNumber(). @@ -88,6 +92,17 @@ class BytesTest extends TestCase { */ public function testValidate($string, bool $expected_result): void { $this->assertSame($expected_result, Bytes::validate($string)); + + $execution_context = $this->prophesize(ExecutionContextInterface::class); + if ($expected_result) { + $execution_context->addViolation(Argument::cetera()) + ->shouldNotBeCalled(); + } + else { + $execution_context->addViolation(Argument::cetera()) + ->shouldBeCalledTimes(1); + } + Bytes::validateConstraint($string, $execution_context->reveal()); } /**