Issue #3373653 by phenaproxima, Wim Leers, borisson_, penyaskito, andypost, lauriii, smustgrave, larowlan: Add a `langcode` data type to config schema

merge-requests/4318/merge
Adam G-H 2023-07-21 21:58:09 +00:00 committed by Ben Mullins
parent b78ae9ef49
commit 6f3215938f
8 changed files with 119 additions and 43 deletions

View File

@ -104,6 +104,15 @@ machine_name:
# @see \Drupal\Core\Config\Entity\ConfigEntityStorage::MAX_ID_LENGTH
max: 166
# A language identifier.
langcode:
type: string
label: 'Language code'
constraints:
NotNull: []
Choice:
callback: 'Drupal\Core\TypedData\Plugin\DataType\LanguageReference::getAllValidLangcodes'
# Complex extended data types:
# Root of a configuration object.
@ -130,8 +139,7 @@ config_object:
_core:
type: _core_config_info
langcode:
type: string
label: 'Language code'
type: langcode
# Mail text with subject and body parts.
mail:
@ -305,8 +313,7 @@ config_entity:
type: uuid
label: 'UUID'
langcode:
type: string
label: 'Language code'
type: langcode
status:
type: boolean
label: 'Status'

View File

@ -2,6 +2,7 @@
namespace Drupal\Core\TypedData\Plugin\DataType;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\TypedData\DataReferenceBase;
/**
@ -30,4 +31,34 @@ class LanguageReference extends DataReferenceBase {
return isset($language) ? $language->id() : NULL;
}
/**
* Returns all valid values for a `langcode` config value.
*
* @return string[]
* All possible valid langcodes. This includes all langcodes in the standard
* list of human languages, along with special langcodes like `und`, `zxx`,
* and `site_default`, which Drupal uses internally. If any custom languages
* are defined, they will be included as well.
*
* @see \Drupal\Core\Language\LanguageManagerInterface::getLanguages()
* @see \Drupal\Core\Language\LanguageManagerInterface::getStandardLanguageList()
*/
public static function getAllValidLangcodes(): array {
$language_manager = \Drupal::languageManager();
return array_unique([
...array_keys($language_manager::getStandardLanguageList()),
// We can't use LanguageInterface::STATE_ALL because it will exclude the
// site default language in certain situations.
// @see \Drupal\Core\Language\LanguageManager::filterLanguages()
...array_keys($language_manager->getLanguages(LanguageInterface::STATE_LOCKED | LanguageInterface::STATE_CONFIGURABLE | LanguageInterface::STATE_SITE_DEFAULT)),
// Include special language codes used internally.
LanguageInterface::LANGCODE_NOT_APPLICABLE,
LanguageInterface::LANGCODE_SITE_DEFAULT,
LanguageInterface::LANGCODE_DEFAULT,
LanguageInterface::LANGCODE_SYSTEM,
LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
}
}

View File

@ -7,6 +7,11 @@ use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Plugin\Validation\Constraint\EmailConstraint;
use Symfony\Component\Validator\Constraints\Blank;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\Choice;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* Constraint plugin manager.
@ -87,24 +92,29 @@ class ConstraintManager extends DefaultPluginManager {
public function registerDefinitions() {
$this->getDiscovery()->setDefinition('Callback', [
'label' => new TranslatableMarkup('Callback'),
'class' => '\Symfony\Component\Validator\Constraints\Callback',
'class' => Callback::class,
'type' => FALSE,
]);
$this->getDiscovery()->setDefinition('Blank', [
'label' => new TranslatableMarkup('Blank'),
'class' => '\Symfony\Component\Validator\Constraints\Blank',
'class' => Blank::class,
'type' => FALSE,
]);
$this->getDiscovery()->setDefinition('NotBlank', [
'label' => new TranslatableMarkup('Not blank'),
'class' => '\Symfony\Component\Validator\Constraints\NotBlank',
'class' => NotBlank::class,
'type' => FALSE,
]);
$this->getDiscovery()->setDefinition('Email', [
'label' => new TranslatableMarkup('Email'),
'class' => '\Drupal\Core\Validation\Plugin\Validation\Constraint\EmailConstraint',
'class' => EmailConstraint::class,
'type' => ['string'],
]);
$this->getDiscovery()->setDefinition('Choice', [
'label' => new TranslatableMarkup('Choice'),
'class' => Choice::class,
'type' => FALSE,
]);
}
/**

View File

@ -23,7 +23,6 @@ use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\editor\EditorInterface;
use Symfony\Component\Validator\Constraints\Choice;
/**
* Implements hook_help().
@ -568,24 +567,6 @@ function ckeditor5_js_alter(&$javascript, AttachedAssetsInterface $assets, Langu
}
}
/**
* Implements hook_validation_constraint_alter().
*/
function ckeditor5_validation_constraint_alter(array &$definitions) {
// Add the Symfony validation constraints that Drupal core does not add in
// \Drupal\Core\Validation\ConstraintManager::registerDefinitions() for
// unknown reasons. Do it defensively, to not break when this changes.
if (!isset($definitions['Choice'])) {
$definitions['Choice'] = [
'label' => 'Choice',
'class' => Choice::class,
'type' => FALSE,
'provider' => 'core',
'id' => 'Choice',
];
}
}
/**
* Implements hook_config_schema_info_alter().
*/

View File

@ -71,7 +71,7 @@ language.negotiation:
type: string
label: 'Domain'
selected_langcode:
type: string
type: langcode
label: 'Selected language'
language.mappings:
@ -118,7 +118,7 @@ language.content_settings.*.*:
type: string
label: 'Bundle'
default_langcode:
type: string
type: langcode
label: 'Default language'
language_alterable:
type: boolean
@ -130,7 +130,7 @@ condition.plugin.language:
langcodes:
type: sequence
sequence:
type: string
type: langcode
field.widget.settings.language_select:
type: mapping

View File

@ -39,7 +39,7 @@ system.site:
type: integer
label: 'Weight element maximum value'
default_langcode:
type: string
type: langcode
label: 'Site default language code'
mail_notification:
type: string

View File

@ -4,7 +4,11 @@ namespace Drupal\KernelTests\Core\Config;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\TypedData\Plugin\DataType\LanguageReference;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Base class for testing validation of config entities.
@ -311,4 +315,53 @@ abstract class ConfigEntityValidationTestBase extends KernelTestBase {
$this->assertSame($expected_messages, $actual_messages);
}
/**
* Tests that the config entity's langcode is validated.
*/
public function testLangcode(): void {
$this->entity->set('langcode', NULL);
$this->assertValidationErrors([
'langcode' => 'This value should not be null.',
]);
// A langcode from the standard list should always be acceptable.
$standard_languages = LanguageManager::getStandardLanguageList();
$this->assertNotEmpty($standard_languages);
$this->entity->set('langcode', key($standard_languages));
$this->assertValidationErrors([]);
// All special, internal langcodes should be acceptable.
$system_langcodes = [
LanguageInterface::LANGCODE_NOT_SPECIFIED,
LanguageInterface::LANGCODE_NOT_APPLICABLE,
LanguageInterface::LANGCODE_DEFAULT,
LanguageInterface::LANGCODE_SITE_DEFAULT,
LanguageInterface::LANGCODE_SYSTEM,
];
foreach ($system_langcodes as $langcode) {
$this->entity->set('langcode', $langcode);
$this->assertValidationErrors([]);
}
// An invalid langcode should be unacceptable, even if it "looks" right.
$fake_langcode = 'definitely-not-a-language';
$this->assertArrayNotHasKey($fake_langcode, LanguageReference::getAllValidLangcodes());
$this->entity->set('langcode', $fake_langcode);
$this->assertValidationErrors([
'langcode' => 'The value you selected is not a valid choice.',
]);
// If a new configurable language is created with a non-standard langcode,
// it should be acceptable.
$this->enableModules(['language']);
// The language doesn't exist yet, so it shouldn't be a valid choice.
$this->entity->set('langcode', 'kthxbai');
$this->assertValidationErrors([
'langcode' => 'The value you selected is not a valid choice.',
]);
// Once we create the language, it should be a valid choice.
ConfigurableLanguage::createFromLangcode('kthxbai')->save();
$this->assertValidationErrors([]);
}
}

View File

@ -68,8 +68,7 @@ class ConfigSchemaTest extends KernelTestBase {
$expected = [];
$expected['label'] = 'Schema test data';
$expected['class'] = Mapping::class;
$expected['mapping']['langcode']['type'] = 'string';
$expected['mapping']['langcode']['label'] = 'Language code';
$expected['mapping']['langcode']['type'] = 'langcode';
$expected['mapping']['_core']['type'] = '_core_config_info';
$expected['mapping']['testitem'] = ['label' => 'Test item'];
$expected['mapping']['testlist'] = ['label' => 'Test list'];
@ -115,8 +114,7 @@ class ConfigSchemaTest extends KernelTestBase {
'type' => 'text',
];
$expected['mapping']['langcode'] = [
'label' => 'Language code',
'type' => 'string',
'type' => 'langcode',
];
$expected['mapping']['_core']['type'] = '_core_config_info';
$expected['type'] = 'system.maintenance';
@ -131,8 +129,7 @@ class ConfigSchemaTest extends KernelTestBase {
$expected['class'] = Mapping::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['mapping']['langcode'] = [
'type' => 'string',
'label' => 'Language code',
'type' => 'langcode',
];
$expected['mapping']['_core']['type'] = '_core_config_info';
$expected['mapping']['label'] = [
@ -179,8 +176,7 @@ class ConfigSchemaTest extends KernelTestBase {
$expected['mapping']['name']['type'] = 'machine_name';
$expected['mapping']['uuid']['type'] = 'uuid';
$expected['mapping']['uuid']['label'] = 'UUID';
$expected['mapping']['langcode']['type'] = 'string';
$expected['mapping']['langcode']['label'] = 'Language code';
$expected['mapping']['langcode']['type'] = 'langcode';
$expected['mapping']['status']['type'] = 'boolean';
$expected['mapping']['status']['label'] = 'Status';
$expected['mapping']['dependencies']['type'] = 'config_dependencies';
@ -247,8 +243,7 @@ class ConfigSchemaTest extends KernelTestBase {
$expected = [];
$expected['label'] = 'Schema multiple filesystem marker test';
$expected['class'] = Mapping::class;
$expected['mapping']['langcode']['type'] = 'string';
$expected['mapping']['langcode']['label'] = 'Language code';
$expected['mapping']['langcode']['type'] = 'langcode';
$expected['mapping']['_core']['type'] = '_core_config_info';
$expected['mapping']['testid']['type'] = 'string';
$expected['mapping']['testid']['label'] = 'ID';
@ -518,8 +513,7 @@ class ConfigSchemaTest extends KernelTestBase {
$expected['class'] = Mapping::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['mapping']['langcode']['type'] = 'string';
$expected['mapping']['langcode']['label'] = 'Language code';
$expected['mapping']['langcode']['type'] = 'langcode';
$expected['mapping']['_core']['type'] = '_core_config_info';
$expected['mapping']['testid']['type'] = 'string';
$expected['mapping']['testid']['label'] = 'ID';