A Quick Way to Test Laravel Database Connections

You can use Tinker to verify if your database connection is working:

php artisan tinker

Then inside Tinker, run:

DB::connection()->getPdo();

If the connection succeeds, you’ll get back PDO instance details. If it fails, you’ll see the actual error message (usually something helpful like credentials issues or host unreachable).

laravel-phone包实现电话号码验证、格式转换等功能

相信你的系统中也一定会有手机注册以及发送短信验证码的功能。那么验证用户填写的手机号是否规范合法就是我们完成后续功能的第一步。propaganistas/laravel-phone包为我们提供了非常强大且可靠的电话验证功能,并且适用于全球所有国家的号码验证。

安装

composer require propaganistas/laravel-phone

配置

服务提供者会被 Laravel 自动发现。在语言目录lang中,为每个 validation.php 语言文件添加一条额外的翻译:

'phone' => 'The :attribute field must be a valid number.',

在线工具网站

https://laravel-phone.herokuapp.com用于测试Laravel-Phone包中的电话号码验证组件。

验证请求字段

你可以在验证规则数组中使用phone关键字,或者使用 Propaganistas\LaravelPhone\Rules\Phone规则类来以更具表现力的方式定义规则。 要限制国家/地区,你可以明确指定允许的国家/地区的代码:

'my_input'       => 'phone:US,BE',
// 'my_input'    => (new Phone)->country(['US', 'BE'])

或者为了使功能更加灵活,还可以与另一个包含国家/地区代码的数据字段进行匹配。例如,要求用户的电话号码与用户提供的居住国家/地区匹配。请确保国家/地区字段的名称与电话号码字段的名称相同,但附加”_country”后缀以便laravel-phone包自动识别:

'my_input'            => 'phone',
// 'my_input'         => (new Phone)
'my_input_country'    => 'required_with:my_input',

或者将自定义的国家/地区字段名称作为参数传递给验证器:

'my_input'            => 'phone:custom_country_field',
// 'my_input'         => (new Phone)->countryField('custom_country_field')
'custom_country_field'  => 'required_with:my_input',

注意:国家代码应符合ISO 3166-1 alpha-2标准。

为了支持除白名单国家/地区之外的任何有效国际格式的电话号码,请使用INTERNATIONAL参数。当你预期接收来自特定国家/地区的本地格式号码,但同时也希望接受任何其他正确输入的国外号码时,此功能会非常有用:

'my_input'            => 'phone:INTERNATIONAL,BE',
// 'my_input'         => (new Phone)->international()->country('BE')

要指定电话号码类型的约束,请将允许的类型附加到参数后面,例如:

'my_input'       => 'phone:mobile',
// 'my_input'    => (new Phone)->type('mobile')
// 'my_input'    => (new Phone)->type(libphonenumber\PhoneNumberType::MOBILE)

最常见的类型是移动电话mobile和固定电话fixed_line,但你可以随意使用此处定义的任何电话号码类型。

在电话号码类型名称前加上感叹号即可将其列入黑名单。请注意,你不能同时使用白名单和黑名单:

'my_input'       => 'phone:!mobile',
// 'my_input'    => (new Phone)->notType('mobile')
// 'my_input'    => (new Phone)->notType(libphonenumber\PhoneNumberType::MOBILE)

你还可以使用LENIENT参数启用宽松验证。启用宽松验证后,系统只会检查数字的长度,而不会检查实际的运营商模式:

'my_input'       => 'phone:LENIENT',
// 'my_input'    => (new Phone)->lenient()

模型属性转换(cast)

为了方便对Eloquent模型属性进行自动类型转换,我们提供了两个转换类:

use Illuminate\Database\Eloquent\Model;
use Propaganistas\LaravelPhone\Casts\RawPhoneNumberCast;
use Propaganistas\LaravelPhone\Casts\E164PhoneNumberCast;

class User extends Model
{
    public $casts = [
        'phone_1' => RawPhoneNumberCast::class.':BE',
        'phone_2' => E164PhoneNumberCast::class.':BE',
    ];
}

这两个类都会自动将数据库中的值转换为PhoneNumber对象,以便在你的应用程序中进一步使用:

$user->phone // PhoneNumber object or null

在设置值时,它们都接受字符串值或PhoneNumber对象。RawPhoneNumberCast会将数据库值更改为原始输入号码,而E164PhoneNumberCast会将格式化的E.164电话号码写入数据库。

