@sun-asterisk/sunlint 1.3.47 → 1.3.49
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/config/rules/rules-registry-generated.json +1717 -282
- package/core/architecture-integration.js +57 -15
- package/core/cli-action-handler.js +51 -36
- package/core/config-manager.js +6 -0
- package/core/config-merger.js +33 -0
- package/core/config-validator.js +37 -2
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/output-service.js +12 -3
- package/core/project-detector.js +517 -0
- package/core/scoring-service.js +12 -6
- package/core/summary-report-service.js +9 -4
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/engines/impact/cli.js +54 -39
- package/engines/impact/config/default-config.js +105 -5
- package/engines/impact/core/impact-analyzer.js +12 -15
- package/engines/impact/core/utils/gitignore-parser.js +123 -0
- package/engines/impact/core/utils/method-call-graph.js +272 -87
- package/origin-rules/dart-en.md +1 -1
- package/origin-rules/go-en.md +231 -0
- package/origin-rules/php-en.md +107 -0
- package/origin-rules/python-en.md +113 -0
- package/origin-rules/ruby-en.md +607 -0
- package/package.json +1 -1
- package/scripts/copy-arch-detect.js +5 -1
- package/scripts/copy-impact-analyzer.js +5 -1
- package/scripts/generate-rules-registry.js +30 -14
- package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
- package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Form Request Classes for Validation
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: keeps controllers thin, centralizes validation logic and authorization, enables reuse
|
|
5
|
+
tags: validation, form-request, controller, laravel, clean-architecture
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Form Request Classes for Validation
|
|
9
|
+
|
|
10
|
+
Putting `$request->validate()` directly in controller actions mixes concerns, prevents reuse, and inflates controllers. Form Requests encapsulate validation + authorization as a dedicated class.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
// Controller bloated with validation logic
|
|
16
|
+
public function store(Request $request)
|
|
17
|
+
{
|
|
18
|
+
$request->validate([
|
|
19
|
+
'name' => 'required|string|max:255',
|
|
20
|
+
'email' => 'required|email|unique:users',
|
|
21
|
+
'age' => 'required|integer|min:18',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
User::create($request->all());
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Correct:**
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Generate a Form Request
|
|
32
|
+
php artisan make:request StoreUserRequest
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```php
|
|
36
|
+
// app/Http/Requests/StoreUserRequest.php
|
|
37
|
+
class StoreUserRequest extends FormRequest
|
|
38
|
+
{
|
|
39
|
+
public function authorize(): bool
|
|
40
|
+
{
|
|
41
|
+
return $this->user()->can('create', User::class);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public function rules(): array
|
|
45
|
+
{
|
|
46
|
+
return [
|
|
47
|
+
'name' => 'required|string|max:255',
|
|
48
|
+
'email' => 'required|email|unique:users',
|
|
49
|
+
'age' => 'required|integer|min:18',
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Controller is now thin
|
|
55
|
+
public function store(StoreUserRequest $request)
|
|
56
|
+
{
|
|
57
|
+
User::create($request->validated()); // never $request->all()
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Key points:**
|
|
62
|
+
- Always use `$request->validated()` not `$request->all()` — only returns fields that passed validation
|
|
63
|
+
- Authorization logic belongs in `authorize()`, not the controller
|
|
64
|
+
- One Form Request per action (StoreUserRequest, UpdateUserRequest are separate)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Eager Load Relationships to Prevent N+1 Queries
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: prevents N+1 query explosion that causes severe performance degradation in production
|
|
5
|
+
tags: n+1, eager-loading, eloquent, performance, with, laravel
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Eager Load Relationships to Prevent N+1 Queries
|
|
9
|
+
|
|
10
|
+
Accessing a relationship inside a loop without eager loading fires one query per iteration. With 1000 posts, that is 1001 queries. Always eager load with `with()` when you know you'll access the relationship.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
// Executes 1 + N queries (one per post)
|
|
16
|
+
$posts = Post::all();
|
|
17
|
+
|
|
18
|
+
foreach ($posts as $post) {
|
|
19
|
+
echo $post->author->name; // <- SELECT * FROM users WHERE id = ? (×N)
|
|
20
|
+
echo $post->category->name; // <- SELECT * FROM categories WHERE id = ? (×N)
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Correct:**
|
|
25
|
+
|
|
26
|
+
```php
|
|
27
|
+
// Executes exactly 3 queries total
|
|
28
|
+
$posts = Post::with(['author', 'category'])->get();
|
|
29
|
+
|
|
30
|
+
foreach ($posts as $post) {
|
|
31
|
+
echo $post->author->name;
|
|
32
|
+
echo $post->category->name;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Nested relationships
|
|
36
|
+
$posts = Post::with(['author.profile', 'tags'])->get();
|
|
37
|
+
|
|
38
|
+
// Selective columns to reduce memory
|
|
39
|
+
$posts = Post::with(['author:id,name,avatar'])->get();
|
|
40
|
+
|
|
41
|
+
// Conditional eager loading
|
|
42
|
+
$posts = Post::with(['comments' => function ($q) {
|
|
43
|
+
$q->where('approved', true)->latest()->limit(5);
|
|
44
|
+
}])->get();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Detection signal:**
|
|
48
|
+
- Any `->relationship` access inside a `foreach`, `map`, `each`, or `Collection` method without preceding `with()`
|
|
49
|
+
- Use Laravel Telescope or Debugbar in development to detect N+1 — queries > 10 for a single request is a red flag
|
|
50
|
+
|
|
51
|
+
**Also applies to:**
|
|
52
|
+
```php
|
|
53
|
+
// WRONG: counting without withCount
|
|
54
|
+
$posts->each(fn($p) => $p->comments->count()); // N queries
|
|
55
|
+
|
|
56
|
+
// CORRECT: withCount
|
|
57
|
+
Post::withCount('comments')->get(); // 1 query
|
|
58
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use config() Helper, Never env() Outside Config Files
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: config caching (php artisan config:cache) breaks env() calls at runtime — app silently returns null
|
|
5
|
+
tags: config, env, caching, laravel, environment
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use config() Instead of env() Outside Config Files
|
|
9
|
+
|
|
10
|
+
`env()` only works reliably before config is cached. Once you run `php artisan config:cache` (required in production), `env()` returns `null` everywhere except in `config/*.php` files.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
// Service class — breaks after config:cache
|
|
16
|
+
class PaymentService
|
|
17
|
+
{
|
|
18
|
+
public function charge()
|
|
19
|
+
{
|
|
20
|
+
$key = env('STRIPE_SECRET'); // NULL in production!
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Controller
|
|
25
|
+
public function show()
|
|
26
|
+
{
|
|
27
|
+
$debug = env('APP_DEBUG'); // NULL after config:cache
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Correct:**
|
|
32
|
+
|
|
33
|
+
```php
|
|
34
|
+
// 1. Define in config/services.php
|
|
35
|
+
return [
|
|
36
|
+
'stripe' => [
|
|
37
|
+
'secret' => env('STRIPE_SECRET'), // env() ONLY here
|
|
38
|
+
],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// 2. Use config() everywhere else
|
|
42
|
+
class PaymentService
|
|
43
|
+
{
|
|
44
|
+
public function charge()
|
|
45
|
+
{
|
|
46
|
+
$key = config('services.stripe.secret'); // always works
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. With default fallback
|
|
51
|
+
$debug = config('app.debug', false);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Rule:** `env()` appears only in `config/` directory. Everywhere else → `config()`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Define $fillable — Never Use $guarded = []
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: disabling mass assignment protection allows attackers to write arbitrary model fields via crafted HTTP requests
|
|
5
|
+
tags: mass-assignment, fillable, guarded, security, eloquent, laravel
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Define $fillable — Never Use $guarded = []
|
|
9
|
+
|
|
10
|
+
Mass assignment attacks occur when a user passes unexpected fields (e.g. `is_admin=1`) that get written to the database. `$guarded = []` disables all protection.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
class User extends Model
|
|
16
|
+
{
|
|
17
|
+
protected $guarded = []; // All fields writable — DANGEROUS
|
|
18
|
+
|
|
19
|
+
// Or: no $fillable defined at all (same effect with $guarded=[])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Attacker sends: POST /users { "name": "Bob", "is_admin": 1 }
|
|
23
|
+
User::create($request->all()); // is_admin gets written!
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Correct:**
|
|
27
|
+
|
|
28
|
+
```php
|
|
29
|
+
class User extends Model
|
|
30
|
+
{
|
|
31
|
+
// Explicit allowlist
|
|
32
|
+
protected $fillable = [
|
|
33
|
+
'name',
|
|
34
|
+
'email',
|
|
35
|
+
'password',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Fields like is_admin, email_verified_at are NOT here
|
|
39
|
+
// They are set explicitly:
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Controller
|
|
43
|
+
User::create($request->validated()); // only validated+fillable fields written
|
|
44
|
+
|
|
45
|
+
// When you must set a guarded field, use direct assignment:
|
|
46
|
+
$user = new User($request->validated());
|
|
47
|
+
$user->is_admin = false; // explicit, intentional
|
|
48
|
+
$user->save();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Also:** use `$request->validated()` not `$request->all()` so only schema-declared fields reach the model.
|
package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Policies and Gates for Authorization
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: centralizes authorization logic, prevents scattered role checks, enables testing and auditing
|
|
5
|
+
tags: authorization, policy, gate, roles, security, laravel
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Policies and Gates for Authorization
|
|
9
|
+
|
|
10
|
+
Manual role checks scattered across controllers (`if ($user->role === 'admin')`) are fragile, hard to audit, and often inconsistently applied.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
// Controller — authorization logic throughout
|
|
16
|
+
public function update(Request $request, Post $post)
|
|
17
|
+
{
|
|
18
|
+
if ($request->user()->id !== $post->user_id) {
|
|
19
|
+
abort(403);
|
|
20
|
+
}
|
|
21
|
+
if ($request->user()->role !== 'editor' && $request->user()->role !== 'admin') {
|
|
22
|
+
abort(403); // duplicated in show, destroy, etc.
|
|
23
|
+
}
|
|
24
|
+
$post->update($request->validated());
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Correct:**
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
php artisan make:policy PostPolicy --model=Post
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```php
|
|
35
|
+
// app/Policies/PostPolicy.php
|
|
36
|
+
class PostPolicy
|
|
37
|
+
{
|
|
38
|
+
public function update(User $user, Post $post): bool
|
|
39
|
+
{
|
|
40
|
+
return $user->id === $post->user_id || $user->isEditor();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public function delete(User $user, Post $post): bool
|
|
44
|
+
{
|
|
45
|
+
return $user->id === $post->user_id || $user->isAdmin();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Controller — thin and readable
|
|
50
|
+
public function update(UpdatePostRequest $request, Post $post)
|
|
51
|
+
{
|
|
52
|
+
$this->authorize('update', $post); // throws 403 if denied
|
|
53
|
+
|
|
54
|
+
$post->update($request->validated());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Or in Form Request
|
|
58
|
+
public function authorize(): bool
|
|
59
|
+
{
|
|
60
|
+
return $this->user()->can('update', $this->route('post'));
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Gates for non-model actions:**
|
|
65
|
+
```php
|
|
66
|
+
// AppServiceProvider
|
|
67
|
+
Gate::define('access-reports', fn(User $u) => $u->hasRole('analyst'));
|
|
68
|
+
|
|
69
|
+
// Anywhere
|
|
70
|
+
if (Gate::denies('access-reports')) abort(403);
|
|
71
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Queue Heavy Tasks — Never Block the Request Cycle
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: synchronous email/external API calls cause request timeouts and degrade user experience under load
|
|
5
|
+
tags: queues, jobs, email, performance, background, laravel
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Queue Heavy Tasks — Never Block the Request Cycle
|
|
9
|
+
|
|
10
|
+
Any task over ~200ms (sending email, calling external APIs, generating reports, processing images) must be dispatched to a queue, not executed synchronously in the HTTP request.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
// Request handler blocked for seconds
|
|
16
|
+
public function register(StoreUserRequest $request)
|
|
17
|
+
{
|
|
18
|
+
$user = User::create($request->validated());
|
|
19
|
+
|
|
20
|
+
// Blocks for 1-3 seconds — user waits, timeout risk
|
|
21
|
+
Mail::to($user)->send(new WelcomeEmail($user));
|
|
22
|
+
|
|
23
|
+
// Calls Slack API — blocks if Slack is slow
|
|
24
|
+
Http::post(config('services.slack.webhook'), ['text' => "New user: {$user->email}"]);
|
|
25
|
+
|
|
26
|
+
return response()->json($user, 201);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Correct:**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
php artisan make:job SendWelcomeEmail
|
|
34
|
+
php artisan make:job NotifySlackNewUser
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```php
|
|
38
|
+
public function register(StoreUserRequest $request)
|
|
39
|
+
{
|
|
40
|
+
$user = User::create($request->validated());
|
|
41
|
+
|
|
42
|
+
// Dispatch to queue — returns immediately
|
|
43
|
+
SendWelcomeEmail::dispatch($user)->onQueue('emails');
|
|
44
|
+
NotifySlackNewUser::dispatch($user)->onQueue('notifications');
|
|
45
|
+
|
|
46
|
+
return response()->json($user, 201); // responds in <50ms
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Job class
|
|
50
|
+
class SendWelcomeEmail implements ShouldQueue
|
|
51
|
+
{
|
|
52
|
+
use Queueable, Dispatchable, InteractsWithQueue, SerializesModels;
|
|
53
|
+
|
|
54
|
+
public function __construct(private User $user) {}
|
|
55
|
+
|
|
56
|
+
public function handle(): void
|
|
57
|
+
{
|
|
58
|
+
Mail::to($this->user)->send(new WelcomeEmail($this->user));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public function failed(Throwable $e): void
|
|
62
|
+
{
|
|
63
|
+
Log::error('WelcomeEmail failed', ['user' => $this->user->id, 'error' => $e->getMessage()]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Queue-worthy operations:** email, SMS, push notifications, Slack/webhook, PDF generation, image resize, CSV export, expensive calculations, third-party API calls.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Hash::make() for Passwords — Never md5/sha1/bcrypt()
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: MD5/SHA1 are broken for passwords; Laravel's Hash facade uses bcrypt/argon2 with proper salting automatically
|
|
5
|
+
tags: password, hashing, bcrypt, security, hash, laravel
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Hash::make() for Passwords
|
|
9
|
+
|
|
10
|
+
Laravel's `Hash` facade automatically uses the configured driver (bcrypt by default, upgradeable to argon2id). Never use PHP's raw `password_hash()` directly without going through Laravel's config, and never use `md5()` or `sha1()`.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
// md5/sha1 — broken, no salting, rainbow-table vulnerable
|
|
16
|
+
$user->password = md5($request->password);
|
|
17
|
+
$user->password = sha1($request->password);
|
|
18
|
+
|
|
19
|
+
// Raw PHP without config — bypasses algorithm config
|
|
20
|
+
$user->password = password_hash($request->password, PASSWORD_BCRYPT);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Correct:**
|
|
24
|
+
|
|
25
|
+
```php
|
|
26
|
+
// Creating a user
|
|
27
|
+
$user = User::create([
|
|
28
|
+
'name' => $request->name,
|
|
29
|
+
'email' => $request->email,
|
|
30
|
+
'password' => Hash::make($request->password), // salted + bcrypt/argon2
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// Verifying on login — NEVER compare raw strings
|
|
34
|
+
if (!Hash::check($request->password, $user->password)) {
|
|
35
|
+
throw ValidationException::withMessages([
|
|
36
|
+
'email' => ['These credentials do not match our records.'],
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Rehash on login if algorithm changed
|
|
41
|
+
if (Hash::needsRehash($user->password)) {
|
|
42
|
+
$user->update(['password' => Hash::make($request->password)]);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Config:** Change algorithm in `config/hashing.php`:
|
|
47
|
+
```php
|
|
48
|
+
'driver' => 'argon2id', // upgrade from default bcrypt for new projects
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**In migrations:** password column must be `string(255)`, not `string(32)` — bcrypt hashes are 60 chars, argon2id up to 95.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Route Model Binding — No Manual find() in Controllers
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: eliminates repetitive find-or-404 boilerplate, centralizes authorization scope
|
|
5
|
+
tags: route-model-binding, controller, eloquent, laravel, routing
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Route Model Binding
|
|
9
|
+
|
|
10
|
+
Laravel resolves model instances automatically from route parameters. Writing `User::findOrFail($id)` manually in every controller method is redundant boilerplate.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
// routes/api.php
|
|
16
|
+
Route::get('/posts/{id}', [PostController::class, 'show']);
|
|
17
|
+
|
|
18
|
+
// Controller
|
|
19
|
+
public function show(int $id)
|
|
20
|
+
{
|
|
21
|
+
$post = Post::findOrFail($id); // unnecessary boilerplate
|
|
22
|
+
$this->authorize('view', $post);
|
|
23
|
+
return PostResource::make($post);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Correct:**
|
|
28
|
+
|
|
29
|
+
```php
|
|
30
|
+
// routes/api.php — use the model name as parameter
|
|
31
|
+
Route::get('/posts/{post}', [PostController::class, 'show']);
|
|
32
|
+
|
|
33
|
+
// Controller — Laravel resolves Post automatically, throws 404 if not found
|
|
34
|
+
public function show(Post $post)
|
|
35
|
+
{
|
|
36
|
+
$this->authorize('view', $post);
|
|
37
|
+
return PostResource::make($post);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Custom binding key** (e.g. slug instead of id):
|
|
42
|
+
|
|
43
|
+
```php
|
|
44
|
+
// Model
|
|
45
|
+
class Post extends Model
|
|
46
|
+
{
|
|
47
|
+
public function getRouteKeyName(): string
|
|
48
|
+
{
|
|
49
|
+
return 'slug'; // /posts/my-post-slug
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Scoped binding** (child must belong to parent):
|
|
55
|
+
|
|
56
|
+
```php
|
|
57
|
+
// /users/{user}/posts/{post} — post must belong to user
|
|
58
|
+
Route::get('/users/{user}/posts/{post}', [PostController::class, 'show'])
|
|
59
|
+
->scopeBindings();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**With eager loading:**
|
|
63
|
+
|
|
64
|
+
```php
|
|
65
|
+
// Customize resolution for eager-loading
|
|
66
|
+
Route::bind('post', fn($value) => Post::with(['author', 'tags'])->findOrFail($value));
|
|
67
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use API Resources for Response Transformation
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: prevents over-exposure of model fields, decouples DB schema from API contract, enables versioning
|
|
5
|
+
tags: api-resources, response, transformation, security, laravel
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use API Resources for Response Transformation
|
|
9
|
+
|
|
10
|
+
Returning `$model->toArray()` or `response()->json($model)` exposes all model attributes including sensitive fields (`password`, `remember_token`, internal flags). API Resources control exactly what is serialized.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
// Exposes ALL columns including password, remember_token, internal flags
|
|
16
|
+
return response()->json($user);
|
|
17
|
+
return response()->json(User::all());
|
|
18
|
+
return response()->json($user->toArray());
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Correct:**
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
php artisan make:resource UserResource
|
|
25
|
+
php artisan make:resource UserCollection
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```php
|
|
29
|
+
// app/Http/Resources/UserResource.php
|
|
30
|
+
class UserResource extends JsonResource
|
|
31
|
+
{
|
|
32
|
+
public function toArray(Request $request): array
|
|
33
|
+
{
|
|
34
|
+
return [
|
|
35
|
+
'id' => $this->id,
|
|
36
|
+
'name' => $this->name,
|
|
37
|
+
'email' => $this->email,
|
|
38
|
+
'avatar_url' => $this->avatar_url,
|
|
39
|
+
'created_at' => $this->created_at->toIso8601String(),
|
|
40
|
+
|
|
41
|
+
// Conditional fields
|
|
42
|
+
'phone' => $this->when(
|
|
43
|
+
$request->user()?->id === $this->id,
|
|
44
|
+
$this->phone
|
|
45
|
+
),
|
|
46
|
+
|
|
47
|
+
// Nested resource
|
|
48
|
+
'posts_count' => $this->whenCounted('posts'),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Controller
|
|
54
|
+
public function show(User $user): UserResource
|
|
55
|
+
{
|
|
56
|
+
return UserResource::make($user);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public function index(): JsonResponse
|
|
60
|
+
{
|
|
61
|
+
$users = User::with('profile')->paginate(20);
|
|
62
|
+
return UserResource::collection($users)->response();
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Password and sensitive fields must be in `$hidden`:**
|
|
67
|
+
```php
|
|
68
|
+
class User extends Model
|
|
69
|
+
{
|
|
70
|
+
protected $hidden = ['password', 'remember_token', 'two_factor_secret'];
|
|
71
|
+
}
|
|
72
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use chunk()/cursor() for Large Datasets — Never Model::all()
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Model::all() on a large table loads all rows into memory, causing OOM errors and PHP timeouts
|
|
5
|
+
tags: chunk, cursor, memory, performance, eloquent, laravel
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use chunk()/cursor() for Large Datasets
|
|
9
|
+
|
|
10
|
+
`Model::all()` or `->get()` with no limit fetches every row into PHP memory. On a 100k-row table this causes memory exhaustion.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
// Memory bomb — loads all rows into PHP array
|
|
16
|
+
$users = User::all();
|
|
17
|
+
foreach ($users as $user) {
|
|
18
|
+
$user->sendNewsletter();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Slightly better but still: loads 50k orders at once
|
|
22
|
+
$orders = Order::where('status', 'pending')->get();
|
|
23
|
+
foreach ($orders as $order) {
|
|
24
|
+
ProcessOrder::dispatch($order);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Correct:**
|
|
29
|
+
|
|
30
|
+
```php
|
|
31
|
+
// chunk: fetches 200 rows at a time, re-queries per batch
|
|
32
|
+
User::chunk(200, function ($users) {
|
|
33
|
+
foreach ($users as $user) {
|
|
34
|
+
$user->sendNewsletter();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// cursor: uses lazy collection (PHP generator), one row in memory at a time
|
|
39
|
+
// Best for read-only iteration without modifying records
|
|
40
|
+
foreach (User::cursor() as $user) {
|
|
41
|
+
$user->sendNewsletter();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// chunkById: safer than chunk for updates (avoids row-skip bug)
|
|
45
|
+
User::where('status', 'inactive')
|
|
46
|
+
->chunkById(500, function ($users) {
|
|
47
|
+
User::whereIn('id', $users->pluck('id'))->delete();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// For commands/jobs that process everything
|
|
51
|
+
User::select(['id', 'email', 'name']) // only needed columns
|
|
52
|
+
->where('newsletter', true)
|
|
53
|
+
->chunkById(1000, function ($batch) {
|
|
54
|
+
SendNewsletterJob::dispatch($batch->pluck('id')->all());
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Rule of thumb:** any query without `->limit()` or `->paginate()` in a loop or report command must use `chunk()` or `cursor()`.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Wrap Multi-Step Database Operations in Transactions
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: partial failures leave the database in an inconsistent state without transactions
|
|
5
|
+
tags: transactions, database, atomicity, eloquent, laravel
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Wrap Multi-Step Database Operations in Transactions
|
|
9
|
+
|
|
10
|
+
Any sequence of writes that must succeed or fail together (e.g. create order + deduct stock + charge payment) must run inside a `DB::transaction()`.
|
|
11
|
+
|
|
12
|
+
**Wrong:**
|
|
13
|
+
|
|
14
|
+
```php
|
|
15
|
+
// If deductStock() succeeds but createTransaction() fails,
|
|
16
|
+
// stock is deducted but no order exists — corrupted state
|
|
17
|
+
public function placeOrder(Cart $cart, User $user): Order
|
|
18
|
+
{
|
|
19
|
+
$order = Order::create([...]);
|
|
20
|
+
$this->deductStock($cart->items); // can throw
|
|
21
|
+
$this->createTransaction($order); // can throw — order exists, stock reduced
|
|
22
|
+
$this->clearCart($cart);
|
|
23
|
+
return $order;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Correct:**
|
|
28
|
+
|
|
29
|
+
```php
|
|
30
|
+
use Illuminate\Support\Facades\DB;
|
|
31
|
+
|
|
32
|
+
public function placeOrder(Cart $cart, User $user): Order
|
|
33
|
+
{
|
|
34
|
+
return DB::transaction(function () use ($cart, $user) {
|
|
35
|
+
$order = Order::create([
|
|
36
|
+
'user_id' => $user->id,
|
|
37
|
+
'total' => $cart->total(),
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
$this->deductStock($cart->items); // exception -> full rollback
|
|
41
|
+
$this->createTransaction($order); // exception -> full rollback
|
|
42
|
+
$this->clearCart($cart);
|
|
43
|
+
|
|
44
|
+
return $order;
|
|
45
|
+
});
|
|
46
|
+
// Any Throwable inside auto-rolls back and re-throws
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**With custom exception handling:**
|
|
51
|
+
|
|
52
|
+
```php
|
|
53
|
+
DB::transaction(function () {
|
|
54
|
+
// ...
|
|
55
|
+
}, attempts: 3); // retry on deadlock (3 attempts)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Manual control when needed:**
|
|
59
|
+
```php
|
|
60
|
+
DB::beginTransaction();
|
|
61
|
+
try {
|
|
62
|
+
// ...
|
|
63
|
+
DB::commit();
|
|
64
|
+
} catch (Throwable $e) {
|
|
65
|
+
DB::rollBack();
|
|
66
|
+
throw $e;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Note:** Dispatching jobs inside a transaction should use `afterCommit()`:
|
|
71
|
+
```php
|
|
72
|
+
ProcessOrder::dispatch($order)->afterCommit(); // only dispatches if transaction commits
|
|
73
|
+
```
|