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 differentQueryFactory - Override
Drupal\Core\Entity\Query\Sql\QueryFactory::get()andDrupal\Core\Entity\Query\Sql\QueryFactory::getAggregate()to use differentQueryandQueryAggregateclasses... - Override
getSqlField()in bothQueryandQueryAggregateso that they use our newTables()class - Override the entirety of
compile()in bothConditionandConditionAggregate - And I haven't even looked to find all the places I would need to override to get it using the correct
ConditionandConditionAggregate
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.
| Comment | File | Size | Author |
|---|---|---|---|
| #46 | 2038707_46.patch | 12.52 KB | chx |
| #46 | interdiff.txt | 1.36 KB | chx |
| #43 | 2038707_43.patch | 12.75 KB | chx |
| #43 | interdiff.txt | 423 bytes | chx |
| #40 | 2038707_40.patch | 12.55 KB | yanniboi |
Comments
Comment #1
andrewbelcher commentedHere 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 inDrupal\Core\Entity\Query\Sql\Tables::addField().Comment #3
andrewbelcher commented#1: 2038707-1-allow_additional_tables_in_entity_definition.patch queued for re-testing.
Comment #4
andrewbelcher commentedHmm... 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()Comment #5
rlmumfordThis issue will potentially be solved by #1497374: Switch from Field-based storage to Entity-based storage
Comment #6
chx commentedThanks 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?
Comment #7
rlmumfordHere'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 inDrupal\Core\Entity\Query\Sqlnamespace.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\Queryor 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 sameDrupal\Core\Entity\Query\Sqlclasses.Comment #9
rlmumfordForgot to add the Tables class to the list at the top.
Comment #11
rlmumfordComment #13
rlmumfordHere's another go.
Comment #14
dawehnerIt 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.
Comment #16
chx commentedThanks 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.
Comment #18
chx commentedPHP sucks. __NAMESPACE__ is the defining class, there's no way to get the current namespace aside from string parsing the current class name.
Comment #19
chx commentedThat regex can be simplified a little.
Comment #21
yanniboi commentedAfter adding the QueryInterface object to the __construct arguments, I think you need to specify:
Comment #22
yanniboi commentedHere is an attempt to fix the test fail...
Comment #24
yanniboi commentedNo, I was wrong, the issue isn't a missing 'use ... ;', it is that in
\Drupal\Core\Entity\Query\Sql\QueryAggregate::conditionAggregateGroupFactorya new ConditionAggregate object is being created:ConditionAggregate extends ConditionAggregateBase which extends ConditionFundamentals.
Since the __construct() for ConditionFundamentals this now needs a QueryInterface object:
we need to add $query to the arguments for conditionAggregateGroupFactory().
Comment #25
yanniboi commentedOk, 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...
Comment #26
chx commentedThanks so much! Welcome to your first core patch :) and that is correct.
Comment #27
dawehnerLet's nitpick ... this should be @param string $conjunction and @return instead.
It would seriously help to document what this regex does.
Let's also add some sentence about it if we already add some documentation
Missings docs for the parameter.
Is there a reason for this function to be public and not protected? I can't find a call outside of the class.
Remove these empty lines to use them later :p
Let's put some empty lines in between.
Comment #28
dawehnerone thing i missed: There should be a space after the ., so for example
Let's also introduce a static helper method on the QueryBase so we don't end up with the same regex 4 times.
Comment #29
chx commentedActually, I have reviewed the PHP docs (I though I already did) and the following syntax is valid:
Please change / simplify the code to use this.
Comment #30
chx commented> 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.
Comment #32
chx commentedAh 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.
Comment #33
tim.plunkettNeeds extra docblock bits.
Looks kosher, I don't know of anything in PHP to do this...
This is great.
Comment #34
chx commentedAdded 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.
Comment #36
chx commentedThis patch tends to go pear shaped...
Comment #37
dawehnerIs @param object $object a valid syntax?
Missing docs for $langcode
Comment #38
yanniboi commented@dawehner
Looks like Drupal\Core\Entity\Query::compile uses the same syntax, and I can't find any use of '@param object $foo'.
Maybe it's fine as is?
Have added docs. Feel free to nitpick.
Comment #39
dawehnerThank you! Here are some nitpicks.
Let's put some empty lines between there.
I guess it would make sense to explain when this exception is thrown, because it does not seem to obvious.
Comment #40
yanniboi commentedNo problem!
Comment #41
dawehnerThank you!
Comment #42
alexpottThis is already declared on ConditionFundementals (which this inherits from) and therefore does not need to be here.
We're adding this interface but we're not using it? I guess it was supposed to be used by the Tables class
Comment #43
chx commented> 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.
Comment #44
alexpott@chx pointed out that the
protected $query;is good because it declares that this is an instance of@var \Drupal\Core\Entity\Query\Sql\Querywhich defines thegetTables()method.Comment #45
alexpottxpost
Comment #46
chx commentedRemoved the addField doxygen in favor of {@inheritdoc} . This is ready . interdiff is against #40.
Comment #47
alexpottCatch 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!
Comment #48
chx commentedCreated Extending the default entity query backend.