SurveyJS Server Integration · PHP / Laravel I.4 · Store files outside the response
You work in a private copy of the demo database, kept until it has been unused for 24 hours.

I.4

Store files outside the response

You need this: Any form with a file question.

By default a file question embeds the file in the response as base64, which bloats every response. Set storeDataAsText to false and upload files to your own endpoint; the response keeps a reference instead of the content. If files are private, which is usual for documents, IDs and scans, store an id and add a second endpoint that checks access and returns the content. The form requests it through onDownloadFile when it shows a preview.

This step on the Server Integration page Source on GitHub

Try this

  1. Add one or two images or PDFs to Receipts. Requests shows one POST /api/files; the line below compares the response size with base64 against the size with ids.
  2. What the server stored lists the files by id, name and type. Press Complete: the stored response holds ids, not contents.
  3. The preview of each file comes through GET /api/files/<id> in Requests. The route checks access first (Gate::define('read-file') in AppServiceProvider): a signed-out visitor gets 404, and real apps check who owns the file there.

Add a file to compare the response sizes.

Code that just ran

These regions are read from the files that served this page, the same lines the Server Integration page shows.

Client · shared/client/store-files.js · sjs:I.4.client View on GitHub
// Upload: your server stores the files and returns an id (or a URL) for each
survey.onUploadFiles.add(async (_, options) => {
  const body = new FormData();
  options.files.forEach(file => body.append("files[]", file));   // "files[]": PHP keeps only the last of several "files"
  const ids = await fetch("/api/files", { method: "POST", body }).then(r => r.json());
  // The response stores these ids, not the file contents
  options.callback(options.files.map((file, i) => ({ file, content: ids[i] })));
});

// Preview: only when the stored value is not a public URL the browser can open
survey.onDownloadFile.add(async (_, options) => {
  const res = await fetch(`/api/files/${encodeURIComponent(options.content)}`);
  if (!res.ok) return options.callback("error");
  const reader = new FileReader();
  reader.onload = () => options.callback("success", reader.result);   // base64 data the form shows
  reader.readAsDataURL(await res.blob());
});
Server · routes/examples/store-files.php · sjs:I.4.server View on GitHub
// POST /api/files — store uploads, return their ids in the same order
Route::post('/api/files', function (Request $request) {
    $ids = [];
    foreach (Arr::wrap($request->file('files')) as $file) {   // field "files[]": PHP keeps only the last of several "files"
        abort_unless($file->isValid(), 400, $file->getErrorMessage());
        $id = (string) Str::uuid();
        $file->storeAs('', $id, 'uploads');                    // the "uploads" disk: local, S3…; named by id, never by the client
        DB::table('files')->insert(['id' => $id, 'name' => $file->getClientOriginalName(), 'type' => $file->getMimeType()]);
        $ids[] = $id;
    }

    return response()->json($ids);
});

// GET /api/files/{id} — the content, for private files only; check access first
Route::get('/api/files/{id}', function (string $id) {
    $file = DB::table('files')->find($id);
    abort_if(! $file || Gate::denies('read-file', $file), 404);   // 404, not 403: don't confirm the file exists

    return Storage::disk('uploads')->response($id, $file->name, [   // nosniff and sandbox: an uploaded HTML or SVG never runs as this site
        'Content-Type' => $file->type, 'X-Content-Type-Options' => 'nosniff', 'Content-Security-Policy' => 'sandbox',
    ]);
});

Definition: shared/definitions/store-files.json