TypedDataManager.php 16.5 KB
Newer Older
1
2
3
4
<?php

/**
 * @file
5
 * Contains \Drupal\Core\TypedData\TypedDataManager.
6
7
8
9
 */

namespace Drupal\Core\TypedData;

10
use Drupal\Component\Plugin\Exception\PluginException;
11
use Drupal\Component\Utility\String;
12
13
14
15
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\Plugin\DefaultPluginManager;
16
17
18
19
20
use Drupal\Core\TypedData\Validation\MetadataFactory;
use Drupal\Core\Validation\ConstraintManager;
use Drupal\Core\Validation\DrupalTranslator;
use Symfony\Component\Validator\ValidatorInterface;
use Symfony\Component\Validator\Validation;
21
22
23
24

/**
 * Manages data type plugins.
 */
25
class TypedDataManager extends DefaultPluginManager {
26

27
28
29
30
31
32
33
34
35
36
37
38
39
40
  /**
   * The validator used for validating typed data.
   *
   * @var \Symfony\Component\Validator\ValidatorInterface
   */
  protected $validator;

  /**
   * The validation constraint manager to use for instantiating constraints.
   *
   * @var \Drupal\Core\Validation\ConstraintManager
   */
  protected $constraintManager;

41
42
43
44
45
46
47
  /**
   * An array of typed data property prototypes.
   *
   * @var array
   */
  protected $prototypes = array();

48
49
  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, LanguageManager $language_manager, ModuleHandlerInterface $module_handler) {
    $this->alterInfo($module_handler, 'data_type_info');
50
    $this->setCacheBackend($cache_backend, $language_manager, 'typed_data_types_plugins');
51

52
    parent::__construct('Plugin/DataType', $namespaces, 'Drupal\Core\TypedData\Annotation\DataType');
53
54
55
  }

  /**
56
   * Instantiates a typed data object.
57
   *
58
59
   * @param string $data_type
   *   The data type, for which a typed object should be instantiated.
60
   * @param array $configuration
61
62
63
64
65
66
67
68
   *   The plugin configuration array, i.e. an array with the following keys:
   *   - data definition: The data definition object, i.e. an instance of
   *     \Drupal\Core\TypedData\DataDefinitionInterface.
   *   - name: (optional) If a property or list item is to be created, the name
   *     of the property or the delta of the list item.
   *   - parent: (optional) If a property or list item is to be created, the
   *     parent typed data object implementing either the ListInterface or the
   *     ComplexDataInterface.
69
   *
70
   * @return \Drupal\Core\TypedData\TypedDataInterface
71
   *   The instantiated typed data object.
72
   */
73
74
75
  public function createInstance($data_type, array $configuration) {
    $data_definition = $configuration['data_definition'];
    $type_definition = $this->getDefinition($data_type);
76
77

    if (!isset($type_definition)) {
78
      throw new \InvalidArgumentException(format_string('Invalid data type %plugin_id has been given.', array('%plugin_id' => $data_type)));
79
80
81
    }

    // Allow per-data definition overrides of the used classes, i.e. take over
82
83
84
    // classes specified in the type definition.
    $class = $data_definition->getClass();
    $class = isset($class) ? $class : $type_definition['class'];
85
86

    if (!isset($class)) {
87
      throw new PluginException(sprintf('The plugin (%s) did not specify an instance class.', $data_type));
88
    }
89
    return new $class($data_definition, $configuration['name'], $configuration['parent']);
90
91
92
  }

  /**
93
   * Creates a new typed data object instance.
94
   *
95
96
97
   * @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
   *   The data definition of the typed data object. For backwards-compatibility
   *   an array representation of the data definition may be passed also.
98
99
100
   * @param mixed $value
   *   (optional) The data value. If set, it has to match one of the supported
   *   data type format as documented for the data type classes.
101
102
103
104
105
106
107
   * @param string $name
   *   (optional) If a property or list item is to be created, the name of the
   *   property or the delta of the list item.
   * @param mixed $parent
   *   (optional) If a property or list item is to be created, the parent typed
   *   data object implementing either the ListInterface or the
   *   ComplexDataInterface.
108
   *
109
   * @return \Drupal\Core\TypedData\TypedDataInterface
110
   *   The instantiated typed data object.
111
   *
112
   * @see \Drupal::typedDataManager()
113
   * @see \Drupal\Core\TypedData\TypedDataManager::getPropertyInstance()
114
115
116
117
118
119
120
121
   * @see \Drupal\Core\TypedData\Plugin\DataType\Integer
   * @see \Drupal\Core\TypedData\Plugin\DataType\Float
   * @see \Drupal\Core\TypedData\Plugin\DataType\String
   * @see \Drupal\Core\TypedData\Plugin\DataType\Boolean
   * @see \Drupal\Core\TypedData\Plugin\DataType\Duration
   * @see \Drupal\Core\TypedData\Plugin\DataType\Date
   * @see \Drupal\Core\TypedData\Plugin\DataType\Uri
   * @see \Drupal\Core\TypedData\Plugin\DataType\Binary
122
   */
