start-vibing-stacks 2.16.0 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/stacks/php/skills/composer-workflow/SKILL.md +118 -34
- package/stacks/php/skills/external-api-patterns/SKILL.md +171 -89
- package/stacks/php/skills/laravel-octane/SKILL.md +74 -2
- package/stacks/php/skills/mariadb-octane/SKILL.md +96 -4
- package/stacks/php/skills/php-patterns/SKILL.md +98 -5
- package/stacks/php/skills/phpstan-analysis/SKILL.md +136 -30
- package/stacks/php/skills/phpunit-testing/SKILL.md +247 -61
- package/stacks/php/skills/security-scan-php/SKILL.md +96 -3
package/package.json
CHANGED
|
@@ -1,65 +1,138 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: composer-workflow
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "Composer 2.8+ (Oct 2024) workflow for Laravel / PHP 8.3-8.5 projects. Covers composer.json structure, PSR-4 layout, scripts orchestration, audit command (--abandoned, --ignore-severity, exit codes 1=vulns/2=abandoned/3=both), --patch-only updates, --ignore-scripts for supply-chain hardening, allow-missing-requirements, lockfile hygiene, optimized autoloader for prod, sort-packages, plugin allowlist. Invoke when initializing a project, adding dependencies, debugging install/update conflicts, or wiring CI quality scripts."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# Composer Workflow
|
|
7
|
+
# Composer Workflow (2.8+)
|
|
8
|
+
|
|
9
|
+
**Invoke when initializing a Composer project, adding deps, debugging install/update, or wiring CI scripts.**
|
|
7
10
|
|
|
8
11
|
## Requirements
|
|
9
12
|
|
|
10
|
-
- **Composer
|
|
11
|
-
- **PHP
|
|
13
|
+
- **Composer ≥ 2.8** (released Oct 2, 2024) — earlier versions miss audit/security features below
|
|
14
|
+
- **PHP ≥ 8.3** in `require` (8.4 recommended for new projects — see `php-patterns`)
|
|
15
|
+
- **`composer.lock` MUST be committed** for both apps and libraries
|
|
12
16
|
|
|
13
17
|
## Essential Commands
|
|
14
18
|
|
|
15
19
|
```bash
|
|
16
|
-
composer install
|
|
17
|
-
composer
|
|
18
|
-
composer
|
|
19
|
-
composer
|
|
20
|
-
composer
|
|
20
|
+
composer install # From lock — used in CI/deploy
|
|
21
|
+
composer install --no-dev --optimize-autoloader --classmap-authoritative # Production
|
|
22
|
+
composer update # Refresh lock — dev only, never CI
|
|
23
|
+
composer update --patch-only # 2.8+: only patch versions (safe weekly)
|
|
24
|
+
composer update vendor/pkg --with-all-dependencies # Update one + its deps
|
|
25
|
+
|
|
26
|
+
composer require vendor/pkg # Add runtime
|
|
27
|
+
composer require --dev vendor/pkg # Add dev-time
|
|
28
|
+
composer remove vendor/pkg
|
|
29
|
+
|
|
30
|
+
composer audit # Security scan — MUST be in CI
|
|
31
|
+
composer audit --abandoned=fail # 2.8+: also fail on abandoned packages
|
|
32
|
+
composer audit --ignore-severity=low # 2.8+: skip noisy lows in CI
|
|
33
|
+
|
|
34
|
+
composer outdated # List packages with newer releases
|
|
35
|
+
composer outdated --direct # Only top-level deps
|
|
36
|
+
composer outdated --major-only # Only majors (planning sessions)
|
|
37
|
+
|
|
38
|
+
composer dump-autoload --optimize # Regenerate, optimized
|
|
39
|
+
composer validate --strict # composer.json sanity check
|
|
40
|
+
composer why vendor/pkg # Who pulled this in?
|
|
41
|
+
composer why-not vendor/pkg "^2.0" # Why can't I install this version?
|
|
21
42
|
```
|
|
22
43
|
|
|
23
|
-
## composer.json
|
|
44
|
+
## composer.json — production-grade baseline
|
|
24
45
|
|
|
25
46
|
```json
|
|
26
47
|
{
|
|
27
48
|
"name": "vendor/project",
|
|
28
49
|
"type": "project",
|
|
50
|
+
"license": "proprietary",
|
|
29
51
|
"require": {
|
|
30
|
-
"php": "
|
|
52
|
+
"php": "^8.3",
|
|
53
|
+
"ext-mbstring": "*",
|
|
54
|
+
"ext-pdo": "*"
|
|
31
55
|
},
|
|
32
56
|
"require-dev": {
|
|
33
|
-
"phpunit/phpunit": "^
|
|
57
|
+
"phpunit/phpunit": "^12.0",
|
|
58
|
+
"pestphp/pest": "^4.0",
|
|
34
59
|
"phpstan/phpstan": "^2.0",
|
|
35
|
-
"
|
|
60
|
+
"phpstan/phpstan-deprecation-rules": "^2.0",
|
|
61
|
+
"friendsofphp/php-cs-fixer": "^3.0",
|
|
62
|
+
"roave/security-advisories": "dev-latest"
|
|
36
63
|
},
|
|
37
64
|
"autoload": {
|
|
38
|
-
"psr-4": {
|
|
39
|
-
"App\\": "src/"
|
|
40
|
-
}
|
|
65
|
+
"psr-4": { "App\\": "src/" }
|
|
41
66
|
},
|
|
42
67
|
"autoload-dev": {
|
|
43
|
-
"psr-4": {
|
|
44
|
-
"Tests\\": "tests/"
|
|
45
|
-
}
|
|
68
|
+
"psr-4": { "Tests\\": "tests/" }
|
|
46
69
|
},
|
|
47
70
|
"scripts": {
|
|
48
|
-
"test":
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
]
|
|
71
|
+
"test": "phpunit",
|
|
72
|
+
"test:cov":"XDEBUG_MODE=coverage phpunit --coverage-text --coverage-clover=coverage.xml",
|
|
73
|
+
"lint": "phpstan analyse --memory-limit=1G",
|
|
74
|
+
"fix": "php-cs-fixer fix",
|
|
75
|
+
"fix:check":"php-cs-fixer fix --dry-run --diff",
|
|
76
|
+
"audit": "composer audit --abandoned=fail",
|
|
77
|
+
"check": ["@lint", "@fix:check", "@audit", "@test"]
|
|
78
|
+
},
|
|
79
|
+
"scripts-descriptions": {
|
|
80
|
+
"check": "Run full quality gate locally — mirrors CI"
|
|
55
81
|
},
|
|
56
82
|
"config": {
|
|
57
83
|
"sort-packages": true,
|
|
58
|
-
"
|
|
59
|
-
|
|
84
|
+
"optimize-autoloader": true,
|
|
85
|
+
"preferred-install": "dist",
|
|
86
|
+
"allow-plugins": {
|
|
87
|
+
"pestphp/pest-plugin": true
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"minimum-stability": "stable",
|
|
91
|
+
"prefer-stable": true
|
|
60
92
|
}
|
|
61
93
|
```
|
|
62
94
|
|
|
95
|
+
## Audit — exit codes (2.8+, use in CI)
|
|
96
|
+
|
|
97
|
+
| Exit | Meaning |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `0` | Clean |
|
|
100
|
+
| `1` | Vulnerabilities found |
|
|
101
|
+
| `2` | Abandoned packages found (with `--abandoned=fail`) |
|
|
102
|
+
| `3` | Both |
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
# .github/workflows/ci.yml — fail PR on vulns OR abandoned packages
|
|
106
|
+
- name: Composer audit
|
|
107
|
+
run: composer audit --abandoned=fail --ignore-severity=low
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The `--ignore-severity` flag is critical for CI ergonomics — without it a single LOW advisory blocks every PR.
|
|
111
|
+
|
|
112
|
+
## Supply-Chain Hardening (links to security-baseline 2025-A03)
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Production install — never run third-party scripts on the build server
|
|
116
|
+
composer install --no-dev --no-scripts --optimize-autoloader --classmap-authoritative
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`config.allow-plugins` is a strict allowlist as of 2.x — Composer prompts/refuses unknown plugins. Keep the list minimal:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
"config": {
|
|
123
|
+
"allow-plugins": {
|
|
124
|
+
"pestphp/pest-plugin": true,
|
|
125
|
+
"php-http/discovery": false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Pin **Roave Security Advisories** as a `dev` dep — it makes Composer **refuse to install** any version with a known CVE:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
composer require --dev roave/security-advisories:dev-latest
|
|
134
|
+
```
|
|
135
|
+
|
|
63
136
|
## PSR-4 Autoloading
|
|
64
137
|
|
|
65
138
|
```
|
|
@@ -71,13 +144,24 @@ src/
|
|
|
71
144
|
├── Models/
|
|
72
145
|
│ └── User.php → App\Models\User
|
|
73
146
|
└── Middleware/
|
|
74
|
-
└── AuthMiddleware.php
|
|
147
|
+
└── AuthMiddleware.php → App\Middleware\AuthMiddleware
|
|
75
148
|
```
|
|
76
149
|
|
|
150
|
+
For Laravel projects use `App\\` → `app/` (Laravel default), not `src/`.
|
|
151
|
+
|
|
152
|
+
## Lockfile Hygiene
|
|
153
|
+
|
|
154
|
+
- **Always commit** `composer.lock`
|
|
155
|
+
- After `composer require/remove/update`, run `composer validate --strict` and commit the lock with the same commit
|
|
156
|
+
- CI uses `composer install` (deterministic) — never `composer update`
|
|
157
|
+
- Dependabot/Renovate watches `composer.json`; configure auto-merge for `--patch-only` after CI green
|
|
158
|
+
|
|
77
159
|
## Rules
|
|
78
160
|
|
|
79
|
-
1.
|
|
80
|
-
2. **
|
|
81
|
-
3. **
|
|
82
|
-
4. **
|
|
83
|
-
5. **
|
|
161
|
+
1. **`composer.lock` is mandatory in git** — apps and libraries
|
|
162
|
+
2. **Production = `--no-dev --no-scripts --optimize-autoloader --classmap-authoritative`** (supply-chain + perf)
|
|
163
|
+
3. **CI runs `composer audit --abandoned=fail`** — failing the build on vulns or abandons
|
|
164
|
+
4. **Never edit `vendor/`** — patch via `cweagans/composer-patches` if absolutely required
|
|
165
|
+
5. **Pin `php` constraint** to your runtime (`^8.3` or `^8.4`); upgrade deliberately
|
|
166
|
+
6. **Roave Security Advisories** as a dev dep on every project
|
|
167
|
+
7. **Allow-plugins allowlist** kept minimal and reviewed
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: external-api-patterns
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "Laravel patterns for consuming external APIs / SDKs / webhooks (2026). Two recommended approaches: (1) Laravel HTTP Client (`Http::`) wrapped in a Service for one-off integrations; (2) Saloon v3 for SDK-style integrations with multiple endpoints (Connectors + Requests + Responses, retry/auth/middleware/pagination first-class, mock client for testing, Laravel plugin). Octane-safe (no static client state), typed DTOs, typed exceptions, structured logging, idempotent webhook reception with signature verification (2025-A10 fail-closed). Invoke when adding any third-party integration or webhook handler."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# External API Patterns —
|
|
7
|
+
# External API Patterns — Laravel + Octane (2026)
|
|
7
8
|
|
|
8
9
|
**ALWAYS invoke when consuming external APIs, webhooks, or third-party services.**
|
|
9
10
|
|
|
11
|
+
## Choose your tool
|
|
12
|
+
|
|
13
|
+
| Scenario | Tool |
|
|
14
|
+
|---|---|
|
|
15
|
+
| 1–3 endpoints from one vendor, simple req/res | Laravel `Http::` Client wrapped in a Service |
|
|
16
|
+
| SDK-shaped integration: many endpoints, auth flows, pagination, custom responses | **Saloon v3** (Connectors + Requests + Responses) |
|
|
17
|
+
| Vendor publishes its own SDK | Use the SDK; still wrap in a Service for testability |
|
|
18
|
+
|
|
19
|
+
Either way: **never call `Http::` or a vendor SDK directly from a Controller**. Always go through an injected Service.
|
|
20
|
+
|
|
10
21
|
## Architecture
|
|
11
22
|
|
|
12
23
|
```
|
|
13
|
-
Controller/Job
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
Controller / Job / Listener
|
|
25
|
+
↓
|
|
26
|
+
ApiService (business logic, transactions)
|
|
27
|
+
↓
|
|
28
|
+
HTTP transport
|
|
29
|
+
├── Http::baseUrl(...) → response()->json() (Laravel Client path)
|
|
30
|
+
└── new Connector → ->send(new Request()) (Saloon path)
|
|
31
|
+
↓
|
|
32
|
+
Typed DTO (readonly class)
|
|
33
|
+
↓
|
|
34
|
+
Typed exception on failure / log + return on success
|
|
21
35
|
```
|
|
22
36
|
|
|
23
37
|
## Service Pattern
|
|
@@ -308,100 +322,157 @@ class AiModelController extends Controller
|
|
|
308
322
|
}
|
|
309
323
|
```
|
|
310
324
|
|
|
311
|
-
##
|
|
325
|
+
## Saloon v3 — SDK-style integrations *(2026 recommended)*
|
|
312
326
|
|
|
313
|
-
|
|
314
|
-
// resources/js/services/api.ts
|
|
315
|
-
import axios, { AxiosError } from 'axios';
|
|
327
|
+
Use when the integration has **many endpoints, auth flows, pagination, or warrants its own folder**. Saloon turns "an API" into an OOP shape: a `Connector` (base URL + auth + middleware) plus one `Request` per endpoint plus optional custom `Response` classes.
|
|
316
328
|
|
|
317
|
-
|
|
318
|
-
success: boolean;
|
|
319
|
-
message: string;
|
|
320
|
-
data: T;
|
|
321
|
-
errors?: Record<string, string[]>;
|
|
322
|
-
}
|
|
329
|
+
### Install
|
|
323
330
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
331
|
+
```bash
|
|
332
|
+
composer require saloonphp/saloon "^3.0"
|
|
333
|
+
composer require saloonphp/laravel-plugin "^4.0" # Laravel integration
|
|
334
|
+
php artisan vendor:publish --tag=saloon-config
|
|
335
|
+
```
|
|
330
336
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
337
|
+
### Folder layout
|
|
338
|
+
|
|
339
|
+
```
|
|
340
|
+
app/Http/Integrations/
|
|
341
|
+
└── OpenAi/
|
|
342
|
+
├── OpenAiConnector.php
|
|
343
|
+
├── Requests/
|
|
344
|
+
│ ├── ChatCompletion.php
|
|
345
|
+
│ └── Embeddings.php
|
|
346
|
+
└── Responses/
|
|
347
|
+
└── ChatCompletionResponse.php
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Connector
|
|
351
|
+
|
|
352
|
+
```php
|
|
353
|
+
// app/Http/Integrations/OpenAi/OpenAiConnector.php
|
|
354
|
+
namespace App\Http\Integrations\OpenAi;
|
|
355
|
+
|
|
356
|
+
use Saloon\Http\Connector;
|
|
357
|
+
use Saloon\Http\Auth\TokenAuthenticator;
|
|
358
|
+
use Saloon\Traits\Plugins\AcceptsJson;
|
|
359
|
+
use Saloon\RateLimitPlugin\Traits\HasRateLimits;
|
|
360
|
+
|
|
361
|
+
class OpenAiConnector extends Connector
|
|
362
|
+
{
|
|
363
|
+
use AcceptsJson;
|
|
364
|
+
use HasRateLimits;
|
|
365
|
+
|
|
366
|
+
public function resolveBaseUrl(): string
|
|
367
|
+
{
|
|
368
|
+
return config('services.openai.base_url', 'https://api.openai.com/v1');
|
|
355
369
|
}
|
|
356
|
-
}
|
|
357
370
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
371
|
+
protected function defaultHeaders(): array
|
|
372
|
+
{
|
|
373
|
+
return ['User-Agent' => 'myapp/1.0'];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
protected function defaultAuth(): TokenAuthenticator
|
|
377
|
+
{
|
|
378
|
+
return new TokenAuthenticator(config('services.openai.key'));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
protected function defaultConfig(): array
|
|
382
|
+
{
|
|
383
|
+
return ['timeout' => 30, 'connect_timeout' => 5];
|
|
384
|
+
}
|
|
364
385
|
}
|
|
365
|
-
|
|
366
|
-
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Request
|
|
389
|
+
|
|
390
|
+
```php
|
|
391
|
+
// app/Http/Integrations/OpenAi/Requests/ChatCompletion.php
|
|
392
|
+
namespace App\Http\Integrations\OpenAi\Requests;
|
|
393
|
+
|
|
394
|
+
use Saloon\Contracts\Body\HasBody;
|
|
395
|
+
use Saloon\Enums\Method;
|
|
396
|
+
use Saloon\Http\Request;
|
|
397
|
+
use Saloon\Traits\Body\HasJsonBody;
|
|
398
|
+
|
|
399
|
+
class ChatCompletion extends Request implements HasBody
|
|
400
|
+
{
|
|
401
|
+
use HasJsonBody;
|
|
402
|
+
|
|
403
|
+
protected Method $method = Method::POST;
|
|
404
|
+
|
|
405
|
+
public function __construct(
|
|
406
|
+
protected readonly string $model,
|
|
407
|
+
protected readonly array $messages,
|
|
408
|
+
protected readonly float $temperature = 0.7,
|
|
409
|
+
) {}
|
|
410
|
+
|
|
411
|
+
public function resolveEndpoint(): string { return '/chat/completions'; }
|
|
412
|
+
|
|
413
|
+
protected function defaultBody(): array
|
|
414
|
+
{
|
|
415
|
+
return [
|
|
416
|
+
'model' => $this->model,
|
|
417
|
+
'messages' => $this->messages,
|
|
418
|
+
'temperature' => $this->temperature,
|
|
419
|
+
];
|
|
420
|
+
}
|
|
367
421
|
}
|
|
368
422
|
```
|
|
369
423
|
|
|
370
|
-
###
|
|
424
|
+
### Calling from a Service
|
|
371
425
|
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
generating: __('messages.ai.generating'),
|
|
375
|
-
error: __('messages.errors.error'),
|
|
376
|
-
rateLimited: __('messages.errors.rate_limited'),
|
|
377
|
-
} as const;
|
|
426
|
+
```php
|
|
427
|
+
namespace App\Services\External;
|
|
378
428
|
|
|
379
|
-
|
|
380
|
-
|
|
429
|
+
use App\Http\Integrations\OpenAi\OpenAiConnector;
|
|
430
|
+
use App\Http\Integrations\OpenAi\Requests\ChatCompletion;
|
|
431
|
+
use App\DTOs\Api\ChatCompletionResponse;
|
|
381
432
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
} else if (error instanceof ValidationError) {
|
|
394
|
-
Object.values(error.errors).flat().forEach(msg => toast.error(msg));
|
|
395
|
-
} else {
|
|
396
|
-
toast.error(error instanceof ApiError ? error.message : LABELS.error);
|
|
397
|
-
}
|
|
398
|
-
} finally {
|
|
399
|
-
setLoading(false);
|
|
400
|
-
}
|
|
401
|
-
};
|
|
433
|
+
class OpenAiService
|
|
434
|
+
{
|
|
435
|
+
public function __construct(private readonly OpenAiConnector $api) {}
|
|
436
|
+
|
|
437
|
+
public function chat(string $model, array $messages): ChatCompletionResponse
|
|
438
|
+
{
|
|
439
|
+
$response = $this->api
|
|
440
|
+
->send(new ChatCompletion($model, $messages))
|
|
441
|
+
->throw();
|
|
442
|
+
return ChatCompletionResponse::fromArray($response->json());
|
|
443
|
+
}
|
|
402
444
|
}
|
|
403
445
|
```
|
|
404
446
|
|
|
447
|
+
### Test with Saloon's MockClient
|
|
448
|
+
|
|
449
|
+
```php
|
|
450
|
+
use Saloon\Http\Faking\MockClient;
|
|
451
|
+
use Saloon\Http\Faking\MockResponse;
|
|
452
|
+
use App\Http\Integrations\OpenAi\Requests\ChatCompletion;
|
|
453
|
+
|
|
454
|
+
it('returns the assistant message', function () {
|
|
455
|
+
$mock = new MockClient([
|
|
456
|
+
ChatCompletion::class => MockResponse::make(['choices' => [['message' => ['content' => 'hi']]], 'usage' => ['prompt_tokens' => 1, 'completion_tokens' => 1]], 200),
|
|
457
|
+
]);
|
|
458
|
+
app(OpenAiConnector::class)->withMockClient($mock);
|
|
459
|
+
|
|
460
|
+
$resp = app(OpenAiService::class)->chat('gpt-4', [['role' => 'user', 'content' => 'hi']]);
|
|
461
|
+
expect($resp->content)->toBe('hi');
|
|
462
|
+
});
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### When Saloon shines vs raw `Http::`
|
|
466
|
+
|
|
467
|
+
- **Auth is non-trivial** (OAuth2 device flow, signed requests, paginated tokens)
|
|
468
|
+
- **Pagination** (cursor / page / link-header) — Saloon ships first-class paginators
|
|
469
|
+
- **Many endpoints** sharing the same base config — DRY via the Connector
|
|
470
|
+
- **Test coverage matters** — `MockClient` captures request shape, not just HTTP verb
|
|
471
|
+
- **Ratelimit awareness** — `HasRateLimits` plugin tracks per-connector budgets
|
|
472
|
+
- **Want the API to feel like a vendor SDK** to your call sites
|
|
473
|
+
|
|
474
|
+
For 1-2 throwaway endpoints, raw `Http::` is fine — don't over-engineer.
|
|
475
|
+
|
|
405
476
|
## Octane Safety
|
|
406
477
|
|
|
407
478
|
```php
|
|
@@ -513,6 +584,17 @@ Log::error('[ServiceName] API failed', [
|
|
|
513
584
|
| `static $client` in Octane | Instance `$this->client` per request |
|
|
514
585
|
| No timeout on HTTP calls | `->timeout(30)->connectTimeout(5)` |
|
|
515
586
|
| No retry logic | `->retry(3, backoff, when)` |
|
|
516
|
-
| Webhook without signature check | ALWAYS verify signatures |
|
|
587
|
+
| Webhook without signature check | ALWAYS verify signatures (2025-A10 fail-closed) |
|
|
517
588
|
| Webhook sync processing | Dispatch job for async |
|
|
518
589
|
| `dd()` / `dump()` API responses | Structured `Log::info/error` |
|
|
590
|
+
| Many endpoints inlined as `Http::` calls | Promote to Saloon Connector + Requests |
|
|
591
|
+
| Mocking Saloon by stubbing `Http::` | Use `MockClient` (matches by Request class) |
|
|
592
|
+
|
|
593
|
+
## See Also
|
|
594
|
+
|
|
595
|
+
- `axios-laravel-api` (frontend) — the React side that consumes this Laravel API
|
|
596
|
+
- `api-design` / `api-security` — request shape + Sanctum patterns
|
|
597
|
+
- `_shared/skills/security-baseline` (2025-A03 + 2025-A10) — supply chain + fail-closed
|
|
598
|
+
- `_shared/skills/observability` — structured logging + redaction
|
|
599
|
+
- `mariadb-octane` — transaction safety when API call participates in a DB transaction
|
|
600
|
+
- `phpunit-testing` — Pest 4 patterns for Saloon `MockClient` and `Http::fake()`
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: laravel-octane
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "Laravel Octane patterns for production 2026 — covers FrankenPHP (recommended for new projects: single binary, automatic HTTPS, HTTP/3, early hints, Brotli, ~5× FPM throughput), RoadRunner (mature, best raw RPS, gRPC/WebSocket), and Swoole (best for I/O-heavy workloads via coroutines). Long-lived worker rules: no static state, no globals, no superglobals, no config() mutations, no die/exit/dd, scoped bindings instead of singletons, persistent DB connections with Octane flush, instance-scoped memoization. Includes server selection matrix and per-server config. Invoke when adopting Octane, choosing a server, debugging worker leaks, or hardening for persistent processes."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# Laravel Octane (RoadRunner)
|
|
7
|
+
# Laravel Octane Patterns (FrankenPHP / RoadRunner / Swoole)
|
|
7
8
|
|
|
8
9
|
## How Octane Works
|
|
9
10
|
|
|
@@ -11,6 +12,16 @@ Octane runs your app in a **long-lived worker process**. The application boots O
|
|
|
11
12
|
|
|
12
13
|
**Consequence:** Any state stored in static properties, globals, or singletons persists across requests and can leak between users.
|
|
13
14
|
|
|
15
|
+
## Server Selection — 2026 matrix
|
|
16
|
+
|
|
17
|
+
| Server | When to choose | Strengths | Trade-offs |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| **FrankenPHP** | New projects; teams that value simplicity | Single Go binary; automatic HTTPS via Caddy; **HTTP/3**, early hints, Brotli; 5× PHP-FPM in 2025 benchmarks | Smaller community; weaker under heavy I/O latency |
|
|
20
|
+
| **RoadRunner** | Production at scale; need consistent top-tier RPS | Battle-tested; ~111% higher RPS than FPM; gRPC + WebSocket built-in; rich plugin ecosystem | Steeper DevOps curve; YAML config |
|
|
21
|
+
| **Swoole** | Workloads dominated by 200–500 ms I/O waits (DB, upstream APIs) | Coroutine concurrency shines under high I/O latency | Lower RPS in light-I/O workloads; smaller adoption for new apps |
|
|
22
|
+
|
|
23
|
+
Default recommendation for new Laravel 12 projects in 2026: **FrankenPHP** (developer experience + modern HTTP). Pick **RoadRunner** if you've already standardized on it or need its specific features.
|
|
24
|
+
|
|
14
25
|
## Critical Rules
|
|
15
26
|
|
|
16
27
|
### DO: Dependency Injection
|
|
@@ -165,6 +176,67 @@ app()->scoped('cart', fn () => new Cart());
|
|
|
165
176
|
// Fresh instance per request, cleared between requests
|
|
166
177
|
```
|
|
167
178
|
|
|
179
|
+
## FrankenPHP Configuration *(2026 default)*
|
|
180
|
+
|
|
181
|
+
Single binary; Caddy-based HTTPS; no PHP extension required.
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
composer require laravel/octane
|
|
185
|
+
php artisan octane:install --server=frankenphp
|
|
186
|
+
php artisan octane:start --server=frankenphp --workers=4 --max-requests=500
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`Caddyfile` (production-grade, with HTTP/3 + early hints):
|
|
190
|
+
|
|
191
|
+
```caddyfile
|
|
192
|
+
{
|
|
193
|
+
frankenphp
|
|
194
|
+
order php_server before file_server
|
|
195
|
+
servers {
|
|
196
|
+
protocols h1 h2 h2c h3
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
api.example.com {
|
|
201
|
+
encode br zstd gzip
|
|
202
|
+
root * public/
|
|
203
|
+
php_server {
|
|
204
|
+
worker /path/to/public/frankenphp-worker.php 4
|
|
205
|
+
}
|
|
206
|
+
header ?Permissions-Policy "interest-cohort=()"
|
|
207
|
+
header Early-Hints rel=preload
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`public/frankenphp-worker.php` (worker mode — what gives you the 5× speedup):
|
|
212
|
+
|
|
213
|
+
```php
|
|
214
|
+
<?php
|
|
215
|
+
ignore_user_abort(true);
|
|
216
|
+
require __DIR__.'/../vendor/autoload.php';
|
|
217
|
+
$app = require __DIR__.'/../bootstrap/app.php';
|
|
218
|
+
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
|
|
219
|
+
|
|
220
|
+
$max = (int)($_SERVER['MAX_REQUESTS'] ?? 500);
|
|
221
|
+
for ($i = 0; $i < $max; $i++) {
|
|
222
|
+
$keepRunning = \frankenphp_handle_request(function () use ($kernel) {
|
|
223
|
+
$request = Illuminate\Http\Request::capture();
|
|
224
|
+
$response = $kernel->handle($request);
|
|
225
|
+
$response->send();
|
|
226
|
+
$kernel->terminate($request, $response);
|
|
227
|
+
});
|
|
228
|
+
gc_collect_cycles();
|
|
229
|
+
if (!$keepRunning) break;
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
| Setting | Where | Notes |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `--workers` | CLI | Defaults to CPU cores; tune to CPU/IO ratio |
|
|
236
|
+
| `--max-requests` | CLI | Restart worker after N requests (memory safety, like RoadRunner's `max_jobs`) |
|
|
237
|
+
| `protocols h3` | Caddyfile | Enable HTTP/3 (QUIC) — single biggest mobile-network win |
|
|
238
|
+
| `Early-Hints` header | Response | Browser preloads CSS/JS during PHP think-time |
|
|
239
|
+
|
|
168
240
|
## RoadRunner Configuration (rr.yaml)
|
|
169
241
|
|
|
170
242
|
```yaml
|