Skip to content
This page View Markdown Open in ChatGPT Open in Claude

PHP

Code in PHP defines function main($g). It runs on the official image of PHP (php:8.4-cli unless the settings name another version, 8.3 at least), with only the packages you add from Packagist.

In short

  • Write function main($g).
  • It runs on php:8.4-cli, unless the settings name another version.
  • Refuse a request with throw new AmErrors($message, 400).
hello.php
1
2
3
4
5
<?php
function main(AmG $g)
{
    return ['hello' => $g->req->body->name ?? 'world'];
}
Runtime PHP 8.3 or later : the released versions of php:<version>-cli (8.5, 8.4, 8.4.12), no release candidate
FFI extension the runner loads API Maker with it : the image of the sandboxes gets it when it is built, and is not built without it
main function main(AmG $g) (the type is optional : function main($g) runs as well), or code which returns a function
Answer what main returns; null lets the API go on
Calls of $g->sys typed : named arguments, each with its type (getAll('mysql_8', 'inventory', 'customers', limit: 10)), and what a call answers has its type (a list of AmObject, ?AmObject, int, UpdateManyResult)
Values objects of JSON are AmObjects : $x->name and $x['name'] are the same, a key which is not there is null
Packages Packagist through Composer : guzzlehttp/guzzle ^7.9
Printing echo, print_r, var_dump go to the console of the account

The request and the answer

Read the request, shape the answer
1
2
3
4
5
6
7
8
9
<?php
function main(AmG $g)
{
    $status = $g->req->query->status ?? 'open';
    $signed = isset($g->req->headers['x-am-authorization']);
    $g->res->statusCode = 201;
    $g->res->headers['X-Request-Kind'] = 'orders';
    return ['customer' => $g->req->params->id, 'status' => $status, 'signed' => $signed];
}
  • $g->req (an AmRequest) and $g->res (an AmResponse) are AmObjects whose keys are documented properties, so an editor of PHP completes them : read and change them with -> or [], foreach over them, ->toArray() for plain arrays.
  • What you change of $g->req in a pre hook is what the API sees.

Bytes as the answer

A file, a picture, a PDF : return its bytes in an AmBytes, or put that in $g->res->output, and name their kind of content. A text of PHP holds bytes as well as letters : AmBytes says that these are bytes. The answer is those bytes as they are, whatever the kind of content, with the headers of the code.

A picture as the answer
1
2
3
4
5
6
7
8
<?php
function main(AmG $g)
{
    $picture = base64_decode('R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=');
    $g->res->contentType = 'image/gif';
    $g->res->headers['Content-Disposition'] = 'inline; filename="pixel.gif"';
    return new AmBytes($picture);
}

Calling API Maker : $g->sys

Each method of $g->sys->db, $g->sys->system and $g->sys->cache is a call of the server, with its types. It takes named arguments, each with its type : what a call is about comes first and needs no name (the instance, the database, the table), the rest is given by name. What it answers has its type : rows are a list of AmObject, a row an AmObject or null, a count an int, what an API describes a class whose keys are documented properties ($changed->updatedRowsCount). A name no call has is an error of PHP (Unknown named parameter) : a key is never sent under a wrong name.

Database calls
1
2
3
4
5
6
7
8
<?php
function main(AmG $g)
{
    $rows = $g->sys->db->getAll('mysql_8', 'inventory', 'customers', limit: 10, sort: '-customer_id');
    $two = $g->sys->db->getById('mysql_8', 'inventory', 'customers', 2);
    $count = $g->sys->db->count('mysql_8', 'inventory', 'customers', find: ['country' => 'IN']);
    return ['first' => $rows[0] ?? null, 'two' => $two, 'inIndia' => $count];
}
Save, update, remove
1
2
3
4
5
6
7
8
9
<?php
function main(AmG $g)
{
    $table = ['mysql_8', 'inventory', 'customers'];
    $saved = $g->sys->db->saveSingleOrMultiple(...$table, saveData: ['first_name' => 'Ada', 'last_name' => 'Lovelace']);
    $g->sys->db->updateById('mysql_8', 'inventory', 'customers', 2, updateData: ['first_name' => 'MALLORY']);
    $g->sys->db->removeById('mysql_8', 'inventory', 'customers', 101);
    return $saved->row();    // what a save answers : $saved->row(), $saved->rows(), $saved->isList()
}

Every request runs in a fiber : while one waits for the server, the other requests of the runner go on.

Rows as they arrive

Sum a large table
1
2
3
4
5
6
7
8
9
<?php
function main(AmG $g)
{
    $total = 0;
    $g->sys->db->getAllByStream(new GetAll('mysql_8', 'inventory', 'orders'), function (AmObject $row) use (&$total) {
        $total += $row->amount ?? 0;
    });
    return ['amount' => $total];
}

