3 namespace Drupal\Core\Config;
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Core\Cache\CacheBackendInterface;
7 use Drupal\Core\Config\Schema\ConfigSchemaAlterException;
8 use Drupal\Core\Config\Schema\ConfigSchemaDiscovery;
9 use Drupal\Core\DependencyInjection\ClassResolverInterface;
10 use Drupal\Core\Config\Schema\Undefined;
11 use Drupal\Core\Extension\ModuleHandlerInterface;
12 use Drupal\Core\TypedData\TypedDataManager;
15 * Manages config schema type plugins.
17 class TypedConfigManager extends TypedDataManager implements TypedConfigManagerInterface {
20 * A storage instance for reading configuration data.
22 * @var \Drupal\Core\Config\StorageInterface
24 protected $configStorage;
27 * A storage instance for reading configuration schema data.
29 * @var \Drupal\Core\Config\StorageInterface
31 protected $schemaStorage;
34 * The array of plugin definitions, keyed by plugin id.
38 protected $definitions;
41 * Creates a new typed configuration manager.
43 * @param \Drupal\Core\Config\StorageInterface $configStorage
44 * The storage object to use for reading schema data
45 * @param \Drupal\Core\Config\StorageInterface $schemaStorage
46 * The storage object to use for reading schema data
47 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
48 * The cache backend to use for caching the definitions.
49 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
51 * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
52 * (optional) The class resolver.
54 public function __construct(StorageInterface $configStorage, StorageInterface $schemaStorage, CacheBackendInterface $cache, ModuleHandlerInterface $module_handler, ClassResolverInterface $class_resolver = NULL) {
55 $this->configStorage = $configStorage;
56 $this->schemaStorage = $schemaStorage;
57 $this->setCacheBackend($cache, 'typed_config_definitions');
58 $this->alterInfo('config_schema_info');
59 $this->moduleHandler = $module_handler;
60 $this->classResolver = $class_resolver ?: \Drupal::service('class_resolver');
66 protected function getDiscovery() {
67 if (!isset($this->discovery)) {
68 $this->discovery = new ConfigSchemaDiscovery($this->schemaStorage);
70 return $this->discovery;
76 public function get($name) {
77 $data = $this->configStorage->read($name);
78 return $this->createFromNameAndData($name, $data);
84 public function buildDataDefinition(array $definition, $value, $name = NULL, $parent = NULL) {
85 // Add default values for data type and replace variables.
86 $definition += ['type' => 'undefined'];
89 $type = $definition['type'];
90 if (strpos($type, ']')) {
91 // Replace variable names in definition.
92 $replace = is_array($value) ? $value : [];
94 $replace['%parent'] = $parent;
97 $replace['%key'] = $name;
99 $type = $this->replaceName($type, $replace);
100 // Remove the type from the definition so that it is replaced with the
101 // concrete type from schema definitions.
102 unset($definition['type']);
104 // Add default values from type definition.
105 $definition += $this->getDefinitionWithReplacements($type, $replace);
107 $data_definition = $this->createDataDefinition($definition['type']);
109 // Pass remaining values from definition array to data definition.
110 foreach ($definition as $key => $value) {
111 if (!isset($data_definition[$key])) {
112 $data_definition[$key] = $value;
115 return $data_definition;
119 * Determines the typed config type for a plugin ID.
121 * @param string $base_plugin_id
123 * @param array $definitions
124 * An array of typed config definitions.
127 * The typed config type for the given plugin ID.
129 protected function determineType($base_plugin_id, array $definitions) {
130 if (isset($definitions[$base_plugin_id])) {
131 $type = $base_plugin_id;
133 elseif (strpos($base_plugin_id, '.') && $name = $this->getFallbackName($base_plugin_id)) {
134 // Found a generic name, replacing the last element by '*'.
138 // If we don't have definition, return the 'undefined' element.
145 * Gets a schema definition with replacements for dynamic names.
147 * @param string $base_plugin_id
149 * @param array $replacements
150 * An array of replacements for dynamic type names.
151 * @param bool $exception_on_invalid
152 * (optional) This parameter is passed along to self::getDefinition().
153 * However, self::getDefinition() does not respect this parameter, so it is
154 * effectively useless in this context.
157 * A schema definition array.
159 protected function getDefinitionWithReplacements($base_plugin_id, array $replacements, $exception_on_invalid = TRUE) {
160 $definitions = $this->getDefinitions();
161 $type = $this->determineType($base_plugin_id, $definitions);
162 $definition = $definitions[$type];
163 // Check whether this type is an extension of another one and compile it.
164 if (isset($definition['type'])) {
165 $merge = $this->getDefinition($definition['type'], $exception_on_invalid);
166 // Preserve integer keys on merge, so sequence item types can override
167 // parent settings as opposed to adding unused second, third, etc. items.
168 $definition = NestedArray::mergeDeepArray([$merge, $definition], TRUE);
170 // Replace dynamic portions of the definition type.
171 if (!empty($replacements) && strpos($definition['type'], ']')) {
172 $sub_type = $this->determineType($this->replaceName($definition['type'], $replacements), $definitions);
173 $sub_definition = $definitions[$sub_type];
174 if (isset($definitions[$sub_type]['type'])) {
175 $sub_merge = $this->getDefinition($definitions[$sub_type]['type'], $exception_on_invalid);
176 $sub_definition = NestedArray::mergeDeepArray([$sub_merge, $definitions[$sub_type]], TRUE);
178 // Merge the newly determined subtype definition with the original
180 $definition = NestedArray::mergeDeepArray([$sub_definition, $definition], TRUE);
181 $type = "$type||$sub_type";
183 // Unset type so we try the merge only once per type.
184 unset($definition['type']);
185 $this->definitions[$type] = $definition;
187 // Add type and default definition class.
189 'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
191 'unwrap_for_canonical_representation' => TRUE,
199 public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE) {
200 return $this->getDefinitionWithReplacements($base_plugin_id, [], $exception_on_invalid);
206 public function clearCachedDefinitions() {
207 $this->schemaStorage->reset();
208 parent::clearCachedDefinitions();
212 * Gets fallback configuration schema name.
214 * @param string $name
215 * Configuration name or key.
217 * @return null|string
218 * The resolved schema name for the given configuration name or key. Returns
219 * null if there is no schema name to fallback to. For example,
220 * breakpoint.breakpoint.module.toolbar.narrow will check for definitions in
221 * the following order:
222 * breakpoint.breakpoint.module.toolbar.*
223 * breakpoint.breakpoint.module.*.*
224 * breakpoint.breakpoint.module.*
225 * breakpoint.breakpoint.*.*.*
226 * breakpoint.breakpoint.*
229 * Colons are also used, for example,
230 * block.settings.system_menu_block:footer will check for definitions in the
232 * block.settings.system_menu_block:*
238 protected function getFallbackName($name) {
239 // Check for definition of $name with filesystem marker.
240 $replaced = preg_replace('/([^\.:]+)([\.:\*]*)$/', '*\2', $name);
241 if ($replaced != $name) {
242 if (isset($this->definitions[$replaced])) {
246 // No definition for this level. Collapse multiple wildcards to a single
247 // wildcard to see if there is a greedy match. For example,
248 // breakpoint.breakpoint.*.* becomes
249 // breakpoint.breakpoint.*
250 $one_star = preg_replace('/\.([:\.\*]*)$/', '.*', $replaced);
251 if ($one_star != $replaced && isset($this->definitions[$one_star])) {
254 // Check for next level. For example, if breakpoint.breakpoint.* has
255 // been checked and no match found then check breakpoint.*.*
256 return $this->getFallbackName($replaced);
262 * Replaces variables in configuration name.
264 * The configuration name may contain one or more variables to be replaced,
265 * enclosed in square brackets like '[name]' and will follow the replacement
266 * rules defined by the replaceVariable() method.
268 * @param string $name
269 * Configuration name with variables in square brackets.
271 * Configuration data for the element.
273 * Configuration name with variables replaced.
275 protected function replaceName($name, $data) {
276 if (preg_match_all("/\[(.*)\]/U", $name, $matches)) {
277 // Build our list of '[value]' => replacement.
279 foreach (array_combine($matches[0], $matches[1]) as $key => $value) {
280 $replace[$key] = $this->replaceVariable($value, $data);
282 return strtr($name, $replace);
290 * Replaces variable values in included names with configuration data.
292 * Variable values are nested configuration keys that will be replaced by
293 * their value or some of these special strings:
294 * - '%key', will be replaced by the element's key.
295 * - '%parent', to reference the parent element.
296 * - '%type', to reference the schema definition type. Can only be used in
297 * combination with %parent.
299 * There may be nested configuration keys separated by dots or more complex
300 * patterns like '%parent.name' which references the 'name' value of the
304 * - 'name.subkey', indicates a nested value of the current element.
305 * - '%parent.name', will be replaced by the 'name' value of the parent.
306 * - '%parent.%key', will be replaced by the parent element's key.
307 * - '%parent.%type', will be replaced by the schema type of the parent.
308 * - '%parent.%parent.%type', will be replaced by the schema type of the
311 * @param string $value
312 * Variable value to be replaced.
314 * Configuration data for the element.
317 * The replaced value if a replacement found or the original value if not.
319 protected function replaceVariable($value, $data) {
320 $parts = explode('.', $value);
321 // Process each value part, one at a time.
322 while ($name = array_shift($parts)) {
323 if (!is_array($data) || !isset($data[$name])) {
324 // Key not found, return original value
328 $value = $data[$name];
329 if (is_bool($value)) {
330 $value = (int) $value;
332 // If no more parts left, this is the final property.
333 return (string) $value;
336 // Get nested value and continue processing.
337 if ($name == '%parent') {
338 /** @var \Drupal\Core\Config\Schema\ArrayElement $parent */
339 // Switch replacement values with values from the parent.
340 $parent = $data['%parent'];
341 $data = $parent->getValue();
342 $data['%type'] = $parent->getDataDefinition()->getDataType();
343 // The special %parent and %key values now need to point one level up.
344 if ($new_parent = $parent->getParent()) {
345 $data['%parent'] = $new_parent;
346 $data['%key'] = $new_parent->getName();
350 $data = $data[$name];
359 public function hasConfigSchema($name) {
360 // The schema system falls back on the Undefined class for unknown types.
361 $definition = $this->getDefinition($name);
362 return is_array($definition) && ($definition['class'] != Undefined::class);
368 protected function alterDefinitions(&$definitions) {
369 $discovered_schema = array_keys($definitions);
370 parent::alterDefinitions($definitions);
371 $altered_schema = array_keys($definitions);
372 if ($discovered_schema != $altered_schema) {
373 $added_keys = implode(',', array_diff($altered_schema, $discovered_schema));
374 $removed_keys = implode(',', array_diff($discovered_schema, $altered_schema));
375 if (!empty($added_keys) && !empty($removed_keys)) {
376 $message = "Invoking hook_config_schema_info_alter() has added ($added_keys) and removed ($removed_keys) schema definitions";
378 elseif (!empty($added_keys)) {
379 $message = "Invoking hook_config_schema_info_alter() has added ($added_keys) schema definitions";
382 $message = "Invoking hook_config_schema_info_alter() has removed ($removed_keys) schema definitions";
384 throw new ConfigSchemaAlterException($message);
391 public function createFromNameAndData($config_name, array $config_data) {
392 $definition = $this->getDefinition($config_name);
393 $data_definition = $this->buildDataDefinition($definition, $config_data);
394 return $this->create($data_definition, $config_data);