Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
43b21a8
feat(types): support primitive union type hints
ptondereau Apr 28, 2026
de0ecbe
feat(types): support nullable primitive unions
ptondereau Apr 28, 2026
6bb4751
feat(types): support union return types
ptondereau Apr 28, 2026
dea4d06
feat(describe)!: carry PHP union types through stub generation
ptondereau Apr 28, 2026
b19e42c
feat(describe)!: carry PHP union types on class properties
ptondereau Apr 28, 2026
5004084
feat(types): support class union type hints
ptondereau Apr 28, 2026
e3efc00
feat(describe)!: carry class unions through stub generation
ptondereau Apr 28, 2026
26d2388
test(integration): cover Foo|Bar class union end-to-end
ptondereau Apr 28, 2026
7cfa381
fix(zend): emit class union as literal name on every PHP version
ptondereau Apr 28, 2026
360dcdc
feat(types): support PHP intersection type hints
ptondereau Apr 29, 2026
f5caa66
feat(describe)!: carry intersection types through stub generation
ptondereau Apr 29, 2026
a21d48f
test(integration): cover Foo&Bar intersection end-to-end
ptondereau Apr 29, 2026
ad21952
chore(flake): add php82 and php83 devshells
ptondereau Apr 29, 2026
879c440
feat(types): support PHP DNF type hints
ptondereau Apr 29, 2026
e3be026
feat(describe)!: carry DNF types through stub generation
ptondereau Apr 29, 2026
c34be54
test(integration): cover (A&B)|C DNF end-to-end on PHP 8.3+
ptondereau Apr 29, 2026
4400fed
feat(types): add FromStr parser and Display for PhpType
ptondereau Apr 29, 2026
5c9fe97
feat(macros): add #[php(types/returns)] proc-macro attribute
ptondereau Apr 29, 2026
a568486
feat(macros): add #[derive(PhpUnion)] for Rust enums
ptondereau Apr 29, 2026
eaefedf
feat(args)!: replace From<Arg> for _zend_expected_type with safe wrapper
ptondereau Apr 30, 2026
3681a6d
chore(macros): wrap LitStr in backticks in validate_php_types_litstr doc
ptondereau Apr 30, 2026
2045f70
chore(clippy): clear pedantic warnings in macro tests and PHP fixtures
ptondereau Apr 30, 2026
f8003dc
feat(builders)!: register typed properties via zend_declare_typed_pro…
ptondereau May 4, 2026
94e82b2
test(builders): add register_property validation gate tests
ptondereau May 4, 2026
8aef195
feat!: extract type-string parser into ext-php-rs-types crate
ptondereau May 4, 2026
ee2fcda
docs(examples): showcase compile-time #[php(types/returns)] in hello_…
ptondereau May 4, 2026
784bcf4
test(integration): cover compound returns via #[php(returns)] attribute
ptondereau May 4, 2026
d5b885f
docs(examples): showcase Rust-class type-string in hello_world
ptondereau May 4, 2026
13bf5b3
docs(guide): document class-union refs in #[php(types/returns)]
ptondereau May 4, 2026
9620a08
docs(macros): sync function/zval_convert lib.rs docs from guide
ptondereau May 4, 2026
83a8fc9
docs(guide): drop internal slice-06 reference in php_union page
ptondereau May 4, 2026
c0667e7
fix(types): rename lits to literals to satisfy typos linter
ptondereau May 4, 2026
940485c
fix(docs): backtick arg_info in PHP-type override section
ptondereau May 4, 2026
8ff69f1
fix(types): import DnfTerm locally in pre-82 property test
ptondereau May 4, 2026
fd93c72
chore(macros): regenerate generated artifacts
ptondereau May 5, 2026
d96d000
fix(types): gate DNF property registration at PHP 8.3+
ptondereau May 5, 2026
d213d81
fix(build): introduce optional libphp linking with bail-on-missing
ptondereau May 5, 2026
3e1fd43
fix(tests): wrap PHP function handlers in zend_fastcall! for windows
ptondereau May 5, 2026
9b7f469
style(tests): apply mago formatting to type fixtures
ptondereau Jun 2, 2026
f10b358
docs(macros): port Exception::getMessage leak note into guide source
ptondereau Jun 2, 2026
17e9eee
fix(types): match PHP semantics for primitive-union parsing
ptondereau Jun 2, 2026
524fba4
docs(macros): note PhpUnion class-union limitation
ptondereau Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,14 @@ jobs:
run: cargo build --release --features closure,anyhow,runtime,observer --workspace
# Test
- name: Test inline examples
# Macos fails on unstable rust. We skip the inline examples test for now.
if: "!(contains(matrix.os, 'macos') && matrix.rust == 'nightly')"
# macOS test binaries crash at load on macos-15 (chained fixups, ld-prime)
# because shivammathur/setup-php's Homebrew formulas (NTS and -debug-zts)
# do not ship libphp at <php-config --prefix>/lib, leaving PHP runtime
# data symbols unresolved. Build coverage on macOS still runs above.
# Restore this step for macOS once a libphp is available on the runner.
if: "!contains(matrix.os, 'macos')"
env:
EXT_PHP_RS_LINK_LIBPHP: "1"
run: cargo test --release --workspace --features closure,anyhow,runtime,observer --no-fail-fast
test-embed:
name: Test with embed (${{ matrix.label }})
Expand Down Expand Up @@ -311,10 +317,3 @@ jobs:
-w /workspace \
extphprs/ext-php-rs:musl-${{ matrix.php }}-${{ matrix.phpts[1] }} \
build --release --features closure,anyhow,runtime,observer --workspace
- name: Run tests
run: |
docker run \
-v $(pwd):/workspace \
-w /workspace \
extphprs/ext-php-rs:musl-${{ matrix.php }}-${{ matrix.phpts[1] }} \
test --workspace --release --features closure,anyhow,runtime,observer --no-fail-fast
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ smartstring = { version = "1", optional = true }
indexmap = { version = "2", optional = true }
inventory = "0.3"
ext-php-rs-derive = { version = "=0.11.14", path = "./crates/macros" }
ext-php-rs-types = { version = "=0.1.0", path = "./crates/types" }

