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
然后需要手动添加到 EventServiceProvider
的 boot
方法中。
EventServiceProvider
中的 $listen
数组配置了监听器:
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
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。
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));
来触发事件。
如果你要在 Listener 中执行一些繁重的操作,那么可以使用 Queued Event Listener
:
在 Listener
上指定实现 ShouldQueue
,然后记得配置好队列。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
//
}
这样,当一个事件发生后,Listener 会自动被添加到队列中。
可以调用 Events
的 dispatch()
方法。
当需要构建一个网络应用的时候,可能有一些任务,比如解析、存储、传输 CSV 文件等等,可能需要花费较长的时间。Laravel 提供了一个非常简单的队列 API,可以让这些操作可以在后台进行。让这些繁重的任务在后台执行可以有效的提高应用的响应速度,提升用户使用体验。
Laravel 队列提供了一个统一的 API 访问入口,可以支持不同的队列:
Laravel 队列的配置在 config/queue.php
中。
Laravel 还提供了一个 Redis 队列的 Dashboard 叫做 Horizon。但是这一篇文章不会涉及到 Horizon 相关内容。
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
如果使用数据库作为队列驱动,那么需要创建一张表来存储队列任务。运行 queue:table
来创建表:
php artisan queue:table
php artisan migrate
最后不要忘了给应用配置 database
驱动:
QUEUE_CONNECTION=database
如果消息队列使用 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 中拉取数据。
如果要使用其他队列,需要安装其他依赖:
aws/aws-sdk-php ~3.0
pda/pheanstalk ~4.0
predis/predis ~1.0
or phpredis PHP extension默认所有的队列任务存放在 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();
}
有些时候期望只有一个实例任务会被放到队列中,可以实现 ShouldBeUnique
:
<?php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
...
}
上面的例子中,UpdateSearchIndex
任务是唯一的,这就保证了如果队列中的任务没有完成,就不会有新的任务被加入进去。
可以通过 uniqueId
和 uniqueFor
属性来特别指定想要的主键:
<?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
。
需要在 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 机制,定义一个重试时间。
通常情况下如果一个任务抛出了异常,任务会马上重试。
一旦写好了任务类,就可以使用 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 允许你指定一组任务,这些任务应该按照顺序依次执行,如果一个任务失败了,接下来的任务就不会执行。
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();
向特定队列分发任务。
<?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 交互,可以使用 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');
}
Laravel 包括了一个 Artisan 命令可以用来开始一个队列的 worker,开始处理新的任务。
可以使用 queue:work
命令:
php artisan queue:work
为了使得 queue:work
命令常驻后台,可以使用进程管理器 Supervisor。
注意,queue workers 会将应用保存到内存中。这也就意味着代码的改动不会立即生效。在开发的过程中,注意重启 queue workers。
或者可以执行 queue:listen
命令
php artisan queue:listen
该命令只建议在开发过程中使用。
安装 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 提供了很多方式了定义任务可以重试的次数,一旦次数达到,任务就会被放到失败队列。
当一个任务失败时,你可能希望执行一些操作,比如发送通知,或者更新一些数据,这个时候可以定义 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 之前需要配置队列。
在我最初的设计中一共有两个地方需要发送邮件:
所以接下来就记录一下使用 Laravel 发送邮件。
在 Laravel 中发送邮件并不是那么复杂。Laravel 通过 Symfony Mailer 实现了一套非常简洁的邮件 API。
Laravel 中提供了很多种方式来发送邮件:
在 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 模板文件,邮件的正文部分。
文件创建在 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>
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 在主服务失败之后切换到备份邮件服务器。
为了实现这个目的需要在 mail
配置文件中使用 failover
transport。failover
配置也需要是一个数组。
'mailers' => [
'failover' => [
'transport' => 'failover',
'mailers' => [
'postmark',
'mailgun',
'sendmail',
],
],
// ...
],
一旦定义了 failover 邮件,你需要设置:
'default' => env('MAIL_MAILER', 'failover'),
在 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'],
在 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 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 会自动使用其 email
和 name
属性来决定目的地址。
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));
在搭建了自己的邮件服务器之后,经常收到 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 的文章中介绍过其中 rua
和 ruf
两个配置的作用,这两个配置分别用来配置邮箱地址,用来接收 DMARC aggregate report 和 DMARC Failure report。
默认情况下,邮件服务提供商,比如 Gmail,Outlook 等等,会每隔 24 小时发送一次 DMARC aggregate report 到 rua
指定的邮件地址。每一份报告中都写明了一段时间内 DMARC 的验证次数和合规问题。
DMARC 报告中提供了该域名下邮件发送整体情况的表现。如果有人伪装该域名进行钓鱼邮件的发送,那么也会在报告中有所体现。
如果你是一个邮件服务器的管理员,你马上就会被收件箱中的邮件无数的 DMARC report 邮件所淹没。根据邮件的不同使用情况,每一天都会收到成百上千的 reports。
在附件里面是两份从 Google 和 Outlook 收到的 report。可以看到格式是 XML 格式,其中包含了验证的次数,验证的结果等等信息。
下面是 DMARC aggregate 报告和 DMARC 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>
上传文件是一个网页应用必不可少的一部分,这里就记录一下 Laravel 中如何上传,并展示图片。
拆分开来主要分为如下几个步骤:
首先使用 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
创建 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);
在 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>
Laravel 提供了多种安装方式:
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 启动之后会自动拉取如下组件:
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 up
重新构建:
sail build --no-cache
执行 php 命令:
sail php --version
如果本地电脑已经安装了 PHP 和 Composer,可以直接通过 Composer 来初始化 Laravel 项目。一旦项目创建了,可以使用 Artisan CLI 的 serve
命令启动 Laravel 本地开发服务器。
composer create-project laravel/laravel example-app
cd example-app
php artisan serve
还可以通过 Laravel 安装器来初始化项目:
composer global require laravel/installer
laravel new example-app
cd example-app
php artisan serve
注意,这里需要将 laravel
加入系统的环境变量。
$HOME/.composer/vendor/bin
%USERPROFILE%\AppData\Roaming\Composer\vendor\bin
$HOME/.config/composer/vendor/bin
or $HOME/.composer/vendor/bin
当 Laravel 安装好之后,我本地使用 JetBrains 提供的 PhpStorm,然后安装了 Laravel Idea 插件,可以按照官网提供的方式来申请开源许可。
从上周开始和朋友进行一个以 21 天为一个周期的计划,每个人都制定了一个 21 天的目标,从计划到完成每一个步骤都计分,最后按照打分给每个小伙伴奖励或者惩罚。
所以我从我的计划列表中搜寻了一下大致看了一些 TODO,很多细碎的任务大都花不了一两天时间,所以就思考了一下有没有什么目标适合这个时间段,后来发现最近自建的一些项目好像都是用一个框架写成的,比如有一个可以自建的 PT 站 [[UNIT3D]],比如非常轻量的论坛 [[Flarum]],还有 [[Koel]] 一个在线的音乐播放器,还有很多很多。所以想来 21 天可以用来熟悉一下这个框架,也可以用来快速实现一些想法。所以就有了这个开篇。
[[laravel]] 是一个 PHP 编写的 Web 框架,如果类比的话,那 Laravel 之于 PHP,就像 Spring 生态之于 Java。
我学习 Laravel 的目标有两个:
学习计划非常简单,我提前看了一下 Laravel 的官方文档,非常详细,几乎包括了这个框架用户要了解的所有内容。所以我按照官网的组织结构,列了一些重点。
[[laravel 21天挑战计划]]
[[2022-03-29-laravel-send-email | Laravel 学习笔记:发送邮件]] |
官方:
准备和文档一起交叉阅读:
YouTube 上有非常多的视频,但我认为现阶段我只需要注重在官方文档即可。如果实在遇到无法实践的再寻求 YouTube,比如之前不知道怎么调试 PHP,找了半天文字材料都不行之后才去寻求视频,毕竟视频材料还是效率比较低的。
Warp 是一个 Rust 编写,使用 GPU 渲染的终端(terminal)应用。目标是提升开发者的效率。
最近 Warp 发布了新闻稿,筹集了 2300 万美元的资金全力用来构建这个终端。
它之前筹集了 600 万美元的种子轮融资,由 GV 领投,Neo 和 BoxGroup 参投。还筹集了 1700 万美元的 A 轮融资,由 Figma 的联合创始人兼首席执行官 Dylan Field 领投。由企业家主导的这一轮投资,其参与者包括 Elad Gil、LinkedIn 前首席执行官 Jeff Weiner 和 Salesforce 的联合创始人兼联合首席执行官 Marc Benioff。
上手体验的过程也可以知道,Warp 基本上没什么配置,基本上所有的功能都是内建快捷键调用,对新手非常友好。
早在之前的文章中我就 提到过 因为使用越来越多的 zsh 插件,导致每一次打开新的 Tab 终端都要有一个明显的停顿,虽然后来使用 zinit 优化了一下插件加载,但是还是会感觉到一小点的卡顿,希望 Warp 能解决这个问题吧。
通过终端自身的能力,可以精减掉一些 zsh 的插件。
Warp 的创始人 Zach Lloyd 说过,走过任何一个开发人员的桌面,都会看到一个打开的终端,还有代码编辑器,VS Code 完成了代码编辑器的重新定义,Warp 就去重新定义终端。
Warp 内置的命令自动补全,还有尚且不是很完善的 AI 搜索都是朝着智能化的方向前进的。GitHub Copilot 已经让我非常惊讶其代码自动补全的能力,我相信 Warp 未来在 AI 自动补全方面也会令人耳目一新。
Warp 的创始人在总结其过去 20 年的程序生涯的时候说过现存的终端存在的两个痛点:
想来和他自身作为 Google Docs 的开发有着非常大的关系。当前的任何桌面程序如果加上了多人协作都是一个非常大的想象空间。那为什么终端不行?
在 Warp 中用户可以共享自己的命令行,设置,和历史。
可以点击 这里 下载体验。
如果在国内,大概率会卡在登录的界面(在还没有开放 beta testing 的时候会卡在输入邀请码的界面)。
这个时候需要本地其一个 1080 端口的代理,然后使用代理来启动 Warp:
export HTTP_PROXY=http://127.0.0.1:1080
export HTTPS_PROXY=http://127.0.0.1:1080
/Applications/Warp.app/Contents/MacOS/stable
在 macOS 上推荐使用 Clash for windows 。
ctrl
+ shift
+ r
可以从社区或者自定义的命令中搜索并执行长命令cmd
+ p
非常常见的 Command Palette,现在很多应用都使用这个涉及,我现在在用的 Obsidian 也是,可以调用出应用内置的命令ctrl
+ r
搜索历史cmd
+ d
切分窗口ctrl
+ cmd
+ t
用来切换主题!更多的快捷键可以在设置中查看。
Warp 自带了命令的补全,效果还不错。
Warp 提供了对于命令结果的快速复制,以及分享,点击 Share 可以非常轻松的把命令结果分享给别人。
比如对于我上面的 git --help
的结果可以到 这里查看
当然如果没有 Warp,我会在 Tmux 中用 Tmux 的复制粘贴来实现这个需要。
fe
就是 fuzzy find 当前目录,然后输入模糊搜索词,回车之后就会立即用 vim 编辑,但是在 Warp 中并不能用 fzf。当然这可能和 fe 使用的 zsh 脚本有关系,有时间再看看怎么解决。
which fe
fe () {
local files
IFS=$'\n' files=($(fzf-tmux --query="$1" --multi --select-1 --exit-0))
[[ -n "$files" ]] && ${EDITOR:-vim} "${files[@]}"
}
Warp 不需要用户查看文档也可以有一个不错的上手体验,更不像其他终端那样需要一个非常复杂的配置文档才能将其调教得比较好用。
如果你看完这篇文章觉得 Warp.dev 还不错,可以点击 我的邀请 来尝试一下。
一台新的 Ubuntu 服务器通常时区可能不是想要的时区,可以通过如下步骤设定时区。
检查当前时区,在命令行下输入 date
:
date
可以查看当前的时间。
输入 timedatectl
可以查看更具体的时区。
修改为东八区北京时间。
sudo timedatectl set-timezone "Asia/Shanghai"
也可以通过软链接来修改系统的时区,在 Linux 下 /etc/localtime
中定义了系统要使用的时区。正确的配置在 /usr/share/zonefine
目录中
mv /etc/localtime /etc/localtime-backup
ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
修改完成后可以通过 timedatectl
来验证。
DMARC 是 Domain-based Message Authentication Reporting & Conformance 的缩写,是一个标准的电子邮件验证标准。1 DMARC 帮助邮箱管理员防止黑客或其他攻击者伪装(Spoofing)其组织和域名。Spoofing(伪装)是一种电子邮件攻击方式,攻击者通过伪装邮件地址中的 From 字段,来假装发件人。DMARC 会检查电子邮件是否来自邮件中声称的发送者。
DMARC 构建在 [[SPF]] 和 [[DKIM]] 基础之上,来防止域名欺诈。
之前提到过自建域名邮箱 的文章中就配置过 DMARC,这篇文章就对 DMARC 具体展开讲讲。
DMARC 建立在广泛使用的 SPF 和 DKIM 协议上, 并且添加了域名对齐检查和报告发送功能。这样可以有效保护域名免受钓鱼攻击。
来自 dmarc.org 的示意图:
根据 dmarc.org 的说法:
随着社交网络和电子商务的繁荣,垃圾邮件发送者和钓鱼攻击发起者基于利益的原因,想要入侵用户的账户,破解用户的信用卡等。Email 的相对容易攻击的特性备受罪犯们的青睐。只是简单地把企业的 logo 嵌入到 email 中,就能获取用户的信任。
用户很难辨别一封假的 email,邮件提供商也很难判断哪些邮件有可能会伤害用户。邮件发送者基本上对邮件认证的问题一无所知,因为他们缺少合理的反馈机制。那些尝试部署 SPF 和 DKIM 的企业的进展非常慢,因为没有监督进度和除错的机制。
DMARC 解决了这些问题。它帮助 email 发送者和接收者来共同保护 email,避免了昂贵的入侵损失。
DMARC 记录作为 TXT 记录发布到 DNS 中。指示邮件收件服务当验证失败时,应当如何处理收到的邮件。
比如以下发布在域名 “sender.exampledomain.com” 上的 DMARC 记录:
v=DMARC1;p=reject;pct=100;rua=mailto:postmaster@exampledomain.com
在这个例子中表示发件人要求收件人如果遇到验证未通过的邮件,则完全拒绝,并且发送相关报告到 postmaster@exampledomain.com
。如果发件人在测试配置,”reject” 可以被替换成 “quarantine”,表示验证未通过的 邮件将被隔离。
DMARC 记录使用可扩展的 “标签-值” 语法。
这是一个典型的 DMARC 记录:
v=DMARC1; p=none; rua=mailto:dmarc_report@example.com; ruf=mailto:dmarc_report@example.com; pct=100; adkim=s; aspf=s;
它由多个 key-value 标签组成。这些标签会告诉邮件服务提供商如何发送 DMARC 报告:
v
是 DMARC 协议版本,它的值必须是 DMARC1
p
是策略,策略将被应用到验证失败的邮件上,可以设置成 ‘none’, ‘quarantine’, 或者 ‘reject’
rua
是一组用来接收报告的电子邮件地址(DMARC aggregate)ruf
是一系列用来接受失败报告的电邮地址(DMARC failure)pct
对失败邮件应用策略的百分比adkim
制定 DKIM 的对齐策略,可选 s
或 r
aspf
制定 SPF 的对齐策略,可选 s
或 r
如果不想配置报告失败的邮箱,可以
v=DMARC1; p=none;, p=reject;, p=quarantine;
DMARC 策略是在 DMARC 记录中制定的 p 标签。它指示邮件服务提供商应该如何处理验证失败的邮件。
DMARC 策略可以取三个值中的一个:none (monitor), quarantine 和 reject。
– none: 邮件提供商对验证失败邮件不采取任何处理。这个模式用来收集 DMARC 报告。 – quarantine: 邮件提供商把验证失败邮件放到垃圾邮件文件夹。 – reject: 邮件提供商拒收验证失败邮件。
DMARC 对齐策略。DMARC 通过检验 header 部分是否和发送的域名相匹配,这个动作称为 alignment
,这个部分验证依赖 SPF 和 DKIM。
用户可以在 DMARC 中配置两种 alignment 模式,strict
和 relaxed
。上面提到的 aspf
和 adkim
选项就是这个作用。s
和 r
分别是两个缩写。
验证方式 | Strict | Relaxed |
---|---|---|
SPF | SPF 验证域名,和邮件 Header 中 From 一致 | Header 中的 From 必须匹配域名或子域名 |
DKIM | From 中的地址和 DKIM 配置一致 | From 必须匹配 DKIM signature d= 后配置的域名或子域名 |
Envelope sender address: 是 Return-Path 头的邮件地址,收件人是看不到该地址的
From: 地址,收件人看到的地址
SPF alignment example
Envelop sender address | Header From | strict | relaxed |
---|---|---|---|
admin@example.com | admin@example.com | Pass | Pass |
admin@mail.example.com | admin@example.com | Fail | Pass |
admin@example.com | admin@example.org | Fail | Fail |
DKIM alignment example
DKIM signature domain 中的 d=
后面部分。
From | DKIM d= | strict | relaxed |
---|---|---|---|
admin@example.com | example.com | Pass | Pass |
admin@mail.example.com | example.com | Fail | Pass |
admin@example.org | example.com | Fail | Fail |
发布 DMARC 记录需要你有权限修改域名的 DNS 记录。
在 DNS 记录后台,需要创建一条 DMARC 记录。
创建一条 TXT 记录:
Type: TXT Host: _dmarc TXT Value: (DMARC record generated above) TTL: 1 hour
以 Cloudflare 后台为例:
如果用其他的域名服务的话,界面类似。
保存更改。现在你已经在域名 yourdomain.com 上面成功发布 DMARC 记录了。
如果不知道怎么写 DMARC 记录也可以在这个网站 通过表单的形式生成。