PHP code example of cloude / framework

1. Go to this page and download the library: Download cloude/framework library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

cloude / framework example snippets




declare(strict_types=1);

g.php';   // defines BASE_URL, DEBUG, ...

if (\Cloude\Bootstrap::serveStaticIfExists(__DIR__)) {
    return false;   // dev-server static-file passthrough
}

\Cloude\Bootstrap::run(
    debug:    DEBUG,
    viewBase: __DIR__ . '/../app/views',
);

use Cloude\Http\Response;
use Cloude\Input;
use Cloude\Router;
use Cloude\View;

$router = new Router(BASE_URL);

$router->get('/', fn () => View::render('home.html.php', ['title' => 'Hello']));

$router->get('/users/{id:\d+}', fn (array $p) => Response::json(['id' => $p['id']]));

$router->group('/api/v1', function (Router $r) {
    $r->post('/echo', fn () => Response::json(Input::json() ?? []));
});

$router->setNotFound(fn () => View::render('404.php'));
$router->dispatch();

$router = new Router(basePath: '/api');       // optional, stripped from the URI

$router->get('/users/{id}', $handler);        // GET
$router->post('/users', $handler);            // POST
$router->any('/*', $handler);                 // any method
$router->add(['/foo', '/bar'], $handler);     // same handler, multiple routes

$router->setNotFound(fn() => ...);            // custom 404
$router->dispatch();

$router->get('/users/{id:\d+}',     $h);   // matches /users/42, not /users/abc
$router->get('/posts/{slug?}',      $h);   // matches /posts AND /posts/hello
$router->get('/{lang:(es|en)}/...', $h);   // enforces 'es' or 'en' in URL

$router->group('/api/v1', function (Router $r) {
    $r->get('/parties',          $list);     // → /api/v1/parties
    $r->get('/parties/{slug}',   $show);     // → /api/v1/parties/{slug}

    $r->group('/admin', function (Router $r) {
        $r->get('/stats', $stats);           // → /api/v1/admin/stats
    });
});

Input::method();            // GET, POST, ...
Input::uri();               // path without query string, no double slashes
Input::get('q');            // $_GET['q'] or null
Input::post('name');        // $_POST['name'] or null
Input::json();              // decodes JSON body into an array
Input::body();              // raw request body
Input::header('User-Agent');
Input::ip(trustProxy: false);

View::setBasePath(__DIR__ . '/views');

View::render('home.html.php', ['title' => 'Hello']);   // prints
$html = View::capture('home.html.php', $vars);         // returns a string
echo View::e($text);                                   // HTML escape

 // views/layout.html.php 

// app/config/app.php
return [
    'aliases' => ['View', 'Input', 'Str', 'DateTime'],
    // …other config
];

 // views/layout.html.php — no `use` needed 

$result = Markdown::parse($markdownContent);
// => ['meta' => [...], 'html' => '...', 'description' => '...', ...]

$html = Markdown::toHtml($md);

\Cloude\Markdown::useParser(fn (string $md) => (new \Parsedown())->text($md));

$html = \Cloude\Markdown\Parser::toHtml("# Hello\n\nFirst **paragraph**.");

// Basic manipulation
Str::upTo('hello world', ' ');           // 'hello'
Str::truncate('long text', 4);           // 'long...'
Str::truncateMiddle('/var/log/app/very/deep/path/file.log', 25);
                                          // '/var/log/app...h/file.log' (paths, hashes, breadcrumbs)
Str::words('a b c d e', 3);              // 'a b c...'
Str::after('foo.bar.baz', '.');          // 'bar.baz'
Str::afterLast('foo.bar.baz', '.');      // 'baz'
Str::between('hi [there] go', '[', ']'); // 'there'
Str::squish("  a\n  b\t  c ");           // 'a b c'
Str::mask('[email protected]', '*', 2);  // 'pe***************'
Str::mask('+34600123456', '*', 4, -3);   // '+346*****456'

