Skip to content

Commit

Permalink
Project Config now labels UIDs in Yaml files with comments to help re…
Browse files Browse the repository at this point in the history
…adability.

Resolve #7584
  • Loading branch information
andris-sevcenko committed Feb 26, 2021
1 parent 4213ac1 commit 1dee096
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@

### Added
- GraphQL schemas now include settings that determine whether inactive elements, drafts, and/or revisions should be included in query results. ([#7590](https://github.com/craftcms/cms/issues/7590)).
- Project Config now labels UIDs in Yaml files with comments to help readability. ([#7584](https://github.com/craftcms/cms/issues/7584))
- Added `craft\gql\base\ElementArguments::getRevisionArguments()`.
- Added `craft\helpers\Gql::canQueryDrafts()`.
- Added `craft\helpers\Gql::canQueryInactiveElements()`.
Expand Down
2 changes: 1 addition & 1 deletion src/config/app.php
Expand Up @@ -4,7 +4,7 @@
'id' => 'CraftCMS',
'name' => 'Craft CMS',
'version' => '3.6.7',
'schemaVersion' => '3.6.5',
'schemaVersion' => '3.6.6',
'minVersionRequired' => '2.6.2788',
'basePath' => dirname(__DIR__), // Defines the @app alias
'runtimePath' => '@storage/runtime', // Defines the @runtime alias
Expand Down
2 changes: 2 additions & 0 deletions src/db/Table.php
Expand Up @@ -66,6 +66,8 @@ abstract class Table
const PLUGINS = '{{%plugins}}';
/** @since 3.4.0 */
const PROJECTCONFIG = '{{%projectconfig}}';
/** @since 3.6.8 */
const PROJECTCONFIGNAMES = '{{%projectconfignames}}';
const QUEUE = '{{%queue}}';
const RELATIONS = '{{%relations}}';
const SECTIONS = '{{%sections}}';
Expand Down
5 changes: 5 additions & 0 deletions src/migrations/Install.php
Expand Up @@ -481,6 +481,11 @@ public function createTables()
'value' => $this->text()->notNull(),
'PRIMARY KEY([[path]])',
]);
$this->createTable(Table::PROJECTCONFIGNAMES, [
'uid' => $this->uid()->notNull(),
'name' => $this->string()->notNull(),
'PRIMARY KEY([[uid]])',
]);
$this->createTable(Table::QUEUE, [
'id' => $this->primaryKey(),
'channel' => $this->string()->notNull()->defaultValue('queue'),
Expand Down
34 changes: 34 additions & 0 deletions src/migrations/m210224_162000_add_projectconfignames_table.php
@@ -0,0 +1,34 @@
<?php

namespace craft\migrations;

use Craft;
use craft\db\Migration;
use craft\db\Table;

/**
* m210224_162000_add_projectconfignames_table migration.
*/
class m210224_162000_add_projectconfignames_table extends Migration
{
/**
* @inheritdoc
*/
public function safeUp()
{
$this->createTable(Table::PROJECTCONFIGNAMES, [
'uid' => $this->uid()->notNull(),
'name' => $this->string()->notNull(),
'PRIMARY KEY([[uid]])',
]);
}

/**
* @inheritdoc
*/
public function safeDown()
{
echo "m210224_162000_add_projectconfignames_table cannot be reverted.\n";
return false;
}
}
152 changes: 138 additions & 14 deletions src/services/ProjectConfig.php
Expand Up @@ -362,6 +362,11 @@ class ProjectConfig extends Component
*/
private $_sortedChangeEventHandlers = [];

/**
* @var array A list of updated project config name changes.
*/
private $_projectConfigNameChanges = [];

/**
* @inheritdoc
*/
Expand All @@ -380,7 +385,7 @@ public function __construct($config = [])
*/
public function init()
{
Craft::$app->on(Application::EVENT_AFTER_REQUEST, function () {
Craft::$app->on(Application::EVENT_AFTER_REQUEST, function() {
$this->saveModifiedConfigData();
}, null, false);

Expand Down Expand Up @@ -453,12 +458,14 @@ public function get(string $path = null, $getFromYaml = false)
* @param mixed $value The config item value
* @param string|null $message The message describing changes.
* @param bool $updateTimestamp Whether the `dateModified` value should be updated, if it hasn’t been updated yet for this request
* @throws NotSupportedException if the service is set to read-only mode
* @param bool $rebuilding Whether the change should always be processed. This should only used when rebuilding.
* @throws ErrorException
* @throws Exception
* @throws NotSupportedException if the service is set to read-only mode
* @throws ServerErrorHttpException
* @throws \yii\base\InvalidConfigException
*/
public function set(string $path, $value, string $message = null, bool $updateTimestamp = true)
public function set(string $path, $value, string $message = null, bool $updateTimestamp = true, $rebuilding = false)
{
// If we haven't yet pulled in the YAML changes, then anything in there should be discarded
if (empty($this->_appliedConfig)) {
Expand All @@ -469,9 +476,9 @@ public function set(string $path, $value, string $message = null, bool $updateTi
$value = ProjectConfigHelper::cleanupConfig($value);
}

$valueChanged = false;
$valueChanged = $rebuilding;

if ($value !== $this->get($path)) {
if (!$rebuilding && $value !== $this->get($path)) {
if ($this->readOnly) {
// If we're applying yaml changes that are coming in via `project.yaml`, anyway, bail silently.
if ($this->getIsApplyingYamlChanges() && $value === $this->get($path, true)) {
Expand Down Expand Up @@ -718,6 +725,12 @@ private function _processConfigChangesInternal(string $path, bool $triggerUpdate
$newValue = $this->get($path, true);
$valueChanged = $triggerUpdate || $this->forceUpdate || $this->encodeValueAsString($oldValue) !== $this->encodeValueAsString($newValue);

if ($newValue === null && is_array($oldValue)) {
$this->_removeContainedProjectConfigNames(pathinfo($path, PATHINFO_EXTENSION), $oldValue);
} else if (is_array($newValue)) {
$this->_setContainedProjectConfigNames(pathinfo($path, PATHINFO_EXTENSION), $newValue);
}

if ($valueChanged && !$this->muteEvents) {
$event = new ConfigEvent(compact('path', 'oldValue', 'newValue'));
if ($newValue === null && $oldValue !== null) {
Expand Down Expand Up @@ -812,6 +825,8 @@ public function updateParsedConfigTimes(): bool
*/
public function saveModifiedConfigData(bool $writeYaml = null)
{
$this->_processProjectConfigNameChanges();

if ($this->_isConfigModified) {
$this->_updateConfigVersion();

Expand Down Expand Up @@ -1200,6 +1215,7 @@ private function _sortChangeEventHandlers(string $event)
public function rebuild()
{
$this->reset();
$this->_discardProjectConfigNames();

$config = $this->get();
$config['dateModified'] = DateTimeHelper::currentTimeStamp();
Expand Down Expand Up @@ -1230,19 +1246,20 @@ public function rebuild()
$readOnly = $this->readOnly;
$this->readOnly = false;

// Flush it out to yaml files first.
// Process the changes
foreach ($event->config as $path => $value) {
$this->set($path, $value, 'Project config rebuild', false, true);
}

// Flush it out to yaml files.
$this->_saveConfig($event->config);
$this->_updateConfigVersion();

if ($this->writeYamlAutomatically) {
$this->_processProjectConfigNameChanges();
$this->_updateYamlFiles();
}

// Now we can process the changes
foreach ($event->config as $path => $value) {
$this->set($path, $value, 'Project config rebuild');
}

// And now ensure that Project Config doesn't attempt to save to yaml files again
$this->_isConfigModified = false;
$this->_updateInternalConfig = true;
Expand Down Expand Up @@ -1450,7 +1467,7 @@ private function _getPendingChanges(array $configData = null, bool $existsOnly =
}

// Sort by number of dots to ensure deepest paths listed first
$sorter = function ($a, $b) {
$sorter = function($a, $b) {
$aDepth = substr_count($a, '.');
$bDepth = substr_count($b, '.');

Expand Down Expand Up @@ -1673,11 +1690,32 @@ private function _updateYamlFiles()
'except' => ['.*', '.*/'],
]);

$projectConfigNames = (new Query())
->select(['uid', 'name'])
->from([Table::PROJECTCONFIGNAMES])
->pairs();

$uids = [];
$replacements = [];

if (!empty($projectConfigNames)) {
foreach ($projectConfigNames as $uid => $name) {
$uids[] = '/^(.*' . preg_quote($uid) . '.*)$/mi';
$replacements[] = '$1 # ' . $name;
}
}

foreach ($config as $relativeFile => $configData) {
$configData = ProjectConfigHelper::cleanupConfig($configData);
ksort($configData);
$filePath = $basePath . DIRECTORY_SEPARATOR . $relativeFile;
FileHelper::writeToFile($filePath, Yaml::dump($configData, 20, 2));
$yamlContent = Yaml::dump($configData, 20, 2);

if (!empty($uids)) {
$yamlContent = preg_replace($uids, $replacements, $yamlContent);
}

FileHelper::writeToFile($filePath, $yamlContent);
}
} catch (\Throwable $e) {
Craft::$app->getCache()->set(self::FILE_ISSUES_CACHE_KEY, true, self::CACHE_DURATION);
Expand All @@ -1697,6 +1735,52 @@ private function _updateYamlFiles()
Craft::$app->getCache()->delete(self::FILE_ISSUES_CACHE_KEY);
}

/**
* Discard all project config names.
*
* @throws \yii\db\Exception
*/
private function _discardProjectConfigNames(): void
{
$this->_projectConfigNameChanges = [];
Db::delete(Table::PROJECTCONFIGNAMES);
}

/**
* Process any queued up project config name changes.
*
* @throws \yii\db\Exception
*/
private function _processProjectConfigNameChanges(): void
{
if (!empty($this->_projectConfigNameChanges)) {
$remove = [];
$set = [];

foreach ($this->_projectConfigNameChanges as $uid => $name) {
if ($name === null) {
$remove[] = $uid;
} else {
$set[$uid] = $name;
}
}

if (!empty($remove)) {
Db::delete(Table::PROJECTCONFIGNAMES, ['uid' => $remove]);
}

if (!empty($set)) {
Db::delete(Table::PROJECTCONFIGNAMES, ['uid' => array_keys($set)]);
array_walk($set, function(&$value, $key) {
$value = [$key, $value];
});
Db::batchInsert(Table::PROJECTCONFIGNAMES, ['uid', 'name'], $set, false);
}

$this->_projectConfigNameChanges = [];
}
}

/**
* Returns whether we have a record of issues writing out files to the project config folder.
*
Expand Down Expand Up @@ -1795,7 +1879,7 @@ private function _loadInternalConfigData()
}

// See if we can get away with using the cached data
return Craft::$app->getCache()->getOrSet(self::STORED_CACHE_KEY, function () {
return Craft::$app->getCache()->getOrSet(self::STORED_CACHE_KEY, function() {
$data = [];
// Load the project config data
$rows = $this->_createProjectConfigQuery()->orderBy('path')->pairs();
Expand Down Expand Up @@ -2124,4 +2208,44 @@ protected function encodeValueAsString($value): string
{
return Json::encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION);
}

/**
* Set all the contained project config names to the buffer.
*
* @param string $lastPathSegment
* @param array $data
*/
private function _setContainedProjectConfigNames(string $lastPathSegment, array $data): void
{
if (preg_match('/^' . StringHelper::UUID_PATTERN . '$/i', $lastPathSegment) && isset($data['name'])) {
$this->_projectConfigNameChanges[$lastPathSegment] = $data['name'];
}

foreach ($data as $key => $value) {
// Traverse further
if (is_array($value)) {
$this->_setContainedProjectConfigNames($key, $value);
}
}
}

/**
* Mark any contained project config names for removal.
*
* @param string $lastPathSegment
* @param array $data
*/
private function _removeContainedProjectConfigNames(string $lastPathSegment, array $data): void
{
if (preg_match('/^' . StringHelper::UUID_PATTERN . '$/i', $lastPathSegment)) {
$this->_projectConfigNameChanges[$lastPathSegment] = null;
}

foreach ($data as $key => $value) {
// Traverse further
if (is_array($value)) {
$this->_setContainedProjectConfigNames($key, $value);
}
}
}
}

0 comments on commit 1dee096

Please sign in to comment.