Skip to content

[12.x] Add append keys with arr helper #55565

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: 12.x
Choose a base branch
from

Conversation

ldommer
Copy link

@ldommer ldommer commented Apr 26, 2025

Non-breaking change that adds the missing appendKeysWith method to the Arr helper class.

This can be useful for mass-updating the keys of API response data or preparing validation rule arrays that are reused, but under different keys having suffixes.

The docs PR can be found here: laravel/docs#10352

@shaedrich
Copy link
Contributor

shaedrich commented Apr 26, 2025

Why not use Arr::mapWithKeys() directly? The next PR probably will be Arr::prependKeysWith(), then something like Arr::transformKeysToUpperCase(), etc.

@ldommer
Copy link
Author

ldommer commented Apr 26, 2025

I think Laravel is a lot about expressive code and also a bit about syntactic sugar: The prependKeysWith method is already part of the framework, so I thought it would be consistent to also have the appendKeysWith method in the Arr helper class.

@rodrigopedra
Copy link
Contributor

Maybe adding a Arr::transformKeys(callable $transformer)?

Then we can deprecate the Arr::prependKeysWith() on a new release?

@shaedrich
Copy link
Contributor

shaedrich commented Apr 26, 2025

I think Laravel is a lot about expressive code and also a bit about syntactic sugar: The prependKeysWith method is already part of the framework, so I thought it would be consistent to also have the appendKeysWith method in the Arr helper class.

@ldommer @AndrewMast Yeah, I don't feel, this was an ideal addition either. There should be something more along the lines of (pseudo code, mind you)

enum ArrayEntryTuplePart {
    case Key;
    case Value;
    case Both;
}
enum ArrayEntryTransformType {
    case Prepend;
    case Append;
    case Replace;
}

class Arr
{
    // …

    static public function map(
        array $array,
        callable $callback,
        ArrayEntryTuplePart $part = ArrayEntryTuplePart::Value,
        ArrayEntryTransformType $type = ArrayEntryTransformType::Replace
    ): array {
        $transform = fn (string|int $old, string|int $new) => match ($type) {
            ArrayEntryTransformType::Prepend => $new.$old,
            ArrayEntryTransformType::Append => $old.$new,
            ArrayEntryTransformType::Replace => $new,
        };
        $mapKey = function (array $array, string|int $key, callable $callback) {
            $result = $callback($array[$key], $key);
            $value = reset($result);
            return [ key($result), $value ];
        };
        return match ($part) {
            ArrayEntryTuplePart::Key => array_reduce(array_keys($array), fn (array $carry, $key) => [ ...$carry, $transform($key, $callback($key)) => $array[$key] ], []),
            ArrayEntryTuplePart::Value => array_map($callback, $array),
            default => array_reduce(array_keys($array), fn (array $carry, $key) => [ ...$carry, ($transform($key, ([ $newKey, $newValue ] = $mapKey($array, $key, $callback))[0])) => $newValue ], []),
        };
    }

    // …
}

This, of course, then could get wrappers that would have pre-configured values for the flags.

More examples
    static public function mapKeys(array $array, callable $callback, ArrayEntryTransformType $type = ArrayEntryTransformType::Replace)
    {
        return self::map($array, $callback, ArrayEntryTuplePart::Key, $type);
    }

    static public function prependKeysWith(array $array, string|int $prefix)
    {
        return self::map($array, fn () => $prefix, ArrayEntryTuplePart::Key, ArrayEntryTransformType::Prepend);
    }

    static public function appendKeysWith(array $array, string|int $suffix)
    {
        return self::map($array, fn () => $suffix, ArrayEntryTuplePart::Key, ArrayEntryTransformType::Append);
    }

    /**
     * @param int-mask-of<MB_CASE_*> $case
     * @see [`mb_convert_case()`](https://www.php.net/manual/en/function.mb-convert-case.php)
     */
    static public function transformKeyCase(array $array, int $case)
    {
        return self::map($array, fn ($key) => mb_convert_case($key, $case), ArrayEntryTuplePart::Key);
    }

    static public function pregMap(array $array, string|array $pattern, string|array $replacement, ArrayEntryTuplePart $part = ArrayEntryTuplePart::Value)
    {
        return self::map($array, fn ($entry) => preg_replace($pattern, $replacement, $entry), $part);
    }

Then we can deprecate the Arr::prependKeysWith() on a new release?

@rodrigopedra This, because of the aforementioned point, goes more in the direction I imagined 👍🏻

@AndrewMast
Copy link
Contributor

AndrewMast commented Apr 26, 2025

I like @rodrigopedra's idea of a transformKeys method, but I think that we should keep the existing prependKeysWith method.

/**
 * Transform the key names of an associative array.
 *
 * @template TKey
 * @template TValue
 * @template TTransformedKey of array-key
 *
 * @param  array<TKey, TValue>  $array
 * @param  callable(TKey): TTransformedKey  $callback
 * @return array<TTransformedKey, TValue>
 */
