From e18bfeb9d6ba81bd382d7998b3357784010599b1 Mon Sep 17 00:00:00 2001 From: CodingwithRK Date: Fri, 11 Sep 2026 23:24:25 +0400 Subject: [PATCH 1/3] Support NativePHP Mobile v4 and fix broken bridge calling - Widen the nativephp/mobile constraint to ^3.0|^4.0 in composer.json and in configure.php's rewriteComposerJson() (the hardcoded copy used when scaffolding a new plugin), so plugins can install into v3 or v4 apps. - Fix Plugin::callBridge()/isAvailable() (and the matching stubs/plugin.stub used for scaffolding): they previously only ever resolved a 'nativephp.mobile.bridge' container binding, which nothing in the template or nativephp/mobile itself binds outside of tests. In a real app this meant isAvailable() always returned false and every facade call threw a RuntimeException. They now check for a bound test double first (so BridgeFake keeps working unchanged), then fall back to the documented nativephp_call() global helper that NativePHP's embedded PHP runtime actually provides. Verified by materializing the template via configure.php and running the full suite: pest (17/17 passing), phpstan analyse (no errors). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014H1fiP5bRgiJgVv5t7kunH --- composer.json | 2 +- configure.php | 2 +- src/Plugin.php | 42 ++++++++++++++++++++++++--------------- stubs/plugin.stub | 50 ++++++++++++++++++++++++++++++++++++++--------- 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/composer.json b/composer.json index 22c726a..74015ef 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "php": "^8.2", "illuminate/contracts": "^11.0|^12.0", "illuminate/support": "^11.0|^12.0", - "nativephp/mobile": "^3.0" + "nativephp/mobile": "^3.0|^4.0" }, "require-dev": { "larastan/larastan": "^3.0", diff --git a/configure.php b/configure.php index a34bda5..fb60506 100644 --- a/configure.php +++ b/configure.php @@ -338,7 +338,7 @@ private function rewriteComposerJson( 'php' => '^8.2', 'illuminate/contracts' => '^11.0|^12.0', 'illuminate/support' => '^11.0|^12.0', - 'nativephp/mobile' => '^3.0', + 'nativephp/mobile' => '^3.0|^4.0', ], 'require-dev' => [ 'larastan/larastan' => '^3.0', diff --git a/src/Plugin.php b/src/Plugin.php index b8ebfa1..23e5cf0 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -38,13 +38,13 @@ public function manifest(): array public function isAvailable(): bool { - if (! $this->app->bound('nativephp.mobile.bridge')) { - return false; - } + if ($this->app->bound('nativephp.mobile.bridge')) { + $bridge = $this->app->make('nativephp.mobile.bridge'); - $bridge = $this->app->make('nativephp.mobile.bridge'); + return is_object($bridge) && is_callable([$bridge, 'call']); + } - return is_object($bridge) && is_callable([$bridge, 'call']); + return function_exists('nativephp_call'); } /** @@ -54,22 +54,32 @@ public function isAvailable(): bool */ private function callBridge(string $function, array $payload): array { - $bridge = $this->app->make('nativephp.mobile.bridge'); + // Tests bind a fake (or any double) into the container under this key. + // Prefer it over the real bridge so plugin behaviour stays testable + // without a compiled NativePHP mobile app. + if ($this->app->bound('nativephp.mobile.bridge')) { + $bridge = $this->app->make('nativephp.mobile.bridge'); + + if (is_object($bridge) && is_callable([$bridge, 'call'])) { + /** @var mixed $response */ + $response = $bridge->call($function, $payload); + + return is_array($response) ? $response : ['value' => $response]; + } + } - if (! is_object($bridge) || ! is_callable([$bridge, 'call'])) { + // In a real NativePHP mobile app, native calls go through the + // `nativephp_call()` helper provided by the embedded PHP runtime. + // See: https://nativephp.com/docs/mobile/4/plugins/bridge-functions + if (! function_exists('nativephp_call')) { throw new RuntimeException('The NativePHP mobile bridge is not available for {{ vendor }}/{{ package }}.'); } - /** @var mixed $response */ - $response = $bridge->call($function, $payload); + $response = nativephp_call($function, json_encode($payload, JSON_THROW_ON_ERROR)); - if (is_array($response)) { - /** @var array $response */ - return $response; - } + /** @var mixed $decoded */ + $decoded = json_decode((string) $response, true); - return [ - 'value' => $response, - ]; + return is_array($decoded) ? $decoded : ['value' => $decoded]; } } diff --git a/stubs/plugin.stub b/stubs/plugin.stub index b255b9c..cb80ef1 100644 --- a/stubs/plugin.stub +++ b/stubs/plugin.stub @@ -5,6 +5,7 @@ declare(strict_types=1); namespace {{ namespace }}; use Illuminate\Contracts\Foundation\Application; +use RuntimeException; use {{ namespace }}\Contracts\{{ plugin }}Contract; final class Plugin implements {{ plugin }}Contract @@ -16,23 +17,54 @@ final class Plugin implements {{ plugin }}Contract public function example(array $payload = []): array { - $bridge = $this->app->make('nativephp.mobile.bridge'); + return $this->callBridge('{{ plugin }}.Example', $payload); + } + + public function isAvailable(): bool + { + if ($this->app->bound('nativephp.mobile.bridge')) { + $bridge = $this->app->make('nativephp.mobile.bridge'); - /** @var array $response */ - $response = $bridge->call('{{ plugin }}.Example', $payload); + return is_object($bridge) && is_callable([$bridge, 'call']); + } - return $response; + return function_exists('nativephp_call'); } - public function isAvailable(): bool + /** + * @param array $payload + * + * @return array + */ + private function callBridge(string $function, array $payload): array { - if (! $this->app->bound('nativephp.mobile.bridge')) { - return false; + // Tests bind a fake (or any double) into the container under this key. + // Prefer it over the real bridge so plugin behaviour stays testable + // without a compiled NativePHP mobile app. + if ($this->app->bound('nativephp.mobile.bridge')) { + $bridge = $this->app->make('nativephp.mobile.bridge'); + + if (is_object($bridge) && is_callable([$bridge, 'call'])) { + /** @var mixed $response */ + $response = $bridge->call($function, $payload); + + return is_array($response) ? $response : ['value' => $response]; + } } - $bridge = $this->app->make('nativephp.mobile.bridge'); + // In a real NativePHP mobile app, native calls go through the + // `nativephp_call()` helper provided by the embedded PHP runtime. + // See: https://nativephp.com/docs/mobile/4/plugins/bridge-functions + if (! function_exists('nativephp_call')) { + throw new RuntimeException('The NativePHP mobile bridge is not available for {{ vendor }}/{{ package }}.'); + } + + $response = nativephp_call($function, json_encode($payload, JSON_THROW_ON_ERROR)); + + /** @var mixed $decoded */ + $decoded = json_decode((string) $response, true); - return is_object($bridge) && is_callable([$bridge, 'call']); + return is_array($decoded) ? $decoded : ['value' => $decoded]; } public function manifest(): array From 8096350b5f36134f24788bae4b7de68570f3e03c Mon Sep 17 00:00:00 2001 From: CodingwithRK Date: Fri, 11 Sep 2026 23:44:05 +0400 Subject: [PATCH 2/3] Unwrap BridgeResponse envelope in callBridge() nativephp_call() returns a BridgeResponse envelope (`{"success": true, "data": {...}}`), matching the documented PHP pattern `json_decode($result)?->data`. callBridge() was returning the raw decoded envelope instead of its `data` payload, so callers got `['success' => true, 'data' => [...]]` rather than the native result. Unwrap the `data` key when present, in both src/Plugin.php and the matching stubs/plugin.stub scaffold. The container-double path used by BridgeFake/tests is unaffected since it already returns raw payloads. Verified via configure.php + pest (17/17 passing) + phpstan (no errors). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014H1fiP5bRgiJgVv5t7kunH --- src/Plugin.php | 16 +++++++++++++++- stubs/plugin.stub | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Plugin.php b/src/Plugin.php index 23e5cf0..02a2af8 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -80,6 +80,20 @@ private function callBridge(string $function, array $payload): array /** @var mixed $decoded */ $decoded = json_decode((string) $response, true); - return is_array($decoded) ? $decoded : ['value' => $decoded]; + if (! is_array($decoded)) { + return ['value' => $decoded]; + } + + // The bridge wraps native responses in a BridgeResponse envelope + // (`{"success": true, "data": {...}}`); unwrap it so callers get the + // native payload rather than the envelope itself. + if (array_key_exists('data', $decoded)) { + /** @var mixed $data */ + $data = $decoded['data']; + + return is_array($data) ? $data : ['value' => $data]; + } + + return $decoded; } } diff --git a/stubs/plugin.stub b/stubs/plugin.stub index cb80ef1..0dba593 100644 --- a/stubs/plugin.stub +++ b/stubs/plugin.stub @@ -64,7 +64,21 @@ final class Plugin implements {{ plugin }}Contract /** @var mixed $decoded */ $decoded = json_decode((string) $response, true); - return is_array($decoded) ? $decoded : ['value' => $decoded]; + if (! is_array($decoded)) { + return ['value' => $decoded]; + } + + // The bridge wraps native responses in a BridgeResponse envelope + // (`{"success": true, "data": {...}}`); unwrap it so callers get the + // native payload rather than the envelope itself. + if (array_key_exists('data', $decoded)) { + /** @var mixed $data */ + $data = $decoded['data']; + + return is_array($data) ? $data : ['value' => $data]; + } + + return $decoded; } public function manifest(): array From 465444c00580c208b866e46d63443f6a874ced5d Mon Sep 17 00:00:00 2001 From: CodingwithRK Date: Fri, 11 Sep 2026 23:49:50 +0400 Subject: [PATCH 3/3] Trim explanatory comments in callBridge() Drop the inline commentary added alongside the bridge-fallback and BridgeResponse-unwrap fixes; behavior is unchanged. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014H1fiP5bRgiJgVv5t7kunH --- src/Plugin.php | 9 --------- stubs/plugin.stub | 9 --------- 2 files changed, 18 deletions(-) diff --git a/src/Plugin.php b/src/Plugin.php index 02a2af8..ce64319 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -54,9 +54,6 @@ public function isAvailable(): bool */ private function callBridge(string $function, array $payload): array { - // Tests bind a fake (or any double) into the container under this key. - // Prefer it over the real bridge so plugin behaviour stays testable - // without a compiled NativePHP mobile app. if ($this->app->bound('nativephp.mobile.bridge')) { $bridge = $this->app->make('nativephp.mobile.bridge'); @@ -68,9 +65,6 @@ private function callBridge(string $function, array $payload): array } } - // In a real NativePHP mobile app, native calls go through the - // `nativephp_call()` helper provided by the embedded PHP runtime. - // See: https://nativephp.com/docs/mobile/4/plugins/bridge-functions if (! function_exists('nativephp_call')) { throw new RuntimeException('The NativePHP mobile bridge is not available for {{ vendor }}/{{ package }}.'); } @@ -84,9 +78,6 @@ private function callBridge(string $function, array $payload): array return ['value' => $decoded]; } - // The bridge wraps native responses in a BridgeResponse envelope - // (`{"success": true, "data": {...}}`); unwrap it so callers get the - // native payload rather than the envelope itself. if (array_key_exists('data', $decoded)) { /** @var mixed $data */ $data = $decoded['data']; diff --git a/stubs/plugin.stub b/stubs/plugin.stub index 0dba593..bd9ad22 100644 --- a/stubs/plugin.stub +++ b/stubs/plugin.stub @@ -38,9 +38,6 @@ final class Plugin implements {{ plugin }}Contract */ private function callBridge(string $function, array $payload): array { - // Tests bind a fake (or any double) into the container under this key. - // Prefer it over the real bridge so plugin behaviour stays testable - // without a compiled NativePHP mobile app. if ($this->app->bound('nativephp.mobile.bridge')) { $bridge = $this->app->make('nativephp.mobile.bridge'); @@ -52,9 +49,6 @@ final class Plugin implements {{ plugin }}Contract } } - // In a real NativePHP mobile app, native calls go through the - // `nativephp_call()` helper provided by the embedded PHP runtime. - // See: https://nativephp.com/docs/mobile/4/plugins/bridge-functions if (! function_exists('nativephp_call')) { throw new RuntimeException('The NativePHP mobile bridge is not available for {{ vendor }}/{{ package }}.'); } @@ -68,9 +62,6 @@ final class Plugin implements {{ plugin }}Contract return ['value' => $decoded]; } - // The bridge wraps native responses in a BridgeResponse envelope - // (`{"success": true, "data": {...}}`); unwrap it so callers get the - // native payload rather than the envelope itself. if (array_key_exists('data', $decoded)) { /** @var mixed $data */ $data = $decoded['data'];