crystoline/xpsr-orm-eloquent

Eloquent models for XPSR applications, without the rest of Laravel. Models run on XPSR's database connections from crystoline/xpsr-database: Eloquent queries the PDO that XPSR's connection opened, and its transactions go through XPSR's, so models, plain DBAL queries, and test rollbacks share one transaction. Schema changes are XPSR migrations.

composer require crystoline/xpsr-orm-eloquent

Add both providers to app.registry in config/app.php, database first:

Crystoline\Xpsr\Database\DatabaseServiceRegistry::class,
Crystoline\Xpsr\Orm\Eloquent\EloquentServiceRegistry::class,

There is nothing else to configure: connections are the ones in config/database.php.

Models

php xpsr make:model Task
php xpsr make:migration create_tasks_table --create=tasks

make:model writes app/Model/Task.php (App\Model\Task) for the table Eloquent would choose (tasks):

namespace App\Model;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    protected $table = 'tasks';
    protected $fillable = ['title', 'done'];
    protected $casts = ['done' => 'boolean'];
}
$task = Task::query()->create(['title' => 'Ship it']);
$open = Task::query()->where('done', false)->orderBy('id')->get();
$task->update(['done' => true]);

Everything Eloquent offers works: relationships, casts, scopes, accessors, soft deletes, and model events and observers. A model with protected $connection = 'reporting'; uses the XPSR connection named reporting.

Two features need more packages: paginate() needs illuminate/pagination, and model factories need fakerphp/faker. Without them, use forPage($page, $perPage) and plain test data.

Schema changes

Tables are created and changed with XPSR migrations (make:migration, migrate, migrate:rollback), written with Doctrine DBAL's schema API; see the database package's README. Eloquent's own migrations and schema builder are not used, so every database adapter shares one set of migration commands. Eloquent has no mapping to compare with the database, so make:migration --diff is for Doctrine entities only.

Transactions

use Illuminate\Database\ConnectionInterface;

$db->transaction(static function () {
    $order = Order::query()->create([...]);
    $order->lines()->createMany([...]);
});

Eloquent's transactions run through the XPSR connection, which nests them with savepoints. An Eloquent transaction inside an XPSR one (as in DatabaseTransactions tests), or the reverse, never starts a second transaction on the same PDO, and rolling back the outer one undoes everything inside it. transaction($callback, attempts: 3) retries deadlocks at the outermost level, as in Laravel.

Users

Models can be the users of config/auth.php:

'users' => ['driver' => 'eloquent', 'model' => App\Model\User::class, 'login' => 'email', 'password' => 'password'],
use Crystoline\Xpsr\Auth\UserInterface;
use Crystoline\Xpsr\Orm\Eloquent\Auth\Authenticatable;

class User extends Model implements UserInterface
{
    use Authenticatable;

    protected $hidden = ['password'];
}

Authenticatable reads the key and the password attribute; sign-in saves an upgraded hash to the password column. See the XPSR repository's docs/AUTH.md.

Testing

Crystoline\Xpsr\Database\Testing\DatabaseTransactions from the database package covers models too: they write through the same connection, so their changes are rolled back after each test.

This adapter passes Crystoline\Xpsr\Database\Testing\OrmAdapterContractTestCase. Its tests, including transactions nested both ways, run against SQLite, MySQL 8.4, MariaDB 11, and PostgreSQL 17.

Static analysis tools do not see Eloquent's static forwarding (Task::where(...)); Task::query()->where(...) is the same call and is understood.

Services

Service Provides
Crystoline\Xpsr\Orm\Eloquent\ConnectionResolver Eloquent's connections by XPSR connection name (also Illuminate\Database\ConnectionResolverInterface). Shared.
Illuminate\Database\ConnectionInterface The default connection, for the query builder: $db->table('tasks')->count().

Requires PHP 8.4+ and a PDO driver: pdo_mysql, pdo_pgsql, pdo_sqlite, or pdo_sqlsrv.