Laravel firstOrCreate()结合事件监听器时重复存储手机号问题
我有一个名为phones的表,包含id和number字段,要求仅存储唯一手机号。之前用$phone = Phone::firstOrCreate(['number' => $phone['number']]);可以正常实现去重。现在需要存储无空格的手机号,但展示时带空格提升可读性,于是在Phone模型中注册了saving和retrieved事件监听器:
protected $dispatchesEvents = [ 'saving' => \App\Events\Phone\PhoneSavingEvent::class, 'retrieved' => \App\Events\Phone\PhoneRetrievedEvent::class, ];
监听器实现如下:
// PhoneSavingEvent 处理逻辑 public function handle(PhoneSavingEvent $event) { $event->phone->number = preg_replace('/\s+/', '', $event->phone->number); }
// PhoneRetrievedEvent 处理逻辑 public function handle(PhoneRetrievedEvent $event) { $event->phone->number = wordwrap($event->phone->number, 3, ' ', true); }
监听器能正常处理手机号的存储和展示,但firstOrCreate()现在会存储重复手机号。
补充说明:如果不用$dispatchesEvents,改用模型启动方法:
protected static function booted() { static::saving(function ($phone) { $phone->number = preg_replace('/\s+/', '', $phone->number); }); }
存储无空格手机号(如“111222333”)时firstOrCreate()可正常去重,但存储带空格的手机号(如“111 222 333”)时,会转为无空格形式后重复存储,而该手机号实际已存在。请问该如何处理手机号来解决这个问题?
方法1:先格式化查询条件再调用firstOrCreate
firstOrCreate的逻辑是先按传入条件查记录,找不到再创建。问题核心是查询时用的是原始带空格的手机号,而数据库存的是无空格版本,导致查不到就重复创建。
直接先格式化手机号,再执行方法:
$formattedNumber = preg_replace('/\s+/', '', $phone['number']); $phone = Phone::firstOrCreate(['number' => $formattedNumber]);
方法2:用模型访问器+修改器(推荐)
Laravel的访问器(读取时格式化)和修改器(存储时格式化)是专门处理字段格式的方案,比事件更直观,还能避免firstOrCreate的查询问题:
- 定义修改器,存储时自动去空格:
public function setNumberAttribute($value) { $this->attributes['number'] = preg_replace('/\s+/', '', $value); }
- 定义访问器,读取时自动加空格:
public function getNumberAttribute($value) { return wordwrap($value, 3, ' ', true); }
不管传入带空格还是无空格的手机号,修改器都会先处理成无空格再存储;读取时访问器自动格式化展示。而且调用firstOrCreate时,Laravel会自动用修改器处理传入的number字段,再执行查询和创建,不会出现重复。
方法3:重写模型的firstOrCreate方法(可选)
如果不想每次调用都手动格式化,可以在Phone模型里重写方法,自动处理手机号:
public static function firstOrCreate(array $attributes, array $values = []) { if (isset($attributes['number'])) { $attributes['number'] = preg_replace('/\s+/', '', $attributes['number']); } return parent::firstOrCreate($attributes, $values); }
之后直接调用Phone::firstOrCreate(['number' => $phone['number']]);即可,内部会自动格式化手机号后执行逻辑。
内容的提问来源于stack exchange,提问作者andrysikova.petra

