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

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

merged 5 commits into from Sep 18, 2017

Conversation

bojanz
Copy link
Collaborator

@bojanz bojanz commented Oct 13, 2016

No description provided.

entity.module Outdated
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', 'Drupal\entity\BundlePlugin\BundlePluginHandler');
Copy link

Choose a reason for hiding this comment

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

should use BundlePluginHandler::class instead of the string

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hm, are we fine with the fact that doing so will cause BundlePluginHandler to be autoloaded on every request?

Copy link

Choose a reason for hiding this comment

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

autoloaded just because you use ::class???? why would you need to load the class for that? Please dear PHP god no ...

Copy link

Choose a reason for hiding this comment

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

Phew, I just tested this and of course the ::class syntax does not invoke the autoloader. You almost got me there!

Copy link
Collaborator Author

@bojanz bojanz Oct 13, 2016

Choose a reason for hiding this comment

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

Nice! I learned something new. I assumed the use statement would be enough to trigger it.

Copy link

Choose a reason for hiding this comment

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

Yeah, I remember this discussion. We used to use FQDN in install files instead of use statements before 8.0.x release. Then we cleared the idea that a use statement is not equal to include/include_once.

@Berdir
Copy link
Collaborator

Berdir commented Oct 13, 2016

So this is for the case where the you directly set the plugin on the content entity and that defines the bundle, correct? So like payment.module for example handles the payment type. And not like media_entity.module, which has bundles and then the bundle has a plugin, but you could have multiple bundles using the same plugin?

@bojanz
Copy link
Collaborator Author

bojanz commented Oct 13, 2016

The plugin is the bundle. $entity->type stores the plugin ID. Instead of saying "this entity type uses config bundles, discover them and use them" we say "this entity type uses a plugin bundle, discover them and use them".

So code instead of config driven. And you can always have an $entity->getBundlePlugin() method that gives you the plugin on which you can call other methods.

Copy link

@larowlan larowlan left a comment

Choose a reason for hiding this comment

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

Love this. Awesome work @bojanz

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);

Choose a reason for hiding this comment

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

nit, should pass TRUE as third param

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Have we started enforcing this in core? I'm fine with adding it, just don't see it usually (the array is full of strings here, and strict handling is usually important only for numbers)

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);

Choose a reason for hiding this comment

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

same


use Drupal\entity\BundlePlugin\BundlePluginInterface;

interface BundlePluginTestInterface extends BundlePluginInterface {

Choose a reason for hiding this comment

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

nit: missing docblock. Do we need this or could the manager just look for the parent interface?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added it as an example (cause you'd usually have other methods in there), but I agree that it contributes nothing in this particular case, I'll remove it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

D'oh, Plugin API actually requires an interface to exist.

public function testPluginBundles() {
$bundled_entity_types = entity_get_bundle_plugin_entity_types();
/** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
$entity_type = reset($bundled_entity_types);

Choose a reason for hiding this comment

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

could this lead to a fragile test? The results are keyed by ID, we could access it direct, without the reset()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, there's no reason for that reset.

@larowlan
Copy link

I assume when combined with the plugin module, this gives you the ability to have a field type of 'plugin' that refers back to the instance of the bundle. So you could put additional logic in your interface/on the bundle. $entity->bundle_plugin->instance->doSomething() which is sweet.

@bojanz
Copy link
Collaborator Author

bojanz commented Oct 13, 2016

@larowlan
Sure. I do this in Commerce, no need for Plugin module:

  public function getType() {
    $payment_type_manager = \Drupal::service('plugin.manager.commerce_payment_type');
    return $payment_type_manager->createInstance($this->bundle());
  }

Then $payment->getType()->doSomething().

* example uses BaseFieldDefinition, which is wrong. Tracked in #2346347.
*
* Note that this class implements both FieldStorageDefinitionInterface and
* FieldStorageDefinitionInterface. This is a simplification for DX reasons,

Choose a reason for hiding this comment

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

Does it implement the same FieldStorageDefinitionInterface twice?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch!

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

$bundle_info = $entity_type_bundle_info->getBundleInfo('entity_test_bundle_plugin');
$this->assertEquals(2, count($bundle_info));
$this->assertTrue(isset($bundle_info['first']));
$this->assertTrue(isset($bundle_info['second']));
Copy link

Choose a reason for hiding this comment

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

could use ->assertArrayHasKey() for better readability and verbosity should the test ever fail.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks, addressed.

@jibran
Copy link

jibran commented Oct 13, 2016

To summarize this we can say, a content entity can have different basefields based on different bundles(instances of bundle plugin type).
Edit: These are not basefields

 class BundleFieldDefinition extends BaseFieldDefinition {

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

 }

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.

/**
* Tests the bundle plugins.
*/
public function testPluginBundles() {
Copy link

Choose a reason for hiding this comment

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

Can we add some EFQ asserts here as well? Just to make sure everything is dandy.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea.

$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!

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

$bundles = [];
foreach ($this->pluginManager->getDefinitions() as $plugin_id => $definition) {
$bundles[$plugin_id] = [
'label' => $definition['label'],
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!

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.

@dawehner
Copy link
Collaborator

I think we are good with this PR now. 🎆

@dawehner dawehner merged commit 061376a into fago:8.x-1.x Sep 18, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants