Skip to content

feat: add PHP SDK#3235

Open
countradooku wants to merge 16 commits into
apache:masterfrom
countradooku:add-php-sdk
Open

feat: add PHP SDK#3235
countradooku wants to merge 16 commits into
apache:masterfrom
countradooku:add-php-sdk

Conversation

@countradooku
Copy link
Copy Markdown

@countradooku countradooku commented May 11, 2026

Summary

Adds an experimental PHP SDK under foreign/php, implemented as a Rust-backed PHP extension over the in-tree Iggy Rust SDK.

The PR includes:

  • synchronous IggyClient bindings
  • stream/topic/message/consumer-group coverage
  • PHP integration tests and TLS opt-in tests
  • Dockerized PHP SDK test image and compose workflow
  • CI component detection and pre-merge tasks for PHP SDK changes
  • root Cargo workspace exclusion for foreign/php, matching the existing foreign SDK pattern

Validation

Ran the following locally:

  • cargo fmt --manifest-path foreign/php/Cargo.toml --check
  • composer validate --working-dir foreign/php --strict
  • PHP syntax lint for all foreign/php/tests/*.php
  • cargo metadata --manifest-path foreign/php/Cargo.toml --no-deps --format-version 1
  • docker compose -f foreign/php/docker-compose.test.yml config
  • docker compose -f foreign/php/docker-compose.test.yml build php-tests
  • PHP extension smoke in Docker: extension_loaded("iggy-php") === true
  • PHP integration smoke against apache/iggy:latest: 23 passed, 3 skipped, 0 failed

Notes

The full compose test path is present, but I only completed the php-tests image build plus an integration smoke against an already-running healthy Iggy container. TLS tests are opt-in and skipped unless IGGY_TLS_CONNECTION_STRING / IGGY_TLS_PLAINTEXT_ADDRESS are set.

@countradooku countradooku marked this pull request as ready for review May 11, 2026 17:44
@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.79%. Comparing base (99c82b8) to head (2cc80e6).

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3235      +/-   ##
============================================
+ Coverage     73.78%   73.79%   +0.01%     
  Complexity      943      943              
============================================
  Files          1200     1200              
  Lines        109116   109116              
  Branches      86004    86025      +21     
============================================
+ Hits          80516    80527      +11     
+ Misses        25874    25842      -32     
- Partials       2726     2747      +21     
Components Coverage Δ
Rust Core 74.95% <ø> (+0.02%) ⬆️
Java SDK 58.44% <ø> (ø)
C# SDK 69.13% <ø> (-0.33%) ⬇️
Python SDK 81.43% <ø> (ø)
Node SDK 91.52% <ø> (+0.08%) ⬆️
Go SDK 39.91% <ø> (ø)
see 28 files with indirect coverage changes
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@countradooku countradooku changed the title Add PHP SDK feat: add PHP SDK May 11, 2026
The PHP extension client needs to build independently from the root Rust workspace while still testing against the in-tree Rust SDK. This adds the PHP binding package, Dockerized integration test path, and CI detection so PHP-only changes exercise the right checks.

Constraint: Foreign SDK crates are kept outside the root workspace like the existing Python and C++ SDKs
Constraint: PHP extension builds require php-config, cargo-php, and libclang in containerized CI
Rejected: Leave PHP tests manual only | PHP-only PRs would not run SDK-specific validation
Confidence: medium
Scope-risk: moderate
Directive: Keep foreign/php excluded from the root Cargo workspace unless the extension build is intentionally made workspace-compatible
Tested: cargo fmt --manifest-path foreign/php/Cargo.toml --check; composer validate --working-dir foreign/php --strict; PHP syntax lint for tests; cargo metadata --manifest-path foreign/php/Cargo.toml; docker compose config; docker compose build php-tests; PHP integration smoke against apache/iggy:latest with 23 passed, 3 skipped, 0 failed
Not-tested: Full docker compose test with source-built Iggy server after php-tests image build
The PHP and Python SDK integration suites depend on a server container that can be checked for readiness and reached from sibling containers. The previous healthcheck hit an authenticated HTTP endpoint with curl, while the runtime image did not install curl and the default server binds loopback-only addresses. The compose files now start the server with default root credentials, bind service ports to all interfaces, and use the bundled CLI ping for readiness.

Constraint: Runtime image should not need extra healthcheck packages just for foreign SDK tests
Constraint: SDK test containers connect through the Docker service name, not the server container loopback interface
Rejected: Install curl and keep /stats healthcheck | /stats requires authentication and does not prove TCP SDK readiness
Confidence: high
Scope-risk: narrow
Directive: Keep foreign SDK compose server bind addresses aligned with client container hostnames
Tested: docker compose config for PHP and Python test files; git diff --check
Not-tested: Full Docker build/test run locally because Docker daemon was unavailable
Comment thread .github/workflows/pre-merge.yml
@slbotbm
Copy link
Copy Markdown
Contributor

slbotbm commented May 12, 2026

I would recommend removing the cargo.lock file from version control. The python and cpp bindings also do not commit the lock files.

Maybe also consider splitting this into two PRs - one for the blocking client and one for the async client. That'll reduce the LoC and make it easier for people to review.

The PHP SDK addition was too large and carried an async bridge plus Docker-heavy CI path in the first review. This narrows the PR to the blocking client, removes async-only dependencies and lockfiles, and moves PHP checks into a language-specific pre-merge action that reports into the existing CI status gate.

Constraint: Reviewer requested the async client be split out to reduce LoC and review risk

Constraint: PHP CI must avoid the previous 22 minute Docker Compose path

Rejected: Keep async client in this PR | too much surface area and an extra php-tokio dependency for initial review

Rejected: Keep inline PHP workflow steps | inconsistent with other SDK CI actions and missed status aggregation

Confidence: high

Scope-risk: moderate

Directive: Reintroduce IggyAsyncClient in a separate PR after the blocking client and CI path settle

Tested: Ruby YAML parse for workflows/actions; PHP syntax checks; composer.json JSON parse; cargo fmt --manifest-path foreign/php/Cargo.toml -- --check; Docker PHP test image build

Not-tested: Full GitHub Actions runtime duration
The Python test compose adjustment was unrelated to the PHP SDK review and made the PR harder to reason about. Restore it to master so this branch only carries the PHP SDK and its CI wiring.

Constraint: Review feedback is focused on PHP SDK size and CI validity

Rejected: Keep Python compose reachability tweak | unrelated to this PR and should be handled separately if still needed

Confidence: high

Scope-risk: narrow

Directive: Do not mix foreign SDK compose changes into the PHP SDK PR unless they are required by PHP tests

Tested: git diff confirmed foreign/python/docker-compose.test.yml matches master

Not-tested: Python Docker Compose runtime
@countradooku
Copy link
Copy Markdown
Author

@hubcio @slbotbm I did therequired changes

@countradooku countradooku requested a review from hubcio May 12, 2026 12:46
The PHP test action wrote IGGY_PHP_EXTENSION through GITHUB_ENV before starting the Iggy server. Server config validation treats every IGGY_* variable as typed configuration and panicked on the unknown extension path variable, so the PHP test job never reached the test runner.

Constraint: Iggy server rejects unknown IGGY_* environment variables

Rejected: Add IGGY_PHP_EXTENSION to server ignored vars | the variable is test-runner specific and should not enter server config

Confidence: high

Scope-risk: narrow

Directive: Keep PHP test harness variables outside the IGGY_* namespace unless they are real server configuration

Tested: bash -n foreign/php/scripts/test.sh; Ruby YAML parse for .github/actions/php/pre-merge/action.yml

Not-tested: Full GitHub Actions rerun
@hubcio
Copy link
Copy Markdown
Contributor

hubcio commented May 14, 2026

/ready

@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label May 14, 2026
Comment thread foreign/php/src/receive_message.rs Outdated
Comment thread foreign/php/src/consumer.rs Outdated
Comment thread foreign/php/Dockerfile.test Outdated
Comment thread foreign/php/tests/IggySdkTest.php Outdated
Comment thread foreign/php/tests/IggySdkTest.php Outdated
Comment thread foreign/php/src/identifier.rs Outdated
Comment thread foreign/php/src/identifier.rs Outdated
Comment thread foreign/php/src/client.rs Outdated
Comment thread foreign/php/tests/run.php Outdated
Comment thread foreign/php/src/send_message.rs Outdated
Copy link
Copy Markdown
Contributor

@slbotbm slbotbm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/author

Comment thread foreign/php/src/iterator.rs Outdated
The PHP adapter review surfaced avoidable production panics, duplicated runtime setup, a test harness that bypassed PHPUnit, and an exported iterator wrapper that did not provide native PHP iterator semantics. This keeps the blocking PHP API smaller by using callback consumption, one shared Tokio runtime, fallible identifier conversion, and PHPUnit-backed integration tests.

Constraint: PHP adapter remains a synchronous blocking API over async Rust client calls
Constraint: composer.lock is intentionally ignored by foreign/php/.gitignore
Rejected: Keep ReceiveMessageIterator as a PHP class | it was not a real PHP Iterator and duplicated consumeMessages behavior
Rejected: Add new runtime instances per exported class | one shared runtime is simpler and matches the blocking extension boundary
Confidence: medium
Scope-risk: moderate
Directive: Do not reintroduce unwrap-based identifier conversion at PHP boundaries; return PHP exceptions instead
Tested: cargo fmt --manifest-path foreign/php/Cargo.toml -- --check
Tested: php -l tests/IggySdkTest.php && php -l tests/TlsTest.php && php -l tests/bootstrap.php
Tested: composer validate --no-check-publish
Tested: composer install --dry-run --no-interaction --prefer-dist
Not-tested: cargo check --manifest-path foreign/php/Cargo.toml fails in ext-php-rs 0.15.13 with local Homebrew PHP 8.5 headers before this crate compiles
Not-tested: Docker integration image and PHPUnit integration suite because Docker daemon is not running
Copy link
Copy Markdown
Contributor

@slbotbm slbotbm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides the comments, some more feedback:

  • Please generate stubs and also commit them
  • there is no test coverage for things like numeric PhpIdentifiers, pollMessages with pollingStrategy::next() across repeated calls, to name a few.

Comment thread foreign/php/README.md
Comment thread foreign/php/Cargo.toml Outdated
Comment thread foreign/php/src/identifier.rs Outdated
Comment thread foreign/php/src/consumer.rs Outdated
Comment thread foreign/php/src/send_message.rs Outdated
Comment thread foreign/php/tests/IggySdkTest.php
Comment thread foreign/php/tests/IggySdkTest.php
Comment thread foreign/php/tests/IggySdkTest.php
Comment thread foreign/php/tests/IggySdkTest.php Outdated
@slbotbm
Copy link
Copy Markdown
Contributor

slbotbm commented May 18, 2026

/author

@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels May 18, 2026
The PHP SDK review uncovered a stale local dependency version, missing generated stubs, weak polling coverage, and resource cleanup gaps. This keeps the binding surface aligned with the local SDK crate, centralizes PHP exception mapping, adds cleanup APIs, and strengthens the PHPUnit suite around identifiers, polling strategy behavior, and consumer group metadata.

Constraint: Review feedback requested committed PHP stubs and stronger Docker-backed test coverage

Rejected: Keep the iggy dependency version on the path dependency | local path dependencies do not need a crate version and prerelease workspace versions break resolution

Confidence: high

Scope-risk: narrow

Directive: Regenerate foreign/php/iggy-php.stubs.php whenever exported PHP classes or methods change

Tested: cargo fmt --manifest-path foreign/php/Cargo.toml -- --check

Tested: cargo build --manifest-path foreign/php/Cargo.toml

Tested: cargo clippy --manifest-path foreign/php/Cargo.toml -- -D warnings

Tested: cargo test --manifest-path foreign/php/Cargo.toml

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

Tested: php -l foreign/php/iggy-php.stubs.php; php -l foreign/php/tests/IggySdkTest.php; php -l foreign/php/tests/bootstrap.php; php -l foreign/php/tests/TlsTest.php

Tested: Docker PHP test image run against Docker Iggy server passed: 19 tests, 58 assertions, 3 skipped

Tested: docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from php-tests passed with host port publishing: 19 tests, 58 assertions, 3 skipped

Tested: git diff --check
Apache master changed the pre-merge finalization job while the PHP SDK branch added the PHP test matrix. The merge keeps the upstream finalize_pr behavior and preserves test-php in the dependency list so PHP failures still contribute to the PR gate.

Constraint: PR apache#3235 was marked DIRTY by GitHub against apache/iggy:master

Rejected: Drop test-php from finalize_pr needs | would make PHP SDK checks invisible to the final gate

Confidence: high

Scope-risk: narrow

Tested: ruby YAML parse for .github/workflows/pre-merge.yml

Tested: cargo metadata --manifest-path foreign/php/Cargo.toml --locked --format-version 1

Tested: git diff --cached --check
@countradooku
Copy link
Copy Markdown
Author

/ready

@github-actions github-actions Bot added S-waiting-on-review PR is waiting on a reviewer and removed S-waiting-on-author PR is waiting on author response labels May 20, 2026
The PHP pre-merge test job builds the extension and starts Iggy, then invokes foreign/php/scripts/test.sh. That script runs vendor/bin/phpunit, but the GitHub action never installed Composer dependencies, so CI failed after the build with 'Could not open input file: vendor/bin/phpunit'.

Constraint: PHP tests depend on composer.json require-dev packages and the SDK intentionally ignores composer.lock and vendor/

Rejected: Change scripts/test.sh to download PHPUnit ad hoc | duplicates Composer dependency management already used by the Docker test image

Confidence: high

Scope-risk: narrow

Tested: ruby YAML parse for .github/actions/php/pre-merge/action.yml

Tested: composer install --no-interaction --prefer-dist --no-progress

Tested: composer validate --strict

Tested: cargo metadata --manifest-path foreign/php/Cargo.toml --locked --format-version 1

Tested: git diff --check
@countradooku
Copy link
Copy Markdown
Author

/ready

Comment thread foreign/php/src/client.rs

match (init_retries, init_retry_interval_micros) {
(Some(retries), Some(micros)) => {
builder = builder.init_retries(retries, IggyDuration::from(micros));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the (Some, Some) branch passes init_retry_interval_micros straight through as IggyDuration::from(micros). if the caller passes 0, upstream core/sdk/src/clients/consumer.rs:313 calls time::interval(Duration::ZERO) which panics per tokio's contract. panic unwinds across extern "C" -> process abort, not a php Throwable. reachable even with init_retries=0 since the timer is built before the retry-count check.

reject Some(0) here. same shape applies to poll_interval_micros and polling_retry_interval_micros above.

///
/// The callback is called as callback(ReceiveMessage $message). If limit is null,
/// this method runs until the consumer stream ends or an error occurs.
pub fn consume_messages(&self, callback: ZendCallable, limit: Option<u32>) -> PhpResult<u32> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consume_messages(limit=None) resolves to max_messages = u32::MAX and drives next_message() via block_on until the stream ends. with an empty topic, upstream core/sdk/src/clients/consumer.rs:983 re-arms poll_future indefinitely. php's max_execution_time (SIGPROF) can't interrupt native frames inside the tokio runtime, so the php-fpm worker is wedged until killed externally.

require an explicit limit, or enforce a min default poll_interval, or add a wall-time select! deadline.


pub fn after(after: &AutoCommitAfter) -> Self {
Self {
inner: RustAutoCommit::After(after.inner),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plain AutoCommit::After(_) is a silent no-op upstream. core/sdk/src/clients/consumer.rs:181-207 only populates store_offset_after_* from When / IntervalOrWhen, and the init match at lines 375-380 only spawns the interval task for Interval / IntervalOrWhen / IntervalOrAfter. plain After falls into _ => {} and commits nothing.

same applies to AutoCommit::interval_or_after() at lines 180-187 - the interval half fires, but the "after" trigger half is silently dropped upstream.

for v0 either drop AutoCommit::after() + AutoCommitAfter from the php surface, or wire the After half upstream across both variants.

break;
};

callback
Copy link
Copy Markdown
Contributor

@hubcio hubcio May 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the php callback runs after RustIggyConsumer::next() has already mutated last_consumed_offsets and enqueued send_store_offset (core/sdk/src/clients/consumer.rs:929-944 + 1036-1055). under any AutoCommit::When(*) mode a callback throw can't revert the commit -> at-most-once delivery, not at-least-once.

minimum fix: document loudly + recommend AutoCommit::Disabled + explicit storeOffset() after callback success

Comment thread foreign/php/src/receive_message.rs
(inputs.task == 'build-aarch64-gnu' || inputs.task == 'build-aarch64-musl') && 'ubuntu-24.04-arm' ||
inputs.task == 'build-macos-aarch64' && 'macos-14' ||
inputs.task == 'build-windows-sdk' && 'windows-latest' ||
inputs.component == 'sdk-php' && 'ubuntu-24.04' ||
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sdk-php is pinned to ubuntu-24.04 while everything else uses ubuntu-latest. any reason for that?

shell: bash
working-directory: foreign/php
run: composer install --no-interaction --prefer-dist --no-progress

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this step doesn't export IGGY_TLS_CONNECTION_STRING / IGGY_TLS_PLAINTEXT_ADDRESS, so TlsTest.php is skipped in CI. no TLS cert is provisioned anywhere in the workflow. follow-up: provision a self-signed cert fixture and wire the env vars so the TLS path is actually exercised.

IMHO worth fixing in follow up, but please create an issue for that first. (like test php + tls)

@@ -0,0 +1,395 @@
<?php
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no regression test for "callback throws after next() already committed". that's the callback-after-commit critical's canary - add it and expect it to fail today; drop the xfail only after the fix lands.

Comment thread foreign/php/README.md
@@ -0,0 +1,146 @@
# iggy-php
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing: consumer-group example end-to-end, TLS connection-string format, and the micros-since-epoch note for PollingStrategy::timestamp(). the timestamp gap is the same foot-gun called out in receive_message.rs.

Comment thread foreign/php/Cargo.toml
Comment on lines +38 to +39
[profile.release]
strip = "debuginfo"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can add lto=thin

@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-author PR is waiting on author response

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants