jamieleepreece/laravel-custom-relation 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

jamieleepreece/laravel-custom-relation

Composer 安装命令:

composer require jamieleepreece/laravel-custom-relation

包简介

A custom relation for when stock relations aren't enough.

README 文档

README

A custom relation wrapper for when stock relations aren't enough.

Do I need custom relationships? You may if...

  • None of the stock Relations fit the bill. (BelongsToManyThrough, etc)
  • You want/need your application to execute more optimised queries/relationships instead of performing multiple chained relationships (reduce overhead & N+1)
  • You want more control over what tables you can join whilst using native Laravel methods e.g. with (eager loading) or whereHas (existence) queries
  • You want control over every step of the relationship lifecycle

Use Cases

Basic Overview

Let's say we have 3 models:

  • User
  • Role
  • Permission

Let's also say User has a many-to-many relation with Role, and Role has a many-to-many relation with Permission.

So their models might look something like this. (I kept them brief on purpose.)

class User
{
    public function roles() {
        return $this->belongsToMany(Role::class);
    }
}
class Role
{
    public function users() {
        return $this->belongsToMany(User::class);
    }

    public function permissions() {
        return $this->belongsToMany(Permission::class);
    }
}
class Permission
{
    public function roles() {
        return $this->belongsToMany(Role::class);
    }
}

What if you wanted to get all the Permissions for a User, or all the Users with a particular Permission? There no stock Relation in Laravel to describe this. What we need is a BelongsToManyThrough but no such thing exists in stock Laravel.

Example

Custom relationship for fetching users permissions

use LaravelCustomRelation\HasCustomRelations;

class User
{
    use HasCustomRelations;

    /**
     * Get the related permissions
     *
     * @return App\Relations\Custom
     */
    public function permissions()
    {
        return $this->customRelationship(
            related: Permission::class,
            baseConstraints: function ($relation)
            {
                # Add base constraints (the base relationship query)
                function ($relation) 
                {
                    $relation->getQuery()
                        // join the pivot table for permission and roles
                        ->join('permission_role', 'permission_role.permission_id', '=', 'permissions.id')
                        // join the pivot table for users and roles
                        ->join('role_user', 'role_user.role_id', '=', 'permission_role.role_id');
                }
            },
            foreignKey: 'role_user.user_id'
        );
    }
}

Simple example using named arguments

The first two named arguments are required to define a custom relationship. The related argument is the NS for the target Model and the baseConstraints is for providing the base query of the custom relationship. This does not require any WHERE constraints, as these are applied dynamically depending on the relationship being called.

The foreignKey here is optional, but is passed so that default logic in the relationship lifecycle can be applied, such as mapping models to the parent, existence queries and eager loading. However, if you wanted to write your own handlers then you can pass through additional closures like so

use LaravelCustomRelation\HasCustomRelations;

class User
{
    use HasCustomRelations;

    /**
     * Get the related permissions
     *
     * @return App\Relations\Custom
     */
    public function permissions()
    {
        return $this->custom(

            # The target Model you want to obtain in the relationship
            Permission::class,

            # Add base constraints (the base relationship query)
            function ($relation) 
            {
                $relation->getQuery()
                    // join the pivot table for permission and roles
                    ->join('permission_role', 'permission_role.permission_id', '=', 'permissions.id')
                    // join the pivot table for users and roles
                    ->join('role_user', 'role_user.role_id', '=', 'permission_role.role_id');
            },

            function ($relation) 
            {
                # Specify model ID if if calling on single Model
                if ($this->id)
                {
                    $relation->getQuery()->where('role_user.user_id', $this->id);
                }
            },

            # Add eager constraints
            function ($relation, $models) 
            {
                # Specify where IDs for multiple models
                $relation->getQuery()->whereIn('role_user.user_id', collect($models)->pluck('id'));
            },

            # Map relationship models back into the parent models.
            # This example uses a dictionary for optimised sorting
            function($models, $results, $relation, $relationshipBuilder)
            {
                $dictionary = $relationshipBuilder->buildDictionary($results, 'user_id');

                foreach ($models as $model) {

                    if (isset($dictionary[$key = $model->getAttribute('id')]))
                    {
                        $values = $dictionary[$key];

                        $model->setRelation(
                            $relation, $relationshipBuilder->getRelated()->newCollection($values)
                        );
                    }
                }

                # Must return models
                return $models;
            },

            # Provide columns for existence join
            # For `has` (existence) queries, provide the correct columns for the join
            function($query, $parentQuery)
            {
                return $query->whereColumn(
                    $parentQuery->getModel()->getTable() . '.id', '=', 'role_user.user_id'
                );
            },
        );
    }
}

Long example using custom logic for every part of the relationship lifecycle

For Optimising Queries

Laravel provides an easy way to access relationships (data). However, distant relationships can become cumbersome to an application and provide both unnecessary overhead and in some cases N+1 issues.

Consider a scenario where an existing DB cannot be easily altered and the data needed has to be eager loaded via multiple chained relationships. Lets say you wanted to get all discounted products for all users over the past year. The eager loading approach for this would look like this:

User::with('orders.lines.product.discounts');

This would perform the following queries:

  1. Fetch all users
  2. Fetch all orders for all users (WHERE orders.user_id user IN (...[IDs]))
  3. Fetch all line items for every order (WHERE line_items.order_id user IN (...[IDs]))
  4. Fetch all products for every line (WHERE products.id user IN (...[IDs]))
  5. Fetch all discounts via pivot for every product (WHERE discounts.id user IN (...[IDs]))

In some cases the DB can be redesigned to avoid this. However, if this is not possible you may become stuck with a less performant way to obtain the required data. The DB facade also provides a way to manually join all the above tables and obtain the data in a separate query. The caveat for this is that you have to write additional code to supplement your existing queries outside of other eager loading relationships.

To rectify this, custom relationships allow a complex join to be placed within a relationship wrapper. The above relationships can be aggregated together in one big table join, allowing the Discount models to be loaded directly onto the User models. This essentially reduces five queries and additional overhead into two:

  1. User models are collected
  2. All Discount models are collected and mapped into the parent models

Example

use LaravelCustomRelation\HasCustomRelations;

class User
{
    use HasCustomRelations;

    /**
     * Get all distinct products which have been tagged
     *
     * @return App\Relations\Custom
     */
    public function discountedProducts()
    {
        return $this->customRelationship(

            # The target Model you want to obtain in the relationship
            related: Discount::class,

            # Add base constraints (the base relationship query)
            baseConstraints: function ($relation)
            {
                # Query for the discounts table
                $relation->getQuery()
                    ->distinct()
                    ->join('products_discount_pivot', 'discount.id', '=', 'products_discount_pivot.discount_id')
                    ->join('products', 'products_discount_pivot.product_id', '=', 'products.id')
                    ->join('line_items', 'products.id', '=', 'line_items.product_id')
                    ->join('orders', 'line_items.order_id', '=', 'orders.id')
            },

            foreignKey: 'orders.user_id'
        );
    }
}

Join multiple tables in one convenient relationship

This query will now provide an optimised way to gather distant relationships without additional overhead in your application. This is just an example, but is intended to be scaled up to applications where optimisations on every level are required.

By also allowing most of the default logic to run for the relationship lifecycle (not providing optional Closures as per above example), then a custom relationship can be defined with minimal code and lower code repetition.

Arguments

  1. related:

    • required
    • The fully qualified namespace of the target Model (what is to be fetched)
  2. baseConstraints:

    • required
    • The custom relationship query. This is where the joins are to be specified for the relationship, as well as any other SQL arguments, such as DISTINCT etc. There are no restrictions as to what query you can construct here, however, it should not contain the WHERE for foreign to primary keys, as this is applied on the fly.
  3. singleConstraints:

    • optional
    • Specify the WHERE clause for matching the parent ID to the foreign key
    • If not specified, the foreign key name is used in a WHERE constraint with the parent ID
    • This logic is only fired when executing the relationship from a single Model
  4. eagerConstraints:

    • optional
    • Specify the constraint to be applied while eager loading
    • If not specified, the logic is to apply a WHERE IN on the foreign key with all parent IDs
    • This logic is only applied when eager loading e.g. ->with('products')
  5. eagerMatcher:

    • optional
    • Specify the map function to assign all collected relationship models into the parent models
    • If not specified, the collection is mapped into the parent models using the foreignKey and localKey
    • This is only executed after an eager relationship query has ran
  6. existenceJoin:

    • optional
    • The additional constraint to be applied when using has (EXISTS)
    • If not specified, a join is created via the foreignKey and localKey columns
    • This is only executed when using has() / whereHas()
  7. localKey:

    • optional
    • Specify the local key (primary key) column which is to be used in queries. Table and key can be specified, or just the column name e.g. 'products.id'/'id'
    • If not specified, the primary key of the parent model is obtained
    • This key will be used throughout all default relationship logic. It will not be required if all other closures are provided e.g. singleConstraints, eagerConstraints, eagerMatcher and existenceJoin
  8. foreignKey:

    • optional
    • Specify the foreign key, which will be used within all default logic. In most cases a table and column dot notation key will be required e.g. 'orders.user_id'
    • If not specified, the foreignKey will be set as null, as it is impossible to guess the correct key.
    • Similar to the localKey, the foreignKey will also be used throughout all internal relationship lifecycle logic.

Testing Relationships

It is recommended to test a relationship type at a time after creating your baseConstraints query. You could start with testing the relationship on a single Model, then move onto testing eager loading etc. If the default logic provided does not fit the bill, then you may have to provide a custom Closure to take control over that part of the relationship. This package is flexible in covering the basic logic, but with the ability to provide bespoke code for every part of the relationship lifecycle.

Debugging Relationships

Recommend some sort of query debugging package, such as Clockwork, as well as the typical dd() within closures etc.

jamieleepreece/laravel-custom-relation 适用场景与选型建议

jamieleepreece/laravel-custom-relation 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 5 次下载、GitHub Stars 达 1, 最近一次更新时间为 2024 年 03 月 13 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「laravel」 「relation」 「custom」 「eloquent」 「relationship」 「bespoke」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 jamieleepreece/laravel-custom-relation 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 jamieleepreece/laravel-custom-relation 我们能提供哪些服务?
定制开发 / 二次开发

基于 jamieleepreece/laravel-custom-relation 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 5
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 12
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 0
  • Forks: 26
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2024-03-13