Single document upload
This form stages one PDF or text document, validates it, and saves a record and a private file.
Complete installation first. Copy the files below to the indicated application locations.
Create the documents table using the schema tab and a writable private TMP . 'example-documents/' directory (usually tmp/example-documents/). This sample output directory is separate from Interact's staging directory. Choose your application's persistent storage location for production.
document is a transient validation field, not a database column. The example uses a generic entity; an existing Document entity must allow the supplied fields.
Files
<?php
declare(strict_types=1);
namespace App\Island;
use Cake\Datasource\EntityInterface;
use Cake\ORM\Locator\LocatorAwareTrait;
use Interact\Attribute\Action;
use Interact\Attribute\Upload;
use Interact\Island;
use Psr\Http\Message\UploadedFileInterface;
/**
* Creates a document record from one staged file using normal Table validation.
*
* The entity stays on the server so FormHelper can render its validation errors.
*
* Save as src/Island/DocumentUploadIsland.php.
*/
class DocumentUploadIsland extends Island
{
use LocatorAwareTrait;
#[Upload(maxFileSize: '1MB')]
public ?UploadedFileInterface $document = null;
protected ?EntityInterface $record = null;
/**
* Validate and save the record, then move the file into private storage.
*/
#[Action]
public function save(): void
{
$documents = $this->fetchTable('Documents');
$this->record = $documents->newEntity([
'filename' => $this->document?->getClientFilename(),
'document' => $this->document,
]);
if (!$documents->save($this->record)) {
$this->setErrors($this->record->getErrors() ?: ['_form' => ['Unable to save the document.']]);
return;
}
$this->document?->moveTo(TMP . 'example-documents/' . (string)$this->record->get('id') . '.upload');
$this->document = null;
}
/**
* Give FormHelper the current entity, including validation or staging errors.
*
* @return array{record: EntityInterface} Request-local form context.
*/
public function viewData(): array
{
$record = $this->record ?? $this->fetchTable('Documents')->newEmptyEntity();
$record->setErrors($record->getErrors() + $this->errors());
return ['record' => $record];
}
}<?php
declare(strict_types=1);
use function Cake\Core\h;
/**
* templates/island/document_upload.php
*
* @var \Interact\View\TemplateView $this
* @var \Cake\Datasource\EntityInterface $record
* @var \Psr\Http\Message\UploadedFileInterface|null $document
* @var array<string, list<string>> $errors
*/
?>
<?= $this->Form->create($record, [
'url' => false,
'type' => 'file',
'x-interact:submit' => '$server.call("save")',
]) ?>
<?= $this->Form->control('document', [
'type' => 'file',
'label' => 'Document',
'accept' => '.pdf,.txt',
'x-interact:model' => 'document',
]) ?>
<?php if ($document !== null): ?>
<p>Selected: <?=(string)h($document->getClientFilename()) ?></p>
<?php endif; ?>
<?php foreach ($errors['_form'] ?? [] as $message): ?>
<p role="alert"><?=(string)h($message) ?></p>
<?php endforeach; ?>
<?= $this->Form->button('Save document') ?>
<?php if (!$record->isNew()): ?>
<p role="status">Document saved.</p>
<?php endif; ?>
<p role="alert" x-text="$server.error ? $server.error.message : ''"></p>
<?= $this->Form->end() ?><?php
declare(strict_types=1);
namespace App\Model\Table;
use Cake\ORM\Entity;
use Cake\ORM\Table;
use Cake\Validation\Validator;
/**
* Validates document records and their transient uploaded-file field before saving.
*
* Save as src/Model/Table/DocumentsTable.php.
*/
class DocumentsTable extends Table
{
/**
* Use a generic entity for the record and its transient file field.
*/
public function initialize(array $config): void
{
parent::initialize($config);
$this->setEntityClass(Entity::class);
}
/**
* Require a PDF or text document and keep its display name within the column size.
*/
public function validationDefault(Validator $validator): Validator
{
return $validator->maxLength('filename', 255)
->requirePresence('document', 'create')
->notEmptyFile('document', 'Choose a document.')
->uploadedFile(
'document',
['types' => ['application/pdf', 'text/plain'], 'maxSize' => 1024 * 1024],
'Choose a PDF or text file up to 1 MiB.',
);
}
}CREATE TABLE documents (id INTEGER PRIMARY KEY, filename VARCHAR(255) NOT NULL);<?= $this->Interact->render('DocumentUpload') ?>Behavior
Selection stages the file. save() validates through DocumentsTable, keeps the failed entity for FormHelper, and moves the accepted file only after a successful insert. Validation failures retain the staged file, so submitting again does not transfer it again. Clearing the property releases its staging reference.
The record is inserted before the move. A storage failure can therefore leave a row behind. Coordinate rollback and cleanup through your application's storage service or transaction boundary when required; the filesystem is not transactional. See File uploads for limits, multiple files, and cleanup.