Should be trivial to work with those given that we already have sorting and ranges.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

chx’s picture

Title: Add page and tablesort to EntityFieldQuery » Add pager and tablesort to EntityFieldQuery
Priority: Normal » Major
bojanz’s picture

Assigned: chx » bojanz

Talked this over with chx. He's posting an explanation, and I'm assigning this to myself

chx’s picture

Assigned: bojanz » chx
  1. pager just needs the $element and absolutely nothing else. Just called ->extend('PagerQuery') from PropertyQuery and field_sql_storage_field_storage_query where db_select is fired.
  2. For TableSort, pass in a header array which has entity, property, field keys. the TableSort method needs to iterate for fields and fill out $this->fields. Then you can strike at the same place as PagerQuery convert the efq_header to an SQL header for orderByHeader.
Anonymous’s picture

+

bojanz’s picture

Assigned: chx » bojanz

Patch coming in 10, 9, 8, 7...

bojanz’s picture

The pager stuff is trivial and done.

However, I'm having trouble with tablesort.
It works okay in PagerQuery, but in field_sql_storage I am having problems (because of the conversion of the efg_header array to a orderByHeader array, and guessing the field names, table prefixes....)
Would have been much easier to just do our own ordering and skip tablesort.inc completely.

Will return to it tomorrow to try and sort out the uglyness.

Edit: Works, now it's only ugly. Should have something prettier for tomorrow.

bojanz’s picture

Status: Active » Needs review
FileSize
6.18 KB

Here's a work in progress patch.
It's not big, the problems I thought existed (with the joins) actually disappeared once I looked at the code rested & clear of mind.

As you can see, it still has some ugly moments.
Let me know your thoughts, and how it can be prettified.

My usage example looks like this:

  $header = array(
    'title' => array('data' => t('Title'), 'property' => 'title'),
    'type' => array('data' => t('Type'), 'entity' => 'bundle'),
    'author' => t('Author'),
    'secret' => array('data' => t('Secret'), 'field' => array('field_name' => 'field_secret', 'column' => 'value')),
    'status' => array('data' => t('Status'), 'property' => 'status'),
    'changed' => array('data' => t('Updated'), 'property' => 'changed', 'sort' => 'desc')
  );
  
  $query = new EntityFieldQuery;
  $query->entityCondition('entity_type', 'node');
  $query->pager(20);
  $query->tablesort($header);
  $result = $query->execute();
  
  $nodes = node_load_multiple(array_keys($result['node']));

  foreach ($nodes as $node) {
    $options[$node->nid] = array(
      // load the data here...
    );
  }

  $form['nodes'] = array(
    '#type' => 'tableselect',
    '#header' => $query->tablesort,
    '#options' => $options,
    '#empty' => t('No content available.'),
  );
  $form['pager'] = array('#markup' => theme('pager', array('tags' => NULL)));

Don't have a good feeling about using $query->tablesort directly at the end of the example, but since the array is transformed at query time it's either that or a getter function...

I can post the example module with the next iteration of the patch.

chx’s picture

Status: Needs review » Needs work

This is great... but you are violating coding standards, else if is on a separate line (run your code through coder).

Also, there is no need to transform the EFQ->tablesort in place Iwith unset) just compile it an another variable.

And, needs test.

bojanz’s picture

Status: Needs work » Needs review
FileSize
6.45 KB