对于RawPhoneNumberCast类,需要指定电话号码所属国家/地区,才能正确地将原始号码解析为PhoneNumber对象。对于E164PhoneNumberCast类,如果待设置的值并非已采用某种国际格式,则也需要指定电话号码所属国家/地区,才能正确地转换该值。

这两个类以相同方式接受类型转换参数:

  • 如果存在名称相似但后缀为”_country”的属性(例如phone_country),则类型转换器会自动检测并使用该属性
  • 提供另一个属性的名称作为类型转换参数
  • 提供一个或多个国家/地区代码作为类型转换参数
public $casts = [
    'phone_1' => RawPhoneNumberCast::class.':country_field',
    'phone_2' => E164PhoneNumberCast::class.':BE',
];

重要提示:这两种类型转换都需要有效的电话号码才能顺利地进行PhoneNumber对象的转换。请在将电话号码设置到模型之前进行验证。有关如何验证电话号码,请参阅上节“验证请求字段”。

⚠️ 属性赋值和E164PhoneNumberCast

由于E164PhoneNumberCast的特性,如果电话号码不是国际格式,则需要提供有效的国家/地区代码。由于Laravel在设置属性时会立即执行类型转换,因此请务必在设置电话号码属性之前设置国家/地区代码属性。否则,E164PhoneNumberCast将遇到空的国家/地区代码值并抛出意外异常。

// 错误
$model->fill([
    'phone' => '012 34 56 78',
    'phone_country' => 'BE',
]);

// 正确
$model->fill([
    'phone_country' => 'BE',
    'phone' => '012 34 56 78',
]);

// 错误
$model->phone = '012 34 56 78';
$model->phone_country = 'BE';

// 正确
$model->phone_country = 'BE';
$model->phone = '012 34 56 78';

实用电话号码类PhoneNumber

电话号码可以封装在Propaganistas\LaravelPhone\PhoneNumber类中,从而为其添加实用的方法。在视图中或保存到数据库时,可以直接引用这些对象,它们会优雅地降级为E.164格式。

use Propaganistas\LaravelPhone\PhoneNumber;

(string) new PhoneNumber('+3212/34.56.78');                // +3212345678
(string) new PhoneNumber('012 34 56 78', 'BE');            // +3212345678

或者,你可以使用phone()辅助函数。它会返回一个Propaganistas\LaravelPhone\PhoneNumber实例,如果提供了$format参数,则返回格式化后的字符串:

phone('+3212/34.56.78');                // PhoneNumber instance
phone('012 34 56 78', 'BE');            // PhoneNumber instance
phone('012 34 56 78', 'BE', $format);   // string

格式化号码

可以多种方式格式化PhoneNumber实例:

$phone = new PhoneNumber('012/34.56.78', 'BE');

$phone->format($format);       // 见libphonenumber\PhoneNumberFormat
$phone->formatE164();          // +3212345678
$phone->formatInternational(); // +32 12 34 56 78
$phone->formatRFC3966();       // tel:+32-12-34-56-78
$phone->formatNational();      // 012 34 56 78

// 格式化后,即可直接从提供的国家/地区拨打该号码
$phone->formatForCountry('BE'); // 012 34 56 78
$phone->formatForCountry('NL'); // 00 32 12 34 56 78
$phone->formatForCountry('US'); // 011 32 12 34 56 78

// 格式化后的号码在手机上可以直接点击,然后可以用手机直接从指定的国家/地区拨打该号码
$phone->formatForMobileDialingInCountry('BE'); // 012345678
$phone->formatForMobileDialingInCountry('NL'); // +3212345678
$phone->formatForMobileDialingInCountry('US'); // +3212345678

号码信息

获取一些关于该号码的信息:

$phone = new PhoneNumber('012 34 56 78', 'BE');

$phone->getType();              // libphonenumber\PhoneNumberType::FIXED_LINE
$phone->isOfType('fixed_line'); // true    (or use $phone->isOfType(libphonenumber\PhoneNumberType::FIXED_LINE) )
$phone->getCountry();           // 'BE'
$phone->isOfCountry('BE');      // true

等值比较

比较两个号码相同还是不同:

$phone = new PhoneNumber('012 34 56 78', 'BE');

