Laravel 學習筆記

# 前言

本篇為 Laravel 的學習筆記, 主要將看到的, 學到的技術轉換成 Q&A 的方式以加速學習



# Production 優化

# PHP

php.ini 文件

php.ini 中的 memory_limit 用於設定單個 PHP process 可以使用的系統內存最大值


# Laravel timezone 與 MySQL timezone

# 當 Laravel 收到沒有時區的時間

  • 預設此時間為 Laravel timezone, 不另外做轉換直接帶給資料庫, 如下圖:
    sequenceDiagram
    participant Client
    participant Laravel
    participant MySQL
    NOTE OVER Client,Laravel: 假設 Laravel 時區為 Asia/Taipei
    Client-->>Laravel: 給你 2021-02-05 15:27:46
    NOTE OVER Client,Laravel: Laravel 會默認上述時間為 Asia/Taipei
    Laravel-->>MySQL: 給你 2021-02-05 15:27:46

# 當 Laravel 收到有時區的時間

  • 將時間轉為 Laravel 時區, 如下圖:
    sequenceDiagram
    participant Client
    participant Laravel
    participant MySQL
    NOTE OVER Client,Laravel: 假設 Laravel 時區為 Asia/Taipei
    Client-->>Laravel: 給你 2021-02-05T15:27:46+0400
    NOTE OVER Client,Laravel: Laravel 會將上面的時間轉為 Asia/Taipei
    NOTE OVER Client,Laravel: 即 2021-02-05T19:27:46+0800
    Laravel-->>MySQL: 給你 2021-02-05 19:27:46

# Laravel 回傳時間

sequenceDiagram
participant Client
participant Laravel
participant MySQL
NOTE OVER Client,Laravel: 假設 Laravel 時區為 Asia/Taipei
MySQL-->>Laravel: 給你 2021-02-05 19:27:46
NOTE OVER Client,Laravel: Laravel 默認上面時間的時區為 Asia/Taipei
NOTE OVER Client,Laravel: 即 2021-02-05T11:27:46+0000 (預設回傳 ISO8601)
Laravel-->>Client: 給你 2021-02-05T11:27:46+0000

# 當 MySQL 的 column 為 datetime

sequenceDiagram
participant Laravel
participant MySQL
Laravel-->>MySQL: 給你 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: 當 column type 為 datetime, 不做任何變更
NOTE OVER Laravel,MySQL: 實存 2021-02-05 19:27:46 到資料庫

# 當 MySQL 的 column 為 timestamp

sequenceDiagram
participant Laravel
participant MySQL
Laravel-->>MySQL: 給你 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: MySQL column type 為 timestamp, 假設 MySQL timezone 為 Asia/Taipei
NOTE OVER Laravel,MySQL: 使用 default timezone 將該時間轉為 UTC 儲存
NOTE OVER Laravel,MySQL: 實存 2021-02-05T11:27:46+0000, 顯示 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: 之後若是更換時區, 會以 2021-02-05T11:27:46+0000 來轉換為該時區時間顯示

# 調整 MySQL timezone 對 Laravel 輸出的影響 (datetime)

sequenceDiagram
participant Laravel
participant MySQL
NOTE OVER Laravel,MySQL: 時區為 Asia/Taipei
Laravel-->>MySQL: 給你 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: 實存 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: 修改時區為 UTC
MySQL->>Laravel: 給你 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: datetime 不受更換時區影響

# 調整 MySQL timezone 對 Laravel 輸出的影響 (timestamp)

sequenceDiagram
participant Laravel
participant MySQL
NOTE OVER Laravel,MySQL: 時區為 Asia/Taipei
Laravel-->>MySQL: 給你 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: 實存 2021-02-05T11:27:46+0000
NOTE OVER Laravel,MySQL: 修改時區為 UTC
MySQL->>Laravel: 給你 2021-02-05 11:27:46
NOTE OVER Laravel,MySQL: timestamp 會因為修改時區而回傳不一樣的 datetime string

# Questions and Answers

IoC container, 具體 IoC 反轉了什麼?

IoC 出現之前, 假設 A 對象需要 C 資源, 需要在 A 對象中主動獲取 C 資源
IoC 出現之後, 由 container 獲取 C 資源, 注入 A 對象, 這樣的行為稱為反轉

IoC container, 某對象, 外部資源, 三者中, 誰控制誰? 控制了什麼?

container 控制某對象, 控制對象實例的創建

IoC container, 某對象, 外部資源, 三者中, 誰注入誰?

container 注入外部資源到某對象

IoC container, 某對象, 外部資源, 三者中, 誰依賴誰? 為什麼?

某對象依賴於 container, 因為需要 container 提供外部資源

IoC container 中, 一般有哪三個參與者?

(1) 某對象, 即任意一個 class
(2) container, 即 IoC container
(3) 外部資源, 即某對象需要的, 但從某對象外部獲取的資源

以下的 Laravel example code 的意思是?
  • Example:
    <?php
    collect([1,2,3,4])->shift()
    // 1
    collect([1,2,3,4])->shift(3)
    // [1,2,3]
  • Answer:
    shift 可帶入要 shift 的數量
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    collect([1,2,3,4])->pop(3)
    // [2,3,4]
  • Answer:
    pop 可帶入要 pop 的數量
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    // Before
    [
    'email'=> [
    Rule::unique('users')->whereNull('deleted_at'),
    ],
    ];

    // After
    [
    'email'=> [
    Rule::unique('users')->ignoreTrashed(),
    ],
    ];
  • Answer:
    使用 ignoreTrashed() 忽略 soft-deleted 資料
以下的 Laravel example code 的意思是?
  • Example:
    composer require laravel/octane
    php artisan octane:install
  • Answer:
    安裝 octane
Laravel 中, cursor paginator 有什麼限制?

(1) 跟 simple pagination 一樣, 只可顯示上一頁以及下一頁
(2) 排序需基於 1 個或多個 unique key

