Download the PHP package lemmon/garner without Composer

On this page you can find all versions of the php package lemmon/garner. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package garner

Garner

An agent-first, flat-file PHP CMS. Content lives as plain files on disk, the directory tree defines the routes, and pages render through Twig. There is no proprietary content format to learn — humans and AI agents edit the same files.

Requirements

Quick start

Add a page by creating a directory with a +page.json entry:

That answers /hello. main.md is exposed to the template as content.main.

Project layout

Using Garner as a package

A site can require Garner as a Composer dependency and keep only its own content and configuration (routes/, app/, config/, public/). The web entry point is a two-liner — public/index.php:

GARNER_PROJECT_ROOT declares where the site lives — the directory holding routes/, app/, and config/. The boot cannot reliably infer it on its own: it runs before the autoloader exists, server variables vary across SAPIs, and Garner's own file location is misleading once it is a vendor package — under a symlinked Composer path repository, PHP resolves __DIR__ to the real checkout. The constant also tells the boot which vendor/autoload.php to load: the project's, never Garner's own development install.

For local development, serve through the bundled router script — it sets GARNER_PROJECT_ROOT from the document root, serves published media and other static files directly, and hands everything else to Garner:

How a page works

A page is a directory under routes/. Its route is its path: routes/+page.json/, routes/blog/post/+page.json/blog/post. A directory without an entry file is a non-routable container; its children still route.

Route paths are canonical without a trailing slash (the root / being the only exception). A request that differs from a routable path only by slashes (/about/, /about////) gets a permanent redirect (308, query string preserved) to the canonical form, so the same content is never served at more than one URL. Paths whose canonical form doesn't route just 404.

A directory that has a +controller.php but no entry file is a route endpoint: it routes and dispatches its controller (the usual (page, site, app) contract, returning a RenderedResponse), but carries no metadata and is excluded from the page tree — it never appears in site.index, children, or findById. Use it for sitemap.txt, feeds, and JSON APIs that should not be treated as content pages.

The +page.json contract

+page.json is the only file Garner constrains, and it has no required fields — a directory with a +page.json (even {}) is a page. Any keys are kept as freeform metadata.

Field Default Notes
id the directory name Any unique string; explicit value wins.
template default Twig template / controller name.
draft false Hides the page and its route descendants.
draft_preview none Unlisted preview link secret; see below.
sort 0 Integer; lower comes first in listings.
created none Non-empty string (timestamp) when present.

YAML is accepted as an alternative entry file (+page.yaml / +page.yml).

Content files

Any recognized file beside the entry becomes a named value on content, keyed by its basename:

So main.mdcontent.main, data.jsoncontent.data. Files beginning with + or . are reserved and never loaded as content.

Files and media

Any other file beside a page (an image, PDF, video, download) is a file asset owned by that page. Reach them from the page:

file.url() publishes the file into the gitignored public/media/<hash>/ directory (a content hash, so the URL is immutable and cache-busts on edit) and the web server serves it directly. Publishing makes a file publicly downloadable — keep private files out of url() and stream them through a controller instead.

Metadata is optional and lives in a sibling sidecar, never created automatically:

The sidecar attaches to the file (page.file('team.jpg').meta) and is not loaded as a content value. See docs/media-handling.md for the full design and open questions.

Co-located template, controller, and action

Three optional + files let a page override its view and behavior:

A RenderedResponse is immutable; withHeader() and withCookie() return modified copies for extra response headers and cookies:

Drafts and visibility

Garner has exactly one publication flag in core: draft. A draft and everything beneath it in the route tree 404 publicly and are excluded from listings.

Effective state Resolves at URL In listings
published yes yes
draft or beneath a draft no (404) no

page.isDraft reports the page's own flag; page.isHidden reports the effective state after inheriting visibility from its ancestors.

Toggle the flag from the shell rather than hand-editing +page.json:

Both are no-ops (not errors) when the page is already in the target state, and only edit a +page.json entry — a +page.yaml/+page.yml page fails naming the command instead.

Draft preview links

Set draft_preview on a draft page to hand it to a client for review before it publishes:

The page then answers at /path?preview=letmein — still a 404 without the matching value, still absent from listings, and served with X-Robots-Tag: noindex. It's a soft, unlisted-link gate for showing someone an unpublished page, not a mechanism for protecting sensitive data: the value is a plain string, reuse across pages is fine, and each page's own value only unlocks that page.

Manage it with the CLI rather than hand-editing when convenient:

For a quick local look rather than a link handed to someone else, --open issues a one-time token that never touches +page.json at all — it lives only in the disposable application cache, so it can't be redeemed anywhere but the machine that generated it, and it stops working the moment it's used:

--base-url defaults to app.url/APP_URL when set. Unused, the token is discarded after app.preview.open_ttl seconds (default 1800, i.e. 30 minutes) — pure housekeeping, not a security boundary, since it's already single-use and redeemable only on the machine that generated it.

Known limitation: the token must be on the request's own query string — it does not survive a redirect that doesn't explicitly carry it forward. A form submitted on a previewed draft page (+action.php's Post/Redirect/Get success redirect) or a controller-returned redirect lands on a follow-up request with no ?preview=, so it 404s even though the submission itself succeeded. Preview links are meant for reviewing content, not exercising interactive flows on an unpublished page; re-append ?preview= by hand (or mint a fresh --open link, since one-time tokens are spent after the first view regardless) if you need to keep going.

Finer visibility — "in the footer but not the header", "featured", "archived" — is a listing decision, not a global page property, so it lives in your own freeform fields and is filtered with the collection API (see Traversal):

Ordering

Listings order by sort, then route path. sort defaults to 0, so negatives pin to the top, positives sink below the defaults, and unset pages sort by path.

Traversal

Available in templates and via the Garner\Content\Pages repository:

Listings exclude hidden pages and are ordered by sort then path. Each returns a Garner\Content\PageCollection (a Laravel collection of Page), so the full query API is available — filter, reject, where, sortBy, first, take, plus published() and drafts():

To include explicit drafts and descendants hidden by them in a listing, pass drafts: true: page.children(drafts=true). parent()/ancestors() are not visibility-filtered: grouping directories are skipped, but real ancestor pages are returned even when they are drafts because the relationship is structural.

The same tree is inspectable from the shell without SQL or template code:

page:show accepts either a route path or a stable id; a route lookup includes drafts, but the id-lookup fallback only resolves visible pages (findById()'s existing contract), so a hidden page must be addressed by route. If the argument matches both a route and a different page's id — a nested page's id happening to equal another page's route text — it fails rather than guessing; pass --route or --id to say which one you mean.

References

Reference another page by its stable id and resolve it at render time, so moving a page never breaks the link:

How you store a reference (a +page.json field, a value in a content file) is up to you — Garner only resolves the id to its current page.

Routing index

Routes resolve through a derived SQLite index at runtime/index.sqlite. The files are canonical; the index is a rebuildable cache. Its freshness mirrors Twig:

This follows app.debug by default; override with app.index.mode (scan / locked). Rebuild manually:

Compiled Twig templates are cached the same way (runtime/cache/twig, never recompiled in production), so a deploy must refresh both derived caches or keep serving stale pages:

For the freshness model in depth — the two kinds of staleness, per-environment guidance, and the schema-version auto-heal that recovers from engine upgrades — see docs/index-freshness.md.

Rendering

Twig templates live in app/templates/, resolved by the page's template field (falling back to default). Markdown is rendered through league/commonmark, exposed as a markdown Twig filter:

Controllers

Data for a template comes from up to two controllers, both with the same (page, site, app) contract:

Form actions

A co-located +action.php handles the page's POST — the write side, kept separate from the controller's read side. It returns a callable with the controller contract plus the request prepended:

For htmx forms, wrap the form in a named block and point the failure at it — the fragment lives inside the page template it belongs to, no separate partial file:

The fragment block renders alone: {% set %} statements elsewhere in the template do not run, so keep the block self-contained — derived values belong in the controller (its data is part of the fragment context) or inside the block itself. Under strict_variables an outside dependency throws; under Twig's lax default it silently renders empty. A template that cannot meet this should skip fragment: and let the form pluck its piece from the full re-render with hx-select.

One htmx default to know about: htmx does not swap 4xx responses out of the box, so a 422 failure would be silently ignored. Opt the site in with htmx's own configuration mechanism, e.g.:

Page dispatch is method-aware: HEAD routes like GET, POST goes to the action, and a verb the page cannot answer returns 405 Method Not Allowed with an Allow header. A page controller may still answer any verb with a RenderedResponse (method branching predating actions keeps working), and route endpoints keep full method freedom. Cross-site form POSTs are already rejected by the origin check before an action runs.

Twig extensions

app/twig.php extends the Twig environment: it returns a callable (Environment $twig, Application $app): void that registers functions, filters, or globals. Use it for render-time computation that belongs in templates — e.g. values derived from a title that child templates override via blocks, which no controller can know ahead of rendering:

Sessions

$app->session() gives a controller or action per-visitor key/value state — get(), set(), has(), remove(), plus flash()/consumeFlash() (and hasFlash() to peek without consuming) for a value that survives exactly one redirect (the Post/Redirect/Get flash-message case). One key is reserved: _flash carries flash metadata internally, and set('_flash', ...) throws rather than letting the value be silently lost:

Activation is lazy: for a visitor with no session, reading or never touching the session costs nothing and sends no cookie, so a plain content page stays exactly as stateless and cache-friendly as it would be without this feature at all. A cookie is only set once a request calls set(), flash(), or destroy(). A flashed value survives exactly one request whether or not it is consumed — the load that makes it readable also expires it. An incoming session cookie is only trusted when it names a session Garner itself issued — an unrecognized or tampered value is never adopted, so a client can't plant a session id (session fixation). Call regenerate() the moment a session's privilege changes (e.g. right after a login feature authenticates someone). Session ids always come from a dedicated cryptographically random generator, independent of ids.generator — that setting shapes scaffolded content ids and may be made predictable, but a session id is a bearer token and must stay unguessable.

Data persists through a pluggable SessionStoreFileSessionStore by default, one file per session under storage/sessions, no extra dependency required. The directory and its files are owner-only (0700/0600 — session files hold per-visitor state and their names are the session ids), and each write lands atomically via a temp-file rename, so concurrent requests never see a half-written session. Session files use PHP's serialize(), not JSON: nobody hand-edits them, so Garner's file-legibility bias (which is about content meant for human editing) doesn't apply, and serialize() avoids known JSON round-trip hazards for values stored blindly (whole-number float precision, objects silently decoding as plain arrays). Sweep expired sessions with php bin/garner session:gc (wire it into a deploy hook or cron, the same way reindex is). This is a generic primitive, not a "logged-in user" concept — a future auth feature would store a user id in it rather than inventing its own storage.

Application cache

$app->cache() stores disposable computed values — remote API responses, expensive transformations, or other data that can be recomputed when absent. It is site-wide like store(), but its contract is the opposite: cache data lives under runtime/, may expire, is cleared during deployment, and must never be treated as canonical:

The surface is intentionally small: get(key, default), set(key, value, ttl = null), has(key), remove(key), remember(key, callback, ttl = null), and clear(). A null TTL keeps the value until explicit clearing; a TTL at or below zero removes it. remember() may run the callback in two concurrent requests that miss at the same time — v1 provides safe complete writes, not a single-flight lock.

Values use PHP serialization rather than JSON. The cache is opaque, process-managed runtime state, so round-trip fidelity wins over inspectability: empty arrays and empty objects stay distinct, floats keep their type, and serializable application objects retain their class. Classes are allowed when decoding because the cache file is trusted Garner-written state, kept owner-only and never populated from an encoded request or content payload. Closures, resources visible in arrays or ordinary object properties, recursive arrays, values too deep or complex to inspect safely (nested more than 64 levels deep, or spanning more than 100,000 arrays and objects — generous enough for a multi-megabyte decoded API response), and serialization failures are rejected. Objects with opaque internal or custom serialization state—including internal containers such as ArrayObject—are responsible for producing resource-free state themselves: Garner does not invoke custom serialization once for validation and again for the actual write. Corrupt or class-incompatible entries behave as misses and are removed when get() or remember() reads them; has() checks a row's presence and expiry without decoding the payload.

The SQLite backing file is runtime/cache/data.sqlite by default (cache.path config) and is created lazily on the first set(). Clear it together with compiled Twig templates using php bin/garner cache:clear.

Key-value store

$app->store() is durable site-wide storage — string keys, JSON values — for the things an action needs to keep: form submissions, counters, site state. Where sessions are per-visitor and expiring, the store remembers indefinitely, keyed by what the data is rather than who sent it:

add() is the uniqueness primitive: an atomic insert-if-absent that returns false when the key already exists — no check-then-insert race between concurrent POSTs, because the key is the primary key. set() is the upsert for genuinely mutable keys, and get(key, default) / has() / remove() mirror the Session surface exactly. Multi-item data follows a one-key-per-item convention (email:<hash>, not one growing array under a single key — a read-modify-write on a shared array would race and lose updates); key construction stays in userland — Garner does not hash, normalize, or namespace on the site's behalf. items(prefix) lists a namespace as an Illuminate Collection keyed by full key (plain string prefix matching — : is a convention, not an API concept; note it loads the whole namespace, fine at the hundreds-of-items scale this targets), and count(prefix) answers "how many" without loading anything.

Values are JSON: scalars, lists, and maps round-trip; the contract is "JSON-encodable in, decoded value out," so objects come back as arrays by contract, and a non-encodable value throws rather than storing garbage. One caveat inherited from PHP's JSON functions: whole-number floats are preserved on the write Garner controls (2.0 stays a float), but a site needing exact float typing through arbitrary tooling should store the value as a string. This is deliberately a key-value store, not a database layer — no queries into values, no TTL, no multiple stores. A site that outgrows it brings its own PDO connection (SQLite is already a hard dependency) and its own file under storage/.

Data lives in a single SQLite file, storage/store.sqlite by default (store.path config), created lazily on first write — never touching the store never creates the file. Unlike runtime/index.sqlite (a disposable, rebuildable cache), the store is canonical: there is nothing to rebuild it from, so back up storage/ and ignore runtime/. The file is kept owner-only (0600, re-asserted when a process first writes; a created storage directory is 0700 — store values are site data, possibly personal, the same stance as session files), and Garner refuses to open a store.sqlite that is a symlink — a link there is never Garner's own doing. The store is never a black box: values sit as JSON in a plain TEXT column, inspectable with the sqlite3 CLI or the console commands:

Configuration

See config/app.php. Notable keys: debug, url (site base URL — see below), ids.generator (cuid2 default, also ulid, uuid_v4, uuid_v7, or a custom generator), index.mode, rendering.default_template, twig.*, session.* (cookie, lifetime, store, path — see Sessions above), cache.path (where the disposable application cache keeps its SQLite file), store.path (where the key-value store keeps its SQLite file — see Key-value store above), and csrf.check_origin (on by default: cross-site form POSTs — mismatched Origin / Sec-Fetch-Site — answer 403; JSON APIs and header-less non-browser clients are unaffected).

php bin/garner id:generate [--count=N] [--json] prints one or more fresh ids from the project's configured ids.generator — the same primitive page:create calls internally, useful from the shell or a script that needs an id (e.g. seeding a store:set key) without hand-rolling one in a format the project isn't actually configured for. --count accepts 1–10000.

Environment variables (APP_URL, APP_DEBUG, APP_ENV) can come from the real environment or from a .env file in the project root, loaded via symfony/dotenv before config is read. The Symfony cascade applies — .env, .env.local, .env.{APP_ENV}, .env.{APP_ENV}.local — and real environment variables always win over file values. Variables are read from the process environment (getenv()) first, then $_ENV and $_SERVER (the stock php.ini leaves $_ENV empty), so a deployment can skip .env entirely and configure through the server. The file is optional; keep .env out of version control (it may hold secrets) and commit a .env.example documenting the keys instead.

site.url is the site's base URL (scheme://host, no trailing slash), available in templates and via Application::siteUrl(). It is inferred from each request by default; set app.url (or the APP_URL env) to pin a canonical origin — needed for CLI builds, sitemaps, and stable canonical URLs.

One rule across the API: url() means absolute URL, path() means route path. page.url is the page's full URL (site.url plus the route path, e.g. https://example.com/about) — ready for hrefs, sitemaps, og:url, and rel=canonical as-is. page.path is the bare route path (/about): the page's routing identity, independent of where the site is hosted.

Development

Built with

Twig, league/commonmark, lemmon/validator, illuminate/collections, and Symfony components (console, yaml, uid, error-handler, var-dumper).

License

MIT. See LICENSE.


All versions of garner with dependencies

PHP Build Version
Package Version
Requires php Version ^8.4
ext-pdo Version *
ext-pdo_sqlite Version *
illuminate/collections Version ^13
league/commonmark Version ^2
lemmon/validator Version ^0.17
symfony/console Version ^8
symfony/dotenv Version ^8
symfony/error-handler Version ^8
symfony/filesystem Version ^8
symfony/http-foundation Version ^8.1
symfony/uid Version ^8
symfony/var-dumper Version ^8
symfony/yaml Version ^8
twig/twig Version ^3
visus/cuid2 Version ^6
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package lemmon/garner contains the following files

Loading the files please wait ...