> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 任務排程

> 學習如何用 Laravel 的排程器優雅地管理定期執行的任務。

## 什麼是任務排程

過去在伺服器上要定期執行的任務，每個都需手動撰寫 cron entry。
但這樣的排程定義存在於原始碼之外，無法納入版本控制，每次確認或變更都需要 SSH 登入。

使用 Laravel 的排程器，可**在應用程式內流暢定義排程**。
在伺服器上只需登記一行 cron entry，排程定義隨程式碼一起納入版控。

排程通常寫在 `routes/console.php`，是標準風格。

```php theme={null}
<?php

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;

Schedule::call(function () {
    DB::table('recent_users')->delete();
})->daily();
```

<Info>
  用 `schedule:list` Artisan 指令可查看已定義任務清單與下次執行時間。

  ```shell theme={null}
  php artisan schedule:list
  ```
</Info>

### 排程器的執行流程

```mermaid theme={null}
flowchart TD
    A["* * * * * artisan schedule:run<br>（每分鐘由 cron 呼叫）"] --> B["Console Kernel"]
    B --> C["呼叫 schedule() 方法"]
    C --> D["依序評估已登記任務"]
    D --> E{"Cron 式是否符合<br>當前時間？"}
    E -->|"不符合"| F["略過"]
    E -->|"符合"| G{"檢查 when / skip /<br>environments 條件"}
    G -->|"條件不符"| F
    G -->|"條件符合"| H{"是否於維護<br>模式中？"}
    H -->|"一般模式"| I["執行任務<br>(before hook → 本體 → after hook)"]
    H -->|"維護模式"| J{"是否有<br>evenInMaintenanceMode()？"}
    J -->|"是"| I
    J -->|"否"| F
```

## 排程的定義方式

排程定義寫在 `routes/console.php`。
也可以透過 `bootstrap/app.php` 的 `withSchedule` 方法定義。

```php theme={null}
// bootstrap/app.php
use Illuminate\Console\Scheduling\Schedule;

->withSchedule(function (Schedule $schedule) {
    $schedule->call(new DeleteRecentUsers)->daily();
})
```

## 可排程的任務種類

### Artisan 指令的排程

用 `command` 方法排程 Artisan 指令。
可用指令名稱或類別名稱指定。

```php theme={null}
use App\Console\Commands\SendEmailsCommand;
use Illuminate\Support\Facades\Schedule;

// 以指令名稱指定
Schedule::command('emails:send Taylor --force')->daily();

// 以類別名稱指定（可用陣列傳引數）
Schedule::command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();
```

在 closure 定義的 Artisan 指令，也可在定義後緊接鏈式排程方法。

```php theme={null}
Artisan::command('delete:recent-users', function () {
    DB::table('recent_users')->delete();
})->purpose('Delete recent users')->daily();
```

### Queue Job 的排程

用 `job` 方法排程[queue job](/zh-TW/queues)。
是不用 closure 就能把 job 送入 queue 的便利方法。

```php theme={null}
use App\Jobs\Heartbeat;
use Illuminate\Support\Facades\Schedule;

Schedule::job(new Heartbeat)->everyFiveMinutes();
```

也可指定 queue 名稱或連線目標。

```php theme={null}
// 分派到 "heartbeats" queue、"sqs" 連線
Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();
```

### Shell 指令的排程

用 `exec` 方法執行 OS 指令。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

Schedule::exec('node /home/forge/script.js')->daily();
```

### Closure 的排程

用 `call` 方法排程任意 PHP closure。

```php theme={null}
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;

Schedule::call(function () {
    DB::table('recent_users')->delete();
})->daily();
```

也能傳入有 `__invoke` 方法的 Invocable 物件。

```php theme={null}
Schedule::call(new DeleteRecentUsers)->daily();
```

## 設定排程頻率

### 主要頻率方法

以下為常用的頻率方法。

| 方法                         | 說明               |
| -------------------------- | ---------------- |
| `->everySecond()`          | 每秒執行             |
| `->everyMinute()`          | 每分執行             |
| `->everyFiveMinutes()`     | 每 5 分執行          |
| `->everyFifteenMinutes()`  | 每 15 分執行         |
| `->everyThirtyMinutes()`   | 每 30 分執行         |
| `->hourly()`               | 每小時執行            |
| `->hourlyAt(17)`           | 每小時 17 分執行       |
| `->daily()`                | 每日 0 時執行         |
| `->dailyAt('13:00')`       | 每日 13 時執行        |
| `->twiceDaily(1, 13)`      | 每日 1 時與 13 時執行   |
| `->weekly()`               | 每週日 0 時執行        |
| `->weeklyOn(1, '8:00')`    | 每週一 8 時執行        |
| `->monthly()`              | 每月 1 日 0 時執行     |
| `->monthlyOn(4, '15:00')`  | 每月 4 日 15 時執行    |
| `->quarterly()`            | 每季首日 0 時執行       |
| `->yearly()`               | 每年 1 月 1 日 0 時執行 |
| `->timezone('Asia/Tokyo')` | 指定時區             |

### 用 cron 式直接指定

也可用 `cron` 方法直接指定 cron 式。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')->cron('0 9 * * *'); // 每日 9 時執行
```

