← Docs

PHP

One require, one connection. Microsecond reads from L1 cache.

Install

composer require goldlapel/goldlapel

# PHP's built-in PDO + pdo_pgsql extension handles the actual database connection.
# Enable pdo_pgsql in php.ini if it's not already active.

The wrapper uses PHP's built-in PDO with the pdo_pgsql extension — no third-party driver to install. Any higher-level layer (Laravel Eloquent, Doctrine, raw PDO) works against the proxy.

Quick Start

use GoldLapel\GoldLapel;

// Static factory — spawns the proxy and returns a GoldLapel instance
$gl = GoldLapel::start('postgresql://user:pass@db/mydb', [
    'proxy_port' => 7932,
    'log_level' => 'info',   // trace | debug | info | warn | error
]);

// $gl->url() is the driver-agnostic proxy URL.
// PDO can't take a postgresql:// URL directly, so use the pdoDsn() / pdoCredentials() helpers:
$pdo = new PDO($gl->pdoDsn(), ...$gl->pdoCredentials());

// Wrapper methods — no connection argument needed
$hits = $gl->search('articles', 'body', 'postgres tuning');
$gl->docInsert('events', ['type' => 'signup']);

// Clean up (also runs in __destruct())
$gl->stop();

Repeated reads serve in microseconds from the built-in L1 cache.

After startup, Gold Lapel prints a one-line summary to stderr and serves a dashboard:

$gl->dashboardUrl();
// => "http://127.0.0.1:7933" (or null if not running / dashboard disabled)

Banner goes to stderr so it never pollutes stdout-piped application output. Pass 'silent' => true in the options array to suppress it entirely — useful for daemons and structured-log pipelines that inspect both streams.

$gl->url() is the driver-agnostic proxy URL. For PDO specifically, use $gl->pdoDsn() with $gl->pdoCredentials() — PDO takes a pgsql:host=...;port=... DSN plus a separate username and password, not a postgresql:// URL.

Transactional coordination

When you want wrapper methods to run inside your own transaction, pass your connection via $gl->using($conn, $callback) (scoped) or the conn: named argument (per call):

// Scoped transactional coordination — closure receives $gl, all wrapper calls inside use $pdo.
// Backed by an instance property; unwinds in a finally block even on exception.
$pdo->beginTransaction();

$gl->using($pdo, function ($gl) {
    $gl->docInsert('events', ['type' => 'order.created']);
    $gl->incr('counters', 'orders');
});

$pdo->commit();

For a single call, use the named argument:

// For a single call, use the named conn: argument (PHP 8+)
$gl->docInsert('events', ['type' => 'x'], conn: $pdo);

Scope is stored on the GoldLapel instance and unwound in a finally block, so it survives exceptions. For async PHP, use the GoldLapel\Amp\ namespace (requires amphp/postgres). ReactPHP and Swoole are on the roadmap.

API

GoldLapel::start($upstream, array $options = [])