以下的 Laravel example code 的意思是?
  • Example:
    <?php
    $users = DB::table('users')->orderBy('id')->cursorPaginate(15);
  • Answer:
    建立一個 cursorPaginator, 主要是使用 compare operator, 而不是 offset, 所以在效能上比較好
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    public function currentPricing()
    {
    return $this->hasOne(Price::class)->ofMany([
    'published_at' => 'max',
    'id' => 'max',
    ], function ($query) {
    $query->where('published_at', '<', now());
    });
    }
  • Answer:
    定義 one of many relationship, 從 hasMany relationshipo price model 中, 取得 published_at 以及 id 最新的那筆資料, 且 published_at 需 < now()
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    public function largestOrder()
    {
    return $this->hasOne(Order::class)->ofMany('price', 'max');
    }
  • Answer:
    定義 one of many relationship, 從 hasMany relationshipo price max 的那一筆資料
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    public function oldestOrder()
    {
    return $this->hasOne(Order::class)->oldestOfMany();
    }
  • Answer:
    定義 one of many relationship, 從 hasMany relationshipo 中取得最舊的那一筆, 預設 id 排序
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    public function latestOrder()
    {
    return $this->hasOne(Order::class)->latestOfMany();
    }
  • Answer:
    定義 one of many relationship, 從 hasMany relationshipo 中取得最新的那一筆, 預設 id 排序
Laravel 中, 為何不建議使用 MySQL 作為 queue driver?

因為 MySQL 8 之前的版本可能會造成死鎖

以下的 Laravel example code 的意思是?
  • Example:
    <?php
    $response->assertDownload();
    $response->assertDownload('image.jpg');
  • Answer:
    斷言 response 有觸發下載動作
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    class Post extends Model
    {
    use BroadcastsEvents, HasFactory;

    public function user()
    {
    return $this->belongsTo(User::class);
    }

    public function broadcastOn($event)
    {
    return match($event) {
    'deleted' => [],
    default => [$this, $this->user],
    };
    }

    }
  • Answer:
    use BroadcastsEvents trait, 且定義 broadcastOn method, 這樣在每次 model event 被觸發時, 會自動 broadcast 給指定的 user
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    use App\Services\Transistor;
    use App\Services\PodcastParser;

    $this->app->scoped(Transistor::class, function ($app) {
    return new Transistor($app->make(PodcastParser::class));
    });
  • Answer:
    與 singleton 類似, 差別在於, scoped() binded class 會在每次新的 Laravel life cycle flush, 像是 queue worker process a new job, Octane worker process a new request
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    Request::macro('xml', function () {
    return CustomSuperDuperXmlParser::parse($this->body());
    });
    Http::fake([
    'example.com/*' => function (Request $request) {
    $this->assertSame($request->xml()->someProperty, 'some value')
    return Http::response();
    },
    ]);
  • Answer:
    使用 Requset 的 macro 來建立 customized method
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    use Illuminate\Support\Facades\Storage;

    $disk = Storage::build([
    'driver' => 'local',
    'root' => '/path/to/root',
    ]);

    $disk->put('image.jpg', $content);
  • Answer:
    可以在 runtime 利用 configuration 建立一個沒有事先定義於 config 檔中的 storage
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    # Default 24 hours
    php artisan prune:failed
    # Specify hours
    php artisan prune:failed --hours=12
  • Answer:
    利用 artisan command 清除 failed job table
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    php artisan route:list --sort precedence
  • Answer:
    使用 route resolved 的順序來排列
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    $response = new Response('foo');
    $response->setStatusCode(404);
    $response->statusText(); // i.e., Not Found
  • Answer:
    可使用 statusText() 取得 status 的 text
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    class AssignRequestId
    {
    public function handle($request, Closure $next)
    {
    $requestId = (string) Str::uuid();

    Log::withContext([
    'request-id' => $requestId
    ]);

    return $next($request)->header('Request-Id', $requestId);
    }
    }
  • Answer:
    使用 Log::withContext method, 給該 request 中的每一個 log 都加上一個 request-id 作為識別
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    Illuminate\Database\Connection::forgetRecordModificationState()
  • Answer:
    可在 boot() 中使用, 當讀寫分離時, 讓 queue worker 可以忘掉之前的狀態, 以免在 sticky 的設定下, 重複的使用 write 連線

存在瀏覽器的 cookie 中, 並且加密過

