diff --git a/core/lib/Drupal/Core/Config/Entity/Query/Query.php b/core/lib/Drupal/Core/Config/Entity/Query/Query.php index 109efb8382af18b36e7f3ca3ec430cfbd77e41fc..1a589a5af9fffbe1326af1a7681388ef024756ab 100644 --- a/core/lib/Drupal/Core/Config/Entity/Query/Query.php +++ b/core/lib/Drupal/Core/Config/Entity/Query/Query.php @@ -78,6 +78,9 @@ public function condition($property, $value = NULL, $operator = NULL, $langcode * {@inheritdoc} */ public function execute() { + // Invoke entity query alter hooks. + $this->alter(); + // Load the relevant config records. $configs = $this->loadRecords(); diff --git a/core/lib/Drupal/Core/Entity/Query/QueryBase.php b/core/lib/Drupal/Core/Entity/Query/QueryBase.php index 585a8fdf9946f461cb9eeee0ab0351090d86d569..d4dd99e5bd29ef60af3a27166d4a4d05854ef79b 100644 --- a/core/lib/Drupal/Core/Entity/Query/QueryBase.php +++ b/core/lib/Drupal/Core/Entity/Query/QueryBase.php @@ -510,4 +510,29 @@ public static function getClass(array $namespaces, $short_class_name) { } } + /** + * Invoke hooks to allow modules to alter the entity query. + * + * Modules may alter all queries or only those having a particular tag. + * Alteration happens before the query is prepared for execution, so that + * the alterations then get prepared in the same way. + * + * @return $this + * Returns the called object. + */ + protected function alter(): QueryInterface { + $hooks = ['entity_query', 'entity_query_' . $this->getEntityTypeId()]; + if ($this->alterTags) { + foreach ($this->alterTags as $tag => $value) { + // Tags and entity type ids may well contain single underscores, and + // 'tag' is a possible entity type id. Therefore use double underscores + // to avoid collisions. + $hooks[] = 'entity_query_tag__' . $tag; + $hooks[] = 'entity_query_tag__' . $this->getEntityTypeId() . '__' . $tag; + } + } + \Drupal::moduleHandler()->alter($hooks, $this); + return $this; + } + } diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Query.php b/core/lib/Drupal/Core/Entity/Query/Sql/Query.php index 61cdd98874ed3ab000975ce88977b73c7a4d0899..65de824756c93ff5371d06e9376b758727104ee1 100644 --- a/core/lib/Drupal/Core/Entity/Query/Sql/Query.php +++ b/core/lib/Drupal/Core/Entity/Query/Sql/Query.php @@ -77,6 +77,7 @@ public function __construct(EntityTypeInterface $entity_type, $conjunction, Conn */ public function execute() { return $this + ->alter() ->prepare() ->compile() ->addSort() diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php index f3c6e1ae9c4fd1ee4989db66020fed8253fd0861..816817190fc65bcd6e466cdaf44c6567c511fb87 100644 --- a/core/lib/Drupal/Core/Entity/entity.api.php +++ b/core/lib/Drupal/Core/Entity/entity.api.php @@ -2289,6 +2289,65 @@ function hook_entity_extra_field_info_alter(&$info) { } } +/** + * Alter an entity query. + * + * @param \Drupal\Core\Entity\Query\QueryInterface $query + * The entity query. + * + * @see hook_entity_query_ENTITY_TYPE_alter() + * @see hook_entity_query_tag__TAG_alter() + * @see \Drupal\Core\Entity\Query\QueryInterface + */ +function hook_entity_query_alter(\Drupal\Core\Entity\Query\QueryInterface $query): void { + if ($query->hasTag('entity_reference')) { + $entityType = \Drupal::entityTypeManager()->getDefinition($query->getEntityTypeId()); + $query->sort($entityType->getKey('id'), 'desc'); + } +} + +/** + * Alter an entity query for a specific entity type. + * + * @param \Drupal\Core\Entity\Query\QueryInterface $query + * The entity query. + * + * @see hook_entity_query_alter() + * @see \Drupal\Core\Entity\Query\QueryInterface + */ +function hook_entity_query_ENTITY_TYPE_alter(\Drupal\Core\Entity\Query\QueryInterface $query): void { + $query->condition('id', '1', '<>'); +} + +/** + * Alter an entity query that has a specific tag. + * + * @param \Drupal\Core\Entity\Query\QueryInterface $query + * The entity query. + * + * @see hook_entity_query_alter() + * @see hook_entity_query_tag__ENTITY_TYPE__TAG_alter() + * @see \Drupal\Core\Entity\Query\QueryInterface + */ +function hook_entity_query_tag__TAG_alter(\Drupal\Core\Entity\Query\QueryInterface $query): void { + $entityType = \Drupal::entityTypeManager()->getDefinition($query->getEntityTypeId()); + $query->sort($entityType->getKey('id'), 'desc'); +} + +/** + * Alter an entity query for a specific entity type that has a specific tag. + * + * @param \Drupal\Core\Entity\Query\QueryInterface $query + * The entity query. + * + * @see hook_entity_query_ENTITY_TYPE_alter() + * @see hook_entity_query_tag__TAG_alter() + * @see \Drupal\Core\Entity\Query\QueryInterface + */ +function hook_entity_query_tag__ENTITY_TYPE__TAG_alter(\Drupal\Core\Entity\Query\QueryInterface $query): void { + $query->condition('id', '1', '<>'); +} + /** * @} End of "addtogroup hooks". */ diff --git a/core/modules/config/tests/config_test/config_test.module b/core/modules/config/tests/config_test/config_test.module index ac06237e3577ec9ce57534db58b458f8f24c8c04..af5d99cbd28fc34f755551351fc76fad102e9961 100644 --- a/core/modules/config/tests/config_test/config_test.module +++ b/core/modules/config/tests/config_test/config_test.module @@ -7,6 +7,8 @@ require_once dirname(__FILE__) . '/config_test.hooks.inc'; +use Drupal\Core\Entity\Query\QueryInterface; + /** * Implements hook_cache_flush(). */ @@ -41,3 +43,15 @@ function config_test_entity_type_alter(array &$entity_types) { $entity_types['config_test']->set('lookup_keys', ['uuid', 'style']); } } + +/** + * Implements hook_entity_query_tag__ENTITY_TYPE__TAG_alter(). + * + * Entity type is 'config_query_test' and tag is + * 'config_entity_query_alter_hook_test'. + * + * @see Drupal\KernelTests\Core\Entity\ConfigEntityQueryTest::testAlterHook + */ +function config_test_entity_query_tag__config_query_test__config_entity_query_alter_hook_test_alter(QueryInterface $query): void { + $query->condition('id', '7', '<>'); +} diff --git a/core/modules/field/tests/modules/field_test/field_test.module b/core/modules/field/tests/modules/field_test/field_test.module index 34d590bbd2ca2ffb4c9a7ec1016621bc3a645ec2..cbc870be54e042068816b7e5f6a57a7377278f92 100644 --- a/core/modules/field/tests/modules/field_test/field_test.module +++ b/core/modules/field/tests/modules/field_test/field_test.module @@ -13,6 +13,7 @@ */ use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\Core\StringTranslation\TranslatableMarkup; @@ -231,3 +232,46 @@ function field_test_entity_reference_selection_alter(array &$definitions): void unset($definitions['broken']); } } + +/** + * Implements hook_entity_query_alter(). + * + * @see Drupal\KernelTests\Core\Entity\EntityQueryTest::testAlterHook + */ +function field_test_entity_query_alter(QueryInterface $query): void { + if ($query->hasTag('entity_query_alter_hook_test')) { + $query->condition('id', '5', '<>'); + } +} + +/** + * Implements hook_entity_query_ENTITY_TYPE_alter() for 'entity_test_mulrev'. + * + * @see Drupal\KernelTests\Core\Entity\EntityQueryTest::testAlterHook + */ +function field_test_entity_query_entity_test_mulrev_alter(QueryInterface $query): void { + if ($query->hasTag('entity_query_entity_test_mulrev_alter_hook_test')) { + $query->condition('id', '7', '<>'); + } +} + +/** + * Implements hook_entity_query_tag__TAG_alter() for 'entity_query_alter_tag_test'. + * + * @see Drupal\KernelTests\Core\Entity\EntityQueryTest::testAlterHook + */ +function field_test_entity_query_tag__entity_query_alter_tag_test_alter(QueryInterface $query): void { + $query->condition('id', '13', '<>'); +} + +/** + * Implements hook_entity_query_tag__ENTITY_TYPE__TAG_alter(). + * + * Entity type is 'entity_test_mulrev' and tag is + * 'entity_query_entity_test_mulrev_alter_tag_test'. + * + * @see Drupal\KernelTests\Core\Entity\EntityQueryTest::testAlterHook + */ +function field_test_entity_query_tag__entity_test_mulrev__entity_query_entity_test_mulrev_alter_tag_test_alter(QueryInterface $query): void { + $query->condition('id', '15', '<>'); +} diff --git a/core/tests/Drupal/KernelTests/Core/Entity/ConfigEntityQueryTest.php b/core/tests/Drupal/KernelTests/Core/Entity/ConfigEntityQueryTest.php index fe051636930af34f48f2b3fd29361532ae9a9521..cc01678df88f238b64c7b8786a5f6a08893701b7 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/ConfigEntityQueryTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/ConfigEntityQueryTest.php @@ -759,6 +759,24 @@ public function testLookupKeys() { $this->assertNull($key_value->get('style:test')); } + /** + * Test the entity query alter hooks are invoked. + * + * @see config_test_entity_query_tag__config_query_test__config_entity_query_alter_hook_test_alter() + */ + public function testAlterHook(): void { + // Run a test without any condition. + $this->queryResults = $this->entityStorage->getQuery() + ->execute(); + $this->assertResults(['1', '2', '3', '4', '5', '6', '7']); + + // config_test alter hook removes the entity with id '7'. + $this->queryResults = $this->entityStorage->getQuery() + ->addTag('config_entity_query_alter_hook_test') + ->execute(); + $this->assertResults(['1', '2', '3', '4', '5', '6']); + } + /** * Asserts the results as expected regardless of order. * diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php index ee944a5b50d7f3d4a17fa27d7388d903ca58959c..19b9245591460db6d85884d59cbbe764ced25444 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php @@ -1294,6 +1294,60 @@ public function testWithTwoEntityReferenceFieldsToSameEntityType() { $this->assertEquals($entity->id(), reset($result)); } + /** + * Test the entity query alter hooks are invoked. + * + * Hook functions in field_test.module add additional conditions to the query + * removing entities with specific ids. + */ + public function testAlterHook(): void { + $basicQuery = $this->storage + ->getQuery() + ->accessCheck(FALSE) + ->exists($this->greetings, 'tr') + ->condition($this->figures . ".color", 'red') + ->sort('id'); + + // Verify assumptions about the unaltered result. + $query = clone $basicQuery; + $this->queryResults = $query->execute(); + $this->assertResult(5, 7, 13, 15); + + // field_test_entity_query_alter() removes the entity with id '5'. + $query = clone $basicQuery; + $this->queryResults = $query + // Add a tag that no hook function matches. + ->addTag('entity_query_alter_hook_test') + ->execute(); + $this->assertResult(7, 13, 15); + + // field_test_entity_query_entity_test_mulrev_alter() removes the + // entity with id '7'. + $query = clone $basicQuery; + $this->queryResults = $query + // Add a tag that no hook function matches. + ->addTag('entity_query_entity_test_mulrev_alter_hook_test') + ->execute(); + $this->assertResult(5, 13, 15); + + // field_test_entity_query_tag__entity_query_alter_tag_test_alter() removes + // the entity with id '13'. + $query = clone $basicQuery; + $this->queryResults = $query + ->addTag('entity_query_alter_tag_test') + ->execute(); + $this->assertResult(5, 7, 15); + + // field_test_entity_query_tag__entity_test_mulrev__entity_query_ + // entity_test_mulrev_alter_tag_test_alter() + // removes the entity with id '15'. + $query = clone $basicQuery; + $this->queryResults = $query + ->addTag('entity_query_entity_test_mulrev_alter_tag_test') + ->execute(); + $this->assertResult(5, 7, 13); + } + /** * Tests entity queries with condition on the revision metadata keys. */ diff --git a/core/tests/Drupal/Tests/Core/Entity/Query/Sql/QueryTest.php b/core/tests/Drupal/Tests/Core/Entity/Query/Sql/QueryTest.php index 5da95031017f1990ec680eeb5a9d2b1fca68fd55..0da918c29f79800ebe53d77d6cdbd9294c986c66 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Query/Sql/QueryTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Query/Sql/QueryTest.php @@ -5,9 +5,11 @@ namespace Drupal\Tests\Core\Entity\Query\Sql; use Drupal\Core\Entity\EntityType; +use Drupal\Core\Extension\ModuleHandler; use Drupal\Tests\UnitTestCase; use Drupal\Core\Entity\Query\QueryException; use Drupal\Core\Entity\Query\Sql\Query; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * @coversDefaultClass \Drupal\Core\Entity\Query\Sql\Query @@ -33,6 +35,13 @@ protected function setUp(): void { $namespaces = ['Drupal\Core\Entity\Query\Sql']; $this->query = new Query($entity_type, $conjunction, $connection, $namespaces); + + $container = $this->createMock(ContainerInterface::class); + $container->expects($this->any()) + ->method('get') + ->with('module_handler') + ->will($this->returnValue($this->createMock(ModuleHandler::class))); + \Drupal::setContainer($container); } /**