22795ad6fffdc4554a568de3ffa25cd1a4c4620d
[yaffs-website] / web / core / lib / Drupal / Core / Config / TypedConfigManager.php
1 <?php
2
3 namespace Drupal\Core\Config;
4
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\Config\Schema\Undefined;
10 use Drupal\Core\Extension\ModuleHandlerInterface;
11 use Drupal\Core\TypedData\TypedDataManager;
12
13 /**
14  * Manages config schema type plugins.
15  */
16 class TypedConfigManager extends TypedDataManager implements TypedConfigManagerInterface {
17
18   /**
19    * A storage instance for reading configuration data.
20    *
21    * @var \Drupal\Core\Config\StorageInterface
22    */
23   protected $configStorage;
24
25   /**
26    * A storage instance for reading configuration schema data.
27    *
28    * @var \Drupal\Core\Config\StorageInterface
29    */
30   protected $schemaStorage;
31
32   /**
33    * The array of plugin definitions, keyed by plugin id.
34    *
35    * @var array
36    */
37   protected $definitions;
38
39   /**
40    * Creates a new typed configuration manager.
41    *
42    * @param \Drupal\Core\Config\StorageInterface $configStorage
43    *   The storage object to use for reading schema data
44    * @param \Drupal\Core\Config\StorageInterface $schemaStorage
45    *   The storage object to use for reading schema data
46    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
47    *   The cache backend to use for caching the definitions.
48    */
49   public function __construct(StorageInterface $configStorage, StorageInterface $schemaStorage, CacheBackendInterface $cache, ModuleHandlerInterface $module_handler) {
50     $this->configStorage = $configStorage;
51     $this->schemaStorage = $schemaStorage;
52     $this->setCacheBackend($cache, 'typed_config_definitions');
53     $this->alterInfo('config_schema_info');
54     $this->moduleHandler = $module_handler;
55   }
56
57   /**
58    * {@inheritdoc}
59    */
60   protected function getDiscovery() {
61     if (!isset($this->discovery)) {
62       $this->discovery = new ConfigSchemaDiscovery($this->schemaStorage);
63     }
64     return $this->discovery;
65   }
66
67   /**
68    * {@inheritdoc}
69    */
70   public function get($name) {
71     $data = $this->configStorage->read($name);
72     $type_definition = $this->getDefinition($name);
73     $data_definition = $this->buildDataDefinition($type_definition, $data);
74     return $this->create($data_definition, $data);
75   }
76
77   /**
78    * {@inheritdoc}
79    */
80   public function buildDataDefinition(array $definition, $value, $name = NULL, $parent = NULL) {
81     // Add default values for data type and replace variables.
82     $definition += ['type' => 'undefined'];
83
84     $replace = [];
85     $type = $definition['type'];
86     if (strpos($type, ']')) {
87       // Replace variable names in definition.
88       $replace = is_array($value) ? $value : [];
89       if (isset($parent)) {
90         $replace['%parent'] = $parent;
91       }
92       if (isset($name)) {
93         $replace['%key'] = $name;
94       }
95       $type = $this->replaceName($type, $replace);
96       // Remove the type from the definition so that it is replaced with the
97       // concrete type from schema definitions.
98       unset($definition['type']);
99     }
100     // Add default values from type definition.
101     $definition += $this->getDefinitionWithReplacements($type, $replace);
102
103     $data_definition = $this->createDataDefinition($definition['type']);
104
105     // Pass remaining values from definition array to data definition.
106     foreach ($definition as $key => $value) {
107       if (!isset($data_definition[$key])) {
108         $data_definition[$key] = $value;
109       }
110     }
111     return $data_definition;
112   }
113
114   /**
115    * Determines the typed config type for a plugin ID.
116    *
117    * @param string $base_plugin_id
118    *   The plugin ID.
119    * @param array $definitions
120    *   An array of typed config definitions.
121    *
122    * @return string
123    *   The typed config type for the given plugin ID.
124    */
125   protected function determineType($base_plugin_id, array $definitions) {
126     if (isset($definitions[$base_plugin_id])) {
127       $type = $base_plugin_id;
128     }
129     elseif (strpos($base_plugin_id, '.') && $name = $this->getFallbackName($base_plugin_id)) {
130       // Found a generic name, replacing the last element by '*'.
131       $type = $name;
132     }
133     else {
134       // If we don't have definition, return the 'undefined' element.
135       $type = 'undefined';
136     }
137     return $type;
138   }
139
140   /**
141    * Gets a schema definition with replacements for dynamic names.
142    *
143    * @param string $base_plugin_id
144    *   A plugin ID.
145    * @param array $replacements
146    *   An array of replacements for dynamic type names.
147    * @param bool $exception_on_invalid
148    *   (optional) This parameter is passed along to self::getDefinition().
149    *   However, self::getDefinition() does not respect this parameter, so it is
150    *   effectively useless in this context.
151    *
152    * @return array
153    *   A schema definition array.
154    */
155   protected function getDefinitionWithReplacements($base_plugin_id, array $replacements, $exception_on_invalid = TRUE) {
156     $definitions = $this->getDefinitions();
157     $type = $this->determineType($base_plugin_id, $definitions);
158     $definition = $definitions[$type];
159     // Check whether this type is an extension of another one and compile it.
160     if (isset($definition['type'])) {
161       $merge = $this->getDefinition($definition['type'], $exception_on_invalid);
162       // Preserve integer keys on merge, so sequence item types can override
163       // parent settings as opposed to adding unused second, third, etc. items.
164       $definition = NestedArray::mergeDeepArray([$merge, $definition], TRUE);
165
166       // Replace dynamic portions of the definition type.
167       if (!empty($replacements) && strpos($definition['type'], ']')) {
168         $sub_type = $this->determineType($this->replaceName($definition['type'], $replacements), $definitions);
169         $sub_definition = $definitions[$sub_type];
170         if (isset($definitions[$sub_type]['type'])) {
171           $sub_merge = $this->getDefinition($definitions[$sub_type]['type'], $exception_on_invalid);
172           $sub_definition = NestedArray::mergeDeepArray([$sub_merge, $definitions[$sub_type]], TRUE);
173         }
174         // Merge the newly determined subtype definition with the original
175         // definition.
176         $definition = NestedArray::mergeDeepArray([$sub_definition, $definition], TRUE);
177         $type = "$type||$sub_type";
178       }
179       // Unset type so we try the merge only once per type.
180       unset($definition['type']);
181       $this->definitions[$type] = $definition;
182     }
183     // Add type and default definition class.
184     $definition += [
185       'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
186       'type' => $type,
187     ];
188     return $definition;
189   }
190
191   /**
192    * {@inheritdoc}
193    */
194   public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE) {
195     return $this->getDefinitionWithReplacements($base_plugin_id, [], $exception_on_invalid);
196   }
197
198   /**
199    * {@inheritdoc}
200    */
201   public function clearCachedDefinitions() {
202     $this->schemaStorage->reset();
203     parent::clearCachedDefinitions();
204   }
205
206   /**
207    * Gets fallback configuration schema name.
208    *
209    * @param string $name
210    *   Configuration name or key.
211    *
212    * @return null|string
213    *   The resolved schema name for the given configuration name or key. Returns
214    *   null if there is no schema name to fallback to. For example,
215    *   breakpoint.breakpoint.module.toolbar.narrow will check for definitions in
216    *   the following order:
217    *     breakpoint.breakpoint.module.toolbar.*
218    *     breakpoint.breakpoint.module.*.*
219    *     breakpoint.breakpoint.module.*
220    *     breakpoint.breakpoint.*.*.*
221    *     breakpoint.breakpoint.*
222    *     breakpoint.*.*.*.*
223    *     breakpoint.*
224    *   Colons are also used, for example,
225    *   block.settings.system_menu_block:footer will check for definitions in the
226    *   following order:
227    *     block.settings.system_menu_block:*
228    *     block.settings.*:*
229    *     block.settings.*
230    *     block.*.*:*
231    *     block.*
232    */
233   protected function getFallbackName($name) {
234     // Check for definition of $name with filesystem marker.
235     $replaced = preg_replace('/([^\.:]+)([\.:\*]*)$/', '*\2', $name);
236     if ($replaced != $name) {
237       if (isset($this->definitions[$replaced])) {
238         return $replaced;
239       }
240       else {
241         // No definition for this level. Collapse multiple wildcards to a single
242         // wildcard to see if there is a greedy match. For example,
243         // breakpoint.breakpoint.*.* becomes
244         // breakpoint.breakpoint.*
245         $one_star = preg_replace('/\.([:\.\*]*)$/', '.*', $replaced);
246         if ($one_star != $replaced && isset($this->definitions[$one_star])) {
247           return $one_star;
248         }
249         // Check for next level. For example, if breakpoint.breakpoint.* has
250         // been checked and no match found then check breakpoint.*.*
251         return $this->getFallbackName($replaced);
252       }
253     }
254   }
255
256   /**
257    * Replaces variables in configuration name.
258    *
259    * The configuration name may contain one or more variables to be replaced,
260    * enclosed in square brackets like '[name]' and will follow the replacement
261    * rules defined by the replaceVariable() method.
262    *
263    * @param string $name
264    *   Configuration name with variables in square brackets.
265    * @param mixed $data
266    *   Configuration data for the element.
267    * @return string
268    *   Configuration name with variables replaced.
269    */
270   protected function replaceName($name, $data) {
271     if (preg_match_all("/\[(.*)\]/U", $name, $matches)) {
272       // Build our list of '[value]' => replacement.
273       $replace = [];
274       foreach (array_combine($matches[0], $matches[1]) as $key => $value) {
275         $replace[$key] = $this->replaceVariable($value, $data);
276       }
277       return strtr($name, $replace);
278     }
279     else {
280       return $name;
281     }
282   }
283
284   /**
285    * Replaces variable values in included names with configuration data.
286    *
287    * Variable values are nested configuration keys that will be replaced by
288    * their value or some of these special strings:
289    * - '%key', will be replaced by the element's key.
290    * - '%parent', to reference the parent element.
291    * - '%type', to reference the schema definition type. Can only be used in
292    *   combination with %parent.
293    *
294    * There may be nested configuration keys separated by dots or more complex
295    * patterns like '%parent.name' which references the 'name' value of the
296    * parent element.
297    *
298    * Example patterns:
299    * - 'name.subkey', indicates a nested value of the current element.
300    * - '%parent.name', will be replaced by the 'name' value of the parent.
301    * - '%parent.%key', will be replaced by the parent element's key.
302    * - '%parent.%type', will be replaced by the schema type of the parent.
303    * - '%parent.%parent.%type', will be replaced by the schema type of the
304    *   parent's parent.
305    *
306    * @param string $value
307    *   Variable value to be replaced.
308    * @param mixed $data
309    *   Configuration data for the element.
310    *
311    * @return string
312    *   The replaced value if a replacement found or the original value if not.
313    */
314   protected function replaceVariable($value, $data) {
315     $parts = explode('.', $value);
316     // Process each value part, one at a time.
317     while ($name = array_shift($parts)) {
318       if (!is_array($data) || !isset($data[$name])) {
319         // Key not found, return original value
320         return $value;
321       }
322       elseif (!$parts) {
323         $value = $data[$name];
324         if (is_bool($value)) {
325           $value = (int) $value;
326         }
327         // If no more parts left, this is the final property.
328         return (string) $value;
329       }
330       else {
331         // Get nested value and continue processing.
332         if ($name == '%parent') {
333           /** @var \Drupal\Core\Config\Schema\ArrayElement $parent */
334           // Switch replacement values with values from the parent.
335           $parent = $data['%parent'];
336           $data = $parent->getValue();
337           $data['%type'] = $parent->getDataDefinition()->getDataType();
338           // The special %parent and %key values now need to point one level up.
339           if ($new_parent = $parent->getParent()) {
340             $data['%parent'] = $new_parent;
341             $data['%key'] = $new_parent->getName();
342           }
343         }
344         else {
345           $data = $data[$name];
346         }
347       }
348     }
349   }
350
351   /**
352    * {@inheritdoc}
353    */
354   public function hasConfigSchema($name) {
355     // The schema system falls back on the Undefined class for unknown types.
356     $definition = $this->getDefinition($name);
357     return is_array($definition) && ($definition['class'] != Undefined::class);
358   }
359
360   /**
361    * {@inheritdoc}
362    */
363   protected function alterDefinitions(&$definitions) {
364     $discovered_schema = array_keys($definitions);
365     parent::alterDefinitions($definitions);
366     $altered_schema = array_keys($definitions);
367     if ($discovered_schema != $altered_schema) {
368       $added_keys = implode(',', array_diff($altered_schema, $discovered_schema));
369       $removed_keys = implode(',', array_diff($discovered_schema, $altered_schema));
370       if (!empty($added_keys) && !empty($removed_keys)) {
371         $message = "Invoking hook_config_schema_info_alter() has added ($added_keys) and removed ($removed_keys) schema definitions";
372       }
373       elseif (!empty($added_keys)) {
374         $message = "Invoking hook_config_schema_info_alter() has added ($added_keys) schema definitions";
375       }
376       else {
377         $message = "Invoking hook_config_schema_info_alter() has removed ($removed_keys) schema definitions";
378       }
379       throw new ConfigSchemaAlterException($message);
380     }
381   }
382
383 }