public static function transformKeys(array $array, callable $transformer)
{
    return static::mapWithKeys($array, fn ($item, $key) => [$transformer($key) => $item]);
}

Then the methods could be like this:

/**
 * Prepend the key names of an associative array.
 *
 * @param  array  $array
 * @param  string  $prependWith
 * @return array
 */
public static function prependKeysWith($array, $prependWith)
{
    return static::transformKeys($array, fn ($key) => $prependWith.$key);
}
/**
 * Append the key names of an associative array.
 *
 * @param  array  $array
 * @param  string  $appendWith
 * @return array
 */
public static function appendKeysWith($array, $appendWith)
{
    return static::transformKeys($array, fn ($key) => $key.$appendWith);
}

@shaedrich
Copy link
Contributor

shaedrich commented Apr 26, 2025

@AndrewMast I updated my reply with examples. These also include @rodrigopedra's transformKeys() method but offer a higher level of abstraction, linking it to the base map() method (a little like how we handle where() in the query builder)

@AndrewMast
Copy link
Contributor

@shaedrich I feel like your method adds a level of unneeded complexity that will be run for every map call.

This is my logic:

  1. We already have prependKeysWith. It's been in Laravel since 9.x ([9.x] Introduce Arr::prependKeysWith helper #42448). Removing it doesn't make sense, especially since Arr is a helper class meant to add tools to manipulating arrays.
  2. Making an appendKeysWith is a logical step in my opinion (I think this PR is a good idea).
  3. However, to stop the addition of even more methods (Arr::transformKeysToUpperCase() like you mentioned), I think creating a method like transformKeys like Rodrigo suggested is also a good idea. I would argue that figuring out how to make a closure/function transform the keys the way the user wants to will be easier than figuring out which of the enum types should be used in the arguments you have theoretically added to map.

@shaedrich
Copy link
Contributor

shaedrich commented Apr 26, 2025

I feel like your method adds a level of unneeded complexity that will be run for every map call.

A normal map call would

  • be called with $part defaulting to ArrayEntryTuplePart::Value and $type defaulting to ArrayEntryTransformType::Replace without any additional imperative logic
  • Jump to return match ($part) {
  • And then continue on the ArrayEntryTuplePart::Value => match arm, calling array_map($callback, $array)
  • and be done with it
  1. We already have prependKeysWith. It's been in Laravel since 9.x […] Removing it doesn't make sense […] Making an appendKeysWith is a logical step in my opinion

Sorry if I gave that impression. Removing it never has been my intention. Adding your method, at least, is a reasonable follow-up as we already have the complementary method and won't remove it, at least in the near future.

3. However, to stop the addition of even more methods (Arr::transformKeysToUpperCase() like you mentioned), I think creating a method like transformKeys like Rodrigo suggested is also a good idea.

I hope, that has the wanted effect. Keep in mind that the class is already Macroable 😉

  1. […] I would argue that figuring out how to make a closure/function transform the keys the way the user wants to will be easier than figuring out which of the enum types should be used in the arguments you have theoretically added to map.

I used default parameters to both prevent BC problems as well as allowing for users not to case about them as long as they want to just map or use a wrapper helper like shown in my additional hypothetical examples

@AndrewMast
Copy link
Contributor

AndrewMast commented Apr 26, 2025

@shaedrich Saying we shouldn't remove the prependKeysWith was more a response to Rodrigo's suggestion of deprecation. I tried to address all of the discussion with my points, but I did @ you so that is confusing. While I do like the idea of robust methods, just the fact that the utilization of the map with enums goes from one import statement (Arr) to three (two more for the enums), makes me hesitant to accept it.

@ldommer I think this PR is a pretty good idea. I suggest you add a test for the method. Take a look at the current test for prependKeysWith here. We can worry about adding transformKeys in another PR, depending on the response to this one (EDIT: also, I would suggest drafting your laravel/docs PR until this one is merged)

@ldommer
Copy link
Author

ldommer commented Apr 26, 2025

I can totally see the point of @shaedrich of not bloating the Arr helper class with endless variations of key transformation methods.

But I really like the argumentation and solution of @AndrewMast and couldn't have written it better 👍. I think it makes totally sense to have the two probably most used key transformations as expressive predefined methods and mapWithKeys for other cases. (I think Laravel does this in a lot of situations, providing a bit of syntactic sugar for the most used cases of an API, although the developer could have achieved the same result with a single line of code and using another more flexible but less expressive method of the same API.)

I also really like the idea of @rodrigopedra of having a flexible transformer for all kinds of string operations on array keys, because a) this solution could catch every still so rare edge case someone wants to execute and b) it resembles Laravels callback approach.

@AndrewMast Thank you for the hint of adding a test. I didn't find the Arr helper class tests at first glance, because the class itself lives inside the Collections directory, whereas the tests seem to be located inside the support directory 👍.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants