Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
164 changes: 164 additions & 0 deletions src/Edge/ImplicitlyBoundMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

namespace Native\Mobile\Edge;

use Illuminate\Container\BoundMethod;
use Illuminate\Contracts\Routing\UrlRoutable as ImplicitlyBindable;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use ReflectionClass;
use ReflectionNamedType;

/**
* Resolves component method parameters with the same rules as Livewire:
* ordinary classes come from the container; routable models and backed enums
* are implicitly bound from matching values passed to the method.
*/
class ImplicitlyBoundMethod extends BoundMethod
{
protected static function getMethodDependencies($container, $callback, array $parameters = [])
{
return static::resolveMethodDependencies($container, $callback, $parameters)['positional'];
}

public static function resolveMethodDependencies($container, $callback, array $parameters = []): array
{
$positional = [];
$named = [];
$parameterIndex = 0;

foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
$parameterPosition = count($positional);

static::substituteNameBindingForCallParameter($parameter, $parameters, $parameterIndex);
static::substituteImplicitBindingForCallParameter($container, $parameter, $parameters);
static::addDependencyForCallParameter($container, $parameter, $parameters, $positional);

$parameterDependencies = array_slice($positional, $parameterPosition);

if ($parameterDependencies !== []) {
$named[$parameter->getName()] = $parameter->isVariadic()
? $parameterDependencies
: $parameterDependencies[0];
}
}

return [
'positional' => array_values(array_merge($positional, $parameters)),
'named' => $named,
];
}

protected static function substituteNameBindingForCallParameter($parameter, array &$parameters, int &$parameterIndex): void
{
if (! array_key_exists($parameterIndex, $parameters)) {
return;
}

if ($parameter->isVariadic()) {
$parameters = array_merge(
array_filter($parameters, fn ($key) => ! is_int($key), ARRAY_FILTER_USE_KEY),
array_values(array_filter($parameters, fn ($key) => is_int($key), ARRAY_FILTER_USE_KEY)),
);

return;
}

$class = static::getClassForDependencyInjection($parameter);

if ($class !== null && ! $parameters[$parameterIndex] instanceof $class) {
return;
}

if (! array_key_exists($parameter->getName(), $parameters)) {
$parameters[$parameter->getName()] = $parameters[$parameterIndex];
unset($parameters[$parameterIndex]);
$parameterIndex++;
}
}

protected static function substituteImplicitBindingForCallParameter($container, $parameter, array &$parameters): void
{
$class = static::getClassForImplicitBinding($parameter);

if ($class === null) {
return;
}

$name = $parameter->getName();

if (array_key_exists($name, $parameters) && ! $parameters[$name] instanceof $class) {
$parameters[$name] = static::getImplicitBinding($container, $class, $parameters[$name]);
} elseif (array_key_exists($class, $parameters) && ! $parameters[$class] instanceof $class) {
$parameters[$class] = static::getImplicitBinding($container, $class, $parameters[$class]);
}
}

protected static function getClassForDependencyInjection($parameter): ?string
{
$class = static::getParameterClassName($parameter);

if ($class === null || static::isEnum($parameter) || static::implementsImplicitlyBindable($parameter)) {
return null;
}

return $class;
}

protected static function getClassForImplicitBinding($parameter): ?string
{
$class = static::getParameterClassName($parameter);

if ($class === null) {
return null;
}

return static::isEnum($parameter) || static::implementsImplicitlyBindable($parameter)
? $class
: null;
}

protected static function getImplicitBinding($container, string $class, mixed $value): mixed
{
if ($value === null) {
return null;
}

if ((new ReflectionClass($class))->isEnum()) {
return $class::tryFrom($value);
}

$model = $container->make($class)->resolveRouteBinding($value);

if (! $model) {
throw (new ModelNotFoundException)->setModel($class, [$value]);
}

return $model;
}

public static function getParameterClassName($parameter): ?string
{
$type = $parameter->getType();

if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) {
return null;
}

return $type->getName();
}

public static function implementsImplicitlyBindable($parameter): bool
{
$class = static::getParameterClassName($parameter);

return $class !== null
&& (new ReflectionClass($class))->implementsInterface(ImplicitlyBindable::class);
}

public static function isEnum($parameter): bool
{
$class = static::getParameterClassName($parameter);

return $class !== null && (new ReflectionClass($class))->isEnum();
}
}
140 changes: 136 additions & 4 deletions src/Edge/NativeComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Native\Mobile\Edge;

use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
use Illuminate\View\Engines\CompilerEngine;
Expand Down Expand Up @@ -1969,9 +1972,138 @@ private function makeEventInstance(string $eventClass, array $payload): object
return new $eventClass(...$args);
}

public function mount(): void
/**
* Hydrate route-bound public properties and invoke the component's
* optional mount() method through Laravel's container.
*
* NativeComponent deliberately does not declare mount() itself. That lets
* application components use any signature, including route-bound models
* and container dependencies, without violating PHP's inheritance rules.
*
* @internal Called by the router, runloop, and test harness.
*/
final public function mountComponent(): void
{
//
$this->hydrateRouteBoundProperties();

if (! method_exists($this, 'mount')) {
return;
}

$parameters = [];

foreach ((new \ReflectionMethod($this, 'mount'))->getParameters() as $parameter) {
$routeParameter = $this->routeParameterFor($parameter->getName());

if ($routeParameter === null) {
continue;
}

[$routeParameterName, $value] = $routeParameter;
$value = $this->resolveRouteValue($parameter->getType(), $value);

$parameters[$parameter->getName()] = $value;
$this->nativeParams[$routeParameterName] = $value;
}

ImplicitlyBoundMethod::call(app(), [$this, 'mount'], $parameters);
}

/** Hydrate Livewire-style public properties from matching route params. */
private function hydrateRouteBoundProperties(): void
{
$reflection = new \ReflectionClass($this);

foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
if ($property->isStatic()) {
continue;
}

$routeParameter = $this->routeParameterFor($property->getName());

if ($routeParameter === null) {
continue;
}

[$routeParameterName, $value] = $routeParameter;
$value = $this->resolveRouteValue($property->getType(), $value);

$property->setValue($this, $value);
$this->nativeParams[$routeParameterName] = $value;
}
}

/** @return array{string, mixed}|null */
private function routeParameterFor(string $name): ?array
{
foreach ([$name, Str::snake($name)] as $candidate) {
if (array_key_exists($candidate, $this->nativeParams)) {
return [$candidate, $this->nativeParams[$candidate]];
}
}

return null;
}

private function resolveRouteValue(?\ReflectionType $type, mixed $value): mixed
{
if (! $type instanceof \ReflectionNamedType || $type->isBuiltin()) {
return $value;
}

$class = $type->getName();

if (enum_exists($class) && is_subclass_of($class, \BackedEnum::class)) {
return $this->resolveRouteEnum($class, $value);
}

if (is_a($class, UrlRoutable::class, true)) {
return $this->resolveRouteBinding($class, $value);
}

return $value;
}

/** @param class-string<\BackedEnum> $class */
private function resolveRouteEnum(string $class, mixed $value): ?\BackedEnum
{
if ($value === null) {
return null;
}

if ($value instanceof $class) {
return $value;
}

$resolved = $class::tryFrom($value);

if ($resolved === null) {
throw new BackedEnumCaseNotFoundException($class, $value);
}

return $resolved;
}

/** @param class-string<UrlRoutable> $class */
private function resolveRouteBinding(string $class, mixed $value): ?UrlRoutable
{
if ($value === null) {
return null;
}

if ($value instanceof $class) {
return $value;
}

/** @var UrlRoutable $instance */
$instance = app()->make($class);
$resolved = $instance->resolveRouteBinding($value);

if (! $resolved instanceof $class) {
throw (new ModelNotFoundException)->setModel($class, [$value]);
}

return $resolved;
}

public function unmount(): void
Expand Down Expand Up @@ -2083,7 +2215,7 @@ public function run(): void
$this->publishPlaceholder();

try {
$this->mount();
$this->mountComponent();
} catch (NativeDumpException $e) {
$this->renderDumpScreen($e);
} catch (\Throwable $e) {
Expand Down Expand Up @@ -3163,7 +3295,7 @@ public function mountChildComponent(string $tag, array $attrs): void
$this->nativeChildComponentsSeen[$identity] = true;

if ($isNew) {
$child->mount();
$child->mountComponent();
}

$child->renderAsChild();
Expand Down
4 changes: 2 additions & 2 deletions src/Edge/NativeRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ public function preloadStack(array $entries): void
if (! empty($resolved['layout'])) {
$component->setLayout($resolved['layout']);
}
$component->mount();
$component->mountComponent();
} catch (\Throwable $e) {
static::debugLog("preloadStack: skipped $uri — ".$e->getMessage());

Expand Down Expand Up @@ -426,7 +426,7 @@ protected function loop(): ?string
// (potentially slow) mount() so navigation feels instant.
$component->publishPlaceholder();
static::debugLog('loop: calling mount() on '.get_class($component));
$component->mount();
$component->mountComponent();
$this->announce(new ScreenMounted(get_class($component), $entry['uri'] ?? null));
} else {
static::debugLog('loop: calling onResume() on '.get_class($component));
Expand Down
2 changes: 1 addition & 1 deletion src/Testing/TestableComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ protected function __construct(string $componentClass, array $params, array $dat
// device; keep that behavior so the publish is observable.
$component->publishPlaceholder();

$component->mount();
$component->mountComponent();

// A redirect from mount() (e.g. an auth gate) skips the first
// render, exactly like runLoop() honoring a pre-set intent.
Expand Down
Loading
Loading