This RFC is based on the scalar objects extension.
Scalar extensions allow extending scalar types with methods. The motivation for this RFC is readability and the opportunity for a cleaner standard library.
use extension string StringExtension; use extension array ArrayExtension; class StringExtension { public static function split($self, $separator) { return explode($separator, $self); } } class ArrayExtension { public static function map($self, $callable) { return array_map($callable, $self); } public static function join($self, $separator) { return implode($separator, $self); } } $x = '1, 2, 3, 4' ->split(', ') ->map(fn($y) => $y * 2) ->join(', '); // $x = '2, 4, 6, 8'
Currently libraries provides functions that operate on scalar values. Examples are array_map
, explode
, preg_match
, etc. When you start nesting these function calls the code can become very unreadable, especially when the functions accept multiple parameters. Compare the following two code snippets.
$x = range(1, 10) ->filter(fn($y) => $y % 2 === 0) ->map(fn($y) => $y * 2); // vs $x = array_map( fn($y) => $y * 2, array_filter( range(1, 10), fn($x) => $x % 2 === 0 ) );
Nested function calls have to be read from the inside out. Chaining the methods can be read from left to right / top to bottom.
Chaining methods also automatically solves the needle/haystack problem.
// Which one is the needle again? str_contains('foo', 'f'); strpos('foo', 'f'); // Ah, much more obvious 'foo'->contains('f'); 'foo'->indexOf('f');
Function names are usually prefixed by the scalar type (string_
, str
, array_
, etc.) to make them unique. Scalar extensions make prefixes unnecessary as the extension is restricted to a given type.
// No need to array_/string_ prefix, it's clear from the operand type ['foo']->contains('foo'); 'foo'->contains('f');
use extension
is only applied to the current file. This will allow seamless integration of libraries that might be using different scalar extensions. use extension
is only valid at the top of the file.
// file1.php use extension string FooStringExtension; 'foo'->foo(); 'foo'->bar(); // Not valid here // file2.php use extension string BarStringExtension; 'bar'->bar(); 'bar'->foo(); // Not valid here
You can register multiple handlers per type. They will be tried in sequence until a method with the given name is found.
use extension string FooStringExtension; use extension string BarStringExtension; class FooStringExtension { public function foo($self) {} public function both($self) {} } class BarStringExtension { public function bar($self) {} public function both($self) {} } 'foo'->foo(); // Ok 'bar'->bar(); // Ok 'both'->both(); // FooStringExtension::both called 'baz'->baz(); // Error: Call to undefined method string::baz()
The handlers __callStatic
method will be called if it is implemented.
use extension string FooStringExtension; class DynamicStringExtension { public function foo($self) {} public function __callStatic($self) {} } 'foo'->foo(); // DynamicStringExtension::foo called 'bar'->bar(); // DynamicStringExtension::__callStatic called
Scalars are normally passed by value. The same goes for scalar extensions. Modifying $self
in a scalar extension does not modify the original value. To actually modify the original value you need to pass $self
by reference using &
.
use extension array ArrayExtension; class ArrayExtension { public static function append($self, $value) { $self[] = $value; } public static function appendByRef(&$self, $value) { $self[] = $value; } } $x = []; $x->append('foo'); // $x is still empty $x->appendByRef('foo'); // Now $x is ['foo']
The autoloading of the extension class is only triggered when a method is called on a value of the given type.
use extension array ArrayExtension; // ArrayExtension is registered as an extension but not loaded $x = []; $x->anything(); // Only now does PHP look for a class named ArrayExtension
There is no performance overhead to existing method calls. Scalar extensions are only triggered if the value is a scalar.
This RFC provides a big opportunity to provide a better standard library for scalar types. Designing and implementing a standard library is a large undertaking and out of scope for a single proposal. It’s important we don’t rush it as we’ll have to live with this API for a long time. Each component should be introduced in a separate RFC.
This RFC does not provide a standard library. But it’s an invite for other proposals to add build the standard library bit by bit.
All examples in this RFC are only examples.
We might want to allow extending other types like classes and interfaces in the future.
use Acme\Foo; use extension Foo FooExtension; class FooExtension { function bar($self) {} } $foo = new Foo(); $foo->bar();
There are no backward incompatible changes in this RFC.