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

Reply via email to