Search code examples
phplaravelmany-to-many

Is there a way to dinamically call a laravel relationship in this scenario?


I'm working on a Laravel project with a few models. My main model Product has a few many to many relationships with some secondary models on the app. I created the required methods to setup the relationships on the respective models, the thing is, that now I need a clean way to add or remove the related secondary models. Having my Product defined as:

class Product extends Model 
{

    public function equipments()
    {
        return $this->belongsToMany('App\Equipment', 'product_has_equipment');
    }

    public function rawmaterials()
    {
        return $this->belongsToMany('App\RawMaterial', 'product_has_raw_material')->withPivot('composition');
    }

    public function indirectcosts()
    {
        return $this->belongsToMany('App\IndirectCost', 'product_has_indirect_cost');
    }

    public function presentations()
    {
        return $this->belongsToMany('App\Presentation', 'product_has_presentation');
    }
}

I tried to do the following:

public function addRelated(string $type, array $items)
{
    $this->__call($type, [])->attach($items);
}

I thougth that way it would be possible to just specify a string matching one of the relationships names, and with __call it could be called dinamically, so if I pass 'equipments' as $type on the above function, I hoped the inner call would be:

$this->equipments()->attach($items);

But instead I got the error

Ridiculous error

This seems strange, given that the "undefined method" is exactly the same as the one adviced by laravel in same page.

Take into account that I DO know that this approach may be insecure code-wise, but this project will be an API running on a local server only to be consumed by an electron app, so security is not my priority in this case.


Solution

  • check the source code of Laravel model

    /**
         * Handle dynamic method calls into the model.
         *
         * @param  string  $method
         * @param  array  $parameters
         * @return mixed
         */
        public function __call($method, $parameters)
        {
            if (in_array($method, ['increment', 'decrement'])) {
                return $this->$method(...$parameters);
            }
    
            return $this->forwardCallTo($this->newQuery(), $method, $parameters);
        }
    

    and forwardCall says

    /**
         * Forward a method call to the given object.
         *
         * @param  mixed  $object
         * @param  string  $method
         * @param  array  $parameters
         * @return mixed
         *
         * @throws \BadMethodCallException
         */
        protected function forwardCallTo($object, $method, $parameters)
        {
            try {
                return $object->{$method}(...$parameters);
            } catch (Error | BadMethodCallException $e) {
                $pattern = '~^Call to undefined method (?P<class>[^:]+)::(?P<method>[^\(]+)\(\)$~';
    
                if (! preg_match($pattern, $e->getMessage(), $matches)) {
                    throw $e;
                }
    
                if ($matches['class'] != get_class($object) ||
                    $matches['method'] != $method) {
                    throw $e;
                }
    
                static::throwBadMethodCallException($method);
            }
        }
    

    since the $this->newQuery() returns a Illuminate\Database\Eloquent\Builder;,which doesn't have wanted methods