Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #2817751 by bojanz: Create an API for bundle plugins #49

Merged
merged 5 commits into from
Sep 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
82 changes: 82 additions & 0 deletions entity.module
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,85 @@
* @file
* Provides expanded entity APIs.
*/

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\entity\BundlePlugin\BundlePluginHandler;

/**
* Gets the entity types which use bundle plugins.
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
* The entity types.
*/
function entity_get_bundle_plugin_entity_types() {
$entity_types = \Drupal::entityTypeManager()->getDefinitions();
$entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) {
return $entity_type->hasHandlerClass('bundle_plugin');
});

return $entity_types;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should static cache this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? It's used when rebuilding the cached bundle or field data, it's not in any hot path. Plus, the function is not doing any I/O, it's just filtering an array.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was just and uber nit pick. I thought hook_modules_installed and hook_entity_bundle_info can be called on same request.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, let's skip that then.

}

/**
* Implements hook_entity_type_build().
*/
function entity_entity_type_build(array &$entity_types) {
foreach ($entity_types as $entity_type) {
if ($entity_type->get('bundle_plugin_type')) {
$entity_type->setHandlerClass('bundle_plugin', BundlePluginHandler::class);
}
}
}

/**
* Implements hook_entity_bundle_info().
*/
function entity_entity_bundle_info() {
$bundles = [];
foreach (entity_get_bundle_plugin_entity_types() as $entity_type) {
/** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */
$bundle_handler = \Drupal::entityTypeManager()->getHandler($entity_type->id(), 'bundle_plugin');
$bundles[$entity_type->id()] = $bundle_handler->getBundleInfo();
}
return $bundles;
}

/**
* Implements hook_entity_field_storage_info().
*/
function entity_entity_field_storage_info(EntityTypeInterface $entity_type) {
if ($entity_type->hasHandlerClass('bundle_plugin')) {
/** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */
$bundle_handler = \Drupal::entityTypeManager()->getHandler($entity_type->id(), 'bundle_plugin');
return $bundle_handler->getFieldStorageDefinitions();
}
}

/**
* Implements hook_entity_bundle_field_info().
*/
function entity_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle) {
if ($entity_type->hasHandlerClass('bundle_plugin')) {
/** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */
$bundle_handler = \Drupal::entityTypeManager()->getHandler($entity_type->id(), 'bundle_plugin');
return $bundle_handler->getFieldDefinitions($bundle);
}
}

/**
* Implements hook_modules_installed().
*/
function entity_modules_installed($modules) {
foreach (entity_get_bundle_plugin_entity_types() as $entity_type) {
\Drupal::service('entity.bundle_plugin_installer')->installBundles($entity_type, $modules);
}
}

/**
* Implements hook_module_preuninstall().
*/
function entity_module_preuninstall($module) {
foreach (entity_get_bundle_plugin_entity_types() as $entity_type) {
\Drupal::service('entity.bundle_plugin_installer')->uninstallBundles($entity_type, [$module]);
}
}
10 changes: 10 additions & 0 deletions entity.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,13 @@ services:
arguments: ['@entity_type.manager', '@current_route_match']
tags:
- { name: access_check, applies_to: _entity_access_revision }

entity.bundle_plugin_installer:
class: Drupal\entity\BundlePlugin\BundlePluginInstaller
arguments: ['@entity_type.manager', '@entity_bundle.listener', '@field_storage_definition.listener', '@field_definition.listener']

entity.bundle_plugin.uninstall_validator:
class: \Drupal\entity\BundlePlugin\BundlePluginUninstallValidator
tags:
- { name: module_install.uninstall_validator }
arguments: ['@entity_type.manager', '@string_translation']
27 changes: 27 additions & 0 deletions src/BundleFieldDefinition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Drupal\entity;

use Drupal\Core\Field\BaseFieldDefinition;

