分类 Laravel-admin 下的文章

我的个人博客:逐步前行STEP

有时候我们使用Laravel-admin管理数据时,需要保存一些通过程序运算出来的数据,而不只是存储写在表单中的数据,也就是需要在保存数据前可以设置或改变数据。
比如存在这么个需求:

为了快速创建\管理一些测试数据,在Admin管理端创建一个用户账户的同时,创建相关的用户信息,用户订单、地址库等等一系列信息。

以这个需求中用户关联的数据量来说,每个数据都手动输入的话,效率太低了,所以只能自动创建。
所以实现的思路是这样的,表单中只有用户的基本信息,其它的订单、地址都是在执行store函数前自动生成的,然后关联保存即可。
先看Form.php的store函数源码:

public function store()
    {
        $data = Input::all();
        ......
    }

数据来源是request对象,所以,轻而易举地想到:

在store之前往request中塞入订单、地址的关联数据

这个数据的格式在我的另一篇博文中有详细解释:laravel-admin grid中使用switch操作一对一关联属性,但是执行提交后发现并没有成功保存,于是走一波源码调试。
首先看到更新关联模型数据的代码:

 public function store()
    {
......

        DB::transaction(function () {
            $inserts = $this->prepareInsert($this->updates);

            foreach ($inserts as $column => $value) {
                $this->model->setAttribute($column, $value);
            }
            $this->model->save();

            //在这里保更新关联模型
            $this->updateRelation($this->relations);
        });
......
    }

继续进入updateRelation方法跟踪:

