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

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

namespace Drupal\Core\TypedData;

10
11
12
13
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\Plugin\DefaultPluginManager;
14
use InvalidArgumentException;
15
16
17
18
19
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;
20
21
22
23

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

26
27
28
29
30
31
32
33
34
35
36
37
38
39
  /**
   * 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;

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

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

51
52
53
54
    $annotation_namespaces = array(
      'Drupal\Core\TypedData\Annotation' => DRUPAL_ROOT . '/core/lib',
    );
    parent::__construct('DataType', $namespaces, $annotation_namespaces, 'Drupal\Core\TypedData\Annotation\DataType');
55
56
57
  }

  /**
58
   * Implements \Drupal\Component\Plugin\PluginManagerInterface::createInstance().
59
60
61
62
63
   *
   * @param string $plugin_id
   *   The id of a plugin, i.e. the data type.
   * @param array $configuration
   *   The plugin configuration, i.e. the data definition.
64
65
66
67
68
69
70
   * @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.
71
   *
72
   * @return \Drupal\Core\TypedData\TypedDataInterface
73
   *   The instantiated typed data object.
74
   */
75
  public function createInstance($plugin_id, array $configuration, $name = NULL, $parent = NULL) {
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
    $type_definition = $this->getDefinition($plugin_id);

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

    // Allow per-data definition overrides of the used classes, i.e. take over
    // classes specified in the data definition.
    $key = empty($configuration['list']) ? 'class' : 'list_class';
    if (isset($configuration[$key])) {
      $class = $configuration[$key];
    }
    elseif (isset($type_definition[$key])) {
      $class = $type_definition[$key];
    }

    if (!isset($class)) {
      throw new PluginException(sprintf('The plugin (%s) did not specify an instance class.', $plugin_id));
    }
    return new $class($configuration, $name, $parent);
96
97
98
  }

  /**
99
   * Creates a new typed data object instance.
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
   *
   * @param array $definition
   *   The data definition array with the following array keys and values:
   *   - type: The data type of the data to wrap. Required.
   *   - label: A human readable label.
   *   - description: A human readable description.
   *   - list: Whether the data is multi-valued, i.e. a list of data items.
   *     Defaults to FALSE.
   *   - computed: A boolean specifying whether the data value is computed by
   *     the object, e.g. depending on some other values.
   *   - read-only: A boolean specifying whether the data is read-only. Defaults
   *     to TRUE for computed properties, to FALSE otherwise.
   *   - class: If set and 'list' is FALSE, the class to use for creating the
   *     typed data object; otherwise the default class of the data type will be
   *     used.
115
   *   - list_class: If set and 'list' is TRUE, the class to use for creating
116
117
118
119
   *     the typed data object; otherwise the default list class of the data
   *     type will be used.
   *   - settings: An array of settings, as required by the used 'class'. See
   *     the documentation of the class for supported or required settings.
120
121
   *   - list_settings: An array of settings as required by the used
   *     'list_class'. See the documentation of the list class for support or
122
   *     required settings.
123
124
   *   - constraints: An array of validation constraints. See
   *     \Drupal\Core\TypedData\TypedDataManager::getConstraints() for details.
125
126
127
   *   - required: A boolean specifying whether a non-NULL value is mandatory.
   *   Further keys may be supported in certain usages, e.g. for further keys
   *   supported for entity field definitions see
128
   *   \Drupal\Core\Entity\StorageControllerInterface::getPropertyDefinitions().
129
130
131
   * @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.
132
133
134
135
136
137
138
   * @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.
139
   *
140
   * @return \Drupal\Core\TypedData\TypedDataInterface
141
   *   The instantiated typed data object.
142
   *
143
   * @see \Drupal::typedData()
144
   * @see \Drupal\Core\TypedData\TypedDataManager::getPropertyInstance()
145
146
147
148
149
150
151
152
   * @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
153
   * @see \Drupal\Core\Entity\Field\EntityWrapper
154
   */
