TypedConfigManager.php 12 KB
Newer Older
1 2 3 4 5 6 7 8 9
<?php

/**
 * @file
 * Contains \Drupal\Core\Config\TypedConfigManager.
 */

namespace Drupal\Core\Config;

10
use Drupal\Component\Utility\NestedArray;
11
use Drupal\Core\Cache\CacheBackendInterface;
12
use Drupal\Core\Config\Schema\ArrayElement;
13
use Drupal\Core\Config\Schema\ConfigSchemaAlterException;
14 15
use Drupal\Core\Config\Schema\ConfigSchemaDiscovery;
use Drupal\Core\Extension\ModuleHandlerInterface;
16
use Drupal\Core\TypedData\TypedDataManager;
17 18

/**
19
 * Manages config schema type plugins.
20
 */
21
class TypedConfigManager extends TypedDataManager implements TypedConfigManagerInterface {
22 23

  /**
24
   * A storage instance for reading configuration data.
25
   *
26
   * @var \Drupal\Core\Config\StorageInterface
27
   */
28
  protected $configStorage;
29

30
  /**
31
   * A storage instance for reading configuration schema data.
32 33 34 35 36 37 38 39 40 41
   *
   * @var \Drupal\Core\Config\StorageInterface
   */
  protected $schemaStorage;

  /**
   * The array of plugin definitions, keyed by plugin id.
   *
   * @var array
   */
42
  protected $definitions;
43

44 45 46
  /**
   * Creates a new typed configuration manager.
   *
47
   * @param \Drupal\Core\Config\StorageInterface $configStorage
48
   *   The storage object to use for reading schema data
49
   * @param \Drupal\Core\Config\StorageInterface $schemaStorage
50
   *   The storage object to use for reading schema data
51 52
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   The cache backend to use for caching the definitions.
53
   */
54
  public function __construct(StorageInterface $configStorage, StorageInterface $schemaStorage, CacheBackendInterface $cache, ModuleHandlerInterface $module_handler) {
55
    $this->configStorage = $configStorage;
56
    $this->schemaStorage = $schemaStorage;
57 58 59
    $this->setCacheBackend($cache, 'typed_config_definitions');
    $this->alterInfo('config_schema_info');
    $this->moduleHandler = $module_handler;
60 61
  }

62 63 64 65 66 67 68 69 70 71 72
  /**
   * {@inheritdoc}
   */
  protected function getDiscovery() {
    if (!isset($this->discovery)) {
      $this->discovery = new ConfigSchemaDiscovery($this->schemaStorage);
    }
    return $this->discovery;
  }


73 74 75 76 77 78
  /**
   * Gets typed configuration data.
   *
   * @param string $name
   *   Configuration object name.
   *
79 80
   * @return \Drupal\Core\Config\Schema\TypedConfigInterface
   *   Typed configuration data.
81 82
   */
  public function get($name) {
83
    $data = $this->configStorage->read($name);
84
    $type_definition = $this->getDefinition($name);
85
    $data_definition = $this->buildDataDefinition($type_definition, $data);
86
    return $this->create($data_definition, $data);
87 88 89
  }

  /**
90
   * {@inheritdoc}
91
   */
92 93 94 95
  public function buildDataDefinition(array $definition, $value, $name = NULL, $parent = NULL) {
    // Add default values for data type and replace variables.
    $definition += array('type' => 'undefined');

96 97
    $type = $definition['type'];
    if (strpos($type, ']')) {
98 99 100
      // Replace variable names in definition.
      $replace = is_array($value) ? $value : array();
      if (isset($parent)) {
101
        $replace['%parent'] = $parent;
102 103 104 105
      }
      if (isset($name)) {
        $replace['%key'] = $name;
      }
106 107 108 109
      $type = $this->replaceName($type, $replace);
      // Remove the type from the definition so that it is replaced with the
      // concrete type from schema definitions.
      unset($definition['type']);
110
    }
111
    // Add default values from type definition.
112
    $definition += $this->getDefinition($type);
113

114
    $data_definition = $this->createDataDefinition($definition['type']);
115

116 117 118 119 120
    // Pass remaining values from definition array to data definition.
    foreach ($definition as $key => $value) {
      if (!isset($data_definition[$key])) {
        $data_definition[$key] = $value;
      }
121
    }
122
    return $data_definition;
123 124 125
  }

