Skip to content
Merged
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
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
"suggest": {
"ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command."
},
"scripts": {
"test": "phpunit",
"lint": "pint",
"analyze": "phpstan analyze --memory-limit=256M"
},
"extra": {
"laravel": {
"providers": [
Expand Down
28 changes: 28 additions & 0 deletions src/Mergeable.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,32 @@ public function shouldDeepMerge();
* @return array<int, string>
*/
public function matchesOn();

/**
* Determine if the property should be appended at the root level.
*
* @return bool
*/
public function appendsAtRoot();

/**
* Determine if the property should be prepended at the root level.
*
* @return bool
*/
public function prependsAtRoot();

/**
* Get the paths to append when merging.
*
* @return array<int, string>
*/
public function appendsAtPaths(): array;

/**
* Get the paths to prepend when merging.
*
* @return array<int, string>
*/
public function prependsAtPaths(): array;
}
111 changes: 111 additions & 0 deletions src/MergesProps.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ trait MergesProps
*/
protected array $matchOn = [];

/**
* Indicates if the property values should be appended or prepended.
*/
protected bool $append = true;

/**
* The paths to append.
*
* @var array<int, string>
*/
protected array $appendsAtPaths = [];

/**
* The paths to prepend.
*
* @var array<int, string>
*/
protected array $prependsAtPaths = [];

/**
* Mark the property for merging.
*/
Expand Down Expand Up @@ -80,4 +99,96 @@ public function matchesOn(): array
{
return $this->matchOn;
}

/**
* Determine if the property should be appended at the root level.
*
* @return bool
*/
public function appendsAtRoot()
{
return $this->append && $this->mergesAtRoot();
}

/**
* Determine if the property should be prepended at the root level.
*
* @return bool
*/
public function prependsAtRoot()
{
return ! $this->append && $this->mergesAtRoot();
}

/**
* Determine if the property merges at the root level.
*/
protected function mergesAtRoot(): bool
{
return count($this->appendsAtPaths) === 0 && count($this->prependsAtPaths) === 0;
}

/**
* Specify that the value should be appended, optionally providing a key to append and a property to match on.
*
* @param bool|string|array<array-key, string> $path
*/
public function append(bool|string|array $path = true, ?string $matchOn = null): static
{
match (true) {
is_bool($path) => $this->append = $path,
is_string($path) => $this->appendsAtPaths[] = $path,
is_array($path) => collect($path)->each(
fn ($value, $key) => is_numeric($key) ? $this->append($value) : $this->append($key, $value)
),
};

if (is_string($path) && $matchOn) {
$this->matchOn([...$this->matchOn, "{$path}.{$matchOn}"]);
}

return $this;
}

/**
* Specify that the value should be prepended, optionally providing a key to prepend and a property to match on.
*
* @param bool|string|array<array-key, string> $path
*/
public function prepend(bool|string|array $path = true, ?string $matchOn = null): static
{
match (true) {
is_bool($path) => $this->append = ! $path,
is_string($path) => $this->prependsAtPaths[] = $path,
is_array($path) => collect($path)->each(
fn ($value, $key) => is_numeric($key) ? $this->prepend($value) : $this->prepend($key, $value)
),
};

if (is_string($path) && $matchOn) {
$this->matchOn([...$this->matchOn, "{$path}.{$matchOn}"]);
}

return $this;
}

/**
* Get the paths to append.
*
* @return array<int, string>
*/
public function appendsAtPaths(): array
{
return $this->appendsAtPaths;
}

/**
* Get the paths to prepend.
*
* @return array<int, string>
*/
public function prependsAtPaths(): array
{
return $this->prependsAtPaths;
}
}
26 changes: 26 additions & 0 deletions src/ProvidesScrollMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Inertia;

interface ProvidesScrollMetadata
{
/**
* Get the page name parameter.
*/
public function getPageName(): string;

/**
* Get the previous page identifier.
*/
public function getPreviousPage(): int|string|null;

/**
* Get the next page identifier.
*/
public function getNextPage(): int|string|null;

/**
* Get the current page identifier.
*/
public function getCurrentPage(): int|string|null;
}
134 changes: 111 additions & 23 deletions src/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Response as ResponseFactory;
use Illuminate\Support\Str;
Expand Down Expand Up @@ -188,6 +189,7 @@ public function toResponse($request)
$this->resolveMergeProps($request),
$this->resolveDeferredProps($request),
$this->resolveCacheDirections($request),
$this->resolveScrollProps($request),
);