以下的 Laravel example code 的意思是?
  • Example:
    <?php
    use App\Exceptions\InternalException;

    protected function refundInstallmentItem(InstallmentItem $item)
    {
    $refundNo = $this->order->refund_no.'_'.$item->sequence;
    switch ($item->payment_method) {
    case 'wechat':
    app('wechat_pay')->refund([
    'transaction_id' => $item->payment_no,
    'total_fee' => $item->total * 100,
    'refund_fee' => $item->base * 100,
    'out_refund_no' => $refundNo,
    'notify_url' => '' // todo,
    ]);
    $item->update([
    'refund_status' => InstallmentItem::REFUND_STATUS_PROCESSING,
    ]);
    break;
    case 'alipay':
    $ret = app('alipay')->refund([
    'trade_no' => $item->payment_no,
    'refund_amount' => $item->base,
    'out_request_no' => $refundNo,
    ]);
    if ($ret->sub_code) {
    $item->update([
    'refund_status' => InstallmentItem::REFUND_STATUS_FAILED,
    ]);
    } else {
    $item->update([
    'refund_status' => InstallmentItem::REFUND_STATUS_SUCCESS,
    ]);
    }
    break;
    default:
    throw new InternalException('未知订单支付方式:'.$item->payment_method);
    break;
    }
    }
    }
  • Answer:
    <?php
    use App\Exceptions\InternalException;

    protected function refundInstallmentItem(InstallmentItem $item)
    {
    // 退款单号使用商品订单的退款号与当前还款计划的序号拼接而成
    $refundNo = $this->order->refund_no.'_'.$item->sequence;
    // 根据还款计划的支付方式执行对应的退款逻辑
    switch ($item->payment_method) {
    case 'wechat':
    app('wechat_pay')->refund([
    'transaction_id' => $item->payment_no, // 这里我们使用微信订单号来退款
    'total_fee' => $item->total * 100, //原订单金额,单位分
    'refund_fee' => $item->base * 100, // 要退款的订单金额,单位分,分期付款的退款只退本金
    'out_refund_no' => $refundNo, // 退款订单号
    // 微信支付的退款结果并不是实时返回的,而是通过退款回调来通知,因此这里需要配上退款回调接口地址
    'notify_url' => '' // todo,
    ]);
    // 将还款计划退款状态改成退款中
    $item->update([
    'refund_status' => InstallmentItem::REFUND_STATUS_PROCESSING,
    ]);
    break;
    case 'alipay':
    $ret = app('alipay')->refund([
    'trade_no' => $item->payment_no, // 使用支付宝交易号来退款
    'refund_amount' => $item->base, // 退款金额,单位元,只退回本金
    'out_request_no' => $refundNo, // 退款订单号
    ]);
    // 根据支付宝的文档,如果返回值里有 sub_code 字段说明退款失败
    if ($ret->sub_code) {
    $item->update([
    'refund_status' => InstallmentItem::REFUND_STATUS_FAILED,
    ]);
    } else {
    // 将订单的退款状态标记为退款成功并保存退款订单号
    $item->update([
    'refund_status' => InstallmentItem::REFUND_STATUS_SUCCESS,
    ]);
    }
    break;
    default:
    // 原则上不可能出现,这个只是为了代码健壮性
    throw new InternalException('未知订单支付方式:'.$item->payment_method);
    break;
    }
    }
    }
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    namespace App\Jobs;

    class RefundInstallmentOrder implements ShouldQueue
    {
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $order;

    public function __construct(Order $order)
    {
    $this->order = $order;
    }

    public function handle()
    {
    if ($this->order->payment_method !== 'installment'
    || !$this->order->paid_at
    || $this->order->refund_status !== Order::REFUND_STATUS_PROCESSING) {
    return;
    }
    if (!$installment = Installment::query()->where('order_id', $this->order->id)->first()) {
    return;
    }
    foreach ($installment->items as $item) {
    if (!$item->paid_at || in_array($item->refund_status, [
    InstallmentItem::REFUND_STATUS_SUCCESS,
    InstallmentItem::REFUND_STATUS_PROCESSING,
    ])) {
    continue;
    }

    try {
    $this->refundInstallmentItem($item);
    } catch (\Exception $e) {
    \Log::warning('分期退款失败:'.$e->getMessage(), [
    'installment_item_id' => $item->id,
    ]);

    continue;
    }
    }
    $allSuccess = true;
    foreach ($installment->items as $item) {
    if ($item->paid_at &&
    $item->refund_status !== InstallmentItem::REFUND_STATUS_SUCCESS) {
    $allSuccess = false;
    break;
    }
    }

    if ($allSuccess) {
    $this->order->update([
    'refund_status' => Order::REFUND_STATUS_SUCCESS,
    ]);
    }
    }

    protected function refundInstallmentItem(InstallmentItem $item)
    {
    // todo
    }
    }
  • Answer:
    <?php
    namespace App\Jobs;

    class RefundInstallmentOrder implements ShouldQueue
    {
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $order;

    public function __construct(Order $order)
    {
    $this->order = $order;
    }

    public function handle()
    {
    // 如果商品订单支付方式不是分期付款、订单未支付、订单退款状态不是退款中,则不执行后面的逻辑
    if ($this->order->payment_method !== 'installment'
    || !$this->order->paid_at
    || $this->order->refund_status !== Order::REFUND_STATUS_PROCESSING) {
    return;
    }
    // 找不到对应的分期付款,原则上不可能出现这种情况,这里的判断只是增加代码健壮性
    if (!$installment = Installment::query()->where('order_id', $this->order->id)->first()) {
    return;
    }
    // 遍历对应分期付款的所有还款计划
    foreach ($installment->items as $item) {
    // 如果还款计划未支付,或者退款状态为退款成功或退款中,则跳过
    if (!$item->paid_at || in_array($item->refund_status, [
    InstallmentItem::REFUND_STATUS_SUCCESS,
    InstallmentItem::REFUND_STATUS_PROCESSING,
    ])) {
    continue;
    }
    // 调用具体的退款逻辑,
    try {
    $this->refundInstallmentItem($item);
    } catch (\Exception $e) {
    \Log::warning('分期退款失败:'.$e->getMessage(), [
    'installment_item_id' => $item->id,
    ]);
    // 假如某个还款计划退款报错了,则暂时跳过,继续处理下一个还款计划的退款
    continue;
    }
    }
    // 设定一个全部退款成功的标志位
    $allSuccess = true;
    // 再次遍历所有还款计划
    foreach ($installment->items as $item) {
    // 如果该还款计划已经还款,但退款状态不是成功
    if ($item->paid_at &&
    $item->refund_status !== InstallmentItem::REFUND_STATUS_SUCCESS) {
    // 则将标志位记为 false
    $allSuccess = false;
    break;
    }
    }
    // 如果所有退款都成功,则将对应商品订单的退款状态修改为退款成功
    if ($allSuccess) {
    $this->order->update([
    'refund_status' => Order::REFUND_STATUS_SUCCESS,
    ]);
    }
    }

    protected function refundInstallmentItem(InstallmentItem $item)
    {
    // todo
    }
    }
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    $mock = \Mockery::mock('CaptainsConsole');
    $mock->shouldReceive('foo->bar->zebra->alpha->selfDestruct')->andReturn('Ten!');
  • Answer:
    當一個 object 會呼叫多個 method chain 時, 可帶入整個 chain, 並定義 return value, mockery 會忽略中間所有被呼叫的 method
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    class Model
    {
    public function test(&$data)
    {
    return $this->doTest($data);
    }

    protected function doTest(&$data)
    {
    $data['something'] = 'wrong';
    return $this;
    }
    }

    class Test extends \PHPUnit\Framework\TestCase
    {
    public function testModel()
    {
    $mock = \Mockery::mock('Model[test]')->shouldAllowMockingProtectedMethods();

    $mock->shouldReceive('test')
    ->with(\Mockery::on(function(&$data) {
    $data['something'] = 'wrong';
    return true;
    }));

    $data = array('foo' => 'bar');

    $mock->test($data);
    $this->assertTrue(isset($data['something']));
    $this->assertEquals('wrong', $data['something']);
    }
    }

  • Answer:
    通常比較少遇到這種案例, 帶入 reference parameter 到 protected method, 可 mock 上一層的 public method with shouldAllowMockingProtectedMethods(), 這個 public method 會被當作是 protected method 的 proxy
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    namespace App\Console\Commands\Cron;

    class CalculateInstallmentFine extends Command
    {
    protected $signature = 'cron:calculate-installment-fine';

    protected $description = '计算分期付款逾期费';

    public function handle()
    {
    InstallmentItem::query()
    // 预加载分期付款数据,避免 N + 1 问题
    ->with(['installment'])
    ->whereHas('installment', function ($query) {
    // 对应的分期状态为还款中
    $query->where('status', Installment::STATUS_REPAYING);
    })
    // 还款截止日期在当前时间之前
    ->where('due_date', '<=', Carbon::now())
    // 尚未还款
    ->whereNull('paid_at')
    // 使用 chunkById 避免一次性查询太多记录
    ->chunkById(1000, function ($items) {
    // 遍历查询出来的还款计划
    foreach ($items as $item) {
    // 通过 Carbon 对象的 diffInDays 直接得到逾期天数
    $overdueDays = Carbon::now()->diffInDays($item->due_date);
    // 本金与手续费之和
    $base = big_number($item->base)->add($item->fee)->getValue();
    // 计算逾期费
    $fine = big_number($base)
    ->multiply($overdueDays)
    ->multiply($item->installment->fine_rate)
    ->divide(100)
    ->getValue();
    // 避免逾期费高于本金与手续费之和,使用 compareTo 方法来判断
    // 如果 $fine 大于 $base,则 compareTo 会返回 1,相等返回 0,小于返回 -1
    $fine = big_number($fine)->compareTo($base) === 1 ? $base : $fine;
    $item->update([
    'fine' => $fine,
    ]);
    }
    });
    }
    }
  • Answer:
    跑 cron 例行檢查逾期款項, 並算出逾期金額
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    public function testCanOverrideExpectedParametersOfInternalPHPClassesToPreserveRefs()
    {
    \Mockery::getConfiguration()->setInternalClassMethodParamMap(
    'MongoCollection',
    'insert',
    array('&$data', '$options = array()')
    );
    $m = \Mockery::mock('MongoCollection');
    $m->shouldReceive('insert')->with(
    \Mockery::on(function(&$data) {
    if (!is_array($data)) return false;
    $data['_id'] = 123;
    return true;
    }),
    \Mockery::any()
    );

    $data = array('a'=>1,'b'=>2);
    $m->insert($data);

    $this->assertTrue(isset($data['_id']));
    $this->assertEquals(123, $data['_id']);

    \Mockery::resetContainer();
    }
  • Answer:
    一般來說, mockery 可以 mock call by reference, 但對 internal method 不起作用。 可以使用 setInternalClassMethodParamMap() 來 map class, method, 以及 args
Laravel 當中, 以下的語法代表什麼意思?

列出 $users query builder 的 query 語法以及帶入的變數

<?php
dd($users->toSql(), $users->getBindings());
Laravel 當中, 以下代碼代表什麼意思?

$user->address 可獲得時, 取值, 不可獲得時, 回傳 null

<?php
return optional($user->address)->street;
以下的 Laravel 程式碼代表什麼意思?
<?php
$users = User::merchant()
->with('owner')
->when($q, function ($query, $q) {
return $query->where('username', 'LIKE', '%'.$q.'%')
->orWhere('name', 'LIKE', '%'.$q.'%')
->orWhere('email', 'LIKE', '%'.$q.'%');
});

public function scopeMerchant(Builder $query)
{
return $query->where('role_id', Role::MERCHANT);
}

取得 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
return new UserCollection($users->paginate($row)->appends(request()->query->all()));
以下的 Laravel 程式碼中的 latest 代表什麼意思? 以 id 排序
<?php
$transactions = auth()->user()->transactions()
->whereBetween('created_at', [$startedAt, $endedAt])
->latest('id')
->with('wallet')
->paginate()
->appends($request->query->all());
以下的 Laravel 程式碼中的 fill 代表什麼意思? 將值注入 userBankCard model, 帶入參數可以是一個 array
<?php
$userBankCard->fill(
$request->only(
'card_holder_name', 'card_number', 'bank_name'
)
);
以下的 Laravel 程式碼中, 為什麼要使用 collect function? 這樣如果前端帶錯, 帶成 string 的話, 會先將 string 轉成 collection, 再轉成 array
<?php
$userBankCards = UserBankCard::when(request()->q, function ($query, $q) {
$query->where(function ($query) use ($q) {
$query->where('card_holder_name', 'LIKE', '%' . $q . '%')
->orWhere('card_number', 'LIKE', '%' . $q . '%')
->orWhere('bank_name', 'LIKE', "%$q%");
});
})
->when(request()->status, function ($query, $status) {
$query->whereIn('status', collect($status)->toArray());
})
->where('user_id', auth()->user()->getKey())
->paginate($row)
->appends(request()->query->all());
以下的 Laravel 程式碼代表什麼意思?
<?php
$depositStats = Deposit::whereBetween('created_at', [$startDate, $endDate])->where('status', Deposit::STATUS_SUCCESS)->groupBy('system_bank_card_type')->get([
'system_bank_card_type',
DB::raw('SUM(amount) AS total_amount, SUM(fee) AS total_fee, COUNT(id) AS total_count')
])->keyBy('system_bank_card_type');
取得 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
$withdrawStat = Withdraw::whereBetween('created_at', [$startDate, $endDate])
->where('status', Withdraw::STATUS_SUCCESS)
->first([
DB::raw('SUM(amount) AS total_amount, SUM(fee) AS total_fee, COUNT(id) AS total_count')
]);
以下的 Laravel 程式碼中, keyBy 的用途是?

如果不使用 keyBy 的話, 正常來說一個 collection 裡頭有多個 model 會以默認 index, 0, 1, 2 …, keyBy 可以使用指定的 key 來給 model 分組, 在這個例子中, 就是以 model 下的 slug 欄位的值做分組

<?php
$wallets = auth()->user()->wallets()->get()->keyBy('slug');
以下的 Laravel 程式碼中, data_get 的用途是?

$depositStats 結構像是這樣 $depositStats = ['BankCard::TYPE_FEE' => ['total_count', 'total_amount', 'total_count', 'total_amount']], data_get 可以取得一個 collection 裡頭的巢狀 array 值

<?php
return response()->json([
'data' => [
'fee_wallet_deposit_success_count' => data_get($depositStats, [BankCard::TYPE_FEE, 'total_count'], 0),
'fee_wallet_deposit_success_amount' => data_get($depositStats, [BankCard::TYPE_FEE, 'total_amount'], 0) / 100, // todo remove hard code
'withdraw_wallet_deposit_success_count' => data_get($depositStats, [BankCard::TYPE_WITHDRAW, 'total_count'], 0),
'withdraw_wallet_deposit_success_amount' => data_get($depositStats, [BankCard::TYPE_WITHDRAW, 'total_amount'], 0) / 100, // todo remove hard code
'withdraw_success_count' => $withdrawStat->total_count ?? 0,
'withdraw_success_amount' => ($withdrawStat->total_amount ?? 0) / 100, // todo remove hard code
'fee_wallet_balance' => data_get($wallets, [User::SLUG_FEE_WALLET, 'balanceFloat'], 0),
'withdraw_wallet_balance' => data_get($wallets, [User::SLUG_WITHDRAW_WALLET, 'balanceFloat'], 0),
],
]);
以下的 Laravel 程式碼代表什麼意思?