  /**
126
   * {@inheritdoc}
127
   */
128
  public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE) {
129 130
    $definitions = $this->getDefinitions();
    if (isset($definitions[$base_plugin_id])) {
131 132 133 134 135 136 137
      $type = $base_plugin_id;
    }
    elseif (strpos($base_plugin_id, '.') && $name = $this->getFallbackName($base_plugin_id)) {
      // Found a generic name, replacing the last element by '*'.
      $type = $name;
    }
    else {
138 139
      // If we don't have definition, return the 'undefined' element.
      $type = 'undefined';
140
    }
141
    $definition = $definitions[$type];
142 143
    // Check whether this type is an extension of another one and compile it.
    if (isset($definition['type'])) {
144
      $merge = $this->getDefinition($definition['type'], $exception_on_invalid);
145 146 147
      // Preserve integer keys on merge, so sequence item types can override
      // parent settings as opposed to adding unused second, third, etc. items.
      $definition = NestedArray::mergeDeepArray(array($merge, $definition), TRUE);
148 149 150 151
      // Unset type so we try the merge only once per type.
      unset($definition['type']);
      $this->definitions[$type] = $definition;
    }
152
    // Add type and default definition class.
153
    $definition += array(
154 155 156
      'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
      'type' => $type,
    );
157
    return $definition;
158 159
  }

160 161 162 163
  /**
   * {@inheritdoc}
   */
  public function clearCachedDefinitions() {
164
    $this->schemaStorage->reset();
165
    parent::clearCachedDefinitions();
166 167
  }

168
  /**
169
   * Gets fallback configuration schema name.
170 171 172 173 174
   *
   * @param string $name
   *   Configuration name or key.
   *
   * @return null|string
175 176 177 178
   *   The resolved schema name for the given configuration name or key. Returns
   *   null if there is no schema name to fallback to. For example,
   *   breakpoint.breakpoint.module.toolbar.narrow will check for definitions in
   *   the following order:
179 180
   *     breakpoint.breakpoint.module.toolbar.*
   *     breakpoint.breakpoint.module.*.*
181
   *     breakpoint.breakpoint.module.*
182
   *     breakpoint.breakpoint.*.*.*
183
   *     breakpoint.breakpoint.*
184
   *     breakpoint.*.*.*.*
185
   *     breakpoint.*
186 187 188 189 190 191 192 193
   *   Colons are also used, for example,
   *   block.settings.system_menu_block:footer will check for definitions in the
   *   following order:
   *     block.settings.system_menu_block:*
   *     block.settings.*:*
   *     block.settings.*
   *     block.*.*:*
   *     block.*
194 195 196
   */
  protected function getFallbackName($name) {
    // Check for definition of $name with filesystem marker.
197 198
    $replaced = preg_replace('/([^\.:]+)([\.:\*]*)$/', '*\2', $name);
    if ($replaced != $name) {
199 200 201 202
      if (isset($this->definitions[$replaced])) {
        return $replaced;
      }
      else {
203 204 205 206
        // No definition for this level. Collapse multiple wildcards to a single
        // wildcard to see if there is a greedy match. For example,
        // breakpoint.breakpoint.*.* becomes
        // breakpoint.breakpoint.*
207
        $one_star = preg_replace('/\.([:\.\*]*)$/', '.*', $replaced);
208 209 210 211 212
        if ($one_star != $replaced && isset($this->definitions[$one_star])) {
          return $one_star;
        }
        // Check for next level. For example, if breakpoint.breakpoint.* has
        // been checked and no match found then check breakpoint.*.*
213
        return $this->getFallbackName($replaced);
214 215
      }
    }
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
  }

  /**
   * Replaces variables in configuration name.
   *
   * The configuration name may contain one or more variables to be replaced,
   * enclosed in square brackets like '[name]' and will follow the replacement
   * rules defined by the replaceVariable() method.
   *
   * @param string $name
   *   Configuration name with variables in square brackets.
   * @param mixed $data
   *   Configuration data for the element.
   * @return string
   *   Configuration name with variables replaced.
   */
