znck/plug
A collection of pluggable Eloquent traits for Laravel models, adding features like BelongsToThrough, UUID keys, and fixes for belongsTo/morphTo relations. Just use a trait and it boots automatically—no config needed.
Architecture fit
znck/plug is a thin layer of Eloquent traits — no service provider, no config, no migrations, no facade. That is about as low-ceremony as a PHP dependency gets, and it fits our architecture in the narrow sense: if we are on Eloquent, use SomeTrait; on a model is a pattern we already use.
The fit degrades on inspection, though. Traits are a class-level composition mechanism: once applied, behavior is bound to the model for all call sites, cannot be swapped per-request, and collides badly when two traits touch the same lifecycle hook (saving, creating, getAttribute, setAttribute). For a package whose entire value proposition is "many small traits you can mix in," the composition model is the risk model.
Integration feasibility
Technically high, practically low.
illuminate/* constraints. On a modern Laravel/PHP stack, composer require will most likely fail resolution outright.Technical risk
| Risk | Severity | Note |
|---|---|---|
| Abandonment | High | Last release 2016-09-27; no commits path to modern Laravel |
| Single-maintainer / bus factor | High | No triage, no disclosure channel, no CVE process |
| Zero adoption signal | High | 13 stars, 0 dependents — no field testing, no Stack Overflow corpus |
| Dead CI evidence | Medium | StyleCI / CircleCI / Coveralls badges; CircleCI 1.0 was retired in 2018, so the "green build" is not a live signal |
| Lifecycle-hook conflicts | Medium | Traits that hook saving/creating interact with observers, soft deletes, and event ordering |
| Event bypass | Medium | insert()/upsert()/query-builder writes skip model events entirely, producing inconsistent rows |
| Security exposure | Low–Medium | MIT, no network surface, but unpatched dependencies are a hygiene flag at audit time |
Recommendation (TPM position): Do not adopt znck/plug as a runtime Composer dependency. Treat it as a reference implementation. Vendor the specific traits we need into our own namespace under our own test coverage, or reimplement them outright.
Key questions to close before committing engineering time
Stack fit
Eloquent-only, framework-version-coupled through illuminate/* constraints. No infrastructure footprint: no queue, no cache dependency, no external service. That makes it easy to evaluate in isolation and easy to rip out — the main reason this is a low-blast-radius decision rather than a strategic one.
Migration path — three options, ranked
app/Support/Eloquent/Concerns/, re-namespace, retain the MIT header, add unit + integration tests. Behavior becomes ours, visible, covered.composer.json constraints, tag an internal version, require via repositories VCS entry. Only justified if we need a large subset and expect upstream movement — and there has been none in ~8 years, so upstream-sync value is effectively zero.Default to A, fall back to C per trait where the original is doing something we would not write today.
Compatibility checks per trait
boot{TraitName}) — confirm they still fire and in what order relative to other traits.getAttribute / setAttribute / casts — these internals have shifted across Laravel majors.array_get, str_*) removed in later Laravel versions.$fillable / mass-assignment interaction if the trait sets attributes.SoftDeletes, observers, and Model::withoutEvents().Sequencing
insert() are handled explicitly.composer require znck/plug.Maintenance
Upstream maintenance is zero and will remain zero. The real question is only where the maintenance burden sits: behind a composer.json line (invisible, surprises us at upgrade time) or in our own repo (visible, owned, testable). Vendoring makes the cost legible; a dependency hides it.
Budget ~0.5–2 engineer-days for the initial port, test coverage, and ADR; then a few hours per Laravel major to re-validate.
Support
No maintainer SLA, no issue triage, no security contact, single contributor. We are the support organization for anything we ship that depends on this code. That is acceptable for vendored traits we own; it is not acceptable for a black-box dependency.
Scaling
Runtime overhead is whatever the trait's own logic costs — traits themselves add none. The scaling risk is in the logic:
SELECT per save become write-path N+1s and, under concurrency, a read-modify-write race. At high insert volume this is a contention and duplicate-data problem, not a performance problem.saving and perform I/O lengthen transaction hold times.Benchmark any trait that issues a query during save before it lands on a hot model.
Failure modes
| Mode | Detection | Impact |
|---|---|---|
| Composer resolution failure on Laravel upgrade | CI, immediately | Build break — loud, cheap |
| Silent behavior drift post-upgrade (hook no longer fires) | Only with explicit assertions | Bad data: rows missing slug/UUID — quiet, expensive |
| Event-order conflict with observers / soft deletes | Integration test | Inconsistent lifecycle behavior |
Bulk insert/upsert bypasses trait logic |
Seeder/import test | Heterogeneous rows, downstream null-key bugs |
$fillable / mass-assignment collision |
Feature test | Unexpected overwrites |
| Trait method-name collision across models | Static analysis / review | Fatal at class load |
The asymmetry matters: build failures are harmless, silent drift is not. That argues for assertion-style tests ("after save, the row has a slug") rather than mocking the trait.
Ramp-up
Near zero for any engineer fluent in Eloquent — traits are a familiar idiom and the source is short enough to read in full. The actual ramp-up cost is organizational: engineers must be told the package is vendored and deliberately not a dependency, or someone will "helpfully" add it back. That is what the ADR and the namespace choice (App\Support\Eloquent\Concerns) are for.
How can I help you explore Laravel packages today?