[PHP-DEV] Feature Discussion |

Hello, Internals!

I’ve implemented an alpha implementation of the Extension Functions in PHP.
Basically, it’s a syntax sugar of the imperative call of the function with the passing the object as a first argument, but anyway.

Here is how it looks like in Kotlin: https://kotlinlang.org/docs/extensions.html

In PHP it might looks like so:

function stdClass.getName() {
return $this->name;
}

$obj = new stdClass;
$obj->name = ‘Dmitrii’;
echo $obj->getName(); // prints Dmitrii

so desugared version is:

function getName(stdClass $obj) {
return $obj->name;
}

$obj = new stdClass;
$obj->name = ‘Dmitrii’;
echo getName($obj); // prints Dmitrii

Quite simple improvement, but a really convenient way to “attach” behaviour and extend vendor code to make it more readable.

Functions register as usual, using use keyword. So there are no surprises when you call a function which does not exist, only an implicit way to specify the function.

“Attaching” the function with the name of an existing member function raises an exception.
But you may juggle different functions from different namespaces:

namespace A;

use function aa; // from global namespace
OR
use function B\aa; // from B namespace
OR
use function B\aa as Baa; // from B namespace with alias

use function construction may be enlarged with the “extension” keyword:
use function getTime; // regular function
use extension function getTime; // extension function, unable to call without correct receiver

As in Kotlin, it should access only public members.

class A { private $name; }

function A.getName() {
return $this->name; // raises an exception because private and protected members aren’t available
}

Supporting types DNF is under question, but I’d leave them as future scope.

function ((A&B)|null).smth() { … }
but it could be resolved to
function smth((A&B)|null $obj) { … }

The dot as a delimiter is also under the question, here are a few options:

function stdClass::getName();
function stdClass->getName();
function stdClass.getName();
function ::stdClass getName();
function stdClass<-getName();
function getName of stdClass();

etc.

I’ve tried to implement the feature through attaching a function to the class functions scope, which is wrong and does not follow the requirements. It has memory leaks.
https://github.com/php/php-src/compare/master…xepozz:php-src:extension-functions?expand=1#diff-1dd36b02e5025ec3a5a546f8e41374ee4fc18c411bce447dd1dc2952329ccbe6R25

I also thought about adding this feature as a custom attribute behaviour, but it’s a way difficult:

#[Extension(“stdClass”)]
function hasName(): bool {
return $this->name;
}

I can try to implement the de-sugared version to make it work correctly.

···

Best regards,

Dmitrii Derepko.
@xepozz

On Mon, Jun 9, 2025, at 12:34 PM, Dmitry Derepko wrote:

Hello, Internals!

I've implemented an alpha implementation of the Extension Functions in
PHP.
Basically, it's a syntax sugar of the imperative call of the function
with the passing the object as a first argument, but anyway.

Here is how it looks like in Kotlin: Extensions | Kotlin Documentation

In PHP it might looks like so:

function stdClass.getName() {
    return $this->name;
}

$obj = new stdClass;
$obj->name = 'Dmitrii';
echo $obj->getName(); // prints Dmitrii

so desugared version is:

function getName(stdClass $obj) {
    return $obj->name;
}

$obj = new stdClass;
$obj->name = 'Dmitrii';
echo getName($obj); // prints Dmitrii

Quite simple improvement, but a really convenient way to "attach"
behaviour and extend vendor code to make it more readable.

Functions register as usual, using `use` keyword. So there are no
surprises when you call a function which does not exist, only an
implicit way to specify the function.

"Attaching" the function with the name of an existing member function
raises an exception.
But you may juggle different functions from different namespaces:

namespace A;

use function aa; // from global namespace
OR
use function B\aa; // from B namespace
OR
use function B\aa as Baa; // from B namespace with alias

`use function` construction may be enlarged with the "extension"
keyword:
use function getTime; // regular function
use extension function getTime; // extension function, unable to call
without correct receiver

As in Kotlin, it should access only public members.

class A { private $name; }

function A.getName() {
    return $this->name; // raises an exception because private and
protected members aren't available
}

Supporting types DNF is under question, but I'd leave them as future scope.

function ((A&B)|null).smth() { ... }
but it could be resolved to
function smth((A&B)|null $obj) { ... }

The dot as a delimiter is also under the question, here are a few options:

function stdClass::getName();
function stdClass->getName();
function stdClass.getName();
function ::stdClass getName();
function stdClass<-getName();
function getName of stdClass();

etc.

