orchestra/canvas-core
Orchestra Canvas Core is the code generator builder behind Laravel scaffolding. It provides reusable generators and APIs to create application and package classes, migrations, configs, and structures, helping developers automate boilerplate with consistent, extensible output.
Understand what you are installing. orchestra/canvas-core ships no commands. If you want php artisan make:model to write into a package namespace instead of app/, you want the sibling package orchestra/canvas. You install core directly only when you are authoring generators or a custom Preset — i.e. when you are building the scaffolding layer for your own package or a mono-repo module.
composer require --dev orchestra/canvas-core
require-dev is deliberate. This is a build-time/CLI concern with no production runtime footprint — keep it out of anything deployed.
Where to look first, in this order:
src/Presets/Preset.php — the abstract class that resolves namespaces and paths (app namespace, model/factory/seeder namespaces, migration path, shared stub path). This is the contract that decides where generated files land.src/Concerns/CodeGenerators.php — the trait your generator commands will use; it wires up generator resolution and stub lookup.src/Contracts/ — GeneratesCode and GeneratesCodeListener are the extension points (listeners get generatingCode() before and afterCodeHasBeenGenerated() after).src/Generators/ — the concrete generator classes, which are thin wrappers over Laravel's own Illuminate\Console\GeneratorCommand / MigrationGeneratorCommand.First use case — build one command, not a suite. Pick a single repeated scaffold you actually hand-roll today (e.g. "Action + FormRequest + test + migration for a new domain module"). Subclass Preset to point at your package's src/ and namespace:
use Orchestra\Canvas\Core\Presets\Preset;
final class ModulePreset extends Preset
{
// Implement the abstract namespace/path resolvers for models,
// factories, seeders, and migrations. Confirm the exact abstract
// signatures in source — there is no published API doc.
}
Because the README is the only documentation, discover the surface with reflection rather than guessing:
php -r "require 'vendor/autoload.php';
print_r(array_map(fn(\ReflectionMethod \$m) => \$m->getName(),
(new \ReflectionClass(\Orchestra\Canvas\Core\Presets\Preset::class))
->getMethods(\ReflectionMethod::IS_ABSTRACT)));"
Then subclass a generator command, register it in a service provider ($this->commands([...])), and run it against a throwaway testbench app before pointing it at real code.
Pattern 1 — thin wrapper, not a fork. Your generators should extend the core's generator commands and override stubs and conventions only. Never copy generator logic; the core tracks Laravel internals for you, and a fork means you own that tracking forever.
final class MakeActionCommand extends GeneratorCommand
{
use CodeGenerators; // resolves generator + stub file
protected $signature = 'make:action {name}';
}
Pattern 2 — listeners for post-generation side effects. Anything that must happen after a file exists — running Pint, appending a route, registering a migration, updating a composer.json autoload block — belongs in a GeneratesCodeListener, not in the command's handle(). This keeps the generator idempotent and makes side effects individually disableable.
final class RegisterGeneratedMigration implements GeneratesCodeListener
{
public function generatingCode(string $stub, string $name): string { return $stub; }
public function afterCodeHasBeenGenerated(string $file, string $name): void
{
// e.g. $this->formatter->format($file);
}
}
Pattern 3 — one preset per target, selected by config. Applications use an app preset; packages use a package preset with a different root namespace and path. Keep the selection in a published config key (presets.<name>) rather than branching inside the command, so a new module type is a config line, not a code change. Note that older releases had a generators config key that could be null rather than an array — several patch releases exist purely to guard that; if you write your own config reader, cast defensively.
Pattern 4 — test generation like you test output, not behaviour. Use orchestra/testbench and assert on the file contents produced in a fixture app. A high-value CI job: regenerate a canonical reference module and git diff --exit-code. That single check catches stub drift, Laravel-internals drift, and silent convention changes in one shot.
Workflow tips:
stubs/) and resolve every stub through the preset's shared stub path, so overriding a stub for one module type is a file drop, not a code edit.canvas-core v9.x ↔ Laravel 11, v10.x ↔ Laravel 12, v11.x ↔ Laravel 13) so anyone can self-check before adopting.This API chases Laravel internals and will churn. Release history shows tight Laravel-major tracking plus migration to Illuminate\Console\GeneratorCommand, MigrationGeneratorCommand, PromptsForMissingInput, PHP Attributes, and Filesystem\join_paths(). Pin the version, and treat a Laravel major bump as a scheduled work item with the fixture-app generation test — never an automatic composer update.
Confusing deprecation trail. possibleModelsUsingCanvas() was added in v10.1.0 and soft-deprecated in v10.1.1 in favour of Laravel's possibleModels(). The changelogs are internally inconsistent about direction. Read the source for the method you're calling; don't trust the release note.
Contain the blast radius with your own wrapper. Because the core mirrors framework internals, only your thin internal package should touch the core's surface. Then a Laravel upgrade is one file to change instead of every generator.
Name collisions are silent-ish. GeneratorCommand will happily refuse or overwrite depending on flags. Decide up front whether your commands are --force-by-default (bad in a real repo) or fail-loud, and make the error message tell the developer what to do next.
Stub path resolution bites on first run. If a stub "isn't found," the cause is almost always the preset resolving to the package's stub dir rather than the shared one. Dump the resolved path in the command before debugging anything else.
Don't make the generator the only path. Document, prominently, that a developer can hand-write the module. The moment generation is mandatory, a broken command becomes a stopped team.
Operational reality: zero tracked dependents understates usage — real consumption flows through orchestra/canvas, which won't show up in that count. But it does mean you cannot assume an external debugging community for the core itself. Write the internal how-to as a deliverable of the pilot, not an afterthought, and put a name on stub ownership; unowned build tooling rots silently until it blocks a release.
How can I help you explore Laravel packages today?