diff --git a/PLUGIN_TYPES.md b/PLUGIN_TYPES.md
index 7e6ccf2..f7542ed 100644
--- a/PLUGIN_TYPES.md
+++ b/PLUGIN_TYPES.md
@@ -11,9 +11,11 @@ default class takes the following:
 - description (optional): the human-readable US English plugin description.
 - plugin_manager_service_id (required): the ID of the plugin type's plugin 
   manager in the service container.
-- plugin_definition_mapper_class (optional): the fully qualified name of a class 
-  that implements `\Drupal\plugin\Plugin\PluginDefinitionMapperInterface`. 
-  Defaults to `\Drupal\plugin\Plugin\DefaultPluginDefinitionMapper`.
+- plugin_definition_decorator_class (optional): the fully qualified name of a class 
+  that implements 
+  `\Drupal\plugin\PluginDefinition\PluginDefinitionDecoratorInterface` if the 
+  original plugin definitions do not implement 
+  `\Drupal\plugin\PluginDefinition\PluginDefinitionInterface`.
 - operations_provider_class (optional): the fully qualified name of a class that
   implements `\Drupal\plugin\PluginTypeOperationsProviderInterface`. Defaults to
   `\Drupal\plugin\DefaultPluginTypeOperationsProvider`.
diff --git a/modules/plugin_test_helper/src/Plugin/PluginTestHelper/MockManager.php b/modules/plugin_test_helper/src/Plugin/PluginTestHelper/MockManager.php
index 2d5f831..e1209f4 100644
--- a/modules/plugin_test_helper/src/Plugin/PluginTestHelper/MockManager.php
+++ b/modules/plugin_test_helper/src/Plugin/PluginTestHelper/MockManager.php
@@ -10,11 +10,13 @@ namespace Drupal\plugin_test_helper\Plugin\PluginTestHelper;
 use Drupal\Component\Plugin\Factory\DefaultFactory;
 use Drupal\Component\Plugin\PluginManagerBase;
 use Drupal\Component\Plugin\Discovery\StaticDiscovery;
+use Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator;
+use Drupal\plugin\PluginDiscovery\TypedDiscoveryInterface;
 
 /**
  * Provides a plugin manager for testing plugin-related functionality.
  */
