output:
Chunk ID: 26a633
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 438
Output:
 .../Api/Storefront/V1/SearchController.php         |  62 ++++
 .../Api/Storefront/V1/SearchProductsRequest.php    |  50 +++
 .../Api/Storefront/V1/SuggestProductsRequest.php   |  24 ++
 .../Storefront/V1/SearchProductResource.php        |  49 +++
 app/Livewire/Admin/Search/Settings.php             | 164 +++++++++
 app/Livewire/Storefront/Search/Index.php           | 103 +++++-
 app/Models/SearchQuery.php                         |  55 +++
 app/Models/SearchSettings.php                      |  57 +++
 app/Observers/ProductObserver.php                  |  29 ++
 app/Providers/AppServiceProvider.php               |  14 +
 app/Services/SearchService.php                     | 403 +++++++++++++++++++++
 database/factories/SearchQueryFactory.php          |  28 ++
 database/factories/SearchSettingsFactory.php       |  29 ++
 ...6_05_04_030847_create_search_settings_table.php |  29 ++
 ...2026_05_04_030848_create_products_fts_table.php |  41 +++
 ...26_05_04_030848_create_search_queries_table.php |  35 ++
 database/seeders/DatabaseSeeder.php                |   1 +
 database/seeders/SearchSettingsSeeder.php          |  31 ++
 resources/views/layouts/app/sidebar.blade.php      |   4 +
 .../views/livewire/admin/search/settings.blade.php |  74 ++++
 .../livewire/storefront/search/index.blade.php     |  80 +++-
 routes/api.php                                     |   6 +
 routes/web.php                                     |   2 +
 specs/progress.md                                  |  38 +-
 tests/Feature/Admin/SearchSettingsTest.php         |  80 ++++
 tests/Feature/Api/StorefrontSearchApiTest.php      | 131 +++++++
 tests/Feature/Search/SearchServiceTest.php         | 101 ++++++
 27 files changed, 1676 insertions(+), 44 deletions(-)


command:
/bin/zsh
-lc
git diff --cached --check

type: unknown

cmd: git diff --cached --check

status: completed


output:
Chunk ID: 16450a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


message: The staged diff is coherent and `git diff --cached --check` is clean. I’m committing the search implementation now.

phase: commentary


type: message

role: assistant

text: The staged diff is coherent and `git diff --cached --check` is clean. I’m committing the search implementation now.


cmd: git commit -m "Add storefront search foundation"


command: git commit -m "Add storefront search foundation"

aggregated_output:
[2026-05-04-codex-5-5-goal a05467c4] Add storefront search foundation
 27 files changed, 1676 insertions(+), 44 deletions(-)
 create mode 100644 app/Http/Controllers/Api/Storefront/V1/SearchController.php
 create mode 100644 app/Http/Requests/Api/Storefront/V1/SearchProductsRequest.php
 create mode 100644 app/Http/Requests/Api/Storefront/V1/SuggestProductsRequest.php
 create mode 100644 app/Http/Resources/Storefront/V1/SearchProductResource.php
 create mode 100644 app/Livewire/Admin/Search/Settings.php
 create mode 100644 app/Models/SearchQuery.php
 create mode 100644 app/Models/SearchSettings.php
 create mode 100644 app/Observers/ProductObserver.php
 create mode 100644 app/Services/SearchService.php
 create mode 100644 database/factories/SearchQueryFactory.php
 create mode 100644 database/factories/SearchSettingsFactory.php
 create mode 100644 database/migrations/2026_05_04_030847_create_search_settings_table.php
 create mode 100644 database/migrations/2026_05_04_030848_create_products_fts_table.php
 create mode 100644 database/migrations/2026_05_04_030848_create_search_queries_table.php
 create mode 100644 database/seeders/SearchSettingsSeeder.php
 create mode 100644 resources/views/livewire/admin/search/settings.blade.php
 create mode 100644 tests/Feature/Admin/SearchSettingsTest.php
 create mode 100644 tests/Feature/Api/StorefrontSearchApiTest.php
 create mode 100644 tests/Feature/Search/SearchServiceTest.php


output:
Chunk ID: a13b2d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 360
Output:
[2026-05-04-codex-5-5-goal a05467c4] Add storefront search foundation
 27 files changed, 1676 insertions(+), 44 deletions(-)
 create mode 100644 app/Http/Controllers/Api/Storefront/V1/SearchController.php
 create mode 100644 app/Http/Requests/Api/Storefront/V1/SearchProductsRequest.php
 create mode 100644 app/Http/Requests/Api/Storefront/V1/SuggestProductsRequest.php
 create mode 100644 app/Http/Resources/Storefront/V1/SearchProductResource.php
 create mode 100644 app/Livewire/Admin/Search/Settings.php
 create mode 100644 app/Models/SearchQuery.php
 create mode 100644 app/Models/SearchSettings.php
 create mode 100644 app/Observers/ProductObserver.php
 create mode 100644 app/Services/SearchService.php
 create mode 100644 database/factories/SearchQueryFactory.php
 create mode 100644 database/factories/SearchSettingsFactory.php
 create mode 100644 database/migrations/2026_05_04_030847_create_search_settings_table.php
 create mode 100644 database/migrations/2026_05_04_030848_create_products_fts_table.php
 create mode 100644 database/migrations/2026_05_04_030848_create_search_queries_table.php
 create mode 100644 database/seeders/SearchSettingsSeeder.php
 create mode 100644 resources/views/livewire/admin/search/settings.blade.php
 create mode 100644 tests/Feature/Admin/SearchSettingsTest.php
 create mode 100644 tests/Feature/Api/StorefrontSearchApiTest.php
 create mode 100644 tests/Feature/Search/SearchServiceTest.php


message: Search is committed as `a05467c4 Add storefront search foundation`. I’m moving to the remaining Phase 7 work, starting by reading the analytics/app/webhook specs and current event/job structure.


text: Search is committed as `a05467c4 Add storefront search foundation`. I’m moving to the remaining Phase 7 work, starting by reading the analytics/app/webhook specs and current event/job structure.


step: Implement search schema/service/API/admin settings with tests

step: Inspect analytics/apps/webhooks specs and current implementation gaps

step: Implement the next focused Phase 7 slice with tests

status: pending

step: Run formatter, tests, build/browser checks as needed

step: Update progress and commit the slice

explanation: Search is committed. Continuing with the remaining Phase 7 areas.


output: Plan updated


cmd: sed -n '896,924p' specs/09-IMPLEMENTATION-ROADMAP.md && sed -n '1430,1485p' specs/05-BUSINESS-LOGIC.md && sed -n '914,965p' specs/01-DATABASE-SCHEMA.md


cmd: sed -n '1140,1210p' specs/02-API-ROUTES.md && sed -n '2120,2208p' specs/03-ADMIN-UI.md


cmd: sed -n '1,220p' routes/console.php && rg -n "Analytics|analytics|AggregateAnalytics|OrderCreated|checkout_completed|add_to_cart|product_view" app routes database tests specs/07-SEEDERS-AND-TEST-DATA.md


command: sed -n '896,924p' specs/09-IMPLEMENTATION-ROADMAP.md && sed -n '1430,1485p' specs/05-BUSINESS-LOGIC.md && sed -n '914,965p' specs/01-DATABASE-SCHEMA.md

type: read

aggregated_output:
1. `create_analytics_events_table` - FK: `store_id`, `customer_id`
2. `create_analytics_daily_table` - composite PK: `store_id` + `date`

#### Step 9.2: Analytics Service

**File to create:** `App\Services\AnalyticsService`

| Method | Description |
|--------|-------------|
| `track(Store $store, string $type, array $properties, ?string $sessionId, ?int $customerId): void` | Inserts raw event |
| `getDailyMetrics(Store $store, string $startDate, string $endDate): Collection` | Reads pre-aggregated daily data |

Event types: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`.

**File to create:** `App\Jobs\AggregateAnalytics`

Requirements:
- Runs daily via `routes/console.php`
- Aggregates raw events into `analytics_daily` rows
- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count

---

### Phase 10: Apps and Webhooks

**Priority:** LOW - extensibility.

#### Step 10.1: Migrations

## 14. Analytics

### 14.1 Event Ingestion

**Endpoint:** `POST /api/storefront/v1/analytics/events`

**Request structure:**

| Field | Type | Required | Description |
|---|---|---|---|
Events are submitted in batches:

```json
{
  "events": [
    {
      "type": "page_view",
      "session_id": "...",
      "client_event_id": "...",
      "occurred_at": "2025-01-15T10:30:00Z",
      "properties": { ... }
    }
  ]
}
```

**Event fields:**

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | string | Yes | One of: `page_view`, `product_view`, `add_to_cart`, `checkout_started`, `checkout_completed` |
| `session_id` | string | Yes | Client session identifier |
| `client_event_id` | string | Yes | Client-generated unique ID for deduplication |
| `occurred_at` | string (ISO-8601) | Yes | When the event occurred on the client |
| `properties` | object | No | Event-specific data (product_id, variant_id, etc.) |

**Deduplication:** Events are deduplicated by `client_event_id` within the store scope. If a `client_event_id` has already been recorded for the current store (enforced by the unique index on `store_id, client_event_id`), the duplicate event is silently dropped.

**Rate limiting:** 60 requests per minute per IP address, using Laravel's throttle middleware.

### 14.2 Aggregation

**Job:** `App\Jobs\AggregateAnalytics`

- Schedule: daily at 01:00 UTC
- Aggregates the previous day's raw `analytics_events` into `analytics_daily`.

**Aggregated metrics:**

| Metric | Calculation |
|---|---|
| `orders_count` | Count of `checkout_completed` events for the day |
| `revenue_amount` | Sum of order totals from `checkout_completed` event properties |
| `aov_amount` | `revenue_amount / orders_count` (integer division, 0 if no orders) |
| `visits_count` | Count of distinct `session_id` values with `page_view` events |
| `add_to_cart_count` | Count of `add_to_cart` events |
### analytics_events

Raw event stream for storefront analytics.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| type | TEXT | No | - | - | Event type (see notes) |
| session_id | TEXT | Yes | NULL | - | Browser session identifier |
| customer_id | INTEGER | Yes | NULL | FK -> customers(id) ON DELETE SET NULL | Nullable for anonymous visitors |
| properties_json | TEXT | No | '{}' | - | Event-specific data as JSON |
| client_event_id | TEXT | Yes | NULL | UNIQUE per (store_id, client_event_id) | Client-provided idempotency key for deduplication |
| occurred_at | TEXT | Yes | NULL | - | ISO-8601 timestamp of when the event actually occurred on the client |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_analytics_events_store_id` on (`store_id`) - FK lookup
- `idx_analytics_events_store_type` on (`store_id`, `type`) - Filtering events by type per store
- `idx_analytics_events_store_created` on (`store_id`, `created_at`) - Time-range event queries
- `idx_analytics_events_session` on (`session_id`) - Session-based event grouping
- `idx_analytics_events_customer` on (`customer_id`) - Customer activity history
- `idx_analytics_events_client_event` UNIQUE on (`store_id`, `client_event_id`) - Client-side event deduplication

**Notes:** Event types include: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. This table will grow fast - consider periodic archiving or partitioning by date for production workloads beyond SQLite.

---

### analytics_daily

Pre-aggregated daily metrics per store. Composite primary key.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| store_id | INTEGER | No | - | PK (composite), FK -> stores(id) ON DELETE CASCADE | |
| date | TEXT | No | - | PK (composite) | ISO-8601 date (YYYY-MM-DD) |
| orders_count | INTEGER | No | 0 | - | Number of orders placed |
| revenue_amount | INTEGER | No | 0 | - | Total revenue in minor units |
| aov_amount | INTEGER | No | 0 | - | Average order value in minor units |
| visits_count | INTEGER | No | 0 | - | Unique sessions |
| add_to_cart_count | INTEGER | No | 0 | - | Add-to-cart events |
| checkout_started_count | INTEGER | No | 0 | - | Checkout-started events |
| checkout_completed_count | INTEGER | No | 0 | - | Checkout-completed events |

**Primary Key:** (`store_id`, `date`)

**Indexes:**

- `idx_analytics_daily_store_date` on (`store_id`, `date`) - Time-range metric queries (covered by PK but explicit for clarity)

**Notes:** `date` is an ISO-8601 date string (YYYY-MM-DD). `revenue_amount` and `aov_amount` (average order value) are in minor units (cents). This table is populated by a scheduled aggregation job.


command:
/bin/zsh
-lc
git status --short

cmd: git status --short


output:
Chunk ID: 664f14
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


output:
Chunk ID: 6c0d89
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1466
Output:
1. `create_analytics_events_table` - FK: `store_id`, `customer_id`
2. `create_analytics_daily_table` - composite PK: `store_id` + `date`

#### Step 9.2: Analytics Service

**File to create:** `App\Services\AnalyticsService`

| Method | Description |
|--------|-------------|
| `track(Store $store, string $type, array $properties, ?string $sessionId, ?int $customerId): void` | Inserts raw event |
| `getDailyMetrics(Store $store, string $startDate, string $endDate): Collection` | Reads pre-aggregated daily data |

Event types: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`.

**File to create:** `App\Jobs\AggregateAnalytics`

Requirements:
- Runs daily via `routes/console.php`
- Aggregates raw events into `analytics_daily` rows
- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count

---

### Phase 10: Apps and Webhooks

**Priority:** LOW - extensibility.

#### Step 10.1: Migrations

## 14. Analytics

### 14.1 Event Ingestion

**Endpoint:** `POST /api/storefront/v1/analytics/events`

**Request structure:**

| Field | Type | Required | Description |
|---|---|---|---|
Events are submitted in batches:

```json
{
  "events": [
    {
      "type": "page_view",
      "session_id": "...",
      "client_event_id": "...",
      "occurred_at": "2025-01-15T10:30:00Z",
      "properties": { ... }
    }
  ]
}
```

**Event fields:**

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | string | Yes | One of: `page_view`, `product_view`, `add_to_cart`, `checkout_started`, `checkout_completed` |
| `session_id` | string | Yes | Client session identifier |
| `client_event_id` | string | Yes | Client-generated unique ID for deduplication |
| `occurred_at` | string (ISO-8601) | Yes | When the event occurred on the client |
| `properties` | object | No | Event-specific data (product_id, variant_id, etc.) |

**Deduplication:** Events are deduplicated by `client_event_id` within the store scope. If a `client_event_id` has already been recorded for the current store (enforced by the unique index on `store_id, client_event_id`), the duplicate event is silently dropped.

**Rate limiting:** 60 requests per minute per IP address, using Laravel's throttle middleware.

### 14.2 Aggregation

**Job:** `App\Jobs\AggregateAnalytics`

- Schedule: daily at 01:00 UTC
- Aggregates the previous day's raw `analytics_events` into `analytics_daily`.

**Aggregated metrics:**

| Metric | Calculation |
|---|---|
| `orders_count` | Count of `checkout_completed` events for the day |
| `revenue_amount` | Sum of order totals from `checkout_completed` event properties |
| `aov_amount` | `revenue_amount / orders_count` (integer division, 0 if no orders) |
| `visits_count` | Count of distinct `session_id` values with `page_view` events |
| `add_to_cart_count` | Count of `add_to_cart` events |
### analytics_events

Raw event stream for storefront analytics.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| type | TEXT | No | - | - | Event type (see notes) |
| session_id | TEXT | Yes | NULL | - | Browser session identifier |
| customer_id | INTEGER | Yes | NULL | FK -> customers(id) ON DELETE SET NULL | Nullable for anonymous visitors |
| properties_json | TEXT | No | '{}' | - | Event-specific data as JSON |
| client_event_id | TEXT | Yes | NULL | UNIQUE per (store_id, client_event_id) | Client-provided idempotency key for deduplication |
| occurred_at | TEXT | Yes | NULL | - | ISO-8601 timestamp of when the event actually occurred on the client |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_analytics_events_store_id` on (`store_id`) - FK lookup
- `idx_analytics_events_store_type` on (`store_id`, `type`) - Filtering events by type per store
- `idx_analytics_events_store_created` on (`store_id`, `created_at`) - Time-range event queries
- `idx_analytics_events_session` on (`session_id`) - Session-based event grouping
- `idx_analytics_events_customer` on (`customer_id`) - Customer activity history
- `idx_analytics_events_client_event` UNIQUE on (`store_id`, `client_event_id`) - Client-side event deduplication

**Notes:** Event types include: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. This table will grow fast - consider periodic archiving or partitioning by date for production workloads beyond SQLite.

---

### analytics_daily

Pre-aggregated daily metrics per store. Composite primary key.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| store_id | INTEGER | No | - | PK (composite), FK -> stores(id) ON DELETE CASCADE | |
| date | TEXT | No | - | PK (composite) | ISO-8601 date (YYYY-MM-DD) |
| orders_count | INTEGER | No | 0 | - | Number of orders placed |
| revenue_amount | INTEGER | No | 0 | - | Total revenue in minor units |
| aov_amount | INTEGER | No | 0 | - | Average order value in minor units |
| visits_count | INTEGER | No | 0 | - | Unique sessions |
| add_to_cart_count | INTEGER | No | 0 | - | Add-to-cart events |
| checkout_started_count | INTEGER | No | 0 | - | Checkout-started events |
| checkout_completed_count | INTEGER | No | 0 | - | Checkout-completed events |

**Primary Key:** (`store_id`, `date`)

**Indexes:**

- `idx_analytics_daily_store_date` on (`store_id`, `date`) - Time-range metric queries (covered by PK but explicit for clarity)

**Notes:** `date` is an ISO-8601 date string (YYYY-MM-DD). `revenue_amount` and `aov_amount` (average order value) are in minor units (cents). This table is populated by a scheduled aggregation job.


command: sed -n '1140,1210p' specs/02-API-ROUTES.md && sed -n '2120,2208p' specs/03-ADMIN-UI.md

aggregated_output:
### 2.6 Analytics Endpoints

#### POST /api/storefront/v1/analytics/events

Batch submit storefront analytics events.

- **Authentication:** None
- **Rate limit:** `analytics` (60/min per IP)

**Request body:**

```json
{
  "events": [
    {
      "type": "page_view",
      "session_id": "sess_abc123",
      "client_event_id": "evt_001",
      "properties": {
        "url": "/products/classic-t-shirt",
        "referrer": "https://google.com",
        "user_agent": "Mozilla/5.0..."
      },
      "occurred_at": "2026-02-11T10:00:00Z"
    },
    {
      "type": "add_to_cart",
      "session_id": "sess_abc123",
      "client_event_id": "evt_002",
      "properties": {
        "product_id": 10,
        "variant_id": 101,
        "quantity": 1,
        "price_amount": 2500,
        "currency": "EUR"
      },
      "occurred_at": "2026-02-11T10:01:00Z"
    }
  ]
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `events` | array | Yes | Min: 1, max: 50 events per batch. |
| `events[].type` | string | Yes | One of: `page_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. |
| `events[].session_id` | string | Yes | Max: 100 characters. |
| `events[].client_event_id` | string | Yes | Max: 100 characters. If a `client_event_id` has already been recorded for the current store, the duplicate event is silently dropped (unique on `(store_id, client_event_id)`). |
| `events[].properties` | object | No | Freeform JSON. Max depth: 3 levels. |
| `events[].occurred_at` | string (ISO 8601) | Yes | Must not be more than 1 hour in the past or in the future. |

**Success response:** `202 Accepted`

```json
{
  "accepted": 2,
  "rejected": 0
}
```

**Error responses:**
- `422` - Validation errors (invalid event types, too many events, etc.)

---

## 3. Admin REST API

**Prefix:** `/api/admin/v1`
**Middleware:** `auth:sanctum`, `store.resolve`, `throttle:api.admin`
**Content-Type:** `application/json`
---

## 17. Analytics Page

**Route:** `GET /admin/analytics`

**Livewire component:** `App\Livewire\Admin\Analytics\Index`

**Blade view:** `resources/views/livewire/admin/analytics/index.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `dateRange` | `string` | Date range preset |
| `customStartDate` | `?string` | Custom start date |
| `customEndDate` | `?string` | Custom end date |
| `channelFilter` | `string` | Sales channel filter (`all`, `storefront`, `api`) |
| `deviceFilter` | `string` | Device filter (`all`, `desktop`, `mobile`, `tablet`) |
| `totalSales` | `int` | Total sales for the period |
| `ordersCount` | `int` | Orders count |
| `averageOrderValue` | `int` | AOV |
| `conversionRate` | `float` | Conversion rate percentage |
| `salesChartData` | `array` | Daily sales chart data |
| `topProducts` | `Collection` | Top products by revenue |
| `topReferrers` | `array` | Top traffic sources |
| `isExporting` | `bool` | Whether an export is in progress |
| `exportUrl` | `?string` | Download URL when export is ready |

| Action | Behavior |
|--------|----------|
| `updatedDateRange()` | Reloads analytics data |
| `updatedChannelFilter()` | Reloads with channel filter |
| `updatedDeviceFilter()` | Reloads with device filter |
| `loadAnalytics()` | Master method to reload all analytics data |
| `exportCsv()` | Dispatches an async job to generate a CSV export |

### Layout Wireframe

```
+==================================================================+
| Analytics                                                         |
+==================================================================+
| [Today v]  [Channel: All v]  [Device: All v]  [Export CSV]       |
+==================================================================+
|                                                                   |
| KPI TILES (same as Dashboard + Conversion Rate)                  |
| +------------+ +------------+ +------------+ +-----------+       |
| | Total Sales| | Orders     | | AOV        | | Conv Rate |       |
| | $45,678    | | 523        | | $87.34     | | 3.2%      |       |
| +------------+ +------------+ +------------+ +-----------+       |
|                                                                   |
| SALES CHART (larger than dashboard version)                      |
| [line/bar chart - daily revenue/orders]                          |
|                                                                   |
| TOP PRODUCTS (10-20 rows)                                         |
| Rank | Product     | Units Sold | Revenue  | % of Total          |
| 1    | Blue Shirt  | 145        | $14,500  | 31.7%               |
| 2    | Red Hat     | 98         | $9,800   | 21.4%               |
| ...                                                               |
|                                                                   |
| TOP REFERRERS                                                     |
| Source          | Sessions | Orders | Conversion Rate             |
| Google          | 1,200    | 45     | 3.75%                      |
| Direct          | 800      | 32     | 4.00%                      |
| Instagram       | 450      | 12     | 2.67%                      |
+==================================================================+
```

Similar to Dashboard but with additional controls and detail.

**Filter bar:**
- Date range dropdown (same as Dashboard: Today, Last 7 days, Last 30 days, Custom)
- Channel filter: Flux select with All, Storefront, API
- Device filter: Flux select with All, Desktop, Mobile, Tablet
- Export button: ghost variant with download icon - calls `exportCsv()`

When `isExporting` is true, show a loading indicator next to the export button. When `exportUrl` is set, show a download link.

**KPI tiles:** Same as Dashboard (Total Sales, Orders, AOV, Conversion Rate) but with the additional filters applied.

**Sales chart:** Larger chart than Dashboard, line or bar chart showing daily revenue/orders.

**Top Products table:** Extended version with 10-20 rows. Columns: Rank, Product, Units Sold, Revenue, Percentage of Total.

**Top Referrers table:** Columns: Traffic source, Sessions, Orders, Conversion Rate.

---


output:
Chunk ID: 27eebe
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1563
Output:

### 2.6 Analytics Endpoints

#### POST /api/storefront/v1/analytics/events

Batch submit storefront analytics events.

- **Authentication:** None
- **Rate limit:** `analytics` (60/min per IP)

**Request body:**

```json
{
  "events": [
    {
      "type": "page_view",
      "session_id": "sess_abc123",
      "client_event_id": "evt_001",
      "properties": {
        "url": "/products/classic-t-shirt",
        "referrer": "https://google.com",
        "user_agent": "Mozilla/5.0..."
      },
      "occurred_at": "2026-02-11T10:00:00Z"
    },
    {
      "type": "add_to_cart",
      "session_id": "sess_abc123",
      "client_event_id": "evt_002",
      "properties": {
        "product_id": 10,
        "variant_id": 101,
        "quantity": 1,
        "price_amount": 2500,
        "currency": "EUR"
      },
      "occurred_at": "2026-02-11T10:01:00Z"
    }
  ]
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `events` | array | Yes | Min: 1, max: 50 events per batch. |
| `events[].type` | string | Yes | One of: `page_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. |
| `events[].session_id` | string | Yes | Max: 100 characters. |
| `events[].client_event_id` | string | Yes | Max: 100 characters. If a `client_event_id` has already been recorded for the current store, the duplicate event is silently dropped (unique on `(store_id, client_event_id)`). |
| `events[].properties` | object | No | Freeform JSON. Max depth: 3 levels. |
| `events[].occurred_at` | string (ISO 8601) | Yes | Must not be more than 1 hour in the past or in the future. |

**Success response:** `202 Accepted`

```json
{
  "accepted": 2,
  "rejected": 0
}
```

**Error responses:**
- `422` - Validation errors (invalid event types, too many events, etc.)

---

## 3. Admin REST API

**Prefix:** `/api/admin/v1`
**Middleware:** `auth:sanctum`, `store.resolve`, `throttle:api.admin`
**Content-Type:** `application/json`
---

## 17. Analytics Page

**Route:** `GET /admin/analytics`

**Livewire component:** `App\Livewire\Admin\Analytics\Index`

**Blade view:** `resources/views/livewire/admin/analytics/index.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `dateRange` | `string` | Date range preset |
| `customStartDate` | `?string` | Custom start date |
| `customEndDate` | `?string` | Custom end date |
| `channelFilter` | `string` | Sales channel filter (`all`, `storefront`, `api`) |
| `deviceFilter` | `string` | Device filter (`all`, `desktop`, `mobile`, `tablet`) |
| `totalSales` | `int` | Total sales for the period |
| `ordersCount` | `int` | Orders count |
| `averageOrderValue` | `int` | AOV |
| `conversionRate` | `float` | Conversion rate percentage |
| `salesChartData` | `array` | Daily sales chart data |
| `topProducts` | `Collection` | Top products by revenue |
| `topReferrers` | `array` | Top traffic sources |
| `isExporting` | `bool` | Whether an export is in progress |
| `exportUrl` | `?string` | Download URL when export is ready |

| Action | Behavior |
|--------|----------|
| `updatedDateRange()` | Reloads analytics data |
| `updatedChannelFilter()` | Reloads with channel filter |
| `updatedDeviceFilter()` | Reloads with device filter |
| `loadAnalytics()` | Master method to reload all analytics data |
| `exportCsv()` | Dispatches an async job to generate a CSV export |

### Layout Wireframe

```
+==================================================================+
| Analytics                                                         |
+==================================================================+
| [Today v]  [Channel: All v]  [Device: All v]  [Export CSV]       |
+==================================================================+
|                                                                   |
| KPI TILES (same as Dashboard + Conversion Rate)                  |
| +------------+ +------------+ +------------+ +-----------+       |
| | Total Sales| | Orders     | | AOV        | | Conv Rate |       |
| | $45,678    | | 523        | | $87.34     | | 3.2%      |       |
| +------------+ +------------+ +------------+ +-----------+       |
|                                                                   |
| SALES CHART (larger than dashboard version)                      |
| [line/bar chart - daily revenue/orders]                          |
|                                                                   |
| TOP PRODUCTS (10-20 rows)                                         |
| Rank | Product     | Units Sold | Revenue  | % of Total          |
| 1    | Blue Shirt  | 145        | $14,500  | 31.7%               |
| 2    | Red Hat     | 98         | $9,800   | 21.4%               |
| ...                                                               |
|                                                                   |
| TOP REFERRERS                                                     |
| Source          | Sessions | Orders | Conversion Rate             |
| Google          | 1,200    | 45     | 3.75%                      |
| Direct          | 800      | 32     | 4.00%                      |
| Instagram       | 450      | 12     | 2.67%                      |
+==================================================================+
```

Similar to Dashboard but with additional controls and detail.

**Filter bar:**
- Date range dropdown (same as Dashboard: Today, Last 7 days, Last 30 days, Custom)
- Channel filter: Flux select with All, Storefront, API
- Device filter: Flux select with All, Desktop, Mobile, Tablet
- Export button: ghost variant with download icon - calls `exportCsv()`

When `isExporting` is true, show a loading indicator next to the export button. When `exportUrl` is set, show a download link.

**KPI tiles:** Same as Dashboard (Total Sales, Orders, AOV, Conversion Rate) but with the additional filters applied.

**Sales chart:** Larger chart than Dashboard, line or bar chart showing daily revenue/orders.

**Top Products table:** Extended version with 10-20 rows. Columns: Rank, Product, Units Sold, Revenue, Percentage of Total.

**Top Referrers table:** Columns: Traffic source, Sessions, Orders, Conversion Rate.

---


command: sed -n '1,220p' routes/console.php && rg -n "Analytics|analytics|AggregateAnalytics|OrderCreated|checkout_completed|add_to_cart|product_view" app routes database tests specs/07-SEEDERS-AND-TEST-DATA.md

console.php

cmd: sed -n '1,220p' routes/console.php

type: search

cmd: rg -n 'Analytics|analytics|AggregateAnalytics|OrderCreated|checkout_completed|add_to_cart|product_view' app routes database tests specs/07-SEEDERS-AND-TEST-DATA.md

query: Analytics|analytics|AggregateAnalytics|OrderCreated|checkout_completed|add_to_cart|product_view

path: app

aggregated_output:
<?php

use App\Jobs\CancelUnpaidBankTransferOrders;
use App\Jobs\CleanupAbandonedCarts;
use App\Jobs\ExpireAbandonedCheckouts;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;

Artisan::command('inspire', function () {
    $this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');

Schedule::job(new ExpireAbandonedCheckouts)->everyFifteenMinutes();
Schedule::job(new CleanupAbandonedCarts)->daily();
Schedule::job(new CancelUnpaidBankTransferOrders)->daily();
specs/07-SEEDERS-AND-TEST-DATA.md:31:| 17 | AnalyticsSeeder | Stores, Customers | Creates daily aggregates and event stream |
specs/07-SEEDERS-AND-TEST-DATA.md:57:    B --> Q[AnalyticsSeeder]
specs/07-SEEDERS-AND-TEST-DATA.md:718:### 2.26 AnalyticsEventFactory
specs/07-SEEDERS-AND-TEST-DATA.md:720:**Model:** AnalyticsEvent
specs/07-SEEDERS-AND-TEST-DATA.md:727:| type | Random from: page_view, product_view, add_to_cart, remove_from_cart, checkout_started, checkout_completed, search |
specs/07-SEEDERS-AND-TEST-DATA.md:738:| `productView` | type = "product_view", properties_json includes product_id and product_title |
specs/07-SEEDERS-AND-TEST-DATA.md:739:| `addToCart` | type = "add_to_cart", properties_json includes variant_id and quantity |
specs/07-SEEDERS-AND-TEST-DATA.md:2003:### 3.17 AnalyticsSeeder
specs/07-SEEDERS-AND-TEST-DATA.md:2005:**Acme Fashion - analytics_daily (30 days of daily data):**
specs/07-SEEDERS-AND-TEST-DATA.md:2021:    INSERT analytics_daily:
specs/07-SEEDERS-AND-TEST-DATA.md:2025:        add_to_cart_count = addToCart
specs/07-SEEDERS-AND-TEST-DATA.md:2043:**Acme Fashion - analytics_events (200+ events, last 7 days):**
specs/07-SEEDERS-AND-TEST-DATA.md:2050:| product_view | 25% | ~55 |
specs/07-SEEDERS-AND-TEST-DATA.md:2051:| add_to_cart | 15% | ~33 |
specs/07-SEEDERS-AND-TEST-DATA.md:2053:| checkout_completed | 5% | ~11 |
specs/07-SEEDERS-AND-TEST-DATA.md:2065:| product_view | product_id, product_title, url |
specs/07-SEEDERS-AND-TEST-DATA.md:2066:| add_to_cart | product_id, variant_id, quantity, price_amount |
specs/07-SEEDERS-AND-TEST-DATA.md:2068:| checkout_completed | order_id, order_number, total_amount |
tests/Feature/Orders/OrderServiceTest.php:8:use App\Events\OrderCreated;
tests/Feature/Orders/OrderServiceTest.php:178:    Event::assertDispatched(OrderCreated::class, fn (OrderCreated $event): bool => $event->order->is($order));
tests/Feature/Orders/OrderServiceTest.php:218:    Event::assertDispatched(OrderCreated::class);
app/Services/OrderService.php:13:use App\Events\OrderCreated;
app/Services/OrderService.php:132:                event(new OrderCreated($order));
app/Livewire/Admin/Dashboard.php:63:     * @var array{visits: int, add_to_cart: int, checkout_started: int, checkout_completed: int}
app/Livewire/Admin/Dashboard.php:67:        'add_to_cart' => 0,
app/Livewire/Admin/Dashboard.php:69:        'checkout_completed' => 0,
app/Livewire/Admin/Dashboard.php:180:            'add_to_cart' => Cart::withoutGlobalScopes()
app/Livewire/Admin/Dashboard.php:188:            'checkout_completed' => $this->ordersCount,
app/Events/OrderCreated.php:9:class OrderCreated
app/Providers/AppServiceProvider.php:94:        RateLimiter::for('analytics', function (Request $request): Limit {


output:
Chunk ID: 931a04
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 826
Output:
<?php

use App\Jobs\CancelUnpaidBankTransferOrders;
use App\Jobs\CleanupAbandonedCarts;
use App\Jobs\ExpireAbandonedCheckouts;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;

Artisan::command('inspire', function () {
    $this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');

Schedule::job(new ExpireAbandonedCheckouts)->everyFifteenMinutes();
Schedule::job(new CleanupAbandonedCarts)->daily();
Schedule::job(new CancelUnpaidBankTransferOrders)->daily();
specs/07-SEEDERS-AND-TEST-DATA.md:31:| 17 | AnalyticsSeeder | Stores, Customers | Creates daily aggregates and event stream |
specs/07-SEEDERS-AND-TEST-DATA.md:57:    B --> Q[AnalyticsSeeder]
specs/07-SEEDERS-AND-TEST-DATA.md:718:### 2.26 AnalyticsEventFactory
specs/07-SEEDERS-AND-TEST-DATA.md:720:**Model:** AnalyticsEvent
specs/07-SEEDERS-AND-TEST-DATA.md:727:| type | Random from: page_view, product_view, add_to_cart, remove_from_cart, checkout_started, checkout_completed, search |
specs/07-SEEDERS-AND-TEST-DATA.md:738:| `productView` | type = "product_view", properties_json includes product_id and product_title |
specs/07-SEEDERS-AND-TEST-DATA.md:739:| `addToCart` | type = "add_to_cart", properties_json includes variant_id and quantity |
specs/07-SEEDERS-AND-TEST-DATA.md:2003:### 3.17 AnalyticsSeeder
specs/07-SEEDERS-AND-TEST-DATA.md:2005:**Acme Fashion - analytics_daily (30 days of daily data):**
specs/07-SEEDERS-AND-TEST-DATA.md:2021:    INSERT analytics_daily:
specs/07-SEEDERS-AND-TEST-DATA.md:2025:        add_to_cart_count = addToCart
specs/07-SEEDERS-AND-TEST-DATA.md:2043:**Acme Fashion - analytics_events (200+ events, last 7 days):**
specs/07-SEEDERS-AND-TEST-DATA.md:2050:| product_view | 25% | ~55 |
specs/07-SEEDERS-AND-TEST-DATA.md:2051:| add_to_cart | 15% | ~33 |
specs/07-SEEDERS-AND-TEST-DATA.md:2053:| checkout_completed | 5% | ~11 |
specs/07-SEEDERS-AND-TEST-DATA.md:2065:| product_view | product_id, product_title, url |
specs/07-SEEDERS-AND-TEST-DATA.md:2066:| add_to_cart | product_id, variant_id, quantity, price_amount |
specs/07-SEEDERS-AND-TEST-DATA.md:2068:| checkout_completed | order_id, order_number, total_amount |
tests/Feature/Orders/OrderServiceTest.php:8:use App\Events\OrderCreated;
tests/Feature/Orders/OrderServiceTest.php:178:    Event::assertDispatched(OrderCreated::class, fn (OrderCreated $event): bool => $event->order->is($order));
tests/Feature/Orders/OrderServiceTest.php:218:    Event::assertDispatched(OrderCreated::class);
app/Services/OrderService.php:13:use App\Events\OrderCreated;
app/Services/OrderService.php:132:                event(new OrderCreated($order));
app/Livewire/Admin/Dashboard.php:63:     * @var array{visits: int, add_to_cart: int, checkout_started: int, checkout_completed: int}
app/Livewire/Admin/Dashboard.php:67:        'add_to_cart' => 0,
app/Livewire/Admin/Dashboard.php:69:        'checkout_completed' => 0,
app/Livewire/Admin/Dashboard.php:180:            'add_to_cart' => Cart::withoutGlobalScopes()
app/Livewire/Admin/Dashboard.php:188:            'checkout_completed' => $this->ordersCount,
app/Events/OrderCreated.php:9:class OrderCreated
app/Providers/AppServiceProvider.php:94:        RateLimiter::for('analytics', function (Request $request): Limit {


packages: laravel/framework livewire/livewire livewire/flux pestphp/pest

queries: migration composite primary key unique index foreign key json column

queries: form request validate nested array date after before enum

queries: rate limiting routes throttle middleware

queries: database aggregation group by date upsert

queries: Livewire full page component computed pagination testing


server: laravel-boost

tool: search-docs

type: text

text:
# Search Results

## Routing Rate Limiting Attaching Rate Limiters to Routes
Routing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md

### Attaching Rate Limiters to Routes

Rate limiters may be attached to routes or route groups using the `throttle` [middleware](/docs/{{version}}/middleware). The throttle middleware accepts the name of the rate limiter you wish to assign to the route:

```php
Route::middleware(['throttle:uploads'])->group(function () {
    Route::post('/audio', function () {
        // ...
    });

    Route::post('/video', function () {
        // ...
    });
});
```

---

## Components Pagination > Large result set
Components Pagination | livewire/flux@2.x | Source: https://fluxui.dev/components/pagination#livewire/flux

Large result set

When working with large result sets, the pagination component automatically adapts to show a reasonable number of page links. It shows the first and last pages, along with a window of pages around the current page, and adds ellipses for any gaps to ensure efficient navigation through numerous pages.

<flux:pagination :paginator="$orders" />

---

## Verification Introduction Database Preparation
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

### Database Preparation

Next, your `users` table must contain an `email_verified_at` column to store the date and time that the user's email address was verified. Typically, this is included in Laravel's default `0001_01_01_000000_create_users_table.php` database migration.

---

## Components Select > flux:select
Components Select | livewire/flux@2.x | Source: https://fluxui.dev/components/select#livewire/flux

flux:select

<ui-table-scroll-area>

Prop

Description

    wire:model

Binds the select to a Livewire property. See the wire:model documentation for more information.

    placeholder

Text displayed when no option is selected.

    label

Label text displayed above the select. When provided, wraps the select in a flux:field component with an adjacent flux:label component. See the field component.

    description

Help text displayed below the select. When provided alongside label, appears between the label and select within the flux:field wrapper. See the field component.

    description:trailing

The description provided will be displayed below the select instead of above it.

    badge

Badge text displayed at the end of the flux:label component when the label prop is provided.

    size

Size of the select. Options: sm, xs.

    variant

Visual style of the select. Options: default (native select), listbox, combobox.

    multiple

Allows selecting multiple options (listbox variant only).

    filter

If false, disables client-side filtering.

    searchable

Adds a search input to filter options (listbox and combobox variants only).

    empty

Message shown when no search results are found for searchable selects. Default: No results found.

    clearable

Displays a clear button when an option is selected (listbox and combobox variants only).

    selected-suffix

Text appended to the number of selected options in multiple mode (listbox variant only).

    clear

When to clear the search input. Options: select (default), close (listbox and combobox variants only).

    disabled

Prevents user interaction with the select.

    invalid

Applies error styling to the select.

</ui-table-scroll-area>

<ui-table-scroll-area>

Slot

Description

    default

The select options.

    trigger

Custom trigger content. Typically the select.button or select.input component (listbox and combobox variants only).

    empty

Custom content shown when no options are found for searchable selects. Typically the select.option.empty component (listbox and combobox variants only).

</ui-table-scroll-area>

<ui-table-scroll-area>

Attribute

Description

    data-flux-select

Applied to the root element for styling and identification.

</ui-table-scroll-area>

---

## Validation Form objects
Validation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation

## Form objects

As more properties and validation rules are added to a Livewire component, it can begin to feel too crowded. To alleviate this pain and also provide a helpful abstraction for code reuse, you can use Livewire's *Form Objects* to store your properties and validation rules.

Below is the same `CreatePost` example, but now the properties and rules have been extracted to a dedicated form object named `PostForm`:

```php
<?php

namespace App\Livewire\Forms;

use Livewire\Attributes\Validate;
use Livewire\Form;

class PostForm extends Form
{
    #[Validate('required|min:3')]
	public $title = '';

    #[Validate('required|min:3')]
    public $content = '';
}
```

The `PostForm` above can now be defined as a property on the `CreatePost` component:

```php
<?php

namespace App\Livewire;

use App\Livewire\Forms\PostForm;
use Livewire\Component;
use App\Models\Post;

class CreatePost extends Component
{
    public PostForm $form;

    public function save()
    {
		Post::create(
    		$this->form->all()
    	);

		return redirect()->to('/posts');
    }

    // ...
}
```

As you can see, instead of listing out each property individually, we can retrieve all the property values using the `->all()` method on the form object.

Also, when referencing the property names in the template, you must prepend `form.` to each instance:

```blade
<form wire:submit="save">
	<input type="text" wire:model="form.title">
    <div>@error('form.title') {{ $message }} @enderror</div>

	<textarea wire:model="form.content"></textarea>
    <div>@error('form.content') {{ $message }} @enderror</div>

	<button type="submit">Save</button>
</form>
```

When using form objects, `#[Validate]` attribute validation will be run every time a property is updated. However, if you disable this behavior by specifying `onUpdate: false` on the attribute, you can manually run a form object's validation using `$this->form->validate()`:

```php
public function save()
{
    Post::create(
        $this->form->validate()
    );

    return redirect()->to('/posts');
}
```

Form objects are a useful abstraction for most larger datasets and a variety of additional features that make them even more powerful. For more information, check out the comprehensive [form object documentation](/docs/4.x/forms#extracting-a-form-object).

---

## Telescope Available Watchers Schedule Watcher
Telescope | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/telescope.md

### Schedule Watcher

The schedule watcher records the command and output of any [scheduled tasks](/docs/{{version}}/scheduling) run by your application.

---

## Migrating From Phpunit Guide
Migrating From Phpunit Guide | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/migrating-from-phpunit-guide.md

---
title: Migrating from PHPUnit
description: Migrating from PHPUnit to Pest is a simple process that can be completed in just a few steps.
---

# Migrating from PHPUnit

Pest is built on top of PHPUnit, so migrating from PHPUnit to Pest is a simple process that can be completed in just a few steps. Once you have Pest installed, you should require the `pestphp/pest-plugin-drift` package as a "dev" dependency in your project.

```bash
composer require pestphp/pest-plugin-drift --dev
```

Drift is a simple yet powerful plugin that will automatically convert your PHPUnit tests to Pest, simply by running the `--drift` option.

```bash
./vendor/bin/pest --drift
```

So, typically, a PHPUnit test looks like this:

```php
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    public function test_that_true_is_true(): void
    {
        $this->assertTrue(true);
    }
}
```

Should look like this after running `--drift`:

```php
test('true is true', function () {
    expect(true)->toBeTrue();
});
```

---
## Convert Phpunit tests only for certain folder

If you want to convert Phpunit tests only for certain folder, you can pass _path_ as first argument when calling `--drift`:

Example call, if you want to run the conversion for folder `/tests/Helpers`:
```console
/vendor/bin/pest --drift tests/Helpers
```

Output:
```console
/vendor/bin/pest --drift tests/Helpers

✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔

INFO  The [tests/Helpers] directory has been migrated to PEST with XY files changed.
```

---

The output will contain a summary of the conversion process, as well as a list of the files that were converted.

While most of your tests should be converted automatically, and you should be able to run them without any issues, there are some cases where you may need to manually convert some of your tests.

---

Of course, this particular chapter is only for those who are migrating from PHPUnit. Next, let's learn how you can contribute to the growth of Pest: [Community Guide](/docs/community-guide)

---

## Components Badge > As button
Components Badge | livewire/flux@2.x | Source: https://fluxui.dev/components/badge#livewire/flux

As button

Make the entire badge clickable by wrapping it in a button element.

<flux:badge as="button" rounded icon="plus" size="lg">Amount</flux:badge>

---

## Components Pagination > flux:pagination
Components Pagination | livewire/flux@2.x | Source: https://fluxui.dev/components/pagination#livewire/flux

flux:pagination

<ui-table-scroll-area>

Prop

Description

    paginator

The paginator instance to display.

    scroll-to

Scroll to an element when a pagination button is clicked. Pass a CSS selector to target a specific element. Default: body.

</ui-table-scroll-area>

---

## Components Badge > With close button
Components Badge | livewire/flux@2.x | Source: https://fluxui.dev/components/badge#livewire/flux

With close button

Make a badge removable by appending a close button.

<flux:badge>    Admin <flux:badge.close /></flux:badge>

---

## Scheduling Events
Scheduling | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scheduling.md

## Events

Laravel dispatches a variety of [events](/docs/{{version}}/events) during the scheduling process. You may [define listeners](/docs/{{version}}/events) for any of the following events:

| Event Name                                                  |
| ----------------------------------------------------------- |
| `Illuminate\Console\Events\ScheduledTaskStarting`           |
| `Illuminate\Console\Events\ScheduledTaskFinished`           |
| `Illuminate\Console\Events\ScheduledBackgroundTaskFinished` |
| `Illuminate\Console\Events\ScheduledTaskSkipped`            |
| `Illuminate\Console\Events\ScheduledTaskFailed`             |

---

## Lifecycle Hooks Hydrate & Dehydrate
Lifecycle Hooks | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/lifecycle-hooks

## Hydrate & Dehydrate

Hydrate and dehydrate are lesser-known and lesser-utilized hooks. However, there are specific scenarios where they can be powerful.

The terms "dehydrate" and "hydrate" refer to a Livewire component being serialized to JSON for the client-side and then unserialized back into a PHP object on the subsequent request.

We often use the terms "hydrate" and "dehydrate" to refer to this process throughout Livewire's codebase and the documentation. If you'd like more clarity on these terms, you can learn more by [consulting our hydration documentation](/docs/4.x/hydration).

Let's look at an example that uses both `mount()` , `hydrate()`, and `dehydrate()` all together to support using a custom [data transfer object (DTO)](https://en.wikipedia.org/wiki/Data_transfer_object) instead of an Eloquent model to store the post data in the component:

```php
<?php // resources/views/components/post/⚡show.blade.php

use Livewire\Component;

new class extends Component {
    public $post;

    public function mount($title, $content)
    {
        // Runs at the beginning of the first initial request...

        $this->post = new PostDto([
            'title' => $title,
            'content' => $content,
        ]);
    }

    public function hydrate()
    {
        // Runs at the beginning of every "subsequent" request...
        // This doesn't run on the initial request ("mount" does)...

        $this->post = new PostDto($this->post);
    }

    public function dehydrate()
    {
        // Runs at the end of every single request...

        $this->post = $this->post->toArray();
    }

    // ...
};
```

Now, from actions and other places inside your component, you can access the `PostDto` object instead of the primitive data.

The above example mainly demonstrates the abilities and nature of the `hydrate()` and `dehydrate()` hooks. However, it is recommended that you use [Wireables or Synthesizers](/docs/4.x/properties#supporting-custom-types) to accomplish this instead.

---

## Understanding Nesting Performance implications
Understanding Nesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/understanding-nesting

## Performance implications

Livewire's independent component architecture can have both positive and negative implications for your application.

An advantage of this architecture is it allows you to isolate expensive portions of your application. For example, you can quarantine a slow database query to its own independent component, and its performance overhead won't impact the rest of the page.

However, the biggest drawback of this approach is that because components are entirely separate, inter-component communication/dependencies becomes more difficult.

For example, if you had a property passed down from the above parent `Posts` component to the nested `ShowPost` component, it wouldn't be "reactive". Because each component is independent, if a request to the parent component changed the value of the property being passed into `ShowPost`, it wouldn't update inside `ShowPost`.

Livewire has overcome a number of these hurdles and provides dedicated APIs for these scenarios like: [Reactive properties](/docs/4.x/nesting#reactive-props), [Modelable components](/docs/4.x/nesting#binding-to-child-data-using-wiremodel), and [the `$parent` object](/docs/4.x/nesting#directly-accessing-the-parent-from-the-child).

Armed with this knowledge of how nested Livewire components operate, you will be able to make more informed decisions about when and how to nest components within your application.

---

## Upgrade Upgrading To 12.0 From 11.x Database Database Constructor Signature Changes
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Database Constructor Signature Changes

**Likelihood Of Impact: Very Low**

In Laravel 12, several low-level database classes now require an `Illuminate\Database\Connection` instance to be provided via their constructors.

**These changes are primarily applicable to database package maintainers - it is extremely unlikely any of these changes affect normal application development.**

`Illuminate\Database\Schema\Blueprint`

The constructor of the `Illuminate\Database\Schema\Blueprint` class now expects a `Connection` instance as its first argument. This primarily affects applications or packages that manually instantiate `Blueprint` instances.

`Illuminate\Database\Grammar`

The constructor of the `Illuminate\Database\Grammar` class also now requires a `Connection` instance. In previous versions, the connection was assigned after construction using the `setConnection()` method. This method has been removed in Laravel 12:

```php
// Laravel <= 11.x
$grammar = new MySqlGrammar;
$grammar->setConnection($connection);

// Laravel >= 12.x
$grammar = new MySqlGrammar($connection);
````

In addition, the following APIs have been removed or deprecated:

- The `Blueprint::getPrefix()` method is deprecated.
- The `Connection::withTablePrefix()` method has been removed.
- The `Grammar::getTablePrefix()` and `setTablePrefix()` methods are deprecated.
- The `Grammar::setConnection()` method has been removed.

When working with table prefixes, you should now retrieve them directly from the database connection:

```php
$prefix = $connection->getTablePrefix();
```

If you maintain custom database drivers, schema builders, or grammar implementations, you should review their constructors and ensure a `Connection` instance is provided.

---

## Docs Upgrading > Migrating accent colors
Docs Upgrading | livewire/flux@2.x | Source: https://fluxui.dev/docs/upgrading#livewire/flux

            Migrating accent colors

If you had previously defined custom accent colors in your app.css and tailwind.config.js file, then you will need to update them in your app.css file.

Before, these were defined in a single @layer base block within your app.css file, but now they are defined as two separate blocks:

    default colors are now inside the @theme block

    dark mode colors are now inside the @layer theme block

Here's an example of defining sky as your application's accent color:

@theme {    --color-accent: var(--color-sky-600);    --color-accent-content: var(--color-sky-600);    --color-accent-foreground: var(--color-white);}@layer theme {    .dark {        --color-accent: var(--color-sky-600);        --color-accent-content: var(--color-sky-400);        --color-accent-foreground: var(--color-white);    }}

If you're still using your old Tailwind config, you can now remove the following lines:

export default {    ...    theme: {        extend: {            colors: {                ...--             accent: {--                  DEFAULT: 'var(--color-accent)',--                  content: 'var(--color-accent-content)',--                  foreground: 'var(--color-accent-foreground)',--              },            },        },    },};

---

## Scheduling Defining Schedules Timezones
Scheduling | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scheduling.md

### Timezones

Using the `timezone` method, you may specify that a scheduled task's time should be interpreted within a given timezone:

```php
use Illuminate\Support\Facades\Schedule;

Schedule::command('report:generate')
    ->timezone('America/New_York')
    ->at('2:00')
```

If you are repeatedly assigning the same timezone to all of your scheduled tasks, you can specify which timezone should be assigned to all schedules by defining a `schedule_timezone` option within your application's `app` configuration file:

```php
'timezone' => 'UTC',

'schedule_timezone' => 'America/Chicago',
```

> [!WARNING]
> Remember that some timezones utilize daylight savings time. When daylight saving time changes occur, your scheduled task may run twice or even not run at all. For this reason, we recommend avoiding timezone scheduling when possible.

---

## Validation Available Validation Rules date_format:_format_,...
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### date_format:_format_,...

The field under validation must match one of the given _formats_. You should use **either** `date` or `date_format` when validating a field, not both. This validation rule supports all formats supported by PHP's [DateTime](https://www.php.net/manual/en/class.datetime.php) class.

For convenience, date-based rules may be constructed using the fluent `date` rule builder:

```php
use Illuminate\Validation\Rule;

'start_date' => [
    'required',
    Rule::date()->format('Y-m-d'),
],
```

---

## Queries Running Database Queries Chunking Results
Queries | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/queries.md

### Chunking Results

If you need to work with thousands of database records, consider using the `chunk` method provided by the `DB` facade. This method retrieves a small chunk of results at a time and feeds each chunk into a closure for processing. For example, let's retrieve the entire `users` table in chunks of 100 records at a time:

```php
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) {
    foreach ($users as $user) {
        // ...
    }
});
```

You may stop further chunks from being processed by returning `false` from the closure:

```php
DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) {
    // Process the records...

    return false;
});
```

If you are updating database records while chunking results, your chunk results could change in unexpected ways. If you plan to update the retrieved records while chunking, it is always best to use the `chunkById` method instead. This method will automatically paginate the results based on the record's primary key:

```php
DB::table('users')->where('active', false)
    ->chunkById(100, function (Collection $users) {
        foreach ($users as $user) {
            DB::table('users')
                ->where('id', $user->id)
                ->update(['active' => true]);
        }
    });
```

Since the `chunkById` and `lazyById` methods add their own "where" conditions to the query being executed, you should typically [logically group](#logical-grouping) your own conditions within a closure:

```php
DB::table('users')->where(function ($query) {
    $query->where('credits', 1)->orWhere('credits', 2);
})->chunkById(100, function (Collection $users) {
    foreach ($users as $user) {
        DB::table('users')
            ->where('id', $user->id)
            ->update(['credits' => 3]);
    }
});
```

> [!WARNING]
> When updating or deleting records inside the chunk callback, any changes to the primary key or foreign keys could affect the chunk query. This could potentially result in records not being included in the chunked results.

---

## Migrations Columns Available Column Types Object & Json Types
Migrations | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/migrations.md

#### Object & Json Types

[json](#column-method-json)
[jsonb](#column-method-jsonb)

---

## Redirecting Redirecting to full-page components
Redirecting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/redirecting

## Redirecting to full-page components

Because Livewire uses Laravel's built-in redirection feature, you can use all of the redirection methods available to you in a typical Laravel application.

For example, if you are using a Livewire component as a full-page component for a route like so:

```php
Route::livewire('/posts', 'pages::show-posts');
```

You can redirect to it simply by using the route path:

```php
public function save()
{
    // ...

    $this->redirect('/posts');
}
```

---

## Lazy Full-page lazy loading
Lazy | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/lazy

## Full-page lazy loading

You can lazy load or defer full-page Livewire components using route methods.

### Lazy loading full pages

Use `->lazy()` to load the component when it enters the viewport:

```php
Route::livewire('/dashboard', 'pages::dashboard')->lazy();
```

### Deferring full pages

Use `->defer()` to load the component immediately after the page loads:

```php
Route::livewire('/dashboard', 'pages::dashboard')->defer();
```

### Disabling lazy/defer loading

If a component is lazy or deferred by default (via the `#[Lazy]` or `#[Defer]` attribute), you can opt-out using `enabled: false`:

```php
Route::livewire('/dashboard', 'pages::dashboard')->lazy(enabled: false);
Route::livewire('/dashboard', 'pages::dashboard')->defer(enabled: false);
```

---

## Components Badge > flux:badge
Components Badge | livewire/flux@2.x | Source: https://fluxui.dev/components/badge#livewire/flux

flux:badge

<ui-table-scroll-area>

Prop

Description

    color

Badge color (e.g., zinc, red, blue). Default: zinc.

    size

Badge size. Options: sm, lg.

    rounded

If present or true, makes the badge fully rounded instead of slightly rounded corners.

    variant

Badge style variant. Options: solid (depracated: pill, use rounded prop instead).

    icon

Name of the icon to display before the badge text.

    icon:trailing

Name of the icon to display after the badge text.

    icon:variant

Icon variant. Options: outline, solid, mini, micro. Default: mini.

    as

HTML element to render the badge as. Options: button. Default: div.

    inset

Add negative margins to specific sides. Options: top, bottom, left, right, or any combination of the four.

</ui-table-scroll-area>

---

## Stress Testing The Stress function Requests Upload Data Rate
Stress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md

### Requests Upload Data Rate

Returns the request upload data rate in bytes per second.

```php
$result->requests()->upload()->data()->rate();
```

---

## Scheduling Task Hooks Pinging URLs
Scheduling | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scheduling.md

#### Pinging URLs

Using the `pingBefore` and `thenPing` methods, the scheduler can automatically ping a given URL before or after a task is executed. This method is useful for notifying an external service, such as [Envoyer](https://envoyer.io), that your scheduled task is beginning or has finished execution:

```php
Schedule::command('emails:send')
    ->daily()
    ->pingBefore($url)
    ->thenPing($url);
```

The `pingOnSuccess` and `pingOnFailure` methods may be used to ping a given URL only if the task succeeds or fails. A failure indicates that the scheduled Artisan or system command terminated with a non-zero exit code:

```php
Schedule::command('emails:send')
    ->daily()
    ->pingOnSuccess($successUrl)
    ->pingOnFailure($failureUrl);
```

The `pingBeforeIf`,`thenPingIf`,`pingOnSuccessIf`, and `pingOnFailureIf` methods may be used to ping a given URL only if a given condition is `true`:

```php
Schedule::command('emails:send')
    ->daily()
    ->pingBeforeIf($condition, $url)
    ->thenPingIf($condition, $url);

Schedule::command('emails:send')
    ->daily()
    ->pingOnSuccessIf($condition, $successUrl)
    ->pingOnFailureIf($condition, $failureUrl);
```

---

## Upgrade Upgrading To 12.0 From 11.x Database Multi-Schema Database Inspecting
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Multi-Schema Database Inspecting

**Likelihood Of Impact: Low**

The `Schema::getTables()`, `Schema::getViews()`, and `Schema::getTypes()` methods now include the results from all schemas by default. You may pass the `schema` argument to retrieve the result for the given schema only:

```php
// All tables on all schemas...
$tables = Schema::getTables();

// All tables on the 'main' schema...
$tables = Schema::getTables(schema: 'main');

// All tables on the 'main' and 'blog' schemas...
$tables = Schema::getTables(schema: ['main', 'blog']);
```

The `Schema::getTableListing()` method now returns schema-qualified table names by default. You may pass the `schemaQualified` argument to change the behavior as desired:

```php
$tables = Schema::getTableListing();
// ['main.migrations', 'main.users', 'blog.posts']

$tables = Schema::getTableListing(schema: 'main');
// ['main.migrations', 'main.users']

$tables = Schema::getTableListing(schema: 'main', schemaQualified: false);
// ['migrations', 'users']
```

The `db:table` and `db:show` commands now output the results of all schemas on MySQL, MariaDB, and SQLite, just like PostgreSQL and SQL Server.

---

## Lifecycle Hooks Using hooks inside a form object
Lifecycle Hooks | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/lifecycle-hooks

## Using hooks inside a form object

Form objects in Livewire support property update hooks. These hooks work similarly to [component update hooks](#update), letting you perform actions when properties in the form object change.

Below is an example of a component using a `PostForm` form object:

```php
<?php // resources/views/components/post/⚡create.blade.php

use Livewire\Component;

new class extends Component {
    public PostForm $form;

    // ...
};
```

Here's the `PostForm` form object containing all the available hooks:

```php
namespace App\Livewire\Forms;

use Livewire\Attributes\Validate;
use Livewire\Form;

class PostForm extends Form
{
    public $title = '';

    public $tags = [];

    public function updating($property, $value)
    {
        // ...
    }

    public function updated($property, $value)
    {
        // ...
    }

    public function updatingTitle($value)
    {
        // ...
    }

    public function updatedTitle($value)
    {
        // ...
    }

    public function updatingTags($value, $key)
    {
        // ...
    }

    public function updatedTags($value, $key)
    {
        // ...
    }

    // ...
}
```

---

## Scheduling Running the Scheduler Sub-Minute Scheduled Tasks
Scheduling | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scheduling.md

### Sub-Minute Scheduled Tasks

On most operating systems, cron jobs are limited to running a maximum of once per minute. However, Laravel's scheduler allows you to schedule tasks to run at more frequent intervals, even as often as once per second:

```php
use Illuminate\Support\Facades\Schedule;

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

When sub-minute tasks are defined within your application, the `schedule:run` command will continue running until the end of the current minute instead of exiting immediately. This allows the command to invoke all required sub-minute tasks throughout the minute.

Since sub-minute tasks that take longer than expected to run could delay the execution of later sub-minute tasks, it is recommended that all sub-minute tasks dispatch queued jobs or background commands to handle the actual task processing:

```php
use App\Jobs\DeleteRecentUsers;

Schedule::job(new DeleteRecentUsers)->everyTenSeconds();

Schedule::command('users:delete')->everyTenSeconds()->runInBackground();
```

---

## Packages Resources Migrations
Packages | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/packages.md

### Migrations

If your package contains [database migrations](/docs/{{version}}/migrations), you may use the `publishesMigrations` method to inform Laravel that the given directory or file contains migrations. When Laravel publishes the migrations, it will automatically update the timestamp within their filename to reflect the current date and time:

```php
/**
 * Bootstrap any package services.
 */
public function boot(): void
{
    $this->publishesMigrations([
        __DIR__.'/../database/migrations' => database_path('migrations'),
    ]);
}
```

---

## Routing Rate Limiting Attaching Rate Limiters to Routes Throttling With Redis
Routing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md

#### Throttling With Redis

By default, the `throttle` middleware is mapped to the `Illuminate\Routing\Middleware\ThrottleRequests` class. However, if you are using Redis as your application's cache driver, you may wish to instruct Laravel to use Redis to manage rate limiting. To do so, you should use the `throttleWithRedis` method in your application's `bootstrap/app.php` file. This method maps the `throttle` middleware to the `Illuminate\Routing\Middleware\ThrottleRequestsWithRedis` middleware class:

```php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->throttleWithRedis();
    // ...
})
```

---

## Attribute Title Only for full-page components
Attribute Title | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-title

## Only for full-page components

> [!info] Full-page components only
> The `#[Title]` attribute only works for full-page components accessed via routes. Regular components rendered within other views don't use titles—they inherit the parent page's title.

---

## Components Badge > flux:badge.close
Components Badge | livewire/flux@2.x | Source: https://fluxui.dev/components/badge#livewire/flux

flux:badge.close

<ui-table-scroll-area>

Prop

Description

    icon

Name of the icon to display. Default: x-mark.

    icon:variant

Icon variant. Options: outline, solid, mini, micro. Default: mini.

</ui-table-scroll-area>

---

## Forms See also
Forms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms

## See also

- **[Validation](/docs/4.x/validation)** — Validate form inputs with real-time feedback
- **[wire:model](/docs/4.x/wire-model)** — Bind form inputs to component properties
- **[File Uploads](/docs/4.x/uploads)** — Handle file uploads in forms
- **[Actions](/docs/4.x/actions)** — Process form submissions with actions
- **[Loading States](/docs/4.x/loading-states)** — Show loading indicators during form submission

---

## Stress Testing The Stress function Requests Download Data Rate
Stress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md

### Requests Download Data Rate

Returns the request download data rate in bytes per second.

```php
$result->requests()->download()->data()->rate();
```

---

## Scheduling Task Hooks
Scheduling | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scheduling.md

## Task Hooks

Using the `before` and `after` methods, you may specify code to be executed before and after the scheduled task is executed:

```php
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')
    ->daily()
    ->before(function () {
        // The task is about to execute...
    })
    ->after(function () {
        // The task has executed...
    });
```

The `onSuccess` and `onFailure` methods allow you to specify code to be executed if the scheduled task succeeds or fails. A failure indicates that the scheduled Artisan or system command terminated with a non-zero exit code:

```php
Schedule::command('emails:send')
    ->daily()
    ->onSuccess(function () {
        // The task succeeded...
    })
    ->onFailure(function () {
        // The task failed...
    });
```

If output is available from your command, you may access it in your `after`, `onSuccess` or `onFailure` hooks by type-hinting an `Illuminate\Support\Stringable` instance as the `$output` argument of your hook's closure definition:

```php
use Illuminate\Support\Stringable;

Schedule::command('emails:send')
    ->daily()
    ->onSuccess(function (Stringable $output) {
        // The task succeeded...
    })
    ->onFailure(function (Stringable $output) {
        // The task failed...
    });
```

---

## Upgrade Upgrading To 12.0 From 11.x Authentication Updated `DatabaseTokenRepository` Constructor Signature
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Updated `DatabaseTokenRepository` Constructor Signature

**Likelihood Of Impact: Very Low**

The constructor of the `Illuminate\Auth\Passwords\DatabaseTokenRepository` class now expects the `$expires` parameter to be given in seconds, rather than minutes.

---

## Queries Running Database Queries Aggregates
Queries | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/queries.md

### Aggregates

The query builder also provides a variety of methods for retrieving aggregate values like `count`, `max`, `min`, `avg`, and `sum`. You may call any of these methods after constructing your query:

```php
use Illuminate\Support\Facades\DB;

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

$price = DB::table('orders')->max('price');
```

Of course, you may combine these methods with other clauses to fine-tune how your aggregate value is calculated:

```php
$price = DB::table('orders')
    ->where('finalized', 1)
    ->avg('price');
```

---

## Migrations Events
Migrations | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/migrations.md

## Events

For convenience, each migration operation will dispatch an [event](/docs/{{version}}/events). All of the following events extend the base `Illuminate\Database\Events\MigrationEvent` class:

| Class                                            | Description                                      |
| ------------------------------------------------ | ------------------------------------------------ |
| `Illuminate\Database\Events\MigrationsStarted`   | A batch of migrations is about to be executed.   |
| `Illuminate\Database\Events\MigrationsEnded`     | A batch of migrations has finished executing.    |
| `Illuminate\Database\Events\MigrationStarted`    | A single migration is about to be executed.      |
| `Illuminate\Database\Events\MigrationEnded`      | A single migration has finished executing.       |
| `Illuminate\Database\Events\NoPendingMigrations` | A migration command found no pending migrations. |
| `Illuminate\Database\Events\SchemaDumped`        | A database schema dump has completed.            |
| `Illuminate\Database\Events\SchemaLoaded`        | An existing database schema dump has loaded.     |

---

## Validation Form Request Validation
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

## Form Request Validation

---

## Components Button > Full width
Components Button | livewire/flux@2.x | Source: https://fluxui.dev/components/button#livewire/flux

Full width

A button that spans the full width of the container.

<flux:button variant="primary" class="w-full">Send invite</flux:button>

---

## Migrations Columns Available Column Types `json()` {.collection-method}
Migrations | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/migrations.md

#### `json()` {.collection-method}

The `json` method creates a `JSON` equivalent column:

```php
$table->json('options');
```

When using SQLite, a `TEXT` column will be created.

---

## Stress Testing The Stress function Requests Failed Rate
Stress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md

### Requests Failed Rate

Returns the number of requests that failed per second.

```php
$result->requests()->failed()->rate();
```

---

## Routing Rate Limiting Defining Rate Limiters
Routing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md

### Defining Rate Limiters

Laravel includes powerful and customizable rate limiting services that you may utilize to restrict the amount of traffic for a given route or group of routes. To get started, you should define rate limiter configurations that meet your application's needs.

Rate limiters may be defined within the `boot` method of your application's `App\Providers\AppServiceProvider` class:

```php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
}
```

Rate limiters are defined using the `RateLimiter` facade's `for` method. The `for` method accepts a rate limiter name and a closure that returns the limit configuration that should apply to routes that are assigned to the rate limiter. Limit configuration are instances of the `Illuminate\Cache\RateLimiting\Limit` class. This class contains helpful "builder" methods so that you can quickly define your limit. The rate limiter name may be any string you wish:

```php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    RateLimiter::for('global', function (Request $request) {
        return Limit::perMinute(1000);
    });
}
```

If the incoming request exceeds the specified rate limit, a response with a 429 HTTP status code will automatically be returned by Laravel. If you would like to define your own response that should be returned by a rate limit, you may use the `response` method:

```php
RateLimiter::for('global', function (Request $request) {
    return Limit::perMinute(1000)->response(function (Request $request, array $headers) {
        return response('Custom response...', 429, $headers);
    });
});
```

Since rate limiter callbacks receive the incoming HTTP request instance, you may build the appropriate rate limit dynamically based on the incoming request or authenticated user:

```php
RateLimiter::for('uploads', function (Request $request) {
    return $request->user()->vipCustomer()
        ? Limit::none()
        : Limit::perHour(10);
});
```

---

## Migrations Indexes Foreign Key Constraints Toggling Foreign Key Constraints
Migrations | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/migrations.md

#### Toggling Foreign Key Constraints

You may enable or disable foreign key constraints within your migrations by using the following methods:

```php
Schema::enableForeignKeyConstraints();

Schema::disableForeignKeyConstraints();

Schema::withoutForeignKeyConstraints(function () {
    // Constraints disabled within this closure...
});
```

> [!WARNING]
> SQLite disables foreign key constraints by default. When using SQLite, make sure to [enable foreign key support](/docs/{{version}}/database#configuration) in your database configuration before attempting to create them in your migrations.

---

## Scheduling Task Output
Scheduling | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scheduling.md

## Task Output

The Laravel scheduler provides several convenient methods for working with the output generated by scheduled tasks. First, using the `sendOutputTo` method, you may send the output to a file for later inspection:

```php
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')
    ->daily()
    ->sendOutputTo($filePath);
```

If you would like to append the output to a given file, you may use the `appendOutputTo` method:

```php
Schedule::command('emails:send')
    ->daily()
    ->appendOutputTo($filePath);
```

Using the `emailOutputTo` method, you may email the output to an email address of your choice. Before emailing the output of a task, you should configure Laravel's [email services](/docs/{{version}}/mail):

```php
Schedule::command('report:generate')
    ->daily()
    ->sendOutputTo($filePath)
    ->emailOutputTo('taylor@example.com');
```

If you only want to email the output if the scheduled Artisan or system command terminates with a non-zero exit code, use the `emailOutputOnFailure` method:

```php
Schedule::command('report:generate')
    ->daily()
    ->emailOutputOnFailure('taylor@example.com');
```

> [!WARNING]
> The `emailOutputTo`, `emailOutputOnFailure`, `sendOutputTo`, and `appendOutputTo` methods are exclusive to the `command` and `exec` methods.

---

## Forms Throttling input
Forms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms

## Throttling input

As stated previously, when an input debounce is applied to a field, a network request will not be sent until the user has stopped typing for a certain amount of time. This means if the user continues typing a long message, a network request won't be sent until the user is finished.

Sometimes this isn't the desired behavior, and you would rather send a request as the user types, not when they've finished or taken a break.

In these cases, you can instead use `.throttle` to signify a time interval to send network requests:

```blade
<input type="text" wire:model.live.throttle.150ms="title" >
```

In the above example, as a user is typing continuously in the "title" field, a network request will be sent every 150 milliseconds until the user is finished.

---

## Scheduling Running the Scheduler Running the Scheduler Locally
Scheduling | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scheduling.md

### Running the Scheduler Locally

Typically, you would not add a scheduler cron entry to your local development machine. Instead, you may use the `schedule:work` Artisan command. This command will run in the foreground and invoke the scheduler every minute until you terminate the command. When sub-minute tasks are defined, the scheduler will continue running within each minute to process those tasks:

```shell
php artisan schedule:work
```

---

## Testing Running Tests Running Tests in Parallel Parallel Testing and Databases
Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/testing.md

#### Parallel Testing and Databases

As long as you have configured a primary database connection, Laravel automatically handles creating and migrating a test database for each parallel process that is running your tests. The test databases will be suffixed with a process token which is unique per process. For example, if you have two parallel test processes, Laravel will create and use `your_db_test_1` and `your_db_test_2` test databases.

By default, test databases persist between calls to the `test` Artisan command so that they can be used again by subsequent `test` invocations. However, you may re-create them using the `--recreate-databases` option:

```shell
php artisan test --parallel --recreate-databases
```

---

## Search Full-Text Search Running Full-Text Queries
Search | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/search.md

### Running Full-Text Queries

Once the index is in place, use the `whereFullText` query builder method to search against it. Laravel will generate the appropriate SQL for your database driver — for example, `MATCH(...) AGAINST(...)` on MariaDB and MySQL, and `to_tsvector(...) @@ plainto_tsquery(...)` on PostgreSQL:

```php
$articles = Article::whereFullText('body', 'web developer')->get();
```

When using MariaDB and MySQL, results are automatically ordered by relevance score. On PostgreSQL, `whereFullText` filters matching records but does not order them by relevance — if you need automatic relevance ordering on PostgreSQL, consider using [Scout's database engine](#database-engine), which handles this for you.

If you created a composite full-text index across multiple columns, you may search against all of them by passing the same array of columns to `whereFullText`:

```php
$articles = Article::whereFullText(
    ['title', 'body'], 'web developer'
)->get();
```

The `orWhereFullText` method may be used to add a full-text search clause as an "or" condition. For complete details, consult the [query builder documentation](/docs/{{version}}/queries#full-text-where-clauses).

_[Truncated]_