File uploads
Interact transfers selected files to private temporary storage and makes accepted files available as PHP uploaded-file objects.
#[Upload]
Import Upload from Interact\Attribute and UploadedFileInterface from Psr\Http\Message:
#[Upload(maxFileSize: '1MB')]
public ?UploadedFileInterface $document = null;<label for="document">Document</label>
<input
id="document"
name="document"
type="file"
accept=".pdf,.txt"
x-interact:model="document"
/><?= $this->Form->control('document', [
'type' => 'file',
'label' => 'Document',
'accept' => '.pdf,.txt',
'x-interact:model' => 'document',
]) ?>Selection starts staging immediately. Use x-interact:model without timing modifiers. No upload action is required. An accepted transfer does not save a record or move the file to permanent storage.
PHP file objects
The island receives $this->document; its template receives $document. The object implements UploadedFileInterface, including getStream(), getSize(), getClientFilename(), and moveTo().
<?php if ($document !== null): ?>
<p><?= h($document->getClientFilename()) ?></p>
<?php endif; ?>Import Cake\Core\h in this template. Interact's TemporaryUpload also exposes an opaque readonly id for individual removal. Files and references are separate from writable JSON state; objects and contents are never deserialized from it.
Multiple files
/**
* Accepted attachments in selection order.
*
* @var list<UploadedFileInterface>
*/
#[Upload(multiple: true, maxFiles: 3)]
public array $attachments = [];<label for="attachments">Attachments</label>
<input
id="attachments"
name="attachments[]"
type="file"
multiple
x-interact:model="attachments"
/><?= $this->Form->control('attachments', [
'type' => 'file',
'multiple' => true,
'x-interact:model' => 'attachments',
]) ?>Further selections append. Single-file fields replace the previous file only after a replacement succeeds. PHP can clear a property to release its references.
Field limits
| Attribute option | Default | Description |
|---|---|---|
multiple | false | One nullable file, or an ordered array initialized to [] |
maxFileSize | Inherit | Positive integer bytes or CakePHP size string |
maxFiles | Inherit | Positive count for multiple-file fields |
Field limits may lower global ceilings, but cannot raise them. Single-file fields always allow one file and cannot declare maxFiles. Browser checks size and slots before sending; PHP independently checks actual bytes and capacity under the session lock. Excess items fail without replacing accepted files.
These are staging limits. Validate required files, MIME types, dimensions, and application rules in CakePHP. The native accept attribute is chooser feedback, not server validation.
Validation and saving
Pass the uploaded file as an entity field, validate through the Table, then save and move it in your application action. The complete single-document example includes the Table and entity context. Accepted files remain staged after validation fails, so another submission does not resend them. Clear the upload property after successful storage.
For image thumbnails, prepare a URL with an application image service in viewData(). Interact does not provide image processing or public preview routes. Authorize routes serving temporary files and choose private storage paths.
Progress and transfer controls
$server.uploads exposes per-field progress and cancel(), remove(), and retry(). Removal accepts browser item IDs or PHP TemporaryUpload::id. Progress at 100% does not itself prove server acceptance.
Saves wait for preceding transfers. Failed/cancelled prerequisites block the queued save; changing a selection also discards a waiting action. Submit again when the intended files are ready. Retry never saves automatically. Polling skips unfinished or failed selections.
Expected file rejections appear as field errors. Transport failures use Error handling. A 413 becomes upload_too_large, including when PHP discards an oversized multipart body before application validation. Multipart overhead also counts toward server body limits.
Private staging and cleanup
Ordinary installations need no storage registration. Configure optional limits in your application's configuration:
'Interact' => [
'uploads' => [
'directory' => TMP . 'interact/uploads',
'maxFileSize' => '10MB',
'maxFiles' => 10,
'maxIslandSize' => '50MB',
'maxSessionSize' => '100MB',
'maxSessionFiles' => 100,
'ttl' => 86_400,
],
],Size settings accept integer bytes or human-readable strings such as '10MB' and '1.5GB', parsed by CakePHP's Text::parseFileSize(). Counts remain positive integers and retention is in seconds. These are transport/storage ceilings; your application can impose smaller limits and its own allowed types. Abandoned transfers count toward capacity until cleanup. Zero-byte files count toward the file-count limit too.
Keep staging outside the webroot. Configure PHP's upload_max_filesize, post_max_size, and your web server's body limits to accommodate a file plus its multipart envelope. Those limits can reject a request before application file validation. The existing 1 MiB JSON-envelope limit remains separate from bytes.
Staging performs bounded expiry cleanup. Also schedule the CakePHP command so abandoned files are removed when the application is idle:
bin/cake interact.cleanup_uploadsThe default retention is 24 hours. Signed snapshots still have their own shorter lifetime; uploads do not restore a form after reload or extend page expiry. Missing or expired files block the requested action and ask for reselection, while preserving ordinary drafts and other available files. Draft export does not contain file contents or restore chooser selections.
For a different staging backend, replace UploadStorageInterface through the application container. Its contract covers owned staging, restoration, deletion, expiry, and atomic capacity checks. Permanent storage remains separate. Multiple application nodes using the default backend need access to the same private filesystem.