使用 ed25519 SSH Key 代替 RSA 密钥

什么是 ed25519

ed25519 是一个相对较新的加密算法,实现了 Edwards-curve Digital Signature Algorithm(EdDSA)。但实际上 ed25519 早已经在 5 年前就被 OpenSSH 实现,并不算什么前沿科技。但很多人,即使是每天都使用 SSH/SCP 的人可能并不清楚这个新类型 key。

不过要注意的是并不是所有的软件目前都实现了 ed25519,但是大多数最近的操作系统 SSH 都已经支持了。

ed25519 的好处

  • 相较于 RSA key,最明显的一个好处就是 ed25519 key 非常短,这就非常方便存储以及传输 key
  • 另外就是产生和校验更快
  • collision resilience,这意味着可以有效的避免 hash 碰撞攻击

生成 ed25519 SSH key

ssh-keygen -t ed25519 -C "your@gmail.com"

可以检查 ~/.ssh 目录下的 key,会发现 ed25519 的公钥只有简短的一行:

ssh-ed25519 AAAACxxxx  your@gmail.com

2022-04-05 ssh , ssh-keygen , key

Laravel 学习笔记:Model Factoris 批量创建假数据

在开发环节要测试的时候,如果想要在数据库中批量插入一些假数据,这个时候就可以使用 model factories

database/factories/ 目录下面默认定义了一个 UserFactory.php

namespace Database\Factories;
 
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
 
class UserFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }
}

可以看到在这个类中给 User 的每一个字段都设置了一个 faker 方法。

产生 Factories

通过命令:

php artisan make:factory UserFactory

这个时候在 database/factories 下面就会有一个 UserFactory,你需要按照 faker 的方式给 Model 每一个自定义的字段都加上 fake 方式。

tinker

然后执行 tinker:

php artisan tinker

进入交互式命令行之后:

use App\Models\User;
User::factory(10)->create();

执行完成之后就会往数据库中插入 10 条假数据。


2022-04-05 laravel , database , fake

Laravel 学习笔记:分页

Laravel 的分页实现集成了 Query Builder 和 Eloquent ORM,提供了一种非常方便的分页接口。

基础使用

最简单的方式就是使用 query builder 和 Eloquent query 的 paginate 方法,这个方法会自动处理请求的 limit 和 offset 参数。

默认情况下,当前页面的参数使用 page 表示。

所以在 Controller 中直接指定一页请求的条数即可:

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
 
class UserController extends Controller
{
    /**
     * Show all application users.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        return view('user.index', [
            'users' => DB::table('users')->paginate(15)
        ]);
    }
}

默认情况下 paginate 方法会统计总条数,如果不需要,可以使用

$users = DB::table('users')->simplePaginate(15);

如果使用 Eloquent,可以直接在 Model 上调用:

$users = User::paginate(15);

排序

如果要倒序来分页,有两种写法,一种是直接使用 DB:

$ondata = DB::table('official_news')->orderBy('created_date', 'desc')->paginate(10);

另外一种就是使用 Model:

$posts = Post::orderBy('id', 'desc')->paginate(10);

Cursor 分页

除了使用 offset 方式分页,还可以使用游标:

$users = DB::table('users')->orderBy('id')->cursorPaginate(15);

这样每一次请求,会在分页请求中带上 cursor 参数:

http://localhost/users?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0

表示了下一页的开始。

  • 对于大数据表,游标分页可以提供更好的性能,在使用 order by ,并且有索引的情况下
  • 如果数据表经常写,offset 分页可能会跳过记录或者出现重复条数,如果有记录被频繁的增加,或删除的话

分页的一些问题和局限:

  • simplePaginate 只能显示 NextPrevious 链接,无法支持展示页码
  • 需要排序的列是唯一的,包含 null 的列无法支持排序

自定义分页 URLs

默认情况下,分页器产生的链接会匹配请求的 URI。withPath 方法允许自定义 URL。

如果想要产生的链接是 http://example.com/admin/users?page=N 需要传入 /admin/users

use App\Models\User;
 
Route::get('/users', function () {
    $users = User::paginate(15);
 
    $users->withPath('/admin/users');
 
    //
});

展示分页结果

当调用 paginate 方法时,会获得一个 Illuminate\Pagination\LengthAwarePaginator 实例,当调用 simplePaginate 方法时,会获得 Illuminate\Pagination\Paginator.

cursorPaginate 会获得 Illuminate\Pagination\CursorPaginator

这些对象都提供了一些方法来获取结果集。

<div class="container">
    @foreach ($users as $user)
        
    @endforeach
</div>
 


2022-04-04 laravel , php , laravel-pagination

Laravel 学习笔记:Model 之间关系

Laravel 使用的 Eloquent ORM 中的 Model 可以用一种非常易读的方式去定义 Model 和 Model 之间的关系。

1 对 1 关系

比如 User 和 Phone 都是一个 Model,要去表示用户和 Phone 的关系,可以:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
 
class User extends Model
{
    /**
     * Get the phone associated with the user.
     */
    public function phone()
    {
        return $this->hasOne(Phone::class);
    }
}

在 User Model 中定义 phone() 方法,然后使用 Illuminate\Database\Eloquent\Model 中定义的 hasOne() 方法。

hasOne() 方法的第一个参数是 Model 的类名。一旦定义了就可以动态的直接通过用户 Model 去访问 Phone

$phone = User::find(1)->phone;

上面的方式默认 Phone 这个 Model 中有一个 user_id 的外键。如果定义了其他名字,可以将外键名字传入第二个参数:

return $this->hasOne(Phone::class, 'foreign_key');

定义一对一逆向关系

比如 Phone 属于用户:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
 
class Phone extends Model
{
    /**
     * Get the user that owns the phone.
     */
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Eloquent 会按照约定,假设 Phone model 中含有一个 user_id 列。

一对多关系

一对多关系通常用来定义一个 Model 是其他 Model 的父节点。比如一篇文章可嗯有无数的评论。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
 
class Post extends Model
{
    /**
     * Get the comments for the blog post.
     */
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

注意 Eloquent 会自动决定外键的列名,按约定,Eloquent 会自动使用 parent model 的 snake case 名字然后加上 _id 作为外键。所以上面的例子中,Eloquent 会自动认为 Comment model 中有一个 post_id 的外键。

一旦定义了关系,就可以动态的获取:

use App\Models\Post;
 
$comments = Post::find(1)->comments;
 
foreach ($comments as $comment) {
    //
}

逆向一对多关系

使用 belongsTo 方法:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
 
class Comment extends Model
{
    /**
     * Get the post that owns the comment.
     */
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

获取:

use App\Models\Comment;
 
$comment = Comment::find(1);
 
return $comment->post->title;

多对多关系

最常见的多对多关系就是,用户-角色,用户可能有多重角色,同一个角色也会有不同的用户。另外一个比较常见的场景就是标签系统,一本书会有标签1,2,3,标签1也会包含多本书。

class User extends Model
{
    public function roles() {
        return $this->belongsToMany(
            Role:class,
            // pivot table
            'role_user',
            'user_id',
            'role_id'
        )
    }
}

Role 定义:

class Role extends Model {
    public function users() {
        return $this->belongsToMany(
            User:class,
            'role_user',
            'role_id',
            'user_id'
        )
    }
}

远程一对一

三个 Model 之间都是一对一关系,那么就可以建立远程一对一关系。


2022-04-03 laravel , orm , laravel-orm , php

Laravel 学习笔记:事件

Laravel Event 提供了一个最简单的观察者模式实现,可以订阅监听应用中发生的事件。事件通常放在 app/Events 目录,监听器放在 app/Listeners

事件是应用功能模块之间解耦的有效方法。单个事件可以有多个监听器,监听器之间相互没有影响。

比如说每次有订单产生时,发送给用户一个 Slack 通知,通过事件,可以将处理订单的代码和 Slack 通知代码解耦,只需要发起一个事件,监听器监听订单产生事件,然后触发响应的动作即可。

注册事件/监听器

可以使用如下的命令创建 Event

php artisan event:generate

或者分别单独创建 Event 和 Listener:

php artisan make:event PodcastProcessed

php artisan make:listener SendPodcastNotification --event=PodcastProcessed

然后需要手动添加到 EventServiceProviderboot 方法中。

EventServiceProvider 中的 $listen 数组配置了监听器:

    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
    ];

Defining Event

Event class 是一个包含了 Event 信息的数据容器。

<?php

namespace App\Events;

use App\Models\Subscribe;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class SubscribeEvent
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $subscribe;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Subscribe $subscribe)
    {
        $this->$subscribe = $subscribe;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('channel-name');
    }
}

可以看到 Event 中几乎没有什么逻辑,只是保存了一个 Subscribe Model。

Defining Listeners

Event listeners 会在 handle 方法中被触发。也 handle 方法中可以执行对应的事件响应。

<?php

namespace App\Listeners;

use App\Events\SubscribeEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class SubscribeListener
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  object  $event
     * @return void
     */
    public function handle(SubscribeEvent $event)
    {
        //
    }
}

handle 方法会接受一个 Event 参数,这个参数就是定义的 Event。

定义好 Event 和 Listener 之后,在 EventServiceProvider 注册,就可以通过

event(new \App\Events\SubscribeEvent($subscribe));

来触发事件。

Queued Event Listener

如果你要在 Listener 中执行一些繁重的操作,那么可以使用 Queued Event Listener:

Listener 上指定实现 ShouldQueue,然后记得配置好队列。

<?php
 
namespace App\Listeners;
 
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
 
class SendShipmentNotification implements ShouldQueue
{
    //
}

这样,当一个事件发生后,Listener 会自动被添加到队列中。