protected function updateRelation($relationsData)
    {
        foreach ($relationsData as $name => $values) {

            if (!method_exists($this->model, $name)) {
                continue;
            }
            $relation = $this->model->$name();

            //在这里判定是否是一对一
            $oneToOneRelation = $relation instanceof Relations\HasOne
                || $relation instanceof Relations\MorphOne
                || $relation instanceof Relations\BelongsTo;

            //在这里做一个预处理
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);
            //预处理的结果为空则没有后续处理
            if (empty($prepared)) {
                continue;
            }

经过打断点调试,发现我在store之前插入的订单、地址关联数据并没有通过预处理,所以再来看看prepareUpdate预处理是什么鬼:

protected function prepareUpdate(array $updates, $oneToOneRelation = false)
    {
        $prepared = [];

        /** @var Field $field */
        //$this->builder->fields() 就是表单字段相关属性
        foreach ($this->builder->fields() as $field) {
            $columns = $field->column();//这个是字段名称了

            // If column not in input array data, then continue.
            if (!array_has($updates, $columns)) {//关键在这,如果表单字段不在request的数据中就过滤掉
                continue;
            }

      ......

    }

在上面的注释中,清楚地表明了一个状况:

form表单的提交保存只能保存表单中有的字段,不然都会被过滤掉。

所以,我们需要让Laravel-admin认为我们表单中有订单、地址关联字段,根据我们的需求,很容易想到使用hidden组件:

只要把所有关联关系需要更新的字段都使用hidden列出来即可,不需要赋值

而且在store前,如果根据某些条件不需要保存这个关联关系的话,直接使用request的offsetUnset将那个关联关系整体删除即可。

我的个人博客:逐步前行STEP

由于Laravel-admin只支持表单保存一对一关联数据,要想保存一对多关联数据,还得从了解它的源码入手,看有没有空子可钻。
首先,进入源码中的Form.php的store函数中看它是怎么保存表单数据的:

    public function store()
    {

    //为了节省篇幅,就省略无关代码吧
......

        DB::transaction(function () {
        //这里是保存表单中当前模型的数据
            $inserts = $this->prepareInsert($this->updates);

            foreach ($inserts as $column => $value) {
                $this->model->setAttribute($column, $value);
            }
            $this->model->save();
        //这里是保存表单中关联模型数据
            $this->updateRelation($this->relations);
        });
 ......
    }

然后进入到保存关联模型数据的updateRelation函数中:

 protected function updateRelation($relationsData)
    {
        foreach ($relationsData as $name => $values) {

            if (!method_exists($this->model, $name)) {
                continue;
            }
            $relation = $this->model->$name();

            //在这里判断了关联关系是否是一对一关联、一对一多态关联,不是的话就无法进行后续处理了
            $oneToOneRelation = $relation instanceof Relations\HasOne
                || $relation instanceof Relations\MorphOne
                || $relation instanceof Relations\BelongsTo;

            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);

            if (empty($prepared)) {
                continue;
            }
......
}

额,所以必须使用一对一关联关系来保存数据,即将本来是一对多的关系写一个一对一的关联关系用户保存数据,比如:user模型中又一个friends一对多关联模型,但是为了在表单中保存一个friends数据,创建一个friend一对一关联关系在表单中用户保存关联数据。

我的个人博客:逐步前行STEP

项目中有一个商品表(production) ,有一个库存表(repertory),两者一对一关系,production有发布字段(release),需求是在repertory的grid中,有一个switch开关,用于发布production。
如果直接使用

$grid->column('production.release')->switch($states);

该列只会显示一个复选框,样式崩了。
经过调试,发现:
vendor/encore/laravel-admin/src/Grid/Displayers/SwitchDisplay.php中的


    public function display($states = [])
    {
        $this->updateStates($states);

        $name = $this->column->getName();
        
        $class = "grid-switch-{$name}";
        //这里直接拼接的类名中会有dot导致样式失效
        
        .........
    }

所以将列名改成release:

$grip->column('release')->switch($states);

然后在repertory模型中设置该值:

    protected $appends = ['release'];
    ......
    public function getReleaseAttribute()
    {
        ......
        return $release;
    }

此时grid中的release开关能正确显示,但是无法正确执行操作,因为repertory模型中没有release字段,需要在更新的时候将release改成关联production的release:。

查看vendor/encore/laravel-admin/src/Form.php中的update函数中获取数据的代码如下:

$data = ($data) ?: Input::all();

说明update函数直接使用request的数据,那么可以重写update函数,修改request的数据之后再传入:

    
  public function update()
    {
        if(!is_null(request()->get('release'))){
             $status = request()->get('release')==='on'?1:2;
             //获取到switch开关传的值了,需要将值设置到一个新的数据上
        }
        $data = request()->all();
        $id_arr = request()->route()->parameters();
        return $this->form()->update($id_arr['repertory'],$data);
    }

要设置一个新的数据作为关联关系去更新,应该有什么样的格式呢?带着这个疑问,先来走一下update的流程:

public function update($id, $data = null)
    {
        //在这里获取到了数据
        $data = ($data) ?: Input::all();
        //是否可编辑
        $isEditable = $this->isEditable($data);
        //处理可编辑的数据
        $data = $this->handleEditable($data);
        //这里不太清楚,大概是处理文件删除,没验证过
        $data = $this->handleFileDelete($data);
        //如果是排序操作的话,执行到这,获取排序后的数据就结束了
        if ($this->handleOrderable($id, $data)) {
            return response([
                'status'  => true,
                'message' => trans('admin.update_succeeded'),
            ]);
        }


        /* @var Model $this->model */
        //获取当前模型
        $this->model = $this->model->with($this->getRelations())->findOrFail($id);
        //设置字段原始值
        $this->setFieldOriginalValue();

        // Handle validation errors.
        //处理表单验证
        if ($validationMessages = $this->validationMessages($data)) {
            if (!$isEditable) {
                return back()->withInput()->withErrors($validationMessages);
            } else {
                return response()->json(['errors' => array_dot($validationMessages->getMessages())], 422);
            }
        }
        //预处理,得到$this->updates和$this->relations
        if (($response = $this->prepare($data)) instanceof Response) {
            return $response;
        }


        DB::transaction(function () {
            //预更新,更新该模型字段
            $updates = $this->prepareUpdate($this->updates);

            foreach ($updates as $column => $value) {
                /* @var Model $this->model */
                $this->model->setAttribute($column, $value);
            }
            
            $this->model->save();
            //更新关联模型字段
            $this->updateRelation($this->relations);
        });

        if (($result = $this->complete($this->saved)) instanceof Response) {
            return $result;
        }

        if ($response = $this->ajaxResponse(trans('admin.update_succeeded'))) {
            return $response;
        }

        return $this->redirectAfterUpdate();
    }

进入$this->prepare($data):

    /**
     * Prepare input data for insert or update.
     *
     * @param array $data
     *
     * @return mixed
     */
    protected function prepare($data = [])
    {

        if (($response = $this->callSubmitted()) instanceof Response) {
            return $response;
        }

        $this->inputs = $this->removeIgnoredFields($data);

        if (($response = $this->callSaving()) instanceof Response) {
            return $response;
        }
        //在这里得到关联关系
        $this->relations = $this->getRelationInputs($this->inputs);
        //在这里得到去除关联关系的本模型数据
        $this->updates = array_except($this->inputs, array_keys($this->relations));
    }

进入$this->getRelationInputs:

    /**
     * Get inputs for relations.
     *
     * @param array $inputs
     *
     * @return array
     */
    protected function getRelationInputs($inputs = [])
    {
        $relations = [];
        
        foreach ($inputs as $column => $value) {
            //判断函数是否存在
            if (method_exists($this->model, $column)) {
                //说明正确的关联关系数据是一个数组并且key为关联关系函数名
                $relation = call_user_func([$this->model, $column]);

                if ($relation instanceof Relation) {
                    //在此设置关联关系的字段和值,
                    $relations[$column] = $value;
                }
            }
        }

        return $relations;
    }

在上一个步骤找出了关联关系数据的大略格式:以关联函数为键的数组。
再回到update函数,进入$this->updateRelation($this->relations):

protected function updateRelation($relationsData)
    {
        foreach ($relationsData as $name => $values) {
            ;
            if (!method_exists($this->model, $name)) {
                continue;
            }

            $relation = $this->model->$name();

            $oneToOneRelation = $relation instanceof \Illuminate\Database\Eloquent\Relations\HasOne
                || $relation instanceof \Illuminate\Database\Eloquent\Relations\MorphOne;
//            echo "start\r\n";
            //上面只检测了$name,如果关联关系数据的键为关联函数名的话,可以顺利到这一步,而且因为当前要操作的数据的关联关系是1对1,所有$oneToOneRelation为true
            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);

            if (empty($prepared)) {
                continue;
            }

            switch (get_class($relation)) {
                ..........................
            }
        ................
        }

进入:$prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);:


    protected function prepareUpdate(array $updates, $oneToOneRelation = false)
    {
        $prepared = [];
//        print_r($updates);
        foreach ($this->builder->fields() as $field) {
            $columns = $field->column();
            //获取该模型对应的form的所有列名,我们关注的release在form中应为repertory.release
            
            // If column not in input array data, then continue.
            //这个函数很重要,决定了流程是否可以往下走,进去看一看
            if (!array_has($updates, $columns)) {
                continue;
            }
            ........
    }

进入:array_has($updates, $columns):

/**
     * Check if an item or items exist in an array using "dot" notation.
     *
     * @param  \ArrayAccess|array  $array
     * @param  string|array  $keys
     * @return bool
     */
    function array_has($array, $keys)
    {
        return Arr::has($array, $keys);
    }

看注释!!!,该函数用于检查第二个参数根据dot处理成数组后,是否存在于第一个参数的数组中,由于在form中该字段是repertory.release,即$columns等于repertory.release,要使程序往下运行,则$updates必为['repertory'=>['release'=>?]],到此,可以确定关联关系数据必是

['repertory'=>['release'=>1]]

于是,在控制器中重写的update中重写一个关联关系数据如下:


    public function update()
    {
        $release = request()->get('release');
        if(!is_null($release)){
            $status = $release === 'on' ? 1 : 2;
            //添加一个request字段
            request()->offsetSet('repertory',['release'=>$release]);
        }
        $data = request()->all();
        $id_arr = request()->route()->parameters();
        return $this->form()->update($id_arr['repertory'],$data);
    }

这样就修改了request提交的数据,现在request中有一个数组:

['repertory'=>['release'=>1]]

去除调试打印,在表格中操作开关,结果正确。

我的个人博客:逐步前行STEP

为了添加自定义按钮,按官方文档分4步走:
1、先定义工具类app/Admin/Extensions/Tools/ShowArtwork.php:

<?php

namespace App\Admin\Extensions\Tools;

use Encore\Admin\Admin;
use Encore\Admin\Grid\Tools\AbstractTool;
use Illuminate\Support\Facades\Request;

class ShowArtwork extends AbstractTool
{
    protected  $url;
    protected  $icon;
    function __construct($url,$icon,$text)
    {
        $this->url = $url;
        $this->icon = $icon;
        $this->text = $text;
    }

    public function render()
    {
        $url = $this->url;
        $icon = $this->icon;
        $text = $this->text;
        return view('admin.tools.button', compact('url','icon','text'));
    }
}

2、定义试图文件:resources/views/admin/tools/button.blade.php

<div class="btn">
    <a class="btn btn-sm btn-default  pull-right" href="{{$url}}"><i class="fa {{$icon}}"></i> {{$text}}</a>
</div>

3、在model-grid引入这个工具:


            $grid->tools(function ($tools)use($artworkid) {
                $url = "/admin/artimage";
                $icon = "fa-backward";
                $text = "Back";
                $tools->append(new ShowArtwork($url,$icon,$text));

                $url = "/admin/artimage/".$artworkid."/view";
                $icon = "fa-eye";
                $text = "Show Artwork";
                $tools->append(new ShowArtwork($url,$icon,$text));
            });

大功告成!

相册与照片是一对多的关系,有以下需求:
1、点开一条相册数据看到相册的照片列表
2、为相册添加照片时,表单中要看到相册的基本信息

以下是实现步骤:
第一步:构建带参数路由:

$router->resource('manage/{albumid}/photo',Manage\PhotoController::class);

第二步:处理操作对应的函数:
(1)列表查看

public function index($albumid)
    {
        return Admin::content(function (Content $content)use($albumid) {

            $content->header('header');
            $content->description('description');

            $content->body($this->grid($albumid));
        });
    }

    protected function grid($albumid)
    {
        return Admin::grid(Photos::class, function (Grid $grid)use($albumid) {

            $grid->model()->where('album_id',$albumid);
            ......
            }
    }

(2)创建照片,并在创建表单上显示所属相册的基本属性

protected function form()
    {
        return Admin::form(Photos::class, function (Form $form){
            $paramters = request()->route()->parameters();

            $artworkid  = isset($paramters['albumid'])?$paramters['albumid']:request()->input('albumid');

            $album= ArtImage::find($albumid);
            ......
        }
    }

(3)更新照片

    //重写update,多接收一个参数
    public function update($album,$id)
    {
        return $this->form()->update($id);
    }

(4)删除照片

    //重写destroy,多接收一个参数
    public function destroy($albumid,$id)
    {
        if ($this->form()->destroy($id)) {
            return response()->json([
                'status'  => true,
                'message' => trans('delete_succeeded'),
            ]);
        } else {
            return response()->json([
                'status'  => false,
                'message' => trans('delete_failed'),
            ]);
        }
    }