I've tried to implement the feature through attaching a function to the
class functions scope, which is wrong and does not follow the
requirements. It has memory leaks.
Comparing php:master...xepozz:extension-functions · php/php-src · GitHub

I also thought about adding this feature as a custom attribute
behaviour, but it's a way difficult:

#[Extension("stdClass")]
function hasName(): bool {
    return $this->name;
}

I can try to implement the de-sugared version to make it work correctly.

---

WDYT guys?

1. I love extension functions as a concept. Easily my favorite part of Kotlin, coming from PHP. So, +1 in the abstract.

2. Please link to a PR of your actual implementation. In context it looks like your branch comparison link is to the version you said didn't work, so it's not that helpful.

3. The biggest question that has come up in the past (Ilija and I have discussed it at length) is, naturally, autoloading. How if at all do you address that?

4. The other big question was determining when to match a given "method" call to an extension function, when the type of a variable is not always known at compile time. How did you address this?

--Larry Garfield

Thanks for participating, Larry.

  1. Please link to a PR of your actual implementation. In context it looks like your branch comparison link is to the version you said didn’t work, so it’s not that helpful.

Correct, I don’t have another one. This is big feature, I need a lot of time to implement it. I don’t want to waste my time if we decide that RFC won’t pass at all.

  1. The biggest question that has come up in the past (Ilija and I have discussed it at length) is, naturally, autoloading. How if at all do you address that?

In the original message I mentioned use extension construction. This should be enough for solution, isn’t it?

  1. The other big question was determining when to match a given “method” call to an extension function, when the type of a variable is not always known at compile time. How did you address this?

Cannot understand the passage, could you explain more?

The only one way to use these extension functions – import them.

namespace A {
use B\Foo;

function Foo::hello(){…}
function Bar::hello(){…} <---- Error, unknown Bar class}

namepsace B {
class Foo {}

new Foo->hello() <— Error, hello() is not a member of Foo
}

namepsace C {
use B\Foo;
use extension function A\Foo::hello;

new Foo->hello() ← Everything’s great
}

···

Best regards,

Dmitrii Derepko.
@xepozz

On Tue, Jun 10, 2025, at 2:45 PM, Dmitry Derepko wrote:

Thanks for participating, Larry.

On Mon, Jun 9, 2025 at 10:29 PM Larry Garfield <larry@garfieldtech.com> wrote:

2. Please link to a PR of your actual implementation. In context it looks like your branch comparison link is to the version you said didn't work, so it's not that helpful.

Correct, I don't have another one. This is big feature, I need a lot of
time to implement it. I don't want to waste my time if we decide that
RFC won't pass at all.

Understood.

3. The biggest question that has come up in the past (Ilija and I have discussed it at length) is, naturally, autoloading. How if at all do you address that?

In the original message I mentioned `use extension` construction. This
should be enough for solution, isn't it?

Very much not. The `use` construct has no bearing on autoloading currently. Autoloading happens only for classes and class-like things (interfaces, traits, enums). If a function is not found, PHP just crashes. Various solutions to this have been discussed over the years, none of which ever made it as far as a vote.

I toyed with the idea of having extension functions compile to a static method on a class as a way around this, but of course then you end up with a file-per-function, and the file name has to match not the function name, but whatever mangled class name gets generated. Not at all intuitive.

In fairness, I think with universal opcache, preloading, and the increasing use of persistent processes, just skipping autoloading for functions and front-loading them via composer.json's "files" block is fine for the 80% case. But it feels like I am in the minority on that position.

4. The other big question was determining when to match a given "method" call to an extension function, when the type of a variable is not always known at compile time. How did you address this?

Cannot understand the passage, could you explain more?

<?php
// index.php

function Point.area(): int {
  return $this->x & $this->y;
}

function doStuff($p) {
  // At compile time, we don't know that $p is a Point. In fact, it may not be.
  // The function will allow any value here, even a non-object, so it doesn't know
  // if this should be compiled to Point__area($p) or left as is.
  print $p->area();
}

The only way I could think of to handle that is to have a method call "trap" similar to class autoloading, that when hit checks at runtime "hey, this method didn't exist, but is there a `use`d function that would match based on the runtime type of this value?" But Ilija felt that would be prohibitively slow. It would certainly be slower than just a function/method call since the trap takes time.

And then we get into questions of inheritance, and, well, it gets even messier fast.

One possibility that we riffed on during the pipes discussion (mostly off list, I think) was using +> for some combination of extension functions and Elixir-style first-arg pipe passing, so that $p+>area() would signal to the engine (compile time or runtime) that area() wasn't a method but a function that should get $p passed to it. That would solve the "trap" problem, but still doesn't address autoloading, the lack of compile time type knowledge, or how to differentiate between Point.area(), ShapeInterface.area(), Rect.area(), etc.

