Eloquent internals — Model.php, newQuery(), getTable(), hydration
Concept
Understanding Eloquent's internals helps debug unexpected behavior and write more effective extensions.
Illuminate\Database\Eloquent\Model: The base class. ~2500 lines. Manages attributes (raw database values), relations (loaded relationship data), and the query builder connection.
newQuery(): Called every time you chain query methods on a model class. Returns an Illuminate\Database\Eloquent\Builder wrapping the underlying Illuminate\Database\Query\Builder. The Eloquent Builder adds model-specific methods (with, withCount, scopes) while delegating base query construction to the Query Builder.
newInstance(): Creates a new model instance. Called during find(), get(), etc. for each result row.
Hydration: The process of converting raw database rows (arrays of column → value) into model instances. Model::hydrate(array $items) — given an array of arrays, returns a Collection of hydrated models. Hydration sets attributes, applies casts, and resolves eager-loaded relationships.
Attribute storage: Raw database values are stored in $model->attributes array. Accessing $model->name triggers __get() → getAttribute() → applies cast if configured, runs accessor if defined, returns raw value otherwise. Casting and accessors run on every access.
getTable(): Returns the table name. Override $table to change it.
getDirty(): Returns only changed attributes (not yet saved). getOriginal() returns original values (before modification). Both are used heavily in Observers.
toArray(): Iterates over $attributes + $appends, applies accessors and casts, merges loaded relations (also toArray()'d recursively).
Code Example
<?php
// Exploring Model internals
$user = User::find(1);
// Raw stored attributes (pre-cast)
$user->getAttributes(); // ['id' => 1, 'name' => 'alice', 'role' => 'admin']
$user->getRawOriginal(); // same as above (before any changes)
// After modification
$user->name = 'Alice';
$user->getDirty(); // ['name' => 'Alice'] — changed, not yet saved
$user->getOriginal('name'); // 'alice' — original value
// Hydration — useful for testing and raw result conversion
$rows = DB::select('SELECT * FROM users WHERE active = 1');
// $rows is array of stdClass — convert to User models manually:
$users = User::hydrate(
array_map(fn($row) => (array) $row, $rows) // stdClass → array
);
// $users is now an Eloquent Collection of User models with casts applied
// newQuery() — every Model::where() calls this internally
$builder = (new User)->newQuery(); // fresh Eloquent Builder
$builder->where('active', true)->get();
// Accessing underlying Query Builder
$eloquentBuilder = User::query();
$queryBuilder = $eloquentBuilder->getQuery(); // base Query\Builder instance
// Debug the SQL:
$eloquentBuilder->where('active', true)->toSql();
// "select * from `users` where `active` = ?"
$eloquentBuilder->where('active', true)->getBindings();
// [true]
// Full SQL dump (for debugging only, never in production)
$sql = User::where('active', true)->with('posts')->toSql();
// "select * from `users` where `active` = ?" — note: with() doesn't affect main SQL
// Model::unguard() / reguard() — disable mass assignment protection temporarily
// Used in migration-style operations
Model::unguard();
User::create(['id' => 1, 'name' => 'Alice', 'password' => '...']); // no fillable check
Model::reguard();