Newer
Older
Dries Buytaert
committed
<?php
/**
* @file
* Contains \Drupal\Core\TypedData\TypedDataManager.
Dries Buytaert
committed
*/
namespace Drupal\Core\TypedData;
Alex Pott
committed
use Drupal\Component\Plugin\Exception\PluginException;
Alex Pott
committed
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\Plugin\DefaultPluginManager;
Dries Buytaert
committed
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;
Dries Buytaert
committed
/**
* Manages data type plugins.
*/
Alex Pott
committed
class TypedDataManager extends DefaultPluginManager {
Dries Buytaert
committed
Dries Buytaert
committed
/**
* 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;
/**
* An array of typed data property prototypes.
*
* @var array
*/
protected $prototypes = array();
Alex Pott
committed
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, LanguageManager $language_manager, ModuleHandlerInterface $module_handler) {
$this->alterInfo($module_handler, 'data_type_info');
catch
committed
$this->setCacheBackend($cache_backend, $language_manager, 'typed_data_types_plugins');
Angie Byron
committed
parent::__construct('Plugin/DataType', $namespaces, 'Drupal\Core\TypedData\Annotation\DataType');
Dries Buytaert
committed
}
/**
Angie Byron
committed
* Instantiates a typed data object.
Dries Buytaert
committed
*
Angie Byron
committed
* @param string $data_type
* The data type, for which a typed object should be instantiated.
Dries Buytaert
committed
* @param array $configuration
Angie Byron
committed
* 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.
Dries Buytaert
committed
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The instantiated typed data object.
Dries Buytaert
committed
*/
Angie Byron
committed
public function createInstance($data_type, array $configuration) {
$data_definition = $configuration['data_definition'];
$type_definition = $this->getDefinition($data_type);
Alex Pott
committed
if (!isset($type_definition)) {
Angie Byron
committed
throw new \InvalidArgumentException(format_string('Invalid data type %plugin_id has been given.', array('%plugin_id' => $data_type)));
Alex Pott
committed
}
// Allow per-data definition overrides of the used classes, i.e. take over
Angie Byron
committed
// classes specified in the type definition.
$class = $data_definition->getClass();
$class = isset($class) ? $class : $type_definition['class'];
Alex Pott
committed
if (!isset($class)) {
Angie Byron
committed
throw new PluginException(sprintf('The plugin (%s) did not specify an instance class.', $data_type));
Alex Pott
committed
}
Angie Byron
committed
return new $class($data_definition, $configuration['name'], $configuration['parent']);
Dries Buytaert
committed
}
/**
* Creates a new typed data object instance.
Dries Buytaert
committed
*
Angie Byron
committed
* @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.
* @todo: Need to remove array support in https://drupal.org/node/2112239.
Dries Buytaert
committed
* @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.
* @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.
Dries Buytaert
committed
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The instantiated typed data object.
Dries Buytaert
committed
*
Alex Pott
committed
* @see \Drupal::typedData()
* @see \Drupal\Core\TypedData\TypedDataManager::getPropertyInstance()
Alex Pott
committed
* @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
Dries Buytaert
committed
*/
Angie Byron
committed
public function create($definition, $value = NULL, $name = NULL, $parent = NULL) {
// @todo: Remove array support once https://drupal.org/node/2112239 is in.
if (is_array($definition)) {
$definition = DataDefinition::createFromOldStyleDefinition($definition);
}
$typed_data = $this->createInstance($definition->getDataType(), array(
'data_definition' => $definition,
'name' => $name,
'parent' => $parent,
));
Dries Buytaert
committed
if (isset($value)) {
Angie Byron
committed
$typed_data->setValue($value, FALSE);
Dries Buytaert
committed
}
Angie Byron
committed
return $typed_data;
}
/**
* 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
catch
committed
* TypedDataInterface and either the ListInterface or the
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
* 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.
*
catch
committed
* @param \Drupal\Core\TypedData\TypedDataInterface $object
* The parent typed data object, implementing the TypedDataInterface and
* 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()
*/
public function getPropertyInstance(TypedDataInterface $object, $property_name, $value = NULL) {
$definition = $object->getRoot()->getDefinition();
$key = $definition['type'];
if (isset($definition['settings'])) {
$key .= ':' . implode(',', $definition['settings']);
$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;
// Make sure we have a prototype. Then, clone the prototype and set object
// specific values, i.e. the value and the context.
if (!isset($this->prototypes[$key]) || !$key) {
// Create the initial prototype. For that we need to fetch the definition
// of the to be created property instance from the parent.
if ($object instanceof ComplexDataInterface) {
$definition = $object->getPropertyDefinition($property_name);
Dries Buytaert
committed
}
elseif ($object instanceof ListInterface) {
$definition = $object->getItemDefinition();
Dries Buytaert
committed
}
else {
Angie Byron
committed
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) {
Angie Byron
committed
throw new \InvalidArgumentException('Property ' . check_plain($property_name) . ' is unknown.');
}
// Now create the prototype using the definition, but do not pass the
// given value as it will serve as prototype for any further instance.
$this->prototypes[$key] = $this->create($definition, NULL, $property_name, $object);
Dries Buytaert
committed
}
// Clone from the prototype, then update the parent relationship and set the
// data value if necessary.
$property = clone $this->prototypes[$key];
catch
committed
$property->setContext($property_name, $object);
if (isset($value)) {
catch
committed
$property->setValue($value, FALSE);
}
return $property;
Dries Buytaert
committed
}
Dries Buytaert
committed
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
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
/**
* 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
*
Angie Byron
committed
* @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
* A data definition.
Dries Buytaert
committed
*
* @return array
* Array of constraints, each being an instance of
* \Symfony\Component\Validator\Constraint.
Angie Byron
committed
*
* @todo: Having this as well as $definition->getConstraints() is confusing.
Dries Buytaert
committed
*/
Angie Byron
committed
public function getConstraints(DataDefinitionInterface $definition) {
Dries Buytaert
committed
$constraints = array();
Alex Pott
committed
$validation_manager = $this->getValidationConstraintManager();
Angie Byron
committed
$type_definition = $this->getDefinition($definition->getDataType());
Alex Pott
committed
// 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());
Dries Buytaert
committed
}
// Add in constraints specified by the data type.
if (isset($type_definition['constraints'])) {
foreach ($type_definition['constraints'] as $name => $options) {
Alex Pott
committed
// Annotations do not support empty arrays.
if ($options === TRUE) {
$options = array();
}
Alex Pott
committed
$constraints[] = $validation_manager->create($name, $options);
Dries Buytaert
committed
}
}
// Add any constraints specified as part of the data definition.
Angie Byron
committed
$defined_constraints = $definition->getConstraints();
foreach ($defined_constraints as $name => $options) {
$constraints[] = $validation_manager->create($name, $options);
Dries Buytaert
committed
}
// Add the NotNull constraint for required data.
Angie Byron
committed
if ($definition->isRequired() && !isset($defined_constraints['NotNull'])) {
Alex Pott
committed
$constraints[] = $validation_manager->create('NotNull', array());
Dries Buytaert
committed
}
Alex Pott
committed
// If the definition does not provide a class use the class from the type
// definition for performing interface checks.
Angie Byron
committed
$class = $definition->getClass();
if (!$class) {
$class = $type_definition['class'];
}
Alex Pott
committed
// Check if the class provides allowed values.
if (array_key_exists('Drupal\Core\TypedData\AllowedValuesInterface', class_implements($class))) {
$constraints[] = $validation_manager->create('AllowedValues', array());
}
Dries Buytaert
committed
return $constraints;
}