# PHP

> Write custom APIs, hooks, schedulers and event listeners of API Maker in PHP 8.3 or later. function main(AmG $g), $g->req, $g->res, the typed calls of $g->sys->db with named arguments, errors, Composer packages from Packagist, with many examples.

Source: https://docs.apimaker.dev/v4/docs/languages/php.html

Code in PHP defines `function main($g)`. It runs on the official image of PHP (`php:8.4-cli` unless the [settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) 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**

```php
<?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 `AmObject`s : `$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**

```php
<?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 `AmObject`s 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**

```php
<?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**

```php
<?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**

```php
<?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**

```php
<?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**

```php
<?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**

```php
<?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
<?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**

```php
<?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**

```php
<?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**

```php
<?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
<?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](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html) 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
<?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
<?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](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#other-languages) 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
<?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](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#packages), then :

**A package of Packagist**

```php
<?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](https://docs.apimaker.dev/v4/docs/languages/index.html#editor).

**Where the editor helps**

```php
<?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](https://docs.apimaker.dev/v4/docs/features/debugging.html).

## Related

- [Languages and runtimes](https://docs.apimaker.dev/v4/docs/languages/index.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [The global object g](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html)
