From 94dce4f021c04d6dfbebece4f00db8c8b2728f0f Mon Sep 17 00:00:00 2001 From: root Date: Sat, 27 Dec 2025 23:49:21 +0000 Subject: [PATCH] Add transparent type_ref conversion in WHERE clauses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Database/RestrictedEloquentBuilder.php | 178 ++++++++++++++++++++ app/RSpade/Core/Database/TypeRefs/CLAUDE.md | 20 +++ 2 files changed, 198 insertions(+) diff --git a/app/Database/RestrictedEloquentBuilder.php b/app/Database/RestrictedEloquentBuilder.php index 24a0f2a29..fa098bdd3 100755 --- a/app/Database/RestrictedEloquentBuilder.php +++ b/app/Database/RestrictedEloquentBuilder.php @@ -3,6 +3,7 @@ namespace App\Database; use Illuminate\Database\Eloquent\Builder; +use App\RSpade\Core\Database\TypeRefs\Type_Ref_Registry; /** * Custom Eloquent query builder that prevents eager loading and unsafe operations @@ -10,9 +11,186 @@ use Illuminate\Database\Eloquent\Builder; * This builder overrides dangerous methods to enforce RSpade framework safety rules: * - All eager loading methods throw exceptions (with/withCount/etc) * - DELETE without WHERE clause throws exception to prevent accidental mass deletion + * - Automatic type_ref column conversion in WHERE clauses */ class RestrictedEloquentBuilder extends Builder { + /** + * Cached list of type_ref columns for this builder's model + * @var array|null + */ + protected $_type_ref_columns_cache = null; + + /** + * Get the type_ref columns for the current model + * + * @return array + */ + protected function _get_type_ref_columns(): array + { + if ($this->_type_ref_columns_cache === null) { + $model = $this->getModel(); + $reflection = new \ReflectionClass($model); + + // Access protected static property + if ($reflection->hasProperty('type_ref_columns')) { + $property = $reflection->getProperty('type_ref_columns'); + $property->setAccessible(true); + $this->_type_ref_columns_cache = $property->getValue() ?: []; + } else { + $this->_type_ref_columns_cache = []; + } + } + + return $this->_type_ref_columns_cache; + } + + /** + * Check if a column is a type_ref column + * + * @param string $column + * @return bool + */ + protected function _is_type_ref_column(string $column): bool + { + return in_array($column, $this->_get_type_ref_columns()); + } + + /** + * Convert a type_ref value (class name string) to its integer ID + * + * @param mixed $value + * @return mixed + */ + protected function _convert_type_ref_value($value) + { + if ($value === null || $value === '') { + return $value; + } + + // If already an integer, return as-is + if (is_int($value) || (is_string($value) && ctype_digit($value))) { + return (int) $value; + } + + // Convert class name to ID + return Type_Ref_Registry::class_to_id((string) $value); + } + + /** + * Override where() to auto-convert type_ref columns + * + * @param \Closure|string|array $column + * @param mixed $operator + * @param mixed $value + * @param string $boolean + * @return $this + */ + public function where($column, $operator = null, $value = null, $boolean = 'and') + { + // Handle closure form - no conversion needed + if ($column instanceof \Closure) { + return parent::where($column, $operator, $value, $boolean); + } + + // Handle array form: ['column' => 'value'] + if (is_array($column)) { + $converted = []; + foreach ($column as $col => $val) { + if (is_string($col) && $this->_is_type_ref_column($col)) { + $converted[$col] = $this->_convert_type_ref_value($val); + } else { + $converted[$col] = $val; + } + } + return parent::where($converted, $operator, $value, $boolean); + } + + // Handle 2-arg form: where('column', 'value') -> operator is actually value + // Handle 3-arg form: where('column', '=', 'value') + if (is_string($column) && $this->_is_type_ref_column($column)) { + if (func_num_args() === 2) { + // 2-arg form: $operator is actually the value + $operator = $this->_convert_type_ref_value($operator); + } else { + // 3+ arg form: $value is the value + $value = $this->_convert_type_ref_value($value); + } + } + + return parent::where($column, $operator, $value, $boolean); + } + + /** + * Override orWhere() to auto-convert type_ref columns + * + * @param \Closure|string|array $column + * @param mixed $operator + * @param mixed $value + * @return $this + */ + public function orWhere($column, $operator = null, $value = null) + { + return $this->where($column, $operator, $value, 'or'); + } + + /** + * Override whereIn() to auto-convert type_ref columns + * + * @param string $column + * @param mixed $values + * @param string $boolean + * @param bool $not + * @return $this + */ + public function whereIn($column, $values, $boolean = 'and', $not = false) + { + if (is_string($column) && $this->_is_type_ref_column($column)) { + if (is_array($values)) { + $values = array_map(fn($v) => $this->_convert_type_ref_value($v), $values); + } + } + + return parent::whereIn($column, $values, $boolean, $not); + } + + /** + * Override orWhereIn() to auto-convert type_ref columns + * + * @param string $column + * @param mixed $values + * @return $this + */ + public function orWhereIn($column, $values) + { + return $this->whereIn($column, $values, 'or'); + } + + /** + * Override whereNotIn() to auto-convert type_ref columns + * + * @param string $column + * @param mixed $values + * @param string $boolean + * @return $this + */ + public function whereNotIn($column, $values, $boolean = 'and') + { + return $this->whereIn($column, $values, $boolean, true); + } + + /** + * Override orWhereNotIn() to auto-convert type_ref columns + * + * @param string $column + * @param mixed $values + * @return $this + */ + public function orWhereNotIn($column, $values) + { + return $this->whereIn($column, $values, 'or', true); + } + /** * Prevent eager loading via with() * diff --git a/app/RSpade/Core/Database/TypeRefs/CLAUDE.md b/app/RSpade/Core/Database/TypeRefs/CLAUDE.md index 60f074bac..9b6f7532d 100755 --- a/app/RSpade/Core/Database/TypeRefs/CLAUDE.md +++ b/app/RSpade/Core/Database/TypeRefs/CLAUDE.md @@ -89,12 +89,32 @@ The `Type_Ref_Registry::register_morph_map()` method is called during framework $model->attachable_type = class_basename($related); ``` +## Query Builder Integration + +The `RestrictedEloquentBuilder` automatically converts type_ref columns in WHERE clauses: + +```php +// All of these work transparently - 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()` + ## Important Notes - **Simple Names Only**: Always use simple class names (`Contact_Model`), never FQCNs - **Auto-Registration**: New classes are auto-registered when first used - **Transparent**: After setup, code works identically to VARCHAR storage - **Laravel Compatible**: Works with `morphTo()`, `morphMany()`, etc. +- **Query Logs**: Show integer IDs, not class names (expected behavior) ## Reference