start-vibing-stacks 2.16.0 → 2.18.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/stacks/python/skills/api-security-python/SKILL.md +118 -15
- package/stacks/python/skills/async-patterns/SKILL.md +166 -62
- package/stacks/python/skills/django-patterns/SKILL.md +102 -11
- package/stacks/python/skills/fastapi-patterns/SKILL.md +277 -62
- package/stacks/python/skills/pydantic-validation/SKILL.md +106 -11
- package/stacks/python/skills/pytest-testing/SKILL.md +172 -54
- package/stacks/python/skills/python-patterns/SKILL.md +49 -7
- package/stacks/python/skills/python-performance/SKILL.md +183 -3
- package/stacks/python/skills/scripting-automation/SKILL.md +205 -119
|
@@ -1,12 +1,91 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mariadb-octane
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "MariaDB patterns for Laravel Octane (long-lived workers) targeting MariaDB 11.8 LTS (released 2025, supported until June 2028). Octane-safe connection config (PERSISTENT + EMULATE_PREPARES off + STRICT_TRANS_TABLES + utf8mb4 default in 11.8), automatic flush on request boundary, Octane::tick health probes, MariaDB 11.8 features (VECTOR data type + VEC_DISTANCE_*, JSON Schema validation, system-versioned temporal tables, TIMESTAMP range to 2106, PARSEC auth plugin, parallel mariadb-dump), strict migrations with explicit lengths, composite/covering indexes, cursor pagination, transaction safety in workers, immutable_datetime casts. Invoke for any query, migration, model, or DB config in Laravel Octane."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# MariaDB + Octane — Database Patterns
|
|
7
|
+
# MariaDB + Octane — Database Patterns (MariaDB 11.8 LTS, 2026)
|
|
7
8
|
|
|
8
9
|
**ALWAYS invoke when writing queries, migrations, models, or DB config in Laravel Octane.**
|
|
9
10
|
|
|
11
|
+
## MariaDB 11.8 LTS — what changed (release Jun 2025; LTS until Jun 2028)
|
|
12
|
+
|
|
13
|
+
MariaDB 11.8 supersedes 11.4 LTS. Adopt for new projects; plan migration for existing.
|
|
14
|
+
|
|
15
|
+
| Feature | Why it matters |
|
|
16
|
+
|---|---|
|
|
17
|
+
| **Default charset is now `utf8mb4`** (was `latin1` for ~20 years) | New tables don't need explicit charset; old tables stay as-is until ALTERed |
|
|
18
|
+
| **Native `VECTOR` type + `VEC_DISTANCE_COSINE` / `VEC_DISTANCE_EUCLIDEAN`** | Embeddings + similarity search in the same DB — RAG without a separate vector store. SIMD on AVX2/AVX512/ARM/PowerPC |
|
|
19
|
+
| **JSON Schema validation** at column level | Constrain `JSON` columns instead of validating only in PHP |
|
|
20
|
+
| **System-versioned (temporal) tables** | Native row history, point-in-time queries — replaces hand-rolled audit tables |
|
|
21
|
+
| **`TIMESTAMP` range extended to 2106** | The 2038 problem is fixed for new schemas |
|
|
22
|
+
| **PARSEC auth plugin** (Password Auth Response Signed by Elliptic Curves) | Stronger than `mysql_native_password`; safer than `caching_sha2_password` for SSL-less internal hops |
|
|
23
|
+
| **Parallel `mariadb-dump` / `mariadb-import`** | Backup/restore minutes instead of hours on multi-GB DBs |
|
|
24
|
+
|
|
25
|
+
### Vector search example (RAG-style)
|
|
26
|
+
|
|
27
|
+
```sql
|
|
28
|
+
-- MariaDB 11.7+
|
|
29
|
+
CREATE TABLE doc_chunks (
|
|
30
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
31
|
+
text TEXT NOT NULL,
|
|
32
|
+
embedding VECTOR(1536) NOT NULL, -- e.g. text-embedding-3-small
|
|
33
|
+
VECTOR INDEX (embedding) USING HNSW -- approximate nearest neighbor
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
-- Similarity search
|
|
37
|
+
SELECT id, text, VEC_DISTANCE_COSINE(embedding, VEC_FromText(?)) AS distance
|
|
38
|
+
FROM doc_chunks
|
|
39
|
+
ORDER BY distance
|
|
40
|
+
LIMIT 5;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```php
|
|
44
|
+
// Eloquent — bind embedding as a JSON-encoded array of floats; MariaDB driver coerces
|
|
45
|
+
$results = DB::select(
|
|
46
|
+
'SELECT id, text, VEC_DISTANCE_COSINE(embedding, VEC_FromText(?)) AS distance
|
|
47
|
+
FROM doc_chunks ORDER BY distance LIMIT 5',
|
|
48
|
+
[json_encode($queryEmbedding)],
|
|
49
|
+
);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### System-versioned tables (audit history without triggers)
|
|
53
|
+
|
|
54
|
+
```sql
|
|
55
|
+
CREATE TABLE accounts (
|
|
56
|
+
id INT PRIMARY KEY,
|
|
57
|
+
balance DECIMAL(12,2) NOT NULL,
|
|
58
|
+
PERIOD FOR system_time(start_ts, end_ts),
|
|
59
|
+
start_ts TIMESTAMP(6) GENERATED ALWAYS AS ROW START,
|
|
60
|
+
end_ts TIMESTAMP(6) GENERATED ALWAYS AS ROW END
|
|
61
|
+
) WITH SYSTEM VERSIONING;
|
|
62
|
+
|
|
63
|
+
-- Query historical state
|
|
64
|
+
SELECT * FROM accounts FOR SYSTEM_TIME AS OF '2026-04-01 12:00:00';
|
|
65
|
+
SELECT * FROM accounts FOR SYSTEM_TIME BETWEEN '2026-04-01' AND '2026-05-01';
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Replaces hand-rolled `accounts_history` tables fed by triggers — MariaDB does it natively, including `pg_dump`-style point-in-time queries.
|
|
69
|
+
|
|
70
|
+
### JSON Schema column constraint
|
|
71
|
+
|
|
72
|
+
```sql
|
|
73
|
+
ALTER TABLE leads
|
|
74
|
+
MODIFY metadata JSON
|
|
75
|
+
CHECK (JSON_SCHEMA_VALID('{
|
|
76
|
+
"type": "object",
|
|
77
|
+
"required": ["source", "campaign"],
|
|
78
|
+
"properties": {
|
|
79
|
+
"source": { "type": "string" },
|
|
80
|
+
"campaign": { "type": "string" }
|
|
81
|
+
}
|
|
82
|
+
}', metadata));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Bad payloads now fail at INSERT time, not at the next time PHP tries to read them.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
10
89
|
## Why Octane Changes Everything
|
|
11
90
|
|
|
12
91
|
```
|
|
@@ -39,8 +118,9 @@ Problems in Octane:
|
|
|
39
118
|
'database' => env('DB_DATABASE'),
|
|
40
119
|
'username' => env('DB_USERNAME'),
|
|
41
120
|
'password' => env('DB_PASSWORD'),
|
|
42
|
-
'charset' => 'utf8mb4',
|
|
43
|
-
'collation' => '
|
|
121
|
+
'charset' => 'utf8mb4', // default in MariaDB 11.8 — keep explicit for older servers
|
|
122
|
+
'collation' => 'utf8mb4_uca1400_ai_ci', // 11.x — Unicode 14, accent-insensitive, case-insensitive
|
|
123
|
+
// (use utf8mb4_unicode_ci on MariaDB ≤ 10.11 / MySQL)
|
|
44
124
|
'prefix' => '',
|
|
45
125
|
'strict' => true, // ← MANDATORY
|
|
46
126
|
'engine' => 'InnoDB',
|
|
@@ -388,3 +468,15 @@ Route::get('/health', function () {
|
|
|
388
468
|
| `Lead::count()` on millions of rows | Cached count or approximate count |
|
|
389
469
|
| `'metadata' => 'json'` cast | `'metadata' => 'array'` (auto decode) |
|
|
390
470
|
| `datetime` cast | `immutable_datetime` (Octane-safe, no mutation) |
|
|
471
|
+
| Hand-rolled `*_history` tables fed by triggers | System-versioned (`WITH SYSTEM VERSIONING`) — MariaDB 11.x |
|
|
472
|
+
| Storing embeddings as `LONGTEXT` JSON | Native `VECTOR(N)` + `VEC_DISTANCE_*` (MariaDB 11.7+) |
|
|
473
|
+
| Validating JSON only in PHP | Add `JSON_SCHEMA_VALID` CHECK constraint on the column |
|
|
474
|
+
|
|
475
|
+
## See Also
|
|
476
|
+
|
|
477
|
+
- `_shared/skills/postgres-patterns` — analogous skill on the Postgres side (PG 18 features); MariaDB-specific equivalents documented here
|
|
478
|
+
- `_shared/skills/database-migrations` — universal parallel-change / backfill rules
|
|
479
|
+
- `laravel-octane` — worker safety rules this skill plugs into
|
|
480
|
+
- `php-patterns` — `readonly`, enums, immutable_datetime that pair with Eloquent casts
|
|
481
|
+
- `phpstan-analysis` (Larastan) — model PHPDoc generation makes Eloquent type-safe
|
|
482
|
+
- `api-design` — pagination shape that backend cursor pagination feeds
|
|
@@ -1,15 +1,108 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: php-patterns
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "Modern PHP idioms for Laravel apps targeting PHP 8.3 and 8.4 (Nov 2024). Covers property hooks (replace boilerplate getters/setters), asymmetric visibility (public read / private write), lazy objects via Reflection, typed class constants (8.3), readonly classes, enums, match, named args, null-safe, first-class callable syntax, constructor promotion, Octane-safe DI patterns. Invoke for any new PHP class, refactor of an old one, or when reviewing OOP code."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# PHP 8.3
|
|
7
|
+
# PHP 8.3 / 8.4 Patterns for Laravel
|
|
7
8
|
|
|
8
9
|
## Version Requirements
|
|
9
10
|
|
|
10
|
-
- **PHP
|
|
11
|
-
- **
|
|
12
|
-
-
|
|
11
|
+
- **PHP ≥ 8.3** — minimum supported (Laravel 12 supports 8.2–8.5; we target 8.3+).
|
|
12
|
+
- **PHP 8.4 strongly recommended** for new projects (released **Nov 21, 2024**) — adds property hooks + asymmetric visibility + lazy objects.
|
|
13
|
+
- **Composer ≥ 2.8** — see `composer-workflow`.
|
|
14
|
+
- `declare(strict_types=1);` in EVERY file. No exceptions.
|
|
15
|
+
|
|
16
|
+
## PHP 8.4 Highlights — adopt for new code
|
|
17
|
+
|
|
18
|
+
### Property Hooks — replace getter/setter boilerplate
|
|
19
|
+
|
|
20
|
+
The single most consequential 8.4 feature. Replaces hand-rolled `get`/`set` methods that exist solely to wrap a stored value.
|
|
21
|
+
|
|
22
|
+
```php
|
|
23
|
+
class User
|
|
24
|
+
{
|
|
25
|
+
public function __construct(
|
|
26
|
+
public string $firstName,
|
|
27
|
+
public string $lastName,
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
// Computed property — no method call, no boilerplate
|
|
31
|
+
public string $fullName {
|
|
32
|
+
get => trim("{$this->firstName} {$this->lastName}");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Validation on assignment
|
|
36
|
+
public string $email {
|
|
37
|
+
set (string $value) {
|
|
38
|
+
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
|
39
|
+
throw new \InvalidArgumentException("Invalid email: {$value}");
|
|
40
|
+
}
|
|
41
|
+
$this->email = strtolower($value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
$u = new User('John', 'Doe');
|
|
47
|
+
echo $u->fullName; // "John Doe" — read like a field
|
|
48
|
+
$u->email = 'JOHN@DOE.COM'; // validated + lowercased on assign
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use property hooks when the field has computation or validation. **Don't** wrap a plain value just because old code did.
|
|
52
|
+
|
|
53
|
+
### Asymmetric Visibility — read public, write protected
|
|
54
|
+
|
|
55
|
+
```php
|
|
56
|
+
final class Order
|
|
57
|
+
{
|
|
58
|
+
public function __construct(
|
|
59
|
+
public private(set) string $id, // readable everywhere; writable only inside Order
|
|
60
|
+
public protected(set) string $status, // writable in Order + subclasses
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
public function markPaid(): void { $this->status = 'paid'; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
$o = new Order('ord_123', 'pending');
|
|
67
|
+
echo $o->id; // OK
|
|
68
|
+
$o->status = 'paid'; // ERROR — only Order/subclasses can write
|
|
69
|
+
$o->markPaid(); // OK
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Replaces the `private $id; public function id() { return $this->id; }` pattern entirely.
|
|
73
|
+
|
|
74
|
+
### Lazy Objects — defer expensive init
|
|
75
|
+
|
|
76
|
+
```php
|
|
77
|
+
$reflector = new \ReflectionClass(ExpensiveService::class);
|
|
78
|
+
$service = $reflector->newLazyGhost(function (ExpensiveService $svc): void {
|
|
79
|
+
// Runs only on first real access — bootstraps DB conn, loads cache, etc.
|
|
80
|
+
$svc->__construct(/* ... */);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// $service behaves like ExpensiveService but does nothing until used:
|
|
84
|
+
$service->doSomething(); // <-- init runs here, exactly once
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Mostly relevant for ORM-style proxies / DI containers. Day-to-day app code rarely needs it.
|
|
88
|
+
|
|
89
|
+
### New JIT (IR Framework)
|
|
90
|
+
|
|
91
|
+
Enabled by default in PHP 8.4. No app code change needed; just verify your `opcache.ini` enables JIT in production:
|
|
92
|
+
|
|
93
|
+
```ini
|
|
94
|
+
opcache.jit_buffer_size=128M
|
|
95
|
+
opcache.jit=tracing
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Other 8.4 deltas worth knowing
|
|
99
|
+
|
|
100
|
+
- HTML5-aware `Dom\HTMLDocument` (replaces the legacy quirks-mode parser)
|
|
101
|
+
- `array_find()`, `array_find_key()`, `array_any()`, `array_all()` — finally
|
|
102
|
+
- Object API for `BCMath\Number` — chainable arbitrary-precision math
|
|
103
|
+
- 4-year support lifecycle (active + security)
|
|
104
|
+
|
|
105
|
+
## Modern PHP Features (USE THESE)
|
|
13
106
|
|
|
14
107
|
## Modern PHP Features (USE THESE)
|
|
15
108
|
|
|
@@ -1,73 +1,179 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: phpstan-analysis
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "PHPStan 2.x (released Nov 11, 2024) static analysis for PHP 8.3-8.5 / Laravel 12. Covers Level 10 (strict mixed even from inferred types — new in 2.0), the list<T> array type for sequential int-keyed arrays, baseline workflow for legacy code, generics + array shapes annotations, larastan extension for Laravel-specific rules (Eloquent magic, facades, container resolution), 50-70% memory reduction in 2.0, CI integration with --error-format. Invoke when configuring PHPStan, raising the level, fixing reported issues, or onboarding a legacy codebase."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# PHPStan Static Analysis
|
|
7
|
+
# PHPStan Static Analysis (2.x, Nov 2024)
|
|
8
|
+
|
|
9
|
+
**Invoke when configuring PHPStan, raising the level, fixing reports, or wiring it into CI.**
|
|
10
|
+
|
|
11
|
+
> PHPStan 2.0 (Nov 11, 2024) added **Level 10** and the **`list<T>`** type, dropped 50-70% memory usage, and tightened reference-parameter analysis. It's a transparent upgrade from 1.x — same config keys, stricter inferences. Update as part of any project refresh.
|
|
7
12
|
|
|
8
13
|
## Setup
|
|
9
14
|
|
|
10
15
|
```bash
|
|
11
|
-
composer require --dev phpstan/phpstan
|
|
16
|
+
composer require --dev phpstan/phpstan "^2.0"
|
|
17
|
+
|
|
18
|
+
# Laravel projects — add Larastan for framework awareness
|
|
19
|
+
composer require --dev larastan/larastan "^3.0"
|
|
12
20
|
```
|
|
13
21
|
|
|
14
|
-
## Configuration (phpstan.neon)
|
|
22
|
+
## Configuration (`phpstan.neon` / `phpstan.neon.dist`)
|
|
15
23
|
|
|
16
24
|
```neon
|
|
25
|
+
# Plain PHP project
|
|
17
26
|
parameters:
|
|
18
|
-
level:
|
|
27
|
+
level: 8 # Target. Raise toward 10.
|
|
19
28
|
paths:
|
|
20
29
|
- src
|
|
21
|
-
-
|
|
30
|
+
- tests
|
|
22
31
|
excludePaths:
|
|
23
32
|
- vendor
|
|
24
33
|
- storage
|
|
25
34
|
- bootstrap/cache
|
|
26
|
-
|
|
35
|
+
treatPhpDocTypesAsCertain: false # Pragmatic for first month at high level
|
|
36
|
+
reportUnmatchedIgnoredErrors: true
|
|
37
|
+
parallel:
|
|
38
|
+
maximumNumberOfProcesses: 4
|
|
39
|
+
|
|
40
|
+
# Laravel project (Larastan)
|
|
41
|
+
includes:
|
|
42
|
+
- vendor/larastan/larastan/extension.neon
|
|
43
|
+
parameters:
|
|
44
|
+
level: 8
|
|
45
|
+
paths:
|
|
46
|
+
- app
|
|
47
|
+
- bootstrap
|
|
48
|
+
- config
|
|
49
|
+
- database
|
|
50
|
+
- routes
|
|
51
|
+
excludePaths:
|
|
52
|
+
- storage
|
|
53
|
+
- bootstrap/cache
|
|
27
54
|
```
|
|
28
55
|
|
|
29
|
-
## Levels
|
|
56
|
+
## Levels — what each one adds
|
|
30
57
|
|
|
31
|
-
| Level |
|
|
32
|
-
|
|
33
|
-
| 0 |
|
|
34
|
-
| 1 | Possibly undefined variables,
|
|
35
|
-
| 2 | Unknown methods on all expressions |
|
|
58
|
+
| Level | Catches |
|
|
59
|
+
|---|---|
|
|
60
|
+
| 0 | Unknown classes / functions / methods on `$this` |
|
|
61
|
+
| 1 | Possibly undefined variables, undefined params on `$this`, unreachable code |
|
|
62
|
+
| 2 | Unknown methods on **all** expressions (not just `$this`) |
|
|
36
63
|
| 3 | Return types, property types |
|
|
37
|
-
| 4 | Dead code, always
|
|
38
|
-
| 5 | Argument types |
|
|
39
|
-
|
|
|
40
|
-
| 7 |
|
|
41
|
-
| 8 |
|
|
42
|
-
| 9 |
|
|
64
|
+
| 4 | Dead code, always-true/always-false branches |
|
|
65
|
+
| 5 | Argument types in function/method calls |
|
|
66
|
+
| 6 | Missing typehints (params + returns) |
|
|
67
|
+
| 7 | Partially-wrong union types |
|
|
68
|
+
| 8 | Calling methods / accessing props on nullable types |
|
|
69
|
+
| 9 | `mixed` is treated strictly when **explicitly typed** |
|
|
70
|
+
| **10 (NEW in 2.0)** | `mixed` strict even when **implicitly inferred** (any unknown type) |
|
|
71
|
+
|
|
72
|
+
**Recommended target:** Level 8 for new projects (achievable with Larastan + readonly DTOs); raise to 9 once stable; 10 only after eliminating all `mixed` from your own code.
|
|
73
|
+
|
|
74
|
+
## `list<T>` — the new sequential array type *(2.0)*
|
|
75
|
+
|
|
76
|
+
Use when you have a 0-indexed contiguous array. PHPStan can then narrow types far more precisely than with `array<int, T>`.
|
|
77
|
+
|
|
78
|
+
```php
|
|
79
|
+
/**
|
|
80
|
+
* @param list<int> $ids sequential 0,1,2,... no gaps
|
|
81
|
+
* @return list<User> same shape on the way out
|
|
82
|
+
*/
|
|
83
|
+
public function findManyById(array $ids): array
|
|
84
|
+
{
|
|
85
|
+
return User::whereIn('id', $ids)->get()->all();
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Other useful array types:
|
|
90
|
+
|
|
91
|
+
| PHPDoc | Meaning |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `array<int>` | Any-shape array of ints |
|
|
94
|
+
| `list<int>` | Sequential, 0-indexed, no gaps |
|
|
95
|
+
| `non-empty-array<string, int>` | At least one entry; string keys, int values |
|
|
96
|
+
| `non-empty-list<User>` | Sequential AND non-empty |
|
|
97
|
+
| `array{name: string, age: int}` | Array shape (object-like) |
|
|
98
|
+
| `array{name: string, age?: int}` | Optional key |
|
|
43
99
|
|
|
44
100
|
## Running
|
|
45
101
|
|
|
46
102
|
```bash
|
|
47
|
-
vendor/bin/phpstan analyse
|
|
48
|
-
vendor/bin/phpstan analyse --level=
|
|
49
|
-
vendor/bin/phpstan analyse --
|
|
103
|
+
vendor/bin/phpstan analyse # Use config
|
|
104
|
+
vendor/bin/phpstan analyse --level=8 src/ # Override level + path
|
|
105
|
+
vendor/bin/phpstan analyse --memory-limit=1G # Bigger projects (still ~50% less than 1.x)
|
|
106
|
+
vendor/bin/phpstan analyse --generate-baseline # Snapshot legacy errors → unblocks CI
|
|
107
|
+
vendor/bin/phpstan analyse --error-format=github # CI annotations on PRs
|
|
50
108
|
```
|
|
51
109
|
|
|
110
|
+
## Baseline workflow (legacy adoption)
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Day 1: ratchet to a level that's mostly clean, baseline the rest
|
|
114
|
+
vendor/bin/phpstan analyse --level=6 --generate-baseline
|
|
115
|
+
|
|
116
|
+
# Day 2+: every new file/edit cannot ADD baseline entries
|
|
117
|
+
# Periodically: chip away at the baseline (delete entries, fix code)
|
|
118
|
+
vendor/bin/phpstan analyse --level=6 # must pass
|
|
119
|
+
vendor/bin/phpstan analyse --level=6 --error-format=github
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`reportUnmatchedIgnoredErrors: true` ensures the baseline shrinks (or fails CI) instead of growing silently.
|
|
123
|
+
|
|
52
124
|
## Common Fixes
|
|
53
125
|
|
|
54
126
|
```php
|
|
55
|
-
// ❌
|
|
127
|
+
// ❌ Parameter $data has no type hint
|
|
56
128
|
function process($data) {}
|
|
57
129
|
|
|
58
|
-
// ✅
|
|
130
|
+
// ✅
|
|
59
131
|
function process(array $data): void {}
|
|
60
132
|
|
|
61
|
-
|
|
133
|
+
|
|
134
|
+
// ❌ Method returns mixed
|
|
62
135
|
function getData() { return $this->db->query(); }
|
|
63
136
|
|
|
64
|
-
// ✅
|
|
65
|
-
/** @return array<string, mixed
|
|
137
|
+
// ✅ — PHPDoc + native return type
|
|
138
|
+
/** @return list<array<string, mixed>> */
|
|
66
139
|
function getData(): array { return $this->db->query(); }
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
// ❌ "Cannot access property $name on User|null"
|
|
143
|
+
echo $request->user()->name;
|
|
144
|
+
|
|
145
|
+
// ✅ — assert or null-coalesce
|
|
146
|
+
echo $request->user()?->name ?? 'guest';
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
// ❌ "Property User::$email is never written, only read"
|
|
150
|
+
class User {
|
|
151
|
+
public function __construct(public readonly string $email) {}
|
|
152
|
+
// PHPStan now reports if you typo-shadow the property elsewhere
|
|
153
|
+
}
|
|
67
154
|
```
|
|
68
155
|
|
|
156
|
+
## CI integration
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
# .github/workflows/ci.yml
|
|
160
|
+
- name: PHPStan
|
|
161
|
+
run: vendor/bin/phpstan analyse --error-format=github --memory-limit=1G
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
GitHub annotations show inline diff comments on the PR — much higher signal than the textual report.
|
|
165
|
+
|
|
69
166
|
## Rules
|
|
70
167
|
|
|
71
|
-
1. **
|
|
72
|
-
2. **
|
|
73
|
-
3. **
|
|
168
|
+
1. **Target level 8 minimum** for new code; raise to 9/10 incrementally
|
|
169
|
+
2. **Larastan is mandatory** on Laravel projects (Eloquent magic methods, facades, container)
|
|
170
|
+
3. **Baseline only legacy code** — new files must be clean
|
|
171
|
+
4. **Run before every commit** — wired into the quality gate
|
|
172
|
+
5. **`reportUnmatchedIgnoredErrors: true`** — baseline shrinks, never grows silently
|
|
173
|
+
6. **No `@phpstan-ignore` without a comment** explaining why and a link to the issue if upstream
|
|
174
|
+
|
|
175
|
+
## See Also
|
|
176
|
+
|
|
177
|
+
- `quality-gate` — typecheck step ordering
|
|
178
|
+
- `composer-workflow` — `--memory-limit` and CI script wiring
|
|
179
|
+
- `phpunit-testing` — Pest 4 + PHPUnit 12 typing patterns that benefit from PHPStan
|