Dispatching Events

可以调用 Eventsdispatch() 方法。

Event Subscribers


2022-04-01 laravel , laravel-event , learning-note , php

Laravel 学习笔记:队列

当需要构建一个网络应用的时候,可能有一些任务,比如解析、存储、传输 CSV 文件等等,可能需要花费较长的时间。Laravel 提供了一个非常简单的队列 API,可以让这些操作可以在后台进行。让这些繁重的任务在后台执行可以有效的提高应用的响应速度,提升用户使用体验。

Laravel 队列提供了一个统一的 API 访问入口,可以支持不同的队列:

  • [[Amazon SQS]]
  • [[Redis]]
  • [[Beanstalk]]
  • 甚至关系型数据库

Laravel 队列的配置在 config/queue.php 中。

Laravel 还提供了一个 Redis 队列的 Dashboard 叫做 Horizon。但是这一篇文章不会涉及到 Horizon 相关内容。

Connection Vs. Queues

Laravel 队列的相关配置都在 config/queue.php 配置文件,其中有一个 connections 配置数组。这个选项用来定义和后端队列服务(比如 Amazon SQS,Beanstalk,Redis) 的连接。

每一个 connection 配置,都有一个 queue 属性。如果没有指定队列,那么就会放到 default

use App\Jobs\ProcessPodcast;
 
// This job is sent to the default connection's default queue...
ProcessPodcast::dispatch();
 
// This job is sent to the default connection's "emails" queue...
ProcessPodcast::dispatch()->onQueue('emails');

Laravel 队列允许用户指定队列的优先级:

php artisan queue:work --queue=high,default

Driver Notes & Prerequisites

Database

如果使用数据库作为队列驱动,那么需要创建一张表来存储队列任务。运行 queue:table 来创建表:

php artisan queue:table
php artisan migrate

最后不要忘了给应用配置 database 驱动:

QUEUE_CONNECTION=database

Redis

如果消息队列使用 Redis Cluster,队列的子必须包含 key hash tag ,为了确保所有的 Redis keys 都在同一个 hash slot:

'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => '{default}',
    'retry_after' => 90,
    'block_for' => 5,
],

在使用 Redis Queue 的时候,可以使用 block_for 配置,用来指定驱动应该等待多久才 Redis 中拉取数据。

如果要使用其他队列,需要安装其他依赖:

  • Amazon SQS: aws/aws-sdk-php ~3.0
  • Beanstalkd: pda/pheanstalk ~4.0
  • Redis: predis/predis ~1.0 or phpredis PHP extension

Creating Jobs

默认所有的队列任务存放在 app/Jobs 目录中,如果目录不存在可以用 artisan 命令生成:

php artisan make:job ProcessPodcast

产生的类会实现 Illuminate\Contracts\Queue\ShouldQueue 接口,告诉 Laravel 这个任务应该被放到队列中异步执行。

Job 类非常简单,包含一个 handle 方法,会被队列调用。

<?php
 
namespace App\Jobs;
 
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
 
class ProcessPodcast implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
    /**
     * The podcast instance.
     *
     * @var \App\Models\Podcast
     */
    protected $podcast;
 
    /**
     * Create a new job instance.
     *
     * @param  App\Models\Podcast  $podcast
     * @return void
     */
    public function __construct(Podcast $podcast)
    {
        $this->podcast = $podcast;
    }
 
    /**
     * Execute the job.
     *
     * @param  App\Services\AudioProcessor  $processor
     * @return void
     */
    public function handle(AudioProcessor $processor)
    {
        // Process uploaded podcast...
    }
}

在这个例子中,传入了一个 Eloquent model,因为 Job 使用了 SerializesModels,Eloquent models 会自动序列化和反序列化。

二进制数据,比如图片内容,应该通过 base64_encode 方法,然后再传入队列。否则任务可能无法序列化成 JSON,然后放到队列中。

可能 model 的关系也会被序列化,这可能会很大,所以为了避免 model 关系被序列化,可以调用 withoutRelations

public function __construct(Podcast $podcast)
{
    $this->podcast = $podcast->withoutRelations();
}

Unique Jobs

有些时候期望只有一个实例任务会被放到队列中,可以实现 ShouldBeUnique:

<?php
 
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
 
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    ...
}

上面的例子中,UpdateSearchIndex 任务是唯一的,这就保证了如果队列中的任务没有完成,就不会有新的任务被加入进去。

可以通过 uniqueIduniqueFor 属性来特别指定想要的主键:

<?php
 
use App\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
 
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    /**
     * The product instance.
     *
     * @var \App\Product
     */
    public $product;
 
    /**
     * The number of seconds after which the job's unique lock will be released.
     *
     * @var int
     */
    public $uniqueFor = 3600;
 
    /**
     * The unique ID of the job.
     *
     * @return string
     */
    public function uniqueId()
    {
        return $this->product->id;
    }
}

