Updated Drupal to 8.6. This goes with the following updates because it's possible...
[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\DependencyInjection\ClassResolverInterface;
10 use Drupal\Core\Config\Schema\Undefined;
11 use Drupal\Core\Extension\ModuleHandlerInterface;
12 use Drupal\Core\TypedData\TypedDataManager;
13
14 /**
15  * Manages config schema type plugins.
16  */
17 class TypedConfigManager extends TypedDataManager implements TypedConfigManagerInterface {
18
19   /**
20    * A storage instance for reading configuration data.
21    *
22    * @var \Drupal\Core\Config\StorageInterface
23    */
24   protected $configStorage;
25
26   /**
27    * A storage instance for reading configuration schema data.
28    *
29    * @var \Drupal\Core\Config\StorageInterface
30    */
31   protected $schemaStorage;
32
33   /**
34    * The array of plugin definitions, keyed by plugin id.
35    *
36    * @var array
37    */
38   protected $definitions;
39
40   /**
41    * Creates a new typed configuration manager.
42    *
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
50    *   The module handler.
51    * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
52    *   (optional) The class resolver.
53    */
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');
61   }
62
63   /**
64    * {@inheritdoc}
65    */
66   protected function getDiscovery() {
67     if (!isset($this->discovery)) {
68       $this->discovery = new ConfigSchemaDiscovery($this->schemaStorage);
69     }
70     return $this->discovery;
71   }
72
73   /**
74    * {@inheritdoc}
75    */
76   public function get($name) {
77     $data = $this->configStorage->read($name);
78     return $this->createFromNameAndData($name, $data);
79   }
80
81   /**
82    * {@inheritdoc}
83    */
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'];
87
88     $replace = [];
89     $type = $definition['type'];
90     if (strpos($type, ']')) {
91       // Replace variable names in definition.
92       $replace = is_array($value) ? $value : [];
93       if (isset($parent)) {
94         $replace['%parent'] = $parent;
95       }
96       if (isset($name)) {
97         $replace['%key'] = $name;
98       }
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']);
103     }
104     // Add default values from type definition.
105     $definition += $this->getDefinitionWithReplacements($type, $replace);
106
107     $data_definition = $this->createDataDefinition($definition['type']);
108
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;
113       }
114     }
115     return $data_definition;
116   }
117
118   /**
119    * Determines the typed config type for a plugin ID.
120    *
121    * @param string $base_plugin_id
122    *   The plugin ID.
123    * @param array $definitions
124    *   An array of typed config definitions.
125    *
126    * @return string
127    *   The typed config type for the given plugin ID.
128    */
129   protected function determineType($base_plugin_id, array $definitions) {
130     if (isset($definitions[$base_plugin_id])) {
131       $type = $base_plugin_id;
132     }
133     elseif (strpos($base_plugin_id, '.') && $name = $this->getFallbackName($base_plugin_id)) {
134       // Found a generic name, replacing the last element by '*'.
135       $type = $name;
136     }
137     else {
138       // If we don't have definition, return the 'undefined' element.
139       $type = 'undefined';
140     }
141     return $type;
142   }
143
144   /**
145    * Gets a schema definition with replacements for dynamic names.
146    *
147    * @param string $base_plugin_id
148    *   A 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.
155    *
156    * @return array
157    *   A schema definition array.
158    */
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);
169
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);
177         }
178         // Merge the newly determined subtype definition with the original
179         // definition.
180         $definition = NestedArray::mergeDeepArray([$sub_definition, $definition], TRUE);
181         $type = "$type||$sub_type";
182       }
183       // Unset type so we try the merge only once per type.
184       unset($definition['type']);
185       $this->definitions[$type] = $definition;
186     }
187     // Add type and default definition class.
188     $definition += [
189       'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
190       'type' => $type,
191       'unwrap_for_canonical_representation' => TRUE,
192     ];
193     return $definition;
194   }
195
196   /**
197    * {@inheritdoc}
198    */
199   public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE) {
200     return $this->getDefinitionWithReplacements($base_plugin_id, [], $exception_on_invalid);
201   }
202
203   /**
204    * {@inheritdoc}
205    */
206   public function clearCachedDefinitions() {
207     $this->schemaStorage->reset();
208     parent::clearCachedDefinitions();
209   }
210
211   /**
212    * Gets fallback configuration schema name.
213    *
214    * @param string $name
215    *   Configuration name or key.
216    *
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.*
227    *     breakpoint.*.*.*.*
228    *     breakpoint.*
229    *   Colons are also used, for example,
230    *   block.settings.system_menu_block:footer will check for definitions in the
231    *   following order:
232    *     block.settings.system_menu_block:*
233    *     block.settings.*:*
234    *     block.settings.*
235    *     block.*.*:*
236    *     block.*
237    */
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])) {
243         return $replaced;
244       }
245       else {
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])) {
252           return $one_star;
253         }
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);
257       }
258     }
259   }
260
261   /**
262    * Replaces variables in configuration name.
263    *
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.
267    *
268    * @param string $name
269    *   Configuration name with variables in square brackets.
270    * @param mixed $data
271    *   Configuration data for the element.
272    * @return string
273    *   Configuration name with variables replaced.
274    */
275   protected function replaceName($name, $data) {
276     if (preg_match_all("/\[(.*)\]/U", $name, $matches)) {
277       // Build our list of '[value]' => replacement.
278       $replace = [];
279       foreach (array_combine($matches[0], $matches[1]) as $key => $value) {
280         $replace[$key] = $this->replaceVariable($value, $data);
281       }
282       return strtr($name, $replace);
283     }
284     else {
285       return $name;
286     }
287   }
288
289   /**
290    * Replaces variable values in included names with configuration data.
291    *
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.
298    *
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
301    * parent element.
302    *
303    * Example patterns:
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
309    *   parent's parent.
310    *
311    * @param string $value
312    *   Variable value to be replaced.
313    * @param mixed $data
314    *   Configuration data for the element.
315    *
316    * @return string
317    *   The replaced value if a replacement found or the original value if not.
318    */
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
325         return $value;
326       }
327       elseif (!$parts) {
328         $value = $data[$name];
329         if (is_bool($value)) {
330           $value = (int) $value;
331         }
332         // If no more parts left, this is the final property.
333         return (string) $value;
334       }
335       else {
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();
347           }
348         }
349         else {
350           $data = $data[$name];
351         }
352       }
353     }
354   }
355
356   /**
357    * {@inheritdoc}
358    */
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);
363   }
364
365   /**
366    * {@inheritdoc}
367    */
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";
377       }
378       elseif (!empty($added_keys)) {
379         $message = "Invoking hook_config_schema_info_alter() has added ($added_keys) schema definitions";
380       }
381       else {
382         $message = "Invoking hook_config_schema_info_alter() has removed ($removed_keys) schema definitions";
383       }
384       throw new ConfigSchemaAlterException($message);
385     }
386   }
387
388   /**
389    * {@inheritdoc}
390    */
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);
395   }
396
397 }