在mvc框架中如何封装业务和功能代码

在laravel中如何使用repository模式

单一的CRUD使用ORM,没有封装的必要,比如banner的增删改查

将单一的功能封装成类,比如验证码的发送和验证,数学公式计算,

具有复杂逻辑的同一类需求,购物车整套逻辑, 商品SKU筛选,订单价格计算器(优惠,抵扣,积分等),这里需求可能需要查询多个模型,或者需要复杂的处理数据

一般在框架中封装成单个类放到services目录中

//验证码肯定具有两个方法,发送和验证
//这种固定方法的类可以使用静态方法方便调用
class VerificationCode
{
    const KEY_TEMPLATE = 'verify_code_of_%s';

    /**
     * 创建并存储验证码
     *
     * @param string $phone
     * @return int
     */
    public static function create($phone)
    {
        ...
    }

    /**
     * 检查手机号与验证码是否匹配.
     *
     * @param string $phone
     * @param int    $code
     *
     * @return bool
     */
    public static function validate($phone, $code)
    {
        ...
    }
}

第三方接口调用的封装

只有几个接口,封装成单个类,放到services目录中

需要调用很多接口,有场景和功能的划分,新建SDK目录,按接口功能类型或需求,使用设计模式(也可以不用)封装成简易SDK

一个项目示例

需要快速调用第三方接口完成业务需求,但是又没有顺手的sdk,或者过于复杂文档不全,学习成本过高,可以使用trait快速完成业务分割解耦,实现功能同时又易于维护。

使用http客户端guzzle,主文件sdkclient,实现签名认证参数组合 ,发送请求等方法。

<?php
/**
 * 契约锁SDK
 * https://open.qiyuesuo.com/document?id=2279605053767548928
 */

namespace App\Services\QiYueSuo;

use App\Exceptions\QiYueSuoException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Storage;

class SDKClient
{
   //使用trait 引用template业务相关接口
    use Template,Helps;

    public $config;

    public function __construct()
    {
        $this->config = config('services.qiyuesuo');
    }

    /**
     * 发送请求
     * 使用laravel封装的http客户端,DOC:https://learnku.com/docs/laravel/7.x/http-client/7487#introduction
     *
     * @param string $method 请求方法 'get', 'post', 'put', 'patch', 'delete'
     * @param string $subUrl 接口url,不包含域名
     * @param array $params 请求参数
     * @param string $content_type 请求类型 'asJson', 'asForm', 'asMultipart'
     * @param array $files 文件上传 $files['name']上传字段名称 $files['contents']文件内容,$files['filename']文件名,可选
     * @param bool $debug 是否开启debug日志,true是 false否
     * @return mixed
     */
    public function sendRequest(string $method, string $subUrl, array $params, string $content_type = '', array $files = [], bool $debug = false)
    {
        if (! in_array($method, ['get', 'post', 'put', 'patch', 'delete'])) {
            throw new QiYueSuoException('Error Request Method');
        }

        if ($content_type && ! in_array($content_type, ['asJson', 'asForm', 'asMultipart'])) {
            throw new QiYueSuoException('Error Method');
        }

        $url = $this->config['api_host'].$subUrl;
        $headers = $this->playloadHeader();

        $http = Http::withHeaders($headers)->timeout($this->config['timeout']);

        //上传文件附件
        if ($files) {
            $http = $http->attach($files['name'], $files['contents'], $files['filename'] ?? null);
        }

        //请求类型
        if ($content_type) {
            $http = $http->$content_type();
        }

        //请求方法
        $response = $http->$method($url, $params);
        $headers = $response->headers();

        //记录debug日志
        if ($debug) {
            Log::channel('qiyueuso')
            ->info($url.'---->',
             [
                 'params' => $params,
                 'response_json'=>$response->json(),
                 'response_status' => $response->status(),
             ]);
        }

        //响应json
        if (isset($headers['Content-Type'][0]) && strpos($headers['Content-Type'][0], 'application/json') !== false) {
            $result_arr = $response->json();
        } else {
            abort(422, '契约锁,接口响应数据类型不是json');
        }

        //dd(json_encode($params), $result_arr);
        if (! $response->successful()) {
            //转换成422http异常
            abort(422, $result_arr['message']);
        } else {
            //状态码不是成功的响应错误信息
            if (isset($result_arr['code']) && $result_arr['code'] != 0) {
                abort(422, $result_arr['message']);
            } else {
                return $result_arr['result'] ?? [];
            }
        }
    }

