Skip to content

WavestormSoftware/WavePHP

WavePHP

"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.

PHP Version License Spec Tests


Why WavePHP?

  • 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.

Quick start

Option A: scaffold with tide new (Laravel-style)

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 serve

Open 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.git use a custom starter repo
  • tide new my-app --no-install skip composer install
  • tide new my-app --no-key skip APP_KEY generation
  • The starter URL is also read from the WAVEPHP_STARTER env var

Option B: composer require in an existing project

composer require wavephp/wavephp

Then 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.

Option C: clone the dev repo

git clone https://github.com/WavestormSoftware/WavePHP.git
cd WavePHP
composer install
php tide serve

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',
    ];
}

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 HomeController

vendor/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.

Or: clone the dev repo

git clone https://github.com/WavestormSoftware/WavePHP.git
cd WavePHP
composer install
php tide serve

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',
    ];
}

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',
    ];
}

What's included

  • Routing — attribute-driven #[Route] with regex, optional, and wildcard params, plus programmatic Wave::get/post/....
  • ORM — Eloquent-style Active Record with relations, scopes, casts, soft deletes, property hooks, asymmetric visibility.
  • MigrationsSchema::create('posts', fn(Blueprint $t) => ...) with 30+ column types and modifiers.
  • Validator#[Validate('required|email|min:8')] on form request DTOs, or Wave::validate([...], [...]) ad-hoc.
  • Authauth()->attempt(['email', 'password']), plus TokenGuard, JwtGuard, OAuth, and passwordless.
  • CacheCache::remember('key', 60, fn() => ...) over file, redis, memcached, apcu, or database.
  • Queue#[OnQueue], #[Retries], #[Delay]; php tide queue:work.
  • StorageStorage::put('avatars', $file) with local, S3, R2, FTP, and custom drivers.
  • MailWaveMailable with 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:serve process with channels, rooms, presence, broadcasting, and TLS.
  • Server-driven UIWaveComponent with mount, hydrate, dehydrate, updating, updated{Property}. Renders the initial HTML on the server; the client shim takes over in the browser.
  • Templating.wave templates compile to PHP; layouts, blocks, slots, custom filters, custom directives.
  • GraphQL — executor with types, queries, mutations, and subscriptions; persisted queries.
  • REST + OpenAPIApiResponse envelope, doc-comment-driven OpenAPI 3.1 spec, Swagger UI.
  • CLIphp 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.

PHP 8.4 features used

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 compliance

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

tide CLI

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 --coverage

tide 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.


Project structure

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

Documentation

The full docs are in docs/:


Test suite

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 class

Security

Found 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.


Contributing

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:, …).

Versioning

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.


License

WavePHP is open-source software licensed under the MIT License.


Credits

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.

About

Heavily work in progress PHP 8.4+ framework.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages