预加载相同的关系,不同的约束

Eager-load Same Relationship, Different Constraints

在 Eloquent 中,我试图以不同的约束对同一关系进行两次延迟加载。这里的目标是了解有关员工时间表的两件事。一个是他们去年工作的所有时间。另一个是他们工作的第一次约会。

第一个关系约束是这样的:

$employeeTimeSheets = app(Timesheet::class)
    ->with([
        'punches' => function (Relation $query) {
            $query->where('punch_date', '>=', Carbon::now()->subYear());
        }
    ])

第二个是:

$employeeTimeSheets = app(Timesheet::class)
    ->with([
        'punches' => function (Relation $query) {
            $query
                ->whereNotNull('punch_date')
                ->orderBy('punch_date')
                ->limit(1);
        }
    ])

问题当然是'punches'只允许一次。并且将有几千名员工被拉到这里,所以为了性能能够预先加载这些数据对我来说很重要。

这些是一种任意条件,只在整个系统中出现一次。所以我不确定是否有必要向 Timesheet 模型添加一个全新的关系方法。而且我也不想对每个员工施加 所有 的压力,只是为了提取最后一年和事后的最小值,因为那将是一座数据山。

如果不覆盖 Eloquent 处理关系的方式以在名称中添加对 as 关键字的支持,我不确定这样做是否可行。但我真正想要的是这样的感觉:

$employeeTimeSheets = app(Timesheet::class)
    ->with([
        'punches as last_year' => function (Relation $query) {
            $query->where('punch_date', '>=', Carbon::now()->subYear());
        },
        'punches as first_punch' => function (Relation $query) {
            $query
                ->whereNotNull('punch_date')
                ->orderBy('punch_date')
                ->limit(1);
        }
    ])

有没有人有更好的方法?

想通了。覆盖模型 class 中的一些方法以解析 as 关键字就可以了。我将所有这些都塞进了一个特征中,但它可以很容易地移动到由所有模型扩展的基础 class,并且它本身扩展模型:

/**
 * @uses \Illuminate\Database\Eloquent\Model
 * @uses \Illuminate\Database\Eloquent\Concerns\HasAttributes
 * @uses \Illuminate\Database\Eloquent\Concerns\HasRelationships
 *
 * Trait RelationAlias
 */
trait RelationAlias
{
    protected $validOperators = [
        'as'
    ];

    /**
     * @param string $method
     * @param array $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        if ($key = $this->parseKey($method)) {
            $method = $key['concrete'];
            if (method_exists($this, $method)) {
                return $this->$method(...$parameters);
            }
        }

        return parent::__call($method, $parameters);
    }

    /**
     * @return array
     */
    protected function getArrayableRelations()
    {
        $arrayableRelations = parent::getArrayableRelations();
        foreach ($arrayableRelations as $key => $value) {
            if ($aliased = $this->parseKey($key)) {
                $arrayableRelations[$aliased['alias']] = $value;
                unset($arrayableRelations[$key]);
            }
        }

        return $arrayableRelations;
    }

    /**
     * @param $key
     * @return mixed
     */
    public function getRelationValue($key)
    {
        if ($found = parent::getRelationValue($key)) {
            return $found;
        }

        $relations = array_keys($this->relations);
        foreach ($relations as $relation) {
            $aliased = $this->parseKey($relation);
            if ($aliased && $aliased['alias'] == $key) {
                if ($this->relationLoaded($relation)) {
                    return $this->relations[$relation];
                }

                if (method_exists($this, $aliased['concrete'])) {
                    return $this->getRelationshipFromMethod($key);
                }
            }
        }
    }

    /**
     * @param $key
     * @return array|null
     */
    protected function parseKey($key)
    {
        $concrete = $operator = $alias = null;
        foreach ($this->validOperators as $operator) {
            if (preg_match("/.+ $operator .+/i", $key)) {
                list($concrete, $operator, $alias) = explode(' ', $key);
                break;
            }
        }

        return $alias ? compact('concrete', 'operator', 'alias') : null;
    }
}