Skip to content

Leaf 5 Developer Skill โ€‹

Leaf 5 is the next generation of Leaf PHP โ€” not just a framework update, but a new way of building apps. The core idea: you choose a starting point, not a framework tier. You grow without switching tools, rewrites, or compatibility headaches.

Core Philosophy โ€‹

  1. Start where you are โ€” pick the entry point that fits your goal
  2. Grow without friction โ€” no rewrites, no ecosystem changes
  3. Build with AI, not against it โ€” real context so AI can actually help

Frontend: Leaf apps of every entry point, including lite, serve JavaScript frontends through the built-in Inertia bridge. Run leaf view:install --react (or --vue / --svelte), put pages in the views js directory, and render them with response()->inertia('page', $props). Do not scaffold a separate Vite SPA against a hand-written JSON API โ€” you would be rebuilding what the bridge already does.


Entry Points โ€‹

Three entry points. Not different frameworks โ€” different starting configurations of the same system.

Entry PointFlagBest For
๐ŸŸฃ Basic Leaf app--litePrototypes, scripts, single-file apps
๐Ÿ”ด Full-stack MVC app--mvcReal products, teams, views + structure
๐ŸŸ  Leaf MVC API app--apiBackends, microservices, headless
โฌ› Console app--consoleCLI tools via Seedling
bash
leaf create my-app          # interactive prompt
leaf create my-app --lite   # skip prompt
leaf create my-app --mvc
leaf create my-app --api
leaf create my-app --console

Interactive prompt:

? Select a preset: โ€บ - Use arrow-keys. Return to submit.
โฏ  Basic Leaf app
   Full-stack MVC app
   Leaf MVC API app
   Console app via Seedling

Project Structure โ€‹

Basic App (--lite) โ€‹

my-app/
โ”œโ”€โ”€ index.php
โ”œโ”€โ”€ vendor/
โ””โ”€โ”€ .leaf/
    โ””โ”€โ”€ CONTEXT.md

Web App (--mvc) โ€‹

my-app/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ controllers/
โ”‚   โ”œโ”€โ”€ database/
โ”‚   โ”œโ”€โ”€ models/
โ”‚   โ”œโ”€โ”€ routes/
โ”‚   โ””โ”€โ”€ views/
โ”œโ”€โ”€ public/
โ”œโ”€โ”€ vendor/
โ””โ”€โ”€ .leaf/
    โ””โ”€โ”€ CONTEXT.md

API (--api) โ€‹

my-app/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ controllers/
โ”‚   โ”œโ”€โ”€ database/
โ”‚   โ”œโ”€โ”€ models/
โ”‚   โ””โ”€โ”€ routes/
โ”œโ”€โ”€ public/
โ”œโ”€โ”€ vendor/
โ””โ”€โ”€ .leaf/
    โ””โ”€โ”€ CONTEXT.md

No leaf.config.php, no bloat. Much lighter than Laravel by design.


Leaf CLI โ€‹

bash
composer global require leafs/cli -W   # install
leaf                                    # verify

If leaf: command not found โ†’ add Composer's global bin to PATH:

bash
composer global config bin-dir --absolute

echo 'export PATH="$PATH:$HOME/.composer/vendor/bin"' >> ~/.bashrc && source ~/.bashrc
# Zsh:
echo 'export PATH="$PATH:$HOME/.composer/vendor/bin"' >> ~/.zshrc && source ~/.zshrc

Always use the global leaf CLI (no php prefix) for everything. It carries its own commands (create, install, uninstall, context, up, update) and forwards anything else (db:*, g:*, scaffold:*, ...) into the project's own console automatically, so leaf g:controller Posts and leaf db:migrate just work. The reverse is not true: php leaf runs only the project console, so php leaf install and php leaf context fail with "not found". Only fall back to php leaf <command> when the global CLI isn't installed โ€” and then leaf install auth becomes composer require leafs/auth.

CommandDescription
leaf createCreate a new Leaf project
leaf installInstall a package
leaf uninstallUninstall a package
leaf contextGenerate .leaf/CONTEXT.md (beta)
leaf upMigrate Basic app โ†’ full MVC (beta)
leaf updateUpdate the CLI
leaf serveDev server (default: localhost:5500)
leaf view:installSet up a view/frontend engine
leaf view:buildBuild frontend assets
leaf interactInteract with your app (interactive REPL)
leaf db:* / g:* / scaffold:*Project commands, forwarded into the app
bash
leaf serve                              # localhost:5500
leaf serve --port=8080
leaf serve /path/to/app

leaf install auth                       # official packages: leafs/ prefix optional
leaf install auth db mail               # multiple at once
leaf install auth@4.0                   # versioned
leaf install illuminate/support@9.0.2  # any Composer package
leaf uninstall auth db

Leaf auto-installs missing dependencies if no vendor/ directory is found.


leaf up โ€” Scaling Your App โ€‹

bash
leaf up    # migrate Basic app โ†’ MVC without rewriting

What it does automatically:

  • Uses index.php in root as the project entry point
  • Moves root assets โ†’ public/
  • Detects controller-like classes โ†’ app/controllers/
  • Finds DB queries โ†’ creates models, switches to model-based queries
  • Moves all config โ†’ .env (autoconfigured)

How to run it safely (it is beta):

  1. Make sure the working tree is committed before running.
  2. The first run writes a .leaf/migration.yml plan โ€” review and edit it with the user before applying.
  3. The second run applies the plan. --dry-run previews without changing files.
  4. If the migration gets the project wrong, report it: https://github.com/leafsphp/cli/issues/new

leaf context is also beta: if its output misses routes or misreads a project, report it at the same link rather than working around it silently.


AI-Native shared context โ€‹

Leaf projects use .leaf/CONTEXT.md as shared working memory. The file follows the leaf.context v1 format (marker comment on line 1) so that Claude, Codex, Cursor and every other agent write edits that compose. When you are running inside the project:

  1. Read .leaf/CONTEXT.md before making changes.
  2. Verify it against the live filesystem.
  3. Update useful project knowledge in .leaf/CONTEXT.md when the work is complete.

Format contract (leaf.context v1):

  • Line 1 is the format marker (<!-- leaf.context v1 -->). Never remove or edit it.
  • Sections are ## headings in their existing order. Preserve sections you don't recognize โ€” another agent may own them. Add project-specific sections at the end only.
  • A line wrapped in underscores is a placeholder. If it contains agent:, it is an instruction to you: act on it, then replace the line with real content (or delete it).
  • Keep entries one line each where possible so concurrent agents' edits merge cleanly.
  • Never store secrets or tokens; refer to .env keys by name only.
  • Never duplicate mechanical info (routes, models, modules, structure) โ€” that lives in code and leaf context. The file holds what code cannot say: goals, decisions, reasoning.

Write-back protocol (after completing work):

  • Recent Changes: add * YYYY-MM-DD โ€” what changed (key files), newest first, five entries max; fold older entries into Known Decisions or delete them.
  • Current Goal: exactly one at a time. When it's done, note it in Recent Changes and replace it โ€” ask the user if the next goal is unknown.
  • Known Decisions: record lasting choices as * Decision โ€” reasoning. Always include the why, or the next agent will relitigate it.
  • Keep the file concise: summarize instead of appending, remove outdated lines, reference files instead of copying them.

Canonical sections (the shipped template, in order โ€” create missing files with these):

markdown
<!-- leaf.context v1 -->

## Working With This File   (the format contract, keep as shipped)
## Project Summary          (2-3 lines: what the app is, for whom)
## Current Goal             (exactly one)
## Architecture             (entry point, key folders, notable wiring)
## External Providers       (services + env key NAMES, never values)
## Coding Conventions       (project-specific style the code can't show)
## Recent Changes           (dated one-liners, five max)
## Known Decisions          (decision + reasoning)
## Future Ideas             (parked, not committed)