### 頻率與星期的組合

結合頻率方法與星期限制可製作更精細的排程。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

// 每週一 13 時執行
Schedule::call(function () {
    // ...
})->weekly()->mondays()->at('13:00');

// 週間每小時 8 時到 17 時執行
Schedule::command('foo')
    ->weekdays()
    ->hourly()
    ->timezone('Asia/Tokyo')
    ->between('8:00', '17:00');
```

### 設定時區

用 `timezone` 方法可為個別任務指定時區。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

Schedule::command('report:generate')
    ->timezone('Asia/Tokyo')
    ->at('9:00');
```

若要為所有任務設定共同時區，使用 `config/app.php` 的 `schedule_timezone`。

```php theme={null}
// config/app.php
'schedule_timezone' => 'Asia/Tokyo',
```

<Warning>
  若使用有夏令時間的時區，切換時刻可能導致任務執行兩次或完全不執行。
  建議盡量使用 UTC。
</Warning>

## 條件限制

### when / skip

`when` 在 closure 回傳 `true` 時執行任務。
`skip` 相反，回傳 `true` 時跳過。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

// 條件為 true 時才執行
Schedule::command('emails:send')->daily()->when(function () {
    return true;
});

// 條件為 true 時跳過
Schedule::command('emails:send')->daily()->skip(function () {
    return true;
});
```

### environments

用 `environments` 方法可限制只在特定環境執行。

```php theme={null}
Schedule::command('emails:send')
    ->daily()
    ->environments(['staging', 'production']);
```

### 時間的限制

用 `between` / `unlessBetween` 可限制執行時段。

```php theme={null}
// 只在 7 到 22 時之間執行
Schedule::command('emails:send')
    ->hourly()
    ->between('7:00', '22:00');

// 23 到 4 時之間不執行
Schedule::command('emails:send')
    ->hourly()
    ->unlessBetween('23:00', '4:00');
```

### 星期的限制

| 方法                            | 說明               |
| ----------------------------- | ---------------- |
| `->weekdays()`                | 僅平日              |
| `->weekends()`                | 僅週末              |
| `->mondays()` 〜 `->sundays()` | 僅特定星期            |
| `->days([0, 3])`              | 週日(0)與週三(3)等多個指定 |

```php theme={null}
use Illuminate\Support\Facades;
use Illuminate\Console\Scheduling\Schedule;

Facades\Schedule::command('emails:send')
    ->hourly()
    ->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);
```

## 防止重複

預設情況下，即使上次的任務仍在執行也會啟動下次執行。
用 `withoutOverlapping` 可讓下一次執行等待上次結束。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')->withoutOverlapping();
```

也能指定鎖定的有效期限（分鐘）。預設 24 小時。

```php theme={null}
// 10 分後鎖定失效
Schedule::command('emails:send')->withoutOverlapping(10);
```

<Info>
  `withoutOverlapping` 使用應用程式的快取管理鎖定。
  任務因非預期問題卡住時，可用 `schedule:clear-cache` 解除鎖定。

  ```shell theme={null}
  php artisan schedule:clear-cache
  ```
</Info>

## 多台伺服器的執行控制

若多台伺服器都執行排程器，可用 `onOneServer` 讓任務只在一台伺服器執行。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

Schedule::command('report:generate')
    ->fridays()
    ->at('17:00')
    ->onOneServer();
```

<Warning>
  使用此功能需將應用預設快取 driver 設為 `database`、`memcached`、`dynamodb` 或 `redis`，且所有伺服器需連到同一台快取伺服器。
</Warning>

### onOneServer() 的分散執行控制流程

```mermaid theme={null}
flowchart TD
    A["多台伺服器<br>同時執行 artisan schedule:run"] --> B["伺服器 1"]
    A --> C["伺服器 2"]
    A --> D["伺服器 3"]

    B --> E["在共享快取伺服器<br>嘗試取得 atomic lock"]
    C --> E
    D --> E

    E -->|"取得成功（第一台）"| F["執行任務"]
    E -->|"取得失敗（其他伺服器）"| G["略過任務"]

    F --> H["執行完成後釋放 lock"]
```

## 任務分組

當多個任務要套用相同設定時，可用 `group` 方法整合。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

Schedule::daily()
    ->onOneServer()
    ->timezone('Asia/Tokyo')
    ->group(function () {
        Schedule::command('emails:send --force');
        Schedule::command('emails:prune');
    });
```

## 背景執行

同時刻排程的任務預設會依定義順序依序執行。
若有長時間任務，會延遲後續任務的開始。
用 `runInBackground` 可讓任務在背景平行執行。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

Schedule::command('analytics:report')
    ->daily()
    ->runInBackground();
