Detector: NamespaceCycleDetector
Report (why the flagged code is CORRECT and the detector is wrong):
These are Eloquent relations and a search-index hook, not a layer breach. A user HAS a picker profile and HAS a cashier profile; hasOne(Picker::class) is how that is declared, and the domain projections carry the inverse belongsTo(User::class) because the association is genuinely two-way. There is no arrow to invert: an association is symmetric by nature, and Eloquent requires both ends to name each other's class. Removing either end deletes the relation rather than redirecting it.
Cleanest design the reporter can conceive:
User declares hasOne(Picker::class) and hasOne(Cashier::class), and each projection declares the inverse belongsTo(User::class). SearchIndexLookup::apply() keeps the index query in one place instead of on each searchable model. Nothing else about a picker or a cashier is known to User — it holds the relations and nothing more.
⚖️ Maintainer litmus: a valid detector-report needs the flagged code to ALREADY BE the
cleanest design. If the design above differs from the flagged code at all, THAT design is
the owed fix — close this report; the fix is still owed.
Where: app/Models/User.php:112
Code (app/Models/User.php:112):
109 */
110 public function picker(): HasOne
111 {
→ 112 return $this->hasOne(Picker::class);
113 }
114
115 /**
116 * @return HasOne<Cashier, $this>
117 */
118 public function cashier(): HasOne
119 {
120 return $this->hasOne(Cashier::class);
121 }
122
123 protected function hasCashierPin(): Attribute
124 {
125 return Attribute::get(fn (): bool => $this->cashier?->pin_code_hash !== null);
126 }
127
128 // ----------[ Accessors ]----------
129
130 /**
131 * Picker-assigned display color — null when the user has no picker row.
132 */
133 protected function color(): Attribute
134 {
135 return Attribute::get(fn (): Color | null => $this->picker?->color);
136 }
Where: app/Models/User.php:120
Code (app/Models/User.php:120):
117 */
118 public function cashier(): HasOne
119 {
→ 120 return $this->hasOne(Cashier::class);
121 }
122
123 protected function hasCashierPin(): Attribute
124 {
125 return Attribute::get(fn (): bool => $this->cashier?->pin_code_hash !== null);
126 }
127
128 // ----------[ Accessors ]----------
129
130 /**
131 * Picker-assigned display color — null when the user has no picker row.
132 */
133 protected function color(): Attribute
134 {
135 return Attribute::get(fn (): Color | null => $this->picker?->color);
136 }
137
138 /**
139 * Handheld theme preference. Falls back to dark when the user has no picker row.
140 */
141 protected function theme(): Attribute
142 {
143 return Attribute::get(fn (): Theme => $this->picker?->theme ?? Theme::DARK);
144 }
Where: app/Models/User.php:102
Code (app/Models/User.php:102):
99
100 public function searchFor(Builder $builder, string $value): void
101 {
→ 102 SearchIndexLookup::apply($builder, 'user', $value);
103 }
104
105 // ----------[ Relations ]----------
106
107 /**
108 * @return HasOne<Picker, $this>
109 */
110 public function picker(): HasOne
111 {
112 return $this->hasOne(Picker::class);
113 }
114
115 /**
116 * @return HasOne<Cashier, $this>
117 */
118 public function cashier(): HasOne
119 {
120 return $this->hasOne(Cashier::class);
121 }
122
123 protected function hasCashierPin(): Attribute
124 {
125 return Attribute::get(fn (): bool => $this->cashier?->pin_code_hash !== null);
126 }
Filed via commandments report from a consumer project.
Detector:
NamespaceCycleDetectorReport (why the flagged code is CORRECT and the detector is wrong):
These are Eloquent relations and a search-index hook, not a layer breach. A user HAS a picker profile and HAS a cashier profile; hasOne(Picker::class) is how that is declared, and the domain projections carry the inverse belongsTo(User::class) because the association is genuinely two-way. There is no arrow to invert: an association is symmetric by nature, and Eloquent requires both ends to name each other's class. Removing either end deletes the relation rather than redirecting it.
Cleanest design the reporter can conceive:
User declares hasOne(Picker::class) and hasOne(Cashier::class), and each projection declares the inverse belongsTo(User::class). SearchIndexLookup::apply() keeps the index query in one place instead of on each searchable model. Nothing else about a picker or a cashier is known to User — it holds the relations and nothing more.
Where:
app/Models/User.php:112Code (
app/Models/User.php:112):Where:
app/Models/User.php:120Code (
app/Models/User.php:120):Where:
app/Models/User.php:102Code (
app/Models/User.php:102):Filed via
commandments reportfrom a consumer project.