定義一個 unique rule, 並且將範圍限定在特定的 user 上, 代表不同 user 之間的訂單不需要 unique

<?php
$orderNumberUniqueRule = Rule::unique($withdrawTable, 'order_number')->where(function ($query) use ($user) {
$query->where('user_id', $user->getKey());
});
以下的 Laravel function 的作用是什麼?
  1. 將 request 裡的參數除了 sign 之外都調出來
  2. 排列這些 key
  3. 首先使用 http_build_query function 針對剛剛的參數來產生一組 url 加密的字串, 然後將這字串與 user 的 secret_key 欄位內的值相串, 然後使用 url 解密這一整個字串, 最後再使用 md5 處理取得 hash 值, 我們比對這個值跟帶進來的 sign 有沒有一樣, 如果不一樣就是不合法
  4. 唯有知道 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 的作用是什麼?
  1. 宣告 lock-key 以及持有時間
  2. 嘗試取得 lock-key, 如果不可得, 持續嘗試五秒
  3. 用 transaction 實作, 若有任何錯誤皆返回
  4. 如果無法取得 lock-key, 返回錯誤
  5. 回返錯誤訊息
  6. 如果 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
$transaction = $user->withdrawFloat($request->input('amount'), [
'before_balance' => $user->balance,
]);
以下的 Laravel 程式碼是什麼意思呢?

將資料存入 mysql 中的 json 欄位

<?php
$deposit->user_bank_meta = (object)[
'subbranch' => $userBankCard->subbranch,
'province' => $userBankCard->province,
'city' => $userBankCard->city,
];
下面的 Laravel 程式碼是什麼意思?

將檔案存在 $deposit->getTable(), 檔名為 $deposit->system_order_number, 使用 filesystem.cloud, 可設為 s3

<?php
$request->file('payment_instrument')
->storeAs($deposit->getTable(), $deposit->system_order_number, config('filesystems.cloud'));
下面的 Laravel Requests 代表什麼意思?

使用 captcha 的 extension captcha_api 來驗證, 因為該驗證器一定需要一個 key, 如果在 validation 期間 key 為 null, 那直接就回 500 了, 所以這邊處理, 當沒有 captcha_key 時, 給一個隨機 10 碼, 這樣會驗不過(key 沒帶原本就應該驗不過), 但是不會 500

<?php
public function rules()
{
return [
'username' => 'required_without:email|string',
'email' => 'required_without:username|email',
'password' => 'required|string',
'captcha_key' => 'required',
'captcha' => 'required|captcha_api:'.($this->request->get('captcha_key') ?? Str::random(10)),
];
}
以下的 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
2. withTrashed 代表強制顯示已被 soft deleted 的 model
3. 取出第一筆
4. 若無結果, 則根據輸入的資料建立一張卡
5. 更新 bankCard
6. 若 bankCard 為 soft deleted, 解除它
<?php
$bankCard = BankCard::where($request->only('card_number'))
->withTrashed()
->first() ?? BankCard::create($data);

$bankCard->update($data);
$bankCard->restore();
在 Laravel 中, 如何從一個 collection 當中取得其中一個 model, 而該 model 中的 price 欄位的值是在這個 collection 的所有 model 之中最小或最大的?
<?php
$min = $data->where('price', $data->min('price'))->first();
// ['name' => 'test', 'price' => 10]
$max = $data->where('price', $data->max('price'))->first();
// ['name' => 'test', 'price' => 600]
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    collection->push($model)
  • Answer:
    將 $model push 到該 collection 中
以下的 Laravel 程式碼代表什麼意思?
1. 帶入欄位的名稱需與資料庫的欄位一樣
2. 依序檢查指定的欄位
3. 若 request 中的該欄位是有值的, 並且該值與目前資料庫中的值是不同的, 這代表有變更產生
4. 將變更的 key 跟 value 放入空的 collection $updateAttributes 中
5. 如果這個 collection 含有 card_number, 這代表卡號變更了, 將 balance => 0 放入 updateAttributes
6. 如果 updateAttributes 是存在的, 開始更新
<?php
$updatedAttributes = collect();

foreach (['card_holder_name', 'card_number', 'bank_name', 'type', 'auto_withdraw'] as $attribute) {
if (!is_null($request->$attribute) && ($request->$attribute != $bankCard->$attribute)) {
$updatedAttributes = $updatedAttributes->merge([$attribute => $request->$attribute]);
}
}

if ($updatedAttributes->has('card_number')) {
$updatedAttributes = $updatedAttributes->merge(['balance' => 0]);
}

if ($updatedAttributes->isNotEmpty()) {
$bankCard->update($updatedAttributes->toArray());
}
Laravel 中, Model 的命名通常是單數還是複數?

單數

Laravel 中的變數命名習慣是?

camel case

Laravel 中, 如何將 namespace, prefix, middleware 同時作用到複數的 route 上?
<?php
Route::group([
'namespace' => 'Worker',
'prefix' => 'worker',
'middleware' => 'check.worker.token',
], function () {
Route::apiResource('working-tasks', 'WorkingTaskController')->only('store', 'update');
Route::apiResource('bank-cards', 'BankCardController')->only('update');

Route::post('captcha-cracks', 'CrackCaptchaController');
});
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
public function handle($request, Closure $next)
{
if (app()->environment(['production'])) {
abort_if(!IPs::where('address', Arr::last($request->ips()))->exists(), Response::HTTP_UNAUTHORIZED,
'Invalid source');
}

return $next($request);
}
以下的兩段 Laravel 在 Resource 中的程式碼, 有什麼差異?
  1. <?php
    'withdraws' => $this->whenLoaded('withdraws', Withdraw::collection($this->withdraws))
  2. <?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 天
2.7 GHz 的 processor, 每秒可以跑多少 cycle?

2,700,000,000

Laravel 中, 何謂 I/O bound code?

Waits for DB queries, HTTP requests, etc…

Laravel 中, 何謂 CPU bound code?

Do a lot of calculation

Laravel 中, 如何增加 php-fpm worker 的數量?

update pm.max_children inside the /etc/php/{version}/fpm/pool.d/www.conf file

Laravel 中, 為何不建議以下的 example 語法?
  • Example:
    <?php
    $posts = POST::whereDate('created_at', '>=', now() )->get();
  • Answer:
    會使用到 MYSQL function, 變成 full table scan
Laravel 中, 何時該使用 chunkById()?

只要 id 是 auto increment primary key, 都使用 chunkById()