$phone->equals('012/34.56.76', 'BE')       // true
$phone->equals('+32 12 34 56 78')          // true
$phone->equals( $anotherPhoneObject )      // true/false

$phone->notEquals('045 67 89 10', 'BE')    // true
$phone->notEquals('+32 45 67 89 10')       // true
$phone->notEquals( $anotherPhoneObject )   // true/false

数据库注意事项

免责声明:不同应用程序处理电话号码的方式各不相同。因此,以下内容仅供参考,旨在启发思考;我们不提供相关技术支持。

将电话号码存储在数据库中一直是一个值得探讨的问题,而且并没有一劳永逸的解决方案(没有银弹)。一切都取决于你的应用程序需求。以下是一些需要考虑的因素,以及一个实现建议。你理想的数据库设置可能需要结合以下列出的一些要点。

Laravel框架基于外键关系的级联删除的两种实现方法

例如一个论坛系统有帖子表topics和评论表replies。一个帖子可以有0条或多条评论。评论表replies有一个topic_id列,是指向帖子表topics的id列的外键。

当删除一个帖子时,应该级联删除其所有评论。有两种方法:

  • 方法一,在评论表replies的迁移文件中,定义外键约束时调用onDelete('cascade')方法表示删除应级联:
$table->foreignId('topic_id')
      ->constrained()
      ->onDelete('cascade');

让Laravel框架帮我们做基于外键关系的级联删除操作。参考官方文档外键约束。这种方法简单有效,缺点是不够灵活,有些开发规范不推荐使用外键约束。

  • 方法二, 在Eloquent模型的deleted事件的监听器中自己写代码实现级联删除,好处是灵活、扩展性强,不受底层数据库约束,坏处是要自己写很多代码,容易产生bug。

在实际开发中,方法二用得更多,因为更灵活、扩展性强。例如可以方便实现“不真正删除而是放入回收站表”这种功能。

参考

9.3. 防止数据损坏

Laravel Eloquent模型类使用selectRaw或DB::raw方法执行聚合查询会报错MissingAttributeException

我的Laravel版本是11。我执行以下代码能正常获得查询结果:

$topic_users = DB::table('topics')->select(DB::raw('user_id, count(*) as topic_count'))->where('created_at', '>=', Carbon::now()->subDays(60))->groupBy('user_id')->get();

$topic_users = DB::table('topics')->selectRaw('user_id, count(*) as topic_count')->where('created_at', '>=', Carbon::now()->subDays(60))->groupBy('user_id')->get();

但执行以下代码:

$topic_users = Topic::select(DB::raw('user_id, count(*) as topic_count'))->where('created_at', '>=', Carbon::now()->subDays(60))->groupBy('user_id')->get();

$topic_users = Topic::query()->select(DB::raw('user_id, count(*) as topic_count'))->where('created_at', '>=', Carbon::now()->subDays(60))->groupBy('user_id')->get();

$topic_users = Topic::selectRaw('user_id, count(*) as topic_count')->where('created_at', '>=', Carbon::now()->subDays(60))->groupBy('user_id')->get();

报错:

Illuminate\Database\Eloquent\MissingAttributeException  The attribute [view_count] either does not exist or was not retrieved for model [App\Models\Topic].

该错误的原因是在使用Topic模型执行查询时,Laravel Eloquent默认会尝试加载模型中声明的所有属性(即数据库表中的所有列),但是你的查询语句是聚合查询,只返回user_id和聚合字段topic_count。Laravel Eloquent默认会尝试加载Topic模型中的所有字段,期望你返回的是一个“普通”的字段列表,而不是聚合后的结果,因此出现了MissingAttributeException错误,因为在查询结果中并没有view_count字段。

Initial Data in Laravel Projects

Initial data for a project, such as roles (e.g., super admin role), permissions, and which users are granted admin privileges, is typically designed during the requirements analysis phase.

This initial data is a part of the project’s operation and will be used in the production environment. However, data seeding is generally used during the development phase.

Although Laravel doesn’t offer a built-in solution for this, we can leverage the database migration feature to achieve it. In terms of functionality, data migration is also part of the project, with the execution timing aligning perfectly with the installation of the project. The execution order is critical, ensuring that the initialization data is applied after the database table structure is created.

We can generate migration files for initializing data using the following command:

php artisan make:migration seed_categories_data

We define the naming convention for such migration files as seed_(table_name)_data.

