"The right wave for every shore."
A small, secure, fast PHP 8.4+ framework. Web framework, ORM, WebSocket server, server-driven UI, and command-line tooling — in about 13,000 lines of code. Zero runtime Composer dependencies for the framework itself; opt in to whatever you need.
- Small. You can read the framework in a weekend. The hot path is sub-millisecond.
- Secure by default. SQL identifier regex checks, value binding, header sanitization, JWT alg pin, CSRF, password hashing, rate limiting — all on by default.
- Batteries included. Routing, ORM, migrations, validator, auth, cache, queue, mail, events, WebSockets, server-driven UI, CLI, test framework, debug bar. Install only what you need.
- Standards compliant. PSR-7, PSR-11, PSR-14, PSR-15, PSR-17.
- No magic. Autowiring is reflection-based, not annotation-based. Every binding is explicit.
- One composer file. The framework itself has no runtime Composer dependencies. Your app can.
Install the framework globally, then scaffold a fresh app:
composer global require wavephp/wavephp
tide new my-app
cd my-app
php vendor/bin/tide serveOpen http://localhost:8000.
tide new clones the official starter
(WavestormSoftware/wavephp-starter),
strips its .git/, runs composer install, and generates an APP_KEY.
The new app's composer.json requires wavephp/wavephp: ^0.1 and
defines the App\\ and Database\\ PSR-4 namespaces for you.
Options:
tide new my-app --starter=https://github.com/me/private-starter.gituse a custom starter repotide new my-app --no-installskipcomposer installtide new my-app --no-keyskipAPP_KEYgeneration- The starter URL is also read from the
WAVEPHP_STARTERenv var
composer require wavephp/wavephpThen add the App\\ and Database\\ PSR-4 namespaces to your
own composer.json:
{
"autoload": {
"psr-4": {
"Wave\\": "vendor/wavephp/wavephp/src/",
"App\\": "app/",
"Database\\": "database/"
}
}
}Run composer dump-autoload and you're ready to use the framework.
git clone https://github.com/WavestormSoftware/WavePHP.git
cd WavePHP
composer install
php tide serve<?php
namespace App\Controllers;
use Wave\Http\Controller;
use Wave\Http\Response;
use Wave\Core\Routing\Attributes\Route;
class HomeController extends Controller
{
#[Route('GET', '/')]
public function index(): Response
{
return $this->render('home', ['app' => config('app.name')]);
}
}A first route in routes/web.php:
use Wave\Core\Wave;
use App\Controllers\HomeController;
Wave::get('/', [HomeController::class, 'index']);A first model (app/Models/Post.php):
<?php
namespace App\Models;
use Wave\Database\Model;
class Post extends Model
{
protected string $table = 'posts';
protected array $fillable = ['title', 'body', 'published_at'];
protected array $casts = [
'published_at' => 'datetime',
];
}Then in your project, add the App\\ and Database\\ PSR-4 namespaces to your
own composer.json so the framework can find your application code:
{
"autoload": {
"psr-4": {
"Wave\\": "vendor/wavephp/wavephp/src/",
"App\\": "app/",
"Database\\": "database/"
}
}
}Or scaffold a minimal project structure with tide itself:
mkdir -p app/Controllers app/Models app/Middleware
mkdir -p database/migrations database/seeders
mkdir -p config public resources/views routes storage/cache
vendor/bin/tide make:controller HomeControllervendor/bin/tide is the installed CLI (Composer creates it from the
framework's bin entry). It works the same way in a fresh project as it
does in a git clone of the repo.
git clone https://github.com/WavestormSoftware/WavePHP.git
cd WavePHP
composer install
php tide serveOpen http://localhost:8000.
<?php
namespace App\Controllers;
use Wave\Http\Controller;
use Wave\Http\Response;
use Wave\Core\Routing\Attributes\Route;
class HomeController extends Controller
{
#[Route('GET', '/')]
public function index(): Response
{
return $this->render('home', ['app' => config('app.name')]);
}
}A first route in routes/web.php:
use Wave\Core\Wave;
use App\Controllers\HomeController;
Wave::get('/', [HomeController::class, 'index']);A first model (app/Models/Post.php):
<?php
namespace App\Models;
use Wave\Database\Model;
class Post extends Model
{
protected string $table = 'posts';
protected array $fillable = ['title', 'body', 'published_at'];
protected array $casts = [
'published_at' => 'datetime',
];
}Open http://localhost:8000.
A first controller (app/Controllers/HomeController.php):
<?php
namespace App\Controllers;
use Wave\Http\Controller;
use Wave\Http\Response;
use Wave\Core\Routing\Attributes\Route;
class HomeController extends Controller
{
#[Route('GET', '/')]
public function index(): Response
{
return $this->render('home', ['app' => config('app.name')]);
}
}A first route in routes/web.php:
use Wave\Core\Wave;
use App\Controllers\HomeController;
Wave::get('/', [HomeController::class, 'index']);A first model (app/Models/Post.php):
<?php
namespace App\Models;
use Wave\Database\Model;
class Post extends Model
{
protected string $table = 'posts';
protected array $fillable = ['title', 'body', 'published_at'];
protected array $casts = [
'published_at' => 'datetime',
];
}- Routing — attribute-driven
#[Route]with regex, optional, and wildcard params, plus programmaticWave::get/post/.... - ORM — Eloquent-style Active Record with relations, scopes, casts, soft deletes, property hooks, asymmetric visibility.
- Migrations —
Schema::create('posts', fn(Blueprint $t) => ...)with 30+ column types and modifiers. - Validator —
#[Validate('required|email|min:8')]on form request DTOs, orWave::validate([...], [...])ad-hoc. - Auth —
auth()->attempt(['email', 'password']), plus TokenGuard, JwtGuard, OAuth, and passwordless. - Cache —
Cache::remember('key', 60, fn() => ...)over file, redis, memcached, apcu, or database. - Queue —
#[OnQueue],#[Retries],#[Delay];php tide queue:work. - Storage —
Storage::put('avatars', $file)with local, S3, R2, FTP, and custom drivers. - Mail —
WaveMailablewith SMTP, Mailgun, Postmark, SES; header sanitization on the wire. - Events — PSR-14 dispatcher with class-name listeners, closures, wildcards, and one-times.
- WebSockets — long-running
php tide ws:serveprocess with channels, rooms, presence, broadcasting, and TLS. - Server-driven UI —
WaveComponentwithmount,hydrate,dehydrate,updating,updated{Property}. Renders the initial HTML on the server; the client shim takes over in the browser. - Templating —
.wavetemplates compile to PHP; layouts, blocks, slots, custom filters, custom directives. - GraphQL — executor with types, queries, mutations, and subscriptions; persisted queries.
- REST + OpenAPI —
ApiResponseenvelope, doc-comment-driven OpenAPI 3.1 spec, Swagger UI. - CLI —
php tide <command>, fully scriptable. - Testing — PHPUnit 11 with HTTP, component, WebSocket, queue, and mail fakes; in-memory SQLite.
- Debug bar — request/response, query log, route & middleware inspector, cache log, event log.
- APM — response time, slow query, queue depth, WebSocket connection stats.
WavePHP targets 8.4+ on purpose.
| Feature | Where |
|---|---|
| Attributes | #[Route], #[Validate], #[Auth], #[Can], #[Role], #[Middleware], #[Channel], #[Poll], #[OnQueue], #[Delay], #[Retries], #[ApiController], #[ApiRoute], #[ApiDoc] |
| Property hooks | Computed model attributes, WaveComponent state |
| Asymmetric visibility | Model fillable / hidden properties, read-only state |
| Fibers | Native WebSocket server concurrency |
| Enums | DB drivers, cache drivers, queue drivers, HTTP methods, user roles |
| Readonly properties | Immutable events, request DTOs |
| Named arguments | Cache::put(key: 'x', ttl: 3600), Storage::put(disk: 's3') |
| Match expressions | Route dispatching, driver resolution |
| First-class callables | Event listeners, middleware closures |
| Intersection types | Container type hints |
#[\Deprecated] |
Deprecation lifecycle for the framework's own APIs |
| Tracing JIT | On by default in production for numeric workloads |
| PSR | Interface | Used for |
|---|---|---|
| PSR-3 | Logger | Optional, in modules |
| PSR-4 | Autoloading | Yes |
| PSR-7 | HTTP Message | Yes — Request, Response, Stream, Uri |
| PSR-11 | Container | Yes — Wave\Core\Container\Container |
| PSR-14 | Event Dispatcher | Yes — Wave\Events\WaveEvents |
| PSR-15 | HTTP Middleware | Yes — MiddlewareInterface, MiddlewarePipeline |
| PSR-16 | Simple Cache | Optional adapter for the cache layer |
| PSR-17 | HTTP Factories | Optional adapter |
After composer require, the CLI lives at vendor/bin/tide. In a
checkout of the repo, you can also invoke it as php tide.
vendor/bin/tide --version # Print the framework version
vendor/bin/tide help # List every command
vendor/bin/tide serve # Start dev server on 127.0.0.1:8000
vendor/bin/tide ws:serve # Start WebSocket server
vendor/bin/tide queue:work # Run the queue worker
vendor/bin/tide make:controller Foo
vendor/bin/tide make:model Foo --migration
vendor/bin/tide make:component Counter
vendor/bin/tide make:migration create_posts_table
vendor/bin/tide make:job SendWelcomeEmail
vendor/bin/tide make:mail WelcomeEmail
vendor/bin/tide make:channel ChatChannel
vendor/bin/tide make:request UserRequest
vendor/bin/tide db:migrate
vendor/bin/tide db:fresh --seed
vendor/bin/tide db:status
vendor/bin/tide cache:clear
vendor/bin/tide view:compile
vendor/bin/tide module:list
vendor/bin/tide module:install auth
vendor/bin/tide module:install guard
vendor/bin/tide module:install jwt
vendor/bin/tide test
vendor/bin/tide test --filter=UserTest
vendor/bin/tide test --coveragetide resolves project-relative paths (app/, config/, database/,
public/, storage/, resources/, routes/, tests/) against your
current working directory, so generators and serve work the same way
whether you composer required the package or cloned the repo.
my-app/
├── app/ ← your code
│ ├── Controllers/
│ ├── Models/
│ ├── Components/
│ ├── Channels/
│ ├── Middleware/
│ └── Mail/
├── bootstrap/ ← framework bootstrap
├── config/ ← framework config (app, db, cache, queue, ...)
├── database/
│ ├── migrations/
│ ├── seeders/
│ └── factories/
├── public/ ← web entry point
│ └── index.php
├── resources/
│ └── views/ ← .wave templates
├── routes/ ← route files
│ ├── web.php
│ ├── api.php
│ └── cli.php
├── storage/ ← cache, logs, uploads
├── tests/
│ ├── Unit/
│ ├── Feature/
│ └── Integration/
├── tide ← CLI entry script
└── composer.json
The full docs are in docs/:
- Getting started
- Tutorial: blog
- Tutorial: API
- Tutorial: realtime
- Architecture
- Cookbook
- Deployment
- Security
- Testing guide
- Upgrade guide
- Contributing
- FAQ
- Comparison
- Spec mapping
- Internal changelog
- And 20 feature docs (
docs/01-routing.mdthroughdocs/20-php84.md).
402 tests, 610 assertions, all green.
vendor/bin/phpunit # full suite
vendor/bin/phpunit --testsuite=unit # unit only
vendor/bin/phpunit --testsuite=feature # feature only
vendor/bin/phpunit --filter=UserTest # one test classFound a vulnerability? Email security@wavestorm.example (replace
with the real address). Don't open a public issue.
See Security for the full security posture and threat model.
PRs welcome. See Contributing for the workflow.
The short version:
- PSR-12 +
declare(strict_types=1);on every file. - PHPUnit 11 for tests; all new code requires tests.
- PHPStan level 8 (
vendor/bin/phpstan analyse). - Conventional Commits (
feat:,fix:,docs:, …).
SemVer 2.0.0. The 0.x line may break across minor versions. The 1.0 line, when it ships, is "no breaking changes for 12 months."
See Upgrade guide.
WavePHP is open-source software licensed under the MIT License.
Built by the WavePHP team and contributors. Special thanks to the
PSR working group for the interfaces we depend on, the PHP
internals team for Fibers, asymmetric visibility, property hooks,
and #[\Deprecated], and the maintainers of PHPUnit, MariaDB, and
PostgreSQL.