diff --git a/.agents/skills/testing-guidelines/SKILL.md b/.agents/skills/testing-guidelines/SKILL.md index 8f3d3071061..b7a80b693bd 100644 --- a/.agents/skills/testing-guidelines/SKILL.md +++ b/.agents/skills/testing-guidelines/SKILL.md @@ -37,6 +37,7 @@ uses(UnitTestCase::class)->in('Unit'); ## Core Rules - Do not add comments in test files — no section separators (e.g., `// -- section --`), no inline explanations, no docblocks. Test names should be descriptive enough on their own. Use `describe()` blocks to group related tests instead of comments. +- Keep test-local abstractions proportional to the repetition they remove. Small one-off helpers such as route wrapper closures or tiny passthrough methods usually shouldn’t exist; inline the setup or request unless the extraction materially improves readability or reuse. - Use `CraftCms\Cms\Cms::config()->cpTrigger` when asserting CP URLs; never hard-code `/admin`. - Do not instantiate element classes directly with `new` in tests; use factories to ensure database state. - Prefer factories and element queries over Eloquent models when asserting element behavior. diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index ab45ec0c3dc..446444cc6da 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -52,7 +52,7 @@ - Deprecated `craft\enums\Color`. `CraftCms\Cms\Support\Enums\Color` should be used instead. - Deprecated `craft\enums\AttributeStatus`. `CraftCms\Cms\Element\Enums\AttributeStatus` should be used instead. - Deprecated `craft\enums\CmsEdition`. `CraftCms\Cms\Edition` should be used instead. -- Deprecated `craft\enums\ElementIndexViewMode`. `CraftCms\Cms\Field\Enums\ElementIndexViewMode` should be used instead. +- Deprecated `craft\enums\ElementIndexViewMode`. `CraftCms\Cms\Element\Enums\ElementIndexViewMode` should be used instead. - Deprecated `craft\enums\LicenseKeyStatus`. `CraftCms\Cms\Support\Enums\LicenseKeyStatus` should be used instead. - Deprecated `craft\enums\MenuItemType`. `CraftCms\Cms\Element\Enums\MenuItemType` should be used instead. - Deprecated `craft\enums\PropagationMethod`. `CraftCms\Cms\Element\Enums\PropagationMethod` should be used instead. @@ -440,7 +440,7 @@ Craft 6 now uses [Laravel's authorization system](https://laravel.com/docs/12.x/ - Deprecated `craft\services\Elements::trackActivity()`. `CraftCms\Cms\Element\ElementActivity::trackActivity()` should be used instead. - Added `CraftCms\Cms\Element\Actions\ElementAction`, `CraftCms\Cms\Element\ElementActions`, `CraftCms\Cms\Element\Contracts\DeleteActionInterface`, `CraftCms\Cms\Element\Contracts\ElementActionInterface`, `CraftCms\Cms\Element\Events\AfterPerformAction`, `CraftCms\Cms\Element\Events\BeforePerformAction`, `CraftCms\Cms\Http\Controllers\Elements\PerformElementActionController`, and `CraftCms\Cms\Support\Facades\ElementActions`. - Added Laravel-native element action classes under `CraftCms\Cms\Element\Actions`, `CraftCms\Cms\Asset\Actions`, `CraftCms\Cms\Entry\Actions`, and `CraftCms\Cms\User\Actions`. -- Added `CraftCms\Cms\Element\ElementExporters`, `CraftCms\Cms\Element\Contracts\ElementExporterInterface`, `CraftCms\Cms\Element\Exporters\ElementExporter`, `CraftCms\Cms\Http\Controllers\Elements\ExportElementIndexController`, and `CraftCms\Cms\Support\Facades\ElementExporters`. +- Added `CraftCms\Cms\Element\ElementExporters`, `CraftCms\Cms\Element\Contracts\ElementExporterInterface`, `CraftCms\Cms\Element\Exporters\ElementExporter`, `CraftCms\Cms\Http\Controllers\Elements\ElementIndex\ExportElementIndexController`, and `CraftCms\Cms\Support\Facades\ElementExporters`. - Added Laravel-native element exporter classes under `CraftCms\Cms\Element\Exporters`. - Deprecated `craft\errors\InvalidTypeException`. `CraftCms\Cms\Element\Exceptions\InvalidTypeException` should be used instead. - Deprecated `craft\errors\UnsupportedSiteException`. `CraftCms\Cms\Element\Exceptions\UnsupportedSiteException` should be used instead. @@ -586,12 +586,12 @@ Craft 6 introduces a new validation system that uses Laravel's Validator instead - Deprecated `craft\fieldlayoutelements\addresses\OrganizationTaxIdField`. `CraftCms\Cms\FieldLayout\LayoutElements\addresses\OrganizationTaxIdField` should be used instead. - Deprecated `craft\fieldlayoutelements\assets\AssetTitleField`. `CraftCms\Cms\FieldLayout\LayoutElements\assets\AssetTitleField` should be used instead. - Deprecated `craft\fieldlayoutelements\assets\AltField`. `CraftCms\Cms\FieldLayout\LayoutElements\assets\AltField` should be used instead. -- Deprecated `craft\fieldlayoutelements\entries\EntryTitleField`. `CraftCms\Cms\FieldLayout\LayoutElements\entries\EntryTitleField` should be used instead. -- Deprecated `craft\fieldlayoutelements\users\UsernameField`. `CraftCms\Cms\FieldLayout\LayoutElements\users\UsernameField` should be used instead. -- Deprecated `craft\fieldlayoutelements\users\FullNameField`. `CraftCms\Cms\FieldLayout\LayoutElements\users\FullNameField` should be used instead. -- Deprecated `craft\fieldlayoutelements\users\EmailField`. `CraftCms\Cms\FieldLayout\LayoutElements\users\EmailField` should be used instead. -- Deprecated `craft\fieldlayoutelements\users\AffiliatedSiteField`. `CraftCms\Cms\FieldLayout\LayoutElements\users\AffiliatedSiteField` should be used instead. -- Deprecated `craft\fieldlayoutelements\users\PhotoField`. `CraftCms\Cms\FieldLayout\LayoutElements\users\PhotoField` should be used instead. +- Deprecated `craft\fieldlayoutelements\entries\EntryTitleField`. `CraftCms\Cms\FieldLayout\LayoutElements\Entries\EntryTitleField` should be used instead. +- Deprecated `craft\fieldlayoutelements\users\UsernameField`. `CraftCms\Cms\FieldLayout\LayoutElements\Users\UsernameField` should be used instead. +- Deprecated `craft\fieldlayoutelements\users\FullNameField`. `CraftCms\Cms\FieldLayout\LayoutElements\Users\FullNameField` should be used instead. +- Deprecated `craft\fieldlayoutelements\users\EmailField`. `CraftCms\Cms\FieldLayout\LayoutElements\Users\EmailField` should be used instead. +- Deprecated `craft\fieldlayoutelements\users\AffiliatedSiteField`. `CraftCms\Cms\FieldLayout\LayoutElements\Users\AffiliatedSiteField` should be used instead. +- Deprecated `craft\fieldlayoutelements\users\PhotoField`. `CraftCms\Cms\FieldLayout\LayoutElements\Users\PhotoField` should be used instead. - Deprecated `craft\events\CreateFieldLayoutFormEvent`. `CraftCms\Cms\FieldLayout\Events\CreateFieldLayoutForm` should be used instead. - Deprecated `craft\events\DefineFieldLayoutCustomFieldsEvent`. `CraftCms\Cms\FieldLayout\Events\DefineCustomFields` should be used instead. - Deprecated `craft\events\DefineFieldLayoutElementsEvent`. `CraftCms\Cms\FieldLayout\Events\DefineUIElements` should be used instead. diff --git a/composer.json b/composer.json index c93ea2e1014..15511b6da1b 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +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/laravel-ruleset-validation": "^1.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 17b0f71ec9a..e9a49eefee8 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": "f021fb13ae5803c1ef5aa53145b4e877", + "content-hash": "a6c4f08c1188b01800e240ddb00392ca", "packages": [ { "name": "bacon/bacon-qr-code", @@ -617,16 +617,16 @@ }, { "name": "craftcms/laravel-ruleset-validation", - "version": "1.0.1", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/craftcms/laravel-ruleset-validation.git", - "reference": "66867ade9c09c6c8165bd62c0ebb7e4843399aab" + "reference": "5fd54f4643f4746d6ac662bbec13aa225f62edfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/laravel-ruleset-validation/zipball/66867ade9c09c6c8165bd62c0ebb7e4843399aab", - "reference": "66867ade9c09c6c8165bd62c0ebb7e4843399aab", + "url": "https://api.github.com/repos/craftcms/laravel-ruleset-validation/zipball/5fd54f4643f4746d6ac662bbec13aa225f62edfb", + "reference": "5fd54f4643f4746d6ac662bbec13aa225f62edfb", "shasum": "" }, "require": { @@ -675,9 +675,9 @@ ], "support": { "issues": "https://github.com/craftcms/laravel-ruleset-validation/issues", - "source": "https://github.com/craftcms/laravel-ruleset-validation/tree/1.0.1" + "source": "https://github.com/craftcms/laravel-ruleset-validation/tree/1.1.0" }, - "time": "2026-04-21T07:56:55+00:00" + "time": "2026-04-22T11:01:37+00:00" }, { "name": "craftcms/plugin-installer", @@ -2203,16 +2203,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": { @@ -2270,22 +2270,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.4.0", + "version": "v13.6.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "912de244f88a69742b76e8a2807f6765947776da" + "reference": "416a93ea9c53161e0d4b8a44045f447b65a7d2f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/912de244f88a69742b76e8a2807f6765947776da", - "reference": "912de244f88a69742b76e8a2807f6765947776da", + "url": "https://api.github.com/repos/laravel/framework/zipball/416a93ea9c53161e0d4b8a44045f447b65a7d2f1", + "reference": "416a93ea9c53161e0d4b8a44045f447b65a7d2f1", "shasum": "" }, "require": { @@ -2439,6 +2439,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).", @@ -2494,20 +2495,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-04-07T13:38:26+00:00" + "time": "2026-04-21T13:32:11+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": { @@ -2551,22 +2552,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.11", + "version": "v2.0.12", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "d1af40ac4a6ccc12bd062a7184f63c9995a63bdd" + "reference": "a6abb4e54f6fcd3138120b9ad497f0bd146f9919" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/d1af40ac4a6ccc12bd062a7184f63c9995a63bdd", - "reference": "d1af40ac4a6ccc12bd062a7184f63c9995a63bdd", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/a6abb4e54f6fcd3138120b9ad497f0bd146f9919", + "reference": "a6abb4e54f6fcd3138120b9ad497f0bd146f9919", "shasum": "" }, "require": { @@ -2614,20 +2615,20 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-04-07T13:32:18+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": { @@ -2677,7 +2678,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", @@ -3286,16 +3287,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": { @@ -3352,7 +3353,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": [ { @@ -3360,7 +3361,7 @@ "type": "github" } ], - "time": "2025-12-10T09:58:31+00:00" + "time": "2026-04-11T18:38:28+00:00" }, { "name": "markbaker/complex", @@ -4304,16 +4305,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": { @@ -4407,9 +4408,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", @@ -6874,16 +6875,16 @@ }, { "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": { @@ -6933,7 +6934,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": [ { @@ -6953,20 +6954,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": { @@ -7015,7 +7016,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": [ { @@ -7035,11 +7036,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", @@ -7102,7 +7103,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": [ { @@ -7126,7 +7127,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", @@ -7187,7 +7188,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": [ { @@ -7211,16 +7212,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": { @@ -7272,7 +7273,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": [ { @@ -7292,20 +7293,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": { @@ -7356,7 +7357,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": [ { @@ -7376,20 +7377,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": { @@ -7436,7 +7437,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": [ { @@ -7456,20 +7457,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": { @@ -7516,7 +7517,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": [ { @@ -7536,20 +7537,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": { @@ -7596,7 +7597,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": [ { @@ -7616,20 +7617,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": { @@ -7679,7 +7680,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": [ { @@ -7699,7 +7700,7 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/process", @@ -9169,23 +9170,23 @@ }, { "name": "voku/portable-ascii", - "version": "2.0.3", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/voku/portable-ascii.git", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + "reference": "d870a33f0f79d2b4579740b0620200221ee44aeb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/d870a33f0f79d2b4579740b0620200221ee44aeb", + "reference": "d870a33f0f79d2b4579740b0620200221ee44aeb", "shasum": "" }, "require": { - "php": ">=7.0.0" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" }, "suggest": { "ext-intl": "Use Intl for transliterator_transliterate() support" @@ -9215,7 +9216,7 @@ ], "support": { "issues": "https://github.com/voku/portable-ascii/issues", - "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + "source": "https://github.com/voku/portable-ascii/tree/2.1.0" }, "funding": [ { @@ -9239,7 +9240,7 @@ "type": "tidelift" } ], - "time": "2024-11-21T01:49:47+00:00" + "time": "2026-04-16T23:10:39+00:00" }, { "name": "web-auth/cose-lib", @@ -9400,16 +9401,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": { @@ -9456,9 +9457,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", @@ -10801,16 +10802,16 @@ }, { "name": "driftingly/rector-laravel", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/driftingly/rector-laravel.git", - "reference": "807840ceb09de6764cbfcce0719108d044a459a9" + "reference": "3c1c13f335b3b4d1a1f944a8ea194020044871ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/driftingly/rector-laravel/zipball/807840ceb09de6764cbfcce0719108d044a459a9", - "reference": "807840ceb09de6764cbfcce0719108d044a459a9", + "url": "https://api.github.com/repos/driftingly/rector-laravel/zipball/3c1c13f335b3b4d1a1f944a8ea194020044871ed", + "reference": "3c1c13f335b3b4d1a1f944a8ea194020044871ed", "shasum": "" }, "require": { @@ -10831,9 +10832,9 @@ "description": "Rector upgrades rules for Laravel Framework", "support": { "issues": "https://github.com/driftingly/rector-laravel/issues", - "source": "https://github.com/driftingly/rector-laravel/tree/2.2.0" + "source": "https://github.com/driftingly/rector-laravel/tree/2.3.0" }, - "time": "2026-03-19T17:24:38+00:00" + "time": "2026-04-08T10:52:44+00:00" }, { "name": "fakerphp/faker", @@ -11035,12 +11036,12 @@ "version": "v7.0.5", "source": { "type": "git", - "url": "https://github.com/firebase/php-jwt.git", + "url": "https://github.com/googleapis/php-jwt.git", "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380", "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380", "shasum": "" }, @@ -11089,8 +11090,8 @@ "php" ], "support": { - "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v7.0.5" + "issues": "https://github.com/googleapis/php-jwt/issues", + "source": "https://github.com/googleapis/php-jwt/tree/v7.0.5" }, "time": "2026-04-01T20:38:03+00:00" }, @@ -11248,16 +11249,16 @@ }, { "name": "larastan/larastan", - "version": "v3.9.3", + "version": "v3.9.6", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65" + "reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65", - "reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65", + "url": "https://api.github.com/repos/larastan/larastan/zipball/9ad17e83e96b63536cb6ac39c3d40d29ff9cf636", + "reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636", "shasum": "" }, "require": { @@ -11271,7 +11272,7 @@ "illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13", "illuminate/support": "^11.44.2 || ^12.4.1 || ^13", "php": "^8.2", - "phpstan/phpstan": "^2.1.32" + "phpstan/phpstan": "^2.1.44" }, "require-dev": { "doctrine/coding-standard": "^13", @@ -11326,7 +11327,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.9.3" + "source": "https://github.com/larastan/larastan/tree/v3.9.6" }, "funding": [ { @@ -11334,7 +11335,7 @@ "type": "github" } ], - "time": "2026-02-20T12:07:12+00:00" + "time": "2026-04-16T10:02:43+00:00" }, { "name": "laravel/pail", @@ -11418,16 +11419,16 @@ }, { "name": "laravel/pint", - "version": "v1.29.0", + "version": "v1.29.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", - "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", + "url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80", "shasum": "" }, "require": { @@ -11438,14 +11439,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.94.2", - "illuminate/view": "^12.54.1", - "larastan/larastan": "^3.9.3", - "laravel-zero/framework": "^12.0.5", + "friendsofphp/php-cs-fixer": "^3.95.1", + "illuminate/view": "^12.56.0", + "larastan/larastan": "^3.9.6", + "laravel-zero/framework": "^12.1.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.4.0", "pestphp/pest": "^3.8.6", - "shipfastlabs/agent-detector": "^1.1.0" + "shipfastlabs/agent-detector": "^1.1.3" }, "bin": [ "builds/pint" @@ -11482,7 +11483,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-03-12T15:51:39+00:00" + "time": "2026-04-20T15:26:14+00:00" }, { "name": "laravel/socialite", @@ -11558,16 +11559,16 @@ }, { "name": "laravel/tinker", - "version": "v3.0.0", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "cc74081282ba2e3dae1f0068ccb330370d24634e" + "reference": "4faba77764bd33411735936acdf30446d058c78b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/cc74081282ba2e3dae1f0068ccb330370d24634e", - "reference": "cc74081282ba2e3dae1f0068ccb330370d24634e", + "url": "https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b", + "reference": "4faba77764bd33411735936acdf30446d058c78b", "shasum": "" }, "require": { @@ -11621,9 +11622,9 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v3.0.0" + "source": "https://github.com/laravel/tinker/tree/v3.0.2" }, - "time": "2026-03-17T14:53:17+00:00" + "time": "2026-03-17T14:54:13+00:00" }, { "name": "league/oauth1-client", @@ -11904,23 +11905,23 @@ }, { "name": "nunomaduro/collision", - "version": "v8.9.3", + "version": "v8.9.4", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "b0d8ab95b29c3189aeeb902d81215231df4c1b64" + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/b0d8ab95b29c3189aeeb902d81215231df4c1b64", - "reference": "b0d8ab95b29c3189aeeb902d81215231df4c1b64", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", "shasum": "" }, "require": { "filp/whoops": "^2.18.4", "nunomaduro/termwind": "^2.4.0", "php": "^8.2.0", - "symfony/console": "^7.4.8 || ^8.0.4" + "symfony/console": "^7.4.8 || ^8.0.8" }, "conflict": { "laravel/framework": "<11.48.0 || >=14.0.0", @@ -11928,12 +11929,12 @@ }, "require-dev": { "brianium/paratest": "^7.8.5", - "larastan/larastan": "^3.9.3", - "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.2.0", - "laravel/pint": "^1.29.0", - "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.0.0", + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", - "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.0.0" + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" }, "type": "library", "extra": { @@ -11996,7 +11997,7 @@ "type": "patreon" } ], - "time": "2026-04-06T19:25:53+00:00" + "time": "2026-04-21T14:04:20+00:00" }, { "name": "orchestra/canvas", @@ -12195,25 +12196,25 @@ }, { "name": "orchestra/testbench", - "version": "v11.0.0", + "version": "v11.1.0", "source": { "type": "git", "url": "https://github.com/orchestral/testbench.git", - "reference": "60efc9e8ec12137a2a7084e9d54c976f78c192f5" + "reference": "997f33e5200c7e8db4756b35a9deb3f5f3086759" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/testbench/zipball/60efc9e8ec12137a2a7084e9d54c976f78c192f5", - "reference": "60efc9e8ec12137a2a7084e9d54c976f78c192f5", + "url": "https://api.github.com/repos/orchestral/testbench/zipball/997f33e5200c7e8db4756b35a9deb3f5f3086759", + "reference": "997f33e5200c7e8db4756b35a9deb3f5f3086759", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "fakerphp/faker": "^1.23", - "laravel/framework": "^13.0.0", + "laravel/framework": "^13.1.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^11.0.0", - "orchestra/workbench": "^11.0.0", + "orchestra/testbench-core": "^11.2.0", + "orchestra/workbench": "^11.0.1", "php": "^8.3", "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", "symfony/process": "^7.4.5|^8.0.5", @@ -12244,22 +12245,22 @@ ], "support": { "issues": "https://github.com/orchestral/testbench/issues", - "source": "https://github.com/orchestral/testbench/tree/v11.0.0" + "source": "https://github.com/orchestral/testbench/tree/v11.1.0" }, - "time": "2026-03-16T15:02:09+00:00" + "time": "2026-04-09T05:11:06+00:00" }, { "name": "orchestra/testbench-core", - "version": "v11.2.0", + "version": "v11.3.0", "source": { "type": "git", "url": "https://github.com/orchestral/testbench-core.git", - "reference": "ca97baee646235b10e4fcf04c2076a9d075e9a34" + "reference": "4cd065eb0b03de8eb103b3b325481895984b8f3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/ca97baee646235b10e4fcf04c2076a9d075e9a34", - "reference": "ca97baee646235b10e4fcf04c2076a9d075e9a34", + "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/4cd065eb0b03de8eb103b3b325481895984b8f3f", + "reference": "4cd065eb0b03de8eb103b3b325481895984b8f3f", "shasum": "" }, "require": { @@ -12270,14 +12271,14 @@ }, "conflict": { "brianium/paratest": "<7.3.0|>=8.0.0", - "laravel/framework": "<13.1.1|>=14.0.0", + "laravel/framework": "<13.4.0|>=14.0.0", "laravel/serializable-closure": ">=2.0.0 <2.0.10|>=3.0.0", "nunomaduro/collision": "<8.9.0|>=9.0.0", "phpunit/phpunit": "<11.5.50|>=12.0.0 <12.5.8|>=13.2.0" }, "require-dev": { "fakerphp/faker": "^1.24", - "laravel/framework": "^13.1.1", + "laravel/framework": "^13.4.0", "laravel/pint": "^1.24", "laravel/serializable-closure": "^2.0.10", "mockery/mockery": "^1.6.10", @@ -12338,7 +12339,7 @@ "issues": "https://github.com/orchestral/testbench/issues", "source": "https://github.com/orchestral/testbench-core" }, - "time": "2026-04-07T03:00:08+00:00" + "time": "2026-04-23T11:01:49+00:00" }, { "name": "orchestra/workbench", @@ -12459,40 +12460,41 @@ }, { "name": "pestphp/pest", - "version": "v4.4.5", + "version": "v4.6.3", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "9797a71dbc776f46d6fcacb708b002755da6f37a" + "reference": "bff44562a99d30aa37573995566051b0344f9f8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/9797a71dbc776f46d6fcacb708b002755da6f37a", - "reference": "9797a71dbc776f46d6fcacb708b002755da6f37a", + "url": "https://api.github.com/repos/pestphp/pest/zipball/bff44562a99d30aa37573995566051b0344f9f8e", + "reference": "bff44562a99d30aa37573995566051b0344f9f8e", "shasum": "" }, "require": { "brianium/paratest": "^7.20.0", - "nunomaduro/collision": "^8.9.2", + "nunomaduro/collision": "^8.9.3", "nunomaduro/termwind": "^2.4.0", "pestphp/pest-plugin": "^4.0.0", - "pestphp/pest-plugin-arch": "^4.0.0", + "pestphp/pest-plugin-arch": "^4.0.2", "pestphp/pest-plugin-mutate": "^4.0.1", "pestphp/pest-plugin-profanity": "^4.2.1", "php": "^8.3.0", - "phpunit/phpunit": "^12.5.16", + "phpunit/phpunit": "^12.5.23", "symfony/process": "^7.4.8|^8.0.8" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.16", + "phpunit/phpunit": ">12.5.23", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { + "mrpunyapal/peststan": "^0.2.5", "pestphp/pest-dev-tools": "^4.1.0", - "pestphp/pest-plugin-browser": "^4.3.0", - "pestphp/pest-plugin-type-coverage": "^4.0.3", + "pestphp/pest-plugin-browser": "^4.3.1", + "pestphp/pest-plugin-type-coverage": "^4.0.4", "psy/psysh": "^0.12.22" }, "bin": [ @@ -12559,7 +12561,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.4.5" + "source": "https://github.com/pestphp/pest/tree/v4.6.3" }, "funding": [ { @@ -12571,7 +12573,7 @@ "type": "github" } ], - "time": "2026-04-03T13:43:28+00:00" + "time": "2026-04-18T13:51:25+00:00" }, { "name": "pestphp/pest-plugin", @@ -12645,26 +12647,26 @@ }, { "name": "pestphp/pest-plugin-arch", - "version": "v4.0.0", + "version": "v4.0.2", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-arch.git", - "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d" + "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/25bb17e37920ccc35cbbcda3b00d596aadf3e58d", - "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", + "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", "shasum": "" }, "require": { "pestphp/pest-plugin": "^4.0.0", "php": "^8.3", - "ta-tikoma/phpunit-architecture-test": "^0.8.5" + "ta-tikoma/phpunit-architecture-test": "^0.8.7" }, "require-dev": { - "pestphp/pest": "^4.0.0", - "pestphp/pest-dev-tools": "^4.0.0" + "pestphp/pest": "^4.4.6", + "pestphp/pest-dev-tools": "^4.1.0" }, "type": "library", "extra": { @@ -12699,7 +12701,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.0" + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.2" }, "funding": [ { @@ -12711,7 +12713,7 @@ "type": "github" } ], - "time": "2025-08-20T13:10:51+00:00" + "time": "2026-04-10T17:20:19+00:00" }, { "name": "pestphp/pest-plugin-laravel", @@ -13039,16 +13041,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.50", + "version": "3.0.51", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b" + "reference": "d59c94077f9c9915abb51ddb52ce85188ece1748" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b", - "reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748", + "reference": "d59c94077f9c9915abb51ddb52ce85188ece1748", "shasum": "" }, "require": { @@ -13129,7 +13131,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.50" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.51" }, "funding": [ { @@ -13145,15 +13147,15 @@ "type": "tidelift" } ], - "time": "2026-03-19T02:57:58+00:00" + "time": "2026-04-10T01:33:53+00:00" }, { "name": "phpstan/phpstan", - "version": "2.1.46", + "version": "2.1.51", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", - "reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59", + "reference": "dc3b523c45e714c70de2ac5113b958223b55dc59", "shasum": "" }, "require": { @@ -13198,20 +13200,20 @@ "type": "github" } ], - "time": "2026-04-01T09:25:14+00:00" + "time": "2026-04-21T18:22:01+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "12.5.3", + "version": "12.5.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" + "reference": "876099a072646c7745f673d7aeab5382c4439691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", - "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691", + "reference": "876099a072646c7745f673d7aeab5382c4439691", "shasum": "" }, "require": { @@ -13220,7 +13222,6 @@ "ext-xmlwriter": "*", "nikic/php-parser": "^5.7.0", "php": ">=8.3", - "phpunit/php-file-iterator": "^6.0", "phpunit/php-text-template": "^5.0", "sebastian/complexity": "^5.0", "sebastian/environment": "^8.0.3", @@ -13267,7 +13268,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6" }, "funding": [ { @@ -13287,7 +13288,7 @@ "type": "tidelift" } ], - "time": "2026-02-06T06:01:44+00:00" + "time": "2026-04-15T08:23:17+00:00" }, { "name": "phpunit/php-file-iterator", @@ -13548,16 +13549,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.16", + "version": "12.5.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b2429f58ae75cae980b5bb9873abe4de6aac8b58" + "reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b2429f58ae75cae980b5bb9873abe4de6aac8b58", - "reference": "b2429f58ae75cae980b5bb9873abe4de6aac8b58", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969", + "reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969", "shasum": "" }, "require": { @@ -13571,15 +13572,15 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.5.3", + "phpunit/php-code-coverage": "^12.5.6", "phpunit/php-file-iterator": "^6.0.1", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", "sebastian/cli-parser": "^4.2.0", - "sebastian/comparator": "^7.1.4", + "sebastian/comparator": "^7.1.6", "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.0.4", + "sebastian/environment": "^8.1.0", "sebastian/exporter": "^7.0.2", "sebastian/global-state": "^8.0.2", "sebastian/object-enumerator": "^7.0.0", @@ -13626,7 +13627,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.16" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.23" }, "funding": [ { @@ -13634,7 +13635,7 @@ "type": "other" } ], - "time": "2026-04-03T05:26:42+00:00" + "time": "2026-04-18T06:12:49+00:00" }, { "name": "psy/psysh", @@ -13717,21 +13718,21 @@ }, { "name": "rector/rector", - "version": "2.4.0", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "a51dfddbf6a29ed9fbf6e8410fc90c1608df1b5d" + "reference": "e645b6463c6a88ea5b44b17d3387d35a912c7946" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/a51dfddbf6a29ed9fbf6e8410fc90c1608df1b5d", - "reference": "a51dfddbf6a29ed9fbf6e8410fc90c1608df1b5d", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/e645b6463c6a88ea5b44b17d3387d35a912c7946", + "reference": "e645b6463c6a88ea5b44b17d3387d35a912c7946", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.41" + "phpstan/phpstan": "^2.1.48" }, "conflict": { "rector/rector-doctrine": "*", @@ -13765,7 +13766,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.4.0" + "source": "https://github.com/rectorphp/rector/tree/2.4.2" }, "funding": [ { @@ -13773,7 +13774,7 @@ "type": "github" } ], - "time": "2026-04-04T07:37:45+00:00" + "time": "2026-04-16T13:07:34+00:00" }, { "name": "sebastian/cli-parser", @@ -13846,16 +13847,16 @@ }, { "name": "sebastian/comparator", - "version": "7.1.4", + "version": "7.1.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" + "reference": "c769009dee98f494e0edc3fd4f4087501688f11e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", - "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c769009dee98f494e0edc3fd4f4087501688f11e", + "reference": "c769009dee98f494e0edc3fd4f4087501688f11e", "shasum": "" }, "require": { @@ -13914,7 +13915,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.6" }, "funding": [ { @@ -13934,7 +13935,7 @@ "type": "tidelift" } ], - "time": "2026-01-24T09:28:48+00:00" + "time": "2026-04-14T08:23:15+00:00" }, { "name": "sebastian/complexity", @@ -14063,16 +14064,16 @@ }, { "name": "sebastian/environment", - "version": "8.0.4", + "version": "8.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11" + "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", - "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b121608b28a13f721e76ffbbd386d08eff58f3f6", + "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6", "shasum": "" }, "require": { @@ -14087,7 +14088,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-main": "8.1-dev" } }, "autoload": { @@ -14115,7 +14116,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4" + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.0" }, "funding": [ { @@ -14135,7 +14136,7 @@ "type": "tidelift" } ], - "time": "2026-03-15T07:05:40+00:00" + "time": "2026-04-15T12:13:01+00:00" }, { "name": "sebastian/exporter", diff --git a/database/Factories/AddressFactory.php b/database/Factories/AddressFactory.php index 015c73b734f..a2e4037bf7c 100644 --- a/database/Factories/AddressFactory.php +++ b/database/Factories/AddressFactory.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\Database\Factories; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address as AddressElement; use CraftCms\Cms\Address\Models\Address; use CraftCms\Cms\Database\Factories\Concerns\CreatesElement; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Models\Element; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\DB; diff --git a/database/Factories/EntryFactory.php b/database/Factories/EntryFactory.php index f9a9b7df89d..d01c0a57505 100644 --- a/database/Factories/EntryFactory.php +++ b/database/Factories/EntryFactory.php @@ -13,9 +13,12 @@ use CraftCms\Cms\Entry\Models\EntryType; use CraftCms\Cms\FieldLayout\Models\FieldLayout; use CraftCms\Cms\Section\Models\Section; +use CraftCms\Cms\Site\Models\Site; use CraftCms\Cms\Support\Arr; +use CraftCms\Cms\Support\Facades\Elements; use CraftCms\Cms\Support\Facades\EntryTypes; use CraftCms\Cms\Support\Facades\Fields; +use CraftCms\Cms\Support\Facades\Search; use Illuminate\Database\Eloquent\Factories\Factory; use Override; @@ -84,6 +87,21 @@ public function forEntryType(EntryType $type): static return $this->state(fn () => ['typeId' => $type->id]); } + public function enabledForSites(int|Site ...$sites): static + { + return $this->afterCreating(function (Entry $entry) use ($sites) { + $element = EntryElement::find()->id($entry->id)->one(); + $enabledForSite = [$element->siteId => true]; + + foreach ($sites as $site) { + $enabledForSite[$site instanceof Site ? $site->id : $site] = true; + } + + $element->setEnabledForSite($enabledForSite); + Elements::saveElement($element); + }); + } + public function withFieldLayout(FieldLayout|FieldLayoutFactory $fieldLayout): static { if ($fieldLayout instanceof FieldLayoutFactory) { @@ -97,6 +115,13 @@ public function withFieldLayout(FieldLayout|FieldLayoutFactory $fieldLayout): st }); } + public function indexed(): static + { + return $this->afterCreating(function (Entry $entry) { + Search::indexElementAttributes($this->queryElement($entry->id)); + }); + } + public function createElement(array $attributes = []): EntryElement { $factory = $this; diff --git a/database/Factories/SectionFactory.php b/database/Factories/SectionFactory.php index 79df8e55e1d..0ee1c038fdb 100644 --- a/database/Factories/SectionFactory.php +++ b/database/Factories/SectionFactory.php @@ -5,6 +5,7 @@ namespace CraftCms\Cms\Database\Factories; use CraftCms\Cms\Entry\Models\EntryType; +use CraftCms\Cms\Field\Fields; use CraftCms\Cms\Section\Enums\SectionType; use CraftCms\Cms\Section\Models\Section; use CraftCms\Cms\Section\Models\SectionSiteSettings; @@ -52,4 +53,24 @@ public function withEntryTypes(EntryType ...$types): static } }); } + + public function withSites(Site ...$sites): static + { + return $this->afterCreating(function (Section $section) use ($sites) { + foreach ($sites as $site) { + SectionSiteSettings::query()->firstOrCreate([ + 'sectionId' => $section->id, + 'siteId' => $site->id, + ], [ + 'uid' => (string) str()->uuid(), + 'dateCreated' => $section->dateCreated, + 'dateUpdated' => $section->dateUpdated, + 'hasUrls' => true, + ]); + } + + app(Fields::class)->invalidateCaches(); + app(Fields::class)->refreshFields(); + }); + } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 92dda397c92..0354e8b1b61 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,7 +15,7 @@ ./tests/Unit - ./tests/ArchTest.php + ./tests/Arch diff --git a/resources/templates/_components/fieldtypes/Assets/input.twig b/resources/templates/_components/fieldtypes/Assets/input.twig index af498775cd5..309a6168589 100644 --- a/resources/templates/_components/fieldtypes/Assets/input.twig +++ b/resources/templates/_components/fieldtypes/Assets/input.twig @@ -11,7 +11,7 @@ sources: sources, condition: condition ? condition.getConfig() : null, referenceElementId: referenceElement.id ?? null, - referenceElementOwnerId: (referenceElement ?? null) is instance of('craft\\base\\NestedElementInterface') + referenceElementOwnerId: (referenceElement ?? null) is instance of('CraftCms\\Cms\\Element\\Contracts\\NestedElementInterface') ? referenceElement.getOwnerId() : null, referenceElementSiteId: referenceElement.siteId ?? null, diff --git a/resources/templates/_includes/forms/elementSelect.twig b/resources/templates/_includes/forms/elementSelect.twig index 81916e61444..8e5246f3f4b 100644 --- a/resources/templates/_includes/forms/elementSelect.twig +++ b/resources/templates/_includes/forms/elementSelect.twig @@ -114,7 +114,7 @@ sources: sources, condition: condition ? condition.getConfig() : null, referenceElementId: referenceElement.id ?? null, - referenceElementOwnerId: (referenceElement ?? null) is instance of('craft\\base\\NestedElementInterface') + referenceElementOwnerId: (referenceElement ?? null) is instance of('CraftCms\\Cms\\Element\\Contracts\\NestedElementInterface') ? referenceElement.getOwnerId() : null, referenceElementSiteId: referenceElement.siteId ?? null, diff --git a/resources/templates/_layouts/components/notifications.twig b/resources/templates/_layouts/components/notifications.twig index 381b1de5a29..2978851cd07 100644 --- a/resources/templates/_layouts/components/notifications.twig +++ b/resources/templates/_layouts/components/notifications.twig @@ -3,7 +3,7 @@ {% for type in ['notice', 'success', 'error'] %} - {% set notification = craft.app.session.getFlash("cp-notification-#{type}") %} + {% set notification = session.get("cp-notification-#{type}") %} {% if notification %} {% js %} Craft.cp.displayNotification({{ type|json_encode|raw }}, {{ notification[0]|json_encode|raw }}, {{ notification[1]|json_encode|raw }}); diff --git a/routes/actions.php b/routes/actions.php index 554fa61f560..09bbfac9b40 100644 --- a/routes/actions.php +++ b/routes/actions.php @@ -30,8 +30,25 @@ use CraftCms\Cms\Http\Controllers\Dashboard\Widgets\NewUsersController; use CraftCms\Cms\Http\Controllers\Dashboard\WidgetsController; use CraftCms\Cms\Http\Controllers\EditionController; -use CraftCms\Cms\Http\Controllers\Elements\ExportElementIndexController; +use CraftCms\Cms\Http\Controllers\Elements\CopyElementValuesController; +use CraftCms\Cms\Http\Controllers\Elements\CreateElementController; +use CraftCms\Cms\Http\Controllers\Elements\DeleteElementController; +use CraftCms\Cms\Http\Controllers\Elements\DuplicateElementController; +use CraftCms\Cms\Http\Controllers\Elements\EditElementController; +use CraftCms\Cms\Http\Controllers\Elements\ElementActivityController; +use CraftCms\Cms\Http\Controllers\Elements\ElementDraftsController; +use CraftCms\Cms\Http\Controllers\Elements\ElementIndex\ElementIndexController; +use CraftCms\Cms\Http\Controllers\Elements\ElementIndex\ElementIndexSourcesController; +use CraftCms\Cms\Http\Controllers\Elements\ElementIndex\ExportElementIndexController; +use CraftCms\Cms\Http\Controllers\Elements\ElementIndex\SaveElementIndexElementsController; +use CraftCms\Cms\Http\Controllers\Elements\ElementRevisionsController; +use CraftCms\Cms\Http\Controllers\Elements\ElementSelectorModalController; +use CraftCms\Cms\Http\Controllers\Elements\ElementSourcesController; use CraftCms\Cms\Http\Controllers\Elements\PerformElementActionController; +use CraftCms\Cms\Http\Controllers\Elements\SaveElementController; +use CraftCms\Cms\Http\Controllers\Elements\SearchController as ElementSearchController; +use CraftCms\Cms\Http\Controllers\Elements\UpdateFieldLayoutController; +use CraftCms\Cms\Http\Controllers\Elements\ValidateElementController; use CraftCms\Cms\Http\Controllers\Entries\CreateEntryController; use CraftCms\Cms\Http\Controllers\Entries\MoveEntryToSectionController; use CraftCms\Cms\Http\Controllers\Entries\StoreEntryController; @@ -237,8 +254,42 @@ }); // Elements + Route::post('elements/create', CreateElementController::class); + Route::any('elements/edit', EditElementController::class); + Route::post('elements/save', [SaveElementController::class, 'store']); + Route::post('elements/save-nested-element-for-derivative', [SaveElementController::class, 'storeForDerivative']); + Route::post('elements/delete', [DeleteElementController::class, 'destroy']); + Route::post('elements/delete-for-site', [DeleteElementController::class, 'destroyForSite']); + Route::post('elements/save-draft', [ElementDraftsController::class, 'store']); + Route::post('elements/ensure-draft', [ElementDraftsController::class, 'ensure']); + Route::post('elements/apply-draft', [ElementDraftsController::class, 'apply']); + Route::post('elements/delete-draft', [ElementDraftsController::class, 'destroy']); + Route::post('elements/revert', [ElementRevisionsController::class, 'revert']); + Route::post('elements/validate', ValidateElementController::class); + Route::post('elements/recent-activity', ElementActivityController::class); + Route::post('elements/update-field-layout', UpdateFieldLayoutController::class); + Route::post('elements/duplicate', [DuplicateElementController::class, 'duplicate']); + Route::post('elements/bulk-duplicate', [DuplicateElementController::class, 'bulkDuplicate']); + Route::post('elements/copy-values-from-site', CopyElementValuesController::class); + + // Element Indexes + Route::post('element-indexes/source-path', [ElementIndexSourcesController::class, 'sourcePath']); + Route::post('element-indexes/source-attribute-info', [ElementIndexSourcesController::class, 'sourceAttributeInfo']); + Route::post('element-indexes/get-elements', [ElementIndexController::class, 'getElements']); + Route::post('element-indexes/get-more-elements', [ElementIndexController::class, 'getMoreElements']); + Route::post('element-indexes/count-elements', [ElementIndexController::class, 'countElements']); + Route::post('element-indexes/get-source-tree-html', [ElementIndexSourcesController::class, 'getSourceTreeHtml']); + Route::post('element-indexes/filter-hud', [ElementIndexController::class, 'filterHud']); + Route::post('element-indexes/element-table-html', [ElementIndexController::class, 'elementTableHtml']); + Route::post('element-indexes/save-elements', SaveElementIndexElementsController::class); Route::post('element-indexes/export', ExportElementIndexController::class); Route::post('element-indexes/perform-action', PerformElementActionController::class); + Route::post('element-search/search', ElementSearchController::class); + Route::post('element-selector-modals/body', ElementSelectorModalController::class); + Route::middleware([RequireAdminChanges::class])->group(function () { + Route::post('element-index-settings/get-customize-sources-modal-data', [ElementSourcesController::class, 'show']); + Route::post('element-index-settings/save-customize-sources-modal-settings', [ElementSourcesController::class, 'store']); + }); // Entries Route::post('entries/create', CreateEntryController::class); diff --git a/routes/cp.php b/routes/cp.php index 00ffca27dd6..06b3abfc2ad 100644 --- a/routes/cp.php +++ b/routes/cp.php @@ -10,6 +10,10 @@ use CraftCms\Cms\Http\Controllers\Auth\TwoFactorAuthenticationController; use CraftCms\Cms\Http\Controllers\Auth\VerifyEmailController; use CraftCms\Cms\Http\Controllers\Dashboard\DashboardController; +use CraftCms\Cms\Http\Controllers\Elements\EditElementController; +use CraftCms\Cms\Http\Controllers\Elements\ElementRedirectController; +use CraftCms\Cms\Http\Controllers\Elements\ElementRevisionsController; +use CraftCms\Cms\Http\Controllers\Elements\PreviewElementController; use CraftCms\Cms\Http\Controllers\Entries\CreateEntryController; use CraftCms\Cms\Http\Controllers\Entries\EntriesIndexController; use CraftCms\Cms\Http\Controllers\FieldsController; @@ -79,6 +83,30 @@ Route::view('settings/addresses', 'settings/addresses/_fields'); }); + /** + * Elements + */ + $idSlugParams = [ + 'id' => '\d+', + 'slug' => '(?:-[^\/]*)', + ]; + + Route::get('preview/{id}{slug}', PreviewElementController::class)->where($idSlugParams); + Route::get('edit/{id}{slug}', ElementRedirectController::class)->where($idSlugParams); + Route::get('edit/{uid}', ElementRedirectController::class); + Route::get('revisions/{id}{slug}', [ElementRevisionsController::class, 'index'])->where($idSlugParams); + Route::get('entries/{section}/{id}{slug}/revisions', [ElementRevisionsController::class, 'index'])->where($idSlugParams); + Route::get('content/{page}/{section}/{id}{slug}/revisions', [ElementRevisionsController::class, 'index'])->where([ + ...$idSlugParams, + 'page' => '[^\/]+', + ]); + Route::get('assets/edit/{id}{slug}', EditElementController::class)->where($idSlugParams); + Route::get('entries/{section}/{id}{slug}', EditElementController::class)->where($idSlugParams); + Route::get('content/{page}/{section}/{id}{slug}', EditElementController::class)->where([ + ...$idSlugParams, + 'page' => '[^\/]+', + ]); + /** * Entries & Content */ diff --git a/src/Address/Conditions/AddressLine1ConditionRule.php b/src/Address/Conditions/AddressLine1ConditionRule.php index b3d9883b85f..ab499635e1a 100644 --- a/src/Address/Conditions/AddressLine1ConditionRule.php +++ b/src/Address/Conditions/AddressLine1ConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Address/Conditions/AddressLine2ConditionRule.php b/src/Address/Conditions/AddressLine2ConditionRule.php index 6a030a0018a..8444670686f 100644 --- a/src/Address/Conditions/AddressLine2ConditionRule.php +++ b/src/Address/Conditions/AddressLine2ConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Address/Conditions/AddressLine3ConditionRule.php b/src/Address/Conditions/AddressLine3ConditionRule.php index be1803e8319..5f5bc798dc5 100644 --- a/src/Address/Conditions/AddressLine3ConditionRule.php +++ b/src/Address/Conditions/AddressLine3ConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Address/Conditions/AdministrativeAreaConditionRule.php b/src/Address/Conditions/AdministrativeAreaConditionRule.php index 41ab25c3d85..17a05d4da70 100644 --- a/src/Address/Conditions/AdministrativeAreaConditionRule.php +++ b/src/Address/Conditions/AdministrativeAreaConditionRule.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Support\Facades\Addresses; diff --git a/src/Address/Conditions/CountryConditionRule.php b/src/Address/Conditions/CountryConditionRule.php index e7d6f0cae71..5c13bb4afcb 100644 --- a/src/Address/Conditions/CountryConditionRule.php +++ b/src/Address/Conditions/CountryConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Support\Facades\Addresses; diff --git a/src/Address/Conditions/DependentLocalityConditionRule.php b/src/Address/Conditions/DependentLocalityConditionRule.php index f7eb9ed9536..7432730f333 100644 --- a/src/Address/Conditions/DependentLocalityConditionRule.php +++ b/src/Address/Conditions/DependentLocalityConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Address/Conditions/FieldConditionRule.php b/src/Address/Conditions/FieldConditionRule.php index 2af4b51e36d..908c2ef864b 100644 --- a/src/Address/Conditions/FieldConditionRule.php +++ b/src/Address/Conditions/FieldConditionRule.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; use CraftCms\Cms\Element\Conditions\HintableConditionRuleTrait; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Field\Addresses; diff --git a/src/Address/Conditions/FullNameConditionRule.php b/src/Address/Conditions/FullNameConditionRule.php index b1cb7a964e1..09dbddc07e7 100644 --- a/src/Address/Conditions/FullNameConditionRule.php +++ b/src/Address/Conditions/FullNameConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Address/Conditions/LocalityConditionRule.php b/src/Address/Conditions/LocalityConditionRule.php index 5a407989b48..a276aaae1a7 100644 --- a/src/Address/Conditions/LocalityConditionRule.php +++ b/src/Address/Conditions/LocalityConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Address/Conditions/OrganizationConditionRule.php b/src/Address/Conditions/OrganizationConditionRule.php index 9861f638a8a..106fe7e7c92 100644 --- a/src/Address/Conditions/OrganizationConditionRule.php +++ b/src/Address/Conditions/OrganizationConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Address/Conditions/OrganizationTaxIdConditionRule.php b/src/Address/Conditions/OrganizationTaxIdConditionRule.php index 726ad768878..552c98c83d1 100644 --- a/src/Address/Conditions/OrganizationTaxIdConditionRule.php +++ b/src/Address/Conditions/OrganizationTaxIdConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Address/Conditions/PostalCodeConditionRule.php b/src/Address/Conditions/PostalCodeConditionRule.php index 05c1a448b0b..01fc8b8c4e3 100644 --- a/src/Address/Conditions/PostalCodeConditionRule.php +++ b/src/Address/Conditions/PostalCodeConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Address/Conditions/SortingCodeConditionRule.php b/src/Address/Conditions/SortingCodeConditionRule.php index 998403c1f0b..ffb79300e3c 100644 --- a/src/Address/Conditions/SortingCodeConditionRule.php +++ b/src/Address/Conditions/SortingCodeConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Address\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Address/Elements/Address.php b/src/Address/Elements/Address.php index 85d7a803f50..88d116bc33e 100644 --- a/src/Address/Elements/Address.php +++ b/src/Address/Elements/Address.php @@ -8,7 +8,6 @@ use CommerceGuys\Addressing\AddressInterface; use CommerceGuys\Addressing\Country\Country; use CommerceGuys\Addressing\Subdivision\SubdivisionUpdater; -use craft\base\NestedElementInterface; use craft\base\NestedElementTrait; use CraftCms\Cms\Address\Addresses; use CraftCms\Cms\Address\Conditions\AddressCondition; @@ -18,6 +17,7 @@ use CraftCms\Cms\Database\Table; use CraftCms\Cms\Element\Actions\Copy; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\FieldLayout\FieldLayout; @@ -25,9 +25,8 @@ use CraftCms\Cms\Twig\Attributes\AllowedInSandbox; use CraftCms\Cms\User\Elements\User; use CraftCms\RulesetValidation\Attributes\Ruleset; -use Deprecated; use Override; -use yii\base\InvalidConfigException; +use RuntimeException; use yii\helpers\Html; use function CraftCms\Cms\t; @@ -41,10 +40,98 @@ class Address extends Element implements AddressInterface, NestedElementInterfac use HasNames; use NestedElementTrait; + public const string GQL_TYPE_NAME = 'Address'; + /** - * @since 5.0.0 + * @var string Two-letter country code + * + * @see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 */ - public const string GQL_TYPE_NAME = 'Address'; + #[AllowedInSandbox] + public string $countryCode; + + /** + * @var string|null Administrative area + */ + #[AllowedInSandbox] + public ?string $administrativeArea = null; + + /** + * @var string|null Locality + */ + #[AllowedInSandbox] + public ?string $locality = null; + + /** + * @var string|null Dependent locality + */ + #[AllowedInSandbox] + public ?string $dependentLocality = null; + + /** + * @var string|null Postal code + */ + #[AllowedInSandbox] + public ?string $postalCode = null; + + /** + * @var string|null Sorting code + */ + #[AllowedInSandbox] + public ?string $sortingCode = null; + + /** + * @var string|null First line of the address + */ + #[AllowedInSandbox] + public ?string $addressLine1 = null; + + /** + * @var string|null Second line of the address + */ + #[AllowedInSandbox] + public ?string $addressLine2 = null; + + /** + * @var string|null Third line of the address + */ + #[AllowedInSandbox] + public ?string $addressLine3 = null; + + /** + * @var string|null Organization name + */ + #[AllowedInSandbox] + public ?string $organization = null; + + /** + * @var string|null Organization tax ID + */ + #[AllowedInSandbox] + public ?string $organizationTaxId = null; + + /** + * @var string|null Latitude + */ + #[AllowedInSandbox] + public ?string $latitude = null; + + /** + * @var string|null Longitude + */ + #[AllowedInSandbox] + public ?string $longitude = null; + + public function __construct($config = []) + { + parent::__construct($config); + + if (! isset($this->countryCode)) { + $this->countryCode = Cms::config()->defaultCountryCode; + } + + $this->normalizeNames(); + } #[Override] public static function displayName(): string @@ -199,116 +286,8 @@ public static function addressAttributes(): array ]; } - /** - * Returns an address attribute label. - */ - #[Deprecated(message: 'in 4.3.0. [[\craft\services\Addresses::getFieldLabel()]] should be used instead.')] - public static function addressAttributeLabel(string $attribute, string $countryCode): ?string - { - if (! AddressField::exists($attribute)) { - return null; - } - - /** @phpstan-var AddressField::* $attribute */ - return app(Addresses::class)->getFieldLabel($attribute, $countryCode); - } - - /** - * @var string Two-letter country code - * - * @see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 - */ - #[AllowedInSandbox] - public string $countryCode; - - /** - * @var string|null Administrative area - */ - #[AllowedInSandbox] - public ?string $administrativeArea = null; - - /** - * @var string|null Locality - */ - #[AllowedInSandbox] - public ?string $locality = null; - - /** - * @var string|null Dependent locality - */ - #[AllowedInSandbox] - public ?string $dependentLocality = null; - - /** - * @var string|null Postal code - */ - #[AllowedInSandbox] - public ?string $postalCode = null; - - /** - * @var string|null Sorting code - */ - #[AllowedInSandbox] - public ?string $sortingCode = null; - - /** - * @var string|null First line of the address - */ - #[AllowedInSandbox] - public ?string $addressLine1 = null; - - /** - * @var string|null Second line of the address - */ - #[AllowedInSandbox] - public ?string $addressLine2 = null; - - /** - * @var string|null Third line of the address - * - * @since 5.0.0 - */ - #[AllowedInSandbox] - public ?string $addressLine3 = null; - - /** - * @var string|null Organization name - */ - #[AllowedInSandbox] - public ?string $organization = null; - - /** - * @var string|null Organization tax ID - */ - #[AllowedInSandbox] - public ?string $organizationTaxId = null; - - /** - * @var string|null Latitude - */ - #[AllowedInSandbox] - public ?string $latitude = null; - - /** - * @var string|null Longitude - */ - #[AllowedInSandbox] - public ?string $longitude = null; - #[Override] - public function init(): void - { - parent::init(); - - if (! isset($this->countryCode)) { - $this->countryCode = Cms::config()->defaultCountryCode; - } - - $this->normalizeNames(); - } - - #[Override] - public function setAttributes($values, $safeOnly = true): void + public function setAttributes($values): void { // Don't even allow setting a blank country code if (array_key_exists('countryCode', $values) && empty($values['countryCode'])) { @@ -323,7 +302,7 @@ public function setAttributes($values, $safeOnly = true): void $this->firstName = $this->lastName = null; } - parent::setAttributes($values, $safeOnly); + parent::setAttributes($values); } #[Override] @@ -348,8 +327,6 @@ public function getAttributeLabel($attribute): string /** * Returns whether the address belongs to the currently logged-in user. - * - * @since 4.5.13 */ public function getBelongsToCurrentUser(): bool { @@ -366,8 +343,6 @@ public function getCountryCode(): string /** * Returns a [[Country]] object representing the address’ country. - * - * @since 5.3.0 */ #[AllowedInSandbox] public function getCountry(): Country @@ -453,9 +428,6 @@ public function getLocale(): string return app()->getLocale(); } - /** - * @since 3.3.0 - */ #[Override] public function getGqlTypeName(): string { @@ -541,7 +513,7 @@ public function beforeSave(bool $isNew): bool ); } // Andorra is the only country with remapped localities. - if ($this->countryCode == 'AD' && isset($this->locality)) { + if ($this->countryCode === 'AD' && isset($this->locality)) { $this->locality = SubdivisionUpdater::updateValue( $this->countryCode, $this->locality, @@ -553,7 +525,7 @@ public function beforeSave(bool $isNew): bool } /** - * @throws InvalidConfigException + * @throws RuntimeException */ #[Override] public function afterSave(bool $isNew): void @@ -562,7 +534,7 @@ public function afterSave(bool $isNew): void $model = AddressModel::find($this->id); if (! $model) { - throw new InvalidConfigException("Invalid address ID: $this->id"); + throw new RuntimeException("Invalid address ID: $this->id"); } } else { $model = new AddressModel; diff --git a/src/Address/Policies/AddressPolicy.php b/src/Address/Policies/AddressPolicy.php index d6fa23ee940..027b4368bf1 100644 --- a/src/Address/Policies/AddressPolicy.php +++ b/src/Address/Policies/AddressPolicy.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Address\Policies; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Policies\ElementPolicy; use CraftCms\Cms\User\Elements\User; diff --git a/src/Address/Validation/AddressRules.php b/src/Address/Validation/AddressRules.php index 7d16d0753b9..5e6c090ded3 100644 --- a/src/Address/Validation/AddressRules.php +++ b/src/Address/Validation/AddressRules.php @@ -8,9 +8,9 @@ use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Cms; use CraftCms\Cms\Element\Validation\ElementRules; -use CraftCms\Cms\FieldLayout\LayoutElements\addresses\LatLongField; -use CraftCms\Cms\FieldLayout\LayoutElements\addresses\OrganizationField; -use CraftCms\Cms\FieldLayout\LayoutElements\addresses\OrganizationTaxIdField; +use CraftCms\Cms\FieldLayout\LayoutElements\Addresses\LatLongField; +use CraftCms\Cms\FieldLayout\LayoutElements\Addresses\OrganizationField; +use CraftCms\Cms\FieldLayout\LayoutElements\Addresses\OrganizationTaxIdField; use CraftCms\Cms\FieldLayout\LayoutElements\BaseNativeField; use CraftCms\Cms\FieldLayout\LayoutElements\FullNameField; use CraftCms\Cms\Support\Arr; diff --git a/src/Asset/AssetIndexer.php b/src/Asset/AssetIndexer.php index 16039536304..4344feab9fa 100644 --- a/src/Asset/AssetIndexer.php +++ b/src/Asset/AssetIndexer.php @@ -9,6 +9,7 @@ use CraftCms\Cms\Asset\Data\Volume; use CraftCms\Cms\Asset\Data\VolumeFolder; use CraftCms\Cms\Asset\Elements\Asset; +use CraftCms\Cms\Asset\Enums\FileKind; use CraftCms\Cms\Asset\Exceptions\AssetDisallowedExtensionException; use CraftCms\Cms\Asset\Exceptions\AssetException; use CraftCms\Cms\Asset\Exceptions\AssetNotIndexableException; @@ -638,7 +639,7 @@ public function indexFileByEntry( $asset->setMimeType($asset->getMimeType()); } - if ($asset->kind === Asset::KIND_IMAGE) { + if ($asset->kind === FileKind::Image->value) { $dimensions = null; $tempPath = null; diff --git a/src/Asset/Assets.php b/src/Asset/Assets.php index 1a09af591aa..23cbc03a011 100644 --- a/src/Asset/Assets.php +++ b/src/Asset/Assets.php @@ -7,6 +7,7 @@ use CraftCms\Cms\Asset\Contracts\AssetPreviewHandlerInterface; use CraftCms\Cms\Asset\Data\VolumeFolder; use CraftCms\Cms\Asset\Elements\Asset; +use CraftCms\Cms\Asset\Enums\FileKind; use CraftCms\Cms\Asset\Events\AfterReplaceAsset; use CraftCms\Cms\Asset\Events\BeforeReplaceAsset; use CraftCms\Cms\Asset\Events\DefineThumbUrl; @@ -43,9 +44,9 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; use InvalidArgumentException; +use RuntimeException; use Throwable; use Tpetry\QueryExpressions\Language\Alias; -use yii\base\InvalidConfigException; use function CraftCms\Cms\t; @@ -205,7 +206,7 @@ public function getImagePreviewUrl(Asset $asset, int $maxWidth, int $maxHeight): /** * @throws AssetOperationException - * @throws InvalidConfigException + * @throws RuntimeException */ public function getNameReplacementInFolder(string $originalFilename, int $folderId): string { @@ -292,16 +293,16 @@ public function getAssetPreviewHandler(Asset $asset): ?AssetPreviewHandlerInterf } return match ($asset->kind) { - Asset::KIND_IMAGE => new ImagePreview($asset), - Asset::KIND_PDF => new Pdf($asset), - Asset::KIND_VIDEO => new Video($asset), - Asset::KIND_HTML, Asset::KIND_JAVASCRIPT, Asset::KIND_JSON, Asset::KIND_PHP, Asset::KIND_TEXT, Asset::KIND_XML => new Text($asset), + FileKind::Image->value => new ImagePreview($asset), + FileKind::Pdf->value => new Pdf($asset), + FileKind::Video->value => new Video($asset), + FileKind::Html->value, FileKind::Javascript->value, FileKind::Json->value, FileKind::Php->value, FileKind::Text->value, FileKind::Xml->value => new Text($asset), default => null, }; } /** - * @throws InvalidConfigException + * @throws RuntimeException */ public function getTempAssetUploadFs(): FsInterface { @@ -312,11 +313,11 @@ public function getTempAssetUploadFs(): FsInterface } return Filesystems::resolve($handle) - ?? throw new InvalidConfigException("The tempAssetUploadFs config setting is set to an invalid filesystem value: $handle"); + ?? throw new RuntimeException("The tempAssetUploadFs config setting is set to an invalid filesystem value: $handle"); } /** - * @throws InvalidConfigException + * @throws RuntimeException */ public function getTempAssetUploadDisk(): FilesystemAdapter { @@ -331,7 +332,7 @@ public function getTempAssetUploadDisk(): FilesystemAdapter return Storage::disk( Filesystems::resolveDiskName($handle) - ?? throw new InvalidConfigException("The tempAssetUploadFs config setting is set to an invalid filesystem value: $handle") + ?? throw new RuntimeException("The tempAssetUploadFs config setting is set to an invalid filesystem value: $handle") ); } diff --git a/src/Asset/AssetsHelper.php b/src/Asset/AssetsHelper.php index c484617c330..a245d398283 100644 --- a/src/Asset/AssetsHelper.php +++ b/src/Asset/AssetsHelper.php @@ -4,14 +4,15 @@ namespace CraftCms\Cms\Asset; -use craft\base\ElementInterface; use CraftCms\Aliases\Aliases; use CraftCms\Cms\Asset\Data\Volume; use CraftCms\Cms\Asset\Data\VolumeFolder; use CraftCms\Cms\Asset\Elements\Asset; +use CraftCms\Cms\Asset\Enums\FileKind; use CraftCms\Cms\Asset\Events\RegisterFileKinds; use CraftCms\Cms\Asset\Events\SetAssetFilename; use CraftCms\Cms\Cms; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Filesystem\Contracts\FsInterface; use CraftCms\Cms\Filesystem\Exceptions\FilesystemException; @@ -44,18 +45,18 @@ class AssetsHelper public const string INDEX_SKIP_ITEMS_PATTERN = '/.*(Thumbs\.db|__MACOSX|__MACOSX\/|__MACOSX\/.*|\.DS_STORE)$/i'; /** - * @var array Supported file kinds + * @var array|null Supported file kinds * * @see getFileKinds() */ - private static array $_fileKinds; + private static ?array $_fileKinds; /** - * @var array Allowed file kinds + * @var array|null Allowed file kinds * * @see getAllowedFileKinds() */ - private static array $_allowedFileKinds; + private static ?array $_allowedFileKinds; /** * Get a temporary file path. @@ -350,7 +351,7 @@ public static function getAllowedFileKinds(): array */ public static function getFileKindLabel(string $kind): string { - return self::fileKinds()[$kind]['label'] ?? Asset::KIND_UNKNOWN; + return self::fileKinds()[$kind]['label'] ?? FileKind::Unknown->value; } /** @@ -371,7 +372,7 @@ public static function getFileKindByExtension(string $file): string } } - return Asset::KIND_UNKNOWN; + return FileKind::Unknown->value; } /** @@ -399,241 +400,10 @@ private static function fileKinds(): array return self::$_fileKinds; } - self::$_fileKinds = [ - Asset::KIND_ACCESS => [ - 'label' => 'Access', - 'extensions' => [ - 'accdb', - 'accde', - 'accdr', - 'accdt', - 'adp', - 'mdb', - ], - ], - Asset::KIND_AUDIO => [ - 'label' => t('Audio'), - 'extensions' => [ - '3gp', - 'aac', - 'act', - 'aif', - 'aifc', - 'aiff', - 'alac', - 'amr', - 'au', - 'dct', - 'dss', - 'dvf', - 'flac', - 'gsm', - 'iklax', - 'ivs', - 'm4a', - 'm4p', - 'mmf', - 'mp3', - 'mpc', - 'msv', - 'oga', - 'ogg', - 'opus', - 'ra', - 'tta', - 'vox', - 'wav', - 'wma', - 'wv', - ], - ], - Asset::KIND_CAPTIONS_SUBTITLES => [ - 'label' => t('Captions/Subtitles'), - 'extensions' => [ - 'asc', - 'cap', - 'cin', - 'dfxp', - 'itt', - 'lrc', - 'mcc', - 'mpsub', - 'rt', - 'sami', - 'sbv', - 'scc', - 'smi', - 'srt', - 'stl', - 'sub', - 'tds', - 'ttml', - 'vtt', - ], - ], - Asset::KIND_COMPRESSED => [ - 'label' => t('Compressed'), - 'extensions' => [ - '7z', - 'bz2', - 'dmg', - 'gz', - 'rar', - 's7z', - 'tar', - 'tgz', - 'zip', - 'zipx', - ], - ], - Asset::KIND_EXCEL => [ - 'label' => 'Excel', - 'extensions' => [ - 'xls', - 'xlsm', - 'xlsx', - 'xltm', - 'xltx', - ], - ], - Asset::KIND_HTML => [ - 'label' => 'HTML', - 'extensions' => [ - 'htm', - 'html', - ], - ], - Asset::KIND_ILLUSTRATOR => [ - 'label' => 'Illustrator', - 'extensions' => [ - 'ai', - ], - ], - Asset::KIND_IMAGE => [ - 'label' => t('Image'), - 'extensions' => [ - 'avif', - 'bmp', - 'gif', - 'heic', - 'heif', - 'jfif', - 'jp2', - 'jpe', - 'jpeg', - 'jpg', - 'jpx', - 'pam', - 'pfm', - 'pgm', - 'png', - 'pnm', - 'ppm', - 'svg', - 'tif', - 'tiff', - 'webp', - ], - ], - Asset::KIND_JAVASCRIPT => [ - 'label' => 'JavaScript', - 'extensions' => [ - 'js', - ], - ], - Asset::KIND_JSON => [ - 'label' => 'JSON', - 'extensions' => [ - 'json', - ], - ], - Asset::KIND_PDF => [ - 'label' => 'PDF', - 'extensions' => [ - 'pdf', - ], - ], - Asset::KIND_PHOTOSHOP => [ - 'label' => 'Photoshop', - 'extensions' => [ - 'psb', - 'psd', - ], - ], - Asset::KIND_PHP => [ - 'label' => 'PHP', - 'extensions' => [ - 'php', - ], - ], - Asset::KIND_POWERPOINT => [ - 'label' => 'PowerPoint', - 'extensions' => [ - 'potx', - 'pps', - 'ppsm', - 'ppsx', - 'ppt', - 'pptm', - 'pptx', - ], - ], - Asset::KIND_TEXT => [ - 'label' => t('Text'), - 'extensions' => [ - 'text', - 'txt', - ], - ], - Asset::KIND_VIDEO => [ - 'label' => t('Video'), - 'extensions' => [ - 'asf', - 'asx', - 'avchd', - 'avi', - 'fla', - 'flv', - 'hevc', - 'm1s', - 'm2s', - 'm2t', - 'm2v', - 'm4v', - 'mkv', - 'mng', - 'mov', - 'mp2v', - 'mp4', - 'mpeg', - 'mpg', - 'ogg', - 'ogv', - 'qt', - 'rm', - 'vob', - 'webm', - 'wmv', - ], - ], - Asset::KIND_WORD => [ - 'label' => 'Word', - 'extensions' => [ - 'doc', - 'docm', - 'docx', - 'dot', - 'dotm', - 'dotx', - ], - ], - Asset::KIND_XML => [ - 'label' => 'XML', - 'extensions' => [ - 'xml', - ], - ], - ]; + self::$_fileKinds = collect(FileKind::cases()) + ->filter(fn (FileKind $kind) => $kind !== FileKind::Unknown) + ->mapWithKeys(fn (FileKind $kind) => [$kind->value => $kind->toArray()]) + ->all(); // Merge with the extraFileKinds setting self::$_fileKinds = Arr::merge(self::$_fileKinds, Cms::config()->extraFileKinds); @@ -643,6 +413,12 @@ private static function fileKinds(): array return self::$_fileKinds = Arr::sort($event->fileKinds, 'label'); } + public static function clear(): void + { + self::$_fileKinds = null; + self::$_allowedFileKinds = null; + } + /** * Returns the maximum allowed upload size in bytes per all config settings combined. */ diff --git a/src/Asset/Conditions/DateModifiedConditionRule.php b/src/Asset/Conditions/DateModifiedConditionRule.php index 5f166a05bd8..dfe71321388 100644 --- a/src/Asset/Conditions/DateModifiedConditionRule.php +++ b/src/Asset/Conditions/DateModifiedConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Asset\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Condition\BaseDateRangeConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AssetQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Asset/Conditions/FileSizeConditionRule.php b/src/Asset/Conditions/FileSizeConditionRule.php index 431d4d035eb..ba2ab5b9f62 100644 --- a/src/Asset/Conditions/FileSizeConditionRule.php +++ b/src/Asset/Conditions/FileSizeConditionRule.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\Asset\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Condition\BaseNumberConditionRule; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AssetQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Support\Html; diff --git a/src/Asset/Conditions/FileTypeConditionRule.php b/src/Asset/Conditions/FileTypeConditionRule.php index 7806cc71898..36d8d8b5924 100644 --- a/src/Asset/Conditions/FileTypeConditionRule.php +++ b/src/Asset/Conditions/FileTypeConditionRule.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\Asset\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\AssetsHelper; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AssetQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use Override; diff --git a/src/Asset/Conditions/FilenameConditionRule.php b/src/Asset/Conditions/FilenameConditionRule.php index 3c4f79913a7..3dbd70d85b9 100644 --- a/src/Asset/Conditions/FilenameConditionRule.php +++ b/src/Asset/Conditions/FilenameConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Asset\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AssetQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Asset/Conditions/HasAltConditionRule.php b/src/Asset/Conditions/HasAltConditionRule.php index 5c7e21036ac..1ce68b148ee 100644 --- a/src/Asset/Conditions/HasAltConditionRule.php +++ b/src/Asset/Conditions/HasAltConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Asset\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Condition\BaseLightswitchConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AssetQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Asset/Conditions/HeightConditionRule.php b/src/Asset/Conditions/HeightConditionRule.php index e387eb01885..03c88583a77 100644 --- a/src/Asset/Conditions/HeightConditionRule.php +++ b/src/Asset/Conditions/HeightConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Asset\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Condition\BaseNumberConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AssetQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Asset/Conditions/SavableConditionRule.php b/src/Asset/Conditions/SavableConditionRule.php index 28d7f0f1cbb..547eef1f060 100644 --- a/src/Asset/Conditions/SavableConditionRule.php +++ b/src/Asset/Conditions/SavableConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Asset\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseLightswitchConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AssetQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use Illuminate\Support\Facades\Gate; diff --git a/src/Asset/Conditions/UploaderConditionRule.php b/src/Asset/Conditions/UploaderConditionRule.php index 5ddd03cf513..0ef055ed86b 100644 --- a/src/Asset/Conditions/UploaderConditionRule.php +++ b/src/Asset/Conditions/UploaderConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Asset\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Condition\BaseElementSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AssetQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\User\Elements\User; diff --git a/src/Asset/Conditions/ViewableConditionRule.php b/src/Asset/Conditions/ViewableConditionRule.php index 0a95a72deac..3931bf54875 100644 --- a/src/Asset/Conditions/ViewableConditionRule.php +++ b/src/Asset/Conditions/ViewableConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Asset\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseLightswitchConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AssetQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use Illuminate\Support\Facades\Gate; diff --git a/src/Asset/Conditions/VolumeConditionRule.php b/src/Asset/Conditions/VolumeConditionRule.php index c9edf78c7aa..fe706da6daa 100644 --- a/src/Asset/Conditions/VolumeConditionRule.php +++ b/src/Asset/Conditions/VolumeConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Asset\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AssetQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Support\Facades\Volumes; diff --git a/src/Asset/Conditions/WidthConditionRule.php b/src/Asset/Conditions/WidthConditionRule.php index bbe6c6955e6..ca40185630c 100644 --- a/src/Asset/Conditions/WidthConditionRule.php +++ b/src/Asset/Conditions/WidthConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Asset\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Condition\BaseNumberConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\AssetQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Asset/Data/Volume.php b/src/Asset/Data/Volume.php index df7143f0a5a..1352bb71e74 100644 --- a/src/Asset/Data/Volume.php +++ b/src/Asset/Data/Volume.php @@ -26,7 +26,6 @@ use Illuminate\Support\Facades\Storage; use Override; use RuntimeException; -use yii\base\InvalidConfigException; use function CraftCms\Cms\t; @@ -162,6 +161,7 @@ public function attributeLabels(): array ]; } + #[Override] public function getAttributeLabel(string $attribute): string { return $this->attributeLabels()[$attribute] ?? $attribute; @@ -334,7 +334,7 @@ public function getFs(): FsInterface { if (! isset($this->_fs)) { if (! $this->getFsHandle()) { - throw new InvalidConfigException('Volume is missing its filesystem handle.'); + throw new RuntimeException('Volume is missing its filesystem handle.'); } $target = $this->resolveStorageTargetKey($this->_fsHandle); @@ -522,7 +522,7 @@ private function diskNameForOperations(?string $handle = null): string { $target = $this->resolveStorageTargetKey($handle ?? $this->_fsHandle); if ($target === null) { - throw new InvalidConfigException('Volume is missing or has an invalid filesystem handle.'); + throw new RuntimeException('Volume is missing or has an invalid filesystem handle.'); } if (str_starts_with($target, self::STORAGE_DISK_PREFIX)) { @@ -532,13 +532,13 @@ private function diskNameForOperations(?string $handle = null): string if (str_starts_with($target, self::STORAGE_FS_PREFIX)) { $handle = substr($target, strlen(self::STORAGE_FS_PREFIX)); if ($handle === '') { - throw new InvalidConfigException('Volume has an invalid filesystem handle.'); + throw new RuntimeException('Volume has an invalid filesystem handle.'); } return Filesystems::toDiskName($handle); } - throw new InvalidConfigException('Volume has an invalid filesystem handle.'); + throw new RuntimeException('Volume has an invalid filesystem handle.'); } private function storageDiskFor(string $diskName, ?string $prefix): FilesystemAdapter @@ -554,7 +554,7 @@ private function storageDiskFor(string $diskName, ?string $prefix): FilesystemAd ]); if (! $disk instanceof FilesystemAdapter) { - throw new InvalidConfigException('Invalid filesystem disk configuration.'); + throw new RuntimeException('Invalid filesystem disk configuration.'); } return $disk; diff --git a/src/Asset/Elements/Asset.php b/src/Asset/Elements/Asset.php index 56e39942093..0ddaef6a9c8 100644 --- a/src/Asset/Elements/Asset.php +++ b/src/Asset/Elements/Asset.php @@ -5,11 +5,6 @@ namespace CraftCms\Cms\Asset\Elements; use Craft; -use craft\base\ElementInterface; -use craft\controllers\ElementIndexesController; -use craft\controllers\ElementSelectorModalsController; -use craft\elements\conditions\assets\AssetCondition; -use craft\validators\AssetLocationValidator; use CraftCms\Aliases\Aliases; use CraftCms\Cms\Asset\Actions\CopyReferenceTag; use CraftCms\Cms\Asset\Actions\CopyUrl; @@ -22,8 +17,10 @@ use CraftCms\Cms\Asset\Actions\ReplaceFile; use CraftCms\Cms\Asset\Actions\ShowInFolder; use CraftCms\Cms\Asset\AssetsHelper; +use CraftCms\Cms\Asset\Conditions\AssetCondition; use CraftCms\Cms\Asset\Data\Volume; use CraftCms\Cms\Asset\Data\VolumeFolder; +use CraftCms\Cms\Asset\Enums\FileKind; use CraftCms\Cms\Asset\Events\AfterGenerateTransform; use CraftCms\Cms\Asset\Events\BeforeDefineAssetUrl; use CraftCms\Cms\Asset\Events\BeforeGenerateTransform; @@ -35,13 +32,17 @@ use CraftCms\Cms\Asset\Exceptions\VolumeException; use CraftCms\Cms\Asset\Models\Asset as AssetModel; use CraftCms\Cms\Asset\Validation\AssetRules; +use CraftCms\Cms\Asset\Validation\Rules\AssetLocationRule; use CraftCms\Cms\Cms; +use CraftCms\Cms\Component\Exceptions\UnknownPropertyException; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Cp\Html\ElementHtml; use CraftCms\Cms\Database\Table; use CraftCms\Cms\Edition; use CraftCms\Cms\Element\Actions\Restore; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\CurrentElementIndex; use CraftCms\Cms\Element\Data\EagerLoadPlan; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementAttributeRenderer; @@ -50,7 +51,6 @@ use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\Exceptions\QueryAbortedException; use CraftCms\Cms\Field\Enums\TranslationMethod; -use CraftCms\Cms\Field\Field; use CraftCms\Cms\FieldLayout\FieldLayout; use CraftCms\Cms\Filesystem\Exceptions\FilesystemException; use CraftCms\Cms\Filesystem\Filesystems\Filesystem; @@ -62,6 +62,7 @@ use CraftCms\Cms\Search\SearchQuery; use CraftCms\Cms\Search\SearchQueryTerm; use CraftCms\Cms\Search\SearchQueryTermGroup; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\Assets as AssetsService; use CraftCms\Cms\Support\Facades\ElementSources; @@ -85,23 +86,21 @@ use CraftCms\RulesetValidation\Attributes\Ruleset; use DateInterval; use DateTime; +use Exception; use GraphQL\Type\Definition\Type; use Illuminate\Database\Query\Builder; use Illuminate\Filesystem\LocalFilesystemAdapter; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\DB as DbFacade; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Log; use InvalidArgumentException; +use League\Flysystem\UnableToDeleteFile; use Override; +use RuntimeException; use Stringable; use Twig\Markup; -use yii\base\Exception; -use yii\base\InvalidCallException; -use yii\base\InvalidConfigException; -use yii\base\NotSupportedException; -use yii\base\UnknownPropertyException; use function CraftCms\Cms\t; @@ -147,63 +146,188 @@ class Asset extends Element public const string ERROR_FILENAME_CONFLICT = 'filename_conflict'; - // File kinds - // ------------------------------------------------------------------------- + private static string $_displayName; - public const string KIND_ACCESS = 'access'; + /** + * @var bool Whether this asset represents a folder. + * + * @internal + */ + public bool $isFolder = false; - public const string KIND_AUDIO = 'audio'; + /** + * @var array|null The source path, if this represents a folder. + * + * @internal + */ + public ?array $sourcePath = null; - public const string KIND_CAPTIONS_SUBTITLES = 'captionsSubtitles'; + /** + * @var int|null Folder ID + */ + public ?int $folderId = null; - public const string KIND_COMPRESSED = 'compressed'; + /** + * @var int|null The ID of the user who first added this asset (if known) + */ + public ?int $uploaderId = null; - public const string KIND_EXCEL = 'excel'; + /** + * @var string|null Folder path + */ + public ?string $folderPath = null; - public const string KIND_FLASH = 'flash'; + /** + * @var string|null Kind + */ + #[AllowedInSandbox] + public ?string $kind = null; - public const string KIND_HTML = 'html'; + /** + * @var string|null Alternative text + */ + #[AllowedInSandbox] + public ?string $alt = null; - public const string KIND_ILLUSTRATOR = 'illustrator'; + /** + * @var int|null Size + */ + #[AllowedInSandbox] + public ?int $size = null; - public const string KIND_IMAGE = 'image'; + /** + * @var bool|null Whether the file was kept around when the asset was deleted + */ + public ?bool $keptFile = null; - public const string KIND_JAVASCRIPT = 'javascript'; + /** + * @var DateTime|null Date modified + */ + public ?DateTime $dateModified = null; - public const string KIND_JSON = 'json'; + /** + * @var string|null New file location + */ + public ?string $newLocation = null; - public const string KIND_PDF = 'pdf'; + /** + * @var string|null Location error code + * + * @see AssetLocationRule + */ + public ?string $locationError = null; - public const string KIND_PHOTOSHOP = 'photoshop'; + /** + * @var string|null New filename + */ + public ?string $newFilename = null; - public const string KIND_PHP = 'php'; + /** + * @var int|null New folder ID + */ + public ?int $newFolderId = null; - public const string KIND_POWERPOINT = 'powerpoint'; + /** + * @var string|null The temp file path + */ + public ?string $tempFilePath = null; - public const string KIND_TEXT = 'text'; + /** + * @var bool Whether the asset should avoid filename conflicts when saved. + */ + public bool $avoidFilenameConflicts = false; - public const string KIND_VIDEO = 'video'; + /** + * @var string|null The suggested filename in case of a conflict. + */ + public ?string $suggestedFilename = null; - public const string KIND_WORD = 'word'; + /** + * @var string|null The filename that was used that caused a conflict. + */ + public ?string $conflictingFilename = null; - public const string KIND_XML = 'xml'; + /** + * @var bool Whether the asset was deleted along with its volume + * + * @see beforeDelete() + */ + public bool $deletedWithVolume = false; - public const string KIND_UNKNOWN = 'unknown'; + /** + * @var bool Whether the associated file should be preserved if the asset record is deleted. + * + * @see beforeDelete() + * @see afterDelete() + */ + public bool $keepFileOnDelete = false; - private static string $_displayName; + /** + * @var bool|null Whether the associated file should be sanitized on upload, if it's an image. Defaults to `true`, + * unless it’s a control panel request and is disabled. + * + * @see afterSave() + */ + public ?bool $sanitizeOnUpload = null; - #[Override] - public static function displayName(): string + /** + * @var int|null Volume ID + */ + private ?int $_volumeId = null; + + /** + * @var string Filename + */ + private string $_filename; + + private ?string $_mimeType = null; + + /** + * @var int|null Width + */ + private ?int $_width = null; + + /** + * @var int|null Height + */ + private ?int $_height = null; + + /** + * @var array|null Focal point + */ + private ?array $_focalPoint = null; + + private ?ImageTransform $_transform = null; + + private ?Volume $_volume = null; + + private ?User $_uploader = null; + + private ?int $_oldVolumeId = null; + + public function __construct($config = []) { - if (! isset(self::$_displayName)) { - if (self::isFolderIndex()) { - self::$_displayName = t('Folder'); - } else { - self::$_displayName = t('Asset'); - } + // alt='' actually means something, so we should preserve it. + $alt = Arr::pull($config, 'alt'); + if ($alt !== null) { + $this->alt = $alt; } - return self::$_displayName; + parent::__construct($config); + + if (isset($this->alt)) { + $this->alt = trim($this->alt); + } + + $this->_oldVolumeId = $this->_volumeId; + } + + #[Override] + public static function displayName(): string + { + return self::$_displayName ??= self::isFolderIndex() + ? t('Folder') + : t('Asset'); } #[Override] @@ -272,7 +396,7 @@ public static function eagerLoadingMap(array $sourceElements, string $handle): a // Get the source element IDs $sourceElementIds = array_map(fn (ElementInterface $element) => $element->id, $sourceElements); - $map = DbFacade::table(Table::ASSETS) + $map = DB::table(Table::ASSETS) ->select(['id as source', 'uploaderId as target']) ->whereIn('id', $sourceElementIds) ->whereNotNull('uploaderId') @@ -325,14 +449,10 @@ public static function gqlScopesByContext(mixed $context): array protected static function defineSources(string $context): array { $sources = []; - - if ($context === ElementSources::CONTEXT_INDEX) { - $volumeIds = Volumes::getViewableVolumeIds(); - } else { - $volumeIds = Volumes::getAllVolumeIds(); - } - $user = Auth::user(); + $volumeIds = $context === ElementSources::CONTEXT_INDEX + ? Volumes::getViewableVolumeIds() + : Volumes::getAllVolumeIds(); foreach ($volumeIds as $volumeId) { $folder = Folders::getRootFolderByVolumeId($volumeId); @@ -454,8 +574,8 @@ protected static function defineActions(string $source): array } // Show in folder - $query = Craft::$app->controller instanceof ElementIndexesController - ? Craft::$app->controller->getElementQuery() + $query = app(CurrentElementIndex::class)->isActive() + ? app(CurrentElementIndex::class)->query() : null; if ( $query instanceof AssetQuery && @@ -675,7 +795,7 @@ protected static function indexElements(ElementQueryInterface $elementQuery, ?st $folderQuery = self::_createFolderQueryForIndex($elementQuery, $queryFolder); $totalFolders = $folderQuery->count(); - if ((int) $totalFolders > (int) $elementQuery->offset) { + if ($totalFolders > (int) $elementQuery->offset) { $source = ElementSources::findSource(self::class, $sourceKey); if (isset($source['criteria']['folderId'])) { $baseFolder = Folders::getFolderById($source['criteria']['folderId']); @@ -880,7 +1000,7 @@ private static function _applyFolderQuerySearchCondition(Builder $query, SearchQ return; } - $isPgsql = DbFacade::getDriverName() === 'pgsql'; + $isPgsql = DB::getDriverName() === 'pgsql'; /** @var SearchQueryTerm $token */ if ($token->subLeft || $token->subRight) { @@ -954,188 +1074,14 @@ private static function _assembleSourceInfoForFolder(VolumeFolder $folder, ?User private static function isFolderIndex(): bool { - return ( - Craft::$app->controller instanceof ElementIndexesController || - Craft::$app->controller instanceof ElementSelectorModalsController - ) && (bool) request()->input('foldersOnly'); - } - - /** - * @var bool Whether this asset represents a folder. - * - * @internal - */ - public bool $isFolder = false; - - /** - * @var array|null The source path, if this represents a folder. - * - * @internal - */ - public ?array $sourcePath = null; - - /** - * @var int|null Folder ID - */ - public ?int $folderId = null; - - /** - * @var int|null The ID of the user who first added this asset (if known) - */ - public ?int $uploaderId = null; - - /** - * @var string|null Folder path - */ - public ?string $folderPath = null; - - /** - * @var string|null Kind - */ - #[AllowedInSandbox] - public ?string $kind = null; - - /** - * @var string|null Alternative text - */ - #[AllowedInSandbox] - public ?string $alt = null; - - /** - * @var int|null Size - */ - #[AllowedInSandbox] - public ?int $size = null; - - /** - * @var bool|null Whether the file was kept around when the asset was deleted - */ - public ?bool $keptFile = null; - - /** - * @var DateTime|null Date modified - */ - public ?DateTime $dateModified = null; - - /** - * @var string|null New file location - */ - public ?string $newLocation = null; - - /** - * @var string|null Location error code - * - * @see AssetLocationValidator::validateAttribute() - */ - public ?string $locationError = null; - - /** - * @var string|null New filename - */ - public ?string $newFilename = null; - - /** - * @var int|null New folder ID - */ - public ?int $newFolderId = null; - - /** - * @var string|null The temp file path - */ - public ?string $tempFilePath = null; - - /** - * @var bool Whether the asset should avoid filename conflicts when saved. - */ - public bool $avoidFilenameConflicts = false; - - /** - * @var string|null The suggested filename in case of a conflict. - */ - public ?string $suggestedFilename = null; - - /** - * @var string|null The filename that was used that caused a conflict. - */ - public ?string $conflictingFilename = null; - - /** - * @var bool Whether the asset was deleted along with its volume - * - * @see beforeDelete() - */ - public bool $deletedWithVolume = false; - - /** - * @var bool Whether the associated file should be preserved if the asset record is deleted. - * - * @see beforeDelete() - * @see afterDelete() - */ - public bool $keepFileOnDelete = false; - - /** - * @var bool|null Whether the associated file should be sanitized on upload, if it's an image. Defaults to `true`, - * unless it’s a control panel request and is disabled. - * - * @see afterSave() - */ - public ?bool $sanitizeOnUpload = null; - - /** - * @var int|null Volume ID - */ - private ?int $_volumeId = null; - - /** - * @var string Filename - */ - private string $_filename; - - private ?string $_mimeType = null; - - /** - * @var int|null Width - */ - private ?int $_width = null; - - /** - * @var int|null Height - */ - private ?int $_height = null; - - /** - * @var array|null Focal point - */ - private ?array $_focalPoint = null; - - private ?ImageTransform $_transform = null; - - private ?Volume $_volume = null; - - private ?User $_uploader = null; - - private ?int $_oldVolumeId = null; - - public function __construct($config = []) - { - // alt='' actually means something, so we should preserve it. - $alt = Arr::pull($config, 'alt'); - if ($alt !== null) { - $this->alt = $alt; - } - - parent::__construct($config); + return app(CurrentElementIndex::class)->isActive() && request()->boolean('foldersOnly'); } #[Override] public function __toString(): string { - if (isset($this->_transform)) { - $url = $this->getUrl(); - if ($url) { - return $url; - } + if (isset($this->_transform) && $url = $this->getUrl()) { + return $url; } return parent::__toString(); @@ -1157,6 +1103,7 @@ public function __isset($name): bool if (parent::__isset($name)) { return true; } + if (str_starts_with($name, 'transform:')) { return true; } @@ -1173,9 +1120,6 @@ public function __isset($name): bool * * @param string $name The property name * @return mixed The property value - * - * @throws UnknownPropertyException if the property is not defined - * @throws InvalidCallException if the property is write-only. */ #[Override] public function __get($name) @@ -1186,8 +1130,7 @@ public function __get($name) try { return parent::__get($name); - /** @phpstan-ignore catch.neverThrown */ - } catch (UnknownPropertyException|\CraftCms\Cms\Component\Exceptions\UnknownPropertyException $e) { + } catch (UnknownPropertyException $e) { // Is $name a transform handle? if (($transform = app(ImageTransforms::class)->getTransformByHandle($name)) !== null) { return $this->copyWithTransform($transform); @@ -1197,23 +1140,12 @@ public function __get($name) } } - #[Override] - public function init(): void - { - parent::init(); - - if (isset($this->alt)) { - $this->alt = trim($this->alt); - } - - $this->_oldVolumeId = $this->_volumeId; - } - #[Override] public function setAttributesFromRequest(array $values): void { // alt='' actually means something, so we should preserve it. $alt = Arr::pull($values, 'alt'); + if ($alt !== null) { $this->alt = $alt; } @@ -1248,7 +1180,7 @@ protected function cacheTags(): array ]; // Did the volume just change? - if ($this->_volumeId != $this->_oldVolumeId) { + if ($this->_volumeId !== $this->_oldVolumeId) { $tags[] = "volume:$this->_oldVolumeId"; } @@ -1571,7 +1503,7 @@ protected function safeActionMenuItems(): array if ( $this->getSupportsImageEditor() && Gate::check("editImages:$volume->uid") && - (Auth::id() == $this->uploaderId || Gate::check("editPeerImages:$volume->uid")) + (Auth::id() === $this->uploaderId || Gate::check("editPeerImages:$volume->uid")) ) { $editImageId = sprintf('action-image-edit-%s', mt_rand()); $items[] = [ @@ -1613,7 +1545,7 @@ protected function safeActionMenuItems(): array #[AllowedInSandbox] public function getImg(mixed $transform = null, ?array $sizes = null): ?Markup { - if ($this->kind !== self::KIND_IMAGE) { + if ($this->kind !== FileKind::Image->value) { return null; } @@ -1622,9 +1554,7 @@ public function getImg(mixed $transform = null, ?array $sizes = null): ?Markup $this->setTransform($transform); } - $url = $this->getUrl(); - - if ($url) { + if ($url = $this->getUrl()) { $img = Html::tag('img', '', [ 'src' => $url, 'width' => $this->getWidth(), @@ -1717,7 +1647,7 @@ public function getSrcset(array $sizes, mixed $transform = null): string|false #[AllowedInSandbox] public function getUrlsBySize(array $sizes, mixed $transform = null): array { - if ($this->kind !== self::KIND_IMAGE) { + if ($this->kind !== FileKind::Image->value) { return []; } @@ -1811,7 +1741,7 @@ public function getFieldLayout(): ?FieldLayout { try { return $this->getVolume()->getFieldLayout(); - } catch (InvalidConfigException) { + } catch (RuntimeException) { return null; } } @@ -1819,16 +1749,16 @@ public function getFieldLayout(): ?FieldLayout /** * Returns the asset’s volume folder. * - * @throws InvalidConfigException if [[folderId]] is missing or invalid + * @throws RuntimeException if [[folderId]] is missing or invalid */ public function getFolder(): VolumeFolder { if (! isset($this->folderId)) { - throw new InvalidConfigException('Asset is missing its folder ID'); + throw new RuntimeException('Asset is missing its folder ID'); } if (($folder = Folders::getFolderById($this->folderId)) === null) { - throw new InvalidConfigException('Invalid folder ID: '.$this->folderId); + throw new RuntimeException('Invalid folder ID: '.$this->folderId); } return $folder; @@ -1837,7 +1767,7 @@ public function getFolder(): VolumeFolder /** * Returns the asset’s volume. * - * @throws InvalidConfigException if [[volumeId]] is missing or invalid + * @throws RuntimeException if [[volumeId]] is missing or invalid */ public function getVolume(): Volume { @@ -1850,7 +1780,7 @@ public function getVolume(): Volume } if (($volume = Volumes::getVolumeById($this->_volumeId)) === null) { - throw new InvalidConfigException('Invalid volume ID: '.$this->_volumeId); + throw new RuntimeException('Invalid volume ID: '.$this->_volumeId); } return $this->_volume = $volume; @@ -1909,7 +1839,7 @@ public function setTransform(mixed $transform): Asset * which the rest of the settings should be applied to. * @param bool|null $immediately Whether the image should be transformed immediately * - * @throws InvalidConfigException + * @throws RuntimeException */ #[Override] public function getUrl(mixed $transform = null, ?bool $immediately = null): ?string @@ -2093,7 +2023,7 @@ public function getPreviewTargets(): array /** * Returns the filename, with or without the extension. * - * @throws InvalidConfigException if the filename isn’t set yet + * @throws RuntimeException if the filename isn’t set yet */ #[AllowedInSandbox] public function getFilename(bool $withExtension = true): string @@ -2103,7 +2033,7 @@ public function getFilename(bool $withExtension = true): string } if (! isset($this->_filename)) { - throw new InvalidConfigException('Asset not configured with its filename'); + throw new RuntimeException('Asset not configured with its filename'); } if ($withExtension) { @@ -2321,7 +2251,7 @@ public function getImageTransformSourcePath(): string * Get a temporary copy of the actual file. * * @throws VolumeException If unable to fetch file from volume. - * @throws InvalidConfigException If no volume can be found. + * @throws RuntimeException If no volume can be found. */ public function getCopyOfFile(): string { @@ -2337,7 +2267,7 @@ public function getCopyOfFile(): string * * @return resource * - * @throws InvalidConfigException if [[volumeId]] is missing or invalid + * @throws RuntimeException if [[volumeId]] is missing or invalid * @throws FilesystemException if a stream cannot be created */ public function getStream() @@ -2354,7 +2284,7 @@ public function getStream() /** * Returns the file’s contents. * - * @throws InvalidConfigException if [[volumeId]] is missing or invalid + * @throws RuntimeException if [[volumeId]] is missing or invalid * @throws AssetException if a stream could not be created */ public function getContents(): string @@ -2365,7 +2295,7 @@ public function getContents(): string /** * Generates a base64-encoded [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) for the asset. * - * @throws InvalidConfigException if [[volumeId]] is missing or invalid + * @throws RuntimeException if [[volumeId]] is missing or invalid * @throws AssetException if a stream could not be created */ #[AllowedInSandbox] @@ -2399,7 +2329,7 @@ public function getHasFocalPoint(): bool */ public function getFocalPoint(bool $asCss = false): array|string|null { - if (! in_array($this->kind, [self::KIND_IMAGE, self::KIND_VIDEO], true)) { + if (! in_array($this->kind, [FileKind::Image->value, FileKind::Video->value], true)) { return null; } @@ -2530,7 +2460,7 @@ protected function inlineAttributeInputHtml(string $attribute): string /** * Returns the HTML for asset previews. * - * @throws InvalidConfigException + * @throws RuntimeException */ public function getPreviewHtml(): string { @@ -2544,11 +2474,11 @@ public function getPreviewHtml(): string $editable = ( $this->getSupportsImageEditor() && Gate::check("editImages:$volume->uid") && - (Auth::id() == $this->uploaderId || Gate::check("editPeerImages:$volume->uid")) + (Auth::id() === $this->uploaderId || Gate::check("editPeerImages:$volume->uid")) ); $previewInner = match ($this->kind) { - Asset::KIND_VIDEO => Html::tag('video', Html::tag('source', '', [ + FileKind::Video->value => Html::tag('video', Html::tag('source', '', [ 'type' => $this->getMimeType(), 'src' => $this->url, ]), [ @@ -2556,7 +2486,7 @@ public function getPreviewHtml(): string 'controls' => true, 'preload' => 'metadata', ]), - Asset::KIND_AUDIO => Html::tag('audio', Html::tag('source', '', [ + FileKind::Audio->value => Html::tag('audio', Html::tag('source', '', [ 'src' => $this->url, 'type' => $this->getMimeType(), ]), [ @@ -2655,7 +2585,7 @@ public function getPreviewHtml(): string } $html .= $previewThumbHtml; - } catch (NotSupportedException) { + } catch (RuntimeException) { // NBD } @@ -2906,7 +2836,7 @@ private function _setKind(): void } /** - * @throws InvalidConfigException + * @throws RuntimeException */ #[Override] public function afterSave(bool $isNew): void @@ -2915,8 +2845,8 @@ public function afterSave(bool $isNew): void // Are we uploading an image that needs to be sanitized? if ( isset($this->tempFilePath) && - in_array($this->ruleset->getScenario(), [AssetRules::SCENARIO_REPLACE, AssetRules::SCENARIO_CREATE], true) && - AssetsHelper::getFileKindByExtension($this->tempFilePath) === self::KIND_IMAGE && + $this->ruleset->inScenarios(AssetRules::SCENARIO_REPLACE, AssetRules::SCENARIO_CREATE) && + AssetsHelper::getFileKindByExtension($this->tempFilePath) === FileKind::Image->value && ($this->sanitizeOnUpload ?? ( ! request()->isCpRequest() || Cms::config()->sanitizeCpImageUploads @@ -2931,8 +2861,8 @@ public function afterSave(bool $isNew): void $fallbackHeight = null; if ( isset($this->tempFilePath) && - in_array($this->ruleset->getScenario(), [AssetRules::SCENARIO_REPLACE, AssetRules::SCENARIO_CREATE], true) && - AssetsHelper::getFileKindByExtension($this->tempFilePath) === self::KIND_IMAGE + $this->ruleset->inScenarios(AssetRules::SCENARIO_REPLACE, AssetRules::SCENARIO_CREATE) && + AssetsHelper::getFileKindByExtension($this->tempFilePath) === FileKind::Image->value ) { $imageSize = getimagesize($this->tempFilePath); if (isset($imageSize[0])) { @@ -3000,7 +2930,7 @@ public function afterSave(bool $isNew): void } } - DbFacade::table(Table::ASSETS_SITES) + DB::table(Table::ASSETS_SITES) ->upsert([ 'assetId' => $this->id, 'siteId' => $this->siteId, @@ -3018,7 +2948,7 @@ public function beforeDelete(): bool } // Update the asset record - DbFacade::table(Table::ASSETS) + DB::table(Table::ASSETS) ->where('id', $this->id) ->update([ 'deletedWithVolume' => $this->deletedWithVolume, @@ -3034,7 +2964,7 @@ public function afterDelete(): void if (! $this->keepFileOnDelete) { try { $this->getVolume()->sourceDisk()->delete($this->getPath()); - } catch (InvalidConfigException|NotSupportedException) { + } catch (UnableToDeleteFile) { // NBD } } @@ -3091,7 +3021,7 @@ protected function htmlAttributes(string $context): array ], ]; - if ($this->kind === self::KIND_IMAGE) { + if ($this->kind === FileKind::Image->value) { $attributes['data']['image-width'] = $this->getWidth(); $attributes['data']['image-height'] = $this->getHeight(); } @@ -3141,13 +3071,13 @@ protected function htmlAttributes(string $context): array */ private function _dimensions(mixed $transform = null): array { - if (! in_array($this->kind, [self::KIND_IMAGE, self::KIND_VIDEO], true)) { + if (! in_array($this->kind, [FileKind::Image->value, FileKind::Video->value], true)) { return [null, null]; } if (! $this->_width || ! $this->_height) { if ( - $this->kind === self::KIND_IMAGE && + $this->kind === FileKind::Image->value && $this->ruleset->getScenario() !== AssetRules::SCENARIO_CREATE ) { Log::warning("Asset $this->id is missing its width or height", [__METHOD__]); @@ -3190,7 +3120,7 @@ private function _relocateFile(): void $filename = $this->_filename; } - $hasNewFolder = $folderId != $this->folderId; + $hasNewFolder = $folderId !== $this->folderId; $tempPath = null; @@ -3204,7 +3134,7 @@ private function _relocateFile(): void $newPath = ($newFolder->path ? rtrim((string) $newFolder->path, '/').'/' : '').$filename; // Is this just a simple move/rename within the same volume? - if (! isset($this->tempFilePath) && $oldFolder !== null && $oldFolder->volumeId == $newFolder->volumeId) { + if (! isset($this->tempFilePath) && $oldFolder !== null && $oldFolder->volumeId === $newFolder->volumeId) { if (! $oldVolume->sourceDisk()->move($oldPath, $newPath)) { throw new FilesystemException("Unable to move $oldPath to $newPath"); } @@ -3276,7 +3206,7 @@ private function _relocateFile(): void if ($tempPath && file_exists($tempPath)) { $this->kind = AssetsHelper::getFileKindByExtension($filename); - if ($this->kind === self::KIND_IMAGE) { + if ($this->kind === FileKind::Image->value) { [$this->_width, $this->_height] = ImageHelper::imageSize($tempPath); } else { $this->_width = null; diff --git a/src/Asset/Enums/FileKind.php b/src/Asset/Enums/FileKind.php new file mode 100644 index 00000000000..dc7412cad40 --- /dev/null +++ b/src/Asset/Enums/FileKind.php @@ -0,0 +1,91 @@ + 'Access', + self::Audio => t('Audio'), + self::CaptionsSubtitles => t('Captions/Subtitles'), + self::Compressed => t('Compressed'), + self::Excel => 'Excel', + self::Flash => 'Flash', + self::Html => 'HTML', + self::Illustrator => 'Illustrator', + self::Image => t('Image'), + self::Javascript => 'JavaScript', + self::Json => 'JSON', + self::Pdf => 'PDF', + self::Photoshop => 'Photoshop', + self::Php => 'PHP', + self::Powerpoint => 'PowerPoint', + self::Text => t('Text'), + self::Video => t('Video'), + self::Word => 'Word', + self::Xml => 'XML', + self::Unknown => t('Unknown'), + }; + } + + public function extensions(): array + { + return match ($this) { + self::Access => ['accdb', 'accde', 'accdr', 'accdt', 'adp', 'mdb'], + self::Audio => ['3gp', 'aac', 'act', 'aif', 'aifc', 'aiff', 'alac', 'amr', 'au', 'dct', 'dss', 'dvf', 'flac', 'gsm', 'iklax', 'ivs', 'm4a', 'm4p', 'mmf', 'mp3', 'mpc', 'msv', 'oga', 'ogg', 'opus', 'ra', 'tta', 'vox', 'wav', 'wma', 'wv'], + self::CaptionsSubtitles => ['asc', 'cap', 'cin', 'dfxp', 'itt', 'lrc', 'mcc', 'mpsub', 'rt', 'sami', 'sbv', 'scc', 'smi', 'srt', 'stl', 'sub', 'tds', 'ttml', 'vtt'], + self::Compressed => ['7z', 'bz2', 'dmg', 'gz', 'rar', 's7z', 'tar', 'tgz', 'zip', 'zipx'], + self::Excel => ['xls', 'xlsm', 'xlsx', 'xltm', 'xltx'], + self::Flash => [], + self::Html => ['htm', 'html'], + self::Illustrator => ['ai'], + self::Image => ['avif', 'bmp', 'gif', 'heic', 'heif', 'jfif', 'jp2', 'jpe', 'jpeg', 'jpg', 'jpx', 'pam', 'pfm', 'pgm', 'png', 'pnm', 'ppm', 'svg', 'tif', 'tiff', 'webp'], + self::Javascript => ['js'], + self::Json => ['json'], + self::Pdf => ['pdf'], + self::Photoshop => ['psb', 'psd'], + self::Php => ['php'], + self::Powerpoint => ['potx', 'pps', 'ppsm', 'ppsx', 'ppt', 'pptm', 'pptx'], + self::Text => ['text', 'txt'], + self::Video => ['asf', 'asx', 'avchd', 'avi', 'fla', 'flv', 'hevc', 'm1s', 'm2s', 'm2t', 'm2v', 'm4v', 'mkv', 'mng', 'mov', 'mp2v', 'mp4', 'mpeg', 'mpg', 'ogg', 'ogv', 'qt', 'rm', 'vob', 'webm', 'wmv'], + self::Word => ['doc', 'docm', 'docx', 'dot', 'dotm', 'dotx'], + self::Xml => ['xml'], + self::Unknown => [], + }; + } + + public function toArray(): array + { + return [ + 'label' => $this->label(), + 'extensions' => $this->extensions(), + ]; + } +} diff --git a/src/Asset/PreviewHandlers/Text.php b/src/Asset/PreviewHandlers/Text.php index 98ecc83d46a..8c7465193e2 100644 --- a/src/Asset/PreviewHandlers/Text.php +++ b/src/Asset/PreviewHandlers/Text.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Asset\PreviewHandlers; -use CraftCms\Cms\Asset\Elements\Asset; +use CraftCms\Cms\Asset\Enums\FileKind; use CraftCms\Cms\Support\File; use CraftCms\Cms\Support\Html; @@ -18,7 +18,7 @@ public function getPreviewHtml(array $variables = []): string $contents = Html::encode(file_get_contents($localCopy)); File::delete($localCopy); - $language = $this->asset->kind === Asset::KIND_HTML ? 'markup' : $this->asset->kind; + $language = $this->asset->kind === FileKind::Html->value ? 'markup' : $this->asset->kind; return template('assets/_previews/text', array_merge([ 'asset' => $this->asset, diff --git a/src/Asset/Validation/AssetRules.php b/src/Asset/Validation/AssetRules.php index 2275e3be49a..3db5469ffa9 100644 --- a/src/Asset/Validation/AssetRules.php +++ b/src/Asset/Validation/AssetRules.php @@ -7,7 +7,7 @@ use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Asset\Validation\Rules\AssetLocationRule; use CraftCms\Cms\Element\Validation\ElementRules; -use CraftCms\Cms\FieldLayout\LayoutElements\assets\AltField; +use CraftCms\Cms\FieldLayout\LayoutElements\Assets\AltField; use CraftCms\Cms\Validation\Rules\DisallowMb4; use Illuminate\Validation\Rule; use Override; diff --git a/src/Auth/Events/AuthorizingElement.php b/src/Auth/Events/AuthorizingElement.php index 92051ae86a8..5150b6d5e74 100644 --- a/src/Auth/Events/AuthorizingElement.php +++ b/src/Auth/Events/AuthorizingElement.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Auth\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\User\Elements\User; use Illuminate\Foundation\Events\Dispatchable; diff --git a/src/Component/Component.php b/src/Component/Component.php index bae3d69cc2f..9477cc76f4f 100644 --- a/src/Component/Component.php +++ b/src/Component/Component.php @@ -4,6 +4,7 @@ namespace CraftCms\Cms\Component; +use ArrayAccess; use CraftCms\Cms\Component\Contracts\ComponentInterface; use CraftCms\Cms\Component\Exceptions\InvalidCallException; use CraftCms\Cms\Component\Exceptions\UnknownPropertyException; @@ -22,7 +23,7 @@ use Yiisoft\Arrays\ArrayableTrait; #[Ruleset(ComponentRules::class)] -abstract class Component implements Arrayable, ArrayableInterface, ComponentInterface, Validatable +abstract class Component implements Arrayable, ArrayableInterface, ArrayAccess, ComponentInterface, Validatable { use ArrayableTrait { fields as private traitFields; @@ -171,6 +172,26 @@ public function __unset(string $name): void throw new InvalidCallException('Unsetting an unknown or read-only property: '.static::class.'::'.$name); } + public function offsetExists(mixed $offset): bool + { + return isset($this->$offset); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->$offset; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->$offset = $value; + } + + public function offsetUnset(mixed $offset): void + { + $this->$offset = null; + } + public function __serialize(): array { return $this->toArray(); diff --git a/src/Condition/BaseCondition.php b/src/Condition/BaseCondition.php index 8d8033b1961..ade61422818 100644 --- a/src/Condition/BaseCondition.php +++ b/src/Condition/BaseCondition.php @@ -22,8 +22,8 @@ use Illuminate\Support\Facades\Log; use InvalidArgumentException; use Override; +use RuntimeException; use Throwable; -use yii\base\InvalidConfigException; use function CraftCms\Cms\t; @@ -594,7 +594,7 @@ final public function getConfig(): array ->map(function (ConditionRuleInterface $rule) { try { return $rule->getConfig(); - } catch (InvalidConfigException) { + } catch (RuntimeException) { // The rule is misconfigured return null; } diff --git a/src/Condition/BaseElementSelectConditionRule.php b/src/Condition/BaseElementSelectConditionRule.php index 6701fddbaff..983b2a828e1 100644 --- a/src/Condition/BaseElementSelectConditionRule.php +++ b/src/Condition/BaseElementSelectConditionRule.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\Condition; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Cp\RequestedSite; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; use CraftCms\Cms\Element\Conditions\ElementCondition; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Env; use Override; diff --git a/src/Condition/BaseMultiSelectConditionRule.php b/src/Condition/BaseMultiSelectConditionRule.php index 1390f2ebb57..75ae5f049ea 100644 --- a/src/Condition/BaseMultiSelectConditionRule.php +++ b/src/Condition/BaseMultiSelectConditionRule.php @@ -10,7 +10,7 @@ use CraftCms\Cms\Support\Html; use CraftCms\Cms\Support\Query; use Override; -use yii\base\InvalidConfigException; +use RuntimeException; /** * BaseMultiSelectConditionRule provides a base implementation for condition rules that are composed of a multi-select input. @@ -162,7 +162,7 @@ protected function paramValue(?callable $normalizeValue = null): string|array|nu return match ($this->operator) { self::OPERATOR_IN => $values, self::OPERATOR_NOT_IN => array_merge(['not'], $values), - default => throw new InvalidConfigException("Invalid operator: $this->operator"), + default => throw new RuntimeException("Invalid operator: $this->operator"), }; } @@ -193,7 +193,7 @@ protected function matchValue(array|string|null $value): bool return match ($this->operator) { self::OPERATOR_IN => ! empty(array_intersect($value, $this->_values)), self::OPERATOR_NOT_IN => empty(array_intersect($value, $this->_values)), - default => throw new InvalidConfigException("Invalid operator: $this->operator"), + default => throw new RuntimeException("Invalid operator: $this->operator"), }; } } diff --git a/src/Condition/BaseTextConditionRule.php b/src/Condition/BaseTextConditionRule.php index c3cab4a0bcb..aa6486591fa 100644 --- a/src/Condition/BaseTextConditionRule.php +++ b/src/Condition/BaseTextConditionRule.php @@ -9,7 +9,7 @@ use CraftCms\Cms\Support\Html; use CraftCms\Cms\Support\Query; use Override; -use yii\base\InvalidConfigException; +use RuntimeException; /** * BaseTextConditionRule provides a base implementation for condition rules that are composed of an operator menu and text input. @@ -147,7 +147,7 @@ protected function matchValue(mixed $value): bool self::OPERATOR_BEGINS_WITH => is_string($value) && str_starts_with(mb_strtolower($value), mb_strtolower($this->value)), self::OPERATOR_ENDS_WITH => is_string($value) && str_ends_with(mb_strtolower($value), mb_strtolower($this->value)), self::OPERATOR_CONTAINS => is_string($value) && str_contains(mb_strtolower($value), mb_strtolower($this->value)), - default => throw new InvalidConfigException("Invalid operator: $this->operator"), + default => throw new RuntimeException("Invalid operator: $this->operator"), }; } diff --git a/src/Condition/Contracts/ConditionRuleInterface.php b/src/Condition/Contracts/ConditionRuleInterface.php index dd53b8ed3ac..34fbce1dad2 100644 --- a/src/Condition/Contracts/ConditionRuleInterface.php +++ b/src/Condition/Contracts/ConditionRuleInterface.php @@ -6,7 +6,7 @@ use CraftCms\Cms\Component\Contracts\ComponentInterface; use CraftCms\Cms\Condition\BaseConditionRule; -use yii\base\InvalidConfigException; +use RuntimeException; /** * ConditionRuleInterface defines the common interface to be implemented by condition rule classes. @@ -51,7 +51,7 @@ public function getGroupLabel(): ?string; /** * Returns the rule’s portable config. * - * @throws InvalidConfigException if the rule is misconfigured + * @throws RuntimeException if the rule is misconfigured */ public function getConfig(): array; diff --git a/src/Config/GeneralConfig.php b/src/Config/GeneralConfig.php index 3dee51e577d..c4eae3198c0 100644 --- a/src/Config/GeneralConfig.php +++ b/src/Config/GeneralConfig.php @@ -20,7 +20,7 @@ use Illuminate\Support\Traits\Conditionable; use InvalidArgumentException; use Override; -use yii\base\InvalidConfigException; +use RuntimeException; use function CraftCms\Cms\t; @@ -3922,7 +3922,7 @@ public function defaultCookieDomain(string $value): self public function defaultCountryCode(string $value): self { if (empty($value)) { - throw new InvalidConfigException('`defaultCountryCode` cannot be empty', 0); + throw new RuntimeException('`defaultCountryCode` cannot be empty', 0); } $this->defaultCountryCode = $value; @@ -3939,7 +3939,7 @@ public function defaultCountryCode(string $value): self * * @group System * - * @throws InvalidConfigException + * @throws RuntimeException * * @see $defaultCpLanguage */ @@ -3950,7 +3950,7 @@ public function defaultCpLanguage(?string $value): self $value = I18N::normalizeLanguage($value); /** @phpstan-ignore catch.neverThrown */ } catch (InvalidArgumentException $e) { - throw new InvalidConfigException($e->getMessage(), 0, $e); + throw new RuntimeException($e->getMessage(), 0, $e); } } @@ -4425,7 +4425,7 @@ public function extraAllowedFileExtensions(?array $value): self * * @param string[] $value * - * @throws InvalidConfigException + * @throws RuntimeException * * @see $extraAppLocales */ @@ -4436,7 +4436,7 @@ public function extraAppLocales(array $value): self $localeId = I18N::normalizeLanguage($localeId); /** @phpstan-ignore catch.neverThrown */ } catch (InvalidArgumentException $e) { - throw new InvalidConfigException($e->getMessage(), 0, $e); + throw new RuntimeException($e->getMessage(), 0, $e); } } @@ -5623,7 +5623,7 @@ public function rasterizeSvgThumbs(bool $value = true): self * * @defaultAlt 14 days * - * @throws InvalidConfigException + * @throws RuntimeException * * @see $rememberedUserSessionDuration * @since 4.2.0 @@ -5634,7 +5634,7 @@ public function rememberedUserSessionDuration(mixed $value): self try { $interval = DateTimeHelper::toDateInterval($value); } catch (InvalidArgumentException $e) { - throw new InvalidConfigException($e->getMessage(), 0, $e); + throw new RuntimeException($e->getMessage(), 0, $e); } $this->rememberedUserSessionDuration = $interval ? ConfigHelper::durationInSeconds($interval) : 0; diff --git a/src/Console/Commands/Utils/PruneProvisionalDraftsCommand.php b/src/Console/Commands/Utils/PruneProvisionalDraftsCommand.php index 757df99b238..bb103e53c21 100644 --- a/src/Console/Commands/Utils/PruneProvisionalDraftsCommand.php +++ b/src/Console/Commands/Utils/PruneProvisionalDraftsCommand.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Console\Commands\Utils; -use craft\base\ElementInterface; use CraftCms\Cms\Console\CraftCommand; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Elements; use CraftCms\Cms\Support\Str; use Illuminate\Console\Command; diff --git a/src/Console/Commands/Utils/PruneRevisionsCommand.php b/src/Console/Commands/Utils/PruneRevisionsCommand.php index 153a41b9e8d..af4798eaaee 100644 --- a/src/Console/Commands/Utils/PruneRevisionsCommand.php +++ b/src/Console/Commands/Utils/PruneRevisionsCommand.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Console\Commands\Utils; -use craft\base\ElementInterface; use CraftCms\Cms\Config\GeneralConfig; use CraftCms\Cms\Console\CraftCommand; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Elements; use CraftCms\Cms\Support\Facades\Sections; use CraftCms\Cms\Support\Str; diff --git a/src/Cp/Events/DefineElementCardHtml.php b/src/Cp/Events/DefineElementCardHtml.php index 9dc54f641e4..049648ef9ba 100644 --- a/src/Cp/Events/DefineElementCardHtml.php +++ b/src/Cp/Events/DefineElementCardHtml.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Cp\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class DefineElementCardHtml { diff --git a/src/Cp/Events/DefineElementChipHtml.php b/src/Cp/Events/DefineElementChipHtml.php index 19cb3153df7..93fe9a2232c 100644 --- a/src/Cp/Events/DefineElementChipHtml.php +++ b/src/Cp/Events/DefineElementChipHtml.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Cp\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class DefineElementChipHtml { diff --git a/src/Cp/FormFields.php b/src/Cp/FormFields.php index 0a51f755dd8..31cc17a6601 100644 --- a/src/Cp/FormFields.php +++ b/src/Cp/FormFields.php @@ -18,8 +18,9 @@ use CraftCms\Cms\Support\Str; use CraftCms\Cms\View\TemplateMode; use Illuminate\Support\Facades\Auth; +use Illuminate\Validation\ConditionalRules; +use Illuminate\Validation\Rules\RequiredIf; use InvalidArgumentException; -use yii\validators\RequiredValidator; use function CraftCms\Cms\t; use function CraftCms\Cms\template; @@ -565,20 +566,22 @@ public static function addressFieldsHtml(Address $address, bool $static = false) $requiredFields = []; $scenario = $address->ruleset->getScenario(); $address->ruleset->useScenario(ElementRules::SCENARIO_LIVE); - $activeValidators = $address->getActiveValidators(); - $address->ruleset->useScenario($scenario); - $belongsToCurrentUser = $address->getBelongsToCurrentUser(); - foreach ($activeValidators as $validator) { - if ($validator instanceof RequiredValidator) { - foreach ($validator->getAttributeNames() as $attr) { - if ($validator->when === null || call_user_func($validator->when, $address, $attr)) { - $requiredFields[$attr] = true; - } + $activeRules = $address->ruleset->rules(); + + foreach ($activeRules as $attribute => $rules) { + foreach (Arr::wrap($rules) as $rule) { + if (self::isRequiredRule($rule)) { + $requiredFields[$attribute] = true; + + break; } } } + $address->ruleset->useScenario($scenario); + $belongsToCurrentUser = $address->getBelongsToCurrentUser(); + $addressesService = app(Addresses::class); $visibleFields = array_flip(array_merge( $addressesService->getUsedFields($address->countryCode), @@ -697,6 +700,29 @@ public static function addressFieldsHtml(Address $address, bool $static = false) ]); } + private static function isRequiredRule(mixed $rule): bool + { + if ($rule === 'required') { + return true; + } + + if ($rule instanceof RequiredIf) { + return (string) $rule === 'required'; + } + + if ($rule instanceof ConditionalRules) { + $conditionalRules = $rule->passes() ? $rule->rules() : $rule->defaultRules(); + + foreach (Arr::wrap($conditionalRules) as $conditionalRule) { + if (self::isRequiredRule($conditionalRule)) { + return true; + } + } + } + + return false; + } + private static function subdivisionParents(Address $address, array $visibleFields): array { $baseSubdivisionRepository = new BaseSubdivisionRepository; diff --git a/src/Cp/Html/ElementHtml.php b/src/Cp/Html/ElementHtml.php index bcbafbb22ba..7a50cabcd2c 100644 --- a/src/Cp/Html/ElementHtml.php +++ b/src/Cp/Html/ElementHtml.php @@ -4,8 +4,6 @@ namespace CraftCms\Cms\Cp\Html; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; use CraftCms\Cms\Component\Contracts\Actionable; use CraftCms\Cms\Component\Contracts\Chippable; use CraftCms\Cms\Component\Contracts\Colorable; @@ -20,6 +18,8 @@ use CraftCms\Cms\Cp\Events\DefineElementChipHtml; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Cp\Icons; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Enums\AttributeStatus; use CraftCms\Cms\Shared\Enums\Color; @@ -30,7 +30,7 @@ use Illuminate\Container\Attributes\Singleton; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Gate; -use yii\base\InvalidConfigException; +use RuntimeException; use function CraftCms\Cms\t; @@ -573,7 +573,7 @@ private function elementOwnerIsCanonical(ElementInterface $element): bool $owner = null; try { $owner = $element instanceof NestedElementInterface ? $element->getPrimaryOwner() : null; - } catch (InvalidConfigException) { + } catch (RuntimeException) { } if (! $owner) { break; diff --git a/src/Cp/Html/ElementIndexHtml.php b/src/Cp/Html/ElementIndexHtml.php index 441611ae8d1..682ae1c8916 100644 --- a/src/Cp/Html/ElementIndexHtml.php +++ b/src/Cp/Html/ElementIndexHtml.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Cp\Html; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementSources; use CraftCms\Cms\Site\Sites; use CraftCms\Cms\Support\Facades\HtmlStack; diff --git a/src/Cp/Html/MenuHtml.php b/src/Cp/Html/MenuHtml.php index 954711eb22f..908af489923 100644 --- a/src/Cp/Html/MenuHtml.php +++ b/src/Cp/Html/MenuHtml.php @@ -4,11 +4,13 @@ namespace CraftCms\Cms\Cp\Html; +use CraftCms\Cms\Cms; use CraftCms\Cms\Element\Enums\MenuItemType; use CraftCms\Cms\Site\Data\Site; use CraftCms\Cms\Site\Sites; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\SiteGroups; +use CraftCms\Cms\Support\Str; use CraftCms\Cms\Support\Url; use CraftCms\Cms\View\TemplateMode; use Illuminate\Container\Attributes\Singleton; @@ -151,7 +153,7 @@ public function siteMenuItems( ->keyBy(fn (array $site) => $site['site']->id) ->all(); - $path = request()->path(); + $path = Str::after(request()->decodedPath(), Cms::config()->cpTrigger.'/'); $params = Arr::except(request()->query(), 'fresh'); $totalSites = 0; diff --git a/src/Cp/Html/PreviewHtml.php b/src/Cp/Html/PreviewHtml.php index 33f2b225bba..24284456423 100644 --- a/src/Cp/Html/PreviewHtml.php +++ b/src/Cp/Html/PreviewHtml.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Cp\Html; -use craft\base\ElementInterface; use CraftCms\Cms\Component\Contracts\Chippable; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Html; use CraftCms\Cms\Support\Json; use CraftCms\Cms\Translation\I18N; diff --git a/src/Database/ElementRelationParamFilter.php b/src/Database/ElementRelationParamFilter.php index d077446d6d7..5c468549038 100644 --- a/src/Database/ElementRelationParamFilter.php +++ b/src/Database/ElementRelationParamFilter.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Database; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Field\BaseRelationField; use CraftCms\Cms\Field\Contracts\FieldInterface; diff --git a/src/Database/Migrations/BaseContentRefactorMigration.php b/src/Database/Migrations/BaseContentRefactorMigration.php index 1c95b2ed621..27be15db4d7 100644 --- a/src/Database/Migrations/BaseContentRefactorMigration.php +++ b/src/Database/Migrations/BaseContentRefactorMigration.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Database\Migrations; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Migration; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; use CraftCms\Cms\Field\Field; use CraftCms\Cms\Field\MissingField; diff --git a/src/Database/Migrations/Install.php b/src/Database/Migrations/Install.php index f7518585090..187cfeb21cd 100644 --- a/src/Database/Migrations/Install.php +++ b/src/Database/Migrations/Install.php @@ -6,7 +6,7 @@ namespace CraftCms\Cms\Database\Migrations; -use CraftCms\Cms\Asset\Elements\Asset; +use CraftCms\Cms\Asset\Enums\FileKind; use CraftCms\Cms\Cms; use CraftCms\Cms\Database\Migration; use CraftCms\Cms\Database\Migrations\Event\PostCreateTables; @@ -157,7 +157,7 @@ public function createTables(): void $table->integer('uploaderId')->nullable(); $table->string('filename'); $table->string('mimeType')->nullable(); - $table->string('kind', 50)->default(Asset::KIND_UNKNOWN); + $table->string('kind', 50)->default(FileKind::Unknown->value); $table->text('alt')->nullable(); $table->unsignedInteger('width')->nullable(); $table->unsignedInteger('height')->nullable(); diff --git a/src/Element/Actions/ChangeSortOrder.php b/src/Element/Actions/ChangeSortOrder.php index 509455128c3..a6ed0e69343 100644 --- a/src/Element/Actions/ChangeSortOrder.php +++ b/src/Element/Actions/ChangeSortOrder.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Actions; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\HtmlStack; use function CraftCms\Cms\t; diff --git a/src/Element/Actions/Delete.php b/src/Element/Actions/Delete.php index a9c9c24ec26..7c4bb2fe75a 100644 --- a/src/Element/Actions/Delete.php +++ b/src/Element/Actions/Delete.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\Element\Actions; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; use craft\services\Elements; use CraftCms\Cms\Database\Table; use CraftCms\Cms\Element\Contracts\DeleteActionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Support\Facades\HtmlStack; use CraftCms\Cms\Support\Html; diff --git a/src/Element/Actions/DeleteForSite.php b/src/Element/Actions/DeleteForSite.php index c3ea463c406..aedc46f8f7b 100644 --- a/src/Element/Actions/DeleteForSite.php +++ b/src/Element/Actions/DeleteForSite.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Actions; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Support\Facades\Elements; use CraftCms\Cms\Support\Facades\HtmlStack; diff --git a/src/Element/Actions/Duplicate.php b/src/Element/Actions/Duplicate.php index 061075a22ec..ae37854586f 100644 --- a/src/Element/Actions/Duplicate.php +++ b/src/Element/Actions/Duplicate.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Actions; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Support\Facades\Elements; use CraftCms\Cms\Support\Facades\HtmlStack; diff --git a/src/Element/Actions/ElementAction.php b/src/Element/Actions/ElementAction.php index 7203499e103..1116e437877 100644 --- a/src/Element/Actions/ElementAction.php +++ b/src/Element/Actions/ElementAction.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Element\Actions; -use craft\base\ElementInterface; use CraftCms\Cms\Component\Component; use CraftCms\Cms\Component\Concerns\ConfigurableComponent; use CraftCms\Cms\Element\Contracts\ElementActionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Element/Actions/MoveDown.php b/src/Element/Actions/MoveDown.php index 461c20c4c0a..ebcddad4e42 100644 --- a/src/Element/Actions/MoveDown.php +++ b/src/Element/Actions/MoveDown.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Actions; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\HtmlStack; use function CraftCms\Cms\t; diff --git a/src/Element/Actions/MoveUp.php b/src/Element/Actions/MoveUp.php index 8880cb9f462..c0a83fad232 100644 --- a/src/Element/Actions/MoveUp.php +++ b/src/Element/Actions/MoveUp.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Actions; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\HtmlStack; use function CraftCms\Cms\t; diff --git a/src/Element/Actions/SetStatus.php b/src/Element/Actions/SetStatus.php index d51cb090c06..730ec46c635 100644 --- a/src/Element/Actions/SetStatus.php +++ b/src/Element/Actions/SetStatus.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Actions; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Validation\ElementRules; diff --git a/src/Element/BulkOp/BulkOps.php b/src/Element/BulkOp/BulkOps.php index 0cc7f306255..9cd86adc82b 100644 --- a/src/Element/BulkOp/BulkOps.php +++ b/src/Element/BulkOp/BulkOps.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Element\BulkOp; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; use CraftCms\Cms\Element\BulkOp\Events\AfterBulkOp; use CraftCms\Cms\Element\BulkOp\Events\BeforeBulkOp; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Str; use Illuminate\Container\Attributes\Scoped; diff --git a/src/Element/Commands/Concerns/ResolvesElementById.php b/src/Element/Commands/Concerns/ResolvesElementById.php index 1f0190e3353..1b1e496f18f 100644 --- a/src/Element/Commands/Concerns/ResolvesElementById.php +++ b/src/Element/Commands/Concerns/ResolvesElementById.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Commands\Concerns; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\Elements; use Illuminate\Console\Command; diff --git a/src/Element/Commands/DeleteAllOfTypeCommand.php b/src/Element/Commands/DeleteAllOfTypeCommand.php index ac9b0a473f4..ff1be489f09 100644 --- a/src/Element/Commands/DeleteAllOfTypeCommand.php +++ b/src/Element/Commands/DeleteAllOfTypeCommand.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Element\Commands; -use craft\base\ElementInterface; use CraftCms\Cms\Component\ComponentHelper; use CraftCms\Cms\Console\CraftCommand; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Section\Enums\SectionType; use CraftCms\Cms\Support\Facades\Sections; diff --git a/src/Element/Commands/Resave/ResaveCommand.php b/src/Element/Commands/Resave/ResaveCommand.php index 27591b6943f..82e89cf12be 100644 --- a/src/Element/Commands/Resave/ResaveCommand.php +++ b/src/Element/Commands/Resave/ResaveCommand.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Commands\Resave; -use craft\base\ElementInterface; use CraftCms\Cms\Console\CraftCommand; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Events\AfterPropagateElement; diff --git a/src/Element/Concerns/Cacheable.php b/src/Element/Concerns/Cacheable.php index 2e0e1d2121e..fe341a4d73b 100644 --- a/src/Element/Concerns/Cacheable.php +++ b/src/Element/Concerns/Cacheable.php @@ -20,8 +20,6 @@ trait Cacheable * Returns the cache tags that should be cleared when this element is saved. * * @return string[] - * - * @since 3.5.0 */ public function getCacheTags(): array { @@ -34,8 +32,6 @@ public function getCacheTags(): array * Returns the cache tags that should be cleared when this element is saved. * * @return string[] - * - * @since 4.1.0 */ protected function cacheTags(): array { diff --git a/src/Element/Concerns/DisplayedInIndex.php b/src/Element/Concerns/DisplayedInIndex.php index 16f47461760..88af0ab5058 100644 --- a/src/Element/Concerns/DisplayedInIndex.php +++ b/src/Element/Concerns/DisplayedInIndex.php @@ -4,12 +4,12 @@ namespace CraftCms\Cms\Element\Concerns; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; use CraftCms\Cms\Auth\SessionAuth; -use CraftCms\Cms\Element\Drafts; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementAttributeRenderer; +use CraftCms\Cms\Element\Enums\ElementIndexViewMode; use CraftCms\Cms\Element\Events\PrepQueryForTableAttribute; use CraftCms\Cms\Element\Events\RegisterCardAttributes; use CraftCms\Cms\Element\Events\RegisterDefaultCardAttributes; @@ -18,14 +18,13 @@ use CraftCms\Cms\Element\Events\RegisterSortOptions; use CraftCms\Cms\Element\Events\RegisterTableAttributes; 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\Drafts; use CraftCms\Cms\Support\Facades\ElementSources; use CraftCms\Cms\Support\Facades\Fields; -use CraftCms\Cms\Support\Facades\I18N; use CraftCms\Cms\Support\Facades\Structures; use DateInterval; use DateTime; @@ -49,8 +48,6 @@ trait DisplayedInIndex { /** * @var string|null The view mode used to show this element (e.g. `structure`, `table`, `thumbs`, `cards`). - * - * @since 5.6.0 */ public ?string $viewMode = null; @@ -59,7 +56,7 @@ trait DisplayedInIndex * * @return string[] The searchable attributes */ - public static function searchableAttributes(): array + final public static function searchableAttributes(): array { event($event = new RegisterSearchableAttributes( elementType: static::class, @@ -232,7 +229,7 @@ public static function indexHtml( } // See if there are any provisional changes we should show - app(Drafts::class)->loadProvisionalChanges($elements); + Drafts::loadProvisionalChanges($elements); if (request()->boolean('prevalidate')) { foreach ($elements as $element) { @@ -256,10 +253,10 @@ public static function indexHtml( /** * Returns an element query without descendant ID exclusions. * - * @param ElementQueryInterface|ElementQuery $elementQuery The element query - * @return ElementQueryInterface|ElementQuery The modified element query + * @param ElementQueryInterface $elementQuery The element query + * @return ElementQueryInterface The modified element query */ - private static function elementQueryWithAllDescendants(ElementQueryInterface $elementQuery): ElementQueryInterface|ElementQuery + private static function elementQueryWithAllDescendants(ElementQueryInterface $elementQuery): ElementQueryInterface { $wheres = $elementQuery->getQuery()->wheres; @@ -323,8 +320,6 @@ private static function prepCustomFieldQuery(ElementQueryInterface $elementQuery * @param ElementQueryInterface $elementQuery The element query * @param string|null $sourceKey The source key * @return ElementInterface[] The elements - * - * @since 4.4.0 */ protected static function indexElements(ElementQueryInterface $elementQuery, ?string $sourceKey): array { @@ -345,48 +340,21 @@ public static function indexElementCount(ElementQueryInterface $elementQuery, ?s /** * Returns the available view modes for the element index. - * - * @return array The view modes */ public static function indexViewModes(): array { - $viewModes = [ - [ - 'mode' => 'structure', - 'title' => t('Display in a structured table'), - 'icon' => I18N::getLocale()->getOrientation() === 'rtl' ? 'structurertl' : 'structure', + return array_values(array_filter([ + array_merge(ElementIndexViewMode::Structure->toArray(), [ 'structuresOnly' => true, - ], - [ - 'mode' => 'table', - 'title' => t('Display in a table'), - 'icon' => 'list', + ]), + array_merge(ElementIndexViewMode::Table->toArray(), [ 'availableOnMobile' => false, - ], - ]; - - if (static::hasThumbs()) { - $viewModes[] = [ - 'mode' => 'thumbs', - 'title' => t('Display as thumbnails'), - 'icon' => 'grid', - ]; - } - - $viewModes[] = [ - 'mode' => 'cards', - 'title' => t('Display as cards'), - 'icon' => 'element-cards', - ]; - - return $viewModes; + ]), + static::hasThumbs() ? ElementIndexViewMode::Thumbs->toArray() : null, + ElementIndexViewMode::Cards->toArray(), + ])); } - /** - * Returns the sort options for the element type. - * - * @return array The sort options - */ public static function sortOptions(): array { $sortOptions = static::defineSortOptions(); @@ -415,21 +383,11 @@ public static function sortOptions(): array protected static function defineSortOptions(): array { // Default to the available table attributes - $tableAttributes = ElementSources::getAvailableTableAttributes(static::class); - $sortOptions = []; - - foreach ($tableAttributes as $key => $labelInfo) { - $sortOptions[$key] = $labelInfo['label']; - } - - return $sortOptions; + return ElementSources::getAvailableTableAttributes(static::class) + ->map(fn (array $labelInfo) => $labelInfo['label']) + ->all(); } - /** - * Returns the table attributes for the element type. - * - * @return array The table attributes - */ public static function tableAttributes(): array { event($event = new RegisterTableAttributes( @@ -500,9 +458,7 @@ public static function defaultTableAttributes(string $source): array protected static function defineDefaultTableAttributes(string $source): array { // Return all of them by default - $availableTableAttributes = static::tableAttributes(); - - return array_keys($availableTableAttributes); + return array_keys(static::tableAttributes()); } /** @@ -510,8 +466,6 @@ protected static function defineDefaultTableAttributes(string $source): array * * @param FieldLayout|null $fieldLayout The field layout * @return array The card attributes - * - * @since 5.5.0 */ public static function cardAttributes(?FieldLayout $fieldLayout = null): array { @@ -530,7 +484,6 @@ public static function cardAttributes(?FieldLayout $fieldLayout = null): array * @return array The card attributes. * * @see cardAttributes() - * @since 5.5.0 */ protected static function defineCardAttributes(): array { @@ -579,8 +532,6 @@ protected static function defineCardAttributes(): array * * @param array $attribute The attribute configuration * @return mixed The preview HTML - * - * @since 5.5.0 */ public static function attributePreviewHtml(array $attribute): mixed { @@ -597,8 +548,6 @@ public static function attributePreviewHtml(array $attribute): mixed * Returns the default card attributes. * * @return string[] The default card attribute keys - * - * @since 5.5.0 */ public static function defaultCardAttributes(): array { @@ -617,7 +566,6 @@ public static function defaultCardAttributes(): array * * @see defaultCardAttributes() * @see cardAttributes() - * @since 5.5.0 */ protected static function defineDefaultCardAttributes(): array { @@ -759,7 +707,7 @@ private static function resolveSourceSortOption(string $sourceKey, string $attri $orderBy = $sortOption['orderBy']; if ($orderBy instanceof Coalesce) { - $sql = $orderBy->getValue(DB::connection()->getQueryGrammar()); + $sql = $orderBy->getValue(DB::getQueryGrammar()); } elseif (is_string($orderBy)) { $sql = $orderBy; } else { diff --git a/src/Element/Concerns/Draftable.php b/src/Element/Concerns/Draftable.php index 2f1612df3c6..9e75b69d0e6 100644 --- a/src/Element/Concerns/Draftable.php +++ b/src/Element/Concerns/Draftable.php @@ -5,8 +5,7 @@ namespace CraftCms\Cms\Element\Concerns; use CraftCms\Cms\Database\Table; -use CraftCms\Cms\Element\Events\AuthorizeCreateDrafts; -use CraftCms\Cms\User\Elements\User as UserElement; +use CraftCms\Cms\User\Elements\User; use Illuminate\Support\Facades\DB; /** @@ -27,8 +26,6 @@ trait Draftable /** * @var bool Whether the element is a draft that is about to be applied to the canonical element. - * - * @since 5.9.0 */ public bool $applyingDraft = false; @@ -68,36 +65,33 @@ trait Draftable public bool $markDraftAsSaved = true; /** - * @var UserElement|null|false The creator + * @var User|null|false The creator */ - private UserElement|false|null $draftCreator = null; + private User|false|null $draftCreator = null; /** * Returns the draft’s creator. */ - public function getDraftCreator(): ?UserElement + public function getDraftCreator(): ?User { - if (! isset($this->draftCreator)) { - if (! $this->draftCreatorId) { - return null; - } - - /** @var UserElement|null $creator */ - $creator = UserElement::find() - ->id($this->draftCreatorId) - ->status(null) - ->one(); - - $this->draftCreator = $creator ?? false; + if (isset($this->draftCreator)) { + return $this->draftCreator ?: null; } - return $this->draftCreator ?: null; + if (! $this->draftCreatorId) { + return null; + } + + /** @var User|null $creator */ + $creator = User::find() + ->id($this->draftCreatorId) + ->status(null) + ->first(); + + return $this->draftCreator = $creator ?? false; } - /** - * Sets the draft's creator. - */ - public function setDraftCreator(?UserElement $creator = null): void + public function setDraftCreator(?User $creator = null): void { $this->draftCreator = $creator ?? false; } @@ -137,14 +131,12 @@ public function handleDraftDelete(): void DB::table(Table::DRAFTS)->delete($this->draftId); } - public function canCreateDrafts(UserElement $user): bool + public function canCreateDrafts(User $user): bool { - event($event = new AuthorizeCreateDrafts($this, $user)); - - return $event->authorized; + return $user->can('createDrafts', $this); } - public function canDuplicateAsDraft(UserElement $user): bool + public function canDuplicateAsDraft(User $user): bool { // if anything, this will be more lenient than canDuplicate() return $user->can('duplicate', $this); diff --git a/src/Element/Concerns/Eagerloadable.php b/src/Element/Concerns/Eagerloadable.php index 58cb43e1a9e..5df3a6608b7 100644 --- a/src/Element/Concerns/Eagerloadable.php +++ b/src/Element/Concerns/Eagerloadable.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Concerns; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Data\EagerLoadInfo; use CraftCms\Cms\Element\Data\EagerLoadPlan; use CraftCms\Cms\Element\ElementCollection; @@ -33,15 +33,11 @@ trait Eagerloadable { /** * @var ElementInterface[]|null All elements that the element was queried with. - * - * @since 5.0.0 */ public ?array $elementQueryResult = null; /** * @var EagerLoadInfo|null Info about the eager loading setup used to query this element. - * - * @since 5.0.0 */ public ?EagerLoadInfo $eagerLoadInfo = null; @@ -82,8 +78,6 @@ trait Eagerloadable * but still used elsewhere. * * @phpstan-return EagerLoadingMap|null|false - * - * @since 3.1.0 */ public static function eagerLoadingMap(array $sourceElements, string $handle): array|null|false { @@ -218,10 +212,10 @@ private static function _mapDescendants(array $sourceElements, bool $children): $map = []; foreach ($elementStructureData as $elementStructureDatum) { foreach ($descendantStructureData as $descendantStructureDatum) { - if (! ($descendantStructureDatum['structureId'] == $elementStructureDatum['structureId'] && $descendantStructureDatum['lft'] > $elementStructureDatum['lft'] && $descendantStructureDatum['rgt'] < $elementStructureDatum['rgt'])) { + if (! ($descendantStructureDatum['structureId'] === $elementStructureDatum['structureId'] && $descendantStructureDatum['lft'] > $elementStructureDatum['lft'] && $descendantStructureDatum['rgt'] < $elementStructureDatum['rgt'])) { continue; } - if (! (! $children || $descendantStructureDatum['level'] == $elementStructureDatum['level'] + 1)) { + if (! (! $children || $descendantStructureDatum['level'] === $elementStructureDatum['level'] + 1)) { continue; } if (! $descendantStructureDatum['elementId']) { @@ -282,10 +276,10 @@ private static function _mapAncestors(array $sourceElements, bool $parents): ?ar foreach ($elementStructureData as $elementStructureDatum) { foreach ($ancestorStructureData as $ancestorStructureDatum) { if ( - $ancestorStructureDatum['structureId'] == $elementStructureDatum['structureId'] && + $ancestorStructureDatum['structureId'] === $elementStructureDatum['structureId'] && $ancestorStructureDatum['lft'] < $elementStructureDatum['lft'] && $ancestorStructureDatum['rgt'] > $elementStructureDatum['rgt'] && - (! $parents || $ancestorStructureDatum['level'] == $elementStructureDatum['level'] - 1) + (! $parents || $ancestorStructureDatum['level'] === $elementStructureDatum['level'] - 1) ) { if ($ancestorStructureDatum['elementId']) { $map[] = [ @@ -533,7 +527,6 @@ private static function _mapRevisionCreators(array $sourceElements): array * @return bool Whether the eager-loaded elements exist * * @see SetEagerLoadedElements() - * @since 3.5.0 */ public function hasEagerLoadedElements(string $handle): bool { @@ -555,7 +548,6 @@ public function hasEagerLoadedElements(string $handle): bool * @return ElementCollection|null The eager-loaded elements, or null if they don't exist * * @see SetEagerLoadedElements() - * @since 3.5.0 */ public function getEagerLoadedElements(string $handle): ?ElementCollection { @@ -570,6 +562,7 @@ public function getEagerLoadedElements(string $handle): ?ElementCollection } $elements = $this->_eagerLoadedElements[$handle]; + ElementHelper::setNextPrevOnElements($elements); return $elements; @@ -583,7 +576,6 @@ public function getEagerLoadedElements(string $handle): ?ElementCollection * @param EagerLoadPlan $plan The eager-load plan that was used to load the elements * * @see getEagerLoadedElements() - * @since 3.5.0 */ public function setEagerLoadedElements(string $handle, array $elements, EagerLoadPlan $plan): void { @@ -625,8 +617,6 @@ public function setEagerLoadedElements(string $handle, array $elements, EagerLoa * * @param string $handle The handle to mark as lazy-loaded * @param bool $value Whether the elements should be lazy-loaded - * - * @since 5.0.0 */ public function setLazyEagerLoadedElements(string $handle, bool $value = true): void { @@ -640,7 +630,6 @@ public function setLazyEagerLoadedElements(string $handle, bool $value = true): * @return int|null The number of eager-loaded elements, or null if they don't exist * * @see setEagerLoadedElementCount() - * @since 3.5.0 */ public function getEagerLoadedElementCount(string $handle): ?int { @@ -662,7 +651,6 @@ public function getEagerLoadedElementCount(string $handle): ?int * @param int $count The number of eager-loaded elements * * @see getEagerLoadedElementCount() - * @since 3.5.0 */ public function setEagerLoadedElementCount(string $handle, int $count): void { diff --git a/src/Element/Concerns/Exportable.php b/src/Element/Concerns/Exportable.php index a1143c22c56..fcf17726ad1 100644 --- a/src/Element/Concerns/Exportable.php +++ b/src/Element/Concerns/Exportable.php @@ -23,8 +23,6 @@ trait Exportable * * @param string $source The selected source's key * @return array The available element exporters - * - * @since 3.4.0 */ public static function exporters(string $source): array { @@ -44,7 +42,6 @@ public static function exporters(string $source): array * @return array The available element exporters * * @see exporters() - * @since 3.4.0 */ protected static function defineExporters(string $source): array { diff --git a/src/Element/Concerns/HasActions.php b/src/Element/Concerns/HasActions.php index e04652d8b5a..cf2547b3367 100644 --- a/src/Element/Concerns/HasActions.php +++ b/src/Element/Concerns/HasActions.php @@ -88,8 +88,6 @@ public static function actions(string $source): array /** * Returns whether the Set Status action should be included in [[actions()]] automatically. - * - * @since 4.3.2 */ protected static function includeSetStatusAction(): bool { diff --git a/src/Element/Concerns/HasCanonical.php b/src/Element/Concerns/HasCanonical.php index 03fac5f9ce6..69d1ebe1e08 100644 --- a/src/Element/Concerns/HasCanonical.php +++ b/src/Element/Concerns/HasCanonical.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\Element\Concerns; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Queries\Contracts\NestedElementQueryInterface; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use DateTime; -use yii\base\NotSupportedException; /** * HasCanonical provides support for canonical elements and their derivatives. @@ -29,15 +29,11 @@ trait HasCanonical { /** * @var DateTime|null The date that the canonical element was last merged into this one - * - * @since 3.7.0 */ public ?DateTime $dateLastMerged = null; /** * @var bool Whether recent changes to the canonical element are being merged into this element. - * - * @since 3.7.0 */ public bool $mergingCanonicalChanges = false; @@ -45,8 +41,6 @@ trait HasCanonical * @var bool Whether the element is being updated from a derivative element, such as a draft or revision. * * If this is true, the derivative element can be accessed via [[duplicateOf]]. - * - * @since 3.7.0 */ public bool $updatingFromDerivative = false; diff --git a/src/Element/Concerns/HasControlPanelUI.php b/src/Element/Concerns/HasControlPanelUI.php index 4c66b503367..1339146155c 100644 --- a/src/Element/Concerns/HasControlPanelUI.php +++ b/src/Element/Concerns/HasControlPanelUI.php @@ -4,12 +4,10 @@ namespace CraftCms\Cms\Element\Concerns; -use Craft; -use craft\base\NestedElementInterface; -use craft\controllers\ElementsController; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Cp\Html\ElementHtml; use CraftCms\Cms\Cp\Html\MenuHtml; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\ElementAttributeRenderer; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Events\DefineActionMenuItems; @@ -21,6 +19,7 @@ use CraftCms\Cms\Element\Events\DefineMetaFieldsHtml; use CraftCms\Cms\Element\Events\DefineSidebarHtml; use CraftCms\Cms\Element\Events\RegisterHtmlAttributes; +use CraftCms\Cms\Http\Requests\ElementRequest; use CraftCms\Cms\Http\Responses\CpScreenResponse; use CraftCms\Cms\Shared\Enums\Color; use CraftCms\Cms\Support\Arr; @@ -32,7 +31,7 @@ use CraftCms\Cms\Translation\Formatter; use Illuminate\Support\Facades\Gate; use Stringable; -use yii\web\Response; +use Symfony\Component\HttpFoundation\Response; use function CraftCms\Cms\t; @@ -66,7 +65,7 @@ trait HasControlPanelUI /** * Performs any action after the element's editor is fully ready. */ - public function prepareEditScreen(Response|CpScreenResponse $response, string $containerId): void {} + public function prepareEditScreen(Response|CpScreenResponse|\yii\web\Response $response, string $containerId): void {} public function getAdditionalButtons(): string|Stringable { @@ -156,7 +155,6 @@ public function getActionMenuItems(): array * * @see getActionMenuItems() * @see MenuHtml::disclosureMenu() - * @since 5.0.0 */ protected function safeActionMenuItems(): array { @@ -166,8 +164,7 @@ protected function safeActionMenuItems(): array if ( ! $this->getIsRevision() && ! request()->headers->has('X-Craft-Container-Id') && - Craft::$app->controller instanceof ElementsController && - Craft::$app->controller->element === $this + app(ElementRequest::class)->element === $this ) { $validateId = sprintf('action-validate-%s', mt_rand()); $items[] = [ @@ -292,7 +289,6 @@ protected function safeActionMenuItems(): array * * @see getActionMenuItems() * @see MenuHtml::disclosureMenu() - * @since 5.0.0 */ protected function destructiveActionMenuItems(): array { @@ -472,7 +468,6 @@ public function getInlineAttributeInputHtml(string $attribute): string|Stringabl * @return string The HTML that should be shown for a given attribute in table and card views. * * @see getAttributeHtml() - * @since 5.0.0 */ protected function attributeHtml(string $attribute): string|Stringable { @@ -486,7 +481,6 @@ protected function attributeHtml(string $attribute): string|Stringable * @return string The HTML that should be shown for a given attribute's inline input. * * @see getInlineAttributeInputHtml() - * @since 5.0.0 */ protected function inlineAttributeInputHtml(string $attribute): string|Stringable { @@ -522,8 +516,6 @@ public function getSidebarHtml(bool $static): string|Stringable * Returns the HTML for any meta fields that should be shown within the editor sidebar. * * @param bool $static Whether the fields should be static (non-interactive) - * - * @since 3.7.0 */ protected function metaFieldsHtml(bool $static): string|Stringable { @@ -536,8 +528,6 @@ protected function metaFieldsHtml(bool $static): string|Stringable * Returns the HTML for the element's Slug field. * * @param bool $static Whether the fields should be static (non-interactive) - * - * @since 3.7.0 */ protected function slugFieldHtml(bool $static): string|Stringable { @@ -562,9 +552,7 @@ protected function slugFieldHtml(bool $static): string|Stringable /** * Returns whether the Status field should be shown for this element. * - * If set to `false`, the element's status can't be updated via edit forms, the Set Status action, or `resave/*` commands. - * - * @since 4.5.0 + * If set to `false`, the element's status can't be updated via edit forms, the Set Status action, or `resave/*` commands. */ protected function showStatusField(): bool { @@ -573,8 +561,6 @@ protected function showStatusField(): bool /** * Returns the status field HTML for the sidebar. - * - * @since 4.0.0 */ protected function statusFieldHtml(): string|Stringable { @@ -628,8 +614,6 @@ protected function statusFieldHtml(): string|Stringable /** * Returns the notes field HTML for the sidebar. - * - * @since 4.0.0 */ protected function notesFieldHtml(): string|Stringable { @@ -653,8 +637,6 @@ protected function notesFieldHtml(): string|Stringable * Returns whether the element has a field layout with at least one tab. * * @return bool Returns whether the element has a field layout with at least one tab. - * - * @since 3.7.0 */ protected function hasFieldLayout(): bool { @@ -725,8 +707,6 @@ public function getMetadata(): array * * @return array The data, with keys representing the labels. The values can either be strings or callables. * If a value is `false`, it will be omitted. - * - * @since 3.7.0 */ protected function metadata(): array { @@ -761,7 +741,6 @@ public function getCrumbs(): array /** * Returns the breadcrumbs that lead up to the element. * - * @since 5.0.0 * @see getCrumbs() */ protected function crumbs(): array @@ -791,8 +770,6 @@ public function setUiLabelPath(array $path): void /** * Returns what the element should be called within the control panel. - * - * @since 3.6.4 */ protected function uiLabel(): ?string { diff --git a/src/Element/Concerns/HasCustomFields.php b/src/Element/Concerns/HasCustomFields.php index 1c23c5bb92e..a0eda0b7ecd 100644 --- a/src/Element/Concerns/HasCustomFields.php +++ b/src/Element/Concerns/HasCustomFields.php @@ -12,8 +12,8 @@ use CraftCms\Cms\FieldLayout\FieldLayout; use Illuminate\Database\Query\Builder; use Illuminate\Support\Facades\DB; +use RuntimeException; use UnitEnum; -use yii\base\InvalidConfigException; /** * HasCustomFields provides custom field handling for elements. @@ -393,7 +393,7 @@ protected function fieldLayoutFields(bool $visibleOnly = false, bool $editableOn { try { $fieldLayout = $this->getFieldLayout(); - } catch (InvalidConfigException) { + } catch (RuntimeException) { return []; } diff --git a/src/Element/Concerns/HasGqlType.php b/src/Element/Concerns/HasGqlType.php index 345e208330b..c85f6a29d2d 100644 --- a/src/Element/Concerns/HasGqlType.php +++ b/src/Element/Concerns/HasGqlType.php @@ -23,18 +23,12 @@ public static function baseGqlType(): Type return ElementGqlType::getType(); } - /** - * @since 3.3.0 - */ public static function gqlScopesByContext(mixed $context): array { // Default to no scopes required return []; } - /** - * @since 3.3.0 - */ public function getGqlTypeName(): string { // Default to the short class name diff --git a/src/Element/Concerns/HasPreviewTargets.php b/src/Element/Concerns/HasPreviewTargets.php index 06bb5606428..7624fe33131 100644 --- a/src/Element/Concerns/HasPreviewTargets.php +++ b/src/Element/Concerns/HasPreviewTargets.php @@ -24,8 +24,6 @@ trait HasPreviewTargets { /** * @var bool Whether the element is currently being previewed. - * - * @since 3.2.0 */ public bool $previewing = false; @@ -73,7 +71,6 @@ public function getPreviewTargets(): array * Each target should be represented by a sub-array with `'label'` and `'url'` keys. * * @see getPreviewTargets() - * @since 3.2.0 */ protected function previewTargets(): array { diff --git a/src/Element/Concerns/HasRoutesAndUrls.php b/src/Element/Concerns/HasRoutesAndUrls.php index c0bae7c50ac..c90a59e2454 100644 --- a/src/Element/Concerns/HasRoutesAndUrls.php +++ b/src/Element/Concerns/HasRoutesAndUrls.php @@ -4,16 +4,16 @@ namespace CraftCms\Cms\Element\Concerns; -use craft\base\NestedElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Events\BeforeDefineUrl; use CraftCms\Cms\Element\Events\DefineUrl; use CraftCms\Cms\Element\Events\SetRoute; use CraftCms\Cms\Support\Html; -use CraftCms\Cms\Support\Template; use CraftCms\Cms\Support\Url; use CraftCms\Cms\Twig\Attributes\AllowedInSandbox; +use Illuminate\Support\HtmlString; use Twig\Markup; /** @@ -51,17 +51,14 @@ public function getUriFormat(): ?string public function getRoute(): mixed { - // Fire a 'setRoute' event event($event = new SetRoute($this)); + if ($event->handled || $event->route !== null) { return $event->route ?: null; } - if ($this instanceof NestedElementInterface) { - $field = $this->getField(); - if ($field) { - return $field->getRouteForElement($this); - } + if ($this instanceof NestedElementInterface && $field = $this->getField()) { + return $field->getRouteForElement($this); } return $this->route(); @@ -86,8 +83,8 @@ public function getIsHomepage(): bool public function getUrl(): ?string { - // Fire a 'beforeDefineUrl' event event($beforeEvent = new BeforeDefineUrl($this)); + $url = $beforeEvent->url; $handled = $beforeEvent->handled; @@ -97,8 +94,8 @@ public function getUrl(): ?string $url = Url::siteUrl($path, null, null, $this->siteId); } - // Fire a 'defineUrl' event event($event = new DefineUrl($this, $url)); + // If DefineUrl::$url is set to null, only respect that if $handled is true if ($event->url !== null || $event->handled) { $url = $event->url; @@ -124,8 +121,6 @@ public function getCpEditUrl(): ?string /** * Returns the element's edit URL in the control panel. - * - * @since 3.7.0 */ protected function cpEditUrl(): ?string { @@ -137,7 +132,7 @@ public function getPostEditUrl(): ?string return null; } - public function getLink(): ?Markup + public function getLink(): ?HtmlString { if (($url = $this->getUrl()) === null) { return null; @@ -145,6 +140,6 @@ public function getLink(): ?Markup $a = Html::a(Html::encode($this->getUiLabel()), $url); - return Template::raw($a); + return new HtmlString($a); } } diff --git a/src/Element/Concerns/HasSources.php b/src/Element/Concerns/HasSources.php index 94345949616..33914d40af3 100644 --- a/src/Element/Concerns/HasSources.php +++ b/src/Element/Concerns/HasSources.php @@ -86,7 +86,6 @@ public static function fieldLayouts(?string $source): array * @return FieldLayout[] The associated field layouts * * @see fieldLayouts() - * @since 3.5.0 */ protected static function defineFieldLayouts(?string $source): array { diff --git a/src/Element/Concerns/HasStatuses.php b/src/Element/Concerns/HasStatuses.php index 74fdb0de9dd..113cc259882 100644 --- a/src/Element/Concerns/HasStatuses.php +++ b/src/Element/Concerns/HasStatuses.php @@ -21,14 +21,13 @@ */ trait HasStatuses { - public const STATUS_ENABLED = 'enabled'; + public const string STATUS_ENABLED = 'enabled'; - public const STATUS_DISABLED = 'disabled'; + public const string STATUS_DISABLED = 'disabled'; - public const STATUS_ARCHIVED = 'archived'; + public const string STATUS_ARCHIVED = 'archived'; - /** @since 5.0.0 */ - public const STATUS_DRAFT = 'draft'; + public const string STATUS_DRAFT = 'draft'; /** * @var bool Whether the element is enabled @@ -73,11 +72,16 @@ public function getEnabledForSite(?int $siteId = null): ?bool /** * Sets whether the element is enabled for the current site. * - * @param array|bool $enabledForSite Whether the element is enabled for the current site, - * or an array of site ID => enabled status pairs. + * @param array|bool|int $enabledForSite Whether the element is enabled for the current site, + * or an array of site ID => enabled status pairs. */ - public function setEnabledForSite(array|bool $enabledForSite): void + public function setEnabledForSite(array|bool|int $enabledForSite): void { + /** This gets retrieved as an int from the database in some cases */ + if (is_int($enabledForSite)) { + $enabledForSite = (bool) $enabledForSite; + } + $this->_enabledForSite = is_array($enabledForSite) ? array_map(boolval(...), $enabledForSite) : $enabledForSite; diff --git a/src/Element/Concerns/HasThumbnails.php b/src/Element/Concerns/HasThumbnails.php index 0ec893d3e25..86e8bfc7d37 100644 --- a/src/Element/Concerns/HasThumbnails.php +++ b/src/Element/Concerns/HasThumbnails.php @@ -89,8 +89,6 @@ private function renderSvgThumb(string $thumbSvg): string * Returns the URL to the element's thumbnail, if it has one. * * @param int $size The maximum width and height the thumbnail should have. - * - * @since 5.0.0 */ protected function thumbUrl(int $size): ?string { @@ -100,8 +98,6 @@ protected function thumbUrl(int $size): ?string /** * Returns the element's thumbnail SVG contents, which should be used as a fallback when [[getThumbUrl()]] * returns `null`. - * - * @since 4.5.0 */ protected function thumbSvg(): ?string { @@ -110,8 +106,6 @@ protected function thumbSvg(): ?string /** * Returns alt text for the element's thumbnail. - * - * @since 5.0.0 */ protected function thumbAlt(): ?string { @@ -120,8 +114,6 @@ protected function thumbAlt(): ?string /** * Returns whether the element's thumbnail should have a checkered background. - * - * @since 5.0.0 */ protected function hasCheckeredThumb(): bool { @@ -130,8 +122,6 @@ protected function hasCheckeredThumb(): bool /** * Returns whether the element's thumbnail should be rounded. - * - * @since 5.0.0 */ protected function hasRoundedThumb(): bool { @@ -140,8 +130,6 @@ protected function hasRoundedThumb(): bool /** * Returns whether the element's thumbnail is potentially animated. - * - * @since 5.7.0 */ protected function couldHaveAnimatedThumb(): bool { diff --git a/src/Element/Concerns/Localizable.php b/src/Element/Concerns/Localizable.php index 466275eec5f..968a920584a 100644 --- a/src/Element/Concerns/Localizable.php +++ b/src/Element/Concerns/Localizable.php @@ -4,17 +4,16 @@ namespace CraftCms\Cms\Element\Concerns; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; -use CraftCms\Cms\Element\Queries\ElementQuery; use CraftCms\Cms\Field\Enums\TranslationMethod; use CraftCms\Cms\Site\Data\Site; use CraftCms\Cms\Support\Facades\Sites; use CraftCms\Cms\Twig\Attributes\AllowedInSandbox; -use yii\base\InvalidConfigException; +use RuntimeException; /** * Localizable provides localization functionality for elements. @@ -46,45 +45,33 @@ trait Localizable /** * @var ElementInterface|null The element that this element is being propagated from. - * - * @since 5.0.0 */ public ?ElementInterface $propagatingFrom = null; /** * @var bool Whether all element attributes should be propagated across all its supported sites, even if that means * overwriting existing site-specific values. - * - * @since 3.2.0 */ public bool $propagateAll = false; /** * @var bool Whether all required element attributes should be propagated across all its supported sites, but only if otherwise * they wouldn’t validate. - * - * @since 5.9.0 */ public bool $propagateRequired = false; /** * @var int[] The site IDs that the element was just propagated to for the first time. - * - * @since 3.2.9 */ public array $newSiteIds = []; /** * @var bool Whether the element is being saved to the current site for the first time. - * - * @since 3.7.15 */ public bool $isNewForSite = false; /** * @var bool Whether this is for a newly-created site. - * - * @since 5.6.10 */ public bool $isNewSite = false; @@ -104,10 +91,7 @@ public function getRootOwner(): ElementInterface : $this; } - /** - * @since 3.5.0 - */ - public function getLocalized(): ElementQueryInterface|ElementQuery|ElementCollection + public function getLocalized(): ElementQueryInterface|ElementCollection { // Eager-loaded? if ($localized = $this->getEagerLoadedElements('localized')) { @@ -166,7 +150,7 @@ public function getIsCrossSiteCopyable(): bool } /** - * @throws InvalidConfigException if [[siteId]] is invalid + * @throws RuntimeException if [[siteId]] is invalid */ public function getSite(): Site { @@ -174,12 +158,9 @@ public function getSite(): Site return $site; } - throw new InvalidConfigException("Invalid site ID: {$this->siteId}"); + throw new RuntimeException("Invalid site ID: {$this->siteId}"); } - /** - * @since 3.5.0 - */ public function getLanguage(): string { return $this->getSite()->getLanguage(); diff --git a/src/Element/Concerns/NestedElement.php b/src/Element/Concerns/NestedElement.php new file mode 100644 index 00000000000..ecc645cf377 --- /dev/null +++ b/src/Element/Concerns/NestedElement.php @@ -0,0 +1,474 @@ +|null Owner type + */ + private ?string $ownerType = null; + + /** + * @var int|null Field ID + */ + #[AllowedInSandbox] + public ?int $fieldId = null; + + /** + * @var int|null Sort order + */ + public ?int $sortOrder = null; + + /** + * @var bool Whether to save the element’s row in the `elements_owners` table from `afterSave()`. + */ + public bool $saveOwnership = true; + + /** + * @var bool Whether the search index should be updated for the owner element, alongside this element. + * + * This will only be checked if [[fieldId]] is set, and `false` isn’t passed to the `updateSearchIndex` + * argument of [[\craft\services\Elements::saveElement()]]. + * + * @since 5.2.0 + */ + public bool $updateSearchIndexForOwner = false; + + /** + * @var ElementInterface|false|null The primary owner element, or false if [[primaryOwnerId]] is invalid + * + * @see getPrimaryOwner() + * @see setPrimaryOwner() + */ + private ElementInterface|false|null $_primaryOwner = null; + + /** + * @var ElementInterface|false|null The owner element, or false if [[ownerId]] is invalid + * + * @see getOwner() + * @see setOwner() + */ + private ElementInterface|false|null $_owner = null; + + /** + * @var ElementInterface[] + * + * @see getOwners() + */ + private array $_owners; + + public static function eagerLoadingMap(array $sourceElements, string $handle): array|null|false + { + return match ($handle) { + 'owner', 'primaryOwner' => [ + /** @phpstan-ignore-next-line */ + 'map' => array_filter(array_map(function (NestedElementInterface $element) use ($handle) { + $ownerId = match ($handle) { + 'owner' => $element->getOwnerId(), + 'primaryOwner' => $element->getPrimaryOwnerId(), + }; + + return $ownerId ? [ + 'source' => $element->id, + 'target' => $ownerId, + ] : null; + }, $sourceElements)), + 'criteria' => [ + 'status' => null, + ], + ], + default => parent::eagerLoadingMap($sourceElements, $handle), + }; + } + + public function __clone(): void + { + parent::__clone(); + + $this->_primaryOwner = null; + $this->_owner = null; + $this->ownerType = null; + } + + public function __get($name) + { + return match ($name) { + 'ownerId' => $this->getOwnerId(), + 'primaryOwnerId' => $this->getPrimaryOwnerId(), + default => parent::__get($name), + }; + } + + public function attributes(): array + { + $names = parent::attributes(); + $names[] = 'primaryOwnerId'; + $names[] = 'ownerId'; + + return $names; + } + + public function extraFields(): array + { + $names = parent::extraFields(); + $names[] = 'primaryOwner'; + $names[] = 'owner'; + + return $names; + } + + public function getPrimaryOwnerId(): ?int + { + return $this->primaryOwnerId ?? $this->ownerId; + } + + public function setPrimaryOwnerId(?int $id): void + { + $this->primaryOwnerId = $id; + + if (! $id || $this->_primaryOwner === false || $this->_primaryOwner?->id !== $id) { + $this->_primaryOwner = null; + } + } + + public function getPrimaryOwner(): ?ElementInterface + { + if (isset($this->_primaryOwner)) { + return $this->_primaryOwner ?: null; + } + + if (! $primaryOwnerId = $this->getPrimaryOwnerId()) { + return null; + } + + $sameSiteElements = isset($this->id, $this->elementQueryResult) + ? array_filter($this->elementQueryResult, fn (ElementInterface $element) => $element->siteId === $this->siteId) + : []; + + if (! empty($sameSiteElements)) { + // Eager-load the primary owner for each of the elements in the result, + // as we're probably going to end up needing them too + Elements::eagerLoadElements($this::class, $sameSiteElements, [ + [ + 'path' => 'primaryOwner', + 'criteria' => $this->ownerCriteria(), + ], + ]); + } + + /** @phpstan-ignore-next-line */ + if (! isset($this->_primaryOwner) || $this->_primaryOwner === false) { + // Either we didn't try, or the primary owner couldn't be eager-loaded for some reason + if (! $ownerType = $this->ownerType()) { + return null; + } + + $query = $ownerType::find()->id($primaryOwnerId); + Typecast::configure($query, $this->ownerCriteria()); + $this->_primaryOwner = $query->one() ?? false; + + if (! $this->_primaryOwner) { + throw new RuntimeException("Invalid owner ID: $primaryOwnerId"); + } + } + + return $this->_primaryOwner; + } + + public function setPrimaryOwner(?ElementInterface $owner): void + { + $this->_primaryOwner = $owner ?? false; + $this->primaryOwnerId = $owner->id ?? null; + } + + public function getOwnerId(): ?int + { + return $this->ownerId ?? $this->primaryOwnerId; + } + + public function setOwnerId(?int $id): void + { + $this->ownerId = $id; + + if (! $id || $this->_owner === false || $this->_owner?->id !== $id) { + $this->_owner = null; + } + } + + public function getOwner(): ?ElementInterface + { + if (! isset($this->_owner)) { + $ownerId = $this->getOwnerId(); + if (! $ownerId) { + return null; + } + + // If ownerId and primaryOwnerId are the same, return the primary owner + if ($ownerId === $this->getPrimaryOwnerId()) { + return $this->getPrimaryOwner(); + } + + $sameSiteElements = isset($this->id, $this->elementQueryResult) + ? array_filter($this->elementQueryResult, fn (ElementInterface $element) => $element->siteId === $this->siteId) + : []; + + if (! empty($sameSiteElements)) { + // Eager-load the owner for each of the elements in the result, + // as we're probably going to end up needing them too + Elements::eagerLoadElements($this::class, $sameSiteElements, [ + [ + 'path' => 'owner', + 'criteria' => $this->ownerCriteria(), + ], + ]); + } + + /** @phpstan-ignore-next-line */ + if (! isset($this->_owner) || $this->_owner === false) { + // Either we didn't try, or the owner couldn't be eager-loaded for some reason + $ownerType = $this->ownerType(); + if (! $ownerType) { + return null; + } + + $query = $ownerType::find()->id($ownerId); + Typecast::configure($query, $this->ownerCriteria()); + $this->_owner = $query->one() ?? false; + + if (! $this->_owner) { + throw new RuntimeException("Invalid owner ID: $ownerId"); + } + } + } + + return $this->_owner ?: null; + } + + public function getOwners(array $criteria = []): array + { + if (! isset($this->_owners)) { + $this->_owners = []; + $ownerType = $this->ownerType(); + if ($ownerType) { + $ownerIds = DB::table(Table::ELEMENTS_OWNERS) + ->where('elementId', $this->id) + ->pluck('ownerId') + ->all(); + + if (! empty($ownerIds)) { + $query = $ownerType::find()->id($ownerIds); + Typecast::configure($query, $criteria + $this->ownerCriteria()); + $this->_owners = $query->all(); + } + } + } + + return $this->_owners; + } + + private function ownerCriteria(): array + { + return [ + 'site' => '*', + 'preferSites' => [$this->siteId], + 'unique' => true, + 'status' => null, + 'drafts' => null, + 'provisionalDrafts' => null, + 'revisions' => null, + 'trashed' => null, + ]; + } + + public function setOwner(?ElementInterface $owner): void + { + $this->_owner = $owner ?? false; + $this->ownerId = $owner->id ?? null; + } + + public function getField(): ?ElementContainerFieldInterface + { + if (! isset($this->fieldId)) { + return null; + } + + $field = null; + + try { + $field = $this->getOwner()?->getFieldLayout()?->getFieldById($this->fieldId); + } catch (RuntimeException) { + // carry on as we might still be able to get the field by ID + } + + if (! $field) { + $field = app(Fields::class)->getFieldById($this->fieldId); + } + + if (! $field instanceof ElementContainerFieldInterface) { + throw new RuntimeException("Invalid field ID: $this->fieldId"); + } + + return $field; + } + + public function getSortOrder(): ?int + { + return $this->sortOrder; + } + + public function setSortOrder(?int $sortOrder): void + { + $this->sortOrder = $sortOrder; + } + + public function setSaveOwnership(bool $saveOwnership): void + { + $this->saveOwnership = $saveOwnership; + } + + public function addInvalidNestedElementIds(array $ids): void + { + parent::addInvalidNestedElementIds($ids); + + if (isset($this->_owner)) { + $this->_owner->addInvalidNestedElementIds($ids); + } + } + + public function setEagerLoadedElements(string $handle, array $elements, EagerLoadPlan $plan): void + { + match ($plan->handle) { + 'owner' => $this->setOwner(reset($elements) ?: null), + 'primaryOwner' => $this->setPrimaryOwner(reset($elements) ?: null), + default => parent::setEagerLoadedElements($handle, $elements, $plan), + }; + } + + /** + * Returns the owner element’s type. + * + * @return class-string|null + */ + protected function ownerType(): ?string + { + if (isset($this->ownerType)) { + return $this->ownerType; + } + + if (! $ownerId = $this->getOwnerId()) { + return null; + } + + if (! $ownerType = Elements::getElementTypeById($ownerId)) { + return null; + } + + return $this->ownerType = $ownerType; + } + + /** + * Saves the element’s ownership data, if it belongs to a field + owner element + */ + private function saveOwnership(bool $isNew, string $elementTable, string $fieldIdColumn = 'fieldId'): void + { + if (! $this->saveOwnership || ! isset($this->fieldId) || $this->resaving) { + return; + } + + if (! $ownerId = $this->getOwnerId()) { + return; + } + + if (! isset($this->sortOrder) && (! $isNew || $this->duplicateOf)) { + // figure out if we should proceed this way + // if we're dealing with an element that's being duplicated, and it has a draftId + // it means we're creating a draft of something + // if we're duplicating element via duplicate action - draftId would be empty + $elementId = null; + + if ($this->duplicateOf) { + if ($this->draftId) { + $elementId = $this->duplicateOf->id; + } + } else { + // if we're not duplicating, use this element's id + $elementId = $this->id; + } + + if ($elementId) { + $this->sortOrder = DB::table(Table::ELEMENTS_OWNERS) + ->where('elementId', $elementId) + ->where('ownerId', $ownerId) + ->value('sortOrder') ?: null; + } + } + + if (! isset($this->sortOrder)) { + $max = DB::table(Table::ELEMENTS_OWNERS, 'eo') + ->join(new Alias($elementTable, 'e'), 'e.id', '=', 'eo.elementId') + ->where('eo.ownerId', $ownerId) + ->where("e.$fieldIdColumn", $this->fieldId) + ->max('eo.sortOrder'); + + $this->sortOrder = $max ? $max + 1 : 1; + } + + $ownerIds = array_unique([ + $this->getPrimaryOwnerId(), + $ownerId, + ]); + + if (! $isNew) { + DB::table(Table::ELEMENTS_OWNERS) + ->where('elementId', $this->id) + ->whereIn('ownerId', $ownerIds) + ->delete(); + } + + foreach ($ownerIds as $ownerId) { + DB::table(Table::ELEMENTS_OWNERS)->insert([ + 'elementId' => $this->id, + 'ownerId' => $ownerId, + 'sortOrder' => $this->sortOrder, + ]); + } + } +} diff --git a/src/Element/Concerns/Queryable.php b/src/Element/Concerns/Queryable.php index c85489e30c1..abae4810d7c 100644 --- a/src/Element/Concerns/Queryable.php +++ b/src/Element/Concerns/Queryable.php @@ -39,9 +39,6 @@ public static function findAll(mixed $criteria = null): array return static::findByCondition($criteria, false); } - /** - * @interitdoc - */ public static function get(int|string $id): ?static { return static::find() @@ -77,6 +74,7 @@ protected static function findByCondition(mixed $criteria, bool $one): array|sta if (! is_array($criteria) || Arr::isList($criteria)) { $criteria = ['id' => $criteria]; } + Typecast::configure($query, $criteria); } diff --git a/src/Element/Concerns/Renderable.php b/src/Element/Concerns/Renderable.php index 2625c3cdf69..fb965769af4 100644 --- a/src/Element/Concerns/Renderable.php +++ b/src/Element/Concerns/Renderable.php @@ -10,7 +10,7 @@ use CraftCms\Cms\Support\Html; use CraftCms\Cms\Twig\TemplateResolver; use CraftCms\Cms\View\TemplateMode; -use Twig\Markup; +use Illuminate\Support\HtmlString; use function CraftCms\Cms\template; @@ -24,7 +24,7 @@ */ trait Renderable { - public function render(array $variables = []): Markup + public function render(array $variables = []): HtmlString { $templates = $this->partialTemplatePathCandidates(); @@ -39,7 +39,7 @@ public function render(array $variables = []): Markup )); if ($event->output !== null) { - return new Markup($event->output, 'UTF-8'); + return new HtmlString($event->output); } $templates = $event->templates; @@ -53,22 +53,20 @@ public function render(array $variables = []): Markup $output = template($template['template'], $variables, templateMode: TemplateMode::Site); - return new Markup($output, 'UTF-8'); + return new HtmlString($output); } } // fallback to the string representation of the element $output = Html::tag('p', Html::encode((string) $this)); - return new Markup($output, 'UTF-8'); + return new HtmlString($output); } /** * Returns the template paths to check when rendering the element’s partial template. * * @return array{template:string,priority:int}[] - * - * @since 5.8.0 */ protected function partialTemplatePathCandidates(): array { diff --git a/src/Element/Concerns/Revisionable.php b/src/Element/Concerns/Revisionable.php index 96fbbe9fa99..332222aa35f 100644 --- a/src/Element/Concerns/Revisionable.php +++ b/src/Element/Concerns/Revisionable.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Concerns; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\Sites; use CraftCms\Cms\Support\Url; use CraftCms\Cms\User\Elements\User; @@ -25,8 +25,6 @@ trait Revisionable { /** * @var int|null The ID of the revision’s row in the `revisions` table - * - * @since 3.2.0 */ public ?int $revisionId = null; @@ -69,7 +67,7 @@ public function getRevisionCreator(): ?User $creator = User::find() ->id($this->revisionCreatorId) ->status(null) - ->one(); + ->first(); $this->revisionCreator = $creator ?? false; } diff --git a/src/Element/Concerns/Searchable.php b/src/Element/Concerns/Searchable.php index 3b55b42ea8f..c36653cf58e 100644 --- a/src/Element/Concerns/Searchable.php +++ b/src/Element/Concerns/Searchable.php @@ -29,8 +29,6 @@ trait Searchable * @var bool Whether the element's search keywords should be indexed immediately. * * If `null`, the search index will only be updated immediately for console requests. - * - * @since 5.8.0 */ public ?bool $updateSearchIndexImmediately = null; @@ -53,8 +51,6 @@ public function getSearchKeywords(string $attribute): string /** * Returns the search keywords for a given search attribute. - * - * @since 3.5.0 */ protected function searchKeywords(string $attribute): string { diff --git a/src/Element/Concerns/Structurable.php b/src/Element/Concerns/Structurable.php index a40b792b4f3..a67717bc8db 100644 --- a/src/Element/Concerns/Structurable.php +++ b/src/Element/Concerns/Structurable.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Concerns; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\Events\AfterMoveInStructure; @@ -358,7 +358,6 @@ public function isNextSiblingOf(ElementInterface $element): bool public function beforeMoveInStructure(int $structureId): bool { - // Fire a 'beforeMoveInStructure' event event($event = new BeforeMoveInStructure($this, $structureId)); return $event->isValid; @@ -366,10 +365,8 @@ public function beforeMoveInStructure(int $structureId): bool public function afterMoveInStructure(int $structureId): void { - // Fire an 'afterMoveInStructure' event event(new AfterMoveInStructure($this, $structureId)); - // Invalidate caches for this element ElementCaches::invalidateForElement($this); } @@ -382,8 +379,7 @@ private function _getRelativeElement(mixed $criteria, int $direction): ?ElementI if ($criteria instanceof ElementQueryInterface) { $query = clone $criteria; } else { - $query = static::find() - ->siteId($this->siteId); + $query = static::find()->siteId($this->siteId); if ($criteria) { Typecast::configure($query, $criteria); @@ -392,7 +388,7 @@ private function _getRelativeElement(mixed $criteria, int $direction): ?ElementI /** @var ElementQuery $query */ $elementIds = $query->cache()->ids(); - $key = array_search($this->getCanonicalId(), $elementIds, false); + $key = array_search($this->getCanonicalId(), $elementIds); if ($key === false || ! isset($elementIds[$key + $direction])) { return null; diff --git a/src/Element/Conditions/Contracts/ElementConditionInterface.php b/src/Element/Conditions/Contracts/ElementConditionInterface.php index 13881c39fdf..86a5ba96f8b 100644 --- a/src/Element/Conditions/Contracts/ElementConditionInterface.php +++ b/src/Element/Conditions/Contracts/ElementConditionInterface.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions\Contracts; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\Contracts\ConditionInterface; use CraftCms\Cms\Element\Conditions\ElementCondition; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\FieldLayout\FieldLayout; diff --git a/src/Element/Conditions/Contracts/ElementConditionRuleInterface.php b/src/Element/Conditions/Contracts/ElementConditionRuleInterface.php index 3e73cf666bb..9532666bbaa 100644 --- a/src/Element/Conditions/Contracts/ElementConditionRuleInterface.php +++ b/src/Element/Conditions/Contracts/ElementConditionRuleInterface.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Conditions\Contracts; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\Contracts\ConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; /** diff --git a/src/Element/Conditions/DateCreatedConditionRule.php b/src/Element/Conditions/DateCreatedConditionRule.php index 6fa2a3bce09..0711f690b09 100644 --- a/src/Element/Conditions/DateCreatedConditionRule.php +++ b/src/Element/Conditions/DateCreatedConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseDateRangeConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use function CraftCms\Cms\t; diff --git a/src/Element/Conditions/DateUpdatedConditionRule.php b/src/Element/Conditions/DateUpdatedConditionRule.php index ff64ab99fc9..e4e1b55d926 100644 --- a/src/Element/Conditions/DateUpdatedConditionRule.php +++ b/src/Element/Conditions/DateUpdatedConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseDateRangeConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use function CraftCms\Cms\t; diff --git a/src/Element/Conditions/ElementCondition.php b/src/Element/Conditions/ElementCondition.php index 63bfa56edbd..f18e6c761ff 100644 --- a/src/Element/Conditions/ElementCondition.php +++ b/src/Element/Conditions/ElementCondition.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseCondition; use CraftCms\Cms\Condition\Contracts\ConditionRuleInterface; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Exceptions\InvalidTypeException; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Field\Conditions\Contracts\FieldConditionRuleInterface; @@ -19,7 +19,7 @@ use CraftCms\Cms\Support\Facades\SiteGroups; use CraftCms\Cms\Support\Facades\Sites; use Override; -use yii\base\InvalidConfigException; +use RuntimeException; class ElementCondition extends BaseCondition implements ElementConditionInterface { @@ -82,7 +82,7 @@ public function __construct(?string $elementType = null, array $config = []) $elementType !== null && (! class_exists($elementType) || ! is_subclass_of($elementType, ElementInterface::class)) ) { - throw new InvalidConfigException("Invalid element type: $elementType"); + throw new RuntimeException("Invalid element type: $elementType"); } if ($elementType !== null) { diff --git a/src/Element/Conditions/HasDescendantsRule.php b/src/Element/Conditions/HasDescendantsRule.php index 4811a2e0ddf..224c05b3b9a 100644 --- a/src/Element/Conditions/HasDescendantsRule.php +++ b/src/Element/Conditions/HasDescendantsRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseLightswitchConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use function CraftCms\Cms\t; diff --git a/src/Element/Conditions/HasUrlConditionRule.php b/src/Element/Conditions/HasUrlConditionRule.php index 16a2981b905..22933e17fef 100644 --- a/src/Element/Conditions/HasUrlConditionRule.php +++ b/src/Element/Conditions/HasUrlConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseLightswitchConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use function CraftCms\Cms\t; diff --git a/src/Element/Conditions/IdConditionRule.php b/src/Element/Conditions/IdConditionRule.php index 9c55f7f5023..e49a4c18014 100644 --- a/src/Element/Conditions/IdConditionRule.php +++ b/src/Element/Conditions/IdConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseNumberConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use function CraftCms\Cms\t; diff --git a/src/Element/Conditions/LanguageConditionRule.php b/src/Element/Conditions/LanguageConditionRule.php index 26bca708558..afc13b20426 100644 --- a/src/Element/Conditions/LanguageConditionRule.php +++ b/src/Element/Conditions/LanguageConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Support\Facades\I18N; use CraftCms\Cms\Translation\Locale; diff --git a/src/Element/Conditions/LevelConditionRule.php b/src/Element/Conditions/LevelConditionRule.php index 2ec3f6fc6ce..7ab6c7439e7 100644 --- a/src/Element/Conditions/LevelConditionRule.php +++ b/src/Element/Conditions/LevelConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseNumberConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use function CraftCms\Cms\t; diff --git a/src/Element/Conditions/NotRelatedToConditionRule.php b/src/Element/Conditions/NotRelatedToConditionRule.php index 040d9622e9a..d7ed475f23e 100644 --- a/src/Element/Conditions/NotRelatedToConditionRule.php +++ b/src/Element/Conditions/NotRelatedToConditionRule.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use function CraftCms\Cms\t; diff --git a/src/Element/Conditions/RelatedToConditionRule.php b/src/Element/Conditions/RelatedToConditionRule.php index 9339b2fb1f4..d7ac474363b 100644 --- a/src/Element/Conditions/RelatedToConditionRule.php +++ b/src/Element/Conditions/RelatedToConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseElementSelectConditionRule; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Field\BaseRelationField; diff --git a/src/Element/Conditions/SiteConditionRule.php b/src/Element/Conditions/SiteConditionRule.php index 0f14b41d16d..1cbb711c2aa 100644 --- a/src/Element/Conditions/SiteConditionRule.php +++ b/src/Element/Conditions/SiteConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Site\Data\Site; use CraftCms\Cms\Support\Facades\Sites; diff --git a/src/Element/Conditions/SiteGroupConditionRule.php b/src/Element/Conditions/SiteGroupConditionRule.php index cc315d94213..7f9288bf749 100644 --- a/src/Element/Conditions/SiteGroupConditionRule.php +++ b/src/Element/Conditions/SiteGroupConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Site\Data\Site; use CraftCms\Cms\Site\Data\SiteGroup; diff --git a/src/Element/Conditions/SlugConditionRule.php b/src/Element/Conditions/SlugConditionRule.php index dd68a6a22c0..5d970fb3000 100644 --- a/src/Element/Conditions/SlugConditionRule.php +++ b/src/Element/Conditions/SlugConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Element/Conditions/StatusConditionRule.php b/src/Element/Conditions/StatusConditionRule.php index 622b938e73c..b55179cb546 100644 --- a/src/Element/Conditions/StatusConditionRule.php +++ b/src/Element/Conditions/StatusConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use function CraftCms\Cms\t; diff --git a/src/Element/Conditions/TitleConditionRule.php b/src/Element/Conditions/TitleConditionRule.php index cda779885a0..f72e2fefba2 100644 --- a/src/Element/Conditions/TitleConditionRule.php +++ b/src/Element/Conditions/TitleConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use function CraftCms\Cms\t; diff --git a/src/Element/Conditions/UriConditionRule.php b/src/Element/Conditions/UriConditionRule.php index 56be76ec783..ac0d7a21bca 100644 --- a/src/Element/Conditions/UriConditionRule.php +++ b/src/Element/Conditions/UriConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use function CraftCms\Cms\t; diff --git a/src/Element/Contracts/ElementActionInterface.php b/src/Element/Contracts/ElementActionInterface.php index 29529310b26..db54c13f439 100644 --- a/src/Element/Contracts/ElementActionInterface.php +++ b/src/Element/Contracts/ElementActionInterface.php @@ -4,7 +4,6 @@ namespace CraftCms\Cms\Element\Contracts; -use craft\base\ElementInterface; use CraftCms\Cms\Component\Contracts\ComponentInterface; use CraftCms\Cms\Component\Contracts\ConfigurableComponentInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Element/Contracts/ElementExporterInterface.php b/src/Element/Contracts/ElementExporterInterface.php index 4092c1962f9..9a412e60d40 100644 --- a/src/Element/Contracts/ElementExporterInterface.php +++ b/src/Element/Contracts/ElementExporterInterface.php @@ -4,7 +4,6 @@ namespace CraftCms\Cms\Element\Contracts; -use craft\base\ElementInterface; use CraftCms\Cms\Component\Contracts\ComponentInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/src/Element/Contracts/ElementInterface.php b/src/Element/Contracts/ElementInterface.php new file mode 100644 index 00000000000..200d0d1338a --- /dev/null +++ b/src/Element/Contracts/ElementInterface.php @@ -0,0 +1,1591 @@ +,source:int,target:int} + * @phpstan-type EagerLoadingMap array{elementType?:class-string,map:EagerLoadingMapItem[],criteria?:array,createElement?:callable} + */ +#[AllowedInSandbox] +interface ElementInterface extends Actionable, ArrayAccess, Chippable, ComponentInterface, CpEditable, IteratorAggregate, Statusable, Thumbable, Validatable +{ + /** + * Returns the lowercase version of [[displayName()]]. + */ + public static function lowerDisplayName(): string; + + /** + * Returns the plural version of [[displayName()]]. + */ + public static function pluralDisplayName(): string; + + /** + * Returns the plural, lowercase version of [[displayName()]]. + */ + public static function pluralLowerDisplayName(): string; + + /** + * Returns the handle that should be used to refer to this element type from reference tags. + * + * @return string|null The reference handle, or null if the element type doesn’t support reference tags + */ + public static function refHandle(): ?string; + + /** + * Returns whether element indexes should show the “Drafts” status option. + */ + public static function hasDrafts(): bool; + + /** + * Returns whether Craft should keep track of attribute and custom field changes made to this element type, + * including when the last time they were changed, and who was logged-in at the time. + * + * @return bool Whether to track changes made to elements of this type. + * + * @see getDirtyAttributes() + * @see getDirtyFields() + */ + public static function trackChanges(): bool; + + /** + * Returns whether elements of this type have traditional titles. + * + * @return bool Whether elements of this type have traditional titles. + */ + public static function hasTitles(): bool; + + /** + * Returns whether element indexes should include a thumbnail view by default. + */ + public static function hasThumbs(): bool; + + /** + * Returns whether elements of this type can have their own slugs and URIs. + * + * Note that individual elements must also return a URI format from [[getUriFormat()]] if they are to actually get a URI. + * + * @return bool Whether elements of this type can have their own slugs and URIs. + * + * @see getUriFormat() + */ + public static function hasUris(): bool; + + /** + * Returns whether elements of this type store content on a per-site basis. + * + * If this returns `true`, the element’s [[getSupportedSites()]] method will + * be responsible for defining which sites its content should be stored in. + * + * @return bool Whether elements of this type store data on a per-site basis. + */ + public static function isLocalized(): bool; + + /** + * Returns whether elements of this type have statuses. + * + * If this returns `true`, the element index template will show a Status menu by default, and your elements will + * get status indicator icons next to them. + * Use [[statuses()]] to customize which statuses the elements might have. + * + * @return bool Whether elements of this type have statuses. + * + * @see statuses() + */ + public static function hasStatuses(): bool; + + /** + * Creates an [[ElementQueryInterface]] instance for query purpose. + * + * The returned [[ElementQueryInterface]] instance can be further customized by calling + * methods defined in [[ElementQueryInterface]] before `one()` or `all()` is called to return + * populated [[ElementInterface]] instances. For example, + * + * ```php + * // Find the entry whose ID is 5 + * $entry = Entry::find()->id(5)->one(); + * // Find all assets and order them by their filename: + * $assets = Asset::find() + * ->orderBy('filename') + * ->all(); + * ``` + * + * If you want to define custom criteria parameters for your elements, you can do so by overriding + * this method and returning a custom query class. For example, + * + * ```php + * class Product extends Element + * { + * public static function find(): ElementQueryInterface|ElementQuery + * { + * // use ProductQuery instead of the default ElementQuery + * return new ProductQuery(get_called_class()); + * } + * } + * ``` + * + * You can also set default criteria parameters on the ElementQuery if you don’t have a need for + * a custom query class. For example, + * + * ```php + * class Customer extends ActiveRecord + * { + * public static function find(): ElementQueryInterface|ElementQuery + * { + * return parent::find()->limit(50); + * } + * } + * ``` + * + * @return ElementQueryInterface The newly created [[ElementQueryInterface]] instance. + */ + public static function find(): ElementQueryInterface; + + /** + * Returns a single element instance by a primary key or a set of element criteria parameters. + * + * The method accepts: + * + * - an int: query by a single ID value and return the corresponding element (or null if not found). + * - an array of name-value pairs: query by a set of parameter values and return the first element + * matching all of them (or null if not found). + * + * Note that this method will automatically call the `one()` method and return an + * [[ElementInterface|\craft\base\Element]] instance. For example, + * + * ```php + * // find a single entry whose ID is 10 + * $entry = Entry::findOne(10); + * // the above code is equivalent to: + * $entry = Entry::find->id(10)->one(); + * // find the first user whose email ends in "example.com" + * $user = User::findOne(['email' => '*example.com']); + * // the above code is equivalent to: + * $user = User::find()->email('*example.com')->one(); + * ``` + * + * @param mixed $criteria The element ID or a set of element criteria parameters + * @return static|null Element instance matching the condition, or null if nothing matches. + */ + public static function findOne(mixed $criteria = null): ?static; + + /** + * Returns a list of elements that match the specified ID(s) or a set of element criteria parameters. + * + * The method accepts: + * + * - an int: query by a single ID value and return an array containing the corresponding element + * (or an empty array if not found). + * - an array of integers: query by a list of ID values and return the corresponding elements (or an + * empty array if none was found). + * Note that an empty array will result in an empty result as it will be interpreted as a search for + * primary keys and not an empty set of element criteria parameters. + * - an array of name-value pairs: query by a set of parameter values and return an array of elements + * matching all of them (or an empty array if none was found). + * + * Note that this method will automatically call the `all()` method and return an array of + * [[ElementInterface|\craft\base\Element]] instances. For example, + * + * ```php + * // find the entries whose ID is 10 + * $entries = Entry::findAll(10); + * // the above code is equivalent to: + * $entries = Entry::find()->id(10)->all(); + * // find the entries whose ID is 10, 11 or 12. + * $entries = Entry::findAll([10, 11, 12]); + * // the above code is equivalent to: + * $entries = Entry::find()->id([10, 11, 12]])->all(); + * // find users whose email ends in "example.com" + * $users = User::findAll(['email' => '*example.com']); + * // the above code is equivalent to: + * $users = User::find()->email('*example.com')->all(); + * ``` + * + * @param mixed $criteria The element ID, an array of IDs, or a set of element criteria parameters + * @return static[] an array of Element instances, or an empty array if nothing matches. + */ + public static function findAll(mixed $criteria = null): array; + + /** + * Returns an element condition for the element type. + */ + public static function createCondition(): ElementConditionInterface; + + /** + * Returns whether the element type’s sources can be split into multiple pages. + */ + public static function multiPageSources(): bool; + + /** + * Returns the source definitions that elements of this type may belong to. + * + * This defines what will show up in the source list on element indexes and element selector modals. + * + * Each item in the array should be set to an array that has the following keys: + * - **`page`** – The source’s page label. (Optional) + * - **`key`** – The source’s key. This is the string that will be passed into the $source argument of [[actions()]], + * [[indexHtml()]], and [[defaultTableAttributes()]]. + * - **`label`** – The human-facing label of the source. + * - **`status`** – The status color that should be shown beside the source label. Possible values include `green`, + * `orange`, `red`, `yellow`, `pink`, `purple`, `blue`, `turquoise`, `light`, `grey`, `black`, and `white`. (Optional) + * - **`badgeCount`** – The badge count that should be displayed alongside the label. (Optional) + * - **`badgeLabel`** – The badge count label that should be provided for screen readers. (Optional) + * - **`sites`** – An array of site IDs or UUIDs that the source should be shown for, on multi-site element indexes. + * (Optional; by default the source will be shown for all sites.) + * - **`criteria`** – An array of element criteria parameters that the source should use when the source is selected. + * (Optional) + * - **`data`** – An array of `data-X` attributes that should be set on the source’s `` tag in the source list’s, + * HTML, where each key is the name of the attribute (without the “data-” prefix), and each value is the value of + * the attribute. (Optional) + * - **`defaultSort`** – A string identifying the sort attribute that should be selected by default, or an array where + * the first value identifies the sort attribute, and the second determines which direction to sort by. (Optional) + * - **`defaultFilter`** – An element condition instance or config, which should be used by default when the source + * is first selected. + * - **`hasThumbs`** – A bool that defines whether this source supports Thumbs View. (Use your element’s + * [[getThumbUrl()]] method to define your elements’ thumb URL.) (Optional) + * - **`structureId`** – The ID of the Structure that contains the elements in this source. If set, Structure View + * will be available to this source. (Optional) + * - **`newChildUrl`** – The URL that should be loaded when a user selects the “New child” menu option on an + * element in this source while it is in Structure View. (Optional) + * - **`nested`** – An array of sources that are nested within this one. Each nested source can have the same keys + * as top-level sources. + * + * ::: tip + * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::defineSources()]] + * instead of this method. + * ::: + * + * @param string $context The context ('index', 'modal', 'field', or 'settings'). + * @return array The sources. + */ + public static function sources(string $context): array; + + /** + * Returns a source definition by a given source key/path and context. + */ + public static function findSource(string $sourceKey, ?string $context): ?array; + + /** + * Returns the source path for a given source key, step key, and context. + * + * @return array[]|null + */ + public static function sourcePath(string $sourceKey, string $stepKey, ?string $context): ?array; + + /** + * Returns all the field layouts associated with elements from the given source. + * + * This is used to determine which custom fields should be included in the element index sort menu, + * and other things. + * + * @param string|null $source The selected source’s key, or `null` if all known field layouts should be returned + * @return FieldLayout[] + */ + public static function fieldLayouts(?string $source): array; + + /** + * Modifies a custom source’s config, before it’s returned by [[craft\services\ElementSources::getSources()]] + */ + public static function modifyCustomSource(array $config): array; + + /** + * Returns the available [bulk element actions](https://craftcms.com/docs/5.x/extend/element-actions.html) + * for a given source. + * + * The actions can be represented by their fully qualified class name, a config array with the class name + * set to a `type` key, or by an instantiated element action object. + * + * ::: tip + * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::defineActions()]] + * instead of this method. + * ::: + * + * @param string $source The selected source’s key. + * @return array The available bulk element actions. + * + * @phpstan-return array|array{type:class-string}> + */ + public static function actions(string $source): array; + + /** + * Returns the available export options for a given source. + * + * The exporters can be represented by their fully qualified class name, a config array with the class name + * set to a `type` key, or by an instantiated element exporter object. + * + * ::: tip + * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::defineExporters()]] + * instead of this method. + * ::: + * + * @param string $source The selected source’s key. + * @return array The available element exporters. + */ + public static function exporters(string $source): array; + + /** + * Defines which element attributes should be searchable. + * + * This method should return an array of attribute names that can be accessed on your elements. + * [[\craft\services\Search]] will call this method when it is indexing keywords for one of your elements, + * and for each attribute it returns, it will fetch the corresponding property’s value on the element. + * For example, if your elements have a “color” attribute which you want to be indexed, this method could return: + * + * ```php + * return ['color']; + * ``` + * + * Not only will the “color” attribute’s values start getting indexed, but users will also be able to search + * directly against that attribute’s values using this search syntax: + * + * color:blue + * + * There is no need for this method to worry about the ‘title’ or ‘slug’ attributes, or custom field handles; + * those are indexed automatically. + * + * ::: tip + * Element types that extend [[\craft\base\Element]] should override + * [[\craft\base\Element::defineSearchableAttributes()]] instead of this method. + * ::: + * + * @return string[] The element attributes that should be searchable + */ + public static function searchableAttributes(): array; + + /** + * Returns the base attributes that should be applied when bulk-duplicating elements of this type. + */ + public static function baseBulkDuplicateAttributes(): array; + + /** + * Returns the element index HTML. + * + * @param int[]|null $disabledElementIds + * @return string|Stringable The element index HTML + */ + public static function indexHtml( + ElementQueryInterface $elementQuery, + ?array $disabledElementIds, + array $viewState, + ?string $sourceKey, + ?string $context, + bool $includeContainer, + bool $selectable, + bool $sortable, + ): string|Stringable; + + /** + * Returns the total number of elements that will be shown on an element index, for the given element query. + */ + public static function indexElementCount(ElementQueryInterface $elementQuery, ?string $sourceKey): int; + + /** + * Returns the sort options for the element type. + * + * This method should return an array, where each item is a sub-array with the following keys: + * + * - `label` – The sort option label + * - `orderBy` – An array, comma-delimited string, or a callback function that defines the columns to order the query by. If set to a callback + * function, the function will be passed two arguments: `$dir` (either `SORT_ASC` or `SORT_DESC`) and `$db` (a [[\craft\db\Connection]] object), + * and it should return an array of column names or an [[\yii\db\ExpressionInterface]] object. + * - `attribute` _(optional)_ – The [[tableAttributes()|table attribute]] name that this option is associated + * with (required if `orderBy` is an array or more than one column name) + * - `defaultDir` _(optional)_ – The default sort direction that should be used when sorting by this option + * (set to either `asc` or `desc`). Defaults to `asc` if not specified. + * + * ```php + * return [ + * [ + * 'label' => \CraftCms\Cms\t('Attribute Label'), + * 'orderBy' => 'columnName', + * 'attribute' => 'attributeName', + * 'defaultDir' => 'asc', + * ], + * ]; + * ``` + * + * A shorthand syntax is also supported, if there is no corresponding table attribute, or the table attribute + * has the exact same name as the column. + * + * ```php + * return [ + * 'columnName' => \CraftCms\Cms\t('Attribute Label'), + * ]; + * ``` + * + * Note that this method will only get called once for the entire index; not each time that a new source is + * selected. + * + * @return array The attributes that elements can be sorted by + */ + public static function sortOptions(): array; + + /** + * Returns the view modes available for the element type. + * + * This method should return an array, where each item is a sub-array with the following keys: + * + * - `mode` – Name of the view mode + * - `title` – How this mode should be described to the user + * - `icon` – Icon representing this view mode + * - `availableOnMobile` - Whether the view mode is available on mobile devices (defaults to `true`) + * - `structuresOnly` – Whether the view mode should only be available for structured sources (defaults to `false`) + * + * ```php + * return [ + * [ + * 'mode' => 'table', + * 'title' => \CraftCms\Cms\t('Display in a table'), + * 'icon' => 'list', + * 'availableOnMobile' => false, + * ], + * ]; + * ``` + * + * @return array The view modes. + */ + public static function indexViewModes(): array; + + /** + * Defines all of the available columns that can be shown in table views. + * + * This method should return an array whose keys represent element attribute names, and whose values make + * up the table’s column headers. + * + * @return array The table attributes. + */ + public static function tableAttributes(): array; + + /** + * Returns the list of table attribute keys that should be shown by default. + * + * This method should return an array where each element in the array maps to one of the keys of the array returned + * by [[tableAttributes()]]. + * + * @param string $source The selected source’s key + * @return string[] The table attribute keys + */ + public static function defaultTableAttributes(string $source): array; + + /** + * Defines all the available attributes that can be shown in card views. + * + * This method should return an array whose keys represent element attribute names, and whose values make + * up the table’s column headers. + * + * @return array The card attributes. + */ + public static function cardAttributes(?FieldLayout $fieldLayout = null): array; + + /** + * Returns the list of card attribute keys that should be shown by default, if the field layout hasn't been customised. + * + * This method should return an array where each element in the array maps to one of the keys of the array returned + * by [[cardAttributes()]]. + * + * @return string[] The card attribute keys + */ + public static function defaultCardAttributes(): array; + + /** + * Return HTML for the attribute in the card preview. + */ + public static function attributePreviewHtml(array $attribute): mixed; + + /** + * Returns an array that maps source-to-target element IDs based on the given sub-property handle. + * + * This method aids in the eager-loading of elements when performing an element query. The returned array should + * contain the following keys: + * - `map` – an array defining source-target element mappings + * - `elementType` *(optional)* – the fully qualified class name of the element type that should be eager-loaded, + * if each target element is of the same element type + * - `criteria` *(optional)* – any criteria parameters that should be applied to the element query when fetching the + * eager-loaded elements + * - `createElement` *(optional)* - an element factory function, which will be passed the element query, the current + * query result data, and the first source element that the result was eager-loaded for + * + * Each mapping listed in `map` should be an array with the following keys: + * - `source` – the source element ID + * - `target` – the target element ID + * - `elementType` *(optional)* – the target element type (only checked for if the top-level array doesn’t specify + * an `elementType` key) + * + * ```php + * use CraftCms\Cms\Element\Contracts\ElementInterface; + * use craft\db\Query; + * + * public static function eagerLoadingMap(array $sourceElements, string $handle) + * { + * switch ($handle) { + * case 'author': + * $bookIds = array_map(fn(ElementInterface $element) => $element->id, $sourceElements); + * $map = (new Query) + * ->select(['source' => 'id', 'target' => 'authorId']) + * ->from('{{%books}}') + * ->where(['id' => $bookIds) + * ->all(); + * return [ + * 'elementType' => \my\plugin\Author::class, + * 'map' => $map, + * ]; + * case 'bookClubs': + * $bookIds = array_map(fn(ElementInterface $element) => $element->id, $sourceElements); + * $map = (new Query) + * ->select(['source' => 'bookId', 'target' => 'clubId']) + * ->from('{{%bookclub_books}}') + * ->where(['bookId' => $bookIds) + * ->all(); + * return [ + * 'elementType' => \my\plugin\BookClub::class, + * 'map' => $map, + * ]; + * default: + * return parent::eagerLoadMap($sourceElements, $handle); + * } + * } + * ``` + * + * Alternatively, the method can return an array of multiple sets of mappings, each with their own nested `map`, + * `elementType`, `criteria`, and `createElement` keys. + * + * @param self[] $sourceElements An array of the source elements + * @param string $handle The property handle used to identify which target elements should be included in the map + * @return EagerLoadingMap|EagerLoadingMap[]|null|false The eager-loading element ID mappings, false if no mappings + * exist, or null if the result should be ignored + */ + public static function eagerLoadingMap(array $sourceElements, string $handle): array|null|false; + + /** + * Returns the base GraphQL type name that represents elements of this type. + */ + public static function baseGqlType(): Type; + + /** + * Returns the GraphQL scopes required by element’s context. + * + * @param mixed $context The element’s context, such as a volume, entry type or Matrix block type. + */ + public static function gqlScopesByContext(mixed $context): array; + + /** + * Returns whether this is a draft. + */ + public function getIsDraft(): bool; + + /** + * Returns whether this is a revision. + */ + public function getIsRevision(): bool; + + /** + * Returns whether this is the canonical element. + */ + public function getIsCanonical(): bool; + + /** + * Returns whether this is a derivative element, such as a draft or revision. + */ + public function getIsDerivative(): bool; + + /** + * Returns the canonical version of the element. + * + * If this is a draft or revision, the canonical element will be returned. + * + * @param bool $anySite Whether the canonical element can be retrieved in any site + */ + public function getCanonical(bool $anySite = false): self; + + /** + * Sets the canonical version of the element. + */ + public function setCanonical(self $element): void; + + /** + * Returns the element’s canonical ID. + * + * If this is a draft or revision, the canonical element’s ID will be returned. + */ + public function getCanonicalId(): ?int; + + /** + * Sets the element’s canonical ID. + */ + public function setCanonicalId(?int $canonicalId): void; + + /** + * Returns the element’s canonical UUID. + * + * If this is a draft or revision, the canonical element’s UUID will be returned. + */ + public function getCanonicalUid(): ?string; + + /** + * Returns whether the element is an unpublished draft. + */ + public function getIsUnpublishedDraft(): bool; + + /** + * Merges changes from the canonical element into this one. + * + * @see Elements::mergeCanonicalChanges() + */ + public function mergeCanonicalChanges(): void; + + /** + * Returns the field layout used by this element. + */ + public function getFieldLayout(): ?FieldLayout; + + /** + * Returns the site the element is associated with. + */ + public function getSite(): Site; + + /** + * Returns the language of the element. + */ + public function getLanguage(): string; + + /** + * Returns the sites this element is associated with. + * + * The function can either return an array of site IDs, or an array of sub-arrays, + * each with the following keys: + * + * - `siteId` (integer) - The site ID + * - `propagate` (boolean) – Whether the element should be propagated to this site on save (`true` by default) + * - `enabledByDefault` (boolean) – Whether the element should be enabled in this site by default + * (`true` by default) + */ + public function getSupportedSites(): array; + + /** + * Returns the URI format used to generate this element’s URI. + * + * Note that element types that can have URIs must return `true` from [[hasUris()]]. + * + * @see hasUris() + * @see getRoute() + */ + public function getUriFormat(): ?string; + + /** + * Returns the search keywords for a given search attribute. + */ + public function getSearchKeywords(string $attribute): string; + + /** + * Returns the route that should be used when the element’s URI is requested. + * + * ::: tip + * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::route()]] + * instead of this method. + * ::: + * + * @return mixed The route that the request should use, or null if no special action should be taken + */ + public function getRoute(): mixed; + + /** + * Returns whether this element represents the site homepage. + */ + public function getIsHomepage(): bool; + + /** + * Returns the element’s full URL. + */ + public function getUrl(): ?string; + + /** + * Returns an anchor pre-filled with this element’s URL and title. + */ + public function getLink(): ?HtmlString; + + /** + * Returns the breadcrumbs that lead up to the element. + */ + public function getCrumbs(): array; + + /** + * Defines what the element should be called within the control panel. + */ + public function setUiLabel(?string $label): void; + + /** + * Returns any path segment labels that should be prepended to the element’s UI label. + * + * @return string[] + */ + public function getUiLabelPath(): array; + + /** + * Defines any path segment labels that should be prepended to the element’s UI label. + * + * @param string[] $path + */ + public function setUiLabelPath(array $path): void; + + /** + * Returns the label HTML for element chips. + */ + public function getChipLabelHtml(): string|Stringable; + + /** + * Returns whether chips and cards for this element should include a status indicator. + */ + public function showStatusIndicator(): bool; + + /** + * Returns the titlebar label for element cards. + */ + public function getCardTitle(): ?string; + + /** + * Returns the body HTML for element cards. + */ + public function getCardBodyHtml(): ?string; + + /** + * Returns the reference string to this element. + */ + public function getRef(): ?string; + + /** + * Creates a new element (without saving it) based on this one. + * + * This will be called by the “Save and add another” action on the element’s edit page. + * + * Note that permissions don’t need to be considered here. The created element’s [[canSave()]] method will be called before saving. + */ + public function createAnother(): ?self; + + /** + * Returns whether the given user is authorized to view this element’s edit page. + * + * If they can view but not [[canSave()|save]], the edit form will either render statically, + * or be restricted to only saving changes as a draft, depending on [[canCreateDrafts()]]. + */ + public function canView(User $user): bool; + + /** + * Returns whether the given user is authorized to save this element in its current form. + * + * This will only be called if the element can be [[canView()|viewed]]. + */ + public function canSave(User $user): bool; + + /** + * Returns whether the given user is authorized to duplicate this element. + * + * This will only be called if the element can be [[canView()|viewed]] and/or [[canSave()|saved]]. + */ + public function canDuplicate(User $user): bool; + + /** + * Returns whether the given user is authorized to duplicate this element as an unpublished draft. + */ + public function canDuplicateAsDraft(User $user): bool; + + /** + * Returns whether the given user is authorized to copy this element, to be duplicated elsewhere. + */ + public function canCopy(User $user): bool; + + /** + * Returns whether the given user is authorized to delete this element. + * + * This will only be called if the element can be [[canView()|viewed]] and/or [[canSave()|saved]]. + */ + public function canDelete(User $user): bool; + + /** + * Returns whether the given user is authorized to delete this element for its current site. + * + * This will only be called if the element can be [[canView()|viewed]] and/or [[canSave()|saved]]. + */ + public function canDeleteForSite(User $user): bool; + + /** + * Returns whether the given user is authorized to create drafts for this element. + * + * This will only be called if the element can be [[canView()|viewed]] and/or [[canSave()|saved]]. + * + * ::: tip + * If this is going to return `true` under any circumstances, make sure [[trackChanges()]] is returning `true`, + * so drafts can be automatically updated with upstream content changes. + * ::: + */ + public function canCreateDrafts(User $user): bool; + + /** + * Returns whether revisions should be created when this element is saved. + */ + public function hasRevisions(): bool; + + /** + * Prepares the response for the element’s Edit screen. + * + * @param Response $response The response being prepared + * @param string $containerId The ID of the element editor’s container element + */ + public function prepareEditScreen(Response|CpScreenResponse $response, string $containerId): void; + + /** + * Returns the URL that users should be redirected to after editing the element. + */ + public function getPostEditUrl(): ?string; + + /** + * Returns the element’s revisions index URL in the control panel. + */ + public function getCpRevisionsUrl(): ?string; + + /** + * Returns additional buttons that should be shown at the top of the element’s edit page. + */ + public function getAdditionalButtons(): string|Stringable; + + /** + * Returns alternative form actions for the element. + * + * {@see CpScreenResponse::altActions()} for documentation on supported action properties. + */ + public function getAltActions(): array; + + /** + * Returns the additional locations that should be available for previewing the element, besides its primary [[getUrl()|URL]]. + * + * Each target should be represented by a sub-array with the following keys: + * + * - `label` – What the preview target will be called in the control panel. + * - `url` – The URL that the preview target should open. + * - `refresh` – Whether preview frames should be automatically refreshed when content changes (`true` by default). + * + * ::: tip + * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::previewTargets()]] + * instead of this method. + * ::: + */ + public function getPreviewTargets(): array; + + /** + * Returns whether the element is enabled for the current site. + * + * This can also be set to an array of site ID/site-enabled mappings. + * + * @param int|null $siteId The ID of the site to return for. If `null`, the current site status will be returned. + * @return bool|null Whether the element is enabled for the given site. `null` will be returned if a `$siteId` was + * passed, but that site’s status wasn’t provided via [[setEnabledForSite()]]. + */ + public function getEnabledForSite(?int $siteId = null): ?bool; + + /** + * Sets whether the element is enabled for the current site. + * + * This can also be set to an array of site ID/site-enabled mappings. + * + * @param bool|bool[] $enabledForSite + */ + public function setEnabledForSite(array|bool $enabledForSite): void; + + /** + * Returns the root owner element. + */ + public function getRootOwner(): self; + + /** + * Returns the same element in other locales. + */ + public function getLocalized(): ElementQueryInterface|ElementCollection; + + /** + * Returns a query for the same element in other locales. + */ + public function getLocalizedQuery(): ElementQueryInterface; + + /** + * Returns the next element relative to this one, from a given set of criteria. + */ + public function getNext(mixed $criteria = false): ?self; + + /** + * Returns the previous element relative to this one, from a given set of criteria. + */ + public function getPrev(mixed $criteria = false): ?self; + + /** + * Sets the default next element. + */ + public function setNext(self|false $element): void; + + /** + * Sets the default previous element. + */ + public function setPrev(self|false $element): void; + + /** + * Returns the element’s parent. + */ + public function getParent(): ?self; + + /** + * Returns the parent element’s URI, if there is one. + * + * If the parent’s URI is `__home__` (the homepage URI), then `null` will be returned. + */ + public function getParentUri(): ?string; + + /** + * Sets the element’s parent. + */ + public function setParent(?self $parent): void; + + /** + * Returns the element’s ancestors. + */ + public function getAncestors(?int $dist = null): ElementQueryInterface|ElementCollection; + + /** + * Returns the element’s descendants. + */ + public function getDescendants(?int $dist = null): ElementQueryInterface|ElementCollection; + + /** + * Returns the element’s children. + */ + public function getChildren(): ElementQueryInterface|ElementCollection; + + /** + * Returns all of the element’s siblings. + */ + public function getSiblings(): ElementQueryInterface|ElementCollection; + + /** + * Returns the element’s previous sibling. + */ + public function getPrevSibling(): ?self; + + /** + * Returns the element’s next sibling. + */ + public function getNextSibling(): ?self; + + /** + * Returns whether the element has descendants. + */ + public function getHasDescendants(): bool; + + /** + * Returns the total number of descendants that the element has. + */ + public function getTotalDescendants(): int; + + /** + * Returns whether this element is an ancestor of another one. + */ + public function isAncestorOf(self $element): bool; + + /** + * Returns whether this element is a descendant of another one. + */ + public function isDescendantOf(self $element): bool; + + /** + * Returns whether this element is a direct parent of another one. + */ + public function isParentOf(self $element): bool; + + /** + * Returns whether this element is a direct child of another one. + */ + public function isChildOf(self $element): bool; + + /** + * Returns whether this element is a sibling of another one. + */ + public function isSiblingOf(self $element): bool; + + /** + * Returns whether this element is the direct previous sibling of another one. + */ + public function isPrevSiblingOf(self $element): bool; + + /** + * Returns whether this element is the direct next sibling of another one. + */ + public function isNextSiblingOf(self $element): bool; + + /** + * Treats custom fields as array offsets. + * + * @param string|int $offset + */ + public function offsetExists($offset): bool; + + /** + * Sets the element’s attributes from an element editor submission. + * + * @param array $values The attribute values + */ + public function setAttributesFromRequest(array $values): void; + + /** + * Returns the status of a given attribute. + * + * @return array{0:AttributeStatus|value-of,1:string}|null + */ + public function getAttributeStatus(string $attribute): ?array; + + /** + * Returns the attribute names that have been updated on the canonical element since the last time it was + * merged into this element. + * + * @return string[] + */ + public function getOutdatedAttributes(): array; + + /** + * Returns whether an attribute value has fallen behind the canonical element’s value. + */ + public function isAttributeOutdated(string $name): bool; + + /** + * Returns the attribute names that have changed for this element. + * + * @return string[] + */ + public function getModifiedAttributes(): array; + + /** + * Returns whether an attribute value has changed for this element. + */ + public function isAttributeModified(string $name): bool; + + /** + * Returns whether an attribute has changed since the element was first loaded. + */ + public function isAttributeDirty(string $name): bool; + + /** + * Returns a list of attribute names that have changed since the element was first loaded. + * + * @return string[] + */ + public function getDirtyAttributes(): array; + + /** + * Sets the list of dirty attribute names. + * + * @param string[] $names + * @param bool $merge Whether these attributes should be merged with existing dirty attributes + * + * @see getDirtyAttributes() + */ + public function setDirtyAttributes(array $names, bool $merge = true): void; + + /** + * Returns whether the Title field should be shown as translatable in the UI. + * + * Note this method has no effect on whether titles will get copied over to other + * sites when the element is actually getting saved. That is determined by [[getTitleTranslationKey()]]. + */ + public function getIsTitleTranslatable(): bool; + + /** + * Returns the description of the Title field’s translation support. + */ + public function getTitleTranslationDescription(): ?string; + + /** + * Returns the Title’s translation key. + * + * When saving an element on a multi-site Craft install, if `$propagate` is `true` for [[\craft\services\Elements::saveElement()]], + * then `getTitleTranslationKey()` will be called for each site the element should be propagated to. + * If the method returns the same value as it did for the initial site, then the initial site’s title will be copied over + * to the target site. + * + * @return string The translation key + */ + public function getTitleTranslationKey(): string; + + /** + * Returns whether the Slug field should be shown as translatable in the UI. + * + * Note this method has no effect on whether slugs will get copied over to other + * sites when the element is actually getting saved. That is determined by [[getSlugTranslationKey()]]. + */ + public function getIsSlugTranslatable(): bool; + + /** + * Returns the description of the Slug field’s translation support. + */ + public function getSlugTranslationDescription(): ?string; + + /** + * Returns the Slug’s translation key. + * + * When saving an element on a multi-site Craft install, if `$propagate` is `true` for [[\craft\services\Elements::saveElement()]], + * then `getSlugTranslationKey()` will be called for each site the element should be propagated to. + * If the method returns the same value as it did for the initial site, then the initial site’s slug will be copied over + * to the target site. + * + * @return string The translation key + */ + public function getSlugTranslationKey(): string; + + /** + * Returns whether a field is empty. + */ + public function isFieldEmpty(string $handle): bool; + + /** + * Returns the element’s normalized custom field values, indexed by their handles. + * + * @param string[]|null $fieldHandles The list of field handles whose values + * need to be returned. Defaults to null, meaning all fields’ values will be + * returned. If it is an array, only the fields in the array will be returned. + * @return array The field values (handle => value) + */ + public function getFieldValues(?array $fieldHandles = null): array; + + /** + * Returns an array of the element’s serialized custom field values, indexed by their handles. + * + * @param string[]|null $fieldHandles The list of field handles whose values + * need to be returned. Defaults to null, meaning all fields’ values will be + * returned. If it is an array, only the fields in the array will be returned. + */ + public function getSerializedFieldValues(?array $fieldHandles = null): array; + + /** + * Returns an array of the element’s serialized custom field values, indexed by their handles, + * for database storage. + * + * @param string[]|null $fieldHandles The list of field handles whose values + * need to be returned. Defaults to null, meaning all fields’ values will be + * returned. If it is an array, only the fields in the array will be returned. + */ + public function getSerializedFieldValuesForDb(?array $fieldHandles = null): array; + + /** + * Sets the element’s custom field values. + * + * @param array $values The custom field values (handle => value) + */ + public function setFieldValues(array $values): void; + + /** + * Returns the value for a given field. + * + * @param string $fieldHandle The field handle whose value needs to be returned + * @return mixed The field value + * + * @throws InvalidFieldException if the element doesn’t have a field with the handle specified by `$fieldHandle` + */ + public function getFieldValue(string $fieldHandle): mixed; + + /** + * Sets the value for a given field. + * + * @param string $fieldHandle The field handle whose value needs to be set + * @param mixed $value The value to set on the field + */ + public function setFieldValue(string $fieldHandle, mixed $value): void; + + /** + * Sets the value for a given field. The value should have originated from post data. + * + * @param string $fieldHandle The field handle whose value needs to be set + * @param mixed $value The value to set on the field + * + * @throws InvalidFieldException if `$fieldHandle` is an invalid field handle + */ + public function setFieldValueFromRequest(string $fieldHandle, mixed $value): void; + + /** + * Returns the field handles that have been updated on the canonical element since the last time it was + * merged into this element. + * + * @return string[] + */ + public function getOutdatedFields(): array; + + /** + * Returns whether a field value has fallen behind the canonical element’s value. + */ + public function isFieldOutdated(string $fieldHandle): bool; + + /** + * Returns the field handles that have changed for this element. + * + * @param bool $anySite Whether to check for fields that have changed across any site + * @return string[] + */ + public function getModifiedFields(bool $anySite = false): array; + + /** + * Returns whether a field value has changed for this element. + * + * @param bool $anySite Whether to check if the field has changed across any site + */ + public function isFieldModified(string $fieldHandle, bool $anySite = false): bool; + + /** + * Returns whether a custom field value has changed since the element was first loaded. + */ + public function isFieldDirty(string $fieldHandle): bool; + + /** + * Returns a list of custom field handles that have changed since the element was first loaded. + * + * @return string[] + */ + public function getDirtyFields(): array; + + /** + * Sets the list of dirty field handles. + * + * @param string[] $fieldHandles + * @param bool $merge Whether these fields should be merged with existing dirty fields + * + * @see getDirtyFields() + */ + public function setDirtyFields(array $fieldHandles, bool $merge = true): void; + + /** + * Marks all fields and attributes as dirty. + */ + public function markAsDirty(): void; + + /** + * Resets the record of dirty attributes and fields. + */ + public function markAsClean(): void; + + /** + * Returns the cache tags that should be cleared when this element is saved. + * + * @return string[] + */ + public function getCacheTags(): array; + + /** + * Sets the element’s custom field values, when the values have come from post data. + * + * @param string $paramNamespace The field param namespace + */ + public function setFieldValuesFromRequest(string $paramNamespace): void; + + /** + * Returns the namespace used by custom field params on the request. + * + * @return string|null The field param namespace + */ + public function getFieldParamNamespace(): ?string; + + /** + * Sets the namespace used by custom field params on the request. + * + * @param string $namespace The field param namespace + */ + public function setFieldParamNamespace(string $namespace): void; + + /** + * Returns the field context this element’s content uses. + */ + public function getFieldContext(): string; + + /** + * Returns the generated field values for the element, indexed by handle. + * + * @return array + */ + public function getGeneratedFieldValues(): array; + + /** + * Sets the generated field values for the element, indexed by handle. + * + * @param array $values + */ + public function setGeneratedFieldValues(array $values): void; + + /** + * Returns the element’s invalid nested element IDs. + * + * @return int[] + */ + public function getInvalidNestedElementIds(): array; + + /** + * Registers invalid nested element IDs with the element, so an `error` class can be added on their cards. + * + * @param int[] $ids + */ + public function addInvalidNestedElementIds(array $ids): void; + + /** + * Returns whether elements have been eager-loaded with a given handle. + * + * @param string $handle The handle of the eager-loaded elements + * @return bool Whether elements have been eager-loaded with the given handle + */ + public function hasEagerLoadedElements(string $handle): bool; + + /** + * Returns the eager-loaded elements for a given handle. + * + * @param string $handle The handle of the eager-loaded elements + * @return ElementCollection|null The eager-loaded elements, or null if they hadn't been eager-loaded + */ + public function getEagerLoadedElements(string $handle): ?ElementCollection; + + /** + * Sets some eager-loaded elements on a given handle. + * + * @param string $handle The handle that was used to eager-load the elements + * @param self[] $elements The eager-loaded elements + * @param EagerLoadPlan $plan The eager-loading plan + */ + public function setEagerLoadedElements(string $handle, array $elements, EagerLoadPlan $plan): void; + + /** + * Sets whether the given eager-loaded element handles were eager-loaded lazily. + * + * @param string $handle The handle that was used to eager-load the elements + */ + public function setLazyEagerLoadedElements(string $handle, bool $value = true): void; + + /** + * Returns the count of eager-loaded elements for a given handle. + * + * @param string $handle The handle of the eager-loaded elements + * @return int|null The eager-loaded element count, or null if it hadn't been eager-loaded + */ + public function getEagerLoadedElementCount(string $handle): ?int; + + /** + * Sets the count of eager-loaded elements for a given handle. + * + * @param string $handle The handle to load the elements with in the future + * @param int $count The eager-loaded element count + */ + public function setEagerLoadedElementCount(string $handle, int $count): void; + + /** + * Returns whether the element is "fresh" (not yet explicitly saved, and without validation errors). + */ + public function getIsFresh(): bool; + + /** + * Sets whether the element is "fresh" (not yet explicitly saved, and without validation errors). + */ + public function setIsFresh(bool $isFresh = true): void; + + /** + * Sets the revision creator ID to be saved. + */ + public function setRevisionCreatorId(?int $creatorId): void; + + /** + * Sets the revision notes to be saved. + */ + public function setRevisionNotes(?string $notes): void; + + /** + * Returns the element’s current revision, if one exists. + */ + public function getCurrentRevision(): ?self; + + /** + * Return if the element is copyable between sites. + * Checks if it's a multisite installation, if user can edit the element in other sites, + * and if the element actually exists in other sites. + */ + public function getIsCrossSiteCopyable(): bool; + + // Indexes, etc. + // ------------------------------------------------------------------------- + /** + * Returns any attributes that should be included in the element’s chips and cards. + * + * The attribute HTML will be rendered with [[\yii\helpers\BaseHtml::renderTagAttributes()]]. + * + * ::: tip + * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::htmlAttributes()]] + * instead of this method. + * ::: + * + * @param string $context The context that the element is being rendered in ('index', 'modal', 'field', or 'settings'.) + */ + public function getHtmlAttributes(string $context): array; + + /** + * Returns the HTML that should be shown for a given attribute in table and card views. + * + * ::: tip + * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::attributeHtml()]] + * instead of this method. + * ::: + * + * @param string $attribute The attribute name. + * @return string|Stringable The HTML that should be shown for a given attribute in table and card views. + */ + public function getAttributeHtml(string $attribute): string|Stringable; + + /** + * Returns the HTML that should be shown for a given attribute's inline editing input. + * + * @param string $attribute The attribute name. + * @return string|Stringable The HTML that should be shown for the element input. + */ + public function getInlineAttributeInputHtml(string $attribute): string|Stringable; + + /** + * Returns the HTML for any fields/info that should be shown within the editor sidebar. + * + * @param bool $static Whether any fields within the sidebar should be static (non-interactive) + */ + public function getSidebarHtml(bool $static): string|Stringable; + + /** + * Returns element metadata that should be shown within the editor sidebar. + * + * @return array The data, with keys representing the labels. The values can either be strings or callables. + * If a value is `false`, it will be omitted. + */ + public function getMetadata(): array; + + /** + * Returns the GraphQL type name for this element type. + */ + public function getGqlTypeName(): string; + + // Events + // ------------------------------------------------------------------------- + + /** + * Performs actions before an element is saved. + * + * @param bool $isNew Whether the element is brand new + * @return bool Whether the element should be saved + */ + public function beforeSave(bool $isNew): bool; + + /** + * Performs actions after an element is saved. + * + * @param bool $isNew Whether the element is brand new + */ + public function afterSave(bool $isNew): void; + + /** + * Performs actions after an element is fully saved and propagated to other sites. + * + * ::: tip + * This will get called regardless of whether `$propagate` is `true` or `false` for [[\craft\services\Elements::saveElement()]]. + * ::: + * + * @param bool $isNew Whether the element is brand new + */ + public function afterPropagate(bool $isNew): void; + + /** + * Performs actions before an element is deleted. + * + * @return bool Whether the element should be deleted + */ + public function beforeDelete(): bool; + + /** + * Performs actions after an element is deleted. + */ + public function afterDelete(): void; + + /** + * Performs actions before an element is deleted for a site. + * + * @return bool Whether the element should be deleted + */ + public function beforeDeleteForSite(): bool; + + /** + * Performs actions after an element is deleted for a site. + */ + public function afterDeleteForSite(): void; + + /** + * Performs actions before an element is restored. + * + * @return bool Whether the element should be restored + */ + public function beforeRestore(): bool; + + /** + * Performs actions after an element is restored. + */ + public function afterRestore(): void; + + /** + * Performs actions before an element is moved within a structure. + * + * @param int $structureId The structure ID + * @return bool Whether the element should be moved within the structure + */ + public function beforeMoveInStructure(int $structureId): bool; + + /** + * Performs actions after an element is moved within a structure. + * + * @param int $structureId The structure ID + */ + public function afterMoveInStructure(int $structureId): void; + + /** + * Returns the string representation of the element. + */ + public function __toString(): string; + + /** + * Renders the element using its partial template. + * + * If no partial template exists for the element, its string representation will be output instead. + * + * @throws RuntimeException + */ + public function render(array $variables = []): HtmlString; +} diff --git a/src/Element/Contracts/NestedElementInterface.php b/src/Element/Contracts/NestedElementInterface.php new file mode 100644 index 00000000000..5fcbdcbba6e --- /dev/null +++ b/src/Element/Contracts/NestedElementInterface.php @@ -0,0 +1,97 @@ +active = true; + $this->query = $query; + } + + public function isActive(): bool + { + return $this->active; + } + + public function query(): ?ElementQueryInterface + { + return $this->query; + } + + public function reset(): void + { + $this->active = false; + $this->query = null; + } +} diff --git a/src/Element/Data/EagerLoadInfo.php b/src/Element/Data/EagerLoadInfo.php index 57d60627027..e514ebe294e 100644 --- a/src/Element/Data/EagerLoadInfo.php +++ b/src/Element/Data/EagerLoadInfo.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Data; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class EagerLoadInfo { diff --git a/src/Element/Data/EagerLoadPlan.php b/src/Element/Data/EagerLoadPlan.php index 798271d6be9..5d24354943f 100644 --- a/src/Element/Data/EagerLoadPlan.php +++ b/src/Element/Data/EagerLoadPlan.php @@ -16,7 +16,7 @@ public function __construct( /** * @var callable|null A PHP callable whose return value determines whether to apply eager-loaded elements to the given element. * - * The signature of the callable should be `function (\craft\base\ElementInterface $element): bool`, where `$element` refers to the element + * The signature of the callable should be `function (\CraftCms\Cms\Element\Contracts\ElementInterface $element): bool`, where `$element` refers to the element * the eager-loaded elements are about to be applied to. The callable should return a boolean value. */ public $when = null, diff --git a/src/Element/Data/ElementActivity.php b/src/Element/Data/ElementActivity.php index 0ab6dcaaf3a..62ddcf9fb5b 100644 --- a/src/Element/Data/ElementActivity.php +++ b/src/Element/Data/ElementActivity.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Data; -use craft\base\ElementInterface; use CraftCms\Cms\Component\Component; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Enums\ElementActivityType; use CraftCms\Cms\User\Elements\User; use DateTime; diff --git a/src/Element/Drafts.php b/src/Element/Drafts.php index c837d69318e..9b928c38d94 100644 --- a/src/Element/Drafts.php +++ b/src/Element/Drafts.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Element; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; use CraftCms\Cms\Cms; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Events\ApplyingDraft; use CraftCms\Cms\Element\Events\CreatingDraft; use CraftCms\Cms\Element\Events\DraftApplied; diff --git a/src/Element/Element.php b/src/Element/Element.php index 886423c48c9..04e650247b1 100644 --- a/src/Element/Element.php +++ b/src/Element/Element.php @@ -6,9 +6,11 @@ use ArrayIterator; use BadMethodCallException; -use craft\base\Component; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; +use CraftCms\Cms\Component\Component; +use CraftCms\Cms\Component\Exceptions\InvalidCallException; +use CraftCms\Cms\Component\Exceptions\UnknownPropertyException; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Validation\ElementRules; use CraftCms\Cms\Field\Fields; use CraftCms\Cms\FieldLayout\LayoutElements\BaseField; @@ -16,6 +18,7 @@ use CraftCms\Cms\Support\Str; use CraftCms\Cms\Support\Utils; use CraftCms\Cms\Twig\Attributes\AllowedInSandbox; +use CraftCms\Cms\User\Elements\User; use CraftCms\Cms\Validation\Concerns\Validates; use CraftCms\RulesetValidation\Attributes\Ruleset; use DateTime; @@ -25,9 +28,7 @@ use Override; use Throwable; use Traversable; -use yii\base\ArrayableTrait; -use yii\base\InvalidCallException; -use yii\base\UnknownPropertyException; +use Yiisoft\Arrays\ArrayableTrait; use function CraftCms\Cms\t; @@ -210,6 +211,19 @@ public static function hasTitles(): bool return false; } + public function getCreator(): ?User + { + if ($this->getIsDraft()) { + return $this->getDraftCreator(); + } + + if ($this->getIsRevision()) { + return $this->getRevisionCreator(); + } + + return null; + } + /** * @var array|null * @@ -232,13 +246,20 @@ public function __construct($config = []) } parent::__construct($config); + + if (! isset($this->siteId) && Cms::isInstalled()) { + $this->siteId = Sites::getPrimarySite()->id; + } + + if (static::hasTitles()) { + $this->_savedTitle = $this->title; + } + + $this->_initialized = true; } - #[Override] public function __clone() { - parent::__clone(); - // Mark all fields as dirty $this->_allDirty = true; $this->_hasNewParent = null; @@ -302,8 +323,8 @@ public function __get($name) } // Is this the "field:handle" syntax? - if (str_starts_with($name, 'field:')) { - return $this->getFieldValue(substr($name, 6)); + if (str_starts_with((string) $name, 'field:')) { + return $this->getFieldValue(substr((string) $name, 6)); } // If this is a field, make sure the value has been normalized before returning it @@ -315,7 +336,7 @@ public function __get($name) return $this->getCustomFieldRawValue($name); } - if (isset($this->_generatedFieldValues) && array_key_exists($name, $this->_generatedFieldValues)) { + if (isset($this->_generatedFieldValues) && array_key_exists((string) $name, $this->_generatedFieldValues)) { return $this->_generatedFieldValues[$name]; } @@ -327,7 +348,7 @@ public function __get($name) } #[Override] - public function __set($name, $value) + public function __set(string $name, $value): void { // Is this the "field:handle" syntax? if (str_starts_with($name, 'field:')) { @@ -338,8 +359,7 @@ public function __set($name, $value) try { parent::__set($name, $value); - /** @phpstan-ignore-next-line */ - } catch (InvalidCallException|UnknownPropertyException|\CraftCms\Cms\Component\Exceptions\InvalidCallException|\CraftCms\Cms\Component\Exceptions\UnknownPropertyException $e) { + } catch (InvalidCallException|UnknownPropertyException $e) { // Is this is a field? if ($this->fieldByHandle($name) !== null) { $this->setFieldValue($name, $value); @@ -363,28 +383,6 @@ public function __call($name, $params) } } - #[Override] - protected function defineBehaviors(): array - { - return []; - } - - #[Override] - public function init(): void - { - parent::init(); - - if (! isset($this->siteId) && Cms::isInstalled()) { - $this->siteId = Sites::getPrimarySite()->id; - } - - if (static::hasTitles()) { - $this->_savedTitle = $this->title; - } - - $this->_initialized = true; - } - /** * @TODO: Remove parameters once Element no longer extends Yii Model */ @@ -406,7 +404,6 @@ public function validationData($names = null, $except = []): array return $values; } - #[Override] public function attributes(): array { $names = array_flip(Utils::getPublicAttributes($this)); @@ -464,7 +461,8 @@ public function attributes(): array #[Override] public function fields(): array { - $fields = parent::fields(); + $attributes = $this->attributes(); + $fields = array_combine($attributes, $attributes); foreach ($this->fieldLayoutFields() as $field) { if (! isset($fields[$field->handle])) { @@ -522,27 +520,7 @@ public function extraFields(): array } #[Override] - public function getIterator(): Traversable - { - $attributes = $this->getAttributes(); - - // Include custom fields - $fieldLayout = $this->getFieldLayout(); - - if ($fieldLayout !== null) { - foreach ($fieldLayout->getCustomFieldElements() as $layoutElement) { - $field = $layoutElement->getField(); - if (! isset($attributes[$field->handle])) { - $attributes[$field->handle] = $this->getFieldValue($field->handle); - } - } - } - - return new ArrayIterator($attributes); - } - - #[Override] - public function getAttributeLabel($attribute): string + public function getAttributeLabel(string $attribute): string { // Is this the "field:handle" syntax? if (str_starts_with($attribute, 'field:')) { @@ -707,9 +685,27 @@ public function setAttributesFromRequest(array $values): void $this->setAttributes($values); } - #[Override] public function safeAttributes(): array { return array_keys($this->ruleset->rules()); } + + public function getIterator(): Traversable + { + $attributes = $this->validationData(); + + // Include custom fields + $fieldLayout = $this->getFieldLayout(); + + if ($fieldLayout !== null) { + foreach ($fieldLayout->getCustomFieldElements() as $layoutElement) { + $field = $layoutElement->getField(); + if (! isset($attributes[$field->handle])) { + $attributes[$field->handle] = $this->getFieldValue($field->handle); + } + } + } + + return new ArrayIterator($attributes); + } } diff --git a/src/Element/ElementActions.php b/src/Element/ElementActions.php index 1dd7e485527..9f5ab8ba3fe 100644 --- a/src/Element/ElementActions.php +++ b/src/Element/ElementActions.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\Element; -use craft\base\ElementInterface; use CraftCms\Cms\Component\ComponentHelper; use CraftCms\Cms\Element\Actions\Restore; use CraftCms\Cms\Element\Contracts\DeleteActionInterface; use CraftCms\Cms\Element\Contracts\ElementActionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Events\AfterPerformAction; use CraftCms\Cms\Element\Events\BeforePerformAction; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; @@ -53,7 +53,7 @@ public function availableActions(string $elementType, string $sourceKey, Element } /** - * @param ElementActionInterface|class-string|array{type:class-string} $action + * @param ElementActionInterface|class-string|array{type:class-string, ...} $action * @param class-string $elementType */ public function createAction(mixed $action, string $elementType): ElementActionInterface diff --git a/src/Element/ElementActivity.php b/src/Element/ElementActivity.php index 14dc6e67de5..0d5e20730ac 100644 --- a/src/Element/ElementActivity.php +++ b/src/Element/ElementActivity.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Data\ElementActivity as ElementActivityData; use CraftCms\Cms\Element\Enums\ElementActivityType; use CraftCms\Cms\Support\DateTimeHelper; diff --git a/src/Element/ElementAttributeRenderer.php b/src/Element/ElementAttributeRenderer.php index 0b00958f54a..40f19292700 100644 --- a/src/Element/ElementAttributeRenderer.php +++ b/src/Element/ElementAttributeRenderer.php @@ -4,12 +4,12 @@ namespace CraftCms\Cms\Element; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; use CraftCms\Cms\Cp\Html\ElementHtml; use CraftCms\Cms\Cp\Html\PreviewHtml; use CraftCms\Cms\Cp\Html\StatusHtml; use CraftCms\Cms\Cp\Icons; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\ContentBlock as ContentBlockField; use CraftCms\Cms\Field\Contracts\EagerLoadingFieldInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; diff --git a/src/Element/ElementCaches.php b/src/Element/ElementCaches.php index a613369a2c1..990c1fb5f74 100644 --- a/src/Element/ElementCaches.php +++ b/src/Element/ElementCaches.php @@ -4,16 +4,16 @@ namespace CraftCms\Cms\Element; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Events\InvalidateElementCaches; use CraftCms\Cms\View\CacheCollectors\DependencyCollector; use CraftCms\Cms\View\Data\TemplateCacheContext; use CraftCms\DependencyAwareCache\Dependency\TagDependency; use DateTime; use Illuminate\Container\Attributes\Singleton; +use RuntimeException; use Throwable; -use yii\base\InvalidConfigException; #[Singleton] readonly class ElementCaches @@ -113,7 +113,7 @@ private function tagsForElement(ElementInterface $element): array if ($element instanceof NestedElementInterface) { try { $owner = $element->getOwner(); - } catch (InvalidConfigException) { + } catch (RuntimeException) { $owner = null; } diff --git a/src/Element/ElementCollection.php b/src/Element/ElementCollection.php index 5439f5988e1..371b58974a9 100644 --- a/src/Element/ElementCollection.php +++ b/src/Element/ElementCollection.php @@ -6,7 +6,7 @@ use Closure; use Craft; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\Elements; use Illuminate\Contracts\Support\Arrayable; diff --git a/src/Element/ElementExporters.php b/src/Element/ElementExporters.php index 719aa49d621..ba7d33b42dd 100644 --- a/src/Element/ElementExporters.php +++ b/src/Element/ElementExporters.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element; -use craft\base\ElementInterface; use CraftCms\Cms\Component\ComponentHelper; use CraftCms\Cms\Element\Contracts\ElementExporterInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Support\Json; use CraftCms\Cms\Support\Str; diff --git a/src/Element/ElementHelper.php b/src/Element/ElementHelper.php index 6712de21452..8c01ba86b98 100644 --- a/src/Element/ElementHelper.php +++ b/src/Element/ElementHelper.php @@ -5,11 +5,11 @@ namespace CraftCms\Cms\Element; use Craft; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; use CraftCms\Cms\Cms; use CraftCms\Cms\Database\Table; use CraftCms\Cms\Element\Contracts\ElementActionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Field\Exceptions\FieldNotFoundException; use CraftCms\Cms\FieldLayout\LayoutElements\CustomField; use CraftCms\Cms\Shared\Exceptions\OperationAbortedException; diff --git a/src/Element/ElementRelations.php b/src/Element/ElementRelations.php index 19169b83084..c7aa7d97e44 100644 --- a/src/Element/ElementRelations.php +++ b/src/Element/ElementRelations.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; use CraftCms\Cms\Field\Contracts\RelationalFieldInterface; use CraftCms\Cms\Support\Arr; diff --git a/src/Element/ElementSources.php b/src/Element/ElementSources.php index 1544acf49c3..a9b962ad2cf 100644 --- a/src/Element/ElementSources.php +++ b/src/Element/ElementSources.php @@ -4,12 +4,12 @@ namespace CraftCms\Cms\Element; -use craft\base\ElementInterface; use craft\db\CoalesceColumnsExpression; use CraftCms\Cms\Condition\Contracts\ConditionInterface; use CraftCms\Cms\Cp\Icons; use CraftCms\Cms\Database\Expressions\JsonExtract; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Events\DefineSourceSortOptions; use CraftCms\Cms\Element\Events\DefineSourceTableAttributes; use CraftCms\Cms\Field\Contracts\PreviewableFieldInterface; @@ -47,6 +47,8 @@ class ElementSources public const string CONTEXT_SETTINGS = 'settings'; + public const string CONTEXT_EMBEDDED_INDEX = 'embeddedIndex'; + /** * @see defineSources() */ diff --git a/src/Element/Elements.php b/src/Element/Elements.php index 7e486c43bf4..77bdfcb16b4 100644 --- a/src/Element/Elements.php +++ b/src/Element/Elements.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\Element; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Component\ComponentHelper; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Data\EagerLoadPlan; use CraftCms\Cms\Element\Events\RegisterElementTypes; use CraftCms\Cms\Element\Exceptions\InvalidElementException; diff --git a/src/Element/Enums/ElementIndexViewMode.php b/src/Element/Enums/ElementIndexViewMode.php new file mode 100644 index 00000000000..fff44f2948a --- /dev/null +++ b/src/Element/Enums/ElementIndexViewMode.php @@ -0,0 +1,51 @@ + t('Display as cards'), + self::Structure => t('Display in a structured table'), + self::Table => t('Display in a table'), + self::Thumbs => t('Display as thumbnails'), + }; + } + + public function icon(): string + { + return match ($this) { + self::Cards => 'element-cards', + self::Structure => I18N::getLocale()->getOrientation() === 'rtl' + ? 'structurertl' + : 'structure', + self::Table => 'list', + self::Thumbs => 'grid', + }; + } + + public function toArray(): array + { + return [ + 'mode' => $this->value, + 'title' => $this->title(), + 'icon' => $this->icon(), + ]; + } +} diff --git a/src/Element/Events/AfterDelete.php b/src/Element/Events/AfterDelete.php index fcca36f2361..4f415590547 100644 --- a/src/Element/Events/AfterDelete.php +++ b/src/Element/Events/AfterDelete.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; /** diff --git a/src/Element/Events/AfterDeleteElement.php b/src/Element/Events/AfterDeleteElement.php index 4a6b0b8b077..23478ac50f7 100644 --- a/src/Element/Events/AfterDeleteElement.php +++ b/src/Element/Events/AfterDeleteElement.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class AfterDeleteElement { diff --git a/src/Element/Events/AfterDeleteForSite.php b/src/Element/Events/AfterDeleteForSite.php index e61ddb6781d..42d10212f3b 100644 --- a/src/Element/Events/AfterDeleteForSite.php +++ b/src/Element/Events/AfterDeleteForSite.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class AfterDeleteForSite { diff --git a/src/Element/Events/AfterMergeCanonicalChanges.php b/src/Element/Events/AfterMergeCanonicalChanges.php index ac371ecb6ce..cb1e017dc52 100644 --- a/src/Element/Events/AfterMergeCanonicalChanges.php +++ b/src/Element/Events/AfterMergeCanonicalChanges.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class AfterMergeCanonicalChanges { diff --git a/src/Element/Events/AfterMoveInStructure.php b/src/Element/Events/AfterMoveInStructure.php index bd0827672e7..c7024503ee6 100644 --- a/src/Element/Events/AfterMoveInStructure.php +++ b/src/Element/Events/AfterMoveInStructure.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; use CraftCms\Cms\Element\Concerns\Structurable; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * @event AfterMoveInStructure The event that is triggered after the element is moved in a structure. diff --git a/src/Element/Events/AfterPropagate.php b/src/Element/Events/AfterPropagate.php index 0be0ae43704..7a4159f584c 100644 --- a/src/Element/Events/AfterPropagate.php +++ b/src/Element/Events/AfterPropagate.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; /** diff --git a/src/Element/Events/AfterPropagateElement.php b/src/Element/Events/AfterPropagateElement.php index 5588825d727..ed9387a6226 100644 --- a/src/Element/Events/AfterPropagateElement.php +++ b/src/Element/Events/AfterPropagateElement.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use Throwable; diff --git a/src/Element/Events/AfterResaveElement.php b/src/Element/Events/AfterResaveElement.php index d00629249d5..aa683056011 100644 --- a/src/Element/Events/AfterResaveElement.php +++ b/src/Element/Events/AfterResaveElement.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use Throwable; diff --git a/src/Element/Events/AfterRestore.php b/src/Element/Events/AfterRestore.php index 8a5079d26b3..90ac221da65 100644 --- a/src/Element/Events/AfterRestore.php +++ b/src/Element/Events/AfterRestore.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; /** diff --git a/src/Element/Events/AfterRestoreElement.php b/src/Element/Events/AfterRestoreElement.php index d17836787f5..94ed17d1186 100644 --- a/src/Element/Events/AfterRestoreElement.php +++ b/src/Element/Events/AfterRestoreElement.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class AfterRestoreElement { diff --git a/src/Element/Events/AfterSave.php b/src/Element/Events/AfterSave.php index f3b5743dde3..c43733193b7 100644 --- a/src/Element/Events/AfterSave.php +++ b/src/Element/Events/AfterSave.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; /** diff --git a/src/Element/Events/AfterSaveElement.php b/src/Element/Events/AfterSaveElement.php index 0378587ead1..3702e0b37b1 100644 --- a/src/Element/Events/AfterSaveElement.php +++ b/src/Element/Events/AfterSaveElement.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class AfterSaveElement { diff --git a/src/Element/Events/AfterSaveNestedElements.php b/src/Element/Events/AfterSaveNestedElements.php index ee6ce4c762c..c6eba093a6f 100644 --- a/src/Element/Events/AfterSaveNestedElements.php +++ b/src/Element/Events/AfterSaveNestedElements.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\NestedElementManager; class AfterSaveNestedElements diff --git a/src/Element/Events/AfterUpdateSlugAndUri.php b/src/Element/Events/AfterUpdateSlugAndUri.php index 2a50aae61be..e1291f05c1f 100644 --- a/src/Element/Events/AfterUpdateSlugAndUri.php +++ b/src/Element/Events/AfterUpdateSlugAndUri.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class AfterUpdateSlugAndUri { diff --git a/src/Element/Events/AuthorizeCreateDrafts.php b/src/Element/Events/AuthorizeCreateDrafts.php deleted file mode 100644 index c61659a918d..00000000000 --- a/src/Element/Events/AuthorizeCreateDrafts.php +++ /dev/null @@ -1,17 +0,0 @@ - 'nested-element-cards', ]); - /** @var ElementQueryInterface|ElementCollection $value */ $value = $this->getValue($owner, true); if ($value instanceof ElementCollection) { /** @var NestedElementInterface[] $elements */ diff --git a/src/Element/Operations/ElementCanonicalChanges.php b/src/Element/Operations/ElementCanonicalChanges.php index 8d2ad539800..0530d44feb6 100644 --- a/src/Element/Operations/ElementCanonicalChanges.php +++ b/src/Element/Operations/ElementCanonicalChanges.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Operations; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; use CraftCms\Cms\Element\BulkOp\BulkOps; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Events\AfterMergeCanonicalChanges; use CraftCms\Cms\Element\Events\BeforeMergeCanonicalChanges; diff --git a/src/Element/Operations/ElementDeletions.php b/src/Element/Operations/ElementDeletions.php index 2ad9859e046..91babcc1f7f 100644 --- a/src/Element/Operations/ElementDeletions.php +++ b/src/Element/Operations/ElementDeletions.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Operations; -use craft\base\ElementInterface; use craft\behaviors\CustomFieldBehavior; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementCaches; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\ElementHelper; diff --git a/src/Element/Operations/ElementDuplicates.php b/src/Element/Operations/ElementDuplicates.php index d3142c9a152..948c5c794a2 100644 --- a/src/Element/Operations/ElementDuplicates.php +++ b/src/Element/Operations/ElementDuplicates.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Operations; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Exceptions\InvalidElementException; diff --git a/src/Element/Operations/ElementEagerLoader.php b/src/Element/Operations/ElementEagerLoader.php index e9742f43df1..6878d7ed0dc 100644 --- a/src/Element/Operations/ElementEagerLoader.php +++ b/src/Element/Operations/ElementEagerLoader.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Operations; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Contracts\ExpirableElementInterface; use CraftCms\Cms\Element\Data\EagerLoadInfo; use CraftCms\Cms\Element\Data\EagerLoadPlan; diff --git a/src/Element/Operations/ElementPlaceholders.php b/src/Element/Operations/ElementPlaceholders.php index dd22571a444..83d04d3f710 100644 --- a/src/Element/Operations/ElementPlaceholders.php +++ b/src/Element/Operations/ElementPlaceholders.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Operations; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use Illuminate\Container\Attributes\Scoped; use InvalidArgumentException; diff --git a/src/Element/Operations/ElementRefs.php b/src/Element/Operations/ElementRefs.php index 54e94a88595..49674c6b436 100644 --- a/src/Element/Operations/ElementRefs.php +++ b/src/Element/Operations/ElementRefs.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Operations; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Elements; use CraftCms\Cms\Site\Exceptions\SiteNotFoundException; use CraftCms\Cms\Site\Sites; diff --git a/src/Element/Operations/ElementUris.php b/src/Element/Operations/ElementUris.php index 409d4d9334f..beaf6f061be 100644 --- a/src/Element/Operations/ElementUris.php +++ b/src/Element/Operations/ElementUris.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Operations; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementCaches; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Elements; diff --git a/src/Element/Operations/ElementWrites.php b/src/Element/Operations/ElementWrites.php index 6400971d0b8..c2787aac14a 100644 --- a/src/Element/Operations/ElementWrites.php +++ b/src/Element/Operations/ElementWrites.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Operations; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\ElementCaches; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Elements; diff --git a/src/Element/Policies/ElementPolicy.php b/src/Element/Policies/ElementPolicy.php index b3dee610d1f..a61fc1c5c2c 100644 --- a/src/Element/Policies/ElementPolicy.php +++ b/src/Element/Policies/ElementPolicy.php @@ -5,9 +5,9 @@ namespace CraftCms\Cms\Element\Policies; use BadMethodCallException; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; use CraftCms\Cms\Auth\Events\AuthorizingElement; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface; use CraftCms\Cms\Site\Models\Site; use CraftCms\Cms\Support\Facades\Sites; diff --git a/src/Element/Queries/AssetQuery.php b/src/Element/Queries/AssetQuery.php index acf20bd692c..43574d1dfb1 100644 --- a/src/Element/Queries/AssetQuery.php +++ b/src/Element/Queries/AssetQuery.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element\Queries; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Concerns\Asset\EagerloadsTransforms; use CraftCms\Cms\Element\Queries\Concerns\Asset\QueriesAlt; use CraftCms\Cms\Element\Queries\Concerns\Asset\QueriesAssetLocation; diff --git a/src/Element/Queries/Concerns/HydratesElements.php b/src/Element/Queries/Concerns/HydratesElements.php index d0b5ef15b66..297be022bc1 100644 --- a/src/Element/Queries/Concerns/HydratesElements.php +++ b/src/Element/Queries/Concerns/HydratesElements.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Queries\Concerns; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Contracts\ExpirableElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\ElementHelper; diff --git a/src/Element/Queries/Concerns/OverridesResults.php b/src/Element/Queries/Concerns/OverridesResults.php index e61af41c477..8eaecd375a8 100644 --- a/src/Element/Queries/Concerns/OverridesResults.php +++ b/src/Element/Queries/Concerns/OverridesResults.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Queries\Concerns; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * @template TValue of ElementInterface diff --git a/src/Element/Queries/Concerns/QueriesDraftsAndRevisions.php b/src/Element/Queries/Concerns/QueriesDraftsAndRevisions.php index 6dff9744028..b3b88470363 100644 --- a/src/Element/Queries/Concerns/QueriesDraftsAndRevisions.php +++ b/src/Element/Queries/Concerns/QueriesDraftsAndRevisions.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Queries\Concerns; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\ElementQuery; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\User\Elements\User; diff --git a/src/Element/Queries/Concerns/QueriesEagerly.php b/src/Element/Queries/Concerns/QueriesEagerly.php index 7d28cf96bdd..be54c5f879d 100644 --- a/src/Element/Queries/Concerns/QueriesEagerly.php +++ b/src/Element/Queries/Concerns/QueriesEagerly.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Queries\Concerns; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Data\EagerLoadPlan; use CraftCms\Cms\Support\Facades\Elements; use Illuminate\Support\Collection; diff --git a/src/Element/Queries/Concerns/QueriesNestedElements.php b/src/Element/Queries/Concerns/QueriesNestedElements.php index 23f80f3e358..2135bebabbf 100644 --- a/src/Element/Queries/Concerns/QueriesNestedElements.php +++ b/src/Element/Queries/Concerns/QueriesNestedElements.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Queries\Concerns; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\ContentBlockQuery; use CraftCms\Cms\Element\Queries\ElementQuery; use CraftCms\Cms\Element\Queries\EntryQuery; diff --git a/src/Element/Queries/Concerns/QueriesSites.php b/src/Element/Queries/Concerns/QueriesSites.php index a395741542d..4bbe85b8987 100644 --- a/src/Element/Queries/Concerns/QueriesSites.php +++ b/src/Element/Queries/Concerns/QueriesSites.php @@ -30,6 +30,8 @@ trait QueriesSites */ public mixed $siteId = null; + private mixed $appliedSiteId = null; + protected function initQueriesSites(): void { $this->beforeQuery(function (ElementQuery $elementQuery) { @@ -50,6 +52,8 @@ protected function initQueriesSites(): void throw new QueryAbortedException($e->getMessage(), 0, $e); } + $elementQuery->appliedSiteId = $elementQuery->siteId; + if (Sites::isMultiSite(false, true)) { $elementQuery->whereIn('elements_sites.siteId', Arr::wrap($elementQuery->siteId)); } diff --git a/src/Element/Queries/Concerns/QueriesStructures.php b/src/Element/Queries/Concerns/QueriesStructures.php index 0d8f6b0e4f5..9ae935c00ac 100644 --- a/src/Element/Queries/Concerns/QueriesStructures.php +++ b/src/Element/Queries/Concerns/QueriesStructures.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Queries\Concerns; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\ElementQuery; use CraftCms\Cms\Element\Queries\Exceptions\QueryAbortedException; use CraftCms\Cms\Support\Facades\Elements; diff --git a/src/Element/Queries/Concerns/QueriesUniqueElements.php b/src/Element/Queries/Concerns/QueriesUniqueElements.php index 145f0486dff..9c36161c944 100644 --- a/src/Element/Queries/Concerns/QueriesUniqueElements.php +++ b/src/Element/Queries/Concerns/QueriesUniqueElements.php @@ -44,13 +44,15 @@ protected function applyUniqueParams(ElementQuery $elementQuery): void return; } - if ($elementQuery->siteId && - (! is_array($elementQuery->siteId) || count($elementQuery->siteId) === 1) + $siteIds = $elementQuery->appliedSiteId ?? $elementQuery->siteId; + + if ($siteIds && + (! is_array($siteIds) || count($siteIds) === 1) ) { return; } - $preferSites = collect($elementQuery->preferSites ?? Sites::getCurrentSite()->id) + $preferSites = collect($elementQuery->preferSites ?? [Sites::getCurrentSite()->id]) ->map(fn (string|int $preferSite) => match (true) { is_numeric($preferSite) => $preferSite, ! is_null($site = Sites::getSiteByHandle($preferSite)) => $site->id, diff --git a/src/Element/Queries/Concerns/User/QueriesAuthors.php b/src/Element/Queries/Concerns/User/QueriesAuthors.php index 9d96f4adc89..d99beba4d73 100644 --- a/src/Element/Queries/Concerns/User/QueriesAuthors.php +++ b/src/Element/Queries/Concerns/User/QueriesAuthors.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Element\Queries\Concerns\User; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Exceptions\QueryAbortedException; use CraftCms\Cms\Element\Queries\UserQuery; use Illuminate\Support\Facades\DB; diff --git a/src/Element/Queries/Contracts/ElementQueryInterface.php b/src/Element/Queries/Contracts/ElementQueryInterface.php index 75118473616..3d9d44b19f7 100644 --- a/src/Element/Queries/Contracts/ElementQueryInterface.php +++ b/src/Element/Queries/Contracts/ElementQueryInterface.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Queries\Contracts; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\ElementQuery; use CraftCms\Cms\FieldLayout\FieldLayout; use Illuminate\Contracts\Database\Query\Builder; diff --git a/src/Element/Queries/Contracts/NestedElementQueryInterface.php b/src/Element/Queries/Contracts/NestedElementQueryInterface.php index 7125f5f84c3..7e916c1a274 100644 --- a/src/Element/Queries/Contracts/NestedElementQueryInterface.php +++ b/src/Element/Queries/Contracts/NestedElementQueryInterface.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Queries\Contracts; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * NestedElementQueryInterface defines the common interface to be implemented by element query classes diff --git a/src/Element/Queries/ElementQuery.php b/src/Element/Queries/ElementQuery.php index 3ba7bba784c..600c7d2a78b 100644 --- a/src/Element/Queries/ElementQuery.php +++ b/src/Element/Queries/ElementQuery.php @@ -5,9 +5,9 @@ namespace CraftCms\Cms\Element\Queries; use Closure; -use craft\base\ElementInterface; use CraftCms\Cms\Component\Component; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\ElementHelper; @@ -26,6 +26,8 @@ use Illuminate\Database\MultipleRecordsFoundException; use Illuminate\Database\Query\Builder; use Illuminate\Database\RecordsNotFoundException; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Pagination\Paginator; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\LazyCollection; @@ -545,6 +547,59 @@ public function pluck($column, $key = null): Collection return $this->query->pluck($column, $key); } + /** + * Paginate the given query. + * + * @param int|null|Closure $perPage + * @param array|string $columns + * @param string $pageName + * @param int|null $page + * @param Closure|int|null $total + * + * @throws InvalidArgumentException + */ + public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null, $total = null): LengthAwarePaginator + { + $page = $page ?: Paginator::resolveCurrentPage($pageName); + + $total = value($total) ?? $this->getCountForPagination(); + + $perPage = value($perPage, $total); + + $results = $total + ? $this->forPage($page, $perPage)->get($columns) + : new ElementCollection; + + return $this->paginator($results, $total, $perPage, $page, [ + 'path' => Paginator::resolveCurrentPath(), + 'pageName' => $pageName, + ]); + } + + /** + * Paginate the given query into a simple paginator. + * + * @param int|null $perPage + * @param array|string $columns + * @param string $pageName + * @param int|null $page + * @return \Illuminate\Contracts\Pagination\Paginator + */ + public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null) + { + $page = $page ?: Paginator::resolveCurrentPage($pageName); + + // Next we will set the limit and offset for this query so that when we get the + // results we get the proper section of results. Then, we'll create the full + // paginator instances for these results with the given page and per page. + $this->offset(($page - 1) * $perPage)->limit($perPage + 1); + + return $this->simplePaginator($this->get($columns), $perPage, $page, [ + 'path' => Paginator::resolveCurrentPath(), + 'pageName' => $pageName, + ]); + } + /** @TODO: Remove $_ variable after ElementQueryInterface is removed */ public function count($columns = '*', $_ = null): int { diff --git a/src/Element/Queries/Events/ElementHydrated.php b/src/Element/Queries/Events/ElementHydrated.php index 49900e8e38c..d1fadcfbe1e 100644 --- a/src/Element/Queries/Events/ElementHydrated.php +++ b/src/Element/Queries/Events/ElementHydrated.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Queries\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class ElementHydrated { diff --git a/src/Element/Queries/Events/ElementsHydrated.php b/src/Element/Queries/Events/ElementsHydrated.php index f220a4cf57f..70c5e9d7071 100644 --- a/src/Element/Queries/Events/ElementsHydrated.php +++ b/src/Element/Queries/Events/ElementsHydrated.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Queries\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class ElementsHydrated { diff --git a/src/Element/Queries/Events/HydratingElement.php b/src/Element/Queries/Events/HydratingElement.php index fd5f9087d24..125df63c049 100644 --- a/src/Element/Queries/Events/HydratingElement.php +++ b/src/Element/Queries/Events/HydratingElement.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Element\Queries\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class HydratingElement { diff --git a/src/Element/Queries/Exceptions/ElementNotFoundException.php b/src/Element/Queries/Exceptions/ElementNotFoundException.php index 11fae7db378..50f5d05e8a5 100644 --- a/src/Element/Queries/Exceptions/ElementNotFoundException.php +++ b/src/Element/Queries/Exceptions/ElementNotFoundException.php @@ -8,7 +8,7 @@ use Illuminate\Database\RecordsNotFoundException; /** - * @template TElement of \craft\base\ElementInterface + * @template TElement of \CraftCms\Cms\Element\Contracts\ElementInterface */ class ElementNotFoundException extends RecordsNotFoundException { diff --git a/src/Element/Revisions.php b/src/Element/Revisions.php index 76fe19814ec..4d142057cc1 100644 --- a/src/Element/Revisions.php +++ b/src/Element/Revisions.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Element; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Events\CreatingRevision; use CraftCms\Cms\Element\Events\RevertedToRevision; use CraftCms\Cms\Element\Events\RevertingToRevision; @@ -189,7 +189,6 @@ public function createRevision( */ public function revertToRevision(ElementInterface $revision, int $creatorId): ElementInterface { - /** @var ElementInterface $revision */ $canonical = $revision->getCanonical(); event(new RevertingToRevision( diff --git a/src/Element/Validation/ElementRules.php b/src/Element/Validation/ElementRules.php index d8b8cbdaa95..8fab2f43549 100644 --- a/src/Element/Validation/ElementRules.php +++ b/src/Element/Validation/ElementRules.php @@ -5,8 +5,8 @@ namespace CraftCms\Cms\Element\Validation; use Closure; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Validation\Rules\ElementUriRule; @@ -18,8 +18,8 @@ use CraftCms\Cms\Validation\Ruleset; use Illuminate\Validation\Rule; use Override; +use RuntimeException; use Throwable; -use yii\base\InvalidConfigException; use function CraftCms\Cms\t; @@ -102,7 +102,7 @@ private function addTitleRules(array $rules): array } else { array_unshift($rules['title'], 'nullable'); } - } catch (InvalidConfigException) { + } catch (RuntimeException) { // Related to sectionId, fieldId and ownerId being missing // Which will be caught by the other validation rules. } diff --git a/src/Entry/Conditions/AuthorConditionRule.php b/src/Entry/Conditions/AuthorConditionRule.php index 14e1a054008..1e87ce0c426 100644 --- a/src/Entry/Conditions/AuthorConditionRule.php +++ b/src/Entry/Conditions/AuthorConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Entry\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseElementSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\EntryQuery; use CraftCms\Cms\Entry\Elements\Entry; diff --git a/src/Entry/Conditions/AuthorGroupConditionRule.php b/src/Entry/Conditions/AuthorGroupConditionRule.php index 323040b7646..bdaa70f1a78 100644 --- a/src/Entry/Conditions/AuthorGroupConditionRule.php +++ b/src/Entry/Conditions/AuthorGroupConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Entry\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\EntryQuery; use CraftCms\Cms\Entry\Elements\Entry; diff --git a/src/Entry/Conditions/ExpiryDateConditionRule.php b/src/Entry/Conditions/ExpiryDateConditionRule.php index dc778d1237b..1dfdd36ae06 100644 --- a/src/Entry/Conditions/ExpiryDateConditionRule.php +++ b/src/Entry/Conditions/ExpiryDateConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Entry\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseDateRangeConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\EntryQuery; use CraftCms\Cms\Entry\Elements\Entry; diff --git a/src/Entry/Conditions/FieldConditionRule.php b/src/Entry/Conditions/FieldConditionRule.php index 69765bdac05..090ebc058d6 100644 --- a/src/Entry/Conditions/FieldConditionRule.php +++ b/src/Entry/Conditions/FieldConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Entry\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; use CraftCms\Cms\Element\Conditions\HintableConditionRuleTrait; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\EntryQuery; use CraftCms\Cms\Entry\Elements\Entry; diff --git a/src/Entry/Conditions/PostDateConditionRule.php b/src/Entry/Conditions/PostDateConditionRule.php index ae4283a1914..5f7f5cbe174 100644 --- a/src/Entry/Conditions/PostDateConditionRule.php +++ b/src/Entry/Conditions/PostDateConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Entry\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseDateRangeConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\EntryQuery; use CraftCms\Cms\Entry\Elements\Entry; diff --git a/src/Entry/Conditions/SavableConditionRule.php b/src/Entry/Conditions/SavableConditionRule.php index 5f93623bfda..8d5b9196429 100644 --- a/src/Entry/Conditions/SavableConditionRule.php +++ b/src/Entry/Conditions/SavableConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Entry\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseLightswitchConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\EntryQuery; use Illuminate\Support\Facades\Gate; diff --git a/src/Entry/Conditions/SectionConditionRule.php b/src/Entry/Conditions/SectionConditionRule.php index 8de9a258691..319ca66880e 100644 --- a/src/Entry/Conditions/SectionConditionRule.php +++ b/src/Entry/Conditions/SectionConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Entry\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; use CraftCms\Cms\Element\Conditions\HintableConditionRuleTrait; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\EntryQuery; use CraftCms\Cms\Entry\Elements\Entry; diff --git a/src/Entry/Conditions/TypeConditionRule.php b/src/Entry/Conditions/TypeConditionRule.php index 3915a118f5f..21e2d5bbb22 100644 --- a/src/Entry/Conditions/TypeConditionRule.php +++ b/src/Entry/Conditions/TypeConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Entry\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\EntryQuery; use CraftCms\Cms\Entry\Data\EntryType; diff --git a/src/Entry/Conditions/ViewableConditionRule.php b/src/Entry/Conditions/ViewableConditionRule.php index 02c9abac1af..0e1301e28fc 100644 --- a/src/Entry/Conditions/ViewableConditionRule.php +++ b/src/Entry/Conditions/ViewableConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Entry\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseLightswitchConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\EntryQuery; use Illuminate\Support\Facades\Gate; diff --git a/src/Entry/Elements/Entry.php b/src/Entry/Elements/Entry.php index 69bbced14fa..774c80b613b 100644 --- a/src/Entry/Elements/Entry.php +++ b/src/Entry/Elements/Entry.php @@ -5,11 +5,6 @@ namespace CraftCms\Cms\Entry\Elements; use Craft; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; -use craft\base\NestedElementTrait; -use craft\controllers\ElementIndexesController; -use craft\controllers\ElementsController; use CraftCms\Cms\Cms; use CraftCms\Cms\Component\Contracts\Colorable; use CraftCms\Cms\Component\Contracts\Iconic; @@ -24,8 +19,12 @@ use CraftCms\Cms\Element\Actions\DeleteForSite; use CraftCms\Cms\Element\Actions\Duplicate; use CraftCms\Cms\Element\Actions\Restore; +use CraftCms\Cms\Element\Concerns\NestedElement; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Contracts\ExpirableElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; +use CraftCms\Cms\Element\CurrentElementIndex; use CraftCms\Cms\Element\Data\EagerLoadPlan; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementHelper; @@ -53,8 +52,9 @@ use CraftCms\Cms\Field\Fields; use CraftCms\Cms\Field\Matrix; use CraftCms\Cms\FieldLayout\FieldLayout; -use CraftCms\Cms\FieldLayout\LayoutElements\entries\EntryTitleField; +use CraftCms\Cms\FieldLayout\LayoutElements\Entries\EntryTitleField; use CraftCms\Cms\Gql\Interfaces\Elements\Entry as EntryInterface; +use CraftCms\Cms\Http\Requests\ElementRequest; use CraftCms\Cms\Section\Data\Section; use CraftCms\Cms\Section\Data\SectionSiteSettings; use CraftCms\Cms\Section\Enums\DefaultPlacement; @@ -87,15 +87,12 @@ use DateTime; use GraphQL\Type\Definition\Type; use Illuminate\Database\Query\JoinClause; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use InvalidArgumentException; use Override; -use Throwable; +use RuntimeException; use Tpetry\QueryExpressions\Language\Alias; -use yii\base\Exception; -use yii\base\InvalidConfigException; use function CraftCms\Cms\renderObjectTemplate; use function CraftCms\Cms\t; @@ -112,7 +109,7 @@ #[Ruleset(EntryRules::class)] class Entry extends Element implements Colorable, ExpirableElementInterface, Iconic, NestedElementInterface { - use NestedElementTrait { + use NestedElement { eagerLoadingMap as traitEagerLoadingMap; attributes as traitAttributes; extraFields as traitExtraFields; @@ -125,6 +122,137 @@ class Entry extends Element implements Colorable, ExpirableElementInterface, Ico public const string STATUS_EXPIRED = 'expired'; + /** + * @var int|null Section ID + * --- + * ```php + * echo $entry->sectionId; + * ``` + * ```twig + * {{ entry.sectionId }} + * ``` + */ + public ?int $sectionId = null; + + /** + * @var bool Collapsed + */ + public bool $collapsed = false; + + /** + * @var DateTime|null Post date + * --- + * ```php + * echo Craft::$app->formatter->asDate($entry->postDate, 'short'); + * ``` + * ```twig + * {{ entry.postDate|date('short') }} + * ``` + */ + #[AllowedInSandbox] + public ?DateTime $postDate = null; + + /** + * @var DateTime|null Expiry date + * --- + * ```php + * if ($entry->expiryDate) { + * echo Craft::$app->formatter->asDate($entry->expiryDate, 'short'); + * } + * ``` + * ```twig + * {% if entry.expiryDate %} + * {{ entry.expiryDate|date('short') }} + * {% endif %} + * ``` + */ + #[AllowedInSandbox] + public ?DateTime $expiryDate = null; + + /** + * @var self::STATUS_*|null The entry’s previous status, if it had one + */ + public ?string $oldStatus = null; + + /** + * @var self::STATUS_LIVE|self::STATUS_PENDING|self::STATUS_EXPIRED + */ + private string $status; + + /** + * @var bool Whether the entry was deleted along with its entry type + * + * @see beforeDelete() + * + * @internal + */ + public bool $deletedWithEntryType = false; + + /** + * @var bool Whether the entry was deleted along with its section + * + * @see beforeDelete() + * + * @internal + */ + public bool $deletedWithSection = false; + + /** + * @var bool Whether to force-place the entry within its structure. + */ + public bool $placeInStructure = false; + + /** + * @var int[] Entry author IDs + * + * @see getAuthorIds() + * @see setAuthorIds() + */ + private array $_authorIds; + + /** + * @var int[] Original entry author IDs + * + * @see getOldAuthorIds() + * @see setAuthorIds() + */ + private ?array $_oldAuthorIds = null; + + /** + * @var User[]|null Entry authors + * + * @see getAuthors() + * @see setAuthors() + */ + private ?array $_authors = null; + + /** + * @var int|null Type ID + * + * @see getType() + */ + private ?int $_typeId = null; + + private ?int $_oldTypeId = null; + + /** + * @var EntryType|null Entry Type + * + * @see getType() + */ + private ?EntryType $_type = null; + + public function __construct($config = []) + { + parent::__construct($config); + + if (isset($this->id)) { + $this->oldStatus = $this->getStatus(); + } + + $this->_oldTypeId = $this->_typeId; + } + #[Override] public static function displayName(): string { @@ -201,9 +329,6 @@ public static function statuses(): array ]; } - /** - * @return EntryQuery The newly created [[EntryQuery]] instance. - */ #[Override] public static function find(): EntryQuery { @@ -387,8 +512,8 @@ protected static function defineFieldLayouts(?string $source): array protected static function defineActions(string $source): array { // Get the selected site - $elementQuery = Craft::$app->controller instanceof ElementIndexesController - ? Craft::$app->controller->getElementQuery() + $elementQuery = app(CurrentElementIndex::class)->isActive() + ? app(CurrentElementIndex::class)->query() : null; $site = $elementQuery && $elementQuery->siteId ? Sites::getSiteById($elementQuery->siteId) @@ -443,7 +568,6 @@ protected static function defineActions(string $source): array $user->can("createEntries:$section->uid") && $user->can("saveEntries:$section->uid") ) { - // Duplicate $actions[] = [ 'type' => Duplicate::class, 'asDrafts' => true, @@ -457,27 +581,21 @@ protected static function defineActions(string $source): array ]; } - // Copy $actions[] = Copy::class; - - // Move to section $actions[] = MoveToSection::class; } - // Delete? $actions[] = Delete::class; - if ($user->can("deleteEntries:$section->uid")) { - if ( - $section->type === SectionType::Structure && - $section->maxLevels != 1 && - $user->can("deletePeerEntries:$section->uid") - ) { - $actions[] = [ - 'type' => Delete::class, - 'withDescendants' => true, - ]; - } + if ($user->can("deleteEntries:$section->uid") + && $section->type === SectionType::Structure + && $section->maxLevels !== 1 + && $user->can("deletePeerEntries:$section->uid") + ) { + $actions[] = [ + 'type' => Delete::class, + 'withDescendants' => true, + ]; } } else { $actions[] = Copy::class; @@ -501,7 +619,6 @@ protected static function defineActions(string $source): array $actions[] = DeleteForSite::class; } - // Restore $actions[] = Restore::class; return $actions; @@ -533,9 +650,11 @@ protected static function defineSortOptions(): array 'label' => t('Section'), 'orderBy' => function (int $dir) { $sectionIds = Sections::getAllSections() - ->sort(fn (Section $a, Section $b) => $dir === SORT_ASC - ? $a->name <=> $b->name - : $b->name <=> $a->name) + ->sort( + fn (Section $a, Section $b) => $dir === SORT_ASC + ? $a->name <=> $b->name + : $b->name <=> $a->name + ) ->pluck('id') ->all(); @@ -741,8 +860,6 @@ public static function eagerLoadingMap(array $sourceElements, string $handle): a /** * Returns the GraphQL type name that entries should use, based on their entry type. - * - * @since 5.0.0 */ public static function gqlTypeName(EntryType $entryType): string { @@ -764,9 +881,7 @@ public static function gqlScopesByContext(mixed $context): array /** @var Section $section */ $section = $context['section']; - return [ - "sections.$section->uid", - ]; + return ["sections.$section->uid"]; } #[Override] @@ -778,149 +893,13 @@ protected static function prepElementQueryForTableAttribute(ElementQueryInterfac }; } - /** - * @var int|null Section ID - * --- - * ```php - * echo $entry->sectionId; - * ``` - * ```twig - * {{ entry.sectionId }} - * ``` - */ - public ?int $sectionId = null; - - /** - * @var bool Collapsed - * - * @since 5.0.0 - */ - public bool $collapsed = false; - - /** - * @var DateTime|null Post date - * --- - * ```php - * echo Craft::$app->formatter->asDate($entry->postDate, 'short'); - * ``` - * ```twig - * {{ entry.postDate|date('short') }} - * ``` - */ - #[AllowedInSandbox] - public ?DateTime $postDate = null; - - /** - * @var DateTime|null Expiry date - * --- - * ```php - * if ($entry->expiryDate) { - * echo Craft::$app->formatter->asDate($entry->expiryDate, 'short'); - * } - * ``` - * ```twig - * {% if entry.expiryDate %} - * {{ entry.expiryDate|date('short') }} - * {% endif %} - * ``` - */ - #[AllowedInSandbox] - public ?DateTime $expiryDate = null; - - /** - * @var self::STATUS_*|null The entry’s previous status, if it had one - */ - public ?string $oldStatus = null; - - /** - * @var self::STATUS_LIVE|self::STATUS_PENDING|self::STATUS_EXPIRED - */ - private string $status; - - /** - * @var bool Whether the entry was deleted along with its entry type - * - * @see beforeDelete() - * - * @internal - */ - public bool $deletedWithEntryType = false; - - /** - * @var bool Whether the entry was deleted along with its section - * - * @see beforeDelete() - * - * @internal - */ - public bool $deletedWithSection = false; - - /** - * @var bool Whether to force-place the entry within its structure. - * - * @since 5.7.0 - */ - public bool $placeInStructure = false; - - /** - * @var int[] Entry author IDs - * - * @see getAuthorIds() - * @see setAuthorIds() - */ - private array $_authorIds; - - /** - * @var int[] Original entry author IDs - * - * @see getOldAuthorIds() - * @see setAuthorIds() - */ - private ?array $_oldAuthorIds = null; - - /** - * @var User[]|null Entry authors - * - * @see getAuthors() - * @see setAuthors() - */ - private ?array $_authors = null; - - /** - * @var int|null Type ID - * - * @see getType() - */ - private ?int $_typeId = null; - - private ?int $_oldTypeId = null; - - /** - * @var EntryType|null Entry Type - * - * @see getType() - */ - private ?EntryType $_type = null; - - /** - * @since 3.5.0 - */ - #[Override] - public function init(): void - { - parent::init(); - if (isset($this->id)) { - $this->oldStatus = $this->getStatus(); - } - $this->_oldTypeId = $this->_typeId; - } - #[Override] public function attributes(): array { $names = array_flip($this->traitAttributes()); - unset($names['deletedWithEntryType']); - unset($names['deletedWithSection']); + + unset($names['deletedWithEntryType'], $names['deletedWithSection']); + $names['authorId'] = true; $names['authorIds'] = true; $names['typeId'] = true; @@ -1016,7 +995,7 @@ public function getSupportedSites(): array } if (! isset($this->sectionId)) { - throw new InvalidConfigException('Either `sectionId` or `fieldId` + `ownerId` must be set on the entry.'); + throw new RuntimeException('Either `sectionId` or `fieldId` + `ownerId` must be set on the entry.'); } $section = $this->getSection(); @@ -1062,33 +1041,22 @@ public function getSupportedSites(): array } foreach ($section->getSiteSettings() as $siteSettings) { - switch ($section->propagationMethod) { - case PropagationMethod::None: - $include = $siteSettings->siteId === $this->siteId; - $propagate = true; - break; - case PropagationMethod::SiteGroup: - $include = $allSites[$siteSettings->siteId]->groupId === $allSites[$this->siteId]->groupId; - $propagate = true; - break; - case PropagationMethod::Language: - $include = $allSites[$siteSettings->siteId]->getLanguage() === $allSites[$this->siteId]->getLanguage(); - $propagate = true; - break; - case PropagationMethod::Custom: - $include = true; + [$include, $propagate] = match ($section->propagationMethod) { + PropagationMethod::None => [$siteSettings->siteId === $this->siteId, true], + PropagationMethod::SiteGroup => [$allSites[$siteSettings->siteId]->groupId === $allSites[$this->siteId]->groupId, true], + PropagationMethod::Language => [$allSites[$siteSettings->siteId]->getLanguage() === $allSites[$this->siteId]->getLanguage(), true], + PropagationMethod::Custom => [ + true, // Only actually propagate to this site if it's the current site, or the entry has been assigned // a status for this site, or the entry already exists for this site - $propagate = ( + ( $siteSettings->siteId === $this->siteId || $this->getEnabledForSite($siteSettings->siteId) !== null || isset($currentSites[$siteSettings->siteId]) - ); - break; - default: - $include = $propagate = true; - break; - } + ), + ], + default => [true, true], + }; if ($include) { $sites[] = [ @@ -1102,18 +1070,17 @@ public function getSupportedSites(): array return $sites; } - /** - * @since 3.5.0 - */ #[Override] protected function cacheTags(): array { + $type = $this->getType(); + $tags = [ - sprintf('entryType:%s', $this->getType()->id), + sprintf('entryType:%s', $type->id), ]; // Did the entry type just change? - if ($this->getType()->id !== $this->_oldTypeId) { + if ($type->id !== $this->_oldTypeId) { $tags[] = "entryType:$this->_oldTypeId"; } @@ -1127,7 +1094,7 @@ protected function cacheTags(): array } /** - * @throws InvalidConfigException if [[siteId]] is not set to a site ID that the entry’s section is enabled for + * @throws RuntimeException if [[siteId]] is not set to a site ID that the entry’s section is enabled for */ public function getUriFormat(): ?string { @@ -1136,13 +1103,13 @@ public function getUriFormat(): ?string } if (! isset($this->sectionId)) { - throw new InvalidConfigException('Either `sectionId` or `fieldId` + `ownerId` must be set on the entry.'); + throw new RuntimeException('Either `sectionId` or `fieldId` + `ownerId` must be set on the entry.'); } $sectionSiteSettings = $this->getSection()->getSiteSettings(); if (! isset($sectionSiteSettings[$this->siteId])) { - throw new InvalidConfigException('Entry’s section ('.$this->sectionId.') is not enabled for site '.$this->siteId); + throw new RuntimeException('Entry’s section ('.$this->sectionId.') is not enabled for site '.$this->siteId); } return $sectionSiteSettings[$this->siteId]->uriFormat; @@ -1151,13 +1118,11 @@ public function getUriFormat(): ?string protected function route(): ?array { // Make sure that the entry is actually live - if (! $this->previewing && $this->getStatus() != self::STATUS_LIVE) { + if (! $this->previewing && $this->getStatus() !== self::STATUS_LIVE) { return null; } - $section = $this->getSection(); - - if (! $section) { + if (! $section = $this->getSection()) { return null; } @@ -1215,7 +1180,6 @@ protected function crumbs(): array return isset($sourceKeys[$key]); }); - /** @var Collection $sectionOptions */ $sectionOptions = $sections ->filter(fn (Section $s) => $s->type !== SectionType::Single) ->map(fn (Section $s) => [ @@ -1259,15 +1223,13 @@ protected function crumbs(): array $ancestors->status(null); } - foreach ($ancestors->all() as $ancestor) { - if ($user->can('view', $ancestor)) { - $crumbs[] = [ - 'html' => app(ElementHtml::class)->elementChipHtml($ancestor, [ - 'class' => 'chromeless', - 'hyperlink' => true, - ]), - ]; - } + foreach ($ancestors->get()->filter(fn ($ancestor) => $user->can('view', $ancestor)) as $ancestor) { + $crumbs[] = [ + 'html' => app(ElementHtml::class)->elementChipHtml($ancestor, [ + 'class' => 'chromeless', + 'hyperlink' => true, + ]), + ]; } } @@ -1314,6 +1276,7 @@ protected function uiLabel(): ?string public function getChipLabelHtml(): string { $html = parent::getChipLabelHtml(); + if ($html !== '') { return $html; } @@ -1410,7 +1373,7 @@ public function getFieldLayout(): ?FieldLayout { try { return $this->getType()->getFieldLayout(); - } catch (InvalidConfigException) { + } catch (RuntimeException) { // The entry type was probably deleted return null; } @@ -1432,7 +1395,7 @@ public function getExpiryDate(): ?DateTime * {% set section = entry.section %} * ``` * - * @throws InvalidConfigException if [[sectionId]] is missing or invalid + * @throws RuntimeException if [[sectionId]] is missing or invalid */ public function getSection(): ?Section { @@ -1440,29 +1403,18 @@ public function getSection(): ?Section return null; } - $section = Sections::getSectionById($this->sectionId); - if (! $section) { - throw new InvalidConfigException("Invalid section ID: $this->sectionId"); + if (! $section = Sections::getSectionById($this->sectionId)) { + throw new RuntimeException("Invalid section ID: $this->sectionId"); } return $section; } - /** - * Returns the entry type ID. - * - * @since 4.0.0 - */ public function getTypeId(): int { return $this->getType()->id; } - /** - * Sets the entry type ID. - * - * @since 4.0.0 - */ public function setTypeId(int $typeId): void { $this->_typeId = $typeId; @@ -1475,23 +1427,18 @@ public function setTypeId(int $typeId): void * * @return EntryType[] * - * @throws InvalidConfigException - * - * @since 3.6.0 + * @throws RuntimeException */ public function getAvailableEntryTypes(bool $triggerEvent = true): array { - if (isset($this->fieldId)) { - /** @var EntryType[] $entryTypes */ - $entryTypes = array_values(array_filter( + $entryTypes = match (true) { + isset($this->fieldId) => array_values(array_filter( $this->getField()->getFieldLayoutProviders(), fn ($provider) => $provider instanceof EntryType, - )); - } elseif (isset($this->sectionId)) { - $entryTypes = $this->getSection()->getEntryTypes(); - } else { - throw new InvalidConfigException('Either `sectionId` or `fieldId` + `ownerId` must be set on the entry.'); - } + )), + isset($this->sectionId) => $this->getSection()->getEntryTypes(), + default => throw new RuntimeException('Either `sectionId` or `fieldId` + `ownerId` must be set on the entry.'), + }; if ($triggerEvent) { event($event = new DefineEntryTypes($this, $entryTypes)); @@ -1518,42 +1465,37 @@ public function getAvailableEntryTypes(bool $triggerEvent = true): array * {% endswitch %} * ``` * - * @throws InvalidConfigException if [[typeId]] is invalid, or the section has no entry types + * @throws RuntimeException if [[typeId]] is invalid, or the section has no entry types */ public function getType(): EntryType { - if (! isset($this->_type)) { - if (isset($this->_typeId)) { - $entryType = Arr::first( - $this->getAvailableEntryTypes(false), - fn (EntryType $entryType) => $entryType->id === $this->_typeId, - ); - if (! $entryType) { - // Maybe the section/field no longer allows this type, - // so get it directly from the Entries service instead - $entryType = EntryTypes::getEntryTypeById($this->_typeId, true); - if (! $entryType) { - throw new InvalidConfigException("Invalid entry type ID: $this->_typeId"); - } - } - } else { - // Default to the section/field's first entry type - $entryType = Arr::first($this->getAvailableEntryTypes()); - if (! $entryType) { - throw new InvalidConfigException('Entry is missing its type ID'); - } + if (isset($this->_type)) { + return $this->_type; + } + + if (isset($this->_typeId)) { + $entryType = Arr::first( + $this->getAvailableEntryTypes(false), + fn (EntryType $entryType) => $entryType->id === $this->_typeId, + ); + + // Maybe the section/field no longer allows this type, + // so get it directly from the Entries service instead + if (! $entryType && ! $entryType = EntryTypes::getEntryTypeById($this->_typeId, true)) { + throw new RuntimeException("Invalid entry type ID: $this->_typeId"); } - $this->_type = $entryType; + + return $this->_type = $entryType; } - return $this->_type; + // Default to the section/field's first entry type + if (! $entryType = Arr::first($this->getAvailableEntryTypes())) { + throw new RuntimeException('Entry is missing its type ID'); + } + + return $this->_type = $entryType; } - /** - * Returns the entry author’s ID. - * - * @since 4.0.0 - */ #[AllowedInSandbox] public function getAuthorId(): ?int { @@ -1564,8 +1506,6 @@ public function getAuthorId(): ?int * Sets the entry author’s ID. * * @param int|array{0:int}|string|null $authorId - * - * @since 4.0.0 */ public function setAuthorId(array|int|string|null $authorId): void { @@ -1577,17 +1517,11 @@ public function setAuthorId(array|int|string|null $authorId): void * Returns the primary entry authors’ IDs. * * @return int[] - * - * @since 5.0.0 */ #[AllowedInSandbox] public function getAuthorIds(): array { - if (! isset($this->_authorIds)) { - $this->_authorIds = array_map(fn (User $author) => $author->id, $this->getAuthors()); - } - - return $this->_authorIds; + return $this->_authorIds ??= array_map(fn (User $author) => $author->id, $this->getAuthors()); } public function getOldAuthorIds(): ?array @@ -1599,17 +1533,13 @@ public function getOldAuthorIds(): ?array * Sets the entry authors’ IDs. * * @param User[]|int[]|string|int|null $authorIds - * - * @since 5.0.0 */ public function setAuthorIds(array|string|int|null $authorIds): void { $authorIds = $this->normalizeAuthorIds($authorIds); - if (isset($this->_authorIds)) { - if ($authorIds === $this->_authorIds) { - return; - } + if (isset($this->_authorIds) && $authorIds === $this->_authorIds) { + return; } $this->_authorIds = $authorIds; @@ -1639,7 +1569,7 @@ private function normalizeAuthorIds(array|string|int|null $authorIds): array *

By {{ entry.author.name }}

* ``` * - * @throws InvalidConfigException if [[authorId]] is set but invalid + * @throws RuntimeException if [[authorId]] is set but invalid */ #[AllowedInSandbox] public function getAuthor(): ?User @@ -1669,46 +1599,50 @@ public function setAuthor(?User $author = null): void * ``` * * @return User[] - * - * @since 5.0.0 */ #[AllowedInSandbox] public function getAuthors(): array { - if (! isset($this->_authors)) { - if (! isset($this->sectionId)) { - $authors = []; - } elseif (isset($this->_authorIds)) { - $authors = User::find() - ->id($this->_authorIds) - ->fixedOrder() - ->status(null) - ->all(); - } else { - if (isset($this->elementQueryResult) && count($this->elementQueryResult) > 1) { - // eager-load authors for all queried entries - Elements::eagerLoadElements(self::class, $this->elementQueryResult, ['authors']); + if (isset($this->_authors)) { + return $this->_authors; + } - return $this->_authors ?? []; - } + if (! isset($this->sectionId)) { + $this->setAuthors([]); - $authors = User::find() - ->authorOf($this) - ->status(null) - ->join( - new Alias(Table::ENTRIES_AUTHORS, 'entries_authors'), - function (JoinClause $join) { - $join->on('entries_authors.authorId', '=', 'users.id') - ->where('entries_authors.entryId', '=', $this->id); - } - ) - ->orderBy('entries_authors.sortOrder') - ->all(); - } + return $this->_authors; + } + + if (isset($this->_authorIds)) { + $this->setAuthors(User::find() + ->id($this->_authorIds) + ->fixedOrder() + ->status(null) + ->all()); + + return $this->_authors; + } + + if (isset($this->elementQueryResult) && count($this->elementQueryResult) > 1) { + // eager-load authors for all queried entries + Elements::eagerLoadElements(self::class, $this->elementQueryResult, ['authors']); - $this->setAuthors($authors); + return []; } + $this->setAuthors(User::find() + ->authorOf($this) + ->status(null) + ->join( + new Alias(Table::ENTRIES_AUTHORS, 'entries_authors'), + function (JoinClause $join) { + $join->on('entries_authors.authorId', '=', 'users.id') + ->where('entries_authors.entryId', '=', $this->id); + } + ) + ->orderBy('entries_authors.sortOrder') + ->all()); + return $this->_authors; } @@ -1716,8 +1650,6 @@ function (JoinClause $join) { * Sets the entry authors. * * @param User[] $authors - * - * @since 5.0.0 */ public function setAuthors(array $authors): void { @@ -1755,8 +1687,6 @@ private function _status(): string * Sets the status, if it’s stored statically. * * @param self::STATUS_LIVE|self::STATUS_PENDING|self::STATUS_EXPIRED $status - * - * @since 5.7.0 */ public function setStatus(string $status): void { @@ -1776,11 +1706,10 @@ public function createAnother(): self 'authorIds' => $this->getAuthorIds(), ]); - $section = $this->getSection(); - if ($section) { + if ($section = $this->getSection()) { // Set the default status based on the section's settings /** @var SectionSiteSettings $siteSettings */ - $siteSettings = Collection::make($section->getSiteSettings())->firstWhere('siteId', $this->siteId); + $siteSettings = collect($section->getSiteSettings())->firstWhere('siteId', $this->siteId); $enabled = $siteSettings->enabledByDefault; } else { $enabled = true; @@ -1805,8 +1734,7 @@ public function createAnother(): self #[Override] public function hasRevisions(): bool { - $section = $this->getSection(); - if ($section) { + if ($section = $this->getSection()) { return $section->enableVersioning; } @@ -1817,9 +1745,7 @@ public function hasRevisions(): bool protected function cpEditUrl(): string { - $section = $this->getSection(); - - if (! $section) { + if (! $section = $this->getSection()) { // use the generic element editor URL return ElementHelper::elementEditorUrl($this, false); } @@ -1910,14 +1836,13 @@ protected function safeActionMenuItems(): array // Field settings if ( ! empty($this->fieldId) && - Craft::$app->controller instanceof ElementsController && - Craft::$app->controller->element === $this + app(ElementRequest::class)->element === $this ) { $fieldEditId = sprintf('edit-field-%s', mt_rand()); $actions[] = [ 'id' => $fieldEditId, 'icon' => 'gear', - 'label' => Craft::t('app', 'Field settings'), + 'label' => t('Field settings'), ]; HtmlStack::jsWithVars(fn ($id, $params) => << 'chromeless', 'showThumb' => $this->viewMode !== 'cards', ]); - } catch (InvalidConfigException) { + } catch (RuntimeException) { return t('Unknown'); } default: @@ -2055,21 +1977,16 @@ protected function htmlAttributes(string $context): array /** * Returns whether the given user is authorized to move this entry to a different section. - * - * @since 5.9.14 */ public function canMove(?User $user = null): bool { + $user ??= Auth::user(); + if (! $user) { - $user = Auth::user(); - if (! $user) { - return false; - } + return false; } - $section = $this->getSection(); - - if (! $section) { + if (! $section = $this->getSection()) { return false; } @@ -2084,18 +2001,14 @@ public function canMove(?User $user = null): bool } if ($this->getIsDraft()) { - return - $this->draftCreatorId === $user->id || - $user->can("savePeerEntryDrafts:$section->uid"); + return $this->draftCreatorId === $user->id || $user->can("savePeerEntryDrafts:$section->uid"); } if (! $user->can("saveEntries:$section->uid")) { return false; } - return - in_array($user->id, $this->getAuthorIds(), true) || - $user->can("savePeerEntries:$section->uid"); + return in_array($user->id, $this->getAuthorIds(), true) || $user->can("savePeerEntries:$section->uid"); } /** @@ -2108,17 +2021,12 @@ private function _moveCompatibleSectionsCount(): int // get sections all editable sections without singles and without the section this entry belongs to // get all entry types for them - $sections = Sections::getEditableSections() + return Sections::getEditableSections() ->filter(fn (Section $s) => $s->type !== SectionType::Single && $s->id !== $this->sectionId) - ->map(fn (Section $s) => [ - 'entryTypes' => $s->getEntryTypes(), - ]); - - // get sections that use the same entry type as this entry - $compatibleSections = $sections - ->filter(fn (array $s) => collect($s['entryTypes'])->contains('id', $entryTypeId)); - - return $compatibleSections->count(); + ->map(fn (Section $s) => ['entryTypes' => $s->getEntryTypes()]) + // get sections that use the same entry type as this entry + ->filter(fn (array $s) => collect($s['entryTypes'])->contains('id', $entryTypeId)) + ->count(); } #[Override] @@ -2133,7 +2041,7 @@ public function metaFieldsHtml(bool $static): string // Type $fields['type'] = (function () use ($static) { $entryTypes = $this->getAvailableEntryTypes(); - if (Collection::make($entryTypes)->doesntContain(fn (EntryType $entryType) => $entryType->id === $this->typeId)) { + if (collect($entryTypes)->doesntContain(fn (EntryType $entryType) => $entryType->id === $this->typeId)) { $entryTypes[] = $this->getType(); } if (count($entryTypes) <= 1 && $this->isEntryTypeAllowed($entryTypes)) { @@ -2270,8 +2178,6 @@ public function metaFieldsHtml(bool $static): string /** * Checks if the "Apply Draft" and "Revert to a revision" buttons should be disabled and if so * applies the tooltip message. - * - * @throws InvalidConfigException */ private function _applyActionBtnEntryTypeCompatibility(): void { @@ -2330,7 +2236,7 @@ public function showStatusField(): bool { try { $showStatusField = $this->getType()->showStatusField; - } catch (InvalidConfigException) { + } catch (RuntimeException) { $showStatusField = true; } @@ -2383,8 +2289,6 @@ private function _parentOptionCriteria(Section $section): array /** * Updates the entry’s title, if its entry type has a dynamic title format. - * - * @since 3.0.3 */ public function updateTitle(): void { @@ -2430,9 +2334,6 @@ private function _userPostDate(): ?DateTime // Events // ------------------------------------------------------------------------- - /** - * @throws Exception if reasons - */ #[Override] public function beforeSave(bool $isNew): bool { @@ -2463,8 +2364,7 @@ public function beforeSave(bool $isNew): bool } } - $section = $this->getSection(); - if ($section) { + if ($section = $this->getSection()) { // Set the structure ID for Element::attributes() and afterSave() if ($section->type === SectionType::Structure) { $this->structureId = $section->structureId; @@ -2479,7 +2379,7 @@ public function beforeSave(bool $isNew): bool ]); if (! $parentEntry) { - throw new InvalidConfigException("Invalid parent ID: $parentId"); + throw new RuntimeException("Invalid parent ID: $parentId"); } } else { $parentEntry = null; @@ -2514,20 +2414,18 @@ private function maybeSetDefaultAttributes(): void } $section = $this->getSection(); - if ( - $section?->type !== SectionType::Single && - $section?->maxAuthors !== 0 && - empty($this->getAuthors()) + if ($section?->type !== SectionType::Single + && $section?->maxAuthors !== 0 + && empty($this->getAuthors()) + && $user = Auth::user() ) { - if ($user = Auth::user()) { - $this->setAuthor($user); - } + $this->setAuthor($user); } if ( ! $this->_userPostDate() && ( - in_array($this->scenario, [ElementRules::SCENARIO_LIVE, ElementRules::SCENARIO_DEFAULT]) || + $this->ruleset->inScenarios(ElementRules::SCENARIO_LIVE, ElementRules::SCENARIO_DEFAULT) || ! $this->getIsDraft() ) ) { @@ -2542,9 +2440,6 @@ private function maybeSetDefaultAttributes(): void } } - /** - * @throws InvalidConfigException - */ #[Override] public function afterSave(bool $isNew): void { @@ -2594,7 +2489,7 @@ public function afterSave(bool $isNew): void if ( (! $this->duplicateOf || $this->updatingFromDerivative || $this->placeInStructure) && isset($this->sectionId) && - $section->type == SectionType::Structure + $section->type === SectionType::Structure ) { // Has the parent changed? if ($this->placeInStructure || $this->hasNewParent()) { @@ -2611,12 +2506,6 @@ public function afterSave(bool $isNew): void parent::afterSave($isNew); } - /** - * Save authors - * - * @throws Throwable - * @throws \yii\db\Exception - */ private function _saveAuthors(): void { if (! isset($this->_oldAuthorIds)) { @@ -2663,7 +2552,7 @@ private function _placeInStructure(bool $isNew, Section $section): void ->unique() ->value('id'); - if ($parentId == $canonicalParentId) { + if ($parentId === $canonicalParentId) { Structures::remove($this->structureId, $this); return; @@ -2673,17 +2562,13 @@ private function _placeInStructure(bool $isNew, Section $section): void $mode = $isNew ? Mode::Insert : Mode::Auto; if (! $parentId) { - if ($section->defaultPlacement === DefaultPlacement::Beginning) { - Structures::prependToRoot($this->structureId, $this, $mode); - } else { - Structures::appendToRoot($this->structureId, $this, $mode); - } + $section->defaultPlacement === DefaultPlacement::Beginning + ? Structures::prependToRoot($this->structureId, $this, $mode) + : Structures::appendToRoot($this->structureId, $this, $mode); } else { - if ($section->defaultPlacement === DefaultPlacement::Beginning) { - Structures::prepend($this->structureId, $this, $this->getParent(), $mode); - } else { - Structures::append($this->structureId, $this, $this->getParent(), $mode); - } + $section->defaultPlacement === DefaultPlacement::Beginning + ? Structures::prepend($this->structureId, $this, $this->getParent(), $mode) + : Structures::append($this->structureId, $this, $this->getParent(), $mode); } } @@ -2745,6 +2630,7 @@ public function afterRestore(): void ]); $section = $this->getSection(); + if ($section?->type === SectionType::Structure) { // Add the entry back into its structure /** @var self|null $parent */ @@ -2754,11 +2640,9 @@ public function afterRestore(): void ->where('j.id', $this->id) ->first(); - if (! $parent) { - Structures::appendToRoot($section->structureId, $this); - } else { - Structures::append($section->structureId, $this, $parent); - } + $parent + ? Structures::append($section->structureId, $this, $parent) + : Structures::appendToRoot($section->structureId, $this); } parent::afterRestore(); @@ -2770,7 +2654,7 @@ public function afterMoveInStructure(int $structureId): void // Was the entry moved within its section's structure? $section = $this->getSection(); - if ($section->type === SectionType::Structure && $section->structureId == $structureId) { + if ($section->type === SectionType::Structure && $section->structureId === $structureId) { Elements::updateElementSlugAndUri( element: $this, updateOtherSites: true, @@ -2815,10 +2699,6 @@ private function _shouldSaveRevision(): bool /** * Returns whether the entry’s type is allowed in its section. - * - * @throws InvalidConfigException - * - * @since 5.3.0 */ public function isEntryTypeCompatible(): bool { @@ -2829,7 +2709,7 @@ public function isEntryTypeCompatible(): bool return true; } - $sectionEntryTypes = Collection::make($section->getEntryTypes()) + $sectionEntryTypes = collect($section->getEntryTypes()) ->map(fn (EntryType $et) => $et->id) ->all(); @@ -2839,8 +2719,6 @@ public function isEntryTypeCompatible(): bool /** * Check if current typeId is in the array of passed in entry types. * If no entry types are passed, check get all the available ones. - * - * @throws InvalidConfigException */ public function isEntryTypeAllowed(?array $entryTypes = null): bool { @@ -2853,8 +2731,7 @@ public function isEntryTypeAllowed(?array $entryTypes = null): bool private function handleChangedTypeId(): void { - $oldLayout = EntryTypes::getEntryTypeById($this->_oldTypeId)?->getFieldLayout(); - if (! $oldLayout) { + if (! $oldLayout = EntryTypes::getEntryTypeById($this->_oldTypeId)?->getFieldLayout()) { return; } @@ -2887,6 +2764,7 @@ protected function partialTemplatePathCandidates(): array $templates = parent::partialTemplatePathCandidates(); $entryType = $this->getType(); + if (isset($entryType->original) && $entryType->original->handle !== $entryType->handle) { $templates[] = [ 'template' => sprintf( diff --git a/src/Field/Addresses.php b/src/Field/Addresses.php index a8eaff5f3c0..067c33cee50 100644 --- a/src/Field/Addresses.php +++ b/src/Field/Addresses.php @@ -5,13 +5,14 @@ namespace CraftCms\Cms\Field; use Closure; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Database\Table as DbTable; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementCollection; +use CraftCms\Cms\Element\Enums\ElementIndexViewMode; use CraftCms\Cms\Element\NestedElementManager; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; @@ -21,7 +22,6 @@ use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; use CraftCms\Cms\Field\Contracts\MergeableFieldInterface; -use CraftCms\Cms\Field\Enums\ElementIndexViewMode; use CraftCms\Cms\Field\Enums\TranslationMethod; use CraftCms\Cms\Field\Exceptions\InvalidFieldException; use CraftCms\Cms\Gql\Arguments\Elements\Address as AddressArguments; @@ -42,8 +42,8 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use Override; +use RuntimeException; use Tpetry\QueryExpressions\Language\Alias; -use yii\base\InvalidConfigException; use function CraftCms\Cms\craftAsset; use function CraftCms\Cms\t; @@ -218,7 +218,7 @@ public function getSupportedSitesForElement(NestedElementInterface $element): ar { try { $owner = $element->getOwner(); - } catch (InvalidConfigException) { + } catch (RuntimeException) { $owner = $element->duplicateOf; } @@ -574,7 +574,7 @@ public function getTranslationDescription(?ElementInterface $element): ?string } /** - * @throws InvalidConfigException + * @throws RuntimeException */ #[Override] protected function inputHtml(mixed $value, ?ElementInterface $element, bool $inline): string diff --git a/src/Field/Assets.php b/src/Field/Assets.php index 27fc80b734b..e659955e862 100644 --- a/src/Field/Assets.php +++ b/src/Field/Assets.php @@ -5,7 +5,6 @@ namespace CraftCms\Cms\Field; use Closure; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\AssetsHelper; use CraftCms\Cms\Asset\Data\Volume; use CraftCms\Cms\Asset\Data\VolumeFolder; @@ -15,6 +14,7 @@ use CraftCms\Cms\Cms; use CraftCms\Cms\Cp\Html\PreviewHtml; use CraftCms\Cms\Element\Conditions\ElementCondition; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Queries\AssetQuery; diff --git a/src/Field/BaseOptionsField.php b/src/Field/BaseOptionsField.php index f6edf0f9ec9..46f9f4c07e2 100644 --- a/src/Field/BaseOptionsField.php +++ b/src/Field/BaseOptionsField.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Cp\Icons; use CraftCms\Cms\Database\QueryParam; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Conditions\OptionsFieldConditionRule; use CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface; use CraftCms\Cms\Field\Contracts\MergeableFieldInterface; diff --git a/src/Field/BaseRelationField.php b/src/Field/BaseRelationField.php index 565bc03a799..64fb6c22a32 100644 --- a/src/Field/BaseRelationField.php +++ b/src/Field/BaseRelationField.php @@ -6,8 +6,6 @@ use Closure; use Craft; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; use CraftCms\Cms\Condition\Contracts\ConditionInterface; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Cp\Html\ElementHtml; @@ -18,6 +16,8 @@ use CraftCms\Cms\Database\Table; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; use CraftCms\Cms\Element\Conditions\ElementCondition; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementCollection; @@ -62,8 +62,8 @@ use Illuminate\Support\Facades\Validator as ValidatorFacade; use Illuminate\Validation\Validator; use Override; +use RuntimeException; use Tpetry\QueryExpressions\Language\Alias; -use yii\base\InvalidConfigException; use yii\db\Schema; use function CraftCms\Cms\craftAsset; @@ -456,12 +456,15 @@ public function getRules(): array ]); } + public function afterValidate(?Validator $validator = null): void + { + $this->validateSources(); + } + /** * Ensure only one structured source is selected when maintainHierarchy is true. - * - * @todo This needs to be called from somewhere */ - public function validateSources(string $attribute): void + public function validateSources(): void { if (! $this->maintainHierarchy) { return; @@ -1170,7 +1173,7 @@ public function getRelationTargetIds(ElementInterface $element): array is_array($value->id) && Arr::isNumeric($value->id) ) { - $targetIds = $value->id ?: []; + $targetIds = $value->id; } elseif ( $value instanceof ElementQuery && ($where = $value->getWhereForColumn('elements.id')) !== null && @@ -1368,8 +1371,9 @@ public function getViewModeFieldHtml(): ?string 'width' => $key === self::VIEW_MODE_LIST ? 48 : 80, 'height' => 60, ]). - Html::radio('viewMode', $key === $this->viewMode, [ + Html::radio('viewMode', $key, [ 'value' => $key, + 'checked' => $this->viewMode === $key, ]). ' '.$label. Html::endTag('label'); @@ -1480,7 +1484,7 @@ protected function inputTemplateVariables( if ($el) { $disabledElementIds[] = $el->getCanonicalId(); } - } catch (InvalidConfigException) { + } catch (RuntimeException) { break; } } while ($el instanceof NestedElementInterface); diff --git a/src/Field/ButtonGroup.php b/src/Field/ButtonGroup.php index 0d9a6da1511..41cb7a1f51e 100644 --- a/src/Field/ButtonGroup.php +++ b/src/Field/ButtonGroup.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Contracts\SortableFieldInterface; use CraftCms\Cms\Field\Data\SingleOptionFieldData; use CraftCms\Cms\Support\Facades\DeltaRegistry; diff --git a/src/Field/Checkboxes.php b/src/Field/Checkboxes.php index fa9e1fdd42a..8755f4661c6 100644 --- a/src/Field/Checkboxes.php +++ b/src/Field/Checkboxes.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Data\OptionData; use CraftCms\Cms\Support\Facades\DeltaRegistry; use Illuminate\Support\Collection; diff --git a/src/Field/Color.php b/src/Field/Color.php index 96bac95a502..077f5ecbdd4 100644 --- a/src/Field/Color.php +++ b/src/Field/Color.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface; use CraftCms\Cms\Field\Contracts\InlineEditableFieldInterface; diff --git a/src/Field/Concerns/RelationalField.php b/src/Field/Concerns/RelationalField.php index d8913e354e8..ca0f54b0201 100644 --- a/src/Field/Concerns/RelationalField.php +++ b/src/Field/Concerns/RelationalField.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field\Concerns; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * RelationalFieldTrait provides a base implementation for {@see \CraftCms\Cms\Field\Contracts\RelationalFieldInterface}. diff --git a/src/Field/Conditions/CountryFieldConditionRule.php b/src/Field/Conditions/CountryFieldConditionRule.php index 4e3204d9ba9..2a70eb67e93 100644 --- a/src/Field/Conditions/CountryFieldConditionRule.php +++ b/src/Field/Conditions/CountryFieldConditionRule.php @@ -8,7 +8,7 @@ use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Field\Conditions\Contracts\FieldConditionRuleInterface; use CraftCms\Cms\Field\Country; -use yii\base\InvalidConfigException; +use RuntimeException; class CountryFieldConditionRule extends BaseMultiSelectConditionRule implements FieldConditionRuleInterface { @@ -23,7 +23,7 @@ protected function options(): array protected function inputHtml(): string { if (! $this->field() instanceof Country) { - throw new InvalidConfigException; + throw new RuntimeException; } return parent::inputHtml(); diff --git a/src/Field/Conditions/DateFieldConditionRule.php b/src/Field/Conditions/DateFieldConditionRule.php index cc19e299379..dd63fbac9c7 100644 --- a/src/Field/Conditions/DateFieldConditionRule.php +++ b/src/Field/Conditions/DateFieldConditionRule.php @@ -8,7 +8,7 @@ use CraftCms\Cms\Field\Conditions\Contracts\FieldConditionRuleInterface; use CraftCms\Cms\Field\Date; use DateTime; -use yii\base\InvalidConfigException; +use RuntimeException; class DateFieldConditionRule extends BaseDateRangeConditionRule implements FieldConditionRuleInterface { @@ -18,7 +18,7 @@ class DateFieldConditionRule extends BaseDateRangeConditionRule implements Field protected function inputHtml(): string { if (! $this->field() instanceof Date) { - throw new InvalidConfigException; + throw new RuntimeException; } return parent::inputHtml(); diff --git a/src/Field/Conditions/EmptyFieldConditionRule.php b/src/Field/Conditions/EmptyFieldConditionRule.php index 8951a33e71e..819e039af9e 100644 --- a/src/Field/Conditions/EmptyFieldConditionRule.php +++ b/src/Field/Conditions/EmptyFieldConditionRule.php @@ -5,11 +5,11 @@ namespace CraftCms\Cms\Field\Conditions; use craft\base\conditions\BaseConditionRule; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Conditions\Contracts\FieldConditionRuleInterface; use CraftCms\Cms\Field\Exceptions\InvalidFieldException; -use yii\base\InvalidConfigException; -use yii\base\NotSupportedException; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; +use RuntimeException; class EmptyFieldConditionRule extends BaseConditionRule implements FieldConditionRuleInterface { @@ -31,7 +31,7 @@ public function matchElement(ElementInterface $element): bool { try { $field = $this->field(); - } catch (InvalidConfigException) { + } catch (RuntimeException) { // The field doesn't exist return true; } @@ -57,7 +57,7 @@ protected function elementQueryParam(): int|string|null return match ($this->operator) { self::OPERATOR_EMPTY => ':empty:', self::OPERATOR_NOT_EMPTY => 'not :empty:', - default => throw new InvalidConfigException("Invalid operator: $this->operator"), + default => throw new RuntimeException("Invalid operator: $this->operator"), }; } diff --git a/src/Field/Conditions/FieldConditionRuleTrait.php b/src/Field/Conditions/FieldConditionRuleTrait.php index 050a8ca15f6..89121f38f53 100644 --- a/src/Field/Conditions/FieldConditionRuleTrait.php +++ b/src/Field/Conditions/FieldConditionRuleTrait.php @@ -4,12 +4,12 @@ namespace CraftCms\Cms\Field\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; use Illuminate\Contracts\Database\Query\Builder; use Illuminate\Support\Facades\Auth; -use yii\base\InvalidConfigException; +use RuntimeException; use function CraftCms\Cms\t; @@ -66,7 +66,7 @@ public function setLayoutElementUid(?string $uid): void * * @return FieldInterface[] * - * @throws InvalidConfigException if [[fieldUid]] or [[layoutElementUid]] are invalid + * @throws RuntimeException if [[fieldUid]] or [[layoutElementUid]] are invalid */ protected function fieldInstances(): array { @@ -75,7 +75,7 @@ protected function fieldInstances(): array } if (! isset($this->_fieldUid)) { - throw new InvalidConfigException('No field UUID set on the field condition rule yet.'); + throw new RuntimeException('No field UUID set on the field condition rule yet.'); } // Loop through all the layout's fields, and look for the selected field instance @@ -117,11 +117,11 @@ protected function fieldInstances(): array if (empty($this->_fieldInstances)) { if (! isset($this->_layoutElementUid)) { - throw new InvalidConfigException("Field $this->_fieldUid is not included in the available field layouts."); + throw new RuntimeException("Field $this->_fieldUid is not included in the available field layouts."); } if (empty($potentialInstances)) { - throw new InvalidConfigException("Invalid field layout element UUID: $this->_layoutElementUid"); + throw new RuntimeException("Invalid field layout element UUID: $this->_layoutElementUid"); } // Just go with the first one @@ -146,7 +146,7 @@ protected function fieldInstances(): array /** * Returns the first custom field instance associated with this rule. * - * @throws InvalidConfigException if [[fieldUid]] or [[layoutElementUid]] are invalid + * @throws RuntimeException if [[fieldUid]] or [[layoutElementUid]] are invalid */ protected function field(): FieldInterface { @@ -165,7 +165,7 @@ public function getLabel(): string { $instances = $this->fieldInstances(); if (empty($instances)) { - throw new InvalidConfigException('No field instances for this condition rule.'); + throw new RuntimeException('No field instances for this condition rule.'); } return $instances[0]->layoutElement->label(); @@ -185,7 +185,7 @@ public function getExclusiveQueryParams(): array { try { $instances = $this->fieldInstances(); - } catch (InvalidConfigException) { + } catch (RuntimeException) { return []; } @@ -219,7 +219,7 @@ public function matchElement(ElementInterface $element): bool { try { $fieldInstances = $this->fieldInstances(); - } catch (InvalidConfigException) { + } catch (RuntimeException) { // The field doesn't exist return true; } diff --git a/src/Field/Conditions/GeneratedFieldConditionRule.php b/src/Field/Conditions/GeneratedFieldConditionRule.php index 6cdd8b13b63..ce39518a2de 100644 --- a/src/Field/Conditions/GeneratedFieldConditionRule.php +++ b/src/Field/Conditions/GeneratedFieldConditionRule.php @@ -4,13 +4,13 @@ namespace CraftCms\Cms\Field\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Database\Expressions\JsonExtract; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; -use yii\base\InvalidConfigException; +use RuntimeException; use yii\db\Schema; use function CraftCms\Cms\t; @@ -41,7 +41,7 @@ public function getLabel(): string { $field = $this->getFieldConfig(); if (! $field) { - throw new InvalidConfigException("Invalid generated field UUID: $this->fieldUid"); + throw new RuntimeException("Invalid generated field UUID: $this->fieldUid"); } return $field['name']; diff --git a/src/Field/Conditions/LightswitchFieldConditionRule.php b/src/Field/Conditions/LightswitchFieldConditionRule.php index 9a851ad3d32..7bb27644afe 100644 --- a/src/Field/Conditions/LightswitchFieldConditionRule.php +++ b/src/Field/Conditions/LightswitchFieldConditionRule.php @@ -7,7 +7,7 @@ use CraftCms\Cms\Condition\BaseLightswitchConditionRule; use CraftCms\Cms\Field\Conditions\Contracts\FieldConditionRuleInterface; use CraftCms\Cms\Field\Lightswitch; -use yii\base\InvalidConfigException; +use RuntimeException; class LightswitchFieldConditionRule extends BaseLightswitchConditionRule implements FieldConditionRuleInterface { @@ -17,7 +17,7 @@ class LightswitchFieldConditionRule extends BaseLightswitchConditionRule impleme protected function inputHtml(): string { if (! $this->field() instanceof Lightswitch) { - throw new InvalidConfigException; + throw new RuntimeException; } return parent::inputHtml(); diff --git a/src/Field/Conditions/MoneyFieldConditionRule.php b/src/Field/Conditions/MoneyFieldConditionRule.php index d56e171ef05..cf9860bc4ec 100644 --- a/src/Field/Conditions/MoneyFieldConditionRule.php +++ b/src/Field/Conditions/MoneyFieldConditionRule.php @@ -14,7 +14,7 @@ use Money\Currency; use Money\Money as MoneyLibrary; use Override; -use yii\base\InvalidConfigException; +use RuntimeException; use function CraftCms\Cms\t; @@ -41,7 +41,7 @@ public function setAttributes($values, $safeOnly = true): void $field = $this->field(); if (! $field instanceof Money) { - throw new InvalidConfigException; + throw new RuntimeException; } if (isset($value, $this->_fieldUid)) { @@ -66,7 +66,7 @@ protected function inputHtml(): string $field = $this->field(); if (! $field instanceof Money) { - throw new InvalidConfigException; + throw new RuntimeException; } // don't show the value input if the condition checks for empty/notempty diff --git a/src/Field/Conditions/OptionsFieldConditionRule.php b/src/Field/Conditions/OptionsFieldConditionRule.php index 31945e09f07..28fd763629d 100644 --- a/src/Field/Conditions/OptionsFieldConditionRule.php +++ b/src/Field/Conditions/OptionsFieldConditionRule.php @@ -11,7 +11,7 @@ use CraftCms\Cms\Field\Data\OptionData; use CraftCms\Cms\Field\Data\SingleOptionFieldData; use Illuminate\Support\Collection; -use yii\base\InvalidConfigException; +use RuntimeException; class OptionsFieldConditionRule extends BaseMultiSelectConditionRule implements FieldConditionRuleInterface { @@ -43,7 +43,7 @@ protected function options(): array protected function inputHtml(): string { if (! $this->field() instanceof BaseOptionsField) { - throw new InvalidConfigException; + throw new RuntimeException; } return parent::inputHtml(); diff --git a/src/Field/Conditions/RelationalFieldConditionRule.php b/src/Field/Conditions/RelationalFieldConditionRule.php index cf77e7a7079..eb8402d88cf 100644 --- a/src/Field/Conditions/RelationalFieldConditionRule.php +++ b/src/Field/Conditions/RelationalFieldConditionRule.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Field\Conditions; -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseElementSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Field\BaseRelationField; @@ -14,7 +14,7 @@ use CraftCms\Cms\FieldLayout\LayoutElements\BaseField; use CraftCms\Cms\FieldLayout\LayoutElements\CustomField; use Illuminate\Database\Query\Builder; -use yii\base\InvalidConfigException; +use RuntimeException; use function CraftCms\Cms\t; @@ -93,7 +93,7 @@ protected function operatorLabel(string $operator): string protected function inputHtml(): string { if (! $this->field() instanceof BaseRelationField) { - throw new InvalidConfigException; + throw new RuntimeException; } return match ($this->operator) { diff --git a/src/Field/ContentBlock.php b/src/Field/ContentBlock.php index 8b799244bca..241e04e8d0a 100644 --- a/src/Field/ContentBlock.php +++ b/src/Field/ContentBlock.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Data\EagerLoadPlan; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\Element; @@ -42,7 +42,7 @@ use Illuminate\Validation\Validator; use InvalidArgumentException; use Override; -use yii\base\InvalidConfigException; +use RuntimeException; use function CraftCms\Cms\craftAsset; use function CraftCms\Cms\t; @@ -261,7 +261,7 @@ public function getSupportedSitesForElement(NestedElementInterface $element): ar { try { $owner = $element->getOwner(); - } catch (InvalidConfigException) { + } catch (RuntimeException) { $owner = $element->duplicateOf; } diff --git a/src/Field/Contracts/CrossSiteCopyableFieldInterface.php b/src/Field/Contracts/CrossSiteCopyableFieldInterface.php index d30b56bc53e..7676c92f672 100644 --- a/src/Field/Contracts/CrossSiteCopyableFieldInterface.php +++ b/src/Field/Contracts/CrossSiteCopyableFieldInterface.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field\Contracts; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * CrossSiteCopyableFieldInterface defines the common interface to be implemented by field classes diff --git a/src/Field/Contracts/EagerLoadingFieldInterface.php b/src/Field/Contracts/EagerLoadingFieldInterface.php index a53bd3c39dd..646d86535ef 100644 --- a/src/Field/Contracts/EagerLoadingFieldInterface.php +++ b/src/Field/Contracts/EagerLoadingFieldInterface.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field\Contracts; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * EagerLoadingFieldInterface defines the common interface to be implemented by field classes that support eager-loading. diff --git a/src/Field/Contracts/ElementContainerFieldInterface.php b/src/Field/Contracts/ElementContainerFieldInterface.php index 731b2dd3387..d490c0e63d1 100644 --- a/src/Field/Contracts/ElementContainerFieldInterface.php +++ b/src/Field/Contracts/ElementContainerFieldInterface.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field\Contracts; -use craft\base\NestedElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\FieldLayout\Contracts\FieldLayoutProviderInterface; use CraftCms\Cms\User\Elements\User; diff --git a/src/Field/Contracts/FieldInterface.php b/src/Field/Contracts/FieldInterface.php index c45b6a6ac5a..511b8a12ab3 100644 --- a/src/Field/Contracts/FieldInterface.php +++ b/src/Field/Contracts/FieldInterface.php @@ -4,13 +4,13 @@ namespace CraftCms\Cms\Field\Contracts; -use craft\base\ElementInterface; use CraftCms\Cms\Component\Concerns\SavableComponent; use CraftCms\Cms\Component\Contracts\Chippable; use CraftCms\Cms\Component\Contracts\ConfigurableComponentInterface; use CraftCms\Cms\Component\Contracts\CpEditable; use CraftCms\Cms\Component\Contracts\Grippable; use CraftCms\Cms\Component\Contracts\SavableComponentInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Enums\AttributeStatus; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Field\Enums\TranslationMethod; diff --git a/src/Field/Contracts/InlineEditableFieldInterface.php b/src/Field/Contracts/InlineEditableFieldInterface.php index bc746cc51a1..510bee443fd 100644 --- a/src/Field/Contracts/InlineEditableFieldInterface.php +++ b/src/Field/Contracts/InlineEditableFieldInterface.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field\Contracts; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * InlineEditableFieldInterface defines the common interface to be implemented by field classes diff --git a/src/Field/Contracts/PreviewableFieldInterface.php b/src/Field/Contracts/PreviewableFieldInterface.php index 1c50d34469b..11fd7a7841a 100644 --- a/src/Field/Contracts/PreviewableFieldInterface.php +++ b/src/Field/Contracts/PreviewableFieldInterface.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field\Contracts; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * PreviewableFieldInterface defines the common interface to be implemented by field classes diff --git a/src/Field/Contracts/RelationalFieldInterface.php b/src/Field/Contracts/RelationalFieldInterface.php index 285a776c676..d070284bf3c 100644 --- a/src/Field/Contracts/RelationalFieldInterface.php +++ b/src/Field/Contracts/RelationalFieldInterface.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field\Contracts; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * RelationalFieldInterface defines the common interface to be implemented by field classes diff --git a/src/Field/Contracts/ThumbableFieldInterface.php b/src/Field/Contracts/ThumbableFieldInterface.php index c44a6f1599a..07a713bba56 100644 --- a/src/Field/Contracts/ThumbableFieldInterface.php +++ b/src/Field/Contracts/ThumbableFieldInterface.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field\Contracts; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * ThumbableFieldInterface defines the common interface to be implemented by field classes diff --git a/src/Field/Country.php b/src/Field/Country.php index 8a817aef364..5d6d674885e 100644 --- a/src/Field/Country.php +++ b/src/Field/Country.php @@ -6,9 +6,9 @@ use CommerceGuys\Addressing\Country\Country as CountryModel; use CommerceGuys\Addressing\Exception\UnknownCountryException; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Addresses; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Conditions\CountryFieldConditionRule; use CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface; use CraftCms\Cms\Field\Contracts\InlineEditableFieldInterface; diff --git a/src/Field/Data/JsonData.php b/src/Field/Data/JsonData.php index 75f07265235..43162c55f15 100644 --- a/src/Field/Data/JsonData.php +++ b/src/Field/Data/JsonData.php @@ -13,11 +13,12 @@ use CraftCms\Cms\Support\Json; use CraftCms\Cms\Twig\Attributes\AllowedInSandbox; use IteratorAggregate; +use Override; use Stringable; use Traversable; #[AllowedInSandbox] -class JsonData extends Component implements ArrayAccess, IteratorAggregate, Serializable, Stringable +class JsonData extends Component implements IteratorAggregate, Serializable, Stringable { public function __construct( private mixed $value, @@ -31,7 +32,7 @@ public function __toString(): string return $this->getJson(); } - #[\Override] + #[Override] public function __call($method, $parameters) { try { @@ -81,16 +82,19 @@ public function getJson(bool $pretty = false, string $indent = ' '): string return $json; } + #[Override] public function offsetGet(mixed $offset): mixed { return $this->value[$offset]; } + #[Override] public function offsetSet(mixed $offset, mixed $value): void { $this->value[$offset] = $value; } + #[Override] public function offsetExists(mixed $offset): bool { if (is_string($this->value)) { @@ -108,6 +112,7 @@ public function offsetExists(mixed $offset): bool return false; } + #[Override] public function offsetUnset(mixed $offset): void { unset($this->value[$offset]); diff --git a/src/Field/Data/LinkData.php b/src/Field/Data/LinkData.php index 398c7903602..5bcaee9d894 100644 --- a/src/Field/Data/LinkData.php +++ b/src/Field/Data/LinkData.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Field\Data; -use craft\base\ElementInterface; use craft\base\Serializable; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Field\LinkTypes\BaseElementLinkType; use CraftCms\Cms\Field\LinkTypes\BaseLinkType; diff --git a/src/Field/Date.php b/src/Field/Date.php index e4551428e7f..b3abf630b67 100644 --- a/src/Field/Date.php +++ b/src/Field/Date.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Field\Conditions\DateFieldConditionRule; use CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface; diff --git a/src/Field/Dropdown.php b/src/Field/Dropdown.php index bc0b6fd8167..3fa93b6779f 100644 --- a/src/Field/Dropdown.php +++ b/src/Field/Dropdown.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Enums\AttributeStatus; use CraftCms\Cms\Field\Contracts\InlineEditableFieldInterface; use CraftCms\Cms\Field\Contracts\SortableFieldInterface; diff --git a/src/Field/Elements/ContentBlock.php b/src/Field/Elements/ContentBlock.php index 459a3627dd2..68f0005f2c3 100644 --- a/src/Field/Elements/ContentBlock.php +++ b/src/Field/Elements/ContentBlock.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Field\Elements; -use craft\base\NestedElementInterface; use craft\base\NestedElementTrait; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Queries\ContentBlockQuery; use CraftCms\Cms\Field\ContentBlock as ContentBlockField; diff --git a/src/Field/Email.php b/src/Field/Email.php index c5678f649df..0994daf0e4f 100644 --- a/src/Field/Email.php +++ b/src/Field/Email.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Field\Conditions\TextFieldConditionRule; use CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface; diff --git a/src/Field/Entries.php b/src/Field/Entries.php index 1ac2d6ab08e..940eaa8cfdc 100644 --- a/src/Field/Entries.php +++ b/src/Field/Entries.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\Html\ElementHtml; use CraftCms\Cms\Element\Conditions\ElementCondition; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\EntryQuery; diff --git a/src/Field/Enums/ElementIndexViewMode.php b/src/Field/Enums/ElementIndexViewMode.php deleted file mode 100644 index 90957ae2f38..00000000000 --- a/src/Field/Enums/ElementIndexViewMode.php +++ /dev/null @@ -1,16 +0,0 @@ -entryTypes)) { - throw new InvalidConfigException('At least one entry type is required.'); + throw new RuntimeException('At least one entry type is required.'); } return array_values($event->entryTypes); @@ -456,7 +456,7 @@ public function getSupportedSitesForElement(NestedElementInterface $element): ar { try { $owner = $element->getOwner(); - } catch (InvalidConfigException) { + } catch (RuntimeException) { $owner = $element->duplicateOf; } @@ -1031,7 +1031,7 @@ public function getTranslationDescription(?ElementInterface $element): ?string } /** - * @throws InvalidConfigException + * @throws RuntimeException */ #[Override] protected function inputHtml(mixed $value, ?ElementInterface $element, bool $inline): string diff --git a/src/Field/MissingField.php b/src/Field/MissingField.php index 9b3f4862374..aac9729fbda 100644 --- a/src/Field/MissingField.php +++ b/src/Field/MissingField.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; use CraftCms\Cms\Component\Concerns\MissingComponentTrait; use CraftCms\Cms\Component\Contracts\MissingComponentInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; use Override; diff --git a/src/Field/Money.php b/src/Field/Money.php index 1839dcd162e..79f610a57ae 100644 --- a/src/Field/Money.php +++ b/src/Field/Money.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Field\Conditions\MoneyFieldConditionRule; use CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface; diff --git a/src/Field/MultiSelect.php b/src/Field/MultiSelect.php index f8faa2b2a84..14b0051baa5 100644 --- a/src/Field/MultiSelect.php +++ b/src/Field/MultiSelect.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Data\MultiOptionsFieldData; use CraftCms\Cms\Support\Facades\DeltaRegistry; use Illuminate\Support\Collection; diff --git a/src/Field/Number.php b/src/Field/Number.php index 7751c1ee3a6..86677028cd8 100644 --- a/src/Field/Number.php +++ b/src/Field/Number.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Field\Conditions\NumberFieldConditionRule; use CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface; diff --git a/src/Field/PlainText.php b/src/Field/PlainText.php index 502084063bc..27b7bd1dad5 100644 --- a/src/Field/PlainText.php +++ b/src/Field/PlainText.php @@ -5,7 +5,7 @@ namespace CraftCms\Cms\Field; use Craft; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Field\Conditions\TextFieldConditionRule; use CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface; diff --git a/src/Field/Policies/ContentBlockPolicy.php b/src/Field/Policies/ContentBlockPolicy.php index 852662043da..1d40f6a4ae1 100644 --- a/src/Field/Policies/ContentBlockPolicy.php +++ b/src/Field/Policies/ContentBlockPolicy.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field\Policies; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Policies\ElementPolicy; use CraftCms\Cms\Field\Elements\ContentBlock; use CraftCms\Cms\User\Elements\User; diff --git a/src/Field/RadioButtons.php b/src/Field/RadioButtons.php index d68375919a3..f180ff64a54 100644 --- a/src/Field/RadioButtons.php +++ b/src/Field/RadioButtons.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Contracts\SortableFieldInterface; use CraftCms\Cms\Field\Data\SingleOptionFieldData; use CraftCms\Cms\Support\Facades\DeltaRegistry; diff --git a/src/Field/Range.php b/src/Field/Range.php index dece219f3ad..ece05da8d1d 100644 --- a/src/Field/Range.php +++ b/src/Field/Range.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Field\Conditions\NumberFieldConditionRule; use CraftCms\Cms\Field\Contracts\InlineEditableFieldInterface; diff --git a/src/Field/Table.php b/src/Field/Table.php index 5a161217273..86bb948aa09 100644 --- a/src/Field/Table.php +++ b/src/Field/Table.php @@ -6,8 +6,8 @@ use Closure; use Craft; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface; use CraftCms\Cms\Field\Data\ColorData; use CraftCms\Cms\Gql\GqlEntityRegistry; diff --git a/src/Field/Time.php b/src/Field/Time.php index 43508fa04f9..5c50cc1150d 100644 --- a/src/Field/Time.php +++ b/src/Field/Time.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Field; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Field\Conditions\EmptyFieldConditionRule; use CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface; diff --git a/src/FieldLayout/Concerns/HasFieldLayout.php b/src/FieldLayout/Concerns/HasFieldLayout.php index b2c1765153d..df65d2446fa 100644 --- a/src/FieldLayout/Concerns/HasFieldLayout.php +++ b/src/FieldLayout/Concerns/HasFieldLayout.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\FieldLayout\Concerns; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Data\EntryType; use CraftCms\Cms\Field\Contracts\FieldInterface; use CraftCms\Cms\FieldLayout\Contracts\FieldLayoutProviderInterface; diff --git a/src/FieldLayout/Events/CreateFieldLayoutForm.php b/src/FieldLayout/Events/CreateFieldLayoutForm.php index 8190b53a66e..ea7185b49d3 100644 --- a/src/FieldLayout/Events/CreateFieldLayoutForm.php +++ b/src/FieldLayout/Events/CreateFieldLayoutForm.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\FieldLayout\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\FieldLayout; use CraftCms\Cms\FieldLayout\FieldLayoutForm; use CraftCms\Cms\FieldLayout\FieldLayoutTab; diff --git a/src/FieldLayout/Events/DefineActionMenuItems.php b/src/FieldLayout/Events/DefineActionMenuItems.php index 164bc9cb4a1..56501437a75 100644 --- a/src/FieldLayout/Events/DefineActionMenuItems.php +++ b/src/FieldLayout/Events/DefineActionMenuItems.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\FieldLayout\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; class DefineActionMenuItems extends \CraftCms\Cms\Element\Events\DefineActionMenuItems { diff --git a/src/FieldLayout/Events/DefineShowInForm.php b/src/FieldLayout/Events/DefineShowInForm.php index 2a3ad9119f5..adb2ddbe2b3 100644 --- a/src/FieldLayout/Events/DefineShowInForm.php +++ b/src/FieldLayout/Events/DefineShowInForm.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\FieldLayout\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\FieldLayout; use CraftCms\Cms\FieldLayout\FieldLayoutComponent; use CraftCms\Cms\Shared\Concerns\HandleableEvent; diff --git a/src/FieldLayout/FieldLayout.php b/src/FieldLayout/FieldLayout.php index dcc1ed4cddc..a319c7164a3 100644 --- a/src/FieldLayout/FieldLayout.php +++ b/src/FieldLayout/FieldLayout.php @@ -5,8 +5,8 @@ namespace CraftCms\Cms\FieldLayout; use Closure; -use craft\base\ElementInterface; use CraftCms\Cms\Component\Component; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\ContentBlock; use CraftCms\Cms\Field\Contracts\FieldInterface; use CraftCms\Cms\Field\Contracts\PreviewableFieldInterface; @@ -39,7 +39,7 @@ use Illuminate\Support\Facades\Validator; use InvalidArgumentException; use Override; -use yii\base\InvalidConfigException; +use RuntimeException; use function CraftCms\Cms\t; @@ -457,7 +457,7 @@ public function getAvailableNativeFields(): array }; if (! $field instanceof BaseField) { - throw new InvalidConfigException('Invalid standard field config'); + throw new RuntimeException('Invalid standard field config'); } $field->setLayout($this); @@ -498,7 +498,7 @@ public function getAvailableUiElements(): array }; if (! $element instanceof FieldLayoutElement) { - throw new InvalidConfigException('Invalid UI element config'); + throw new RuntimeException('Invalid UI element config'); } } diff --git a/src/FieldLayout/FieldLayoutComponent.php b/src/FieldLayout/FieldLayoutComponent.php index fd87a0c92d2..428bb916e99 100644 --- a/src/FieldLayout/FieldLayoutComponent.php +++ b/src/FieldLayout/FieldLayoutComponent.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\FieldLayout; -use craft\base\ElementInterface; use CraftCms\Cms\Component\Component; use CraftCms\Cms\Condition\Contracts\ConditionInterface; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\Events\DefineShowInForm; use CraftCms\Cms\Support\Facades\Conditions; use CraftCms\Cms\Support\Html; diff --git a/src/FieldLayout/FieldLayoutElement.php b/src/FieldLayout/FieldLayoutElement.php index c366157be13..0070f98683c 100644 --- a/src/FieldLayout/FieldLayoutElement.php +++ b/src/FieldLayout/FieldLayoutElement.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\FieldLayout; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use DateTime; use Override; diff --git a/src/FieldLayout/FieldLayoutServiceProvider.php b/src/FieldLayout/FieldLayoutServiceProvider.php index 88abb30e450..d346f9d7d99 100644 --- a/src/FieldLayout/FieldLayoutServiceProvider.php +++ b/src/FieldLayout/FieldLayoutServiceProvider.php @@ -9,21 +9,21 @@ use CraftCms\Cms\Cms; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\FieldLayout\Events\DefineNativeFields; -use CraftCms\Cms\FieldLayout\LayoutElements\addresses\AddressField; -use CraftCms\Cms\FieldLayout\LayoutElements\addresses\CountryCodeField; -use CraftCms\Cms\FieldLayout\LayoutElements\addresses\LabelField; -use CraftCms\Cms\FieldLayout\LayoutElements\addresses\LatLongField; -use CraftCms\Cms\FieldLayout\LayoutElements\addresses\OrganizationField; -use CraftCms\Cms\FieldLayout\LayoutElements\addresses\OrganizationTaxIdField; -use CraftCms\Cms\FieldLayout\LayoutElements\assets\AltField; -use CraftCms\Cms\FieldLayout\LayoutElements\assets\AssetTitleField; -use CraftCms\Cms\FieldLayout\LayoutElements\entries\EntryTitleField; +use CraftCms\Cms\FieldLayout\LayoutElements\Addresses\AddressField; +use CraftCms\Cms\FieldLayout\LayoutElements\Addresses\CountryCodeField; +use CraftCms\Cms\FieldLayout\LayoutElements\Addresses\LabelField; +use CraftCms\Cms\FieldLayout\LayoutElements\Addresses\LatLongField; +use CraftCms\Cms\FieldLayout\LayoutElements\Addresses\OrganizationField; +use CraftCms\Cms\FieldLayout\LayoutElements\Addresses\OrganizationTaxIdField; +use CraftCms\Cms\FieldLayout\LayoutElements\Assets\AltField; +use CraftCms\Cms\FieldLayout\LayoutElements\Assets\AssetTitleField; +use CraftCms\Cms\FieldLayout\LayoutElements\Entries\EntryTitleField; use CraftCms\Cms\FieldLayout\LayoutElements\FullNameField; -use CraftCms\Cms\FieldLayout\LayoutElements\users\AffiliatedSiteField; -use CraftCms\Cms\FieldLayout\LayoutElements\users\EmailField; -use CraftCms\Cms\FieldLayout\LayoutElements\users\FullNameField as UserFullNameField; -use CraftCms\Cms\FieldLayout\LayoutElements\users\PhotoField; -use CraftCms\Cms\FieldLayout\LayoutElements\users\UsernameField; +use CraftCms\Cms\FieldLayout\LayoutElements\Users\AffiliatedSiteField; +use CraftCms\Cms\FieldLayout\LayoutElements\Users\EmailField; +use CraftCms\Cms\FieldLayout\LayoutElements\Users\FullNameField as UserFullNameField; +use CraftCms\Cms\FieldLayout\LayoutElements\Users\PhotoField; +use CraftCms\Cms\FieldLayout\LayoutElements\Users\UsernameField; use CraftCms\Cms\Site\Sites; use CraftCms\Cms\User\Elements\User; use Illuminate\Support\Facades\Event; diff --git a/src/FieldLayout/FieldLayoutTab.php b/src/FieldLayout/FieldLayoutTab.php index 76a96d3d50a..576ae972b08 100644 --- a/src/FieldLayout/FieldLayoutTab.php +++ b/src/FieldLayout/FieldLayoutTab.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\FieldLayout; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Cp\Icons; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Exceptions\FieldNotFoundException; use CraftCms\Cms\Field\Fields; use CraftCms\Cms\FieldLayout\LayoutElements\BaseField; @@ -19,7 +19,7 @@ use Illuminate\Support\Facades\Log; use InvalidArgumentException; use Override; -use yii\base\InvalidConfigException; +use RuntimeException; use function CraftCms\Cms\t; @@ -205,7 +205,7 @@ public function getElementConfigs(): array * * @return FieldLayout The tab’s layout. * - * @throws InvalidConfigException if [[layoutId]] is set but invalid + * @throws RuntimeException if [[layoutId]] is set but invalid */ #[Override] public function getLayout(): FieldLayout @@ -215,11 +215,11 @@ public function getLayout(): FieldLayout } if (! $this->layoutId) { - throw new InvalidConfigException('Field layout tab is missing its field layout.'); + throw new RuntimeException('Field layout tab is missing its field layout.'); } if (($this->_layout = app(Fields::class)->getLayoutById($this->layoutId)) === null) { - throw new InvalidConfigException('Invalid layout ID: '.$this->layoutId); + throw new RuntimeException('Invalid layout ID: '.$this->layoutId); } return $this->_layout; diff --git a/src/FieldLayout/LayoutElements/addresses/AddressField.php b/src/FieldLayout/LayoutElements/Addresses/AddressField.php similarity index 98% rename from src/FieldLayout/LayoutElements/addresses/AddressField.php rename to src/FieldLayout/LayoutElements/Addresses/AddressField.php index 7ec868b1bc8..b875900cfa9 100644 --- a/src/FieldLayout/LayoutElements/addresses/AddressField.php +++ b/src/FieldLayout/LayoutElements/Addresses/AddressField.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\addresses; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Addresses; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Addresses; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\LayoutElements\BaseField; use CraftCms\Cms\Support\Facades\HtmlStack; use CraftCms\Cms\Support\Facades\InputNamespace; diff --git a/src/FieldLayout/LayoutElements/addresses/CountryCodeField.php b/src/FieldLayout/LayoutElements/Addresses/CountryCodeField.php similarity index 96% rename from src/FieldLayout/LayoutElements/addresses/CountryCodeField.php rename to src/FieldLayout/LayoutElements/Addresses/CountryCodeField.php index 1a95070864f..919d41c455b 100644 --- a/src/FieldLayout/LayoutElements/addresses/CountryCodeField.php +++ b/src/FieldLayout/LayoutElements/Addresses/CountryCodeField.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\addresses; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Addresses; use CommerceGuys\Addressing\Country\Country; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Addresses; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\LayoutElements\BaseNativeField; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Html; diff --git a/src/FieldLayout/LayoutElements/addresses/LabelField.php b/src/FieldLayout/LayoutElements/Addresses/LabelField.php similarity index 88% rename from src/FieldLayout/LayoutElements/addresses/LabelField.php rename to src/FieldLayout/LayoutElements/Addresses/LabelField.php index 6583078f728..acafe0cb085 100644 --- a/src/FieldLayout/LayoutElements/addresses/LabelField.php +++ b/src/FieldLayout/LayoutElements/Addresses/LabelField.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\addresses; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Addresses; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\LayoutElements\TitleField; use CraftCms\Cms\Support\Arr; use Override; diff --git a/src/FieldLayout/LayoutElements/addresses/LatLongField.php b/src/FieldLayout/LayoutElements/Addresses/LatLongField.php similarity index 97% rename from src/FieldLayout/LayoutElements/addresses/LatLongField.php rename to src/FieldLayout/LayoutElements/Addresses/LatLongField.php index 70fd93b9b10..76ed8a19461 100644 --- a/src/FieldLayout/LayoutElements/addresses/LatLongField.php +++ b/src/FieldLayout/LayoutElements/Addresses/LatLongField.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\addresses; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Addresses; -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\LayoutElements\BaseNativeField; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Html; diff --git a/src/FieldLayout/LayoutElements/addresses/OrganizationField.php b/src/FieldLayout/LayoutElements/Addresses/OrganizationField.php similarity index 89% rename from src/FieldLayout/LayoutElements/addresses/OrganizationField.php rename to src/FieldLayout/LayoutElements/Addresses/OrganizationField.php index 2e7464b5592..20ff65ddc1f 100644 --- a/src/FieldLayout/LayoutElements/addresses/OrganizationField.php +++ b/src/FieldLayout/LayoutElements/Addresses/OrganizationField.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\addresses; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Addresses; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\LayoutElements\TextField; use CraftCms\Cms\Support\Arr; use Override; diff --git a/src/FieldLayout/LayoutElements/addresses/OrganizationTaxIdField.php b/src/FieldLayout/LayoutElements/Addresses/OrganizationTaxIdField.php similarity index 90% rename from src/FieldLayout/LayoutElements/addresses/OrganizationTaxIdField.php rename to src/FieldLayout/LayoutElements/Addresses/OrganizationTaxIdField.php index cf3bdb800a5..963d0c0b779 100644 --- a/src/FieldLayout/LayoutElements/addresses/OrganizationTaxIdField.php +++ b/src/FieldLayout/LayoutElements/Addresses/OrganizationTaxIdField.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\addresses; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Addresses; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\LayoutElements\TextField; use CraftCms\Cms\Support\Arr; use Override; diff --git a/src/FieldLayout/LayoutElements/assets/AltField.php b/src/FieldLayout/LayoutElements/Assets/AltField.php similarity index 95% rename from src/FieldLayout/LayoutElements/assets/AltField.php rename to src/FieldLayout/LayoutElements/Assets/AltField.php index 3c68e1533ba..cb52a8fb7a1 100644 --- a/src/FieldLayout/LayoutElements/assets/AltField.php +++ b/src/FieldLayout/LayoutElements/Assets/AltField.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\assets; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Assets; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\Elements\Asset; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Enums\TranslationMethod; use CraftCms\Cms\FieldLayout\LayoutElements\TextareaField; use CraftCms\Cms\Support\Arr; diff --git a/src/FieldLayout/LayoutElements/assets/AssetTitleField.php b/src/FieldLayout/LayoutElements/Assets/AssetTitleField.php similarity index 90% rename from src/FieldLayout/LayoutElements/assets/AssetTitleField.php rename to src/FieldLayout/LayoutElements/Assets/AssetTitleField.php index 0c96f44c607..7a9fd547445 100644 --- a/src/FieldLayout/LayoutElements/assets/AssetTitleField.php +++ b/src/FieldLayout/LayoutElements/Assets/AssetTitleField.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\assets; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Assets; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\Elements\Asset; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Enums\TranslationMethod; use CraftCms\Cms\FieldLayout\LayoutElements\TitleField; use InvalidArgumentException; diff --git a/src/FieldLayout/LayoutElements/BaseField.php b/src/FieldLayout/LayoutElements/BaseField.php index fc126f03f0f..14557069ac9 100644 --- a/src/FieldLayout/LayoutElements/BaseField.php +++ b/src/FieldLayout/LayoutElements/BaseField.php @@ -5,9 +5,9 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; use Craft; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Cp\Icons; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementAttributeRenderer; use CraftCms\Cms\FieldLayout\Events\DefineActionMenuItems; use CraftCms\Cms\FieldLayout\FieldLayoutElement; diff --git a/src/FieldLayout/LayoutElements/BaseNativeField.php b/src/FieldLayout/LayoutElements/BaseNativeField.php index cfd3055f928..45e4951f999 100644 --- a/src/FieldLayout/LayoutElements/BaseNativeField.php +++ b/src/FieldLayout/LayoutElements/BaseNativeField.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\Events\DefineNativeFields; use CraftCms\Cms\Support\Arr; use Override; diff --git a/src/FieldLayout/LayoutElements/CustomField.php b/src/FieldLayout/LayoutElements/CustomField.php index f275525433f..ab35e8eea24 100644 --- a/src/FieldLayout/LayoutElements/CustomField.php +++ b/src/FieldLayout/LayoutElements/CustomField.php @@ -4,12 +4,12 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; use CraftCms\Cms\Component\Contracts\Actionable; use CraftCms\Cms\Component\Contracts\Iconic; use CraftCms\Cms\Cp\FieldLayoutDesigner\CardDesigner; use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\ContentBlock; use CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; @@ -27,8 +27,8 @@ use CraftCms\Cms\User\Elements\User; use Illuminate\Support\Facades\Auth; use Override; +use RuntimeException; use Throwable; -use yii\base\InvalidConfigException; use function CraftCms\Cms\t; use function CraftCms\Cms\template; @@ -335,7 +335,7 @@ public function keywords(): array /** * Returns the custom field this layout field is based on. * - * @throws InvalidConfigException + * @throws RuntimeException * @throws FieldNotFoundException */ public function getField(): FieldInterface @@ -345,7 +345,7 @@ public function getField(): FieldInterface } if (! isset($this->_fieldUid)) { - throw new InvalidConfigException('No field UUID set.'); + throw new RuntimeException('No field UUID set.'); } if (($field = Fields::getFieldByUid($this->_fieldUid)) === null) { diff --git a/src/FieldLayout/LayoutElements/entries/EntryTitleField.php b/src/FieldLayout/LayoutElements/Entries/EntryTitleField.php similarity index 96% rename from src/FieldLayout/LayoutElements/entries/EntryTitleField.php rename to src/FieldLayout/LayoutElements/Entries/EntryTitleField.php index 61c76506702..fe7e99af0b1 100644 --- a/src/FieldLayout/LayoutElements/entries/EntryTitleField.php +++ b/src/FieldLayout/LayoutElements/Entries/EntryTitleField.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\entries; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Entries; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Field\Enums\TranslationMethod; use CraftCms\Cms\FieldLayout\LayoutElements\TitleField; diff --git a/src/FieldLayout/LayoutElements/FullNameField.php b/src/FieldLayout/LayoutElements/FullNameField.php index 78e10591d6b..cad221ae4c1 100644 --- a/src/FieldLayout/LayoutElements/FullNameField.php +++ b/src/FieldLayout/LayoutElements/FullNameField.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Html as HtmlHelper; use Illuminate\Support\Facades\Auth; diff --git a/src/FieldLayout/LayoutElements/Heading.php b/src/FieldLayout/LayoutElements/Heading.php index 237bf899bad..d3f97e7ff32 100644 --- a/src/FieldLayout/LayoutElements/Heading.php +++ b/src/FieldLayout/LayoutElements/Heading.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Html; use Override; diff --git a/src/FieldLayout/LayoutElements/HorizontalRule.php b/src/FieldLayout/LayoutElements/HorizontalRule.php index 8a8c9dd869a..9fa1985cd57 100644 --- a/src/FieldLayout/LayoutElements/HorizontalRule.php +++ b/src/FieldLayout/LayoutElements/HorizontalRule.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\Icons; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\FieldLayoutElement; use CraftCms\Cms\Support\Html; use Override; diff --git a/src/FieldLayout/LayoutElements/Html.php b/src/FieldLayout/LayoutElements/Html.php index c9aed077279..f0e467cdf59 100644 --- a/src/FieldLayout/LayoutElements/Html.php +++ b/src/FieldLayout/LayoutElements/Html.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\FieldLayoutElement; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Html as HtmlHelper; -use yii\base\NotSupportedException; class Html extends FieldLayoutElement { diff --git a/src/FieldLayout/LayoutElements/LineBreak.php b/src/FieldLayout/LayoutElements/LineBreak.php index dc6a7afc3df..c93a710cd91 100644 --- a/src/FieldLayout/LayoutElements/LineBreak.php +++ b/src/FieldLayout/LayoutElements/LineBreak.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\Icons; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\FieldLayoutElement; use CraftCms\Cms\Support\Html; use Override; diff --git a/src/FieldLayout/LayoutElements/Markdown.php b/src/FieldLayout/LayoutElements/Markdown.php index 648a205868a..549974fc4ed 100644 --- a/src/FieldLayout/LayoutElements/Markdown.php +++ b/src/FieldLayout/LayoutElements/Markdown.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\Markdown as MarkdownFacade; use CraftCms\Cms\Support\Html; use CraftCms\Cms\Support\Str; diff --git a/src/FieldLayout/LayoutElements/Template.php b/src/FieldLayout/LayoutElements/Template.php index aaac1f440f8..e96a08d386b 100644 --- a/src/FieldLayout/LayoutElements/Template.php +++ b/src/FieldLayout/LayoutElements/Template.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\Twig; use CraftCms\Cms\Support\Html; use CraftCms\Cms\Twig\Environment; diff --git a/src/FieldLayout/LayoutElements/TextField.php b/src/FieldLayout/LayoutElements/TextField.php index d95a409ffb5..e21b83ff581 100644 --- a/src/FieldLayout/LayoutElements/TextField.php +++ b/src/FieldLayout/LayoutElements/TextField.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Arr; use Illuminate\Support\Facades\Auth; use Override; diff --git a/src/FieldLayout/LayoutElements/TextareaField.php b/src/FieldLayout/LayoutElements/TextareaField.php index 44af6ff14de..ce1faba054d 100644 --- a/src/FieldLayout/LayoutElements/TextareaField.php +++ b/src/FieldLayout/LayoutElements/TextareaField.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Html as HtmlHelper; use Illuminate\Support\Facades\Auth; diff --git a/src/FieldLayout/LayoutElements/Tip.php b/src/FieldLayout/LayoutElements/Tip.php index 3b6927540a6..804d027503d 100644 --- a/src/FieldLayout/LayoutElements/Tip.php +++ b/src/FieldLayout/LayoutElements/Tip.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\InputNamespace; use CraftCms\Cms\Support\Facades\Markdown; use CraftCms\Cms\Support\Html; diff --git a/src/FieldLayout/LayoutElements/TitleField.php b/src/FieldLayout/LayoutElements/TitleField.php index 263fd73a79b..a457bb1931a 100644 --- a/src/FieldLayout/LayoutElements/TitleField.php +++ b/src/FieldLayout/LayoutElements/TitleField.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\FieldLayout\LayoutElements; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\HtmlStack; diff --git a/src/FieldLayout/LayoutElements/users/AffiliatedSiteField.php b/src/FieldLayout/LayoutElements/Users/AffiliatedSiteField.php similarity index 96% rename from src/FieldLayout/LayoutElements/users/AffiliatedSiteField.php rename to src/FieldLayout/LayoutElements/Users/AffiliatedSiteField.php index d2cdc425a52..73aafc2ff1e 100644 --- a/src/FieldLayout/LayoutElements/users/AffiliatedSiteField.php +++ b/src/FieldLayout/LayoutElements/Users/AffiliatedSiteField.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\users; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Users; -use craft\base\ElementInterface; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\LayoutElements\BaseNativeField; use CraftCms\Cms\Site\Data\Site; use CraftCms\Cms\Support\Arr; diff --git a/src/FieldLayout/LayoutElements/users/EmailField.php b/src/FieldLayout/LayoutElements/Users/EmailField.php similarity index 96% rename from src/FieldLayout/LayoutElements/users/EmailField.php rename to src/FieldLayout/LayoutElements/Users/EmailField.php index a1e7a8ecf7e..d141c103c06 100644 --- a/src/FieldLayout/LayoutElements/users/EmailField.php +++ b/src/FieldLayout/LayoutElements/Users/EmailField.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\users; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Users; -use craft\base\ElementInterface; use CraftCms\Cms\Edition; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\LayoutElements\TextField; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\HtmlStack; diff --git a/src/FieldLayout/LayoutElements/users/FullNameField.php b/src/FieldLayout/LayoutElements/Users/FullNameField.php similarity index 86% rename from src/FieldLayout/LayoutElements/users/FullNameField.php rename to src/FieldLayout/LayoutElements/Users/FullNameField.php index 85d04dd45de..55f7f95ad61 100644 --- a/src/FieldLayout/LayoutElements/users/FullNameField.php +++ b/src/FieldLayout/LayoutElements/Users/FullNameField.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\users; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Users; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\LayoutElements\FullNameField as BaseFullNameField; use CraftCms\Cms\User\Elements\User; use InvalidArgumentException; diff --git a/src/FieldLayout/LayoutElements/users/PhotoField.php b/src/FieldLayout/LayoutElements/Users/PhotoField.php similarity index 96% rename from src/FieldLayout/LayoutElements/users/PhotoField.php rename to src/FieldLayout/LayoutElements/Users/PhotoField.php index ec722864a13..8eaf327a18a 100644 --- a/src/FieldLayout/LayoutElements/users/PhotoField.php +++ b/src/FieldLayout/LayoutElements/Users/PhotoField.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\users; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Users; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\LayoutElements\BaseNativeField; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\HtmlStack; diff --git a/src/FieldLayout/LayoutElements/users/UsernameField.php b/src/FieldLayout/LayoutElements/Users/UsernameField.php similarity index 94% rename from src/FieldLayout/LayoutElements/users/UsernameField.php rename to src/FieldLayout/LayoutElements/Users/UsernameField.php index 543e1999c4c..22292afccf6 100644 --- a/src/FieldLayout/LayoutElements/users/UsernameField.php +++ b/src/FieldLayout/LayoutElements/Users/UsernameField.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace CraftCms\Cms\FieldLayout\LayoutElements\users; +namespace CraftCms\Cms\FieldLayout\LayoutElements\Users; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\LayoutElements\TextField; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\User\Elements\User; diff --git a/src/GarbageCollection/Actions/DeleteOrphanedFieldLayouts.php b/src/GarbageCollection/Actions/DeleteOrphanedFieldLayouts.php index 07ffc166e79..f20f1608816 100644 --- a/src/GarbageCollection/Actions/DeleteOrphanedFieldLayouts.php +++ b/src/GarbageCollection/Actions/DeleteOrphanedFieldLayouts.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\GarbageCollection\Actions; -use craft\base\ElementInterface; use CraftCms\Cms\Config\GeneralConfig; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\GarbageCollection\GarbageCollection; use Illuminate\Support\Facades\DB; use Tpetry\QueryExpressions\Language\Alias; diff --git a/src/GarbageCollection/Actions/DeleteOrphanedNestedElements.php b/src/GarbageCollection/Actions/DeleteOrphanedNestedElements.php index fa95b93c6eb..ae384fad7ea 100644 --- a/src/GarbageCollection/Actions/DeleteOrphanedNestedElements.php +++ b/src/GarbageCollection/Actions/DeleteOrphanedNestedElements.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\GarbageCollection\Actions; -use craft\base\ElementInterface; use CraftCms\Cms\Config\GeneralConfig; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\GarbageCollection\GarbageCollection; use Illuminate\Support\Facades\DB; use Tpetry\QueryExpressions\Language\Alias; diff --git a/src/GarbageCollection/Actions/DeletePartialElements.php b/src/GarbageCollection/Actions/DeletePartialElements.php index 28a8947e2f2..d24cc365b65 100644 --- a/src/GarbageCollection/Actions/DeletePartialElements.php +++ b/src/GarbageCollection/Actions/DeletePartialElements.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\GarbageCollection\Actions; -use craft\base\ElementInterface; use CraftCms\Cms\Config\GeneralConfig; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\GarbageCollection\GarbageCollection; use Illuminate\Support\Facades\DB; use Tpetry\QueryExpressions\Language\Alias; diff --git a/src/GarbageCollection/Actions/HardDeleteElements.php b/src/GarbageCollection/Actions/HardDeleteElements.php index e5530011d3f..7de60dfc260 100644 --- a/src/GarbageCollection/Actions/HardDeleteElements.php +++ b/src/GarbageCollection/Actions/HardDeleteElements.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\GarbageCollection\Actions; -use craft\base\NestedElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Support\Facades\Elements; use Illuminate\Support\Facades\DB; use Tpetry\QueryExpressions\Function\Conditional\Coalesce; diff --git a/src/Gql/ArgumentManager.php b/src/Gql/ArgumentManager.php index 17db53a4ac1..5960061ef7f 100644 --- a/src/Gql/ArgumentManager.php +++ b/src/Gql/ArgumentManager.php @@ -121,7 +121,6 @@ public function prepareArguments(array $arguments): array protected function createHandler(string $handler): ArgumentHandlerInterface|string { if (is_a($handler, ArgumentHandlerInterface::class, true)) { - /** @var ArgumentHandlerInterface $handler */ $handler = new $handler; $handler->setArgumentManager($this); } diff --git a/src/Gql/Concerns/PerformsStructureMutations.php b/src/Gql/Concerns/PerformsStructureMutations.php index 42324a6a68e..584180d0d4f 100644 --- a/src/Gql/Concerns/PerformsStructureMutations.php +++ b/src/Gql/Concerns/PerformsStructureMutations.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Gql\Concerns; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Support\Facades\Elements; use CraftCms\Cms\Support\Facades\Structures; diff --git a/src/Gql/Events/AfterPopulateElement.php b/src/Gql/Events/AfterPopulateElement.php index 2ff60f21272..1b2b5fab9cb 100644 --- a/src/Gql/Events/AfterPopulateElement.php +++ b/src/Gql/Events/AfterPopulateElement.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Gql\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Gql\Resolvers\ElementMutationResolver; /** diff --git a/src/Gql/Events/BeforePopulateElement.php b/src/Gql/Events/BeforePopulateElement.php index 3824fd0e6cd..56a41b8549f 100644 --- a/src/Gql/Events/BeforePopulateElement.php +++ b/src/Gql/Events/BeforePopulateElement.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Gql\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Gql\Resolvers\ElementMutationResolver; /** diff --git a/src/Gql/Gql.php b/src/Gql/Gql.php index d105a8fa018..2e3665f089e 100644 --- a/src/Gql/Gql.php +++ b/src/Gql/Gql.php @@ -4,12 +4,12 @@ namespace CraftCms\Cms\Gql; -use craft\base\ElementInterface as BaseElementInterface; use craft\behaviors\FieldLayoutBehavior; use CraftCms\Cms\Asset\Volumes; use CraftCms\Cms\Cms; use CraftCms\Cms\Database\Table; use CraftCms\Cms\Edition; +use CraftCms\Cms\Element\Contracts\ElementInterface as BaseElementInterface; use CraftCms\Cms\Element\ElementCaches; use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; diff --git a/src/Gql/GqlHelper.php b/src/Gql/GqlHelper.php index b341d265f6a..117ab5df518 100644 --- a/src/Gql/GqlHelper.php +++ b/src/Gql/GqlHelper.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Gql; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Data\EntryType; use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface; use CraftCms\Cms\Field\Fields; diff --git a/src/Gql/Handlers/RelationArgumentHandler.php b/src/Gql/Handlers/RelationArgumentHandler.php index 197fd9e09ee..f616c1739fe 100644 --- a/src/Gql/Handlers/RelationArgumentHandler.php +++ b/src/Gql/Handlers/RelationArgumentHandler.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Gql\Handlers; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\ElementQuery; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\Elements; diff --git a/src/Gql/Mutations/Entry.php b/src/Gql/Mutations/Entry.php index 9092f09d754..40ab2632cf1 100644 --- a/src/Gql/Mutations/Entry.php +++ b/src/Gql/Mutations/Entry.php @@ -169,7 +169,6 @@ public static function createSaveMutations( $draftMutationArguments = DraftMutationArguments::getArguments(); $generatedType = EntryType::generateType($entryType); - /** @var EntryMutationResolver $resolver */ $resolver = new EntryMutationResolver; $resolver->setResolutionData('entryType', $entryType); $resolver->setResolutionData('section', $section); @@ -235,7 +234,6 @@ public static function createSaveMutationsForField( $draftMutationArguments = DraftMutationArguments::getArguments(); $generatedType = EntryType::generateType($entryType); - /** @var EntryMutationResolver $resolver */ $resolver = new EntryMutationResolver; $resolver->setResolutionData('entryType', $entryType); $resolver->setResolutionData('field', $field); diff --git a/src/Gql/Resolvers/ElementMutationResolver.php b/src/Gql/Resolvers/ElementMutationResolver.php index fbce86c3727..b8c7ce76a66 100644 --- a/src/Gql/Resolvers/ElementMutationResolver.php +++ b/src/Gql/Resolvers/ElementMutationResolver.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Gql\Resolvers; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Validation\ElementRules; use CraftCms\Cms\Gql\Events\AfterPopulateElement; diff --git a/src/Gql/Resolvers/ElementResolver.php b/src/Gql/Resolvers/ElementResolver.php index 5ee0782a562..a935e900bb6 100644 --- a/src/Gql/Resolvers/ElementResolver.php +++ b/src/Gql/Resolvers/ElementResolver.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Gql\Resolvers; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\ElementQuery; diff --git a/src/Gql/Resolvers/Elements/ContentBlock.php b/src/Gql/Resolvers/Elements/ContentBlock.php index 19aa9d78a04..3f66a6efb44 100644 --- a/src/Gql/Resolvers/Elements/ContentBlock.php +++ b/src/Gql/Resolvers/Elements/ContentBlock.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Gql\Resolvers\Elements; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Gql\GqlHelper; use CraftCms\Cms\Gql\Resolvers\Resolver; use GraphQL\Type\Definition\ResolveInfo; diff --git a/src/Gql/Resolvers/Mutations/Asset.php b/src/Gql/Resolvers/Mutations/Asset.php index 2ca2aed3150..4918ef24c08 100644 --- a/src/Gql/Resolvers/Mutations/Asset.php +++ b/src/Gql/Resolvers/Mutations/Asset.php @@ -4,7 +4,6 @@ namespace CraftCms\Cms\Gql\Resolvers\Mutations; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\AssetsHelper; use CraftCms\Cms\Asset\Data\Volume; use CraftCms\Cms\Asset\Elements\Asset as AssetElement; @@ -14,6 +13,7 @@ use CraftCms\Cms\Asset\Validation\AssetRules; use CraftCms\Cms\Cms; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Gql\Resolvers\ElementMutationResolver; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\Elements; diff --git a/src/Gql/Types/Elements/Element.php b/src/Gql/Types/Elements/Element.php index 8afc4f29c25..72f23072571 100644 --- a/src/Gql/Types/Elements/Element.php +++ b/src/Gql/Types/Elements/Element.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Gql\Types\Elements; -use craft\base\ElementInterface as BaseElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface as BaseElementInterface; use CraftCms\Cms\Gql\ArgumentManager; use CraftCms\Cms\Gql\Gql; use CraftCms\Cms\Gql\Interfaces\Element as ElementInterface; diff --git a/src/Http/Controllers/App/RenderController.php b/src/Http/Controllers/App/RenderController.php index d36bb08b4d6..6f97e2e62e5 100644 --- a/src/Http/Controllers/App/RenderController.php +++ b/src/Http/Controllers/App/RenderController.php @@ -4,13 +4,13 @@ namespace CraftCms\Cms\Http\Controllers\App; -use craft\base\ElementInterface; use CraftCms\Cms\Asset\Data\Volume; use CraftCms\Cms\Asset\Volumes; use CraftCms\Cms\Component\Contracts\Chippable; use CraftCms\Cms\Component\Contracts\Iconic; use CraftCms\Cms\Cp\Html\ElementHtml; use CraftCms\Cms\Cp\Html\MenuHtml; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\Queries\Contracts\NestedElementQueryInterface; use CraftCms\Cms\Support\Arr; diff --git a/src/Http/Controllers/Assets/ImageEditorController.php b/src/Http/Controllers/Assets/ImageEditorController.php index 9c44fc5565f..d82ec409af0 100644 --- a/src/Http/Controllers/Assets/ImageEditorController.php +++ b/src/Http/Controllers/Assets/ImageEditorController.php @@ -14,10 +14,10 @@ use CraftCms\Cms\Image\ImageTransformer; use CraftCms\Cms\Image\ImageTransformHelper; use CraftCms\Cms\Image\ImageTransforms; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; -use yii\base\NotSupportedException; use function CraftCms\Cms\t; use function CraftCms\Cms\template; diff --git a/src/Http/Controllers/Auth/OAuthController.php b/src/Http/Controllers/Auth/OAuthController.php index bed46e23c4c..dd57dac7666 100644 --- a/src/Http/Controllers/Auth/OAuthController.php +++ b/src/Http/Controllers/Auth/OAuthController.php @@ -107,7 +107,7 @@ public function callback(Request $request, string $provider, OAuth $oauthManager return $this->failedResponse( $isCpRequest, - implode(', ', $user->getErrorSummary(true)) ?: t('Unable to save the user.'), + implode(', ', $user->errors()->all()) ?: t('Unable to save the user.'), previous: $e, ); } catch (Throwable $e) { diff --git a/src/Http/Controllers/Elements/Concerns/CreatesElement.php b/src/Http/Controllers/Elements/Concerns/CreatesElement.php new file mode 100644 index 00000000000..d5c886ac13d --- /dev/null +++ b/src/Http/Controllers/Elements/Concerns/CreatesElement.php @@ -0,0 +1,44 @@ +request->elementType(); + + $this->request->validateElementType($elementType); + + /** @var ElementInterface $element */ + $element = app()->make($elementType); + + if ($this->request->has('siteId') && $element::isLocalized()) { + $element->siteId = $this->request->integer('siteId'); + } + + if ($this->request->has('ownerId') && $element instanceof NestedElementInterface) { + $element->setOwnerId($this->request->integer('ownerId')); + } + + $element->setAttributesFromRequest($this->request->validated() + array_filter(['fieldId' => $this->request->input('fieldId')])); + + Gate::authorize('save', $element); + + if (! $element->slug) { + $element->slug = ElementHelper::tempSlug(); + } + + return $element; + } +} diff --git a/src/Http/Controllers/Elements/Concerns/EditsElement.php b/src/Http/Controllers/Elements/Concerns/EditsElement.php new file mode 100644 index 00000000000..d98c3d8bda8 --- /dev/null +++ b/src/Http/Controllers/Elements/Concerns/EditsElement.php @@ -0,0 +1,46 @@ +title !== null && $element->title !== '' => $element->title, + ! $element->id || $element->getIsUnpublishedDraft() => t('Create a new {type}', [ + 'type' => $element::lowerDisplayName(), + ]), + default => $element->getUiLabel(), + }; + + $docTitle = $element->getUiLabel(); + + if ($element->getIsDraft() && ! $element->getIsUnpublishedDraft()) { + if ($element->isProvisionalDraft) { + $docTitle .= ' — '.t('Edited'); + } else { + $docTitle .= " ($element->draftName)"; + } + } elseif ($element->getIsRevision()) { + $docTitle .= ' ('.$element->getRevisionLabel().')'; + } + + // Include site name if localized + if ($element::isLocalized() && Sites::isMultiSite()) { + $docTitle .= sprintf(' - %s', $element->getSite()->getUiLabel()); + } + + return [$docTitle, $title]; + } +} diff --git a/src/Http/Controllers/Elements/Concerns/ElementCrumbs.php b/src/Http/Controllers/Elements/Concerns/ElementCrumbs.php new file mode 100644 index 00000000000..ee422263e8f --- /dev/null +++ b/src/Http/Controllers/Elements/Concerns/ElementCrumbs.php @@ -0,0 +1,30 @@ +isProvisionalDraft + ? $element->getCanonical(true)->getCrumbs() + : $element->getCrumbs(); + + return [ + ...$crumbs, + [ + 'html' => app(ElementHtml::class)->elementChipHtml($element, [ + 'showDraftName' => ! $current, + 'class' => 'chromeless', + 'hyperlink' => true, + ]), + 'current' => $current, + ], + ]; + } +} diff --git a/src/Http/Controllers/Elements/Concerns/InteractsWithElementIndexes.php b/src/Http/Controllers/Elements/Concerns/InteractsWithElementIndexes.php index bcf33ff7d0b..b3a44a76281 100644 --- a/src/Http/Controllers/Elements/Concerns/InteractsWithElementIndexes.php +++ b/src/Http/Controllers/Elements/Concerns/InteractsWithElementIndexes.php @@ -4,14 +4,17 @@ namespace CraftCms\Cms\Http\Controllers\Elements\Concerns; -use craft\base\ElementInterface; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; +use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; use CraftCms\Cms\Element\Conditions\ElementCondition; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\ElementSources; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\ExcludeDescendantIdsExpression; +use CraftCms\Cms\FieldLayout\FieldLayout; use CraftCms\Cms\Support\Facades\Conditions; +use CraftCms\Cms\Support\Facades\ElementExporters; use CraftCms\Cms\Support\Facades\Elements; use CraftCms\Cms\Support\Facades\ElementSources as ElementSourcesFacade; use CraftCms\Cms\Support\Typecast; @@ -20,7 +23,7 @@ trait InteractsWithElementIndexes { - protected function condition(): ?ElementConditionInterface + protected function resolveElementIndexCondition(): ?ElementConditionInterface { /** @var array|null $conditionConfig */ /** @phpstan-var array{class:class-string}|null $conditionConfig */ @@ -58,7 +61,7 @@ protected function condition(): ?ElementConditionInterface * @param class-string $elementType * @return array{0:?string,1:?array} */ - protected function source(string $elementType, ?string $sourceKey, string $context): array + protected function resolveSource(string $elementType, ?string $sourceKey, string $context): array { if (! isset($sourceKey)) { return [$sourceKey, null]; @@ -82,7 +85,24 @@ protected function source(string $elementType, ?string $sourceKey, string $conte return [$sourceKey, $source]; } - protected function viewState(): array + /** + * @return FieldLayout[]|null + */ + protected function resolveFieldLayouts(): ?array + { + $fieldLayouts = request()->input('fieldLayouts'); + + if (empty($fieldLayouts) || ! is_array($fieldLayouts)) { + return null; + } + + return array_map( + FieldLayout::createFromConfig(...), + $fieldLayouts, + ); + } + + protected function resolveViewState(): array { $viewState = request()->input('viewState', []); @@ -95,18 +115,22 @@ protected function viewState(): array /** * @param class-string $elementType + * @return array{query:ElementQueryInterface,unfilteredQuery:ElementQueryInterface|null} */ - protected function elementQuery( + protected function buildElementQueryState( string $elementType, ?array $source, ?ElementConditionInterface $condition, - ): ElementQueryInterface { + ): array { $query = $elementType::find(); if (! $source) { $query->id(false); - return $query; + return [ + 'query' => $query, + 'unfilteredQuery' => null, + ]; } if ($source['type'] === ElementSources::TYPE_CUSTOM) { @@ -115,9 +139,9 @@ protected function elementQuery( $sourceCondition->modifyQuery($query); } - $applyCriteria = function (array $criteria) use ($query): void { + $applyCriteria = function (array $criteria) use ($query): bool { if (! $criteria) { - return; + return false; } if (isset($criteria['trashed'])) { @@ -137,15 +161,24 @@ protected function elementQuery( } Typecast::configure($query, ElementHelper::cleanseQueryCriteria($criteria)); + + return true; }; $applyCriteria(request()->input('baseCriteria') ?? []); + $unfilteredQuery = clone $query; + $hasFilters = false; + if ($condition) { $condition->modifyQuery($query); + + $hasFilters = true; } - $applyCriteria(request()->input('criteria') ?? []); + if ($applyCriteria(request()->input('criteria') ?? [])) { + $hasFilters = true; + } $filterConditionConfig = request()->input('filterConfig'); @@ -158,12 +191,17 @@ protected function elementQuery( /** @var ElementConditionInterface $filterCondition */ $filterCondition = Conditions::createCondition($filterConditionConfig); $filterCondition->modifyQuery($query); + + $hasFilters = true; } $collapsedElementIds = request()->input('collapsedElementIds'); if (! $collapsedElementIds) { - return $query; + return [ + 'query' => $query, + 'unfilteredQuery' => $hasFilters ? $unfilteredQuery : null, + ]; } $descendantQuery = (clone $query) @@ -180,7 +218,10 @@ protected function elementQuery( ->all(); if (empty($collapsedElements)) { - return $query; + return [ + 'query' => $query, + 'unfilteredQuery' => $hasFilters ? $unfilteredQuery : null, + ]; } $descendantIds = []; @@ -198,14 +239,63 @@ protected function elementQuery( } if (empty($descendantIds)) { - return $query; + return [ + 'query' => $query, + 'unfilteredQuery' => $hasFilters ? $unfilteredQuery : null, + ]; } - return $query->where(new ExcludeDescendantIdsExpression($descendantIds)); + $query->where(new ExcludeDescendantIdsExpression($descendantIds)); + + return [ + 'query' => $query, + 'unfilteredQuery' => $unfilteredQuery, + ]; } - protected function isAdministrative(string $context): bool + /** + * @param class-string $elementType + */ + protected function availableExporters(string $elementType, string $sourceKey): ?array { - return in_array($context, ['index', 'embedded-index'], true); + if (request()->isMobileBrowser()) { + return null; + } + + return ElementExporters::availableExporters($elementType, $sourceKey); + } + + protected function populateFilterHudQueryParams( + ElementConditionInterface $condition, + ?array $source, + ?string $sourceKey, + ?ElementConditionInterface $currentCondition, + ): void { + if ($source !== null) { + if ($source['type'] === ElementSources::TYPE_NATIVE) { + $condition->queryParams = array_keys($source['criteria'] ?? []); + $condition->sourceKey = $sourceKey; + } else { + /** @var ElementConditionInterface $sourceCondition */ + $sourceCondition = Conditions::createCondition($source['condition']); + $condition->queryParams = []; + + foreach ($sourceCondition->getConditionRules() as $rule) { + /** @var ElementConditionRuleInterface $rule */ + array_push($condition->queryParams, ...$rule->getExclusiveQueryParams()); + } + } + } + + if ($currentCondition) { + foreach ($currentCondition->getConditionRules() as $rule) { + /** @var ElementConditionRuleInterface $rule */ + array_push($condition->queryParams, ...$rule->getExclusiveQueryParams()); + } + } + + $condition->queryParams[] = 'site'; + $condition->queryParams[] = 'status'; + $condition->queryParams = array_values(array_unique($condition->queryParams)); } } diff --git a/src/Http/Controllers/Elements/Concerns/SavesElement.php b/src/Http/Controllers/Elements/Concerns/SavesElement.php new file mode 100644 index 00000000000..aa04aff74ce --- /dev/null +++ b/src/Http/Controllers/Elements/Concerns/SavesElement.php @@ -0,0 +1,101 @@ +getIsRevision()) { + return false; + } + + if ($element->isProvisionalDraft) { + $element = $element->getCanonical(true); + } + + return $user->can('save', $element); + } + + protected function applyParamsToElement(ElementInterface $element): void + { + $fieldsLocation = $this->request->input('fieldsLocation', 'fields'); + $applyParams = $this->request->boolean('applyParams', true) || ! $this->request->isMethod('POST'); + + if (! $applyParams) { + return; + } + + $enabled = $this->request->boolean('setEnabled', true) ? true : null; + + if (! is_null($enabledForSite = $this->request->input('enabledForSite'))) { + if (is_array($enabledForSite)) { + // Make sure they are allowed to edit all of the posted site IDs + $editableSiteIds = Sites::getEditableSiteIds()->all(); + + if (array_diff(array_keys($enabledForSite), $editableSiteIds)) { + abort(403, 'User not authorized to edit element statuses for all the submitted site IDs.'); + } + + // Set the global status to true if it's enabled for *any* sites, or if already enabled. + $element->enabled = in_array(true, $enabledForSite) || $element->enabled; + } + + $element->setEnabledForSite($enabledForSite); + } elseif (isset($enabled)) { + $element->enabled = $enabled; + } + + if ($this->request->boolean('fresh')) { + $element->setIsFresh(); + + if ($element->getIsUnpublishedDraft()) { + $element->propagateAll = true; + } + } + + if ($element->getIsDraft()) { + /** @var ElementInterface $element */ + if ($this->request->has('draftName')) { + $element->draftName = $this->request->input('draftName'); + } + if ($this->request->has('notes')) { + $element->draftNotes = $this->request->input('notes'); + } + } elseif ($this->request->has('notes')) { + $element->setRevisionNotes($this->request->input('notes')); + } + + if ($this->request->has('updateSearchIndexImmediately')) { + $element->updateSearchIndexImmediately = $this->request->boolean('updateSearchIndexImmediately'); + } + + $element->ruleset->withScenario( + ElementRules::SCENARIO_LIVE, + function () use ($element) { + $element->setAttributesFromRequest($this->request->validated() + array_filter(['fieldId' => $this->request->input('fieldId')])); + + if ($this->request->has('slug')) { + $element->slug = $this->request->input('slug'); + } + }, + ); + + // Now that the element is fully configured, make sure the user can actually view it + Gate::authorize('view', $element); + + // Set the custom field values + $element->setFieldValuesFromRequest($fieldsLocation); + } +} diff --git a/src/Http/Controllers/Elements/Concerns/UpdatesFieldLayout.php b/src/Http/Controllers/Elements/Concerns/UpdatesFieldLayout.php new file mode 100644 index 00000000000..9e485ffdde1 --- /dev/null +++ b/src/Http/Controllers/Elements/Concerns/UpdatesFieldLayout.php @@ -0,0 +1,71 @@ +header('X-Craft-Namespace'); + $fieldLayout = $element->getFieldLayout(); + $form = $fieldLayout->createForm($element, false, $formConfig + [ + 'namespace' => $namespace, + 'registerDeltas' => false, + 'visibleElements' => request()->input('visibleLayoutElements'), + 'staticElements' => request()->input('staticLayoutElements'), + ]); + $missingElements = []; + + foreach ($form->tabs as $tab) { + if (! $tab->getUid()) { + continue; + } + + $elementInfo = []; + + foreach ($tab->elements as $formElement) { + if ($formElement->isConditional) { + $elementInfo[] = [ + 'uid' => $formElement->layoutElement->uid, + 'html' => $formElement->html, + 'static' => $formElement->isStatic, + ]; + } + } + + $missingElements[] = [ + 'uid' => $tab->getUid(), + 'id' => $tab->getId(), + 'elements' => $elementInfo, + ]; + } + + $tabs = $form->getTabMenu(); + if (count($tabs) > 1) { + $selectedTab = request()->input('selectedTab'); + $selectedTab = isset($tabs[$selectedTab]) ? $selectedTab : null; + $tabHtml = InputNamespace::namespaceInputs(fn () => template('_includes/tabs', [ + 'tabs' => $tabs, + 'selectedTab' => $selectedTab, + ], templateMode: TemplateMode::Cp), $namespace); + } else { + $tabHtml = null; + } + + return [ + 'tabs' => $tabHtml, + 'missingElements' => $missingElements, + 'headHtml' => HtmlStack::headHtml(), + 'bodyHtml' => HtmlStack::bodyHtml(), + ]; + } +} diff --git a/src/Http/Controllers/Elements/CopyElementValuesController.php b/src/Http/Controllers/Elements/CopyElementValuesController.php new file mode 100644 index 00000000000..161f5e2d384 --- /dev/null +++ b/src/Http/Controllers/Elements/CopyElementValuesController.php @@ -0,0 +1,106 @@ +request->element(checkForProvisionalDraft: true); + + if ($element instanceof Response) { + return $element; + } + + if (! $element || $element->getIsRevision()) { + abort(400, 'No element was identified by the request.'); + } + + $this->request->validate([ + 'fromSiteId' => ['required', 'integer'], + 'layoutElementUid' => ['required', 'uuid'], + 'namespace' => ['nullable', 'string'], + ]); + + $copyFromSiteId = $this->request->integer('fromSiteId'); + + if (! $site = $this->sites->getSiteById($copyFromSiteId)) { + abort(400, "Invalid site ID: $copyFromSiteId"); + } + + $this->requirePermission("editSite:$site->uid"); + + $layoutElementUid = $this->request->input('layoutElementUid'); + $namespace = $this->request->input('namespace'); + + $fromElement = $element::find() + ->id($element->id) + ->structureId($element->structureId) + ->siteId($copyFromSiteId) + ->drafts(null) + ->provisionalDrafts(null) + ->one(); + + if (! $fromElement) { + throw new UnsupportedSiteException($element, $copyFromSiteId, 'Attempting to copy element content from an unsupported site.'); + } + + $layoutElement = $element->getFieldLayout()->getElementByUid($layoutElementUid); + if (! $layoutElement instanceof BaseField || ! $layoutElement->isCrossSiteCopyable($element)) { + abort(400, "Invalid layout element UUID: $layoutElementUid"); + } + + if ($layoutElement instanceof CustomField) { + /** @var FieldInterface&CrossSiteCopyableFieldInterface $field */ + $field = $layoutElement->getField(); + $field->copyCrossSiteValue($fromElement, $element); + } else { + $attribute = $layoutElement->attribute(); + $element->$attribute = $fromElement->$attribute; + } + + $html = InputNamespace::namespaceInputs( + html: fn () => $layoutElement->formHtml($element), + namespace: $namespace + ); + + if ($html) { + $html = Html::modifyTagAttributes($html, [ + 'data' => [ + 'layout-element' => $layoutElement->uid, + ], + ]); + } + + return new ElementResponse()->success($element, t('Field value copied.'), [ + 'fieldHtml' => $html, + 'headHtml' => HtmlStack::headHtml(), + 'bodyHtml' => HtmlStack::bodyHtml(), + ]); + } +} diff --git a/src/Http/Controllers/Elements/CreateElementController.php b/src/Http/Controllers/Elements/CreateElementController.php new file mode 100644 index 00000000000..0825be11189 --- /dev/null +++ b/src/Http/Controllers/Elements/CreateElementController.php @@ -0,0 +1,55 @@ +createElement(); + $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS); + + if (! $drafts->saveElementAsDraft($element, $request->user()->id, markAsSaved: false)) { + return new ElementResponse()->failure($element, mb_ucfirst(t('Couldn’t create {type}.', [ + 'type' => $element::lowerDisplayName(), + ]))); + } + + // Redirect to its edit page + $editUrl = $element->getCpEditUrl() ?? Url::actionUrl('elements/edit', [ + 'draftId' => $element->draftId, + 'siteId' => $element->siteId, + ]); + + $response = new ElementResponse()->success($element, t('{type} created.', [ + 'type' => t('Draft'), + ]), array_filter([ + 'cpEditUrl' => $this->request->isCpRequest ? $editUrl : null, + ])); + + if (! $this->request->acceptsJson()) { + return redirect(Url::urlWithParams($editUrl, ['fresh' => '1'])); + } + + return $response; + } +} diff --git a/src/Http/Controllers/Elements/DeleteElementController.php b/src/Http/Controllers/Elements/DeleteElementController.php new file mode 100644 index 00000000000..26d4286b50f --- /dev/null +++ b/src/Http/Controllers/Elements/DeleteElementController.php @@ -0,0 +1,77 @@ +request->element(); + + // If this is a provisional draft, delete the canonical + if ($element && $element->isProvisionalDraft) { + $element = $element->getCanonical(true); + } + + if (! $element || $element->getIsDraft() || $element->getIsRevision()) { + abort(400, 'No element was identified by the request.'); + } + + Gate::authorize('delete', $element); + + if (! $this->elements->deleteElement($element)) { + return new ElementResponse()->failure($element, t('Couldn’t delete {type}.', [ + 'type' => $element::lowerDisplayName(), + ])); + } + + return new ElementResponse()->success($element, t('{type} deleted.', [ + 'type' => $element::displayName(), + ])); + } + + public function destroyForSite(): Response + { + $element = $this->request->element(checkForProvisionalDraft: true); + + if (! $element || $element->getIsRevision()) { + abort(400, 'No element was identified by the request.'); + } + + Gate::authorize('deleteForSite', $element); + + $this->elements->deleteElementForSite($element); + + if ($element->isProvisionalDraft) { + // see if the canonical element exists for this site + $canonical = $element->getCanonical(); + + if ($canonical->id !== $element->id) { + $element = $canonical; + + $this->elements->deleteElementForSite($element); + } + } + + return new ElementResponse()->success($element, t('{type} deleted for site.', [ + 'type' => $element->getIsDraft() && ! $element->isProvisionalDraft + ? t('Draft') + : $element::displayName(), + ])); + } +} diff --git a/src/Http/Controllers/Elements/DuplicateElementController.php b/src/Http/Controllers/Elements/DuplicateElementController.php new file mode 100644 index 00000000000..76814d562d4 --- /dev/null +++ b/src/Http/Controllers/Elements/DuplicateElementController.php @@ -0,0 +1,162 @@ +request->element(); + + if (! $element || $element->getIsRevision()) { + abort(400, 'No element was identified by the request.'); + } + + // save as a new is now available to people who can create drafts + $asUnpublishedDraft = $this->request->boolean('asUnpublishedDraft') && $element::hasDrafts(); + $asUnpublishedDraft + ? Gate::authorize('duplicateAsDraft', $element) + : Gate::authorize('duplicate', $element); + + $newAttributes = [ + 'isProvisionalDraft' => false, + 'draftId' => null, + ]; + + if ($asUnpublishedDraft && + ($element->getIsCanonical() || $element->isProvisionalDraft) && + $element->slug === $element->getCanonical()->slug + ) { + $newAttributes += [ + 'slug' => null, + ]; + } + + if ($element instanceof NestedElementInterface) { + $newAttributes += [ + 'primaryOwnerId' => $element->getOwnerId(), + 'ownerId' => $element->getOwnerId(), + 'sortOrder' => null, + ]; + } + + try { + $newElement = $this->elements->duplicateElement( + $element, + $newAttributes, + asUnpublishedDraft: $asUnpublishedDraft, + ); + } catch (InvalidElementException $e) { + return new ElementResponse()->failure($e->element, t('Couldn’t duplicate {type}.', [ + 'type' => $element::lowerDisplayName(), + ])); + } + + // If the original element is a provisional draft, + // delete the draft as the changes are likely no longer wanted. + if ($this->request->boolean('deleteProvisionalDraft') && $element->isProvisionalDraft) { + $this->elements->deleteElement($element); + } + + return new ElementResponse()->success($newElement, t('{type} duplicated.', [ + 'type' => $element::displayName(), + ])); + } + + public function bulkDuplicate(): Response + { + $this->request->validate([ + 'elements' => ['required', 'array'], + 'newAttributes' => ['required', 'array'], + ]); + + $elementInfo = $this->request->array('elements'); + $newAttributes = $this->request->array('newAttributes'); + + $newElementInfo = []; + + $result = DB::transaction(function () use ($elementInfo, $newAttributes, &$newElementInfo) { + return BulkOps::ensure(function () use ($elementInfo, $newAttributes, &$newElementInfo) { + foreach ($elementInfo as $info) { + $element = $this->request->element($info); + + if (! $element instanceof ElementInterface) { + Log::warning(sprintf('Unable to duplicate element: %s', Json::encode($info)), [__METHOD__]); + + continue; + } + + $safeNewAttributes = collect($newAttributes) + ->only($element->safeAttributes()) + ->all(); + + // if element is a revision, we need to nullify some additional attributes + if ($element->getIsRevision()) { + $safeNewAttributes['revisionId'] = null; + + if ($element->dateDeleted !== null) { + $safeNewAttributes['dateDeleted'] = null; + $safeNewAttributes['deletedWithOwner'] = null; + $safeNewAttributes['trashed'] = false; + } + } + + try { + $newElement = $this->elements->duplicateElement( + $element, + $safeNewAttributes + $element::baseBulkDuplicateAttributes(), + false, + checkAuthorization: true, + ); + } catch (InvalidElementException $e) { + return new ElementResponse()->failure($e->element, t('Couldn’t duplicate {type}.', [ + 'type' => $element::lowerDisplayName(), + ])); + } + + $newElementInfo[] = $newElement->toArray($newElement->attributes()); + } + + return null; + }); + }); + + if ($result !== null) { + return $result; + } + + /** @var class-string $elementType */ + $elementType = $elementInfo[0]['type']; + + return $this->asSuccess(mb_ucfirst(t('{type} duplicated.', [ + 'type' => count($elementInfo) === 1 ? $elementType::displayName() : $elementType::pluralDisplayName(), + ])), [ + 'newElements' => $newElementInfo, + ]); + } +} diff --git a/src/Http/Controllers/Elements/EditElementController.php b/src/Http/Controllers/Elements/EditElementController.php new file mode 100644 index 00000000000..9770b0b8b14 --- /dev/null +++ b/src/Http/Controllers/Elements/EditElementController.php @@ -0,0 +1,744 @@ +element = $element; + + return $this; + } + + public function __invoke(): Response|CpScreenResponse + { + $strictSite = $this->request->acceptsJson(); + $elementId = $this->request->route('id') ?? $this->request->integer('elementId'); + + /** + * @var Element|Response|null $element + */ + $element = $this->element ?? $this->request->element([ + 'id' => $elementId, + ], checkForProvisionalDraft: true, strictSite: $strictSite); + + if ($element instanceof Response) { + return $element; + } + + if (! $element) { + abort(400, 'No element was identified by the request.'); + } + + // If this is an outdated draft, merge in the latest canonical changes + $mergeCanonicalChanges = ( + $element::trackChanges() && + $element->getIsDraft() && + ! $element->getIsUnpublishedDraft() && + ElementHelper::isOutdated($element) + ); + + if ($mergeCanonicalChanges) { + $this->elements->mergeCanonicalChanges($element); + } + + $this->applyParamsToElement($element); + + // Prevalidate? + if ($this->request->boolean('prevalidate') && $element->enabled && $element->getEnabledForSite()) { + $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); + $element->validate(); + } + + // Figure out what we're dealing with here + $isCanonical = $element->getIsCanonical(); + $isDraft = $element->getIsDraft(); + $isUnpublishedDraft = $element->getIsUnpublishedDraft(); + $isRevision = $element->getIsRevision(); + $isCurrent = $isCanonical || $element->isProvisionalDraft; + $canonical = $element->getCanonical(true); + + // Site info + $supportedSites = ElementHelper::supportedSitesForElement($element, true); + $allEditableSiteIds = $this->sites->getEditableSiteIds()->all(); + $propSites = array_values(array_filter($supportedSites, fn ($site) => $site['propagate'])); + $propSiteIds = array_column($propSites, 'siteId'); + $propEditableSiteIds = array_intersect($propSiteIds, $allEditableSiteIds); + $addlEditableSites = array_values(array_filter($supportedSites, fn ($site) => ! $site['propagate'] && in_array($site['siteId'], $allEditableSiteIds))); + $canEditMultipleSites = count($propEditableSiteIds) > 1 || $addlEditableSites; + + // Permissions + $canSave = $this->canSave($element, $this->request->user()); + $canSaveCanonical = Gate::check('saveCanonical', $element); + $canCreateDrafts = Gate::check('createDrafts', $canonical); + $canDuplicate = ! $isRevision && Gate::check('duplicateAsDraft', $element); + + // Preview targets + $previewTargets = $element->id ? $element->getPreviewTargets() : []; + $enablePreview = ( + ! empty($previewTargets) && + ! $this->request->isMobileBrowser(true) && + ( + ($isDraft && $canSave) || + ($isCurrent && $canCreateDrafts) + ) + ); + + if ($previewTargets) { + match (true) { + $isDraft && ! $element->isProvisionalDraft => SessionAuth::authorize("previewDraft:$element->draftId"), + $isRevision => SessionAuth::authorize("previewRevision:$element->revisionId"), + default => SessionAuth::authorize("previewElement:$canonical->id"), + }; + } + + // Screen prep + [$docTitle, $title] = $this->editElementTitles($element); + $enabledForSite = $element->getEnabledForSite(); + $hasRoute = $element->getRoute() !== null; + $redirectUrl = $this->request->getSigned('returnUrl') ?? Url::cpReferralUrl() ?? ElementHelper::postEditUrl($element); + + // Site statuses + if ($canEditMultipleSites) { + $siteStatuses = ElementHelper::siteStatusesForElement($element, true); + } else { + $siteStatuses = [ + $element->siteId => $element->enabled, + ]; + } + + $previewToken = $previewTargets ? Str::random(32, extendedChars: true) : null; + + $notice = match (true) { + $element->isProvisionalDraft => $this->draftNotice(...), + $isRevision => fn () => $this->revisionNotice($element::lowerDisplayName()), + default => null, + }; + + $enabledSiteIds = $element->enabled && $element->id + ? array_flip($this->elements->getEnabledSiteIdsForElement($element->id)) + : []; + + $response = new CpScreenResponse() + ->editUrl($element->getCpEditUrl()) + ->docTitle($docTitle) + ->title($title) + ->site($element::isLocalized() ? $element->getSite() : null) + ->selectableSites(array_map(fn (int $siteId) => [ + 'site' => $this->sites->getSiteById($siteId), + 'status' => isset($enabledSiteIds[$siteId]) ? 'enabled' : 'disabled', + ], $propEditableSiteIds)) + ->crumbs($this->crumbs($element)) + ->contextMenuItems(fn () => $this->contextMenuItems( + element: $element, + isUnpublishedDraft: $isUnpublishedDraft, + canCreateDrafts: $canCreateDrafts, + )) + ->toolbarHtml( + // if we're in a slideout, we don't want to add the .flex-grow to the header toolbar + // as it'll mess with the width available for the tabs + // see https://github.com/craftcms/cms/issues/17260 + ($this->isSlideout() ? '' : Html::tag('div', attributes: ['class' => 'flex-grow'])). + Html::tag('div', attributes: ['class' => 'activity-container']), + ) + ->additionalButtonsHtml(fn () => $this->additionalButtons( + element: $element, + canonical: $canonical, + isRevision: $isRevision, + canSave: $canSave, + canSaveCanonical: $canSaveCanonical, + canCreateDrafts: $canCreateDrafts, + canDuplicate: $canDuplicate, + previewTargets: $previewTargets, + enablePreview: $enablePreview, + isCurrent: $isCurrent, + isUnpublishedDraft: $isUnpublishedDraft, + isDraft: $isDraft + )) + ->actionMenuItems(fn () => $this->actionMenuItems($element, $previewTargets)) + ->noticeHtml($notice) + ->errorSummary(fn () => new ElementResponse()->errorSummary($element)) + ->prepareScreen( + fn (CpScreenResponse $response, string $containerId) => $this->prepareEditor( + $element, + $isUnpublishedDraft, + $canSave, + $response, + $containerId, + fn (?FieldLayoutForm $form) => $this->editorContent($element, $canSave, $form), + fn (?FieldLayoutForm $form) => $this->editorSidebar($element, $mergeCanonicalChanges, $canSave), + fn (?FieldLayoutForm $form) => [ + 'additionalSites' => $addlEditableSites, + 'canCreateDrafts' => $canCreateDrafts, + 'canEditMultipleSites' => $canEditMultipleSites, + 'canSave' => $canSave, + 'canSaveCanonical' => $canSaveCanonical, + 'elementId' => $element->id, + 'canonicalId' => $canonical->id, + 'draftId' => $element->draftId, + 'draftName' => $isDraft ? $element->draftName : null, + 'elementType' => $element::class, + 'enablePreview' => $enablePreview, + 'enabledForSite' => $element->enabled && $enabledForSite, + 'hashedCpEditUrl' => Crypt::encrypt('{cpEditUrl}'), + 'isLive' => $isCurrent && ! $element->getIsDraft() && $element->enabled && $enabledForSite && $hasRoute, + 'isProvisionalDraft' => $element->isProvisionalDraft, + 'isUnpublishedDraft' => $isUnpublishedDraft, + 'previewTargets' => $previewTargets, + 'previewToken' => $previewToken, + 'hashedPreviewToken' => $previewToken ? Crypt::encrypt($previewToken) : null, + 'previewParamValue' => $previewTargets ? Crypt::encrypt(Str::random(10)) : null, + 'revisionId' => $element->revisionId, + 'fieldId' => $element instanceof NestedElementInterface ? $element->getField()?->id : null, + 'ownerId' => $element instanceof NestedElementInterface ? $element->getOwnerId() : null, + 'siteId' => $element->siteId, + 'siteStatuses' => $siteStatuses, + 'siteToken' => (! app()->isLive() || ! $element->getSite()->getEnabled()) ? Crypt::encrypt((string) $element->siteId) : null, + 'visibleLayoutElements' => $form?->getVisibleElements() ?? [], + 'staticLayoutElements' => $form?->getStaticElements() ?? [], + 'updatedTimestamp' => $element->dateUpdated?->getTimestamp(), + 'canonicalUpdatedTimestamp' => $canonical->dateUpdated?->getTimestamp(), + 'isStatic' => $isRevision || ! $canSave, + ] + ) + ); + + if ($canSave) { + match (true) { + $isUnpublishedDraft => $response->when( + value: $canSaveCanonical, + callback: fn (CpScreenResponse $response) => $response + ->submitButtonLabel(mb_ucfirst(t('Create {type}', [ + 'type' => $element::lowerDisplayName(), + ]))) + ->action('elements/apply-draft') + ->redirectUrl("$redirectUrl#"), + default: fn (CpScreenResponse $response) => $response + ->action('elements/save-draft') + ->redirectUrl("$redirectUrl#") + ), + $element->isProvisionalDraft => $response + ->action('elements/apply-draft') + ->redirectUrl("$redirectUrl#"), + $isDraft => $response + ->submitButtonLabel(mb_ucfirst(t('Save {type}', [ + 'type' => t('draft'), + ]))) + ->action('elements/save-draft') + ->redirectUrl('{cpEditUrl}'), + default => $response + ->action('elements/save') + ->redirectUrl("$redirectUrl#") + }; + + $response + ->saveShortcutRedirectUrl('{cpEditUrl}') + ->altActions($element->getAltActions()); + } + + return $response; + } + + private function draftNotice(): string + { + return + Html::beginTag('div', [ + 'class' => 'draft-notice', + ]). + Html::tag('div', '', [ + 'class' => ['draft-icon'], + 'aria' => ['hidden' => 'true'], + 'data' => ['icon' => 'edit'], + ]). + Html::tag('p', t('Showing your unsaved changes.')). + Html::button(t('Discard'), [ + 'class' => ['discard-changes-btn', 'btn'], + ]). + Html::endTag('div'); + } + + private function revisionNotice(string $elementType): string + { + return + Html::beginTag('div', [ + 'class' => 'content-notice', + ]). + Html::tag('div', '', [ + 'class' => ['content-notice-icon'], + 'aria' => ['hidden' => 'true'], + 'data' => ['icon' => 'lightbulb'], + ]). + Html::tag('p', t('You’re viewing a revision. None of the {type}’s fields are editable.', [ + 'type' => $elementType, + ])). + Html::endTag('div'); + } + + private function contextMenuItems( + ElementInterface $element, + bool $isUnpublishedDraft, + bool $canCreateDrafts, + ): array { + if ($element->isProvisionalDraft) { + $element = $element->getCanonical(true); + } + + if (! $element->id || $element->getIsUnpublishedDraft()) { + return []; + } + + if (! $isUnpublishedDraft) { + $drafts = $element::find() + ->draftOf($element) + ->siteId($element->siteId) + ->status(null) + ->orderByDesc('dateUpdated') + ->with(['draftCreator']) + ->get() + ->filter(fn (ElementInterface $draft) => $this->request->user()->can('view', $draft)) + ->all(); + } else { + $drafts = []; + } + + $revisionsPageUrl = null; + $hasMoreRevisions = false; + + if ($element->hasRevisions() && Cms::config()->maxRevisions !== 1) { + $revisionsQuery = $element::find() + ->revisionOf($element) + ->siteId($element->siteId) + ->status(null) + ->offset(1) + ->limit(Cms::config()->maxRevisions ? min(Cms::config()->maxRevisions - 1, 10) : 10) + ->orderByDesc('dateCreated') + ->with(['revisionCreator']); + + $revisions = (clone $revisionsQuery)->get(); + $revisionsPageUrl = $element->getCpRevisionsUrl(); + + if ($revisionsPageUrl) { + $hasMoreRevisions = ($revisionsQuery->getCountForPagination() - 1) > 0; + } + } else { + $revisions = collect(); + } + + // if we're viewing a revision, make sure it's in the list + if ( + $element->getIsRevision() && + $revisions->doesntContain(fn (ElementInterface $revision) => $revision->id === $element->id) + ) { + $revisions[] = $element; + } + + if (empty($drafts) && empty($revisions) && ! $canCreateDrafts) { + return []; + } + + $formatter = I18N::getFormatter(); + + $baseParams = $this->request->query(); + unset($baseParams['draftId'], $baseParams['revisionId'], $baseParams['siteId'], $baseParams['fresh']); + if (isset(Cms::config()->pathParam)) { + unset($baseParams[Cms::config()->pathParam]); + } + + $isDraft = $element->getIsDraft(); + $isRevision = $element->getIsRevision(); + $cpEditUrl = Url::cpUrl($element->getCpEditUrl(), [ + 'draftId' => null, + 'revisionId' => null, + ]); + + $revision = $element->getCurrentRevision(); + $creator = $revision?->getRevisionCreator(); + $timestamp = $formatter->asTimestamp($revision->dateCreated ?? $element->dateUpdated, Locale::LENGTH_SHORT, true); + + $items = [ + [ + 'heading' => t('Context'), + 'headingTag' => 'h2', + 'headingAttributes' => ['class' => ['visually-hidden']], + 'listAttributes' => ['class' => ['revision-group-current']], + 'items' => [ + [ + 'label' => t('Current'), + 'description' => $creator + ? t('Saved {timestamp} by {creator}', [ + 'timestamp' => $timestamp, + 'creator' => $creator->name, + ]) + : t('Last saved {timestamp}', [ + 'timestamp' => $timestamp, + ]), + 'url' => $cpEditUrl, + 'selected' => ! $isDraft && ! $isRevision, + ], + ], + ], + ]; + + if (! empty($drafts)) { + $items[] = [ + 'heading' => t('Drafts'), + 'listAttributes' => ['class' => ['revision-group-drafts']], + 'items' => array_map(function ($draft) use ($element, $formatter, $cpEditUrl, $baseParams) { + /** @var ElementInterface $draft */ + $creator = $draft->getDraftCreator(); + $timestamp = $formatter->asTimestamp($draft->dateUpdated, Locale::LENGTH_SHORT, true); + $timestampWithDate = $formatter->asDatetime($draft->dateUpdated, Locale::LENGTH_SHORT); + + return [ + 'label' => $draft->draftName, + 'description' => $creator + ? Template::raw(t('Saved by {creator}', [ + 'timestampWithDate' => $timestampWithDate, + 'timestamp' => $timestamp, + 'creator' => Html::encode($creator->name), + ])) + : Template::raw(t('Last saved ', [ + 'timestampWithDate' => $timestampWithDate, + 'timestamp' => $timestamp, + ])), + 'url' => Url::urlWithParams($cpEditUrl, array_merge($baseParams, [ + 'draftId' => $draft->draftId, + ])), + 'selected' => $draft->id === $element->id, + ]; + }, $drafts), + ]; + } + + if (! empty($revisions)) { + $items[] = [ + 'heading' => t('Recent Revisions'), + 'listAttributes' => ['class' => ['revision-group-revisions']], + 'items' => $revisions->map(function (ElementInterface $revision) use ($element, $formatter, $cpEditUrl, $baseParams) { + $creator = $revision->getRevisionCreator(); + $timestamp = $formatter->asTimestamp($revision->dateCreated, Locale::LENGTH_SHORT, true); + $timestampWithDate = $formatter->asDatetime($revision->dateCreated, Locale::LENGTH_SHORT); + + return [ + 'label' => $revision->getRevisionLabel(), + 'description' => $creator + ? Template::raw(t('Saved by {creator}', [ + 'timestampWithDate' => $timestampWithDate, + 'timestamp' => $timestamp, + 'creator' => Html::encode($creator->name), + ])) + : Template::raw(t('Saved ', [ + 'timestampWithDate' => $timestampWithDate, + 'timestamp' => $timestamp, + ])), + 'url' => Url::urlWithParams($cpEditUrl, array_merge($baseParams, [ + 'revisionId' => $revision->revisionId, + ])), + 'selected' => $revision->id === $element->id, + ]; + })->all(), + ]; + } + + if ($hasMoreRevisions && $revisionsPageUrl) { + $items[] = ['type' => MenuItemType::HR]; + $items[] = [ + 'label' => t('View all revisions'), + 'url' => $revisionsPageUrl, + 'attributes' => [ + 'class' => ['go'], + ], + ]; + } + + return $items; + } + + private function isSlideout(): bool + { + return $this->request->hasHeader('X-Craft-Container-Id'); + } + + private function additionalButtons( + ElementInterface $element, + ElementInterface $canonical, + bool $isRevision, + bool $canSave, + bool $canSaveCanonical, + bool $canCreateDrafts, + bool $canDuplicate, + ?array $previewTargets, + bool $enablePreview, + bool $isCurrent, + bool $isUnpublishedDraft, + bool $isDraft, + ): string { + $components = []; + + // Preview (View will be added later by JS) + if ($previewTargets) { + $components[] = + Html::beginTag('div', [ + 'class' => ['preview-btn-container', 'btngroup'], + ]). + ($enablePreview + ? Html::beginTag('button', [ + 'type' => 'button', + 'class' => ['preview-btn', 'btn'], + ]). + Html::tag('span', t('Preview'), ['class' => 'label']). + Html::endTag('button') + : ''). + Html::endTag('div'); + } + + // Create a draft + if ($isCurrent && ! $isUnpublishedDraft && $canCreateDrafts) { + if ($canSave) { + $components[] = Html::button(t('Create a draft'), [ + 'class' => ['btn', 'formsubmit'], + 'data' => [ + 'action' => 'elements/save-draft', + 'redirect' => Crypt::encrypt('{cpEditUrl}'), + 'params' => ['dropProvisional' => 1], + ], + ]); + } else { + $components[] = Html::beginForm(). + Html::actionInput('elements/save-draft'). + Html::redirectInput('{cpEditUrl}'). + Html::hiddenInput('elementId', (string) $canonical->id). + Html::button(t('Create a draft'), [ + 'class' => ['btn', 'formsubmit'], + ]). + Html::endForm(); + } + } + + if (! $canSave && $canDuplicate) { + // save as a new is now available to people who can create drafts + $components[] = Html::beginForm(). + Html::actionInput('elements/duplicate'). + Html::redirectInput('{cpEditUrl}'). + Html::hiddenInput('elementId', (string) $canonical->id). + Html::hiddenInput('asUnpublishedDraft', '1'). + Html::button(t('Save as a new {type}', ['type' => $element::lowerDisplayName()]), [ + 'class' => ['btn', 'formsubmit'], + ]). + Html::endForm(); + } + + // Apply draft + if ($isDraft && ! $isCurrent && $canSave && $canSaveCanonical) { + $components[] = Html::button(t('Apply draft'), [ + 'class' => ['btn', 'secondary', 'formsubmit', 'tooltip-draft-btn'], + 'data' => [ + 'action' => 'elements/apply-draft', + 'redirect' => Crypt::encrypt('{cpEditUrl}'), + ], + ]); + } + + // Revert content from this revision + if ($isRevision && $canSaveCanonical && $element->hasRevisions()) { + $components[] = Html::beginForm(). + Html::actionInput('elements/revert'). + Html::redirectInput('{cpEditUrl}'). + Html::hiddenInput('elementId', (string) $canonical->id). + Html::hiddenInput('revisionId', (string) $element->revisionId). + Html::button(t('Revert content from this revision'), [ + 'class' => ['btn', 'formsubmit', 'revision-draft-btn'], + ]). + Html::endForm(); + } + + $components[] = $element->getAdditionalButtons(); + + return implode("\n", array_filter($components)); + } + + private function actionMenuItems(ElementInterface $element, array $previewTargets): array + { + if (! $element->id) { + return []; + } + + $hideViewAction = ! empty($previewTargets) && ! $this->isSlideout(); + + return array_filter( + $element->getActionMenuItems(), + function (array $item) use ($hideViewAction) { + // filter out "Edit" item - no point showing edit action on the edit page, + if (str_starts_with($item['id'] ?? '', 'action-edit-')) { + return false; + } + + // and "View in a new tab" item, if we have at least one preview target, and it's not a slideout + // as that action is already covered by the "View" button; + // (https://github.com/craftcms/cms/issues/16556) + if ($hideViewAction && str_starts_with($item['id'] ?? '', 'action-view-')) { + return false; + } + + return true; + }, + ); + } + + private function prepareEditor( + ElementInterface $element, + bool $isUnpublishedDraft, + bool $canSave, + CpScreenResponse $response, + string $containerId, + callable $contentFn, + callable $sidebarFn, + callable $jsSettingsFn, + ) { + $fieldLayout = $element->getFieldLayout(); + $form = $fieldLayout?->createForm($element, ! $canSave, [ + 'registerDeltas' => true, + ]); + $contentHtml = $contentFn($form); + $sidebarHtml = $sidebarFn($form); + + if ($contentHtml === '' && $sidebarHtml !== '' && $this->request->acceptsJson()) { + $contentHtml = Html::tag('div', $sidebarHtml, [ + 'class' => 'details', + ]); + $sidebarHtml = ''; + $response->slideoutBodyClass = 'so-full-details'; + } + + if ($canSave) { + $components = []; + + if ($element->id) { + // don't use the canonical ID if this is a normal element that's keeping track of its canonical + // e.g. nested Matrix entries that were duplicated for an owner's draft + $id = $element->getIsDraft() || $element->getIsRevision() ? $element->getCanonicalId() : $element->id; + $components[] = Html::hiddenInput('elementId', (string) $id); + } + + if ($element->siteId) { + $components[] = Html::hiddenInput('siteId', (string) $element->siteId); + } + + if ($element->fieldLayoutId) { + $components[] = Html::hiddenInput('fieldLayoutId', (string) $element->fieldLayoutId); + } + + if ($isUnpublishedDraft && $this->request->boolean('fresh')) { + $components[] = Html::hiddenInput('fresh', '1'); + } + + if ($this->request->boolean('updateSearchIndexImmediately')) { + $components[] = Html::hiddenInput('updateSearchIndexImmediately', '1'); + } + + $components[] = $contentHtml; + $contentHtml = implode("\n", $components); + } + + $response->tabs($form?->getTabMenu() ?? []); + $response->contentHtml($contentHtml); + $response->metaSidebarHtml($sidebarHtml); + + $settings = $jsSettingsFn($form); + + if ($this->isSlideout()) { + HtmlStack::jsWithVars(fn ($settings) => << <<prepareEditScreen($response, $containerId); + } + + private function editorContent(ElementInterface $element, bool $canSave, ?FieldLayoutForm $form): string + { + $html = $form?->render() ?? ''; + + event($event = new DefineElementEditorContent($element, $html, ! $canSave)); + + return trim($event->html); + } + + private function editorSidebar(ElementInterface $element, bool $mergedCanonicalChanges, bool $canSave): string + { + $components = []; + + if ($mergedCanonicalChanges) { + $components[] = + Html::beginTag('div', [ + 'class' => ['meta', 'warning'], + ]). + Html::tag('p', t('Recent changes to the Current revision have been merged into this draft.')). + Html::endTag('div'); + } + + $components[] = $element->getSidebarHtml(! $canSave); + + if ($this->request->route()?->getActionName()) { + $components[] = app(ContentHtml::class)->metadataHtml($element->getMetadata()); + } + + return trim(implode("\n", $components)); + } +} diff --git a/src/Http/Controllers/Elements/ElementActivityController.php b/src/Http/Controllers/Elements/ElementActivityController.php new file mode 100644 index 00000000000..2d7b4156ecb --- /dev/null +++ b/src/Http/Controllers/Elements/ElementActivityController.php @@ -0,0 +1,43 @@ +request->element(); + + if ($element instanceof Response) { + return $element; + } + + if (! $element || $element->getIsRevision()) { + abort(400, 'No element was identified by the request.'); + } + + $activity = $this->elementActivity->getRecentActivity($element, $this->request->user()->id); + + $this->elementActivity->trackActivity($element, ElementActivityType::View, $this->request->user()); + + return new JsonResponse([ + 'activity' => $activity->map(fn (ElementActivityData $record) => $record->toActivityRow($element))->all(), + 'updatedTimestamp' => $element->dateUpdated->getTimestamp(), + 'canonicalUpdatedTimestamp' => $element->getCanonical()->dateUpdated->getTimestamp(), + ]); + } +} diff --git a/src/Http/Controllers/Elements/ElementDraftsController.php b/src/Http/Controllers/Elements/ElementDraftsController.php new file mode 100644 index 00000000000..b022c151c54 --- /dev/null +++ b/src/Http/Controllers/Elements/ElementDraftsController.php @@ -0,0 +1,392 @@ +request->element(); + + // this can happen if we're creating e.g. nested entry in a matrix field (cards or element index) + // and we hit "create entry" before the autosave kicks in + if ($element instanceof Response) { + return $element; + } + + if (! $element || $element->getIsRevision()) { + abort(400, 'No element was identified by the request.'); + } + + $provisional = $this->request->boolean('provisional'); + + if (! $element->getIsDraft() && ! $provisional) { + Gate::authorize('createDrafts', $element); + } elseif (! $this->canSave($element, $this->request->user())) { + abort(403, 'User not authorized to save this element.'); + } + + if (! $element->getIsDraft() && $provisional) { + // Make sure a provisional draft doesn't already exist for this element/user combo + $existingProvisionalDraft = $element::find() + ->provisionalDrafts() + ->draftOf($element->id) + ->draftCreator($this->request->user()->id) + ->site('*') + ->status(null) + ->one(); + + if ($existingProvisionalDraft) { + Log::warning("Overwriting an existing provisional draft for element/user $element->id/{$this->request->user()->id}", [__METHOD__]); + + $this->elements->deleteElement($existingProvisionalDraft, true); + } + } + + // Keep track of all newly-created draft IDs + $draftElementIds = []; + $draftElementUids = []; + + Event::listen(function (DraftCreated $event) use (&$draftElementIds, &$draftElementUids) { + $draftElementIds[$event->canonical->id] = $event->draft->id; + $draftElementUids[$event->canonical->uid] = $event->draft->uid; + }); + + DB::beginTransaction(); + + try { + // Are we creating the draft here? + if (! $element->getIsDraft()) { + /** @var Element $element */ + $draft = $this->drafts->createDraft( + canonical: $element, + creatorId: $this->request->user()->id, + provisional: $provisional, + ); + + $draft->setCanonical($element); + + $element = $draft; + } + + // keep track of the original field layout ID, in case it changes here + $oldFieldLayoutId = $element->getFieldLayout()?->id; + + $this->applyParamsToElement($element); + + // Make sure nothing just changed that would prevent the user from saving + if (! $this->canSave($element, $this->request->user())) { + abort(403, 'User not authorized to save this element.'); + } + + if ($this->request->boolean('dropProvisional')) { + $element->isProvisionalDraft = false; + } + + $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS); + + // If the field layout ID changed, save all content + $saveContent = $element->getFieldLayout()?->id !== $oldFieldLayoutId; + + if (! $this->elements->saveElement($element, saveContent: $saveContent)) { + DB::rollBack(); + + return new ElementResponse()->failure($element, mb_ucfirst(t('Couldn’t save {type}.', [ + 'type' => t('draft'), + ]))); + } + + DB::commit(); + } catch (Throwable $e) { + DB::rollBack(); + throw $e; + } + + $this->elementActivity->trackActivity($element, ElementActivityType::Save); + + $data = [ + 'canonicalId' => $element->getCanonicalId(), + 'elementId' => $element->id, + 'draftId' => $element->draftId, + 'timestamp' => I18N::getFormatter()->asTimestamp($element->dateUpdated, 'short', true), + 'creator' => $element->getDraftCreator()?->getName(), + 'draftName' => $element->draftName, + 'draftNotes' => $element->draftNotes, + 'modifiedAttributes' => $element->getModifiedAttributes(), + 'draftElementIds' => $draftElementIds, + 'draftElementUids' => $draftElementUids, + ]; + + if ($this->request->isCpRequest()) { + [$docTitle, $title] = $this->editElementTitles($element); + $previewTargets = $element->getPreviewTargets(); + $data += $this->fieldLayoutData($element, [ + 'registerDeltas' => true, + ]); + $data += [ + 'docTitle' => $docTitle, + 'title' => $title, + 'previewTargets' => $previewTargets, + 'previewParamValue' => $previewTargets ? Crypt::encrypt(Str::random(10)) : null, + 'deltaNames' => DeltaRegistry::getNames(), + 'initialDeltaValues' => DeltaRegistry::getInitialValues(), + 'updatedTimestamp' => $element->dateUpdated->getTimestamp(), + 'canonicalUpdatedTimestamp' => $element->getCanonical()->dateUpdated->getTimestamp(), + ]; + } + + // Make sure the user is authorized to preview the draft + SessionAuth::authorize("previewDraft:$element->draftId"); + + return new ElementResponse()->success($element, t('{type} saved.', [ + 'type' => t('Draft'), + ]), $data, true); + } + + public function ensure(): Response + { + $element = $this->request->element(checkForProvisionalDraft: true); + + if (! $element || $element->getIsRevision()) { + abort(400, 'No element was identified by the request.'); + } + + if ($element->getIsDraft()) { + return $this->asSuccess(data: [ + 'elementId' => $element->id, + ]); + } + + Gate::authorize('createDrafts', $element); + + // Make sure a provisional draft doesn't already exist for this element/user combo + $provisionalId = $element::find() + ->provisionalDrafts() + ->draftOf($element->id) + ->draftCreator($this->request->user()->id) + ->site('*') + ->status(null) + ->ids()[0] ?? null; + + if ($provisionalId) { + return $this->asSuccess(data: [ + 'elementId' => $provisionalId, + ]); + } + + $draft = $this->drafts->createDraft( + canonical: $element, + creatorId: $this->request->user()->id, + provisional: true, + ); + + return $this->asSuccess(data: [ + 'elementId' => $draft->id, + ]); + } + + public function apply(): Response + { + $element = $this->request->element(); + + // this can happen if creating element via slideout, and we hit "create entry" before the autosave kicks in + if ($element instanceof Response) { + return $element; + } + + if (! $element || ! $element->getIsDraft()) { + abort(400, 'No draft was identified by the request.'); + } + + // keep track of the original field layout ID, in case it changes here + $oldFieldLayoutId = $element->getFieldLayout()?->id; + + $this->applyParamsToElement($element); + + Gate::authorize('save', $element); + + $isUnpublishedDraft = $element->getIsUnpublishedDraft(); + + if (! Gate::check('saveCanonical', $element)) { + abort(403, $isUnpublishedDraft + ? 'User not authorized to create this element.' + : 'User not authorized to save this element.' + ); + } + + // Validate and save the draft + if ($element->enabled && $element->getEnabledForSite()) { + $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); + } + + // if we're about to apply an unpublished draft, set propagateRequired to true + if ($isUnpublishedDraft) { + $element->propagateRequired = true; + } + + $element->applyingDraft = true; + + // If the field layout ID changed, save all content + $saveContent = $element->getFieldLayout()?->id !== $oldFieldLayoutId; + + $namespace = $this->request->header('X-Craft-Namespace'); + $crossSiteValidate = $namespace === null && Sites::isMultiSite(); + + if (! $this->elements->saveElement( + element: $element, + crossSiteValidate: $crossSiteValidate, + saveContent: $saveContent, + )) { + // save the draft anyway, so we don’t lose the latest changes + // (see https://github.com/craftcms/cms/issues/18657) + $errors = $element->getErrors(); + $invalidNestedElementIds = $element->getInvalidNestedElementIds(); + $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS); + $this->elements->saveElement(element: $element, saveContent: $saveContent); + $element->clearErrors(); + $element->errors()->merge($errors); + $element->addInvalidNestedElementIds($invalidNestedElementIds); + + return new ElementResponse()->applyDraftFailure($element); + } + + $element->applyingDraft = false; + + if (! $isUnpublishedDraft) { + $mutex = Cache::lock("element:$element->canonicalId", 15); + if (! $mutex->get()) { + abort(500, 'Could not acquire a lock to save the element.'); + } + } + + $attributes = []; + + if ($element instanceof NestedElementInterface) { + $attributes['updateSearchIndexForOwner'] = true; + } + + try { + $element->propagateRequired = false; + $canonical = $this->drafts->applyDraft($element, $attributes); + } catch (InvalidElementException) { + return new ElementResponse()->applyDraftFailure($element); + } finally { + if (! $isUnpublishedDraft) { + $mutex->release(); + } + } + + $this->elementActivity->trackActivity($canonical, ElementActivityType::Save); + + if (! $this->request->expectsJson()) { + // Tell all browser windows about the element save + session()->broadcastToJs([ + 'event' => 'saveElement', + 'id' => $canonical->id, + ]); + + if (! $isUnpublishedDraft) { + session()->broadcastToJs([ + 'event' => 'deleteDraft', + 'canonicalId' => $element->getCanonicalId(), + 'draftId' => $element->draftId, + ]); + } + } + + $message = match (true) { + $isUnpublishedDraft => t('{type} created.', [ + 'type' => $element::displayName(), + ]), + $element->isProvisionalDraft => t('{type} saved.', [ + 'type' => $element::displayName(), + ]), + default => t('Draft applied.'), + }; + + return new ElementResponse()->success($canonical, $message, supportsAddAnother: true); + } + + public function destroy(): Response + { + $element = $this->request->element(); + + if ($element instanceof Response) { + return $element; + } + + if (! $element || ! $element->getIsDraft()) { + abort(400, 'No draft was identified by the request.'); + } + + Gate::authorize('delete', $element); + + if (! $this->elements->deleteElement($element, true)) { + return new ElementResponse()->failure($element, t('Couldn’t delete {type}.', [ + 'type' => t('draft'), + ])); + } + + $message = $element->isProvisionalDraft + ? t('Changes discarded.') + : t('{type} deleted.', [ + 'type' => t('Draft'), + ]); + + if (! $this->request->acceptsJson()) { + // Tell all browser windows about the draft deletion + session()->broadcastToJs([ + 'event' => 'deleteDraft', + 'canonicalId' => $element->getCanonicalId(), + 'draftId' => $element->draftId, + ]); + } + + return new ElementResponse()->success($element, $message); + } +} diff --git a/src/Http/Controllers/Elements/ElementIndex/ElementIndexController.php b/src/Http/Controllers/Elements/ElementIndex/ElementIndexController.php new file mode 100644 index 00000000000..ec9bd91eb9a --- /dev/null +++ b/src/Http/Controllers/Elements/ElementIndex/ElementIndexController.php @@ -0,0 +1,158 @@ +request->elementType(); + [$sourceKey, $source] = $this->resolveSource($elementType, $this->request->input('source'), $this->request->context()); + $elementQueryState = $this->buildElementQueryState( + elementType: $elementType, + source: $source, + condition: $this->request->condition(), + ); + + $total = $elementType::indexElementCount($elementQueryState['query'], $sourceKey); + $unfilteredTotal = $elementQueryState['unfilteredQuery'] + ? $elementType::indexElementCount($elementQueryState['unfilteredQuery'], $sourceKey) + : $total; + + return new JsonResponse([ + 'resultSet' => $this->request->input('resultSet'), + 'total' => $total, + 'unfilteredTotal' => $unfilteredTotal, + ]); + } + + public function filterHud(CurrentElementIndex $currentElementIndex): JsonResponse + { + $elementType = $this->request->elementType(); + $context = $this->request->context(); + [$sourceKey, $source] = $this->resolveSource($elementType, $this->request->input('source'), $context); + $fieldLayouts = $this->resolveFieldLayouts(); + $currentCondition = $this->resolveElementIndexCondition(); + $id = $this->request->input('id'); + + abort_if($id === null || $id === '', 400, 'Request missing required body param'); + + $conditionConfig = $this->request->input('conditionConfig'); + $serialized = $this->request->input('serialized'); + + if (! $conditionConfig && $serialized) { + parse_str((string) $serialized, $conditionConfig); + $conditionConfig = $conditionConfig['condition'] ?? null; + } + + /** @var ElementConditionInterface $condition */ + $condition = $conditionConfig + ? $this->conditions->createCondition($conditionConfig) + : $elementType::createCondition(); + + if (! empty($fieldLayouts)) { + $condition->setFieldLayouts($fieldLayouts); + } + + $condition->mainTag = 'div'; + $condition->id = (string) $id; + $condition->addRuleLabel = t('Add a filter'); + + $this->populateFilterHudQueryParams($condition, $source, $sourceKey, $currentCondition); + $currentElementIndex->activate(); + + return new JsonResponse([ + 'hudHtml' => $condition->getBuilderHtml(), + 'headHtml' => HtmlStack::headHtml(), + 'bodyHtml' => HtmlStack::bodyHtml(), + ]); + } + + public function elementTableHtml(): JsonResponse + { + $this->request->validate([ + 'id' => ['required', 'integer', 'min:1'], + ]); + + $elementType = $this->request->elementType(); + [$sourceKey, $source] = $this->resolveSource($elementType, $this->request->input('source'), $this->request->context()); + $elementQuery = $this->buildElementQueryState( + elementType: $elementType, + source: $source, + condition: $this->request->condition(), + )['query']; + + abort_if(! $sourceKey, 400, 'Request missing required body param'); + + /** @var ElementInterface|null $element */ + $element = (clone $elementQuery) + ->draftOf($this->request->integer('id')) + ->draftCreator($this->request->user()) + ->provisionalDrafts() + ->status(null) + ->one(); + + if (! $element) { + /** @var ElementInterface|null $element */ + $element = (clone $elementQuery) + ->id($this->request->integer('id')) + ->status(null) + ->one(); + } + + abort_if(! $element, 400, 'Invalid element ID: '.$this->request->integer('id')); + + $attributes = $this->elementSources->getTableAttributes( + elementType: $elementType, + sourceKey: $sourceKey, + customAttributes: $this->resolveViewState()['tableColumns'] ?? null, + fieldLayouts: $this->resolveFieldLayouts(), + ); + + $attributeHtml = []; + + foreach ($attributes as [$attribute]) { + $attributeHtml[$attribute] = $element->getAttributeHtml($attribute); + } + + return new JsonResponse([ + 'attributeHtml' => $attributeHtml, + ]); + } +} diff --git a/src/Http/Controllers/Elements/ElementIndex/ElementIndexSourcesController.php b/src/Http/Controllers/Elements/ElementIndex/ElementIndexSourcesController.php new file mode 100644 index 00000000000..dcf6a63d0f5 --- /dev/null +++ b/src/Http/Controllers/Elements/ElementIndex/ElementIndexSourcesController.php @@ -0,0 +1,112 @@ +request->validate([ + 'stepKey' => ['required', 'string'], + ]); + + $elementType = $this->request->elementType(); + $sourceKey = $this->request->input('source', ''); + $stepKey = $this->request->input('stepKey'); + + $currentElementIndex->activate(); + + return new JsonResponse([ + 'sourcePath' => $elementType::sourcePath( + sourceKey: $sourceKey, + stepKey: $stepKey, + context: $this->request->context(), + ), + ]); + } + + public function sourceAttributeInfo(CurrentElementIndex $currentElementIndex): JsonResponse + { + $elementType = $this->request->elementType(); + $context = $this->request->context(); + [$sourceKey] = $this->resolveSource($elementType, $this->request->input('source'), $context); + $fieldLayouts = $this->resolveFieldLayouts(); + + $currentElementIndex->activate(); + + if (! $sourceKey) { + return new JsonResponse([ + 'sortOptions' => [], + 'tableColumns' => [], + 'defaultTableColumns' => [], + ]); + } + + $sortOptions = $this->elementSources->getSourceSortOptions($elementType, $sourceKey) + ->map(fn (array $option) => [ + 'label' => $option['label'], + 'attr' => $option['attribute'] ?? $option['orderBy'], + 'defaultDir' => $option['defaultDir'] ?? 'asc', + ]) + ->values() + ->all(); + + $tableColumns = $this->elementSources->getSourceTableAttributes($elementType, $sourceKey) + ->map(fn (array $attribute, string $key) => [ + ...$attribute, + 'attr' => $key, + ]) + ->values() + ->all(); + + $defaultTableColumns = $this->elementSources->getTableAttributes( + elementType: $elementType, + sourceKey: $sourceKey, + fieldLayouts: $fieldLayouts, + ) + ->map(fn (array $attribute) => $attribute[0]) + ->filter(fn (string $attribute) => $attribute !== 'title') + ->values() + ->all(); + + return new JsonResponse(compact( + 'sortOptions', + 'tableColumns', + 'defaultTableColumns', + )); + } + + public function getSourceTreeHtml(CurrentElementIndex $currentElementIndex): JsonResponse + { + $currentElementIndex->activate(); + + $sources = $this->elementSources->getSources( + elementType: $elementType = $this->request->elementType(), + context: $this->request->context(), + )->all(); + + return new JsonResponse([ + 'html' => template('_elements/sources', [ + 'elementType' => $elementType, + 'sources' => $sources, + ]), + ]); + } +} diff --git a/src/Http/Controllers/Elements/ExportElementIndexController.php b/src/Http/Controllers/Elements/ElementIndex/ExportElementIndexController.php similarity index 77% rename from src/Http/Controllers/Elements/ExportElementIndexController.php rename to src/Http/Controllers/Elements/ElementIndex/ExportElementIndexController.php index e7f272d041f..6f40d8b8840 100644 --- a/src/Http/Controllers/Elements/ExportElementIndexController.php +++ b/src/Http/Controllers/Elements/ElementIndex/ExportElementIndexController.php @@ -2,17 +2,16 @@ declare(strict_types=1); -namespace CraftCms\Cms\Http\Controllers\Elements; +namespace CraftCms\Cms\Http\Controllers\Elements\ElementIndex; use Closure; -use craft\base\ElementInterface; use CraftCms\Cms\Element\Contracts\ElementExporterInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementExporters; -use CraftCms\Cms\Element\ElementSources; use CraftCms\Cms\Element\Exceptions\InvalidTypeException; use CraftCms\Cms\Element\Exporters\Raw; use CraftCms\Cms\Http\Controllers\Elements\Concerns\InteractsWithElementIndexes; -use Illuminate\Http\Request; +use CraftCms\Cms\Http\Requests\ElementIndexRequest; use Symfony\Component\HttpFoundation\Response; readonly class ExportElementIndexController @@ -20,7 +19,7 @@ use InteractsWithElementIndexes; public function __construct( - private Request $request, + private ElementIndexRequest $request, private ElementExporters $elementExporters, ) {} @@ -42,11 +41,11 @@ function (string $attribute, mixed $value, Closure $fail): void { /** @var class-string $elementType */ $elementType = $validated['elementType']; - $context = $this->request->input('context', ElementSources::CONTEXT_INDEX); + $context = $this->request->context(); - [$sourceKey, $source] = $this->source($elementType, $this->request->input('source'), $context); + [$sourceKey, $source] = $this->resolveSource($elementType, $this->request->input('source'), $context); abort_if(! isset($sourceKey), 400, 'Request missing required body param'); - abort_if(! $this->isAdministrative($context), 400, 'Request missing index context'); + abort_if(! $this->request->isAdministrative(), 400, 'Request missing index context'); $exporters = $this->availableExporters($elementType, $sourceKey); $exporter = $this->elementExporters->resolveExporter( @@ -58,7 +57,11 @@ function (string $attribute, mixed $value, Closure $fail): void { return $this->elementExporters->export( exporter: $exporter, - query: $this->elementQuery($elementType, $source, $this->condition()), + query: $this->buildElementQueryState( + elementType: $elementType, + source: $source, + condition: $this->request->condition() + )['query'], format: $this->request->input('format', 'csv'), ); } diff --git a/src/Http/Controllers/Elements/ElementIndex/SaveElementIndexElementsController.php b/src/Http/Controllers/Elements/ElementIndex/SaveElementIndexElementsController.php new file mode 100644 index 00000000000..6e9929c9e11 --- /dev/null +++ b/src/Http/Controllers/Elements/ElementIndex/SaveElementIndexElementsController.php @@ -0,0 +1,143 @@ +request->elementType(); + + $this->request->validate([ + 'siteId' => ['required', 'integer', 'min:1'], + 'namespace' => ['required', 'string'], + $namespace = $this->request->input('namespace') => ['required', 'array'], + ]); + + $data = $this->request->array($namespace); + + $elements = $this->getElements( + elementType: $elementType, + siteId: $this->request->integer('siteId'), + data: $data, + ); + + if ($elements->isEmpty()) { + throw ValidationException::withMessages([ + $namespace => 'No valid element IDs provided.', + ]); + } + + foreach ($elements as $element) { + Gate::authorize('save', $element); + } + + $errors = $this->validateElements($elements, $namespace, $data); + + if (! empty($errors)) { + return new JsonResponse([ + 'errors' => $errors, + ]); + } + + DB::transaction(function () use ($elements) { + foreach ($elements as $element) { + if (! $this->elements->saveElement($element)) { + Log::error("Couldn’t save element {$element->id}: ".implode(', ', $element->getFirstErrors())); + abort(500, "Couldn’t save element {$element->id}"); + } + } + }); + + return $this->asSuccess(); + } + + /** + * @param class-string $elementType + * @param array $data + * @return Collection + */ + private function getElements(string $elementType, int $siteId, array $data): Collection + { + $elementIds = array_map( + fn (string $key): int => (int) Str::chopStart($key, 'element-'), + array_keys($data), + ); + + /** @var Collection */ + return $elementType::find() + ->id($elementIds) + ->status(null) + ->drafts(null) + ->provisionalDrafts(null) + ->siteId($siteId) + ->get(); + } + + /** + * @param Collection $elements + * @param array $data + * @return array>> + */ + private function validateElements(Collection $elements, string $namespace, array $data): array + { + $errors = []; + + foreach ($elements as $element) { + $attributes = Arr::except($data["element-$element->id"] ?? [], 'fields'); + + if (! empty($attributes)) { + $element->ruleset->withScenario( + ElementRules::SCENARIO_LIVE, + fn () => $element->setAttributesFromRequest($attributes), + ); + } + + $element->setFieldValuesFromRequest("$namespace.element-$element->id.fields"); + + if ($element->getIsUnpublishedDraft()) { + $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS); + } elseif ($element->enabled && $element->getEnabledForSite()) { + $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); + } + + $names = array_merge( + array_keys($attributes), + array_map( + fn (string $handle): string => "field:$handle", + array_keys($data["element-$element->id"]['fields'] ?? []), + ), + ); + + if (! $element->validate($names)) { + $errors[$element->getCanonicalId()] = $element->errors()->getMessages(); + } + } + + return $errors; + } +} diff --git a/src/Http/Controllers/Elements/ElementRedirectController.php b/src/Http/Controllers/Elements/ElementRedirectController.php new file mode 100644 index 00000000000..9a320dff56c --- /dev/null +++ b/src/Http/Controllers/Elements/ElementRedirectController.php @@ -0,0 +1,53 @@ +request->route('id'); + $uid = $this->request->route('uid'); + + if (is_numeric($id)) { + $id = (int) $id; + } else { + $id = null; + } + + $element = $this->request->element([ + 'id' => $id, + 'uid' => $uid, + ]); + + if ($element instanceof Response) { + return $element; + } + + $url = $element->getCpEditUrl(); + + abort_if(! $url, 500, 'The element doesn’t have an edit page.'); + + $editUrl = Url::removeParam(Url::cpUrl('edit'), 'site'); + + if (str_starts_with((string) $url, $editUrl)) { + return app(EditElementController::class)->setElement($element)(); + } + + return redirect($url); + } +} diff --git a/src/Http/Controllers/Elements/ElementRevisionsController.php b/src/Http/Controllers/Elements/ElementRevisionsController.php new file mode 100644 index 00000000000..4ebd78cf136 --- /dev/null +++ b/src/Http/Controllers/Elements/ElementRevisionsController.php @@ -0,0 +1,84 @@ +request->element([ + 'id' => $this->request->route('id'), + ]); + + if ($element->getIsUnpublishedDraft()) { + abort(400, 'Unpublished drafts don\'t have revisions'); + } + + if (! $element->hasRevisions()) { + abort(400, 'Element doesn\'t have revisions'); + } + + return new CpScreenResponse() + ->title(t('Revisions for “{title}”', [ + 'title' => $element->getUiLabel(), + ])) + ->crumbs([ + ...$this->crumbs($element, current: false), + [ + 'label' => t('Revisions'), + 'current' => true, + ], + ]) + ->contentTemplate('_elements/revisions', [ + 'element' => $element, + 'revisionsQuery' => $element::find() + ->revisionOf($element) + ->site('*') + ->preferSites([$element->siteId]) + ->unique() + ->status(null) + ->whereNot('elements.dateCreated', Query::prepareDateForDb($element->dateUpdated)) + ->with(['revisionCreator']), + ]); + } + + public function revert(Revisions $revisions, ElementActivity $elementActivity): Response + { + $element = $this->request->element(); + + if (! $element || ! $element->getIsRevision()) { + abort(400, 'No revision was identified by the request.'); + } + + Gate::authorize('save', $element->getCanonical(true)); + + $canonical = $revisions->revertToRevision($element, $this->request->user()->id); + + $elementActivity->trackActivity($canonical, ElementActivityType::Save); + + return new ElementResponse()->success($canonical, t('{type} reverted to past revision.', [ + 'type' => $element::displayName(), + ])); + } +} diff --git a/src/Http/Controllers/Elements/ElementSelectorModalController.php b/src/Http/Controllers/Elements/ElementSelectorModalController.php new file mode 100644 index 00000000000..c63288f16c4 --- /dev/null +++ b/src/Http/Controllers/Elements/ElementSelectorModalController.php @@ -0,0 +1,60 @@ +validate([ + 'showSiteMenu' => ['nullable', 'in:0,1'], + 'sources' => ['nullable', 'array'], + 'sources.*' => ['string'], + ]); + + $elementType = $request->elementType(); + $currentElementIndex->activate(); + $condition = $request->condition(); + $hasStatuses = $elementType::hasStatuses(); + + if ($hasStatuses) { + $statuses = $elementType::statuses(); + + if ($condition) { + /** @var StatusConditionRule|null $statusRule */ + $statusRule = collect($condition->getConditionRules()) + ->firstWhere(fn ($rule) => $rule instanceof StatusConditionRule); + + if ($statusRule) { + $statusValues = $statusRule->getValues(); + $statuses = collect($statuses) + ->filter(function ($info, string $status) use ($statusRule, $statusValues) { + $inValues = in_array($status, $statusValues); + + return $statusRule->operator === 'in' ? $inValues : ! $inValues; + }); + } + } + } + + return new JsonResponse([ + 'html' => $elementIndexHtml->html($elementType, [ + 'class' => 'content', + 'context' => $request->context(), + 'registerJs' => false, + 'showSiteMenu' => $request->input('showSiteMenu', 'auto'), + 'showStatusMenu' => $hasStatuses, + 'sources' => $request->input('sources'), + 'statuses' => $statuses ?? null, + ]), + ]); + } +} diff --git a/yii2-adapter/legacy/controllers/ElementIndexSettingsController.php b/src/Http/Controllers/Elements/ElementSourcesController.php similarity index 71% rename from yii2-adapter/legacy/controllers/ElementIndexSettingsController.php rename to src/Http/Controllers/Elements/ElementSourcesController.php index 280177e25b1..23af62bf907 100644 --- a/yii2-adapter/legacy/controllers/ElementIndexSettingsController.php +++ b/src/Http/Controllers/Elements/ElementSourcesController.php @@ -1,67 +1,40 @@ - * @since 3.0.0 - */ -class ElementIndexSettingsController extends BaseElementsController +readonly class ElementSourcesController { - /** - * @inheritdoc - */ - public function beforeAction($action): bool - { - if (!parent::beforeAction($action)) { - return false; - } - - $this->requireAcceptsJson(); - $this->requireAdmin(); + use RespondsWithFlash; - return true; - } - - /** - * Returns all the info needed by the Customize Sources modal. - * - * @return Response - */ - public function actionGetCustomizeSourcesModalData(): Response + public function show(ElementIndexRequest $request, ElementSources $elementSources, Fields $fields, UserGroups $userGroups): JsonResponse { /** @var class-string $elementType */ - $elementType = $this->elementType(); + $elementType = $request->elementType(); // Global sort options - $baseSortOptions = Collection::make($elementType::sortOptions()) - ->map(fn($option, $key) => [ + $baseSortOptions = collect($elementType::sortOptions()) + ->map(fn ($option, $key) => [ 'label' => $option['label'] ?? $option, 'attr' => $option['attribute'] ?? $option['orderBy'] ?? $key, 'defaultDir' => $option['defaultDir'] ?? 'asc', @@ -70,17 +43,16 @@ public function actionGetCustomizeSourcesModalData(): Response ->all(); // Get the source info - $sourcesService = app(ElementSources::class); - $sources = $sourcesService->getSources($elementType, ElementSources::CONTEXT_INDEX, true)->all(); + $sources = $elementSources->getSources($elementType, ElementSources::CONTEXT_INDEX, true)->all(); $multiPage = $elementType::multiPageSources(); foreach ($sources as &$source) { if ($multiPage) { // ensure we're using the EN translation here - $language = Craft::$app->language; - Craft::$app->language = Craft::$app->sourceLanguage; + $language = app()->getLocale(); + app()->setLocale('en'); $source['page'] ??= $elementType::pluralDisplayName(); - Craft::$app->language = $language; + app()->setLocale($language); } if ($source['type'] === ElementSources::TYPE_HEADING) { @@ -99,8 +71,8 @@ public function actionGetCustomizeSourcesModalData(): Response : null, ]), $baseSortOptions, - $sourcesService->getSourceSortOptions($elementType, $source['key']) - ->map(fn($option) => [ + $elementSources->getSourceSortOptions($elementType, $source['key']) + ->map(fn ($option) => [ 'label' => $option['label'], 'attr' => $option['attribute'] ?? $option['orderBy'], 'defaultDir' => $option['defaultDir'] ?? 'asc', @@ -114,16 +86,16 @@ public function actionGetCustomizeSourcesModalData(): Response if (isset($source['defaultSort'])) { if (is_string($source['defaultSort'])) { - $defaultSortOption = Collection::make($source['sortOptions'])->firstWhere('attr', $source['defaultSort']); + $defaultSortOption = collect($source['sortOptions'])->firstWhere('attr', $source['defaultSort']); } elseif (is_array($source['defaultSort']) && isset($source['defaultSort'][0])) { - $defaultSortOption = Collection::make($source['sortOptions'])->firstWhere('attr', $source['defaultSort'][0]); + $defaultSortOption = collect($source['sortOptions'])->firstWhere('attr', $source['defaultSort'][0]); if ($defaultSortOption && isset($source['defaultSort'][1])) { $defaultSortDir = $source['defaultSort'][1]; } } } - if (!$defaultSortOption) { + if (! $defaultSortOption) { $defaultSortOption = reset($source['sortOptions']); } @@ -134,14 +106,14 @@ public function actionGetCustomizeSourcesModalData(): Response // Available custom field attributes $source['availableTableAttributes'] = []; - foreach ($sourcesService->getSourceTableAttributes($elementType, $source['key']) as $key => $labelInfo) { + foreach ($elementSources->getSourceTableAttributes($elementType, $source['key']) as $key => $labelInfo) { $source['availableTableAttributes'][] = [$key, $labelInfo['label']]; } // Selected table attributes - $tableAttributes = $sourcesService->getTableAttributes($elementType, $source['key'])->all(); + $tableAttributes = $elementSources->getTableAttributes($elementType, $source['key'])->all(); array_shift($tableAttributes); - $source['tableAttributes'] = array_map(fn($a) => [$a[0], $a[1]['label']], $tableAttributes); + $source['tableAttributes'] = array_map(fn ($a) => [$a[0], $a[1]['label']], $tableAttributes); if ($source['type'] === ElementSources::TYPE_CUSTOM) { if (isset($source['condition'])) { @@ -161,7 +133,7 @@ public function actionGetCustomizeSourcesModalData(): Response if (isset($source['sites'])) { $source['sites'] = array_values(array_filter(array_map( - fn(int $siteId) => Sites::getSiteById($siteId)?->uid, + fn (int $siteId) => Sites::getSiteById($siteId)?->uid, $source['sites'] ?: [], ))); } @@ -176,13 +148,13 @@ public function actionGetCustomizeSourcesModalData(): Response } unset($source); - $viewModes = array_map(fn(array $viewMode) => array_merge($viewMode, [ + $viewModes = array_map(fn (array $viewMode) => array_merge($viewMode, [ 'iconSvg' => Icons::svg($viewMode['icon'] ?? 'table'), ]), $elementType::indexViewModes()); // Get the default sort options for custom sources - $defaultSortOptions = $sourcesService->getSourceSortOptions($elementType, 'custom:x') - ->map(fn(array $option) => [ + $defaultSortOptions = $elementSources->getSourceSortOptions($elementType, 'custom:x') + ->map(fn (array $option) => [ 'label' => $option['label'], 'attr' => $option['attribute'] ?? $option['orderBy'], 'defaultDir' => $option['defaultDir'] ?? 'asc', @@ -193,14 +165,14 @@ public function actionGetCustomizeSourcesModalData(): Response // Get the available table attributes $availableTableAttributes = []; - foreach ($sourcesService->getAvailableTableAttributes($elementType) as $key => $labelInfo) { + foreach ($elementSources->getAvailableTableAttributes($elementType) as $key => $labelInfo) { $availableTableAttributes[] = [$key, $labelInfo['label']]; } // Get previewable custom fields that should be available for all custom sources $customFieldAttributes = []; - foreach (app(Fields::class)->getLayoutsByType($elementType) as $fieldLayout) { + foreach ($fields->getLayoutsByType($elementType) as $fieldLayout) { foreach ($fieldLayout->getCustomFields() as $field) { if ($field instanceof PreviewableFieldInterface) { $customFieldAttributes[] = ["field:$field->uid", t($field->name, category: 'site')]; @@ -220,19 +192,17 @@ public function actionGetCustomizeSourcesModalData(): Response $conditionBuilderHtml = $condition->getBuilderHtml(); $conditionBuilderJs = HtmlStack::clearJsBuffer(); - $userGroups = UserGroups::getAllGroups() - ->map(fn(UserGroup $group) => [ + $userGroups = $userGroups->getAllGroups() + ->map(fn (UserGroup $group) => [ 'label' => t($group->name, category: 'site'), 'value' => $group->uid, ]) ->all(); - $pageSettings = $sourcesService->getPageSettings($elementType); - - return $this->asJson([ + return new JsonResponse([ 'multiPage' => $multiPage, 'sources' => $sources, - 'pageSettings' => $pageSettings, + 'pageSettings' => $elementSources->getPageSettings($elementType), 'viewModes' => $viewModes, 'baseSortOptions' => $baseSortOptions, 'defaultSortOptions' => $defaultSortOptions, @@ -247,39 +217,33 @@ public function actionGetCustomizeSourcesModalData(): Response ]); } - /** - * Saves the Customize Sources modal settings. - * - * @return Response - */ - public function actionSaveCustomizeSourcesModalSettings(): Response + public function store(ElementIndexRequest $request, ElementSources $elementSources, ProjectConfig $projectConfig) { - $elementType = $this->elementType(); + $elementType = $request->elementType(); $multiPage = $elementType::multiPageSources(); // Get the old source configs - $projectConfig = app(ProjectConfig::class); - $oldSourceConfigs = $projectConfig->get(ProjectConfig::PATH_ELEMENT_SOURCES . ".$elementType") ?? []; - $oldSourceConfigs = Collection::make($oldSourceConfigs) + $oldSourceConfigs = $projectConfig->get(ProjectConfig::PATH_ELEMENT_SOURCES.".$elementType") ?? []; + $oldSourceConfigs = collect($oldSourceConfigs) ->keyBy('key') ->all(); - $sourceOrder = $this->request->getBodyParam('sourceOrder', []); - $sourceSettings = $this->request->getBodyParam('sources', []); + $sourceOrder = $request->array('sourceOrder'); + $sourceSettings = $request->array('sources'); $newSourceConfigs = []; $disabledSourceKeys = []; if ($multiPage) { - $sourcePages = $this->request->getBodyParam('sourcePages', []); - $pageSettings = $this->request->getBodyParam('pageSettings', []); + $sourcePages = $request->array('sourcePages'); + $pageSettings = $request->array('pageSettings'); $sourcePageIndexes = []; } // Normalize to the way it's stored in the DB foreach ($sourceOrder as $key) { $type = match (true) { - str_starts_with($key, 'custom:') => ElementSources::TYPE_CUSTOM, - str_starts_with($key, 'heading:') => ElementSources::TYPE_HEADING, + str_starts_with((string) $key, 'custom:') => ElementSources::TYPE_CUSTOM, + str_starts_with((string) $key, 'heading:') => ElementSources::TYPE_HEADING, default => ElementSources::TYPE_NATIVE, }; @@ -325,14 +289,14 @@ public function actionSaveCustomizeSourcesModalSettings(): Response } elseif ($type === ElementSources::TYPE_HEADING) { $sourceConfig['heading'] = $postedSettings['heading']; } elseif (isset($postedSettings['enabled'])) { - $sourceConfig['disabled'] = !$postedSettings['enabled']; + $sourceConfig['disabled'] = ! $postedSettings['enabled']; if ($sourceConfig['disabled']) { $disabledSourceKeys[] = $key; } } } elseif (isset($oldSourceConfigs[$key])) { $sourceConfig += $oldSourceConfigs[$key]; - if (!empty($sourceConfig['disabled'])) { + if (! empty($sourceConfig['disabled'])) { $disabledSourceKeys[] = $key; } } elseif ($isCustom) { @@ -352,18 +316,16 @@ public function actionSaveCustomizeSourcesModalSettings(): Response array_multisort($sourcePageIndexes, SORT_NUMERIC, range(1, count($newSourceConfigs)), SORT_NUMERIC, $newSourceConfigs); } - $sourcesService = app(ElementSources::class); - $sourcesService->saveSources($elementType, $newSourceConfigs); + $elementSources->saveSources($elementType, $newSourceConfigs); + if ($multiPage) { - $sourcesService->savePageSettings($elementType, array_map( - fn(array $settings) => array_filter($settings, fn($setting) => $setting !== null && $setting !== ''), + $elementSources->savePageSettings($elementType, array_map( + fn (array $settings) => array_filter($settings, fn ($setting) => $setting !== null && $setting !== ''), $pageSettings, )); } - Craft::$app->getSession()->setSuccess(t('Source settings saved')); - - return $this->asSuccess(data: [ + return $this->asSuccess(t('Source settings saved'), data: [ 'disabledSourceKeys' => $disabledSourceKeys, ]); } diff --git a/src/Http/Controllers/Elements/PerformElementActionController.php b/src/Http/Controllers/Elements/PerformElementActionController.php index fd8ad0884cf..5e39fc529de 100644 --- a/src/Http/Controllers/Elements/PerformElementActionController.php +++ b/src/Http/Controllers/Elements/PerformElementActionController.php @@ -4,70 +4,52 @@ namespace CraftCms\Cms\Http\Controllers\Elements; -use Closure; -use craft\base\ElementInterface; -use CraftCms\Cms\Element\Contracts\ElementExporterInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\CurrentElementIndex; use CraftCms\Cms\Element\ElementActions; -use CraftCms\Cms\Element\ElementExporters; use CraftCms\Cms\Element\ElementSources; -use CraftCms\Cms\Element\Exceptions\InvalidTypeException; -use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Http\Controllers\Elements\Concerns\InteractsWithElementIndexes; +use CraftCms\Cms\Http\Requests\ElementIndexRequest; +use CraftCms\Cms\Http\Resources\ElementIndexResource; use CraftCms\Cms\Http\RespondsWithFlash; -use CraftCms\Cms\Support\Facades\HtmlStack; -use CraftCms\Cms\Support\Html; use CraftCms\Cms\Translation\I18N as TranslationI18N; -use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; -use function CraftCms\Cms\t; - readonly class PerformElementActionController { use InteractsWithElementIndexes; use RespondsWithFlash; public function __construct( - private Request $request, + private ElementIndexRequest $request, private ElementActions $elementActions, private ElementSources $elementSources, private TranslationI18N $i18N, - private ElementExporters $elementExporters, ) {} - public function __invoke(): SymfonyResponse + public function __invoke(CurrentElementIndex $currentElementIndex): SymfonyResponse { $validated = $this->request->validate([ - 'elementType' => [ - 'required', - 'string', - function (string $attribute, mixed $value, Closure $fail): void { - if (! is_string($value) || ! is_subclass_of($value, ElementInterface::class)) { - $fail(new InvalidTypeException((string) $value, ElementInterface::class)->getMessage()); - } - }, - ], 'elementAction' => ['required', 'string'], 'elementIds' => ['required', 'array'], ]); /** @var class-string $elementType */ - $elementType = $validated['elementType']; + $elementType = $this->request->elementType(); $actionClass = $validated['elementAction']; $elementIds = $validated['elementIds']; - $context = $this->request->input('context', ElementSources::CONTEXT_INDEX); + $context = $this->request->context(); + + [$sourceKey, $source] = $this->resolveSource($elementType, $this->request->input('source'), $context); + $queryState = $this->buildElementQueryState($elementType, $source, $this->request->condition()); + $elementQuery = $queryState['query']; - [$sourceKey, $source] = $this->source($elementType, $this->request->input('source'), $context); - $condition = $this->condition(); - $viewState = $this->viewState(); - $elementQuery = $this->elementQuery($elementType, $source, $condition); + $currentElementIndex->activate($elementQuery); $actions = null; - $exporters = null; - if ($this->isAdministrative($context) && isset($sourceKey)) { - $actions = $this->availableActions($elementType, $sourceKey, $elementQuery); - $exporters = $this->availableExporters($elementType, $sourceKey); + if ($this->request->isAdministrative($context) && isset($sourceKey)) { + $actions = $this->elementActions->availableActions($elementType, $sourceKey, $elementQuery); } $action = $this->elementActions->resolveAction($actions ?? [], $actionClass); @@ -102,17 +84,7 @@ function (string $attribute, mixed $value, Closure $fail): void { return $this->asFailure($result['message']); } - $responseData = $this->elementResponseData( - elementType: $elementType, - elementQuery: $elementQuery, - viewState: $viewState, - sourceKey: $sourceKey, - context: $context, - actions: $actions, - exporters: $exporters, - includeContainer: true, - includeActions: true, - ); + $responseData = new ElementIndexResource()->toArray($this->request); $formatter = $this->i18N->getFormatter(); @@ -128,104 +100,4 @@ function (string $attribute, mixed $value, Closure $fail): void { return $this->asSuccess($result['message'], $responseData); } - - /** - * @param class-string $elementType - */ - private function availableActions( - string $elementType, - string $sourceKey, - ElementQueryInterface $elementQuery, - ): array { - return $this->elementActions->availableActions($elementType, $sourceKey, $elementQuery); - } - - /** - * @param class-string $elementType - * @return ElementExporterInterface[]|null - */ - private function availableExporters(string $elementType, string $sourceKey): ?array - { - if ($this->request->isMobileBrowser()) { - return null; - } - - return $this->elementExporters->availableExporters($elementType, $sourceKey); - } - - /** - * @param class-string $elementType - * @param ElementExporterInterface[]|null $exporters - */ - private function elementResponseData( - string $elementType, - ElementQueryInterface $elementQuery, - array $viewState, - ?string $sourceKey, - string $context, - ?array $actions, - ?array $exporters, - bool $includeContainer, - bool $includeActions, - ): array { - $responseData = []; - - if ($includeActions) { - $responseData['actions'] = $viewState['static'] === true ? [] : $this->actionData($actions); - $responseData['actionsHeadHtml'] = HtmlStack::headHtml(); - $responseData['actionsBodyHtml'] = HtmlStack::bodyHtml(); - $responseData['exporters'] = $this->exporterData($exporters); - } - - $disabledElementIds = $this->request->input('disabledElementIds', []); - $selectable = ( - ((! empty($actions)) || $this->request->boolean('selectable')) && - empty($viewState['inlineEditing']) - ); - $sortable = $this->isAdministrative($context) && $this->request->boolean('sortable'); - - if ($sourceKey) { - $responseData['html'] = $elementType::indexHtml( - $elementQuery, - $disabledElementIds, - $viewState, - $sourceKey, - $context, - $includeContainer, - $selectable, - $sortable, - ); - $responseData['headHtml'] = HtmlStack::headHtml(); - $responseData['bodyHtml'] = HtmlStack::bodyHtml(); - - return $responseData; - } - - $responseData['html'] = Html::tag('div', t('Nothing yet.'), [ - 'class' => ['zilch', 'small'], - ]); - - return $responseData; - } - - private function actionData(?array $actions): ?array - { - if (empty($actions)) { - return null; - } - - return $this->elementActions->serializeActions($actions); - } - - /** - * @param ElementExporterInterface[]|null $exporters - */ - private function exporterData(?array $exporters): ?array - { - if (empty($exporters)) { - return null; - } - - return $this->elementExporters->serializeExporters($exporters); - } } diff --git a/src/Http/Controllers/Elements/PreviewElementController.php b/src/Http/Controllers/Elements/PreviewElementController.php new file mode 100644 index 00000000000..134e8fe11b3 --- /dev/null +++ b/src/Http/Controllers/Elements/PreviewElementController.php @@ -0,0 +1,68 @@ +request->element([ + 'id' => $id, + ], checkForProvisionalDraft: true); + + if ($element instanceof Response) { + return $element; + } + + abort_if(is_null($element), 400, 'No element was identified by the request.'); + + $redirectUrl = $this->request->getSigned('returnUrl', ElementHelper::postEditUrl($element)); + + HtmlStack::jsWithVars(fn ( + $elementType, + $elementId, + $draftId, + $revisionId, + $siteId, + $redirectUrl, + ) => << { + const preview = new Craft.Preview({ + elementType: $elementType, + elementId: $elementId, + draftId: $draftId, + revisionId: $revisionId, + siteId: $siteId, + standaloneMode: true, + redirectUrl: $redirectUrl, + }) + preview.open(); + })(); + JS, [ + $element::class, + $element->isProvisionalDraft ? $element->getCanonicalId() : $element->id, + ! $element->isProvisionalDraft ? $element->draftId : null, + $element->revisionId, + $element->siteId, + $redirectUrl, + ]); + + [$docTitle, $title] = $this->editElementTitles($element); + + return view('_layouts/base', compact('docTitle', 'title')); + } +} diff --git a/src/Http/Controllers/Elements/SaveElementController.php b/src/Http/Controllers/Elements/SaveElementController.php new file mode 100644 index 00000000000..9760dfc7692 --- /dev/null +++ b/src/Http/Controllers/Elements/SaveElementController.php @@ -0,0 +1,241 @@ +request->element(); + + if ($element instanceof Response) { + return $element; + } + + if (! $element || $element->getIsDraft() || $element->getIsRevision()) { + abort(400, 'No element was identified by the request.'); + } + + // Check save permissions before and after applying POST params to the element + // in case the request was tampered with. + Gate::authorize('save', $element); + + $this->applyParamsToElement($element); + + Gate::authorize('save', $element); + + if ($element->enabled && $element->getEnabledForSite()) { + $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); + } + + $isNotNew = $element->id; + if ($isNotNew) { + $mutex = Cache::lock("element:$element->id", 15); + if (! $mutex->get()) { + abort(500, 'Could not acquire a lock to save the element.'); + } + } + + if ($element instanceof NestedElementInterface && property_exists($element, 'updateSearchIndexForOwner')) { + $element->updateSearchIndexForOwner = true; + } + + try { + $namespace = $this->request->header('X-Craft-Namespace'); + // crossSiteValidate only if it's multisite, element supports drafts and we're not in a slideout + $success = $this->elements->saveElement( + $element, + crossSiteValidate: ( + $namespace === null + && $this->sites->isMultiSite() + && Gate::check('createDrafts', $element) + ), + ); + } catch (UnsupportedSiteException $e) { + $element->errors()->add('siteId', $e->getMessage()); + $success = false; + } finally { + if ($isNotNew) { + $mutex->release(); + } + } + + if (! $success) { + return new ElementResponse()->failure($element, mb_ucfirst(t('Couldn’t save {type}.', [ + 'type' => $element::lowerDisplayName(), + ]))); + } + + $this->elementActivity->trackActivity($element, ElementActivityType::Save); + + // See if the user happens to have a provisional element. If so delete it. + $provisional = $element::find() + ->provisionalDrafts() + ->draftOf($element->id) + ->draftCreator($this->request->user()) + ->siteId($element->siteId) + ->status(null) + ->one(); + + if ($provisional) { + $this->elements->deleteElement($provisional, true); + } + + if (! $this->request->acceptsJson()) { + // Tell all browser windows about the element save + session()->broadcastToJs([ + 'event' => 'saveElement', + 'id' => $element->id, + ]); + } + + return new ElementResponse()->success($element, t('{type} saved.', [ + 'type' => $element::displayName(), + ]), supportsAddAnother: true); + } + + public function storeForDerivative(): Response + { + if (! $this->request->has('newOwnerId')) { + abort(400, 'No new owner was identified by the request.'); + } + + $element = $this->request->element(); + + if ( + ! $element instanceof NestedElementInterface || + ! $element->getOwnerId() || + ! $element->getIsDraft() || + $element->getIsCanonical() + ) { + abort(400, 'No element was identified by the request.'); + } + + // Check save permissions before and after applying POST params to the element + // in case the request was tampered with. + Gate::authorize('save', $element); + + // Get the new owner and make sure it's a derivative element, + // and that its canonical element is the nested element's primary owner + $owner = $this->elements->getElementById($this->request->integer('newOwnerId'), siteId: $element->siteId); + + if ($owner->getIsCanonical()) { + abort(400, 'The owner element must be a derivative.'); + } + + if ($owner->getCanonicalId() !== $element->getPrimaryOwnerId()) { + // the owner might be a derivative of another canonical element + $canonicalOwner = $owner->getCanonical(); + if ($canonicalOwner->getCanonicalId() !== $element->getPrimaryOwnerId()) { + abort(400, 'The canonical owner element must be the primary owner of the nested element.'); + } + } + + Gate::authorize('save', $owner); + + // Get the old sort order + $sortOrder = DB::table(Table::ELEMENTS_OWNERS) + ->where('elementId', $element->id) + ->where('ownerId', $element->getOwnerId()) + ->value('sortOrder'); + + $element->setSortOrder($sortOrder); + + DB::beginTransaction(); + + try { + // Remove existing ownership data for the element within the canonical owner, + // and for its canonical element within the derivative + DB::table(Table::ELEMENTS_OWNERS) + ->where('elementId', $element->id) + ->where('ownerId', $owner->getCanonicalId()) + ->orWhere(fn (Builder $query) => $query + ->where('elementId', $element->getCanonicalId()) + ->where('ownerId', $owner->id) + ) + ->delete(); + + // Remove existing ownership data for the element within the canonical owner + DB::table(Table::ELEMENTS_OWNERS) + ->where('elementId', $element->id) + ->where('ownerId', $owner->getCanonicalId()) + ->delete(); + + // Remove the draft data, but preserve the canonicalId + $element->setPrimaryOwner($owner); + $element->setOwner($owner); + + $this->elements->saveElement($element); + + $this->applyParamsToElement($element); + + Gate::authorize('save', $element); + + if ($element->enabled && $element->getEnabledForSite()) { + $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); + } + + try { + $success = $this->elements->saveElement($element); + } catch (UnsupportedSiteException $e) { + $element->errors()->add('siteId', $e->getMessage()); + $success = false; + } + + if (! $success) { + DB::rollBack(); + + return new ElementResponse()->failure($element, mb_ucfirst(t('Couldn’t save {type}.', [ + 'type' => $element::lowerDisplayName(), + ]))); + } + + if ($element->getIsDraft()) { + $this->drafts->removeDraftData($element); + } + + DB::commit(); + } catch (Throwable $e) { + DB::rollBack(); + + throw $e; + } + + return new ElementResponse()->success($element, t('{type} saved.', [ + 'type' => $element::displayName(), + ])); + } +} diff --git a/src/Http/Controllers/Elements/SearchController.php b/src/Http/Controllers/Elements/SearchController.php new file mode 100644 index 00000000000..3ace859272e --- /dev/null +++ b/src/Http/Controllers/Elements/SearchController.php @@ -0,0 +1,128 @@ +request->validate([ + 'siteId' => ['nullable'], + 'criteria' => ['nullable', 'array'], + 'excludeIds' => ['nullable', 'array'], + 'excludeIds.*' => ['integer'], + 'referenceElementId' => ['nullable', 'integer'], + 'referenceElementOwnerId' => ['nullable', 'integer'], + 'referenceElementSiteId' => ['nullable', 'integer'], + 'search' => ['required', 'string', 'max:255'], + ]); + + $query = $this->request->elementType()::find() + ->siteId($this->request->input('siteId')) + ->search($this->request->input('search')) + ->orderByDesc('score') + ->limit(5); + + if ($criteria = $this->request->array('criteria')) { + // Remove unsupported criteria attributes + $criteria = ElementHelper::cleanseQueryCriteria($criteria); + + Typecast::configure($query, $criteria); + } + + $this->applyCondition($query); + + $elements = $query->get(); + + if ($elements->isEmpty()) { + return new JsonResponse([ + 'elements' => [], + 'exactMatch' => false, + ]); + } + + $return = []; + $exactMatches = []; + $excludes = []; + $exactMatch = false; + + $search = Search::normalizeKeywords($this->request->input('search', '')); + + foreach ($elements as $element) { + $exclude = in_array($element->id, $this->request->array('excludeIds')); + + $return[] = [ + 'id' => $element->id, + 'title' => $element->title, + 'html' => app(ElementHtml::class)->chipHtml($element, [ + 'hyperlink' => false, + 'class' => 'chromeless', + ]), + 'exclude' => $exclude, + ]; + + $title = $element->title ?? (string) $element; + $title = Search::normalizeKeywords($title); + + if ($title === $search) { + $exactMatches[] = 1; + $exactMatch = true; + } else { + $exactMatches[] = 0; + } + + $excludes[] = $exclude ? 1 : 0; + } + + // prevent the default sort order from changing beyond $excludes + $exactMatches + $range = range(1, count($return)); + + array_multisort($excludes, SORT_ASC, $exactMatches, SORT_DESC, $range, $return); + + return new JsonResponse([ + 'elements' => $return, + 'exactMatch' => $exactMatch, + ]); + } + + private function applyCondition(ElementQueryInterface $query): void + { + if (! $condition = $this->request->condition()) { + return; + } + + if ($referenceElementId = $this->request->input('referenceElementId')) { + $ownerId = $this->request->input('referenceElementOwnerId'); + $siteId = $this->request->input('referenceElementSiteId'); + $criteria = []; + + if ($ownerId) { + $criteria['ownerId'] = $ownerId; + } + + $condition->referenceElement = $this->elements->getElementById( + (int) $referenceElementId, + siteId: $siteId, + criteria: $criteria, + ); + } + + $condition->modifyQuery($query); + } +} diff --git a/src/Http/Controllers/Elements/UpdateFieldLayoutController.php b/src/Http/Controllers/Elements/UpdateFieldLayoutController.php new file mode 100644 index 00000000000..b6453f30ee2 --- /dev/null +++ b/src/Http/Controllers/Elements/UpdateFieldLayoutController.php @@ -0,0 +1,71 @@ +request->has('elementId') || $this->request->has('elementUid')) { + $element = $this->request->element(); + } else { + $element = $this->createElement(); + } + + // Prevalidate? + if ($this->request->boolean('prevalidate') && $element->enabled && $element->getEnabledForSite()) { + $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); + $element->validate(); + } + + /** + * see https://github.com/craftcms/cms/issues/14635#issuecomment-2349006694 for details + * + * @var Element|Response|null $element + */ + if ($element instanceof Response) { + return $element; + } + + if (! $element || $element->getIsRevision()) { + abort(400, 'No element was identified by the request.'); + } + + Gate::authorize('view', $element); + + $this->applyParamsToElement($element); + + // Make sure nothing just changed that would prevent the user from saving + Gate::authorize('view', $element); + + $data = $this->fieldLayoutData($element); + + $data += [ + 'initialDeltaValues' => $this->deltaRegistry->getInitialValues(), + ]; + + return new ElementResponse()->success($element, 'Field layout updated.', $data, true); + } +} diff --git a/src/Http/Controllers/Elements/ValidateElementController.php b/src/Http/Controllers/Elements/ValidateElementController.php new file mode 100644 index 00000000000..b1bef5a798a --- /dev/null +++ b/src/Http/Controllers/Elements/ValidateElementController.php @@ -0,0 +1,46 @@ +request->element(); + + // this can happen if we're creating e.g. nested entry in a matrix field (cards or element index) + // and we hit "create entry" before the autosave kicks in + if ($element instanceof Response) { + return $element; + } + + if (! $element || $element->getIsRevision()) { + abort(400, 'No element was identified by the request.'); + } + + $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); + + if (! $element->validate()) { + return new ElementResponse()->failure($element, t('{type} validation failed.', [ + 'type' => $element::displayName(), + ])); + } + + return new ElementResponse()->success($element, t('{type} validation successful.', [ + 'type' => $element::displayName(), + ])); + } +} diff --git a/src/Http/Controllers/FieldsController.php b/src/Http/Controllers/FieldsController.php index a5c9dc0dfe1..78ab38a37ae 100644 --- a/src/Http/Controllers/FieldsController.php +++ b/src/Http/Controllers/FieldsController.php @@ -4,7 +4,6 @@ namespace CraftCms\Cms\Http\Controllers; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; use CraftCms\Cms\Component\ComponentHelper; use CraftCms\Cms\Component\Contracts\Chippable; @@ -16,6 +15,7 @@ use CraftCms\Cms\Cp\FieldLayoutDesigner\FieldLayoutDesigner; use CraftCms\Cms\Cp\Html\ContentHtml; use CraftCms\Cms\Cp\Icons; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; use CraftCms\Cms\Field\Enums\TranslationMethod; use CraftCms\Cms\Field\Field; diff --git a/src/Http/Controllers/MatrixController.php b/src/Http/Controllers/MatrixController.php index 22465d301cf..dfceb72ae6f 100644 --- a/src/Http/Controllers/MatrixController.php +++ b/src/Http/Controllers/MatrixController.php @@ -127,7 +127,6 @@ public function createEntry(Request $request): Response ])); } } else { - /** @var Entry $entry */ $entry = new Entry([ ...$attributes, ]); diff --git a/src/Http/Controllers/NestedElementsController.php b/src/Http/Controllers/NestedElementsController.php index 1937c211f06..300cadb5889 100644 --- a/src/Http/Controllers/NestedElementsController.php +++ b/src/Http/Controllers/NestedElementsController.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\Http\Controllers; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; use CraftCms\Cms\Auth\SessionAuth; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\ElementCaches; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\Elements; diff --git a/src/Http/Controllers/PreviewController.php b/src/Http/Controllers/PreviewController.php index dfab9a5bdb4..d7fb8afa129 100644 --- a/src/Http/Controllers/PreviewController.php +++ b/src/Http/Controllers/PreviewController.php @@ -27,10 +27,12 @@ public function createToken(Request $request, RouteTokens $tokens): JsonResponse|RedirectResponse { - $tokenData = new RouteToken($request->all()); + $tokenData = new RouteToken($request->post()); + if ($token = $request->input('previewToken')) { $tokenData->previewToken = Crypt::decrypt($token); } + $tokenData->validate(throw: true); match (true) { diff --git a/src/Http/Controllers/RelationalFieldsController.php b/src/Http/Controllers/RelationalFieldsController.php index f287ed331de..5aa0c2d6b45 100644 --- a/src/Http/Controllers/RelationalFieldsController.php +++ b/src/Http/Controllers/RelationalFieldsController.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Http\Controllers; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Structure\Structures; use CraftCms\Cms\Support\Facades\HtmlStack; diff --git a/src/Http/Controllers/Settings/EntryTypesController.php b/src/Http/Controllers/Settings/EntryTypesController.php index be08c15e720..87dd9613c76 100644 --- a/src/Http/Controllers/Settings/EntryTypesController.php +++ b/src/Http/Controllers/Settings/EntryTypesController.php @@ -20,7 +20,7 @@ use CraftCms\Cms\Field\Fields; use CraftCms\Cms\FieldLayout\FieldLayout; use CraftCms\Cms\FieldLayout\FieldLayoutElement; -use CraftCms\Cms\FieldLayout\LayoutElements\entries\EntryTitleField; +use CraftCms\Cms\FieldLayout\LayoutElements\Entries\EntryTitleField; use CraftCms\Cms\Http\RespondsWithFlash; use CraftCms\Cms\Http\Responses\CpScreenResponse; use CraftCms\Cms\Section\Data\Section; diff --git a/src/Http/Controllers/Settings/FilesystemsController.php b/src/Http/Controllers/Settings/FilesystemsController.php index 3843f4b120c..77b2b2c399a 100644 --- a/src/Http/Controllers/Settings/FilesystemsController.php +++ b/src/Http/Controllers/Settings/FilesystemsController.php @@ -121,7 +121,6 @@ public function save(Request $request): Response { $type = $request->input('type'); - /** @var FsInterface $fs */ $fs = $this->filesystems->createFilesystem([ 'type' => $type, 'name' => $request->input('name'), diff --git a/src/Http/Controllers/StructuresController.php b/src/Http/Controllers/StructuresController.php index 5be7d95455d..11ca284b909 100644 --- a/src/Http/Controllers/StructuresController.php +++ b/src/Http/Controllers/StructuresController.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Http\Controllers; -use craft\base\ElementInterface; use CraftCms\Cms\Auth\Concerns\EnforcesPermissions; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Elements; use CraftCms\Cms\Http\RespondsWithFlash; use CraftCms\Cms\Structure\Data\Structure; diff --git a/src/Http/Controllers/Users/EditUserTrait.php b/src/Http/Controllers/Users/EditUserTrait.php index ba894f03b14..5b7d5d6f091 100644 --- a/src/Http/Controllers/Users/EditUserTrait.php +++ b/src/Http/Controllers/Users/EditUserTrait.php @@ -63,7 +63,7 @@ protected function editedUser(?int $userId): User return $user; } - protected function asEditUserScreen(User $user, string $screen): CpScreenResponse + protected function asEditUserScreen(User $user, string $screen, ?CpScreenResponse $response = null): CpScreenResponse { $screens = [ self::SCREEN_PROFILE => ['label' => t('Profile')], @@ -93,7 +93,7 @@ protected function asEditUserScreen(User $user, string $screen): CpScreenRespons abort_if(! isset($screens[$screen]), 403, 'User not authorized to perform this action.'); $pageName = $screens[$screen]['label']; - $response = new CpScreenResponse() + $response = ($response ?? new CpScreenResponse) ->when( $user->getIsCurrent(), fn (CpScreenResponse $response) => $response diff --git a/src/Http/Controllers/Users/UsersController.php b/src/Http/Controllers/Users/UsersController.php index 81eb56d3936..2eb25c92eaa 100644 --- a/src/Http/Controllers/Users/UsersController.php +++ b/src/Http/Controllers/Users/UsersController.php @@ -4,20 +4,18 @@ namespace CraftCms\Cms\Http\Controllers\Users; -use Craft; -use craft\web\CpScreenResponseBehavior; use CraftCms\Cms\Auth\Concerns\EnforcesPermissions; use CraftCms\Cms\Edition; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\Elements; use CraftCms\Cms\Element\Validation\ElementRules; use CraftCms\Cms\Entry\Elements\Entry; +use CraftCms\Cms\Http\Controllers\Elements\EditElementController; use CraftCms\Cms\Http\RespondsWithFlash; use CraftCms\Cms\Http\Responses\CpScreenResponse; use CraftCms\Cms\Section\Data\Section; use CraftCms\Cms\Section\Sections; use CraftCms\Cms\Support\Url; -use CraftCms\Cms\Support\Utils; use CraftCms\Cms\User\Elements\User; use CraftCms\Cms\User\Events\DefineUserContentSummary; use CraftCms\Cms\User\Users; @@ -25,8 +23,6 @@ use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use ReflectionClass; -use ReflectionException; use Symfony\Component\HttpFoundation\Response; use function CraftCms\Cms\t; @@ -79,39 +75,20 @@ public function create(Request $request, Drafts $drafts): Response ])); } - public function edit(?int $userId = null): CpScreenResponse + public function edit(?int $userId = null): Response|CpScreenResponse { $user = $this->editedUser($userId); /** - * @TODO: Refactor away the runAction - * let the elements/edit action do most of the work + * Let the elements/edit action do most of the work */ - Craft::$app->request->setIsCpRequest(true); - $response = Craft::$app->runAction('elements/edit', [ - 'element' => $user, - ]); - - /** - * This transforms the old Yii CpScreen to the new - * - * @var CpScreenResponseBehavior $cpScreen - */ - $cpScreen = $response->getBehavior('cp-screen'); - $response = $this->asEditUserScreen($user, self::SCREEN_PROFILE); - $reflection = new ReflectionClass($response); - foreach (Utils::getPublicProperties($cpScreen) as $property => $value) { - if (isset($response->{$property})) { - continue; - } + $response = app(EditElementController::class)->setElement($user)(); - try { - $reflection->getProperty($property)->setValue($response, $value); - } catch (ReflectionException) { - } + if (! $response instanceof CpScreenResponse) { + return $response; } - return $response + return $this->asEditUserScreen($user, self::SCREEN_PROFILE, $response) ->when( $user->getIsUnpublishedDraft() && $this->showPermissionsScreen(), function (CpScreenResponse $response) use ($user) { diff --git a/src/Http/Mixins/SessionMixin.php b/src/Http/Mixins/SessionMixin.php new file mode 100644 index 00000000000..f45f2daf466 --- /dev/null +++ b/src/Http/Mixins/SessionMixin.php @@ -0,0 +1,64 @@ +isCpRequest()) { + return; + } + + /** + * @var SessionManager $this + * + * @phpstan-ignore-next-line + */ + $this->flashJs(Json::encode($message)); + }; + } + + public function getJs(): Closure + { + return function (bool $delete = true): array { + if ($delete) { + /** + * @var SessionManager $this + * + * @phpstan-ignore-next-line + */ + return $this->pull('__js', []); + } + + /** + * @var SessionManager $this + * + * @phpstan-ignore-next-line + */ + return $this->get('__js', []); + }; + } + + public function flashJs(): Closure + { + return function (string $js, Position $position = Position::Head, ?string $key = null): void { + /** + * @var SessionManager $this + * + * @phpstan-ignore-next-line + */ + $scripts = $this->getJs(); + $scripts[] = [$js, $position->value, $key]; + $this->flash('__js', $scripts); + }; + } +} diff --git a/src/Http/Requests/ElementIndexRequest.php b/src/Http/Requests/ElementIndexRequest.php new file mode 100644 index 00000000000..9a1346c125b --- /dev/null +++ b/src/Http/Requests/ElementIndexRequest.php @@ -0,0 +1,114 @@ + + */ + public function elementType(): string + { + $this->validate([ + 'elementType' => ['required', 'string', function (string $attribute, mixed $value, Closure $fail): void { + if (! ComponentHelper::validateComponentClass($value, ElementInterface::class)) { + $fail(new InvalidTypeException((string) $value, ElementInterface::class)->getMessage()); + } + }], + ]); + + return $this->input('elementType'); + } + + /** + * Returns the context that this controller is being called in. + */ + public function context(): string + { + return $this->input('context', ElementSources::CONTEXT_INDEX); + } + + public function isAdministrative(?string $context = null): bool + { + return in_array($context ?? $this->context(), [ElementSources::CONTEXT_INDEX, ElementSources::CONTEXT_EMBEDDED_INDEX]); + } + + /** + * Returns the condition that should be applied to the element query. + */ + public function condition(): ?ElementConditionInterface + { + $this->validate([ + 'condition' => ['nullable', function (string $attribute, mixed $value, Closure $fail): void { + if (! is_array($value) && ! is_string($value)) { + $fail(t('The {attribute} field must be a string or array.', ['attribute' => $attribute])); + + return; + } + + if (is_array($value)) { + $class = $value['class'] ?? null; + + if (! is_string($class) || trim($class) === '') { + $fail(t('The {attribute} field must contain a `class` value.', ['attribute' => $attribute])); + } + } + }], + 'referenceElementId' => ['nullable', 'integer'], + 'referenceElementOwnerId' => ['nullable', 'integer'], + 'referenceElementSiteId' => ['nullable', 'integer'], + ]); + + /** @var array{class:class-string}|null $conditionConfig */ + $conditionConfig = $this->input('condition'); + + if (! $conditionConfig) { + return null; + } + + $condition = Conditions::createCondition($conditionConfig); + + if ($condition instanceof ElementCondition) { + $referenceElementId = $this->input('referenceElementId'); + + if ($referenceElementId) { + $ownerId = $this->input('referenceElementOwnerId'); + $siteId = $this->input('referenceElementSiteId'); + $criteria = []; + + if ($ownerId) { + $criteria['ownerId'] = $ownerId; + } + + $condition->referenceElement = Elements::getElementById( + (int) $referenceElementId, + siteId: $siteId, + criteria: $criteria, + ); + } + } + + if (! $condition instanceof ElementConditionInterface) { + return null; + } + + return $condition; + } +} diff --git a/src/Http/Requests/ElementRequest.php b/src/Http/Requests/ElementRequest.php new file mode 100644 index 00000000000..a9265637908 --- /dev/null +++ b/src/Http/Requests/ElementRequest.php @@ -0,0 +1,369 @@ + + */ + private string $elementType; + + public ?ElementInterface $element = null; + + public function rules(): array + { + $fieldsLocation = $this->input('fieldsLocation', 'fields'); + + return [ + '*' => [], + 'id' => ['missing'], + 'canonicalId' => ['missing'], + + /** + * These need to be excluded from the ->validated() call + * which is passed to setAttributesFromRequest. + */ + 'elementType' => ['exclude'], + 'elementId' => ['exclude'], + 'elementUid' => ['exclude'], + 'draftId' => ['exclude'], + 'revisionId' => ['exclude'], + 'fieldId' => ['exclude'], + 'ownerId' => ['exclude'], + 'newOwnerId' => ['exclude'], + 'siteId' => ['exclude'], + 'enabled' => ['exclude'], + 'setEnabled' => ['exclude'], + 'enabledForSite' => ['exclude'], + 'slug' => ['exclude'], + 'fresh' => ['exclude'], + 'draftName' => ['exclude'], + 'notes' => ['exclude'], + 'fieldsLocation' => ['exclude'], + 'provisional' => ['exclude'], + 'dropProvisional' => ['exclude'], + 'addAnother' => ['exclude'], + 'visibleLayoutElements' => ['exclude'], + 'staticLayoutElements' => ['exclude'], + 'selectedTab' => ['exclude'], + 'applyParams' => ['exclude'], + 'prevalidate' => ['exclude'], + 'asUnpublishedDraft' => ['exclude'], + 'deleteProvisionalDraft' => ['exclude'], + 'updateSearchIndexImmediately' => ['exclude'], + 'failMessage' => ['exclude'], + 'redirect' => ['exclude'], + 'successMessage' => ['exclude'], + $fieldsLocation => ['exclude'], + ]; + } + + public function element(array $overrides = [], bool $checkForProvisionalDraft = false, bool $strictSite = true): ElementInterface|Response|null + { + $this->overrides = $overrides; + $this->checkForProvisionalDraft = $checkForProvisionalDraft; + $this->strictSite = $strictSite; + $this->elementType = $this->elementType(); + + $this->validateElementType($this->elementType); + + $elementId = Arr::get($overrides, 'id', $this->input('elementId')); + $elementUid = Arr::get($overrides, 'uid', $this->input('elementUid')); + $draftId = Arr::get($overrides, 'draftId', $this->input('draftId')); + $revisionId = Arr::get($overrides, 'revisionId', $this->input('revisionId')); + + [$siteId, $preferSites] = $this->site(); + + $element = match (true) { + $draftId || $revisionId => $this->elementByDraftOrRevision($draftId, $revisionId), + ! is_null($elementId) => $this->elementById(), + ! is_null($elementUid) => $this->elementByUid(), + default => null, + }; + + if (is_null($element)) { + $this->element = null; + + return null; + } + + abort_unless($this->user()->can('view', $element), 403, 'User not authorized to view this element.'); + + if ( + ! $this->strictSite && + $element->siteId !== $siteId && + ! $this->wantsJson() + ) { + return redirect($element->getCpEditUrl()); + } + + if ($element instanceof ElementInterface) { + $this->element = $element; + } + + return $element; + } + + /** + * @return class-string + */ + public function elementType(): string + { + $elementType = Arr::get($this->overrides, 'type', $this->input('elementType')); + $elementId = Arr::get($this->overrides, 'id', $this->input('elementId')); + $elementUid = Arr::get($this->overrides, 'uid', $this->input('elementUid')); + + if ($elementType) { + return $this->elementType = $elementType; + } + + if ($elementId) { + abort_if( + is_null($elementType = Elements::getElementTypeById($elementId)), + 400, + "Invalid element ID: $elementId", + ); + + return $this->elementType = $elementType; + } + + if ($elementUid) { + abort_if( + is_null($elementType = Elements::getElementTypeByUid($elementUid)), + 400, + "Invalid element UUID: $elementUid", + ); + + return $this->elementType = $elementType; + } + + abort(400, 'Request missing required param.'); + } + + private function elementQuery(): ElementQueryInterface + { + $query = $this->elementType::find(); + + if ($query instanceof NestedElementQueryInterface) { + $fieldId = Arr::get($this->overrides, 'fieldId', $this->input('fieldId')); + $ownerId = Arr::get($this->overrides, 'ownerId', $this->input('ownerId')); + + $query + ->fieldId($fieldId) + ->ownerId($ownerId); + } + + return $query; + } + + public function validateElementType(string $elementType): void + { + if (ComponentHelper::validateComponentClass($elementType, ElementInterface::class)) { + return; + } + + abort(400, new InvalidTypeException($elementType, ElementInterface::class)->getMessage()); + } + + /** + * @return array{0: int|int[]|null, 1: int[]|null} + */ + public function site(): array + { + if (! $this->elementType::isLocalized()) { + return [null, null]; + } + + $siteId = Arr::get($this->overrides, 'siteId', $this->input('siteId')); + + if ($siteId) { + $site = Sites::getSiteById($siteId, true); + + abort_if(is_null($site), 400, "Invalid site ID: $siteId"); + + if (Sites::isMultiSite() && ! $this->user()->can("editSite:$site->uid")) { + abort(403, 'User not authorized to edit content for this site.'); + } + } else { + $site = app(RequestedSite::class)->get(); + + abort_if(is_null($site), 400, 'User not authorized to edit content in any sites.'); + } + + if ($this->strictSite) { + return [$site->id, null]; + } + + return [ + Sites::getEditableSiteIds()->all(), + [$site->id], + ]; + } + + private function elementByDraftOrRevision(mixed $draftId, mixed $revisionId): ElementInterface|Response + { + $hasExplicitProvisional = Arr::has($this->overrides, 'isProvisionalDraft') || $this->has('provisional'); + $provisional = Arr::get($this->overrides, 'isProvisionalDraft', $this->input('provisional')); + [$siteId, $preferSites] = $this->site(); + + $query = $this->elementQuery() + ->draftId($draftId ? (int) $draftId : null) + ->revisionId($revisionId ? (int) $revisionId : null) + ->provisionalDrafts($hasExplicitProvisional ? (bool) $provisional : null) + ->siteId($siteId) + ->preferSites($preferSites) + ->unique() + ->status(null); + + if ($revisionId) { + $query->trashed(null); + } + + $element = $query->first(); + + if (! $element) { + // check for the canonical element as a fallback + $element = $this->elementById() ?? $this->elementByUid(); + + if ($element && $this->user()->can('view', $element)) { + if (! $this->wantsJson()) { + return redirect($element->getCpEditUrl()); + } + + return $element; + } + } + + if ($element) { + return $element; + } + + abort(400, $draftId ? "Invalid draft ID: $draftId" : "Invalid revision ID: $revisionId"); + } + + private function elementById(): ?ElementInterface + { + $elementId = Arr::get($this->overrides, 'id', $this->input('elementId')); + + if (! $elementId) { + return null; + } + + [$siteId, $preferSites] = $this->site(); + + // First check for a provisional draft, if we're open to it + if ($this->checkForProvisionalDraft) { + $element = $this->elementQuery() + ->provisionalDrafts() + ->draftOf($elementId) + ->draftCreator($this->user()) + ->siteId($siteId) + ->preferSites($preferSites) + ->unique() + ->status(null) + ->one(); + + if ($element && $this->canSave($element, $this->user())) { + return $element; + } + } + + $element = $this->elementQuery() + ->id($elementId) + ->siteId($siteId) + ->preferSites($preferSites) + ->unique() + ->drafts(null) + ->provisionalDrafts(null) + ->revisions(null) + ->status(null) + ->one(); + + if ($element) { + return $element; + } + + // finally, check for an unpublished draft + // (see https://github.com/craftcms/cms/issues/14199) + return $this->elementQuery() + ->id($elementId) + ->siteId($siteId) + ->preferSites($preferSites) + ->unique() + ->draftOf(false) + ->status(null) + ->one(); + } + + private function elementByUid(): ?ElementInterface + { + $elementUid = Arr::get($this->overrides, 'uid', $this->input('elementUid')); + + if (! $elementUid) { + return null; + } + + [$siteId, $preferSites] = $this->site(); + + $element = $this->elementQuery() + ->uid($elementUid) + ->siteId($siteId) + ->preferSites($preferSites) + ->unique() + ->status(null) + ->one(); + + if ($element) { + return $element; + } + + // check for an unpublished draft if we got this far + // (e.g. newly added matrix "block" or where autosaveDrafts is off) + // https://github.com/craftcms/cms/issues/15985 + return $this->elementQuery() + ->uid($elementUid) + ->siteId($siteId) + ->preferSites($preferSites) + ->unique() + ->status(null) + ->draftOf(false) + ->one(); + } + + private function canSave(ElementInterface $element, User $user): bool + { + if ($element->getIsRevision()) { + return false; + } + + if ($element->isProvisionalDraft) { + $element = $element->getCanonical(true); + } + + return $user->can('save', $element); + } +} diff --git a/src/Http/Resources/ElementIndexResource.php b/src/Http/Resources/ElementIndexResource.php new file mode 100644 index 00000000000..267dd46bff4 --- /dev/null +++ b/src/Http/Resources/ElementIndexResource.php @@ -0,0 +1,99 @@ +elementType(); + [$sourceKey, $source] = $this->resolveSource($elementType, $request->input('source'), $request->context()); + $elementQuery = $this->buildElementQueryState( + elementType: $elementType, + source: $source, + condition: $request->condition(), + )['query']; + + app(CurrentElementIndex::class)->activate($elementQuery); + + $viewState = $this->resolveViewState(); + + $responseData = []; + $actions = null; + $exporters = null; + + if ($this->includeActions) { + if ($request->isAdministrative() && isset($sourceKey)) { + $actions = ElementActions::availableActions($elementType, $sourceKey, $elementQuery); + $exporters = $this->availableExporters($elementType, $sourceKey); + } + + $responseData['actions'] = match (true) { + ($viewState['static'] ?? false) === true => [], + empty($actions) => null, + default => ElementActions::serializeActions($actions), + }; + + $responseData['actionsHeadHtml'] = HtmlStack::headHtml(); + $responseData['actionsBodyHtml'] = HtmlStack::bodyHtml(); + $responseData['exporters'] = empty($exporters) ? null : ElementExporters::serializeExporters($exporters); + } + + if (! $sourceKey) { + $responseData['html'] = Html::tag('div', t('Nothing yet.'), [ + 'class' => ['zilch', 'small'], + ]); + + return $responseData; + } + + $responseData['html'] = $elementType::indexHtml( + elementQuery: $elementQuery, + disabledElementIds: $request->array('disabledElementIds'), + viewState: [...$viewState, 'fieldLayouts' => $this->resolveFieldLayouts()], + sourceKey: $sourceKey, + context: $request->context(), + includeContainer: $this->includeContainer, + selectable: ( + ((! empty($actions)) || request()->boolean('selectable')) && + empty($viewState['inlineEditing']) + ), + sortable: $request->isAdministrative() && $request->boolean('sortable'), + ); + + $responseData['headHtml'] = HtmlStack::headHtml(); + $responseData['bodyHtml'] = HtmlStack::bodyHtml(); + + return $responseData; + } +} diff --git a/src/Http/RespondsWithFlash.php b/src/Http/RespondsWithFlash.php index 5d18d613907..e1cec899717 100644 --- a/src/Http/RespondsWithFlash.php +++ b/src/Http/RespondsWithFlash.php @@ -41,13 +41,13 @@ public function asJsonFailure(?string $message = null, array $data = []): JsonRe ]), 400); } - public function asSuccess(?string $message = null, array $data = [], ?string $redirect = null): Response + public function asSuccess(?string $message = null, array $data = [], ?string $redirect = null, array $notificationSettings = []): Response { if (request()->expectsJson()) { return $this->asJsonSuccess($message, $data); } - Flash::success($message); + Flash::success($message, $notificationSettings); $redirect ??= $this->getPostedRedirectUrl(); diff --git a/src/Http/Responses/ElementResponse.php b/src/Http/Responses/ElementResponse.php new file mode 100644 index 00000000000..871344eb4fb --- /dev/null +++ b/src/Http/Responses/ElementResponse.php @@ -0,0 +1,224 @@ + 'element', + 'element' => $element->toArray($element->attributes()), + ]; + + $response = $this->asSuccess($message, $data, $this->getPostedRedirectUrl($element), [ + 'details' => ! $element->dateDeleted + ? app(ElementHtml::class)->elementChipHtml($element, ['hyperlink' => true]) + : null, + ]); + + if ($supportsAddAnother && request()->boolean('addAnother')) { + $user = request()->user(); + $newElement = $element->createAnother(); + + if (! $newElement || ! Gate::check('save', $newElement)) { + abort(500, 'Unable to create a new element.'); + } + + if (! $newElement->slug) { + $newElement->slug = ElementHelper::tempSlug(); + } + + $newElement->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS); + + if (! Drafts::saveElementAsDraft($newElement, $user->id, null, null, false)) { + abort(500, sprintf('Unable to create a new element: %s', implode(', ', $element->errors()->all()))); + } + + $url = $newElement->getCpEditUrl(); + + if ($url) { + $url = Url::urlWithParams($url, ['fresh' => 1]); + } else { + $url = Url::actionUrl('elements/edit', [ + 'draftId' => $newElement->draftId, + 'siteId' => $newElement->siteId, + 'fresh' => 1, + ]); + } + + return redirect($url); + } + + return $response; + } + + public function failure(ElementInterface $element, string $message): Response + { + $data = [ + 'modelName' => 'element', + 'element' => $element->toArray($element->attributes()), + 'errors' => $element->errors()->getMessages(), + 'errorSummary' => $this->errorSummary($element), + 'invalidNestedElementIds' => $element->getInvalidNestedElementIds(), + ]; + + return $this->asFailure($message, $data); + } + + public function applyDraftFailure(ElementInterface $element): Response + { + $message = match (true) { + $element->getIsUnpublishedDraft() => mb_ucfirst(t('Couldn’t create {type}.', [ + 'type' => $element::lowerDisplayName(), + ])), + $element->isProvisionalDraft => mb_ucfirst(t('Couldn’t save {type}.', [ + 'type' => $element::lowerDisplayName(), + ])), + default => t('Couldn’t apply draft.'), + }; + + return $this->failure($element, $message); + } + + public function errorSummary(ElementInterface $element): string + { + $html = ''; + + if ($element->errors()->isNotEmpty()) { + $allErrors = $element->errors()->getMessages(); + $allKeys = array_keys($allErrors); + + // only show "top-level" errors + // if you e.g. have an assets field which is set to validate related assets, + // you should only see the top-level "Fix validation errors on the related asset" error + // and not the details of what's wrong with the selected asset; + foreach ($allKeys as $key) { + $dotPos = strrpos((string) $key, '.'); + $bracketPos = strrpos((string) $key, '['); + + if ($dotPos === false && $bracketPos === false) { + continue; + } + + $lastNestedKey = $key; + + if ($dotPos !== false) { + $lastNestedKey = substr_replace($lastNestedKey, '', $dotPos); + } + + $bracketPos = strrpos((string) $lastNestedKey, '['); + + if ($bracketPos !== false) { + $lastNestedKey = substr_replace($lastNestedKey, '', $bracketPos); + } + + if (! empty($lastNestedKey) && in_array($lastNestedKey, $allKeys)) { + unset($allErrors[$key]); + } + } + $errorsList = []; + $tabs = $element->getFieldLayout()->getTabs(); + foreach ($allErrors as $key => $errors) { + foreach ($errors as $error) { + // this is true in case of e.g. cross site validation error + if (preg_match('/^\s?\
getElements() as $layoutElement) { + if ($layoutElement instanceof BaseField && $layoutElement->attribute() === $fieldKey) { + $tabUid = $tab->uid; + break 2; + } + } + } + + // If the error is for a recursively-nested Matrix field, + // manipulate the key to only reference the nested Matrix field, entry and inner field + // Before: foo[].bar[].baz + // After: bar[].baz + if (substr_count((string) $key, '.') > 1) { + $keyParts = explode('.', (string) $key); + if (preg_match(sprintf('/\[%s\]$/', Str::uuidPattern()), $keyParts[count($keyParts) - 3])) { + $key = implode('.', array_slice($keyParts, -2)); + } + } + + $errorItem = null; + if ($error !== null) { + $error = Markdown::parseParagraph(htmlspecialchars($error)); + $errorItem = Html::beginTag('li'); + $errorItem .= Html::a(t($error), '#', [ + 'data' => [ + 'field-error-key' => $key, + 'layout-tab' => $tabUid, + ], + ]); + $errorItem .= Html::endTag('li'); + } + } + + if ($errorItem !== null) { + $errorsList[] = $errorItem; + } + } + } + + if (! empty($errorsList)) { + $heading = t('Found {num, number} {num, plural, =1{error} other{errors}}', [ + 'num' => count($errorsList), + ]); + + $html = Html::beginTag('div', [ + 'class' => ['error-summary'], + 'tabindex' => '-1', + ]). + Html::beginTag('div'). + Html::tag('span', '', [ + 'class' => 'notification-icon', + 'data-icon' => 'alert', + 'aria-label' => t('Error'), + 'role' => 'img', + ]). + Html::tag('h2', $heading). + Html::endTag('div'). + Html::beginTag('ul', [ + 'class' => ['errors'], + ]). + implode('', $errorsList). + Html::endTag('ul'). + Html::endTag('div'); + } + } + + return $html; + } +} diff --git a/src/Image/ImageTransformHelper.php b/src/Image/ImageTransformHelper.php index ba2cf888e41..d54c53c1ebb 100644 --- a/src/Image/ImageTransformHelper.php +++ b/src/Image/ImageTransformHelper.php @@ -6,6 +6,7 @@ use CraftCms\Cms\Asset\AssetsHelper; use CraftCms\Cms\Asset\Elements\Asset; +use CraftCms\Cms\Asset\Enums\FileKind; use CraftCms\Cms\Asset\Exceptions\AssetException; use CraftCms\Cms\Asset\Exceptions\AssetOperationException; use CraftCms\Cms\Asset\Exceptions\ImageException; @@ -83,7 +84,7 @@ public static function detectTransformFormat(Asset $asset): string return $ext; } - if ($asset->kind !== Asset::KIND_IMAGE) { + if ($asset->kind !== FileKind::Image->value) { throw new AssetOperationException(t('Tried to detect the appropriate image format for a non-image!')); } diff --git a/src/Image/ImageTransformer.php b/src/Image/ImageTransformer.php index 9430091947d..7f6c1ef1b25 100644 --- a/src/Image/ImageTransformer.php +++ b/src/Image/ImageTransformer.php @@ -20,6 +20,7 @@ use CraftCms\Cms\Image\Events\DeletingTransformedImage; use CraftCms\Cms\Image\Events\TransformingImage; use CraftCms\Cms\Image\Jobs\GenerateImageTransform; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\DateTimeHelper; use CraftCms\Cms\Support\Facades\I18N; @@ -32,9 +33,8 @@ use Illuminate\Filesystem\LocalFilesystemAdapter; use Illuminate\Support\Facades\DB; use Illuminate\Support\Sleep; +use RuntimeException; use Throwable; -use yii\base\InvalidConfigException; -use yii\base\NotSupportedException; use function CraftCms\Cms\maxPowerCaptain; use function CraftCms\Cms\t; @@ -184,7 +184,7 @@ public function deleteImageTransformFile(Asset $asset, ImageTransformIndex $tran try { $asset->getVolume()->transformDisk()->delete($path); - } catch (InvalidConfigException|NotSupportedException) { + } catch (RuntimeException|NotSupportedException) { // NBD } } diff --git a/src/ProjectConfig/ProjectConfig.php b/src/ProjectConfig/ProjectConfig.php index 5db76ebc849..296274d622f 100644 --- a/src/ProjectConfig/ProjectConfig.php +++ b/src/ProjectConfig/ProjectConfig.php @@ -6,12 +6,12 @@ use Craft; use craft\helpers\App; -use craft\services\ElementSources; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Asset\Data\Volume; use CraftCms\Cms\Cms; use CraftCms\Cms\Config\GeneralConfig; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\ElementSources; use CraftCms\Cms\Entry\Data\EntryType; use CraftCms\Cms\Field\Contracts\FieldInterface; use CraftCms\Cms\Filesystem\Contracts\FsInterface; @@ -31,6 +31,7 @@ use CraftCms\Cms\ProjectConfig\Exceptions\ReadonlyException; use CraftCms\Cms\ProjectConfig\Exceptions\StaleResourceException; use CraftCms\Cms\Section\Data\Section; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Shared\Exceptions\OperationAbortedException; use CraftCms\Cms\Shared\Models\Info; use CraftCms\Cms\Site\Data\Site; @@ -60,6 +61,7 @@ use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; use InvalidArgumentException; +use RuntimeException; use SplFileInfo; use Symfony\Component\Finder\Finder; use Symfony\Component\Yaml\Yaml; @@ -67,8 +69,6 @@ use yii\base\Application; use yii\base\ErrorException; use yii\base\Exception; -use yii\base\InvalidConfigException; -use yii\base\NotSupportedException; use yii\web\ServerErrorHttpException; use function Illuminate\Filesystem\join_paths; @@ -446,7 +446,7 @@ private function findInternal(array $config, callable $callback, ?string $path, * @throws Exception * @throws NotSupportedException if the service is set to read-only mode * @throws ServerErrorHttpException - * @throws InvalidConfigException + * @throws RuntimeException * @throws BusyResourceException if a lock could not be acquired * @throws StaleResourceException if the loaded project config is out-of-date */ @@ -1759,7 +1759,7 @@ private function _getElementSourceData(array $sourceConfigs): array if ($config['type'] === ElementSources::TYPE_CUSTOM && isset($config['condition'])) { try { $config['condition'] = Conditions::createCondition($config['condition'])->getConfig(); - } catch (InvalidArgumentException|InvalidConfigException) { + } catch (InvalidArgumentException|RuntimeException) { // Ignore it } } diff --git a/src/ProjectConfig/ProjectConfigHelper.php b/src/ProjectConfig/ProjectConfigHelper.php index 9ed403a59c7..0d51b925d52 100644 --- a/src/ProjectConfig/ProjectConfigHelper.php +++ b/src/ProjectConfig/ProjectConfigHelper.php @@ -18,8 +18,8 @@ use CraftCms\DependencyAwareCache\Facades\DependencyCache; use Illuminate\Support\Facades\Log; use InvalidArgumentException; +use RuntimeException; use StdClass; -use yii\base\InvalidConfigException; class ProjectConfigHelper { @@ -259,7 +259,7 @@ public static function reset(): void * * @param array $config Config array to clean * - * @throws InvalidConfigException if config contains unexpected data. + * @throws RuntimeException if config contains unexpected data. */ public static function cleanupConfig(array $config): array { @@ -282,7 +282,7 @@ public static function cleanupConfig(array $config): array /** * Cleans a config value. * - * @throws InvalidConfigException + * @throws RuntimeException */ private static function _cleanupConfigValue(mixed $value): mixed { @@ -293,7 +293,7 @@ private static function _cleanupConfigValue(mixed $value): mixed if (! empty($value) && ! is_scalar($value) && ! is_array($value)) { Log::info('Unexpected data encountered in config data - '.print_r($value, true)); - throw new InvalidConfigException('Unexpected data encountered in config data'); + throw new RuntimeException('Unexpected data encountered in config data'); } if (is_array($value)) { diff --git a/src/Providers/AppServiceProvider.php b/src/Providers/AppServiceProvider.php index b076009ab43..b6d2709cd3c 100644 --- a/src/Providers/AppServiceProvider.php +++ b/src/Providers/AppServiceProvider.php @@ -9,6 +9,7 @@ use CraftCms\Cms\Edition; use CraftCms\Cms\GarbageCollection\GarbageCollection; use CraftCms\Cms\Http\Mixins\RequestMixin; +use CraftCms\Cms\Http\Mixins\SessionMixin; use CraftCms\Cms\ProjectConfig\ProjectConfig; use CraftCms\Cms\Support\Env; use CraftCms\Cms\Support\Facades\Path; @@ -29,6 +30,7 @@ use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Redirect; +use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; use Illuminate\Validation\Rules\Password; @@ -123,6 +125,7 @@ private function registerMacros(): void }); Request::mixin(new RequestMixin); + Session::mixin(new SessionMixin); Response::macro('setNoCacheHeaders', function (bool $replace = true) { $this->header('Expires', '0', $replace); diff --git a/src/Queue/BatchedElementJob.php b/src/Queue/BatchedElementJob.php index cf81ca15575..873ec76d3a4 100644 --- a/src/Queue/BatchedElementJob.php +++ b/src/Queue/BatchedElementJob.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Queue; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\ElementQuery; use CraftCms\Cms\Support\Facades\BulkOps; use CraftCms\Cms\Support\Typecast; diff --git a/src/Queue/BatchedJob.php b/src/Queue/BatchedJob.php index 9c50d7b2d92..75bc27bbb2e 100644 --- a/src/Queue/BatchedJob.php +++ b/src/Queue/BatchedJob.php @@ -7,6 +7,7 @@ use CraftCms\Cms\Support\Facades\I18N; use CraftCms\Cms\Support\PHP; use Illuminate\Contracts\Database\Query\Builder; +use Illuminate\Queue\Jobs\SyncJob; use Override; use function CraftCms\Cms\t; @@ -127,6 +128,13 @@ protected function spawnNextBatch(): void { $nextJob = clone $this; $nextJob->batchIndex++; + + if ($nextJob->job instanceof SyncJob) { + $nextJob->handle(); + + return; + } + dispatch($nextJob); } diff --git a/src/Queue/Job.php b/src/Queue/Job.php index 42d7219d0a3..ea493eb9553 100644 --- a/src/Queue/Job.php +++ b/src/Queue/Job.php @@ -89,6 +89,14 @@ public function middleware(): array */ public function shouldStillRun(): bool { + if ( + $this->job !== null && + method_exists($this->job, 'getConnectionName') && + $this->job->getConnectionName() === 'sync' + ) { + return true; + } + $uuid = $this->job?->uuid(); if ($uuid === null) { diff --git a/src/Route/MatchedElement.php b/src/Route/MatchedElement.php index 0a5c1cc0022..a34300a3ec4 100644 --- a/src/Route/MatchedElement.php +++ b/src/Route/MatchedElement.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Route; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use Illuminate\Support\Facades\Context; /** diff --git a/src/RouteToken/Data/RouteToken.php b/src/RouteToken/Data/RouteToken.php index ba5bc554884..e000ed192ac 100644 --- a/src/RouteToken/Data/RouteToken.php +++ b/src/RouteToken/Data/RouteToken.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\RouteToken\Data; -use craft\base\ElementInterface; use CraftCms\Cms\Component\Component; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use Illuminate\Validation\Rule; class RouteToken extends Component diff --git a/src/Search/Events/BeforeIndexKeywords.php b/src/Search/Events/BeforeIndexKeywords.php index 5036094ce8c..cbb19d49b8f 100644 --- a/src/Search/Events/BeforeIndexKeywords.php +++ b/src/Search/Events/BeforeIndexKeywords.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Search\Events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Shared\Concerns\ValidatableEvent; /** diff --git a/src/Search/Jobs/UpdateSearchIndex.php b/src/Search/Jobs/UpdateSearchIndex.php index cb69b9cc86b..9ecf5959ba9 100644 --- a/src/Search/Jobs/UpdateSearchIndex.php +++ b/src/Search/Jobs/UpdateSearchIndex.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Search\Jobs; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Queue\Job; use CraftCms\Cms\Support\Facades\I18N; use CraftCms\Cms\Support\Facades\Search; diff --git a/src/Search/Search.php b/src/Search/Search.php index 7756fd1acba..2a006ca572e 100644 --- a/src/Search/Search.php +++ b/src/Search/Search.php @@ -4,9 +4,9 @@ namespace CraftCms\Cms\Search; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Element\Queries\ElementQuery; use CraftCms\Cms\Field\Contracts\FieldInterface; @@ -166,7 +166,7 @@ public function queueIndexElement(ElementInterface $element, array $fieldHandles elementId: $element->id, siteId: $element->siteId, queued: true, - ))->onQueue(Cms::config()->lowPriorityQueueName); + ))->onQueue(Cms::config()->lowPriorityQueueName)->afterCommit(); } /** @@ -324,8 +324,8 @@ public function searchElements(ElementQueryInterface $elementQuery): array if ($elementQuery instanceof ElementQuery) { $elementQuery->reorder(); $elementQuery->select('elements.id as id'); - $elementQuery->getQuery()->offset = null; - $elementQuery->getQuery()->limit = null; + $elementQuery->offset(null); + $elementQuery->limit(null); $ids = $elementQuery->pluck('id')->all(); } else { $ids = $elementQuery; diff --git a/src/Section/Sections.php b/src/Section/Sections.php index 408dc31f184..502c0213317 100644 --- a/src/Section/Sections.php +++ b/src/Section/Sections.php @@ -4,7 +4,6 @@ namespace CraftCms\Cms\Section; -use Craft; use CraftCms\Cms\Cms; use CraftCms\Cms\Database\Table; use CraftCms\Cms\Element\Element; @@ -54,9 +53,9 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use RuntimeException; use Throwable; use Tpetry\QueryExpressions\Language\Alias; -use yii\base\InvalidConfigException; #[Scoped] class Sections @@ -752,7 +751,7 @@ public function handleChangedSection(ConfigEvent $event): void }); $this->elements->restoreElements($typeEntries); - } catch (InvalidConfigException) { + } catch (RuntimeException) { // the entry type probably wasn't restored } } diff --git a/src/Shared/Exceptions/NotSupportedException.php b/src/Shared/Exceptions/NotSupportedException.php new file mode 100644 index 00000000000..2a77074af40 --- /dev/null +++ b/src/Shared/Exceptions/NotSupportedException.php @@ -0,0 +1,9 @@ + getEditableDrafts(\craft\base\ElementInterface $element, string|null $permission = null) - * @method static \craft\base\ElementInterface createDraft(\craft\base\ElementInterface $canonical, int|null $creatorId = null, string|null $name = null, string|null $notes = null, array $newAttributes = [], bool $provisional = false) + * @method static \Illuminate\Support\Collection<\CraftCms\Cms\Element\Contracts\ElementInterface> getEditableDrafts(\CraftCms\Cms\Element\Contracts\ElementInterface $element, string|null $permission = null) + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface createDraft(\CraftCms\Cms\Element\Contracts\ElementInterface $canonical, int|null $creatorId = null, string|null $name = null, string|null $notes = null, array $newAttributes = [], bool $provisional = false) * @method static string generateDraftName(int $canonicalId) - * @method static bool saveElementAsDraft(\craft\base\ElementInterface $element, int|null $creatorId = null, string|null $name = null, string|null $notes = null, bool $markAsSaved = true) - * @method static \craft\base\ElementInterface applyDraft(\craft\base\ElementInterface $draft, array $newAttributes = []) - * @method static void removeDraftData(\craft\base\ElementInterface $draft) + * @method static bool saveElementAsDraft(\CraftCms\Cms\Element\Contracts\ElementInterface $element, int|null $creatorId = null, string|null $name = null, string|null $notes = null, bool $markAsSaved = true) + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface applyDraft(\CraftCms\Cms\Element\Contracts\ElementInterface $draft, array $newAttributes = []) + * @method static void removeDraftData(\CraftCms\Cms\Element\Contracts\ElementInterface $draft) * @method static void purgeUnsavedDrafts() * @method static int insertDraftRow(string|null $name, string|null $notes = null, int|null $creatorId = null, int|null $canonicalId = null, bool $trackChanges = false, bool $provisional = false) - * @method static \craft\base\ElementInterface[] withProvisionalDrafts(\craft\base\ElementInterface[] $elements, \CraftCms\Cms\User\Elements\User|null $user = null) - * @method static void loadProvisionalChanges(\craft\base\ElementInterface[] $elements, \CraftCms\Cms\User\Elements\User|null $user = null) + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface[] withProvisionalDrafts(\CraftCms\Cms\Element\Contracts\ElementInterface[] $elements, \CraftCms\Cms\User\Elements\User|null $user = null) + * @method static void loadProvisionalChanges(\CraftCms\Cms\Element\Contracts\ElementInterface[] $elements, \CraftCms\Cms\User\Elements\User|null $user = null) * * @see \CraftCms\Cms\Element\Drafts */ diff --git a/src/Support/Facades/ElementActions.php b/src/Support/Facades/ElementActions.php index d9f7629a7d2..5403b33d602 100644 --- a/src/Support/Facades/ElementActions.php +++ b/src/Support/Facades/ElementActions.php @@ -8,8 +8,8 @@ use Override; /** - * @method static \CraftCms\Cms\Element\Contracts\ElementActionInterface[] availableActions(class-string<\craft\base\ElementInterface> $elementType, string $sourceKey, \CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface $elementQuery) - * @method static \CraftCms\Cms\Element\Contracts\ElementActionInterface createAction(\CraftCms\Cms\Element\Contracts\ElementActionInterface|class-string<\CraftCms\Cms\Element\Contracts\ElementActionInterface>|array $action, class-string<\craft\base\ElementInterface> $elementType) + * @method static \CraftCms\Cms\Element\Contracts\ElementActionInterface[] availableActions(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, string $sourceKey, \CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface $elementQuery) + * @method static \CraftCms\Cms\Element\Contracts\ElementActionInterface createAction(\CraftCms\Cms\Element\Contracts\ElementActionInterface|class-string<\CraftCms\Cms\Element\Contracts\ElementActionInterface>|array $action, class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType) * @method static array serializeActions(iterable<\CraftCms\Cms\Element\Contracts\ElementActionInterface> $actions) * @method static \CraftCms\Cms\Element\Contracts\ElementActionInterface|null resolveAction(iterable<\CraftCms\Cms\Element\Contracts\ElementActionInterface> $actions, string $actionClass) * @method static array invoke(\CraftCms\Cms\Element\Contracts\ElementActionInterface $action, \CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface $query) diff --git a/src/Support/Facades/ElementActivity.php b/src/Support/Facades/ElementActivity.php index c455628f8f5..32ab2ff8b24 100644 --- a/src/Support/Facades/ElementActivity.php +++ b/src/Support/Facades/ElementActivity.php @@ -8,8 +8,8 @@ use Override; /** - * @method static \Illuminate\Support\Collection getRecentActivity(\craft\base\ElementInterface $element, int|null $excludeUserId = null) - * @method static void trackActivity(\craft\base\ElementInterface $element, \CraftCms\Cms\Element\Enums\ElementActivityType $type, \CraftCms\Cms\User\Elements\User|null $user = null) + * @method static \Illuminate\Support\Collection getRecentActivity(\CraftCms\Cms\Element\Contracts\ElementInterface $element, int|null $excludeUserId = null) + * @method static void trackActivity(\CraftCms\Cms\Element\Contracts\ElementInterface $element, \CraftCms\Cms\Element\Enums\ElementActivityType $type, \CraftCms\Cms\User\Elements\User|null $user = null) * * @see \CraftCms\Cms\Element\ElementActivity */ diff --git a/src/Support/Facades/ElementCaches.php b/src/Support/Facades/ElementCaches.php index ab81f72ba69..9cab07f71d9 100644 --- a/src/Support/Facades/ElementCaches.php +++ b/src/Support/Facades/ElementCaches.php @@ -12,11 +12,11 @@ * @method static void startCollectingCacheInfo() * @method static void collectCacheTags(array $tags) * @method static void setCacheExpiryDate(\DateTime $expiryDate) - * @method static void collectCacheInfoForElement(\craft\base\ElementInterface $element) + * @method static void collectCacheInfoForElement(\CraftCms\Cms\Element\Contracts\ElementInterface $element) * @method static array stopCollectingCacheInfo() * @method static array invalidateAll() - * @method static array invalidateForElementType(class-string<\craft\base\ElementInterface> $elementType) - * @method static array invalidateForElement(\craft\base\ElementInterface $element) + * @method static array invalidateForElementType(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType) + * @method static array invalidateForElement(\CraftCms\Cms\Element\Contracts\ElementInterface $element) * * @see \CraftCms\Cms\Element\ElementCaches */ diff --git a/src/Support/Facades/ElementExporters.php b/src/Support/Facades/ElementExporters.php index 8c6c6e8a7ab..099e8220c1b 100644 --- a/src/Support/Facades/ElementExporters.php +++ b/src/Support/Facades/ElementExporters.php @@ -8,8 +8,8 @@ use Override; /** - * @method static \CraftCms\Cms\Element\Contracts\ElementExporterInterface[] availableExporters(class-string<\craft\base\ElementInterface> $elementType, string $sourceKey) - * @method static \CraftCms\Cms\Element\Contracts\ElementExporterInterface createExporter(\CraftCms\Cms\Element\Contracts\ElementExporterInterface|class-string<\CraftCms\Cms\Element\Contracts\ElementExporterInterface>|array $exporter, class-string<\craft\base\ElementInterface> $elementType) + * @method static \CraftCms\Cms\Element\Contracts\ElementExporterInterface[] availableExporters(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, string $sourceKey) + * @method static \CraftCms\Cms\Element\Contracts\ElementExporterInterface createExporter(\CraftCms\Cms\Element\Contracts\ElementExporterInterface|class-string<\CraftCms\Cms\Element\Contracts\ElementExporterInterface>|array $exporter, class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType) * @method static array serializeExporters(iterable<\CraftCms\Cms\Element\Contracts\ElementExporterInterface> $exporters) * @method static \CraftCms\Cms\Element\Contracts\ElementExporterInterface|null resolveExporter(iterable<\CraftCms\Cms\Element\Contracts\ElementExporterInterface> $exporters, string $exporterClass) * @method static \Symfony\Component\HttpFoundation\Response export(\CraftCms\Cms\Element\Contracts\ElementExporterInterface $exporter, \CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface $query, string $format = 'csv') diff --git a/src/Support/Facades/ElementSources.php b/src/Support/Facades/ElementSources.php index 4e62166d0b1..4fca2c67083 100644 --- a/src/Support/Facades/ElementSources.php +++ b/src/Support/Facades/ElementSources.php @@ -9,23 +9,23 @@ /** * @method static \Illuminate\Support\Collection filterExtraHeadings(array[]|\Illuminate\Support\Collection $sources) - * @method static \Illuminate\Support\Collection getSources(class-string<\craft\base\ElementInterface> $elementType, string $context = 'index', bool $withDisabled = false, string|null $page = null) - * @method static bool sourceExists(class-string<\craft\base\ElementInterface> $elementType, string $sourceKey, string $context = 'index', bool $withDisabled = false, string|null $page = null) - * @method static array|null findSource(class-string<\craft\base\ElementInterface> $elementType, string $sourceKey, string $context = 'index', bool $withDisabled = false, string|null $page = null) - * @method static \Illuminate\Support\Collection getPages(class-string<\craft\base\ElementInterface> $elementType) - * @method static string|null getFirstPage(class-string<\craft\base\ElementInterface> $elementType, string $context = 'index', bool $withDisabled = false) - * @method static bool pageExists(class-string<\craft\base\ElementInterface> $elementType, string $page, string $context = 'index', bool $withDisabled = false) + * @method static \Illuminate\Support\Collection getSources(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, string $context = 'index', bool $withDisabled = false, string|null $page = null) + * @method static bool sourceExists(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, string $sourceKey, string $context = 'index', bool $withDisabled = false, string|null $page = null) + * @method static array|null findSource(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, string $sourceKey, string $context = 'index', bool $withDisabled = false, string|null $page = null) + * @method static \Illuminate\Support\Collection getPages(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType) + * @method static string|null getFirstPage(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, string $context = 'index', bool $withDisabled = false) + * @method static bool pageExists(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, string $page, string $context = 'index', bool $withDisabled = false) * @method static string pageNameId(string $page) - * @method static void saveSources(class-string<\craft\base\ElementInterface> $elementType, array $sources) - * @method static \Illuminate\Support\Collection getAvailableTableAttributes(class-string<\craft\base\ElementInterface> $elementType) - * @method static \Illuminate\Support\Collection getTableAttributes(class-string<\craft\base\ElementInterface> $elementType, string $sourceKey, string[]|null $customAttributes = null, \CraftCms\Cms\FieldLayout\FieldLayout[]|null $fieldLayouts = null) - * @method static \Illuminate\Support\Collection<\CraftCms\Cms\FieldLayout\FieldLayout> getFieldLayoutsForSource(class-string<\craft\base\ElementInterface> $elementType, string $sourceKey) - * @method static \Illuminate\Support\Collection getSourceSortOptions(class-string<\craft\base\ElementInterface> $elementType, string $sourceKey) + * @method static void saveSources(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, array $sources) + * @method static \Illuminate\Support\Collection getAvailableTableAttributes(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType) + * @method static \Illuminate\Support\Collection getTableAttributes(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, string $sourceKey, string[]|null $customAttributes = null, \CraftCms\Cms\FieldLayout\FieldLayout[]|null $fieldLayouts = null) + * @method static \Illuminate\Support\Collection<\CraftCms\Cms\FieldLayout\FieldLayout> getFieldLayoutsForSource(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, string $sourceKey) + * @method static \Illuminate\Support\Collection getSourceSortOptions(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, string $sourceKey) * @method static \Illuminate\Support\Collection getSortOptionsForFieldLayouts(\CraftCms\Cms\FieldLayout\FieldLayout[]|\Illuminate\Support\Collection<\CraftCms\Cms\FieldLayout\FieldLayout> $fieldLayouts) - * @method static \Illuminate\Support\Collection getSourceTableAttributes(class-string<\craft\base\ElementInterface> $elementType, string $sourceKey) + * @method static \Illuminate\Support\Collection getSourceTableAttributes(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, string $sourceKey) * @method static \Illuminate\Support\Collection getTableAttributesForFieldLayouts(\CraftCms\Cms\FieldLayout\FieldLayout[]|\Illuminate\Support\Collection<\CraftCms\Cms\FieldLayout\FieldLayout> $fieldLayouts) - * @method static array getPageSettings(class-string<\craft\base\ElementInterface> $elementType) - * @method static void savePageSettings(class-string<\craft\base\ElementInterface> $elementType, array $pageSettings) + * @method static array getPageSettings(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType) + * @method static void savePageSettings(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, array $pageSettings) * * @see \CraftCms\Cms\Element\ElementSources */ diff --git a/src/Support/Facades/Elements.php b/src/Support/Facades/Elements.php index ef514e24c37..541d2e5e871 100644 --- a/src/Support/Facades/Elements.php +++ b/src/Support/Facades/Elements.php @@ -8,44 +8,44 @@ use Override; /** - * @method static class-string<\craft\base\ElementInterface>|null getElementTypeById(int $elementId) + * @method static class-string<\CraftCms\Cms\Element\Contracts\ElementInterface>|null getElementTypeById(int $elementId) * @method static string|null getElementTypeByUid(string $uid) * @method static string|null getElementTypeByKey(string $property, int|string $elementId) * @method static string[] getElementTypesByIds(int[] $elementIds) - * @method static class-string<\craft\base\ElementInterface>[] getAllElementTypes() + * @method static class-string<\CraftCms\Cms\Element\Contracts\ElementInterface>[] getAllElementTypes() * @method static string|null getElementTypeByRefHandle(string $refHandle) - * @method static \craft\base\ElementInterface createElement(class-string<\craft\base\ElementInterface>|array $config) - * @method static \CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface createElementQuery(class-string<\craft\base\ElementInterface> $elementType) - * @method static \craft\base\ElementInterface|null getElementById(int $elementId, class-string<\craft\base\ElementInterface>|null $elementType = null, int|string|int[]|null $siteId = null, array $criteria = []) - * @method static \craft\base\ElementInterface|null getElementByUid(string $uid, class-string<\craft\base\ElementInterface>|null $elementType = null, int|string|int[]|null $siteId = null, array $criteria = []) - * @method static \craft\base\ElementInterface|null getElementByUri(string $uri, int|null $siteId = null, bool $enabledOnly = false) + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface createElement(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface>|array $config) + * @method static \CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface createElementQuery(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType) + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface|null getElementById(int $elementId, class-string<\CraftCms\Cms\Element\Contracts\ElementInterface>|null $elementType = null, int|string|int[]|null $siteId = null, array $criteria = []) + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface|null getElementByUid(string $uid, class-string<\CraftCms\Cms\Element\Contracts\ElementInterface>|null $elementType = null, int|string|int[]|null $siteId = null, array $criteria = []) + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface|null getElementByUri(string $uri, int|null $siteId = null, bool $enabledOnly = false) * @method static string|null getElementUriForSite(int $elementId, int $siteId) * @method static int[] getEnabledSiteIdsForElement(int $elementId) - * @method static bool saveElement(\craft\base\ElementInterface $element, bool $runValidation = true, bool $propagate = true, bool|null $updateSearchIndex = null, bool $forceTouch = false, bool|null $crossSiteValidate = false, bool $saveContent = false) - * @method static void setElementUri(\craft\base\ElementInterface $element) - * @method static void mergeCanonicalChanges(\craft\base\ElementInterface $element) - * @method static \craft\base\ElementInterface updateCanonicalElement(\craft\base\ElementInterface $element, array $newAttributes = []) + * @method static bool saveElement(\CraftCms\Cms\Element\Contracts\ElementInterface $element, bool $runValidation = true, bool $propagate = true, bool|null $updateSearchIndex = null, bool $forceTouch = false, bool|null $crossSiteValidate = false, bool $saveContent = false) + * @method static void setElementUri(\CraftCms\Cms\Element\Contracts\ElementInterface $element) + * @method static void mergeCanonicalChanges(\CraftCms\Cms\Element\Contracts\ElementInterface $element) + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface updateCanonicalElement(\CraftCms\Cms\Element\Contracts\ElementInterface $element, array $newAttributes = []) * @method static void resaveElements(\CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface $query, bool $continueOnError = false, bool $skipRevisions = true, bool|null $updateSearchIndex = null, bool $touch = false) * @method static void propagateElements(\CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface $query, int|int[]|null $siteIds = null, bool $continueOnError = false) - * @method static \craft\base\ElementInterface propagateElement(\craft\base\ElementInterface $element, int $siteId, \craft\base\ElementInterface|false|null $siteElement = null) - * @method static \craft\base\ElementInterface duplicateElement(\craft\base\ElementInterface $element, array $newAttributes = [], bool $placeInStructure = true, bool $asUnpublishedDraft = false, bool $checkAuthorization = false, bool $copyModifiedFields = false) - * @method static void updateElementSlugAndUri(\craft\base\ElementInterface $element, bool $updateOtherSites = true, bool $updateDescendants = true, bool $queue = false) - * @method static void updateElementSlugAndUriInOtherSites(\craft\base\ElementInterface $element) - * @method static void updateDescendantSlugsAndUris(\craft\base\ElementInterface $element, bool $updateOtherSites = true, bool $queue = false) + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface propagateElement(\CraftCms\Cms\Element\Contracts\ElementInterface $element, int $siteId, \CraftCms\Cms\Element\Contracts\ElementInterface|false|null $siteElement = null) + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface duplicateElement(\CraftCms\Cms\Element\Contracts\ElementInterface $element, array $newAttributes = [], bool $placeInStructure = true, bool $asUnpublishedDraft = false, bool $checkAuthorization = false, bool $copyModifiedFields = false) + * @method static void updateElementSlugAndUri(\CraftCms\Cms\Element\Contracts\ElementInterface $element, bool $updateOtherSites = true, bool $updateDescendants = true, bool $queue = false) + * @method static void updateElementSlugAndUriInOtherSites(\CraftCms\Cms\Element\Contracts\ElementInterface $element) + * @method static void updateDescendantSlugsAndUris(\CraftCms\Cms\Element\Contracts\ElementInterface $element, bool $updateOtherSites = true, bool $queue = false) * @method static bool mergeElementsByIds(int $mergedElementId, int $prevailingElementId) - * @method static bool mergeElements(\craft\base\ElementInterface $mergedElement, \craft\base\ElementInterface $prevailingElement) - * @method static bool deleteElementById(int $elementId, class-string<\craft\base\ElementInterface>|null $elementType = null, int|null $siteId = null, bool $hardDelete = false) - * @method static bool deleteElement(\craft\base\ElementInterface $element, bool $hardDelete = false) - * @method static void deleteElementForSite(\craft\base\ElementInterface $element) - * @method static void deleteElementsForSite(\craft\base\ElementInterface[] $elements) - * @method static bool restoreElement(\craft\base\ElementInterface $element) - * @method static bool restoreElements(\craft\base\ElementInterface[] $elements) + * @method static bool mergeElements(\CraftCms\Cms\Element\Contracts\ElementInterface $mergedElement, \CraftCms\Cms\Element\Contracts\ElementInterface $prevailingElement) + * @method static bool deleteElementById(int $elementId, class-string<\CraftCms\Cms\Element\Contracts\ElementInterface>|null $elementType = null, int|null $siteId = null, bool $hardDelete = false) + * @method static bool deleteElement(\CraftCms\Cms\Element\Contracts\ElementInterface $element, bool $hardDelete = false) + * @method static void deleteElementForSite(\CraftCms\Cms\Element\Contracts\ElementInterface $element) + * @method static void deleteElementsForSite(\CraftCms\Cms\Element\Contracts\ElementInterface[] $elements) + * @method static bool restoreElement(\CraftCms\Cms\Element\Contracts\ElementInterface $element) + * @method static bool restoreElements(\CraftCms\Cms\Element\Contracts\ElementInterface[] $elements) * @method static string parseRefs(string $str, int|null $defaultSiteId = null) - * @method static void setPlaceholderElement(\craft\base\ElementInterface $element) - * @method static \craft\base\ElementInterface[] getPlaceholderElements() - * @method static \craft\base\ElementInterface|null getPlaceholderElement(int $sourceId, int $siteId) + * @method static void setPlaceholderElement(\CraftCms\Cms\Element\Contracts\ElementInterface $element) + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface[] getPlaceholderElements() + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface|null getPlaceholderElement(int $sourceId, int $siteId) * @method static \CraftCms\Cms\Element\Data\EagerLoadPlan[] createEagerLoadingPlans(array|string $with) - * @method static void eagerLoadElements(class-string<\craft\base\ElementInterface> $elementType, \craft\base\ElementInterface[] $elements, array|string|\CraftCms\Cms\Element\Data\EagerLoadPlan[] $with) + * @method static void eagerLoadElements(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, \CraftCms\Cms\Element\Contracts\ElementInterface[] $elements, array|string|\CraftCms\Cms\Element\Data\EagerLoadPlan[] $with) * * @see \CraftCms\Cms\Element\Elements */ diff --git a/src/Support/Facades/Fields.php b/src/Support/Facades/Fields.php index 87e49b5172d..99836895e91 100644 --- a/src/Support/Facades/Fields.php +++ b/src/Support/Facades/Fields.php @@ -45,15 +45,15 @@ * @method static \CraftCms\Cms\FieldLayout\FieldLayout|null getLayoutById(int $layoutId, bool $withTrashed = false) * @method static \CraftCms\Cms\FieldLayout\FieldLayout|null getLayoutByUid(string $uid) * @method static \Illuminate\Support\Collection<\CraftCms\Cms\FieldLayout\FieldLayout> getLayoutsByIds(int[] $layoutIds) - * @method static \CraftCms\Cms\FieldLayout\FieldLayout|null getLayoutByType(class-string<\craft\base\ElementInterface> $type, bool $create = true) - * @method static \Illuminate\Support\Collection<\CraftCms\Cms\FieldLayout\FieldLayout> getLayoutsByType(class-string<\craft\base\ElementInterface> $type) + * @method static \CraftCms\Cms\FieldLayout\FieldLayout|null getLayoutByType(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $type, bool $create = true) + * @method static \Illuminate\Support\Collection<\CraftCms\Cms\FieldLayout\FieldLayout> getLayoutsByType(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $type) * @method static \CraftCms\Cms\FieldLayout\FieldLayout createLayout(array $config) * @method static \CraftCms\Cms\FieldLayout\FieldLayoutElement createLayoutElement(array $config) * @method static \CraftCms\Cms\FieldLayout\FieldLayout assembleLayoutFromPost(string|null $namespace = null) * @method static bool saveLayout(\CraftCms\Cms\FieldLayout\FieldLayout $layout, bool $runValidation = true) * @method static bool deleteLayoutById(int|int[] $layoutId, bool $hardDelete = false) * @method static bool deleteLayout(\CraftCms\Cms\FieldLayout\FieldLayout $layout, bool $hardDelete = false) - * @method static bool deleteLayoutsByType(class-string<\craft\base\ElementInterface> $type) + * @method static bool deleteLayoutsByType(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $type) * @method static bool restoreLayoutById(int $id) * @method static void applyFieldSave(string $fieldUid, array $data, string $context) * diff --git a/src/Support/Facades/Gql.php b/src/Support/Facades/Gql.php index 2e8b072088b..f431956a140 100644 --- a/src/Support/Facades/Gql.php +++ b/src/Support/Facades/Gql.php @@ -37,12 +37,12 @@ * @method static \CraftCms\Cms\Gql\Data\GqlSchema|null getSchemaById(int $id) * @method static \CraftCms\Cms\Gql\Data\GqlSchema|null getSchemaByUid(string $uid) * @method static \CraftCms\Cms\Gql\Data\GqlSchema[] getSchemas() - * @method static array getOrSetContentArguments(class-string<\craft\base\ElementInterface> $elementType, callable $setter) + * @method static array getOrSetContentArguments(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, callable $setter) * @method static array getFieldLayoutArguments(\CraftCms\Cms\FieldLayout\FieldLayout $fieldLayout) - * @method static array defineContentArgumentsForFieldLayouts(class-string<\craft\base\ElementInterface> $elementType, \CraftCms\Cms\FieldLayout\FieldLayout[] $fieldLayouts) - * @method static array defineContentArgumentsForFields(class-string<\craft\base\ElementInterface> $elementType, \CraftCms\Cms\Field\Contracts\FieldInterface[] $fields) - * @method static array defineContentArgumentsForGeneratedFields(class-string<\craft\base\ElementInterface> $elementType, array $fields) - * @method static array getContentArguments(array $contexts, class-string<\craft\base\ElementInterface> $elementType) + * @method static array defineContentArgumentsForFieldLayouts(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, \CraftCms\Cms\FieldLayout\FieldLayout[] $fieldLayouts) + * @method static array defineContentArgumentsForFields(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, \CraftCms\Cms\Field\Contracts\FieldInterface[] $fields) + * @method static array defineContentArgumentsForGeneratedFields(class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType, array $fields) + * @method static array getContentArguments(array $contexts, class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType) * @method static \GraphQL\Error\Error[] handleQueryErrors(\GraphQL\Error\Error[] $errors, callable $formatter) * @method static array prepareFieldDefinitions(array $fields, string $typeName) * diff --git a/src/Support/Facades/Revisions.php b/src/Support/Facades/Revisions.php index 25e7f9efcc1..f433e3be002 100644 --- a/src/Support/Facades/Revisions.php +++ b/src/Support/Facades/Revisions.php @@ -8,8 +8,8 @@ use Override; /** - * @method static int createRevision(\craft\base\ElementInterface $canonical, int|null $creatorId = null, string|null $notes = null, array $newAttributes = [], bool $force = false) - * @method static \craft\base\ElementInterface revertToRevision(\craft\base\ElementInterface $revision, int $creatorId) + * @method static int createRevision(\CraftCms\Cms\Element\Contracts\ElementInterface $canonical, int|null $creatorId = null, string|null $notes = null, array $newAttributes = [], bool $force = false) + * @method static \CraftCms\Cms\Element\Contracts\ElementInterface revertToRevision(\CraftCms\Cms\Element\Contracts\ElementInterface $revision, int $creatorId) * * @see \CraftCms\Cms\Element\Revisions */ diff --git a/src/Support/Facades/Search.php b/src/Support/Facades/Search.php index 1a4b536781c..5c0a1a3c17d 100644 --- a/src/Support/Facades/Search.php +++ b/src/Support/Facades/Search.php @@ -8,9 +8,9 @@ use Override; /** - * @method static bool indexElementAttributes(\craft\base\ElementInterface $element, array|null $fieldHandles = null) - * @method static void queueIndexElement(\craft\base\ElementInterface $element, string[] $fieldHandles) - * @method static void indexElementIfQueued(int $elementId, int $siteId, class-string<\craft\base\ElementInterface>|null $elementType = null) + * @method static bool indexElementAttributes(\CraftCms\Cms\Element\Contracts\ElementInterface $element, array|null $fieldHandles = null) + * @method static void queueIndexElement(\CraftCms\Cms\Element\Contracts\ElementInterface $element, string[] $fieldHandles) + * @method static void indexElementIfQueued(int $elementId, int $siteId, class-string<\CraftCms\Cms\Element\Contracts\ElementInterface>|null $elementType = null) * @method static bool shouldCallSearchElements(\CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface $elementQuery) * @method static array searchElements(\CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface $elementQuery) * @method static \Illuminate\Database\Query\Builder|false createDbQuery(\CraftCms\Cms\Search\SearchQuery|array|string $searchQuery, \CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface $elementQuery) diff --git a/src/Support/Facades/Structures.php b/src/Support/Facades/Structures.php index a67bb13159b..e77dada3c56 100644 --- a/src/Support/Facades/Structures.php +++ b/src/Support/Facades/Structures.php @@ -9,18 +9,18 @@ /** * @method static \CraftCms\Cms\Structure\Data\Structure|null getStructureById(int $structureId, bool $withTrashed = false) * @method static \CraftCms\Cms\Structure\Data\Structure|null getStructureByUid(string $structureUid, bool $withTrashed = false) - * @method static void fillGapsInElements(\craft\base\ElementInterface[] $elements) - * @method static void applyBranchLimitToElements(\craft\base\ElementInterface[] $elements, int $branchLimit) + * @method static void fillGapsInElements(\CraftCms\Cms\Element\Contracts\ElementInterface[] $elements) + * @method static void applyBranchLimitToElements(\CraftCms\Cms\Element\Contracts\ElementInterface[] $elements, int $branchLimit) * @method static bool saveStructure(\CraftCms\Cms\Structure\Data\Structure $structure) * @method static bool deleteStructureById(int $structureId) - * @method static int getElementLevelDelta(int $structureId, \craft\base\ElementInterface $element) - * @method static bool prepend(int $structureId, \craft\base\ElementInterface $element, \craft\base\ElementInterface|int $parentElement, \CraftCms\Cms\Structure\Enums\Mode $mode = 'auto') - * @method static bool append(int $structureId, \craft\base\ElementInterface $element, \craft\base\ElementInterface|int $parentElement, \CraftCms\Cms\Structure\Enums\Mode $mode = 'auto') - * @method static bool prependToRoot(int $structureId, \craft\base\ElementInterface $element, \CraftCms\Cms\Structure\Enums\Mode $mode = 'auto') - * @method static bool appendToRoot(int $structureId, \craft\base\ElementInterface $element, \CraftCms\Cms\Structure\Enums\Mode $mode = 'auto') - * @method static bool moveBefore(int $structureId, \craft\base\ElementInterface $element, \craft\base\ElementInterface|int $nextElement, \CraftCms\Cms\Structure\Enums\Mode $mode = 'auto') - * @method static bool moveAfter(int $structureId, \craft\base\ElementInterface $element, \craft\base\ElementInterface|int $prevElement, \CraftCms\Cms\Structure\Enums\Mode $mode = 'auto') - * @method static bool remove(int $structureId, \craft\base\ElementInterface $element) + * @method static int getElementLevelDelta(int $structureId, \CraftCms\Cms\Element\Contracts\ElementInterface $element) + * @method static bool prepend(int $structureId, \CraftCms\Cms\Element\Contracts\ElementInterface $element, \CraftCms\Cms\Element\Contracts\ElementInterface|int $parentElement, \CraftCms\Cms\Structure\Enums\Mode $mode = 'auto') + * @method static bool append(int $structureId, \CraftCms\Cms\Element\Contracts\ElementInterface $element, \CraftCms\Cms\Element\Contracts\ElementInterface|int $parentElement, \CraftCms\Cms\Structure\Enums\Mode $mode = 'auto') + * @method static bool prependToRoot(int $structureId, \CraftCms\Cms\Element\Contracts\ElementInterface $element, \CraftCms\Cms\Structure\Enums\Mode $mode = 'auto') + * @method static bool appendToRoot(int $structureId, \CraftCms\Cms\Element\Contracts\ElementInterface $element, \CraftCms\Cms\Structure\Enums\Mode $mode = 'auto') + * @method static bool moveBefore(int $structureId, \CraftCms\Cms\Element\Contracts\ElementInterface $element, \CraftCms\Cms\Element\Contracts\ElementInterface|int $nextElement, \CraftCms\Cms\Structure\Enums\Mode $mode = 'auto') + * @method static bool moveAfter(int $structureId, \CraftCms\Cms\Element\Contracts\ElementInterface $element, \CraftCms\Cms\Element\Contracts\ElementInterface|int $prevElement, \CraftCms\Cms\Structure\Enums\Mode $mode = 'auto') + * @method static bool remove(int $structureId, \CraftCms\Cms\Element\Contracts\ElementInterface $element) * * @see \CraftCms\Cms\Structure\Structures */ diff --git a/src/Support/Flash.php b/src/Support/Flash.php index 323dd42632b..261decb8a34 100644 --- a/src/Support/Flash.php +++ b/src/Support/Flash.php @@ -12,31 +12,39 @@ public static function success(?string $default = null, array $settings = []): v { $message = request('successMessage', $default); - if ($message !== null) { - if (request()->isCpRequest()) { - session()->flash('cp-notification-success', [$message, $settings + [ - 'icon' => 'check', - 'iconLabel' => t('Success'), - ]]); - } else { - session()->flash('success', $message); - } + if (is_null($message)) { + return; } + + if (! request()->isCpRequest()) { + session()->flash('success', $message); + + return; + } + + session()->flash('cp-notification-success', [$message, $settings + [ + 'icon' => 'check', + 'iconLabel' => t('Success'), + ]]); } public static function fail(?string $default = null, array $settings = []): void { $message = request('failMessage', $default); - if ($message !== null) { - if (request()->isCpRequest()) { - session()->flash('cp-notification-error', [$message, $settings + [ - 'icon' => 'alert', - 'iconLabel' => t('Error'), - ]]); - } else { - session()->flash('error', $message); - } + if ($message === null) { + return; } + + if (! request()->isCpRequest()) { + session()->flash('error', $message); + + return; + } + + session()->flash('cp-notification-error', [$message, $settings + [ + 'icon' => 'alert', + 'iconLabel' => t('Error'), + ]]); } } diff --git a/src/Support/Html.php b/src/Support/Html.php index 7da09780bdc..fc776209771 100644 --- a/src/Support/Html.php +++ b/src/Support/Html.php @@ -20,10 +20,10 @@ use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Log; use InvalidArgumentException; +use RuntimeException; use Stringable; use Symfony\Component\DomCrawler\Crawler; use Throwable; -use yii\base\InvalidConfigException; use Yiisoft\Html\Html as YiiHtml; use Yiisoft\Html\NoEncode; use Yiisoft\Html\Tag\Button; @@ -224,7 +224,7 @@ public static function actionInput(string $route, array $options = []): string * @return string The generated hidden input tag * * @throws \yii\base\Exception if the validation key could not be written - * @throws InvalidConfigException when HMAC generation fails + * @throws RuntimeException when HMAC generation fails */ public static function redirectInput(string $url, array $options = []): string { @@ -242,7 +242,7 @@ public static function redirectInput(string $url, array $options = []): string * @return string The generated hidden input tag * * @throws Exception if the validation key could not be written - * @throws InvalidConfigException when HMAC generation fails + * @throws RuntimeException when HMAC generation fails */ public static function failMessageInput(string $message, array $options = []): string { @@ -260,7 +260,7 @@ public static function failMessageInput(string $message, array $options = []): s * @return string The generated hidden input tag * * @throws Exception if the validation key could not be written - * @throws InvalidConfigException when HMAC generation fails + * @throws RuntimeException when HMAC generation fails */ public static function successMessageInput(string $message, array $options = []): string { diff --git a/src/Support/Str.php b/src/Support/Str.php index 9d5c4e9cddb..7c27e15c15c 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -13,9 +13,9 @@ use Override; use Ramsey\Uuid\Validator\GenericValidator; use ReflectionClass; +use RuntimeException; use voku\helper\ASCII; use yii\base\Exception; -use yii\base\InvalidConfigException; class Str extends \Illuminate\Support\Str { @@ -136,7 +136,7 @@ public static function convertLineBreaks(string $str): string * * @param string $str The string. * - * @throws InvalidConfigException on OpenSSL not loaded + * @throws RuntimeException on OpenSSL not loaded * @throws Exception on OpenSSL error */ public static function decdec(string $str): string @@ -175,7 +175,7 @@ public static function emojiToShortcodes(string $str): string * * @param string $str the string * - * @throws InvalidConfigException on OpenSSL not loaded + * @throws RuntimeException on OpenSSL not loaded * @throws Exception on OpenSSL error * * @see decdec() diff --git a/src/Support/Template.php b/src/Support/Template.php index 920855ab344..69980c86991 100644 --- a/src/Support/Template.php +++ b/src/Support/Template.php @@ -4,8 +4,8 @@ namespace CraftCms\Cms\Support; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Shared\BaseModel; use CraftCms\Cms\Support\Facades\ElementCaches; use CraftCms\Cms\Support\Facades\Entries; @@ -14,6 +14,7 @@ use CraftCms\Cms\Twig\Variables\Paginate; use CraftCms\Cms\View\Enums\Position; use Illuminate\Database\Query\Builder; +use RuntimeException; use Stringable; use Twig\Environment; use Twig\Error\RuntimeError; @@ -24,7 +25,6 @@ use Twig\Template as TwigTemplate; use Twig\TemplateWrapper; use yii\base\BaseObject; -use yii\base\InvalidConfigException; use yii\base\UnknownPropertyException; class Template @@ -145,7 +145,7 @@ public static function html(string $html, int|Position $position = Position::Bod HtmlStack::html($html, $position); } - /** @throws InvalidConfigException */ + /** @throws RuntimeException */ public static function js(string $js, array $options = [], ?string $key = null): void { if (preg_match('/^[^\r\n]+\.js(\.gz)?$/i', $js) || Url::isAbsoluteUrl($js)) { diff --git a/src/Twig/Extensions/ArrayTwigExtension.php b/src/Twig/Extensions/ArrayTwigExtension.php index e51c7c0fc15..800dcba6053 100644 --- a/src/Twig/Extensions/ArrayTwigExtension.php +++ b/src/Twig/Extensions/ArrayTwigExtension.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Twig\Extensions; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Support\Arr; use Illuminate\Support\Collection; diff --git a/src/Twig/Extensions/CoreTwigExtension.php b/src/Twig/Extensions/CoreTwigExtension.php index c084788563b..e3ac4803db6 100644 --- a/src/Twig/Extensions/CoreTwigExtension.php +++ b/src/Twig/Extensions/CoreTwigExtension.php @@ -6,11 +6,11 @@ use CommerceGuys\Addressing\Formatter\FormatterInterface; use Craft; -use craft\base\ElementInterface; use CraftCms\Aliases\Aliases; use CraftCms\Cms\Address\Addresses; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Component\Contracts\MissingComponentInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Queries\AddressQuery; use CraftCms\Cms\Element\Queries\AssetQuery; diff --git a/src/Twig/Extensions/HtmlTwigExtension.php b/src/Twig/Extensions/HtmlTwigExtension.php index c3069652053..2bee49bd0fb 100644 --- a/src/Twig/Extensions/HtmlTwigExtension.php +++ b/src/Twig/Extensions/HtmlTwigExtension.php @@ -17,11 +17,11 @@ use Illuminate\Support\Facades\Log; use InvalidArgumentException; use Override; +use RuntimeException; use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; -use yii\base\InvalidConfigException; class HtmlTwigExtension extends AbstractExtension { @@ -170,7 +170,7 @@ public function markdownFilter( } /** - * @throws InvalidConfigException + * @throws RuntimeException * @throws AssetException */ public function dataUrlFunction(Asset|string $file, ?string $mimeType = null): string diff --git a/src/User/Commands/CreateCommand.php b/src/User/Commands/CreateCommand.php index a6309811cc5..314aea53278 100644 --- a/src/User/Commands/CreateCommand.php +++ b/src/User/Commands/CreateCommand.php @@ -64,7 +64,7 @@ public function handle(Elements $elements, GeneralConfig $generalConfig, Users $ if (! empty($attributes) && ! $user->validate(array_keys($attributes))) { $this->error('Invalid arguments:'); - $this->error(implode(PHP_EOL, $user->getErrorSummary(true))); + $this->error(implode(PHP_EOL, $user->errors()->all())); return self::FAILURE; } @@ -126,7 +126,7 @@ public function handle(Elements $elements, GeneralConfig $generalConfig, Users $ if ($failed) { $this->components->error('Failed to save the user.'); - $this->components->error(implode(PHP_EOL, $user->getErrorSummary(true))); + $this->components->error(implode(PHP_EOL, $user->errors()->all())); return self::FAILURE; } diff --git a/src/User/Conditions/AdminConditionRule.php b/src/User/Conditions/AdminConditionRule.php index b306589ba39..204f045a264 100644 --- a/src/User/Conditions/AdminConditionRule.php +++ b/src/User/Conditions/AdminConditionRule.php @@ -4,11 +4,11 @@ namespace CraftCms\Cms\User\Conditions; -use craft\base\ElementInterface; use craft\elements\db\UserQuery; use CraftCms\Cms\Condition\BaseLightswitchConditionRule; use CraftCms\Cms\Edition; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\User\Elements\User; diff --git a/src/User/Conditions/AffiliatedSiteConditionRule.php b/src/User/Conditions/AffiliatedSiteConditionRule.php index 91a4e8ec443..a600d0938eb 100644 --- a/src/User/Conditions/AffiliatedSiteConditionRule.php +++ b/src/User/Conditions/AffiliatedSiteConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\User\Conditions; -use craft\base\ElementInterface; use craft\elements\db\UserQuery; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Site\Data\Site; use CraftCms\Cms\Support\Facades\Sites; diff --git a/src/User/Conditions/CredentialedConditionRule.php b/src/User/Conditions/CredentialedConditionRule.php index 88a75254484..53d9b4b1a2c 100644 --- a/src/User/Conditions/CredentialedConditionRule.php +++ b/src/User/Conditions/CredentialedConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\User\Conditions; -use craft\base\ElementInterface; use craft\elements\db\UserQuery; use CraftCms\Cms\Condition\BaseLightswitchConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\User\Elements\User; diff --git a/src/User/Conditions/EmailConditionRule.php b/src/User/Conditions/EmailConditionRule.php index eb4e3ddc958..bb1f057f432 100644 --- a/src/User/Conditions/EmailConditionRule.php +++ b/src/User/Conditions/EmailConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\User\Conditions; -use craft\base\ElementInterface; use craft\elements\db\UserQuery; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\User\Elements\User; diff --git a/src/User/Conditions/FirstNameConditionRule.php b/src/User/Conditions/FirstNameConditionRule.php index 0dae8b0b03b..23ec1664892 100644 --- a/src/User/Conditions/FirstNameConditionRule.php +++ b/src/User/Conditions/FirstNameConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\User\Conditions; -use craft\base\ElementInterface; use craft\elements\db\UserQuery; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\User\Elements\User; diff --git a/src/User/Conditions/GroupConditionRule.php b/src/User/Conditions/GroupConditionRule.php index 9fe66804a76..ec6bb6dd811 100644 --- a/src/User/Conditions/GroupConditionRule.php +++ b/src/User/Conditions/GroupConditionRule.php @@ -4,15 +4,15 @@ namespace CraftCms\Cms\User\Conditions; -use craft\base\ElementInterface; use craft\elements\db\UserQuery; use CraftCms\Cms\Condition\BaseMultiSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\UserGroups; use CraftCms\Cms\User\Elements\User; -use yii\base\InvalidConfigException; +use RuntimeException; use function CraftCms\Cms\t; @@ -46,7 +46,7 @@ public function modifyQuery(ElementQueryInterface $query): void } /** - * @throws InvalidConfigException + * @throws RuntimeException */ public function matchElement(ElementInterface $element): bool { diff --git a/src/User/Conditions/LastLoginDateConditionRule.php b/src/User/Conditions/LastLoginDateConditionRule.php index e0a8df63b6f..5cbb9896b6a 100644 --- a/src/User/Conditions/LastLoginDateConditionRule.php +++ b/src/User/Conditions/LastLoginDateConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\User\Conditions; -use craft\base\ElementInterface; use craft\elements\db\UserQuery; use CraftCms\Cms\Condition\BaseDateRangeConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\User\Elements\User; diff --git a/src/User/Conditions/LastNameConditionRule.php b/src/User/Conditions/LastNameConditionRule.php index c8eca4e5e3d..4dd3fa12e45 100644 --- a/src/User/Conditions/LastNameConditionRule.php +++ b/src/User/Conditions/LastNameConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\User\Conditions; -use craft\base\ElementInterface; use craft\elements\db\UserQuery; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\User\Elements\User; diff --git a/src/User/Conditions/UsernameConditionRule.php b/src/User/Conditions/UsernameConditionRule.php index 1a559ad34d5..1f5304629bc 100644 --- a/src/User/Conditions/UsernameConditionRule.php +++ b/src/User/Conditions/UsernameConditionRule.php @@ -4,10 +4,10 @@ namespace CraftCms\Cms\User\Conditions; -use craft\base\ElementInterface; use craft\elements\db\UserQuery; use CraftCms\Cms\Condition\BaseTextConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\User\Elements\User; diff --git a/src/User/Elements/User.php b/src/User/Elements/User.php index e7f3d1ce37a..40e41b188f1 100644 --- a/src/User/Elements/User.php +++ b/src/User/Elements/User.php @@ -4,8 +4,6 @@ namespace CraftCms\Cms\User\Elements; -use craft\base\ElementInterface; -use craft\elements\conditions\users\UserCondition; use CraftCms\Cms\Address\Elements\Address; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Auth\Concerns\ConfirmsPasswords; @@ -16,7 +14,7 @@ use CraftCms\Cms\Database\Table; use CraftCms\Cms\Edition; use CraftCms\Cms\Element\Actions\Restore; -use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Data\EagerLoadPlan; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementCollection; @@ -29,7 +27,6 @@ use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Field\Fields; use CraftCms\Cms\FieldLayout\FieldLayout; -use CraftCms\Cms\ProjectConfig\ProjectConfig; use CraftCms\Cms\Shared\Concerns\HasNames; use CraftCms\Cms\Shared\Enums\Color; use CraftCms\Cms\Site\Data\Site; @@ -41,6 +38,7 @@ use CraftCms\Cms\Support\Facades\HtmlStack; use CraftCms\Cms\Support\Facades\I18N; use CraftCms\Cms\Support\Facades\InputNamespace; +use CraftCms\Cms\Support\Facades\ProjectConfig; use CraftCms\Cms\Support\Facades\Sites; use CraftCms\Cms\Support\Facades\UserGroups; use CraftCms\Cms\Support\Facades\Users; @@ -53,6 +51,7 @@ use CraftCms\Cms\User\Actions\DeleteUsers; use CraftCms\Cms\User\Actions\SuspendUsers; use CraftCms\Cms\User\Actions\UnsuspendUsers; +use CraftCms\Cms\User\Conditions\UserCondition; use CraftCms\Cms\User\Data\UserGroup; use CraftCms\Cms\User\Events\DefineFriendlyName; use CraftCms\Cms\User\Events\DefineName; @@ -64,7 +63,7 @@ use DateInterval; use DateTime; use DateTimeZone; -use Deprecated; +use Exception; use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Auth\Passwords\PasswordBroker; @@ -74,7 +73,6 @@ use Illuminate\Contracts\Auth\MustVerifyEmail as MustVerifyEmailContract; use Illuminate\Foundation\Auth\Access\Authorizable; use Illuminate\Notifications\Notifiable; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\DB as DbFacade; @@ -85,9 +83,6 @@ use Override; use Stringable; use Throwable; -use yii\base\Exception; -use yii\base\InvalidConfigException; -use yii\web\BadRequestHttpException; use function CraftCms\Cms\t; @@ -118,9 +113,6 @@ class User extends Element implements AuthenticatableContract, AuthorizableContr use Macroable; use Notifiable; - /** - * @since 5.0.0 - */ public const string GQL_TYPE_NAME = 'User'; private static array $photoColors = [ @@ -146,9 +138,6 @@ class User extends Element implements AuthenticatableContract, AuthorizableContr // User statuses // ------------------------------------------------------------------------- - /** - * @since 4.0.0 - */ public const string STATUS_INACTIVE = 'inactive'; public const string STATUS_ACTIVE = 'active'; @@ -159,6 +148,203 @@ class User extends Element implements AuthenticatableContract, AuthorizableContr public const string STATUS_LOCKED = 'locked'; + /** + * @var int|null Photo asset ID + */ + #[AllowedInSandbox] + public ?int $photoId = null; + + /** + * @var bool Active + */ + #[AllowedInSandbox] + public bool $active = false; + + /** + * @var bool Pending + */ + #[AllowedInSandbox] + public bool $pending = false; + + /** + * @var bool Locked + */ + #[AllowedInSandbox] + public bool $locked = false; + + /** + * @var bool Suspended + */ + #[AllowedInSandbox] + public bool $suspended = false; + + /** + * @var bool Admin + */ + #[AllowedInSandbox] + public bool $admin = false; + + /** + * @var string|null Username + */ + #[AllowedInSandbox] + public ?string $username = null; + + /** + * @var string|null Email + */ + #[AllowedInSandbox] + public ?string $email = null; + + /** + * @var string|null Password + */ + public ?string $password = null; + + /** + * @var int|null Affiliated site ID + */ + #[AllowedInSandbox] + public ?int $affiliatedSiteId = null; + + /** + * @var DateTime|null Last login date + */ + #[AllowedInSandbox] + public ?DateTime $lastLoginDate = null; + + /** + * @var int|null Invalid login count + */ + public ?int $invalidLoginCount = null; + + /** + * @var DateTime|null Last invalid login date + */ + public ?DateTime $lastInvalidLoginDate = null; + + /** + * @var DateTime|null Lockout date + */ + public ?DateTime $lockoutDate = null; + + /** + * @var bool Whether the user has a dashboard + */ + public bool $hasDashboard = false; + + /** + * @var bool Password reset required + */ + public bool $passwordResetRequired = false; + + /** + * @var DateTime|null Last password change date + */ + public ?DateTime $lastPasswordChangeDate = null; + + /** + * @var string|null Unverified email + */ + public ?string $unverifiedEmail = null; + + /** + * @var string|null New password + */ + public ?string $newPassword = null; + + /** + * @var string|null Current password + */ + public ?string $currentPassword = null; + + /** + * @var string|null Last login attempt IP address. + */ + public ?string $lastLoginAttemptIp = null; + + /** + * @var string|null Session remember token + */ + public ?string $remember_token = null; + + /** + * @var self|null The user who should take over the user’s content if the user is deleted. + */ + public ?User $inheritorOnDelete = null; + + /** + * @var ElementCollection
Addresses + * + * @see getAddresses() + */ + private ElementCollection $_addresses; + + /** + * @see getAddressManager() + */ + private NestedElementManager $_addressManager; + + /** + * @see getName() + * @see setName() + */ + private ?string $_name = null; + + /** + * @see getFriendlyName() + * @see setFriendlyName() + */ + private string|bool|null $_friendlyName = null; + + /** + * @var Asset|false|null user photo + */ + private Asset|null|false $_photo = null; + + /** + * @var UserGroup[]|null The cached list of groups the user belongs to. Set by [[getGroups()]]. + */ + private ?array $_groups = null; + + /** + * @see setAttributesFromRequest() + * @see afterSave() + */ + private bool $sendVerificationEmailAfterRequest = false; + + public function __construct($config = []) + { + parent::__construct($config); + + // Is this user in cooldown mode, and are they past their window? + if ( + $this->locked && + Cms::config()->cooldownDuration && + ! $this->getRemainingCooldownTime() + ) { + Users::unlockUser($this); + } + + // Convert IDNA ASCII to Unicode + if ($this->username) { + $this->username = Str::idnToUtf8Email($this->username); + } + if ($this->email) { + $this->email = Str::idnToUtf8Email($this->email); + } + + if (empty($this->username) && Cms::config()->useEmailAsUsername) { + $this->username = $this->email; + } + + if ($this->password === '') { + $this->password = null; + } + + $this->normalizeNames(); + } + public function getAuthIdentifierName(): string { return 'id'; @@ -247,11 +433,8 @@ public static function find(): UserQuery return new UserQuery; } - /** - * @return UserCondition - */ #[Override] - public static function createCondition(): ElementConditionInterface + public static function createCondition(): UserCondition { return new UserCondition(self::class); } @@ -333,25 +516,11 @@ protected static function defineSources(string $context): array #[Override] protected static function defineActions(string $source): array { - $actions = []; - - if (Gate::check('moderateUsers')) { - // Suspend - $actions[] = SuspendUsers::class; - - // Unsuspend - $actions[] = UnsuspendUsers::class; - } - - if (Gate::check('deleteUsers')) { - // Delete - $actions[] = DeleteUsers::class; - } - - // Restore - $actions[] = Restore::class; - - return $actions; + return collect() + ->when(Gate::check('moderateUsers'), fn ($actions) => $actions->push(SuspendUsers::class, UnsuspendUsers::class)) + ->when(Gate::check('deleteUsers'), fn ($actions) => $actions->push(DeleteUsers::class)) + ->push(Restore::class) + ->all(); } #[Override] @@ -561,177 +730,6 @@ public static function eagerLoadingMap(array $sourceElements, string $handle): a return parent::eagerLoadingMap($sourceElements, $handle); } - /** - * @var int|null Photo asset ID - */ - #[AllowedInSandbox] - public ?int $photoId = null; - - /** - * @var bool Active - * - * @since 4.0.0 - */ - #[AllowedInSandbox] - public bool $active = false; - - /** - * @var bool Pending - */ - #[AllowedInSandbox] - public bool $pending = false; - - /** - * @var bool Locked - */ - #[AllowedInSandbox] - public bool $locked = false; - - /** - * @var bool Suspended - */ - #[AllowedInSandbox] - public bool $suspended = false; - - /** - * @var bool Admin - */ - #[AllowedInSandbox] - public bool $admin = false; - - /** - * @var string|null Username - */ - #[AllowedInSandbox] - public ?string $username = null; - - /** - * @var string|null Email - */ - #[AllowedInSandbox] - public ?string $email = null; - - /** - * @var string|null Password - */ - public ?string $password = null; - - /** - * @var int|null Affiliated site ID - * - * @since 5.6.0 - */ - #[AllowedInSandbox] - public ?int $affiliatedSiteId = null; - - /** - * @var DateTime|null Last login date - */ - #[AllowedInSandbox] - public ?DateTime $lastLoginDate = null; - - /** - * @var int|null Invalid login count - */ - public ?int $invalidLoginCount = null; - - /** - * @var DateTime|null Last invalid login date - */ - public ?DateTime $lastInvalidLoginDate = null; - - /** - * @var DateTime|null Lockout date - */ - public ?DateTime $lockoutDate = null; - - /** - * @var bool Whether the user has a dashboard - * - * @since 3.0.4 - */ - public bool $hasDashboard = false; - - /** - * @var bool Password reset required - */ - public bool $passwordResetRequired = false; - - /** - * @var DateTime|null Last password change date - */ - public ?DateTime $lastPasswordChangeDate = null; - - /** - * @var string|null Unverified email - */ - public ?string $unverifiedEmail = null; - - /** - * @var string|null New password - */ - public ?string $newPassword = null; - - /** - * @var string|null Current password - */ - public ?string $currentPassword = null; - - /** - * @var string|null Last login attempt IP address. - */ - public ?string $lastLoginAttemptIp = null; - - /** - * @var string|null Session remember token - */ - public ?string $remember_token = null; - - /** - * @var self|null The user who should take over the user’s content if the user is deleted. - */ - public ?User $inheritorOnDelete = null; - - /** - * @var ElementCollection
Addresses - * - * @see getAddresses() - */ - private ElementCollection $_addresses; - - /** - * @see getAddressManager() - */ - private NestedElementManager $_addressManager; - - /** - * @see getName() - * @see setName() - */ - private ?string $_name = null; - - /** - * @see getFriendlyName() - * @see setFriendlyName() - */ - private string|bool|null $_friendlyName = null; - - /** - * @var Asset|false|null user photo - */ - private Asset|null|false $_photo = null; - - /** - * @var UserGroup[]|null The cached list of groups the user belongs to. Set by [[getGroups()]]. - */ - private ?array $_groups = null; - - /** - * @see setAttributesFromRequest() - * @see afterSave() - */ - private bool $sendVerificationEmailAfterRequest = false; - public function sendPasswordResetNotification($token): void { $this->notify(new ResetPasswordNotification($token)); @@ -777,39 +775,6 @@ public function getEmailForVerification(): string return $this->unverifiedEmail ?? $this->email; } - #[Override] - public function init(): void - { - parent::init(); - - // Is this user in cooldown mode, and are they past their window? - if ( - $this->locked && - Cms::config()->cooldownDuration && - ! $this->getRemainingCooldownTime() - ) { - Users::unlockUser($this); - } - - // Convert IDNA ASCII to Unicode - if ($this->username) { - $this->username = Str::idnToUtf8Email($this->username); - } - if ($this->email) { - $this->email = Str::idnToUtf8Email($this->email); - } - - if (empty($this->username) && Cms::config()->useEmailAsUsername) { - $this->username = $this->email; - } - - if ($this->password === '') { - $this->password = null; - } - - $this->normalizeNames(); - } - /** * Use the full name or username as the string representation. */ @@ -817,6 +782,7 @@ public function init(): void public function __toString(): string { $name = $this->getName(); + if ($name !== '') { return $name; } @@ -921,7 +887,7 @@ public function setAttributesFromRequest($values): void if (isset($values['email'])) { // make sure they have an elevated session if (! $this->isPasswordConfirmed()) { - throw new BadRequestHttpException(t('An elevated session is required to change a user’s email.')); + abort(400, t('An elevated session is required to change a user’s email.')); } if ($this->email !== null) { @@ -929,7 +895,7 @@ public function setAttributesFromRequest($values): void if ($this->getIsCurrent() || Gate::check('administrateUsers')) { if ( Edition::get()->value >= Edition::Pro->value && - app(ProjectConfig::class)->get('users.requireEmailVerification') && + ProjectConfig::get('users.requireEmailVerification') && ! Gate::check('administrateUsers') ) { // set it as the unverified email instead, and @@ -946,7 +912,7 @@ public function setAttributesFromRequest($values): void } #[Override] - public function setAttributes($values, $safeOnly = true): void + public function setAttributes($values): void { if (array_key_exists('firstName', $values) || array_key_exists('lastName', $values)) { // Unset fullName so NameTrait::prepareNamesForSave() can set it @@ -956,13 +922,11 @@ public function setAttributes($values, $safeOnly = true): void $this->firstName = $this->lastName = null; } - parent::setAttributes($values, $safeOnly); + parent::setAttributes($values); } /** * Returns whether the user account can be logged into. - * - * @since 4.0.0 */ public function getIsCredentialed(): bool { @@ -971,8 +935,6 @@ public function getIsCredentialed(): bool /** * Returns whether the user has a password. - * - * @since 5.6.0 */ #[AllowedInSandbox] public function getHasPassword(): bool @@ -988,8 +950,6 @@ public function getHasPassword(): bool /** * Returns whether the user has an associated SSO identity. - * - * @since 5.7.8 */ #[AllowedInSandbox] public function getHasSsoIdentity(): bool @@ -1004,7 +964,6 @@ public function getHasSsoIdentity(): bool #[Override] public function getFieldLayout(): ?FieldLayout { - // @TODO: Field layout for non-legacy return app(Fields::class)->getLayoutByType(User::class); } @@ -1012,51 +971,43 @@ public function getFieldLayout(): ?FieldLayout * Gets the user’s addresses. * * @return ElementCollection
- * - * @since 4.0.0 */ #[AllowedInSandbox] public function getAddresses(): ElementCollection { - if (! isset($this->_addresses)) { - if (! $this->id) { - /** @var ElementCollection
*/ - return ElementCollection::make(); - } + if (isset($this->_addresses)) { + return $this->_addresses; + } - $this->_addresses = $this->createAddressQuery() - ->whereNull('fieldId') - ->get(); + if (! $this->id) { + return new ElementCollection; } - return $this->_addresses; + return $this->_addresses = $this->createAddressQuery() + ->whereNull('fieldId') + ->get(); } /** * Returns a nested element manager for the user’s addresses. - * - * @since 5.0.0 */ public function getAddressManager(): NestedElementManager { - if (! isset($this->_addressManager)) { - $this->_addressManager = new NestedElementManager( - Address::class, - fn () => $this->createAddressQuery(), - [ - 'attribute' => 'addresses', - 'propagationMethod' => PropagationMethod::None, - ], - ); - } - - return $this->_addressManager; + return $this->_addressManager ??= new NestedElementManager( + Address::class, + fn () => $this->createAddressQuery(), + [ + 'attribute' => 'addresses', + 'propagationMethod' => PropagationMethod::None, + ], + ); } #[Override] public function afterRestore(): void { $this->getAddressManager()->restoreNestedElements($this); + parent::afterRestore(); } @@ -1064,7 +1015,7 @@ private function createAddressQuery(): AddressQuery { return Address::find() ->owner($this) - ->orderBy(['id' => SORT_ASC]); + ->orderBy('id'); } /** @@ -1097,11 +1048,11 @@ public function getGroups(): array /** * Sets an array of user groups on the user. * - * @param UserGroup[]|UserGroup[] $groups An array of UserGroup objects. + * @param UserGroup[] $groups An array of UserGroup objects. */ public function setGroups(array $groups): void { - if (Edition::get()->value >= Edition::Pro->value) { + if (Edition::isAtLeast(Edition::Pro)) { $this->_groups = $groups; } } @@ -1123,11 +1074,11 @@ public function isInGroup(UserGroup|int|string $group): bool } if (is_numeric($group)) { - return Collection::make($this->getGroups())->contains('id', $group); + return collect($this->getGroups())->contains('id', $group); } /** @phpstan-ignore argument.type */ - return Collection::make($this->getGroups())->containsStrict('handle', $group); + return collect($this->getGroups())->containsStrict('handle', $group); } /** @@ -1138,8 +1089,6 @@ public function isInGroup(UserGroup|int|string $group): bool * * @param array $groups The user groups, handles, or IDs * @param bool $all Whether to only return `true` if the user is in *all* of the provided groups - * - * @since 5.9.0 */ #[AllowedInSandbox] public function isInGroups(array $groups, bool $all = false): bool @@ -1151,27 +1100,13 @@ public function isInGroups(array $groups, bool $all = false): bool return array_all($groups, fn ($group) => $this->isInGroup($group)); } - /** - * Returns the user’s full name. - */ - #[Deprecated(message: 'in 4.0.0. [[fullName]] should be used instead.')] - #[AllowedInSandbox] - public function getFullName(): ?string - { - return $this->fullName; - } - /** * Returns the user’s full name or username. */ #[AllowedInSandbox] public function getName(): string { - if (! isset($this->_name)) { - $this->_name = $this->_defineName(); - } - - return $this->_name; + return $this->_name ??= $this->_defineName(); } private function _defineName(): string @@ -1183,8 +1118,6 @@ private function _defineName(): string /** * Sets the user’s name. - * - * @since 3.7.0 */ public function setName(string $name): void { @@ -1213,8 +1146,6 @@ private function _defineFriendlyName(): ?string /** * Sets the user’s friendly name. - * - * @since 3.7.0 */ public function setFriendlyName(string $friendlyName): void { @@ -1223,8 +1154,6 @@ public function setFriendlyName(string $friendlyName): void /** * Returns the user’s affiliated site, if they have one. - * - * @since 5.6.0 */ #[AllowedInSandbox] public function getAffiliatedSite(): ?Site @@ -1237,38 +1166,24 @@ public function getAffiliatedSite(): ?Site } #[Override] - public function getStatus(): ?string + public function getStatus(): string { // If they're disabled or archived, go with that $status = parent::getStatus(); - if ($status !== self::STATUS_ENABLED) { - return $status; - } - - if ($this->suspended) { - return self::STATUS_SUSPENDED; - } - - if ($this->archived) { - return self::STATUS_ARCHIVED; - } - - if ($this->pending) { - return self::STATUS_PENDING; - } - if ($this->active) { - return self::STATUS_ACTIVE; - } - - return self::STATUS_INACTIVE; + return match (true) { + $status !== self::STATUS_ENABLED => $status, + $this->suspended => self::STATUS_SUSPENDED, + $this->archived => self::STATUS_ARCHIVED, + $this->pending => self::STATUS_PENDING, + $this->active => self::STATUS_ACTIVE, + default => self::STATUS_INACTIVE, + }; } protected function thumbUrl(int $size): ?string { - $photo = $this->getPhoto(); - - if ($photo) { + if ($photo = $this->getPhoto()) { return AssetsService::getThumbUrl($photo, $size, iconFallback: false); } @@ -1348,24 +1263,18 @@ public function isAdmin(): bool /** * Returns whether the user can register additional users. - * - * @since 5.0.0 */ final public function canRegisterUsers(): bool { - return - $this->can('registerUsers') && - Users::canCreateUsers(); + return $this->can('registerUsers') && Users::canCreateUsers(); } /** * Returns whether the user is authorized to assign any user groups to users. - * - * @since 4.0.0 */ public function canAssignUserGroups(): bool { - if (Edition::get()->value < Edition::Pro->value) { + if (! Edition::isAtLeast(Edition::Pro)) { return false; } @@ -1799,8 +1708,6 @@ public function getPreferredLanguage(): ?string * If the user doesn’t have a preferred locale, their preferred language will be used instead. * * @return string|null The preferred locale - * - * @since 3.5.0 */ public function getPreferredLocale(): ?string { @@ -1809,8 +1716,6 @@ public function getPreferredLocale(): ?string /** * Returns whether the user prefers to have form fields autofocused on page load. - * - * @since 5.0.0 */ public function getAutofocusPreferred(): bool { @@ -2002,9 +1907,6 @@ protected function metadata(): array ]; } - /** - * @since 3.3.0 - */ #[Override] public function getGqlTypeName(): string { @@ -2031,10 +1933,6 @@ final public function beforeSave(bool $isNew): bool return parent::beforeSave($isNew); } - /** - * @throws InvalidConfigException - * @throws Exception - */ #[Override] public function afterSave(bool $isNew): void { diff --git a/src/User/Models/User.php b/src/User/Models/User.php index 69ff2cfce4c..4389aab5e85 100644 --- a/src/User/Models/User.php +++ b/src/User/Models/User.php @@ -13,6 +13,7 @@ use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\UserGroups; use Illuminate\Auth\MustVerifyEmail; +use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -22,6 +23,10 @@ use Illuminate\Support\Facades\DB; use Override; +#[Hidden([ + 'password', + 'rememberToken', +])] class User extends BaseModel { use HasFactory; @@ -30,17 +35,6 @@ class User extends BaseModel #[Override] public $incrementing = false; - /** - * The attributes that should be hidden for serialization. - * - * @var list - */ - #[Override] - protected $hidden = [ - 'password', - 'rememberToken', - ]; - private ?Collection $userGroupData = null; #[Override] diff --git a/src/User/Validation/UserRules.php b/src/User/Validation/UserRules.php index b612301db1f..303dfe8f683 100644 --- a/src/User/Validation/UserRules.php +++ b/src/User/Validation/UserRules.php @@ -7,7 +7,7 @@ use CraftCms\Cms\Cms; use CraftCms\Cms\Database\Table; use CraftCms\Cms\Element\Validation\ElementRules; -use CraftCms\Cms\FieldLayout\LayoutElements\users\FullNameField; +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; diff --git a/src/Validation/Concerns/Validates.php b/src/Validation/Concerns/Validates.php index 99aa90d5f2e..41ee64898f7 100644 --- a/src/Validation/Concerns/Validates.php +++ b/src/Validation/Concerns/Validates.php @@ -5,7 +5,9 @@ namespace CraftCms\Cms\Validation\Concerns; use CraftCms\Cms\Support\Arr; +use CraftCms\Cms\Support\Str; use CraftCms\Cms\Support\Utils; +use CraftCms\Cms\Validation\Contracts\Validatable; use CraftCms\RulesetValidation\Concerns\HasRuleset; use Illuminate\Support\MessageBag; use Illuminate\Validation\Validator; @@ -26,6 +28,30 @@ public function errors(): MessageBag return $this->errors ??= new MessageBag; } + public function clearErrors(?string $attribute = null): void + { + if ($attribute) { + $this->errors()->forget($attribute); + + return; + } + + $this->errors = new MessageBag; + } + + public function addModelErrors(Validatable $model, string $attrPrefix = ''): void + { + if ($attrPrefix !== '') { + $attrPrefix = rtrim($attrPrefix, '.').'.'; + } + + foreach ($model->errors()->getMessages() as $attribute => $errors) { + foreach ($errors as $error) { + $this->errors()->add($attrPrefix.$attribute, $error); + } + } + } + /** * TODO: Add types to method signature once components no longer rely * on craft/base/Model @@ -36,7 +62,7 @@ public function errors(): MessageBag public function validate($attributeNames = null, $clearErrors = true, bool $throw = false): bool { if ($clearErrors) { - $this->errors = new MessageBag; + $this->clearErrors(); } if (is_string($attributeNames)) { @@ -75,6 +101,24 @@ public function attributeLabels(): array return []; } + public function getAttributeLabel(string $attribute): string + { + $labels = $this->attributeLabels(); + + return $labels[$attribute] ?? $this->generateAttributeLabel($attribute); + } + + /** + * Generates a user friendly attribute label based on the give attribute name. + * This is done by replacing underscores, dashes and dots with blanks and + * changing the first letter of each word to upper case. + * For example, 'department_name' or 'DepartmentName' will generate 'Department Name'. + */ + public function generateAttributeLabel(string $name): string + { + return Str::camel2words($name); + } + public function prepareForValidation(): void {} public function passedValidation(): void {} diff --git a/src/Validation/Contracts/Validatable.php b/src/Validation/Contracts/Validatable.php index 926b9c23ca4..997cfb42cd6 100644 --- a/src/Validation/Contracts/Validatable.php +++ b/src/Validation/Contracts/Validatable.php @@ -34,6 +34,20 @@ public function getMessages(): array; */ public function attributeLabels(): array; + /** + * Returns the text label for the specified attribute. + */ + public function getAttributeLabel(string $attribute): string; + + /** + * Generates a user-friendly attribute label based on the give attribute name. + * This is done by replacing underscores, dashes and dots with blanks and + * changing the first letter of each word to uppercase. + * + * For example, 'department_name' or 'DepartmentName' will generate 'Department Name'. + */ + public function generateAttributeLabel(string $name): string; + /** * Sets attribute values. * @@ -77,4 +91,9 @@ public function getFirstErrors(): array; * Returns the validation error messages. */ public function errors(): MessageBag; + + /** + * Clears validation errors for the specified attribute or all attributes. + */ + public function clearErrors(?string $attribute = null): void; } diff --git a/src/Validation/Rules/MoneyRule.php b/src/Validation/Rules/MoneyRule.php index 73de4efeb32..ab224bd1e14 100644 --- a/src/Validation/Rules/MoneyRule.php +++ b/src/Validation/Rules/MoneyRule.php @@ -5,7 +5,7 @@ namespace CraftCms\Cms\Validation\Rules; use Closure; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\I18N; use Illuminate\Contracts\Validation\ValidationRule; use Money\Currencies\ISOCurrencies; diff --git a/src/View/CacheCollectors/DependencyCollector.php b/src/View/CacheCollectors/DependencyCollector.php index d143f50924a..10856dec5e1 100644 --- a/src/View/CacheCollectors/DependencyCollector.php +++ b/src/View/CacheCollectors/DependencyCollector.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\View\CacheCollectors; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Contracts\ExpirableElementInterface; use CraftCms\Cms\Support\DateTimeHelper; use CraftCms\Cms\View\Contracts\CacheCollectorInterface; diff --git a/src/View/Hooks/PrepareElementIndexVariables.php b/src/View/Hooks/PrepareElementIndexVariables.php index 55e3c0e6f1c..6a29b7058fd 100644 --- a/src/View/Hooks/PrepareElementIndexVariables.php +++ b/src/View/Hooks/PrepareElementIndexVariables.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\View\Hooks; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementSources; use CraftCms\Cms\Site\Sites; diff --git a/src/View/Hooks/PrepareElementSourcesVariables.php b/src/View/Hooks/PrepareElementSourcesVariables.php index 4889f994eff..febd6e602cc 100644 --- a/src/View/Hooks/PrepareElementSourcesVariables.php +++ b/src/View/Hooks/PrepareElementSourcesVariables.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\View\Hooks; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementSources; readonly class PrepareElementSourcesVariables diff --git a/src/View/Hooks/PrepareElementToolbarVariables.php b/src/View/Hooks/PrepareElementToolbarVariables.php index 711c5064273..c5cfdfc27c5 100644 --- a/src/View/Hooks/PrepareElementToolbarVariables.php +++ b/src/View/Hooks/PrepareElementToolbarVariables.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\View\Hooks; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Site\Sites; readonly class PrepareElementToolbarVariables diff --git a/src/View/TemplateGlobals.php b/src/View/TemplateGlobals.php index 08fbfe705f9..eacb2710301 100644 --- a/src/View/TemplateGlobals.php +++ b/src/View/TemplateGlobals.php @@ -64,6 +64,7 @@ public function resolve(): array 'craft' => new CraftVariable, 'sessionErrors' => $errors, 'request' => $this->request, + 'session' => $this->request->hasSession() ? $this->request->session() : null, 'pluginAssets' => $this->plugins->getAssetsHtml(), 'currentSite' => $currentSite, 'currentUser' => $currentUser, diff --git a/tests/ArchTest.php b/tests/Arch/ArchTest.php similarity index 100% rename from tests/ArchTest.php rename to tests/Arch/ArchTest.php diff --git a/tests/Feature/Asset/AssetsTest.php b/tests/Feature/Asset/AssetsTest.php index 5ec8a75221b..e795936d188 100644 --- a/tests/Feature/Asset/AssetsTest.php +++ b/tests/Feature/Asset/AssetsTest.php @@ -5,6 +5,7 @@ use craft\assetpreviews\Text; use CraftCms\Cms\Asset\Assets; use CraftCms\Cms\Asset\Elements\Asset; +use CraftCms\Cms\Asset\Enums\FileKind; use CraftCms\Cms\Asset\Events\BeforeReplaceAsset; use CraftCms\Cms\Asset\Events\DefineThumbUrl; use CraftCms\Cms\Asset\Events\RegisterPreviewHandler; @@ -124,7 +125,7 @@ 'volumeId' => $volume->id, 'folderId' => $folder->id, 'filename' => 'test.txt', - 'kind' => Asset::KIND_TEXT, + 'kind' => FileKind::Text->value, ]); $handler = $this->assets->getAssetPreviewHandler($textAsset); diff --git a/tests/Feature/Asset/Elements/AssetValidationTest.php b/tests/Feature/Asset/Elements/AssetValidationTest.php index 56a15382b6e..2ac015ba5f2 100644 --- a/tests/Feature/Asset/Elements/AssetValidationTest.php +++ b/tests/Feature/Asset/Elements/AssetValidationTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use CraftCms\Cms\Asset\Elements\Asset; +use CraftCms\Cms\Asset\Enums\FileKind; use CraftCms\Cms\Asset\Models\Asset as AssetModel; use CraftCms\Cms\Asset\Validation\AssetRules; @@ -29,7 +29,7 @@ })->with([ 'null is invalid' => [null, true], 'empty string is invalid' => ['', true], - 'KIND_IMAGE is valid' => [Asset::KIND_IMAGE, false], + 'KIND_IMAGE is valid' => [FileKind::Image->value, false], ]); }); @@ -110,7 +110,7 @@ $asset = AssetModel::factory()->createElement(); $asset->ruleset->useScenario(AssetRules::SCENARIO_INDEX); - $activeAttributes = $asset->activeAttributes(); + $activeAttributes = array_keys($asset->getRules()); expect($activeAttributes)->toBe([]); }); @@ -157,7 +157,7 @@ expect($asset->errors()->has('kind'))->toBeTrue(); }); - test('all valid asset kinds are accepted', function (string $kind) { + test('valid asset kinds are accepted', function (string $kind) { $asset = AssetModel::factory()->createElement(); $asset->kind = $kind; @@ -165,25 +165,6 @@ expect($asset->errors()->has('kind'))->toBeFalse(); })->with([ - 'KIND_ACCESS' => [Asset::KIND_ACCESS], - 'KIND_AUDIO' => [Asset::KIND_AUDIO], - 'KIND_CAPTIONS_SUBTITLES' => [Asset::KIND_CAPTIONS_SUBTITLES], - 'KIND_COMPRESSED' => [Asset::KIND_COMPRESSED], - 'KIND_EXCEL' => [Asset::KIND_EXCEL], - 'KIND_FLASH' => [Asset::KIND_FLASH], - 'KIND_HTML' => [Asset::KIND_HTML], - 'KIND_ILLUSTRATOR' => [Asset::KIND_ILLUSTRATOR], - 'KIND_IMAGE' => [Asset::KIND_IMAGE], - 'KIND_JAVASCRIPT' => [Asset::KIND_JAVASCRIPT], - 'KIND_JSON' => [Asset::KIND_JSON], - 'KIND_PDF' => [Asset::KIND_PDF], - 'KIND_PHOTOSHOP' => [Asset::KIND_PHOTOSHOP], - 'KIND_PHP' => [Asset::KIND_PHP], - 'KIND_POWERPOINT' => [Asset::KIND_POWERPOINT], - 'KIND_TEXT' => [Asset::KIND_TEXT], - 'KIND_VIDEO' => [Asset::KIND_VIDEO], - 'KIND_WORD' => [Asset::KIND_WORD], - 'KIND_XML' => [Asset::KIND_XML], - 'KIND_UNKNOWN' => [Asset::KIND_UNKNOWN], + 'KIND_IMAGE' => [FileKind::Image->value], ]); }); diff --git a/tests/Feature/Asset/VolumeFilesystemResolutionTest.php b/tests/Feature/Asset/VolumeFilesystemResolutionTest.php index f830c955224..f32e74afcb3 100644 --- a/tests/Feature/Asset/VolumeFilesystemResolutionTest.php +++ b/tests/Feature/Asset/VolumeFilesystemResolutionTest.php @@ -13,7 +13,6 @@ use CraftCms\Cms\Support\Facades\Filesystems; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; -use yii\base\InvalidConfigException; it('resolves explicit disk targets to Laravel disk wrappers', function () { config()->set('filesystems.disks.explicit-disk', [ @@ -363,7 +362,7 @@ ]); $volume->getFs(); -})->throws(InvalidConfigException::class, 'Volume is missing its filesystem handle.'); +})->throws(RuntimeException::class, 'Volume is missing its filesystem handle.'); it('returns MissingFs when the filesystem handle cannot be resolved', function () { $volume = new Volume([ diff --git a/tests/Feature/Condition/ConditionRuleHtmlTest.php b/tests/Feature/Condition/ConditionRuleHtmlTest.php index ece82a28379..ebbbb858865 100644 --- a/tests/Feature/Condition/ConditionRuleHtmlTest.php +++ b/tests/Feature/Condition/ConditionRuleHtmlTest.php @@ -2,7 +2,6 @@ declare(strict_types=1); -use craft\base\ElementInterface; use CraftCms\Cms\Condition\BaseSelectConditionRule; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; use CraftCms\Cms\Element\Conditions\DateCreatedConditionRule; @@ -11,6 +10,7 @@ use CraftCms\Cms\Element\Conditions\IdConditionRule; use CraftCms\Cms\Element\Conditions\StatusConditionRule; use CraftCms\Cms\Element\Conditions\TitleConditionRule; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Shared\Enums\DateRangeType; diff --git a/tests/Feature/Element/Concerns/HasCanonicalTest.php b/tests/Feature/Element/Concerns/HasCanonicalTest.php index 2b03380bdf2..628e80108b7 100644 --- a/tests/Feature/Element/Concerns/HasCanonicalTest.php +++ b/tests/Feature/Element/Concerns/HasCanonicalTest.php @@ -5,8 +5,8 @@ use CraftCms\Cms\Element\Element; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Entry\Models\Entry as EntryModel; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\User\Elements\User; -use yii\base\NotSupportedException; use function Pest\Laravel\actingAs; diff --git a/tests/Feature/Element/Concerns/HasCustomFieldsTest.php b/tests/Feature/Element/Concerns/HasCustomFieldsTest.php index 10269ce83ab..18252f8bb75 100644 --- a/tests/Feature/Element/Concerns/HasCustomFieldsTest.php +++ b/tests/Feature/Element/Concerns/HasCustomFieldsTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); use Carbon\Carbon; -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Entry\Models\Entry; use CraftCms\Cms\Field\Models\Field; diff --git a/tests/Feature/Element/Concerns/HasRoutesAndUrlsTest.php b/tests/Feature/Element/Concerns/HasRoutesAndUrlsTest.php index be743d7b992..35c8429dbbe 100644 --- a/tests/Feature/Element/Concerns/HasRoutesAndUrlsTest.php +++ b/tests/Feature/Element/Concerns/HasRoutesAndUrlsTest.php @@ -11,7 +11,7 @@ use CraftCms\Cms\Support\Facades\Sites; use CraftCms\Cms\User\Elements\User; use Illuminate\Support\Facades\Event; -use Twig\Markup; +use Illuminate\Support\HtmlString; use function Pest\Laravel\actingAs; @@ -187,7 +187,7 @@ protected function route(): array|string|null $link = $element->getLink(); - expect($link)->toBeInstanceOf(Markup::class); + expect($link)->toBeInstanceOf(HtmlString::class); expect((string) $link)->toContain('toContain('test-path'); }); diff --git a/tests/Feature/Element/Concerns/LocalizableTest.php b/tests/Feature/Element/Concerns/LocalizableTest.php index a880c05ebd5..de78306b0f6 100644 --- a/tests/Feature/Element/Concerns/LocalizableTest.php +++ b/tests/Feature/Element/Concerns/LocalizableTest.php @@ -2,14 +2,13 @@ declare(strict_types=1); -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\Queries\ElementQuery; use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface; use CraftCms\Cms\Support\Facades\Sites; -use yii\base\InvalidConfigException; class TestLocalizableElement extends Element { @@ -160,7 +159,7 @@ public function setSaveOwnership(bool $saveOwnership): void $element = new TestLocalizableElement; $element->siteId = 99999; - expect(fn () => $element->getSite())->toThrow(InvalidConfigException::class); + expect(fn () => $element->getSite())->toThrow(RuntimeException::class); }); }); diff --git a/tests/Feature/Element/Concerns/RenderableTest.php b/tests/Feature/Element/Concerns/RenderableTest.php index 4eaf7f4f643..8603636bc29 100644 --- a/tests/Feature/Element/Concerns/RenderableTest.php +++ b/tests/Feature/Element/Concerns/RenderableTest.php @@ -7,7 +7,7 @@ use CraftCms\Cms\Entry\Models\Entry as EntryModel; use CraftCms\Cms\User\Elements\User; use Illuminate\Support\Facades\Event; -use Twig\Markup; +use Illuminate\Support\HtmlString; use function Pest\Laravel\actingAs; @@ -25,7 +25,7 @@ test('returns markup', function () { $markup = $this->entry->render(); - expect($markup)->toBeInstanceOf(Markup::class); + expect($markup)->toBeInstanceOf(HtmlString::class); }); test('Render event allows setting custom output', function () { diff --git a/tests/Feature/Element/DraftsTest.php b/tests/Feature/Element/DraftsTest.php index d36e0877e7e..52075886df9 100644 --- a/tests/Feature/Element/DraftsTest.php +++ b/tests/Feature/Element/DraftsTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\Events\ApplyingDraft; use CraftCms\Cms\Element\Events\CreatingDraft; diff --git a/tests/Feature/Element/ElementActivityTest.php b/tests/Feature/Element/ElementActivityTest.php index e941940cfad..09c7fbfb498 100644 --- a/tests/Feature/Element/ElementActivityTest.php +++ b/tests/Feature/Element/ElementActivityTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Data\ElementActivity as ElementActivityData; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\ElementActivity as ElementActivityService; diff --git a/tests/Feature/Element/ElementCachesTest.php b/tests/Feature/Element/ElementCachesTest.php index d2f1f9c0264..478fbc0485f 100644 --- a/tests/Feature/Element/ElementCachesTest.php +++ b/tests/Feature/Element/ElementCachesTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Contracts\ExpirableElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementCaches; use CraftCms\Cms\Element\Events\InvalidateElementCaches; diff --git a/tests/Feature/Element/ElementCanonicalChanges/UpdateCanonicalElementTest.php b/tests/Feature/Element/ElementCanonicalChanges/UpdateCanonicalElementTest.php index f890ed9eef8..374d1c05b29 100644 --- a/tests/Feature/Element/ElementCanonicalChanges/UpdateCanonicalElementTest.php +++ b/tests/Feature/Element/ElementCanonicalChanges/UpdateCanonicalElementTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; use CraftCms\Cms\Element\BulkOp\BulkOps; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\Operations\ElementCanonicalChanges; use CraftCms\Cms\Element\Operations\ElementDuplicates; diff --git a/tests/Feature/Element/ElementDeletions/CascadeDeleteDraftsAndRevisionsTest.php b/tests/Feature/Element/ElementDeletions/CascadeDeleteDraftsAndRevisionsTest.php index c89d84dd8a2..fc0629f02c6 100644 --- a/tests/Feature/Element/ElementDeletions/CascadeDeleteDraftsAndRevisionsTest.php +++ b/tests/Feature/Element/ElementDeletions/CascadeDeleteDraftsAndRevisionsTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\Operations\ElementDeletions; use CraftCms\Cms\Element\Revisions; diff --git a/tests/Feature/Element/ElementDeletions/RestoreElementsTest.php b/tests/Feature/Element/ElementDeletions/RestoreElementsTest.php index 32876fa9881..0f03389f415 100644 --- a/tests/Feature/Element/ElementDeletions/RestoreElementsTest.php +++ b/tests/Feature/Element/ElementDeletions/RestoreElementsTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementCaches; use CraftCms\Cms\Element\Elements; diff --git a/tests/Feature/Element/ElementDuplicates/DuplicateElementTest.php b/tests/Feature/Element/ElementDuplicates/DuplicateElementTest.php index 2d7a2f8160c..1c654b7502b 100644 --- a/tests/Feature/Element/ElementDuplicates/DuplicateElementTest.php +++ b/tests/Feature/Element/ElementDuplicates/DuplicateElementTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\Events\AfterPropagate; use CraftCms\Cms\Element\Exceptions\InvalidElementException; diff --git a/tests/Feature/Element/ElementEagerLoaderTest.php b/tests/Feature/Element/ElementEagerLoaderTest.php index 71e58dce17e..34eff461ae0 100644 --- a/tests/Feature/Element/ElementEagerLoaderTest.php +++ b/tests/Feature/Element/ElementEagerLoaderTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use craft\base\ElementInterface; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Data\EagerLoadPlan; use CraftCms\Cms\Element\ElementCaches; use CraftCms\Cms\Element\Elements; diff --git a/tests/Feature/Element/ElementWrites/SaveElementTest.php b/tests/Feature/Element/ElementWrites/SaveElementTest.php index 231cb348d83..206300b557e 100644 --- a/tests/Feature/Element/ElementWrites/SaveElementTest.php +++ b/tests/Feature/Element/ElementWrites/SaveElementTest.php @@ -21,6 +21,7 @@ use CraftCms\Cms\Support\Facades\Sites; use CraftCms\Cms\Tests\TestClasses\TestEntryWithAfterValidate; use CraftCms\Cms\User\Elements\User; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; @@ -307,6 +308,8 @@ function createEntryWithPlainTextField(array $entryAttributes = []): array } }); + Config::set('queue.default', 'null'); + expect($this->writes->saveElement($entry))->toBeTrue() ->and($beforeUpdateTriggered)->toBeTrue() ->and(DB::table(Table::SEARCHINDEXQUEUE) diff --git a/tests/Feature/Element/NestedElementManagerTest.php b/tests/Feature/Element/NestedElementManagerTest.php index 78c12282741..21c108cc1b3 100644 --- a/tests/Feature/Element/NestedElementManagerTest.php +++ b/tests/Feature/Element/NestedElementManagerTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -use craft\base\ElementInterface; use CraftCms\Cms\Address\Elements\Address as AddressElement; use CraftCms\Cms\Address\Models\Address as AddressModel; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\NestedElementManager; use CraftCms\Cms\Element\Queries\EntryQuery; use CraftCms\Cms\Entry\Elements\Entry as EntryElement; diff --git a/tests/Feature/Element/Policies/ElementPolicyTest.php b/tests/Feature/Element/Policies/ElementPolicyTest.php index a1b4d7c9ace..16bd7e9d213 100644 --- a/tests/Feature/Element/Policies/ElementPolicyTest.php +++ b/tests/Feature/Element/Policies/ElementPolicyTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; use CraftCms\Cms\Auth\Events\AuthorizingElement; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Policies\ElementPolicy; use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface; diff --git a/tests/Feature/Element/Queries/Concerns/QueriesUniqueElementsTest.php b/tests/Feature/Element/Queries/Concerns/QueriesUniqueElementsTest.php index 10d3423ed79..c46dfa9ea91 100644 --- a/tests/Feature/Element/Queries/Concerns/QueriesUniqueElementsTest.php +++ b/tests/Feature/Element/Queries/Concerns/QueriesUniqueElementsTest.php @@ -1,8 +1,15 @@ site('*')->preferSites([$site2->id, $site1->id])->unique()->first()->siteId)->toBe($site2->id); expect(entryQuery()->site('*')->preferSites([$site2->handle, $site1->handle])->unique()->first()->siteId)->toBe($site2->id); }); + +test('unique still deduplicates when siteId changes after the site filter is applied', function () { + $site1 = Site::firstOrFail(); + $site2 = Site::factory()->create(); + + $field = Field::factory()->create([ + 'type' => ContentBlockField::class, + ]); + + $owner = Entry::factory()->create(); + $owner->element->siteSettings()->create([ + 'siteId' => $site2->id, + ]); + $owner->section->siteSettings()->create([ + 'siteId' => $site2->id, + ]); + + $contentBlock = ElementModel::factory()->create([ + 'type' => ContentBlockElement::class, + ]); + $contentBlock->siteSettings()->create([ + 'siteId' => $site2->id, + ]); + + DB::table(Table::CONTENTBLOCKS)->insert([ + 'id' => $contentBlock->id, + 'fieldId' => $field->id, + 'primaryOwnerId' => $owner->id, + ]); + + DB::table(Table::ELEMENTS_OWNERS)->insert([ + 'elementId' => $contentBlock->id, + 'ownerId' => $owner->id, + 'sortOrder' => 1, + ]); + + $contentBlockQuery = fn () => tap( + ContentBlockElement::find() + ->fieldId($field->id) + ->siteId([$site1->id, $site2->id]) + ->preferSites([$site1->id]) + ->status(null), + fn (ContentBlockQuery $query) => $query->beforeQuery( + fn (ContentBlockQuery $query) => $query->owner(entryQuery()->id($owner->id)->one()) + ), + ); + + expect($contentBlockQuery()->count())->toBe(2); + expect($contentBlockQuery()->unique()->count())->toBe(1); + expect($contentBlockQuery()->unique()->first()->siteId)->toBe($site1->id); +}); diff --git a/tests/Feature/Element/Queries/Concerns/SearchesElementsTest.php b/tests/Feature/Element/Queries/Concerns/SearchesElementsTest.php index 8d046bc7892..8059ef9dc44 100644 --- a/tests/Feature/Element/Queries/Concerns/SearchesElementsTest.php +++ b/tests/Feature/Element/Queries/Concerns/SearchesElementsTest.php @@ -1,48 +1,18 @@ create(); - $entry1->element->siteSettings->first()->update([ - 'title' => 'Foo', - ]); - - $entry2 = EntryModel::factory()->create(); - $entry2->element->siteSettings->first()->update([ - 'title' => 'Bar', - ]); - - $element1 = Elements::getElementById($entry1->id); - $element2 = Elements::getElementById($entry2->id); - - Search::indexElementAttributes($element1); - Search::indexElementAttributes($element2); + $entry1 = EntryModel::factory()->title('Foo')->indexed()->create(); + $entry2 = EntryModel::factory()->title('Bar')->indexed()->create(); expect(entryQuery()->count())->toBe(2); expect(entryQuery()->search('Foo')->count())->toBe(1); }); test('search with score', function () { - $entry1 = EntryModel::factory()->create(); - $entry1->element->siteSettings->first()->update([ - 'title' => 'Foo', - 'content' => '', - ]); - - $entry2 = EntryModel::factory()->create(); - $entry2->element->siteSettings->first()->update([ - 'title' => 'Bar', - 'slug' => 'Foo', - ]); - - $element1 = Elements::getElementById($entry1->id); - $element2 = Elements::getElementById($entry2->id); - - Search::indexElementAttributes($element1); - Search::indexElementAttributes($element2); + $entry1 = EntryModel::factory()->title('Foo')->indexed()->create(); + $entry2 = EntryModel::factory()->title('Bar')->slug('Foo')->indexed()->create(); expect(entryQuery()->orderBy('score')->count())->toBe(2); expect(entryQuery()->search('Foo')->orderBy('score')->count())->toBe(2); diff --git a/tests/Feature/Element/Queries/ContentBlockQueryTest.php b/tests/Feature/Element/Queries/ContentBlockQueryTest.php index 7abaddd220e..7fefa8df29a 100644 --- a/tests/Feature/Element/Queries/ContentBlockQueryTest.php +++ b/tests/Feature/Element/Queries/ContentBlockQueryTest.php @@ -1,17 +1,27 @@ create(); + $field = Field::factory()->create([ + 'type' => ContentBlockField::class, + ]); + $owner = Entry::factory()->create(); + $contentBlock = ElementModel::factory()->create([ + 'type' => ContentBlock::class, + ]); DB::table(Table::CONTENTBLOCKS) ->insert([ - 'primaryOwnerId' => 1, + 'id' => $contentBlock->id, + 'primaryOwnerId' => $owner->id, 'fieldId' => $field->id, ]); diff --git a/tests/Feature/Http/Controllers/Elements/CopyElementValuesControllerTest.php b/tests/Feature/Http/Controllers/Elements/CopyElementValuesControllerTest.php new file mode 100644 index 00000000000..96b9ff95b27 --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/CopyElementValuesControllerTest.php @@ -0,0 +1,244 @@ +secondarySite = Site::factory()->create([ + 'handle' => 'secondary', + ]); + + $this->field = Field::factory()->create([ + 'name' => 'Copy Field', + 'handle' => 'copyField', + 'type' => PlainText::class, + 'translationMethod' => 'site', + ]); + $this->entryType = EntryType::factory()->withField($this->field)->create(); + $this->section = Section::factory() + ->withSites($this->secondarySite) + ->withEntryTypes($this->entryType) + ->create([ + 'handle' => 'blog', + ]); +}); + +it('requires authentication', function () { + Auth::logout(); + + postJson(action(CopyElementValuesController::class), [ + 'elementType' => Entry::class, + ])->assertUnauthorized(); +}); + +it('returns responses resolved by the element request', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + post(action(CopyElementValuesController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'draftId' => 999999, + 'siteId' => $entry->siteId, + ])->assertRedirect($entry->getCpEditUrl()); +}); + +it('returns 400 when no element is identified by the request', function () { + postJson(action(CopyElementValuesController::class), [ + 'elementType' => Entry::class, + 'elementId' => 999999, + 'siteId' => Sites::getPrimarySite()->id, + ])->assertBadRequest(); +}); + +it('returns 400 for revisions', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + $revisionId = app(Revisions::class)->createRevision($entry, auth()->id()); + + postJson(action(CopyElementValuesController::class), [ + 'elementType' => Entry::class, + 'revisionId' => $revisionId, + 'siteId' => $entry->siteId, + ])->assertBadRequest(); +}); + +it('validates the request payload', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + postJson(action(CopyElementValuesController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ])->assertUnprocessable() + ->assertJsonValidationErrors(['fromSiteId', 'layoutElementUid']); +}); + +it('returns 400 for invalid source site ids', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + postJson(action(CopyElementValuesController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'fromSiteId' => 999999, + 'layoutElementUid' => Str::uuid()->toString(), + ])->assertBadRequest(); +}); + +it('forbids copying values from a site the user cannot edit', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + $viewer = UserModel::factory() + ->withPermissions([ + 'accessCp', + sprintf('editSite:%s', Sites::getPrimarySite()->uid), + sprintf('viewEntries:%s', $this->section->uid), + sprintf('viewPeerEntries:%s', $this->section->uid), + ]) + ->createElement(); + + actingAs($viewer); + + postJson(action(CopyElementValuesController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'fromSiteId' => $this->secondarySite->id, + 'layoutElementUid' => customFieldUid($entry), + ])->assertForbidden(); +}); + +it('returns 400 for invalid layout element uuids', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + localizeEntry($entry, $this->secondarySite->id); + + postJson(action(CopyElementValuesController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'fromSiteId' => $this->secondarySite->id, + 'layoutElementUid' => Str::uuid()->toString(), + ])->assertBadRequest(); +}); + +it('copies a title field value from another site and returns updated field html', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Primary Title', + 'slug' => 'primary-title', + ]); + localizeEntry($entry, $this->secondarySite->id); + + /** @var Entry $secondaryEntry */ + $secondaryEntry = Entry::find() + ->id($entry->id) + ->siteId($this->secondarySite->id) + ->status(null) + ->one(); + $secondaryEntry->setFieldValue('copyField', 'Secondary field value'); + $secondaryEntry->setAuthorIds([auth()->id()]); + Elements::saveElement($secondaryEntry); + + postJson(action(CopyElementValuesController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'fromSiteId' => $this->secondarySite->id, + 'layoutElementUid' => customFieldUid($entry), + 'namespace' => 'copyNamespace', + ])->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', t('Field value copied.')) + ->where('modelName', 'element') + ->where('fieldHtml', fn (string $html) => $html !== '' + && str_contains($html, 'data-layout-element="'.customFieldUid($entry).'"')) + ->has('headHtml') + ->has('bodyHtml') + ->etc() + ); +}); + +function customFieldUid(Entry $entry): string +{ + $layoutElement = $entry->getFieldLayout()->getCustomFieldElements()[0] ?? null; + + expect($layoutElement?->uid)->not->toBeNull(); + + return $layoutElement->uid; +} + +function localizeEntry(Entry $entry, int $siteId): void +{ + SectionSiteSettings::query()->firstOrCreate([ + 'sectionId' => $entry->sectionId, + 'siteId' => $siteId, + ], [ + 'uid' => (string) Str::uuid(), + 'hasUrls' => true, + ]); + + EntryModel::query()->findOrFail($entry->id)->element->siteSettings()->firstOrCreate([ + 'siteId' => $siteId, + ]); +} diff --git a/tests/Feature/Http/Controllers/Elements/CreateElementControllerTest.php b/tests/Feature/Http/Controllers/Elements/CreateElementControllerTest.php new file mode 100644 index 00000000000..bcd802d9b0f --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/CreateElementControllerTest.php @@ -0,0 +1,133 @@ +entryType = EntryType::factory()->create(); + $this->section = Section::factory()->withEntryTypes($this->entryType)->create([ + 'handle' => 'blog', + ]); +}); + +function createElementControllerPayload(object $section, object $entryType, array $overrides = []): array +{ + return array_merge([ + 'elementType' => Entry::class, + 'siteId' => Sites::getPrimarySite()->id, + 'sectionId' => $section->id, + 'typeId' => $entryType->id, + 'title' => 'New Draft Entry', + ], $overrides); +} + +it('requires authentication', function () { + Auth::logout(); + + postJson(action(CreateElementController::class), [ + 'elementType' => Entry::class, + ])->assertUnauthorized(); +}); + +it('requires a valid element type', function () { + postJson(action(CreateElementController::class), [ + 'elementType' => stdClass::class, + ])->assertBadRequest(); +}); + +it('forbids creating an element when the user cannot save it', function () { + $viewer = UserModel::factory() + ->withPermissions([ + 'accessCp', + sprintf('editSite:%s', Sites::getPrimarySite()->uid), + ]) + ->createElement(); + + actingAs($viewer); + + postJson(action(CreateElementController::class), createElementControllerPayload($this->section, $this->entryType)) + ->assertForbidden(); +}); + +it('returns a failure response when saving the draft fails', function () { + app()->instance(Drafts::class, new readonly class(app(Elements::class)) extends Drafts + { + public function saveElementAsDraft( + ElementInterface $element, + ?int $creatorId = null, + ?string $name = null, + ?string $notes = null, + bool $markAsSaved = true, + ): bool { + return false; + } + }); + + postJson(action(CreateElementController::class), createElementControllerPayload($this->section, $this->entryType)) + ->assertBadRequest() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', mb_ucfirst(t('Couldn’t create {type}.', [ + 'type' => Entry::lowerDisplayName(), + ]))) + ->where('modelName', 'element') + ->where('element.title', 'New Draft Entry') + ->etc() + ); +}); + +it('creates a draft and returns its control panel edit url for json requests', function () { + $response = postJson( + cp_url('actions/elements/create'), + createElementControllerPayload($this->section, $this->entryType), + )->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', t('{type} created.', ['type' => t('Draft')])) + ->where('modelName', 'element') + ->where('element.title', 'New Draft Entry') + ->where('element.sectionId', $this->section->id) + ->where('element.typeId', $this->entryType->id) + ->where('element.draftId', fn (int $draftId) => $draftId > 0) + ->etc() + ); + + /** @var Entry $draft */ + $draft = Entry::find() + ->id($response->json('element.id')) + ->drafts() + ->status(null) + ->one(); + + expect($draft)->not->toBeNull() + ->and($draft->draftId)->toBe($response->json('element.draftId')) + ->and($draft->getIsUnpublishedDraft())->toBeTrue() + ->and($draft->title)->toBe('New Draft Entry'); +}); + +it('redirects to the draft edit page for non-json requests', function () { + $draftCount = Entry::find()->drafts()->status(null)->count(); + + post(cp_url('actions/elements/create'), createElementControllerPayload($this->section, $this->entryType)) + ->assertRedirect(); + + expect(Entry::find()->drafts()->status(null)->count())->toBe($draftCount + 1); +}); diff --git a/tests/Feature/Http/Controllers/Elements/DeleteElementControllerTest.php b/tests/Feature/Http/Controllers/Elements/DeleteElementControllerTest.php new file mode 100644 index 00000000000..869b7889ece --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/DeleteElementControllerTest.php @@ -0,0 +1,220 @@ +logout(); + + postJson(action([DeleteElementController::class, 'destroy']))->assertUnauthorized(); + postJson(action([DeleteElementController::class, 'destroyForSite']))->assertUnauthorized(); +}); + +describe('destroy', function () { + it('returns 400 when no element is identified by the request', function () { + postJson(action([DeleteElementController::class, 'destroy']), [ + 'elementType' => Entry::class, + 'siteId' => Sites::getPrimarySite()->id, + ])->assertBadRequest(); + }); + + it('returns 400 for saved drafts', function () { + $entry = EntryModel::factory()->createElement(); + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Delete Draft'); + + postJson(action([DeleteElementController::class, 'destroy']), [ + 'elementId' => $entry->id, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + ])->assertBadRequest(); + }); + + it('returns 400 for revisions', function () { + $entry = EntryModel::factory()->createElement(); + /** @var Entry $revision */ + $revision = ElementsFacade::getElementById(app(Revisions::class)->createRevision($entry, auth()->id())); + + postJson(action([DeleteElementController::class, 'destroy']), [ + 'elementId' => $entry->id, + 'revisionId' => $revision->revisionId, + 'siteId' => $entry->siteId, + ])->assertBadRequest(); + }); + + it('deletes the canonical element when a provisional draft is requested', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Entry', + ]); + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), provisional: true); + + postJson(action([DeleteElementController::class, 'destroy']), [ + 'elementId' => $draft->id, + 'siteId' => $draft->siteId, + ])->assertOk() + ->assertJsonPath('message', t('{type} deleted.', ['type' => Entry::displayName()])); + + expect(Entry::find()->status(null)->id($entry->id)->trashed()->one()?->dateDeleted) + ->not->toBeNull(); + }); + + it('returns a failure response when deleting the element fails', function () { + $entry = EntryModel::factory()->createElement(); + + Event::listen(BeforeDelete::class, function (BeforeDelete $event) use ($entry) { + if ($event->element->id === $entry->id) { + $event->isValid = false; + } + }); + + postJson(action([DeleteElementController::class, 'destroy']), [ + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ])->assertStatus(400) + ->assertJsonPath('message', t('Couldn’t delete {type}.', ['type' => $entry::lowerDisplayName()])); + }); +}); + +describe('destroyForSite', function () { + it('returns 400 when no element is identified by the request', function () { + postJson(action([DeleteElementController::class, 'destroyForSite']), [ + 'elementType' => Entry::class, + 'siteId' => Sites::getPrimarySite()->id, + ])->assertBadRequest(); + }); + + it('returns 400 for revisions', function () { + $secondarySite = Site::factory()->create(); + $section = Section::factory() + ->withSites($secondarySite) + ->withEntryTypes($entryType = EntryType::factory()->create()) + ->create([ + 'propagationMethod' => PropagationMethod::Custom, + ]); + $entry = EntryModel::factory() + ->forSection($section) + ->forEntryType($entryType) + ->enabledForSites($secondarySite) + ->createElement(); + /** @var Entry $revision */ + $revision = ElementsFacade::getElementById(app(Revisions::class)->createRevision($entry, auth()->id())); + + postJson(action([DeleteElementController::class, 'destroyForSite']), [ + 'elementId' => $entry->id, + 'revisionId' => $revision->revisionId, + 'siteId' => $entry->siteId, + ])->assertBadRequest(); + }); + + it('deletes only the requested site for canonical entries', function () { + $secondarySite = Site::factory()->create(); + $section = Section::factory() + ->withSites($secondarySite) + ->withEntryTypes($entryType = EntryType::factory()->create()) + ->create([ + 'propagationMethod' => PropagationMethod::Custom, + ]); + $entry = EntryModel::factory() + ->forSection($section) + ->forEntryType($entryType) + ->enabledForSites($secondarySite) + ->createElement([ + 'title' => 'Multi-site entry', + ]); + + postJson(action([DeleteElementController::class, 'destroyForSite']), [ + 'elementId' => $entry->id, + 'siteId' => $secondarySite->id, + ])->assertOk() + ->assertJsonPath('message', t('{type} deleted for site.', ['type' => Entry::displayName()])); + + expect(entryQuery()->id($entry->id)->siteId($entry->siteId)->status(null)->exists())->toBeTrue() + ->and(entryQuery()->id($entry->id)->siteId($secondarySite->id)->status(null)->exists())->toBeFalse(); + }); + + it('returns the draft label when deleting a saved draft for a site', function () { + $secondarySite = Site::factory()->create(); + $section = Section::factory() + ->withSites($secondarySite) + ->withEntryTypes($entryType = EntryType::factory()->create()) + ->create([ + 'propagationMethod' => PropagationMethod::Custom, + ]); + $entry = EntryModel::factory() + ->forSection($section) + ->forEntryType($entryType) + ->enabledForSites($secondarySite) + ->createElement([ + 'title' => 'Multi-site entry', + ]); + + $secondaryEntry = entryQuery() + ->id($entry->id) + ->siteId($secondarySite->id) + ->status(null) + ->one(); + $draft = app(Drafts::class)->createDraft($secondaryEntry, auth()->id(), name: 'Site Draft'); + + postJson(action([DeleteElementController::class, 'destroyForSite']), [ + 'elementId' => $draft->id, + 'siteId' => $draft->siteId, + ])->assertOk() + ->assertJsonPath('message', t('{type} deleted for site.', ['type' => t('Draft')])); + }); + + it('deletes the canonical site when the current user has a provisional draft for that site', function () { + $secondarySite = Site::factory()->create(); + $section = Section::factory() + ->withSites($secondarySite) + ->withEntryTypes($entryType = EntryType::factory()->create()) + ->create([ + 'propagationMethod' => PropagationMethod::Custom, + ]); + $entry = EntryModel::factory() + ->forSection($section) + ->forEntryType($entryType) + ->enabledForSites($secondarySite) + ->createElement([ + 'title' => 'Multi-site entry', + ]); + + $secondaryEntry = entryQuery() + ->id($entry->id) + ->siteId($secondarySite->id) + ->status(null) + ->one(); + + $draft = app(Drafts::class)->createDraft($secondaryEntry, auth()->id(), provisional: true); + + postJson(action([DeleteElementController::class, 'destroyForSite']), [ + 'elementId' => $entry->id, + 'siteId' => $secondarySite->id, + ])->assertOk() + ->assertJsonPath('message', t('{type} deleted for site.', ['type' => Entry::displayName()])); + + expect(entryQuery()->id($entry->id)->siteId($entry->siteId)->status(null)->exists())->toBeTrue() + ->and(entryQuery()->id($entry->id)->siteId($secondarySite->id)->status(null)->exists())->toBeFalse() + ->and(Entry::find()->id($draft->id)->drafts()->provisionalDrafts()->siteId($secondarySite->id)->status(null)->exists())->toBeFalse(); + }); +}); diff --git a/tests/Feature/Http/Controllers/Elements/DuplicateElementControllerTest.php b/tests/Feature/Http/Controllers/Elements/DuplicateElementControllerTest.php new file mode 100644 index 00000000000..ae1cc919da0 --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/DuplicateElementControllerTest.php @@ -0,0 +1,339 @@ +entryType = EntryType::factory()->create(); + $this->section = Section::factory()->withEntryTypes($this->entryType)->create([ + 'handle' => 'blog', + 'enableVersioning' => true, + ]); +}); + +describe('duplicate', function () { + it('requires authentication', function () { + Auth::logout(); + + postJson(action([DuplicateElementController::class, 'duplicate']), [ + 'elementType' => Entry::class, + ])->assertUnauthorized(); + }); + + it('returns 400 when no element is identified by the request', function () { + postJson(action([DuplicateElementController::class, 'duplicate']), [ + 'elementType' => Entry::class, + 'elementId' => 999999, + 'siteId' => Sites::getPrimarySite()->id, + ])->assertBadRequest(); + }); + + it('returns 400 for revisions', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $revision */ + $revision = Elements::getElementById(app(Revisions::class)->createRevision($entry, auth()->id())); + + postJson(action([DuplicateElementController::class, 'duplicate']), [ + 'elementType' => Entry::class, + 'revisionId' => $revision->revisionId, + 'siteId' => $revision->siteId, + ])->assertBadRequest(); + }); + + it('forbids duplicating an element without permission', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + $viewer = UserModel::factory() + ->withPermissions([ + 'accessCp', + sprintf('editSite:%s', Sites::getPrimarySite()->uid), + sprintf('viewEntries:%s', $this->section->uid), + sprintf('viewPeerEntries:%s', $this->section->uid), + ]) + ->createElement(); + + actingAs($viewer); + + postJson(action([DuplicateElementController::class, 'duplicate']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ])->assertForbidden(); + }); + + it('returns a failure response when duplication raises an invalid element exception', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + $entry->errors()->add('title', 'Title is invalid.'); + + $elements = Mockery::mock(ElementsService::class); + $elements->shouldReceive('duplicateElement') + ->once() + ->andThrow(new InvalidElementException($entry)); + + app()->instance(ElementsService::class, $elements); + + postJson(action([DuplicateElementController::class, 'duplicate']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ])->assertBadRequest() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', t('Couldn’t duplicate {type}.', ['type' => Entry::lowerDisplayName()])) + ->where('errors.title.0', 'Title is invalid.') + ->etc() + ); + }); + + it('duplicates a canonical element', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + $response = postJson(action([DuplicateElementController::class, 'duplicate']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ])->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', t('{type} duplicated.', ['type' => Entry::displayName()])) + ->where('modelName', 'element') + ->where('element.title', 'Canonical Title') + ->etc() + ); + + /** @var Entry $duplicate */ + $duplicate = Entry::find() + ->id($response->json('element.id')) + ->siteId($entry->siteId) + ->status(null) + ->one(); + + expect($duplicate)->not->toBeNull() + ->and($duplicate->id)->not->toBe($entry->id) + ->and($duplicate->getCanonicalId())->toBe($duplicate->id) + ->and($duplicate->draftId)->toBeNull() + ->and($duplicate->title)->toBe('Canonical Title'); + }); + + it('duplicates a provisional draft as an unpublished draft and deletes the provisional source', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), provisional: true); + + $response = postJson(action([DuplicateElementController::class, 'duplicate']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + 'asUnpublishedDraft' => true, + 'deleteProvisionalDraft' => true, + ])->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', t('{type} duplicated.', ['type' => Entry::displayName()])) + ->where('element.draftName', t('First draft')) + ->etc() + ); + + /** @var Entry $duplicate */ + $duplicate = Entry::find() + ->id($response->json('element.id')) + ->drafts() + ->status(null) + ->one(); + + expect($duplicate)->not->toBeNull() + ->and($duplicate->draftId)->not->toBeNull() + ->and($duplicate->getIsUnpublishedDraft())->toBeTrue() + ->and($duplicate->draftName)->toBe(t('First draft')) + ->and(Entry::find()->draftId($draft->draftId)->status(null)->one())->toBeNull(); + }); +}); + +describe('bulkDuplicate', function () { + it('requires authentication', function () { + Auth::logout(); + + postJson(action([DuplicateElementController::class, 'bulkDuplicate']))->assertUnauthorized(); + }); + + it('validates the payload', function () { + postJson(action([DuplicateElementController::class, 'bulkDuplicate']), []) + ->assertUnprocessable() + ->assertJsonValidationErrors(['elements', 'newAttributes']); + }); + + it('skips unidentified elements', function () { + postJson(action([DuplicateElementController::class, 'bulkDuplicate']), [ + 'elements' => [[ + 'type' => Entry::class, + 'id' => 999999, + 'siteId' => Sites::getPrimarySite()->id, + ]], + 'newAttributes' => [ + 'sectionId' => $this->section->id, + 'typeId' => $this->entryType->id, + ], + ])->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', mb_ucfirst(t('{type} duplicated.', ['type' => Entry::displayName()]))) + ->where('newElements', []) + ); + }); + + it('forbids bulk duplication when the user cannot duplicate the element', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + $viewer = UserModel::factory() + ->withPermissions([ + 'accessCp', + sprintf('editSite:%s', Sites::getPrimarySite()->uid), + sprintf('viewEntries:%s', $this->section->uid), + sprintf('viewPeerEntries:%s', $this->section->uid), + ]) + ->createElement(); + + actingAs($viewer); + + postJson(action([DuplicateElementController::class, 'bulkDuplicate']), [ + 'elements' => [[ + 'type' => Entry::class, + 'id' => $entry->id, + 'siteId' => $entry->siteId, + ]], + 'newAttributes' => [ + 'sectionId' => $this->section->id, + 'typeId' => $this->entryType->id, + ], + ])->assertForbidden(); + }); + + it('returns a failure response when bulk duplication raises an invalid element exception', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + $entry->errors()->add('title', 'Title is invalid.'); + + $elements = Mockery::mock(ElementsService::class); + $elements->shouldReceive('duplicateElement') + ->once() + ->andThrow(new InvalidElementException($entry)); + + app()->instance(ElementsService::class, $elements); + + postJson(action([DuplicateElementController::class, 'bulkDuplicate']), [ + 'elements' => [[ + 'type' => Entry::class, + 'id' => $entry->id, + 'siteId' => $entry->siteId, + ]], + 'newAttributes' => [ + 'sectionId' => $this->section->id, + 'typeId' => $this->entryType->id, + ], + ])->assertBadRequest() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', t('Couldn’t duplicate {type}.', ['type' => Entry::lowerDisplayName()])) + ->where('errors.title.0', 'Title is invalid.') + ->etc() + ); + }); + + it('duplicates revisions as regular elements', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Original Title', + 'slug' => 'original-title', + ]); + /** @var Entry $revision */ + $revision = Elements::getElementById(app(Revisions::class)->createRevision($entry, auth()->id())); + + $response = postJson(action([DuplicateElementController::class, 'bulkDuplicate']), [ + 'elements' => [[ + 'type' => Entry::class, + 'revisionId' => $revision->revisionId, + 'siteId' => $revision->siteId, + ]], + 'newAttributes' => [ + 'sectionId' => $this->section->id, + 'typeId' => $this->entryType->id, + ], + ])->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', mb_ucfirst(t('{type} duplicated.', ['type' => Entry::displayName()]))) + ->has('newElements', 1) + ->where('newElements.0.title', 'Original Title') + ); + + /** @var Entry $duplicate */ + $duplicate = Entry::find() + ->id($response->json('newElements.0.id')) + ->siteId($entry->siteId) + ->status(null) + ->one(); + + expect($duplicate)->not->toBeNull() + ->and($duplicate->id)->not->toBe($entry->id) + ->and($duplicate->revisionId)->toBeNull() + ->and($duplicate->title)->toBe('Original Title'); + }); +}); diff --git a/tests/Feature/Http/Controllers/Elements/EditElementControllerTest.php b/tests/Feature/Http/Controllers/Elements/EditElementControllerTest.php new file mode 100644 index 00000000000..8d111d6a290 --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/EditElementControllerTest.php @@ -0,0 +1,329 @@ + [ + fn (Entry $entry) => cp_url(sprintf( + 'entries/%s/%d-%s', + $entry->getSection()->handle, + $entry->id, + $entry->slug, + )), + ], + 'content route' => [ + fn (Entry $entry) => cp_url(sprintf( + 'content/entries/%s/%d-%s', + $entry->getSection()->handle, + $entry->id, + $entry->slug, + )), + ], +]); + +beforeEach(function () { + actingAs(User::findOne()); + + config()->set('filesystems.disks.edit-element-controller-test', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/edit-element-controller-test'), + ]); + + $this->entryType = EntryType::factory()->create(); + $this->section = Section::factory()->withEntryTypes($this->entryType)->create([ + 'handle' => 'news', + 'enableVersioning' => true, + ]); + $this->volume = Volume::factory()->create(['fs' => 'disk:edit-element-controller-test']); + $this->folder = VolumeFolderModel::factory()->create(['volumeId' => $this->volume->id]); +}); + +it('requires login for each entry control panel edit route', function (Closure $route) { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Current Title', + 'slug' => 'current-title', + ]); + + Auth::logout(); + + get($route($entry))->assertRedirectContains('login'); +})->with('editElementEntryRoutes'); + +it('requires login for the asset control panel edit route', function () { + $asset = AssetModel::factory()->createElement([ + 'volumeId' => $this->volume->id, + 'folderId' => $this->folder->id, + ]); + + Auth::logout(); + + get($asset->getCpEditUrl())->assertRedirectContains('login'); +}); + +it('requires authentication for the action route', function () { + Auth::logout(); + + postJson(action(EditElementController::class), [ + 'elementType' => Entry::class, + ], [ + 'X-Craft-Container-Id' => 'slideout', + ])->assertUnauthorized(); +}); + +it('renders the current entry edit screen for each control panel route', function (Closure $route) { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Current Title', + 'slug' => 'current-title', + ]); + + get($route($entry)) + ->assertOk() + ->assertSeeText('Current Title') + ->assertSeeText('Create a draft') + ->assertSee('elements/save', false); +})->with('editElementEntryRoutes'); + +it('renders the asset edit screen', function () { + $asset = AssetModel::factory()->createElement([ + 'volumeId' => $this->volume->id, + 'folderId' => $this->folder->id, + 'filename' => 'featured-image.jpg', + ]); + + get($asset->getCpEditUrl()) + ->assertOk() + ->assertSee(sprintf('"elementId":%d', $asset->id), false); +}); + +it('returns responses resolved by the element request', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + post(action(EditElementController::class), [ + 'elementType' => $entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'draftId' => 999999, + ])->assertRedirect($entry->getCpEditUrl()); +}); + +it('returns 400 when no element is identified by the request', function () { + postJson(action(EditElementController::class), [ + 'elementType' => Entry::class, + 'siteId' => Sites::getPrimarySite()->id, + ], [ + 'X-Craft-Container-Id' => 'slideout', + ])->assertBadRequest(); +}); + +it('returns a json editor payload for the current element', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Current Title', + 'slug' => 'current-title', + ]); + + getJson(action(EditElementController::class, [ + 'elementType' => $entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ]), [ + 'X-Craft-Container-Id' => 'slideout', + ]) + ->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('action', 'elements/save') + ->where('notice', null) + ->where('content', fn (string $content) => $content !== '' + && str_contains($content, 'elements/save')) + ->where('bodyHtml', fn (string $html) => str_contains($html, sprintf('"elementId":%d', $entry->id)) + && str_contains($html, sprintf('"canonicalId":%d', $entry->id)) + && str_contains($html, '"isStatic":false') + && str_contains($html, '"isProvisionalDraft":false') + && str_contains($html, '"isUnpublishedDraft":false')) + ->has('headHtml') + ->has('bodyHtml') + ->has('deltaNames') + ->has('initialDeltaValues') + ->etc() + ); +}); + +it('prevalidates enabled live elements and returns an error summary', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Current Title', + 'slug' => 'current-title', + ]); + + postJson(action(EditElementController::class), [ + 'elementType' => $entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'prevalidate' => 1, + 'title' => '', + ], [ + 'X-Craft-Container-Id' => 'slideout', + ]) + ->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('action', 'elements/save') + ->where('errorSummary', fn (?string $summary) => is_string($summary) + && str_contains($summary, 'The title field is required.') + && str_contains($summary, 'field-error-key')) + ->etc() + ); +}); + +it('renders draft editing controls for saved drafts', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Working Draft'); + + get(cp_url(sprintf( + 'entries/%s/%d-%s?draftId=%d', + $entry->getSection()->handle, + $entry->id, + $entry->slug, + $draft->draftId, + ))) + ->assertOk() + ->assertSeeText('Apply draft') + ->assertSeeText(mb_ucfirst(t('Save {type}', ['type' => t('draft')]))) + ->assertSee('elements/save-draft', false); +}); + +it('renders revision notices and controls for revisions', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $revision */ + $revision = Elements::getElementById(app(Revisions::class)->createRevision($entry, auth()->id(), 'Revision notes')); + + get(cp_url(sprintf( + 'entries/%s/%d-%s?revisionId=%d', + $entry->getSection()->handle, + $entry->id, + $entry->slug, + $revision->revisionId, + ))) + ->assertOk() + ->assertSeeText('viewing a revision') + ->assertSeeText('Revert content from this revision') + ->assertSee('elements/revert', false); +}); + +it('renders provisional draft notices when a provisional draft exists', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + app(Drafts::class)->createDraft($entry, auth()->id(), provisional: true); + + get(cp_url(sprintf( + 'entries/%s/%d-%s', + $entry->getSection()->handle, + $entry->id, + $entry->slug, + ))) + ->assertOk() + ->assertSeeText('Showing your unsaved changes.') + ->assertSee('elements/apply-draft', false); +}); + +it('renders unpublished draft controls', function () { + /** @var Entry $draft */ + $draft = app(Entry::class); + $draft->siteId = Sites::getPrimarySite()->id; + $draft->sectionId = $this->section->id; + $draft->typeId = $this->entryType->id; + $draft->title = 'Unpublished Draft'; + $draft->slug = Str::slug($draft->title); + $draft->setAuthorIds([auth()->id()]); + + app(Drafts::class)->saveElementAsDraft($draft, auth()->id(), markAsSaved: false); + + get($draft->getCpEditUrl()) + ->assertOk() + ->assertSeeText(mb_ucfirst(t('Create {type}', ['type' => Entry::lowerDisplayName()]))) + ->assertSee('elements/apply-draft', false); +}); + +it('merges canonical changes into outdated drafts before rendering', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Working Draft'); + + $entry->title = 'Updated Canonical Title'; + Elements::saveElement($entry); + + get(cp_url(sprintf( + 'entries/%s/%d-%s?draftId=%d', + $entry->getSection()->handle, + $entry->id, + $entry->slug, + $draft->draftId, + ))) + ->assertOk() + ->assertSeeText('Recent changes to the Current revision have been merged into this draft.'); +}); diff --git a/tests/Feature/Http/Controllers/Elements/ElementActivityControllerTest.php b/tests/Feature/Http/Controllers/Elements/ElementActivityControllerTest.php new file mode 100644 index 00000000000..3063161dced --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/ElementActivityControllerTest.php @@ -0,0 +1,106 @@ + Entry::class, + ])->assertUnauthorized(); +}); + +it('returns responses resolved by the element request', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + post(action(ElementActivityController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'draftId' => 999999, + 'siteId' => $entry->siteId, + ])->assertRedirect($entry->getCpEditUrl()); +}); + +it('returns 400 when no element is identified by the request', function () { + postJson(action(ElementActivityController::class), [ + 'elementType' => Entry::class, + 'siteId' => 1, + ])->assertBadRequest(); +}); + +it('returns 400 for revisions', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $revision */ + $revision = Elements::getElementById(app(Revisions::class)->createRevision($entry, auth()->id())); + + postJson(action(ElementActivityController::class), [ + 'elementType' => Entry::class, + 'revisionId' => $revision->revisionId, + 'siteId' => $revision->siteId, + ])->assertBadRequest(); +}); + +it('returns recent activity and tracks the current user viewing the element', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + $otherUser = UserModel::factory()->createElement(); + + app(ElementActivityService::class)->trackActivity($entry, ElementActivityType::Edit, $otherUser); + + $response = postJson(action(ElementActivityController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ]) + ->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('updatedTimestamp', $entry->dateUpdated->getTimestamp()) + ->where('canonicalUpdatedTimestamp', $entry->getCanonical()->dateUpdated->getTimestamp()) + ->has('activity', 1) + ->where('activity.0.userId', $otherUser->id) + ->where('activity.0.userName', $otherUser->getName()) + ->where('activity.0.type', ElementActivityType::Edit->value) + ); + + expect($response->json('activity.0.message'))->toContain('is editing this entry.'); + + $viewerActivity = DB::table(Table::ELEMENTACTIVITY) + ->where('elementId', $entry->id) + ->where('userId', auth()->id()) + ->where('siteId', $entry->siteId) + ->whereNull('draftId') + ->where('type', ElementActivityType::View->value) + ->first(); + + expect($viewerActivity)->not->toBeNull(); +}); diff --git a/tests/Feature/Http/Controllers/Elements/ElementDraftsControllerTest.php b/tests/Feature/Http/Controllers/Elements/ElementDraftsControllerTest.php new file mode 100644 index 00000000000..e8404d46726 --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/ElementDraftsControllerTest.php @@ -0,0 +1,716 @@ + $entry->id, + 'siteId' => $entry->siteId, + 'title' => 'Ported Draft Title', + 'slug' => 'ported-draft-title', + 'draftName' => 'Ported Draft', + 'notes' => 'Ported draft notes', + ], $overrides); +} + +beforeEach(function () { + actingAs(User::findOne()); +}); + +describe('ensure', function () { + it('returns 400 when ensure does not identify an element', function () { + postJson(action([ElementDraftsController::class, 'ensure']), [ + 'elementType' => Entry::class, + 'siteId' => Sites::getPrimarySite()->id, + ])->assertBadRequest(); + }); + + it('returns 400 for revisions when ensuring drafts', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $revision */ + $revision = ElementsFacade::getElementById(app(Revisions::class)->createRevision($entry, auth()->id())); + + postJson(action([ElementDraftsController::class, 'ensure']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'revisionId' => $revision->revisionId, + 'siteId' => $entry->siteId, + ])->assertBadRequest(); + }); + + it('returns the existing draft when ensure identifies one', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Existing Draft'); + + postJson(action([ElementDraftsController::class, 'ensure']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + ])->assertOk() + ->assertJsonPath('elementId', $draft->id); + }); + + it('returns the existing provisional draft for the requested element', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), provisional: true); + + postJson(action([ElementDraftsController::class, 'ensure']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ])->assertOk() + ->assertJsonPath('elementId', $draft->id); + + expect( + Entry::find() + ->drafts() + ->provisionalDrafts() + ->draftOf($entry->id) + ->draftCreator(auth()->id()) + ->status(null) + ->count() + )->toBe(1); + }); + + it('returns an existing provisional draft after resolving the canonical element', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), provisional: true); + + $request = Mockery::mock(ElementRequest::class); + $request->shouldReceive('element') + ->once() + ->with([], true) + ->andReturn($entry); + $request->shouldReceive('user') + ->once() + ->andReturn(auth()->user()); + + app()->instance('request', Request::create('/actions/elements/ensure-draft', 'POST', [], [], [], [ + 'HTTP_ACCEPT' => 'application/json', + ])); + + $response = app()->make(ElementDraftsController::class, [ + 'request' => $request, + ])->ensure(); + + $payload = json_decode($response->getContent(), true, flags: JSON_THROW_ON_ERROR); + + expect($response->getStatusCode())->toBe(200) + ->and($payload['elementId'])->toBe($draft->id) + ->and( + Entry::find() + ->drafts() + ->provisionalDrafts() + ->draftOf($entry->id) + ->draftCreator(auth()->id()) + ->status(null) + ->count() + )->toBe(1); + }); + + it('creates a provisional draft when ensuring a canonical element', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + $response = postJson(action([ElementDraftsController::class, 'ensure']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ]); + + $response->assertOk(); + + /** @var Entry $draft */ + $draft = Entry::find() + ->id($response->json('elementId')) + ->drafts() + ->provisionalDrafts() + ->status(null) + ->one(); + + expect($draft)->not->toBeNull() + ->and($draft->id)->not->toBe($entry->id) + ->and($draft->getCanonicalId())->toBe($entry->id) + ->and($draft->isProvisionalDraft)->toBeTrue() + ->and($draft->draftCreatorId)->toBe(auth()->id()); + }); +}); + +describe('store', function () { + it('requires authentication', function () { + auth()->logout(); + + postJson(action([ElementDraftsController::class, 'store']))->assertUnauthorized(); + }); + + it('returns 400 when no element is identified by the request', function () { + postJson(action([ElementDraftsController::class, 'store']), [ + 'elementType' => Entry::class, + 'siteId' => Sites::getPrimarySite()->id, + ])->assertBadRequest(); + }); + + it('returns 400 for revisions', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $revision */ + $revision = ElementsFacade::getElementById(app(Revisions::class)->createRevision($entry, auth()->id())); + + postJson(action([ElementDraftsController::class, 'store']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'revisionId' => $revision->revisionId, + 'siteId' => $entry->siteId, + ])->assertBadRequest(); + }); + + it('creates a draft from a canonical element and authorizes previewing it', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + $response = postJson( + action([ElementDraftsController::class, 'store']), + elementDraftsControllerPayload($entry), + ); + + $response->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', t('{type} saved.', ['type' => t('Draft')])) + ->where('canonicalId', $entry->id) + ->where('draftName', 'Ported Draft') + ->where('draftNotes', 'Ported draft notes') + ->where('creator', auth()->user()->getName()) + ->etc() + ); + + /** @var Entry $draft */ + $draft = Entry::find() + ->id($response->json('elementId')) + ->drafts() + ->status(null) + ->one(); + + expect($draft)->not->toBeNull() + ->and($draft->getCanonicalId())->toBe($entry->id) + ->and($draft->title)->toBe('Ported Draft Title') + ->and($draft->slug)->toBe('ported-draft-title') + ->and($draft->draftName)->toBe('Ported Draft') + ->and($draft->draftNotes)->toBe('Ported draft notes') + ->and(SessionAuth::checkAuthorization("previewDraft:$draft->draftId"))->toBeTrue(); + + expect($response->json('draftElementIds'))->toMatchArray([ + (string) $entry->id => $draft->id, + ])->and($response->json('draftElementUids'))->toMatchArray([ + $entry->uid => $draft->uid, + ]); + }); + + it('updates an existing draft', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Existing Draft'); + + postJson(action([ElementDraftsController::class, 'store']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + 'title' => 'Updated Draft Title', + 'slug' => 'updated-draft-title', + 'draftName' => 'Renamed Draft', + 'notes' => 'Updated draft notes', + ])->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('elementId', $draft->id) + ->where('draftId', $draft->draftId) + ->where('draftName', 'Renamed Draft') + ->where('draftNotes', 'Updated draft notes') + ->etc() + ); + + /** @var Entry $updatedDraft */ + $updatedDraft = Entry::find() + ->draftId($draft->draftId) + ->siteId($draft->siteId) + ->status(null) + ->one(); + + expect($updatedDraft->title)->toBe('Updated Draft Title') + ->and($updatedDraft->slug)->toBe('updated-draft-title') + ->and($updatedDraft->draftName)->toBe('Renamed Draft') + ->and($updatedDraft->draftNotes)->toBe('Updated draft notes'); + }); + + it('overwrites an existing provisional draft for the same element and user', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $existingDraft */ + $existingDraft = app(Drafts::class)->createDraft($entry, auth()->id(), provisional: true); + $existingDraft->title = 'Existing provisional title'; + ElementsFacade::saveElement($existingDraft); + + $response = postJson( + action([ElementDraftsController::class, 'store']), + elementDraftsControllerPayload($entry, [ + 'provisional' => true, + 'title' => 'Replacement provisional title', + 'slug' => 'replacement-provisional-title', + ]), + ); + + $response->assertOk(); + + /** @var Entry $replacementDraft */ + $replacementDraft = Entry::find() + ->id($response->json('elementId')) + ->drafts() + ->provisionalDrafts() + ->status(null) + ->one(); + + expect($replacementDraft)->not->toBeNull() + ->and($replacementDraft->id)->not->toBe($existingDraft->id) + ->and($replacementDraft->title)->toBe('Replacement provisional title') + ->and($replacementDraft->isProvisionalDraft)->toBeTrue() + ->and(Entry::find()->id($existingDraft->id)->drafts()->status(null)->one())->toBeNull() + ->and( + Entry::find() + ->drafts() + ->provisionalDrafts() + ->draftOf($entry->id) + ->draftCreator(auth()->id()) + ->status(null) + ->count() + )->toBe(1); + }); + + it('drops provisional status when requested', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), provisional: true); + + postJson(action([ElementDraftsController::class, 'store']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + 'dropProvisional' => true, + 'title' => 'Saved Draft Title', + ])->assertOk(); + + /** @var Entry $savedDraft */ + $savedDraft = Entry::find() + ->draftId($draft->draftId) + ->siteId($draft->siteId) + ->status(null) + ->one(); + + expect($savedDraft->isProvisionalDraft)->toBeFalse() + ->and($savedDraft->title)->toBe('Saved Draft Title'); + }); + + it('includes cp editor payload fields on the control panel action route', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + postJson( + cp_url('actions/elements/save-draft'), + elementDraftsControllerPayload($entry, [ + 'title' => 'CP Draft Title', + 'slug' => 'cp-draft-title', + 'draftName' => 'CP Draft', + ]), + )->assertOk() + ->assertJsonPath('title', 'CP Draft Title') + ->assertJsonPath('docTitle', fn (string $docTitle) => str_contains($docTitle, '(CP Draft)')) + ->assertJsonStructure([ + 'previewTargets', + 'previewParamValue', + 'deltaNames', + 'initialDeltaValues', + 'updatedTimestamp', + 'canonicalUpdatedTimestamp', + ]); + }); + + it('forbids saving a peer draft without save permission', function () { + $entryType = EntryType::factory()->create(); + $section = Section::factory()->withEntryTypes($entryType)->create(); + $entry = EntryModel::factory() + ->forSection($section) + ->forEntryType($entryType) + ->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Peer Draft'); + + $viewer = UserModel::factory() + ->withPermissions([ + 'accessCp', + sprintf('editSite:%s', Sites::getPrimarySite()->uid), + sprintf('viewEntries:%s', $section->uid), + sprintf('viewPeerEntryDrafts:%s', $section->uid), + ]) + ->createElement(); + + actingAs($viewer); + + postJson(action([ElementDraftsController::class, 'store']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + 'title' => 'Unauthorized Update', + ])->assertForbidden(); + }); + + it('returns any response resolved by the element request', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + post(action([ElementDraftsController::class, 'store']), [ + 'elementId' => $entry->id, + 'draftId' => 999999, + 'siteId' => $entry->siteId, + ])->assertRedirect($entry->getCpEditUrl()); + }); + + it('returns a failure response when saving the draft fails', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Failing Draft'); + + Event::listen(BeforeSave::class, function (BeforeSave $event) use ($draft) { + if ($event->element->id === $draft->id) { + $event->isValid = false; + } + }); + + postJson(action([ElementDraftsController::class, 'store']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + ])->assertStatus(400) + ->assertJsonPath('message', t('Couldn’t save {type}.', ['type' => t('draft')])); + }); + + it('rechecks save authorization after applying request params', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Guarded Draft'); + + $request = ElementRequest::create('/actions/elements/save-draft', 'POST', [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + ]); + $request->setUserResolver(fn () => auth()->user()); + app()->instance('request', $request); + + $controller = new class($request, app(Drafts::class), app(Elements::class), app(ElementActivity::class)) extends ElementDraftsController + { + private int $canSaveCalls = 0; + + protected function applyParamsToElement(ElementInterface $element): void {} + + protected function canSave(ElementInterface $element, User $user): bool + { + return ++$this->canSaveCalls === 1; + } + }; + + expect(fn () => $controller->store()) + ->toThrow(HttpException::class, 'User not authorized to save this element.'); + }); +}); + +describe('apply', function () { + it('requires authentication', function () { + auth()->logout(); + + postJson(action([ElementDraftsController::class, 'apply']))->assertUnauthorized(); + }); + + it('returns 400 when no draft is identified by the request', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + + postJson(action([ElementDraftsController::class, 'apply']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ])->assertBadRequest(); + }); + + it('returns any response resolved by the element request', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + post(action([ElementDraftsController::class, 'apply']), [ + 'elementId' => $entry->id, + 'draftId' => 999999, + 'siteId' => $entry->siteId, + ])->assertRedirect($entry->getCpEditUrl()); + }); + + it('applies a draft to its canonical element', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Existing Draft'); + + postJson(action([ElementDraftsController::class, 'apply']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + 'title' => 'Applied Draft Title', + 'slug' => 'applied-draft-title', + ])->assertOk() + ->assertJsonPath('message', t('Draft applied.')); + + /** @var Entry $canonical */ + $canonical = Entry::find() + ->id($entry->id) + ->status(null) + ->one(); + + expect($canonical->title)->toBe('Applied Draft Title') + ->and($canonical->slug)->toBe('applied-draft-title') + ->and(Entry::find()->draftId($draft->draftId)->status(null)->one())->toBeNull(); + }); + + it('forbids applying a draft when the user cannot save the canonical element', function () { + $entryType = EntryType::factory()->create(); + $section = Section::factory()->withEntryTypes($entryType)->create(); + $entry = EntryModel::factory() + ->forSection($section) + ->forEntryType($entryType) + ->createElement([ + 'title' => 'Canonical Title', + ]); + + $viewer = UserModel::factory() + ->withPermissions([ + 'accessCp', + sprintf('editSite:%s', Sites::getPrimarySite()->uid), + sprintf('viewEntries:%s', $section->uid), + ]) + ->createElement(); + + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, $viewer->id, name: 'Viewer Draft'); + + actingAs($viewer); + + postJson(action([ElementDraftsController::class, 'apply']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + 'title' => 'Unauthorized Apply', + ])->assertForbidden(); + }); + + it('returns a failure response and preserves the draft when applying fails validation', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Failing Draft'); + + $failedSave = false; + + Event::listen(BeforeSave::class, function (BeforeSave $event) use ($draft, &$failedSave) { + if ($event->element->id === $draft->id && ! $failedSave) { + $event->isValid = false; + $failedSave = true; + } + }); + + postJson(action([ElementDraftsController::class, 'apply']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + 'title' => 'Failed Apply Title', + 'slug' => 'failed-apply-title', + ])->assertBadRequest() + ->assertJsonPath('message', t('Couldn’t apply draft.')); + + /** @var Entry $savedDraft */ + $savedDraft = Entry::find() + ->draftId($draft->draftId) + ->siteId($draft->siteId) + ->status(null) + ->one(); + + /** @var Entry $canonical */ + $canonical = Entry::find() + ->id($entry->id) + ->status(null) + ->one(); + + expect($savedDraft)->not->toBeNull() + ->and($savedDraft->title)->toBe('Failed Apply Title') + ->and($savedDraft->slug)->toBe('failed-apply-title') + ->and($canonical->title)->toBe('Canonical Title') + ->and($canonical->slug)->toBe('canonical-title'); + }); +}); + +describe('destroy', function () { + it('returns any response resolved by the element request', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + post(action([ElementDraftsController::class, 'destroy']), [ + 'elementId' => $entry->id, + 'draftId' => 999999, + 'siteId' => $entry->siteId, + ])->assertRedirect($entry->getCpEditUrl()); + }); + + it('returns 400 when no draft is identified by the request', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + + postJson(action([ElementDraftsController::class, 'destroy']), [ + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ])->assertBadRequest(); + }); + + it('deletes a draft', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Disposable Draft'); + + $response = postJson(action([ElementDraftsController::class, 'destroy']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + ]); + + $response->assertOk() + ->assertJsonPath('message', t('{type} deleted.', ['type' => t('Draft')])); + + expect(Entry::find()->draftId($draft->draftId)->status(null)->one())->toBeNull(); + }); + + it('discards provisional draft changes', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), provisional: true); + + $response = postJson(action([ElementDraftsController::class, 'destroy']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + ]); + + $response->assertOk() + ->assertJsonPath('message', t('Changes discarded.')); + + expect( + Entry::find() + ->drafts() + ->provisionalDrafts() + ->draftOf($entry->id) + ->draftCreator(auth()->id()) + ->status(null) + ->one() + )->toBeNull(); + }); + + it('returns a failure response when deleting the draft fails', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Undeletable Draft'); + + Event::listen(BeforeDelete::class, function (BeforeDelete $event) use ($draft) { + if ($event->element->id === $draft->id) { + $event->isValid = false; + } + }); + + postJson(action([ElementDraftsController::class, 'destroy']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + ])->assertStatus(400) + ->assertJsonPath('message', t('Couldn’t delete {type}.', ['type' => t('draft')])); + + expect(Entry::find()->draftId($draft->draftId)->status(null)->one())->not->toBeNull(); + }); +}); diff --git a/tests/Feature/Http/Controllers/Elements/ElementIndexControllerTest.php b/tests/Feature/Http/Controllers/Elements/ElementIndexControllerTest.php new file mode 100644 index 00000000000..e3526aba24a --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/ElementIndexControllerTest.php @@ -0,0 +1,163 @@ +postIndexAction = fn (string $path, array $payload = []) => postJson( + action([ElementIndexController::class, match ($path) { + 'get-elements' => 'getElements', + 'get-more-elements' => 'getMoreElements', + 'count-elements' => 'countElements', + 'filter-hud' => 'filterHud', + 'element-table-html' => 'elementTableHtml', + }]), + array_merge([ + 'context' => ElementSources::CONTEXT_INDEX, + 'elementType' => Entry::class, + 'source' => '*', + 'viewState' => [ + 'mode' => 'table', + 'static' => false, + ], + ], $payload), + [ + 'Accept' => 'application/json', + ], + ); +}); + +it('requires authentication for get-elements', function () { + auth()->logout(); + + postJson(action([ElementIndexController::class, 'getElements']), [ + 'elementType' => Entry::class, + ])->assertUnauthorized(); +}); + +it('returns element HTML and action metadata for get-elements', function () { + EntryModel::factory()->count(2)->create(); + + ($this->postIndexAction)('get-elements')->assertOk() + ->assertJsonStructure([ + 'html', + 'headHtml', + 'bodyHtml', + 'actionsHeadHtml', + 'actionsBodyHtml', + 'exporters', + ]); +}); + +it('omits action metadata for get-more-elements', function () { + EntryModel::factory()->count(2)->create(); + + ($this->postIndexAction)('get-more-elements')->assertOk() + ->assertJsonMissingPath('actions') + ->assertJsonMissingPath('actionsHeadHtml') + ->assertJsonMissingPath('actionsBodyHtml') + ->assertJsonMissingPath('exporters') + ->assertJsonStructure([ + 'html', + ]); +}); + +it('returns different filtered and unfiltered counts when filters are applied', function () { + EntryModel::factory()->count(2)->create(); + + $entry = Entry::find()->status(null)->orderBy('elements.id')->first(); + + ($this->postIndexAction)('count-elements', [ + 'criteria' => [ + 'id' => [$entry->id], + ], + 'resultSet' => 'filtered', + ])->assertOk() + ->assertJsonPath('resultSet', 'filtered') + ->assertJsonPath('total', 1) + ->assertJsonPath('unfilteredTotal', 2); +}); + +it('accepts the embedded index context for element index routes', function () { + EntryModel::factory()->create(); + + ($this->postIndexAction)('get-elements', [ + 'context' => ElementSources::CONTEXT_EMBEDDED_INDEX, + ])->assertOk(); +}); + +it('returns filter hud html with asset payloads', function () { + ($this->postIndexAction)('filter-hud', [ + 'id' => 'filters', + 'conditionConfig' => [ + 'class' => ElementCondition::class, + 'elementType' => Entry::class, + ], + ])->assertOk() + ->assertJsonPath('hudHtml', fn (string $html) => str_contains($html, 'condition-container')) + ->assertJsonStructure([ + 'headHtml', + 'bodyHtml', + ]); +}); + +it('prefers the current users provisional draft for element table html', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + ]); + + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), provisional: true); + $draft->title = 'Draft Title'; + Elements::saveElement($draft); + + postJson(action([ElementIndexController::class, 'elementTableHtml']), [ + 'context' => ElementSources::CONTEXT_INDEX, + 'elementType' => Entry::class, + 'source' => '*', + 'id' => $entry->id, + 'viewState' => [ + 'mode' => 'table', + 'tableColumns' => ['title'], + ], + ], [ + 'Accept' => 'application/json', + ])->assertOk() + ->assertJsonPath('attributeHtml.title', fn (string $html) => str_contains($html, 'Draft Title')); +}); + +it('preserves the legacy action route contract for get-elements', function () { + EntryModel::factory()->create(); + + postJson('/'.implode('/', array_filter([ + Cms::config()->cpTrigger, + Cms::config()->actionTrigger, + 'element-indexes/get-elements', + ])), [ + 'context' => ElementSources::CONTEXT_INDEX, + 'elementType' => Entry::class, + 'source' => '*', + 'viewState' => [ + 'mode' => 'table', + 'static' => false, + ], + ], [ + 'Accept' => 'application/json', + ])->assertOk() + ->assertJsonStructure([ + 'html', + ]); +}); diff --git a/tests/Feature/Http/Controllers/Elements/ElementIndexSourcesControllerTest.php b/tests/Feature/Http/Controllers/Elements/ElementIndexSourcesControllerTest.php new file mode 100644 index 00000000000..f057fd980fb --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/ElementIndexSourcesControllerTest.php @@ -0,0 +1,82 @@ +postIndexSourceAction = fn (string $path, array $payload = []) => postJson( + action([ElementIndexSourcesController::class, match ($path) { + 'source-path' => 'sourcePath', + 'source-attribute-info' => 'sourceAttributeInfo', + 'get-source-tree-html' => 'getSourceTreeHtml', + }]), + array_merge([ + 'context' => ElementSources::CONTEXT_INDEX, + 'elementType' => Entry::class, + 'source' => '*', + 'viewState' => [ + 'mode' => 'table', + 'static' => false, + ], + ], $payload), + [ + 'Accept' => 'application/json', + ], + ); +}); + +it('returns source attribute info for the selected source', function () { + $response = ($this->postIndexSourceAction)('source-attribute-info'); + + $response->assertOk() + ->assertJsonStructure([ + 'sortOptions', + 'tableColumns', + 'defaultTableColumns', + ]); + + expect($response->json('sortOptions'))->toBeArray() + ->and($response->json('defaultTableColumns'))->toContain('status'); +}); + +it('returns source path info for asset folder steps', function () { + config()->set('filesystems.disks.test-disk', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/element-index-controller-test/test-disk'), + ]); + + $volume = Volume::factory()->create(['fs' => 'disk:test-disk']); + $folder = VolumeFolder::factory()->create([ + 'volumeId' => $volume->id, + 'name' => 'Docs', + 'path' => 'docs/', + ]); + + postJson(action([ElementIndexSourcesController::class, 'sourcePath']), [ + 'context' => ElementSources::CONTEXT_INDEX, + 'elementType' => Asset::class, + 'source' => "volume:$volume->uid", + 'stepKey' => "folder:$folder->uid", + ], [ + 'Accept' => 'application/json', + ])->assertOk() + ->assertJsonPath('sourcePath.0.key', "volume:$volume->uid") + ->assertJsonPath('sourcePath.0.folderId', $folder->id); +}); + +it('returns source tree html', function () { + ($this->postIndexSourceAction)('get-source-tree-html')->assertOk() + ->assertJsonPath('html', fn (string $html) => str_contains($html, 'sources-list')); +}); diff --git a/tests/Feature/Http/Controllers/Elements/ElementRedirectControllerTest.php b/tests/Feature/Http/Controllers/Elements/ElementRedirectControllerTest.php new file mode 100644 index 00000000000..b8af06b4617 --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/ElementRedirectControllerTest.php @@ -0,0 +1,187 @@ +edition = Edition::get(); + $this->tempAssetUploadFs = Cms::config()->tempAssetUploadFs; + $this->entryType = EntryType::factory()->create(); + $this->section = Section::factory()->withEntryTypes($this->entryType)->create([ + 'handle' => 'news', + ]); +}); + +afterEach(function () { + Edition::set($this->edition); + Cms::config()->tempAssetUploadFs = $this->tempAssetUploadFs; +}); + +it('returns redirect responses returned by the element request for id routes', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + get(cp_url("edit/$entry->id-$entry->slug")."?draftId=999999&siteId=$entry->siteId") + ->assertRedirect($entry->getCpEditUrl()); +}); + +it('returns redirect responses returned by the element request for uuid routes', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + get(cp_url("edit/$entry->uid")."?draftId=999999&siteId=$entry->siteId") + ->assertRedirect($entry->getCpEditUrl()); +}); + +it('redirects to non-standard control panel edit urls', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + get(cp_url("edit/$entry->id-$entry->slug")) + ->assertRedirect($entry->getCpEditUrl()); +}); + +it('aborts when the element has no control panel edit url', function () { + config()->set('filesystems.disks.element-redirect-temp-disk', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/element-redirect-controller/temp-disk'), + ]); + Cms::config()->tempAssetUploadFs = 'disk:element-redirect-temp-disk'; + + $volume = Volume::factory()->create(['fs' => 'disk:element-redirect-temp-disk']); + $folder = VolumeFolderModel::factory()->create(['volumeId' => $volume->id]); + $asset = AssetModel::factory()->createElement([ + 'volumeId' => $volume->id, + 'folderId' => $folder->id, + 'filename' => 'temp-file.jpg', + 'uploaderId' => auth()->id(), + ]); + + $this->withoutExceptionHandling(); + + expect(fn () => get(cp_url("edit/$asset->id-test"))) + ->toThrow(HttpException::class, 'The element doesn’t have an edit page.'); +}); + +it('returns inline edit responses for standard control panel edit urls', function () { + $innerField = Field::factory()->create([ + 'name' => 'Inner Text', + 'handle' => 'innerText', + 'type' => PlainText::class, + ]); + + $matrixEntryType = EntryType::factory() + ->withField($innerField) + ->create([ + 'name' => 'Matrix Block', + 'handle' => 'matrixBlock', + 'hasTitleField' => true, + ]); + + $matrixField = Field::factory()->create([ + 'name' => 'Matrix Field', + 'handle' => 'matrixField', + 'type' => Matrix::class, + 'settings' => ['entryTypes' => [$matrixEntryType->id]], + ]); + + $ownerType = EntryType::factory() + ->withField($matrixField) + ->create([ + 'name' => 'Owner', + 'handle' => 'owner', + 'hasTitleField' => true, + ]); + + $section = Section::factory() + ->withEntryTypes($ownerType) + ->create([ + 'handle' => 'owners', + ]); + + $owner = EntryModel::factory() + ->forSection($section) + ->forEntryType($ownerType) + ->createElement([ + 'title' => 'Owner Entry', + 'slug' => 'owner-entry', + ]); + + EntryTypesFacade::refreshEntryTypes(); + FieldsFacade::invalidateCaches(); + FieldsFacade::refreshFields(); + + $matrixField = FieldsFacade::getFieldById($matrixField->id); + /** @var Entry $owner */ + $owner = Entry::find()->id($owner->id)->status(null)->one(); + + $blockUid = fake()->uuid(); + $owner->setFieldValueFromRequest('matrixField', [ + 'entries' => [ + "uid:$blockUid" => [ + 'type' => $matrixEntryType->handle, + 'title' => 'Inline Block', + 'enabled' => true, + 'fields' => [ + 'innerText' => 'Inline block content', + ], + ], + ], + 'sortOrder' => [$blockUid], + ]); + + expect(Elements::saveElement($owner))->toBeTrue(); + + /** @var Entry $entry */ + $entry = Entry::find() + ->fieldId($matrixField->id) + ->ownerId($owner->id) + ->siteId($owner->siteId) + ->status(null) + ->one(); + + expect($entry->getCpEditUrl())->toStartWith(cp_url('edit')); + + get(cp_url("edit/$entry->id-$entry->slug")) + ->assertOk() + ->assertSeeText('Inline Block') + ->assertSee('elements/save', false); +}); diff --git a/tests/Feature/Http/Controllers/Elements/ElementRevisionsControllerTest.php b/tests/Feature/Http/Controllers/Elements/ElementRevisionsControllerTest.php new file mode 100644 index 00000000000..5ba9e5d9b27 --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/ElementRevisionsControllerTest.php @@ -0,0 +1,236 @@ +entryType = EntryType::factory()->create(); + $this->section = Section::factory()->withEntryTypes($this->entryType)->create([ + 'handle' => 'blog', + 'enableVersioning' => true, + ]); +}); + +dataset('elementRevisionRoutes', [ + 'generic revisions route' => [ + fn (Entry $entry) => action([ElementRevisionsController::class, 'index'], [ + 'id' => $entry->id, + 'slug' => "-$entry->slug", + ]), + ], + 'entries revisions route' => [ + fn (Entry $entry) => cp_url(sprintf( + 'entries/%s/%d-%s/revisions', + $entry->getSection()->handle, + $entry->id, + $entry->slug, + )), + ], + 'content revisions route' => [ + fn (Entry $entry) => cp_url(sprintf( + '%s/%d-%s/revisions', + $entry->getSection()->getCpIndexUri(), + $entry->id, + $entry->slug, + )), + ], +]); + +describe('index', function () { + it('requires login for each control panel revisions route', function (Closure $route) { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'My Entry', + 'slug' => 'my-entry', + ]); + + Auth::logout(); + + get($route($entry))->assertRedirectContains('login'); + })->with('elementRevisionRoutes'); + + it('renders the revisions screen for each control panel revisions route', function (Closure $route) { + $timestamp = now()->startOfMinute(); + Date::setTestNow($timestamp); + + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Current Title', + 'slug' => 'current-title', + ]); + + app(Revisions::class)->createRevision($entry, auth()->id(), 'Initial notes'); + + Date::setTestNow($timestamp->copy()->addMinutes(5)); + + $entry->title = 'Updated Title'; + Elements::saveElement($entry); + + Date::setTestNow(); + + get($route($entry)) + ->assertOk() + ->assertSeeText('Revisions for') + ->assertSeeText('Updated Title') + ->assertSee('id="revisions"', false) + ->assertSeeText('Revision 1') + ->assertSeeText('Initial notes'); + })->with('elementRevisionRoutes'); + + it('returns 400 when the element type does not support revisions', function () { + $entryType = EntryType::factory()->create(); + $section = Section::factory()->withEntryTypes($entryType)->create([ + 'handle' => 'plain', + 'enableVersioning' => false, + ]); + + $entry = EntryModel::factory() + ->forSection($section) + ->forEntryType($entryType) + ->createElement([ + 'title' => 'Plain Entry', + 'slug' => 'plain-entry', + ]); + + get(cp_url("revisions/$entry->id-$entry->slug"))->assertBadRequest(); + }); + + it('returns 400 for unpublished drafts', function () { + $draft = app(Entry::class); + $draft->siteId = Sites::getPrimarySite()->id; + $draft->sectionId = $this->section->id; + $draft->typeId = $this->entryType->id; + $draft->title = 'Unpublished Draft'; + $draft->slug = 'unpublished-draft'; + $draft->setAuthorIds([auth()->id()]); + + app(Drafts::class)->saveElementAsDraft($draft, auth()->id(), markAsSaved: false); + + get(cp_url("revisions/$draft->id-$draft->slug"))->assertBadRequest(); + }); +}); + +describe('revert', function () { + it('requires login', function () { + Auth::logout(); + + postJson(action([ElementRevisionsController::class, 'revert']))->assertUnauthorized(); + }); + + it('returns 400 when no revision is identified by the request', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + postJson(action([ElementRevisionsController::class, 'revert']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ])->assertBadRequest(); + }); + + it('forbids reverting a revision when the user cannot save the canonical element', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $revision */ + $revision = Elements::getElementById(app(Revisions::class)->createRevision($entry, auth()->id())); + + $viewer = UserModel::factory() + ->withPermissions([ + 'accessCp', + sprintf('editSite:%s', Sites::getPrimarySite()->uid), + sprintf('viewEntries:%s', $this->section->uid), + ]) + ->createElement(); + + actingAs($viewer); + + postJson(action([ElementRevisionsController::class, 'revert']), [ + 'elementType' => Entry::class, + 'revisionId' => $revision->revisionId, + 'siteId' => $revision->siteId, + ])->assertForbidden(); + }); + + it('reverts a revision to its canonical element and tracks save activity', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Original Title', + 'slug' => 'original-title', + ]); + /** @var Entry $revision */ + $revision = Elements::getElementById(app(Revisions::class)->createRevision($entry, auth()->id(), 'Initial notes')); + + $entry->title = 'Updated Title'; + $entry->slug = 'updated-title'; + Elements::saveElement($entry); + + postJson(action([ElementRevisionsController::class, 'revert']), [ + 'elementType' => Entry::class, + 'revisionId' => $revision->revisionId, + 'siteId' => $revision->siteId, + ])->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', t('{type} reverted to past revision.', ['type' => Entry::displayName()])) + ->where('element.title', 'Original Title') + ->where('element.slug', 'original-title') + ->etc() + ); + + /** @var Entry $canonical */ + $canonical = Entry::find() + ->id($entry->id) + ->siteId($entry->siteId) + ->status(null) + ->one(); + + $activity = DB::table(Table::ELEMENTACTIVITY)->first(); + + expect($canonical->title)->toBe('Original Title') + ->and($canonical->slug)->toBe('original-title') + ->and($activity->elementId)->toBe($entry->id) + ->and($activity->userId)->toBe(auth()->id()) + ->and($activity->draftId)->toBeNull() + ->and($activity->type)->toBe(ElementActivityType::Save->value); + }); +}); diff --git a/tests/Feature/Http/Controllers/Elements/ElementSelectorModalControllerTest.php b/tests/Feature/Http/Controllers/Elements/ElementSelectorModalControllerTest.php new file mode 100644 index 00000000000..6ad43593bec --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/ElementSelectorModalControllerTest.php @@ -0,0 +1,213 @@ +elementIndexHtmlState = new stdClass; + + app()->instance(ElementIndexHtml::class, new readonly class($this->elementIndexHtmlState) extends ElementIndexHtml + { + public function __construct( + private stdClass $state, + ) {} + + public function html(string $elementType, array $config = []): string + { + $this->state->elementType = $elementType; + $this->state->config = $config; + $this->state->sortOptions = $elementType::sortOptions(); + + return '
Modal body
'; + } + }); +}); + +it('requires authentication', function () { + Auth::logout(); + + postJson(action(ElementSelectorModalController::class), [ + 'elementType' => Entry::class, + ])->assertUnauthorized(); +}); + +it('validates missing required payload', function () { + postJson(action(ElementSelectorModalController::class), []) + ->assertUnprocessable() + ->assertJsonValidationErrors(['elementType']); +}); + +it('validates invalid request payloads', function (array $payload, array $errors) { + postJson(action(ElementSelectorModalController::class), $payload) + ->assertUnprocessable() + ->assertJsonValidationErrors($errors); +})->with([ + 'invalid element type' => [[ + 'elementType' => ElementSelectorModalController::class, + ], ['elementType']], + 'invalid show site menu' => [[ + 'elementType' => Entry::class, + 'showSiteMenu' => 'auto', + ], ['showSiteMenu']], + 'invalid sources type' => [[ + 'elementType' => Entry::class, + 'sources' => 'not-an-array', + ], ['sources']], + 'invalid source item type' => [[ + 'elementType' => Entry::class, + 'sources' => [123], + ], ['sources.0']], + 'invalid condition type' => [[ + 'elementType' => Entry::class, + 'condition' => 123, + ], ['condition']], + 'missing condition class' => [[ + 'elementType' => Entry::class, + 'condition' => [], + ], ['condition']], + 'invalid reference element ids' => [[ + 'elementType' => Entry::class, + 'referenceElementId' => 'invalid', + 'referenceElementOwnerId' => 'invalid', + 'referenceElementSiteId' => 'invalid', + ], ['referenceElementId', 'referenceElementOwnerId', 'referenceElementSiteId']], +]); + +it('renders modal HTML with the expected config', function () { + $response = postJson(action(ElementSelectorModalController::class), [ + 'elementType' => Entry::class, + 'context' => ElementSources::CONTEXT_MODAL, + 'showSiteMenu' => '1', + 'sources' => ['*', 'singles'], + ]) + ->assertOk() + ->assertExactJson([ + 'html' => '
Modal body
', + ]); + + expect($response->json('html'))->toBe('
Modal body
') + ->and($this->elementIndexHtmlState->elementType)->toBe(Entry::class) + ->and($this->elementIndexHtmlState->config)->toMatchArray([ + 'class' => 'content', + 'context' => ElementSources::CONTEXT_MODAL, + 'registerJs' => false, + 'showSiteMenu' => '1', + 'showStatusMenu' => true, + 'sources' => ['*', 'singles'], + ]) + ->and(array_keys($this->elementIndexHtmlState->config['statuses']))->toBe(array_keys(Entry::statuses())); +}); + +it('passes the provided context through to the element index html', function () { + postJson(action(ElementSelectorModalController::class), [ + 'elementType' => Entry::class, + 'context' => ElementSources::CONTEXT_INDEX, + ])->assertOk(); + + expect($this->elementIndexHtmlState->config['context'])->toBe(ElementSources::CONTEXT_INDEX); +}); + +it('uses auto for show site menu when it is omitted', function () { + postJson(action(ElementSelectorModalController::class), [ + 'elementType' => Entry::class, + ])->assertOk(); + + expect($this->elementIndexHtmlState->config['showSiteMenu'])->toBe('auto'); +}); + +it('passes null statuses and disables the status menu for element types without statuses', function () { + postJson(action(ElementSelectorModalController::class), [ + 'elementType' => Address::class, + ])->assertOk(); + + expect($this->elementIndexHtmlState->elementType)->toBe(Address::class) + ->and($this->elementIndexHtmlState->config['showStatusMenu'])->toBeFalse() + ->and($this->elementIndexHtmlState->config['statuses'])->toBeNull(); +}); + +it('filters statuses using an in status condition rule', function () { + postJson(action(ElementSelectorModalController::class), [ + 'elementType' => Entry::class, + 'condition' => [ + 'class' => ElementCondition::class, + 'elementType' => Entry::class, + 'conditionRules' => [[ + 'class' => StatusConditionRule::class, + 'operator' => 'in', + 'values' => ['live'], + ]], + ], + ])->assertOk(); + + expect($this->elementIndexHtmlState->config['statuses']->keys()->all())->toBe(['live']); +}); + +it('filters statuses using an excluding status condition rule', function () { + postJson(action(ElementSelectorModalController::class), [ + 'elementType' => Entry::class, + 'condition' => [ + 'class' => ElementCondition::class, + 'elementType' => Entry::class, + 'conditionRules' => [[ + 'class' => StatusConditionRule::class, + 'operator' => 'not in', + 'values' => ['pending'], + ]], + ], + ])->assertOk(); + + expect($this->elementIndexHtmlState->config['statuses']->keys()->all()) + ->toBe(array_values(array_diff(array_keys(Entry::statuses()), ['pending']))); +}); + +it('leaves statuses unchanged when the condition has no status rule', function () { + postJson(action(ElementSelectorModalController::class), [ + 'elementType' => Entry::class, + 'condition' => [ + 'class' => ElementCondition::class, + 'elementType' => Entry::class, + ], + ])->assertOk(); + + expect(array_keys($this->elementIndexHtmlState->config['statuses']))->toBe(array_keys(Entry::statuses())); +}); + +it('activates element index context for folder-only asset selector requests', function () { + config()->set('filesystems.disks.test-disk', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/element-selector-modal-controller-test/test-disk'), + ]); + + $volume = Volume::factory()->create(['fs' => 'disk:test-disk']); + VolumeFolderModel::factory()->create([ + 'volumeId' => $volume->id, + 'name' => 'Docs', + 'path' => 'docs/', + ]); + + postJson(action(ElementSelectorModalController::class), [ + 'elementType' => Asset::class, + 'foldersOnly' => true, + ])->assertOk(); + + expect($this->elementIndexHtmlState->sortOptions)->toBe([ + 'title' => 'Folder', + ]); +}); diff --git a/tests/Feature/Http/Controllers/Elements/ElementSourcesControllerTest.php b/tests/Feature/Http/Controllers/Elements/ElementSourcesControllerTest.php new file mode 100644 index 00000000000..94898baafbc --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/ElementSourcesControllerTest.php @@ -0,0 +1,394 @@ +refreshFields(); +}); + +it('returns fully normalized source customization data', function () { + $primarySite = Sites::getPrimarySite(); + + $field = Field::factory()->create([ + 'name' => 'Preview Field', + 'handle' => 'previewField', + 'type' => Dropdown::class, + 'settings' => [ + 'options' => [ + ['label' => 'Alpha', 'value' => 'alpha'], + ['label' => 'Beta', 'value' => 'beta'], + ], + ], + ]); + + FieldLayoutRecord::factory() + ->forField($field) + ->create([ + 'type' => TestElementSourcesElement::class, + ]); + + $userGroup = UserGroup::factory()->create([ + 'name' => 'Editors', + 'handle' => 'editors', + 'uid' => Str::uuid()->toString(), + ]); + + app(Fields::class)->refreshFields(); + + app(ProjectConfig::class)->set(sprintf('%s.%s', ProjectConfig::PATH_ELEMENT_SOURCES, TestElementSourcesElement::class), [ + [ + 'type' => ElementSources::TYPE_HEADING, + 'heading' => 'Primary Sources', + ], + [ + 'type' => ElementSources::TYPE_NATIVE, + 'key' => 'structured', + 'defaultSort' => 'slug', + ], + [ + 'type' => ElementSources::TYPE_NATIVE, + 'key' => 'fallback', + ], + [ + 'type' => ElementSources::TYPE_CUSTOM, + 'key' => 'custom:existing', + 'label' => 'Existing Custom', + 'defaultSort' => ['field:'.$field->uid, 'desc'], + 'condition' => [ + 'class' => ElementCondition::class, + 'elementType' => TestElementSourcesElement::class, + 'conditionRules' => [], + ], + 'sites' => [$primarySite->uid, 'missing-site'], + 'userGroups' => false, + ], + [ + 'type' => ElementSources::TYPE_CUSTOM, + 'key' => 'custom:false-sites', + 'label' => 'No Sites', + 'sites' => false, + ], + ]); + + app(ProjectConfig::class)->set(sprintf('%s.%s', ProjectConfig::PATH_ELEMENT_SOURCE_PAGES, TestElementSourcesElement::class), [ + 'entries' => ['label' => 'Entries'], + ]); + + $response = postJson(action([ElementSourcesController::class, 'show']), [ + 'elementType' => TestElementSourcesElement::class, + ]); + + $response->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('multiPage', true) + ->where('sources.0.type', ElementSources::TYPE_HEADING) + ->where('sources.0.page', 'Test Elements') + ->where('sources.1.page', 'Test Elements') + ->where('sources.1.sortOptions.0.attr', 'structure') + ->where('sources.1.defaultSort.0', 'slug') + ->where('sources.1.defaultSort.1', 'asc') + ->where('sources.1.tableAttributes.0.0', 'slug') + ->where('sources.1.tableAttributes.0.1', 'Slug') + ->where('sources.2.defaultSort.0', 'id') + ->where('sources.2.defaultSort.1', 'asc') + ->where('sources.3.defaultSort.0', 'field:'.$field->uid) + ->where('sources.3.defaultSort.1', 'desc') + ->where('sources.3.sites.0', $primarySite->uid) + ->where('sources.3.userGroups', []) + ->missing('sources.3.condition') + ->where('sources.4.sites', []) + ->where('pageSettings.entries.label', 'Entries') + ->where('defaultSortOptions.0.attr', 'field:'.$field->uid) + ->where('availableTableAttributes.0.0', 'title') + ->where('customFieldAttributes.0.0', 'field:'.$field->uid) + ->where('customFieldAttributes.0.1', 'Preview Field') + ->where('elementTypeName', 'Test Element') + ->where('userGroups.0.label', t($userGroup->name, category: 'site')) + ->where('userGroups.0.value', $userGroup->uid) + ->etc() + ); + + $payload = $response->json(); + $structuredSortAttrs = array_column($payload['sources'][1]['sortOptions'], 'attr'); + $baseSortAttrs = array_column($payload['baseSortOptions'], 'attr'); + $viewModes = collect($payload['viewModes']); + + expect($payload['sources'][3]['conditionBuilderHtml'])->toContain('condition-container') + ->and($payload['sources'][3]['conditionBuilderJs'])->toContain('Craft.initUiElements') + ->and($payload['sources'][1]['availableTableAttributes'])->toBe([]) + ->and($payload['sources'][3]['tableAttributes'][0])->toBe(['title', 'Test Element']) + ->and($structuredSortAttrs)->toContain('title', 'slug', 'postDate') + ->and($baseSortAttrs)->toContain('id', 'title', 'slug', 'postDate') + ->and($viewModes->contains(fn (array $viewMode) => $viewMode['mode'] === 'table' && is_string($viewMode['iconSvg'])))->toBeTrue() + ->and($payload['conditionBuilderHtml'])->toContain('__SOURCE_KEY__') + ->and($payload['conditionBuilderJs'])->toContain('Craft.initUiElements') + ->and($payload['headHtml'])->toBeString() + ->and($payload['bodyHtml'])->toBeString(); +}); + +it('stores normalized source settings for multi-page sources', function () { + $projectConfig = app(ProjectConfig::class); + + $projectConfig->set(sprintf('%s.%s', ProjectConfig::PATH_ELEMENT_SOURCES, TestElementSourcesElement::class), [ + [ + 'key' => 'native-disabled', + 'type' => ElementSources::TYPE_NATIVE, + 'page' => 'Archived', + 'disabled' => true, + 'tableAttributes' => ['slug'], + ], + ]); + + $response = postJson(action([ElementSourcesController::class, 'store']), [ + 'elementType' => TestElementSourcesElement::class, + 'sourceOrder' => [ + 'native-enabled', + 'custom:new', + 'heading:content', + 'native-disabled', + 'custom:missing', + ], + 'sourcePages' => [ + 'native-enabled' => 'Archived', + 'custom:new' => 'Content', + 'heading:content' => 'Content', + 'native-disabled' => 'Archived', + ], + 'pageSettings' => [ + 'Content' => [ + 'label' => 'Content', + 'description' => '', + ], + 'Archived' => [ + 'label' => 'Archived', + 'description' => null, + ], + ], + 'sources' => [ + 'native-enabled' => [ + 'tableAttributes' => ['', 'slug'], + 'defaultSort' => ['slug', 'desc'], + 'defaultViewMode' => 'cards', + 'enabled' => true, + ], + 'custom:new' => [ + 'label' => 'Fresh Custom', + 'tableAttributes' => ['', 'postDate'], + 'defaultSort' => ['postDate', 'desc'], + 'defaultViewMode' => 'cards', + 'condition' => [ + 'class' => ElementCondition::class, + 'elementType' => TestElementSourcesElement::class, + 'conditionRules' => [], + ], + 'sites' => 'not-an-array', + 'userGroups' => ['group-editors'], + ], + 'heading:content' => [ + 'heading' => 'Content Heading', + ], + ], + ]); + + $response->assertOk() + ->assertJsonPath('message', t('Source settings saved')) + ->assertJsonPath('disabledSourceKeys.0', 'native-disabled'); + + expect(normalizeConfig($projectConfig->get(sprintf('%s.%s', ProjectConfig::PATH_ELEMENT_SOURCES, TestElementSourcesElement::class))))->toBe(normalizeConfig([ + [ + 'type' => ElementSources::TYPE_CUSTOM, + 'key' => 'custom:new', + 'page' => 'Content', + 'tableAttributes' => ['postDate'], + 'defaultSort' => ['postDate', 'desc'], + 'defaultViewMode' => 'cards', + 'label' => 'Fresh Custom', + 'condition' => [ + 'elementType' => TestElementSourcesElement::class, + 'fieldContext' => 'global', + 'class' => ElementCondition::class, + ], + 'sites' => false, + 'userGroups' => ['group-editors'], + ], + [ + 'type' => ElementSources::TYPE_HEADING, + 'key' => 'heading:content', + 'page' => 'Content', + 'heading' => 'Content Heading', + ], + [ + 'type' => ElementSources::TYPE_NATIVE, + 'key' => 'native-enabled', + 'page' => 'Archived', + 'tableAttributes' => ['slug'], + 'defaultSort' => ['slug', 'desc'], + 'defaultViewMode' => 'cards', + 'disabled' => false, + ], + [ + 'type' => ElementSources::TYPE_NATIVE, + 'key' => 'native-disabled', + 'page' => 'Archived', + 'tableAttributes' => ['slug'], + 'disabled' => true, + ], + ])) + ->and(normalizeConfig($projectConfig->get(sprintf('%s.%s', ProjectConfig::PATH_ELEMENT_SOURCE_PAGES, TestElementSourcesElement::class))))->toBe(normalizeConfig([ + 'Content' => ['label' => 'Content'], + 'Archived' => ['label' => 'Archived'], + ])); +}); + +it('stores single-page source settings without page reordering', function () { + $projectConfig = app(ProjectConfig::class); + + $response = postJson(action([ElementSourcesController::class, 'store']), [ + 'elementType' => TestSinglePageElementSourcesElement::class, + 'sourceOrder' => ['native-only'], + 'sources' => [ + 'native-only' => [ + 'tableAttributes' => [], + 'enabled' => false, + ], + ], + ]); + + $response->assertOk() + ->assertJsonPath('disabledSourceKeys.0', 'native-only'); + + expect(normalizeConfig($projectConfig->get(sprintf('%s.%s', ProjectConfig::PATH_ELEMENT_SOURCES, TestSinglePageElementSourcesElement::class))))->toBe(normalizeConfig([ + [ + 'type' => ElementSources::TYPE_NATIVE, + 'key' => 'native-only', + 'tableAttributes' => '-', + 'disabled' => true, + ], + ])) + ->and($projectConfig->get(sprintf('%s.%s', ProjectConfig::PATH_ELEMENT_SOURCE_PAGES, TestSinglePageElementSourcesElement::class)))->toBeNull(); +}); + +function normalizeConfig(mixed $value): mixed +{ + if (! is_array($value)) { + return $value; + } + + if (array_is_list($value)) { + return array_map(normalizeConfig(...), $value); + } + + ksort($value); + + foreach ($value as $key => $nestedValue) { + $value[$key] = normalizeConfig($nestedValue); + } + + return $value; +} + +class TestElementSourcesElement extends Element +{ + #[Override] + public static function displayName(): string + { + return 'Test Element'; + } + + #[Override] + public static function pluralDisplayName(): string + { + return 'Test Elements'; + } + + #[Override] + public static function multiPageSources(): bool + { + return true; + } + + #[Override] + protected static function defineSources(string $context): array + { + return [ + [ + 'heading' => 'Primary Sources', + ], + [ + 'key' => 'structured', + 'label' => 'Structured', + 'structureId' => 1, + ], + [ + 'key' => 'fallback', + 'label' => 'Fallback', + ], + ]; + } + + #[Override] + protected static function defineFieldLayouts(?string $source): array + { + return match ($source) { + 'structured' => [], + 'fallback' => [], + default => app(Fields::class)->getLayoutsByType(static::class)->all(), + }; + } + + #[Override] + protected static function defineTableAttributes(): array + { + return [ + 'title' => ['label' => 'Test Element'], + 'slug' => ['label' => 'Slug'], + 'postDate' => ['label' => 'Post Date'], + ]; + } + + #[Override] + protected static function defineDefaultTableAttributes(string $source): array + { + return match ($source) { + 'structured' => ['slug'], + default => ['title'], + }; + } + + #[Override] + public function getCanonical(bool $anySite = false): ElementInterface + { + return $this; + } +} + +class TestSinglePageElementSourcesElement extends TestElementSourcesElement +{ + #[Override] + public static function multiPageSources(): bool + { + return false; + } +} diff --git a/tests/Feature/Http/Controllers/Elements/ExportElementIndexControllerTest.php b/tests/Feature/Http/Controllers/Elements/ExportElementIndexControllerTest.php index 32209c0d7a9..97becb05aae 100644 --- a/tests/Feature/Http/Controllers/Elements/ExportElementIndexControllerTest.php +++ b/tests/Feature/Http/Controllers/Elements/ExportElementIndexControllerTest.php @@ -9,7 +9,7 @@ use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Entry\Models\Entry as EntryModel; -use CraftCms\Cms\Http\Controllers\Elements\ExportElementIndexController; +use CraftCms\Cms\Http\Controllers\Elements\ElementIndex\ExportElementIndexController; use CraftCms\Cms\User\Elements\User; use Illuminate\Support\Facades\Event; diff --git a/tests/Feature/Http/Controllers/Elements/PreviewElementControllerTest.php b/tests/Feature/Http/Controllers/Elements/PreviewElementControllerTest.php new file mode 100644 index 00000000000..43e24a0e618 --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/PreviewElementControllerTest.php @@ -0,0 +1,146 @@ +createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Preview Draft'); + $draft->title = 'Draft Title'; + Elements::saveElement($draft); + + $request = Mockery::mock(ElementRequest::class); + $request->shouldReceive('element') + ->once() + ->with(['id' => $entry->id], true) + ->andReturn($draft); + $request->shouldReceive('getSigned') + ->once() + ->with('returnUrl', ElementHelper::postEditUrl($draft)) + ->andReturn('entries'); + + HtmlStack::clear(); + + $view = new PreviewElementController($request)->__invoke($entry->id, "-$entry->slug"); + $html = $view->render(); + + expect($view->getData()['title'])->toBe('Draft Title') + ->and($view->getData()['docTitle'])->toContain("($draft->draftName)") + ->and($html)->toContain('new Craft.Preview({') + ->and($html)->toContain(sprintf('elementId: %d', $draft->id)) + ->and($html)->toContain(sprintf('draftId: %d', $draft->draftId)) + ->and($html)->toContain('revisionId: null') + ->and($html)->toContain('redirectUrl: "entries"'); +}); + +it('renders preview pages for provisional drafts using canonical ids in the preview config', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), provisional: true); + $draft->title = 'Edited Title'; + Elements::saveElement($draft); + + $request = Mockery::mock(ElementRequest::class); + $request->shouldReceive('element') + ->once() + ->with(['id' => $entry->id], true) + ->andReturn($draft); + $request->shouldReceive('getSigned') + ->once() + ->with('returnUrl', ElementHelper::postEditUrl($draft)) + ->andReturn(ElementHelper::postEditUrl($draft)); + + HtmlStack::clear(); + + $view = new PreviewElementController($request)->__invoke($entry->id, "-$entry->slug"); + $html = $view->render(); + + expect($view->getData()['title'])->toBe('Edited Title') + ->and($view->getData()['docTitle'])->toContain('Edited') + ->and($html)->toContain(sprintf('elementId: %d', $entry->id)) + ->and($html)->toContain('draftId: null') + ->and($html)->toContain( + sprintf('redirectUrl: %s', json_encode(ElementHelper::postEditUrl($draft), JSON_THROW_ON_ERROR)), + ); +}); + +it('renders preview pages for revisions', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Current Title', + 'slug' => 'current-title', + ]); + + $revisionElementId = app(Revisions::class)->createRevision($entry, auth()->id()); + $revision = Elements::getElementById($revisionElementId, Entry::class, $entry->siteId); + + $request = Mockery::mock(ElementRequest::class); + $request->shouldReceive('element') + ->once() + ->with(['id' => $entry->id], true) + ->andReturn($revision); + $request->shouldReceive('getSigned') + ->once() + ->with('returnUrl', ElementHelper::postEditUrl($revision)) + ->andReturn(ElementHelper::postEditUrl($revision)); + + HtmlStack::clear(); + + $view = new PreviewElementController($request)->__invoke($entry->id, "-$entry->slug"); + $html = $view->render(); + + expect($view->getData()['title'])->toBe($revision->title) + ->and($view->getData()['docTitle'])->toContain($revision->getRevisionLabel()) + ->and($html)->toContain(sprintf('elementId: %d', $revision->id)) + ->and($html)->toContain('draftId: null') + ->and($html)->toContain(sprintf('revisionId: %d', $revision->revisionId)); +}); + +it('redirects to the canonical edit url when the requested draft is invalid', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + $redirect = redirect($entry->getCpEditUrl()); + + $request = Mockery::mock(ElementRequest::class); + $request->shouldReceive('element') + ->once() + ->with(['id' => $entry->id], true) + ->andReturn($redirect); + + $response = new PreviewElementController($request)->__invoke($entry->id, "-$entry->slug"); + + expect($response)->toBe($redirect); +}); + +it('returns a bad request when no element matches the preview request', function () { + get(action(PreviewElementController::class, [ + 'id' => 999999, + 'slug' => '-missing', + ]))->assertBadRequest(); +}); diff --git a/tests/Feature/Http/Controllers/Elements/SaveElementControllerTest.php b/tests/Feature/Http/Controllers/Elements/SaveElementControllerTest.php new file mode 100644 index 00000000000..8d682ef1893 --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/SaveElementControllerTest.php @@ -0,0 +1,740 @@ +entryType = EntryType::factory()->create(); + $this->section = Section::factory()->withEntryTypes($this->entryType)->create([ + 'handle' => 'news', + 'enableVersioning' => true, + ]); +}); + +function createSaveElementMatrixFixture(): array +{ + $innerField = Field::factory()->create([ + 'name' => 'Inner Text', + 'handle' => 'innerText', + 'type' => PlainText::class, + ]); + + $matrixEntryType = EntryType::factory() + ->withField($innerField) + ->create([ + 'name' => 'Matrix Block', + 'handle' => 'matrixBlock', + 'hasTitleField' => true, + ]); + + $matrixField = Field::factory()->create([ + 'name' => 'Matrix Field', + 'handle' => 'matrixField', + 'type' => Matrix::class, + 'settings' => ['entryTypes' => [$matrixEntryType->id]], + ]); + + $ownerType = EntryType::factory() + ->withField($matrixField) + ->create([ + 'name' => 'Owner', + 'handle' => 'owner', + 'hasTitleField' => true, + ]); + + $section = Section::factory() + ->withEntryTypes($ownerType) + ->create([ + 'handle' => 'owners', + ]); + + $owner = EntryModel::factory() + ->forSection($section) + ->forEntryType($ownerType) + ->createElement([ + 'title' => 'Owner Entry', + 'slug' => Str::slug('Owner Entry '.Str::random(6)), + ]); + + EntryTypesFacade::refreshEntryTypes(); + FieldsFacade::invalidateCaches(); + + $matrixField = FieldsFacade::getFieldById($matrixField->id); + /** @var Entry $owner */ + $owner = Entry::find()->id($owner->id)->status(null)->one(); + + $blockUid = Str::uuid()->toString(); + $owner->setFieldValueFromRequest('matrixField', [ + 'entries' => [ + "uid:$blockUid" => [ + 'type' => $matrixEntryType->handle, + 'title' => 'Block 1', + 'enabled' => true, + 'fields' => [ + 'innerText' => 'Canonical matrix value', + ], + ], + ], + 'sortOrder' => [$blockUid], + ]); + + expect(Elements::saveElement($owner))->toBeTrue(); + + $owner = Entry::find()->id($owner->id)->status(null)->one(); + $canonicalBlock = $owner->getFieldValue('matrixField')->status(null)->one(); + $ownerDraft = app(Drafts::class)->createDraft($owner, auth()->id(), name: 'Owner Draft'); + $draftBlock = app(Drafts::class)->createDraft($canonicalBlock, auth()->id(), name: 'Block Draft'); + + /** @var Entry $ownerDraft */ + $ownerDraft = Entry::find() + ->draftId($ownerDraft->draftId) + ->siteId($ownerDraft->siteId) + ->status(null) + ->one(); + $draftBlock = Entry::find() + ->draftId($draftBlock->draftId) + ->fieldId($matrixField->id) + ->ownerId($owner->id) + ->drafts() + ->siteId($draftBlock->siteId) + ->status(null) + ->one(); + + return [ + 'field' => $matrixField, + 'owner' => $owner, + 'ownerDraft' => $ownerDraft, + 'canonicalBlock' => $canonicalBlock, + 'draftBlock' => $draftBlock, + ]; +} + +describe('store', function () { + it('requires authentication', function () { + Auth::logout(); + + postJson(action([SaveElementController::class, 'store']))->assertUnauthorized(); + }); + + it('returns any response resolved by the element request', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + post(action([SaveElementController::class, 'store']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'draftId' => 999999, + ])->assertRedirect($entry->getCpEditUrl()); + }); + + it('returns 400 when no element is identified by the request', function () { + postJson(action([SaveElementController::class, 'store']), [ + 'elementType' => Entry::class, + 'siteId' => Sites::getPrimarySite()->id, + ])->assertBadRequest(); + }); + + it('returns 400 for drafts', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $draft */ + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), name: 'Existing Draft'); + + postJson(action([SaveElementController::class, 'store']), [ + 'elementType' => Entry::class, + 'draftId' => $draft->draftId, + 'siteId' => $draft->siteId, + ])->assertBadRequest(); + }); + + it('returns 400 for revisions', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + $revisionId = app(Revisions::class)->createRevision($entry, auth()->id()); + + postJson(action([SaveElementController::class, 'store']), [ + 'elementType' => Entry::class, + 'revisionId' => $revisionId, + 'siteId' => $entry->siteId, + ])->assertBadRequest(); + }); + + it('forbids saving when the user cannot save the element', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + $viewer = UserModel::factory() + ->withPermissions([ + 'accessCp', + sprintf('editSite:%s', Sites::getPrimarySite()->uid), + sprintf('viewEntries:%s', $this->section->uid), + sprintf('viewPeerEntries:%s', $this->section->uid), + ]) + ->createElement(); + + actingAs($viewer); + + postJson(action([SaveElementController::class, 'store']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'title' => 'Updated Title', + ])->assertForbidden(); + }); + + it('returns 500 when it cannot acquire a lock for an existing element', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + $lock = Cache::lock("element:$entry->id", 15); + + expect($lock->get())->toBeTrue(); + + try { + postJson(action([SaveElementController::class, 'store']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'title' => 'Updated Title', + ])->assertInternalServerError(); + } finally { + $lock->release(); + } + }); + + it('returns a failure response when saving fails', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + $entry->errors()->add('title', 'Title is invalid.'); + + app()->instance(ElementsService::class, new class(app(ElementPlaceholders::class)) extends ElementsService + { + public function saveElement( + ElementInterface $element, + bool $runValidation = true, + bool $propagate = true, + ?bool $updateSearchIndex = null, + bool $forceTouch = false, + ?bool $crossSiteValidate = false, + bool $saveContent = false, + ): bool { + return false; + } + }); + + postJson(action([SaveElementController::class, 'store']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'title' => 'Updated Title', + ])->assertBadRequest() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', mb_ucfirst(t('Couldn’t save {type}.', [ + 'type' => Entry::lowerDisplayName(), + ]))) + ->where('modelName', 'element') + ->etc() + ); + }); + + it('returns a failure response when saving throws an unsupported site exception', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + app()->instance(ElementsService::class, new class(app(ElementPlaceholders::class)) extends ElementsService + { + public function saveElement( + ElementInterface $element, + bool $runValidation = true, + bool $propagate = true, + ?bool $updateSearchIndex = null, + bool $forceTouch = false, + ?bool $crossSiteValidate = false, + bool $saveContent = false, + ): bool { + throw new UnsupportedSiteException($element, 999999, 'Unsupported site.'); + } + }); + + postJson(action([SaveElementController::class, 'store']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'title' => 'Updated Title', + ])->assertBadRequest() + ->assertJson(fn (AssertableJson $json) => $json + ->where('errors.siteId.0', 'Unsupported site.') + ->etc() + ); + }); + + it('saves canonical elements, tracks save activity, deletes provisional drafts, and cross-site validates for multisite requests', function () { + Site::factory()->create(['handle' => 'secondary']); + + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + app(Drafts::class)->createDraft($entry, auth()->id(), provisional: true); + + $elements = new class(app(ElementPlaceholders::class)) extends ElementsService + { + public ?bool $capturedCrossSiteValidate = null; + + public function saveElement( + ElementInterface $element, + bool $runValidation = true, + bool $propagate = true, + ?bool $updateSearchIndex = null, + bool $forceTouch = false, + ?bool $crossSiteValidate = false, + bool $saveContent = false, + ): bool { + $this->capturedCrossSiteValidate = $crossSiteValidate; + + return parent::saveElement( + $element, + $runValidation, + $propagate, + $updateSearchIndex, + $forceTouch, + $crossSiteValidate, + $saveContent, + ); + } + }; + + app()->instance(ElementsService::class, $elements); + + $response = postJson(action([SaveElementController::class, 'store']), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'title' => 'Updated Title', + 'slug' => 'updated-title', + ]); + + $response->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', t('{type} saved.', ['type' => Entry::displayName()])) + ->where('modelName', 'element') + ->where('element.id', $entry->id) + ->where('element.title', 'Updated Title') + ->where('element.slug', 'updated-title') + ->etc() + ); + + expect($elements->capturedCrossSiteValidate)->toBeTrue() + ->and(Entry::find()->id($entry->id)->status(null)->one()->title)->toBe('Updated Title') + ->and( + Entry::find() + ->drafts() + ->provisionalDrafts() + ->draftOf($entry->id) + ->draftCreator(auth()->id()) + ->status(null) + ->count() + )->toBe(0) + ->and(DB::table(Table::ELEMENTACTIVITY) + ->where('elementId', $entry->id) + ->where('userId', auth()->id()) + ->where('type', ElementActivityType::Save->value) + ->exists()) + ->toBeTrue(); + }); + + it('marks nested elements to update their owner search index before saving', function () { + $fixture = createSaveElementMatrixFixture(); + + $elements = new class(app(ElementPlaceholders::class)) extends ElementsService + { + public bool $capturedNestedOwnerIndexFlag = false; + + public function saveElement( + ElementInterface $element, + bool $runValidation = true, + bool $propagate = true, + ?bool $updateSearchIndex = null, + bool $forceTouch = false, + ?bool $crossSiteValidate = false, + bool $saveContent = false, + ): bool { + if ($element instanceof NestedElementInterface) { + $this->capturedNestedOwnerIndexFlag = $element->updateSearchIndexForOwner; + } + + return parent::saveElement( + $element, + $runValidation, + $propagate, + $updateSearchIndex, + $forceTouch, + $crossSiteValidate, + $saveContent, + ); + } + }; + + app()->instance(ElementsService::class, $elements); + + postJson(action([SaveElementController::class, 'store']), [ + 'elementType' => Entry::class, + 'elementId' => $fixture['canonicalBlock']->id, + 'siteId' => $fixture['canonicalBlock']->siteId, + 'ownerId' => $fixture['owner']->id, + 'fieldId' => $fixture['field']->id, + 'title' => 'Updated Block Title', + ])->assertOk(); + + expect($elements->capturedNestedOwnerIndexFlag)->toBeTrue(); + }); + + it('redirects to a new draft when add another is requested', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + $draftCount = Entry::find()->drafts()->status(null)->count(); + + post(cp_url('actions/elements/save'), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'title' => 'Updated Title', + 'addAnother' => 1, + ])->assertRedirect(); + + expect(Entry::find()->drafts()->status(null)->count())->toBe($draftCount + 1); + }); +}); + +describe('storeForDerivative', function () { + it('requires authentication', function () { + Auth::logout(); + + postJson(action([SaveElementController::class, 'storeForDerivative']))->assertUnauthorized(); + }); + + it('returns 400 when no new owner is identified', function () { + postJson(action([SaveElementController::class, 'storeForDerivative']), [ + 'elementType' => Entry::class, + ])->assertBadRequest(); + }); + + it('returns 400 when the element is not a nested draft derivative', function () { + $fixture = createSaveElementMatrixFixture(); + + postJson(action([SaveElementController::class, 'storeForDerivative']), [ + 'elementType' => Entry::class, + 'elementId' => $fixture['canonicalBlock']->id, + 'siteId' => $fixture['canonicalBlock']->siteId, + 'ownerId' => $fixture['owner']->id, + 'fieldId' => $fixture['field']->id, + 'newOwnerId' => $fixture['ownerDraft']->id, + ])->assertBadRequest(); + }); + + it('returns 400 when the new owner is canonical', function () { + $fixture = createSaveElementMatrixFixture(); + + DB::table(Table::ENTRIES) + ->where('id', $fixture['draftBlock']->id) + ->update(['primaryOwnerId' => $fixture['owner']->id]); + + postJson(action([SaveElementController::class, 'storeForDerivative']), [ + 'elementType' => Entry::class, + 'elementId' => $fixture['draftBlock']->id, + 'siteId' => $fixture['draftBlock']->siteId, + 'ownerId' => $fixture['ownerDraft']->id, + 'fieldId' => $fixture['field']->id, + 'draftId' => $fixture['draftBlock']->draftId, + 'newOwnerId' => $fixture['owner']->id, + ])->assertBadRequest(); + }); + + it('returns 400 when the new owner does not share the nested element primary owner', function () { + $fixture = createSaveElementMatrixFixture(); + + DB::table(Table::ENTRIES) + ->where('id', $fixture['draftBlock']->id) + ->update(['primaryOwnerId' => $fixture['owner']->id]); + + $otherFixture = createSaveElementMatrixFixture(); + + postJson(action([SaveElementController::class, 'storeForDerivative']), [ + 'elementType' => Entry::class, + 'elementId' => $fixture['draftBlock']->id, + 'siteId' => $fixture['draftBlock']->siteId, + 'ownerId' => $fixture['ownerDraft']->id, + 'fieldId' => $fixture['field']->id, + 'draftId' => $fixture['draftBlock']->draftId, + 'newOwnerId' => $otherFixture['ownerDraft']->id, + ])->assertBadRequest(); + }); + + it('forbids saving when the derivative owner cannot be saved', function () { + $fixture = createSaveElementMatrixFixture(); + + DB::table(Table::ENTRIES) + ->where('id', $fixture['draftBlock']->id) + ->update(['primaryOwnerId' => $fixture['owner']->id]); + + Gate::before(function ($user, string $ability, array $arguments) use ($fixture) { + if ($ability === 'save' && ($arguments[0]->id ?? null) === $fixture['ownerDraft']->id) { + return false; + } + + return null; + }); + + postJson(action([SaveElementController::class, 'storeForDerivative']), [ + 'elementType' => Entry::class, + 'elementId' => $fixture['draftBlock']->id, + 'siteId' => $fixture['draftBlock']->siteId, + 'ownerId' => $fixture['ownerDraft']->id, + 'fieldId' => $fixture['field']->id, + 'draftId' => $fixture['draftBlock']->draftId, + 'newOwnerId' => $fixture['ownerDraft']->id, + ])->assertForbidden(); + }); + + it('returns a failure response when saving the derivative fails', function () { + $fixture = createSaveElementMatrixFixture(); + + DB::table(Table::ENTRIES) + ->where('id', $fixture['draftBlock']->id) + ->update(['primaryOwnerId' => $fixture['owner']->id]); + + $elements = new class(app(ElementPlaceholders::class)) extends ElementsService + { + public int $saveCalls = 0; + + public function saveElement( + ElementInterface $element, + bool $runValidation = true, + bool $propagate = true, + ?bool $updateSearchIndex = null, + bool $forceTouch = false, + ?bool $crossSiteValidate = false, + bool $saveContent = false, + ): bool { + $this->saveCalls++; + + if ($this->saveCalls === 1) { + return true; + } + + $element->errors()->add('title', 'Title is invalid.'); + + return false; + } + }; + + app()->instance(ElementsService::class, $elements); + + postJson(action([SaveElementController::class, 'storeForDerivative']), [ + 'elementType' => Entry::class, + 'elementId' => $fixture['draftBlock']->id, + 'siteId' => $fixture['draftBlock']->siteId, + 'ownerId' => $fixture['ownerDraft']->id, + 'fieldId' => $fixture['field']->id, + 'draftId' => $fixture['draftBlock']->draftId, + 'newOwnerId' => $fixture['ownerDraft']->id, + 'title' => 'Updated Block Title', + ])->assertBadRequest() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', mb_ucfirst(t('Couldn’t save {type}.', [ + 'type' => Entry::lowerDisplayName(), + ]))) + ->etc() + ); + }); + + it('returns a failure response when saving the derivative throws an unsupported site exception', function () { + $fixture = createSaveElementMatrixFixture(); + + DB::table(Table::ENTRIES) + ->where('id', $fixture['draftBlock']->id) + ->update(['primaryOwnerId' => $fixture['owner']->id]); + + $elements = new class(app(ElementPlaceholders::class)) extends ElementsService + { + public int $saveCalls = 0; + + public function saveElement( + ElementInterface $element, + bool $runValidation = true, + bool $propagate = true, + ?bool $updateSearchIndex = null, + bool $forceTouch = false, + ?bool $crossSiteValidate = false, + bool $saveContent = false, + ): bool { + $this->saveCalls++; + + if ($this->saveCalls === 1) { + return true; + } + + throw new UnsupportedSiteException($element, 999999, 'Unsupported site.'); + } + }; + + app()->instance(ElementsService::class, $elements); + + postJson(action([SaveElementController::class, 'storeForDerivative']), [ + 'elementType' => Entry::class, + 'elementId' => $fixture['draftBlock']->id, + 'siteId' => $fixture['draftBlock']->siteId, + 'ownerId' => $fixture['ownerDraft']->id, + 'fieldId' => $fixture['field']->id, + 'draftId' => $fixture['draftBlock']->draftId, + 'newOwnerId' => $fixture['ownerDraft']->id, + 'title' => 'Updated Block Title', + ])->assertBadRequest() + ->assertJson(fn (AssertableJson $json) => $json + ->where('errors.siteId.0', 'Unsupported site.') + ->etc() + ); + }); + + it('saves nested draft elements for derivative owners and removes their draft data', function () { + $fixture = createSaveElementMatrixFixture(); + + DB::table(Table::ENTRIES) + ->where('id', $fixture['draftBlock']->id) + ->update(['primaryOwnerId' => $fixture['owner']->id]); + + $sortOrder = DB::table(Table::ELEMENTS_OWNERS) + ->where('elementId', $fixture['draftBlock']->id) + ->where('ownerId', $fixture['owner']->id) + ->value('sortOrder'); + + $draftRowExists = DB::table(Table::DRAFTS) + ->where('id', $fixture['draftBlock']->draftId) + ->exists(); + + expect($draftRowExists)->toBeTrue(); + + $response = postJson(action([SaveElementController::class, 'storeForDerivative']), [ + 'elementType' => Entry::class, + 'elementId' => $fixture['draftBlock']->id, + 'siteId' => $fixture['draftBlock']->siteId, + 'ownerId' => $fixture['ownerDraft']->id, + 'fieldId' => $fixture['field']->id, + 'draftId' => $fixture['draftBlock']->draftId, + 'newOwnerId' => $fixture['ownerDraft']->id, + 'title' => 'Updated Block Title', + ]); + + $response->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', t('{type} saved.', ['type' => Entry::displayName()])) + ->where('modelName', 'element') + ->where('element.id', $fixture['draftBlock']->id) + ->where('element.title', 'Updated Block Title') + ->etc() + ); + + /** @var Entry $savedBlock */ + $savedBlock = Entry::find() + ->id($fixture['draftBlock']->id) + ->status(null) + ->one(); + + expect($savedBlock->draftId)->toBeNull() + ->and($savedBlock->getOwnerId())->toBe($fixture['ownerDraft']->id) + ->and($savedBlock->getPrimaryOwnerId())->toBe($fixture['ownerDraft']->id) + ->and($savedBlock->title)->toBe('Updated Block Title') + ->and(DB::table(Table::ELEMENTS_OWNERS) + ->where('elementId', $savedBlock->id) + ->where('ownerId', $fixture['ownerDraft']->id) + ->value('sortOrder')) + ->toBe($sortOrder) + ->and(DB::table(Table::DRAFTS) + ->where('id', $fixture['draftBlock']->draftId) + ->exists()) + ->toBeFalse(); + }); +}); diff --git a/tests/Feature/Http/Controllers/Elements/SaveElementIndexElementsControllerTest.php b/tests/Feature/Http/Controllers/Elements/SaveElementIndexElementsControllerTest.php new file mode 100644 index 00000000000..09f6a2706a4 --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/SaveElementIndexElementsControllerTest.php @@ -0,0 +1,202 @@ +postSaveElements = fn (array $payload = []) => postJson( + action(SaveElementIndexElementsController::class), + array_merge([ + 'elementType' => Entry::class, + ], $payload), + ); +}); + +it('requires authentication', function () { + auth()->logout(); + + postJson(action(SaveElementIndexElementsController::class), [ + 'elementType' => Entry::class, + ])->assertUnauthorized(); +}); + +it('rejects requests without element data', function () { + $entry = EntryModel::factory()->createElement(); + + ($this->postSaveElements)([ + 'siteId' => $entry->siteId, + 'namespace' => 'elementindex-test', + ])->assertStatus(422) + ->assertJsonPath('message', 'The elementindex-test field is required.'); +}); + +it('rejects requests without valid element ids', function () { + $entry = EntryModel::factory()->createElement(); + + ($this->postSaveElements)([ + 'siteId' => $entry->siteId, + 'namespace' => 'elementindex-test', + 'elementindex-test' => [ + 'element-999999' => [ + 'title' => 'After Save', + ], + ], + ])->assertStatus(422) + ->assertJsonPath('message', 'No valid element IDs provided.'); +}); + +it('aggregates validation errors when saving inline-edited elements', function () { + $field = Field::factory()->create([ + 'handle' => 'requiredField', + 'type' => PlainText::class, + ]); + + $firstEntry = EntryModel::factory() + ->withFieldLayout(FieldLayout::factory()->forField($field, true)) + ->createElement(); + $secondEntry = EntryModel::factory() + ->withFieldLayout(FieldLayout::factory()->forField($field, true)) + ->createElement(); + + ($this->postSaveElements)([ + 'siteId' => $firstEntry->siteId, + 'namespace' => 'elementindex-test', + 'elementindex-test' => [ + "element-$firstEntry->id" => [ + 'fields' => [ + 'requiredField' => '', + ], + ], + "element-$secondEntry->id" => [ + 'fields' => [ + 'requiredField' => '', + ], + ], + ], + ])->assertOk() + ->assertJsonPath("errors.$firstEntry->id.requiredField.0", fn (string $message) => $message !== '') + ->assertJsonPath("errors.$secondEntry->id.requiredField.0", fn (string $message) => $message !== ''); +}); + +it('saves inline-edited elements in a batch', function () { + $firstEntry = EntryModel::factory()->createElement([ + 'title' => 'First Before Save', + ]); + $secondEntry = EntryModel::factory()->createElement([ + 'title' => 'Second Before Save', + ]); + + ($this->postSaveElements)([ + 'siteId' => $firstEntry->siteId, + 'namespace' => 'elementindex-test', + 'elementindex-test' => [ + "element-$firstEntry->id" => [ + 'title' => 'First After Save', + ], + "element-$secondEntry->id" => [ + 'title' => 'Second After Save', + ], + ], + ])->assertOk(); + + expect(Entry::find()->id($firstEntry->id)->status(null)->one()?->title)->toBe('First After Save') + ->and(Entry::find()->id($secondEntry->id)->status(null)->one()?->title)->toBe('Second After Save'); +}); + +it('rolls back prior saves when a later element fails', function () { + $firstEntry = EntryModel::factory()->createElement([ + 'title' => 'First Before Save', + ]); + $secondEntry = EntryModel::factory()->createElement([ + 'title' => 'Second Before Save', + ]); + + app()->instance(Elements::class, new class(app(ElementPlaceholders::class), $secondEntry->id) extends Elements + { + public function __construct( + ElementPlaceholders $placeholders, + private readonly int $failingElementId, + ) { + parent::__construct($placeholders); + } + + public function saveElement( + ElementInterface $element, + bool $runValidation = true, + bool $propagate = true, + ?bool $updateSearchIndex = null, + bool $forceTouch = false, + ?bool $crossSiteValidate = false, + bool $saveContent = false, + ): bool { + if ($element->id === $this->failingElementId) { + return false; + } + + return parent::saveElement( + $element, + $runValidation, + $propagate, + $updateSearchIndex, + $forceTouch, + $crossSiteValidate, + $saveContent, + ); + } + }); + + ($this->postSaveElements)([ + 'siteId' => $firstEntry->siteId, + 'namespace' => 'elementindex-test', + 'elementindex-test' => [ + "element-$firstEntry->id" => [ + 'title' => 'First After Save', + ], + "element-$secondEntry->id" => [ + 'title' => 'Second After Save', + ], + ], + ])->assertServerError(); + + expect(Entry::find()->id($firstEntry->id)->status(null)->one()?->title)->toBe('First Before Save') + ->and(Entry::find()->id($secondEntry->id)->status(null)->one()?->title)->toBe('Second Before Save'); +}); + +it('preserves the legacy action route contract for save-elements', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Before Save', + ]); + + postJson('/'.implode('/', array_filter([ + Cms::config()->cpTrigger, + Cms::config()->actionTrigger, + 'element-indexes/save-elements', + ])), [ + 'elementType' => Entry::class, + 'siteId' => $entry->siteId, + 'namespace' => 'elementindex-test', + 'elementindex-test' => [ + "element-$entry->id" => [ + 'title' => 'After Save', + ], + ], + ])->assertOk(); + + expect(Entry::find()->id($entry->id)->status(null)->one()?->title)->toBe('After Save'); +}); diff --git a/tests/Feature/Http/Controllers/Elements/SearchControllerTest.php b/tests/Feature/Http/Controllers/Elements/SearchControllerTest.php new file mode 100644 index 00000000000..6d0aef705de --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/SearchControllerTest.php @@ -0,0 +1,354 @@ +useFullText = false; + } + + $this->entryType = EntryTypeModel::factory() + ->withFieldLayout() + ->create([ + 'hasTitleField' => true, + ]); + + $this->section = SectionModel::factory() + ->withEntryTypes($this->entryType) + ->create(); + + actingAs(User::findOne()); +}); + +it('requires authentication', function () { + Auth::logout(); + + postJson(action(SearchController::class), [ + 'elementType' => Entry::class, + 'search' => 'Alpha', + ])->assertUnauthorized(); +}); + +it('validates the required payload', function () { + postJson(action(SearchController::class), []) + ->assertUnprocessable() + ->assertJsonValidationErrors(['search']); +}); + +it('validates invalid request payloads', function (array $payload, array $errors) { + postJson(action(SearchController::class), $payload) + ->assertUnprocessable() + ->assertJsonValidationErrors($errors); +})->with([ + 'invalid element type' => [[ + 'elementType' => SearchController::class, + 'search' => 'Alpha', + ], ['elementType']], + 'invalid criteria type' => [[ + 'elementType' => Entry::class, + 'search' => 'Alpha', + 'criteria' => 'invalid', + ], ['criteria']], + 'invalid exclude ids' => [[ + 'elementType' => Entry::class, + 'search' => 'Alpha', + 'excludeIds' => ['invalid'], + ], ['excludeIds.0']], + 'invalid condition type' => [[ + 'elementType' => Entry::class, + 'search' => 'Alpha', + 'condition' => 123, + ], ['condition']], + 'missing condition class' => [[ + 'elementType' => Entry::class, + 'search' => 'Alpha', + 'condition' => [], + ], ['condition']], + 'invalid reference element ids' => [[ + 'elementType' => Entry::class, + 'search' => 'Alpha', + 'referenceElementId' => 'invalid', + 'referenceElementOwnerId' => 'invalid', + 'referenceElementSiteId' => 'invalid', + ], ['referenceElementId', 'referenceElementOwnerId', 'referenceElementSiteId']], +]); + +it('returns an empty result set when nothing matches', function () { + $entry = EntryModel::factory() + ->indexed() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement(['title' => 'Something Else Entirely']); + + postJson(action(SearchController::class), [ + 'elementType' => Entry::class, + 'search' => 'No Matches Here', + ]) + ->assertOk() + ->assertExactJson([ + 'elements' => [], + 'exactMatch' => false, + ]); +}); + +it('applies sanitized criteria to the query', function () { + $matchingEntry = EntryModel::factory() + ->indexed() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement(['title' => 'Criteria Target']); + + $otherEntry = EntryModel::factory() + ->indexed() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement(['title' => 'Criteria Target']); + + $response = postJson(action(SearchController::class), [ + 'elementType' => Entry::class, + 'search' => 'Criteria Target', + 'criteria' => [ + 'id' => [$matchingEntry->id], + 'where' => ['id' => 999999], + ], + ]) + ->assertOk(); + + expect($response->json('elements'))->toHaveCount(1) + ->and($response->json('elements.0.id'))->toBe($matchingEntry->id) + ->and($response->json('elements.0.html'))->toContain('chromeless') + ->and($response->json('elements.0.html'))->toContain('Criteria Target') + ->and($response->json('exactMatch'))->toBeTrue(); +}); + +it('marks exact matches and sorts excluded results last', function () { + $exactIncluded = EntryModel::factory() + ->indexed() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement(['title' => 'Alpha']); + + $partialMatch = EntryModel::factory() + ->indexed() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement(['title' => 'Alpha Beta']); + + $exactExcluded = EntryModel::factory() + ->indexed() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement(['title' => 'Alpha']); + + $response = postJson(action(SearchController::class), [ + 'elementType' => Entry::class, + 'search' => 'Alpha', + 'excludeIds' => [$exactExcluded->id], + ]) + ->assertOk(); + + expect(collect($response->json('elements'))->pluck('id')->all()) + ->toBe([$exactIncluded->id, $partialMatch->id, $exactExcluded->id]) + ->and($response->json('elements.0.exclude'))->toBeFalse() + ->and($response->json('elements.1.exclude'))->toBeFalse() + ->and($response->json('elements.2.exclude'))->toBeTrue() + ->and($response->json('exactMatch'))->toBeTrue(); +}); + +it('applies element conditions to the query', function () { + $matchingEntry = EntryModel::factory() + ->indexed() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement(['title' => 'Conditional Result']); + + $otherEntry = EntryModel::factory() + ->indexed() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement(['title' => 'Conditional Result']); + + $response = postJson(action(SearchController::class), [ + 'elementType' => Entry::class, + 'search' => 'Conditional Result', + 'condition' => [ + 'class' => ElementCondition::class, + 'elementType' => Entry::class, + 'conditionRules' => [[ + 'class' => IdConditionRule::class, + 'operator' => '=', + 'value' => (string) $matchingEntry->id, + ]], + ], + ]) + ->assertOk(); + + expect($response->json('elements'))->toHaveCount(1) + ->and($response->json('elements.0.id'))->toBe($matchingEntry->id) + ->and($response->json('exactMatch'))->toBeTrue(); +}); + +it('ignores non-element conditions', function () { + $state = new stdClass; + + app()->instance(Conditions::class, new readonly class($state) extends Conditions + { + public function __construct( + public stdClass $state, + ) {} + + public function createCondition(array|string $config): ConditionInterface + { + $this->state->config = $config; + + return new class extends BaseCondition + { + protected function selectableConditionRules(): array + { + return []; + } + }; + } + }); + + $firstEntry = EntryModel::factory() + ->indexed() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement(['title' => 'Ignored Condition']); + + $secondEntry = EntryModel::factory() + ->indexed() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement(['title' => 'Ignored Condition']); + + $response = postJson(action(SearchController::class), [ + 'elementType' => Entry::class, + 'search' => 'Ignored Condition', + 'condition' => 'ignored-condition', + ]) + ->assertOk(); + + expect(collect($response->json('elements'))->pluck('id')->sort()->values()->all()) + ->toBe(collect([$firstEntry->id, $secondEntry->id])->sort()->values()->all()) + ->and($state->config)->toBe('ignored-condition'); +}); + +it('passes the reference element context into element conditions', function () { + $referenceEntry = EntryModel::factory() + ->indexed() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement(['title' => 'Reference Result']); + + $otherEntry = EntryModel::factory() + ->indexed() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement(['title' => 'Reference Result']); + + $state = new stdClass; + + $conditions = new readonly class($state) extends Conditions + { + public function __construct( + public stdClass $state, + ) {} + + public function createCondition(array|string $config): ConditionInterface + { + $this->state->config = $config; + + return new class($this->state) extends ElementCondition + { + public function __construct( + public stdClass $state, + ) { + parent::__construct(Entry::class); + } + + public function modifyQuery(ElementQueryInterface $query): void + { + $this->state->modifyQueryCalled = true; + $this->state->referenceElementId = $this->referenceElement?->id; + + $query->id($this->referenceElement?->id ?? 0); + } + }; + } + }; + + $elements = new class(app(ElementPlaceholders::class), $referenceEntry) extends Elements + { + public ?int $requestedElementId = null; + + public array|int|string|null $requestedSiteId = null; + + public array $requestedCriteria = []; + + public function __construct( + ElementPlaceholders $placeholders, + private readonly Entry $referenceEntry, + ) { + parent::__construct($placeholders); + } + + public function getElementById( + int $elementId, + ?string $elementType = null, + array|int|string|null $siteId = null, + array $criteria = [], + ): ElementInterface { + $this->requestedElementId = $elementId; + $this->requestedSiteId = $siteId; + $this->requestedCriteria = $criteria; + + return $this->referenceEntry; + } + }; + + app()->instance(Conditions::class, $conditions); + app()->instance(Elements::class, $elements); + + $response = postJson(action(SearchController::class), [ + 'elementType' => Entry::class, + 'search' => 'Reference Result', + 'condition' => 'reference-condition', + 'referenceElementId' => $referenceEntry->id, + 'referenceElementOwnerId' => 123, + 'referenceElementSiteId' => $referenceEntry->siteId, + ]) + ->assertOk(); + + expect($response->json('elements'))->toHaveCount(1) + ->and($response->json('elements.0.id'))->toBe($referenceEntry->id) + ->and($state->config)->toBe('reference-condition') + ->and($state->modifyQueryCalled)->toBeTrue() + ->and($state->referenceElementId)->toBe($referenceEntry->id) + ->and($elements->requestedElementId)->toBe($referenceEntry->id) + ->and($elements->requestedSiteId)->toBe($referenceEntry->siteId) + ->and($elements->requestedCriteria)->toBe(['ownerId' => 123]); +}); diff --git a/tests/Feature/Http/Controllers/Elements/UpdateFieldLayoutControllerTest.php b/tests/Feature/Http/Controllers/Elements/UpdateFieldLayoutControllerTest.php new file mode 100644 index 00000000000..364119de5f2 --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/UpdateFieldLayoutControllerTest.php @@ -0,0 +1,133 @@ +entryType = EntryType::factory()->create(); + $this->section = Section::factory()->withEntryTypes($this->entryType)->create([ + 'handle' => 'blog', + ]); +}); + +it('requires authentication', function () { + Auth::logout(); + + postJson(action(UpdateFieldLayoutController::class), [ + 'elementType' => Entry::class, + ])->assertUnauthorized(); +}); + +it('returns responses resolved by the element request', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + post(action(UpdateFieldLayoutController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'draftId' => 999999, + 'siteId' => $entry->siteId, + ])->assertRedirect($entry->getCpEditUrl()); +}); + +it('returns 400 when no element is identified by the request', function () { + postJson(action(UpdateFieldLayoutController::class), [ + 'elementType' => Entry::class, + 'elementId' => 999999, + 'siteId' => Sites::getPrimarySite()->id, + ])->assertBadRequest(); +}); + +it('returns 400 for revisions', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $revision */ + $revision = Elements::getElementById(app(Revisions::class)->createRevision($entry, auth()->id())); + + postJson(action(UpdateFieldLayoutController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'revisionId' => $revision->revisionId, + 'siteId' => $revision->siteId, + ])->assertBadRequest(); +}); + +it('returns updated field layout data for existing elements', function () { + $entry = EntryModel::factory() + ->forSection($this->section) + ->forEntryType($this->entryType) + ->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + postJson(action(UpdateFieldLayoutController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + 'title' => 'Updated Title', + 'slug' => 'updated-title', + ])->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', 'Field layout updated.') + ->where('modelName', 'element') + ->where('element.id', $entry->id) + ->where('element.title', 'Updated Title') + ->where('element.slug', 'updated-title') + ->has('missingElements') + ->has('initialDeltaValues') + ->has('headHtml') + ->has('bodyHtml') + ->etc() + ); +}); + +it('returns field layout data for new elements', function () { + postJson(action(UpdateFieldLayoutController::class), [ + 'elementType' => Entry::class, + 'siteId' => Sites::getPrimarySite()->id, + 'sectionId' => $this->section->id, + 'typeId' => $this->entryType->id, + 'title' => 'Unsaved Entry', + ])->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', 'Field layout updated.') + ->where('modelName', 'element') + ->where('element.title', 'Unsaved Entry') + ->where('element.sectionId', $this->section->id) + ->where('element.typeId', $this->entryType->id) + ->where('element.slug', fn (string $slug) => $slug !== '') + ->has('missingElements') + ->has('initialDeltaValues') + ->has('headHtml') + ->has('bodyHtml') + ->etc() + ); +}); diff --git a/tests/Feature/Http/Controllers/Elements/ValidateElementControllerTest.php b/tests/Feature/Http/Controllers/Elements/ValidateElementControllerTest.php new file mode 100644 index 00000000000..ac52763896b --- /dev/null +++ b/tests/Feature/Http/Controllers/Elements/ValidateElementControllerTest.php @@ -0,0 +1,111 @@ + Entry::class, + ])->assertUnauthorized(); +}); + +it('returns responses resolved by the element request', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + + post(action(ValidateElementController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'draftId' => 999999, + 'siteId' => $entry->siteId, + ])->assertRedirect($entry->getCpEditUrl()); +}); + +it('returns 400 when no element is identified by the request', function () { + postJson(action(ValidateElementController::class), [ + 'elementType' => Entry::class, + 'siteId' => 1, + ])->assertBadRequest(); +}); + +it('returns 400 for revisions', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Canonical Title', + 'slug' => 'canonical-title', + ]); + /** @var Entry $revision */ + $revision = Elements::getElementById(app(Revisions::class)->createRevision($entry, auth()->id())); + + postJson(action(ValidateElementController::class), [ + 'elementType' => Entry::class, + 'revisionId' => $revision->revisionId, + 'siteId' => $revision->siteId, + ])->assertBadRequest(); +}); + +it('returns a failure response for invalid elements', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Valid Title', + 'slug' => 'valid-title', + ]); + + $response = postJson(action(ValidateElementController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ]) + ->assertBadRequest() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', t('{type} validation failed.', ['type' => Entry::displayName()])) + ->where('modelName', 'element') + ->where('element.title', 'Valid Title') + ->where('errors.authorIds.0', 'The author ids field is required.') + ->etc() + ); + + expect($response->json('errorSummary'))->toContain('field-error-key'); +}); + +it('returns a success response for valid elements', function () { + $entry = EntryModel::factory()->createElement([ + 'title' => 'Valid Title', + 'slug' => 'valid-title', + ]); + $entry->setAuthorIds([auth()->id()]); + Elements::saveElement($entry); + + postJson(action(ValidateElementController::class), [ + 'elementType' => Entry::class, + 'elementId' => $entry->id, + 'siteId' => $entry->siteId, + ]) + ->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->where('message', t('{type} validation successful.', ['type' => Entry::displayName()])) + ->where('modelName', 'element') + ->where('element.title', 'Valid Title') + ->missing('errors') + ->etc() + ); +}); diff --git a/tests/Feature/Http/Controllers/MatrixControllerTest.php b/tests/Feature/Http/Controllers/MatrixControllerTest.php index 5bb10b23532..b7fa7b371c6 100644 --- a/tests/Feature/Http/Controllers/MatrixControllerTest.php +++ b/tests/Feature/Http/Controllers/MatrixControllerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\Elements; use CraftCms\Cms\Element\Exceptions\InvalidElementException; diff --git a/tests/Feature/Http/Controllers/NestedElementsControllerTest.php b/tests/Feature/Http/Controllers/NestedElementsControllerTest.php index d65b4c15bae..fc035c8a827 100644 --- a/tests/Feature/Http/Controllers/NestedElementsControllerTest.php +++ b/tests/Feature/Http/Controllers/NestedElementsControllerTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -use craft\base\ElementInterface; use CraftCms\Cms\Address\Models\Address as AddressModel; use CraftCms\Cms\Auth\SessionAuth; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\Events\BeforeDelete; use CraftCms\Cms\Entry\Elements\Entry as EntryElement; diff --git a/tests/Feature/Http/Requests/ElementRequestTest.php b/tests/Feature/Http/Requests/ElementRequestTest.php new file mode 100644 index 00000000000..afea64a7460 --- /dev/null +++ b/tests/Feature/Http/Requests/ElementRequestTest.php @@ -0,0 +1,102 @@ +createElement([ + 'title' => 'Canonical Title', + ]); + + $draft = app(Drafts::class)->createDraft($entry, auth()->id(), provisional: true); + $draft->title = 'Edited Title'; + Elements::saveElement($draft); + + $request = ElementRequest::create('/', 'POST'); + $request->setUserResolver(fn () => auth()->user()); + + app()->instance('request', $request); + app(RequestedSite::class)->reset(); + + $element = $request->element(['id' => $entry->id], checkForProvisionalDraft: true); + + expect($element)->not()->toBeNull() + ->and($element->id)->toBe($draft->id) + ->and($element->draftId)->toBe($draft->draftId) + ->and($element->isProvisionalDraft)->toBeTrue(); +}); + +it('resolves unpublished drafts by id when no canonical element exists', function () { + $entryType = EntryType::factory()->create(); + $section = Section::factory()->withEntryTypes($entryType)->create(); + + $draft = app(Entry::class); + $draft->siteId = Sites::getPrimarySite()->id; + $draft->sectionId = $section->id; + $draft->typeId = $entryType->id; + $draft->title = 'Unpublished Draft'; + $draft->slug = 'unpublished-draft'; + $draft->setAuthorIds([auth()->id()]); + + app(Drafts::class)->saveElementAsDraft($draft, auth()->id(), markAsSaved: false); + + $request = ElementRequest::create('/', 'POST'); + $request->setUserResolver(fn () => auth()->user()); + + app()->instance('request', $request); + app(RequestedSite::class)->reset(); + + $element = $request->element(['id' => $draft->id]); + + expect($element)->not()->toBeNull() + ->and($element->id)->toBe($draft->id) + ->and($element->draftId)->toBe($draft->draftId) + ->and($element->getIsUnpublishedDraft())->toBeTrue(); +}); + +it('resolves unpublished drafts by uid when no canonical element exists', function () { + $entryType = EntryType::factory()->create(); + $section = Section::factory()->withEntryTypes($entryType)->create(); + + $draft = app(Entry::class); + $draft->siteId = Sites::getPrimarySite()->id; + $draft->sectionId = $section->id; + $draft->typeId = $entryType->id; + $draft->title = 'UID Draft'; + $draft->slug = 'uid-draft'; + $draft->setAuthorIds([auth()->id()]); + + app(Drafts::class)->saveElementAsDraft($draft, auth()->id(), markAsSaved: false); + + $request = ElementRequest::create('/', 'POST', [ + 'elementUid' => $draft->uid, + ]); + $request->setUserResolver(fn () => auth()->user()); + + app()->instance('request', $request); + app(RequestedSite::class)->reset(); + + $element = $request->element(); + + expect($element)->not()->toBeNull() + ->and($element->id)->toBe($draft->id) + ->and($element->uid)->toBe($draft->uid) + ->and($element->draftId)->toBe($draft->draftId) + ->and($element->getIsUnpublishedDraft())->toBeTrue(); +}); diff --git a/tests/Feature/Queue/JobTest.php b/tests/Feature/Queue/JobTest.php index ea97c0e9b1d..7249812ae5a 100644 --- a/tests/Feature/Queue/JobTest.php +++ b/tests/Feature/Queue/JobTest.php @@ -178,3 +178,28 @@ public function handle(): void {} // No job property set, so uuid() will return null expect($job->shouldStillRun())->toBeTrue(); }); + +it('returns true from shouldStillRun for sync jobs without progress', function () { + $job = new class extends Job + { + public function handle(): void {} + }; + + $mockQueueJob = new class + { + public function uuid(): string + { + return 'sync-job-uuid'; + } + + public function getConnectionName(): string + { + return 'sync'; + } + }; + + $reflection = new ReflectionProperty($job, 'job'); + $reflection->setValue($job, $mockQueueJob); + + expect($job->shouldStillRun())->toBeTrue(); +}); diff --git a/tests/Feature/Queue/TestClasses/TestBatchedElementJob.php b/tests/Feature/Queue/TestClasses/TestBatchedElementJob.php index 7101cdf8657..19132247040 100644 --- a/tests/Feature/Queue/TestClasses/TestBatchedElementJob.php +++ b/tests/Feature/Queue/TestClasses/TestBatchedElementJob.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Tests\Feature\Queue\TestClasses; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Queue\BatchedElementJob; class TestBatchedElementJob extends BatchedElementJob diff --git a/tests/Feature/Search/Jobs/UpdateSearchIndexTest.php b/tests/Feature/Search/Jobs/UpdateSearchIndexTest.php index 702d9b2f988..0187b720e6c 100644 --- a/tests/Feature/Search/Jobs/UpdateSearchIndexTest.php +++ b/tests/Feature/Search/Jobs/UpdateSearchIndexTest.php @@ -2,10 +2,13 @@ declare(strict_types=1); +use CraftCms\Cms\Database\Table; use CraftCms\Cms\Entry\Elements\Entry as EntryElement; use CraftCms\Cms\Entry\Models\Entry; use CraftCms\Cms\Queue\Job; use CraftCms\Cms\Search\Jobs\UpdateSearchIndex; +use CraftCms\Cms\Support\Facades\Sites; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Queue; it('extends Job', function () { @@ -109,6 +112,35 @@ expect(true)->toBeTrue(); }); +it('can execute queued updates on the sync queue', function () { + $entry = Entry::factory()->create(); + $siteId = Sites::getCurrentSite()->id; + + DB::table(Table::SEARCHINDEX) + ->where('elementId', $entry->id) + ->delete(); + + $jobId = DB::table(Table::SEARCHINDEXQUEUE)->insertGetId([ + 'elementId' => $entry->id, + 'siteId' => $siteId, + 'reserved' => false, + ]); + + dispatch_sync(new UpdateSearchIndex( + elementType: EntryElement::class, + elementId: $entry->id, + siteId: $siteId, + queued: true, + )); + + expect(DB::table(Table::SEARCHINDEX) + ->where('elementId', $entry->id) + ->exists())->toBeTrue() + ->and(DB::table(Table::SEARCHINDEXQUEUE) + ->where('id', $jobId) + ->exists())->toBeFalse(); +}); + it('handles case with no matching elements', function () { $job = new UpdateSearchIndex( elementType: EntryElement::class, diff --git a/tests/Feature/Search/SearchTest.php b/tests/Feature/Search/SearchTest.php index 13019ce5eae..1c0d890b80f 100644 --- a/tests/Feature/Search/SearchTest.php +++ b/tests/Feature/Search/SearchTest.php @@ -29,16 +29,15 @@ function createIndexedEntry(string $title, ?string $slug = null): EntryModel { - $entryModel = EntryModel::factory()->create(); - $entryModel->element->siteSettings->first()->update(array_filter([ - 'title' => $title, - 'slug' => $slug, - ])); + $factory = EntryModel::factory() + ->indexed() + ->title($title); - $element = Elements::getElementById($entryModel->id); - Search::indexElementAttributes($element); + if ($slug !== null) { + $factory = $factory->slug($slug); + } - return $entryModel; + return $factory->create(); } describe('indexElementAttributes', function () { diff --git a/tests/Feature/User/Elements/UserValidationTest.php b/tests/Feature/User/Elements/UserValidationTest.php index 6f49df31ea1..3198f03fbf2 100644 --- a/tests/Feature/User/Elements/UserValidationTest.php +++ b/tests/Feature/User/Elements/UserValidationTest.php @@ -5,7 +5,7 @@ use CraftCms\Cms\Cms; use CraftCms\Cms\Edition; use CraftCms\Cms\FieldLayout\FieldLayout; -use CraftCms\Cms\FieldLayout\LayoutElements\users\FullNameField; +use CraftCms\Cms\FieldLayout\LayoutElements\Users\FullNameField; use CraftCms\Cms\User\Elements\User; use CraftCms\Cms\User\Models\User as UserModel; use CraftCms\Cms\User\Validation\UserRules; diff --git a/tests/TestClasses/Element/ElementEagerLoader/TestElementEagerLoaderElement.php b/tests/TestClasses/Element/ElementEagerLoader/TestElementEagerLoaderElement.php index b33c4a69b01..614d4458323 100644 --- a/tests/TestClasses/Element/ElementEagerLoader/TestElementEagerLoaderElement.php +++ b/tests/TestClasses/Element/ElementEagerLoader/TestElementEagerLoaderElement.php @@ -5,7 +5,7 @@ namespace CraftCms\Cms\Tests\TestClasses\Element\ElementEagerLoader; use Closure; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Queries\ElementQuery; diff --git a/tests/TestClasses/Element/ElementEagerLoader/TestElementEagerLoaderQuery.php b/tests/TestClasses/Element/ElementEagerLoader/TestElementEagerLoaderQuery.php index 4305bc19464..8d978881c7f 100644 --- a/tests/TestClasses/Element/ElementEagerLoader/TestElementEagerLoaderQuery.php +++ b/tests/TestClasses/Element/ElementEagerLoader/TestElementEagerLoaderQuery.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Tests\TestClasses\Element\ElementEagerLoader; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\ElementQuery; use Illuminate\Support\Collection; diff --git a/tests/TestClasses/Element/TestDuplicateElementActionElement.php b/tests/TestClasses/Element/TestDuplicateElementActionElement.php index 752a4084c23..646b1505367 100644 --- a/tests/TestClasses/Element/TestDuplicateElementActionElement.php +++ b/tests/TestClasses/Element/TestDuplicateElementActionElement.php @@ -4,7 +4,7 @@ namespace CraftCms\Cms\Tests\TestClasses\Element; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Queries\ElementQuery; use CraftCms\Cms\Site\Models\Site; diff --git a/tests/Unit/Asset/AssetsHelperTest.php b/tests/Unit/Asset/AssetsHelperTest.php index 20f1f0c55c4..a8e3aad9f42 100644 --- a/tests/Unit/Asset/AssetsHelperTest.php +++ b/tests/Unit/Asset/AssetsHelperTest.php @@ -3,7 +3,7 @@ declare(strict_types=1); use CraftCms\Cms\Asset\AssetsHelper; -use CraftCms\Cms\Asset\Elements\Asset; +use CraftCms\Cms\Asset\Enums\FileKind; use CraftCms\Cms\Asset\Events\SetAssetFilename; use CraftCms\Cms\Cms; use CraftCms\Cms\Support\Path; @@ -116,60 +116,60 @@ test('returns correct kind for known extensions', function (string $file, string $expectedKind) { expect(AssetsHelper::getFileKindByExtension($file))->toBe($expectedKind); })->with([ - 'jpg image' => ['photo.jpg', Asset::KIND_IMAGE], - 'jpeg image' => ['photo.jpeg', Asset::KIND_IMAGE], - 'png image' => ['photo.png', Asset::KIND_IMAGE], - 'gif image' => ['photo.gif', Asset::KIND_IMAGE], - 'webp image' => ['photo.webp', Asset::KIND_IMAGE], - 'svg image' => ['photo.svg', Asset::KIND_IMAGE], - 'avif image' => ['photo.avif', Asset::KIND_IMAGE], - 'mp3 audio' => ['song.mp3', Asset::KIND_AUDIO], - 'wav audio' => ['song.wav', Asset::KIND_AUDIO], - 'flac audio' => ['song.flac', Asset::KIND_AUDIO], - 'mp4 video' => ['movie.mp4', Asset::KIND_VIDEO], - 'mov video' => ['movie.mov', Asset::KIND_VIDEO], - 'webm video' => ['movie.webm', Asset::KIND_VIDEO], - 'pdf document' => ['doc.pdf', Asset::KIND_PDF], - 'json file' => ['data.json', Asset::KIND_JSON], - 'xml file' => ['data.xml', Asset::KIND_XML], - 'html file' => ['page.html', Asset::KIND_HTML], - 'htm file' => ['page.htm', Asset::KIND_HTML], - 'js file' => ['script.js', Asset::KIND_JAVASCRIPT], - 'php file' => ['script.php', Asset::KIND_PHP], - 'txt file' => ['readme.txt', Asset::KIND_TEXT], - 'zip compressed' => ['archive.zip', Asset::KIND_COMPRESSED], - 'doc word' => ['document.doc', Asset::KIND_WORD], - 'docx word' => ['document.docx', Asset::KIND_WORD], - 'xls excel' => ['sheet.xls', Asset::KIND_EXCEL], - 'xlsx excel' => ['sheet.xlsx', Asset::KIND_EXCEL], - 'ppt powerpoint' => ['slides.ppt', Asset::KIND_POWERPOINT], - 'pptx powerpoint' => ['slides.pptx', Asset::KIND_POWERPOINT], - 'psd photoshop' => ['design.psd', Asset::KIND_PHOTOSHOP], - 'ai illustrator' => ['design.ai', Asset::KIND_ILLUSTRATOR], - 'srt subtitles' => ['subtitles.srt', Asset::KIND_CAPTIONS_SUBTITLES], - 'vtt subtitles' => ['subtitles.vtt', Asset::KIND_CAPTIONS_SUBTITLES], - 'accdb access' => ['file.accdb', Asset::KIND_ACCESS], + 'jpg image' => ['photo.jpg', FileKind::Image->value], + 'jpeg image' => ['photo.jpeg', FileKind::Image->value], + 'png image' => ['photo.png', FileKind::Image->value], + 'gif image' => ['photo.gif', FileKind::Image->value], + 'webp image' => ['photo.webp', FileKind::Image->value], + 'svg image' => ['photo.svg', FileKind::Image->value], + 'avif image' => ['photo.avif', FileKind::Image->value], + 'mp3 audio' => ['song.mp3', FileKind::Audio->value], + 'wav audio' => ['song.wav', FileKind::Audio->value], + 'flac audio' => ['song.flac', FileKind::Audio->value], + 'mp4 video' => ['movie.mp4', FileKind::Video->value], + 'mov video' => ['movie.mov', FileKind::Video->value], + 'webm video' => ['movie.webm', FileKind::Video->value], + 'pdf document' => ['doc.pdf', FileKind::Pdf->value], + 'json file' => ['data.json', FileKind::Json->value], + 'xml file' => ['data.xml', FileKind::Xml->value], + 'html file' => ['page.html', FileKind::Html->value], + 'htm file' => ['page.htm', FileKind::Html->value], + 'js file' => ['script.js', FileKind::Javascript->value], + 'php file' => ['script.php', FileKind::Php->value], + 'txt file' => ['readme.txt', FileKind::Text->value], + 'zip compressed' => ['archive.zip', FileKind::Compressed->value], + 'doc word' => ['document.doc', FileKind::Word->value], + 'docx word' => ['document.docx', FileKind::Word->value], + 'xls excel' => ['sheet.xls', FileKind::Excel->value], + 'xlsx excel' => ['sheet.xlsx', FileKind::Excel->value], + 'ppt powerpoint' => ['slides.ppt', FileKind::Powerpoint->value], + 'pptx powerpoint' => ['slides.pptx', FileKind::Powerpoint->value], + 'psd photoshop' => ['design.psd', FileKind::Photoshop->value], + 'ai illustrator' => ['design.ai', FileKind::Illustrator->value], + 'srt subtitles' => ['subtitles.srt', FileKind::CaptionsSubtitles->value], + 'vtt subtitles' => ['subtitles.vtt', FileKind::CaptionsSubtitles->value], + 'accdb access' => ['file.accdb', FileKind::Access->value], ]); test('returns unknown for unrecognized extensions', function () { - expect(AssetsHelper::getFileKindByExtension('file.xyz123'))->toBe(Asset::KIND_UNKNOWN); + expect(AssetsHelper::getFileKindByExtension('file.xyz123'))->toBe(FileKind::Unknown->value); }); test('returns unknown for files without extension', function () { - expect(AssetsHelper::getFileKindByExtension('README'))->toBe(Asset::KIND_UNKNOWN); + expect(AssetsHelper::getFileKindByExtension('README'))->toBe(FileKind::Unknown->value); }); test('returns unknown for bare extension name without dot', function () { - expect(AssetsHelper::getFileKindByExtension('html'))->toBe(Asset::KIND_UNKNOWN); + expect(AssetsHelper::getFileKindByExtension('html'))->toBe(FileKind::Unknown->value); }); test('is case insensitive', function () { - expect(AssetsHelper::getFileKindByExtension('photo.JPG'))->toBe(Asset::KIND_IMAGE); - expect(AssetsHelper::getFileKindByExtension('photo.Png'))->toBe(Asset::KIND_IMAGE); + expect(AssetsHelper::getFileKindByExtension('photo.JPG'))->toBe(FileKind::Image->value); + expect(AssetsHelper::getFileKindByExtension('photo.Png'))->toBe(FileKind::Image->value); }); test('handles full paths', function () { - expect(AssetsHelper::getFileKindByExtension('/path/to/photo.jpg'))->toBe(Asset::KIND_IMAGE); + expect(AssetsHelper::getFileKindByExtension('/path/to/photo.jpg'))->toBe(FileKind::Image->value); }); }); @@ -188,8 +188,8 @@ ]); test('returns unknown for unrecognized kind', function () { - expect(AssetsHelper::getFileKindLabel('Raaa'))->toBe(Asset::KIND_UNKNOWN); - expect(AssetsHelper::getFileKindLabel('nonexistent_kind'))->toBe(Asset::KIND_UNKNOWN); + expect(AssetsHelper::getFileKindLabel('Raaa'))->toBe(FileKind::Unknown->value); + expect(AssetsHelper::getFileKindLabel('nonexistent_kind'))->toBe(FileKind::Unknown->value); }); }); @@ -372,16 +372,16 @@ expect($kinds)->toHaveKey($kind); })->with([ - 'image' => [Asset::KIND_IMAGE], - 'audio' => [Asset::KIND_AUDIO], - 'video' => [Asset::KIND_VIDEO], - 'pdf' => [Asset::KIND_PDF], - 'json' => [Asset::KIND_JSON], - 'xml' => [Asset::KIND_XML], - 'compressed' => [Asset::KIND_COMPRESSED], - 'excel' => [Asset::KIND_EXCEL], - 'word' => [Asset::KIND_WORD], - 'powerpoint' => [Asset::KIND_POWERPOINT], + 'image' => [FileKind::Image->value], + 'audio' => [FileKind::Audio->value], + 'video' => [FileKind::Video->value], + 'pdf' => [FileKind::Pdf->value], + 'json' => [FileKind::Json->value], + 'xml' => [FileKind::Xml->value], + 'compressed' => [FileKind::Compressed->value], + 'excel' => [FileKind::Excel->value], + 'word' => [FileKind::Word->value], + 'powerpoint' => [FileKind::Powerpoint->value], ]); test('results are sorted by label', function () { @@ -393,6 +393,19 @@ expect($labels)->toBe($sorted); }); + + test('it merges in extraFileKinds', function () { + AssetsHelper::clear(); + + Cms::config()->extraFileKinds = [ + 'stylesheet' => [ + 'label' => 'Stylesheet', + 'extensions' => ['css', 'less', 'pcss', 'sass', 'scss', 'styl'], + ], + ]; + + expect(AssetsHelper::getFileKinds())->toHaveKey('stylesheet'); + }); }); describe('getAllowedFileKinds', function () { @@ -427,7 +440,7 @@ test('image kind is allowed by default', function () { $allowed = AssetsHelper::getAllowedFileKinds(); - expect($allowed)->toHaveKey(Asset::KIND_IMAGE); + expect($allowed)->toHaveKey(FileKind::Image->value); }); }); diff --git a/tests/Unit/Cp/FormFieldsTest.php b/tests/Unit/Cp/FormFieldsTest.php index 95d9353ba2c..23a55cb820c 100644 --- a/tests/Unit/Cp/FormFieldsTest.php +++ b/tests/Unit/Cp/FormFieldsTest.php @@ -2,9 +2,24 @@ declare(strict_types=1); +use CraftCms\Cms\Address\Addresses; +use CraftCms\Cms\Address\Elements\Address; +use CraftCms\Cms\Address\Validation\AddressRules; use CraftCms\Cms\Cp\FormFields; +use CraftCms\Cms\FieldLayout\FieldLayout; use CraftCms\Cms\Support\Facades\Sites; use CraftCms\Cms\Twig\Exceptions\TemplateLoaderException; +use CraftCms\RulesetValidation\Attributes\Ruleset; + +#[Ruleset(AddressRules::class)] +class TestAddressForFormFields extends Address +{ + #[Override] + public function getFieldLayout(): FieldLayout + { + return app(Addresses::class)->getFieldLayout(); + } +} describe('fieldHtml', function () { it('renders the field container and optional label', function () { @@ -82,3 +97,16 @@ [' 'US']); + $originalScenario = $address->getScenario(); + + $html = FormFields::addressFieldsHtml($address); + + expect((bool) preg_match('/id="addressLine1-label".*?Required<\/span>.*?toBeTrue() + ->and((bool) preg_match('/id="sortingCode-label".*?Required<\/span>/s', $html))->toBeFalse() + ->and($address->getScenario())->toBe($originalScenario); + }); +}); diff --git a/tests/Unit/Element/ElementWrites/PropagateElementTest.php b/tests/Unit/Element/ElementWrites/PropagateElementTest.php index fd244d27084..e360fc19765 100644 --- a/tests/Unit/Element/ElementWrites/PropagateElementTest.php +++ b/tests/Unit/Element/ElementWrites/PropagateElementTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use craft\base\ElementInterface; use craft\base\FieldInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementCaches; use CraftCms\Cms\Element\Elements; diff --git a/tests/Unit/Element/ElementWrites/PropagateElementsTest.php b/tests/Unit/Element/ElementWrites/PropagateElementsTest.php index 177f2a0ffca..57b7f286dd7 100644 --- a/tests/Unit/Element/ElementWrites/PropagateElementsTest.php +++ b/tests/Unit/Element/ElementWrites/PropagateElementsTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use craft\base\ElementInterface; use CraftCms\Cms\Element\BulkOp\BulkOps; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementCaches; use CraftCms\Cms\Element\Elements; diff --git a/tests/Unit/Element/ElementWrites/ResaveElementsTest.php b/tests/Unit/Element/ElementWrites/ResaveElementsTest.php index d27cc3eff74..1fd827a6ed5 100644 --- a/tests/Unit/Element/ElementWrites/ResaveElementsTest.php +++ b/tests/Unit/Element/ElementWrites/ResaveElementsTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; use CraftCms\Cms\Element\BulkOp\BulkOps as BulkOpsService; +use CraftCms\Cms\Element\Contracts\ElementInterface; +use CraftCms\Cms\Element\Contracts\NestedElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementCaches; use CraftCms\Cms\Element\Elements; diff --git a/tests/Unit/Support/TemplateTest.php b/tests/Unit/Support/TemplateTest.php index 4018c4705d0..670fa88dfe8 100644 --- a/tests/Unit/Support/TemplateTest.php +++ b/tests/Unit/Support/TemplateTest.php @@ -5,6 +5,7 @@ use CraftCms\Cms\Shared\BaseModel; use CraftCms\Cms\Support\Template; use CraftCms\Cms\View\HtmlStack; +use Illuminate\Database\Eloquent\Attributes\Table; use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Loader\ArrayLoader; @@ -147,8 +148,5 @@ public function describeArgument(mixed $argument): string } } -class TemplateModelAttributeTarget extends BaseModel -{ - #[Override] - protected $table = 'template_test_models'; -} +#[Table(name: 'template_test_models')] +class TemplateModelAttributeTarget extends BaseModel {} diff --git a/workbench/database/seeders/DatabaseSeeder.php b/workbench/database/seeders/DatabaseSeeder.php index a895901109d..1b871e8b184 100644 --- a/workbench/database/seeders/DatabaseSeeder.php +++ b/workbench/database/seeders/DatabaseSeeder.php @@ -8,7 +8,7 @@ use CraftCms\Cms\Database\Migrations\Install; use CraftCms\Cms\Entry\Data\EntryType; use CraftCms\Cms\Entry\Elements\Entry; -use CraftCms\Cms\FieldLayout\LayoutElements\entries\EntryTitleField; +use CraftCms\Cms\FieldLayout\LayoutElements\Entries\EntryTitleField; use CraftCms\Cms\FieldLayout\Models\FieldLayout; use CraftCms\Cms\ProjectConfig\ProjectConfig; use CraftCms\Cms\Section\Data\Section; diff --git a/yii2-adapter/composer.lock b/yii2-adapter/composer.lock index 0c1423c8d4f..916b84b7353 100644 --- a/yii2-adapter/composer.lock +++ b/yii2-adapter/composer.lock @@ -479,7 +479,7 @@ "dist": { "type": "path", "url": "..", - "reference": "897e980ca4751c82d305e7f66e6e2fec93322d06" + "reference": "52fbc060042b9199686ac7376f94d1245a0abc4a" }, "require": { "bacon/bacon-qr-code": "^2.0", @@ -487,7 +487,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/laravel-ruleset-validation": "^1.1", "craftcms/plugin-installer": "~1.6.0", "craftcms/server-check": "~5.1.0", "craftcms/yii2-adapter": "self.version", @@ -801,16 +801,16 @@ }, { "name": "craftcms/laravel-ruleset-validation", - "version": "1.0.1", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/craftcms/laravel-ruleset-validation.git", - "reference": "66867ade9c09c6c8165bd62c0ebb7e4843399aab" + "reference": "5fd54f4643f4746d6ac662bbec13aa225f62edfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/laravel-ruleset-validation/zipball/66867ade9c09c6c8165bd62c0ebb7e4843399aab", - "reference": "66867ade9c09c6c8165bd62c0ebb7e4843399aab", + "url": "https://api.github.com/repos/craftcms/laravel-ruleset-validation/zipball/5fd54f4643f4746d6ac662bbec13aa225f62edfb", + "reference": "5fd54f4643f4746d6ac662bbec13aa225f62edfb", "shasum": "" }, "require": { @@ -859,9 +859,9 @@ ], "support": { "issues": "https://github.com/craftcms/laravel-ruleset-validation/issues", - "source": "https://github.com/craftcms/laravel-ruleset-validation/tree/1.0.1" + "source": "https://github.com/craftcms/laravel-ruleset-validation/tree/1.1.0" }, - "time": "2026-04-21T07:56:55+00:00" + "time": "2026-04-22T11:01:37+00:00" }, { "name": "craftcms/plugin-installer", @@ -2328,16 +2328,16 @@ }, { "name": "laravel/framework", - "version": "v13.5.0", + "version": "v13.6.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "ffa1850049a691b93129808f27ecd10e65c9d1a5" + "reference": "416a93ea9c53161e0d4b8a44045f447b65a7d2f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/ffa1850049a691b93129808f27ecd10e65c9d1a5", - "reference": "ffa1850049a691b93129808f27ecd10e65c9d1a5", + "url": "https://api.github.com/repos/laravel/framework/zipball/416a93ea9c53161e0d4b8a44045f447b65a7d2f1", + "reference": "416a93ea9c53161e0d4b8a44045f447b65a7d2f1", "shasum": "" }, "require": { @@ -2547,7 +2547,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-04-14T13:55:03+00:00" + "time": "2026-04-21T13:32:11+00:00" }, { "name": "laravel/prompts", @@ -9291,23 +9291,23 @@ }, { "name": "voku/portable-ascii", - "version": "2.0.3", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/voku/portable-ascii.git", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + "reference": "d870a33f0f79d2b4579740b0620200221ee44aeb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/d870a33f0f79d2b4579740b0620200221ee44aeb", + "reference": "d870a33f0f79d2b4579740b0620200221ee44aeb", "shasum": "" }, "require": { - "php": ">=7.0.0" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" }, "suggest": { "ext-intl": "Use Intl for transliterator_transliterate() support" @@ -9337,7 +9337,7 @@ ], "support": { "issues": "https://github.com/voku/portable-ascii/issues", - "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + "source": "https://github.com/voku/portable-ascii/tree/2.1.0" }, "funding": [ { @@ -9361,7 +9361,7 @@ "type": "tidelift" } ], - "time": "2024-11-21T01:49:47+00:00" + "time": "2026-04-16T23:10:39+00:00" }, { "name": "web-auth/cose-lib", @@ -10860,16 +10860,16 @@ }, { "name": "brianium/paratest", - "version": "v7.19.0", + "version": "v7.20.0", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6" + "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", - "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d", "shasum": "" }, "require": { @@ -10883,9 +10883,9 @@ "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", "phpunit/php-file-iterator": "^6.0.1 || ^7", "phpunit/php-timer": "^8 || ^9", - "phpunit/phpunit": "^12.5.9 || ^13", + "phpunit/phpunit": "^12.5.14 || ^13.0.5", "sebastian/environment": "^8.0.3 || ^9", - "symfony/console": "^7.4.4 || ^8.0.4", + "symfony/console": "^7.4.7 || ^8.0.7", "symfony/process": "^7.4.5 || ^8.0.5" }, "require-dev": { @@ -10893,11 +10893,11 @@ "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.38", - "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.12", - "phpstan/phpstan-strict-rules": "^2.0.8", - "symfony/filesystem": "^7.4.0 || ^8.0.1" + "phpstan/phpstan": "^2.1.44", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "symfony/filesystem": "^7.4.6 || ^8.0.6" }, "bin": [ "bin/paratest", @@ -10937,7 +10937,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.19.0" + "source": "https://github.com/paratestphp/paratest/tree/v7.20.0" }, "funding": [ { @@ -10949,7 +10949,7 @@ "type": "paypal" } ], - "time": "2026-02-06T10:53:26+00:00" + "time": "2026-03-29T15:46:14+00:00" }, { "name": "codeception/codeception", @@ -11908,16 +11908,16 @@ }, { "name": "firebase/php-jwt", - "version": "v7.0.3", + "version": "v7.0.5", "source": { "type": "git", - "url": "https://github.com/firebase/php-jwt.git", - "reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e" + "url": "https://github.com/googleapis/php-jwt.git", + "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/28aa0694bcfdfa5e2959c394d5a1ee7a5083629e", - "reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380", + "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380", "shasum": "" }, "require": { @@ -11925,6 +11925,7 @@ }, "require-dev": { "guzzlehttp/guzzle": "^7.4", + "phpfastcache/phpfastcache": "^9.2", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", "psr/cache": "^2.0||^3.0", @@ -11964,10 +11965,10 @@ "php" ], "support": { - "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v7.0.3" + "issues": "https://github.com/googleapis/php-jwt/issues", + "source": "https://github.com/googleapis/php-jwt/tree/v7.0.5" }, - "time": "2026-02-25T22:16:40+00:00" + "time": "2026-04-01T20:38:03+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -12123,16 +12124,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "v6.7.2", + "version": "6.8.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0" + "reference": "89ac92bcfe5d0a8a4433c7b89d394553ae7250cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/6fea66c7204683af437864e7c4e7abf383d14bc0", - "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/89ac92bcfe5d0a8a4433c7b89d394553ae7250cc", + "reference": "89ac92bcfe5d0a8a4433c7b89d394553ae7250cc", "shasum": "" }, "require": { @@ -12192,22 +12193,22 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/v6.7.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.8.0" }, - "time": "2026-02-15T15:06:22+00:00" + "time": "2026-04-02T12:43:11+00:00" }, { "name": "larastan/larastan", - "version": "v3.9.3", + "version": "v3.9.6", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65" + "reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65", - "reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65", + "url": "https://api.github.com/repos/larastan/larastan/zipball/9ad17e83e96b63536cb6ac39c3d40d29ff9cf636", + "reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636", "shasum": "" }, "require": { @@ -12221,7 +12222,7 @@ "illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13", "illuminate/support": "^11.44.2 || ^12.4.1 || ^13", "php": "^8.2", - "phpstan/phpstan": "^2.1.32" + "phpstan/phpstan": "^2.1.44" }, "require-dev": { "doctrine/coding-standard": "^13", @@ -12276,7 +12277,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.9.3" + "source": "https://github.com/larastan/larastan/tree/v3.9.6" }, "funding": [ { @@ -12284,7 +12285,7 @@ "type": "github" } ], - "time": "2026-02-20T12:07:12+00:00" + "time": "2026-04-16T10:02:43+00:00" }, { "name": "laravel/pail", @@ -12368,16 +12369,16 @@ }, { "name": "laravel/socialite", - "version": "v5.25.0", + "version": "v5.26.1", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "231f572e1a37c9ca1fb8085e9fb8608285beafb3" + "reference": "db6ec2ee967b7f06412c3a0cf1daaf072f4752a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/231f572e1a37c9ca1fb8085e9fb8608285beafb3", - "reference": "231f572e1a37c9ca1fb8085e9fb8608285beafb3", + "url": "https://api.github.com/repos/laravel/socialite/zipball/db6ec2ee967b7f06412c3a0cf1daaf072f4752a4", + "reference": "db6ec2ee967b7f06412c3a0cf1daaf072f4752a4", "shasum": "" }, "require": { @@ -12436,20 +12437,20 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2026-02-27T13:56:35+00:00" + "time": "2026-03-29T14:50:53+00:00" }, { "name": "laravel/tinker", - "version": "v3.0.0", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "cc74081282ba2e3dae1f0068ccb330370d24634e" + "reference": "4faba77764bd33411735936acdf30446d058c78b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/cc74081282ba2e3dae1f0068ccb330370d24634e", - "reference": "cc74081282ba2e3dae1f0068ccb330370d24634e", + "url": "https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b", + "reference": "4faba77764bd33411735936acdf30446d058c78b", "shasum": "" }, "require": { @@ -12503,9 +12504,9 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v3.0.0" + "source": "https://github.com/laravel/tinker/tree/v3.0.2" }, - "time": "2026-03-17T14:53:17+00:00" + "time": "2026-03-17T14:54:13+00:00" }, { "name": "league/factory-muffin", @@ -13002,23 +13003,23 @@ }, { "name": "nunomaduro/collision", - "version": "v8.9.1", + "version": "v8.9.4", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935" + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", - "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", "shasum": "" }, "require": { "filp/whoops": "^2.18.4", "nunomaduro/termwind": "^2.4.0", "php": "^8.2.0", - "symfony/console": "^7.4.4 || ^8.0.4" + "symfony/console": "^7.4.8 || ^8.0.8" }, "conflict": { "laravel/framework": "<11.48.0 || >=14.0.0", @@ -13026,12 +13027,12 @@ }, "require-dev": { "brianium/paratest": "^7.8.5", - "larastan/larastan": "^3.9.2", - "laravel/framework": "^11.48.0 || ^12.52.0", - "laravel/pint": "^1.27.1", - "orchestra/testbench-core": "^9.12.0 || ^10.9.0", - "pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0", - "sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0" + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" }, "type": "library", "extra": { @@ -13094,7 +13095,7 @@ "type": "patreon" } ], - "time": "2026-02-17T17:33:08+00:00" + "time": "2026-04-21T14:04:20+00:00" }, { "name": "orchestra/canvas", @@ -13293,25 +13294,25 @@ }, { "name": "orchestra/testbench", - "version": "v11.0.0", + "version": "v11.1.0", "source": { "type": "git", "url": "https://github.com/orchestral/testbench.git", - "reference": "60efc9e8ec12137a2a7084e9d54c976f78c192f5" + "reference": "997f33e5200c7e8db4756b35a9deb3f5f3086759" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/testbench/zipball/60efc9e8ec12137a2a7084e9d54c976f78c192f5", - "reference": "60efc9e8ec12137a2a7084e9d54c976f78c192f5", + "url": "https://api.github.com/repos/orchestral/testbench/zipball/997f33e5200c7e8db4756b35a9deb3f5f3086759", + "reference": "997f33e5200c7e8db4756b35a9deb3f5f3086759", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "fakerphp/faker": "^1.23", - "laravel/framework": "^13.0.0", + "laravel/framework": "^13.1.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^11.0.0", - "orchestra/workbench": "^11.0.0", + "orchestra/testbench-core": "^11.2.0", + "orchestra/workbench": "^11.0.1", "php": "^8.3", "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", "symfony/process": "^7.4.5|^8.0.5", @@ -13342,22 +13343,22 @@ ], "support": { "issues": "https://github.com/orchestral/testbench/issues", - "source": "https://github.com/orchestral/testbench/tree/v11.0.0" + "source": "https://github.com/orchestral/testbench/tree/v11.1.0" }, - "time": "2026-03-16T15:02:09+00:00" + "time": "2026-04-09T05:11:06+00:00" }, { "name": "orchestra/testbench-core", - "version": "v11.0.1", + "version": "v11.3.0", "source": { "type": "git", "url": "https://github.com/orchestral/testbench-core.git", - "reference": "b50cc6e765cef8a56a4bb17a4e603ba99d4768be" + "reference": "4cd065eb0b03de8eb103b3b325481895984b8f3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/b50cc6e765cef8a56a4bb17a4e603ba99d4768be", - "reference": "b50cc6e765cef8a56a4bb17a4e603ba99d4768be", + "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/4cd065eb0b03de8eb103b3b325481895984b8f3f", + "reference": "4cd065eb0b03de8eb103b3b325481895984b8f3f", "shasum": "" }, "require": { @@ -13368,14 +13369,14 @@ }, "conflict": { "brianium/paratest": "<7.3.0|>=8.0.0", - "laravel/framework": "<13.0.0|>=14.0.0", + "laravel/framework": "<13.4.0|>=14.0.0", "laravel/serializable-closure": ">=2.0.0 <2.0.10|>=3.0.0", "nunomaduro/collision": "<8.9.0|>=9.0.0", - "phpunit/phpunit": "<11.5.50|>=12.0.0 <12.5.8|>=13.1.0" + "phpunit/phpunit": "<11.5.50|>=12.0.0 <12.5.8|>=13.2.0" }, "require-dev": { "fakerphp/faker": "^1.24", - "laravel/framework": "^13.0.0", + "laravel/framework": "^13.4.0", "laravel/pint": "^1.24", "laravel/serializable-closure": "^2.0.10", "mockery/mockery": "^1.6.10", @@ -13390,7 +13391,7 @@ "brianium/paratest": "Allow using parallel testing (^7.3).", "ext-pcntl": "Required to use all features of the console signal trapping.", "fakerphp/faker": "Allow using Faker for testing (^1.23).", - "laravel/framework": "Required for testing (^13.0.0).", + "laravel/framework": "Required for testing (^13.1.1).", "mockery/mockery": "Allow using Mockery for testing (^1.6).", "nunomaduro/collision": "Allow using Laravel style tests output and parallel testing (^8.9).", "orchestra/testbench-dusk": "Allow using Laravel Dusk for testing (^11.0).", @@ -13436,20 +13437,20 @@ "issues": "https://github.com/orchestral/testbench/issues", "source": "https://github.com/orchestral/testbench-core" }, - "time": "2026-03-18T12:48:16+00:00" + "time": "2026-04-23T11:01:49+00:00" }, { "name": "orchestra/workbench", - "version": "v11.0.1", + "version": "v11.1.0", "source": { "type": "git", "url": "https://github.com/orchestral/workbench.git", - "reference": "39bcc78c7108b9b225446ff391400e53cd5bf253" + "reference": "e750c7bcae4405e054ff286475502e23274de04b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/workbench/zipball/39bcc78c7108b9b225446ff391400e53cd5bf253", - "reference": "39bcc78c7108b9b225446ff391400e53cd5bf253", + "url": "https://api.github.com/repos/orchestral/workbench/zipball/e750c7bcae4405e054ff286475502e23274de04b", + "reference": "e750c7bcae4405e054ff286475502e23274de04b", "shasum": "" }, "require": { @@ -13461,7 +13462,7 @@ "nunomaduro/collision": "^8.9", "orchestra/canvas": "^11.0.1", "orchestra/sidekick": "~1.1.23|~1.2.20", - "orchestra/testbench-core": "^11.0.0", + "orchestra/testbench-core": "^11.1.0", "php": "^8.3", "symfony/process": "^7.4|^8.0", "symfony/yaml": "^7.4|^8.0" @@ -13501,9 +13502,9 @@ ], "support": { "issues": "https://github.com/orchestral/workbench/issues", - "source": "https://github.com/orchestral/workbench/tree/v11.0.1" + "source": "https://github.com/orchestral/workbench/tree/v11.1.0" }, - "time": "2026-03-18T23:03:03+00:00" + "time": "2026-03-24T23:09:55+00:00" }, { "name": "paragonie/random_compat", @@ -13557,41 +13558,42 @@ }, { "name": "pestphp/pest", - "version": "v4.4.2", + "version": "v4.6.3", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701" + "reference": "bff44562a99d30aa37573995566051b0344f9f8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701", - "reference": "5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701", + "url": "https://api.github.com/repos/pestphp/pest/zipball/bff44562a99d30aa37573995566051b0344f9f8e", + "reference": "bff44562a99d30aa37573995566051b0344f9f8e", "shasum": "" }, "require": { - "brianium/paratest": "^7.19.0", - "nunomaduro/collision": "^8.9.1", + "brianium/paratest": "^7.20.0", + "nunomaduro/collision": "^8.9.3", "nunomaduro/termwind": "^2.4.0", "pestphp/pest-plugin": "^4.0.0", - "pestphp/pest-plugin-arch": "^4.0.0", + "pestphp/pest-plugin-arch": "^4.0.2", "pestphp/pest-plugin-mutate": "^4.0.1", "pestphp/pest-plugin-profanity": "^4.2.1", "php": "^8.3.0", - "phpunit/phpunit": "^12.5.12", - "symfony/process": "^7.4.5|^8.0.5" + "phpunit/phpunit": "^12.5.23", + "symfony/process": "^7.4.8|^8.0.8" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.12", + "phpunit/phpunit": ">12.5.23", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { + "mrpunyapal/peststan": "^0.2.5", "pestphp/pest-dev-tools": "^4.1.0", - "pestphp/pest-plugin-browser": "^4.3.0", - "pestphp/pest-plugin-type-coverage": "^4.0.3", - "psy/psysh": "^0.12.21" + "pestphp/pest-plugin-browser": "^4.3.1", + "pestphp/pest-plugin-type-coverage": "^4.0.4", + "psy/psysh": "^0.12.22" }, "bin": [ "bin/pest" @@ -13657,7 +13659,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.4.2" + "source": "https://github.com/pestphp/pest/tree/v4.6.3" }, "funding": [ { @@ -13669,7 +13671,7 @@ "type": "github" } ], - "time": "2026-03-10T21:09:12+00:00" + "time": "2026-04-18T13:51:25+00:00" }, { "name": "pestphp/pest-plugin", @@ -13743,26 +13745,26 @@ }, { "name": "pestphp/pest-plugin-arch", - "version": "v4.0.0", + "version": "v4.0.2", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-arch.git", - "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d" + "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/25bb17e37920ccc35cbbcda3b00d596aadf3e58d", - "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", + "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", "shasum": "" }, "require": { "pestphp/pest-plugin": "^4.0.0", "php": "^8.3", - "ta-tikoma/phpunit-architecture-test": "^0.8.5" + "ta-tikoma/phpunit-architecture-test": "^0.8.7" }, "require-dev": { - "pestphp/pest": "^4.0.0", - "pestphp/pest-dev-tools": "^4.0.0" + "pestphp/pest": "^4.4.6", + "pestphp/pest-dev-tools": "^4.1.0" }, "type": "library", "extra": { @@ -13797,7 +13799,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.0" + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.2" }, "funding": [ { @@ -13809,7 +13811,7 @@ "type": "github" } ], - "time": "2025-08-20T13:10:51+00:00" + "time": "2026-04-10T17:20:19+00:00" }, { "name": "pestphp/pest-plugin-mutate", @@ -14063,16 +14065,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.50", + "version": "3.0.51", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b" + "reference": "d59c94077f9c9915abb51ddb52ce85188ece1748" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b", - "reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748", + "reference": "d59c94077f9c9915abb51ddb52ce85188ece1748", "shasum": "" }, "require": { @@ -14153,7 +14155,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.50" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.51" }, "funding": [ { @@ -14169,15 +14171,15 @@ "type": "tidelift" } ], - "time": "2026-03-19T02:57:58+00:00" + "time": "2026-04-10T01:33:53+00:00" }, { "name": "phpstan/phpstan", - "version": "2.1.42", + "version": "2.1.51", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0", - "reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59", + "reference": "dc3b523c45e714c70de2ac5113b958223b55dc59", "shasum": "" }, "require": { @@ -14222,20 +14224,20 @@ "type": "github" } ], - "time": "2026-03-17T14:58:32+00:00" + "time": "2026-04-21T18:22:01+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "12.5.3", + "version": "12.5.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" + "reference": "876099a072646c7745f673d7aeab5382c4439691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", - "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691", + "reference": "876099a072646c7745f673d7aeab5382c4439691", "shasum": "" }, "require": { @@ -14244,7 +14246,6 @@ "ext-xmlwriter": "*", "nikic/php-parser": "^5.7.0", "php": ">=8.3", - "phpunit/php-file-iterator": "^6.0", "phpunit/php-text-template": "^5.0", "sebastian/complexity": "^5.0", "sebastian/environment": "^8.0.3", @@ -14291,7 +14292,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6" }, "funding": [ { @@ -14311,7 +14312,7 @@ "type": "tidelift" } ], - "time": "2026-02-06T06:01:44+00:00" + "time": "2026-04-15T08:23:17+00:00" }, { "name": "phpunit/php-file-iterator", @@ -14572,16 +14573,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.12", + "version": "12.5.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "418e06b3b46b0d54bad749ff4907fc7dfb530199" + "reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/418e06b3b46b0d54bad749ff4907fc7dfb530199", - "reference": "418e06b3b46b0d54bad749ff4907fc7dfb530199", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969", + "reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969", "shasum": "" }, "require": { @@ -14595,15 +14596,15 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.5.3", + "phpunit/php-code-coverage": "^12.5.6", "phpunit/php-file-iterator": "^6.0.1", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", "sebastian/cli-parser": "^4.2.0", - "sebastian/comparator": "^7.1.4", + "sebastian/comparator": "^7.1.6", "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.0.3", + "sebastian/environment": "^8.1.0", "sebastian/exporter": "^7.0.2", "sebastian/global-state": "^8.0.2", "sebastian/object-enumerator": "^7.0.0", @@ -14650,31 +14651,15 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.12" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.23" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2026-02-16T08:34:36+00:00" + "time": "2026-04-18T06:12:49+00:00" }, { "name": "predis/predis", @@ -14741,16 +14726,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.21", + "version": "v0.12.22", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97" + "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", - "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/3be75d5b9244936dd4ac62ade2bfb004d13acf0f", + "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f", "shasum": "" }, "require": { @@ -14814,27 +14799,27 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.21" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.22" }, - "time": "2026-03-06T21:21:28+00:00" + "time": "2026-03-22T23:03:24+00:00" }, { "name": "rector/rector", - "version": "2.3.9", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "917842143fd9f5331a2adefc214b8d7143bd32c4" + "reference": "e645b6463c6a88ea5b44b17d3387d35a912c7946" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/917842143fd9f5331a2adefc214b8d7143bd32c4", - "reference": "917842143fd9f5331a2adefc214b8d7143bd32c4", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/e645b6463c6a88ea5b44b17d3387d35a912c7946", + "reference": "e645b6463c6a88ea5b44b17d3387d35a912c7946", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.40" + "phpstan/phpstan": "^2.1.48" }, "conflict": { "rector/rector-doctrine": "*", @@ -14868,7 +14853,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.3.9" + "source": "https://github.com/rectorphp/rector/tree/2.4.2" }, "funding": [ { @@ -14876,7 +14861,7 @@ "type": "github" } ], - "time": "2026-03-16T09:43:55+00:00" + "time": "2026-04-16T13:07:34+00:00" }, { "name": "sebastian/cli-parser", @@ -14949,16 +14934,16 @@ }, { "name": "sebastian/comparator", - "version": "7.1.4", + "version": "7.1.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" + "reference": "c769009dee98f494e0edc3fd4f4087501688f11e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", - "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c769009dee98f494e0edc3fd4f4087501688f11e", + "reference": "c769009dee98f494e0edc3fd4f4087501688f11e", "shasum": "" }, "require": { @@ -15017,7 +15002,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.6" }, "funding": [ { @@ -15037,7 +15022,7 @@ "type": "tidelift" } ], - "time": "2026-01-24T09:28:48+00:00" + "time": "2026-04-14T08:23:15+00:00" }, { "name": "sebastian/complexity", @@ -15166,16 +15151,16 @@ }, { "name": "sebastian/environment", - "version": "8.0.4", + "version": "8.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11" + "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", - "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b121608b28a13f721e76ffbbd386d08eff58f3f6", + "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6", "shasum": "" }, "require": { @@ -15190,7 +15175,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-main": "8.1-dev" } }, "autoload": { @@ -15218,7 +15203,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4" + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.0" }, "funding": [ { @@ -15238,7 +15223,7 @@ "type": "tidelift" } ], - "time": "2026-03-15T07:05:40+00:00" + "time": "2026-04-15T12:13:01+00:00" }, { "name": "sebastian/exporter", @@ -15899,16 +15884,16 @@ }, { "name": "symfony/browser-kit", - "version": "v7.4.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "bed167eadaaba641f51fc842c9227aa5e251309e" + "reference": "41850d8f8ddef9a9cd7314fa9f4902cf48885521" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/bed167eadaaba641f51fc842c9227aa5e251309e", - "reference": "bed167eadaaba641f51fc842c9227aa5e251309e", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/41850d8f8ddef9a9cd7314fa9f4902cf48885521", + "reference": "41850d8f8ddef9a9cd7314fa9f4902cf48885521", "shasum": "" }, "require": { @@ -15948,7 +15933,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v7.4.4" + "source": "https://github.com/symfony/browser-kit/tree/v7.4.8" }, "funding": [ { @@ -15968,7 +15953,7 @@ "type": "tidelift" } ], - "time": "2026-01-13T10:40:19+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symplify/easy-coding-standard", diff --git a/yii2-adapter/legacy/base/AssetPreviewHandlerInterface.php b/yii2-adapter/legacy/base/AssetPreviewHandlerInterface.php index 0dc68b95400..6a9fe308c83 100644 --- a/yii2-adapter/legacy/base/AssetPreviewHandlerInterface.php +++ b/yii2-adapter/legacy/base/AssetPreviewHandlerInterface.php @@ -7,7 +7,7 @@ namespace craft\base; -use yii\base\NotSupportedException; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; /** @phpstan-ignore-next-line */ if (false) { diff --git a/yii2-adapter/legacy/base/Element.php b/yii2-adapter/legacy/base/Element.php index 4bbb2c6ac0f..10630289f38 100644 --- a/yii2-adapter/legacy/base/Element.php +++ b/yii2-adapter/legacy/base/Element.php @@ -86,7 +86,6 @@ use CraftCms\Cms\Element\Events\SetRoute; use CraftCms\Cms\Element\Validation\ElementRules; use Illuminate\Support\Facades\Event; -use Override; /** * @since 3.0.0 @@ -104,10 +103,15 @@ abstract class Element extends \CraftCms\Cms\Element\Element public const string SCENARIO_LIVE = ElementRules::SCENARIO_LIVE; - public function init(): void + public function __construct($config = []) { - parent::init(); + parent::__construct($config); + + $this->init(); + } + public function init(): void + { // Stop allowing setting custom field values directly on the behavior /** @var CustomFieldBehavior $behavior */ $behavior = $this->getBehavior('customFields'); @@ -117,7 +121,6 @@ public function init(): void /** * {@inheritdoc} */ - #[Override] protected function defineBehaviors(): array { return [ diff --git a/yii2-adapter/legacy/base/ElementExporter.php b/yii2-adapter/legacy/base/ElementExporter.php index 18b3d0a36a5..d86c7e63201 100644 --- a/yii2-adapter/legacy/base/ElementExporter.php +++ b/yii2-adapter/legacy/base/ElementExporter.php @@ -7,6 +7,8 @@ namespace craft\base; +use CraftCms\Cms\Element\Contracts\ElementInterface; + /** * ElementExporter is the base class for classes representing element exporters in terms of objects. * diff --git a/yii2-adapter/legacy/base/ElementInterface.php b/yii2-adapter/legacy/base/ElementInterface.php index 39afa5912b5..af8b9ce0f60 100644 --- a/yii2-adapter/legacy/base/ElementInterface.php +++ b/yii2-adapter/legacy/base/ElementInterface.php @@ -1,1960 +1,20 @@ ,source:int,target:int} - * @phpstan-type EagerLoadingMap array{elementType?:class-string,map:EagerLoadingMapItem[],criteria?:array,createElement?:callable} + * * @author Pixel & Tonic, Inc. + * * @since 3.0.0 + * @deprecated 6.0.0 use {@see \CraftCms\Cms\Element\Contracts\ElementInterface} instead. */ -#[AllowedInSandbox] -interface ElementInterface extends - ComponentInterface, - ModelInterface, - \CraftCms\Cms\Component\Contracts\Chippable, - \CraftCms\Cms\Component\Contracts\CpEditable, - \CraftCms\Cms\Component\Contracts\Thumbable, - \CraftCms\Cms\Component\Contracts\Statusable, - \CraftCms\Cms\Component\Contracts\Actionable, - Validatable +interface ElementInterface extends \CraftCms\Cms\Element\Contracts\ElementInterface { - /** - * Returns the lowercase version of [[displayName()]]. - * - * @return string - * @since 3.3.17 - */ - public static function lowerDisplayName(): string; - - /** - * Returns the plural version of [[displayName()]]. - * - * @return string - * @since 3.2.0 - */ - public static function pluralDisplayName(): string; - - /** - * Returns the plural, lowercase version of [[displayName()]]. - * - * @return string - * @since 3.3.17 - */ - public static function pluralLowerDisplayName(): string; - - /** - * Returns the handle that should be used to refer to this element type from reference tags. - * - * @return string|null The reference handle, or null if the element type doesn’t support reference tags - */ - public static function refHandle(): ?string; - - /** - * Returns whether element indexes should show the “Drafts” status option. - * - * @return bool - * @since 5.0.0 - */ - public static function hasDrafts(): bool; - - /** - * Returns whether Craft should keep track of attribute and custom field changes made to this element type, - * including when the last time they were changed, and who was logged-in at the time. - * - * @return bool Whether to track changes made to elements of this type. - * @see getDirtyAttributes() - * @see getDirtyFields() - * @since 3.4.0 - */ - public static function trackChanges(): bool; - - /** - * Returns whether elements of this type have traditional titles. - * - * @return bool Whether elements of this type have traditional titles. - */ - public static function hasTitles(): bool; - - /** - * Returns whether element indexes should include a thumbnail view by default. - * - * @return bool - * @since 5.0.0 - */ - public static function hasThumbs(): bool; - - /** - * Returns whether elements of this type can have their own slugs and URIs. - * - * Note that individual elements must also return a URI format from [[getUriFormat()]] if they are to actually get a URI. - * - * @return bool Whether elements of this type can have their own slugs and URIs. - * @see getUriFormat() - */ - public static function hasUris(): bool; - - /** - * Returns whether elements of this type store content on a per-site basis. - * - * If this returns `true`, the element’s [[getSupportedSites()]] method will - * be responsible for defining which sites its content should be stored in. - * - * @return bool Whether elements of this type store data on a per-site basis. - */ - public static function isLocalized(): bool; - - /** - * Returns whether elements of this type have statuses. - * - * If this returns `true`, the element index template will show a Status menu by default, and your elements will - * get status indicator icons next to them. - * Use [[statuses()]] to customize which statuses the elements might have. - * - * @return bool Whether elements of this type have statuses. - * @see statuses() - */ - public static function hasStatuses(): bool; - - /** - * Creates an [[ElementQueryInterface]] instance for query purpose. - * - * The returned [[ElementQueryInterface]] instance can be further customized by calling - * methods defined in [[ElementQueryInterface]] before `one()` or `all()` is called to return - * populated [[ElementInterface]] instances. For example, - * - * ```php - * // Find the entry whose ID is 5 - * $entry = Entry::find()->id(5)->one(); - * // Find all assets and order them by their filename: - * $assets = Asset::find() - * ->orderBy('filename') - * ->all(); - * ``` - * - * If you want to define custom criteria parameters for your elements, you can do so by overriding - * this method and returning a custom query class. For example, - * - * ```php - * class Product extends Element - * { - * public static function find(): ElementQueryInterface|ElementQuery - * { - * // use ProductQuery instead of the default ElementQuery - * return new ProductQuery(get_called_class()); - * } - * } - * ``` - * - * You can also set default criteria parameters on the ElementQuery if you don’t have a need for - * a custom query class. For example, - * - * ```php - * class Customer extends ActiveRecord - * { - * public static function find(): ElementQueryInterface|ElementQuery - * { - * return parent::find()->limit(50); - * } - * } - * ``` - * - * @return ElementQueryInterface The newly created [[ElementQueryInterface]] instance. - */ - public static function find(): ElementQueryInterface; - - /** - * Returns a single element instance by a primary key or a set of element criteria parameters. - * - * The method accepts: - * - * - an int: query by a single ID value and return the corresponding element (or null if not found). - * - an array of name-value pairs: query by a set of parameter values and return the first element - * matching all of them (or null if not found). - * - * Note that this method will automatically call the `one()` method and return an - * [[ElementInterface|\craft\base\Element]] instance. For example, - * - * ```php - * // find a single entry whose ID is 10 - * $entry = Entry::findOne(10); - * // the above code is equivalent to: - * $entry = Entry::find->id(10)->one(); - * // find the first user whose email ends in "example.com" - * $user = User::findOne(['email' => '*example.com']); - * // the above code is equivalent to: - * $user = User::find()->email('*example.com')->one(); - * ``` - * - * @param mixed $criteria The element ID or a set of element criteria parameters - * @return static|null Element instance matching the condition, or null if nothing matches. - */ - public static function findOne(mixed $criteria = null): ?static; - - /** - * Returns a list of elements that match the specified ID(s) or a set of element criteria parameters. - * - * The method accepts: - * - * - an int: query by a single ID value and return an array containing the corresponding element - * (or an empty array if not found). - * - an array of integers: query by a list of ID values and return the corresponding elements (or an - * empty array if none was found). - * Note that an empty array will result in an empty result as it will be interpreted as a search for - * primary keys and not an empty set of element criteria parameters. - * - an array of name-value pairs: query by a set of parameter values and return an array of elements - * matching all of them (or an empty array if none was found). - * - * Note that this method will automatically call the `all()` method and return an array of - * [[ElementInterface|\craft\base\Element]] instances. For example, - * - * ```php - * // find the entries whose ID is 10 - * $entries = Entry::findAll(10); - * // the above code is equivalent to: - * $entries = Entry::find()->id(10)->all(); - * // find the entries whose ID is 10, 11 or 12. - * $entries = Entry::findAll([10, 11, 12]); - * // the above code is equivalent to: - * $entries = Entry::find()->id([10, 11, 12]])->all(); - * // find users whose email ends in "example.com" - * $users = User::findAll(['email' => '*example.com']); - * // the above code is equivalent to: - * $users = User::find()->email('*example.com')->all(); - * ``` - * - * @param mixed $criteria The element ID, an array of IDs, or a set of element criteria parameters - * @return static[] an array of Element instances, or an empty array if nothing matches. - */ - public static function findAll(mixed $criteria = null): array; - - /** - * Returns an element condition for the element type. - * - * @return ElementConditionInterface - * @since 4.0.0 - */ - public static function createCondition(): ElementConditionInterface; - - /** - * Returns whether the element type’s sources can be split into multiple pages. - * - * @return bool - * @since 5.9.0 - */ - public static function multiPageSources(): bool; - - /** - * Returns the source definitions that elements of this type may belong to. - * - * This defines what will show up in the source list on element indexes and element selector modals. - * - * Each item in the array should be set to an array that has the following keys: - * - **`page`** – The source’s page label. (Optional) - * - **`key`** – The source’s key. This is the string that will be passed into the $source argument of [[actions()]], - * [[indexHtml()]], and [[defaultTableAttributes()]]. - * - **`label`** – The human-facing label of the source. - * - **`status`** – The status color that should be shown beside the source label. Possible values include `green`, - * `orange`, `red`, `yellow`, `pink`, `purple`, `blue`, `turquoise`, `light`, `grey`, `black`, and `white`. (Optional) - * - **`badgeCount`** – The badge count that should be displayed alongside the label. (Optional) - * - **`badgeLabel`** – The badge count label that should be provided for screen readers. (Optional) - * - **`sites`** – An array of site IDs or UUIDs that the source should be shown for, on multi-site element indexes. - * (Optional; by default the source will be shown for all sites.) - * - **`criteria`** – An array of element criteria parameters that the source should use when the source is selected. - * (Optional) - * - **`data`** – An array of `data-X` attributes that should be set on the source’s `
` tag in the source list’s, - * HTML, where each key is the name of the attribute (without the “data-” prefix), and each value is the value of - * the attribute. (Optional) - * - **`defaultSort`** – A string identifying the sort attribute that should be selected by default, or an array where - * the first value identifies the sort attribute, and the second determines which direction to sort by. (Optional) - * - **`defaultFilter`** – An element condition instance or config, which should be used by default when the source - * is first selected. - * - **`hasThumbs`** – A bool that defines whether this source supports Thumbs View. (Use your element’s - * [[getThumbUrl()]] method to define your elements’ thumb URL.) (Optional) - * - **`structureId`** – The ID of the Structure that contains the elements in this source. If set, Structure View - * will be available to this source. (Optional) - * - **`newChildUrl`** – The URL that should be loaded when a user selects the “New child” menu option on an - * element in this source while it is in Structure View. (Optional) - * - **`nested`** – An array of sources that are nested within this one. Each nested source can have the same keys - * as top-level sources. - * - * ::: tip - * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::defineSources()]] - * instead of this method. - * ::: - * - * @param string $context The context ('index', 'modal', 'field', or 'settings'). - * @return array The sources. - */ - public static function sources(string $context): array; - - /** - * Returns a source definition by a given source key/path and context. - * - * @param string $sourceKey - * @param string|null $context - * @return array|null - * @since 4.4.0 - */ - public static function findSource(string $sourceKey, ?string $context): ?array; - - /** - * Returns the source path for a given source key, step key, and context. - * - * @param string $sourceKey - * @param string $stepKey - * @param string|null $context - * @return array[]|null - * @since 4.4.12 - */ - public static function sourcePath(string $sourceKey, string $stepKey, ?string $context): ?array; - - /** - * Returns all the field layouts associated with elements from the given source. - * - * This is used to determine which custom fields should be included in the element index sort menu, - * and other things. - * - * @param string|null $source The selected source’s key, or `null` if all known field layouts should be returned - * @return FieldLayout[] - * @since 3.5.0 - */ - public static function fieldLayouts(?string $source): array; - - /** - * Modifies a custom source’s config, before it’s returned by [[craft\services\ElementSources::getSources()]] - * - * @param array $config - * @return array - * @since 4.5.0 - */ - public static function modifyCustomSource(array $config): array; - - /** - * Returns the available [bulk element actions](https://craftcms.com/docs/5.x/extend/element-actions.html) - * for a given source. - * - * The actions can be represented by their fully qualified class name, a config array with the class name - * set to a `type` key, or by an instantiated element action object. - * - * ::: tip - * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::defineActions()]] - * instead of this method. - * ::: - * - * @param string $source The selected source’s key. - * @return array The available bulk element actions. - * @phpstan-return array|array{type:class-string}> - */ - public static function actions(string $source): array; - - /** - * Returns the available export options for a given source. - * - * The exporters can be represented by their fully qualified class name, a config array with the class name - * set to a `type` key, or by an instantiated element exporter object. - * - * ::: tip - * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::defineExporters()]] - * instead of this method. - * ::: - * - * @param string $source The selected source’s key. - * @return array The available element exporters. - * @since 3.4.0 - */ - public static function exporters(string $source): array; - - /** - * Defines which element attributes should be searchable. - * - * This method should return an array of attribute names that can be accessed on your elements. - * [[\craft\services\Search]] will call this method when it is indexing keywords for one of your elements, - * and for each attribute it returns, it will fetch the corresponding property’s value on the element. - * For example, if your elements have a “color” attribute which you want to be indexed, this method could return: - * - * ```php - * return ['color']; - * ``` - * - * Not only will the “color” attribute’s values start getting indexed, but users will also be able to search - * directly against that attribute’s values using this search syntax: - * - * color:blue - * - * There is no need for this method to worry about the ‘title’ or ‘slug’ attributes, or custom field handles; - * those are indexed automatically. - * - * ::: tip - * Element types that extend [[\craft\base\Element]] should override - * [[\craft\base\Element::defineSearchableAttributes()]] instead of this method. - * ::: - * - * @return string[] The element attributes that should be searchable - */ - public static function searchableAttributes(): array; - - /** - * Returns the base attributes that should be applied when bulk-duplicating elements of this type. - * - * @return array - * @since 5.7.0 - */ - public static function baseBulkDuplicateAttributes(): array; - - /** - * Returns the element index HTML. - * - * @param ElementQueryInterface $elementQuery - * @param int[]|null $disabledElementIds - * @param array $viewState - * @param string|null $sourceKey - * @param string|null $context - * @param bool $includeContainer - * @param bool $selectable - * @param bool $sortable - * @return string The element index HTML - */ - public static function indexHtml( - ElementQueryInterface $elementQuery, - ?array $disabledElementIds, - array $viewState, - ?string $sourceKey, - ?string $context, - bool $includeContainer, - bool $selectable, - bool $sortable, - ): string|Stringable; - - /** - * Returns the total number of elements that will be shown on an element index, for the given element query. - * - * @param ElementQueryInterface $elementQuery - * @param string|null $sourceKey - * @return int - * @since 4.4.0 - */ - public static function indexElementCount(ElementQueryInterface $elementQuery, ?string $sourceKey): int; - - /** - * Returns the sort options for the element type. - * - * This method should return an array, where each item is a sub-array with the following keys: - * - * - `label` – The sort option label - * - `orderBy` – An array, comma-delimited string, or a callback function that defines the columns to order the query by. If set to a callback - * function, the function will be passed two arguments: `$dir` (either `SORT_ASC` or `SORT_DESC`) and `$db` (a [[\craft\db\Connection]] object), - * and it should return an array of column names or an [[\yii\db\ExpressionInterface]] object. - * - `attribute` _(optional)_ – The [[tableAttributes()|table attribute]] name that this option is associated - * with (required if `orderBy` is an array or more than one column name) - * - `defaultDir` _(optional)_ – The default sort direction that should be used when sorting by this option - * (set to either `asc` or `desc`). Defaults to `asc` if not specified. - * - * ```php - * return [ - * [ - * 'label' => \CraftCms\Cms\t('Attribute Label'), - * 'orderBy' => 'columnName', - * 'attribute' => 'attributeName', - * 'defaultDir' => 'asc', - * ], - * ]; - * ``` - * - * A shorthand syntax is also supported, if there is no corresponding table attribute, or the table attribute - * has the exact same name as the column. - * - * ```php - * return [ - * 'columnName' => \CraftCms\Cms\t('Attribute Label'), - * ]; - * ``` - * - * Note that this method will only get called once for the entire index; not each time that a new source is - * selected. - * - * @return array The attributes that elements can be sorted by - */ - public static function sortOptions(): array; - - /** - * Returns the view modes available for the element type. - * - * This method should return an array, where each item is a sub-array with the following keys: - * - * - `mode` – Name of the view mode - * - `title` – How this mode should be described to the user - * - `icon` – Icon representing this view mode - * - `availableOnMobile` - Whether the view mode is available on mobile devices (defaults to `true`) - * - `structuresOnly` – Whether the view mode should only be available for structured sources (defaults to `false`) - * - * ```php - * return [ - * [ - * 'mode' => 'table', - * 'title' => \CraftCms\Cms\t('Display in a table'), - * 'icon' => 'list', - * 'availableOnMobile' => false, - * ], - * ]; - * ``` - * - * @return array The view modes. - * @since 5.5.0 - */ - public static function indexViewModes(): array; - - /** - * Defines all of the available columns that can be shown in table views. - * - * This method should return an array whose keys represent element attribute names, and whose values make - * up the table’s column headers. - * - * @return array The table attributes. - */ - public static function tableAttributes(): array; - - /** - * Returns the list of table attribute keys that should be shown by default. - * - * This method should return an array where each element in the array maps to one of the keys of the array returned - * by [[tableAttributes()]]. - * - * @param string $source The selected source’s key - * @return string[] The table attribute keys - */ - public static function defaultTableAttributes(string $source): array; - - /** - * Defines all the available attributes that can be shown in card views. - * - * This method should return an array whose keys represent element attribute names, and whose values make - * up the table’s column headers. - * - * @param \CraftCms\Cms\FieldLayout\FieldLayout|null $fieldLayout - * - * @return array The card attributes. - * - * @since 5.9.0 - * @since 5.5.0 - */ - public static function cardAttributes(?FieldLayout $fieldLayout = null): array; - - /** - * Returns the list of card attribute keys that should be shown by default, if the field layout hasn't been customised. - * - * This method should return an array where each element in the array maps to one of the keys of the array returned - * by [[cardAttributes()]]. - * - * @return string[] The card attribute keys - * @since 5.5.0 - */ - public static function defaultCardAttributes(): array; - - /** - * Return HTML for the attribute in the card preview. - * - * @param array $attribute - * @return mixed - * @since 5.5.0 - */ - public static function attributePreviewHtml(array $attribute): mixed; - - /** - * Returns an array that maps source-to-target element IDs based on the given sub-property handle. - * - * This method aids in the eager-loading of elements when performing an element query. The returned array should - * contain the following keys: - * - `map` – an array defining source-target element mappings - * - `elementType` *(optional)* – the fully qualified class name of the element type that should be eager-loaded, - * if each target element is of the same element type - * - `criteria` *(optional)* – any criteria parameters that should be applied to the element query when fetching the - * eager-loaded elements - * - `createElement` *(optional)* - an element factory function, which will be passed the element query, the current - * query result data, and the first source element that the result was eager-loaded for - * - * Each mapping listed in `map` should be an array with the following keys: - * - `source` – the source element ID - * - `target` – the target element ID - * - `elementType` *(optional)* – the target element type (only checked for if the top-level array doesn’t specify - * an `elementType` key) - * - * ```php - * use craft\base\ElementInterface; - * use craft\db\Query; - * - * public static function eagerLoadingMap(array $sourceElements, string $handle) - * { - * switch ($handle) { - * case 'author': - * $bookIds = array_map(fn(ElementInterface $element) => $element->id, $sourceElements); - * $map = (new Query) - * ->select(['source' => 'id', 'target' => 'authorId']) - * ->from('{{%books}}') - * ->where(['id' => $bookIds) - * ->all(); - * return [ - * 'elementType' => \my\plugin\Author::class, - * 'map' => $map, - * ]; - * case 'bookClubs': - * $bookIds = array_map(fn(ElementInterface $element) => $element->id, $sourceElements); - * $map = (new Query) - * ->select(['source' => 'bookId', 'target' => 'clubId']) - * ->from('{{%bookclub_books}}') - * ->where(['bookId' => $bookIds) - * ->all(); - * return [ - * 'elementType' => \my\plugin\BookClub::class, - * 'map' => $map, - * ]; - * default: - * return parent::eagerLoadMap($sourceElements, $handle); - * } - * } - * ``` - * - * Alternatively, the method can return an array of multiple sets of mappings, each with their own nested `map`, - * `elementType`, `criteria`, and `createElement` keys. - * - * @param self[] $sourceElements An array of the source elements - * @param string $handle The property handle used to identify which target elements should be included in the map - * @return EagerLoadingMap|EagerLoadingMap[]|null|false The eager-loading element ID mappings, false if no mappings - * exist, or null if the result should be ignored - */ - public static function eagerLoadingMap(array $sourceElements, string $handle): array|null|false; - - /** - * Returns the base GraphQL type name that represents elements of this type. - * - * @return Type - * @since 5.7.0 - */ - public static function baseGqlType(): Type; - - /** - * Returns the GraphQL scopes required by element’s context. - * - * @param mixed $context The element’s context, such as a volume, entry type or Matrix block type. - * @return array - * @since 3.3.0 - */ - public static function gqlScopesByContext(mixed $context): array; - - /** - * Returns whether this is a draft. - * - * @return bool - * @since 3.2.0 - */ - public function getIsDraft(): bool; - - /** - * Returns whether this is a revision. - * - * @return bool - * @since 3.2.0 - */ - public function getIsRevision(): bool; - - /** - * Returns whether this is the canonical element. - * - * @return bool - * @since 3.7.0 - */ - public function getIsCanonical(): bool; - - /** - * Returns whether this is a derivative element, such as a draft or revision. - * - * @return bool - * @since 3.7.0 - */ - public function getIsDerivative(): bool; - - /** - * Returns the canonical version of the element. - * - * If this is a draft or revision, the canonical element will be returned. - * - * @param bool $anySite Whether the canonical element can be retrieved in any site - * @return self - * @since 3.7.0 - */ - public function getCanonical(bool $anySite = false): self; - - /** - * Sets the canonical version of the element. - * - * @param self $element - * @since 3.7.0 - */ - public function setCanonical(self $element): void; - - /** - * Returns the element’s canonical ID. - * - * If this is a draft or revision, the canonical element’s ID will be returned. - * - * @return int|null - * @since 3.7.0 - */ - public function getCanonicalId(): ?int; - - /** - * Sets the element’s canonical ID. - * - * @param int|null $canonicalId - * @since 3.7.0 - */ - public function setCanonicalId(?int $canonicalId): void; - - /** - * Returns the element’s canonical UUID. - * - * If this is a draft or revision, the canonical element’s UUID will be returned. - * - * @return string|null - * @since 3.7.11 - */ - public function getCanonicalUid(): ?string; - - /** - * Returns whether the element is an unpublished draft. - * - * @return bool - * @since 3.6.0 - */ - public function getIsUnpublishedDraft(): bool; - - /** - * Merges changes from the canonical element into this one. - * - * @see \CraftCms\Cms\Element\Elements::mergeCanonicalChanges() - * @since 3.7.0 - */ - public function mergeCanonicalChanges(): void; - - /** - * Returns the field layout used by this element. - * - * @return FieldLayout|null - */ - public function getFieldLayout(): ?FieldLayout; - - /** - * Returns the site the element is associated with. - * - * @return Site - */ - public function getSite(): Site; - - /** - * Returns the language of the element. - * - * @return string - * @since 3.5.0 - */ - public function getLanguage(): string; - - /** - * Returns the sites this element is associated with. - * - * The function can either return an array of site IDs, or an array of sub-arrays, - * each with the following keys: - * - * - `siteId` (integer) - The site ID - * - `propagate` (boolean) – Whether the element should be propagated to this site on save (`true` by default) - * - `enabledByDefault` (boolean) – Whether the element should be enabled in this site by default - * (`true` by default) - * - * @return array - */ - public function getSupportedSites(): array; - - /** - * Returns the URI format used to generate this element’s URI. - * - * Note that element types that can have URIs must return `true` from [[hasUris()]]. - * - * @return string|null - * @see hasUris() - * @see getRoute() - */ - public function getUriFormat(): ?string; - - /** - * Returns the search keywords for a given search attribute. - * - * @param string $attribute - * @return string - */ - public function getSearchKeywords(string $attribute): string; - - /** - * Returns the route that should be used when the element’s URI is requested. - * - * ::: tip - * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::route()]] - * instead of this method. - * ::: - * - * @return mixed The route that the request should use, or null if no special action should be taken - */ - public function getRoute(): mixed; - - /** - * Returns whether this element represents the site homepage. - * - * @return bool - * @since 3.3.6 - */ - public function getIsHomepage(): bool; - - /** - * Returns the element’s full URL. - * - * @return string|null - */ - public function getUrl(): ?string; - - /** - * Returns an anchor pre-filled with this element’s URL and title. - * - * @return Markup|null - */ - public function getLink(): ?Markup; - - /** - * Returns the breadcrumbs that lead up to the element. - * - * @return array - * @since 5.0.0 - */ - public function getCrumbs(): array; - - /** - * Defines what the element should be called within the control panel. - * - * @param string|null $label - * @since 3.6.3 - */ - public function setUiLabel(?string $label): void; - - /** - * Returns any path segment labels that should be prepended to the element’s UI label. - * - * @return string[] - * @since 4.4.0 - */ - public function getUiLabelPath(): array; - - /** - * Defines any path segment labels that should be prepended to the element’s UI label. - * - * @param string[] $path - * @since 4.4.0 - */ - public function setUiLabelPath(array $path): void; - - /** - * Returns the label HTML for element chips. - * - * @return string - * @since 5.0.0 - */ - public function getChipLabelHtml(): string|Stringable; - - /** - * Returns whether chips and cards for this element should include a status indicator. - * - * @return bool - * @since 5.4.0 - */ - public function showStatusIndicator(): bool; - - /** - * Returns the titlebar label for element cards. - * - * @return string|null - * @since 5.7.0 - */ - public function getCardTitle(): ?string; - - /** - * Returns the body HTML for element cards. - * - * @return string|null - * @since 5.0.0 - */ - public function getCardBodyHtml(): ?string; - - /** - * Returns the reference string to this element. - * - * @return string|null - */ - public function getRef(): ?string; - - /** - * Creates a new element (without saving it) based on this one. - * - * This will be called by the “Save and add another” action on the element’s edit page. - * - * Note that permissions don’t need to be considered here. The created element’s [[canSave()]] method will be called before saving. - * - * @return self|null - */ - public function createAnother(): ?self; - - /** - * Returns whether the given user is authorized to view this element’s edit page. - * - * If they can view but not [[canSave()|save]], the edit form will either render statically, - * or be restricted to only saving changes as a draft, depending on [[canCreateDrafts()]]. - * - * @param User $user - * @return bool - * @since 4.0.0 - */ - public function canView(User $user): bool; - - /** - * Returns whether the given user is authorized to save this element in its current form. - * - * This will only be called if the element can be [[canView()|viewed]]. - * - * @param User $user - * @return bool - * @since 4.0.0 - */ - public function canSave(User $user): bool; - - /** - * Returns whether the given user is authorized to duplicate this element. - * - * This will only be called if the element can be [[canView()|viewed]] and/or [[canSave()|saved]]. - * - * @param User $user - * @return bool - * @since 4.0.0 - */ - public function canDuplicate(User $user): bool; - - /** - * Returns whether the given user is authorized to duplicate this element as an unpublished draft. - * - * @param User $user - * @return bool - * @since 5.0.0 - */ - public function canDuplicateAsDraft(User $user): bool; - - /** - * Returns whether the given user is authorized to copy this element, to be duplicated elsewhere. - * - * @param User $user - * @return bool - * @since 5.7.0 - */ - public function canCopy(User $user): bool; - - /** - * Returns whether the given user is authorized to delete this element. - * - * This will only be called if the element can be [[canView()|viewed]] and/or [[canSave()|saved]]. - * - * @param User $user - * @return bool - * @since 4.0.0 - */ - public function canDelete(User $user): bool; - - /** - * Returns whether the given user is authorized to delete this element for its current site. - * - * This will only be called if the element can be [[canView()|viewed]] and/or [[canSave()|saved]]. - * - * @param User $user - * @return bool - * @since 4.0.0 - */ - public function canDeleteForSite(User $user): bool; - - /** - * Returns whether the given user is authorized to create drafts for this element. - * - * This will only be called if the element can be [[canView()|viewed]] and/or [[canSave()|saved]]. - * - * ::: tip - * If this is going to return `true` under any circumstances, make sure [[trackChanges()]] is returning `true`, - * so drafts can be automatically updated with upstream content changes. - * ::: - * - * @param User $user - * @return bool - * @since 4.0.0 - */ - public function canCreateDrafts(User $user): bool; - - /** - * Returns whether revisions should be created when this element is saved. - * - * @return bool - * @since 4.0.0 - */ - public function hasRevisions(): bool; - - /** - * Prepares the response for the element’s Edit screen. - * - * @param Response $response The response being prepared - * @param string $containerId The ID of the element editor’s container element - * @since 4.0.0 - */ - public function prepareEditScreen(Response|CpScreenResponse $response, string $containerId): void; - - /** - * Returns the URL that users should be redirected to after editing the element. - * - * @return string|null - * @since 4.0.0 - */ - public function getPostEditUrl(): ?string; - - /** - * Returns the element’s revisions index URL in the control panel. - * - * @return string|null - * @since 4.4.0 - */ - public function getCpRevisionsUrl(): ?string; - - /** - * Returns additional buttons that should be shown at the top of the element’s edit page. - * - * @return string - * @since 4.0.0 - */ - public function getAdditionalButtons(): string|Stringable; - - /** - * Returns alternative form actions for the element. - * - * See [[\craft\web\CpScreenResponseBehavior::altActions()]] for documentation on supported action properties. - * - * @return array - * @since 5.6.0 - */ - public function getAltActions(): array; - - /** - * Returns the additional locations that should be available for previewing the element, besides its primary [[getUrl()|URL]]. - * - * Each target should be represented by a sub-array with the following keys: - * - * - `label` – What the preview target will be called in the control panel. - * - `url` – The URL that the preview target should open. - * - `refresh` – Whether preview frames should be automatically refreshed when content changes (`true` by default). - * - * ::: tip - * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::previewTargets()]] - * instead of this method. - * ::: - * - * @return array - * @since 3.2.0 - */ - public function getPreviewTargets(): array; - - /** - * Returns whether the element is enabled for the current site. - * - * This can also be set to an array of site ID/site-enabled mappings. - * - * @param int|null $siteId The ID of the site to return for. If `null`, the current site status will be returned. - * @return bool|null Whether the element is enabled for the given site. `null` will be returned if a `$siteId` was - * passed, but that site’s status wasn’t provided via [[setEnabledForSite()]]. - * @since 3.4.0 - */ - public function getEnabledForSite(?int $siteId = null): ?bool; - - /** - * Sets whether the element is enabled for the current site. - * - * This can also be set to an array of site ID/site-enabled mappings. - * - * @param bool|bool[] $enabledForSite - * @since 3.4.0 - */ - public function setEnabledForSite(array|bool $enabledForSite): void; - - /** - * Returns the root owner element. - * - * @return self - * @since 5.4.0 - */ - public function getRootOwner(): self; - - /** - * Returns the same element in other locales. - * - * @return ElementQueryInterface|ElementCollection - */ - public function getLocalized(): ElementQueryInterface|ElementQuery|ElementCollection; - - /** - * Returns a query for the same element in other locales. - * - * @return ElementQueryInterface - */ - public function getLocalizedQuery(): ElementQueryInterface; - - /** - * Returns the next element relative to this one, from a given set of criteria. - * - * @param mixed $criteria - * @return self|null - */ - public function getNext(mixed $criteria = false): ?self; - - /** - * Returns the previous element relative to this one, from a given set of criteria. - * - * @param mixed $criteria - * @return self|null - */ - public function getPrev(mixed $criteria = false): ?self; - - /** - * Sets the default next element. - * - * @param self|false $element - */ - public function setNext(self|false $element): void; - - /** - * Sets the default previous element. - * - * @param self|false $element - */ - public function setPrev(self|false $element): void; - - /** - * Returns the element’s parent. - * - * @return self|null - */ - public function getParent(): ?self; - - /** - * Returns the parent element’s URI, if there is one. - * - * If the parent’s URI is `__home__` (the homepage URI), then `null` will be returned. - * - * @return string|null - */ - public function getParentUri(): ?string; - - /** - * Sets the element’s parent. - * - * @param self|null $parent - */ - public function setParent(?self $parent): void; - - /** - * Returns the element’s ancestors. - * - * @param int|null $dist - * @return ElementQueryInterface|ElementCollection - */ - public function getAncestors(?int $dist = null): ElementQueryInterface|ElementQuery|ElementCollection; - - /** - * Returns the element’s descendants. - * - * @param int|null $dist - * @return ElementQueryInterface|ElementCollection - */ - public function getDescendants(?int $dist = null): ElementQueryInterface|ElementQuery|ElementCollection; - - /** - * Returns the element’s children. - * - * @return ElementQueryInterface|ElementCollection - */ - public function getChildren(): ElementQueryInterface|ElementQuery|ElementCollection; - - /** - * Returns all of the element’s siblings. - * - * @return ElementQueryInterface|ElementCollection - */ - public function getSiblings(): ElementQueryInterface|ElementQuery|ElementCollection; - - /** - * Returns the element’s previous sibling. - * - * @return self|null - */ - public function getPrevSibling(): ?self; - - /** - * Returns the element’s next sibling. - * - * @return self|null - */ - public function getNextSibling(): ?self; - - /** - * Returns whether the element has descendants. - * - * @return bool - */ - public function getHasDescendants(): bool; - - /** - * Returns the total number of descendants that the element has. - * - * @return int - */ - public function getTotalDescendants(): int; - - /** - * Returns whether this element is an ancestor of another one. - * - * @param self $element - * @return bool - */ - public function isAncestorOf(self $element): bool; - - /** - * Returns whether this element is a descendant of another one. - * - * @param self $element - * @return bool - */ - public function isDescendantOf(self $element): bool; - - /** - * Returns whether this element is a direct parent of another one. - * - * @param self $element - * @return bool - */ - public function isParentOf(self $element): bool; - - /** - * Returns whether this element is a direct child of another one. - * - * @param self $element - * @return bool - */ - public function isChildOf(self $element): bool; - - /** - * Returns whether this element is a sibling of another one. - * - * @param self $element - * @return bool - */ - public function isSiblingOf(self $element): bool; - - /** - * Returns whether this element is the direct previous sibling of another one. - * - * @param self $element - * @return bool - */ - public function isPrevSiblingOf(self $element): bool; - - /** - * Returns whether this element is the direct next sibling of another one. - * - * @param self $element - * @return bool - */ - public function isNextSiblingOf(self $element): bool; - - /** - * Treats custom fields as array offsets. - * - * @param string|int $offset - * @return bool - */ - public function offsetExists($offset): bool; - - /** - * Sets the element’s attributes from an element editor submission. - * - * @param array $values The attribute values - * @since 5.0.0 - */ - public function setAttributesFromRequest(array $values): void; - - /** - * Returns the status of a given attribute. - * - * @param string $attribute - * @return array{0:AttributeStatus|value-of,1:string}|null - * @since 3.4.0 - */ - public function getAttributeStatus(string $attribute): ?array; - - /** - * Returns the attribute names that have been updated on the canonical element since the last time it was - * merged into this element. - * - * @return string[] - * @since 3.7.0 - */ - public function getOutdatedAttributes(): array; - - /** - * Returns whether an attribute value has fallen behind the canonical element’s value. - * - * @param string $name - * @return bool - * @since 3.7.0 - */ - public function isAttributeOutdated(string $name): bool; - - /** - * Returns the attribute names that have changed for this element. - * - * @return string[] - * @since 3.7.0 - */ - public function getModifiedAttributes(): array; - - /** - * Returns whether an attribute value has changed for this element. - * - * @param string $name - * @return bool - * @since 3.7.0 - */ - public function isAttributeModified(string $name): bool; - - /** - * Returns whether an attribute has changed since the element was first loaded. - * - * @param string $name - * @return bool - * @since 3.5.0 - */ - public function isAttributeDirty(string $name): bool; - - /** - * Returns a list of attribute names that have changed since the element was first loaded. - * - * @return string[] - * @since 3.4.0 - */ - public function getDirtyAttributes(): array; - - /** - * Sets the list of dirty attribute names. - * - * @param string[] $names - * @param bool $merge Whether these attributes should be merged with existing dirty attributes - * @see getDirtyAttributes() - * @since 3.5.0 - */ - public function setDirtyAttributes(array $names, bool $merge = true): void; - - /** - * Returns whether the Title field should be shown as translatable in the UI. - * - * Note this method has no effect on whether titles will get copied over to other - * sites when the element is actually getting saved. That is determined by [[getTitleTranslationKey()]]. - * - * @return bool - * @since 3.5.0 - */ - public function getIsTitleTranslatable(): bool; - - /** - * Returns the description of the Title field’s translation support. - * - * @return string|null - * @since 3.5.0 - */ - public function getTitleTranslationDescription(): ?string; - - /** - * Returns the Title’s translation key. - * - * When saving an element on a multi-site Craft install, if `$propagate` is `true` for [[\craft\services\Elements::saveElement()]], - * then `getTitleTranslationKey()` will be called for each site the element should be propagated to. - * If the method returns the same value as it did for the initial site, then the initial site’s title will be copied over - * to the target site. - * - * @return string The translation key - * @since 3.5.0 - */ - public function getTitleTranslationKey(): string; - - /** - * Returns whether the Slug field should be shown as translatable in the UI. - * - * Note this method has no effect on whether slugs will get copied over to other - * sites when the element is actually getting saved. That is determined by [[getSlugTranslationKey()]]. - * - * @return bool - * @since 4.5.0 - */ - public function getIsSlugTranslatable(): bool; - - /** - * Returns the description of the Slug field’s translation support. - * - * @return string|null - * @since 4.5.0 - */ - public function getSlugTranslationDescription(): ?string; - - /** - * Returns the Slug’s translation key. - * - * When saving an element on a multi-site Craft install, if `$propagate` is `true` for [[\craft\services\Elements::saveElement()]], - * then `getSlugTranslationKey()` will be called for each site the element should be propagated to. - * If the method returns the same value as it did for the initial site, then the initial site’s slug will be copied over - * to the target site. - * - * @return string The translation key - * @since 4.5.0 - */ - public function getSlugTranslationKey(): string; - - /** - * Returns whether a field is empty. - * - * @param string $handle - * @return bool - */ - public function isFieldEmpty(string $handle): bool; - - /** - * Returns the element’s normalized custom field values, indexed by their handles. - * - * @param string[]|null $fieldHandles The list of field handles whose values - * need to be returned. Defaults to null, meaning all fields’ values will be - * returned. If it is an array, only the fields in the array will be returned. - * @return array The field values (handle => value) - */ - public function getFieldValues(?array $fieldHandles = null): array; - - /** - * Returns an array of the element’s serialized custom field values, indexed by their handles. - * - * @param string[]|null $fieldHandles The list of field handles whose values - * need to be returned. Defaults to null, meaning all fields’ values will be - * returned. If it is an array, only the fields in the array will be returned. - * @return array - */ - public function getSerializedFieldValues(?array $fieldHandles = null): array; - - /** - * Returns an array of the element’s serialized custom field values, indexed by their handles, - * for database storage. - * - * @param string[]|null $fieldHandles The list of field handles whose values - * need to be returned. Defaults to null, meaning all fields’ values will be - * returned. If it is an array, only the fields in the array will be returned. - * @return array - * @since 5.7.0 - */ - public function getSerializedFieldValuesForDb(?array $fieldHandles = null): array; - - /** - * Sets the element’s custom field values. - * - * @param array $values The custom field values (handle => value) - */ - public function setFieldValues(array $values): void; - - /** - * Returns the value for a given field. - * - * @param string $fieldHandle The field handle whose value needs to be returned - * @return mixed The field value - * @throws InvalidFieldException if the element doesn’t have a field with the handle specified by `$fieldHandle` - */ - public function getFieldValue(string $fieldHandle): mixed; - - /** - * Sets the value for a given field. - * - * @param string $fieldHandle The field handle whose value needs to be set - * @param mixed $value The value to set on the field - */ - public function setFieldValue(string $fieldHandle, mixed $value): void; - - /** - * Sets the value for a given field. The value should have originated from post data. - * - * @param string $fieldHandle The field handle whose value needs to be set - * @param mixed $value The value to set on the field - * @throws InvalidFieldException if `$fieldHandle` is an invalid field handle - * @since 4.5.0 - */ - public function setFieldValueFromRequest(string $fieldHandle, mixed $value): void; - - /** - * Returns the field handles that have been updated on the canonical element since the last time it was - * merged into this element. - * - * @return string[] - * @since 3.7.0 - */ - public function getOutdatedFields(): array; - - /** - * Returns whether a field value has fallen behind the canonical element’s value. - * - * @param string $fieldHandle - * @return bool - * @since 3.7.0 - */ - public function isFieldOutdated(string $fieldHandle): bool; - - /** - * Returns the field handles that have changed for this element. - * - * @param bool $anySite Whether to check for fields that have changed across any site - * @return string[] - * @since 3.7.0 - */ - public function getModifiedFields(bool $anySite = false): array; - - /** - * Returns whether a field value has changed for this element. - * - * @param string $fieldHandle - * @param bool $anySite Whether to check if the field has changed across any site - * @return bool - * @since 3.7.0 - */ - public function isFieldModified(string $fieldHandle, bool $anySite = false): bool; - - /** - * Returns whether a custom field value has changed since the element was first loaded. - * - * @param string $fieldHandle - * @return bool - * @since 3.4.0 - */ - public function isFieldDirty(string $fieldHandle): bool; - - /** - * Returns a list of custom field handles that have changed since the element was first loaded. - * - * @return string[] - * @since 3.4.0 - */ - public function getDirtyFields(): array; - - /** - * Sets the list of dirty field handles. - * - * @param string[] $fieldHandles - * @param bool $merge Whether these fields should be merged with existing dirty fields - * @see getDirtyFields() - * @since 4.5.0 - */ - public function setDirtyFields(array $fieldHandles, bool $merge = true): void; - - /** - * Marks all fields and attributes as dirty. - * - * @since 3.4.10 - */ - public function markAsDirty(): void; - - /** - * Resets the record of dirty attributes and fields. - * - * @since 3.4.0 - */ - public function markAsClean(): void; - - /** - * Returns the cache tags that should be cleared when this element is saved. - * - * @return string[] - * @since 3.5.0 - */ - public function getCacheTags(): array; - - /** - * Sets the element’s custom field values, when the values have come from post data. - * - * @param string $paramNamespace The field param namespace - */ - public function setFieldValuesFromRequest(string $paramNamespace): void; - - /** - * Returns the namespace used by custom field params on the request. - * - * @return string|null The field param namespace - */ - public function getFieldParamNamespace(): ?string; - - /** - * Sets the namespace used by custom field params on the request. - * - * @param string $namespace The field param namespace - */ - public function setFieldParamNamespace(string $namespace): void; - - /** - * Returns the field context this element’s content uses. - * - * @return string - */ - public function getFieldContext(): string; - - /** - * Returns the generated field values for the element, indexed by handle. - * - * @return array - * @since 5.8.0 - */ - public function getGeneratedFieldValues(): array; - - /** - * Sets the generated field values for the element, indexed by handle. - * - * @param array $values - * @since 5.8.0 - */ - public function setGeneratedFieldValues(array $values): void; - - /** - * Returns the element’s invalid nested element IDs. - * - * @return int[] - * @since 5.3.0 - */ - public function getInvalidNestedElementIds(): array; - - /** - * Registers invalid nested element IDs with the element, so an `error` class can be added on their cards. - * - * @param int[] $ids - * @since 5.3.0 - */ - public function addInvalidNestedElementIds(array $ids): void; - - /** - * Returns whether elements have been eager-loaded with a given handle. - * - * @param string $handle The handle of the eager-loaded elements - * @return bool Whether elements have been eager-loaded with the given handle - */ - public function hasEagerLoadedElements(string $handle): bool; - - /** - * Returns the eager-loaded elements for a given handle. - * - * @param string $handle The handle of the eager-loaded elements - * @return ElementCollection|null The eager-loaded elements, or null if they hadn't been eager-loaded - */ - public function getEagerLoadedElements(string $handle): ?ElementCollection; - - /** - * Sets some eager-loaded elements on a given handle. - * - * @param string $handle The handle that was used to eager-load the elements - * @param self[] $elements The eager-loaded elements - * @param EagerLoadPlan $plan The eager-loading plan - */ - public function setEagerLoadedElements(string $handle, array $elements, EagerLoadPlan $plan): void; - - /** - * Sets whether the given eager-loaded element handles were eager-loaded lazily. - * - * @param string $handle The handle that was used to eager-load the elements - * @param bool $value - */ - public function setLazyEagerLoadedElements(string $handle, bool $value = true): void; - - /** - * Returns the count of eager-loaded elements for a given handle. - * - * @param string $handle The handle of the eager-loaded elements - * @return int|null The eager-loaded element count, or null if it hadn't been eager-loaded - * @since 3.4.0 - */ - public function getEagerLoadedElementCount(string $handle): ?int; - - /** - * Sets the count of eager-loaded elements for a given handle. - * - * @param string $handle The handle to load the elements with in the future - * @param int $count The eager-loaded element count - * @since 3.4.0 - */ - public function setEagerLoadedElementCount(string $handle, int $count): void; - - /** - * Returns whether the element is "fresh" (not yet explicitly saved, and without validation errors). - * - * @return bool - * @since 3.7.14 - */ - public function getIsFresh(): bool; - - /** - * Sets whether the element is "fresh" (not yet explicitly saved, and without validation errors). - * - * @param bool $isFresh - * @since 3.7.14 - */ - public function setIsFresh(bool $isFresh = true): void; - - /** - * Sets the revision creator ID to be saved. - * - * @param int|null $creatorId - * @since 3.2.0 - */ - public function setRevisionCreatorId(?int $creatorId): void; - - /** - * Sets the revision notes to be saved. - * - * @param string|null $notes - * @since 3.2.0 - */ - public function setRevisionNotes(?string $notes): void; - - /** - * Returns the element’s current revision, if one exists. - * - * @return self|null - * @since 3.2.0 - */ - public function getCurrentRevision(): ?self; - - /** - * Return if the element is copyable between sites. - * Checks if it's a multisite installation, if user can edit the element in other sites, - * and if the element actually exists in other sites. - * - * @return bool - * @since 5.6.0 - */ - public function getIsCrossSiteCopyable(): bool; - - // Indexes, etc. - // ------------------------------------------------------------------------- - - /** - * Returns any attributes that should be included in the element’s chips and cards. - * - * The attribute HTML will be rendered with [[\yii\helpers\BaseHtml::renderTagAttributes()]]. - * - * ::: tip - * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::htmlAttributes()]] - * instead of this method. - * ::: - * - * @param string $context The context that the element is being rendered in ('index', 'modal', 'field', or 'settings'.) - * @return array - */ - public function getHtmlAttributes(string $context): array; - - /** - * Returns the HTML that should be shown for a given attribute in table and card views. - * - * ::: tip - * Element types that extend [[\craft\base\Element]] should override [[\craft\base\Element::attributeHtml()]] - * instead of this method. - * ::: - * - * @param string $attribute The attribute name. - * @return string The HTML that should be shown for a given attribute in table and card views. - * @since 5.0.0 - */ - public function getAttributeHtml(string $attribute): string|Stringable; - - /** - * Returns the HTML that should be shown for a given attribute's inline editing input. - * - * @param string $attribute The attribute name. - * @return string The HTML that should be shown for the element input. - * @since 5.0.0 - */ - public function getInlineAttributeInputHtml(string $attribute): string|Stringable; - - /** - * Returns the HTML for any fields/info that should be shown within the editor sidebar. - * - * @param bool $static Whether any fields within the sidebar should be static (non-interactive) - * @return string - * @since 3.7.0 - */ - public function getSidebarHtml(bool $static): string|Stringable; - - /** - * Returns element metadata that should be shown within the editor sidebar. - * - * @return array The data, with keys representing the labels. The values can either be strings or callables. - * If a value is `false`, it will be omitted. - * @since 3.7.0 - */ - public function getMetadata(): array; - - /** - * Returns the GraphQL type name for this element type. - * - * @return string - * @since 3.3.0 - */ - public function getGqlTypeName(): string; - - // Events - // ------------------------------------------------------------------------- - - /** - * Performs actions before an element is saved. - * - * @param bool $isNew Whether the element is brand new - * @return bool Whether the element should be saved - */ - public function beforeSave(bool $isNew): bool; - - /** - * Performs actions after an element is saved. - * - * @param bool $isNew Whether the element is brand new - */ - public function afterSave(bool $isNew): void; - - /** - * Performs actions after an element is fully saved and propagated to other sites. - * - * ::: tip - * This will get called regardless of whether `$propagate` is `true` or `false` for [[\craft\services\Elements::saveElement()]]. - * ::: - * - * @param bool $isNew Whether the element is brand new - * @since 3.2.0 - */ - public function afterPropagate(bool $isNew): void; - - /** - * Performs actions before an element is deleted. - * - * @return bool Whether the element should be deleted - */ - public function beforeDelete(): bool; - - /** - * Performs actions after an element is deleted. - * - */ - public function afterDelete(): void; - - /** - * Performs actions before an element is deleted for a site. - * - * @return bool Whether the element should be deleted - * @since 4.7.0 - */ - public function beforeDeleteForSite(): bool; - - /** - * Performs actions after an element is deleted for a site. - * - * @since 4.7.0 - */ - public function afterDeleteForSite(): void; - - /** - * Performs actions before an element is restored. - * - * @return bool Whether the element should be restored - * @since 3.1.0 - */ - public function beforeRestore(): bool; - - /** - * Performs actions after an element is restored. - * - * @since 3.1.0 - */ - public function afterRestore(): void; - - /** - * Performs actions before an element is moved within a structure. - * - * @param int $structureId The structure ID - * @return bool Whether the element should be moved within the structure - */ - public function beforeMoveInStructure(int $structureId): bool; - - /** - * Performs actions after an element is moved within a structure. - * - * @param int $structureId The structure ID - */ - public function afterMoveInStructure(int $structureId): void; - - /** - * Returns the string representation of the element. - */ - public function __toString(): string; - - /** - * Renders the element using its partial template. - * - * If no partial template exists for the element, its string representation will be output instead. - * - * @param array $variables - * @return Markup - * @throws InvalidConfigException - * @throws NotSupportedException - * @since 5.8.0 - */ - public function render(array $variables = []): Markup; } diff --git a/yii2-adapter/legacy/base/Field.php b/yii2-adapter/legacy/base/Field.php index 983f77a68be..e8107f4f94e 100644 --- a/yii2-adapter/legacy/base/Field.php +++ b/yii2-adapter/legacy/base/Field.php @@ -9,6 +9,7 @@ use Closure; use Craft; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Validation\ElementRules; use CraftCms\Cms\Field\Enums\TranslationMethod; @@ -77,6 +78,10 @@ public function getElementValidationRules(): array public function getElementRules(ElementInterface $element): array { + if (!$element instanceof Model) { + return []; + } + return [ function(string $attribute, mixed $value, Closure $fail) use ($element) { $scenario = $element->ruleset->getScenario(); @@ -105,7 +110,7 @@ function(string $attribute, mixed $value, Closure $fail) use ($element) { private function _normalizeFieldValidator( string $attribute, mixed $rule, - ElementInterface $element, + Model $element, callable $isEmpty, ): Validator { if ($rule instanceof Validator) { diff --git a/yii2-adapter/legacy/base/Model.php b/yii2-adapter/legacy/base/Model.php index a06938ff565..b4da2cede18 100644 --- a/yii2-adapter/legacy/base/Model.php +++ b/yii2-adapter/legacy/base/Model.php @@ -280,11 +280,31 @@ public function attributes(): array return parent::attributes(); } + public function safeAttributes(): array + { + return parent::safeAttributes(); + } + + public function getAttributeLabel($attribute): string + { + return parent::getAttributeLabel($attribute); + } + + public function generateAttributeLabel($name): string + { + return parent::generateAttributeLabel($name); + } + public function errors(): MessageBag { return new \Illuminate\Support\MessageBag($this->getErrors()); } + public function clearErrors($attribute = null): void + { + parent::clearErrors($attribute); + } + /** * @inheritdoc */ diff --git a/yii2-adapter/legacy/base/NestedElementInterface.php b/yii2-adapter/legacy/base/NestedElementInterface.php index 7cae8ec3a40..1583737e0f8 100644 --- a/yii2-adapter/legacy/base/NestedElementInterface.php +++ b/yii2-adapter/legacy/base/NestedElementInterface.php @@ -1,16 +1,8 @@ * @since 5.0.0 + * @deprecated 6.0.0 use {@see \CraftCms\Cms\Element\Contracts\NestedElementInterface} instead. */ -interface NestedElementInterface extends ElementInterface +interface NestedElementInterface extends \CraftCms\Cms\Element\Contracts\NestedElementInterface { - /** - * Returns the primary owner element’s ID, if the element has one. - * - * @return int|null - * @throws InvalidConfigException if the element is misconfigured - */ - #[AllowedInSandbox] - public function getPrimaryOwnerId(): ?int; - - /** - * Sets the primary owner element’s ID, if the element has one. - * - * @param int|null $id - */ - public function setPrimaryOwnerId(?int $id): void; - - /** - * Returns the primary owner element, if the element has one. - * - * @return ElementInterface|null - * @throws InvalidConfigException if the element is misconfigured - */ - public function getPrimaryOwner(): ?ElementInterface; - - /** - * Sets the primary owner element, if the element has one. - * - * @param ElementInterface|null $owner - */ - public function setPrimaryOwner(?ElementInterface $owner): void; - - /** - * Returns the owner element’s ID, if the element has one. - * - * @return int|null - * @throws InvalidConfigException if the element is misconfigured - */ - #[AllowedInSandbox] - public function getOwnerId(): ?int; - - /** - * Sets the owner element’s ID, if the element has one. - * - * @param int|null $id - */ - public function setOwnerId(?int $id): void; - - /** - * Returns the owner element, if the element has one. - * - * @return ElementInterface|null - * @throws InvalidConfigException if the element is misconfigured - */ - public function getOwner(): ?ElementInterface; - - /** - * Sets the owner element, if the element has one. - * - * @param ElementInterface|null $owner - */ - public function setOwner(?ElementInterface $owner): void; - - /** - * Returns each of the element’s owners - * - * @param array $criteria - * @return ElementInterface[] - * @throws InvalidConfigException if the element is misconfigured - * @since 5.8.17 - */ - public function getOwners(array $criteria = []): array; - - /** - * Returns the field that contains the element. - * - * @return ElementContainerFieldInterface|null - * @throws InvalidConfigException if the element is misconfigured - */ - public function getField(): ?ElementContainerFieldInterface; - - /** - * Returns the element’s sort order, if it has one. - * - * @return int|null - */ - public function getSortOrder(): ?int; - - /** - * Sets the element’s sort order. - * - * @param int|null $sortOrder - */ - public function setSortOrder(?int $sortOrder): void; - - /** - * Sets whether the element’s ownership should be saved when the element is saved. - * - * @param bool $saveOwnership - */ - public function setSaveOwnership(bool $saveOwnership): void; } diff --git a/yii2-adapter/legacy/base/NestedElementTrait.php b/yii2-adapter/legacy/base/NestedElementTrait.php index f8a811ecfb5..42b28b094c7 100644 --- a/yii2-adapter/legacy/base/NestedElementTrait.php +++ b/yii2-adapter/legacy/base/NestedElementTrait.php @@ -8,16 +8,9 @@ namespace craft\base; -use CraftCms\Cms\Database\Table; -use CraftCms\Cms\Element\Data\EagerLoadPlan; +use CraftCms\Cms\Element\Concerns\NestedElement; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface; -use CraftCms\Cms\Field\Fields; -use CraftCms\Cms\Support\Facades\Elements; -use CraftCms\Cms\Support\Typecast; -use CraftCms\Cms\Twig\Attributes\AllowedInSandbox; -use Illuminate\Support\Facades\DB; -use Tpetry\QueryExpressions\Language\Alias; -use yii\base\InvalidConfigException; /** * NestedElementTrait @@ -30,506 +23,10 @@ * @mixin \CraftCms\Cms\Element\Element * @author Pixel & Tonic, Inc. * @since 5.0.0 + * @deprecated 6.0.0 use {@see \CraftCms\Cms\Element\Concerns\NestedElement} instead. * @phpstan-ignore trait.unused */ trait NestedElementTrait { - /** - * @inheritdoc - */ - public static function eagerLoadingMap(array $sourceElements, string $handle): array|null|false - { - switch ($handle) { - case 'owner': - case 'primaryOwner': - /** @phpstan-ignore-next-line */ - return [ - /** @phpstan-ignore-next-line */ - 'map' => array_filter(array_map(function(NestedElementInterface $element) use ($handle) { - $ownerId = match ($handle) { - 'owner' => $element->getOwnerId(), - 'primaryOwner' => $element->getPrimaryOwnerId(), - }; - return $ownerId ? [ - 'source' => $element->id, - 'target' => $ownerId, - ] : null; - }, $sourceElements)), - 'criteria' => [ - 'status' => null, - ], - ]; - default: - return parent::eagerLoadingMap($sourceElements, $handle); - } - } - - /** - * @var int|null Primary owner ID - */ - private ?int $primaryOwnerId = null; - - /** - * @var int|null Owner ID - */ - private ?int $ownerId = null; - - /** - * @var class-string|null Owner type - */ - private ?string $ownerType = null; - - /** - * @var int|null Field ID - */ - #[AllowedInSandbox] - public ?int $fieldId = null; - - /** - * @var int|null Sort order - */ - public ?int $sortOrder = null; - - /** - * @var bool Whether to save the element’s row in the `elements_owners` table from `afterSave()`. - */ - public bool $saveOwnership = true; - - /** - * @var bool Whether the search index should be updated for the owner element, alongside this element. - * - * This will only be checked if [[fieldId]] is set, and `false` isn’t passed to the `updateSearchIndex` - * argument of [[\craft\services\Elements::saveElement()]]. - * - * @since 5.2.0 - */ - public bool $updateSearchIndexForOwner = false; - - /** - * @var ElementInterface|false|null The primary owner element, or false if [[primaryOwnerId]] is invalid - * @see getPrimaryOwner() - * @see setPrimaryOwner() - */ - private ElementInterface|false|null $_primaryOwner = null; - - /** - * @var ElementInterface|false|null The owner element, or false if [[ownerId]] is invalid - * @see getOwner() - * @see setOwner() - */ - private ElementInterface|false|null $_owner = null; - - /** - * @var ElementInterface[] - * @see getOwners() - */ - private array $_owners; - - public function __clone(): void - { - parent::__clone(); - - $this->_primaryOwner = null; - $this->_owner = null; - $this->ownerType = null; - } - - public function __get($name) - { - return match ($name) { - 'ownerId' => $this->getOwnerId(), - 'primaryOwnerId' => $this->getPrimaryOwnerId(), - default => parent::__get($name), - }; - } - - /** - * @inheritdoc - */ - public function attributes(): array - { - $names = parent::attributes(); - $names[] = 'primaryOwnerId'; - $names[] = 'ownerId'; - return $names; - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $names = parent::extraFields(); - $names[] = 'primaryOwner'; - $names[] = 'owner'; - return $names; - } - - /** - * @inheritdoc - */ - public function getPrimaryOwnerId(): ?int - { - return $this->primaryOwnerId ?? $this->ownerId; - } - - /** - * @inheritdoc - */ - public function setPrimaryOwnerId(?int $id): void - { - $this->primaryOwnerId = $id; - - if (!$id || $this->_primaryOwner === false || $this->_primaryOwner?->id !== $id) { - $this->_primaryOwner = null; - } - } - - /** - * @inheritdoc - */ - public function getPrimaryOwner(): ?ElementInterface - { - if (!isset($this->_primaryOwner)) { - $primaryOwnerId = $this->getPrimaryOwnerId(); - if (!$primaryOwnerId) { - return null; - } - - $sameSiteElements = isset($this->id, $this->elementQueryResult) - ? array_filter($this->elementQueryResult, fn(ElementInterface $element) => $element->siteId === $this->siteId) - : []; - - if (!empty($sameSiteElements)) { - // Eager-load the primary owner for each of the elements in the result, - // as we're probably going to end up needing them too - Elements::eagerLoadElements($this::class, $sameSiteElements, [ - [ - 'path' => 'primaryOwner', - 'criteria' => $this->ownerCriteria(), - ], - ]); - } - - /** @phpstan-ignore-next-line */ - if (!isset($this->_primaryOwner) || $this->_primaryOwner === false) { - // Either we didn't try, or the primary owner couldn't be eager-loaded for some reason - $ownerType = $this->ownerType(); - if (!$ownerType) { - return null; - } - - $query = $ownerType::find()->id($primaryOwnerId); - Typecast::configure($query, $this->ownerCriteria()); - $this->_primaryOwner = $query->one() ?? false; - - if (!$this->_primaryOwner) { - throw new InvalidConfigException("Invalid owner ID: $primaryOwnerId"); - } - } - } - - return $this->_primaryOwner ?: null; - } - - /** - * @inheritdoc - */ - public function setPrimaryOwner(?ElementInterface $owner): void - { - $this->_primaryOwner = $owner ?? false; - $this->primaryOwnerId = $owner->id ?? null; - } - - /** - * @inheritdoc - */ - public function getOwnerId(): ?int - { - return $this->ownerId ?? $this->primaryOwnerId; - } - - /** - * @inheritdoc - */ - public function setOwnerId(?int $id): void - { - $this->ownerId = $id; - - if (!$id || $this->_owner === false || $this->_owner?->id !== $id) { - $this->_owner = null; - } - } - - /** - * @inheritdoc - */ - public function getOwner(): ?ElementInterface - { - if (!isset($this->_owner)) { - $ownerId = $this->getOwnerId(); - if (!$ownerId) { - return null; - } - - // If ownerId and primaryOwnerId are the same, return the primary owner - if ($ownerId === $this->getPrimaryOwnerId()) { - return $this->getPrimaryOwner(); - } - - $sameSiteElements = isset($this->id, $this->elementQueryResult) - ? array_filter($this->elementQueryResult, fn(ElementInterface $element) => $element->siteId === $this->siteId) - : []; - - if (!empty($sameSiteElements)) { - // Eager-load the owner for each of the elements in the result, - // as we're probably going to end up needing them too - Elements::eagerLoadElements($this::class, $sameSiteElements, [ - [ - 'path' => 'owner', - 'criteria' => $this->ownerCriteria(), - ], - ]); - } - - /** @phpstan-ignore-next-line */ - if (!isset($this->_owner) || $this->_owner === false) { - // Either we didn't try, or the owner couldn't be eager-loaded for some reason - $ownerType = $this->ownerType(); - if (!$ownerType) { - return null; - } - - $query = $ownerType::find()->id($ownerId); - Typecast::configure($query, $this->ownerCriteria()); - $this->_owner = $query->one() ?? false; - - if (!$this->_owner) { - throw new InvalidConfigException("Invalid owner ID: $ownerId"); - } - } - } - - return $this->_owner ?: null; - } - - /** - * @inheritdoc - */ - public function getOwners(array $criteria = []): array - { - if (!isset($this->_owners)) { - $this->_owners = []; - $ownerType = $this->ownerType(); - if ($ownerType) { - $ownerIds = DB::table(Table::ELEMENTS_OWNERS) - ->where('elementId', $this->id) - ->pluck('ownerId') - ->all(); - - if (!empty($ownerIds)) { - $query = $ownerType::find()->id($ownerIds); - Typecast::configure($query, $criteria + $this->ownerCriteria()); - $this->_owners = $query->all(); - } - } - } - - return $this->_owners; - } - - private function ownerCriteria(): array - { - return [ - 'site' => '*', - 'preferSites' => [$this->siteId], - 'unique' => true, - 'status' => null, - 'drafts' => null, - 'provisionalDrafts' => null, - 'revisions' => null, - 'trashed' => null, - ]; - } - - /** - * @inheritdoc - */ - public function setOwner(?ElementInterface $owner): void - { - $this->_owner = $owner ?? false; - $this->ownerId = $owner->id ?? null; - } - - /** - * @inheritdoc - */ - public function getField(): ?ElementContainerFieldInterface - { - if (!isset($this->fieldId)) { - return null; - } - - $field = null; - - try { - $field = $this->getOwner()?->getFieldLayout()?->getFieldById($this->fieldId); - } catch (InvalidConfigException) { - // carry on as we might still be able to get the field by ID - } - - if (!$field) { - $field = app(Fields::class)->getFieldById($this->fieldId); - } - - if (!$field instanceof ElementContainerFieldInterface) { - throw new InvalidConfigException("Invalid field ID: $this->fieldId"); - } - - return $field; - } - - /** - * @inheritdoc - */ - public function getSortOrder(): ?int - { - return $this->sortOrder; - } - - /** - * @inheritdoc - */ - public function setSortOrder(?int $sortOrder): void - { - $this->sortOrder = $sortOrder; - } - - /** - * @inheritdoc - */ - public function setSaveOwnership(bool $saveOwnership): void - { - $this->saveOwnership = $saveOwnership; - } - - /** - * @inheritdoc - */ - public function addInvalidNestedElementIds(array $ids): void - { - parent::addInvalidNestedElementIds($ids); - - if (isset($this->_owner)) { - $this->_owner->addInvalidNestedElementIds($ids); - } - } - - /** - * @inheritdoc - */ - public function setEagerLoadedElements(string $handle, array $elements, EagerLoadPlan $plan): void - { - switch ($plan->handle) { - case 'owner': - $this->setOwner(reset($elements) ?: null); - break; - case 'primaryOwner': - $this->setPrimaryOwner(reset($elements) ?: null); - break; - default: - parent::setEagerLoadedElements($handle, $elements, $plan); - } - } - - /** - * Returns the owner element’s type. - * - * @return class-string|null - * @since 5.6.0 - */ - protected function ownerType(): ?string - { - if (!isset($this->ownerType)) { - $ownerId = $this->getOwnerId(); - if (!$ownerId) { - return null; - } - $ownerType = Elements::getElementTypeById($ownerId); - if (!$ownerType) { - return null; - } - $this->ownerType = $ownerType; - } - return $this->ownerType; - } - - /** - * Saves the element’s ownership data, if it belongs to a field + owner element - */ - private function saveOwnership(bool $isNew, string $elementTable, string $fieldIdColumn = 'fieldId'): void - { - if (!$this->saveOwnership || !isset($this->fieldId) || $this->resaving) { - return; - } - - $ownerId = $this->getOwnerId(); - if (!$ownerId) { - return; - } - - if (!isset($this->sortOrder) && (!$isNew || $this->duplicateOf)) { - // figure out if we should proceed this way - // if we're dealing with an element that's being duplicated, and it has a draftId - // it means we're creating a draft of something - // if we're duplicating element via duplicate action - draftId would be empty - $elementId = null; - - if ($this->duplicateOf) { - if ($this->draftId) { - $elementId = $this->duplicateOf->id; - } - } else { - // if we're not duplicating, use this element's id - $elementId = $this->id; - } - - if ($elementId) { - $this->sortOrder = DB::table(Table::ELEMENTS_OWNERS) - ->where('elementId', $elementId) - ->where('ownerId', $ownerId) - ->value('sortOrder') ?: null; - } - } - - if (!isset($this->sortOrder)) { - $max = DB::table(Table::ELEMENTS_OWNERS, 'eo') - ->join(new Alias($elementTable, 'e'), 'e.id', '=', 'eo.elementId') - ->where('eo.ownerId', $ownerId) - ->where("e.$fieldIdColumn", $this->fieldId) - ->max('eo.sortOrder'); - - $this->sortOrder = $max ? $max + 1 : 1; - } - - $ownerIds = array_unique([ - $this->getPrimaryOwnerId(), - $ownerId, - ]); - - if (!$isNew) { - DB::table(Table::ELEMENTS_OWNERS) - ->where('elementId', $this->id) - ->whereIn('ownerId', $ownerIds) - ->delete(); - } - - foreach ($ownerIds as $ownerId) { - DB::table(Table::ELEMENTS_OWNERS)->insert([ - 'elementId' => $this->id, - 'ownerId' => $ownerId, - 'sortOrder' => $this->sortOrder, - ]); - } - } + use NestedElement; } diff --git a/yii2-adapter/legacy/behaviors/BaseRevisionBehavior.php b/yii2-adapter/legacy/behaviors/BaseRevisionBehavior.php index 552f8270211..4dc6d7c0261 100644 --- a/yii2-adapter/legacy/behaviors/BaseRevisionBehavior.php +++ b/yii2-adapter/legacy/behaviors/BaseRevisionBehavior.php @@ -8,7 +8,7 @@ namespace craft\behaviors; use CraftCms\Cms\Element\Element; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\User\Elements\User; use CraftCms\Cms\Support\Facades\Deprecator; use yii\base\Behavior; @@ -16,7 +16,7 @@ /** * BaseRevisionBehavior is the base implementation of draft & revision behaviors. * - * @template T of Element + * @template T of \CraftCms\Cms\Element\Contracts\ElementInterface&\craft\base\Component * @extends Behavior * @property User|null $creator * @property-read int $sourceId diff --git a/yii2-adapter/legacy/behaviors/CustomFieldBehavior.php.template b/yii2-adapter/legacy/behaviors/CustomFieldBehavior.php.template index f0fdb3d30aa..54ccf3dad2e 100644 --- a/yii2-adapter/legacy/behaviors/CustomFieldBehavior.php.template +++ b/yii2-adapter/legacy/behaviors/CustomFieldBehavior.php.template @@ -7,7 +7,7 @@ namespace craft\behaviors; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use craft\elements\db\ElementQueryInterface; use yii\base\Behavior; diff --git a/yii2-adapter/legacy/behaviors/FieldLayoutBehavior.php b/yii2-adapter/legacy/behaviors/FieldLayoutBehavior.php index a6e7ab97bf6..481a44b000e 100644 --- a/yii2-adapter/legacy/behaviors/FieldLayoutBehavior.php +++ b/yii2-adapter/legacy/behaviors/FieldLayoutBehavior.php @@ -7,8 +7,8 @@ namespace craft\behaviors; -use craft\base\ElementInterface; use craft\models\EntryType; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; use CraftCms\Cms\Field\Fields; use CraftCms\Cms\FieldLayout\Contracts\FieldLayoutProviderInterface; diff --git a/yii2-adapter/legacy/behaviors/SessionBehavior.php b/yii2-adapter/legacy/behaviors/SessionBehavior.php index 8052b399db3..1afe9d003b7 100644 --- a/yii2-adapter/legacy/behaviors/SessionBehavior.php +++ b/yii2-adapter/legacy/behaviors/SessionBehavior.php @@ -7,11 +7,10 @@ namespace craft\behaviors; -use Craft; use craft\web\Session; use craft\web\View; use CraftCms\Cms\Auth\SessionAuth; -use CraftCms\Cms\Support\Json; +use CraftCms\Cms\View\Enums\Position; use yii\base\Behavior; use yii\base\Exception; use yii\web\AssetBundle; @@ -51,13 +50,13 @@ class SessionBehavior extends Behavior */ public function setNotice(string $message, array $settings = []): void { - if (Craft::$app->getRequest()->getIsCpRequest()) { + if (request()->isCpRequest()) { $this->_setNotificationFlash('notice', $message, $settings + [ 'icon' => 'info', 'iconLabel' => t('Notice'), ]); } else { - $this->owner->setFlash('notice', $message); + session()->flash('notice', $message); } } @@ -74,13 +73,13 @@ public function setNotice(string $message, array $settings = []): void */ public function setSuccess(string $message, array $settings = []): void { - if (Craft::$app->getRequest()->getIsCpRequest()) { + if (request()->isCpRequest()) { $this->_setNotificationFlash('success', $message, $settings + [ 'icon' => 'check', 'iconLabel' => t('Success'), ]); } else { - $this->owner->setFlash('success', $message); + session()->flash('success', $message); } } @@ -96,13 +95,13 @@ public function setSuccess(string $message, array $settings = []): void */ public function setError(string $message, array $settings = []): void { - if (Craft::$app->getRequest()->getIsCpRequest()) { + if (request()->isCpRequest()) { $this->_setNotificationFlash('error', $message, $settings + [ 'icon' => 'alert', 'iconLabel' => t('Error'), ]); } else { - $this->owner->setFlash('error', $message); + session()->flash('error', $message); } } @@ -113,11 +112,11 @@ public function setError(string $message, array $settings = []): void */ public function getNotice(): ?string { - if (Craft::$app->getRequest()->getIsCpRequest()) { + if (request()->isCpRequest()) { return $this->_getNotificationFlashMessage('notice'); } - return $this->owner->getFlash('notice'); + return session()->get('notice'); } /** @@ -128,11 +127,11 @@ public function getNotice(): ?string */ public function getSuccess(): ?string { - if (Craft::$app->getRequest()->getIsCpRequest()) { + if (request()->isCpRequest()) { return $this->_getNotificationFlashMessage('success'); } - return $this->owner->getFlash('success'); + return session()->get('success'); } /** @@ -142,21 +141,21 @@ public function getSuccess(): ?string */ public function getError(): ?string { - if (Craft::$app->getRequest()->getIsCpRequest()) { + if (request()->isCpRequest()) { return $this->_getNotificationFlashMessage('error'); } - return $this->owner->getFlash('error'); + return session()->get('error'); } private function _getNotificationFlashMessage(string $type) { - return $this->owner->getFlash("cp-notification-$type")[0] ?? null; + return session()->get("cp-notification-$type")[0] ?? null; } private function _setNotificationFlash(string $type, string $message, array $settings = []) { - $this->owner->setFlash("cp-notification-$type", [$message, $settings]); + session()->flash("cp-notification-$type", [$message, $settings]); } /** @@ -178,7 +177,7 @@ public function addAssetBundleFlash(string $name, ?int $position = null): void $assetBundles = $this->getAssetBundleFlashes(false); $assetBundles[$name] = $position; - $this->owner->setFlash($this->assetBundleFlashKey, $assetBundles); + session()->flash($this->assetBundleFlashKey, $assetBundles); } /** @@ -190,7 +189,11 @@ public function addAssetBundleFlash(string $name, ?int $position = null): void */ public function getAssetBundleFlashes(bool $delete = false): array { - return $this->owner->getFlash($this->assetBundleFlashKey, [], $delete); + if ($delete) { + return session()->pull($this->assetBundleFlashKey, []); + } + + return session()->get($this->assetBundleFlashKey, []); } /** @@ -203,26 +206,28 @@ public function getAssetBundleFlashes(bool $delete = false): array * @param int $position the position at which the JS script tag should * be inserted in a page. * @param string|null $key the key that identifies the JS code block. + * * @see getJsFlashes() * @see View::registerJs() + * @deprecated 6.0.0 use {@see \Illuminate\Support\Facades\Session::flashJs()} instead. */ public function addJsFlash(string $js, int $position = View::POS_READY, ?string $key = null): void { - $scripts = $this->getJsFlashes(); - $scripts[] = [$js, $position, $key]; - $this->owner->setFlash($this->jsFlashKey, $scripts); + session()->flashJs($js, Position::tryFrom($position) ?? Position::Head, $key); } /** * Returns the stored JS flashes. * * @param bool $delete Whether to delete the stored flashes. Defaults to `true`. + * * @return array The stored JS flashes. * @see addJsFlash() + * @deprecated 6.0.0 use {@see \Illuminate\Support\Facades\Session::getJs()} instead. */ public function getJsFlashes(bool $delete = true): array { - return $this->owner->getFlash($this->jsFlashKey, [], $delete); + return session()->getJs($delete); } /** @@ -230,21 +235,11 @@ public function getJsFlashes(bool $delete = true): array * * @param string|array $message The message to broadcast. * @since 4.0.0 + * @deprecated 6.0.0 use {@see \Illuminate\Support\Facades\Session::broadcastToJs()} instead. */ public function broadcastToJs(string|array $message): void { - // This is a control panel-only feature - if (!Craft::$app->getRequest()->getIsCpRequest()) { - return; - } - - $jsonMessage = Json::encode($message); - $this->addJsFlash(<<broadcastToJs($message); } // Session-Based Authorization diff --git a/yii2-adapter/legacy/cache/DbCache.php b/yii2-adapter/legacy/cache/DbCache.php index 08b81b48cd9..00155033c3a 100644 --- a/yii2-adapter/legacy/cache/DbCache.php +++ b/yii2-adapter/legacy/cache/DbCache.php @@ -10,12 +10,12 @@ use craft\db\Connection; use craft\helpers\DateTimeHelper; use craft\helpers\Db; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use Exception; use Illuminate\Support\Facades\Log; use PDO; use Throwable; use yii\base\InvalidConfigException; -use yii\base\NotSupportedException; use yii\caching\DbCache as YiiDbCache; use yii\db\PdoValue; diff --git a/yii2-adapter/legacy/config/cproutes/common.php b/yii2-adapter/legacy/config/cproutes/common.php index c9e865fab19..0b67a5fe474 100644 --- a/yii2-adapter/legacy/config/cproutes/common.php +++ b/yii2-adapter/legacy/config/cproutes/common.php @@ -1,17 +1,3 @@ ' => 'elements/edit', - 'edit/' => 'elements/redirect', - 'edit/' => 'elements/redirect', - 'revisions/' => 'elements/revisions', - 'entries//' => 'elements/edit', - 'entries///revisions' => 'elements/revisions', - - 'content///' => 'elements/edit', - 'content////revisions' => 'elements/revisions', - - 'preview/' => 'elements/preview', -]; +return []; diff --git a/yii2-adapter/legacy/console/controllers/ResaveController.php b/yii2-adapter/legacy/console/controllers/ResaveController.php index 27e24a4aa4d..68509b70504 100644 --- a/yii2-adapter/legacy/console/controllers/ResaveController.php +++ b/yii2-adapter/legacy/console/controllers/ResaveController.php @@ -8,7 +8,6 @@ namespace craft\console\controllers; use Craft; -use craft\base\ElementInterface; use craft\base\Event as YiiEvent; use craft\console\Controller; use craft\elements\Category; @@ -23,6 +22,7 @@ use CraftCms\Cms\Asset\Data\Volume; use CraftCms\Cms\Asset\Volumes; use CraftCms\Cms\Element\Commands\Resave\ResaveCommand; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Events\DefineResaveCommands; diff --git a/yii2-adapter/legacy/controllers/BaseElementsController.php b/yii2-adapter/legacy/controllers/BaseElementsController.php deleted file mode 100644 index a3ef0ce6122..00000000000 --- a/yii2-adapter/legacy/controllers/BaseElementsController.php +++ /dev/null @@ -1,116 +0,0 @@ - - * @since 3.0.0 - */ -abstract class BaseElementsController extends Controller -{ - /** - * @inheritdoc - */ - public function beforeAction($action): bool - { - if (!parent::beforeAction($action)) { - return false; - } - - // All actions require control panel requests - $this->requireCpRequest(); - - return true; - } - - /** - * Returns the posted element type class. - * - * @return class-string - * @throws BadRequestHttpException if the requested element type is invalid - */ - protected function elementType(): string - { - $class = $this->request->getRequiredParam('elementType'); - - // TODO: should probably move the code inside try{} to a helper method - try { - if (!is_subclass_of($class, ElementInterface::class)) { - throw new InvalidTypeException($class, ElementInterface::class); - } - } catch (InvalidTypeException $e) { - throw new BadRequestHttpException($e->getMessage()); - } - - return $class; - } - - /** - * Returns the context that this controller is being called in. - * - * @return string - */ - protected function context(): string - { - return $this->request->getParam('context') ?? ElementSources::CONTEXT_INDEX; - } - - /** - * Returns the condition that should be applied to the element query. - * - * @return ElementConditionInterface|null - * @since 5.9.0 - */ - protected function condition(): ?ElementConditionInterface - { - /** @var array|null $conditionConfig */ - /** @phpstan-var array{class:class-string}|null $conditionConfig */ - $conditionConfig = $this->request->getBodyParam('condition'); - - if (!$conditionConfig) { - return null; - } - - /** @var ElementConditionInterface $condition */ - $condition = Conditions::createCondition($conditionConfig); - - if ($condition instanceof ElementCondition) { - $referenceElementId = $this->request->getBodyParam('referenceElementId'); - if ($referenceElementId) { - $ownerId = $this->request->getBodyParam('referenceElementOwnerId'); - $siteId = $this->request->getBodyParam('referenceElementSiteId'); - $criteria = []; - if ($ownerId) { - $criteria['ownerId'] = $ownerId; - } - $condition->referenceElement = Elements::getElementById( - (int)$referenceElementId, - siteId: $siteId, - criteria: $criteria, - ); - } - } - - /** @var ElementConditionInterface $condition */ - return $condition; - } -} diff --git a/yii2-adapter/legacy/controllers/ElementIndexesController.php b/yii2-adapter/legacy/controllers/ElementIndexesController.php deleted file mode 100644 index bcc33f3f92a..00000000000 --- a/yii2-adapter/legacy/controllers/ElementIndexesController.php +++ /dev/null @@ -1,796 +0,0 @@ - - * - * @since 3.0.0 - */ -class ElementIndexesController extends BaseElementsController -{ - /** - * @var class-string - */ - protected string $elementType; - - protected string $context; - - protected ?string $sourceKey = null; - - protected ?array $source = null; - - /** - * @var FieldLayout[]|null - * @since 5.9.18 - */ - protected ?array $fieldLayouts = null; - - /** - * @var ElementConditionInterface|null - * @since 4.0.0 - */ - protected ?ElementConditionInterface $condition = null; - - protected ?array $viewState = null; - - protected ElementQueryInterface|null $elementQuery = null; - - /** - * @since 5.0.0 - */ - protected ElementQueryInterface|null $unfilteredElementQuery = null; - - /** - * @var \CraftCms\Cms\Element\Contracts\ElementActionInterface[]|null - */ - protected ?array $actions = null; - - /** - * @var ElementExporterInterface[]|null - */ - protected ?array $exporters = null; - - /** - * {@inheritdoc} - */ - public function beforeAction($action): bool - { - if (!parent::beforeAction($action)) { - return false; - } - - $this->requireAcceptsJson(); - - $this->elementType = $this->elementType(); - $this->context = $this->context(); - $this->sourceKey = $this->request->getParam('source') ?: null; - $this->source = $this->source(); - $this->fieldLayouts = $this->fieldLayouts(); - $this->condition = $this->condition(); - - if (!in_array($action->id, ['filter-hud', 'save-elements'])) { - $this->viewState = $this->viewState(); - $this->elementQuery = $this->elementQuery(); - - if ( - in_array($action->id, ['get-elements', 'get-more-elements'], true) && - $this->isAdministrative() && - isset($this->sourceKey) - ) { - $this->actions = $this->availableActions(); - $this->exporters = $this->availableExporters(); - } - } - - return true; - } - - /** - * Returns the element query that’s defining which elements will be returned in the current request. - * - * Other components can fetch this like so: - * - * ```php - * $criteria = Craft::$app->controller->getElementQuery(); - * ``` - */ - public function getElementQuery(): ElementQueryInterface - { - return $this->elementQuery; - } - - /** - * Returns the source path for the given source key, step key, and context. - * - * @since 4.4.12 - */ - public function actionSourcePath(): Response - { - $stepKey = $this->request->getRequiredBodyParam('stepKey'); - $sourcePath = $this->elementType::sourcePath($this->sourceKey, $stepKey, $this->context); - - return $this->asJson([ - 'sourcePath' => $sourcePath, - ]); - } - - /** - * Returns attribute info for the current source. - * - * @since 5.9.0 - */ - public function actionSourceAttributeInfo(): Response - { - $elementSources = app(ElementSources::class); - - if ($this->sourceKey) { - $sortOptions = $elementSources->getSourceSortOptions($this->elementType, $this->sourceKey) - ->map(fn(array $option) => [ - 'label' => $option['label'], - 'attr' => $option['attribute'] ?? $option['orderBy'], - 'defaultDir' => $option['defaultDir'] ?? 'asc', - ]) - ->values() - ->all(); - - $tableColumns = $elementSources->getSourceTableAttributes($this->elementType, $this->sourceKey) - ->map(fn(array $attribute, string $key) => [ - ...$attribute, - 'attr' => $key, - ]) - ->values() - ->all(); - - $defaultTableColumns = Collection::make($elementSources->getTableAttributes( - elementType: $this->elementType, - sourceKey: $this->sourceKey, - fieldLayouts: $this->fieldLayouts - )) - ->map(fn(array $attribute) => $attribute[0]) - ->filter(fn(string $attribute) => $attribute !== 'title') - ->values() - ->all(); - } else { - $sortOptions = []; - $tableColumns = []; - $defaultTableColumns = []; - } - - return $this->asJson(compact( - 'sortOptions', - 'tableColumns', - 'defaultTableColumns', - )); - } - - /** - * Renders and returns an element index container, plus its first batch of elements. - */ - public function actionGetElements(): Response - { - $responseData = $this->elementResponseData(true, $this->isAdministrative()); - - return $this->asJson($responseData); - } - - /** - * Renders and returns a subsequent batch of elements for an element index. - */ - public function actionGetMoreElements(): Response - { - $responseData = $this->elementResponseData(false, false); - - return $this->asJson($responseData); - } - - /** - * Returns the total number of elements that match the current criteria. - * - * @since 3.4.6 - */ - public function actionCountElements(): Response - { - $total = $this->elementType::indexElementCount($this->elementQuery, $this->sourceKey); - - if (isset($this->unfilteredElementQuery)) { - $unfilteredTotal = $this->elementType::indexElementCount($this->unfilteredElementQuery, $this->sourceKey); - } else { - $unfilteredTotal = $total; - } - - return $this->asJson([ - 'resultSet' => $this->request->getParam('resultSet'), - 'total' => $total, - 'unfilteredTotal' => $unfilteredTotal, - ]); - } - - /** - * Returns the source tree HTML for an element index. - */ - public function actionGetSourceTreeHtml(): Response - { - $this->requireAcceptsJson(); - - $sources = app(ElementSources::class)->getSources($this->elementType, $this->context); - - return $this->asJson([ - 'html' => template('_elements/sources', [ - 'elementType' => $this->elementType, - 'sources' => $sources->all(), - ]), - ]); - } - - /** - * Creates a filter HUD’s contents. - * - * @since 4.0.0 - */ - public function actionFilterHud(): Response - { - $id = $this->request->getRequiredBodyParam('id'); - $conditionConfig = $this->request->getBodyParam('conditionConfig'); - $serialized = $this->request->getBodyParam('serialized'); - - if (!$conditionConfig && $serialized) { - parse_str($serialized, $conditionConfig); - $conditionConfig = $conditionConfig['condition']; - } - - if ($conditionConfig) { - /** @var ElementConditionInterface $condition */ - $condition = Conditions::createCondition($conditionConfig); - } else { - $condition = $this->elementType()::createCondition(); - } - - if (!empty($this->fieldLayouts)) { - $condition->setFieldLayouts($this->fieldLayouts); - } - - $condition->mainTag = 'div'; - $condition->id = $id; - $condition->addRuleLabel = t('Add a filter'); - - // Filter out any condition rules that touch the same query params as the source criteria - if ($this->source['type'] === ElementSources::TYPE_NATIVE) { - $condition->queryParams = array_keys($this->source['criteria'] ?? []); - $condition->sourceKey = $this->sourceKey; - } else { - /** @var ElementConditionInterface $sourceCondition */ - $sourceCondition = Conditions::createCondition($this->source['condition']); - $condition->queryParams = []; - foreach ($sourceCondition->getConditionRules() as $rule) { - /** @var ElementConditionRuleInterface $rule */ - $params = $rule->getExclusiveQueryParams(); - foreach ($params as $param) { - $condition->queryParams[] = $param; - } - } - } - - if ($this->condition) { - foreach ($this->condition->getConditionRules() as $rule) { - /** @var ElementConditionRuleInterface $rule */ - $params = $rule->getExclusiveQueryParams(); - foreach ($params as $param) { - $condition->queryParams[] = $param; - } - } - } - - $condition->queryParams[] = 'site'; - $condition->queryParams[] = 'status'; - - $html = $condition->getBuilderHtml(); - - return $this->asJson([ - 'hudHtml' => $html, - 'headHtml' => HtmlStack::headHtml(), - 'bodyHtml' => HtmlStack::bodyHtml(), - ]); - } - - /** - * Saves inline-edited elements. - * - * @since 5.0.0 - */ - public function actionSaveElements(): Response - { - $siteId = $this->request->getRequiredBodyParam('siteId'); - $namespace = $this->request->getRequiredBodyParam('namespace'); - $data = $this->request->getRequiredBodyParam($namespace); - - if (empty($data)) { - throw new BadRequestHttpException('No element data provided.'); - } - - // get all the elements - $elementIds = array_map( - fn(string $key) => (int) Str::chopStart($key, 'element-'), - array_keys($data), - ); - $elements = $this->elementType()::find() - ->id($elementIds) - ->status(null) - ->drafts(null) - ->provisionalDrafts(null) - ->siteId($siteId) - ->all(); - - if (empty($elements)) { - throw new BadRequestHttpException('No valid element IDs provided.'); - } - - // make sure they're editable - foreach ($elements as $element) { - Gate::authorize('save', $element); - } - - // set attributes and validate everything - $errors = []; - foreach ($elements as $element) { - $attributes = Arr::except($data["element-$element->id"], 'fields'); - if (!empty($attributes)) { - $scenario = $element->ruleset->getScenario(); - $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); - $element->setAttributesFromRequest($attributes); - $element->ruleset->useScenario($scenario); - } - - $element->setFieldValuesFromRequest("$namespace.element-$element->id.fields"); - - if ($element->getIsUnpublishedDraft()) { - $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS); - } elseif ($element->enabled && $element->getEnabledForSite()) { - $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); - } - - $names = array_merge( - array_keys($attributes), - array_map(fn(string $handle) => "field:$handle", array_keys($data["element-$element->id"]['fields'] ?? [])), - ); - - if (!$element->validate($names)) { - $errors[$element->getCanonicalId()] = $element->errors()->getMessages(); - } - } - - if (!empty($errors)) { - return $this->asJson([ - 'errors' => $errors, - ]); - } - - // now save everything - DB::beginTransaction(); - - try { - foreach ($elements as $element) { - if (!Elements::saveElement($element)) { - Log::error("Couldn’t save element $element->id: " . implode(', ', $element->getFirstErrors())); - throw new ServerErrorHttpException("Couldn’t save element $element->id"); - } - } - - DB::commit(); - } catch (Throwable $e) { - DB::rollBack(); - throw $e; - } - - return $this->asSuccess(); - } - - /** - * Returns whether the element index has an administrative context (`index` or `embedded-index`). - * - * @since 5.0.0 - */ - protected function isAdministrative(): bool - { - return in_array($this->context, ['index', 'embedded-index']); - } - - /** - * Returns the selected source info. - * - * @throws ForbiddenHttpException if the user is not permitted to access the requested source - */ - protected function source(): ?array - { - if (!isset($this->sourceKey)) { - return null; - } - - if ($this->sourceKey === '__IMP__') { - return [ - 'type' => ElementSources::TYPE_NATIVE, - 'key' => '__IMP__', - 'label' => t('All elements'), - 'hasThumbs' => $this->elementType::hasThumbs(), - ]; - } - - $source = app(ElementSources::class)->findSource($this->elementType, $this->sourceKey, $this->context); - - if ($source === null) { - // That wasn't a valid source, or the user doesn't have access to it in this context - $this->sourceKey = null; - } - - return $source; - } - - private function fieldLayouts(): ?array - { - $fieldLayouts = $this->request->getBodyParam('fieldLayouts'); - - if (empty($fieldLayouts)) { - return null; - } - - return array_map( - fn(array $config) => FieldLayout::createFromConfig($config), - Component::cleanseConfig($fieldLayouts), - ); - } - - /** - * Returns the current view state. - */ - protected function viewState(): array - { - $viewState = $this->request->getParam('viewState', []); - - if (empty($viewState['mode'])) { - $viewState['mode'] = 'table'; - } - - return $viewState; - } - - /** - * Returns the element query based on the current params. - */ - protected function elementQuery(): ElementQueryInterface - { - $query = $this->elementType::find(); - - if (!$this->source) { - $query->id(false); - - return $query; - } - - // Does the source specify any criteria attributes? - if ($this->source['type'] === ElementSources::TYPE_CUSTOM) { - /** @var ElementConditionInterface $sourceCondition */ - $sourceCondition = Conditions::createCondition($this->source['condition']); - $sourceCondition->modifyQuery($query); - } - - $applyCriteria = function(array $criteria) use ($query): bool { - if (!$criteria) { - return false; - } - - if (isset($criteria['trashed'])) { - $criteria['trashed'] = filter_var($criteria['trashed'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false; - } - if (isset($criteria['drafts'])) { - $criteria['drafts'] = filter_var($criteria['drafts'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false; - } - if (isset($criteria['draftOf'])) { - if (is_numeric($criteria['draftOf']) && $criteria['draftOf'] != 0) { - $criteria['draftOf'] = (int) $criteria['draftOf']; - } else { - $criteria['draftOf'] = filter_var($criteria['draftOf'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); - } - } - - // Remove unsupported criteria attributes - $criteria = ElementHelper::cleanseQueryCriteria($criteria); - - Typecast::configure($query, $criteria); - - return true; - }; - - $applyCriteria($this->request->getBodyParam('baseCriteria') ?? []); - - // Now we move onto things the user could have modified... - $unfilteredQuery = (clone $query); - $hasFilters = false; - - // Was a condition provided? - if (isset($this->condition)) { - $this->condition->modifyQuery($query); - $hasFilters = true; - } - - if ($applyCriteria($this->request->getBodyParam('criteria') ?? [])) { - $hasFilters = true; - } - - // Override with the custom filters - $filterConditionConfig = $this->request->getBodyParam('filterConfig'); - if (!$filterConditionConfig) { - $filterConditionStr = $this->request->getBodyParam('filters'); - if ($filterConditionStr) { - parse_str($filterConditionStr, $filterConditionConfig); - $filterConditionConfig = $filterConditionConfig['condition']; - } - } - if ($filterConditionConfig) { - /** @var ElementConditionInterface $filterCondition */ - $filterCondition = Conditions::createCondition($filterConditionConfig); - $filterCondition->modifyQuery($query); - $hasFilters = true; - } - - // Exclude descendants of the collapsed element IDs - $collapsedElementIds = $this->request->getParam('collapsedElementIds'); - - if ($collapsedElementIds) { - /** @var ElementQuery $query */ - $descendantQuery = (clone $query) - ->offset(null) - ->limit(null) - ->reorder() - ->positionedAfter(null) - ->positionedBefore(null) - ->status(null); - - // Get the actual elements - $collapsedElements = (clone $descendantQuery) - ->id($collapsedElementIds) - ->orderBy('lft') - ->all(); - - if (!empty($collapsedElements)) { - $descendantIds = []; - - foreach ($collapsedElements as $element) { - // Make sure we haven't already excluded this one, because its ancestor is collapsed as well - if (in_array($element->id, $descendantIds, false)) { - continue; - } - - $elementDescendantIds = (clone $descendantQuery) - ->descendantOf($element) - ->ids(); - - $descendantIds = array_merge($descendantIds, $elementDescendantIds); - } - - if (!empty($descendantIds)) { - $query->where(new ExcludeDescendantIdsExpression($descendantIds)); - - $hasFilters = true; - } - } - } - - // Only set unfilteredElementQuery if there were any filters, - // so we know there weren't any filters in play if it's null - if ($hasFilters) { - $this->unfilteredElementQuery = $unfilteredQuery; - } - - return $query; - } - - /** - * Returns the element data to be returned to the client. - * - * @param bool $includeContainer Whether the element container should be included in the response data - * @param bool $includeActions Whether info about the available actions should be included in the response data - */ - protected function elementResponseData(bool $includeContainer, bool $includeActions): array - { - $responseData = []; - $view = $this->getView(); - - // Get the action head/foot HTML before any more is added to it from the element HTML - if ($includeActions) { - $responseData['actions'] = $this->viewState['static'] === true ? [] : $this->actionData(); - $responseData['actionsHeadHtml'] = $view->getHeadHtml(); - $responseData['actionsBodyHtml'] = $view->getBodyHtml(); - $responseData['exporters'] = $this->exporterData(); - } - - $disabledElementIds = $this->request->getParam('disabledElementIds', []); - $selectable = ( - (!empty($this->actions) || $this->request->getParam('selectable')) && - empty($this->viewState['inlineEditing']) - ); - $sortable = $this->isAdministrative() && $this->request->getParam('sortable'); - - if ($this->sourceKey) { - $responseData['html'] = $this->elementType::indexHtml( - $this->elementQuery, - $disabledElementIds, - [ - ...$this->viewState, - 'fieldLayouts' => $this->fieldLayouts, - ], - $this->sourceKey, - $this->context, - $includeContainer, - $selectable, - $sortable, - ); - - $responseData['headHtml'] = $view->getHeadHtml(); - $responseData['bodyHtml'] = $view->getBodyHtml(); - } else { - $responseData['html'] = Html::tag('div', t('Nothing yet.'), [ - 'class' => ['zilch', 'small'], - ]); - } - - return $responseData; - } - - /** - * Returns the available actions for the current source. - * - * @return \CraftCms\Cms\Element\Contracts\ElementActionInterface[]|null - */ - protected function availableActions(): ?array - { - return ElementActions::availableActions( - elementType: $this->elementType, - sourceKey: $this->sourceKey, - elementQuery: $this->elementQuery, - ); - } - - /** - * Returns the available exporters for the current source. - * - * @return ElementExporterInterface[]|null - * - * @since 3.4.0 - */ - protected function availableExporters(): ?array - { - if ($this->request->isMobileBrowser()) { - return null; - } - - return ElementExporters::availableExporters($this->elementType, $this->sourceKey); - } - - /** - * Returns the data for the available actions. - */ - protected function actionData(): ?array - { - if (empty($this->actions)) { - return null; - } - - return ElementActions::serializeActions($this->actions); - } - - /** - * Returns the data for the available exporters. - * - * @since 3.4.0 - */ - protected function exporterData(): ?array - { - if (empty($this->exporters)) { - return null; - } - - return ElementExporters::serializeExporters($this->exporters); - } - - /** - * Returns the updated table attribute HTML for an element. - * - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - */ - public function actionElementTableHtml(): Response - { - $this->requireAcceptsJson(); - - if (!$this->sourceKey) { - throw new BadRequestHttpException('Request missing required body param'); - } - - $id = $this->request->getRequiredBodyParam('id'); - if (!$id || !is_numeric($id)) { - throw new BadRequestHttpException("Invalid element ID: $id"); - } - - // check for a provisional draft first - /** @var ElementInterface|null $element */ - $element = (clone $this->elementQuery) - ->draftOf($id) - ->draftCreator(static::currentUser()) - ->provisionalDrafts() - ->status(null) - ->one(); - - if (!$element) { - /** @var ElementInterface|null $element */ - $element = (clone $this->elementQuery) - ->id($id) - ->status(null) - ->one(); - } - - if (!$element) { - throw new BadRequestHttpException("Invalid element ID: $id"); - } - - $attributes = Craft::$app->getElementSources()->getTableAttributes( - elementType: $this->elementType, - sourceKey: $this->sourceKey, - customAttributes: $this->viewState['tableColumns'] ?? null, - fieldLayouts: $this->fieldLayouts, - ); - $attributeHtml = []; - - foreach ($attributes as [$attribute]) { - $attributeHtml[$attribute] = $element->getAttributeHtml($attribute); - } - - return $this->asJson([ - 'attributeHtml' => $attributeHtml, - ]); - } -} diff --git a/yii2-adapter/legacy/controllers/ElementSearchController.php b/yii2-adapter/legacy/controllers/ElementSearchController.php deleted file mode 100644 index e555e18927b..00000000000 --- a/yii2-adapter/legacy/controllers/ElementSearchController.php +++ /dev/null @@ -1,141 +0,0 @@ - - * @since 5.8.0 - */ -class ElementSearchController extends Controller -{ - /** - * Searches for elements. - * - * @return Response - */ - public function actionSearch(): Response - { - $this->requireCpRequest(); - $this->requirePostRequest(); - $this->requireAcceptsJson(); - - /** @var class-string $elementType */ - $elementType = $this->request->getBodyParam('elementType'); - $siteId = $this->request->getBodyParam('siteId'); - $criteria = $this->request->getBodyParam('criteria'); - /** @var array{class:class-string}|null $conditionConfig */ - $conditionConfig = $this->request->getBodyParam('condition'); - $excludeIds = $this->request->getBodyParam('excludeIds') ?? []; - $search = trim($this->request->getBodyParam('search')); - - if (!ComponentHelper::validateComponentClass($elementType, ElementInterface::class)) { - $message = (new InvalidTypeException($elementType, ElementInterface::class))->getMessage(); - throw new BadRequestHttpException($message); - } - - $query = $elementType::find() - ->siteId($siteId) - ->search($search) - ->orderBy(['score' => SORT_DESC]) - ->limit(5); - - if ($criteria) { - // Remove unsupported criteria attributes - $criteria = ElementHelper::cleanseQueryCriteria($criteria); - - Typecast::configure($query, $criteria); - } - - if ($conditionConfig) { - $condition = Conditions::createCondition($conditionConfig); - - if ($condition instanceof ElementCondition) { - $referenceElementId = $this->request->getBodyParam('referenceElementId'); - if ($referenceElementId) { - $ownerId = $this->request->getBodyParam('referenceElementOwnerId'); - $siteId = $this->request->getBodyParam('referenceElementSiteId'); - $criteria = []; - if ($ownerId) { - $criteria['ownerId'] = $ownerId; - } - $condition->referenceElement = Elements::getElementById( - (int)$referenceElementId, - siteId: $siteId, - criteria: $criteria, - ); - } - - $condition->modifyQuery($query); - } - } - - $elements = $query->all(); - - $return = []; - $exactMatches = []; - $excludes = []; - $exactMatch = false; - - if (!empty($elements)) { - $search = Search::normalizeKeywords($search); - - foreach ($elements as $element) { - $exclude = in_array($element->id, $excludeIds, false); - - $return[] = [ - 'id' => $element->id, - 'title' => $element->title, - 'html' => app(ElementHtml::class)->chipHtml($element, [ - 'hyperlink' => false, - 'class' => 'chromeless', - ]), - 'exclude' => $exclude, - ]; - - $title = $element->title ?? (string)$element; - $title = Search::normalizeKeywords($title); - - if ($title == $search) { - $exactMatches[] = 1; - $exactMatch = true; - } else { - $exactMatches[] = 0; - } - - $excludes[] = $exclude ? 1 : 0; - } - - // prevent the default sort order from changing beyond $excludes + $exactMatches - $range = range(1, count($return)); - - array_multisort($excludes, SORT_ASC, $exactMatches, SORT_DESC, $range, $return); - } - - return $this->asJson([ - 'elements' => $return, - 'exactMatch' => $exactMatch, - ]); - } -} diff --git a/yii2-adapter/legacy/controllers/ElementSelectorModalsController.php b/yii2-adapter/legacy/controllers/ElementSelectorModalsController.php deleted file mode 100644 index 3cea1f8bbcd..00000000000 --- a/yii2-adapter/legacy/controllers/ElementSelectorModalsController.php +++ /dev/null @@ -1,67 +0,0 @@ - - * @since 4.0.0 - */ -class ElementSelectorModalsController extends BaseElementsController -{ - /** - * Renders and returns the body of an ElementSelectorModal. - * - * @return Response - */ - public function actionBody(): Response - { - $this->requireAcceptsJson(); - - $elementType = $this->elementType(); - $hasStatuses = $elementType::hasStatuses(); - - if ($hasStatuses) { - $statuses = $elementType::statuses(); - $condition = $this->condition(); - - if ($condition) { - /** @var StatusConditionRule|null $statusRule */ - $statusRule = Collection::make($condition->getConditionRules()) - ->firstWhere(fn($rule) => $rule instanceof StatusConditionRule); - - if ($statusRule) { - $statusValues = $statusRule->getValues(); - $statuses = Collection::make($statuses) - ->filter(function($info, string $status) use ($statusRule, $statusValues) { - $inValues = in_array($status, $statusValues); - return $statusRule->operator === 'in' ? $inValues : !$inValues; - }); - } - } - } - - return $this->asJson([ - 'html' => app(ElementIndexHtml::class)->html($elementType, [ - 'class' => 'content', - 'context' => $this->context(), - 'registerJs' => false, - 'showSiteMenu' => $this->request->getParam('showSiteMenu', 'auto'), - 'showStatusMenu' => $hasStatuses, - 'sources' => $this->request->getParam('sources'), - 'statuses' => $statuses ?? null, - ]), - ]); - } -} diff --git a/yii2-adapter/legacy/controllers/ElementsController.php b/yii2-adapter/legacy/controllers/ElementsController.php index b994ac64a7e..58041d8d030 100644 --- a/yii2-adapter/legacy/controllers/ElementsController.php +++ b/yii2-adapter/legacy/controllers/ElementsController.php @@ -7,78 +7,20 @@ namespace craft\controllers; -use Craft; -use craft\base\ElementInterface; -use craft\base\NestedElementInterface; +use craft\base\Event as YiiEvent; use craft\events\DefineElementEditorHtmlEvent; -use craft\services\Drafts; use craft\web\Controller; -use craft\web\CpScreenResponseBehavior; -use craft\web\UrlManager; -use CraftCms\Cms\Auth\SessionAuth; -use CraftCms\Cms\Cms; -use CraftCms\Cms\Component\ComponentHelper; -use CraftCms\Cms\Cp\Html\ContentHtml; -use CraftCms\Cms\Cp\Html\ElementHtml; -use CraftCms\Cms\Cp\RequestedSite; -use CraftCms\Cms\Database\Table; -use CraftCms\Cms\Element\Data\ElementActivity; use CraftCms\Cms\Element\Element; -use CraftCms\Cms\Element\ElementHelper; -use CraftCms\Cms\Element\Enums\ElementActivityType; -use CraftCms\Cms\Element\Enums\MenuItemType; -use CraftCms\Cms\Element\Events\DraftCreated; -use CraftCms\Cms\Element\Exceptions\InvalidElementException; -use CraftCms\Cms\Element\Exceptions\InvalidTypeException; -use CraftCms\Cms\Element\Exceptions\UnsupportedSiteException; -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; -use CraftCms\Cms\Http\Responses\CpScreenResponse; -use CraftCms\Cms\Support\Arr; -use CraftCms\Cms\Support\Facades\BulkOps; -use CraftCms\Cms\Support\Facades\DeltaRegistry; -use CraftCms\Cms\Support\Facades\ElementActivity as ElementActivityFacade; +use CraftCms\Cms\Element\Events\DefineElementEditorContent; use CraftCms\Cms\Support\Facades\Elements; -use CraftCms\Cms\Support\Facades\HtmlStack; -use CraftCms\Cms\Support\Facades\I18N; -use CraftCms\Cms\Support\Facades\InputNamespace; -use CraftCms\Cms\Support\Facades\Sites; -use CraftCms\Cms\Support\Html; -use CraftCms\Cms\Support\Json; -use CraftCms\Cms\Support\Query; -use CraftCms\Cms\Support\Str; -use CraftCms\Cms\Support\Template; -use CraftCms\Cms\Support\Url; -use CraftCms\Cms\Translation\Locale; -use CraftCms\Cms\User\Elements\User; -use CraftCms\Cms\View\Enums\Position; -use CraftCms\Cms\View\TemplateMode; -use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Crypt; -use Illuminate\Support\Facades\DB as DbFacade; use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Gate; -use Illuminate\Support\Facades\Log; -use Throwable; -use yii\helpers\Markdown; -use yii\web\BadRequestHttpException; -use yii\web\ForbiddenHttpException; -use yii\web\Response; -use yii\web\ServerErrorHttpException; -use function CraftCms\Cms\t; /** * Elements controller. * * @author Pixel & Tonic, Inc. * @since 3.0.0 + * @deprecated 6.0.0 */ class ElementsController extends Controller { @@ -86,2869 +28,24 @@ class ElementsController extends Controller * @event DefineElementEditorHtmlEvent The event that is triggered when rendering an element editor’s content. * @see _editorContent() */ - public const EVENT_DEFINE_EDITOR_CONTENT = 'defineEditorContent'; + public const string EVENT_DEFINE_EDITOR_CONTENT = 'defineEditorContent'; - /** - * @var ElementInterface|null The element currently being managed. - * @since 4.3.0 - */ - public ?ElementInterface $element = null; - - private array $_attributes; - private ?string $_elementType = null; - private ?int $_elementId = null; - private ?string $_elementUid = null; - private ?int $_draftId = null; - private ?int $_revisionId = null; - private ?int $_fieldId = null; - private ?int $_ownerId = null; - private ?int $_newOwnerId = null; - private ?int $_siteId = null; - - private ?bool $_enabled = null; - /** - * @var bool|bool[]|null - */ - private array|bool|null $_enabledForSite = null; - private ?string $_slug = null; - private bool $_fresh; - private ?string $_draftName = null; - private ?string $_notes = null; - private string $_fieldsLocation; - private bool $_provisional; - private bool $_dropProvisional; - private bool $_addAnother; - private array $_visibleLayoutElements; - private array $_staticLayoutElements; - private ?string $_selectedTab = null; - private bool $_applyParams; - private bool $_prevalidate; - private bool $_asUnpublishedDraft; - private bool $_deleteProvisionalDraft; - private ?bool $_updateSearchIndexImmediately = null; - - /** - * @inheritdoc - */ - public function beforeAction($action): bool - { - if (!parent::beforeAction($action)) { - return false; - } - - $this->_attributes = $this->request->getBodyParams(); - - // No funny business - if (isset($this->_attributes['id']) || isset($this->_attributes['canonicalId'])) { - throw new BadRequestHttpException('Changing an element’s ID is not allowed.'); - } - - $this->_elementType = $this->_param('elementType'); - $this->_elementId = $this->_param('elementId'); - $this->_elementUid = $this->_param('elementUid'); - $this->_draftId = $this->_param('draftId'); - $this->_revisionId = $this->_param('revisionId'); - $this->_fieldId = $this->_param('fieldId') ?: null; - $this->_ownerId = $this->_param('ownerId') ?: null; - $this->_newOwnerId = $this->_param('newOwnerId') ?: null; - $this->_siteId = $this->_param('siteId'); - $this->_enabled = $this->_param('enabled', $this->_param('setEnabled', true) ? true : null); - $this->_enabledForSite = $this->_param('enabledForSite'); - $this->_slug = $this->_param('slug'); - $this->_fresh = (bool)$this->_param('fresh'); - $this->_draftName = $this->_param('draftName'); - $this->_notes = $this->_param('notes'); - $this->_fieldsLocation = $this->_param('fieldsLocation') ?? 'fields'; - $this->_provisional = (bool)$this->_param('provisional'); - $this->_dropProvisional = (bool)$this->_param('dropProvisional'); - $this->_addAnother = (bool)$this->_param('addAnother'); - $this->_visibleLayoutElements = $this->_param('visibleLayoutElements') ?? []; - $this->_staticLayoutElements = $this->_param('staticLayoutElements') ?? []; - $this->_selectedTab = $this->_param('selectedTab'); - $this->_applyParams = $this->_param('applyParams', true) || !$this->request->getIsPost(); - $this->_prevalidate = (bool)$this->_param('prevalidate'); - $this->_asUnpublishedDraft = (bool)$this->_param('asUnpublishedDraft'); - $this->_deleteProvisionalDraft = (bool)$this->_param('deleteProvisionalDraft'); - $this->_updateSearchIndexImmediately = $this->_param('updateSearchIndexImmediately'); - - unset($this->_attributes['failMessage']); - unset($this->_attributes['redirect']); - unset($this->_attributes['successMessage']); - unset($this->_attributes[$this->_fieldsLocation]); - - return true; - } - - /** - * @param string $name - * @param mixed $default - * @return mixed - */ - private function _param(string $name, mixed $default = null): mixed - { - $value = Arr::pull($this->_attributes, $name, $this->request->getQueryParam($name)); - if ($value === null && $default !== null && $this->request->getIsPost()) { - return $default; - } - return $value; - } - - /** - * Redirects to an element’s edit page. - * - * @param int|null $elementId - * @param string|null $elementUid - * @return Response - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @throws ServerErrorHttpException - * @since 4.0.0 - */ - public function actionRedirect(?int $elementId = null, ?string $elementUid = null): Response - { - $this->_elementId = $elementId ?? $this->_elementId; - $this->_elementUid = $elementUid ?? $this->_elementUid; - $element = $this->_element(); - - if ($element instanceof Response) { - return $element; - } - - $this->element = $element; - $url = $element->getCpEditUrl(); - - if (!$url) { - throw new ServerErrorHttpException('The element doesn’t have an edit page.'); - } - - $editUrl = Url::removeParam(Url::cpUrl('edit'), 'site'); - if (str_starts_with($url, $editUrl)) { - /** @var UrlManager $urlManager */ - $urlManager = Craft::$app->getUrlManager(); - return $this->runAction('edit', array_merge($urlManager->getRouteParams(), [ - 'elementId' => $element->id, - ])); - } - - return $this->redirect($url); - } - - /** - * Creates a new element and redirects to its edit page. - * - * @return Response - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @throws ServerErrorHttpException - * @since 4.0.0 - */ - public function actionCreate(): Response - { - $element = $this->_createElement(); - $user = static::currentUser(); - - // Save it - $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(), - ]))); - } - - // Redirect to its edit page - $editUrl = $element->getCpEditUrl() ?? Url::actionUrl('elements/edit', [ - 'draftId' => $element->draftId, - 'siteId' => $element->siteId, - ]); - - $response = $this->_asSuccess(t('{type} created.', [ - 'type' => t('Draft'), - ]), $element, array_filter([ - 'cpEditUrl' => $this->request->isCpRequest ? $editUrl : null, - ])); - - if (!$this->request->getAcceptsJson()) { - $response->redirect(Url::urlWithParams($editUrl, ['fresh' => '1'])); - } - - return $response; - } - - /** - * Returns an element edit screen. - * - * @param ElementInterface|null $element - * @param int|null $elementId - * @return Response - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @since 4.0.0 - */ - public function actionEdit(?ElementInterface $element, ?int $elementId = null): Response - { - $this->requireCpRequest(); - - $strictSite = $this->request->getAcceptsJson(); - - if ($element === null) { - $this->_elementId = $elementId ?? $this->_elementId; - /** - * @var Element|Response|null $element - */ - $element = $this->_element(checkForProvisionalDraft: true, strictSite: $strictSite); - - if ($element instanceof Response) { - return $element; - } - - if (!$element) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - // If this is an outdated draft, merge in the latest canonical changes - $mergeCanonicalChanges = ( - $element::trackChanges() && - $element->getIsDraft() && - !$element->getIsUnpublishedDraft() && - ElementHelper::isOutdated($element) - ); - if ($mergeCanonicalChanges) { - Elements::mergeCanonicalChanges($element); - } - - $this->_applyParamsToElement($element); - - // Prevalidate? - if ($this->_prevalidate && $element->enabled && $element->getEnabledForSite()) { - $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); - $element->validate(); - } - } else { - $mergeCanonicalChanges = false; - } - - $this->element = $element; - - $user = static::currentUser(); - - // Figure out what we're dealing with here - $isCanonical = $element->getIsCanonical(); - $isDraft = $element->getIsDraft(); - $isUnpublishedDraft = $element->getIsUnpublishedDraft(); - $isRevision = $element->getIsRevision(); - $isCurrent = $isCanonical || $element->isProvisionalDraft; - $canonical = $element->getCanonical(true); - - // Site info - $supportedSites = ElementHelper::supportedSitesForElement($element, true); - $allEditableSiteIds = Sites::getEditableSiteIds()->all(); - $propSites = array_values(array_filter($supportedSites, fn($site) => $site['propagate'])); - $propSiteIds = array_column($propSites, 'siteId'); - $propEditableSiteIds = array_intersect($propSiteIds, $allEditableSiteIds); - $addlEditableSites = array_values(array_filter($supportedSites, fn($site) => !$site['propagate'] && in_array($site['siteId'], $allEditableSiteIds))); - $canEditMultipleSites = count($propEditableSiteIds) > 1 || $addlEditableSites; - - // Permissions - $canSave = $this->_canSave($element, $user); - $canSaveCanonical = Gate::check('saveCanonical', $element); - $canCreateDrafts = Gate::check('createDrafts', $canonical); - $canDuplicate = !$isRevision && Gate::check('duplicateAsDraft', $element); - - // Preview targets - $previewTargets = $element->id ? $element->getPreviewTargets() : []; - $enablePreview = ( - !empty($previewTargets) && - !$this->request->isMobileBrowser(true) && - ( - ($isDraft && $canSave) || - ($isCurrent && $canCreateDrafts) - ) - ); - - if ($previewTargets) { - if ($isDraft && !$element->isProvisionalDraft) { - SessionAuth::authorize("previewDraft:$element->draftId"); - } elseif ($isRevision) { - SessionAuth::authorize("previewRevision:$element->revisionId"); - } else { - SessionAuth::authorize("previewElement:$canonical->id"); - } - } - - // Screen prep - [$docTitle, $title] = $this->_editElementTitles($element); - $enabledForSite = $element->getEnabledForSite(); - $hasRoute = $element->getRoute() !== null; - $redirectUrl = $this->request->getValidatedQueryParam('returnUrl') ?? Url::cpReferralUrl() ?? ElementHelper::postEditUrl($element); - - // Site statuses - if ($canEditMultipleSites) { - $siteStatuses = ElementHelper::siteStatusesForElement($element, true); - } else { - $siteStatuses = [ - $element->siteId => $element->enabled, - ]; - } - - $previewToken = $previewTargets ? Str::random(extendedChars: true) : null; - - $notice = null; - if ($element->isProvisionalDraft) { - $notice = fn() => $this->_draftNotice(); - } elseif ($isRevision) { - $notice = fn() => $this->_revisionNotice($element::lowerDisplayName()); - } - - if ($element->enabled && $element->id) { - $enabledSiteIds = array_flip(Elements::getEnabledSiteIdsForElement($element->id)); - } else { - $enabledSiteIds = []; - } - - $response = $this->asCpScreen() - ->editUrl($element->getCpEditUrl()) - ->docTitle($docTitle) - ->title($title) - ->site($element::isLocalized() ? $element->getSite() : null) - ->selectableSites(array_map(fn(int $siteId) => [ - 'site' => Sites::getSiteById($siteId), - 'status' => isset($enabledSiteIds[$siteId]) ? 'enabled' : 'disabled', - ], $propEditableSiteIds)) - ->crumbs($this->_crumbs($element)) - ->contextMenuItems(fn() => $this->_contextMenuItems( - $element, - $isUnpublishedDraft, - $canCreateDrafts, - )) - ->toolbarHtml( - // if we're in a slideout, we don't want to add the .flex-grow to the header toolbar - // as it'll mess with the width available for the tabs - // see https://github.com/craftcms/cms/issues/17260 - ($this->_isSlideout() ? '' : Html::tag('div', attributes: ['class' => 'flex-grow'])) . - Html::tag('div', attributes: ['class' => 'activity-container']), - ) - ->additionalButtonsHtml(fn() => $this->_additionalButtons( - $element, - $canonical, - $isRevision, - $canSave, - $canSaveCanonical, - $canCreateDrafts, - $canDuplicate, - $previewTargets, - $enablePreview, - $isCurrent, - $isUnpublishedDraft, - $isDraft - )) - ->actionMenuItems(fn() => $this->_actionMenuItems($element, $previewTargets)) - ->noticeHtml($notice) - ->errorSummary(fn() => $this->_errorSummary($element)) - ->prepareScreen( - fn(Response|CpScreenResponse $response, string $containerId) => $this->_prepareEditor( - $element, - $isUnpublishedDraft, - $canSave, - $response, - $containerId, - fn(?FieldLayoutForm $form) => $this->_editorContent($element, $canSave, $form), - fn(?FieldLayoutForm $form) => $this->_editorSidebar($element, $mergeCanonicalChanges, $canSave), - fn(?FieldLayoutForm $form) => [ - 'additionalSites' => $addlEditableSites, - 'canCreateDrafts' => $canCreateDrafts, - 'canEditMultipleSites' => $canEditMultipleSites, - 'canSave' => $canSave, - 'canSaveCanonical' => $canSaveCanonical, - 'elementId' => $element->id, - 'canonicalId' => $canonical->id, - 'draftId' => $element->draftId, - 'draftName' => $isDraft ? $element->draftName : null, - 'elementType' => get_class($element), - 'enablePreview' => $enablePreview, - 'enabledForSite' => $element->enabled && $enabledForSite, - 'hashedCpEditUrl' => Crypt::encrypt('{cpEditUrl}'), - 'isLive' => $isCurrent && !$element->getIsDraft() && $element->enabled && $enabledForSite && $hasRoute, - 'isProvisionalDraft' => $element->isProvisionalDraft, - 'isUnpublishedDraft' => $isUnpublishedDraft, - 'previewTargets' => $previewTargets, - 'previewToken' => $previewToken, - 'hashedPreviewToken' => $previewToken ? Crypt::encrypt($previewToken) : null, - 'previewParamValue' => $previewTargets ? Crypt::encrypt(Str::random(10)) : null, - 'revisionId' => $element->revisionId, - 'fieldId' => $element instanceof NestedElementInterface ? $element->getField()?->id : null, - 'ownerId' => $element instanceof NestedElementInterface ? $element->getOwnerId() : null, - 'siteId' => $element->siteId, - 'siteStatuses' => $siteStatuses, - 'siteToken' => (!app()->isLive() || !$element->getSite()->getEnabled()) ? Crypt::encrypt((string)$element->siteId) : null, - 'visibleLayoutElements' => $form?->getVisibleElements() ?? [], - 'staticLayoutElements' => $form?->getStaticElements() ?? [], - 'updatedTimestamp' => $element->dateUpdated?->getTimestamp(), - 'canonicalUpdatedTimestamp' => $canonical->dateUpdated?->getTimestamp(), - 'isStatic' => $isRevision || !$canSave, - ] - ) - ); - - if ($canSave) { - if ($isUnpublishedDraft) { - if ($canSaveCanonical) { - $response - ->submitButtonLabel(mb_ucfirst(t('Create {type}', [ - 'type' => $element::lowerDisplayName(), - ]))) - ->action('elements/apply-draft') - ->redirectUrl("$redirectUrl#"); - } else { - $response - ->action('elements/save-draft') - ->redirectUrl("$redirectUrl#"); - } - } elseif ($element->isProvisionalDraft) { - $response - ->action('elements/apply-draft') - ->redirectUrl("$redirectUrl#"); - } elseif ($isDraft) { - $response - ->submitButtonLabel(mb_ucfirst(t('Save {type}', [ - 'type' => t('draft'), - ]))) - ->action('elements/save-draft') - ->redirectUrl("{cpEditUrl}"); - } else { - $response - ->action('elements/save') - ->redirectUrl("$redirectUrl#"); - } - - $response - ->saveShortcutRedirectUrl('{cpEditUrl}') - ->altActions($element->getAltActions()); - } - - return $response; - } - - /** - * Displays a standalone Live Preview editor for an element. - * - * @param int $elementId - * @since 5.6.0 - */ - public function actionPreview(int $elementId): Response - { - $this->requireCpRequest(); - - $this->_elementId = $elementId; - /** - * @var Element|Response|null $element - */ - $element = $this->_element(checkForProvisionalDraft: true); - - if ($element instanceof Response) { - return $element; - } - - if (!$element) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - $this->element = $element; - - // Screen prep - $redirectUrl = $this->request->getValidatedQueryParam('returnUrl') ?? ElementHelper::postEditUrl($element); - - HtmlStack::jsWithVars(fn( - $elementType, - $elementId, - $draftId, - $revisionId, - $siteId, - $redirectUrl, - ) => << { - const preview = new Craft.Preview({ - elementType: $elementType, - elementId: $elementId, - draftId: $draftId, - revisionId: $revisionId, - siteId: $siteId, - standaloneMode: true, - redirectUrl: $redirectUrl, - }) - preview.open(); -})(); -JS, [ - $element::class, - $element->isProvisionalDraft ? $element->getCanonicalId() : $element->id, - !$element->isProvisionalDraft ? $element->draftId : null, - $element->revisionId, - $element->siteId, - $redirectUrl, - ], Position::BodyEnd); - - [$docTitle, $title] = $this->_editElementTitles($element); - - return $this->rendertemplate('_layouts/base', [ - 'docTitle' => $docTitle, - 'title' => $title, - ]); - } - - /** - * Copies field/attribute values on an element from one site to another. - * - * @return Response - * @since 5.6.0 - */ - public function actionCopyValuesFromSite(): Response - { - $this->requireCpRequest(); - - /** @var Element|Response|null $element */ - $element = $this->_element(checkForProvisionalDraft: true); - - if ($element instanceof Response) { - return $element; - } - - if (!$element || $element->getIsRevision()) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - $copyFromSiteId = (int)$this->request->getRequiredBodyParam('fromSiteId'); - $site = Sites::getSiteById($copyFromSiteId); - if (!$site) { - throw new BadRequestHttpException("Invalid site ID: $copyFromSiteId"); - } - $this->requirePermission("editSite:$site->uid"); - - $layoutElementUid = $this->request->getRequiredBodyParam('layoutElementUid'); - $namespace = $this->request->getBodyParam('namespace'); - - $fromElement = $element::find() - ->id($element->id) - ->structureId($element->structureId) - ->siteId($copyFromSiteId) - ->drafts(null) - ->provisionalDrafts(null) - ->one(); - - if (!$fromElement) { - throw new UnsupportedSiteException($element, $copyFromSiteId, 'Attempting to copy element content from an unsupported site.'); - } - - $layoutElement = $element->getFieldLayout()->getElementByUid($layoutElementUid); - if (!$layoutElement instanceof BaseField || !$layoutElement->isCrossSiteCopyable($element)) { - throw new BadRequestHttpException("Invalid layout element UUID: $layoutElementUid"); - } - if ($layoutElement instanceof CustomField) { - /** @var \CraftCms\Cms\Field\Contracts\FieldInterface&\CraftCms\Cms\Field\Contracts\CrossSiteCopyableFieldInterface $field */ - $field = $layoutElement->getField(); - $field->copyCrossSiteValue($fromElement, $element); - } else { - $attribute = $layoutElement->attribute(); - $element->$attribute = $fromElement->$attribute; - } - - $view = $this->getView(); - $html = InputNamespace::namespaceInputs(fn() => $layoutElement->formHtml($element), $namespace); - - if ($html) { - $html = Html::modifyTagAttributes($html, [ - 'data' => [ - 'layout-element' => $layoutElement->uid, - ], - ]); - } - - return $this->_asSuccess(t('Field value copied.'), $element, [ - 'fieldHtml' => $html, - 'headHtml' => $view->getHeadHtml(), - 'bodyHtml' => $view->getBodyHtml(), - ]); - } - - /** - * Returns an element revisions index screen. - * - * @param int $elementId - * @return Response - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @since 4.4.0 - */ - public function actionRevisions(int $elementId): Response - { - $this->requireCpRequest(); - - $this->_elementId = $elementId; - /** - * @var Element|Response|null $element - */ - $element = $this->_element(); - - if (!$element) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - if ($element->getIsUnpublishedDraft()) { - throw new BadRequestHttpException('Unpublished drafts don\'t have revisions'); - } - - if (!$element->hasRevisions()) { - throw new BadRequestHttpException('Element doesn\'t have revisions'); - } - - return $this->asCpScreen() - ->title(t('Revisions for “{title}”', [ - 'title' => $element->getUiLabel(), - ])) - ->crumbs([ - ...$this->_crumbs($element, false), - [ - 'label' => t('Revisions'), - 'current' => true, - ], - ]) - ->contentTemplate('_elements/revisions', [ - 'element' => $element, - 'revisionsQuery' => $element::find() - ->revisionOf($element) - ->site('*') - ->preferSites([$element->siteId]) - ->unique() - ->status(null) - ->where('elements.dateCreated', '!=', Query::prepareDateForDb($element->dateUpdated)) - ->with(['revisionCreator']), - ]); - } - - /** - * Returns the page title and document title that should be used for Edit Element pages. - * - * @param ElementInterface $element - * @return string[] - * @since 3.7.0 - */ - private function _editElementTitles(ElementInterface $element): array - { - if ($element::hasTitles() && $element->title !== null && $element->title !== '') { - $title = $element->title; - } elseif (!$element->id || $element->getIsUnpublishedDraft()) { - $title = t('Create a new {type}', [ - 'type' => $element::lowerDisplayName(), - ]); - } else { - $title = $element->getUiLabel(); - } - - $docTitle = $element->getUiLabel(); - - if ($element->getIsDraft() && !$element->getIsUnpublishedDraft()) { - /** @var ElementInterface $element */ - if ($element->isProvisionalDraft) { - $docTitle .= ' — ' . t('Edited'); - } else { - $docTitle .= " ($element->draftName)"; - } - } elseif ($element->getIsRevision()) { - /** @var ElementInterface $element */ - $docTitle .= ' (' . $element->getRevisionLabel() . ')'; - } - - // Include site name if localized - if ($element::isLocalized() && Sites::isMultiSite()) { - $docTitle .= sprintf(' - %s', $element->getSite()->getUiLabel()); - } - - return [$docTitle, $title]; - } - - private function _crumbs(ElementInterface $element, bool $current = true): array - { - if ($element->isProvisionalDraft) { - $crumbs = $element->getCanonical(true)->getCrumbs(); - } else { - $crumbs = $element->getCrumbs(); - } - - return [ - ...$crumbs, - [ - 'html' => app(ElementHtml::class)->elementChipHtml($element, [ - 'showDraftName' => !$current, - 'class' => 'chromeless', - 'hyperlink' => true, - ]), - 'current' => $current, - ], - ]; - } - - private function _contextMenuItems( - ElementInterface $element, - bool $isUnpublishedDraft, - bool $canCreateDrafts, - ): array { - if ($element->isProvisionalDraft) { - $element = $element->getCanonical(true); - } - - if (!$element->id || $element->getIsUnpublishedDraft()) { - return []; - } - - if (!$isUnpublishedDraft) { - $user = Auth::user(); - - if ($element instanceof User) { - $drafts = $element::find() - ->draftOf($element) - ->siteId($element->siteId) - ->status(null) - ->orderByDesc('dateUpdated') - ->with(['draftCreator']) - ->get() - ->filter(fn(ElementInterface $draft) => $user->can('view', $draft)) - ->all(); - } else { - $drafts = $element::find() - ->draftOf($element) - ->siteId($element->siteId) - ->status(null) - ->orderBy(['dateUpdated' => SORT_DESC]) - ->with(['draftCreator']) - ->get() - ->filter(fn(ElementInterface $draft) => $user->can('view', $draft)) - ->all(); - } - } else { - $drafts = []; - } - - $generalConfig = Cms::config(); - $revisionsPageUrl = null; - $hasMoreRevisions = false; - - if ($element->hasRevisions() && $generalConfig->maxRevisions !== 1) { - $revisionsQuery = $element::find() - ->revisionOf($element) - ->siteId($element->siteId) - ->status(null) - ->offset(1) - ->limit($generalConfig->maxRevisions ? min($generalConfig->maxRevisions - 1, 10) : 10) - ->orderByDesc('dateCreated') - ->with(['revisionCreator']); - - $revisions = $revisionsQuery->all(); - $revisionsPageUrl = $element->getCpRevisionsUrl(); - - if ($revisionsPageUrl) { - $hasMoreRevisions = ($revisionsQuery->count() - 1) > 0; - } - } else { - $revisions = []; - } - - // if we're viewing a revision, make sure it's in the list - if ( - $element->getIsRevision() && - !Collection::make($revisions)->contains(fn(ElementInterface $revision) => $revision->id === $element->id) - ) { - $revisions[] = $element; - } - - if (empty($drafts) && empty($revisions) && !$canCreateDrafts) { - return []; - } - - $formatter = I18N::getFormatter(); - - $baseParams = $this->request->getQueryParams(); - unset($baseParams['draftId'], $baseParams['revisionId'], $baseParams['siteId'], $baseParams['fresh']); - if (isset($generalConfig->pathParam)) { - unset($baseParams[$generalConfig->pathParam]); - } - - $isDraft = $element->getIsDraft(); - $isRevision = $element->getIsRevision(); - $cpEditUrl = Url::cpUrl($element->getCpEditUrl(), [ - 'draftId' => null, - 'revisionId' => null, - ]); - - /** @var ElementInterface|null $revision */ - $revision = $element->getCurrentRevision(); - $creator = $revision?->getRevisionCreator(); - $timestamp = $formatter->asTimestamp($revision->dateCreated ?? $element->dateUpdated, Locale::LENGTH_SHORT, true); - - $items = [ - [ - 'heading' => t('Context'), - 'headingTag' => 'h2', - 'headingAttributes' => ['class' => ['visually-hidden']], - 'listAttributes' => ['class' => ['revision-group-current']], - 'items' => [ - [ - 'label' => t('Current'), - 'description' => $creator - ? t('Saved {timestamp} by {creator}', [ - 'timestamp' => $timestamp, - 'creator' => $creator->name, - ]) - : t('Last saved {timestamp}', [ - 'timestamp' => $timestamp, - ]), - 'url' => $cpEditUrl, - 'selected' => !$isDraft && !$isRevision, - ], - ], - ], - ]; - - if (!empty($drafts)) { - $items[] = [ - 'heading' => t('Drafts'), - 'listAttributes' => ['class' => ['revision-group-drafts']], - 'items' => array_map(function($draft) use ($element, $formatter, $cpEditUrl, $baseParams) { - /** @var ElementInterface $draft */ - $creator = $draft->getDraftCreator(); - $timestamp = $formatter->asTimestamp($draft->dateUpdated, Locale::LENGTH_SHORT, true); - $timestampWithDate = $formatter->asDatetime($draft->dateUpdated, Locale::LENGTH_SHORT); - - return [ - 'label' => $draft->draftName, - 'description' => $creator - ? Template::raw(t('Saved by {creator}', [ - 'timestampWithDate' => $timestampWithDate, - 'timestamp' => $timestamp, - 'creator' => Html::encode($creator->name), - ])) - : Template::raw(t('Last saved ', [ - 'timestampWithDate' => $timestampWithDate, - 'timestamp' => $timestamp, - ])), - 'url' => Url::urlWithParams($cpEditUrl, array_merge($baseParams, [ - 'draftId' => $draft->draftId, - ])), - 'selected' => $draft->id === $element->id, - ]; - }, $drafts), - ]; - } - - if (!empty($revisions)) { - $items[] = [ - 'heading' => t('Recent Revisions'), - 'listAttributes' => ['class' => ['revision-group-revisions']], - 'items' => array_map(function($revision) use ($element, $formatter, $cpEditUrl, $baseParams) { - /** @var ElementInterface $revision */ - $creator = $revision->getRevisionCreator(); - $timestamp = $formatter->asTimestamp($revision->dateCreated, Locale::LENGTH_SHORT, true); - $timestampWithDate = $formatter->asDatetime($revision->dateCreated, Locale::LENGTH_SHORT); - - return [ - 'label' => $revision->getRevisionLabel(), - 'description' => $creator - ? Template::raw(t('Saved by {creator}', [ - 'timestampWithDate' => $timestampWithDate, - 'timestamp' => $timestamp, - 'creator' => Html::encode($creator->name), - ])) - : Template::raw(t('Saved ', [ - 'timestampWithDate' => $timestampWithDate, - 'timestamp' => $timestamp, - ])), - 'url' => Url::urlWithParams($cpEditUrl, array_merge($baseParams, [ - 'revisionId' => $revision->revisionId, - ])), - 'selected' => $revision->id === $element->id, - ]; - }, $revisions), - ]; - } - - if ($hasMoreRevisions && $revisionsPageUrl) { - $items[] = ['type' => MenuItemType::HR]; - $items[] = [ - 'label' => t('View all revisions'), - 'url' => $revisionsPageUrl, - 'attributes' => [ - 'class' => ['go'], - ], - ]; - } - - return $items; - } - - private function _additionalButtons( - ElementInterface $element, - ElementInterface $canonical, - bool $isRevision, - bool $canSave, - bool $canSaveCanonical, - bool $canCreateDrafts, - bool $canDuplicate, - ?array $previewTargets, - bool $enablePreview, - bool $isCurrent, - bool $isUnpublishedDraft, - bool $isDraft, - ): string { - $components = []; - - // Preview (View will be added later by JS) - if ($previewTargets) { - $components[] = - Html::beginTag('div', [ - 'class' => ['preview-btn-container', 'btngroup'], - ]) . - ($enablePreview - ? Html::beginTag('button', [ - 'type' => 'button', - 'class' => ['preview-btn', 'btn'], - ]) . - Html::tag('span', t('Preview'), ['class' => 'label']) . - Html::endTag('button') - : '') . - Html::endTag('div'); - } - - // Create a draft - if ($isCurrent && !$isUnpublishedDraft && $canCreateDrafts) { - if ($canSave) { - $components[] = Html::button(t('Create a draft'), [ - 'class' => ['btn', 'formsubmit'], - 'data' => [ - 'action' => 'elements/save-draft', - 'redirect' => Crypt::encrypt('{cpEditUrl}'), - 'params' => ['dropProvisional' => 1], - ], - ]); - } else { - $components[] = Html::beginForm() . - Html::actionInput('elements/save-draft') . - Html::redirectInput('{cpEditUrl}') . - Html::hiddenInput('elementId', (string)$canonical->id) . - Html::button(t('Create a draft'), [ - 'class' => ['btn', 'formsubmit'], - ]) . - Html::endForm(); - } - } - - if (!$canSave && $canDuplicate) { - // save as a new is now available to people who can create drafts - $components[] = Html::beginForm() . - Html::actionInput('elements/duplicate') . - Html::redirectInput('{cpEditUrl}') . - Html::hiddenInput('elementId', (string)$canonical->id) . - Html::hiddenInput('asUnpublishedDraft', '1') . - Html::button(t('Save as a new {type}', ['type' => $element::lowerDisplayName()]), [ - 'class' => ['btn', 'formsubmit'], - ]) . - Html::endForm(); - } - - // Apply draft - if ($isDraft && !$isCurrent && $canSave && $canSaveCanonical) { - $components[] = Html::button(t('Apply draft'), [ - 'class' => ['btn', 'secondary', 'formsubmit', 'tooltip-draft-btn'], - 'data' => [ - 'action' => 'elements/apply-draft', - 'redirect' => Crypt::encrypt('{cpEditUrl}'), - ], - ]); - } - - // Revert content from this revision - if ($isRevision && $canSaveCanonical && $element->hasRevisions()) { - $components[] = Html::beginForm() . - Html::actionInput('elements/revert') . - Html::redirectInput('{cpEditUrl}') . - Html::hiddenInput('elementId', (string)$canonical->id) . - Html::hiddenInput('revisionId', (string)$element->revisionId) . - Html::button(t('Revert content from this revision'), [ - 'class' => ['btn', 'formsubmit', 'revision-draft-btn'], - ]) . - Html::endForm(); - } - - $components[] = $element->getAdditionalButtons(); - - return implode("\n", array_filter($components)); - } - - private function _prepareEditor( - ElementInterface $element, - bool $isUnpublishedDraft, - bool $canSave, - Response|CpScreenResponse $response, - string $containerId, - callable $contentFn, - callable $sidebarFn, - callable $jsSettingsFn, - ) { - $fieldLayout = $element->getFieldLayout(); - $form = $fieldLayout?->createForm($element, !$canSave, [ - 'registerDeltas' => true, - ]); - $contentHtml = $contentFn($form); - $sidebarHtml = $sidebarFn($form); - - if ($response instanceof CpScreenResponse) { - $behavior = $response; - } else { - /** @var CpScreenResponseBehavior|null $behavior */ - $behavior = $response->getBehavior(CpScreenResponseBehavior::NAME); - } - - if ($contentHtml === '' && $sidebarHtml !== '' && $this->request->getAcceptsJson()) { - $contentHtml = Html::tag('div', $sidebarHtml, [ - 'class' => 'details', - ]); - $sidebarHtml = ''; - $behavior->slideoutBodyClass = 'so-full-details'; - } - - if ($canSave) { - $components = []; - - if ($element->id) { - // don't use the canonical ID if this is a normal element that's keeping track of its canonical - // e.g. nested Matrix entries that were duplicated for an owner's draft - $id = $element->getIsDraft() || $element->getIsRevision() ? $element->getCanonicalId() : $element->id; - $components[] = Html::hiddenInput('elementId', (string)$id); - } - - if ($element->siteId) { - $components[] = Html::hiddenInput('siteId', (string)$element->siteId); - } - - if ($element->fieldLayoutId) { - $components[] = Html::hiddenInput('fieldLayoutId', (string)$element->fieldLayoutId); - } - - if ($isUnpublishedDraft && $this->_fresh) { - $components[] = Html::hiddenInput('fresh', '1'); - } - - if ($this->_updateSearchIndexImmediately) { - $components[] = Html::hiddenInput('updateSearchIndexImmediately', '1'); - } - - $components[] = $contentHtml; - $contentHtml = implode("\n", $components); - } - - $behavior->tabs($form?->getTabMenu() ?? []); - $behavior->contentHtml($contentHtml); - $behavior->metaSidebarHtml($sidebarHtml); - - $settings = $jsSettingsFn($form); - - if ($this->_isSlideout()) { - HtmlStack::jsWithVars(fn($settings) => << <<prepareEditScreen($response, $containerId); - } - - private function _editorContent( - ElementInterface $element, - bool $canSave, - ?FieldLayoutForm $form, - ): string { - $html = $form?->render() ?? ''; - - // Fire a 'defineEditorContent' event - if ($this->hasEventHandlers(self::EVENT_DEFINE_EDITOR_CONTENT)) { - $event = new DefineElementEditorHtmlEvent([ - 'element' => $element, - 'html' => $html, - 'static' => !$canSave, - ]); - $this->trigger(self::EVENT_DEFINE_EDITOR_CONTENT, $event); - $html = $event->html; - } - - return trim($html); - } - - /** - * Return html for errors summary box - * - * @param ElementInterface $element - * @return string - */ - private function _errorSummary(ElementInterface $element): string - { - $html = ''; - - if ($element->errors()->isNotEmpty()) { - $allErrors = $element->errors()->getMessages(); - $allKeys = array_keys($allErrors); - - // only show "top-level" errors - // if you e.g. have an assets field which is set to validate related assets, - // you should only see the top-level "Fix validation errors on the related asset" error - // and not the details of what's wrong with the selected asset; - foreach ($allKeys as $key) { - $lastNestedKey = substr_replace($key, '', strrpos($key, '.')); - $lastNestedKey = substr_replace($lastNestedKey, '', strrpos($lastNestedKey, '[')); - if (!empty($lastNestedKey)) { - if (in_array($lastNestedKey, $allKeys)) { - unset($allErrors[$key]); - } - } - } - $errorsList = []; - $tabs = $element->getFieldLayout()->getTabs(); - foreach ($allErrors as $key => $errors) { - foreach ($errors as $error) { - // this is true in case of e.g. cross site validation error - if (preg_match('/^\s?\getElements() as $layoutElement) { - if ($layoutElement instanceof BaseField && $layoutElement->attribute() === $fieldKey) { - $tabUid = $tab->uid; - break 2; - } - } - } - - // If the error is for a recursively-nested Matrix field, - // manipulate the key to only reference the nested Matrix field, entry and inner field - // Before: foo[].bar[].baz - // After: bar[].baz - if (substr_count($key, '.') > 1) { - $keyParts = explode('.', $key); - if (preg_match(sprintf('/\[%s\]$/', Str::uuidPattern()), $keyParts[count($keyParts) - 3])) { - $key = implode('.', array_slice($keyParts, -2)); - } - } - - $errorItem = null; - if ($error !== null) { - $error = Markdown::processParagraph(htmlspecialchars($error)); - $errorItem = Html::beginTag('li'); - $errorItem .= Html::a(t($error), '#', [ - 'data' => [ - 'field-error-key' => $key, - 'layout-tab' => $tabUid, - ], - ]); - $errorItem .= Html::endTag('li'); - } - } - - if ($errorItem !== null) { - $errorsList[] = $errorItem; - } - } - } - - if (!empty($errorsList)) { - $heading = t('Found {num, number} {num, plural, =1{error} other{errors}}', [ - 'num' => count($errorsList), - ]); - - $html = Html::beginTag('div', [ - 'class' => ['error-summary'], - 'tabindex' => '-1', - ]) . - Html::beginTag('div') . - Html::tag('span', '', [ - 'class' => 'notification-icon', - 'data-icon' => 'alert', - 'aria-label' => t('Error'), - 'role' => 'img', - ]) . - Html::tag('h2', $heading) . - Html::endTag('div') . - Html::beginTag('ul', [ - 'class' => ['errors'], - ]) . - implode('', $errorsList) . - Html::endTag('ul') . - Html::endTag('div'); - } - } - - return $html; - } - - private function _editorSidebar( - ElementInterface $element, - bool $mergedCanonicalChanges, - bool $canSave, - ): string { - $components = []; - - if ($mergedCanonicalChanges) { - $components[] = - Html::beginTag('div', [ - 'class' => ['meta', 'warning'], - ]) . - Html::tag('p', t('Recent changes to the Current revision have been merged into this draft.')) . - Html::endTag('div'); - } - - $components[] = $element->getSidebarHtml(!$canSave); - - if ($this->id) { - $components[] = app(ContentHtml::class)->metadataHtml($element->getMetadata()); - } - - return trim(implode("\n", $components)); - } - - /** - * Returns an array of action menu items for the element. - * - * @param ElementInterface $element - * @param array $previewTargets - * @return array - */ - private function _actionMenuItems(ElementInterface $element, array $previewTargets): array - { - if (!$element->id) { - return []; - } - - $hideViewAction = !empty($previewTargets) && !$this->_isSlideout(); - - return array_filter( - $element->getActionMenuItems(), - function(array $item) use ($hideViewAction) { - // filter out "Edit" item - no point showing edit action on the edit page, - if (str_starts_with($item['id'] ?? '', 'action-edit-')) { - return false; - } - - // and "View in a new tab" item, if we have at least one preview target, and it's not a slideout - // as that action is already covered by the "View" button; - // (https://github.com/craftcms/cms/issues/16556) - if ($hideViewAction && str_starts_with($item['id'] ?? '', 'action-view-')) { - return false; - } - - return true; - }, - ); - } - - /** - * Returns whether this is for a slideout. - * - * @return bool - */ - private function _isSlideout(): bool - { - return $this->request->getHeaders()->has('X-Craft-Container-Id'); - } - - private function _draftNotice(): string - { - return - Html::beginTag('div', [ - 'class' => 'draft-notice', - ]) . - Html::tag('div', '', [ - 'class' => ['draft-icon'], - 'aria' => ['hidden' => 'true'], - 'data' => ['icon' => 'edit'], - ]) . - Html::tag('p', t('Showing your unsaved changes.')) . - Html::button(t('Discard'), [ - 'class' => ['discard-changes-btn', 'btn'], - ]) . - Html::endTag('div'); - } - - private function _revisionNotice($elementType): string - { - return - Html::beginTag('div', [ - 'class' => 'content-notice', - ]) . - Html::tag('div', '', [ - 'class' => ['content-notice-icon'], - 'aria' => ['hidden' => 'true'], - 'data' => ['icon' => 'lightbulb'], - ]) . - Html::tag('p', t( - 'You’re viewing a revision. None of the {type}’s fields are editable.', - [ - 'type' => $elementType, - ] - )) . - Html::endTag('div'); - } - - /** - * Saves an element. - * - * @return Response|null - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @throws ServerErrorHttpException - * @since 4.0.0 - */ - public function actionSave(): ?Response - { - $this->requirePostRequest(); - - $element = $this->_element(); - - if ($element instanceof Response) { - return $element; - } - - if (!$element || $element->getIsDraft() || $element->getIsRevision()) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - $this->element = $element; - - // Check save permissions before and after applying POST params to the element - // in case the request was tampered with. - Gate::authorize('save', $element); - - $this->_applyParamsToElement($element); - - Gate::authorize('save', $element); - - if ($element->enabled && $element->getEnabledForSite()) { - $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); - } - - $isNotNew = $element->id; - if ($isNotNew) { - $mutex = Cache::lock("element:$element->id", 15); - if (!$mutex->get()) { - throw new ServerErrorHttpException('Could not acquire a lock to save the element.'); - } - } - - if ($element instanceof NestedElementInterface && property_exists($element, 'updateSearchIndexForOwner')) { - $element->updateSearchIndexForOwner = true; - } - - try { - $namespace = $this->request->getHeaders()->get('X-Craft-Namespace'); - // crossSiteValidate only if it's multisite, element supports drafts and we're not in a slideout - $success = Elements::saveElement( - $element, - crossSiteValidate: ($namespace === null && Sites::isMultiSite() && Gate::check('createDrafts', $element)), - ); - } catch (UnsupportedSiteException $e) { - $element->errors()->add('siteId', $e->getMessage()); - $success = false; - } finally { - if ($isNotNew) { - $mutex->release(); - } - } - - if (!$success) { - return $this->_asFailure($element, mb_ucfirst(t('Couldn’t save {type}.', [ - 'type' => $element::lowerDisplayName(), - ]))); - } - - ElementActivityFacade::trackActivity($element, ElementActivityType::Save); - - // See if the user happens to have a provisional element. If so delete it. - $provisional = $element::find() - ->provisionalDrafts() - ->draftOf($element->id) - ->draftCreator(static::currentUser()) - ->siteId($element->siteId) - ->status(null) - ->one(); - - if ($provisional) { - Elements::deleteElement($provisional, true); - } - - if (!$this->request->getAcceptsJson()) { - // Tell all browser windows about the element save - Craft::$app->getSession()->broadcastToJs([ - 'event' => 'saveElement', - 'id' => $element->id, - ]); - } - - return $this->_asSuccess(t('{type} saved.', [ - 'type' => $element::displayName(), - ]), $element, supportsAddAnother: true); - } - - /** - * Saves a nested element for a derivative of its owner. - * - * @return Response|null - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @throws ServerErrorHttpException - * @since 5.5.0 - */ - public function actionSaveNestedElementForDerivative(): ?Response - { - $this->requirePostRequest(); - $this->requireAcceptsJson(); - - if (!isset($this->_newOwnerId)) { - throw new BadRequestHttpException('No new owner was identified by the request.'); - } - - /** @var Element|null $element */ - $element = $this->_element(); - - if ( - !$element instanceof NestedElementInterface || - !$element->getOwnerId() || - !$element->getIsDraft() || - $element->getIsCanonical() - ) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - $this->element = $element; - $user = static::currentUser(); - - // Check save permissions before and after applying POST params to the element - // in case the request was tampered with. - Gate::authorize('save', $element); - - // Get the new owner and make sure it's a derivative element, - // and that its canonical element is the nested element's primary owner - $owner = Elements::getElementById($this->_newOwnerId, siteId: $element->siteId); - if ($owner->getIsCanonical()) { - throw new BadRequestHttpException('The owner element must be a derivative.'); - } - if ($owner->getCanonicalId() !== $element->getPrimaryOwnerId()) { - // the owner might be a derivative of another canonical element - $canonicalOwner = $owner->getCanonical(); - if ($canonicalOwner->getCanonicalId() !== $element->getPrimaryOwnerId()) { - throw new BadRequestHttpException('The canonical owner element must be the primary owner of the nested element.'); - } - } - - Gate::authorize('save', $owner); - - // Get the old sort order - $sortOrder = DbFacade::table(Table::ELEMENTS_OWNERS) - ->where('elementId', $element->id) - ->where('ownerId', $element->getOwnerId()) - ->value('sortOrder'); - - $element->setSortOrder($sortOrder); - - DbFacade::beginTransaction(); - - try { - // Remove existing ownership data for the element within the canonical owner, - // and for its canonical element within the derivative - DbFacade::table(Table::ELEMENTS_OWNERS) - ->where(['elementId' => $element->id, 'ownerId' => $owner->getCanonicalId()]) - ->orWhere(['elementId' => $element->getCanonicalId(), 'ownerId' => $owner->id]) - ->delete(); - - // Remove existing ownership data for the element within the canonical owner - DbFacade::table(Table::ELEMENTS_OWNERS) - ->where([ - 'elementId' => $element->id, - 'ownerId' => $owner->getCanonicalId(), - ]) - ->delete(); - - // Remove the draft data, but preserve the canonicalId - $element->setPrimaryOwner($owner); - $element->setOwner($owner); - Elements::saveElement($element); - - $this->_applyParamsToElement($element); - - Gate::authorize('save', $element); - - if ($element->enabled && $element->getEnabledForSite()) { - $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); - } - - try { - $success = Elements::saveElement($element); - } catch (UnsupportedSiteException $e) { - $element->errors()->add('siteId', $e->getMessage()); - $success = false; - } - - if (!$success) { - DbFacade::rollBack(); - return $this->_asFailure($element, mb_ucfirst(t('Couldn’t save {type}.', [ - 'type' => $element::lowerDisplayName(), - ]))); - } - - if ($element->getIsDraft()) { - app(Drafts::class)->removeDraftData($element); - } - - DbFacade::commit(); - } catch (Throwable $e) { - DbFacade::rollBack(); - throw $e; - } - - return $this->_asSuccess(t('{type} saved.', [ - 'type' => $element::displayName(), - ]), $element); - } - - /** - * Duplicates an element. - * - * @return Response|null - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @throws ServerErrorHttpException - * @since 4.0.0 - */ - public function actionDuplicate(): ?Response - { - $this->requirePostRequest(); - - /** @var (ElementInterface)|null $element */ - $element = $this->_element(); - - if (!$element || $element->getIsRevision()) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - $this->element = $element; - - // save as a new is now available to people who can create drafts - $asUnpublishedDraft = $this->_asUnpublishedDraft && $element::hasDrafts(); - if ($asUnpublishedDraft) { - Gate::authorize('duplicateAsDraft', $element); - } else { - Gate::authorize('duplicate', $element); - } - - $newAttributes = [ - 'isProvisionalDraft' => false, - 'draftId' => null, - ]; - - if ($asUnpublishedDraft && - ($element->getIsCanonical() || $element->isProvisionalDraft) && - $element->slug === $element->getCanonical()->slug - ) { - $newAttributes += [ - 'slug' => null, - ]; - } - - if ($element instanceof NestedElementInterface) { - $newAttributes += [ - 'primaryOwnerId' => $element->getOwnerId(), - 'ownerId' => $element->getOwnerId(), - 'sortOrder' => null, - ]; - } - - try { - $newElement = Elements::duplicateElement( - $element, - $newAttributes, - asUnpublishedDraft: $asUnpublishedDraft, - ); - } catch (InvalidElementException $e) { - return $this->_asFailure($e->element, t('Couldn’t duplicate {type}.', [ - 'type' => $element::lowerDisplayName(), - ])); - } catch (Throwable $e) { - throw new ServerErrorHttpException('An error occurred when duplicating the element.', 0, $e); - } - - // If the original element is a provisional draft, - // delete the draft as the changes are likely no longer wanted. - if ($this->_deleteProvisionalDraft && $element->isProvisionalDraft) { - Elements::deleteElement($element); - } - - return $this->_asSuccess(t('{type} duplicated.', [ - 'type' => $element::displayName(), - ]), $newElement); - } - - /** - * Duplicates multiple elements with the given new attributes. - * - * @return Response|null - * @since 5.7.0 - */ - public function actionBulkDuplicate(): ?Response - { - $this->requirePostRequest(); - - $elementInfo = $this->request->getRequiredBodyParam('elements'); - $newAttributes = $this->request->getRequiredBodyParam('newAttributes'); - - $newElementInfo = []; - - $result = DbFacade::transaction(function() use ($elementInfo, $newAttributes, &$newElementInfo) { - return BulkOps::ensure(function() use ($elementInfo, $newAttributes, &$newElementInfo) { - foreach ($elementInfo as $info) { - $element = $this->_element($info); - - if (!$element instanceof ElementInterface) { - Log::warning(sprintf('Unable to duplicate element: %s', Json::encode($info)), [__METHOD__]); - continue; - } - - $safeNewAttributes = Collection::make($newAttributes) - ->only($element->safeAttributes()) - ->all(); - - // if element is a revision, we need to nullify some additional attributes - if ($element->getIsRevision()) { - $safeNewAttributes['revisionId'] = null; - - if ($element->dateDeleted !== null) { - $safeNewAttributes['dateDeleted'] = null; - $safeNewAttributes['deletedWithOwner'] = null; - $safeNewAttributes['trashed'] = false; - } - } - - try { - $newElement = Elements::duplicateElement( - $element, - $safeNewAttributes + $element::baseBulkDuplicateAttributes(), - false, - checkAuthorization: true, - ); - } catch (InvalidElementException $e) { - return $this->_asFailure($e->element, t('Couldn’t duplicate {type}.', [ - 'type' => $element::lowerDisplayName(), - ])); - } catch (ForbiddenHttpException $e) { - throw $e; - } catch (Throwable $e) { - throw new ServerErrorHttpException('An error occurred when duplicating the element.', 0, $e); - } - - $newElementInfo[] = $newElement->toArray($newElement->attributes()); - } - - return null; - }); - }); - - if ($result !== null) { - return $result; - } - - /** @var class-string $elementType */ - $elementType = $elementInfo[0]['type']; - return $this->asSuccess(mb_ucfirst(t('{type} duplicated.', [ - 'type' => count($elementInfo) === 1 ? $elementType::displayName() : $elementType::pluralDisplayName(), - ])), [ - 'newElements' => $newElementInfo, - ]); - } - - /** - * Deletes an element. - * - * @return Response|null - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @since 4.0.0 - */ - public function actionDelete(): ?Response - { - $this->requirePostRequest(); - - /** @var Element|null $element */ - $element = $this->_element(); - - // If this is a provisional draft, delete the canonical - if ($element && $element->isProvisionalDraft) { - $element = $element->getCanonical(true); - } - - if (!$element || $element->getIsDraft() || $element->getIsRevision()) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - $this->element = $element; - - Gate::authorize('delete', $element); - - if (!Elements::deleteElement($element)) { - return $this->_asFailure($element, t('Couldn’t delete {type}.', [ - 'type' => $element::lowerDisplayName(), - ])); - } - - return $this->_asSuccess(t('{type} deleted.', [ - 'type' => $element::displayName(), - ]), $element); - } - - /** - * Deletes an element for a single site. - * - * @return Response - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @since 4.0.0 - */ - public function actionDeleteForSite(): Response - { - $this->requirePostRequest(); - - /** @var Element|null $element */ - $element = $this->_element(checkForProvisionalDraft: true); - - if (!$element || $element->getIsRevision()) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - $this->element = $element; - - Gate::authorize('deleteForSite', $element); - - Elements::deleteElementForSite($element); - - if ($element->isProvisionalDraft) { - // see if the canonical element exists for this site - $canonical = $element->getCanonical(); - if ($canonical->id !== $element->id) { - $element = $canonical; - Elements::deleteElementForSite($element); - } - } - - return $this->_asSuccess(t('{type} deleted for site.', [ - 'type' => $element->getIsDraft() && !$element->isProvisionalDraft ? t('Draft') : $element::displayName(), - ]), $element); - } - - /** - * Validates an element. - * - * @return Response|null - * @since 5.8.0 - */ - public function actionValidate(): ?Response - { - $this->requirePostRequest(); - - /** - * @var Element|Response|null $element - */ - $element = $this->_element(); - - // this can happen if we're creating e.g. nested entry in a matrix field (cards or element index) - // and we hit "create entry" before the autosave kicks in - if ($element instanceof Response) { - return $element; - } - - if (!$element || $element->getIsRevision()) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); - - if (!$element->validate()) { - return $this->_asFailure($element, t('{type} validation failed.', [ - 'type' => $element::displayName(), - ])); - } - - return $this->_asSuccess(t('{type} validation successful.', [ - 'type' => $element::displayName(), - ]), $element); - } - - /** - * Saves a draft. - * - * @return Response|null - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @throws ServerErrorHttpException - * @since 4.0.0 - */ - public function actionSaveDraft(): ?Response - { - $this->requirePostRequest(); - - /** - * @var Element|Response|null $element - */ - $element = $this->_element(); - - // this can happen if we're creating e.g. nested entry in a matrix field (cards or element index) - // and we hit "create entry" before the autosave kicks in - if ($element instanceof Response) { - return $element; - } - - if (!$element || $element->getIsRevision()) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - $user = static::currentUser(); - - if (!$element->getIsDraft() && !$this->_provisional) { - Gate::authorize('createDrafts', $element); - } elseif (!$this->_canSave($element, $user)) { - throw new ForbiddenHttpException('User not authorized to save this element.'); - } - - $this->element = $element; - - if (!$element->getIsDraft() && $this->_provisional) { - // Make sure a provisional draft doesn't already exist for this element/user combo - $existingProvisionalDraft = $element::find() - ->provisionalDrafts() - ->draftOf($element->id) - ->draftCreator($user->id) - ->site('*') - ->status(null) - ->one(); - - if ($existingProvisionalDraft) { - Log::warning("Overwriting an existing provisional draft for element/user $element->id/$user->id", [__METHOD__]); - Elements::deleteElement($existingProvisionalDraft, true); - } - } - - // Keep track of all newly-created draft IDs - $draftElementIds = []; - $draftElementUids = []; - $draftsService = app(Drafts::class); - - Event::listen(DraftCreated::class, function(DraftCreated $event) use (&$draftElementIds, &$draftElementUids) { - $draftElementIds[$event->canonical->id] = $event->draft->id; - $draftElementUids[$event->canonical->uid] = $event->draft->uid; - }); - - DbFacade::beginTransaction(); - - try { - // Are we creating the draft here? - if (!$element->getIsDraft()) { - /** @var Element $element */ - $draft = $draftsService->createDraft($element, $user->id, null, null, [], $this->_provisional); - $draft->setCanonical($element); - $element = $this->element = $draft; - } - - // keep track of the original field layout ID, in case it changes here - $oldFieldLayoutId = $element->getFieldLayout()?->id; - - $this->_applyParamsToElement($element); - - // Make sure nothing just changed that would prevent the user from saving - if (!$this->_canSave($element, $user)) { - throw new ForbiddenHttpException('User not authorized to save this element.'); - } - - if ($this->_dropProvisional) { - $element->isProvisionalDraft = false; - } - - $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS); - - // If the field layout ID changed, save all content - $saveContent = $element->getFieldLayout()?->id !== $oldFieldLayoutId; - - if (!Elements::saveElement($element, saveContent: $saveContent)) { - DbFacade::rollBack(); - return $this->_asFailure($element, mb_ucfirst(t('Couldn’t save {type}.', [ - 'type' => t('draft'), - ]))); - } - - DbFacade::commit(); - } catch (Throwable $e) { - DbFacade::rollBack(); - throw $e; - } - - ElementActivityFacade::trackActivity($element, ElementActivityType::Save); - - $creator = $element->getDraftCreator(); - - $data = [ - 'canonicalId' => $element->getCanonicalId(), - 'elementId' => $element->id, - 'draftId' => $element->draftId, - 'timestamp' => I18N::getFormatter()->asTimestamp($element->dateUpdated, 'short', true), - 'creator' => $creator?->getName(), - 'draftName' => $element->draftName, - 'draftNotes' => $element->draftNotes, - 'modifiedAttributes' => $element->getModifiedAttributes(), - 'draftElementIds' => $draftElementIds, - 'draftElementUids' => $draftElementUids, - ]; - - if ($this->request->getIsCpRequest()) { - [$docTitle, $title] = $this->_editElementTitles($element); - $previewTargets = $element->getPreviewTargets(); - $data += $this->_fieldLayoutData($element, [ - 'registerDeltas' => true, - ]); - $data += [ - 'docTitle' => $docTitle, - 'title' => $title, - 'previewTargets' => $previewTargets, - 'previewParamValue' => $previewTargets ? Crypt::encrypt(Str::random(10)) : null, - 'deltaNames' => DeltaRegistry::getNames(), - 'initialDeltaValues' => DeltaRegistry::getInitialValues(), - 'updatedTimestamp' => $element->dateUpdated->getTimestamp(), - 'canonicalUpdatedTimestamp' => $element->getCanonical()->dateUpdated->getTimestamp(), - ]; - } - - // Make sure the user is authorized to preview the draft - SessionAuth::authorize("previewDraft:$element->draftId"); - - return $this->_asSuccess(t('{type} saved.', [ - 'type' => t('Draft'), - ]), $element, $data, true); - } - - /** - * Ensures that a provisional draft exists for the element, unless it’s already a draft. - * - * @return Response - * @since 5.0.0 - */ - public function actionEnsureDraft(): Response - { - $this->requirePostRequest(); - - /** - * @var Element|null $element - */ - $element = $this->_element(checkForProvisionalDraft: true); - - if (!$element || $element->getIsRevision()) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - if ($element->getIsDraft()) { - return $this->asSuccess(data: [ - 'elementId' => $element->id, - ]); - } - - $user = static::currentUser(); - - Gate::authorize('createDrafts', $element); - - $this->element = $element; - - // Make sure a provisional draft doesn't already exist for this element/user combo - $provisionalId = $element::find() - ->provisionalDrafts() - ->draftOf($element->id) - ->draftCreator($user->id) - ->site('*') - ->status(null) - ->ids()[0] ?? null; - - if ($provisionalId) { - return $this->asSuccess(data: [ - 'elementId' => $provisionalId, - ]); - } - - /** @var Element $element */ - $draft = app(Drafts::class)->createDraft($element, $user->id, provisional: true); - - return $this->asSuccess(data: [ - 'elementId' => $draft->id, - ]); - } - - /** - * Applies a draft to its canonical element. - * - * @return Response|null - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @throws ServerErrorHttpException - * @since 4.0.0 - */ - public function actionApplyDraft(): ?Response + public static function registerEvents(): void { - $this->requirePostRequest(); - - /** - * @var Element|Response|null $element - */ - $element = $this->_element(); - - // this can happen if creating element via slideout, and we hit "create entry" before the autosave kicks in - if ($element instanceof Response) { - return $element; - } - - if (!$element || !$element->getIsDraft()) { - throw new BadRequestHttpException('No draft was identified by the request.'); - } - - $this->element = $element; - - // keep track of the original field layout ID, in case it changes here - $oldFieldLayoutId = $element->getFieldLayout()?->id; - - $this->_applyParamsToElement($element); - - Gate::authorize('save', $element); - - $isUnpublishedDraft = $element->getIsUnpublishedDraft(); - - if (!Gate::check('saveCanonical', $element)) { - throw new ForbiddenHttpException($isUnpublishedDraft - ? 'User not authorized to create this element.' - : 'User not authorized to save this element.'); - } - - // Validate and save the draft - if ($element->enabled && $element->getEnabledForSite()) { - $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); - } - - // if we're about to apply an unpublished draft, set propagateRequired to true - if ($isUnpublishedDraft) { - $element->propagateRequired = true; - } - - $element->applyingDraft = true; - - // If the field layout ID changed, save all content - $saveContent = $element->getFieldLayout()?->id !== $oldFieldLayoutId; - - $namespace = $this->request->getHeaders()->get('X-Craft-Namespace'); - $crossSiteValidate = $namespace === null && Craft::$app->getIsMultiSite(); - - if (!Elements::saveElement( - element: $element, - crossSiteValidate: $crossSiteValidate, - saveContent: $saveContent, - )) { - // save the draft anyway, so we don’t lose the latest changes - // (see https://github.com/craftcms/cms/issues/18657) - $errors = $element->getErrors(); - $invalidNestedElementIds = $element->getInvalidNestedElementIds(); - $element->ruleset->useScenario(ElementRules::SCENARIO_ESSENTIALS); - Elements::saveElement(element: $element, saveContent: $saveContent); - $element->clearErrors(); - $element->addErrors($errors); - $element->addInvalidNestedElementIds($invalidNestedElementIds); - return $this->_asAppyDraftFailure($element); - } - - $element->applyingDraft = false; - - if (!$isUnpublishedDraft) { - $mutex = Cache::lock("element:$element->canonicalId", 15); - if (!$mutex->get()) { - throw new ServerErrorHttpException('Could not acquire a lock to save the element.'); - } - } - - $attributes = []; - if ($element instanceof NestedElementInterface) { - $attributes['updateSearchIndexForOwner'] = true; - } - - try { - $element->propagateRequired = false; - $canonical = app(Drafts::class)->applyDraft($element, $attributes); - } catch (InvalidElementException) { - return $this->_asAppyDraftFailure($element); - } finally { - if (!$isUnpublishedDraft) { - $mutex->release(); - } - } - - ElementActivityFacade::trackActivity($canonical, ElementActivityType::Save); - - if (!$this->request->getAcceptsJson()) { - // Tell all browser windows about the element save - $session = Craft::$app->getSession(); - $session->broadcastToJs([ - 'event' => 'saveElement', - 'id' => $canonical->id, - ]); - if (!$isUnpublishedDraft) { - $session->broadcastToJs([ - 'event' => 'deleteDraft', - 'canonicalId' => $element->getCanonicalId(), - 'draftId' => $element->draftId, - ]); + Event::listen(function(DefineElementEditorContent $event) { + if (!YiiEvent::hasHandlers(ElementsController::class, ElementsController::EVENT_DEFINE_EDITOR_CONTENT)) { + return; } - } - - if ($isUnpublishedDraft) { - $message = t('{type} created.', [ - 'type' => $element::displayName(), - ]); - } elseif ($element->isProvisionalDraft) { - $message = t('{type} saved.', [ - 'type' => $element::displayName(), - ]); - } else { - $message = t('Draft applied.'); - } - - return $this->_asSuccess($message, $canonical, supportsAddAnother: true); - } - - private function _asAppyDraftFailure(ElementInterface $element): ?Response - { - if ($element->getIsUnpublishedDraft()) { - $message = mb_ucfirst(t('Couldn’t create {type}.', [ - 'type' => $element::lowerDisplayName(), - ])); - } elseif ($element->isProvisionalDraft) { - $message = mb_ucfirst(t('Couldn’t save {type}.', [ - 'type' => $element::lowerDisplayName(), - ])); - } else { - $message = t('Couldn’t apply draft.'); - } - - return $this->_asFailure($element, $message); - } - - /** - * Deletes a draft. - * - * @return Response|null - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @since 4.0.0 - */ - public function actionDeleteDraft(): ?Response - { - $this->requirePostRequest(); - - $element = $this->_element(); - if ($element instanceof Response) { - return $element; - } - - if (!$element || !$element->getIsDraft()) { - throw new BadRequestHttpException('No draft was identified by the request.'); - } - - $this->element = $element; - - Gate::authorize('delete', $element); - - if (!Elements::deleteElement($element, true)) { - return $this->_asFailure($element, t('Couldn’t delete {type}.', [ - 'type' => t('draft'), - ])); - } - - if ($element->isProvisionalDraft) { - $message = t('Changes discarded.'); - } else { - $message = t('{type} deleted.', [ - 'type' => t('Draft'), - ]); - } - - if (!$this->request->getAcceptsJson()) { - // Tell all browser windows about the draft deletion - Craft::$app->getSession()->broadcastToJs([ - 'event' => 'deleteDraft', - 'canonicalId' => $element->getCanonicalId(), - 'draftId' => $element->draftId, + $yiiEvent = new DefineElementEditorHtmlEvent([ + 'element' => $event->element, + 'html' => $event->html, + 'static' => $event->static, ]); - } - - return $this->_asSuccess($message, $element); - } - - /** - * Reverts an element’s content to a revision. - * - * @return Response - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @since 4.0.0 - */ - public function actionRevert(): Response - { - $this->requirePostRequest(); - - /** - * @var Element|null $element - */ - $element = $this->_element(); - - if (!$element || !$element->getIsRevision()) { - throw new BadRequestHttpException('No revision was identified by the request.'); - } - - $this->element = $element; - - $user = static::currentUser(); - - Gate::authorize('save', $element->getCanonical(true)); - - $canonical = app(Revisions::class)->revertToRevision($element, $user->id); - - ElementActivityFacade::trackActivity($canonical, ElementActivityType::Save); - - return $this->_asSuccess(t('{type} reverted to past revision.', [ - 'type' => $element::displayName(), - ]), $canonical); - } - - /** - * Returns an element’s missing field layout components. - * - * @return Response|null - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @throws ServerErrorHttpException - * @since 4.6.0 - */ - public function actionUpdateFieldLayout(): ?Response - { - $this->requirePostRequest(); - $this->requireCpRequest(); - - if ($this->_elementId || $this->_elementUid) { - $element = $this->_element(); - } else { - $element = $this->_createElement(); - } - - // Prevalidate? - if ($this->_prevalidate && $element->enabled && $element->getEnabledForSite()) { - $element->ruleset->useScenario(ElementRules::SCENARIO_LIVE); - $element->validate(); - } - - /** - * see https://github.com/craftcms/cms/issues/14635#issuecomment-2349006694 for details - * @var Element|Response|null $element - */ - if ($element instanceof Response) { - return $element; - } - - if (!$element || $element->getIsRevision()) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - Gate::authorize('view', $element); - - $this->element = $element; - $this->_applyParamsToElement($element); - // Make sure nothing just changed that would prevent the user from saving - Gate::authorize('view', $element); + YiiEvent::trigger(ElementsController::class, ElementsController::EVENT_DEFINE_EDITOR_CONTENT, $yiiEvent); - $data = $this->_fieldLayoutData($this->element); - - $data += [ - 'initialDeltaValues' => DeltaRegistry::getInitialValues(), - ]; - - return $this->_asSuccess('Field layout updated.', $element, $data, true); - } - - private function _fieldLayoutData(ElementInterface $element, array $formConfig = []): array - { - $namespace = $this->request->getHeaders()->get('X-Craft-Namespace'); - $fieldLayout = $element->getFieldLayout(); - $form = $fieldLayout->createForm($element, false, $formConfig + [ - 'namespace' => $namespace, - 'registerDeltas' => false, - 'visibleElements' => $this->_visibleLayoutElements, - 'staticElements' => $this->_staticLayoutElements, - ]); - $missingElements = []; - foreach ($form->tabs as $tab) { - if (!$tab->getUid()) { - continue; - } - - $elementInfo = []; - - foreach ($tab->elements as $formElement) { - if ($formElement->isConditional) { - $elementInfo[] = [ - 'uid' => $formElement->layoutElement->uid, - 'html' => $formElement->html, - 'static' => $formElement->isStatic, - ]; - } - } - - $missingElements[] = [ - 'uid' => $tab->getUid(), - 'id' => $tab->getId(), - 'elements' => $elementInfo, - ]; - } - - $tabs = $form->getTabMenu(); - if (count($tabs) > 1) { - $selectedTab = isset($tabs[$this->_selectedTab]) ? $this->_selectedTab : null; - $tabHtml = InputNamespace::namespaceInputs(fn() => \CraftCms\Cms\template('_includes/tabs', [ - 'tabs' => $tabs, - 'selectedTab' => $selectedTab, - ], templateMode: TemplateMode::Cp), $namespace); - } else { - $tabHtml = null; - } - - return [ - 'tabs' => $tabHtml, - 'missingElements' => $missingElements, - 'headHtml' => HtmlStack::headHtml(), - 'bodyHtml' => HtmlStack::bodyHtml(), - ]; - } - - /** - * Returns any recent activity for an element, and records that the user is viewing the element. - * - * @return Response - * @since 4.5.0 - */ - public function actionRecentActivity(): Response - { - $element = $this->_element(); - - if ($element instanceof Response) { - return $element; - } - - if (!$element || $element->getIsRevision()) { - throw new BadRequestHttpException('No element was identified by the request.'); - } - - $currentUser = Auth::user(); - $activity = ElementActivityFacade::getRecentActivity($element, $currentUser->id); - ElementActivityFacade::trackActivity($element, ElementActivityType::View, $currentUser); - - return $this->asJson([ - 'activity' => $activity->map(fn(ElementActivity $record) => $record->toActivityRow($element))->all(), - 'updatedTimestamp' => $element->dateUpdated->getTimestamp(), - 'canonicalUpdatedTimestamp' => $element->getCanonical()->dateUpdated->getTimestamp(), - ]); - } - - /** - * Returns the requested element, populated with any posted attributes. - * - * @param array|null $elementInfo - * @param bool $checkForProvisionalDraft - * @param bool $strictSite - * @return ElementInterface|Response|null - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - */ - private function _element( - ?array $elementInfo = null, - bool $checkForProvisionalDraft = false, - bool $strictSite = true, - ): ElementInterface|Response|null { - $user = static::currentUser(); - - $elementType = $elementInfo['type'] ?? $this->_elementType; - $elementId = $elementInfo['id'] ?? $this->_elementId; - $elementUid = $elementInfo['uid'] ?? $this->_elementUid; - $fieldId = $elementInfo['fieldId'] ?? $this->_fieldId; - $ownerId = $elementInfo['ownerId'] ?? $this->_ownerId; - $siteId = $elementInfo['siteId'] ?? $this->_siteId; - $draftId = $elementInfo['draftId'] ?? $this->_draftId; - $revisionId = $elementInfo['revisionId'] ?? $this->_revisionId; - $provisional = $elementInfo['isProvisionalDraft'] ?? $this->_provisional; - - if (!$elementType) { - if ($elementId) { - $elementType = Elements::getElementTypeById($elementId); - if (!$elementType) { - throw new BadRequestHttpException("Invalid element ID: $elementId"); - } - } elseif ($elementUid) { - $elementType = Elements::getElementTypeByUid($elementUid); - if (!$elementType) { - throw new BadRequestHttpException("Invalid element UUID: $elementUid"); - } - } else { - throw new BadRequestHttpException('Request missing required param.'); - } - } - - /** @var class-string $elementType */ - $this->_validateElementType($elementType); - - if ($elementType::isLocalized()) { - if ($siteId) { - $site = Sites::getSiteById($siteId, true); - if (!$site) { - throw new BadRequestHttpException("Invalid side ID: $siteId"); - } - if (Sites::isMultiSite() && !$user->can("editSite:$site->uid")) { - throw new ForbiddenHttpException('User not authorized to edit content for this site.'); - } - } else { - $site = app(RequestedSite::class)->get(); - if (!$site) { - throw new ForbiddenHttpException('User not authorized to edit content in any sites.'); - } - } - - if ($strictSite) { - $siteId = $site->id; - $preferSites = null; - } else { - $siteId = Sites::getEditableSiteIds()->all(); - $preferSites = [$site->id]; - } - } else { - $siteId = $preferSites = null; - } - - // Loading an existing element? - if ($draftId || $revisionId) { - $query = $this->_elementQuery($elementType, $fieldId, $ownerId) - ->draftId($draftId) - ->revisionId($revisionId) - ->provisionalDrafts($provisional) - ->siteId($siteId) - ->preferSites($preferSites) - ->unique() - ->status(null); - - if ($revisionId) { - $query->trashed(null); - } - - $element = $query->one(); - - if (!$element) { - // check for the canonical element as a fallback - $element = $this->_elementById( - $elementId, - $elementUid, - $fieldId, - $ownerId, - false, - $elementType, - $user, - $siteId, - $preferSites, - ); - if ($element && $user->can('view', $element)) { - if (!$this->request->getAcceptsJson()) { - return $this->redirect($element->getCpEditUrl()); - } - return $element; - } - throw new BadRequestHttpException($draftId ? "Invalid draft ID: $draftId" : "Invalid revision ID: $revisionId"); - } - } elseif ($elementId || $elementUid) { - $element = $this->_elementById( - $elementId, - $elementUid, - $fieldId, - $ownerId, - $checkForProvisionalDraft, - $elementType, - $user, - $siteId, - $preferSites, - ); - if (!$element) { - throw new BadRequestHttpException($elementId ? "Invalid element ID: $elementId" : "Invalid element UUID: $elementUid"); - } - } else { - return null; - } - - if (!$user->can('view', $element)) { - throw new ForbiddenHttpException('User not authorized to edit this element.'); - } - - if ( - !$strictSite && - isset($site) && - $element->siteId !== $site->id && - !$this->request->getAcceptsJson() - ) { - return $this->redirect($element->getCpEditUrl()); - } - - return $element; - } - - /** - * @param int|null $elementId - * @param string|null $elementUid - * @param int|null $fieldId - * @param int|null $ownerId - * @param bool $checkForProvisionalDraft - * @param class-string $elementType - * @param User $user - * @param int|array|null $siteId - * @param array|null $preferSites - * @return ElementInterface|null - */ - private function _elementById( - ?int $elementId, - ?string $elementUid, - ?int $fieldId, - ?int $ownerId, - bool $checkForProvisionalDraft, - string $elementType, - User $user, - int|array|null $siteId, - ?array $preferSites, - ): ?ElementInterface { - if ($elementId) { - // First check for a provisional draft, if we're open to it - if ($checkForProvisionalDraft) { - $element = $this->_elementQuery($elementType, $fieldId, $ownerId) - ->provisionalDrafts() - ->draftOf($elementId) - ->draftCreator($user) - ->siteId($siteId) - ->preferSites($preferSites) - ->unique() - ->status(null) - ->one(); - - if ($element && $this->_canSave($element, $user)) { - return $element; - } - } - - $element = $this->_elementQuery($elementType, $fieldId, $ownerId) - ->id($elementId) - ->siteId($siteId) - ->preferSites($preferSites) - ->unique() - ->drafts(null) - ->provisionalDrafts(null) - ->revisions(null) - ->status(null) - ->one(); - - if ($element) { - return $element; - } - - // finally, check for an unpublished draft - // (see https://github.com/craftcms/cms/issues/14199) - return $this->_elementQuery($elementType, $fieldId, $ownerId) - ->id($elementId) - ->siteId($siteId) - ->preferSites($preferSites) - ->unique() - ->draftOf(false) - ->status(null) - ->one(); - } - - if ($elementUid) { - $element = $this->_elementQuery($elementType, $fieldId, $ownerId) - ->uid($elementUid) - ->siteId($siteId) - ->preferSites($preferSites) - ->unique() - ->status(null) - ->one(); - - if ($element) { - return $element; - } - - // check for an unpublished draft if we got this far - // (e.g. newly added matrix "block" or where autosaveDrafts is off) - // https://github.com/craftcms/cms/issues/15985 - return $this->_elementQuery($elementType, $fieldId, $ownerId) - ->uid($elementUid) - ->siteId($siteId) - ->preferSites($preferSites) - ->unique() - ->status(null) - ->draftOf(false) - ->one(); - } - - return null; - } - - /** - * @param class-string $elementType - * @param int|null $fieldId - * @param int|null $ownerId - * @return ElementQueryInterface - */ - private function _elementQuery(string $elementType, ?int $fieldId, ?int $ownerId): ElementQueryInterface - { - $query = $elementType::find(); - if ($query instanceof NestedElementQueryInterface) { - $query - ->fieldId($fieldId) - ->ownerId($ownerId); - } - return $query; - } - - /** - * Creates a new element. - * - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - */ - private function _createElement(): ElementInterface - { - if (!$this->_elementType) { - throw new BadRequestHttpException('Request missing required body param.'); - } - - $this->_validateElementType($this->_elementType); - - /** @var ElementInterface $element */ - $element = $this->element = app()->make($this->_elementType); - if (isset($this->_siteId) && $element::isLocalized()) { - $element->siteId = $this->_siteId; - } - if (isset($this->_ownerId) && $element instanceof NestedElementInterface) { - $element->setOwnerId($this->_ownerId); - } - $element->setAttributesFromRequest($this->_attributes + array_filter(['fieldId' => $this->_fieldId])); - - Gate::authorize('save', $element); - - if (!$element->slug) { - $element->slug = ElementHelper::tempSlug(); - } - - return $element; - } - - /** - * Ensures the given element type is valid. - * - * @param class-string $elementType - * @throws BadRequestHttpException - */ - private function _validateElementType(string $elementType): void - { - if (!ComponentHelper::validateComponentClass($elementType, ElementInterface::class)) { - $message = (new InvalidTypeException($elementType, ElementInterface::class))->getMessage(); - throw new BadRequestHttpException($message); - } - } - - /** - * Applies the request params to the given element. - * - * @param ElementInterface $element - * @throws ForbiddenHttpException - */ - private function _applyParamsToElement(ElementInterface $element): void - { - if (!$this->_applyParams) { - return; - } - - if (isset($this->_enabledForSite)) { - if (is_array($this->_enabledForSite)) { - // Make sure they are allowed to edit all of the posted site IDs - $editableSiteIds = Sites::getEditableSiteIds()->all(); - if (array_diff(array_keys($this->_enabledForSite), $editableSiteIds)) { - throw new ForbiddenHttpException('User not authorized to edit element statuses for all the submitted site IDs.'); - } - - // Set the global status to true if it's enabled for *any* sites, or if already enabled. - $element->enabled = in_array(true, $this->_enabledForSite) || $element->enabled; - } - - $element->setEnabledForSite($this->_enabledForSite); - } elseif (isset($this->_enabled)) { - $element->enabled = $this->_enabled; - } - - if ($this->_fresh) { - $element->setIsFresh(); - - if ($element->getIsUnpublishedDraft()) { - $element->propagateAll = true; - } - } - - if ($element->getIsDraft()) { - /** @var ElementInterface $element */ - if (isset($this->_draftName)) { - $element->draftName = $this->_draftName; - } - if (isset($this->_notes)) { - $element->draftNotes = $this->_notes; - } - } elseif (isset($this->_notes)) { - $element->setRevisionNotes($this->_notes); - } - - if ($this->_updateSearchIndexImmediately !== null) { - $element->updateSearchIndexImmediately = $this->_updateSearchIndexImmediately; - } - - $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->ruleset->useScenario($scenario); - - // Now that the element is fully configured, make sure the user can actually view it - if (!Gate::check('view', $element)) { - throw new ForbiddenHttpException('User not authorized to edit this element.'); - } - - // Set the custom field values - $element->setFieldValuesFromRequest($this->_fieldsLocation); - } - - /** - * Returns whether an element can be saved by the given user. - * - * If the element is a provisional draft, the canonical element will be used instead. - * - * @param ElementInterface $element - * @param User $user - * @return bool - */ - private function _canSave(ElementInterface $element, User $user): bool - { - if ($element->getIsRevision()) { - return false; - } - - if ($element->isProvisionalDraft) { - $element = $element->getCanonical(true); - } - - return $user->can('save', $element); - } - - /** - * @throws Throwable - * @throws ServerErrorHttpException - */ - private function _asSuccess( - string $message, - ElementInterface $element, - array $data = [], - bool $supportsAddAnother = false, - ): Response { - /** @var Element $element */ - // Don't call asModelSuccess() here so we can avoid including custom fields in the element data - $data += [ - 'modelName' => 'element', - 'element' => $element->toArray($element->attributes()), - ]; - $response = $this->asSuccess($message, $data, $this->getPostedRedirectUrl($element), [ - 'details' => !$element->dateDeleted - ? app(ElementHtml::class)->elementChipHtml($element, ['hyperlink' => true]) - : null, - ]); - - if ($supportsAddAnother && $this->_addAnother) { - $user = static::currentUser(); - $newElement = $element->createAnother(); - - if (!$newElement || !Gate::check('save', $newElement)) { - throw new ServerErrorHttpException('Unable to create a new element.'); - } - - if (!$newElement->slug) { - $newElement->slug = ElementHelper::tempSlug(); - } - - $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)))); - } - - $url = $newElement->getCpEditUrl(); - - if ($url) { - $url = Url::urlWithParams($url, ['fresh' => 1]); - } else { - $url = Url::actionUrl('elements/edit', [ - 'draftId' => $newElement->draftId, - 'siteId' => $newElement->siteId, - 'fresh' => 1, - ]); - } - - $response->redirect($url); - } - - return $response; - } - - private function _asFailure(ElementInterface $element, string $message): ?Response - { - $data = [ - 'modelName' => 'element', - 'element' => $element->toArray($element->attributes()), - 'errors' => $element->errors()->getMessages(), - 'errorSummary' => $this->_errorSummary($element), - 'invalidNestedElementIds' => $element->getInvalidNestedElementIds(), - ]; - - return $this->asFailure($message, $data, ['element' => $element]); + $event->html = $yiiEvent->html; + }); } } diff --git a/yii2-adapter/legacy/db/Connection.php b/yii2-adapter/legacy/db/Connection.php index 8735c188cfd..245cd05b886 100644 --- a/yii2-adapter/legacy/db/Connection.php +++ b/yii2-adapter/legacy/db/Connection.php @@ -23,6 +23,7 @@ use CraftCms\Cms\Database\Events\BeforeCreateBackup; use CraftCms\Cms\Database\Events\BeforeRestoreBackup; use CraftCms\Cms\Database\Exceptions\CommandFailedException; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Env; use CraftCms\Cms\Support\Str; use CraftCms\Yii2Adapter\DatabaseConnection; @@ -34,7 +35,6 @@ use Throwable; use yii\base\Event; use yii\base\Exception; -use yii\base\NotSupportedException; use yii\db\Exception as DbException; use yii\db\Transaction; diff --git a/yii2-adapter/legacy/db/ExpressionBuilder.php b/yii2-adapter/legacy/db/ExpressionBuilder.php index c6d5c093e59..533ebe9710b 100644 --- a/yii2-adapter/legacy/db/ExpressionBuilder.php +++ b/yii2-adapter/legacy/db/ExpressionBuilder.php @@ -7,7 +7,7 @@ namespace craft\db; -use yii\base\NotSupportedException; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use yii\db\ExpressionBuilderInterface; use yii\db\ExpressionBuilderTrait; use yii\db\ExpressionInterface as BaseExpressionInterface; diff --git a/yii2-adapter/legacy/db/MigrationManager.php b/yii2-adapter/legacy/db/MigrationManager.php index 6762b516f52..158e496cf31 100644 --- a/yii2-adapter/legacy/db/MigrationManager.php +++ b/yii2-adapter/legacy/db/MigrationManager.php @@ -12,6 +12,7 @@ use craft\helpers\FileHelper; use CraftCms\Aliases\Aliases; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use Illuminate\Database\Query\Builder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -19,7 +20,6 @@ use yii\base\Component; use yii\base\Exception; use yii\base\InvalidConfigException; -use yii\base\NotSupportedException; use yii\db\MigrationInterface; use yii\di\Instance; use function CraftCms\Cms\maxPowerCaptain; diff --git a/yii2-adapter/legacy/db/Query.php b/yii2-adapter/legacy/db/Query.php index 5d0433f8178..8c0365bf457 100644 --- a/yii2-adapter/legacy/db/Query.php +++ b/yii2-adapter/legacy/db/Query.php @@ -11,6 +11,7 @@ use ArrayIterator; use craft\base\ClonefixTrait; use craft\events\DefineBehaviorsEvent; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Url; use Illuminate\Pagination\AbstractPaginator; @@ -18,7 +19,6 @@ use Illuminate\Support\Collection; use IteratorAggregate; use yii\base\Exception; -use yii\base\NotSupportedException; use yii\base\UnknownPropertyException; use yii\db\Connection as YiiConnection; diff --git a/yii2-adapter/legacy/db/mysql/QueryBuilder.php b/yii2-adapter/legacy/db/mysql/QueryBuilder.php index 613c982871e..f8cc0b871ef 100644 --- a/yii2-adapter/legacy/db/mysql/QueryBuilder.php +++ b/yii2-adapter/legacy/db/mysql/QueryBuilder.php @@ -9,9 +9,9 @@ use craft\db\Connection; use craft\helpers\Db; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Json; use Illuminate\Support\Facades\DB as DbFacade; -use yii\base\NotSupportedException; /** * @inheritdoc diff --git a/yii2-adapter/legacy/db/mysql/Schema.php b/yii2-adapter/legacy/db/mysql/Schema.php index c380903eee1..a440e8db000 100644 --- a/yii2-adapter/legacy/db/mysql/Schema.php +++ b/yii2-adapter/legacy/db/mysql/Schema.php @@ -16,6 +16,7 @@ use craft\helpers\Db; use craft\helpers\FileHelper; use CraftCms\Cms\Cms; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Str; use Illuminate\Support\Facades\Log; use InvalidArgumentException; @@ -23,7 +24,6 @@ use PDO; use PDOException; use yii\base\ErrorException; -use yii\base\NotSupportedException; use yii\db\Exception; use function CraftCms\Cms\normalizeVersion; diff --git a/yii2-adapter/legacy/elements/Address.php b/yii2-adapter/legacy/elements/Address.php index 1681c0f1264..f7d6da1c148 100644 --- a/yii2-adapter/legacy/elements/Address.php +++ b/yii2-adapter/legacy/elements/Address.php @@ -2,7 +2,10 @@ namespace craft\elements; +use CommerceGuys\Addressing\AddressFormat\AddressField; use craft\base\ElementEventConstants; +use CraftCms\Cms\Address\Addresses; +use Deprecated; /** * Address element class @@ -15,4 +18,18 @@ class Address extends \CraftCms\Cms\Address\Elements\Address { use ElementEventConstants; + + /** + * Returns an address attribute label. + */ + #[Deprecated(message: 'in 4.3.0. [[\craft\services\Addresses::getFieldLabel()]] should be used instead.')] + public static function addressAttributeLabel(string $attribute, string $countryCode): ?string + { + if (!AddressField::exists($attribute)) { + return null; + } + + /** @phpstan-var AddressField::* $attribute */ + return app(Addresses::class)->getFieldLabel($attribute, $countryCode); + } } diff --git a/yii2-adapter/legacy/elements/Asset.php b/yii2-adapter/legacy/elements/Asset.php index b87a72e71a6..e2ba267488a 100644 --- a/yii2-adapter/legacy/elements/Asset.php +++ b/yii2-adapter/legacy/elements/Asset.php @@ -14,6 +14,7 @@ use craft\events\AssetEvent; use craft\events\DefineAssetUrlEvent; use craft\events\GenerateTransformEvent; +use CraftCms\Cms\Asset\Enums\FileKind; use CraftCms\Cms\Asset\Events\AfterGenerateTransform; use CraftCms\Cms\Asset\Events\BeforeDefineAssetUrl; use CraftCms\Cms\Asset\Events\BeforeGenerateTransform; @@ -47,6 +48,66 @@ class Asset extends \CraftCms\Cms\Asset\Elements\Asset public const string SCENARIO_REPLACE = AssetRules::SCENARIO_REPLACE; + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_ACCESS = FileKind::Access->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_AUDIO = FileKind::Audio->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_CAPTIONS_SUBTITLES = FileKind::CaptionsSubtitles->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_COMPRESSED = FileKind::Compressed->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_EXCEL = FileKind::Excel->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_FLASH = FileKind::Flash->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_HTML = FileKind::Html->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_ILLUSTRATOR = FileKind::Illustrator->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_IMAGE = FileKind::Image->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_JAVASCRIPT = FileKind::Javascript->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_JSON = FileKind::Json->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_PDF = FileKind::Pdf->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_PHOTOSHOP = FileKind::Photoshop->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_PHP = FileKind::Php->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_POWERPOINT = FileKind::Powerpoint->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_TEXT = FileKind::Text->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_VIDEO = FileKind::Video->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_WORD = FileKind::Word->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_XML = FileKind::Xml->value; + + /** @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Enums\FileKind} instead. */ + public const string KIND_UNKNOWN = FileKind::Unknown->value; + // Events // ------------------------------------------------------------------------- diff --git a/yii2-adapter/legacy/elements/Category.php b/yii2-adapter/legacy/elements/Category.php index eee3c9ae08c..174bdaf9a3c 100644 --- a/yii2-adapter/legacy/elements/Category.php +++ b/yii2-adapter/legacy/elements/Category.php @@ -8,7 +8,7 @@ namespace craft\elements; use Craft; -use craft\controllers\ElementIndexesController; +use craft\base\LegacyEventConstants; use craft\db\Table; use craft\elements\actions\Delete; use craft\elements\actions\Duplicate; @@ -23,6 +23,7 @@ use CraftCms\Cms\Cp\FormFields; use CraftCms\Cms\Cp\Html\ElementHtml; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface; +use CraftCms\Cms\Element\CurrentElementIndex; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\FieldLayout\FieldLayout; @@ -35,6 +36,8 @@ use CraftCms\Cms\Support\Str; use CraftCms\Cms\Support\Url; use CraftCms\Cms\User\Elements\User; +use CraftCms\RulesetValidation\Attributes\Ruleset; +use CraftCms\Yii2Adapter\Validation\LegacyElementRules; use GraphQL\Type\Definition\Type; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; @@ -51,8 +54,11 @@ * @since 3.0.0 * @deprecated in 6.0.0 */ +#[Ruleset(LegacyElementRules::class)] class Category extends Element { + use LegacyEventConstants; + /** * @inheritdoc */ @@ -236,13 +242,10 @@ protected static function defineFieldLayouts(?string $source): array protected static function defineActions(string $source): array { // Get the selected site - $controller = Craft::$app->controller; - if ($controller instanceof ElementIndexesController) { - /** @var ElementQueryInterface $elementQuery */ - $elementQuery = $controller->getElementQuery(); - } else { - $elementQuery = null; - } + $elementQuery = app(CurrentElementIndex::class)->isActive() + ? app(CurrentElementIndex::class)->query() + : null; + $site = $elementQuery && $elementQuery->siteId ? Sites::getSiteById($elementQuery->siteId) : Sites::getCurrentSite(); @@ -408,7 +411,7 @@ public function extraFields(): array */ protected function defineRules(): array { - $rules = parent::defineRules(); + $rules = []; $rules[] = [['groupId'], 'number', 'integerOnly' => true]; return $rules; } diff --git a/yii2-adapter/legacy/elements/GlobalSet.php b/yii2-adapter/legacy/elements/GlobalSet.php index 60398d66836..a8b7f29c6e9 100644 --- a/yii2-adapter/legacy/elements/GlobalSet.php +++ b/yii2-adapter/legacy/elements/GlobalSet.php @@ -7,6 +7,7 @@ namespace craft\elements; +use craft\base\LegacyEventConstants; use craft\behaviors\FieldLayoutBehavior; use craft\elements\db\GlobalSetQuery; use craft\records\GlobalSet as GlobalSetRecord; @@ -19,6 +20,8 @@ use CraftCms\Cms\FieldLayout\FieldLayout; use CraftCms\Cms\Support\Url; use CraftCms\Cms\User\Elements\User; +use CraftCms\RulesetValidation\Attributes\Ruleset; +use CraftCms\Yii2Adapter\Validation\LegacyElementRules; use Illuminate\Support\Facades\Log; use yii\base\InvalidConfigException; use function CraftCms\Cms\t; @@ -31,8 +34,11 @@ * @since 3.0.0 * @deprecated in 6.0.0 */ +#[Ruleset(LegacyElementRules::class)] class GlobalSet extends Element implements FieldLayoutProviderInterface { + use LegacyEventConstants; + /** * @since 4.4.6 */ @@ -189,7 +195,7 @@ public function __toString(): string */ protected function defineBehaviors(): array { - $behaviors = parent::defineBehaviors(); + $behaviors = []; $behaviors['fieldLayout'] = [ 'class' => FieldLayoutBehavior::class, 'elementType' => self::class, @@ -213,7 +219,7 @@ public function attributeLabels(): array */ protected function defineRules(): array { - $rules = parent::defineRules(); + $rules = []; $rules[] = [['fieldLayoutId'], 'number', 'integerOnly' => true]; $rules[] = [['name', 'handle'], 'string', 'max' => 255]; $rules[] = [['name', 'handle'], 'required']; @@ -242,17 +248,6 @@ protected function defineRules(): array return $rules; } - /** - * @inheritdoc - */ - public function scenarios(): array - { - $scenarios = parent::scenarios(); - $scenarios[self::SCENARIO_SAVE_SET] = $scenarios[self::SCENARIO_DEFAULT]; - - return $scenarios; - } - /** * @inheritdoc */ diff --git a/yii2-adapter/legacy/elements/Tag.php b/yii2-adapter/legacy/elements/Tag.php index d9954333ea9..d6b4bc9ede3 100644 --- a/yii2-adapter/legacy/elements/Tag.php +++ b/yii2-adapter/legacy/elements/Tag.php @@ -8,6 +8,7 @@ namespace craft\elements; use Craft; +use craft\base\LegacyEventConstants; use craft\elements\conditions\tags\TagCondition; use craft\elements\db\TagQuery; use craft\gql\interfaces\elements\Tag as TagInterface; @@ -19,6 +20,8 @@ use CraftCms\Cms\Element\Validation\ElementRules; use CraftCms\Cms\FieldLayout\FieldLayout; use CraftCms\Cms\User\Elements\User; +use CraftCms\RulesetValidation\Attributes\Ruleset; +use CraftCms\Yii2Adapter\Validation\LegacyElementRules; use GraphQL\Type\Definition\Type; use yii\base\InvalidConfigException; use yii\validators\InlineValidator; @@ -32,8 +35,11 @@ * @since 3.0.0 * @deprecated in 6.0.0 */ +#[Ruleset(LegacyElementRules::class)] class Tag extends Element { + use LegacyEventConstants; + /** * @inheritdoc */ @@ -208,7 +214,7 @@ public function extraFields(): array */ protected function defineRules(): array { - $rules = parent::defineRules(); + $rules = []; $rules[] = [['groupId'], 'number', 'integerOnly' => true]; $rules[] = [ ['title'], @@ -239,7 +245,7 @@ public function validateTitle(string $attribute, ?array $params, InlineValidator } if ($query->exists()) { - $validator->addError($this, $attribute, t('{attribute} "{value}" has already been taken.')); + $this->addError($attribute, t('{attribute} "{value}" has already been taken.')); } } diff --git a/yii2-adapter/legacy/elements/User.php b/yii2-adapter/legacy/elements/User.php index aeec061e31d..f6b4881bb1c 100644 --- a/yii2-adapter/legacy/elements/User.php +++ b/yii2-adapter/legacy/elements/User.php @@ -15,10 +15,12 @@ use craft\events\DefineValueEvent; use CraftCms\Cms\Auth\Events\Authenticating; use CraftCms\Cms\Element\Validation\ElementRules; +use CraftCms\Cms\Twig\Attributes\AllowedInSandbox; 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 Deprecated; use Illuminate\Support\Facades\Event; /** @@ -62,6 +64,16 @@ class User extends UserElement */ public const string EVENT_BEFORE_AUTHENTICATE = 'beforeAuthenticate'; + /** + * Returns the user’s full name. + */ + #[Deprecated(message: 'in 4.0.0. [[fullName]] should be used instead.')] + #[AllowedInSandbox] + public function getFullName(): ?string + { + return $this->fullName; + } + public static function registerEvents(): void { Event::listen(function(DefineName $event) { diff --git a/yii2-adapter/legacy/elements/conditions/categories/GroupConditionRule.php b/yii2-adapter/legacy/elements/conditions/categories/GroupConditionRule.php index 060320d2f6e..455f2a27af2 100644 --- a/yii2-adapter/legacy/elements/conditions/categories/GroupConditionRule.php +++ b/yii2-adapter/legacy/elements/conditions/categories/GroupConditionRule.php @@ -4,10 +4,10 @@ use Craft; use craft\base\conditions\BaseMultiSelectConditionRule; -use craft\base\ElementInterface; use craft\elements\Category; use craft\elements\db\CategoryQuery; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Support\Arr; use function CraftCms\Cms\t; diff --git a/yii2-adapter/legacy/elements/conditions/tags/GroupConditionRule.php b/yii2-adapter/legacy/elements/conditions/tags/GroupConditionRule.php index 017b6321a50..4e2656c5132 100644 --- a/yii2-adapter/legacy/elements/conditions/tags/GroupConditionRule.php +++ b/yii2-adapter/legacy/elements/conditions/tags/GroupConditionRule.php @@ -4,10 +4,10 @@ use Craft; use craft\base\conditions\BaseMultiSelectConditionRule; -use craft\base\ElementInterface; use craft\elements\db\TagQuery; use craft\elements\Tag; use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionRuleInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Support\Arr; use function CraftCms\Cms\t; diff --git a/yii2-adapter/legacy/elements/db/AssetQuery.php b/yii2-adapter/legacy/elements/db/AssetQuery.php index 48669976ba2..7f3446a480e 100644 --- a/yii2-adapter/legacy/elements/db/AssetQuery.php +++ b/yii2-adapter/legacy/elements/db/AssetQuery.php @@ -8,7 +8,6 @@ namespace craft\elements\db; use Craft; -use craft\base\ElementInterface; use craft\db\Query; use craft\db\QueryAbortedException; use craft\db\Table; @@ -19,6 +18,7 @@ use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Asset\Folders; use CraftCms\Cms\Asset\Volumes; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\ImageTransforms; use CraftCms\Cms\User\Elements\User; diff --git a/yii2-adapter/legacy/elements/db/ElementQuery.php b/yii2-adapter/legacy/elements/db/ElementQuery.php index 14234639f1f..ef27de8ae1f 100644 --- a/yii2-adapter/legacy/elements/db/ElementQuery.php +++ b/yii2-adapter/legacy/elements/db/ElementQuery.php @@ -9,7 +9,6 @@ use Closure; use Craft; -use craft\base\ElementInterface; use craft\behaviors\CustomFieldBehavior; use craft\cache\ElementQueryTagDependency; use craft\db\CoalesceColumnsExpression; @@ -25,6 +24,7 @@ use craft\helpers\Db; use CraftCms\Cms\Cms; use CraftCms\Cms\Database\QueryParam; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Contracts\ExpirableElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\Element; @@ -33,6 +33,7 @@ use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; use CraftCms\Cms\Field\Fields; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Site\Data\Site; use CraftCms\Cms\Site\Exceptions\SiteNotFoundException; use CraftCms\Cms\Support\Arr; @@ -56,7 +57,6 @@ use yii\base\Exception; use yii\base\InvalidConfigException; use yii\base\InvalidValueException; -use yii\base\NotSupportedException; use yii\db\Connection as YiiConnection; use yii\db\Expression; use yii\db\ExpressionInterface; @@ -4027,4 +4027,9 @@ public function getCountForPagination($columns = ['*']) ->select(new Expression('1')) ->count(); } + + public function get(): Collection + { + return collect($this->all()); + } } diff --git a/yii2-adapter/legacy/elements/db/ElementRelationParamParser.php b/yii2-adapter/legacy/elements/db/ElementRelationParamParser.php index 5e8ca485017..232c9e57307 100644 --- a/yii2-adapter/legacy/elements/db/ElementRelationParamParser.php +++ b/yii2-adapter/legacy/elements/db/ElementRelationParamParser.php @@ -7,10 +7,10 @@ namespace craft\elements\db; -use craft\base\ElementInterface; use craft\db\Query; use craft\db\Table; use CraftCms\Cms\Database\ElementRelationParamFilter; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Field\BaseRelationField; use CraftCms\Cms\Field\Contracts\FieldInterface; diff --git a/yii2-adapter/legacy/elements/db/NestedElementQueryTrait.php b/yii2-adapter/legacy/elements/db/NestedElementQueryTrait.php index 2abede577e9..c69ce467d31 100644 --- a/yii2-adapter/legacy/elements/db/NestedElementQueryTrait.php +++ b/yii2-adapter/legacy/elements/db/NestedElementQueryTrait.php @@ -7,11 +7,11 @@ namespace craft\elements\db; -use craft\base\ElementInterface; use craft\db\Query; use craft\db\QueryAbortedException; use craft\db\Table; use craft\helpers\Db; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface; use CraftCms\Cms\Field\Fields; use CraftCms\Cms\Support\Arr; diff --git a/yii2-adapter/legacy/enums/ElementIndexViewMode.php b/yii2-adapter/legacy/enums/ElementIndexViewMode.php index 2d75384391f..3311d46016a 100644 --- a/yii2-adapter/legacy/enums/ElementIndexViewMode.php +++ b/yii2-adapter/legacy/enums/ElementIndexViewMode.php @@ -6,11 +6,11 @@ if (false) { /** * @since 5.0.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\Field\Enums\ElementIndexViewMode} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\Element\Enums\ElementIndexViewMode} instead. */ enum ElementIndexViewMode: string { } } -class_alias(\CraftCms\Cms\Field\Enums\ElementIndexViewMode::class, ElementIndexViewMode::class); +class_alias(\CraftCms\Cms\Element\Enums\ElementIndexViewMode::class, ElementIndexViewMode::class); diff --git a/yii2-adapter/legacy/events/AuthorizationCheckEvent.php b/yii2-adapter/legacy/events/AuthorizationCheckEvent.php index fae046134fe..96f3d4a19d6 100644 --- a/yii2-adapter/legacy/events/AuthorizationCheckEvent.php +++ b/yii2-adapter/legacy/events/AuthorizationCheckEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\User\Elements\User; /** diff --git a/yii2-adapter/legacy/events/BulkElementsEvent.php b/yii2-adapter/legacy/events/BulkElementsEvent.php index 483be0039e8..fefe1d220d9 100644 --- a/yii2-adapter/legacy/events/BulkElementsEvent.php +++ b/yii2-adapter/legacy/events/BulkElementsEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * Bulk element event diff --git a/yii2-adapter/legacy/events/CreateFieldLayoutFormEvent.php b/yii2-adapter/legacy/events/CreateFieldLayoutFormEvent.php index 123a110222f..f988638a047 100644 --- a/yii2-adapter/legacy/events/CreateFieldLayoutFormEvent.php +++ b/yii2-adapter/legacy/events/CreateFieldLayoutFormEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\FieldLayoutForm; use CraftCms\Cms\FieldLayout\FieldLayoutTab; diff --git a/yii2-adapter/legacy/events/DefineEagerLoadingMapEvent.php b/yii2-adapter/legacy/events/DefineEagerLoadingMapEvent.php index d597e6fcedf..8f1ac31bec5 100644 --- a/yii2-adapter/legacy/events/DefineEagerLoadingMapEvent.php +++ b/yii2-adapter/legacy/events/DefineEagerLoadingMapEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * DefineEagerLoadingMapEvent class. diff --git a/yii2-adapter/legacy/events/DefineElementEditorHtmlEvent.php b/yii2-adapter/legacy/events/DefineElementEditorHtmlEvent.php index 68b236f7229..fb9bbbac889 100644 --- a/yii2-adapter/legacy/events/DefineElementEditorHtmlEvent.php +++ b/yii2-adapter/legacy/events/DefineElementEditorHtmlEvent.php @@ -7,7 +7,7 @@ namespace craft\events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * DefineElementEditorHtmlEvent is used to define the HTML for an element editor. diff --git a/yii2-adapter/legacy/events/DefineEntryTypesForFieldEvent.php b/yii2-adapter/legacy/events/DefineEntryTypesForFieldEvent.php index 54bc9031fe4..89e557d9b71 100644 --- a/yii2-adapter/legacy/events/DefineEntryTypesForFieldEvent.php +++ b/yii2-adapter/legacy/events/DefineEntryTypesForFieldEvent.php @@ -7,7 +7,7 @@ namespace craft\events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Elements\Entry; /** diff --git a/yii2-adapter/legacy/events/DefineFieldActionsEvent.php b/yii2-adapter/legacy/events/DefineFieldActionsEvent.php index a42d409a195..3ca6dc069b7 100644 --- a/yii2-adapter/legacy/events/DefineFieldActionsEvent.php +++ b/yii2-adapter/legacy/events/DefineFieldActionsEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * DefineFieldActionsEvent event class. diff --git a/yii2-adapter/legacy/events/DefineFieldHtmlEvent.php b/yii2-adapter/legacy/events/DefineFieldHtmlEvent.php index a9ba31246c4..2b7c86bc4e9 100644 --- a/yii2-adapter/legacy/events/DefineFieldHtmlEvent.php +++ b/yii2-adapter/legacy/events/DefineFieldHtmlEvent.php @@ -7,7 +7,7 @@ namespace craft\events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * DefineFieldHtmlEvent is used to define the HTML for a field input. diff --git a/yii2-adapter/legacy/events/DefineFieldKeywordsEvent.php b/yii2-adapter/legacy/events/DefineFieldKeywordsEvent.php index b13d779cf7c..9fc89297a52 100644 --- a/yii2-adapter/legacy/events/DefineFieldKeywordsEvent.php +++ b/yii2-adapter/legacy/events/DefineFieldKeywordsEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * DefineFieldKeywordsEvent class. diff --git a/yii2-adapter/legacy/events/DefineInputOptionsEvent.php b/yii2-adapter/legacy/events/DefineInputOptionsEvent.php index 1747aa2540c..56e327c74cf 100644 --- a/yii2-adapter/legacy/events/DefineInputOptionsEvent.php +++ b/yii2-adapter/legacy/events/DefineInputOptionsEvent.php @@ -2,8 +2,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * DefineInputOptionsEvent class. diff --git a/yii2-adapter/legacy/events/DefineMetaFields.php b/yii2-adapter/legacy/events/DefineMetaFields.php index 2dbe1af2845..ef6247ea9fb 100644 --- a/yii2-adapter/legacy/events/DefineMetaFields.php +++ b/yii2-adapter/legacy/events/DefineMetaFields.php @@ -8,8 +8,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * class DefineEntryMetaFields diff --git a/yii2-adapter/legacy/events/DefineShowFieldLayoutComponentInFormEvent.php b/yii2-adapter/legacy/events/DefineShowFieldLayoutComponentInFormEvent.php index 1ce920703fa..cff09b7fada 100644 --- a/yii2-adapter/legacy/events/DefineShowFieldLayoutComponentInFormEvent.php +++ b/yii2-adapter/legacy/events/DefineShowFieldLayoutComponentInFormEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\FieldLayout; /** diff --git a/yii2-adapter/legacy/events/DefineSourceSortOptionsEvent.php b/yii2-adapter/legacy/events/DefineSourceSortOptionsEvent.php index 64cb0544bd2..cefa780c807 100644 --- a/yii2-adapter/legacy/events/DefineSourceSortOptionsEvent.php +++ b/yii2-adapter/legacy/events/DefineSourceSortOptionsEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * DefineSourceSortOptionEvent class. diff --git a/yii2-adapter/legacy/events/DefineSourceTableAttributesEvent.php b/yii2-adapter/legacy/events/DefineSourceTableAttributesEvent.php index 381fdd996e8..f9c97884390 100644 --- a/yii2-adapter/legacy/events/DefineSourceTableAttributesEvent.php +++ b/yii2-adapter/legacy/events/DefineSourceTableAttributesEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * DefineSourceTableAttributesEvent class. @@ -39,9 +39,9 @@ class DefineSourceTableAttributesEvent extends Event * - `icon` _(optional)_ – The name of the icon that should be shown instead of a textual label (e.g. `'world'`) * * The first item in the array will determine the first table column’s header (and which - * [[\craft\base\ElementInterface::sortOptions()|sort option]] it should be mapped to, if any), however it + * [[\CraftCms\Cms\Element\Contracts\ElementInterface::sortOptions()|sort option]] it should be mapped to, if any), however it * doesn’t have any effect on the table body, because the first column is reserved for displaying whatever - * the elements’ [[\craft\base\ElementInterface::getUiLabel()|getUiLabel()]] methods return. + * the elements’ [[\CraftCms\Cms\Element\Contracts\ElementInterface::getUiLabel()|getUiLabel()]] methods return. */ public array $attributes = []; } diff --git a/yii2-adapter/legacy/events/DraftEvent.php b/yii2-adapter/legacy/events/DraftEvent.php index cba135abb07..b1e5379c6ad 100644 --- a/yii2-adapter/legacy/events/DraftEvent.php +++ b/yii2-adapter/legacy/events/DraftEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * Draft event class. diff --git a/yii2-adapter/legacy/events/DuplicateNestedElementsEvent.php b/yii2-adapter/legacy/events/DuplicateNestedElementsEvent.php index 678fdf572d5..29745ebb284 100644 --- a/yii2-adapter/legacy/events/DuplicateNestedElementsEvent.php +++ b/yii2-adapter/legacy/events/DuplicateNestedElementsEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * Duplicate nested elements event diff --git a/yii2-adapter/legacy/events/EagerLoadElementsEvent.php b/yii2-adapter/legacy/events/EagerLoadElementsEvent.php index a412fcbaf89..df26115a51a 100644 --- a/yii2-adapter/legacy/events/EagerLoadElementsEvent.php +++ b/yii2-adapter/legacy/events/EagerLoadElementsEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Data\EagerLoadPlan; /** diff --git a/yii2-adapter/legacy/events/ElementContentEvent.php b/yii2-adapter/legacy/events/ElementContentEvent.php index 631268e9752..b7ff5d78be1 100644 --- a/yii2-adapter/legacy/events/ElementContentEvent.php +++ b/yii2-adapter/legacy/events/ElementContentEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * Element content event class. diff --git a/yii2-adapter/legacy/events/ElementEvent.php b/yii2-adapter/legacy/events/ElementEvent.php index 8f4b5e3dc7e..6c4b960e27e 100644 --- a/yii2-adapter/legacy/events/ElementEvent.php +++ b/yii2-adapter/legacy/events/ElementEvent.php @@ -7,7 +7,7 @@ namespace craft\events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * Element event class. diff --git a/yii2-adapter/legacy/events/FieldElementEvent.php b/yii2-adapter/legacy/events/FieldElementEvent.php index 30f00f9258c..4f1382bdd44 100644 --- a/yii2-adapter/legacy/events/FieldElementEvent.php +++ b/yii2-adapter/legacy/events/FieldElementEvent.php @@ -7,7 +7,7 @@ namespace craft\events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * FieldElementEvent class. diff --git a/yii2-adapter/legacy/events/IndexKeywordsEvent.php b/yii2-adapter/legacy/events/IndexKeywordsEvent.php index a94f4f235e1..bdee5a2b9ac 100644 --- a/yii2-adapter/legacy/events/IndexKeywordsEvent.php +++ b/yii2-adapter/legacy/events/IndexKeywordsEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * Index keywords event class. diff --git a/yii2-adapter/legacy/events/InvalidateElementCachesEvent.php b/yii2-adapter/legacy/events/InvalidateElementCachesEvent.php index a1b26363e8f..e38bfd7c338 100644 --- a/yii2-adapter/legacy/events/InvalidateElementCachesEvent.php +++ b/yii2-adapter/legacy/events/InvalidateElementCachesEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * InvalidateElementCachesEvent class. diff --git a/yii2-adapter/legacy/events/LocateUploadedFilesEvent.php b/yii2-adapter/legacy/events/LocateUploadedFilesEvent.php index bc2ff1be0dd..5a8831dd2ee 100644 --- a/yii2-adapter/legacy/events/LocateUploadedFilesEvent.php +++ b/yii2-adapter/legacy/events/LocateUploadedFilesEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * LocateUploadedFilesEvent event class. diff --git a/yii2-adapter/legacy/events/MoveElementEvent.php b/yii2-adapter/legacy/events/MoveElementEvent.php index 82e15d83307..46b9e5d9536 100644 --- a/yii2-adapter/legacy/events/MoveElementEvent.php +++ b/yii2-adapter/legacy/events/MoveElementEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\services\Structures; +use CraftCms\Cms\Element\Contracts\ElementInterface; use yii\base\InvalidConfigException; /** diff --git a/yii2-adapter/legacy/events/MultiElementActionEvent.php b/yii2-adapter/legacy/events/MultiElementActionEvent.php index 82f6ea0090d..00e3b13d979 100644 --- a/yii2-adapter/legacy/events/MultiElementActionEvent.php +++ b/yii2-adapter/legacy/events/MultiElementActionEvent.php @@ -7,7 +7,7 @@ namespace craft\events; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use Throwable; /** diff --git a/yii2-adapter/legacy/events/PopulateElementsEvent.php b/yii2-adapter/legacy/events/PopulateElementsEvent.php index 3682dd948f8..f14ba685150 100644 --- a/yii2-adapter/legacy/events/PopulateElementsEvent.php +++ b/yii2-adapter/legacy/events/PopulateElementsEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * PopulateElementsEvent event class. diff --git a/yii2-adapter/legacy/events/RevisionEvent.php b/yii2-adapter/legacy/events/RevisionEvent.php index eef4e790fd8..7e8be706c1c 100644 --- a/yii2-adapter/legacy/events/RevisionEvent.php +++ b/yii2-adapter/legacy/events/RevisionEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * Revision event class. diff --git a/yii2-adapter/legacy/events/SetEagerLoadedElementsEvent.php b/yii2-adapter/legacy/events/SetEagerLoadedElementsEvent.php index dd28bb446aa..03d60ba186d 100644 --- a/yii2-adapter/legacy/events/SetEagerLoadedElementsEvent.php +++ b/yii2-adapter/legacy/events/SetEagerLoadedElementsEvent.php @@ -7,8 +7,8 @@ namespace craft\events; -use craft\base\ElementInterface; use craft\base\Event; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Data\EagerLoadPlan; /** diff --git a/yii2-adapter/legacy/fieldlayoutelements/addresses/AddressField.php b/yii2-adapter/legacy/fieldlayoutelements/addresses/AddressField.php index 74fefb9adc6..f266e166b63 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/addresses/AddressField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/addresses/AddressField.php @@ -8,14 +8,16 @@ namespace craft\fieldlayoutelements\addresses; +use craft\base\LegacyEventConstants; + /** * AddressField represents an Address field that can be included within an Address field layout designer. * * @author Pixel & Tonic, Inc. * @since 4.0.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\addresses\AddressField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Addresses\AddressField} instead. */ -class AddressField extends \CraftCms\Cms\FieldLayout\LayoutElements\addresses\AddressField +class AddressField extends \CraftCms\Cms\FieldLayout\LayoutElements\Addresses\AddressField { - use \craft\base\LegacyEventConstants; + use LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/addresses/CountryCodeField.php b/yii2-adapter/legacy/fieldlayoutelements/addresses/CountryCodeField.php index 1caf2da68cd..8b2e1915413 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/addresses/CountryCodeField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/addresses/CountryCodeField.php @@ -9,15 +9,17 @@ namespace craft\fieldlayoutelements\addresses; +use craft\base\LegacyEventConstants; + /** * Class CountryCodeField. * * @author Pixel & Tonic, Inc. * * @since 4.0.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\addresses\CountryCodeField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Addresses\CountryCodeField} instead. */ -class CountryCodeField extends \CraftCms\Cms\FieldLayout\LayoutElements\addresses\CountryCodeField +class CountryCodeField extends \CraftCms\Cms\FieldLayout\LayoutElements\Addresses\CountryCodeField { - use \craft\base\LegacyEventConstants; + use LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/addresses/LabelField.php b/yii2-adapter/legacy/fieldlayoutelements/addresses/LabelField.php index 7a6e15990fc..941502ae85c 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/addresses/LabelField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/addresses/LabelField.php @@ -9,15 +9,17 @@ namespace craft\fieldlayoutelements\addresses; +use craft\base\LegacyEventConstants; + /** * Class LabelField. * * @author Pixel & Tonic, Inc. * * @since 4.0.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\addresses\LabelField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Addresses\LabelField} instead. */ -class LabelField extends \CraftCms\Cms\FieldLayout\LayoutElements\addresses\LabelField +class LabelField extends \CraftCms\Cms\FieldLayout\LayoutElements\Addresses\LabelField { - use \craft\base\LegacyEventConstants; + use LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/addresses/LatLongField.php b/yii2-adapter/legacy/fieldlayoutelements/addresses/LatLongField.php index 95a80df88af..6e060617df3 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/addresses/LatLongField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/addresses/LatLongField.php @@ -9,15 +9,17 @@ namespace craft\fieldlayoutelements\addresses; +use craft\base\LegacyEventConstants; + /** * Class LatLongField. * * @author Pixel & Tonic, Inc. * * @since 4.0.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\addresses\LatLongField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Addresses\LatLongField} instead. */ -class LatLongField extends \CraftCms\Cms\FieldLayout\LayoutElements\addresses\LatLongField +class LatLongField extends \CraftCms\Cms\FieldLayout\LayoutElements\Addresses\LatLongField { - use \craft\base\LegacyEventConstants; + use LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/addresses/OrganizationField.php b/yii2-adapter/legacy/fieldlayoutelements/addresses/OrganizationField.php index 6c4b7506a1d..1e43ff699bf 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/addresses/OrganizationField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/addresses/OrganizationField.php @@ -9,15 +9,17 @@ namespace craft\fieldlayoutelements\addresses; +use craft\base\LegacyEventConstants; + /** * Class OrganizationField. * * @author Pixel & Tonic, Inc. * * @since 4.0.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\addresses\OrganizationField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Addresses\OrganizationField} instead. */ -class OrganizationField extends \CraftCms\Cms\FieldLayout\LayoutElements\addresses\OrganizationField +class OrganizationField extends \CraftCms\Cms\FieldLayout\LayoutElements\Addresses\OrganizationField { - use \craft\base\LegacyEventConstants; + use LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/addresses/OrganizationTaxIdField.php b/yii2-adapter/legacy/fieldlayoutelements/addresses/OrganizationTaxIdField.php index 7f4f9a5fcb0..13a236ef404 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/addresses/OrganizationTaxIdField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/addresses/OrganizationTaxIdField.php @@ -9,15 +9,17 @@ namespace craft\fieldlayoutelements\addresses; +use craft\base\LegacyEventConstants; + /** * Class OrganizationTaxIdField. * * @author Pixel & Tonic, Inc. * * @since 4.0.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\addresses\OrganizationTaxIdField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Addresses\OrganizationTaxIdField} instead. */ -class OrganizationTaxIdField extends \CraftCms\Cms\FieldLayout\LayoutElements\addresses\OrganizationTaxIdField +class OrganizationTaxIdField extends \CraftCms\Cms\FieldLayout\LayoutElements\Addresses\OrganizationTaxIdField { - use \craft\base\LegacyEventConstants; + use LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/assets/AltField.php b/yii2-adapter/legacy/fieldlayoutelements/assets/AltField.php index 8c052de3209..cbf74f17606 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/assets/AltField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/assets/AltField.php @@ -11,15 +11,17 @@ namespace craft\fieldlayoutelements\assets; +use craft\base\LegacyEventConstants; + /** * AltField represents an Alternative Text field that can be included within a volume's field layout designer. * * @author Pixel & Tonic, Inc. * * @since 4.0.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\assets\AltField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Assets\AltField} instead. */ -class AltField extends \CraftCms\Cms\FieldLayout\LayoutElements\assets\AltField +class AltField extends \CraftCms\Cms\FieldLayout\LayoutElements\Assets\AltField { - use \craft\base\LegacyEventConstants; + use LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/assets/AssetTitleField.php b/yii2-adapter/legacy/fieldlayoutelements/assets/AssetTitleField.php index 91b1b9eb37d..b16e4978422 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/assets/AssetTitleField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/assets/AssetTitleField.php @@ -11,15 +11,17 @@ namespace craft\fieldlayoutelements\assets; +use craft\base\LegacyEventConstants; + /** * AssetTitleField represents a Title field that can be included within a volume's field layout designer. * * @author Pixel & Tonic, Inc. * * @since 3.6.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\assets\AssetTitleField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Assets\AssetTitleField} instead. */ -class AssetTitleField extends \CraftCms\Cms\FieldLayout\LayoutElements\assets\AssetTitleField +class AssetTitleField extends \CraftCms\Cms\FieldLayout\LayoutElements\Assets\AssetTitleField { - use \craft\base\LegacyEventConstants; + use LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/entries/EntryTitleField.php b/yii2-adapter/legacy/fieldlayoutelements/entries/EntryTitleField.php index 17353adc2f6..e3a5bb861bb 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/entries/EntryTitleField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/entries/EntryTitleField.php @@ -11,15 +11,17 @@ namespace craft\fieldlayoutelements\entries; +use craft\base\LegacyEventConstants; + /** * EntryTitleField represents a Title field that can be included within an entry type's field layout designer. * * @author Pixel & Tonic, Inc. * * @since 3.5.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\entries\EntryTitleField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Entries\EntryTitleField} instead. */ -class EntryTitleField extends \CraftCms\Cms\FieldLayout\LayoutElements\entries\EntryTitleField +class EntryTitleField extends \CraftCms\Cms\FieldLayout\LayoutElements\Entries\EntryTitleField { - use \craft\base\LegacyEventConstants; + use LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/users/AffiliatedSiteField.php b/yii2-adapter/legacy/fieldlayoutelements/users/AffiliatedSiteField.php index f1dfd240584..e5fbe48296c 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/users/AffiliatedSiteField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/users/AffiliatedSiteField.php @@ -15,9 +15,9 @@ * @author Pixel & Tonic, Inc. * * @since 5.6.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\users\AffiliatedSiteField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Users\AffiliatedSiteField} instead. */ -class AffiliatedSiteField extends \CraftCms\Cms\FieldLayout\LayoutElements\users\AffiliatedSiteField +class AffiliatedSiteField extends \CraftCms\Cms\FieldLayout\LayoutElements\Users\AffiliatedSiteField { use \craft\base\LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/users/EmailField.php b/yii2-adapter/legacy/fieldlayoutelements/users/EmailField.php index ed310be3a8f..4dbe857f16d 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/users/EmailField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/users/EmailField.php @@ -15,9 +15,9 @@ * @author Pixel & Tonic, Inc. * * @since 5.0.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\users\EmailField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Users\EmailField} instead. */ -class EmailField extends \CraftCms\Cms\FieldLayout\LayoutElements\users\EmailField +class EmailField extends \CraftCms\Cms\FieldLayout\LayoutElements\Users\EmailField { use \craft\base\LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/users/FullNameField.php b/yii2-adapter/legacy/fieldlayoutelements/users/FullNameField.php index 6d614d2ac3b..70c5781f68f 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/users/FullNameField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/users/FullNameField.php @@ -15,9 +15,9 @@ * @author Pixel & Tonic, Inc. * * @since 5.0.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\users\FullNameField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Users\FullNameField} instead. */ -class FullNameField extends \CraftCms\Cms\FieldLayout\LayoutElements\users\FullNameField +class FullNameField extends \CraftCms\Cms\FieldLayout\LayoutElements\Users\FullNameField { use \craft\base\LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/users/PhotoField.php b/yii2-adapter/legacy/fieldlayoutelements/users/PhotoField.php index 885bb91431b..4b7bdd961f0 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/users/PhotoField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/users/PhotoField.php @@ -15,9 +15,9 @@ * @author Pixel & Tonic, Inc. * * @since 5.0.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\users\PhotoField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Users\PhotoField} instead. */ -class PhotoField extends \CraftCms\Cms\FieldLayout\LayoutElements\users\PhotoField +class PhotoField extends \CraftCms\Cms\FieldLayout\LayoutElements\Users\PhotoField { use \craft\base\LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fieldlayoutelements/users/UsernameField.php b/yii2-adapter/legacy/fieldlayoutelements/users/UsernameField.php index 3ded4962830..d72f6ba2cdd 100644 --- a/yii2-adapter/legacy/fieldlayoutelements/users/UsernameField.php +++ b/yii2-adapter/legacy/fieldlayoutelements/users/UsernameField.php @@ -15,9 +15,9 @@ * @author Pixel & Tonic, Inc. * * @since 5.0.0 - * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\users\UsernameField} instead. + * @deprecated 6.0.0 use {@see \CraftCms\Cms\FieldLayout\LayoutElements\Users\UsernameField} instead. */ -class UsernameField extends \CraftCms\Cms\FieldLayout\LayoutElements\users\UsernameField +class UsernameField extends \CraftCms\Cms\FieldLayout\LayoutElements\Users\UsernameField { use \craft\base\LegacyEventConstants; } diff --git a/yii2-adapter/legacy/fields/BaseRelationField.php b/yii2-adapter/legacy/fields/BaseRelationField.php index a4d8b9191ee..25146a8bbb0 100644 --- a/yii2-adapter/legacy/fields/BaseRelationField.php +++ b/yii2-adapter/legacy/fields/BaseRelationField.php @@ -8,13 +8,13 @@ namespace craft\fields; use Craft; -use craft\base\ElementInterface; use craft\behaviors\EventBehavior; use craft\db\FixedOrderExpression; use craft\db\Table as DbTable; use craft\elements\db\ElementQuery; use craft\elements\db\OrderByPlaceholderExpression; use craft\events\CancelableEvent; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\ElementSources; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; diff --git a/yii2-adapter/legacy/fields/Categories.php b/yii2-adapter/legacy/fields/Categories.php index 9d251e0cbaa..fdb99aa27b3 100644 --- a/yii2-adapter/legacy/fields/Categories.php +++ b/yii2-adapter/legacy/fields/Categories.php @@ -5,7 +5,6 @@ namespace craft\fields; use Craft; -use craft\base\ElementInterface; use craft\base\LegacyEventConstants; use craft\elements\Category; use craft\elements\db\CategoryQuery; @@ -15,6 +14,7 @@ use craft\helpers\Gql; use craft\helpers\Gql as GqlHelper; use craft\services\Gql as GqlService; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\ElementSources; use CraftCms\Cms\Gql\Data\GqlSchema; diff --git a/yii2-adapter/legacy/fields/Tags.php b/yii2-adapter/legacy/fields/Tags.php index 7506bc17d76..075dfbd1834 100644 --- a/yii2-adapter/legacy/fields/Tags.php +++ b/yii2-adapter/legacy/fields/Tags.php @@ -5,7 +5,6 @@ namespace craft\fields; use Craft; -use craft\base\ElementInterface; use craft\base\LegacyEventConstants; use craft\elements\db\TagQuery; use craft\elements\Tag; @@ -16,6 +15,7 @@ use craft\helpers\Gql as GqlHelper; use craft\models\TagGroup; use craft\services\Gql as GqlService; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\ElementCollection; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Gql\Data\GqlSchema; diff --git a/yii2-adapter/legacy/helpers/Component.php b/yii2-adapter/legacy/helpers/Component.php index 83ee777407b..6f713cd7853 100644 --- a/yii2-adapter/legacy/helpers/Component.php +++ b/yii2-adapter/legacy/helpers/Component.php @@ -7,12 +7,12 @@ namespace craft\helpers; -use craft\base\ElementInterface; use craft\base\Model; use CraftCms\Cms\Component\ComponentHelper; use CraftCms\Cms\Component\Contracts\ComponentInterface; use CraftCms\Cms\Component\Exceptions\MissingComponentException; use CraftCms\Cms\Cp\Icons; +use CraftCms\Cms\Element\Contracts\ElementInterface; use DateTime; use RuntimeException; use yii\base\InvalidConfigException; diff --git a/yii2-adapter/legacy/helpers/Cp.php b/yii2-adapter/legacy/helpers/Cp.php index 49e8ff50687..225cdcfe7d5 100644 --- a/yii2-adapter/legacy/helpers/Cp.php +++ b/yii2-adapter/legacy/helpers/Cp.php @@ -9,7 +9,6 @@ namespace craft\helpers; -use craft\base\ElementInterface; use craft\base\Event as YiiEvent; use craft\events\DefineElementHtmlEvent; use craft\events\DefineElementInnerHtmlEvent; @@ -32,6 +31,7 @@ use CraftCms\Cms\Cp\Html\StatusHtml; use CraftCms\Cms\Cp\Icons; use CraftCms\Cms\Cp\RequestedSite; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\FieldLayout; use CraftCms\Cms\FieldLayout\FieldLayoutElement; diff --git a/yii2-adapter/legacy/helpers/Db.php b/yii2-adapter/legacy/helpers/Db.php index d74c25d5c6a..d377bf5e3ed 100644 --- a/yii2-adapter/legacy/helpers/Db.php +++ b/yii2-adapter/legacy/helpers/Db.php @@ -16,6 +16,7 @@ use craft\db\Table; use CraftCms\Cms\Database\QueryParam; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Money as MoneyHelper; use CraftCms\Cms\Support\Query as QueryHelper; @@ -26,7 +27,6 @@ use PDO; use Throwable; use yii\base\Exception; -use yii\base\NotSupportedException; use yii\db\BatchQueryResult; use yii\db\Exception as DbException; use yii\db\ExpressionInterface; diff --git a/yii2-adapter/legacy/helpers/ElementHelper.php b/yii2-adapter/legacy/helpers/ElementHelper.php index d7524b537e4..b8aa0242d1e 100644 --- a/yii2-adapter/legacy/helpers/ElementHelper.php +++ b/yii2-adapter/legacy/helpers/ElementHelper.php @@ -7,8 +7,8 @@ namespace craft\helpers; -use craft\base\ElementInterface; use CraftCms\Cms\Element\Contracts\ElementActionInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Drafts; use CraftCms\Cms\Element\ElementAttributeRenderer; use CraftCms\Cms\Element\ElementHelper as LaravelElementHelper; diff --git a/yii2-adapter/legacy/migrations/BaseContentRefactorMigration.php b/yii2-adapter/legacy/migrations/BaseContentRefactorMigration.php index 1505853e9d1..bedceff0564 100644 --- a/yii2-adapter/legacy/migrations/BaseContentRefactorMigration.php +++ b/yii2-adapter/legacy/migrations/BaseContentRefactorMigration.php @@ -2,7 +2,6 @@ namespace craft\migrations; -use craft\base\ElementInterface; use craft\db\Migration; use craft\db\Query; use craft\db\Table; @@ -10,6 +9,7 @@ use craft\helpers\Db; use craft\helpers\Json; use craft\models\FieldLayout; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; use CraftCms\Cms\Field\MissingField; use CraftCms\Cms\FieldLayout\LayoutElements\CustomField; diff --git a/yii2-adapter/legacy/models/ElementActivity.php b/yii2-adapter/legacy/models/ElementActivity.php index f8da883704f..0cb732fd564 100644 --- a/yii2-adapter/legacy/models/ElementActivity.php +++ b/yii2-adapter/legacy/models/ElementActivity.php @@ -7,7 +7,7 @@ namespace craft\models; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\User\Elements\User; use DateTime; diff --git a/yii2-adapter/legacy/models/FieldLayout.php b/yii2-adapter/legacy/models/FieldLayout.php index c487634ea2d..baeb8307099 100644 --- a/yii2-adapter/legacy/models/FieldLayout.php +++ b/yii2-adapter/legacy/models/FieldLayout.php @@ -4,12 +4,12 @@ namespace craft\models; -use craft\base\ElementInterface; use craft\base\Event as YiiEvent; use craft\events\CreateFieldLayoutFormEvent; use craft\events\DefineFieldLayoutCustomFieldsEvent; use craft\events\DefineFieldLayoutElementsEvent; use craft\events\DefineFieldLayoutFieldsEvent; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\FieldLayout\Events\CreateFieldLayoutForm; use CraftCms\Cms\FieldLayout\Events\DefineCustomFields; use CraftCms\Cms\FieldLayout\Events\DefineNativeFields; diff --git a/yii2-adapter/legacy/queue/jobs/ApplyNewPropagationMethod.php b/yii2-adapter/legacy/queue/jobs/ApplyNewPropagationMethod.php index 0e2daa86d0f..cb6fd030528 100644 --- a/yii2-adapter/legacy/queue/jobs/ApplyNewPropagationMethod.php +++ b/yii2-adapter/legacy/queue/jobs/ApplyNewPropagationMethod.php @@ -9,8 +9,8 @@ namespace craft\queue\jobs; -use craft\base\ElementInterface; use craft\queue\BaseJob; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * ApplyNewPropagationMethod loads all elements that match a given criteria, diff --git a/yii2-adapter/legacy/queue/jobs/PropagateElements.php b/yii2-adapter/legacy/queue/jobs/PropagateElements.php index b255ad5032b..d56b14d36f1 100644 --- a/yii2-adapter/legacy/queue/jobs/PropagateElements.php +++ b/yii2-adapter/legacy/queue/jobs/PropagateElements.php @@ -9,8 +9,8 @@ namespace craft\queue\jobs; -use craft\base\ElementInterface; use craft\queue\BaseJob; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * PropagateElements job diff --git a/yii2-adapter/legacy/queue/jobs/PruneRevisions.php b/yii2-adapter/legacy/queue/jobs/PruneRevisions.php index 0cc4c61f844..18a540d2ab8 100644 --- a/yii2-adapter/legacy/queue/jobs/PruneRevisions.php +++ b/yii2-adapter/legacy/queue/jobs/PruneRevisions.php @@ -9,8 +9,8 @@ namespace craft\queue\jobs; -use craft\base\ElementInterface; use craft\queue\BaseJob; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\I18N; /** diff --git a/yii2-adapter/legacy/queue/jobs/ResaveElements.php b/yii2-adapter/legacy/queue/jobs/ResaveElements.php index 944e308cf29..c968f1c9c05 100644 --- a/yii2-adapter/legacy/queue/jobs/ResaveElements.php +++ b/yii2-adapter/legacy/queue/jobs/ResaveElements.php @@ -9,8 +9,8 @@ namespace craft\queue\jobs; -use craft\base\ElementInterface; use craft\queue\BaseJob; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\I18N; /** diff --git a/yii2-adapter/legacy/queue/jobs/UpdateElementSlugsAndUris.php b/yii2-adapter/legacy/queue/jobs/UpdateElementSlugsAndUris.php index ef649df99e3..ba23ab98d8d 100644 --- a/yii2-adapter/legacy/queue/jobs/UpdateElementSlugsAndUris.php +++ b/yii2-adapter/legacy/queue/jobs/UpdateElementSlugsAndUris.php @@ -9,8 +9,8 @@ namespace craft\queue\jobs; -use craft\base\ElementInterface; use craft\queue\BaseJob; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\I18N; /** diff --git a/yii2-adapter/legacy/queue/jobs/UpdateSearchIndex.php b/yii2-adapter/legacy/queue/jobs/UpdateSearchIndex.php index 16dd2a71e14..06d9e1269a9 100644 --- a/yii2-adapter/legacy/queue/jobs/UpdateSearchIndex.php +++ b/yii2-adapter/legacy/queue/jobs/UpdateSearchIndex.php @@ -9,8 +9,8 @@ namespace craft\queue\jobs; -use craft\base\ElementInterface; use craft\queue\BaseJob; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Facades\I18N; /** diff --git a/yii2-adapter/legacy/services/Drafts.php b/yii2-adapter/legacy/services/Drafts.php index e88104e5534..d5c7a5d51d4 100644 --- a/yii2-adapter/legacy/services/Drafts.php +++ b/yii2-adapter/legacy/services/Drafts.php @@ -8,9 +8,9 @@ namespace craft\services; use Craft; -use craft\base\ElementInterface; use craft\events\DraftEvent; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Events\ApplyingDraft; use CraftCms\Cms\Element\Events\CreatingDraft; diff --git a/yii2-adapter/legacy/services/ElementSources.php b/yii2-adapter/legacy/services/ElementSources.php index bfac1e497e1..9e6ec3efa90 100644 --- a/yii2-adapter/legacy/services/ElementSources.php +++ b/yii2-adapter/legacy/services/ElementSources.php @@ -8,9 +8,9 @@ namespace craft\services; use Craft; -use craft\base\ElementInterface; use craft\events\DefineSourceSortOptionsEvent; use craft\events\DefineSourceTableAttributesEvent; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Events\DefineSourceSortOptions; use CraftCms\Cms\Element\Events\DefineSourceTableAttributes; use CraftCms\Cms\FieldLayout\FieldLayout; diff --git a/yii2-adapter/legacy/services/Elements.php b/yii2-adapter/legacy/services/Elements.php index 87af174f046..bfae1c595b1 100644 --- a/yii2-adapter/legacy/services/Elements.php +++ b/yii2-adapter/legacy/services/Elements.php @@ -10,7 +10,6 @@ namespace craft\services; use Craft; -use craft\base\ElementInterface; use craft\errors\ElementNotFoundException; use craft\events\AuthorizationCheckEvent; use craft\events\BulkOpEvent; @@ -29,6 +28,7 @@ use CraftCms\Cms\Element\BulkOp\Events\BeforeBulkOp; use CraftCms\Cms\Element\Contracts\ElementActionInterface; use CraftCms\Cms\Element\Contracts\ElementExporterInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Data\EagerLoadPlan; use CraftCms\Cms\Element\Data\ElementActivity as ElementActivityData; use CraftCms\Cms\Element\Drafts; diff --git a/yii2-adapter/legacy/services/Fields.php b/yii2-adapter/legacy/services/Fields.php index f56868aa744..ef337d72ded 100644 --- a/yii2-adapter/legacy/services/Fields.php +++ b/yii2-adapter/legacy/services/Fields.php @@ -8,12 +8,12 @@ namespace craft\services; use Craft; -use craft\base\ElementInterface; use craft\events\ApplyFieldSaveEvent; use craft\events\DefineCompatibleFieldTypesEvent; use craft\events\FieldEvent; use craft\events\LocateUploadedFilesEvent; use craft\events\RegisterComponentTypesEvent; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\BaseRelationField; use CraftCms\Cms\Field\Contracts\ElementContainerFieldInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; diff --git a/yii2-adapter/legacy/services/Gc.php b/yii2-adapter/legacy/services/Gc.php index 8c43544959c..9f7105bb6a3 100644 --- a/yii2-adapter/legacy/services/Gc.php +++ b/yii2-adapter/legacy/services/Gc.php @@ -8,7 +8,7 @@ namespace craft\services; use Craft; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Filesystem\Exceptions\FilesystemException; use CraftCms\Cms\GarbageCollection\Actions\DeleteOrphanedFieldLayouts; use CraftCms\Cms\GarbageCollection\Actions\DeleteOrphanedNestedElements; diff --git a/yii2-adapter/legacy/services/ProjectConfig.php b/yii2-adapter/legacy/services/ProjectConfig.php index 672d527ecc4..9b0cb7c8b06 100644 --- a/yii2-adapter/legacy/services/ProjectConfig.php +++ b/yii2-adapter/legacy/services/ProjectConfig.php @@ -19,6 +19,7 @@ use CraftCms\Cms\ProjectConfig\Exceptions\BusyResourceException; use CraftCms\Cms\ProjectConfig\Exceptions\StaleResourceException; use CraftCms\Cms\ProjectConfig\ProjectConfigHelper; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\DependencyAwareCache\Dependency\CallbackDependency; use Illuminate\Support\Facades\Event; use ReflectionClass; @@ -27,7 +28,6 @@ use yii\base\ErrorException; use yii\base\Exception; use yii\base\InvalidConfigException; -use yii\base\NotSupportedException; use yii\web\ServerErrorHttpException; /** diff --git a/yii2-adapter/legacy/services/Relations.php b/yii2-adapter/legacy/services/Relations.php index a03ace325f1..0b8c1d68902 100644 --- a/yii2-adapter/legacy/services/Relations.php +++ b/yii2-adapter/legacy/services/Relations.php @@ -7,9 +7,9 @@ namespace craft\services; -use craft\base\ElementInterface; use craft\db\Command; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\BaseRelationField; use CraftCms\Cms\FieldLayout\LayoutElements\CustomField; use CraftCms\Cms\Support\Arr; diff --git a/yii2-adapter/legacy/services/Revisions.php b/yii2-adapter/legacy/services/Revisions.php index c9ddcc4e110..d3ec691569e 100644 --- a/yii2-adapter/legacy/services/Revisions.php +++ b/yii2-adapter/legacy/services/Revisions.php @@ -8,8 +8,8 @@ namespace craft\services; use Craft; -use craft\base\ElementInterface; use craft\events\RevisionEvent; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Events\CreatingRevision; use CraftCms\Cms\Element\Events\RevertedToRevision; use CraftCms\Cms\Element\Events\RevertingToRevision; diff --git a/yii2-adapter/legacy/services/Search.php b/yii2-adapter/legacy/services/Search.php index d913c24428b..beb59b91e68 100644 --- a/yii2-adapter/legacy/services/Search.php +++ b/yii2-adapter/legacy/services/Search.php @@ -8,12 +8,12 @@ namespace craft\services; use Craft; -use craft\base\ElementInterface; use craft\db\Query; use craft\db\Table; use craft\events\IndexKeywordsEvent; use craft\events\SearchEvent; use CraftCms\Cms\Cms; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface; use CraftCms\Cms\Field\Contracts\FieldInterface; use CraftCms\Cms\Field\Fields; diff --git a/yii2-adapter/legacy/services/Sites.php b/yii2-adapter/legacy/services/Sites.php index 34ded449e8e..456b2b2e969 100644 --- a/yii2-adapter/legacy/services/Sites.php +++ b/yii2-adapter/legacy/services/Sites.php @@ -17,6 +17,7 @@ use craft\models\Site as LegacySite; use craft\models\SiteGroup as LegacySiteGroup; use CraftCms\Cms\ProjectConfig\Events\ConfigEvent; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Site\Data\Site; use CraftCms\Cms\Site\Data\SiteGroup; use CraftCms\Cms\Site\Events\ApplyingSiteDelete; @@ -40,7 +41,6 @@ use Throwable; use yii\base\Component; use yii\base\Exception; -use yii\base\NotSupportedException; use yii\db\Exception as DbException; /** diff --git a/yii2-adapter/legacy/services/Structures.php b/yii2-adapter/legacy/services/Structures.php index 7e55c316e16..6956d571fc1 100644 --- a/yii2-adapter/legacy/services/Structures.php +++ b/yii2-adapter/legacy/services/Structures.php @@ -8,8 +8,8 @@ namespace craft\services; use Craft; -use craft\base\ElementInterface; use craft\events\MoveElementEvent; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Structure\Data\Structure; use CraftCms\Cms\Structure\Enums\Action; use CraftCms\Cms\Structure\Enums\Mode; diff --git a/yii2-adapter/legacy/test/Craft.php b/yii2-adapter/legacy/test/Craft.php index e45d760f309..f874e6eef93 100644 --- a/yii2-adapter/legacy/test/Craft.php +++ b/yii2-adapter/legacy/test/Craft.php @@ -12,7 +12,6 @@ use Codeception\PHPUnit\TestCase as CodeceptionTestCase; use Codeception\Stub; use Codeception\TestInterface; -use craft\base\ElementInterface; use craft\config\DbConfig; use craft\console\Application as ConsoleApplication; use craft\errors\ElementNotFoundException; @@ -25,6 +24,7 @@ use CraftCms\Cms\Cms; use CraftCms\Cms\Database\Table; use CraftCms\Cms\Edition; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\EntryTypes; use CraftCms\Cms\Field\Fields; use CraftCms\Cms\FieldLayout\FieldLayout; diff --git a/yii2-adapter/legacy/test/fixtures/FieldLayoutFixture.php b/yii2-adapter/legacy/test/fixtures/FieldLayoutFixture.php index a297f140609..58264294ad8 100644 --- a/yii2-adapter/legacy/test/fixtures/FieldLayoutFixture.php +++ b/yii2-adapter/legacy/test/fixtures/FieldLayoutFixture.php @@ -18,6 +18,7 @@ use CraftCms\Cms\FieldLayout\FieldLayout; use CraftCms\Cms\FieldLayout\FieldLayoutTab; use CraftCms\Cms\FieldLayout\LayoutElements\CustomField; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Typecast; use Illuminate\Support\Facades\DB; @@ -25,7 +26,6 @@ use PDO; use Throwable; use yii\base\Exception as YiiBaseException; -use yii\base\NotSupportedException; use yii\test\DbFixture; use yii\test\FileFixtureTrait; diff --git a/yii2-adapter/legacy/test/fixtures/elements/AssetFixture.php b/yii2-adapter/legacy/test/fixtures/elements/AssetFixture.php index 8e3d58d5810..d5272273001 100644 --- a/yii2-adapter/legacy/test/fixtures/elements/AssetFixture.php +++ b/yii2-adapter/legacy/test/fixtures/elements/AssetFixture.php @@ -8,11 +8,11 @@ namespace craft\test\fixtures\elements; use Craft; -use craft\base\ElementInterface; use craft\helpers\FileHelper; use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Asset\Models\VolumeFolder as VolumeFolderModel; use CraftCms\Cms\Asset\Volumes; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * Class AssetFixture. diff --git a/yii2-adapter/legacy/test/fixtures/elements/BaseContentFixture.php b/yii2-adapter/legacy/test/fixtures/elements/BaseContentFixture.php index 23e35ea2b6f..85450659933 100644 --- a/yii2-adapter/legacy/test/fixtures/elements/BaseContentFixture.php +++ b/yii2-adapter/legacy/test/fixtures/elements/BaseContentFixture.php @@ -7,7 +7,7 @@ namespace craft\test\fixtures\elements; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Exceptions\InvalidElementException; use CraftCms\Cms\Support\Facades\Elements; use CraftCms\Cms\Support\Typecast; diff --git a/yii2-adapter/legacy/test/fixtures/elements/BaseElementFixture.php b/yii2-adapter/legacy/test/fixtures/elements/BaseElementFixture.php index a039c6f2281..2b511d96a73 100644 --- a/yii2-adapter/legacy/test/fixtures/elements/BaseElementFixture.php +++ b/yii2-adapter/legacy/test/fixtures/elements/BaseElementFixture.php @@ -8,9 +8,9 @@ namespace craft\test\fixtures\elements; use Craft; -use craft\base\ElementInterface; use craft\test\DbFixtureTrait; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Exceptions\InvalidElementException; use CraftCms\Cms\Element\Validation\ElementRules; diff --git a/yii2-adapter/legacy/test/fixtures/elements/CategoryFixture.php b/yii2-adapter/legacy/test/fixtures/elements/CategoryFixture.php index 1d667f98299..861a920d636 100644 --- a/yii2-adapter/legacy/test/fixtures/elements/CategoryFixture.php +++ b/yii2-adapter/legacy/test/fixtures/elements/CategoryFixture.php @@ -8,8 +8,8 @@ namespace craft\test\fixtures\elements; use Craft; -use craft\base\ElementInterface; use craft\elements\Category; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * Class CategoryFixture. diff --git a/yii2-adapter/legacy/test/fixtures/elements/EntryFixture.php b/yii2-adapter/legacy/test/fixtures/elements/EntryFixture.php index 4469fe29245..0df74760eaa 100644 --- a/yii2-adapter/legacy/test/fixtures/elements/EntryFixture.php +++ b/yii2-adapter/legacy/test/fixtures/elements/EntryFixture.php @@ -7,7 +7,7 @@ namespace craft\test\fixtures\elements; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Support\Facades\EntryTypes; use CraftCms\Cms\Support\Facades\Sections; diff --git a/yii2-adapter/legacy/test/fixtures/elements/GlobalSetFixture.php b/yii2-adapter/legacy/test/fixtures/elements/GlobalSetFixture.php index 8df87d50a86..3c7bb128565 100644 --- a/yii2-adapter/legacy/test/fixtures/elements/GlobalSetFixture.php +++ b/yii2-adapter/legacy/test/fixtures/elements/GlobalSetFixture.php @@ -8,9 +8,9 @@ namespace craft\test\fixtures\elements; use Craft; -use craft\base\ElementInterface; use craft\elements\GlobalSet; use craft\records\GlobalSet as GlobalSetRecord; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * Class GlobalSetFixture diff --git a/yii2-adapter/legacy/test/fixtures/elements/TagFixture.php b/yii2-adapter/legacy/test/fixtures/elements/TagFixture.php index 42a9028d941..de0f4711212 100644 --- a/yii2-adapter/legacy/test/fixtures/elements/TagFixture.php +++ b/yii2-adapter/legacy/test/fixtures/elements/TagFixture.php @@ -8,8 +8,8 @@ namespace craft\test\fixtures\elements; use Craft; -use craft\base\ElementInterface; use craft\elements\Tag; +use CraftCms\Cms\Element\Contracts\ElementInterface; /** * Class TagFixture diff --git a/yii2-adapter/legacy/test/fixtures/elements/UserFixture.php b/yii2-adapter/legacy/test/fixtures/elements/UserFixture.php index 91eb5a46a2c..30b4a871ec7 100644 --- a/yii2-adapter/legacy/test/fixtures/elements/UserFixture.php +++ b/yii2-adapter/legacy/test/fixtures/elements/UserFixture.php @@ -7,7 +7,7 @@ namespace craft\test\fixtures\elements; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\User\Elements\User; /** diff --git a/yii2-adapter/legacy/test/mockclasses/elements/MockElementQuery.php b/yii2-adapter/legacy/test/mockclasses/elements/MockElementQuery.php index ae26ac2adc4..c72f1b2d8b6 100644 --- a/yii2-adapter/legacy/test/mockclasses/elements/MockElementQuery.php +++ b/yii2-adapter/legacy/test/mockclasses/elements/MockElementQuery.php @@ -8,8 +8,8 @@ namespace craft\test\mockclasses\elements; use Craft; -use craft\base\ElementInterface; use craft\elements\db\ElementQuery; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Support\Str; /** diff --git a/yii2-adapter/legacy/validators/AssetLocationValidator.php b/yii2-adapter/legacy/validators/AssetLocationValidator.php index 483b16965b1..c574435de94 100644 --- a/yii2-adapter/legacy/validators/AssetLocationValidator.php +++ b/yii2-adapter/legacy/validators/AssetLocationValidator.php @@ -7,12 +7,9 @@ namespace craft\validators; -use craft\helpers\Assets; -use CraftCms\Cms\Asset\AssetsHelper; use CraftCms\Cms\Asset\Elements\Asset; -use CraftCms\Cms\Asset\Folders; +use CraftCms\Cms\Asset\Validation\Rules\AssetLocationRule; use CraftCms\Cms\Cms; -use yii\base\InvalidConfigException; use yii\base\Model; use yii\validators\Validator; use function CraftCms\Cms\t; @@ -22,6 +19,7 @@ * * @author Pixel & Tonic, Inc. * @since 3.0.0 + * @deprecated 6.0.0 use {@see \CraftCms\Cms\Asset\Validation\AssetLocationRule} instead. */ class AssetLocationValidator extends Validator { @@ -96,63 +94,30 @@ public function init(): void public function validateAttribute($model, $attribute): void { /** @var Asset $model */ - [$folderId, $filename] = Assets::parseFileLocation($model->$attribute); - - // Figure out which of them has changed - $hasNewFolderId = $folderId != $model->{$this->folderIdAttribute}; - $hasNewFilename = $filename != $model->{$this->filenameAttribute}; - - // If nothing has changed, just null-out the newLocation attribute - if (!$hasNewFolderId && !$hasNewFilename) { - $model->$attribute = null; - - return; - } - - // Get the folder - if (app(Folders::class)->getFolderById($folderId) === null) { - throw new InvalidConfigException('Invalid folder ID: ' . $folderId); - } - - // Make sure the new filename has a valid extension - $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); - - if (is_array($this->allowedExtensions) && !in_array($extension, $this->allowedExtensions, true)) { - $this->addLocationError($model, $attribute, Asset::ERROR_DISALLOWED_EXTENSION, $this->disallowedExtension, ['extension' => $extension]); - return; - } - - // Prepare the filename - $filename = AssetsHelper::prepareAssetName($filename); - $suggestedFilename = app(\CraftCms\Cms\Asset\Assets::class)->getNameReplacementInFolder($filename, $folderId); - - if ($suggestedFilename !== $filename) { - $model->{$this->conflictingFilenameAttribute} = $filename; - $model->{$this->suggestedFilenameAttribute} = $suggestedFilename; - - if (!$this->avoidFilenameConflicts) { - $this->addLocationError($model, $attribute, Asset::ERROR_FILENAME_CONFLICT, $this->filenameConflict, ['filename' => $filename]); - - return; + $rule = new AssetLocationRule( + asset: $model, + folderIdAttribute: $this->folderIdAttribute, + filenameAttribute: $this->filenameAttribute, + suggestedFilenameAttribute: $this->suggestedFilenameAttribute, + conflictingFilenameAttribute: $this->conflictingFilenameAttribute, + errorCodeAttribute: $this->errorCodeAttribute, + allowedExtensions: $this->allowedExtensions, + disallowedExtension: $this->disallowedExtension, + filenameConflict: $this->filenameConflict, + ); + + $validator = \Illuminate\Support\Facades\Validator::make([ + $attribute => $model->$attribute, + ], [ + $attribute => $rule, + ]); + + if ($validator->fails()) { + foreach ($validator->errors()->get($attribute) as $messages) { + foreach ($messages as $message) { + $model->addError($attribute, $message[0]); + } } } - - // Update the newLocation attribute in case the filename changed - $model->$attribute = "{folder:$folderId}$suggestedFilename"; - } - - /** - * Adds a location error to the model. - * - * @param Model $model - * @param string $attribute - * @param string $errorCode - * @param string $message - * @param array $params - */ - public function addLocationError(Model $model, string $attribute, string $errorCode, string $message, array $params = []): void - { - $this->addError($model, $attribute, $message, $params); - $model->{$this->errorCodeAttribute} = $errorCode; } } diff --git a/yii2-adapter/legacy/validators/ElementUriValidator.php b/yii2-adapter/legacy/validators/ElementUriValidator.php index bb5b0668a4b..55a7a500e3a 100644 --- a/yii2-adapter/legacy/validators/ElementUriValidator.php +++ b/yii2-adapter/legacy/validators/ElementUriValidator.php @@ -7,7 +7,7 @@ namespace craft\validators; -use craft\base\ElementInterface; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\Validation\ElementRules; use CraftCms\Cms\Shared\Exceptions\OperationAbortedException; diff --git a/yii2-adapter/legacy/validators/SlugValidator.php b/yii2-adapter/legacy/validators/SlugValidator.php index 36a77b9f414..28bfa53c828 100644 --- a/yii2-adapter/legacy/validators/SlugValidator.php +++ b/yii2-adapter/legacy/validators/SlugValidator.php @@ -7,8 +7,8 @@ namespace craft\validators; -use craft\base\ElementInterface; use CraftCms\Cms\Cms; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Element\ElementHelper; use CraftCms\Cms\Element\Validation\ElementRules; diff --git a/yii2-adapter/legacy/web/Application.php b/yii2-adapter/legacy/web/Application.php index 12e61fa0ae2..fedda2a1558 100644 --- a/yii2-adapter/legacy/web/Application.php +++ b/yii2-adapter/legacy/web/Application.php @@ -23,6 +23,9 @@ use CraftCms\Cms\Plugin\Plugins; use CraftCms\Cms\Support\Typecast; use CraftCms\Cms\Support\Url; +use CraftCms\Yii2Adapter\Web\Response as IlluminateBridgeResponse; +use Illuminate\Contracts\Http\Kernel; +use Illuminate\Http\Request as IlluminateRequest; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Gate; @@ -30,6 +33,7 @@ use IntlDateFormatter; use IntlException; use ReflectionClass; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Throwable; use yii\base\Component; use yii\base\ErrorException; @@ -237,7 +241,15 @@ public function handleRequest($request, bool $skipSpecialHandling = false): Base */ public function runAction($route, $params = []): ?BaseResponse { - $result = parent::runAction($route, $params); + try { + $result = parent::runAction($route, $params); + } catch (InvalidRouteException $e) { + if (($response = $this->runLaravelAction($route, $params)) !== null) { + return $response; + } + + throw $e; + } if ($result === null || $result instanceof BaseResponse) { return $result; @@ -248,6 +260,55 @@ public function runAction($route, $params = []): ?BaseResponse return $response; } + private function runLaravelAction(string $route, array $params = []): ?BaseResponse + { + $actionUri = request()->actionSegmentsToRoute(explode('/', $route)); + + if ($actionUri === null) { + return null; + } + + /** @var IlluminateRequest $request */ + $request = request(); + + if ($request->headers->has('X-Craft-Legacy-Action-Bridge')) { + return null; + } + + $payload = $request->merge($params)->all(); + + unset($payload['action'], $payload['p']); + + if ($request->hasSession()) { + $payload['_token'] ??= $request->session()->token(); + } + + $internalRequest = $request->duplicate( + query: [], + request: $payload, + server: array_merge($request->server->all(), [ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => $actionUri, + 'HTTP_X_CRAFT_LEGACY_ACTION_BRIDGE' => '1', + ]), + ); + + if ($request->hasSession()) { + $internalRequest->setLaravelSession($request->session()); + } + + /** @var SymfonyResponse $laravelResponse */ + $laravelResponse = app(Kernel::class)->handle($internalRequest); + + $response = $this->getResponse(); + + if ($response instanceof IlluminateBridgeResponse) { + return $response->setIlluminateResponse($laravelResponse); + } + + return $response; + } + /** * @inheritdoc */ diff --git a/yii2-adapter/legacy/web/Controller.php b/yii2-adapter/legacy/web/Controller.php index 18bc79895f9..48d8c222b51 100644 --- a/yii2-adapter/legacy/web/Controller.php +++ b/yii2-adapter/legacy/web/Controller.php @@ -8,7 +8,6 @@ namespace craft\web; use Craft; -use craft\base\ModelInterface; use craft\events\DefineBehaviorsEvent; use CraftCms\Cms\Auth\Concerns\ConfirmsPasswords; use CraftCms\Cms\Auth\SessionAuth; @@ -386,7 +385,7 @@ public function asSuccess( /** * Sends a failure response for a model. * - * @param Model|ModelInterface $model The model that was being operated on + * @param object $model The model that was being operated on * @param string|null $message * @param string|null $modelName The route param name that the model should be set to * @param array $data Additional data to include in the JSON response @@ -395,7 +394,7 @@ public function asSuccess( * @since 4.0.0 */ public function asModelFailure( - mixed $model, + object $model, ?string $message = null, ?string $modelName = null, array $data = [], @@ -419,7 +418,7 @@ public function asModelFailure( /** * Sends a success response for a model. * - * @param Model|ModelInterface $model The model that was being operated on + * @param object $model The model that was being operated on * @param string|null $message * @param string|null $modelName The route param name that the model should be set to * @param array $data Additional data to include in the JSON response @@ -428,7 +427,7 @@ public function asModelFailure( * @since 4.0.0 */ public function asModelSuccess( - mixed $model, + object $model, ?string $message = null, ?string $modelName = null, array $data = [], diff --git a/yii2-adapter/legacy/web/UrlManager.php b/yii2-adapter/legacy/web/UrlManager.php index ad388e6c293..c016097eb59 100644 --- a/yii2-adapter/legacy/web/UrlManager.php +++ b/yii2-adapter/legacy/web/UrlManager.php @@ -8,10 +8,10 @@ namespace craft\web; use Craft; -use craft\base\ElementInterface; use craft\events\RegisterUrlRulesEvent; use craft\web\UrlRule as CraftUrlRule; use CraftCms\Cms\Cms; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Element; use CraftCms\Cms\Route\MatchedElement; use CraftCms\Cms\Support\Arr; diff --git a/yii2-adapter/legacy/web/View.php b/yii2-adapter/legacy/web/View.php index 09508432a50..9bf56f42e28 100644 --- a/yii2-adapter/legacy/web/View.php +++ b/yii2-adapter/legacy/web/View.php @@ -16,6 +16,7 @@ use craft\events\TemplateEvent; use craft\helpers\Cp; use CraftCms\Cms\Cp\Html\ElementHtml; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Facades\DeltaRegistry; use CraftCms\Cms\Support\Facades\Deprecator; use CraftCms\Cms\Support\Facades\InputNamespace; @@ -48,7 +49,6 @@ use Twig\Error\SyntaxError as TwigSyntaxError; use Twig\Extension\ExtensionInterface; use yii\base\Exception; -use yii\base\NotSupportedException; use yii\web\AssetBundle as YiiAssetBundle; use function CraftCms\Cms\t; diff --git a/yii2-adapter/src/Event/EventCompatibility.php b/yii2-adapter/src/Event/EventCompatibility.php index f4c800b4ecb..fc127ef55f2 100644 --- a/yii2-adapter/src/Event/EventCompatibility.php +++ b/yii2-adapter/src/Event/EventCompatibility.php @@ -9,6 +9,7 @@ use craft\base\Event as YiiEvent; use craft\base\FieldLayoutComponent; use craft\console\controllers\ResaveController; +use craft\controllers\ElementsController; use craft\controllers\UsersController; use craft\db\Connection; use craft\elements\Asset; @@ -141,6 +142,7 @@ public function boot(): void */ ResaveController::registerEvents(); UsersController::registerEvents(); + ElementsController::registerEvents(); /** * Utilities diff --git a/yii2-adapter/src/Http/LegacyMiddleware.php b/yii2-adapter/src/Http/LegacyMiddleware.php index 1003371ddc9..35f8fbe2ab5 100644 --- a/yii2-adapter/src/Http/LegacyMiddleware.php +++ b/yii2-adapter/src/Http/LegacyMiddleware.php @@ -84,12 +84,12 @@ public function handle(Request $request, Closure $next): mixed /** * Creates HTTP response for this middleware. * - * @return Response HTTP response instance. + * @return \Symfony\Component\HttpFoundation\Response HTTP response instance. * *@see DummyResponse * @see \CraftCms\Yii2Adapter\Web\Response */ - public static function createResponse(): Response + public static function createResponse(): \Symfony\Component\HttpFoundation\Response { if (headers_sent()) { self::cleanup(); diff --git a/yii2-adapter/src/IdentityWrapper.php b/yii2-adapter/src/IdentityWrapper.php index ef7ce366824..9c34d648d10 100644 --- a/yii2-adapter/src/IdentityWrapper.php +++ b/yii2-adapter/src/IdentityWrapper.php @@ -6,12 +6,12 @@ use CraftCms\Cms\Auth\Impersonation; use CraftCms\Cms\Database\Table; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Json; use CraftCms\Cms\User\Elements\User; use Illuminate\Support\Facades\DB as DbFacade; use Illuminate\Support\Traits\ForwardsCalls; use yii\base\Exception; -use yii\base\NotSupportedException; use yii\web\IdentityInterface; /** diff --git a/yii2-adapter/src/Mixins/ElementQueryMixin.php b/yii2-adapter/src/Mixins/ElementQueryMixin.php index c10fa64dd57..3c737f8ea56 100644 --- a/yii2-adapter/src/Mixins/ElementQueryMixin.php +++ b/yii2-adapter/src/Mixins/ElementQueryMixin.php @@ -6,9 +6,9 @@ use Closure; use Craft; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Arr; use CraftCms\Cms\Support\Facades\Deprecator; -use yii\base\NotSupportedException; class ElementQueryMixin { diff --git a/yii2-adapter/src/Mixins/ValidateMixin.php b/yii2-adapter/src/Mixins/ValidateMixin.php index bc437309ad5..3766c2f3f86 100644 --- a/yii2-adapter/src/Mixins/ValidateMixin.php +++ b/yii2-adapter/src/Mixins/ValidateMixin.php @@ -77,34 +77,6 @@ public function addError(): Closure }; } - public function clearErrors(): Closure - { - return function($attribute = null): void { - Deprecator::log($this::class . '->clearErrors', 'Calling `->clearErrors` is deprecated. Use `->errors()->forget()` instead.'); - - if ($attribute === null) { - /** - * @var Element|Field $this - * - * @phpstan-ignore-next-line - */ - foreach ($this->errors()->getMessages() as $key => $messages) { - /** @phpstan-ignore-next-line */ - $this->errors()->forget($key); - } - - return; - } - - /** - * @var Element|Field $this - * - * @phpstan-ignore-next-line - */ - $this->errors()->forget($attribute); - }; - } - public function getFirstError(): Closure { return function(string $attribute): ?string { @@ -119,17 +91,17 @@ public function getFirstError(): Closure }; } - public function getAttributeLabel(): Closure + public function getErrorSummary(): Closure { - return function(string $attribute): string { - Deprecator::log($this::class . '->getAttributeLabel', 'Calling `->getAttributeLabel` is deprecated. Use `->attributeLabels()` instead.'); + return function($showAllErrors = false) { + Deprecator::log($this::class . '->getErrorSummary', 'Calling `->getErrorSummary` is deprecated. Use `->errors()->all()` instead.'); /** - * @var Volume|Widget|Element|Field|FieldLayoutComponent|Filesystem $this + * @var \CraftCms\Cms\Validation\Contracts\Validatable $this * * @phpstan-ignore-next-line */ - return $this->attributeLabels()[$attribute] ?? $attribute; + return $this->errors()->all(); }; } diff --git a/yii2-adapter/src/Web/Response.php b/yii2-adapter/src/Web/Response.php index 6ced9328a2a..f1178438514 100644 --- a/yii2-adapter/src/Web/Response.php +++ b/yii2-adapter/src/Web/Response.php @@ -12,6 +12,7 @@ use Craft; use Illuminate\Http\Response as IlluminateResponse; use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Throwable; use Yii; use yii\web\HeadersAlreadySentException; @@ -50,7 +51,7 @@ * @see IlluminateResponse * @see YiiApplicationMiddleware * - * @property IlluminateResponse $illuminateResponse related Laravel response. + * @property SymfonyResponse $illuminateResponse related Laravel response. * * @author Paul Klimov * @@ -59,14 +60,14 @@ class Response extends \yii\web\Response { /** - * @var IlluminateResponse|null related Laravel response. + * @var SymfonyResponse|null related Laravel response. */ private $_illuminateResponse; /** * @param bool $create whether to create a response, if it is empty. */ - public function getIlluminateResponse(bool $create = false): ?IlluminateResponse + public function getIlluminateResponse(bool $create = false): ?SymfonyResponse { if ($create) { $this->_illuminateResponse ??= $this->createIlluminateResponse(); @@ -80,6 +81,13 @@ protected function createIlluminateResponse(): IlluminateResponse return app()->make(IlluminateResponse::class); } + public function setIlluminateResponse(SymfonyResponse $response): static + { + $this->_illuminateResponse = $response; + + return $this; + } + /** * {@inheritdoc} */ diff --git a/yii2-adapter/stubs/_generated.stub b/yii2-adapter/stubs/_generated.stub index bee499c9ea9..038015ba6df 100644 --- a/yii2-adapter/stubs/_generated.stub +++ b/yii2-adapter/stubs/_generated.stub @@ -15,11 +15,11 @@ trait FunctionalTesterActions public function expectEvent(string $class, string $eventName, callable $callback, string $eventInstance = "", array $eventValues = []): void {} /** - * @param class-string<\craft\base\ElementInterface> $elementType + * @param class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType * @param array $searchProperties * @param int $amount * @param bool $searchAll - * @return \craft\base\ElementInterface[] + * @return \CraftCms\Cms\Element\Contracts\ElementInterface[] */ public function assertElementsExist(string $elementType, array $searchProperties = [], int $amount = 1, bool $searchAll = false): array {} } @@ -37,11 +37,11 @@ trait GqlTesterActions public function expectEvent(string $class, string $eventName, callable $callback, string $eventInstance = "", array $eventValues = []): void {} /** - * @param class-string<\craft\base\ElementInterface> $elementType + * @param class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType * @param array $searchProperties * @param int $amount * @param bool $searchAll - * @return \craft\base\ElementInterface[] + * @return \CraftCms\Cms\Element\Contracts\ElementInterface[] */ public function assertElementsExist(string $elementType, array $searchProperties = [], int $amount = 1, bool $searchAll = false): array {} } @@ -59,11 +59,11 @@ trait UnitTesterActions public function expectEvent(string $class, string $eventName, callable $callback, string $eventInstance = "", array $eventValues = []): void {} /** - * @param class-string<\craft\base\ElementInterface> $elementType + * @param class-string<\CraftCms\Cms\Element\Contracts\ElementInterface> $elementType * @param array $searchProperties * @param int $amount * @param bool $searchAll - * @return \craft\base\ElementInterface[] + * @return \CraftCms\Cms\Element\Contracts\ElementInterface[] */ public function assertElementsExist(string $elementType, array $searchProperties = [], int $amount = 1, bool $searchAll = false): array {} } diff --git a/yii2-adapter/tests-laravel/Legacy/NestedElementManagerCompatibilityTest.php b/yii2-adapter/tests-laravel/Legacy/NestedElementManagerCompatibilityTest.php index 32808894b76..f3465279708 100644 --- a/yii2-adapter/tests-laravel/Legacy/NestedElementManagerCompatibilityTest.php +++ b/yii2-adapter/tests-laravel/Legacy/NestedElementManagerCompatibilityTest.php @@ -4,11 +4,11 @@ namespace CraftCms\Yii2Adapter\Tests\Legacy; -use craft\base\ElementInterface; use craft\base\Event as YiiEvent; use craft\elements\NestedElementManager as LegacyNestedElementManager; use craft\events\BulkElementsEvent as LegacyBulkElementsEvent; use CraftCms\Cms\Address\Elements\Address; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Events\AfterSaveNestedElements; use CraftCms\Yii2Adapter\Tests\TestCase; use Illuminate\Support\Facades\Event; diff --git a/yii2-adapter/tests/fixtures/data/field-layout.php b/yii2-adapter/tests/fixtures/data/field-layout.php index 6c2013e25c2..b2c82c23594 100644 --- a/yii2-adapter/tests/fixtures/data/field-layout.php +++ b/yii2-adapter/tests/fixtures/data/field-layout.php @@ -14,7 +14,7 @@ use CraftCms\Cms\Field\Number; use CraftCms\Cms\Field\PlainText; use CraftCms\Cms\Field\Table; -use CraftCms\Cms\FieldLayout\LayoutElements\entries\EntryTitleField; +use CraftCms\Cms\FieldLayout\LayoutElements\Entries\EntryTitleField; use CraftCms\Cms\User\Elements\User; return [ diff --git a/yii2-adapter/tests/unit/elements/UserElementTest.php b/yii2-adapter/tests/unit/elements/UserElementTest.php index 90c085619ea..83a782344ad 100644 --- a/yii2-adapter/tests/unit/elements/UserElementTest.php +++ b/yii2-adapter/tests/unit/elements/UserElementTest.php @@ -19,8 +19,8 @@ use DateInterval; use DateTime; use DateTimeZone; +use Exception; use UnitTester; -use yii\base\Exception; /** * Unit tests for the User Element diff --git a/yii2-adapter/tests/unit/gql/TypeResolverTest.php b/yii2-adapter/tests/unit/gql/TypeResolverTest.php index 2d3a62a48bd..dfb9dd3607f 100644 --- a/yii2-adapter/tests/unit/gql/TypeResolverTest.php +++ b/yii2-adapter/tests/unit/gql/TypeResolverTest.php @@ -9,7 +9,6 @@ use ArrayObject; use Craft; -use craft\base\ElementInterface; use craft\elements\GlobalSet; use craft\gql\base\Resolver; use craft\gql\resolvers\elements\Asset as AssetResolver; @@ -19,6 +18,7 @@ use craft\test\mockclasses\elements\ExampleElement; use craft\test\TestCase; use CraftCms\Cms\Asset\Elements\Asset; +use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Element\Queries\ElementQuery; use CraftCms\Cms\Entry\Elements\Entry; use CraftCms\Cms\Support\Str; diff --git a/yii2-adapter/tests/unit/helpers/dbhelper/MysqlDbHelperTest.php b/yii2-adapter/tests/unit/helpers/dbhelper/MysqlDbHelperTest.php index 049e49dbc2b..6041a77cb6e 100644 --- a/yii2-adapter/tests/unit/helpers/dbhelper/MysqlDbHelperTest.php +++ b/yii2-adapter/tests/unit/helpers/dbhelper/MysqlDbHelperTest.php @@ -13,8 +13,8 @@ use craft\db\pgsql\Schema as PgsqlSchema; use craft\helpers\Db; use craft\test\TestCase; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use yii\base\Exception; -use yii\base\NotSupportedException; /** * Unit tests for the DB Helper class where its output may need to be mysql specific. Will be skipped if db isn't mysql. diff --git a/yii2-adapter/tests/unit/helpers/dbhelper/PgsqlDbHelperTest.php b/yii2-adapter/tests/unit/helpers/dbhelper/PgsqlDbHelperTest.php index 4fdea1ef6a5..8a42509c360 100644 --- a/yii2-adapter/tests/unit/helpers/dbhelper/PgsqlDbHelperTest.php +++ b/yii2-adapter/tests/unit/helpers/dbhelper/PgsqlDbHelperTest.php @@ -12,8 +12,8 @@ use craft\db\pgsql\Schema; use craft\helpers\Db; use craft\test\TestCase; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use yii\base\Exception; -use yii\base\NotSupportedException; /** * Unit tests for the DB Helper class where its output may need to be pgsql specific. Will be skipped if db isn't pgsql. diff --git a/yii2-adapter/tests/unit/validators/UsernameValidatorTest.php b/yii2-adapter/tests/unit/validators/UsernameValidatorTest.php index 6e64f2aa494..db29459fb9a 100644 --- a/yii2-adapter/tests/unit/validators/UsernameValidatorTest.php +++ b/yii2-adapter/tests/unit/validators/UsernameValidatorTest.php @@ -9,7 +9,7 @@ use craft\test\TestCase; use craft\validators\UsernameValidator; -use yii\base\NotSupportedException; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; /** * Class UsernameValidatorTest. diff --git a/yii2-adapter/tests/unit/web/twig/ExtensionTest.php b/yii2-adapter/tests/unit/web/twig/ExtensionTest.php index 0125a57d0e4..a6f7d3e7d89 100644 --- a/yii2-adapter/tests/unit/web/twig/ExtensionTest.php +++ b/yii2-adapter/tests/unit/web/twig/ExtensionTest.php @@ -23,6 +23,7 @@ use CraftCms\Cms\Field\PlainText; use CraftCms\Cms\FieldLayout\Models\FieldLayout; use CraftCms\Cms\ProjectConfig\ProjectConfig; +use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\Facades\EntryTypes; use CraftCms\Cms\User\Elements\User; use CraftCms\Cms\View\TemplateMode; @@ -45,7 +46,6 @@ use yii\base\ErrorException; use yii\base\Exception; use yii\base\InvalidConfigException; -use yii\base\NotSupportedException; use yii\web\ServerErrorHttpException; use function CraftCms\Cms\renderString; use function CraftCms\Cms\t;