TypedDataManager.php 15.4 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
  public function createInstance($data_type, array $configuration = array()) {
87 88
    $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
    // classes specified in the type definition.
    $class = $data_definition->getClass();
97 98

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

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

197 198 199 200 201 202
  /**
   * 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
203
   *     TypedDataInterface and either the ListInterface or the
204 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
   *     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.
   *
234 235
   * @param \Drupal\Core\TypedData\TypedDataInterface $object
   *   The parent typed data object, implementing the TypedDataInterface and
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
   *   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()
   */
252
  public function getPropertyInstance(TypedDataInterface $object, $property_name, $value = NULL) {
253
    $definition = $object->getRoot()->getDataDefinition();
254 255
    // If the definition is a list, we need to look at the data type and the
    // settings of its item definition.
256
    if ($definition instanceof ListDataDefinition) {
257 258 259 260 261
      $definition = $definition->getItemDefinition();
    }
    $key = $definition->getDataType();
    if ($settings = $definition->getSettings()) {
      $key .= ':' . implode(',', $settings);
262
    }
263 264 265 266
    $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;
267 268 269

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

291 292
    // Clone from the prototype, then update the parent relationship and set the
    // data value if necessary.
293
    $property = clone $this->prototypes[$key];
294
    $property->setContext($property_name, $object);
295
    if (isset($value)) {
296
      $property->setValue($value, FALSE);
297 298
    }
    return $property;
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

  /**
   * 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;
  }

  /**
351
   * Gets default constraints for the given data definition.
352
   *
353 354 355 356
   * This generates default constraint definitions based on the data definition;
   * e.g. a NotNull constraint is generated if the data is defined as required.
   * Besides that any constraints defined for the data type, i.e. below the
   * 'constraint' key of the type's plugin definition, are taken into account.
357
   *
358 359
   * @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
   *   A data definition.
360 361
   *
   * @return array
362 363 364
   *   An array of validation constraint definitions, keyed by constraint name.
   *   Each constraint definition can be used for instantiating
   *   \Symfony\Component\Validator\Constraint objects.
365
   */
366
  public function getDefaultConstraints(DataDefinitionInterface $definition) {
367
    $constraints = array();
368
    $type_definition = $this->getDefinition($definition->getDataType());
369 370 371
    // Auto-generate a constraint for data types implementing a primitive
    // interface.
    if (is_subclass_of($type_definition['class'], '\Drupal\Core\TypedData\PrimitiveInterface')) {
372
      $constraints['PrimitiveType'] = array();
373 374 375
    }
    // Add in constraints specified by the data type.
    if (isset($type_definition['constraints'])) {
376
      $constraints += $type_definition['constraints'];
377 378
    }
    // Add the NotNull constraint for required data.
379 380
    if ($definition->isRequired()) {
      $constraints['NotNull'] = array();
381
    }
382
    // Check if the class provides allowed values.
383 384
    if (is_subclass_of($definition->getClass(),'Drupal\Core\TypedData\AllowedValuesInterface')) {
      $constraints['AllowedValues'] = array();
385
    }
386 387
    // Add any constraints about referenced data.
    if ($definition instanceof DataReferenceDefinitionInterface) {
388
      $constraints += $definition->getTargetDefinition()->getConstraints();
389
    }
390 391
    return $constraints;
  }
392 393 394 395 396 397 398 399 400

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

401
}