Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
481 lines (408 sloc) 14.3 KB
<?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\Field;
use craft\db\Table;
use craft\elements\Tag;
use craft\errors\TagGroupNotFoundException;
use craft\events\ConfigEvent;
use craft\events\FieldEvent;
use craft\events\TagGroupEvent;
use craft\helpers\ArrayHelper;
use craft\helpers\Db;
use craft\helpers\ProjectConfig as ProjectConfigHelper;
use craft\helpers\StringHelper;
use craft\models\FieldLayout;
use craft\models\TagGroup;
use craft\records\TagGroup as TagGroupRecord;
use yii\base\Component;
/**
* Tags service.
* An instance of the Tags service is globally accessible in Craft via [[\craft\base\ApplicationTrait::getTags()|`Craft::$app->tags`]].
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 3.0
*/
class Tags extends Component
{
// Constants
// =========================================================================
/**
* @event TagGroupEvent The event that is triggered before a tag group is saved.
*/
const EVENT_BEFORE_SAVE_GROUP = 'beforeSaveGroup';
/**
* @event TagGroupEvent The event that is triggered after a tag group is saved.
*/
const EVENT_AFTER_SAVE_GROUP = 'afterSaveGroup';
/**
* @event TagGroupEvent The event that is triggered before a tag group is deleted.
*/
const EVENT_BEFORE_DELETE_GROUP = 'beforeDeleteGroup';
/**
* @event TagGroupEvent The event that is triggered before a tag group delete is applied to the database.
*/
const EVENT_BEFORE_APPLY_GROUP_DELETE = 'beforeApplyGroupDelete';
/**
* @event TagGroupEvent The event that is triggered after a tag group is deleted.
*/
const EVENT_AFTER_DELETE_GROUP = 'afterDeleteGroup';
const CONFIG_TAGGROUP_KEY = 'tagGroups';
// Properties
// =========================================================================
/**
* @var TagGroup[]
*/
private $_tagGroups;
// Public Methods
// =========================================================================
// Tag groups
// -------------------------------------------------------------------------
/**
* Returns all of the group IDs.
*
* @return array
*/
public function getAllTagGroupIds(): array
{
return ArrayHelper::getColumn($this->getAllTagGroups(), 'id');
}
/**
* Returns all tag groups.
*
* @return TagGroup[]
*/
public function getAllTagGroups(): array
{
if ($this->_tagGroups !== null) {
return $this->_tagGroups;
}
$this->_tagGroups = [];
$records = TagGroupRecord::find()
->orderBy(['name' => SORT_ASC])
->all();
foreach ($records as $record) {
$this->_tagGroups[] = new TagGroup($record->toArray([
'id',
'name',
'handle',
'fieldLayoutId',
'uid',
]));
}
return $this->_tagGroups;
}
/**
* Gets the total number of tag groups.
*
* @return int
*/
public function getTotalTagGroups(): int
{
return count($this->getAllTagGroups());
}
/**
* Returns a group by its ID.
*
* @param int $groupId
* @return TagGroup|null
*/
public function getTagGroupById(int $groupId)
{
return ArrayHelper::firstWhere($this->getAllTagGroups(), 'id', $groupId);
}
/**
* Returns a group by its UID.
*
* @param string $groupUid
* @return TagGroup|null
*/
public function getTagGroupByUid(string $groupUid)
{
return ArrayHelper::firstWhere($this->getAllTagGroups(), 'uid', $groupUid, true);
}
/**
* Gets a group by its handle.
*
* @param string $groupHandle
* @return TagGroup|null
*/
public function getTagGroupByHandle(string $groupHandle)
{
return ArrayHelper::firstWhere($this->getAllTagGroups(), 'handle', $groupHandle, true);
}
/**
* Saves a tag group.
*
* @param TagGroup $tagGroup The tag group to be saved
* @param bool $runValidation Whether the tag group should be validated
* @return bool Whether the tag group was saved successfully
* @throws TagGroupNotFoundException if $tagGroup->id is invalid
* @throws \Throwable if reasons
*/
public function saveTagGroup(TagGroup $tagGroup, bool $runValidation = true): bool
{
$isNewTagGroup = !$tagGroup->id;
// Fire a 'beforeSaveGroup' event
if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_GROUP)) {
$this->trigger(self::EVENT_BEFORE_SAVE_GROUP, new TagGroupEvent([
'tagGroup' => $tagGroup,
'isNew' => $isNewTagGroup
]));
}
if ($runValidation && !$tagGroup->validate()) {
Craft::info('Tag group not saved due to validation error.', __METHOD__);
return false;
}
if ($isNewTagGroup) {
$tagGroup->uid = StringHelper::UUID();
} else if (!$tagGroup->uid) {
$tagGroup->uid = Db::uidById(Table::TAGGROUPS, $tagGroup->id);
}
$projectConfig = Craft::$app->getProjectConfig();
$configData = [
'name' => $tagGroup->name,
'handle' => $tagGroup->handle,
];
$fieldLayout = $tagGroup->getFieldLayout();
$fieldLayoutConfig = $fieldLayout->getConfig();
if ($fieldLayoutConfig) {
if (empty($fieldLayout->id)) {
$layoutUid = StringHelper::UUID();
$fieldLayout->uid = $layoutUid;
} else {
$layoutUid = Db::uidById(Table::FIELDLAYOUTS, $fieldLayout->id);
}
$configData['fieldLayouts'] = [
$layoutUid => $fieldLayoutConfig
];
}
$configPath = self::CONFIG_TAGGROUP_KEY . '.' . $tagGroup->uid;
$projectConfig->set($configPath, $configData);
if ($isNewTagGroup) {
$tagGroup->id = Db::idByUid(Table::TAGGROUPS, $tagGroup->uid);
}
return true;
}
/**
* Handle tag group change
*
* @param ConfigEvent $event
*/
public function handleChangedTagGroup(ConfigEvent $event)
{
$tagGroupUid = $event->tokenMatches[0];
$data = $event->newValue;
// Make sure fields are processed
ProjectConfigHelper::ensureAllFieldsProcessed();
$transaction = Craft::$app->getDb()->beginTransaction();
try {
$tagGroupRecord = $this->_getTagGroupRecord($tagGroupUid, true);
$isNewTagGroup = $tagGroupRecord->getIsNewRecord();
$tagGroupRecord->name = $data['name'];
$tagGroupRecord->handle = $data['handle'];
$tagGroupRecord->uid = $tagGroupUid;
if (!empty($data['fieldLayouts'])) {
// Save the field layout
$layout = FieldLayout::createFromConfig(reset($data['fieldLayouts']));
$layout->id = $tagGroupRecord->fieldLayoutId;
$layout->type = Tag::class;
$layout->uid = key($data['fieldLayouts']);
Craft::$app->getFields()->saveLayout($layout);
$tagGroupRecord->fieldLayoutId = $layout->id;
} else if ($tagGroupRecord->fieldLayoutId) {
// Delete the field layout
Craft::$app->getFields()->deleteLayoutById($tagGroupRecord->fieldLayoutId);
$tagGroupRecord->fieldLayoutId = null;
}
// Save the tag group
if ($wasTrashed = (bool)$tagGroupRecord->dateDeleted) {
$tagGroupRecord->restore();
} else {
$tagGroupRecord->save(false);
}
$transaction->commit();
} catch (\Throwable $e) {
$transaction->rollBack();
throw $e;
}
// Clear caches
$this->_tagGroups = null;
if ($wasTrashed) {
// Restore the tags that were deleted with the group
$tags = Tag::find()
->groupId($tagGroupRecord->id)
->trashed()
->andWhere(['tags.deletedWithGroup' => true])
->all();
Craft::$app->getElements()->restoreElements($tags);
}
// Fire an 'afterSaveGroup' event
if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_GROUP)) {
$this->trigger(self::EVENT_AFTER_SAVE_GROUP, new TagGroupEvent([
'tagGroup' => $this->getTagGroupById($tagGroupRecord->id),
'isNew' => $isNewTagGroup,
]));
}
}
/**
* Deletes a tag group by its ID.
*
* @param int $groupId The tag group's ID
* @return bool Whether the tag group was deleted successfully
* @throws \Throwable if reasons
*/
public function deleteTagGroupById(int $groupId): bool
{
if (!$groupId) {
return false;
}
$group = $this->getTagGroupById($groupId);
if (!$group) {
return false;
}
return $this->deleteTagGroup($group);
}
/**
* Deletes a tag group.
*
* @param TagGroup $tagGroup The tag group
* @return bool Whether the tag group was deleted successfully
* @throws \Throwable if reasons
*/
public function deleteTagGroup(TagGroup $tagGroup): bool
{
if (!$tagGroup) {
return false;
}
// Fire a 'beforeDeleteGroup' event
if ($this->hasEventHandlers(self::EVENT_BEFORE_DELETE_GROUP)) {
$this->trigger(self::EVENT_BEFORE_DELETE_GROUP, new TagGroupEvent([
'tagGroup' => $tagGroup
]));
}
Craft::$app->getProjectConfig()->remove(self::CONFIG_TAGGROUP_KEY . '.' . $tagGroup->uid);
return true;
}
/**
* Handle Tag group getting deleted
*
* @param ConfigEvent $event
*/
public function handleDeletedTagGroup(ConfigEvent $event)
{
$uid = $event->tokenMatches[0];
$tagGroupRecord = $this->_getTagGroupRecord($uid);
if (!$tagGroupRecord->id) {
return;
}
/** @var TagGroup $tagGroup */
$tagGroup = $this->getTagGroupById($tagGroupRecord->id);
// Fire a 'beforeApplyGroupDelete' event
if ($this->hasEventHandlers(self::EVENT_BEFORE_APPLY_GROUP_DELETE)) {
$this->trigger(self::EVENT_BEFORE_APPLY_GROUP_DELETE, new TagGroupEvent([
'tagGroup' => $tagGroup,
]));
}
$transaction = Craft::$app->getDb()->beginTransaction();
try {
// Delete the tags
$tags = Tag::find()
->anyStatus()
->groupId($tagGroupRecord->id)
->all();
$elementsService = Craft::$app->getElements();
foreach ($tags as $tag) {
$tag->deletedWithGroup = true;
$elementsService->deleteElement($tag);
}
// Delete the field layout
if ($tagGroupRecord->fieldLayoutId) {
Craft::$app->getFields()->deleteLayoutById($tagGroupRecord->fieldLayoutId);
}
// Delete the tag group
Craft::$app->getDb()->createCommand()
->softDelete(Table::TAGGROUPS, ['id' => $tagGroupRecord->id])
->execute();
$transaction->commit();
} catch (\Throwable $e) {
$transaction->rollBack();
throw $e;
}
// Clear caches
$this->_tagGroups = null;
// Fire an 'afterDeleteGroup' event
if ($this->hasEventHandlers(self::EVENT_AFTER_DELETE_GROUP)) {
$this->trigger(self::EVENT_AFTER_DELETE_GROUP, new TagGroupEvent([
'tagGroup' => $tagGroup
]));
}
}
/**
* Prune a deleted field from tag group layouts.
*
* @param FieldEvent $event
*/
public function pruneDeletedField(FieldEvent $event)
{
/** @var Field $field */
$field = $event->field;
$fieldUid = $field->uid;
$projectConfig = Craft::$app->getProjectConfig();
$tagGroups = $projectConfig->get(self::CONFIG_TAGGROUP_KEY);
// Engage stealth mode
$projectConfig->muteEvents = true;
// Loop through the tag groups and prune the UID from field layouts.
if (is_array($tagGroups)) {
foreach ($tagGroups as $tagGroupUid => $tagGroup) {
if (!empty($tagGroup['fieldLayouts'])) {
foreach ($tagGroup['fieldLayouts'] as $layoutUid => $layout) {
if (!empty($layout['tabs'])) {
foreach ($layout['tabs'] as $tabUid => $tab) {
$projectConfig->remove(self::CONFIG_TAGGROUP_KEY . '.' . $tagGroupUid . '.fieldLayouts.' . $layoutUid . '.tabs.' . $tabUid . '.fields.' . $fieldUid);
}
}
}
}
}
}
// Nuke all the layout fields from the DB
Craft::$app->getDb()->createCommand()->delete('{{%fieldlayoutfields}}', ['fieldId' => $field->id])->execute();
// Allow events again
$projectConfig->muteEvents = false;
}
// Tags
// -------------------------------------------------------------------------
/**
* Returns a tag by its ID.
*
* @param int $tagId
* @param int|null $siteId
* @return Tag|null
*/
public function getTagById(int $tagId, int $siteId = null)
{
/** @noinspection PhpIncompatibleReturnTypeInspection */
return Craft::$app->getElements()->getElementById($tagId, Tag::class, $siteId);
}
// Private methods
// =========================================================================
/**
* Gets a tag group's record by uid.
*
* @param string $uid
* @param bool $withTrashed Whether to include trashed tag groups in search
* @return TagGroupRecord
*/
private function _getTagGroupRecord(string $uid, bool $withTrashed = false): TagGroupRecord
{
$query = $withTrashed ? TagGroupRecord::findWithTrashed() : TagGroupRecord::find();
$query->andWhere(['uid' => $uid]);
return $query->one() ?? new TagGroupRecord();
}
}
You can’t perform that action at this time.