diff --git a/CHANGELOG-v3.5.md b/CHANGELOG-v3.5.md new file mode 100644 index 00000000000..8ba2efcb09b --- /dev/null +++ b/CHANGELOG-v3.5.md @@ -0,0 +1,196 @@ +# Running Release Notes for Craft CMS 3.5 + +> {warning} If you have the `baseCpUrl` config setting set, Craft 3.5 will **only** allow the control panel to be accessed from that URL. + +### Added +- Added the “Show rounded icons” user preference. ([#5518](https://github.com/craftcms/cms/issues/5518)) +- Added the “Use shapes to represent statuses” user preference. ([#3293](https://github.com/craftcms/cms/issues/3293)) +- Added the “Suspend by default” user registration setting. ([#5830](https://github.com/craftcms/cms/issues/5830)) +- Added the ability to disable sites on the front end. ([#3005](https://github.com/craftcms/cms/issues/3005)) +- Soft-deleted elements now have a “Delete permanently” element action. ([#4420](https://github.com/craftcms/cms/issues/4420)) +- Assets now have a “Copy URL” element action. ([#2944](https://github.com/craftcms/cms/issues/2944)) +- Entry indexes can now show “Revision Notes” and “Last Edited By” columns. ([#5907](https://github.com/craftcms/cms/issues/5907)) +- Sections now have a new Propagation Method option, which gives entries control over which sites they should be saved to. ([#5988](https://github.com/craftcms/cms/issues/5988)) +- It’s now possible to set a custom route that handles Set Password requests. ([#5722](https://github.com/craftcms/cms/issues/5722)) +- Field labels now reveal their handles when the Option/ALT key is pressed. ([#5833](https://github.com/craftcms/cms/issues/5833)) +- Added the `allowedGraphqlOrigins` config setting. ([#5933](https://github.com/craftcms/cms/issues/5933)) +- Added the `brokenImagePath` config setting. ([#5877](https://github.com/craftcms/cms/issues/5877)) +- Added the `cpHeadTags` config setting, making it possible to give the control panel a custom favicon. ([#4003](https://github.com/craftcms/cms/issues/4003)) +- Added the `siteToken` config setting. +- Added the `install/check` command. ([#5810](https://github.com/craftcms/cms/issues/5810)) +- Added the `plugin/install`, `plugin/uninstall`, `plugin/enable`, and `plugin/disable` commands. ([#5817](https://github.com/craftcms/cms/issues/5817)) +- Added the `{% html %}` Twig tag, which makes it possible to register arbitrary HTML for inclusion in the ``, beginning of ``, or end of ``. ([#5955](https://github.com/craftcms/cms/issues/5955)) +- Added the `|diff` Twig filter. +- Added the `|explodeClass` Twig filter, which converts class names into an array. +- Added the `|explodeStyle` Twig filter, which converts CSS styles into an array of property/value pairs. +- Added the `|push` Twig filter, which returns a new array with one or more items appended to it. +- Added the `|unshift` Twig filter, which returns a new array with one or more items prepended to it. +- Added the `|where` Twig filter. +- Added the `raw()` Twig function, which wraps the given string in a `Twig\Markup` object to prevent it from getting HTML-encoded. +- Added support for eager-loading elements’ current revisions, via `currentRevision`. +- Added support for eager-loading drafts’ and revisions’ creators, via `draftCreator` and `revisionCreator`. +- Added support for the `CRAFT_CP` PHP constant. ([#5122](https://github.com/craftcms/cms/issues/5122)) +- Added support for [GraphQL mutations](https://docs.craftcms.com/v3/graphql.html#mutations). ([#4835](https://github.com/craftcms/cms/issues/4835)) +- Added the `drafts`, `draftOf`, `draftId`, `draftCreator`, `revisions`, `revisionOf`, `revisionId` and `revisionCreator` arguments to element queries using GraphQL API. ([#5580](https://github.com/craftcms/cms/issues/5580)) +- Added the `isDraft`, `isRevision`, `sourceId`, `sourceUid`, and `isUnsavedDraft` fields to elements when using GraphPQL API. ([#5580](https://github.com/craftcms/cms/issues/5580)) +- Added the `assetCount`, `categoryCount`, `entryCount`, `tagCount`, and `userCount` queries for fetching the element counts to the GraphPQL API. ([#4847](https://github.com/craftcms/cms/issues/4847)) +- Added the `locale` argument to the `formatDateTime` GraphQL directive. ([#5593](https://github.com/craftcms/cms/issues/5593)) +- Added support for specifying a transform on assets’ `width` and `height` fields via GraphQL. +- Added `craft\base\Element::EVENT_SET_EAGER_LOADED_ELEMENTS`. +- Added `craft\base\ElementInterface::getIconUrl()`. +- Added `craft\base\ElementInterface::gqlMutationNameByContext()`. +- Added `craft\base\ElementTrait::$elementSiteId`. +- Added `craft\behaviors\BaseRevisionBehavior`. +- Added `craft\base\FieldInterface::getContentGqlMutationArgumentType()`. +- Added `craft\base\FieldInterface::getContentGqlQueryArgumentType()`. +- Added `craft\config\GeneralConfig::getTestToEmailAddress()`. +- Added `craft\console\controllers\MailerController::$to`. +- Added `craft\controllers\AppController::actionBrokenImage()`. +- Added `craft\controllers\BaseEntriesController::enforceSitePermissions()`. +- Added `craft\elements\actions\CopyUrl`. +- Added `craft\elements\actions\Delete::$hard`. +- Added `craft\elements\Asset::getSrcset()`. ([#5774](https://github.com/craftcms/cms/issues/5774)) +- Added `craft\events\RegisterGqlMutationsEvent`. +- Added `craft\events\RegisterGqlSchemaComponentsEvent`. +- Added `craft\events\SetEagerLoadedElementsEvent`. +- Added `craft\gql\arguments\mutations\Asset`. +- Added `craft\gql\arguments\mutations\Draft`. +- Added `craft\gql\arguments\mutations\Entry`. +- Added `craft\gql\arguments\mutations\Structure`. +- Added `craft\gql\base\ElementMutationArguments`. +- Added `craft\gql\base\ElementMutationResolver`. +- Added `craft\gql\base\MutationArguments`. +- Added `craft\gql\base\MutationResolver`. +- Added `craft\gql\base\SingleGeneratorInterface`. +- Added `craft\gql\base\StructureMutationTrait`. +- Added `craft\gql\ElementQueryConditionBuilder`. +- Added `craft\gql\Mutation`. +- Added `craft\gql\mutations\Category`. +- Added `craft\gql\mutations\Entry`. +- Added `craft\gql\mutations\GlobalSet`. +- Added `craft\gql\mutations\Ping`. +- Added `craft\gql\mutations\Tag`. +- Added `craft\gql\resolvers\mutations\Asset`. +- Added `craft\gql\resolvers\mutations\Category`. +- Added `craft\gql\resolvers\mutations\Entry`. +- Added `craft\gql\resolvers\mutations\GlobalSet`. +- Added `craft\gql\resolvers\mutations\Tag`. +- Added `craft\gql\types\input\File`. +- Added `craft\gql\types\input\Matrix`. +- Added `craft\gql\types\Mutation`. +- Added `craft\gql\types\TableRow::prepareRowFieldDefinition()`. +- Added `craft\helpers\Assets::parseSrcsetSize()`. +- Added `craft\helpers\Assets::scaledDimensions()`. +- Added `craft\helpers\Console::ensureProjectConfigFileExists()`. +- Added `craft\helpers\Db::batchInsert()`. +- Added `craft\helpers\Db::delete()`. +- Added `craft\helpers\Db::insert()`. +- Added `craft\helpers\Db::replace()`. +- Added `craft\helpers\Db::update()`. +- Added `craft\helpers\Db::upsert()`. +- Added `craft\helpers\Gql::canMutateAssets()`. +- Added `craft\helpers\Gql::canMutateCategories()`. +- Added `craft\helpers\Gql::canMutateEntries()`. +- Added `craft\helpers\Gql::canMutateGlobalSets()`. +- Added `craft\helpers\Gql::canMutateTags()`. +- Added `craft\helpers\Gql::extractEntityAllowedActions()`. +- Added `craft\helpers\FileHelper::addFilesToZip()`. +- Added `craft\helpers\FileHelper::zip()`. +- Added `craft\helpers\Html::explodeClass()`. +- Added `craft\helpers\Html::explodeStyle()`. +- Added `craft\helpers\Html::id()`. +- Added `craft\helpers\Html::namespaceAttributes()`. +- Added `craft\helpers\Html::namespaceHtml()`. +- Added `craft\helpers\Html::namespaceId()`. +- Added `craft\helpers\Html::namespaceInputName()`. +- Added `craft\helpers\Html::namespaceInputs()`. +- Added `craft\helpers\MailerHelper::normalizeEmails()`. +- Added `craft\helpers\MailerHelper::settingsReport()`. +- Added `craft\helpers\Queue`. +- Added `craft\models\Section::PROPAGATION_METHOD_CUSTOM`. +- Added `craft\models\Site::$enabled`. +- Added `craft\services\Composer::handleError()`. +- Added `craft\services\Composer::run()`. +- Added `craft\services\Elements::createElementQuery()`. +- Added `craft\services\Gql::getAllSchemaComponents()`. +- Added `craft\services\ProjectConfig::$filename`. ([#5982](https://github.com/craftcms/cms/issues/5982)) +- Added `craft\queue\jobs\PruneRevisions`. +- Added `craft\test\mockclasses\elements\MockElementQuery`. +- Added `craft\web\AssetBundle\ContentWindowAsset`. +- Added `craft\web\AssetBundle\IframeResizerAsset`. +- Added `craft\web\Request::getAcceptsImage()`. +- Added `craft\web\Request::getFullUri()`. +- Added the `_includes/forms/password.html` control panel template. +- Added the `_includes/forms/copytext.html` control panel template. +- Added the `copytext` and `copytextField` macros to the `_includes/forms.html` control panel template. +- Added the `Craft.ui.createCopyTextInput()`, `createCopyTextField()`, and `createCopyTextPrompt()` JavaScript methods. +- Added the [iFrame Resizer](http://davidjbradshaw.github.io/iframe-resizer/) library. + +### Changed +- User registration forms in the control panel now give users the option to send an activation email, even if email verification isn’t required. ([#5836](https://github.com/craftcms/cms/issues/5836)) +- Activation emails are now sent automatically on public registration if the `deferPublicRegistrationPassword` config setting is enabled, even if email verification isn’t required. ([#5836](https://github.com/craftcms/cms/issues/5836)) +- Large asset thumbnails now use the same aspect ratio as the source image. ([#5515](https://github.com/craftcms/cms/issues/5515)) +- Preview frames now maintain their scroll position across refreshes, even for cross-origin preview targets. +- Preview targets that aren’t directly rendered by Craft must now include `lib/iframe-resizer-cw/iframeResizer.contentWindow.js` in order to maintain scroll position across refreshes. +- The preview frame header no longer hides the top 54px of the preview frame when it’s scrolled all the way to the top. ([#5547](https://github.com/craftcms/cms/issues/5547)) +- Modal backdrops no longer blur the page content. ([#5651](https://github.com/craftcms/cms/issues/5651)) +- Element editor HUDs now warn before switching to another site, if there are any unsaved content changes. ([#2512](https://github.com/craftcms/cms/issues/2512)) +- Improved the styling of password inputs in the control panel. +- Improved the UI for copying user activation URLs, asset reference tags, and GraphQL tokens’ authentication headers. +- Improved the wording of the meta info displayed in entry revision menus. ([#5889](https://github.com/craftcms/cms/issues/5889)) +- Plain Text fields are now sortable in the control panel. ([#5819](https://github.com/craftcms/cms/issues/5819)) +- Extra entry revisions (per the `maxRevisions` config setting) are now pruned via a background job. ([#5902](https://github.com/craftcms/cms/issues/5902)) +- Database backups created by the Database Backup utility are now saved as zip files. ([#5822](https://github.com/craftcms/cms/issues/5822)) +- It’s now possible to specify aliases when eager-loading elements via the `with` param. ([#5793](https://github.com/craftcms/cms/issues/5793)) +- It’s now possible to eager-load elements’ ancestors and parents. ([#1382](https://github.com/craftcms/cms/issues/1382)) +- The `cpTrigger` config setting can now be set to `null`. ([#5122](https://github.com/craftcms/cms/issues/5122)) +- The `pathParam` config setting can now be set to `null`. ([#5676](https://github.com/craftcms/cms/issues/5676)) +- If the `baseCpUrl` config setting is set, Craft will no longer treat any other base URLs as control panel requests, even if they contain the correct trigger segment. ([#5860](https://github.com/craftcms/cms/issues/5860)) +- The `mailer/test` command now only supports testing the current email settings. +- Reference tags can now provide a fallback value to be used if the reference can’t be resolved. ([#5589](https://github.com/craftcms/cms/issues/5589)) +- It’s no longer necessary to append the `|raw` filter after the `|namespace` filter. +- The `|namespace` Twig filter now namespaces ID selectors within ` + *

...

+ * ``` + * + * would become this, if it were namespaced with `foo`: + * + * ```html + * + *

...

