# 前言
本篇為 Laravel 的學習筆記, 主要將看到的, 學到的技術轉換成 Q&A 的方式以加速學習
# Production 優化
# PHP
php.ini 文件
php.ini 中的 memory_limit
用於設定單個 PHP process 可以使用的系統內存最大值
# Questions and Answers
以下的 Laravel example code 的意思是?
Example:
<?php
public function store()
{
$attendee = Attendee::create(
[
'conference_id' => $conference->id,
'name' => request('name'),
'reference' => $reference = Str::uuid() // ...
]);
$invoice = BillingProvider::invoice([
'customer_reference' => $reference, 'card_token' => request('card_token'), // ...
]);
SendTicketInformation::dispatch($attendee);
}Answer:
在 transaction 內, 建立 attendee 並 bill, 如果失敗則回滾並 throw exception, 如果都成功, 則 dispatch SendTicketInformation job
以下的 Laravel example code 的意思是?
Example:
<?php
public function store()
{
$attendee = Attendee::create(
[
'conference_id' => $conference->id,
'name' => request('name'),
'reference' => $reference = Str::uuid() // ...
]);
$invoice = BillingProvider::invoice([
'customer_reference' => $reference, 'card_token' => request('card_token'), // ...
]);
SendTicketInformation::dispatch($attendee);
}Answer:
在 transaction 內, 建立 attendee 並 bill, 如果失敗則回滾並 throw exception, 如果都成功, 則 dispatch SendTicketInformation job
以下的 Laravel example code 的意思是?
Example:
<?php
Bus::batch($jobs)->allowFailures()
->then(...)
->catch(...)->finally(function ($batch) {
$conference = Conference::firstWhere('refunds_batch', '=', $batch->id);
Mail::to($conference->organizer)->send(
'Refunding attendees completed!'
);
})->dispatch();Answer:
當所有 job 都執行完畢, 儘管有些 fail, 有些 succeed, 那便會觸發 finally(), 通知 organizer
以下的 Laravel example code 的意思是?
Example:
<?php
Bus::batch($jobs)
->allowFailures()
->then(...)
->catch(function ($batch, $e) {
$conference = Conference::firstWhere('refunds_batch', '=', $batch->id);
Mail::to($conference->organizer)->send(
'We failed to refund some of the attendees!'
);
})
->dispatch();Answer:
當首次有 job fails 時, 會執行 cache() 內的 closure, 通知 organizer, 如果有 job fails 就不會觸發 then()
以下的 Laravel example code 的意思是?
Example:
<?php
Bus::batch($jobs)->allowFailures()->then(function ($batch) {
$conference = Conference::firstWhere('refunds_batch', '=', $batch->id);
Mail::to($conference->organizer)->send(
'All attendees were refunded successfully!'
);
})->dispatch();Answer:
當 batch 內所有的 job 都 successfully executed, 會執行 then() 內的 closure, 發 mail 通知 organizer
以下的 Laravel example code 的意思是?
Example:
<?php
$batch = Bus::findBatch($conference->refunds_batch);
return [
'progress' => $batch->progress().'%',
'remaining_refunds' => $batch->pendingJobs,
'has_failures' => $batch->hasFailures(),
'is_cancelled' => $batch->canceled()
];Answer:
取得 batch 目前的整體進度 (1-100, int)
取得目前 pending jobs 的數量 (int)
取得是否有 failed job (boolean)
取得該 batch 是否 cancelled (boolean)
以下的 Laravel example command 的意思是?
Example:
php artisan queue:retry-batch {batch_id}
Answer:
使用 CLI retry 指定 batch 中的 failed jobs
以下的 Laravel example code 的意思是?
Example:
<?php
$batch = Bus::batch($jobs)->allowFailures()->dispatch();Answer:
當使用 batch dispatch jobs, 預設如果其中一個 job fails, 那該 batch 就會終止, 使用 allowFailures(), 即使有 job fails 該 batch 也會 dispatch 其他 job
以下的 Laravel example code 的意思是?
Example:
<?php
$batch = Bus::findBatch($conference->refunds_batch);
$batch->cancel();
// in job class
use Batchable;
public function handle()
{
if ($this->batch()->canceled()) {
return;
}
// Actual refunding code here ...
$this->attendee->update([
'refunded' => true
]);
}Answer:
從存在資料庫的 batch_id 取得該 batch, 並執行 cancel(), 該 batch 會被 marked as cancelled
在 job handle() 中, 判斷如果該 job 的 batch 已經被 marked as cancelled, 那立即 return 該 job, 若否則繼續執行
藉此當我們 mark batch as cancelled 之後, 所以在 queue 中尚未執行的 job 都會被 cancelled
以下的 Laravel example command 的意思是?
Example:
php artisan queue:batches-table
Answer:
Laravel 會將 batch 資料存在 table, 因此需要建立該 table
以下的 Laravel example code 的意思是?
Example:
<?php
use Illuminate\Bus\Batchable;
class RefundAttendee implements ShouldQueue
{
use Batchable;
}Answer:
若要 job 可被 batch dispatch, 需 useBatchable
trait
以下的 Laravel example code 的意思是?
Example:
<?php
$jobs = $this->conference->attendees->map(function ($attendee) {
return new RefundAttendee($attendee);
});
$batch = Bus::batch($jobs)->dispatch();
$this->conference->update([
'refunds_batch' => $batch->id
]);Answer:
使用 batch(), 一次性的 queue 複數的 jobs, 相當於用一句 command 將複數的 job push 到 queue, 而不是分成多句 command
並將 batch id 儲存在資料庫
以下的 Laravel job example 中, 要是 worker 在取得 lock 之後 crash 了, 那會發生什麼事?
Example:
<?php
public function handle()
{
$invoice = $this->attendee->invoice;
Cache::lock('refund.'.$invoice->id)
->get(function() {
if (! $invoice->wasRefunded()) {
$invoice->refund();
}
Mail::to($this->attendee)->send(...);
});
}Answer:
因為 lock 並沒有設定持有秒數, 所以會永遠持有, 那當該 job reserved tag 被移除後, 其他 worker 會嘗試 pick up 該 job, 但會卡在 lock 處, 直到 $timeout, 最後耗盡 $tries 次數
以下的 Laravel example code 的意思是?
Example:
<?php
class RefundAttendee implements ShouldQueue
{
public $tries = 3;
public $timeout = 60;
public $backoff = 11;
private $attendee;
public function __construct(Attendee $attendee)
{
$this->attendee = $attendee;
}
public function handle()
{
$invoice = $this->attendee->invoice;
Cache::lock('refund.'.$invoice->id, 10)
->get(function() {
if (! $invoice->wasRefunded()) {
$invoice->refund();
}
Mail::to($this->attendee)->send(...);
});
}
}
// wasRefunded
public function wasRefunded()
{
$response = HTTP::timeout(5)->get('../invoice/'.$id)->throw()
->json();
return $response['invoice']['status'] == 'refunded';
}Answer:
使用 lock, 這樣當不小心 dispatch 同一個 job 兩次時, 也不用擔心 worker 會執行兩次
設定 lock 秒數, 如果沒設定, 假如 worker obtain lock 之後 crash, 那該 job 將永遠不會被執行, 直到 $tries 耗盡, 因為 lock 並沒有釋放
$backoff 設定 11 秒, 以避免 lock 10 秒期間 worker 不停的嘗試 obtain lock
在真正 refund 之前, 都呼叫 billing provider 以確定該 invoice 是否已經 refunded, 以避免 refund 兩次
以下的 Laravel example code 的意思是?
Example:
<?php
// on controller
$this->conference->attendees->each(function ($attendee) {
RefundAttendee::dispatch($attendee);
});
// on job class
class RefundAttendee implements ShouldQueue
{
public $tries = 3;
public $timeout = 60;
private $attendee;
public function __construct(Attendee $attendee)
{
$this->attendee = $attendee;
}
public function handle()
{
$this->attendee->invoice->refund();
Mail::to($this->attendee)->send(...);
}
}
// worker
php artisan queue:work --queue=cancelations,default
php artisan queue:work --queue=default,cancelationsAnswer:
假如一場 conference 忽然要取消, 我取消 refund 這場 conference 中的每一個 attendee, 可將 refund 作業分拆成多個 small job
使用兩個 worker, 定義不同的 priority 以避免 starvation 的狀況
以下的 Laravel example code 的意思是?
Example:
<?php
'connections' => [
'database' => [
'driver' => 'database',
'retry_after' => 60,
],
'database_long_running' => [
'driver' => 'database',
'retry_after' => 18060,
],
],Answer:
retry_after 會將 job 的 reserved tag 移除, 以避免 worker crash 後該 job 始終保持 reserved 狀態而永遠不被執行
但一個 connection 只能設定一個 retry_after, 所以如果有job 運行時間會比較久
的需求時, 很可能需要第二個 connection
或是盡可能地避免這種狀況, 將需要運行比較久的 job 分拆成多個 job
以下的 Laravel example code 的意思是?
Example:
<?php
// job
class CancelConference implements ShouldQueue
{
public $timeout = 18000;
}
// config/queue.php
'connections' => [
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 18060,
],
],Answer:
假如該 job timeout, 給 worker 60 秒的時間完成清理工作, 不管如何, 18060 秒之後, 該 job 都會被移除reserved
tag
以下的 Laravel example code 的意思是?
Example:
<?php
class ProvisionServer implements ShouldQueue
{
private $server;
private $payload;
public $tries = 20;
public $maxExceptions = 3;
public function __construct(Server $server, $payload)
{
$this->server = $server;
$this->payload = $payload;
}
public function handle()
{
if (!$this->server->forge_server_id) {
$response = Http::timeout(5)->post(
'.../servers', $this->payload)->throw()->json();
$this->server->update([
'forge_server_id' => $response['id']
]);
return $this->release(120);
}
if ($this->server->stillProvisioning($this->server)) {
return $this->release(60);
}
$this->server->update([
'is_ready' => true
]);
}
public function failed(Exception $e)
{
Alert::create([
// ...
'message' => "Provisioning failed!"
]);
$this->server->delete();
}
}Answer:
如果沒有 forge_server_id, 代表 Forge server 尚未建立, 所以呼叫 Forge API, 建立 server 並取得 server id, 將 job 丟回 queue, 指定 120 秒後再執行一次
如果 server 處於 provisioning 狀態, 丟回 queue, 60 秒後再執行一次
建立完成後, 更新 Server model 中的 id_ready 為 true
該 job 可被執行最多達 20 次, 但如果是 exception, 最多 3 次
如果失敗, 建立 Alert 並刪除此筆 server record
以下位於 config/queue.php 的 Laravel example code 的意思是?
Example:
<?php
'connections' => [
'database' => [
'driver' => 'database',
// ...
'retry_after' => 90,
],
],Answer:
當一個 worker pick up 一個 job 時, 會在這個 job markreserved
, 這樣其他 worker 就不會嘗試 pick up 這個 job, 但當一個 worker 在執行這個 job 時 crash 了, 會導致這個 job 的reserved
mark 不會被清除, 導致這個 job 永遠不會被執行。 Laravel 為了預防這一點, 定義了一個 job 最長可以處於reserved
state 多久, 就是 retry_after
Laravel Queue 中, 當一個 worker 執行一個 job 時, 為何其他 worker 不會執行同一個 job?
因為當一個 worker pick up 一個 job 時, 會 mark job as reserved
Laravel Queue 中, 當使用 job 的 retry_until = 120, 代表該 job 在 120 秒後就會立即被執行嗎?
不是哦, 這只代表該 job 在 release 或 dispatch 後的 120 秒內不會被執行, 如果 worker 都很忙, 10 分鐘後才被執行也是有可能的
以下的 Laravel example code 的意思是?
Example:
<?php
public $tries = 0;
public function handle()
{
$response = Http::timeout(...)->post(...);
if ($response->failed()) {
$this->release(
now()->addMinutes(15 * $this->attempts()));
}
}
public function retryUntil()
{
return now()->addDay();
}
public function failed(Exception $e)
{
Mail::to($this->integration->developer_email
)->send(...);
}Answer:
如果 request 失敗的話, 逐次的遞增再次執行該 job 的時間間隔, 最多嘗試一天(會從第一次 dispatch 時開始計算時間), 在一天內可以無限次數嘗試(依照定義的時間間隔頻率), 若超過一天則視為 failed job
失敗的話, 採取相對應動作
以下的 Laravel example code 的意思是?
Example:
<?php
class CheckoutController
{
public function store()
{
$order = Order::create([
'status' => Order::PENDING, // ...
]);
MonitorPendingOrder::dispatch($order)->delay(900);
// also ... delay(now()->addMinutes(15))
}
}
// MonitorPendingOrder class
public $tries = 4;
public function handle()
{
if ($this->order->status == Order::CONFIRMED || $this->order->status == Order::CANCELED) {
return;
}
if ($this->order->olderThan(59, 'minutes')) {
$this->order->markAsCanceled();
return;
}
SMS::send(...);
$this->release(now()->addMinutes(15)
);
}Answer:
常常使用者建立訂單後, 忽然決定不買了, 但也不會手動取消這張訂單
如上 example 邏輯
訂單建立後每 15 分鐘提醒使用者有這張訂單存在
若過了 59 分鐘後, 訂單並未被 confirmed, 也未 cancelled, 這時 job 就取消這張訂單
public $tries 確保這個 job 能被執行 4 次, 這邊 4 次還是有點太少了, 可以在設大一點
以下的 Laravel example command 的意思是?
Example:
php artisan queue:work --backoff=60,120
Answer:
定義該 worker 在嘗試 job 的時間間隔, 比如說嘗試執行 job A 失敗了, 第一次要間隔 60 秒之後才可嘗試再次執行, 第二次之後都要間隔至少 120 秒
Laravel 8 之前又叫做 –delay
以下的 Laravel example code 的意思是?
Example:
<?php
namespace App\Jobs;
class SendVerificationMessage implements ShouldQueue
{
public $tries = 3;
public $backoff = [60, 120];
}Answer:
$tries 定義該 job 最多會被執行 3 次, release() 也算一次, 如果 3 次過了還在 queue 當中, 那就會被放到 failed_job 當中
$backoff 定義每次嘗試執行 job 的時間間隔, 第一次為 60 秒, 之後都間隔 120 秒, Laravel 8.0 之前叫做 retryAfter
以下的 Laravel example code 的意思是?
Example:
php artisan queue:work --queue=payments,default
php artisan queue:work --queue=default,paymentsAnswer:
讓兩個 worker 分別執行兩個 queue, 以避免一個執行完就處於 idle 狀態
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$customers = Customer::inRandomOrder()->take(1)->get();
$regions = Region::hasCustomer($customers->first())->get();
return view('customers', [
'customers' => $customers,
'regions' => $regions,
]);
}
public function scopeHasCustomer($query, Customer $customer)
{
$query->whereRaw('ST_Contains(regions.geometry, ?)', [$customer->location]);
}Answer:
<?php
public function index()
{
// 取得 random user
$customers = Customer::inRandomOrder()->take(1)->get();
// 取得該 customer 所在的 region
$regions = Region::hasCustomer($customers->first())->get();
return view('customers', [
'customers' => $customers,
'regions' => $regions,
]);
}
public function scopeHasCustomer($query, Customer $customer)
{
// 使用 ST_Contains function, arg1 為 regions table 的 geometry column, arg2 為帶入的 customer location, 以取得該 customer 所在的 region
$query->whereRaw('ST_Contains(regions.geometry, ?)', [$customer->location]);
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$regions = Region::all();
$customers = Customer::query()
->inRegion(Region::where('name', 'The Prairies')->first())
->get();
return view('customers', [
'customers' => $customers,
'regions' => $regions,
]);
}
public function scopeInRegion($query, Region $region)
{
$query->whereRaw('ST_Contains(?, customers.location)', [$region->geometry]);
}Answer:
<?php
public function index()
{
$regions = Region::all();
// 目標在於 select 出, 位於 'The Prairies' 這個 region 的 user
$customers = Customer::query()
->inRegion(Region::where('name', 'The Prairies')->first())
->get();
return view('customers', [
'customers' => $customers,
'regions' => $regions,
]);
}
public function scopeInRegion($query, Region $region)
{
// 使用 ST_Contains, arg1 為 geometry format, arg2 為 user location, 所以會 select 出 location 位於此 geometry 的 users
$query->whereRaw('ST_Contains(?, customers.location)', [$region->geometry]);
}
以下的 Laravel example code 的意思是?
Example:
<?php
protected function seedRegions()
{
$this->getRegions()->each(fn ($region) => Region::create([
'name' => $region['name'],
'color' => $region['color'],
'geometry' => (function () use ($region) {
return DB::raw("ST_SRID(ST_GeomFromText('".$region['geometry']."'), 4326)");
})(),
]));
}Answer:
取得 Regions 後, 在 Region table 建立資料, geometry column, 使用 ST_SRID 來設定 spatial reference ID, 4326 表示 world, 使用 ST_GeomFrom Text function, 從 text 建立 geometry data, geometry 格式如下:'geometry' => 'MultiPolygon (((-136.21070809681475566 57.03101434136195991, -133.72877891703720366 54.61201637520210284, -133.47202762257742847 53.45604439614419334, -131.01862636440651499 51.6878513109907729, -126.28299137770447658 48.97546429328855311, -125.05629074861900563 48.48622934024960074, -123.37102414358200519 48.11059520996249717, -122.52069777133300477 48.99007009784259736, -113.96448759224449532 48.98897113447335272, -114.59407626641420563 50.4166998690063437, -119.86365984910806048 53.47639943303917676, -119.94099441499190561 59.97238022147455894, -140.92938615814153991 60.06202748779399059, -140.74999819452486349 59.61135341849296054, -138.11897472814712273 58.90840120048653006, -136.21070809681475566 57.03101434136195991)))',
以下的 Laravel example code 中, Multipolygon 的意思是?
Example:
public function getRegions()
{
return collect([
[
'name' => 'British Columbia',
'color' => '#F56565',
'geometry' => 'MultiPolygon (((-136.21070809681475566 57.03101434136195991, -133.72877891703720366 54.61201637520210284, -133.47202762257742847 53.45604439614419334, -131.01862636440651499 51.6878513109907729, -126.28299137770447658 48.97546429328855311, -125.05629074861900563 48.48622934024960074, -123.37102414358200519 48.11059520996249717, -122.52069777133300477 48.99007009784259736, -113.96448759224449532 48.98897113447335272, -114.59407626641420563 50.4166998690063437, -119.86365984910806048 53.47639943303917676, -119.94099441499190561 59.97238022147455894, -140.92938615814153991 60.06202748779399059, -140.74999819452486349 59.61135341849296054, -138.11897472814712273 58.90840120048653006, -136.21070809681475566 57.03101434136195991)))',
],
]);
}Answer:
用多個 point 來代表一個區域
以下的 Laravel example code 的意思是?
Example:
<?php
public function up()
{
Schema::create('regions', function (Blueprint $table) {
$table->geometry('geometry');
});
}Answer:
geometry column type, 可以用多個 point 連起來代表一個區塊
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$myLocation = [-79.47, 43.14];
$stores = Store::query()
->selectDistanceTo($myLocation)
->withinDistanceTo($myLocation, 10000) // 10km
->orderByDistanceTo($myLocation)
->paginate();
return view('stores', ['stores' => $stores]);
}
public function scopeOrderByDistanceTo($query, array $coordinates, string $direction = 'asc')
{
$direction = strtolower($direction) === 'asc' ? 'asc' : 'desc';
$query->orderByRaw('ST_Distance(
location,
ST_SRID(Point(?, ?), 4326)
) '.$direction, $coordinates);
}Answer:
<?php
public function scopeOrderByDistanceTo($query, array $coordinates, string $direction = 'asc')
{
// 由於 $direction 是從外部帶入, 若要直接使用於 orderByRaw, 需要消毒
$direction = strtolower($direction) === 'asc' ? 'asc' : 'desc';
// ST_Distance function 取得距離, 第一個 location 為資料庫中的 location column, 第二個為 request user
// 的 location, 由於 ST_Distance 只接受 valid geographic object, 所以要先轉成 SRID
// 空一格之後, 再接 $direction
$query->orderByRaw('ST_Distance(
location,
ST_SRID(Point(?, ?), 4326)
) '.$direction, $coordinates);
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function up()
{
Schema::create('stores', function (Blueprint $table) {
$table->point('location', 4326);
});
}Answer:
point 為 geographic 的一種形式, 需使用類似$user->location = \Illuminate\Support\Facades\DB::raw('ST_SRID(Point('.$l.', '.$a.'), 4326)');
這種方式儲存, $l 為longitude, $a 為 Latitude
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$myLocation = [-79.47, 43.14];
$stores = Store::query()
->selectDistanceTo($myLocation)
->withinDistanceTo($myLocation, 10000) // 10km
->paginate();
return view('stores', ['stores' => $stores]);
}
public function scopeWithinDistanceTo($query, array $coordinates, int $distance)
{
$query->whereRaw('ST_Distance(
location,
ST_SRID(Point(?, ?), 4326)
) <= ?', [...$coordinates, $distance]);
}Answer:
<?php
public function index()
{
$myLocation = [-79.47, 43.14];
$stores = Store::query()
// select 出 distance column
->selectDistanceTo($myLocation)
// 取得距離在 10km 裡的 record
// ST_DISTANCE function 得到的結果為 meter
// 而 10000 meter = 10km
->withinDistanceTo($myLocation, 10000) // 10km
->paginate();
return view('stores', ['stores' => $stores]);
}
public function scopeWithinDistanceTo($query, array $coordinates, int $distance)
{
// 使用 ST_Distance function
// arg1 為 store 的 location, 可使用 point type column
// 在儲存時就存成 point 格式, 這樣會增進 select 效能
// Point(?, ?) 為當前 User 的座標
// 最後取得 distance <= 10km 的 record
$query->whereRaw('ST_Distance(
location,
ST_SRID(Point(?, ?), 4326)
) <= ?', [...$coordinates, $distance]);
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$myLocation = [-79.47, 43.14];
$stores = Store::query()
->selectDistanceTo($myLocation)
->paginate();
return view('stores', ['stores' => $stores]);
}
public function scopeSelectDistanceTo($query, array $coordinates)
{
if (is_null($query->getQuery()->columns)) {
$query->select('*');
}
$query->selectRaw('ST_Distance(
location,
ST_SRID(Point(?, ?), 4326)
) as distance', $coordinates);
}Answer:
<?php
public function index()
{
$myLocation = [-79.47, 43.14];
// 取得當前 user 的座標與各個 store 之間的距離
$stores = Store::query()
->selectDistanceTo($myLocation)
->paginate();
return view('stores', ['stores' => $stores]);
}
public function scopeSelectDistanceTo($query, array $coordinates)
{
// 如果當前 $query 沒有取得任何 column 的話, 那就 select 所有 column
// 因為如果沒這麼做的話, 下面的 query 只會取得 selectRaw 的那一個 column
if (is_null($query->getQuery()->columns)) {
$query->select('*');
}
// 使用 MySQL 的 ST_Distance 來取得兩個點的距離
// ST_Distance function 要求的 args 必須要是 valid geographic object
// 所以使用 ST_SRID function 來取得
// ST_SRID args 為兩個座標, 以及使用的 spatial reference ID, 4326 代表World
$query->selectRaw('ST_Distance(
location,
ST_SRID(Point(?, ?), 4326)
) as distance', $coordinates);
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function up()
{
DB::statement('CREATE FULLTEXT INDEX posts_fulltext_index ON posts(title, body) WITH PARSER ngram');
}Answer:
加入 fulltext index 到 posts table 的 title, body column, 使用 ngram parser 取代預設 parser
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$posts = Post::query()
->with('author')
->when(request('search'), function ($query, $search) {
$query->whereRaw('match(title, body) against(? in boolean mode)', [$search])
->selectRaw('*, match(title, body) against(? in boolean mode) as score', [$search]);
}, function ($query) {
$query->latest('published_at');
})
->paginate();
return view('posts', ['posts' => $posts]);
}Answer:
<?php
public function index()
{
$posts = Post::query()
->with('author')
// 當 $request->search === true
->when(request('search'), function ($query, $search) {
// 使用 match method, 須事先先加 full-text index, boolean mode 效能較佳
$query->whereRaw('match(title, body) against(? in boolean mode)', [$search])
// 增加 score column 代表命中程度
->selectRaw('*, match(title, body) against(? in boolean mode) as score', [$search]);
// 當 $request->search === false, 照 published_at 排序, 因為 match score 排序跟 published_at 排序不同
}, function ($query) {
$query->latest('published_at');
})
->paginate();
return view('posts', ['posts' => $posts]);
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$devices = Device::query()
->orderByRaw('naturalsort(name)')
->paginate();
return view('devices', ['devices' => $devices]);
}Answer:
使用 natual sort, 但 MySQL 不支援, 所以必須先在 MySQL 中增加 natualsort function
MySQL 不支援 natural sort, 該怎麼做?
自己增加一個 MySQL function, 如下:
- Example:
<?php
public function up()
{
if (config('database.default') === 'mysql') {
// https://www.drupal.org/project/natsort
DB::unprepared("
drop function if exists naturalsort;
create function naturalsort(s varchar(255)) returns varchar(255)
no sql
deterministic
begin
declare orig varchar(255) default s;
declare ret varchar(255) default '';
if s is null then
return null;
elseif not s regexp '[0-9]' then
set ret = s;
else
set s = replace(replace(replace(replace(replace(s, '0', '#'), '1', '#'), '2', '#'), '3', '#'), '4', '#');
set s = replace(replace(replace(replace(replace(s, '5', '#'), '6', '#'), '7', '#'), '8', '#'), '9', '#');
set s = replace(s, '.#', '##');
set s = replace(s, '#,#', '###');
begin
declare numpos int;
declare numlen int;
declare numstr varchar(255);
lp1: loop
set numpos = locate('#', s);
if numpos = 0 then
set ret = concat(ret, s);
leave lp1;
end if;
set ret = concat(ret, substring(s, 1, numpos - 1));
set s = substring(s, numpos);
set orig = substring(orig, numpos);
set numlen = char_length(s) - char_length(trim(leading '#' from s));
set numstr = cast(replace(substring(orig,1,numlen), ',', '') as decimal(13,3));
set numstr = lpad(numstr, 15, '0');
set ret = concat(ret, '[', numstr, ']');
set s = substring(s, numlen+1);
set orig = substring(orig, numlen+1);
end loop;
end;
end if;
set ret = replace(replace(replace(replace(replace(replace(replace(ret, ' ', ''), ',', ''), ':', ''), '.', ''), ';', ''), '(', ''), ')', '');
return ret;
end;
");
}
if (config('database.default') === 'sqlite') {
throw new \Exception('This lesson does not support SQLite.');
}
if (config('database.default') === 'pgsql') {
// http://www.rhodiumtoad.org.uk/junk/naturalsort.sql
DB::unprepared('
create or replace function naturalsort(text)
returns bytea language sql immutable strict as
$f$ select string_agg(convert_to(coalesce(r[2],length(length(r[1])::text) || length(r[1])::text || r[1]),\'SQL_ASCII\'),\'\x00\')
from regexp_matches($1, \'0*([0-9]+)|([^0-9]+)\', \'g\') r; $f$;
');
}
}
public function down()
{
if (config('database.default') === 'mysql') {
DB::unprepared('drop function if exists naturalsort');
}
if (config('database.default') === 'sqlite') {
throw new \Exception('This lesson does not support SQLite.');
}
if (config('database.default') === 'pgsql') {
DB::unprepared('drop function if exists naturalsort');
}
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$devices = ['iPhone 3', 'iPhone 11'];
sort($devices, SORT_NATUAL);
return $devices;
}Answer:
使用 PHP 的 natual sort 參數, 正常 sort 是iPhone 11, iPhone 3
, natual sort 是iPhone 3, iPhone 11
什麼是 natual sorting?
跟 Alphabetical Sort 幾乎相同, 唯一的不同在於, natual sorting 會將多位數的數字當成一個單一字符
換句話說, ['3', '11', '2']
Alphabetical 排序會是 11, 2, 3
, 而 natual sort 會是 2, 3, 11
以下的 Laravel example code 的意思是?
Example:
<?php
public function scopeOrderByUpcomingBirthdays()
{
$query->orderByRaw('
case
when (birth_date + interval (year(?) - year(birth_date)) year) >= ?
then (birth_date + interval (year(?) - year(birth_date)) year)
else (birth_date + interval (year(?) - year(birth_date)) + 1 year)
end
', [
array_fill(0, 4, Carbon::now()->startOfWeek()->toDateString()),
]);
}Answer:
<?php
public function scopeOrderByUpcomingBirthdays()
{
// 概念為, 將 'birth_date' 加上 '今年到你生日那年總共間隔多少年' 會等於你的 birth_date, 但
// 年份換成是今年, 如果這個值大於今天的年月日的話, 那代表你的生日還沒過, 反之, 如果這個值小於的話
// 那代表今年你的生日已經過了, 所以會排到明年去。
// 這樣一來便可以 order by upcoming birthday
$query->orderByRaw('
case
when (birth_date + interval (year(?) - year(birth_date)) year) >= ?
then (birth_date + interval (year(?) - year(birth_date)) year)
else (birth_date + interval (year(?) - year(birth_date)) + 1 year)
end
', [
// 從 index 0 開始 fill, fill 4 次, fill 這個禮拜的第一天, 以 dateString 的格式
// 這邊我覺得是看需求, 如果是 weekly email notification 的話, 那就用 startOfWeek()
// 但若是即時查詢的話, 就要用 now(), 否則會取得生日已經過了的 model
array_fill(0, 4, Carbon::now()->startOfWeek()->toDateString()),
]);
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function test()
{
Carbon::setTestNow(Carbon::parse('January 1, 2020'))
}Answer:
使用 setTestNow() 可以設定 Carbon 的當前時間
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$users = User::query()
->whereBirthdayThisWeek()
->orderByBirthday()
// ->orderByUpcomingBirthdays()
->orderBy('name')
->paginate();
return view('users', ['users' => $users]);
}
public function scopeOrderByBirthday($query)
{
$query->orderByRaw('date_format(birth_date, "%m-%d")');
}Answer:
<?php
public function index()
{
$users = User::query()
->whereBirthdayThisWeek()
->orderByBirthday()
// ->orderByUpcomingBirthdays()
->orderBy('name')
->paginate();
return view('users', ['users' => $users]);
}
public function scopeOrderByBirthday($query)
{
// order by raw 'birth_date' 欄位, 並以 "%m-%d" 格式排序
$query->orderByRaw('date_format(birth_date, "%m-%d")');
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->rawIndex("(date_format(birth_date, '%m-%d')), name", 'users_birthday_name_index');
});
}Answer:
<?php
public function up()
{
Schema::create('users', function (Blueprint $table) {
// 增加 compound index, 欄位為使用 date_format function
// reformat 過的 birth_date column, 以及 name column
$table->rawIndex("(date_format(birth_date, '%m-%d')), name", 'users_birthday_name_index');
});
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$users = User::query()
->whereBirthdayThisWeek()
->orderByBirthday()
// ->orderByUpcomingBirthdays()
->orderBy('name')
->paginate();
return view('users', ['users' => $users]);
}
public function scopeWhereBirthdayThisWeek($query)
{
$dates = Carbon::now()->startOfWeek()
->daysUntil(Carbon::now()->endOfWeek())
->map(fn ($date) => $date->format('m-d'));
$query->whereRaw('date_format(birth_date, "%m-%d") in (?,?,?,?,?,?,?)', iterator_to_array($dates));
}Answer:
<?php
public function index()
{
$users = User::query()
// 取得生日在本週的 User model
->whereBirthdayThisWeek()
->orderByBirthday()
->orderBy('name')
->paginate();
return view('users', ['users' => $users]);
}
public function scopeWhereBirthdayThisWeek($query)
{
// 取得 this week 的每一個 date 的日期, 並轉成 'm-d' 格式
$dates = Carbon::now()->startOfWeek()
->daysUntil(Carbon::now()->endOfWeek())
->map(fn ($date) => $date->format('m-d'));
// 取得 birth_date 為這個禮拜的 model, 之所以使用 in 而不是 between, 那是
// 因為當遇到 12-25 到 1-2 的情況時, 因為前者比後者大, 會出現 query 沒有結果的
// 問題, 而之所以使用 iterator_to_array, 那是因為 Carbon 會 return 一個
// generator object
$query->whereRaw('date_format(birth_date, "%m-%d") in (?,?,?,?,?,?,?)', iterator_to_array($dates));
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function up()
{
Schema::create('features', function (Blueprint $table) {
$table->rawIndex("(
case
when status = 'Requested' then 1
when status = 'Approved' then 2
when status = 'Completed' then 3
end
)", 'features_status_ranking_index');
});
}Answer:
<?php
public function up()
{
Schema::create('features', function (Blueprint $table) {
// 當使用 orderRaw("(
// case
// when status = 'Requested' then 1
// when status = 'Approved' then 2
// when status = 'Completed' then 3
// end
// )")
// 時, 可加上 index
$table->rawIndex("(
case
when status = 'Requested' then 1
when status = 'Approved' then 2
when status = 'Completed' then 3
end
// 命名 index
)", 'features_status_ranking_index');
});
}
以下的 Laravel example code 的意思是?
Example:
<?php
class FeaturesController extends Controller
{
public function index()
{
$features = Feature::query()
->withCount('comments', 'votes')
->when(request('sort'), function ($query, $sort) {
switch ($sort) {
case 'title': return $query->orderBy('title', request('direction'));
case 'status': return $query->orderByStatus(request('direction'));
case 'activity': return $query->orderByActivity(request('direction'));
}
})
->latest()
->paginate();
return view('features', ['features' => $features]);
}
public function scopeOrderByActivity($query, $direction)
{
$query->orderBy(
DB::raw('-(votes_count + (comments_count * 2))'),
$direction
);
}
}Answer:
<?php
class FeaturesController extends Controller
{
public function index()
{
$features = Feature::query()
// 取得 comments 以及 votes column counts
->withCount('comments', 'votes')
// 'sort' === 'title' || 'status' || 'activity'
// request['direction'] 為 desc, 或 asc
->when(request('sort'), function ($query, $sort) {
switch ($sort) {
case 'title': return $query->orderBy('title', request('direction'));
case 'status': return $query->orderByStatus(request('direction'));
case 'activity': return $query->orderByActivity(request('direction'));
}
})
->latest()
->paginate();
return view('features', ['features' => $features]);
}
public function scopeOrderByActivity($query, $direction)
{
// 我可以使用 query 中實際或虛擬的 column 來排序
// - 表示顛倒排序結果
// 若 votes_count 以及 comments_count 取自虛擬 column, 那是無法做 index 的
// 可以考慮實際增加兩個欄位, 或是用 virtual column
$query->orderBy(
DB::raw('-(votes_count + (comments_count * 2))'),
$direction
);
}
以下的 Laravel example code 的意思是?
Example:
<?php
class FeaturesController extends Controller
{
public function index()
{
$features = Feature::query()
->withCount('comments', 'votes')
->when(request('sort'), function ($query, $sort) {
switch ($sort) {
case 'title': return $query->orderBy('title', request('direction'));
case 'status': return $query->orderByStatus(request('direction'));
case 'activity': return $query->orderByActivity(request('direction'));
}
})
->latest()
->paginate();
return view('features', ['features' => $features]);
}
public function scopeOrderByStatus($query, $direction)
{
$query->orderBy(DB::raw("
case
when status = 'Requested' then 1
when status = 'Approved' then 2
when status = 'Completed' then 3
end
"), $direction);
}
}Answer:
<?php
class FeaturesController extends Controller
{
public function index()
{
$features = Feature::query()
// 取得 comments 以及 votes column counts
->withCount('comments', 'votes')
// 'sort' === 'title' || 'status' || 'activity'
// request['direction'] 為 desc, 或 asc
->when(request('sort'), function ($query, $sort) {
switch ($sort) {
case 'title': return $query->orderBy('title', request('direction'));
case 'status': return $query->orderByStatus(request('direction'));
case 'activity': return $query->orderByActivity(request('direction'));
}
})
->latest()
->paginate();
return view('features', ['features' => $features]);
}
public function scopeOrderByStatus($query, $direction)
{
// 預設 'Requested', 'Approved', 'Completed' 這三個 value 會依照 Alph 方式排序
// 若想變更排序規則, 可使用以下的方式, 將 value 依照自己想要的順序 return 成數字
// DB 會依照數字順序排列
$query->orderBy(DB::raw("
case
when status = 'Requested' then 1
when status = 'Approved' then 2
when status = 'Completed' then 3
end
"), $direction);
}
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$users = User::query()
->when(request('sort') === 'town', function ($query) {
if (config('database.default') === 'mysql' || config('database.default') === 'sqlite') {
$query->orderByRaw('town is null')
->orderBy('town', request('direction'));
}
if (config('database.default') === 'pgsql') {
$query->orderByNullsLast('town', request('direction'));
}
})
->orderBy('name')
->paginate();
return view('users', ['users' => $users]);
}Answer:
<?php
public function index()
{
$users = User::query()
->when(request('sort') === 'town', function ($query) {
if (config('database.default') === 'mysql' || config('database.default') === 'sqlite') {
// 如果不加這一行, 那 order by 時會將 null 的顯示在最前面, 加了這一行後, 值為 null 的 row 會被排到最後
$query->orderByRaw('town is null')
->orderBy('town', request('direction'));
}
if (config('database.default') === 'pgsql') {
$query->orderByNullsLast('town', request('direction'));
}
})
->orderBy('name')
->paginate();
return view('users', ['users' => $users]);
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function boot()
{
Builder::macro('orderByNullsLast', function ($column, $direction = 'asc') {
$column = $this->getGrammar()->wrap($column);
$direction = strtolower($direction) === 'asc' ? 'asc' : 'desc';
return $this->orderByRaw("$column $direction nulls last");
});
}Answer:
<?php
public function boot()
{
// 建立一個 new query builder method, 名為 orderByNullsLast
Builder::macro('orderByNullsLast', function ($column, $direction = 'asc') {
// wrap $column, 因為 $column 為外部帶入參數, 並且又使用 raw method, 為避免 SQL injection, 需使用 wrap
$column = $this->getGrammar()->wrap($column);
$direction = strtolower($direction) === 'asc' ? 'asc' : 'desc';
// 此為 PostgreSQL 語法, 效果為將值為 null 的 column 排在最後
return $this->orderByRaw("$column $direction nulls last");
});
}
Laravel 中, updateOrCreate() 與 MySQL 中的 INSERT … ON DUPLICATE KEY UPDATE 的差異是?
基本上兩者的效果相同, 但實作原理不同, 效能也不同
INSERT … ON DUPLICATE KEY UPDATE 為 MySQL 內建語法, 會透過 unique key 去判斷該 row 是否為 duplicate key
updateOrCreate() 為複數 query 實作而成的 method, 會先 select 比較, 再決定 insert or update
前者的效能較佳
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$books = Book::query()
->select('book.*')
->join('checkouts', 'checkouts.book_id', '=', 'books.id')
->groupBy('books.id')
->orderByRaw('max(checkouts.borrowed_date) desc')
->withLastCheckout()
->with('lastCheckout.user')
->paginate();
return view('books', ['books' => $books]);
}
public function scopeWithLastCheckout($query)
{
$query->addSelect(['last_checkout_id' => Checkout::select('checkouts.id')
->whereColumn('book_id', 'books.id')
->latest('borrowed_date')
->limit(1),
])->with('lastCheckout');
}Answer:
<?php
public function index()
{
$books = Book::query()
->select('books.*')
->join('checkouts', 'checkouts.book_id', '=', 'books.id')
// 如果不使用 group by, 則或出現多個重複的 book model, 因為每一個 book model 都會對應到多個 checkouts model
->groupBy('books.id')
// 取得最大的 borrowed_date, 即最後借出日期, 在 query 過程中, 每個 row 都會新增一個 column 名為 borrowed_date
// 其值為 max(checkouts.borrowed_date), 最後 order by 這個 column
// 這邊 INDEX 是不吃的, 若要優化, 可在 book table 中新增一個 last_checkout_id column, 每次 book 被 checkout 時
// 都更新這個欄位
->orderByRaw('max(checkouts.borrowed_date) desc')
// dynanmic relation 的概念, 每個 book model 對應多個 checkout model, 但我們只取其中一筆 checkout model
->withLastCheckout()
// 已事先於 checkout model 中定義 user belongsTo relation, 所以直接 eager load 對應的 user model
->with('lastCheckout.user')
->paginate();
return view('books', ['books' => $books]);
}
public function scopeWithLastCheckout($query)
{
// 新增 'last_checkout_id' column
$query->addSelect(['last_checkout_id' => Checkout::select('checkouts.id')
// 將 checkout table 與 book table 對應起來
->whereColumn('book_id', 'books.id')
// 以 borrowed_date 排序
->latest('borrowed_date')
// 只取第一筆
->limit(1),
// 取得 last_checkout_id 之後, 便可利用 dynanmic relation 的概念, 取得事先定義的 lastCheckout belongsTo relation
])->with('lastCheckout');
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$books = Book::query()
->orderBy(User::select('name')
->join('checkouts', 'checkouts.user_id', '=', 'users.id')
->whereColumn('checkouts.book_id', 'books.id')
->latest('checkouts.borrowed_date')
->take(1)
)
->withLastCheckout()
->with('lastCheckout.user')
->paginate();
return view('books', ['books' => $books]);
}
public function scopeWithLastCheckout($query)
{
$query->addSelect(['last_checkout_id' => Checkout::select('checkouts.id')
->whereColumn('book_id', 'books.id')
->latest('borrowed_date')
->limit(1),
])->with('lastCheckout');
}Answer:
<?php
public function index()
{
$books = Book::query()
// 以 name 排序
->orderBy(User::select('name')
// 透過 join, 取得 user 的所有借書紀錄
->join('checkouts', 'checkouts.user_id', '=', 'users.id')
// 透過 where, 取得每一個 book row 對應到的 data, 如果不使用 where 來把 book.id
// 以及 checkouts.book_id 做關聯, 那 user name 永遠都會是一樣的
// 但加上 where 之後, 就可以取得每一個 book 的所有借閱紀錄, 並取得最後借出的那個 user name
// 最後再 order by 這個 user name
->whereColumn('checkouts.book_id', 'books.id')
// 因為要取得最後借出的 user, 所以要先加以排序
->latest('checkouts.borrowed_date')
// 因為要取得最後借出的 user, 因此只取最後的那一個 user
->take(1)
)
// 利用 dynanmic relation 的概念, 先新增一個 last_checkout_id 虛擬欄位, 再透過
// book model 中的 belongs to relation, 經由這個虛擬的 last_checkout_id 取得
// 最後 checkout 的那一筆 checkout 資料
->withLastCheckout()
// checkout model 中已有先定義與 user 的 belongsTo relation, 因此可以取得與
// 該 checkout model 相關的 user model
->with('lastCheckout.user')
->paginate();
return view('books', ['books' => $books]);
}
public function scopeWithLastCheckout($query)
{
$query->addSelect(['last_checkout_id' => Checkout::select('checkouts.id')
// 透過 where, 可取得與每一行 book 相關的 checkout records
->whereColumn('book_id', 'books.id')
// 因為每一個 book model 都會有多筆的 checkout records, 因此須加以排序
->latest('borrowed_date')
// 排序後只取得最新的那一筆, 即最後借出那一筆紀錄, 至此, 只為了取得這一筆 records 的 id
->limit(1),
])->with('lastCheckout');
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$users = User::query()
->orderByLastLogin()
->withLastLogin()
->paginate();
return view('users', ['users' => $users]);
}
public function scopeOrderByLastLogin($query)
{
$query->orderByDesc(Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->take(1)
);
}
public function scopeWithLastLogin($query)
{
$query->addSelect(['last_login_id' => Login::select('id')
->whereColumn('user_id', 'users.id')
->latest()
->take(1),
])->with('lastLogin');
}Answer:
<?php
public function index()
{
$users = User::query()
->orderByLastLogin()
->withLastLogin()
->paginate();
return view('users', ['users' => $users]);
}
public function scopeOrderByLastLogin($query)
{
// 每個 User 都有很多個 Login, 所以在 orderByDesc 當中使用 subquery
// 取得與該 user 相關的 login records 之後, 再排序, 然後取第一筆
// 所以會 orderByDesc 每個 user 的最新一筆 login
$query->orderByDesc(Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->take(1)
);
}
public function scopeWithLastLogin($query)
{
// 每個 User 都有 many Login, 利用 addSelect 新增一個 column 'last_login_id'
// 正常來說 User 跟 Login 的 relationship 應該是 User hasMany Login
// 但這邊為 dynamic model 的概念, 目的為從 hasMany 眾多 records 當中
// 只 load 想要的那筆資料, 大幅增進效能
$query->addSelect(['last_login_id' => Login::select('id')
->whereColumn('user_id', 'users.id')
->latest()
->take(1),
])->with('lastLogin');
}
public function lastLogin()
{
// 正常來說 User 跟 Login 的 relationship 應該是 User hasMany Login
// 但這邊為 dynamic model 的概念, 目的為從 hasMany 眾多 records 當中
// 只 load 想要的那筆資料, 大幅增進效能
return $this->belongsTo(Login::class);
}
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$users = User::query()
->select('users.*')
->join('companies', 'companies.user_id', '=', 'users.id')
->orderBy('companies.name')
->with('company')
->paginate();
return view('users', ['users' => $users]);
}Answer:
當需要 orderBy hasOne relationship 的某一個 column, 務必使用 join approach
以下的 Laravel example code 的意思是?
Example:
<?php
public function index()
{
$users = User::query()
->select('users.*')
->join('companies', 'companies.user_id', '=', 'users.id')
->orderBy('companies.name')
->with('company')
->paginate();
return view('users', ['users' => $users]);
}Answer:
當需要 orderBy belognsTo relationship 的某一個 column, 務必使用 join approach
Laravel 中, 當使用 pagination 時, 務必要使用 orderBy, 為什麼?
因為 pagination 會 offset 並 limit record 數量, 若無使用 orderBy 的話, 很可能每一次的排序都有所不同, 這將導致 pagination 的每一頁的結果是沒有順序性的
Laravel 中, 以下的 migration example code 中, 如果使用 orderBy(‘first_name’)->orderBy(‘last_name’), index 會生效嗎??
Example:
<?php
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->index(['last_name', 'first_name']);
});
}Answer:
不會, 因為 orderBy 順序必須符合 index 的順序
Laravel 中, 以下的 example code 的意思是?
Example:
<?php
public function index()
{
Auth::login(User::where('name', 'Sarah Seller')->first());
$customers = Customer::query()
->visibleTo(Auth::user())
->with('salesRep')
->orderBy('name')
->paginate();
return view('customers', ['customers' => $customers]);
}
public function scopeVisibleTo($query, User $user)
{
if (! $user->is_owner) {
$query->where('sales_rep_id', $user->id);
}
}Answer:
情境為, 當 user 為 owner 時, 可以看到所有的 customer, 而當 user 為 salesRep 時, 只可看到自己底下的 customer
主要概念為, 若有這樣的情境限制, 務必將 filter 做在 database layer
Laravel 中, 以下的 migration example code 的意思是?
Example:
<?php
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->index(['last_name', 'first_name']);
});
}Answer:
增加一個 compound index, 若是使用 orderBy 時, 順序必須跟 compound index 的順序相同
Laravel 中, 以下的 example code 的意思是?
Example:
<?php
public function scopeSearch($query, string $terms = null)
{
collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
$term = preg_replace('/[^A-Za-z0-9]/', '', $term).'%';
$query->whereIn('id', function ($query) use ($term) {
$query->select('id')
->from(function ($query) use ($term) {
$query->select('users.id')
->from('users')
->where('users.first_name_normalized', 'like', $term)
->orWhere('users.last_name_normalized', 'like', $term)
->union(
$query->newQuery()
->select('users.id')
->from('users')
->join('companies', 'users.company_id', '=', 'companies.id')
->where('companies.name_normalized', 'like', $term)
);
}, 'matches');
});
});
}Answer:
<?php
public function scopeSearch($query, string $terms = null)
{
collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
// 將 $term 中 "非 A-Za-z0-9" 的內容都替換成 '', 也就是刪去
$term = preg_replace('/[^A-Za-z0-9]/', '', $term).'%';
// 之所以不在
$query->whereIn('id', function ($query) use ($term) {
$query->select('id')
->from(function ($query) use ($term) {
$query->select('users.id')
->from('users')
// 從 first_name_normalized virtual column 中 query
// 因為若使用 whereRaw 的 regular expression 語法, 將無法
// 使用 index
->where('users.first_name_normalized', 'like', $term)
->orWhere('users.last_name_normalized', 'like', $term)
->union(
$query->newQuery()
->select('users.id')
->from('users')
->join('companies', 'users.company_id', '=', 'companies.id')
->where('companies.name_normalized', 'like', $term)
);
}, 'matches');
});
});
}
Laravel 中, 以下的 example code 的意思是?
Example:
<?php
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->foreignId('company_id')->constrained('companies');
$table->string('first_name_normalized')->virtualAs("regexp_replace(first_name, '[^A-Za-z0-9]', '')")->index();
$table->string('last_name');
$table->string('last_name_normalized')->virtualAs("regexp_replace(last_name, '[^A-Za-z0-9]', '')")->index();
});
}Answer:
<?php
public function up()
{
Schema::create('users', function (Blueprint $table) {
// foreignId 為 unsignedBigInteger 的 alias, 而 constrained 會自動為
// users table 中的 company_id 與 companies table 中的 id column 建立 foreign key index
$table->foreignId('company_id')->constrained('companies');
// virtualAs 作用為建立一張虛擬表, 此行 code 會將 first_name column 中的內容
// 只要不是 a-zA-Z0-9 的都替換成 '', 然後再將內容存到 first_name_normalized 這張
// 虛擬 table, 並建立 index
$table->string('first_name_normalized')->virtualAs("regexp_replace(first_name, '[^A-Za-z0-9]', '')")->index();
$table->string('last_name');
// 同上
$table->string('last_name_normalized')->virtualAs("regexp_replace(last_name, '[^A-Za-z0-9]', '')")->index();
});
}
Laravel 中, 以下的 example code 的意思是?
Example:
<?php
public function scopeSearch($query, string $terms = null)
{
collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
$term = $term.'%';
$query->whereIn('id', function ($query) use ($term) {
$query->select('id')
->from(function ($query) use ($term) {
$query->select('users.id')
->from('users')
->where('users.first_name', 'like', $term)
->orWhere('users.last_name', 'like', $term)
->union(
$query->newQuery()
->select('users.id')
->from('users')
->join('companies', 'users.company_id', '=', 'companies.id')
->where('companies.name', 'like', $term)
);
}, 'matches');
});
});
}Answer:
<?php
public function scopeSearch($query, string $terms = null)
{
collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
$term = $term.'%';
// 最終目的, 是要找到符合 query 條件的 users, 又要讓 index 生效
// 這邊是 select * from users whereIn('id', nextQuery)
$query->whereIn('id', function ($query) use ($term) {
// 這邊是 select id from derived table, 之所以使用 derived table, 是為了
// 不要讓內外 query 相互依賴, 如果單純在 whereIn 當中使用 subquery 的話, 就會
// 產生互相依賴, 造成 index 無法生效
$query->select('id')
->from(function ($query) use ($term) {
$query->select('users.id')
->from('users')
->where('users.first_name', 'like', $term)
->orWhere('users.last_name', 'like', $term)
// 使用 union 來串接兩句語法
->union(
$query->newQuery()
->select('users.id')
->from('users')
->join('companies', 'users.company_id', '=', 'companies.id')
->where('companies.name', 'like', $term)
);
// derived table 必須要有一個 alias
}, 'matches');
});
});
}
Laravel 中, 以下的 example code 的意思是?
Example:
<?php
public function scopeSearch($query, string $terms = null)
{
collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
$term = $term.'%';
$query->where(function ($query) use ($term) {
$query->where('first_name', 'like', $term)
->orWhere('last_name', 'like', $term)
->orWhereIn('company_id', Company::query()
->where('name', 'like', $term)
->pluck('id')
);
});
});
}Answer:
雖然沒有使用 sub query 造成 query 的數量增加了, 但 users table 因為沒有使用 sub query 的關係而使 index 生效, 雖然 query 數量較多, 但速度卻變快了
Laravel 中, 以下的 example code 的意思是?
Example:
<?php
public function scopeSearch($query, string $terms = null)
{
collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
$term = $term.'%';
$query->where(function ($query) use ($term) {
$query->where('first_name', 'like', $term)
->orWhere('last_name', 'like', $term)
->orWhereIn('company_id', function ($query) use ($term) {
$query->select('id')
->from('companies')
->where('name', 'like', $term);
});
});
});
}Answer:
<?php
public function scopeSearch($query, string $terms = null)
{
// str_getcsv 會將 ' ' (空白) 分出來, " (double quote) 也分出來, return 一個 array
// 再把這個 array 使用 each 迭代
collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
// prefix 不用 %, 因為 prefix % 不適用於 index
$term = $term.'%';
$query->where(function ($query) use ($term) {
$query->where('first_name', 'like', $term)
->orWhere('last_name', 'like', $term)
// 這邊不使用 orWhereHas, 因為 SQL 語法會關聯兩張表,
// 這個 dependency 會造成 company_name 不適用 index
// 因此使用 whereIn 來避開此 dependency
->orWhereIn('company_id', function ($query) use ($term) {
$query->select('id')
->from('companies')
->where('name', 'like', $term);
});
});
});
}
Laravel 中, 以下的 example code 的意思是?
Example:
<?php
public function scopeSearch($query, string $terms = null)
{
collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
$term = $term.'%';
$query->where(function ($query) use ($term) {
$query->where('first_name', 'like', $term)
->orWhere('last_name', 'like', $term)
->orWhereHas('company', function ($query) use ($term) {
$query->where('name', 'like', $term);
});
});
});
}Answer:
str_getcsv
會把空白隔開的當成一個 value,"
內的也當成一個 value, 再把各個 value 變成 $term 下去 query
Laravel 中, 以下的 example code 的意思是?
Example:
<?php
public function show(Feature $feature)
{
$feature->load('comments.user');
$feature->comments->each->setRelation('feature', $feature);
return view('feature', ['feature' => $feature]);
}Answer:
<?php
public function show(Feature $feature)
{
$feature->load('comments.user');
// 這裡手動的為每個 comments 建立 relation 名為 feature, 代入當前已載入 memory 的 $feature
// 如此一來, 在上一行 eager load comments 之後, 每一個 comments 都還會有在上一行已經完成
// eager load 的 feature relation, 那當我們執行 $feature->comments->feature->comments
// 時就不會重複 SQL 語法, 也不會重複的加載 model
// 當執行 $feature->comments->feature->comments 時, 如果不使用 eager loading, 會有
// n+1 issue, 但如果使用 with('comments.feature.comments') 的話, 則會 n*n 的加載不必要
// 的 model
// 這樣的 relation 又稱為 circular relation
$feature->comments->each->setRelation('feature', $feature);
return view('feature', ['feature' => $feature]);
}
Laravel 中, 動態 model 的技術概念是?
假設 User model 跟 Login model 的 relation 為 hasMany, 但我在取得 User model 時, 我只想要取得最近 login 的 Login model
所以先用 subquery 來取得一個虛擬的 column 為 login_id, 這時再用這個 login_id 透過 belongsTo 的 relation 取得單筆 model
Laravel 中, 假設今天我想要取得 hasMany relation 中的一筆 record, 這時如果使用 with() 的話會 load 所有的 model, 不使用 with() 的話又會執行很多次 query, 有什麼好的解法?
Example:
<?php
$users = User::query()
->with('login')
->orderBy('name')
->paginate();Answer:
<?php
// 使用 subquery, 從 hasMany relation 中只 query 出我們要的那筆資料, 再將這筆資料變成主 query 中的一個 column
$users = User::query()
->addSelect(['last_login_at' => Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->take(1)
])
->orderBy('name')
->paginate();
Laravel 中, 如果使用 composer 安裝套件時, 超過容許的 memory 限制, 可以使用哪個 flag 讓容許 memory 無上限?
COMPOSER_MEMORY_LIMIT=-1 |
以下的 Laravel 程式碼的意思是?
Example:
<?php
$ip = data_get($this->pendingWalletSettlement->settleable, 'depositDetail.client_ipv4')Answer:
<?php
// 從 settleable 得到的 model 裏頭, 取得他的 relation depositDetail 的 attribute client_ipv4
$ip = data_get($this->pendingWalletSettlement->settleable, 'depositDetail.client_ipv4')
解釋以下的 Laravel example
Example:
<?php
public function scopeOf(Builder $query, ...$models)
{
foreach ($models as $model) {
/** @var $model Model */
$query->where($model->getForeignKey(), $model->getKey());
}
}Answer:
<?php
// 可帶入多個 model
public function scopeOf(Builder $query, ...$models)
{
// loop 每個 model
foreach ($models as $model) {
/** @var $model Model */
// 取得該 model 的 `foreign key name`, 以及 `primary key value`
// 所以可以在目標 model 使用其他 model 為條件來 query
$query->where($model->getForeignKey(), $model->getKey());
}
}
安裝完新的 PHP Extension, 記得要做些什麼事? 重啟 PHP, 重啟 Web server, 若使用 valet, 記得重啟 valet
在 Laravel 中, 如何確認 model 是否已經被改過?
model->isDirty() |
在 Laravel 中, 如何得到與該 model 相關的 table 的名字?
model->getTable() |
在 Laravel 中, 下面的代碼是什麼意思? 若 $request 中有指定的值, 如 ‘row’, 則使用該值, 若該值不存在, 則使用第二個自己指定的值
$row = request('row', (new Deposit)->getPerPage()); |
在 Laravel 中, 下面的代碼是什麼意思?
foreach ( |
- foreach 依序 loop array 當中的每一個 value
- $availableFilterKey 這裡表示 array 當中的 value
- when 表示條件句, [boolean, executeIfTrue, executeIfFalse]
- $builder 代表 $deposit
- $filterValue 代表 $request->availableFilterKey
- 參考文件
在 Laravel 中, 如何取得 query builder?
$query = yourModel::query(); |
在 Laravel 中, ** 代表什麼意思?
power operator, 如果是 2 ** 16, 代表 2 * 2 * .. 16 次 |
在 Laravel 中, 如何取得 parent 的 method?
# 如果沒複寫, 直接呼叫即可 |
Laravel 的時區設定檔位置在?
config 中的 app.php |
資料庫欄位 int, 什麼是 signed 跟 unsigned?
signed 的數值範圍橫跨正負數, unsigned 只有正數, 且為 signed 的兩倍, 但最小的負數為 0 |
Laravel 當中, 以下的語法代表什麼意思? 列出 $users query builder 的 query 語法以及帶入的變數
<?php |
Laravel 當中, 以下代碼代表什麼意思? 當 $user->address
可獲得時, 取值, 不可獲得時, 回傳 null
<?php |
Laravel 當中, 哪裡可以設定 queue 的名字?
config 資料夾中的 queue |
Laravel 當中, 以下的 agent_id 驗證 exists 邏輯代表什麼意思? 在 users table 當中, 帶入的 agent_id 必須要跟 table 裡頭的 id 相同, 且 role_id 必須得跟 Role::AGENT 相同
<?php |
以下的 Laravel 程式碼代表什麼意思?
<?php |
取得 User model 的 merchant scope, eager load owner relation, 當 $q 不為 null, 執行 closure, 接上 closure 中的 query builder, 在 username, name, email 三個欄位中模糊搜尋 $q
以下的 Laravel 程式碼的 appends 邏輯是什麼? 在 Laravel 的分頁模式中, 若未將全部來自於前端的 query 帶入, 那分頁的 url 將會缺少必要的 query, 等於只有第一頁會有 帶入的 query 結果
<?php |
以下的 Laravel 程式碼中的 latest 代表什麼意思? 以 id 排序
<?php |
以下的 Laravel 程式碼中的 fill 代表什麼意思? 將值注入 userBankCard model, 帶入參數可以是一個 array
<?php |
以下的 Laravel 程式碼中, 為什麼要使用 collect function? 這樣如果前端帶錯, 帶成 string 的話, 會先將 string 轉成 collection, 再轉成 array
<?php |
以下的 Laravel 程式碼代表什麼意思?
<?php |
取得 Deposit model, 以帶入日期過濾, 以 SUCCESS status 過濾, 以 system_bank_card_type 分類, 在取得四個值, 分別是 system_bank_card_type, total_amount, total_fee, total_count, 若照預設, 會是一個 collection 裡面有兩個 model, index 為 0 跟 1, 使用 keyBy 來將 0 跟 1 依照 system_bank_card_type 做區分, 所以會變成一個 collection 裡頭有兩個 model, 以 system_bank_card_type 做區分
以下的 Laravel 程式碼為什麼要使用 first? 因為該 query 撈出來後, 只會有一筆 model, 如果是用 get 的話, 會是一個 collection 裡有一個 model, 所以直接使用 first() 即可
<?php |
以下的 Laravel 程式碼中, keyBy 的用途是? 如果不使用 keyBy 的話, 正常來說一個 collection 裡頭有多個 model 會以默認 index, 0, 1, 2 …, keyBy 可以使用指定的 key 來給 model 分組, 在這個例子中, 就是以 model 下的 slug 欄位的值做分組
<?php |
以下的 Laravel 程式碼中, data_get 的用途是? $depositStats 結構像是這樣 $depositStats = ['BankCard::TYPE_FEE' => ['total_count', 'total_amount', 'total_count', 'total_amount']]
, data_get 可以取得一個 collection 裡頭的巢狀 array 值
<?php |
以下的 Laravel 程式碼代表什麼意思? 定義一個 unique rule, 並且將範圍限定在特定的 user 上, 代表不同 user 之間的訂單不需要 unique
<?php |
以下的 Laravel function 的作用是什麼?
- 將 request 裡的參數除了 sign 之外都調出來
- 排列這些 key
- 首先使用 http_build_query function 針對剛剛的參數來產生一組 url 加密的字串, 然後將這字串與 user 的 secret_key 欄位內的值相串, 然後使用 url 解密這一整個字串, 最後再使用 md5 處理取得 hash 值, 我們比對這個值跟帶進來的 sign 有沒有一樣, 如果不一樣就是不合法
- 唯有知道 secret_key 的雙方可以對內容加解密, 而經由這樣的加解密驗證, 確保 request 的內容再傳送過程中未被串改
<?php
private function signValid(Request $request, $secretKey)
{
$allParametersExceptSign = $request->except('sign');
ksort($allParametersExceptSign);
return strcasecmp(
md5(urldecode(http_build_query($allParametersExceptSign)) . $secretKey),
$request->sign
) === 0;
}
以下的 Laravel function 的作用是什麼?
- 宣告 lock-key 以及持有時間
- 嘗試取得 lock-key, 如果不可得, 持續嘗試五秒
- 用 transaction 實作, 若有任何錯誤皆返回
- 如果無法取得 lock-key, 返回錯誤
- 回返錯誤訊息
- 如果 lock-key 還被持有中, 釋放 lock-key
<?php
public function lock(User $user, $action)
{
$lock = Cache::lock($user->lockKey(), 10);
try
{
$lock->block(5);
return DB::transaction($action);
} catch (LockTimeoutException $e)
{
abort(Response::HTTP_CONFLICT, '请稍候再试');
} finally
{
optional($lock)->release();
}
}
以下的 Laravel 程式碼中, balance 是扣款前還是扣款後? 扣款前, 因為 $transaction 還沒被執行完畢
<?php |
以下的 Laravel 程式碼是什麼意思呢? 將資料存入 mysql 中的 json 欄位
<?php |
下面的 Laravel 程式碼是什麼意思? 將檔案存在 $deposit->getTable(), 檔名為 $deposit->system_order_number, 使用 filesystem.cloud, 可設為 s3
<?php |
下面的 Laravel Requests 代表什麼意思? 使用 captcha 的 extension captcha_api 來驗證, 因為該驗證器一定需要一個 key, 如果在 validation 期間 key 為 null, 那直接就回 500 了, 所以這邊處理, 當沒有 captcha_key 時, 給一個隨機 10 碼, 這樣會驗不過(key 沒帶原本就應該驗不過), 但是不會 500
<?php |
以下的 Laravel 程式碼中, where 內為什麼只有一個參數?
Laravel 中, 如果 request 中的 parameter 與資料庫中的欄位名稱相同, 就可以直接用這種方式 query
<?php
$bankCard = BankCard::where($request->only('card_number'))
->withTrashed()
->first() ?? BankCard::create($data);
以下的 Laravel 程式碼是什麼意思?
1. query 出 card_number 的 model |
<?php |
在 Laravel 中, 如何從一個 collection 當中取得其中一個 model, 而該 model 中的 price 欄位的值是在這個 collection 的所有 model 之中最小或最大的?
<?php |
在 Laravel 當中, 如何在一個 collection 裡放入多個 model ?
<?php |
以下的 Laravel 程式碼代表什麼意思?
1. 帶入欄位的名稱需與資料庫的欄位一樣 |
<?php |
Laravel 中, Model 的命名通常是單數還是複數?
單數
操作 Resource 的 controller, 通常命名的規則是?
該 Resource 對應的名稱, 如 IpController
Laravel 中的變數命名習慣是?
camel case
Laravel 中, blade view 的命名習慣是?
會以資料夾做區分, 像是 withdraws/index.blade.php、withdraws/store.blade.php
Laravel 中, 如何將 namespace, prefix, middleware 同時作用到複數的 route 上?
<?php |
Laravel 中, 可否在 route 的 group 內再使用一個 group?
可以的
Laravel 中, 巢狀內的 route group 的屬性會不會繼承外層的 group 的屬性?
會的
, 像是<?php
Route::group([
'middleware' => ['auth']
], function () {
Route::get('me', 'AuthController@me');
Route::group([
'namespace' => 'Admin',
'prefix' => 'admin',
'middleware' => ['check.role.admin', 'check.source.admin'],
], function () {
Route::post('users/{user}/reset-password', 'UsersController@resetPassword');
Route::post('users/{user}/reset-withdraw-password', 'UsersController@resetWithdrawPassword');
Route::post('users/{user}/reset-google2fa-secret', 'UsersController@resetGoogle2faSecret');
Route::post('users/{user}/reset-secret-key', 'UsersController@resetSecretKey');
Route::put('users/{user}/delete-group', 'UsersController@deleteGroup')
->where(['user' => '[0-9]+']);
});
});
以下的 Laravel 程式碼中, 邏輯是怎麼樣的?
如果環境是在 production 的話, 檢查來源 ip, 來源 ip 可能有很多個, 取最後一個代表 client, 若不存在則拒絕存取 |
<?php |
以下的兩段 Laravel 在 Resource 中的程式碼, 有什麼差異?
<?php
'withdraws' => $this->whenLoaded('withdraws', Withdraw::collection($this->withdraws))<?php
'withdraws' => $this->whenLoaded('withdraws', function () {
return Withdraw::collection($this->withdraws);
})第一個 block 中, PHP 會先去執行作為參數帶入的 $this->withdraws, 再將結果帶入 whenLoaded, 這便符合了 whenLoaded 的 relation 載入條件, 所以依然會將當前 resource 下的 relation 顯示出來, 實際運行上因為會先執行 $this->withdraws, 因此也會造成效能上的浪費
第二個 block 中, closure 在被呼叫之前, PHP 並不會去解析它, 所以會先執行 whenLoaded 函式, 如果條件吻合, 才會執行 closure, 所以不會去執行 $this->withdraws, 自然 whenLoaded 的條件就不會吻合, resource 中也就不會多撈一層
Laravel 中, 如何將 array 存到資料庫?
資料庫類別為 json
<?php
Schema::table('users', function (Blueprint $table) {
$table->json('iAmarray')->nullable();
});存到資料庫前, 先使用
json_encode
<?php
data = json_encode($iAmArray);在 model 加入
<?php
protected $casts = [
'iAmArray' => 'array'
];
當我在 Route 當中使用 apiResource 如下, 自動帶入 controller 的 model binding 的變數名稱為?
sub_account<?php
Route::apiResource('sub-accounts', 'SubAccountsController')->only(['store', 'update']);
Laravel 中, 當我使用 scheduler, 腳本內的 user 務必要使用?
與 webserver 同一個 user
以下的 Laravel 程式碼的邏輯是?
程式碼:
<?php
$canSeeSecretKey = optional(auth()->user())->isAdmin() || $this->is(auth()->user());Answer:
- 如果 auth()->user() 的身份是 admin 的話
- 如果被帶入 resource 中的 model 的身份跟 auth()->user() 是同一個人的話(代表本人)
以下的 Laravel 程式碼的作用是?
code:
<?php
public static function depositTypeText()
{
return collect((new ReflectionClass(__CLASS__))->getConstants())
->filter(
function ($value, $key) {
return Str::startsWith($key, 'TYPE_DEPOSIT');
}
)
->mapWithKeys(
function ($value, $key) {
return [$value => $key];
}
);
}Answer:
- RefectionClass: 取得指定 class 中的資料
- __class__: 代表當前 class
- getConstants: 取得 constants
- filter: 只取符合條件的 key
- mapWithKeys: 取得符合條件的 key/value pair
以下的 Laravel 程式碼的邏輯是?
Example:
<?php
if ($endedAt->diffInDays($startedAt) > 31) {
$request->merge(
[
'ended_at' => (clone $startedAt)->addDays(31)->format('Y-m-d H:i:s'),
]
);
}Answer:
如果 $endedAt 跟 $startedAt 相差大於 31 天, 那就把範圍定在最多相差 31 天
留言