Laravel to Waaseyaa Package Migration
Overview
A repeatable pattern for building new Waaseyaa packages that replace Laravel functionality. Each package follows a strict scaffold-TDD-wire-deploy pipeline proven with waaseyaa/inertia.
When to Use
- Creating a new package in
/home/fsd42/dev/waaseyaa/packages/ - Porting a Laravel feature (auth, billing, notifications, etc.) to Waaseyaa
- Building any new
waaseyaa/*composer package in the monorepo - When the user mentions "new waaseyaa package" or "migrate X from Laravel"
When NOT to Use
- Modifying existing Waaseyaa packages (use normal development workflow)
- Frontend-only changes (Vue/Inertia client-side)
- Go API changes in goforms/
The Pipeline
Every new package follows these 6 phases in order. Do not skip phases.
Phase 1: Research
Before writing any code, understand what you're replacing:
- Read the migration design spec if one exists (
docs/superpowers/specs/) - Study the Laravel source — identify the classes, interfaces, and behaviors to port
- Map Laravel concepts to Waaseyaa equivalents:
| Laravel | Waaseyaa |
|---|---|
| Service Provider | ServiceProvider (extends Waaseyaa\Foundation\ServiceProvider\ServiceProvider) |
| Middleware | HttpMiddlewareInterface (in Waaseyaa\Foundation\Middleware) |
| Facade | Static class (no magic, explicit methods) |
| Controller returning view | Controller returning SsrResponse or InertiaResponse |
| Config files | $this->config array from kernel context |
| Artisan commands | Symfony Console commands via commands() method |
| Eloquent models | Waaseyaa Entity system |
| Route registration | routes(WaaseyaaRouter $router) method on ServiceProvider |
- Check existing packages for patterns to follow — especially
packages/ssr/,packages/inertia/,packages/mail/
Phase 2: Scaffold
Create the package skeleton:
packages/<name>/
├── composer.json
├── src/
│ └── <Name>ServiceProvider.php
└── tests/
└── Unit/
composer.json template:
{
"name": "waaseyaa/<name>",
"description": "<one-line description>",
"type": "library",
"license": "GPL-2.0-or-later",
"repositories": [
{ "type": "path", "url": "../foundation" }
],
"require": {
"php": ">=8.4",
"waaseyaa/foundation": "@dev"
},
"require-dev": {
"phpunit/phpunit": "^10.5"
},
"autoload": {
"psr-4": { "Waaseyaa\\<Name>\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Waaseyaa\\<Name>\\Tests\\": "tests/" }
},
"extra": {
"waaseyaa": {
"providers": ["Waaseyaa\\<Name>\\<Name>ServiceProvider"]
},
"branch-alias": { "dev-main": "0.1.x-dev" }
},
"minimum-stability": "dev",
"prefer-stable": true
}
ServiceProvider stub:
<?php
declare(strict_types=1);
namespace Waaseyaa\<Name>;
use Waaseyaa\Foundation\ServiceProvider\ServiceProvider;
final class <Name>ServiceProvider extends ServiceProvider
{
public function register(): void {}
}
Wire to root composer.json — add three entries:
repositories[]—{ "type": "path", "url": "packages/<name>" }require—"waaseyaa/<name>": "@dev"autoload-dev.psr-4—"Waaseyaa\\<Name>\\Tests\\": "packages/<name>/tests/"
Verify: composer update waaseyaa/<name> — must resolve without errors.
Phase 3: TDD Core Components
For each source file in the package:
- Write the test first at
tests/Unit/<ClassName>Test.php - Run the test — verify it fails (class not found)
- Write the implementation at
src/<ClassName>.php - Run the test — verify it passes
- Move to next component
Conventions:
declare(strict_types=1)on every filefinalclasses by defaultreadonlyvalue objects where appropriate- PHPUnit
#[CoversClass]attributes on test classes - No docblocks on obvious methods — only
@param/@returnfor arrays
Test runner: vendor/bin/phpunit packages/<name>/tests/
Phase 4: Wire Service Provider
Once core components exist:
- Register middleware via
middleware(EntityTypeManager)if the package has middleware - Register routes via
routes(WaaseyaaRouter)if the package has routes - Add any ControllerDispatcher integration (soft
instanceofchecks — no hard dependency)
Phase 5: Verify
- Run package tests:
vendor/bin/phpunit packages/<name>/tests/ - Run CS Fixer:
vendor/bin/php-cs-fixer fix packages/<name>/ - Run foundation tests if ControllerDispatcher was modified:
vendor/bin/phpunit packages/foundation/tests/ - Commit with conventional commit:
feat(<name>): <description>
Phase 6: Deploy
- Add to split workflow —
.github/workflows/split.ymlmatrix, correct layer comment - Create split target repo —
waaseyaa/<name>on GitHub (public, no README init) - Commit and push split.yml change
- Tag and push — increment the alpha tag:
git tag v0.1.0-alpha.<next> && git push origin v0.1.0-alpha.<next>to trigger split. Check latest tag withgit tag --sort=-v:refname | head -1. - Submit to Packagist —
https://packagist.org/packages/submitwithhttps://github.com/waaseyaa/<name>
ControllerDispatcher Integration Pattern
When a package returns custom response types from callable controllers, add handling in packages/foundation/src/Http/ControllerDispatcher.php in the callable controller branch (after SsrResponse check):
if ($result instanceof \Waaseyaa\<Name>\<ResponseType>) {
// Handle response — use ResponseSender::json() or ResponseSender::html()
}
This is a soft dependency — if the package isn't installed, the instanceof check returns false. No error.
Waaseyaa Architecture Reference
Layer 0: Foundation (foundation, cache, plugin, typed-data, database-legacy, testing, i18n, queue, state, validation, mail, github)
Layer 1: Core Data (entity, entity-storage, access, user, config, field)
Layer 2: Content Types (node, taxonomy, media, path, menu, note, relationship)
Layer 3: Services (workflows)
Layer 4: API (api, routing)
Layer 5: AI (ai-schema, ai-agent, ai-pipeline, ai-vector)
Layer 5.5: GraphQL (graphql)
Layer 6: Interfaces (cli, inertia, mcp, ssr, telescope, admin-surface)
Meta: cms, core, full
Dependencies point inward — higher layers depend on lower layers, never the reverse.
Common Mistakes
| Mistake | Fix |
|---|---|
Forgetting autoload-dev in root composer.json |
Tests won't be found by PHPUnit |
| Hard dependency on inertia/ssr in foundation | Use soft instanceof checks in ControllerDispatcher |
| Skipping CS Fixer before push | CI will fail on fn () vs fn() style |
| Not creating the GitHub split repo before tagging | Split workflow fails silently |
| Adding package to wrong layer in split.yml | Check dependency direction — higher layers only |
Quick Reference
# Scaffold
mkdir -p packages/<name>/{src,tests/Unit}
# Write composer.json, ServiceProvider, wire root composer.json
composer update waaseyaa/<name>
# TDD loop (per component)
vendor/bin/phpunit packages/<name>/tests/Unit/<Test>.php # RED
# write implementation
vendor/bin/phpunit packages/<name>/tests/Unit/<Test>.php # GREEN
# Verify
vendor/bin/phpunit packages/<name>/tests/
vendor/bin/php-cs-fixer fix packages/<name>/
vendor/bin/phpunit packages/foundation/tests/ # if dispatcher modified
# Deploy
# Edit .github/workflows/split.yml
# Create waaseyaa/<name> repo on GitHub
git push
git tag v0.1.0-alpha.<next> && git push origin v0.1.0-alpha.<next>
# Submit to packagist.org