Laravel 中, chunkById() 與 chunk() 的差異是?
  • chunk:
    select * from posts offset 0 limit 100
    select * from posts offset 101 limit 100
  • chunkById:
    select * from posts order by id asc limit 100
    select * from posts where id > 100 order by id asc limit 100
Laravel 中, 何時使用 cursor(), 何時使用 chunkById()?

當 DB memory 比較充裕時, 使用 cursor(), 當 APP memory 比較充裕時, 使用 chunkById()

以下的 Laravel example code 的意思是?
  • Example:
    <?php
    public function getIsYearFeeLateAttribute(): bool
    {
    return $this->memberDetail->{__FUNCTION__}();
    }
  • Answer:
    從 relational model 執行此 function name, 所以在別處也可以用這個寫在 relational model 的 method, 不需要重寫
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 client 端傳來的時間是沒有時區的, Laravel 會默認該時間的時區為 Laravel 的 default timezone, 不多做處理直接存到資料庫
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 client 端傳來的時間有時區的, Laravel 會將傳來的時間轉換為 default timezone 的時區, 再存到資料庫
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 Laravel 從資料庫取得時間後, 會將該時間的時區默認為 Laravel default timezone, 並轉成 ISO8601 格式給 client
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 MySQL 的 column type 為 datetime 時, 收到什麼就存什麼, 不另外轉換
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 MySQL 的 column type 為 timestamp 時, 會將收到的時間的時區設為當下 MySQL default timezone, 並轉為 UTC timezone 儲存下來, 當使用 select 時, MySQL 會將實際上存為 UTC 的時間轉換為當下 MySQL timezone 的時間
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 MySQL 時區為 datetime 時, 不管怎麼調整 MySQL 的時區, 都不會影響回傳的結果
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 MySQL 時區為 timestamp 時, 如果去修改 MySQL timezone, 會先將實際儲存的 UTC timezone 時間轉換為該 timezone 的時間, 再回傳
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    $branchManagers = BranchManager::query()
    ->select(['branch_managers.account', 'branch_managers.created_at', 'shops.name', 'branch_managers.status_id'])
    ->join('shops', 'branch_managers.shop_id', '=', 'shops.id')
    ->when($search, function ($query) use ($search) {
    $query->whereIn('branch_managers.id', function ($query) use ($search) {
    $query->select('id')
    ->from(function ($query) use ($search) {
    $query->select('id')
    ->from('branch_managers')
    ->whereRaw('match(account) against (? in boolean mode)', [$search])
    ->union(
    $query->newQuery()
    ->select('branch_managers.id')
    ->from('branch_managers')
    ->join('shops', 'branch_managers.shop_id', '=', 'shops.id')
    ->whereRaw('match(name) against (? in boolean mode)', [$search])
    );
    }, 'matches');
    });
    })->get();
  • Answer:
    無法直接使用 orWhereRaw (match(…) …) 這樣子的 full text search 在多個表格, 因此統一取得 id, 在使用 where in 取得最後需要的資料
    使用 union 來取得兩個 select query 取得的不重複 id
    使用 derived table, 避免 where in 與 union query 之間的 dependency
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    class OrderRequest extends Request
    {
    public function rules()
    {
    return [
    'address_id' => [
    'required',
    Rule::exists('user_addresses', 'id')->where('user_id', $this->user()->id),
    ],
    'items.*.sku_id' => [
    'required',
    function ($attribute, $value, $fail) {
    if (!$sku = ProductSku::find($value)) {
    return $fail('该商品不存在');
    }
    if (!$sku->product->on_sale) {
    return $fail('该商品未上架');
    }
    if ($sku->stock === 0) {
    return $fail('该商品已售完');
    }
    preg_match('/items\.(\d+)\.sku_id/', $attribute, $m);
    $index = $m[1];
    $amount = $this->input('items')[$index]['amount'];
    if ($amount > 0 && $amount > $sku->stock) {
    return $fail('该商品库存不足');
    }
    },
    ],
    'items.*.amount' => ['required', 'integer', 'min:1'],
    ];
    }
    }
  • Answer:
    檢查該 address 是否屬於該 user
    從 items 中使用正則找到該 sku_id, 並檢查庫存是否足夠
以下的 Laravel example code 的意思是?
  • Example:
    <?php

    namespace App\Models;

    class Order extends Model
    {
    use HasFactory;

    protected static function boot()
    {
    parent::boot();
    // 监听模型创建事件,在写入数据库之前触发
    static::creating(function ($model) {
    // 如果模型的 no 字段为空
    if (!$model->no) {
    // 调用 findAvailableNo 生成订单流水号
    $model->no = static::findAvailableNo();
    // 如果生成失败,则终止创建订单
    if (!$model->no) {
    return false;
    }
    }
    });
    }

    public static function findAvailableNo()
    {
    // 订单流水号前缀
    $prefix = date('YmdHis');
    for ($i = 0; $i < 10; $i++) {
    // 随机生成 6 位的数字
    $no = $prefix.str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
    // 判断是否已经存在
    if (!static::query()->where('no', $no)->exists()) {
    return $no;
    }
    }
    \Log::warning('find order no failed');

    return false;
    }
    }
  • Answer:
    在 order model 建立時, 觸發 creating event, 建立訂單流水號
以下的 Laravel example code 的意思是?
  • Example:
    <?php

    namespace App\Http\Requests;

    use App\Models\ProductSku;

    class AddCartRequest extends Request
    {
    public function rules()
    {
    return [
    'sku_id' => [
    'required',
    function ($attribute, $value, $fail) {
    if (!$sku = ProductSku::find($value)) {
    return $fail('该商品不存在');
    }
    if (!$sku->product->on_sale) {
    return $fail('该商品未上架');
    }
    if ($sku->stock === 0) {
    return $fail('该商品已售完');
    }
    if ($this->input('amount') > 0 && $sku->stock < $this->input('amount')) {
    return $fail('该商品库存不足');
    }
    },
    ],
    'amount' => ['required', 'integer', 'min:1'],
    ];
    }
    }
  • Answer:
    使用 closure 驗證商品是否存在, 是否上架, 是否售完, 以及是否有數量但實際庫存不足
