HOW TO ENABLE RELATIONSHIP AUTOLOADING IN VERSIONS BEFORE V12.8

Apr 11, 2025 Copy Link

السلام عليكم 🙋‍♂️

 

النهارده جايلكوا بمقالة مهمة جداً لكل Laravel Developer و هتكلم عن إن إزاي تعمل load لل relationships في لارفيل automatically من غير ما تستخدم اي ميثود لل Eager Loading 🤩

 

من كام يوم ضاف Litvinchuk ميثود `withRelationshipAutoloading` في ال PR رقم #53655 و دي زي ما موجود في شرح الميرج إنها بتشوف ال relationships اللي انت مستدعيها و بتعملها Auto Eager Loading, طبعاً الميثود دي هتكون متاحة من أول فيرجن v12.8 فا أنا قررت أضيفها في ال versions اللي قبل كدا عشان لو اشتغلت علي مشروع قبل الفيرجن دا 🔥

 

تعالي نمشي خطوة خطوة مع بعض عشان اقولك عملت كدا إزاي 👣

 

في البداية كدا هنعمل overwrite علي شوية classes في لارفيل زي ال Model و ال Eloquent Collection و ال Eloquent Builder مع شوية traits...

 

هنروح جوا ال app و نكريت فولدر إسمه Overwrites و جواه two folders الأول Classes و التاني Traits, فا هنبدء بال traits و هنكريت ال HasAttributes و دا trait موجود بالفعل في لارفيل فا إحنا هنستعير الإسم دا (تقدر تختار اي إسم عادي)

namespace App\Overwrites\Traits;

trait HasAttributes
{
    /**
     * Get a relationship.
     *
     * @param  string  $key
     * @return mixed
     */
    public function getRelationValue($key)
    {
        // If the key already exists in the relationships array, it just means the
        // relationship has already been loaded, so we'll just return it out of
        // here because there is no need to query within the relations twice.
        if ($this->relationLoaded($key)) {
            return $this->relations[$key];
        }

        if (! $this->isRelation($key)) {
            return;
        }

        if ($this->attemptToAutoloadRelation($key)) {
            return $this->relations[$key];
        }

        if ($this->preventsLazyLoading) {
            $this->handleLazyLoadingViolation($key);
        }

        // If the "attribute" exists as a method on the model, we will just assume
        // it is a relationship and will load and return results from the query
        // and hydrate the relationship's value on the "relationships" array.
        return $this->getRelationshipFromMethod($key);
    }
}

 

و نفس الكلام مع ال HasRelationships trait

namespace App\Overwrites\Traits;

use Closure;
use Illuminate\Database\Eloquent\Model;

trait HasRelationships
{
    /**
     * The relationship autoloader callback.
     *
     * @var \Closure|null
     */
    protected $relationAutoloadCallback = null;

    /**
     * Determine if a relationship autoloader callback has been defined.
     *
     * @return bool
     */
    public function hasRelationAutoloadCallback()
    {
        return ! is_null($this->relationAutoloadCallback);
    }

    /**
     * Define an automatic relationship autoloader callback for this model and its relations.
     *
     * @param  \Closure  $callback
     * @param  mixed  $context
     * @return $this
     */
    public function autoloadRelationsUsing(Closure $callback, $context = null)
    {
        $this->relationAutoloadCallback = $callback;

        foreach ($this->relations as $key => $value) {
            $this->propagateRelationAutoloadCallbackToRelation($key, $value, $context);
        }

        return $this;
    }

    /**
     * Enable relationship autoloading for this model.
     *
     * @return $this
     */
    public function withRelationshipAutoloading()
    {
        $this->newCollection([$this])->withRelationshipAutoloading();

        return $this;
    }

    /**
     * Attempt to autoload the given relationship using the autoload callback.
     *
     * @param  string  $key
     * @return bool
     */
    protected function attemptToAutoloadRelation($key)
    {
        if (! $this->hasRelationAutoloadCallback()) {
            return false;
        }

        $this->invokeRelationAutoloadCallbackFor($key, []);

        return $this->relationLoaded($key);
    }

    /**
     * Invoke the relationship autoloader callback for the given relationships.
     *
     * @param  string  $key
     * @param  array  $tuples
     * @return void
     */
    protected function invokeRelationAutoloadCallbackFor($key, $tuples)
    {
        $tuples = array_merge([[$key, get_class($this)]], $tuples);

        call_user_func($this->relationAutoloadCallback, $tuples);
    }

    /**
     * Propagate the relationship autoloader callback to the given related models.
     *
     * @param  string  $key
     * @param  mixed  $values
     * @param  mixed  $context
     * @return void
     */
    protected function propagateRelationAutoloadCallbackToRelation($key, $models, $context = null)
    {
        if (! $this->hasRelationAutoloadCallback() || ! $models) {
            return;
        }

        if ($models instanceof Model) {
            $models = [$models];
        }

        if (! is_iterable($models)) {
            return;
        }

        $callback = fn(array $tuples) => $this->invokeRelationAutoloadCallbackFor($key, $tuples);

        foreach ($models as $model) {
            // Check if relation autoload contexts are different to avoid circular relation autoload...
            if (is_null($context) || $context !== $model) {
                $model->autoloadRelationsUsing($callback, $context);
            }
        }
    }
}

 

و ندخل بقا في ال classes و نبدء بال Builder class

namespace App\Overwrites\Classes;

use Illuminate\Database\Eloquent\Builder as BaseBuilder;

