Problem/Motivation

Entity storage controllers allow you to retrieve and save data on multiple tables, but entity query's SQL back end does not support the same. This limits the flexibility of extending entities with swappable entity storage controllers. We have run into this problem trying to port our contrib module to drupal 8.

Context

The specific thing we are working on is allowing an Anonymous User API where we create the separation between People and Accounts as talked about in #1806514: Unify anonymous and registered users. Swappable entity storage controllers allow us to leave account information in the users table and introduce a new table for people who may or may not have an account.

The storage controller deals with saving to both tables and retrieving the information via a join. However, entity queries break when trying to add a condition on a property of a joined table as Drupal\Core\Entity\Query\Sql\Tables::ensureEntityTable() is hard-coded to the base table and data table from the entity info.

To solve this, I thought I could override Tables with a specific one for my entity. However, Tables is a hard-coded class name in numerous places, meaning the only way to do that would be to the:

  • Override Drupal\Core\Entity\DatabaseStorageController::getQueryServiceName() to use a different QueryFactory
  • Override Drupal\Core\Entity\Query\Sql\QueryFactory::get() and Drupal\Core\Entity\Query\Sql\QueryFactory::getAggregate() to use different Query and QueryAggregate classes...
  • Override getSqlField() in both Query and QueryAggregate so that they use our new Tables() class
  • Override the entirety of compile() in both Condition and ConditionAggregate
  • And I haven't even looked to find all the places I would need to override to get it using the correct Condition and ConditionAggregate

While this route would should work, it is not "Don't Repeat Yourself", requires lots of contrib code and diminishes DX.

Proposed resolution

I have thought of a couple approaches that could make this whole thing a bunch easier:

A) Allow entity definitions or storage controllers to specify/return the Tables implementation:
This would allow contrib developers to override anything they need to in Tables to match whatever they are doing.

B) Allow additional tables to be included in the entity info the same way that data_table is:
This would be a much simpler change, but allow lots of flexibility for contrib developers to store information across multiple tables if required.

API changes

Depending on approach, probably either some additional information in the entity definition or an additional method on the storage controller. In either case, they would only need a developer to do anything if they want to do something different to the default behavior.

Comments

andrewbelcher’s picture

Status: Active » Needs review
StatusFileSize
new1.77 KB

Here is a patch for solution B.

I've added key to the entity info to allow entity definitions and hook_entity_info_alter() to specify additional table that data can be looked up on for entity queries and added them in to the list of tables in Drupal\Core\Entity\Query\Sql\Tables::addField().

Status: Needs review » Needs work
Issue tags: -Contributed project blocker

The last submitted patch, 2038707-1-allow_additional_tables_in_entity_definition.patch, failed testing.

andrewbelcher’s picture

Status: Needs work » Needs review
Issue tags: +Contributed project blocker
andrewbelcher’s picture