以下的 Laravel example code 的意思是?
  • Example:
    <?php

    namespace Database\Factories;

    use App\Models\UserAddress;
    use Illuminate\Database\Eloquent\Factories\Factory;

    class UserAddressFactory extends Factory
    {
    protected $model = UserAddress::class;

    public function definition()
    {
    $addresses = [
    ["北京市", "市辖区", "东城区"],
    ["河北省", "石家庄市", "长安区"],
    ["江苏省", "南京市", "浦口区"],
    ["江苏省", "苏州市", "相城区"],
    ["广东省", "深圳市", "福田区"],
    ];
    $address = $this->faker->randomElement($addresses);

    return [
    'province' => $address[0],
    'city' => $address[1],
    'district' => $address[2],
    'address' => sprintf('第%d街道第%d号', $this->faker->randomNumber(2), $this->faker->randomNumber(3)),
    'zip' => $this->faker->postcode,
    'contact_name' => $this->faker->name,
    'contact_phone' => $this->faker->phoneNumber,
    ];
    }
    }
  • Answer:
    利用 factory 產生住址
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    public function getSpecifiedConstants($className, $needle)
    {
    return collect((new \ReflectionClass($className))->getConstants())
    ->filter(function ($value, $key) use ($needle) {
    return Str::startsWith($key, $needle);
    });
    }
  • Answer:
    使用 ReflectionClass(), 從指定的 class 中取得定義於該 class 中的 constants, 在使用特定的條件篩選
以下的 Laravel example code, 有何差異?
  • Example:
    <?php
    $rows = [$row1, $row2, $row3]
    DB::table('voucher_statements')->insert($rows);
    Voucher::insert($row1);
  • Answer:
    前者可支援多筆, 但不會 insert created_at 跟 updated_at, 後者只能一次一筆, 但會 insert created_at, updated_at
以下的 Laravel example code 會使用幾筆 SQL query?
  • Example:
    <?php
    $rows = [$row1, $row2, $row3]
    DB::table('voucher_statements')->insert($rows);
  • Answer:
    一筆
以下的 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 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 columnquery
    // 因為若使用 whereRawregular 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 作用為建立一個 virtual column, 此行 code 會將 first_name column 中
    // 只要不是 a-zA-Z0-9 的都替換成 '', 然後再將內容存到 first_name_normalized 這個
    // 虛擬 column, 並建立 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
安裝完新的 PHP Extension, 記得要做些什麼事?

重啟 PHP, 重啟 Web server, 若使用 valet, 記得重啟 valet

以下的 Laravel example code 的意思是?
  • Example:
    <?php
    use Illuminate\Validation\Rule;
    use App\Models\Installment;
    .
    .
    .
    public function payByInstallment(Order $order, Request $request)
    {
    // 判断订单是否属于当前用户
    $this->authorize('own', $order);
    // 订单已支付或者已关闭
    if ($order->paid_at || $order->closed) {
    throw new InvalidRequestException('订单状态不正确');
    }
    // 订单不满足最低分期要求
    if ($order->total_amount < config('app.min_installment_amount')) {
    throw new InvalidRequestException('订单金额低于最低分期金额');
    }
    // 校验用户提交的还款月数,数值必须是我们配置好费率的期数
    $this->validate($request, [
    'count' => ['required', Rule::in(array_keys(config('app.installment_fee_rate')))],
    ]);
    // 删除同一笔商品订单发起过其他的状态是未支付的分期付款,避免同一笔商品订单有多个分期付款
    Installment::query()
    ->where('order_id', $order->id)
    ->where('status', Installment::STATUS_PENDING)
    ->delete();
    $count = $request->input('count');
    // 创建一个新的分期付款对象
    $installment = new Installment([
    // 总本金即为商品订单总金额
    'total_amount' => $order->total_amount,
    // 分期期数
    'count' => $count,
    // 从配置文件中读取相应期数的费率
    'fee_rate' => config('app.installment_fee_rate')[$count],
    // 从配置文件中读取当期逾期费率
    'fine_rate' => config('app.installment_fine_rate'),
    ]);
    $installment->user()->associate($request->user());
    $installment->order()->associate($order);
    $installment->save();
    // 第一期的还款截止日期为明天凌晨 0 点
    $dueDate = Carbon::tomorrow();
    // 计算每一期的本金
    $base = big_number($order->total_amount)->divide($count)->getValue();
    // 计算每一期的手续费
    $fee = big_number($base)->multiply($installment->fee_rate)->divide(100)->getValue();
    // 根据用户选择的还款期数,创建对应数量的还款计划
    for ($i = 0; $i < $count; $i++) {
    // 最后一期的本金需要用总本金减去前面几期的本金
    if ($i === $count - 1) {
    $base = big_number($order->total_amount)->subtract(big_number($base)->multiply($count - 1));
    }
    $installment->items()->create([
    'sequence' => $i,
    'base' => $base,
    'fee' => $fee,
    'due_date' => $dueDate,
    ]);
    // 还款截止日期加 30 天
    $dueDate = $dueDate->copy()->addDays(30);
    }

    return $installment;
    }
    .
    .
    .
  • Answer:
    分期付款的 example
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    namespace App\Services;

    use App\Models\Category;

    class CategoryService
    {
    // 这是一个递归方法
    // $parentId 参数代表要获取子类目的父类目 ID,null 代表获取所有根类目
    // $allCategories 参数代表数据库中所有的类目,如果是 null 代表需要从数据库中查询
    public function getCategoryTree($parentId = null, $allCategories = null)
    {
    if (is_null($allCategories)) {
    // 从数据库中一次性取出所有类目
    $allCategories = Category::all();
    }

    return $allCategories
    // 从所有类目中挑选出父类目 ID 为 $parentId 的类目
    ->where('parent_id', $parentId)
    // 遍历这些类目,并用返回值构建一个新的集合
    ->map(function (Category $category) use ($allCategories) {
    $data = ['id' => $category->id, 'name' => $category->name];
    // 如果当前类目不是父类目,则直接返回
    if (!$category->is_directory) {
    return $data;
    }
    // 否则递归调用本方法,将返回值放入 children 字段中
    $data['children'] = $this->getCategoryTree($category->id, $allCategories);

    return $data;
    });
    }
    }
  • Answer:
    使用遞迴, 將資料庫中多層 category 的結構, 撈出並組成 multi array
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    use App\Models\Category;
    .
    .
    .
    public function index(Request $request)
    {
    $builder = Product::query()->where('on_sale', true);
    if ($search = $request->input('search', '')) {
    .
    .
    .
    }

    // 如果有传入 category_id 字段,并且在数据库中有对应的类目
    if ($request->input('category_id') && $category = Category::find($request->input('category_id'))) {
    // 如果这是一个父类目
    if ($category->is_directory) {
    // 则筛选出该父类目下所有子类目的商品
    $builder->whereHas('category', function ($query) use ($category) {
    // 这里的逻辑参考本章第一节
    $query->where('path', 'like', $category->path.$category->id.'-%');
    });
    } else {
    // 如果这不是一个父类目,则直接筛选此类目下的商品
    $builder->where('category_id', $category->id);
    }
    }
    .
    .
    .
    }
    .
    .
    .
  • Answer:
    使用 path string 來取得所有 children rows