/**
* Provides a field definition class for bundle fields.
*
* Core currently doesn't provide one, the hook_entity_bundle_field_info()
* example uses BaseFieldDefinition, which is wrong. Tracked in #2346347.
*
* Note that this class implements both FieldStorageDefinitionInterface and
* FieldDefinitionInterface. This is a simplification for DX reasons,
* allowing code to return just the bundle definitions instead of having to
* return both storage definitions and bundle definitions.
*/
class BundleFieldDefinition extends BaseFieldDefinition {

/**
* {@inheritdoc}
*/
public function isBaseField() {
return FALSE;
}

}
102 changes: 102 additions & 0 deletions src/BundlePlugin/BundlePluginHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace Drupal\entity\BundlePlugin;

use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class BundlePluginHandler implements BundlePluginHandlerInterface {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't planning on adding one. I don't see a point in a comment that doesn't add any new information, such as "Default implementation of the BundlePluginHandlerInterface."

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK


/**
* The entity type.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;

/**
* The bundle plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $pluginManager;

/**
* Constructs a new BundlePluginHandler object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager
* The bundle plugin manager.
*/
public function __construct(EntityTypeInterface $entity_type, PluginManagerInterface $plugin_manager) {
$this->entityType = $entity_type;
$this->pluginManager = $plugin_manager;
}

/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('plugin.manager.' . $entity_type->get('bundle_plugin_type'))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we catch the exception here and maybe provide a helpful error message "you need to implement a plugin manager for your bundle type called '....'"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is overengineering. The backtrace will show what is going on

);
}

/**
* {@inheritdoc}
*/
public function getBundleInfo() {
$bundles = [];
foreach ($this->pluginManager->getDefinitions() as $plugin_id => $definition) {
$bundles[$plugin_id] = [
'label' => $definition['label'],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be feasible to add something like 'description' => $definition['description'], here? This would enable us to add a bundle description & make the add-page considerably nicer looking.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like a nice idea!

'description' => isset($definition['description']) ? $definition['description'] : '',
'translatable' => $this->entityType->isTranslatable(),
'provider' => $definition['provider'],
];
}
return $bundles;
}

/**
* {@inheritdoc}
*/
public function getFieldStorageDefinitions() {
$definitions = [];
foreach (array_keys($this->pluginManager->getDefinitions()) as $plugin_id) {
/** @var \Drupal\entity\BundlePlugin\BundlePluginInterface $plugin */
$plugin = $this->pluginManager->createInstance($plugin_id);
$definitions += $plugin->buildFieldDefinitions();
}
// Ensure the presence of required keys which aren't set by the plugin.
foreach ($definitions as $field_name => $definition) {
$definition->setName($field_name);
$definition->setTargetEntityTypeId($this->entityType->id());
$definitions[$field_name] = $definition;
}

return $definitions;
}

/**
* {@inheritdoc}
*/
public function getFieldDefinitions($bundle) {
/** @var \Drupal\entity\BundlePlugin\BundlePluginInterface $plugin */
$plugin = $this->pluginManager->createInstance($bundle);
$definitions = $plugin->buildFieldDefinitions();
// Ensure the presence of required keys which aren't set by the plugin.
foreach ($definitions as $field_name => $definition) {
$definition->setName($field_name);
$definition->setTargetEntityTypeId($this->entityType->id());
$definition->setTargetBundle($bundle);
$definitions[$field_name] = $definition;
}

return $definitions;
}

}
37 changes: 37 additions & 0 deletions src/BundlePlugin/BundlePluginHandlerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Drupal\entity\BundlePlugin;

use Drupal\Core\Entity\EntityHandlerInterface;

/**
* Handles plugin-provided bundles.
*/
interface BundlePluginHandlerInterface extends EntityHandlerInterface {

/**
* Gets the bundle info.
*
* @return array
* An array of bundle information keyed by the bundle name.
* The format expected by hook_entity_bundle_info().
*/
public function getBundleInfo();

/**
* Gets the field storage definitions.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Let's add documentation about the return type.

*/
public function getFieldStorageDefinitions();

/**
* Gets the field definitions for a specific bundle.
*
* @param string $bundle
* The bundle name.
*
* @return \Drupal\entity\BundleFieldDefinition[]
* An array of bundle field definitions, keyed by field name.
*/
public function getFieldDefinitions($bundle);

}
94 changes: 94 additions & 0 deletions src/BundlePlugin/BundlePluginInstaller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

