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 #3314741: EntityFieldManager::getFieldMap() doesn't show farmOS bundle fields #583

Open
wants to merge 3 commits into
base: 2.x
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- [Issue #3314741: EntityFieldManager::getFieldMap() doesn't show farmOS bundle fields](https://www.drupal.org/project/farm/issues/3314741)

## [2.0.0-beta7] 2022-09-29

### Added
Expand Down
6 changes: 3 additions & 3 deletions modules/asset/equipment/farm_equipment.module
Expand Up @@ -8,9 +8,9 @@
use Drupal\Core\Entity\EntityTypeInterface;

/**
* Implements hook_farm_entity_bundle_field_info().
* Implements hook_entity_base_field_info().
*/
function farm_equipment_farm_entity_bundle_field_info(EntityTypeInterface $entity_type, string $bundle) {
function farm_equipment_entity_base_field_info(EntityTypeInterface $entity_type) {
$fields = [];

// Add an Equipment reference field to logs.
Expand All @@ -27,7 +27,7 @@ function farm_equipment_farm_entity_bundle_field_info(EntityTypeInterface $entit
'view' => -5,
],
];
$fields['equipment'] = \Drupal::service('farm_field.factory')->bundleFieldDefinition($options);
$fields['equipment'] = \Drupal::service('farm_field.factory')->baseFieldDefinition($options);
}

return $fields;
Expand Down
115 changes: 115 additions & 0 deletions modules/core/entity/farm_entity.module
Expand Up @@ -14,6 +14,121 @@ use Drupal\entity\EntityPermissionProvider;
use Drupal\farm_entity\BundlePlugin\FarmEntityBundlePluginHandler;
use Drupal\farm_entity\Routing\DefaultHtmlRouteProvider;

/**
* Implements hook_modules_installed().
*/
function farm_entity_modules_installed($modules, $is_syncing) {

// Add bundle fields to the bundle field map.
_farm_entity_rebuild_bundle_field_map('install', $modules);
}

/**
* Implements hook_modules_uninstalled().
*/
function farm_entity_modules_uninstalled($modules, $is_syncing) {

// Remove bundle fields from the bundle field map.
_farm_entity_rebuild_bundle_field_map('uninstall', $modules);
}

/**
* Helper function for rebuilding bundle field maps.
*
* This runs when modules are installed/uninstalled, loads bundle fields that
* are defined by the module via hook_farm_entity_bundle_field_info(), and
* updates the entity field map accordingly.
*
* @param string $op
* Operation being performed. Must be either 'install' or 'uninstall'.
* @param array $modules
* The list of modules that are being installed/uninstalled.
*/
function _farm_entity_rebuild_bundle_field_map(string $op, array $modules) {

// If the operation is not "install" or "uninstall", bail.
if (!in_array($op, ['install', 'uninstall'])) {
return;
}

// If none of the modules implement hook_farm_entity_bundle_field_info(),
// bail.
if (!\Drupal::moduleHandler()->hasImplementations('farm_entity_bundle_field_info', $modules)) {
return;
}

// Iterate through entity types.
$entity_type_definitions = \Drupal::service('entity_type.manager')->getDefinitions();
foreach ($entity_type_definitions as $entity_type => $entity_type_definition) {

// Only proceed for entity types that use bundle plugins.
if (!in_array($entity_type, ['asset', 'log', 'plan', 'quantity'])) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm seeing this array in a lot of places - sometimes in different orders or with/without the 'data_stream' entry.

Would it make sense to refactor it into a constant - or maybe just additional attributes on the entity definitions themselves?

e.g. here;

if (!$entity_type_definition->supportsBundlePlugins()) {
    continue;
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh good idea! Didn't know about the supportsBundlePlugins() method!

Copy link
Collaborator

Choose a reason for hiding this comment

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

image

Copy link
Member

Choose a reason for hiding this comment

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

I think we could use EntityTypeInterface::getBundleEntityType()


  /**
   * Gets the name of the entity type which provides bundles.
   *
   * @return string|null
   *   The name of the entity type which provides bundles, or NULL if the entity
   *   type does not have a bundle entity type.
   */
  public function getBundleEntityType();

continue;
}

// Get the bundle field map key value collection.
$bundle_field_map = \Drupal::service('keyvalue')->get('entity.definitions.bundle_field_map')->get($entity_type) ?? [];

// Get a list of installed bundles for this entity type.
$bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type);

// Iterate through bundles.
foreach ($bundles as $bundle => $bundle_info) {

// Iterate through the modules.
foreach ($modules as $module) {

// Invoke hook_farm_entity_bundle_field_info() to get bundle field
// definitions added by the module.
/** @var \Drupal\entity\BundleFieldDefinition[] $bundle_fields */
$bundle_fields = \Drupal::service('module_handler')->invoke($module, 'farm_entity_bundle_field_info', [$entity_type_definition, $bundle]) ?? [];

// If bundle fields are empty, skip.
if (empty($bundle_fields)) {
continue;
}

// Iterate through the bundle field definitions to update the bundle
// field map. This mimics the field_definition.listener service's
// onFieldDefinitionCreate() and onFieldDefinitionDelete() behavior.
// @see Drupal\Core\Field\FieldDefinitionListener::onFieldDefinitionCreate()
// @see Drupal\Core\Field\FieldDefinitionListener::onFieldDefinitionDelete()
foreach ($bundle_fields as $field_name => $bundle_field) {

// If we are installing, add to the field map.
if ($op == 'install') {
if (!isset($bundle_field_map[$field_name])) {
// This field did not exist yet, initialize it with the type and
// empty bundle list.
$bundle_field_map[$field_name] = [
'type' => $bundle_field->getType(),
'bundles' => [],
];
}
$bundle_field_map[$field_name]['bundles'][$bundle] = $bundle;
}

// If we are uninstalling, remove from the field map.
elseif ($op == 'uninstall') {
unset($bundle_field_map[$field_name]['bundles'][$bundle]);
if (empty($bundle_field_map[$field_name]['bundles'])) {
unset($bundle_field_map[$field_name]);
}
}
}
}
}

// Set the bundle field map key value collection.
if (!empty($bundle_field_map)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What if it became empty as part of the changes above? Shouldn't it still get set in that case?

\Drupal::service('keyvalue')->get('entity.definitions.bundle_field_map')->set($entity_type, $bundle_field_map);
}
}

// Clear the entity field map cache.
\Drupal::service('cache.discovery')->delete('entity_field_map');
}