155
  public function create(array $definition, $value = NULL, $name = NULL, $parent = NULL) {
156
    $wrapper = $this->createInstance($definition['type'], $definition, $name, $parent);
157
    if (isset($value)) {
158
      $wrapper->setValue($value, FALSE);
159
    }
160
161
162
163
164
165
166
167
168
    return $wrapper;
  }

  /**
   * 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
169
   *     TypedDataInterface and either the ListInterface or the
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
   *     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.
   *
200
201
   * @param \Drupal\Core\TypedData\TypedDataInterface $object
   *   The parent typed data object, implementing the TypedDataInterface and
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
   *   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()
217
218
219
   *
   * @todo: Add type-hinting to $object once entities implement the
   *   TypedDataInterface.
220
   */
221
  public function getPropertyInstance($object, $property_name, $value = NULL) {
222
223
224
225
226
227
228
229
230
231
232
    if ($root = $object->getRoot()) {
      $key = $root->getType() . ':' . $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;
    }
    else {
      // Missing context, thus we cannot determine a unique key for prototyping.
      // Fall back to create a new prototype on each call.
      $key = FALSE;
    }
233
234
235

    // Make sure we have a prototype. Then, clone the prototype and set object
    // specific values, i.e. the value and the context.
236
    if (!isset($this->prototypes[$key]) || !$key) {
237
238
      // Create the initial prototype. For that we need to fetch the definition
      // of the to be created property instance from the parent.
239
240
      if ($object instanceof ComplexDataInterface) {
        $definition = $object->getPropertyDefinition($property_name);
241
      }
242
243
      elseif ($object instanceof ListInterface) {
        $definition = $object->getItemDefinition();
244
      }
245
246
247
248
249
250
251
      else {
        throw new InvalidArgumentException("The passed object has to either implement the ComplexDataInterface or the ListInterface.");
      }
      // Make sure we have got a valid definition.
      if (!$definition) {
        throw new InvalidArgumentException('Property ' . check_plain($property_name) . ' is unknown.');
      }
252
253
      // Now create the prototype using the definition, but do not pass the
      // given value as it will serve as prototype for any further instance.
254
      $this->prototypes[$key] = $this->create($definition, NULL, $property_name, $object);
255
    }
256

257
258
    // Clone from the prototype, then update the parent relationship and set the
    // data value if necessary.
259
    $property = clone $this->prototypes[$key];
260
    $property->setContext($property_name, $object);
261
    if (isset($value)) {
262
      $property->setValue($value, FALSE);
263
264
    }
    return $property;
265
  }
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
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

  /**
   * 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
   *
   * @param array $definition
   *   A data definition array.
   *
   * @return array
   *   Array of constraints, each being an instance of
   *   \Symfony\Component\Validator\Constraint.
   */
  public function getConstraints($definition) {
    $constraints = array();
356
357
    $validation_manager = $this->getValidationConstraintManager();

358
359
    $type_definition = $this->getDefinition($definition['type']);
    // Auto-generate a constraint for the primitive type if we have a mapping.
360
361
    if (isset($type_definition['primitive_type'])) {
      $constraints[] = $validation_manager->create('PrimitiveType', array('type' => $type_definition['primitive_type']));
362
363
364
365
    }
    // Add in constraints specified by the data type.
    if (isset($type_definition['constraints'])) {
      foreach ($type_definition['constraints'] as $name => $options) {
366
367
368
369
        // Annotations do not support empty arrays.
        if ($options === TRUE) {
          $options = array();
        }
370
        $constraints[] = $validation_manager->create($name, $options);
371
372
373
374
375
      }
    }
    // Add any constraints specified as part of the data definition.
    if (isset($definition['constraints'])) {
      foreach ($definition['constraints'] as $name => $options) {
376
        $constraints[] = $validation_manager->create($name, $options);
377
378
379
380
      }
    }
    // Add the NotNull constraint for required data.
    if (!empty($definition['required']) && empty($definition['constraints']['NotNull'])) {
381
      $constraints[] = $validation_manager->create('NotNull', array());
382
383
384
    }
    return $constraints;
  }
385
}