Repository navigation
File Uploads
A capability declares a file input with the rule file, an optional allow-list of extensions and an optional maximum
size. Each channel then brings the file its own way, and handle() always gets a string: the file's public URL (web
pages, REST) or a URL or path (AI agents, the command line). Since 2.19.0; required with a file on every channel since
2.19.1.
public $rules = array('cover' => 'required|file|extension,png;jpg;webp|max_size,2mb');/caligrafy-quill:new-capability set_cover books:write: sets the cover image of a book (PNG, JPG or WebP, 2 MB at most), from a form on the book's page
Check before you say OK:
- the rule has
file, anextensionlist and amax_size; - the form has
enctype='multipart/form-data'; - a GIF, a 3 MB file and no file each come back with a message under the field.
-
A column for the file. The table holds data: add it with a migration:
docker compose exec dev-box php caligrafer.php migrate make add_books_cover, then in the file it names:-- add_books_cover ALTER TABLE books ADD COLUMN cover VARCHAR(255) NULL AFTER summary;
Run
docker compose exec dev-box php caligrafer.php migrate. -
The capability.
docker compose exec dev-box php caligrafer.php service Bookshelf/SetCover --scope books:write, then makeapplication/services/Bookshelf/SetCover.phpread:<?php use Caligrafy\Capability; use Caligrafy\Context; use Caligrafy\Database; use Caligrafy\Service; #[Capability( name: 'set_cover', description: 'Sets the cover image of a book, by its id. Returns the id and the URL of the cover.', scope: 'books:write' )] class SetCover extends Service { public $rules = array( 'id' => 'required|integer|exists,books.id', 'cover' => 'required|file|extension,png;jpg;webp|max_size,2mb', ); public function handle(array $input, Context $context) { Database::table('books')->where('id', $input['id'])->update(array('cover' => $input['cover'])); return array('id' => (int) $input['id'], 'cover' => $input['cover']); } }
-
The route and the form. In
web.php:Route::form('/books/{id}/cover', 'set_cover', ['success' => '/books/{id}', 'flash' => 'Cover saved']);
On the book's page:
form(method='POST', action=$home . 'books/' . $book['id'] . '/cover', enctype='multipart/form-data') =csrf() .cq-field label(for='cover') Cover (PNG, JPG or WebP, 2 MB at most) input#cover(type='file', name='cover', accept='.png,.jpg,.webp') if errors('cover') p.cq-field-error= errors('cover') button.btn.btn-secondary(type='submit') Save the cover
-
Try it. Choose a PNG and click Save the cover: "Cover saved", and the
covercolumn holdshttp://localhost:8080/public/uploads/<24 random characters>.png. A GIF answers "This type of file is not accepted: png, jpg, webp"; a 3 MB PNG "The file is too large (at most 2 MB)"; no file "The Cover field needs a file".
| Rule | What it does |
|---|---|
file |
the input is a file |
extension,png;jpg |
the types allowed; without it, the default list of uploadFile() applies (images, documents, audio, video, zip, json). Scripts and markup (php, phtml, phar, html, js, svg...) are always refused |
max_size,2mb |
a number of bytes, or kb, mb, gb (500kb, 1.5mb). PHP's own limits (upload_max_filesize, post_max_size) apply first |
required with file, or required_file
|
the file is required on every channel: no file, URL or path answers "The <Field> field needs a file", and the input schema of MCP and GET /capabilities lists the field in required (2.19.1) |
The file rules are never given to the validation library: Service::validate() takes them out and
Caligrafy\Uploads applies them.
| Channel | What handle() gets |
|---|---|
Web pages (Route::form, Route::action) and REST (Route::capability, Route::capabilities) |
the file is taken from the request once the caller is allowed to call the capability, checked (one file, the allow-list, the size), saved in public/uploads/ under a new random name (the name the browser sent is never used), and handle() gets its public URL. A text sent in that field is dropped. When the capability fails, the saved file is deleted |
| AI agents (MCP) and the command line | they cannot send a file: they send a URL or a path, whose extension is checked against the allow-list. Never open it without checking where it points |
Caligrafy\Uploads- The helpers
uploadFile(),isAllowedUpload()anddeleteFile()
-
A file form without
enctype='multipart/form-data': no file arrives, and the form comes back with "The Cover field needs a file". -
required_filealone on 2.19.0 was ignored; since 2.19.1 it makes the file required on every channel. - Opening the path an agent sent without checking where it points: the command line and MCP send a string, not an uploaded file.
Caligrafy: Describe it. Approve it. Ship it. Your idea turned into a complete app: every feature built once, for web pages, Vue apps, a REST API and AI agents, and online.
Why Caligrafy: the benefits, the measurements, and what it is not yet
-
Quickstart with Claude: describe it, approve it, ship it
or the Quickstart: the same three steps by hand, in 5 minutes
- How Caligrafy works: the ideas in 5 minutes
- Your first capability: build a feature, page included
-
Capabilities in depth
-
#[Capability]: every option - Scheduled jobs: capabilities on a timer
- Notifications: web push and email
- Background jobs: capabilities run later
- Migrations: database changes, in order
- The cache
-
Testing your app
- Dependency injection
- Validation rules
-
Web pages
- Views and page components
- Design styles: another look, one setting
- Declarative routes: every request, in one line
-
Route::page·Route::form·Route::auth·Route::webhook·Route::download·Route::redirect·Route::action·Route::snap· capability routes - How to export a list as CSV
- Controllers: moving one to a declared route · how to, step by step
- Forms and helpers
-
Vue apps
-
Mobile and tablet
-
REST API
-
AI agents
-
Snap Cards and the assistant
- Payments: Stripe and crypto
- Face detection and face login
- OpenAI: chat and images · Assistants
- Open models: Llama, Mistral...
- Chatbots with Watson
- Machine learning in the browser
- Metadata and social cards · search and analytics
-
Putting it online: the production image, the database at every start
-
API reference: every class and method
-
Routing ·
Route·AuthRoutes· pages ·Controller· capabilities -
Data ·
Model·Query· sign-in · jobs ·Notify· payments · AI -
Interfaces · Snap ·
Snap\Cards·Snap\Conversation· testing · tooling · helper functions
-
Routing ·
-
Caligrafer: every command
- Project · build · data · agents · jobs and tests · maintenance · deploy
- Installation, configuration and Caligrafer
- Routing · Request · Controllers · Views
- Vue: flow of information · routes · validations
- Security and upgrading
- Release notes: 2.26 · 2.25 · 2.24 · 2.23 · 2.22 · 2.21 · 2.20 · 2.19 · 2.18 · 2.17 · 2.16 · 2.15 · 2.14 · 2.13 · 2.12 · 2.11 · 2.10 · 2.9 · 2.8 · 2.7 · 2.6 · 2.5 · 2.4 · 2.3 · 2.1 · October 2026 · changelog
- Testing Caligrafy (contributors)