Hmm... that was an odd fail... Perhaps it was just some invalid characters...
Link with label Test Operation: 9(S\8G'9 found. Other EntityOperationsTest.php 54 Drupal\system\Tests\Entity\EntityOperationsTest->testEntityOperationAlter()

rlmumford’s picture

chx’s picture

Thanks for the bug report, this is very interesting. I am fairly certain this is not the solution we will go with because it puts an SQL-only thing on the entity class. The simplest would be put this on the storage controller? Then you would only need to override the storage controller and that'd be it.

Alternatively, we could look at an extender-like solution for the entity query system -- would that help, I wonder? Could we put the aggregator under it?

rlmumford’s picture

StatusFileSize
new3.1 KB

Here's a patch that puts a getTables() method into the DatabaseStorageController class. This method allows us to do everything we need (because we can swap 'Tables' out for our own class that extends it very easily.

It feels like the getTables() method would fit better on Drupal\Core\Entity\Query\Sql\Query, however if you put the method there you have to override every class in Drupal\Core\Entity\Query\Sql namespace.

I think the only other option is to make it so you can pass the Table's class into the constructor of Drupal\Core\Entity\Query\Sql\Query or add a setTables() method to all of those classes. Then it would be possible to register a new service for a given entity type and have a new factory object that passes a different Tables class in, but that uses the same Drupal\Core\Entity\Query\Sql classes.

Status: Needs review » Needs work

The last submitted patch, 2038707-7.patch, failed testing.

rlmumford’s picture

Status: Needs work » Needs review
StatusFileSize
new3.37 KB

Forgot to add the Tables class to the list at the top.

Status: Needs review » Needs work

The last submitted patch, 2038707-9.patch, failed testing.

rlmumford’s picture

Status: Needs work » Needs review
StatusFileSize
new3.37 KB

Status: Needs review » Needs work

The last submitted patch, 2038707-11.patch, failed testing.

rlmumford’s picture

Status: Needs work » Needs review
StatusFileSize
new3.6 KB

Here's another go.

dawehner’s picture

It would be great if the issue summary would describe why the chosen route was implemented (A not B).

As chx suggested in IRC, there might be better a TableInterface.

chx’s picture

StatusFileSize
new10.52 KB

Thanks much! I think making Tables pluggable makes a tremendous amount of sense -- we made everything pluggable here, the Query, the Condition class so why not Tables. However, this doesn't belong to the storage controller either, it's an internal affair to the query class. I have made it so that just overriding QueryFactory, Query, QueryAggregate in a new namespace without any methods will behave appropriately: QueryFactory will construct Query and QueryAggregate in that namespace and Query will use the Tables again in that namespace.

Discussed the removal of conditionGroupFactory from the interface with alexpott and he greenlighted the API change.

Status: Needs review » Needs work

The last submitted patch, 2038707_16.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
StatusFileSize
new2.17 KB
new10.75 KB

PHP sucks. __NAMESPACE__ is the defining class, there's no way to get the current namespace aside from string parsing the current class name.

chx’s picture

StatusFileSize
new10.73 KB
new2.29 KB

That regex can be simplified a little.

Status: Needs review » Needs work

The last submitted patch, 2038707_19.patch, failed testing.

yanniboi’s picture

+++ b/core/lib/Drupal/Core/Entity/Query/ConditionFundamentals.php
@@ -30,13 +30,21 @@
+  public function __construct($conjunction, QueryInterface $query) {

After adding the QueryInterface object to the __construct arguments, I think you need to specify:

use Drupal\Core\Entity\Query\QueryInterface;
yanniboi’s picture

Status: Needs work » Needs review
StatusFileSize
new13.45 KB
new494 bytes

Here is an attempt to fix the test fail...

Status: Needs review » Needs work

The last submitted patch, 2038707-21.patch, failed testing.

yanniboi’s picture

No, I was wrong, the issue isn't a missing 'use ... ;', it is that in \Drupal\Core\Entity\Query\Sql\QueryAggregate::conditionAggregateGroupFactory a new ConditionAggregate object is being created:

  /**
   * Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::conditionAggregateGroupFactory().
   */
  public function conditionAggregateGroupFactory($conjunction = 'AND') {
    return new ConditionAggregate($conjunction);
  }

ConditionAggregate extends ConditionAggregateBase which extends ConditionFundamentals.
Since the __construct() for ConditionFundamentals this now needs a QueryInterface object:

+++ b/core/lib/Drupal/Core/Entity/Query/ConditionFundamentals.php
@@ -30,13 +30,21 @@
+  public function __construct($conjunction, QueryInterface $query) {

we need to add $query to the arguments for conditionAggregateGroupFactory().

yanniboi’s picture

Status: Needs work » Needs review
StatusFileSize
new635 bytes
new11.35 KB

Ok, I ran tests this time before patching, and this seems to pass the test, although it seems almost too simple. I passed '$this' as a second argument to conditionAggregateGroupFactory() as $this is a QueryAggregate object and thus and extension of QueryInterface.

Let me know if this is wrong...

chx’s picture

Thanks so much! Welcome to your first core patch :) and that is correct.

dawehner’s picture

@@ -171,6 +171,24 @@ public function range($start = NULL, $length = NULL) {
+   * @param $conjunction
...
+   * return \Drupal\Core\Entity\Query\ConditionInterface

Let's nitpick ... this should be @param string $conjunction and @return instead.

@@ -171,6 +171,24 @@ public function range($start = NULL, $length = NULL) {
+    preg_match('/^.*\\\\/', get_class($this), $matches);
+    $class = $matches[0] .'Condition';

@@ -320,4 +314,15 @@ public function __clone() {
+    preg_match('/^.*\\\\/', get_class($this), $matches);
+    $class = $matches[0] .'Tables';

@@ -48,8 +48,10 @@ function __construct(Connection $connection) {
+    preg_match('/^.*\\\\/', get_class($this), $matches);
+    $class = $matches[0] .'Query';

@@ -64,8 +66,10 @@ function get($entity_type, $conjunction, EntityManager $entity_manager) {
+    preg_match('/^.*\\\\/', get_class($this), $matches);
+    $class = $matches[0] .'QueryAggregate';

It would seriously help to document what this regex does.

@@ -15,6 +15,11 @@
   /**
+   * @var \Drupal\Core\Entity\Query\Sql\Query
+   */
+  protected $query;

Let's also add some sentence about it if we already add some documentation

@@ -320,4 +314,15 @@ public function __clone() {
+   * Gets the Tables object for this query.
+   *

Missings docs for the parameter.

@@ -320,4 +314,15 @@ public function __clone() {
+  public function getTables(SelectInterface $sql_query) {

Is there a reason for this function to be public and not protected? I can't find a call outside of the class.

@@ -0,0 +1,29 @@
+
...
+

Remove these empty lines to use them later :p

@@ -0,0 +1,29 @@
+interface TablesInterface {
+  /**
...
+  public function addField($field, $type, $langcode);
+}

Let's put some empty lines in between.

dawehner’s picture

one thing i missed: There should be a space after the ., so for example

$class = $matches[0] . 'QueryAggregate';

Let's also introduce a static helper method on the QueryBase so we don't end up with the same regex 4 times.

chx’s picture

Status: Needs review » Needs work

Actually, I have reviewed the PHP docs (I though I already did) and the following syntax is valid:

$a = new namespace\sub\cname(); // instantiates object of class MyProject\sub\cname

Please change / simplify the code to use this.

chx’s picture

Status: Needs work » Needs review
StatusFileSize
new3.85 KB
new11.21 KB

> Is there a reason for this function to be public and not protected? I can't find a call outside of the class.

Condition calls it.

Status: Needs review » Needs work

The last submitted patch, 2038707_30.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
StatusFileSize
new12 KB
new2.94 KB

Ah yes, that's not good enough. Posted a question to http://stackoverflow.com/questions/18091684/tthe-namespace-equivalent-of... here and here's the helper.

tim.plunkett’s picture

+++ b/core/lib/Drupal/Core/Entity/Query/QueryBase.phpundefined
@@ -402,4 +419,17 @@ protected function getAggregationAlias($field, $function) {
+   * @param $object

+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Query.phpundefined
@@ -320,4 +314,17 @@ public function __clone() {
+   * @return \Drupal\Core\Entity\Query\Sql\TablesInterface

Needs extra docblock bits.

+++ b/core/lib/Drupal/Core/Entity/Query/QueryBase.phpundefined
@@ -402,4 +419,17 @@ protected function getAggregationAlias($field, $function) {
+  public static function getNamespace($object) {
+    return preg_replace('/.[^\\\\]+$/', '', get_class($object));

Looks kosher, I don't know of anything in PHP to do this...

+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Query.phpundefined
@@ -289,7 +283,7 @@ protected function result() {
-      $this->tables = new Tables($this->sqlQuery);
+      $this->tables = $this->getTables($this->sqlQuery);

This is great.

chx’s picture

StatusFileSize
new1.09 KB
new12.09 KB

Added extra doxygen and changed getNamespaces to not use a regex cos this is ever so slightly faster. And fixed getTables to actually do what we wanted it to do, the namespace stuff got stuck in there.

Status: Needs review » Needs work

The last submitted patch, 2038707_34.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
StatusFileSize
new12.08 KB
new449 bytes

This patch tends to go pear shaped...

dawehner’s picture

@@ -402,4 +419,18 @@ protected function getAggregationAlias($field, $function) {
+   * @param $object

Is @param object $object a valid syntax?

@@ -0,0 +1,29 @@
+  public function addField($field, $type, $langcode);

Missing docs for $langcode

yanniboi’s picture

StatusFileSize
new2.01 KB
new12.42 KB

@dawehner

Is @param object $object a valid syntax?

Looks like Drupal\Core\Entity\Query::compile uses the same syntax, and I can't find any use of '@param object $foo'.

   * @param $query
   *   The query object this conditional clause belongs to.
   */
  public function compile($query);

Maybe it's fine as is?

Missing docs for $langcode

Have added docs. Feel free to nitpick.

dawehner’s picture

Thank you! Here are some nitpicks.

@@ -50,7 +50,20 @@ public function __construct(SelectInterface $sql_query) {
+   *   The language code the field values are to be shown in.

@@ -13,12 +13,16 @@
+   *   The language code the field values are to be shown in.
...
    * @return string

Let's put some empty lines between there.

@@ -50,7 +50,20 @@ public function __construct(SelectInterface $sql_query) {
+   * @throws \Drupal\Core\Entity\Query\QueryException

@@ -13,12 +13,16 @@
+   * @throws \Drupal\Core\Entity\Query\QueryException

I guess it would make sense to explain when this exception is thrown, because it does not seem to obvious.

yanniboi’s picture

StatusFileSize
new1.47 KB
new12.55 KB

No problem!

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Thank you!

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Condition.phpundefined
@@ -15,6 +15,13 @@
   /**
+   * The SQL entity query object this condition belongs to.
+   *
+   * @var \Drupal\Core\Entity\Query\Sql\Query
+   */
+  protected $query;

This is already declared on ConditionFundementals (which this inherits from) and therefore does not need to be here.

+++ b/core/lib/Drupal/Core/Entity/Query/Sql/TablesInterface.phpundefined
@@ -0,0 +1,36 @@
+interface TablesInterface {

We're adding this interface but we're not using it? I guess it was supposed to be used by the Tables class

chx’s picture

Status: Needs work » Needs review
StatusFileSize
new423 bytes
new12.75 KB

> This is already declared on ConditionFundementals (which this inherits from) and therefore does not need to be here.

Nope, the generic is merely QueryInterface, this specific $query is a Sql one so separate doxygen is justified.

alexpott’s picture

Status: Needs review » Needs work

@chx pointed out that the protected $query; is good because it declares that this is an instance of @var \Drupal\Core\Entity\Query\Sql\Query which defines the getTables() method.

alexpott’s picture

Status: Needs work » Needs review

xpost

chx’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new1.36 KB
new12.52 KB

Removed the addField doxygen in favor of {@inheritdoc} . This is ready . interdiff is against #40.

alexpott’s picture

Title: Entity query sql backend limits storage controllers changes in contrib » Change notice: Entity query sql backend limits storage controllers changes in contrib
Priority: Normal » Critical
Status: Reviewed & tested by the community » Active
Issue tags: +API change, +Approved API change

Catch and I have discussed this offline and have agreed this is a good change to make it easier to provide entity storage controllers in contrib.

Committed 265940c and pushed to 8.x. Thanks!

chx’s picture

Title: Change notice: Entity query sql backend limits storage controllers changes in contrib » Entity query sql backend limits storage controllers changes in contrib
Priority: Critical » Normal
Status: Active » Fixed

Automatically closed -- issue fixed for 2 weeks with no activity.