jumilla/source-generator
A source code generation toolkit for Laravel and PHP projects that helps you build and output PHP source files programmatically. Useful for scaffolding, automation, and reducing repetitive boilerplate in your development workflow.
Install and verify. The package is archived but still resolvable from Packagist, so composer require jumilla/source-generator works — expect Composer to print an "abandoned" warning. Check composer.json in the repo for the PHP constraint before pulling it into a modern project; generators of this vintage are often pinned to ^7.x and may or may not run cleanly on 8.2+. If the constraint blocks you, require it via a VCS repository and alias the version, or fork it into packages/ and use a path repository.
composer require jumilla/source-generator
Where to look first. There is no meaningful documentation, so the source is your manual. Open src/ and skim in this order:
generate(), getSource(), __toString(), or similar) — that is your output boundary.tests/ directory. Fixture files showing "input chain → expected PHP output" are worth more than any README here.Budget an hour to read the builder classes end-to-end before writing code. Because the library is unmaintained, you are effectively adopting the source, not just the API.
First use case. The natural fit is a class shape you already describe in data — a DTO, an entity, a repository interface, a set of value objects derived from a schema. The canonical first experiment: build one small DTO by hand in the chain API, emit it, diff the generated file against a hand-written version. That tells you immediately how close the output is to your house style and whether you need a formatting pass.
// Shape only — consult src/ for exact method names.
$source = $generator
->namespace('App\Dto')
->class('UserDto')
->property('id', 'int')
->property('email', 'string')
->generate();
file_put_contents(app_path('Dto/UserDto.php'), $source);
If that file is syntactically valid and diffs cleanly against your hand-written class, the package is viable for your scaffolding job. If not, stop here — see the alternatives note in Gotchas.
Treat it as a template engine, not an AST. You are producing a string. That means everything the generator does not model — declare(strict_types=1), use imports, attributes, docblocks, readonly — you either supply through the chain or prepend/append yourself. Structure your own code around that reality:
final class ClassWriter
{
public function __construct(private SourceGenerator $gen) {}
public function write(ClassSpec $spec): string
{
return "<?php\n\ndeclare(strict_types=1);\n\n"
. $this->gen
->namespace($spec->namespace)
->class($spec->name)
// ... chain the members from your spec object
->generate();
}
}
Keep the generator behind a thin wrapper like this. It gives you one place to swap the library out later, one place to normalize formatting, and it keeps fluent chains from sprawling across your codebase.
Drive it from a spec object, not from scattered chains. The ergonomic win of a method chain is that it reads well locally; the ergonomic failure is that chains embedded in service methods are untestable and unreadable at length. Convert your input (a config array, a DB schema row, a JSON descriptor) into an intermediate spec — a plain PHP object or array — then have a single builder class walk the spec and emit. This makes regeneration deterministic and makes the spec the thing you version-control and review.
Use it for Artisan scaffolding commands. This is the strongest day-to-day pattern:
class MakeDtoCommand extends Command
{
protected $signature = 'make:dto {name} {--force}';
public function handle(): int
{
$path = app_path('Dto/' . $this->argument('name') . '.php');
if (file_exists($path) && ! $this->option('force')) {
$this->error('Already exists.'); return self::FAILURE;
}
file_put_contents($path, $this->writeDto($this->argument('name')));
$this->info("Created {$path}");
return self::SUCCESS;
}
}
Stamp every generated file with a // @generated by make:dto — do not edit header so humans and future tooling know the file is disposable. Pair the writer with --force and a --dry-run flag; a generator without a dry-run is a generator nobody trusts.
Bulk generation with a manifest. For schema-driven work (generating DTOs for all tables, all message types in a queue contract, all endpoints in an OpenAPI spec), do not overwrite in place. Emit to a temp directory, then diff-copy or use git diff as the review gate. Regeneration should be a reviewable commit, not a silent side effect.
Test the output as a string, and validate it by parsing. Assert on the generated source with snapshot-style comparisons so formatting regressions are visible. Where practical, feed the output to nikic/php-parser's parser in your test suite — a green parse proves you did not emit a syntax error, which is the failure mode that matters most:
$code = (new ClassWriter($gen))->write($spec);
$this->assertStringContainsString('class UserDto', $code);
(new ParserFactory())->createForNewestSupportedLanguage()
->parse($code); // throws on invalid PHP
Compose for larger graphs. If the package models methods and parameters, you likely have a natural "emit a method, then attach it to a class" flow. Build small reusable emit helpers (one per member kind) and assemble classes from those, rather than maintaining one 200-line fluent chain.
The package is archived — treat maintenance as yours. No security patches, no PHP version bumps, no issue triage. Pin the exact version in composer.json, and if you depend on it beyond a throwaway script, fork it internally so you can patch. Composer's abandoned-package warning is a real signal here, not noise.
Seriously evaluate nette/php-generator or nikic/php-parser first. Nette's generator is the maintained, widely-used equivalent of exactly this functionality, with far better coverage of modern PHP (attributes, union types, readonly, enums). nikic/php-parser's pretty printer handles emission from an AST. If your only objection to those is that they are heavier than a 4-star method-chain library, note that the "lightweight" advantage evaporates the first time you need readonly or an attribute and this package cannot express it. Reaching this package's builder API through a ClassWriter wrapper (above) makes that switch cheap.
Formatting will not match PSR-12 out of the box. Expect missing blank lines between members, inconsistent indentation, or the absence of a trailing newline. Do not fix this by hand — run the output through a real formatter as a pipeline step. php-cs-fixer with a temp file, or better, parse-and-reprint with nikic/php-parser, gives you deterministic style regardless of what the generator emits. Decide this on day one; retrofitting formatting onto a working generator is tedious.
String values and default arguments are the classic injection bug. Anywhere you pass a literal (a default property value, a constant, a string in a docblock) you are concatenating into PHP source. Quote and escape deliberately — var_export($value, true) is the safe default for scalars. A generator that emits unescaped user input is a code-execution path on regeneration. Audit how the chain handles string arguments before feeding it anything dynamic.
Namespaces and imports are usually your problem, not the generator's. Most small generators emit fully-qualified names or expect you to pass them pre-resolved. If the output is littered with \App\Foo\Bar, either accept it (FQCNs are valid and explicit) or post-process. Do not assume an auto-import pass exists; read the builder to confirm.
Regeneration destroys manual edits. There is no round-trip story here — this is one-way emission. Enforce the @generated header and keep generated and hand-written classes in separate namespaces/directories. Mixing them is how teams end up with a --force flag nobody dares use.
Debugging a wrong chain is awkward because failures are silent. Unsupported or misspelled members may produce no output rather than an exception, since fluent builders frequently just ignore unknown state. When a generated class is missing a member, first var_dump the intermediate object state before calling generate() — the problem is almost always that a chain call did not set the property the emitter reads, not that emission is broken. Keeping your own spec object and emitting from it (the pattern above) sidesteps this entirely by making the input inspectable.
Extension points are limited, so extend by wrapping. Do not subclass the generator to add behavior; subclass your ClassWriter wrapper instead. The library is unmaintained, so internal methods carry no backward-compatibility guarantee across a fork, and you want your customization layer independent of its internals.
How can I help you explore Laravel packages today?