Data definitions are the objects used to store all that meaning about the underlying data we talked about. They define the type of data they can hold (using an existing DataType plugin) and any kind of other meaningful information about that data. So, together with the plugins, the data definitions are one mean data modeling machine.
At the lowest level, they implement the DataDefinitionInterface and typically extend the DataDefinition class (or one of its subclasses). Important subclasses of DataDefinition are the ListDefinition and ComplexDefinitionBase which are used to define more complex data types. And as you might expect, they correlate to the ListInterface and ComplexDataInterface plugins I mentioned earlier.
Let us see an example of a simple usage of data definitions and DataType plugins by modeling a simple string--my_value.
It all starts with the definition:
$definition = DataDefinition::create('string');
The argument we pass to the create() method is the DataType plugin ID we want to be defining our data as. In this case, it is the StringData plugin.
We already have some options out of the box to define our string data. For example, we can set a label:
$definition->setLabel('Defines a simple string');
We can also mark it as read only or set whatever "settings" we want onto the definition. However, one thing we don't do is deal with the actual value. This is where the DataType plugin comes into play. The way this happens is that we have to create a new plugin instance, based on our definition and a value:
/** @var TypedDataInterface $data */
$data = \Drupal::typedDataManager()->create($definition, 'my_value');
We used the TypedDataManager to create a new instance of our definition with our actual string value. What we get is a plugin that we can use to interact with our data, understand it better, change its value, and so on:
$value = $data->getValue();
$data->setValue('another string')
$type = $data->getDataDefinition()->getDataType();
$label = $data->getDataDefinition()->getLabel();
We can see what kind of data we are dealing with, its label, and other things.
Let's take a look at a slightly more complex example and model our license plate use case we talked about earlier.
We first define the number:
$plate_number_definition = DataDefinition::create('string');
$plate_number_definition->setLabel('A license plate number.');
Then, we define the state code:
$state_code_definition = DataDefinition::create('string');
$state_code_definition->setLabel('A state code');
We are keeping these generic because nobody says we cannot reuse these elsewhere; we might need to deal with state codes.
Next, we create our full plate definition:
$plate_definition = MapDataDefinition::create();
$plate_definition->setLabel('A US license plate');
We use the MapDataDefinition here which uses by default the Map DataType plugin. Essentially, this is a well defined associative array of properties. So, let's add our definitions to it:
$plate_definition->setPropertyDefinition('number', $plate_number_definition);
$plate_definition->setPropertyDefinition('state', $state_code_definition);
This map definition gets two named property definitions--number and state. You can see now the hierarchical aspect of the TypedData API.
Finally, we instantiate the plugin:
/** @var Map $plate */
$plate = \Drupal::typedDataManager()->create($plate_definition, ['state' => 'NY', 'number' => '405-307']);
The value we pass to this type of data is an array whose keys should map to the property names and values to the individual property definitions (which in this case are strings).
Now, we can benefit from all the goodness:
$label = $plate->getDataDefinition()->getLabel();
$number = $plate->get('number');
$state = $plate->get('state');
The $number and $state variables are StringData plugins which can then be used to access the individual values inside:
$state_code = $state->getValue();
Their respective definitions can be accessed the same way we did before. So, we managed in these few lines to properly define a US license plate construct and make it intelligible by the rest of our code. Next, we will look at even more complex examples and inspect how content entity data is modeled using TypedData. Configuration entities, as we saw, rely on configuration schemas to define the data types. Under the hood, the schema types themselves reference TypedData API data type plugins themselves. So, behind the scenes, the same low level API is used. To keep things a bit simpler, we will look at content entities where this API is much more explicit and you will actually have to deal with it.