DEV Community

Wes
Wes

Posted on • Edited on

9 3

Unified symbol tables for properties and methods in PHP

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.

Closures 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"]);
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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 Closures

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!
Enter fullscreen mode Exit fullscreen mode

But Symbola solves this by keeping a handle for each of the created Closures, 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!
Enter fullscreen mode Exit fullscreen mode

The handles for Closures 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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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(){}
}
Enter fullscreen mode Exit fullscreen mode

Now, that's far from being nice, but... it's something. Refactoring works, static analysis works (mostly), search works.

Result in PHPStorm

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 Closures 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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)