Ariadne 9.0: Closures in Pinp Templates

Ariadne allows you to program your site or application using templates. These templates allow you to program using a subset of PHP, called PINP - Pinp Is Not PHP. Untill Ariadne 9.0 these templates were limited in that you cannot create new classes or functions, among others. This was a decision based on the idea that templates should have their effects limited to the subtree of Ariadne on which they were defined or called.

If you could create a normal PHP function in a template, then so could anyone. If somehow during a single request both of these templates were called and both defined a PHP function with the exact same name, PHP would trigger a fatal error.

So instead we disallowed function definitions altogether. Since PHP 5.4 however, it has become possible to use anonymous functions or closures(1). These are defined as a variable and thus they do not have the same problems. With Ariadne 9.0 we set the base PHP version to 5.4 and now allow closures in PINP templates.

(1) PHP 5.3 did have closure support, but did not bind them to $this.

How to use closures in PINP

PINP closely follows the PHP syntax, by design. This means that you can simply type the following:

<pinp>
    $f = function() {
        echo 'Hello world.';
    };
    $f();
</pinp>

And if you run it, it should say 'Hello world.', as expected.

Arguments also work as they should:

<pinp>
    $f = function($arg) {
        echo 'I got: '.$arg;
    };
    $f(1);
</pinp>

Which should say: 'I got 1'.

Closures are also scoped, so variables from outside have no impact on the inside:

<pinp>
    $a = 'A';
    $f = function() {
        echo '$a is '; var_dump($a);
    }
    $f();
</pinp>

This should say '$a is null'.

However all closures are connected to the object where they are defined using the $this variable:

<pinp>
    $f = function() {
        echo 'my path is '.$this->path;
    }
    $f();
</pinp>

This should say 'my path is /my/path/', where the path would depend on where the template was run.

Using this feature you can allow a closure to access variables from outside:

<pinp>
    $this->a = 'A';
    $f = function() {
        echo '$this->a is '.$this->a;
    }
    $f();
</pinp>

Now the closure does have access and says '$this->a is A'.

This also allows you to call closures from eachother:

<pinp>
    $this->f = function() {
        echo 'I am f().';
    }
    $this->f2 = function() {
        $this->f();
        echo ' And I am f2().';
    }
    $this->f2();
</pinp>

Which says 'I am f(). And I am f2().'.

Closures behave like any other variable, so you can put them in Ariadne's global scope and retrieve them:

template a:

<pinp>
$f = function() {
echo 'I am defined at '.$this->path;
}
ar::putvar('f', $f);
</pinp>

template b: 

<pinp>
ar::get('child/')->call('a');
$f = ar::getvar('f');
$f();
</pinp>

Which will say something like: 'I am defined at /path/to/b/child/';

Or pass them on as a return value of a template:

template a:

<pinp>
return function() {
echo 'I am defined at '.$this->path;
}
</pinp>

template b:

<pinp>
$f = current(ar::get('child/')->call('a'));
$f();
</pinp>

Which will work exactly as the previous example.

You can also pass closures to a number of newly allowed PHP functions in PINP:

<pinp>
    $array = array( 1, 2, 3, 4 );
    $even = array_filter( $array, function($i) {
        return ($i % 2) == 0;
    });
    var_dump($even);
</pinp>

Which will only leave 2 and 4 in $even. All of the array functions in PHP that have a callback parameter are now available in Ariadne 9.0. The important difference is that they only accept a closure as the callback parameter. You cannot pass a string with a function name, or an array with a class or object and method name. You cannot even pass a function created with mod_util::create_function(). All these options have been disabled to prevent access to insecure functions or methods.

If you already know how closures work in PHP, you may have noticed we skipped one feature: the use keyword. This is as yet not supported in Ariadne 9.0. In this case not because of some inherent security risks, but simply because adding this feature made our PINP compiler much more complex and brittle. So for now this will not work:

<pinp>
    $a = 'A';
    $f = function() use ( $a ) {
        echo '$a is '.$a;
    };
    $f();
</pinp>

If you do try this, the PINP compiler will show an error and refuse to compile it. If you do need access to 'outside' variables, for now you must either use $this->variable or use the ar::putvar() and ar::getvar() methods to put them in Ariadne's global scope.

How to use closures with Ariadne

Creating and calling closures is a nice feature, but it becomes much more useful when combined with Ariadne's own API. Instead of using ar::call() with a template name, you can now also use a closure. So all of these work:

$getName = function($ob) {
    return $ob->nlsdata->name;
}
$myName = ar::call($getName);
$myChildrensNames = ar::ls()->call($getName);
$someNames = ar::find('object.priority>0')->call($getName);
$myParentNames = ar::parents()->call($getName);

The important thing to note here is that the object that is called is passed to the closure as the first and only argument. If instead of $ob->nlsdata->name you return $this->nlsdata->name, you will find that all names are the same. The value of $this doesn't change, no matter on which object the closure is called. It is set only to the object where the closure is defined.

Using closures in this way has a number of advantages:

  • You can call any object, even outside the path where the current template is available. So you can call ar::parents() and use the closure without any problems. No template needs to be loaded, so no library needs to configured.
  • Calling a closure is much faster than calling a template. Templates can change at any object in Ariadne. You can load a new library with the same name - or no name - at any point. So for each called object, Ariadne checks which template matches the given name. This is cached, but still takes time. Closures have no need for this, there will always be the one closure.

The disadvantages follow from this:

  • You cannot override a closure on a child object called with it.
  • You cannot override a closure based on the type or subtype of an object.
  • So there is no support for polymorphism when using closures.

How to use closures with events

Just like ar::call() Ariadne's event listeners also call templates. So now they can also call closures:

<pinp>
    ar('events')->listen('onbeforesave')->call(function() {
        $event = ar('events')->event();
        // do something
    });
</pinp>

The closure follows the same rules as a template would. The event data must be retrieved with the ar('events')->event() method. Any arguments to the closure must be set in the listen() method, just like with a template.

<pinp>
    ar('events')
    ->listen('anevent')
    ->call(
        function($a, $b) {
            echo "$a and $b";
        },
        array( 'a', 'b' )
    );
</pinp>

Just like with a PINP template, the listener is called on the object where it is defined, not on the object that triggered the event. So $this will always point to the same object. Since Ariadne 9.0 the event data contains the path and full object where the event was triggered. So you can do this:

<pinp>
    ar('events')
    ->listen('anevent')
    ->call(
        function() {
            $event = ar('events')->event();
            echo 'Fired on '.$event->path.'<br>';
            echo 'Listener on '.$this->path.'<br>';
        }
    );
</pinp>