healthengine/laravel-better-models
A Laravel package that enhances Eloquent models with helpful conventions and utilities for cleaner, more maintainable model code. Useful for teams standardizing model behavior and reducing boilerplate in Laravel applications.
Read the source before you install it. This package is archived (last release 2020-09-15), has zero dependents, and predates Laravel's native strict-mode features. For a package this small, the ideas in src/ are worth more than the dependency. Clone it, read the base model class and any traits, and decide whether you want the package or a 40-line copy in app/Support/.
composer.json and look at the illuminate/* constraints. A 2020 release targets Laravel 6/7. Confirm what it supports before running composer require healthengine/laravel-better-models.Model subclass and/or a trait in src/, plus any publishable config (php artisan vendor:publish). "Better model defaults" almost always means: a base class that sets $guarded, $casts, key/timestamp conventions, and possibly stricter attribute/lazy-load behaviour.app/Models/BaseModel.php that extends the package class (or uses the trait). Every model extends your BaseModel, never the vendor class directly. This is the single most important step — it gives you a one-file exit path when the package rots.BaseModel, run the suite, and diff the behaviour: mass assignment, toArray() output, date serialization, generated SQL. Only then migrate the rest.First realistic use case: you have 40 models each re-declaring $guarded = [], $casts = ['created_at' => 'datetime'], and a Uuid boot trait. This package centralises that. Start there, not with the strictness features.
Base model as the single seam. Most day-to-day usage is "write models the same way you always did, but extend BaseModel." Keep that seam thin: only put things in BaseModel that are true for every model in your app. Anything conditional belongs in a trait applied per model.
Incremental migration, model by model. Change extends Model → extends BaseModel in small batches grouped by domain (billing, auth, catalogue), running tests between batches. A global find-and-replace across 200 models will produce a wall of failures you can't attribute.
Compensate for permissive mass assignment. If the defaults include $guarded = [], the correct pairing is FormRequest validation plus explicit $fillable on anything touching money, identity, or permissions (User, Payment, Role). Never let $request->all() reach a permissive model.
Use the defaults to shrink model files. Once $casts and key conventions are inherited, delete the boilerplate from individual models rather than leaving dead declarations that shadow the base class — duplicated $casts arrays are a classic source of "why isn't my default applying?"
Keep query helpers out of controllers. If the package ships scopes or finder helpers, call them from repositories/actions/services so the model layer stays testable and you can swap the implementation later.
Test the seam, not just the models. Add one test that instantiates every model and asserts toArray() on a factory-made instance. That single test catches most global-default regressions (serialization shape, cast errors, missing attributes) in one place.
When writing your own package, depend on illuminate/database directly and copy the relevant defaults. Do not take a dependency on an archived package from code you intend to publish.
The package's reason for existing is now in core. Laravel 8 added Model::preventLazyLoading() / preventAccessingMissingAttributes(); Laravel 9 consolidated these into Model::shouldBeStrict(). If the package's value proposition is strictness, your migration path is to drop it and add this to a service provider:
Model::shouldBeStrict(! app()->isProduction());
Do that instead of carrying an unmaintained dependency into Laravel 10/11.
Global defaults are global. Changing the base class changes every model's serialization, mass assignment, and hydration. Observers, Nova/Filament resources, queue payloads, and API snapshot tests all silently depend on toArray() shape. Run your API contract tests before merging.
$guarded = [] is a security decision, not a convenience. Audit it explicitly. If the package sets it globally, add a lint/CI check that flags models with permissive guarding that also accept user input directly.
Date and timezone casts. A base $casts entry for datetime changes JSON output and can shift timezones if $dateFormat or app.timezone differ. Check one endpoint's response body before and after.
UUID/ULID key defaults need matching migrations. If the base class sets $keyType = 'string' and $incrementing = false, every table must use uuid/char(36) columns and related foreign keys must match. Migrations are the failure point, not the models.
Debugging checklist when a model misbehaves after adoption:
(new MyModel)->getAttributes() and ->getCasts() — confirm inherited defaults actually landed.DB::listen(...) or Telescope — confirm no surprise extra queries from lazy-load guards.composer why-not healthengine/laravel-better-models — confirm the constraint that's blocking an upgrade.$casts/$guarded overrides, it doesn't merge.Extend, don't patch. If you need different behaviour, override the method in App\Models\BaseModel. Never edit vendor/. If you must fork, move the code into app/ and drop the dependency — with zero dependents and no maintainer, you are the maintainer the moment you install it.
Pin the version and set a review date. "healthengine/laravel-better-models": "1.x" with a calendar reminder to re-evaluate at your next Laravel major. Realistically, plan to remove it within one upgrade cycle.
How can I help you explore Laravel packages today?