@pilllesss/yorn 1.0.182 → 1.0.183

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.

Potentially problematic release.


This version of @pilllesss/yorn might be problematic. Click here for more details.

Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/providers/data/.manifest.json +1 -1
  3. package/dist/skills/code-review/LICENSE +21 -0
  4. package/dist/skills/code-review/SKILL.md +233 -0
  5. package/dist/skills/code-review/assets/pr-review-template.md +137 -0
  6. package/dist/skills/code-review/assets/review-checklist.md +123 -0
  7. package/dist/skills/code-review/reference/angular.md +768 -0
  8. package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
  9. package/dist/skills/code-review/reference/c.md +890 -0
  10. package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
  11. package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
  12. package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
  13. package/dist/skills/code-review/reference/cpp.md +893 -0
  14. package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  15. package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  16. package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  17. package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  18. package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  19. package/dist/skills/code-review/reference/csharp.md +525 -0
  20. package/dist/skills/code-review/reference/css-less-sass.md +661 -0
  21. package/dist/skills/code-review/reference/dart.md +670 -0
  22. package/dist/skills/code-review/reference/django.md +985 -0
  23. package/dist/skills/code-review/reference/fastapi.md +580 -0
  24. package/dist/skills/code-review/reference/go.md +993 -0
  25. package/dist/skills/code-review/reference/java.md +409 -0
  26. package/dist/skills/code-review/reference/java8.md +586 -0
  27. package/dist/skills/code-review/reference/kotlin.md +1018 -0
  28. package/dist/skills/code-review/reference/nestjs.md +593 -0
  29. package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
  30. package/dist/skills/code-review/reference/php.md +684 -0
  31. package/dist/skills/code-review/reference/python.md +1073 -0
  32. package/dist/skills/code-review/reference/qt.md +757 -0
  33. package/dist/skills/code-review/reference/react.md +871 -0
  34. package/dist/skills/code-review/reference/ruby.md +964 -0
  35. package/dist/skills/code-review/reference/rust.md +846 -0
  36. package/dist/skills/code-review/reference/security-review-guide.md +494 -0
  37. package/dist/skills/code-review/reference/svelte.md +1064 -0
  38. package/dist/skills/code-review/reference/swift.md +936 -0
  39. package/dist/skills/code-review/reference/typescript.md +1016 -0
  40. package/dist/skills/code-review/reference/vue.md +924 -0
  41. package/dist/skills/code-review/reference/zig.md +440 -0
  42. package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
  43. package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
  44. package/dist/yorn.cjs +628 -628
  45. package/package.json +2 -2
@@ -0,0 +1,684 @@
1
+ # PHP Code Review Guide
2
+
3
+ > PHP 8.x code review guide covering the type system, modern language features, OOP modeling, PDO data access, security, error handling, Composer dependencies, performance, and testing.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Quick Review Checklist](#quick-review-checklist)
8
+ - [Type System & Modern PHP](#type-system--modern-php)
9
+ - [Object Modeling](#object-modeling)
10
+ - [Input, Output & Security](#input-output--security)
11
+ - [Database Access](#database-access)
12
+ - [Error Handling](#error-handling)
13
+ - [Composer & Dependencies](#composer--dependencies)
14
+ - [Performance & Resource Management](#performance--resource-management)
15
+ - [Testing & Static Analysis](#testing--static-analysis)
16
+ - [Review Checklist](#review-checklist)
17
+ - [References](#references)
18
+
19
+ ---
20
+
21
+ ## Quick Review Checklist
22
+
23
+ ### Must-check
24
+
25
+ - [ ] New files enable `declare(strict_types=1);`
26
+ - [ ] Public APIs have parameter, return, and property types
27
+ - [ ] User input is validated; output is escaped per context
28
+ - [ ] SQL uses parameterized queries or ORM binding
29
+ - [ ] Passwords use `password_hash()` / `password_verify()`
30
+ - [ ] File uploads validate MIME, size, extension, and storage path
31
+ - [ ] `composer.lock` is committed; dependency ranges are reasonable
32
+ - [ ] PHPUnit/Pest tests and PHPStan/Psalm static analysis are present
33
+
34
+ ### Common issues
35
+
36
+ - [ ] Loose comparison `==` / `!=` causing type-juggling vulnerabilities
37
+ - [ ] `md5()` / `sha1()` used to store passwords
38
+ - [ ] Concatenating SQL, HTML, shell commands, or file paths
39
+ - [ ] Using `@` to suppress errors
40
+ - [ ] `unserialize()` on untrusted data
41
+ - [ ] `$_GET` / `$_POST` / `$_FILES` flowing straight into business logic
42
+ - [ ] PHP 8.2+ dynamic properties trigger a deprecation; PHP 9 may turn it into an error
43
+
44
+ ---
45
+
46
+ ## Type System & Modern PHP
47
+
48
+ ### strict_types and explicit types
49
+
50
+ ```php
51
+ <?php
52
+
53
+ // ❌ weak boundary: passing "42" gets silently coerced
54
+ function findUser($id) {
55
+ return User::find($id);
56
+ }
57
+
58
+ // ✅ enable strict_types at the top of the file; type the public API
59
+ declare(strict_types=1);
60
+
61
+ function findUser(int $id): ?User
62
+ {
63
+ return User::find($id);
64
+ }
65
+ ```
66
+
67
+ Don't leave type checking entirely to runtime input validation. Type declarations express an internal contract; input validation expresses how much to trust the boundary. You need both.
68
+
69
+ ### Avoid loose comparisons
70
+
71
+ ```php
72
+ <?php
73
+
74
+ // ❌ strings like "0e12345" can be treated as 0 under loose comparison
75
+ if ($providedHash == $storedHash) {
76
+ grantAccess();
77
+ }
78
+
79
+ // ✅ strict comparison; use hash_equals() for secrets or tokens
80
+ if (hash_equals($storedHash, $providedHash)) {
81
+ grantAccess();
82
+ }
83
+
84
+ // ✅ match uses identity checks, so fewer type-juggling surprises than switch
85
+ $status = match ($code) {
86
+ 200 => 'ok',
87
+ 404 => 'not_found',
88
+ default => 'unknown',
89
+ };
90
+ ```
91
+
92
+ Pay attention to `==`, `!=`, and `in_array($x, $list)` (loose by default) in auth, payment, state machine, and permission logic. Use `===`, `!==`, and `in_array($x, $list, true)` where it matters.
93
+
94
+ ### Union / intersection / nullable types
95
+
96
+ ```php
97
+ <?php
98
+
99
+ // ❌ mixed or untyped makes callers guess the return shape
100
+ function loadConfig($source) {
101
+ return parseConfig($source);
102
+ }
103
+
104
+ // ✅ express the real contract with types
105
+ function loadConfig(string|PathInfo $source): Config
106
+ {
107
+ return parseConfig($source);
108
+ }
109
+
110
+ // ✅ make null explicit when it's a real business state
111
+ function currentUser(): ?User
112
+ {
113
+ return Auth::user();
114
+ }
115
+ ```
116
+
117
+ `mixed` can show up at the boundary or while migrating legacy code, but in core business services it usually signals missing modeling.
118
+
119
+ ### The nullsafe operator shouldn't hide missing state
120
+
121
+ ```php
122
+ <?php
123
+
124
+ // ❌ chained nullsafe blurs the reason for failure
125
+ $country = $order?->customer?->profile?->country;
126
+
127
+ // ✅ branch explicitly on critical business state
128
+ if ($order === null) {
129
+ throw new OrderNotFound();
130
+ }
131
+
132
+ $customer = $order->customer();
133
+ if ($customer === null) {
134
+ throw new MissingCustomer($order->id);
135
+ }
136
+
137
+ $country = $customer->profile()?->country;
138
+ ```
139
+
140
+ Distinguish "optional display field" from "business invariant that must exist." The former is a good fit for `?->`; the latter should fail loudly.
141
+
142
+ ---
143
+
144
+ ## Object Modeling
145
+
146
+ ### Use readonly properties and value objects
147
+
148
+ ```php
149
+ <?php
150
+
151
+ // ❌ public mutable fields let callers change state at will
152
+ class Money
153
+ {
154
+ public $amount;
155
+ public $currency;
156
+ }
157
+
158
+ // ✅ express an immutable value object with types and readonly
159
+ final readonly class Money
160
+ {
161
+ public function __construct(
162
+ public int $amount,
163
+ public string $currency,
164
+ ) {
165
+ if ($amount < 0) {
166
+ throw new InvalidArgumentException('Amount must be non-negative');
167
+ }
168
+ }
169
+ }
170
+ ```
171
+
172
+ For DTOs, config, and domain value objects, check first whether a `readonly class` or readonly properties can remove hidden side effects.
173
+
174
+ ### Enums instead of string states
175
+
176
+ ```php
177
+ <?php
178
+
179
+ // ❌ string states are easy to typo and can't enumerate the legal set
180
+ if ($order->status === 'paied') {
181
+ ship($order);
182
+ }
183
+
184
+ // ✅ an enum surfaces illegal states earlier
185
+ enum OrderStatus: string
186
+ {
187
+ case Pending = 'pending';
188
+ case Paid = 'paid';
189
+ case Cancelled = 'cancelled';
190
+ }
191
+
192
+ if ($order->status === OrderStatus::Paid) {
193
+ ship($order);
194
+ }
195
+ ```
196
+
197
+ When reviewing state machines, permissions, or type fields, look for "magic string values." If the value set is stable, suggest an enum; if it comes from an external system, convert it to an internal enum before it enters the business layer.
198
+
199
+ ### Don't rely on dynamic properties
200
+
201
+ ```php
202
+ <?php
203
+
204
+ // ❌ PHP 8.2+ triggers a deprecation when creating a dynamic property
205
+ $user = new User();
206
+ $user->emali = 'a@example.com'; // a typo also silently creates a property
207
+
208
+ // ✅ declare properties or use a dedicated data structure
209
+ final class User
210
+ {
211
+ public function __construct(
212
+ public string $email,
213
+ ) {}
214
+ }
215
+ ```
216
+
217
+ `#[AllowDynamicProperties]` should be an exception for legacy compatibility, not the default for new code. Watch for serialization, ORM hydration, and test doubles that secretly rely on dynamic properties.
218
+
219
+ ### Don't do heavy I/O in constructors
220
+
221
+ ```php
222
+ <?php
223
+
224
+ // ❌ quietly connecting to the DB on construction makes testing and error handling hard
225
+ final class ReportService
226
+ {
227
+ private PDO $pdo;
228
+
229
+ public function __construct()
230
+ {
231
+ $this->pdo = new PDO($_ENV['DSN']);
232
+ }
233
+ }
234
+
235
+ // ✅ inject dependencies from the outside
236
+ final class ReportService
237
+ {
238
+ public function __construct(
239
+ private PDO $pdo,
240
+ ) {}
241
+ }
242
+ ```
243
+
244
+ A constructor should establish the object's invariants — not send HTTP requests, open connections, read large files, or run complex queries.
245
+
246
+ ---
247
+
248
+ ## Input, Output & Security
249
+
250
+ ### Validate input at the boundary
251
+
252
+ ```php
253
+ <?php
254
+
255
+ // ❌ superglobals flow straight into business logic
256
+ $user = $service->create($_POST['email'], $_POST['age']);
257
+
258
+ // ✅ validate and coerce types at the boundary first
259
+ $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
260
+ $age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
261
+ 'options' => ['min_range' => 0, 'max_range' => 130],
262
+ ]);
263
+
264
+ if ($email === false || $email === null || $age === false || $age === null) {
265
+ throw new InvalidInput();
266
+ }
267
+
268
+ $user = $service->create($email, $age);
269
+ ```
270
+
271
+ `filter_input()` only handles a slice of basic validation. Complex rules, cross-field constraints, and business constraints still need a dedicated validator or request DTO.
272
+
273
+ ### Escape output per context
274
+
275
+ ```php
276
+ <?php
277
+
278
+ // ❌ user input goes straight into HTML
279
+ echo "<h1>Hello {$_GET['name']}</h1>";
280
+
281
+ // ✅ use htmlspecialchars in an HTML text context
282
+ $name = (string) ($_GET['name'] ?? '');
283
+ echo '<h1>Hello ' . htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</h1>';
284
+ ```
285
+
286
+ Different contexts need different escaping: HTML text, HTML attributes, URLs, JavaScript strings, and CSS are all different. When a template engine's default escaping is turned off, treat it as a security risk.
287
+
288
+ ### Passwords and randomness
289
+
290
+ ```php
291
+ <?php
292
+
293
+ // ❌ md5/sha1 must not be used for password storage
294
+ $hash = md5($password);
295
+
296
+ // ✅ use PHP's built-in password API
297
+ $hash = password_hash($password, PASSWORD_DEFAULT);
298
+
299
+ if (!password_verify($password, $hash)) {
300
+ throw new InvalidCredentials();
301
+ }
302
+
303
+ // ✅ use a CSPRNG for tokens
304
+ $token = bin2hex(random_bytes(32));
305
+ $code = random_int(100000, 999999);
306
+ ```
307
+
308
+ Don't hand-roll salts, round migration, or password comparison. Use `password_needs_rehash()` when you need to upgrade the cost factor.
309
+
310
+ ### Deserialization and object injection
311
+
312
+ ```php
313
+ <?php
314
+
315
+ // ❌ untrusted input into unserialize can trigger object injection
316
+ $payload = unserialize($_COOKIE['state']);
317
+
318
+ // ✅ prefer JSON for external data, and validate its schema/shape
319
+ $payload = json_decode($_COOKIE['state'] ?? '{}', true, flags: JSON_THROW_ON_ERROR);
320
+ ```
321
+
322
+ If you must process historical serialized data, at least restrict `allowed_classes` and make sure the relevant classes' magic methods can't produce dangerous side effects.
323
+
324
+ ### File uploads and paths
325
+
326
+ ```php
327
+ <?php
328
+
329
+ // ❌ building the path from the raw filename
330
+ $target = __DIR__ . '/uploads/' . $_FILES['avatar']['name'];
331
+ move_uploaded_file($_FILES['avatar']['tmp_name'], $target);
332
+
333
+ // ✅ generate a server-side filename, check the upload error and MIME
334
+ $file = $_FILES['avatar'];
335
+ if ($file['error'] !== UPLOAD_ERR_OK) {
336
+ throw new UploadFailed();
337
+ }
338
+
339
+ $finfo = new finfo(FILEINFO_MIME_TYPE);
340
+ $mime = $finfo->file($file['tmp_name']);
341
+ if (!in_array($mime, ['image/png', 'image/jpeg'], true)) {
342
+ throw new InvalidFileType();
343
+ }
344
+
345
+ $target = __DIR__ . '/uploads/' . bin2hex(random_bytes(16)) . '.jpg';
346
+ move_uploaded_file($file['tmp_name'], $target);
347
+ ```
348
+
349
+ When reviewing upload features, check size limits, MIME detection, extensions, a non-executable storage directory, path traversal, overwrite protection, and any virus-scan or async-processing requirements.
350
+
351
+ ---
352
+
353
+ ## Database Access
354
+
355
+ ### Use parameterized queries
356
+
357
+ PHP's PDO and mysqli both support prepared statements. Never concatenate user input into SQL strings. Dynamic identifiers (table/column names) must go through a whitelist mapping.
358
+
359
+ > **跨语言 SQL 注入防护详见 [SQL Injection Prevention Guide](cross-cutting/sql-injection-prevention.md)**,含 Python/Java/Go/Node.js/PHP/C# 示例及 ORM 不安全用法。
360
+
361
+ ### Wrap multi-step writes in transactions
362
+
363
+ ```php
364
+ <?php
365
+
366
+ // ❌ multi-step writes with no transaction leave half-finished state on failure
367
+ $orderId = $orders->create($cart);
368
+ $inventory->reserve($cart);
369
+ $payments->charge($orderId);
370
+
371
+ // ✅ explicit transaction boundary
372
+ $pdo->beginTransaction();
373
+ try {
374
+ $orderId = $orders->create($cart);
375
+ $inventory->reserve($cart);
376
+ $payments->recordIntent($orderId);
377
+ $pdo->commit();
378
+ } catch (Throwable $e) {
379
+ $pdo->rollBack();
380
+ throw $e;
381
+ }
382
+ ```
383
+
384
+ Don't casually put external, non-rollbackable side effects (an actual charge, an email, a message dispatch) inside a database transaction. Common patterns are an outbox, an idempotency key, or triggering after the transaction commits.
385
+
386
+ ### Avoid N+1 queries
387
+
388
+ > 📖 For cross-language N+1 patterns and solutions, see [N+1 Queries Guide](cross-cutting/n-plus-one-queries.md)
389
+
390
+ ```php
391
+ <?php
392
+
393
+ // ❌ querying inside a loop
394
+ foreach ($orders as $order) {
395
+ $customer = $customerRepo->find($order->customerId);
396
+ render($order, $customer);
397
+ }
398
+
399
+ // ✅ batch-load, then map
400
+ $customerIds = array_unique(array_map(fn ($o) => $o->customerId, $orders));
401
+ $customers = $customerRepo->findByIds($customerIds);
402
+
403
+ foreach ($orders as $order) {
404
+ render($order, $customers[$order->customerId] ?? null);
405
+ }
406
+ ```
407
+
408
+ In ORMs like Laravel/Doctrine, check eager loading, join fetch, selected columns, pagination, and indexes.
409
+
410
+ ---
411
+
412
+ ## Error Handling
413
+
414
+ > 📖 For cross-language error handling principles, see [Error Handling Guide](cross-cutting/error-handling-principles.md)
415
+
416
+ ### Catch specific exceptions, keep context
417
+
418
+ ```php
419
+ <?php
420
+
421
+ // ❌ swallowing the exception leaves callers unable to know it failed
422
+ try {
423
+ $mailer->send($message);
424
+ } catch (Exception $e) {
425
+ }
426
+
427
+ // ✅ catch a specific exception, keep context, and rethrow
428
+ try {
429
+ $mailer->send($message);
430
+ } catch (TransportException $e) {
431
+ throw new NotificationFailed($userId, previous: $e);
432
+ }
433
+ ```
434
+
435
+ Empty `catch` blocks, `error_log()`-and-continue without surfacing the error, and turning every exception into `RuntimeException('failed')` in production code all deserve a question.
436
+
437
+ ### Don't suppress errors with @
438
+
439
+ ```php
440
+ <?php
441
+
442
+ // ❌ hides the real error and makes debugging hard
443
+ $content = @file_get_contents($path);
444
+
445
+ // ✅ handle failure explicitly
446
+ $content = file_get_contents($path);
447
+ if ($content === false) {
448
+ throw new RuntimeException("Unable to read file: {$path}");
449
+ }
450
+ ```
451
+
452
+ `@` is common around file, network, array access, and legacy library calls. Push for an explicit branch, or convert third-party errors into project exceptions.
453
+
454
+ ### Don't leak sensitive data in logs
455
+
456
+ ```php
457
+ <?php
458
+
459
+ // ❌ writing tokens, passwords, or the full request body to the log
460
+ $logger->error('Login failed', ['request' => $_POST]);
461
+
462
+ // ✅ log non-sensitive context that still helps locate the problem
463
+ $logger->warning('Login failed', [
464
+ 'email_hash' => hash('sha256', strtolower($email)),
465
+ 'ip' => $requestIp,
466
+ ]);
467
+ ```
468
+
469
+ Check logs, exception messages, the debug toolbar, error pages, and failed-queue records. Sensitive data includes passwords, tokens, sessions, PII, payment data, and full cookies.
470
+
471
+ ---
472
+
473
+ ## Composer & Dependencies
474
+
475
+ ### Lock reproducible dependencies
476
+
477
+ ```json
478
+ {
479
+ "require": {
480
+ "php": "^8.2",
481
+ "monolog/monolog": "^3.0"
482
+ },
483
+ "require-dev": {
484
+ "phpunit/phpunit": "^11.0",
485
+ "phpstan/phpstan": "^1.10"
486
+ }
487
+ }
488
+ ```
489
+
490
+ When reviewing `composer.json` / `composer.lock`, watch for:
491
+
492
+ - Application repos commit `composer.lock`; library repos usually don't
493
+ - `require-dev` shouldn't make it into the production image
494
+ - The PHP platform version matches the CI version
495
+ - Autoload rules aren't too broad (don't load test or script directories)
496
+ - `scripts` commands don't depend on a developer's local secret config
497
+
498
+ ### Dependency security and maintenance
499
+
500
+ ```bash
501
+ composer audit
502
+ composer outdated --direct
503
+ composer validate --strict
504
+ ```
505
+
506
+ When adding a package, look at its maintenance status — download count isn't the only signal. What matters is its security history, release cadence, minimal dependency footprint, and whether it duplicates the standard library or a framework built-in.
507
+
508
+ ---
509
+
510
+ ## Performance & Resource Management
511
+
512
+ ### Stream large datasets with generators or pagination
513
+
514
+ ```php
515
+ <?php
516
+
517
+ // ❌ loading every record at once
518
+ $rows = $repo->all();
519
+ foreach ($rows as $row) {
520
+ exportRow($row);
521
+ }
522
+
523
+ // ✅ paginate or use a generator to avoid a memory spike
524
+ foreach ($repo->cursor() as $row) {
525
+ exportRow($row);
526
+ }
527
+ ```
528
+
529
+ A PHP request lifecycle is short, but CLI jobs, queue workers, and export tasks run for a long time. For that kind of code, watch memory growth, unclosed resources, and global-state pollution especially closely.
530
+
531
+ ### Avoid expensive work inside loops
532
+
533
+ ```php
534
+ <?php
535
+
536
+ // ❌ re-parsing config or opening a connection on every iteration
537
+ foreach ($items as $item) {
538
+ $client = new ApiClient($_ENV['API_KEY']);
539
+ $client->send($item);
540
+ }
541
+
542
+ // ✅ create reusable dependencies outside the loop
543
+ $client = new ApiClient($_ENV['API_KEY']);
544
+ foreach ($items as $item) {
545
+ $client->send($item);
546
+ }
547
+ ```
548
+
549
+ Watch for database queries, HTTP requests, regex compilation, large array copies, accumulating `array_merge()` appends, and repeatedly reading env vars or config files inside loops.
550
+
551
+ ### Release or scope resources
552
+
553
+ ```php
554
+ <?php
555
+
556
+ // ✅ close file handles after use
557
+ $handle = fopen($path, 'rb');
558
+ if ($handle === false) {
559
+ throw new RuntimeException('Unable to open file');
560
+ }
561
+
562
+ try {
563
+ while (($line = fgets($handle)) !== false) {
564
+ process($line);
565
+ }
566
+ } finally {
567
+ fclose($handle);
568
+ }
569
+ ```
570
+
571
+ PDO connections are usually managed by the container, but file handles, curl handles, temp files, locks, and cached objects in queue workers still need an explicit lifecycle.
572
+
573
+ ---
574
+
575
+ ## Testing & Static Analysis
576
+
577
+ ### Test behavior, not implementation details
578
+
579
+ ```php
580
+ <?php
581
+
582
+ // ❌ asserting an internal method call makes refactoring expensive
583
+ $mailer->expects($this->once())->method('buildTemplate');
584
+
585
+ // ✅ assert observable results
586
+ $service->sendWelcomeEmail($user);
587
+
588
+ $this->assertTrue($mailbox->hasMessageFor($user->email));
589
+ ```
590
+
591
+ For business services, controllers, and queue jobs, prefer covering observable behavior: inputs/outputs, database state, published events, and dispatched messages.
592
+
593
+ ### Static analysis and formatting
594
+
595
+ ```bash
596
+ vendor/bin/phpunit
597
+ vendor/bin/phpstan analyse
598
+ vendor/bin/psalm
599
+ vendor/bin/php-cs-fixer fix --dry-run --diff
600
+ vendor/bin/rector process --dry-run
601
+ ```
602
+
603
+ When reviewing a PR, check whether the new code lowers the PHPStan/Psalm level, leans heavily on baseline ignores, or uses `@phpstan-ignore-next-line` to paper over a real type problem.
604
+
605
+ ### Isolate test data
606
+
607
+ ```php
608
+ <?php
609
+
610
+ // ❌ the test depends on real time and external services
611
+ $service->expireOldSessions();
612
+
613
+ // ✅ inject a clock and a fake gateway
614
+ $clock->setNow(new DateTimeImmutable('2026-01-01T00:00:00Z'));
615
+ $service->expireOldSessions();
616
+ ```
617
+
618
+ Watch for database transaction rollback, fixture cleanup, randomness, time, queues, caches, and external APIs. Slow PHP tests are usually not a language problem — it's that the boundaries aren't isolated.
619
+
620
+ ---
621
+
622
+ ## Review Checklist
623
+
624
+ ### Types & modeling
625
+
626
+ - [ ] `declare(strict_types=1);` at the top of the file
627
+ - [ ] Parameters, return values, and properties have explicit types
628
+ - [ ] `===` / `!==` used; collection lookups use strict mode
629
+ - [ ] Stable state sets use an enum, not magic strings
630
+ - [ ] New code doesn't rely on dynamic properties
631
+ - [ ] Value objects are readonly or otherwise immutable
632
+
633
+ ### Security
634
+
635
+ - [ ] Input is validated and type-coerced at the boundary
636
+ - [ ] Output is escaped per HTML/URL/JS/CSS context
637
+ - [ ] SQL uses prepared statements or ORM binding
638
+ - [ ] Dynamic table/column/sort names go through a whitelist
639
+ - [ ] Passwords use `password_hash()` / `password_verify()`
640
+ - [ ] Tokens, codes, and filenames use `random_bytes()` / `random_int()`
641
+ - [ ] Untrusted input never reaches `unserialize()`
642
+ - [ ] File uploads check the error code, size, MIME, extension, and storage directory
643
+ - [ ] No injection or leakage risk in shell commands, path building, or log output
644
+
645
+ ### Data & transactions
646
+
647
+ - [ ] Multi-step writes have a transaction or compensation mechanism
648
+ - [ ] External side effects are designed to be idempotent
649
+ - [ ] N+1 queries avoided
650
+ - [ ] Pagination, indexes, and selected columns are reasonable
651
+ - [ ] Database errors aren't swallowed
652
+
653
+ ### Maintainability
654
+
655
+ - [ ] Constructors don't do heavy I/O
656
+ - [ ] Dependency injection is clear; no hidden global state
657
+ - [ ] No `@` error suppression
658
+ - [ ] Exceptions preserve context and `previous`
659
+ - [ ] Composer dependency ranges, autoload, and scripts are reasonable
660
+ - [ ] Application repos commit `composer.lock`
661
+
662
+ ### Testing & tooling
663
+
664
+ - [ ] PHPUnit/Pest cover the critical and failure paths
665
+ - [ ] PHPStan/Psalm config doesn't lower strictness
666
+ - [ ] New ignores/baselines are explained
667
+ - [ ] Formatting tools and CI commands are reproducible
668
+ - [ ] Tests isolate time, randomness, the database, queues, and external APIs
669
+
670
+ ---
671
+
672
+ ## References
673
+
674
+ - [PHP Manual: Type declarations](https://www.php.net/manual/en/language.types.declarations.php)
675
+ - [PHP Manual: match](https://www.php.net/match)
676
+ - [PHP Manual: Enumerations](https://www.php.net/manual/en/language.enumerations.overview.php)
677
+ - [PHP Manual: Properties](https://www.php.net/manual/en/language.oop5.properties.php)
678
+ - [PHP Manual: PDO](https://www.php.net/manual/en/class.pdo.php)
679
+ - [PHP Manual: password_hash](https://www.php.net/manual/en/function.password-hash.php)
680
+ - [PHP Manual: random_bytes](https://www.php.net/manual/en/function.random-bytes.php)
681
+ - [Composer documentation](https://getcomposer.org/doc/)
682
+ - [PHPUnit documentation](https://docs.phpunit.de/)
683
+ - [PHPStan documentation](https://phpstan.org/user-guide/getting-started)
684
+ - [Psalm documentation](https://psalm.dev/docs/)