framework: Collection methods don't work with built-in PHP functions
- Laravel Version: #.#.#
- PHP Version: #.#.#
- Database Driver & Version:
Description:
Built-in PHP functions can’t be used with Collection
methods.
Steps To Reproduce:
In PHP there are a lot of types of callable
, for example Closure
is callable, anonymous function, also [$this, 'method']
and "strtoupper"
for example.
This kind of code work fine:
$collection = collect(['one']);
$collection->map(function ($value) { return strtoupper($value); });
but this one doesn’t
$collection = collect(['one']);
$collection->map('strtoupper');
it throws
ArgumentCountError : strtoupper() expects exactly 1 argument, 2 given
About PHP functions
"strtoupper"
is a valid callable in PHP, for example
is_callable("strtoupper"); // true
and also
$function = "strtoupper";
$function("one"); // "ONE"
It also works fine with array_map()
:
array_map(['one', 'two'], 'strtoupper'); // ["ONE", "TWO"]
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 22 (13 by maintainers)
I should probably ask why isn’t that a framework problem?
And why is that a framework problem?
@rodrigopedra Let’s just make sure we’re not talking passed one-another. Me voicing my opinion, you voicing yours.
Had the laravel documentation present the
Collection.map()
in the documentation like this:and described it as “function that maps a value and a key to other value”, and also probably written “There is no function to map only the value to another value”. Then you would have all the rights in the world. You would be most kind of correct, and noone could argue with you otherwise. Actually, I would agree with you more than I can say. That function should only work with
callables
that accept two arguments and should not work with"strtoupper"
. No question about it.But that’s not the case.
What Laravel documentation now says is
This is fine.
Collection.map()
“maps one value to another”. It presents themap()
function as a function that works fine with one argument.And it works… only if you pass in a function that ignores superfluous arguments, because if you pass a function that doesn’t (like
"strtoupper"
) it throws an error. Why? Because that is a lie. It’s not a function that accepts one argument. It only works, because the documentation and most of the use-cases are filled with anonymous functions which ignore the superfluous arguments. It’s actually fooling its users to believe that the function “has a variable amount of arguments”. If you passfunction ($value) {}
it accepts one, if you passfunction ($value, $key) {}
i accepts two. But it’s not a feature ofCollection.map()
, but of the subset of PHP functions - lambdas.This trick could only work in languages like PHP and maybe JS, where superfluous arguments are ignored. Other dynamic languages like Python or Ruby throw exceptions for superfluous arguments everytime.
It’s bad design.
In my opinion, the right way to handle it would be:
->map()
(and instead add function likemapEntries()
)Closure
instead ofcallable
, then you can pass all the superfluous arguments you want (because allClosure
will ignore them, unline somecallable
s)In other words:
Collection.map()
appears to acceptcallable
, but when you treat it seriously, it appears that it doesn’t accept them.That’s not accurate, either.
Very rarely does something documented as accepting a callable actually mean “anything that can be called upon in the context of the function it was provided to”. More often than not, those callables are expected to have specific parameters and returns and not matching that expectation will eventually result in something breaking. Just using the helper functions Laravel provides, I wouldn’t expect
collect()->map(encrypt(...));
to work correctly without wrapping it in a lambda if I need to change the second parameter as its second parameter expects a boolean value (which means through type coercion you’ll always get a boolean true, which by coincidence matches the default behavior). Likewise, if you wanted to do something likecollect()->map(object_get(...));
, that’s only going to work if your collection is a list of objects; and when it’s a collection of objects, a callable expecting strings would then not be usable because of type conflicts.The callback has a callable declaration in the doc blocks nowadays. That’s the best option you have for making sure what you’re passing through matches the callable the
map()
method requires. Adding runtime validation in for the callable here (or in any of the collection class methods) honestly offers little gain; if the code’s going to error out, PHP will give you something more useful than anything the framework could check.As for built-in functions erroring out when called with extra arguments compared to userland functions not, take that one up with PHP core. IMO it’s one of those inconsistent behaviors that should be changed one way or the other (probably so the C-defined functions behave the same as userland, as there’d be a lot of bad side effects if userland were changed to behave the same as core).
Well the interface of
->map()
is: “callback receive the element and its index”, as stated in Laravel documentation:https://laravel.com/docs/9.x/collections#method-map
So, by your own criteria, you are the one not adhering to the documented interface. So by your own terms, you are not accepting the issue, this is not a frameworks problem.
Of course there is:
This will generate a triangular pattern, which could be used in a CLI program to output cool effects. Due to you not finding a use case, doesn’t mean there is not an use case.
@Danon I am actually the one who thinks you are not understanding the issue.
I won’t reply anymore, I suggest you do either the following:
Collection@map
to use PHP’s reflection classes to assess its callback arity and conform to the user expected “interface”Good luck 😉
It’s frameworks choice how to do things. If it wants to pass 2 arguments - it does, don’t like it don’t use it or do a pull request and see what they think.