diff --git a/.gitattributes b/.gitattributes index 7450a3ed..a1fcbb6d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,4 @@ /tests export-ignore /CHANGELOG.md export-ignore /README.md export-ignore +/phpstan.neon export-ignore \ No newline at end of file diff --git a/.github/workflows/facade.yml b/.github/workflows/facade.yml index b2917730..222232cd 100644 --- a/.github/workflows/facade.yml +++ b/.github/workflows/facade.yml @@ -21,7 +21,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 tools: composer:v2 coverage: none diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 00000000..76e03c0b --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,14 @@ +name: static analysis + +on: + push: + branches: + - "2.x" + pull_request: + +permissions: + contents: read + +jobs: + tests: + uses: laravel/.github/.github/workflows/static-analysis.yml@main diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3591aea7..9bf4e713 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,6 +37,10 @@ jobs: tools: composer:v2 coverage: none + - name: Remove PHPStan on PHP 8.1 and Laravel 10 + run: composer remove --dev larastan/larastan + if: matrix.php == 8.1 || matrix.laravel == 10 + - name: Set Minimum PHP 8.1 Versions uses: nick-fields/retry@v3 with: diff --git a/composer.json b/composer.json index d26a3e36..d8ea1ddf 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,8 @@ "orchestra/testbench": "^8.0|^9.2|^10.0", "mockery/mockery": "^1.3.3", "phpunit/phpunit": "^10.4|^11.5", - "laravel/pint": "^1.16" + "laravel/pint": "^1.16", + "larastan/larastan": "^3.0" }, "suggest": { "ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command." @@ -53,4 +54,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} +} \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..6bbf8839 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,17 @@ +includes: + - vendor/larastan/larastan/extension.neon + +parameters: + level: 6 + paths: + - src + - tests + excludePaths: + - src/Testing/Concerns + ignoreErrors: + - '#Call to an undefined method.*TestResponse.*::assertInertia\(\)#' + - '#Call to an undefined method.*TestResponse.*::inertiaPage\(\)#' + - '#Call to an undefined method.*TestResponse.*::inertiaProps\(\)#' + - '#Call to an undefined.*method.*Request::inertia\(\)#' + - '#Call to an undefined.*method.*Route::inertia\(\)#' + diff --git a/src/AlwaysProp.php b/src/AlwaysProp.php index 62066ebf..056a23c6 100644 --- a/src/AlwaysProp.php +++ b/src/AlwaysProp.php @@ -6,10 +6,20 @@ class AlwaysProp { - /** @var mixed */ + /** + * The property value. + * + * Always included in Inertia responses, bypassing partial reload filtering. + * + * @var mixed + */ protected $value; /** + * Create a new always property instance. Always properties are included + * in every Inertia response, even during partial reloads when only + * specific props are requested. + * * @param mixed $value */ public function __construct($value) @@ -17,6 +27,11 @@ public function __construct($value) $this->value = $value; } + /** + * Resolve the property value. + * + * @return mixed + */ public function __invoke() { return is_callable($this->value) ? App::call($this->value) : $this->value; diff --git a/src/Commands/CheckSsr.php b/src/Commands/CheckSsr.php index fa355b1d..98c71244 100644 --- a/src/Commands/CheckSsr.php +++ b/src/Commands/CheckSsr.php @@ -25,7 +25,7 @@ class CheckSsr extends Command protected $description = 'Check the Inertia SSR server health status'; /** - * check the SSR server via a Node process. + * Check the Inertia SSR server health status. */ public function handle(Gateway $gateway): int { diff --git a/src/Commands/CreateMiddleware.php b/src/Commands/CreateMiddleware.php index bd5c4a09..9c28d230 100644 --- a/src/Commands/CreateMiddleware.php +++ b/src/Commands/CreateMiddleware.php @@ -50,6 +50,8 @@ protected function getDefaultNamespace($rootNamespace): string /** * Get the console command arguments. + * + * @return array> */ protected function getArguments(): array { @@ -60,6 +62,8 @@ protected function getArguments(): array /** * Get the console command options. + * + * @return array> */ protected function getOptions(): array { diff --git a/src/Commands/StartSsr.php b/src/Commands/StartSsr.php index ae5d96f4..99d1ff98 100644 --- a/src/Commands/StartSsr.php +++ b/src/Commands/StartSsr.php @@ -26,7 +26,7 @@ class StartSsr extends Command protected $description = 'Start the Inertia SSR server'; /** - * Start the SSR server via a Node process. + * Start the Inertia SSR server. */ public function handle(): int { diff --git a/src/Commands/StopSsr.php b/src/Commands/StopSsr.php index 0989bfc0..a62b9314 100644 --- a/src/Commands/StopSsr.php +++ b/src/Commands/StopSsr.php @@ -23,7 +23,7 @@ class StopSsr extends Command protected $description = 'Stop the Inertia SSR server'; /** - * Stop the SSR server. + * Stop the Inertia SSR server. */ public function handle(): int { diff --git a/src/Controller.php b/src/Controller.php index 0a19f766..3b5c63f3 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -6,6 +6,10 @@ class Controller { + /** + * Handle the incoming request and render the Inertia response. + * Renders the component and props defined in the route defaults. + */ public function __invoke(Request $request): Response { return Inertia::render( diff --git a/src/DeferProp.php b/src/DeferProp.php index db722c75..ed93219f 100644 --- a/src/DeferProp.php +++ b/src/DeferProp.php @@ -8,21 +8,50 @@ class DeferProp implements IgnoreFirstLoad, Mergeable { use MergesProps; + /** + * The callback to resolve the property. + * + * Loaded asynchronously after initial page render for performance. + * + * @var callable + */ protected $callback; + /** + * The defer group. + * + * @var string|null + */ protected $group; + /** + * Create a new deferred property instance. Deferred properties are excluded + * from the initial page load and only evaluated when requested by the + * frontend, improving initial page performance. + */ public function __construct(callable $callback, ?string $group = null) { $this->callback = $callback; $this->group = $group; } + /** + * Get the defer group for this property. Properties with the same group + * are loaded together in a single request, allowing for efficient + * batching of related deferred data. + * + * @return string|null + */ public function group() { return $this->group; } + /** + * Resolve the property value. + * + * @return mixed + */ public function __invoke() { return App::call($this->callback); diff --git a/src/Directive.php b/src/Directive.php index dca132f7..b3114a9f 100644 --- a/src/Directive.php +++ b/src/Directive.php @@ -5,7 +5,9 @@ class Directive { /** - * Compiles the "@inertia" directive. + * Compile the "@inertia" Blade directive. This directive renders the + * Inertia root element with the page data, handling both client-side + * rendering and SSR fallback scenarios. * * @param string $expression */ @@ -30,7 +32,9 @@ public static function compile($expression = ''): string } /** - * Compiles the "@inertiaHead" directive. + * Compile the "@inertiaHead" Blade directive. This directive renders the + * head content for SSR responses, including meta tags, title, and other + * head elements from the server-side render. * * @param string $expression */ diff --git a/src/EncryptHistoryMiddleware.php b/src/EncryptHistoryMiddleware.php index e7eefa1a..78f34ee0 100644 --- a/src/EncryptHistoryMiddleware.php +++ b/src/EncryptHistoryMiddleware.php @@ -4,14 +4,15 @@ use Closure; use Illuminate\Http\Request; -use Symfony\Component\HttpFoundation\Response; class EncryptHistoryMiddleware { /** - * Handle the incoming request. + * Handle the incoming request and enable history encryption. This middleware + * enables encryption of the browser history state, providing additional + * security for sensitive data in Inertia responses. * - * @return Response + * @return \Symfony\Component\HttpFoundation\Response */ public function handle(Request $request, Closure $next) { diff --git a/src/Inertia.php b/src/Inertia.php index 1d0adb38..47754ae3 100644 --- a/src/Inertia.php +++ b/src/Inertia.php @@ -6,7 +6,7 @@ /** * @method static void setRootView(string $name) - * @method static void share(string|array|\Illuminate\Contracts\Support\Arrayable $key, mixed $value = null) + * @method static void share(string|array|\Illuminate\Contracts\Support\Arrayable|\Inertia\ProvidesInertiaProperties $key, mixed $value = null) * @method static mixed getShared(string|null $key = null, mixed $default = null) * @method static void clearHistory() * @method static void encryptHistory($encrypt = true) @@ -20,7 +20,7 @@ * @method static \Inertia\AlwaysProp always(mixed $value) * @method static \Inertia\MergeProp merge(mixed $value) * @method static \Inertia\MergeProp deepMerge(mixed $value) - * @method static \Inertia\Response render(string $component, array|\Illuminate\Contracts\Support\Arrayable $props = []) + * @method static \Inertia\Response render(string $component, array|\Illuminate\Contracts\Support\Arrayable|\Inertia\ProvidesInertiaProperties $props = []) * @method static \Symfony\Component\HttpFoundation\Response location(string|\Symfony\Component\HttpFoundation\RedirectResponse $url) * @method static void macro(string $name, object|callable $macro) * @method static void mixin(object $mixin, bool $replace = true) diff --git a/src/LazyProp.php b/src/LazyProp.php index 5495aac7..da53cf31 100644 --- a/src/LazyProp.php +++ b/src/LazyProp.php @@ -4,15 +4,31 @@ use Illuminate\Support\Facades\App; +/** + * @deprecated Use OptionalProp instead for clearer semantics. + */ class LazyProp implements IgnoreFirstLoad { + /** + * The callback to resolve the property. + * + * @var callable + */ protected $callback; + /** + * Create a new lazy property instance. + */ public function __construct(callable $callback) { $this->callback = $callback; } + /** + * Resolve the property value. + * + * @return mixed + */ public function __invoke() { return App::call($this->callback); diff --git a/src/MergeProp.php b/src/MergeProp.php index 499b43f4..efcf027f 100644 --- a/src/MergeProp.php +++ b/src/MergeProp.php @@ -8,10 +8,20 @@ class MergeProp implements Mergeable { use MergesProps; - /** @var mixed */ + /** + * The property value. + * + * Merged with existing client-side data during partial reloads. + * + * @var mixed + */ protected $value; /** + * Create a new merge property instance. Merge properties are combined + * with existing client-side data during partial reloads instead of + * completely replacing the property value. + * * @param mixed $value */ public function __construct($value) @@ -20,6 +30,11 @@ public function __construct($value) $this->merge = true; } + /** + * Resolve the property value. + * + * @return mixed + */ public function __invoke() { return is_callable($this->value) ? App::call($this->value) : $this->value; diff --git a/src/Mergeable.php b/src/Mergeable.php index 64878a05..f4f5281d 100644 --- a/src/Mergeable.php +++ b/src/Mergeable.php @@ -4,7 +4,31 @@ interface Mergeable { + /** + * Mark the property for merging. + * + * @return static + */ public function merge(); + /** + * Determine if the property should be merged. + * + * @return bool + */ public function shouldMerge(); + + /** + * Determine if the property should be deep merged. + * + * @return bool + */ + public function shouldDeepMerge(); + + /** + * Get the properties to match on for merging. + * + * @return array + */ + public function matchesOn(); } diff --git a/src/MergesProps.php b/src/MergesProps.php index 6e5423d0..af7ee12a 100644 --- a/src/MergesProps.php +++ b/src/MergesProps.php @@ -6,12 +6,26 @@ trait MergesProps { + /** + * Indicates if the property should be merged. + */ protected bool $merge = false; + /** + * Indicates if the property should be deep merged. + */ protected bool $deepMerge = false; + /** + * The properties to match on for merging. + * + * @var array + */ protected array $matchOn = []; + /** + * Mark the property for merging. + */ public function merge(): static { $this->merge = true; @@ -19,6 +33,9 @@ public function merge(): static return $this; } + /** + * Mark the property for deep merging. + */ public function deepMerge(): static { $this->deepMerge = true; @@ -26,6 +43,11 @@ public function deepMerge(): static return $this->merge(); } + /** + * Set the properties to match on for merging. + * + * @param string|array $matchOn + */ public function matchOn(string|array $matchOn): static { $this->matchOn = Arr::wrap($matchOn); @@ -33,16 +55,27 @@ public function matchOn(string|array $matchOn): static return $this; } + /** + * Determine if the property should be merged. + */ public function shouldMerge(): bool { return $this->merge; } + /** + * Determine if the property should be deep merged. + */ public function shouldDeepMerge(): bool { return $this->deepMerge; } + /** + * Get the properties to match on for merging. + * + * @return array + */ public function matchesOn(): array { return $this->matchOn; diff --git a/src/Middleware.php b/src/Middleware.php index 482a99d5..81b6ab9d 100644 --- a/src/Middleware.php +++ b/src/Middleware.php @@ -4,7 +4,9 @@ use Closure; use Illuminate\Http\Request; +use Illuminate\Session\Store; use Illuminate\Support\Facades\Redirect; +use Illuminate\Support\MessageBag; use Inertia\Support\Header; use Symfony\Component\HttpFoundation\Response; @@ -20,9 +22,7 @@ class Middleware protected $rootView = 'app'; /** - * Determines the current asset version. - * - * @see https://inertiajs.com/asset-versioning + * Determine the current asset version. * * @return string|null */ @@ -44,9 +44,7 @@ public function version(Request $request) } /** - * Defines the props that are shared by default. - * - * @see https://inertiajs.com/shared-data + * Define the props that are shared by default. * * @return array */ @@ -58,9 +56,7 @@ public function share(Request $request) } /** - * Sets the root template that's loaded on the first page visit. - * - * @see https://inertiajs.com/server-side-setup#root-template + * Set the root template that is loaded on the first page visit. * * @return string */ @@ -70,9 +66,9 @@ public function rootView(Request $request) } /** - * Defines a callback that returns the relative URL. + * Define a callback that returns the relative URL. * - * @return Closure|null + * @return \Closure|null */ public function urlResolver() { @@ -82,7 +78,7 @@ public function urlResolver() /** * Handle the incoming request. * - * @return Response + * @return \Symfony\Component\HttpFoundation\Response */ public function handle(Request $request, Closure $next) { @@ -120,8 +116,7 @@ public function handle(Request $request, Closure $next) } /** - * Determines what to do when an Inertia action returned with no response. - * By default, we'll redirect the user back to where they came from. + * Handle empty responses. */ public function onEmptyResponse(Request $request, Response $response): Response { @@ -129,21 +124,21 @@ public function onEmptyResponse(Request $request, Response $response): Response } /** - * Determines what to do when the Inertia asset version has changed. - * By default, we'll initiate a client-side location visit to force an update. + * Handle version changes. */ public function onVersionChange(Request $request, Response $response): Response { if ($request->hasSession()) { - $request->session()->reflash(); + /** @var Store $session */ + $session = $request->session(); + $session->reflash(); } return Inertia::location($request->fullUrl()); } /** - * Resolves and prepares validation errors in such - * a way that they are easier to use client-side. + * Resolve validation errors for client-side use. * * @return object */ @@ -153,7 +148,10 @@ public function resolveValidationErrors(Request $request) return (object) []; } - return (object) collect($request->session()->get('errors')->getBags())->map(function ($bag) { + /** @var array $bags */ + $bags = $request->session()->get('errors')->getBags(); + + return (object) collect($bags)->map(function ($bag) { return (object) collect($bag->messages())->map(function ($errors) { return $errors[0]; })->toArray(); diff --git a/src/OptionalProp.php b/src/OptionalProp.php index 93b1c7d7..b4e3351d 100644 --- a/src/OptionalProp.php +++ b/src/OptionalProp.php @@ -6,13 +6,30 @@ class OptionalProp implements IgnoreFirstLoad { + /** + * The callback to resolve the property. + * + * Only included when explicitly requested via partial reloads. + * + * @var callable + */ protected $callback; + /** + * Create a new optional property instance. Optional properties are only + * included when explicitly requested via partial reloads, reducing + * initial payload size and improving performance. + */ public function __construct(callable $callback) { $this->callback = $callback; } + /** + * Resolve the property value. + * + * @return mixed + */ public function __invoke() { return App::call($this->callback); diff --git a/src/PropertyContext.php b/src/PropertyContext.php index 8375850e..4442ec4d 100644 --- a/src/PropertyContext.php +++ b/src/PropertyContext.php @@ -6,6 +6,13 @@ class PropertyContext { + /** + * Create a new property context instance. The property context provides + * information about the current property being resolved to objects + * implementing ProvidesInertiaProperty. + * + * @param array $props + */ public function __construct( public string $key, public array $props, diff --git a/src/ProvidesInertiaProperties.php b/src/ProvidesInertiaProperties.php index 15e0cc01..9ad59673 100644 --- a/src/ProvidesInertiaProperties.php +++ b/src/ProvidesInertiaProperties.php @@ -4,5 +4,12 @@ interface ProvidesInertiaProperties { + /** + * Get the properties to be provided to Inertia. This method allows objects + * to dynamically provide properties that will be serialized and sent + * to the frontend. + * + * @return array + */ public function toInertiaProperties(RenderContext $context): iterable; } diff --git a/src/ProvidesInertiaProperty.php b/src/ProvidesInertiaProperty.php index b54ee742..1b57da75 100644 --- a/src/ProvidesInertiaProperty.php +++ b/src/ProvidesInertiaProperty.php @@ -4,5 +4,10 @@ interface ProvidesInertiaProperty { + /** + * Convert the instance to an Inertia property value. This method is called + * when the object is used as a property value in an Inertia response, + * allowing for custom serialization. + */ public function toInertiaProperty(PropertyContext $prop): mixed; } diff --git a/src/RenderContext.php b/src/RenderContext.php index 3e50f223..ff69109d 100644 --- a/src/RenderContext.php +++ b/src/RenderContext.php @@ -6,6 +6,11 @@ class RenderContext { + /** + * Create a new render context instance. The render context provides + * information about the current Inertia render operation to objects + * implementing ProvidesInertiaProperties. + */ public function __construct( public string $component, public Request $request diff --git a/src/Response.php b/src/Response.php index c0933da5..9108020f 100644 --- a/src/Response.php +++ b/src/Response.php @@ -20,26 +20,71 @@ class Response implements Responsable { use Macroable; + /** + * The name of the root component. + * + * @var string + */ protected $component; + /** + * The page props. + * + * @var array + */ protected $props; + /** + * The name of the root view. + * + * @var string + */ protected $rootView; + /** + * The asset version. + * + * @var string + */ protected $version; + /** + * Indicates if the browser history should be cleared. + * + * @var bool + */ protected $clearHistory; + /** + * Indicates if the browser history should be encrypted. + * + * @var bool + */ protected $encryptHistory; + /** + * The view data. + * + * @var array + */ protected $viewData = []; + /** + * The cache duration settings. + * + * @var array + */ protected $cacheFor = []; + /** + * The URL resolver callback. + */ protected ?Closure $urlResolver = null; /** - * @param array|Arrayable $props + * Create a new Inertia response instance. + * + * @param array $props */ public function __construct( string $component, @@ -50,7 +95,7 @@ public function __construct( ?Closure $urlResolver = null ) { $this->component = $component; - $this->props = $props instanceof Arrayable ? $props->toArray() : $props; + $this->props = $props; $this->rootView = $rootView; $this->version = $version; $this->clearHistory = session()->pull('inertia.clear_history', false); @@ -59,7 +104,9 @@ public function __construct( } /** - * @param string|array|ProvidesInertiaProperties $key + * Add additional properties to the page. + * + * @param string|array|ProvidesInertiaProperties $key * @param mixed $value * @return $this */ @@ -77,7 +124,9 @@ public function with($key, $value = null): self } /** - * @param string|array $key + * Add additional data to the view. + * + * @param string|array $key * @param mixed $value * @return $this */ @@ -92,6 +141,11 @@ public function withViewData($key, $value = null): self return $this; } + /** + * Set the root view. + * + * @return $this + */ public function rootView(string $rootView): self { $this->rootView = $rootView; @@ -99,6 +153,12 @@ public function rootView(string $rootView): self return $this; } + /** + * Set the cache duration for the response. + * + * @param string|array $cacheFor + * @return $this + */ public function cache(string|array $cacheFor): self { $this->cacheFor = is_array($cacheFor) ? $cacheFor : [$cacheFor]; @@ -138,7 +198,10 @@ public function toResponse($request) } /** - * Resolve the properites for the response. + * Resolve the properties for the response. + * + * @param array $props + * @return array */ public function resolveProperties(Request $request, array $props): array { @@ -153,6 +216,9 @@ public function resolveProperties(Request $request, array $props): array /** * Resolve the ProvidesInertiaProperties props. + * + * @param array $props + * @return array */ public function resolveInertiaPropsProviders(array $props, Request $request): array { @@ -163,10 +229,9 @@ public function resolveInertiaPropsProviders(array $props, Request $request): ar foreach ($props as $key => $value) { if (is_numeric($key) && $value instanceof ProvidesInertiaProperties) { // Pipe into a Collection to leverage Collection::getArrayableItems() - $newProps = array_merge( - $newProps, - collect($value->toInertiaProperties($renderContext))->all() - ); + /** @var array $inertiaProps */ + $inertiaProps = collect($value->toInertiaProperties($renderContext))->all(); + $newProps = array_merge($newProps, $inertiaProps); } else { $newProps[$key] = $value; } @@ -176,7 +241,12 @@ public function resolveInertiaPropsProviders(array $props, Request $request): ar } /** - * Resolve the `only` and `except` partial request props. + * Resolve properties for partial requests. Filters properties based on + * 'only' and 'except' headers from the client, allowing for selective + * data loading to improve performance. + * + * @param array $props + * @return array */ public function resolvePartialProperties(array $props, Request $request): array { @@ -207,7 +277,12 @@ public function resolvePartialProperties(array $props, Request $request): array } /** - * Resolve all arrayables properties into an array. + * Resolve arrayable properties and closures. Converts Arrayable objects + * to arrays, evaluates closures, and handles dot notation properties + * for nested data structures. + * + * @param array $props + * @return array */ public function resolveArrayableProperties(array $props, Request $request, bool $unpackDotProps = true): array { @@ -237,6 +312,9 @@ public function resolveArrayableProperties(array $props, Request $request, bool /** * Resolve the `only` partial request props. + * + * @param array $props + * @return array */ public function resolveOnly(Request $request, array $props): array { @@ -253,6 +331,9 @@ public function resolveOnly(Request $request, array $props): array /** * Resolve the `except` partial request props. + * + * @param array $props + * @return array */ public function resolveExcept(Request $request, array $props): array { @@ -264,7 +345,10 @@ public function resolveExcept(Request $request, array $props): array } /** - * Resolve `always` properties that should always be included on all visits, regardless of "only" or "except" requests. + * Resolve `always` properties that should always be included. + * + * @param array $props + * @return array */ public function resolveAlways(array $props): array { @@ -280,6 +364,9 @@ public function resolveAlways(array $props): array /** * Resolve all necessary class instances in the given props. + * + * @param array $props + * @return array */ public function resolvePropertyInstances(array $props, Request $request, ?string $parentKey = null): array { @@ -331,6 +418,8 @@ public function resolvePropertyInstances(array $props, Request $request, ?string /** * Resolve the cache directions for the response. + * + * @return array */ public function resolveCacheDirections(Request $request): array { @@ -349,6 +438,11 @@ public function resolveCacheDirections(Request $request): array ]; } + /** + * Resolve merge props configuration for client-side prop merging. + * + * @return array + */ public function resolveMergeProps(Request $request): array { $resetProps = array_filter(explode(',', $request->header(Header::RESET, ''))); @@ -367,7 +461,7 @@ public function resolveMergeProps(Request $request): array ->keys(); $matchPropsOn = $mergeProps - ->map(function ($prop, $key) { + ->map(function (Mergeable $prop, $key) { return collect($prop->matchesOn()) ->map(fn ($strategy) => $key.'.'.$strategy) ->toArray(); @@ -386,6 +480,11 @@ public function resolveMergeProps(Request $request): array ], fn ($prop) => count($prop) > 0); } + /** + * Resolve deferred props configuration for client-side lazy loading. + * + * @return array + */ public function resolveDeferredProps(Request $request): array { if ($this->isPartial($request)) { @@ -418,7 +517,7 @@ public function isPartial(Request $request): bool } /** - * Get the URL from the request (without the scheme and host) while preserving the trailing slash if it exists. + * Get the URL from the request while preserving the trailing slash. */ protected function getUrl(Request $request): string { @@ -434,7 +533,7 @@ protected function getUrl(Request $request): string } /** - * Ensure the URL has a trailing slash before the query string (if it exists). + * Ensure the URL has a trailing slash before the query string. */ protected function finishUrlWithTrailingSlash(string $url): string { diff --git a/src/ResponseFactory.php b/src/ResponseFactory.php index 0278f24f..d24feac9 100644 --- a/src/ResponseFactory.php +++ b/src/ResponseFactory.php @@ -19,25 +19,52 @@ class ResponseFactory { use Macroable; - /** @var string */ + /** + * The name of the root view. + * + * @var string + */ protected $rootView = 'app'; - /** @var array */ + /** + * The shared properties. + * + * @var array + */ protected $sharedProps = []; - /** @var Closure|string|null */ + /** + * The asset version. + * + * @var Closure|string|null + */ protected $version; + /** + * Indicates if the browser history should be cleared. + * + * @var bool + */ protected $clearHistory = false; + /** + * Indicates if the browser history should be encrypted. + * + * @var bool|null + */ protected $encryptHistory; - /** @var Closure|null */ + /** + * The URL resolver callback. + * + * @var Closure|null + */ protected $urlResolver; - /*** - * @param string $name The name of the root view - * @return void + /** + * Set the root view template for Inertia responses. This template + * serves as the HTML wrapper that contains the Inertia root element + * where the frontend application will be mounted. */ public function setRootView(string $name): void { @@ -45,7 +72,11 @@ public function setRootView(string $name): void } /** - * @param string|array|Arrayable $key + * Share data across all Inertia responses. This data is automatically + * included with every response, making it ideal for user authentication + * state, flash messages, etc. + * + * @param string|array|\Illuminate\Contracts\Support\Arrayable|\Inertia\ProvidesInertiaProperties $key * @param mixed $value */ public function share($key, $value = null): void @@ -54,12 +85,18 @@ public function share($key, $value = null): void $this->sharedProps = array_merge($this->sharedProps, $key); } elseif ($key instanceof Arrayable) { $this->sharedProps = array_merge($this->sharedProps, $key->toArray()); + } elseif ($key instanceof ProvidesInertiaProperties) { + $this->sharedProps = array_merge($this->sharedProps, [$key]); } else { Arr::set($this->sharedProps, $key, $value); } } /** + * Get the shared data for a given key. Returns all shared data if + * no key is provided, or the value for a specific key with an + * optional default fallback. + * * @param mixed $default * @return mixed */ @@ -73,6 +110,8 @@ public function getShared(?string $key = null, $default = null) } /** + * Flush all shared data. + * * @return void */ public function flushShared() @@ -81,13 +120,18 @@ public function flushShared() } /** - * @param Closure|string|null $version + * Set the asset version. + * + * @param \Closure|string|null $version */ public function version($version): void { $this->version = $version; } + /** + * Get the asset version. + */ public function getVersion(): string { $version = $this->version instanceof Closure @@ -97,17 +141,25 @@ public function getVersion(): string return (string) $version; } + /** + * Set the URL resolver. + */ public function resolveUrlUsing(?Closure $urlResolver = null): void { $this->urlResolver = $urlResolver; } + /** + * Clear the browser history on the next visit. + */ public function clearHistory(): void { session(['inertia.clear_history' => true]); } /** + * Encrypt the browser history. + * * @param bool $encrypt */ public function encryptHistory($encrypt = true): void @@ -116,6 +168,8 @@ public function encryptHistory($encrypt = true): void } /** + * Create a lazy property. + * * @deprecated Use `optional` instead. */ public function lazy(callable $callback): LazyProp @@ -123,17 +177,25 @@ public function lazy(callable $callback): LazyProp return new LazyProp($callback); } + /** + * Create an optional property. + */ public function optional(callable $callback): OptionalProp { return new OptionalProp($callback); } + /** + * Create a deferred property. + */ public function defer(callable $callback, string $group = 'default'): DeferProp { return new DeferProp($callback, $group); } /** + * Create a merge property. + * * @param mixed $value */ public function merge($value): MergeProp @@ -142,6 +204,8 @@ public function merge($value): MergeProp } /** + * Create a deep merge property. + * * @param mixed $value */ public function deepMerge($value): MergeProp @@ -150,6 +214,8 @@ public function deepMerge($value): MergeProp } /** + * Create an always property. + * * @param mixed $value */ public function always($value): AlwaysProp @@ -158,7 +224,9 @@ public function always($value): AlwaysProp } /** - * @throws ComponentNotFoundException + * Find the component or fail. + * + * @throws \Inertia\ComponentNotFoundException */ protected function findComponentOrFail(string $component): void { @@ -170,7 +238,9 @@ protected function findComponentOrFail(string $component): void } /** - * @param array|Arrayable|ProvidesInertiaProperties $props + * Create an Inertia response. + * + * @param array|\Illuminate\Contracts\Support\Arrayable|ProvidesInertiaProperties $props */ public function render(string $component, $props = []): Response { @@ -196,7 +266,9 @@ public function render(string $component, $props = []): Response } /** - * @param string|SymfonyRedirect $url + * Create an Inertia location response. + * + * @param string|\Symfony\Component\HttpFoundation\RedirectResponse $url */ public function location($url): SymfonyResponse { diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 28cab930..920b602a 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -12,10 +12,12 @@ use Inertia\Support\Header; use Inertia\Testing\TestResponseMacros; use LogicException; -use ReflectionException; class ServiceProvider extends BaseServiceProvider { + /** + * Register the service provider. + */ public function register(): void { $this->app->singleton(ResponseFactory::class); @@ -49,6 +51,9 @@ public function register(): void }); } + /** + * Boot the service provider. + */ public function boot(): void { $this->registerConsoleCommands(); @@ -58,6 +63,10 @@ public function boot(): void ]); } + /** + * Register @inertia and @inertiaHead directives for rendering the Inertia + * root element and SSR head content in Blade templates. + */ protected function registerBladeDirectives(): void { $this->callAfterResolving('blade.compiler', function ($blade) { @@ -66,6 +75,10 @@ protected function registerBladeDirectives(): void }); } + /** + * Register Artisan commands for managing Inertia middleware creation + * and server-side rendering operations when running in console mode. + */ protected function registerConsoleCommands(): void { if (! $this->app->runningInConsole()) { @@ -80,6 +93,10 @@ protected function registerConsoleCommands(): void ]); } + /** + * Add an 'inertia' method to the Request class that returns true + * if the current request is an Inertia request. + */ protected function registerRequestMacro(): void { Request::macro('inertia', function () { @@ -87,8 +104,14 @@ protected function registerRequestMacro(): void }); } + /** + * Register the router macro. + */ protected function registerRouterMacro(): void { + /** + * @param array $props + */ Router::macro('inertia', function ($uri, $component, $props = []) { return $this->match(['GET', 'HEAD'], $uri, '\\'.Controller::class) ->defaults('component', $component) @@ -97,7 +120,9 @@ protected function registerRouterMacro(): void } /** - * @throws ReflectionException|LogicException + * Register the testing macros. + * + * @throws \LogicException */ protected function registerTestingMacros(): void { @@ -110,6 +135,9 @@ protected function registerTestingMacros(): void throw new LogicException('Could not detect TestResponse class.'); } + /** + * Register the middleware aliases. + */ protected function registerMiddleware(): void { $this->app['router']->aliasMiddleware( diff --git a/src/Ssr/BundleDetector.php b/src/Ssr/BundleDetector.php index 2d27e555..9fbe6a04 100644 --- a/src/Ssr/BundleDetector.php +++ b/src/Ssr/BundleDetector.php @@ -4,6 +4,11 @@ class BundleDetector { + /** + * Detect and return the path to the SSR bundle file. + * + * @return string|null + */ public function detect() { return collect([ diff --git a/src/Ssr/Gateway.php b/src/Ssr/Gateway.php index 31f69b5c..9ccb5132 100644 --- a/src/Ssr/Gateway.php +++ b/src/Ssr/Gateway.php @@ -5,7 +5,9 @@ interface Gateway { /** - * Dispatch the Inertia page to the Server Side Rendering engine. + * Dispatch the Inertia page to the SSR engine. + * + * @param array $page */ public function dispatch(array $page): ?Response; } diff --git a/src/Ssr/HasHealthCheck.php b/src/Ssr/HasHealthCheck.php index 2e3212b2..9da2c690 100644 --- a/src/Ssr/HasHealthCheck.php +++ b/src/Ssr/HasHealthCheck.php @@ -5,7 +5,7 @@ interface HasHealthCheck { /** - * Determine if the SSR server is healthy. + * Determine if the SSR server is healthy and responsive. */ public function isHealthy(): bool; } diff --git a/src/Ssr/HttpGateway.php b/src/Ssr/HttpGateway.php index db43ecaf..9cc1c1a9 100644 --- a/src/Ssr/HttpGateway.php +++ b/src/Ssr/HttpGateway.php @@ -10,7 +10,9 @@ class HttpGateway implements Gateway, HasHealthCheck { /** - * Dispatch the Inertia page to the Server Side Rendering engine. + * Dispatch the Inertia page to the SSR engine via HTTP. + * + * @param array $page */ public function dispatch(array $page): ?Response { @@ -63,7 +65,7 @@ public function isHealthy(): bool } /** - * Determine if dispatch should proceed even if no bundle is detected. + * Determine if dispatch should proceed without bundle detection. */ protected function shouldDispatchWithoutBundle(): bool { @@ -79,7 +81,7 @@ protected function bundleExists(): bool } /** - * Get the SSR URL from the configuration, ensuring it ends with '/{$path}'. + * Get the complete SSR URL by combining the base URL with the given path. */ public function getUrl(string $path): string { diff --git a/src/Ssr/Response.php b/src/Ssr/Response.php index 63cca461..103a8ec6 100644 --- a/src/Ssr/Response.php +++ b/src/Ssr/Response.php @@ -5,17 +5,21 @@ class Response { /** + * The HTML head content from server-side rendering. + * * @var string */ public $head; /** + * The HTML body content from server-side rendering. + * * @var string */ public $body; /** - * Prepare the Inertia Server Side Rendering (SSR) response. + * Create a new SSR response instance. */ public function __construct(string $head, string $body) { diff --git a/src/Support/Header.php b/src/Support/Header.php index 5091b67f..ca6d51fb 100644 --- a/src/Support/Header.php +++ b/src/Support/Header.php @@ -4,19 +4,43 @@ class Header { + /** + * The main Inertia request header. + */ public const INERTIA = 'X-Inertia'; + /** + * Header for specifying which error bag to use for validation errors. + */ public const ERROR_BAG = 'X-Inertia-Error-Bag'; + /** + * Header for external redirects. + */ public const LOCATION = 'X-Inertia-Location'; + /** + * Header for the current asset version. + */ public const VERSION = 'X-Inertia-Version'; + /** + * Header specifying the component for partial reloads. + */ public const PARTIAL_COMPONENT = 'X-Inertia-Partial-Component'; + /** + * Header specifying which props to include in partial reloads. + */ public const PARTIAL_ONLY = 'X-Inertia-Partial-Data'; + /** + * Header specifying which props to exclude from partial reloads. + */ public const PARTIAL_EXCEPT = 'X-Inertia-Partial-Except'; + /** + * Header for resetting the page state. + */ public const RESET = 'X-Inertia-Reset'; } diff --git a/src/Testing/AssertableInertia.php b/src/Testing/AssertableInertia.php index 1be7cd6a..87e05e2f 100644 --- a/src/Testing/AssertableInertia.php +++ b/src/Testing/AssertableInertia.php @@ -3,6 +3,7 @@ namespace Inertia\Testing; use Closure; +use Illuminate\Http\Response; use Illuminate\Testing\Fluent\AssertableJson; use Illuminate\Testing\TestResponse; use InvalidArgumentException; @@ -11,21 +12,46 @@ class AssertableInertia extends AssertableJson { - /** @var string */ + /** + * The Inertia component name for this page. + * + * @var string + */ private $component; - /** @var string */ + /** + * The current page URL. + * + * @var string + */ private $url; - /** @var string|null */ + /** + * The current asset version. + * + * @var string|null + */ private $version; - /** @var bool */ + /** + * Whether history state should be encrypted. + * + * @var bool + */ private $encryptHistory; - /** @var bool */ + /** + * Whether history should be cleared. + * + * @var bool + */ private $clearHistory; + /** + * Create an AssertableInertia instance from a test response. + * + * @param TestResponse $response + */ public static function fromTestResponse(TestResponse $response): self { try { @@ -53,6 +79,11 @@ public static function fromTestResponse(TestResponse $response): self return $instance; } + /** + * Assert that the page uses the given component. + * + * @param bool|null $shouldExist + */ public function component(?string $value = null, $shouldExist = null): self { PHPUnit::assertSame($value, $this->component, 'Unexpected Inertia page component.'); @@ -68,6 +99,9 @@ public function component(?string $value = null, $shouldExist = null): self return $this; } + /** + * Assert that the current page URL matches the expected value. + */ public function url(string $value): self { PHPUnit::assertSame($value, $this->url, 'Unexpected Inertia page url.'); @@ -75,6 +109,9 @@ public function url(string $value): self return $this; } + /** + * Assert that the current asset version matches the expected value. + */ public function version(string $value): self { PHPUnit::assertSame($value, $this->version, 'Unexpected Inertia asset version.'); @@ -84,6 +121,9 @@ public function version(string $value): self /** * Reload the Inertia page and perform assertions on the response. + * + * @param array|string|null $only + * @param array|string|null $except */ public function reload(?Closure $callback = null, array|string|null $only = null, array|string|null $except = null): self { @@ -119,6 +159,8 @@ public function reload(?Closure $callback = null, array|string|null $only = null /** * Reload the Inertia page as a partial request with only the specified props. + * + * @param array|string $only */ public function reloadOnly(array|string $only, ?Closure $callback = null): self { @@ -133,6 +175,8 @@ public function reloadOnly(array|string $only, ?Closure $callback = null): self /** * Reload the Inertia page as a partial request excluding the specified props. + * + * @param array|string $except */ public function reloadExcept(array|string $except, ?Closure $callback = null): self { @@ -145,6 +189,11 @@ public function reloadExcept(array|string $except, ?Closure $callback = null): s }); } + /** + * Convert the instance to an array. + * + * @return array + */ public function toArray() { return [ diff --git a/src/Testing/Concerns/Debugging.php b/src/Testing/Concerns/Debugging.php index 86a6269c..f1ecc611 100644 --- a/src/Testing/Concerns/Debugging.php +++ b/src/Testing/Concerns/Debugging.php @@ -2,6 +2,10 @@ namespace Inertia\Testing\Concerns; +/** + * @deprecated This trait is deprecated and will be removed in a future version. + * @see https://github.com/inertiajs/inertia-laravel/pull/338 + */ trait Debugging { public function dump(?string $prop = null): self diff --git a/src/Testing/Concerns/Has.php b/src/Testing/Concerns/Has.php index 37866b60..8d27f9f3 100644 --- a/src/Testing/Concerns/Has.php +++ b/src/Testing/Concerns/Has.php @@ -7,6 +7,10 @@ use Illuminate\Support\Collection; use PHPUnit\Framework\Assert as PHPUnit; +/** + * @deprecated This trait is deprecated and will be removed in a future version. + * @see https://github.com/inertiajs/inertia-laravel/pull/338 + */ trait Has { protected function count(string $key, int $length): self diff --git a/src/Testing/Concerns/Interaction.php b/src/Testing/Concerns/Interaction.php index 145d405e..eb97dcbc 100644 --- a/src/Testing/Concerns/Interaction.php +++ b/src/Testing/Concerns/Interaction.php @@ -5,6 +5,10 @@ use Illuminate\Support\Str; use PHPUnit\Framework\Assert as PHPUnit; +/** + * @deprecated This trait is deprecated and will be removed in a future version. + * @see https://github.com/inertiajs/inertia-laravel/pull/338 + */ trait Interaction { /** @var array */ diff --git a/src/Testing/Concerns/Matching.php b/src/Testing/Concerns/Matching.php index 7ead91ba..feb0f4fd 100644 --- a/src/Testing/Concerns/Matching.php +++ b/src/Testing/Concerns/Matching.php @@ -9,6 +9,10 @@ use Illuminate\Support\Collection; use PHPUnit\Framework\Assert as PHPUnit; +/** + * @deprecated This trait is deprecated and will be removed in a future version. + * @see https://github.com/inertiajs/inertia-laravel/pull/338 + */ trait Matching { public function whereAll(array $bindings): self diff --git a/src/Testing/Concerns/PageObject.php b/src/Testing/Concerns/PageObject.php index 109df780..cfc016b9 100644 --- a/src/Testing/Concerns/PageObject.php +++ b/src/Testing/Concerns/PageObject.php @@ -6,6 +6,10 @@ use InvalidArgumentException; use PHPUnit\Framework\Assert as PHPUnit; +/** + * @deprecated This trait is deprecated and will be removed in a future version. + * @see https://github.com/inertiajs/inertia-laravel/pull/338 + */ trait PageObject { public function component(?string $value = null, $shouldExist = null): self diff --git a/src/Testing/ReloadRequest.php b/src/Testing/ReloadRequest.php index 789962dd..cf692d3f 100644 --- a/src/Testing/ReloadRequest.php +++ b/src/Testing/ReloadRequest.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Testing\Concerns\MakesHttpRequests; +use Illuminate\Http\Response; use Illuminate\Testing\TestResponse; use Inertia\Support\Header; @@ -26,7 +27,9 @@ public function __construct( } /** - * Request the Inertia page as a partial reload. + * Execute the reload request with appropriate Inertia headers. + * + * @return TestResponse */ public function __invoke(): TestResponse { diff --git a/src/Testing/TestResponseMacros.php b/src/Testing/TestResponseMacros.php index cf4e5e29..207b59db 100644 --- a/src/Testing/TestResponseMacros.php +++ b/src/Testing/TestResponseMacros.php @@ -7,9 +7,15 @@ class TestResponseMacros { + /** + * Register the 'assertInertia' macro for TestResponse. + * + * @return Closure + */ public function assertInertia() { return function (?Closure $callback = null) { + /** @phpstan-ignore-next-line */ $assert = AssertableInertia::fromTestResponse($this); if (is_null($callback)) { @@ -22,17 +28,31 @@ public function assertInertia() }; } + /** + * Register the 'inertiaPage' macro for TestResponse. + * + * @return Closure + */ public function inertiaPage() { return function () { + /** @phpstan-ignore-next-line */ return AssertableInertia::fromTestResponse($this)->toArray(); }; } + /** + * Register the 'inertiaProps' macro for TestResponse. + * + * @return Closure + */ public function inertiaProps() { return function (?string $propName = null) { - return Arr::get($this->inertiaPage()['props'], $propName); + /** @phpstan-ignore-next-line */ + $page = AssertableInertia::fromTestResponse($this)->toArray(); + + return Arr::get($page['props'], $propName); }; } } diff --git a/tests/AlwaysPropTest.php b/tests/AlwaysPropTest.php index df1e41a8..8949bbc4 100644 --- a/tests/AlwaysPropTest.php +++ b/tests/AlwaysPropTest.php @@ -27,7 +27,7 @@ public function test_can_accept_callables(): void { $callable = new class { - public function __invoke() + public function __invoke(): string { return 'An always value'; } diff --git a/tests/Commands/CheckSsrTest.php b/tests/Commands/CheckSsrTest.php index cbf426c5..529e5222 100644 --- a/tests/Commands/CheckSsrTest.php +++ b/tests/Commands/CheckSsrTest.php @@ -7,7 +7,7 @@ class CheckSsrTest extends TestCase { - public function test_success_on_healthy_ssr_server() + public function test_success_on_healthy_ssr_server(): void { $this->mock(HttpGateway::class, fn ($mock) => $mock ->shouldReceive('isHealthy') @@ -20,7 +20,7 @@ public function test_success_on_healthy_ssr_server() ->assertExitCode(0); } - public function test_failure_on_unhealthy_ssr_server() + public function test_failure_on_unhealthy_ssr_server(): void { $this->mock(HttpGateway::class, fn ($mock) => $mock ->shouldReceive('isHealthy') @@ -33,7 +33,7 @@ public function test_failure_on_unhealthy_ssr_server() ->assertExitCode(1); } - public function test_failure_on_unsupported_gateway() + public function test_failure_on_unsupported_gateway(): void { $this->mock(Gateway::class); diff --git a/tests/DirectiveTest.php b/tests/DirectiveTest.php index 356f2348..263dac1a 100644 --- a/tests/DirectiveTest.php +++ b/tests/DirectiveTest.php @@ -30,7 +30,9 @@ protected function setUp(): void $this->app->bind(Gateway::class, FakeGateway::class); $this->filesystem = m::mock(Filesystem::class); - $this->compiler = new BladeCompiler($this->filesystem, __DIR__.'/cache/views'); + /** @var Filesystem $filesystem */ + $filesystem = $this->filesystem; + $this->compiler = new BladeCompiler($filesystem, __DIR__.'/cache/views'); $this->compiler->directive('inertia', [Directive::class, 'compile']); $this->compiler->directive('inertiaHead', [Directive::class, 'compileHead']); } @@ -41,7 +43,10 @@ protected function tearDown(): void parent::tearDown(); } - protected function renderView($contents, $data = []) + /** + * @param array $data + */ + protected function renderView(string $contents, array $data = []): string { return Blade::render($contents, $data, true); } diff --git a/tests/HttpGatewayTest.php b/tests/HttpGatewayTest.php index c34236c4..fc848868 100644 --- a/tests/HttpGatewayTest.php +++ b/tests/HttpGatewayTest.php @@ -21,7 +21,7 @@ protected function setUp(): void Http::preventStrayRequests(); } - public function test_it_returns_null_when_ssr_is_disabled() + public function test_it_returns_null_when_ssr_is_disabled(): void { config([ 'inertia.ssr.enabled' => false, @@ -31,7 +31,7 @@ public function test_it_returns_null_when_ssr_is_disabled() $this->assertNull($this->gateway->dispatch(['page' => self::EXAMPLE_PAGE_OBJECT])); } - public function test_it_returns_null_when_no_bundle_file_is_detected() + public function test_it_returns_null_when_no_bundle_file_is_detected(): void { config([ 'inertia.ssr.enabled' => true, @@ -41,7 +41,7 @@ public function test_it_returns_null_when_no_bundle_file_is_detected() $this->assertNull($this->gateway->dispatch(['page' => self::EXAMPLE_PAGE_OBJECT])); } - public function test_it_uses_the_configured_http_url_when_the_bundle_file_is_detected() + public function test_it_uses_the_configured_http_url_when_the_bundle_file_is_detected(): void { config([ 'inertia.ssr.enabled' => true, @@ -63,7 +63,7 @@ public function test_it_uses_the_configured_http_url_when_the_bundle_file_is_det $this->assertEquals('
SSR Response
', $response->body); } - public function test_it_uses_the_configured_http_url_when_bundle_file_detection_is_disabled() + public function test_it_uses_the_configured_http_url_when_bundle_file_detection_is_disabled(): void { config([ 'inertia.ssr.enabled' => true, @@ -86,7 +86,7 @@ public function test_it_uses_the_configured_http_url_when_bundle_file_detection_ $this->assertEquals('
SSR Response
', $response->body); } - public function test_it_returns_null_when_the_http_request_fails() + public function test_it_returns_null_when_the_http_request_fails(): void { config([ 'inertia.ssr.enabled' => true, @@ -100,7 +100,7 @@ public function test_it_returns_null_when_the_http_request_fails() $this->assertNull($this->gateway->dispatch(['page' => self::EXAMPLE_PAGE_OBJECT])); } - public function test_it_returns_null_when_invalid_json_is_returned() + public function test_it_returns_null_when_invalid_json_is_returned(): void { config([ 'inertia.ssr.enabled' => true, @@ -114,7 +114,7 @@ public function test_it_returns_null_when_invalid_json_is_returned() $this->assertNull($this->gateway->dispatch(['page' => self::EXAMPLE_PAGE_OBJECT])); } - public function test_health_check_the_ssr_server() + public function test_health_check_the_ssr_server(): void { Http::fake([ $this->gateway->getUrl('health') => Http::sequence() diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index 75102a66..fc143f75 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -4,6 +4,7 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Http\Request; +use Illuminate\Routing\Route as RouteInstance; use Illuminate\Session\Middleware\StartSession; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Session; @@ -131,7 +132,7 @@ public function test_it_will_instruct_inertia_to_reload_on_a_version_mismatch(): self::assertEmpty($response->getContent()); } - public function test_the_url_can_be_resolved_with_a_custom_resolver() + public function test_the_url_can_be_resolved_with_a_custom_resolver(): void { $this->prepareMockEndpoint(middleware: new CustomUrlResolverMiddleware); @@ -308,7 +309,10 @@ public function test_determine_the_version_by_a_hash_of_the_mix_manifest(): void $response->assertViewHas('page.version', hash('xxh128', $contents)); } - private function prepareMockEndpoint($version = null, $shared = [], $middleware = null): \Illuminate\Routing\Route + /** + * @param array $shared + */ + private function prepareMockEndpoint(int|string|null $version = null, array $shared = [], ?Middleware $middleware = null): RouteInstance { if (is_null($middleware)) { $middleware = new ExampleMiddleware($version, $shared); diff --git a/tests/ResponseFactoryTest.php b/tests/ResponseFactoryTest.php index acf9de8c..49b08837 100644 --- a/tests/ResponseFactoryTest.php +++ b/tests/ResponseFactoryTest.php @@ -18,9 +18,8 @@ use Inertia\LazyProp; use Inertia\MergeProp; use Inertia\OptionalProp; -use Inertia\ProvidesInertiaProperties; -use Inertia\RenderContext; use Inertia\ResponseFactory; +use Inertia\Tests\Stubs\ExampleInertiaPropsProvider; use Inertia\Tests\Stubs\ExampleMiddleware; class ResponseFactoryTest extends TestCase @@ -32,6 +31,7 @@ public function test_can_macro(): void return 'bar'; }); + /** @phpstan-ignore-next-line */ $this->assertEquals('bar', $factory->foo()); } @@ -130,7 +130,7 @@ public function test_the_version_can_be_a_closure(): void $response->assertJson(['component' => 'User/Edit']); } - public function test_the_url_can_be_resolved_with_a_custom_resolver() + public function test_the_url_can_be_resolved_with_a_custom_resolver(): void { Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { Inertia::resolveUrlUsing(function ($request, ResponseFactory $otherDependency) { @@ -354,7 +354,7 @@ public function test_can_create_always_prop(): void $this->assertInstanceOf(AlwaysProp::class, $alwaysProp); } - public function test_will_accept_arrayabe_props() + public function test_will_accept_arrayabe_props(): void { Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { Inertia::share('foo', 'bar'); @@ -380,18 +380,12 @@ public function toArray() ]); } - public function test_will_accept_instances_of_provides_inertia_props() + public function test_will_accept_instances_of_provides_inertia_props(): void { Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { - return Inertia::render('User/Edit', new class implements ProvidesInertiaProperties - { - public function toInertiaProperties(RenderContext $context): iterable - { - return [ - 'foo' => 'bar', - ]; - } - }); + return Inertia::render('User/Edit', new ExampleInertiaPropsProvider([ + 'foo' => 'bar', + ])); }); $response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']); @@ -404,6 +398,80 @@ public function toInertiaProperties(RenderContext $context): iterable ]); } + public function test_will_accept_arrays_containing_provides_inertia_props_in_render(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + return Inertia::render('User/Edit', [ + 'regular' => 'prop', + new ExampleInertiaPropsProvider([ + 'from_object' => 'value', + ]), + 'another' => 'normal_prop', + ]); + }); + + $response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']); + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'props' => [ + 'regular' => 'prop', + 'from_object' => 'value', + 'another' => 'normal_prop', + ], + ]); + } + + public function test_can_share_instances_of_provides_inertia_props(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::share(new ExampleInertiaPropsProvider([ + 'shared' => 'data', + ])); + + return Inertia::render('User/Edit', [ + 'regular' => 'prop', + ]); + }); + + $response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']); + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'props' => [ + 'shared' => 'data', + 'regular' => 'prop', + ], + ]); + } + + public function test_can_share_arrays_containing_provides_inertia_props(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::share([ + 'regular' => 'shared_prop', + new ExampleInertiaPropsProvider([ + 'from_object' => 'shared_value', + ]), + ]); + + return Inertia::render('User/Edit', [ + 'component' => 'prop', + ]); + }); + + $response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']); + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'props' => [ + 'regular' => 'shared_prop', + 'from_object' => 'shared_value', + 'component' => 'prop', + ], + ]); + } + public function test_will_throw_exception_if_component_does_not_exist_when_ensuring_is_enabled(): void { config()->set('inertia.ensure_pages_exist', true); diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 7db3af05..b5a78722 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -5,7 +5,6 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\ResourceCollection; use Illuminate\Http\Response as BaseResponse; use Illuminate\Pagination\LengthAwarePaginator; @@ -33,6 +32,7 @@ public function test_can_macro(): void return 'bar'; }); + /** @phpstan-ignore-next-line */ $this->assertEquals('bar', $response->foo()); } @@ -42,6 +42,7 @@ public function test_server_response(): void $user = ['name' => 'Jonathan']; $response = new Response('User/Edit', ['user' => $user], 'app', '123'); + /** @var BaseResponse $response */ $response = $response->toResponse($request); $view = $response->getOriginalContent(); $page = $view->getData()['page']; @@ -75,6 +76,7 @@ public function test_server_response_with_deferred_prop(): void '123' ); $response = $response->toResponse($request); + /** @var BaseResponse $response */ $view = $response->getOriginalContent(); $page = $view->getData()['page']; @@ -116,6 +118,7 @@ public function test_server_response_with_deferred_prop_and_multiple_groups(): v '123' ); $response = $response->toResponse($request); + /** @var BaseResponse $response */ $view = $response->getOriginalContent(); $page = $view->getData()['page']; @@ -151,6 +154,7 @@ public function test_server_response_with_merge_props(): void '123' ); $response = $response->toResponse($request); + /** @var BaseResponse $response */ $view = $response->getOriginalContent(); $page = $view->getData()['page']; @@ -186,6 +190,7 @@ public function test_server_response_with_deep_merge_props(): void '123' ); $response = $response->toResponse($request); + /** @var BaseResponse $response */ $view = $response->getOriginalContent(); $page = $view->getData()['page']; @@ -221,6 +226,7 @@ public function test_server_response_with_merge_strategies(): void '123' ); $response = $response->toResponse($request); + /** @var BaseResponse $response */ $view = $response->getOriginalContent(); $page = $view->getData()['page']; @@ -263,6 +269,7 @@ public function test_server_response_with_defer_and_merge_props(): void '123' ); $response = $response->toResponse($request); + /** @var BaseResponse $response */ $view = $response->getOriginalContent(); $page = $view->getData()['page']; @@ -303,6 +310,7 @@ public function test_server_response_with_defer_and_deep_merge_props(): void '123' ); $response = $response->toResponse($request); + /** @var BaseResponse $response */ $view = $response->getOriginalContent(); $page = $view->getData()['page']; @@ -343,6 +351,7 @@ public function test_exclude_merge_props_from_partial_only_response(): void 'app', '123' ); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -374,6 +383,7 @@ public function test_exclude_merge_props_from_partial_except_response(): void 'app', '123' ); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -394,6 +404,7 @@ public function test_xhr_response(): void $user = (object) ['name' => 'Jonathan']; $response = new Response('User/Edit', ['user' => $user], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -412,6 +423,7 @@ public function test_resource_response(): void $resource = new FakeResource(['name' => 'Jonathan']); $response = new Response('User/Edit', ['user' => $resource], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -431,6 +443,7 @@ public function test_lazy_callable_resource_response(): void 'users' => fn () => [['name' => 'Jonathan']], 'organizations' => fn () => [['name' => 'Inertia']], ], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -457,6 +470,7 @@ public function test_lazy_callable_resource_partial_response(): void 'users' => fn () => [['name' => 'Jonathan']], 'organizations' => fn () => [['name' => 'Inertia']], ], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -484,10 +498,11 @@ public function test_lazy_resource_response(): void $callable = static function () use ($users) { $page = new LengthAwarePaginator($users->take(2), $users->count(), 2); - return new class($page, JsonResource::class) extends ResourceCollection {}; + return new class($page) extends ResourceCollection {}; }; $response = new Response('User/Index', ['users' => $callable], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -537,11 +552,12 @@ public function test_nested_lazy_resource_response(): void // nested array with ResourceCollection to resolve return [ - 'users' => new class($page, JsonResource::class) extends ResourceCollection {}, + 'users' => new class($page) extends ResourceCollection {}, ]; }; $response = new Response('User/Index', ['something' => $callable], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -585,6 +601,7 @@ public function test_arrayable_prop_response(): void $resource = FakeResource::make(['name' => 'Jonathan']); $response = new Response('User/Edit', ['user' => $resource], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -605,9 +622,10 @@ public function test_promise_props_are_resolved(): void $promise = Mockery::mock('GuzzleHttp\Promise\PromiseInterface') ->shouldReceive('wait') ->andReturn($user) - ->mock(); + ->getMock(); $response = new Response('User/Edit', ['user' => $promise], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -627,6 +645,7 @@ public function test_xhr_partial_response(): void $user = (object) ['name' => 'Jonathan']; $response = new Response('User/Edit', ['user' => $user, 'partial' => 'partial-data'], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -650,6 +669,7 @@ public function test_exclude_props_from_partial_response(): void $user = (object) ['name' => 'Jonathan']; $response = new Response('User/Edit', ['user' => $user, 'partial' => 'partial-data'], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -688,6 +708,7 @@ public function test_nested_partial_props(): void ]; $response = new Response('User/Edit', $props); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -722,6 +743,7 @@ public function test_exclude_nested_props_from_partial_response(): void ]; $response = new Response('User/Edit', $props); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -740,6 +762,7 @@ public function test_lazy_props_are_not_included_by_default(): void }); $response = new Response('Users', ['users' => [], 'lazy' => $lazyProp], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -759,6 +782,7 @@ public function test_lazy_props_are_included_in_partial_reload(): void }); $response = new Response('Users', ['users' => [], 'lazy' => $lazyProp], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -784,6 +808,7 @@ public function toArray() }); $response = new Response('Users', ['users' => [], 'defer' => $deferProp], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -816,6 +841,7 @@ public function test_always_props_are_included_on_partial_reload(): void ]; $response = new Response('User/Edit', $props, 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -832,6 +858,9 @@ public function test_inertia_responsable_objects(): void 'foo' => 'bar', new class implements ProvidesInertiaProperties { + /** + * @return \Illuminate\Support\Collection + */ public function toInertiaProperties(RenderContext $context): iterable { return collect([ @@ -842,6 +871,7 @@ public function toInertiaProperties(RenderContext $context): iterable 'quux' => 'corge', ], 'app', '123'); + /** @var BaseResponse $response */ $response = $response->toResponse($request); $view = $response->getOriginalContent(); $page = $view->getData()['page']; @@ -866,6 +896,7 @@ public function test_inertia_response_type_prop(): void ], ], ], 'app', '123'); + /** @var BaseResponse $response */ $response = $response->toResponse($request); $view = $response->getOriginalContent(); $page = $view->getData()['page']; @@ -892,6 +923,7 @@ public function test_top_level_dot_props_get_unpacked(): void $request->headers->add(['X-Inertia' => 'true']); $response = new Response('User/Edit', $props, 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(true); @@ -919,6 +951,7 @@ public function test_nested_dot_props_do_not_get_unpacked(): void $request->headers->add(['X-Inertia' => 'true']); $response = new Response('User/Edit', $props, 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(true); @@ -937,12 +970,16 @@ public function test_props_can_be_added_using_the_with_method(): void ->with(['quux' => 'corge']) ->with(new class implements ProvidesInertiaProperties { + /** + * @return \Illuminate\Support\Collection + */ public function toInertiaProperties(RenderContext $context): iterable { return collect(['grault' => 'garply']); } }); + /** @var BaseResponse $response */ $response = $response->toResponse($request); $view = $response->getOriginalContent(); $page = $view->getData()['page']; @@ -960,6 +997,7 @@ public function test_responsable_with_invalid_key(): void $resource = new FakeResource(["\x00*\x00_invalid_key" => 'for object']); $response = new Response('User/Edit', ['resource' => $resource], 'app', '123'); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(true); @@ -979,6 +1017,7 @@ public function test_the_page_url_is_prefixed_with_the_proxy_prefix(): void $user = ['name' => 'Jonathan']; $response = new Response('User/Edit', ['user' => $user], 'app', '123'); + /** @var BaseResponse $response */ $response = $response->toResponse($request); $view = $response->getOriginalContent(); $page = $view->getData()['page']; @@ -998,6 +1037,7 @@ public function test_the_page_url_doesnt_double_up(): void $request->headers->add(['X-Inertia' => 'true']); $response = new Response('Product/Show', []); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -1010,6 +1050,7 @@ public function test_trailing_slashes_in_a_url_are_preserved(): void $request->headers->add(['X-Inertia' => 'true']); $response = new Response('User/Index', []); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -1022,6 +1063,7 @@ public function test_trailing_slashes_in_a_url_with_query_parameters_are_preserv $request->headers->add(['X-Inertia' => 'true']); $response = new Response('User/Index', []); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -1034,6 +1076,7 @@ public function test_a_url_without_trailing_slash_is_resolved_correctly(): void $request->headers->add(['X-Inertia' => 'true']); $response = new Response('User/Index', []); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); @@ -1046,6 +1089,7 @@ public function test_a_url_without_trailing_slash_and_query_parameters_is_resolv $request->headers->add(['X-Inertia' => 'true']); $response = new Response('User/Index', []); + /** @var JsonResponse $response */ $response = $response->toResponse($request); $page = $response->getData(); diff --git a/tests/Stubs/ExampleInertiaPropsProvider.php b/tests/Stubs/ExampleInertiaPropsProvider.php new file mode 100644 index 00000000..abd1854a --- /dev/null +++ b/tests/Stubs/ExampleInertiaPropsProvider.php @@ -0,0 +1,24 @@ + $properties + */ + public function __construct( + protected array $properties + ) {} + + /** + * @return array + */ + public function toInertiaProperties(RenderContext $context): iterable + { + return $this->properties; + } +} diff --git a/tests/Stubs/ExampleMiddleware.php b/tests/Stubs/ExampleMiddleware.php index bb5e5305..95ebb8b9 100644 --- a/tests/Stubs/ExampleMiddleware.php +++ b/tests/Stubs/ExampleMiddleware.php @@ -19,7 +19,10 @@ class ExampleMiddleware extends Middleware */ protected $shared = []; - public function __construct($version = null, $shared = []) + /** + * @param array $shared + */ + public function __construct(mixed $version = null, array $shared = []) { $this->version = $version; $this->shared = $shared; diff --git a/tests/Stubs/FakeResource.php b/tests/Stubs/FakeResource.php index a80c216a..19d804c5 100644 --- a/tests/Stubs/FakeResource.php +++ b/tests/Stubs/FakeResource.php @@ -9,9 +9,9 @@ class FakeResource extends JsonResource /** * The data that will be used. * - * @var array + * @var array */ - private $data; + private array $data; /** * The "data" wrapper that should be applied. @@ -20,6 +20,9 @@ class FakeResource extends JsonResource */ public static $wrap = null; + /** + * @param array $resource + */ public function __construct(array $resource) { parent::__construct(null); @@ -30,6 +33,7 @@ public function __construct(array $resource) * Transform the resource into an array. * * @param \Illuminate\Http\Request $request + * @return array */ public function toArray($request): array { diff --git a/tests/Stubs/MergeWithSharedProp.php b/tests/Stubs/MergeWithSharedProp.php index a4121b92..426474e3 100644 --- a/tests/Stubs/MergeWithSharedProp.php +++ b/tests/Stubs/MergeWithSharedProp.php @@ -8,6 +8,9 @@ class MergeWithSharedProp implements ProvidesInertiaProperty { + /** + * @param array $items + */ public function __construct(protected array $items = []) {} public function toInertiaProperty(PropertyContext $prop): mixed diff --git a/tests/TestCase.php b/tests/TestCase.php index a4e286a7..f6504baf 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,6 +2,7 @@ namespace Inertia\Tests; +use Illuminate\Http\Response; use Illuminate\Support\Facades\View; use Illuminate\Testing\TestResponse; use Inertia\Inertia; @@ -33,7 +34,10 @@ protected function setUp(): void config()->set('inertia.testing.page_paths', [realpath(__DIR__)]); } - protected function makeMockRequest($view): TestResponse + /** + * @return TestResponse + */ + protected function makeMockRequest(mixed $view): TestResponse { app('router')->get('/example-url', function () use ($view) { return is_callable($view) ? $view() : $view;