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

I.7

Store responses in a relational database

You need this: When you report on answers with SQL or join them with your own tables.

Always keep the original response JSON and the version of the definition it was filled against. With both, you can re-map old responses whenever your tables change. Then copy the answers you query into columns. valueName makes this safe: it sets the key a question writes into the response independently of the question's name, so a form author can rename or restructure questions in Creator without breaking your columns. For more mapping metadata, such as a column type or a target table, add a custom property to questions; it then appears in Creator's property grid too.

This step on the Server Integration page Source on GitHub

Try this

  1. Fill in the v1 form and press Complete. The stored panel shows the responses row with definition_version: "v1", and the claims row with customer_email and amount filled.
  2. Switch to v2: the questions are renamed (contact_email, claimed_total) and moved into panels, but keep valueName. Submit again: the same columns fill.
  3. The custom dbColumn property is registered here and on the III.1 Creator page, where it shows in the property grid's Data category.

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/relational-storage.js · sjs:I.7.client View on GitHub
import { Model, Serializer } from "survey-core";

// Optional: a custom property for richer mapping, editable in Creator's property grid
Serializer.addProperty("question", { name: "dbColumn", category: "data" });

// valueName fixes the key your columns read → response: { "customer_email": "...", "amount": 1840.5 }
const survey = new Model(definitions[definitionVersion]);

// Post the original response with the version of the definition it was filled against
survey.onComplete.add(async (sender) => {
  await fetch("/api/claims", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ data: sender.data, definitionVersion })
  });
});
Server · routes/examples/relational-storage.php · sjs:I.7.server View on GitHub
// POST /api/claims — original JSON first, then the columns you query
Route::post('/api/claims', function (Request $request) {
    // The raw body, not $request->input(): PHP arrays would turn {} into [] in the stored JSON
    $body = json_decode($request->getContent(), flags: JSON_THROW_ON_ERROR);
    abort_unless(isset($body->data, $body->definitionVersion), 400, 'Expected { data, definitionVersion }');

    $id = DB::transaction(function () use ($body) {
        $responseId = DB::table('responses')->insertGetId([
            'form_id' => 'claim',
            'definition_version' => $body->definitionVersion,                    // keep the original and its version
            'data' => json_encode($body->data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION | JSON_THROW_ON_ERROR),
            'created_at' => now('UTC')->toIso8601ZuluString(),
        ]);
        DB::table('claims')->insert([                                            // keys come from valueName
            'response_id' => $responseId,
            'customer_email' => $body->data->customer_email ?? null,
            'amount' => $body->data->amount ?? null,
        ]);

        return $responseId;
    });

    return response()->json(['id' => $id], 201);
});

Definition: shared/definitions/relational-storage.v1.json, shared/definitions/relational-storage.v2.json