if ($request->header(Header::INERTIA)) {
Expand Down Expand Up @@ -371,13 +373,18 @@ public function resolveAlways(array $props): array
public function resolvePropertyInstances(array $props, Request $request, ?string $parentKey = null): array
{
foreach ($props as $key => $value) {
if ($value instanceof ScrollProp) {
$value->configureMergeIntent($request);
}

$resolveViaApp = collect([
Closure::class,
LazyProp::class,
OptionalProp::class,
DeferProp::class,
AlwaysProp::class,
MergeProp::class,
ScrollProp::class,
])->first(fn ($class) => $value instanceof $class);

if ($resolveViaApp) {
Expand Down Expand Up @@ -439,45 +446,112 @@ public function resolveCacheDirections(Request $request): array
}

/**
* Resolve merge props configuration for client-side prop merging.
* Get the props that should be considered for merging based on the request headers.
*
* @return array<string, mixed>
* @return \Illuminate\Support\Collection<string, \Inertia\Mergeable>
*/
public function resolveMergeProps(Request $request): array
protected function getMergePropsForRequest(Request $request): Collection
{
$resetProps = array_filter(explode(',', $request->header(Header::RESET, '')));
$onlyProps = array_filter(explode(',', $request->header(Header::PARTIAL_ONLY, '')));
$exceptProps = array_filter(explode(',', $request->header(Header::PARTIAL_EXCEPT, '')));

$mergeProps = collect($this->props)
return collect($this->props)
->filter(fn ($prop) => $prop instanceof Mergeable)
->filter(fn ($prop) => $prop->shouldMerge())
->reject(fn ($_, $key) => in_array($key, $resetProps))
->filter(fn ($_, $key) => count($onlyProps) === 0 || in_array($key, $onlyProps))
->reject(fn ($_, $key) => in_array($key, $exceptProps));
->filter(fn (Mergeable $prop) => $prop->shouldMerge())
->reject(fn ($_, string $key) => in_array($key, $resetProps))
->filter(fn ($_, string $key) => count($onlyProps) === 0 || in_array($key, $onlyProps))
->reject(fn ($_, string $key) => in_array($key, $exceptProps));
}

/**
* Resolve merge props configuration for client-side prop merging.
*
* @return array<string, mixed>
*/
public function resolveMergeProps(Request $request): array
{
$mergeProps = $this->getMergePropsForRequest($request);

$deepMergeProps = $mergeProps
->filter(fn ($prop) => $prop->shouldDeepMerge())
->keys();
return array_filter([
'mergeProps' => $this->resolveAppendMergeProps($mergeProps),
'prependProps' => $this->resolvePrependMergeProps($mergeProps),
'deepMergeProps' => $this->resolveDeepMergeProps($mergeProps),
'matchPropsOn' => $this->resolveMergeMatchingKeys($mergeProps),
], fn ($prop) => count($prop) > 0);
}

$matchPropsOn = $mergeProps
/**
* Resolve props that should be appended during merging.
*
* @param \Illuminate\Support\Collection<string, \Inertia\Mergeable> $mergeProps
* @return array<int, string>
*/
protected function resolveAppendMergeProps(Collection $mergeProps): array
{
[$rootAppendProps, $nestedAppendProps] = $mergeProps
->reject(fn (Mergeable $prop) => $prop->shouldDeepMerge())
->partition(fn (Mergeable $prop) => $prop->appendsAtRoot());

return $nestedAppendProps
->flatMap(fn (Mergeable $prop, string $key) => collect($prop->appendsAtPaths())->map(fn ($path) => $key.'.'.$path))
->merge($rootAppendProps->keys())
->unique()
->values()
->toArray();
}

/**
* Resolve props that should be prepended during merging.
*
* @param \Illuminate\Support\Collection<string, \Inertia\Mergeable> $mergeProps
* @return array<int, string>
*/
protected function resolvePrependMergeProps(Collection $mergeProps): array
{
[$rootPrependProps, $nestedPrependProps] = $mergeProps
->reject(fn (Mergeable $prop) => $prop->shouldDeepMerge())
->partition(fn (Mergeable $prop) => $prop->prependsAtRoot());

return $nestedPrependProps
->flatMap(fn (Mergeable $prop, string $key) => collect($prop->prependsAtPaths())->map(fn ($path) => $key.'.'.$path))
->merge($rootPrependProps->keys())
->unique()
->values()
->toArray();
}

/**
* Resolve props that should be deep merged.
*
* @param \Illuminate\Support\Collection<string, \Inertia\Mergeable> $mergeProps
* @return array<int, string>
*/
protected function resolveDeepMergeProps(Collection $mergeProps): array
{
return $mergeProps
->filter(fn (Mergeable $prop) => $prop->shouldDeepMerge())
->keys()
->toArray();
}

/**
* Resolve the matching keys for merge props.
*
* @param \Illuminate\Support\Collection<string, \Inertia\Mergeable> $mergeProps
* @return array<int, string>
*/
protected function resolveMergeMatchingKeys(Collection $mergeProps): array
{
return $mergeProps
->map(function (Mergeable $prop, $key) {
return collect($prop->matchesOn())
->map(fn ($strategy) => $key.'.'.$strategy)
->toArray();
})
->flatten()
->values();

$mergeProps = $mergeProps
->filter(fn ($prop) => ! $prop->shouldDeepMerge())
->keys();

return array_filter([
'mergeProps' => $mergeProps->toArray(),
'deepMergeProps' => $deepMergeProps->toArray(),
'matchPropsOn' => $matchPropsOn->toArray(),
], fn ($prop) => count($prop) > 0);
->values()
->toArray();
}

/**
Expand Down Expand Up @@ -508,6 +582,20 @@ public function resolveDeferredProps(Request $request): array
return $deferredProps->isNotEmpty() ? ['deferredProps' => $deferredProps->toArray()] : [];
}

/**
* Resolve scroll props configuration for client-side infinite scrolling.
*
* @return array<string, mixed>
*/
public function resolveScrollProps(Request $request): array
{
$scrollProps = $this->getMergePropsForRequest($request)
->filter(fn (Mergeable $prop) => $prop instanceof ScrollProp)
->mapWithKeys(fn (ScrollProp $prop, string $key) => [$key => $prop->metadata()]);

return $scrollProps->isNotEmpty() ? ['scrollProps' => $scrollProps->toArray()] : [];
}

/**
* Determine if the request is a partial request.
*/
Expand Down
Loading