dam-bal/eloquentity
A Laravel package that extends Eloquent with entity-style models, helping you separate domain entities from database concerns while keeping familiar query building and relationships. Lightweight integration for cleaner, more maintainable applications.
composer require dam-bal/eloquentity. The package is pre-1.0 (currently 0.5.3), so pin the minor version in composer.json ("^0.5.3") until you have a test suite that catches breakage.Eloquentity base class — it is the single entry point for everything the package does. Read it before reading the README examples; the examples only make sense once you see how the class resolves its underlying Eloquent model and how attributes are read/written.Eloquentity, and declare the model it maps to (or rely on the package's naming convention if one is configured). Keep the entity thin on day one: no behaviour, just the mapping.Post::find($id) and returns the model directly, and replace the return value with your entity. If the JSON payload is unchanged, the mapping is correct and you can start moving logic out of the controller.Entity as the boundary object. The most valuable pattern is letting entities cross layer boundaries (controller → service → response) while Eloquent models stay inside repositories. Controllers and jobs should never type-hint a Model; they should type-hint your entity. This is what makes the package worth the dependency rather than just using Eloquent directly.
Repository returns entities, not models. Wrap queries so callers get entities back:
final class PostRepository
{
public function find(int $id): ?PostEntity
{
return PostEntity::from(Post::find($id));
}
}
Keep the repository the only place that touches Eloquent, so swapping to a different persistence layer later doesn't ripple.
Mocking in tests (new in 0.5.3). Eloquentity was final up to 0.5.2, which forced teams to either instantiate real entities or wrap them behind an interface to mock them. As of 0.5.3 you can do:
$entity = Mockery::mock(PostEntity::class);
$entity->shouldReceive('title')->andReturn('Fake');
Prefer this only for entities with behaviour. For plain data carriers, construct a real instance — it is cheaper and less brittle than a mock.
Mapping through relationships. When an entity exposes relations (author, tags), eager-load on the model before constructing the entity. The mapping layer typically reads already-loaded relations, so Post::with('author')->find($id) is the pattern; lazy loading inside the entity is an N+1 waiting to happen.
Serialization at the edge. If entities implement JsonSerializable or toArray(), keep the shape identical to what the raw model produced during the migration period. That lets you cut over controller by controller instead of in one big-bang release.
Use in queued jobs. Because entities are plain PHP objects, they are convenient job payloads — but check how the entity serializes. If it holds a reference back to an Eloquent model, SerializesModels behaviour may not apply cleanly; pass the model's primary key into the job and re-hydrate the entity in handle().
final removal is a double-edged sword. 0.5.3 dropped final from Eloquentity so it can be mocked. That also means the class is now openly extensible, and Mockery will happily mock any method on it. Do not treat the absence of final as an invitation to build an inheritance hierarchy on top of the base class — compose instead, or you will re-create the god-object problem the package is meant to solve.0.5.x releases have shipped small, sharp changes (the final removal is a typical example — harmless for callers, but it changes what is mockable). Read each changelog before bumping; a 0.5.4 could just as easily make a method protected or rename a mapping method.$casts apply to the model, not automatically to the entity. If your entity exposes created_at, verify it arrives as a Carbon instance and not a raw string, and add explicit casting in the entity if the package doesn't inherit it.php artisan vendor:publish) rather than relying on defaults — naming conventions for the model behind an entity are the usual thing to override, and a mismatch surfaces as a confusing "class not found" at runtime rather than at boot.$model->getAttributes()) immediately before the mapping call. Most failures are a hidden/$appends attribute on the model or a relation that wasn't eager-loaded, not a bug in the package.0.5.3 these are more accessible than before simply because the class isn't final — you can subclass to add behaviour rather than patching the package.How can I help you explore Laravel packages today?