```

<Warning>
  `runInBackground` 只能用於 `command` 與 `exec` 方法。
</Warning>

## 維護模式

當應用程式處於維護模式時，排程任務不會執行。
若要在維護模式中仍強制執行，使用 `evenInMaintenanceMode`。

```php theme={null}
Schedule::command('emails:send')->evenInMaintenanceMode();
```

### 維護模式的處理流程

```mermaid theme={null}
flowchart TD
    A["artisan schedule:run"] --> B{"應用是否處於<br>維護模式？<br>(artisan down)"}
    B -->|"一般模式"| C["正常執行任務"]
    B -->|"維護模式"| D{"任務是否設定<br>evenInMaintenanceMode()？"}
    D -->|"是"| C
    D -->|"否"| E["略過任務"]
```

## 暫停排程器

可在不改動程式碼下暫停排程器。

```shell theme={null}
# 停止排程器
php artisan schedule:pause

# 恢復排程器
php artisan schedule:continue
```

若在停止期間仍想讓特定任務持續執行，使用 `evenWhenPaused`。
適用於健康檢查或系統監控等即使維護中也需持續執行的任務。

```php theme={null}
Schedule::command('emails:send')->evenWhenPaused();
```

## 輸出處理

### 輸出到檔案

用 `sendOutputTo` 可將任務輸出存入檔案。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')
    ->daily()
    ->sendOutputTo(storage_path('logs/emails-send.log'));
```

用 `appendOutputTo` 則會附加到既有檔案。

```php theme={null}
Schedule::command('emails:send')
    ->daily()
    ->appendOutputTo(storage_path('logs/emails-send.log'));
```

### 輸出到郵件

用 `emailOutputTo` 可將任務輸出以郵件寄送。
需先完成 Laravel 的[郵件設定](/zh-TW/mail)。

```php theme={null}
Schedule::command('report:generate')
    ->daily()
    ->sendOutputTo($filePath)
    ->emailOutputTo('admin@example.com');
```

僅在失敗時寄信可用 `emailOutputOnFailure`。

```php theme={null}
Schedule::command('report:generate')
    ->daily()
    ->emailOutputOnFailure('admin@example.com');
```

## 任務 Hook

以 `before` / `after` 方法可在任務執行前後插入處理。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')
    ->daily()
    ->before(function () {
        // 任務執行前的處理
    })
    ->after(function () {
        // 任務執行後的處理
    });
```

成功／失敗時的 hook 用 `onSuccess` / `onFailure` 定義。

```php theme={null}
use Illuminate\Support\Stringable;

Schedule::command('emails:send')
    ->daily()
    ->onSuccess(function (Stringable $output) {
        // 成功時的處理
    })
    ->onFailure(function (Stringable $output) {
        // 失敗時的處理
    });
```

## 部署到伺服器

<Steps>
  <Step title="加入 cron entry">
    在伺服器的 crontab 加入以下一行，Laravel 排程器就會每分鐘執行。

    ```shell theme={null}
    * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
    ```

    可用 `crontab -e` 指令編輯。
  </Step>

  <Step title="確認排程器運作">
    確認已定義任務清單與下次執行時間。

    ```shell theme={null}
    php artisan schedule:list
    ```
  </Step>
</Steps>

<Tip>
  使用 [Laravel Cloud](https://cloud.laravel.com) 可以不用設定 cron 也能管理排程任務。
</Tip>

### 本機開發時的執行

本機可不用 cron，而是用 `schedule:work` 指令常駐排程器。

```shell theme={null}
php artisan schedule:work
```

此指令會在前景執行，每分鐘呼叫排程器。按 `Ctrl+C` 停止前會持續運作。

### Sub-minute 排程（小於 1 分的頻率）

一般 cron 最小單位為 1 分鐘，但 Laravel 可設定以秒為單位的排程。

```php theme={null}
use Illuminate\Support\Facades\Schedule;

Schedule::call(function () {
    DB::table('recent_users')->delete();
})->everySecond();
```

當有 sub-minute 任務時，`schedule:run` 會持續運行到該分鐘結束以處理所有 sub-minute 任務。

<Tip>
  Sub-minute 任務建議委派給 queue job 或背景指令執行。
  因為任務本體若花費時間過長，會延遲後續 sub-minute 任務的執行。
</Tip>

若要在部署期間中斷執行中的 `schedule:run`，在部署腳本加入以下：

```shell theme={null}
php artisan schedule:interrupt
```

## 常用指令總覽

```shell theme={null}
# 顯示任務清單
php artisan schedule:list

# 手動執行排程器（伺服器 cron 呼叫的指令）
php artisan schedule:run

# 本機開發用 常駐執行
php artisan schedule:work

# 暫停排程器
php artisan schedule:pause

# 恢復排程器
php artisan schedule:continue

# 清除防重複的 lock
php artisan schedule:clear-cache

# 中斷 sub-minute 任務執行（部署時）
php artisan schedule:interrupt
```


## Related topics

- [Bot 教學 - Laravel Bluesky](/zh-TW/packages/laravel-bluesky/bot-tutorial.md)
- [教學 - Laravel Console Starter](/zh-TW/packages/laravel-console-starter/tutorial.md)
- [Laravel Nightwatch 入門](/zh-TW/blog/nightwatch-introduction.md)
- [Feed Generator](/zh-TW/packages/laravel-bluesky/feed-generator.md)
- [Labeler](/zh-TW/packages/laravel-bluesky/labeler.md)