// Slugs / transliteration
Str::slug('Hello World');                // 'hello-world'
Str::ascii('Análisis Político');         // 'Analisis Politico'
Str::ascii('Москва');                    // 'Moskva'

// Case conversion
Str::camel('user_profile_id');           // 'userProfileId'
Str::pascal('user_profile_id');          // 'UserProfileId'
Str::snake('userProfileId');             // 'user_profile_id'
Str::kebab('userProfileId');             // 'user-profile-id'

// Random / hash
Str::random();                           // 22-char URL-safe token
Str::random(32);                         // 43-char URL-safe token
Str::uuid();                             // RFC 4122 v4
Str::hash('hola');                       // sha256 hex
Str::hash('hola', 'sha1');               // any hash_algos() entry

use Cloude\Arr;

Arr::get($cfg, 'db.read.host', 'localhost');     // dot-path with default
Arr::set($cfg, 'db.read.port', 5432);            // creates intermediates
Arr::has($cfg, 'db.read');                       // bool
Arr::forget($cfg, 'db.read.port');               // unset

// Working with row lists
Arr::pluck($users, 'name');                      // [0 => 'Ana', 1 => 'Bea']
Arr::pluck($users, 'name', 'id');                // [42 => 'Ana', 51 => 'Bea']
Arr::pluck($events, 'meta.title');               // dot-path supported

// Subset / inverse
Arr::only($row, ['id', 'name']);
Arr::except($row, ['password', 'salt']);

// Flatten / inflate
Arr::dot(['a' => ['b' => 1]]);                   // ['a.b' => 1]
Arr::undot(['a.b' => 1]);                        // ['a' => ['b' => 1]]

// Recursive merge — later overrides earlier; lists replaced wholesale
Arr::merge(['db' => ['host' => 'a', 'port' => 1]], ['db' => ['port' => 2]]);
// => ['db' => ['host' => 'a', 'port' => 2]]

use Cloude\Collection;

$top = Collection::make($users)
    ->filter(fn ($u) => $u['active'])
    ->sortBy('score', descending: true)
    ->take(3)
    ->pluck('name', 'id')
    ->all();

$c->all();                    // array
$c->count();                  // int
$c->isEmpty(); $c->isNotEmpty();
$c->first(); $c->first(fn ($v) => ...);
$c->last();
$c->contains($value);
$c->every(callable);  $c->some(callable);
$c->reduce(callable, $initial);
$c->sum('column'); $c->avg('column'); $c->min('col'); $c->max('col');

$c->map(fn ($v, $k) => ...);
$c->filter(?callable);  $c->reject(callable);
$c->each(callable);                        // returns $this; false stops
$c->pluck('column', 'key');                // dot-paths supported
$c->keyBy('column' | callable);
$c->groupBy('column' | callable);
$c->sortBy('column' | callable, descending: false);
$c->sort(?callable);
$c->reverse();
$c->take(int);  $c->slice($offset, $length);
$c->chunk(int);
$c->unique(?'column');
$c->values();  $c->keys();
$c->merge(...$others);

class PartiesRepo extends \Cloude\Data\JsonRepository
{
    public function __construct(string $country)
    {
        parent::__construct(DATA_DIR . "/scopes/{$country}/parties");
    }

    /** Normalize each row into a stable shape, slug ll,
        ] + $data;
    }

    public function byFamily(string $family): \Cloude\Collection
    {
        return $this->all()->filter(fn ($p) => $p['family'] === $family);
    }
}

$parties = new PartiesRepo('espana');

$parties->slugs();                     // ['psoe', 'pp', 'vox', ...]
$parties->exists('psoe');              // bool
$parties->find('psoe');                // ?array (null if missing)
$parties->findOr('psoe', []);          // array (default if missing)
$parties->all();                       // Cloude\Collection keyed by slug
$parties->byFamily('left');            // Cloude\Collection
$parties->write('new', [...]);         // atomic write (.json)

class User extends \Cloude\Model\Model
{
    protected static string $table      = 'users';
    protected static string $primaryKey = 'id';

    protected static array $properties = ['id', 'email', 'name', 'role_id', 'active', 'created_at', 'tags', 'status'];

    protected static array $types = [
        'id'         => 'int',
        'active'     => 'bool',
        'tags'       => 'json',
        'created_at' => 'datetime',
        'status'     => 'enum:' . Status::class,
    ];

    protected static array $indexes = [
        ['type' => 'unique', 'columns' => ['email']],
        ['type' => 'index',  'columns' => ['role_id']],
    ];

    protected static array $foreignKeys = [
        ['columns' => ['role_id'], 'references' => 'roles', 'on' => ['id'],
         'on_delete' => 'set null', 'on_update' => 'cascade'],
    ];
}

foreach (User::indexesSql()     as $sql) $pdo->exec($sql);
foreach (User::foreignKeysSql() as $sql) $pdo->exec($sql);

use Cloude\Model\Model;

enum Status: string { case Active = 'active'; case Banned = 'banned'; }

class Product extends Model
{
    protected static string $table = 'products';
    protected static array  $types = [
        'id'          => 'int',
        'price'       => 'decimal:2',
        'in_stock'    => 'bool',
        'tags'        => 'json',
        'created_at'  => 'datetime',
        'status'      => 'enum:' . Status::class,
    ];
}

$p = Product::find(1);
$p->price;          // string "12.50"   (preserves precision; use bcmath for arithmetic)
$p->in_stock;       // bool
$p->tags;           // array
$p->created_at;     // \DateTimeImmutable
$p->status;         // Status::Active

$q = User::query();              // shorthand
$q = new Query($pdo, 'users');   // direct
$q = new Query($pdo, User::as('u'));   // aliased

$q->where('active', 1)
  ->where('age', '>', 18)
  ->orderBy('name')
  ->limit(10)
  ->get();                                // list<array<string,mixed>>

$q->where('email', '[email protected]')->first();   // ?array
$q->where('active', 1)->count();          // int
$q->select('email')->pluck('email');      // list<string>
$q->select('id', 'name')->pluck('name', 'id');   // [id => name, ...]
$q->select('name')->where(...)->value('name');   // first scalar

$q->where('age', '>', 18);
$q->where('email', '[email protected]');           // shorthand → '='
$q->where('age', '=', null);             // auto-rewritten as IS NULL
$q->whereIn('id', [1, 2, 3]);
$q->whereNotIn('id', [...]);
$q->whereNull('deleted_at');
$q->whereNotNull('email');
$q->whereBetween('age', 18, 65);
$q->orWhere('role', 'admin');            // OR-joined to previous predicate

$q->where('active', 1)
  ->whereGroup(fn ($g) =>
      $g->where('role', 'admin')->orWhere('role', 'editor'))
  ->orWhereGroup(fn ($g) =>
      $g->where('country', 'ES')->where('vip', 1));

// → WHERE active = 1
//      AND (role = 'admin' OR role = 'editor')
//      OR  (country = 'ES' AND vip = 1)

$rows = User::query()
    ->select('users.name', 'orders.total')
    ->leftJoin('orders', 'orders.user_id', '=', 'users.id')
    ->where('orders.status', 'paid')
    ->orderBy('users.name')
    ->get();

$q->select('id', ['name', 'type_name']);          // recommended — typed tuple
$q->select('id', 'name AS type_name');            // also accepted (legacy / hand-written)

// Same with helpers that emit the tuple shape:
$q->select('id', User::alias('name', 'type_name'));
//   ['users.name', 'type_name']  → `users`.`name` AS `type_name`

$u = User::as('u');
$q->from($u)->select($u->alias('name', 'who'));
//   ['u.name', 'who']            → `u`.`name` AS `who`

$u = User::as('u');
$o = Order::as('o');

$rows = User::query()->from($u)
    ->select($u->field('name'), $o->field('total'))
    ->join($o, $o->field('user_id'), '=', $u->field('id'))
    ->where($o->field('status'), 'paid')
    ->get();

// SELECT `u`.`name`, `o`.`total`
// FROM `users` AS `u`
// JOIN `orders` AS `o` ON `o`.`user_id` = `u`.`id`
// WHERE `o`.`status` = ?

$id    = $q->insert(['name' => 'Ada', 'email' => 'a@x']);   // last-insert id
$count = $q->where('age', '<', 18)->update(['active' => 0]); // affected rows
$count = $q->where('active', 0)->delete();

echo $q->where('age', '>', 18)->compile();
// SELECT * FROM `users` WHERE `age` > 18

use Cloude\Storage\{StorageException, DuplicateKeyException, TableNotFoundException};

try {
    User::create(['email' => $email, 'name' => $name]);
} catch (DuplicateKeyException $e) {
    return Response::json(['error' => 'email_taken'], 409);
} catch (TableNotFoundException $e) {
    // Probably a missing migration.
    Logger::error("missing table", ['sql' => $e->sql]);
    throw $e;
} catch (StorageException $e) {
    // Anything else SQL-related — log structured fields, re-throw or
    // render. $e->getPrevious() is the original PDOException.
    Logger::error('db', [
        'sqlstate' => $e->sqlState,
        'sql'      => $e->sql,
        'bindings' => $e->bindings,
        'message'  => $e->getMessage(),
    ]);
    throw $e;
}

use Cloude\Storage\Transaction;

$orderId = Transaction::run(function () use ($payload) {
    $order = Order::create($payload);
    Inventory::reserve($order->id, $payload['items']);
    return $order->id;
});
// Throws → ROLLBACK (and rethrows). Returns → COMMIT.

Transaction::begin();
try {
    Order::create([...]);
    AuditLog::create([...]);
    Transaction::commit();
} catch (\Throwable $e) {
    Transaction::rollback();
    throw $e;
}

Transaction::inTransaction();    // bool — any depth > 0
Transaction::depth();            // 0 outside, 1 = BEGIN, 2+ = SAVEPOINT levels

Transaction::run(function () {
    Order::create(...);                        // outer work

    try {
        Transaction::run(fn () => Risky::do());  // SAVEPOINT
    } catch (\Throwable) {
        // inner rollback already happened; outer keeps going
    }

    AuditLog::create(...);                     // also kept
});

Transaction::run($fn, 'analytics');
Transaction::begin('replica_writes');

use Cloude\Storage\Schema;

$sql = Schema::createTableSql('users', [
    'id'         => ['type' => 'BIGINT', 'unsigned' => true, 'null' => false, 'auto_increment' => true, 'primary' => true],
    'email'      => ['type' => 'VARCHAR(255)', 'null' => false],
    'role_id'    => ['type' => 'BIGINT',       'unsigned' => true, 'null' => true,  'default' => null],
    'created_at' => ['type' => 'DATETIME',     'null' => false, 'default' => 'CURRENT_TIMESTAMP'],
], indexes: [
    ['type' => 'unique', 'columns' => ['email']],
    ['type' => 'index',  'columns' => ['role_id']],
], foreignKeys: [
    [
        'columns'    => ['role_id'],
        'references' => 'roles',
        'on'         => ['id'],
        'on_delete'  => 'set null',
        'on_update'  => 'cascade',
    ],
]);
// → CREATE TABLE `users` (
//     `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
//     `email` VARCHAR(255) NOT NULL,
//     `role_id` BIGINT UNSIGNED NULL DEFAULT NULL,
//     `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
//     UNIQUE KEY `uq_users_email` (`email`),
//     KEY `idx_users_role_id` (`role_id`),
//     CONSTRAINT `fk_users_role_id` FOREIGN KEY (`role_id`)
//       REFERENCES `roles` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
//   ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

echo Schema::indexSql('users', ['type' => 'unique', 'columns' => ['email']]);
// → CREATE UNIQUE INDEX `uq_users_email` ON `users` (`email`)

echo Schema::foreignKeySql('orders', [
    'columns'    => ['user_id'],
    'references' => 'users',
    'on'         => ['id'],
    'on_delete'  => 'set null',
]);
// → ALTER TABLE `orders` ADD CONSTRAINT `fk_orders_user_id`
//   FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL

class User extends Model {
    protected static string $table = 'users';

    protected static array $indexes = [
        ['type' => 'unique', 'columns' => ['email']],
        ['type' => 'index',  'columns' => ['role_id']],
    ];

    protected static array $foreignKeys = [
        ['columns' => ['role_id'], 'references' => 'roles', 'on' => ['id'],
         'on_delete' => 'set null', 'on_update' => 'cascade'],
    ];
}

foreach (User::indexesSql() as $sql) { $pdo->exec($sql); }
foreach (User::foreignKeysSql() as $sql) { $pdo->exec($sql); }

Schema::foreignKeySql('orders', [
    'columns'    => ['user_id'],
    'references' => 'users',
    'on'         => ['id'],
    // on_delete / on_update omitted
]);
// → ALTER TABLE `orders` ADD CONSTRAINT `fk_orders_user_id`
//   FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
//   ON DELETE NO ACTION ON UPDATE NO ACTION

use Cloude\Session;

// Typed access — the first call auto-starts the session with hardened defaults.
Session::set('user_id', 42);
$id = Session::get('user_id', $default = null);
Session::has('user_id');
Session::forget('user_id');
$snapshot = Session::all();        // excludes internal flash/CSRF buckets

// Flash — value survives exactly one redirect
Session::flash('success', 'Saved.');
$msg = Session::pullFlash('success');   // null on the originating request,
                                        // 'Saved.' on the next one (then gone)
Session::reflash('success');            // keep it for one more cycle

// CSRF
$token = Session::csrfToken();          // minted on first call, stable per session
if (!Session::checkCsrf((string) Input::post('_csrf'))) {
    throw new \Cloude\Http\HttpException(419, 'CSRF token mismatch');
}

// On login (prevents fixation)
Session::regenerate();

// On logout
Session::destroy();

Session::start([], cookieParams: [
    'samesite' => 'Strict',
    'domain'   => '.example.com',
]);
// any subsequent Session::* calls in this request reuse the
// configured params; auto-start no longer fires.



\Cloude\Bootstrap::initPaths(
    docroot: __DIR__,
    apppath: dirname(__DIR__) . '/app',
    // basepath: defaults to dirname(apppath) if omitted
);
\Cloude\Config::configure(APPPATH . '/config');

if (\Cloude\Bootstrap::serveStaticIfExists(DOCROOT)) {
    return false;
}

\Cloude\Bootstrap::run();   // reads debug + views from Config

$router = new \Cloude\Router(\Cloude\Config::baseUrl(['example.com', 'localhost']));
// ...routes...
$router->dispatch();

use Cloude\Http\Response;

Response::json(['ok' => true]);                  // 200 + application/json
Response::json(['error' => 'nope'], 422);
Response::html('<h1>Hi</h1>');
Response::xml('<?xml version="1.0"

// Env-var helpers (always available)
Config::env('OPENAI_API_KEY');           // ?string — empty string treated as missing
Config::boolEnv('DEBUG', false);          // bool — 1/true/yes/on (case-insensitive)

// Wire the loader (once, in www/index.php right after initPaths)
Config::configure(APPPATH . '/config');
// optional: ::configure($path, environment: 'prod')
// otherwise APP_ENV / ENVIRONMENT env vars decide

// Read a file/dot-path
Config::get('db.default.dsn');            // any value
Config::load('db');                       // whole merged array
Config::environment();                    // current env name ('dev' default)

// Typed accessors — the recommended way to read framework knobs
Config::baseUrl(['example.com']);         // memoized; reads app.base_url, then env, then auto-detect
Config::debug();                          // bool — reads app.debug, then env DEBUG
Config::path('data');                     // app.paths.data
Config::path('cache', '/tmp/c');          // with fallback

// Legacy global-constants helpers (still supported, back-compat)
Config::defineBaseUrl(['example.com']);   // → define('BASE_URL', ...)
Config::defineDebug();                    // → define('DEBUG', ...)

return [
    'base_url' => Cloude\Config::env('BASE_URL'),    // null → auto-detect
    'debug'    => Cloude\Config::boolEnv('DEBUG'),
    'timezone' => Cloude\Config::env('TZ', 'UTC'),   // Bootstrap::run() applies this
    'paths' => [
        'data'  => BASEPATH . '/data',
        'views' => APPPATH . '/views',
    ],
];

use Cloude\DateTime;

// Static constructors
DateTime::now();                        // current moment
DateTime::today();                      // today at 00:00:00
DateTime::parse('2026-05-18 14:30');    // throws \InvalidArgumentException on bad input
DateTime::fromTimestamp(1716000000);

// Format shortcuts
$d->toDateString();        // 'Y-m-d'         → '2026-05-18'
$d->toTimeString();        // 'H:i:s'         → '14:30:00'
$d->toDateTimeString();    // 'Y-m-d H:i:s'   → '2026-05-18 14:30:00'
$d->toIsoString();         // 'c' / RFC 3339  → '2026-05-18T14:30:00+02:00'
(string) $d;               // 'Y-m-d H:i:s' — MySQL-shaped, drops the offset
                           //   Use toIsoString() when you need timezone in the output.
                           //   Interpolation works: "saved at $d" → "saved at 2026-05-18 …"

// Arithmetic (immutable — always returns a new instance)
$d->addDays(7)->subHours(2)->addMinutes(30);
$d->addWeeks(1); $d->addMonths(1); $d->addYears(1);
$d->subSeconds(10); $d->subDays(3); // ... etc

// Boundaries
$d->startOfDay();          // 00:00:00
$d->endOfDay();            // 23:59:59
$d->startOfMonth();        // 1st of the month at 00:00:00
$d->endOfMonth();          // last of the month at 23:59:59

// Comparisons
$d->isPast();     $d->isFuture();
$d->isToday();    $d->isYesterday();  $d->isTomorrow();
$d->isBefore($other);  $d->isAfter($other);  $d->isSameDay($other);

// Signed diffs — positive when $other is later than $this
$d->diffInSeconds($other);
$d->diffInMinutes($other);
$d->diffInHours($other);
$d->diffInDays($other);

// English "5 minutes ago" / "in 3 days"
$d->diffForHumans();           // vs now()
$d->diffForHumans($reference); // vs explicit reference (testable)

ob_start();
\Cloude\Http\ErrorHandler::register(
    debug:    DEBUG,
    viewBase: dirname(__DIR__) . '/app/views', // optional override directory
);

use Cloude\Http\HttpException;
use Cloude\Http\NotFoundException;

// Idiomatic 404 from a controller:
$book = $repo->find($isbn) ?? throw new NotFoundException("book $isbn");

// Any HTTP status — message goes into the JSON / debug output:
throw new HttpException(403, 'forbidden');

Cache::ok();                          // 1d at the CDN, browser revalidates every request
Cache::ok(3600);                      // 1h at the CDN, browser revalidates every request
Cache::ok(86400, 300);                // 1d at the CDN, 5min in the browser
Cache::notFound();                    // short CDN TTL on 404
Cache::unavailable();                 // no-store + Retry-After on 5xx

if (Cache::conditionalGet(filemtime($path))) {
    return; // 304 sent
}

AssetUrl::configure(BASE_URL, __DIR__ . '/../www/assets');
echo AssetUrl::get('css/styles.css');
// → "{BASE_URL}/{mtime}/assets/css/styles.css"

use Cloude\Markdown\File;

File::exists($path);                   // bool — .md or .md.gz
File::read($path);                     // string — auto-decompressed
File::readPrefix($path, 4096);         // first N bytes (for frontmatter)
File::mtime($path);                    // int
File::write($path, $content);          // writes .md.gz, removes .md

\Cloude\Markdown\Server::serve($path, BASE_URL . '/articles/foo');

use Cloude\Format;

// JSON
Format::json('{"a":1}');                   // ['a' => 1]
Format::json(['a' => 1]);                  // '{"a":1}'
Format::json(['a' => 1], pretty: true);    // pretty-printed

// YAML (flat key:value, frontmatter-compatible)
Format::yaml("title: Hi\nflag: true");     // ['title' => 'Hi', 'flag' => true]
Format::yaml(['title' => 'Hi']);           // "title: Hi\n"

// XML — keys with '@' become attributes, '#text' is text content,
// list arrays repeat the element. See examples/recipes/sitemap.php.
Format::xml(['urlset' => [
    '@xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
    'url'    => [['loc' => 'a'], ['loc' => 'b']],
]], pretty: true);

// Markdown → HTML
Format::markdown('# Hello **world**');     // "<h1>Hello <strong>world</strong></h1>\n"

use Cloude\JsonFile;

JsonFile::read($path);                 // ?array — cached, null if missing/invalid
JsonFile::readOr($path, []);           // array — never null
JsonFile::write($path, $data);         // atomic (temp + rename); UNESCAPED_UNICODE | UNESCAPED_SLASHES
JsonFile::write($path, $data, true);   // pretty-print
JsonFile::clearCache();                // clear all, or pass a path

EventLog::configure('https://webhook.site/<uuid>');
EventLog::send(['event' => 'page_view', 'path' => '/foo']);

$schema = [
    'type' => 'object',
    'properties' => [
        'country' => ['type' => 'string', 'pattern' => '^[a-z]{2}$'],
        'limit'   => ['type' => 'integer', 'minimum' => 1, 'maximum' => 1000],
    ],
    'gs, $schema);   // bool shortcut

use Cloude\Mcp\JsonRpc;
use Cloude\Mcp\McpException;
use Cloude\Mcp\Server;

$mcp = new Server(
    name:        'my-data',
    version:     '1.0',
    description: 'Public dataset.',
    endpoint:    BASE_URL . '/mcp',
);

$mcp->tool(
    name:        'echo',
    description: 'Echoes the message.',
    inputSchema: [
        'type'       => 'object',
        'properties' => ['message' => ['type' => 'string', 'minLength' => 1]],
        'i' => 'mem://hi', 'name' => 'Hi', 'mimeType' => 'text/plain']]);
$mcp->resourceReader(fn($uri) => $uri === 'mem://hi'
    ? ['uri' => $uri, 'mimeType' => 'text/plain', 'text' => 'world']
    : null);

// Wire up routes.
$router->get('/.well-known/mcp.json', fn () => $mcp->respondManifest());
$router->any(['/mcp', '/mcp-server'], fn () => $mcp->dispatch());

#!/usr/bin/env php

  = Cli::parseArgs($argv);          // ['_' => [...], 'dry-run' => true, 'limit' => '100', ...]
$dryRun = Cli::flag($args, 'dry-run');    // bool
$limit  = (int) (Cli::option($args, 'limit') ?? 100);
$path   = Cli::positional($args, 0);      // first non-flag argument

Cli::info("processing $limit items" . ($dryRun ? ' (dry run)' : ''));
if ($errors > 0) {
    Cli::abort(1, "$errors items failed");
}
Cli::success('done');

$log = new \Cloude\Logger('/var/log/myapp.log', minLevel: 'info');
$log->info('http request', ['path' => '/foo']);
$log->error('db unreachable', ['code' => 503]);
// → /var/log/myapp-2026-05-07.log:
//   [2026-05-07T08:30:12Z] [INFO] http request {"path":"/foo"}
//   [2026-05-07T08:30:12Z] [ERROR] db unreachable {"code":503}

use Cloude\Cli;
use Cloude\TaskRunner;

class ContentTasks
{
    /** Rebuild the search index for $country (default es). */
    public static function rebuildIndex(array $args): int
    {
        $country = Cli::option($args, 'country', 'es');
        $dryRun  = Cli::flag($args, 'dry-run');
        Cli::info("rebuilding index for {$country}" . ($dryRun ? ' (dry run)' : ''));
        return 0;
    }