[dev-dependencies]
skeptic = "0.13"
Expand Down Expand Up @@ -61,7 +62,11 @@ runtime = ["ext-php-rs-bindgen/runtime"]
static = ["ext-php-rs-bindgen/static"]

[workspace]
members = ["crates/macros", "crates/cli", "crates/php-build", "tests"]
members = ["crates/macros", "crates/cli", "crates/php-build", "crates/types", "tests"]

[workspace.dependencies]
proc-macro2 = "1.0.26"
quote = "1.0.9"

[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docs"]
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ For more examples read the library
- **Lightweight:** You don't have to use the built-in helper macros. It's
possible to write your own glue code around your own functions.
- **Extensible:** Implement `IntoZval` and `FromZval` for your own custom types,
allowing the type to be used as function parameters and return types.
allowing the type to be used as function parameters and return types. For
PHP type shapes that don't map to a single Rust type (primitive unions,
class unions, intersections, DNF), use `#[php(types = "...")]` on a
parameter or `#[php(returns = "...")]` on a function — see the
[function macro guide](guide/src/macros/function.md#overriding-the-registered-php-type).

## Goals

Expand Down
14 changes: 14 additions & 0 deletions allowed_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,19 @@ bind! {
_sapi_module_struct,
_zend_expected_type,
_zend_expected_type_Z_EXPECTED_ARRAY,
_zend_expected_type_Z_EXPECTED_ARRAY_OR_NULL,
_zend_expected_type_Z_EXPECTED_BOOL,
_zend_expected_type_Z_EXPECTED_BOOL_OR_NULL,
_zend_expected_type_Z_EXPECTED_DOUBLE,
_zend_expected_type_Z_EXPECTED_DOUBLE_OR_NULL,
_zend_expected_type_Z_EXPECTED_LONG,
_zend_expected_type_Z_EXPECTED_LONG_OR_NULL,
_zend_expected_type_Z_EXPECTED_OBJECT,
_zend_expected_type_Z_EXPECTED_OBJECT_OR_NULL,
_zend_expected_type_Z_EXPECTED_RESOURCE,
_zend_expected_type_Z_EXPECTED_RESOURCE_OR_NULL,
_zend_expected_type_Z_EXPECTED_STRING,
_zend_expected_type_Z_EXPECTED_STRING_OR_NULL,
_zend_new_array,
_zval_struct__bindgen_ty_1,
_zval_struct__bindgen_ty_2,
Expand Down Expand Up @@ -86,6 +93,7 @@ bind! {
zend_class_entry,
zend_declare_class_constant,
zend_declare_property,
zend_declare_typed_property,
zend_do_implement_interface,
zend_empty_array,
zend_read_property,
Expand Down Expand Up @@ -146,6 +154,7 @@ bind! {
zend_throw_exception_object,
zend_type,
zend_value,
zend_wrong_parameter_type_error,
zend_wrong_parameters_count_error,
zval,
CONST_CS,
Expand Down Expand Up @@ -267,6 +276,11 @@ bind! {
ts_rsrc_id,
_ZEND_TYPE_NAME_BIT,
_ZEND_TYPE_LITERAL_NAME_BIT,
_ZEND_TYPE_LIST_BIT,
_ZEND_TYPE_UNION_BIT,
_ZEND_TYPE_INTERSECTION_BIT,
_ZEND_TYPE_ARENA_BIT,
zend_type_list,
ZEND_INTERNAL_FUNCTION,
ZEND_USER_FUNCTION,
ZEND_EVAL_CODE,
Expand Down
47 changes: 47 additions & 0 deletions crates/cli/tests/snapshots/stubs_snapshot__hello_world_stubs.snap
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ namespace {

const HELLO_WORLD = 100;

class OtherTestClass {
public function __construct() {}
}

class TestClass {
const NEW_CONSTANT_NAME = 5;

Expand Down Expand Up @@ -58,6 +62,30 @@ namespace {
public static function x(): int {}
}

/**
* Companion to `flexible_id` showing that the same compile-time parsing
* works for class-side type strings. The literal `\TestClass|\OtherTestClass`
* is parsed at macro-expansion time and resolves the class names against
* PHP's global namespace at extension load. Use a leading `\` for the
* fully qualified name; bare `TestClass` works too because the engine
* places `#[php_class]`-defined structs in the global namespace.
*
* @param \TestClass|\OtherTestClass $_value
* @return void
*/
function accept_class_value(\TestClass|\OtherTestClass $_value): void {}

/**
* Demonstrates compound PHP type hints. The argument accepts `int|string`
* and the return type registers as `int|string|null`. Both strings are
* parsed at macro-expansion time, so a typo such as `?Foo&Bar` would
* fail at `cargo build` rather than at extension load.
*
* @param int|string $_value
* @return int|string|null
*/
function flexible_id(int|string $_value): int|string|null {}

/**
* @param object $z
* @return int
Expand All @@ -73,4 +101,23 @@ namespace {
* @return \TestClass
*/
function new_class(): \TestClass {}

/**
* @param bool $use_float
* @return int|float
*/
function pick_number(bool $use_float): int|float {}

/**
* Demonstrates `#[php(returns = "...")]` widening the inferred return
* metadata. The Rust signature returns a concrete `TestClass`, so the
* macro would otherwise register the return type as just `\TestClass`.
* The override widens it to `\TestClass|\OtherTestClass`, which is
* useful when a function returns one specific subtype today but the
* PHP-side contract should leave room for a wider set of legal
* values. Reflection on this function reports the wider union.
*
* @return \TestClass|\OtherTestClass
*/
function produce_test_class_or_other(): \TestClass|\OtherTestClass {}
}
7 changes: 5 additions & 2 deletions crates/macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ proc-macro = true
[dependencies]
syn = { version = "2.0.100", features = ["full", "extra-traits", "printing"] }
darling = "0.23"
quote = "1.0.9"
proc-macro2 = "1.0.26"
quote = { workspace = true }
proc-macro2 = { workspace = true }
convert_case = "0.11.0"
itertools = "0.14.0"
ext-php-rs-types = { version = "=0.1.0", path = "../types", features = [
"proc-macro",
] }

[lints.rust]
missing_docs = "warn"
Expand Down
Loading
Loading