Laravel 服务提供者业务使用实例

laravel服务提供者使用实例

前言

之前在网上看到过很多「laravel容器」、「服务提供者」相关的文章,但大部分只是介绍框架内容器的组成和使用方式,貌似关于实际的业务应用很少,这样下来时间长了,可能仅仅看过源码过段时间就忘记了,正好最近公司的项目里正好要做一些功能的调整,所以就应用在业务中,加深自己理解的同时也深刻体会到框架带来的方便。

业务背景

项目里有一个视频转码的功能,登录会员可以上传任意视频至平台,之前使用的第三方服务是七牛,最近要改成阿里云,我们要做的就是利用容器来自定义调用第三方SDK。

一开始说做这个功能时,第一时间考虑是直接改业务代码。后来想万一哪天又要重新用七牛的怎么办,所以还是需要做成可配置的。事实证明这个决定是正确的,过了没两个月公司果然又用七牛的服务了,这个需求一提,老夫心中暗喜,幸亏提前做了准备,否则又要浪费大把时间来改代码了,就好比已经上路多年的老司机,突然要让你从科目一再重新考一遍驾照,你说费劲么?也不费劲,但就是需要去做重复工作。

开始动工吧

改动之前

在正式开始之前,我觉得有必要稍微花一点点时间看一下改动之前的调用方式,以便可以全面的了解一下,如果不感兴趣的可以直接跳过~