-class MockManager extends PluginManagerBase {
+class MockManager extends PluginManagerBase implements TypedDiscoveryInterface {
 
   /**
    * Constructs a new instance.
@@ -23,18 +25,18 @@ class MockManager extends PluginManagerBase {
     $this->discovery = new StaticDiscovery();
 
     $plugin_id = 'plugin_test_helper_plugin';
-    $this->discovery->setDefinition($plugin_id, [
+    $this->discovery->setDefinition($plugin_id, new ArrayPluginDefinitionDecorator([
       'id' => $plugin_id,
       'label' => t('Plugin'),
       'class' => 'Drupal\plugin_test_helper\Plugin\PluginTestHelper\MockPlugin',
-    ]);
+    ]));
 
     $configurable_plugin_id = 'plugin_test_helper_configurable_plugin';
-    $this->discovery->setDefinition($configurable_plugin_id, [
+    $this->discovery->setDefinition($configurable_plugin_id, new ArrayPluginDefinitionDecorator([
       'id' => $configurable_plugin_id,
       'label' => t('Configurable plugin'),
       'class' => 'Drupal\plugin_test_helper\Plugin\PluginTestHelper\MockConfigurablePlugin',
-    ]);
+    ]));
 
     $this->factory = new DefaultFactory($this->discovery);
   }
diff --git a/plugin.plugin_type.yml b/plugin.plugin_type.yml
index a161a88..2f06442 100644
--- a/plugin.plugin_type.yml
+++ b/plugin.plugin_type.yml
@@ -1,251 +1,305 @@
 plugin_selector:
   label: Plugin selector
   plugin_manager_service_id: plugin.manager.plugin.plugin_selector
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Drupal core's plugin types.
 block:
   label: Block
   provider: block
   plugin_manager_service_id: plugin.manager.block
-  plugin_definition_mapper_class: \Drupal\plugin\Plugin\BlockPluginDefinitionMapper
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\BlockPluginDefinitionDecorator
 entity_reference_selector:
   label: Entity reference selector
   provider: core
   plugin_manager_service_id: plugin.manager.entity_reference_selection
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 field_type:
   label: Field type/item
   provider: core
   plugin_manager_service_id: plugin.manager.field.field_type
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
   field_type: false
 field_widget:
   label: Field widget
   provider: core
   plugin_manager_service_id: plugin.manager.field.widget
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 field_formatter:
   label: Field formatter
   provider: core
   plugin_manager_service_id: plugin.manager.field.formatter
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 archiver:
   label: Archiver
   provider: core
   plugin_manager_service_id: plugin.manager.archiver
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 action:
   label: Action
   provider: core
   plugin_manager_service_id: plugin.manager.action
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 menu_link:
   label: Menu link
   provider: core
   plugin_manager_service_id: plugin.manager.menu.link
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 menu_local_action:
   label: Local action
   provider: core
   plugin_manager_service_id: plugin.manager.menu.local_action
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 menu_local_task:
   label: Local task
   provider: core
   plugin_manager_service_id: plugin.manager.menu.local_task
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 menu_lcontextual_link:
   label: Contextual link
   provider: core
   plugin_manager_service_id: plugin.manager.menu.contextual_link
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 display_variant:
   label: Display variant
   provider: core
   plugin_manager_service_id: plugin.manager.display_variant
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 queue_worker:
   label: Queue worker
   provider: core
   plugin_manager_service_id: plugin.manager.queue_worker
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 mail:
   label: Mail backend
   provider: core
   plugin_manager_service_id: plugin.manager.mail
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 condition:
   label: Condition
   provider: core
   plugin_manager_service_id: plugin.manager.condition
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 element:
   label: Renderable element
   provider: core
   plugin_manager_service_id: plugin.manager.element_info
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Aggregator's plugin types.
 aggregator_fetcher:
   label: Aggregator fetcher
   provider: aggregator
   plugin_manager_service_id: plugin.manager.aggregator.fetcher
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 aggregator_parser:
   label: Aggregator parser
   provider: aggregator
   plugin_manager_service_id: plugin.manager.aggregator.parser
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 aggregator_processor:
   label: Aggregator processor
   provider: aggregator
   plugin_manager_service_id: plugin.manager.aggregator.processor
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # CKEditor's plugin types.
 ckeditor_plugin:
   label: CKEditor plugin
   provider: ckeditor
   plugin_manager_service_id: plugin.manager.ckeditor.plugin
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Configuration Translation's plugin types.
 config_translation_mapper:
   label: Configuration translation mapper
   provider: config_translation
   plugin_manager_service_id: plugin.manager.config_translation.mapper
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Editor's plugin types.
 editor:
   label: Editor
   provider: editor
   plugin_manager_service_id: plugin.manager.editor
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Filter's plugin types.
 filter:
   label: Filter
   provider: filter
   plugin_manager_service_id: plugin.manager.filter
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Image's plugin types.
 image_effect:
   label: Image effect
   provider: image
   plugin_manager_service_id: plugin.manager.image.effect
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Language's plugin types.
 language_negotiation_method:
   label: Language negotiation method
   provider: language
   plugin_manager_service_id: plugin.manager.language_negotiation_method
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Migrate's plugin types.
 migrate_source:
   label: Migration source
   provider: migrate
   plugin_manager_service_id: plugin.manager.migrate.source
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 migrate_process:
   label: Migration process
   provider: migrate
   plugin_manager_service_id: plugin.manager.migrate.process
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 migrate_destination:
   label: Migration destination
   provider: migrate
   plugin_manager_service_id: plugin.manager.migrate.destination
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 migrate_id_map:
   label: Migration ID map
   provider: migrate
   plugin_manager_service_id: plugin.manager.migrate.id_map
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Migrate Drupal's plugin types.
 migrate_drupal_load:
   label: Migration loader
   provider: migrate_drupal
   plugin_manager_service_id: plugin.manager.migrate.load
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 migrate_drupal_cckfield:
   label: Migration CCK source
   provider: migrate_drupal
   plugin_manager_service_id: plugin.manager.migrate.cckfield
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Quick Edit's plugin types.
 quickedit_editor:
   label: Quick Edit editor
   provider: quickedit
   plugin_manager_service_id: plugin.manager.quickedit.editor
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # REST's plugin types.
 rest_resource:
   label: REST resource
   provider: rest
   plugin_manager_service_id: plugin.manager.rest
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Search's plugin types.
 search:
   label: Search method
   provider: search
   plugin_manager_service_id: plugin.manager.search
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Tour's plugin types.
 tour_tip:
   label: Tour tip
   provider: tour
   plugin_manager_service_id: plugin.manager.tour.tip
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 
 # Views's plugin types.
 views_access:
   label: Views access
   provider: views
   plugin_manager_service_id: plugin.manager.views.access
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_area:
   label: Views area
   provider: views
   plugin_manager_service_id: plugin.manager.views.area
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_argument:
   label: Views argument
   provider: views
   plugin_manager_service_id: plugin.manager.views.argument
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_argument_default:
   label: Views argument default
   provider: views
   plugin_manager_service_id: plugin.manager.views.argument_default
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_argument_validator:
   label: Views argument validator
   provider: views
   plugin_manager_service_id: plugin.manager.views.argument_validator
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_cache:
   label: Views cache
   provider: views
   plugin_manager_service_id: plugin.manager.views.cache
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_display_extender:
   label: Views display extender
   provider: views
   plugin_manager_service_id: plugin.manager.views.display_extender
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_display:
   label: Views display
   provider: views
   plugin_manager_service_id: plugin.manager.views.display
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_exposed_form:
   label: Views exposed form
   provider: views
   plugin_manager_service_id: plugin.manager.views.exposed_form
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_field:
   label: Views field
   provider: views
   plugin_manager_service_id: plugin.manager.views.field
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_filter:
   label: Views filter
   provider: views
   plugin_manager_service_id: plugin.manager.views.filter
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_join:
   label: Views join
   provider: views
   plugin_manager_service_id: plugin.manager.views.join
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_pager:
   label: Views pager
   provider: views
   plugin_manager_service_id: plugin.manager.views.pager
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_query:
   label: Views query
   provider: views
   plugin_manager_service_id: plugin.manager.views.query
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_relationship:
   label: Views relationship
   provider: views
   plugin_manager_service_id: plugin.manager.views.relationship
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_row:
   label: Views row
   provider: views
   plugin_manager_service_id: plugin.manager.views.row
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_sort:
   label: Views sort
   provider: views
   plugin_manager_service_id: plugin.manager.views.sort
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_style:
   label: Views style
   provider: views
   plugin_manager_service_id: plugin.manager.views.style
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
 views_wizard:
   label: Views wizard
   provider: views
   plugin_manager_service_id: plugin.manager.views.wizard
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecoratorx§§
diff --git a/src/Controller/ListPlugins.php b/src/Controller/ListPlugins.php
index 90ddc3e..2c82051 100644
--- a/src/Controller/ListPlugins.php
+++ b/src/Controller/ListPlugins.php
@@ -7,6 +7,10 @@
 
 namespace Drupal\plugin\Controller;
 
+use Drupal\plugin\PluginDefinition\PluginDefinitionInterface;
+use Drupal\plugin\PluginDefinition\PluginDescriptionDefinitionInterface;
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionInterface;
+use Drupal\plugin\PluginDiscovery\TypedDefinitionEnsuringPluginDiscoveryDecorator;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
@@ -48,21 +52,25 @@ class ListPlugins extends ListBase {
       '#header' => [$this->t('Type'), $this->t('Description'), $this->t('Provider')],
       '#type' => 'table',
     ];
-    $plugin_definition_mapper = $plugin_type->getPluginDefinitionMapper();
-    $plugin_definitions = $plugin_type->getPluginManager()->getDefinitions();
-    uasort($plugin_definitions, function (array $plugin_definition_a, array $plugin_definition_b) use ($plugin_definition_mapper) {
-      return strnatcasecmp($plugin_definition_mapper->getPluginLabel($plugin_definition_a), $plugin_definition_mapper->getPluginLabel($plugin_definition_b));
-    });
-    foreach ($plugin_definitions as $plugin_id => $plugin_definition) {
-
-      $build[$plugin_id]['label'] = [
-        '#markup' => $plugin_definition_mapper->getPluginLabel($plugin_definition) ?: $plugin_definition_mapper->getPluginId($plugin_definition),
-      ];
-      $build[$plugin_id]['description'] = [
-        '#markup' => $plugin_definition_mapper->getPluginDescription($plugin_definition),
-      ];
-      $build[$plugin_id]['provider'] = [
-        '#markup' => $this->getProviderLabel($plugin_definition_mapper->getPluginProvider($plugin_definition)),
+    $plugin_discovery = new TypedDefinitionEnsuringPluginDiscoveryDecorator($plugin_type);
+    /** @var \Drupal\plugin\PluginDefinition\PluginDefinitionInterface[] $plugin_definitions */
+    $plugin_definitions = [];
+    foreach ($plugin_discovery->getDefinitions() as $plugin_definition) {
+      $label = $plugin_definition instanceof PluginLabelDefinitionInterface ? (string) $plugin_definition->getLabel() : $plugin_definition->getId();
+      $plugin_definitions[$label] = $plugin_definition;
+    }
+    uksort($plugin_definitions, 'strnatcasecmp');
+    foreach ($plugin_definitions as $label => $plugin_definition) {
+      $build[$plugin_definition->getId()] = [
+        'label' => [
+          '#markup' => $label,
+        ],
+        'description' => [
+          '#markup' => $plugin_definition instanceof PluginDescriptionDefinitionInterface ? (string) $plugin_definition->getDescription() : NULL,
+        ],
+        'provider' => [
+          '#markup' => $this->getProviderLabel($plugin_definition->getProvider()),
+        ],
       ];
     }
 
diff --git a/src/Plugin/BlockPluginDefinitionMapper.php b/src/Plugin/BlockPluginDefinitionMapper.php
deleted file mode 100644
index 75ac6cf..0000000
--- a/src/Plugin/BlockPluginDefinitionMapper.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\plugin\Plugin\BlockPluginDefinitionMapper.
- */
-
-namespace Drupal\plugin\Plugin;
-
-/**
- * Provides a plugin definition mapper for blocks.
- */
-class BlockPluginDefinitionMapper extends DefaultPluginDefinitionMapper {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getPluginLabel(array $plugin_definition) {
-    return $this->hasPluginDefinitionProperty($plugin_definition, 'admin_label') ? $this->getPluginDefinitionProperty($plugin_definition, 'admin_label') : NULL;
-  }
-
-}
diff --git a/src/Plugin/DefaultPluginDefinitionMapper.php b/src/Plugin/DefaultPluginDefinitionMapper.php
deleted file mode 100644
index 5f14923..0000000
--- a/src/Plugin/DefaultPluginDefinitionMapper.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\plugin\Plugin\DefaultPluginDefinitionMapper.
- */
-
-namespace Drupal\plugin\Plugin;
-
-/**
- * Provides a default plugin definition mapper.
- */
-class DefaultPluginDefinitionMapper implements PluginDefinitionMapperInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getPluginId(array $plugin_definition) {
-    return $this->getPluginDefinitionProperty($plugin_definition, 'id');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getParentPluginId(array $plugin_definition) {
-    return $this->hasPluginDefinitionProperty($plugin_definition, 'parent_id') ? $this->getPluginDefinitionProperty($plugin_definition, 'parent_id') : NULL;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getPluginLabel(array $plugin_definition) {
-    return $this->hasPluginDefinitionProperty($plugin_definition, 'label') ? $this->getPluginDefinitionProperty($plugin_definition, 'label') : NULL;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getPluginDescription(array $plugin_definition) {
-    return $this->hasPluginDefinitionProperty($plugin_definition, 'description') ? $this->getPluginDefinitionProperty($plugin_definition, 'description') : NULL;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getPluginProvider(array $plugin_definition) {
-    return $this->hasPluginDefinitionProperty($plugin_definition, 'provider') ? $this->getPluginDefinitionProperty($plugin_definition, 'provider') : NULL;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function hasPluginDefinitionProperty(array $plugin_definition, $name) {
-    return array_key_exists($name, $plugin_definition);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getPluginDefinitionProperty(array $plugin_definition, $name) {
-    if ($this->hasPluginDefinitionProperty($plugin_definition, $name)) {
-      return $plugin_definition[$name];
-    }
-    else {
-      throw new \InvalidArgumentException(sprintf('Plugin definition property "%s" does not exist.', $name));
-    }
-  }
-
-}
diff --git a/src/Plugin/Field/FieldFormatter/PluginLabel.php b/src/Plugin/Field/FieldFormatter/PluginLabel.php
index ec01309..805dbca 100644
--- a/src/Plugin/Field/FieldFormatter/PluginLabel.php
+++ b/src/Plugin/Field/FieldFormatter/PluginLabel.php
@@ -9,6 +9,7 @@ namespace Drupal\plugin\Plugin\Field\FieldFormatter;
 
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Field\FormatterBase;
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionInterface;
 
 /**
  * A plugin bag field formatter.
@@ -29,10 +30,9 @@ class PluginLabel extends FormatterBase {
     $build = [];
     /** @var \Drupal\plugin\Plugin\Field\FieldType\PluginCollectionItemInterface $item */
     foreach ($items as $delta => $item) {
-      $plugin_definition = $item->getContainedPluginInstance()->getPluginDefinition();
-      $mapper = $item->getPluginType()->getPluginDefinitionMapper();
+      $plugin_definition = $item->getPluginType()->ensureTypedPluginDefinition($item->getContainedPluginInstance()->getPluginDefinition());
       $build[$delta] = [
-        '#markup' => $mapper->getPluginLabel($plugin_definition),
+        '#markup' => $plugin_definition instanceof PluginLabelDefinitionInterface ? (string) $plugin_definition->getLabel() : $plugin_definition->getId(),
       ];
     }
 
diff --git a/src/Plugin/Plugin/PluginSelector/AdvancedPluginSelectorBase.php b/src/Plugin/Plugin/PluginSelector/AdvancedPluginSelectorBase.php
index dd2750e..601ab10 100644
--- a/src/Plugin/Plugin/PluginSelector/AdvancedPluginSelectorBase.php
+++ b/src/Plugin/Plugin/PluginSelector/AdvancedPluginSelectorBase.php
@@ -66,8 +66,7 @@ abstract class AdvancedPluginSelectorBase extends PluginSelectorBase implements
     }
 
     $available_plugins = [];
-    foreach ($this->selectablePluginDiscovery->getDefinitions() as $plugin_definition) {
-      $plugin_id = $this->selectablePluginType->getPluginDefinitionMapper()->getPluginId($plugin_definition);
+    foreach (array_keys($this->selectablePluginDiscovery->getDefinitions()) as $plugin_id) {
       $available_plugins[] = $this->selectablePluginFactory->createInstance($plugin_id);
     }
 
diff --git a/src/Plugin/Plugin/PluginSelector/PluginSelectorBase.php b/src/Plugin/Plugin/PluginSelector/PluginSelectorBase.php
index 6036ab7..65588f1 100644
--- a/src/Plugin/Plugin/PluginSelector/PluginSelectorBase.php
+++ b/src/Plugin/Plugin/PluginSelector/PluginSelectorBase.php
@@ -11,6 +11,7 @@ use Drupal\Component\Plugin\Factory\FactoryInterface;
 use Drupal\Component\Plugin\PluginInspectionInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\PluginBase;
+use Drupal\plugin\PluginDiscovery\TypedDefinitionEnsuringPluginDiscoveryDecorator;
 use Drupal\plugin\PluginTypeInterface;
 
 /**
@@ -30,9 +31,9 @@ abstract class PluginSelectorBase extends PluginBase implements PluginSelectorIn
   protected $previouslySelectedPlugins = [];
 
   /**
-   * The selectable plugin discovery.
+   * The plugin discovery of selectable plugins.
    *
-   * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
+   * @var \Drupal\plugin\PluginDiscovery\TypedDiscoveryInterface
    */
   protected $selectablePluginDiscovery;
 
@@ -239,7 +240,7 @@ abstract class PluginSelectorBase extends PluginBase implements PluginSelectorIn
    * {@inheritdoc}
    */
   public function setSelectablePluginType(PluginTypeInterface $plugin_type) {
-    $this->selectablePluginDiscovery = $plugin_type->getPluginManager();
+    $this->selectablePluginDiscovery = new TypedDefinitionEnsuringPluginDiscoveryDecorator($plugin_type);
     $this->selectablePluginFactory = $plugin_type->getPluginManager();
     $this->selectablePluginType = $plugin_type;
 
@@ -251,7 +252,7 @@ abstract class PluginSelectorBase extends PluginBase implements PluginSelectorIn
    */
   public function setSelectablePluginDiscovery(DiscoveryInterface $plugin_discovery) {
     $this->validateSelectablePluginType();
-    $this->selectablePluginDiscovery = $plugin_discovery;
+    $this->selectablePluginDiscovery = new TypedDefinitionEnsuringPluginDiscoveryDecorator($this->selectablePluginType, $plugin_discovery);
 
     return $this;
   }
diff --git a/src/Plugin/Plugin/PluginSelector/Radios.php b/src/Plugin/Plugin/PluginSelector/Radios.php
index 29d6ee2..535343c 100644
--- a/src/Plugin/Plugin/PluginSelector/Radios.php
+++ b/src/Plugin/Plugin/PluginSelector/Radios.php
@@ -7,6 +7,7 @@
 namespace Drupal\plugin\Plugin\Plugin\PluginSelector;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionInterface;
 
 /**
  * Provides a plugin selector using a radio buttons.
@@ -38,7 +39,8 @@ class Radios extends AdvancedPluginSelectorBase {
     /** @var \Drupal\Component\Plugin\PluginInspectionInterface[] $plugins */
     $plugin_options = [];
     foreach ($plugins as $plugin) {
-      $plugin_options[$plugin->getPluginId()] = $this->selectablePluginType->getPluginDefinitionMapper()->getPluginLabel($plugin->getPluginDefinition());
+      $plugin_definition = $this->selectablePluginType->ensureTypedPluginDefinition($plugin->getPluginDefinition());
+      $plugin_options[$plugin->getPluginId()] = $plugin_definition instanceof PluginLabelDefinitionInterface ? $plugin_definition->getLabel() : $plugin_definition->getId();
     }
     natcasesort($plugin_options);
     $element['container']['plugin_id'] = array(
diff --git a/src/Plugin/Plugin/PluginSelector/SelectList.php b/src/Plugin/Plugin/PluginSelector/SelectList.php
index 46d0b76..8b2dcef 100644
--- a/src/Plugin/Plugin/PluginSelector/SelectList.php
+++ b/src/Plugin/Plugin/PluginSelector/SelectList.php
@@ -7,6 +7,8 @@
 namespace Drupal\plugin\Plugin\Plugin\PluginSelector;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionInterface;
+use Drupal\plugin\PluginHierarchyTrait;
 
 /**
  * Provides a plugin selector using a <select> element.
@@ -18,6 +20,8 @@ use Drupal\Core\Form\FormStateInterface;
  */
 class SelectList extends AdvancedPluginSelectorBase {
 
+  use PluginHierarchyTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -36,7 +40,7 @@ class SelectList extends AdvancedPluginSelectorBase {
       ),
       '#default_value' => $this->getSelectedPlugin() ? $this->getSelectedPlugin()->getPluginId() : NULL,
       '#empty_value' => '',
-      '#options' => $this->buildOptionsLevel($this->buildHierarchy()),
+      '#options' => $this->buildOptionsLevel($this->buildPluginHierarchy($this->selectablePluginDiscovery)),
       '#required' => $this->isRequired(),
       '#title' => $this->getLabel(),
       '#description' => $this->getDescription(),
@@ -47,52 +51,6 @@ class SelectList extends AdvancedPluginSelectorBase {
   }
 
   /**
-   * Returns a hierarchical plugin representation.
-   *
-   * @return array[]
-   *   A possibly infinitely nested associative array. Keys are plugin IDs and
-   *   values are arrays of similar structure as this method's return value.
-   */
-  protected function buildHierarchy() {
-    $parents = [];
-    $children = [];
-    $definitions = $this->selectablePluginDiscovery->getDefinitions();
-    uasort($definitions, array($this, 'sort'));
-    foreach ($definitions as $plugin_id => $definition) {
-      $parent_plugin_id = $this->selectablePluginType->getPluginDefinitionMapper()->getParentPluginId($definition);
-      if ($parent_plugin_id) {
-        $children[$parent_plugin_id][] = $plugin_id;
-      }
-      else {
-        $parents[] = $plugin_id;
-      }
-    }
-
-    return $this->buildHierarchyLevel($parents, $children);
-  }
-
-  /**
-   * Helper function for self::hierarchy().
-   *
-   * @param array $parent_plugin_ids
-   *   An array with IDs of plugins that are part of the same hierarchy level.
-   * @param array $child_plugin_ids
-   *   Keys are plugin IDs. Values are arrays with those plugin's child
-   *   plugin IDs.
-   *
-   * @return array[]
-   *   The return value is identical to that of self::hierarchy().
-   */
-  protected function buildHierarchyLevel(array $parent_plugin_ids, array $child_plugin_ids) {
-    $hierarchy = [];
-    foreach ($parent_plugin_ids as $plugin_id) {
-      $hierarchy[$plugin_id] = isset($child_plugin_ids[$plugin_id]) ? $this->buildHierarchyLevel($child_plugin_ids[$plugin_id], $child_plugin_ids) : [];
-    }
-
-    return $hierarchy;
-  }
-
-  /**
    * Helper function for self::options().
    *
    * @param array $hierarchy
@@ -105,22 +63,17 @@ class SelectList extends AdvancedPluginSelectorBase {
    *   Keys are plugin IDs.
    */
   protected function buildOptionsLevel(array $hierarchy, $depth = 0) {
-    $definitions = $this->selectablePluginDiscovery->getDefinitions();
+    $plugin_definitions = $this->selectablePluginDiscovery->getDefinitions();
     $options = [];
     $prefix = $depth ? str_repeat('-', $depth) . ' ' : '';
     foreach ($hierarchy as $plugin_id => $child_plugin_ids) {
-      $options[$plugin_id] = $prefix . $this->selectablePluginType->getPluginDefinitionMapper()->getPluginLabel($definitions[$plugin_id]);
+      $plugin_definition = $plugin_definitions[$plugin_id];
+      $label = $plugin_definition instanceof PluginLabelDefinitionInterface ? $plugin_definition->getLabel() : $plugin_definition->getId();
+      $options[$plugin_id] = $prefix . $label;
       $options += $this->buildOptionsLevel($child_plugin_ids, $depth + 1);
     }
 
     return $options;
   }
 
-  /**
-   * Implements uasort() callback to sort plugin definitions by label.
-   */
-  protected function sort(array $definition_a, array $definition_b) {
-    return strcmp($this->selectablePluginType->getPluginDefinitionMapper()->getPluginLabel($definition_a), $this->selectablePluginType->getPluginDefinitionMapper()->getPluginLabel($definition_b));
-  }
-
 }
diff --git a/src/Plugin/PluginDefinitionMapperInterface.php b/src/Plugin/PluginDefinitionMapperInterface.php
deleted file mode 100644
index ed41774..0000000
--- a/src/Plugin/PluginDefinitionMapperInterface.php
+++ /dev/null
@@ -1,92 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\plugin\Plugin\PluginDefinitionMapperInterface.
- */
-
-namespace Drupal\plugin\Plugin;
-
-/**
- * Defines a plugin definition mapper.
- *
- * Plugin definition mappers provide APIs around array plugin definitions which
- * are untyped and therefore not portable across APIs.
- */
-interface PluginDefinitionMapperInterface {
-
-  /**
-   * Gets the ID for a plugin.
-   *
-   * @param array $plugin_definition
-   *
-   * @return string
-   */
-  public function getPluginId(array $plugin_definition);
-
-  /**
-   * Gets the plugin's parent's ID.
-   *
-   * @param array $plugin_definition
-   *
-   * @return string|null
-   *   A plugin ID or NULL if no parent was specified.
-   */
-  public function getParentPluginId(array $plugin_definition);
-
-  /**
-   * Gets the label for a plugin.
-   *
-   * @param array $plugin_definition
-   *
-   * @return string|null
-   */
-  public function getPluginLabel(array $plugin_definition);
-
-  /**
-   * Gets the label for a plugin.
-   *
-   * @param array $plugin_definition
-   *
-   * @return string|null
-   */
-  public function getPluginDescription(array $plugin_definition);
-
-  /**
-   * Gets the provider for a plugin.
-   *
-   * @param array $plugin_definition
-   *
-   * @return string|null
-   */
-  public function getPluginProvider(array $plugin_definition);
-
-  /**
-   * Checks if the plugin has an arbitrary property.
-   *
-   * @param array $plugin_definition
-   * @param string $name
-   *   The property name.
-   *
-   * @return bool
-   */
-  public function hasPluginDefinitionProperty(array $plugin_definition, $name);
-
-  /**
-   * Gets the value for an arbitrary plugin property.
-   *
-   * @param array $plugin_definition
-   * @param string $name
-   *   The property name.
-   *
-   * @return mixed
-   *   The property value.
-   *
-   * @throws \InvalidArgumentException
-   *   Thrown when the requested property does not exist.
-   *
-   * @see self::hasPluginDefinitionProperty
-   */
-  public function getPluginDefinitionProperty(array $plugin_definition, $name);
-
-}
diff --git a/src/PluginDefinition/ArrayPluginDefinitionDecorator.php b/src/PluginDefinition/ArrayPluginDefinitionDecorator.php
new file mode 100644
index 0000000..d946f09
--- /dev/null
+++ b/src/PluginDefinition/ArrayPluginDefinitionDecorator.php
@@ -0,0 +1,340 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+use Drupal\Component\Plugin\Context\ContextDefinitionInterface;
+use Drupal\Component\Utility\NestedArray;
+
+/**
+ * Provides a plugin definition based on an array.
+ *
+ * @ingroup Plugin
+ */
+class ArrayPluginDefinitionDecorator implements ArrayPluginDefinitionInterface, PluginContextDefinitionInterface, PluginDeriverDefinitionInterface, PluginLabelDefinitionInterface, PluginCategoryDefinitionInterface, PluginConfigDependenciesDefinitionInterface, PluginDefinitionDecoratorInterface, PluginHierarchyDefinitionInterface {
+
+  use MergeablePluginDefinitionTrait;
+
+  /**
+   * The array definition.
+   *
+   * @var mixed[]
+   */
+  protected $arrayDefinition = [];
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param array $array_definition
+   *   The array definition.
+   */
+  public function __construct(array $array_definition = []) {
+    if (isset($array_definition['class'])) {
+      PluginDefinitionValidator::validateClass($array_definition['class']);
+    }
+    if (isset($array_definition['deriver'])) {
+      PluginDefinitionValidator::validateDeriverClass($array_definition['deriver']);
+    }
+    if (isset($array_definition['context'])) {
+      PluginDefinitionValidator::validateContextDefinitions($array_definition['context']);
+    }
+    $this->arrayDefinition = $array_definition;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createFromDecoratedDefinition($decorated_plugin_definition) {
+    if (!is_array($decorated_plugin_definition)) {
+      $type = is_object($decorated_plugin_definition) ? get_class($decorated_plugin_definition) : gettype($decorated_plugin_definition);
+      throw new \InvalidArgumentException(sprintf('The decorated plugin definition must be an array, but %s given.', $type));
+    }
+
+    return new static($decorated_plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getArrayDefinition() {
+    return $this->arrayDefinition;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function mergeDefaultArrayDefinition(array $other_definition) {
+    $this->arrayDefinition = NestedArray::mergeDeepArray([$other_definition, $this->arrayDefinition], TRUE);
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function mergeOverrideArrayDefinition(array $other_definition) {
+    $this->arrayDefinition = NestedArray::mergeDeepArray([$this->arrayDefinition, $other_definition], TRUE);
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setId($id) {
+    $this->arrayDefinition['id'] = $id;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getId() {
+    return isset($this->arrayDefinition['id']) ? $this->arrayDefinition['id'] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setClass($class) {
+    PluginDefinitionValidator::validateClass($class);
+
+    $this->arrayDefinition['class'] = $class;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getClass() {
+    return isset($this->arrayDefinition['class']) ? $this->arrayDefinition['class'] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLabel($label) {
+    $this->arrayDefinition['label'] = $label;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLabel() {
+    return isset($this->arrayDefinition['label']) ? $this->arrayDefinition['label'] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDeriverClass($class) {
+    PluginDefinitionValidator::validateDeriverClass($class);
+
+    $this->arrayDefinition['deriver'] = $class;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDeriverClass() {
+    return isset($this->arrayDefinition['deriver']) ? $this->arrayDefinition['deriver'] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setProvider($provider) {
+    $this->arrayDefinition['provider'] = $provider;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProvider() {
+    return isset($this->arrayDefinition['provider']) ? $this->arrayDefinition['provider'] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCategory($category) {
+    $this->arrayDefinition['category'] = $category;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCategory() {
+    return isset($this->arrayDefinition['category']) ? $this->arrayDefinition['category'] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfigDependencies(array $dependencies) {
+    $this->arrayDefinition['config_dependencies'] = $dependencies;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigDependencies() {
+    return isset($this->arrayDefinition['config_dependencies']) ? $this->arrayDefinition['config_dependencies'] : [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setContextDefinitions(array $context_definitions) {
+    PluginDefinitionValidator::validateContextDefinitions($context_definitions);
+
+    $this->arrayDefinition['context'] = $context_definitions;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContextDefinitions() {
+    return isset($this->arrayDefinition['context']) ? $this->arrayDefinition['context'] : [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setContextDefinition($name, ContextDefinitionInterface $context_definition) {
+    $this->arrayDefinition['context'][$name] = $context_definition;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContextDefinition($name) {
+    if (!$this->hasContextDefinition($name)) {
+      throw new \InvalidArgumentException(sprintf('Context %s does not exist.', $name));
+    }
+
+    return $this->arrayDefinition['context'][$name];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasContextDefinition($name) {
+    return isset($this->arrayDefinition['context'][$name]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function doMergeDefaultDefinition(PluginDefinitionInterface $other_definition) {
+    /** @var \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionInterface $other_definition */
+    $this->mergeDefaultArrayDefinition($other_definition->getArrayDefinition());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function doMergeOverrideDefinition(PluginDefinitionInterface $other_definition) {
+    /** @var \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionInterface $other_definition */
+    $this->mergeOverrideArrayDefinition($other_definition->getArrayDefinition());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function isDefinitionCompatible(PluginDefinitionInterface $other_definition) {
+    return $other_definition instanceof $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetExists($offset) {
+    return isset($this->arrayDefinition[$offset]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &offsetGet($offset) {
+    return $this->arrayDefinition[$offset];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetSet($offset, $value) {
+    switch ($offset) {
+      case 'class':
+        $this->setClass($value);
+        break;
+      case 'deriver':
+        $this->setDeriverClass($value);
+        break;
+      case 'context':
+        $this->setContextDefinitions($value);
+        break;
+      default:
+        $this->arrayDefinition[$offset] = $value;
+        break;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetUnset($offset) {
+    unset($this->arrayDefinition[$offset]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function count() {
+    return count($this->arrayDefinition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIterator() {
+    return new \ArrayIterator($this->arrayDefinition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setParentId($id) {
+    $this->arrayDefinition['parent_id'] = $id;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getParentId() {
+    return isset($this->arrayDefinition['parent_id']) ? $this->arrayDefinition['parent_id'] : NULL;
+  }
+
+}
diff --git a/src/PluginDefinition/ArrayPluginDefinitionInterface.php b/src/PluginDefinition/ArrayPluginDefinitionInterface.php
new file mode 100644
index 0000000..2342588
--- /dev/null
+++ b/src/PluginDefinition/ArrayPluginDefinitionInterface.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Plugin\PluginDefinitionInterface.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Defines a plugin definition.
+ *
+ * For backwards compatibility with array-based plugin definitions, this
+ * interface implements \ArrayAccess. The required array keys and their
+ * corresponding setters and getters are:
+ * - id: static::setId() and static::getId()
+ * - class: static::setClass() and static::getClass()
+ * - label: static::setLabel() and static::getLabel()
+ * - deriver: static::setDeriverClass() and static::getDeriverClass()
+ * - context: static::setContextDefinitions() and static::getContextDefinitions()
+ *
+ * @ingroup Plugin
+ *
+ * @deprecated Deprecated as of 8.0.0. Do not rely on array plugin
+ *   definitions.
+ */
+interface ArrayPluginDefinitionInterface extends PluginDefinitionInterface, \ArrayAccess, \IteratorAggregate, \Countable {
+
+  /**
+   * Gets the array definition.
+   *
+   * @return mixed[]
+   *   The array definition.
+   */
+  public function getArrayDefinition();
+
+  /**
+   * Merges another array definition into this one, using the other for defaults.
+   *
+   * @param mixed[] $other_definition
+   *   The other array definition.
+   *
+   * @return $this
+   */
+  public function mergeDefaultArrayDefinition(array $other_definition);
+
+  /**
+   * Merges another array definition into this one, using the other for overrides.
+   *
+   * @param mixed[] $other_definition
+   *   The other array definition.
+   *
+   * @return $this
+   */
+  public function mergeOverrideArrayDefinition(array $other_definition);
+
+}
diff --git a/src/PluginDefinition/BlockPluginDefinitionDecorator.php b/src/PluginDefinition/BlockPluginDefinitionDecorator.php
new file mode 100644
index 0000000..2e93f2c
--- /dev/null
+++ b/src/PluginDefinition/BlockPluginDefinitionDecorator.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\plugin\PluginDefinition\BlockPluginDefinitionDecorator.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+use Drupal\Component\Plugin\Context\ContextDefinitionInterface;
+use Drupal\Component\Utility\NestedArray;
+
+/**
+ * Provides a block plugin definition decorator.
+ *
+ * @ingroup Plugin
+ */
+class BlockPluginDefinitionDecorator extends ArrayPluginDefinitionDecorator {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLabel($label) {
+    $this->arrayDefinition['admin_label'] = $label;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLabel() {
+    return isset($this->arrayDefinition['admin_label']) ? $this->arrayDefinition['admin_label'] : NULL;
+  }
+
+}
diff --git a/src/PluginDefinition/MergeablePluginDefinitionTrait.php b/src/PluginDefinition/MergeablePluginDefinitionTrait.php
new file mode 100644
index 0000000..851c219
--- /dev/null
+++ b/src/PluginDefinition/MergeablePluginDefinitionTrait.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Plugin\MergeablePluginDefinitionTrait.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Implements the plugin merger parts of \Drupal\plugin\PluginDefinition\PluginDefinitionInterface.
+ *
+ * @ingroup Plugin
+ */
+trait MergeablePluginDefinitionTrait {
+
+  /**
+   * Implements \Drupal\plugin\PluginDefinition\PluginDefinitionInterface::mergeDefaultDefinition().
+   */
+  public function mergeDefaultDefinition(PluginDefinitionInterface $other_definition) {
+    $this->validateMergeDefinition($other_definition);
+    $this->doMergeDefaultDefinition($other_definition);
+
+    return $this;
+  }
+
+  /**
+   * Merges another definition into this one, using the other for defaults.
+   *
+   * @param static $other_definition
+   *   The other definition to merge into $this. It will not override $this, but
+   *   be used to extract default values from instead.
+   */
+  protected function doMergeDefaultDefinition(PluginDefinitionInterface $other_definition) {
+    // Child classes can override this to perform an actual merge.
+  }
+
+  /**
+   * Implements \Drupal\plugin\PluginDefinition\PluginDefinitionInterface::mergeOverrideDefinition().
+   */
+  public function mergeOverrideDefinition(PluginDefinitionInterface $other_definition) {
+    $this->validateMergeDefinition($other_definition);
+    $this->doMergeOverrideDefinition($other_definition);
+
+    return $this;
+  }
+
+  /**
+   * Merges another definition into this one, using the other for overrides.
+   *
+   * @param static $other_definition
+   *   The other definition to merge into $this. It will override any values
+   *   already set in $this.
+   */
+  protected function doMergeOverrideDefinition(PluginDefinitionInterface $other_definition) {
+    // Child classes can override this to perform an actual merge.
+  }
+
+  /**
+   * Validates whether another definition is compatible with this one.
+   *
+   * @throws \InvalidArgumentException
+   */
+  protected function validateMergeDefinition(PluginDefinitionInterface $other_definition) {
+    if (!$this->isDefinitionCompatible($other_definition)) {
+      throw new \InvalidArgumentException(sprintf('$other_definition must be an instance of %s, but %s was given.', get_class($this), get_class($other_definition)));
+    }
+  }
+
+  /**
+   * Returns whether another definition is compatible with this one.
+   *
+   * @return bool
+   *   Whether or not the definition is compatible with $this.
+   */
+  abstract protected function isDefinitionCompatible(PluginDefinitionInterface $other_definition);
+
+}
diff --git a/src/PluginDefinition/PluginCategoryDefinitionInterface.php b/src/PluginDefinition/PluginCategoryDefinitionInterface.php
new file mode 100644
index 0000000..0726750
--- /dev/null
+++ b/src/PluginDefinition/PluginCategoryDefinitionInterface.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Plugin\PluginCategoryDefinitionInterface.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Defines a plugin definition that includes a category.
+ *
+ * @ingroup Plugin
+ */
+interface PluginCategoryDefinitionInterface extends PluginDefinitionInterface {
+
+  /**
+   * Sets the category.
+   *
+   *
+   * @param \Drupal\Core\StringTranslation\TranslationWrapper|string $category
+   *   The category.
+   *
+   * @return $this
+   */
+  public function setCategory($category);
+
+  /**
+   * Gets the category.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslationWrapper|string|null
+   *   The category.
+   */
+  public function getCategory();
+
+}
diff --git a/src/PluginDefinition/PluginCategoryDefinitionTrait.php b/src/PluginDefinition/PluginCategoryDefinitionTrait.php
new file mode 100644
index 0000000..c21ce78
--- /dev/null
+++ b/src/PluginDefinition/PluginCategoryDefinitionTrait.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Plugin\PluginCategoryDefinitionTrait.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Implements \Drupal\plugin\PluginDefinition\PluginCategoryDefinitionInterface.
+ *
+ * @ingroup Plugin
+ */
+trait PluginCategoryDefinitionTrait {
+
+  /**
+   * The plugin category.
+   *
+   * @var \Drupal\Core\StringTranslation\TranslationWrapper|string|null
+   */
+  protected $category;
+
+  /**
+   * Implements \Drupal\plugin\PluginDefinition\PluginCategoryDefinitionInterface::setCategory().
+   */
+  public function setCategory($category) {
+    $this->category = $category;
+
+    return $this;
+  }
+
+  /**
+   * Implements \Drupal\plugin\PluginDefinition\PluginCategoryDefinitionInterface::getCategory().
+   */
+  public function getCategory() {
+    return $this->category;
+  }
+
+}
diff --git a/src/PluginDefinition/PluginConfigDependenciesDefinitionInterface.php b/src/PluginDefinition/PluginConfigDependenciesDefinitionInterface.php
new file mode 100644
index 0000000..e0d2f65
--- /dev/null
+++ b/src/PluginDefinition/PluginConfigDependenciesDefinitionInterface.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Plugin\PluginConfigDependenciesDefinitionInterface.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Defines a plugin definition that includes config dependencies.
+ *
+ * @ingroup Plugin
+ */
+interface PluginConfigDependenciesDefinitionInterface extends PluginDefinitionInterface {
+
+  /**
+   * Sets the dependencies.
+   *
+   * @param array[] $dependencies.
+   *   An array of dependencies keyed by the type of dependency. One example:
+   *   @code
+   *   array(
+   *     'module' => array(
+   *       'node',
+   *       'field',
+   *       'image',
+   *     ),
+   *   );
+   *   @endcode
+   *
+   * @return $this
+   */
+  public function setConfigDependencies(array $dependencies);
+
+  /**
+   * Gets the dependencies.
+   *
+   * @return array[]
+   *   See self::setConfigDependencies().
+   */
+  public function getConfigDependencies();
+
+}
diff --git a/src/PluginDefinition/PluginConfigDependenciesDefinitionTrait.php b/src/PluginDefinition/PluginConfigDependenciesDefinitionTrait.php
new file mode 100644
index 0000000..ee28f65
--- /dev/null
+++ b/src/PluginDefinition/PluginConfigDependenciesDefinitionTrait.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Plugin\PluginConfigDependenciesDefinitionTrait.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Implements \Drupal\Core\Plugin\PluginConfigDependenciesDefinitionInterface.
+ *
+ * @ingroup Plugin
+ */
+trait PluginConfigDependenciesDefinitionTrait {
+
+  /**
+   * The dependencies.
+   *
+   * @var array[]
+   *   An array of dependencies keyed by the type of dependency. One example:
+   *   @code
+   *   array(
+   *     'module' => array(
+   *       'node',
+   *       'field',
+   *       'image',
+   *     ),
+   *   );
+   *   @endcode
+   *
+   * @return $this
+   */
+  protected $configDependencies = [];
+
+  /**
+   * Implements \Drupal\Core\Plugin\PluginConfigDependenciesDefinitionInterface::setConfigDependencies().
+   */
+  public function setConfigDependencies(array $dependencies) {
+    $this->configDependencies = $dependencies;
+
+    return $this;
+  }
+
+  /**
+   * Implements \Drupal\Core\Plugin\PluginConfigDependenciesDefinitionInterface::getConfigDependencies().
+   */
+  public function getConfigDependencies() {
+    return $this->configDependencies;
+  }
+
+}
diff --git a/src/PluginDefinition/PluginContextDefinitionInterface.php b/src/PluginDefinition/PluginContextDefinitionInterface.php
new file mode 100644
index 0000000..08f855d
--- /dev/null
+++ b/src/PluginDefinition/PluginContextDefinitionInterface.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Plugin\PluginContextDefinitionInterface.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+use Drupal\Component\Plugin\Context\ContextDefinitionInterface;
+
+/**
+ * Defines a plugin definition that includes contexts.
+ *
+ * @ingroup Plugin
+ */
+interface PluginContextDefinitionInterface extends PluginDefinitionInterface {
+
+  /**
+   * Sets the context definitions.
+   *
+   * @param \Drupal\Component\Plugin\Context\ContextDefinitionInterface[] $context_definitions
+   *   The array of context definitions, keyed by context name.
+   *
+   * @return $this
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown if the definitions are invalid.
+   */
+  public function setContextDefinitions(array $context_definitions);
+
+  /**
+   * Gets the context definitions.
+   *
+   * @return \Drupal\Component\Plugin\Context\ContextDefinitionInterface[]
+   *   The array of context definitions, keyed by context name.
+   */
+  public function getContextDefinitions();
+
+  /**
+   * Sets a specific context definition.
+   *
+   * @param string $name
+   *   The name of the context in the plugin definition.
+   * @param \Drupal\Component\Plugin\Context\ContextDefinitionInterface $context_definition
+   *   The context definition to set.
+   *
+   * @return $this
+   */
+  public function setContextDefinition($name, ContextDefinitionInterface $context_definition);
+
+  /**
+   * Gets a specific context definition.
+   *
+   * @param string $name
+   *   The name of the context in the plugin definition.
+   *
+   * @throws \InvalidArgumentException
+   *   If the requested context does not exist.
+   *
+   * @return \Drupal\Component\Plugin\Context\ContextDefinitionInterface
+   *
+   * @see self::hasContextDefinition()
+   */
+  public function getContextDefinition($name);
+
+  /**
+   * Checks if a specific context definition exists.
+   *
+   * @param string $name
+   *   The name of the context in the plugin definition.
+   *
+   * @return bool
+   *   Whether the context definition exists.
+   */
+  public function hasContextDefinition($name);
+
+}
diff --git a/src/PluginDefinition/PluginContextDefinitionTrait.php b/src/PluginDefinition/PluginContextDefinitionTrait.php
new file mode 100644
index 0000000..94f8956
--- /dev/null
+++ b/src/PluginDefinition/PluginContextDefinitionTrait.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Plugin\PluginContextDefinitionTrait.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+use Drupal\Component\Plugin\Context\ContextDefinitionInterface;
+
+/**
+ * Implements \Drupal\Component\Plugin\PluginContextDefinitionInterface.
+ *
+ * @ingroup Plugin
+ */
+trait PluginContextDefinitionTrait {
+
+  /**
+   * The context definitions.
+   *
+   * @var \Drupal\Component\Plugin\Context\ContextDefinitionInterface[]
+   */
+  protected $contextDefinitions = [];
+
+  /**
+   * Implements \Drupal\Component\Plugin\PluginContextDefinitionInterface::setContextDefinitions().
+   */
+  public function setContextDefinitions(array $context_definitions) {
+    PluginDefinitionValidator::validateContextDefinitions($context_definitions);
+
+    $this->contextDefinitions = $context_definitions;
+
+    return $this;
+  }
+
+  /**
+   * Implements \Drupal\Component\Plugin\PluginContextDefinitionInterface::getContextDefinitions().
+   */
+  public function getContextDefinitions() {
+    return $this->contextDefinitions;
+  }
+
+  /**
+   * Implements \Drupal\Component\Plugin\PluginContextDefinitionInterface::setContextDefinition().
+   */
+  public function setContextDefinition($name, ContextDefinitionInterface $context_definition) {
+    $this->contextDefinitions[$name] = $context_definition;
+
+    return $this;
+  }
+
+  /**
+   * Implements \Drupal\Component\Plugin\PluginContextDefinitionInterface::getContextDefinition().
+   */
+  public function getContextDefinition($name) {
+    if (!$this->hasContextDefinition($name)) {
+      throw new \InvalidArgumentException(sprintf('Context %s does not exist.', $name));
+    }
+
+    return $this->contextDefinitions[$name];
+  }
+
+  /**
+   * Implements \Drupal\Component\Plugin\PluginContextDefinitionInterface::hasContextDefinition().
+   */
+  public function hasContextDefinition($name) {
+    return isset($this->contextDefinitions[$name]);
+  }
+
+}
diff --git a/src/PluginDefinition/PluginDefinition.php b/src/PluginDefinition/PluginDefinition.php
new file mode 100644
index 0000000..1120add
--- /dev/null
+++ b/src/PluginDefinition/PluginDefinition.php
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Plugin\PluginDefinition.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Provides a plugin definition.
+ *
+ * @ingroup Plugin
+ */
+abstract class PluginDefinition implements PluginDefinitionInterface {
+
+  use MergeablePluginDefinitionTrait;
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The class.
+   *
+   * @var string
+   *   A fully qualified class name.
+   */
+  protected $class;
+
+  /**
+   * The plugin provider.
+   *
+   * @var string|null
+   */
+  protected $provider;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setId($id) {
+    $this->id = $id;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getId() {
+    return $this->id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setClass($class) {
+    PluginDefinitionValidator::validateClass($class);
+
+    $this->class = $class;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getClass() {
+    return $this->class;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setProvider($provider) {
+    $this->provider = $provider;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProvider() {
+    return $this->provider;
+  }
+
+}
diff --git a/src/PluginDefinition/PluginDefinitionDecoratorInterface.php b/src/PluginDefinition/PluginDefinitionDecoratorInterface.php
new file mode 100644
index 0000000..933655d
--- /dev/null
+++ b/src/PluginDefinition/PluginDefinitionDecoratorInterface.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\plugin\PluginDefinition\PluginDefinitionDecoratorInterface.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Defines a plugin definition decorator.
+ *
+ * As this interface cannot predict which plugin definitions its implementations
+ * can decorate, it is somewhat generic. When using this interface, developers
+ * are responsible for only using it with definitions that implementations
+ * support.
+ *
+ * @ingroup Plugin
+ */
+interface PluginDefinitionDecoratorInterface extends PluginDefinitionInterface {
+
+  /**
+   * Creates a new plugin definition that decorates another definition.
+   *
+   * @param mixed $decorated_plugin_definition
+   *   The plugin definition to decorate. The supported types depend on the
+   *   implementations of this method.
+   *
+   * @return static
+   */
+  public static function createFromDecoratedDefinition($decorated_plugin_definition);
+
+}
diff --git a/src/PluginDefinition/PluginDefinitionInterface.php b/src/PluginDefinition/PluginDefinitionInterface.php
new file mode 100644
index 0000000..aa78b67
--- /dev/null
+++ b/src/PluginDefinition/PluginDefinitionInterface.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\plugin\PluginDefinition\PluginDefinitionInterface.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Defines a plugin definition.
+ *
+ * @ingroup Plugin
+ */
+interface PluginDefinitionInterface {
+
+  /**
+   * Sets the plugin ID.
+   *
+   * @param string $id
+   *   The plugin ID.
+   *
+   * @return $this
+   */
+  public function setId($id);
+
+  /**
+   * Gets the plugin ID.
+   *
+   * @return string
+   *   The plugin ID.
+   */
+  public function getId();
+
+  /**
+   * Sets the class.
+   *
+   * @param string $class
+   *   A fully qualified class name.
+   *
+   * @return $this
+   *
+   * @throws \InvalidArgumentException
+   */
+  public function setClass($class);
+
+  /**
+   * Gets the class.
+   *
+   * @return string
+   *   A fully qualified class name.
+   */
+  public function getClass();
+
+  /**
+   * Sets the plugin provider.
+   *
+   * The provider is the name of the module that provides the plugin, or "core',
+   * or "component".
+   *
+   * @param string $provider
+   *   The provider.
+   *
+   * @return $this
+   */
+  public function setProvider($provider);
+
+  /**
+   * Gets the plugin provider.
+   *
+   * The provider is the name of the module that provides the plugin, or "core',
+   * or "component".
+   *
+   * @return string
+   *   The provider.
+   */
+  public function getProvider();
+
+  /**
+   * Merges another definition into this one, using the other for defaults.
+   *
+   * @param static $other_definition
+   *   The other definition to merge into $this. It will not override $this, but
+   *   be used to extract default values from instead.
+   *
+   * @return $this
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown if $other_definition is no instance of $this.
+   */
+  public function mergeDefaultDefinition(PluginDefinitionInterface $other_definition);
+
+  /**
+   * Merges another definition into this one, using the other for overrides.
+   *
+   * @param static $other_definition
+   *   The other definition to merge into $this. It will override any values
+   *   already set in $this.
+   *
+   * @return $this
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown if $other_definition is no instance of $this.
+   */
+  public function mergeOverrideDefinition(PluginDefinitionInterface $other_definition);
+
+}
diff --git a/src/PluginDefinition/PluginDefinitionValidator.php b/src/PluginDefinition/PluginDefinitionValidator.php
new file mode 100644
index 0000000..86cd2a1
--- /dev/null
+++ b/src/PluginDefinition/PluginDefinitionValidator.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Plugin\PluginDefinitionValidator.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+use Drupal\Component\Plugin\Context\ContextDefinitionInterface;
+use Drupal\Component\Plugin\Derivative\DeriverInterface;
+
+/**
+ * Provides plugin definition validation.
+ *
+ * @ingroup Plugin
+ */
+class PluginDefinitionValidator {
+
+  /**
+   * Validates a plugin class.
+   *
+   * @param string $class
+   *   A fully qualified class name.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the class is invalid.
+   */
+  public static function validateClass($class) {
+    if (!class_exists($class)) {
+      throw new \InvalidArgumentException(sprintf('Class %s does not exist.', $class));
+    }
+  }
+
+  /**
+   * Validates a plugin deriver class.
+   *
+   * @param string $class
+   *   A fully qualified class name.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the class is invalid.
+   */
+  public static function validateDeriverClass($class) {
+    static::validateClass($class);
+
+    if (!is_subclass_of($class, DeriverInterface::class)) {
+      throw new \InvalidArgumentException(sprintf('Plugin deriver class %s does not implement required %s.', $class, DeriverInterface::class));
+    }
+  }
+
+  /**
+   * Validates plugin context definitions.
+   *
+   * @param \Drupal\Component\Plugin\Context\ContextDefinitionInterface[] $context_definitions
+   *   The array of context definitions, keyed by context name.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the class is invalid.
+   */
+  public static function validateContextDefinitions(array $context_definitions) {
+    foreach ($context_definitions as $name => $context_definition) {
+      if (!($context_definition instanceof ContextDefinitionInterface)) {
+        $type = is_object($context_definition) ? get_class($context_definition) : gettype($context_definition);
+        throw new \InvalidArgumentException(sprintf('$context_definition[%s] (%s) does not implement required %s.', $name, $type, ContextDefinitionInterface::class));
+      }
+    }
+  }
+
+}
diff --git a/src/PluginDefinition/PluginDeriverDefinitionInterface.php b/src/PluginDefinition/PluginDeriverDefinitionInterface.php
new file mode 100644
index 0000000..fb2b475
--- /dev/null
+++ b/src/PluginDefinition/PluginDeriverDefinitionInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Plugin\PluginDeriverDefinitionInterface.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Defines a plugin definition that includes a deriver.
+ *
+ * @ingroup Plugin
+ */
+interface PluginDeriverDefinitionInterface extends PluginDefinitionInterface {
+
+  /**
+   * Sets the deriver class.
+   *
+   * @param string $class
+   *   The fully qualified name of a class that implements
+   *   \Drupal\Component\Plugin\Derivative\DeriverInterface.
+   *
+   * @return $this
+   *
+   * @throws \InvalidArgumentException
+   */
+  public function setDeriverClass($class);
+
+  /**
+   * Gets the deriver class.
+   *
+   * @return string|null
+   *   The fully qualified name of a class that implements
+   *   \Drupal\Component\Plugin\Derivative\DeriverInterface or null.
+   */
+  public function getDeriverClass();
+
+}
diff --git a/src/PluginDefinition/PluginDeriverDefinitionTrait.php b/src/PluginDefinition/PluginDeriverDefinitionTrait.php
new file mode 100644
index 0000000..d1f786a
--- /dev/null
+++ b/src/PluginDefinition/PluginDeriverDefinitionTrait.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Plugin\PluginDeriverDefinitionTrait.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Implements \Drupal\plugin\PluginDefinition\PluginDeriverDefinitionInterface.
+ *
+ * @ingroup Plugin
+ */
+trait PluginDeriverDefinitionTrait {
+
+  /**
+   * The deriver class.
+   *
+   * @var string
+   *   The fully qualified name of a class that implements
+   *   \Drupal\Component\Plugin\Derivative\DeriverInterface.
+   */
+  protected $deriverClass;
+
+  /**
+   * Implements \Drupal\plugin\PluginDefinition\PluginDeriverDefinitionInterface::setDeriverClass().
+   */
+  public function setDeriverClass($class) {
+    PluginDefinitionValidator::validateClass($class);
+
+    $this->deriverClass = $class;
+
+    return $this;
+  }
+
+  /**
+   * Implements \Drupal\plugin\PluginDefinition\PluginDeriverDefinitionInterface::getDeriverClass().
+   */
+  public function getDeriverClass() {
+    return $this->deriverClass;
+  }
+
+}
diff --git a/src/PluginDefinition/PluginDescriptionDefinitionInterface.php b/src/PluginDefinition/PluginDescriptionDefinitionInterface.php
new file mode 100644
index 0000000..a4a89eb
--- /dev/null
+++ b/src/PluginDefinition/PluginDescriptionDefinitionInterface.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Plugin\PluginDescriptionDefinitionInterface.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Defines a plugin definition that includes a description.
+ *
+ * @ingroup Plugin
+ */
+interface PluginDescriptionDefinitionInterface extends PluginDefinitionInterface {
+
+  /**
+   * Sets the human-readable plugin description.
+   *
+   * @param \Drupal\Core\StringTranslation\TranslationWrapper|string $description
+   *   The description.
+   *
+   * @return $this
+   */
+  public function setDescription($description);
+
+  /**
+   * Gets the human-readable plugin description.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslationWrapper|string|null
+   *   The description or NULL if there is none.
+   */
+  public function getDescription();
+
+}
diff --git a/src/PluginDefinition/PluginHierarchyDefinitionInterface.php b/src/PluginDefinition/PluginHierarchyDefinitionInterface.php
new file mode 100644
index 0000000..c53b795
--- /dev/null
+++ b/src/PluginDefinition/PluginHierarchyDefinitionInterface.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Plugin\PluginHierarchyDefinitionInterface.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Defines a plugin definition that defines a parent plugin.
+ *
+ * @ingroup Plugin
+ */
+interface PluginHierarchyDefinitionInterface extends PluginDefinitionInterface {
+
+  /**
+   * Sets the ID of the parent plugin.
+   *
+   * @param string $id
+   *   The ID.
+   *
+   * @return $this
+   */
+  public function setParentId($id);
+
+  /**
+   * Gets the ID of the parent plugin.
+   *
+   * @return string|null
+   *   The ID or NULL if there is no parent.
+   */
+  public function getParentId();
+
+}
diff --git a/src/PluginDefinition/PluginLabelDefinitionInterface.php b/src/PluginDefinition/PluginLabelDefinitionInterface.php
new file mode 100644
index 0000000..34b4acf
--- /dev/null
+++ b/src/PluginDefinition/PluginLabelDefinitionInterface.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Plugin\PluginLabelDefinitionInterface.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Defines a plugin definition that includes a label.
+ *
+ * @ingroup Plugin
+ */
+interface PluginLabelDefinitionInterface extends PluginDefinitionInterface {
+
+  /**
+   * Sets the human-readable plugin label.
+   *
+   * @param \Drupal\Core\StringTranslation\TranslationWrapper|string $label
+   *   The label.
+   *
+   * @return $this
+   */
+  public function setLabel($label);
+
+  /**
+   * Gets the human-readable plugin label.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslationWrapper|string|null
+   *   The label or NULL if there is none.
+   */
+  public function getLabel();
+
+}
diff --git a/src/PluginDefinition/PluginLabelDefinitionTrait.php b/src/PluginDefinition/PluginLabelDefinitionTrait.php
new file mode 100644
index 0000000..e6dda0b
--- /dev/null
+++ b/src/PluginDefinition/PluginLabelDefinitionTrait.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Plugin\PluginLabelDefinitionTrait.
+ */
+
+namespace Drupal\plugin\PluginDefinition;
+
+/**
+ * Implements \Drupal\Component\Plugin\PluginLabelDefinitionInterface.
+ *
+ * @ingroup Plugin
+ */
+trait PluginLabelDefinitionTrait {
+
+  /**
+   * The human-readable label.
+   *
+   * @var string|null
+   */
+  protected $label;
+
+  /**
+   * Implements \Drupal\Component\Plugin\PluginLabelDefinitionInterface::setLabel().
+   */
+  public function setLabel($label) {
+    $this->label = $label;
+
+    return $this;
+  }
+
+  /**
+   * Implements \Drupal\Component\Plugin\PluginLabelDefinitionInterface::getLabel().
+   */
+  public function getLabel() {
+    return $this->label;
+  }
+
+}
diff --git a/src/PluginDiscovery/PluginDiscoveryDecorator.php b/src/PluginDiscovery/PluginDiscoveryDecorator.php
index 1c3ca8d..642b2ae 100644
--- a/src/PluginDiscovery/PluginDiscoveryDecorator.php
+++ b/src/PluginDiscovery/PluginDiscoveryDecorator.php
@@ -10,12 +10,14 @@ namespace Drupal\plugin\PluginDiscovery;
 use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
 use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
 use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 
 /**
  * Decorates plugin discovery.
  */
 class PluginDiscoveryDecorator implements DiscoveryInterface, CachedDiscoveryInterface {
 
+  use DependencySerializationTrait;
   use DiscoveryTrait;
 
   /**
@@ -67,6 +69,8 @@ class PluginDiscoveryDecorator implements DiscoveryInterface, CachedDiscoveryInt
   /**
    * Processes the definitions from the decorated discovery.
    *
+   * Any changes to the decorated definitions should be performed here.
+   *
    * @param mixed[] $decorated_definitions
    *   The decorated plugin definitions, keyed by plugin ID.
    *
diff --git a/src/PluginDiscovery/TypedDefinitionEnsuringPluginDiscoveryDecorator.php b/src/PluginDiscovery/TypedDefinitionEnsuringPluginDiscoveryDecorator.php
new file mode 100644
index 0000000..d997f99
--- /dev/null
+++ b/src/PluginDiscovery/TypedDefinitionEnsuringPluginDiscoveryDecorator.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * Contains \Drupal\plugin\PluginDiscovery\TypedDefinitionEnsuringPluginDiscoveryDecorator.
+ */
+
+namespace Drupal\plugin\PluginDiscovery;
+
+use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
+use Drupal\plugin\PluginTypeInterface;
+
+/**
+ * Provides plugin discovery that ensures all definitions implement
+ * \Drupal\Component\Plugin\PluginDefinitionInterface.
+ */
+class TypedDefinitionEnsuringPluginDiscoveryDecorator extends PluginDiscoveryDecorator implements TypedDiscoveryInterface {
+
+  /**
+   * The type of the plugin definitions to decorate.
+   *
+   * @var \Drupal\plugin\PluginTypeInterface
+   */
+  protected $pluginType;
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param \Drupal\plugin\PluginTypeInterface $plugin_type
+   *   The plugin type of which to decorate definitions.
+   * @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface|NULL $decorated_discovery
+   *   The decorated discovery, or NULL to use the plugin type's default
+   *   discovery.
+   */
+  public function __construct(PluginTypeInterface $plugin_type, DiscoveryInterface $decorated_discovery = NULL) {
+    parent::__construct($decorated_discovery ?: $plugin_type->getPluginManager());
+    $this->pluginType = $plugin_type;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processDecoratedDefinitions(array $decorated_plugin_definitions) {
+    $processed_plugin_definitions = [];
+    foreach ($decorated_plugin_definitions as $plugin_id => $decorated_plugin_definition) {
+      $processed_plugin_definitions[$plugin_id] = $this->pluginType->ensureTypedPluginDefinition($decorated_plugin_definition);
+    }
+
+    return $processed_plugin_definitions;
+  }
+
+}
diff --git a/src/PluginDiscovery/TypedDiscoveryInterface.php b/src/PluginDiscovery/TypedDiscoveryInterface.php
new file mode 100644
index 0000000..4d3de81
--- /dev/null
+++ b/src/PluginDiscovery/TypedDiscoveryInterface.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\plugin\PluginDiscovery\TypedDiscoveryInterface.
+ */
+
+namespace Drupal\plugin\PluginDiscovery;
+
+use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
+
+/**
+ * An interface defining the minimum requirements of building a plugin
+ * discovery component.
+ *
+ * @ingroup plugin_api
+ */
+interface TypedDiscoveryInterface extends DiscoveryInterface {
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\plugin\PluginDefinition\PluginDefinitionInterface|NULL
+   *   A plugin definition, or NULL if the plugin ID is invalid and
+   *   $exception_on_invalid is FALSE.
+   */
+  public function getDefinition($plugin_id, $exception_on_invalid = TRUE);
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\plugin\PluginDefinition\PluginDefinitionInterface[]
+   *   An array of plugin definitions (empty array if no definitions were
+   *   found). Keys are plugin IDs.
+   */
+  public function getDefinitions();
+
+}
diff --git a/src/PluginHierarchyTrait.php b/src/PluginHierarchyTrait.php
new file mode 100644
index 0000000..fc6cc14
--- /dev/null
+++ b/src/PluginHierarchyTrait.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\plugin\PluginHierarchyTrait.
+ */
+
+namespace Drupal\plugin;
+
+use Drupal\plugin\PluginDefinition\PluginDefinitionInterface;
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionInterface;
+use Drupal\plugin\PluginDefinition\PluginHierarchyDefinitionInterface;
+use Drupal\plugin\PluginDiscovery\TypedDiscoveryInterface;
+
+/**
+ * Supports handling hierarchical plugins.
+ */
+trait PluginHierarchyTrait {
+
+  /**
+   * Returns a hierarchical plugin representation.
+   *
+   * @param \Drupal\plugin\PluginDiscovery\TypedDiscoveryInterface $plugin_discovery
+   *   The typed plugin discovery.
+   *
+   * @return array[]
+   *   A possibly infinitely nested associative array. Keys are plugin IDs and
+   *   values are arrays of similar structure as this method's return value.
+   */
+  protected function buildPluginHierarchy(TypedDiscoveryInterface $plugin_discovery) {
+    $parents = [];
+    $children = [];
+    $definitions = $plugin_discovery->getDefinitions();
+    uasort($definitions, array($this, 'sortHierarchicalPlugins'));
+    foreach ($definitions as $plugin_id => $plugin_definition) {
+      if ($plugin_definition instanceof PluginHierarchyDefinitionInterface && $plugin_definition->getParentId()) {
+        $children[$plugin_definition->getParentId()][] = $plugin_id;
+      }
+      else {
+        $parents[] = $plugin_id;
+      }
+    }
+
+    return $this->buildPluginHierarchyLevel($parents, $children);
+  }
+
+  /**
+   * Helper function for self::hierarchy().
+   *
+   * @param array $parent_plugin_ids
+   *   An array with IDs of plugins that are part of the same hierarchy level.
+   * @param array $child_plugin_ids
+   *   Keys are plugin IDs. Values are arrays with those plugin's child
+   *   plugin IDs.
+   *
+   * @return array[]
+   *   The return value is identical to that of self::hierarchy().
+   */
+  protected function buildPluginHierarchyLevel(array $parent_plugin_ids, array $child_plugin_ids) {
+    $hierarchy = [];
+    foreach ($parent_plugin_ids as $plugin_id) {
+      $hierarchy[$plugin_id] = isset($child_plugin_ids[$plugin_id]) ? $this->buildPluginHierarchyLevel($child_plugin_ids[$plugin_id], $child_plugin_ids) : [];
+    }
+
+    return $hierarchy;
+  }
+
+  /**
+   * Implements uasort() callback to sort plugin definitions by label.
+   */
+  protected function sortHierarchicalPlugins(PluginDefinitionInterface $definition_a, PluginDefinitionInterface $definition_b) {
+    $label_a = $definition_a instanceof PluginLabelDefinitionInterface ? $definition_a->getLabel() : $definition_a->getId();
+    $label_b = $definition_b instanceof PluginLabelDefinitionInterface ? $definition_b->getLabel() : $definition_b->getId();
+
+    return strcmp($label_a, $label_b);
+  }
+}
diff --git a/src/PluginManager/PluginManagerDecorator.php b/src/PluginManager/PluginManagerDecorator.php
index b68ee19..a45192f 100644
--- a/src/PluginManager/PluginManagerDecorator.php
+++ b/src/PluginManager/PluginManagerDecorator.php
@@ -17,8 +17,6 @@ use Drupal\plugin\PluginDiscovery\PluginDiscoveryDecorator;
  */
 class PluginManagerDecorator extends PluginDiscoveryDecorator implements PluginManagerInterface {
 
-  use DependencySerializationTrait;
-
   /**
    * The decorated plugin factory.
    *
diff --git a/src/PluginType.php b/src/PluginType.php
index 111dbf7..fd55ed6 100644
--- a/src/PluginType.php
+++ b/src/PluginType.php
@@ -12,7 +12,7 @@ use Drupal\Core\DependencyInjection\ClassResolverInterface;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\StringTranslation\TranslationWrapper;
-use Drupal\plugin\Plugin\DefaultPluginDefinitionMapper;
+use Drupal\plugin\PluginDefinition\PluginDefinitionInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -58,11 +58,16 @@ class PluginType implements PluginTypeInterface {
   protected $operationsProvider;
 
   /**
-   * The plugin definition mapper.
+   * The plugin definition decorator class.
    *
-   * @var \Drupal\plugin\Plugin\PluginDefinitionMapperInterface
+   * @var string|null
+   *   A class that implements
+   *   \Drupal\plugin\PluginDefinition\PluginDefinitionDecoratorInterface or
+   *   NULL if definitions of plugins of this type do not have to be decorated
+   *   (e.g. already implement
+   *   \Drupal\plugin\PluginDefinition\PluginDefinitionInterface).
    */
-  protected $pluginDefinitionMapper;
+  protected $pluginDefinitionDecoratorClass;
 
   /**
    * The plugin type provider.
@@ -101,9 +106,10 @@ class PluginType implements PluginTypeInterface {
     if (array_key_exists('field_type', $definition)) {
       $this->fieldType = $definition['field_type'];
     }
-    $plugin_definition_mapper_class = isset($definition['plugin_definition_mapper_class']) ? $definition['plugin_definition_mapper_class'] : DefaultPluginDefinitionMapper::class;
-    $this->pluginDefinitionMapper = new $plugin_definition_mapper_class();
-    $operations_provider_class = isset($definition['operations_provider_class']) ? $definition['operations_provider_class'] : DefaultPluginTypeOperationsProvider::class;
+    if (array_key_exists('plugin_definition_decorator_class', $definition)) {
+      $this->pluginDefinitionDecoratorClass = $definition['plugin_definition_decorator_class'];
+    }
+    $operations_provider_class = array_key_exists('operations_provider_class', $definition) ? $definition['operations_provider_class'] : DefaultPluginTypeOperationsProvider::class;
     $this->operationsProvider = $class_resolver->getInstanceFromDefinition($operations_provider_class);
     $this->pluginManager = $plugin_manager;
     $this->provider = $definition['provider'];
@@ -154,8 +160,17 @@ class PluginType implements PluginTypeInterface {
   /**
    * {@inheritdoc}
    */
-  public function getPluginDefinitionMapper() {
-    return $this->pluginDefinitionMapper;
+  public function ensureTypedPluginDefinition($plugin_definition) {
+    if ($this->pluginDefinitionDecoratorClass && !($plugin_definition instanceof $this->pluginDefinitionDecoratorClass)) {
+      $plugin_definition_decorator_class = $this->pluginDefinitionDecoratorClass;
+      return $plugin_definition_decorator_class::createFromDecoratedDefinition($plugin_definition);
+    }
+    elseif ($plugin_definition instanceof PluginDefinitionInterface) {
+      return $plugin_definition;
+    }
+    else {
+      throw new \Exception(sprintf('A plugin definition of plugin type %s does not implement required %s, but its type also does not specify a plugin definition decorator.', $this->getId(), PluginDefinitionInterface::class));
+    }
   }
 
   /**
diff --git a/src/PluginTypeInterface.php b/src/PluginTypeInterface.php
index 9db5116..721af23 100644
--- a/src/PluginTypeInterface.php
+++ b/src/PluginTypeInterface.php
@@ -62,11 +62,18 @@ interface PluginTypeInterface {
   public function getPluginManager();
 
   /**
-   * Gets the plugin definition mapper.
+   * Ensures that a plugin definition is typed.
    *
-   * @return \Drupal\plugin\Plugin\PluginDefinitionMapperInterface
+   * @param \Drupal\plugin\PluginDefinition\PluginDefinitionInterface|mixed $plugin_definition
+   *   An original plugin definition of this type. It may already be typed.
+   *
+   * @return \Drupal\plugin\PluginDefinition\PluginDefinitionInterface
+   *   The typed plugin definition.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when a typed definition could not be returned.
    */
-  public function getPluginDefinitionMapper();
+  public function ensureTypedPluginDefinition($plugin_definition);
 
   /**
    * Gets the operations provider.
diff --git a/src/PluginTypeManager.php b/src/PluginTypeManager.php
index ab6d3c4..55ef073 100644
--- a/src/PluginTypeManager.php
+++ b/src/PluginTypeManager.php
@@ -9,7 +9,6 @@ namespace Drupal\plugin;
 
 use Drupal\Component\Discovery\YamlDiscovery;
 use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\plugin\Plugin\DefaultPluginDefinitionMapper;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -32,16 +31,6 @@ class PluginTypeManager implements PluginTypeManagerInterface {
   protected $moduleHandler;
 
   /**
-   * Plugin type definition defaults.
-   *
-   * @var mixed[]
-   */
-  protected $pluginTypeDefinitionDefaults = [
-    'class' => PluginType::class,
-    'plugin_definition_mapper_class' => DefaultPluginDefinitionMapper::class,
-  ];
-
-  /**
    * The known plugin types.
    *
    * @var \Drupal\plugin\PluginTypeInterface[]|null
@@ -97,15 +86,16 @@ class PluginTypeManager implements PluginTypeManagerInterface {
       // For every definition, set defaults and instantiate an object.
       foreach ($plugin_type_definitions_by_module as $module => $plugin_type_definitions) {
         $plugin_type_definition_defaults = [
-            'provider' => $module
-          ] + $this->pluginTypeDefinitionDefaults;
+          'provider' => $module
+        ];
         foreach ($plugin_type_definitions as $plugin_type_id => $plugin_type_definition) {
           $plugin_type_definition += $plugin_type_definition_defaults;
           if ($plugin_type_definition['provider'] == 'core' || $this->moduleHandler->moduleExists($plugin_type_definition['provider'])) {
             $plugin_type_definition['id'] = $plugin_type_id;
             /** @var \Drupal\plugin\PluginTypeInterface $class */
-            $class = $plugin_type_definition['class'];
-            $this->pluginTypes[$plugin_type_id] = $class::createFromDefinition($this->container, $plugin_type_definition);
+            $class = isset($plugin_type_definition['class']) ? $plugin_type_definition['class'] : PluginType::class;
+            $plugin_type = $class::createFromDefinition($this->container, $plugin_type_definition);
+            $this->pluginTypes[$plugin_type_id] = $plugin_type;
           }
         }
       }
diff --git a/tests/src/Unit/Controller/ListPluginsTest.php b/tests/src/Unit/Controller/ListPluginsTest.php
index a87ce8d..0f0bc9e 100644
--- a/tests/src/Unit/Controller/ListPluginsTest.php
+++ b/tests/src/Unit/Controller/ListPluginsTest.php
@@ -11,7 +11,10 @@ use Drupal\Component\Plugin\PluginManagerInterface;
 use Drupal\Core\DependencyInjection\ClassResolverInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\plugin\Controller\ListPlugins;
+use Drupal\plugin\PluginDefinition\PluginDescriptionDefinitionInterface;
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionInterface;
 use Drupal\plugin\PluginType;
+use Drupal\plugin\PluginTypeInterface;
 use Drupal\plugin\PluginTypeManagerInterface;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -121,30 +124,30 @@ class ListPluginsTest extends UnitTestCase {
    * @covers ::execute
    */
   public function testExecute() {
-    $class_resolver = $this->getMock(ClassResolverInterface::class);
-
     $plugin_manager = $this->getMock(PluginManagerInterface::class);
 
-    $plugin_id_a = $this->randomMachineName();
-    $plugin_label_a = $this->randomMachineName();
-    $plugin_description_a = $this->randomMachineName();
-    $plugin_definition_a = [
-      'id' => $plugin_id_a,
-      'label' => $plugin_label_a,
-      'description' => $plugin_description_a,
-    ];
-    $plugin_id_b = $this->randomMachineName();
-    $plugin_label_b = $this->randomMachineName();
-    $plugin_description_b = $this->randomMachineName();
-    $plugin_definition_b = [
-      'id' => $plugin_id_b,
-      'label' => $plugin_label_b,
-      'description' => $plugin_description_b,
-    ];
+    $plugin_definition_id_a = $this->randomMachineName();
+    $plugin_definition_label_a = $this->randomMachineName();
+    $plugin_definition_a = $this->getMock(PluginLabelDefinitionInterface::class);
+    $plugin_definition_a->expects($this->atLeastOnce())
+      ->method('getId')
+      ->willReturn($plugin_definition_id_a);
+    $plugin_definition_a->expects($this->atLeastOnce())
+      ->method('getLabel')
+      ->willReturn($plugin_definition_label_a);
+    $plugin_definition_id_b = $this->randomMachineName();
+    $plugin_definition_description_b = $this->randomMachineName();
+    $plugin_definition_b = $this->getMock(PluginDescriptionDefinitionInterface::class);
+    $plugin_definition_b->expects($this->atLeastOnce())
+      ->method('getId')
+      ->willReturn($plugin_definition_id_b);
+    $plugin_definition_b->expects($this->atLeastOnce())
+      ->method('getDescription')
+      ->willReturn($plugin_definition_description_b);
 
     $plugin_definitions = [
-      $plugin_id_a => $plugin_definition_a,
-      $plugin_id_b => $plugin_definition_b,
+      $plugin_definition_id_a => $plugin_definition_a,
+      $plugin_definition_id_b => $plugin_definition_b,
     ];
 
     $plugin_manager->expects($this->atLeastOnce())
@@ -153,12 +156,13 @@ class ListPluginsTest extends UnitTestCase {
 
     $plugin_type_id = $this->randomMachineName();
 
-    $plugin_type_definition = [
-      'id' => $plugin_type_id,
-      'label' => $this->randomMachineName(),
-      'provider' => $this->randomMachineName(),
-    ];
-    $plugin_type = new PluginType($plugin_type_definition, $this->stringTranslation, $class_resolver, $plugin_manager);
+    $plugin_type = $this->getMock(PluginTypeInterface::class);
+    $plugin_type->expects($this->atLeastOnce())
+      ->method('ensureTypedPluginDefinition')
+      ->willReturnArgument(0);
+    $plugin_type->expects($this->atLeastOnce())
+      ->method('getPluginManager')
+      ->willReturn($plugin_manager);
 
     $this->pluginTypeManager->expects($this->atLeastOnce())
       ->method('getPluginType')
@@ -171,10 +175,10 @@ class ListPluginsTest extends UnitTestCase {
 
     $build = $this->sut->execute($plugin_type_id);
 
-    $this->assertSame((string) $build[$plugin_id_a]['label']['#markup'], $plugin_label_a);
-    $this->assertSame((string) $build[$plugin_id_a]['description']['#markup'], $plugin_description_a);
-    $this->assertSame((string) $build[$plugin_id_b]['label']['#markup'], $plugin_label_b);
-    $this->assertSame((string) $build[$plugin_id_b]['description']['#markup'], $plugin_description_b);
+    $this->assertSame($plugin_definition_label_a, (string) $build[$plugin_definition_id_a]['label']['#markup']);
+    $this->assertNull($build[$plugin_definition_id_a]['description']['#markup']);
+    $this->assertSame($plugin_definition_id_b, (string) $build[$plugin_definition_id_b]['label']['#markup']);
+    $this->assertSame($plugin_definition_description_b, (string) $build[$plugin_definition_id_b]['description']['#markup']);
   }
 
 }
diff --git a/tests/src/Unit/Plugin/BlockPluginDefinitionMapperTest.php b/tests/src/Unit/Plugin/BlockPluginDefinitionMapperTest.php
deleted file mode 100644
index 4274dc6..0000000
--- a/tests/src/Unit/Plugin/BlockPluginDefinitionMapperTest.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Tests\plugin\Unit\Plugin\Plugin\BlockPluginDefinitionMapperTest.
- */
-
-namespace Drupal\Tests\plugin\Unit\Plugin;
-
-use Drupal\plugin\Plugin\BlockPluginDefinitionMapper;
-use Drupal\Tests\UnitTestCase;
-
-/**
- * @coversDefaultClass \Drupal\plugin\Plugin\BlockPluginDefinitionMapper
- *
- * @group Plugin
- */
-class BlockPluginDefinitionMapperTest extends UnitTestCase {
-
-  /**
-   * The plugin definition.
-   *
-   * @var mixed[]
-   */
-  protected $pluginDefinition;
-
-  /**
-   * The class under test.
-   *
-   * @var \Drupal\plugin\Plugin\BlockPluginDefinitionMapper
-   */
-  protected $sut;
-
-  public function setUp() {
-    $this->pluginDefinition = [
-      'admin_label' => $this->getRandomGenerator()->string(),
-    ];
-
-    $this->sut = new BlockPluginDefinitionMapper();
-  }
-
-  /**
-   * @covers ::getPluginLabel
-   */
-  public function testGetPluginLabel() {
-    $this->assertSame($this->pluginDefinition['admin_label'], $this->sut->getPluginLabel($this->pluginDefinition));
-
-    unset($this->pluginDefinition['admin_label']);
-
-    $this->assertNull($this->sut->getPluginLabel($this->pluginDefinition));
-  }
-
-}
diff --git a/tests/src/Unit/Plugin/DefaultPluginDefinitionMapperTest.php b/tests/src/Unit/Plugin/DefaultPluginDefinitionMapperTest.php
deleted file mode 100644
index 12b29cf..0000000
--- a/tests/src/Unit/Plugin/DefaultPluginDefinitionMapperTest.php
+++ /dev/null
@@ -1,106 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Tests\plugin\Unit\Plugin\Plugin\DefaultPluginDefinitionMapperTest.
- */
-
-namespace Drupal\Tests\plugin\Unit\Plugin;
-
-use Drupal\plugin\Plugin\DefaultPluginDefinitionMapper;
-use Drupal\Tests\UnitTestCase;
-
-/**
- * @coversDefaultClass \Drupal\plugin\Plugin\DefaultPluginDefinitionMapper
- *
- * @group Plugin
- */
-class DefaultPluginDefinitionMapperTest extends UnitTestCase {
-
-  /**
-   * The plugin definition.
-   *
-   * @var mixed[]
-   */
-  protected $pluginDefinition;
-
-  /**
-   * The class under test.
-   *
-   * @var \Drupal\plugin\Plugin\DefaultPluginDefinitionMapper
-   */
-  protected $sut;
-
-  public function setUp() {
-    $this->pluginDefinition = [
-      'id' => $this->randomMachineName(),
-      'parent_id' => $this->randomMachineName(),
-      'label' => $this->getRandomGenerator()->string(),
-      'description' => $this->getRandomGenerator()->string(),
-      'foo' => $this->getRandomGenerator()->string(),
-    ];
-
-    $this->sut = new DefaultPluginDefinitionMapper();
-  }
-
-  /**
-   * @covers ::getPluginId
-   */
-  public function testGetPluginId() {
-    $this->assertSame($this->pluginDefinition['id'], $this->sut->getPluginId($this->pluginDefinition));
-  }
-
-  /**
-   * @covers ::getParentPluginId
-   */
-  public function testGetParentPluginId() {
-    $this->assertSame($this->pluginDefinition['parent_id'], $this->sut->getParentPluginId($this->pluginDefinition));
-  }
-
-  /**
-   * @covers ::getPluginLabel
-   */
-  public function testGetPluginLabel() {
-    $this->assertSame($this->pluginDefinition['label'], $this->sut->getPluginLabel($this->pluginDefinition));
-
-    unset($this->pluginDefinition['label']);
-
-    $this->assertNull($this->sut->getPluginLabel($this->pluginDefinition));
-  }
-
-  /**
-   * @covers ::getPluginDescription
-   */
-  public function testGetPluginDescription() {
-    $this->assertSame($this->pluginDefinition['description'], $this->sut->getPluginDescription($this->pluginDefinition));
-
-    unset($this->pluginDefinition['description']);
-
-    $this->assertNull($this->sut->getPluginDescription($this->pluginDefinition));
-  }
-
-  /**
-   * @covers ::hasPluginDefinitionProperty
-   */
-  public function testHasPluginDefinitionProperty() {
-    $this->assertTrue($this->sut->hasPluginDefinitionProperty($this->pluginDefinition, 'foo'));
-    $this->assertFalse($this->sut->hasPluginDefinitionProperty($this->pluginDefinition, $this->randomMachineName()));
-  }
-
-  /**
-   * @covers ::getPluginDefinitionProperty
-   */
-  public function testGetPluginDefinitionPropertyWithExistingProperty() {
-    $this->assertSame($this->pluginDefinition['foo'], $this->sut->getPluginDefinitionProperty($this->pluginDefinition, 'foo'));
-  }
-
-  /**
-   * @covers ::getPluginDefinitionProperty
-   *
-   * @expectedException \InvalidArgumentException
-   */
-  public function testGetPluginDefinitionPropertyWithNonExistingProperty() {
-    $this->sut->getPluginDefinitionProperty($this->pluginDefinition, $this->randomMachineName());
-  }
-
-}
diff --git a/tests/src/Unit/Plugin/Field/FieldFormatter/PluginLabelTest.php b/tests/src/Unit/Plugin/Field/FieldFormatter/PluginLabelTest.php
index 9aaf9cb..2334269 100644
--- a/tests/src/Unit/Plugin/Field/FieldFormatter/PluginLabelTest.php
+++ b/tests/src/Unit/Plugin/Field/FieldFormatter/PluginLabelTest.php
@@ -10,10 +10,11 @@ namespace Drupal\Tests\plugin\Unit\Plugin\Field\FieldFormatter;
 
 use Drupal\Component\Plugin\PluginInspectionInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\plugin\Plugin\DefaultPluginDefinitionMapper;
 use Drupal\plugin\Plugin\Field\FieldFormatter\PluginLabel;
 use Drupal\plugin\Plugin\Field\FieldType\PluginCollectionItemInterface;
 use Drupal\plugin\Plugin\Field\FieldType\PluginCollectionItemList;
+use Drupal\plugin\PluginDefinition\PluginDefinitionInterface;
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionInterface;
 use Drupal\plugin\PluginTypeInterface;
 use Drupal\Tests\UnitTestCase;
 
@@ -51,16 +52,17 @@ class PluginLabelTest extends UnitTestCase {
    * @covers ::viewElements
    */
   public function testViewElements() {
-    $plugin_label_a = $this->randomMachineName();
-    $plugin_label_b = $this->randomMachineName();
-
-    $plugin_definition_a = [
-      'label' => $plugin_label_a,
-    ];
-
-    $plugin_definition_b = [
-      'label' => $plugin_label_b,
-    ];
+    $plugin_definition_label_a = $this->randomMachineName();
+    $plugin_definition_a = $this->getMock(PluginLabelDefinitionInterface::class);
+    $plugin_definition_a->expects($this->atLeastOnce())
+      ->method('getLabel')
+      ->willReturn($plugin_definition_label_a);
+
+    $plugin_definition_id_b = $this->randomMachineName();
+    $plugin_definition_b = $this->getMock(PluginDefinitionInterface::class);
+    $plugin_definition_b->expects($this->atLeastOnce())
+      ->method('getId')
+      ->willReturn($plugin_definition_id_b);
 
     $plugin_instance_a = $this->getMock(PluginInspectionInterface::class);
     $plugin_instance_a->expects($this->atLeastOnce())
@@ -85,12 +87,10 @@ class PluginLabelTest extends UnitTestCase {
     /** @var \Drupal\plugin\Plugin\Field\FieldType\PluginCollectionItemInterface[]|\PHPUnit_Framework_MockObject_MockObject[] $items */
     $items = [$item_a, $item_b];
 
-    $plugin_definition_mapper = new DefaultPluginDefinitionMapper();
-
     $plugin_type = $this->getMock(PluginTypeInterface::class);
     $plugin_type->expects($this->atLeastOnce())
-      ->method('getPluginDefinitionMapper')
-      ->willReturn($plugin_definition_mapper);
+      ->method('ensureTypedPluginDefinition')
+      ->willReturnArgument(0);
 
     foreach ($items as $item) {
       $item->expects($this->atLeastOnce())
@@ -109,10 +109,10 @@ class PluginLabelTest extends UnitTestCase {
 
     $expected_build = [
       [
-        '#markup' => $plugin_label_a,
+        '#markup' => $plugin_definition_label_a,
       ],
       [
-        '#markup' => $plugin_label_b,
+        '#markup' => $plugin_definition_id_b,
       ],
     ];
 
diff --git a/tests/src/Unit/Plugin/PluginSelector/PluginSelector/AdvancedPluginSelectorBaseTest.php b/tests/src/Unit/Plugin/PluginSelector/PluginSelector/AdvancedPluginSelectorBaseTest.php
index 8b91775..6f42cdd 100644
--- a/tests/src/Unit/Plugin/PluginSelector/PluginSelector/AdvancedPluginSelectorBaseTest.php
+++ b/tests/src/Unit/Plugin/PluginSelector/PluginSelector/AdvancedPluginSelectorBaseTest.php
@@ -174,7 +174,7 @@ class AdvancedPluginSelectorBaseTest extends PluginSelectorBaseTestBase {
     $plugin = $this->getMock(PluginInspectionInterface::class);
 
     $plugin_definitions = [
-      [
+      $plugin_id => [
         'id' => $plugin_id,
       ],
     ];
@@ -218,10 +218,10 @@ class AdvancedPluginSelectorBaseTest extends PluginSelectorBaseTestBase {
     $plugin_b = $this->getMock(PluginInspectionInterface::class);
 
     $plugin_definitions = [
-      [
+      $plugin_id_a => [
         'id' => $plugin_id_a,
       ],
-      [
+      $plugin_id_b => [
         'id' => $plugin_id_b,
       ],
     ];
@@ -230,7 +230,7 @@ class AdvancedPluginSelectorBaseTest extends PluginSelectorBaseTestBase {
       [$plugin_id_a, [], $plugin_a],
       [$plugin_id_b, [], $plugin_b],
     ];
-    $this->selectablePluginManager->expects($this->any())
+    $this->selectablePluginManager->expects($this->atLeastOnce())
       ->method('createInstance')
       ->willReturnMap($map);
     $this->selectablePluginManager->expects($this->any())
diff --git a/tests/src/Unit/Plugin/PluginSelector/PluginSelector/PluginSelectorBaseTestBase.php b/tests/src/Unit/Plugin/PluginSelector/PluginSelector/PluginSelectorBaseTestBase.php
index 299c5d5..3ed7a91 100644
--- a/tests/src/Unit/Plugin/PluginSelector/PluginSelector/PluginSelectorBaseTestBase.php
+++ b/tests/src/Unit/Plugin/PluginSelector/PluginSelector/PluginSelectorBaseTestBase.php
@@ -9,9 +9,7 @@ namespace Drupal\Tests\plugin\Unit\Plugin\PluginSelector\PluginSelector;
 
 use Drupal\Component\Plugin\PluginInspectionInterface;
 use Drupal\Component\Plugin\PluginManagerInterface;
-use Drupal\Core\DependencyInjection\ClassResolverInterface;
-use Drupal\plugin\Plugin\PluginDefinitionMapperInterface;
-use Drupal\plugin\PluginType;
+use Drupal\plugin\PluginTypeInterface;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -28,13 +26,6 @@ abstract class PluginSelectorBaseTestBase extends UnitTestCase {
   protected $pluginDefinition = [];
 
   /**
-   * The plugin definition mapper.
-   *
-   * @var \Drupal\plugin\Plugin\PluginDefinitionMapperInterface
-   */
-  protected $pluginDefinitionMapper;
-
-  /**
    * The plugin ID of the class plugin under test.
    *
    * @var array
@@ -74,20 +65,14 @@ abstract class PluginSelectorBaseTestBase extends UnitTestCase {
    *
    */
   public function setUp() {
-    $this->pluginDefinitionMapper = $this->getMock(PluginDefinitionMapperInterface::class);
-
     $this->pluginId = $this->randomMachineName();
 
-    $class_resolver = $this->getMock(ClassResolverInterface::class);
-
     $this->selectablePluginManager = $this->getMock(PluginManagerInterface::class);
 
-    $plugin_type_definition = [
-      'id' => $this->randomMachineName(),
-      'label' => $this->randomMachineName(),
-      'provider' => $this->randomMachineName(),
-    ];
-    $this->selectablePluginType = new PluginType($plugin_type_definition, $this->getStringTranslationStub(), $class_resolver, $this->selectablePluginManager);
+    $this->selectablePluginType = $this->getMock(PluginTypeInterface::class);
+    $this->selectablePluginType->expects($this->any())
+      ->method('getPluginManager')
+      ->willReturn($this->selectablePluginManager);
 
     $this->selectedPlugin = $this->getMock(PluginInspectionInterface::class);
   }
diff --git a/tests/src/Unit/Plugin/PluginSelector/PluginSelector/RadiosTest.php b/tests/src/Unit/Plugin/PluginSelector/PluginSelector/RadiosTest.php
index 1e03abb..0c40841 100644
--- a/tests/src/Unit/Plugin/PluginSelector/PluginSelector/RadiosTest.php
+++ b/tests/src/Unit/Plugin/PluginSelector/PluginSelector/RadiosTest.php
@@ -13,6 +13,7 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\PageCache\ResponsePolicy\KillSwitch;
 use Drupal\plugin\Plugin\Plugin\PluginSelector\AdvancedPluginSelectorBase;
 use Drupal\plugin\Plugin\Plugin\PluginSelector\Radios;
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionInterface;
 
 /**
  * @coversDefaultClass \Drupal\plugin\Plugin\Plugin\PluginSelector\Radios
@@ -87,10 +88,10 @@ class RadiosTest extends PluginSelectorBaseTestBase {
 
     $plugin_id = $this->randomMachineName();
     $plugin_label = $this->randomMachineName();
-    $plugin_definition = [
-      'id' => $plugin_id,
-      'label' => $plugin_label,
-    ];
+    $plugin_definition = $this->getMock(PluginLabelDefinitionInterface::class);
+    $plugin_definition->expects($this->atLeastOnce())
+      ->method('getLabel')
+      ->willReturn($plugin_label);
     $plugin = $this->getMock(PluginInspectionInterface::class);
     $plugin->expects($this->atLeastOnce())
       ->method('getPluginDefinition')
@@ -99,9 +100,9 @@ class RadiosTest extends PluginSelectorBaseTestBase {
       ->method('getPluginId')
       ->willReturn($plugin_id);
 
-    $this->pluginDefinitionMapper->expects($this->any())
-      ->method('getPluginLabel')
-      ->willReturn($plugin_label);
+    $this->selectablePluginType->expects($this->atLeastOnce())
+      ->method('ensureTypedPluginDefinition')
+      ->willReturnArgument(0);
 
     $this->sut->setSelectedPlugin($plugin);
     $selector_title = $this->randomMachineName();
diff --git a/tests/src/Unit/Plugin/PluginSelector/PluginSelector/SelectListTest.php b/tests/src/Unit/Plugin/PluginSelector/PluginSelector/SelectListTest.php
index f5f752e..d81e5ae 100644
--- a/tests/src/Unit/Plugin/PluginSelector/PluginSelector/SelectListTest.php
+++ b/tests/src/Unit/Plugin/PluginSelector/PluginSelector/SelectListTest.php
@@ -12,6 +12,8 @@ use Drupal\Component\Plugin\PluginInspectionInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\PageCache\ResponsePolicy\KillSwitch;
 use Drupal\plugin\Plugin\Plugin\PluginSelector\SelectList;
+use Drupal\plugin\PluginDefinition\PluginDefinitionInterface;
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionInterface;
 
 /**
  * @coversDefaultClass \Drupal\plugin\Plugin\Plugin\PluginSelector\SelectList
@@ -53,16 +55,17 @@ class SelectListTest extends PluginSelectorBaseTestBase {
 
     $this->stringTranslation = $this->getStringTranslationStub();
 
+    $this->selectablePluginType->expects($this->any())
+      ->method('ensureTypedPluginDefinition')
+      ->willReturnArgument(0);
+
     $this->sut = new SelectList([], $this->pluginId, $this->pluginDefinition, $this->stringTranslation, $this->responsePolicy);
     $this->sut->setSelectablePluginType($this->selectablePluginType);
   }
 
   /**
    * @covers ::buildSelector
-   * @covers ::buildHierarchy
-   * @covers ::buildHierarchyLevel
    * @covers ::buildOptionsLevel
-   * @covers ::sort
    */
   public function testBuildSelector() {
     $this->stringTranslation->expects($this->any())
@@ -74,20 +77,19 @@ class SelectListTest extends PluginSelectorBaseTestBase {
 
     $plugin_id_a = $this->randomMachineName();
     $plugin_label_a = $this->randomMachineName();
-    $plugin_definition_a = [
-      'id' => $plugin_id_a,
-      'label' => $plugin_label_a,
-    ];
+    $plugin_definition_a = $this->getMock(PluginLabelDefinitionInterface::class);
+    $plugin_definition_a->expects($this->atLeastOnce())
+      ->method('getLabel')
+      ->willReturn($plugin_label_a);
     $plugin_a = $this->getMock(PluginInspectionInterface::class);
     $plugin_a->expects($this->atLeastOnce())
       ->method('getPluginId')
       ->willReturn($plugin_id_a);
     $plugin_id_b = $this->randomMachineName();
-    $plugin_label_b = $this->randomMachineName();
-    $plugin_definition_b = [
-      'id' => $plugin_id_b,
-      'label' => $plugin_label_b,
-    ];
+    $plugin_definition_b = $this->getMock(PluginDefinitionInterface::class);
+    $plugin_definition_b->expects($this->atLeastOnce())
+      ->method('getId')
+      ->willReturn($plugin_id_b);
     $plugin_b = $this->getMock(PluginInspectionInterface::class);
 
     $this->sut->setSelectedPlugin($plugin_a);
@@ -122,7 +124,7 @@ class SelectListTest extends PluginSelectorBaseTestBase {
       '#empty_value' => '',
       '#options' => array(
         $plugin_id_a => $plugin_label_a,
-        $plugin_id_b => $plugin_label_b,
+        $plugin_id_b => $plugin_id_b,
       ) ,
       '#required' => FALSE,
       '#title' => $selector_title,
diff --git a/tests/src/Unit/PluginDefinition/ArrayPluginDefinitionDecoratorTest.php b/tests/src/Unit/PluginDefinition/ArrayPluginDefinitionDecoratorTest.php
new file mode 100644
index 0000000..fb72947
--- /dev/null
+++ b/tests/src/Unit/PluginDefinition/ArrayPluginDefinitionDecoratorTest.php
@@ -0,0 +1,593 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\plugin\Unit\PluginDefinition\ArrayPluginDefinitionDecoratorTest.
+ */
+
+namespace Drupal\Tests\plugin\Unit\PluginDefinition;
+
+use Drupal\Component\Plugin\Derivative\DeriverInterface;
+use Drupal\Core\Plugin\Context\ContextDefinitionInterface;
+use Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator;
+use Drupal\plugin\PluginDefinition\PluginDefinitionInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
+ *
+ * @group Plugin
+ */
+class ArrayPluginDefinitionDecoratorTest extends UnitTestCase {
+
+  /**
+   * The array definition.
+   *
+   * @var mixed[]
+   */
+  protected $arrayDefinition = [];
+
+  /**
+   * The class under test.
+   *
+   * @var \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->arrayDefinition = [
+      'id' => $this->randomMachineName(),
+      'label' => $this->randomMachineName(),
+      'class' => $this->getMockClass(DeriverInterface::class),
+      'category' => $this->randomMachineName(),
+      'provider' => $this->randomMachineName(),
+      'deriver' => $this->getMockClass(DeriverInterface::class),
+      'context' => [
+        $this->randomMachineName() => $this->getMock(ContextDefinitionInterface::class),
+      ],
+      'config_dependencies' => [
+        'module' => [$this->randomMachineName()],
+      ],
+      'parent_id' => $this->randomMachineName(),
+    ];
+
+    $this->sut = new ArrayPluginDefinitionDecorator($this->arrayDefinition);
+  }
+
+  /**
+   * @covers ::__construct
+   */
+  public function testConstruct() {
+    $this->sut = new ArrayPluginDefinitionDecorator($this->arrayDefinition);
+  }
+
+  /**
+   * @covers ::getArrayDefinition
+   */
+  public function testGetArrayDefiniton() {
+    $this->assertSame($this->arrayDefinition, $this->sut->getArrayDefinition());
+  }
+
+  /**
+   * @covers ::setId
+   * @covers ::getId
+   * @covers ::offsetExists
+   * @covers ::offsetSet
+   * @covers ::offsetGet
+   * @covers ::offsetUnset
+   */
+  public function testGetId() {
+    // Test the injected value.
+    $this->assertSame($this->arrayDefinition['id'], $this->sut->getId());
+    $this->assertSame($this->arrayDefinition['id'], $this->sut->getArrayDefinition()['id']);
+    $this->assertSame($this->arrayDefinition['id'], $this->sut['id']);
+
+    // Test changing the value through the setter.
+    $value = $this->randomMachineName();
+    $this->assertSame($this->sut, $this->sut->setId($value));
+    $this->assertSame($value, $this->sut->getId());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['id']);
+    $this->assertSame($value, $this->sut['id']);
+
+    // Test changing the value through array access.
+    $value = $this->randomMachineName();
+    $this->sut['id'] = $value;
+    $this->assertSame($value, $this->sut->getId());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['id']);
+    $this->assertSame($value, $this->sut['id']);
+
+    // Test unsetting the value.
+    unset($this->sut['id']);
+    $this->assertFalse(isset($this->sut['id']));
+    $this->assertNull($this->sut->getId());
+    $this->assertFalse(isset($this->sut->getArrayDefinition()['id']));
+  }
+
+  /**
+   * @covers ::setLabel
+   * @covers ::getLabel
+   * @covers ::offsetExists
+   * @covers ::offsetSet
+   * @covers ::offsetGet
+   * @covers ::offsetUnset
+   */
+  public function testGetLabel() {
+    // Test the injected value.
+    $this->assertSame($this->arrayDefinition['label'], $this->sut->getLabel());
+    $this->assertSame($this->arrayDefinition['label'], $this->sut->getArrayDefinition()['label']);
+    $this->assertSame($this->arrayDefinition['label'], $this->sut['label']);
+
+    // Test changing the value through the setter.
+    $value = $this->randomMachineName();
+    $this->assertSame($this->sut, $this->sut->setLabel($value));
+    $this->assertSame($value, $this->sut->getLabel());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['label']);
+    $this->assertSame($value, $this->sut['label']);
+
+    // Test changing the value through array access.
+    $value = $this->randomMachineName();
+    $this->sut['label'] = $value;
+    $this->assertSame($value, $this->sut->getLabel());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['label']);
+    $this->assertSame($value, $this->sut['label']);
+
+    // Test unsetting the value.
+    unset($this->sut['label']);
+    $this->assertFalse(isset($this->sut['label']));
+    $this->assertNull($this->sut->getLabel());
+    $this->assertFalse(isset($this->sut->getArrayDefinition()['label']));
+  }
+
+  /**
+   * @covers ::setClass
+   * @covers ::getClass
+   * @covers ::offsetExists
+   * @covers ::offsetSet
+   * @covers ::offsetGet
+   * @covers ::offsetUnset
+   */
+  public function testGetClass() {
+    // Test the injected value.
+    $this->assertSame($this->arrayDefinition['class'], $this->sut->getClass());
+    $this->assertSame($this->arrayDefinition['class'], $this->sut->getArrayDefinition()['class']);
+    $this->assertSame($this->arrayDefinition['class'], $this->sut['class']);
+
+    // Test changing the value through the setter.
+    $value = '\stdClass';
+    $this->assertSame($this->sut, $this->sut->setClass($value));
+    $this->assertSame($value, $this->sut->getClass());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['class']);
+    $this->assertSame($value, $this->sut['class']);
+
+    // Test changing the value through array access.
+    $value = '\stdClass';
+    $this->sut['class'] = $value;
+    $this->assertSame($value, $this->sut->getClass());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['class']);
+    $this->assertSame($value, $this->sut['class']);
+
+    // Test unsetting the value.
+    unset($this->sut['class']);
+    $this->assertFalse(isset($this->sut['class']));
+    $this->assertNull($this->sut->getClass());
+    $this->assertFalse(isset($this->sut->getArrayDefinition()['class']));
+  }
+
+  /**
+   * @covers ::setDeriverClass
+   * @covers ::getDeriverClass
+   * @covers ::offsetExists
+   * @covers ::offsetSet
+   * @covers ::offsetGet
+   * @covers ::offsetUnset
+   */
+  public function testGetDeriverClass() {
+    // Test the injected value.
+    $this->assertSame($this->arrayDefinition['deriver'], $this->sut->getDeriverClass());
+    $this->assertSame($this->arrayDefinition['deriver'], $this->sut->getArrayDefinition()['deriver']);
+    $this->assertSame($this->arrayDefinition['deriver'], $this->sut['deriver']);
+
+    // Test changing the value through the setter.
+    $value = $this->getMockClass(DeriverInterface::class);
+    $this->assertSame($this->sut, $this->sut->setDeriverClass($value));
+    $this->assertSame($value, $this->sut->getDeriverClass());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['deriver']);
+    $this->assertSame($value, $this->sut['deriver']);
+
+    // Test changing the value through array access.
+    $value = $this->getMockClass(DeriverInterface::class);
+    $this->sut['deriver'] = $value;
+    $this->assertSame($value, $this->sut->getDeriverClass());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['deriver']);
+    $this->assertSame($value, $this->sut['deriver']);
+
+    // Test unsetting the value.
+    unset($this->sut['deriver']);
+    $this->assertFalse(isset($this->sut['deriver']));
+    $this->assertNull($this->sut->getDeriverClass());
+    $this->assertFalse(isset($this->sut->getArrayDefinition()['deriver']));
+  }
+
+  /**
+   * @covers ::setContextDefinitions
+   * @covers ::getContextDefinitions
+   * @covers ::offsetExists
+   * @covers ::offsetSet
+   * @covers ::offsetGet
+   * @covers ::offsetUnset
+   */
+  public function testGetContextDefinitions() {
+    // Test the injected value.
+    $this->assertSame($this->arrayDefinition['context'], $this->sut->getContextDefinitions());
+    $this->assertSame($this->arrayDefinition['context'], $this->sut->getArrayDefinition()['context']);
+    $this->assertSame($this->arrayDefinition['context'], $this->sut['context']);
+
+    // Test changing the value through the setter.
+    $context_definition_name_a = $this->randomMachineName();
+    $context_definition_a = $this->getMock(ContextDefinitionInterface::class);
+    $context_definition_name_b = $this->randomMachineName();
+    $context_definition_b = $this->getMock(ContextDefinitionInterface::class);
+    $value = [
+      $context_definition_name_a => $context_definition_a,
+      $context_definition_name_b => $context_definition_b,
+    ];
+    $this->assertSame($this->sut, $this->sut->setContextDefinitions($value));
+    $this->assertSame($value, $this->sut->getContextDefinitions());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['context']);
+    $this->assertSame($value, $this->sut['context']);
+
+    // Test changing the value through array access.
+    $context_definition_name_a = $this->randomMachineName();
+    $context_definition_a = $this->getMock(ContextDefinitionInterface::class);
+    $context_definition_name_b = $this->randomMachineName();
+    $context_definition_b = $this->getMock(ContextDefinitionInterface::class);
+    $value = [
+      $context_definition_name_a => $context_definition_a,
+      $context_definition_name_b => $context_definition_b,
+    ];
+    $this->sut['context'] = $value;
+    $this->assertSame($value, $this->sut->getContextDefinitions());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['context']);
+    $this->assertSame($value, $this->sut['context']);
+
+    // Test unsetting the value.
+    unset($this->sut['context']);
+    $this->assertFalse(isset($this->sut['context']));
+    $this->assertSame([], $this->sut->getContextDefinitions());
+    $this->assertFalse(isset($this->sut->getArrayDefinition()['context']));
+  }
+
+  /**
+   * @covers ::setContextDefinitions
+   * @covers ::offsetSet
+   *
+   * @depends testGetContextDefinitions
+   *
+   * @expectedException \InvalidArgumentException
+   */
+  public function testSetContextDefinitionsWithInvalidDefinition() {
+    $context_definitions = [
+      $this->randomMachineName() => new \stdClass(),
+    ];
+
+    $this->sut['context'] = $context_definitions;
+  }
+
+  /**
+   * @covers ::setContextDefinition
+   * @covers ::getContextDefinition
+   * @covers ::hasContextDefinition
+   */
+  public function testGetContextDefinition() {
+    $name = $this->randomMachineName();
+    $context_definition = $this->getMock(ContextDefinitionInterface::class);
+
+    $this->assertSame($this->sut, $this->sut->setContextDefinition($name, $context_definition));
+    $this->assertSame($context_definition, $this->sut->getContextDefinition($name));
+    $this->assertTrue($this->sut->hasContextDefinition($name));
+  }
+
+  /**
+   * @covers ::getContextDefinition
+   * @covers ::hasContextDefinition
+   *
+   * @depends testGetContextDefinition
+   *
+   * @expectedException \InvalidArgumentException
+   */
+  public function testGetContextDefinitionWithNonExistentDefinition() {
+    $name = $this->randomMachineName();
+
+    $this->assertFalse($this->sut->hasContextDefinition($name));
+    $this->sut->getContextDefinition($name);
+  }
+
+  /**
+   * @covers ::mergeDefaultArrayDefinition
+   */
+  public function testMergeDefaultArrayDefinition() {
+    $other_definition = [
+      'foo' => $this->randomMachineName(),
+      'label' => $this->randomMachineName(),
+    ];
+
+    $this->assertSame($this->sut, $this->sut->mergeDefaultArrayDefinition($other_definition));
+    $this->assertSame($other_definition['foo'], $this->sut->getArrayDefinition()['foo']);
+    $this->assertNotSame($other_definition['label'], $this->sut->getArrayDefinition()['label']);
+  }
+
+  /**
+   * @covers ::mergeDefaultDefinition
+   * @covers ::doMergeDefaultDefinition
+   * @covers ::validateMergeDefinition
+   * @covers ::isDefinitionCompatible
+   *
+   * @depends testMergeDefaultArrayDefinition
+   */
+  public function testMergeDefaultDefinition() {
+    $other_definition = new ArrayPluginDefinitionDecorator();
+    $other_definition['foo'] = $this->randomMachineName();
+    $other_definition['label'] = $this->randomMachineName();
+
+    $this->assertSame($this->sut, $this->sut->mergeDefaultDefinition($other_definition));
+    $this->assertSame($other_definition['foo'], $this->sut->getArrayDefinition()['foo']);
+    $this->assertNotSame($other_definition['label'], $this->sut->getArrayDefinition()['label']);
+  }
+
+  /**
+   * @covers ::mergeDefaultDefinition
+   * @covers ::validateMergeDefinition
+   * @covers ::isDefinitionCompatible
+   *
+   * @depends testMergeDefaultDefinition
+   *
+   * @expectedException \InvalidArgumentException
+   */
+  public function testMergeDefaultDefinitionWithInvalidOtherDefinition() {
+    $other_definition = $this->getMock(PluginDefinitionInterface::class);
+
+    $this->sut->mergeDefaultDefinition($other_definition);
+  }
+
+  /**
+   * @covers ::mergeOverrideArrayDefinition
+   */
+  public function testMergeOverrideArrayDefinition() {
+    $other_definition = [
+      'foo' => $this->randomMachineName(),
+      'label' => $this->randomMachineName(),
+    ];
+
+    $this->assertSame($this->sut, $this->sut->mergeOverrideArrayDefinition($other_definition));
+    $this->assertSame($other_definition['foo'], $this->sut->getArrayDefinition()['foo']);
+    $this->assertSame($other_definition['label'], $this->sut->getArrayDefinition()['label']);
+  }
+
+  /**
+   * @covers ::mergeOverrideDefinition
+   * @covers ::doMergeOverrideDefinition
+   * @covers ::validateMergeDefinition
+   * @covers ::isDefinitionCompatible
+   *
+   * @depends testMergeOverrideArrayDefinition
+   */
+  public function testMergeOverrideDefinition() {
+    $other_definition = new ArrayPluginDefinitionDecorator();
+    $other_definition['foo'] = $this->randomMachineName();
+    $other_definition['label'] = $this->randomMachineName();
+
+    $this->assertSame($this->sut, $this->sut->mergeOverrideDefinition($other_definition));
+    $this->assertSame($other_definition['foo'], $this->sut->getArrayDefinition()['foo']);
+    $this->assertSame($other_definition['label'], $this->sut->getArrayDefinition()['label']);
+  }
+
+  /**
+   * @covers ::mergeOverrideDefinition
+   * @covers ::validateMergeDefinition
+   * @covers ::isDefinitionCompatible
+   *
+   * @depends testMergeOverrideDefinition
+   *
+   * @expectedException \InvalidArgumentException
+   */
+  public function testMergeOverrideDefinitionWithInvalidOtherDefinition() {
+    $other_definition = $this->getMock(PluginDefinitionInterface::class);
+
+    $this->sut->mergeOverrideDefinition($other_definition);
+  }
+
+  /**
+   * @covers ::setCategory
+   * @covers ::getCategory
+   * @covers ::offsetExists
+   * @covers ::offsetSet
+   * @covers ::offsetGet
+   * @covers ::offsetUnset
+   */
+  public function testGetCategory() {
+    // Test the injected value.
+    $this->assertSame($this->arrayDefinition['category'], $this->sut->getCategory());
+    $this->assertSame($this->arrayDefinition['category'], $this->sut->getArrayDefinition()['category']);
+    $this->assertSame($this->arrayDefinition['category'], $this->sut['category']);
+
+    // Test changing the value through the setter.
+    $value = $this->randomMachineName();
+    $this->assertSame($this->sut, $this->sut->setCategory($value));
+    $this->assertSame($value, $this->sut->getCategory());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['category']);
+    $this->assertSame($value, $this->sut['category']);
+
+    // Test changing the value through array access.
+    $value = $this->randomMachineName();
+    $this->sut['category'] = $value;
+    $this->assertSame($value, $this->sut->getCategory());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['category']);
+    $this->assertSame($value, $this->sut['category']);
+
+    // Test unsetting the value.
+    unset($this->sut['category']);
+    $this->assertFalse(isset($this->sut['category']));
+    $this->assertNull($this->sut->getCategory());
+    $this->assertFalse(isset($this->sut->getArrayDefinition()['category']));
+  }
+
+  /**
+   * @covers ::setProvider
+   * @covers ::getProvider
+   * @covers ::offsetExists
+   * @covers ::offsetSet
+   * @covers ::offsetGet
+   * @covers ::offsetUnset
+   */
+  public function testGetProvider() {
+    // Test the injected value.
+    $this->assertSame($this->arrayDefinition['provider'], $this->sut->getProvider());
+    $this->assertSame($this->arrayDefinition['provider'], $this->sut->getArrayDefinition()['provider']);
+    $this->assertSame($this->arrayDefinition['provider'], $this->sut['provider']);
+
+    // Test changing the value through the setter.
+    $value = $this->randomMachineName();
+    $this->assertSame($this->sut, $this->sut->setProvider($value));
+    $this->assertSame($value, $this->sut->getProvider());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['provider']);
+    $this->assertSame($value, $this->sut['provider']);
+
+    // Test changing the value through array access.
+    $value = $this->randomMachineName();
+    $this->sut['provider'] = $value;
+    $this->assertSame($value, $this->sut->getProvider());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['provider']);
+    $this->assertSame($value, $this->sut['provider']);
+
+    // Test unsetting the value.
+    unset($this->sut['provider']);
+    $this->assertFalse(isset($this->sut['provider']));
+    $this->assertNull($this->sut->getProvider());
+    $this->assertFalse(isset($this->sut->getArrayDefinition()['provider']));
+  }
+
+  /**
+   * @covers ::setParentId
+   * @covers ::getParentId
+   * @covers ::offsetExists
+   * @covers ::offsetSet
+   * @covers ::offsetGet
+   * @covers ::offsetUnset
+   */
+  public function testGetParentId() {
+    // Test the injected value.
+    $this->assertSame($this->arrayDefinition['parent_id'], $this->sut->getParentId());
+    $this->assertSame($this->arrayDefinition['parent_id'], $this->sut->getArrayDefinition()['parent_id']);
+    $this->assertSame($this->arrayDefinition['parent_id'], $this->sut['parent_id']);
+
+    // Test changing the value through the setter.
+    $value = $this->randomMachineName();
+    $this->assertSame($this->sut, $this->sut->setParentId($value));
+    $this->assertSame($value, $this->sut->getParentId());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['parent_id']);
+    $this->assertSame($value, $this->sut['parent_id']);
+
+    // Test changing the value through array access.
+    $value = $this->randomMachineName();
+    $this->sut['parent_id'] = $value;
+    $this->assertSame($value, $this->sut->getParentId());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['parent_id']);
+    $this->assertSame($value, $this->sut['parent_id']);
+
+    // Test unsetting the value.
+    unset($this->sut['parent_id']);
+    $this->assertFalse(isset($this->sut['parent_id']));
+    $this->assertNull($this->sut->getParentId());
+    $this->assertFalse(isset($this->sut->getArrayDefinition()['parent_id']));
+  }
+
+  /**
+   * @covers ::setConfigDependencies
+   * @covers ::getConfigDependencies
+   * @covers ::offsetExists
+   * @covers ::offsetSet
+   * @covers ::offsetGet
+   * @covers ::offsetUnset
+   */
+  public function testGetConfigDependencies() {
+    // Test the injected value.
+    $this->assertSame($this->arrayDefinition['config_dependencies'], $this->sut->getConfigDependencies());
+    $this->assertSame($this->arrayDefinition['config_dependencies'], $this->sut->getArrayDefinition()['config_dependencies']);
+    $this->assertSame($this->arrayDefinition['config_dependencies'], $this->sut['config_dependencies']);
+
+    // Test changing the value through the setter.
+    $value = [
+      'module' => [$this->randomMachineName()],
+    ];
+    $this->assertSame($this->sut, $this->sut->setConfigDependencies($value));
+    $this->assertSame($value, $this->sut->getConfigDependencies());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['config_dependencies']);
+    $this->assertSame($value, $this->sut['config_dependencies']);
+
+    // Test changing the value through array access.
+    $value = [
+      'module' => [$this->randomMachineName()],
+    ];
+    $this->sut['config_dependencies'] = $value;
+    $this->assertSame($value, $this->sut->getConfigDependencies());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['config_dependencies']);
+    $this->assertSame($value, $this->sut['config_dependencies']);
+
+    // Test unsetting the value.
+    unset($this->sut['config_dependencies']);
+    $this->assertFalse(isset($this->sut['config_dependencies']));
+    $this->assertSame([], $this->sut->getConfigDependencies());
+    $this->assertFalse(isset($this->sut->getArrayDefinition()['config_dependencies']));
+  }
+
+  /**
+   * @covers ::count
+   */
+  public function testCount() {
+    $this->assertSame(count($this->arrayDefinition), count($this->sut));
+  }
+
+  /**
+   * @covers ::getIterator
+   */
+  public function testGetIterator() {
+    foreach ($this->sut as $key => $value) {
+      $this->assertSame($this->arrayDefinition[$key], $value);
+    }
+  }
+
+  /**
+   * @covers ::createFromDecoratedDefinition
+   */
+  public function testCreateFromDecoratedDefinition() {
+    $this->assertInstanceOf(ArrayPluginDefinitionDecorator::class, ArrayPluginDefinitionDecorator::createFromDecoratedDefinition($this->arrayDefinition));
+  }
+
+  /**
+   * @covers ::createFromDecoratedDefinition
+   *
+   * @dataProvider providerCreateFromDecoratedDefinitionWithInvalidDecoratedDefinition
+   *
+   * @expectedException \InvalidArgumentException
+   */
+  public function testCreateFromDecoratedDefinitionWithInvalidDecoratedDefinition($decorated_definition) {
+    $this->assertInstanceOf(ArrayPluginDefinitionDecorator::class, ArrayPluginDefinitionDecorator::createFromDecoratedDefinition($decorated_definition));
+  }
+
+  /**
+   * Provides data to self::testCreateFromDecoratedDefinitionWithInvalidDecoratedDefinition().
+   */
+  public function providerCreateFromDecoratedDefinitionWithInvalidDecoratedDefinition() {
+    return [
+      [$this->randomMachineName()],
+      [new \stdClass()],
+      [new ArrayPluginDefinitionDecorator()],
+    ];
+  }
+
+}
diff --git a/tests/src/Unit/PluginDefinition/BlockPluginDefinitionDecoratorTest.php b/tests/src/Unit/PluginDefinition/BlockPluginDefinitionDecoratorTest.php
new file mode 100644
index 0000000..c0b18e6
--- /dev/null
+++ b/tests/src/Unit/PluginDefinition/BlockPluginDefinitionDecoratorTest.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\plugin\Unit\PluginDefinition\BlockPluginDefinitionDecoratorTest.
+ */
+
+namespace Drupal\Tests\plugin\Unit\PluginDefinition;
+
+use Drupal\Component\Plugin\Derivative\DeriverInterface;
+use Drupal\Core\Plugin\Context\ContextDefinitionInterface;
+use Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator;
+use Drupal\plugin\PluginDefinition\BlockPluginDefinitionDecorator;
+use Drupal\plugin\PluginDefinition\PluginDefinitionInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\PluginDefinition\BlockPluginDefinitionDecorator
+ *
+ * @group Plugin
+ */
+class BlockPluginDefinitionDecoratorTest extends UnitTestCase {
+
+  /**
+   * The array definition.
+   *
+   * @var mixed[]
+   */
+  protected $arrayDefinition = [];
+
+  /**
+   * The class under test.
+   *
+   * @var \Drupal\plugin\PluginDefinition\BlockPluginDefinitionDecorator
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->arrayDefinition = [
+      'admin_label' => $this->randomMachineName(),
+    ];
+
+    $this->sut = new BlockPluginDefinitionDecorator($this->arrayDefinition);
+  }
+
+  /**
+   * @covers ::setLabel
+   * @covers ::getLabel
+   */
+  public function testGetLabel() {
+    // Test the injected value.
+    $this->assertSame($this->arrayDefinition['admin_label'], $this->sut->getLabel());
+    $this->assertSame($this->arrayDefinition['admin_label'], $this->sut->getArrayDefinition()['admin_label']);
+    $this->assertSame($this->arrayDefinition['admin_label'], $this->sut['admin_label']);
+
+    // Test changing the value through the setter.
+    $value = $this->randomMachineName();
+    $this->assertSame($this->sut, $this->sut->setLabel($value));
+    $this->assertSame($value, $this->sut->getLabel());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['admin_label']);
+    $this->assertSame($value, $this->sut['admin_label']);
+
+    // Test changing the value through array access.
+    $value = $this->randomMachineName();
+    $this->sut['admin_label'] = $value;
+    $this->assertSame($value, $this->sut->getLabel());
+    $this->assertSame($value, $this->sut->getArrayDefinition()['admin_label']);
+    $this->assertSame($value, $this->sut['admin_label']);
+
+    // Test unsetting the value.
+    unset($this->sut['admin_label']);
+    $this->assertFalse(isset($this->sut['admin_label']));
+    $this->assertNull($this->sut->getLabel());
+    $this->assertFalse(isset($this->sut->getArrayDefinition()['admin_label']));
+  }
+
+}
diff --git a/tests/src/Unit/PluginDefinition/MergeablePluginDefinitionTraitTest.php b/tests/src/Unit/PluginDefinition/MergeablePluginDefinitionTraitTest.php
new file mode 100644
index 0000000..97cb8f1
--- /dev/null
+++ b/tests/src/Unit/PluginDefinition/MergeablePluginDefinitionTraitTest.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\plugin\Unit\PluginDefinition\MergeablePluginDefinitionTraitTest.
+ */
+
+namespace Drupal\Tests\plugin\Unit\PluginDefinition;
+
+use Drupal\plugin\PluginDefinition\MergeablePluginDefinitionTrait;
+use Drupal\plugin\PluginDefinition\PluginDefinitionInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\PluginDefinition\MergeablePluginDefinitionTrait
+ * @group Plugin
+ */
+class MergeablePluginDefinitionTraitTest extends UnitTestCase {
+
+  /**
+   * The class under test.
+   *
+   * @var \Drupal\plugin\PluginDefinition\MergeablePluginDefinitionTrait|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->sut = $this->getMockForTrait(MergeablePluginDefinitionTrait::class);
+  }
+
+  /**
+   * @covers ::mergeDefaultDefinition
+   * @covers ::isDefinitionCompatible
+   * @covers ::doMergeDefaultDefinition
+   */
+  public function testMergeDefaultDefinition() {
+    $other_definition = $this->getMock(PluginDefinitionInterface::class);
+
+    $this->sut->expects($this->atLeastOnce())
+      ->method('isDefinitionCompatible')
+      ->willReturnCallback(function ($value) use ($other_definition) {
+        return $value == $other_definition;
+      });
+
+    $this->assertSame($this->sut, $this->sut->mergeDefaultDefinition($other_definition));
+  }
+
+  /**
+   * @covers ::mergeDefaultDefinition
+   * @covers ::isDefinitionCompatible
+   *
+   * @depends testMergeDefaultDefinition
+   *
+   * @expectedException \InvalidArgumentException
+   */
+  public function testMergeDefaultDefinitionWithInvalidOtherDefinition() {
+    $other_definition = $this->getMock(PluginDefinitionInterface::class);
+
+    $this->sut->expects($this->atLeastOnce())
+      ->method('isDefinitionCompatible')
+      ->willReturn(FALSE);
+
+    $this->sut->mergeDefaultDefinition($other_definition);
+  }
+
+  /**
+   * @covers ::mergeOverrideDefinition
+   * @covers ::isDefinitionCompatible
+   * @covers ::doMergeOverrideDefinition
+   */
+  public function testMergeOverrideDefinition() {
+    $other_definition = $this->getMock(PluginDefinitionInterface::class);
+
+    $this->sut->expects($this->atLeastOnce())
+      ->method('isDefinitionCompatible')
+      ->willReturnCallback(function ($value) use ($other_definition) {
+        return $value == $other_definition;
+      });
+
+    $this->assertSame($this->sut, $this->sut->mergeOverrideDefinition($other_definition));
+  }
+
+  /**
+   * @covers ::mergeOverrideDefinition
+   * @covers ::isDefinitionCompatible
+   *
+   * @depends testMergeOverrideDefinition
+   *
+   * @expectedException \InvalidArgumentException
+   */
+  public function testMergeOverrideDefinitionWithInvalidOtherDefinition() {
+    $other_definition = $this->getMock(PluginDefinitionInterface::class);
+
+    $this->sut->expects($this->atLeastOnce())
+      ->method('isDefinitionCompatible')
+      ->willReturn(FALSE);
+
+    $this->sut->mergeOverrideDefinition($other_definition);
+  }
+
+}
diff --git a/tests/src/Unit/PluginDefinition/PluginCategoryDefinitionTraitTest.php b/tests/src/Unit/PluginDefinition/PluginCategoryDefinitionTraitTest.php
new file mode 100644
index 0000000..b948da2
--- /dev/null
+++ b/tests/src/Unit/PluginDefinition/PluginCategoryDefinitionTraitTest.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\plugin\Unit\PluginDefinition\PluginCategoryDefinitionTraitTest.
+ */
+
+namespace Drupal\Tests\plugin\Unit\PluginDefinition;
+
+use Drupal\plugin\PluginDefinition\PluginCategoryDefinitionTrait;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\PluginDefinition\PluginCategoryDefinitionTrait
+ *
+ * @group Plugin
+ */
+class PluginCategoryDefinitionTraitTest extends UnitTestCase {
+
+  /**
+   * The class under test.
+   *
+   * @var \Drupal\plugin\PluginDefinition\PluginCategoryDefinitionTrait
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->sut = $this->getMockForTrait(PluginCategoryDefinitionTrait::class);
+  }
+
+  /**
+   * @covers ::setCategory
+   * @covers ::getCategory
+   */
+  public function testGetCategory() {
+    $category = $this->randomMachineName();
+
+    $this->assertSame($this->sut, $this->sut->setCategory($category));
+    $this->assertSame($category, $this->sut->getCategory());
+  }
+
+}
diff --git a/tests/src/Unit/PluginDefinition/PluginConfigDependenciesDefinitionTraitTest.php b/tests/src/Unit/PluginDefinition/PluginConfigDependenciesDefinitionTraitTest.php
new file mode 100644
index 0000000..a62a205
--- /dev/null
+++ b/tests/src/Unit/PluginDefinition/PluginConfigDependenciesDefinitionTraitTest.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\plugin\Unit\PluginDefinition\PluginConfigDependenciesDefinitionTraitTest.
+ */
+
+namespace Drupal\Tests\plugin\Unit\PluginDefinition;
+
+use Drupal\plugin\PluginDefinition\PluginConfigDependenciesDefinitionTrait;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\PluginDefinition\PluginConfigDependenciesDefinitionTrait
+ *
+ * @group Plugin
+ */
+class PluginConfigDependenciesDefinitionTraitTest extends UnitTestCase {
+
+  /**
+   * The class under test.
+   *
+   * @var \Drupal\plugin\PluginDefinition\PluginConfigDependenciesDefinitionTrait
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->sut = $this->getMockForTrait(PluginConfigDependenciesDefinitionTrait::class);
+  }
+
+  /**
+   * @covers ::setConfigDependencies
+   * @covers ::getConfigDependencies
+   */
+  public function testGetConfigDependencies() {
+    $dependencies = [
+      'module' => [$this->randomMachineName()],
+    ];
+
+    $this->assertSame($this->sut, $this->sut->setConfigDependencies($dependencies));
+    $this->assertSame($dependencies, $this->sut->getConfigDependencies());
+  }
+
+}
diff --git a/tests/src/Unit/PluginDefinition/PluginContextDefinitionTraitTest.php b/tests/src/Unit/PluginDefinition/PluginContextDefinitionTraitTest.php
new file mode 100644
index 0000000..81ef112
--- /dev/null
+++ b/tests/src/Unit/PluginDefinition/PluginContextDefinitionTraitTest.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\plugin\Unit\PluginDefinition\PluginContextDefinitionTraitTest.
+ */
+
+namespace Drupal\Tests\plugin\Unit\PluginDefinition;
+
+use Drupal\Core\Plugin\Context\ContextDefinitionInterface;
+use Drupal\plugin\PluginDefinition\PluginContextDefinitionTrait;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\PluginDefinition\PluginContextDefinitionTrait
+ *
+ * @group Plugin
+ */
+class PluginContextDefinitionTraitTest extends UnitTestCase {
+
+  /**
+   * The class under test.
+   *
+   * @var \Drupal\plugin\PluginDefinition\PluginContextDefinitionTrait
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->sut = $this->getMockForTrait(PluginContextDefinitionTrait::class);
+  }
+
+  /**
+   * @covers ::setContextDefinitions
+   * @covers ::getContextDefinitions
+   */
+  public function testGetContextDefinitions() {
+    $context_definition_name_a = $this->randomMachineName();
+    $context_definition_a = $this->getMock(ContextDefinitionInterface::class);
+    $context_definition_name_b = $this->randomMachineName();
+    $context_definition_b = $this->getMock(ContextDefinitionInterface::class);
+
+    $context_definitions = [
+      $context_definition_name_a => $context_definition_a,
+      $context_definition_name_b => $context_definition_b,
+    ];
+
+    $this->assertSame($this->sut, $this->sut->setContextDefinitions($context_definitions));
+    $this->assertSame($context_definitions, $this->sut->getContextDefinitions());
+  }
+
+  /**
+   * @covers ::setContextDefinitions
+   *
+   * @depends testGetContextDefinitions
+   *
+   * @expectedException \InvalidArgumentException
+   */
+  public function testSetContextDefinitionsWithInvalidDefinition() {
+    $context_definitions = [
+      $this->randomMachineName() => new \stdClass(),
+    ];
+
+    $this->sut->setContextDefinitions($context_definitions);
+  }
+
+  /**
+   * @covers ::setContextDefinition
+   * @covers ::getContextDefinition
+   * @covers ::hasContextDefinition
+   */
+  public function testGetContextDefinition() {
+    $name = $this->randomMachineName();
+    $context_definition = $this->getMock(ContextDefinitionInterface::class);
+
+    $this->assertSame($this->sut, $this->sut->setContextDefinition($name, $context_definition));
+    $this->assertSame($context_definition, $this->sut->getContextDefinition($name));
+    $this->assertTrue($this->sut->hasContextDefinition($name));
+  }
+
+  /**
+   * @covers ::getContextDefinition
+   * @covers ::hasContextDefinition
+   *
+   * @depends testGetContextDefinition
+   *
+   * @expectedException \InvalidArgumentException
+   */
+  public function testGetContextDefinitionWithNonExistentDefinition() {
+    $name = $this->randomMachineName();
+
+    $this->assertFalse($this->sut->hasContextDefinition($name));
+    $this->sut->getContextDefinition($name);
+  }
+
+}
diff --git a/tests/src/Unit/PluginDefinition/PluginDefinitionTest.php b/tests/src/Unit/PluginDefinition/PluginDefinitionTest.php
new file mode 100644
index 0000000..1b62818
--- /dev/null
+++ b/tests/src/Unit/PluginDefinition/PluginDefinitionTest.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\plugin\Unit\PluginDefinition\PluginDefinitionTest.
+ */
+
+namespace Drupal\Tests\plugin\Unit\PluginDefinition;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\plugin\PluginDefinition\PluginDefinition;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\PluginDefinition\PluginDefinition
+ *
+ * @group Plugin
+ */
+class PluginDefinitionTest extends UnitTestCase {
+
+  /**
+   * The class under test.
+   *
+   * @var \Drupal\plugin\PluginDefinition\PluginDefinition
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->sut = $this->getMockForAbstractClass(PluginDefinition::class);
+  }
+
+  /**
+   * @covers ::setId
+   * @covers ::getId
+   */
+  public function testGetId() {
+    $id = $this->randomMachineName();
+
+    $this->assertSame($this->sut, $this->sut->setId($id));
+    $this->assertSame($id, $this->sut->getId());
+  }
+
+  /**
+   * @covers ::setClass
+   * @covers ::getClass
+   */
+  public function testGetClass() {
+    $class = get_class($this->getMock(PluginInspectionInterface::class));
+
+    $this->assertSame($this->sut, $this->sut->setClass($class));
+    $this->assertSame($class, $this->sut->getClass());
+  }
+
+  /**
+   * @covers ::setProvider
+   * @covers ::getProvider
+   */
+  public function testGetProvider() {
+    $provider = $this->randomMachineName();
+
+    $this->assertSame($this->sut, $this->sut->setProvider($provider));
+    $this->assertSame($provider, $this->sut->getProvider());
+  }
+
+}
diff --git a/tests/src/Unit/PluginDefinition/PluginDefinitionValidatorTest.php b/tests/src/Unit/PluginDefinition/PluginDefinitionValidatorTest.php
new file mode 100644
index 0000000..2b452fc
--- /dev/null
+++ b/tests/src/Unit/PluginDefinition/PluginDefinitionValidatorTest.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\plugin\Unit\PluginDefinition\PluginDefinitionValidatorTest.
+ */
+
+namespace Drupal\Tests\plugin\Unit\PluginDefinition;
+
+use Drupal\Component\Plugin\Derivative\DeriverInterface;
+use Drupal\Core\Plugin\Context\ContextDefinitionInterface;
+use Drupal\plugin\PluginDefinition\PluginDefinitionValidator;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\PluginDefinition\PluginDefinitionValidator
+ *
+ * @group Plugin
+ */
+class PluginDefinitionValidatorTest extends UnitTestCase {
+
+  /**
+   * @covers ::validateClass
+   *
+   * @dataProvider providerValidateClass
+   *
+   * @param bool $valid
+   *   Whether or not the class is valid.
+   * @param string $class
+   *   The class to validate.
+   */
+  public function testValidateClass($valid, $class) {
+    if (!$valid) {
+      $this->setExpectedException('\InvalidArgumentException');
+    }
+    PluginDefinitionValidator::validateClass($class);
+  }
+
+  /**
+   * Provides data to self::testValidateClass().
+   */
+  public function providerValidateClass() {
+    return [
+      [TRUE, '\stdClass'],
+      [TRUE, __CLASS__],
+      [FALSE, NULL],
+      [FALSE, $this->randomMachineName()],
+      [FALSE, '\Foo\Bar\Baz\Qux'],
+    ];
+  }
+
+  /**
+   * @covers ::validateDeriverClass
+   * @covers ::validateClass
+   *
+   * @dataProvider providerValidateDeriverClass
+   *
+   * @param bool $valid
+   *   Whether or not the class is valid.
+   * @param string $class
+   *   The class to validate.
+   */
+  public function testValidateDeriverClass($valid, $class) {
+    if (!$valid) {
+      $this->setExpectedException('\InvalidArgumentException');
+    }
+    PluginDefinitionValidator::validateDeriverClass($class);
+  }
+
+  /**
+   * Provides data to self::testValidateDeriverClass().
+   */
+  public function providerValidateDeriverClass() {
+    return [
+      [TRUE, $this->getMockClass(DeriverInterface::class)],
+      [FALSE, NULL],
+      [FALSE, '\stdClass'],
+      [FALSE, $this->randomMachineName()],
+      [FALSE, '\Foo\Bar\Baz\Qux'],
+    ];
+  }
+
+  /**
+   * @covers ::validateContextDefinitions
+   *
+   * @dataProvider providerValidateContextDefinitions
+   *
+   * @param bool $valid
+   *   Whether or not the class is valid.
+   * @param mixed[] $definitions
+   *   The context definitions to validate.
+   */
+  public function testValidateContextDefinitions($valid, array $definitions) {
+    if (!$valid) {
+      $this->setExpectedException('\InvalidArgumentException');
+    }
+    PluginDefinitionValidator::validateContextDefinitions($definitions);
+  }
+
+  /**
+   * Provides data to self::testValidateContextDefinitions().
+   */
+  public function providerValidateContextDefinitions() {
+    return [
+      [TRUE, []],
+      [TRUE, [$this->getMock(ContextDefinitionInterface::class)]],
+      [FALSE, [$this->getMockClass(ContextDefinitionInterface::class)]],
+      [FALSE, [$this->randomMachineName()]],
+      [FALSE, [ContextDefinitionInterface::class]],
+    ];
+  }
+
+}
diff --git a/tests/src/Unit/PluginDefinition/PluginDeriverDefinitionTraitTest.php b/tests/src/Unit/PluginDefinition/PluginDeriverDefinitionTraitTest.php
new file mode 100644
index 0000000..57cb95d
--- /dev/null
+++ b/tests/src/Unit/PluginDefinition/PluginDeriverDefinitionTraitTest.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\plugin\Unit\PluginDefinition\PluginDeriverDefinitionTraitTest.
+ */
+
+namespace Drupal\Tests\plugin\Unit\PluginDefinition;
+
+use Drupal\Component\Plugin\Derivative\DeriverInterface;
+use Drupal\plugin\PluginDefinition\PluginDeriverDefinitionTrait;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\PluginDefinition\PluginDeriverDefinitionTrait
+ *
+ * @group Plugin
+ */
+class PluginDeriverDefinitionTraitTest extends UnitTestCase {
+
+  /**
+   * The class under test.
+   *
+   * @var \Drupal\plugin\PluginDefinition\PluginDeriverDefinitionTrait
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->sut = $this->getMockForTrait(PluginDeriverDefinitionTrait::class);
+  }
+
+
+  /**
+   * @covers ::setDeriverClass
+   * @covers ::getDeriverClass
+   */
+  public function testGetDeriverClass() {
+    $class = get_class($this->getMock(DeriverInterface::class));
+
+    $this->assertSame($this->sut, $this->sut->setDeriverClass($class));
+    $this->assertSame($class, $this->sut->getDeriverClass());
+  }
+
+}
diff --git a/tests/src/Unit/PluginDefinition/PluginLabelDefinitionTraitTest.php b/tests/src/Unit/PluginDefinition/PluginLabelDefinitionTraitTest.php
new file mode 100644
index 0000000..6212b04
--- /dev/null
+++ b/tests/src/Unit/PluginDefinition/PluginLabelDefinitionTraitTest.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\plugin\Unit\PluginDefinition\PluginLabelDefinitionTraitTest.
+ */
+
+namespace Drupal\Tests\plugin\Unit\PluginDefinition;
+
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionTrait;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\PluginDefinition\PluginLabelDefinitionTrait
+ *
+ * @group Plugin
+ */
+class PluginLabelDefinitionTraitTest extends UnitTestCase {
+
+  /**
+   * The class under test.
+   *
+   * @var \Drupal\plugin\PluginDefinition\PluginLabelDefinitionTrait
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->sut = $this->getMockForTrait(PluginLabelDefinitionTrait::class);
+  }
+
+  /**
+   * @covers ::setLabel
+   * @covers ::getLabel
+   */
+  public function testGetLabel() {
+    $label = $this->randomMachineName();
+
+    $this->assertSame($this->sut, $this->sut->setLabel($label));
+    $this->assertSame($label, $this->sut->getLabel());
+  }
+
+}
diff --git a/tests/src/Unit/PluginDiscovery/TypedDefinitionEnsuringPluginDiscoveryDecoratorTest.php b/tests/src/Unit/PluginDiscovery/TypedDefinitionEnsuringPluginDiscoveryDecoratorTest.php
new file mode 100644
index 0000000..a9c9726
--- /dev/null
+++ b/tests/src/Unit/PluginDiscovery/TypedDefinitionEnsuringPluginDiscoveryDecoratorTest.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\plugin\Unit\PluginDiscovery\TypedDefinitionEnsuringPluginDiscoveryDecoratorTest.
+ */
+
+namespace Drupal\Tests\plugin\Unit\PluginDiscovery;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\plugin\PluginDefinition\PluginDefinitionInterface;
+use Drupal\plugin\PluginDiscovery\TypedDefinitionEnsuringPluginDiscoveryDecorator;
+use Drupal\plugin\PluginTypeInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\PluginDiscovery\TypedDefinitionEnsuringPluginDiscoveryDecorator
+ *
+ * @group Plugin
+ */
+class TypedDefinitionEnsuringPluginDiscoveryDecoratorTest extends UnitTestCase {
+
+  /**
+   * The original plugin manager.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $pluginManager;
+
+  /**
+   * The type of the plugin definitions to decorate.
+   *
+   * @var \Drupal\plugin\PluginTypeInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $pluginType;
+
+  /**
+   * The class under test.
+   *
+   * @var \Drupal\plugin\PluginDiscovery\TypedDefinitionEnsuringPluginDiscoveryDecorator
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    $this->pluginManager = $this->getMock(PluginManagerInterface::class);
+
+    $this->pluginType = $this->getMock(PluginTypeInterface::class);
+
+    $this->sut = new TypedDefinitionEnsuringPluginDiscoveryDecorator($this->pluginType, $this->pluginManager);
+  }
+
+  /**
+   * @covers ::__construct
+   */
+  public function testConstruct() {
+    $this->sut = new TypedDefinitionEnsuringPluginDiscoveryDecorator($this->pluginType, $this->pluginManager);
+  }
+
+  /**
+   * @covers ::getDefinitions
+   * @covers ::processDecoratedDefinitions
+   */
+  public function testGetDefinitions() {
+    $decorated_plugin_id_a = $this->randomMachineName();
+    $decorated_plugin_definition_a = [
+      'id' => $decorated_plugin_id_a,
+    ];
+    $decorated_plugin_id_b = $this->randomMachineName();
+    $decorated_plugin_definition_b = [
+      'id' => $decorated_plugin_id_b,
+    ];
+
+    $decorated_plugin_definitions = [
+      $decorated_plugin_id_a => $decorated_plugin_definition_a,
+      $decorated_plugin_id_b => $decorated_plugin_definition_b,
+    ];
+
+    $this->pluginManager->expects($this->once())
+      ->method('getDefinitions')
+      ->willReturn($decorated_plugin_definitions);
+
+    $typed_plugin_definition_a = $this->getMock(PluginDefinitionInterface::class);
+    $typed_plugin_definition_b = $this->getMock(PluginDefinitionInterface::class);
+
+    $map = [
+      [$decorated_plugin_definition_a, $typed_plugin_definition_a],
+      [$decorated_plugin_definition_b, $typed_plugin_definition_b],
+    ];
+    $this->pluginType->expects($this->atLeastOnce())
+      ->method('ensureTypedPluginDefinition')
+      ->willReturnMap($map);
+
+    $expected_plugin_definitions = [
+      $decorated_plugin_id_a => $typed_plugin_definition_a,
+      $decorated_plugin_id_b => $typed_plugin_definition_b,
+    ];
+    $this->assertSame($expected_plugin_definitions, $this->sut->getDefinitions());
+  }
+
+}
diff --git a/tests/src/Unit/PluginHierarchyTraitTest.php b/tests/src/Unit/PluginHierarchyTraitTest.php
new file mode 100644
index 0000000..fbab6cc
--- /dev/null
+++ b/tests/src/Unit/PluginHierarchyTraitTest.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * @file
+ * Contains
+ * \Drupal\Tests\plugin\Unit\Plugin\Plugin\Plugin\SelectListUnitTest.
+ */
+
+namespace Drupal\Tests\plugin\Unit\Plugin\PluginSelector\PluginSelector;
+
+use Drupal\plugin\PluginDefinition\PluginHierarchyDefinitionInterface;
+use Drupal\plugin\PluginDefinition\PluginLabelDefinitionInterface;
+use Drupal\plugin\PluginDiscovery\TypedDiscoveryInterface;
+use Drupal\plugin\PluginHierarchyTrait;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\plugin\PluginHierarchyTrait
+ *
+ * @group Plugin
+ */
+class PluginHierarchyTraitTest extends UnitTestCase {
+
+  /**
+   * The class under test.
+   *
+   * @var \Drupal\plugin\PluginHierarchyTrait
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->sut = $this->getMockForTrait(PluginHierarchyTrait::class);
+  }
+
+  /**
+   * @covers ::buildPluginHierarchy
+   * @covers ::buildPluginHierarchyLevel
+   * @covers ::sortHierarchicalPlugins
+   */
+  public function testBuildPluginHierarchy() {
+    $plugin_definition_id_a = $this->randomMachineName();
+    $plugin_definition_label_a = 'foo';
+    $plugin_definition_a = $this->getMock(PluginLabelDefinitionInterface::class);
+    $plugin_definition_a->expects($this->any())
+      ->method('getLabel')
+      ->willReturn($plugin_definition_label_a);
+
+    $plugin_definition_id_b = $this->randomMachineName();
+    $plugin_definition_label_b = 'Bar';
+    $plugin_definition_b = $this->getMock(PluginLabelDefinitionInterface::class);
+    $plugin_definition_b->expects($this->any())
+      ->method('getLabel')
+      ->willReturn($plugin_definition_label_b);
+
+    $plugin_definition_id_c = $this->randomMachineName();
+    $plugin_definition_c = $this->getMock(PluginHierarchyDefinitionInterface::class);
+    $plugin_definition_c->expects($this->any())
+      ->method('getId')
+      ->willReturn($plugin_definition_id_c);
+    $plugin_definition_c->expects($this->any())
+      ->method('getParentId')
+      ->willReturn($plugin_definition_id_a);
+
+    $plugin_discovery = $this->getMock(TypedDiscoveryInterface::class);
+    $plugin_discovery->expects($this->any())
+      ->method('getDefinitions')
+      ->willReturn([
+        $plugin_definition_id_a => $plugin_definition_a,
+        $plugin_definition_id_b => $plugin_definition_b,
+        $plugin_definition_id_c => $plugin_definition_c,
+      ]);
+
+    $method = new \ReflectionMethod($this->sut, 'buildPluginHierarchy');
+    $method->setAccessible(TRUE);
+    // We need to suppress errors, because using mocks inside user comparison
+    // functions always causes the "Array was modified by the user comparison
+    // function" error. Because we check the output, we catch (most) problems
+    // anyway.
+    $hierarchy = @$method->invokeArgs($this->sut, array($plugin_discovery));
+
+    $expected = [
+      $plugin_definition_id_b => [],
+      $plugin_definition_id_a => [
+        $plugin_definition_id_c => [],
+      ],
+    ];
+
+    $this->assertSame($expected, $hierarchy);
+  }
+
+}
diff --git a/tests/src/Unit/PluginTypeTest.php b/tests/src/Unit/PluginTypeTest.php
index 4f9828e..73e4e65 100644
--- a/tests/src/Unit/PluginTypeTest.php
+++ b/tests/src/Unit/PluginTypeTest.php
@@ -9,6 +9,9 @@ namespace Drupal\Tests\plugin\Unit;
 
 use Drupal\Component\Plugin\PluginManagerInterface;
 use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator;
+use Drupal\plugin\PluginDefinition\PluginDefinitionDecoratorInterface;
+use Drupal\plugin\PluginDefinition\PluginDefinitionInterface;
 use Drupal\plugin\PluginType;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -125,4 +128,55 @@ class PluginTypeTest extends UnitTestCase {
     $this->assertSame($this->pluginTypeDefinition['field_type'], $this->sut->isFieldType());
   }
 
+  /**
+   * @covers ::ensureTypedPluginDefinition
+   * @covers ::createFromDefinition
+   * @covers ::__construct
+   */
+  public function testEnsureTypedPluginDefinition() {
+    $decorated_plugin_definition = [
+      'foo' => $this->randomMachineName(),
+    ];
+
+    $this->pluginTypeDefinition['plugin_definition_decorator_class'] = ArrayPluginDefinitionDecorator::class;
+
+    $this->sut = PluginType::createFromDefinition($this->container, $this->pluginTypeDefinition);
+
+    $typed_plugin_definition = $this->sut->ensureTypedPluginDefinition($decorated_plugin_definition);
+
+    $this->assertInstanceOf(PluginDefinitionInterface::class, $typed_plugin_definition);
+    // We use ArrayPluginDefinitionDecorator for testing. The following
+    // assertion makes sure the method under test correctly passes on the
+    // decorated plugin definition to the decorator. The array handling is not
+    // part of this test.
+    /** @var \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator $typed_plugin_definition */
+    $this->assertSame($decorated_plugin_definition, $typed_plugin_definition->getArrayDefinition());
+  }
+
+  /**
+   * @covers ::ensureTypedPluginDefinition
+   * @covers ::createFromDefinition
+   * @covers ::__construct
+   */
+  public function testEnsureTypedPluginDefinitionWithAlreadyTypedDefinition() {
+    $decorated_plugin_definition = $this->getMock(PluginDefinitionInterface::class);
+
+    $typed_plugin_definition = $this->sut->ensureTypedPluginDefinition($decorated_plugin_definition);
+
+    $this->assertInstanceOf(PluginDefinitionInterface::class, $typed_plugin_definition);
+  }
+
+  /**
+   * @covers ::ensureTypedPluginDefinition
+   *
+   * @expectedException \Exception
+   */
+  public function testEnsureTypedPluginDefinitionWithoutDecorator() {
+    $decorated_plugin_definition = [
+      'foo' => $this->randomMachineName(),
+    ];
+
+    $this->sut->ensureTypedPluginDefinition($decorated_plugin_definition);
+  }
+
 }
