If I kill a specific form alter and the generic form_alter is in the module then drupal_alter will fire the specific one too. #765860: drupal_alter() fails to order modules correctly in some cases is a better solution but it changes an API so it's 8.x with a debate whether backportable while this is the absolute minimal possible solution.

Comments

catch’s picture

subscribing.

Status: Needs review » Needs work

The last submitted patch, drupal_alter_obey_mi.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
StatusFileSize
new1.89 KB

*cough* that was superb dumb, wasnt it?

catch’s picture

chx’s picture

Version: 8.x-dev » 7.x-dev
Issue tags: -Needs backport to D7

Here's an example of a complicated case that even my patch above does not cover (but I have code upcoming that will cover it just need to translate my skeleton tests into proper simpletests): let's say you call drupal_alter(array('foo', 'bar'));. You have three modules, m1, m2, m3 and module_implements('foo_alter') returns array('m1', 'm2');. module_implements('bar_alter') returns array('m1', 'm3', 'm2');. Now the following two ordering must apply:

  1. Current HEAD always fires the functions in the order passed to drupal_alter That can not be changed in 7.x. So in this case, m1_foo_alter must fire before m1_bar_alter. We can review this in D8 but not in D7. There's a separate issue to revisit this ordering.
  2. The secondary ordering which is broken currently should be such that the m3_bar_alter fires some time between m1_bar_alter and m2_bar_alter. That's what module_implements told us. If module_implements would tell us to order in such a way that contradicts 1. the behavior is simply undefined.

The only order that achieves both goals is m1_foo_alter, m1_bar_alter, m3_bar_alter, m2_foo_alter, m2_bar_alter. What the upcoming patch is this:

  1. Keep the first ordering rule, so create array('m1' => array('m1_foo_alter'), 'm2' => array('m2_foo_alter')). We will never change the ordering of these keys. We might want to insert something betweeen them, before them or after them but never change the order.
  2. Second, we crawl the m1, m3, m2 list (given by module_implements('bar_alter')):
    1. First up is m1. Great, we have m1 in the main array so it becomes array('m1' => array('m1_foo_alter', 'm1_bar_alter'), 'm2' => array('m2_foo_alter')).
    2. Second up is m3. We do not have m3 in the main array. Sad panda. Let's set it aside in $todo = array('m3').
    3. Now comes m2. This is set. We want to insert $todo before m2 but to do that, we would need to insert before an array key and there is no PHP function for that (array_splice does not really work for associated arrays -- it destroys the replacement keys). So instead we copy the main array one-by-one and when m2 comes, before writing it in the new main array, we iterate $todo and write those implementations down:array('m1' => array('m1_foo_alter', 'm1_bar_alter'), 'm3' => array('m3_bar_alter')) and then copy m2 over: array('m1' => array('m1_foo_alter', 'm1_bar_alter'), 'm3' => array('m3_bar_alter'), 'm2' => array('m2_foo_alter', 'm2_bar_alter'))

Horribly ugly, complicated but can't find a better way.

chx’s picture

#765860: drupal_alter() fails to order modules correctly in some cases is the 8.x issue. That can talk about changing the order such that all foo fires first and then all bar fires second. That is not acceptable in D7.

fabianx’s picture

Here is an attempt to solve this via a weighted sort function (not a patch, but a test case):

http://paste.pocoo.org/show/401484/

The basic idea is to cascade the module_implements by adding it to a map like:

Array
(
    [0] => Array
        (
            [m1] => 1
            [m2] => 2
        )

    [1] => Array
        (
            [m1] => 1
            [m3] => 2
            [m2] => 3
            [m4] => 4
        )

    [2] => Array
        (
            [m1] => 1
            [m4] => 2
            [m2] => 3
        )
)

As you can see at index 0, comparing * with m3 or m4 results in NaN (as one of them is undefined), such it goes to the next array to compare the modules.

As you can further see the sort order is conflicting, but the first order is used.

It may be possible to still combine a test case so that the sort function ends up trying to compare against NaN, but I've not yet gotten it so.

Edit: Okay, I found a test case now, which is a little problematic:

My code gives an interesting result with:

module_implements('foo_bar_alter', array('m1', 'm3', 'm2'));
module_implements('foo_xyz_alter', array('m1', 'm2', 'm4'));
test();

This gives the map of:

Array
(
    [0] => Array
        (
            [m1] => 1
            [m2] => 2
        )

    [1] => Array
        (
            [m1] => 1
            [m3] => 2
            [m2] => 3
        )

    [2] => Array
        (
            [m1] => 1
            [m2] => 2
            [m4] => 3
            [m3] => 4
        )
)

The resulting order is: m1,m3,m2,m4

As there is a conflict the absolute order is not well defined:

But on second look the partial sort order m1,m3,m2,m4 is correct, because

m1 < m2 -- OK
m1 < m3 -- OK
m3 < m2 -- OK
m1 < m4 -- OK
m2 < m4 -- OK
m3 < m4 -- FAILED, but this is due to the conflict of m3 < m2 and m3 > m2.

So the problem can be really reduced to:

Combine the above mappings into one global sorting order and use that as $module_weight keys.

I think however that the approach of cascaded search should preserve the search order even in case of conflicts.

Best Wishes,

Fabian

chx’s picture

chx’s picture

StatusFileSize
new3.94 KB

This implementation hit me while trying to fall back asleep :) It's way, way simpler than anything we came up with previously. Additionally there is one more ordering it keeps: module_list(). We originally set up a list of module weights in the order of module_list. Then, as module_implements results comes in -- this is the big trick -- we take the weights of the modules returned by m_i and swap them around so that now the weights reflect the new order. We never assign new weights, we just swap them around.

Example: originally you have modules A, B, C, D, E, F weights 0,1,2,3,4,5. m_i returns E, D, B. The potential weights for these three are now 4, 3, 1. Sort the weights: 1, 3, 4. These are the new weights then: E is 1, D is 3 and B is 4.

chx’s picture

StatusFileSize
new4.11 KB

Still needs tests but the above forgot to flatten out $implementations into $functions.

Status: Needs review » Needs work

The last submitted patch, drupal_alter_1123140_10.patch, failed testing.

fabianx’s picture

Nice work!

Why does the Simple Test fail?

Best Wishes,

Fabian

chx’s picture

Status: Needs work » Needs review

Because i used $data accidentally there, still working on the patch and tests.

chx’s picture

StatusFileSize
new6.44 KB

Here's current version.

Status: Needs review » Needs work

The last submitted patch, m_i_1123140.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review

The approach in #14 is faulty, because it is assuming the weights are fixed.

Example:

Sorts: 4,3,1,5 and 4,2,0

Sort Order: 0,4,2,3,1,5

With 2 and 0 that need to be changed either the result is 0,4,2,3,1,5 or 2,4,0,3,1,5.

which is obsivously wrong.

New approach (in pseudo code):

$modules_map = array(
  array(4 => 0,3 => 1,6 => 2,1 => 3,5 => 4),
  array(4 => 0,2 => 1,6 => 2, 0 => 3)
);

$all_modules = array( 0, 1, 2, 3, 4, 5, 6);

This is our setup as sort orders. (the current code can get those)

Modules should of course be named and not numeric, so 0,1,2,3,4,5,6 are module names here, so there is that akey hack below ...

Now we sort it:

$ma = array();

foreach ($all_modules as $akey => $val) {
  $key = 'a' . $akey;

  for ($i = 0; $i < count($modules_map); $i++) {
    $ma[$key][$i] = $modules_map[$i][$akey];
  }
}

$last = 0;

foreach ($modules_map[1] as $akey => $val) {
  $key = 'a' . $akey;
  if (!empty($ma[$key][0])) {
    $last = $ma[$key][0];
  }
  else {
    $ma[$key][0] = $last + 1;
  }
}

array_multisort($ma);

print_r($ma);
exit(1);

And thats it. Works of course only for one alter hook at a time, but the previous patch also had that limitation.

Best Wishes,

Fabian

fabianx’s picture


<?php

/*      $modules_map = array(array_flip($modules));

      foreach ($extra_types as $extra_type) {
        $extra_hook = $extra_type . '_alter';
        $extra_modules = module_implements($extra_hook);
        if ($extra_modules) {
          $modules_map[] = array_flip($extra_modules);
        }
      }*/
  
      $modules_map = array(array_flip(array('a4', 'a3', 'a6', 'a1', 'a5')));
      $modules_map[] = array_flip(array('a4', 'a2', 'a6', 'a0'));
      $modules_map[] = array_flip(array('a4', 'a8'));
      $modules_map[] = array_flip(array('a6', 'a7'));

      $all_modules = array( 'a-1', 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8');

      $n = count($modules_map);

// The idea is setup a sorted table like so:
//
// name | module_list_weight | module_implement_weight_1 | module_implement_weight_2 | etc.
// a4   | 1                  | 1                         | 1
// a2   | NULL               | 2                         | NULL
// a8   | NULL               | NULL                      | 2
// 

      foreach ($all_modules as $module) {
        $count = 0;
        for ($i = 0; $i < $n; $i++) {
          if (isset($modules_map[$i][$module])) {
            $ma[$module][$i] = $modules_map[$i][$module] + 1;
          }
          else {
            $ma[$module][$i] = '';
            $count++;
          }
        }
        // No hook is mplemented by this module
        if ($count == $n) {
          unset($ma[$module]);
        }
      }

// And now we have the problem that a2 would be sorted first (as NULL is < 1 ), but it should be after a4
// because of the second column. And such the column is traversed and the weight is set to the the last value
// found + 1 (because there is a strict order).
//
// In this case this would be:
//
// Before a2 comes a4 and the weight is in the first column 1, so set it to 2.
//
// name | module_list_weight | module_implement_weight_1 | module_implement_weight_2 | etc.
// a4   | 1                  | 1                         | NULL
// a2   | 2*                 | 2                         | NULL
//
// And the same is true for multi-column a8 in third dimension of module_map:
//
// From the third column the last known is a4 again, so the weight of second column is 2.
// And then again, the last known column is a4 again in first column, so it is also 2.
//
// That way all sorting rules are satisfied.
//
// name | module_list_weight | module_implement_weight_1 | module_implement_weight_2 | etc.
// a4   | 1                  | 1                         | 1
// a2   | 2*                 | 2                         | NULL
// a8   | 2*                 | 2*                        | 2
//  

      for ($i = 1; $i < $n; $i++) {
        $last[$i] = 0;
        foreach ($modules_map[$i] as $module => $val) {
          $j = $i;
          for ($j = $i; $j > 0; $j--) {
            if (empty($ma[$module][$j - 1])) {
              $ma[$module][$j - 1] = $last[$j] + 1;
            }
            else {
              $last[$j] = $ma[$module][$j - 1];
            }
          }
        }
      }

array_multisort($ma);

print_r($ma);
exit(1);

Here is the updated code for n dimensions together with an explanation.

Best Wishes,

Fabian

fabianx’s picture

Assigned: Unassigned » fabianx

Assigning to me, so I finally get back to work on this.

catch’s picture

catch’s picture

Priority: Normal » Major

Bumping this to major, it is causing a lot of duplicates apparently, here's another one #1224014: form_FORM_ID_alter problem with hook_module_implements_alter.

xjm’s picture

Tagging issues not yet using summary template.

wzoom’s picture

Hello Franz, is there any progress with this issue? Also for 7.x ?

xjm’s picture

Status: Needs review » Needs work
fabianx’s picture

Putting on my queue for this week.

David_Rothstein’s picture

Just pointing out that since #765860: drupal_alter() fails to order modules correctly in some cases was committed to both D7 and D8, this issue may wind up being unnecessary. (But I'm not familiar enough with the code in this issue to know if that's actually the case, plus the other issue is still being heavily discussed and potentially rolled back.)

catch’s picture

Status: Needs work » Closed (duplicate)

That patch has been in for a while, and I'm pretty sure it fixes the issue here. Closing as duplicate.