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!

"; } } $p = function(){ echo "myProperty() called!

"; }; $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!

" ; }; } private function myMethod (){ echo "myMethod() called!

" ; } 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 () . "

" ; // 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 () . "

" ; // 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 of parent:: methods. I might add this at some point; e.g. Symbola::parent("method") as alternative to Closure::fromCallable("parent::method") .

Probably something else, let me know what :-P

Installation

composer require netmosfera/symbola

Symbola on GitHub

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.