以下的 Laravel example code 的意思是?
  • Example:
    <?php

    use App\Models\Category;
    use Illuminate\Database\Seeder;

    class CategoriesSeeder extends Seeder
    {
    public function run()
    {
    $categories = [
    [
    'name' => '手机配件',
    'children' => [
    ['name' => '手机壳'],
    ['name' => '贴膜'],
    ['name' => '存储卡'],
    ['name' => '数据线'],
    ['name' => '充电器'],
    [
    'name' => '耳机',
    'children' => [
    ['name' => '有线耳机'],
    ['name' => '蓝牙耳机'],
    ],
    ],
    ],
    ],
    [
    'name' => '电脑配件',
    'children' => [
    ['name' => '显示器'],
    ['name' => '显卡'],
    ['name' => '内存'],
    ['name' => 'CPU'],
    ['name' => '主板'],
    ['name' => '硬盘'],
    ],
    ],
    [
    'name' => '电脑整机',
    'children' => [
    ['name' => '笔记本'],
    ['name' => '台式机'],
    ['name' => '平板电脑'],
    ['name' => '一体机'],
    ['name' => '服务器'],
    ['name' => '工作站'],
    ],
    ],
    [
    'name' => '手机通讯',
    'children' => [
    ['name' => '智能机'],
    ['name' => '老人机'],
    ['name' => '对讲机'],
    ],
    ],
    ];

    foreach ($categories as $data) {
    $this->createCategory($data);
    }
    }

    protected function createCategory($data, $parent = null)
    {
    // 创建一个新的类目对象
    $category = new Category(['name' => $data['name']]);
    // 如果有 children 字段则代表这是一个父类目
    $category->is_directory = isset($data['children']);
    // 如果有传入 $parent 参数,代表有父类目
    if (!is_null($parent)) {
    $category->parent()->associate($parent);
    }
    // 保存到数据库
    $category->save();
    // 如果有 children 字段并且 children 字段是一个数组
    if (isset($data['children']) && is_array($data['children'])) {
    // 遍历 children 字段
    foreach ($data['children'] as $child) {
    // 递归调用 createCategory 方法,第二个参数即为刚刚创建的类目
    $this->createCategory($child, $category);
    }
    }
    }
    }
  • Answer:
    使用遞迴來儲存 nested 階層
以下的 Laravel example code 的意思是?
  • Example:
    <?php
    namespace App\Models;

    use Illuminate\Database\Eloquent\Model;

    class Category extends Model
    {
    protected $fillable = ['name', 'is_directory', 'level', 'path'];
    protected $casts = [
    'is_directory' => 'boolean',
    ];

    protected static function boot()
    {
    parent::boot();
    // 监听 Category 的创建事件,用于初始化 path 和 level 字段值
    static::creating(function (Category $category) {
    // 如果创建的是一个根类目
    if (is_null($category->parent_id)) {
    // 将层级设为 0
    $category->level = 0;
    // 将 path 设为 -
    $category->path = '-';
    } else {
    // 将层级设为父类目的层级 + 1
    $category->level = $category->parent->level + 1;
    // 将 path 值设为父类目的 path 追加父类目 ID 以及最后跟上一个 - 分隔符
    $category->path = $category->parent->path.$category->parent_id.'-';
    }
    });
    }

    public function parent()
    {
    return $this->belongsTo(Category::class);
    }

    public function children()
    {
    return $this->hasMany(Category::class, 'parent_id');
    }

    public function products()
    {
    return $this->hasMany(Product::class);
    }

    // 定义一个访问器,获取所有祖先类目的 ID 值
    public function getPathIdsAttribute()
    {
    // trim($str, '-') 将字符串两端的 - 符号去除
    // explode() 将字符串以 - 为分隔切割为数组
    // 最后 array_filter 将数组中的空值移除
    return array_filter(explode('-', trim($this->path, '-')));
    }

    // 定义一个访问器,获取所有祖先类目并按层级排序
    public function getAncestorsAttribute()
    {
    return Category::query()
    // 使用上面的访问器获取所有祖先类目 ID
    ->whereIn('id', $this->path_ids)
    // 按层级排序
    ->orderBy('level')
    ->get();
    }

    // 定义一个访问器,获取以 - 为分隔的所有祖先类目名称以及当前类目的名称
    public function getFullNameAttribute()
    {
    return $this->ancestors // 获取所有祖先类目
    ->pluck('name') // 取出所有祖先类目的 name 字段作为一个数组
    ->push($this->name) // 将当前类目的 name 字段值加到数组的末尾
    ->implode(' - '); // 用 - 符号将数组的值组装成一个字符串
    }
    }
  • Answer:
    使用 path 字串來快速取得 parent 以及 children
以下的 Laravel example code 的意思是?
  • Example:
    foreach (
    ['status', 'system_order_number', 'order_number', 'user_card_number', 'system_card_number'] as
    $availableFilterKey
    ) {
    $deposits->when($request->{$availableFilterKey},
    function (Builder $builder, $filterValue) use ($availableFilterKey) {
    $builder->where($availableFilterKey, $filterValue);
    });
    }
  • Answer:
    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?
# 如果沒複寫, 直接呼叫即可
$this->method

# 如果有複寫, 也是直接呼叫
$this->method

# 除非你有複寫, 但你要沒複寫的版本
parent::method

參考出處: https://stackoverflow.com/questions/11237511/multiple-ways-of-calling-parent-method-in-php
資料庫欄位 int, 什麼是 signed 跟 unsigned?
signed 的數值範圍橫跨正負數, unsigned 只有正數, 且為 signed 的兩倍, 但最小的負數為 0
可參考文件如下: https://dev.mysql.com/doc/refman/8.0/en/integer-types.html
Laravel 中, 什麼是 coroutine?

像是 thread, 但不會 context switching, 因為 OS 並不知道它的存在, 所以當 Library support non-blocking I/O 時, I/O bound work 可以 run concurrently, CPU bound work 還是 synchronously (因為每一個 process 被分配到的 CPU usage 是相同的)

PHPSTORM 學習筆記 PHP 學習筆記

留言

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×