+ * ``` + * + * @param string $html The HTML code + * @param string $namespace The namespace + * @param bool $withClasses Whether class names should be namespaced as well (affects both `class` attributes and class name CSS selectors) + * @return string The HTML with namespaced attributes + * @since 3.5.0 + * @see namespaceHtml() + * @see namespaceInputs() + */ + public static function namespaceAttributes(string $html, string $namespace, bool $withClasses = false): string + { + $markers = self::_escapeTextareas($html); + self::_namespaceAttributes($html, $namespace, $withClasses); + return self::_restoreTextareas($html, $markers); + } + + /** + * @param string $html + * @param string $namespace + * @param bool $withClasses + */ + private static function _namespaceAttributes(string &$html, string $namespace, bool $withClasses) + { + // normalize the namespace + $namespace = static::id($namespace); + + // normal HTML attributes + $html = preg_replace('/(?]*>)(.*?)(<\/style>)/is', function(array $matches) use ($namespace, $dlm) { + $html = preg_replace("/(?]*>)(.*?)(<\/textarea>)/is', function(array $matches) use (&$markers) { + $marker = '{marker:' . StringHelper::randomString() . '}'; + $markers[$marker] = $matches[2]; + return $matches[1] . $marker . $matches[3]; + }, $html); + return $markers; + } + + /** + * Replaces markers with textareas. + * + * @param string $html + * @param array $markers + * @return string + */ + private static function _restoreTextareas(string $html, array &$markers): string + { + return str_replace(array_keys($markers), array_values($markers), $html); + } } diff --git a/src/helpers/MailerHelper.php b/src/helpers/MailerHelper.php index 762c05bbe5a..22aaa374cc8 100644 --- a/src/helpers/MailerHelper.php +++ b/src/helpers/MailerHelper.php @@ -8,6 +8,7 @@ namespace craft\helpers; use Craft; +use craft\elements\User; use craft\errors\MissingComponentException; use craft\events\RegisterComponentTypesEvent; use craft\mail\Mailer; @@ -18,6 +19,7 @@ use craft\mail\transportadapters\TransportAdapterInterface; use craft\models\MailSettings; use yii\base\Event; +use yii\helpers\Inflector; /** * Class MailerHelper @@ -99,4 +101,115 @@ public static function createMailer(MailSettings $settings): Mailer $config = App::mailerConfig($settings); return Craft::createObject($config); } + + /** + * Normalizes To/From/CC/BCC values into an array of email addresses, or email/name pairs. + * + * @param string|array|User|User[]|null $emails + * @return array|null + * @since 3.5.0 + */ + public static function normalizeEmails($emails) + { + if (empty($emails)) { + return null; + } + + if (!is_array($emails)) { + $emails = [$emails]; + } + + $normalized = []; + + foreach ($emails as $key => $value) { + if ($value instanceof User) { + if (($name = $value->getFullName()) !== null) { + $normalized[$value->email] = $name; + } else { + $normalized[] = $value->email; + } + } else if (is_numeric($key)) { + $normalized[] = $value; + } else { + $normalized[$key] = $value; + } + } + + return $normalized; + } + + /** + * Returns a report of the settings used for the given Mailer instance. + * + * @param Mailer $mailer + * @param TransportAdapterInterface|null $transportAdapter + * @return string + * @since 3.5.0 + */ + public static function settingsReport(Mailer $mailer, TransportAdapterInterface $transportAdapter = null): string + { + $transport = $mailer->getTransport(); + $settings = [ + Craft::t('app', 'From') => self::_emailList($mailer->from), + Craft::t('app', 'Reply To') => self::_emailList($mailer->replyTo), + Craft::t('app', 'Template') => $mailer->template, + Craft::t('app', 'Transport Type') => get_class($transport), + ]; + + $transportSettings = []; + $security = Craft::$app->getSecurity(); + + // Use the transport adapter settings if it was sent + if ($transportAdapter !== null) { + /** @var BaseTransportAdapter $transportAdapter */ + foreach ($transportAdapter->settingsAttributes() as $name) { + $transportSettings[$transportAdapter->getAttributeLabel($name)] = $transportAdapter->$name; + } + } else { + // Otherwise just output whatever public properties we have available on the transport + foreach ((array)$transportAdapter as $name => $value) { + $transportSettings[Inflector::camel2words($name, true)] = $value; + } + } + + foreach ($transportSettings as $label => $value) { + if (is_scalar($value)) { + $settings[$label] = $security->redactIfSensitive($label, $value); + } else if (is_array($value)) { + $settings[$label] = 'Array'; + } else if (is_object($value)) { + $settings[$label] = 'Object'; + } + } + + $report = ''; + foreach ($settings as $label => $value) { + $report .= "- **$label:** $value\n"; + } + + return $report; + } + + /** + * Normalizes a list of emails and returns them in a comma-separated list. + * + * @param $emails + * @return string + */ + private static function _emailList($emails): string + { + $normalized = static::normalizeEmails($emails); + if ($normalized === null) { + return ''; + } + $list = []; + foreach ($normalized as $key => $value) { + if (is_numeric($key)) { + $list[] = $value; + } else { + $list[] = "$value <$key>"; + } + } + return implode(', ', $list); + } } diff --git a/src/helpers/Queue.php b/src/helpers/Queue.php new file mode 100644 index 00000000000..d9defd819ba --- /dev/null +++ b/src/helpers/Queue.php @@ -0,0 +1,44 @@ + + * @since 3.5.0 + */ +class Queue +{ + /** + * Pushes a job to the main app queue. + * + * @param JobInterface $job + * @param int|null $priority + * @return string|null The new job ID + */ + public static function push(JobInterface $job, int $priority = null) + { + $queue = Craft::$app->getQueue(); + + if ($priority !== null) { + return $queue->push($job); + } + + try { + return $queue->priority($priority)->push($job); + } catch (NotSupportedException $e) { + // The queue probably doesn't support custom push priorities. Try again without one. + return $queue->push($job); + } + } +} diff --git a/src/helpers/Sequence.php b/src/helpers/Sequence.php index 41346acab27..2656a35857c 100644 --- a/src/helpers/Sequence.php +++ b/src/helpers/Sequence.php @@ -56,13 +56,16 @@ public static function next(string $name, int $length = null) $num = self::_next($name); if ($num === 1) { - Craft::$app->getDb()->createCommand() - ->insert(Table::SEQUENCES, ['name' => $name, 'next' => $num + 1], false) - ->execute(); + Db::insert(Table::SEQUENCES, [ + 'name' => $name, + 'next' => $num + 1, + ], false); } else { - Craft::$app->getDb()->createCommand() - ->update(Table::SEQUENCES, ['next' => $num + 1], ['name' => $name], [], false) - ->execute(); + Db::update(Table::SEQUENCES, [ + 'next' => $num + 1, + ], [ + 'name' => $name, + ], [], false); } } catch (\Throwable $e) { $mutex->release($lockName); diff --git a/src/helpers/StringHelper.php b/src/helpers/StringHelper.php index 6f6a4bc5312..bc1b9c0d74c 100644 --- a/src/helpers/StringHelper.php +++ b/src/helpers/StringHelper.php @@ -9,6 +9,7 @@ use Craft; use Stringy\Stringy as BaseStringy; +use voku\helper\ASCII; use yii\base\Exception; use yii\base\InvalidConfigException; @@ -140,15 +141,13 @@ public static function asciiCharMap(bool $flat = false, string $language = null) // Include language specific replacements (unless the ASCII chars have custom mappings) if ($language !== null) { - $langSpecific = Stringy::getLangSpecificCharsArray($language); - if (!empty($langSpecific)) { - $generalConfig = Craft::$app->getConfig()->getGeneral(); - $customChars = !empty($generalConfig->customAsciiCharMappings) ? call_user_func_array('array_merge', $generalConfig->customAsciiCharMappings) : []; - $customChars = array_flip($customChars); - foreach ($langSpecific[0] as $i => $char) { - if (!isset($customChars[$char])) { - $flatMap[$char] = $langSpecific[1][$i]; - } + $langSpecific = ASCII::charsArrayWithOneLanguage($language); + $generalConfig = Craft::$app->getConfig()->getGeneral(); + $customChars = !empty($generalConfig->customAsciiCharMappings) ? call_user_func_array('array_merge', $generalConfig->customAsciiCharMappings) : []; + $customChars = array_flip($customChars); + foreach ($langSpecific['orig'] as $i => $char) { + if (!isset($customChars[$char])) { + $flatMap[$char] = $langSpecific['replace'][$i]; } } } @@ -1017,14 +1016,13 @@ public static function prepend(string $str, string $string): string * @param int $length The length of the random string. Defaults to 36. * @param bool $extendedChars Whether to include symbols in the random string. * @return string The randomly generated string. - * @throws \Exception */ public static function randomString(int $length = 36, bool $extendedChars = false): string { if ($extendedChars) { $validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890`~!@#$%^&*()-_=+[]\{}|;:\'",./<>?"'; } else { - $validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; + $validChars = 'abcdefghijklmnopqrstuvwxyz'; } return static::randomStringWithChars($validChars, $length); @@ -1038,7 +1036,6 @@ public static function randomString(int $length = 36, bool $extendedChars = fals * @param string $validChars A string containing the valid characters * @param int $length The length of the random string * @return string The randomly generated string. - * @throws \Exception */ public static function randomStringWithChars(string $validChars, int $length): string { @@ -1050,10 +1047,14 @@ public static function randomStringWithChars(string $validChars, int $length): s // repeat the steps until we've created a string of the right length for ($i = 0; $i < $length; $i++) { // pick a random number from 1 up to the number of valid chars - $randomPick = random_int(1, $numValidChars); + try { + $randomPick = random_int(0, $numValidChars - 1); + } catch (\Exception $e) { + $randomPick = rand(0, $numValidChars - 1); + } // take the random character out of the string of valid chars - $randomChar = $validChars[$randomPick - 1]; + $randomChar = $validChars[$randomPick]; // add the randomly-chosen char onto the end of our string $randomString .= $randomChar; @@ -1087,7 +1088,7 @@ public static function regexReplace(string $str, string $pattern, string $replac */ public static function removeHtml(string $str, string $allowableTags = null): string { - return (string)BaseStringy::create($str)->removeHtml($allowableTags); + return (string)BaseStringy::create($str)->removeHtml($allowableTags ?? ''); } /** diff --git a/src/helpers/Stringy.php b/src/helpers/Stringy.php index ce45d3d3d17..b241131e9a1 100644 --- a/src/helpers/Stringy.php +++ b/src/helpers/Stringy.php @@ -8,6 +8,7 @@ namespace craft\helpers; use Craft; +use voku\helper\ASCII; /** * The entire purpose of this class is so we can get at the charsArray in Stringy, which is a protected method @@ -15,6 +16,7 @@ * * @author Pixel & Tonic, Inc. * @since 3.0.0 + * @deprecated in 3.5.0 */ class Stringy extends \Stringy\Stringy { @@ -27,7 +29,11 @@ class Stringy extends \Stringy\Stringy */ public static function getLangSpecificCharsArray(string $language = 'en'): array { - return static::langSpecificCharsArray($language); + $array = ASCII::charsArrayWithOneLanguage($language); + return [ + $array['orig'], + $array['replace'], + ]; } /** @@ -50,7 +56,7 @@ protected function charsArray(): array { static $charsArray; return $charsArray ?? $charsArray = array_merge( - parent::charsArray(), + ASCII::charsArrayWithMultiLanguageValues(), Craft::$app->getConfig()->getGeneral()->customAsciiCharMappings ); } diff --git a/src/helpers/Template.php b/src/helpers/Template.php index ae635518c83..9f0e51e14b8 100644 --- a/src/helpers/Template.php +++ b/src/helpers/Template.php @@ -8,7 +8,6 @@ namespace craft\helpers; use Craft; -use craft\base\Element; use craft\base\ElementInterface; use craft\db\Paginator; use craft\i18n\Locale; @@ -218,7 +217,6 @@ private static function _profileToken(string $type, string $name, int $count): s */ private static function _includeElementInTemplateCaches(ElementInterface $element) { - /** @var Element $element */ $elementId = $element->id; // Don't initialize the TemplateCaches service if we don't have to diff --git a/src/helpers/UrlHelper.php b/src/helpers/UrlHelper.php index 7bd8e8e1830..eeac1c5e5dd 100644 --- a/src/helpers/UrlHelper.php +++ b/src/helpers/UrlHelper.php @@ -240,7 +240,7 @@ public static function url(string $path = '', $params = null, string $scheme = n $request = Craft::$app->getRequest(); if ($request->getIsCpRequest()) { - $path = Craft::$app->getConfig()->getGeneral()->cpTrigger . ($path ? '/' . $path : ''); + $path = static::prependCpTrigger($path); $cpUrl = true; } else { $cpUrl = false; @@ -270,7 +270,7 @@ public static function cpUrl(string $path = '', $params = null, string $scheme = } $path = trim($path, '/'); - $path = Craft::$app->getConfig()->getGeneral()->cpTrigger . ($path ? '/' . $path : ''); + $path = static::prependCpTrigger($path); return self::_createUrl($path, $params, $scheme, true); } @@ -337,12 +337,13 @@ public static function siteUrl(string $path = '', $params = null, string $scheme */ public static function actionUrl(string $path = '', $params = null, string $scheme = null): string { - $path = Craft::$app->getConfig()->getGeneral()->actionTrigger . '/' . trim($path, '/'); + $generalConfig = Craft::$app->getConfig()->getGeneral(); + $path = $generalConfig->actionTrigger . '/' . trim($path, '/'); $request = Craft::$app->getRequest(); if ($request->getIsCpRequest()) { - $path = Craft::$app->getConfig()->getGeneral()->cpTrigger . ($path ? '/' . $path : ''); + $path = static::prependCpTrigger($path); $cpUrl = true; } else { $cpUrl = false; @@ -353,7 +354,7 @@ public static function actionUrl(string $path = '', $params = null, string $sche $scheme = 'https'; } - return self::_createUrl($path, $params, $scheme, $cpUrl, true, false); + return self::_createUrl($path, $params, $scheme, $cpUrl, (bool)$generalConfig->pathParam, false); } /** @@ -541,6 +542,18 @@ public static function hostInfo(string $url): string return $host; } + /** + * Prepends the CP trigger onto the given path. + * + * @param string $path + * @return string + * @since 3.5.0 + */ + public static function prependCpTrigger(string $path): string + { + return implode('/', array_filter([Craft::$app->getConfig()->getGeneral()->cpTrigger, $path])); + } + // Deprecated Methods // ------------------------------------------------------------------------- @@ -554,7 +567,6 @@ public static function hostInfo(string $url): string */ public static function urlWithProtocol(string $url, string $scheme): string { - Craft::$app->getDeprecator()->log('UrlHelper::urlWithProtocol()', 'UrlHelper::urlWithProtocol() is deprecated. Use urlWithScheme() instead.'); return static::urlWithScheme($url, $scheme); } @@ -568,7 +580,6 @@ public static function urlWithProtocol(string $url, string $scheme): string */ public static function getProtocolForTokenizedUrl(): string { - Craft::$app->getDeprecator()->log('UrlHelper::getProtocolForTokenizedUrl()', 'UrlHelper::getProtocolForTokenizedUrl() is deprecated. Use getSchemeForTokenizedUrl() instead.'); return static::getSchemeForTokenizedUrl(); } @@ -639,7 +650,7 @@ private static function _createUrl(string $path, $params, string $scheme = null, } // Put it all together - if (!$showScriptName || $generalConfig->usePathInfo) { + if (!$showScriptName || $generalConfig->usePathInfo || !$generalConfig->pathParam) { if ($path) { $url = rtrim($baseUrl, '/') . '/' . trim($path, '/'); diff --git a/src/i18n/Formatter.php b/src/i18n/Formatter.php index d43374eb236..7517535e2c8 100644 --- a/src/i18n/Formatter.php +++ b/src/i18n/Formatter.php @@ -188,12 +188,14 @@ public function asDatetime($value, $format = null): string * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the * PHP [date()](http://php.net/manual/en/function.date.php)-function. + * @param bool $withPreposition Whether a preposition should be included in the returned string + * (e.g. “**at** 12:00 PM” or “**on** Wednesday”). * @return string the formatted result. * @throws InvalidArgumentException if the input value can not be evaluated as a date value. * @throws InvalidConfigException if the date format is invalid. * @see datetimeFormat */ - public function asTimestamp($value, string $format = null): string + public function asTimestamp($value, string $format = null, bool $withPreposition = false): string { /** @var DateTime $timestamp */ /** @var bool $hasTimeInfo */ @@ -202,22 +204,28 @@ public function asTimestamp($value, string $format = null): string // If it's today or missing date info, just return the local time. if (!$hasDateInfo || DateTimeHelper::isToday($timestamp)) { - return $hasTimeInfo ? $this->asTime($timestamp, $format) : Craft::t('app', 'Today'); + if ($hasTimeInfo) { + $time = $this->asTime($timestamp, $format); + return $withPreposition ? Craft::t('app', 'at {time}', ['time' => $time]) : $time; + } + return $withPreposition ? Craft::t('app', 'today') : Craft::t('app', 'Today'); } // If it was yesterday, display 'Yesterday' if (DateTimeHelper::isYesterday($timestamp)) { - return Craft::t('app', 'Yesterday'); + return $withPreposition ? Craft::t('app', 'yesterday') : Craft::t('app', 'Yesterday'); } // If it were up to 7 days ago, display the weekday name. if (DateTimeHelper::isWithinLast($timestamp, '7 days')) { $day = $timestamp->format('w'); - return Craft::$app->getI18n()->getLocaleById($this->locale)->getWeekDayName($day); + $dayName = Craft::$app->getI18n()->getLocaleById($this->locale)->getWeekDayName($day); + return $withPreposition ? Craft::t('app', 'on {day}', ['day' => $dayName]) : $dayName; } // Otherwise, just return the local date. - return $this->asDate($timestamp, $format); + $date = $this->asDate($timestamp, $format); + return $withPreposition ? Craft::t('app', 'on {date}', ['date' => $date]) : $date; } /** diff --git a/src/image/Raster.php b/src/image/Raster.php index 6063d4a99e7..573bc874866 100644 --- a/src/image/Raster.php +++ b/src/image/Raster.php @@ -159,7 +159,7 @@ public function loadImage(string $path) $mimeType = FileHelper::getMimeType($path, null, false); if ($mimeType !== null && strpos($mimeType, 'image/') !== 0 && strpos($mimeType, 'application/pdf') !== 0) { - throw new ImageException(Craft::t('app', 'The file “{name}” does not appear to be an image.', ['name' => pathinfo($path, PATHINFO_BASENAME)])); + throw new ImageException(Craft::t('app', 'The file “{name}” does not appear to be an image.', ['name' => basename($path)])); } try { diff --git a/src/mail/Mailer.php b/src/mail/Mailer.php index aaf59ed9360..a6a91cda337 100644 --- a/src/mail/Mailer.php +++ b/src/mail/Mailer.php @@ -151,17 +151,9 @@ public function send($message) } // Apply the testToEmailAddress config setting - $testToEmailAddress = Craft::$app->getConfig()->getGeneral()->testToEmailAddress; + $testToEmailAddress = Craft::$app->getConfig()->getGeneral()->getTestToEmailAddress(); if (!empty($testToEmailAddress)) { - $to = []; - foreach ((array)$testToEmailAddress as $emailAddress => $name) { - if (is_numeric($emailAddress)) { - $to[$name] = Craft::t('app', 'Test Recipient'); - } else { - $to[$emailAddress] = $name; - } - } - $message->setTo($to); + $message->setTo($testToEmailAddress); $message->setCc(null); $message->setBcc(null); } diff --git a/src/mail/Message.php b/src/mail/Message.php index 3ec10ff6112..899150145af 100644 --- a/src/mail/Message.php +++ b/src/mail/Message.php @@ -8,6 +8,7 @@ namespace craft\mail; use craft\elements\User; +use craft\helpers\MailerHelper; /** * Represents an email message. @@ -43,9 +44,7 @@ class Message extends \yii\swiftmailer\Message */ public function setFrom($from) { - $from = $this->_normalizeEmails($from); - parent::setFrom($from); - + parent::setFrom(MailerHelper::normalizeEmails($from)); return $this; } @@ -61,9 +60,7 @@ public function setFrom($from) */ public function setReplyTo($replyTo) { - $replyTo = $this->_normalizeEmails($replyTo); - parent::setReplyTo($replyTo); - + parent::setReplyTo(MailerHelper::normalizeEmails($replyTo)); return $this; } @@ -86,9 +83,7 @@ public function setTo($to) $this->variables['user'] = $to; } - $to = $this->_normalizeEmails($to); - parent::setTo($to); - + parent::setTo(MailerHelper::normalizeEmails($to)); return $this; } @@ -103,9 +98,7 @@ public function setTo($to) */ public function setCc($cc) { - $cc = $this->_normalizeEmails($cc); - parent::setCc($cc); - + parent::setCc(MailerHelper::normalizeEmails($cc)); return $this; } @@ -120,42 +113,7 @@ public function setCc($cc) */ public function setBcc($bcc) { - $bcc = $this->_normalizeEmails($bcc); - parent::setBcc($bcc); - + parent::setBcc(MailerHelper::normalizeEmails($bcc)); return $this; } - - /** - * @param string|array|User|User[]|null $emails - * @return string|array - */ - private function _normalizeEmails($emails) - { - if (empty($emails)) { - return null; - } - - if (!is_array($emails)) { - $emails = [$emails]; - } - - $normalized = []; - - foreach ($emails as $key => $value) { - if ($value instanceof User) { - if (($name = $value->getFullName()) !== null) { - $normalized[$value->email] = $name; - } else { - $normalized[] = $value->email; - } - } else if (is_numeric($key)) { - $normalized[] = $value; - } else { - $normalized[$key] = $value; - } - } - - return $normalized; - } } diff --git a/src/migrations/Install.php b/src/migrations/Install.php index f7cab88078c..8fb2827ddc9 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -9,7 +9,6 @@ namespace craft\migrations; use Craft; -use craft\base\Plugin; use craft\db\Migration; use craft\db\Table; use craft\elements\Asset; @@ -121,8 +120,8 @@ public function createTables() 'format' => $this->string(), 'location' => $this->string()->notNull(), 'volumeId' => $this->integer(), - 'fileExists' => $this->boolean()->defaultValue(false)->notNull(), - 'inProgress' => $this->boolean()->defaultValue(false)->notNull(), + 'fileExists' => $this->boolean()->notNull()->defaultValue(false), + 'inProgress' => $this->boolean()->notNull()->defaultValue(false), 'dateIndexed' => $this->dateTime(), 'dateCreated' => $this->dateTime()->notNull(), 'dateUpdated' => $this->dateTime()->notNull(), @@ -169,7 +168,7 @@ public function createTables() 'id' => $this->primaryKey(), 'groupId' => $this->integer()->notNull(), 'siteId' => $this->integer()->notNull(), - 'hasUrls' => $this->boolean()->defaultValue(true)->notNull(), + 'hasUrls' => $this->boolean()->notNull()->defaultValue(true), 'uriFormat' => $this->text(), 'template' => $this->string(500), 'dateCreated' => $this->dateTime()->notNull(), @@ -231,7 +230,7 @@ public function createTables() 'creatorId' => $this->integer(), 'name' => $this->string()->notNull(), 'notes' => $this->text(), - 'trackChanges' => $this->boolean()->defaultValue(false)->notNull(), + 'trackChanges' => $this->boolean()->notNull()->defaultValue(false), 'dateLastMerged' => $this->dateTime(), ]); $this->createTable(Table::ELEMENTINDEXSETTINGS, [ @@ -248,8 +247,8 @@ public function createTables() 'revisionId' => $this->integer(), 'fieldLayoutId' => $this->integer(), 'type' => $this->string()->notNull(), - 'enabled' => $this->boolean()->defaultValue(true)->notNull(), - 'archived' => $this->boolean()->defaultValue(false)->notNull(), + 'enabled' => $this->boolean()->notNull()->defaultValue(true), + 'archived' => $this->boolean()->notNull()->defaultValue(false), 'dateCreated' => $this->dateTime()->notNull(), 'dateUpdated' => $this->dateTime()->notNull(), 'dateDeleted' => $this->dateTime()->null(), @@ -261,7 +260,7 @@ public function createTables() 'siteId' => $this->integer()->notNull(), 'slug' => $this->string(), 'uri' => $this->string(), - 'enabled' => $this->boolean()->defaultValue(true)->notNull(), + 'enabled' => $this->boolean()->notNull()->defaultValue(true), 'dateCreated' => $this->dateTime()->notNull(), 'dateUpdated' => $this->dateTime()->notNull(), 'uid' => $this->uid(), @@ -540,6 +539,7 @@ public function createTables() 'id' => $this->primaryKey(), 'groupId' => $this->integer()->notNull(), 'primary' => $this->boolean()->notNull(), + 'enabled' => $this->boolean()->notNull()->defaultValue(true), 'name' => $this->string()->notNull(), 'handle' => $this->string()->notNull(), 'language' => $this->string(12)->notNull(), @@ -917,10 +917,10 @@ public function createIndexes() $this->addPrimaryKey($this->db->getIndexName(Table::SEARCHINDEX, 'elementId,attribute,fieldId,siteId', true), Table::SEARCHINDEX, 'elementId,attribute,fieldId,siteId'); - $sql = 'CREATE INDEX ' . $this->db->quoteTableName($this->db->getIndexName(Table::SEARCHINDEX, 'keywords_vector')) . ' ON {{%searchindex}} USING GIN([[keywords_vector]] [[pg_catalog]].[[tsvector_ops]]) WITH (FASTUPDATE=YES)'; + $sql = 'CREATE INDEX ' . $this->db->quoteTableName($this->db->getIndexName(Table::SEARCHINDEX, 'keywords_vector')) . ' ON ' . Table::SEARCHINDEX . ' USING GIN([[keywords_vector]] [[pg_catalog]].[[tsvector_ops]]) WITH (FASTUPDATE=YES)'; $this->db->createCommand($sql)->execute(); - $sql = 'CREATE INDEX ' . $this->db->quoteTableName($this->db->getIndexName(Table::SEARCHINDEX, 'keywords')) . ' ON {{%searchindex}} USING btree(keywords)'; + $sql = 'CREATE INDEX ' . $this->db->quoteTableName($this->db->getIndexName(Table::SEARCHINDEX, 'keywords')) . ' ON ' . Table::SEARCHINDEX . ' USING btree(keywords)'; $this->db->createCommand($sql)->execute(); } } @@ -1047,7 +1047,7 @@ public function insertDefaultData() // Compare existing Craft schema version with the one that is being applied. if (!version_compare($craftSchemaVersion, $expectedSchemaVersion, '=')) { - throw new InvalidConfigException("Craft is installed at the wrong schema version ({$craftSchemaVersion}, but project.yaml lists {$expectedSchemaVersion})."); + throw new InvalidConfigException("Craft is installed at the wrong schema version ({$craftSchemaVersion}, but $projectConfig->filename lists {$expectedSchemaVersion})."); } // Make sure at least sites are processed @@ -1060,8 +1060,8 @@ public function insertDefaultData() Craft::$app->getErrorHandler()->logException($e); // Rename project.yaml so we can create a new one - $backupFile = pathinfo(ProjectConfig::CONFIG_FILENAME, PATHINFO_FILENAME) . date('-Ymh-His') . '.yaml'; - echo " > renaming project.yaml to {$backupFile} and moving to config backup folder ... "; + $backupFile = pathinfo($projectConfig->filename, PATHINFO_FILENAME) . date('-Ymh-His') . '.yaml'; + echo " > renaming $projectConfig->filename to $backupFile and moving to config backup folder ... "; rename($configFile, Craft::$app->getPath()->getConfigBackupPath() . '/' . $backupFile); echo "done\n"; @@ -1140,9 +1140,8 @@ private function _installPlugins() $plugin = $pluginsService->createPlugin($handle); $expectedSchemaVersion = $projectConfig->get(Plugins::CONFIG_PLUGINS_KEY . '.' . $handle . '.schemaVersion', true); - /** @var Plugin|null $plugin */ if ($plugin->schemaVersion && $expectedSchemaVersion && $plugin->schemaVersion != $expectedSchemaVersion) { - throw new InvalidPluginException($handle, "{$handle} is installed at the wrong schema version ({$plugin->schemaVersion}, but project.yaml lists {$expectedSchemaVersion})."); + throw new InvalidPluginException($handle, "{$handle} is installed at the wrong schema version ({$plugin->schemaVersion}, but $projectConfig->filename lists {$expectedSchemaVersion})."); } } diff --git a/src/migrations/m151002_095935_volume_cache_settings.php b/src/migrations/m151002_095935_volume_cache_settings.php index 4e1a5e6855d..c0019ff4d4e 100644 --- a/src/migrations/m151002_095935_volume_cache_settings.php +++ b/src/migrations/m151002_095935_volume_cache_settings.php @@ -2,10 +2,10 @@ namespace craft\migrations; -use Craft; use craft\db\Migration; use craft\db\Query; use craft\db\Table; +use craft\helpers\Db; use craft\helpers\Json; /** @@ -35,12 +35,11 @@ public function safeUp() if (!empty($settings['expires']) && preg_match('/(\d+)([a-z]+)/', $settings['expires'], $matches)) { $settings['expires'] = $matches[1] . ' ' . $matches[2]; - Craft::$app->getDb()->createCommand() - ->update( - Table::VOLUMES, - ['settings' => Json::encode($settings)], - ['id' => $volume['id']]) - ->execute(); + Db::update(Table::VOLUMES, [ + 'settings' => Json::encode($settings) + ], [ + 'id' => $volume['id'], + ], [], true, $this->db); } } } diff --git a/src/migrations/m151005_142750_volume_s3_storage_settings.php b/src/migrations/m151005_142750_volume_s3_storage_settings.php index 163a715eb1f..498a5442295 100644 --- a/src/migrations/m151005_142750_volume_s3_storage_settings.php +++ b/src/migrations/m151005_142750_volume_s3_storage_settings.php @@ -2,10 +2,10 @@ namespace craft\migrations; -use Craft; use craft\db\Migration; use craft\db\Query; use craft\db\Table; +use craft\helpers\Db; use craft\helpers\Json; /** @@ -31,12 +31,11 @@ public function safeUp() if (empty($settings['storageClass'])) { $settings['storageClass'] = 'STANDARD'; // value of \craft\base\Volume::STORAGE_STANDARD - Craft::$app->getDb()->createCommand() - ->update( - Table::VOLUMES, - ['settings' => Json::encode($settings)], - ['id' => $volume['id']]) - ->execute(); + Db::update(Table::VOLUMES, [ + 'settings' => Json::encode($settings) + ], [ + 'id' => $volume['id'], + ], [], true, $this->db); } } } diff --git a/src/migrations/m160804_110002_userphotos_to_assets.php b/src/migrations/m160804_110002_userphotos_to_assets.php index d81b242455b..0d86f5f6371 100644 --- a/src/migrations/m160804_110002_userphotos_to_assets.php +++ b/src/migrations/m160804_110002_userphotos_to_assets.php @@ -193,9 +193,7 @@ private function _createUserphotoVolume(): int ]; $db = Craft::$app->getDb(); - $db->createCommand() - ->insert(Table::VOLUMES, $volumeData) - ->execute(); + Db::insert(Table::VOLUMES, $volumeData); $volumeId = $db->getLastInsertID(); @@ -205,9 +203,7 @@ private function _createUserphotoVolume(): int 'name' => $name, 'path' => null ]; - $db->createCommand() - ->insert(Table::VOLUMEFOLDERS, $folderData) - ->execute(); + Db::insert(Table::VOLUMEFOLDERS, $folderData); return $volumeId; } @@ -266,8 +262,8 @@ private function _convertPhotosToAssets(int $volumeId, array $userList): array $assetExists = (new Query()) ->select(['assets.id']) - ->from(['{{%assets}} assets']) - ->innerJoin('{{%volumefolders}} volumefolders', '[[volumefolders.id]] = [[assets.folderId]]') + ->from(['assets' => Table::ASSETS]) + ->innerJoin(['volumefolders' => Table::VOLUMEFOLDERS], '[[volumefolders.id]] = [[assets.folderId]]') ->where([ 'assets.folderId' => $folderId, 'filename' => $user['photo'] @@ -280,9 +276,7 @@ private function _convertPhotosToAssets(int $volumeId, array $userList): array 'enabled' => 1, 'archived' => 0 ]; - $db->createCommand() - ->insert(Table::ELEMENTS, $elementData) - ->execute(); + Db::insert(Table::ELEMENTS, $elementData); $elementId = $db->getLastInsertID(); @@ -294,18 +288,14 @@ private function _convertPhotosToAssets(int $volumeId, array $userList): array 'uri' => null, 'enabled' => 1 ]; - $db->createCommand() - ->insert('{{%elements_i18n}}', $elementI18nData) - ->execute(); + Db::insert('{{%elements_i18n}}', $elementI18nData); $contentData = [ 'elementId' => $elementId, 'locale' => $locale, 'title' => AssetsHelper::filename2Title(pathinfo($user['photo'], PATHINFO_FILENAME)) ]; - $db->createCommand() - ->insert(Table::CONTENT, $contentData) - ->execute(); + Db::insert(Table::CONTENT, $contentData); } $imageSize = Image::imageSize($filePath); @@ -320,9 +310,7 @@ private function _convertPhotosToAssets(int $volumeId, array $userList): array 'height' => $imageSize[1], 'dateModified' => Db::prepareDateForDb(filemtime($filePath)) ]; - $db->createCommand() - ->insert(Table::ASSETS, $assetData) - ->execute(); + Db::insert(Table::ASSETS, $assetData); $changes[$user['id']] = $elementId; } @@ -339,11 +327,12 @@ private function _convertPhotosToAssets(int $volumeId, array $userList): array private function _setPhotoIdValues(array $userlist) { if (is_array($userlist)) { - $db = Craft::$app->getDb(); foreach ($userlist as $userId => $assetId) { - $db->createCommand() - ->update(Table::USERS, ['photoId' => $assetId], ['id' => $userId]) - ->execute(); + Db::update(Table::USERS, [ + 'photoId' => $assetId, + ], [ + 'id' => $userId, + ], [], true, $this->db); } } } diff --git a/src/migrations/m160807_144858_sites.php b/src/migrations/m160807_144858_sites.php index 8835fa39a3c..50c8440c021 100644 --- a/src/migrations/m160807_144858_sites.php +++ b/src/migrations/m160807_144858_sites.php @@ -529,7 +529,7 @@ protected function locale2handle(string $locale): string $localeParts = array_filter(preg_split('/[^a-zA-Z0-9]/', $locale)); // Prefix with a random string so there's no chance of a conflict with other locales - return StringHelper::randomStringWithChars('abcdefghijklmnopqrstuvwxyz', 7) . ($localeParts ? '_' . implode('_', $localeParts) : ''); + return StringHelper::randomString(7) . ($localeParts ? '_' . implode('_', $localeParts) : ''); } return $locale; diff --git a/src/migrations/m160829_000000_pending_user_content_cleanup.php b/src/migrations/m160829_000000_pending_user_content_cleanup.php index 20f72654e99..ccae5b75cff 100644 --- a/src/migrations/m160829_000000_pending_user_content_cleanup.php +++ b/src/migrations/m160829_000000_pending_user_content_cleanup.php @@ -22,8 +22,8 @@ public function safeUp(): bool // Find any orphaned entries. $ids = (new Query()) ->select(['el.id']) - ->from(['{{%elements}} el']) - ->leftJoin('{{%entries}} en', '[[en.id]] = [[el.id]]') + ->from(['el' => Table::ELEMENTS]) + ->leftJoin(['en' => Table::ENTRIES], '[[en.id]] = [[el.id]]') ->where([ 'el.type' => Entry::class, 'en.id' => null diff --git a/src/migrations/m160912_230520_require_entry_type_id.php b/src/migrations/m160912_230520_require_entry_type_id.php index fc31752b3f2..8c5bda2fc90 100644 --- a/src/migrations/m160912_230520_require_entry_type_id.php +++ b/src/migrations/m160912_230520_require_entry_type_id.php @@ -21,7 +21,7 @@ public function safeUp() // Get all of the sections' primary entry type IDs $subQuery = (new Query()) ->select(['et.id']) - ->from(['{{%entrytypes}} et']) + ->from(['et' => Table::ENTRYTYPES]) ->where('[[et.sectionId]] = [[s.id]]') ->orderBy(['sortOrder' => SORT_ASC]) ->limit(1); @@ -31,7 +31,7 @@ public function safeUp() 'sectionId' => 's.id', 'typeId' => $subQuery ]) - ->from(['{{%sections}} s']) + ->from(['s' => Table::SECTIONS]) ->all($this->db); if (!empty($results)) { diff --git a/src/migrations/m170306_150500_asset_temporary_uploads.php b/src/migrations/m170306_150500_asset_temporary_uploads.php index 5baf356b319..64b4bdb7a0c 100644 --- a/src/migrations/m170306_150500_asset_temporary_uploads.php +++ b/src/migrations/m170306_150500_asset_temporary_uploads.php @@ -26,8 +26,8 @@ public function safeUp() // Get indexed temporary uploads $assets = (new Query()) ->select(['assets.id', 'assets.filename', 'assets.folderId', 'volumeFolders.path']) - ->from('{{%assets}} assets') - ->innerJoin('{{%volumefolders}} volumeFolders', $folderId . ' = ' . $volumeFoldersId) + ->from(['assets' => Table::ASSETS]) + ->innerJoin(['volumeFolders' => Table::VOLUMEFOLDERS], $folderId . ' = ' . $volumeFoldersId) ->where(['assets.volumeId' => null]) ->all($this->db); diff --git a/src/migrations/m171107_000000_assign_group_permissions.php b/src/migrations/m171107_000000_assign_group_permissions.php index 71ba5ae45c6..1ebd6dc23c4 100644 --- a/src/migrations/m171107_000000_assign_group_permissions.php +++ b/src/migrations/m171107_000000_assign_group_permissions.php @@ -19,15 +19,15 @@ public function safeUp() // See which users & groups already have the "assignUserPermissions" permission $userIds = (new Query()) ->select(['up_u.userId']) - ->from(['{{%userpermissions_users}} up_u']) - ->innerJoin('{{%userpermissions}} up', '[[up.id]] = [[up_u.permissionId]]') + ->from(['up_u' => Table::USERPERMISSIONS_USERS]) + ->innerJoin(['up' => Table::USERPERMISSIONS], '[[up.id]] = [[up_u.permissionId]]') ->where(['up.name' => 'assignuserpermissions']) ->column($this->db); $groupIds = (new Query()) ->select(['up_ug.groupId']) - ->from(['{{%userpermissions_usergroups}} up_ug']) - ->innerJoin('{{%userpermissions}} up', '[[up.id]] = [[up_ug.permissionId]]') + ->from(['up_ug' => Table::USERPERMISSIONS_USERGROUPS]) + ->innerJoin(['up' => Table::USERPERMISSIONS], '[[up.id]] = [[up_ug.permissionId]]') ->where(['up.name' => 'assignuserpermissions']) ->column($this->db); diff --git a/src/migrations/m180128_235202_set_tag_slugs.php b/src/migrations/m180128_235202_set_tag_slugs.php index e51b1439604..00ca87a1bb8 100644 --- a/src/migrations/m180128_235202_set_tag_slugs.php +++ b/src/migrations/m180128_235202_set_tag_slugs.php @@ -2,9 +2,9 @@ namespace craft\migrations; -use Craft; use craft\db\Migration; use craft\elements\Tag; +use craft\helpers\Queue; use craft\queue\jobs\ResaveElements; /** @@ -17,7 +17,7 @@ class m180128_235202_set_tag_slugs extends Migration */ public function safeUp() { - Craft::$app->getQueue()->push(new ResaveElements([ + Queue::push(new ResaveElements([ 'elementType' => Tag::class, 'criteria' => [ 'slug' => ':empty:', diff --git a/src/migrations/m180521_173000_initial_yml_and_snapshot.php b/src/migrations/m180521_173000_initial_yml_and_snapshot.php index 76a18a00532..df45c7a286d 100644 --- a/src/migrations/m180521_173000_initial_yml_and_snapshot.php +++ b/src/migrations/m180521_173000_initial_yml_and_snapshot.php @@ -30,8 +30,8 @@ public function safeUp() $configFile = Craft::$app->getPath()->getProjectConfigFilePath(); if (file_exists($configFile)) { // Make a backup of the old config - $backupFile = pathinfo(ProjectConfig::CONFIG_FILENAME, PATHINFO_FILENAME) . date('-Ymh-His') . '.yaml'; - echo " > renaming project.yaml to {$backupFile} and moving to config backup folder ... "; + $backupFile = pathinfo($projectConfig->filename, PATHINFO_FILENAME) . date('-Ymh-His') . '.yaml'; + echo " > renaming $projectConfig->filename to $backupFile and moving to config backup folder ... "; rename($configFile, Craft::$app->getPath()->getConfigBackupPath() . '/' . $backupFile); // Forget everything we knew about the old config @@ -134,8 +134,8 @@ private function _getSiteData(): array 'sites.primary', 'siteGroups.uid AS siteGroup', ]) - ->from(['{{%sites}} sites']) - ->innerJoin('{{%sitegroups}} siteGroups', '[[sites.groupId]] = [[siteGroups.id]]') + ->from(['sites' => Table::SITES]) + ->innerJoin(['siteGroups' => Table::SITEGROUPS], '[[siteGroups.id]] = [[sites.groupId]]') ->all(); foreach ($sites as $site) { @@ -171,8 +171,8 @@ private function _getSectionData(): array 'structures.uid AS structure', 'structures.maxLevels AS structureMaxLevels', ]) - ->from(['{{%sections}} sections']) - ->leftJoin('{{%structures}} structures', '[[structures.id]] = [[sections.structureId]]') + ->from(['sections' => Table::SECTIONS]) + ->leftJoin(['structures' => Table::STRUCTURES], '[[structures.id]] = [[sections.structureId]]') ->all(); $sectionData = []; @@ -207,9 +207,9 @@ private function _getSectionData(): array 'sites.uid AS siteUid', 'sections.uid AS sectionUid', ]) - ->from(['{{%sections_sites}} sections_sites']) - ->innerJoin('{{%sites}} sites', '[[sites.id]] = [[sections_sites.siteId]]') - ->innerJoin('{{%sections}} sections', '[[sections.id]] = [[sections_sites.sectionId]]') + ->from(['sections_sites' => Table::SECTIONS_SITES]) + ->innerJoin(['sites' => Table::SITES], '[[sites.id]] = [[sections_sites.siteId]]') + ->innerJoin(['sections' => Table::SECTIONS], '[[sections.id]] = [[sections_sites.sectionId]]') ->all(); foreach ($sectionSiteRows as $sectionSiteRow) { @@ -235,8 +235,8 @@ private function _getSectionData(): array 'entrytypes.uid', 'sections.uid AS sectionUid', ]) - ->from(['{{%entrytypes}} as entrytypes']) - ->innerJoin('{{%sections}} sections', '[[sections.id]] = [[entrytypes.sectionId]]') + ->from(['entrytypes' => Table::ENTRYTYPES]) + ->innerJoin(['sections' => Table::SECTIONS], '[[sections.id]] = [[entrytypes.sectionId]]') ->all(); $layoutIds = array_filter(ArrayHelper::getColumn($entryTypeRows, 'fieldLayoutId')); @@ -310,8 +310,8 @@ private function _getFieldData(): array 'fields.uid', 'fieldGroups.uid AS fieldGroup', ]) - ->from(['{{%fields}} fields']) - ->leftJoin('{{%fieldgroups}} fieldGroups', '[[fields.groupId]] = [[fieldGroups.id]]') + ->from(['fields' => Table::FIELDS]) + ->leftJoin(['fieldGroups' => Table::FIELDGROUPS], '[[fieldGroups.id]] = [[fields.groupId]]') ->all(); $fields = []; @@ -356,8 +356,8 @@ private function _getMatrixBlockTypeData(): array 'bt.uid', 'f.uid AS field', ]) - ->from(['{{%matrixblocktypes}} bt']) - ->innerJoin('{{%fields}} f', '[[bt.fieldId]] = [[f.id]]') + ->from(['bt' => Table::MATRIXBLOCKTYPES]) + ->innerJoin(['f' => Table::FIELDS], '[[f.id]] = [[bt.fieldId]]') ->all(); $layoutIds = []; @@ -408,7 +408,7 @@ private function _getVolumeData(): array 'volumes.sortOrder', 'volumes.uid', ]) - ->from(['{{%volumes}} volumes']) + ->from(['volumes' => Table::VOLUMES]) ->all(); $layoutIds = []; @@ -536,8 +536,8 @@ private function _getCategoryGroupData(): array 'structures.uid AS structure', 'structures.maxLevels AS structureMaxLevels', ]) - ->from(['{{%categorygroups}} groups']) - ->leftJoin('{{%structures}} structures', '[[structures.id]] = [[groups.structureId]]') + ->from(['groups' => Table::CATEGORYGROUPS]) + ->leftJoin(['structures' => Table::STRUCTURES], '[[structures.id]] = [[groups.structureId]]') ->all(); $groupData = []; @@ -581,9 +581,9 @@ private function _getCategoryGroupData(): array 'sites.uid AS siteUid', 'groups.uid AS groupUid', ]) - ->from(['{{%categorygroups_sites}} groups_sites']) - ->innerJoin('{{%sites}} sites', '[[sites.id]] = [[groups_sites.siteId]]') - ->innerJoin('{{%categorygroups}} groups', '[[groups.id]] = [[groups_sites.groupId]]') + ->from(['groups_sites' => Table::CATEGORYGROUPS_SITES]) + ->innerJoin(['sites' => Table::SITES], '[[sites.id]] = [[groups_sites.siteId]]') + ->innerJoin(['groups' => Table::CATEGORYGROUPS], '[[groups.id]] = [[groups_sites.groupId]]') ->all(); foreach ($groupSiteRows as $groupSiteRow) { @@ -613,7 +613,7 @@ private function _getTagGroupData(): array 'groups.uid', 'groups.fieldLayoutId', ]) - ->from(['{{%taggroups}} groups']) + ->from(['groups' => Table::TAGGROUPS]) ->all(); $groupData = []; @@ -655,7 +655,7 @@ private function _getGlobalSetData(): array 'sets.uid', 'sets.fieldLayoutId', ]) - ->from(['{{%globalsets}} sets']) + ->from(['sets' => Table::GLOBALSETS]) ->all(); $setData = []; @@ -753,10 +753,10 @@ private function _generateFieldLayoutArray(array $layoutIds): array 'tabs.uid AS tabUid', 'layouts.id AS layoutId', ]) - ->from(['{{%fieldlayoutfields}} AS layoutFields']) - ->innerJoin('{{%fieldlayouttabs}} AS tabs', '[[layoutFields.tabId]] = [[tabs.id]]') - ->innerJoin('{{%fieldlayouts}} AS layouts', '[[layoutFields.layoutId]] = [[layouts.id]]') - ->innerJoin('{{%fields}} AS fields', '[[layoutFields.fieldId]] = [[fields.id]]') + ->from(['layoutFields' => Table::FIELDLAYOUTFIELDS]) + ->innerJoin(['tabs' => Table::FIELDLAYOUTTABS], '[[tabs.id]] = [[layoutFields.tabId]]') + ->innerJoin(['layouts' => Table::FIELDLAYOUTS], '[[layouts.id]] = [[layoutFields.layoutId]]') + ->innerJoin(['fields' => Table::FIELDS], '[[fields.id]] = [[layoutFields.fieldId]]') ->where(['layouts.id' => $layoutIds]) ->orderBy(['tabs.sortOrder' => SORT_ASC, 'layoutFields.sortOrder' => SORT_ASC]) ->all(); diff --git a/src/migrations/m180901_151639_fix_matrixcontent_tables.php b/src/migrations/m180901_151639_fix_matrixcontent_tables.php index 63e39c1a3d8..850abf75b60 100644 --- a/src/migrations/m180901_151639_fix_matrixcontent_tables.php +++ b/src/migrations/m180901_151639_fix_matrixcontent_tables.php @@ -136,9 +136,9 @@ private function _cleanUpTable(int $fieldId, string $tableName, array $originalF // get all of the columns this field needs $subFields = (new Query()) ->select(['f.handle', 'mbt.handle as blockTypeHandle']) - ->from(['{{%fields}} f']) - ->innerJoin('{{%fieldlayoutfields}} flf', '[[flf.fieldId]] = [[f.id]]') - ->innerJoin('{{%matrixblocktypes}} mbt', '[[mbt.fieldLayoutId]] = [[flf.layoutId]]') + ->from(['f' => Table::FIELDS]) + ->innerJoin(['flf' => Table::FIELDLAYOUTFIELDS], '[[flf.fieldId]] = [[f.id]]') + ->innerJoin(['mbt' => Table::MATRIXBLOCKTYPES], '[[mbt.fieldLayoutId]] = [[flf.layoutId]]') ->where(['mbt.fieldId' => $fieldId]) ->all(); diff --git a/src/migrations/m181211_143040_fix_entry_type_uids.php b/src/migrations/m181211_143040_fix_entry_type_uids.php index 8c7a754b78c..71e31b93b5c 100644 --- a/src/migrations/m181211_143040_fix_entry_type_uids.php +++ b/src/migrations/m181211_143040_fix_entry_type_uids.php @@ -27,8 +27,8 @@ public function safeUp() // Get all entry types from the DB $dbEntryTypes = (new Query()) ->select(['et.id', 'et.uid', 'et.handle', 's.uid as sectionUid']) - ->from(['{{%entrytypes}} et']) - ->innerJoin('{{%sections}} s', '[[s.id]] = [[et.sectionId]]') + ->from(['et' => Table::ENTRYTYPES]) + ->innerJoin(['s' => Table::SECTIONS], '[[s.id]] = [[et.sectionId]]') ->all(); // Index by section UID, handle diff --git a/src/migrations/m190108_113000_asset_field_setting_change.php b/src/migrations/m190108_113000_asset_field_setting_change.php index 5bee649f790..9e6d9c4372a 100644 --- a/src/migrations/m190108_113000_asset_field_setting_change.php +++ b/src/migrations/m190108_113000_asset_field_setting_change.php @@ -35,8 +35,8 @@ public function safeUp() $this->_volumesByFolderUids = (new Query()) ->select(['folders.uid folderUid', 'volumes.uid volumeUid']) - ->from(['{{%volumes}} volumes']) - ->innerJoin(['{{%volumefolders}} folders'], '[[volumes.id]] = [[folders.volumeId]]') + ->from(['volumes' => Table::VOLUMES]) + ->innerJoin(['folders' => Table::VOLUMEFOLDERS], '[[folders.volumeId]] = [[volumes.id]]') ->pairs(); $projectConfig = Craft::$app->getProjectConfig(); diff --git a/src/migrations/m190114_143000_more_asset_field_setting_changes.php b/src/migrations/m190114_143000_more_asset_field_setting_changes.php index 42cf4924fa2..43cc44aca7b 100644 --- a/src/migrations/m190114_143000_more_asset_field_setting_changes.php +++ b/src/migrations/m190114_143000_more_asset_field_setting_changes.php @@ -35,8 +35,8 @@ public function safeUp() $this->_volumesByFolderUids = (new Query()) ->select(['folders.uid folderUid', 'volumes.uid volumeUid']) - ->from(['{{%volumes}} volumes']) - ->innerJoin(['{{%volumefolders}} folders'], '[[volumes.id]] = [[folders.volumeId]]') + ->from(['volumes' => Table::VOLUMES]) + ->innerJoin(['folders' => Table::VOLUMEFOLDERS], '[[folders.volumeId]] = [[volumes.id]]') ->pairs(); $projectConfig = Craft::$app->getProjectConfig(); diff --git a/src/migrations/m190218_143000_element_index_settings_uid.php b/src/migrations/m190218_143000_element_index_settings_uid.php index d0d4d6a9b80..58f4ef3b070 100644 --- a/src/migrations/m190218_143000_element_index_settings_uid.php +++ b/src/migrations/m190218_143000_element_index_settings_uid.php @@ -2,7 +2,6 @@ namespace craft\migrations; -use Craft; use craft\db\Migration; use craft\db\Query; use craft\db\Table; @@ -10,6 +9,7 @@ use craft\elements\Category; use craft\elements\Entry; use craft\elements\User; +use craft\helpers\Db; use craft\helpers\Json; /** @@ -80,9 +80,11 @@ public function safeUp() $data = Json::encode($data); - Craft::$app->getDb()->createCommand() - ->update(Table::ELEMENTINDEXSETTINGS, ['settings' => $data], ['id' => $row['id']], [], false) - ->execute(); + Db::update(Table::ELEMENTINDEXSETTINGS, [ + 'settings' => $data, + ], [ + 'id' => $row['id'], + ], [], false, $this->db); } } } diff --git a/src/migrations/m190312_152740_element_revisions.php b/src/migrations/m190312_152740_element_revisions.php index 48ad26197ce..8fa45c3be26 100644 --- a/src/migrations/m190312_152740_element_revisions.php +++ b/src/migrations/m190312_152740_element_revisions.php @@ -2,9 +2,9 @@ namespace craft\migrations; -use Craft; use craft\db\Migration; use craft\db\Table; +use craft\helpers\Queue; use craft\queue\jobs\ConvertEntryRevisions; /** @@ -66,7 +66,7 @@ public function safeUp() $this->addForeignKey(null, '{{%entryversionerrors}}', ['versionId'], Table::ENTRYVERSIONS, ['id'], 'CASCADE'); // Queue up a ConvertEntryRevisions job - Craft::$app->getQueue()->push(new ConvertEntryRevisions()); + Queue::push(new ConvertEntryRevisions()); } /** diff --git a/src/migrations/m190417_085010_add_image_editor_permissions.php b/src/migrations/m190417_085010_add_image_editor_permissions.php index 9050ec1fd3e..a97406974fe 100644 --- a/src/migrations/m190417_085010_add_image_editor_permissions.php +++ b/src/migrations/m190417_085010_add_image_editor_permissions.php @@ -94,15 +94,15 @@ public function safeUp() // Get all the eligible users $userIds = (new Query()) ->select(['users.id']) - ->from([Table::USERS . ' AS users']) - ->innerJoin(Table::USERPERMISSIONS_USERS . ' AS saveUserPermissions', '[[saveUserPermissions.userId]] = [[users.id]]') - ->innerJoin(Table::USERPERMISSIONS . ' AS saveInVolume', [ + ->from(['users' => Table::USERS]) + ->innerJoin(['saveUserPermissions' => Table::USERPERMISSIONS_USERS], '[[saveUserPermissions.userId]] = [[users.id]]') + ->innerJoin(['saveInVolume' => Table::USERPERMISSIONS], [ 'and', '[[saveInVolume.id]] = [[saveUserPermissions.permissionId]]', ['saveInVolume.name' => $savePermission] ]) - ->innerJoin(Table::USERPERMISSIONS_USERS . ' AS deleteUserPermissions', '[[deleteUserPermissions.userId]] = [[users.id]]') - ->innerJoin(Table::USERPERMISSIONS . ' AS deleteInVolume', [ + ->innerJoin(['deleteUserPermissions' => Table::USERPERMISSIONS_USERS], '[[deleteUserPermissions.userId]] = [[users.id]]') + ->innerJoin(['deleteInVolume' => Table::USERPERMISSIONS], [ 'and', '[[deleteInVolume.id]] = [[deleteUserPermissions.permissionId]]', ['deleteInVolume.name' => $deletePermission] diff --git a/src/migrations/m191216_191635_asset_upload_tracking.php b/src/migrations/m191216_191635_asset_upload_tracking.php index 9c01dfc3133..1560b64b707 100644 --- a/src/migrations/m191216_191635_asset_upload_tracking.php +++ b/src/migrations/m191216_191635_asset_upload_tracking.php @@ -2,7 +2,6 @@ namespace craft\migrations; -use Craft; use craft\db\Migration; use craft\db\Table; use craft\helpers\MigrationHelper; diff --git a/src/migrations/m191222_002848_peer_asset_permissions.php b/src/migrations/m191222_002848_peer_asset_permissions.php index 398f69a38e1..6caa95ec6ed 100644 --- a/src/migrations/m191222_002848_peer_asset_permissions.php +++ b/src/migrations/m191222_002848_peer_asset_permissions.php @@ -3,7 +3,6 @@ namespace craft\migrations; use Craft; -use craft\base\Volume; use craft\db\Migration; use craft\db\Query; use craft\db\Table; @@ -22,7 +21,6 @@ public function safeUp() // mapped to the old permissions users/groups must have to gain the new ones $newPermissions = []; foreach (Craft::$app->getVolumes()->getAllVolumes() as $volume) { - /** @var Volume $volume */ $suffix = ":{$volume->uid}"; $newPermissions += [ "viewvolume{$suffix}" => "viewpeerfilesinvolume{$suffix}", @@ -37,7 +35,7 @@ public function safeUp() $userIds = (new Query()) ->select(['upu.userId']) ->from(['upu' => Table::USERPERMISSIONS_USERS]) - ->innerJoin(Table::USERPERMISSIONS . ' up', '[[up.id]] = [[upu.permissionId]]') + ->innerJoin(['up' => Table::USERPERMISSIONS], '[[up.id]] = [[upu.permissionId]]') ->where(['up.name' => $oldPermission]) ->column($this->db); if (!empty($userIds)) { diff --git a/src/migrations/m200127_172522_queue_channels.php b/src/migrations/m200127_172522_queue_channels.php index 408b8bffebb..4a0dc143930 100644 --- a/src/migrations/m200127_172522_queue_channels.php +++ b/src/migrations/m200127_172522_queue_channels.php @@ -2,7 +2,6 @@ namespace craft\migrations; -use Craft; use craft\db\Migration; use craft\db\Table; use craft\helpers\MigrationHelper; diff --git a/src/migrations/m200228_195211_long_deprecation_messages.php b/src/migrations/m200228_195211_long_deprecation_messages.php index 7e770f68057..66a9f292d5a 100644 --- a/src/migrations/m200228_195211_long_deprecation_messages.php +++ b/src/migrations/m200228_195211_long_deprecation_messages.php @@ -2,7 +2,6 @@ namespace craft\migrations; -use Craft; use craft\db\Migration; use craft\db\Table; diff --git a/src/migrations/m200306_054652_disabled_sites.php b/src/migrations/m200306_054652_disabled_sites.php new file mode 100644 index 00000000000..381cf0bd2fd --- /dev/null +++ b/src/migrations/m200306_054652_disabled_sites.php @@ -0,0 +1,28 @@ +addColumn(Table::SITES, 'enabled', $this->boolean()->notNull()->defaultValue(true)->after('primary')); + } + + /** + * @inheritdoc + */ + public function safeDown() + { + $this->dropColumn(Table::SITES, 'enabled'); + } +} diff --git a/src/models/FieldLayout.php b/src/models/FieldLayout.php index a7c93a2973c..1401f20fc84 100644 --- a/src/models/FieldLayout.php +++ b/src/models/FieldLayout.php @@ -8,7 +8,6 @@ namespace craft\models; use Craft; -use craft\base\Field; use craft\base\FieldInterface; use craft\base\Model; @@ -89,7 +88,6 @@ public function getConfig() 'sortOrder' => (int)$tab->sortOrder, ]; - /** @var Field $field */ foreach ($tab->getFields() as $field) { $tabData['fields'][$field->uid] = [ 'required' => (bool)$field->required, @@ -126,7 +124,6 @@ public static function createFromConfig(array $config): self $layoutFields = []; foreach ($tab['fields'] as $uid => $field) { - /** @var Field $createdField */ $createdField = $fieldService->getFieldByUid($uid); if ($createdField) { @@ -177,7 +174,6 @@ public function getFieldIds(): array $ids = []; foreach ($this->getFields() as $field) { - /** @var Field $field */ $ids[] = $field->id; } @@ -188,12 +184,11 @@ public function getFieldIds(): array * Returns a field by its handle. * * @param string $handle The field handle. - * @return Field|FieldInterface|null + * @return FieldInterface|null */ public function getFieldByHandle(string $handle) { foreach ($this->getFields() as $field) { - /** @var Field $field */ if ($field->handle === $handle) { return $field; } diff --git a/src/models/FieldLayoutTab.php b/src/models/FieldLayoutTab.php index 27531fceced..2ebffb3c6d0 100644 --- a/src/models/FieldLayoutTab.php +++ b/src/models/FieldLayoutTab.php @@ -8,9 +8,7 @@ namespace craft\models; use Craft; -use craft\base\Element; use craft\base\ElementInterface; -use craft\base\Field; use craft\base\FieldInterface; use craft\base\Model; use craft\helpers\StringHelper; @@ -119,7 +117,6 @@ public function getFields(): array if ($layout = $this->getLayout()) { foreach ($layout->getFields() as $field) { - /** @var Field $field */ if ($field->tabId == $this->id) { $this->_fields[] = $field; } @@ -158,13 +155,11 @@ public function getHtmlId(): string */ public function elementHasErrors(ElementInterface $element): bool { - /** @var Element $element */ if (!$element->hasErrors()) { return false; } foreach ($this->getFields() as $field) { - /** @var Field $field */ if ($element->hasErrors("{$field->handle}.*")) { return true; } diff --git a/src/models/Section.php b/src/models/Section.php index a86e8bb8af0..d1c7ccc901d 100644 --- a/src/models/Section.php +++ b/src/models/Section.php @@ -37,6 +37,10 @@ class Section extends Model const PROPAGATION_METHOD_SITE_GROUP = 'siteGroup'; const PROPAGATION_METHOD_LANGUAGE = 'language'; const PROPAGATION_METHOD_ALL = 'all'; + /** + * @since 3.5.0 + */ + const PROPAGATION_METHOD_CUSTOM = 'custom'; /** * @var int|null ID @@ -168,7 +172,8 @@ protected function defineRules(): array self::PROPAGATION_METHOD_NONE, self::PROPAGATION_METHOD_SITE_GROUP, self::PROPAGATION_METHOD_LANGUAGE, - self::PROPAGATION_METHOD_ALL + self::PROPAGATION_METHOD_ALL, + self::PROPAGATION_METHOD_CUSTOM, ] ]; $rules[] = [['name', 'handle'], UniqueValidator::class, 'targetClass' => SectionRecord::class]; diff --git a/src/models/Section_SiteSettings.php b/src/models/Section_SiteSettings.php index cc4e34fd44d..0659a09630e 100644 --- a/src/models/Section_SiteSettings.php +++ b/src/models/Section_SiteSettings.php @@ -62,6 +62,22 @@ class Section_SiteSettings extends Model */ private $_section; + /** + * @inheritdoc + * @since 3.5.0 + */ + public function init() + { + // Typecast DB values + $this->id = (int)$this->id ?: null; + $this->sectionId = (int)$this->sectionId ?: null; + $this->siteId = (int)$this->siteId ?: null; + $this->enabledByDefault = (bool)$this->enabledByDefault; + $this->hasUrls = (bool)$this->hasUrls; + + parent::init(); + } + /** * Returns the section. * diff --git a/src/models/Site.php b/src/models/Site.php index a355cb9dd76..f53fb808a56 100644 --- a/src/models/Site.php +++ b/src/models/Site.php @@ -55,6 +55,12 @@ class Site extends Model */ public $primary = false; + /** + * @var bool Enabled? + * @since 3.5.0 + */ + public $enabled = true; + /** * @var bool Has URLs */ @@ -95,6 +101,23 @@ class Site extends Model */ public $dateUpdated; + /** + * @inheritdoc + * @since 3.5.0 + */ + public function init() + { + // Typecast DB values + $this->id = (int)$this->id ?: null; + $this->groupId = (int)$this->groupId ?: null; + $this->primary = (bool)$this->primary; + $this->enabled = (bool)$this->enabled; + $this->hasUrls = (bool)$this->hasUrls; + $this->sortOrder = (int)$this->sortOrder; + + parent::init(); + } + /** * Returns the site’s base URL. * @@ -155,6 +178,14 @@ protected function defineRules(): array $rules[] = [['name', 'handle'], UniqueValidator::class, 'targetClass' => SiteRecord::class]; } + $rules[] = [ + ['enabled'], function(string $attribute) { + if ($this->primary && !$this->enabled) { + $this->addError($attribute, Craft::t('app', 'The primary site cannot be disabled.')); + } + } + ]; + return $rules; } diff --git a/src/queue/Queue.php b/src/queue/Queue.php index cbcc1788a9b..fad1328db27 100644 --- a/src/queue/Queue.php +++ b/src/queue/Queue.php @@ -192,24 +192,18 @@ public function push($job) */ public function retry(string $id) { - $this->db->createCommand() - ->update( - $this->tableName, - [ - 'dateReserved' => null, - 'timeUpdated' => null, - 'progress' => 0, - 'progressLabel' => null, - 'attempt' => 0, - 'fail' => false, - 'dateFailed' => null, - 'error' => null, - ], - ['id' => $id], - [], - false - ) - ->execute(); + Db::update($this->tableName, [ + 'dateReserved' => null, + 'timeUpdated' => null, + 'progress' => 0, + 'progressLabel' => null, + 'attempt' => 0, + 'fail' => false, + 'dateFailed' => null, + 'error' => null, + ], [ + 'id' => $id, + ], [], false, $this->db); } /** @@ -220,27 +214,19 @@ public function retryAll() // Move expired messages into waiting list $this->_moveExpired(); - $this->db->createCommand() - ->update( - $this->tableName, - [ - 'dateReserved' => null, - 'timeUpdated' => null, - 'progress' => 0, - 'progressLabel' => null, - 'attempt' => 0, - 'fail' => false, - 'dateFailed' => null, - 'error' => null, - ], - [ - 'channel' => $this->channel, - 'fail' => true, - ], - [], - false - ) - ->execute(); + Db::update($this->tableName, [ + 'dateReserved' => null, + 'timeUpdated' => null, + 'progress' => 0, + 'progressLabel' => null, + 'attempt' => 0, + 'fail' => false, + 'dateFailed' => null, + 'error' => null, + ], [ + 'channel' => $this->channel, + 'fail' => true, + ], [], false, $this->db); } /** @@ -248,9 +234,9 @@ public function retryAll() */ public function release(string $id) { - $this->db->createCommand() - ->delete($this->tableName, ['id' => $id]) - ->execute(); + Db::delete($this->tableName, [ + 'id' => $id, + ], [], $this->db); } /** @@ -258,9 +244,9 @@ public function release(string $id) */ public function releaseAll() { - $this->db->createCommand() - ->delete($this->tableName, ['channel' => $this->channel]) - ->execute(); + Db::delete($this->tableName, [ + 'channel' => $this->channel, + ], [], $this->db); } /** @@ -277,9 +263,9 @@ public function setProgress(int $progress, string $label = null) $data['progressLabel'] = $label; } - $this->db->createCommand() - ->update($this->tableName, $data, ['id' => $this->_executingJobId], [], false) - ->execute(); + Db::update($this->tableName, $data, [ + 'id' => $this->_executingJobId, + ], [], false, $this->db); } /** @@ -448,19 +434,13 @@ public function handleError(ExecEvent $event) // Have we given up? if (parent::handleError($event)) { // Mark the job as failed - $this->db->createCommand() - ->update( - $this->tableName, - [ - 'fail' => true, - 'dateFailed' => Db::prepareDateForDb(new \DateTime()), - 'error' => $event->error ? $event->error->getMessage() : null, - ], - ['id' => $event->id], - [], - false - ) - ->execute(); + Db::update($this->tableName, [ + 'fail' => true, + 'dateFailed' => Db::prepareDateForDb(new \DateTime()), + 'error' => $event->error ? $event->error->getMessage() : null, + ], [ + 'id' => $event->id, + ], [], false, $this->db); } // Don't tell run() to release the job @@ -544,10 +524,7 @@ protected function pushMessage($message, $ttr, $delay, $priority) $data['channel'] = $this->channel; } - $this->db->createCommand() - ->insert($this->tableName, $data, false) - ->execute(); - + Db::insert($this->tableName, $data, false, $this->db); return $this->db->getLastInsertID($this->tableName); } @@ -576,19 +553,13 @@ protected function reserve() $payload['dateReserved'] = new \DateTime(); $payload['timeUpdated'] = $payload['dateReserved']->getTimestamp(); $payload['attempt'] = (int)$payload['attempt'] + 1; - $this->db->createCommand() - ->update( - $this->tableName, - [ - 'dateReserved' => Db::prepareDateForDb($payload['dateReserved']), - 'timeUpdated' => $payload['timeUpdated'], - 'attempt' => $payload['attempt'] - ], - ['id' => $payload['id']], - [], - false - ) - ->execute(); + Db::update($this->tableName, [ + 'dateReserved' => Db::prepareDateForDb($payload['dateReserved']), + 'timeUpdated' => $payload['timeUpdated'], + 'attempt' => $payload['attempt'], + ], [ + 'id' => $payload['id'], + ], [], false, $this->db); } $this->mutex->release($lockName); @@ -630,27 +601,21 @@ private function _moveExpired() { if ($this->_reserveTime !== time()) { $this->_reserveTime = time(); - $this->db->createCommand() - ->update( - $this->tableName, - [ - 'dateReserved' => null, - 'timeUpdated' => null, - 'progress' => 0, - 'progressLabel' => null, - ], - [ - 'and', - [ - 'channel' => $this->channel, - 'fail' => false - ], - '[[timeUpdated]] < :time - [[ttr]]', - ], - [':time' => $this->_reserveTime], - false - ) - ->execute(); + Db::update($this->tableName, [ + 'dateReserved' => null, + 'timeUpdated' => null, + 'progress' => 0, + 'progressLabel' => null, + ], [ + 'and', + [ + 'channel' => $this->channel, + 'fail' => false, + ], + '[[timeUpdated]] < :time - [[ttr]]', + ], [ + ':time' => $this->_reserveTime, + ], false, $this->db); } } diff --git a/src/queue/jobs/ApplyNewPropagationMethod.php b/src/queue/jobs/ApplyNewPropagationMethod.php index 35f74185d63..16ebe90f855 100644 --- a/src/queue/jobs/ApplyNewPropagationMethod.php +++ b/src/queue/jobs/ApplyNewPropagationMethod.php @@ -8,11 +8,8 @@ namespace craft\queue\jobs; use Craft; -use craft\base\Element; use craft\base\ElementInterface; -use craft\elements\MatrixBlock; use craft\events\BatchElementActionEvent; -use craft\fields\Matrix; use craft\helpers\ArrayHelper; use craft\helpers\ElementHelper; use craft\queue\BaseJob; @@ -81,7 +78,6 @@ public function execute($queue) } // Load the element in any sites that it's about to be deleted for - /** @var Element $element */ $element = $e->element; $otherSiteElements = $elementType::find() ->id($element->id) @@ -94,7 +90,6 @@ public function execute($queue) // Duplicate those blocks so their content can live on while (!empty($otherSiteElements)) { $otherSiteElement = array_pop($otherSiteElements); - /** @var Element $newElement */ $newElement = $elementsService->duplicateElement($otherSiteElement); // This may support more than just the site it was saved in $newElementSiteIds = ArrayHelper::getColumn(ElementHelper::supportedSitesForElement($newElement), 'siteId'); diff --git a/src/queue/jobs/ConvertEntryRevisions.php b/src/queue/jobs/ConvertEntryRevisions.php index dfc97dc62a2..7ba92868aa3 100644 --- a/src/queue/jobs/ConvertEntryRevisions.php +++ b/src/queue/jobs/ConvertEntryRevisions.php @@ -9,16 +9,15 @@ use Craft; use craft\base\Element; -use craft\base\Field; use craft\behaviors\DraftBehavior; use craft\behaviors\RevisionBehavior; -use craft\db\Connection; use craft\db\Query; use craft\db\Table; use craft\elements\Entry; use craft\elements\User; use craft\helpers\ArrayHelper; use craft\helpers\DateTimeHelper; +use craft\helpers\Db; use craft\helpers\Json; use craft\queue\BaseJob; use craft\services\Drafts; @@ -38,8 +37,6 @@ class ConvertEntryRevisions extends BaseJob { private $queue; - /** @var Connection */ - private $db; /** @var Elements */ private $elementsService; /** @var Entries */ @@ -61,7 +58,6 @@ class ConvertEntryRevisions extends BaseJob public function execute($queue) { $this->queue = $queue; - $this->db = Craft::$app->getDb(); $this->elementsService = Craft::$app->getElements(); $this->entriesService = Craft::$app->getEntries(); $this->fieldsService = Craft::$app->getFields(); @@ -106,7 +102,6 @@ private function setFieldValues(Entry $entry, $fieldValues) } foreach ($fieldValues as $id => $value) { - /** @var Field $field */ $field = $this->fieldsService->getFieldById($id); if ($field) { $entry->setFieldValue($field->handle, $value); @@ -116,7 +111,7 @@ private function setFieldValues(Entry $entry, $fieldValues) private function convertDrafts() { - if (!$this->db->tableExists(Table::ENTRYDRAFTS)) { + if (!Craft::$app->getDb()->tableExists(Table::ENTRYDRAFTS)) { return; } @@ -133,17 +128,15 @@ private function convertDrafts() try { $this->convertDraft($result); } catch (\Throwable $e) { - $this->db->createCommand() - ->insert('{{%entrydrafterrors}}', [ - 'draftId' => $result['id'], - 'error' => $e->getMessage(), - ], false) - ->execute(); + Db::insert('{{%entrydrafterrors}}', [ + 'draftId' => $result['id'], + 'error' => $e->getMessage(), + ], false); continue; } - $this->db->createCommand() - ->delete(Table::ENTRYDRAFTS, ['id' => $result['id']]) - ->execute(); + Db::delete(Table::ENTRYDRAFTS, [ + 'id' => $result['id'], + ], []); } } @@ -197,7 +190,7 @@ private function convertVersions() // If maxRevisions is set, filter out versions that would have been deleted by now $maxRevisions = Craft::$app->getConfig()->getGeneral()->maxRevisions; if ($maxRevisions > 0) { - $numSql = $this->db->getIsMysql() ? 'cast([[num]] as signed)' : '[[num]]'; + $numSql = Craft::$app->getDb()->getIsMysql() ? 'cast([[num]] as signed)' : '[[num]]'; $query->andWhere([ '>', 'num', (new Query()) ->select(new Expression("max({$numSql})" . ($maxRevisions ? " - {$maxRevisions}" : ''))) @@ -213,17 +206,15 @@ private function convertVersions() try { $this->convertVersion($result); } catch (\Throwable $e) { - $this->db->createCommand() - ->insert('{{%entryversionerrors}}', [ - 'versionId' => $result['id'], - 'error' => $e->getMessage(), - ], false) - ->execute(); + Db::insert('{{%entryversionerrors}}', [ + 'versionId' => $result['id'], + 'error' => $e->getMessage(), + ], false); continue; } - $this->db->createCommand() - ->delete(Table::ENTRYVERSIONS, ['id' => $result['id']]) - ->execute(); + Db::delete(Table::ENTRYVERSIONS, [ + 'id' => $result['id'], + ], []); } } @@ -240,7 +231,7 @@ private function convertVersion(array $result) $lowestNum = (new Query()) ->select(['min([[num]])']) ->from(['r' => Table::REVISIONS]) - ->innerJoin(Table::ELEMENTS . ' e', '[[e.revisionId]] = [[r.id]]') + ->innerJoin(['e' => Table::ELEMENTS], '[[e.revisionId]] = [[r.id]]') ->where(['sourceId' => $entry->id]) ->andWhere(['>=', 'e.dateCreated', $result['dateCreated']]) ->scalar(); @@ -248,13 +239,13 @@ private function convertVersion(array $result) if ($lowestNum) { $diff = ($result['num'] - $lowestNum) + 1; if ($diff) { - $this->db->createCommand()->update(Table::REVISIONS, [ - 'num' => new Expression('[[num]]' . ($diff > 0 ? '+' : '-') . abs($diff)) + Db::update(Table::REVISIONS, [ + 'num' => new Expression('[[num]]' . ($diff > 0 ? '+' : '-') . abs($diff)), ], [ 'and', ['sourceId' => $entry->id], ['>=', 'num', $lowestNum], - ], [], false)->execute(); + ], [], false); } } diff --git a/src/queue/jobs/FindAndReplace.php b/src/queue/jobs/FindAndReplace.php index 635069224fc..c857798ecde 100644 --- a/src/queue/jobs/FindAndReplace.php +++ b/src/queue/jobs/FindAndReplace.php @@ -8,10 +8,10 @@ namespace craft\queue\jobs; use Craft; -use craft\base\Field; use craft\base\FieldInterface; use craft\db\Table; use craft\fields\Matrix; +use craft\helpers\Db; use craft\queue\BaseJob; use yii\base\Exception; @@ -62,9 +62,7 @@ public function execute($queue) foreach ($this->_textColumns as $i => list($table, $column)) { $this->setProgress($queue, $i / $totalTextColumns); - Craft::$app->getDb()->createCommand() - ->replace($table, $column, $this->find, $this->replace) - ->execute(); + Db::replace($table, $column, $this->find, $this->replace); } } @@ -88,7 +86,6 @@ protected function defaultDescription(): string */ private function _checkField(FieldInterface $field, string $table, string $fieldColumnPrefix) { - /** @var Field $field */ if (!$field::hasContentColumn()) { return; } diff --git a/src/queue/jobs/LocalizeRelations.php b/src/queue/jobs/LocalizeRelations.php index d4d5815b4de..55ba86966d8 100644 --- a/src/queue/jobs/LocalizeRelations.php +++ b/src/queue/jobs/LocalizeRelations.php @@ -10,6 +10,7 @@ use Craft; use craft\db\Query; use craft\db\Table; +use craft\helpers\Db; use craft\queue\BaseJob; /** @@ -42,32 +43,26 @@ public function execute($queue) $totalRelations = count($relations); $allSiteIds = Craft::$app->getSites()->getAllSiteIds(); $primarySiteId = array_shift($allSiteIds); - $db = Craft::$app->getDb(); foreach ($relations as $i => $relation) { $this->setProgress($queue, $i / $totalRelations); // Set the existing relation to the primary site - $db->createCommand() - ->update( - Table::RELATIONS, - ['sourceSiteId' => $primarySiteId], - ['id' => $relation['id']]) - ->execute(); + Db::update(Table::RELATIONS, [ + 'sourceSiteId' => $primarySiteId, + ], [ + 'id' => $relation['id'], + ]); // Duplicate it for the other sites foreach ($allSiteIds as $siteId) { - $db->createCommand() - ->insert( - Table::RELATIONS, - [ - 'fieldId' => $this->fieldId, - 'sourceId' => $relation['sourceId'], - 'sourceSiteId' => $siteId, - 'targetId' => $relation['targetId'], - 'sortOrder' => $relation['sortOrder'], - ]) - ->execute(); + Db::insert(Table::RELATIONS, [ + 'fieldId' => $this->fieldId, + 'sourceId' => $relation['sourceId'], + 'sourceSiteId' => $siteId, + 'targetId' => $relation['targetId'], + 'sortOrder' => $relation['sortOrder'], + ]); } } } diff --git a/src/queue/jobs/PruneRevisions.php b/src/queue/jobs/PruneRevisions.php new file mode 100644 index 00000000000..69012f2593a --- /dev/null +++ b/src/queue/jobs/PruneRevisions.php @@ -0,0 +1,77 @@ + + * @since 3.5.0 + */ +class PruneRevisions extends BaseJob +{ + /** + * @var string|ElementInterface The type of elements to update. + */ + public $elementType; + + /** + * @var int The ID of the source element. + */ + public $sourceId; + + /** + * @var int The site ID of the source element + */ + public $siteId; + + /** + * @inheritdoc + */ + public function execute($queue) + { + // Make sure maxRevisions is still set + $generalConfig = Craft::$app->getConfig()->getGeneral(); + if (!$generalConfig->maxRevisions) { + return; + } + + $class = $this->elementType; + $extraRevisions = $class::find() + ->revisionOf($this->sourceId) + ->siteId($this->siteId) + ->anyStatus() + ->orderBy(['num' => SORT_DESC]) + ->offset($generalConfig->maxRevisions) + ->all(); + + if (empty($extraRevisions)) { + return; + } + + $total = count($extraRevisions); + $elementsService = Craft::$app->getElements(); + + foreach ($extraRevisions as $i => $extraRevision) { + $this->setProgress($queue, ($i + 1) / $total); + $elementsService->deleteElement($extraRevision, true); + } + } + + /** + * @inheritdoc + */ + protected function defaultDescription(): string + { + return Craft::t('app', 'Pruning extra revisions'); + } +} diff --git a/src/queue/jobs/ResaveElements.php b/src/queue/jobs/ResaveElements.php index 9f772ff6228..40a73b08b50 100644 --- a/src/queue/jobs/ResaveElements.php +++ b/src/queue/jobs/ResaveElements.php @@ -14,7 +14,6 @@ use craft\events\BatchElementActionEvent; use craft\queue\BaseJob; use craft\services\Elements; -use yii\db\Exception; /** * ResaveElements job diff --git a/src/queue/jobs/UpdateElementSlugsAndUris.php b/src/queue/jobs/UpdateElementSlugsAndUris.php index 57948f9dd3a..7d720e7e065 100644 --- a/src/queue/jobs/UpdateElementSlugsAndUris.php +++ b/src/queue/jobs/UpdateElementSlugsAndUris.php @@ -8,7 +8,6 @@ namespace craft\queue\jobs; use Craft; -use craft\base\Element; use craft\base\ElementInterface; use craft\elements\db\ElementQuery; use craft\elements\db\ElementQueryInterface; @@ -88,7 +87,6 @@ protected function defaultDescription(): string */ private function _createElementQuery(): ElementQueryInterface { - /** @var Element $class */ $class = $this->elementType; return $class::find() diff --git a/src/queue/jobs/UpdateSearchIndex.php b/src/queue/jobs/UpdateSearchIndex.php index b92b774fb97..d38e5eca89b 100644 --- a/src/queue/jobs/UpdateSearchIndex.php +++ b/src/queue/jobs/UpdateSearchIndex.php @@ -8,7 +8,6 @@ namespace craft\queue\jobs; use Craft; -use craft\base\Element; use craft\base\ElementInterface; use craft\queue\BaseJob; @@ -46,7 +45,6 @@ class UpdateSearchIndex extends BaseJob */ public function execute($queue) { - /** @var Element $class */ $class = $this->elementType; $elements = $class::find() ->id($this->elementId) diff --git a/src/records/GlobalSet.php b/src/records/GlobalSet.php index 3339d5a6715..16f62083a52 100644 --- a/src/records/GlobalSet.php +++ b/src/records/GlobalSet.php @@ -50,7 +50,7 @@ public static function find() $query ->where([ 'exists', (new Query()) - ->from([Table::ELEMENTS . ' e']) + ->from(['e' => Table::ELEMENTS]) ->where('[[e.id]] = ' . static::tableName() . '.[[id]]') ->andWhere(['e.dateDeleted' => null]) ]); @@ -74,7 +74,7 @@ public static function findTrashed(): ActiveQuery { return static::find()->where([ 'not exists', (new Query()) - ->from([Table::ELEMENTS . ' e']) + ->from(['e' => Table::ELEMENTS]) ->where('[[e.id]] = ' . static::tableName() . '.[[id]]') ->andWhere(['e.dateDeleted' => null]) ]); diff --git a/src/records/Site.php b/src/records/Site.php index b85d182aea4..306a56394aa 100644 --- a/src/records/Site.php +++ b/src/records/Site.php @@ -21,6 +21,7 @@ * @property string $handle Handle * @property string $language Language * @property bool $primary Primary + * @property bool $enabled Enabled * @property bool $hasUrls Has URLs * @property bool $baseUrl Base URL * @property int $sortOrder Sort order diff --git a/src/services/AssetIndexer.php b/src/services/AssetIndexer.php index 521dd2c59b2..00240f1ae50 100644 --- a/src/services/AssetIndexer.php +++ b/src/services/AssetIndexer.php @@ -4,7 +4,6 @@ use Craft; use craft\base\LocalVolumeInterface; -use craft\base\Volume; use craft\base\VolumeInterface; use craft\db\Query; use craft\db\Table; @@ -57,7 +56,6 @@ public function getIndexingSessionId(): string public function prepareIndexList(string $sessionId, int $volumeId, string $directory = ''): array { try { - /** @var Volume $volume */ $volume = Craft::$app->getVolumes()->getVolumeById($volumeId); // Get the file list. @@ -209,12 +207,7 @@ public function storeIndexList(array $indexList, string $sessionId, int $volumeI $values[] = [$volumeId, $sessionId, $entry['path'], $entry['size'], Db::prepareDateForDb(new \DateTime('@' . $entry['timestamp'])), false, false]; } - Craft::$app->getDb()->createCommand() - ->batchInsert( - Table::ASSETINDEXDATA, - $attributes, - $values) - ->execute(); + Db::batchInsert(Table::ASSETINDEXDATA, $attributes, $values); } /** @@ -298,13 +291,9 @@ public function updateIndexEntry(int $entryId, array $data) { // Only allow a few fields to be updated. $data = array_intersect_key($data, array_flip(['inProgress', 'completed', 'recordId'])); - - Craft::$app->getDb()->createCommand() - ->update( - Table::ASSETINDEXDATA, - $data, - ['id' => $entryId]) - ->execute(); + Db::update(Table::ASSETINDEXDATA, $data, [ + 'id' => $entryId, + ]); } @@ -345,10 +334,10 @@ public function getMissingFiles(string $sessionId): array $processedFiles = array_flip($processedFiles); $assets = (new Query()) ->select(['fi.volumeId', 'fi.id AS assetId', 'fi.filename', 'fo.path', 's.name AS volumeName']) - ->from(['{{%assets}} fi']) - ->innerJoin('{{%volumefolders}} fo', '[[fi.folderId]] = [[fo.id]]') - ->innerJoin('{{%volumes}} s', '[[s.id]] = [[fi.volumeId]]') - ->innerJoin('{{%elements}} e', '[[e.id]] = [[fi.id]]') + ->from(['fi' => Table::ASSETS]) + ->innerJoin(['fo' => Table::VOLUMEFOLDERS], '[[fo.id]] = [[fi.folderId]]') + ->innerJoin(['s' => Table::VOLUMES], '[[s.id]] = [[fi.volumeId]]') + ->innerJoin(['e' => Table::ELEMENTS], '[[e.id]] = [[fi.id]]') ->where(['fi.volumeId' => $volumeIds]) ->andWhere(['e.dateDeleted' => null]) ->all(); @@ -365,7 +354,7 @@ public function getMissingFiles(string $sessionId): array /** * Index a single file by Volume and path. * - * @param Volume $volume + * @param VolumeInterface $volume * @param string $path * @param string $sessionId optional indexing session id. * @param bool $cacheImages Whether remotely-stored images should be downloaded and stored locally, to speed up transform generation. @@ -374,7 +363,7 @@ public function getMissingFiles(string $sessionId): array * @throws MissingAssetException if the asset record doesn't exist and $createIfMissing is false * @throws VolumeObjectNotFoundException If the file to be indexed cannot be found. */ - public function indexFile(Volume $volume, string $path, string $sessionId = '', bool $cacheImages = false, bool $createIfMissing = true) + public function indexFile(VolumeInterface $volume, string $path, string $sessionId = '', bool $cacheImages = false, bool $createIfMissing = true) { $fileInfo = $volume->getFileMetadata($path); $folderPath = dirname($path); @@ -412,9 +401,6 @@ public function indexFileByEntry(AssetIndexData $indexEntry, bool $cacheImages = $indexEntry->completed = false; $recordData = $indexEntry->toArray(); - // For some reason Postgres chokes if we don't do that. - unset($recordData['id']); - $record = new AssetIndexDataRecord($recordData); $record->save(); @@ -462,7 +448,6 @@ private function _indexFileByIndexData(AssetIndexData $indexEntry, bool $createI throw new AssetLogicException("The folder {$path} does not exist"); } - /** @var Volume $volume */ $volume = $folder->getVolume(); // Check if the extension is allowed diff --git a/src/services/AssetTransforms.php b/src/services/AssetTransforms.php index ebd3cb9829a..3e91668c8fa 100644 --- a/src/services/AssetTransforms.php +++ b/src/services/AssetTransforms.php @@ -267,11 +267,9 @@ public function handleChangedTransform(ConfigEvent $event) } if ($deleteTransformIndexes) { - Craft::$app->getDb()->createCommand() - ->delete( - Table::ASSETTRANSFORMINDEX, - ['location' => $this->_getNamedTransformFolderName($transformRecord->handle)]) - ->execute(); + Db::delete(Table::ASSETTRANSFORMINDEX, [ + 'location' => $this->_getNamedTransformFolderName($transformRecord->handle), + ]); } // Clear caches @@ -357,11 +355,9 @@ public function handleDeletedTransform(ConfigEvent $event) ])); } - Craft::$app->getDb()->createCommand() - ->delete( - Table::ASSETTRANSFORMS, - ['uid' => $transformUid]) - ->execute(); + Db::delete(Table::ASSETTRANSFORMS, [ + 'uid' => $transformUid, + ]); // Clear caches $this->_transforms = null; @@ -377,6 +373,23 @@ public function handleDeletedTransform(ConfigEvent $event) /** * Eager-loads transform indexes for a given set of file IDs. * + * You can include `srcset`-style sizes (e.g. `100w` or `2x`) following a normal transform definition, for example: + * + * ::: code + * + * ```twig + * [{width: 1000, height: 600}, '1.5x', '2x', '3x'] + * ``` + * + * ```php + * [['width' => 1000, 'height' => 600], '1.5x', '2x', '3x'] + * ``` + * + * ::: + * + * When a `srcset`-style size is encountered, the preceding normal transform definition will be used as a + * reference when determining the resulting transform dimensions. + * * @param Asset[]|array $assets The files to eager-load tranforms for * @param array $transforms The transform definitions to eager-load */ @@ -393,26 +406,66 @@ public function eagerLoadTransforms(array $assets, array $transforms) $transformsByFingerprint = []; $indexCondition = ['or']; - foreach ($transforms as $transform) { - $transform = $this->normalizeTransform($transform); + /** @var AssetTransform|null $refTransform */ + $refTransform = null; - if ($transform !== null) { - $location = $fingerprint = $this->_getTransformFolderName($transform); + foreach ($transforms as $transform) { + // Is this a srcset-style size (2x, 100w, etc.)? + try { + list($sizeValue, $sizeUnit) = AssetsHelper::parseSrcsetSize($transform); + } catch (InvalidArgumentException $e) { + // All good. + } - $transformCondition = ['and', ['location' => $location]]; + if (isset($sizeValue, $sizeUnit)) { + if ($refTransform === null || !$refTransform->width) { + throw new InvalidArgumentException("Can’t eager-load transform “{$transform}” without a prior transform that specifies the base width"); + } - if ($transform->format === null) { - $transformCondition[] = ['format' => null]; + $transform = []; + if ($sizeUnit === 'w') { + $transform['width'] = (int)$sizeValue; } else { - $transformCondition[] = ['format' => $transform->format]; - $fingerprint .= ':' . $transform->format; + $transform['width'] = (int)ceil($refTransform->width * $sizeValue); } - $indexCondition[] = $transformCondition; - $transformsByFingerprint[$fingerprint] = $transform; + // Only set the height if the reference transform has a height set on it + if ($refTransform && $refTransform->height) { + if ($sizeUnit === 'w') { + $transform['height'] = (int)ceil($refTransform->height * $transform['width'] / $refTransform->width); + } else { + $transform['height'] = (int)ceil($refTransform->height * $sizeValue); + } + } + } + + $transform = $this->normalizeTransform($transform); + if ($transform === null) { + continue; + } + + $location = $fingerprint = $this->_getTransformFolderName($transform); + + $transformCondition = ['and', ['location' => $location]]; + + if ($transform->format === null) { + $transformCondition[] = ['format' => null]; + } else { + $transformCondition[] = ['format' => $transform->format]; + $fingerprint .= ':' . $transform->format; + } + + $indexCondition[] = $transformCondition; + $transformsByFingerprint[$fingerprint] = $transform; + + if (!isset($sizeValue)) { + // Use this as the reference transform in case any srcset-style transforms follow it + $refTransform = $transform; } } + unset($refTransform); + // Query for the indexes $results = $this->_createTransformIndexQuery() ->where([ @@ -447,11 +500,9 @@ public function eagerLoadTransforms(array $assets, array $transforms) // Delete any invalid indexes if (!empty($invalidIndexIds)) { - Craft::$app->getDb()->createCommand() - ->delete( - Table::ASSETTRANSFORMINDEX, - ['id' => $invalidIndexIds]) - ->execute(); + Db::delete(Table::ASSETTRANSFORMINDEX, [ + 'id' => $invalidIndexIds, + ]); } } @@ -478,7 +529,6 @@ public function getTransformIndex(Asset $asset, $transform): AssetTransformIndex if (isset($this->_eagerLoadedTransformIndexes[$fingerprint])) { $result = $this->_eagerLoadedTransformIndexes[$fingerprint]; - return new AssetTransformIndex($result); } @@ -505,9 +555,9 @@ public function getTransformIndex(Asset $asset, $transform): AssetTransformIndex } // Delete the out-of-date record - Craft::$app->getDb()->createCommand() - ->delete(Table::ASSETTRANSFORMINDEX, ['id' => $result['id']]) - ->execute(); + Db::delete(Table::ASSETTRANSFORMINDEX, [ + 'id' => $result['id'], + ]); // And the file. $transformUri = $asset->getFolder()->path . $this->getTransformSubpath($asset, new AssetTransformIndex($result)); @@ -772,16 +822,13 @@ public function storeTransformIndexData(AssetTransformIndex $index): AssetTransf ], [], false) ); - $dbConnection = Craft::$app->getDb(); - if (null !== $index->id) { - $dbConnection->createCommand() - ->update(Table::ASSETTRANSFORMINDEX, $values, ['id' => $index->id]) - ->execute(); + if ($index->id !== null) { + Db::update(Table::ASSETTRANSFORMINDEX, $values, [ + 'id' => $index->id, + ]); } else { - $dbConnection->createCommand() - ->insert(Table::ASSETTRANSFORMINDEX, $values) - ->execute(); - $index->id = $dbConnection->getLastInsertID(Table::ASSETTRANSFORMINDEX); + Db::insert(Table::ASSETTRANSFORMINDEX, $values); + $index->id = Craft::$app->getDb()->getLastInsertID(Table::ASSETTRANSFORMINDEX); } return $index; @@ -869,9 +916,9 @@ public function getUrlForTransformByAssetAndTransformIndex(Asset $asset, AssetTr */ public function deleteTransformIndexDataByAssetId(int $assetId) { - Craft::$app->getDb()->createCommand() - ->delete(Table::ASSETTRANSFORMINDEX, ['assetId' => $assetId]) - ->execute(); + Db::delete(Table::ASSETTRANSFORMINDEX, [ + 'assetId' => $assetId, + ]); } /** @@ -881,9 +928,9 @@ public function deleteTransformIndexDataByAssetId(int $assetId) */ public function deleteTransformIndex(int $indexId) { - Craft::$app->getDb()->createCommand() - ->delete(Table::ASSETTRANSFORMINDEX, ['id' => $indexId]) - ->execute(); + Db::delete(Table::ASSETTRANSFORMINDEX, [ + 'id' => $indexId, + ]); } /** diff --git a/src/services/Assets.php b/src/services/Assets.php index 8f1b36f5507..099d469fc97 100644 --- a/src/services/Assets.php +++ b/src/services/Assets.php @@ -9,11 +9,10 @@ use Craft; use craft\assetpreviews\Image as ImagePreview; -use craft\assetpreviews\Text; use craft\assetpreviews\Pdf; +use craft\assetpreviews\Text; use craft\assetpreviews\Video; use craft\base\AssetPreviewHandlerInterface; -use craft\base\Volume; use craft\base\VolumeInterface; use craft\db\Query; use craft\db\Table; @@ -38,6 +37,7 @@ use craft\helpers\FileHelper; use craft\helpers\Image; use craft\helpers\Json; +use craft\helpers\Queue; use craft\helpers\UrlHelper; use craft\image\Raster; use craft\models\AssetTransform; @@ -611,7 +611,7 @@ public function getAssetUrl(Asset $asset, $transform = null, bool $generateNow = // Queue up a new Generate Pending Transforms job if (!$this->_queuedGeneratePendingTransformsJob) { - Craft::$app->getQueue()->push(new GeneratePendingTransforms()); + Queue::push(new GeneratePendingTransforms()); $this->_queuedGeneratePendingTransformsJob = true; } @@ -629,7 +629,7 @@ public function getAssetUrl(Asset $asset, $transform = null, bool $generateNow = * @param bool $fallbackToIcon whether to return the URL to a generic icon if a thumbnail can't be generated * @return string * @throws NotSupportedException if the asset can't have a thumbnail, and $fallbackToIcon is `false` - * @see Asset::getThumbUrl() + * @see Asset::getIconUrl() */ public function getThumbUrl(Asset $asset, int $width, int $height = null, bool $generate = false, bool $fallbackToIcon = true): string { @@ -811,8 +811,8 @@ public function getNameReplacementInFolder(string $originalFilename, int $folder // Get a list from DB as well $fileList = (new Query()) ->select(['assets.filename']) - ->from(['{{%assets}} assets']) - ->innerJoin(['{{%elements}} elements'], '[[assets.id]] = [[elements.id]]') + ->from(['assets' => Table::ASSETS]) + ->innerJoin(['elements' => Table::ELEMENTS], '[[elements.id]] = [[assets.id]]') ->where([ 'assets.folderId' => $folderId, 'elements.dateDeleted' => null @@ -880,7 +880,6 @@ public function getNameReplacementInFolder(string $originalFilename, int $folder */ public function ensureFolderByFullPathAndVolume(string $fullPath, VolumeInterface $volume, bool $justRecord = true): int { - /** @var Volume $volume */ $parentId = Craft::$app->getVolumes()->ensureTopFolder($volume); $folderId = $parentId; @@ -992,7 +991,6 @@ public function getUserTemporaryUploadFolder(User $user = null) if (!$volume) { throw new VolumeException(Craft::t('app', 'The volume set for temp asset storage is not valid.')); } - /** @var Volume $volume */ $path = (isset($assetSettings['tempSubpath']) ? $assetSettings['tempSubpath'] . '/' : '') . $folderName; $folderId = $this->ensureFolderByFullPathAndVolume($path, $volume, false); diff --git a/src/services/Categories.php b/src/services/Categories.php index ecde161dda2..1a40191b56a 100644 --- a/src/services/Categories.php +++ b/src/services/Categories.php @@ -8,7 +8,6 @@ namespace craft\services; use Craft; -use craft\base\Field; use craft\db\Query; use craft\db\Table; use craft\elements\Category; @@ -326,8 +325,7 @@ public function handleChangedCategoryGroup(ConfigEvent $event) ProjectConfigHelper::ensureAllSitesProcessed(); ProjectConfigHelper::ensureAllFieldsProcessed(); - $db = Craft::$app->getDb(); - $transaction = $db->beginTransaction(); + $transaction = Craft::$app->getDb()->beginTransaction(); try { $structureData = $data['structure']; @@ -451,15 +449,12 @@ public function handleChangedCategoryGroup(ConfigEvent $event) if (!empty($siteData)) { // Drop the old category URIs for any site settings that don't have URLs if (!empty($sitesNowWithoutUrls)) { - $db->createCommand() - ->update( - Table::ELEMENTS_SITES, - ['uri' => null], - [ - 'elementId' => $categoryIds, - 'siteId' => $sitesNowWithoutUrls, - ]) - ->execute(); + Db::update(Table::ELEMENTS_SITES, [ + 'uri' => null, + ], [ + 'elementId' => $categoryIds, + 'siteId' => $sitesNowWithoutUrls, + ]); } else if (!empty($sitesWithNewUriFormats)) { foreach ($categoryIds as $categoryId) { App::maxPowerCaptain(); @@ -646,7 +641,6 @@ public function handleDeletedCategoryGroup(ConfigEvent $event) */ public function pruneDeletedField(FieldEvent $event) { - /** @var Field $field */ $field = $event->field; $fieldUid = $field->uid; @@ -672,7 +666,9 @@ public function pruneDeletedField(FieldEvent $event) } // Nuke all the layout fields from the DB - Craft::$app->getDb()->createCommand()->delete('{{%fieldlayoutfields}}', ['fieldId' => $field->id])->execute(); + Db::delete(Table::FIELDLAYOUTFIELDS, [ + 'fieldId' => $field->id, + ]); // Allow events again $projectConfig->muteEvents = false; @@ -717,8 +713,8 @@ public function getCategoryById(int $categoryId, int $siteId = null) // Get the structure ID $structureId = (new Query()) ->select(['categorygroups.structureId']) - ->from(['{{%categories}} categories']) - ->innerJoin('{{%categorygroups}} categorygroups', '[[categorygroups.id]] = [[categories.groupId]]') + ->from(['categories' => Table::CATEGORIES]) + ->innerJoin(['categorygroups' => Table::CATEGORYGROUPS], '[[categorygroups.id]] = [[categories.groupId]]') ->where(['categories.id' => $categoryId]) ->scalar(); diff --git a/src/services/Composer.php b/src/services/Composer.php index 0855e46dfe6..a720e47c81c 100644 --- a/src/services/Composer.php +++ b/src/services/Composer.php @@ -56,6 +56,12 @@ class Composer extends Component */ public $maxBackups = 50; + /** + * @var callable|null The previous error handler. + * @see run() + */ + private $_errorHandler; + /** * @var string[]|null */ @@ -165,7 +171,7 @@ public function install(array $requirements = null, IOInterface $io = null, $whi try { // Run the installer - $status = $installer->run(); + $status = $this->run($installer); } catch (\Throwable $exception) { $status = 1; } @@ -189,7 +195,7 @@ public function install(array $requirements = null, IOInterface $io = null, $whi $contents = "_composerClasses); foreach ($this->_composerClasses as $class) { - $contents .= " '{$class}',\n"; + $contents .= " $class::class,\n"; } $contents .= "];\n"; FileHelper::writeToFile(dirname(__DIR__) . '/config/composer-classes.php', $contents); @@ -259,7 +265,7 @@ public function uninstall(array $packages, IOInterface $io = null) ->setOptimizeAutoloader(true) ->setClassMapAuthoritative($config->get('classmap-authoritative')); - $status = $installer->run(); + $status = $this->run($installer); } catch (\Throwable $exception) { $status = 1; } @@ -552,4 +558,40 @@ protected function backupComposerFiles() ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); } } + + /** + * @param Installer $installer + * @return int The response status + * @throws \Exception + * @since 3.5.0 + */ + protected function run(Installer $installer): int + { + $this->_errorHandler = set_error_handler([$this, 'handleError'], E_USER_DEPRECATED); + $status = $installer->run(); + set_error_handler($this->_errorHandler); + return $status; + } + + /** + * Handles an error triggered by Composer + * + * @param int $code the level of the error raised. + * @param string $message the error message. + * @param string $file the filename that the error was raised in. + * @param int $line the line number the error was raised at. + * @return bool whether the normal error handler continues. + * @since 3.5.0 + */ + public function handleError(int $code, string $message, string $file, int $line): bool + { + // Ignore deprecated errors + if ($code === E_USER_DEPRECATED) { + return true; + } + if ($this->_errorHandler !== null) { + return ($this->_errorHandler)($code, $message, $file, $line); + } + return false; + } } diff --git a/src/services/Content.php b/src/services/Content.php index c147faeacdc..80cd8586245 100644 --- a/src/services/Content.php +++ b/src/services/Content.php @@ -8,9 +8,7 @@ namespace craft\services; use Craft; -use craft\base\Element; use craft\base\ElementInterface; -use craft\base\Field; use craft\db\Query; use craft\db\Table; use craft\events\ElementContentEvent; @@ -60,7 +58,6 @@ class Content extends Component */ public function getContentRow(ElementInterface $element) { - /** @var Element $element */ if (!$element->id || !$element->siteId) { return null; } @@ -99,7 +96,6 @@ public function getContentRow(ElementInterface $element) */ public function populateElementContent(ElementInterface $element) { - /** @var Element $element */ // Make sure the element has content if (!$element->hasContent()) { return; @@ -116,7 +112,6 @@ public function populateElementContent(ElementInterface $element) if ($fieldLayout) { foreach ($fieldLayout->getFields() as $field) { - /** @var Field $field */ if ($field::hasContentColumn()) { $element->setFieldValue($field->handle, $row[$field->handle]); } @@ -135,7 +130,6 @@ public function populateElementContent(ElementInterface $element) */ public function saveContent(ElementInterface $element): bool { - /** @var Element $element */ if (!$element->id) { throw new Exception('Cannot save the content of an unsaved element.'); } @@ -166,7 +160,6 @@ public function saveContent(ElementInterface $element): bool $fieldLayout = $element->getFieldLayout(); if ($fieldLayout) { foreach ($fieldLayout->getFields() as $field) { - /** @var Field $field */ if ( (!$element->contentId || $element->isFieldDirty($field->handle)) && $field::hasContentColumn() @@ -192,14 +185,12 @@ public function saveContent(ElementInterface $element): bool // Insert/update the DB row if ($element->contentId) { // Update the existing row - Craft::$app->getDb()->createCommand() - ->update($this->contentTable, $values, ['id' => $element->contentId]) - ->execute(); + Db::update($this->contentTable, $values, [ + 'id' => $element->contentId, + ]); } else { // Insert a new row and store its ID on the element - Craft::$app->getDb()->createCommand() - ->insert($this->contentTable, $values) - ->execute(); + Db::insert($this->contentTable, $values); $element->contentId = Craft::$app->getDb()->getLastInsertID($this->contentTable); } diff --git a/src/services/Dashboard.php b/src/services/Dashboard.php index 0ea073128da..5f30bf4fcf1 100644 --- a/src/services/Dashboard.php +++ b/src/services/Dashboard.php @@ -8,7 +8,6 @@ namespace craft\services; use Craft; -use craft\base\Widget; use craft\base\WidgetInterface; use craft\db\Query; use craft\db\Table; @@ -17,6 +16,7 @@ use craft\events\RegisterComponentTypesEvent; use craft\events\WidgetEvent; use craft\helpers\Component as ComponentHelper; +use craft\helpers\Db; use craft\records\Widget as WidgetRecord; use craft\widgets\CraftSupport as CraftSupportWidget; use craft\widgets\Feed as FeedWidget; @@ -116,7 +116,6 @@ public function createWidget($config): WidgetInterface } try { - /** @var Widget $widget */ $widget = ComponentHelper::createComponent($config, WidgetInterface::class); } catch (MissingComponentException $e) { $config['errorMessage'] = $e->getMessage(); @@ -189,7 +188,6 @@ public function getWidgetById(int $id) */ public function saveWidget(WidgetInterface $widget, bool $runValidation = true): bool { - /** @var Widget $widget */ $isNewWidget = $widget->getIsNew(); // Fire a 'beforeSaveWidget' event @@ -277,7 +275,6 @@ public function deleteWidgetById(int $widgetId): bool */ public function deleteWidget(WidgetInterface $widget): bool { - /** @var Widget $widget */ // Fire a 'beforeDeleteWidget' event if ($this->hasEventHandlers(self::EVENT_BEFORE_DELETE_WIDGET)) { $this->trigger(self::EVENT_BEFORE_DELETE_WIDGET, new WidgetEvent([ @@ -384,9 +381,11 @@ private function _addDefaultUserWidgets() // Update the user record $user->hasDashboard = true; - Craft::$app->getDb()->createCommand() - ->update(Table::USERS, ['hasDashboard' => true], ['id' => $user->id]) - ->execute(); + Db::update(Table::USERS, [ + 'hasDashboard' => true, + ], [ + 'id' => $user->id, + ]); } /** diff --git a/src/services/Deprecator.php b/src/services/Deprecator.php index 932d7ee99a6..52ae6e2408f 100644 --- a/src/services/Deprecator.php +++ b/src/services/Deprecator.php @@ -122,23 +122,17 @@ public function storeLogs() $db = Craft::$app->getDb(); foreach ($this->_requestLogs as $log) { - $command = $db->createCommand() - ->upsert( - Table::DEPRECATIONERRORS, - [ - 'key' => $log->key, - 'fingerprint' => $log->fingerprint - ], - [ - 'lastOccurrence' => Db::prepareDateForDb($log->lastOccurrence), - 'file' => $log->file, - 'line' => $log->line, - 'message' => $log->message, - 'traces' => Json::encode($log->traces), - ]); - try { - $command->execute(); + Db::upsert(Table::DEPRECATIONERRORS, [ + 'key' => $log->key, + 'fingerprint' => $log->fingerprint, + ], [ + 'lastOccurrence' => Db::prepareDateForDb($log->lastOccurrence), + 'file' => $log->file, + 'line' => $log->line, + 'message' => $log->message, + 'traces' => Json::encode($log->traces), + ]); $log->id = $db->getLastInsertID(); } catch (IntegrityException $e) { // todo: remove this try/catch after the next breakpoint @@ -218,9 +212,9 @@ public function getLogById(int $logId) */ public function deleteLogById(int $id): bool { - $affectedRows = Craft::$app->getDb()->createCommand() - ->delete(Table::DEPRECATIONERRORS, ['id' => $id]) - ->execute(); + $affectedRows = Db::delete(Table::DEPRECATIONERRORS, [ + 'id' => $id, + ]); return (bool)$affectedRows; } @@ -232,9 +226,7 @@ public function deleteLogById(int $id): bool */ public function deleteAllLogs(): bool { - $affectedRows = Craft::$app->getDb()->createCommand() - ->delete(Table::DEPRECATIONERRORS) - ->execute(); + $affectedRows = Db::delete(Table::DEPRECATIONERRORS); return (bool)$affectedRows; } diff --git a/src/services/Drafts.php b/src/services/Drafts.php index 6db6db58210..c35dc64c575 100644 --- a/src/services/Drafts.php +++ b/src/services/Drafts.php @@ -8,7 +8,6 @@ namespace craft\services; use Craft; -use craft\base\Element; use craft\base\ElementInterface; use craft\behaviors\DraftBehavior; use craft\db\Query; @@ -79,7 +78,6 @@ public function getEditableDrafts(ElementInterface $element, string $permission return []; } - /** @var Element $element */ $query = $element::find() ->draftOf($element) ->siteId($element->siteId) @@ -107,7 +105,6 @@ public function getEditableDrafts(ElementInterface $element, string $permission public function createDraft(ElementInterface $source, int $creatorId, string $name = null, string $notes = null, array $newAttributes = []): ElementInterface { // Make sure the source isn't a draft or revision - /** @var Element $source */ if ($source->getIsDraft() || $source->getIsRevision()) { throw new InvalidArgumentException('Cannot create a draft from another draft or revision.'); } @@ -152,7 +149,6 @@ public function createDraft(ElementInterface $source, int $creatorId, string $na ]; // Duplicate the element - /** @var Element $draft */ $draft = Craft::$app->getElements()->duplicateElement($source, $newAttributes); $transaction->commit(); @@ -194,7 +190,6 @@ public function saveElementAsDraft(ElementInterface $element, int $creatorId, st // Create the draft row $draftId = $this->_insertDraftRow(null, $creatorId, $name, $notes); - /** @var Element $element */ $element->draftId = $draftId; $element->attachBehavior('draft', new DraftBehavior([ 'creatorId' => $creatorId, @@ -214,7 +209,7 @@ public function saveElementAsDraft(ElementInterface $element, int $creatorId, st */ public function mergeSourceChanges(ElementInterface $draft) { - /** @var Element|DraftBehavior $draft */ + /** @var ElementInterface|DraftBehavior $draft */ /** @var DraftBehavior $behavior */ $behavior = $draft->getBehavior('draft'); @@ -227,7 +222,6 @@ public function mergeSourceChanges(ElementInterface $draft) return; } - /** @var Element[] $sourceElements */ $sourceElements = $draft::find() ->id($sourceId) ->siteId('*') @@ -236,11 +230,6 @@ public function mergeSourceChanges(ElementInterface $draft) ->indexBy('siteId') ->all(); - // Make sure a source element exists for the draft's current site ID - if (!isset($sourceElements[$draft->siteId])) { - throw new Exception('Attempting to merge source changes for a draft in an unsupported site.'); - } - // Make sure the draft actually supports its own site ID $supportedSites = ElementHelper::supportedSitesForElement($draft); $supportedSiteIds = ArrayHelper::getColumn($supportedSites, 'siteId'); @@ -251,7 +240,7 @@ public function mergeSourceChanges(ElementInterface $draft) // Fire a 'beforeMergeSource' event if ($this->hasEventHandlers(self::EVENT_BEFORE_MERGE_SOURCE_CHANGES)) { $this->trigger(self::EVENT_BEFORE_MERGE_SOURCE_CHANGES, new DraftEvent([ - 'source' => $sourceElements[$draft->siteId], + 'source' => $sourceElements[$draft->siteId] ?? reset($sourceElements), 'creatorId' => $behavior->creatorId, 'draftName' => $behavior->draftName, 'draftNotes' => $behavior->draftNotes, @@ -262,10 +251,12 @@ public function mergeSourceChanges(ElementInterface $draft) $transaction = Craft::$app->getDb()->beginTransaction(); try { // Start with $draft's site - $this->_mergeSourceChangesInternal($sourceElements[$draft->siteId], $draft); + if (isset($sourceElements[$draft->siteId])) { + $this->_mergeSourceChangesInternal($sourceElements[$draft->siteId], $draft); + } // Now the other sites - /** @var ElementInterface[]|Element[]|DraftBehavior[] $otherSiteDrafts */ + /** @var ElementInterface[]|DraftBehavior[] $otherSiteDrafts */ $otherSiteDrafts = $draft::find() ->drafts() ->id($draft->id) @@ -293,7 +284,7 @@ public function mergeSourceChanges(ElementInterface $draft) // Fire an 'afterMergeSource' event if ($this->hasEventHandlers(self::EVENT_AFTER_MERGE_SOURCE_CHANGES)) { $this->trigger(self::EVENT_AFTER_MERGE_SOURCE_CHANGES, new DraftEvent([ - 'source' => $sourceElements[$draft->siteId], + 'source' => $sourceElements[$draft->siteId] ?? reset($sourceElements), 'creatorId' => $behavior->creatorId, 'draftName' => $behavior->draftName, 'draftNotes' => $behavior->draftNotes, @@ -310,7 +301,7 @@ public function mergeSourceChanges(ElementInterface $draft) */ private function _mergeSourceChangesInternal(ElementInterface $source, ElementInterface $draft) { - /** @var Element|DraftBehavior $draft */ + /** @var ElementInterface|DraftBehavior $draft */ /** @var DraftBehavior $behavior */ $behavior = $draft->getBehavior('draft'); @@ -344,12 +335,34 @@ private function _mergeSourceChangesInternal(ElementInterface $source, ElementIn */ public function applyDraft(ElementInterface $draft): ElementInterface { - /** @var Element|DraftBehavior $draft */ + /** @var ElementInterface|DraftBehavior $draft */ /** @var DraftBehavior $behavior */ $behavior = $draft->getBehavior('draft'); - /** @var Element $source */ $source = ElementHelper::sourceElement($draft); + // If there is no source element for the same site, find a different site to do this from + if ($source === null) { + $draftSiteIds = ArrayHelper::getColumn(ElementHelper::supportedSitesForElement($draft), 'siteId'); + $source = $draft::find() + ->id($draft->getSourceId()) + ->siteId($draftSiteIds) + ->unique() + ->anyStatus() + ->one(); + if ($source === null) { + throw new Exception('Could not find a source element for the draft in any of its supported sites.'); + } + $draft = $draft::find() + ->drafts() + ->id($draft->id) + ->siteId($source->siteId) + ->anyStatus() + ->one(); + if ($draft === null) { + throw new Exception("Could not load the draft for site ID $source->siteId"); + } + } + // Fire a 'beforeApplyDraft' event if ($this->hasEventHandlers(self::EVENT_BEFORE_APPLY_DRAFT)) { $this->trigger(self::EVENT_BEFORE_APPLY_DRAFT, new DraftEvent([ @@ -453,13 +466,12 @@ public function purgeUnsavedDrafts() $drafts = (new Query()) ->select(['e.draftId', 'e.type']) ->from(['e' => Table::ELEMENTS]) - ->innerJoin(Table::DRAFTS . ' d', '[[d.id]] = [[e.draftId]]') + ->innerJoin(['d' => Table::DRAFTS], '[[d.id]] = [[e.draftId]]') ->where(['d.sourceId' => null]) ->andWhere(['<', 'e.dateCreated', Db::prepareDateForDb($pastTime)]) ->all(); $elementsService = Craft::$app->getElements(); - $db = Craft::$app->getDb(); foreach ($drafts as $draftInfo) { /** @var ElementInterface|string $elementType */ @@ -476,9 +488,9 @@ public function purgeUnsavedDrafts() // Perhaps the draft's row in the `entries` table was deleted manually or something. // Just drop its row in the `drafts` table, and let that cascade to `elements` and whatever other tables // still have rows for the draft. - $db->createCommand() - ->delete(Table::DRAFTS, ['id' => $draftInfo['draftId']]) - ->execute(); + Db::delete(Table::DRAFTS, [ + 'id' => $draftInfo['draftId'], + ]); } Craft::info("Just deleted unsaved draft ID {$draftInfo['draftId']}", __METHOD__); @@ -498,16 +510,13 @@ public function purgeUnsavedDrafts() */ private function _insertDraftRow(int $sourceId = null, int $creatorId, string $name = null, string $notes = null, bool $trackChanges = false): int { - $db = Craft::$app->getDb(); - $db->createCommand() - ->insert(Table::DRAFTS, [ - 'sourceId' => $sourceId, - 'creatorId' => $creatorId, - 'name' => $name, - 'notes' => $notes, - 'trackChanges' => $trackChanges, - ], false) - ->execute(); - return $db->getLastInsertID(Table::DRAFTS); + Db::insert(Table::DRAFTS, [ + 'sourceId' => $sourceId, + 'creatorId' => $creatorId, + 'name' => $name, + 'notes' => $notes, + 'trackChanges' => $trackChanges, + ], false); + return Craft::$app->getDb()->getLastInsertID(Table::DRAFTS); } } diff --git a/src/services/ElementIndexes.php b/src/services/ElementIndexes.php index 0831582136e..167acc19c69 100644 --- a/src/services/ElementIndexes.php +++ b/src/services/ElementIndexes.php @@ -9,11 +9,11 @@ use Craft; use craft\base\ElementInterface; -use craft\base\Field; use craft\base\FieldInterface; use craft\base\PreviewableFieldInterface; use craft\db\Query; use craft\db\Table; +use craft\helpers\Db; use craft\helpers\Json; use yii\base\Component; @@ -119,20 +119,18 @@ public function saveSettings(string $elementType, array $newSettings): bool } } - $affectedRows = Craft::$app->getDb()->createCommand() - ->upsert( - Table::ELEMENTINDEXSETTINGS, - ['type' => $elementType], - ['settings' => Json::encode($settings)]) - ->execute(); + $success = (bool)Db::upsert(Table::ELEMENTINDEXSETTINGS, [ + 'type' => $elementType, + ], [ + 'settings' => Json::encode($settings), + ]); - if ($affectedRows) { - $this->_indexSettings[$elementType] = $settings; - - return true; + if (!$success) { + return false; } - return false; + $this->_indexSettings[$elementType] = $settings; + return true; } /** @@ -213,7 +211,6 @@ public function getAvailableTableAttributes(string $elementType, bool $includeFi if ($includeFields) { // Mix in custom fields foreach ($this->getAvailableTableFields($elementType) as $field) { - /** @var Field $field */ $attributes['field:' . $field->id] = ['label' => Craft::t('site', $field->name)]; } } diff --git a/src/services/Elements.php b/src/services/Elements.php index 0819fcc1214..9780221103f 100644 --- a/src/services/Elements.php +++ b/src/services/Elements.php @@ -9,11 +9,9 @@ use Craft; use craft\base\Element; -use craft\base\ElementAction; use craft\base\ElementActionInterface; use craft\base\ElementExporterInterface; use craft\base\ElementInterface; -use craft\base\Field; use craft\behaviors\DraftBehavior; use craft\behaviors\RevisionBehavior; use craft\db\Query; @@ -21,7 +19,6 @@ use craft\db\Table; use craft\elements\Asset; use craft\elements\Category; -use craft\elements\db\ElementQuery; use craft\elements\db\ElementQueryInterface; use craft\elements\Entry; use craft\elements\GlobalSet; @@ -43,6 +40,7 @@ use craft\helpers\DateTimeHelper; use craft\helpers\Db; use craft\helpers\ElementHelper; +use craft\helpers\Queue; use craft\helpers\StringHelper; use craft\queue\jobs\FindAndReplace; use craft\queue\jobs\UpdateElementSlugsAndUris; @@ -50,12 +48,12 @@ use craft\records\Element as ElementRecord; use craft\records\Element_SiteSettings as Element_SiteSettingsRecord; use craft\records\StructureElement as StructureElementRecord; +use craft\validators\HandleValidator; use craft\validators\SlugValidator; use yii\base\Behavior; use yii\base\Component; use yii\base\Exception; use yii\base\InvalidArgumentException; -use yii\base\NotSupportedException; use yii\db\Exception as DbException; /** @@ -272,6 +270,23 @@ public function createElement($config): ElementInterface return ComponentHelper::createComponent($config, ElementInterface::class); } + /** + * Creates an element query for a given element type. + * + * @param string $elementType The element class + * @return ElementQueryInterface The element query + * @throws InvalidArgumentException if $elementType is not a valid element + * @since 3.5.0 + */ + public function createElementQuery(string $elementType): ElementQueryInterface + { + if (!is_subclass_of($elementType, ElementInterface::class)) { + throw new InvalidArgumentException("$elementType is not a valid element."); + } + + return $elementType::find(); + } + // Finding Elements // ------------------------------------------------------------------------- @@ -308,9 +323,7 @@ public function getElementById(int $elementId, string $elementType = null, $site return null; } - /** @var Element $elementType */ - /** @var ElementQuery $query */ - $query = $elementType::find(); + $query = $this->createElementQuery($elementType); $query->id = $elementId; $query->siteId = $siteId; $query->anyStatus(); @@ -364,8 +377,8 @@ public function getElementByUri(string $uri, int $siteId = null, bool $enabledOn // First get the element ID and type $query = (new Query()) ->select(['elements.id', 'elements.type']) - ->from(['{{%elements}} elements']) - ->innerJoin('{{%elements_sites}} elements_sites', '[[elements_sites.elementId]] = [[elements.id]]') + ->from(['elements' => Table::ELEMENTS]) + ->innerJoin(['elements_sites' => Table::ELEMENTS_SITES], '[[elements_sites.elementId]] = [[elements.id]]') ->where([ 'elements_sites.siteId' => $siteId, ]); @@ -525,7 +538,6 @@ public function getEnabledSiteIdsForElement(int $elementId): array public function saveElement(ElementInterface $element, bool $runValidation = true, bool $propagate = true, bool $updateSearchIndex = null): bool { // Force propagation for new elements - /** @var Element $element */ $propagate = !$element->id || $propagate; return $this->_saveElementInternal($element, $runValidation, $propagate, $updateSearchIndex); @@ -554,11 +566,9 @@ public function resaveElements(ElementQueryInterface $query, bool $continueOnErr $position = 0; try { - /** @var ElementQuery $query */ foreach ($query->each() as $element) { $position++; - /** @var Element $element */ $element->setScenario(Element::SCENARIO_ESSENTIALS); $element->resaving = true; @@ -651,14 +661,11 @@ public function propagateElements(ElementQueryInterface $query, $siteIds = null, $position = 0; try { - /** @var ElementQuery $query */ foreach ($query->each() as $element) { $position++; - /** @var Element $element */ $element->setScenario(Element::SCENARIO_ESSENTIALS); $elementSiteIds = $siteIds ?? ArrayHelper::getColumn(ElementHelper::supportedSitesForElement($element), 'siteId'); - /** @var ElementInterface|string $elementType */ $elementType = get_class($element); // Fire a 'beforePropagateElement' event @@ -677,7 +684,6 @@ public function propagateElements(ElementQueryInterface $query, $siteIds = null, foreach ($elementSiteIds as $siteId) { if ($siteId != $element->siteId) { // Make sure the site element wasn't updated more recently than the main one - /** @var Element $siteElement */ $siteElement = $this->getElementById($element->id, $elementType, $siteId); if ($siteElement === null || $siteElement->dateUpdated < $element->dateUpdated) { $this->propagateElement($element, $siteId, $siteElement ?? false); @@ -729,17 +735,16 @@ public function propagateElements(ElementQueryInterface $query, $siteIds = null, public function duplicateElement(ElementInterface $element, array $newAttributes = []): ElementInterface { // Make sure the element exists - /** @var Element $element */ if (!$element->id) { throw new Exception('Attempting to duplicate an unsaved element.'); } // Create our first clone for the $element's site $element->getFieldValues(); - /** @var Element $mainClone */ $mainClone = clone $element; $mainClone->id = null; $mainClone->uid = null; + $mainClone->elementSiteId = null; $mainClone->contentId = null; $mainClone->dateCreated = null; $mainClone->duplicateOf = $element; @@ -757,7 +762,7 @@ public function duplicateElement(ElementInterface $element, array $newAttributes } // Make sure the element actually supports its own site ID - $supportedSites = ElementHelper::supportedSitesForElement($mainClone); + $supportedSites = ElementHelper::supportedSitesForElement($element); $supportedSiteIds = ArrayHelper::getColumn($supportedSites, 'siteId'); if (!in_array($mainClone->siteId, $supportedSiteIds, false)) { throw new Exception('Attempting to duplicate an element in an unsupported site.'); @@ -791,7 +796,7 @@ public function duplicateElement(ElementInterface $element, array $newAttributes // Propagate it foreach ($supportedSites as $siteInfo) { if ($siteInfo['siteId'] != $mainClone->siteId) { - $siteQuery = $element::find() + $siteQuery = $this->createElementQuery(get_class($element)) ->id($element->id ?: false) ->siteId($siteInfo['siteId']) ->anyStatus(); @@ -810,13 +815,13 @@ public function duplicateElement(ElementInterface $element, array $newAttributes } $siteElement->getFieldValues(); - /** @var Element $siteClone */ $siteClone = clone $siteElement; $siteClone->duplicateOf = $siteElement; $siteClone->propagating = true; $siteClone->id = $mainClone->id; $siteClone->uid = $mainClone->uid; $siteClone->enabled = $mainClone->enabled; + $siteClone->elementSiteId = null; $siteClone->contentId = null; $siteClone->dateCreated = null; @@ -877,9 +882,8 @@ public function duplicateElement(ElementInterface $element, array $newAttributes */ public function updateElementSlugAndUri(ElementInterface $element, bool $updateOtherSites = true, bool $updateDescendants = true, bool $queue = false) { - /** @var Element $element */ if ($queue) { - Craft::$app->getQueue()->push(new UpdateElementSlugsAndUris([ + Queue::push(new UpdateElementSlugsAndUris([ 'elementId' => $element->id, 'elementType' => get_class($element), 'siteId' => $element->siteId, @@ -901,18 +905,13 @@ public function updateElementSlugAndUri(ElementInterface $element, bool $updateO ])); } - Craft::$app->getDb()->createCommand() - ->update( - Table::ELEMENTS_SITES, - [ - 'slug' => $element->slug, - 'uri' => $element->uri - ], - [ - 'elementId' => $element->id, - 'siteId' => $element->siteId - ]) - ->execute(); + Db::update(Table::ELEMENTS_SITES, [ + 'slug' => $element->slug, + 'uri' => $element->uri, + ], [ + 'elementId' => $element->id, + 'siteId' => $element->siteId, + ]); // Fire a 'afterUpdateSlugAndUri' event if ($this->hasEventHandlers(self::EVENT_AFTER_UPDATE_SLUG_AND_URI)) { @@ -940,13 +939,12 @@ public function updateElementSlugAndUri(ElementInterface $element, bool $updateO */ public function updateElementSlugAndUriInOtherSites(ElementInterface $element) { - /** @var Element $element */ foreach (Craft::$app->getSites()->getAllSiteIds() as $siteId) { if ($siteId == $element->siteId) { continue; } - $elementInOtherSite = $element::find() + $elementInOtherSite = $this->createElementQuery(get_class($element)) ->id($element->id) ->siteId($siteId) ->one(); @@ -966,9 +964,7 @@ public function updateElementSlugAndUriInOtherSites(ElementInterface $element) */ public function updateDescendantSlugsAndUris(ElementInterface $element, bool $updateOtherSites = true, bool $queue = false) { - /** @var Element $element */ - /** @var ElementQuery $query */ - $query = $element::find() + $query = $this->createElementQuery(get_class($element)) ->descendantOf($element) ->descendantDist(1) ->anyStatus() @@ -978,7 +974,7 @@ public function updateDescendantSlugsAndUris(ElementInterface $element, bool $up $childIds = $query->ids(); if (!empty($childIds)) { - Craft::$app->getQueue()->push(new UpdateElementSlugsAndUris([ + Queue::push(new UpdateElementSlugsAndUris([ 'elementId' => $childIds, 'elementType' => get_class($element), 'siteId' => $element->siteId, @@ -1041,10 +1037,7 @@ public function mergeElementsByIds(int $mergedElementId, int $prevailingElementI */ public function mergeElements(ElementInterface $mergedElement, ElementInterface $prevailingElement): bool { - /** @var Element $mergedElement */ - /** @var Element $prevailingElement */ - $db = Craft::$app->getDb(); - $transaction = $db->beginTransaction(); + $transaction = Craft::$app->getDb()->beginTransaction(); try { // Update any relations that point to the merged element $relations = (new Query()) @@ -1066,16 +1059,11 @@ public function mergeElements(ElementInterface $mergedElement, ElementInterface ->exists(); if (!$persistingElementIsRelatedToo) { - $db->createCommand() - ->update( - Table::RELATIONS, - [ - 'targetId' => $prevailingElement->id - ], - [ - 'id' => $relation['id'] - ]) - ->execute(); + Db::update(Table::RELATIONS, [ + 'targetId' => $prevailingElement->id, + ], [ + 'id' => $relation['id'], + ]); } } @@ -1097,15 +1085,11 @@ public function mergeElements(ElementInterface $mergedElement, ElementInterface ->exists(); if (!$persistingElementIsInStructureToo) { - $db->createCommand() - ->update(Table::STRUCTUREELEMENTS, - [ - 'elementId' => $prevailingElement->id - ], - [ - 'id' => $structureElement['id'] - ]) - ->execute(); + Db::update(Table::STRUCTUREELEMENTS, [ + 'elementId' => $prevailingElement->id, + ], [ + 'id' => $structureElement['id'], + ]); } } @@ -1115,15 +1099,14 @@ public function mergeElements(ElementInterface $mergedElement, ElementInterface if ($elementType !== null && ($refHandle = $elementType::refHandle()) !== null) { $refTagPrefix = "{{$refHandle}:"; - $queue = Craft::$app->getQueue(); - $queue->push(new FindAndReplace([ + Queue::push(new FindAndReplace([ 'description' => Craft::t('app', 'Updating element references'), 'find' => $refTagPrefix . $mergedElement->id . ':', 'replace' => $refTagPrefix . $prevailingElement->id . ':', ])); - $queue->push(new FindAndReplace([ + Queue::push(new FindAndReplace([ 'description' => Craft::t('app', 'Updating element references'), 'find' => $refTagPrefix . $mergedElement->id . '}', 'replace' => $refTagPrefix . $prevailingElement->id . '}', @@ -1205,7 +1188,6 @@ public function deleteElementById(int $elementId, string $elementType = null, in */ public function deleteElement(ElementInterface $element, bool $hardDelete = false): bool { - /** @var Element $element */ // Fire a 'beforeDeleteElement' event $event = new DeleteElementEvent([ 'element' => $element, @@ -1239,12 +1221,12 @@ public function deleteElement(ElementInterface $element, bool $hardDelete = fals Craft::$app->getTemplateCaches()->deleteCachesByElementId($element->id, false); if ($element->hardDelete) { - $db->createCommand() - ->delete(Table::ELEMENTS, ['id' => $element->id]) - ->execute(); - $db->createCommand() - ->delete(Table::SEARCHINDEX, ['elementId' => $element->id]) - ->execute(); + Db::delete(Table::ELEMENTS, [ + 'id' => $element->id, + ]); + Db::delete(Table::SEARCHINDEX, [ + 'elementId' => $element->id, + ]); } else { // Soft delete the elements table row $db->createCommand() @@ -1299,7 +1281,6 @@ public function restoreElements(array $elements): bool { // Fire "before" events foreach ($elements as $element) { - /** @var Element $element */ // Fire a 'beforeRestoreElement' event if ($this->hasEventHandlers(self::EVENT_BEFORE_RESTORE_ELEMENT)) { $this->trigger(self::EVENT_BEFORE_RESTORE_ELEMENT, new ElementEvent([ @@ -1330,12 +1311,11 @@ public function restoreElements(array $elements): bool // Get the element in each supported site $siteElements = []; - /** @var Element|string $class */ $class = get_class($element); foreach ($supportedSites as $siteInfo) { $siteId = $siteInfo['siteId']; if ($siteId != $element->siteId) { - $siteElement = $class::find() + $siteElement = $this->createElementQuery($class) ->id($element->id) ->siteId($siteId) ->anyStatus() @@ -1442,10 +1422,7 @@ public function getAllElementTypes(): array */ public function createAction($config): ElementActionInterface { - /** @var ElementAction $action */ - $action = ComponentHelper::createComponent($config, ElementActionInterface::class); - - return $action; + return ComponentHelper::createComponent($config, ElementActionInterface::class); } /** @@ -1505,37 +1482,42 @@ public function parseRefs(string $str, int $defaultSiteId = null): string $sitesService = Craft::$app->getSites(); $allRefTagTokens = []; $str = preg_replace_callback( - '/\{([\w\\\\]+)\:([^@\:\}]+)(?:@([^\:\}]+))?(?:\:([^\}]+))?\}/', - function($matches) use ( + '/\{([\w\\\\]+)\:([^@\:\}]+)(?:@([^\:\}]+))?(?:\:([^\}\| ]+))?(?: *\|\| *([^\}]+))?\}/', + function(array $matches) use ( $defaultSiteId, $sitesService, &$allRefTagTokens ) { + $matches = array_pad($matches, 6, null); + list($fullMatch, $elementType, $ref, $siteId, $attribute, $fallback) = $matches; + if ($fallback === null) { + $fallback = $fullMatch; + } + // Does it already have a full element type class name? - if (is_subclass_of($matches[1], ElementInterface::class)) { - $elementType = $matches[1]; - } else if (($elementType = $this->getElementTypeByRefHandle($matches[1])) === null) { - // Leave the tag alone - return $matches[0]; + if ( + !is_subclass_of($elementType, ElementInterface::class) && + ($elementType = $this->getElementTypeByRefHandle($elementType)) === null + ) { + return $fallback; } // Get the site - if (!empty($matches[3])) { - if (is_numeric($matches[3])) { - $siteId = (int)$matches[3]; + if (!empty($siteId)) { + if (is_numeric($siteId)) { + $siteId = (int)$siteId; } else { try { - if (StringHelper::isUUID($matches[3])) { - $site = $sitesService->getSiteByUid($matches[3]); + if (StringHelper::isUUID($siteId)) { + $site = $sitesService->getSiteByUid($siteId); } else { - $site = $sitesService->getSiteByHandle($matches[3]); + $site = $sitesService->getSiteByHandle($siteId); } } catch (SiteNotFoundException $e) { $site = null; } if (!$site) { - // Leave the tag alone - return $matches[0]; + return $fallback; } $siteId = $site->id; } @@ -1543,9 +1525,9 @@ function($matches) use ( $siteId = $defaultSiteId; } - $refType = is_numeric($matches[2]) ? 'id' : 'ref'; + $refType = is_numeric($ref) ? 'id' : 'ref'; $token = '{' . StringHelper::randomString(9) . '}'; - $allRefTagTokens[$siteId][$elementType][$refType][$matches[2]][] = [$token, $matches]; + $allRefTagTokens[$siteId][$elementType][$refType][$ref][] = [$token, $attribute, $fallback, $fullMatch]; return $token; }, $str, -1, $count); @@ -1561,11 +1543,10 @@ function($matches) use ( foreach ($allRefTagTokens as $siteId => $siteTokens) { foreach ($siteTokens as $elementType => $tokensByType) { - /** @var Element|string $elementType */ foreach ($tokensByType as $refType => $tokensByName) { // Get the elements, indexed by their ref value $refNames = array_keys($tokensByName); - $elementQuery = $elementType::find() + $elementQuery = $this->createElementQuery($elementType) ->siteId($siteId) ->anyStatus(); @@ -1581,9 +1562,9 @@ function($matches) use ( foreach ($tokensByName as $refName => $tokens) { $element = $elements[$refName] ?? null; - foreach ($tokens as list($token, $matches)) { + foreach ($tokens as list($token, $attribute, $fallback, $fullMatch)) { $search[] = $token; - $replace[] = $this->_getRefTokenReplacement($element, $matches); + $replace[] = $this->_getRefTokenReplacement($element, $attribute, $fallback, $fullMatch); } } } @@ -1608,7 +1589,6 @@ function($matches) use ( */ public function setPlaceholderElement(ElementInterface $element) { - /** @var Element $element */ // Won't be able to do anything with this if it doesn't have an ID or site ID if (!$element->id || !$element->siteId) { throw new InvalidArgumentException('Placeholder element is missing an ID'); @@ -1658,215 +1638,198 @@ public function getPlaceholderElement(int $sourceId, int $siteId) */ public function eagerLoadElements(string $elementType, array $elements, $with) { - /** @var Element[] $elements */ + /** @var ElementInterface|string $elementType */ // Bail if there aren't even any elements if (empty($elements)) { return; } - // Normalize the paths and find any custom path criterias + $elements = array_values($elements); + + // Normalize the paths and group based on the top level eager loading handle if (is_string($with)) { $with = StringHelper::split($with); } - $paths = []; - $pathCriterias = []; - $countPaths = []; + $groups = []; foreach ($with as $path) { - // Using the array syntax? - // ['foo.bar'] or ['foo.bar', criteria] + // Separate the path and the criteria if (is_array($path)) { - if (!empty($path[1])) { - // Is this a count path? - if (ArrayHelper::remove($path[1], 'count', false)) { - $countPaths[$path[0]] = true; - } - $pathCriterias['__root__.' . $path[0]] = $path[1]; - } - - $paths[] = $path[0]; + $criteria = $path[1] ?? null; + $path = $path[0]; } else { - $paths[] = $path; + $criteria = null; } - } - // Load 'em up! - $elementsByPath = ['__root__' => $elements]; - $elementTypesByPath = ['__root__' => $elementType]; - $eagerLoadingMapsByPath = []; + // Split the path into the top segment and subpath + if (($dot = strpos($path, '.')) !== false) { + $handle = substr($path, 0, $dot); + $subpath = substr($path, $dot + 1); + } else { + $handle = $path; + $subpath = null; + } - foreach ($paths as $path) { - $pathSegments = explode('.', $path); - $totalSegments = count($pathSegments); - $sourcePath = '__root__'; + // Get the handle & alias + if (preg_match('/^(' . HandleValidator::$handlePattern . ')\s+as\s+(' . HandleValidator::$handlePattern . ')$/', $handle, $match)) { + $handle = $match[1]; + $alias = $match[2]; + } else { + $alias = $handle; + } - foreach ($pathSegments as $segIndex => $segment) { - $targetPath = $sourcePath . '.' . $segment; - $pathCriteria = $pathCriterias[$targetPath] ?? []; + if (!isset($groups[$alias])) { + // [handle, criteria, getCount, subWith] + $groups[$alias] = [$handle, [], false, []]; + } - // Are we just fetching the count? - $getCount = isset($countPaths[$path]) && $segIndex === $totalSegments - 1; + // Only set the criteria if there's no subpath + if ($subpath === null) { + if ($criteria !== null) { + $getCount = ArrayHelper::remove($criteria, 'count', false); + $groups[$alias][1] = $criteria; + $groups[$alias][2] = $groups[$alias][2] || $getCount; + } + } else { + // Add this as a nested "with" + $groups[$alias][3][] = [$subpath, $criteria]; + } + } - // Figure out the path mapping wants a custom order - $useCustomOrder = !$getCount && ( - !empty($pathCriteria['orderBy']) || - !empty($pathCriteria['order']) - ); + foreach ($groups as $alias => list($handle, $criteria, $getCount, $subWith)) { + /** @var string $handle */ + /** @var array $criteria */ + /** @var bool $getCount */ + /** @var array $subWith */ - // Make sure we haven't already eager-loaded this target path - if ($getCount || !isset($elementsByPath[$targetPath])) { - // Have we already fetched the map from an earlier `count` path? - if (array_key_exists($targetPath, $eagerLoadingMapsByPath)) { - $map = $eagerLoadingMapsByPath[$targetPath]; - } else { - // Get the eager-loading map from the source element type - /** @var Element $sourceElementType */ - $sourceElementType = $elementTypesByPath[$sourcePath]; - $map = $eagerLoadingMapsByPath[$targetPath] = $sourceElementType::eagerLoadingMap(array_values($elementsByPath[$sourcePath]), $segment); - } + // Figure out the path mapping wants a custom order + $useCustomOrder = !empty($criteria['orderBy']) || !empty($criteria['order']); - if ($map === null) { - break; - } + // Get the eager-loading map from the source element type + $map = $elementType::eagerLoadingMap($elements, $handle); - $targetElementIdsBySourceIds = null; - $query = null; - $offset = 0; - $limit = null; - - if ($map && !empty($map['map'])) { - // Remember the element type in case there are more segments after this - $elementTypesByPath[$targetPath] = $map['elementType']; + if ($map === null) { + // Null means to skip eager-loading this segment + continue; + } - // Loop through the map to find: - // - unique target element IDs - // - target element IDs indexed by source element IDs - $uniqueTargetElementIds = []; - $targetElementIdsBySourceIds = []; + $targetElementIdsBySourceIds = null; + $query = null; + $offset = 0; + $limit = null; + + if (!empty($map['map'])) { + // Loop through the map to find: + // - unique target element IDs + // - target element IDs indexed by source element IDs + $uniqueTargetElementIds = []; + $targetElementIdsBySourceIds = []; + + foreach ($map['map'] as $mapping) { + $uniqueTargetElementIds[$mapping['target']] = true; + $targetElementIdsBySourceIds[$mapping['source']][$mapping['target']] = true; + } - foreach ($map['map'] as $mapping) { - if (!in_array($mapping['target'], $uniqueTargetElementIds, false)) { - $uniqueTargetElementIds[] = $mapping['target']; - } + // Get the target elements + $query = $this->createElementQuery($map['elementType']); - $targetElementIdsBySourceIds[$mapping['source']][$mapping['target']] = true; - } + // Default to no order, offset, or limit, but allow the element type/path criteria to override + $query->orderBy = null; + $query->offset = null; + $query->limit = null; - // Get the target elements - /** @var Element $targetElementType */ - $targetElementType = $map['elementType']; - /** @var ElementQuery $query */ - $query = $targetElementType::find(); + $criteria = array_merge( + $map['criteria'] ?? [], + $criteria + ); - // Default to no order, offset, or limit, but allow the element type/path criteria to override - $query->orderBy = null; - $query->offset = null; - $query->limit = null; + // Save the offset & limit params for later + $offset = ArrayHelper::remove($criteria, 'offset', 0); + $limit = ArrayHelper::remove($criteria, 'limit'); - $criteria = array_merge( - $map['criteria'] ?? [], - $pathCriteria - ); + Craft::configure($query, $criteria); - // Save the offset & limit params for later - $offset = ArrayHelper::remove($criteria, 'offset', 0); - $limit = ArrayHelper::remove($criteria, 'limit'); + if (!$query->siteId) { + $query->siteId = reset($elements)->siteId; + } - Craft::configure($query, $criteria); + $query->andWhere([ + 'elements.id' => array_keys($uniqueTargetElementIds), + ]); + } - if (!$query->siteId) { - $query->siteId = reset($elements)->siteId; + // Do we just need the count? + if ($getCount && empty($subWith)) { + // Just fetch the target elements’ IDs + $targetElementIds = $query ? array_flip($query->ids()) : []; + + // Loop through the source elements and count up their targets + foreach ($elements as $sourceElement) { + $count = 0; + if (!empty($targetElementIds) && isset($targetElementIdsBySourceIds[$sourceElement->id])) { + foreach (array_keys($targetElementIdsBySourceIds[$sourceElement->id]) as $targetElementId) { + if (isset($targetElementIds[$targetElementId])) { + $count++; + } } - - $query->andWhere(['elements.id' => $uniqueTargetElementIds]); } - - if ($getCount) { - // Just fetch the target elements’ IDs - $targetElementIds = $query ? array_flip($query->ids()) : []; - - // Loop through the source elements and count up their targets - foreach ($elementsByPath[$sourcePath] as $sourceElement) { - $count = 0; - if (!empty($targetElementIds) && isset($targetElementIdsBySourceIds[$sourceElement->id])) { - foreach (array_keys($targetElementIdsBySourceIds[$sourceElement->id]) as $targetElementId) { - if (isset($targetElementIds[$targetElementId])) { - $count++; - } + $sourceElement->setEagerLoadedElementCount($alias, $count); + } + } else { + /** @var array|ElementInterface[] $targetElementData */ + $targetElementData = $query ? $query->asArray()->indexBy('id')->all() : []; + $targetElements = []; + + // Tell the source elements about their eager-loaded elements + foreach ($elements as $sourceElement) { + $targetElementIdsForSource = []; + $targetElementsForSource = []; + + if (isset($targetElementIdsBySourceIds[$sourceElement->id])) { + if ($useCustomOrder) { + // Assign the elements in the order they were returned from the query + foreach ($targetElementData as &$elementData) { + /** @var array|ElementInterface $elementData */ + if (isset($targetElementIdsBySourceIds[$sourceElement->id][$elementData['id']])) { + $targetElementIdsForSource[] = $elementData['id']; } } - $sourceElement->setEagerLoadedElementCount($segment, $count); - } - } else { - /** @var array|ElementInterface[] $targetElements */ - $targetElements = $query ? $query->asArray()->all() : []; - $elementsByPath[$targetPath] = []; - - // Index the target elements by their IDs if we are using the map-defined order - if (!$useCustomOrder) { - /** @var array|ElementInterface[] $targetElementsById */ - $targetElementsById = []; - foreach ($targetElements as &$targetElement) { - $targetElementsById[$targetElement['id']] = &$targetElement; + unset($elementData); + } else { + // Assign the elements in the order defined by the map + foreach (array_keys($targetElementIdsBySourceIds[$sourceElement->id]) as $targetElementId) { + if (isset($targetElementData[$targetElementId])) { + $targetElementIdsForSource[] = $targetElementId; + } } } - // Tell the source elements about their eager-loaded elements (or lack thereof, as the case may be) - foreach ($elementsByPath[$sourcePath] as $sourceElement) { - /** @var Element $sourceElement */ - $targetElementsForSource = []; - - if (isset($targetElementIdsBySourceIds[$sourceElement->id])) { - if ($useCustomOrder) { - // Assign the elements in the order they were returned from the query - foreach ($targetElements as &$targetElement) { - /** @var array|ElementInterface $targetElement */ - $targetElementId = ArrayHelper::getValue($targetElement, 'id'); - if (isset($targetElementIdsBySourceIds[$sourceElement->id][$targetElementId])) { - $targetElementsForSource[] = &$targetElement; - } - } - } else { - // Assign the elements in the order defined by the map - foreach (array_keys($targetElementIdsBySourceIds[$sourceElement->id]) as $targetElementId) { - if (isset($targetElementsById[$targetElementId])) { - $targetElementsForSource[] = &$targetElementsById[$targetElementId]; - } - } - } - - // Ignore elements that don't fall within the offset & limit - if ($offset || $limit) { - $targetElementsForSource = array_slice($targetElementsForSource, $offset, $limit); - } - - foreach ($targetElementsForSource as &$targetElement) { - // Make sure the element has been instantiated - if (is_array($targetElement)) { - $targetElement = $query->createElement($targetElement); - } + // Ignore elements that don't fall within the offset & limit + if ($offset || $limit) { + $targetElementIdsForSource = array_slice($targetElementIdsForSource, $offset, $limit); + } - // Store it on $elementsByPath FFR - if (!isset($elementsByPath[$targetPath][$targetElement->id])) { - $elementsByPath[$targetPath][$targetElement->id] = $targetElement; - } - } - unset($targetElement); + // Create the elements + foreach ($targetElementIdsForSource as $elementId) { + if (!isset($targetElements[$elementId])) { + $targetElements[$elementId] = $query->createElement($targetElementData[$elementId]); } - - $sourceElement->setEagerLoadedElements($segment, $targetElementsForSource); + $targetElementsForSource[] = $targetElements[$elementId]; } } - } - if (empty($elementsByPath[$targetPath])) { - // Dead end - stop wasting time on this path - break; + $sourceElement->setEagerLoadedElements($alias, $targetElementsForSource); + + if ($getCount) { + $sourceElement->setEagerLoadedElementCount($alias, count($targetElementsForSource)); + } } - // Update the source path - $sourcePath = $targetPath; + // Now eager-load any sub paths + if (!empty($map['map']) && !empty($subWith)) { + $this->eagerLoadElements($map['elementType'], $targetElements, $subWith); + } } } } @@ -1913,23 +1876,22 @@ public function propagateElement(ElementInterface $element, int $siteId, $siteEl */ private function _saveElementInternal(ElementInterface $element, bool $runValidation = true, bool $propagate = true, bool $updateSearchIndex = null): bool { - /** @var Element|DraftBehavior|RevisionBehavior $element */ + /** @var ElementInterface|DraftBehavior|RevisionBehavior $element */ $isNewElement = !$element->id; /** @var DraftBehavior|null $draftBehavior */ $draftBehavior = $element->getIsDraft() ? $element->getBehavior('draft') : null; - $db = Craft::$app->getDb(); - // Are we tracking changes? // todo: remove the tableExists condition after the next breakpoint $trackChanges = ( !$isNewElement && + $element->elementSiteId && $element->duplicateOf === null && $element::trackChanges() && ($draftBehavior->trackChanges ?? true) && !($draftBehavior->mergingChanges ?? false) && - $db->tableExists(Table::CHANGEDATTRIBUTES) + Craft::$app->getDb()->tableExists(Table::CHANGEDATTRIBUTES) ); $dirtyAttributes = []; @@ -2085,7 +2047,7 @@ private function _saveElementInternal(ElementInterface $element, bool $runValida ]); } - if (empty($siteSettingsRecord)) { + if ($isNewSiteElement = empty($siteSettingsRecord)) { // First time we've saved the element for this site $siteSettingsRecord = new Element_SiteSettingsRecord(); $siteSettingsRecord->elementId = $element->id; @@ -2102,7 +2064,7 @@ private function _saveElementInternal(ElementInterface $element, bool $runValida } // Update our list of dirty attributes - if ($trackChanges && !$siteSettingsRecord->getIsNewRecord()) { + if ($trackChanges && !$isNewSiteElement) { ArrayHelper::append($dirtyAttributes, ...array_keys($siteSettingsRecord->getDirtyAttributes([ 'slug', 'uri', @@ -2116,6 +2078,8 @@ private function _saveElementInternal(ElementInterface $element, bool $runValida throw new Exception('Couldn’t save elements’ site settings record.'); } + $element->elementSiteId = $siteSettingsRecord->id; + // Save the content if ($element::hasContent()) { Craft::$app->getContent()->saveContent($element); @@ -2194,19 +2158,12 @@ private function _saveElementInternal(ElementInterface $element, bool $runValida if (Craft::$app->getRequest()->getIsConsoleRequest()) { Craft::$app->getSearch()->indexElementAttributes($element); } else { - $queue = Craft::$app->getQueue(); - $job = new UpdateSearchIndex([ + Queue::push(new UpdateSearchIndex([ 'elementType' => get_class($element), 'elementId' => $element->id, 'siteId' => $propagate ? '*' : $element->siteId, 'fieldHandles' => $element->getDirtyFields(), - ]); - try { - $queue->priority(2048)->push($job); - } catch (NotSupportedException $e) { - // The queue probably doesn't support custom push priorities. Try again without one. - $queue->push($job); - } + ]), 2048); } } @@ -2225,33 +2182,29 @@ private function _saveElementInternal(ElementInterface $element, bool $runValida ArrayHelper::append($dirtyAttributes, ...$element->getDirtyAttributes()); foreach ($dirtyAttributes as $attributeName) { - $db->createCommand() - ->upsert(Table::CHANGEDATTRIBUTES, [ - 'elementId' => $element->id, - 'siteId' => $element->siteId, - 'attribute' => $attributeName, - ], [ - 'dateUpdated' => $timestamp, - 'propagated' => $element->propagating, - 'userId' => $userId, - ], [], false) - ->execute(); + Db::upsert(Table::CHANGEDATTRIBUTES, [ + 'elementId' => $element->id, + 'siteId' => $element->siteId, + 'attribute' => $attributeName, + ], [ + 'dateUpdated' => $timestamp, + 'propagated' => $element->propagating, + 'userId' => $userId, + ], [], false); } if (($fieldLayout = $element->getFieldLayout()) !== null) { foreach ($element->getDirtyFields() as $fieldHandle) { if (($field = $fieldLayout->getFieldByHandle($fieldHandle)) !== null) { - $db->createCommand() - ->upsert(Table::CHANGEDFIELDS, [ - 'elementId' => $element->id, - 'siteId' => $element->siteId, - 'fieldId' => $field->id, - ], [ - 'dateUpdated' => $timestamp, - 'propagated' => $element->propagating, - 'userId' => $userId, - ], [], false) - ->execute(); + Db::upsert(Table::CHANGEDFIELDS, [ + 'elementId' => $element->id, + 'siteId' => $element->siteId, + 'fieldId' => $field->id, + ], [ + 'dateUpdated' => $timestamp, + 'propagated' => $element->propagating, + 'userId' => $userId, + ], [], false); } } } @@ -2273,9 +2226,7 @@ private function _saveElementInternal(ElementInterface $element, bool $runValida */ private function _propagateElement(ElementInterface $element, array $siteInfo, $siteElement = null) { - /** @var Element $element */ // Try to fetch the element in this site - /** @var Element|null $siteElement */ if ($siteElement === null && $element->id) { $siteElement = $this->getElementById($element->id, get_class($element), $siteInfo['siteId']); } else if (!$siteElement) { @@ -2286,6 +2237,7 @@ private function _propagateElement(ElementInterface $element, array $siteInfo, $ if ($isNewSiteForElement = ($siteElement === null)) { $siteElement = clone $element; $siteElement->siteId = $siteInfo['siteId']; + $siteElement->elementSiteId = null; $siteElement->contentId = null; $siteElement->enabledForSite = $siteInfo['enabledByDefault']; @@ -2316,7 +2268,6 @@ private function _propagateElement(ElementInterface $element, array $siteInfo, $ } else if (($fieldLayout = $element->getFieldLayout()) !== null) { // Only copy the non-translatable field values foreach ($fieldLayout->getFields() as $field) { - /** @var Field $field */ // Has this field changed, and does it produce the same translation key as it did for the master element? if ( $element->isFieldDirty($field->handle) && @@ -2358,21 +2309,22 @@ private function _cascadeDeleteDraftsAndRevisions(int $sourceId, bool $delete = ]; $db = Craft::$app->getDb(); + $elementsTable = Table::ELEMENTS; foreach (['draftId' => Table::DRAFTS, 'revisionId' => Table::REVISIONS] as $fk => $table) { if ($db->getIsMysql()) { $sql = <<{$matches[4]})) { + if (empty($attribute) || !isset($element->$attribute)) { // Default to the URL return (string)$element->getUrl(); } try { - $value = $element->{$matches[4]}; + $value = $element->$attribute; if (is_object($value) && !method_exists($value, '__toString')) { throw new Exception('Object of class ' . get_class($value) . ' could not be converted to string'); @@ -2411,10 +2365,10 @@ private function _getRefTokenReplacement(ElementInterface $element = null, array return $this->parseRefs((string)$value); } catch (\Throwable $e) { // Log it - Craft::error('An exception was thrown when parsing the ref tag "' . $matches[0] . "\":\n" . $e->getMessage(), __METHOD__); + Craft::error("An exception was thrown when parsing the ref tag \"$fullMatch\":\n" . $e->getMessage(), __METHOD__); - // Replace the token with the original ref tag - return $matches[0]; + // Replace the token with the default value + return $fallback; } } } diff --git a/src/services/Entries.php b/src/services/Entries.php index 9996eced9e1..303bdfb98ba 100644 --- a/src/services/Entries.php +++ b/src/services/Entries.php @@ -9,6 +9,7 @@ use Craft; use craft\db\Query; +use craft\db\Table; use craft\elements\Entry; use yii\base\Component; @@ -29,26 +30,28 @@ class Entries extends Component * ``` * * @param int $entryId The entry’s ID. - * @param int|null $siteId The site to fetch the entry in. Defaults to the current site. + * @param int|int[]|string|null $siteId The site(s) to fetch the entry in. + * Defaults to the current site. + * @param array $criteria * @return Entry|null The entry with the given ID, or `null` if an entry could not be found. */ - public function getEntryById(int $entryId, int $siteId = null) + public function getEntryById(int $entryId, $siteId = null, array $criteria = []) { if (!$entryId) { return null; } // Get the structure ID - $structureId = (new Query()) - ->select(['sections.structureId']) - ->from(['{{%entries}} entries']) - ->innerJoin('{{%sections}} sections', '[[sections.id]] = [[entries.sectionId]]') - ->where(['entries.id' => $entryId]) - ->scalar(); + if (!isset($criteria['structureId'])) { + $criteria['structureId'] = (new Query()) + ->select(['sections.structureId']) + ->from(['entries' => Table::ENTRIES]) + ->innerJoin(['sections' => Table::SECTIONS], '[[sections.id]] = [[entries.sectionId]]') + ->where(['entries.id' => $entryId]) + ->scalar(); + } /** @noinspection PhpIncompatibleReturnTypeInspection */ - return Craft::$app->getElements()->getElementById($entryId, Entry::class, $siteId, [ - 'structureId' => $structureId, - ]); + return Craft::$app->getElements()->getElementById($entryId, Entry::class, $siteId, $criteria); } } diff --git a/src/services/Fields.php b/src/services/Fields.php index 40278d3021b..9324e8bf103 100644 --- a/src/services/Fields.php +++ b/src/services/Fields.php @@ -179,7 +179,7 @@ class Fields extends Component private $_groups; /** - * @var Field[] + * @var FieldInterface[] */ private $_fields; @@ -411,7 +411,6 @@ public function deleteGroup(FieldGroup $group): bool // Manually delete the fields (rather than relying on cascade deletes) so we have a chance to delete the // content columns - /** @var Field[] $fields */ $fields = $this->getFieldsByGroupId($group->id); foreach ($fields as $field) { @@ -471,7 +470,7 @@ public function getFieldTypesWithContent(): array $fieldTypes = []; foreach ($this->getAllFieldTypes() as $fieldType) { - /** @var Field|string $fieldType */ + /** @var FieldInterface|string $fieldType */ if ($fieldType::hasContentColumn()) { $fieldTypes[] = $fieldType; } @@ -489,7 +488,6 @@ public function getFieldTypesWithContent(): array */ public function getCompatibleFieldTypes(FieldInterface $field, bool $includeCurrent = true): array { - /** @var Field $field */ if (!$field::hasContentColumn()) { return $includeCurrent ? [get_class($field)] : []; } @@ -549,7 +547,6 @@ public function createField($config): FieldInterface } try { - /** @var Field $field */ $field = ComponentHelper::createComponent($config, FieldInterface::class); } catch (MissingComponentException $e) { $config['errorMessage'] = $e->getMessage(); @@ -592,7 +589,6 @@ public function getAllFields($context = null): array } return ArrayHelper::where($this->_fields, function(FieldInterface $field) use ($context) { - /** @var Field $field */ return in_array($field->context, $context, true); }); } @@ -684,8 +680,8 @@ public function getFieldsByGroupId(int $groupId): array public function getFieldsByElementType(string $elementType): array { $results = $this->_createFieldQuery() - ->innerJoin('{{%fieldlayoutfields}} flf', '[[flf.fieldId]] = [[fields.id]]') - ->innerJoin('{{%fieldlayouts}} fl', '[[fl.id]] = [[flf.layoutId]]') + ->innerJoin(['flf' => Table::FIELDLAYOUTFIELDS], '[[flf.fieldId]] = [[fields.id]]') + ->innerJoin(['fl' => Table::FIELDLAYOUTS], '[[fl.id]] = [[flf.layoutId]]') ->where([ 'fl.type' => $elementType, 'fl.dateDeleted' => null, @@ -742,7 +738,6 @@ public function createFieldConfig(FieldInterface $field): array */ public function saveField(FieldInterface $field, bool $runValidation = true): bool { - /** @var Field $field */ $isNewField = $field->getIsNew(); // Fire a 'beforeSaveField' event @@ -789,7 +784,6 @@ public function saveField(FieldInterface $field, bool $runValidation = true): bo */ public function prepFieldForSave(FieldInterface $field) { - /** @var Field $field */ // Clear the translation key format if not using a custom translation method if ($field->translationMethod !== Field::TRANSLATION_METHOD_CUSTOM) { $field->translationKeyFormat = null; @@ -823,6 +817,10 @@ public function handleChangedField(ConfigEvent $event) $data = $event->newValue; $fieldUid = $event->tokenMatches[0]; + if (!is_array($data)) { + return; + } + $this->applyFieldSave($fieldUid, $data, 'global'); } @@ -852,7 +850,6 @@ public function deleteFieldById(int $fieldId): bool */ public function deleteField(FieldInterface $field): bool { - /** @var Field $field */ // Fire a 'beforeDeleteField' event if ($this->hasEventHandlers(self::EVENT_BEFORE_DELETE_FIELD)) { $this->trigger(self::EVENT_BEFORE_DELETE_FIELD, new FieldEvent([ @@ -908,7 +905,6 @@ public function applyFieldDelete($fieldUid) return; } - /** @var Field $field */ $field = $this->getFieldById($fieldRecord->id); // Fire a 'beforeApplyFieldDelete' event @@ -934,9 +930,9 @@ public function applyFieldDelete($fieldUid) } // Delete the row in fields - Craft::$app->getDb()->createCommand() - ->delete(Table::FIELDS, ['id' => $fieldRecord->id]) - ->execute(); + Db::delete(Table::FIELDS, [ + 'id' => $fieldRecord->id, + ]); $field->afterDelete(); @@ -1103,8 +1099,8 @@ public function getFieldsByLayoutId(int $layoutId): array 'flf.required', 'flf.sortOrder', ]) - ->innerJoin('{{%fieldlayoutfields}} flf', '[[flf.fieldId]] = [[fields.id]]') - ->innerJoin('{{%fieldlayouttabs}} flt', '[[flt.id]] = [[flf.tabId]]') + ->innerJoin(['flf' => Table::FIELDLAYOUTFIELDS], '[[flf.fieldId]] = [[fields.id]]') + ->innerJoin(['flt' => Table::FIELDLAYOUTTABS], '[[flt.id]] = [[flf.tabId]]') ->where(['flf.layoutId' => $layoutId]) ->orderBy(['flt.sortOrder' => SORT_ASC, 'flf.sortOrder' => SORT_ASC]) ->all(); @@ -1240,14 +1236,14 @@ public function saveLayout(FieldLayout $layout, bool $runValidation = true): boo if (!$isNewLayout) { // Delete the old tabs/fields - Craft::$app->getDb()->createCommand() - ->delete(Table::FIELDLAYOUTTABS, ['layoutId' => $layout->id]) - ->execute(); + Db::delete(Table::FIELDLAYOUTTABS, [ + 'layoutId' => $layout->id, + ]); // Because in MySQL, you can't even rely on cascading deletes to work. ¯\_(ツ)_/¯ - Craft::$app->getDb()->createCommand() - ->delete(Table::FIELDLAYOUTFIELDS, ['layoutId' => $layout->id]) - ->execute(); + Db::delete(Table::FIELDLAYOUTFIELDS, [ + 'layoutId' => $layout->id, + ]); // Get the current layout $layoutRecord = FieldLayoutRecord::findWithTrashed() @@ -1302,7 +1298,6 @@ public function saveLayout(FieldLayout $layout, bool $runValidation = true): boo $tab->uid = $tabRecord->uid; foreach ($tab->getFields() as $field) { - /** @var Field $field */ $fieldRecord = new FieldLayoutFieldRecord(); $fieldRecord->layoutId = $layout->id; $fieldRecord->tabId = $tab->id; @@ -1447,7 +1442,6 @@ public function applyFieldSave(string $fieldUid, array $data, string $context) $isNewField = $fieldRecord->getIsNewRecord(); $oldSettings = $fieldRecord->getOldAttribute('settings'); - /** @var Field $class */ $class = $data['type']; // Create/alter the content table column @@ -1547,7 +1541,6 @@ public function applyFieldSave(string $fieldUid, array $data, string $context) CustomFieldBehavior::$fieldHandles[$fieldRecord->handle] = true; // For CP save requests, make sure we have all the custom data already saved on the object. - /** @var Field $field */ if (isset($this->_savingFields[$fieldUid])) { $field = $this->_savingFields[$fieldUid]; @@ -1615,7 +1608,7 @@ private function _createFieldQuery(): Query 'fields.settings', 'fields.uid' ]) - ->from(['{{%fields}} fields']) + ->from(['fields' => Table::FIELDS]) ->orderBy(['fields.name' => SORT_ASC, 'fields.handle' => SORT_ASC]); // todo: remove schema version condition after next beakpoint diff --git a/src/services/Gc.php b/src/services/Gc.php index e16c2508782..ca00f36d907 100644 --- a/src/services/Gc.php +++ b/src/services/Gc.php @@ -111,12 +111,8 @@ public function hardDelete($tables) $tables = [$tables]; } - $db = Craft::$app->getDb(); - foreach ($tables as $table) { - $db->createCommand() - ->delete($table, $condition) - ->execute(); + Db::delete($table, $condition); } } @@ -135,8 +131,6 @@ private function _deleteStaleSessions() $expire = DateTimeHelper::currentUTCDateTime(); $pastTime = $expire->sub($interval); - Craft::$app->getDb()->createCommand() - ->delete(Table::SESSIONS, ['<', 'dateUpdated', Db::prepareDateForDb($pastTime)]) - ->execute(); + Db::delete(Table::SESSIONS, ['<', 'dateUpdated', Db::prepareDateForDb($pastTime)]); } } diff --git a/src/services/Globals.php b/src/services/Globals.php index 96cf19e3a76..b87bf9e5a2e 100644 --- a/src/services/Globals.php +++ b/src/services/Globals.php @@ -8,7 +8,6 @@ namespace craft\services; use Craft; -use craft\base\Field; use craft\db\Query; use craft\db\Table; use craft\elements\GlobalSet; @@ -527,7 +526,6 @@ public function handleDeletedGlobalSet(ConfigEvent $event) */ public function pruneDeletedField(FieldEvent $event) { - /** @var Field $field */ $field = $event->field; $fieldUid = $field->uid; @@ -553,7 +551,9 @@ public function pruneDeletedField(FieldEvent $event) } // Nuke all the layout fields from the DB - Craft::$app->getDb()->createCommand()->delete('{{%fieldlayoutfields}}', ['fieldId' => $field->id])->execute(); + Db::delete(Table::FIELDLAYOUTFIELDS, [ + 'fieldId' => $field->id, + ]); // Allow events again $projectConfig->muteEvents = false; diff --git a/src/services/Gql.php b/src/services/Gql.php index 7bdb0eaf872..83c14fcfee3 100644 --- a/src/services/Gql.php +++ b/src/services/Gql.php @@ -17,8 +17,10 @@ use craft\events\DefineGqlValidationRulesEvent; use craft\events\ExecuteGqlQueryEvent; use craft\events\RegisterGqlDirectivesEvent; +use craft\events\RegisterGqlMutationsEvent; use craft\events\RegisterGqlPermissionsEvent; use craft\events\RegisterGqlQueriesEvent; +use craft\events\RegisterGqlSchemaComponentsEvent; use craft\events\RegisterGqlTypesEvent; use craft\gql\base\Directive; use craft\gql\base\GeneratorInterface; @@ -35,6 +37,12 @@ use craft\gql\interfaces\elements\MatrixBlock as MatrixBlockInterface; use craft\gql\interfaces\elements\Tag as TagInterface; use craft\gql\interfaces\elements\User as UserInterface; +use craft\gql\mutations\Asset as AssetMutation; +use craft\gql\mutations\Category as CategoryMutation; +use craft\gql\mutations\Entry as EntryMutation; +use craft\gql\mutations\GlobalSet as GlobalSetMutation; +use craft\gql\mutations\Ping as PingMutation; +use craft\gql\mutations\Tag as TagMutation; use craft\gql\queries\Asset as AssetQuery; use craft\gql\queries\Category as CategoryQuery; use craft\gql\queries\Entry as EntryQuery; @@ -45,6 +53,7 @@ use craft\gql\TypeLoader; use craft\gql\TypeManager; use craft\gql\types\DateTime; +use craft\gql\types\Mutation; use craft\gql\types\Number; use craft\gql\types\Query; use craft\gql\types\QueryArgument; @@ -54,10 +63,10 @@ use craft\helpers\StringHelper; use craft\models\GqlSchema; use craft\models\GqlToken; +use craft\models\Section; use craft\records\GqlSchema as GqlSchemaRecord; use craft\records\GqlToken as GqlTokenRecord; use GraphQL\GraphQL; -use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\Rules\FieldsOnCorrectType; @@ -121,6 +130,31 @@ class Gql extends Component */ const EVENT_REGISTER_GQL_QUERIES = 'registerGqlQueries'; + /** + * @event RegisterGqlMutationsEvent The event that is triggered when registering GraphQL mutations. + * + * Plugins get a chance to add their own GraphQL mutations. + * See [GraphQL](https://docs.craftcms.com/v3/graphql.html) for documentation on adding GraphQL support. + * + * --- + * ```php + * use craft\events\RegisterGqlMutationsEvent; + * use craft\services\GraphQl; + * use yii\base\Event; + * use GraphQL\Type\Definition\Type; + * + * Event::on(Gql::class, Gql::EVENT_REGISTER_GQL_MUTATIONS, function(RegisterGqlMutationsEvent $event) { + * // Add my GraphQL queries + * $event->queries['mutationPluginData'] = + * [ + * 'type' => Type::listOf(MyType::getType())), + * 'args' => MyArguments::getArguments(), + * ]; + * }); + * ``` + */ + const EVENT_REGISTER_GQL_MUTATIONS = 'registerGqlMutations'; + /** * @event RegisterGqlDirectivesEvent The event that is triggered when registering GraphQL directives. * @@ -146,9 +180,16 @@ class Gql extends Component /** * @event RegisterGqlPermissionsEvent The event that is triggered when registering user permissions. * @since 3.4.0 + * @deprecated in 3.5.0. Use the [[EVENT_REGISTER_GQL_SCHEMA_COMPONENTS]] event instead. */ const EVENT_REGISTER_GQL_PERMISSIONS = 'registerGqlPermissions'; + /** + * @event RegisterGqlSchemaComponentsEvent The event that is triggered when registering GraphQL schema components. + * @since 3.5.0 + */ + const EVENT_REGISTER_GQL_SCHEMA_COMPONENTS = 'registerGqlSchemaComponents'; + /** * @event DefineGqlValidationRulesEvent The event that is triggered when defining validation rules to be used. * @@ -268,10 +309,12 @@ public function getSchemaDef(GqlSchema $schema = null, $prebuildSchema = false): // Either cached version was not found or we need a pre-built schema. $registeredTypes = $this->_registerGqlTypes(); $this->_registerGqlQueries(); + $this->_registerGqlMutations(); $schemaConfig = [ 'typeLoader' => TypeLoader::class . '::loadType', 'query' => TypeLoader::loadType('Query'), + 'mutation' => TypeLoader::loadType('Mutation'), 'directives' => $this->_loadGqlDirectives(), ]; @@ -363,7 +406,7 @@ public function executeQuery(GqlSchema $schema, string $query, $variables = [], $event->result = $cachedResult; } else { $schemaDef = $this->getSchemaDef($schema, $debugMode || StringHelper::contains($query, '__schema')); - $event->result = GraphQL::executeQuery($schemaDef, $query, $event->rootValue, $event->context, $event->variables, $event->operationName, null, $this->getValidationRules($debugMode))->toArray(true); + $event->result = GraphQL::executeQuery($schemaDef, $query, $event->rootValue, $event->context, $event->variables, $event->operationName, null, $this->getValidationRules($debugMode))->toArray($debugMode); if (empty($event->result['errors']) && $cacheKey) { $this->setCachedResult($cacheKey, $event->result); @@ -493,44 +536,90 @@ public function getPublicSchema() * Returns all of the known GraphQL permissions, sorted by category. * * @return array + * @deprecated in 3.5.0. Use [[\craft\services\Gql::get()]] instead. */ public function getAllPermissions(): array { - $permissions = []; + return $this->getAllSchemaComponents()['queries']; + } + + /** + * Returns all of the known GraphQL schema components. + * + * @return array + * @since 3.5.0 + */ + public function getAllSchemaComponents(): array + { + $queries = []; + $mutations = []; // Entries // --------------------------------------------------------------------- - $permissions = array_merge($permissions, $this->_getSectionPermissions()); + $components = $this->_getSectionSchemaComponents(); + $label = Craft::t('app', 'Entries'); + $queries[$label] = $components['query'] ?? []; + $mutations[$label] = $components['mutation'] ?? []; // Assets // --------------------------------------------------------------------- - $permissions = array_merge($permissions, $this->_getVolumePermissions()); + $components = $this->_getVolumeSchemaComponents(); + $label = Craft::t('app', 'Assets'); + $queries[$label] = $components['query'] ?? []; + $mutations[$label] = $components['mutation'] ?? []; // Global Sets // --------------------------------------------------------------------- - $permissions = array_merge($permissions, $this->_getGlobalSetPermissions()); + $components = $this->_getGlobalSetSchemaComponents(); + $label = Craft::t('app', 'Global sets'); + $queries[$label] = $components['query'] ?? []; + $mutations[$label] = $components['mutation'] ?? []; // Users // --------------------------------------------------------------------- - $permissions = array_merge($permissions, $this->_getUserPermissions()); + $components = $this->_getUserSchemaComponents(); + $label = Craft::t('app', 'Users'); + $queries[$label] = $components['query'] ?? []; + $mutations[$label] = $components['mutation'] ?? []; // Categories // --------------------------------------------------------------------- - $permissions = array_merge($permissions, $this->_getCategoryPermissions()); + $components = $this->_getCategorySchemaComponents(); + $label = Craft::t('app', 'Categories'); + $queries[$label] = $components['query'] ?? []; + $mutations[$label] = $components['mutation'] ?? []; // Tags // --------------------------------------------------------------------- - $permissions = array_merge($permissions, $this->_getTagPermissions()); + $components = $this->_getTagSchemaComponents(); + $label = Craft::t('app', 'Tags'); + $queries[$label] = $components['query'] ?? []; + $mutations[$label] = $components['mutation'] ?? []; // Let plugins customize them and add new ones // --------------------------------------------------------------------- - $event = new RegisterGqlPermissionsEvent([ - 'permissions' => $permissions + if ($this->hasEventHandlers(self::EVENT_REGISTER_GQL_PERMISSIONS)) { + $deprecatedEvent = new RegisterGqlPermissionsEvent([ + 'permissions' => $queries + ]); + + $this->trigger(self::EVENT_REGISTER_GQL_PERMISSIONS, $deprecatedEvent); + + $queries = $deprecatedEvent->permissions; + } + + $event = new RegisterGqlSchemaComponentsEvent([ + 'queries' => $queries, + 'mutations' => $mutations ]); - $this->trigger(self::EVENT_REGISTER_GQL_PERMISSIONS, $event); - return $event->permissions; + $this->trigger(self::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS, $event); + + return [ + 'queries' => $event->queries, + 'mutations' => $event->mutations + ]; } /** @@ -641,7 +730,7 @@ public function saveToken(GqlToken $token, $runValidation = true): bool $isNewToken = !$token->id; if ($runValidation && !$token->validate()) { - Craft::info('Schema not saved due to validation error.', __METHOD__); + Craft::info('Token not saved due to validation error.', __METHOD__); return false; } @@ -700,7 +789,7 @@ public function saveSchema(GqlSchema $schema, $runValidation = true): bool $isNewScope = !$schema->id; if ($runValidation && !$schema->validate()) { - Craft::info('Scope not saved due to validation error.', __METHOD__); + Craft::info('Schema not saved due to validation error.', __METHOD__); return false; } @@ -827,9 +916,9 @@ public function handleDeletedSchema(ConfigEvent $event) try { // Delete the scope - $db->createCommand() - ->delete(Table::GQLSCHEMAS, ['id' => $schemaRecord->id]) - ->execute(); + Db::delete(Table::GQLSCHEMAS, [ + 'id' => $schemaRecord->id, + ]); $transaction->commit(); } catch (\Throwable $e) { @@ -911,10 +1000,7 @@ public function getContentArguments(array $contexts, $elementClass): array foreach ($context->getFields() as $contentField) { if (!$contentField instanceof GqlInlineFragmentFieldInterface) { - $contentArguments[$contentField->handle] = [ - 'name' => $contentField->handle, - 'type' => Type::listOf(QueryArgument::getType()), - ]; + $contentArguments[$contentField->handle] = $contentField->getContentGqlQueryArgumentType(); } } } @@ -1029,6 +1115,35 @@ private function _registerGqlQueries() }); } + /** + * Get GraphQL mutation definitions + * + * @return void + */ + private function _registerGqlMutations() + { + $mutationList = [ + // Mutations + PingMutation::getMutations(), + EntryMutation::getMutations(), + TagMutation::getMutations(), + CategoryMutation::getMutations(), + GlobalSetMutation::getMutations(), + AssetMutation::getMutations(), + ]; + + + $event = new RegisterGqlMutationsEvent([ + 'mutations' => array_merge(...$mutationList) + ]); + + $this->trigger(self::EVENT_REGISTER_GQL_MUTATIONS, $event); + + TypeLoader::registerType('Mutation', function() use ($event) { + return call_user_func(Mutation::class . '::getType', $event->mutations); + }); + } + /** * Get GraphQL query definitions * @@ -1064,35 +1179,53 @@ private function _loadGqlDirectives(): array * * @return array */ - private function _getSectionPermissions(): array + private function _getSectionSchemaComponents(): array { - $permissions = []; - $sortedEntryTypes = []; foreach (Craft::$app->getSections()->getAllEntryTypes() as $entryType) { $sortedEntryTypes[$entryType->sectionId][] = $entryType; } - if (!empty($sortedEntryTypes)) { - $label = Craft::t('app', 'Entries'); + $queryComponents = []; + $mutationComponents = []; - $sectionPermissions = []; + if (!empty($sortedEntryTypes)) { foreach (Craft::$app->getSections()->getAllSections() as $section) { - $nested = ['label' => Craft::t('app', 'View section - {section}', ['section' => Craft::t('site', $section->name)])]; + $query = ['label' => Craft::t('app', 'Section - {section}', ['section' => Craft::t('site', $section->name)])]; + $mutate = ['label' => Craft::t('app', 'Section - {section}', ['section' => Craft::t('site', $section->name)])]; foreach ($sortedEntryTypes[$section->id] as $entryType) { - $nested['nested']['entrytypes.' . $entryType->uid . ':read'] = ['label' => Craft::t('app', 'View entry type - {entryType}', ['entryType' => Craft::t('site', $entryType->name)])]; + $suffix = 'entrytypes.' . $entryType->uid; + + if ($section->type == Section::TYPE_SINGLE) { + $mutate['nested'][$suffix . ':save'] = ['label' => Craft::t('app', 'Edit “{entryType}”', ['entryType' => Craft::t('site', $entryType->name)])]; + } else { + $mutate['nested'][$suffix . ':edit'] = [ + 'label' => Craft::t('app', 'Edit entries with the “{entryType}” entry type', ['entryType' => Craft::t('site', $entryType->name)]), + 'nested' => [ + $suffix . ':create' => ['label' => Craft::t('app', 'Create entries with the “{entryType}” entry type', ['entryType' => Craft::t('site', $entryType->name)])], + $suffix . ':save' => ['label' => Craft::t('app', 'Save entries with the “{entryType}” entry type', ['entryType' => Craft::t('site', $entryType->name)])], + $suffix . ':delete' => ['label' => Craft::t('app', 'Delete entries with the “{entryType}” entry type', ['entryType' => Craft::t('site', $entryType->name)])], + ], + ]; + } + + $query['nested'][$suffix . ':read'] = [ + 'label' => Craft::t('app', 'View entries with the “{entryType}” entry type', ['entryType' => Craft::t('site', $entryType->name)]), + ]; } - $sectionPermissions['sections.' . $section->uid . ':read'] = $nested; + $queryComponents['sections.' . $section->uid . ':read'] = $query; + $mutationComponents['sections.' . $section->uid . ':edit'] = $mutate; } - - $permissions[$label] = $sectionPermissions; } - return $permissions; + return [ + 'query' => $queryComponents, + 'mutation' => $mutationComponents, + ]; } /** @@ -1100,24 +1233,32 @@ private function _getSectionPermissions(): array * * @return array */ - private function _getVolumePermissions(): array + private function _getVolumeSchemaComponents(): array { - $permissions = []; + $queryComponents = []; + $mutationComponents = []; $volumes = Craft::$app->getVolumes()->getAllVolumes(); if (!empty($volumes)) { - $label = Craft::t('app', 'Assets'); - $volumePermissions = []; - foreach ($volumes as $volume) { - $volumePermissions['volumes.' . $volume->uid . ':read'] = ['label' => Craft::t('app', 'View volume - {volume}', ['volume' => Craft::t('site', $volume->name)])]; + $suffix = 'volumes.' . $volume->uid; + $queryComponents[$suffix . ':read'] = ['label' => Craft::t('app', 'View volume - {volume}', ['volume' => Craft::t('site', $volume->name)])]; + $mutationComponents[$suffix . ':edit'] = [ + 'label' => Craft::t('app', 'Edit assets in the “{volume}” volume', ['volume' => Craft::t('site', $volume->name)]), + 'nested' => [ + $suffix . ':create' => ['label' => Craft::t('app', 'Create assets in the “{volume}” volume', ['volume' => Craft::t('site', $volume->name)])], + $suffix . ':save' => ['label' => Craft::t('app', 'Modify assets in the “{volume}” volume', ['volume' => Craft::t('site', $volume->name)])], + $suffix . ':delete' => ['label' => Craft::t('app', 'Delete assets from the “{volume}” volume', ['volume' => Craft::t('site', $volume->name)])], + ] + ]; } - - $permissions[$label] = $volumePermissions; } - return $permissions; + return [ + 'query' => $queryComponents, + 'mutation' => $mutationComponents, + ]; } /** @@ -1125,25 +1266,25 @@ private function _getVolumePermissions(): array * * @return array */ - private function _getGlobalSetPermissions(): array + private function _getGlobalSetSchemaComponents(): array { - $permissions = []; + $queryComponents = []; + $mutationComponents = []; $globalSets = Craft::$app->getGlobals()->getAllSets(); if (!empty($globalSets)) { - $label = Craft::t('app', 'Globals'); - $globalSetPermissions = []; - foreach ($globalSets as $globalSet) { $suffix = 'globalsets.' . $globalSet->uid; - $globalSetPermissions[$suffix . ':read'] = ['label' => Craft::t('app', 'View global set - {globalSet}', ['globalSet' => Craft::t('site', $globalSet->name)])]; + $queryComponents[$suffix . ':read'] = ['label' => Craft::t('app', 'View global set - {globalSet}', ['globalSet' => Craft::t('site', $globalSet->name)])]; + $mutationComponents[$suffix . ':edit'] = ['label' => Craft::t('app', 'Edit the “{globalSet}” global set.', ['globalSet' => Craft::t('site', $globalSet->name)])]; } - - $permissions[$label] = $globalSetPermissions; } - return $permissions; + return [ + 'query' => $queryComponents, + 'mutation' => $mutationComponents, + ]; } /** @@ -1151,25 +1292,31 @@ private function _getGlobalSetPermissions(): array * * @return array */ - private function _getCategoryPermissions(): array + private function _getCategorySchemaComponents(): array { - $permissions = []; + $queryComponents = []; + $mutationComponents = []; $categoryGroups = Craft::$app->getCategories()->getAllGroups(); if (!empty($categoryGroups)) { - $label = Craft::t('app', 'Categories'); - $categoryPermissions = []; - foreach ($categoryGroups as $categoryGroup) { $suffix = 'categorygroups.' . $categoryGroup->uid; - $categoryPermissions[$suffix . ':read'] = ['label' => Craft::t('app', 'View category group - {categoryGroup}', ['categoryGroup' => Craft::t('site', $categoryGroup->name)])]; + $queryComponents[$suffix . ':read'] = ['label' => Craft::t('app', 'View category group - {categoryGroup}', ['categoryGroup' => Craft::t('site', $categoryGroup->name)])]; + $mutationComponents[$suffix . ':edit'] = [ + 'label' => Craft::t('app', 'Edit categories in the “{categoryGroup}” category group', ['categoryGroup' => Craft::t('site', $categoryGroup->name)]), + 'nested' => [ + $suffix . ':save' => ['label' => Craft::t('app', 'Save categories in the “{categoryGroup}” category group', ['categoryGroup' => Craft::t('site', $categoryGroup->name)])], + $suffix . ':delete' => ['label' => Craft::t('app', 'Delete categories from the “{categoryGroup}” category group', ['categoryGroup' => Craft::t('site', $categoryGroup->name)])], + ] + ]; } - - $permissions[$label] = $categoryPermissions; } - return $permissions; + return [ + 'query' => $queryComponents, + 'mutation' => $mutationComponents, + ]; } /** @@ -1177,25 +1324,31 @@ private function _getCategoryPermissions(): array * * @return array */ - private function _getTagPermissions(): array + private function _getTagSchemaComponents(): array { - $permissions = []; + $queryComponents = []; + $mutationComponents = []; $tagGroups = Craft::$app->getTags()->getAllTagGroups(); if (!empty($tagGroups)) { - $label = Craft::t('app', 'Tags'); - $tagPermissions = []; - foreach ($tagGroups as $tagGroup) { $suffix = 'taggroups.' . $tagGroup->uid; - $tagPermissions[$suffix . ':read'] = ['label' => Craft::t('app', 'View tag group - {tagGroup}', ['tagGroup' => Craft::t('site', $tagGroup->name)])]; + $queryComponents[$suffix . ':read'] = ['label' => Craft::t('app', 'View tag group - {tagGroup}', ['tagGroup' => Craft::t('site', $tagGroup->name)])]; + $mutationComponents[$suffix . ':edit'] = [ + 'label' => Craft::t('app', 'Edit tags in the “{tagGroup}” tag group', ['tagGroup' => Craft::t('site', $tagGroup->name)]), + 'nested' => [ + $suffix . ':save' => ['label' => Craft::t('app', 'Save tags in the “{tagGroup}” tag group', ['tagGroup' => Craft::t('site', $tagGroup->name)])], + $suffix . ':delete' => ['label' => Craft::t('app', 'Delete tags from the “{tagGroup}” tag group', ['tagGroup' => Craft::t('site', $tagGroup->name)])], + ] + ]; } - - $permissions[$label] = $tagPermissions; } - return $permissions; + return [ + 'query' => $queryComponents, + 'mutation' => $mutationComponents, + ]; } /** @@ -1203,24 +1356,21 @@ private function _getTagPermissions(): array * * @return array */ - private function _getUserPermissions(): array + private function _getUserSchemaComponents(): array { - $permissions = []; - + $queryComponents = []; $userGroups = Craft::$app->getUserGroups()->getAllGroups(); - $label = Craft::t('app', 'Users'); - - $userPermissions = ['usergroups.everyone:read' => ['label' => Craft::t('app', 'View all users')]]; + $queryComponents['usergroups.everyone:read'] = ['label' => Craft::t('app', 'View all users')]; foreach ($userGroups as $userGroup) { $suffix = 'usergroups.' . $userGroup->uid; - $userPermissions[$suffix . ':read'] = ['label' => Craft::t('app', 'View user group - {userGroup}', ['userGroup' => Craft::t('site', $userGroup->name)])]; + $queryComponents[$suffix . ':read'] = ['label' => Craft::t('app', 'View user group - {userGroup}', ['userGroup' => Craft::t('site', $userGroup->name)])]; } - $permissions[$label] = $userPermissions; - - return $permissions; + return [ + 'query' => $queryComponents, + ]; } /** diff --git a/src/services/Matrix.php b/src/services/Matrix.php index 36e0ad416e2..e91a703c7d6 100644 --- a/src/services/Matrix.php +++ b/src/services/Matrix.php @@ -8,9 +8,7 @@ namespace craft\services; use Craft; -use craft\base\Element; use craft\base\ElementInterface; -use craft\base\Field; use craft\db\Query; use craft\db\Table; use craft\elements\db\MatrixBlockQuery; @@ -116,7 +114,7 @@ public function getBlockTypesByFieldId(int $fieldId): array public function getAllBlockTypes(): array { $results = $this->_createBlockTypeQuery() - ->innerJoin(Table::FIELDS . ' f', '[[f.id]] = [[bt.fieldId]]') + ->innerJoin(['f' => Table::FIELDS], '[[f.id]] = [[bt.fieldId]]') ->where(['f.type' => MatrixField::class]) ->all(); @@ -186,7 +184,6 @@ public function validateBlockType(MatrixBlockType $blockType, bool $validateUniq $contentService->fieldColumnPrefix = 'field_' . $blockType->handle . '_'; foreach ($blockType->getFields() as $field) { - /** @var Field $field */ // Hack to allow blank field names if (!$field->name) { $field->name = '__blank__'; @@ -245,7 +242,6 @@ public function saveBlockType(MatrixBlockType $blockType, bool $runValidation = $fieldsService = Craft::$app->getFields(); - /** @var Field $parentField */ $parentField = $fieldsService->getFieldById($blockType->fieldId); $isNewBlockType = $blockType->getIsNew(); $projectConfig = Craft::$app->getProjectConfig(); @@ -265,7 +261,6 @@ public function saveBlockType(MatrixBlockType $blockType, bool $runValidation = $configData['fields'] = []; foreach ($blockType->getFields() as $field) { - /** @var Field $field */ $configData['fields'][$field->uid] = $fieldsService->createFieldConfig($field); $field->sortOrder = ++$sortOrder; @@ -447,8 +442,7 @@ public function handleDeletedBlockType(ConfigEvent $event) return; } - $db = Craft::$app->getDb(); - $transaction = $db->beginTransaction(); + $transaction = Craft::$app->getDb()->beginTransaction(); try { $blockType = $this->getBlockTypeById($blockTypeRecord->id); @@ -505,9 +499,9 @@ public function handleDeletedBlockType(ConfigEvent $event) Craft::$app->getFields()->deleteLayoutById($fieldLayoutId); // Finally delete the actual block type - $db->createCommand() - ->delete(Table::MATRIXBLOCKTYPES, ['id' => $blockTypeRecord->id]) - ->execute(); + Db::delete(Table::MATRIXBLOCKTYPES, [ + 'id' => $blockTypeRecord->id, + ]); } $transaction->commit(); @@ -747,7 +741,6 @@ public function getBlockById(int $blockId, int $siteId = null) */ public function saveField(MatrixField $field, ElementInterface $owner) { - /** @var Element $owner */ $elementsService = Craft::$app->getElements(); /** @var MatrixBlockQuery $query */ $query = $owner->getFieldValue($field->handle); @@ -762,7 +755,6 @@ public function saveField(MatrixField $field, ElementInterface $owner) $blockIds = []; $collapsedBlockIds = []; $sortOrder = 0; - $db = Craft::$app->getDb(); $transaction = Craft::$app->getDb()->beginTransaction(); try { @@ -775,10 +767,11 @@ public function saveField(MatrixField $field, ElementInterface $owner) } else if ((int)$block->sortOrder !== $sortOrder) { // Just update its sortOrder $block->sortOrder = $sortOrder; - $db->createCommand()->update(Table::MATRIXBLOCKS, - ['sortOrder' => $sortOrder], - ['id' => $block->id], [], false) - ->execute(); + Db::update(Table::MATRIXBLOCKS, [ + 'sortOrder' => $sortOrder, + ], [ + 'id' => $block->id, + ], [], false); } $blockIds[] = $block->id; @@ -809,7 +802,6 @@ public function saveField(MatrixField $field, ElementInterface $owner) if (!empty($otherSiteIds)) { // Get the original element and duplicated element for each of those sites - /** @var Element[] $otherTargets */ $otherTargets = $owner::find() ->drafts($owner->getIsDraft()) ->revisions($owner->getIsRevision()) @@ -871,8 +863,6 @@ public function saveField(MatrixField $field, ElementInterface $owner) */ public function duplicateBlocks(MatrixField $field, ElementInterface $source, ElementInterface $target, bool $checkOtherSites = false) { - /** @var Element $source */ - /** @var Element $target */ $elementsService = Craft::$app->getElements(); /** @var MatrixBlockQuery $query */ $query = $source->getFieldValue($field->handle); @@ -914,7 +904,6 @@ public function duplicateBlocks(MatrixField $field, ElementInterface $source, El if (!empty($otherSiteIds)) { // Get the original element and duplicated element for each of those sites - /** @var Element[] $otherSources */ $otherSources = $target::find() ->drafts($source->getIsDraft()) ->revisions($source->getIsRevision()) @@ -922,7 +911,6 @@ public function duplicateBlocks(MatrixField $field, ElementInterface $source, El ->siteId($otherSiteIds) ->anyStatus() ->all(); - /** @var Element[] $otherTargets */ $otherTargets = $target::find() ->drafts($target->getIsDraft()) ->revisions($target->getIsRevision()) @@ -980,7 +968,6 @@ public function getSupportedSiteIdsForField(MatrixField $field, ElementInterface */ public function getSupportedSiteIds(string $propagationMethod, ElementInterface $owner): array { - /** @var Element $owner */ /** @var Site[] $allSites */ $allSites = ArrayHelper::index(Craft::$app->getSites()->getAllSites(), 'id'); $ownerSiteIds = ArrayHelper::getColumn(ElementHelper::supportedSitesForElement($owner), 'siteId'); @@ -1092,7 +1079,6 @@ private function _createContentTable(string $tableName) */ private function _deleteOtherBlocks(MatrixField $field, ElementInterface $owner, array $except) { - /** @var Element $owner */ $deleteBlocks = MatrixBlock::find() ->anyStatus() ->ownerId($owner->id) diff --git a/src/services/Path.php b/src/services/Path.php index c8af202f32d..1123c9f2b08 100644 --- a/src/services/Path.php +++ b/src/services/Path.php @@ -70,7 +70,7 @@ public function getConfigPath(): string */ public function getProjectConfigFilePath(): string { - return $this->getConfigPath() . DIRECTORY_SEPARATOR . ProjectConfig::CONFIG_FILENAME; + return $this->getConfigPath() . DIRECTORY_SEPARATOR . Craft::$app->getProjectConfig()->filename; } /** diff --git a/src/services/PluginStore.php b/src/services/PluginStore.php index 052aa906878..b75cb3d67a6 100644 --- a/src/services/PluginStore.php +++ b/src/services/PluginStore.php @@ -10,6 +10,7 @@ use Craft; use craft\db\Table; use craft\helpers\DateTimeHelper; +use craft\helpers\Db; use craft\models\CraftIdToken; use craft\records\CraftIdToken as OauthTokenRecord; use DateInterval; @@ -184,9 +185,9 @@ public function deleteTokenByUserId(int $userId): bool return false; } - Craft::$app->getDb()->createCommand() - ->delete(Table::CRAFTIDTOKENS, ['userId' => $userId]) - ->execute(); + Db::delete(Table::CRAFTIDTOKENS, [ + 'userId' => $userId, + ]); return true; } diff --git a/src/services/Plugins.php b/src/services/Plugins.php index a47805906b3..3342fccae7b 100644 --- a/src/services/Plugins.php +++ b/src/services/Plugins.php @@ -8,7 +8,6 @@ namespace craft\services; use Craft; -use craft\base\Plugin; use craft\base\PluginInterface; use craft\db\MigrationManager; use craft\db\Query; @@ -233,7 +232,6 @@ public function loadPlugins() if ($plugin !== null) { // If we're not updating, check if the plugin's version number changed, but not its schema version. if (!Craft::$app->getIsInMaintenanceMode() && $this->hasPluginVersionNumberChanged($plugin) && !$this->doesPluginRequireDatabaseUpdate($plugin)) { - /** @var Plugin $plugin */ if ( $plugin->minVersionRequired && strpos($row['version'], 'dev-') !== 0 && @@ -248,12 +246,11 @@ public function loadPlugins() } // Update our record of the plugin's version number - Craft::$app->getDb()->createCommand() - ->update( - Table::PLUGINS, - ['version' => $plugin->getVersion()], - ['id' => $row['id']]) - ->execute(); + Db::update(Table::PLUGINS, [ + 'version' => $plugin->getVersion(), + ], [ + 'id' => $row['id'], + ]); } $this->_registerPlugin($plugin); @@ -311,7 +308,6 @@ public function getPluginByPackageName(string $packageName) $this->loadPlugins(); foreach ($this->_plugins as $plugin) { - /** @var Plugin $plugin */ if ($plugin->packageName === $packageName) { return $plugin; } @@ -474,7 +470,6 @@ public function installPlugin(string $handle, string $edition = null): bool $projectConfig = Craft::$app->getProjectConfig(); $configKey = self::CONFIG_PLUGINS_KEY . '.' . $handle; - /** @var Plugin $plugin */ $plugin = $this->createPlugin($handle); // Set the edition @@ -505,9 +500,7 @@ public function installPlugin(string $handle, string $edition = null): bool 'installDate' => Db::prepareDateForDb(new \DateTime()), ]; - $db->createCommand() - ->insert(Table::PLUGINS, $info) - ->execute(); + Db::insert(Table::PLUGINS, $info); $info['installDate'] = DateTimeHelper::toDateTime($info['installDate']); $info['id'] = $db->getLastInsertID(Table::PLUGINS); @@ -519,9 +512,9 @@ public function installPlugin(string $handle, string $edition = null): bool if ($db->getIsMysql()) { // Explicitly remove the plugins row just in case the transaction was implicitly committed - $db->createCommand() - ->delete(Table::PLUGINS, ['handle' => $handle]) - ->execute(); + Db::delete(Table::PLUGINS, [ + 'handle' => $handle, + ]); } return false; @@ -603,9 +596,9 @@ public function uninstallPlugin(string $handle): bool // Clean up the plugins and migrations tables $id = $this->getStoredPluginInfo($handle)['id']; - Craft::$app->getDb()->createCommand() - ->delete(Table::PLUGINS, ['id' => $id]) - ->execute(); + Db::delete(Table::PLUGINS, [ + 'id' => $id, + ]); $transaction->commit(); } catch (\Throwable $e) { @@ -665,7 +658,6 @@ public function switchEdition(string $handle, string $edition) // If it's installed, update the instance and our locally stored info $plugin = $this->getPlugin($handle); if ($plugin !== null) { - /** @var Plugin $plugin */ $plugin->edition = $edition; } } @@ -679,7 +671,6 @@ public function switchEdition(string $handle, string $edition) */ public function savePluginSettings(PluginInterface $plugin, array $settings): bool { - /** @var Plugin $plugin */ // Save the settings on the plugin $plugin->getSettings()->setAttributes($settings, false); @@ -724,7 +715,6 @@ public function savePluginSettings(PluginInterface $plugin, array $settings): bo */ public function hasPluginVersionNumberChanged(PluginInterface $plugin): bool { - /** @var Plugin $plugin */ $this->loadPlugins(); if (($info = $this->getStoredPluginInfo($plugin->id)) === null) { @@ -742,7 +732,6 @@ public function hasPluginVersionNumberChanged(PluginInterface $plugin): bool */ public function doesPluginRequireDatabaseUpdate(PluginInterface $plugin): bool { - /** @var Plugin $plugin */ $this->loadPlugins(); if (($info = $this->getStoredPluginInfo($plugin->id)) === null) { @@ -901,7 +890,6 @@ public function createPlugin(string $handle, array $info = null) } // Create the plugin - /** @var Plugin $plugin */ $plugin = Craft::createObject($config, [$handle, Craft::$app]); $this->_setPluginMigrator($plugin, $info['id'] ?? null); return $plugin; @@ -947,7 +935,6 @@ public function getPluginInfo(string $handle): array $pluginInfo = $this->_enabledPluginInfo[$handle] ?? $this->_disabledPluginInfo[$handle] ?? null; // Get the plugin if it's enabled - /** @var Plugin|null $plugin */ $plugin = $this->getPlugin($handle); $info = array_merge([ @@ -1089,7 +1076,6 @@ public function getPluginIconSvg(string $handle): string { // If it's installed, let the plugin say where it lives if (($plugin = $this->getPlugin($handle)) !== null) { - /** @var Plugin $plugin */ $basePath = $plugin->getBasePath(); } else { if (($basePath = $this->_composerPluginInfo[$handle]['basePath'] ?? false) !== false) { @@ -1135,7 +1121,6 @@ public function setPluginLicenseKey(string $handle, string $licenseKey = null): throw new InvalidPluginException($handle); } - /** @var Plugin $plugin */ // Validate the license key $normalizedLicenseKey = $this->normalizePluginLicenseKey($licenseKey); @@ -1211,13 +1196,12 @@ public function setPluginLicenseKeyStatus(string $handle, string $licenseKeyStat throw new InvalidPluginException($handle); } - /** @var Plugin $plugin */ - Craft::$app->getDb()->createCommand() - ->update(Table::PLUGINS, [ - 'licenseKeyStatus' => $licenseKeyStatus, - 'licensedEdition' => $licensedEdition, - ], ['handle' => $handle]) - ->execute(); + Db::update(Table::PLUGINS, [ + 'licenseKeyStatus' => $licenseKeyStatus, + 'licensedEdition' => $licensedEdition, + ], [ + 'handle' => $handle, + ]); // Update our cache of it if (isset($this->_enabledPluginInfo[$handle])) { @@ -1277,7 +1261,6 @@ private function _normalizeHandle(string $handle): string */ private function _registerPlugin(PluginInterface $plugin) { - /** @var Plugin $plugin */ $this->_plugins[$plugin->id] = $plugin; Craft::$app->setModule($plugin->id, $plugin); } @@ -1289,7 +1272,6 @@ private function _registerPlugin(PluginInterface $plugin) */ private function _unregisterPlugin(PluginInterface $plugin) { - /** @var Plugin $plugin */ unset($this->_plugins[$plugin->id]); Craft::$app->setModule($plugin->id, null); } @@ -1304,7 +1286,6 @@ private function _setPluginMigrator(PluginInterface $plugin, int $id = null) { $ref = new \ReflectionClass($plugin); $ns = $ref->getNamespaceName(); - /** @var Plugin $plugin */ $plugin->set('migrator', [ 'class' => MigrationManager::class, 'type' => MigrationManager::TYPE_PLUGIN, diff --git a/src/services/ProjectConfig.php b/src/services/ProjectConfig.php index 306d6a9df50..64bbf915132 100644 --- a/src/services/ProjectConfig.php +++ b/src/services/ProjectConfig.php @@ -8,7 +8,6 @@ namespace craft\services; use Craft; -use craft\base\Plugin; use craft\db\Query; use craft\db\Table; use craft\elements\User; @@ -17,6 +16,7 @@ use craft\events\RebuildConfigEvent; use craft\helpers\ArrayHelper; use craft\helpers\DateTimeHelper; +use craft\helpers\Db; use craft\helpers\FileHelper; use craft\helpers\Json; use craft\helpers\Path as PathHelper; @@ -51,7 +51,11 @@ class ProjectConfig extends Component // Array key to use if not using config files. const CONFIG_KEY = 'storedConfig'; - // Filename for base config file + /** + * @var string Filename for base config file + * @since 3.1.0 + * @deprecated in 3.5.0. Use [[filename]] instead. + */ const CONFIG_FILENAME = 'project.yaml'; /** @@ -162,6 +166,12 @@ class ProjectConfig extends Component */ const EVENT_REBUILD = 'rebuild'; + /** + * @var string The filename to save the project config as. + * @since 3.5.0 + */ + public $filename = 'project.yaml'; + /** * @var int The maximum number of project.yaml deltas to store in storage/config-backups/ * @since 3.4.0 @@ -757,9 +767,9 @@ public function saveModifiedConfigData() $currentSet = $changeSet; if (!empty($changeSet['removed'])) { - $db->createCommand() - ->delete(Table::PROJECTCONFIG, ['path' => array_keys($changeSet['removed'])]) - ->execute(); + Db::delete(Table::PROJECTCONFIG, [ + 'path' => array_keys($changeSet['removed']), + ]); } if (!empty($changeSet['added'])) { @@ -810,15 +820,13 @@ public function saveModifiedConfigData() // Store in the DB if (!empty($batch)) { - $db->createCommand() - ->delete(Table::PROJECTCONFIG, ['path' => $pathsToInsert]) - ->execute(); - $db->createCommand() - ->delete(Table::PROJECTCONFIG, ['path' => array_keys($additionalCleanupPaths)]) - ->execute(); - $db->createCommand() - ->batchInsert(Table::PROJECTCONFIG, ['path', 'value'], $batch, false) - ->execute(); + Db::delete(Table::PROJECTCONFIG, [ + 'path' => $pathsToInsert, + ]); + Db::delete(Table::PROJECTCONFIG, [ + 'path' => array_keys($additionalCleanupPaths), + ]); + Db::batchInsert(Table::PROJECTCONFIG, ['path', 'value'], $batch, false); } } @@ -910,7 +918,6 @@ public function getAreConfigSchemaVersionsCompatible(&$issues = []) $plugins = Craft::$app->getPlugins()->getAllPlugins(); foreach ($plugins as $plugin) { - /** @var Plugin $plugin */ $incomingSchema = (string)$this->get(Plugins::CONFIG_PLUGINS_KEY . '.' . $plugin->handle . '.schemaVersion', true); $existingSchema = (string)$plugin->schemaVersion; @@ -1745,7 +1752,6 @@ private function _loadInternalConfigData() // See if we can get away with using the cached data $dependency = new DbQueryDependency([ - 'db' => Craft::$app->getDb(), 'query' => $this->_createProjectConfigQuery() ->select(['value']) ->where(['path' => 'dateModified']), @@ -1851,8 +1857,8 @@ private function _getSiteData(): array 'sites.primary', 'siteGroups.uid AS siteGroup', ]) - ->from(['{{%sites}} sites']) - ->innerJoin('{{%sitegroups}} siteGroups', '[[sites.groupId]] = [[siteGroups.id]]') + ->from(['sites' => Table::SITES]) + ->innerJoin(['siteGroups' => Table::SITEGROUPS], '[[siteGroups.id]] = [[sites.groupId]]') ->where(['sites.dateDeleted' => null]) ->andWhere(['siteGroups.dateDeleted' => null]) ->all(); @@ -1891,8 +1897,8 @@ private function _getSectionData(): array 'structures.uid AS structure', 'structures.maxLevels AS structureMaxLevels', ]) - ->from(['{{%sections}} sections']) - ->leftJoin('{{%structures}} structures', '[[structures.id]] = [[sections.structureId]]') + ->from(['sections' => Table::SECTIONS]) + ->leftJoin(['structures' => Table::STRUCTURES], '[[structures.id]] = [[sections.structureId]]') ->where(['sections.dateDeleted' => null]) ->andWhere(['structures.dateDeleted' => null]) ->all(); @@ -1929,9 +1935,9 @@ private function _getSectionData(): array 'sites.uid AS siteUid', 'sections.uid AS sectionUid', ]) - ->from(['{{%sections_sites}} sections_sites']) - ->innerJoin('{{%sites}} sites', '[[sites.id]] = [[sections_sites.siteId]]') - ->innerJoin('{{%sections}} sections', '[[sections.id]] = [[sections_sites.sectionId]]') + ->from(['sections_sites' => Table::SECTIONS_SITES]) + ->innerJoin(['sites' => Table::SITES], '[[sites.id]] = [[sections_sites.siteId]]') + ->innerJoin(['sections' => Table::SECTIONS], '[[sections.id]] = [[sections_sites.sectionId]]') ->where(['sites.dateDeleted' => null]) ->andWhere(['sections.dateDeleted' => null]) ->all(); @@ -1959,8 +1965,8 @@ private function _getSectionData(): array 'entrytypes.uid', 'sections.uid AS sectionUid', ]) - ->from(['{{%entrytypes}} as entrytypes']) - ->innerJoin('{{%sections}} sections', '[[sections.id]] = [[entrytypes.sectionId]]') + ->from(['entrytypes' => Table::ENTRYTYPES]) + ->innerJoin(['sections' => Table::SECTIONS], '[[sections.id]] = [[entrytypes.sectionId]]') ->where(['sections.dateDeleted' => null]) ->andWhere(['entrytypes.dateDeleted' => null]) ->all(); @@ -2035,8 +2041,8 @@ private function _getFieldData(): array 'fields.uid', 'fieldGroups.uid AS fieldGroup', ]) - ->from(['{{%fields}} fields']) - ->leftJoin('{{%fieldgroups}} fieldGroups', '[[fields.groupId]] = [[fieldGroups.id]]') + ->from(['fields' => Table::FIELDS]) + ->leftJoin(['fieldGroups' => Table::FIELDGROUPS], '[[fieldGroups.id]] = [[fields.groupId]]') ->where(['fields.context' => 'global']) ->all(); @@ -2087,8 +2093,8 @@ private function _getMatrixBlockTypeData(): array 'bt.uid', 'f.uid AS field', ]) - ->from(['{{%matrixblocktypes}} bt']) - ->innerJoin('{{%fields}} f', '[[bt.fieldId]] = [[f.id]]') + ->from(['bt' => Table::MATRIXBLOCKTYPES]) + ->innerJoin(['f' => Table::FIELDS], '[[f.id]] = [[bt.fieldId]]') ->all(); $layoutIds = []; @@ -2123,8 +2129,8 @@ private function _getMatrixBlockTypeData(): array 'fields.uid', 'fieldGroups.uid AS fieldGroup', ]) - ->from(['{{%fields}} fields']) - ->leftJoin('{{%fieldgroups}} fieldGroups', '[[fields.groupId]] = [[fieldGroups.id]]') + ->from(['fields' => Table::FIELDS]) + ->leftJoin(['fieldGroups' => Table::FIELDGROUPS], '[[fieldGroups.id]] = [[fields.groupId]]') ->where(['like', 'fields.context', 'matrixBlockType:']) ->all(); @@ -2188,7 +2194,7 @@ private function _getVolumeData(): array 'volumes.sortOrder', 'volumes.uid', ]) - ->from(['{{%volumes}} volumes']) + ->from(['volumes' => Table::VOLUMES]) ->where(['volumes.dateDeleted' => null]) ->all(); @@ -2295,8 +2301,8 @@ private function _getCategoryGroupData(): array 'structures.uid AS structure', 'structures.maxLevels AS structureMaxLevels', ]) - ->from(['{{%categorygroups}} groups']) - ->leftJoin('{{%structures}} structures', '[[structures.id]] = [[groups.structureId]]') + ->from(['groups' => Table::CATEGORYGROUPS]) + ->leftJoin(['structures' => Table::STRUCTURES], '[[structures.id]] = [[groups.structureId]]') ->where(['groups.dateDeleted' => null]) ->andWhere(['structures.dateDeleted' => null]) ->all(); @@ -2342,9 +2348,9 @@ private function _getCategoryGroupData(): array 'sites.uid AS siteUid', 'groups.uid AS groupUid', ]) - ->from(['{{%categorygroups_sites}} groups_sites']) - ->innerJoin('{{%sites}} sites', '[[sites.id]] = [[groups_sites.siteId]]') - ->innerJoin('{{%categorygroups}} groups', '[[groups.id]] = [[groups_sites.groupId]]') + ->from(['groups_sites' => Table::CATEGORYGROUPS_SITES]) + ->innerJoin(['sites' => Table::SITES], '[[sites.id]] = [[groups_sites.siteId]]') + ->innerJoin(['groups' => Table::CATEGORYGROUPS], '[[groups.id]] = [[groups_sites.groupId]]') ->where(['groups.dateDeleted' => null]) ->andWhere(['sites.dateDeleted' => null]) ->all(); @@ -2376,7 +2382,7 @@ private function _getTagGroupData(): array 'groups.uid', 'groups.fieldLayoutId', ]) - ->from(['{{%taggroups}} groups']) + ->from(['groups' => Table::TAGGROUPS]) ->where(['groups.dateDeleted' => null]) ->all(); @@ -2419,7 +2425,7 @@ private function _getGlobalSetData(): array 'sets.uid', 'sets.fieldLayoutId', ]) - ->from(['{{%globalsets}} sets']) + ->from(['sets' => Table::GLOBALSETS]) ->all(); $setData = []; @@ -2572,10 +2578,10 @@ private function _generateFieldLayoutArray(array $layoutIds): array 'tabs.uid AS tabUid', 'layouts.id AS layoutId', ]) - ->from(['{{%fieldlayoutfields}} AS layoutFields']) - ->innerJoin('{{%fieldlayouttabs}} AS tabs', '[[layoutFields.tabId]] = [[tabs.id]]') - ->innerJoin('{{%fieldlayouts}} AS layouts', '[[layoutFields.layoutId]] = [[layouts.id]]') - ->innerJoin('{{%fields}} AS fields', '[[layoutFields.fieldId]] = [[fields.id]]') + ->from(['layoutFields' => Table::FIELDLAYOUTFIELDS]) + ->innerJoin(['tabs' => Table::FIELDLAYOUTTABS], '[[tabs.id]] = [[layoutFields.tabId]]') + ->innerJoin(['layouts' => Table::FIELDLAYOUTS], '[[layouts.id]] = [[layoutFields.layoutId]]') + ->innerJoin(['fields' => Table::FIELDS], '[[fields.id]] = [[layoutFields.fieldId]]') ->where(['layouts.id' => $layoutIds]) ->andWhere(['layouts.dateDeleted' => null]) ->orderBy(['tabs.sortOrder' => SORT_ASC, 'layoutFields.sortOrder' => SORT_ASC]) diff --git a/src/services/Relations.php b/src/services/Relations.php index 4694f365290..89ff0836d9a 100644 --- a/src/services/Relations.php +++ b/src/services/Relations.php @@ -8,12 +8,12 @@ namespace craft\services; use Craft; -use craft\base\Element; use craft\base\ElementInterface; use craft\db\Command; use craft\db\Query; use craft\db\Table; use craft\fields\BaseRelationField; +use craft\helpers\Db; use yii\base\Component; /** @@ -35,7 +35,6 @@ class Relations extends Component */ public function saveRelations(BaseRelationField $field, ElementInterface $source, array $targetIds) { - /** @var Element $source */ if (!is_array($targetIds)) { $targetIds = []; } @@ -105,15 +104,13 @@ public function saveRelations(BaseRelationField $field, ElementInterface $source $sortOrder + 1, ]; } - $db->createCommand() - ->batchInsert(Table::RELATIONS, ['fieldId', 'sourceId', 'sourceSiteId', 'targetId', 'sortOrder'], $values) - ->execute(); + Db::batchInsert(Table::RELATIONS, ['fieldId', 'sourceId', 'sourceSiteId', 'targetId', 'sortOrder'], $values); } if (!empty($deleteIds)) { - $db->createCommand() - ->delete(Table::RELATIONS, ['id' => $deleteIds]) - ->execute(); + Db::delete(Table::RELATIONS, [ + 'id' => $deleteIds, + ]); } $transaction->commit(); diff --git a/src/services/Revisions.php b/src/services/Revisions.php index 9c4ac20c701..dd728d47ad9 100644 --- a/src/services/Revisions.php +++ b/src/services/Revisions.php @@ -8,7 +8,6 @@ namespace craft\services; use Craft; -use craft\base\Element; use craft\base\ElementInterface; use craft\behaviors\RevisionBehavior; use craft\db\Query; @@ -16,7 +15,10 @@ use craft\errors\InvalidElementException; use craft\events\RevisionEvent; use craft\helpers\ArrayHelper; +use craft\helpers\Db; use craft\helpers\ElementHelper; +use craft\helpers\Queue; +use craft\queue\jobs\PruneRevisions; use yii\base\Component; use yii\base\InvalidArgumentException; use yii\db\Exception; @@ -66,12 +68,10 @@ class Revisions extends Component public function createRevision(ElementInterface $source, int $creatorId = null, string $notes = null, array $newAttributes = [], bool $force = false): ElementInterface { // Make sure the source isn't a draft or revision - /** @var Element $source */ if ($source->getIsDraft() || $source->getIsRevision()) { throw new InvalidArgumentException('Cannot create a revision from another revision or draft.'); } - /** @var Element $source */ $lockKey = 'revision:' . $source->id; $mutex = Craft::$app->getMutex(); if (!$mutex->acquire($lockKey)) { @@ -92,7 +92,7 @@ public function createRevision(ElementInterface $source, int $creatorId = null, if (!$force && $lastRevisionNum) { // Get the revision, if it exists for the source's site - /** @var Element|RevisionBehavior|null $lastRevision */ + /** @var ElementInterface|RevisionBehavior|null $lastRevision */ $lastRevision = $source::find() ->revisionOf($source) ->siteId($source->siteId) @@ -132,17 +132,14 @@ public function createRevision(ElementInterface $source, int $creatorId = null, $transaction = Craft::$app->getDb()->beginTransaction(); try { // Create the revision row - $db = Craft::$app->getDb(); - $db->createCommand() - ->insert(Table::REVISIONS, [ - 'sourceId' => $source->id, - 'creatorId' => $creatorId, - 'num' => $num, - 'notes' => $notes, - ], false) - ->execute(); + Db::insert(Table::REVISIONS, [ + 'sourceId' => $source->id, + 'creatorId' => $creatorId, + 'num' => $num, + 'notes' => $notes, + ], false); - $newAttributes['revisionId'] = $db->getLastInsertID(Table::REVISIONS); + $newAttributes['revisionId'] = Craft::$app->getDb()->getLastInsertID(Table::REVISIONS); $newAttributes['behaviors']['revision'] = [ 'class' => RevisionBehavior::class, 'sourceId' => $source->id, @@ -156,7 +153,6 @@ public function createRevision(ElementInterface $source, int $creatorId = null, } // Duplicate the element - /** @var Element $revision */ $revision = $elementsService->duplicateElement($source, $newAttributes); $transaction->commit(); @@ -180,20 +176,12 @@ public function createRevision(ElementInterface $source, int $creatorId = null, $mutex->release($lockKey); // Prune any excess revisions - $maxRevisions = Craft::$app->getConfig()->getGeneral()->maxRevisions; - if ($maxRevisions > 0) { - // Don't count the current revision - $extraRevisions = $source::find() - ->revisionOf($source) - ->siteId($source->siteId) - ->anyStatus() - ->orderBy(['num' => SORT_DESC]) - ->offset($maxRevisions) - ->all(); - - foreach ($extraRevisions as $extraRevision) { - $elementsService->deleteElement($extraRevision, true); - } + if (Craft::$app->getConfig()->getGeneral()->maxRevisions) { + Queue::push(new PruneRevisions([ + 'elementType' => get_class($source), + 'sourceId' => $source->id, + 'siteId' => $source->siteId, + ]), 2049); } return $revision; @@ -210,8 +198,7 @@ public function createRevision(ElementInterface $source, int $creatorId = null, */ public function revertToRevision(ElementInterface $revision, int $creatorId): ElementInterface { - /** @var Element|RevisionBehavior $revision */ - /** @var Element $source */ + /** @var ElementInterface|RevisionBehavior $revision */ $source = ElementHelper::sourceElement($revision); // Fire a 'beforeRevertToRevision' event diff --git a/src/services/Search.php b/src/services/Search.php index 7163adb9e3e..fe226f7171f 100644 --- a/src/services/Search.php +++ b/src/services/Search.php @@ -8,9 +8,8 @@ namespace craft\services; use Craft; -use craft\base\Element; use craft\base\ElementInterface; -use craft\base\Field; +use craft\base\FieldInterface; use craft\db\Query; use craft\db\Table; use craft\errors\SiteNotFoundException; @@ -107,7 +106,6 @@ public function indexElementAttributes(ElementInterface $element, array $fieldHa { // Acquire a lock for this element/site ID $mutex = Craft::$app->getMutex(); - /** @var Element $element */ $lockKey = "searchindex:{$element->id}:{$element->siteId}"; if (!$mutex->acquire($lockKey)) { @@ -116,7 +114,7 @@ public function indexElementAttributes(ElementInterface $element, array $fieldHa } // Figure out which fields to update, and which to ignore - /** @var Field[] $updateFields */ + /** @var FieldInterface[] $updateFields */ $updateFields = []; /** @var string[] $ignoreFieldIds */ $ignoreFieldIds = []; @@ -125,7 +123,6 @@ public function indexElementAttributes(ElementInterface $element, array $fieldHa $fieldHandles = array_flip($fieldHandles); } foreach ($fieldLayout->getFields() as $field) { - /** @var Field $field */ if ($field->searchable) { // Are we updating this field's keywords? if ($fieldHandles === null || isset($fieldHandles[$field->handle])) { @@ -146,9 +143,7 @@ public function indexElementAttributes(ElementInterface $element, array $fieldHa if (!empty($ignoreFieldIds)) { $deleteCondition = ['and', $deleteCondition, ['not', ['fieldId' => $ignoreFieldIds]]]; } - Craft::$app->getDb()->createCommand() - ->delete(Table::SEARCHINDEX, $deleteCondition) - ->execute(); + Db::delete(Table::SEARCHINDEX, $deleteCondition); // Update the element attributes' keywords $searchableAttributes = array_flip($element::searchableAttributes()); @@ -337,17 +332,20 @@ public function filterElementIdsByQuery(array $elementIds, $query, bool $scoreRe public function deleteOrphanedIndexes() { $db = Craft::$app->getDb(); + $searchIndexTable = Table::SEARCHINDEX; + $elementsTable = Table::ELEMENTS; + if ($db->getIsMysql()) { $sql = <<createCommand() - ->insert(Table::SEARCHINDEX, $columns, false) - ->execute(); + Db::insert(Table::SEARCHINDEX, $columns, false); } /** @@ -734,7 +730,6 @@ private function _normalizeTerm(string $term): string private function _getFieldIdFromAttribute(string $attribute): int { // Get field id from service - /** @var Field $field */ $field = Craft::$app->getFields()->getFieldByHandle($attribute); // Fallback to 0 diff --git a/src/services/Sections.php b/src/services/Sections.php index 5a6a0d20bdd..869251749c0 100644 --- a/src/services/Sections.php +++ b/src/services/Sections.php @@ -9,7 +9,6 @@ use Craft; use craft\base\Element; -use craft\base\Field; use craft\db\Query; use craft\db\Table; use craft\elements\Entry; @@ -24,6 +23,7 @@ use craft\helpers\Db; use craft\helpers\Json; use craft\helpers\ProjectConfig as ProjectConfigHelper; +use craft\helpers\Queue; use craft\helpers\StringHelper; use craft\models\EntryType; use craft\models\FieldLayout; @@ -371,8 +371,8 @@ public function getSectionSiteSettings(int $sectionId): array 'sections_sites.uriFormat', 'sections_sites.template', ]) - ->from(['{{%sections_sites}} sections_sites']) - ->innerJoin('{{%sites}} sites', '[[sites.id]] = [[sections_sites.siteId]]') + ->from(['sections_sites' => Table::SECTIONS_SITES]) + ->innerJoin(['sites' => Table::SITES], '[[sites.id]] = [[sections_sites.siteId]]') ->where(['sections_sites.sectionId' => $sectionId]) ->orderBy(['sites.sortOrder' => SORT_ASC]) ->all(); @@ -728,7 +728,7 @@ public function handleChangedSection(ConfigEvent $event) if (!$isNewSection && $resaveEntries) { // If the propagation method just changed, we definitely need to update entries for that if ($propagationMethodChanged) { - Craft::$app->getQueue()->push(new ApplyNewPropagationMethod([ + Queue::push(new ApplyNewPropagationMethod([ 'description' => Craft::t('app', 'Applying new propagation method to {section} entries', [ 'section' => $sectionRecord->name, ]), @@ -738,7 +738,7 @@ public function handleChangedSection(ConfigEvent $event) ], ])); } else if ($this->autoResaveEntries) { - Craft::$app->getQueue()->push(new ResaveElements([ + Queue::push(new ResaveElements([ 'description' => Craft::t('app', 'Resaving {section} entries', [ 'section' => $sectionRecord->name, ]), @@ -935,7 +935,6 @@ public function pruneDeletedSite(DeleteSiteEvent $event) */ public function pruneDeletedField(FieldEvent $event) { - /** @var Field $field */ $field = $event->field; $fieldUid = $field->uid; @@ -965,7 +964,9 @@ public function pruneDeletedField(FieldEvent $event) } // Nuke all the layout fields from the DB - Craft::$app->getDb()->createCommand()->delete('{{%fieldlayoutfields}}', ['fieldId' => $field->id])->execute(); + Db::delete(Table::FIELDLAYOUTFIELDS, [ + 'fieldId' => $field->id, + ]); // Allow events again $projectConfig->muteEvents = false; @@ -1275,7 +1276,7 @@ public function handleChangedEntryType(ConfigEvent $event) $this->_ensureSingleEntry($section); } else if (!$isNewEntryType && $resaveEntries && $this->autoResaveEntries) { // Re-save the entries of this type - Craft::$app->getQueue()->push(new ResaveElements([ + Queue::push(new ResaveElements([ 'description' => Craft::t('app', 'Resaving {type} entries', [ 'type' => ($section->type !== Section::TYPE_SINGLE ? $section->name . ' - ' : '') . $entryType->name, ]), @@ -1486,8 +1487,8 @@ private function _createSectionQuery(): Query 'sections.uid', 'structures.maxLevels', ]) - ->leftJoin('{{%structures}} structures', $joinCondition) - ->from(['{{%sections}} sections']) + ->leftJoin(['structures' => Table::STRUCTURES], $joinCondition) + ->from(['sections' => Table::SECTIONS]) ->where($condition) ->orderBy(['name' => SORT_ASC]); diff --git a/src/services/Security.php b/src/services/Security.php index 128ea3def56..0282b81e258 100644 --- a/src/services/Security.php +++ b/src/services/Security.php @@ -77,7 +77,6 @@ public function hashPassword(string $password, bool $validateHash = false): stri */ public function getValidationKey(): string { - Craft::$app->getDeprecator()->log(__METHOD__, 'Craft::$app->security->getValidationKey() has been deprecated. Use Craft::$app->config->general->securityKey instead.'); return Craft::$app->getConfig()->getGeneral()->securityKey; } diff --git a/src/services/Sites.php b/src/services/Sites.php index 1e8a61f4959..52e5ab95745 100644 --- a/src/services/Sites.php +++ b/src/services/Sites.php @@ -8,7 +8,7 @@ namespace craft\services; use Craft; -use craft\base\Element; +use craft\base\ElementInterface; use craft\db\Query; use craft\db\Table; use craft\elements\Asset; @@ -24,6 +24,7 @@ use craft\helpers\App; use craft\helpers\ArrayHelper; use craft\helpers\Db; +use craft\helpers\Queue; use craft\helpers\StringHelper; use craft\models\Site; use craft\models\SiteGroup; @@ -137,19 +138,13 @@ class Sites extends Component * @var Site[] * @see getSiteById() */ - private $_sitesById; + private $_allSitesById; /** * @var Site[] - * @see getSiteByUid() - */ - private $_sitesByUid; - - /** - * @var Site[] - * @see getSiteByHandle() + * @see getSiteById() */ - private $_sitesByHandle; + private $_enabledSitesById; /** * @var Site|null the current site @@ -388,26 +383,29 @@ public function deleteGroup(SiteGroup $group): bool /** * Returns all of the site IDs. * + * @param bool|null $withDisabled * @return int[] All the sites’ IDs */ - public function getAllSiteIds(): array + public function getAllSiteIds(bool $withDisabled = null): array { - return array_keys($this->_sitesById); + return ArrayHelper::getColumn($this->_allSites($withDisabled), 'id', false); } /** * Returns a site by it's UID. * + * @param string $uid + * @param bool|null $withDisabled * @return Site the site * @throws SiteNotFoundException if no sites exist */ - public function getSiteByUid(string $uid): Site + public function getSiteByUid(string $uid, bool $withDisabled = null): Site { - if (!isset($this->_sitesByUid[$uid])) { + $site = ArrayHelper::firstWhere($this->_allSites($withDisabled), 'uid', $uid, true); + if ($site === null) { throw new SiteNotFoundException('Site with UID ”' . $uid . '“ not found!'); } - - return $this->_sitesByUid[$uid]; + return $site; } /** @@ -508,9 +506,10 @@ public function getEditableSiteIds(): array } $this->_editableSiteIds = []; + $userSession = Craft::$app->getUser(); foreach ($this->getAllSites() as $site) { - if (Craft::$app->getUser()->checkPermission('editSite:' . $site->uid)) { + if ($userSession->checkPermission("editSite:$site->uid")) { $this->_editableSiteIds[] = $site->id; } } @@ -521,11 +520,12 @@ public function getEditableSiteIds(): array /** * Returns all sites. * + * @param bool|null $withDisabled * @return Site[] All the sites */ - public function getAllSites(): array + public function getAllSites(bool $withDisabled = null): array { - return array_values($this->_sitesById); + return array_values($this->_allSites($withDisabled)); } /** @@ -551,17 +551,12 @@ public function getEditableSites(): array * Returns sites by a group ID. * * @param int $groupId + * @param bool|null $withDisabled * @return Site[] */ - public function getSitesByGroupId(int $groupId): array + public function getSitesByGroupId(int $groupId, bool $withDisabled = null): array { - $sites = []; - - foreach ($this->getAllSites() as $site) { - if ($site->groupId == $groupId) { - $sites[] = $site; - } - } + $sites = ArrayHelper::where($this->_allSites($withDisabled), 'groupId', $groupId); // Using array_multisort threw a nesting error for no obvious reason, so don't use it here. ArrayHelper::multisort($sites, 'sortOrder', SORT_ASC, SORT_NUMERIC); @@ -593,22 +588,24 @@ public function getTotalEditableSites(): int * Returns a site by its ID. * * @param int $siteId + * @param bool|null $withDisabled * @return Site|null */ - public function getSiteById(int $siteId) + public function getSiteById(int $siteId, bool $withDisabled = null) { - return $this->_sitesById[$siteId] ?? null; + return $this->_allSites($withDisabled)[$siteId] ?? null; } /** * Returns a site by its handle. * * @param string $siteHandle + * @param bool|null $withDisabled * @return Site|null */ - public function getSiteByHandle(string $siteHandle) + public function getSiteByHandle(string $siteHandle, bool $withDisabled = null) { - return $this->_sitesByHandle[$siteHandle] ?? null; + return ArrayHelper::firstWhere($this->_allSites($withDisabled), 'handle', $siteHandle, true); } /** @@ -624,7 +621,7 @@ public function saveSite(Site $site, bool $runValidation = true): bool { $isNewSite = !$site->id; - if (!empty($this->_sitesById)) { + if (!empty($this->_allSitesById)) { $oldPrimarySiteId = $this->getPrimarySite()->id; } else { $oldPrimarySiteId = null; @@ -656,6 +653,7 @@ public function saveSite(Site $site, bool $runValidation = true): bool 'baseUrl' => $site->baseUrl, 'sortOrder' => (int)$site->sortOrder, 'primary' => (bool)$site->primary, + 'enabled' => (bool)$site->enabled, ]; if ($isNewSite) { @@ -717,6 +715,7 @@ public function handleChangedSite(ConfigEvent $event) $siteRecord->hasUrls = $data['hasUrls']; $siteRecord->baseUrl = $data['baseUrl']; $siteRecord->primary = $data['primary']; + $siteRecord->enabled = $data['enabled'] ?? true; $siteRecord->sortOrder = $data['sortOrder']; if ($siteRecord->dateDeleted) { @@ -766,7 +765,6 @@ public function handleChangedSite(ConfigEvent $event) // Re-save most localizable element types // (skip entries because they only support specific sites) // (skip Matrix blocks because they will be re-saved when their owners are re-saved). - $queue = Craft::$app->getQueue(); $elementTypes = [ GlobalSet::class, Asset::class, @@ -775,7 +773,7 @@ public function handleChangedSite(ConfigEvent $event) ]; foreach ($elementTypes as $elementType) { - $queue->push(new PropagateElements([ + Queue::push(new PropagateElements([ 'elementType' => $elementType, 'criteria' => [ 'siteId' => $oldPrimarySiteId, @@ -908,12 +906,11 @@ public function deleteSite(Site $site, int $transferContentTo = null): bool if ($transferContentTo !== null) { $transferContentToSite = $this->getSiteById($transferContentTo); - Craft::$app->getDb()->createCommand() - ->update( - Table::SECTIONS_SITES, - ['siteId' => $transferContentTo], - ['sectionId' => $soloSectionIds]) - ->execute(); + Db::update(Table::SECTIONS_SITES, [ + 'siteId' => $transferContentTo, + ], [ + 'sectionId' => $soloSectionIds, + ]); // Update the project config too $muteEvents = $projectConfig->muteEvents; @@ -938,30 +935,25 @@ public function deleteSite(Site $site, int $transferContentTo = null): bool Craft::$app->getTemplateCaches()->deleteCachesByElementId($entryIds); // Update the entry tables - Craft::$app->getDb()->createCommand() - ->update( - Table::CONTENT, - ['siteId' => $transferContentTo], - ['elementId' => $entryIds]) - ->execute(); - - Craft::$app->getDb()->createCommand() - ->update( - Table::ELEMENTS_SITES, - ['siteId' => $transferContentTo], - ['elementId' => $entryIds]) - ->execute(); - - Craft::$app->getDb()->createCommand() - ->update( - Table::RELATIONS, - ['sourceSiteId' => $transferContentTo], - [ - 'and', - ['sourceId' => $entryIds], - ['not', ['sourceSiteId' => null]] - ]) - ->execute(); + Db::update(Table::CONTENT, [ + 'siteId' => $transferContentTo, + ], [ + 'elementId' => $entryIds, + ]); + + Db::update(Table::ELEMENTS_SITES, [ + 'siteId' => $transferContentTo, + ], [ + 'elementId' => $entryIds, + ]); + + Db::update(Table::RELATIONS, [ + 'sourceSiteId' => $transferContentTo, + ], [ + 'and', + ['sourceId' => $entryIds], + ['not', ['sourceSiteId' => null]], + ]); // All the Matrix tables $blockIds = (new Query()) @@ -971,60 +963,43 @@ public function deleteSite(Site $site, int $transferContentTo = null): bool ->column(); if (!empty($blockIds)) { - Craft::$app->getDb()->createCommand() - ->delete( - Table::ELEMENTS_SITES, - [ - 'elementId' => $blockIds, - 'siteId' => $transferContentTo - ]) - ->execute(); - - Craft::$app->getDb()->createCommand() - ->update( - Table::ELEMENTS_SITES, - ['siteId' => $transferContentTo], - [ - 'elementId' => $blockIds, - 'siteId' => $site->id - ]) - ->execute(); + Db::delete(Table::ELEMENTS_SITES, [ + 'elementId' => $blockIds, + 'siteId' => $transferContentTo, + ]); + + Db::update(Table::ELEMENTS_SITES, [ + 'siteId' => $transferContentTo, + ], [ + 'elementId' => $blockIds, + 'siteId' => $site->id, + ]); $matrixTablePrefix = Craft::$app->getDb()->getSchema()->getRawTableName('{{%matrixcontent_}}'); foreach (Craft::$app->getDb()->getSchema()->getTableNames() as $tableName) { if (strpos($tableName, $matrixTablePrefix) === 0) { - Craft::$app->getDb()->createCommand() - ->delete( - $tableName, - [ - 'elementId' => $blockIds, - 'siteId' => $transferContentTo - ]) - ->execute(); - - Craft::$app->getDb()->createCommand() - ->update( - $tableName, - ['siteId' => $transferContentTo], - [ - 'elementId' => $blockIds, - 'siteId' => $site->id - ]) - ->execute(); + Db::delete($tableName, [ + 'elementId' => $blockIds, + 'siteId' => $transferContentTo, + ]); + + Db::update($tableName, [ + 'siteId' => $transferContentTo, + ], [ + 'elementId' => $blockIds, + 'siteId' => $site->id, + ]); } } - Craft::$app->getDb()->createCommand() - ->update( - Table::RELATIONS, - ['sourceSiteId' => $transferContentTo], - [ - 'and', - ['sourceId' => $blockIds], - ['not', ['sourceSiteId' => null]] - ]) - ->execute(); + Db::update(Table::RELATIONS, [ + 'sourceSiteId' => $transferContentTo, + ], [ + 'and', + ['sourceId' => $blockIds], + ['not', ['sourceSiteId' => null]], + ]); } } } else { @@ -1117,7 +1092,8 @@ public function restoreSiteById(int $id): bool */ private function _refreshAllSites() { - $this->_sitesById = null; + $this->_allSitesById = null; + $this->_enabledSitesById = null; $this->_loadAllSites(); Craft::$app->getIsMultiSite(true); } @@ -1127,13 +1103,12 @@ private function _refreshAllSites() */ private function _loadAllSites() { - if ($this->_sitesById !== null) { + if ($this->_allSitesById !== null) { return; } - $this->_sitesById = []; - $this->_sitesByHandle = []; - $this->_sitesByUid = []; + $this->_allSitesById = []; + $this->_enabledSitesById = []; if (!Craft::$app->getIsInstalled()) { return; @@ -1148,6 +1123,7 @@ private function _loadAllSites() 's.handle', 'language', 's.primary', + 's.enabled', 's.hasUrls', 's.baseUrl', 's.sortOrder', @@ -1155,8 +1131,8 @@ private function _loadAllSites() 's.dateCreated', 's.dateUpdated', ]) - ->from(['{{%sites}} s']) - ->innerJoin('{{%sitegroups}} sg', '[[sg.id]] = [[s.groupId]]') + ->from(['s' => Table::SITES]) + ->innerJoin(['sg' => Table::SITEGROUPS], '[[sg.id]] = [[s.groupId]]') ->where(['s.dateDeleted' => null]) ->andWhere(['sg.dateDeleted' => null]) ->orderBy(['sg.name' => SORT_ASC, 's.sortOrder' => SORT_ASC]) @@ -1167,7 +1143,7 @@ private function _loadAllSites() if (isset($e->errorInfo[0]) && in_array($e->errorInfo[0], ['42S02', '42P01'], true)) { return; } - // If the error code is 42S22 (MySQL) or 42703 (PostgreSQL), then the sites table doesn't have a groupId or dateDeleted column yet + // If the error code is 42S22 (MySQL) or 42703 (PostgreSQL), then the sites table doesn't have a groupId, dateDeleted, or enabled column yet if (isset($e->errorInfo[0]) && in_array($e->errorInfo[0], ['42S22', '42703'], true)) { $results = (new Query()) ->select([ @@ -1181,7 +1157,7 @@ private function _loadAllSites() 's.sortOrder', 's.uid', ]) - ->from(['{{%sites}} s']) + ->from(['s' => Table::SITES]) ->orderBy(['s.name' => SORT_ASC]) ->all(); } @@ -1197,9 +1173,10 @@ private function _loadAllSites() foreach ($results as $i => $result) { $site = new Site($result); - $this->_sitesById[$site->id] = $site; - $this->_sitesByHandle[$site->handle] = $site; - $this->_sitesByUid[$site->uid] = $site; + $this->_allSitesById[$site->id] = $site; + if ($site->enabled) { + $this->_enabledSitesById[$site->id] = $site; + } if ($site->primary) { $this->_primarySite = $site; @@ -1259,6 +1236,21 @@ private function _getGroupRecord($criteria, bool $withTrashed = false): SiteGrou return $query->one() ?? new SiteGroupRecord(); } + /** + * Returns all sites, or only enabled sites. + * + * @param bool|null $withDisabled + * @return Site[] + */ + private function _allSites(bool $withDisabled = null) + { + if ($withDisabled === null) { + $withDisabled = Craft::$app->getRequest()->getIsCpRequest(); + } + + return $withDisabled ? $this->_allSitesById : $this->_enabledSitesById; + } + /** * Gets a site record or creates a new one. * @@ -1293,18 +1285,22 @@ private function _processNewPrimarySite(int $oldPrimarySiteId, int $newPrimarySi $transaction = $db->beginTransaction(); try { - $db->createCommand() - ->update(Table::SITES, ['primary' => false], ['id' => $oldPrimarySiteId]) - ->execute(); - $db->createCommand() - ->update(Table::SITES, ['primary' => true], ['id' => $newPrimarySiteId]) - ->execute(); + Db::update(Table::SITES, [ + 'primary' => false, + ], [ + 'id' => $oldPrimarySiteId, + ]); + Db::update(Table::SITES, [ + 'primary' => true, + ], [ + 'id' => $newPrimarySiteId, + ]); // Update all of the non-localized elements $nonLocalizedElementTypes = []; foreach (Craft::$app->getElements()->getAllElementTypes() as $elementType) { - /** @var Element|string $elementType */ + /** @var ElementInterface|string $elementType */ if (!$elementType::isLocalized()) { $nonLocalizedElementTypes[] = $elementType; } @@ -1326,29 +1322,17 @@ private function _processNewPrimarySite(int $oldPrimarySiteId, int $newPrimarySi ['not', ['siteId' => $oldPrimarySiteId]] ]; - $db->createCommand() - ->delete(Table::ELEMENTS_SITES, $deleteCondition) - ->execute(); - $db->createCommand() - ->delete(Table::CONTENT, $deleteCondition) - ->execute(); - $db->createCommand() - ->delete(Table::SEARCHINDEX, $deleteCondition) - ->execute(); + Db::delete(Table::ELEMENTS_SITES, $deleteCondition); + Db::delete(Table::CONTENT, $deleteCondition); + Db::delete(Table::SEARCHINDEX, $deleteCondition); // Now swap the sites $updateColumns = ['siteId' => $newPrimarySiteId]; $updateCondition = ['elementId' => $elementIds]; - $db->createCommand() - ->update(Table::ELEMENTS_SITES, $updateColumns, $updateCondition, [], false) - ->execute(); - $db->createCommand() - ->update(Table::CONTENT, $updateColumns, $updateCondition, [], false) - ->execute(); - $db->createCommand() - ->update(Table::SEARCHINDEX, $updateColumns, $updateCondition, [], false) - ->execute(); + Db::update(Table::ELEMENTS_SITES, $updateColumns, $updateCondition, [], false); + Db::update(Table::CONTENT, $updateColumns, $updateCondition, [], false); + Db::update(Table::SEARCHINDEX, $updateColumns, $updateCondition, [], false); } } diff --git a/src/services/Structures.php b/src/services/Structures.php index 1d3bf6b2707..42eadbfadcc 100644 --- a/src/services/Structures.php +++ b/src/services/Structures.php @@ -8,7 +8,6 @@ namespace craft\services; use Craft; -use craft\base\Element; use craft\base\ElementInterface; use craft\db\Query; use craft\db\Table; @@ -324,7 +323,6 @@ public function moveAfter(int $structureId, ElementInterface $element, ElementIn */ private function _getElementRecord(int $structureId, ElementInterface $element) { - /** @var Element $element */ $elementId = $element->id; if ($elementId) { @@ -386,7 +384,6 @@ private function _doIt($structureId, ElementInterface $element, StructureElement $elementRecord = null; - /** @var Element $element */ // Figure out what we're doing if ($mode !== 'insert') { // See if there's an existing structure element record diff --git a/src/services/Tags.php b/src/services/Tags.php index 628bf26c3a6..7a491681321 100644 --- a/src/services/Tags.php +++ b/src/services/Tags.php @@ -8,7 +8,6 @@ namespace craft\services; use Craft; -use craft\base\Field; use craft\db\Table; use craft\elements\Tag; use craft\errors\TagGroupNotFoundException; @@ -406,7 +405,6 @@ public function handleDeletedTagGroup(ConfigEvent $event) */ public function pruneDeletedField(FieldEvent $event) { - /** @var Field $field */ $field = $event->field; $fieldUid = $field->uid; @@ -432,7 +430,9 @@ public function pruneDeletedField(FieldEvent $event) } // Nuke all the layout fields from the DB - Craft::$app->getDb()->createCommand()->delete('{{%fieldlayoutfields}}', ['fieldId' => $field->id])->execute(); + Db::delete(Table::FIELDLAYOUTFIELDS, [ + 'fieldId' => $field->id, + ]); // Allow events again $projectConfig->muteEvents = false; diff --git a/src/services/TemplateCaches.php b/src/services/TemplateCaches.php index 726b81b7bd6..110542c8506 100644 --- a/src/services/TemplateCaches.php +++ b/src/services/TemplateCaches.php @@ -8,13 +8,13 @@ namespace craft\services; use Craft; -use craft\base\Element; use craft\base\ElementInterface; use craft\db\Query; use craft\db\Table; use craft\elements\db\ElementQuery; use craft\events\DeleteTemplateCachesEvent; use craft\helpers\Db; +use craft\helpers\Queue; use craft\helpers\StringHelper; use craft\queue\jobs\DeleteStaleTemplateCaches; use DateTime; @@ -272,18 +272,13 @@ public function endTemplateCache(string $key, bool $global, string $duration = n $transaction = Craft::$app->getDb()->beginTransaction(); try { - Craft::$app->getDb()->createCommand() - ->insert( - Table::TEMPLATECACHES, - [ - 'cacheKey' => $key, - 'siteId' => Craft::$app->getSites()->getCurrentSite()->id, - 'path' => $global ? null : $this->_getPath(), - 'expiryDate' => Db::prepareDateForDb($expiration), - 'body' => $body - ], - false) - ->execute(); + Db::insert(Table::TEMPLATECACHES, [ + 'cacheKey' => $key, + 'siteId' => Craft::$app->getSites()->getCurrentSite()->id, + 'path' => $global ? null : $this->_getPath(), + 'expiryDate' => Db::prepareDateForDb($expiration), + 'body' => $body, + ], false); $cacheId = Craft::$app->getDb()->getLastInsertID(Table::TEMPLATECACHES); @@ -299,13 +294,8 @@ public function endTemplateCache(string $key, bool $global, string $duration = n // We can no longer say we’ve deleted all template caches for this element type unset($this->_deletedCachesByElementType[$query[0]]); } - Craft::$app->getDb()->createCommand() - ->batchInsert(Table::TEMPLATECACHEQUERIES, [ - 'cacheId', - 'type', - 'query' - ], $values, false) - ->execute(); + + Db::batchInsert(Table::TEMPLATECACHEQUERIES, ['cacheId', 'type', 'query'], $values, false); unset($this->_cachedQueries[$key]); } @@ -317,14 +307,7 @@ public function endTemplateCache(string $key, bool $global, string $duration = n $values[] = [$cacheId, $elementId]; } - Craft::$app->getDb()->createCommand() - ->batchInsert( - Table::TEMPLATECACHEELEMENTS, - ['cacheId', 'elementId'], - $values, - false) - ->execute(); - + Db::batchInsert(Table::TEMPLATECACHEELEMENTS, ['cacheId', 'elementId'], $values, false); unset($this->_cacheElementIds[$key]); } @@ -362,9 +345,9 @@ public function deleteCacheById($cacheId): bool ])); } - $affectedRows = Craft::$app->getDb()->createCommand() - ->delete(Table::TEMPLATECACHES, ['id' => $cacheId]) - ->execute(); + $affectedRows = Db::delete(Table::TEMPLATECACHES, [ + 'id' => $cacheId, + ]); // Fire an 'afterDeleteCaches' event if ($affectedRows && $this->hasEventHandlers(self::EVENT_AFTER_DELETE_CACHES)) { @@ -422,7 +405,7 @@ public function deleteCachesByElement($elements): bool $deleteQueryCaches = !isset($this->_deletedCachesByElementType[$elementType]); $elementIds = []; - /** @var Element[] $elements */ + /** @var ElementInterface[] $elements */ foreach ($elements as $element) { $elementIds[] = $element->id; } @@ -481,7 +464,7 @@ public function handleResponse() { // It's possible this is already null if ($this->_deleteCachesIndex !== null) { - Craft::$app->getQueue()->push(new DeleteStaleTemplateCaches([ + Queue::push(new DeleteStaleTemplateCaches([ 'elementId' => array_keys($this->_deleteCachesIndex), ])); diff --git a/src/services/Tokens.php b/src/services/Tokens.php index 13391383802..293f593ac7e 100644 --- a/src/services/Tokens.php +++ b/src/services/Tokens.php @@ -132,18 +132,11 @@ public function getTokenRoute(string $token) */ public function incrementTokenUsageCountById(int $tokenId): bool { - $affectedRows = Craft::$app->getDb()->createCommand() - ->update( - Table::TOKENS, - [ - 'usageCount' => new Expression('[[usageCount]] + 1') - ], - [ - 'id' => $tokenId - ]) - ->execute(); - - return (bool)$affectedRows; + return (bool)Db::update(Table::TOKENS, [ + 'usageCount' => new Expression('[[usageCount]] + 1'), + ], [ + 'id' => $tokenId, + ]); } /** @@ -154,9 +147,9 @@ public function incrementTokenUsageCountById(int $tokenId): bool */ public function deleteTokenById(int $tokenId): bool { - Craft::$app->getDb()->createCommand() - ->delete(Table::TOKENS, ['id' => $tokenId]) - ->execute(); + Db::delete(Table::TOKENS, [ + 'id' => $tokenId, + ]); return true; } @@ -173,9 +166,7 @@ public function deleteExpiredTokens(): bool return false; } - $affectedRows = Craft::$app->getDb()->createCommand() - ->delete(Table::TOKENS, ['<=', 'expiryDate', Db::prepareDateForDb(new DateTime())]) - ->execute(); + $affectedRows = Db::delete(Table::TOKENS, ['<=', 'expiryDate', Db::prepareDateForDb(new DateTime())]); $this->_deletedExpiredTokens = true; diff --git a/src/services/Updates.php b/src/services/Updates.php index c1d32b5be32..4e6dcda7afa 100644 --- a/src/services/Updates.php +++ b/src/services/Updates.php @@ -8,11 +8,11 @@ namespace craft\services; use Craft; -use craft\base\Plugin; use craft\base\PluginInterface; use craft\db\Table; use craft\errors\MigrateException; use craft\helpers\ArrayHelper; +use craft\helpers\Db; use craft\helpers\FileHelper; use craft\models\Updates as UpdatesModel; use yii\base\Component; @@ -135,16 +135,12 @@ public function cacheUpdates(array $updateData): UpdatesModel */ public function setNewPluginInfo(PluginInterface $plugin): bool { - /** @var Plugin $plugin */ - $affectedRows = Craft::$app->getDb()->createCommand() - ->update( - Table::PLUGINS, - [ - 'version' => $plugin->getVersion(), - 'schemaVersion' => $plugin->schemaVersion - ], - ['handle' => $plugin->id]) - ->execute(); + $success = (bool)Db::update(Table::PLUGINS, [ + 'version' => $plugin->getVersion(), + 'schemaVersion' => $plugin->schemaVersion, + ], [ + 'handle' => $plugin->id, + ]); // Only update the schema version if it's changed from what's in the file, // so we don't accidentally overwrite other pending changes @@ -155,7 +151,7 @@ public function setNewPluginInfo(PluginInterface $plugin): bool Craft::$app->getProjectConfig()->set($key, $plugin->schemaVersion, "Update plugin schema version for “{$plugin->handle}”"); } - return (bool)$affectedRows; + return $success; } /** @@ -177,7 +173,6 @@ public function getPendingMigrationHandles($includeContent = false): array $pluginsService = Craft::$app->getPlugins(); foreach ($pluginsService->getAllPlugins() as $plugin) { - /** @var Plugin $plugin */ if ($pluginsService->doesPluginRequireDatabaseUpdate($plugin)) { $handles[] = $plugin->id; } @@ -227,7 +222,6 @@ public function runMigrations(array $handles) } else if ($handle === 'content') { Craft::$app->getContentMigrator()->up(); } else { - /** @var Plugin $plugin */ $plugin = Craft::$app->getPlugins()->getPlugin($handle); $name = $plugin->name; $plugin->getMigrator()->up(); diff --git a/src/services/UserGroups.php b/src/services/UserGroups.php index c2a8fc4c5e2..4da7da5c26f 100644 --- a/src/services/UserGroups.php +++ b/src/services/UserGroups.php @@ -175,8 +175,8 @@ public function getGroupsByUserId(int $userId): array 'g.handle', 'g.uid' ]) - ->from(['{{%usergroups}} g']) - ->innerJoin('{{%usergroups_users}} gu', '[[gu.groupId]] = [[g.id]]') + ->from(['g' => Table::USERGROUPS]) + ->innerJoin(['gu' => Table::USERGROUPS_USERS], '[[gu.groupId]] = [[g.id]]') ->where(['gu.userId' => $userId]) ->all(); @@ -289,9 +289,9 @@ public function handleDeletedUserGroup(ConfigEvent $event) ])); } - Craft::$app->getDb()->createCommand() - ->delete(Table::USERGROUPS, ['uid' => $uid]) - ->execute(); + Db::delete(Table::USERGROUPS, [ + 'uid' => $uid, + ]); // Fire an 'afterDeleteUserGroup' event if ($this->hasEventHandlers(self::EVENT_AFTER_DELETE_USER_GROUP)) { diff --git a/src/services/UserPermissions.php b/src/services/UserPermissions.php index d0b4dfa2ad5..43fb28310f3 100644 --- a/src/services/UserPermissions.php +++ b/src/services/UserPermissions.php @@ -9,15 +9,14 @@ namespace craft\services; use Craft; -use craft\base\Plugin; use craft\base\UtilityInterface; -use craft\base\Volume; use craft\db\Query; use craft\db\Table; use craft\elements\User; use craft\errors\WrongEditionException; use craft\events\ConfigEvent; use craft\events\RegisterUserPermissionsEvent; +use craft\helpers\Db; use craft\helpers\ProjectConfig as ProjectConfigHelper; use craft\models\CategoryGroup; use craft\models\Section; @@ -83,7 +82,6 @@ public function getAllPermissions(): array ]; foreach (Craft::$app->getPlugins()->getAllPlugins() as $plugin) { - /** @var Plugin $plugin */ if ($plugin->hasCpSection) { $general['accessCp']['nested']['accessPlugin-' . $plugin->id] = [ 'label' => Craft::t('app', 'Access {plugin}', ['plugin' => $plugin->name]) @@ -192,7 +190,6 @@ public function getAllPermissions(): array // Volumes // --------------------------------------------------------------------- - /** @var Volume[] $volumes */ $volumes = Craft::$app->getVolumes()->getAllVolumes(); foreach ($volumes as $volume) { @@ -252,7 +249,7 @@ public function getPermissionsByGroupId(int $groupId): array { if (!isset($this->_permissionsByGroupId[$groupId])) { $groupPermissions = $this->_createUserPermissionsQuery() - ->innerJoin('{{%userpermissions_usergroups}} p_g', '[[p_g.permissionId]] = [[p.id]]') + ->innerJoin(['p_g' => Table::USERPERMISSIONS_USERGROUPS], '[[p_g.permissionId]] = [[p.id]]') ->where(['p_g.groupId' => $groupId]) ->column(); @@ -271,8 +268,8 @@ public function getPermissionsByGroupId(int $groupId): array public function getGroupPermissionsByUserId(int $userId): array { return $this->_createUserPermissionsQuery() - ->innerJoin('{{%userpermissions_usergroups}} p_g', '[[p_g.permissionId]] = [[p.id]]') - ->innerJoin('{{%usergroups_users}} g_u', '[[g_u.groupId]] = [[p_g.groupId]]') + ->innerJoin(['p_g' => Table::USERPERMISSIONS_USERGROUPS], '[[p_g.permissionId]] = [[p.id]]') + ->innerJoin(['g_u' => Table::USERGROUPS_USERS], '[[g_u.groupId]] = [[p_g.groupId]]') ->where(['g_u.userId' => $userId]) ->column(); } @@ -330,7 +327,7 @@ public function getPermissionsByUserId(int $userId): array $groupPermissions = $this->getGroupPermissionsByUserId($userId); $userPermissions = $this->_createUserPermissionsQuery() - ->innerJoin('{{%userpermissions_users}} p_u', '[[p_u.permissionId]] = [[p.id]]') + ->innerJoin(['p_u' => Table::USERPERMISSIONS_USERS], '[[p_u.permissionId]] = [[p.id]]') ->where(['p_u.userId' => $userId]) ->column(); @@ -369,9 +366,9 @@ public function saveUserPermissions(int $userId, array $permissions): bool Craft::$app->requireEdition(Craft::Pro); // Delete any existing user permissions - Craft::$app->getDb()->createCommand() - ->delete(Table::USERPERMISSIONS_USERS, ['userId' => $userId]) - ->execute(); + Db::delete(Table::USERPERMISSIONS_USERS, [ + 'userId' => $userId, + ]); // Lowercase the permissions $permissions = array_map('strtolower', $permissions); @@ -389,12 +386,7 @@ public function saveUserPermissions(int $userId, array $permissions): bool } // Add the new user permissions - Craft::$app->getDb()->createCommand() - ->batchInsert( - Table::USERPERMISSIONS_USERS, - ['permissionId', 'userId'], - $userPermissionVals) - ->execute(); + Db::batchInsert(Table::USERPERMISSIONS_USERS, ['permissionId', 'userId'], $userPermissionVals); } // Cache the new permissions @@ -419,9 +411,9 @@ public function handleChangedGroupPermissions(ConfigEvent $event) $userGroup = Craft::$app->getUserGroups()->getGroupByUid($uid); // Delete any existing group permissions - Craft::$app->getDb()->createCommand() - ->delete(Table::USERPERMISSIONS_USERGROUPS, ['groupId' => $userGroup->id]) - ->execute(); + Db::delete(Table::USERPERMISSIONS_USERGROUPS, [ + 'groupId' => $userGroup->id, + ]); $groupPermissionVals = []; @@ -432,12 +424,7 @@ public function handleChangedGroupPermissions(ConfigEvent $event) } // Add the new group permissions - Craft::$app->getDb()->createCommand() - ->batchInsert( - Table::USERPERMISSIONS_USERGROUPS, - ['permissionId', 'groupId'], - $groupPermissionVals) - ->execute(); + Db::batchInsert(Table::USERPERMISSIONS_USERGROUPS, ['permissionId', 'groupId'], $groupPermissionVals); } // Update caches @@ -751,6 +738,6 @@ private function _createUserPermissionsQuery(): Query { return (new Query()) ->select(['p.name']) - ->from(['{{%userpermissions}} p']); + ->from(['p' => Table::USERPERMISSIONS]); } } diff --git a/src/services/Users.php b/src/services/Users.php index b47575eb72d..b9707b5a3f2 100644 --- a/src/services/Users.php +++ b/src/services/Users.php @@ -8,8 +8,6 @@ namespace craft\services; use Craft; -use craft\base\Field; -use craft\base\Volume; use craft\db\Query; use craft\db\Table; use craft\elements\Asset; @@ -289,14 +287,11 @@ public function saveUserPreferences(User $user, array $preferences) { $preferences = $user->mergePreferences($preferences); - Craft::$app->getDb()->createCommand() - ->upsert( - Table::USERPREFERENCES, - ['userId' => $user->id], - ['preferences' => Json::encode($preferences)], - [], - false) - ->execute(); + Db::upsert(Table::USERPREFERENCES, [ + 'userId' => $user->id, + ], [ + 'preferences' => Json::encode($preferences), + ], [], false); } /** @@ -431,7 +426,6 @@ public function saveUserPhoto(string $fileLocation, User $user, string $filename } } - /** @var Volume $volume */ $assetsService = Craft::$app->getAssets(); // If the photo exists, just replace the file. @@ -782,19 +776,12 @@ public function unsuspendUser(User $user): bool */ public function shunMessageForUser(int $userId, string $message, DateTime $expiryDate = null): bool { - $affectedRows = Craft::$app->getDb()->createCommand() - ->upsert( - Table::SHUNNEDMESSAGES, - [ - 'userId' => $userId, - 'message' => $message - ], - [ - 'expiryDate' => Db::prepareDateForDb($expiryDate) - ]) - ->execute(); - - return (bool)$affectedRows; + return (bool)Db::upsert(Table::SHUNNEDMESSAGES, [ + 'userId' => $userId, + 'message' => $message, + ], [ + 'expiryDate' => Db::prepareDateForDb($expiryDate), + ]); } /** @@ -806,16 +793,10 @@ public function shunMessageForUser(int $userId, string $message, DateTime $expir */ public function unshunMessageForUser(int $userId, string $message): bool { - $affectedRows = Craft::$app->getDb()->createCommand() - ->delete( - Table::SHUNNEDMESSAGES, - [ - 'userId' => $userId, - 'message' => $message - ]) - ->execute(); - - return (bool)$affectedRows; + return (bool)Db::delete(Table::SHUNNEDMESSAGES, [ + 'userId' => $userId, + 'message' => $message, + ]); } /** @@ -913,9 +894,9 @@ public function assignUserToGroups(int $userId, array $groupIds): bool } // Delete their existing groups - Craft::$app->getDb()->createCommand() - ->delete(Table::USERGROUPS_USERS, ['userId' => $userId]) - ->execute(); + Db::delete(Table::USERGROUPS_USERS, [ + 'userId' => $userId, + ]); if (!empty($groupIds)) { // Add the new ones @@ -924,15 +905,7 @@ public function assignUserToGroups(int $userId, array $groupIds): bool $values[] = [$groupId, $userId]; } - Craft::$app->getDb()->createCommand() - ->batchInsert( - Table::USERGROUPS_USERS, - [ - 'groupId', - 'userId' - ], - $values) - ->execute(); + Db::batchInsert(Table::USERGROUPS_USERS, ['groupId', 'userId'], $values); } // Fire an 'afterAssignUserToGroups' event @@ -1103,7 +1076,6 @@ public function canImpersonate(User $impersonator, User $impersonatee): bool */ public function pruneDeletedField(FieldEvent $event) { - /** @var Field $field */ $field = $event->field; $fieldUid = $field->uid; @@ -1125,7 +1097,9 @@ public function pruneDeletedField(FieldEvent $event) } // Nuke all the layout fields from the DB - Craft::$app->getDb()->createCommand()->delete('{{%fieldlayoutfields}}', ['fieldId' => $field->id])->execute(); + Db::delete(Table::FIELDLAYOUTFIELDS, [ + 'fieldId' => $field->id, + ]); // Allow events again $projectConfig->muteEvents = false; @@ -1207,7 +1181,6 @@ private function _getUserUrl(User $user, string $fePath, string $cpPath): string $unhashedVerificationCode = $this->_setVerificationCodeOnUserRecord($userRecord); $userRecord->save(); - $generalConfig = Craft::$app->getConfig()->getGeneral(); $params = [ 'code' => $unhashedVerificationCode, 'id' => $user->uid @@ -1221,11 +1194,11 @@ private function _getUserUrl(User $user, string $fePath, string $cpPath): string // Only use cpUrl() if the base CP URL has been explicitly set, // so UrlHelper won't use HTTP_HOST - if ($generalConfig->baseCpUrl) { + if (Craft::$app->getConfig()->getGeneral()->baseCpUrl) { return UrlHelper::cpUrl($cpPath, $params, $scheme); } - $path = $generalConfig->cpTrigger . '/' . $cpPath; + $path = UrlHelper::prependCpTrigger($cpPath); return UrlHelper::siteUrl($path, $params, $scheme); } } diff --git a/src/services/Volumes.php b/src/services/Volumes.php index 5dc3a7012e7..b8b1762cf45 100644 --- a/src/services/Volumes.php +++ b/src/services/Volumes.php @@ -3,8 +3,6 @@ namespace craft\services; use Craft; -use craft\base\Field; -use craft\base\Volume; use craft\base\VolumeInterface; use craft\db\Query; use craft\db\Table; @@ -152,7 +150,6 @@ public function getViewableVolumes(): array { $userSession = Craft::$app->getUser(); return ArrayHelper::where($this->getAllVolumes(), function(VolumeInterface $volume) use ($userSession) { - /** @var Volume $volume */ return $userSession->checkPermission('viewVolume:' . $volume->uid); }); } @@ -280,7 +277,6 @@ public function getVolumeByHandle(string $handle) */ public function saveVolume(VolumeInterface $volume, bool $runValidation = true): bool { - /** @var Volume $volume */ $isNewVolume = $volume->getIsNew(); // Fire a 'beforeSaveVolume' event @@ -425,7 +421,6 @@ public function handleChangedVolume(ConfigEvent $event) // Clear caches $this->_volumes = null; - /** @var Volume $volume */ $volume = $this->getVolumeById($volumeRecord->id); $volume->afterSave($isNewVolume); @@ -513,7 +508,6 @@ public function createVolume($config): VolumeInterface } try { - /** @var Volume $volume */ $volume = ComponentHelper::createComponent($config, VolumeInterface::class); } catch (UnknownPropertyException $e) { // Special case for Local volumes that are being converted to something else @@ -549,7 +543,6 @@ public function createVolume($config): VolumeInterface */ public function ensureTopFolder(VolumeInterface $volume): int { - /** @var Volume $volume */ $folder = VolumeFolder::findOne( [ 'name' => $volume->name, @@ -596,7 +589,6 @@ public function deleteVolumeById(int $volumeId): bool */ public function deleteVolume(VolumeInterface $volume): bool { - /** @var Volume $volume */ // Fire a 'beforeDeleteVolume' event if ($this->hasEventHandlers(self::EVENT_BEFORE_DELETE_VOLUME)) { $this->trigger(self::EVENT_BEFORE_DELETE_VOLUME, new VolumeEvent([ @@ -626,7 +618,6 @@ public function handleDeletedVolume(ConfigEvent $event) return; } - /** @var Volume $volume */ $volume = $this->getVolumeById($volumeRecord->id); // Fire a 'beforeApplyVolumeDelete' event @@ -691,7 +682,6 @@ public function handleDeletedVolume(ConfigEvent $event) */ public function pruneDeletedField(FieldEvent $event) { - /** @var Field $field */ $field = $event->field; $fieldUid = $field->uid; @@ -717,7 +707,9 @@ public function pruneDeletedField(FieldEvent $event) } // Nuke all the layout fields from the DB - Craft::$app->getDb()->createCommand()->delete('{{%fieldlayoutfields}}', ['fieldId' => $field->id])->execute(); + Db::delete(Table::FIELDLAYOUTFIELDS, [ + 'fieldId' => $field->id, + ]); // Allow events again $projectConfig->muteEvents = false; diff --git a/src/templates/_components/fieldtypes/Assets/settings.html b/src/templates/_components/fieldtypes/Assets/settings.html index a6f561b98d3..32a0d424fc4 100644 --- a/src/templates/_components/fieldtypes/Assets/settings.html +++ b/src/templates/_components/fieldtypes/Assets/settings.html @@ -33,7 +33,7 @@ {% block fieldSettings %} {{ forms.checkboxField({ - label: "Restrict uploads to a single folder?"|t('app'), + label: 'Restrict uploads to a single folder'|t('app'), id: 'useSingleFolder-toggle', name: 'useSingleFolder', class: 'use-single-folder-cb', @@ -86,7 +86,7 @@ }) }} {{ forms.checkboxField({ - label: "Restrict allowed file types?"|t('app'), + label: 'Restrict allowed file types'|t('app'), class: 'restrictFiles', id: 'restrictFiles', name: 'restrictFiles', diff --git a/src/templates/_components/fieldtypes/Matrix/settings.html b/src/templates/_components/fieldtypes/Matrix/settings.html index 626da7b6e85..0e6dfbfea9d 100644 --- a/src/templates/_components/fieldtypes/Matrix/settings.html +++ b/src/templates/_components/fieldtypes/Matrix/settings.html @@ -101,7 +101,7 @@
{{ "Field Settings"|t('app') }}
}) }} {{ forms.checkboxField({ - label: "Use this field’s values as search keywords?"|t('app'), + label: 'Use this field’s values as search keywords'|t('app'), id: 'searchable', name: 'searchable', checked: field.searchable diff --git a/src/templates/_components/utilities/AssetIndexes.html b/src/templates/_components/utilities/AssetIndexes.html index a42a8349a4a..63e0f80d3b0 100644 --- a/src/templates/_components/utilities/AssetIndexes.html +++ b/src/templates/_components/utilities/AssetIndexes.html @@ -8,7 +8,7 @@ {{ forms.lightswitchField({ name: 'cacheImages', - label: 'Cache remote images?'|t('app'), + label: 'Cache remote images'|t('app'), instructions: 'Whether remotely-stored images should be downloaded and stored locally, to speed up transform generation.'|t('app'), on: true, }) }} diff --git a/src/templates/_components/utilities/DbBackup.html b/src/templates/_components/utilities/DbBackup.html index d7c5d9735cb..b250bd61965 100644 --- a/src/templates/_components/utilities/DbBackup.html +++ b/src/templates/_components/utilities/DbBackup.html @@ -7,7 +7,7 @@ {{ forms.checkbox({ name: 'downloadBackup', id: 'download-backup', - label: 'Download backup?'|t('app'), + label: 'Download backup'|t('app'), checked: true, }) }} diff --git a/src/templates/_components/utilities/SystemMessages/message-modal.html b/src/templates/_components/utilities/SystemMessages/message-modal.html index 5d6e66d37f5..25ddc6863df 100644 --- a/src/templates/_components/utilities/SystemMessages/message-modal.html +++ b/src/templates/_components/utilities/SystemMessages/message-modal.html @@ -28,7 +28,7 @@

{{ "Edit Message"|t('app') }}

{{ forms.textareaField({ label: "Body"|t('app'), id: 'message-body', - class: 'message-body code', + class: ['message-body', 'code'], value: message.body, rows: 15 }) }} diff --git a/src/templates/_components/widgets/CraftSupport/body.html b/src/templates/_components/widgets/CraftSupport/body.html index 8326c7602ea..134a4745580 100644 --- a/src/templates/_components/widgets/CraftSupport/body.html +++ b/src/templates/_components/widgets/CraftSupport/body.html @@ -69,14 +69,14 @@

CraftQuest