Score:2

How to use a referenced entity's values in a TableSort?

ai flag

I have a custom entity with a field which is defined as follows.

$fields['file'] = BaseFieldDefinition::create('file')
  ->setDescription('Reference to the built-in core File entity type.')
  ->setLabel('File')
  ->setRequired(TRUE)
  ->setSetting('file_extensions', 'pdf rtf doc docx')
  ->setSetting('max_filesize', '20MB')
  ->setSetting('description_field', TRUE)
  ->setDisplayOptions('form', ['type' => 'file'])
  ->setDisplayOptions('view', ['type' => 'file']);

I have no problem getting to the fields of the referenced File entity from instances of my own entity type. So for example, these lines work as expected.

$filename = $my_entity->file->entity->filename->value;
$username = $my_entity->file->entity->uid->entity->name->value;

When I try to use the fields of the referenced File entity with a TableSort, which I want to plug into an entity query on my custom entity type, the code throws an exception.

$header['name'] = [
  'data' => 'File Name',
  'field' => 'file.entity.filename',
  'specifier' => 'file.entity.filename',
];
$header['user'] = [
  'data' => 'Uploaded By',
  'field' => 'file.entity.uid.entity.name',
  'specifier' => 'file.entity.uid.entity.name',
];

// …

$query->tableSort($header);

Uncaught PHP Exception Drupal\Core\Entity\Query\QueryException: "'file' not found"

For the syntax of the $header array I'm largely relying on clues I got from Tablesort with EntityQuery, in which Berdir recommends that we look at the load() and buildHeader() methods of the UserListBuilder class. Unfortunately, that example appears to be using field values which are stored directly in the base table, so I'm falling back on the assumption that, since we're in entity query land, the same syntax described in QueryInterface::condition() would be expected.

I've looked at a number of the open issues, such as Table sort ignores "field", always adds header title as order query parameter and Tablesort cleanup, but I didn't see anything which shed any light on the problem.

cn flag
It's definitely supported with the syntax you're using, as this works fine for nodes using e.g. `uid.entity.name` as the specifier. Have you had any trouble installing this file field? Or maybe changed it after installation? Changed the cardinality, that sort of thing?
ai flag
@Clive - No problems installing the field, and no modification have been made to its configuration. I can populate the field perfectly well when creating or updating the entities, and can get to any of the field's properties using the standard entity field access syntax. Should I file a bug report (with a repro case), do you think?
cn flag
I'm not sure if it's a bug or just something undocumented, there is a syntax which works for base file fields. I put an answer in but I'll hopefully update it when I have time to find out more
ai flag
@Clive - First of all, I'm _very_ impressed that you figured this out. I have updated my [bug report](https://www.drupal.org/project/drupal/issues/3278083) to reflect this new information. Short version: it's a bug in the documentation or the software, and I believe the stronger case can be made for making the software conform to the syntax currently documented for _any_ core reference field type. I'm accepting your brilliant answer, even before you've had a chance to update it.
Score:2
cn flag

There's a bug in core, and for the time being a workaround is:

'specifier' => 'file.target_id.entity.filename',

The reason for this is the entity query relationship syntax started out as join support, it predates the entity reference support in core and as such while $entity->file->target_id->entity is not supported by core, file.target_id.entity is supported by entity query to indicate it should join the entity specified by the file field and the entity id should be what is specified by target_id. It's a bit clunky but at the time, it was what worked.

It should be able to figure out file.entity.filename as well because the main property name for the file field happens to be target_id and so it is able to expand it for you into file.target_id.entity.filename. However, the class in case Drupal\Core\Entity\Query\Sql\Tables is extremely convoluted and this expansion has a bug for the shared table case which will be fixed in https://www.drupal.org/project/drupal/issues/3278083

ai flag
So you're suggesting it's the documentation which needs to be fixed, not the code? Wouldn't be the first time.
ai flag
I'm accepting your excellent answer, though I'm not sure I understand what you mean in your explanation. I have countless examples of entity reference conditions in entity queries which rely on a reference field stored in the base table, and they work just fine with the documented syntax.
cn flag
Yeah vanilla entity reference as a base field seems to work as you'd expect. If I was a betting man, without looking properly I'd say it's probably because the base entity reference field only has a single DB column (`target_id`), so there would be no need for the syntax to support the "sub-fields"; the `fieldname__target_id` column can be inferred (sub-fields is likely NOT an official term by the way :) ). File fields have 3 DB columns, so queries need to be told explicitly which `file__*` column in the base table needs to be used, hence the other syntax. Could still be something else though
cn flag
Found it - `DefaultTableMapping::getFieldColumnName()` is the smoking gun. If a field has only one column, its DB column name is the name of the field. So an entity reference field with an ID of `uid`, has a DB column named `uid`. When you query for `uid.entity.*`, there's a straight match on the DB column name and everything's fine. When a field has more than one column, the DB columns are named `fieldname__property_name`, so when you query for `fieldname.entity`, there's no matching `fieldname` DB column. The extended syntax is there to bridge that gap and let you select the right column
ai flag
Thanks for this excellent spelunking. I'll include a link to this page from the bug report.
Smartsheet eng avatar
um flag
This is a workaround which works but it is not the cause for `fieldname.entity.propertyname` not to work. The cause is that if 1) a field with multiple columns 2) is stored in the shared table 3) and is used in a query where specifiers follow the field name -- then the table mapping code is not called at all. There's a bug in Tables. I left an answer and filed a patch. Also, "the system simply doesn't know what file is." it certainly knows, it's a field. The code just doesn't call `$sql_column = $table_mapping->getFieldColumnName($field_storage, $column);` this is very rare edge case.
cn flag
@Smartsheeteng Indeed, a bug could easily be the reason that _the system simply doesn't know what file is_ at that point (which it doesn't, the table mappings don't contain `file`, they contain `file__*`)
Smartsheet eng avatar
um flag
@Clive the system does know what file is, it's a field. You are looking at the wrong class, the bug is in Tables not the mapping class. The very bug is that class is not called in the first place for this specifier. This answer is fully wrong, I will edit it and delete mine because it's superflous.
cn flag
@Smartsheeteng I didn't realise you wrote the original code, so fair enough, answer edited
Score:1
ai flag

The failure described above appears to be the result of a bug in the FileItem plugin. Even though the field type plugin claims it is an entity reference (the @FieldType annotation says category = @Translation("Reference")), and in all other respects that is true, the support for the documented entity query syntax is broken.

Until the bug is fixed, the workaround would be to give up the specialization implemented by the FileItem field type and use a plain entity_reference field instead. With that change the TableSort code above works correctly.

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.