blob: ff6e4c6e039048ce9737d656fd3cafb86c70bcfa [file] [view]
# iggy-php
PHP extension bindings for [Apache Iggy](https://iggy.apache.org/), built in Rust with
[`ext-php-rs`](https://github.com/davidcole1340/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:
```sh
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
```sh
cargo build --release
```
Generate IDE stubs after changing the exported PHP API:
```sh
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
```sh
cargo php install --release --yes
```
If the extension is already enabled, reinstall it with:
```sh
cargo php remove --yes
cargo php install --release --yes
```
Verify PHP can load it:
```sh
php -r 'var_dump(extension_loaded("iggy-php"));'
```
## Run Iggy
```sh
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:
```sh
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
<?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
<?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:
```sh
docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from php-tests
```
Run the PHP test suite:
```sh
composer install
composer test
```
Run Rust verification:
```sh
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:
```text
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.