@sun-asterisk/sunlint 1.3.48 → 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.
Files changed (152) hide show
  1. package/core/file-targeting-service.js +148 -15
  2. package/core/init-command.js +118 -70
  3. package/core/project-detector.js +517 -0
  4. package/core/tui-select.js +245 -0
  5. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
  6. package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
  7. package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
  8. package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
  9. package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
  10. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
  11. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
  12. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
  13. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
  14. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
  15. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
  16. package/package.json +1 -1
  17. package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
  18. package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
  19. package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
  20. package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
  21. package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
  22. package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
  23. package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
  24. package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
  25. package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
  26. package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
  27. package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
  28. package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
  29. package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
  30. package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
  31. package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
  32. package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
  33. package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
  34. package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
  35. package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
  36. package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
  37. package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
  38. package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
  39. package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
  40. package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
  41. package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
  42. package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
  43. package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
  44. package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
  45. package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
  46. package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
  47. package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
  48. package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
  49. package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
  50. package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
  51. package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
  52. package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
  53. package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
  54. package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
  55. package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
  56. package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
  57. package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
  58. package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
  59. package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
  60. package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
  61. package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
  62. package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
  63. package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
  64. package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
  65. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
  66. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
  67. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
  68. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
  69. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
  70. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
  71. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
  72. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
  73. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
  74. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
  75. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
  76. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
  77. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
  78. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
  79. package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
  80. package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
  81. package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
  82. package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
  83. package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
  84. package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
  85. package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
  86. package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
  87. package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
  88. package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
  89. package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
  90. package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
  91. package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
  92. package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
  93. package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
  94. package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
  95. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
  96. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
  97. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
  98. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
  99. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
  100. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
  101. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
  102. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
  103. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
  104. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
  105. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
  106. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
  107. package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
  108. package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
  109. package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
  110. package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
  111. package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
  112. package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
  113. package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
  114. package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
  115. package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
  116. package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
  117. package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
  118. package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
  119. package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
  120. package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
  121. package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
  122. package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
  123. package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
  124. package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
  125. package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
  126. package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
  127. package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
  128. package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
  129. package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
  130. package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
  131. package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
  132. package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
  133. package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
  134. package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
  135. package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
  136. package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
  137. package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
  138. package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
  139. package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
  140. package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
  141. package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
  142. package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
  143. package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
  144. package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
  145. package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
  146. package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
  147. package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
  148. package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
  149. package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
  150. package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
  151. package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
  152. package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
@@ -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
+ ```
@@ -0,0 +1,78 @@
1
+ ---
2
+ title: Use Service Layer — Controllers Must Not Contain Business Logic
3
+ impact: HIGH
4
+ impactDescription: fat controllers are untestable, unmaintainable, and couple HTTP concerns with domain logic
5
+ tags: service-layer, architecture, clean-code, controller, laravel
6
+ ---
7
+
8
+ ## Use Service Layer — Controllers Must Not Contain Business Logic
9
+
10
+ Controllers handle HTTP: parse request, call service, return response. Domain logic (calculations, business rules, orchestration) belongs in a dedicated Service class.
11
+
12
+ **Wrong:**
13
+
14
+ ```php
15
+ // OrderController doing everything — HTTP + business + DB
16
+ public function store(StoreOrderRequest $request)
17
+ {
18
+ $subtotal = 0;
19
+ foreach ($request->items as $item) {
20
+ $product = Product::find($item['product_id']);
21
+ if ($product->stock < $item['qty']) {
22
+ return response()->json(['error' => 'Insufficient stock'], 422);
23
+ }
24
+ $subtotal += $product->price * $item['qty'];
25
+ }
26
+
27
+ $discount = $request->user()->isPremium() ? $subtotal * 0.1 : 0;
28
+ $total = $subtotal - $discount + $this->calculateShipping($request->address);
29
+
30
+ $order = Order::create([...]);
31
+ foreach ($request->items as $item) { /* create OrderItem, deduct stock */ }
32
+
33
+ Mail::to($request->user())->send(new OrderConfirmation($order));
34
+ return OrderResource::make($order);
35
+ }
36
+ ```
37
+
38
+ **Correct:**
39
+
40
+ ```php
41
+ // Service contains all business logic
42
+ class OrderService
43
+ {
44
+ public function __construct(
45
+ private readonly InventoryService $inventory,
46
+ private readonly PricingService $pricing,
47
+ ) {}
48
+
49
+ public function placeOrder(User $user, array $items, Address $address): Order
50
+ {
51
+ $this->inventory->reserveItems($items); // throws on insufficient stock
52
+ $total = $this->pricing->calculate($items, $user, $address);
53
+
54
+ return DB::transaction(function () use ($user, $items, $total) {
55
+ $order = Order::create(['user_id' => $user->id, 'total' => $total]);
56
+ $this->inventory->commitReservation($order, $items);
57
+ SendOrderConfirmation::dispatch($order);
58
+ return $order;
59
+ });
60
+ }
61
+ }
62
+
63
+ // Controller is just HTTP glue
64
+ class OrderController
65
+ {
66
+ public function __construct(private readonly OrderService $orderService) {}
67
+
68
+ public function store(StoreOrderRequest $request): JsonResponse
69
+ {
70
+ $order = $this->orderService->placeOrder(
71
+ $request->user(),
72
+ $request->validated('items'),
73
+ Address::fromRequest($request),
74
+ );
75
+ return OrderResource::make($order)->response()->setStatusCode(201);
76
+ }
77
+ }
78
+ ```
@@ -0,0 +1,75 @@
1
+ ---
2
+ title: Use RefreshDatabase + Factories in Tests — No Manual Inserts
3
+ impact: MEDIUM
4
+ impactDescription: manual DB inserts create brittle tests tightly coupled to schema; factories stay in sync with model changes
5
+ tags: testing, factories, refresh-database, pest, phpunit, laravel
6
+ ---
7
+
8
+ ## Use RefreshDatabase + Factories in Tests
9
+
10
+ Hardcoded `DB::table()->insert()` or `User::create([...])` with raw data creates brittle tests. Use model factories with `RefreshDatabase` for isolated, maintainable tests.
11
+
12
+ **Wrong:**
13
+
14
+ ```php
15
+ class OrderTest extends TestCase
16
+ {
17
+ protected function setUp(): void
18
+ {
19
+ parent::setUp();
20
+ DB::table('users')->insert(['id' => 1, 'name' => 'Test', 'email' => 'test@test.com', 'password' => 'xxx']);
21
+ DB::table('products')->insert(['id' => 1, 'name' => 'Widget', 'price' => 100, 'stock' => 10]);
22
+ // Test fails when schema changes
23
+ }
24
+
25
+ public function test_can_place_order()
26
+ {
27
+ $response = $this->post('/orders', ['product_id' => 1, 'qty' => 2]);
28
+ $this->assertEquals(200, $response->getStatusCode());
29
+ }
30
+ }
31
+ ```
32
+
33
+ **Correct (PHPUnit):**
34
+
35
+ ```php
36
+ class OrderTest extends TestCase
37
+ {
38
+ use RefreshDatabase; // wraps each test in a transaction, rolled back after
39
+
40
+ public function test_authenticated_user_can_place_order(): void
41
+ {
42
+ $user = User::factory()->create();
43
+ $product = Product::factory()->create(['price' => 100, 'stock' => 10]);
44
+
45
+ $response = $this->actingAs($user)
46
+ ->postJson('/api/orders', ['product_id' => $product->id, 'qty' => 2])
47
+ ->assertCreated()
48
+ ->assertJsonPath('data.total', 200);
49
+
50
+ $this->assertDatabaseHas('orders', ['user_id' => $user->id, 'total' => 200]);
51
+ $this->assertDatabaseHas('products', ['id' => $product->id, 'stock' => 8]);
52
+ }
53
+
54
+ public function test_order_fails_when_insufficient_stock(): void
55
+ {
56
+ $user = User::factory()->create();
57
+ $product = Product::factory()->create(['stock' => 1]);
58
+
59
+ $this->actingAs($user)
60
+ ->postJson('/api/orders', ['product_id' => $product->id, 'qty' => 5])
61
+ ->assertUnprocessable()
62
+ ->assertJsonValidationErrors(['qty']);
63
+ }
64
+ }
65
+ ```
66
+
67
+ **Factory patterns:**
68
+ ```php
69
+ // Specific state
70
+ $admin = User::factory()->admin()->create();
71
+ $banned = User::factory()->banned()->unverified()->create();
72
+
73
+ // With relations
74
+ $post = Post::factory()->for($user)->hasComments(3)->create();
75
+ ```
@@ -0,0 +1,61 @@
1
+ ---
2
+ title: Register Services in AppServiceProvider — Never new() in Controllers
3
+ impact: MEDIUM
4
+ impactDescription: direct instantiation prevents mocking in tests and creates hidden dependencies
5
+ tags: service-container, dependency-injection, ioc, laravel, testing
6
+ ---
7
+
8
+ ## Register Services via Service Container — Never new() in Controllers
9
+
10
+ Laravel's IoC container resolves dependencies automatically via constructor injection. Using `new MyService()` inside a controller or service bypasses the container, making tests hard and coupling impossible to break.
11
+
12
+ **Wrong:**
13
+
14
+ ```php
15
+ class OrderController extends Controller
16
+ {
17
+ public function store(StoreOrderRequest $request)
18
+ {
19
+ // Hidden dependencies, impossible to mock
20
+ $service = new OrderService(new InventoryService(), new PricingService());
21
+ $mailer = new OrderMailer(new SmtpTransport());
22
+ }
23
+ }
24
+ ```
25
+
26
+ **Correct:**
27
+
28
+ ```php
29
+ // 1. Bind in AppServiceProvider if needed (often not needed — auto-resolved)
30
+ class AppServiceProvider extends ServiceProvider
31
+ {
32
+ public function register(): void
33
+ {
34
+ $this->app->singleton(PaymentGateway::class, function () {
35
+ return new StripeGateway(config('services.stripe.secret'));
36
+ });
37
+
38
+ // Interface → concrete binding
39
+ $this->app->bind(NotifierInterface::class, SlackNotifier::class);
40
+ }
41
+ }
42
+
43
+ // 2. Controller receives via constructor — auto-resolved by container
44
+ class OrderController extends Controller
45
+ {
46
+ public function __construct(
47
+ private readonly OrderService $orderService,
48
+ private readonly NotifierInterface $notifier,
49
+ ) {}
50
+
51
+ public function store(StoreOrderRequest $request): JsonResponse
52
+ {
53
+ $order = $this->orderService->placeOrder($request->user(), $request->validated());
54
+ $this->notifier->notify("New order #{$order->id}");
55
+ return OrderResource::make($order)->response()->setStatusCode(201);
56
+ }
57
+ }
58
+
59
+ // 3. In tests — swap the binding
60
+ $this->app->instance(NotifierInterface::class, $mockNotifier);
61
+ ```
@@ -0,0 +1,55 @@
1
+ ---
2
+ title: Do Not Use Mutable Default Argument
3
+ impact: HIGH
4
+ impactDescription: Mutable default arguments are shared across all calls, causing subtle state bugs that are very hard to trace.
5
+ tags: python, bugs, arguments, pitfalls, quality
6
+ ---
7
+
8
+ ## Do Not Use Mutable Default Argument
9
+
10
+ In Python, default argument values are evaluated **once** at function definition time, not at each call. Using mutable objects (list, dict, set) as defaults causes all callers to share the same object, leading to unexpected side effects across calls.
11
+
12
+ **Incorrect:**
13
+ ```python
14
+ def add_item(item, collection=[]):
15
+ collection.append(item)
16
+ return collection
17
+
18
+ print(add_item("a")) # ["a"]
19
+ print(add_item("b")) # ["a", "b"] ← bug: list persists between calls
20
+
21
+ def create_user(name, roles={"admin": False}):
22
+ roles["user"] = True
23
+ return {"name": name, "roles": roles}
24
+ ```
25
+
26
+ **Correct:**
27
+ ```python
28
+ def add_item(item, collection=None):
29
+ if collection is None:
30
+ collection = []
31
+ collection.append(item)
32
+ return collection
33
+
34
+ print(add_item("a")) # ["a"]
35
+ print(add_item("b")) # ["b"] ← each call gets a fresh list
36
+
37
+ def create_user(name, roles=None):
38
+ if roles is None:
39
+ roles = {"admin": False}
40
+ roles["user"] = True
41
+ return {"name": name, "roles": roles}
42
+ ```
43
+
44
+ **Dataclass alternative (Python 3.7+):**
45
+ ```python
46
+ from dataclasses import dataclass, field
47
+ from typing import List
48
+
49
+ @dataclass
50
+ class Task:
51
+ name: str
52
+ tags: List[str] = field(default_factory=list) # safe: fresh list per instance
53
+ ```
54
+
55
+ **Tools:** Ruff `B006` (mutable-argument-default), Pylint `W0102` (dangerous-default-value), flake8-bugbear, mypy