Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Eloquent Otf Laravel Package

calhoun/eloquent-otf

Eloquent OTF lets Laravel developers define Eloquent relationships on the fly, adding hasOne, hasMany, belongsTo, and similar relations without editing model classes. Useful for packages, dynamic dashboards, and flexible apps. It keeps models clean while enabling runtime relationship configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

calhoun/eloquent-otf ("on the fly") is aimed at the case where you have a database table but no dedicated Eloquent model class — legacy schemas, per-tenant or per-period table names, reporting tables, or throwaway read queries. Instead of generating and committing a class per table, you get a model instance/class configured at runtime.

Minimal steps:

  1. composer require calhoun/eloquent-otf
  2. If the package ships a service provider, confirm it is auto-discovered (php artisan vendor:publish --provider="Calhoun\EloquentOtf\..." if there is a config file). Look at config/eloquent-otf.php for defaults around connection, primary key, timestamps, and base model.
  3. Open the package's src/ and read, in this order:
    • the entry point (facade / helper / trait) — this tells you the exact fluent API,
    • the class that actually builds the model (usually a subclass of Illuminate\Database\Eloquent\Model),
    • the tests/ directory — the fastest, most accurate documentation of intended usage.
  4. First use case — query a legacy table with zero ceremony:
$orders = EloquentOTF::table('legacy_orders')
    ->key('order_id')
    ->timestamps(false)
    ->make();

$orders::query()->where('status', 'open')->limit(50)->get();

The mental model to hold onto: this package is a factory that returns a configured Eloquent model class, not a new query builder. Everything downstream — scopes, where, with, chunk, toArray — behaves exactly as Eloquent does, because it is Eloquent. That also means every Eloquent assumption about keys, timestamps, casts, and fillable still applies and is now your responsibility.

Implementation Patterns

Wrap it, don't sprinkle it. OTF calls are cheap to write and easy to litter. Put each runtime table behind a small named factory or repository method so the rest of the app sees a normal class-like API:

final class LegacyOrders
{
    public static function query(): Builder
    {
        return EloquentOTF::table('legacy_orders')
            ->key('order_id')
            ->timestamps(false)
            ->make()::query();
    }
}

Cache the built class. Building a model class on every request (or inside a loop) is wasteful and can cause subtle issues if the package generates/evals classes. Memoize per table name in a container singleton or a static registry, and — critically — memoize per connection too if you run multi-tenant.

Sharded / periodic tables. This is the strongest fit. For events_2024_01, events_2024_02, … or tenant_42_orders, derive the table name and hand it to the factory; keep the query layer identical across shards:

$model = EloquentOTF::table("events_{$year}_{$month}")
    ->connection($tenant->connection)
    ->make();

Then use ->newQuery() for the union/merge patterns and cursor()/chunkById() for cross-shard iteration. Use chunkById rather than chunk when the key is non-standard, since offset paging on large legacy tables will drift.

Explicit schema contract. Since there is no model file to read, declare the important facts at the call site so they're greppable: primary key, incrementing/non-incrementing, timestamps on/off, connection, and $casts for dates and JSON columns stored as text. Treat that block as your "model file" and review it in PRs like one.

Relationships. You can still define relations on runtime models via closures or by having the factory return a subclass of your own base model (the package's documented extension point). If relationships matter, prefer defining a thin real model that extends the factory-produced one, or attach the relation closures once in a service provider. Don't try to build deep with() graphs from raw OTF calls — it gets unreadable fast.

Read-only discipline. Most OTF tables don't have your app's conventions (no updated_at, no auto-increment key, no guarded-friendly columns). Use these models for reads, reporting, and imports. For writes, set $guarded = [] deliberately and wrap in a transaction — never let an OTF model be mass-assigned from $request->all().

Testing. Because the "model" is config, test the factory output rather than the query: assert the resolved table, connection, key, and timestamp flags, then hit an in-memory SQLite schema you create in setUp. Keep one integration test against the real legacy schema shape.

Gotchas and Tips

  • Unproven dependency. 72 stars, zero dependents, single maintainer. Pin the version in composer.json and read the source before upgrading. Don't put it on a critical write path without a fallback.
  • Timestamps bite first. Legacy tables usually lack created_at/updated_at. If you forget timestamps(false), inserts fail or silently write columns that don't exist. Same for incrementing, keyType (string keys), and perPage.
  • $fillable is empty by default, so create() and update() quietly no-op or throw MassAssignmentException. Set guarded/fillable explicitly — quiet no-ops are a classic debugging sink here.
  • Casts are not inferred. Anything stored as a string — dates, booleans, JSON, decimals — comes back as a string. Decimal columns as floats will bite you in money math; cast to decimal:2 or handle as strings.
  • Class identity matters for eager loading and morph maps. Two OTF calls for the same table can produce distinct class names; with() and polymorphic relations key off class name. Memoize the class, don't just memoize the query.
  • Serialization and queues. Models serialized into jobs must be re-hydratable — if the class is generated at runtime, the queued job may not resolve it. Convert to arrays/DTOs before dispatch.
  • Debugging: dump the resolved class and SQL with get_class($model), $model->getTable(), $model->getConnectionName(), and ->toSql() with bindings. When a query returns nothing, check the connection before the where clause — the default connection is the most common silent failure.
  • Static analysis and IDE support won't work well: no model file means no attribute autocompletion, no Larastan property inference. Consider @property stubs or a real (thin) model class for tables you touch often; OTF is a convenience, not a replacement for a model when the table is core to your domain.
  • N+1 is easier to create here because there's no obvious relation to eager-load. Watch query counts in reporting loops specifically.
  • Extension points to prefer: a custom base model class (override getTable, getKeyName, casts), a container binding that resolves the factory, and per-table config in the published config file. Avoid forking the package — the surface is small enough that composition covers most needs.
  • Schema drift is invisible. Migrations live elsewhere; nothing validates that the table still has the columns your code assumes. A small smoke test per OTF table pays for itself.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource