Download the PHP package cloude/framework without Composer
On this page you can find all versions of the php package cloude/framework. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download cloude/framework
More information about cloude/framework
Files in cloude/framework
Package framework
Short Description Minimalist PHP 8.3+ micro-framework: router, input, views, markdown and string helpers. No magic, no service container. Optional persistence: file-based (JSON, Markdown) and a thin PDO Active Record via Cloude\Model. Ships its own test runner (bin/cloude-test).
License MIT
Informations about the package framework
Cloude Framework
A minimalist PHP micro-framework. No magic, no service container. Persistence is opt-in: file-based (JSON / Markdown) by default, with a thin Active Record over PDO via Cloude\Model when you need a relational database.
- PHP 8.3+ (tested on 8.3, 8.4 and 8.5)
- PSR-4 autoloading, namespace
Cloude\ - PSR-12 / PER-CS 2.0 coding style
declare(strict_types=1)everywhere- Zero runtime dependencies. Markdown rendering is in-house. Slug
transliteration uses
ext-intlwhen present (Cyrillic, Greek and other scripts romanise correctly). Withoutext-intlnon-ASCII characters are dropped — install it for sites that need accent-aware slugs.
Built with Claude Code
Cloude is shaped while pair-coding with Claude Code — Anthropic's terminal-native AI coding agent. The "one file per class, no magic, no DSL, no annotations" rules aren't aesthetic preferences: they exist so the agent (and you) can reason about any piece of the framework without loading a runtime in your head.
Try it:
Then ask Claude Code to scaffold a front controller with Cloude\Bootstrap,
expose an MCP server with Cloude\Mcp\Server, or migrate a routes file into
nested groups. The surface area is small enough that the agent usually nails
it in one prompt.
For agents: see
AGENTS.md— a tight reference card with the decision matrix, idioms and anti-patterns. AI tools (Claude Code, Cursor, Codex, …) pick it up automatically when they see the file at the repo root.For Claude Code users specifically:
CLAUDE.mdis the "from zero to running feature" playbook — quick start, project layout, the standard add-a-feature workflow, common patterns, and how to brief Claude effectively. Copy it into your own project root once you start building on top ofcloude/framework.For design-pattern guidance:
PATTERNS.mdis the decision guide for picking an architecture — Transaction Script, MVC
- Repository, or DDD layered. It maps "what your app looks like" to "which example to copy", with migration paths and anti-patterns.
For brand-new projects, AI-guided:
SETUP.mdis an interview script for AI coding agents. Point Claude Code (or any tool-capable agent) at this file and it walks you through eight steps — namespace, docroot, run mode (php -S/ Docker / both), pattern, CSS, JS, optional modules — and scaffolds the project based on your answers.
Installation
Repository layout
Components
Core
| Class | Responsibility |
|---|---|
Cloude\Arr |
Array helpers with dot-notation: get/set/has/forget/pluck/only/except/dot/undot/merge |
Cloude\Bootstrap |
Front-controller bootstrap: initPaths() defines DOCROOT/APPPATH/BASEPATH; run() wires cli-server passthrough + ob_start + ErrorHandler + view base (pulls debug/views from Config when omitted) |
Cloude\Cli |
Argv parsing + colored output for app/cli/ scripts |
Cloude\Collection |
Fluent, chainable wrapper: map/filter/reduce/pluck/keyBy/groupBy/sortBy/take/chunk/unique/sum/avg/min/max/... |
Cloude\Config |
Env helpers (env/boolEnv); multi-env file loader (configure/load/get); typed accessors (baseUrl/debug/path); legacy defineBaseUrl/defineDebug |
Cloude\DateTime |
Tiny immutable date helper extending \DateTimeImmutable. Static constructors (now/today/parse/fromTimestamp); format shortcuts (toDateString/toTimeString/toDateTimeString/toIsoString); arithmetic (addDays/addHours/addMinutes/…); boundaries (startOfDay/endOfMonth/…); comparisons (isPast/isToday/isSameDay/…); signed diffIn{Days,Hours,Minutes,Seconds} and English diffForHumans(). Carbon-style setTestNow() / clearTestNow() for test isolation. Used automatically by the datetime cast |
Cloude\EventLog |
Fire-and-forget POST to a webhook for usage analytics |
Cloude\Format |
Yaml / json / xml / markdown encode-decode dispatcher (string ↔ array) |
Cloude\Input |
Wrapper over $_GET, $_POST, $_SERVER, raw body and JSON |
Cloude\JsonFile |
Per-request cached, atomic-write helper for JSON files |
Cloude\JsonSchema |
In-house JSON Schema subset validator (no external deps) |
Cloude\Logger |
File-backed logger with daily rotation and debug/info/warn/error |
Cloude\TaskRunner |
CLI task runner. prefix:method dispatch over registered callables or static class methods, with auto list / help |
Cloude\Router |
Router with /{param}, /{param?}, /{param:regex} patterns, nested route groups, and get/post/put/patch/delete/any helpers |
Cloude\Session |
Static façade over $_SESSION with hardened cookie defaults (httponly, samesite=Lax, secure on HTTPS). Typed get/set/has/forget/all, flash messages (flash/pullFlash/reflash), CSRF helpers (csrfToken/checkCsrf), regenerate() for login flows |
Cloude\Str |
String utilities: upTo/truncate/truncateMiddle/words/after/afterLast/between/squish/mask, slug/ascii, camel/pascal/snake/kebab, random/uuid/hash |
Cloude\View |
Plain PHP template rendering with variable extraction and HTML escape |
Cloude\Http\…
| Class | Responsibility |
|---|---|
Cloude\Http\AssetUrl |
Versioned asset URLs (/{mtime}/assets/...) for cache-busting |
Cloude\Http\Cache |
HTTP cache headers (ok, notFound, unavailable) and conditionalGet() |
Cloude\Http\ErrorHandler |
Global error handler. Defaults to 503 (Retry-After: 600); honors HttpException::statusCode when thrown. HTML / JSON / .md / CLI / AJAX negotiation, debug mode |
Cloude\Http\HttpException |
Throwable carrying a status code; caught by ErrorHandler to render with that status |
Cloude\Http\NotFoundException |
HttpException pinned to 404; renders bundled 404.html.php (or your override) |
Cloude\Http\Response |
One-call response helpers: json, html, xml, markdown, redirect, notFound, noContent |
Cloude\Data\…
| Class | Responsibility |
|---|---|
Cloude\Data\Repository |
Abstract base for directory-of-files repositories. Subclasses implement find / slugs / path; exists, findOr, all and the transform() hook come for free |
Cloude\Data\JsonRepository |
One .json per entity. Atomic writes, per-request read cache via JsonFile |
Cloude\Data\MarkdownRepository |
One .md (or .md.gz) per entity. Read returns frontmatter + parsed HTML via Markdown::parse |
Cloude\Model\… and Cloude\Storage\…
| Class | Responsibility |
|---|---|
Cloude\Model\Model |
Abstract Active Record. Subclass with protected static string $table + $connection (+ optional $types). CRUD via find / findBy / create / save / delete. Static helpers: table(), field('col'), as('alias'), ref(), query() |
Cloude\Model\Cast |
Opt-in attribute coercion driven by the $types map: int, float, string, bool, decimal[:N], json/array, datetime[:FMT], date[:FMT], enum:FQCN. Null passes through |
Cloude\Model\Storage\PdoStorage |
PDO-backed adapter. Driven by Cloude\Storage\Connection named pool |
Cloude\Model\Storage\JsonStorage |
One JSON file per row (collection mode) or one big array (collection storage) |
Cloude\Model\Storage\MarkdownStorage |
Markdown body + frontmatter as a row |
Cloude\Model\Storage\ArrayStorage |
In-memory rows; ideal for tests |
Cloude\Storage\Query |
Fluent SQL builder. SELECT / INSERT / UPDATE / DELETE + WHERE (with nested AND/OR groups) + INNER/LEFT/RIGHT/CROSS JOIN + ORDER BY + LIMIT/OFFSET + count() |
Cloude\Storage\TableRef |
Lightweight (table, alias) value object. Pair with Model::as('u') for typed joins; __toString() yields the quoted FROM/JOIN expression |
Cloude\Storage\Identifier |
SQL identifier-quoting helper. quote() for single names, qualify() for table.column / table.* / * |
Cloude\Storage\Connection |
Named PDO pool keyed off Config::get('storage.{name}') |
Cloude\Storage\Factory |
Builds a Storage adapter from a config row (dispatches on driver) |
Cloude\Storage\StorageException |
Framework-level wrapper around \PDOException. Public readonly $sqlState, $sql, $bindings. Specialised subclasses: TableNotFoundException (42S02/42P01), ColumnNotFoundException (42S22/42703), DuplicateKeyException (23000+1062, 23505), IntegrityConstraintException (23xxx), ConnectionException (08xxx), SyntaxErrorException (42000/42601) |
Cloude\Storage\Transaction |
begin / commit / rollback / inTransaction / depth + closure-based run(fn, $connection). Real nested transactions via SAVEPOINTs. Wraps PDO errors as StorageException |
Cloude\Storage\Schema |
DDL emitter — produces CREATE TABLE / DROP TABLE SQL from structured arrays. Indexes (UNIQUE/INDEX), foreign keys (ON DELETE/ON UPDATE), DEFAULT NULL, composite primary keys, MySQL + Postgres dialects. Not a migration framework — feed the SQL to your existing tooling |
Cloude\Markdown\…
| Class | Responsibility |
|---|---|
Cloude\Markdown |
Frontmatter + body parser. Body rendered via Markdown\Parser by default; swappable with useParser() |
Cloude\Markdown\File |
Disk I/O for markdown with transparent gzip (.md + .md.gz) |
Cloude\Markdown\Parser |
In-house markdown → HTML parser. No external dependency |
Cloude\Markdown\Server |
Serves a markdown file with 304 / canonical / gzip passthrough |
Cloude\Mcp\…
| Class | Responsibility |
|---|---|
Cloude\Mcp\Server |
MCP server (Model Context Protocol) over HTTP / JSON-RPC 2.0, with auto inputSchema validation |
Cloude\Mcp\JsonRpc |
Constants for JSON-RPC 2.0 + MCP error codes |
Cloude\Testing\… — built-in test framework (no PHPUnit dependency)
| Class | Responsibility |
|---|---|
Cloude\Testing\TestCase |
Test base class. Lifecycle (setUp/tearDown), PHPUnit-compatible assertion surface (assertSame/assertTrue/assertInstanceOf/…), exception expectations, Cloude-specific helpers (useArrayModel, useSqliteModel, useMockModel, captureHttp, freezeTime, …) |
Cloude\Testing\Assert |
Static assertion library used by TestCase. Each method increments an internal counter shown in the runner's summary |
Cloude\Testing\Runner |
Discovery + execution + reporting. bin/cloude-test calls Runner::main($argv). Supports --filter=PATTERN and one or more path arguments |
Cloude\Testing\DataProvider |
#[DataProvider('cases')] attribute — name a static method returning an iterable of arg arrays; one test invocation per row |
Cloude\Testing\AssertionFailedException |
Thrown by Assert methods on failure. The runner catches it to flag the test as failed (vs. errored) |
Cloude\Testing\MockStorage |
Recording wrapper around ArrayStorage for behaviour assertions ($store->received('update', times: 1)) |
Cloude\Mcp\McpException |
Structured-error throwable for tool / resource handlers |
Quick start
A typical www/index.php looks like this:
Bootstrap::run wires up ob_start, the global 503 error handler, and the
view base path in one call. Drop in app/config.php and you have a complete
front controller in ~15 lines.
Example projects
Ready-to-run sample apps live in examples/. Each one is
fully self-contained — no cross-dependencies, no Apache or nginx
required, no composer install needed when running from inside this
repo. From the repository root:
| Folder | What it shows |
|---|---|
examples/basic/ |
Front-controller skeleton: routing, dynamic params, JSON echo, plain-PHP views |
examples/contacts/ |
Form + JsonSchema validation + accent-insensitive search + fetch() from JS |
examples/library/ |
DDD layering: Domain / Application / Infrastructure / Presentation |
examples/recipes/ |
Standalone snippets — sitemap, JSON-LD, MCP, CLI tasks, repos |
Run via Docker without installing PHP locally — see
DEPLOYMENT.md.
Class reference
Cloude\Router
{name} segments are extracted into an associative array and passed as the
first handler argument.
Pattern syntax:
| Form | Meaning |
|---|---|
/{name} |
captures any non-slash segment as $params['name'] |
/{name?} |
optional — the segment and its leading / can be absent |
/{name:regex} |
constrains the capture to regex (e.g. \d+, [a-z]{2}) |
/{name?:regex} |
optional + constrained |
Examples:
Route groups stack a URL prefix onto a block of routes. Nestable.
Cloude\Input
Cloude\View
View templates use the .html.php double extension by convention —
visually separates HTML-producing templates from PHP source files
(controllers, models) and improves IDE highlighting. The render() /
capture() methods don't enforce it; any require-able path works.
Short names inside views
include-based templates run in their own scope, so View::e() etc.
need the framework namespace. Two equally supported shapes — pick the
one that fits the project:
Option A — standard use statement (per-view, explicit, IDE-friendly):
Option B — declarative aliases in app/config/app.php (one
declaration, every view inherits):
Bootstrap::run() reads the aliases list and calls
class_alias('Cloude\<short>', '<short>') for each entry. The
framework skips silently when the short name is already taken
(your own classes, PHP built-ins, prior aliases) so user code is
never stomped. Default config registers no aliases — the feature is
strictly opt-in.
Bundled examples (examples/basic/, examples/contacts/,
examples/library/) use Option A so they're explicit about every
import. Real apps with many views often prefer Option B to drop the
use boilerplate.
Cloude\Markdown
Supports minimal YAML frontmatter (single-line key: value pairs):
The body is rendered with the in-house Cloude\Markdown\Parser. To swap in a
different engine (e.g. Parsedown if you want its full feature set):
Cloude\Markdown\Parser
Minimalist Markdown → HTML parser. Covers the editorial subset:
| Block | Inline | |
|---|---|---|
ATX headings (#–######) |
**bold** / __bold__ |
|
| Paragraphs | *italic* / _italic_ |
|
| Unordered / ordered lists | `inline code` |
|
| Fenced code blocks (` markdown | ||
| Type | Bore | License |
| --- | :---: | ---: |
| Shotgun | smooth | E |
| Rifle | rifled | D |
app/config/ ├── app.php # base — always loaded ├── db.php ├── mail.php ├── dev/ # active when environment is 'dev' │ └── app.php # deep-merged onto the base └── prod/ ├── app.php └── db.php json {"error": "not_found", "status": 404} apacheconf RewriteRule ^[0-9]+/assets/(.*)$ /assets/$1 [L] bash php app/cli/tasks.php # list every task php app/cli/tasks.php help content:rebuild-index # describe one php app/cli/tasks.php content:rebuild-index --country=fr --dry-run php app/cli/tasks.php content:purge-old --days=30 bash composer install composer test # cloude-test (Cloude\Testing\Runner) composer cs-check # php-cs-fixer in dry-run mode composer cs-fix # apply fixes bash git tag -a v0.15.0 -m "v0.15.0" git push origin v0.15.0 bash composer require cloude/framework bash vendor/bin/cloude-test # run tests/ (default) vendor/bin/cloude-test tests/Storage # scope to a directory vendor/bin/cloude-test --filter=Cast # regex match on ClassName::method vendor/bin/cloude-test --help # usage summary
The runner discovers `*Test.php` files recursively, instantiates every
class extending `Cloude\Testing\TestCase`, and runs each public
method whose name starts with `test`. Dots / `F` / `E` printed
PHPUnit-style; final summary lists failures and errors with stack
traces. Exit code 0 on green, 1 on anything red.
**Why ship a custom runner?** The framework's philosophy is small,
hand-rolled, no dependencies the user didn't ask for. Dropping PHPUnit
(15+ MB in `vendor/`) keeps `composer install` lean for downstream
consumers; the runner itself is < 500 LOC across `Runner.php`,
`TestCase.php`, `Assert.php` and `bin/cloude-test`. PHPUnit's API
shape (method names, attributes, lifecycle) is mirrored so the muscle
memory transfers and existing tests migrate with two `use`
substitutions.
#### Writing a test
#### Assertions
PHPUnit-compatible names (`assertSame`, `assertTrue`, etc.) are
available both as `$this->...` and `self::...` (route to the same
implementation). The full list:
| Equality | `assertSame`, `assertNotSame`, `assertEquals`, `assertEqualsWithDelta` |
| Booleans / null | `assertTrue`, `assertFalse`, `assertNull`, `assertNotNull` |
| Containers | `assertCount`, `assertEmpty`, `assertNotEmpty`, `assertContains`, `assertArrayHasKey`, `assertArrayNotHasKey` |
| Type | `assertInstanceOf`, `assertNotInstanceOf`, `assertIsString` |
| Strings | `assertStringContainsString`, `assertStringNotContainsString`, `assertStringStartsWith`, `assertStringEndsWith`, `assertMatchesRegularExpression`, `assertJson` |
| Comparison | `assertGreaterThan`, `assertLessThan`, `assertLessThanOrEqual` |
| Filesystem | `assertFileExists`, `assertDirectoryExists` |
| Escape hatch | `fail($message)` |
#### Cloude-specific helpers
| Helper | Purpose |
|--------------------------------------------|------------------------------------------------------------------------------------------|
| `useArrayModel(class, rows = [])` | Configure a `Model` subclass with `ArrayStorage` + seed rows for the duration of the test |
| `useSqliteModel(class, createSql)` | Same, but with an in-memory SQLite + `PdoStorage`. Returns the PDO for extra setup |
| `useMockModel(class, rows = [])` | Like `useArrayModel()` but the storage records every call. Returns a `MockStorage` for assertions |
| `captureHttp($handler)` | Run a route handler; return `['status' => …, 'body' => …]` |
| `assertJsonResponse($expected, $handler)` | Capture + decode + structural compare. Optional `status:` named arg |
| `assertHttpException($status, $handler)` | Catch a `Cloude\Http\HttpException`; check status; return the exception for chaining |
| `freezeTime($when)` / `unfreezeTime()` | Pin `DateTime::now()` to a fixed instant for deterministic time-aware tests |
| `assertModelReceived($store, $method, times: ?int)` | Assert that a `MockStorage` got `$method` (optionally exactly `$times` times) |
| `assertModelDidNotReceive($store, $method)` | Inverse — assert `$method` was never called |
| `assertModelHas($model, $attributes)` | Assert each attribute key in `$attributes` matches on the model |
**Picking a model helper:**
| Need | Helper |
|---------------------------------------------------------------|-------------------|
| State-based test ("after doing X, the row has these fields") | `useArrayModel` |
| Behaviour test ("X causes a delete on PK 42") | `useMockModel` |
| Tests that go through `Model::query()` (joins, raw SQL builder) | `useSqliteModel` |
`useMockModel` is the right choice when you want to assert _how_ the
code used the storage. Don't use it for code that calls
`Model::query()` — faking the SQL builder leads to tests that pass
even when the underlying SQL is wrong. SQLite in-memory (the
`useSqliteModel` path) is fast enough that real SQL is the better
default whenever the query shape matters.
State that bleeds across tests is automatically cleared in
`setUp()` / `tearDown()`:
- `Cloude\Config::reset()` — forgets all loaded config files
- `Cloude\DateTime::clearTestNow()` — releases any frozen `now()`
## Philosophy
- **No magic**: the code you read is the code that runs. No generators, annotations or proxies.
- **Small classes**: each class fits in a file you can read in one sitting — and so can [Claude Code](https://claude.com/claude-code).
- **No required dependencies**: the core pulls nothing in. `ext-intl` is recommended for slug transliteration but not required.
- **No global state**: no container, no singletons. Static classes are just namespaces for functions.
- **AI-readable by design**: explicit APIs, no DSL, no inheritance webs. The framework is small on purpose so an agent can edit any piece without missing context.
## License
MIT - see [LICENSE](LICENSE).