feat: add PHP SDK#3235
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 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
🚀 New features to boost your workflow:
|
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
|
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
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
|
/ready |
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
slbotbm
left a comment
There was a problem hiding this comment.
Besides the comments, some more feedback:
- Please generate stubs and also commit them
- there is no test coverage for things like numeric
PhpIdentifiers,pollMessageswithpollingStrategy::next()across repeated calls, to name a few.
|
/author |
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
|
/ready |
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
|
/ready |
|
|
||
| match (init_retries, init_retry_interval_micros) { | ||
| (Some(retries), Some(micros)) => { | ||
| builder = builder.init_retries(retries, IggyDuration::from(micros)); |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
| (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' || |
There was a problem hiding this comment.
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 | ||
|
|
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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.
| @@ -0,0 +1,146 @@ | |||
| # iggy-php | |||
There was a problem hiding this comment.
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.
| [profile.release] | ||
| strip = "debuginfo" |
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:
IggyClientbindingsforeign/php, matching the existing foreign SDK patternValidation
Ran the following locally:
cargo fmt --manifest-path foreign/php/Cargo.toml --checkcomposer validate --working-dir foreign/php --strictforeign/php/tests/*.phpcargo metadata --manifest-path foreign/php/Cargo.toml --no-deps --format-version 1docker compose -f foreign/php/docker-compose.test.yml configdocker compose -f foreign/php/docker-compose.test.yml build php-testsextension_loaded("iggy-php") === trueapache/iggy:latest:23 passed, 3 skipped, 0 failedNotes
The full compose test path is present, but I only completed the
php-testsimage build plus an integration smoke against an already-running healthy Iggy container. TLS tests are opt-in and skipped unlessIGGY_TLS_CONNECTION_STRING/IGGY_TLS_PLAINTEXT_ADDRESSare set.