Skip to content

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
<?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
<?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
<?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.',
            );
    }
}
sql
CREATE TABLE documents (id INTEGER PRIMARY KEY, filename VARCHAR(255) NOT NULL);
php
<?= $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.