Laravel-admin是一款开箱即用的后台管理框架,功能强大。但是由于其依赖的底层框架技术(Laravel、AdminLTE、Datetimepicker等)众多,自身的文档内容也较简略,导致实现某些自定义功能的时候难度较大。本文就针对其中的存储checkbox数据问题提出一个解决方案,并详细描述由发现问题到解决问题的过程,希望给遇到这类问题的同学们一些启发。
问题原因
当在新建页面使用checkbox组件,勾选后提交,传递给后端的数据将被转成一个数组的形式。但是在存储数据库的时候,构建sql时就会出现将array转为string的情况(不可能直接将php中的array直接存入数据库),自然就报出了这个错误。
解决方案
考虑到有些同学可能急于解决这个问题,所以先基于一个例子抛出解决方案:
例如你正在存储一篇文章(model名为blog
)中的tags
,你想将所有选中的tag用逗号拼接起来存入数据库中的tags
字段。这时候前端就可以用checkbox
。提交表单时前端传递上来字段名是tags
(类似于发表文章是可以勾选多个tag),正常情况下这个tags会被转成一个Array,直接存的话就会报本文的错误。那么你需要在对应的Model中新建一个setTagsAttribute:
public function setTagsAttribute($tags) {
$this->attributes['tags'] = trim(implode($tags, ','), ',');
}
这个方法的作用在于存储model时,会将原有提交表单中的$tags拼接成字符串,设置到model的属性中,即可完成存储。
以上就是解决方案,但本文的重点在于解决这个问题的过程,所以我建议同学继续阅读下面的内容。
解决过程
解决问题的思路一直很清晰:根据路由规则找到表单提交的controller,然后将在存入数据库之前,将数组拼接成字符串。
Xdebug是解决该问题必备的工具,稍后我将开设专栏详细讲解Xdebug相关的东西。
确定路由
laravel的路由一般是通过Route::get/post的形式注册到容器中的。但是如果你和我一样,使用laravel-admin的时候,是用artisan来生成各种controller的话,那么必然没有自己去注册路由。
对于laravel老手,应该可以很轻易的找到注册路由的地方。但是我是laravel新手,所以我一开始并没有找到。这是因为laravel的官方文档中,http kernal的路由注册时在routes/web.php中进行的。当我打开这个文件时,并没有找到相关代码,而是最基础的一个欢迎页面的路由注册:
Route::get('/', function () {
return view('welcome');
});
所以我开始用xdebug一步一步分析,确定路由注册的地方。首先我查阅了laravel的生命周期的相关资料[^1]。确定了容器中的路由规则是存储在Illuminate\Routing\RouteCollection
类的allRoutes
属性中。但是还是无法定位到注册这些路由的地方。
这里困扰了我好久,最终我想到了一个办法:RouteCollection类中的allRoutes必然是通过add
或者addToCollections
方法添加进来的,那么只要我打断点在这两个方法里面,就可以抓到添加路由的时候,然后再通过查看调用栈,就知道到底是在哪里注册的路由了。如下图所示:
最终通过这个方法,我定位到了路由注册竟然是在眼皮底下:app/admin/routes.php
,简直无语…
进入控制器
通过var_dump出来allRoutes,可以找到对应的控制器。此时,我终于进了Controller
的store
方法。注意这里的store方法是来自于HasResourceActions
这个traits,如果你不知道traits的话,可以看看我之前的这篇文章[^2]。
控制器的代码很简单:
public function store()
{
return $this->form()->store();
}
简单理解,就是先构建了一个Form,然后调用store方法存入数据库。那么很显然,我们只需要在store方法中做工作就可以了。二话不说,进入store方法:
// 获取输入
$data = Input::all();
// 校验参数.
if ($validationMessages = $this->validationMessages($data)) {
return back()->withInput()->withErrors($validationMessages);
}
// 这里应该也是一种直接返回的情况,没有操作db,不深究
if (($response = $this->prepare($data)) instanceof Response) {
return $response;
}
// 很显然,是在这里存数据库的
DB::transaction(function () {
// 准备插入数据
$inserts = $this->prepareInsert($this->updates);
// 将数据set到model的属性中
foreach ($inserts as $column => $value) {
$this->model->setAttribute($column, $value);
}
// 存储model
$this->model->save();
// 后面就不管了
$this->updateRelation($this->relations);
});
if (($response = $this->callSaved()) instanceof Response) {
return $response;
}
if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
return $response;
}
return $this->redirectAfterStore();
在这个方法里,首先获取了输入,然后校验参数,然后有个准备数据的过程,再将数据set到model的属性中,最后调用model的save方法完成数据的存入。
一开始的时候,我想在数据prepare的时候,将数组转化成字符串。所以我进入了prepareInsert方法:
if ($this->isHasOneRelation($inserts)) {
$inserts = array_dot($inserts);
}
foreach ($inserts as $column => $value) {
if (is_null($field = $this->getFieldByColumn($column))) {
unset($inserts[$column]);
continue;
}
// 这里是最关键的处理
$inserts[$column] = $field->prepare($value);
}
$prepared = [];
foreach ($inserts as $key => $value) {
array_set($prepared, $key, $value);
}
return $prepared;
可以看到我的注释,在这个方法里,调用了每个field的prepare方法。所以我想到的就是改写checkbox的prepare方法。注意:Form类中的每个生成前端组件的方法都对应一个Field下的一个组件,具体对应在Form的注释中有:
/**
* Class Form.
*
* @method Field\Text text($column, $label = '')
* @method Field\Checkbox checkbox($column, $label = '')
* @method Field\Radio radio($column, $label = '')
* ...
* /
所以我就在checkbox类中重写了父类的prepare方法:
public function prepare($value)
{
$value = parent::prepare($value);
return trim(implode($value, ','), ',');
}
这样确实达到了效果。
但是,并没有结束,这样做,岂不是后续所有的checkbox都会转成字符串的形式?如果有别的表中的字段使用checkbox,我想使用json数组呢?所以这并不是一种好办法。所以我继续寻找…
方案终章
注意看刚刚的这一段:
foreach ($inserts as $column => $value) {
$this->model->setAttribute($column, $value);
}
$this->model->save();
这里调用了先调用了model的setAttribute方法将数据设置为model的属性,然后save。那么我们是否可以针对这个单独的model来在这里处理呢?这样的好处在于不会影响别的model。进入setAttribute方法:
// First we will check for the presence of a mutator for the set operation
// which simply lets the developers tweak the attribute as it is set on
// the model, such as "json_encoding" an listing of data for storage.
if ($this->hasSetMutator($key)) {
return $this->setMutatedAttributeValue($key, $value);
}
...
英文好的同学看注释应该可以看到,这个方法的第一步,就是判断这个属性有没有单独的设置方法(hasSetMutator),这不正好满足了我们的需求么?进入hasSetMutator方法:
return method_exists($this, 'set'.Str::studly($key).'Attribute');
发现,这里使用了一个字符串拼接的方式来确定是否要调用该属性的设置方法。也就是说,如果在model中定义了某个属性的设置方法setXxxAttribute
,就会调用这个方法来赋这个属性的值(调用setMutatedAttributeValue方法):
return $this->{'set'.Str::studly($key).'Attribute'}($value);
到这里,就得到了一开始给出的解决方案。基本也就完结了。
通过这长达2-3个小时的解决过程,收获还是颇丰的。希望也能帮到你