123
  public function create(DataDefinitionInterface $definition, $value = NULL, $name = NULL, $parent = NULL) {
124
125
126
127
128
    $typed_data = $this->createInstance($definition->getDataType(), array(
      'data_definition' => $definition,
      'name' => $name,
      'parent' => $parent,
    ));
129
    if (isset($value)) {
130
      $typed_data->setValue($value, FALSE);
131
    }
132
    return $typed_data;
133
134
  }

135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
  /**
   * Creates a new data definition object.
   *
   * While data definitions objects may be created directly if the definition
   * class used by a data type is known, this method allows the creation of data
   * definitions for any given data type.
   *
   * E.g., if a definition for a map is to be created, the following code
   * could be used instead of calling this method with the argument 'map':
   * @code
   *   $map_definition = \Drupal\Core\TypedData\MapDataDefinition::create();
   * @endcode
   *
   * @param string $data_type
   *   The data type, for which a data definition should be created.
   *
   * @return \Drupal\Core\TypedData\DataDefinitionInterface
   *   A data definition for the given data type.
   *
   * @see \Drupal\Core\TypedData\TypedDataManager::createListDataDefinition()
   */
  public function createDataDefinition($data_type) {
    $type_definition = $this->getDefinition($data_type);
    if (!isset($type_definition)) {
      throw new \InvalidArgumentException(format_string('Invalid data type %plugin_id has been given.', array('%plugin_id' => $data_type)));
    }
    $class = $type_definition['definition_class'];
    return $class::createFromDataType($data_type);
  }

  /**
   * Creates a new list data definition for items of the given data type.
   *
   * @param string $item_type
   *   The item type, for which a list data definition should be created.
   *
   * @return \Drupal\Core\TypedData\ListDataDefinitionInterface
   *   A list definition for items of the given data type.
   *
   * @see \Drupal\Core\TypedData\TypedDataManager::createDataDefinition()
   */
  public function createListDataDefinition($item_type) {
    $type_definition = $this->getDefinition($item_type);
    if (!isset($type_definition)) {
      throw new \InvalidArgumentException(format_string('Invalid data type %plugin_id has been given.', array('%plugin_id' => $item_type)));
    }
    $class = $type_definition['list_definition_class'];
    return $class::createFromItemType($item_type);
  }

