Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accessing template functions without $this #9

Closed
reinink opened this issue Feb 26, 2014 · 4 comments
Closed

Accessing template functions without $this #9

reinink opened this issue Feb 26, 2014 · 4 comments

Comments

@reinink
Copy link
Contributor

reinink commented Feb 26, 2014

The idea

I have investigated the possibility of making template functions available without using the $this operator. For example, instead of calling $this->layout('template') you could use layout('template'). The purpose of doing this is to try reduce the amount of unnecessary syntax within the templates. Below are my findings.

The implementation

There is no way to dynamically create user-defined functions in PHP, which means all template functions, including extension functions, need to explicitly defined. Here is an example of this using the layout() function:

// functions.php

use \League\Plates\Template;

if (!function_exists('layout')) {
    function layout()
    {
        return call_user_func_array(array(Template::getInstance(), __FUNCTION__), func_get_args());
    }
}

The template (and extension) functions require an instance of the template object, however, in global scope the template is not available. The workaround here is to add a public static $instance parameter to the template object which is temporarily assigned during rendering. This requires the following updates to the template object:

// Template.php

namespace League\Plates;

class Template
{
    public static $instance;

    public function render($name, Array $data = null)
    {
        ob_start();

        $this->data($data);

        include_once 'functions.php';

        self::$instance = $this;

        include($this->_internal['engine']->resolvePath($name);

        if (isset($this->_internal['layout'])) {

            $this->_internal['child'] = ob_get_contents();

            ob_clean();

            include($this->_internal['engine']->resolvePath($this->_internal['layout']));
        }

        self::$instance = null;

        return ob_get_clean();
    }

    public static function getInstance()
    {
        if (is_null(self::$instance)) {
            throw new \LogicException('Cannot use template functions outside of templates.');
        }

        return self::$instance;
    }
}

Enable/disable this feature

A new engine option could even be made available to enable this feature, such as $engine->useGlobalFunctions(boolean). Setting this option to true would automatically include the template functions. This could be implemented within the template's render() method like this:

if ($this->_internal['engine']->useGlobalFunctions()) {

    include_once 'functions.php';

    self::$instance = $this;
}

The issues

While the above implementation works, there are a number of problems:

Too many global functions

The most obvious issue is that the new global template functions could conflict with other libraries and application code. For example, is it really that smart to have a global insert() function just for the sake of this feature?

Issues around autoloading

As far as I can see, functions must be included in a separate functions.php file, as opposed to just adding them to the bottom of the existing class files. For some reason when using class autoloading, functions defined at the bottom of a class file are not available at runtime. However, adding them to a separate file and then including them seems to work fine. For the main template functions (ie. layout, start, stop, child, insert, etc.) this isn't a big deal as it could be worked into the library, but it certainly adds a layer of complexity when creating extensions.

Consistency with variable access

In my mind this feature would only make sense if the same approach could be taken with variable access as well, essentially making it possible to access both functions and variables without the $this operator. However, this is very unlikely, as there is no way to make ALL template variables accessible in local scope, given how Plates has been designed.

This is quite easy to do with variables assigned outside of a template (ie. in a controller). These can be converted to the local scope easily within the template's render() method like so:

foreach (get_object_vars($this) as $var => $value) {

    if ($var !== '_internal') {
        ${$name} = $value;
    }
}

However, what makes Plates so powerful is its ability to assigned variables within the templates themselves. For example, you may assign your page title at the top of your template like this: <?php $this->title = 'About Us' ?>.

Since these variables are assigned to the template object during the rendering process, there is no way to convert them to variables in the local scope. Sure, you could assign them immediately to the local scope (ie. <?php $title = 'About Us' ?>), but then they would not be available to other included templates (ie. layout and nested). Not good.

The problem worsens for helper functions, like the inheritance start('sidebar') and stop() functions. The content generated between these two functions is saved as a parameter of the template object itself, making them available to all included templates (ie. <?=$this->sidebar?>). Again, in this situation there is no way to convert this new object parameter into a local scope variable (ie. <?=$sidebar?>), as the local scope is actually within the render() method.

So, unless template variables can be made entirely available without the $this operator, it would only add confusion to have some variables available this way, and others not.

Closing thoughts

One "last ditch" idea I had was to make all functions within local scope accessible using closures, which are actually aliases to the underlying object methods. So, for example, $this->layout('template') could become $layout('template'). Closures wouldn't suffer from the scope issues that variables suffer from, as all functions are known before the rendering begins. They can also be dynamically generated, and therefore don't suffer from the autoloading issues. Honestly, if variable access was possible this way (which is isn't , see above), I'd seriously consider this.

Finally, a simple option would be to just assign the $this operator to a shorter variable name, such as $t. I'm personally not a fan of this given that 1) it could conflict with local scope variables, 2) it makes it less clear that the template is essentially an object, and 3) it only saves 3 characters.

Because of the above issues, this idea will not be added to Plates at this time. Accessing all template variables and functions using the $this operator keeps things very "neat and tidy". It also makes it very obvious that it's a template specific variable or function.

@SxDx
Copy link
Contributor

SxDx commented Feb 26, 2014

Totally agree on that. Mainly because global functions conflicts happen way to often, e.g. it is impossible to use klein\klein with anything that requires illuminate\support.

@reinink
Copy link
Contributor Author

reinink commented Feb 26, 2014

@SxDx Thanks for taking the time to read. I wanted to get this stuff written down, because I'm sure it will come up in the future and I'll want to know why I decided against it. ;)

@reinink
Copy link
Contributor Author

reinink commented Jun 30, 2014

Closures wouldn't suffer from the scope issues that variables suffer from, as all functions are known before the rendering begins. They can also be dynamically generated, and therefore don't suffer from the autoloading issues. Honestly, if variable access was possible this way (which is isn't , see above), I'd seriously consider this.

This is actually a very bad idea, as all function names (core and extensions) could then conflict with template variables, which would be very annoying. For example: $data, $layout, $content, $child, $start, $end, $e, $escape, $asset, $batch.

@lalop
Copy link

lalop commented Jul 1, 2014

Isn't possible to define this functions inside the Template class namespace ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants