Skip to content

SDKs

PHP SDK

Convert spreadsheets, CSV and JSON into SQL from PHP with intosql/intosql - upload, wait, download, webhooks and errors.

View as Markdown

intosql/intosql wraps the IntoSQL API for PHP, one of the official SDKs. One call uploads a file, starts the conversion and waits for the result. The package depends only on Guzzle 7.

Pro: The API is part of IntoSQL Pro. Make an API key in Settings, API keys and keep it out of your source code.

Install

Terminal
composer require intosql/intosql

The package runs on PHP 8.2 or newer. Everything is in the IntoSQL namespace.

Create a client

PHP
use IntoSQL\IntoSQL;

$intosql = new IntoSQL(apiKey: getenv('INTOSQL_API_KEY'));

The constructor throws an InvalidArgumentException if apiKey is an empty string. Every option is a named argument.

Option Type Default What it does
apiKey string required An API key from Settings, API keys. Sent as Authorization: Bearer <key>.
baseUrl string https://intosql.com/api/v1 The API's base address. A trailing slash is removed.
maxRetries int 2 Attempts after the first one for rate limits, server errors and lost connections. 0 turns retries off.
timeout float 60.0 Seconds each request to the API may take. 0 waits forever. The file upload has no limit.
httpClient GuzzleHttp\ClientInterface new GuzzleHttp\Client The Guzzle client the SDK uses for every call, the upload included. Useful for proxies, middleware and tests.
sleep callable(float): void usleep Waits the given seconds between retries and between reads of a conversion. Replace it in tests so they don't wait.

The client never sends your key to another origin. If a pagination link points somewhere other than baseUrl's origin, the call throws instead. Every request carries User-Agent: intosql-php/<version>; the version is IntoSQL::VERSION.

Convert a file

convert($file, $spec) does the whole flow and returns the finished conversion.

PHP
use IntoSQL\IntoSQL;
use IntoSQL\Spec;

$intosql = new IntoSQL(apiKey: getenv('INTOSQL_API_KEY'));

$conversion = $intosql->convert('ventas.xlsx', Spec::make(dialect: 'postgresql', table: 'ventas'));

echo $conversion->report['rows']['converted'] ?? 0, ' ', $conversion->output?->url;

What happens, in order:

  1. Open. POST /conversions with name, the file's size in bytes and spec. The answer has an upload address.
  2. Upload. The file is sent with a PUT to that address, with the headers the API gave, as it is. Nothing is read into memory first.
  3. Start. POST /conversions/{id}/start.
  4. Wait. The conversion is read until its status is done or failed.

$file is a path, a stream resource from fopen, or a PSR-7 StreamInterface. For a path, name defaults to the file's name and the size is read from the disk. For a stream, pass name; the size comes from the stream, or from bytes when the stream doesn't know it. The size must be exact, since the conversion only starts once the whole file has arrived.

PHP
use GuzzleHttp\Client;

$response = (new Client)->get('https://example.com/exports/pedidos.json', ['stream' => true]);

$conversion = $intosql->convert(
    $response->getBody(),
    Spec::make(dialect: 'sqlite'),
    name: 'pedidos.json',
    bytes: (int) $response->getHeaderLine('Content-Length'),
);

Contents already in memory go through GuzzleHttp\Psr7\Utils::streamFor($csv), which knows its size.

name helps tell the file's format, and the downloaded result is named after it: ventas.xlsx downloads as ventas.sql. Set table in the spec to choose the table's name.

A MySQL or MariaDB dump (.sql, .sql.gz) converts to PostgreSQL like any file. $conversion->jobReport() reads the report into classes; for a dump, tables lists each table with its rows, columns, fixes and problems:

PHP
foreach ($conversion->jobReport()?->tables ?? [] as $table) {
    echo $table->name, ': ', $table->rows, " rows\n";
}

How it waits

convert, convertUrl and conversions->wait read the conversion again and again with growing pauses: half a second, then 1, 2 and 4 seconds, then every 5 seconds. They return when the status is done, throw ConversionFailedException when it is failed, and ConversionCanceledException when it was canceled.

Wait option Type Default What it does
timeout float 3600.0 (1 h) Gives up after this many seconds and throws a RuntimeException saying the conversion is still queued or running.
interval ?float null Seconds between reads. null uses the growing pauses above.

Giving up only stops the waiting. The conversion keeps running on IntoSQL's servers, and you can read it later by its id.

PHP
$conversion = $intosql->convert(
    'clientes_2024.xlsx',
    Spec::make(dialect: 'mysql'),
    timeout: 10 * 60,
);

Convert a file by URL

convertUrl($url, $spec) asks IntoSQL to download the file from a public HTTPS address, then waits the same way. There is no upload step.

PHP
$conversion = $intosql->convertUrl(
    'https://example.com/exports/ventas.csv',
    Spec::make(dialect: 'mysql'),
);

It also takes an optional name (otherwise the name comes from the server's Content-Disposition header or the address), and the wait options. The address must be HTTPS and reachable from the internet; anything else is refused with invalid_url. If the download fails, the conversion ends as failed.

Download the result

When status is ConversionStatus::Done, output->url downloads the result. The address works for ten minutes. After that, read the conversion again to get a fresh one.

PHP
use GuzzleHttp\Client;

$fresh = $intosql->conversions->retrieve($conversion->id);

(new Client)->get($fresh->output->url, ['sink' => 'ventas.sql']);

Send no Authorization header to output->url; the address carries its own signature. Files are deleted 24 hours after the conversion finishes, or right away with conversions->delete.

Build a spec

Spec::make() returns a full JobSpec array with IntoSQL's defaults. Only dialect is required.

PHP
use IntoSQL\Spec;

$spec = Spec::make(
    dialect: 'sql_server',
    table: 'clientes',
    schema: 'ventas',
    sheet: 'Clientes 2024',
    batchRows: 1000,
);
Option Type Default Spec field
dialect 'postgresql', 'mysql', 'mariadb', 'sqlite', 'sql_server' or 'oracle' required output.dialect
format 'sql', 'csv' or 'json' 'sql' output.format
table ?string chosen by the engine output.table
schema ?string none output.schema
sheet ?string first sheet input.sheet
createTable bool true output.create_table
batchRows int 500 output.batch_rows

The rest is fixed by the helper: format, encoding, delimiter and header row detected from the file; column names in snake_case; INSERT statements (style: 'inserts'); types inferred from the first 10,000 rows; date order detected; leading zeros kept; no per-column overrides. For anything else (bulk output, a date order, a locale, columns renamed, kept as text or left out), change the array it returns or write the array yourself. Every field is in Conversion spec.

PHP
$spec = Spec::make(dialect: 'postgresql', table: 'pedidos');

$spec['output']['style'] = 'bulk';
$spec['inference']['date_order'] = 'day_first';
$spec['columns'] = [['index' => 0, 'as_text' => true]];

Conversions

$intosql->conversions exposes each step on its own, for when you want to upload from somewhere else, start later, or not wait.

Method Request Returns
create($name, $bytes, $spec) POST /conversions Conversion with upload
upload($conversion, $file) PUT to $conversion->upload->url void
start($id) POST /conversions/{id}/start Conversion, queued
createFromUrl($url, $spec, name: null) POST /conversions with url Conversion, queued
retrieve($id) GET /conversions/{id} Conversion
wait($id, timeout: 3600.0, interval: null) GET /conversions/{id}, repeated Conversion, done
list(limit: null, status: null) GET /conversions, every page Generator<int, Conversion>
cancel($id) POST /conversions/{id}/cancel Conversion, canceled
delete($id) DELETE /conversions/{id} void
createCase($id, consent:, share:, email:, note:) POST /conversions/{id}/cases ConversionCase

Step by step

PHP
$opened = $intosql->conversions->create('ventas.xlsx', filesize('ventas.xlsx'), Spec::make(dialect: 'mariadb'));

$intosql->conversions->upload($opened, 'ventas.xlsx');
$intosql->conversions->start($opened->id);

// Later, or from a webhook handler:
$conversion = $intosql->conversions->retrieve($opened->id);

The upload address from create works for an hour. upload takes the same files as convert and sends Content-Length from the conversion's bytes. It throws a LogicException if the conversion has no upload address (one read with retrieve never does), and an IntoSQLException with code upload_failed if storage doesn't answer with a 2xx.

List

list() yields every conversion on the account, newest first. It fetches a page at a time as you iterate (50, or limit from 1 to 100) and follows links.next until there are no more pages. status keeps only conversions in that status; it takes a ConversionStatus or its string value.

PHP
use IntoSQL\ConversionStatus;

foreach ($intosql->conversions->list(status: ConversionStatus::Failed) as $conversion) {
    echo $conversion->id, ' ', $conversion->error?->code, ' ', $conversion->name, PHP_EOL;
}

Stop early with break; no further pages are fetched.

Cancel

cancel($id) stops a conversion that hasn't ended and deletes its files. It stays listed as canceled and doesn't count toward the month. Canceling it again answers the same; canceling one that already ended throws IntoSQLException with code already_finished.

PHP
$intosql->conversions->cancel($conversion->id);

Send us a conversion

When a file didn't convert well, or at all, createCase sends the conversion to IntoSQL so support for it gets added. Pass consent: true only when the person agreed to send it. share: 'file' sends the file as well as the report; a failed conversion keeps its upload until it expires, so the file is still there to send.

PHP
$intosql->conversions->createCase(
    $conversion->id,
    consent: true,
    share: 'file',
    note: 'Exported from our ERP; the dates come out wrong.',
);

Cases are private, kept 90 days, and then deleted. The privacy policy says how they are handled.

Delete

delete($id) removes the conversion's input and output files now instead of after 24 hours. Reading it afterwards answers not_found.

PHP
$intosql->conversions->delete($conversion->id);

Usage

usage() returns the bytes the plan includes this month, used and left.

PHP
$usage = $intosql->usage();

echo $usage->limit, ' ', $usage->used, ' ', $usage->left, ' ', $usage->periodStart;

Failed conversions don't count toward used.

Verify webhooks

With a webhook address saved in Settings, API keys, IntoSQL sends a signed POST when a conversion is done, failed or canceled. Webhook::parse checks the IntoSQL-Signature header against the raw body and returns the typed event; it throws an UnexpectedValueException when the signature doesn't match.

PHP
use IntoSQL\Conversion;
use IntoSQL\Webhook;

try {
    $event = Webhook::parse(
        file_get_contents('php://input'),
        $_SERVER['HTTP_INTOSQL_SIGNATURE'] ?? '',
        getenv('INTOSQL_WEBHOOK_SECRET'),
    );
} catch (UnexpectedValueException) {
    http_response_code(400);
    exit;
}

if ($event->type === 'conversion.done' && $event->data instanceof Conversion) {
    // $event->data is the conversion; $event->id is the same on every retry.
}

http_response_code(204);

In Laravel, pass $request->getContent() and $request->header('IntoSQL-Signature', ''), and exclude the route from CSRF checks.

Webhook::verify($body, $header, $secret) does the check alone and returns true or false. Both refuse a call when the header is malformed, the signature doesn't match, or the timestamp is more than 300 seconds from now. They take toleranceSeconds to change that window and now (Unix seconds) to fix the current time in tests. The signature is compared in constant time. Pass the body exactly as received, before any JSON parsing. The payload and retries are in Webhooks.

Errors

The SDK throws three exception classes, all in the IntoSQL namespace and all extending RuntimeException.

  • IntoSQLException: the API answered with an error. It has status (the HTTP status, also getCode()), errorCode (stable between versions), getMessage() (for people), fields (per field messages when the code is invalid_request, otherwise []) and requestId, to quote when you write to us. If the answer has no error body, errorCode is http_error.
  • ConversionFailedException: the conversion ended as failed. It has id, errorCode (stable: ours, such as timed_out, or the engine's, such as encrypted_file), problems (what the engine found) and the explanation as its message.
  • ConversionCanceledException: the conversion was canceled while it was awaited. It has id.

A connection that keeps failing after the retries throws Guzzle's own exception (a GuzzleHttp\Exception\ConnectException). Waiting past timeout throws a plain RuntimeException.

PHP
use IntoSQL\ConversionFailedException;
use IntoSQL\IntoSQLException;

try {
    $intosql->convert('ventas.xlsx', Spec::make(dialect: 'oracle'));
} catch (ConversionFailedException $error) {
    error_log("Conversion {$error->id} failed ({$error->errorCode}): {$error->getMessage()}");
} catch (IntoSQLException $error) {
    if ($error->errorCode === 'over_quota') {
        error_log("This month's conversions are used up.");
    } else {
        error_log("{$error->status} {$error->errorCode} {$error->getMessage()} ".json_encode($error->fields));
    }
}

Every code, its status and how to fix it is in Errors.

Retries and idempotency

The client retries a request when the connection fails, when the API answers rate_limited, too_many_active or request_in_progress, on any 5xx, and on 408 and 425 from the network in between, up to maxRetries times (2 by default). It waits for Retry-After when the answer has it, and otherwise half a second, then 1 second, doubling up to 8 seconds.

Every POST gets an Idempotency-Key header with a random UUID, and its retries reuse the same key. If a first attempt reached the API but its answer was lost, the retry gets that first answer back instead of opening a second conversion.

Not retried: the file upload to storage (a failed PUT throws upload_failed), already_started (the conversion is on its way; read it instead), and any other 4xx such as invalid_spec or over_quota.

Types

The package has these classes alongside the client. Their properties are public and read-only, in camelCase.

Class What it is
Conversion A conversion as the API answers it: id, status, name, bytes, spec, report, error, output, upload, createdAt, startedAt, finishedAt, expiresAt. Times are ISO 8601 strings.
ConversionStatus An enum: AwaitingUpload, Queued, Running, Done, Failed, Canceled, and Unknown for a status added after this version.
ConversionFailure A failed conversion's error: code, message, problems.
ConversionOutput A done conversion's output: bytes, url.
ConversionUpload A new conversion's upload: method, url, headers.
ConversionCase A conversion sent to IntoSQL: id, conversionId, share, status, createdAt, expiresAt.
WebhookEvent What Webhook::parse returns: id, type, createdAt, data (a Conversion for conversion.* events, an array such as ['sent_at' => ...] for ping).
Usage limit, used, left, periodStart.
Page One page of a list: data (a list of Conversion), next, perPage, nextCursor, prevCursor.
JobReport What $conversion->jobReport() returns: dialect, table, columns, fixes, problems, and for a database dump source, tables, notConverted (null and empty lists otherwise).
SourceReport Where a dump comes from: dialect, version, producer, charset, databases.
TableReport One table of a dump: sourceName, name, schema, rows, columns, fixes, problems.
ColumnReport One column: sourceName, name, sqlType, kind, precision, scale, maxChars, nullable, fixes, excluded, asText.
Fix A correction: number, code, column, row, args.
Problem Something to look at: code, severity, column, row, args.
NotConverted An object of a dump that was not carried: kind (view, trigger, procedure, function, event) and name.

spec and report are arrays exactly as the API sends them, with the fields of JobSpec and JobReport: report is described in Conversions, and fixes in Fixes. Codes, kinds and dialects in the report classes are strings, so a value added later reads as it is.