diff --git a/app/Database/RestrictedEloquentBuilder.php b/app/Database/RestrictedEloquentBuilder.php index fa098bdd3..748eb2c12 100755 --- a/app/Database/RestrictedEloquentBuilder.php +++ b/app/Database/RestrictedEloquentBuilder.php @@ -191,6 +191,104 @@ class RestrictedEloquentBuilder extends Builder return $this->whereIn($column, $values, 'or', true); } + /** + * ========================================== + * POLYMORPHIC JOIN HELPERS + * ========================================== + */ + + /** + * Add a polymorphic INNER JOIN to the query + * + * Joins a table with polymorphic columns (e.g., fileable_type, fileable_id) + * to the current model's table, automatically handling type_ref conversion. + * + * @param string $table The table containing the polymorphic columns + * @param string $morphName The morph column prefix (e.g., 'fileable' for fileable_type/fileable_id) + * @param string|null $morphClass The class name to match. If null, uses current model's class. + * @return $this + * + * @example + * // Get contacts with their attachments + * Contact_Model::query() + * ->joinMorph('file_attachments', 'fileable') + * ->get(); + * + * // Explicit class (when joining from a different model) + * SomeModel::query() + * ->joinMorph('file_attachments', 'fileable', Contact_Model::class) + * ->get(); + */ + public function joinMorph(string $table, string $morphName, ?string $morphClass = null): self + { + return $this->_addMorphJoin('join', $table, $morphName, $morphClass); + } + + /** + * Add a polymorphic LEFT JOIN to the query + * + * @param string $table The table containing the polymorphic columns + * @param string $morphName The morph column prefix (e.g., 'fileable') + * @param string|null $morphClass The class name to match. If null, uses current model's class. + * @return $this + * + * @example + * // Get all contacts, with attachments if they exist + * Contact_Model::query() + * ->leftJoinMorph('file_attachments', 'fileable') + * ->get(); + */ + public function leftJoinMorph(string $table, string $morphName, ?string $morphClass = null): self + { + return $this->_addMorphJoin('leftJoin', $table, $morphName, $morphClass); + } + + /** + * Add a polymorphic RIGHT JOIN to the query + * + * @param string $table The table containing the polymorphic columns + * @param string $morphName The morph column prefix (e.g., 'fileable') + * @param string|null $morphClass The class name to match. If null, uses current model's class. + * @return $this + */ + public function rightJoinMorph(string $table, string $morphName, ?string $morphClass = null): self + { + return $this->_addMorphJoin('rightJoin', $table, $morphName, $morphClass); + } + + /** + * Internal helper to add a polymorphic join + * + * @param string $joinMethod The join method to use (join, leftJoin, rightJoin) + * @param string $table The table containing the polymorphic columns + * @param string $morphName The morph column prefix + * @param string|null $morphClass The class name to match + * @return $this + */ + protected function _addMorphJoin(string $joinMethod, string $table, string $morphName, ?string $morphClass): self + { + // Determine the class name to use + if ($morphClass === null) { + // Use the current model's simple class name + $morphClass = class_basename($this->getModel()); + } else { + // Extract simple class name if FQCN was provided + $morphClass = class_basename($morphClass); + } + + // Get the type_ref ID for the class + $typeRefId = Type_Ref_Registry::class_to_id($morphClass); + + // Get the current model's table name + $baseTable = $this->getModel()->getTable(); + + // Build the join + return $this->$joinMethod($table, function ($join) use ($table, $baseTable, $morphName, $typeRefId) { + $join->on("{$table}.{$morphName}_id", '=', "{$baseTable}.id") + ->where("{$table}.{$morphName}_type", '=', $typeRefId); + }); + } + /** * Prevent eager loading via with() * diff --git a/app/RSpade/Core/Database/TypeRefs/CLAUDE.md b/app/RSpade/Core/Database/TypeRefs/CLAUDE.md index 9b6f7532d..f7b5c38d6 100755 --- a/app/RSpade/Core/Database/TypeRefs/CLAUDE.md +++ b/app/RSpade/Core/Database/TypeRefs/CLAUDE.md @@ -108,6 +108,35 @@ File_Attachment_Model::where('fileable_type', 42)->get(); Supported methods: `where()`, `orWhere()`, `whereIn()`, `orWhereIn()`, `whereNotIn()`, `orWhereNotIn()` +## Polymorphic Join Helpers + +Join tables with polymorphic columns using dedicated helpers: + +```php +// Get contacts that have attachments (INNER JOIN) +Contact_Model::query() + ->joinMorph('file_attachments', 'fileable') + ->select('contacts.*', 'file_attachments.filename') + ->get(); + +// Get all contacts, with attachments if they exist (LEFT JOIN) +Contact_Model::query() + ->leftJoinMorph('file_attachments', 'fileable') + ->get(); + +// Explicit class (when querying from a different model) +SomeModel::query() + ->leftJoinMorph('file_attachments', 'fileable', Contact_Model::class) + ->get(); +``` + +Available methods: `joinMorph()`, `leftJoinMorph()`, `rightJoinMorph()` + +Parameters: +- `$table` - Table with polymorphic columns (e.g., `'file_attachments'`) +- `$morphName` - Column prefix (e.g., `'fileable'` for `fileable_type`/`fileable_id`) +- `$morphClass` - Optional class name (defaults to current model) + ## Important Notes - **Simple Names Only**: Always use simple class names (`Contact_Model`), never FQCNs diff --git a/app/RSpade/man/polymorphic.txt b/app/RSpade/man/polymorphic.txt index 993c006d8..53a8b3b39 100755 --- a/app/RSpade/man/polymorphic.txt +++ b/app/RSpade/man/polymorphic.txt @@ -130,6 +130,66 @@ LARAVEL MORPH MAP INTEGRATION The morph map uses simple class names (e.g., "Contact_Model") not fully qualified names, matching how RSX models work throughout the framework. +QUERY BUILDER INTEGRATION + Transparent WHERE Clauses + + The query builder automatically converts type_ref columns in WHERE + clauses. You can use class name strings directly: + + // All of these work - class names auto-converted to IDs + File_Attachment_Model::where('fileable_type', 'Contact_Model')->get(); + File_Attachment_Model::where('fileable_type', '=', 'Contact_Model')->get(); + File_Attachment_Model::where(['fileable_type' => 'Contact_Model'])->get(); + + // whereIn also works + File_Attachment_Model::whereIn('fileable_type', [ + 'Contact_Model', + 'Project_Model' + ])->get(); + + // Integer IDs still work (pass-through) + File_Attachment_Model::where('fileable_type', 42)->get(); + + Supported methods: where(), orWhere(), whereIn(), orWhereIn(), + whereNotIn(), orWhereNotIn() + + Polymorphic Join Helpers + + Join tables with polymorphic columns using dedicated helpers: + + // Get contacts that have attachments (INNER JOIN) + Contact_Model::query() + ->joinMorph('file_attachments', 'fileable') + ->select('contacts.*', 'file_attachments.filename') + ->get(); + + // Get all contacts, with attachments if they exist (LEFT JOIN) + Contact_Model::query() + ->leftJoinMorph('file_attachments', 'fileable') + ->get(); + + // RIGHT JOIN + Contact_Model::query() + ->rightJoinMorph('file_attachments', 'fileable') + ->get(); + + Parameters: + $table - Table with polymorphic columns (e.g., 'file_attachments') + $morphName - Column prefix (e.g., 'fileable' for fileable_type/fileable_id) + $morphClass - Optional class name (defaults to current model) + + Explicit class (when querying from a different model context): + + SomeModel::query() + ->leftJoinMorph('file_attachments', 'fileable', Contact_Model::class) + ->get(); + + The generated SQL is equivalent to: + + LEFT JOIN file_attachments + ON file_attachments.fileable_id = contacts.id + AND file_attachments.fileable_type = + FORM HANDLING Client-Side Format