Introduction
If you ever worked with the callable
type, you probably already know its quirks (... and even more quirks). I bet you hate them as much as I do. In short, a callable that once passed a callable
type declaration, not necessarily will pass all callable
type declarations (example). I think the callable
type was a bad idea altogether, and I think modern PHP code can be much better than that.
Closure
s type declarations have no context-dependent behavior. A Closure
reference is actually callable everywhere.
What we all should be doing, instead, is:
<?php
// 1- Retrieve the Closure from a callable just once
// 2- Only pass around Closures and always type-hint for Closure
// ...
// 42- Profit!
public function bar(Closure $in): Closure{
$in();
return Closure::fromCallable([$this, "method"]);
}
Making it nicer with Symbola
I wrote a small tool that improves the way we access object members. You can immediately install it and try it out using composer (composer require netmosfera/symbola
) or have a look at it on github.
Here is what it does:
<?php
use Netmosfera\Symbola\Symbola;
class Bar
{
use Symbola;
public $myProperty;
function __construct(Closure $p){
$this->myProperty = $p;
}
public function myMethod(){
echo "myMethod() called!\n";
}
}
$p = function(){
echo "myProperty() called!\n";
};
$bar = new Bar($p);
// Feature #1:
// Call a property like a method:
$bar->myProperty(); // myProperty() called!
// Feature #2:
// Reference a method like a property:
$methodReference = $bar->myMethod;
$methodReference(); // method() called!
Visibility support
Properties can be called only from compatible scopes, and method handles can be retrieved only from compatible scopes:
<?php
use Netmosfera\Symbola\Symbola;
class Bar
{
use Symbola;
private $myProperty;
function __construct(){
$this->myProperty = function(){
echo "myProperty() called!\n";
};
}
private function myMethod(){
echo "myMethod() called!\n";
}
public function callProperty(){
$this->myProperty();
}
public function getMethod(): Closure{
return $this->myMethod;
}
}
$bar = new Bar();
//------------------------------------------------------------------------------------------
try{
// The property is private, it cannot be called publicly:
$bar->myProperty();
} catch(Error $e){
echo $e->getMessage() . "\n";
// Error: Referenced the either undefined or
// non-public object member `Bar::myProperty`
}
//------------------------------------------------------------------------------------------
try{
// The method is private, it cannot be referenced publicly:
$methodReference = $bar->myMethod;
} catch(Error $e){
echo $e->getMessage() . "\n";
// Error: Referenced the either undefined or
// non-public object member `Bar::myMethod`
}
//------------------------------------------------------------------------------------------
// The property can be called privately, however:
$bar->callProperty(); // myProperty() called!
// Similarly, the method can be referenced privately,
// and the obtained reference is free to be passed to
// other scopes:
$methodReference = $bar->getMethod();
$methodReference(); // myMethod() called!
Obviously, protected
is also supported; a protected method can be referenced only within the class' hierarchy, and a protected property can be called only within the class' hierarchy.
Equatable Closure
s
A problem that Closure::fromCallable()
has, is that the returned objects cannot be compared for equality (latest PHP version tested is 7.2):
<?php
class A{ function bar(){} }
$a = new A;
$c1 = Closure::fromCallable([$a, "bar"]);
$c2 = Closure::fromCallable([$a, "bar"]);
assert($c1 === $c2); // Fail!
But Symbola solves this by keeping a handle for each of the created Closure
s, which is then reused when needed:
<?php
use Netmosfera\Symbola\Symbola;
class A{ use Symbola; function bar(){} }
$a = new A;
$c1 = $a->bar;
$c2 = $a->bar;
assert($c1 === $c2); // Works!
The handles for Closure
s are kept in the object itself and they are garbage-collected when the object is destroyed, thus naturally limiting the number of objects created in the program.
What's $this
set to?
$this
in property-Closures is not rebound to the host class' $this
; if this is needed, it must be performed manually using Closure::bind()
.
<?php
use Netmosfera\Symbola\Symbola;
class Baz
{
use Symbola;
public $qux;
function __construct($qux){
$this->qux = $qux;
}
}
class Foo
{
function getClosure(): Closure{
return function(){
assert($this instanceof Foo);
};
}
}
$foo = new Foo();
$baz = new Baz($foo->getClosure());
$baz->qux(); // $this in qux() is $foo, not $baz
IDE support
One of the worst problems with callable
is the lack of static analysis. IDEs really struggle distinguishing between strings/arrays and callables
.
<?php
// I look like a simple string, but trust me,
// I'm actually a callable ¯\_(ツ)_/¯
$foo = "baz";
// ...
// ಠ_ಠ
$foo();
With Symbola, magic functionality can be annotated using phpdoc's @property
and @method
:
<?php
use Netmosfera\Symbola\Symbola;
/**
* @property Closure $bar
* @method int qux(float $a)
*/
class Baz
{
use Symbola;
public $qux;
function __construct(){
$this->qux = function(float $a): int{};
}
function bar(){}
}
Now, that's far from being nice, but... it's something. Refactoring works, static analysis works (mostly), search works.
What's missing
Function-scope
static
variables don't work, but nobody uses them, right? They are a mess regardless.Static methods and properties are also not supported. I don't think it's important to add support only for
__callStatic
given the lack of a__getStatic
magic method.It is not possible to create
Closure
s out ofparent::
methods. I might add this at some point; e.g.Symbola::parent("method")
as alternative toClosure::fromCallable("parent::method")
.Probably something else, let me know what :-P
Installation
composer require netmosfera/symbola
I hope I convinced you to try it out, I bet you will like it!
Thanks for reading, and let me know what you think in the comments.
Top comments (0)