For project initialization data, using a Seeder is less convenient than using a database migration file, especially in collaborative development. With multiple migration files from different developers, you only need to execute a single migration command. If issues arise, you can easily roll back, whereas Seeders lack rollback functionality. Rollback is crucial, both in development and production environments. If the initial data can be fully determined before the project is deployed (which is almost impossible), then using a Seeder is acceptable and recommended. However, after the project goes live, it’s likely that the initial data will require modifications (such as additions, deletions, or updates). In such cases, using a Seeder is inappropriate, and database migration files should be used instead. Seeders are primarily used for generating test data, and they should not be used for altering or deleting production data.

Laravel项目的初始化数据

项目的初始化数据,例如一个应用程序有哪些角色(例如超级管理员角色)、哪些权限、授予哪些用户管理员权限是在需求分析阶段就设计好的。

项目的初始化数据是项目运行的一部分,在生产环境下也会使用到,而数据填充(Seeder)一般在开发时使用。

虽然Laravel没有自带此类解决方案,不过我们可以借助数据迁移功能来实现。在功能定位上,数据迁移也是项目的一部分,执行的时机刚好是在项目安装时。并且区分执行先后顺序,这确保了初始化数据发生在数据表结构创建完成后。

我们可以使用命令生成数据迁移文件,作为初始化数据的迁移文件:

php artisan make:migration seed_categories_data

我们定义这种迁移文件的命名规范为seed_(数据库表名称)_data

对于项目的初始化数据,使用Seeder没有使用数据库迁移文件来得方便,特别是在多人协作开发时,对于别的开发者提供的多个数据库迁移文件,你只需执行一条迁移命令即可,如果有问题还能回滚,Seeder没有回滚功能。回滚功能无论是对于开发环境还是生产环境都是很重要的。 如果项目的初始化数据在项目上线(部署到生产环境)之前,就能全部确定下来(这几乎不可能),那么使用Seeder是可以的,也是应该的。但是在项目上线之后,很可能还要对初始化数据进行增删改操作,此时再使用Seeder是不合适的,应该使用数据库迁移文件。Seeder主要用来生成测试数据,很难、也不要用Seeder来删改数据库数据。

Laravel’s DB Facade Doesn’t Trigger Eloquent ORM Model Events

When using the DB facade to perform database operations in Laravel, Eloquent ORM model events like saving, saved, etc., are not triggered.

If you want to avoid triggering model events in event listeners or queued tasks, aside from using Eloquent’s saveQuietly, deleteQuietly, and similar methods, you can directly use the DB facade to execute database operations.

This approach allows you to bypass Eloquent’s event handling when necessary.

Laravel使用DB façade执行数据库操作不会触发Eloquent ORM模型事件

使用DB façade执行数据库操作不会触发saving、saved等Eloquent ORM模型事件。当我们不想在事件监听器或队列任务中触发Eloquent ORM模型事件时,除了使用Eloquent ORM模型的saveQuietly、deleteQuietly等方法之外,还可以直接使用DB façade执行数据库操作。

在Eloquent ORM模型事件监听器和队列任务中,要避免使用Eloquent模型增删改查方法,例如create、update、save等。否则会陷入调用死循环 —— 模型事件监听器分发队列任务,队列任务触发模型事件,模型事件监听器再次分发队列任务,队列任务再次触发模型事件……死循环了。

Modify and Persist Model Instances in Laravel Using the saved Event, Not the saving Event

In Laravel, don’t call the save method on a model instance inside the saving event listener.

If you need to modify a model’s field and persist it within a model event listener, make sure you’re using the saved event, not the saving event. This is particularly important when your event listeners are queued for asynchronous execution.

The saving event occurs before the model is persisted to the database. If you try to modify a field and call save() within this listener, it won’t actually persist to the database, especially when the listener is queued for async execution. For example, modifying the slug field might not actually update in the database.

Instead, use the saved event listener and call saveQuietly to persist the changes, as shown in the example:

static::saved(queueable(function (Topic $topic) {
    // If the slug is empty, translate the title into a slug
    if (!$topic->slug) {
        $topic->slug = app(SlugTranslateHandler::class)->translate($topic->title);
        $topic->saveQuietly();
    }
}));

By using the saved event and saveQuietly, you ensure that your changes are made after the model is successfully persisted, avoiding any issues with asynchronous queue execution.