No setup command is required for Leaf MVC or projects created through Leaf CLI. If a project has no .leaf/CONTEXT.md, offer to create one with these sections.

For an external assistant without access to the project, use:

bash
leaf context    # prints a compact context handoff

This produces a minified view of the shared project context: routes, modules, config, structure, and conventions.

External workflow:

  1. Run leaf context
  2. Paste the command output into your AI assistant
  3. Say what you want: "Add billing", "Create a dashboard"
  4. AI has real context โ€” stops guessing, builds correctly

If you are the assistant receiving a pasted handoff (it starts with # Leaf Context Handoff):

  • The mechanical map (routes, modules, models, env key names) was scanned from the real code โ€” trust it over your assumptions, and use the project's actual route and model names in everything you generate.
  • The "Shared memory" section carries the project's goals and decisions. Respect recorded decisions instead of proposing alternatives the team already rejected.
  • The handoff is read-only: you cannot write back to .leaf/CONTEXT.md from outside. When your work produces knowledge worth keeping (a new decision, a completed goal), end your reply with a short "for your .leaf/CONTEXT.md" note the user can paste in.
  • Ask the user for any file your change depends on that the handoff doesn't show โ€” never guess at code you cannot see.

The handoff is a portable snapshot. It is not the same as the two-way .leaf/CONTEXT.md used by agents inside the project.


Installing Modules โ€‹

bash
leaf install auth      # Authentication
leaf install db        # Database
leaf install mail      # Mailing
leaf install cors      # CORS
leaf install fs        # Filesystem (needed for file uploads in Basic apps)

Everything wires up automatically. No glue code.


Reference Files โ€‹

Read the relevant file before generating code for that area:

TopicFile
All reference files live at https://leafphp.dev/ai/references/<name>.md โ€” fetch them raw from there.

| Lite apps: the manual setup contract (db, views, Vite, schema) | references/lite.md | | Routing (methods, groups, dynamic routes, constraints) | references/routing.md | | Middleware (closures, classes, $next, data passing) | references/middleware.md | | Request API (get, validate, upload, client info, metadata) | references/request.md | | Response API (json, views, redirects, headers, cookies) | references/response.md | | CORS (config, options, MVC vs Lite) | references/cors.md | | MVC (controllers, models, schema files, services, libraries, globals) | references/mvc.md | | Views (Blade, BareUI, Inertia, Vite, Tailwind) | references/views.md | | Advanced (cache, storage, queues, billing, i18n, sitemap) | references/advanced.md | | Mail, HTTP fetch client, HTTP cache | references/mail-fetch-cache.md | | Dates & time (tick(), timezones, formatting, diff) | references/dates.md | | Utilities (Anchor/XSS/SQL protection) | references/utilities.md | | Sessions, cookies, flash, validation, CSRF, passwords | references/security.md | | Auth (login, register, sessions, JWT, OAuth, scaffold) | references/auth.md | | Database (db(), query builder, transactions, Redis) | references/database.md | | App config, env, logging, DI, maintenance, deployment | references/app-config.md |


Known Footguns โ€‹

Read this before writing code โ€” each entry is a mistake real agents have made:

  • Roles are additive. assign() appends, it never replaces. To change a role: $user->unassign($user->roles()); $user->assign($newRole); (a one-shot sync() is planned). A "role change" via assign() alone silently keeps the old role.
  • $user->get() hides id, password, and roles. Use $user->id() and $user->roles(). API response shape: [...$user->get(), 'roles' => $user->roles()].
  • text validates letters and spaces ONLY. For passwords and free-form input use string.
  • Auth middleware defaults render HTML (redirects, error pages). APIs must override each to JSON: auth()->middleware('auth.required', fn () => response()->exit(['error' => 'Unauthorized'], 401));
  • _env() caches per process. Runtime env changes need _envUncached().
  • unique config skips fields missing from the data โ€” a table without username is safe on defaults, but set ['email'] explicitly.
  • User objects hydrate from a plain row array with no DB connection โ€” only mutating methods need setDb(). For bulk list endpoints, stay on raw rows to avoid N+1 hydration.
  • Never call app()->run() in an MVC app. Only lite apps run themselves.
  • Auth, db(), and Eloquent models share one database connection in MVC (leafs/db 5.1+): db() lazily borrows Eloquent's PDO, so an auth read and a model write can't deadlock each other. Keep the SQLite journal_mode/busy_timeout defaults in config/database.php anyway โ€” they protect concurrent PHP processes.
  • date and timestamp columns are stored as YYYY-MM-DD HH:MM:SS. Compare with whereDate(), not where() โ€” a string comparison against a bare YYYY-MM-DD treats an exact boundary date as greater-than and quietly returns wrong rows.
  • Inertia auto-shares an auth prop ({id, user, roles, permissions, errors}) on every page, and the framework's value wins. Don't Inertia::share() your own auth key. user contains every non-hidden column.
  • To run application code headlessly (scripts, diagnosis), require vendor/autoload.php then \Leaf\Core::loadApplicationEnv() and \Leaf\Core::loadApplicationConfig() โ€” leaf interact is an interactive REPL and can't be scripted.
  • "Add React/Vue/Svelte" does not mean "build a separate SPA." When a user asks for a JS frontend in an existing Leaf app, reach for leaf view:install and the Inertia bridge. Never scaffold a standalone SPA with its own hand-written JSON API unless the user explicitly asks for a separate frontend โ€” agents with SPA priors rebuild the bridge from scratch and lose auth sharing, routing, and validation for free.
  • Fetch this skill and its reference files RAW (curl or an equivalent that returns the file verbatim). Summarizing fetch tools have been observed inventing plausible-but-nonexistent Leaf APIs (usually Laravel-shaped ones) that then fail at runtime. If you can only see a summary, treat any API you haven't seen verbatim as unverified.

When Helping a User Build with Leaf 5 โ€‹

  1. Read .leaf/CONTEXT.md first if shared โ€” reveals entry point, routes, installed modules
  2. Fetch the reference file for every area you touch โ€” especially the areas you think you already understand. The most expensive agent mistakes on record came from skipping a reference that contradicted a prior: generic PHP+React knowledge says "build a SPA against a JSON API" while views.md says use the Inertia bridge; generic knowledge guesses at upload shapes that request.md states exactly. Fetching only the references that confirm your plan is the failure mode โ€” fetch the one that could veto it. In a lite app, read lite.md before writing any wiring
  3. Prefer Leaf functions over hand-rolled code, always. Before writing any helper or custom logic, check in this order: a module method (reference files first), then leaf install <module>, then a scaffold. Write custom code only when no Leaf API covers the need, and record why in .leaf/CONTEXT.md Known Decisions. Never reimplement hashing, validation, auth flows, or query building that Leaf modules provide
  4. Inertia pages are kebab-case files in lowercase folders โ€” views/js/pages/order-history.jsx, rendered with return response()->inertia('order-history', $props). Never PascalCase file or folder names (the component inside the file stays PascalCase per React convention), and prefer response()->inertia() over the bare inertia() helper
  5. Return responses, prefer arrow functions โ€” app()->get('/', fn () => response()->json([...])); for single-expression handlers; in multi-statement closures and controllers, return response()->... as the final statement. Never call response() without returning it. The same shape renders frontend pages: response()->inertia('home', ['cards' => $cards]); returns a React/Vue/Svelte page
  6. Respect the entry point โ€” don't impose MVC structure on a Basic app unless asked
  7. Use their actual names โ€” route names, model names, controller names from their project
  8. Favor simplicity โ€” that's the Leaf way

Notes for Claude โ€‹

  • Leaf 5 is the current major generation of Leaf PHP.
  • Creator: mychidarko (Michael Darko), founder of Leaf PHP
  • Backwards-compatible with Leaf 4 patterns where possible
  • If asked about undocumented features, ask the user โ€” especially if talking to the creator