First Project Tutorial
This guide assumes PHP ≥ 8.4, Composer, and MySQL 8 running locally.
Step | Goal | Packages used |
---|---|---|
1 | New project skeleton | Core, CLI |
2 | Hello-world route | Router, Dev Server |
3 | Blade-style view | Template |
4 | Entity + migration | Entity, Database, Migration |
5 | Query layer | Query |
6 | JWT login & auth guard | Auth |
7 | Validation & i18n | Validation, I18n |
8 | Telemetry & events | Telemetry, Events |
9 | “prod-ish” build | Dev-Server, Config, CLI |
1 · Create & bootstrap
composer create-project monkeyscloud/monkeyslegion-skeleton blog
cd blog
php vendor/bin/ml key:generate # RSA keypair for JWT
The skeleton wires Core, DI, CLI, Dev Server.
Open config/app.mlc and set app.name = "Blog".
2 · Hello, world
Create a controller:
php vendor/bin/ml make:controller HomeController
src/Http/Controller/HomeController.php
use MonkeysLegion\Router\Attribute\Route;
use Laminas\Diactoros\Response\HtmlResponse;
final class HomeController
{
#[Route('GET', '/')]
public function index(): HtmlResponse
{
return new HtmlResponse('<h1>Hello MonkeysLegion!</h1>');
}
}
Start the dev server:
composer serve
Visit http://localhost:8000—you should see the greeting.
3 · Add a template view
resources/views/home.ml.php
<x-layout>
<x-slot:title>Hello ✨</x-slot:title>
<p>Served at {{ date('H:i:s') }}</p>
</x-layout>
A minimal component:
app/View/Components/Layout.php
namespace App\View\Components;
use MonkeysLegion\Template\Component;
final class Layout extends Component
{
public function render(): string
{
return <<<ML
<!doctype html>
<title>{{ \$this->slot('title') }}</title>
<body>{{ \$this->slot('default') }}</body>
ML;
}
}
Update the controller:
public function index(MLView $view): HtmlResponse
{
return new HtmlResponse($view->render('home'));
}
Save any file—dev server auto-reloads.
4 · Persist data with Entity + Migration
Generate:
php vendor/bin/ml make:entity Post
Edit the new entity:
#[Entity(table: 'posts')]
final class Post
{
#[Id(strategy: 'auto')] public int $id;
#[Field(type: 'string', length:160)] public string $title;
#[Field(type: 'text')] public string $body;
#[Field(type: 'datetime')] public \DateTimeImmutable $publishedAt;
}
Create SQL:
php vendor/bin/ml migrate # autodetects schema diff
A file like 2025_06_11_…_CreatePosts.php is generated & executed.
5 · Query layer
src/Repository/PostRepository.php
final class PostRepository extends EntityRepository
{
protected string $table = 'posts';
protected string $entityClass = Post::class;
public function latest(int $limit = 10): array
{
return $this->qb->select('*')
->from($this->table)
->orderBy('publishedAt','DESC')
->limit($limit)
->fetchAll(Post::class);
}
}
Controller route:
#[Route('GET','/posts')]
public function list(PostRepository $posts, MLView $view): ResponseInterface
{
return new HtmlResponse($view->render('posts/list', [
'posts' => $posts->latest()
]));
}
6 · Auth: register, login, guard
DTO with validation rules:
final class RegisterUser
{
#[Assert\Email] public string $email;
#[Assert\Length(min:6)] public string $password;
}
Controller:
#[Route('POST','/auth/register')]
public function register(
RegisterUser $input,
AuthService $auth
): JsonResponse {
$auth->create($input->email, $input->password);
return new JsonResponse(['ok'=>true],201);
}
#[Route('POST','/auth/login')]
public function login(
LoginUser $input,
AuthService $auth
): JsonResponse {
[$jwt] = $auth->attempt($input->email,$input->password);
return new JsonResponse(['token'=>$jwt]);
}
Protect routes:
#[Can('edit', Post::class)]
#[Route('PUT','/posts/{id}')]
public function edit(...) { … }
Add middleware order in bootstrap/http.php:
$kernel->add(new JwtAuthMiddleware($jwt));
$kernel->add(new AuthorizationMiddleware($authz));
7 · Validation & i18n in one shot
ValidationMiddleware binds DTOs & returns 422 on failures.
Use @lang('posts.count') in templates; Translator picks locale from request.
Add resources/lang/es.json to get instant Spanish pages.
8 · Telemetry & events
$kernel->add(new LatencyMiddleware($metrics));
$dispatcher->dispatch(new UserRegistered($id,$email));
Expose Prometheus stats at /metrics, scrape with Prometheus, chart in Grafana.
9 · Ship it
Config → create config/env/prod.mlc (app.debug? = false).
Build → composer install --no-dev --optimize-autoloader.
Serve → point Nginx to public/index.php or run in a Docker container that calls php artisan serve equivalent.
Migrate → php vendor/bin/ml migrate --env=prod.