Here's a new patch.
Still no tests, but everything else should be much better and more polished.
Tried to better replicate the cramped core coding style (I usually put a blank line after the else, foreach closing braces for example, but the existing code doesn't do that...)

bojanz’s picture

FileSize
6.55 KB

Updated to remove the ternary operator discussed on IRC.

bojanz’s picture

bojanz’s picture

Status: Needs review » Needs work

Changing status so that the bot doesn't choke.

chx’s picture

Well, this is not really what I had in mind. I was thinking that we emulate TableSort on a query level instead of transforming into it. Think of it -- TableSort is nothing more than taking an order from the URL and ordering based on that. As we get the header we can pick the one that's being used, put that into $this->order and the existing code will use it. The enormous advantage of this over reusing the TableSort extender is that it's not SQL dependent!

Edit: and let's think whether we can do it for pager too. We have pager_find_page, pager_default_initialize and pager_get_query_parameters. We already have range functionality. This should be doable.

Edit2: I know #3 said otherwise but that was before you became awesome and cleaned up ordering :)

bojanz’s picture

Oh, didn't realize you wanted to go all the way, and emulate it.
Totally overlooked the benefit (having it implemented automatically for every storage driver), that sounds awesome.

Will cook something up in the next few days.

bojanz’s picture

FileSize
14.17 KB

Here's a patch to look at.

Tablesort is now incredibly simple. I am berating myself about not thinking of it myself, instead of coming up with the #11 complex monstrosity.

Question: We here also have the if/elseif/elseif combination that checks for the order types (entity, property, field). Would such a use case be served better with a switch instead? We use if/elseif/elseif in #908798: EntityFieldQuery does not handle ordering correctly too, so I stayed with it for consistency.

However, I really don't like the new pager implementation. I hate it, actually.
The code doesn't even work (the clone fails), it's just there for reference (I don't care at the moment since I'm arguing against the approach)

1) We need to duplicate the $element setting logic that we fixed in #903110: Multiple pager support is partially broken.. And it's not covered by tests like in PagerDefault. Right now PagerDefault::$maxElement doesn't work since $maxElement is a protected property. Unless that is changed, EntityFieldQuery paging can't coexist with another ordinary pager on the same page (because of element overlapping).
2) The pager code still needs to reside in finishQuery because of the need to get the total number of results, and we can only do that after the whole query is assembled. And a field storage engine (like mongodb or cassandra) might not even call finishQuery because it does ranges or something else differently. That means we gain nothing by going through all the trouble of duplicating code (and objects...)

So, I'm suggestion a mixed approach for the final patch: the new tablesort + the old pager code.

bojanz’s picture

FileSize
14.17 KB

That was the wrong patch, see this one instead.

Also, what I noticed when testing with a small module (and not just with simpletests), in order for the tablesort to work, the 'field' needs to be initialized, for example:

  $header = array(
    'title' => t('Title'),
    'type' => array('data' => t('Type'), 'type' => 'entity', 'specifier' => 'bundle', 'field' => ''),
  );

If 'field' is not present, the tablesort header cell is not a link (there's an isset() check in tablesort_header())

chx’s picture

    foreach ($headers as $key => $header) {
      if (!is_array($header) || ($header['data'] != $order['name'])) {
        continue;
      }
      $this->order[] = $header;
    }

hrm?

chx’s picture

Of course,

    foreach ($headers as $key => $header) {
      if (is_array($header) && ($header['data'] == $order['name'])) {
        $this->order[] = $header;
      }
    }

is even simpler. Yes, order will contain a number of unnecessary keys, but who care? It has a type, a specifier, and a direction.

chx’s picture

Also, do this loop before the tablesort calls and make sure to += field '' if there is ordering info in there so that tablesort is happy.

chx’s picture

FileSize
2.07 KB
13.88 KB

Trying to run tests w this patch is superb weird but there are some fixes I wanted to get in.h

bojanz’s picture

Also, do this loop before the tablesort calls and make sure to += field '' if there is ordering info in there so that tablesort is happy.

Something like that. We need to pass $headers by reference and then change it, since tablesort_header is called outside of EFQ.

However, my main points against the current pager still stand. As it is, it can't work. Currently it throws a fatal error with the example module, and the tests crash, so that's a good hint it sucks :)

Let's discuss it on IRC.

bojanz’s picture

FileSize
15.78 KB

Discussed, edited, reviewed on IRC.

Nice new patch.
Passes all tests on my local install.

bojanz’s picture

FileSize
17.81 KB

This one should have no leading whitespace.
It also fixes the leading whitespace problems that I snuck in #908798: EntityFieldQuery does not handle ordering correctly.
Since these two patches should go in together, I won't reroll the old one unless requested.

chx’s picture

Please move the range addition into initializePager.

Edit: also please remove that continue, invert the condiiton and move things inside that if. A bit weird, this way.

bojanz’s picture

FileSize
17.29 KB

Good catch, no idea why I didn't move it when I made initializePager.

New patch, addressing the concerns above, and a comment or two more.
Too bad we can't call initializePager() in execute, that would remove the need to call it in every query callback... But still, adding one line to each field storage driver is not bad.

bojanz’s picture

FileSize
17.23 KB

