空字符串替代null值在Eloquent中的使用

31

我想使用批量赋值的 Eloquent 功能创建实体...

$new = new Contact(Input::all());
$new->save();
问题在于,这种方法会用空字符串填充每个字段,而不是我所期望的null值。
我正在开发系统,有些表列仍未定义,因此使用这种方法来避免将每个新字段添加到$fillable数组和new Contact(array(...));中。
此外,这张表中大约有20个字段,如果要使用这样的数组会有点丑陋。
$new = new Contact(array(
    'salutation' => Input::get('salutation'),
    'first_name' => Input::get('first_name'),
    'last_name'  => Input::get('last_name'),
    'company_id' => Input::get('company_id'),
    'city' => ...
    ...
));

有什么方法可以做到这一点或解决这个问题吗?

更新 到现在为止,我已经通过在App :: before()过滤器中使用array_filter来解决了这个问题。

更新 在过滤器中有点混乱。最终我做了这些:

public static function allEmptyIdsToNull()
{
    $input = Input::all();

    $result = preg_grep_keys ( '/_id$/' , $input );

    $nulledResults = array_map(function($item) {
        if (empty($item))
            return null;

        return $item;
    }, $result);

    return array_merge($input, $nulledResults);
}

而且在我的functions.php文件中。

if ( ! function_exists('preg_grep_keys'))
{
    /**
    * This function gets does the same as preg_grep but applies the regex
    * to the array keys instead to the array values as this last does.
    * Returns an array containing only the keys that match the exp.
    * 
    * @author Daniel Klein
    * 
    * @param  string  $pattern
    * @param  array  $input
    * @param  integer $flags
    * @return array
    */
    function preg_grep_keys($pattern, array $input, $flags = 0) {
        return array_intersect_key($input, array_flip(preg_grep($pattern, array_keys($input), $flags)));
    }
}

目前只使用以"_id"结尾的字段。这是我的最大问题,因为如果关系不为NULL,数据库将抛出错误,因为找不到外键""。

完美运作。有任何评论吗?


把这个放在一行里... Contact::create(Input::all()) 赢了。 - Rob W
我用这种方式不是得到了空值吗?我认为它做的事情和 $new = new Contact(Input::all()); 是一样的。 - Israel Ortuño
7个回答

64

我自己也在寻找答案,最接近的解决方法是使用 Mutators (http://laravel.com/docs/eloquent#accessors-and-mutators)。

在模型中添加一个(神奇的!)Mutator方法来解决外键字段的相同问题:

public function setHeaderImageIdAttribute($value)
{
    $this->attributes['header_image_id'] = $value ?: null;
}

对于有许多外键的表格,这可能会变得笨重,但这是我发现处理此类情况最为“内建”的方法。好处是它是自动完成的,所以你只需要创建该方法就可以轻松使用。

更新--Laravel 5.4及以上版本

从 Laravel 5.4 开始,\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class 中间件在接收到请求时处理此问题。在上面的例子中,如果请求包含一个空字符串值的 'header_image_id',这个中间件会将其自动转换为 null ,然后再分配给我的模型。


10
这是正确的 Laravel 解决方案。这就是为什么 Eloquent 存在访问器和修改器的原因。与已接受的答案一样,它还可以更精确地控制设置为 null 的内容,而不是一概而论地“将所有内容都设置为 null”。 - Jake Wilson
5
对于这个回答点赞。这绝对是分配 null 键的正确方式,可以更好地控制每个列。如果您将来需要更改列的默认值或类似内容,这也会非常有益。 - Steve Bauman
1
非常好的回答,这让我花了很长时间才理解。提醒一下,要记得用ID来命名mutator -- 也就是说,它不是在突变关系函数,而是在突变数据库字段。我犯了一个错误,在函数后面叫错了函数"setHeaderImageAttribute"而不是像上面例子中的"setHeaderImageIdAttribute"。然后Laravel就错过了捕捉到mutator的机会。事后看来显而易见,但是找了很久才发现。 - Watercayman
1
+1 我本来想发表另一种答案,但看到了这个答案。在我看来,这比被接受的答案要好得多。 - Thomas F.
1
至少Laravel 5.7的新网址 https://laravel.com/docs/5.7/eloquent-mutators#accessors-and-mutators - Xedecimal
在使用 ?: 时要小心:在 PHP 中,字符串 "0" 是假值,因此它也会被转换为 null。如果 0 真的是一个允许的值,你必须显式地与 "" 进行比较以防止这种情况发生。 - bart

19

Laravel 4

如果需要,你可以通过 过滤 来删除数组中的空字符串。

$input = array_filter(Input::all(), 'strlen');

如果你有像array('a' => 'a', 'b' => '')这样的内容,将会得到:array('a' => 'a')

据我所知,在Laravel Eloquent ORM中,如果一个字段没有在批量赋值的数组中指定,它会被视为NULL


Laravel 5

$input = array_filter(Request::all(), 'strlen');
或者
// If you inject the request.
$input = array_filter($request->all(), 'strlen');

好主意...我曾考虑过迭代输入并想要一个更简洁的解决方案。 - Israel Ortuño
array_filter本身就足够了,不需要提供strlen作为回调函数来使用。 - Jason Lewis
6
你说得对,@JasonLewis,但是...仅使用array_filter将过滤掉所有假值。如果你有一个字符串值为0,它也会被过滤掉,这可能不是预期的结果。 - Rubens Mariuzzo
1
如果您不想更新任何列,那么这很好。但是如果您有一个日期,并且希望将其更新为NULL,该怎么办?您该如何处理? - Claire
5
请注意,此方法不适用于更新操作,因为用户重置的字段将被忽略,以前的值将被保留。在这种情况下,可以采用类似的方法: $input = array_map(function($e) { return $e ?: null; }, Input::all()); - invkesg
显示剩余3条评论

12

可能更通用的解决方案:

class EloquentBaseModel extends Eloquent {

    public static function boot()
    {
        parent::boot();

        static::saving(function($model)
        {
            if (count($model->forcedNullFields) > 0) {
                foreach ($model->toArray() as $fieldName => $fieldValue) {
                    if ( empty($fieldValue) && in_array($fieldName,$model->forcedNullFields)) {
                        $model->attributes[$fieldName] = null;
                    }
                }
            }

            return true;
        });

    }

}

如果您需要清洗空表单字段的模型,应该扩展此类,然后您应该填写$forcedNullFields数组,其中包含在表单字段为空的情况下需要设置为 NULL 的字段名称:

扩展此类以清理空表单字段的模型,需将$forcedNullFields数组填充为需要在表单字段为空时设置为NULL的字段名称:

class SomeModel extends EloquentBaseModel {
    protected $forcedNullFields = ['BirthDate','AnotherNullableField'];
}

就这些了,你不应该在mutators中重复相同的代码。


在我看来,这是最好的答案。它提供了完美的颗粒控制,而不需要重复冗长的代码。而且它甚至更像 Laravel,这很不错。 - jlbang
不过,如果零是一个有效值,则不返回零。 - Ilya Sheershoff
在你的示例模型中,数组值应该是 snake_case 格式的,对吗? - QuickDanger

10

使用模型“saving”事件查找空模型并将其显式设置为null。“Project”是此示例中我的模型名称。将其放在辅助函数中并将其连接到所有模型。

使用“saving”事件检测空模型,并将其置空。以“Project”为例,将此功能封装成一个辅助函数,并将其应用于所有的模型。

Project::saving(function($model) {
    foreach ($model->toArray() as $name => $value) {
        if (empty($value)) {
            $model->{$name} = null;
        }
    }

    return true;
});

LARAVEL 5更新内容 (2016年4月11日)

我最终创建了一个Http中间件,来清除HTTP请求中的输入数据,并在将数据发送到数据库之前进行清理。

class InputCleanup
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $input = $request->input();

        array_walk_recursive($input, function(&$value) {

            if (is_string($value)) {
                $value = StringHelper::trimNull($value);
            }
        });

        $request->replace($input);

        return $next($request);
    }
}

