Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Generator Laravel Package

popo/generator

popo/generator scaffolds Plain Old PHP Object (POPO) classes from your database tables, reducing boilerplate and helping Laravel developers build a simple, maintainable data layer.

View on GitHub
Deep Wiki
Context7

Getting Started

popo/generator is a build-time tool: you describe your data structures in YAML, it emits plain PHP classes. Treat the YAML as the source of truth and the generated classes as committed artifacts — not something you edit.

composer require --dev popo/generator

It ships a CLI entry point (typically vendor/bin/popo); if you pull in the Laravel bridge you also get a service provider and an artisan command. Confirm the exact binary name and config keys in the repo README before wiring up CI — this package moves fast (recent release) and option names have shifted between versions.

First three steps:

  1. Read the README and the examples/ directory in the repo first. The YAML dialect is package-specific; the schema syntax is where you will spend 90% of your learning time, not the generated PHP.
  2. Publish the config (php artisan vendor:publish --tag=popo, or edit config/popo.php) and set two things: where schemas live (popo/ or resources/popo/) and the output path/namespace (app/Dto, App\Dto).
  3. Write the smallest possible schema and generate:
# popo/user.yml
App\Dto\User:
  properties:
    id: int
    email: string
    name: string?
    roles:
      type: array
      of: string
vendor/bin/popo generate
# or
php artisan popo:generate

Then composer dump-autoload if you introduced a new namespace, and inspect the generated file. Do this on day one — the shape of the output (public properties vs. getters/setters, whether fromArray/toArray exist, how nullables are typed) determines whether this package fits your codebase.

First real use case: a third-party webhook or API response. Instead of letting $request->all() arrays flow through controllers, jobs, and services, hydrate a generated DTO at the boundary and pass the object inward. You get autocomplete, a single place to see the payload shape, and a diff in code review when the vendor changes their contract.

Implementation Patterns

Hydrate at the boundary, not in the domain. The generated class is a dumb container — do the mapping in a controller, form request, or a small *Mapper class:

final class UserController
{
    public function store(StoreUserRequest $request): UserResource
    {
        $dto = UserDto::fromArray($request->validated());

        $user = $this->createUser->handle($dto);

        return new UserResource($user);
    }
}

Eloquent → DTO. UserDto::fromArray($user->toArray()) works for flat models but silently drops relations and mangles dates. Be explicit:

$dto = UserDto::fromArray([
    'id'    => $user->id,
    'email' => $user->email,
    'name'  => $user->name,
    'roles' => $user->roles->pluck('name')->all(),
]);

Nested structures and collections. For array of SomeDto, most generators hydrate only the top level. Write a tiny helper rather than hand-rolling loops everywhere:

$dto->items = array_map(
    fn (array $row) => LineItemDto::fromArray($row),
    $payload['items']
);

Queue payloads. Generated DTOs are not automatically serialization-safe. Either convert to an array when constructing the job (new ProcessOrder($dto->toArray())) or implement __serialize/__unserialize on the job. Prefer passing scalars into the job and rebuilding the DTO in handle() — it keeps your queue contract stable when the schema evolves.

API resources. Let the DTO feed JsonResource rather than returning it directly, so you control field visibility and naming:

return UserResource::make($dto); // toArray() inside the resource

Regeneration as a workflow, not a one-off. Add a composer script and a CI check so nobody edits generated code by hand:

"scripts": {
    "popo": "popo generate",
    "popo:check": "popo generate && git diff --exit-code app/Dto"
}

Running generate in CI and failing on a dirty diff catches both "schema changed but classes not regenerated" and "someone hand-edited a generated class."

Testing. Snapshot the generated files (assertFileExists + hash, or a golden-file comparison), and unit-test the mapper functions — not the generated getters. Test the round trip fromArray($x)->toArray() === $x, because that's where nullable and default-value bugs surface.

Layering. Keep DTOs out of your Eloquent models' business logic. They should be usable from a plain PHPUnit test with no Laravel bootstrapping — that's the whole point of "POPO."

Gotchas and Tips

  • Never hand-edit generated classes. Regeneration overwrites them silently. If you need behaviour, put it in a base class, a trait, or a wrapper — check the package's template/stub overrides for your extension point.
  • Schema-to-type mapping is the fragile part. YAML has no real type system; verify how int, float, bool, string?, and nested class references actually render in PHP. A ? that produces ?string versus string|null matters for static analysis and for strict_types callers.
  • Default values and missing keys. Confirm whether an absent key becomes null, an uninitialized typed property, or a constructor error. This is the #1 runtime surprise when a vendor omits an optional field.
  • Validation is not generated. The DTO guarantees shape, not correctness. Keep Laravel's FormRequest rules as the validation layer; don't try to encode constraints in the YAML.
  • Config cache. After changing config/popo.php, php artisan config:clear — otherwise the generator writes to the old path and you'll debug a phantom PSR-4 error.
  • Autoload and IDE index. New classes need composer dump-autoload and, in PhpStorm, a re-index. If you ever "can't find the class," check the namespace in the YAML against your PSR-4 root.
  • Exclude from coverage, not from analysis. Add app/Dto to phpunit.xml coverage excludes to keep metrics honest, but keep it in PHPStan/Psalm — generated code often reveals nullability mistakes in your own mappers.
  • Debugging generate failures. Run with a verbose/dry-run flag if available, and keep one class per YAML file. Large schemas produce error messages that don't tell you which node failed; small files make bisecting trivial. YAML anchors/<<: merges help when many DTOs share a shape.
  • Don't generate in production deploys. This is a dev dependency. Generate locally, commit the output, deploy the PHP.
  • Know when not to use it. If you need runtime hydration, validation, lazy properties, and transformation in one object, spatie/laravel-data solves that with less ceremony. popo/generator wins when you want zero runtime dependency, plain objects, and a reviewable YAML contract for structures you don't own (vendor webhooks, legacy APIs, event payloads).
  • Small ecosystem risk. 20 stars and near-zero dependents means few eyes on edge cases; pin the version, and budget time to read the generator's source when a type mapping surprises you.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource