aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-06 20:38:19 +0900
committernsfisis <nsfisis@gmail.com>2026-08-06 20:38:19 +0900
commit91e7102d02cfcda0bf1b82ae1b3fb30900495360 (patch)
tree36be9051ad3a0a12c72ec4ab2afb2359b8983c65
parenta7bd624b3e9d4e9493435be1e6016303830e7087 (diff)
downloadphp-shirabe-91e7102d02cfcda0bf1b82ae1b3fb30900495360.tar.gz
php-shirabe-91e7102d02cfcda0bf1b82ae1b3fb30900495360.tar.zst
php-shirabe-91e7102d02cfcda0bf1b82ae1b3fb30900495360.zip
fix(platform-repository): report the icu and imagick libraries
PlatformRepository probes ResourceBundle, IntlChar and Imagick to derive lib-icu-cldr, lib-icu-unicode and lib-imagick-imagemagick. Those probes ran against the shim's hard-coded class_exists allowlist, which never names them, so the packages were silently missing: on a machine with intl, `show --platform` listed fewer libraries than upstream Composer does. The runtime seam now asks the real PHP: hasClass over RPC, and construct / invoke through the worker for the three classes PlatformRepository reaches. A live PHP object has no PhpMixed counterpart, so the seam answers with the entries the caller reads off it. The seam's own callers read those entries instead of returning null and the empty string. Two addLibrary calls also had replaces and provides swapped, dropping `lib-libxslt replaces lib-xsl` and `lib-zip-libzip replaces lib-zip`. `show --platform` now matches upstream Composer byte for byte, and all 59 provideLibraryTestCases datasets pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs8
-rw-r--r--crates/shirabe-php-shim/src/var.rs6
-rw-r--r--crates/shirabe/src/console/application.rs4
-rw-r--r--crates/shirabe/src/platform/runtime.rs110
-rw-r--r--crates/shirabe/src/repository/platform_repository.rs27
-rw-r--r--crates/shirabe/tests/repository/platform_repository_test.rs9
6 files changed, 129 insertions, 35 deletions
diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs
index 05067420..085425c9 100644
--- a/crates/shirabe-php-rpc/src/lib.rs
+++ b/crates/shirabe-php-rpc/src/lib.rs
@@ -44,6 +44,14 @@ pub fn get_constant(name: &str) -> PhpMixed {
call("constant", name)
}
+/// PHP `class_exists($name)`, with autoloading, as the runtime sees it.
+pub fn class_exists(name: &str) -> bool {
+ match call("class_exists", name) {
+ PhpMixed::Bool(exists) => exists,
+ other => panic!("PHP RPC: `class_exists` returned an unexpected value: {other:?}"),
+ }
+}
+
/// PHP `inet_pton($address)`.
pub fn inet_pton(address: &str) -> PhpMixed {
call("inet_pton", address)
diff --git a/crates/shirabe-php-shim/src/var.rs b/crates/shirabe-php-shim/src/var.rs
index 4b0ed2f5..dfbd2f03 100644
--- a/crates/shirabe-php-shim/src/var.rs
+++ b/crates/shirabe-php-shim/src/var.rs
@@ -249,12 +249,6 @@ pub fn get_debug_type_obj<T>(_value: &T) -> String {
std::any::type_name::<T>().to_string()
}
-pub fn instantiate_class(_class: &str, _args: Vec<PhpMixed>) -> PhpMixed {
- // TODO(php-runtime): instantiating a class by name needs a runtime class registry (reflection),
- // which the shim does not provide.
- todo!()
-}
-
pub fn php_to_string(value: &PhpMixed) -> String {
match value {
PhpMixed::Null => String::new(),
diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs
index d6a287c2..4136a830 100644
--- a/crates/shirabe/src/console/application.rs
+++ b/crates/shirabe/src/console/application.rs
@@ -2466,10 +2466,6 @@ impl ApplicationHandle {
// recognizes user classes, so the arm stays
// unreachable until the checks and the instantiation
// go through the worker.
- let _ = shirabe_php_shim::instantiate_class(
- &dummy_str,
- vec![PhpMixed::String(script.clone())],
- );
todo!(
"plugin: import a user Command class as a live application command"
);
diff --git a/crates/shirabe/src/platform/runtime.rs b/crates/shirabe/src/platform/runtime.rs
index 28cfa095..265f007c 100644
--- a/crates/shirabe/src/platform/runtime.rs
+++ b/crates/shirabe/src/platform/runtime.rs
@@ -2,9 +2,10 @@
use indexmap::IndexMap;
use shirabe_external_packages::composer::pcre::{CaptureKey, Preg};
+use shirabe_php_rpc::{PhpThrow, PluginValue};
use shirabe_php_shim::{
- PhpMixed, class_exists, function_exists, html_entity_decode, implode, instantiate_class, ltrim,
- php_regex, strip_tags, trim,
+ PhpMixed, RuntimeException, function_exists, html_entity_decode, implode, ltrim, php_regex,
+ strip_tags, trim,
};
/// Seam over the PHP runtime so PlatformRepository can be tested against mocked
@@ -57,19 +58,35 @@ impl RuntimeInterface for Runtime {
}
PhpMixed::Array(version)
}
- _ => todo!(),
+ (PhpMixed::List(spec), _) => match class_callable(spec) {
+ ("ResourceBundle", "create") => resource_bundle_create(arguments),
+ ("IntlChar", "getUnicodeVersion") => {
+ php_value(shirabe_php_rpc::call_static_method(
+ "IntlChar",
+ "getUnicodeVersion",
+ Vec::new(),
+ None,
+ ))
+ }
+ (class, method) => panic!(
+ "the PHP callable `{class}::{method}` is not wired through the runtime seam"
+ ),
+ },
+ _ => panic!("the PHP callable {callable:?} is not wired through the runtime seam"),
}
}
fn has_class(&self, class: &str) -> bool {
- class_exists(class)
+ shirabe_php_rpc::class_exists(class)
}
fn construct(&self, class: &str, arguments: Vec<PhpMixed>) -> anyhow::Result<PhpMixed> {
- if arguments.is_empty() {
- Ok(instantiate_class(class, vec![]))
- } else {
- Ok(instantiate_class(class, arguments))
+ match class {
+ "Imagick" => imagick_version(arguments),
+ other => Err(anyhow::anyhow!(RuntimeException {
+ message: format!("the PHP class `{other}` is not wired through the runtime seam"),
+ code: 0,
+ })),
}
}
@@ -86,6 +103,83 @@ impl RuntimeInterface for Runtime {
}
}
+/// The `[class, method]` pair of a PHP callable given in array form.
+fn class_callable(spec: &[PhpMixed]) -> (&str, &str) {
+ match spec {
+ [PhpMixed::String(class), PhpMixed::String(method)] => (class, method),
+ other => panic!("a PHP callable given as an array must be [class, method], got {other:?}"),
+ }
+}
+
+/// Unwraps an RPC outcome whose failure means the runtime probe itself is broken, not that the
+/// probed extension is absent.
+fn php_value(outcome: anyhow::Result<Result<PluginValue, PhpThrow>>) -> PhpMixed {
+ match outcome {
+ Ok(Ok(value)) => value
+ .to_php_mixed()
+ .expect("a runtime probe answers with plain values"),
+ Ok(Err(throw)) => panic!("the PHP runtime probe failed: {}", throw.message),
+ Err(e) => panic!("the PHP runtime probe could not be sent: {e:#}"),
+ }
+}
+
+/// PHP `ResourceBundle::create(...)`, whose result the caller reads `->get('Version')` off.
+/// A live PHP object has no `PhpMixed` counterpart, so that entry crosses in its place.
+fn resource_bundle_create(arguments: Vec<PhpMixed>) -> PhpMixed {
+ let bundle = match php_handle(shirabe_php_rpc::call_static_method(
+ "ResourceBundle",
+ "create",
+ arguments.iter().map(PluginValue::from_php_mixed).collect(),
+ None,
+ )) {
+ Some(phandle) => phandle,
+ // PHP returns null when the bundle cannot be opened.
+ None => return PhpMixed::Null,
+ };
+ let version = php_value(shirabe_php_rpc::call_php_method(
+ bundle,
+ "get",
+ vec![PluginValue::string("Version")],
+ None,
+ ));
+ let _ = shirabe_php_rpc::release_php_handle(bundle);
+ PhpMixed::Object(IndexMap::from([("Version".to_string(), version)]))
+}
+
+/// PHP `(new Imagick())->getVersion()`, reported as the entries the caller reads.
+fn imagick_version(arguments: Vec<PhpMixed>) -> anyhow::Result<PhpMixed> {
+ let imagick = php_handle(shirabe_php_rpc::new_object(
+ "Imagick",
+ arguments.iter().map(PluginValue::from_php_mixed).collect(),
+ None,
+ ))
+ .ok_or_else(|| {
+ anyhow::anyhow!(RuntimeException {
+ message: "`new Imagick` did not answer with an object".to_string(),
+ code: 0,
+ })
+ })?;
+ let version = php_value(shirabe_php_rpc::call_php_method(
+ imagick,
+ "getVersion",
+ Vec::new(),
+ None,
+ ));
+ let _ = shirabe_php_rpc::release_php_handle(imagick);
+ Ok(version)
+}
+
+/// The handle of a PHP-side object an RPC answered with, or `None` when it answered with null.
+fn php_handle(outcome: anyhow::Result<Result<PluginValue, PhpThrow>>) -> Option<u64> {
+ match outcome {
+ Ok(Ok(PluginValue::PhpHandle(handle))) => Some(handle.phandle),
+ Ok(Ok(PluginValue::Null)) => None,
+ Ok(Ok(other)) => panic!("the PHP runtime probe answered with {other:?}, not an object"),
+ Ok(Err(throw)) => panic!("the PHP runtime probe failed: {}", throw.message),
+ Err(e) => panic!("the PHP runtime probe could not be sent: {e:#}"),
+ }
+}
+
impl Runtime {
pub fn has_function(&self, f: &str) -> bool {
function_exists(f)
diff --git a/crates/shirabe/src/repository/platform_repository.rs b/crates/shirabe/src/repository/platform_repository.rs
index 3f22954c..5b205ba0 100644
--- a/crates/shirabe/src/repository/platform_repository.rs
+++ b/crates/shirabe/src/repository/platform_repository.rs
@@ -1378,8 +1378,8 @@ impl PlatformRepository {
"libxslt",
libxslt_str.as_deref(),
None,
- &[],
&["xsl".to_string()],
+ &[],
)?;
let info = self.runtime.get_extension_info("xsl")?;
@@ -1443,8 +1443,8 @@ impl PlatformRepository {
&format!("{}-libzip", name),
libzip_str.as_deref(),
None,
- &[],
&["zip".to_string()],
+ &[],
)?;
}
}
@@ -1839,14 +1839,25 @@ impl PlatformRepository {
package.as_complete().is_some()
}
- fn resource_bundle_get(_value: &PhpMixed, _key: &str) -> PhpMixed {
- // TODO(plugin): proper ResourceBundle::get($key) dispatch on a PHP object.
- PhpMixed::Null
+ /// PHP `$resourceBundle->get($key)`. A live PHP object has no `PhpMixed` counterpart, so
+ /// [`RuntimeInterface`] answers with the entries the caller reads instead of the object.
+ fn resource_bundle_get(value: &PhpMixed, key: &str) -> PhpMixed {
+ Self::php_object_field(value, key).unwrap_or(PhpMixed::Null)
}
- fn imagick_get_version_string(_value: &PhpMixed) -> String {
- // TODO(plugin): proper Imagick->getVersion()['versionString'] dispatch.
- "".to_string()
+ /// PHP `$imagick->getVersion()['versionString']`, read the same way.
+ fn imagick_get_version_string(value: &PhpMixed) -> String {
+ match Self::php_object_field(value, "versionString") {
+ Some(PhpMixed::String(version)) => version,
+ _ => String::new(),
+ }
+ }
+
+ fn php_object_field(value: &PhpMixed, key: &str) -> Option<PhpMixed> {
+ match value {
+ PhpMixed::Object(fields) | PhpMixed::Array(fields) => fields.get(key).cloned(),
+ _ => None,
+ }
}
fn php_array_to_string_vec(value: &PhpMixed) -> Vec<String> {
diff --git a/crates/shirabe/tests/repository/platform_repository_test.rs b/crates/shirabe/tests/repository/platform_repository_test.rs
index 507938a0..3a4bf45d 100644
--- a/crates/shirabe/tests/repository/platform_repository_test.rs
+++ b/crates/shirabe/tests/repository/platform_repository_test.rs
@@ -1612,16 +1612,7 @@ fn assert_package_links(
}
}
-// TODO(phase-d): blocked by the TODO(plugin) stubs below; re-check once the plugin RPC
-// mechanism can dispatch method calls on PHP objects.
#[test]
-#[ignore = "all 59 provideLibraryTestCases datasets ported faithfully; blocked by two \
- TODO(plugin) stubs in PlatformRepository that need dynamic method dispatch on \
- PHP objects ($resourceBundle->get('Version'), $imagick->getVersion()): \
- resource_bundle_get returns Null so the intl dataset drops lib-icu-cldr, and \
- imagick_get_version_string returns \"\" so the imagick datasets drop \
- lib-imagick-imagemagick (confirmed current failure mode: package-set mismatch \
- on the intl dataset, not a panic)"]
fn test_library_information() {
let extension_version = "100.200.300";