class Builder extends BaseBuilder
{
    /**
     * Execute the query as a "select" statement.
     *
     * @param  array|string  $columns
     * @return \Illuminate\Database\Eloquent\Collection<int, TModel>
     */
    public function get($columns = ['*'])
    {
        $builder = $this->applyScopes();

        // If we actually found models we will also eager load any relationships that
        // have been specified as needing to be eager loaded, which will solve the
        // n+1 query issue for the developers to avoid running a lot of queries.
        if (count($models = $builder->getModels($columns)) > 0) {
            $models = $builder->eagerLoadRelations($models);
        }

        $collection = $builder->getModel()->newCollection($models);

        if (Model::isAutomaticallyEagerLoadingRelationships()) {
            $collection->withRelationshipAutoloading();
        }

        return $this->applyAfterQueryCallbacks($collection);
    }
}

 

خلي بالك إن الميثود اللي فوق دي مش تبع ال PR بمعني إنها مش زيادة علي لارفيل زي باقي الشغل بتاعنا فهي تعتبر override علي ال get ميثود اللي موجوده جوا لارفيل, فا لازم تتأكد إنك مأثرتش علي ال functionalities الأساسية للميثود دي جوا لارفيل.

 

و بعدها  ال Collection class

namespace App\Overwrites\Classes;

use Illuminate\Database\Eloquent\Collection as BaseCollection;

class Collection extends BaseCollection
{
    /**
     * Load a relationship path for models of the given type if it is not already eager loaded.
     *
     * @param  array<int, <string, class-string>>  $tuples
     * @return void
     */
    public function loadMissingRelationshipChain(array $tuples)
    {
        [$relation, $class] = array_shift($tuples);

        $this->filter(function ($model) use ($relation, $class) {
            return ! is_null($model) &&
                ! $model->relationLoaded($relation) &&
                $model::class === $class;
        })->load($relation);

        if (empty($tuples)) {
            return;
        }

        $models = $this->pluck($relation)->whereNotNull();

        if ($models->first() instanceof BaseCollection) {
            $models = $models->collapse();
        }

        (new static($models))->loadMissingRelationshipChain($tuples);
    }

    /**
     * Enable relationship autoloading for all models in this collection.
     *
     * @return $this
     */
    public function withRelationshipAutoloading()
    {
        $callback = fn($tuples) => $this->loadMissingRelationshipChain($tuples);

        foreach ($this as $model) {
            if (! $model->hasRelationAutoloadCallback()) {
                $model->autoloadRelationsUsing($callback);
            }
        }

        return $this;
    }
}

 

و بعد كدا هنكريت ال Model class بتاعنا إحنا و اللي لازم يورث ال Model class الخاص ب لارفيل عشان نحتفظ بنفس ال functionalities و اللي من خلاله هنخلي لارفيل تستخدم فيه ال two classes اللي فوق دول بدل اللي هي بتستخدمهم

namespace App\Overwrites\Classes;

use App\Overwrites\Traits\{HasAttributes, HasRelationships};
use Illuminate\Database\Eloquent\Model as BaseModel;
use App\Overwrites\Classes\Collection as CustomCollection;
use App\Overwrites\Classes\Builder as CustomBuilder;

abstract class Model extends BaseModel
{
    use HasAttributes, HasRelationships;

    /**
     * Indicates whether relations should be automatically loaded on all models when they are accessed.
     *
     * @var bool
     */
    protected static $modelsShouldAutomaticallyEagerLoadRelationships = false;

    /**
     * Determine if model relationships should be automatically eager loaded when accessed.
     *
     * @param  bool  $value
     * @return void
     */
    public static function automaticallyEagerLoadRelationships($value = true)
    {
        static::$modelsShouldAutomaticallyEagerLoadRelationships = $value;
    }

    /**
     * Determine if relationships are being automatically eager loaded when accessed.
     *
     * @return bool
     */
    public static function isAutomaticallyEagerLoadingRelationships()
    {
        return static::$modelsShouldAutomaticallyEagerLoadRelationships;
    }

    /**
     * Create a new Eloquent query builder for the model.
     *
     * @param \Illuminate\Database\Query\Builder $query
     * @return \App\Overwrites\Builder
     */
    public function newEloquentBuilder($query)
    {
        return new CustomBuilder($query);
    }

    /**
     * Create a new Eloquent Collection instance.
     *
     * @param  array<array-key, \Illuminate\Database\Eloquent\Model>  $models
     * @return TCollection
     */
    public function newCollection(array $models = [])
    {
        return new CustomCollection($models);
    }
}

 

و بعد كدا كل اللي عليك إنك تخلي كل ال models بتوعك يورثوا ال Model class بتاعنا و تقدر تعمل تيست للشغل دا عن طريق laravel-debugbar و متنساش تدعيلي 😉

 

التحايل اللي عملناه دا عشان ن call ال relationships بشكل automatic علي اد ما هو هيساعدك و هيوفر عليك وقت, اكيد ليه عيوب زي العيب اللي ذكرته فوق, فا ممكن يقابلك مشاكل مش معمول حسابها و عن تجربة شخصية علي مشروع فيرجن v9. فا لازم تفكر كويس قبل ما تستخدم الطريقة دي.

 

و بكدا اقدر اقول إني خلصت و أتمني تكون إستفدت ✔

Share via

Mahmoud Ramadan

Mahmoud Ramadan

Mahmoud is the creator of Digging Code and a contributor to Laravel since 2020.

Newly published

  • How to Enable Relationship Autoloading in Versions Before v12.8

    How to Enable Relationship Autol...

    FREE

  • Get your environment ready to welcome Laravel v12

    Get your environment ready to we...

    FREE

  • How to generate Arabic PDF using TCPDF

    How to generate Arabic PDF using...

    FREE