namespace Drupal\entity\BundlePlugin;

use Drupal\Core\Entity\EntityBundleListenerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionListenerInterface;
use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;

class BundlePluginInstaller implements BundlePluginInstallerInterface {

/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;

/**
* The entity bundle listener.
*
* @var \Drupal\Core\Entity\EntityBundleListenerInterface
*/
protected $entityBundleListener;

/**
* The field storage definition listener.
*
* @var \Drupal\Core\Field\FieldStorageDefinitionListenerInterface
*/
protected $fieldStorageDefinitionListener;

/**
* The field definition listener.
*
* @var \Drupal\Core\Field\FieldDefinitionListenerInterface
*/
protected $fieldDefinitionListener;

/**
* Constructs a new BundlePluginInstaller object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityBundleListenerInterface $entity_bundle_listener
* The entity bundle listener.
* @param \Drupal\Core\Field\FieldStorageDefinitionListenerInterface $field_storage_definition_listener
* The field storage definition listener.
* @param \Drupal\Core\Field\FieldDefinitionListenerInterface $field_definition_listener
* The field definition listener.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityBundleListenerInterface $entity_bundle_listener, FieldStorageDefinitionListenerInterface $field_storage_definition_listener, FieldDefinitionListenerInterface $field_definition_listener) {
$this->entityTypeManager = $entity_type_manager;
$this->entityBundleListener = $entity_bundle_listener;
$this->fieldStorageDefinitionListener = $field_storage_definition_listener;
$this->fieldDefinitionListener = $field_definition_listener;
}

/**
* {@inheritdoc}
*/
public function installBundles(EntityTypeInterface $entity_type, array $modules) {
$bundle_handler = $this->entityTypeManager->getHandler($entity_type->id(), 'bundle_plugin');
$bundles = array_filter($bundle_handler->getBundleInfo(), function ($bundle_info) use ($modules) {
return in_array($bundle_info['provider'], $modules, TRUE);
});
foreach (array_keys($bundles) as $bundle) {
$this->entityBundleListener->onBundleCreate($bundle, $entity_type->id());
foreach ($bundle_handler->getFieldDefinitions($bundle) as $definition) {
$this->fieldStorageDefinitionListener->onFieldStorageDefinitionCreate($definition);
$this->fieldDefinitionListener->onFieldDefinitionCreate($definition);
}
}
}

/**
* {@inheritdoc}
*/
public function uninstallBundles(EntityTypeInterface $entity_type, array $modules) {
$bundle_handler = $this->entityTypeManager->getHandler($entity_type->id(), 'bundle_plugin');
$bundles = array_filter($bundle_handler->getBundleInfo(), function ($bundle_info) use ($modules) {
return in_array($bundle_info['provider'], $modules, TRUE);
});
foreach (array_keys($bundles) as $bundle) {
$this->entityBundleListener->onBundleDelete($bundle, $entity_type->id());
foreach ($bundle_handler->getFieldDefinitions($bundle) as $definition) {
$this->fieldDefinitionListener->onFieldDefinitionDelete($definition);
$this->fieldStorageDefinitionListener->onFieldStorageDefinitionDelete($definition);
}
}
}

}
34 changes: 34 additions & 0 deletions src/BundlePlugin/BundlePluginInstallerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Drupal\entity\BundlePlugin;

use Drupal\Core\Entity\EntityTypeInterface;

/**
* Installs and uninstalls bundle plugins.
*
* Ensures that the fields provided by the bundle plugins are created/deleted.
*/
interface BundlePluginInstallerInterface {

/**
* Installs the bundle plugins provided by the specified modules.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param array $modules
* The modules.
*/
public function installBundles(EntityTypeInterface $entity_type, array $modules);

/**
* Uninstalls the bundle plugins provided by the specified modules.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param array $modules
* The modules.
*/
public function uninstallBundles(EntityTypeInterface $entity_type, array $modules);

}