uawdijnntqw1x1x1
IP : 216.73.216.107
Hostname : toronto-dev2
Kernel : Linux toronto-dev2 4.15.0-213-generic #224-Ubuntu SMP Mon Jun 19 13:30:12 UTC 2023 x86_64
Disable Function : None :)
OS : Linux
PATH:
/
srv
/
users
/
craft4
/
apps
/
craft4-newsite-space
/
.
/
vendor
/
.
/
craftcms
/
cms
/
src
/
.
/
services
/
Categories.php
/
/
<?php /** * @link https://craftcms.com/ * @copyright Copyright (c) Pixel & Tonic, Inc. * @license https://craftcms.github.io/license/ */ namespace craft\services; use Craft; use craft\base\MemoizableArray; use craft\db\Query; use craft\db\Table; use craft\elements\Category; use craft\errors\CategoryGroupNotFoundException; use craft\events\CategoryGroupEvent; use craft\events\ConfigEvent; use craft\events\DeleteSiteEvent; use craft\helpers\App; use craft\helpers\ArrayHelper; use craft\helpers\Db; use craft\helpers\ProjectConfig as ProjectConfigHelper; use craft\helpers\StringHelper; use craft\models\CategoryGroup; use craft\models\CategoryGroup_SiteSettings; use craft\models\FieldLayout; use craft\models\Structure; use craft\records\CategoryGroup as CategoryGroupRecord; use craft\records\CategoryGroup_SiteSettings as CategoryGroup_SiteSettingsRecord; use craft\web\View; use DateTime; use Throwable; use yii\base\Component; use yii\base\Exception; /** * Categories service. * * An instance of the service is available via [[\craft\base\ApplicationTrait::getCategories()|`Craft::$app->categories`]]. * * @author Pixel & Tonic, Inc. <support@pixelandtonic.com> * @since 3.0.0 */ class Categories extends Component { /** * @event CategoryGroupEvent The event that is triggered before a category group is saved. */ public const EVENT_BEFORE_SAVE_GROUP = 'beforeSaveGroup'; /** * @event CategoryGroupEvent The event that is triggered after a category group is saved. */ public const EVENT_AFTER_SAVE_GROUP = 'afterSaveGroup'; /** * @event CategoryGroupEvent The event that is triggered before a category group is deleted. */ public const EVENT_BEFORE_DELETE_GROUP = 'beforeDeleteGroup'; /** * @event CategoryGroupEvent The event that is triggered before a category group delete is applied to the database. * @since 3.1.0 */ public const EVENT_BEFORE_APPLY_GROUP_DELETE = 'beforeApplyGroupDelete'; /** * @event CategoryGroupEvent The event that is triggered after a category group is deleted. */ public const EVENT_AFTER_DELETE_GROUP = 'afterDeleteGroup'; /** * @var MemoizableArray<CategoryGroup>|null * @see _groups() */ private ?MemoizableArray $_groups = null; /** * Serializer */ public function __serialize() { $vars = get_object_vars($this); unset($vars['_groups']); return $vars; } // Category groups // ------------------------------------------------------------------------- /** * Returns all of the group IDs. * * @return int[] */ public function getAllGroupIds(): array { return array_map(fn(CategoryGroup $group) => $group->id, $this->getAllGroups()); } /** * Returns all of the category group IDs that are editable by the current user. * * @return int[] */ public function getEditableGroupIds(): array { return array_map(fn(CategoryGroup $group) => $group->id, $this->getEditableGroups()); } /** * Returns a memoizable array of all category groups. * * @return MemoizableArray<CategoryGroup> */ private function _groups(): MemoizableArray { if (!isset($this->_groups)) { $groupRecords = CategoryGroupRecord::find() ->orderBy(['name' => SORT_ASC]) ->with('structure') ->all(); $this->_groups = new MemoizableArray( $groupRecords, fn(CategoryGroupRecord $record) => $this->_createCategoryGroupFromRecord($record), ); } return $this->_groups; } /** * Returns all category groups. * * @return CategoryGroup[] */ public function getAllGroups(): array { return $this->_groups()->all(); } /** * Returns all editable groups. * * @return CategoryGroup[] */ public function getEditableGroups(): array { if (Craft::$app->getRequest()->getIsConsoleRequest()) { return $this->getAllGroups(); } $user = Craft::$app->getUser()->getIdentity(); if (!$user) { return []; } return ArrayHelper::where($this->getAllGroups(), function(CategoryGroup $group) use ($user) { return $user->can("viewCategories:$group->uid"); }, true, true, false); } /** * Gets the total number of category groups. * * @return int */ public function getTotalGroups(): int { return count($this->getAllGroups()); } /** * Returns a group by its ID. * * @param int $groupId * @return CategoryGroup|null */ public function getGroupById(int $groupId): ?CategoryGroup { return $this->_groups()->firstWhere('id', $groupId); } /** * Returns a group by its UID. * * @param string $uid * @return CategoryGroup|null * @since 3.1.0 */ public function getGroupByUid(string $uid): ?CategoryGroup { return $this->_groups()->firstWhere('uid', $uid, true); } /** * Returns a group by its handle. * * @param string $groupHandle * @param bool $withTrashed * @return CategoryGroup|null */ public function getGroupByHandle(string $groupHandle, bool $withTrashed = false): ?CategoryGroup { /** @var CategoryGroup|null $group */ $group = $this->_groups()->firstWhere('handle', $groupHandle, true); if (!$group && $withTrashed) { /** @var CategoryGroupRecord|null $record */ $record = CategoryGroupRecord::findWithTrashed() ->andWhere(['handle' => $groupHandle]) ->one(); if ($record) { $group = $this->_createCategoryGroupFromRecord($record); } } return $group; } /** * Returns a group's site settings. * * @param int $groupId * @return CategoryGroup_SiteSettings[] */ public function getGroupSiteSettings(int $groupId): array { /** @var CategoryGroup_SiteSettingsRecord[] $results */ $results = CategoryGroup_SiteSettingsRecord::find() ->where(['groupId' => $groupId]) ->all(); $siteSettings = []; foreach ($results as $result) { $siteSettings[] = new CategoryGroup_SiteSettings($result->toArray([ 'id', 'groupId', 'siteId', 'hasUrls', 'uriFormat', 'template', ])); } return $siteSettings; } /** * Saves a category group. * * @param CategoryGroup $group The category group to be saved * @param bool $runValidation Whether the category group should be validated * @return bool Whether the category group was saved successfully * @throws CategoryGroupNotFoundException if $group has an invalid ID * @throws Throwable if reasons */ public function saveGroup(CategoryGroup $group, bool $runValidation = true): bool { $isNewCategoryGroup = !$group->id; // Fire a 'beforeSaveGroup' event if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_GROUP)) { $this->trigger(self::EVENT_BEFORE_SAVE_GROUP, new CategoryGroupEvent([ 'categoryGroup' => $group, 'isNew' => $isNewCategoryGroup, ])); } if ($runValidation && !$group->validate()) { Craft::info('Category group not saved due to validation error.', __METHOD__); return false; } if ($isNewCategoryGroup) { $group->uid = StringHelper::UUID(); } // If they've set maxLevels to 0 (don't ask why), then pretend like there are none. if ((int)$group->maxLevels === 0) { $group->maxLevels = null; } // Make sure the group isn't missing any site settings $allSiteSettings = $group->getSiteSettings(); foreach (Craft::$app->getSites()->getAllSiteIds() as $siteId) { if (!isset($allSiteSettings[$siteId])) { throw new Exception('Tried to save a category group that is missing site settings'); } } $configPath = ProjectConfig::PATH_CATEGORY_GROUPS . '.' . $group->uid; $configData = $group->getConfig(); Craft::$app->getProjectConfig()->set($configPath, $configData, "Save category group “{$group->handle}”"); if ($isNewCategoryGroup) { $group->id = Db::idByUid(Table::CATEGORYGROUPS, $group->uid); } return true; } /** * Handle category group change * * @param ConfigEvent $event */ public function handleChangedCategoryGroup(ConfigEvent $event): void { $categoryGroupUid = $event->tokenMatches[0]; $data = $event->newValue; // Make sure fields and sites are processed ProjectConfigHelper::ensureAllSitesProcessed(); ProjectConfigHelper::ensureAllFieldsProcessed(); $transaction = Craft::$app->getDb()->beginTransaction(); try { $structureData = $data['structure']; $siteData = $data['siteSettings']; $structureUid = $structureData['uid']; // Basic data $groupRecord = $this->_getCategoryGroupRecord($categoryGroupUid, true); $isNewCategoryGroup = $groupRecord->getIsNewRecord(); $groupRecord->name = $data['name']; $groupRecord->handle = $data['handle']; $groupRecord->uid = $categoryGroupUid; $groupRecord->defaultPlacement = $data['defaultPlacement'] ?? CategoryGroup::DEFAULT_PLACEMENT_END; // Structure $structuresService = Craft::$app->getStructures(); $structure = $structuresService->getStructureByUid($structureUid, true) ?? new Structure(['uid' => $structureUid]); $structure->maxLevels = $structureData['maxLevels']; $structuresService->saveStructure($structure); $groupRecord->structureId = $structure->id; // Save the field layout if (!empty($data['fieldLayouts'])) { // Save the field layout $layout = FieldLayout::createFromConfig(reset($data['fieldLayouts'])); $layout->id = $groupRecord->fieldLayoutId; $layout->type = Category::class; $layout->uid = key($data['fieldLayouts']); Craft::$app->getFields()->saveLayout($layout, false); $groupRecord->fieldLayoutId = $layout->id; } elseif ($groupRecord->fieldLayoutId) { // Delete the field layout Craft::$app->getFields()->deleteLayoutById($groupRecord->fieldLayoutId); $groupRecord->fieldLayoutId = null; } // Save the category group if ($wasTrashed = (bool)$groupRecord->dateDeleted) { $groupRecord->restore(); } else { $groupRecord->save(false); } // Update the site settings // ----------------------------------------------------------------- $sitesNowWithoutUrls = []; $sitesWithNewUriFormats = []; if (!$isNewCategoryGroup) { // Get the old category group site settings /** @var CategoryGroup_SiteSettingsRecord[] $allOldSiteSettingsRecords */ $allOldSiteSettingsRecords = CategoryGroup_SiteSettingsRecord::find() ->where(['groupId' => $groupRecord->id]) ->indexBy('siteId') ->all(); } $siteIdMap = Db::idsByUids(Table::SITES, array_keys($siteData)); foreach ($siteData as $siteUid => $siteSettings) { $siteId = $siteIdMap[$siteUid]; // Was this already selected? if (!$isNewCategoryGroup && isset($allOldSiteSettingsRecords[$siteId])) { $siteSettingsRecord = $allOldSiteSettingsRecords[$siteId]; } else { $siteSettingsRecord = new CategoryGroup_SiteSettingsRecord(); $siteSettingsRecord->groupId = $groupRecord->id; $siteSettingsRecord->siteId = $siteId; } if ($siteSettingsRecord->hasUrls = $siteSettings['hasUrls']) { $siteSettingsRecord->uriFormat = $siteSettings['uriFormat']; $siteSettingsRecord->template = $siteSettings['template']; } else { $siteSettingsRecord->uriFormat = null; $siteSettingsRecord->template = null; } if (!$siteSettingsRecord->getIsNewRecord()) { // Did it used to have URLs, but not anymore? if ($siteSettingsRecord->isAttributeChanged('hasUrls', false) && !$siteSettings['hasUrls']) { $sitesNowWithoutUrls[] = $siteId; } // Does it have URLs, and has its URI format changed? if ($siteSettings['hasUrls'] && $siteSettingsRecord->isAttributeChanged('uriFormat', false)) { $sitesWithNewUriFormats[] = $siteId; } } $siteSettingsRecord->save(false); } if (!$isNewCategoryGroup) { // Drop any site settings that are no longer being used, as well as the associated category/element // site rows $affectedSiteUids = array_keys($siteData); foreach ($allOldSiteSettingsRecords as $siteId => $siteSettingsRecord) { $siteUid = array_search($siteId, $siteIdMap, false); if (!in_array($siteUid, $affectedSiteUids, false)) { $siteSettingsRecord->delete(); } } } // Finally, deal with the existing categories... // ----------------------------------------------------------------- if (!$isNewCategoryGroup) { // Get all of the category IDs in this group $categoryIds = Category::find() ->groupId($groupRecord->id) ->status(null) ->ids(); // Are there any sites left? if (!empty($siteData)) { // Drop the old category URIs for any site settings that don't have URLs if (!empty($sitesNowWithoutUrls)) { Db::update(Table::ELEMENTS_SITES, [ 'uri' => null, ], [ 'elementId' => $categoryIds, 'siteId' => $sitesNowWithoutUrls, ]); } elseif (!empty($sitesWithNewUriFormats)) { foreach ($categoryIds as $categoryId) { App::maxPowerCaptain(); // Loop through each of the changed sites and update all of the categories’ slugs and URIs foreach ($sitesWithNewUriFormats as $siteId) { /** @var Category|null $category */ $category = Category::find() ->id($categoryId) ->siteId($siteId) ->status(null) ->one(); if ($category) { Craft::$app->getElements()->updateElementSlugAndUri($category, false, false); } } } } } } $transaction->commit(); } catch (Throwable $e) { $transaction->rollBack(); throw $e; } // Clear caches $this->_groups = null; if ($wasTrashed) { // Restore the categories that were deleted with the group /** @var Category[] $categories */ $categories = Category::find() ->groupId($groupRecord->id) ->trashed() ->andWhere(['categories.deletedWithGroup' => true]) ->all(); Craft::$app->getElements()->restoreElements($categories); } // Fire an 'afterSaveGroup' event if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_GROUP)) { $this->trigger(self::EVENT_AFTER_SAVE_GROUP, new CategoryGroupEvent([ 'categoryGroup' => $this->getGroupById($groupRecord->id), 'isNew' => $isNewCategoryGroup, ])); } // Invalidate category caches Craft::$app->getElements()->invalidateCachesForElementType(Category::class); } /** * Deletes a category group by its ID. * * @param int $groupId The category group's ID * @return bool Whether the category group was deleted successfully * @throws Throwable if reasons * @since 3.0.12 */ public function deleteGroupById(int $groupId): bool { if (!$groupId) { return false; } $group = $this->getGroupById($groupId); if (!$group) { return false; } return $this->deleteGroup($group); } /** * Deletes a category group. * * @param CategoryGroup $group The category group * @return bool Whether the category group was deleted successfully */ public function deleteGroup(CategoryGroup $group): bool { // Fire a 'beforeDeleteGroup' event if ($this->hasEventHandlers(self::EVENT_BEFORE_DELETE_GROUP)) { $this->trigger(self::EVENT_BEFORE_DELETE_GROUP, new CategoryGroupEvent([ 'categoryGroup' => $group, ])); } Craft::$app->getProjectConfig()->remove(ProjectConfig::PATH_CATEGORY_GROUPS . '.' . $group->uid, "Delete category group “{$group->handle}”"); return true; } /** * Returns whether a group’s categories have URLs for the given site ID, and if the group’s template path is valid. * * @param CategoryGroup $group * @param int $siteId * @return bool */ public function isGroupTemplateValid(CategoryGroup $group, int $siteId): bool { $categoryGroupSiteSettings = $group->getSiteSettings(); if (!isset($categoryGroupSiteSettings[$siteId]) || !$categoryGroupSiteSettings[$siteId]->hasUrls) { return false; } $template = (string)$categoryGroupSiteSettings[$siteId]->template; return Craft::$app->getView()->doesTemplateExist($template, View::TEMPLATE_MODE_SITE); } /** * Handle Category group getting deleted * * @param ConfigEvent $event */ public function handleDeletedCategoryGroup(ConfigEvent $event): void { $uid = $event->tokenMatches[0]; $categoryGroupRecord = $this->_getCategoryGroupRecord($uid); if (!$categoryGroupRecord->id) { return; } /** @var CategoryGroup $group */ $group = $this->getGroupById($categoryGroupRecord->id); // Fire a 'beforeApplyGroupDelete' event if ($this->hasEventHandlers(self::EVENT_BEFORE_APPLY_GROUP_DELETE)) { $this->trigger(self::EVENT_BEFORE_APPLY_GROUP_DELETE, new CategoryGroupEvent([ 'categoryGroup' => $group, ])); } $transaction = Craft::$app->getDb()->beginTransaction(); try { // Delete the categories $elementsTable = Table::ELEMENTS; $categoriesTable = Table::CATEGORIES; $now = Db::prepareDateForDb(new DateTime()); $db = Craft::$app->getDb(); $conditionSql = <<<SQL [[categories.groupId]] = $group->id AND [[categories.id]] = [[elements.id]] AND [[elements.canonicalId]] IS NULL AND [[elements.revisionId]] IS NULL AND [[elements.dateDeleted]] IS NULL SQL; if ($db->getIsMysql()) { $db->createCommand(<<<SQL UPDATE $elementsTable [[elements]], $categoriesTable [[categories]] SET [[elements.dateDeleted]] = '$now', [[categories.deletedWithGroup]] = 1 WHERE $conditionSql SQL)->execute(); } else { // Not possible to update two tables simultaneously with Postgres $db->createCommand(<<<SQL UPDATE $categoriesTable [[categories]] SET [[deletedWithGroup]] = TRUE FROM $elementsTable [[elements]] WHERE $conditionSql SQL)->execute(); $db->createCommand(<<<SQL UPDATE $elementsTable [[elements]] SET [[dateDeleted]] = '$now' FROM $categoriesTable [[categories]] WHERE $conditionSql SQL)->execute(); } // Delete the structure Craft::$app->getStructures()->deleteStructureById($categoryGroupRecord->structureId); // Delete the field layout if ($categoryGroupRecord->fieldLayoutId) { Craft::$app->getFields()->deleteLayoutById($categoryGroupRecord->fieldLayoutId); } // Delete the category group Craft::$app->getDb()->createCommand() ->softDelete(Table::CATEGORYGROUPS, ['id' => $categoryGroupRecord->id]) ->execute(); $transaction->commit(); } catch (Throwable $e) { $transaction->rollBack(); throw $e; } // Clear caches $this->_groups = null; // Fire an 'afterDeleteGroup' event if ($this->hasEventHandlers(self::EVENT_AFTER_DELETE_GROUP)) { $this->trigger(self::EVENT_AFTER_DELETE_GROUP, new CategoryGroupEvent([ 'categoryGroup' => $group, ])); } // Invalidate category caches Craft::$app->getElements()->invalidateCachesForElementType(Category::class); } /** * @deprecated in 4.0.5. Unused fields will be pruned automatically as field layouts are resaved. */ public function pruneDeletedField(): void { } /** * Prune a deleted site from category group site settings. * * @param DeleteSiteEvent $event */ public function pruneDeletedSite(DeleteSiteEvent $event): void { $siteUid = $event->site->uid; $projectConfig = Craft::$app->getProjectConfig(); $categoryGroups = $projectConfig->get(ProjectConfig::PATH_CATEGORY_GROUPS); // Loop through the category groups and prune the UID from field layouts. if (is_array($categoryGroups)) { foreach ($categoryGroups as $categoryGroupUid => $categoryGroup) { $projectConfig->remove(ProjectConfig::PATH_CATEGORY_GROUPS . '.' . $categoryGroupUid . '.siteSettings.' . $siteUid, 'Prune deleted site settings'); } } } // Categories // ------------------------------------------------------------------------- /** * Returns a category by its ID. * * @param int $categoryId * @param int|int[]|string|null $siteId * @param array $criteria * @return Category|null */ public function getCategoryById(int $categoryId, mixed $siteId = null, array $criteria = []): ?Category { if (!$categoryId) { return null; } // Get the structure ID if (!isset($criteria['structureId'])) { $criteria['structureId'] = (new Query()) ->select(['categorygroups.structureId']) ->from(['categories' => Table::CATEGORIES]) ->innerJoin(['categorygroups' => Table::CATEGORYGROUPS], '[[categorygroups.id]] = [[categories.groupId]]') ->where(['categories.id' => $categoryId]) ->scalar(); } // All categories are part of a structure if (!$criteria['structureId']) { return null; } return Craft::$app->getElements()->getElementById($categoryId, Category::class, $siteId, $criteria); } /** * Patches an array of categories, filling in any gaps in the tree. * * @param Category[] $categories * @deprecated in 3.6.0. Use [[\craft\services\Structures::fillGapsInElements()]] instead. */ public function fillGapsInCategories(array &$categories): void { Craft::$app->getStructures()->fillGapsInElements($categories); } /** * Filters an array of categories down to only <= X branches. * * @param Category[] $categories * @param int $branchLimit * @deprecated in 3.6.0. Use [[\craft\services\Structures::applyBranchLimitToElements()]] instead. */ public function applyBranchLimitToCategories(array &$categories, int $branchLimit): void { Craft::$app->getStructures()->applyBranchLimitToElements($categories, $branchLimit); } /** * Creates a CategoryGroup with attributes from a CategoryGroupRecord. * * @param CategoryGroupRecord|null $groupRecord * @return CategoryGroup|null */ private function _createCategoryGroupFromRecord(?CategoryGroupRecord $groupRecord = null): ?CategoryGroup { if (!$groupRecord) { return null; } $group = new CategoryGroup($groupRecord->toArray([ 'id', 'structureId', 'fieldLayoutId', 'name', 'handle', 'defaultPlacement', 'dateDeleted', 'uid', ])); if ($groupRecord->structure) { $group->maxLevels = $groupRecord->structure->maxLevels; } return $group; } /** * Gets a category group's record by uid. * * @param string $uid * @param bool $withTrashed Whether to include trashed category groups in search * @return CategoryGroupRecord */ private function _getCategoryGroupRecord(string $uid, bool $withTrashed = false): CategoryGroupRecord { $query = $withTrashed ? CategoryGroupRecord::findWithTrashed() : CategoryGroupRecord::find(); $query->andWhere(['uid' => $uid]); /** @noinspection PhpIncompatibleReturnTypeInspection */ /** @var CategoryGroupRecord */ return $query->one() ?? new CategoryGroupRecord(); } }
/srv/users/craft4/apps/craft4-newsite-space/./vendor/./craftcms/cms/src/./services/Categories.php