3
使用这段代码一段时间后,要小心使用。它有一个有趣的副作用,即在每次更新时将值设置为 null。这可能会产生意想不到的后果。最好检查该值是否已经从原始状态改变。 - craig.tadlock
我在这里扩展了你的方法:http://www.laravel-tricks.com/tricks/setting-null-values-only-on-certain-fields - Mei Gwilym
@craigtadlock,现在我们已经有了 Laravel 5.1,我没有看到你提到的副作用。添加此评论是为了让其他人知道这不再是一个问题。 - karmendra

1
对于表单输入,空值比null值更正常、更合理。如果您真的认为将输入直接放入数据库是最好的方法,那么将空值转换为null的解决方案如下所示。
$input = Input::all();
foreach ($input as &$value) {
    if (empty($value) { $value = null; }
}
$new = new Contact(Input::all());
$new->save();

就我个人而言,我不赞同这种解决方案,但对于某些人来说确实有效。


直接将空字符串存储到数据库中即可。毕竟,我认为您不会呈现空值... - Rubens Mariuzzo
1
我也不接受空值,因为我正在使用ImnoDB、关系和约束。如果一个外键不是NULL,它会尝试查找外部ID,即使它是"",显然在外部表中不存在,并抛出MySQL错误,例如“无法添加或更新子行:违反外键约束”。 - Israel Ortuño
@RubensMariuzzo 你可能想在数据库中使用Null值的一些原因。例如:我在我的DB中存储一个包含序列化数据对象的字符串,如果数据尚未生成,则该值为null,如果数据已经生成但没有要存储的数据,则为空字符串。Israel Ortuño:实际上,我会写出所有字段,因为我认为最好“白名单”您获得的所有输入,这样您就永远不会处理不想要的数据。 - Nico Kaag
我已经尝试过那种方式了...但是仍然得到一个空字段,因为Input::get()返回字段值(为空)而不是null - Israel Ortuño
尝试使用Input::get('salutation', null),我认为这样应该可以解决问题。这样你也可以设置其他默认值,而不必使用if和else语句。 - Nico Kaag
@NicoKaag 我已经尝试过了,但它仍然会将空字段作为不是 NULL 而被 get() 方法排除掉。请查看我的最新更新以查看我最终所做的事情。 - Israel Ortuño

1
另一个简单的解决方案是创建基本模型类,并从中扩展模型:
class Model extends \Illuminate\Database\Eloquent\Model
{
    /**
     * Set field to null if empty string
     *
     * @var array
     */
    protected $forcedNullFields = [];

    /**
     * Set a given attribute on the model.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return \Illuminate\Database\Eloquent\Model
     */
    public function setAttribute($key, $value)
    {
        if (in_array($key, $this->forcedNullFields) && $value === '') {
            $value = null;
        }

        return parent::setAttribute($key, $value);
    }
}

如果需要的话,只需在每个模型中填写$forcedNullFields中所需的字段。


-2
在您的Eloquent模型中,请确保$guarded为空。您不需要设置$fillable
通常情况下,批量赋值并不是一个好主意,但在某些情况下可以这样做 - 您必须确定何时使用。
请参见:http://laravel.com/docs/eloquent#mass-assignment

我在进行批量赋值时没有问题,只是遇到了一个问题,即每个空表单字段都将其值设置为 "" 而不是 NULL - Israel Ortuño
当您进行POST请求时,这些字段是否存在?如果存在,它们是否为空?对于表列,您的迁移和/或默认值是什么样子的? - Rob W
当我进行POST操作时,这些字段是存在的,这就是为什么我得到空值而不是NULL的原因。主要问题如下所述。 - Israel Ortuño
如果你想要一个空行,为什么不直接调用 Model::create(array()) 呢? - Rob W
是的...这就是我想要的,我本来就不希望从Input::all()中得到空字段...现在我想我必须手动处理它了 ;) - Israel Ortuño
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接