Static factory that spawns the proxy and returns a GoldLapel instance. Eagerly opens the wrapper's internal PDO connection so wrapper methods are fast from the first call.

  • $upstream — your Postgres connection string (e.g. postgresql://user:pass@localhost:5432/mydb)
  • $options — associative array:
    • 'proxy_port' — proxy port (default: 7932)
    • 'dashboard_port' — dashboard port (default: proxy_port + 1; set to 0 to disable)
    • 'invalidation_port' — cache-invalidation port (default: proxy_port + 2)
    • 'log_level'trace / debug / info / warn / error
    • 'mode' — operating mode (waiter, bellhop)
    • 'license' — path to the signed license file
    • 'client' — client identifier (defaults to "php")
    • 'config_file' — path to a TOML config file read by the Rust binary
    • 'config' — snake_case array of structured tuning knobs (see Configuration)
    • 'extra_args' — raw CLI flags passed to the binary
    • 'silent' — suppress the stderr startup banner (default: false)
    • 'mesh' — opt into the mesh at startup (HQ enforces the license; denial is non-fatal)
    • 'mesh_tag' — optional mesh tag; instances sharing a tag cluster together

$gl->url()

Driver-agnostic proxy URL string (postgresql://user:pass@127.0.0.1:7932/mydb).

$gl->pdoDsn() / $gl->pdoCredentials()

PDO-specific helpers — pdoDsn() returns a pgsql:host=...;port=...;dbname=... DSN, and pdoCredentials() returns [username, password] for new PDO($dsn, ...$creds). Required because PDO doesn't accept postgresql:// URLs directly.

$gl->dashboardUrl()

Dashboard URL (e.g. http://127.0.0.1:7933), or null if the dashboard is disabled or the proxy is not running.

$gl->using($conn, callable $callback)

Runs $callback($gl) with $conn as the implicit connection for any wrapper method invoked inside. Stored on the instance and unwound in finally, so exceptions are safe. Every wrapper method also accepts a trailing conn: named argument for one-off overrides.

$gl->stop()

Stops the proxy and closes the internal PDO connection. Idempotent. __destruct() calls this as a backup, so forgotten stops don't leak processes.

GoldLapel::startProxyOnly($url, array $options = [])

Low-level variant that starts the proxy and returns the instance without opening an internal PDO. Useful when the framework manages PDO construction (e.g. Laravel's service provider). Hold onto the returned instance and call $gl->stop() at worker shutdown (Octane, Swoole, RoadRunner) to release the subprocess deterministically.

// Low-level variant — starts the proxy but does NOT open an internal PDO.
// Useful when the framework manages PDO construction (e.g. Laravel's
// service provider). Fetch the proxy URL with $gl->url(). Hold onto the
// instance and call $gl->stop() at worker shutdown (Octane, Swoole,
// RoadRunner) to release the subprocess deterministically.
$gl = GoldLapel::startProxyOnly('postgresql://user:pass@db/mydb', ['proxy_port' => 7932]);
$proxyUrl = $gl->url();

$gl->script($luaCode, ...$args)

No conn: override on script(). The variadic ...$args would swallow any trailing \PDO passed as the per-call connection. To run script() on a specific connection, wrap it in using() or use the static helper:

// No conn: override on script() — the variadic ...$args would swallow
// a trailing \PDO. Wrap in using() instead:
$gl->using($myPdo, fn ($gl) => $gl->script($lua, 'arg1', 'arg2'));

// Or call the underlying static helper directly:
GoldLapel\Utils::script($myPdo, $lua, 'arg1', 'arg2');

GoldLapel::configKeys()

Returns the list of all valid config key names.

use GoldLapel\GoldLapel;
print_r(GoldLapel::configKeys());

Multiple instances

Each GoldLapel::start() call spawns its own proxy subprocess and returns a fresh instance. Use different ports to run several side by side.

// Each start() call spawns its own proxy subprocess and returns a fresh instance
$gl1 = GoldLapel::start('postgresql://user:pass@localhost:5432/app_db',       ['proxy_port' => 7932]);
$gl2 = GoldLapel::start('postgresql://user:pass@localhost:5432/analytics_db', ['proxy_port' => 7942]);

// Each instance manages its own proxy and cache
$gl1->stop();
$gl2->stop();

Configuration

Pass a config array under 'config'. Keys use snake_case and map directly to CLI flags (pool_size--pool-size). The 'log_level' option accepts string levels and translates to the binary's verbose flags internally.

$gl = GoldLapel::start('postgresql://user:pass@localhost/mydb', [
    'proxy_port' => 7932,
    'log_level' => 'info',    // top-level: trace | debug | info | warn | error
    'mode' => 'waiter',        // top-level: waiter | bellhop
    'mesh' => true,            // top-level: opt into the mesh at startup
    'mesh_tag' => 'prod-east', // top-level: optional mesh tag
    'config' => [              // structured tuning knobs only
        'pool_size' => 50,
        'disable_matviews' => true,
        'replica' => ['postgresql://user:pass@replica1/mydb'],
    ],
]);

Unknown keys throw InvalidArgumentException immediately. See the configuration reference for the complete list.

Raw CLI flags

You can also pass raw CLI flags via extra_args:

$gl = GoldLapel::start('postgresql://user:pass@localhost:5432/mydb', [
    'extra_args' => ['--threshold-duration-ms', '200', '--refresh-interval-secs', '30'],
]);

Environment variables

The binary also reads GOLDLAPEL_PROXY_PORT, GOLDLAPEL_UPSTREAM, and all other GOLDLAPEL_* env vars automatically. Set GOLDLAPEL_BINARY to override the binary location.

Framework integrations

Gold Lapel speaks plain PDO, so any framework that wraps PDO (Laravel Eloquent, Doctrine DBAL, Symfony's Doctrine bundle) works against $gl->pdoDsn().

  • Laravel — the Laravel integration ships inside the same goldlapel/goldlapel Composer package. Wire up the service provider and facade under the GoldLapel\Laravel\... namespace:
// config/database.php
'connections' => [
    'goldlapel' => [
        'driver' => 'pgsql',
        'host' => '127.0.0.1',
        'port' => 5432,                  // upstream Postgres port (standard Laravel pgsql setting)
        // ...username / password / database / etc.
        'goldlapel' => [
            'proxy_port' => 7932,       // Gold Lapel proxy port
            // Optional: 'mode' => 'waiter', 'log_level' => 'info', 'invalidation_port' => 7934
        ],
    ],
],

// Register the service provider / facade (auto-discovered in most setups)
use GoldLapel\Laravel\GoldLapelServiceProvider;
use GoldLapel\Laravel\Facades\GoldLapel;

// Then in any controller or job:
$hits = GoldLapel::search('articles', 'body', 'postgres tuning');

See the Laravel guide for the full setup.

Upgrading from v0.1

v0.2 is a breaking redesign. GoldLapel::start() used to return a PDO connection directly; now it returns a GoldLapel instance, and you bring your own PDO pointed at $gl->pdoDsn().

// v0.1.x (old) — static start() returned a PDO connection directly
$conn = GoldLapel::start('postgresql://...');
$conn->query('SELECT 1');
GoldLapel::stop();

// v0.2 (new) — static factory returns a GoldLapel instance, bring your own PDO
$gl = GoldLapel::start('postgresql://...', ['proxy_port' => 7932]);
$pdo = new PDO($gl->pdoDsn(), ...$gl->pdoCredentials());
$pdo->query('SELECT 1');
$gl->stop();
  • GoldLapel::start($url, $options) returns a GoldLapel instance (previously returned a PDO connection).
  • Options are an associative array — 'port', 'log_level', 'config', 'extra_args' — replacing positional named arguments.
  • New $gl->pdoDsn() / $gl->pdoCredentials() helpers replace any previous PDO-URL hacks.
  • All wrapper methods now accept an optional conn: named argument, and $gl->using($conn, $cb) provides scoped override.
  • Method names adopt camelCase (docInsert, createSearchConfig) to match the rest of the object API — snake_case static helpers are gone.
  • Multiple instances are first-class — each start() call spawns its own proxy.

Async — Amp

The package also ships a native async layer built on amphp/postgres. Existing sync code is unaffected — the async classes live under GoldLapel\Amp\ and are only loaded if you opt in. amphp/postgres is a suggest dependency, not a hard requirement, so the sync path stays lean for users who don't want it. PHP 8.1+ required (Amp 3.x uses native Fibers).

Install

composer require goldlapel/goldlapel amphp/postgres

Quick start

use GoldLapel\Amp\GoldLapel;

// Inside a Revolt / Amp event-loop context (any fiber):
$gl = GoldLapel::start('postgresql://user:pass@localhost/mydb', [
    'proxy_port' => 7932,
])->await();

// Every wrapper method returns Amp\Future<T>.
$hits = $gl->search('articles', 'body', 'postgres tuning')->await();
$gl->docInsert('events', ['type' => 'signup'])->await();

$gl->stop()->await();

The async API mirrors the sync API 1:1 — every sync method has an async sibling returning Amp\Future<T> from the same facade. Internally it uses amphp/postgres's native Postgres driver (no PDO), so nothing blocks the event loop.

Transactional scope with using()

$tx = $gl->connection()->beginTransaction();
$gl->using($tx, function ($gl) {
    $gl->docInsert('events', ['type' => 'order.created'])->await();
    $gl->incr('counters', 'orders')->await();
})->await();
$tx->commit();

Scope is fiber-local (backed by Revolt\EventLoop\FiberLocal) — sibling fibers running concurrently on the same GoldLapel instance each see their own scope (or the default connection, outside any using()), so no cross-fiber leaks. Direct wrapper calls inside the using() callback pick up the scope naturally.

Child fibers you spawn don't inherit the scope. If you call async(...) or Revolt\EventLoop::defer(...) inside a using() block, the new fiber starts with an empty slot and wrapper calls there see the default connection. Pass $conn explicitly to those calls (trailing positional argument), or open a nested using($conn, ...) inside the child fiber.

Streaming cursors

docFindCursor() returns an Amp\Pipeline\ConcurrentIterator<array> — iterate with foreach inside a fiber. Each batch is tagged with the goldlapel:skip annotation so the proxy's result cache doesn't replay a stale batch:

$iter = $gl->docFindCursor('events', null, ['created_at' => -1], null, null, 500);
foreach ($iter as $row) {
    process($row);
}

L1 cache

$gl->cached() returns a GoldLapel\Amp\CachedConnection wrapping the async connection with the same NativeCache instance used by the sync CachedPDO. Writes through either path cross-invalidate.

$cached = $gl->cached();
// Reads go through the cache; writes invalidate
foreach ($cached->query("SELECT * FROM users WHERE active = true") as $row) {
    // ...
}

Subscribing to changes

subscribe() and docWatch() require a PostgresConnection (LISTEN doesn't run on transactions). The sync variant polls; the async variant uses amphp's native $conn->listen($channel) iterator:

// Run the listener until it's unlistened or the connection closes
$gl->subscribe('my_channel', function (string $channel, string $payload) {
    echo "got: {$payload}\n";
})->await();

For long-running listeners you'll typically want a dedicated connection rather than the factory's primary connection — amphp/postgres's connect() function opens additional connections against the same proxy.

Amp only. Swoole and ReactPHP aren't supported today. The subprocess is still started via proc_open — fast enough inside a fiber. Async cleanup on stop() closes the amphp connection first, then terminates the Rust subprocess.

Redis Replacement

Gold Lapel includes a Redis Replacement API — common Redis patterns backed by PostgreSQL. Every method hangs directly off $gl and accepts an optional conn: named argument for transactional coordination.

Pub/Sub

// Pub/Sub
$gl->publish('orders', 'new order');
$gl->subscribe('orders', fn ($msg) => print_r($msg));

Queues

// Queues
$gl->enqueue('jobs', ['task' => 'send_email']);
$job = $gl->dequeue('jobs');

Counters

// Counters
$gl->incr('page_views', 'home');
$count = $gl->getCounter('page_views', 'home');

Sorted Sets

// Sorted Sets
$gl->zadd('leaderboard', 'player1', 100);
$top = $gl->zrange('leaderboard', 0, 9);

See the Redis Replacement docs for the complete API — including geospatial, rate limiting, sessions, and scripting.

Document Store

docFind, docInsert, docUpdate, docDelete and friends operate on JSONB-backed collections. Tables are auto-created on first use.

Filter operators

docFind supports the MongoDB filter operators you'd reach for — $elemMatch, $text, $gt, $in, and more.

// $elemMatch — scope multi-condition filters to a single array element
$orders = $gl->docFind('orders', [
    'items' => ['$elemMatch' => ['sku' => 'ABC-123', 'qty' => ['$gte' => 2]]],
]);
// $text — full-text search, document-wide or field-scoped
$hits = $gl->docFind('articles', [
    '$text' => ['$search' => 'postgres tuning'],
]);

See Appendix D: Filter Operator Reference for the full list, Postgres translations, and index notes.

Search

Full-text search utilities backed by PostgreSQL tsvector/tsquery. No extensions required.

Facets

// Facets — value counts, optionally filtered by a search query
$results  = $gl->facets('articles', 'category');
$filtered = $gl->facets('articles', 'category',
    query: 'machine learning', queryColumn: 'body');

Aggregations

// Aggregations — count, sum, avg, min, max with optional grouping
$byRegion = $gl->aggregate('orders', 'total', 'avg', groupBy: 'region');

Custom Search Config

// Custom search config
$gl->createSearchConfig('my_english', copyFrom: 'english');

Percolator

Store queries, then match documents against them. Like Elasticsearch percolate.

// Percolator — store queries, match documents against them
$gl->percolateAdd('alerts', 'breaking-news',
    'breaking news earthquake', metadata: ['notify' => 'slack']);

$matches = $gl->percolate('alerts',
    'A 6.2 magnitude earthquake struck the coast.');

$gl->percolateDelete('alerts', 'breaking-news');

Analyze

// Analyze — show tokenization pipeline
$tokens = $gl->analyze('The quick brown foxes');

Explain Score

// Explain score — score breakdown for a specific document
$result = $gl->explainScore('articles', 'body',
    'machine learning', 'id', 42);

See the API reference for full parameter details on all search methods.