/**
* Implements hook_module_implements_alter().
*/
Expand Down
72 changes: 72 additions & 0 deletions modules/core/entity/farm_entity.post_update.php
@@ -0,0 +1,72 @@
<?php

/**
* @file
* Post update functions for farmOS entity module.
*/

/**
* Add hook_farm_entity_bundle_field_info() fields to bundle field maps.
*/
function farm_entity_post_update_add_farm_entity_bundle_field_maps(&$sandbox = NULL) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's this $sandbox parameter? Do we need to use/honor it?

Comment on lines +8 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

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

Probably just my Drupal naivete showing here, but does this implement a hook or is it called some other way?


// Iterate through entity types.
$entity_type_definitions = \Drupal::service('entity_type.manager')->getDefinitions();
foreach ($entity_type_definitions as $entity_type => $entity_type_definition) {

// Only proceed for entity types that use bundle plugins.
if (!in_array($entity_type, ['asset', 'log', 'plan', 'quantity'])) {
continue;
}

// Get the bundle field map key value collection.
$bundle_field_map = \Drupal::service('keyvalue')->get('entity.definitions.bundle_field_map')->get($entity_type) ?? [];

// Get a list of installed bundles for this entity type.
$bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type);

// Iterate through bundles.
foreach ($bundles as $bundle => $bundle_info) {

// Invoke hook_farm_entity_bundle_field_info() on all modules with a
// callback function to add bundle fields to the entity field map.
\Drupal::service('module_handler')->invokeAllWith(
'farm_entity_bundle_field_info',
function (callable $hook) use ($entity_type_definition, $bundle, &$bundle_field_map) {

// Get bundle fields defined by the module.
$bundle_fields = $hook($entity_type_definition, $bundle) ?? [];

// If bundle fields are empty, bail.
if (empty($bundle_fields)) {
return;
}

// Iterate through the bundle field definitions to add fields to the
// bundle field map. This mimics the field_definition.listener
// service's onFieldDefinitionCreate() behavior.
// @see Drupal\Core\Field\FieldDefinitionListener::onFieldDefinitionCreate()
foreach ($bundle_fields as $field_name => $bundle_field) {
if (!isset($bundle_field_map[$field_name])) {
// This field did not exist yet, initialize it with the type and
// empty bundle list.
$bundle_field_map[$field_name] = [
'type' => $bundle_field->getType(),
'bundles' => [],
];
}
$bundle_field_map[$field_name]['bundles'][$bundle] = $bundle;
}
}
);
}

// Set the bundle field map key value collection.
if (!empty($bundle_field_map)) {
\Drupal::service('keyvalue')->get('entity.definitions.bundle_field_map')->set($entity_type, $bundle_field_map);
}
}

// Delete the entity field map cache entry.
\Drupal::service('cache.discovery')->delete('entity_field_map');
Comment on lines +12 to +71
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why implement this all again? Couldn't it just call _farm_entity_rebuild_bundle_field_map('install', $modules) with a list of all the modules that implement hook_farm_entity_bundle_field_info?

}
Expand Up @@ -59,6 +59,49 @@ protected function setUp():void {
$this->moduleInstaller = $this->container->get('module_installer');
}