保持任务唯一,直到开始处理,可以实现 ShouldBeUniqueUntilProcessing

Job Middleware

速率限制 限流

需要在 AppServiceProvider 类的 boot 方法中定义一个 RateLimiter

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
 
/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
    RateLimiter::for('backups', function ($job) {
        return $job->user->vipCustomer()
                    ? Limit::none()
                    : Limit::perHour(1)->by($job->user->id);
    });
}

防止任务重叠

避免一个任务在修改资源的时候,另一个任务也在修改。

Throttling Exceptions

当和一个不稳定的外部接口通信时,一旦抛出异常,可以制定 Throttling Exceptions 机制,定义一个重试时间。

通常情况下如果一个任务抛出了异常,任务会马上重试。

Dispatching Jobs

一旦写好了任务类,就可以使用 dispatch 方法来分发。

可以在 Controller 中手动分发任务。

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\Request;
 
class PodcastController extends Controller
{
    /**
     * Store a new podcast.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $podcast = Podcast::create(...);
 
        // ...
 
        ProcessPodcast::dispatch($podcast);
    }
}

如果想要有条件分发任务可以使用 dispatchIf 或者 dispatchUnless

延迟分发

可以使用 delay 方法来延迟分发任务:

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\Request;
 
class PodcastController extends Controller
{
    /**
     * Store a new podcast.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $podcast = Podcast::create(...);
 
        // ...
 
        ProcessPodcast::dispatch($podcast)
                    ->delay(now()->addMinutes(10));
    }
}

在返回浏览器请求后分发

use App\Jobs\SendNotification;
 
SendNotification::dispatchAfterResponse();

Job Chaining

Job chaining 允许你指定一组任务,这些任务应该按照顺序依次执行,如果一个任务失败了,接下来的任务就不会执行。

use App\Jobs\OptimizePodcast;
use App\Jobs\ProcessPodcast;
use App\Jobs\ReleasePodcast;
use Illuminate\Support\Facades\Bus;
 
Bus::chain([
    new ProcessPodcast,
    new OptimizePodcast,
    new ReleasePodcast,
])->dispatch();

或者:

Bus::chain([
    new ProcessPodcast,
    new OptimizePodcast,
    function () {
        Podcast::update(...);
    },
])->dispatch();

自定义 Queue 和 Connection

向特定队列分发任务。

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\Request;
 
class PodcastController extends Controller
{
    /**
     * Store a new podcast.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $podcast = Podcast::create(...);
 
        // Create podcast...
 
        ProcessPodcast::dispatch($podcast)->onQueue('processing');
    }
}

或者直接在任务的构造方法中定义:

<?php
 
namespace App\Jobs;
 
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
 
class ProcessPodcast implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->onQueue('processing');
    }
}

分发到指定 Connection

如果应用和多个队列 connection 交互,可以使用 onConnection 来指定:

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\Request;
 
class PodcastController extends Controller
{
    /**
     * Store a new podcast.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $podcast = Podcast::create(...);
 
        // Create podcast...
 
        ProcessPodcast::dispatch($podcast)->onConnection('sqs');
    }
}

同样的是,也可以在构造方法中指定:

    public function __construct()
    {
        $this->onConnection('sqs');
    }

Running The Queue Worker

Laravel 包括了一个 Artisan 命令可以用来开始一个队列的 worker,开始处理新的任务。

可以使用 queue:work 命令:

php artisan queue:work

为了使得 queue:work 命令常驻后台,可以使用进程管理器 Supervisor

注意,queue workers 会将应用保存到内存中。这也就意味着代码的改动不会立即生效。在开发的过程中,注意重启 queue workers。

或者可以执行 queue:listen 命令

php artisan queue:listen

该命令只建议在开发过程中使用。

Supervisor 配置

安装 Supervisor

sudo apt install supervisor

Supervisor 的配置在 /etc/supervisor/conf.d 目录中。可以创建 laravel-worker.conf 文件:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/einverne/app.com/artisan queue:work sqs --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/home/einverne/app.com/worker.log
stopwaitsecs=3600

然后启动 Supervisor

sudo supervisorctl reread
 
sudo supervisorctl update
 
sudo supervisorctl start laravel-worker:*

处理失败的任务

Laravel 提供了很多方式了定义任务可以重试的次数,一旦次数达到,任务就会被放到失败队列。

Cleaning Up After Failed Jobs

当一个任务失败时,你可能希望执行一些操作,比如发送通知,或者更新一些数据,这个时候可以定义 failed 方法。

public function failed(Throwable $exception)
{
    // Send user notification of failure, etc...
}

发送邮件时使用队列

上一篇文章讲到了发送邮件,而发送邮件对于应用的响应时间有直接负面影响,生产环境最好的方式就是将发送邮件的过程放入队列中,在后台进行操作。

Laravel 有一个内置的队列,在发送邮件的时候直接使用 queue 方法:

Mail::to($request->user())
    ->cc($moreUsers)
    ->bcc($evenMoreUsers)
    ->queue(new OrderShipped($order));

在使用 queue 之前需要配置队列。


2022-03-30 laravel , queue , email , mailer , email-service , mail-server

Laravel 学习笔记:发送邮件

在我最初的设计中一共有两个地方需要发送邮件:

  • 第一就是用户注册,发送邮件激活
  • 第二就是当用户订阅一个书单的时候,自动给所有订阅的用户发送带有附件的电子书到其设定的邮箱中

所以接下来就记录一下使用 Laravel 发送邮件。

在 Laravel 中发送邮件并不是那么复杂。Laravel 通过 Symfony Mailer 实现了一套非常简洁的邮件 API。

Laravel 中提供了很多种方式来发送邮件:

  • [[SMTP]] 直接配置 SMTP服务器
  • [[Mailgun]] 通过 Mailgun 等发送邮件的服务提供商
  • [[Postmark]]
  • [[Amazon SES]]
  • sendmail

env 配置文件中选择使用哪个邮件发送方式。

配置

Laravel 邮件服务可以通过 config/mail.php 来配置。每一个邮件配置都有一个唯一 transport,这也就意味着你的应用可以使用不同的服务来发送不同的邮件。比如说你的应用可以使用 Postmark 来发送交易邮件,然后使用 Amazon SES 来批量发送营销邮件。

mail 配置文件中可以发现 mailers 配置数组,这个数组中可以配置不同的邮件服务。default 用来指定默认的邮件发送服务。

因为默认生成的项目在 mail 配置中默认使用了 SMTP,然后使用来环境变量,所以我们可以在 .env 配置文件中配置:

MAIL_DRIVER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=mygoogle@gmail.com
MAIL_PASSWORD=<your_password>
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=mygoogle@gmail.com
MAIL_FROM_NAME="${APP_NAME}"

在 example 中,Laravel 给我们提供了一个测试的 [[mailhog]] MTA,可以用来在本地进行调试。

MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

创建邮件

创建类 BookSendMail 来模拟邮件发送。

php artisan make:mail BookSendMail

文件会创建到 app/Mail 目录下。

可以看到其中一个 build() 方法就是入口方法。

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class BookSendMail extends Mailable
{
    use Queueable, SerializesModels;

    public $detail;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct($detail)
    {
        $this->detail = $detail;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->subject('This is subject')->view('mail.bookSendMail');
    }
}

创建 Blade View

接下来就是创建一个 Blade 模板文件,邮件的正文部分。

文件创建在 resources/views/mail/bookSendMail.blade.php

<!DOCTYPE html>  
<html lang="en">  
<head>  
 <title>name: </title>  
</head>  
<body>  
<h1></h1>  
<p></p>  
  
<p>Thank you</p>  
</body>  
</html>

Add Route

Route::get('send-mail', function () {
    $detail = [
        'title' => 'Mail from Laravel',
        'body' => 'This is a test mail from Laravel',
    ];

    Mail::to('demo@example.com')->send(new \App\Mail\BookSendMail($detail));

    dd("Email sent");
});

然后调用 http://localhost:8080/send-mail,即可。

如果本地用了官方的 Sail 启动,可以访问 http://localhost:8025/ Mailhog 的后台,就可以看到发送的邮件了。

如果测试没有问题,那就可以直接把正式的 SMTP 配置修改到 .env 文件中。

Failover 配置

你可以配置一个额外的邮件服务来防止你的主邮件服务宕机后造成的服务不可用,配置 failover 在主服务失败之后切换到备份邮件服务器。

为了实现这个目的需要在 mail 配置文件中使用 failover transport。failover 配置也需要是一个数组。

'mailers' => [
    'failover' => [
        'transport' => 'failover',
        'mailers' => [
            'postmark',
            'mailgun',
            'sendmail',
        ],
    ],
 
    // ...
],

一旦定义了 failover 邮件,你需要设置:

'default' => env('MAIL_MAILER', 'failover'),

产生 Mailables

在 Laravel 中,每一种类型的邮件可以用 mailable 类来表现,这个类保存在 app/Mail 文件夹中。

可以使用 make:mail 来产生:

php artisan make:mail OrderShipped

产生的 mailable 中最要的一个方法就是 build(),在这个方法中可以调用不同的方法,比如说 from, subject, view, attach 来定义邮件的形式。

配置发送者

使用 mailable 中的方法:

public function build()
{
    return $this->from('example@example.com', 'Example')
                ->view('emails.orders.shipped');
}

或者使用全局 from 配置,可以在 config/mail.php 中配置

'from' => ['address' => 'example@example.com', 'name' => 'App Name'],

定义 View

mailable 类的 build 方法中,你可以使用 view 方法来指定邮件内容。

每一个邮件都可以使用 Blade 模板引擎来渲染其内容。

public function build()
{
    return $this->view('emails.orders.shipped');
}

Blade 模板可以存放在 resources/views/emails 下。

发送带附件的邮件

可以使用 attach 方法:

public function build()
{
    return $this->view('emails.orders.shipped')
                ->attach('/path/to/file');
}

使用 Markdown 引擎 Mailables

如果想要使用 Markdown template ,可以使用 --markdown 选项:

php artisan make:mail OrderShipped --markdown=emails.orders.shipped

然后在 Mailable 中可以使用:

public function build()
{
    return $this->from('example@example.com')
                ->markdown('emails.orders.shipped', [
                    'url' => $this->orderUrl,
                ]);
}

Markdown mailables 使用 Blade components 和 Markdown 语法的组合,允许用户快速构建邮件内容:

@component('mail::message')
# Order Shipped
 
Your order has been shipped!
 
@component('mail::button', ['url' => $url])
View Order
@endcomponent
 
Thanks,<br>

@endcomponent

发送邮件方法

Mail facade 有一个 to 方法,接受一个邮件地址,或者一个用户实例,或者一组用户。

如果你传入一个对象,mailer 会自动使用其 emailname 属性来决定目的地址。

    public function store(Request $request)
    {
        $order = Order::findOrFail($request->order_id);
 
        // Ship the order...
 
        Mail::to($request->user())->send(new OrderShipped($order));
    }

发送邮件时指定邮件服务提供方,默认情况 Laravel 发送邮件会使用默认配置的 default,如果要指定 mailer 可以:

Mail::mailer('postmark')
        ->to($request->user())
        ->send(new OrderShipped($order));

reference


2022-03-29 laravel , email , sendmail , mailer , mailgun , smtp

DMARC 报告

在搭建了自己的邮件服务器之后,经常收到 Gmail 和 Outlook 的 Report,类似:

Report domain: example.com Submitter: google.com Report-ID: 73941XXXXX

Report Domain: example.com Submitter: protection.outlook.com Report-ID: 200aa9XXXXXXXXXX

所以再整理一下 DMARC 报告。

在之前介绍 DMARC 的文章中介绍过其中 ruaruf 两个配置的作用,这两个配置分别用来配置邮箱地址,用来接收 DMARC aggregate report 和 DMARC Failure report。

DMARC aggregate report

默认情况下,邮件服务提供商,比如 Gmail,Outlook 等等,会每隔 24 小时发送一次 DMARC aggregate report 到 rua 指定的邮件地址。每一份报告中都写明了一段时间内 DMARC 的验证次数和合规问题。

DMARC 报告中提供了该域名下邮件发送整体情况的表现。如果有人伪装该域名进行钓鱼邮件的发送,那么也会在报告中有所体现。

如果你是一个邮件服务器的管理员,你马上就会被收件箱中的邮件无数的 DMARC report 邮件所淹没。根据邮件的不同使用情况,每一天都会收到成百上千的 reports。

在附件里面是两份从 Google 和 Outlook 收到的 report。可以看到格式是 XML 格式,其中包含了验证的次数,验证的结果等等信息。

对比

下面是 DMARC aggregate 报告和 DMARC failure 报告的对比:

  • aggregate 报告提供一组邮件的聚合数据,failure 报告提供单一邮件的详细信息;
  • 要接收 aggregate 报告,设置 rua 标签;要接收 failure 报告,设置 ruf 标签;
  • aggregate 报告不是实时的:在缺省的情况下,这些报告被每天发送;failure 报告则在 DMARC 验证失败的情况下几乎被立即发送;
  • aggregate 报告使用 XML 格式,failure 报告使用普通文本格式;
  • aggregate 报告不包含可用来辨认个人的信息 (personally identifiable information, PII),比如接收者邮件地址;failure 报告包含 PII;
  • 所有支持 DMARC 的邮箱服务提供商都支持 aggregate 报告,然而仅有一些邮箱服务提供商支持 failure 报告。

附录

Google 的报告:

<?xml version="1.0" encoding="UTF-8" ?>
<feedback>
  <report_metadata>
    <org_name>google.com</org_name>
    <email>noreply-dmarc-support@google.com</email>
    <extra_contact_info>https://support.google.com/a/answer/2466580</extra_contact_info>
    <report_id>73941XXXX1096975</report_id>
    <date_range>
      <begin>1650499200</begin>
      <end>1650585599</end>
    </date_range>
  </report_metadata>
  <policy_published>
    <domain>example.com</domain>
    <adkim>s</adkim>
    <aspf>s</aspf>
    <p>reject</p>
    <sp>reject</sp>
    <pct>100</pct>
  </policy_published>
  <record>
    <row>
      <source_ip>200.15.100.100</source_ip>
      <count>1</count>
      <policy_evaluated>
        <disposition>none</disposition>
        <dkim>pass</dkim>
        <spf>pass</spf>
      </policy_evaluated>
    </row>
    <identifiers>
      <header_from>example.com</header_from>
    </identifiers>
    <auth_results>
      <dkim>
        <domain>example.com</domain>
        <result>pass</result>
        <selector>dkim</selector>
      </dkim>
      <spf>
        <domain>example.com</domain>
        <result>pass</result>
      </spf>
    </auth_results>
  </record>
</feedback>

Outlook 的报告:

<?xml version="1.0"?>
<feedback xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <version>1.0</version>
  <report_metadata>
    <org_name>Outlook.com</org_name>
    <email>dmarcreport@microsoft.com</email>
    <report_id>200aa9xxxxxxxxxxxx16af8b6a8bbaf5</report_id>
    <date_range>
      <begin>1650499200</begin>
      <end>1650585600</end>
    </date_range>
  </report_metadata>
  <policy_published>
    <domain>example.com</domain>
    <adkim>s</adkim>
    <aspf>s</aspf>
    <p>reject</p>
    <sp>reject</sp>
    <pct>100</pct>
    <fo>0</fo>
  </policy_published>
  <record>
    <row>
      <source_ip>200.10.106.108</source_ip>
      <count>1</count>
      <policy_evaluated>
        <disposition>none</disposition>
        <dkim>pass</dkim>
        <spf>pass</spf>
      </policy_evaluated>
    </row>
    <identifiers>
      <envelope_to>outlook.com</envelope_to>
      <envelope_from>example.com</envelope_from>
      <header_from>example.com</header_from>
    </identifiers>
    <auth_results>
      <dkim>
        <domain>example.com</domain>
        <selector>dkim</selector>
        <result>pass</result>
      </dkim>
      <spf>
        <domain>example.com</domain>
        <scope>mfrom</scope>
        <result>pass</result>
      </spf>
    </auth_results>
  </record>
</feedback>

2022-03-29 dmarc , email , spf , self-hosted , email-hosting , dns-record

Laravel 学习笔记:文件上传

上传文件是一个网页应用必不可少的一部分,这里就记录一下 Laravel 中如何上传,并展示图片。

拆分开来主要分为如下几个步骤:

  • 创建数据库 Model,用一个 Model 实体来保存上传图片的路径以及相关的 meta 信息
  • 添加 Controller 层用来处理保存图片逻辑,以及持久化的过程
  • 创建前端 Form 表单,并提交 POST 请求,提交图片

创建数据库 Model

首先使用 artisan 创建一个 Model 和 migration:

php artisan make:model Photo -m

这行命令会创建一个数据库 Migration 文件,在 database/migrations 下:

然后修改 migration 文件,创建数据库 schema:

public function up()
{
    Schema::create('photos', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('path');
        $table->timestamps();
    });
}

上面的操作便是创建了一张表叫做 photos,其中包含了 id, name, path 和时间四列

执行数据库变更,会根据数据库的配置自动创建表:

php artisan migrate

Controller 上传逻辑

创建 route,打开 web.php:

Route::get('upload-image', [UploadImageController::class, 'index']);
Route::post('save', [UploadImageController::class, 'save']);

然后创建 Controller

php artisan make:controller UploadImageController

内容:

<?php
 
namespace App\Http\Controllers;
 
use Illuminate\Http\Request;
use App\Models\Image;
 
 
class UploadImageController extends Controller
{
    public function index()
    {
        return view('image');
    }
 
    public function save(Request $request)
    {
         
        $validatedData = $request->validate([
         'image' => 'required|image|mimes:jpg,png,jpeg,gif,svg|max:2048',
 
        ]);
 
        $name = $request->file('image')->getClientOriginalName();
 
        $path = $request->file('image')->store('public/images');
 
 
        $save = new Photo;
 
        $save->name = $name;
        $save->path = $path;
 
        $save->save();
 
        return redirect('upload-image')->with('status', 'Image Has been uploaded');
 
    }
}

store 方法就把图片保存到了 images 目录中。

为了安全起见,可以修改一下文件路径:

$filename= date('YmdHi').$file->getClientOriginalName();
$file-> move(public_path('public/Image'), $filename);

Blade View

resources/views 下创建 image.blade.php,其中最重要的 form 部分:

<div class="card-body">
    <form method="POST" enctype="multipart/form-data" id="upload-image" action="" >

        <div class="row">

            <div class="col-md-12">
                <div class="form-group">
                    <input type="file" name="image" placeholder="Choose image" id="image">
                @error('image')
                    <div class="alert alert-danger mt-1 mb-1"></div>
                @enderror
                </div>
            </div>

            <div class="col-md-12">
                <button type="submit" class="btn btn-primary" id="submit">Submit</button>
            </div>
        </div>     
    </form>

</div>

2022-03-28 laravel , file-upload

Laravel 学习笔记:开发环境搭建

Laravel 提供了多种安装方式:

  • 可以通过官方提供的 [[Laravel Sail]] 初始化环境,Laravel Sail 是一个轻量的命令行工具可以和 Docker 开发环境交互。这意味着如果要使用 Sail 本地需要安装 Docker 环境。
  • 通过 [[Composer]] 安装,Composer 是一个 PHP 环境下的依赖管理器工具
  • Laravel Installer

Laravel Sail

Sail 的核心是项目中的 docker-compose.yml 文件。

curl -s "https://laravel.build/example-app" | bash

cd example-app
 
./vendor/bin/sail up

设置 alias:

alias sail="bash vendor/bin/sail"

之后就可以直接使用 sail up 命令。

一旦应用启动之后,可以访问本地:http://localhost .

不过在这里,我不清楚为什么我本地的 80 端口始终无法访问,所以我只能按照 docker-compose.yml 中的配置,将 APP_PORT 修改成 8080 端口,才能正常访问到。

Laravel Sail 启动之后会自动拉取如下组件:

  • [[MySQL]]
  • [[Redis]]
  • [[mailhog]],mailhog 是一个本地开发测试的邮件服务器,非常方便在本地测试 SMTP
  • [[meilisearch]]
  • [[Selenium]]
  • Laravel 应用本身

邮件

Laravel Sail 中包含的还包含一个邮件测试服务器 MailHog ,该服务用于在本地开发期间拦截应用发送的所有邮件并提供一个 Web 界面在浏览器中预览这些邮件信息,方便测试和调试。

    mailhog:
        image: 'mailhog/mailhog:latest'
        ports:
            - '${FORWARD_MAILHOG_PORT:-1025}:1025'
            - '${FORWARD_MAILHOG_DASHBOARD_PORT:-8025}:8025'
        networks:
            - sail

MailHog 的 SMTP 服务器的默认端口是 1025。

Sail 运行时可以通过 http://localhost:8025 访问 MailHog Web 界面。

Sail 相关命令

启动

sail up

重新构建:

sail build --no-cache

执行 php 命令:

sail php --version

Composer

如果本地电脑已经安装了 PHP 和 Composer,可以直接通过 Composer 来初始化 Laravel 项目。一旦项目创建了,可以使用 Artisan CLI 的 serve 命令启动 Laravel 本地开发服务器。

composer create-project laravel/laravel example-app
 
cd example-app
 
php artisan serve

Installer

还可以通过 Laravel 安装器来初始化项目:

composer global require laravel/installer
 
laravel new example-app
 
cd example-app
 
php artisan serve

注意,这里需要将 laravel 加入系统的环境变量。

  • macOS: $HOME/.composer/vendor/bin
  • Windows: %USERPROFILE%\AppData\Roaming\Composer\vendor\bin
  • GNU / Linux Distributions: $HOME/.config/composer/vendor/bin or $HOME/.composer/vendor/bin

开发环境

当 Laravel 安装好之后,我本地使用 JetBrains 提供的 PhpStorm,然后安装了 Laravel Idea 插件,可以按照官网提供的方式来申请开源许可。


2022-03-26 laravel , php , dev , ide , phpstorm , jetbrains , docker

电子书

本站提供服务

最近文章

  • 从 Buffer 消费图学习 CCPM 项目管理方法 CCPM(Critical Chain Project Management)中文叫做关键链项目管理方法,是 Eliyahu M. Goldratt 在其著作 Critical Chain 中踢出来的项目管理方法,它侧重于项目执行所需要的资源,通过识别和管理项目关键链的方法来有效的监控项目工期,以及提高项目交付率。
  • AI Shell 让 AI 在命令行下提供 Shell 命令 AI Shell 是一款在命令行下的 AI 自动补全工具,当你想要实现一个功能,敲一大段命令又记不住的时候,使用自然语言让 AI 给你生成一个可执行的命令,然后确认之后执行。
  • 最棒的 Navidrome 音乐客户端 Sonixd(Feishin) Sonixd 是一款跨平台的音乐播放器,可以使用 [[Subsonic API]],兼容 Jellyfin,[[Navidrome]],Airsonic,Airsonic-Advanced,Gonic,Astiga 等等服务端。 Sonixd 是一款跨平台的音乐播放器,可以使用 [[Subsonic API]],兼容 Jellyfin,[[Navidrome]],Airsonic,Airsonic-Advanced,Gonic,Astiga 等等服务端。
  • 中心化加密货币交易所 Gate 注册以及认证 Gate.io 是一个中心化的加密货币交易所。Gate 中文通常被称为「芝麻开门」,Gate 创立于 2013 年,前身是比特儿,是一家致力于安全、稳定的数字货币交易所,支持超过 1600 种数字货币的交易,提供超过 2700 个交易对。
  • 不重启的情况下重新加载 rTorrent 配置文件 因为我在 Screen 下使用 rTorrent,最近经常调试修改 rtorrent.rc 配置文件,所以想要找一个方法可以在不重启 rTorrent 的情况重新加载配置文件,网上调查了一下之后发现原来挺简单的。