Add transparent type_ref conversion in WHERE clauses

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-27 23:49:21 +00:00
parent 56680840a1
commit 94dce4f021
2 changed files with 198 additions and 0 deletions

View File

@@ -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()
*

View File

@@ -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