A real, opinionated starter for WavePHP — a small, secure, fast PHP 8.4+ framework.
This isn't the upstream tide new skeleton. It's a full demo application that exercises every feature of the templating engine and gives you a real layout, partials, error pages, auth scaffold, and CRUD to start from.
# 1. Install dependencies
composer install
# 2. Create your .wave.env (the APP_KEY is required for CSRF + signed URLs)
cp .env.example .wave.env
php -r "echo 'APP_KEY=' . bin2hex(random_bytes(32)) . PHP_EOL;" >> .wave.env
# 3. Initialize the database
php vendor/bin/tide db:migrate
php vendor/bin/tide db:seed
# 4. Start the dev server
php -S 127.0.0.1:8000 -t public
# or:
php vendor/bin/tide serveOpen http://127.0.0.1:8000.
| Area | What you get |
|---|---|
| Layouts | layouts/base.wave (grandparent), layouts/app.wave (standard page) — both chain via {% extends %} |
| Partials | partials/header, partials/footer, partials/flash, partials/forms/csrf, partials/forms/field, partials/pagination, partials/empty |
| Error pages | errors/404, errors/403, errors/500, errors/maintenance — rendered by app/Middleware/NotFoundHandler |
| Home | Marketing-style welcome page with a feature grid, recent posts, filters, and url() |
| Auth | Session-based registration, login, logout, remember-me cookie, CSRF tokens, password hashing, rate-limit-ready |
| Posts | Full CRUD resource with create/edit/delete, validation, pagination, slug generation |
| ORM | App\Models\User (with posts() hasMany), App\Models\Post (with user() belongsTo, scopes, casts) |
| Events | App\Events\UserRegistered → App\Listeners\SendWelcomeEmail → App\Jobs\SendWelcomeEmailJob |
App\Mail\WelcomeMail extending Wave\Mail\WaveMailable with a .wave HTML body |
|
| Queue | Wave\Queue\Attributes\OnQueue + #[Retries], dispatched after registration |
| WebSockets | App\Channels\ChatChannel registered in routes/channels.php |
| Components | App\Components\CounterComponent (server-driven UI demo) with resources/views/components/counter.wave |
| Console | tide inspire (sample command) registered in routes/cli.php |
| OpenAPI / GraphQL | Auto-registered at /api/docs, /api/docs.json, /graphql, /graphql/playground |
| Tests | Wave\Testing\WaveTestCase (in-memory SQLite), assertOk, assertSee, assertDatabaseHas etc. |
| Tooling | phpunit.xml, phpstan.neon, .php-cs-fixer.dist.php, .editorconfig |
my-app/
├── app/ your code
│ ├── Channels/ WebSocket channels
│ ├── Components/ WaveComponent classes
│ ├── Console/Commands/ tide CLI commands
│ ├── Controllers/ HTTP controllers
│ ├── Events/ Wave\Events\WaveEvent subclasses
│ ├── Jobs/ Wave\Queue\WaveJob subclasses
│ ├── Listener/ Wave\Events event listeners
│ ├── Mail/ Wave\Mail\WaveMailable subclasses
│ ├── Middleware/ PSR-15 middleware
│ ├── Models/ Wave\Database\Model subclasses
│ └── Support/ Plain PHP helpers
├── bootstrap/ framework bootstrap (env, helpers, session)
├── config/ framework config (app, db, cache, auth, …)
├── database/
│ ├── factories/ Wave\Seed\Factory subclasses
│ ├── migrations/ Wave\Database\Migration files
│ └── Seeders/ Wave\Seed\WaveSeeder subclasses
├── public/ web root (point your server at this)
│ └── index.php single front controller
├── resources/
│ └── views/ .wave templates (layouts, partials, errors, …)
├── routes/
│ ├── api.php /api/* routes
│ ├── channels.php WebSocket channel handlers
│ ├── cli.php console command registrations
│ └── web.php HTTP routes
├── storage/ writable: cache, logs, sessions, uploads
├── tests/ PHPUnit 11 tests
│ ├── Unit/
│ ├── Feature/
│ └── TestCase.php
├── .env.example
├── .php-cs-fixer.dist.php
├── .editorconfig
├── composer.json
├── phpstan.neon
├── phpunit.xml
└── README.md
# Dev server
php -S 127.0.0.1:8000 -t public
vendor/bin/tide serve
# Database
vendor/bin/tide db:migrate
vendor/bin/tide db:seed
vendor/bin/tide db:status
# Scaffolding
vendor/bin/tide make:controller FooController
vendor/bin/tide make:model Post
vendor/bin/tide make:migration create_posts_table
# Testing
vendor/bin/tide test # or: composer test
# Quality
composer cs-fix # php-cs-fixer fix
composer stan # phpstan analyseThis boilerplate uses every feature of the Wave\Template\TemplateEngine. The home page (resources/views/home.wave) is a working example for most of them:
{# Layouts — extend either a top-level layout or a parent view #}
{% extends 'layouts/app' %}
{# Override a named block from the parent #}
{% block title %}Welcome{% endblock %}
{% block content %}
<h1>Hello</h1>
{% endblock %}
{# Variables: escaped by default #}
{{ $user.name }}
{# Dot notation: $user['name'] #}
{{ $user.display_name }}
{# Raw output (use only for trusted HTML) #}
{{ $rawHtml | raw }}
{# Filters: upper, lower, trim, length, truncate, join, format, ... #}
{{ $post.title | upper }}
{{ $post.body | truncate(120) }}
{{ $post.published_at | format('Y-m-d') }}
{# Control structures #}
{% if $user %}
<p>Hello, {{ $user.name }}</p>
{% else %}
<p>Hello, guest</p>
{% endif %}
{% foreach $posts as $post %}
<li>{{ $post.title }}</li>
{% endforeach %}
{# Layout slots — render only if the child fills them #}
{% slot 'actions' %}
<a class="btn" href="...">Click me</a>
{% endslot %}
{# Partials #}
{% include 'partials/forms/csrf' %}
{% include 'partials/forms/field' with { name: 'email', type: 'email', value: $old.email ?? '', errors: $errors } %}
{# CSRF helpers (PHP-side) #}
{{ csrf_field() | raw }}
{{ csrf_meta() | raw }}
{# URL helper for named routes #}
{{ url('home') }}
{{ url('posts.show', {id: 42}) }}-
Create the controller:
vendor/bin/tide make:controller PagesController
-
Add a method:
#[Route('GET', '/about')] public function about(): Response { return $this->render('about', ['team' => $team]); }
-
Create the view (
resources/views/about.wave):{% extends 'layouts/app' %} {% block title %}About{% endblock %} {% block heading %}<h1>About</h1>{% endblock %} {% block body %} <p>...</p> {% endblock %}
That's it. Routes discovered by #[Route] are wired automatically by Wave::bootstrap().
- CSRF tokens are emitted automatically in every form via
partials/forms/csrfand validated in every POST handler. Tokens are stored in the session. - Passwords use bcrypt via
Wave\Security\Security::hashPassword(). - Sessions are configured by
config/session.php(file driver by default; switch todatabaseto use the bundledsessionstable). app/Middleware/MethodOverride.phprewritesPOSTrequests whose body contains_method=PUT|PATCH|DELETE— required for HTML form-based DELETE.- SQL injection is prevented automatically: every query through the ORM is parameterized.
MIT.