One of the most difficult things about manipulating Drupal forms is the process of manipulating the arrays themselves. And one of the things that I commonly want to do is "stick this stuff in after that". But I have yet to find a PHP function to manipulate arrays in this way.
So I wrote this function called array_insert(). Perhaps there's a cleaner way to handle this, but it seems like a function that should be available in Drupal core. It would certainly help with a lot of common hook_form_alter() tasks.
For instance, this function (finally) offers a way to stick something into the node-type settings page (admin/settings/content-types/[node-type]) immediately before the "submit" buttons, you'd just create an array with your form items and then call
$form = array_insert($form, 'buttons', $my_stuff, TRUE);
/**
* Inserts values from $arr2 after (or before) $key in $arr1
* if $key is not found, $arr2 is appended to $arr1 using array_merge()
*
* @param $arr1
* array to insert into
* @param $key
* key of $arr1 to insert after
* @param $arr2
* array whose values should be inserted
* @param $before
* insert before the given key. defaults to inserting after
* @return
* merged array
*/
function array_insert($arr1, $key, $arr2, $before = FALSE){
$done = FALSE;
foreach($arr1 as $arr1_key => $arr1_val){
if(!$before){
$new_array[$arr1_key] = $arr1_val;
}
if($arr1_key == $key && !$done){
foreach($arr2 as $arr2_key => $arr2_val){
$new_array[$arr2_key] = $arr2_val;
}
$done = TRUE;
}
if($before){
$new_array[$arr1_key] = $arr1_val;
}
}
if(!$done){
$new_array = array_merge($arr1, $arr2);
}
return $new_array;
}
Comments
Comment #1
m3avrck CreditAttribution: m3avrck commentedSeems like this would be useful for core. I'm no array expert but it looks good.
Comment #2
chx CreditAttribution: chx commentedThe functin may be useful.
But, I think you are better off with a combination of array_keys, array_search and array_splice. timer it out.
Comment #3
jjeff CreditAttribution: jjeff commentedPer CHX's advice, here's a new version. It turns out that array_splice() actually destroys the keys of the inserted array, so I've had to go another route.
Comment #4
chx CreditAttribution: chx commentedsorry for not reading the manual page before :( but losing numberic keys is not good :( array_slice also resets numeric keys :(
Comment #5
moshe weitzman CreditAttribution: moshe weitzman commentedi wrote a different version for my big nodeapi submit handler patch. since that patch is dormant for now, i think this function should be considered for core under this issue. my version is very small.
Comment #6
jjeff CreditAttribution: jjeff commentedMoshe's version is shorter because it doesn't do the same thing as mine.
- Mine allows you to stick array key/value pairs into an array either before or after a given key.
- Moshe's just sticks stuff in at a given numeric position.
Both have their merits, but I think mine is a bit more useful for manipulating Drupal forms.
e.g.: Stick this stuff before the 'buttons' in the form.
Comment #7
drummAren't weights supposed to let us put things where we want?
Comment #8
moshe weitzman CreditAttribution: moshe weitzman commentedthere are no weights in submit/validate handlers, for example.
Comment #9
webchickNor in form_alter for most forms. For example, I was trying tonight to alter taxonomy_image so that the form would appear in the actual taxomomy edit page rather than on a separate tab. At the time the form is built, none of the fields are assigned weights, therefore the additions always appear /below/ the Submit/Delete buttons which is less than ideal.
However, this function didn't allow me to do that either. :P
...either that or I'm just tired and being stupid, which is also very likely. ;) I just figured I'd mention it here since this function sounds like it should allow me to do that.
Comment #10
moshe weitzman CreditAttribution: moshe weitzman commented@webchick - in your example you are accepting $original_form as a reference AND returning it. not good, though i dunno if thats bad enough to explain the failure. anyone available to review this some more?
Comment #11
dmitrig01 CreditAttribution: dmitrig01 commentedthere is now a #weight element...
Comment #12
moshe weitzman CreditAttribution: moshe weitzman commentedhuh? we have had #weight since fapi was born. how does that help with the stated problem?
Comment #13
birdmanx35 CreditAttribution: birdmanx35 commentedFeature requests go to 7.x-dev.
Comment #14
cyberswat CreditAttribution: cyberswat commentedI had a need for this functionality so did some testing on the two patches ... I was able to get what I wanted by using jjeff's version in comment 3 ... the following "code" is probably lengthier than it needs to be, but it illustrates that the function performs as promised. I was not able to get this functionality from moshe's patch as they indeed have different purposes.
Just for kicks I tried to break the above tests by setting the key of the array I was passing in to the same as the key I was trying to position before and after. This worked as promised with numbers. When I tried conflicting string keys like in the following it simply returned the original value of $form
Hope that helps
Comment #15
deviantintegral CreditAttribution: deviantintegral commentedThis is great! I did a google search for this functionality and the Drupal page was the first hit :).
#3 works as expected for me, and helps in the use case where no weights are assigned to elements. I'll plan on writing a patch + test for this soon.
Comment #16
effulgentsia CreditAttribution: effulgentsia commentedD8 material. What I'd really like to see in D8 is full DOM-like management of form/render arrays, perhaps modeled after jQuery, if we're able to do so in a performant way.
Comment #17
moshe weitzman CreditAttribution: moshe weitzman commentedOne of Steven's masterpieces - http://drupal.org/project/fquery. He didn't ever maintain it so it has not seen any traction. But someone should really dust it off and promote.
Comment #18
aaron CreditAttribution: aaron commentedsubscribing from #692950: Use hook_module_implements_alter to allow modules to alter the weight of hooks in module_implements.
Comment #19
bfroehle CreditAttribution: bfroehle commentedI'd like to see moshe's patch in #5 make it in. It's short and extremely useful when altering forms. For example, to insert a new column in a tableselect or table, you need to insert a new entry into $element['#header']
Comment #20
LordQuackstar CreditAttribution: LordQuackstar commentedSubscribing...
Comment #21
bfroehle CreditAttribution: bfroehle commentedResurrecting this issue. '#weight' can solve many, but not all, reasons for this helper function. In addition to one use in batch_set() there are certainly some additional uses in contrib. For example, CAS uses this technique to add a column to a table render array. [Clearly this is something '#weight' would never be used for].
Comment #22
joachim CreditAttribution: joachim commentedIf you have to give a numeric position for the insertion, then I don't really see the point of it. You're only saving a couple of lines in cases where you know the position already.
The OP function was useful because you often *don't* know the position of the array where you want to insert something, you know it's 'after key 'foo'' or 'before key 'bar'.
Comment #23
joachim CreditAttribution: joachim commentedHere's the OP's function tweaked for current coding standards and modified to change the given array directly, which keeps it in line with array_splice().
Could do with some more comments to explain how it works, though I haven't fully worked that out yet.... it does work though :D
Comment #24
amontero#15: +1! I googled and grabbed #23. Saved me a lot of work turning dealing with steps in a D7 install profile into one liners!
If it is backport elegible (not sure) or considering backports module, wanted to add my use case. Subscribing.
Comment #25
amonteroTagging
Comment #26
mrfelton CreditAttribution: mrfelton commentedNice. Tis lets me really inject additional views fileds and filters into a specific place with a view:
Comment #27
tim.plunkettThis should be rewritten as a method like in #1705920: Convert all calls to procedural drupal_array_*() functions to Drupal\Component\Utility\NestedArray, once that's in.
Comment #28
13rac1 CreditAttribution: 13rac1 commentedAh ha! Setting my issue/patch as duplicate of this: #1343024: Add drupal_array_insert_before() & drupal_array_insert_after()
Comment #43
DuaelFrFor those who need it today, here is a patch for 7.15 based on #23 (Thanks a lot !)
For consistency reasons, I changed function signature to
Comment #44
jcisio CreditAttribution: jcisio commentedIs core going to use that, or is it just a utility function to help contrib module for _alter()?
Comment #45
bfroehle CreditAttribution: bfroehle commentedA long time ago in #21 there were some proposed uses in core. Not sure if those are still relevant.
Comment #46
amonteroTagging
Comment #47
amonteroAs per #27, rerolling joachim's code (#21) and moving from postponed to active again.
Comment #48
amontero#47: drupal--66183--array_insert-47.patch queued for re-testing.
Comment #49
RobLoachIs there any place where we can take advantage of this? With the introduction of form controllers, I see the need for this slowly fade.
Comment #50
amonteroNot only forms were intended to benefit from this. IMO, it's a useful function to have for dealing with arrays.
My use case was inserting a step in an install profile. I'm not fully aware of how much D8 has evolved code-wise, but would take me by surprise not having other parts of its API dealing with arrays in general.
Maybe forms can not take advantage of it, but I think there are other cases in that it might be helpful.
Comment #51
tstoecklerIf http://drupal.org/node/1876718 does not make it, a lot of contribs will hit this use-case. I would obviously prefer a cooler solution over there, but...
Comment #52
amonteroIMO, this solution is more generic so you get a function you can use everywhere, not only in the linked issue. More like a toolbox function. And use this in #1876718: Allow modules to inject columns into tables more easily, of course.
This would have the nice side benefit of being safely backportable as an API addition to 7.x as drupal_array_insert() for instance ;)
Even more code could benefit.
(Tentatively changing component to better reflect this)
Comment #53
deviantintegral CreditAttribution: deviantintegral commentedWould this still be useful for fields with deltas? If not, I'd say this can be closed as a won't fix.
Comment #54
DuaelFrWhat gives strength to Drupal is its great API which allow developpers to (almost) always find the tool for their needs. Implementing this function has no cost for performances nor maintenance because its code is quite simple so I think we should just do it. We do not need endless topics about such kind of tiny problems.
The main use case in which I needed it was to inject a new filter into a view. Views' code is full of these string indexed arrays and I am sure we could find some other examples where it could help.
Maybe the good practice would be to have weight on everything but as long as we have to deal with contributed modules we cannot ensure that this will happen.
Comment #55
deviantintegral CreditAttribution: deviantintegral commentedComment #56
deviantintegral CreditAttribution: deviantintegral commentedHere's an update that adds tests for the three cases of array insertion (before, middle, end) as well as does some minor documentation style fixes.
Comment #57
tstoecklerIt seems $done is only needed in case there are 2 array keys that are identical to the specified $key. But I don't think this is possible in PHP. Can someone elaborate?
Can we get some braces around the first condition here please. That would make it less ambiguous.
Comment #58
amonteroTo me, it looks like $done helps at least speed the for loop now by short-circuitting the if as I have now swapped conditions' order. Also, it covers the case if there is no key found at all to insert after/before. However I did not made any further test.
But if there are still valid concerns, some of them could be written as tests, as well. This could make safer later speed enhancements that might arise before RTBC, too.
Reroll of deviantart's patch with braces as per #57 and swapped conditions.
So, the interdiff:
Comment #60
amontero#58: drupal--66183--array_insert--58.patch queued for re-testing.
Comment #61
amontero#58: drupal--66183--array_insert--58.patch queued for re-testing.
Comment #63
amonteroNestedArrayUnitTest.php file has been moved to NestedArrayTest.php, rebase'n'reroll of #58.
Comment #64
chx CreditAttribution: chx commentedIf we are going to add it to NestedArray shouldn't it be, you know, nested?
Comment #65
amontero63: drupal--66183--array_insert--63.patch queued for re-testing.
Comment #66
jsobiecki CreditAttribution: jsobiecki commentedPatch applies for me and seems to work. Tests are all green.
Comment #67
thedavidmeister CreditAttribution: thedavidmeister commentedPatch does not apply for me:
But after re-roll I think this is RTBC.
Comment #68
thedavidmeister CreditAttribution: thedavidmeister commentedAlso, I came up against this problem the other day just generally working with renderable arrays so +1 for a tool to make that easier.
Comment #69
Jalandhar CreditAttribution: Jalandhar commentedRerolled patch
Comment #70
thedavidmeister CreditAttribution: thedavidmeister commentedRTBC as per #67
Comment #71
tstoecklerThe "in $array" part sounds weird to me. Suggestion "Values from $insert_array are inserted into $array after $key."
Also is there any specific reason to support a non-existing $key explicitly? I can't come up with a use-case for that, to be honest.
The second sentence is not actually a full sentence. I would suggest simply "The array to insert into, passed by reference."
Missing trailing period.
I personally dislike parameters that change the behavior of a function (and quite drastically, too).
I would prefer separate functions
insertBefore()
andinsertAfter()
.I also tried to simplify the code logic using PHP's internal
array_*
functions, but the result was a little bit slower in terms of performance that's why I don't want to "officially" propose doing that, even though I find the code to much more readable. In case anyone is interested the following code achieves the same that the current does:Moving back to needs review at least to see if people agree with splitting the method into two. The other nitpicks are only minor and I wouldn't want to hold up this very useful utility any further.
I also realized that after adding these methods we shuld probably rename the
NestedArray
class to justArray
as these methods have nothing to do with nested arrays. Let's leave that for a follow-up, though.Comment #72
thedavidmeister CreditAttribution: thedavidmeister commentedRather than introduce insertBefore and insertAfter, why not add an $offset instead of a boolean. The default for $offset 0 would insert after, -1 would be before.
Also, I just had a thought, that it makes no sense to append the insert array at the end of an array if "before" was set.
How much slower is that code really? it's a little difficult when someone says "a little bit slower/faster" without posting their benchmarks/profiling or how to reproduce as the subsequent conversation often ends up taking it as a fact.
The extra legibility could easily be worth a tiny slowdown (I must admit, that foreach did my head in a bit too, and if I had to pause to think I'm sure others will too), I think it's *unlikely* (I won't say impossible :P) to see a function like this on a critical performance path in the wild.
One thing, with the array_slice() approach, the PHP docs say that keys are not preserved without TRUE passed as a fourth parameter.
Consider:
we get:
Comment #73
thedavidmeister CreditAttribution: thedavidmeister commentedworks for the above example
Comment #74
thedavidmeister CreditAttribution: thedavidmeister commentedWhat about this?
If offset is 0 then the insert array will appear immediately before $key, any numeric offset will move the insert array forwards and backwards (so setting offset = 1 has the same effect as setting $before to FALSE in #69). Setting an offset larger than the size of the array sticks the insert array at the start or end of the array as appropriate.
I used isset() as it is fast and allows us to avoid searching for things that don't exist. If the key does not exist, $insert_array will be added to the start of $array.
Comment #75
thedavidmeister CreditAttribution: thedavidmeister commentedhmm, because of use of + for array merging, we get weird behaviour with indexed arrays.
results in:
Comment #76
thedavidmeister CreditAttribution: thedavidmeister commentedThis can handle the cases in #72, #75.
https://ideone.com/j45dxV
I'd suggest that based on how easy it is to make mistakes with array functions, as demonstrated above, we should extend our test coverage to include indexed arrays and not just associative ones.
Comment #77
amonteroReroll.
Comment #79
amonteroRebase & reroll.
Comment #80
thedavidmeister CreditAttribution: thedavidmeister commentedCompletely ignoring #72 through #76?
Comment #85
joachim CreditAttribution: joachim as a volunteer commented> Rather than introduce insertBefore and insertAfter, why not add an $offset instead of a boolean. The default for $offset 0 would insert after, -1 would be before.
I think that makes it less legible, not more.
> I would prefer separate functions insertBefore() and insertAfter().
That would certainly be unambiguous. Those could both wrap around a protected method with the common code.
> I also realized that after adding these methods we shuld probably rename the NestedArray class to just Array as these methods have nothing to do with nested arrays. Let's leave that for a follow-up, though.
Can't rename it now, and NestedArray is a good name for the nester array helper methods. We could add an Array class to go alongside it though.
Comment #86
joachim CreditAttribution: joachim as a volunteer commented... though on reflection, it occurs to me that we probably can't call a class 'Array' as that's reserved in PHP.
Anyone care to bikeshed a better name?
Comment #87
tim.plunkettComment #89
joachim CreditAttribution: joachim as a volunteer commentedBrainwave -- how about we call the class AssociativeArray? Since numeric arrays won't need this as they can use array_splice().
Comment #90
joachim CreditAttribution: joachim as a volunteer commented> Also is there any specific reason to support a non-existing $key explicitly? I can't come up with a use-case for that, to be honest.
Agreed. I've removed that.
> I'd suggest that based on how easy it is to make mistakes with array functions, as demonstrated above, we should extend our test coverage to include indexed arrays and not just associative ones.
I don't think this should cater for indexed arrays. If you want to insert something into an indexed array, can't you just use array_splice()? I don't think there are cases where you'd have an indexed array and want to insert an associative key into it.
> Completely ignoring #72 through #76?
I've left off the changes suggested in those comments. Perhaps they should be added, but I don't currently feel alert enough to parse expressions like:
$array = array_slice($array, 0, $pos, TRUE) + $insert_array + array_slice($array, $pos, NULL, TRUE);
Changes from previous patches:
- code is moved to a new utility class, AssociativeArray, since NestedArray doesn't make sense for this.
- two methods rather than one, insertBefore and insertAfter. I think this makes it much clearer than having a boolean parameter.
- removed support for a non-existent $key, as per previous comments.
Comment #91
tim.plunkettYes, array_splice works fine for numerically indexed arrays. But I think it'd be overall better DX to not have to worry about numeric vs associative, and to provide an InsertArray class (similarly named to DiffArray) that supports both with one method.
Comment #92
joachim CreditAttribution: joachim as a volunteer commented> But I think it'd be overall better DX to not have to worry about numeric vs associative
Numeric values should work with the current brute force approach code -- one advantage over the use of PHP array functions.
Though
That should be a strict comparison, in case we have keys 0 and FALSE and ''.
One thing that I'm confused about:
How is this assertion better/different from assertSame()? Is it just that this code predates the use of PHPUnit in Drupal?
Comment #93
joachim CreditAttribution: joachim as a volunteer commented> How is this assertion better/different from assertSame()? Is it just that this code predates the use of PHPUnit in Drupal?
Turns out it's the same.
> Numeric values should work with the current brute force approach code -- one advantage over the use of PHP array functions.
They don't... I've switched to the approach from #76, although that too had mistakes with the $offset.
Changes since last patch:
- renamed to InsertArray
- use assertSame() instead of custom helper; be explicit about what we expect rather than faff with existing arrays -- it makes the test harder to read and debug
- added testing for numeric arrays
- throw an exception if the array doesn't contain $key
Comment #97
robpowelltested #93 in a hook_form_alter where I wanted to add a render array in the form array, works well.
Comment #98
robpowellComment #99
jcisio CreditAttribution: jcisio as a volunteer commentedRe #91, #93: it's not actually the same.
This does not return the expected result:
To fix it, we can replace the array_merge() with the array addition operator:
However there is a behavior change: when there are duplicate keys, array_merge replaces values in the first array with the later, while the operator keeps values in the first array, and it is even more complicated when merge 3 arrays.
Should we reduce the scope of this class? By saying that 1/ if we support array with numeric keys 2/ what is the expected behaviors when there are duplicated keys. How many places in core that can use of this new utility class? We can then decide on which use cases should it support and how much effort could we spend on this class.
My 2c in this 14 year old issue.
Comment #106
joachim CreditAttribution: joachim as a volunteer commentedFinally taken the time to get my head around #99!
The problem with changing to using the array union + operator instead of array_merge() is that it won't re-index numeric keys when we expect it to. So if we have:
because these arrays are numeric, we'd expect to get something with keys 0, 1, 2.
> Should we reduce the scope of this class? By saying that 1/ if we support array with numeric keys 2/ what is the expected behaviors when there are duplicated keys. How many places in core that can use of this new utility class? We can then decide on which use cases should it support and how much effort could we spend on this class.
I'm inclined to say this class should only support associative arrays, that is, any keys that happen to be numbered are treated as if they were strings, and keys will NOT be re-indexed.
> what is the expected behaviors when there are duplicated keys
We should check for that and throw an exception.