/**
* Test that bundle field maps are updated on install/uninstall.
*/
public function testBundleFieldMapUpdates() {

// Get the entity field map.
$field_map = $this->entityFieldManager->getFieldMap();

// Confirm that the 'test_default_bundle_field' exists in the log field map.
$this->assertArrayHasKey('test_default_bundle_field', $field_map['log']);

// Confirm that the 'test_contrib_hook_bundle_field' does NOT exist (yet).
$this->assertArrayNotHasKey('test_contrib_hook_bundle_field', $field_map['log']);

// Install the farm_entity_contrib_test module.
$result = $this->moduleInstaller->install(['farm_entity_contrib_test']);
$this->assertTrue($result);

// Reload the entity field map.
$this->entityFieldManager->setFieldMap([]);
\Drupal::service('cache.discovery')->delete('entity_field_map');
$field_map = $this->entityFieldManager->getFieldMap();

// Confirm that the 'test_contrib_hook_bundle_field' exists in the log field
// map, and exists in the 'test' bundle, but not in 'test_override'.
$this->assertArrayHasKey('test_contrib_hook_bundle_field', $field_map['log']);
$this->assertContains('test', $field_map['log']['test_contrib_hook_bundle_field']['bundles']);
$this->assertNotContains('test_override', $field_map['log']['test_contrib_hook_bundle_field']['bundles']);

// Uninstall the farm_entity_contrib_test module.
$result = $this->moduleInstaller->uninstall(['farm_entity_contrib_test']);
$this->assertTrue($result);

// Reload the entity field map.
$this->entityFieldManager->setFieldMap([]);
\Drupal::service('cache.discovery')->delete('entity_field_map');
$field_map = $this->entityFieldManager->getFieldMap();

// Confirm that the 'test_contrib_hook_bundle_field' no longer exists in the
// log field map.
$this->assertArrayNotHasKey('test_contrib_hook_bundle_field', $field_map['log']);
}
Comment on lines +62 to +103
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this also test the case where more than one module provides a given bundle field? i.e. that the bundle gets added, the field mapping doesn't get overwritten or prematurely deleted?


/**
* Test installing the farm_entity_contrib_test module after farm_entity_test.
*/
Expand Down
1 change: 1 addition & 0 deletions modules/core/quick/farm_quick.info.yml
Expand Up @@ -6,5 +6,6 @@ core_version_requirement: ^9
dependencies:
- drupal:taxonomy
- farm:asset
- farm:farm_field
- farm:farm_quantity_standard
- log:log
6 changes: 3 additions & 3 deletions modules/core/quick/farm_quick.module
Expand Up @@ -33,9 +33,9 @@ function farm_quick_help($route_name, RouteMatchInterface $route_match) {
}

/**
* Implements hook_farm_entity_bundle_field_info().
* Implements hook_entity_base_field_info().
*/
function farm_quick_farm_entity_bundle_field_info(EntityTypeInterface $entity_type, string $bundle) {
function farm_quick_entity_base_field_info(EntityTypeInterface $entity_type) {
$fields = [];

// We only act on asset and log entities.
Expand All @@ -51,7 +51,7 @@ function farm_quick_farm_entity_bundle_field_info(EntityTypeInterface $entity_ty
'multiple' => TRUE,
'hidden' => TRUE,
];
$fields['quick'] = \Drupal::service('farm_field.factory')->bundleFieldDefinition($options);
$fields['quick'] = \Drupal::service('farm_field.factory')->baseFieldDefinition($options);

return $fields;
}
5 changes: 3 additions & 2 deletions modules/core/quick/tests/src/Kernel/QuickFormTest.php
Expand Up @@ -24,6 +24,7 @@ class QuickFormTest extends KernelTestBase {
*/
protected static $modules = [
'asset',
'farm_field',
'farm_quantity_standard',
'farm_quick',
'farm_quick_test',
Expand Down Expand Up @@ -94,13 +95,13 @@ public function testQuickFormSubmission() {
$this->assertNotEmpty($storage['assets'][0]->id());

// Confirm that the asset is linked to the quick form.
$this->assertEquals('test', $storage['assets'][0]->quick[0]);
$this->assertEquals('test', $storage['assets'][0]->get('quick')[0]->value);
Comment on lines -97 to +98
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why does this need to change?


// Confirm that a log was created.
$this->assertNotEmpty($storage['logs'][0]->id());

// Confirm that the log is linked to the quick form.
$this->assertEquals('test', $storage['logs'][0]->quick[0]);
$this->assertEquals('test', $storage['logs'][0]->get('quick')[0]->value);

// Confirm that a quantity was created.
$this->assertNotEmpty($storage['quantities'][0]->id());
Expand Down