tree: 26560a574e01b782895afc555496f2aea59b26df
  1. .cargo/
  2. scripts/
  3. src/
  4. tests/
  5. .gitattributes
  6. .gitignore
  7. Cargo.toml
  8. composer.json
  9. docker-compose.test.yml
  10. Dockerfile.test
  11. iggy-php.stubs.php
  12. LICENSE
  13. NOTICE
  14. phpunit.xml.dist
  15. README.md
foreign/php/README.md

iggy-php

PHP extension bindings for Apache Iggy, built in Rust with ext-php-rs.

This repository is experimental. The Rust Iggy SDK is async and Tokio-based, but this extension exposes Iggy\Client as a blocking synchronous PHP API. Each call drives the lazy global Tokio runtime and blocks the calling PHP thread until the future resolves; it does not provide fiber-aware or non-blocking I/O.

Requirements

  • Rust and Cargo
  • PHP 8.3 or newer with php-config
  • cargo-php
  • Composer, for installing PHPUnit
  • Docker, for running the integration test server

On macOS with Homebrew PHP:

export PATH="/opt/homebrew/opt/php/bin:$PATH"
export PHP=/opt/homebrew/opt/php/bin/php
export PHP_CONFIG=/opt/homebrew/opt/php/bin/php-config

Build

cargo build --release

Generate IDE stubs after changing the exported PHP API:

cargo php stubs --manifest Cargo.toml -o iggy-php.stubs.php

The CI lint job regenerates this file and fails if the checked-in stubs drift from the Rust signatures.

Install

cargo php install --release --yes

If the extension is already enabled, reinstall it with:

cargo php remove --yes
cargo php install --release --yes

Verify PHP can load it:

php -r 'var_dump(extension_loaded("iggy-php"));'

Run Iggy

docker run --rm --name iggy-php-test \
  -p 8090:8090 \
  -p 3000:3000 \
  apache/iggy:latest

You can also run a local server from the repository root:

cargo run --bin iggy-server --fresh --with-default-root-credentials

The tests assume:

  • host: 127.0.0.1
  • port: 8090
  • username: iggy
  • password: iggy

Override them with IGGY_HOST, IGGY_PORT, IGGY_USERNAME, and IGGY_PASSWORD.

Usage

<?php

$client = new \Iggy\Client('127.0.0.1:8090');
$client->connect();
$client->loginUser('iggy', 'iggy');

$stream = 'php-stream';
$topic = 'php-topic';
$partitionId = 0;

$client->createStream($stream);
$client->createTopic($stream, $topic, 1, null, null, null, null);

$client->sendMessages($stream, $topic, $partitionId, [
    new \Iggy\SendMessage('hello from PHP'),
]);

$messages = $client->pollMessages(
    $stream,
    $topic,
    $partitionId,
    \Iggy\PollingStrategy::first(),
    10,
    true,
);

foreach ($messages as $message) {
    echo $message->payload(), PHP_EOL;
}

Consumer group callbacks require a finite message limit:

<?php

$consumer = $client->consumerGroup(
    'php-consumer',
    $stream,
    $topic,
    $partitionId,
    \Iggy\PollingStrategy::next(),
    10,
    \Iggy\AutoCommit::disabled(),
    true,
    true,
    1_000_000,
    null,
    null,
    null,
    false,
);

$consumer->consumeMessages(
    function (\Iggy\ReceiveMessage $message) use ($consumer): void {
        process($message->payload());
        $consumer->storeOffset($message->offset(), $message->partitionId());
    },
    100,
);

foreach ($consumer->iterMessages() as $message) {
    process($message->payload());
    $consumer->storeOffset($message->offset(), $message->partitionId());

    if (shouldStop()) {
        break;
    }
}

Tests

Run the Dockerized integration suite:

docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from php-tests

Run the PHP test suite:

composer install
composer test

Run Rust verification:

cargo test

TLS tests are opt-in because they require a TLS-enabled Iggy server and certificate setup. Set IGGY_TLS_CONNECTION_STRING to enable TLS connection tests. Set IGGY_TLS_PLAINTEXT_ADDRESS to run the negative plaintext-to-TLS test.

TLS connection strings use the Rust SDK connection-string format, for example:

iggy+tcp://iggy:iggy@127.0.0.1:8090?tls=true&tls_domain=localhost&tls_ca_file=/path/to/ca.pem

API Notes

  • Methods are exposed to PHP as camelCase, for example createStream() and pollMessages().
  • Classes live in the Iggy namespace, for example Iggy\Client and Iggy\SendMessage.
  • Partition IDs use the Iggy partition index. For a topic with one partition, use 0.
  • Passing null as the partition to storeOffset() or deleteOffset() uses the current consumer partition, and is rejected until at least one message has been polled. Pass an explicit partition id before the first poll.
  • consumeMessages() requires an explicit finite limit. It does not run forever by default.
  • iterMessages() returns a PHP Iterator and can be used with foreach. Break out of the loop when the caller's processing limit or shutdown signal is reached.
  • AutoCommit::when() may queue an offset commit before the PHP callback runs. If callback success must control commits, use AutoCommit::disabled() and call storeOffset() after the callback work succeeds.
  • Iggy\PollingStrategy::timestamp() and Iggy\PollingStrategy::timestampMicros() expect microseconds since the Unix epoch. Use Iggy\PollingStrategy::timestampSeconds() for PHP time() values.
  • PHP strings are passed as named identifiers, including strings that contain only digits. PHP integers are passed as numeric identifiers.
  • Iggy\SendMessage::payload and Iggy\ReceiveMessage::payload() copy the payload bytes into a PHP string on each read. Cache large payloads in PHP if they will be read repeatedly.
  • Large unsigned values that can overflow PHP integers, such as message checksums, are returned as decimal strings.
  • Iggy\Client is synchronous and blocks the current PHP thread.
  • The extension owns a lazy global Tokio runtime. Do not call pcntl_fork() after the first Iggy SDK call; the child process inherits file descriptors but not Tokio worker threads. Runtime initialization failure is unrecoverable and aborts extension use.