This module glues illuminate/database (Eloquent) onto a CodeIgniter 3 codebase. As a Laravel developer, treat it as "Eloquent without the framework": you get models, relationships, the query builder, and the Capsule manager, but no Artisan, no service container, no facades, and no migration runner.
Minimal setup
bcit-ci/CodeIgniter) on PHP 7.4+ (8.1+ if you want current illuminate/database).composer require mod-ci/with-eloquent
If your install is module-based (HMVC-style), the package drops into application/modules/with_eloquent/; otherwise rely on Composer's autoloader.application/config/config.php:
$config['composer_autoload'] = FCPATH . 'vendor/autoload.php';
application/config/eloquent.php and point it at the same credentials as application/config/database.php. Keep this file as the single source of truth for Eloquent connections.pre_system or post_controller_constructor hook is the usual place, or the module's own bootstrap if it self-registers).$user = \App\Models\User::find(1);
var_dump($user->toArray());
Where to look first
config/eloquent.php — connection names, fetch mode, and whether it mirrors CI's $db group.CI_Model-adjacent) — this is where $connection, $table, and $timestamps defaults live.vendor/illuminate/database/Capsule/Manager.php — every gotcha downstream traces back to how (and how often) Capsule is booted.First use case
A read-heavy admin screen backed by CI's Query Builder is the lowest-risk adoption point. Swap one listing query for an Eloquent model, confirm pagination and JSON output match, then expand outward into the write paths.
Base model, once
Create a project-level base so you never repeat connection wiring:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
abstract class BaseModel extends Model
{
protected $connection = 'default'; // must match eloquent.php
public $timestamps = true;
protected $guarded = ['id'];
protected $perPage = 25;
}
Models in controllers
Eloquent models are instantiated directly; CI's $this->load->model() will not find them unless the module overrides the loader. A thin CI wrapper keeps existing controller idioms intact:
class User_model extends CI_Model
{
public function find($id) { return \App\Models\User::find($id); }
public function paginate($n = 25) { return \App\Models\User::paginate($n); }
}
Eager loading and scopes
The same patterns you use in Laravel transfer verbatim — which is the whole point of adopting this module:
$orders = \App\Models\Order::with(['customer', 'lines.product'])
->where('status', 'open')
->orderByDesc('created_at')
->paginate(20);
Local scopes, hasMany/belongsToMany, whereHas, and withCount all work. Push reusable filters into scopes rather than controller code so both CI and Eloquent call sites stay consistent.
Pagination into CI views
paginate() returns an Illuminate\Pagination\LengthAwarePaginator, not CI's $this->pagination. Render it explicitly and pass markup to the view:
$data['orders'] = $orders->items();
$data['links'] = $orders->appends($this->input->get())->links('pagination::bootstrap-4');
$this->load->view('orders/index', $data);
Transactions
Prefer Capsule's connection-level transaction helper over manual beginTransaction/commit, and never span a CI connection and an Eloquent connection in one transaction unless both point at the same PDO handle:
\Illuminate\Database\Capsule\Manager::connection()->transaction(function () use ($payload) {
$order = \App\Models\Order::create($payload);
$order->lines()->createMany($payload['lines']);
});
Mixing with the CI Query Builder
Use Capsule::table(...) for the Eloquent-side builder and $this->db for legacy CI code. If you must share the connection, configure Eloquent to reuse CI's PDO instance in the module bootstrap; otherwise assume two connections and design transactions accordingly.
Hydration and validation
Keep form_validation as the validation layer and hydrate Eloquent after it passes:
if ($this->form_validation->run() === false) { /* re-render */ }
\App\Models\Customer::create([
'name' => $this->input->post('name', true),
'email' => $this->input->post('email', true),
]);
Schema and seeding without Artisan
Use illuminate/database's Schema builder from a CLI script or a CI controller guarded by is_cli(), or install Phinx. Do not expect php artisan migrate — it does not exist here.
JSON APIs
No response()->json(); use CI's output class with the model's array form:
$this->output
->set_content_type('application/json')
->set_output(json_encode($orders->toArray()));
toArray() respects $hidden/$visible/$appends, so define those on the model instead of hand-building arrays in controllers.
application/config/database.php and application/config/eloquent.php drift immediately. Generate one from the other, or have the module bootstrap read CI's $db array directly.$this->load->model() will not see Eloquent classes. The CI loader looks in application/models/ by filename convention. Either keep wrappers there or resolve classes through Composer's PSR-4 autoload directly.QueryException is not a CI CI_Model error. It will bubble past CI's database error handler. Wrap risky writes in try/catch and route to log_message('error', ...) so failures land in CI's log pipeline.mod-ci/with-eloquent source, and pin a commit or tag — a repo mismatch here is the fastest way to end up with the wrong code.illuminate/database versions are pinned to PHP minor versions. Lock the pair (illuminate/database:^9 on PHP 8.0, ^10 on 8.1+) before upgrading the host.Carbon; CI's $config['time_reference'] and PHP's date_default_timezone_set must match the connection's timezone, or created_at values shift silently.$config['uri_segment'] is irrelevant to the Eloquent paginator, but your CI routes may still be built around it. Use appends() to preserve query string filters across pages.Capsule::connection()->enableQueryLog() behind a ENVIRONMENT === 'development' guard, or a DB::listen() dump in the module bootstrap.$fillable or $guarded on every model. $this->input->post() returns null for absent keys, which can null out columns unexpectedly if you pass the raw array into create().post_controller hooks for auditing, Eloquent's saving/saved events will not fire those — bridge them explicitly.$this->cache or illuminate/cache if already pulled in. Do not assume Cache::remember exists until you confirm the dependency.use SoftDeletes; plus deleted_at in the schema, or every query silently ignores the concept and your "archive" screen shows nothing.phpunit.xml's bootstrap (or a setUpBeforeClass) against a test connection. Laravel's RefreshDatabase trait is not available; use schema-builder drop/create in setup and truncate between tests.DB::listen. Put cross-cutting concerns there rather than in every model.How can I help you explore Laravel packages today?