After consultation with chx moved initializePager to execute(), allowing pager support for all drivers with no code changes (plus a way for it to be disabled).

Edit: the current initializePager() call placement leads to possible issues with query altering. That needs to be taken care of (moving the init call to the top of the method or adding a flag showing that the query has already been altered).

chx’s picture

This is awesome progress. The problem is with the placement of the clone vs alter. If you clone before the alter then the query will be altered twice. Might be costly. If you put it after as it is now then it has a chance to be altered only once but then the alters need a way to detect they already ran. Let's add an altered property to the query object and add it to the alter docs. This will also allow the storage engines to fire a clone of the query if necessary for whatever reason.

bojanz’s picture

/**
   * TRUE if the query has already been altered, FALSE if it hasn't.
   *
   * Used in alter hooks to check for cloned queries that have already been
   * altered prior to the clone.
   *
   * @var boolean
   */
  public $altered = FALSE;

Not sure if pager should be mentioned, or if it's enough.
Didn't see any EFQ alter docs, should I add a line to the execute() comments?

chx’s picture

modules/system/system.api.php:315:function hook_entity_query_alter($query) {

and yes please mention that pager fires such a query and what the heck, let's movecopy the comment about detecting the pager queries in here.

bojanz’s picture

FileSize
18.59 KB

Let's try this one.

chx’s picture

Note the $query->altered attribute which is TRUE in case the query has already been altered once. This happens with cloned queries. If there is a pager, then such a cloned query will be executed to count all elements. This query can be detected by checking for ($query->pager && $query->count), allowing the driver to return 0 from the count query and disable the pager.

also, + * which allows a driver to return 0 from the count query and disable the pager. -- is over 80 chars.

bojanz’s picture

FileSize
18.66 KB

Updated.

bojanz’s picture

Status: Needs work » Needs review
FileSize
15.12 KB

#908798: EntityFieldQuery does not handle ordering correctly landed. The whitespace fixes got in there after all, so I'm rerolling this one, and letting the bot test it.

bojanz’s picture

FileSize
15.12 KB

else if => elseif.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Code looks fine and testing of efq_views worked fine.

webchick’s picture

Status: Reviewed & tested by the community » Needs review

This is a pretty huge patch for this stage in the release cycle.

Can you explain why this is important and needs to be committed for D7? Then feel free to bump back to RTBC.

bojanz’s picture

Status: Needs review » Reviewed & tested by the community

@webchick
You're fast!

This patch took a long time to mature because of other discovered issues that blocked it (multiple pagers, broken ordering, etc, etc).
So instead of August 31st we are having this conversation on October 9, which means I won't get upset if this gets postponed to Drupal 8.
It's in the "nice to have" category.

Points for considering it:
1) It breaks no APIs, changes no schemas, doesn't create no extra work for anyone using EntityFieldQuery.
2) Most of the patch size is due to tests (10 for tablesort, 4 for pagers) and docs. The added functionality is simple.
3) By giving a developer an easy way to use pagers and tablesorts with EntityFieldQuery, he can create full admin screens using EntityFieldQuery.
That was the only missing piece. With this, the D7 admin screens could use EFQ, that's how complete it is.
Since pagers and tablesorts are pretty essential for many use cases, it would be a shame for contrib authors to have to reinvent it in every module separately.
So in short: this completes the puzzle, and makes EntityFieldQuery 100% awesome.

Note that this patch adds a way for to check if a query has been altered, which is definitely needed for D7, even if the pager is implemented from contrib, so if this gets postponed, that should go in as a separate patch.

I'll be on IRC if you want to discuss this. Thanks!

webchick’s picture

#34: 885014.patch queued for re-testing.

bojanz’s picture

#33: 885014.patch queued for re-testing.

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Ok, I talked to chx about this one last time, and he pointed out that:

a) This is the last patch before EFQViews is ready
b) Even though it's late for these kinds of changes, EFQ itself wasn't added until June or something.
c) Despite its size, there is no possible way for it to break anything, because the only thing currently using it is EFQViews
d) This patch has been running live on Examiner.com severing a quinti-billion page views per second for several months and haven't broken anything.

Also, it's like bojanz's first patch, and he is awesome. ;)

So, given all that...

Committed #34 to HEAD. Great job!

Status: Fixed » Closed (fixed)

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