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.
WDYT guys?
--
Best regards,
Dmitrii Derepko.
@xepozz
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 Dmitriiso desugared version is:
function getName(stdClass $obj) {
return $obj->name;
}$obj = new stdClass;
$obj->name = 'Dmitrii';
echo getName($obj); // prints DmitriiQuite 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 receiverAs 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-1dd36b02e5025ec3a5a546f8e41374ee4fc18c411bce447dd1dc2952329ccbe6R25I 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?
-
I love extension functions as a concept. Easily my favorite part of Kotlin, coming from PHP. So, +1 in the abstract.
-
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.
-
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?
-
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.
On Mon, Jun 9, 2025 at 10:29 PM Larry Garfield larry@garfieldtech.com
wrote:
- 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.
- 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?
- 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
Thanks for participating, Larry.
- 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.
- 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.
- 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
Thanks for participating, Larry.
- 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.
- 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.
- 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.phpfunction 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
I see.
The first thought is about extending the class definition: along with
functions hashtable we would add virtual functions HT and lookup for the
function will be another HT lookup.
[image: image.png]
That for sure is better than trap for cpu, but enlarge memory usage at the
moment.
Moreover, it allows us to keep both static and dynamic functions.
function class::func(){} // static extension class:func()
function class->func(){} // dynamic function $class->func()
this functions can be found dynamically and it seems alright:
// a.php
use Models\User;
function User::getName(){}
// new User->getName is available here
// b.php
use Models\User;
// new User->getName is NOT available here, because a.php wasn't loaded and
here User::getName isn't available
// c.php
namespace Stuff;
include 'a.php';
// new User->getName here you can do it, because a.php was loaded and
functions table was adjusted with getName function
// d.php
use Stuff\Smth;
use Tools\Smth2;
// if it's an app entrypoint
// autoloader loads c.php because of Smth file is found there
// c.php loads a.php
// the extension function User::getName is also available here
// either is this file an entrypoint or not
// it stores the extension function in the class definition
// so Smth2 now can use the extension function, because it was loaded "by
parent"
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.
Is this covered by the texts above?
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.
That's what we have now. Can't see any problems.
On Tue, Jun 10, 2025 at 11:11 PM Larry Garfield larry@garfieldtech.com
wrote:
Thanks for participating, Larry.
On Mon, Jun 9, 2025 at 10:29 PM Larry Garfield larry@garfieldtech.com
wrote:
- 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.
- 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.
- 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.phpfunction 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 ause
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
--
Best regards,
Dmitrii Derepko.
@xepozz
I see.
The first thought is about extending the class definition: along with functions hashtable we would add virtual functions HT and lookup for the function will be another HT lookup.
image.png
That for sure is better than trap for cpu, but enlarge memory usage at the moment.
Moreover, it allows us to keep both static and dynamic functions.
function class::func(){} // static extension class:func()
function class->func(){} // dynamic function $class->func()
this functions can be found dynamically and it seems alright:
// a.php
use Models\User;function User::getName(){}
// new User->getName is available here
// b.php
use Models\User;
// new User->getName is NOT available here, because a.php wasn't loaded and here User::getName isn't available
// c.php
namespace Stuff;
include 'a.php';
// new User->getName here you can do it, because a.php was loaded and functions table was adjusted with getName function
// d.php
use Stuff\Smth;
use Tools\Smth2;// if it's an app entrypoint
// autoloader loads c.php because of Smth file is found there
// c.php loads a.php
// the extension function User::getName is also available here// either is this file an entrypoint or not
// it stores the extension function in the class definition
// so Smth2 now can use the extension function, because it was loaded "by parent"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.Is this covered by the texts above?
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.
That's what we have now. Can't see any problems.
Thanks for participating, Larry.
- 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.
- 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.
- 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.phpfunction 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
--
Best regards,
Dmitrii Derepko.
@xepozz
How does it handle late binding? (This is when a class extends/implements a class that doesn’t exist yet; so the compiler postpones actually creating the class until runtime when it can run the autoloader, if needed) should these functions also late bind in that case?
— Rob
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.htmlIn PHP it might looks like so:
function stdClass.getName() {
return $this->name;
}$obj = new stdClass;
$obj->name = 'Dmitrii';
echo $obj->getName(); // prints Dmitriiso desugared version is:
function getName(stdClass $obj) {
return $obj->name;
}$obj = new stdClass;
$obj->name = 'Dmitrii';
echo getName($obj); // prints DmitriiQuite 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 receiverAs 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.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?
--
Best regards,
Dmitrii Derepko.
@xepozz
It's an interesting concept, and reminds me of how Lua handles this with
string.len(theVariable)
vs theVariable:len()
. The way you define the
function with a dot or colon, determines whether or not you have self
($this
) available or not. Feature wise, what would be use-cases to have
this?
DTOs in libraries, but perhaps also value objects could add a custom
factory function. I don't want my database models/entities to know about
the DTO, but the (vendor) DTO can't know about the database model/entity
either. This scenario would be to define a runtime static function to
create the object, and perhaps even a runtime function the other way around
to populate my entity based on the DTO. I could keep this code separate
from both classes and merely have it act as a glue. I don't know if this is
a good practice though.
What about interfaces and abstract classes, would these functions fulfill
contracts in any way? It sounds like runtime traits in that regard, but
makes static code analysis a lot more complex; not only for tools, but also
for humans to parse and understand. Would runtime traits be an alternate
solution to function extension to deal with autoloading?