Docs

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

  1. Config → create config/env/prod.mlc (app.debug? = false).

  2. Buildcomposer install --no-dev --optimize-autoloader.

  3. Serve → point Nginx to public/index.php or run in a Docker container that calls php artisan serve equivalent.

  4. Migratephp vendor/bin/ml migrate --env=prod.