Skip to content

[Developer Feature Request] Add injection point to CoreExtension::getAttribute for getting attributes from custom objects #4491

Open

Description

@mnpenner

After sitting on Twig 2, PHP 7.1, and Laravel 5.5 for ~10 years, I decided to upgrade some of my packages. One of the many issues I hit was #360

After much trial and error, I figured out that adding this:

if($object instanceof \Illuminate\Database\Eloquent\Model) {
    return $object->getAttribute($item);
}

Right in here:

// object property
if (/* Template::METHOD_CALL */ 'method' !== $type) {

Makes most of my problems go away.


Firstly, yes, I'm using Twig with Laravel (via https://github.com/rcrowe/TwigBridge). As you might know, Laravel is riddled with magic. Models have both __get and __call methods. Some methods look like this:

    public function customer() {
        return $this->belongsTo(Customer::class,'customer_id');
    }

Which represents a database relationship. So when I try {{ booking.customer }} in Twig (which used to work in old versions), Twig will first try looking for a $customer property on my Booking, won't find that, then implicitly tries __get, which might work but if it comes back null (per the referenced issue), it'll then try calling it instead which returns a BelongsTo object. Now any {% if booking.customer %} checks are broken because BelongsTo is truthy. What I really wanted is for Twig to evaluate that relationship, the same way Laravel does it when I call it like $booking->customer (no parens) in PHP.

Now there's no reason for Twig to provide tight coupling/integration with Laravel, so I propose some way for developers to override how getAttribute works. Maybe via a Twig extension, or some configuration option, something that would let me inject that little snippet of code to handle Laravel models properly.

As an alternative, I'm realizing now that adding this to the end of that same block:

if (method_exists($object, '__get')) {
    if ($isDefinedTest) {
        return false;
    }
    return null;
}

Also seems to work. It prevents the method call fallback and allows getters to properly return null. This is probably a backwards-incompatible change, so an option to enable "prevent method call fallback when __get is defined" might work and be simpler.

Activity

  1. ericmorand commented on Dec 1, 2024

    @ericmorand
    Contributor

    As you might know, Laravel is riddled with magic.

    Maybe the issue is that Laravel is plagued with magic.

  2. shyim commented on Dec 11, 2024

    @shyim
  3. ju1ius commented on Jan 29, 2025

    @ju1ius
    Contributor

    some way for developers to override how getAttribute works

    ...would also be welcome in legacy applications where the conventions look like $foo->get_bar().

  4. mnpenner commented on Oct 26, 2025

    @mnpenner
    Author

    I managed to work around this on Laravel's side by making all my models extend this:

    class Model extends \Illuminate\Database\Eloquent\Model {
    
        public function offsetExists($offset): bool {
            if(parent::offsetExists($offset)) {
                return true;
            }
    
            if($this->isRelation($offset)) {
                $shouldPrevent = static::$modelsShouldPreventAccessingMissingAttributes;
                static::$modelsShouldPreventAccessingMissingAttributes = false;
                $value = $this->getRelationValue($offset);
                static::$modelsShouldPreventAccessingMissingAttributes = $shouldPrevent;
    
                return $this->relationLoaded($offset) || $value !== null;
            }
    
            return false;
        }
    
        public function __call($method, $parameters) {
            if(!$parameters) {
                if(array_key_exists($method, $this->attributes) || $this->hasGetMutator($method)) {
                    // Allow Twig to access attributes via method-style lookups (e.g. customer_id()).
                    return $this->getAttributeValue($method);
                }
    
                if(method_exists($this, $method)) {
                    $result = parent::__call($method, $parameters);
                    if($result instanceof Relation) {
                        $relationResults = $result->getResults();
                        $this->setRelation($method, $relationResults);
                        return $relationResults;
                    }
    
                    return $result;
                }
            }
    
            return parent::__call($method, $parameters);
        }
    }

    A bit of a hack, but it allowed me to unfork Twig.

    Seems to be working so far anyway. I still have a million other errors from upgrading everything.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions