@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.
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/project-detector.js +517 -0
- 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/package.json +1 -1
- 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-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,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
|