    /** Drop content older than N days (default 90). */
    public static function purgeOld(array $args): int
    {
        $days = (int) (Cli::option($args, 'days') ?? 90);
        Cli::info("purging content older than {$days} days");
        return 0;
    }
}

$runner = new TaskRunner();
$runner->register('ping', fn () => Cli::out('pong'), 'Connectivity check.');
$runner->registerClass('content', ContentTasks::class);

exit($runner->run($argv));

use Cloude\Domain\{ValueObject, AggregateRoot, DomainEvent, DomainException};

final class Money extends ValueObject
{
    public function __construct(
        public readonly int $amount,        // cents
        public readonly string $currency,
    ) {
        if ($amount < 0) {
            throw new DomainException('Money cannot be negative');
        }
    }
    public function __toString(): string {
        return number_format($this->amount / 100, 2) . ' ' . $this->currency;
    }
}

final class BookBorrowed implements DomainEvent
{
    public function __construct(
        public readonly string $isbn,
        public readonly string $memberId,
        public readonly \DateTimeImmutable $when,
    ) {}
    public function occurredOn(): \DateTimeImmutable { return $this->when; }
}

final class Book extends AggregateRoot
{
    public function __construct(
        public readonly string $isbn,
        public readonly string $title,
        private int $copiesAvailable,
    ) {}

    public function borrow(string $memberId): void {
        if ($this->copiesAvailable === 0) {
            throw new DomainException("No copies of '{$this->title}' available");
        }
        $this->copiesAvailable--;
        $this->recordEvent(new BookBorrowed($this->isbn, $memberId, new \DateTimeImmutable()));
    }
}

// Application layer:
$book->borrow($memberId);
$repo->save($book);
foreach ($book->pullDomainEvents() as $event) {
    $eventLog->record($event);
}

use Cloude\Testing\DataProvider;
use Cloude\Testing\TestCase;

final class StrTest extends TestCase
{
    protected function setUp(): void { /* per-test fixture */ }
    protected function tearDown(): void { /* per-test cleanup */ }

    public function testSlugAscii(): void
    {
        self::assertSame('hello-world', \Cloude\Str::slug('Hello World'));
    }

    public function testInvalidInputThrows(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('empty');
        \Cloude\Str::slug('');
    }

    #[DataProvider('truthyCases')]
    public function testBool(string $input, bool $expected): void
    {
        self::assertSame($expected, \Cloude\Config::boolEnv($input, false));
    }

    public static function truthyCases(): array
    {
        return [
            'yes'   => ['yes',   true],
            'no'    => ['no',    false],
            'empty' => ['',      false],
        ];
    }
}

