Unified symbol tables for properties and methods in PHP
Wes
Posted on January 12, 2018
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.
Posted on January 12, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.