public function transcodeVideo(){
    //这里对参数内容进行了简化,关注流程就好:)
    $url = $this->url;  
    //调用七牛的转码服务
    return FileService::getInstance()->transcodeVideo($url,callback?mid=".$this->id);
}

//调用七牛的转码服务,一开始是考虑直接改这个方法,当然这么做也确实可以。
public function transcodeVideo($video,$callback_url){
    Log::info(__CLASS__.'transcodeVideo param:',compact('video','callback_url'));
    $auth = new Auth(env('QINIU_ACCESSKEY'), env('QINIU_SECRETKEY'));
    $c = new \Qiniu\Processing\PersistentFop($auth);  
    $ret = $c->execute(env('QINIU_BUCKET'),$video,'avthumb/mp4/vcodec/libx264/s/720x720/autoscale/1|saveas/'.\Qiniu\base64_urlSafeEncode(env('QINIU_BUCKET').':'.md5($video).'.mp4'),'video1',$callback_url,true);
    Log::info(__CLASS__.'.transcodeVideo ret:',$ret);
    if(is_array($ret) && count($ret) > 0) {
        return true;
    }else{
        return false;
    }
}

其实有一个问题就是,因为项目肯定不止一个人在用到这个功能,所以调用方也是五花八门的,不全局搜索一下,你都不知道有多少个人在使用这个功能,所以我们做一个统一入口是很有必要的。

改动之后

首先我们注册一个视频转码的服务提供者,具体怎么创建大家可以参考官方文档,有很详细的介绍,这里就不再多说了。

public function register()
{
    //如果要换其他的服务方,直接把匿名函数中的类改一下就好了
    $this->app->singleton(VideoTranscodeService::class, function($app){
        return new \Aliyun\VideoTranscodeService();
    });
}

那么我们看到了,如果要实现这个服务,我们需要构建的基础代码结构:

1.定义抽象父类: VideoTranscodeService

2.定义七牛实现: Qiniu\VideoTranscodeService

3.定义阿里云实现: Aliyun\VideoTranscodeService

下面附上实现代码,有相关业务需求的同学也可以参考一下。

抽象父类:

abstract class VideoTranscodeService{

    protected $sourceUrl = '';
    protected $extra = [];
    protected $scene = '';

    /**
     * 提交转码作业
     * @param string $scene         场景
     * @param string $sourceUrl     源文件地址
     * @param array $extra          额外数据
     * @return mixed
     * @throws BusinessException
     */
    public function videoTranscode($scene, $sourceUrl = '', $extra = [])
    {
        Log::info(__CLASS__.'.videoTranscode param:', compact('scene', 'sourceUrl','extra'));

        if(!$scene) {
            Log::warning(__CLASS__.'.videoTranscode loss scene');
            throw new BusinessException('缺少场景',Error::INVALID_PARAM);
        }

        if(!$sourceUrl) {
            Log::warning(__CLASS__.'.videoTranscode loss sourceUrl');
            throw new BusinessException('缺少源文件地址',Error::INVALID_PARAM);
        }

        $this->scene = $scene;
        $this->sourceUrl = $sourceUrl;
        $this->extra = $extra;

        return call_user_func([$this, $this->scene]);
    }

    /**
     * @param $func
     * @param $arg
     * @throws BusinessException
     */
    public function __call($func = '', $arg = []){
        Log::warning(__CLASS__ . $func . ' function not Exists');
        throw new BusinessException('访问不存在的方法', Error::INVALID_PARAM);
    }

    /**
     * 根据源文件地址,生成转码后地址
     * @param string $sourceUrl
     * @return mixed
     */
    abstract public function makeCompressUrl($sourceUrl);

    /**
     * 根据视频地址,生成封面图地址
     * @param $videoUrl
     * @return mixed
     */
    abstract public function makeCoverImg($videoUrl);

}

七牛转码服务:

class VideoTranscodeService extends  TranscodeService{

    /**
     * 视频转码
     * @param string $sourceUrl
     * @param array $extra
     * @return bool
     * @throws BusinessException
     */
    protected function interaction(){
        Log::info(__CLASS__.'.interaction param:', [$this->sourceUrl, $this->extra]);

        $callbackUrl = $this->makeCallbackUrl($this->extra['id']);

        return $this->submitTranscodeJob($callbackUrl);
    }

    /**
     * 提交转码作业
     * @param $video
     * @param $callbackUrl
     * @return bool
     */
    protected function submitTranscodeJob($callbackUrl = ''){
        //源文件
        $video = basename($this->sourceUrl);

        //认证
        $auth = new Auth(env('QINIU_ACCESSKEY'), env('QINIU_SECRETKEY'));
        $c = new \Qiniu\Processing\PersistentFop($auth);

        //输出(转码)地址
        $output = VideoTranscodeConf::$videoPath[$this->scene]['output'].md5($video).'.mp4';
        $saveAs = \Qiniu\base64_urlSafeEncode(env('QINIU_BUCKET').':'.$output);
        $fops = 'avthumb/mp4/vcodec/libx264/s/720x720/autoscale/1|saveas/'. $saveAs;

        //提交转码作业
        $ret = $c->execute(env('QINIU_BUCKET'),$video,$fops,'video1',$callbackUrl,true);
        Log::info(__CLASS__.'.submitTranscodeJob ret:',$ret);

        if (is_array($ret) && count($ret) > 0) {
            return true;
        }else{
            return false;
        }
    }

    //生成七牛回调地址
    protected function makeCallbackUrl($extra){
        $data = $this->scene.'#'.$extra;
        return route('qiniuCallback',['data' => $data]);
    }

    /**
     * 根据源文件地址,生成转码后地址
     * @param string $sourceUrl
     * @return mixed|string
     * @throws BusinessException
     */
    public function makeCompressUrl($sourceUrl = ''){
        if(!$sourceUrl) {
            Log::warning(__CLASS__.'.makeCompressUrl sourceName Not Exists');
            throw new BusinessException('源文件地址错误',Error::INVALID_PARAM);
        }
        return env('QINIU_HOST')."/".md5(basename($sourceUrl)).".mp4";
    }

    /**
     * 根据视频地址,生成封面图地址
     * @param $videoUrl
     * @return string
     */
    public function makeCoverImg($videoUrl){
        return $videoUrl.'?vframe/jpg/offset/0';
    }
}

阿里云转码服务:

class VideoTranscodeService extends TranscodeService{

    /**
     *
     * VideoTranscodeService constructor.
     * @throws ClientException
     */
    public function __construct(){
        AlibabaCloud::accessKeyClient(AliyunConf::VIDEO_ACCESS_KEY, AliyunConf::VIDEO_ACCESS_KEY_SECRET)
            ->regionId(env('ALIYUN_VIDEO_REGION'))
            ->asGlobalClient();
    }

    /**
     * 视频转码
     * @param string $sourceUrl         输入视频地址 
     * @param array $extra              额外配置参数: id、 width(视频宽度)
     * @return bool                     请求结果,仅代表请求是否成功,不代表最终转码结果
     * @throws BusinessException
     * @throws ClientException
     * @throws ServerException
     */
    protected function interaction(){
        Log::info(__CLASS__.'.interaction param:', [$this->sourceUrl,$this->extra]);

        $ossLocation = env('ALIYUN_VIDEO_BUCKET_LOCATION');
        $ossBucket = env('ALIYUN_VIDEO_BUCKET_NAME');
        $userData = 'interaction#'.$this->extra['id'];
        $templateId = $this->getTemplate($this->extra['width']);

        $input = [
            'Location' => $ossLocation,
            'Bucket' => $ossBucket,
            'Object' => urlencode($this->makeInputObject())
        ];

        $outputs = [[
            'OutputObject' => urlencode($this->makeOutputObject()),
            'Container' => ['Format' => AliyunConf::VIDEO_TRANSCODE_FORMAT],
            'TemplateId' => $templateId,
            'UserData' => $userData
        ]];

        return $this->submitTranscodeJob($input, $outputs, $ossLocation, $ossBucket);
    }

    /**
     * 提交转码作业
     * @param $input
     * @param $outputs
     * @param string $ossLocation
     * @param string $ossBucket
     * @return bool
     * @throws ClientException
     * @throws ServerException
     */
    protected function submitTranscodeJob($input, $outputs, $ossLocation = '', $ossBucket = ''){
        $result = AlibabaCloud::mts()
            ->v20140618()
            ->submitJobs()
            ->setAcceptFormat('JSON')
            ->withInput(json_encode($input))
            ->withOutputs(json_encode($outputs))
            ->withOutputBucket($ossBucket)
            ->withOutputLocation($ossLocation)
            ->withPipelineId(env('ALIYUN_VIDEO_TRANSCODE_PIPELINE'))
            ->request();

        return $result->isSuccess();
    }

    /**
     * 根据源文件名称,生成转码后地址(主要用作于类外直接调用)
     * @param string $sourceUrl     转码前源文件名称  
     * @return string               转码后URL地址    
     * @throws BusinessException
     */
    public function makeCompressUrl($sourceUrl = ''){
        if(!$sourceUrl) {
            Log::warning(__CLASS__.'.makeCompressUrl sourceName Not Exists');
            throw new BusinessException('源文件地址错误',Error::INVALID_PARAM);
        }
        $inputFile = basename($sourceUrl);
        $outputObject = $this->makeOutputObject($inputFile);
        return env('ALIYUN_VIDEO_BUCKET_URL').$outputObject;
    }

    /**
     * 根据视频地址,生成封面图地址
     * @param $videoUrl
     * @return string
     */
    public function makeCoverImg($videoUrl){
        return $videoUrl.'?x-oss-process=video/snapshot,t_0,w_0,h_0,m_fast';
    }

    /**
     * 生成输出文件bucket地址
     * @param $inputFile string 输入文件 -用于类外部直接调用makeCompressUrl时生成的路径
     * @return string
     * @throws BusinessException
     */
    protected function makeOutputObject($inputFile = ''){
        if (isset(VideoTranscodeConf::$videoPath[$this->scene]['output'])) {
            if (!$inputFile) {
                $inputFile = basename($this->sourceUrl);
            }

            return VideoTranscodeConf::$videoPath[$this->scene]['output'].md5($inputFile).'.'. AliyunConf::VIDEO_TRANSCODE_FORMAT;
        }else{
            Log::error(__CLASS__.'.makeOutputObject outputpath not config');
            throw new BusinessException('缺少输出配置文件', Error::INVALID_PARAM);
        }
    }

    /**
     * 生成输入文件bucket地址
     * @return string
     * @throws BusinessException
     */
    protected function makeInputObject(){
        if (isset(VideoTranscodeConf::$videoPath[$this->scene]['input'])) {
            return VideoTranscodeConf::$videoPath[$this->scene]['input'].basename($this->sourceUrl);
        } else {
            Log::error(__CLASS__.'.makeInputObject inputpath not config');
            throw new BusinessException('缺少输入配置文件', Error::INVALID_PARAM);
        }
    }

    /**
     * 根据宽度定义转码模板
     * @param int $width
     * @return string
     */
    protected function getTemplate($width = 0){
        if($width > 1280) {
            $templateId = AliyunConf::VIDEO_TRANSCODE_HD;   //高清
        }else{
            $templateId = AliyunConf::VIDEO_TRANSCODE_SD;   //标清
        }
        return $templateId;
    }
}

这样我们的服务就写好了,在业务代码中只需要调用下面代码即可完成视频的转码,而不需要关注第三方的服务到底是谁。(代码中隐藏了部分参数配置信息,但不影响整体流程)

app(VideoTranscodeService::class)->videoTranscode('interaction', $this->url, $extra);

结尾

至此我们的视频转码服务就已经完成,即便是后期再增加其他的第三方SDK,也仅仅是增加一个实现就可以了,业务代码完全不需要改动。目前来看效果还不错,其实掌握这些框架或者模式相关的东西,不仅是要充实自己的理论知识,同时也是在面对需求调整时可以尽量小的改动代码来完成功能的实现,减少代码的耦合,减轻日后维护的困扰,所以我们在学习过后还是尽可能的用起来,不要仅仅停留在看过xxx,听说过xxx。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 2
wenber

可根据配置自动注册对应的服务提供者.这样免去了切换还要版本更新的麻烦.

4年前 评论

将理论走向实践 点赞

4年前 评论

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