Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Leaf Laravel Package

leafs/leaf

Leaf is a lightweight PHP micro-framework for building web apps and APIs quickly, with simple routing, middleware, database, auth, and an expressive API inspired by modern PHP tools.

View on GitHub
Deep Wiki
Context7

Getting Started

Leaf is a micro-framework, not a Laravel clone: you get routing, request/response, a PDO-backed query builder, auth, validation, Blade templating and a CLI, with almost no opinion about your folder structure. Two ways in:

# 1. Bare framework — you own the structure
composer require leafs/leaf

# 2. MVC skeleton (controllers, models, views, migrations, routes/)
composer global require leafs/cli
leaf create:app my-app        # or the API/bare skeletons; run `leaf list` to see them
cd my-app && leaf serve

Look here first

  • public/index.php (or the skeleton's index.php) — the whole bootstrap: new Leaf\App, routes, $app->run(). This is the app.
  • routes/web.php and routes/api.php in the MVC skeleton — route definitions are grouped by file, nothing magical.
  • config/ + .env — DB credentials, app key, debug flag. In bare Leaf you may have to wire the DB connection yourself.
  • vendor/leafs/* — Leaf is a family of small packages (leaf, db, auth, blade, form/validator, cli). When docs are ambiguous, the source is ~200 lines and readable. Do that instead of guessing.

First use case — a JSON endpoint backed by a table

<?php // public/index.php
require __DIR__ . '/../vendor/autoload.php';

$app = new Leaf\App;

$db = new Leaf\Db;
$db->connect('127.0.0.1', 'root', '', 'app');

$app->get('/notes/{id}', function ($id) use ($db) {
    $note = $db->select('notes')->where('id', $id)->first();
    return $note ?: response()->json(['error' => 'not found'], 404);
});

$app->post('/notes', function () use ($db) {
    $body = request()->body();               // JSON or form-encoded
    $db->insert('notes')->params(['title' => $body['title']])->execute();
    return response()->json(['id' => $db->lastInsertId()], 201);
});

$app->run();

That loop — route → request()->body() → db()->… → response()->json() — is 80% of day-to-day Leaf work.

Implementation Patterns

Route files stay flat, controllers absorb complexity. Anything beyond a few lines becomes a controller method; Leaf passes the route params as positional arguments.

$app->group('/api/v1', function () use ($app) {
    $app->get('/notes', [NoteController::class, 'index']);
    $app->post('/notes', [NoteController::class, 'store']);
});

Query builder first, "model" only if the skeleton gives it to you. Leaf\Db is a thin, predictable builder — reach for it before abstractions:

db()->select('notes')->where('user_id', $userId)->orderBy('created_at', 'desc')->all();
db()->update('notes')->params(['title' => $t])->where('id', $id)->execute();
db()->delete('notes')->where('id', $id)->execute();
db()->raw('SELECT count(*) AS c FROM notes WHERE user_id = ?', [$userId]);

Always push user input through params() / bindings. For anything the builder can't express (window functions, CTEs, bulk upserts), use raw() rather than fighting it. The MVC skeleton's Leaf\Model is convenient for CRUD but leaks quickly — keep it for trivial entities.

Auth: pick one mode per app. Session auth for server-rendered pages, token auth for APIs.

$auth = new Leaf\Auth;
$auth->connect('127.0.0.1', 'root', '', 'app');

$result = $auth->login('email', 'password');   // returns user + token in v3
$auth->register(['name' => $n, 'email' => $e, 'password' => $p]);

Then one middleware guards everything that needs a user — don't sprinkle auth()->user() checks through controllers.

class RequireAuth extends Leaf\Middleware
{
    public function before()
    {
        if (!auth()->user()) {
            response()->json(['error' => 'unauthenticated'], 401);
        }
    }
}

$app->get('/me', [RequireAuth::class, 'before'], fn () => auth()->user());

Validate at the edge, in one call, with the rule-string syntax. Leaf's validator is compact and good enough for form/API input — the trick is returning early with the errors array instead of letting bad data hit the DB.

$form = new Leaf\Form;   // Leaf\Validator in newer releases
if (!$form->validate([
    'title' => 'text:required',
    'email' => 'email:required',
])) {
    return response()->json(['errors' => $form->errors()], 422);
}

Views: Blade for HTML, JSON for APIs — don't mix. In the MVC skeleton, response()->render('notes/index', $data) renders a Blade view; in bare Leaf, wire Leaf\Blade directly (configure($viewsPath, $cachePath) then render()).

Keep deployment boring. Leaf has no build step: composer install --no-dev, point the vhost document root at public/, done. This is the framework's real advantage — it drops onto cheap shared hosting where Laravel's queue/scheduler assumptions don't.

Test at the HTTP boundary. Leaf has no first-class test harness; keep controllers thin, put logic in plain classes, and exercise those with PHPUnit. For route-level confidence, spin up leaf serve and hit endpoints with Guzzle, or invoke route callbacks directly with request() faked where the version allows it.

Gotchas and Tips

  • Version churn is the #1 trap. Class names and return shapes have moved across majors (Leaf\Form → Leaf\Validator, auth returning a bare user vs ['user' => …, 'token' => …], middleware signature changes). Pin the exact version in composer.json, read the upgrade guide before bumping, and verify ambiguous APIs against vendor/leafs/* rather than blog posts.
  • Response helpers write to output immediately. response()->json() (and redirect()) echo and typically halt — returning a value and calling them gives you doubled bodies or headers-already-sent warnings. Pick one: return a string/array from the handler, or call response() and stop.
  • Global helpers (app(), db(), auth(), request(), response()) come from Leaf's bootstrap. They exist in the skeleton and after new Leaf\App, but if you're in a script, a queue worker, or a test, they may not be registered — instantiate the classes directly there.
  • In bare Leaf, nothing connects your DB for you. auth(), db(), and models all need a connection configured first; a silent "no results" is usually an unconfigured connection, not a query bug.
  • Blade needs a writable cache directory. Blank pages or failed to open stream on views are almost always permissions on views/cache (or storage/), not a Blade bug. Clear it when a template change doesn't appear.
  • leaf serve is PHP's built-in server. Single-threaded, no .htaccess, dies on a blocked request. Never benchmark or ship on it; use nginx/Apache with the docroot at public/.
  • Proxy/HTTPS detection is manual. Behind a load balancer, request()->getUri()/scheme can come back as http. Normalize X-Forwarded-Proto/-For yourself in a global middleware.
  • Don't assume the ecosystem. With ~1.3k stars and essentially no third-party dependents, there are no Sanctum/Horizon/Spatie equivalents to composer require. Budget time for hand-rolled rate limiting, queues, and payment integrations — or shell out to standalone libraries (stripe/stripe-php, phpmailer) instead of looking for Leaf wrappers.
  • The upside of the small ecosystem is that everything is debuggable. Turn on APP_DEBUG / the debug config, then read vendor/leafs/* and PHP's error log — a 500 with an empty body is usually a caught exception plus debug off.
  • Extension points, in order of usefulness: global middleware (cross-cutting concerns), route-group middleware (auth/tenancy), Leaf\Config for environment-driven settings, and Leaf's module/attach mechanism if you genuinely need to package a route set for reuse. You can also hand Leaf\Db an existing PDO-style connection if you need to share one with legacy code — but verify the exact method for your version in src/.
  • Know when to leave. Leaf is excellent for prototypes, internal tools, small APIs, and shared-hosting deployments that must "just run PHP." For a large team that needs queues, events, first-party package compatibility and a hiring pool, the friction of re-implementing Laravel's ecosystem will eventually outweigh Leaf's simplicity — that's a scope decision to make up front, not mid-project.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle