@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,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Never Hardcode Cryptographic Keys or Initialization Vectors
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: hardcoded crypto keys or IVs give attackers permanent access to all encrypted data — compromised keys cannot be rotated without code changes
|
|
5
|
+
tags: security, cryptography, java, secrets
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Never Hardcode Cryptographic Keys or Initialization Vectors
|
|
9
|
+
|
|
10
|
+
Hardcoding cryptographic keys, passwords, secrets, or initialization vectors (IVs) directly in source code is a critical security vulnerability (OWASP A02: Cryptographic Failures). Once the code is in source control or compiled into a binary, the key is permanently exposed. Attackers who access the binary, source code, or logs can decrypt all data protected by that key.
|
|
11
|
+
|
|
12
|
+
**Incorrect (hardcoded key/IV):**
|
|
13
|
+
|
|
14
|
+
```java
|
|
15
|
+
import javax.crypto.SecretKey;
|
|
16
|
+
import javax.crypto.spec.SecretKeySpec;
|
|
17
|
+
import javax.crypto.spec.IvParameterSpec;
|
|
18
|
+
|
|
19
|
+
public class EncryptionService {
|
|
20
|
+
// CRITICAL VULNERABILITY: key is hardcoded in source code
|
|
21
|
+
private static final byte[] SECRET_KEY = "MySuperSecretKey".getBytes(); // 16 bytes = AES-128
|
|
22
|
+
private static final byte[] IV = "InitVector123456".getBytes(); // hardcoded IV
|
|
23
|
+
|
|
24
|
+
public byte[] encrypt(byte[] data) throws Exception {
|
|
25
|
+
SecretKey key = new SecretKeySpec(SECRET_KEY, "AES");
|
|
26
|
+
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
|
27
|
+
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
28
|
+
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
|
|
29
|
+
return cipher.doFinal(data);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Also bad: hardcoded passwords for key derivation
|
|
34
|
+
public class KeyDerivation {
|
|
35
|
+
private static final String PASSWORD = "hardcoded_password_123"; // exposed in binary
|
|
36
|
+
private static final byte[] SALT = "fixed_salt".getBytes(); // static salt defeats PBKDF2
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Correct (keys from secure storage):**
|
|
41
|
+
|
|
42
|
+
```java
|
|
43
|
+
import javax.crypto.*;
|
|
44
|
+
import javax.crypto.spec.*;
|
|
45
|
+
import java.security.SecureRandom;
|
|
46
|
+
|
|
47
|
+
public class EncryptionService {
|
|
48
|
+
private final SecretKey key;
|
|
49
|
+
|
|
50
|
+
// Inject key via constructor — loaded from secure storage, not hardcoded
|
|
51
|
+
public EncryptionService(SecretKey key) {
|
|
52
|
+
this.key = key;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public EncryptedData encrypt(byte[] data) throws Exception {
|
|
56
|
+
// Always generate a random IV per encryption operation
|
|
57
|
+
byte[] iv = new byte[16];
|
|
58
|
+
new SecureRandom().nextBytes(iv); // cryptographically random
|
|
59
|
+
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
|
60
|
+
|
|
61
|
+
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
62
|
+
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
|
|
63
|
+
byte[] ciphertext = cipher.doFinal(data);
|
|
64
|
+
|
|
65
|
+
return new EncryptedData(iv, ciphertext); // store IV alongside ciphertext
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Key loading from environment or secrets manager
|
|
70
|
+
public class KeyLoader {
|
|
71
|
+
public SecretKey loadKeyFromEnv() {
|
|
72
|
+
String base64Key = System.getenv("AES_SECRET_KEY"); // from environment
|
|
73
|
+
if (base64Key == null) {
|
|
74
|
+
throw new IllegalStateException("AES_SECRET_KEY environment variable not set");
|
|
75
|
+
}
|
|
76
|
+
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
|
|
77
|
+
return new SecretKeySpec(keyBytes, "AES");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Or use Java KeyStore (JKS/PKCS12):
|
|
81
|
+
public SecretKey loadKeyFromKeystore(String keystorePath, char[] storePassword,
|
|
82
|
+
String alias, char[] keyPassword) throws Exception {
|
|
83
|
+
KeyStore ks = KeyStore.getInstance("PKCS12");
|
|
84
|
+
try (InputStream fis = new FileInputStream(keystorePath)) {
|
|
85
|
+
ks.load(fis, storePassword);
|
|
86
|
+
}
|
|
87
|
+
return (SecretKey) ks.getKey(alias, keyPassword);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Secure key management options:**
|
|
93
|
+
|
|
94
|
+
| Method | Description |
|
|
95
|
+
|--------|-------------|
|
|
96
|
+
| Environment variables | Simple, good for containers (`AES_SECRET_KEY`) |
|
|
97
|
+
| Java KeyStore (JKS/PKCS12) | Standard Java key storage |
|
|
98
|
+
| AWS Secrets Manager / KMS | Cloud-managed, auditable |
|
|
99
|
+
| HashiCorp Vault | Enterprise secret management |
|
|
100
|
+
| Google Cloud Secret Manager | GCP-native secrets |
|
|
101
|
+
|
|
102
|
+
**Additional rules:**
|
|
103
|
+
- Always use a **random IV** per encryption — never a fixed IV.
|
|
104
|
+
- IVs need not be secret but must be unique per encryption.
|
|
105
|
+
- Use AES-GCM instead of AES-CBC where possible (provides authentication).
|
|
106
|
+
- Key length: AES-128 minimum, AES-256 recommended.
|
|
107
|
+
|
|
108
|
+
**Tools:** PMD (`HardCodedCryptoKey`, `InsecureCryptoIv`), SonarQube (`S2068`, `S3329`), SpotBugs (`HardCodedKey`)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Optional Instead of Returning null for Absent Values
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: returning null from methods forces callers to guess whether null is possible, leading to unchecked NullPointerExceptions
|
|
5
|
+
tags: null-safety, design, java, clean-code
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Optional Instead of Returning null for Absent Values
|
|
9
|
+
|
|
10
|
+
`null` is the most common source of `NullPointerException` in Java. When a method may or may not return a value (as opposed to returning a collection), use `java.util.Optional<T>` to make the "absence" explicit in the API contract. `Optional` forces callers to consciously handle the absent case.
|
|
11
|
+
|
|
12
|
+
This is described in *Effective Java* Item 55: *"Return Optionals Judiciously."*
|
|
13
|
+
|
|
14
|
+
**Incorrect (returning null):**
|
|
15
|
+
|
|
16
|
+
```java
|
|
17
|
+
// Caller cannot tell from the signature whether null is possible
|
|
18
|
+
public User findUserByEmail(String email) {
|
|
19
|
+
return userRepository.findByEmail(email); // may return null — not obvious
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public String getPreferredLanguage(Long userId) {
|
|
23
|
+
UserPreferences prefs = preferencesRepo.findByUser(userId);
|
|
24
|
+
if (prefs == null) return null; // unclear API contract
|
|
25
|
+
return prefs.getLanguage();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Callers must remember to null-check:
|
|
29
|
+
User user = userService.findUserByEmail("a@b.com");
|
|
30
|
+
user.getName(); // NPE if forgotten
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Correct (using Optional):**
|
|
34
|
+
|
|
35
|
+
```java
|
|
36
|
+
import java.util.Optional;
|
|
37
|
+
|
|
38
|
+
// Method signature clearly states: "this might not exist"
|
|
39
|
+
public Optional<User> findUserByEmail(String email) {
|
|
40
|
+
User user = userRepository.findByEmail(email);
|
|
41
|
+
return Optional.ofNullable(user); // wraps null into empty Optional
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public Optional<String> getPreferredLanguage(Long userId) {
|
|
45
|
+
return Optional.ofNullable(preferencesRepo.findByUser(userId))
|
|
46
|
+
.map(UserPreferences::getLanguage);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Handling Optional at call sites:**
|
|
51
|
+
|
|
52
|
+
```java
|
|
53
|
+
// Option 1: provide a default value
|
|
54
|
+
String name = userService.findUserByEmail("a@b.com")
|
|
55
|
+
.map(User::getName)
|
|
56
|
+
.orElse("Guest");
|
|
57
|
+
|
|
58
|
+
// Option 2: throw a meaningful exception if absent
|
|
59
|
+
User user = userService.findUserByEmail("a@b.com")
|
|
60
|
+
.orElseThrow(() -> new UserNotFoundException("User not found: " + email));
|
|
61
|
+
|
|
62
|
+
// Option 3: only proceed if present
|
|
63
|
+
userService.findUserByEmail("a@b.com")
|
|
64
|
+
.ifPresent(user -> sendWelcomeEmail(user));
|
|
65
|
+
|
|
66
|
+
// Option 4: ifPresentOrElse (Java 9+)
|
|
67
|
+
userService.findUserByEmail("a@b.com")
|
|
68
|
+
.ifPresentOrElse(
|
|
69
|
+
user -> logger.info("Found user: {}", user.getId()),
|
|
70
|
+
() -> logger.info("User not found")
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Option 5: transform and fall back
|
|
74
|
+
String language = getPreferredLanguage(userId)
|
|
75
|
+
.filter(lang -> supportedLanguages.contains(lang))
|
|
76
|
+
.orElse("en");
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**When NOT to use Optional:**
|
|
80
|
+
|
|
81
|
+
```java
|
|
82
|
+
// 1. Do NOT use Optional for fields — use null or default values
|
|
83
|
+
public class User {
|
|
84
|
+
private Optional<String> middleName; // bad: not serializable, adds overhead
|
|
85
|
+
private String middleName; // good: use null or ""
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Do NOT use Optional for method parameters — use overloading or @Nullable
|
|
89
|
+
public void createUser(Optional<String> role) { ... } // bad
|
|
90
|
+
public void createUser(String role) { ... } // good: role can be nullable
|
|
91
|
+
public void createUser() { ... } // good: overload for absent role
|
|
92
|
+
|
|
93
|
+
// 3. Do NOT use Optional for collections — return empty collection instead
|
|
94
|
+
public Optional<List<Order>> getOrders() { ... } // bad: double-wrap
|
|
95
|
+
public List<Order> getOrders() { ... } // good: return empty list
|
|
96
|
+
|
|
97
|
+
// 4. Do NOT use Optional.get() without checking — defeats the purpose
|
|
98
|
+
user.get().getName(); // same as null, throws NoSuchElementException
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Optional factory methods:**
|
|
102
|
+
|
|
103
|
+
```java
|
|
104
|
+
Optional.of(value) // value must be non-null; throws NPE if null
|
|
105
|
+
Optional.ofNullable(value) // safe: wraps null into empty Optional
|
|
106
|
+
Optional.empty() // explicitly empty
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Tools:** IntelliJ Inspections (`OptionalUsedAsFieldOrParameterType`), SonarQube (`S3553`, `S2789`), PMD
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Laravel Framework — SunLint Agent Guide
|
|
2
|
+
|
|
3
|
+
> Priority directives for AI agents working on Laravel projects.
|
|
4
|
+
> Rule files: `.agent/skills/sunlint-code-quality/rules/`
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Critical Patterns — Apply Every Time
|
|
9
|
+
|
|
10
|
+
### Validation
|
|
11
|
+
- **Always** use `php artisan make:request` → `FormRequest` class, never `$request->validate()` in controller
|
|
12
|
+
- In the controller use `$request->validated()` — never `$request->all()` or `$request->input()` wholesale
|
|
13
|
+
- See: `LV001-form-request-validation.md`
|
|
14
|
+
|
|
15
|
+
### N+1 Queries
|
|
16
|
+
- **Always** check: does this controller/service access a relation in a loop or collection?
|
|
17
|
+
- If yes → add `->with(['relation'])` to the query **before** the loop
|
|
18
|
+
- Use `->withCount('relation')` instead of `->relation->count()` in loops
|
|
19
|
+
- See: `LV002-eager-load-no-n-plus-1.md`
|
|
20
|
+
|
|
21
|
+
### Mass Assignment
|
|
22
|
+
- Every new Model must define explicit `$fillable = [...]`
|
|
23
|
+
- **Never**: `protected $guarded = []`
|
|
24
|
+
- **Never**: `Model::create($request->all())` — use `$request->validated()`
|
|
25
|
+
- See: `LV004-fillable-mass-assignment.md`
|
|
26
|
+
|
|
27
|
+
### Passwords
|
|
28
|
+
- `Hash::make($password)` to store, `Hash::check($plain, $hash)` to verify
|
|
29
|
+
- **Never** `md5()`, `sha1()`, or raw `password_hash()` for passwords
|
|
30
|
+
- See: `LV007-hash-passwords.md`
|
|
31
|
+
|
|
32
|
+
### Authorization
|
|
33
|
+
- **Never** `if ($user->role === 'admin')` scattered in controllers
|
|
34
|
+
- Generate a Policy: `php artisan make:policy ModelPolicy --model=Model`
|
|
35
|
+
- Use `$this->authorize('action', $model)` or Form Request `authorize()`
|
|
36
|
+
- See: `LV005-policies-gates-authorization.md`
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Architecture Patterns
|
|
41
|
+
|
|
42
|
+
### Controller responsibility
|
|
43
|
+
Controllers do exactly 3 things: validate input → call service → return response.
|
|
44
|
+
No business logic, no DB queries, no calculations in controllers.
|
|
45
|
+
```php
|
|
46
|
+
// Controller
|
|
47
|
+
public function store(StoreOrderRequest $request): JsonResponse
|
|
48
|
+
{
|
|
49
|
+
$order = $this->orderService->placeOrder($request->user(), $request->validated());
|
|
50
|
+
return OrderResource::make($order)->response()->setStatusCode(201);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
See: `LV012-service-layer.md`
|
|
54
|
+
|
|
55
|
+
### API Responses
|
|
56
|
+
- Always use `php artisan make:resource` → never `->toArray()`, `->toJson()` or raw `response()->json($model)`
|
|
57
|
+
- Sensitive columns (`password`, `remember_token`) must be in `$hidden` on the model
|
|
58
|
+
- See: `LV009-api-resources.md`
|
|
59
|
+
|
|
60
|
+
### Configuration
|
|
61
|
+
- `env()` appears **only** in `config/*.php` files — never in app code (breaks after `config:cache`)
|
|
62
|
+
- App code always uses `config('key.sub_key')` with optional default
|
|
63
|
+
- See: `LV003-config-not-env.md`
|
|
64
|
+
|
|
65
|
+
### Heavy Operations (email, API calls, reports)
|
|
66
|
+
- Any external call or operation > ~200ms → `Job::dispatch()->onQueue('name')`
|
|
67
|
+
- Job class implements `ShouldQueue`, handles `failed()` method
|
|
68
|
+
- See: `LV006-queue-heavy-tasks.md`
|
|
69
|
+
|
|
70
|
+
### Large Data Sets
|
|
71
|
+
- No `Model::all()` or unbounded `->get()` in commands/jobs
|
|
72
|
+
- Use `->chunkById(500, fn($batch) => ...)` or `->cursor()` for iteration
|
|
73
|
+
- See: `LV010-chunk-large-datasets.md`
|
|
74
|
+
|
|
75
|
+
### Multi-step Writes
|
|
76
|
+
- Any sequence of 2+ DB writes that must be atomic → wrap in `DB::transaction(fn() => { ... })`
|
|
77
|
+
- dispatch jobs inside transactions: `Job::dispatch($model)->afterCommit()`
|
|
78
|
+
- See: `LV011-db-transactions.md`
|
|
79
|
+
|
|
80
|
+
### Route Model Binding
|
|
81
|
+
- Route params matching a Model name auto-resolve — no manual `findOrFail()`
|
|
82
|
+
- `Route::get('/posts/{post}', ...)` → controller receives `Post $post` directly
|
|
83
|
+
- See: `LV008-route-model-binding.md`
|
|
84
|
+
|
|
85
|
+
### Dependency Injection
|
|
86
|
+
- Register services in `AppServiceProvider::register()` as singletons/bindings
|
|
87
|
+
- Controllers receive via constructor — never `new Service()` inside a method
|
|
88
|
+
- See: `LV014-service-container.md`
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Run Commands
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
php artisan make:request StoreXxxRequest # validation
|
|
96
|
+
php artisan make:policy XxxPolicy --model=Xxx
|
|
97
|
+
php artisan make:resource XxxResource
|
|
98
|
+
php artisan make:job ProcessXxx
|
|
99
|
+
php artisan make:service XxxService # (if using stubs)
|
|
100
|
+
|
|
101
|
+
php artisan test --filter ClassName # run specific test
|
|
102
|
+
php artisan test --coverage # coverage report
|
|
103
|
+
php artisan config:cache # validate no env() in app code
|
|
104
|
+
php artisan route:list --path=api # audit routes
|
|
105
|
+
php artisan telescope:install # install query/request debugger
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## What NOT to do (quick reference)
|
|
111
|
+
|
|
112
|
+
| ❌ Wrong | ✅ Correct |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `$request->validate([...])` in controller | `FormRequest` class |
|
|
115
|
+
| `$request->all()` | `$request->validated()` |
|
|
116
|
+
| `Post::all()` | `Post::with([...])->paginate(20)` |
|
|
117
|
+
| `$post->comments->count()` in loop | `Post::withCount('comments')` |
|
|
118
|
+
| `env('KEY')` in service | `config('prefix.key')` |
|
|
119
|
+
| `$guarded = []` | `$fillable = ['col1', 'col2']` |
|
|
120
|
+
| `if ($user->role === 'admin')` | `$this->authorize('action', $model)` |
|
|
121
|
+
| `md5($password)` | `Hash::make($password)` |
|
|
122
|
+
| `response()->json($model)` | `ModelResource::make($model)` |
|
|
123
|
+
| `Mail::send()` in controller | `SendMailJob::dispatch()->onQueue(...)` |
|
|
124
|
+
| `new OrderService()` in controller | Constructor DI auto-resolved by container |
|
|
@@ -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
|
+
```
|