TypedDataManager.php 17.1 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 50 51 52 53 54 55 56 57 58 59 60
 /**
  * Constructs a new TypedDataManager.
  *
  * @param \Traversable $namespaces
  *   An object that implements \Traversable which contains the root paths
  *   keyed by the corresponding namespace to look for plugin implementations.
  * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
  *   Cache backend instance to use.
  * @param \Drupal\Core\Language\LanguageManager $language_manager
  *   The language manager.
  * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  *   The module handler.
  */
61
  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, LanguageManager $language_manager, ModuleHandlerInterface $module_handler) {
62
    $this->alterInfo('data_type_info');
63
    $this->setCacheBackend($cache_backend, $language_manager, 'typed_data_types_plugins');
64

65
    parent::__construct('Plugin/DataType', $namespaces, $module_handler, 'Drupal\Core\TypedData\Annotation\DataType');
66 67 68
  }

  /**
69
   * Instantiates a typed data object.
70
   *
71 72
   * @param string $data_type
   *   The data type, for which a typed object should be instantiated.
73
   * @param array $configuration
74 75 76 77 78 79 80 81
   *   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.
82
   *
83
   * @return \Drupal\Core\TypedData\TypedDataInterface
84
   *   The instantiated typed data object.
85
   */
86 87 88
  public function createInstance($data_type, array $configuration) {
    $data_definition = $configuration['data_definition'];
    $type_definition = $this->getDefinition($data_type);
89 90

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

    // Allow per-data definition overrides of the used classes, i.e. take over
95 96 97
    // classes specified in the type definition.
    $class = $data_definition->getClass();
    $class = isset($class) ? $class : $type_definition['class'];
98 99

    if (!isset($class)) {
100
      throw new PluginException(sprintf('The plugin (%s) did not specify an instance class.', $data_type));
101
    }
102
    return new $class($data_definition, $configuration['name'], $configuration['parent']);
103 104 105
  }

  /**
106
   * Creates a new typed data object instance.
107
   *
108 109 110
   * @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.
111 112 113
   * @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.
114 115 116 117 118 119 120
   * @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.
121
   *
122
   * @return \Drupal\Core\TypedData\TypedDataInterface
123
   *   The instantiated typed data object.
124
   *
125
   * @see \Drupal::typedDataManager()
126
   * @see \Drupal\Core\TypedData\TypedDataManager::getPropertyInstance()
127 128 129 130 131 132 133 134
   * @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
135
   */
136
  public function create(DataDefinitionInterface $definition, $value = NULL, $name = NULL, $parent = NULL) {
137 138 139 140 141
    $typed_data = $this->createInstance($definition->getDataType(), array(
      'data_definition' => $definition,
      'name' => $name,
      'parent' => $parent,
    ));
142
    if (isset($value)) {
143
      $typed_data->setValue($value, FALSE);
144
    }
145
    return $typed_data;
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 185 186 187 188 189 190 191 192 193 194 195 196 197
  /**
   * 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);
  }

198 199 200 201 202 203
  /**
   * 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
204
   *     TypedDataInterface and either the ListInterface or the
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
   *     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.
   *
235 236
   * @param \Drupal\Core\TypedData\TypedDataInterface $object
   *   The parent typed data object, implementing the TypedDataInterface and
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
   *   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()
   */
253
  public function getPropertyInstance(TypedDataInterface $object, $property_name, $value = NULL) {
254
    $definition = $object->getRoot()->getDataDefinition();
255 256
    // If the definition is a list, we need to look at the data type and the
    // settings of its item definition.
257
    if ($definition instanceof ListDataDefinition) {
258 259 260 261 262
      $definition = $definition->getItemDefinition();
    }
    $key = $definition->getDataType();
    if ($settings = $definition->getSettings()) {
      $key .= ':' . implode(',', $settings);
263
    }
264 265 266 267
    $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;
268 269 270

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

292 293
    // Clone from the prototype, then update the parent relationship and set the
    // data value if necessary.
294
    $property = clone $this->prototypes[$key];
295
    $property->setContext($property_name, $object);
296
    if (isset($value)) {
297
      $property->setValue($value, FALSE);
298 299
    }
    return $property;
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 369 370 371 372 373 374 375 376 377 378 379 380 381

  /**
   * 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
   *
382 383
   * @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
   *   A data definition.
384 385 386 387
   *
   * @return array
   *   Array of constraints, each being an instance of
   *   \Symfony\Component\Validator\Constraint.
388 389
   *
   * @todo: Having this as well as $definition->getConstraints() is confusing.
390
   */
391
  public function getConstraints(DataDefinitionInterface $definition) {
392
    $constraints = array();
393 394
    $validation_manager = $this->getValidationConstraintManager();

395
    $type_definition = $this->getDefinition($definition->getDataType());
396 397 398 399
    // 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());
400 401 402 403
    }
    // Add in constraints specified by the data type.
    if (isset($type_definition['constraints'])) {
      foreach ($type_definition['constraints'] as $name => $options) {
404 405 406 407
        // Annotations do not support empty arrays.
        if ($options === TRUE) {
          $options = array();
        }
408
        $constraints[] = $validation_manager->create($name, $options);
409 410 411
      }
    }
    // Add any constraints specified as part of the data definition.
412 413 414
    $defined_constraints = $definition->getConstraints();
    foreach ($defined_constraints as $name => $options) {
      $constraints[] = $validation_manager->create($name, $options);
415 416
    }
    // Add the NotNull constraint for required data.
417
    if ($definition->isRequired() && !isset($defined_constraints['NotNull'])) {
418
      $constraints[] = $validation_manager->create('NotNull', array());
419
    }
420 421 422

    // If the definition does not provide a class use the class from the type
    // definition for performing interface checks.
423 424 425 426
    $class = $definition->getClass();
    if (!$class) {
      $class = $type_definition['class'];
    }
427
    // Check if the class provides allowed values.
428
    if (is_subclass_of($class,'Drupal\Core\TypedData\AllowedValuesInterface')) {
429 430
      $constraints[] = $validation_manager->create('AllowedValues', array());
    }
431 432 433 434 435 436
    // Add any constraints about referenced data.
    if ($definition instanceof DataReferenceDefinitionInterface) {
      foreach ($definition->getTargetDefinition()->getConstraints() as $name => $options) {
        $constraints[] = $validation_manager->create($name, $options);
      }
    }
437

438 439
    return $constraints;
  }
440 441 442 443 444 445 446 447 448

  /**
   * {@inheritdoc}
   */
  public function clearCachedDefinitions() {
    parent::clearCachedDefinitions();
    $this->prototypes = array();
  }

449
}