--Larry Garfield

On Tue, Jun 10, 2025, at 22:09, Larry Garfield wrote:

On Tue, Jun 10, 2025, at 2:45 PM, Dmitry Derepko wrote:

Thanks for participating, Larry.

On Mon, Jun 9, 2025 at 10:29 PM Larry Garfield <larry@garfieldtech.com> wrote:

  1. Please link to a PR of your actual implementation. In context it looks like your branch comparison link is to the version you said didn’t work, so it’s not that helpful.

Correct, I don’t have another one. This is big feature, I need a lot of
time to implement it. I don’t want to waste my time if we decide that
RFC won’t pass at all.

Understood.

I’d recommend figuring it out, even if it costs a lot of time. Some of the feedback I got on nested classes – even after working on the implementation nearly every weekend for months – was that the PoC code was too hacky and didn’t work with opcache. There are hard dates for features to be merged; and if you don’t hit that date … well, fun times to be had trying to get it done by the release time, and nobody wants that crunch (and the inevitable bugs that follow). There’s enough to do just to get a release done. Having working code that is at least somewhat shippable is a big indicator that an RFC will pass.

  1. The biggest question that has come up in the past (Ilija and I have discussed it at length) is, naturally, autoloading. How if at all do you address that?

In the original message I mentioned use extension construction. This
should be enough for solution, isn’t it?

Very much not. The use construct has no bearing on autoloading currently. Autoloading happens only for classes and class-like things (interfaces, traits, enums). If a function is not found, PHP just crashes. Various solutions to this have been discussed over the years, none of which ever made it as far as a vote.

I toyed with the idea of having extension functions compile to a static method on a class as a way around this, but of course then you end up with a file-per-function, and the file name has to match not the function name, but whatever mangled class name gets generated. Not at all intuitive.

In fairness, I think with universal opcache, preloading, and the increasing use of persistent processes, just skipping autoloading for functions and front-loading them via composer.json’s “files” block is fine for the 80% case. But it feels like I am in the minority on that position.

  1. The other big question was determining when to match a given “method” call to an extension function, when the type of a variable is not always known at compile time. How did you address this?

Cannot understand the passage, could you explain more?

<?php // index.php function Point.area(): int { return $this->x & $this->y; } function doStuff($p) { // At compile time, we don't know that $p is a Point. In fact, it may not be. // The function will allow any value here, even a non-object, so it doesn't know // if this should be compiled to Point__area($p) or left as is. print $p->area(); } The only way I could think of to handle that is to have a method call "trap" similar to class autoloading, that when hit checks at runtime "hey, this method didn't exist, but is there a `use`d function that would match based on the runtime type of this value?" But Ilija felt that would be prohibitively slow. It would certainly be slower than just a function/method call since the trap takes time.

Yep, this would def have to be done at runtime; not compile time, which pretty much rules out use as a solution (at least as use is currently implemented). What could happen instead is that it literally patches the class’s method table when you define an extension method. This would make it globally available once defined; but that is probably fine 99% of the time (since you’re most likely the only one using that extension). You could even isolate it to the namespace it is defined in. A solution could be to encode the namespace in the injected method name as a sort of mangling.

namespace Foo;

function \OtherNamespace\Point.area(): int {}

// patches OtherNamespace\Point with a method called \Foo\Area()

Or something like that. The main issue is that then it can’t be resolved at runtime because namespaces are “erased”. You’d have to extract the current namespace from the current name which means bare code won’t be able to call it, even if it is in the namespace because bare code is executed in the global namespace (IIRC): https://3v4l.org/pRsoh

I did quite a bit of experimentation on this for nested classes (which needed to be able to differentiate between a namespace and a class of the same name). It’s not simple.

I don’t really have a good solution; so I’m sorry to only bring issues here. But at least I can name the challenges ahead. Feel free to ask me anything about this.

And then we get into questions of inheritance, and, well, it gets even messier fast.

One possibility that we riffed on during the pipes discussion (mostly off list, I think) was using +> for some combination of extension functions and Elixir-style first-arg pipe passing, so that $p+>area() would signal to the engine (compile time or runtime) that area() wasn’t a method but a function that should get $p passed to it. That would solve the “trap” problem, but still doesn’t address autoloading, the lack of compile time type knowledge, or how to differentiate between Point.area(), ShapeInterface.area(), Rect.area(), etc.

–Larry Garfield

— Rob