关于 Repository 的设计模式

file

设计 Repository 的抽象基类,BaseRepository:

为了提高代码的 重用率,我只在基础类上写了,每个模型所必须要的操作,CURD 在到具体的 Repository 上去做实现:

<?php

namespace App\Repositories;

use App\Exceptions\GeneralException;

/**
 * 抽象的 Repository 类
 *
 * @package App\Repositories
 */
abstract class BaseRepository
{
    /**
     * 根据主键查找
     * 
     * @param $id
     * @param $trashed
     * @return mixed
     */
    public function find($id, $trashed = false)
    {
        if ($trashed) {
            return $this->query()->withTrashed()->findOrFail($id);
        }
        return $this->query()->findOrFail($id);
    }

    /**
     * 查询资源集合
     *
     * @param bool $query_string
     * @param bool $keys
     * @param int $paginate
     * @param bool $trashed
     * @return mixed
     * @throws GeneralException
     */
    public function getAll($query_string = false, $keys = false, $paginate = 15, $trashed = false)
    {
        try {
            $query = $this->query();
            if ($query_string && is_array($keys)) {
                foreach ($keys as $key => $value) {
                    $query->orWhere($value, 'like', "%$query_string%");
                }
            }

            if ($trashed) {
                $query->withTrashed();
            }

            return $query->paginate($paginate);
        } catch (Exception $exception) {
            throw new GeneralException('未知错误,导致查询失败', 500);
        }
    }

    /**
     * 创建查询构造器
     *
     * @return mixed
     */
    public function query()
    {
        return call_user_func(static::MODEL.'::query');
    }

    /**
     * 序列化模型实例
     *
     * @param array $attributes
     * @return mixed
     */
    abstract protected function serialization(array $attributes);
}

模型的 Repository,继承自上面设计的基类:

<?php

namespace App\Repositories;

use App\Models\Company;
use App\Exceptions\GeneralException;

/**
 * Class CompanyRepository
 *
 * @author George <jinrenjie@me.com>
 * @package App\Repositories
 */
class CompanyRepository extends BaseRepository
{
    /**
     * 定义数据模型
     */
    const MODEL = Company::class;

    /**
     * 创建企业信息
     *
     * @param array $attributes
     * @return Company
     * @throws GeneralException
     */
    public function store(array $attributes)
    {
        $compnay = $this->serialization($attributes);

        try {
            $compnay->save();
            return $compnay;
        } catch (Exception $exception) {
            throw new GeneralException('创建企业信息失败');
        }
    }

    /**
     * 更新企业信息
     *
     * @param Company $company
     * @param array $attributes
     * @return Company
     * @throws GeneralException
     */
    public function update(Company $company, array $attributes)
    {
        if (is_array($attributes)) {
            foreach ($attributes as $key => $value) {
                $company->$key = $value;
            }
            $company->saveOrFail();
            return $company;
        }
        throw new GeneralException('要更新的属性必须是数组');
    }

    /**
     * 删除企业信息
     *
     * @param Company $company
     * @param bool $force
     * @return bool|null
     * @throws GeneralException
     */
    public function delete(Company $company, $force = false)
    {
        try {
            return $force ? $company->forceDelete() : $company->delete();
        } catch (Exception $exception) {
            throw new GeneralException('删除企业信息失败');
        }
    }

    /**
     * 序列化用户输入
     *
     * @param array $attributes
     * @return Company
     */
    protected function serialization(array $attributes)
    {
        $company = self::MODEL;
        $company = new $company();

        $company->title = $attributes['title'];
        $company->introduction = array_get($attributes, 'introduction', null);
        $company->url = array_get($attributes, 'url', null);
        $company->logo = array_get($attributes, 'logo', null);
        $company->address = array_get($attributes, 'address', null);
        $company->contact = array_get($attributes, 'contact', null);

        return $company;
    }
}

这一层只处理数据的 CURD,并返回对应的操作结果,除了抛出异常劲量不涉及响应和请求……这样Controller 里只要调用方法而无需考虑异常处理。

<?php

namespace App\Http\Controllers\Interfaces;

use App\Models\Company;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Repositories\CompanyRepository;
use App\Extensions\Library\MaterialHandling;
use App\Http\Requests\Interfaces\Company\DeleteRequest;
use App\Http\Requests\Interfaces\Company\UpdateRequest;
use App\Http\Requests\Interfaces\Company\CreateRequest;
use App\Http\Requests\Interfaces\Company\ChangeLogoRequest;
use App\Http\Requests\Interfaces\Company\QueryResourceRequest;
use App\Http\Requests\Interfaces\Company\QueryCollectionRequest;

/**
 * Class CompanyController
 *
 * @package App\Http\Controllers\Interfaces\User
 */
class CompanyController extends Controller
{
    use MaterialHandling;

    /**
     * 定义保存 CompanyRepository 的实例
     * @var CompanyRepository
     */
    protected $companyRepository;

    /**
     * CompanyController constructor.
     * @param $companyRepository
     */
    public function __construct(CompanyRepository $companyRepository)
    {
        $this->companyRepository = $companyRepository;
    }

    /**
     * 获取企业信息集合
     *
     * @param QueryCollectionRequest $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function index(QueryCollectionRequest $request)
    {
        //获取分页大小
        $paginate = $request->get('paginate', 15);
        //获取模糊查询字符串
        $query_string = $request->get('query_string', false);
        //获取是否显示软删除的数据
        $trashed = $request->get('trashed', false);

        //定义需要模糊查询的字段
        $keys = [
            'title',
            'introduction',
            'address'
        ];

        $collections = $this->companyRepository
            ->getAll($query_string, $keys, $paginate, $trashed);

        return $this->success($collections);
    }

    /**
     * 根据企业 ID 获取企业信息
     *
     * @param Company $company
     * @param QueryResourceRequest $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function show(Company $company, QueryResourceRequest $request)
    {
        return $this->success($company);
    }

    /**
     * 创建企业信息
     *
     * @param CreateRequest $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function create(CreateRequest $request)
    {
        $attributes = $request->all();
        $company = $this->companyRepository->store($attributes);
        return $this->created($company);
    }

    /**
     * 更新企业信息
     *
     * @param Company $company
     * @param UpdateRequest $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function update(Company $company, UpdateRequest $request)
    {
        $attributes = $request->all();
        $company = $this->companyRepository->update($company, $attributes);
        return $this->updated($company);
    }

    /**
     * 删除企业信息
     *
     * @param Company $company
     * @param DeleteRequest $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function delete(Company $company, DeleteRequest $request)
    {
        $this->companyRepository->delete($company);
        return $this->deleted();
    }

    /**
     * 更改企业 Logo
     *
     * @param Company $company
     * @param ChangeLogoRequest $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function changeLogo(Company $company, ChangeLogoRequest $request)
    {
        $file = $request->file('logo');
        $path = $this->store($file, $request->user());
        $result = $this->companyRepository->update($company, ['logo' => $path]);
        return $this->updated($result);
    }
}

控制器里的自定义响应,我参照了@王举 的文章:Laravel5.5+passport 放弃 dingo 开发 API 实战,让 API 开发更省心

我还把 鉴权表单验证及错误信息 放到了 专门的 Request 里面去处理这样整体结构也更加清晰了,比如我的注册请求:

<?php

namespace App\Http\Requests\Auth;

use Illuminate\Support\Facades\Cache;
use Illuminate\Foundation\Http\FormRequest;

/**
 * 用户注册请求
 *
 * @author George <jinrenjie@me.com>
 * @package App\Http\Requests\Auth
 */
class RegisterRequest extends FormRequest
{
    /**
     * 定义授权规则
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * 定义验证规则
     *
     * @return array
     */
    public function rules()
    {
        return [
            'username' => 'required|alpha_dash|min:6|max:16|unique:users',
            'password' => 'required|confirmed|min:8|max:32',
            'mobile' => 'required|min:11|max:11|unique:users',
        ];
    }

    /**
     * 定义错误消息
     *
     * @return array
     */
    public function messages()
    {
        return [
            'username.required' => '请填写用户名',
            'username.alpha_dash' => '用户名只能是字母、数字、及下划线',
            'username.min' => '用户名不得少于6个字符',
            'username.max' => '用户名不得超过16个字符',
            'username.unique' => '该用户名称已存在,请使用其他名称注册',
            'password.required' => '请填写密码',
            'password.confirmed' => '两次填写的密码不一致',
            'password.min' => '密码最少8个字符',
            'password.max' => '密码最多32个字符',
            'mobile.required' => '请填写您的手机号码',
            'mobile.min' => '您输入的号码不满11位',
            'mobile.max' => '号码不得超过11位',
            'mobile.unique' => '该手机号码已被注册',
        ];
    }

    /**
     * 验证用户输入的验证码是否正确
     *
     * @param $validator
     */
    public function withValidator($validator)
    {
        $validator->after(function ($validator) {
            if (Cache::get($this->get('mobile'), false) !== $this->get('verification')) {
                $validator->errors()->add('verification', '你输入的验证码有误');
            } else {
                Cache::forget($this->get('mobile'));
            }
        });
    }
}

当然缺点就是要增加很多代码量,但是对于团队开发来说这样也许比较规范吧……

如果你有更好的分离方法,欢迎一起讨论,谢谢!

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由 Summer 于 6年前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 8

在这个站看到了一些文章,有了解到这么一个开发方案:controller做参数接收和数据响应,service做逻辑处理,repository做数据存取

2年前 评论
fatrbaby

经过我最近的实践来说,在Laravel中使用Repository模式我觉得没有必要。本站推出的Laravel开发规范也是不推荐使用Repository模式的。如果非要我说个理由,那就是不爽。

6年前 评论

@fatrbaby 这一点我在写一些逻辑交错的功能的时候也有感触,比如我会纠结 Relationship 我是直接写在 Controller 里还是 Repository 里,如果写在 Controller 里那么 有悖于 Repository,如果写在 Repository 里又有悖于 代码抽象的原则,这一块我还没想清楚改怎么去分离~
Repository 也许还可以在抽象一层,写成一个 Trait 在多个 Controller 中使用
总之看团队吧,为了后期便于理解和维护,前期多花点时间在基础铺设上,后面才能敏捷的迭代

6年前 评论

@carlclone 这两篇文字之前看过,可以说借鉴了很多,关于 Repository 跨 Model 的想法我还没想好怎么去做。单元测试也是一个大问题

6年前 评论

@Jinrenjie Model只是作为Repository的依赖注入了而已,没有什么跨model的说法

6年前 评论

@Jinrenjie 因为model作为依赖 ,所以在做单元测试的时候就可以mock一个model注入到Repository

6年前 评论

@carlclone
恩,依赖注入 Model 倒也挺好的,
只是调用的时候可能要这样:

//获取 model 实例 
$this->userRepository->model;

我改一下试试 :+1:

6年前 评论

public function delete(Company $company, DeleteRequest $request) { $this->companyRepository->delete($company); return $this->deleted(); }

代码没有贴全呢,deleted方法没有,company是根据id查询后的一个实例吧,在哪儿查询的代码也没贴,学了个大概

3年前 评论
GeorgeKing (楼主) 3年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!