[Concept] Extension methods
| From: | Alex Wells | Date: | Wed, 10 Aug 2022 14:17:02 +0000 |
| Subject: | [Concept] Extension methods | ||
| Groups: | php.internals | ||
| Request: | Send a blank email to internals+get-118395@lists.php.net to get a copy of this message | ||
Hey internals.
The idea is to introduce extension methods, similar to those in Kotlin, C#,
Dart. For those unfamiliar, those are just regular functions with fancy
syntax. However, I think having those will not only improve readability,
but also cover some of the previously requested features.
Say you have a
class Collection and you want to add a new method
map(callable $callable): Collection. The first instinct is to go into
that file and add the method, this will work. But what if `class
Collection` is defined in a vendor package? You could define a regular
function like this: `map(Collection $collection, callable $callable):
Collection`, then import it whenever you need to use that.
This solution works, but in practice is rarely used. The reasons are:
- there's no IDE completion: $collection-> <- here I want IDE to
auto-complete the map method somehow, but since it's a function this is
impossible
- it's ugly looking and hard to read:
`getPromotions(mostExpensiveItem(getShoppingList(getCurrentUser(),
'wishlist'), ['exclude' => 'onSale']), $holiday);`
- it's easy to mess up the order of arguments
The pipe operator RFC [1] was trying to solve that but got rejected.
Major libraries/frameworks like Laravel [2] and Carbon [3] use a solution
with __call that allows users to define custom methods on their classes:
Carbon::mixin('toMyTimeFormat', fn () => $this->format('MM YYYY
DD'));
Again, this solution:
- doesn't offer IDE completion and doesn't offer IDE navigation
- harder to understand
- doesn't work with interfaces, traits, enums or primitives
Other languages (Kotlin [4], C# [5], Dart [6]) I'm aware of solve this
problem with extension methods. All of them use slightly different syntax,
but the main idea is:
- you define an extension method the same way you define a function, except
you specify which type you're extending
- you can use any type that a function can accept. This includes
primitives, classes, interfaces, traits and enums
- the type you're extending is implicitly bound to $this
- you only have access to the public scope - you can't access
private/protected members
- you have to import those the same way you import functions. You can't
define extensions globally
In PHP this could look like this:
```php
// Illuminate/Collection.php
namespace Illuminate;
class Collection {}
// App/CollectionExtension.php
namespace App;
use Illuminate\Collection;
extension CollectionExtension on Collection {
function map(callable $callable): Collection {
return new Collection(array_map($callable, $this->items));
}
}
// App/Business/Logic.php
namespace App\Business;
use extension App\CollectionExtension::map;
(new Collection(1, 2, 3))
->map(fn ($value) => $value + 1)
->map(fn ($value) => $value * 2);
```
The way this should work is PHP first checks whether Collection has map
method. If one's missing, it gets all used extensions that match that
method name, autoloads them (the same way other class-likes are loaded) and
checks whether current type Collection matches the type specified in the
extension. If so, calls the method, otherwise attempts to call __call.
This same concept eliminates the need for scalar objects or scalar
extension methods [7].
I'm guessing there will be problems with optimization and OPCache.
What are your thoughts?
References:
- [1] - https://wiki.php.net/rfc/pipe-operator-v2
- [2] -
https://github.com/laravel/framework/blob/9.x/src/Illuminate/Macroable/Traits/Macroable.php
- [3] -
https://github.com/briannesbitt/Carbon/blob/master/src/Carbon/Traits/Macro.php
- [4] - https://kotlinlang.org/docs/extensions.html
- [5] -
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods
- [6] - https://dart.dev/guides/language/extension-methods
- [7] - https://github.com/nikic/scalar_objects