    /**
     * 下载文件
     *
     * @param string $method 请求方法 get
     * @param string $subUrl 接口路径
     * @param array $params 请求参数
     * @param mixed $save_path 保存路径
     * @param string $disk
     * @return void
     */
    public function downloadFile(string $subUrl, array $params, string $save_path, string $disk = 'public')
    {
        $url = $this->config['api_host'].$subUrl;
        $headers = $this->playloadHeader();
        $response = Http::withHeaders($headers)
            ->timeout($this->config['timeout'])
            ->send(
                'get',
                $url,
                [
                    'query' => $params,
                    'save_to' => $save_path,
                ]
            );

        if (isset($headers['Content-Type'][0]) && strpos($headers['Content-Type'][0], 'application/json') !== false) {
            $result_arr = $response->json();

            if (isset($result_arr['code']) && $result_arr['code'] != 0) {
                //响应错误信息
                abort(422, $result_arr['message']);
            }
        }

        if ($response->successful()) {
            if ($save_path) {
                Storage::disk($disk)->put($save_path, $response->body());

                return $save_path;
            } else {
                return $response->body();
            }
        }

        return false;
    }

    protected function playloadHeader()
    {
        $headers['x-qys-open-timestamp'] = $this->timestamp();
        $headers['x-qys-open-accesstoken'] = $this->config['app_token'];
        $headers['x-qys-open-nonce'] = $this->nonce();
        $headers['x-qys-open-signature'] = $this->signature(
            $this->config['app_token'],
            $this->config['app_secret'],
            $headers['x-qys-open-timestamp'],
            $headers['x-qys-open-nonce']
        );

        return $headers;
    }

    private function timestamp()
    {
        $timestamp = time() * 1000;

        return $timestamp;
    }

    private function nonce()
    {
        return (string) Str::uuid();
    }

    private function signature($app_token, $app_secret, $timestamp, $nonce)
    {
        return md5($app_token.$app_secret.$timestamp.$nonce);
    }
}

使用trait解耦业务方法,模板管理相关业务,可以在方法中进行相关业务处理

<?php
/**
 * 模板管理
 */

namespace App\Services\QiYueSuo;

trait Template
{
    /**
     * 创建word模板
     * DOC:https://open.qiyuesuo.com/document?id=2784729992477544720
     *
     * @param array $params 请求参数
     * @param array $files 合同文件
     * @return array
     */
    public function v3TemplateCreatebyword(array $params, array $files)
    {
        return $this->sendRequest('post', '/v3/template/createbyword', $params, 'asMultipart', $files);
    }

    /**
     * 模板列表
     * DOC:https://open.qiyuesuo.com/document?id=2657160660600767485
     *
     * @param int $page 	  查询起始位置,默认为0 第几页
     * @param int $per_page   查询列表大小,默认1000
     * @param string $tenant_name  	查询条件:子公司名称,若传递了则查询子公司下的模板
     * @param string $modify_time_start  数据变更(创建、变更)时间的起始时间,格式为yyyy-MM-dd HH:mm:ss,默认无限制
     * @param string $modify_time_end  数据变更(创建、更新)时间的结束时间,格式为yyyy-MM-dd HH:mm:ss,默认无限制
     * @return array
     */
    public function v2TemplateList(int $page = 1, int $per_page = 15, string $tenant_name = '', string $modify_time_start = '', string $modify_time_end = '')
    {
        return $this->sendRequest('get', '/v2/template/list', [
            'selectOffset' => $page,
            'selectLimit' => $per_page,
            'tenantName' => $tenant_name,
            'modify_time_start' => $modify_time_start,
            'modify_time_end' => $modify_time_end,
        ]);
    }

    /**
     * 模板详情接口
     * DOC:https://open.qiyuesuo.com/document?id=2657160735708155950
     *
     * @param int $templateId 模板id
     *
     * @return array
     */
    public function v2TemplateDetail(int $templateId)
    {
        return $this->sendRequest('get', '/v2/template/detail', ['templateId'=> $templateId]);
    }

    /**
     * 编辑模板
     * DOC:https://open.qiyuesuo.com/document?id=2784730224355451710
     *
     * @param int $templateId 模板id
     * @param array $params
     *
     * @return array
     */
    public function v2TemplateEdit(int $templateId, array $params)
    {
        return $this->sendRequest('post', '/v2/template/edit',
            array_merge(['templateId' => $templateId], $params),
        );
    }

    /**
     * 下载模板
     * DOC:https://open.qiyuesuo.com/document?id=2784730224355451710
     *
     * @param int $templateId 模板id
     *
     * @return array
     */
    public function v2TemplateDownload(int $templateId)
    {
        return $this->downloadFile('/v2/template/download', ['templateId' => $templateId], false);
    }

    /**
     * 删除模板
     * DOC:https://open.qiyuesuo.com/document?id=2786490624780538307
     *
     * @param int $templateId 模板id
     *
     * @return array
     */
    public function v2TemplateRemove(int $templateId)
    {
        return $this->sendRequest('get', '/v2/template/remove', ['templateId' => $templateId]);
    }
}

以单一模型为主逻辑或功能封装到model中模型中封装方法过多时,可以用trait进行拆分

商品扣库存封装到商品模型中,

添加实体有很多关联表必须依赖主表,将添加主表数据和关联数据的方法封装到主表对应的模型中,举例:添加商品时,要添加商品规格,商品属性等等,将添加完整数据的方法,封装到商品模型中

更新商品的销量统计,需要查询其他关联表,在商品模型封装一个刷新统计的方法