System APIs and cache

Encrypt, hash, events and cache keys
1
2
3
4
5
6
7
8
9
<?php
function main(AmG $g)
{
    $sealed = $g->sys->system->encrypt(['card' => '4111']);
    $digest = $g->sys->system->hash('my text');
    $g->sys->system->emitEvent('order_created', ['id' => 7], ['notify_shop']);
    $g->sys->cache->setKey('last_order', '7', ttlSeconds: 600);
    return ['sealed' => $sealed, 'digest' => $digest, 'last' => $g->sys->cache->getKey('last_order')];
}

Errors

Refuse a request
1
2
3
4
5
6
7
8
<?php
function main(AmG $g)
{
    if (empty($g->req->body->quantity)) {
        throw new AmErrors("'quantity' is missing.", 400);
    }
    return ['ok' => true];
}
Catch the errors of a call
<?php
function main(AmG $g)
{
    try {
        $g->sys->db->removeById('mysql_8', 'inventory', 'customers', 404);
    } catch (AmErrors $refused) {
        $g->logger->warn('not removed :', $refused->getMessage());
        return ['removed' => false, 'code' => $refused->getCode()];    // $refused->errors[0]->field…
    }
    return ['removed' => true];
}
  • Any other exception answers 500 with its class, message and the lines of your code (InvalidArgumentException: x is 1, then at orders.php(7)).
  • A warning is an error of the code : Warning: Undefined array key "qty". Notices and deprecations are not.

Logs, printing and shared values

Logs and shared values
1
2
3
4
5
6
7
8
9
<?php
function main(AmG $g)
{
    $g->logger->log('order', 7, 'paid');          // "order 7 paid" in the logs of the answer
    $g->logger->error(['step' => 'payment']);      // arrays and objects as indented JSON
    echo "kept in the console of the account\n";
    $g->shared->calls = ($g->shared->calls ?? 0) + 1;  // lives as long as the runner
    return $g->shared->calls;
}

Hooks

Pre hook : only my rows
1
2
3
4
5
6
7
8
<?php
function main(AmG $g)
{
    $person = $g->req->auth->authAMDB ?? null;
    $find = $g->req->query->find ?? new AmObject();
    $find->owner_id = $person->id ?? null;
    $g->req->query->find = $find;
}
Post hook : hide a column
1
2
3
4
5
6
7
8
9
<?php
function main(AmG $g)
{
    $rows = $g->res->output ?? [];
    foreach ($rows as $row) {
        unset($row->password);
    }
    $g->res->output = $rows;
}

Your own functions and classes

Each piece of code is compiled in a namespace of its own : functions and classes of the same name in two custom APIs (or in two versions of one) never meet. Classes of PHP and of your packages are found as usual, catch (Exception $e) included.

Helpers and a class
<?php
declare(strict_types=1);

final class Price
{
    public function __construct(public readonly float $amount, public readonly string $currency) {}

    public function withTax(float $rate): Price
    {
        return new Price(round($this->amount * (1 + $rate), 2), $this->currency);
    }
}

function main($g)
{
    $price = new Price(100.0, 'EUR');
    return ['gross' => $price->withTax(0.2)->amount, 'at' => new DateTimeImmutable('2026-01-01T00:00:00Z')];
}

Utility classes

What a utility class written in PHP declares (classes, functions, constants) is there for the other PHP code of the account, by its name.

The utility class Tax
<?php
const TAX_RATE = 0.2;

class Tax
{
    public static function add($amount)
    {
        return cents($amount * (1 + TAX_RATE));   // cents() : a function of another utility class
    }
}
A custom API which uses it
<?php
function main(AmG $g)
{
    return ['total' => Tax::add($g->req->body->amount), 'rate' => TAX_RATE];
}
  • A utility class has no namespace of its own : API Maker loads each class in a namespace it chooses and imports what the code names (Tax, cents, TAX_RATE), so a class which was saved again is loaded next to the one it was. Name things without a leading \.
  • The file of a class is run once per sandbox.
  • In the Git repository the class is src/utility_classes/<folder>---<name>/<name>.php, and composer.json loads those files (autoload.files).

Test cases

A test case written in PHP has a function per test : every function whose name starts with test_. What it throws is its failure.

A test case
<?php
function test_the_cost_of_a_parcel(AmG $g)
{
    $g->sys->test->mock('db.getAll', when: ['collection' => 'rates'], returns: [['per_kg' => 2.5]]);
    $out = $g->sys->test->runCustomApi('shippingCost', body: ['weight' => 4]);
    if ($out->cost != 10) {
        throw new Exception("the cost is {$out->cost}");
    }
}
  • $g->sys->test : runCustomApi($name, version: …, body: …, query: …, params: …, headers: …), runCustomApiFull(…) for the whole answer (data, statusCode, headers, logs), mock($method, when: …, returns: …), mocks([new Mock(…), …]), clearMocks($method = null), calls($method = null).
  • A test case has no namespace of its own. The custom API under test can be written in any language.

Packages

Add guzzlehttp/guzzle in Sandbox Settings, then :

A package of Packagist
1
2
3
4
5
6
7
8
<?php
use GuzzleHttp\Client;

function main($g)
{
    $answer = (new Client(['timeout' => 5]))->get('https://api.example.com/rates');
    return json_decode((string) $answer->getBody(), true);
}
  • Composer and what it unpacks with come into the image only when you add a package.
  • Versions are constraints of Composer : 7.9.2, ^7.9, ~2.0 || ^3.0.

In the code editor

The editor completes $g as PHP writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in the code editor in every language.

Where the editor helps
<?php
function main(AmG $g)
{
    // $g->  →  req, res, sys, logger, shared      $g->sys->db->  →  getAll, getById, query, gen, full…
    $g->logger->log($g->req->body->name);  // $g->req->  →  body, query, headers, auth…      $g->res->  →  statusCode, headers…
    $rows = $g->sys->db->getAll('mysql_8', 'inventory', 'customers', limit: 10);
    //      getAll('  →  your instances, then its databases, then their tables      , l  →  limit: , find: , sort: …
    foreach ($rows as $row) {
        $g->logger->log($row->first_name, $row['last_name']);  // $row->  and  $row['  →  the columns of customers
    }
}
  • Inside the brackets of a call the editor offers its named arguments, each with its type and what it is for (limit:, find:, select:…) ; inside find: [' it offers the columns of the table, inside select: ' and sort: ' too. The same for the classes of what a call takes : new GetAll(.
  • What a call answers has its type : the columns are offered after $rows[0]->, $row->, $row[' and $row->get(', in a foreach and in the function given to getAllByStream ; $changed-> (an updateMany) offers updatedRowsCount, $saved-> offers row, rows and isList, the whole answer of $g->sys->db->full offers success, data, errors…
  • The table is the one the call names : in the call, in a variable ($table + [...]), in named arguments (getById(instance: 'mysql_8', ...)) or in a function which returns the query.
  • $g->req-> and $g->res-> offer the keys of the request and of the answer, $g->req->auth-> the opened tokens.

Reference

$g-> PHP
req, res, shared AmRequest, AmResponse, AmObject : each an AmObject (->, [], foreach, ->get($key, $default), ->toArray())
sys->db getAll, getById, query, saveSingleOrMultiple, masterSave, arrayOperations, updateById, updateMany, replaceById, removeById, removeByQuery, aggregate, count, distinct, distinctQuery, getAllByStream, queryByStream; sys->db->gen->getAllGen…
sys->db->full, sys->db->gen->full, sys->system->full, sys->cache->full the same calls, each with its whole answer : ApiResponse (success, statusCode, data, totalCount, errors, warnings)
sys->system encrypt, encryptWith, decrypt, decryptWith, hash, getSecret, getSecrets, callExternalApi, callExternalApis, getToken, getTokens, executeQuery, getTableMeta, getTableMetas, createIndexes, dropIndexes, dropIndexesOfMany, getIndexes, getIndexesOfMany, emitEvent, emitEventWS, isValidDataForTable(s), isValidDataForCustomAPI(s), multiTenantInstance(s)Updated, isValidConnectionString(s), getLock, releaseLock
sys->cache getKey, getKeys, setKey, setKeys, removeKey, removeKeys, resetCacheDB, resetCacheDBs, resetCacheCustomApis, resetCacheCustomApisOfTenant, resetCacheSystemApis, resetCacheSystemApisOfTenant
sys->call('db.getAll', …) any API by its name in JavaScript, with its arguments as they are
what a call takes GetAll, GetById, Save, Query, DeepItem, GetToken, SetKey, Mock… : classes made with the same named arguments (new GetAll('mysql_8', 'inventory', 'customers', limit: 10)), given in place of them and to the calls of several
logger log, debug, info, warn, error
errors throw new AmErrors($message, $code) or new AmErrors([[...], ...]); ->getCode(), ->getMessage(), ->errors (the list, each a ResponseError)

Method names may be written with underscores too (get_by_id).

Debugging

  • The debug runner of PHP loads Xdebug, which connects to the server for each request : the Debug tab of the API testing page takes it while the page is open, else the code runs on.
  • Write variables as PHP does in a condition, a watch or a logpoint ($total > 100, {$cart->name}).
  • Learn more Debugging.