185
186
187
188
189
190
  /**
   * Implements \Drupal\Component\Plugin\PluginManagerInterface::getInstance().
   *
   * @param array $options
   *   An array of options with the following keys:
   *   - object: The parent typed data object, implementing the
191
   *     TypedDataInterface and either the ListInterface or the
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
   *     ComplexDataInterface.
   *   - property: The name of the property to instantiate, or the delta of the
   *     the list item to instantiate.
   *   - value: The value to set. If set, it has to match one of the supported
   *     data type formats as documented by the data type classes.
   *
   * @throws \InvalidArgumentException
   *   If the given property is not known, or the passed object does not
   *   implement the ListInterface or the ComplexDataInterface.
   *
   * @return \Drupal\Core\TypedData\TypedDataInterface
   *   The new property instance.
   *
   * @see \Drupal\Core\TypedData\TypedDataManager::getPropertyInstance()
   */
  public function getInstance(array $options) {
    return $this->getPropertyInstance($options['object'], $options['property'], $options['value']);
  }

  /**
   * Get a typed data instance for a property of a given typed data object.
   *
   * This method will use prototyping for fast and efficient instantiation of
   * many property objects with the same property path; e.g.,
   * when multiple comments are used comment_body.0.value needs to be
   * instantiated very often.
   * Prototyping is done by the root object's data type and the given
   * property path, i.e. all property instances having the same property path
   * and inheriting from the same data type are prototyped.
   *
222
223
   * @param \Drupal\Core\TypedData\TypedDataInterface $object
   *   The parent typed data object, implementing the TypedDataInterface and
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
   *   either the ListInterface or the ComplexDataInterface.
   * @param string $property_name
   *   The name of the property to instantiate, or the delta of an list item.
   * @param mixed $value
   *   (optional) The data value. If set, it has to match one of the supported
   *   data type formats as documented by the data type classes.
   *
   * @throws \InvalidArgumentException
   *   If the given property is not known, or the passed object does not
   *   implement the ListInterface or the ComplexDataInterface.
   *
   * @return \Drupal\Core\TypedData\TypedDataInterface
   *   The new property instance.
   *
   * @see \Drupal\Core\TypedData\TypedDataManager::create()
   */
240
  public function getPropertyInstance(TypedDataInterface $object, $property_name, $value = NULL) {
241
    $definition = $object->getRoot()->getDataDefinition();
242
243
    // If the definition is a list, we need to look at the data type and the
    // settings of its item definition.
244
    if ($definition instanceof ListDataDefinition) {
245
246
247
248
249
      $definition = $definition->getItemDefinition();
    }
    $key = $definition->getDataType();
    if ($settings = $definition->getSettings()) {
      $key .= ':' . implode(',', $settings);
250
    }
251
252
253
254
    $key .= ':' . $object->getPropertyPath() . '.';
    // If we are creating list items, we always use 0 in the key as all list
    // items look the same.
    $key .= is_numeric($property_name) ? 0 : $property_name;
255
256
257

    // Make sure we have a prototype. Then, clone the prototype and set object
    // specific values, i.e. the value and the context.
258
    if (!isset($this->prototypes[$key]) || !$key) {
259
260
      // Create the initial prototype. For that we need to fetch the definition
      // of the to be created property instance from the parent.
261
      if ($object instanceof ComplexDataInterface) {
262
        $definition = $object->getDataDefinition()->getPropertyDefinition($property_name);
263
      }
264
265
      elseif ($object instanceof ListInterface) {
        $definition = $object->getItemDefinition();
266
      }
267
      else {
268
        throw new \InvalidArgumentException("The passed object has to either implement the ComplexDataInterface or the ListInterface.");
269
270
271
      }
      // Make sure we have got a valid definition.
      if (!$definition) {
272
        throw new \InvalidArgumentException('Property ' . String::checkPlain($property_name) . ' is unknown.');
273
      }
274
275
      // Now create the prototype using the definition, but do not pass the
      // given value as it will serve as prototype for any further instance.
276
      $this->prototypes[$key] = $this->create($definition, NULL, $property_name, $object);
277
    }
278

279
280
    // Clone from the prototype, then update the parent relationship and set the
    // data value if necessary.
281
    $property = clone $this->prototypes[$key];
282
    $property->setContext($property_name, $object);
283
    if (isset($value)) {
284
      $property->setValue($value, FALSE);
285
286
    }
    return $property;
287
  }
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368

  /**
   * Sets the validator for validating typed data.
   *
   * @param \Symfony\Component\Validator\ValidatorInterface $validator
   *   The validator object to set.
   */
  public function setValidator(ValidatorInterface $validator) {
    $this->validator = $validator;
  }

  /**
   * Gets the validator for validating typed data.
   *
   * @return \Symfony\Component\Validator\ValidatorInterface
   *   The validator object.
   */
  public function getValidator() {
    if (!isset($this->validator)) {
      $this->validator = Validation::createValidatorBuilder()
        ->setMetadataFactory(new MetadataFactory())
        ->setTranslator(new DrupalTranslator())
        ->getValidator();
    }
    return $this->validator;
  }

  /**
   * Sets the validation constraint manager.
   *
   * The validation constraint manager is used to instantiate validation
   * constraint plugins.
   *
   * @param \Drupal\Core\Validation\ConstraintManager
   *   The constraint manager to set.
   */
  public function setValidationConstraintManager(ConstraintManager $constraintManager) {
    $this->constraintManager = $constraintManager;
  }

  /**
   * Gets the validation constraint manager.
   *
   * @return \Drupal\Core\Validation\ConstraintManager
   *   The constraint manager.
   */
  public function getValidationConstraintManager() {
    return $this->constraintManager;
  }

  /**
   * Gets configured constraints from a data definition.
   *
   * Any constraints defined for the data type, i.e. below the 'constraint' key
   * of the type's plugin definition, or constraints defined below the data
   * definition's constraint' key are taken into account.
   *
   * Constraints are defined via an array, having constraint plugin IDs as key
   * and constraint options as values, e.g.
   * @code
   * $constraints = array(
   *   'Range' => array('min' => 5, 'max' => 10),
   *   'NotBlank' => array(),
   * );
   * @endcode
   * Options have to be specified using another array if the constraint has more
   * than one or zero options. If it has exactly one option, the value should be
   * specified without nesting it into another array:
   * @code
   * $constraints = array(
   *   'EntityType' => 'node',
   *   'Bundle' => 'article',
   * );
   * @endcode
   *
   * Note that the specified constraints must be compatible with the data type,
   * e.g. for data of type 'entity' the 'EntityType' and 'Bundle' constraints
   * may be specified.
   *
   * @see \Drupal\Core\Validation\ConstraintManager
   *
369
370
   * @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
   *   A data definition.
371
372
373
374
   *
   * @return array
   *   Array of constraints, each being an instance of
   *   \Symfony\Component\Validator\Constraint.
375
376
   *
   * @todo: Having this as well as $definition->getConstraints() is confusing.
377
   */
