【Laravel】 Eager Loading 応用: ピボット上のリレーションの Eager Loading

# ピボット上のリレーションの Eager Loading

## 定義

```php:app/Concerns/Relations/EagerBelongsToMany.php
<?php

declare(strict_types=1);

namespace App\Concerns\Relations;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Collection;

/**
 * Class EagerBelongsToMany
 *
 * BelongsToMany と同一ですが，ピボット上のリレーションのEagerローディングに対応しています。
 */
class EagerBelongsToMany extends BelongsToMany
{
    /**
     * ロードしたいピボット上のリレーションを定義します。
     *
     * @var array
     */
    protected $pivotRelations = [];

    /**
     * 所有者側からみたピボットリレーションを生成しつつ，ピボット上のリレーションもロードします。
     *
     * @param Model[] $models
     */
    protected function hydratePivotRelation(array $models): void
    {
        parent::hydratePivotRelation($models);

        (new Collection($models))->map->{$this->accessor}->load($this->pivotRelations);
    }

    /**
     * ロードしたいピボット上のリレーションを定義します。
     *
     * @param $relations
     * @return $this
     */
    public function withPivotRelation($relations): self
    {
        $this->pivotRelations = array_merge(
            $this->pivotRelations, is_array($relations) ? $relations : func_get_args()
        );

        return $this;
    }

    /**
     * 除外したいピボット上のリレーションを定義します。
     *
     * @param $relations
     * @return $this
     */
    public function withoutPivotRelation($relations): self
    {
        $this->pivotRelations = array_values(array_diff(
            $this->pivotRelations, is_array($relations) ? $relations : func_get_args()
        ));

        return $this;
    }
}
```

```php:app/Concerns/HasExtendedRelationShips.php
<?php

declare(strict_types=1);

namespace App\Concerns;

use App\Concerns\Relations\EagerBelongsToMany;
use Illuminate\Database\Eloquent\Model;

/**
 * Trait HasExtendedRelationships
 *
 * 拡張リレーションを定義します。
 *
 * @mixin Model
 */
trait HasExtendedRelationships
{
    /**
     * belongsToMany と同一ですが，ピボット上のリレーションのEagerローディングに対応しています。
     * コンストラクタコール部分以外はコピペです。
     *
     * @param  string             $related
     * @param  string             $table
     * @param  string             $foreignPivotKey
     * @param  string             $relatedPivotKey
     * @param  string             $parentKey
     * @param  string             $relatedKey
     * @param  string             $relation
     * @return EagerBelongsToMany
     */
    public function eagerBelongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null,
                                  $parentKey = null, $relatedKey = null, $relation = null): EagerBelongsToMany
    {
        // If no relationship name was passed, we will pull backtraces to get the
        // name of the calling function. We will use that function name as the
        // title of this relation since that is a great convention to apply.
        if ($relation === null) {
            $relation = $this->guessBelongsToManyRelation();
        }

        // First, we'll need to determine the foreign key and "other key" for the
        // relationship. Once we have determined the keys we'll make the query
        // instances as well as the relationship instances we need for this.
        $instance = $this->newRelatedInstance($related);

        $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();

        $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();

        // If no table name was provided, we can guess it by concatenating the two
        // models using underscores in alphabetical order. The two model names
        // are transformed to snake case from their default CamelCase also.
        if ($table === null) {
            $table = $this->joiningTable($related);
        }

        return new EagerBelongsToMany(
            $instance->newQuery(), $this, $table, $foreignPivotKey,
            $relatedPivotKey, $parentKey ?: $this->getKeyName(),
            $relatedKey ?: $instance->getKeyName(), $relation
        );
    }
}
```

## 利用例

```php:app/Community.php
// コミュニティ
class Community extends Model {}
```

```php:app/User.php
// ユーザ
class User extends Model
{
    use HasExtendedRelationShips;

    public function communities(): Builder
    {
        return $this->eagerBelongsToMany(Community::class)
            ->as('membership')
            ->using(Membership::class)// ピボットクラスの指定
            ->withPivot('registered_at')// ピボット上の読み込むカラムの指定
            ->withPivotRelation('payment');// ピボット上の読み込むリレーションの指定
    }
}
```

```php:app/Membership.php
// ユーザは入会情報テーブルを通じてコミュニティに属している
// 更に入会情報テーブルから支払い情報にもリンクされている
class Membership extends Pivot
{
    protected $table = 'memberships';

    public function payment(): Builder
    {
        return $this->belongsTo(Payment::class);
    }
}
```

```php:app/Payment.php
// 支払い情報
class Payment extends Model {}
```

```php
$users = User::with('communities')->whereIn('id', [1, 2, 3])->get();

// これらの User が所持しているすべての Payment を取得
// Eager Loading により N+1 問題回避可能
dd($users->map->communities->map->membership->map->payment);
```