You sit down to build a new application. The feature list is clear, the client is waiting, and your fingers are itching to open the editor. The temptation to jump straight into code is powerful. But every experienced developer knows the truth: the time you spend designing before coding is never wasted.
Blueprinting your applicationâsketching out models, controllers, routes, and views before writing a single lineâsaves weeks of rework. This article shows you how to plan your PHP application using tools like Laravel Blueprint, starting with a draft.yaml and building out from there.
Why Blueprint First?
When construction workers build a skyscraper, they donât start with the 50th floor. They build scaffolding firstâtemporary structures that guide the real work. Software scaffolding works the same way. It generates the skeleton of your application from a high-level description, letting you validate your design before committing to implementation details.
The benefits are concrete:
- Consistency. Generated code follows the same patterns every time.
- Speed. Scaffolding tools produce models, migrations, controllers, and tests in seconds.
- Discoverability. Describing your application structure makes missing pieces obvious.
- Documentation. The blueprint itself becomes living documentation of your architecture.
Starting With Blueprint in Laravel
The Laravel Blueprint package generates models, controllers, routes, form requests, and tests from a YAML configuration file. Install it into a fresh Laravel application:
composer create-project laravel/laravel fresh
cd fresh
composer require --dev laravel-shift/blueprint
touch draft.yamlThe draft.yaml file is your blueprint. Everything Blueprint generates flows from this single file.
Modeling Your Data
Start by defining your models and their fields. Hereâs an example for a game tagging application:
models:
Game:
name: string index
slug: string index
site_url: string
publisher_url: string
publisher: string
description: longtext
thumbnail_url: string
screen_url: string
published: boolean
relationships:
hasMany: GameTag
GameTag:
name: string
game_id: id foreign:gamesThis YAML describes two models with their columns, types, indexes, and relationships. Run the build command to generate everything:
php artisan blueprint:buildBlueprint outputs a list of every file it created:
Created:
- database/factories/GameFactory.php
- database/factories/GameTagFactory.php
- database/migrations/...create_games_table.php
- database/migrations/...create_game_tags_table.php
- app/Models/Game.php
- app/Models/GameTag.php
Examining the Generated Code
Blueprint generates production-quality code. Hereâs the Game model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Game extends Model
{
use HasFactory;
protected $fillable = [
'name',
'slug',
'site_url',
'publisher_url',
'publisher',
'description',
'thumbnail_url',
'screen_url',
'published',
];
protected $casts = [
'id' => 'integer',
'published' => 'boolean',
];
public function gameTags()
{
return $this->hasMany(GameTag::class);
}
}Each field is in the $fillable array for mass assignment. Type casting ensures id is an integer and published is a boolean. The gameTags() method defines the hasMany relationship automatically.
The GameTag model gets the inverse relationship:
public function game()
{
return $this->belongsTo(Game::class);
}Migrations and Factories
Blueprint also generates migration files:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateGamesTable extends Migration
{
public function up()
{
Schema::create('games', function (Blueprint $table) {
$table->id();
$table->string('name')->index();
$table->string('slug')->index();
$table->string('site_url');
$table->string('publisher_url');
$table->string('publisher');
$table->longText('description');
$table->string('thumbnail_url');
$table->string('screen_url');
$table->boolean('published');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('games');
}
}The migration looks nearly identical to what you would write manually. Indexes are applied where specified.
Model factories are generated with Faker data. These need review since no library can assume your domainâs intent:
public function definition(): array
{
return [
'name' => $this->faker->name,
'slug' => $this->faker->slug,
'site_url' => $this->faker->url,
'publisher_url' => $this->faker->url,
'publisher' => $this->faker->company,
'description' => $this->faker->text,
'thumbnail_url' => $this->faker->imageUrl(240, 240),
'screen_url' => $this->faker->imageUrl(1280, 720),
'published' => $this->faker->boolean,
];
}Update the Faker methods to match your real data shapes before running tests.
Controllers and Routes
Blueprint also generates controllers and form requests. Expand the draft.yaml:
controllers:
Game:
index:
query: all
render: game.index with:games
create:
render: game.create
store:
validate: name, slug, site_url, publisher_url, publisher, description, thumbnail_url, screen_url, published
save: game
flash: game.name
redirect: game.index
destroy:
delete: game
redirect: game.indexRunning php artisan blueprint:build again adds the controller, updates routes, and creates a form request class:
<?php
namespace App\Http\Controllers;
use App\Http\Requests\GameStoreRequest;
use App\Models\Game;
use Illuminate\Http\Request;
class GameController extends Controller
{
public function index(Request $request)
{
$games = Game::all();
return view('game.index', compact('games'));
}
public function create(Request $request)
{
return view('game.create');
}
public function store(GameStoreRequest $request)
{
$game = Game::create($request->validated());
$request->session()->flash('game.name', $game->name);
return redirect()->route('game.index');
}
public function destroy(Request $request, Game $game)
{
$game->delete();
return redirect()->route('game.index');
}
}The controller is clean. Each method has a single responsibility. Validation logic lives in the GameStoreRequest class, not cluttering the controller. Route model binding handles the destroy methodâs dependency injection.
Test Generation
Blueprint generates PHPUnit tests for every controller method. Add the test assertions package:
composer require --dev jasonmccreary/laravel-test-assertionsThe generated test class:
<?php
namespace Tests\Feature\Http\Controllers;
use App\Models\Game;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use JMac\Testing\Traits\AdditionalAssertions;
use Tests\TestCase;
class GameControllerTest extends TestCase
{
use AdditionalAssertions, RefreshDatabase, WithFaker;
/** @test */
public function index_displays_view(): void
{
$games = Game::factory()->count(3)->create();
$response = $this->get(route('game.index'));
$response->assertOk();
$response->assertViewIs('game.index');
$response->assertViewHas('games');
}
}Run the tests with PHPUnit:
$ php vendor/bin/phpunit
....... 7 / 7 (100%)
Time: 00:00.498, Memory: 32.00 MB
OK (7 tests, 17 assertions)How the Scaffolding Works
Blueprint uses stub files as templates. Look in vendor/laravel-shift/blueprint/stubs/model.class.stub:
<?php
namespace {{ namespace }};
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class {{ class }} extends Model
{
use HasFactory;
}Placeholders like {{ namespace }} and {{ class }} get replaced with values from your YAML configuration. The system is simple but powerfulâthe same approach used by Artisanâs make commands under the hood.
Beyond CRUD: Designing Before You Code
Blueprinting is about more than generated files. The exercise of writing a draft.yaml forces you to think through your data model before you start coding. You discover missing fields, awkward relationships, and naming inconsistencies at the design stage rather than during a refactor.
Hereâs a checklist for your next blueprints:
- Define models first. What entities does your domain have? What are their properties?
- Map relationships. Which entities belong to others? One-to-many? Many-to-many?
- Design routes. What endpoints does your application expose? What HTTP methods?
- Plan validation. What rules govern each field? What happens when validation fails?
- Consider edge cases. What does âemptyâ look like? What about soft deletes?
Conclusion
Blueprinting your PHP application before writing code saves time, reduces bugs, and produces more consistent code. Laravel Blueprintâs draft.yaml approach lets you describe your application structure at a high level and generate production-quality models, controllers, migrations, and tests from that description.
The generated code isnât perfect out of the boxâfactories need domain-specific Faker methods, and views need real HTML. But the skeleton it provides lets you focus on the logic that makes your application unique, not the boilerplate that every CRUD app needs.
Start your next project with a draft.yaml. Your future self will thank you.