378
  public function getConstraints(DataDefinitionInterface $definition) {
379
    $constraints = array();
380
381
    $validation_manager = $this->getValidationConstraintManager();

382
    $type_definition = $this->getDefinition($definition->getDataType());
383
384
385
386
    // Auto-generate a constraint for data types implementing a primitive
    // interface.
    if (is_subclass_of($type_definition['class'], '\Drupal\Core\TypedData\PrimitiveInterface')) {
      $constraints[] = $validation_manager->create('PrimitiveType', array());
387
388
389
390
    }
    // Add in constraints specified by the data type.
    if (isset($type_definition['constraints'])) {
      foreach ($type_definition['constraints'] as $name => $options) {
391
392
393
394
        // Annotations do not support empty arrays.
        if ($options === TRUE) {
          $options = array();
        }
395
        $constraints[] = $validation_manager->create($name, $options);
396
397
398
      }
    }
    // Add any constraints specified as part of the data definition.
399
400
401
    $defined_constraints = $definition->getConstraints();
    foreach ($defined_constraints as $name => $options) {
      $constraints[] = $validation_manager->create($name, $options);
402
403
    }
    // Add the NotNull constraint for required data.
404
    if ($definition->isRequired() && !isset($defined_constraints['NotNull'])) {
405
      $constraints[] = $validation_manager->create('NotNull', array());
406
    }
407
408
409

    // If the definition does not provide a class use the class from the type
    // definition for performing interface checks.
410
411
412
413
    $class = $definition->getClass();
    if (!$class) {
      $class = $type_definition['class'];
    }
414
    // Check if the class provides allowed values.
415
    if (is_subclass_of($class,'Drupal\Core\TypedData\AllowedValuesInterface')) {
416
417
      $constraints[] = $validation_manager->create('AllowedValues', array());
    }
418
419
420
421
422
423
    // Add any constraints about referenced data.
    if ($definition instanceof DataReferenceDefinitionInterface) {
      foreach ($definition->getTargetDefinition()->getConstraints() as $name => $options) {
        $constraints[] = $validation_manager->create($name, $options);
      }
    }
424

425
426
    return $constraints;
  }
427
}