scrumble-nl/popo
Popo is a Laravel package for generating plain old PHP objects (POPOs) from Eloquent models, giving you lightweight, framework-independent data containers that keep application layers clean and decoupled, with simple Artisan commands and an expressive API.
What the package is (working assumption): scrumble-nl/popo is a Laravel-oriented generator/abstraction for Plain Old PHP Objects — typed data-transfer objects used to move validated data between HTTP, service, and persistence layers instead of associative arrays. The evaluation below is written on that assumption and must be confirmed in the first diligence step; if it turns out to be a domain-model/presenter helper instead, the integration plan holds but the value case shifts from "type safety at boundaries" to "serialization consistency."
What changed in 2.0.2 (delta review). The release removes the laravel/helpers dependency — snake_case() is replaced by Str::snake() — because laravel/helpers defines array_first()/array_last() with Laravel's two-argument signature, which collides with PHP 8.5's native one-argument functions. No API changes are declared. Two implications matter more than the diff itself:
Architecture fit. Unchanged and still the package's main virtue. POPO/DTO layers fit cleanly into a conventional Laravel layering (FormRequest → DTO → Action/Service → Eloquent). This is additive: it does not touch routing, the container, or the ORM, so blast radius is low. It fits best where we already have a boundary problem — fat controllers, array-passing between jobs/listeners, payloads crossing into queues. It fits poorly in codebases that are deliberately array-first or that already standardize on Spatie Laravel Data or plain readonly classes; adopting it there creates a second, competing convention. Nothing in 2.0.2 changes this.
Integration feasibility. Still mechanically easy: Composer install, optional service provider auto-discovery, Artisan generator commands, no infrastructure or migrations. 2.0.2 slightly improves feasibility by shrinking the dependency graph — one fewer transitive package to audit, and the removed package is a global-function-definition package, which is exactly the kind of dependency that causes namespace collisions in large applications. The feasibility question remains organizational (will the team keep generated classes in sync with schema), not technical.
Technical risk — materially reduced, but not resolved. The prior assessment called this the dominant concern. The release moves the needle on maintenance and competence; it does not move it on adoption breadth:
array_get(), str_*(), or snake_case() via popo's transitive dependency will now get fatal errors at call sites unrelated to the package. This is the classic "removing a dependency is a breaking change for someone" case. The release notes document the remedy (composer require laravel/helpers), which is good practice, but detection is on us — nothing will fail at composer update time.composer.json constraints match what CI actually instantiates; a matrix listed in prose can be a summary of a narrower job list.config:cache safety.Key questions for the diligence gate (revised).
composer.json constrain Laravel/PHP to versions we run?scrumble-nl accept our patches?readonly properties?Stack fit. Best fit is a Laravel 10/11/12 application on PHP 8.2+ using typed properties or enums, with queue jobs and/or an internal API surface. Worst fit is a legacy app on PHP 7.4/8.0 with array-based conventions, or one already standardized on Spatie Laravel Data. 2.0.2's PHP 8.5 readiness extends the viable lifespan of the dependency by at least one PHP minor, which modestly improves the fit case for teams planning to stay on current PHP.
Migration path. Do not migrate. Introduce incrementally and only at new boundaries:
App\Data\Popo / DtoContract) so call sites reference our symbol, not theirs. This keeps swap-to-in-house a one-file change.Compatibility. Version constraints are now answered by the release notes; the remaining surface to verify explicitly: PHPUnit/Pest version for test generation, and static analysis (PHPStan/Larastan level) — generated classes must pass our current gate or they create noise that engineers suppress, which erodes the whole point. Also verify behavior under php artisan config:cache, route caching, and Laravel Octane.
New in 2.0.2 — the helper-clash pre-flight check. Before upgrading, run a repo-wide grep for global-helper usage (array_get, array_first, array_last, str_*, snake_case, camel_case, studly_case) and confirm each is either unused, provided by laravel/helpers directly, or replaced with Str::/Arr:: equivalents. This is a 30-minute check that prevents a production fatal on upgrade, and it is cheap enough to fold into the same PR that bumps the pin. Note the failure mode is asymmetric: it will not appear during composer update and may only surface on an untested code path.
Sequencing (2-week timebox, revised).
readonly DTO base class.Maintenance. Low runtime maintenance, moderate ownership maintenance — but the ownership burden is now demonstrably shared with an engaged upstream, which lowers it in practice. There is nothing to operate: no service, no daemon, no storage. The package remains part of the build (PHP and Laravel upgrades, static-analysis level bumps, generator output changes), so keep an explicit internal owner, an exact pin in composer.json ("x.y.z", not ^), and a quarterly review entry. 2.0.2 is also a reminder to read changelogs rather than trust the pin: the "no API changes" release did remove a dependency whose absence is breaking for some consumers.
Support. Still no commercial support, no SLA, no visible security advisory feed. Support path is: read the source (small codebase), patch our fork, upstream a PR. Escalation is internal. The release demonstrates the upstream loop functions — a collision with a new PHP minor was identified and fixed rather than left to rot — which is the behaviour we actually care about from a small maintainer. Acceptable for a dev-time code generator; still unacceptable for anything in the request path we cannot replace in a day.
Scaling. No runtime scaling characteristics, which remains a genuine advantage: generated classes add no request-time overhead beyond autoloading, and file count grows linearly with domain size. Removing laravel/helpers slightly improves cold-start and memory characteristics for very large apps by eliminating a package that eagerly defines global functions. The real scaling risk is cognitive — a generated DTO layer that is not consistently used becomes a third convention alongside arrays and Eloquent models. Mitigate with the published internal standard and a lint/review rule.
Failure modes to plan for.
composer require laravel/helpers in the upgrade runbook.@phpstan-ignore to generated code, then to adjacent code. Mitigate by requiring generated output to pass at current level before adoption.Ramp-up. Low and unchanged: an experienced Laravel dev is productive in half a day, and the concepts (typed objects at boundaries) are transferable if we later swap libraries. Cost is concentrated in the first two adopters, who must write the internal standard, the wrapper, and the upgrade runbook (which now includes the helper check). Do not spread adoption wider than one team until that documentation exists.
TPM recommendation. Fund the two-week evaluation, and raise the probability of adoption from roughly a coin flip to modestly favored. The case has genuinely improved: version constraints and CI are answered, the dependency footprint shrank, and upstream is demonstrably tracking PHP minor releases — the single strongest counter-signal to the abandonment thesis. The case has not been made, however, on the one thing that would justify platform-wide commitment: adoption breadth is still effectively zero, so we will be the ones discovering the enum, nullable-nested-DTO, and Octane behaviours. Proceed as vendored and wrapped, pin exactly, complete the now-shorter audit, and hold the in-house readonly DTO base class as the documented fallback.
How can I help you explore Laravel packages today?