laimoon/model-updater
laimoon/model-updater is a Laravel package that provides a clean, reusable way to update Eloquent models. It helps reduce boilerplate by centralizing model update logic, keeping controllers and services tidy while preserving consistent persistence behavior.
This package is archived (last release Jan 2020) and pins an era of Laravel around 5.7–6.x. Before writing any application code, open the repository and read the source—the README is thin, and the day‑to‑day contract lives in src/. Confirm two things up front: which Laravel/PHP versions the constraints allow, and whether the updater is driven by a trait, an abstract base class, or a container binding.
Minimal wiring:
composer require laimoon/model-updater
If a service provider is auto-discovered, no config/app.php edit is needed on Laravel 5.5+. If not, register Laimoon\ModelUpdater\ModelUpdaterServiceProvider manually.
The first useful use case is replacing the classic anti‑pattern:
// before: every column on the request is fair game
$user->update($request->all());
// after: an explicit, per-action class that whitelists what can change
$updater = new UpdateUserProfile($user);
$updater->update($request->validated());
Start with one model and one action (profile update or settings save) where you already know the exact set of mutable attributes. Treat the updater as a boundary object: request data in, persisted model out. Once that path is green, expand to create/update/delete actions that share mapping logic.
Because the package is unmaintained, budget half a day to either (a) pin Laravel to a version it actually supports, or (b) copy the pattern into app/Support/Updaters/ and drop the dependency. For a 40‑star, zero‑dependent package, (b) is usually the right call for a greenfield project.
One updater per action, not per model. Model‑wide updaters tend to accumulate if ($action === ...) branches. Separate classes (CreateOrder, UpdateOrderShipping, ArchiveOrder) keep the whitelist obvious and make tests trivial. If the package only ships a single base class, subclass it per action and override the attribute map.
Keep validation in FormRequests, keep mapping in the updater. The updater should not re‑implement required|string|max:255. Let the controller pass $request->validated() and let the updater decide which keys are actually assignable. This means a field can be validated but deliberately ignored—useful for fields set by observers or policies.
Compose with policies and gates at the call site. Updaters are not authorization. Check $this->authorize('update', $order) in the controller or FormRequest, then hand the model to the updater. Centralizing both concerns in the updater hides permission checks from static analysis and from Laravel's policy tooling.
Use it as a transaction boundary when multiple models are involved.
DB::transaction(function () use ($request) {
(new CreateInvoice($customer))->handle($request->validated());
(new AttachLineItems($invoice))->handle($request->input('items'));
});
If the package fires saving/saved events (it almost certainly delegates to Eloquent under the hood), observers, audit logs, and model event listeners continue to work. Verify this before relying on it—some updaters use fill()->saveQuietly() or raw query builder updates.
Testing. Instantiate the updater directly with a model and an array; assert the database state with assertDatabaseHas. No HTTP layer required. For attribute‑whitelist behaviour, write a test that passes an extra key and asserts it is not persisted—this is the regression guard you want when someone later loosens the map.
Integration with API resources / DTOs. If you already hydrate DTOs from requests, map the DTO to the array shape the updater expects at the edge of your controller. Don't pass DTOs deep into the package; the coupling is not worth it and makes the archived dependency harder to remove later.
Laravel compatibility is the headline risk. A 2020 package predates Laravel 8's model changes, PHP 8 typed properties, and Model::preventLazyLoading(). Expect deprecation notices at minimum. Run composer why-not laravel/framework before committing; if the constraint blocks your Laravel version, stop here.
Mass assignment interacts in non-obvious ways. If the updater uses fill(), $fillable/$guarded still apply on top of any internal whitelist. Two layers of filtering is fine, but debugging "why didn't this field save" means checking both. Prefer forceFill() only when you are certain the updater is the sole writer.
Events vs. quiet saves. Confirm whether saving, updating, and updated fire. If the updater runs inside a queue job or an import command and you rely on observers to recalculate aggregates, a quiet save will silently skip them. Add a failing test that registers an observer and asserts it ran.
Cast and mutator ordering. Dates, JSON casts, and custom accessors are applied by Eloquent, but if the updater pre‑processes values (trimming, nulling empty strings), it can bypass a cast. Keep preprocessing in the FormRequest via prepareForValidation() so the updater receives already‑normalized scalars.
No updates since 2020 = no security patches. The package is small enough that the risk is low, but if it pulls in any transitive dependency, pin it. Run composer audit after install.
Extension points are likely limited. Expect a single abstract method or a protected $attributes array. If you need hooks (e.g., post‑save callbacks), wrap the updater rather than fighting its inheritance chain:
class AuditedUpdateProfile
{
public function __construct(private UpdateUserProfile $inner) {}
public function update(array $data): User
{
$user = $this->inner->update($data);
activity()->performedOn($user)->log('profile updated');
return $user;
}
}
Debugging tip. Because the flow is request → array → updater → Eloquent, log the array immediately before the update call in a local environment. Most "the updater did nothing" reports trace back to the array being empty after validated()—usually a mismatched name attribute in the FormRequest.
Migration path. Treat the package as a temporary seam. Define your own App\Support\Updater interface with a single update(Model $model, array $data): Model method, implement it once against the package, and switch the container binding to a hand‑rolled implementation when you need to upgrade Laravel. That gives you the leverage of the pattern without permanent lock‑in to an archived dependency.
How can I help you explore Laravel packages today?