use Cloude\Testing\TestCase;
use Cloude\Http\NotFoundException;

final class BorrowingTest extends TestCase
{
    public function test_returns_404_when_book_missing(): void
    {
        $this->useArrayModel(Book::class, []);
        $this->assertHttpException(404, function (): void {
            Book::find('does-not-exist') ?? throw new NotFoundException('book');
        });
    }

    public function test_borrow_records_event_at_frozen_time(): void
    {
        $when = $this->freezeTime('2026-05-18 12:00:00');
        $this->useArrayModel(Book::class, [['isbn' => '978', 'copies' => 1]]);

        $book = Book::find('978');
        $book->borrow('member-42');

        $events = $book->pullDomainEvents();
        $this->assertCount(1, $events);
        $this->assertSame($when->getTimestamp(), $events[0]->occurredOn()->getTimestamp());
    }

    public function test_controller_deletes_user_on_ban(): void
    {
        $store = $this->useMockModel(User::class, [
            ['id' => 1, 'email' => 'ada@x', 'active' => 1],
        ]);

        (new BanUserController())->ban(1);

        $this->assertModelReceived($store, 'update', times: 1);
        $this->assertModelDidNotReceive($store, 'delete');
        self::assertSame([1, ['email' => 'ada@x', 'active' => 0]], $store->lastCall('update'));
    }
}