232
  protected function replaceName($name, $data) {
233 234
    if (preg_match_all("/\[(.*)\]/U", $name, $matches)) {
      // Build our list of '[value]' => replacement.
235
      $replace = array();
236
      foreach (array_combine($matches[0], $matches[1]) as $key => $value) {
237
        $replace[$key] = $this->replaceVariable($value, $data);
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
      }
      return strtr($name, $replace);
    }
    else {
      return $name;
    }
  }

  /**
   * Replaces variable values in included names with configuration data.
   *
   * Variable values are nested configuration keys that will be replaced by
   * their value or some of these special strings:
   * - '%key', will be replaced by the element's key.
   * - '%parent', to reference the parent element.
253 254
   * - '%type', to reference the schema definition type. Can only be used in
   *   combination with %parent.
255 256 257 258 259 260 261 262 263
   *
   * There may be nested configuration keys separated by dots or more complex
   * patterns like '%parent.name' which references the 'name' value of the
   * parent element.
   *
   * Example patterns:
   * - 'name.subkey', indicates a nested value of the current element.
   * - '%parent.name', will be replaced by the 'name' value of the parent.
   * - '%parent.%key', will be replaced by the parent element's key.
264 265 266
   * - '%parent.%type', will be replaced by the schema type of the parent.
   * - '%parent.%parent.%type', will be replaced by the schema type of the
   *   parent's parent.
267 268 269
   *
   * @param string $value
   *   Variable value to be replaced.
270 271
   * @param mixed $data
   *   Configuration data for the element.
272 273 274 275
   *
   * @return string
   *   The replaced value if a replacement found or the original value if not.
   */
276
  protected function replaceVariable($value, $data) {
277 278 279 280 281 282 283 284
    $parts = explode('.', $value);
    // Process each value part, one at a time.
    while ($name = array_shift($parts)) {
      if (!is_array($data) || !isset($data[$name])) {
        // Key not found, return original value
        return $value;
      }
      elseif (!$parts) {
285 286 287 288
        $value = $data[$name];
        if (is_bool($value)) {
          $value = (int) $value;
        }
289
        // If no more parts left, this is the final property.
290
        return (string) $value;
291 292 293
      }
      else {
        // Get nested value and continue processing.
294
        if ($name == '%parent') {
295
          /** @var \Drupal\Core\Config\Schema\ArrayElement $parent */
296 297 298
          // Switch replacement values with values from the parent.
          $parent = $data['%parent'];
          $data = $parent->getValue();
299
          $data['%type'] = $parent->getDataDefinition()->getDataType();
300 301 302 303 304 305 306 307 308
          // The special %parent and %key values now need to point one level up.
          if ($new_parent = $parent->getParent()) {
            $data['%parent'] = $new_parent;
            $data['%key'] = $new_parent->getName();
          }
        }
        else {
          $data = $data[$name];
        }
309 310 311 312
      }
    }
  }

313 314 315 316
  /**
   * {@inheritdoc}
   */
  public function hasConfigSchema($name) {
317
    // The schema system falls back on the Undefined class for unknown types.
318
    $definition = $this->getDefinition($name);
319
    return is_array($definition) && ($definition['class'] != '\Drupal\Core\Config\Schema\Undefined');
320 321
  }

322 323 324 325 326 327 328 329
  /**
   * {@inheritdoc}
   */
  protected function alterDefinitions(&$definitions) {
    $discovered_schema = array_keys($definitions);
    parent::alterDefinitions($definitions);
    $altered_schema = array_keys($definitions);
    if ($discovered_schema != $altered_schema) {
330 331
      $added_keys = implode(',', array_diff($altered_schema, $discovered_schema));
      $removed_keys = implode(',', array_diff($discovered_schema, $altered_schema));
332
      if (!empty($added_keys) && !empty($removed_keys)) {
333
        $message = "Invoking hook_config_schema_info_alter() has added ($added_keys) and removed ($removed_keys) schema definitions";
334 335
      }
      elseif (!empty($added_keys)) {
336
        $message = "Invoking hook_config_schema_info_alter() has added ($added_keys) schema definitions";
337 338
      }
      else {
339
        $message = "Invoking hook_config_schema_info_alter() has removed ($removed_keys) schema definitions";
340
      }
341
      throw new ConfigSchemaAlterException($message);
342 343 344
    }
  }

345 346 347 348 349 350 351
  /**
   * {@inheritdoc}
   */
  public function createInstance($data_type, array $configuration = array()) {
    $instance = parent::createInstance($data_type, $configuration);
    // Enable elements to construct their own definitions using the typed config
    // manager.
352
    if ($instance instanceof ArrayElement) {
353 354 355 356 357
      $instance->setTypedConfig($this);
    }
    return $instance;
  }

358
}