diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 7b795d72cfffbe4a70237cee5a789c536bb41203..fe660625d190438992a01ca036f95d6e57917a2c 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1323,3 +1323,18 @@ function node_comment_delete($comment) { function node_config_translation_info_alter(&$info) { $info['node_type']['class'] = 'Drupal\node\ConfigTranslation\NodeTypeMapper'; } + +/** + * Implements hook_ENTITY_TYPE_presave(). + */ +function node_node_type_presave(NodeTypeInterface $node_type) { + // Content types' `help` and `description` fields must be stored as NULL + // at the config level if they are empty. + // @see node_post_update_set_node_type_description_and_help_to_null() + if (trim($node_type->getDescription()) === '') { + $node_type->set('description', NULL); + } + if (trim($node_type->getHelp()) === '') { + $node_type->set('help', NULL); + } +} diff --git a/core/modules/node/node.post_update.php b/core/modules/node/node.post_update.php index c02da56a16a0a08fb56f5cf8c73011f234938450..47894ee83a4d4d31879685007d3dc16947cdd13a 100644 --- a/core/modules/node/node.post_update.php +++ b/core/modules/node/node.post_update.php @@ -14,15 +14,8 @@ function node_post_update_set_node_type_description_and_help_to_null(array &$sandbox): void { \Drupal::classResolver(ConfigEntityUpdater::class) ->update($sandbox, 'node_type', function (NodeTypeInterface $node_type): bool { - // Content types' `help` and `description` fields must be stored as NULL - // at the config level if they are empty. - if (trim($node_type->getDescription()) === '') { - $node_type->set('description', NULL); - } - if (trim($node_type->getHelp()) === '') { - $node_type->set('help', NULL); - } - return TRUE; + // @see node_node_type_presave() + return trim($node_type->getDescription()) === '' || trim($node_type->getHelp()) === ''; }); } diff --git a/core/modules/taxonomy/config/schema/taxonomy.schema.yml b/core/modules/taxonomy/config/schema/taxonomy.schema.yml index 5fe96fce454d922c0c840f140521f3665bd0d9e8..4b4cbc0a22cd1107d6dc20484bafedc3fcf635b9 100644 --- a/core/modules/taxonomy/config/schema/taxonomy.schema.yml +++ b/core/modules/taxonomy/config/schema/taxonomy.schema.yml @@ -22,6 +22,8 @@ taxonomy.settings: taxonomy.vocabulary.*: type: config_entity label: 'Vocabulary' + constraints: + FullyValidatable: ~ mapping: name: type: required_label @@ -35,11 +37,18 @@ taxonomy.vocabulary.*: Length: max: 32 description: - type: label + type: text label: 'Description' + nullable: true + constraints: + NotBlank: + allowNull: true weight: type: integer label: 'Weight' + # A weight can be any integer, positive or negative. + constraints: + NotNull: [] new_revision: type: boolean label: 'Whether a new revision should be created by default' diff --git a/core/modules/taxonomy/src/Entity/Vocabulary.php b/core/modules/taxonomy/src/Entity/Vocabulary.php index 606bdb011a1780e7c6bfdc27dbffed52b8b58821..790a3744c8e1ac7e8d526f3f01e7ecafe789ebb1 100644 --- a/core/modules/taxonomy/src/Entity/Vocabulary.php +++ b/core/modules/taxonomy/src/Entity/Vocabulary.php @@ -80,9 +80,9 @@ class Vocabulary extends ConfigEntityBundleBase implements VocabularyInterface { /** * Description of the vocabulary. * - * @var string + * @var string|null */ - protected $description; + protected $description = NULL; /** * The weight of this vocabulary in relation to other vocabularies. @@ -102,7 +102,7 @@ public function id() { * {@inheritdoc} */ public function getDescription() { - return $this->description; + return $this->description ?? ''; } /** diff --git a/core/modules/taxonomy/src/Plugin/migrate/destination/EntityTaxonomyVocabulary.php b/core/modules/taxonomy/src/Plugin/migrate/destination/EntityTaxonomyVocabulary.php new file mode 100644 index 0000000000000000000000000000000000000000..20990337acf410eff6175de9d4b9b77891a3ed06 --- /dev/null +++ b/core/modules/taxonomy/src/Plugin/migrate/destination/EntityTaxonomyVocabulary.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\taxonomy\Plugin\migrate\destination; + +use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase; +use Drupal\migrate\Row; + +/** + * @MigrateDestination( + * id = "entity:taxonomy_vocabulary" + * ) + */ +class EntityTaxonomyVocabulary extends EntityConfigBase { + + /** + * {@inheritdoc} + */ + public function getEntity(Row $row, array $old_destination_id_values) { + /** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */ + $vocabulary = parent::getEntity($row, $old_destination_id_values); + + // Config schema does not allow description to be empty. + if (trim($vocabulary->getDescription()) === '') { + $vocabulary->set('description', NULL); + } + return $vocabulary; + } + +} diff --git a/core/modules/taxonomy/src/VocabularyForm.php b/core/modules/taxonomy/src/VocabularyForm.php index c91bf953c89960d68f974b00095b1a1f83cb4787..b822a34c0e75c2fca2690f086a5c30ed9db1ddb0 100644 --- a/core/modules/taxonomy/src/VocabularyForm.php +++ b/core/modules/taxonomy/src/VocabularyForm.php @@ -42,6 +42,21 @@ public static function create(ContainerInterface $container) { ); } + /** + * {@inheritdoc} + */ + public function buildEntity(array $form, FormStateInterface $form_state) { + /** @var \Drupal\taxonomy\VocabularyInterface $entity */ + $entity = parent::buildEntity($form, $form_state); + + // The description cannot be an empty string. + if (trim($form_state->getValue('description')) === '') { + $entity->set('description', NULL); + } + + return $entity; + } + /** * {@inheritdoc} */ diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index d76ef3f103588c6594ab3ff5fc930acc03e90817..787263ca20a68e7754a375fe2955b57ee506d97f 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -11,6 +11,7 @@ use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; use Drupal\taxonomy\Entity\Term; +use Drupal\taxonomy\VocabularyInterface; /** * Implements hook_help(). @@ -279,3 +280,15 @@ function taxonomy_taxonomy_term_delete(Term $term) { /** * @} End of "defgroup taxonomy_index". */ + +/** + * Implements hook_ENTITY_TYPE_presave(). + */ +function taxonomy_taxonomy_vocabulary_presave(VocabularyInterface $vocabulary) { + // Vocabularies' `description` field must be stored as NULL at the config + // level if it is empty. + // @see taxonomy_post_update_set_vocabulary_description_to_null() + if (trim($vocabulary->getDescription()) === '') { + $vocabulary->set('description', NULL); + } +} diff --git a/core/modules/taxonomy/taxonomy.post_update.php b/core/modules/taxonomy/taxonomy.post_update.php index 0a8cc89090b9dccff0f90b8537fbc2220d8b5040..f80db2876d960f8a2b7e33f263759100e29975e4 100644 --- a/core/modules/taxonomy/taxonomy.post_update.php +++ b/core/modules/taxonomy/taxonomy.post_update.php @@ -6,6 +6,7 @@ */ use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\taxonomy\VocabularyInterface; /** * Implements hook_removed_post_updates(). @@ -31,3 +32,14 @@ function taxonomy_post_update_set_new_revision(&$sandbox = NULL) { return TRUE; }); } + +/** + * Converts empty `description` in vocabularies to NULL. + */ +function taxonomy_post_update_set_vocabulary_description_to_null(array &$sandbox): void { + \Drupal::classResolver(ConfigEntityUpdater::class) + ->update($sandbox, 'taxonomy_vocabulary', function (VocabularyInterface $vocabulary): bool { + // @see taxonomy_taxonomy_vocabulary_presave() + return trim($vocabulary->getDescription()) === ''; + }); +} diff --git a/core/modules/taxonomy/tests/fixtures/update/remove-description-from-tags-vocabulary.php b/core/modules/taxonomy/tests/fixtures/update/remove-description-from-tags-vocabulary.php new file mode 100644 index 0000000000000000000000000000000000000000..6cf86e877d7dd2a41ce647b9e0c664946edbd13b --- /dev/null +++ b/core/modules/taxonomy/tests/fixtures/update/remove-description-from-tags-vocabulary.php @@ -0,0 +1,24 @@ +<?php + +/** + * @file + * Empties the description of the `tags` vocabulary. + */ + +use Drupal\Core\Database\Database; + +$connection = Database::getConnection(); + +$data = $connection->select('config') + ->condition('name', 'taxonomy.vocabulary.tags') + ->fields('config', ['data']) + ->execute() + ->fetchField(); +$data = unserialize($data); +$data['description'] = "\n"; +$connection->update('config') + ->condition('name', 'taxonomy.vocabulary.tags') + ->fields([ + 'data' => serialize($data), + ]) + ->execute(); diff --git a/core/modules/taxonomy/tests/src/Functional/Update/NullDescriptionTest.php b/core/modules/taxonomy/tests/src/Functional/Update/NullDescriptionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..391ee7e7ae7b5fc106fe568c2aab7422b6a744b6 --- /dev/null +++ b/core/modules/taxonomy/tests/src/Functional/Update/NullDescriptionTest.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\taxonomy\Functional\Update; + +use Drupal\FunctionalTests\Update\UpdatePathTestBase; +use Drupal\taxonomy\Entity\Vocabulary; + +/** + * Tests the upgrade path for making vocabularies' description NULL. + * + * @group taxonomy + * @see taxonomy_post_update_set_vocabulary_description_to_null() + */ +class NullDescriptionTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.filled.standard.php.gz', + __DIR__ . '/../../../fixtures/update/remove-description-from-tags-vocabulary.php', + ]; + } + + /** + * Tests the upgrade path for updating empty description to NULL. + */ + public function testRunUpdates(): void { + $vocabulary = Vocabulary::load('tags'); + $this->assertInstanceOf(Vocabulary::class, $vocabulary); + + $this->assertSame("\n", $vocabulary->get('description')); + $this->runUpdates(); + + $vocabulary = Vocabulary::load('tags'); + $this->assertInstanceOf(Vocabulary::class, $vocabulary); + + $this->assertNull($vocabulary->get('description')); + $this->assertSame('', $vocabulary->getDescription()); + } + +} diff --git a/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php b/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php index 7de121d2dffe87efbf82e685cc9320634e093f8c..761802fe13f83e5f38e56dc9b0d50d101a37e0b6 100644 --- a/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php +++ b/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php @@ -12,6 +12,11 @@ */ class VocabularyValidationTest extends ConfigEntityValidationTestBase { + /** + * {@inheritdoc} + */ + protected static array $propertiesWithOptionalValues = ['description']; + /** * {@inheritdoc} */