diff --git a/composer.json b/composer.json
index 4b3ba25fd19..c93ea2e1014 100644
--- a/composer.json
+++ b/composer.json
@@ -39,6 +39,7 @@
"composer/semver": "^3.3.2",
"craftcms/laravel-aliases": "^2.0",
"craftcms/laravel-dependency-aware-cache": "^1.1",
+ "craftcms/laravel-ruleset-validation": "^1.0.1",
"craftcms/plugin-installer": "~1.6.0",
"craftcms/server-check": "~5.1.0",
"craftcms/yii2-adapter": "self.version",
diff --git a/composer.lock b/composer.lock
index e377d02a76c..17b0f71ec9a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "2c6fc4f0684a0080927ddac26949c945",
+ "content-hash": "f021fb13ae5803c1ef5aa53145b4e877",
"packages": [
{
"name": "bacon/bacon-qr-code",
@@ -615,6 +615,70 @@
},
"time": "2026-03-17T14:55:31+00:00"
},
+ {
+ "name": "craftcms/laravel-ruleset-validation",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/craftcms/laravel-ruleset-validation.git",
+ "reference": "66867ade9c09c6c8165bd62c0ebb7e4843399aab"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/craftcms/laravel-ruleset-validation/zipball/66867ade9c09c6c8165bd62c0ebb7e4843399aab",
+ "reference": "66867ade9c09c6c8165bd62c0ebb7e4843399aab",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/contracts": "^13.0",
+ "illuminate/support": "^13.0",
+ "illuminate/validation": "^13.0",
+ "php": "^8.4"
+ },
+ "require-dev": {
+ "larastan/larastan": "^3.0",
+ "laravel/pint": "^v1.29",
+ "nunomaduro/collision": "^8.1.1",
+ "orchestra/testbench": "^11.0",
+ "pestphp/pest": "^4.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "CraftCms\\RulesetValidation\\RulesetValidationServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "CraftCms\\RulesetValidation\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Pixel & Tonic",
+ "homepage": "https://pixelandtonic.com/"
+ }
+ ],
+ "description": "Validate requests and objects with reusable Laravel rulesets.",
+ "homepage": "https://github.com/craftcms/laravel-ruleset-validation",
+ "keywords": [
+ "craftcms",
+ "laravel",
+ "rulesets",
+ "validation"
+ ],
+ "support": {
+ "issues": "https://github.com/craftcms/laravel-ruleset-validation/issues",
+ "source": "https://github.com/craftcms/laravel-ruleset-validation/tree/1.0.1"
+ },
+ "time": "2026-04-21T07:56:55+00:00"
+ },
{
"name": "craftcms/plugin-installer",
"version": "1.6.0",
diff --git a/database/Factories/Concerns/HasFieldFactory.php b/database/Factories/Concerns/HasFieldFactory.php
index ed313541d19..634a98ca794 100644
--- a/database/Factories/Concerns/HasFieldFactory.php
+++ b/database/Factories/Concerns/HasFieldFactory.php
@@ -108,7 +108,7 @@ public function createElementWithFields(array $attributes = [], bool $save = tru
$factory->refreshFieldCaches();
$element = $factory->queryElement($model->id);
- $element->setScenario($factory->elementScenario ?? Element::SCENARIO_DEFAULT);
+ $element->ruleset->useScenario($factory->elementScenario ?? Element::SCENARIO_DEFAULT);
$element->title = $element->title ?: 'Test entry';
foreach ($factory->fieldConfigs as $config) {
diff --git a/src/Address/Elements/Address.php b/src/Address/Elements/Address.php
index 13d2e8fb863..85d7a803f50 100644
--- a/src/Address/Elements/Address.php
+++ b/src/Address/Elements/Address.php
@@ -24,7 +24,7 @@
use CraftCms\Cms\Shared\Concerns\HasNames;
use CraftCms\Cms\Twig\Attributes\AllowedInSandbox;
use CraftCms\Cms\User\Elements\User;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use Deprecated;
use Override;
use yii\base\InvalidConfigException;
@@ -32,6 +32,9 @@
use function CraftCms\Cms\t;
+/**
+ * @property AddressRules $ruleset
+ */
#[Ruleset(AddressRules::class)]
class Address extends Element implements AddressInterface, NestedElementInterface
{
@@ -471,7 +474,7 @@ public function shouldValidateTitle(): bool
}
#[Override]
- public function beforeValidate(): bool
+ public function prepareForValidation(): void
{
$usedFields = array_unique([
...app(Addresses::class)->getUsedFields($this->countryCode),
@@ -490,7 +493,7 @@ public function beforeValidate(): bool
$this->$field = null;
}
- return parent::beforeValidate();
+ parent::prepareForValidation();
}
#[Override]
diff --git a/src/Address/Validation/AddressRules.php b/src/Address/Validation/AddressRules.php
index ea0a4300296..7d16d0753b9 100644
--- a/src/Address/Validation/AddressRules.php
+++ b/src/Address/Validation/AddressRules.php
@@ -7,7 +7,6 @@
use CraftCms\Cms\Address\Addresses;
use CraftCms\Cms\Address\Elements\Address;
use CraftCms\Cms\Cms;
-use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\FieldLayout\LayoutElements\addresses\LatLongField;
use CraftCms\Cms\FieldLayout\LayoutElements\addresses\OrganizationField;
@@ -22,7 +21,7 @@
/**
* @extends ElementRules
*
- * @property Address $component
+ * @property Address $subject
*/
class AddressRules extends ElementRules
{
@@ -56,27 +55,27 @@ class AddressRules extends ElementRules
];
#[Override]
- public function prepareForValidation(?array $attributeNames = null): void
+ public function prepareForValidation(): void
{
- parent::prepareForValidation($attributeNames);
+ parent::prepareForValidation();
- $attributesToTrim = is_null($attributeNames)
+ $attributesToTrim = is_null($this->validationAttributes)
? self::TRIMMABLE_ATTRIBUTES
- : array_intersect(self::TRIMMABLE_ATTRIBUTES, $attributeNames);
+ : array_intersect(self::TRIMMABLE_ATTRIBUTES, $this->validationAttributes);
foreach ($attributesToTrim as $attribute) {
- $value = $this->component->{$attribute};
+ $value = $this->subject->{$attribute};
if (is_string($value)) {
- $this->component->{$attribute} = trim($value);
+ $this->subject->{$attribute} = trim($value);
}
}
}
#[Override]
- protected function defineRules(): array
+ public function rules(): array
{
- $rules = parent::defineRules();
+ $rules = parent::rules();
$rules = $this->addAddressAttributeRules($rules);
$rules = $this->addCountryCodeValidation($rules);
@@ -135,24 +134,24 @@ private function addAddressFormatRequirements(array $rules): array
private function isRequiredByAddressFormat(string $attribute): bool
{
- if (! $this->component->inScenarios(Element::SCENARIO_LIVE)) {
+ if (! $this->inScenarios(self::SCENARIO_LIVE)) {
return false;
}
$formatter = app(Addresses::class)
->getAddressFormatRepository()
- ->get($this->component->countryCode);
+ ->get($this->subject->countryCode);
return in_array($attribute, $formatter->getRequiredFields());
}
private function addFieldLayoutRequirements(array $rules): array
{
- $fieldLayout = $this->component->getFieldLayout();
+ $fieldLayout = $this->subject->getFieldLayout();
foreach (self::REQUIRABLE_NATIVE_FIELDS as $fieldClass) {
/** @var BaseNativeField|null $field */
- $field = $fieldLayout->getFirstVisibleElementByType($fieldClass, $this->component);
+ $field = $fieldLayout->getFirstVisibleElementByType($fieldClass, $this->subject);
if (! $field?->required) {
continue;
@@ -160,7 +159,7 @@ private function addFieldLayoutRequirements(array $rules): array
foreach ($this->resolveFieldAttributes($field) as $attribute) {
$rules[$attribute] ??= [];
- $rules[$attribute][] = Rule::requiredIf($this->component->inScenarios(Element::SCENARIO_LIVE));
+ $rules[$attribute][] = Rule::requiredIf($this->inScenarios(self::SCENARIO_LIVE));
}
}
@@ -185,7 +184,7 @@ private function resolveFieldAttributes(BaseNativeField $field): array
private function addCoordinateValidation(array $rules): array
{
- $coordinateScenarios = $this->component->inScenarios(Element::SCENARIO_LIVE, Element::SCENARIO_DEFAULT);
+ $coordinateScenarios = $this->inScenarios(self::SCENARIO_LIVE, self::SCENARIO_DEFAULT);
$rules['latitude'][] = Rule::when($coordinateScenarios, [
'numeric',
diff --git a/src/Asset/AssetIndexer.php b/src/Asset/AssetIndexer.php
index 26d49a70881..16039536304 100644
--- a/src/Asset/AssetIndexer.php
+++ b/src/Asset/AssetIndexer.php
@@ -16,6 +16,7 @@
use CraftCms\Cms\Asset\Exceptions\MissingVolumeFolderException;
use CraftCms\Cms\Asset\Exceptions\VolumeException;
use CraftCms\Cms\Asset\Models\AssetIndexingSession as AssetIndexingSessionModel;
+use CraftCms\Cms\Asset\Validation\AssetRules;
use CraftCms\Cms\Cms;
use CraftCms\Cms\Database\Table;
use CraftCms\Cms\Element\Elements;
@@ -630,7 +631,7 @@ public function indexFileByEntry(
$asset->size = $indexEntry->size;
$timeModified = $indexEntry->timestamp;
- $asset->setScenario(Asset::SCENARIO_INDEX);
+ $asset->ruleset->useScenario(AssetRules::SCENARIO_INDEX);
try {
if ($isLocalFs) {
diff --git a/src/Asset/Assets.php b/src/Asset/Assets.php
index be0f453677b..1a09af591aa 100644
--- a/src/Asset/Assets.php
+++ b/src/Asset/Assets.php
@@ -18,6 +18,7 @@
use CraftCms\Cms\Asset\PreviewHandlers\Pdf;
use CraftCms\Cms\Asset\PreviewHandlers\Text;
use CraftCms\Cms\Asset\PreviewHandlers\Video;
+use CraftCms\Cms\Asset\Validation\AssetRules;
use CraftCms\Cms\Cms;
use CraftCms\Cms\Database\Table;
use CraftCms\Cms\Element\Elements;
@@ -94,7 +95,7 @@ public function replaceAssetFile(Asset $asset, string $pathOnServer, string $fil
$asset->setMimeType(File::getMimeType($pathOnServer, checkExtension: false) ?? $mimeType);
$asset->uploaderId = Auth::user()?->id;
$asset->avoidFilenameConflicts = true;
- $asset->setScenario(Asset::SCENARIO_REPLACE);
+ $asset->ruleset->useScenario(AssetRules::SCENARIO_REPLACE);
$this->elements->saveElement($asset);
event(new AfterReplaceAsset(
@@ -118,9 +119,9 @@ public function moveAsset(Asset $asset, VolumeFolder $folder, string $filename =
if ($filenameChanging) {
$asset->newFilename = $filename;
- $asset->setScenario(Asset::SCENARIO_FILEOPS);
+ $asset->ruleset->useScenario(AssetRules::SCENARIO_FILEOPS);
} else {
- $asset->setScenario(Asset::SCENARIO_MOVE);
+ $asset->ruleset->useScenario(AssetRules::SCENARIO_MOVE);
}
return $this->elements->saveElement($asset);
diff --git a/src/Asset/Data/Volume.php b/src/Asset/Data/Volume.php
index 32d1978d960..df7143f0a5a 100644
--- a/src/Asset/Data/Volume.php
+++ b/src/Asset/Data/Volume.php
@@ -19,7 +19,7 @@
use CraftCms\Cms\Support\Env;
use CraftCms\Cms\Support\Facades\Filesystems;
use CraftCms\Cms\Support\Url;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
@@ -123,19 +123,7 @@ public function __construct(array|object $config = [])
}
#[Override]
- public function attributes(): array
- {
- return array_values(array_unique(array_merge(parent::attributes(), [
- 'fieldLayout',
- 'fsHandle',
- 'subpath',
- 'transformFsHandle',
- 'transformSubpath',
- ])));
- }
-
- #[Override]
- public function getAttributes(): array
+ public function validationData(): array
{
if (is_string($this->name)) {
$this->name = trim($this->name);
@@ -151,7 +139,7 @@ public function getAttributes(): array
$fieldLayout = null;
}
- return array_merge(parent::getAttributes(), [
+ return array_merge(parent::validationData(), [
'fieldLayout' => $fieldLayout,
'fsHandle' => $this->getFsHandle(false),
'subpath' => $this->getSubpath(ensureTrailing: false, parse: false),
diff --git a/src/Asset/Data/VolumeFolder.php b/src/Asset/Data/VolumeFolder.php
index 2225f650239..c97792edc7c 100644
--- a/src/Asset/Data/VolumeFolder.php
+++ b/src/Asset/Data/VolumeFolder.php
@@ -68,20 +68,7 @@ public function getRules(): array
}
#[Override]
- public function attributes(): array
- {
- return [
- 'id',
- 'parentId',
- 'volumeId',
- 'name',
- 'path',
- 'uid',
- ];
- }
-
- #[Override]
- public function getAttributes(): array
+ public function validationData(): array
{
return [
'id' => $this->id,
diff --git a/src/Asset/Elements/Asset.php b/src/Asset/Elements/Asset.php
index c96aad600e3..56e39942093 100644
--- a/src/Asset/Elements/Asset.php
+++ b/src/Asset/Elements/Asset.php
@@ -82,7 +82,7 @@
use CraftCms\Cms\Support\Url;
use CraftCms\Cms\Twig\Attributes\AllowedInSandbox;
use CraftCms\Cms\User\Elements\User;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use DateInterval;
use DateTime;
use GraphQL\Type\Definition\Type;
@@ -147,34 +147,6 @@ class Asset extends Element
public const string ERROR_FILENAME_CONFLICT = 'filename_conflict';
- // Validation scenarios
- // -------------------------------------------------------------------------
-
- /**
- * Validation scenario that should be used when the asset is only getting *moved*; not renamed.
- */
- public const string SCENARIO_MOVE = 'move';
-
- public const string SCENARIO_FILEOPS = 'fileOperations';
-
- public const string SCENARIO_INDEX = 'index';
-
- public const string SCENARIO_CREATE = 'create';
-
- public const string SCENARIO_REPLACE = 'replace';
-
- #[Override]
- public function scenarios(): array
- {
- return array_merge(parent::scenarios(), [
- self::SCENARIO_MOVE => null,
- self::SCENARIO_FILEOPS => null,
- self::SCENARIO_INDEX => [],
- self::SCENARIO_CREATE => null,
- self::SCENARIO_REPLACE => null,
- ]);
- }
-
// File kinds
// -------------------------------------------------------------------------
@@ -2943,7 +2915,7 @@ public function afterSave(bool $isNew): void
// Are we uploading an image that needs to be sanitized?
if (
isset($this->tempFilePath) &&
- in_array($this->getScenario(), [self::SCENARIO_REPLACE, self::SCENARIO_CREATE], true) &&
+ in_array($this->ruleset->getScenario(), [AssetRules::SCENARIO_REPLACE, AssetRules::SCENARIO_CREATE], true) &&
AssetsHelper::getFileKindByExtension($this->tempFilePath) === self::KIND_IMAGE &&
($this->sanitizeOnUpload ?? (
! request()->isCpRequest() ||
@@ -2959,7 +2931,7 @@ public function afterSave(bool $isNew): void
$fallbackHeight = null;
if (
isset($this->tempFilePath) &&
- in_array($this->getScenario(), [self::SCENARIO_REPLACE, self::SCENARIO_CREATE], true) &&
+ in_array($this->ruleset->getScenario(), [AssetRules::SCENARIO_REPLACE, AssetRules::SCENARIO_CREATE], true) &&
AssetsHelper::getFileKindByExtension($this->tempFilePath) === self::KIND_IMAGE
) {
$imageSize = getimagesize($this->tempFilePath);
@@ -3176,7 +3148,7 @@ private function _dimensions(mixed $transform = null): array
if (! $this->_width || ! $this->_height) {
if (
$this->kind === self::KIND_IMAGE &&
- $this->getScenario() !== self::SCENARIO_CREATE
+ $this->ruleset->getScenario() !== AssetRules::SCENARIO_CREATE
) {
Log::warning("Asset $this->id is missing its width or height", [__METHOD__]);
}
diff --git a/src/Asset/Validation/AssetRules.php b/src/Asset/Validation/AssetRules.php
index bd3e5bd78cc..2275e3be49a 100644
--- a/src/Asset/Validation/AssetRules.php
+++ b/src/Asset/Validation/AssetRules.php
@@ -15,18 +15,35 @@
/**
* @extends ElementRules
*
- * @property Asset $component
+ * @property Asset $subject
*/
class AssetRules extends ElementRules
{
+ /**
+ * Validation scenario that should be used when the asset is only getting *moved*; not renamed.
+ */
+ public const string SCENARIO_MOVE = 'move';
+
+ public const string SCENARIO_FILEOPS = 'fileOperations';
+
+ public const string SCENARIO_INDEX = 'index';
+
+ public const string SCENARIO_CREATE = 'create';
+
+ public const string SCENARIO_REPLACE = 'replace';
+
#[Override]
- protected function defineRules(): array
+ public function rules(): array
{
- $rules = parent::defineRules();
+ if ($this->inScenarios(self::SCENARIO_INDEX)) {
+ return [];
+ }
+
+ $rules = parent::rules();
$rules['title'] = [
'nullable',
- Rule::when($this->component->inScenarios(Asset::SCENARIO_CREATE), [
+ Rule::when($this->inScenarios(self::SCENARIO_CREATE), [
'string',
'max:255',
new DisallowMb4,
@@ -43,30 +60,30 @@ protected function defineRules(): array
$rules['newFilename'] = ['nullable'];
$rules['kind'] = ['required', 'string', 'max:50'];
$rules['alt'] = ['nullable', Rule::requiredIf(function () {
- if (! $this->component->inScenarios(Asset::SCENARIO_LIVE)) {
+ if (! $this->inScenarios(self::SCENARIO_LIVE)) {
return false;
}
- return $this->component
+ return $this->subject
->getFieldLayout()
- ?->getFirstVisibleElementByType(AltField::class, $this->component)
+ ?->getFirstVisibleElementByType(AltField::class, $this->subject)
->required ?? false;
})];
$rules['newLocation'] = [
'nullable',
- Rule::requiredIf($this->component->inScenarios(Asset::SCENARIO_CREATE, Asset::SCENARIO_MOVE, Asset::SCENARIO_FILEOPS)),
- Rule::when(! $this->component->inScenarios(Asset::SCENARIO_MOVE), [
- new AssetLocationRule($this->component),
+ Rule::requiredIf($this->inScenarios(self::SCENARIO_CREATE, self::SCENARIO_MOVE, self::SCENARIO_FILEOPS)),
+ Rule::when(! $this->inScenarios(self::SCENARIO_MOVE), [
+ new AssetLocationRule($this->subject),
]),
- Rule::when($this->component->inScenarios(Asset::SCENARIO_MOVE), [
- new AssetLocationRule($this->component, allowedExtensions: '*'),
+ Rule::when($this->inScenarios(self::SCENARIO_MOVE), [
+ new AssetLocationRule($this->subject, allowedExtensions: '*'),
]),
];
$rules['tempFilePath'] = [
'nullable',
- Rule::requiredIf($this->component->inScenarios(Asset::SCENARIO_CREATE, Asset::SCENARIO_REPLACE)),
+ Rule::requiredIf($this->inScenarios(self::SCENARIO_CREATE, self::SCENARIO_REPLACE)),
];
return $rules;
diff --git a/src/Asset/Validation/VolumeRules.php b/src/Asset/Validation/VolumeRules.php
index 4455f99d9a8..e0d28660154 100644
--- a/src/Asset/Validation/VolumeRules.php
+++ b/src/Asset/Validation/VolumeRules.php
@@ -16,21 +16,21 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
+use Override;
use function CraftCms\Cms\t;
/** @extends Ruleset */
class VolumeRules extends Ruleset
{
- #[\Override]
- public function defineRules(): array
+ public function rules(): array
{
$rules = [
'id' => ['nullable', 'integer'],
'fieldLayoutId' => ['nullable', 'integer'],
'name' => [
'required',
- Rule::unique(Table::VOLUMES, 'name')->ignore($this->component->id)->withoutTrashed('dateDeleted'),
+ Rule::unique(Table::VOLUMES, 'name')->ignore($this->subject->id)->withoutTrashed('dateDeleted'),
],
'handle' => [
'required',
@@ -43,9 +43,9 @@ public function defineRules(): array
'title',
'uid',
]),
- Rule::unique(Table::VOLUMES, 'handle')->ignore($this->component->id)->withoutTrashed('dateDeleted'),
+ Rule::unique(Table::VOLUMES, 'handle')->ignore($this->subject->id)->withoutTrashed('dateDeleted'),
],
- 'fieldLayout' => [fn (string $attribute, mixed $value, Closure $fail) => $this->component->validateFieldLayout()],
+ 'fieldLayout' => [fn (string $attribute, mixed $value, Closure $fail) => $this->subject->validateFieldLayout()],
'fsHandle' => [fn (string $attribute, mixed $value, Closure $fail) => $this->validateFilesystemHandle($attribute, $fail)],
'transformFsHandle' => ['nullable', fn (string $attribute, mixed $value, Closure $fail) => $this->validateFilesystemHandle($attribute, $fail)],
'subpath' => [
@@ -54,7 +54,7 @@ public function defineRules(): array
],
];
- $tempAssetUploadTarget = $this->component->resolveStorageTargetKey(Cms::config()->tempAssetUploadFs);
+ $tempAssetUploadTarget = $this->subject->resolveStorageTargetKey(Cms::config()->tempAssetUploadFs);
if ($tempAssetUploadTarget !== null) {
$rules['fsHandle'][] = fn (string $attribute, mixed $value, Closure $fail) => $this->validateReservedTempUploadFilesystem($attribute, $tempAssetUploadTarget, $fail);
@@ -64,7 +64,7 @@ public function defineRules(): array
return $rules;
}
- #[\Override]
+ #[Override]
public function messages(): array
{
return [
@@ -79,7 +79,7 @@ private function validateUniqueSubpath(string $attribute, ?Closure $fail = null)
return;
}
- $subpath = $this->component->getSubpath(ensureTrailing: false, parse: false);
+ $subpath = $this->subject->getSubpath(ensureTrailing: false, parse: false);
if ($subpath === '') {
return;
}
@@ -108,15 +108,15 @@ private function subpathRequired(): bool
*/
private function volumesSharingStorageTarget(): Collection
{
- $storageTarget = $this->component->resolveStorageTargetKey($this->component->getFsHandle(false));
+ $storageTarget = $this->subject->resolveStorageTargetKey($this->subject->getFsHandle(false));
if ($storageTarget === null) {
return collect();
}
return VolumeModel::query()
- ->when($this->component->id !== null, fn (Builder $query) => $query->whereNot('id', $this->component->id))
+ ->when($this->subject->id !== null, fn (Builder $query) => $query->whereNot('id', $this->subject->id))
->get()
- ->filter(fn (VolumeModel $record): bool => $this->component->resolveStorageTargetKey($record->fs) === $storageTarget)
+ ->filter(fn (VolumeModel $record): bool => $this->subject->resolveStorageTargetKey($record->fs) === $storageTarget)
->values();
}
@@ -127,7 +127,7 @@ private function validateFilesystemHandle(string $attribute, ?Closure $fail = nu
if ($handle === null || $handle === '') {
if ($attribute === 'fsHandle') {
$this->pushValidationError($attribute, t('{attribute} cannot be blank.', [
- 'attribute' => $this->component->getAttributeLabel($attribute),
+ 'attribute' => $this->subject->getAttributeLabel($attribute),
]), $fail);
}
@@ -144,7 +144,7 @@ private function validateFilesystemHandle(string $attribute, ?Closure $fail = nu
return;
}
- if ($this->component->resolveStorageTargetKey($handle) === null) {
+ if ($this->subject->resolveStorageTargetKey($handle) === null) {
$this->pushValidationError($attribute, t('This filesystem reference is invalid.'), $fail);
}
}
@@ -157,7 +157,7 @@ private function validateReservedTempUploadFilesystem(string $attribute, string
return;
}
- $target = $this->component->resolveStorageTargetKey($handle);
+ $target = $this->subject->resolveStorageTargetKey($handle);
if ($target !== null && $target === $tempUploadTarget) {
$this->pushValidationError(
$attribute,
@@ -170,8 +170,8 @@ private function validateReservedTempUploadFilesystem(string $attribute, string
private function storageHandleForAttribute(string $attribute): ?string
{
return match ($attribute) {
- 'fsHandle' => $this->component->getFsHandle(false),
- 'transformFsHandle' => $this->component->getTransformFsHandle(false),
+ 'fsHandle' => $this->subject->getFsHandle(false),
+ 'transformFsHandle' => $this->subject->getTransformFsHandle(false),
default => null,
};
}
@@ -184,7 +184,7 @@ private function pushValidationError(string $attribute, string $message, ?Closur
return;
}
- $this->component->errors()->add($attribute, $message);
+ $this->subject->errors()->add($attribute, $message);
}
private function isInternalDiskReference(string $value): bool
diff --git a/src/Component/Component.php b/src/Component/Component.php
index bc2100aa2c5..bae3d69cc2f 100644
--- a/src/Component/Component.php
+++ b/src/Component/Component.php
@@ -7,17 +7,21 @@
use CraftCms\Cms\Component\Contracts\ComponentInterface;
use CraftCms\Cms\Component\Exceptions\InvalidCallException;
use CraftCms\Cms\Component\Exceptions\UnknownPropertyException;
+use CraftCms\Cms\Support\Arr;
use CraftCms\Cms\Support\Concerns\MacroableMagicMethods;
use CraftCms\Cms\Support\DateTimeHelper;
use CraftCms\Cms\Support\Typecast;
+use CraftCms\Cms\Validation\ComponentRules;
use CraftCms\Cms\Validation\Concerns\Validates;
use CraftCms\Cms\Validation\Contracts\Validatable;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use DateTimeInterface;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Traits\Macroable;
use Yiisoft\Arrays\ArrayableInterface;
use Yiisoft\Arrays\ArrayableTrait;
+#[Ruleset(ComponentRules::class)]
abstract class Component implements Arrayable, ArrayableInterface, ComponentInterface, Validatable
{
use ArrayableTrait {
@@ -67,7 +71,7 @@ public static function isSelectable(): bool
public function fields(): array
{
- $fields = $this->traitFields();
+ $fields = Arr::except($this->traitFields(), ['ruleset']);
foreach ($fields as $field => $definition) {
if (! is_string($definition)) {
@@ -97,6 +101,19 @@ public function fields(): array
return $fields;
}
+ public function setAttributes($values): void
+ {
+ Typecast::properties(static::class, $values);
+
+ foreach ($values as $name => $value) {
+ try {
+ $this->$name = $value;
+ } catch (UnknownPropertyException|InvalidCallException|\yii\base\UnknownPropertyException) {
+ // Property or setter doesn't exist
+ }
+ }
+ }
+
public function __get(string $name)
{
$getter = $this->resolveMagicMethod('get', $name);
@@ -153,4 +170,14 @@ public function __unset(string $name): void
throw new InvalidCallException('Unsetting an unknown or read-only property: '.static::class.'::'.$name);
}
+
+ public function __serialize(): array
+ {
+ return $this->toArray();
+ }
+
+ public function __unserialize(array $data): void
+ {
+ self::configure($this, $data);
+ }
}
diff --git a/src/Condition/BaseElementSelectConditionRule.php b/src/Condition/BaseElementSelectConditionRule.php
index dd9963ae9a1..6701fddbaff 100644
--- a/src/Condition/BaseElementSelectConditionRule.php
+++ b/src/Condition/BaseElementSelectConditionRule.php
@@ -51,7 +51,7 @@ public function setAttributes($values, $safeOnly = true): void
$values['elementIds'] = Arr::pull($values, 'elementId');
}
- parent::setAttributes($values, $safeOnly);
+ parent::setAttributes($values);
}
/**
diff --git a/src/Cp/FormFields.php b/src/Cp/FormFields.php
index a7e41e2187a..0a51f755dd8 100644
--- a/src/Cp/FormFields.php
+++ b/src/Cp/FormFields.php
@@ -9,7 +9,7 @@
use CraftCms\Cms\Address\Elements\Address;
use CraftCms\Cms\Cp\Html\ContentHtml;
use CraftCms\Cms\Cp\Html\MenuHtml;
-use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Support\Arr;
use CraftCms\Cms\Support\Facades\I18N;
use CraftCms\Cms\Support\Facades\Markdown;
@@ -563,10 +563,10 @@ public static function autosuggestFieldHtml(array $config): string
public static function addressFieldsHtml(Address $address, bool $static = false): string
{
$requiredFields = [];
- $scenario = $address->getScenario();
- $address->setScenario(Element::SCENARIO_LIVE);
+ $scenario = $address->ruleset->getScenario();
+ $address->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
$activeValidators = $address->getActiveValidators();
- $address->setScenario($scenario);
+ $address->ruleset->useScenario($scenario);
$belongsToCurrentUser = $address->getBelongsToCurrentUser();
foreach ($activeValidators as $validator) {
diff --git a/src/Dashboard/Widgets/Widget.php b/src/Dashboard/Widgets/Widget.php
index 2fc093755e8..22f1bcf3798 100644
--- a/src/Dashboard/Widgets/Widget.php
+++ b/src/Dashboard/Widgets/Widget.php
@@ -127,17 +127,11 @@ public function getBodyHtml(): ?string
}
#[Override]
- public function getAttributes(): array
+ public function validationData(): array
{
return $this->getSettings();
}
- #[Override]
- public function attributes(): array
- {
- return array_keys($this->getSettings());
- }
-
public static function fromConfig(array|WidgetModel $config): WidgetInterface
{
if ($config instanceof WidgetModel) {
diff --git a/src/Element/Actions/SetStatus.php b/src/Element/Actions/SetStatus.php
index 94d24184333..d51cb090c06 100644
--- a/src/Element/Actions/SetStatus.php
+++ b/src/Element/Actions/SetStatus.php
@@ -7,6 +7,7 @@
use craft\base\ElementInterface;
use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Support\Facades\Elements;
use CraftCms\Cms\Support\Facades\HtmlStack;
use CraftCms\Cms\Support\Facades\Sites;
@@ -87,7 +88,7 @@ public function performAction(ElementQueryInterface $query): bool
$element->enabled = true;
$element->setEnabledForSite(true);
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
break;
case self::DISABLED:
diff --git a/src/Element/Commands/Resave/ResaveCommand.php b/src/Element/Commands/Resave/ResaveCommand.php
index dc75f08d127..27591b6943f 100644
--- a/src/Element/Commands/Resave/ResaveCommand.php
+++ b/src/Element/Commands/Resave/ResaveCommand.php
@@ -15,6 +15,7 @@
use CraftCms\Cms\Element\Exceptions\InvalidElementException;
use CraftCms\Cms\Element\Jobs\ResaveElements as ResaveElementsJob;
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\Fields;
use CraftCms\Cms\FieldLayout\FieldLayout;
use CraftCms\Cms\Support\Facades\Elements;
@@ -364,7 +365,7 @@ private function runResaveLoop(ElementQueryInterface $query): int
$shouldSet = false;
}
} elseif ($ifInvalid) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
if ($element->validate($set) && $element->validate("field:$set")) {
$shouldSet = false;
diff --git a/src/Element/Concerns/DisplayedInIndex.php b/src/Element/Concerns/DisplayedInIndex.php
index 29a2979830f..16f47461760 100644
--- a/src/Element/Concerns/DisplayedInIndex.php
+++ b/src/Element/Concerns/DisplayedInIndex.php
@@ -20,6 +20,7 @@
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
use CraftCms\Cms\Element\Queries\ElementQuery;
use CraftCms\Cms\Element\Queries\ExcludeDescendantIdsExpression;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\FieldLayout\FieldLayout;
use CraftCms\Cms\Support\Arr;
use CraftCms\Cms\Support\Facades\ElementSources;
@@ -236,7 +237,7 @@ public static function indexHtml(
if (request()->boolean('prevalidate')) {
foreach ($elements as $element) {
if ($element->enabled && $element->getEnabledForSite()) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
}
$element->validate();
}
diff --git a/src/Element/Concerns/Eagerloadable.php b/src/Element/Concerns/Eagerloadable.php
index d46557beb94..58cb43e1a9e 100644
--- a/src/Element/Concerns/Eagerloadable.php
+++ b/src/Element/Concerns/Eagerloadable.php
@@ -17,8 +17,8 @@
use CraftCms\Cms\User\Elements\User;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
+use Throwable;
use Tpetry\QueryExpressions\Language\Alias;
-use yii\base\InvalidConfigException;
/**
* Eagerloadable provides eager loading functionality for elements.
@@ -678,7 +678,7 @@ private function providerHandle(): ?string
{
try {
return $this->getFieldLayout()?->provider?->getHandle();
- } catch (InvalidConfigException) {
+ } catch (Throwable) {
return null;
}
}
diff --git a/src/Element/Drafts.php b/src/Element/Drafts.php
index 2e91c0ce085..c837d69318e 100644
--- a/src/Element/Drafts.php
+++ b/src/Element/Drafts.php
@@ -14,6 +14,7 @@
use CraftCms\Cms\Element\Events\DraftCreated;
use CraftCms\Cms\Element\Exceptions\InvalidElementException;
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Support\Arr;
use CraftCms\Cms\Support\Facades\Structures;
use CraftCms\Cms\User\Elements\User;
@@ -317,7 +318,7 @@ public function removeDraftData(ElementInterface $draft): void
$draft->firstSave = true;
// We still need to validate so the SlugValidator gets run
- $draft->setScenario(Element::SCENARIO_ESSENTIALS);
+ $draft->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$draft->validate();
// If there are any errors on the URI, re-validate as disabled
diff --git a/src/Element/Element.php b/src/Element/Element.php
index 121a2d02ca5..886423c48c9 100644
--- a/src/Element/Element.php
+++ b/src/Element/Element.php
@@ -16,8 +16,8 @@
use CraftCms\Cms\Support\Str;
use CraftCms\Cms\Support\Utils;
use CraftCms\Cms\Twig\Attributes\AllowedInSandbox;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
use CraftCms\Cms\Validation\Concerns\Validates;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use DateTime;
use Illuminate\Support\Facades\Validator as ValidatorFacade;
use Illuminate\Support\Traits\Macroable;
@@ -33,6 +33,8 @@
/**
* Element is the base class for classes representing elements in terms of objects.
+ *
+ * @property ElementRules $ruleset
*/
#[Ruleset(ElementRules::class)]
abstract class Element extends Component implements ElementInterface
@@ -74,15 +76,6 @@ abstract class Element extends Component implements ElementInterface
*/
public const string HOMEPAGE_URI = '__home__';
- // Validation scenarios
- // -------------------------------------------------------------------------
-
- public const string SCENARIO_DEFAULT = 'default';
-
- public const string SCENARIO_ESSENTIALS = 'essentials';
-
- public const string SCENARIO_LIVE = 'live';
-
/**
* @var int|null The element's ID
*/
@@ -186,19 +179,6 @@ abstract class Element extends Component implements ElementInterface
*/
public bool $hardDelete = false;
- /**
- * @return array|null>
- */
- #[Override]
- public function scenarios(): array
- {
- return [
- self::SCENARIO_DEFAULT => null,
- self::SCENARIO_LIVE => null,
- self::SCENARIO_ESSENTIALS => null,
- ];
- }
-
#[Override]
public static function displayName(): string
{
@@ -409,7 +389,7 @@ public function init(): void
* @TODO: Remove parameters once Element no longer extends Yii Model
*/
#[Override]
- public function getAttributes($names = null, $except = []): array
+ public function validationData($names = null, $except = []): array
{
$attributes = $this->attributes();
$values = [];
@@ -628,7 +608,7 @@ protected function validateCustomFields(): void
return;
}
- $scenario = $this->getScenario();
+ $scenario = $this->ruleset->getScenario();
$layoutElements = $fieldLayout->getEditableCustomFieldElements($this);
foreach ($layoutElements as $layoutElement) {
@@ -642,7 +622,7 @@ protected function validateCustomFields(): void
$isEmpty = fn () => $field->isValueEmpty($this->getFieldValue($field->handle), $this);
$rules = [];
- if ($scenario === self::SCENARIO_LIVE && $layoutElement->required) {
+ if ($scenario === ElementRules::SCENARIO_LIVE && $layoutElement->required) {
$rules[] = function ($attribute, $value, $fail) use ($isEmpty) {
if ($isEmpty()) {
$fail(t('validation.required'));
@@ -730,6 +710,6 @@ public function setAttributesFromRequest(array $values): void
#[Override]
public function safeAttributes(): array
{
- return array_keys($this->getRuleset()->rules());
+ return array_keys($this->ruleset->rules());
}
}
diff --git a/src/Element/Jobs/ApplyNewPropagationMethod.php b/src/Element/Jobs/ApplyNewPropagationMethod.php
index b09e23bc622..879dbfb33b2 100644
--- a/src/Element/Jobs/ApplyNewPropagationMethod.php
+++ b/src/Element/Jobs/ApplyNewPropagationMethod.php
@@ -8,6 +8,7 @@
use CraftCms\Cms\Database\Table;
use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\ElementHelper;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Queue\BatchedJob;
use CraftCms\Cms\Structure\Enums\Mode;
use CraftCms\Cms\Support\Facades\Elements;
@@ -190,7 +191,7 @@ protected function defaultDescription(): string
private function resaveItem(ElementInterface $item): void
{
- $item->setScenario(Element::SCENARIO_ESSENTIALS);
+ $item->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$item->resaving = true;
try {
diff --git a/src/Element/Jobs/PropagateElements.php b/src/Element/Jobs/PropagateElements.php
index 9bee7fa109a..ff802f41719 100644
--- a/src/Element/Jobs/PropagateElements.php
+++ b/src/Element/Jobs/PropagateElements.php
@@ -7,6 +7,7 @@
use craft\base\ElementInterface;
use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\ElementHelper;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Queue\BatchedElementJob;
use CraftCms\Cms\Support\Arr;
use CraftCms\Cms\Support\Facades\Elements;
@@ -70,7 +71,7 @@ protected function getQuery(): Builder
protected function processElement(ElementInterface $element): void
{
- $element->setScenario(Element::SCENARIO_ESSENTIALS);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$element->newSiteIds = [];
$element->isNewSite = $this->isNewSite;
$supportedSiteIds = array_map(fn ($siteInfo) => $siteInfo['siteId'], ElementHelper::supportedSitesForElement($element));
diff --git a/src/Element/Jobs/ResaveElements.php b/src/Element/Jobs/ResaveElements.php
index d5c28f34c67..e637b7dce13 100644
--- a/src/Element/Jobs/ResaveElements.php
+++ b/src/Element/Jobs/ResaveElements.php
@@ -8,6 +8,7 @@
use CraftCms\Cms\Element\Commands\Resave\ResaveCommand;
use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\ElementHelper;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Queue\BatchedElementJob;
use CraftCms\Cms\Support\Facades\Elements;
use CraftCms\Cms\Support\Facades\I18N;
@@ -53,7 +54,7 @@ protected function processElement(ElementInterface $element): void
$set = false;
}
} elseif ($this->ifInvalid) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
if ($element->validate($this->set) && $element->validate("field:$this->set")) {
$set = false;
@@ -66,7 +67,7 @@ protected function processElement(ElementInterface $element): void
}
}
- $element->setScenario(Element::SCENARIO_ESSENTIALS);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$element->resaving = true;
try {
diff --git a/src/Element/NestedElementManager.php b/src/Element/NestedElementManager.php
index f4150ba04e4..5537ac9dd72 100644
--- a/src/Element/NestedElementManager.php
+++ b/src/Element/NestedElementManager.php
@@ -21,6 +21,7 @@
use CraftCms\Cms\Element\Events\CreateNestedElementRevisions;
use CraftCms\Cms\Element\Events\DuplicateNestedElementsEvent;
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\Contracts\FieldInterface;
use CraftCms\Cms\Shared\Enums\Color;
use CraftCms\Cms\Site\Data\Site;
@@ -319,7 +320,7 @@ function (string $id, array $config, $attribute, &$settings) use ($owner) {
if ($this->hasErrors($owner)) {
foreach ($elements as $element) {
if ($element->enabled && $element->getEnabledForSite()) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
}
$element->validate();
}
diff --git a/src/Element/Operations/ElementDeletions.php b/src/Element/Operations/ElementDeletions.php
index b4d5d54beb4..2ad9859e046 100644
--- a/src/Element/Operations/ElementDeletions.php
+++ b/src/Element/Operations/ElementDeletions.php
@@ -7,7 +7,6 @@
use craft\base\ElementInterface;
use craft\behaviors\CustomFieldBehavior;
use CraftCms\Cms\Database\Table;
-use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\ElementCaches;
use CraftCms\Cms\Element\ElementCollection;
use CraftCms\Cms\Element\ElementHelper;
@@ -21,6 +20,7 @@
use CraftCms\Cms\Element\Events\BeforeRestoreElement;
use CraftCms\Cms\Element\Exceptions\UnsupportedSiteException;
use CraftCms\Cms\Element\Queries\Exceptions\ElementNotFoundException;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\BaseRelationField;
use CraftCms\Cms\Search\Jobs\FindAndReplace;
use CraftCms\Cms\Search\Search;
@@ -402,7 +402,7 @@ public function restoreElements(array $elements): bool
$siteElements = [];
}
- $element->setScenario(Element::SCENARIO_ESSENTIALS);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
if (! $element->validate()) {
Log::warning("Unable to restore element $element->id: doesn't pass essential validation: ".print_r($element->errors, true), [__METHOD__]);
DB::rollBack();
@@ -415,7 +415,7 @@ public function restoreElements(array $elements): bool
continue;
}
- $siteElement->setScenario(Element::SCENARIO_ESSENTIALS);
+ $siteElement->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
if (! $siteElement->validate()) {
Log::warning("Unable to restore element $element->id: doesn't pass essential validation for site $element->siteId: ".print_r($element->errors, true), [__METHOD__]);
diff --git a/src/Element/Operations/ElementDuplicates.php b/src/Element/Operations/ElementDuplicates.php
index 8afb29e964a..d3142c9a152 100644
--- a/src/Element/Operations/ElementDuplicates.php
+++ b/src/Element/Operations/ElementDuplicates.php
@@ -7,10 +7,10 @@
use craft\base\ElementInterface;
use CraftCms\Cms\Database\Table;
use CraftCms\Cms\Element\Drafts;
-use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\ElementHelper;
use CraftCms\Cms\Element\Exceptions\InvalidElementException;
use CraftCms\Cms\Element\Exceptions\UnsupportedSiteException;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Shared\Exceptions\OperationAbortedException;
use CraftCms\Cms\Structure\Enums\Mode;
use CraftCms\Cms\Structure\Structures;
@@ -129,7 +129,7 @@ public function duplicateElement(
);
}
- $mainClone->setScenario(Element::SCENARIO_ESSENTIALS);
+ $mainClone->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$mainClone->validate();
if ($mainClone->errors()->has('uri') && $mainClone->enabled) {
diff --git a/src/Element/Operations/ElementWrites.php b/src/Element/Operations/ElementWrites.php
index 41946ca626a..6400971d0b8 100644
--- a/src/Element/Operations/ElementWrites.php
+++ b/src/Element/Operations/ElementWrites.php
@@ -7,7 +7,6 @@
use craft\base\ElementInterface;
use craft\base\NestedElementInterface;
use CraftCms\Cms\Database\Table;
-use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\ElementCaches;
use CraftCms\Cms\Element\ElementHelper;
use CraftCms\Cms\Element\Elements;
@@ -30,6 +29,7 @@
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
use CraftCms\Cms\Element\Queries\Exceptions\ElementNotFoundException;
use CraftCms\Cms\Element\Queries\Exceptions\QueryAbortedException;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\FieldLayout\FieldLayout;
use CraftCms\Cms\Search\Search;
use CraftCms\Cms\Shared\Exceptions\OperationAbortedException;
@@ -142,7 +142,7 @@ public function resaveElements(
$query->each(function (ElementInterface $element) use ($continueOnError, $query, &$position, $skipRevisions, $touch, $updateSearchIndex) {
$position++;
- $element->setScenario(Element::SCENARIO_ESSENTIALS);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$element->resaving = true;
$throwable = null;
@@ -207,7 +207,7 @@ public function propagateElements(
event(new BeforePropagateElement($query, $element, $position));
- $element->setScenario(Element::SCENARIO_ESSENTIALS);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$supportedSites = Arr::keyBy(ElementHelper::supportedSitesForElement($element), 'siteId');
$supportedSiteIds = array_keys($supportedSites);
$elementSiteIds = $siteIds !== null ? array_intersect($siteIds,
@@ -308,401 +308,406 @@ protected function saveInternal(
?ElementSiteSettings &$siteSettingsRecord = null,
?bool $inheritedUpdateSearchIndex = null,
): bool {
- $isNewElement = ! $element->id;
- $trackChanges = ElementHelper::shouldTrackChanges($element);
-
- $propagate = $propagate && $element::isLocalized() && $this->sites->isMultiSite();
- $originalPropagateAll = $element->propagateAll;
- $originalFirstSave = $element->firstSave;
- $originalIsNewForSite = $element->isNewForSite;
- $originalDateUpdated = $element->dateUpdated;
- $dirtyAttributes = [];
-
- $element->firstSave = (
- ! $element->getIsDraft() &&
- ! $element->getIsRevision() &&
- ($element->firstSave || $isNewElement)
- );
+ $originalScenario = $element->ruleset->getScenario();
+ try {
+ $isNewElement = ! $element->id;
+ $trackChanges = ElementHelper::shouldTrackChanges($element);
+
+ $propagate = $propagate && $element::isLocalized() && $this->sites->isMultiSite();
+ $originalPropagateAll = $element->propagateAll;
+ $originalFirstSave = $element->firstSave;
+ $originalIsNewForSite = $element->isNewForSite;
+ $originalDateUpdated = $element->dateUpdated;
+ $dirtyAttributes = [];
+
+ $element->firstSave = (
+ ! $element->getIsDraft() &&
+ ! $element->getIsRevision() &&
+ ($element->firstSave || $isNewElement)
+ );
- if ($isNewElement) {
- $element->uid ??= Str::uuid()->toString();
+ if ($isNewElement) {
+ $element->uid ??= Str::uuid()->toString();
- if (! $element->getIsDraft() && ! $element->getIsRevision()) {
- $element->propagateAll = true;
+ if (! $element->getIsDraft() && ! $element->getIsRevision()) {
+ $element->propagateAll = true;
+ }
}
- }
- event($event = new BeforeSaveElement($element, $isNewElement));
+ event($event = new BeforeSaveElement($element, $isNewElement));
- if (! $event->isValid || ! $element->beforeSave($isNewElement)) {
- $this->resetElement($element, $originalFirstSave, $originalIsNewForSite, $originalPropagateAll);
-
- return false;
- }
-
- $supportedSites ??= Arr::keyBy(ElementHelper::supportedSitesForElement($element), 'siteId');
-
- if (! isset($supportedSites[$element->siteId])) {
- $this->resetElement($element, $originalFirstSave, $originalIsNewForSite, $originalPropagateAll);
+ if (! $event->isValid || ! $element->beforeSave($isNewElement)) {
+ $this->resetElement($element, $originalFirstSave, $originalIsNewForSite, $originalPropagateAll);
- throw new UnsupportedSiteException($element, $element->siteId,
- 'Attempting to save an element in an unsupported site.');
- }
+ return false;
+ }
- if (count($supportedSites) === 1 && ! $element->getEnabledForSite()) {
- $element->enabled = false;
- $element->setEnabledForSite(true);
- }
+ $supportedSites ??= Arr::keyBy(ElementHelper::supportedSitesForElement($element), 'siteId');
- if (! $runValidation && $element::hasTitles()) {
- $element->validate('title');
+ if (! isset($supportedSites[$element->siteId])) {
+ $this->resetElement($element, $originalFirstSave, $originalIsNewForSite, $originalPropagateAll);
- if ($element->errors()->has('title')) {
- $element->title = $isNewElement
- ? t('New {type}', ['type' => $element::displayName()])
- : $element::displayName().' '.$element->id;
+ throw new UnsupportedSiteException($element, $element->siteId,
+ 'Attempting to save an element in an unsupported site.');
}
- }
- $fieldLayout = $element->getFieldLayout();
- $dirtyFields = $element->getDirtyFields();
-
- if (! $isNewElement && ! $element->isNewForSite) {
- $siteSettingsRecord = ElementSiteSettings::query()
- ->where('elementId', $element->id)
- ->where('siteId', $element->siteId)
- ->first();
- }
-
- $element->isNewForSite = $siteSettingsRecord === null;
-
- if ($runValidation) {
- if ($element->propagating && ! (
- $element->getIsDerivative() &&
- $element->getIsDraft() &&
- $element->getEnabledForSite() &&
- ! $element->getCanonical()->getEnabledForSite()
- )) {
- $names = array_map(
- fn (string $handle) => "field:$handle",
- array_unique(array_merge($dirtyFields, $element->getModifiedFields())),
- );
- } else {
- $names = null;
+ if (count($supportedSites) === 1 && ! $element->getEnabledForSite()) {
+ $element->enabled = false;
+ $element->setEnabledForSite(true);
}
- if (($names === null || ! empty($names)) && ! $element->validate($names)) {
- Log::info('Element not saved due to validation error: '.print_r($element->errors, true), [__METHOD__]);
- $this->resetElement($element, $originalFirstSave, $originalIsNewForSite, $originalPropagateAll);
+ if (! $runValidation && $element::hasTitles()) {
+ $element->validate('title');
- return false;
+ if ($element->errors()->has('title')) {
+ $element->title = $isNewElement
+ ? t('New {type}', ['type' => $element::displayName()])
+ : $element::displayName().' '.$element->id;
+ }
}
- }
-
- $success = BulkOps::ensure(function () use (
- $element,
- $isNewElement,
- $forceTouch,
- $saveContent,
- $updateSearchIndex,
- $fieldLayout,
- $propagate,
- $supportedSites,
- $crossSiteValidate,
- $runValidation,
- $dirtyFields,
- $trackChanges,
- $originalFirstSave,
- $originalIsNewForSite,
- $originalPropagateAll,
- $originalDateUpdated,
- $inheritedUpdateSearchIndex,
- &$dirtyAttributes,
- &$siteSettingsRecord,
- ) {
- $resolvedUpdateSearchIndex = $updateSearchIndex ?? $inheritedUpdateSearchIndex ?? true;
- $newSiteIds = $element->newSiteIds;
- $element->newSiteIds = [];
- DB::beginTransaction();
+ $fieldLayout = $element->getFieldLayout();
+ $dirtyFields = $element->getDirtyFields();
- try {
- $this->updateModel($element, $isNewElement, $forceTouch, $fieldLayout, $trackChanges, $dirtyAttributes);
+ if (! $isNewElement && ! $element->isNewForSite) {
+ $siteSettingsRecord = ElementSiteSettings::query()
+ ->where('elementId', $element->id)
+ ->where('siteId', $element->siteId)
+ ->first();
+ }
- if ($siteSettingsRecord === null) {
- $siteSettingsRecord = new ElementSiteSettings;
- $siteSettingsRecord->elementId = $element->id;
- $siteSettingsRecord->siteId = $element->siteId;
+ $element->isNewForSite = $siteSettingsRecord === null;
+
+ if ($runValidation) {
+ if ($element->propagating && ! (
+ $element->getIsDerivative() &&
+ $element->getIsDraft() &&
+ $element->getEnabledForSite() &&
+ ! $element->getCanonical()->getEnabledForSite()
+ )) {
+ $names = array_map(
+ fn (string $handle) => "field:$handle",
+ array_unique(array_merge($dirtyFields, $element->getModifiedFields())),
+ );
+ } else {
+ $names = null;
}
- $title = $element::hasTitles() ? $element->title : null;
- $siteSettingsRecord->title = $title !== null && $title !== '' ? $title : null;
- $siteSettingsRecord->slug = $element->slug;
- $siteSettingsRecord->uri = $element->uri;
+ if (($names === null || ! empty($names)) && ! $element->validate($names)) {
+ Log::info('Element not saved due to validation error: '.print_r($element->errors, true), [__METHOD__]);
+ $this->resetElement($element, $originalFirstSave, $originalIsNewForSite, $originalPropagateAll);
- $enabledForSite = $element->getEnabledForSite();
- if (! $siteSettingsRecord->exists || $siteSettingsRecord->enabled !== $enabledForSite) {
- $siteSettingsRecord->enabled = $enabledForSite;
+ return false;
}
+ }
- if ($trackChanges && ! $element->isNewForSite) {
- array_push($dirtyAttributes, ...array_keys(Arr::only($siteSettingsRecord->getDirty(), [
- 'slug',
- 'uri',
- ])));
- if ($siteSettingsRecord->isDirty('enabled')) {
- $dirtyAttributes[] = 'enabledForSite';
+ $success = BulkOps::ensure(function () use (
+ $element,
+ $isNewElement,
+ $forceTouch,
+ $saveContent,
+ $updateSearchIndex,
+ $fieldLayout,
+ $propagate,
+ $supportedSites,
+ $crossSiteValidate,
+ $runValidation,
+ $dirtyFields,
+ $trackChanges,
+ $originalFirstSave,
+ $originalIsNewForSite,
+ $originalPropagateAll,
+ $originalDateUpdated,
+ $inheritedUpdateSearchIndex,
+ &$dirtyAttributes,
+ &$siteSettingsRecord,
+ ) {
+ $resolvedUpdateSearchIndex = $updateSearchIndex ?? $inheritedUpdateSearchIndex ?? true;
+ $newSiteIds = $element->newSiteIds;
+ $element->newSiteIds = [];
+
+ DB::beginTransaction();
+
+ try {
+ $this->updateModel($element, $isNewElement, $forceTouch, $fieldLayout, $trackChanges, $dirtyAttributes);
+
+ if ($siteSettingsRecord === null) {
+ $siteSettingsRecord = new ElementSiteSettings;
+ $siteSettingsRecord->elementId = $element->id;
+ $siteSettingsRecord->siteId = $element->siteId;
}
- }
- $saveContent = $saveContent || $element->isNewForSite;
- $generatedFields = $fieldLayout?->getGeneratedFields() ?? [];
+ $title = $element::hasTitles() ? $element->title : null;
+ $siteSettingsRecord->title = $title !== null && $title !== '' ? $title : null;
+ $siteSettingsRecord->slug = $element->slug;
+ $siteSettingsRecord->uri = $element->uri;
- if ($saveContent || ! empty($dirtyFields) || ! empty($generatedFields)) {
- $oldContent = $siteSettingsRecord->content ?? [];
- if (is_string($oldContent)) {
- $oldContent = $oldContent !== '' ? Json::decode($oldContent) : [];
+ $enabledForSite = $element->getEnabledForSite();
+ if (! $siteSettingsRecord->exists || $siteSettingsRecord->enabled !== $enabledForSite) {
+ $siteSettingsRecord->enabled = $enabledForSite;
}
- $content = [];
- $validUids = [];
+ if ($trackChanges && ! $element->isNewForSite) {
+ array_push($dirtyAttributes, ...array_keys(Arr::only($siteSettingsRecord->getDirty(), [
+ 'slug',
+ 'uri',
+ ])));
+ if ($siteSettingsRecord->isDirty('enabled')) {
+ $dirtyAttributes[] = 'enabledForSite';
+ }
+ }
- if ($fieldLayout) {
- foreach ($fieldLayout->getCustomFields() as $field) {
- $validUids[$field->layoutElement->uid] = true;
+ $saveContent = $saveContent || $element->isNewForSite;
+ $generatedFields = $fieldLayout?->getGeneratedFields() ?? [];
- if (($saveContent || in_array($field->handle, $dirtyFields)) && $field::dbType() !== null) {
- $value = $element->getFieldValue($field->handle);
- if ($element->isNewForSite && $field->isValueEmpty($value, $element)) {
- continue;
- }
- $serializedValue = $field->serializeValueForDb($value, $element);
- if ($serializedValue !== null) {
- $content[$field->layoutElement->uid] = $serializedValue;
- } elseif (! $saveContent) {
- unset($oldContent[$field->layoutElement->uid]);
+ if ($saveContent || ! empty($dirtyFields) || ! empty($generatedFields)) {
+ $oldContent = $siteSettingsRecord->content ?? [];
+ if (is_string($oldContent)) {
+ $oldContent = $oldContent !== '' ? Json::decode($oldContent) : [];
+ }
+
+ $content = [];
+ $validUids = [];
+
+ if ($fieldLayout) {
+ foreach ($fieldLayout->getCustomFields() as $field) {
+ $validUids[$field->layoutElement->uid] = true;
+
+ if (($saveContent || in_array($field->handle, $dirtyFields)) && $field::dbType() !== null) {
+ $value = $element->getFieldValue($field->handle);
+ if ($element->isNewForSite && $field->isValueEmpty($value, $element)) {
+ continue;
+ }
+ $serializedValue = $field->serializeValueForDb($value, $element);
+ if ($serializedValue !== null) {
+ $content[$field->layoutElement->uid] = $serializedValue;
+ } elseif (! $saveContent) {
+ unset($oldContent[$field->layoutElement->uid]);
+ }
}
}
- }
- if ($oldContent) {
- foreach ($generatedFields as $field) {
- if (isset($oldContent[$field['uid']])) {
- $content[$field['uid']] = $oldContent[$field['uid']];
+ if ($oldContent) {
+ foreach ($generatedFields as $field) {
+ if (isset($oldContent[$field['uid']])) {
+ $content[$field['uid']] = $oldContent[$field['uid']];
+ }
}
}
}
- }
- if (! $saveContent && $oldContent) {
- foreach ($oldContent as $uid => $value) {
- if (! isset($content[$uid]) && isset($validUids[$uid])) {
- $content[$uid] = $value;
+ if (! $saveContent && $oldContent) {
+ foreach ($oldContent as $uid => $value) {
+ if (! isset($content[$uid]) && isset($validUids[$uid])) {
+ $content[$uid] = $value;
+ }
}
}
+
+ $siteSettingsRecord->content = $content ?: null;
}
- $siteSettingsRecord->content = $content ?: null;
- }
+ if (! $siteSettingsRecord->save()) {
+ $this->resetElement($element, $originalFirstSave, $originalIsNewForSite, $originalPropagateAll);
- if (! $siteSettingsRecord->save()) {
- $this->resetElement($element, $originalFirstSave, $originalIsNewForSite, $originalPropagateAll);
+ throw new Exception('Couldn’t save elements’ site settings record.');
+ }
- throw new Exception('Couldn’t save elements’ site settings record.');
- }
+ $element->siteSettingsId = $siteSettingsRecord->id;
- $element->siteSettingsId = $siteSettingsRecord->id;
+ if ($trackChanges) {
+ array_push($dirtyAttributes, ...$element->getDirtyAttributes());
+ $element->setDirtyAttributes($dirtyAttributes, false);
+ }
- if ($trackChanges) {
- array_push($dirtyAttributes, ...$element->getDirtyAttributes());
- $element->setDirtyAttributes($dirtyAttributes, false);
- }
+ $element->afterSave($isNewElement);
- $element->afterSave($isNewElement);
+ $dirtyAttributes = $element->getDirtyAttributes();
- $dirtyAttributes = $element->getDirtyAttributes();
+ $siteElements = [];
+ $siteSettingsRecords = [];
- $siteElements = [];
- $siteSettingsRecords = [];
+ if ($propagate) {
+ $otherSiteIds = array_keys(Arr::except($supportedSites, $element->siteId));
- if ($propagate) {
- $otherSiteIds = array_keys(Arr::except($supportedSites, $element->siteId));
+ if (! empty($otherSiteIds)) {
+ if (! $isNewElement) {
+ $siteElements = $element->getLocalizedQuery()
+ ->siteId($otherSiteIds)
+ ->status(null)
+ ->indexBy('siteId')
+ ->all();
+ }
- if (! empty($otherSiteIds)) {
- if (! $isNewElement) {
- $siteElements = $element->getLocalizedQuery()
- ->siteId($otherSiteIds)
- ->status(null)
- ->indexBy('siteId')
- ->all();
- }
+ foreach (array_keys($supportedSites) as $siteId) {
+ if ($siteId === $element->siteId) {
+ continue;
+ }
- foreach (array_keys($supportedSites) as $siteId) {
- if ($siteId === $element->siteId) {
- continue;
- }
+ $siteElement = $siteElements[$siteId] ?? false;
+ $siteElementRecord = null;
+ if (! $this->propagateInternal(
+ $element,
+ $supportedSites,
+ $siteId,
+ $siteElement,
+ crossSiteValidate: $runValidation && $crossSiteValidate,
+ siteSettingsRecord: $siteElementRecord,
+ inheritedUpdateSearchIndex: $resolvedUpdateSearchIndex,
+ )) {
+ throw new InvalidArgumentException;
+ }
- $siteElement = $siteElements[$siteId] ?? false;
- $siteElementRecord = null;
- if (! $this->propagateInternal(
- $element,
- $supportedSites,
- $siteId,
- $siteElement,
- crossSiteValidate: $runValidation && $crossSiteValidate,
- siteSettingsRecord: $siteElementRecord,
- inheritedUpdateSearchIndex: $resolvedUpdateSearchIndex,
- )) {
- throw new InvalidArgumentException;
+ $siteElements[$siteId] = $siteElement;
+ $siteSettingsRecords[$siteId] = $siteElementRecord;
}
-
- $siteElements[$siteId] = $siteElement;
- $siteSettingsRecords[$siteId] = $siteElementRecord;
}
}
- }
-
- if (! $element->propagating && ! empty($generatedFields)) {
- $siteElements[$element->siteId] = $element;
- $siteSettingsRecords[$element->siteId] = $siteSettingsRecord;
- Event::listen(function (AfterPropagate $event) use ($element, $generatedFields, $siteElements, $siteSettingsRecords) {
- if ($event->element->id !== $element->id) {
- return;
- }
+ if (! $element->propagating && ! empty($generatedFields)) {
+ $siteElements[$element->siteId] = $element;
+ $siteSettingsRecords[$element->siteId] = $siteSettingsRecord;
- foreach ($siteElements as $siteId => $siteElement) {
- $siteSettingsRecord = $siteSettingsRecords[$siteId];
- $content = $siteSettingsRecord->content ?? [];
- if (is_string($content)) {
- $content = $content !== '' ? Json::decode($content) : [];
+ Event::listen(function (AfterPropagate $event) use ($element, $generatedFields, $siteElements, $siteSettingsRecords) {
+ if ($event->element->id !== $element->id) {
+ return;
}
- $generatedFieldValues = [];
- $updated = false;
-
- foreach ($generatedFields as $field) {
- $value = renderObjectTemplate($field['template'] ?? '', $siteElement);
- $value = normalizeValue($value) ?? '';
- if ($value !== ($content[$field['uid']] ?? '')) {
- $updated = true;
+ foreach ($siteElements as $siteId => $siteElement) {
+ $siteSettingsRecord = $siteSettingsRecords[$siteId];
+ $content = $siteSettingsRecord->content ?? [];
+ if (is_string($content)) {
+ $content = $content !== '' ? Json::decode($content) : [];
}
- if ($value !== '') {
- $content[$field['uid']] = $value;
- if (($field['handle'] ?? '') !== '') {
- $generatedFieldValues[$field['handle']] = $value;
+ $generatedFieldValues = [];
+ $updated = false;
+
+ foreach ($generatedFields as $field) {
+ $value = renderObjectTemplate($field['template'] ?? '', $siteElement);
+ $value = normalizeValue($value) ?? '';
+
+ if ($value !== ($content[$field['uid']] ?? '')) {
+ $updated = true;
+ }
+ if ($value !== '') {
+ $content[$field['uid']] = $value;
+ if (($field['handle'] ?? '') !== '') {
+ $generatedFieldValues[$field['handle']] = $value;
+ }
+ } else {
+ unset($content[$field['uid']]);
}
- } else {
- unset($content[$field['uid']]);
}
- }
- if ($updated) {
- $siteSettingsRecord->content = $content;
- $siteSettingsRecord->save();
- $siteElement->setGeneratedFieldValues($generatedFieldValues);
+ if ($updated) {
+ $siteSettingsRecord->content = $content;
+ $siteSettingsRecord->save();
+ $siteElement->setGeneratedFieldValues($generatedFieldValues);
+ }
}
- }
- });
- }
+ });
+ }
- if (
- ! $element->propagating &&
- ! $element->duplicateOf &&
- ! $element->mergingCanonicalChanges
- ) {
- $element->afterPropagate($isNewElement);
- BulkOps::trackElement($element);
- }
+ if (
+ ! $element->propagating &&
+ ! $element->duplicateOf &&
+ ! $element->mergingCanonicalChanges
+ ) {
+ $element->afterPropagate($isNewElement);
+ BulkOps::trackElement($element);
+ }
- DB::commit();
- } catch (Throwable $throwable) {
- DB::rollBack();
+ DB::commit();
+ } catch (Throwable $throwable) {
+ DB::rollBack();
- $this->resetElement($element, $originalFirstSave, $originalIsNewForSite, $originalPropagateAll);
- $element->dateUpdated = $originalDateUpdated;
+ $this->resetElement($element, $originalFirstSave, $originalIsNewForSite, $originalPropagateAll);
+ $element->dateUpdated = $originalDateUpdated;
- if ($throwable instanceof InvalidArgumentException) {
- return false;
+ if ($throwable instanceof InvalidArgumentException) {
+ return false;
+ }
+
+ throw $throwable;
+ } finally {
+ $element->newSiteIds = $newSiteIds;
}
- throw $throwable;
- } finally {
- $element->newSiteIds = $newSiteIds;
- }
+ if (! $element->propagating) {
+ if (! $isNewElement) {
+ $deleteCondition = fn (Builder $query) => $query
+ ->where('elementId', $element->id)
+ ->whereNotIn('siteId', array_keys($supportedSites));
- if (! $element->propagating) {
- if (! $isNewElement) {
- $deleteCondition = fn (Builder $query) => $query
- ->where('elementId', $element->id)
- ->whereNotIn('siteId', array_keys($supportedSites));
+ DB::table(Table::ELEMENTS_SITES)->where($deleteCondition)->delete();
+ DB::table(Table::SEARCHINDEX)->where($deleteCondition)->delete();
+ DB::table(Table::SEARCHINDEXQUEUE)->where($deleteCondition)->delete();
+ }
- DB::table(Table::ELEMENTS_SITES)->where($deleteCondition)->delete();
- DB::table(Table::SEARCHINDEX)->where($deleteCondition)->delete();
- DB::table(Table::SEARCHINDEXQUEUE)->where($deleteCondition)->delete();
+ $this->elementCaches->invalidateForElement($element);
}
- $this->elementCaches->invalidateForElement($element);
- }
-
- if ($resolvedUpdateSearchIndex && ! $element->getIsRevision() && ! ElementHelper::isRevision($element)) {
- $searchableDirtyFields = array_filter(
- $dirtyFields,
- fn (string $handle) => $fieldLayout?->getFieldByHandle($handle)?->searchable,
- );
-
- if (
- ! $trackChanges ||
- ! empty($searchableDirtyFields) ||
- ! empty(array_intersect($dirtyAttributes, ElementHelper::searchableAttributes($element)))
- ) {
- event($event = new BeforeUpdateSearchIndex($element));
-
- if ($event->isValid) {
- $this->updateElementSearchIndex($element, $searchableDirtyFields, $propagate);
+ if ($resolvedUpdateSearchIndex && ! $element->getIsRevision() && ! ElementHelper::isRevision($element)) {
+ $searchableDirtyFields = array_filter(
+ $dirtyFields,
+ fn (string $handle) => $fieldLayout?->getFieldByHandle($handle)?->searchable,
+ );
+
+ if (
+ ! $trackChanges ||
+ ! empty($searchableDirtyFields) ||
+ ! empty(array_intersect($dirtyAttributes, ElementHelper::searchableAttributes($element)))
+ ) {
+ event($event = new BeforeUpdateSearchIndex($element));
+
+ if ($event->isValid) {
+ $this->updateElementSearchIndex($element, $searchableDirtyFields, $propagate);
+ }
}
}
- }
- if ($trackChanges) {
- $userId = Auth::user()?->id;
- $timestamp = now();
-
- foreach ($dirtyAttributes as $attributeName) {
- DB::table(Table::CHANGEDATTRIBUTES)
- ->upsert([
- 'elementId' => $element->id,
- 'siteId' => $element->siteId,
- 'attribute' => $attributeName,
- 'dateUpdated' => $timestamp,
- 'propagated' => $element->propagating,
- 'userId' => $userId,
- ], ['elementId', 'siteId', 'attribute']);
- }
+ if ($trackChanges) {
+ $userId = Auth::user()?->id;
+ $timestamp = now();
+
+ foreach ($dirtyAttributes as $attributeName) {
+ DB::table(Table::CHANGEDATTRIBUTES)
+ ->upsert([
+ 'elementId' => $element->id,
+ 'siteId' => $element->siteId,
+ 'attribute' => $attributeName,
+ 'dateUpdated' => $timestamp,
+ 'propagated' => $element->propagating,
+ 'userId' => $userId,
+ ], ['elementId', 'siteId', 'attribute']);
+ }
- if ($fieldLayout) {
- foreach ($dirtyFields as $fieldHandle) {
- if (($field = $fieldLayout->getFieldByHandle($fieldHandle)) !== null) {
- DB::table(Table::CHANGEDFIELDS)
- ->upsert([
- 'elementId' => $element->id,
- 'siteId' => $element->siteId,
- 'fieldId' => $field->id,
- 'layoutElementUid' => $field->layoutElement->uid,
- 'dateUpdated' => $timestamp,
- 'propagated' => $element->propagating,
- 'userId' => $userId,
- ], ['elementId', 'siteId', 'fieldId', 'layoutElementUid']);
+ if ($fieldLayout) {
+ foreach ($dirtyFields as $fieldHandle) {
+ if (($field = $fieldLayout->getFieldByHandle($fieldHandle)) !== null) {
+ DB::table(Table::CHANGEDFIELDS)
+ ->upsert([
+ 'elementId' => $element->id,
+ 'siteId' => $element->siteId,
+ 'fieldId' => $field->id,
+ 'layoutElementUid' => $field->layoutElement->uid,
+ 'dateUpdated' => $timestamp,
+ 'propagated' => $element->propagating,
+ 'userId' => $userId,
+ ], ['elementId', 'siteId', 'fieldId', 'layoutElementUid']);
+ }
}
}
}
- }
- return true;
- });
+ return true;
+ });
- if (! $success) {
- return false;
+ if (! $success) {
+ return false;
+ }
+ } finally {
+ $element->ruleset->useScenario($originalScenario);
}
event(new AfterSaveElement($element, $isNewElement));
@@ -799,14 +804,14 @@ protected function propagateInternal(
}
}
- $siteElement->setScenario(Element::SCENARIO_ESSENTIALS);
+ $siteElement->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
if (
($crossSiteValidate || $element->propagateRequired) &&
$siteElement->enabled &&
$siteElement->getEnabledForSite()
) {
- $siteElement->setScenario(Element::SCENARIO_LIVE);
+ $siteElement->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
}
$siteElement->setDirtyAttributes(array_filter($element->getDirtyAttributes(),
diff --git a/src/Element/Validation/ElementRules.php b/src/Element/Validation/ElementRules.php
index eb4e678b94d..d8b8cbdaa95 100644
--- a/src/Element/Validation/ElementRules.php
+++ b/src/Element/Validation/ElementRules.php
@@ -26,28 +26,34 @@
/**
* @template T of Element
*
- * @property Element $component
+ * @property Element $subject
*
* @extends Ruleset
*/
-abstract class ElementRules extends Ruleset
+class ElementRules extends Ruleset
{
+ public const string SCENARIO_DEFAULT = 'default';
+
+ public const string SCENARIO_ESSENTIALS = 'essentials';
+
+ public const string SCENARIO_LIVE = 'live';
+
private ?string $uriPreparationError = null;
#[Override]
- public function prepareForValidation(?array $attributeNames = null): void
+ public function prepareForValidation(): void
{
- $shouldPrepare = fn (string $attribute) => is_null($attributeNames) || in_array($attribute, $attributeNames, true);
+ $shouldPrepare = fn (string $attribute) => is_null($this->validationAttributes) || in_array($attribute, $this->validationAttributes, true);
if ($shouldPrepare('title')) {
$this->prepareTitle();
}
- if ($shouldPrepare('slug') && $this->component->hasUris()) {
- $this->prepareSlug($this->component->getSite()->language);
+ if ($shouldPrepare('slug') && $this->subject->hasUris()) {
+ $this->prepareSlug($this->subject->getSite()->language);
}
- if ($shouldPrepare('uri') && $this->component->hasUris()) {
+ if ($shouldPrepare('uri') && $this->subject->hasUris()) {
$this->prepareUri();
}
}
@@ -59,10 +65,9 @@ public function prepareForValidation(?array $attributeNames = null): void
*
* @return array
*/
- #[Override]
- protected function defineRules(): array
+ public function rules(): array
{
- $int = Rule::when($this->component->inScenarios(Element::SCENARIO_DEFAULT, Element::SCENARIO_LIVE), ['nullable', 'integer']);
+ $int = Rule::when($this->inScenarios(self::SCENARIO_DEFAULT, self::SCENARIO_LIVE), ['nullable', 'integer']);
$rules = [
'id' => $int,
@@ -70,10 +75,10 @@ protected function defineRules(): array
'root' => $int,
'lft' => $int,
'rgt' => $int,
- 'level' => Rule::when($this->component->inScenarios(Element::SCENARIO_DEFAULT, Element::SCENARIO_LIVE, Element::SCENARIO_ESSENTIALS), ['nullable', 'integer']),
+ 'level' => Rule::when($this->inScenarios(self::SCENARIO_DEFAULT, self::SCENARIO_LIVE, self::SCENARIO_ESSENTIALS), ['nullable', 'integer']),
'title' => ['string', 'max:255', new DisallowMb4],
'siteId' => Rule::when(
- $this->component->inScenarios(Element::SCENARIO_DEFAULT, Element::SCENARIO_LIVE, Element::SCENARIO_ESSENTIALS),
+ $this->inScenarios(self::SCENARIO_DEFAULT, self::SCENARIO_LIVE, self::SCENARIO_ESSENTIALS),
[
'nullable',
new SiteIdRule(allowDisabled: true),
@@ -92,7 +97,7 @@ protected function defineRules(): array
private function addTitleRules(array $rules): array
{
try {
- if ($this->component->inScenarios(Element::SCENARIO_DEFAULT, Element::SCENARIO_LIVE) && $this->component->shouldValidateTitle()) {
+ if ($this->inScenarios(self::SCENARIO_DEFAULT, self::SCENARIO_LIVE) && $this->subject->shouldValidateTitle()) {
array_unshift($rules['title'], 'required');
} else {
array_unshift($rules['title'], 'nullable');
@@ -107,18 +112,18 @@ private function addTitleRules(array $rules): array
private function addUriRules(array $rules): array
{
- if (! $this->component->hasUris()) {
+ if (! $this->subject->hasUris()) {
return $rules;
}
- $rules['slug'] = [Rule::when($this->component->inScenarios(Element::SCENARIO_DEFAULT, Element::SCENARIO_LIVE, Element::SCENARIO_ESSENTIALS), [
+ $rules['slug'] = [Rule::when($this->inScenarios(self::SCENARIO_DEFAULT, self::SCENARIO_LIVE, self::SCENARIO_ESSENTIALS), [
'max:255',
])];
try {
- $uriFormat = $this->component->getUriFormat() ?? '';
+ $uriFormat = $this->subject->getUriFormat() ?? '';
- if ($this->component->inScenarios(Element::SCENARIO_DEFAULT, Element::SCENARIO_LIVE)
+ if ($this->inScenarios(self::SCENARIO_DEFAULT, self::SCENARIO_LIVE)
&& preg_match('/\bslug\b/', $uriFormat)
) {
array_unshift($rules['slug'], 'required', 'string');
@@ -130,7 +135,7 @@ private function addUriRules(array $rules): array
}
$rules['uri'] = Rule::when(
- $this->component->inScenarios(Element::SCENARIO_DEFAULT, Element::SCENARIO_LIVE, Element::SCENARIO_ESSENTIALS),
+ $this->inScenarios(self::SCENARIO_DEFAULT, self::SCENARIO_LIVE, self::SCENARIO_ESSENTIALS),
[
'bail',
// Fail if we have an uriPreparationError
@@ -150,24 +155,24 @@ function (?string $attribute, mixed $value, Closure $fail) {
private function prepareTitle(): void
{
- $title = $this->component->title;
+ $title = $this->subject->title;
if (! is_string($title)) {
return;
}
- $this->component->title = trim(Str::convertLineBreaks($title));
+ $this->subject->title = trim(Str::convertLineBreaks($title));
}
private function prepareSlug(string $language): void
{
- $slug = (string) $this->component->slug;
+ $slug = (string) $this->subject->slug;
$isTemp = ElementHelper::isTempSlug($slug);
- $element = $this->component;
+ $element = $this->subject;
$isDraft = $element instanceof ElementInterface && $element->getIsDraft();
- if ($isDraft && ! in_array($element->getScenario(), [Element::SCENARIO_LIVE, 'default'], true)) {
+ if ($isDraft && ! in_array($element->ruleset->getScenario(), [self::SCENARIO_LIVE, self::SCENARIO_DEFAULT], true)) {
if ($isTemp) {
return;
}
@@ -202,20 +207,20 @@ private function setSlugOnElement(?ElementInterface $element, string $slug): voi
private function prepareUri(): void
{
- if ($this->component->getIsRevision()) {
+ if ($this->subject->getIsRevision()) {
return;
}
- if ($this->component->getIsDraft() && ! $this->component->getIsUnpublishedDraft()) {
- if (! $this->component->inScenarios(Element::SCENARIO_LIVE)) {
+ if ($this->subject->getIsDraft() && ! $this->subject->getIsUnpublishedDraft()) {
+ if (! $this->inScenarios(self::SCENARIO_LIVE)) {
return;
}
- $canonical = $this->component->getCanonical();
+ $canonical = $this->subject->getCanonical();
if (
- $canonical !== $this->component &&
- $this->component->uri === $canonical->uri &&
+ $canonical !== $this->subject &&
+ $this->subject->uri === $canonical->uri &&
$canonical->enabled &&
$canonical->getEnabledForSite()
) {
@@ -224,12 +229,12 @@ private function prepareUri(): void
}
try {
- Elements::setElementUri($this->component);
+ Elements::setElementUri($this->subject);
} catch (OperationAbortedException) {
if (
- $this->component->enabled &&
- $this->component->getEnabledForSite() &&
- (! $this->component->getIsUnpublishedDraft() || $this->component->inScenarios(Element::SCENARIO_LIVE))
+ $this->subject->enabled &&
+ $this->subject->getEnabledForSite() &&
+ (! $this->subject->getIsUnpublishedDraft() || $this->inScenarios(self::SCENARIO_LIVE))
) {
$this->uriPreparationError = t('Could not generate a unique URI based on the URI format.');
}
diff --git a/src/Entry/Commands/UpdateStatusesCommand.php b/src/Entry/Commands/UpdateStatusesCommand.php
index 39272849a6e..6409256811b 100644
--- a/src/Entry/Commands/UpdateStatusesCommand.php
+++ b/src/Entry/Commands/UpdateStatusesCommand.php
@@ -5,7 +5,6 @@
namespace CraftCms\Cms\Entry\Commands;
use CraftCms\Cms\Console\CraftCommand;
-use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\ElementHelper;
use CraftCms\Cms\Element\Events\AfterResaveElement;
use CraftCms\Cms\Element\Events\AfterResaveElements;
@@ -13,6 +12,7 @@
use CraftCms\Cms\Element\Events\BeforeResaveElements;
use CraftCms\Cms\Element\Exceptions\InvalidElementException;
use CraftCms\Cms\Element\Queries\EntryQuery;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\Support\DateTimeHelper;
use CraftCms\Cms\Support\Facades\Elements;
@@ -139,7 +139,7 @@ private function resaveQuery(EntryQuery $query): void
foreach ($query->cursor() as $entry) {
$position++;
- $entry->setScenario(Element::SCENARIO_ESSENTIALS);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$entry->resaving = true;
event(new BeforeResaveElement($query, $entry, $position));
diff --git a/src/Entry/Conditions/TypeConditionRule.php b/src/Entry/Conditions/TypeConditionRule.php
index 7e3bcf88059..3915a118f5f 100644
--- a/src/Entry/Conditions/TypeConditionRule.php
+++ b/src/Entry/Conditions/TypeConditionRule.php
@@ -43,7 +43,7 @@ public function setAttributes($values, $safeOnly = true): void
unset($values['entryTypeUid'], $values['sectionUid']);
}
- parent::setAttributes($values, $safeOnly);
+ parent::setAttributes($values);
}
protected function options(): array
diff --git a/src/Entry/Data/EntryType.php b/src/Entry/Data/EntryType.php
index 5abef586586..e84871634bc 100644
--- a/src/Entry/Data/EntryType.php
+++ b/src/Entry/Data/EntryType.php
@@ -28,7 +28,7 @@
use CraftCms\Cms\Support\Facades\InputNamespace;
use CraftCms\Cms\Support\Facades\Sections;
use CraftCms\Cms\Support\Url;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use Illuminate\Support\Facades\Auth;
use Stringable;
diff --git a/src/Entry/Elements/Entry.php b/src/Entry/Elements/Entry.php
index be16ee4c8f9..28995a12fe8 100644
--- a/src/Entry/Elements/Entry.php
+++ b/src/Entry/Elements/Entry.php
@@ -33,6 +33,7 @@
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
use CraftCms\Cms\Element\Queries\EntryQuery;
use CraftCms\Cms\Element\Revisions;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Actions\MoveToSection;
use CraftCms\Cms\Entry\Actions\NewChild;
use CraftCms\Cms\Entry\Actions\NewSiblingAfter;
@@ -81,7 +82,7 @@
use CraftCms\Cms\Support\Url;
use CraftCms\Cms\Twig\Attributes\AllowedInSandbox;
use CraftCms\Cms\User\Elements\User;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use DateInterval;
use DateTime;
use GraphQL\Type\Definition\Type;
@@ -2524,7 +2525,7 @@ private function maybeSetDefaultAttributes(): void
if (
! $this->_userPostDate() &&
(
- in_array($this->scenario, [self::SCENARIO_LIVE, self::SCENARIO_DEFAULT]) ||
+ in_array($this->scenario, [ElementRules::SCENARIO_LIVE, ElementRules::SCENARIO_DEFAULT]) ||
! $this->getIsDraft()
)
) {
diff --git a/src/Entry/Entries.php b/src/Entry/Entries.php
index f11530db776..7229707dcca 100644
--- a/src/Entry/Entries.php
+++ b/src/Entry/Entries.php
@@ -9,6 +9,7 @@
use CraftCms\Cms\Element\Elements;
use CraftCms\Cms\Element\Exceptions\InvalidElementException;
use CraftCms\Cms\Element\Exceptions\UnsupportedSiteException;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\Entry\Events\EntryMovedToSection;
use CraftCms\Cms\Entry\Events\MovingEntryToSection;
@@ -172,7 +173,7 @@ public function moveEntryToSection(Entry $entry, Section $section): bool
$entry->sectionId = $section->id;
// Validate
- $entry->setScenario(Element::SCENARIO_ESSENTIALS);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$entry->validate();
// If there are any errors on the URI, re-validate as disabled
diff --git a/src/Entry/Validation/EntryRules.php b/src/Entry/Validation/EntryRules.php
index 6ed21613714..fc7f5f363b9 100644
--- a/src/Entry/Validation/EntryRules.php
+++ b/src/Entry/Validation/EntryRules.php
@@ -16,18 +16,18 @@
/**
* @extends ElementRules
*
- * @property Entry $component
+ * @property Entry $subject
*/
class EntryRules extends ElementRules
{
#[Override]
- protected function defineRules(): array
+ public function rules(): array
{
- $rules = parent::defineRules();
+ $rules = parent::rules();
$rules['sectionId'] = ['nullable', 'integer', 'required_without:fieldId'];
$rules['fieldId'] = ['nullable', 'integer', function (string $attribute, mixed $value, Closure $fail) {
- if ($this->component->sectionId && $this->component->fieldId) {
+ if ($this->subject->sectionId && $this->subject->fieldId) {
$fail(t('`sectionId` and `fieldId` cannot both be set on an entry.'));
}
}];
@@ -35,36 +35,36 @@ protected function defineRules(): array
$rules['primaryOwnerId'] = ['nullable', 'integer'];
$rules['sortOrder'] = ['nullable', 'integer'];
$rules['placeInStructure'] = ['bool'];
- $rules['postDate'] = ['nullable', 'date', Rule::when($this->component->inScenarios(Entry::SCENARIO_LIVE) && ! is_null($this->component->expiryDate), ['before:expiryDate'])];
+ $rules['postDate'] = ['nullable', 'date', Rule::when($this->inScenarios(self::SCENARIO_LIVE) && ! is_null($this->subject->expiryDate), ['before:expiryDate'])];
$rules['expiryDate'] = ['nullable', 'date'];
$rules['typeId'] = [
'required',
'integer',
- Rule::when($this->component->inScenarios(Entry::SCENARIO_DEFAULT, Entry::SCENARIO_LIVE), [
+ Rule::when($this->inScenarios(self::SCENARIO_DEFAULT, self::SCENARIO_LIVE), [
function (string $attribute, int $value, Closure $fail) {
- $typeId = $this->component->getType()->id;
+ $typeId = $this->subject->getType()->id;
- if (array_any($this->component->getAvailableEntryTypes(), fn ($entryType) => $entryType->id === $typeId)) {
+ if (array_any($this->subject->getAvailableEntryTypes(), fn ($entryType) => $entryType->id === $typeId)) {
return;
}
$fail(t('{attribute} is invalid.', [
- 'attribute' => $this->component->getAttributeLabel($attribute),
+ 'attribute' => $this->subject->getAttributeLabel($attribute),
]));
},
]),
- Rule::when($this->component->inScenarios(Entry::SCENARIO_LIVE), [
+ Rule::when($this->inScenarios(self::SCENARIO_LIVE), [
function (string $attribute, int $value, Closure $fail) {
- if (! $this->component->getIsCanonical()) {
+ if (! $this->subject->getIsCanonical()) {
return;
}
- if ($this->component->isEntryTypeAllowed()) {
+ if ($this->subject->isEntryTypeAllowed()) {
return;
}
$fail(t('{type} entries are no longer allowed in this section. Please choose a different entry type.', [
- 'type' => $this->component->getType()->getUiLabel(),
+ 'type' => $this->subject->getType()->getUiLabel(),
]));
},
]),
@@ -73,11 +73,11 @@ function (string $attribute, int $value, Closure $fail) {
'nullable',
'array',
Rule::when(function () {
- if (! $this->component->inScenarios(Entry::SCENARIO_DEFAULT, Entry::SCENARIO_LIVE)) {
+ if (! $this->inScenarios(self::SCENARIO_DEFAULT, self::SCENARIO_LIVE)) {
return false;
}
- $section = $this->component->getSection();
+ $section = $this->subject->getSection();
return
$section &&
@@ -86,7 +86,7 @@ function (string $attribute, int $value, Closure $fail) {
$section->maxAuthors !== 0;
}, [
function (string $attribute, array $value, Closure $fail) {
- $section = $this->component->getSection();
+ $section = $this->subject->getSection();
if (is_null($section)) {
return;
@@ -98,12 +98,12 @@ function (string $attribute, array $value, Closure $fail) {
]));
}
- $authors = $this->component->getAuthors();
- if ($this->component->getOldAuthorIds() !== null) {
+ $authors = $this->subject->getAuthors();
+ if ($this->subject->getOldAuthorIds() !== null) {
foreach ($authors as $author) {
if (
- ! in_array($author->id, $this->component->getOldAuthorIds()) &&
- ! $author->can(sprintf('viewEntries:%s', $this->component->getSection()->uid))
+ ! in_array($author->id, $this->subject->getOldAuthorIds()) &&
+ ! $author->can(sprintf('viewEntries:%s', $this->subject->getSection()->uid))
) {
$fail(t('This user doesn’t have permission to author entries in this section.'));
break;
@@ -113,11 +113,11 @@ function (string $attribute, array $value, Closure $fail) {
},
]),
Rule::requiredIf(function () {
- if (! $this->component->inScenarios(Entry::SCENARIO_LIVE)) {
+ if (! $this->inScenarios(self::SCENARIO_LIVE)) {
return false;
}
- $section = $this->component->getSection();
+ $section = $this->subject->getSection();
if (! $section) {
return false;
diff --git a/src/Entry/Validation/EntryTypeRules.php b/src/Entry/Validation/EntryTypeRules.php
index 3fcd1c7b645..fef609369e3 100644
--- a/src/Entry/Validation/EntryTypeRules.php
+++ b/src/Entry/Validation/EntryTypeRules.php
@@ -17,8 +17,7 @@
/** @extends Ruleset */
class EntryTypeRules extends Ruleset
{
- #[\Override]
- public function defineRules(): array
+ public function rules(): array
{
$rules = [
'id' => ['nullable', 'integer'],
@@ -52,8 +51,8 @@ public function defineRules(): array
],
];
- if ($this->component->validateHandleUniqueness) {
- $rules['handle'][] = Rule::unique(Table::ENTRYTYPES, 'handle')->ignore($this->component->id)->withoutTrashed('dateDeleted');
+ if ($this->subject->validateHandleUniqueness) {
+ $rules['handle'][] = Rule::unique(Table::ENTRYTYPES, 'handle')->ignore($this->subject->id)->withoutTrashed('dateDeleted');
}
return $rules;
diff --git a/src/Field/Addresses.php b/src/Field/Addresses.php
index 2574ace6cf7..a8eaff5f3c0 100644
--- a/src/Field/Addresses.php
+++ b/src/Field/Addresses.php
@@ -15,6 +15,7 @@
use CraftCms\Cms\Element\NestedElementManager;
use CraftCms\Cms\Element\Queries\AddressQuery;
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\Conditions\EmptyFieldConditionRule;
use CraftCms\Cms\Field\Contracts\EagerLoadingFieldInterface;
use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface;
@@ -621,7 +622,7 @@ private function inputHtmlInternal(?ElementInterface $owner, bool $static = fals
#[Override]
public function getElementRules(ElementInterface $element): array
{
- if (! $element->inScenarios(Element::SCENARIO_ESSENTIALS, Element::SCENARIO_DEFAULT, Element::SCENARIO_LIVE)) {
+ if (! $element->ruleset->inScenarios(ElementRules::SCENARIO_ESSENTIALS, ElementRules::SCENARIO_DEFAULT, ElementRules::SCENARIO_LIVE)) {
return [];
}
@@ -648,15 +649,15 @@ private function validateAddresses(ElementInterface $element, AddressQuery|Eleme
->all();
$invalidAddressIds = [];
- $scenario = $element->getScenario();
+ $scenario = $element->ruleset->getScenario();
foreach ($addresses as $address) {
/** @var Address $address */
if (
- $scenario === Element::SCENARIO_ESSENTIALS ||
- ($address->enabled && $scenario === Element::SCENARIO_LIVE)
+ $scenario === ElementRules::SCENARIO_ESSENTIALS ||
+ ($address->enabled && $scenario === ElementRules::SCENARIO_LIVE)
) {
- $address->setScenario($scenario);
+ $address->ruleset->useScenario($scenario);
}
if (! $address->validate()) {
@@ -680,7 +681,7 @@ private function validateAddresses(ElementInterface $element, AddressQuery|Eleme
}
if (
- $element->inScenarios(Element::SCENARIO_LIVE) &&
+ $element->ruleset->inScenarios(ElementRules::SCENARIO_LIVE) &&
($this->minAddresses || $this->maxAddresses)
) {
$rules = [
diff --git a/src/Field/Assets.php b/src/Field/Assets.php
index 89fe826d8ca..27fc80b734b 100644
--- a/src/Field/Assets.php
+++ b/src/Field/Assets.php
@@ -10,6 +10,7 @@
use CraftCms\Cms\Asset\Data\Volume;
use CraftCms\Cms\Asset\Data\VolumeFolder;
use CraftCms\Cms\Asset\Elements\Asset;
+use CraftCms\Cms\Asset\Validation\AssetRules;
use CraftCms\Cms\Auth\SessionAuth;
use CraftCms\Cms\Cms;
use CraftCms\Cms\Cp\Html\PreviewHtml;
@@ -495,7 +496,7 @@ public function beforeElementSave(ElementInterface $element, bool $isNew): bool
$asset->setVolumeId($uploadFolder->volumeId);
$asset->uploaderId = Auth::id();
$asset->avoidFilenameConflicts = true;
- $asset->setScenario(Asset::SCENARIO_CREATE);
+ $asset->ruleset->useScenario(AssetRules::SCENARIO_CREATE);
if (Elements::saveElement($asset)) {
$assetIds[] = $asset->id;
diff --git a/src/Field/BaseRelationField.php b/src/Field/BaseRelationField.php
index 3b328572466..565bc03a799 100644
--- a/src/Field/BaseRelationField.php
+++ b/src/Field/BaseRelationField.php
@@ -26,6 +26,7 @@
use CraftCms\Cms\Element\Jobs\LocalizeRelations;
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
use CraftCms\Cms\Element\Queries\ElementQuery;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\Conditions\RelationalFieldConditionRule;
use CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface;
use CraftCms\Cms\Field\Contracts\EagerLoadingFieldInterface;
@@ -563,7 +564,7 @@ public function getSettingsHtml(): string
#[Override]
public function getElementRules(ElementInterface $element): array
{
- if (! $element->inScenarios(Element::SCENARIO_LIVE)) {
+ if (! $element->ruleset->inScenarios(ElementRules::SCENARIO_LIVE)) {
return [];
}
@@ -692,7 +693,7 @@ private static function _validateRelatedElement(ElementInterface $source, Elemen
// Prevent relational fields on this element from enforcing related element validation
self::$validatingRelatedElements = true;
- $target->setScenario(Element::SCENARIO_LIVE);
+ $target->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
$validates = $target->validate();
self::$validatingRelatedElements = false;
diff --git a/src/Field/Conditions/MoneyFieldConditionRule.php b/src/Field/Conditions/MoneyFieldConditionRule.php
index ca4130bb414..d56e171ef05 100644
--- a/src/Field/Conditions/MoneyFieldConditionRule.php
+++ b/src/Field/Conditions/MoneyFieldConditionRule.php
@@ -13,6 +13,7 @@
use CraftCms\Cms\Support\Money as MoneyHelper;
use Money\Currency;
use Money\Money as MoneyLibrary;
+use Override;
use yii\base\InvalidConfigException;
use function CraftCms\Cms\t;
@@ -21,7 +22,7 @@ class MoneyFieldConditionRule extends BaseNumberConditionRule implements FieldCo
{
use FieldConditionRuleTrait;
- #[\Override]
+ #[Override]
public function setAttributes($values, $safeOnly = true): void
{
// Hold setting of the value attribute until we have all the info we need
@@ -35,7 +36,7 @@ public function setAttributes($values, $safeOnly = true): void
$maxValue = Arr::pull($values, 'maxValue');
}
- parent::setAttributes($values, $safeOnly);
+ parent::setAttributes($values);
$field = $this->field();
@@ -59,7 +60,7 @@ public function setAttributes($values, $safeOnly = true): void
}
}
- #[\Override]
+ #[Override]
protected function inputHtml(): string
{
$field = $this->field();
@@ -95,7 +96,7 @@ protected function inputHtml(): string
return FormFields::moneyInputHtml($this->inputOptions());
}
- #[\Override]
+ #[Override]
protected function inputOptions(): array
{
/** @var Money $field */
diff --git a/src/Field/ContentBlock.php b/src/Field/ContentBlock.php
index d8ad32061c4..8b799244bca 100644
--- a/src/Field/ContentBlock.php
+++ b/src/Field/ContentBlock.php
@@ -14,6 +14,7 @@
use CraftCms\Cms\Element\NestedElementManager;
use CraftCms\Cms\Element\Queries\ContentBlockQuery;
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface;
use CraftCms\Cms\Field\Elements\ContentBlock as ContentBlockElement;
use CraftCms\Cms\Field\Enums\TranslationMethod;
@@ -649,7 +650,7 @@ private function inputHtmlInternal(mixed $value, ?ElementInterface $element, boo
#[Override]
public function getElementRules(ElementInterface $element): array
{
- if (! $element->inScenarios(Element::SCENARIO_ESSENTIALS, Element::SCENARIO_DEFAULT, Element::SCENARIO_LIVE)) {
+ if (! $element->ruleset->inScenarios(ElementRules::SCENARIO_ESSENTIALS, ElementRules::SCENARIO_DEFAULT, ElementRules::SCENARIO_LIVE)) {
return [];
}
@@ -662,8 +663,8 @@ private function validateContentBlock(ElementInterface $element, ContentBlockEle
{
$value->setOwner($element);
- if ($element->inScenarios(Element::SCENARIO_ESSENTIALS, Element::SCENARIO_LIVE)) {
- $value->setScenario($element->getScenario());
+ if ($element->ruleset->inScenarios(ElementRules::SCENARIO_ESSENTIALS, ElementRules::SCENARIO_LIVE)) {
+ $value->ruleset->useScenario($element->ruleset->getScenario());
}
if (! $value->validate()) {
diff --git a/src/Field/Contracts/FieldInterface.php b/src/Field/Contracts/FieldInterface.php
index c6383acc369..c45b6a6ac5a 100644
--- a/src/Field/Contracts/FieldInterface.php
+++ b/src/Field/Contracts/FieldInterface.php
@@ -377,7 +377,7 @@ public function prepareForElementValidation(mixed $value): mixed;
* [
* 'string',
* 'min:3',
- * Rule::when($element->inScenarios(self::SCENARIO_LIVE), ['max:12']),
+ * Rule::when($element->ruleset->inScenarios(\CraftCms\Cms\Element\Validation\ElementRules::SCENARIO_LIVE), ['max:12']),
* ]
* ```
*/
diff --git a/src/Field/Elements/ContentBlock.php b/src/Field/Elements/ContentBlock.php
index 991fbf8d8ee..459a3627dd2 100644
--- a/src/Field/Elements/ContentBlock.php
+++ b/src/Field/Elements/ContentBlock.php
@@ -14,7 +14,7 @@
use CraftCms\Cms\Field\Models\ContentBlock as ContentBlockModel;
use CraftCms\Cms\FieldLayout\FieldLayout;
use CraftCms\Cms\Gql\Interfaces\Elements\ContentBlock as ContentBlockInterface;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use GraphQL\Type\Definition\Type;
use Override;
diff --git a/src/Field/Elements/ContentBlockRules.php b/src/Field/Elements/ContentBlockRules.php
index 90543563509..54e1f4f6f2c 100644
--- a/src/Field/Elements/ContentBlockRules.php
+++ b/src/Field/Elements/ContentBlockRules.php
@@ -13,9 +13,9 @@
class ContentBlockRules extends ElementRules
{
#[Override]
- protected function defineRules(): array
+ public function rules(): array
{
- return array_merge(parent::defineRules(), [
+ return array_merge(parent::rules(), [
'fieldId' => ['nullable', 'integer'],
'ownerId' => ['nullable', 'integer'],
'primaryOwnerId' => ['nullable', 'integer'],
diff --git a/src/Field/Field.php b/src/Field/Field.php
index 663bcebcef0..d35dd02a62d 100644
--- a/src/Field/Field.php
+++ b/src/Field/Field.php
@@ -48,7 +48,6 @@
use GraphQL\Type\Definition\Type;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Contracts\Database\Query\Expression;
-use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
@@ -488,18 +487,6 @@ public function __toString(): string
return t($this->name, category: 'site') ?: static::class;
}
- #[Override]
- public function attributes(): array
- {
- return Collection::make($this->settingsAttributes())
- ->reject(fn ($name): bool => in_array($name, [
- 'validateHandleUniqueness',
- 'layoutElement',
- 'static',
- ]))
- ->all();
- }
-
#[Override]
public function attributeLabels(): array
{
diff --git a/src/Field/Matrix.php b/src/Field/Matrix.php
index b64238fb45d..1749be7d71f 100644
--- a/src/Field/Matrix.php
+++ b/src/Field/Matrix.php
@@ -22,6 +22,7 @@
use CraftCms\Cms\Element\NestedElementManager;
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
use CraftCms\Cms\Element\Queries\EntryQuery;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Data\EntryType;
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\Entry\EntryTypes;
@@ -1267,7 +1268,7 @@ private function defaultCreateButtonLabel(): string
#[Override]
public function getElementRules(ElementInterface $element): array
{
- if (! $element->inScenarios(Element::SCENARIO_ESSENTIALS, Element::SCENARIO_DEFAULT, Element::SCENARIO_LIVE)) {
+ if (! $element->ruleset->inScenarios(ElementRules::SCENARIO_ESSENTIALS, ElementRules::SCENARIO_DEFAULT, ElementRules::SCENARIO_LIVE)) {
return [];
}
@@ -1301,15 +1302,15 @@ private function validateEntries(ElementInterface $element, string $attribute, E
->all();
$invalidEntryIds = [];
- $scenario = $element->getScenario();
+ $scenario = $element->ruleset->getScenario();
foreach ($entries as $entry) {
$entry->setOwner($element);
if (! $entry->enabled) {
- $entry->setScenario(Element::SCENARIO_ESSENTIALS);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
} else {
- $entry->setScenario($scenario);
+ $entry->ruleset->useScenario($scenario);
}
if (! $entry->validate()) {
@@ -1341,7 +1342,7 @@ private function validateEntries(ElementInterface $element, string $attribute, E
}
if (
- $element->inScenarios(Element::SCENARIO_LIVE) &&
+ $element->ruleset->inScenarios(ElementRules::SCENARIO_LIVE) &&
($this->minEntries || $this->maxEntries)
) {
$rules = array_filter([
diff --git a/src/FieldLayout/FieldLayout.php b/src/FieldLayout/FieldLayout.php
index 170fa1043ee..dcc1ed4cddc 100644
--- a/src/FieldLayout/FieldLayout.php
+++ b/src/FieldLayout/FieldLayout.php
@@ -46,7 +46,7 @@
class FieldLayout extends Component
{
use Validates {
- getAttributes as traitGetAttributes;
+ validationData as traitValidationData;
}
public ?int $id = null;
@@ -188,9 +188,9 @@ public function getRules(): array
}
#[Override]
- public function getAttributes(): array
+ public function validationData(): array
{
- return array_merge($this->traitGetAttributes(), [
+ return array_merge($this->traitValidationData(), [
'customFields' => $this->getCustomFields(),
]);
}
diff --git a/src/Gql/Resolvers/ElementMutationResolver.php b/src/Gql/Resolvers/ElementMutationResolver.php
index 48a5ab0f363..be84af5e28a 100644
--- a/src/Gql/Resolvers/ElementMutationResolver.php
+++ b/src/Gql/Resolvers/ElementMutationResolver.php
@@ -6,6 +6,7 @@
use craft\base\ElementInterface;
use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Gql\Events\AfterPopulateElement;
use CraftCms\Cms\Gql\Events\BeforePopulateElement;
use CraftCms\Cms\Gql\Exceptions\GqlException;
@@ -95,8 +96,8 @@ protected function populateElementWithData(ElementInterface $element, array $arg
protected function saveElement(ElementInterface $element): ElementInterface
{
/** @var Element $element */
- if ($element->enabled && $element->inScenarios(Element::SCENARIO_DEFAULT)) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ if ($element->enabled && $element->ruleset->inScenarios(ElementRules::SCENARIO_DEFAULT)) {
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
}
$isNotNew = $element->id;
diff --git a/src/Gql/Resolvers/Mutations/Asset.php b/src/Gql/Resolvers/Mutations/Asset.php
index a53e0be7332..2ca2aed3150 100644
--- a/src/Gql/Resolvers/Mutations/Asset.php
+++ b/src/Gql/Resolvers/Mutations/Asset.php
@@ -11,6 +11,7 @@
use CraftCms\Cms\Asset\Events\AfterReplaceAsset;
use CraftCms\Cms\Asset\Events\BeforeReplaceAsset;
use CraftCms\Cms\Asset\Exceptions\AssetDisallowedExtensionException;
+use CraftCms\Cms\Asset\Validation\AssetRules;
use CraftCms\Cms\Cms;
use CraftCms\Cms\Database\Table;
use CraftCms\Cms\Gql\Resolvers\ElementMutationResolver;
@@ -99,7 +100,7 @@ public function saveAsset(mixed $source, array $arguments, mixed $context, Resol
$asset = $this->populateElementWithData($asset, $arguments, $resolveInfo);
- $triggerReplaceEvents = $asset->getScenario() === AssetElement::SCENARIO_REPLACE;
+ $triggerReplaceEvents = $asset->ruleset->getScenario() === AssetRules::SCENARIO_REPLACE;
if ($triggerReplaceEvents) {
event($event = new BeforeReplaceAsset(
@@ -152,9 +153,9 @@ protected function populateElementWithData(ElementInterface $element, array $arg
$element = parent::populateElementWithData($element, $arguments, $resolveInfo);
if (! empty($fileInformation) && $this->handleUpload($element, $fileInformation)) {
- $element->setScenario($element->id
- ? AssetElement::SCENARIO_REPLACE
- : AssetElement::SCENARIO_CREATE
+ $element->ruleset->useScenario($element->id
+ ? AssetRules::SCENARIO_REPLACE
+ : AssetRules::SCENARIO_CREATE
);
}
diff --git a/src/Gql/Resolvers/Mutations/Entry.php b/src/Gql/Resolvers/Mutations/Entry.php
index 4d722344fcd..3e9bf35e557 100644
--- a/src/Gql/Resolvers/Mutations/Entry.php
+++ b/src/Gql/Resolvers/Mutations/Entry.php
@@ -6,6 +6,7 @@
use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\Queries\EntryQuery;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Data\EntryType;
use CraftCms\Cms\Entry\Elements\Entry as EntryElement;
use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface;
@@ -71,7 +72,7 @@ public function saveEntry(mixed $source, array $arguments, mixed $context, Resol
// TODO refactor saving draft to its own method in 4.0
if (array_key_exists('draftId', $arguments)) {
- $entry->setScenario(Element::SCENARIO_ESSENTIALS);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
}
$canIdentify = ! empty($arguments['id']) || ! empty($arguments['uid']) || ! empty($arguments['draftId']);
@@ -79,7 +80,7 @@ public function saveEntry(mixed $source, array $arguments, mixed $context, Resol
$entry = $this->populateElementWithData($entry, $arguments, $resolveInfo);
if (array_key_exists('asUnpublishedDraft', $arguments) && $arguments['asUnpublishedDraft']) {
- $entry->setScenario(Element::SCENARIO_ESSENTIALS);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
Drafts::saveElementAsDraft(
$entry,
creatorId: Auth::id(),
diff --git a/src/Http/Controllers/Assets/ImageEditorController.php b/src/Http/Controllers/Assets/ImageEditorController.php
index b3beabe9503..9c44fc5565f 100644
--- a/src/Http/Controllers/Assets/ImageEditorController.php
+++ b/src/Http/Controllers/Assets/ImageEditorController.php
@@ -7,6 +7,7 @@
use CraftCms\Cms\Asset\Assets;
use CraftCms\Cms\Asset\Concerns\EnforcesVolumePermissions;
use CraftCms\Cms\Asset\Elements\Asset;
+use CraftCms\Cms\Asset\Validation\AssetRules;
use CraftCms\Cms\Cms;
use CraftCms\Cms\Element\Elements;
use CraftCms\Cms\Http\RespondsWithFlash;
@@ -220,7 +221,7 @@ public function save(Request $request, Elements $elements): Response
$newAsset = new Asset;
$newAsset->avoidFilenameConflicts = true;
- $newAsset->setScenario(Asset::SCENARIO_CREATE);
+ $newAsset->ruleset->useScenario(AssetRules::SCENARIO_CREATE);
$newAsset->tempFilePath = $finalImage;
$newAsset->setFilename($asset->getFilename());
diff --git a/src/Http/Controllers/Assets/UploadController.php b/src/Http/Controllers/Assets/UploadController.php
index 15fae9a5530..a938a819b02 100644
--- a/src/Http/Controllers/Assets/UploadController.php
+++ b/src/Http/Controllers/Assets/UploadController.php
@@ -11,6 +11,7 @@
use CraftCms\Cms\Asset\Exceptions\AssetDisallowedExtensionException;
use CraftCms\Cms\Asset\Exceptions\UploadFailedException;
use CraftCms\Cms\Asset\Folders;
+use CraftCms\Cms\Asset\Validation\AssetRules;
use CraftCms\Cms\Cms;
use CraftCms\Cms\Element\Conditions\ElementCondition;
use CraftCms\Cms\Element\Elements;
@@ -114,7 +115,7 @@ public function upload(Request $request): Response
$asset->title = AssetsHelper::filename2Title(pathinfo($originalFilename, PATHINFO_FILENAME));
}
- $asset->setScenario(Asset::SCENARIO_CREATE);
+ $asset->ruleset->useScenario(AssetRules::SCENARIO_CREATE);
$result = $this->elements->saveElement($asset);
// In case of error, let user know about it.
@@ -136,7 +137,7 @@ public function upload(Request $request): Response
// move it into the original target destination
$asset->newFilename = $originalFilename;
$asset->newFolderId = $originalFolder->id;
- $asset->setScenario(Asset::SCENARIO_MOVE);
+ $asset->ruleset->useScenario(AssetRules::SCENARIO_MOVE);
if (! $this->elements->saveElement($asset)) {
return $this->asModelFailure($asset);
diff --git a/src/Http/Controllers/Auth/SetPasswordController.php b/src/Http/Controllers/Auth/SetPasswordController.php
index 9a6e5ac19c2..f06a74ec770 100644
--- a/src/Http/Controllers/Auth/SetPasswordController.php
+++ b/src/Http/Controllers/Auth/SetPasswordController.php
@@ -13,6 +13,7 @@
use CraftCms\Cms\Support\Url;
use CraftCms\Cms\User\Elements\User;
use CraftCms\Cms\User\Users;
+use CraftCms\Cms\User\Validation\UserRules;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password as PasswordFacade;
@@ -72,7 +73,7 @@ public function store(Request $request, Users $users, Elements $elements): Respo
],
function (User $user, string $password) use ($elements) {
$user->newPassword = $password;
- $user->setScenario(User::SCENARIO_PASSWORD);
+ $user->ruleset->useScenario(UserRules::SCENARIO_PASSWORD);
if (! $elements->saveElement($user)) {
throw new RuntimeException('Couldn’t update password.');
diff --git a/src/Http/Controllers/Entries/CreateEntryController.php b/src/Http/Controllers/Entries/CreateEntryController.php
index 4cc6b778733..3f1fad7b98f 100644
--- a/src/Http/Controllers/Entries/CreateEntryController.php
+++ b/src/Http/Controllers/Entries/CreateEntryController.php
@@ -6,9 +6,9 @@
use CraftCms\Cms\Cp\RequestedSite;
use CraftCms\Cms\Element\Drafts;
-use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\ElementHelper;
use CraftCms\Cms\Element\Enums\PropagationMethod;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\Entry\Entries;
use CraftCms\Cms\Http\RespondsWithFlash;
@@ -95,7 +95,7 @@ public function __invoke(Drafts $drafts, Users $users): Response
}
// Save it
- $entry->setScenario(Element::SCENARIO_ESSENTIALS);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$success = $drafts->saveElementAsDraft($entry, $user->id, markAsSaved: false);
// Resume time
diff --git a/src/Http/Controllers/Entries/StoreEntryController.php b/src/Http/Controllers/Entries/StoreEntryController.php
index 7f5b8acce12..8c91b2cf519 100644
--- a/src/Http/Controllers/Entries/StoreEntryController.php
+++ b/src/Http/Controllers/Entries/StoreEntryController.php
@@ -6,10 +6,10 @@
use CraftCms\Cms\Auth\Concerns\EnforcesPermissions;
use CraftCms\Cms\Cp\Html\ElementHtml;
-use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\Elements;
use CraftCms\Cms\Element\Exceptions\InvalidElementException;
use CraftCms\Cms\Element\Exceptions\UnsupportedSiteException;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\Entry\Entries;
use CraftCms\Cms\Http\RespondsWithFlash;
@@ -63,7 +63,7 @@ public function __invoke(): Response
// Save the entry (finally!)
if ($entry->enabled && $entry->getEnabledForSite()) {
- $entry->setScenario(Element::SCENARIO_LIVE);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
}
$isNotNew = (bool) $entry->id;
diff --git a/src/Http/Controllers/MatrixController.php b/src/Http/Controllers/MatrixController.php
index 2832071e7f4..22465d301cf 100644
--- a/src/Http/Controllers/MatrixController.php
+++ b/src/Http/Controllers/MatrixController.php
@@ -5,12 +5,12 @@
namespace CraftCms\Cms\Http\Controllers;
use CraftCms\Cms\Element\Drafts;
-use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\ElementCollection;
use CraftCms\Cms\Element\ElementHelper;
use CraftCms\Cms\Element\Elements;
use CraftCms\Cms\Element\Exceptions\InvalidElementException;
use CraftCms\Cms\Element\Queries\EntryQuery;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\Entry\EntryTypes;
use CraftCms\Cms\Field\Matrix;
@@ -134,7 +134,7 @@ public function createEntry(Request $request): Response
Gate::authorize('save', $entry);
- $entry->setScenario(Element::SCENARIO_ESSENTIALS);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
if (! $this->drafts->saveElementAsDraft($entry, $request->user()->id, markAsSaved: false)) {
return $this->asFailure(mb_ucfirst(t('Couldn’t create {type}.', [
diff --git a/src/Http/Controllers/Users/AddressesController.php b/src/Http/Controllers/Users/AddressesController.php
index 6b02c6ff37f..192507ad492 100644
--- a/src/Http/Controllers/Users/AddressesController.php
+++ b/src/Http/Controllers/Users/AddressesController.php
@@ -7,6 +7,7 @@
use CraftCms\Cms\Address\Elements\Address;
use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\Elements;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Http\RespondsWithFlash;
use CraftCms\Cms\Http\Responses\CpScreenResponse;
use CraftCms\Cms\Support\Html;
@@ -68,14 +69,14 @@ public function store(Request $request, Elements $elements): Response
Gate::authorize('save', $address);
// Addresses have no status, and the default element save controller also sets the address scenario to live
- $address->setScenario(Element::SCENARIO_LIVE);
+ $address->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
// Name attributes
$this->populateNameAttributes($request, $address);
// All safe attributes
$safeAttributes = [];
- foreach ($address->safeAttributes() as $name) {
+ foreach (array_keys($address->ruleset->rules()) as $name) {
if (in_array($name, ['id', 'uid', 'ownerId'])) {
continue;
}
diff --git a/src/Http/Controllers/Users/PasswordController.php b/src/Http/Controllers/Users/PasswordController.php
index f76221ce530..3f92a98dea4 100644
--- a/src/Http/Controllers/Users/PasswordController.php
+++ b/src/Http/Controllers/Users/PasswordController.php
@@ -13,6 +13,7 @@
use CraftCms\Cms\Http\Responses\CpScreenResponse;
use CraftCms\Cms\User\Elements\User;
use CraftCms\Cms\User\Users;
+use CraftCms\Cms\User\Validation\UserRules;
use CraftCms\Cms\View\LegacyAssets\AuthMethodSetupAsset;
use CraftCms\Cms\View\LegacyAssets\InternalAssetRegistry;
use Exception;
@@ -65,7 +66,7 @@ public function store(Request $request, Elements $elements): Response
}
$user->newPassword = $validated['newPassword'];
- $user->setScenario(User::SCENARIO_PASSWORD);
+ $user->ruleset->useScenario(UserRules::SCENARIO_PASSWORD);
if (! $elements->saveElement($user)) {
return $this->asFailure(
diff --git a/src/Http/Controllers/Users/SaveUserController.php b/src/Http/Controllers/Users/SaveUserController.php
index b98894d8eed..3bf0a89d357 100644
--- a/src/Http/Controllers/Users/SaveUserController.php
+++ b/src/Http/Controllers/Users/SaveUserController.php
@@ -10,8 +10,8 @@
use CraftCms\Cms\Auth\Concerns\ConfirmsPasswords;
use CraftCms\Cms\Config\GeneralConfig;
use CraftCms\Cms\Edition;
-use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\Elements;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Http\RespondsWithFlash;
use CraftCms\Cms\Image\ImageHelper;
use CraftCms\Cms\ProjectConfig\ProjectConfig;
@@ -23,6 +23,7 @@
use CraftCms\Cms\Support\Url;
use CraftCms\Cms\User\Elements\User;
use CraftCms\Cms\User\Users;
+use CraftCms\Cms\User\Validation\UserRules;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
@@ -257,9 +258,9 @@ public function __invoke(Request $request): Response
// Don't validate required custom fields if it's public registration
if (! $isPublicRegistration || ($userSettings['validateOnPublicRegistration'] ?? false)) {
- $user->setScenario(Element::SCENARIO_LIVE);
+ $user->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
} else {
- $user->setScenario(User::SCENARIO_REGISTRATION);
+ $user->ruleset->useScenario(UserRules::SCENARIO_REGISTRATION);
}
// Manually validate the user so we can pass $clearErrors=false
diff --git a/src/Http/Controllers/Users/UsersController.php b/src/Http/Controllers/Users/UsersController.php
index f3c18211f60..81eb56d3936 100644
--- a/src/Http/Controllers/Users/UsersController.php
+++ b/src/Http/Controllers/Users/UsersController.php
@@ -9,8 +9,8 @@
use CraftCms\Cms\Auth\Concerns\EnforcesPermissions;
use CraftCms\Cms\Edition;
use CraftCms\Cms\Element\Drafts;
-use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\Elements;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\Http\RespondsWithFlash;
use CraftCms\Cms\Http\Responses\CpScreenResponse;
@@ -59,7 +59,7 @@ public function create(Request $request, Drafts $drafts): Response
$this->authorize('save', $user);
- $user->setScenario(Element::SCENARIO_ESSENTIALS);
+ $user->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
if (! $drafts->saveElementAsDraft($user, $request->user()->id, markAsSaved: false)) {
return $this->asModelFailure($user, mb_ucfirst(t('Couldn’t create {type}.', [
'type' => User::lowerDisplayName(),
diff --git a/src/Image/ImageTransformHelper.php b/src/Image/ImageTransformHelper.php
index f20af732c0e..ba2cf888e41 100644
--- a/src/Image/ImageTransformHelper.php
+++ b/src/Image/ImageTransformHelper.php
@@ -18,6 +18,7 @@
use CraftCms\Cms\Support\Facades\Path;
use CraftCms\Cms\Support\File;
use CraftCms\Cms\Support\Str;
+use CraftCms\Cms\Support\Utils;
use CraftCms\Cms\Validation\Rules\ColorRule;
use Illuminate\Filesystem\LocalFilesystemAdapter;
use Illuminate\Support\Facades\Log;
@@ -98,7 +99,7 @@ public static function extendTransform(ImageTransform $transform, array $paramet
// Don't change the same transform
$transform = clone $transform;
- $attributes = $transform->attributes();
+ $attributes = Utils::getPublicAttributes($transform);
$nullables = [
'id',
diff --git a/src/Plugin/PluginSettings.php b/src/Plugin/PluginSettings.php
index 2d006c9fefa..80bba19c89a 100644
--- a/src/Plugin/PluginSettings.php
+++ b/src/Plugin/PluginSettings.php
@@ -4,10 +4,6 @@
namespace CraftCms\Cms\Plugin;
-use CraftCms\Cms\Validation\Concerns\Validates;
-use CraftCms\Cms\Validation\Contracts\Validatable;
+use CraftCms\Cms\Component\Component;
-abstract class PluginSettings implements Validatable
-{
- use Validates;
-}
+abstract class PluginSettings extends Component {}
diff --git a/src/Plugin/Plugins.php b/src/Plugin/Plugins.php
index b12ec83862d..1db95415e3e 100644
--- a/src/Plugin/Plugins.php
+++ b/src/Plugin/Plugins.php
@@ -686,7 +686,7 @@ public function savePluginSettings(PluginInterface $plugin, array $settings): bo
}
// Update the plugin’s settings in the project config
- $pluginSettings = ProjectConfigHelper::packAssociativeArrays($pluginSettings->getAttributes());
+ $pluginSettings = ProjectConfigHelper::packAssociativeArrays($pluginSettings->validationData());
app(ProjectConfig::class)->set(
path: ProjectConfig::PATH_PLUGINS.'.'.$plugin->handle.'.settings',
value: $pluginSettings,
diff --git a/src/Section/Data/Section.php b/src/Section/Data/Section.php
index 5e3a3ddaf9a..05995ed1c2b 100644
--- a/src/Section/Data/Section.php
+++ b/src/Section/Data/Section.php
@@ -22,7 +22,7 @@
use CraftCms\Cms\Support\Facades\Sites;
use CraftCms\Cms\Support\Str;
use CraftCms\Cms\Support\Url;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Stringable;
diff --git a/src/Section/Data/SectionSiteSettings.php b/src/Section/Data/SectionSiteSettings.php
index 45bb8d63e5d..4f21c303069 100644
--- a/src/Section/Data/SectionSiteSettings.php
+++ b/src/Section/Data/SectionSiteSettings.php
@@ -9,7 +9,7 @@
use CraftCms\Cms\Site\Data\Site;
use CraftCms\Cms\Support\Facades\Sections;
use CraftCms\Cms\Support\Facades\Sites;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use RuntimeException;
#[Ruleset(SectionSiteSettingsRules::class)]
diff --git a/src/Section/Sections.php b/src/Section/Sections.php
index 5d4ef8f4d25..408dc31f184 100644
--- a/src/Section/Sections.php
+++ b/src/Section/Sections.php
@@ -14,6 +14,7 @@
use CraftCms\Cms\Element\Enums\PropagationMethod;
use CraftCms\Cms\Element\Jobs\ApplyNewPropagationMethod;
use CraftCms\Cms\Element\Jobs\ResaveElements;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Data\EntryType;
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\ProjectConfig\Events\ConfigEvent;
@@ -904,7 +905,7 @@ private function ensureSingleEntry(Section $section, ?array $siteSettings = null
}
// Validate first
- $entry->setScenario(Element::SCENARIO_ESSENTIALS);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$entry->validate();
// If there are any errors on the URI, re-validate as disabled
diff --git a/src/Section/Validation/SectionRules.php b/src/Section/Validation/SectionRules.php
index 6a236744617..bc78859e209 100644
--- a/src/Section/Validation/SectionRules.php
+++ b/src/Section/Validation/SectionRules.php
@@ -14,15 +14,13 @@
use CraftCms\Cms\Validation\Ruleset;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
-use Override;
use function CraftCms\Cms\t;
/** @extends Ruleset */
class SectionRules extends Ruleset
{
- #[Override]
- public function defineRules(): array
+ public function rules(): array
{
return [
'id' => ['nullable', 'integer'],
@@ -35,7 +33,7 @@ public function defineRules(): array
'string',
'max:255',
new HandleRule(['id', 'dateCreated', 'dateUpdated', 'uid', 'title']),
- Rule::unique(Table::SECTIONS)->ignore($this->component->id)->withoutTrashed('dateDeleted'),
+ Rule::unique(Table::SECTIONS)->ignore($this->subject->id)->withoutTrashed('dateDeleted'),
],
'entryTypes' => ['required'],
'type' => ['required', Rule::enum(SectionType::class)],
@@ -62,25 +60,25 @@ private function validateSiteSettings(Closure $fail): void
{
// If this is an existing section, make sure they aren't moving it to a
// completely different set of sites in one fell swoop
- if ($this->component->id) {
+ if ($this->subject->id) {
$currentSiteIds = DB::table(Table::SECTIONS_SITES)
- ->where('sectionId', $this->component->id)
+ ->where('sectionId', $this->subject->id)
->pluck('siteId')
->all();
- if (empty(array_intersect($currentSiteIds, array_keys($this->component->getSiteSettings())))) {
+ if (empty(array_intersect($currentSiteIds, array_keys($this->subject->getSiteSettings())))) {
$fail(t('At least one currently-enabled site must remain enabled.'));
}
}
- foreach ($this->component->getSiteSettings() as $i => $siteSettings) {
+ foreach ($this->subject->getSiteSettings() as $i => $siteSettings) {
if ($siteSettings->validate()) {
continue;
}
foreach ($siteSettings->errors()->getMessages() as $a => $errors) {
foreach ($errors as $error) {
- $this->component->errors()->add("siteSettings[$i].$a", $error);
+ $this->subject->errors()->add("siteSettings[$i].$a", $error);
}
}
}
diff --git a/src/Section/Validation/SectionSiteSettingsRules.php b/src/Section/Validation/SectionSiteSettingsRules.php
index b7a3b1e50bc..fb5a29fa8c4 100644
--- a/src/Section/Validation/SectionSiteSettingsRules.php
+++ b/src/Section/Validation/SectionSiteSettingsRules.php
@@ -10,18 +10,16 @@
use CraftCms\Cms\Validation\Rules\SiteIdRule;
use CraftCms\Cms\Validation\Rules\UriFormatRule;
use CraftCms\Cms\Validation\Ruleset;
-use Override;
use Throwable;
/** @extends Ruleset */
class SectionSiteSettingsRules extends Ruleset
{
- #[Override]
- public function defineRules(): array
+ public function rules(): array
{
$section = null;
try {
- $section = $this->component->getSection();
+ $section = $this->subject->getSection();
} catch (Throwable) {
}
diff --git a/src/Site/Data/Site.php b/src/Site/Data/Site.php
index 79f0ffd2e72..e32a929e4ba 100644
--- a/src/Site/Data/Site.php
+++ b/src/Site/Data/Site.php
@@ -13,7 +13,7 @@
use CraftCms\Cms\Support\Facades\Sites;
use CraftCms\Cms\Translation\Locale;
use CraftCms\Cms\Twig\Attributes\AllowedInSandbox;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use DateTimeInterface;
use Override;
use RuntimeException;
diff --git a/src/Site/Validation/SiteRules.php b/src/Site/Validation/SiteRules.php
index 2eba8d31afc..4d099af2f72 100644
--- a/src/Site/Validation/SiteRules.php
+++ b/src/Site/Validation/SiteRules.php
@@ -16,8 +16,7 @@
/** @extends Ruleset */
class SiteRules extends Ruleset
{
- #[\Override]
- public function defineRules(): array
+ public function rules(): array
{
return [
'language' => [
@@ -30,7 +29,7 @@ public function defineRules(): array
new HandleRule(['id', 'dateCreated', 'dateUpdated', 'uid', 'title']),
Rule::when(
Cms::isInstalled(),
- [new Unique(Table::SITES, 'handle')->ignore($this->component->id)->withoutTrashed('dateDeleted')],
+ [new Unique(Table::SITES, 'handle')->ignore($this->subject->id)->withoutTrashed('dateDeleted')],
),
],
'name' => [
@@ -38,7 +37,7 @@ public function defineRules(): array
'string',
Rule::when(
Cms::isInstalled(),
- [new Unique(Table::SITES, 'name')->ignore($this->component->id)->withoutTrashed('dateDeleted')],
+ [new Unique(Table::SITES, 'name')->ignore($this->subject->id)->withoutTrashed('dateDeleted')],
),
],
];
diff --git a/src/User/Commands/SetPasswordCommand.php b/src/User/Commands/SetPasswordCommand.php
index bc9b31973bf..c093f8384d3 100644
--- a/src/User/Commands/SetPasswordCommand.php
+++ b/src/User/Commands/SetPasswordCommand.php
@@ -6,7 +6,7 @@
use CraftCms\Cms\Console\CraftCommand;
use CraftCms\Cms\Element\Elements;
-use CraftCms\Cms\User\Elements\User;
+use CraftCms\Cms\User\Validation\UserRules;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use Override;
@@ -31,7 +31,7 @@ public function handle(Elements $elements): int
return self::FAILURE;
}
- $user->setScenario(User::SCENARIO_PASSWORD);
+ $user->ruleset->useScenario(UserRules::SCENARIO_PASSWORD);
$user->newPassword = $this->argument('password');
$this->components->task(
diff --git a/src/User/Elements/User.php b/src/User/Elements/User.php
index f23e0451d7c..e7f3d1ce37a 100644
--- a/src/User/Elements/User.php
+++ b/src/User/Elements/User.php
@@ -60,7 +60,7 @@
use CraftCms\Cms\User\Notifications\ResetPasswordNotification;
use CraftCms\Cms\User\Notifications\VerifyEmailNotification;
use CraftCms\Cms\User\Validation\UserRules;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use DateInterval;
use DateTime;
use DateTimeZone;
@@ -159,28 +159,6 @@ class User extends Element implements AuthenticatableContract, AuthorizableContr
public const string STATUS_LOCKED = 'locked';
- // Validation scenarios
- // -------------------------------------------------------------------------
-
- /**
- * @since 4.4.8
- */
- public const string SCENARIO_ACTIVATION = 'activation';
-
- public const string SCENARIO_REGISTRATION = 'registration';
-
- public const string SCENARIO_PASSWORD = 'password';
-
- #[Override]
- public function scenarios(): array
- {
- return array_merge(parent::scenarios(), [
- self::SCENARIO_PASSWORD => ['newPassword'],
- self::SCENARIO_REGISTRATION => ['username', 'email', 'newPassword'],
- self::SCENARIO_ACTIVATION => ['username', 'email'],
- ]);
- }
-
public function getAuthIdentifierName(): string
{
return 'id';
diff --git a/src/User/Users.php b/src/User/Users.php
index f430e81a8c2..9ad0a7307c0 100644
--- a/src/User/Users.php
+++ b/src/User/Users.php
@@ -9,6 +9,7 @@
use CraftCms\Cms\Asset\Elements\Asset;
use CraftCms\Cms\Asset\Exceptions\ImageException;
use CraftCms\Cms\Asset\Exceptions\VolumeException;
+use CraftCms\Cms\Asset\Validation\AssetRules;
use CraftCms\Cms\Auth\Enums\CpAuthPath;
use CraftCms\Cms\Cms;
use CraftCms\Cms\Database\Table;
@@ -56,6 +57,7 @@
use CraftCms\Cms\User\Events\UserUnlocked;
use CraftCms\Cms\User\Events\UserUnsuspended;
use CraftCms\Cms\User\Models\User as UserModel;
+use CraftCms\Cms\User\Validation\UserRules;
use CraftCms\DependencyAwareCache\Dependency\TagDependency;
use DateTime;
use Illuminate\Auth\Passwords\PasswordBroker;
@@ -364,7 +366,7 @@ public function saveUserPhoto(
$filename = AssetsService::getNameReplacementInFolder($filename, $folderId);
$photo = new Asset;
- $photo->setScenario(Asset::SCENARIO_CREATE);
+ $photo->ruleset->useScenario(AssetRules::SCENARIO_CREATE);
$photo->tempFilePath = $fileLocation;
$photo->setFilename($filename);
$photo->setMimeType(File::getMimeType($fileLocation, checkExtension: false) ?? $mimeType);
@@ -397,7 +399,7 @@ public function relocateUserPhoto(User $user): void
return;
}
- $photo->setScenario(Asset::SCENARIO_MOVE);
+ $photo->ruleset->useScenario(AssetRules::SCENARIO_MOVE);
$photo->avoidFilenameConflicts = true;
$photo->newFolderId = $folderId;
$this->elements->saveElement($photo);
@@ -576,7 +578,7 @@ public function activateUser(User $user): void
}
$originalUser = clone $user;
- $user->setScenario(User::SCENARIO_ACTIVATION);
+ $user->ruleset->useScenario(UserRules::SCENARIO_ACTIVATION);
$user->active = true;
$user->pending = false;
$user->locked = false;
@@ -927,7 +929,7 @@ public function setVerificationCodeOnUser(User $user): string
$userModel->save();
$originalUser = clone $user;
- $user->setScenario(User::SCENARIO_ACTIVATION);
+ $user->ruleset->useScenario(UserRules::SCENARIO_ACTIVATION);
$user->pending = $userModel->pending;
if (! $user->validate()) {
diff --git a/src/User/Validation/UserRules.php b/src/User/Validation/UserRules.php
index 604abf13570..b612301db1f 100644
--- a/src/User/Validation/UserRules.php
+++ b/src/User/Validation/UserRules.php
@@ -8,6 +8,7 @@
use CraftCms\Cms\Database\Table;
use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\FieldLayout\LayoutElements\users\FullNameField;
+use CraftCms\Cms\Support\Arr;
use CraftCms\Cms\User\Elements\User;
use CraftCms\Cms\User\Validation\Rules\UsernameRule;
use CraftCms\Cms\User\Validation\Rules\UserPasswordRule;
@@ -22,10 +23,19 @@
/**
* @extends ElementRules
*
- * @property User $component
+ * @property User $subject
*/
class UserRules extends ElementRules
{
+ /**
+ * @since 4.4.8
+ */
+ public const string SCENARIO_ACTIVATION = 'activation';
+
+ public const string SCENARIO_REGISTRATION = 'registration';
+
+ public const string SCENARIO_PASSWORD = 'password';
+
private const array TRIMMABLE_ATTRIBUTES = [
'email',
'unverifiedEmail',
@@ -36,31 +46,31 @@ class UserRules extends ElementRules
];
#[Override]
- public function prepareForValidation(?array $attributeNames = null): void
+ public function prepareForValidation(): void
{
- parent::prepareForValidation($attributeNames);
+ parent::prepareForValidation();
- $attributesToTrim = is_null($attributeNames)
+ $attributesToTrim = is_null($this->validationAttributes)
? self::TRIMMABLE_ATTRIBUTES
- : array_intersect(self::TRIMMABLE_ATTRIBUTES, $attributeNames);
+ : array_intersect(self::TRIMMABLE_ATTRIBUTES, $this->validationAttributes);
foreach ($attributesToTrim as $attribute) {
- $value = $this->component->{$attribute};
+ $value = $this->subject->{$attribute};
if (is_string($value)) {
- $this->component->{$attribute} = trim($value);
+ $this->subject->{$attribute} = trim($value);
}
}
}
#[Override]
- protected function defineRules(): array
+ public function rules(): array
{
- $rules = parent::defineRules();
+ $rules = parent::rules();
- $treatAsActive = $this->component->getIsCredentialed() || $this->component->inScenarios(
- User::SCENARIO_REGISTRATION,
- User::SCENARIO_ACTIVATION,
+ $treatAsActive = $this->subject->getIsCredentialed() || $this->inScenarios(
+ self::SCENARIO_REGISTRATION,
+ self::SCENARIO_ACTIVATION,
);
$unique = fn (string $column) => new UniqueCaseInsensitiveRule(Table::USERS, $column)
@@ -68,7 +78,7 @@ protected function defineRules(): array
->where('active', true)
->orWhere('pending', true)
)
- ->ignore($this->component->id);
+ ->ignore($this->subject->id);
$noProtocol = function ($attribute, $value, $fail) {
if (str_contains($value, '://')) {
@@ -88,7 +98,7 @@ protected function defineRules(): array
'nullable',
'string',
'max:255',
- Rule::requiredIf(fn () => ! $this->component->getIsDraft()),
+ Rule::requiredIf(fn () => ! $this->subject->getIsDraft()),
'email',
],
'unverifiedEmail' => [
@@ -106,12 +116,12 @@ protected function defineRules(): array
]);
$requiredNameField = (fn (bool $requiredWhenFirstAndLastNameFields) => Cms::config()->showFirstAndLastNameFields === $requiredWhenFirstAndLastNameFields
- && ($this->component
+ && ($this->subject
->getFieldLayout()
- ->getFirstVisibleElementByType(FullNameField::class, $this->component)
+ ->getFirstVisibleElementByType(FullNameField::class, $this->subject)
->required ?? false));
- if ($this->component->inScenarios(User::SCENARIO_LIVE)) {
+ if ($this->inScenarios(self::SCENARIO_LIVE)) {
$rules['firstName'][] = Rule::requiredIf($requiredNameField(true));
$rules['lastName'][] = Rule::requiredIf($requiredNameField(true));
$rules['fullName'][] = Rule::requiredIf($requiredNameField(false));
@@ -135,23 +145,43 @@ protected function defineRules(): array
$currentPassword = null;
- if (isset($this->component->id) && $this->component->passwordResetRequired) {
+ if (isset($this->subject->id) && $this->subject->passwordResetRequired) {
$currentPassword = DB::table(Table::USERS)
- ->where('id', $this->component->id)
+ ->where('id', $this->subject->id)
->value('password');
}
$rules['newPassword'] = [
Rule::requiredIf(
! Cms::config()->deferPublicRegistrationPassword
- && $this->component->inScenarios(User::SCENARIO_PASSWORD, User::SCENARIO_REGISTRATION)
+ && $this->inScenarios(self::SCENARIO_PASSWORD, self::SCENARIO_REGISTRATION)
),
new UserPasswordRule(
- forceDifferent: $this->component->passwordResetRequired,
+ forceDifferent: $this->subject->passwordResetRequired,
currentPassword: $currentPassword,
),
];
return $rules;
}
+
+ #[Override]
+ protected function validationRules(): array
+ {
+ $rules = parent::validationRules();
+
+ if ($this->inScenarios(self::SCENARIO_PASSWORD)) {
+ return Arr::only($rules, ['newPassword']);
+ }
+
+ if ($this->inScenarios(self::SCENARIO_REGISTRATION)) {
+ return Arr::only($rules, ['username', 'email', 'newPassword']);
+ }
+
+ if ($this->inScenarios(self::SCENARIO_ACTIVATION)) {
+ return Arr::only($rules, ['username', 'email']);
+ }
+
+ return $rules;
+ }
}
diff --git a/src/Validation/Attributes/Ruleset.php b/src/Validation/Attributes/Ruleset.php
deleted file mode 100644
index f488a83886d..00000000000
--- a/src/Validation/Attributes/Ruleset.php
+++ /dev/null
@@ -1,16 +0,0 @@
- */
- public string $class,
- ) {}
-}
diff --git a/src/Validation/ComponentRules.php b/src/Validation/ComponentRules.php
new file mode 100644
index 00000000000..3b5cce39123
--- /dev/null
+++ b/src/Validation/ComponentRules.php
@@ -0,0 +1,43 @@
+
+ */
+class ComponentRules extends Ruleset
+{
+ public function rules(): array
+ {
+ return $this->subject->getRules();
+ }
+
+ #[Override]
+ public function attributes(): array
+ {
+ return $this->subject->attributeLabels();
+ }
+
+ #[Override]
+ public function messages(): array
+ {
+ return $this->subject->getMessages();
+ }
+
+ #[Override]
+ protected function prepareForValidation(): void
+ {
+ $this->subject->prepareForValidation();
+ }
+
+ #[Override]
+ protected function passedValidation(): void
+ {
+ $this->subject->passedValidation();
+ }
+}
diff --git a/src/Validation/Concerns/HasScenarios.php b/src/Validation/Concerns/HasScenarios.php
deleted file mode 100644
index ac8e6a50b51..00000000000
--- a/src/Validation/Concerns/HasScenarios.php
+++ /dev/null
@@ -1,30 +0,0 @@
-_scenario = $scenario;
- }
-
- public function getScenario(): string
- {
- return $this->_scenario;
- }
-
- public function scenarios(): array
- {
- return [];
- }
-
- public function inScenarios(string ...$scenarios): bool
- {
- return in_array($this->_scenario, $scenarios, true);
- }
-}
diff --git a/src/Validation/Concerns/InteractsWithValidator.php b/src/Validation/Concerns/InteractsWithValidator.php
deleted file mode 100644
index 56b53bc3e5e..00000000000
--- a/src/Validation/Concerns/InteractsWithValidator.php
+++ /dev/null
@@ -1,75 +0,0 @@
- Arr::first($messages), $this->errors()->getMessages());
- }
-
- public function errors(): MessageBag
- {
- return $this->errors ??= new MessageBag;
- }
-
- /**
- * TODO: Add types to method signature once components no longer rely
- * on craft/base/Model
- *
- * @param array|string|null $attributeNames
- * @param bool $clearErrors
- */
- public function validate($attributeNames = null, $clearErrors = true, bool $throw = false): bool
- {
- if ($clearErrors) {
- $this->errors = new MessageBag;
- }
-
- if (is_string($attributeNames)) {
- $attributeNames = [$attributeNames];
- }
-
- if ($ruleset = $this->getRuleset()) {
- $ruleset->prepareForValidation($attributeNames);
- }
-
- $validator = $this->getValidator($attributeNames)
- ->after(fn ($validator) => $this->afterValidate($validator));
-
- if ($throw) {
- $validator->validate();
- }
-
- $result = $validator->passes();
-
- $this->errors()->merge($validator->errors()->getMessages());
-
- return $result && $this->errors()->isEmpty();
- }
-}
diff --git a/src/Validation/Concerns/Validates.php b/src/Validation/Concerns/Validates.php
index 53e2b579dad..99aa90d5f2e 100644
--- a/src/Validation/Concerns/Validates.php
+++ b/src/Validation/Concerns/Validates.php
@@ -4,60 +4,70 @@
namespace CraftCms\Cms\Validation\Concerns;
-use BadMethodCallException;
-use CraftCms\Cms\Component\Exceptions\InvalidCallException;
-use CraftCms\Cms\Component\Exceptions\UnknownPropertyException;
use CraftCms\Cms\Support\Arr;
-use CraftCms\Cms\Support\Typecast;
use CraftCms\Cms\Support\Utils;
-use CraftCms\Cms\Validation\Attributes\Ruleset as RulesetAttribute;
-use CraftCms\Cms\Validation\Contracts\Validatable;
-use CraftCms\Cms\Validation\Ruleset;
-use Illuminate\Support\Facades\Validator as ValidatorFacade;
+use CraftCms\RulesetValidation\Concerns\HasRuleset;
+use Illuminate\Support\MessageBag;
use Illuminate\Validation\Validator;
-use ReflectionClass;
-/**
- * @mixin Validatable
- */
trait Validates
{
- use HasScenarios;
- use InteractsWithValidator;
+ use HasRuleset;
- private Ruleset|false|null $ruleset = null;
+ private ?MessageBag $errors = null;
- public function getRules(): array
+ public function getFirstErrors(): array
{
- return [];
+ return array_map(fn (array $messages) => Arr::first($messages), $this->errors()->getMessages());
}
- public function getMessages(): array
+ public function errors(): MessageBag
{
- return [];
+ return $this->errors ??= new MessageBag;
}
- public function setAttributes($values, $safeOnly = true): void
+ /**
+ * TODO: Add types to method signature once components no longer rely
+ * on craft/base/Model
+ *
+ * @param array|string|null $attributeNames
+ * @param bool $clearErrors
+ */
+ public function validate($attributeNames = null, $clearErrors = true, bool $throw = false): bool
{
- Typecast::properties(static::class, $values);
-
- foreach ($values as $name => $value) {
- try {
- $this->$name = $value;
- } catch (UnknownPropertyException|InvalidCallException|\yii\base\UnknownPropertyException) {
- // Property or setter doesn't exist
- }
+ if ($clearErrors) {
+ $this->errors = new MessageBag;
+ }
+
+ if (is_string($attributeNames)) {
+ $attributeNames = [$attributeNames];
+ }
+
+ $ruleset = $this->ruleset;
+
+ if (! is_null($attributeNames)) {
+ $ruleset->only($attributeNames);
+ }
+
+ if ($throw) {
+ $ruleset->validate();
}
+
+ $result = $ruleset->passes();
+
+ $this->errors()->merge($ruleset->getValidator()->errors());
+
+ return $result && $this->errors()->isEmpty();
}
- public function getAttributes(): array
+ public function getRules(): array
{
- return Utils::getPublicProperties($this);
+ return [];
}
- public function attributes(): array
+ public function getMessages(): array
{
- return Utils::getPublicAttributes($this);
+ return [];
}
public function attributeLabels(): array
@@ -65,46 +75,14 @@ public function attributeLabels(): array
return [];
}
- public function getRuleset(): Ruleset|false
- {
- if (isset($this->ruleset)) {
- return $this->ruleset;
- }
+ public function prepareForValidation(): void {}
- $attributes = new ReflectionClass($this)->getAttributes(RulesetAttribute::class);
+ public function passedValidation(): void {}
- $class = null;
- if (isset($attributes[0])) {
- $class = $attributes[0]->getArguments()[0];
- } elseif (method_exists($this, 'rulesClass')) {
- $class = $this->rulesClass();
- }
-
- if (is_null($class)) {
- return $this->ruleset = false;
- }
-
- if (! is_subclass_of($class, Ruleset::class)) {
- throw new BadMethodCallException('The rules class must be an instance of '.Ruleset::class);
- }
-
- return $this->ruleset = app()->make($class, ['component' => $this]);
- }
+ public function afterValidate(?Validator $validator = null): void {}
- protected function getValidator(?array $attributeNames = null): Validator
+ public function validationData(): array
{
- $ruleset = $this->getRuleset();
- $rules = $ruleset ? $ruleset->rules() : $this->getRules();
- $attributes = $ruleset ? $ruleset->attributes() : $this->attributeLabels();
- $messages = $ruleset ? $ruleset->messages() : $this->getMessages();
-
- return ValidatorFacade::make([], [])
- ->setData($this->getAttributes())
- ->setCustomMessages($messages)
- ->setAttributeNames($attributes)
- ->setRules(is_null($attributeNames)
- ? $rules
- : Arr::only($rules, $attributeNames)
- );
+ return Arr::except(Utils::getPublicProperties($this), ['ruleset']);
}
}
diff --git a/src/Validation/Contracts/Validatable.php b/src/Validation/Contracts/Validatable.php
index c8a40ade287..926b9c23ca4 100644
--- a/src/Validation/Contracts/Validatable.php
+++ b/src/Validation/Contracts/Validatable.php
@@ -4,10 +4,11 @@
namespace CraftCms\Cms\Validation\Contracts;
+use CraftCms\RulesetValidation\Contracts\ValidatesWithRuleset;
use Illuminate\Contracts\Support\MessageBag;
use Illuminate\Validation\Validator;
-interface Validatable
+interface Validatable extends ValidatesWithRuleset
{
/**
* Returns the validation rules or ruleset for attributes.
@@ -24,13 +25,27 @@ public function getRules(): array;
public function getMessages(): array;
/**
- * This method is invoked before validation starts.
- * The default implementation returns true, allowing validation to proceed.
- * Override this method to perform pre-validation logic or to conditionally skip validation.
+ * Returns human-readable labels for attributes.
+ * These labels are used in validation error messages. For example, given an attribute
+ * `firstName`, a label `First Name` can be declared for display to end users.
+ * The default implementation returns an empty array.
+ *
+ * @return array
+ */
+ public function attributeLabels(): array;
+
+ /**
+ * Sets attribute values.
*
- * @return bool whether the validation should be executed.
+ * @param array $values attribute values to set (attribute name => value).
+ */
+ public function setAttributes(array $values): void;
+
+ /**
+ * This method is invoked before validation starts.
+ * Override this method to perform pre-validation logic.
*/
- public function beforeValidate(): bool;
+ public function prepareForValidation(): void;
/**
* Validates the attributes.
@@ -62,84 +77,4 @@ public function getFirstErrors(): array;
* Returns the validation error messages.
*/
public function errors(): MessageBag;
-
- /**
- * Sets attribute values.
- *
- * @param array $values attribute values to set (attribute name => value).
- */
- public function setAttributes(array $values): void;
-
- /**
- * Returns all attribute values.
- * By default, this returns all public non-static properties of the class.
- *
- * @return array
- */
- public function getAttributes(): array;
-
- /**
- * Returns the list of attribute names.
- * By default, this returns all public non-static property names of the class.
- *
- * @return string[] list of attribute names.
- */
- public function attributes(): array;
-
- /**
- * Returns human-readable labels for attributes.
- * These labels are used in validation error messages. For example, given an attribute
- * `firstName`, a label `First Name` can be declared for display to end users.
- * The default implementation returns an empty array.
- *
- * @return array
- */
- public function attributeLabels(): array;
-
- /**
- * Sets the current validation scenario.
- *
- * Scenarios allow components to use different validation rules based on context.
- * For example, a 'create' scenario might require certain fields, while an 'update'
- * scenario might have different requirements.
- *
- * @param string $scenario The scenario name to set
- */
- public function setScenario(string $scenario): void;
-
- /**
- * Returns the current validation scenario.
- *
- * @return string The active scenario name
- */
- public function getScenario(): string;
-
- /**
- * Returns a mapping of scenario names to their active attributes.
- *
- * Each scenario defines which attributes should be validated. The returned array
- * maps scenario names (keys) to either:
- * - An array of attribute names that should be validated in that scenario
- * - null to indicate all attributes should be validated
- *
- * Example:
- * ```php
- * [
- * 'create' => ['title', 'slug', 'body'],
- * 'update' => ['title', 'body'],
- * 'default' => null, // All attributes
- * ]
- * ```
- *
- * @return array|null>
- */
- public function scenarios(): array;
-
- /**
- * Checks if the current scenario matches any of the provided scenarios.
- *
- * @param string ...$scenarios One or more scenario names to check against
- * @return bool True if the current scenario matches any provided scenario
- */
- public function inScenarios(string ...$scenarios): bool;
}
diff --git a/src/Validation/Ruleset.php b/src/Validation/Ruleset.php
index c867361f5bb..668e74231f5 100644
--- a/src/Validation/Ruleset.php
+++ b/src/Validation/Ruleset.php
@@ -6,90 +6,36 @@
use CraftCms\Cms\Element\Validation\Events\DefineValidationRules;
use CraftCms\Cms\Validation\Contracts\Validatable;
+use Illuminate\Validation\Validator;
+use Override;
/**
* @template T of Validatable
+ *
+ * @property T $subject
*/
-abstract class Ruleset
+abstract class Ruleset extends \CraftCms\RulesetValidation\Ruleset
{
- public function __construct(
- /** @var T */
- protected readonly Validatable $component,
- ) {}
-
- /**
- * Returns the validation rules for the current scenario.
- *
- * This method combines the rules defined in defineRules() with any rules
- * added by event listeners, then filters them based on the active scenario.
- * If a scenario is active, only rules for attributes defined in that scenario
- * are returned.
- *
- * @return array
- */
- final public function rules(): array
+ #[Override]
+ protected function validationRules(): array
{
- $rules = $this->defineRules();
-
- event($event = new DefineValidationRules($this->component, $rules));
+ $rules = parent::validationRules();
- $attributes = $this->component->scenarios()[$this->component->getScenario()] ?? null;
+ event($event = new DefineValidationRules($this->subject, $rules));
- return collect($event->rules)
- ->unless(
- is_null($attributes),
- fn ($rules) => $rules->filter(
- fn ($rule, string $attribute) => in_array($attribute, $attributes, true),
- )
- )
- ->filter()
- ->all();
- }
-
- /**
- * Returns custom validation error messages.
- *
- * Override this method to provide custom error messages for specific
- * validation rules.
- *
- * @return array
- */
- public function messages(): array
- {
- return [];
+ return $event->rules;
}
- /**
- * Get custom attributes for validator errors.
- *
- * @return array
- */
- public function attributes(): array
- {
- return $this->component->attributeLabels();
- }
-
- /**
- * Prepare the component for validation.
- *
- * Use this method to normalize or transform attribute values before
- * validation runs (e.g., trimming strings, normalizing slugs).
- *
- * @param array|null $attributeNames The attributes being validated, or null for all
- */
- public function prepareForValidation(?array $attributeNames = null): void {}
-
- /**
- * Define the validation rules for this ruleset.
- *
- * Override this method in subclasses to define the base validation rules
- * for the component. Rules can be modified by event listeners before
- * being returned by the rules() method.
- *
- * @return array
- */
- protected function defineRules(): array
+ public function after(): array
{
- return [];
+ if (! method_exists($this->subject, 'afterValidate')) {
+ return [];
+ }
+
+ return [
+ function (Validator $validator) {
+ $this->subject->afterValidate($validator);
+ },
+ ];
}
}
diff --git a/tests/ArchTest.php b/tests/ArchTest.php
index 1af5576ab63..93fe3a26a8c 100644
--- a/tests/ArchTest.php
+++ b/tests/ArchTest.php
@@ -36,19 +36,19 @@
arch()
->expect(Arr::class)
->not
- ->toBeUsed()
+ ->toBeUsedIn('src')
->ignoring(CraftCms\Cms\Support\Arr::class);
arch()
->expect(Illuminate\Support\Facades\File::class)
->not
- ->toBeUsed()
+ ->toBeUsedIn('src')
->ignoring(File::class);
arch()
->expect(Str::class)
->not
- ->toBeUsed()
+ ->toBeUsedIn('src')
->ignoring(CraftCms\Cms\Support\Str::class);
arch('Only use JSON helper')
diff --git a/tests/Feature/Address/Elements/AddressValidationTest.php b/tests/Feature/Address/Elements/AddressValidationTest.php
index 968de7e745f..8fe740152c2 100644
--- a/tests/Feature/Address/Elements/AddressValidationTest.php
+++ b/tests/Feature/Address/Elements/AddressValidationTest.php
@@ -2,8 +2,8 @@
declare(strict_types=1);
-use CraftCms\Cms\Address\Elements\Address;
use CraftCms\Cms\Address\Models\Address as AddressModel;
+use CraftCms\Cms\Address\Validation\AddressRules;
use CraftCms\Cms\Cms;
describe('Country code format validation', function () {
@@ -134,7 +134,7 @@
describe('Country-specific required fields (SCENARIO_LIVE)', function () {
test('US address required fields on SCENARIO_LIVE', function (string $field, bool $expectError) {
$address = AddressModel::factory()->createElement(['countryCode' => 'US']);
- $address->setScenario(Address::SCENARIO_LIVE);
+ $address->ruleset->useScenario(AddressRules::SCENARIO_LIVE);
$address->{$field} = null;
$address->validate([$field]);
@@ -149,7 +149,7 @@
test('BE address required fields on SCENARIO_LIVE', function (string $field, bool $expectError) {
$address = AddressModel::factory()->createElement(['countryCode' => 'BE']);
- $address->setScenario(Address::SCENARIO_LIVE);
+ $address->ruleset->useScenario(AddressRules::SCENARIO_LIVE);
$address->{$field} = null;
$address->validate([$field]);
@@ -163,7 +163,7 @@
test('fields not required by country pass validation on SCENARIO_LIVE', function () {
$address = AddressModel::factory()->createElement(['countryCode' => 'US']);
- $address->setScenario(Address::SCENARIO_LIVE);
+ $address->ruleset->useScenario(AddressRules::SCENARIO_LIVE);
$address->dependentLocality = null;
$address->sortingCode = null;
diff --git a/tests/Feature/Asset/Elements/AssetValidationTest.php b/tests/Feature/Asset/Elements/AssetValidationTest.php
index 6f3e988bfc6..56a15382b6e 100644
--- a/tests/Feature/Asset/Elements/AssetValidationTest.php
+++ b/tests/Feature/Asset/Elements/AssetValidationTest.php
@@ -4,6 +4,7 @@
use CraftCms\Cms\Asset\Elements\Asset;
use CraftCms\Cms\Asset\Models\Asset as AssetModel;
+use CraftCms\Cms\Asset\Validation\AssetRules;
describe('Required field validation', function () {
test('filename validation', function (string $value, bool $expectError) {
@@ -50,7 +51,7 @@
test('title length validation on create scenario', function (int $length, bool $expectError) {
$asset = AssetModel::factory()->createElement();
$asset->title = str_repeat('a', $length);
- $asset->setScenario(Asset::SCENARIO_CREATE);
+ $asset->ruleset->useScenario(AssetRules::SCENARIO_CREATE);
$asset->validate(['title']);
@@ -75,39 +76,39 @@
describe('Scenario-specific required validation', function () {
test('newLocation is required on specific scenarios', function (string $scenario, bool $expectError) {
$asset = AssetModel::factory()->createElement();
- $asset->setScenario($scenario);
+ $asset->ruleset->useScenario($scenario);
$asset->newLocation = null;
$asset->validate(['newLocation']);
expect($asset->errors()->has('newLocation'))->toBe($expectError);
})->with([
- 'SCENARIO_CREATE requires newLocation' => [Asset::SCENARIO_CREATE, true],
- 'SCENARIO_MOVE requires newLocation' => [Asset::SCENARIO_MOVE, true],
- 'SCENARIO_FILEOPS requires newLocation' => [Asset::SCENARIO_FILEOPS, true],
- 'default scenario does not require newLocation' => [Asset::SCENARIO_DEFAULT, false],
+ 'SCENARIO_CREATE requires newLocation' => [AssetRules::SCENARIO_CREATE, true],
+ 'SCENARIO_MOVE requires newLocation' => [AssetRules::SCENARIO_MOVE, true],
+ 'SCENARIO_FILEOPS requires newLocation' => [AssetRules::SCENARIO_FILEOPS, true],
+ 'default scenario does not require newLocation' => [AssetRules::SCENARIO_DEFAULT, false],
]);
test('tempFilePath is required on specific scenarios', function (string $scenario, bool $expectError) {
$asset = AssetModel::factory()->createElement();
- $asset->setScenario($scenario);
+ $asset->ruleset->useScenario($scenario);
$asset->tempFilePath = null;
$asset->validate(['tempFilePath']);
expect($asset->errors()->has('tempFilePath'))->toBe($expectError);
})->with([
- 'SCENARIO_CREATE requires tempFilePath' => [Asset::SCENARIO_CREATE, true],
- 'SCENARIO_REPLACE requires tempFilePath' => [Asset::SCENARIO_REPLACE, true],
- 'default scenario does not require tempFilePath' => [Asset::SCENARIO_DEFAULT, false],
- 'SCENARIO_MOVE does not require tempFilePath' => [Asset::SCENARIO_MOVE, false],
+ 'SCENARIO_CREATE requires tempFilePath' => [AssetRules::SCENARIO_CREATE, true],
+ 'SCENARIO_REPLACE requires tempFilePath' => [AssetRules::SCENARIO_REPLACE, true],
+ 'default scenario does not require tempFilePath' => [AssetRules::SCENARIO_DEFAULT, false],
+ 'SCENARIO_MOVE does not require tempFilePath' => [AssetRules::SCENARIO_MOVE, false],
]);
});
describe('SCENARIO_INDEX validation', function () {
test('SCENARIO_INDEX has empty validation attributes', function () {
$asset = AssetModel::factory()->createElement();
- $asset->setScenario(Asset::SCENARIO_INDEX);
+ $asset->ruleset->useScenario(AssetRules::SCENARIO_INDEX);
$activeAttributes = $asset->activeAttributes();
@@ -118,7 +119,7 @@
$asset = AssetModel::factory()->createElement();
$asset->kind = '';
$asset->filename = '';
- $asset->setScenario(Asset::SCENARIO_INDEX);
+ $asset->ruleset->useScenario(AssetRules::SCENARIO_INDEX);
$asset->validate();
diff --git a/tests/Feature/Element/ElementWrites/SaveElementTest.php b/tests/Feature/Element/ElementWrites/SaveElementTest.php
index 27d3d3c1d04..231cb348d83 100644
--- a/tests/Feature/Element/ElementWrites/SaveElementTest.php
+++ b/tests/Feature/Element/ElementWrites/SaveElementTest.php
@@ -11,6 +11,7 @@
use CraftCms\Cms\Element\Operations\ElementWrites;
use CraftCms\Cms\Element\Queries\ElementQuery;
use CraftCms\Cms\Element\Queries\Exceptions\ElementNotFoundException;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\Entry\Models\Entry as EntryModel;
use CraftCms\Cms\Field\PlainText;
@@ -346,6 +347,23 @@ function createEntryWithPlainTextField(array $entryAttributes = []): array
Event::assertDispatched(fn (AfterSaveElement $event): bool => $event->element->id === $entry->id && $event->isNew === false);
});
+it('preserves the caller scenario after save', function (?string $scenario) {
+ $entry = EntryModel::factory()->createElement(['title' => 'Initial title']);
+
+ if ($scenario !== null) {
+ $entry->ruleset->useScenario($scenario);
+ }
+
+ $entry->title = 'Saved title';
+
+ expect($this->writes->saveElement($entry, updateSearchIndex: false))->toBeTrue()
+ ->and($entry->ruleset->getScenario())->toBe($scenario ?? ElementRules::SCENARIO_DEFAULT);
+})->with([
+ 'default scenario' => [null],
+ 'live scenario' => [ElementRules::SCENARIO_LIVE],
+ 'essentials scenario' => [ElementRules::SCENARIO_ESSENTIALS],
+]);
+
it('enables the current site when a single-site element is disabled for that site', function () {
$element = new TestSaveElementActionElement;
$element->siteId = Sites::getPrimarySite()->id;
diff --git a/tests/Feature/Element/Elements/ElementValidationTest.php b/tests/Feature/Element/Elements/ElementValidationTest.php
index 8c05bb3b0aa..bd8ddd9d563 100644
--- a/tests/Feature/Element/Elements/ElementValidationTest.php
+++ b/tests/Feature/Element/Elements/ElementValidationTest.php
@@ -3,7 +3,7 @@
declare(strict_types=1);
use CraftCms\Cms\Edition;
-use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Models\Entry as EntryModel;
use CraftCms\Cms\Support\Facades\Sites;
use Illuminate\Support\Facades\DB;
@@ -25,7 +25,7 @@
test('siteId is validated on SCENARIO_LIVE', function () {
$entry = EntryModel::factory()->createElement();
- $entry->setScenario(Element::SCENARIO_LIVE);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
$site = Sites::getPrimarySite();
$entry->siteId = $site->id;
@@ -36,7 +36,7 @@
test('siteId is validated on SCENARIO_ESSENTIALS', function () {
$entry = EntryModel::factory()->createElement();
- $entry->setScenario(Element::SCENARIO_ESSENTIALS);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$site = Sites::getPrimarySite();
$entry->siteId = $site->id;
@@ -90,7 +90,7 @@
test('title is required on SCENARIO_LIVE for elements with titles', function () {
$entry = EntryModel::factory()->createElement();
- $entry->setScenario(Element::SCENARIO_LIVE);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
$entry->title = '';
$entry->validate(['title']);
@@ -139,7 +139,7 @@
test('slug is validated on SCENARIO_ESSENTIALS', function () {
$entry = EntryModel::factory()->createElement();
- $entry->setScenario(Element::SCENARIO_ESSENTIALS);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$entry->slug = str_repeat('a', 256);
$entry->validate(['slug']);
@@ -159,7 +159,7 @@
test('uri is validated on SCENARIO_ESSENTIALS', function () {
$entry = EntryModel::factory()->createElement();
- $entry->setScenario(Element::SCENARIO_ESSENTIALS);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
$entry->validate(['uri']);
@@ -170,7 +170,7 @@
describe('Scenario validation', function () {
test('SCENARIO_LIVE validates title when required', function () {
$entry = EntryModel::factory()->createElement();
- $entry->setScenario(Element::SCENARIO_LIVE);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
$entry->title = '';
$entry->validate(['title']);
diff --git a/tests/Feature/Entry/Elements/EntryValidationTest.php b/tests/Feature/Entry/Elements/EntryValidationTest.php
index a4525cef8d7..1490cdbcf84 100644
--- a/tests/Feature/Entry/Elements/EntryValidationTest.php
+++ b/tests/Feature/Entry/Elements/EntryValidationTest.php
@@ -3,7 +3,7 @@
declare(strict_types=1);
use CraftCms\Cms\Edition;
-use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Models\Entry as EntryModel;
use CraftCms\Cms\Entry\Models\EntryType;
use CraftCms\Cms\Section\Enums\SectionType;
@@ -112,7 +112,7 @@
bool $expectError
) {
$entry = EntryModel::factory()->createElement();
- $entry->setScenario(Element::SCENARIO_LIVE);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
$entry->postDate = $postDate;
$entry->expiryDate = $expiryDate;
@@ -158,7 +158,7 @@
->createElement();
if ($isLiveScenario) {
- $entry->setScenario(Element::SCENARIO_LIVE);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
}
$entry->setAuthorIds([]);
@@ -236,7 +236,7 @@
$entry->setAuthorIds([$user->id]);
- $entry->validate(['authorIds']);
+ expect($entry->validate(['authorIds']))->toBeFalse();
expect($entry->errors()->has('authorIds'))->toBeTrue();
expect($entry->errors()->first('authorIds'))->toContain('permission');
@@ -306,7 +306,7 @@
describe('Scenario-specific validation', function () {
test('SCENARIO_LIVE validates date comparison', function () {
$entry = EntryModel::factory()->createElement();
- $entry->setScenario(Element::SCENARIO_LIVE);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
$entry->postDate = new DateTime('2025-01-01');
$entry->expiryDate = new DateTime('2024-01-01');
@@ -338,7 +338,7 @@
->forSection($section)
->forEntryType($entryType)
->createElement();
- $entry->setScenario(Element::SCENARIO_LIVE);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
$entry->setAuthorIds([]);
$entry->validate(['authorIds']);
diff --git a/tests/Feature/Field/ElementRules/AddressesFieldElementRulesTest.php b/tests/Feature/Field/ElementRules/AddressesFieldElementRulesTest.php
index f88e7174020..5d43243748e 100644
--- a/tests/Feature/Field/ElementRules/AddressesFieldElementRulesTest.php
+++ b/tests/Feature/Field/ElementRules/AddressesFieldElementRulesTest.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Models\Entry as EntryModel;
use CraftCms\Cms\Field\Addresses;
@@ -16,7 +16,7 @@
$result = EntryModel::factory()
->withField('addressesField', Addresses::class, [], value: $value)
- ->withScenario(Element::SCENARIO_LIVE)
+ ->withScenario(ElementRules::SCENARIO_LIVE)
->createElementWithFields(save: false);
$result->element->validate();
diff --git a/tests/Feature/Field/ElementRules/AssetsFieldElementRulesTest.php b/tests/Feature/Field/ElementRules/AssetsFieldElementRulesTest.php
index 24fd6949561..00418196584 100644
--- a/tests/Feature/Field/ElementRules/AssetsFieldElementRulesTest.php
+++ b/tests/Feature/Field/ElementRules/AssetsFieldElementRulesTest.php
@@ -4,7 +4,7 @@
use CraftCms\Cms\Asset\Elements\Asset as AssetElement;
use CraftCms\Cms\Asset\Models\Asset as AssetModel;
-use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Models\Entry as EntryModel;
use CraftCms\Cms\Field\Assets;
@@ -20,7 +20,7 @@
$validResult = EntryModel::factory()
->withField('allowedAssets', Assets::class, ['restrictFiles' => true, 'allowedKinds' => ['image']], value: AssetElement::find()->id($image->id))
- ->withScenario(Element::SCENARIO_LIVE)
+ ->withScenario(ElementRules::SCENARIO_LIVE)
->createElementWithFields(save: false);
$validResult->element->validate();
@@ -28,7 +28,7 @@
$invalidResult = EntryModel::factory()
->withField('blockedAssets', Assets::class, ['restrictFiles' => true, 'allowedKinds' => ['image']], value: AssetElement::find()->id($pdf->id))
- ->withScenario(Element::SCENARIO_LIVE)
+ ->withScenario(ElementRules::SCENARIO_LIVE)
->createElementWithFields(save: false);
$invalidResult->element->validate();
diff --git a/tests/Feature/Field/ElementRules/ContentBlockFieldElementRulesTest.php b/tests/Feature/Field/ElementRules/ContentBlockFieldElementRulesTest.php
index c273cf51b0a..770ff0d387e 100644
--- a/tests/Feature/Field/ElementRules/ContentBlockFieldElementRulesTest.php
+++ b/tests/Feature/Field/ElementRules/ContentBlockFieldElementRulesTest.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Models\Entry as EntryModel;
use CraftCms\Cms\Entry\Models\EntryType;
use CraftCms\Cms\Field\ContentBlock;
@@ -48,7 +48,7 @@
$result = EntryModel::factory()
->withField('contentBlock', ContentBlock::class, $contentBlockSettings, value: ['fields' => ['innerText' => null]])
- ->withScenario(Element::SCENARIO_LIVE)
+ ->withScenario(ElementRules::SCENARIO_LIVE)
->createElementWithFields(save: false);
$result->element->validate();
@@ -95,7 +95,7 @@
$result = EntryModel::factory()
->withField('contentBlock', ContentBlock::class, $contentBlockField->getSettings())
- ->withScenario(Element::SCENARIO_LIVE)
+ ->withScenario(ElementRules::SCENARIO_LIVE)
->createElementWithFields(save: false);
$result->element->setFieldValueFromRequest('contentBlock', [
@@ -141,7 +141,7 @@
$result = EntryModel::factory()
->withField('matrixField', Matrix::class, ['entryTypes' => [$matrixEntryType->id]])
- ->withScenario(Element::SCENARIO_LIVE)
+ ->withScenario(ElementRules::SCENARIO_LIVE)
->createElementWithFields(save: false);
$blockUid = Str::uuid()->toString();
diff --git a/tests/Feature/Field/ElementRules/ElementAfterValidateTest.php b/tests/Feature/Field/ElementRules/ElementAfterValidateTest.php
index 99e50a65093..e4edd380ef9 100644
--- a/tests/Feature/Field/ElementRules/ElementAfterValidateTest.php
+++ b/tests/Feature/Field/ElementRules/ElementAfterValidateTest.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Elements\Entry as EntryElement;
use CraftCms\Cms\Field\Email;
use CraftCms\Cms\Field\Models\Field;
@@ -41,7 +41,7 @@
$entry = new TestEntryWithAfterValidate;
$entry->title = 'Test entry';
- $entry->setScenario(Element::SCENARIO_DEFAULT);
+ $entry->ruleset->useScenario(ElementRules::SCENARIO_DEFAULT);
$entry->setMockFieldLayout($layout);
$entry->setFieldValue($field->handle, 'not-an-email');
diff --git a/tests/Feature/Field/ElementRules/MatrixFieldElementRulesTest.php b/tests/Feature/Field/ElementRules/MatrixFieldElementRulesTest.php
index c6462462bca..52393517744 100644
--- a/tests/Feature/Field/ElementRules/MatrixFieldElementRulesTest.php
+++ b/tests/Feature/Field/ElementRules/MatrixFieldElementRulesTest.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Models\Entry as EntryModel;
use CraftCms\Cms\Entry\Models\EntryType as EntryTypeModel;
use CraftCms\Cms\Field\Matrix;
@@ -23,7 +23,7 @@ function createMatrixRulesEntryType(): EntryTypeModel
$result = EntryModel::factory()
->withField('matrixField', Matrix::class, ['entryTypes' => [$entryType->id], 'minEntries' => 1], value: '')
- ->withScenario(Element::SCENARIO_LIVE)
+ ->withScenario(ElementRules::SCENARIO_LIVE)
->createElementWithFields(save: false);
$result->element->validate();
@@ -45,7 +45,7 @@ function createMatrixRulesEntryType(): EntryTypeModel
$result = EntryModel::factory()
->withField('matrixField', Matrix::class, ['entryTypes' => [$entryType->id], 'viewMode' => Matrix::VIEW_MODE_INDEX], value: $value)
- ->withScenario(Element::SCENARIO_LIVE)
+ ->withScenario(ElementRules::SCENARIO_LIVE)
->createElementWithFields(save: false);
$result->element->validate();
diff --git a/tests/Feature/Field/ElementRules/RelationFieldElementRulesTest.php b/tests/Feature/Field/ElementRules/RelationFieldElementRulesTest.php
index 658ec9e8962..0083510ae81 100644
--- a/tests/Feature/Field/ElementRules/RelationFieldElementRulesTest.php
+++ b/tests/Feature/Field/ElementRules/RelationFieldElementRulesTest.php
@@ -2,8 +2,8 @@
declare(strict_types=1);
-use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\ElementCollection;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Elements\Entry as EntryElement;
use CraftCms\Cms\Entry\Models\Entry as EntryModel;
use CraftCms\Cms\Field\Entries;
@@ -11,7 +11,7 @@
test('relation fields enforce min relations', function () {
$result = EntryModel::factory()
->withField('relatedEntries', Entries::class, ['allowLimit' => true, 'minRelations' => 1], value: [])
- ->withScenario(Element::SCENARIO_LIVE)
+ ->withScenario(ElementRules::SCENARIO_LIVE)
->createElementWithFields(save: false);
$result->element->validate();
@@ -26,7 +26,7 @@
$result = EntryModel::factory()
->withField('relatedEntries', Entries::class, ['validateRelatedElements' => true], value: new ElementCollection([$relatedEntry]))
- ->withScenario(Element::SCENARIO_LIVE)
+ ->withScenario(ElementRules::SCENARIO_LIVE)
->createElementWithFields(save: false);
$result->element->validate();
diff --git a/tests/Feature/Http/RespondsWithFlashTest.php b/tests/Feature/Http/RespondsWithFlashTest.php
index 246e85dea9a..60eadaefd0e 100644
--- a/tests/Feature/Http/RespondsWithFlashTest.php
+++ b/tests/Feature/Http/RespondsWithFlashTest.php
@@ -55,6 +55,11 @@ public function errors(): MessageBag
{
return new MessageBag(['name' => ['Name is required']]);
}
+
+ public function setAttributes(array $values): void
+ {
+ $this->name = $values['name'] ?? '';
+ }
};
return $this->asModelFailure($model, 'Model save failed', 'testModel');
diff --git a/tests/Feature/User/Elements/UserValidationTest.php b/tests/Feature/User/Elements/UserValidationTest.php
index 0843d4b4a80..6f49df31ea1 100644
--- a/tests/Feature/User/Elements/UserValidationTest.php
+++ b/tests/Feature/User/Elements/UserValidationTest.php
@@ -9,7 +9,7 @@
use CraftCms\Cms\User\Elements\User;
use CraftCms\Cms\User\Models\User as UserModel;
use CraftCms\Cms\User\Validation\UserRules;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use Illuminate\Support\Facades\Hash;
#[Ruleset(UserRules::class)]
@@ -440,7 +440,7 @@ public function getFieldLayout(): ?FieldLayout
describe('Scenario validation', function () {
test('SCENARIO_PASSWORD only validates newPassword', function () {
$user = UserModel::factory()->createElement();
- $user->setScenario(User::SCENARIO_PASSWORD);
+ $user->ruleset->useScenario(UserRules::SCENARIO_PASSWORD);
$user->username = str_repeat('a', 256); // Invalid
$user->newPassword = 'p'; // too short
@@ -452,7 +452,7 @@ public function getFieldLayout(): ?FieldLayout
test('SCENARIO_REGISTRATION validates username email and newPassword', function () {
$user = UserModel::factory()->createElement();
- $user->setScenario(User::SCENARIO_REGISTRATION);
+ $user->ruleset->useScenario(UserRules::SCENARIO_REGISTRATION);
$user->username = str_repeat('a', 256); // Invalid
$user->email = 'invalid-email'; // Invalid
$user->newPassword = 'p'; // too short
@@ -468,7 +468,7 @@ public function getFieldLayout(): ?FieldLayout
test('SCENARIO_ACTIVATION validates username and email', function () {
$user = UserModel::factory()->createElement();
- $user->setScenario(User::SCENARIO_ACTIVATION);
+ $user->ruleset->useScenario(UserRules::SCENARIO_ACTIVATION);
$user->username = str_repeat('a', 256); // Invalid
$user->email = 'invalid-email'; // Invalid
@@ -511,7 +511,7 @@ public function getFieldLayout(): ?FieldLayout
$user = new TestUserWithFieldLayout;
$user->setMockFieldLayout($fieldLayout);
- $user->setScenario(User::SCENARIO_LIVE);
+ $user->ruleset->useScenario(UserRules::SCENARIO_LIVE);
$user->email = 'test@example.com';
$user->firstName = '';
$user->lastName = '';
@@ -539,7 +539,7 @@ public function getFieldLayout(): ?FieldLayout
$user = new TestUserWithFieldLayout;
$user->setMockFieldLayout($fieldLayout);
- $user->setScenario(User::SCENARIO_LIVE);
+ $user->ruleset->useScenario(UserRules::SCENARIO_LIVE);
$user->email = 'test@example.com';
$user->firstName = '';
$user->lastName = '';
@@ -564,7 +564,7 @@ public function getFieldLayout(): ?FieldLayout
$user = new TestUserWithFieldLayout;
$user->setMockFieldLayout($fieldLayout);
- $user->setScenario(User::SCENARIO_LIVE);
+ $user->ruleset->useScenario(UserRules::SCENARIO_LIVE);
$user->email = 'test@example.com';
$user->fullName = '';
@@ -588,7 +588,7 @@ public function getFieldLayout(): ?FieldLayout
$user = new TestUserWithFieldLayout;
$user->setMockFieldLayout($fieldLayout);
- $user->setScenario(User::SCENARIO_LIVE);
+ $user->ruleset->useScenario(UserRules::SCENARIO_LIVE);
$user->email = 'test@example.com';
$user->fullName = '';
@@ -611,7 +611,7 @@ public function getFieldLayout(): ?FieldLayout
$user = new TestUserWithFieldLayout;
$user->setMockFieldLayout($fieldLayout);
- $user->setScenario(User::SCENARIO_DEFAULT);
+ $user->ruleset->useScenario(UserRules::SCENARIO_DEFAULT);
$user->email = 'test@example.com';
$user->firstName = '';
$user->lastName = '';
@@ -633,7 +633,7 @@ public function getFieldLayout(): ?FieldLayout
$user = new TestUserWithFieldLayout;
$user->setMockFieldLayout($fieldLayout);
- $user->setScenario(User::SCENARIO_LIVE);
+ $user->ruleset->useScenario(UserRules::SCENARIO_LIVE);
$user->email = 'test@example.com';
$user->firstName = '';
$user->lastName = '';
diff --git a/tests/TestClasses/TestEntryWithAfterValidate.php b/tests/TestClasses/TestEntryWithAfterValidate.php
index 877a2fd5d71..e28683f76f9 100644
--- a/tests/TestClasses/TestEntryWithAfterValidate.php
+++ b/tests/TestClasses/TestEntryWithAfterValidate.php
@@ -7,7 +7,7 @@
use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\FieldLayout\FieldLayout;
-use CraftCms\Cms\Validation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
use Illuminate\Validation\Validator as LaravelValidator;
use Override;
diff --git a/tests/Unit/Element/ElementWrites/PropagateElementTest.php b/tests/Unit/Element/ElementWrites/PropagateElementTest.php
index c25d599e7ec..fd244d27084 100644
--- a/tests/Unit/Element/ElementWrites/PropagateElementTest.php
+++ b/tests/Unit/Element/ElementWrites/PropagateElementTest.php
@@ -11,6 +11,7 @@
use CraftCms\Cms\Element\Models\ElementSiteSettings;
use CraftCms\Cms\Element\Operations\ElementUris;
use CraftCms\Cms\Element\Operations\ElementWrites;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\Field;
use CraftCms\Cms\FieldLayout\FieldLayout;
use CraftCms\Cms\FieldLayout\LayoutElements\CustomField;
@@ -110,7 +111,7 @@
->and($siteElement->getEnabledForSite())->toBeFalse()
->and($siteElement->propagating)->toBeTrue()
->and($siteElement->propagatingFrom)->toBe($element)
- ->and($siteElement->getScenario())->toBe(Element::SCENARIO_ESSENTIALS)
+ ->and($siteElement->ruleset->getScenario())->toBe(ElementRules::SCENARIO_ESSENTIALS)
->and($siteElement->getDirtyAttributes())->toContain('enabled')
->and($element->newSiteIds)->toBe([2]);
@@ -232,7 +233,7 @@
expect($siteElement->getFieldValue('plainText'))->toBe('fallback value')
->and($field->propagateCalls)->toHaveCount(1)
- ->and($siteElement->getScenario())->toBe(Element::SCENARIO_LIVE);
+ ->and($siteElement->ruleset->getScenario())->toBe(ElementRules::SCENARIO_LIVE);
});
it('uses the live scenario when cross-site validation applies to enabled site elements', function () {
@@ -248,7 +249,7 @@
$this->executor->propagate($element, supportedSites(enabledByDefault: true), 2, $siteElement, crossSiteValidate: true);
- expect($siteElement->getScenario())->toBe(Element::SCENARIO_LIVE);
+ expect($siteElement->ruleset->getScenario())->toBe(ElementRules::SCENARIO_LIVE);
});
it('continues when uri generation is aborted', function () {
diff --git a/tests/Unit/Element/ElementWrites/PropagateElementsTest.php b/tests/Unit/Element/ElementWrites/PropagateElementsTest.php
index 0d1a19881f3..177f2a0ffca 100644
--- a/tests/Unit/Element/ElementWrites/PropagateElementsTest.php
+++ b/tests/Unit/Element/ElementWrites/PropagateElementsTest.php
@@ -16,6 +16,7 @@
use CraftCms\Cms\Element\Operations\ElementWrites;
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
use CraftCms\Cms\Element\Queries\Exceptions\QueryAbortedException;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Search\Search;
use CraftCms\Cms\Site\Data\Site;
use CraftCms\Cms\Site\Sites as SitesService;
@@ -216,7 +217,7 @@ function createElement(int $id, int $siteId = 1, ?DateTime $dateUpdated = null):
$this->action->propagateElements($query);
- expect($element->getScenario())->toBe(Element::SCENARIO_ESSENTIALS)
+ expect($element->ruleset->getScenario())->toBe(ElementRules::SCENARIO_ESSENTIALS)
->and($element->newSiteIds)->toBe([])
->and($element->afterPropagateCalled)->toBeTrue();
diff --git a/tests/Unit/Element/ElementWrites/ResaveElementsTest.php b/tests/Unit/Element/ElementWrites/ResaveElementsTest.php
index 4fa5cc8c9ec..d27cc3eff74 100644
--- a/tests/Unit/Element/ElementWrites/ResaveElementsTest.php
+++ b/tests/Unit/Element/ElementWrites/ResaveElementsTest.php
@@ -18,6 +18,7 @@
use CraftCms\Cms\Element\Operations\ElementWrites;
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
use CraftCms\Cms\Element\Queries\Exceptions\QueryAbortedException;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface;
use CraftCms\Cms\Search\Search;
use CraftCms\Cms\Site\Sites;
@@ -59,7 +60,7 @@
expect($this->saveElementAction->calls[0]['updateSearchIndex'])->toBeFalse();
expect($this->saveElementAction->calls[0]['forceTouch'])->toBeTrue();
expect($this->saveElementAction->calls[0]['saveContent'])->toBeTrue();
- expect($this->saveElementAction->calls[0]['scenario'])->toBe(Element::SCENARIO_ESSENTIALS);
+ expect($this->saveElementAction->calls[0]['scenario'])->toBe(ElementRules::SCENARIO_ESSENTIALS);
expect($this->saveElementAction->calls[0]['resaving'])->toBeTrue();
expect($this->saveElementAction->calls[1]['element'])->toBe($secondElement);
@@ -234,7 +235,7 @@ public function save(
'updateSearchIndex' => $updateSearchIndex,
'forceTouch' => $forceTouch,
'saveContent' => $saveContent,
- 'scenario' => $element->getScenario(),
+ 'scenario' => $element->ruleset->getScenario(),
'resaving' => $element->resaving,
];
diff --git a/tests/Unit/Validation/RulesetTest.php b/tests/Unit/Validation/RulesetTest.php
deleted file mode 100644
index 920649879e5..00000000000
--- a/tests/Unit/Validation/RulesetTest.php
+++ /dev/null
@@ -1,307 +0,0 @@
-testMessages;
- }
-
- public function prepareForValidation(?array $attributeNames = null): void
- {
- $this->prepareForValidationCalled = true;
- $this->prepareForValidationAttributes = $attributeNames;
- }
-
- protected function defineRules(): array
- {
- return $this->testRules;
- }
- };
-}
-
-function createTestComponent(array $scenarios = []): Validatable
-{
- return new class($scenarios) implements Validatable
- {
- use Validates;
-
- public function __construct(private array $testScenarios) {}
-
- public function scenarios(): array
- {
- return $this->testScenarios;
- }
-
- public function getRules(): array
- {
- return [];
- }
-
- public function getMessages(): array
- {
- return [];
- }
-
- public function validate(string|array|null $attributeNames = null, bool $clearErrors = true): bool
- {
- return true;
- }
-
- public function getFirstErrors(): array
- {
- return [];
- }
-
- public function errors(): MessageBag
- {
- return new MessageBag;
- }
-
- public function setAttributes(array $values, bool $safeOnly = true): void {}
-
- public function getAttributes(): array
- {
- return [];
- }
-
- public function attributes(): array
- {
- return [];
- }
-
- public function attributeLabels(): array
- {
- return [];
- }
- };
-}
-
-describe('rules', function () {
- test('returns defined rules', function () {
- $component = createTestComponent();
- $ruleset = createTestRuleset($component, rules: [
- 'title' => ['required', 'string'],
- 'slug' => ['nullable', 'string'],
- ]);
-
- $rules = $ruleset->rules();
-
- expect($rules)->toHaveKey('title');
- expect($rules)->toHaveKey('slug');
- expect($rules['title'])->toBe(['required', 'string']);
- });
-
- test('fires DefineValidationRules event', function () {
- Event::fake([DefineValidationRules::class]);
-
- $component = createTestComponent();
- $ruleset = createTestRuleset($component, rules: [
- 'title' => ['required'],
- ]);
-
- $ruleset->rules();
-
- Event::assertDispatched(fn (DefineValidationRules $event) => $event->component === $component
- && isset($event->rules['title']));
- });
-
- test('event listeners can modify rules', function () {
- $component = createTestComponent();
- $ruleset = createTestRuleset($component, rules: [
- 'title' => ['required'],
- ]);
-
- Event::listen(DefineValidationRules::class, function (DefineValidationRules $event) {
- $event->addRule('title', 'max:255');
- $event->addRules('email', ['required', 'email']);
- });
-
- $rules = $ruleset->rules();
-
- expect($rules['title'])->toContain('max:255');
- expect($rules)->toHaveKey('email');
- expect($rules['email'])->toBe(['required', 'email']);
- });
-
- test('filters rules by scenario attributes', function () {
- $component = createTestComponent(
- scenarios: [
- 'essentials' => ['title', 'slug'],
- ],
- );
- $ruleset = createTestRuleset(
- $component,
- rules: [
- 'title' => ['required'],
- 'slug' => ['required'],
- 'body' => ['nullable'],
- ],
- );
- $component->setScenario('essentials');
-
- $rules = $ruleset->rules();
-
- expect($rules)->toHaveKey('title');
- expect($rules)->toHaveKey('slug');
- expect($rules)->not->toHaveKey('body');
- });
-
- test('returns all rules when scenario maps to null', function () {
- $component = createTestComponent(
- scenarios: [
- 'default' => null,
- ],
- );
- $ruleset = createTestRuleset(
- $component,
- rules: [
- 'title' => ['required'],
- 'slug' => ['required'],
- 'body' => ['nullable'],
- ],
- );
- $component->setScenario('default');
-
- $rules = $ruleset->rules();
-
- expect($rules)->toHaveKey('title');
- expect($rules)->toHaveKey('slug');
- expect($rules)->toHaveKey('body');
- });
-
- test('filters out empty rules', function () {
- $component = createTestComponent();
- $ruleset = createTestRuleset($component, rules: [
- 'title' => ['required'],
- 'slug' => [],
- 'body' => null,
- ]);
-
- $rules = $ruleset->rules();
-
- expect($rules)->toHaveKey('title');
- expect($rules)->not->toHaveKey('slug');
- expect($rules)->not->toHaveKey('body');
- });
-});
-
-describe('prepareForValidation', function () {
- test('is called before validation runs', function () {
- $component = createTestComponent();
- $ruleset = createTestRuleset($component, rules: [
- 'title' => ['required'],
- ]);
-
- expect($ruleset->prepareForValidationCalled)->toBeFalse();
-
- $ruleset->prepareForValidation(['title']);
-
- expect($ruleset->prepareForValidationCalled)->toBeTrue();
- expect($ruleset->prepareForValidationAttributes)->toBe(['title']);
- });
-
- test('receives null when validating all attributes', function () {
- $component = createTestComponent();
- $ruleset = createTestRuleset($component);
-
- $ruleset->prepareForValidation();
-
- expect($ruleset->prepareForValidationCalled)->toBeTrue();
- expect($ruleset->prepareForValidationAttributes)->toBeNull();
- });
-});
-
-describe('messages', function () {
- test('returns custom messages', function () {
- $component = createTestComponent();
- $ruleset = createTestRuleset($component, messages: [
- 'title.required' => 'Please enter a title.',
- 'email.email' => 'Invalid email format.',
- ]);
-
- $messages = $ruleset->messages();
-
- expect($messages)->toBe([
- 'title.required' => 'Please enter a title.',
- 'email.email' => 'Invalid email format.',
- ]);
- });
-});
-
-describe('scenarios', function () {
- test('scenarios method controls rule filtering', function () {
- $component = createTestComponent(
- scenarios: [
- 'login' => ['email', 'password'],
- 'profile' => ['title', 'email'],
- ],
- );
- $ruleset = createTestRuleset(
- $component,
- rules: [
- 'title' => ['required'],
- 'email' => ['required', 'email'],
- 'password' => ['required', 'min:8'],
- ],
- );
-
- $component->setScenario('login');
- $loginRules = $ruleset->rules();
-
- expect($loginRules)->toHaveKeys(['email', 'password']);
- expect($loginRules)->not->toHaveKey('title');
-
- $component->setScenario('profile');
- $profileRules = $ruleset->rules();
-
- expect($profileRules)->toHaveKeys(['title', 'email']);
- expect($profileRules)->not->toHaveKey('password');
- });
-
- test('undefined scenario returns all rules', function () {
- $component = createTestComponent(
- scenarios: [
- 'specific' => ['title'],
- ],
- );
- $ruleset = createTestRuleset(
- $component,
- rules: [
- 'title' => ['required'],
- 'email' => ['required'],
- ],
- );
- $component->setScenario('undefined-scenario');
-
- $rules = $ruleset->rules();
-
- expect($rules)->toHaveKeys(['title', 'email']);
- });
-});
diff --git a/tests/Unit/Validation/ValidatesWithRulesetTest.php b/tests/Unit/Validation/ValidatesWithRulesetTest.php
index 5256689c34c..79ad9f5a69d 100644
--- a/tests/Unit/Validation/ValidatesWithRulesetTest.php
+++ b/tests/Unit/Validation/ValidatesWithRulesetTest.php
@@ -2,6 +2,7 @@
declare(strict_types=1);
+use CraftCms\Cms\Validation\ComponentRules;
use CraftCms\Cms\Validation\Concerns\Validates;
use CraftCms\Cms\Validation\Contracts\Validatable;
use CraftCms\Cms\Validation\Ruleset;
@@ -24,32 +25,17 @@ public function __construct(
$this->rulesetClass = $rulesetClass ?? TestRuleset::class;
}
- public function getRules(): array
- {
- return [];
- }
-
- public function getMessages(): array
- {
- return [];
- }
-
public function setAttributes(array $values, bool $safeOnly = true): void
{
$this->testAttributes = array_merge($this->testAttributes, $values);
}
- public function getAttributes(): array
+ public function validationData(): array
{
return $this->testAttributes;
}
- public function attributes(): array
- {
- return array_keys($this->testAttributes);
- }
-
- public function rulesClass(): string
+ public function ruleset(): string
{
return $this->rulesetClass;
}
@@ -58,15 +44,10 @@ public function afterValidate(?Validator $validator = null): void
{
$this->afterValidateCalled = true;
}
-
- public function attributeLabels(): array
- {
- return [];
- }
};
}
-class TestRuleset extends Ruleset
+class TestRuleset extends ComponentRules
{
public bool $prepareForValidationCalled = false;
@@ -81,14 +62,15 @@ public function messages(): array
];
}
- public function prepareForValidation(?array $attributeNames = null): void
+ #[Override]
+ public function prepareForValidation(): void
{
$this->prepareForValidationCalled = true;
- $this->prepareForValidationAttributes = $attributeNames;
+ $this->prepareForValidationAttributes = $this->validationAttributes;
}
#[Override]
- protected function defineRules(): array
+ public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
@@ -99,68 +81,12 @@ protected function defineRules(): array
class EmptyRuleset extends Ruleset
{
- #[Override]
- protected function defineRules(): array
+ public function rules(): array
{
return [];
}
}
-describe('getRuleset', function () {
- test('resolves ruleset via rulesClass method', function () {
- $component = createValidatableComponent(['title' => 'Test']);
-
- $ruleset = $component->getRuleset();
-
- expect($ruleset)->toBeInstanceOf(TestRuleset::class);
- });
-
- test('caches ruleset instance', function () {
- $component = createValidatableComponent(['title' => 'Test']);
-
- $ruleset1 = $component->getRuleset();
- $ruleset2 = $component->getRuleset();
-
- expect($ruleset1)->toBe($ruleset2);
- });
-
- test('returns false when no ruleset configured', function () {
- $component = new class implements Validatable
- {
- use Validates;
-
- public function getRules(): array
- {
- return [];
- }
-
- public function getMessages(): array
- {
- return [];
- }
-
- public function setAttributes(array $values, bool $safeOnly = true): void {}
-
- public function getAttributes(): array
- {
- return [];
- }
-
- public function attributes(): array
- {
- return [];
- }
-
- public function attributeLabels(): array
- {
- return [];
- }
- };
-
- expect($component->getRuleset())->toBeFalse();
- });
-});
-
describe('validate', function () {
test('returns true when validation passes', function () {
$component = createValidatableComponent([
@@ -188,7 +114,7 @@ public function attributeLabels(): array
$component->validate();
- expect($component->getRuleset()->prepareForValidationCalled)->toBeTrue();
+ expect($component->ruleset->prepareForValidationCalled)->toBeTrue();
});
test('passes attribute names to prepareForValidation', function () {
@@ -196,7 +122,7 @@ public function attributeLabels(): array
$component->validate(['title']);
- expect($component->getRuleset()->prepareForValidationAttributes)->toBe(['title']);
+ expect($component->ruleset->prepareForValidationAttributes)->toBe(['title']);
});
test('validates only specified attributes', function () {
diff --git a/yii2-adapter/composer.lock b/yii2-adapter/composer.lock
index d86c974ff21..0c1423c8d4f 100644
--- a/yii2-adapter/composer.lock
+++ b/yii2-adapter/composer.lock
@@ -479,7 +479,7 @@
"dist": {
"type": "path",
"url": "..",
- "reference": "e9bf543d812ecf5ce5452a42d8d7248a582851dd"
+ "reference": "897e980ca4751c82d305e7f66e6e2fec93322d06"
},
"require": {
"bacon/bacon-qr-code": "^2.0",
@@ -487,8 +487,9 @@
"composer/semver": "^3.3.2",
"craftcms/laravel-aliases": "^2.0",
"craftcms/laravel-dependency-aware-cache": "^1.1",
+ "craftcms/laravel-ruleset-validation": "^1.0.1",
"craftcms/plugin-installer": "~1.6.0",
- "craftcms/server-check": "~5.0.1",
+ "craftcms/server-check": "~5.1.0",
"craftcms/yii2-adapter": "self.version",
"elvanto/litemoji": "~4.3.0",
"enshrined/svg-sanitize": "~0.22.0",
@@ -798,6 +799,70 @@
},
"time": "2026-03-17T14:55:31+00:00"
},
+ {
+ "name": "craftcms/laravel-ruleset-validation",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/craftcms/laravel-ruleset-validation.git",
+ "reference": "66867ade9c09c6c8165bd62c0ebb7e4843399aab"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/craftcms/laravel-ruleset-validation/zipball/66867ade9c09c6c8165bd62c0ebb7e4843399aab",
+ "reference": "66867ade9c09c6c8165bd62c0ebb7e4843399aab",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/contracts": "^13.0",
+ "illuminate/support": "^13.0",
+ "illuminate/validation": "^13.0",
+ "php": "^8.4"
+ },
+ "require-dev": {
+ "larastan/larastan": "^3.0",
+ "laravel/pint": "^v1.29",
+ "nunomaduro/collision": "^8.1.1",
+ "orchestra/testbench": "^11.0",
+ "pestphp/pest": "^4.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "CraftCms\\RulesetValidation\\RulesetValidationServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "CraftCms\\RulesetValidation\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Pixel & Tonic",
+ "homepage": "https://pixelandtonic.com/"
+ }
+ ],
+ "description": "Validate requests and objects with reusable Laravel rulesets.",
+ "homepage": "https://github.com/craftcms/laravel-ruleset-validation",
+ "keywords": [
+ "craftcms",
+ "laravel",
+ "rulesets",
+ "validation"
+ ],
+ "support": {
+ "issues": "https://github.com/craftcms/laravel-ruleset-validation/issues",
+ "source": "https://github.com/craftcms/laravel-ruleset-validation/tree/1.0.1"
+ },
+ "time": "2026-04-21T07:56:55+00:00"
+ },
{
"name": "craftcms/plugin-installer",
"version": "1.6.0",
@@ -853,16 +918,16 @@
},
{
"name": "craftcms/server-check",
- "version": "5.0.4",
+ "version": "5.1.0",
"source": {
"type": "git",
"url": "https://github.com/craftcms/server-check.git",
- "reference": "3b1f239c1cc781710978b0baa3e3bc99410d1973"
+ "reference": "7a4f1720c4fe1f0731254a82e63060a217f51cdc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/craftcms/server-check/zipball/3b1f239c1cc781710978b0baa3e3bc99410d1973",
- "reference": "3b1f239c1cc781710978b0baa3e3bc99410d1973",
+ "url": "https://api.github.com/repos/craftcms/server-check/zipball/7a4f1720c4fe1f0731254a82e63060a217f51cdc",
+ "reference": "7a4f1720c4fe1f0731254a82e63060a217f51cdc",
"shasum": ""
},
"type": "library",
@@ -891,7 +956,7 @@
"rss": "https://github.com/craftcms/server-check/releases.atom",
"source": "https://github.com/craftcms/server-check"
},
- "time": "2025-07-23T14:22:43+00:00"
+ "time": "2026-04-07T16:48:35+00:00"
},
{
"name": "creocoder/yii2-nested-sets",
@@ -2190,16 +2255,16 @@
},
{
"name": "inertiajs/inertia-laravel",
- "version": "v3.0.1",
+ "version": "v3.0.6",
"source": {
"type": "git",
"url": "https://github.com/inertiajs/inertia-laravel.git",
- "reference": "4675331c428c0f77b2539684835c5e0fd27ee023"
+ "reference": "c255b1ea050cf563b240542a76f7f756ccdb2d67"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/4675331c428c0f77b2539684835c5e0fd27ee023",
- "reference": "4675331c428c0f77b2539684835c5e0fd27ee023",
+ "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/c255b1ea050cf563b240542a76f7f756ccdb2d67",
+ "reference": "c255b1ea050cf563b240542a76f7f756ccdb2d67",
"shasum": ""
},
"require": {
@@ -2257,22 +2322,22 @@
],
"support": {
"issues": "https://github.com/inertiajs/inertia-laravel/issues",
- "source": "https://github.com/inertiajs/inertia-laravel/tree/v3.0.1"
+ "source": "https://github.com/inertiajs/inertia-laravel/tree/v3.0.6"
},
- "time": "2026-03-25T21:07:46+00:00"
+ "time": "2026-04-10T14:29:45+00:00"
},
{
"name": "laravel/framework",
- "version": "v13.2.0",
+ "version": "v13.5.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "9e48d1fe933e89de628dafa167d2c5778566d4cf"
+ "reference": "ffa1850049a691b93129808f27ecd10e65c9d1a5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/9e48d1fe933e89de628dafa167d2c5778566d4cf",
- "reference": "9e48d1fe933e89de628dafa167d2c5778566d4cf",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/ffa1850049a691b93129808f27ecd10e65c9d1a5",
+ "reference": "ffa1850049a691b93129808f27ecd10e65c9d1a5",
"shasum": ""
},
"require": {
@@ -2390,6 +2455,7 @@
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3",
"predis/predis": "^2.3 || ^3.0",
+ "rector/rector": "^2.3",
"resend/resend-php": "^1.0",
"symfony/cache": "^7.4.0 || ^8.0.0",
"symfony/http-client": "^7.4.0 || ^8.0.0",
@@ -2425,6 +2491,7 @@
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).",
"resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).",
+ "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).",
"symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).",
"symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).",
"symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).",
@@ -2480,20 +2547,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-03-24T18:42:09+00:00"
+ "time": "2026-04-14T13:55:03+00:00"
},
{
"name": "laravel/prompts",
- "version": "v0.3.16",
+ "version": "v0.3.17",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
- "reference": "11e7d5f93803a2190b00e145142cb00a33d17ad2"
+ "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/prompts/zipball/11e7d5f93803a2190b00e145142cb00a33d17ad2",
- "reference": "11e7d5f93803a2190b00e145142cb00a33d17ad2",
+ "url": "https://api.github.com/repos/laravel/prompts/zipball/6a82ac19a28b916ae0885828795dbd4c59d9a818",
+ "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818",
"shasum": ""
},
"require": {
@@ -2537,22 +2604,22 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": {
"issues": "https://github.com/laravel/prompts/issues",
- "source": "https://github.com/laravel/prompts/tree/v0.3.16"
+ "source": "https://github.com/laravel/prompts/tree/v0.3.17"
},
- "time": "2026-03-23T14:35:33+00:00"
+ "time": "2026-04-20T16:07:33+00:00"
},
{
"name": "laravel/serializable-closure",
- "version": "v2.0.10",
+ "version": "v2.0.12",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
- "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669"
+ "reference": "a6abb4e54f6fcd3138120b9ad497f0bd146f9919"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669",
- "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669",
+ "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/a6abb4e54f6fcd3138120b9ad497f0bd146f9919",
+ "reference": "a6abb4e54f6fcd3138120b9ad497f0bd146f9919",
"shasum": ""
},
"require": {
@@ -2600,20 +2667,20 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
- "time": "2026-02-20T19:59:49+00:00"
+ "time": "2026-04-14T13:33:34+00:00"
},
{
"name": "laravel/wayfinder",
- "version": "v0.1.15",
+ "version": "v0.1.16",
"source": {
"type": "git",
"url": "https://github.com/laravel/wayfinder.git",
- "reference": "25b8a947af54d35106dc04e933d05a6b7fad1133"
+ "reference": "6a5c695dedb77a793bba6dc062bdddea94253517"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/wayfinder/zipball/25b8a947af54d35106dc04e933d05a6b7fad1133",
- "reference": "25b8a947af54d35106dc04e933d05a6b7fad1133",
+ "url": "https://api.github.com/repos/laravel/wayfinder/zipball/6a5c695dedb77a793bba6dc062bdddea94253517",
+ "reference": "6a5c695dedb77a793bba6dc062bdddea94253517",
"shasum": ""
},
"require": {
@@ -2663,7 +2730,7 @@
"issues": "https://github.com/laravel/wayfinder/issues",
"source": "https://github.com/laravel/wayfinder"
},
- "time": "2026-03-25T20:46:44+00:00"
+ "time": "2026-04-07T17:07:48+00:00"
},
{
"name": "league/commonmark",
@@ -3272,16 +3339,16 @@
},
{
"name": "maennchen/zipstream-php",
- "version": "3.2.1",
+ "version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
- "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5"
+ "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5",
- "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5",
+ "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
+ "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
"shasum": ""
},
"require": {
@@ -3338,7 +3405,7 @@
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
- "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1"
+ "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.2"
},
"funding": [
{
@@ -3346,7 +3413,7 @@
"type": "github"
}
],
- "time": "2025-12-10T09:58:31+00:00"
+ "time": "2026-04-11T18:38:28+00:00"
},
{
"name": "markbaker/complex",
@@ -3763,16 +3830,16 @@
},
{
"name": "nesbot/carbon",
- "version": "3.11.3",
+ "version": "3.11.4",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
- "reference": "6a7e652845bb018c668220c2a545aded8594fbbf"
+ "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6a7e652845bb018c668220c2a545aded8594fbbf",
- "reference": "6a7e652845bb018c668220c2a545aded8594fbbf",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60",
+ "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60",
"shasum": ""
},
"require": {
@@ -3864,7 +3931,7 @@
"type": "tidelift"
}
],
- "time": "2026-03-11T17:23:39+00:00"
+ "time": "2026-04-07T09:57:54+00:00"
},
{
"name": "nette/schema",
@@ -4357,16 +4424,16 @@
},
{
"name": "phpoffice/phpspreadsheet",
- "version": "5.5.0",
+ "version": "5.7.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
- "reference": "eecd31b885a1c8192f12738130f85bbc6e8906ba"
+ "reference": "9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/eecd31b885a1c8192f12738130f85bbc6e8906ba",
- "reference": "eecd31b885a1c8192f12738130f85bbc6e8906ba",
+ "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8",
+ "reference": "9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8",
"shasum": ""
},
"require": {
@@ -4460,9 +4527,9 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
- "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.5.0"
+ "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.7.0"
},
- "time": "2026-03-01T00:58:56+00:00"
+ "time": "2026-04-20T02:42:17+00:00"
},
{
"name": "phpoption/phpoption",
@@ -5563,20 +5630,20 @@
},
{
"name": "spomky-labs/cbor-php",
- "version": "3.2.2",
+ "version": "3.2.3",
"source": {
"type": "git",
"url": "https://github.com/Spomky-Labs/cbor-php.git",
- "reference": "2a5fb86aacfe1004611370ead6caa2bfc88435d0"
+ "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/2a5fb86aacfe1004611370ead6caa2bfc88435d0",
- "reference": "2a5fb86aacfe1004611370ead6caa2bfc88435d0",
+ "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32",
+ "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32",
"shasum": ""
},
"require": {
- "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14",
+ "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17",
"ext-mbstring": "*",
"php": ">=8.0"
},
@@ -5618,7 +5685,7 @@
],
"support": {
"issues": "https://github.com/Spomky-Labs/cbor-php/issues",
- "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.2.2"
+ "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.2.3"
},
"funding": [
{
@@ -5630,7 +5697,7 @@
"type": "patreon"
}
],
- "time": "2025-11-13T13:00:34+00:00"
+ "time": "2026-04-01T12:15:20+00:00"
},
{
"name": "spomky-labs/pki-framework",
@@ -5744,16 +5811,16 @@
},
{
"name": "symfony/clock",
- "version": "v7.4.0",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/clock.git",
- "reference": "9169f24776edde469914c1e7a1442a50f7a4e110"
+ "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110",
- "reference": "9169f24776edde469914c1e7a1442a50f7a4e110",
+ "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111",
+ "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111",
"shasum": ""
},
"require": {
@@ -5798,7 +5865,7 @@
"time"
],
"support": {
- "source": "https://github.com/symfony/clock/tree/v7.4.0"
+ "source": "https://github.com/symfony/clock/tree/v7.4.8"
},
"funding": [
{
@@ -5818,20 +5885,20 @@
"type": "tidelift"
}
],
- "time": "2025-11-12T15:39:26+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/console",
- "version": "v8.0.7",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a"
+ "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
- "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
+ "url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7",
+ "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7",
"shasum": ""
},
"require": {
@@ -5888,7 +5955,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v8.0.7"
+ "source": "https://github.com/symfony/console/tree/v8.0.8"
},
"funding": [
{
@@ -5908,20 +5975,20 @@
"type": "tidelift"
}
],
- "time": "2026-03-06T14:06:22+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v7.4.6",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
- "reference": "2e7c52c647b406e2107dd867db424a4dbac91864"
+ "reference": "b055f228a4178a1d6774909903905e3475f3eac8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/css-selector/zipball/2e7c52c647b406e2107dd867db424a4dbac91864",
- "reference": "2e7c52c647b406e2107dd867db424a4dbac91864",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/b055f228a4178a1d6774909903905e3475f3eac8",
+ "reference": "b055f228a4178a1d6774909903905e3475f3eac8",
"shasum": ""
},
"require": {
@@ -5957,7 +6024,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/css-selector/tree/v7.4.6"
+ "source": "https://github.com/symfony/css-selector/tree/v7.4.8"
},
"funding": [
{
@@ -5977,7 +6044,7 @@
"type": "tidelift"
}
],
- "time": "2026-02-17T07:53:42+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -6048,16 +6115,16 @@
},
{
"name": "symfony/dom-crawler",
- "version": "v7.4.6",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
- "reference": "487ba8fa43da9a8e6503fe939b45ecd96875410e"
+ "reference": "2918e7c2ba964defca1f5b69c6f74886529e2dc8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/487ba8fa43da9a8e6503fe939b45ecd96875410e",
- "reference": "487ba8fa43da9a8e6503fe939b45ecd96875410e",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2918e7c2ba964defca1f5b69c6f74886529e2dc8",
+ "reference": "2918e7c2ba964defca1f5b69c6f74886529e2dc8",
"shasum": ""
},
"require": {
@@ -6096,7 +6163,7 @@
"description": "Eases DOM navigation for HTML and XML documents",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/dom-crawler/tree/v7.4.6"
+ "source": "https://github.com/symfony/dom-crawler/tree/v7.4.8"
},
"funding": [
{
@@ -6116,20 +6183,20 @@
"type": "tidelift"
}
],
- "time": "2026-02-17T07:53:42+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/error-handler",
- "version": "v8.0.4",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a"
+ "reference": "c1119fe8dcfc3825ec74ec061b96ef0c8f281517"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/error-handler/zipball/7620b97ec0ab1d2d6c7fb737aa55da411bea776a",
- "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/c1119fe8dcfc3825ec74ec061b96ef0c8f281517",
+ "reference": "c1119fe8dcfc3825ec74ec061b96ef0c8f281517",
"shasum": ""
},
"require": {
@@ -6177,7 +6244,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/error-handler/tree/v8.0.4"
+ "source": "https://github.com/symfony/error-handler/tree/v8.0.8"
},
"funding": [
{
@@ -6197,20 +6264,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-23T11:07:10+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v8.0.4",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "99301401da182b6cfaa4700dbe9987bb75474b47"
+ "reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47",
- "reference": "99301401da182b6cfaa4700dbe9987bb75474b47",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f662acc6ab22a3d6d716dcb44c381c6002940df6",
+ "reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6",
"shasum": ""
},
"require": {
@@ -6262,7 +6329,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.8"
},
"funding": [
{
@@ -6282,7 +6349,7 @@
"type": "tidelift"
}
],
- "time": "2026-01-05T11:45:55+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -6362,16 +6429,16 @@
},
{
"name": "symfony/filesystem",
- "version": "v8.0.6",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "7bf9162d7a0dff98d079b72948508fa48018a770"
+ "reference": "66b769ae743ce2d13e435528fbef4af03d623e5a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770",
- "reference": "7bf9162d7a0dff98d079b72948508fa48018a770",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/66b769ae743ce2d13e435528fbef4af03d623e5a",
+ "reference": "66b769ae743ce2d13e435528fbef4af03d623e5a",
"shasum": ""
},
"require": {
@@ -6408,7 +6475,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v8.0.6"
+ "source": "https://github.com/symfony/filesystem/tree/v8.0.8"
},
"funding": [
{
@@ -6428,20 +6495,20 @@
"type": "tidelift"
}
],
- "time": "2026-02-25T16:59:43+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/finder",
- "version": "v8.0.6",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c"
+ "reference": "8da41214757b87d97f181e3d14a4179286151007"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c",
- "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/8da41214757b87d97f181e3d14a4179286151007",
+ "reference": "8da41214757b87d97f181e3d14a4179286151007",
"shasum": ""
},
"require": {
@@ -6476,7 +6543,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v8.0.6"
+ "source": "https://github.com/symfony/finder/tree/v8.0.8"
},
"funding": [
{
@@ -6496,20 +6563,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-29T09:41:02+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/html-sanitizer",
- "version": "v8.0.7",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/html-sanitizer.git",
- "reference": "555b37caeee3d07af33471e02377d5ff561f8ac2"
+ "reference": "b0e4a2d9a82ab6bdcc742a63398781f6dae64fe5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/555b37caeee3d07af33471e02377d5ff561f8ac2",
- "reference": "555b37caeee3d07af33471e02377d5ff561f8ac2",
+ "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/b0e4a2d9a82ab6bdcc742a63398781f6dae64fe5",
+ "reference": "b0e4a2d9a82ab6bdcc742a63398781f6dae64fe5",
"shasum": ""
},
"require": {
@@ -6548,7 +6615,7 @@
"sanitizer"
],
"support": {
- "source": "https://github.com/symfony/html-sanitizer/tree/v8.0.7"
+ "source": "https://github.com/symfony/html-sanitizer/tree/v8.0.8"
},
"funding": [
{
@@ -6568,20 +6635,20 @@
"type": "tidelift"
}
],
- "time": "2026-03-06T13:17:40+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/http-foundation",
- "version": "v8.0.7",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "c5ecf7b07408dbc4a87482634307654190954ae8"
+ "reference": "02656f7ebeae5c155d659e946f6b3a33df24051b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/c5ecf7b07408dbc4a87482634307654190954ae8",
- "reference": "c5ecf7b07408dbc4a87482634307654190954ae8",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/02656f7ebeae5c155d659e946f6b3a33df24051b",
+ "reference": "02656f7ebeae5c155d659e946f6b3a33df24051b",
"shasum": ""
},
"require": {
@@ -6628,7 +6695,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v8.0.7"
+ "source": "https://github.com/symfony/http-foundation/tree/v8.0.8"
},
"funding": [
{
@@ -6648,20 +6715,20 @@
"type": "tidelift"
}
],
- "time": "2026-03-06T13:17:40+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v8.0.7",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "c04721f45723d8ce049fa3eee378b5a505272ac7"
+ "reference": "1770f6818d83b2fddc12185025b93f39a90cb628"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/c04721f45723d8ce049fa3eee378b5a505272ac7",
- "reference": "c04721f45723d8ce049fa3eee378b5a505272ac7",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1770f6818d83b2fddc12185025b93f39a90cb628",
+ "reference": "1770f6818d83b2fddc12185025b93f39a90cb628",
"shasum": ""
},
"require": {
@@ -6732,7 +6799,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-kernel/tree/v8.0.7"
+ "source": "https://github.com/symfony/http-kernel/tree/v8.0.8"
},
"funding": [
{
@@ -6752,20 +6819,20 @@
"type": "tidelift"
}
],
- "time": "2026-03-06T16:58:46+00:00"
+ "time": "2026-03-31T21:14:05+00:00"
},
{
"name": "symfony/mailer",
- "version": "v7.4.6",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
- "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9"
+ "reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9",
- "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9",
+ "url": "https://api.github.com/repos/symfony/mailer/zipball/f6ea532250b476bfc1b56699b388a1bdbf168f62",
+ "reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62",
"shasum": ""
},
"require": {
@@ -6816,7 +6883,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/mailer/tree/v7.4.6"
+ "source": "https://github.com/symfony/mailer/tree/v7.4.8"
},
"funding": [
{
@@ -6836,20 +6903,20 @@
"type": "tidelift"
}
],
- "time": "2026-02-25T16:50:00+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/mime",
- "version": "v7.4.7",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1"
+ "reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/da5ab4fde3f6c88ab06e96185b9922f48b677cd1",
- "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/6df02f99998081032da3407a8d6c4e1dcb5d4379",
+ "reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379",
"shasum": ""
},
"require": {
@@ -6905,7 +6972,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v7.4.7"
+ "source": "https://github.com/symfony/mime/tree/v7.4.8"
},
"funding": [
{
@@ -6925,20 +6992,20 @@
"type": "tidelift"
}
],
- "time": "2026-03-05T15:24:09+00:00"
+ "time": "2026-03-30T14:11:46+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.33.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
+ "reference": "141046a8f9477948ff284fa65be2095baafb94f2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
- "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
+ "reference": "141046a8f9477948ff284fa65be2095baafb94f2",
"shasum": ""
},
"require": {
@@ -6988,7 +7055,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0"
},
"funding": [
{
@@ -7008,20 +7075,20 @@
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.33.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
+ "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
- "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
+ "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"shasum": ""
},
"require": {
@@ -7070,7 +7137,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0"
},
"funding": [
{
@@ -7090,11 +7157,11 @@
"type": "tidelift"
}
],
- "time": "2025-06-27T09:58:17+00:00"
+ "time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
- "version": "v1.33.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
@@ -7157,7 +7224,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.36.0"
},
"funding": [
{
@@ -7181,7 +7248,7 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.33.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -7242,7 +7309,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0"
},
"funding": [
{
@@ -7266,16 +7333,16 @@
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.33.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
+ "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
- "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
+ "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
"shasum": ""
},
"require": {
@@ -7327,7 +7394,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0"
},
"funding": [
{
@@ -7347,20 +7414,20 @@
"type": "tidelift"
}
],
- "time": "2024-12-23T08:48:59+00:00"
+ "time": "2026-04-10T17:25:58+00:00"
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.33.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
+ "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
- "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
+ "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
"shasum": ""
},
"require": {
@@ -7411,7 +7478,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.36.0"
},
"funding": [
{
@@ -7431,20 +7498,20 @@
"type": "tidelift"
}
],
- "time": "2025-01-02T08:10:11+00:00"
+ "time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-php83",
- "version": "v1.33.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
- "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
+ "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
- "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149",
+ "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149",
"shasum": ""
},
"require": {
@@ -7491,7 +7558,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.36.0"
},
"funding": [
{
@@ -7511,20 +7578,20 @@
"type": "tidelift"
}
],
- "time": "2025-07-08T02:45:35+00:00"
+ "time": "2026-04-10T17:25:58+00:00"
},
{
"name": "symfony/polyfill-php84",
- "version": "v1.33.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php84.git",
- "reference": "d8ced4d875142b6a7426000426b8abc631d6b191"
+ "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191",
- "reference": "d8ced4d875142b6a7426000426b8abc631d6b191",
+ "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06",
+ "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06",
"shasum": ""
},
"require": {
@@ -7571,7 +7638,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0"
+ "source": "https://github.com/symfony/polyfill-php84/tree/v1.36.0"
},
"funding": [
{
@@ -7591,20 +7658,20 @@
"type": "tidelift"
}
],
- "time": "2025-06-24T13:30:11+00:00"
+ "time": "2026-04-10T18:47:49+00:00"
},
{
"name": "symfony/polyfill-php85",
- "version": "v1.33.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php85.git",
- "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91"
+ "reference": "2c408a6bb0313e6001a83628dc5506100474254e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
- "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
+ "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e",
+ "reference": "2c408a6bb0313e6001a83628dc5506100474254e",
"shasum": ""
},
"require": {
@@ -7651,7 +7718,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0"
+ "source": "https://github.com/symfony/polyfill-php85/tree/v1.36.0"
},
"funding": [
{
@@ -7671,20 +7738,20 @@
"type": "tidelift"
}
],
- "time": "2025-06-23T16:12:55+00:00"
+ "time": "2026-04-10T16:50:15+00:00"
},
{
"name": "symfony/polyfill-uuid",
- "version": "v1.33.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git",
- "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2"
+ "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
- "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
+ "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94",
+ "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94",
"shasum": ""
},
"require": {
@@ -7734,7 +7801,7 @@
"uuid"
],
"support": {
- "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0"
+ "source": "https://github.com/symfony/polyfill-uuid/tree/v1.36.0"
},
"funding": [
{
@@ -7754,20 +7821,20 @@
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/process",
- "version": "v7.4.5",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "608476f4604102976d687c483ac63a79ba18cc97"
+ "reference": "60f19cd3badc8de688421e21e4305eba50f8089a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97",
- "reference": "608476f4604102976d687c483ac63a79ba18cc97",
+ "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a",
+ "reference": "60f19cd3badc8de688421e21e4305eba50f8089a",
"shasum": ""
},
"require": {
@@ -7799,7 +7866,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.4.5"
+ "source": "https://github.com/symfony/process/tree/v7.4.8"
},
"funding": [
{
@@ -7819,20 +7886,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-26T15:07:59+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/property-access",
- "version": "v7.4.4",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-access.git",
- "reference": "fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1"
+ "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/property-access/zipball/fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1",
- "reference": "fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1",
+ "url": "https://api.github.com/repos/symfony/property-access/zipball/b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc",
+ "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc",
"shasum": ""
},
"require": {
@@ -7880,7 +7947,7 @@
"reflection"
],
"support": {
- "source": "https://github.com/symfony/property-access/tree/v7.4.4"
+ "source": "https://github.com/symfony/property-access/tree/v7.4.8"
},
"funding": [
{
@@ -7900,20 +7967,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-05T08:47:25+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/property-info",
- "version": "v7.4.7",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-info.git",
- "reference": "02501d75fd834345da3ecdd8e3200ced39e370f8"
+ "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/property-info/zipball/02501d75fd834345da3ecdd8e3200ced39e370f8",
- "reference": "02501d75fd834345da3ecdd8e3200ced39e370f8",
+ "url": "https://api.github.com/repos/symfony/property-info/zipball/ac5e82528b986c4f7cfccbf7764b5d2e824d6175",
+ "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175",
"shasum": ""
},
"require": {
@@ -7970,7 +8037,7 @@
"validator"
],
"support": {
- "source": "https://github.com/symfony/property-info/tree/v7.4.7"
+ "source": "https://github.com/symfony/property-info/tree/v7.4.8"
},
"funding": [
{
@@ -7990,20 +8057,20 @@
"type": "tidelift"
}
],
- "time": "2026-03-04T15:53:26+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/routing",
- "version": "v8.0.6",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "053c40fd46e1d19c5c5a94cada93ce6c3facdd55"
+ "reference": "0de330ec2ea922a7b08ec45615bd51179de7fda4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/053c40fd46e1d19c5c5a94cada93ce6c3facdd55",
- "reference": "053c40fd46e1d19c5c5a94cada93ce6c3facdd55",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/0de330ec2ea922a7b08ec45615bd51179de7fda4",
+ "reference": "0de330ec2ea922a7b08ec45615bd51179de7fda4",
"shasum": ""
},
"require": {
@@ -8050,7 +8117,7 @@
"url"
],
"support": {
- "source": "https://github.com/symfony/routing/tree/v8.0.6"
+ "source": "https://github.com/symfony/routing/tree/v8.0.8"
},
"funding": [
{
@@ -8070,20 +8137,20 @@
"type": "tidelift"
}
],
- "time": "2026-02-25T16:59:43+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/serializer",
- "version": "v7.4.7",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/serializer.git",
- "reference": "bd395bbc6fabd136a48e1a6f91b09f88b5050b0b"
+ "reference": "006fd51717addf2df2bd1a64dafef6b7fab6b455"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/serializer/zipball/bd395bbc6fabd136a48e1a6f91b09f88b5050b0b",
- "reference": "bd395bbc6fabd136a48e1a6f91b09f88b5050b0b",
+ "url": "https://api.github.com/repos/symfony/serializer/zipball/006fd51717addf2df2bd1a64dafef6b7fab6b455",
+ "reference": "006fd51717addf2df2bd1a64dafef6b7fab6b455",
"shasum": ""
},
"require": {
@@ -8154,7 +8221,7 @@
"description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/serializer/tree/v7.4.7"
+ "source": "https://github.com/symfony/serializer/tree/v7.4.8"
},
"funding": [
{
@@ -8174,7 +8241,7 @@
"type": "tidelift"
}
],
- "time": "2026-03-06T13:15:18+00:00"
+ "time": "2026-03-30T21:34:42+00:00"
},
{
"name": "symfony/service-contracts",
@@ -8265,16 +8332,16 @@
},
{
"name": "symfony/string",
- "version": "v8.0.6",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
+ "reference": "ae9488f874d7603f9d2dfbf120203882b645d963"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
- "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
+ "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963",
+ "reference": "ae9488f874d7603f9d2dfbf120203882b645d963",
"shasum": ""
},
"require": {
@@ -8331,7 +8398,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v8.0.6"
+ "source": "https://github.com/symfony/string/tree/v8.0.8"
},
"funding": [
{
@@ -8351,20 +8418,20 @@
"type": "tidelift"
}
],
- "time": "2026-02-09T10:14:57+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/translation",
- "version": "v8.0.6",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b"
+ "reference": "27c03ae3940de24ba2f71cfdbac824f2aa1fdf2f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b",
- "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/27c03ae3940de24ba2f71cfdbac824f2aa1fdf2f",
+ "reference": "27c03ae3940de24ba2f71cfdbac824f2aa1fdf2f",
"shasum": ""
},
"require": {
@@ -8424,7 +8491,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/translation/tree/v8.0.6"
+ "source": "https://github.com/symfony/translation/tree/v8.0.8"
},
"funding": [
{
@@ -8444,7 +8511,7 @@
"type": "tidelift"
}
],
- "time": "2026-02-17T13:07:04+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -8530,16 +8597,16 @@
},
{
"name": "symfony/type-info",
- "version": "v8.0.7",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/type-info.git",
- "reference": "3c7de103dd6cb68be24e155838a64ef4a70ae195"
+ "reference": "622d81551770029d44d16be68969712eb47892f1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/type-info/zipball/3c7de103dd6cb68be24e155838a64ef4a70ae195",
- "reference": "3c7de103dd6cb68be24e155838a64ef4a70ae195",
+ "url": "https://api.github.com/repos/symfony/type-info/zipball/622d81551770029d44d16be68969712eb47892f1",
+ "reference": "622d81551770029d44d16be68969712eb47892f1",
"shasum": ""
},
"require": {
@@ -8588,7 +8655,7 @@
"type"
],
"support": {
- "source": "https://github.com/symfony/type-info/tree/v8.0.7"
+ "source": "https://github.com/symfony/type-info/tree/v8.0.8"
},
"funding": [
{
@@ -8608,20 +8675,20 @@
"type": "tidelift"
}
],
- "time": "2026-03-04T13:55:34+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/uid",
- "version": "v7.4.4",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/uid.git",
- "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36"
+ "reference": "6883ebdf7bf6a12b37519dbc0df62b0222401b56"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36",
- "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/6883ebdf7bf6a12b37519dbc0df62b0222401b56",
+ "reference": "6883ebdf7bf6a12b37519dbc0df62b0222401b56",
"shasum": ""
},
"require": {
@@ -8666,7 +8733,7 @@
"uuid"
],
"support": {
- "source": "https://github.com/symfony/uid/tree/v7.4.4"
+ "source": "https://github.com/symfony/uid/tree/v7.4.8"
},
"funding": [
{
@@ -8686,20 +8753,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-03T23:30:35+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v8.0.6",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209"
+ "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209",
- "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfb7badd53bf4177f6e9416cfbbccc13c0e773a1",
+ "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1",
"shasum": ""
},
"require": {
@@ -8753,7 +8820,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v8.0.6"
+ "source": "https://github.com/symfony/var-dumper/tree/v8.0.8"
},
"funding": [
{
@@ -8773,20 +8840,20 @@
"type": "tidelift"
}
],
- "time": "2026-02-15T10:53:29+00:00"
+ "time": "2026-03-31T07:15:36+00:00"
},
{
"name": "symfony/yaml",
- "version": "v8.0.6",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0"
+ "reference": "54174ab48c0c0f9e21512b304be17f8150ccf8f1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/5f006c50a981e1630bbb70ad409c5d85f9a716e0",
- "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/54174ab48c0c0f9e21512b304be17f8150ccf8f1",
+ "reference": "54174ab48c0c0f9e21512b304be17f8150ccf8f1",
"shasum": ""
},
"require": {
@@ -8828,7 +8895,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v8.0.6"
+ "source": "https://github.com/symfony/yaml/tree/v8.0.8"
},
"funding": [
{
@@ -8848,7 +8915,7 @@
"type": "tidelift"
}
],
- "time": "2026-02-09T10:14:57+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "thamtech/yii2-ratelimiter-advanced",
@@ -9298,20 +9365,20 @@
},
{
"name": "web-auth/cose-lib",
- "version": "4.5.0",
+ "version": "4.5.1",
"source": {
"type": "git",
"url": "https://github.com/web-auth/cose-lib.git",
- "reference": "5adac6fe126994a3ee17ed9950efb4947ab132a9"
+ "reference": "3185af4df10dc537b65c140c315b88d15ae15b80"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/5adac6fe126994a3ee17ed9950efb4947ab132a9",
- "reference": "5adac6fe126994a3ee17ed9950efb4947ab132a9",
+ "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/3185af4df10dc537b65c140c315b88d15ae15b80",
+ "reference": "3185af4df10dc537b65c140c315b88d15ae15b80",
"shasum": ""
},
"require": {
- "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14",
+ "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17",
"ext-json": "*",
"ext-openssl": "*",
"php": ">=8.1",
@@ -9353,7 +9420,7 @@
],
"support": {
"issues": "https://github.com/web-auth/cose-lib/issues",
- "source": "https://github.com/web-auth/cose-lib/tree/4.5.0"
+ "source": "https://github.com/web-auth/cose-lib/tree/4.5.1"
},
"funding": [
{
@@ -9365,7 +9432,7 @@
"type": "patreon"
}
],
- "time": "2026-01-03T14:43:18+00:00"
+ "time": "2026-04-01T12:47:39+00:00"
},
{
"name": "web-auth/webauthn-lib",
@@ -9455,16 +9522,16 @@
},
{
"name": "webmozart/assert",
- "version": "2.1.6",
+ "version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
- "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8"
+ "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8",
- "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/eb0d790f735ba6cff25c683a85a1da0eadeff9e4",
+ "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4",
"shasum": ""
},
"require": {
@@ -9511,9 +9578,9 @@
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
- "source": "https://github.com/webmozarts/assert/tree/2.1.6"
+ "source": "https://github.com/webmozarts/assert/tree/2.3.0"
},
- "time": "2026-02-27T10:28:38+00:00"
+ "time": "2026-04-11T10:33:05+00:00"
},
{
"name": "webonyx/graphql-php",
diff --git a/yii2-adapter/legacy/base/Element.php b/yii2-adapter/legacy/base/Element.php
index 1d73e187474..4bbb2c6ac0f 100644
--- a/yii2-adapter/legacy/base/Element.php
+++ b/yii2-adapter/legacy/base/Element.php
@@ -84,6 +84,7 @@
use CraftCms\Cms\Element\Events\Render;
use CraftCms\Cms\Element\Events\SetEagerLoadedElements;
use CraftCms\Cms\Element\Events\SetRoute;
+use CraftCms\Cms\Element\Validation\ElementRules;
use Illuminate\Support\Facades\Event;
use Override;
@@ -97,6 +98,12 @@ abstract class Element extends \CraftCms\Cms\Element\Element
{
use ElementEventConstants;
+ public const string SCENARIO_DEFAULT = ElementRules::SCENARIO_DEFAULT;
+
+ public const string SCENARIO_ESSENTIALS = ElementRules::SCENARIO_ESSENTIALS;
+
+ public const string SCENARIO_LIVE = ElementRules::SCENARIO_LIVE;
+
public function init(): void
{
parent::init();
diff --git a/yii2-adapter/legacy/base/Field.php b/yii2-adapter/legacy/base/Field.php
index e6835674b21..983f77a68be 100644
--- a/yii2-adapter/legacy/base/Field.php
+++ b/yii2-adapter/legacy/base/Field.php
@@ -10,6 +10,7 @@
use Closure;
use Craft;
use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\Enums\TranslationMethod;
use CraftCms\Cms\Support\Arr;
use Illuminate\Contracts\Database\Query\Builder;
@@ -78,14 +79,14 @@ public function getElementRules(ElementInterface $element): array
{
return [
function(string $attribute, mixed $value, Closure $fail) use ($element) {
- $scenario = $element->getScenario();
+ $scenario = $element->ruleset->getScenario();
$isEmpty = fn() => $this->isValueEmpty($element->getFieldValue($this->handle), $element);
foreach ($this->getElementValidationRules() as $rule) {
$validator = $this->_normalizeFieldValidator($attribute, $rule, $element, $isEmpty);
if (
- in_array($element->getScenario(), $validator->on) ||
+ in_array($element->ruleset->getScenario(), $validator->on) ||
(empty($validator->on) && !in_array($scenario, $validator->except))
) {
$validator->validateAttributes($element);
@@ -113,7 +114,7 @@ private function _normalizeFieldValidator(
if (is_string($rule)) {
// "Validator" syntax
- $rule = [$attribute, $rule, 'on' => [Element::SCENARIO_DEFAULT, Element::SCENARIO_LIVE]];
+ $rule = [$attribute, $rule, 'on' => [ElementRules::SCENARIO_DEFAULT, ElementRules::SCENARIO_LIVE]];
}
if (!is_array($rule) || !isset($rule[0])) {
@@ -151,7 +152,7 @@ private function _normalizeFieldValidator(
// Set 'on' to the main scenarios by default
if (!array_key_exists('on', $rule)) {
- $rule['on'] = [Element::SCENARIO_DEFAULT, Element::SCENARIO_LIVE];
+ $rule['on'] = [ElementRules::SCENARIO_DEFAULT, ElementRules::SCENARIO_LIVE];
}
return Validator::createValidator($rule[1], $element, (array) $rule[0], array_slice($rule, 2));
diff --git a/yii2-adapter/legacy/base/Model.php b/yii2-adapter/legacy/base/Model.php
index e3939b90fe1..a06938ff565 100644
--- a/yii2-adapter/legacy/base/Model.php
+++ b/yii2-adapter/legacy/base/Model.php
@@ -14,9 +14,14 @@
use craft\helpers\App;
use craft\helpers\Component;
use craft\helpers\DateTimeHelper;
+use CraftCms\Cms\Support\Arr;
use CraftCms\Cms\Support\Str;
use CraftCms\Cms\Support\Typecast;
+use CraftCms\Cms\Support\Utils;
+use CraftCms\Cms\Validation\ComponentRules;
use CraftCms\Cms\Validation\Contracts\Validatable;
+use CraftCms\RulesetValidation\Attributes\Ruleset;
+use CraftCms\RulesetValidation\Concerns\HasRuleset;
use Illuminate\Contracts\Support\MessageBag;
use yii\validators\Validator;
@@ -27,9 +32,11 @@
* @author Pixel & Tonic, Inc.
* @since 3.0.0
*/
+#[Ruleset(ComponentRules::class)]
abstract class Model extends \yii\base\Model implements ModelInterface, Validatable
{
use ClonefixTrait;
+ use HasRuleset;
/**
* @event \yii\base\Event The event that is triggered after the model's init cycle
@@ -255,7 +262,17 @@ public function setAttributes($values, $safeOnly = true): void
public function getAttributes($names = null, $except = []): array
{
- return parent::getAttributes($names, $except);
+ $attributes = $this->validationData($names, $except);
+
+ if ($names !== null) {
+ $attributes = Arr::only($attributes, $names);
+ }
+
+ if ($except !== []) {
+ $attributes = Arr::except($attributes, $except);
+ }
+
+ return $attributes;
}
public function attributes(): array
@@ -364,9 +381,8 @@ public function getErrorSummary($showAllErrors): array
return parent::getErrorSummary($showAllErrors);
}
- public function beforeValidate(): bool
+ public function prepareForValidation(): void
{
- return true;
}
public function afterValidate(?\Illuminate\Validation\Validator $validator = null): void
@@ -432,6 +448,11 @@ public function getValidationData(): array
return [];
}
+ public function validationData($names = null, $except = []): array
+ {
+ return Arr::except(Utils::getPublicProperties($this), ['ruleset']);
+ }
+
public function attributeLabels(): array
{
return parent::attributeLabels();
@@ -439,6 +460,6 @@ public function attributeLabels(): array
public function inScenarios(string ...$scenarios): bool
{
- return in_array($this->getScenario(), $scenarios, true);
+ return in_array($this->ruleset->getScenario(), $scenarios, true);
}
}
diff --git a/yii2-adapter/legacy/console/controllers/ResaveController.php b/yii2-adapter/legacy/console/controllers/ResaveController.php
index 7a6448a57a5..27e24a4aa4d 100644
--- a/yii2-adapter/legacy/console/controllers/ResaveController.php
+++ b/yii2-adapter/legacy/console/controllers/ResaveController.php
@@ -29,6 +29,7 @@
use CraftCms\Cms\Element\Exceptions\InvalidElementException;
use CraftCms\Cms\Element\Jobs\ResaveElements;
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\Field\Fields;
use CraftCms\Cms\FieldLayout\FieldLayout;
@@ -613,7 +614,7 @@ private function _resaveElements(ElementQueryInterface $query): int
$set = false;
}
} elseif ($this->ifInvalid) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
if ($element->validate($this->set) && $element->validate("field:$this->set")) {
$set = false;
}
diff --git a/yii2-adapter/legacy/controllers/AssetsController.php b/yii2-adapter/legacy/controllers/AssetsController.php
index 7eb3d6fba6d..719f10f4921 100644
--- a/yii2-adapter/legacy/controllers/AssetsController.php
+++ b/yii2-adapter/legacy/controllers/AssetsController.php
@@ -12,7 +12,7 @@
use craft\web\Controller;
use craft\web\UploadedFile;
use CraftCms\Cms\Asset\Elements\Asset;
-use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Support\Facades\Deprecator;
use CraftCms\Cms\Support\Facades\Elements;
use CraftCms\Cms\Support\Facades\Sites;
@@ -83,7 +83,7 @@ public function actionSaveAsset(): ?Response
$asset->setFieldValuesFromRequest($fieldsLocation);
// Save the asset
- $asset->setScenario(Element::SCENARIO_LIVE);
+ $asset->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
if (!Elements::saveElement($asset)) {
return $this->asModelFailure(
diff --git a/yii2-adapter/legacy/controllers/CategoriesController.php b/yii2-adapter/legacy/controllers/CategoriesController.php
index 8cbbfe48f13..beabda42ef2 100644
--- a/yii2-adapter/legacy/controllers/CategoriesController.php
+++ b/yii2-adapter/legacy/controllers/CategoriesController.php
@@ -15,9 +15,9 @@
use CraftCms\Cms\Cms;
use CraftCms\Cms\Cp\RequestedSite;
use CraftCms\Cms\Element\Drafts;
-use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\ElementHelper;
use CraftCms\Cms\Element\Exceptions\InvalidElementException;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\Fields;
use CraftCms\Cms\Support\Facades\Elements;
use CraftCms\Cms\Support\Facades\Sites;
@@ -292,7 +292,7 @@ public function actionCreate(string $groupHandle): ?Response
}
// Save it
- $category->setScenario(Element::SCENARIO_ESSENTIALS);
+ $category->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
if (!app(Drafts::class)->saveElementAsDraft($category, Craft::$app->getUser()->getId(), null, null, false)) {
return $this->asModelFailure($category, mb_ucfirst(t('Couldn’t create {type}.', [
'type' => Category::lowerDisplayName(),
@@ -378,7 +378,7 @@ public function actionSaveCategory(): ?Response
// Save the category
if ($category->enabled && $category->getEnabledForSite()) {
- $category->setScenario(Element::SCENARIO_LIVE);
+ $category->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
}
if (!Elements::saveElement($category)) {
diff --git a/yii2-adapter/legacy/controllers/ElementIndexesController.php b/yii2-adapter/legacy/controllers/ElementIndexesController.php
index 133c8c23763..bcc33f3f92a 100644
--- a/yii2-adapter/legacy/controllers/ElementIndexesController.php
+++ b/yii2-adapter/legacy/controllers/ElementIndexesController.php
@@ -21,6 +21,7 @@
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
use CraftCms\Cms\Element\Queries\ElementQuery;
use CraftCms\Cms\Element\Queries\ExcludeDescendantIdsExpression;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\FieldLayout\FieldLayout;
use CraftCms\Cms\Support\Arr;
use CraftCms\Cms\Support\Facades\Conditions;
@@ -379,18 +380,18 @@ public function actionSaveElements(): Response
foreach ($elements as $element) {
$attributes = Arr::except($data["element-$element->id"], 'fields');
if (!empty($attributes)) {
- $scenario = $element->getScenario();
- $element->setScenario(Element::SCENARIO_LIVE);
+ $scenario = $element->ruleset->getScenario();
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
$element->setAttributesFromRequest($attributes);
- $element->setScenario($scenario);
+ $element->ruleset->useScenario($scenario);
}
$element->setFieldValuesFromRequest("$namespace.element-$element->id.fields");
if ($element->getIsUnpublishedDraft()) {
- $element->setScenario(Element::SCENARIO_ESSENTIALS);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
} elseif ($element->enabled && $element->getEnabledForSite()) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
}
$names = array_merge(
diff --git a/yii2-adapter/legacy/controllers/ElementsController.php b/yii2-adapter/legacy/controllers/ElementsController.php
index 7c639a5ed1b..da9b411fbe6 100644
--- a/yii2-adapter/legacy/controllers/ElementsController.php
+++ b/yii2-adapter/legacy/controllers/ElementsController.php
@@ -34,6 +34,7 @@
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
use CraftCms\Cms\Element\Queries\Contracts\NestedElementQueryInterface;
use CraftCms\Cms\Element\Revisions;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\FieldLayout\FieldLayoutForm;
use CraftCms\Cms\FieldLayout\LayoutElements\BaseField;
use CraftCms\Cms\FieldLayout\LayoutElements\CustomField;
@@ -247,7 +248,7 @@ public function actionCreate(): Response
$user = static::currentUser();
// Save it
- $element->setScenario(Element::SCENARIO_ESSENTIALS);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
if (!app(Drafts::class)->saveElementAsDraft($element, $user->id, null, null, false)) {
return $this->_asFailure($element, mb_ucfirst(t('Couldn’t create {type}.', [
'type' => $element::lowerDisplayName(),
@@ -319,7 +320,7 @@ public function actionEdit(?ElementInterface $element, ?int $elementId = null):
// Prevalidate?
if ($this->_prevalidate && $element->enabled && $element->getEnabledForSite()) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
$element->validate();
}
} else {
@@ -1439,7 +1440,7 @@ public function actionSave(): ?Response
Gate::authorize('save', $element);
if ($element->enabled && $element->getEnabledForSite()) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
}
$isNotNew = $element->id;
@@ -1593,7 +1594,7 @@ public function actionSaveNestedElementForDerivative(): ?Response
Gate::authorize('save', $element);
if ($element->enabled && $element->getEnabledForSite()) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
}
try {
@@ -1882,7 +1883,7 @@ public function actionValidate(): ?Response
throw new BadRequestHttpException('No element was identified by the request.');
}
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
if (!$element->validate()) {
return $this->_asFailure($element, t('{type} validation failed.', [
@@ -1984,7 +1985,7 @@ public function actionSaveDraft(): ?Response
$element->isProvisionalDraft = false;
}
- $element->setScenario(Element::SCENARIO_ESSENTIALS);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
// If the field layout ID changed, save all content
$saveContent = $element->getFieldLayout()?->id !== $oldFieldLayoutId;
@@ -2145,7 +2146,7 @@ public function actionApplyDraft(): ?Response
// Validate and save the draft
if ($element->enabled && $element->getEnabledForSite()) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
}
// if we're about to apply an unpublished draft, set propagateRequired to true
@@ -2169,7 +2170,7 @@ public function actionApplyDraft(): ?Response
// save the draft anyway, so we don’t lose the latest changes
// (see https://github.com/craftcms/cms/issues/18657)
$errors = $element->getErrors();
- $element->setScenario(Element::SCENARIO_ESSENTIALS);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
Elements::saveElement($element, saveContent: $saveContent);
$element->clearErrors();
$element->addErrors($errors);
@@ -2361,7 +2362,7 @@ public function actionUpdateFieldLayout(): ?Response
// Prevalidate?
if ($this->_prevalidate && $element->enabled && $element->getEnabledForSite()) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
$element->validate();
}
@@ -2837,15 +2838,15 @@ private function _applyParamsToElement(ElementInterface $element): void
$element->updateSearchIndexImmediately = $this->_updateSearchIndexImmediately;
}
- $scenario = $element->getScenario();
- $element->setScenario(Element::SCENARIO_LIVE);
+ $scenario = $element->ruleset->getScenario();
+ $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
$element->setAttributesFromRequest($this->_attributes + array_filter(['fieldId' => $this->_fieldId]));
if ($this->_slug !== null) {
$element->slug = $this->_slug;
}
- $element->setScenario($scenario);
+ $element->ruleset->useScenario($scenario);
// Now that the element is fully configured, make sure the user can actually view it
if (!Gate::check('view', $element)) {
@@ -2912,7 +2913,7 @@ private function _asSuccess(
$newElement->slug = ElementHelper::tempSlug();
}
- $newElement->setScenario(Element::SCENARIO_ESSENTIALS);
+ $newElement->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS);
if (!app(\CraftCms\Cms\Element\Drafts::class)->saveElementAsDraft($newElement, $user->id, null, null, false)) {
throw new ServerErrorHttpException(sprintf('Unable to create a new element: %s', implode(', ', $element->getErrorSummary(true))));
diff --git a/yii2-adapter/legacy/controllers/GlobalsController.php b/yii2-adapter/legacy/controllers/GlobalsController.php
index bf9272f7436..32689b5b671 100644
--- a/yii2-adapter/legacy/controllers/GlobalsController.php
+++ b/yii2-adapter/legacy/controllers/GlobalsController.php
@@ -11,7 +11,7 @@
use craft\elements\GlobalSet;
use craft\web\Controller;
use CraftCms\Cms\Cp\RequestedSite;
-use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\Fields;
use CraftCms\Cms\Support\Facades\Elements;
use CraftCms\Cms\Support\Facades\Sites;
@@ -224,7 +224,7 @@ public function actionSaveContent(): ?Response
$fieldsLocation = $this->request->getParam('fieldsLocation', 'fields');
$globalSet->setFieldValuesFromRequest($fieldsLocation);
- $globalSet->setScenario(Element::SCENARIO_LIVE);
+ $globalSet->ruleset->useScenario(ElementRules::SCENARIO_LIVE);
if (!Elements::saveElement($globalSet)) {
$this->setFailFlash(mb_ucfirst(t('Couldn’t save {type}.', [
diff --git a/yii2-adapter/legacy/elements/Asset.php b/yii2-adapter/legacy/elements/Asset.php
index ec56af15f60..b87a72e71a6 100644
--- a/yii2-adapter/legacy/elements/Asset.php
+++ b/yii2-adapter/legacy/elements/Asset.php
@@ -19,6 +19,8 @@
use CraftCms\Cms\Asset\Events\BeforeGenerateTransform;
use CraftCms\Cms\Asset\Events\BeforeHandleFile;
use CraftCms\Cms\Asset\Events\DefineAssetUrl;
+use CraftCms\Cms\Asset\Validation\AssetRules;
+use CraftCms\Cms\Element\Validation\ElementRules;
use Illuminate\Support\Facades\Event;
/**
@@ -29,6 +31,22 @@ class Asset extends \CraftCms\Cms\Asset\Elements\Asset
{
use ElementEventConstants;
+ public const string SCENARIO_DEFAULT = ElementRules::SCENARIO_DEFAULT;
+
+ public const string SCENARIO_ESSENTIALS = ElementRules::SCENARIO_ESSENTIALS;
+
+ public const string SCENARIO_LIVE = ElementRules::SCENARIO_LIVE;
+
+ public const string SCENARIO_MOVE = AssetRules::SCENARIO_MOVE;
+
+ public const string SCENARIO_FILEOPS = AssetRules::SCENARIO_FILEOPS;
+
+ public const string SCENARIO_INDEX = AssetRules::SCENARIO_INDEX;
+
+ public const string SCENARIO_CREATE = AssetRules::SCENARIO_CREATE;
+
+ public const string SCENARIO_REPLACE = AssetRules::SCENARIO_REPLACE;
+
// Events
// -------------------------------------------------------------------------
diff --git a/yii2-adapter/legacy/elements/GlobalSet.php b/yii2-adapter/legacy/elements/GlobalSet.php
index 2f23d3b10e9..60398d66836 100644
--- a/yii2-adapter/legacy/elements/GlobalSet.php
+++ b/yii2-adapter/legacy/elements/GlobalSet.php
@@ -13,6 +13,7 @@
use craft\validators\HandleValidator;
use craft\validators\UniqueValidator;
use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Field\Fields;
use CraftCms\Cms\FieldLayout\Contracts\FieldLayoutProviderInterface;
use CraftCms\Cms\FieldLayout\FieldLayout;
@@ -221,14 +222,14 @@ protected function defineRules(): array
['name', 'handle'],
UniqueValidator::class,
'targetClass' => GlobalSetRecord::class,
- 'except' => [self::SCENARIO_ESSENTIALS],
+ 'except' => [ElementRules::SCENARIO_ESSENTIALS],
];
$rules[] = [
['handle'],
HandleValidator::class,
'reservedWords' => ['id', 'dateCreated', 'dateUpdated', 'uid', 'title'],
- 'except' => [self::SCENARIO_ESSENTIALS],
+ 'except' => [ElementRules::SCENARIO_ESSENTIALS],
];
$rules[] = [['fieldLayout'], function() {
diff --git a/yii2-adapter/legacy/elements/Tag.php b/yii2-adapter/legacy/elements/Tag.php
index 233e3033d3e..d9954333ea9 100644
--- a/yii2-adapter/legacy/elements/Tag.php
+++ b/yii2-adapter/legacy/elements/Tag.php
@@ -16,6 +16,7 @@
use craft\records\Tag as TagRecord;
use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface;
use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\FieldLayout\FieldLayout;
use CraftCms\Cms\User\Elements\User;
use GraphQL\Type\Definition\Type;
@@ -213,7 +214,7 @@ protected function defineRules(): array
['title'],
'validateTitle',
'when' => fn(): bool => !$this->errors()->has('groupId') && !$this->errors()->has('title'),
- 'on' => [self::SCENARIO_DEFAULT, self::SCENARIO_LIVE],
+ 'on' => [ElementRules::SCENARIO_DEFAULT, ElementRules::SCENARIO_LIVE],
];
return $rules;
}
diff --git a/yii2-adapter/legacy/elements/User.php b/yii2-adapter/legacy/elements/User.php
index bce6f1e14db..aeec061e31d 100644
--- a/yii2-adapter/legacy/elements/User.php
+++ b/yii2-adapter/legacy/elements/User.php
@@ -14,9 +14,11 @@
use craft\events\AuthenticateUserEvent;
use craft\events\DefineValueEvent;
use CraftCms\Cms\Auth\Events\Authenticating;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\User\Elements\User as UserElement;
use CraftCms\Cms\User\Events\DefineFriendlyName;
use CraftCms\Cms\User\Events\DefineName;
+use CraftCms\Cms\User\Validation\UserRules;
use Illuminate\Support\Facades\Event;
/**
@@ -26,6 +28,18 @@ class User extends UserElement
{
use ElementEventConstants;
+ public const string SCENARIO_DEFAULT = ElementRules::SCENARIO_DEFAULT;
+
+ public const string SCENARIO_ESSENTIALS = ElementRules::SCENARIO_ESSENTIALS;
+
+ public const string SCENARIO_LIVE = ElementRules::SCENARIO_LIVE;
+
+ public const string SCENARIO_ACTIVATION = UserRules::SCENARIO_ACTIVATION;
+
+ public const string SCENARIO_REGISTRATION = UserRules::SCENARIO_REGISTRATION;
+
+ public const string SCENARIO_PASSWORD = UserRules::SCENARIO_PASSWORD;
+
/**
* @event DefineValueEvent The event that is triggered when defining the user’s name, as returned by [[getName()]] or [[__toString()]].
*
diff --git a/yii2-adapter/legacy/services/Auth.php b/yii2-adapter/legacy/services/Auth.php
index 14494e38e48..982ae3fc68b 100644
--- a/yii2-adapter/legacy/services/Auth.php
+++ b/yii2-adapter/legacy/services/Auth.php
@@ -17,6 +17,7 @@
use CraftCms\Cms\Support\Facades\Elements;
use CraftCms\Cms\Support\Json;
use CraftCms\Cms\User\Elements\User;
+use CraftCms\Cms\User\Validation\UserRules;
use CraftCms\Cms\View\TemplateMode;
use DateTime;
use Illuminate\Support\Collection;
@@ -332,7 +333,7 @@ public static function registerEvents(): void
$user = $event->user;
$user->newPassword = $event->newPassword;
- $user->setScenario(User::SCENARIO_PASSWORD);
+ $user->ruleset->useScenario(UserRules::SCENARIO_PASSWORD);
if (!Elements::saveElement($user)) {
$event->status = 'password.save_failed';
diff --git a/yii2-adapter/legacy/services/Globals.php b/yii2-adapter/legacy/services/Globals.php
index ecf982e5862..fc9b3fdd360 100644
--- a/yii2-adapter/legacy/services/Globals.php
+++ b/yii2-adapter/legacy/services/Globals.php
@@ -304,7 +304,7 @@ public function saveSet(GlobalSet $globalSet, bool $runValidation = true): bool
}
// Prevent most custom field validators
- $globalSet->setScenario(GlobalSet::SCENARIO_SAVE_SET);
+ $globalSet->ruleset->useScenario(GlobalSet::SCENARIO_SAVE_SET);
if ($runValidation && !$globalSet->validate()) {
Log::info('Global set not saved due to validation error.', [__METHOD__]);
diff --git a/yii2-adapter/legacy/test/fixtures/elements/BaseElementFixture.php b/yii2-adapter/legacy/test/fixtures/elements/BaseElementFixture.php
index 4617148d197..e2c27daa520 100644
--- a/yii2-adapter/legacy/test/fixtures/elements/BaseElementFixture.php
+++ b/yii2-adapter/legacy/test/fixtures/elements/BaseElementFixture.php
@@ -107,7 +107,7 @@ public function load(): void
$this->populateElement($element, $data);
if ($element->enabled && $element->getIsCanonical() && !$element->isProvisionalDraft) {
- $element->setScenario(Element::SCENARIO_LIVE);
+ $element->ruleset->useScenario(Element::SCENARIO_LIVE);
}
if (!$this->saveElement($element)) {
diff --git a/yii2-adapter/legacy/validators/ElementUriValidator.php b/yii2-adapter/legacy/validators/ElementUriValidator.php
index e62e40001f8..6ddfd037e83 100644
--- a/yii2-adapter/legacy/validators/ElementUriValidator.php
+++ b/yii2-adapter/legacy/validators/ElementUriValidator.php
@@ -9,6 +9,7 @@
use craft\base\ElementInterface;
use CraftCms\Cms\Element\Element;
+use CraftCms\Cms\Element\Validation\ElementRules;
use CraftCms\Cms\Shared\Exceptions\OperationAbortedException;
use CraftCms\Cms\Support\Facades\Elements;
use yii\base\InvalidConfigException;
@@ -50,7 +51,7 @@ public function validateAttribute($model, $attribute): void
// Ignore published drafts if the scenario isn't "live",
// or if the canonical element is enabled and the URI hasn't changed on the draft
if ($model->getIsDraft() && !$model->getIsUnpublishedDraft()) {
- if ($model->getScenario() !== Element::SCENARIO_LIVE) {
+ if ($model->ruleset->getScenario() !== ElementRules::SCENARIO_LIVE) {
return;
}
@@ -72,7 +73,7 @@ public function validateAttribute($model, $attribute): void
if (
$model->enabled &&
$model->getEnabledForSite() &&
- (!$model->getIsUnpublishedDraft() || $model->getScenario() === Element::SCENARIO_LIVE)
+ (!$model->getIsUnpublishedDraft() || $model->ruleset->getScenario() === Element::SCENARIO_LIVE)
) {
$this->addError($model, $attribute, t('Could not generate a unique URI based on the URI format.'));
return;
diff --git a/yii2-adapter/legacy/validators/SlugValidator.php b/yii2-adapter/legacy/validators/SlugValidator.php
index 45db237effd..36a77b9f414 100644
--- a/yii2-adapter/legacy/validators/SlugValidator.php
+++ b/yii2-adapter/legacy/validators/SlugValidator.php
@@ -11,6 +11,7 @@
use CraftCms\Cms\Cms;
use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\ElementHelper;
+use CraftCms\Cms\Element\Validation\ElementRules;
use yii\validators\Validator;
use function CraftCms\Cms\t;
@@ -64,7 +65,7 @@ public function validateAttribute($model, $attribute): void
$isDraft = $model instanceof ElementInterface && $model->getIsDraft();
// If this is a draft with a temp slug, leave it alone
- if ($isDraft && !in_array($model->getScenario(), [Element::SCENARIO_LIVE, Element::SCENARIO_DEFAULT])) {
+ if ($isDraft && !in_array($model->ruleset->getScenario(), [ElementRules::SCENARIO_LIVE, ElementRules::SCENARIO_DEFAULT])) {
if ($isTemp) {
// Leave it alone
return;
diff --git a/yii2-adapter/lib/ar-softdelete/README.md b/yii2-adapter/lib/ar-softdelete/README.md
index d555b2a5764..3589774ebb2 100644
--- a/yii2-adapter/lib/ar-softdelete/README.md
+++ b/yii2-adapter/lib/ar-softdelete/README.md
@@ -160,7 +160,7 @@ class Item extends ActiveRecord
}
$item = Item::findOne($id);
-$item->setScenario('some');
+$item->ruleset->useScenario('some');
$item->delete(); // nothing happens!
```
diff --git a/yii2-adapter/src/Mixins/ValidateMixin.php b/yii2-adapter/src/Mixins/ValidateMixin.php
index 819f70e951d..bc437309ad5 100644
--- a/yii2-adapter/src/Mixins/ValidateMixin.php
+++ b/yii2-adapter/src/Mixins/ValidateMixin.php
@@ -139,4 +139,32 @@ public function rulesClass(): Closure
return LegacyElementRules::class;
};
}
+
+ public function setScenario(): Closure
+ {
+ return function(string $scenario) {
+ Deprecator::log($this::class . '->setScenario', 'Calling `->setScenario` is deprecated. Use `->ruleset->useScenario()` instead.');
+
+ /**
+ * @var \CraftCms\RulesetValidation\Contracts\ValidatesWithRuleset $this
+ *
+ * @phpstan-ignore-next-line
+ */
+ return $this->ruleset->useScenario($scenario);
+ };
+ }
+
+ public function getScenario(): Closure
+ {
+ return function() {
+ Deprecator::log($this::class . '->getScenario', 'Calling `->getScenario` is deprecated. Use `->ruleset->getScenario()` instead.');
+
+ /**
+ * @var \CraftCms\RulesetValidation\Contracts\ValidatesWithRuleset $this
+ *
+ * @phpstan-ignore-next-line
+ */
+ return $this->ruleset->getScenario();
+ };
+ }
}
diff --git a/yii2-adapter/src/Validation/LegacyElementRules.php b/yii2-adapter/src/Validation/LegacyElementRules.php
index 1fe6e94c2bb..107236c2fce 100644
--- a/yii2-adapter/src/Validation/LegacyElementRules.php
+++ b/yii2-adapter/src/Validation/LegacyElementRules.php
@@ -9,22 +9,22 @@
class LegacyElementRules extends ElementRules
{
- protected function defineRules(): array
+ public function rules(): array
{
- $rules = parent::defineRules();
+ $rules = parent::rules();
- $reflectionClass = new ReflectionClass($this->component);
+ $reflectionClass = new ReflectionClass($this->subject);
if (!$reflectionClass->hasMethod('defineRules')) {
return $rules;
}
$method = $reflectionClass->getMethod('defineRules');
- $yiiRules = $method->invoke($this->component);
+ $yiiRules = $method->invoke($this->subject);
return LegacyYiiRules::mergeWildcardRules(
rules: $rules,
- target: $this->component,
+ target: $this->subject,
yiiRules: $yiiRules,
);
}
diff --git a/yii2-adapter/tests/unit/gql/mutations/GeneralMutationResolverTest.php b/yii2-adapter/tests/unit/gql/mutations/GeneralMutationResolverTest.php
index 34ca5cdeb50..450f554ae68 100644
--- a/yii2-adapter/tests/unit/gql/mutations/GeneralMutationResolverTest.php
+++ b/yii2-adapter/tests/unit/gql/mutations/GeneralMutationResolverTest.php
@@ -247,19 +247,19 @@ public function testSavingElementWithoutValidationError(): void
$entry->sectionId = Section::find()->one()->id;
$scenario = Element::SCENARIO_DEFAULT;
- $entry->setScenario($scenario);
+ $entry->ruleset->useScenario($scenario);
$entry->enabled = false;
$this->invokeMethod($this->resolver, 'saveElement', [$entry]);
// Ensure scenario unchanged for disabled elements
- self::assertSame($scenario, $entry->getScenario());
+ self::assertSame($scenario, $entry->ruleset->getScenario());
$entry->enabled = true;
$this->invokeMethod($this->resolver, 'saveElement', [$entry]);
// Ensure scenario changed for enabled elements with the default scenario
- self::assertNotSame($scenario, $entry->getScenario());
+ self::assertNotSame($scenario, $entry->ruleset->getScenario());
}
public function testNestedNormalizers(): void