bobthecow / mustache.php

A Mustache implementation in PHP.
http://mustache.github.io/
MIT License
3.25k stars 420 forks source link

Unable to define the order of precedence between Methods and Properties #156

Open pwhelan opened 11 years ago

pwhelan commented 11 years ago

I am using Mustache to render Laravel ORM objects. Laravel's ORM specifies relationships, by defining a method that returns a Relationship object, ie:

class Artist extends \Illuminate\Database\Eloquent\Model
{
    public function picture()
    {
        return $this->belongsTo('Picture');
    }
}

This object does not return an instance of the related object to work with, just data to accomplish the link. The actual related object is then accesible as a property (via the magic methods isset and get). Because the order in which the renderer resolves the dot notation is methods over properties it is impossible to reach the actual data in said relationships.

I was able to quickly rectify this but I am wondering what would be a more permanent solution. I can quickly whip up the actual code for said solution. I was thinking of adding a new option; childResolutionOrder (not entirely happy with the name) which by default would be set to ['method', 'property'].

Any thoughts or comments?

bobthecow commented 11 years ago

I've never used Laravel, but that seems like an odd way of defining relationships :P

As a general rule, I try to avoid adding new options. Especially options to fundamentally change the way a library works :) There's often a better way to deal with issues, so let's see if we can find one.

The go-to answer for most things Mustache is "use a view model, rather than your domain model". This makes for a bit more setup cost, but usually pays off in the long run. Your rendering will be faster, your templates will be cleaner, and best of all, your view logic will actually be testable!

But nobody ever likes to hear that answer.

So if that doesn't work for you, there are probably other things we can do too. Maybe a helper for accessing properties on your domain model classes?

class PropertyAccessor {
    protected $target;
    public function __construct($target) {
        $this->target = $target;
    }
    public function __isset($name) {
        return isset($this->target->$name);
    }
    public function __get($name) {
        return $this->target->$name;
    }
}

Then mix that in something like this:

class MoreEloquentModel extends \Illuminate\Database\Eloquent\Model {
    private $props;
    public function props() {
        if (!isset($this->props)) {
            $this->props = PropertyAccessor($this);
        }
        return $this->props;
    }
}

And access properties using that helper instead:

<!-- while this would fail -->
{{ artist.picture }}

<!-- this should work just fine -->
{{ artist.props.picture }}

All without any fundamental changes to the way variable resolution works in Mustache :)

pwhelan commented 10 years ago

How can I continue to use methods on the exposed object if I wrap it in a helper?

bobthecow commented 10 years ago

@pwhelan In the example listed above, you'd just call it on the object:

<!-- this would call a method on artist -->
{{ artist.foo }}

<!-- while this one would use the property accessor to access artist's properties -->
{{ artist.props.foo }}
pwhelan commented 10 years ago

I think I solved it with a decorator that also wraps any outgoing properties that are also Eloquent models.

class MustacheModelHelper
{
    private $model;

    public function __construct(\Illuminate\Database\Eloquent\Model $model)
    {
        $this->model = $model;
    }

    public function __get($key)
    {
        if (isset($this->model->$key)) {
            $ret = $this->model->$key;
            if ($ret instanceof \Illuminate\Database\Eloquent\Model) {
                return new MustacheModelHelper($ret);
            }
            return $ret;
        }
        else if (method_exists($this->model, $key)) {
            return $this->model->$key();
        }
    }

    public function __isset($key)
    {
        if (isset($this->model->$key)) {
            return true;
        }
        else if (method_exists($this->model, $key)) {
            return true;
        }
        return false;
    }
}

With this decorator I should be able to reach nested objects, access properties or methods and all of this without adding some new convention like 'props' to reach them (which isn't an option since I am working with a designer).

bobthecow commented 10 years ago

@pwhelan Yep. That looks great too.

pwhelan commented 10 years ago

I ran into problems the last time I tried it. I'll update the comment if I run into problems with this attempt.