Skip to content

[detector-report] NamespaceCycleDetector #425

Description

@jessegall

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions