diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..92b5bf56 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +The Laravel Code of Conduct can be found in the [Laravel documentation](https://laravel.com/docs/contributions#code-of-conduct). diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 0944fe4d..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: [reinink] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..03786937 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ + diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..800b8aff --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,92 @@ +# Security Policy + +**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).** + +## Supported Versions + +Only the latest major version receives security fixes. + +## Reporting a Vulnerability + +If you discover a security vulnerability within Laravel, please send an email to Taylor Otwell at taylor@laravel.com. All security vulnerabilities will be promptly addressed. + +### Public PGP Key + +``` +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: OpenPGP v2.0.8 +Comment: Report Security Vulnerabilities to taylor@laravel.com + +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 +WBGOG/qJGDlNiqBYYt2xNqzHCJoC +=zXOv +-----END PGP PUBLIC KEY BLOCK----- +``` diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 00000000..f0877fc2 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,3 @@ +# Support Questions + +The Laravel support guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions#support-questions). diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml new file mode 100644 index 00000000..24228e47 --- /dev/null +++ b/.github/workflows/coding-standards.yml @@ -0,0 +1,12 @@ +name: fix code styling + +on: [push] + +permissions: + contents: write + +jobs: + lint: + uses: laravel/.github/.github/workflows/coding-standards.yml@main + with: + php: "8.3" diff --git a/.github/workflows/facade.yml b/.github/workflows/facade.yml index 115daf3d..b2917730 100644 --- a/.github/workflows/facade.yml +++ b/.github/workflows/facade.yml @@ -3,7 +3,7 @@ name: facades on: push: branches: - - 'master' + - "master" jobs: update: @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7060e6e7..3591aea7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,53 +8,25 @@ on: jobs: tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: fail-fast: true matrix: - php: [7.3, 7.4, "8.0", 8.1, 8.2, 8.3] - laravel: [8, 9, 10, 11] + php: [8.1, 8.2, 8.3, 8.4] + laravel: [10, 11, 12] stability: ["prefer-lowest", "prefer-stable"] exclude: - - php: 7.3 - laravel: 9 - - php: 7.3 + - php: 8.4 laravel: 10 - - php: 7.3 - laravel: 11 - - php: 7.4 - laravel: 9 - - php: 7.4 - laravel: 10 - - php: 7.4 - laravel: 11 - - php: '8.0' - laravel: 10 - - php: '8.0' - laravel: 11 - - php: 8.1 - laravel: 6 - - php: 8.1 - laravel: 7 - php: 8.1 laravel: 11 - - php: 8.2 - laravel: 6 - - php: 8.2 - laravel: 7 - - php: 8.2 - laravel: 8 - - php: 8.3 - laravel: 6 - - php: 8.3 - laravel: 7 - - php: 8.3 - laravel: 8 + - php: 8.1 + laravel: 12 name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} (w/ ${{ matrix.stability }}) steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -66,42 +38,41 @@ jobs: coverage: none - name: Set Minimum PHP 8.1 Versions - uses: nick-invision/retry@v1 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 command: | - composer require phpunit/phpunit:^9.5.8 --dev --${{ matrix.stability }} --no-update --no-interaction composer require vlucas/phpdotenv:^5.3.1 --${{ matrix.stability }} --no-update --no-interaction if: matrix.php >= 8.1 && matrix.stability == 'prefer-lowest' - name: Set Minimum PHP 8.2 Versions - uses: nick-invision/retry@v1 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 command: | composer require nesbot/carbon:^2.62.1 --dev --${{ matrix.stability }} --no-update --no-interaction - if: matrix.php >= 8.2 && matrix.stability == 'prefer-lowest' + if: matrix.php >= 8.2 && matrix.stability == 'prefer-lowest' && matrix.laravel < 12 - name: Set Minimum PHP 8.2 Versions and Laravel > 11 - uses: nick-invision/retry@v1 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 command: | - composer require phpunit/phpunit:^10.4 --dev --${{ matrix.stability }} --no-update --no-interaction + composer require "orchestra/testbench:^9.2|^10.0" --dev --${{ matrix.stability }} --no-update --no-interaction if: matrix.php >= 8.2 && matrix.stability == 'prefer-lowest' && matrix.laravel >= 11 - name: Set Laravel version - uses: nick-invision/retry@v1 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 command: composer require "laravel/framework=^${{ matrix.laravel }}" --no-interaction --no-update - name: Install dependencies - uses: nick-invision/retry@v1 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php deleted file mode 100644 index cc0b20ff..00000000 --- a/.php-cs-fixer.dist.php +++ /dev/null @@ -1,28 +0,0 @@ -setUsingCache(false) - ->setRiskyAllowed(true) - ->setRules([ - '@PHP70Migration' => true, - '@PHP71Migration' => true, - '@PSR2' => true, - '@Symfony' => true, - 'array_syntax' => ['syntax' => 'short'], - 'increment_style' => ['style' => 'post'], - 'multiline_whitespace_before_semicolons' => true, - 'array_indentation' => true, - 'not_operator_with_successor_space' => true, - 'ordered_imports' => ['sort_algorithm' => 'length'], - 'php_unit_method_casing' => ['case' => 'snake_case'], - 'semicolon_after_instruction' => false, - 'single_line_throw' => false, - 'yoda_style' => false, - 'strict_comparison' => true, - 'yoda_style' => false, - 'single_line_throw' => false, - 'php_unit_method_casing' => ['case' => 'snake_case'], - 'global_namespace_import' => ['import_classes' => true, 'import_constants' => true, 'import_functions' => true], - ]); diff --git a/CHANGELOG.md b/CHANGELOG.md index ab6fa08b..41da63bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,62 @@ # Release Notes -## [Unreleased](https://github.com/inertiajs/inertia-laravel/compare/v1.2.0...1.x) +## [Unreleased](https://github.com/inertiajs/inertia-laravel/compare/v2.0.2...2.x) -- Add "always" props using new `Inertia::always()` wrapper ([#627](https://github.com/inertiajs/inertia-laravel/pull/627)) +- Nothing! + +## [v2.0.2](https://github.com/inertiajs/inertia-laravel/compare/v2.0.1...v2.0.2) - 2025-04-10 + +### What's Changed + +* [2.x] Supports Laravel 12 by [@crynobone](https://github.com/crynobone) in https://github.com/inertiajs/inertia-laravel/pull/709 +* Add Inertia::deepMerge Method for Handling Complex Data Merges in Responses by [@HichemTab-tech](https://github.com/HichemTab-tech) in https://github.com/inertiajs/inertia-laravel/pull/679 +* Improve PHPDoc annotations for ResponseFactory class by [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) in https://github.com/inertiajs/inertia-laravel/pull/723 +* fix props that extends Responsable after closures / lazy props by [@d8vjork](https://github.com/d8vjork) in https://github.com/inertiajs/inertia-laravel/pull/722 +* [2.x] Allow environment config for `ssr.enabled`, `ssr.url`, and `history.encrypt` by [@bram-pkg](https://github.com/bram-pkg) in https://github.com/inertiajs/inertia-laravel/pull/714 +* Replace `array_merge` with spread operator in `middleware.stub` by [@osbre](https://github.com/osbre) in https://github.com/inertiajs/inertia-laravel/pull/710 +* [2.x] Resolve Closure before checking if a prop implements the Arrayable contract by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/inertiajs/inertia-laravel/pull/706 +* Handle SSR URLs with trailing slashes by [@simon-tma](https://github.com/simon-tma) in https://github.com/inertiajs/inertia-laravel/pull/705 +* [2.x] Call `toArray()` on `Arrayable` props resolved from the Container by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia-laravel/pull/696 +* [2.x] Replace md5 with xxhash by [@RobertBoes](https://github.com/RobertBoes) in https://github.com/inertiajs/inertia-laravel/pull/653 + +### New Contributors + +* [@HichemTab-tech](https://github.com/HichemTab-tech) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/679 +* [@mohammadrasoulasghari](https://github.com/mohammadrasoulasghari) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/723 +* [@d8vjork](https://github.com/d8vjork) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/722 +* [@bram-pkg](https://github.com/bram-pkg) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/714 +* [@osbre](https://github.com/osbre) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/710 +* [@simon-tma](https://github.com/simon-tma) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/705 +* [@pascalbaljet](https://github.com/pascalbaljet) made their first contribution in https://github.com/inertiajs/inertia-laravel/pull/696 + +**Full Changelog**: https://github.com/inertiajs/inertia-laravel/compare/v2.0.1...v2.0.2 + +## [v2.0.1](https://github.com/inertiajs/inertia-laravel/compare/v2.0.0...v2.0.1) - 2025-02-18 + +- Allow Laravel 12.x. + +**Full Changelog**: https://github.com/inertiajs/inertia-laravel/compare/v2.0.0...v2.0.1 + +## [v2.0.0](https://github.com/inertiajs/inertia-laravel/compare/v1.2.0...2.0.0) + +- Add support for Inertia.js v2.0.0 +- Add `Inertia::defer()` to support deferred props +- Add `Inertia::merge()` to support merging props on client +- Add `Inertia::always()` for props that should always be included ([#627](https://github.com/inertiajs/inertia-laravel/pull/627)) +- Add `Inertia::clearHistory()` and `Inertia::encryptHistory()` methods, encryption config, and encryption middleware +- Deprecated `Inertia::lazy()` in favor of `Inertia::optional()` +- Drop support for Laravel 8 and 9 ([#629](https://github.com/inertiajs/inertia-laravel/pull/629)) ## [v1.2.0](https://github.com/inertiajs/inertia-laravel/compare/v1.1.0...v1.2.0) - 2024-05-17 -* [1.x] Make commands lazy by [@timacdonald](https://github.com/timacdonald) in https://github.com/inertiajs/inertia-laravel/pull/601 -* [1.x] Persistent properties by [@lepikhinb](https://github.com/lepikhinb) in https://github.com/inertiajs/inertia-laravel/pull/621 -* [1.x] Exclude properties from partial responses by [@lepikhinb](https://github.com/lepikhinb) in https://github.com/inertiajs/inertia-laravel/pull/622 +- Make commands lazy ([#601](https://github.com/inertiajs/inertia-laravel/pull/601)) +- Add persistent properties ([#621](https://github.com/inertiajs/inertia-laravel/pull/621)) +- Exclude `except` props from partial reloads ([#622](https://github.com/inertiajs/inertia-laravel/pull/622)) ## [v1.1.0](https://github.com/inertiajs/inertia-laravel/compare/v1.0.0...v1.1.0) - 2024-05-16 -* Support dot notation in partial requests by [@lepikhinb](https://github.com/lepikhinb) in https://github.com/inertiajs/inertia-laravel/pull/620 -* [1.x] Add `$request->inertia()` IDE helper by [@ycs77](https://github.com/ycs77) in https://github.com/inertiajs/inertia-laravel/pull/625 +- Support dot notation in partial requests ([#620](https://github.com/inertiajs/inertia-laravel/pull/620)) +- Add `$request->inertia()` IDE helper ([#625](https://github.com/inertiajs/inertia-laravel/pull/625)) ## [v1.0.0](https://github.com/inertiajs/inertia-laravel/compare/v0.6.11...v1.0.0) - 2024-03-08 diff --git a/README.md b/README.md index 52d49ba8..747b29d9 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,19 @@

Visit [inertiajs.com](https://inertiajs.com/) to learn more. + +## Contributing + +Thank you for considering contributing to Inertia! You can read the contribution guide [here](.github/CONTRIBUTING.md). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +Please review [our security policy](https://github.com/inertiajs/inertia-laravel/security/policy) on how to report security vulnerabilities. + +## License + +Inertia is open-sourced software licensed under the [MIT license](LICENSE.md). diff --git a/_ide_helpers.php b/_ide_helpers.php deleted file mode 100644 index 72a525dd..00000000 --- a/_ide_helpers.php +++ /dev/null @@ -1,80 +0,0 @@ - [ - 'enabled' => true, + 'enabled' => (bool) env('INERTIA_SSR_ENABLED', true), - 'url' => 'http://127.0.0.1:13714', + 'url' => env('INERTIA_SSR_URL', 'http://127.0.0.1:13714'), // 'bundle' => base_path('bootstrap/ssr/ssr.mjs'), @@ -64,4 +64,10 @@ ], + 'history' => [ + + 'encrypt' => (bool) env('INERTIA_ENCRYPT_HISTORY', false), + + ], + ]; diff --git a/src/AlwaysProp.php b/src/AlwaysProp.php index 5c9ee6ec..62066ebf 100644 --- a/src/AlwaysProp.php +++ b/src/AlwaysProp.php @@ -10,7 +10,7 @@ class AlwaysProp protected $value; /** - * @param mixed $value + * @param mixed $value */ public function __construct($value) { diff --git a/src/Commands/CreateMiddleware.php b/src/Commands/CreateMiddleware.php index 59bc2981..bd5c4a09 100644 --- a/src/Commands/CreateMiddleware.php +++ b/src/Commands/CreateMiddleware.php @@ -41,7 +41,7 @@ protected function getStub(): string /** * Get the default namespace for the class. * - * @param string $rootNamespace + * @param string $rootNamespace */ protected function getDefaultNamespace($rootNamespace): string { diff --git a/src/Commands/StartSsr.php b/src/Commands/StartSsr.php index c7f1215d..ae5d96f4 100644 --- a/src/Commands/StartSsr.php +++ b/src/Commands/StartSsr.php @@ -2,9 +2,9 @@ namespace Inertia\Commands; -use Inertia\Ssr\SsrException; use Illuminate\Console\Command; use Inertia\Ssr\BundleDetector; +use Inertia\Ssr\SsrException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Process\Process; @@ -36,7 +36,7 @@ public function handle(): int return self::FAILURE; } - $bundle = (new BundleDetector())->detect(); + $bundle = (new BundleDetector)->detect(); $configuredBundle = config('inertia.ssr.bundle'); if ($bundle === null) { diff --git a/src/DeferProp.php b/src/DeferProp.php new file mode 100644 index 00000000..db722c75 --- /dev/null +++ b/src/DeferProp.php @@ -0,0 +1,30 @@ +callback = $callback; + $this->group = $group; + } + + public function group() + { + return $this->group; + } + + public function __invoke() + { + return App::call($this->callback); + } +} diff --git a/src/Directive.php b/src/Directive.php index 71009b61..dca132f7 100644 --- a/src/Directive.php +++ b/src/Directive.php @@ -7,7 +7,7 @@ class Directive /** * Compiles the "@inertia" directive. * - * @param string $expression + * @param string $expression */ public static function compile($expression = ''): string { @@ -32,7 +32,7 @@ public static function compile($expression = ''): string /** * Compiles the "@inertiaHead" directive. * - * @param string $expression + * @param string $expression */ public static function compileHead($expression = ''): string { diff --git a/src/EncryptHistoryMiddleware.php b/src/EncryptHistoryMiddleware.php new file mode 100644 index 00000000..e7eefa1a --- /dev/null +++ b/src/EncryptHistoryMiddleware.php @@ -0,0 +1,22 @@ +value = $value; + $this->merge = true; + $this->mergeStrategies = $mergeStrategies; + } + + public function __invoke() + { + return is_callable($this->value) ? App::call($this->value) : $this->value; + } +} diff --git a/src/Mergeable.php b/src/Mergeable.php new file mode 100644 index 00000000..64878a05 --- /dev/null +++ b/src/Mergeable.php @@ -0,0 +1,10 @@ +merge = true; + + return $this; + } + + public function deepMerge(): static + { + $this->deepMerge = true; + + return $this->merge(); + } + + public function shouldMerge(): bool + { + return $this->merge; + } + + public function shouldDeepMerge(): bool + { + return $this->deepMerge; + } + + public function mergeStrategies(): array + { + return $this->mergeStrategies; + } +} diff --git a/src/Middleware.php b/src/Middleware.php index 24caf9ad..8c2d9e24 100644 --- a/src/Middleware.php +++ b/src/Middleware.php @@ -29,15 +29,15 @@ class Middleware public function version(Request $request) { if (config('app.asset_url')) { - return md5(config('app.asset_url')); + return hash('xxh128', config('app.asset_url')); } - if (file_exists($manifest = public_path('mix-manifest.json'))) { - return md5_file($manifest); + if (file_exists($manifest = public_path('build/manifest.json'))) { + return hash_file('xxh128', $manifest); } - if (file_exists($manifest = public_path('build/manifest.json'))) { - return md5_file($manifest); + if (file_exists($manifest = public_path('mix-manifest.json'))) { + return hash_file('xxh128', $manifest); } return null; diff --git a/src/OptionalProp.php b/src/OptionalProp.php new file mode 100644 index 00000000..93b1c7d7 --- /dev/null +++ b/src/OptionalProp.php @@ -0,0 +1,20 @@ +callback = $callback; + } + + public function __invoke() + { + return App::call($this->callback); + } +} diff --git a/src/Response.php b/src/Response.php index fc7dcf04..81c6813c 100644 --- a/src/Response.php +++ b/src/Response.php @@ -2,19 +2,18 @@ namespace Inertia; +use Carbon\CarbonInterval; use Closure; -use Illuminate\Http\Request; -use Illuminate\Http\JsonResponse; -use Illuminate\Support\Facades\App; -use Illuminate\Support\Str; use GuzzleHttp\Promise\PromiseInterface; -use Illuminate\Support\Traits\Macroable; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Responsable; -use Illuminate\Http\Resources\Json\JsonResource; -use Illuminate\Http\Resources\Json\ResourceResponse; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Response as ResponseFactory; +use Illuminate\Support\Str; +use Illuminate\Support\Traits\Macroable; use Inertia\Support\Header; class Response implements Responsable @@ -22,26 +21,37 @@ class Response implements Responsable use Macroable; protected $component; + protected $props; + protected $rootView; + protected $version; + + protected $clearHistory; + + protected $encryptHistory; + protected $viewData = []; + protected $cacheFor = []; + /** - * @param array|Arrayable $props + * @param array|Arrayable $props */ - public function __construct(string $component, array $props, string $rootView = 'app', string $version = '') + public function __construct(string $component, array $props, string $rootView = 'app', string $version = '', bool $encryptHistory = false) { $this->component = $component; $this->props = $props instanceof Arrayable ? $props->toArray() : $props; $this->rootView = $rootView; $this->version = $version; + $this->clearHistory = session()->pull('inertia.clear_history', false); + $this->encryptHistory = $encryptHistory; } /** - * @param string|array $key - * @param mixed $value - * + * @param string|array $key + * @param mixed $value * @return $this */ public function with($key, $value = null): self @@ -56,9 +66,8 @@ public function with($key, $value = null): self } /** - * @param string|array $key - * @param mixed $value - * + * @param string|array $key + * @param mixed $value * @return $this */ public function withViewData($key, $value = null): self @@ -79,23 +88,36 @@ public function rootView(string $rootView): self return $this; } + public function cache(string|array $cacheFor): self + { + $this->cacheFor = is_array($cacheFor) ? $cacheFor : [$cacheFor]; + + return $this; + } + /** * Create an HTTP response that represents the object. * - * @param \Illuminate\Http\Request $request - * + * @param \Illuminate\Http\Request $request * @return \Symfony\Component\HttpFoundation\Response */ public function toResponse($request) { $props = $this->resolveProperties($request, $this->props); - $page = [ - 'component' => $this->component, - 'props' => $props, - 'url' => Str::start(Str::after($request->fullUrl(), $request->getSchemeAndHttpHost()), '/'), - 'version' => $this->version, - ]; + $page = array_merge( + [ + 'component' => $this->component, + 'props' => $props, + 'url' => $this->getUrl($request), + 'version' => $this->version, + 'clearHistory' => $this->clearHistory, + 'encryptHistory' => $this->encryptHistory, + ], + $this->resolveMergeProps($request), + $this->resolveDeferredProps($request), + $this->resolveCacheDirections($request), + ); if ($request->header(Header::INERTIA)) { return new JsonResponse($page, 200, [Header::INERTIA => 'true']); @@ -109,27 +131,41 @@ public function toResponse($request) */ public function resolveProperties(Request $request, array $props): array { - $isPartial = $request->header(Header::PARTIAL_COMPONENT) === $this->component; + $props = $this->resolvePartialProperties($props, $request); + $props = $this->resolveArrayableProperties($props, $request); + $props = $this->resolveAlways($props); + $props = $this->resolvePropertyInstances($props, $request); - if(! $isPartial) { - $props = array_filter($this->props, static function ($prop) { - return ! ($prop instanceof LazyProp); + return $props; + } + + /** + * Resolve the `only` and `except` partial request props. + */ + public function resolvePartialProperties(array $props, Request $request): array + { + if (! $this->isPartial($request)) { + return array_filter($this->props, static function ($prop) { + return ! ($prop instanceof IgnoreFirstLoad); }); } - $props = $this->resolveArrayableProperties($props, $request); + $only = array_filter(explode(',', $request->header(Header::PARTIAL_ONLY, ''))); + $except = array_filter(explode(',', $request->header(Header::PARTIAL_EXCEPT, ''))); - if($isPartial && $request->hasHeader(Header::PARTIAL_ONLY)) { - $props = $this->resolveOnly($request, $props); - } + if (count($only)) { + $newProps = []; - if($isPartial && $request->hasHeader(Header::PARTIAL_EXCEPT)) { - $props = $this->resolveExcept($request, $props); - } + foreach ($only as $key) { + Arr::set($newProps, $key, Arr::get($props, $key)); + } - $props = $this->resolveAlways($props); + $props = $newProps; + } - $props = $this->resolvePropertyInstances($props, $request); + if ($except) { + Arr::forget($props, $except); + } return $props; } @@ -140,6 +176,10 @@ public function resolveProperties(Request $request, array $props): array public function resolveArrayableProperties(array $props, Request $request, bool $unpackDotProps = true): array { foreach ($props as $key => $value) { + if ($value instanceof Closure) { + $value = App::call($value); + } + if ($value instanceof Arrayable) { $value = $value->toArray(); } @@ -168,7 +208,7 @@ public function resolveOnly(Request $request, array $props): array $value = []; - foreach($only as $key) { + foreach ($only as $key) { Arr::set($value, $key, data_get($props, $key)); } @@ -208,24 +248,33 @@ public function resolveAlways(array $props): array public function resolvePropertyInstances(array $props, Request $request): array { foreach ($props as $key => $value) { - if ($value instanceof Closure) { - $value = App::call($value); - } - - if ($value instanceof LazyProp) { + $resolveViaApp = collect([ + Closure::class, + LazyProp::class, + OptionalProp::class, + DeferProp::class, + AlwaysProp::class, + MergeProp::class, + ])->first(fn ($class) => $value instanceof $class); + + if ($resolveViaApp) { $value = App::call($value); } - if ($value instanceof AlwaysProp) { - $value = App::call($value); + if ($value instanceof Arrayable) { + $value = $value->toArray(); } if ($value instanceof PromiseInterface) { $value = $value->wait(); } - if ($value instanceof ResourceResponse || $value instanceof JsonResource) { - $value = $value->toResponse($request)->getData(true); + if ($value instanceof Responsable) { + $_response = $value->toResponse($request); + + if (method_exists($_response, 'getData')) { + $value = $_response->getData(true); + } } if (is_array($value)) { @@ -237,4 +286,112 @@ public function resolvePropertyInstances(array $props, Request $request): array return $props; } + + /** + * Resolve the cache directions for the response. + */ + public function resolveCacheDirections(Request $request): array + { + if (count($this->cacheFor) === 0) { + return []; + } + + return [ + 'cache' => collect($this->cacheFor)->map(function ($value) { + if ($value instanceof CarbonInterval) { + return $value->totalSeconds; + } + + return intval($value); + }), + ]; + } + + public function resolveMergeProps(Request $request): array + { + $resetProps = collect(explode(',', $request->header(Header::RESET, ''))); + $mergeProps = collect($this->props) + ->filter(fn ($prop) => $prop instanceof Mergeable) + ->filter(fn ($prop) => $prop->shouldMerge()) + ->filter(fn ($_, $key) => ! $resetProps->contains($key)); + + $deepMergeProps = $mergeProps + ->filter(fn ($prop) => $prop->shouldDeepMerge()) + ->keys(); + + $mergeStrategies = $mergeProps + ->map(function ($prop, $key) { + return collect($prop->mergeStrategies()) + ->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(), + 'mergeStrategies' => $mergeStrategies->toArray(), + ], fn ($prop) => count($prop) > 0); + } + + public function resolveDeferredProps(Request $request): array + { + if ($this->isPartial($request)) { + return []; + } + + $deferredProps = collect($this->props) + ->filter(function ($prop) { + return $prop instanceof DeferProp; + }) + ->map(function ($prop, $key) { + return [ + 'key' => $key, + 'group' => $prop->group(), + ]; + }) + ->groupBy('group') + ->map + ->pluck('key'); + + return $deferredProps->isNotEmpty() ? ['deferredProps' => $deferredProps->toArray()] : []; + } + + /** + * Determine if the request is a partial request. + */ + public function isPartial(Request $request): bool + { + return $request->header(Header::PARTIAL_COMPONENT) === $this->component; + } + + /** + * Get the URL from the request (without the scheme and host) while preserving the trailing slash if it exists. + */ + protected function getUrl(Request $request): string + { + $url = Str::start(Str::after($request->fullUrl(), $request->getSchemeAndHttpHost()), '/'); + + $rawUri = Str::before($request->getRequestUri(), '?'); + + return Str::endsWith($rawUri, '/') ? $this->finishUrlWithTrailingSlash($url) : $url; + } + + /** + * Ensure the URL has a trailing slash before the query string (if it exists). + */ + protected function finishUrlWithTrailingSlash(string $url): string + { + // Make sure the relative URL ends with a trailing slash and re-append the query string if it exists. + $urlWithoutQueryWithTrailingSlash = Str::finish(Str::before($url, '?'), '/'); + + return str_contains($url, '?') + ? $urlWithoutQueryWithTrailingSlash.'?'.Str::after($url, '?') + : $urlWithoutQueryWithTrailingSlash; + } } diff --git a/src/ResponseFactory.php b/src/ResponseFactory.php index 28144c7d..a0ba7947 100644 --- a/src/ResponseFactory.php +++ b/src/ResponseFactory.php @@ -3,16 +3,16 @@ namespace Inertia; use Closure; +use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Arr; use Illuminate\Support\Facades\App; -use Illuminate\Support\Facades\Request; -use Illuminate\Support\Traits\Macroable; -use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Facades\Redirect; +use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Response as BaseResponse; +use Illuminate\Support\Traits\Macroable; use Inertia\Support\Header; -use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirect; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; class ResponseFactory { @@ -27,14 +27,22 @@ class ResponseFactory /** @var Closure|string|null */ protected $version; + protected $clearHistory = false; + + protected $encryptHistory; + + /*** + * @param string $name The name of the root view + * @return void + */ public function setRootView(string $name): void { $this->rootView = $name; } /** - * @param string|array|Arrayable $key - * @param mixed $value + * @param string|array|Arrayable $key + * @param mixed $value */ public function share($key, $value = null): void { @@ -48,11 +56,10 @@ public function share($key, $value = null): void } /** - * @param mixed $default - * + * @param mixed $default * @return mixed */ - public function getShared(string $key = null, $default = null) + public function getShared(?string $key = null, $default = null) { if ($key) { return Arr::get($this->sharedProps, $key, $default); @@ -61,13 +68,16 @@ public function getShared(string $key = null, $default = null) return $this->sharedProps; } - public function flushShared(): void + /** + * @return void + */ + public function flushShared() { $this->sharedProps = []; } /** - * @param Closure|string|null $version + * @param Closure|string|null $version */ public function version($version): void { @@ -83,13 +93,57 @@ public function getVersion(): string return (string) $version; } + public function clearHistory(): void + { + session(['inertia.clear_history' => true]); + } + + /** + * @param bool $encrypt + */ + public function encryptHistory($encrypt = true): void + { + $this->encryptHistory = $encrypt; + } + + /** + * @deprecated Use `optional` instead. + */ public function lazy(callable $callback): LazyProp { return new LazyProp($callback); } + public function optional(callable $callback): OptionalProp + { + return new OptionalProp($callback); + } + + public function defer(callable $callback, string $group = 'default'): DeferProp + { + return new DeferProp($callback, $group); + } + + /** + * @param mixed $value + */ + public function merge($value): MergeProp + { + return new MergeProp($value); + } + + /** + * @param mixed $value + * + * @parram null|string|string[] $mergeStrategies + */ + public function deepMerge($value, $mergeStrategies = null): MergeProp + { + return (new MergeProp($value, Arr::wrap($mergeStrategies)))->deepMerge(); + } + /** - * @param mixed $value + * @param mixed $value */ public function always($value): AlwaysProp { @@ -97,7 +151,7 @@ public function always($value): AlwaysProp } /** - * @param array|Arrayable $props + * @param array|Arrayable $props */ public function render(string $component, $props = []): Response { @@ -109,12 +163,13 @@ public function render(string $component, $props = []): Response $component, array_merge($this->sharedProps, $props), $this->rootView, - $this->getVersion() + $this->getVersion(), + $this->encryptHistory ?? config('inertia.history.encrypt', false), ); } /** - * @param string|SymfonyRedirect $url + * @param string|SymfonyRedirect $url */ public function location($url): SymfonyResponse { diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 50868f16..b0e0e0c8 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -2,18 +2,17 @@ namespace Inertia; -use LogicException; -use Inertia\Ssr\Gateway; -use ReflectionException; use Illuminate\Http\Request; -use Inertia\Ssr\HttpGateway; use Illuminate\Routing\Router; -use Illuminate\View\FileViewFinder; -use Illuminate\Testing\TestResponse; -use Inertia\Testing\TestResponseMacros; use Illuminate\Support\ServiceProvider as BaseServiceProvider; -use Illuminate\Foundation\Testing\TestResponse as LegacyTestResponse; +use Illuminate\Testing\TestResponse; +use Illuminate\View\FileViewFinder; +use Inertia\Ssr\Gateway; +use Inertia\Ssr\HttpGateway; use Inertia\Support\Header; +use Inertia\Testing\TestResponseMacros; +use LogicException; +use ReflectionException; class ServiceProvider extends BaseServiceProvider { @@ -31,6 +30,7 @@ public function register(): void $this->registerRequestMacro(); $this->registerRouterMacro(); $this->registerTestingMacros(); + $this->registerMiddleware(); $this->app->bind('inertia.testing.view-finder', function ($app) { return new FileViewFinder( @@ -93,18 +93,19 @@ protected function registerRouterMacro(): void protected function registerTestingMacros(): void { if (class_exists(TestResponse::class)) { - TestResponse::mixin(new TestResponseMacros()); - - return; - } - - // Laravel <= 6.0 - if (class_exists(LegacyTestResponse::class)) { - LegacyTestResponse::mixin(new TestResponseMacros()); + TestResponse::mixin(new TestResponseMacros); return; } throw new LogicException('Could not detect TestResponse class.'); } + + protected function registerMiddleware(): void + { + $this->app['router']->aliasMiddleware( + 'inertia.encrypt', + EncryptHistoryMiddleware::class + ); + } } diff --git a/src/Ssr/HttpGateway.php b/src/Ssr/HttpGateway.php index 65a62b81..9f777454 100644 --- a/src/Ssr/HttpGateway.php +++ b/src/Ssr/HttpGateway.php @@ -12,11 +12,11 @@ class HttpGateway implements Gateway */ public function dispatch(array $page): ?Response { - if (! config('inertia.ssr.enabled', true) || ! (new BundleDetector())->detect()) { + if (! config('inertia.ssr.enabled', true) || ! (new BundleDetector)->detect()) { return null; } - $url = str_replace('/render', '', config('inertia.ssr.url', 'http://127.0.0.1:13714')).'/render'; + $url = str_replace('/render', '', rtrim(config('inertia.ssr.url', 'http://127.0.0.1:13714'), '/')).'/render'; try { $response = Http::post($url, $page)->throw()->json(); diff --git a/src/Ssr/SsrException.php b/src/Ssr/SsrException.php index 32379e1b..7cca6f42 100644 --- a/src/Ssr/SsrException.php +++ b/src/Ssr/SsrException.php @@ -4,6 +4,4 @@ use Exception; -class SsrException extends Exception -{ -} +class SsrException extends Exception {} diff --git a/src/Support/Header.php b/src/Support/Header.php index 2b902d8a..5091b67f 100644 --- a/src/Support/Header.php +++ b/src/Support/Header.php @@ -5,10 +5,18 @@ class Header { public const INERTIA = 'X-Inertia'; + public const ERROR_BAG = 'X-Inertia-Error-Bag'; + public const LOCATION = 'X-Inertia-Location'; + public const VERSION = 'X-Inertia-Version'; + public const PARTIAL_COMPONENT = 'X-Inertia-Partial-Component'; + public const PARTIAL_ONLY = 'X-Inertia-Partial-Data'; + public const PARTIAL_EXCEPT = 'X-Inertia-Partial-Except'; + + public const RESET = 'X-Inertia-Reset'; } diff --git a/src/Testing/AssertableInertia.php b/src/Testing/AssertableInertia.php index 370e748d..d1ee90e7 100644 --- a/src/Testing/AssertableInertia.php +++ b/src/Testing/AssertableInertia.php @@ -2,11 +2,11 @@ namespace Inertia\Testing; -use InvalidArgumentException; +use Illuminate\Testing\Fluent\AssertableJson; use Illuminate\Testing\TestResponse; +use InvalidArgumentException; use PHPUnit\Framework\Assert as PHPUnit; use PHPUnit\Framework\AssertionFailedError; -use Illuminate\Testing\Fluent\AssertableJson; class AssertableInertia extends AssertableJson { @@ -19,6 +19,12 @@ class AssertableInertia extends AssertableJson /** @var string|null */ private $version; + /** @var bool */ + private $encryptHistory; + + /** @var bool */ + private $clearHistory; + public static function fromTestResponse(TestResponse $response): self { try { @@ -30,6 +36,8 @@ public static function fromTestResponse(TestResponse $response): self PHPUnit::assertArrayHasKey('props', $page); PHPUnit::assertArrayHasKey('url', $page); PHPUnit::assertArrayHasKey('version', $page); + PHPUnit::assertArrayHasKey('encryptHistory', $page); + PHPUnit::assertArrayHasKey('clearHistory', $page); } catch (AssertionFailedError $e) { PHPUnit::fail('Not a valid Inertia response.'); } @@ -38,11 +46,13 @@ public static function fromTestResponse(TestResponse $response): self $instance->component = $page['component']; $instance->url = $page['url']; $instance->version = $page['version']; + $instance->encryptHistory = $page['encryptHistory']; + $instance->clearHistory = $page['clearHistory']; return $instance; } - public function component(string $value = null, $shouldExist = null): self + public function component(?string $value = null, $shouldExist = null): self { PHPUnit::assertSame($value, $this->component, 'Unexpected Inertia page component.'); @@ -78,6 +88,8 @@ public function toArray() 'props' => $this->prop(), 'url' => $this->url, 'version' => $this->version, + 'encryptHistory' => $this->encryptHistory, + 'clearHistory' => $this->clearHistory, ]; } } diff --git a/src/Testing/Concerns/Debugging.php b/src/Testing/Concerns/Debugging.php index 612374de..86a6269c 100644 --- a/src/Testing/Concerns/Debugging.php +++ b/src/Testing/Concerns/Debugging.php @@ -4,17 +4,17 @@ trait Debugging { - public function dump(string $prop = null): self + public function dump(?string $prop = null): self { dump($this->prop($prop)); return $this; } - public function dd(string $prop = null): void + public function dd(?string $prop = null): void { dd($this->prop($prop)); } - abstract protected function prop(string $key = null); + abstract protected function prop(?string $key = null); } diff --git a/src/Testing/Concerns/Has.php b/src/Testing/Concerns/Has.php index b48511c7..37866b60 100644 --- a/src/Testing/Concerns/Has.php +++ b/src/Testing/Concerns/Has.php @@ -36,11 +36,10 @@ public function hasAll($key): self } /** - * @param mixed $value - * + * @param mixed $value * @return $this */ - public function has(string $key, $value = null, Closure $scope = null): self + public function has(string $key, $value = null, ?Closure $scope = null): self { PHPUnit::assertTrue( Arr::has($this->prop(), $key), @@ -108,7 +107,7 @@ public function misses(string $key): self return $this->missing($key); } - abstract protected function prop(string $key = null); + abstract protected function prop(?string $key = null); abstract protected function dotPath(string $key): string; diff --git a/src/Testing/Concerns/Interaction.php b/src/Testing/Concerns/Interaction.php index 506d64d2..145d405e 100644 --- a/src/Testing/Concerns/Interaction.php +++ b/src/Testing/Concerns/Interaction.php @@ -37,5 +37,5 @@ public function etc(): self return $this; } - abstract protected function prop(string $key = null); + abstract protected function prop(?string $key = null); } diff --git a/src/Testing/Concerns/Matching.php b/src/Testing/Concerns/Matching.php index 6cb4404b..7ead91ba 100644 --- a/src/Testing/Concerns/Matching.php +++ b/src/Testing/Concerns/Matching.php @@ -3,11 +3,11 @@ namespace Inertia\Testing\Concerns; use Closure; -use Illuminate\Support\Collection; -use PHPUnit\Framework\Assert as PHPUnit; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\ResourceResponse; +use Illuminate\Support\Collection; +use PHPUnit\Framework\Assert as PHPUnit; trait Matching { @@ -68,7 +68,7 @@ protected function ensureSorted(&$value): void abstract protected function dotPath(string $key): string; - abstract protected function prop(string $key = null); + abstract protected function prop(?string $key = null); - abstract public function has(string $key, $value = null, Closure $scope = null); + abstract public function has(string $key, $value = null, ?Closure $scope = null); } diff --git a/src/Testing/Concerns/PageObject.php b/src/Testing/Concerns/PageObject.php index 1dc98389..109df780 100644 --- a/src/Testing/Concerns/PageObject.php +++ b/src/Testing/Concerns/PageObject.php @@ -8,7 +8,7 @@ trait PageObject { - public function component(string $value = null, $shouldExist = null): self + public function component(?string $value = null, $shouldExist = null): self { PHPUnit::assertSame($value, $this->component, 'Unexpected Inertia page component.'); @@ -23,7 +23,7 @@ public function component(string $value = null, $shouldExist = null): self return $this; } - protected function prop(string $key = null) + protected function prop(?string $key = null) { return Arr::get($this->props, $key); } @@ -49,6 +49,8 @@ public function toArray(): array 'props' => $this->props, 'url' => $this->url, 'version' => $this->version, + 'encryptHistory' => $this->encryptHistory, + 'clearHistory' => $this->clearHistory, ]; } } diff --git a/src/Testing/TestResponseMacros.php b/src/Testing/TestResponseMacros.php index a4ca6802..cf4e5e29 100644 --- a/src/Testing/TestResponseMacros.php +++ b/src/Testing/TestResponseMacros.php @@ -3,12 +3,13 @@ namespace Inertia\Testing; use Closure; +use Illuminate\Support\Arr; class TestResponseMacros { public function assertInertia() { - return function (Closure $callback = null) { + return function (?Closure $callback = null) { $assert = AssertableInertia::fromTestResponse($this); if (is_null($callback)) { @@ -27,4 +28,11 @@ public function inertiaPage() return AssertableInertia::fromTestResponse($this)->toArray(); }; } + + public function inertiaProps() + { + return function (?string $propName = null) { + return Arr::get($this->inertiaPage()['props'], $propName); + }; + } } diff --git a/stubs/middleware.stub b/stubs/middleware.stub index 256618b7..09f6b641 100644 --- a/stubs/middleware.stub +++ b/stubs/middleware.stub @@ -35,8 +35,9 @@ class {{ class }} extends Middleware */ public function share(Request $request): array { - return array_merge(parent::share($request), [ + return [ + ...parent::share($request), // - ]); + ]; } } diff --git a/tests/AlwaysPropTest.php b/tests/AlwaysPropTest.php index c1756e53..df1e41a8 100644 --- a/tests/AlwaysPropTest.php +++ b/tests/AlwaysPropTest.php @@ -25,8 +25,10 @@ public function test_can_accept_scalar_values(): void public function test_can_accept_callables(): void { - $callable = new class { - public function __invoke() { + $callable = new class + { + public function __invoke() + { return 'An always value'; } }; diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php index 5e90f3e1..3648a0fc 100644 --- a/tests/ControllerTest.php +++ b/tests/ControllerTest.php @@ -2,10 +2,10 @@ namespace Inertia\Tests; -use Inertia\Controller; +use Illuminate\Session\Middleware\StartSession; use Illuminate\Support\Facades\Route; +use Inertia\Controller; use Inertia\Tests\Stubs\ExampleMiddleware; -use Illuminate\Session\Middleware\StartSession; class ControllerTest extends TestCase { @@ -28,6 +28,8 @@ public function test_controller_returns_an_inertia_response(): void ], 'url' => '/', 'version' => '', + 'encryptHistory' => false, + 'clearHistory' => false, ]); } } diff --git a/tests/DeepMergePropTest.php b/tests/DeepMergePropTest.php new file mode 100644 index 00000000..4e282dec --- /dev/null +++ b/tests/DeepMergePropTest.php @@ -0,0 +1,37 @@ + 'A merge prop value'))->deepMerge(); + + $this->assertSame('A merge prop value', $mergeProp()); + } + + public function test_can_invoke_with_a_non_callback(): void + { + $mergeProp = (new MergeProp(['key' => 'value']))->deepMerge(); + + $this->assertSame(['key' => 'value'], $mergeProp()); + } + + public function test_can_resolve_bindings_when_invoked(): void + { + $mergeProp = (new MergeProp(fn (Request $request) => $request))->deepMerge(); + + $this->assertInstanceOf(Request::class, $mergeProp()); + } + + public function test_can_use_single_string_as_merge_strategy(): void + { + $mergeProp = (new MergeProp(['key' => 'value'], ['key']))->deepMerge(); + + $this->assertEquals(['key'], $mergeProp->mergeStrategies()); + } +} diff --git a/tests/DeferPropTest.php b/tests/DeferPropTest.php new file mode 100644 index 00000000..83a27ae7 --- /dev/null +++ b/tests/DeferPropTest.php @@ -0,0 +1,37 @@ +assertSame('A deferred value', $deferProp()); + $this->assertSame('default', $deferProp->group()); + } + + public function test_can_invoke_and_merge(): void + { + $deferProp = (new DeferProp(function () { + return 'A deferred value'; + }))->merge(); + + $this->assertSame('A deferred value', $deferProp()); + } + + public function test_can_resolve_bindings_when_invoked(): void + { + $deferProp = new DeferProp(function (Request $request) { + return $request; + }); + + $this->assertInstanceOf(Request::class, $deferProp()); + } +} diff --git a/tests/DirectiveTest.php b/tests/DirectiveTest.php index 82e6b737..f1afdd07 100644 --- a/tests/DirectiveTest.php +++ b/tests/DirectiveTest.php @@ -2,18 +2,18 @@ namespace Inertia\Tests; -use Throwable; -use Mockery as m; -use Inertia\Directive; -use Inertia\Ssr\Gateway; -use Illuminate\View\View; -use Illuminate\View\Factory; -use Inertia\Tests\Stubs\FakeGateway; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Config; -use Illuminate\View\Engines\PhpEngine; use Illuminate\View\Compilers\BladeCompiler; +use Illuminate\View\Engines\PhpEngine; +use Illuminate\View\Factory; +use Illuminate\View\View; +use Inertia\Directive; +use Inertia\Ssr\Gateway; +use Inertia\Tests\Stubs\FakeGateway; +use Mockery as m; +use Throwable; class DirectiveTest extends TestCase { @@ -30,9 +30,9 @@ class DirectiveTest extends TestCase /** * Example Page Objects. */ - protected const EXAMPLE_PAGE_OBJECT = ['component' => 'Foo/Bar', 'props' => ['foo' => 'bar'], 'url' => '/test', 'version' => '']; + protected const EXAMPLE_PAGE_OBJECT = ['component' => 'Foo/Bar', 'props' => ['foo' => 'bar'], 'url' => '/test', 'version' => '', 'encryptHistory' => false, 'clearHistory' => false]; - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -65,7 +65,7 @@ protected function renderView($contents, $data = []) // Next, we'll 'render' out compiled view. $view = new View( m::mock(Factory::class), - new PhpEngine(new Filesystem()), + new PhpEngine(new Filesystem), 'fake-view', $path, $data @@ -94,7 +94,7 @@ public function test_inertia_directive_renders_the_root_element(): void { Config::set(['inertia.ssr.enabled' => false]); - $html = '
'; + $html = '
'; $this->assertSame($html, $this->renderView('@inertia', ['page' => self::EXAMPLE_PAGE_OBJECT])); $this->assertSame($html, $this->renderView('@inertia()', ['page' => self::EXAMPLE_PAGE_OBJECT])); @@ -116,7 +116,7 @@ public function test_inertia_directive_can_use_a_different_root_element_id(): vo { Config::set(['inertia.ssr.enabled' => false]); - $html = '
'; + $html = '
'; $this->assertSame($html, $this->renderView('@inertia(foo)', ['page' => self::EXAMPLE_PAGE_OBJECT])); $this->assertSame($html, $this->renderView("@inertia('foo')", ['page' => self::EXAMPLE_PAGE_OBJECT])); @@ -143,7 +143,7 @@ public function test_inertia_head_directive_renders_server_side_rendered_head_el public function test_the_server_side_rendering_request_is_dispatched_only_once_per_request(): void { Config::set(['inertia.ssr.enabled' => true]); - $this->app->instance(Gateway::class, $gateway = new FakeGateway()); + $this->app->instance(Gateway::class, $gateway = new FakeGateway); $view = "\n\n\n@inertiaHead\n\n\n@inertia\n\n"; $expected = "\n\n\n\nExample SSR Title\n\n\n

This is some example SSR content

\n"; diff --git a/tests/HistoryTest.php b/tests/HistoryTest.php new file mode 100644 index 00000000..85e94c53 --- /dev/null +++ b/tests/HistoryTest.php @@ -0,0 +1,165 @@ +get('/', function () { + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'encryptHistory' => false, + 'clearHistory' => false, + ]); + } + + public function test_the_history_can_be_encrypted(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::encryptHistory(); + + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'encryptHistory' => true, + ]); + } + + public function test_the_history_can_be_encrypted_via_middleware(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class, EncryptHistoryMiddleware::class])->get('/', function () { + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'encryptHistory' => true, + ]); + } + + public function test_the_history_can_be_encrypted_via_middleware_alias(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class, 'inertia.encrypt'])->get('/', function () { + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'encryptHistory' => true, + ]); + } + + public function test_the_history_can_be_encrypted_globally(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Config::set('inertia.history.encrypt', true); + + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'encryptHistory' => true, + ]); + } + + public function test_the_history_can_be_encrypted_globally_and_overridden(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Config::set('inertia.history.encrypt', true); + + Inertia::encryptHistory(false); + + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'encryptHistory' => false, + ]); + } + + public function test_the_history_can_be_cleared(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::clearHistory(); + + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'clearHistory' => true, + ]); + } + + public function test_the_history_can_be_cleared_when_redirecting(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::clearHistory(); + + return redirect('/users'); + }); + + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/users', function () { + return Inertia::render('User/Edit'); + }); + + $this->followingRedirects(); + + $response = $this->withoutExceptionHandling()->get('/', [ + 'X-Inertia' => 'true', + ]); + + $response->assertSuccessful(); + $response->assertContent('
'); + } +} diff --git a/tests/LazyPropTest.php b/tests/LazyPropTest.php index d8ae0e50..f1127328 100644 --- a/tests/LazyPropTest.php +++ b/tests/LazyPropTest.php @@ -2,8 +2,8 @@ namespace Inertia\Tests; -use Inertia\LazyProp; use Illuminate\Http\Request; +use Inertia\LazyProp; class LazyPropTest extends TestCase { diff --git a/tests/MergePropTest.php b/tests/MergePropTest.php new file mode 100644 index 00000000..dfd21213 --- /dev/null +++ b/tests/MergePropTest.php @@ -0,0 +1,34 @@ +assertSame('A merge prop value', $mergeProp()); + } + + public function test_can_invoke_with_a_non_callback(): void + { + $mergeProp = new MergeProp(['key' => 'value']); + + $this->assertSame(['key' => 'value'], $mergeProp()); + } + + public function test_can_resolve_bindings_when_invoked(): void + { + $mergeProp = new MergeProp(function (Request $request) { + return $request; + }); + + $this->assertInstanceOf(Request::class, $mergeProp()); + } +} diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index 4fb9f5d4..002499fa 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -2,20 +2,28 @@ namespace Inertia\Tests; -use LogicException; -use Inertia\Inertia; -use Inertia\Middleware; +use Illuminate\Filesystem\Filesystem; use Illuminate\Http\Request; -use Illuminate\Support\MessageBag; -use Illuminate\Support\ViewErrorBag; +use Illuminate\Session\Middleware\StartSession; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Session; -use Inertia\Tests\Stubs\ExampleMiddleware; -use Illuminate\Session\Middleware\StartSession; +use Illuminate\Support\MessageBag; +use Illuminate\Support\ViewErrorBag; use Inertia\AlwaysProp; +use Inertia\Inertia; +use Inertia\Middleware; +use Inertia\Tests\Stubs\ExampleMiddleware; +use LogicException; +use PHPUnit\Framework\Attributes\After; class MiddlewareTest extends TestCase { + #[After] + public function cleanupPublicFolder(): void + { + (new Filesystem)->cleanDirectory(public_path()); + } + public function test_no_response_value_by_default_means_automatically_redirecting_back_for_inertia_requests(): void { $fooCalled = false; @@ -145,7 +153,7 @@ public function test_validation_errors_can_be_empty(): void public function test_validation_errors_are_returned_in_the_correct_format(): void { - Session::put('errors', (new ViewErrorBag())->put('default', new MessageBag([ + Session::put('errors', (new ViewErrorBag)->put('default', new MessageBag([ 'name' => 'The name field is required.', 'email' => 'Not a valid email address.', ]))); @@ -163,7 +171,7 @@ public function test_validation_errors_are_returned_in_the_correct_format(): voi public function test_validation_errors_with_named_error_bags_are_scoped(): void { - Session::put('errors', (new ViewErrorBag())->put('example', new MessageBag([ + Session::put('errors', (new ViewErrorBag)->put('example', new MessageBag([ 'name' => 'The name field is required.', 'email' => 'Not a valid email address.', ]))); @@ -181,7 +189,7 @@ public function test_validation_errors_with_named_error_bags_are_scoped(): void public function test_default_validation_errors_can_be_overwritten(): void { - Session::put('errors', (new ViewErrorBag())->put('example', new MessageBag([ + Session::put('errors', (new ViewErrorBag)->put('example', new MessageBag([ 'name' => 'The name field is required.', 'email' => 'Not a valid email address.', ]))); @@ -198,7 +206,7 @@ public function test_default_validation_errors_can_be_overwritten(): void public function test_validation_errors_are_scoped_to_error_bag_header(): void { - Session::put('errors', (new ViewErrorBag())->put('default', new MessageBag([ + Session::put('errors', (new ViewErrorBag)->put('default', new MessageBag([ 'name' => 'The name field is required.', 'email' => 'Not a valid email address.', ]))); @@ -216,7 +224,8 @@ public function test_validation_errors_are_scoped_to_error_bag_header(): void public function test_middleware_can_change_the_root_view_via_a_property(): void { - $this->prepareMockEndpoint(null, [], new class() extends Middleware { + $this->prepareMockEndpoint(null, [], new class extends Middleware + { protected $rootView = 'welcome'; }); @@ -227,7 +236,8 @@ public function test_middleware_can_change_the_root_view_via_a_property(): void public function test_middleware_can_change_the_root_view_by_overriding_the_rootview_method(): void { - $this->prepareMockEndpoint(null, [], new class() extends Middleware { + $this->prepareMockEndpoint(null, [], new class extends Middleware + { public function rootView(Request $request): string { return 'welcome'; @@ -239,6 +249,49 @@ public function rootView(Request $request): string $response->assertViewIs('welcome'); } + public function test_determine_the_version_by_a_hash_of_the_asset_url(): void + { + config(['app.asset_url' => $url = 'https://example.com/assets']); + + $this->prepareMockEndpoint(middleware: new Middleware); + + $response = $this->get('/'); + $response->assertOk(); + $response->assertViewHas('page.version', hash('xxh128', $url)); + } + + public function test_determine_the_version_by_a_hash_of_the_vite_manifest(): void + { + $filesystem = new Filesystem; + $filesystem->ensureDirectoryExists(public_path('build')); + $filesystem->put( + public_path('build/manifest.json'), + $contents = json_encode(['vite' => true]) + ); + + $this->prepareMockEndpoint(middleware: new Middleware); + + $response = $this->get('/'); + $response->assertOk(); + $response->assertViewHas('page.version', hash('xxh128', $contents)); + } + + public function test_determine_the_version_by_a_hash_of_the_mix_manifest(): void + { + $filesystem = new Filesystem; + $filesystem->ensureDirectoryExists(public_path()); + $filesystem->put( + public_path('mix-manifest.json'), + $contents = json_encode(['mix' => true]) + ); + + $this->prepareMockEndpoint(middleware: new Middleware); + + $response = $this->get('/'); + $response->assertOk(); + $response->assertViewHas('page.version', hash('xxh128', $contents)); + } + private function prepareMockEndpoint($version = null, $shared = [], $middleware = null): \Illuminate\Routing\Route { if (is_null($middleware)) { diff --git a/tests/OptionalPropTest.php b/tests/OptionalPropTest.php new file mode 100644 index 00000000..932c803b --- /dev/null +++ b/tests/OptionalPropTest.php @@ -0,0 +1,27 @@ +assertSame('A lazy value', $optionalProp()); + } + + public function test_can_resolve_bindings_when_invoked(): void + { + $optionalProp = new OptionalProp(function (Request $request) { + return $request; + }); + + $this->assertInstanceOf(Request::class, $optionalProp()); + } +} diff --git a/tests/ResponseFactoryTest.php b/tests/ResponseFactoryTest.php index b86754b9..65e7df46 100644 --- a/tests/ResponseFactoryTest.php +++ b/tests/ResponseFactoryTest.php @@ -2,26 +2,29 @@ namespace Inertia\Tests; -use Inertia\Inertia; -use Inertia\LazyProp; -use Inertia\ResponseFactory; -use Illuminate\Http\Response; -use Illuminate\Http\RedirectResponse; -use Illuminate\Support\Facades\Route; -use Illuminate\Support\Facades\Request; -use Inertia\Tests\Stubs\ExampleMiddleware; use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request as HttpRequest; +use Illuminate\Http\Response; use Illuminate\Session\Middleware\StartSession; use Illuminate\Session\NullSessionHandler; use Illuminate\Session\Store; +use Illuminate\Support\Facades\Request; +use Illuminate\Support\Facades\Route; use Inertia\AlwaysProp; +use Inertia\DeferProp; +use Inertia\Inertia; +use Inertia\LazyProp; +use Inertia\MergeProp; +use Inertia\OptionalProp; +use Inertia\ResponseFactory; +use Inertia\Tests\Stubs\ExampleMiddleware; class ResponseFactoryTest extends TestCase { public function test_can_macro(): void { - $factory = new ResponseFactory(); + $factory = new ResponseFactory; $factory->macro('foo', function () { return 'bar'; }); @@ -35,7 +38,7 @@ public function test_location_response_for_inertia_requests(): void return true; }); - $response = (new ResponseFactory())->location('https://inertiajs.com'); + $response = (new ResponseFactory)->location('https://inertiajs.com'); $this->assertInstanceOf(Response::class, $response); $this->assertEquals(Response::HTTP_CONFLICT, $response->getStatusCode()); @@ -48,7 +51,7 @@ public function test_location_response_for_non_inertia_requests(): void return false; }); - $response = (new ResponseFactory())->location('https://inertiajs.com'); + $response = (new ResponseFactory)->location('https://inertiajs.com'); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); @@ -62,7 +65,7 @@ public function test_location_response_for_inertia_requests_using_redirect_respo }); $redirect = new RedirectResponse('https://inertiajs.com'); - $response = (new ResponseFactory())->location($redirect); + $response = (new ResponseFactory)->location($redirect); $this->assertInstanceOf(Response::class, $response); $this->assertEquals(409, $response->getStatusCode()); @@ -72,7 +75,7 @@ public function test_location_response_for_inertia_requests_using_redirect_respo public function test_location_response_for_non_inertia_requests_using_redirect_response(): void { $redirect = new RedirectResponse('https://inertiajs.com'); - $response = (new ResponseFactory())->location($redirect); + $response = (new ResponseFactory)->location($redirect); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); @@ -81,7 +84,7 @@ public function test_location_response_for_non_inertia_requests_using_redirect_r public function test_location_redirects_are_not_modified(): void { - $response = (new ResponseFactory())->location('/foo'); + $response = (new ResponseFactory)->location('/foo'); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); @@ -93,7 +96,7 @@ public function test_location_response_for_non_inertia_requests_using_redirect_r $redirect = new RedirectResponse('https://inertiajs.com'); $redirect->setSession($session = new Store('test', new NullSessionHandler)); $redirect->setRequest($request = new HttpRequest); - $response = (new ResponseFactory())->location($redirect); + $response = (new ResponseFactory)->location($redirect); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertEquals(Response::HTTP_FOUND, $response->getStatusCode()); @@ -109,7 +112,7 @@ public function test_the_version_can_be_a_closure(): void $this->assertSame('', Inertia::getVersion()); Inertia::version(function () { - return md5('Inertia'); + return hash('xxh128', 'Inertia'); }); return Inertia::render('User/Edit'); @@ -117,7 +120,7 @@ public function test_the_version_can_be_a_closure(): void $response = $this->withoutExceptionHandling()->get('/', [ 'X-Inertia' => 'true', - 'X-Inertia-Version' => 'b19a24ee5c287f42ee1d465dab77ab37', + 'X-Inertia-Version' => 'f445bd0a2c393a5af14fc677f59980a9', ]); $response->assertSuccessful(); @@ -143,6 +146,87 @@ public function test_shared_data_can_be_shared_from_anywhere(): void ]); } + public function test_dot_props_are_merged_from_shared(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::share('auth.user', [ + 'name' => 'Jonathan', + ]); + + return Inertia::render('User/Edit', [ + 'auth.user.can.create_group' => false, + ]); + }); + + $response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'props' => [ + 'auth' => [ + 'user' => [ + 'name' => 'Jonathan', + 'can' => [ + 'create_group' => false, + ], + ], + ], + ], + ]); + } + + public function test_shared_data_can_resolve_closure_arguments(): void + { + Inertia::share('query', fn (HttpRequest $request) => $request->query()); + + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + return Inertia::render('User/Edit'); + }); + + $response = $this->withoutExceptionHandling()->get('/?foo=bar', ['X-Inertia' => 'true']); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'props' => [ + 'query' => [ + 'foo' => 'bar', + ], + ], + ]); + } + + public function test_dot_props_with_callbacks_are_merged_from_shared(): void + { + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { + Inertia::share('auth.user', fn () => [ + 'name' => 'Jonathan', + ]); + + return Inertia::render('User/Edit', [ + 'auth.user.can.create_group' => false, + ]); + }); + + $response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']); + + $response->assertSuccessful(); + $response->assertJson([ + 'component' => 'User/Edit', + 'props' => [ + 'auth' => [ + 'user' => [ + 'name' => 'Jonathan', + 'can' => [ + 'create_group' => false, + ], + ], + ], + ], + ]); + } + public function test_can_flush_shared_data(): void { Inertia::share('foo', 'bar'); @@ -153,7 +237,7 @@ public function test_can_flush_shared_data(): void public function test_can_create_lazy_prop(): void { - $factory = new ResponseFactory(); + $factory = new ResponseFactory; $lazyProp = $factory->lazy(function () { return 'A lazy value'; }); @@ -161,9 +245,81 @@ public function test_can_create_lazy_prop(): void $this->assertInstanceOf(LazyProp::class, $lazyProp); } + public function test_can_create_deferred_prop(): void + { + $factory = new ResponseFactory; + $deferredProp = $factory->defer(function () { + return 'A deferred value'; + }); + + $this->assertInstanceOf(DeferProp::class, $deferredProp); + $this->assertSame($deferredProp->group(), 'default'); + } + + public function test_can_create_deferred_prop_with_custom_group(): void + { + $factory = new ResponseFactory; + $deferredProp = $factory->defer(function () { + return 'A deferred value'; + }, 'foo'); + + $this->assertInstanceOf(DeferProp::class, $deferredProp); + $this->assertSame($deferredProp->group(), 'foo'); + } + + public function test_can_create_merged_prop(): void + { + $factory = new ResponseFactory; + $mergedProp = $factory->merge(function () { + return 'A merged value'; + }); + + $this->assertInstanceOf(MergeProp::class, $mergedProp); + } + + public function test_can_create_deep_merged_prop(): void + { + $factory = new ResponseFactory; + $mergedProp = $factory->deepMerge(function () { + return 'A merged value'; + }); + + $this->assertInstanceOf(MergeProp::class, $mergedProp); + } + + public function test_can_create_deferred_and_merged_prop(): void + { + $factory = new ResponseFactory; + $deferredProp = $factory->defer(function () { + return 'A deferred + merged value'; + })->merge(); + + $this->assertInstanceOf(DeferProp::class, $deferredProp); + } + + public function test_can_create_deferred_and_deep_merged_prop(): void + { + $factory = new ResponseFactory; + $deferredProp = $factory->defer(function () { + return 'A deferred + merged value'; + })->deepMerge(); + + $this->assertInstanceOf(DeferProp::class, $deferredProp); + } + + public function test_can_create_optional_prop(): void + { + $factory = new ResponseFactory; + $optionalProp = $factory->optional(function () { + return 'An optional value'; + }); + + $this->assertInstanceOf(OptionalProp::class, $optionalProp); + } + public function test_can_create_always_prop(): void { - $factory = new ResponseFactory(); + $factory = new ResponseFactory; $alwaysProp = $factory->always(function () { return 'An always value'; }); @@ -176,7 +332,8 @@ public function test_will_accept_arrayabe_props() Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { Inertia::share('foo', 'bar'); - return Inertia::render('User/Edit', new class() implements Arrayable { + return Inertia::render('User/Edit', new class implements Arrayable + { public function toArray() { return [ diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index d36cdaae..e3a9f25a 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -2,20 +2,23 @@ namespace Inertia\Tests; -use Mockery; -use Inertia\LazyProp; -use Inertia\Response; -use Illuminate\View\View; -use Illuminate\Http\Request; -use Illuminate\Support\Fluent; +use Illuminate\Contracts\Support\Arrayable; use Illuminate\Http\JsonResponse; -use Illuminate\Support\Collection; -use Inertia\Tests\Stubs\FakeResource; -use Illuminate\Http\Response as BaseResponse; -use Illuminate\Pagination\LengthAwarePaginator; +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; +use Illuminate\Support\Collection; +use Illuminate\Support\Fluent; +use Illuminate\View\View; use Inertia\AlwaysProp; +use Inertia\DeferProp; +use Inertia\LazyProp; +use Inertia\MergeProp; +use Inertia\Response; +use Inertia\Tests\Stubs\FakeResource; +use Mockery; class ResponseTest extends TestCase { @@ -46,7 +49,275 @@ public function test_server_response(): void $this->assertSame('Jonathan', $page['props']['user']['name']); $this->assertSame('/user/123', $page['url']); $this->assertSame('123', $page['version']); - $this->assertSame('
', $view->render()); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('
', $view->render()); + } + + public function test_server_response_with_deferred_prop(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => new DeferProp(function () { + return 'bar'; + }, 'default'), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'default' => ['foo'], + ], $page['deferredProps']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('
', $view->render()); + } + + public function test_server_response_with_deferred_prop_and_multiple_groups(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => new DeferProp(function () { + return 'foo value'; + }, 'default'), + 'bar' => new DeferProp(function () { + return 'bar value'; + }, 'default'), + 'baz' => new DeferProp(function () { + return 'baz value'; + }, 'custom'), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'default' => ['foo', 'bar'], + 'custom' => ['baz'], + ], $page['deferredProps']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('
', $view->render()); + } + + public function test_server_response_with_merge_props(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => new MergeProp('foo value'), + 'bar' => new MergeProp('bar value'), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'foo', + 'bar', + ], $page['mergeProps']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('
', $view->render()); + } + + public function test_server_response_with_deep_merge_props(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => (new MergeProp('foo value'))->deepMerge(), + 'bar' => (new MergeProp('bar value'))->deepMerge(), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'foo', + 'bar', + ], $page['deepMergeProps']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('
', $view->render()); + } + + public function test_server_response_with_merge_strategies(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => (new MergeProp('foo value', ['foo-key']))->deepMerge(), + 'bar' => (new MergeProp('bar value', ['bar-key']))->deepMerge(), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'foo', + 'bar', + ], $page['deepMergeProps']); + $this->assertSame([ + 'foo.foo-key', + 'bar.bar-key', + ], $page['mergeStrategies']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('
', $view->render()); + } + + public function test_server_response_with_defer_and_merge_props(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => (new DeferProp(function () { + return 'foo value'; + }, 'default'))->merge(), + 'bar' => new MergeProp('bar value'), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'default' => ['foo'], + ], $page['deferredProps']); + $this->assertSame([ + 'foo', + 'bar', + ], $page['mergeProps']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('
', $view->render()); + } + + public function test_server_response_with_defer_and_deep_merge_props(): void + { + $request = Request::create('/user/123', 'GET'); + + $user = ['name' => 'Jonathan']; + $response = new Response( + 'User/Edit', + [ + 'user' => $user, + 'foo' => (new DeferProp(function () { + return 'foo value'; + }, 'default'))->deepMerge(), + 'bar' => (new MergeProp('bar value'))->deepMerge(), + ], + 'app', + '123' + ); + $response = $response->toResponse($request); + $view = $response->getOriginalContent(); + $page = $view->getData()['page']; + + $this->assertInstanceOf(BaseResponse::class, $response); + $this->assertInstanceOf(View::class, $view); + + $this->assertSame('User/Edit', $page['component']); + $this->assertSame('Jonathan', $page['props']['user']['name']); + $this->assertSame('/user/123', $page['url']); + $this->assertSame('123', $page['version']); + $this->assertSame([ + 'default' => ['foo'], + ], $page['deferredProps']); + $this->assertSame([ + 'foo', + 'bar', + ], $page['deepMergeProps']); + $this->assertFalse($page['clearHistory']); + $this->assertFalse($page['encryptHistory']); + $this->assertSame('
', $view->render()); } public function test_xhr_response(): void @@ -84,6 +355,54 @@ public function test_resource_response(): void $this->assertSame('123', $page->version); } + public function test_lazy_callable_resource_response(): void + { + $request = Request::create('/users', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + + $response = new Response('User/Index', [ + 'users' => fn () => [['name' => 'Jonathan']], + 'organizations' => fn () => [['name' => 'Inertia']], + ], 'app', '123'); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame('User/Index', $page->component); + $this->assertSame('/users', $page->url); + $this->assertSame('123', $page->version); + tap($page->props->users, function ($users) { + $this->assertSame(json_encode([['name' => 'Jonathan']]), json_encode($users)); + }); + tap($page->props->organizations, function ($organizations) { + $this->assertSame(json_encode([['name' => 'Inertia']]), json_encode($organizations)); + }); + } + + public function test_lazy_callable_resource_partial_response(): void + { + $request = Request::create('/users', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + $request->headers->add(['X-Inertia-Partial-Data' => 'users']); + $request->headers->add(['X-Inertia-Partial-Component' => 'User/Index']); + + $response = new Response('User/Index', [ + 'users' => fn () => [['name' => 'Jonathan']], + 'organizations' => fn () => [['name' => 'Inertia']], + ], 'app', '123'); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame('User/Index', $page->component); + $this->assertSame('/users', $page->url); + $this->assertSame('123', $page->version); + $this->assertFalse(property_exists($page->props, 'organizations')); + tap($page->props->users, function ($users) { + $this->assertSame(json_encode([['name' => 'Jonathan']]), json_encode($users)); + }); + } + public function test_lazy_resource_response(): void { $request = Request::create('/users', 'GET', ['page' => 1]); @@ -98,8 +417,7 @@ 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, JsonResource::class) extends ResourceCollection {}; }; $response = new Response('User/Index', ['users' => $callable], 'app', '123'); @@ -381,6 +699,31 @@ public function test_lazy_props_are_included_in_partial_reload(): void $this->assertSame('A lazy value', $page->props->lazy); } + public function test_defer_arrayable_props_are_resolved_in_partial_reload(): void + { + $request = Request::create('/users', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + $request->headers->add(['X-Inertia-Partial-Component' => 'Users']); + $request->headers->add(['X-Inertia-Partial-Data' => 'defer']); + + $deferProp = new DeferProp(function () { + return new class implements Arrayable + { + public function toArray() + { + return ['foo' => 'bar']; + } + }; + }); + + $response = new Response('Users', ['users' => [], 'defer' => $deferProp], 'app', '123'); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertFalse(property_exists($page->props, 'users')); + $this->assertEquals((object) ['foo' => 'bar'], $page->props->defer); + } + public function test_always_props_are_included_on_partial_reload(): void { $request = Request::create('/user/123', 'GET'); @@ -400,9 +743,9 @@ public function test_always_props_are_included_on_partial_reload(): void ], 'errors' => new AlwaysProp(function () { return [ - 'name' => 'The email field is required.' + 'name' => 'The email field is required.', ]; - }) + }), ]; $response = new Response('User/Edit', $props, 'app', '123'); @@ -523,4 +866,52 @@ public function test_the_page_url_doesnt_double_up(): void $this->assertSame('/subpath/product/123', $page->url); } + + public function test_trailing_slashes_in_a_url_are_preserved(): void + { + $request = Request::create('/users/', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + + $response = new Response('User/Index', []); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertSame('/users/', $page->url); + } + + public function test_trailing_slashes_in_a_url_with_query_parameters_are_preserved(): void + { + $request = Request::create('/users/?page=1&sort=name', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + + $response = new Response('User/Index', []); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertSame('/users/?page=1&sort=name', $page->url); + } + + public function test_a_url_without_trailing_slash_is_resolved_correctly(): void + { + $request = Request::create('/users', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + + $response = new Response('User/Index', []); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertSame('/users', $page->url); + } + + public function test_a_url_without_trailing_slash_and_query_parameters_is_resolved_correctly(): void + { + $request = Request::create('/users?page=1&sort=name', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + + $response = new Response('User/Index', []); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertSame('/users?page=1&sort=name', $page->url); + } } diff --git a/tests/ServiceProviderTest.php b/tests/ServiceProviderTest.php index 28fe55fe..efb270b4 100644 --- a/tests/ServiceProviderTest.php +++ b/tests/ServiceProviderTest.php @@ -30,10 +30,13 @@ public function test_route_macro_is_registered(): void $routes = Route::getRoutes(); $this->assertNotEmpty($routes->getRoutes()); - $this->assertEquals($route, $routes->getRoutes()[0]); - $this->assertEquals(['GET', 'HEAD'], $route->methods); - $this->assertEquals('/', $route->uri); - $this->assertEquals(['uses' => '\Inertia\Controller@__invoke', 'controller' => '\Inertia\Controller'], $route->action); - $this->assertEquals(['component' => 'User/Edit', 'props' => ['user' => ['name' => 'Jonathan']]], $route->defaults); + + $inertiaRoute = collect($routes->getRoutes())->first(fn ($route) => $route->uri === '/'); + + $this->assertEquals($route, $inertiaRoute); + $this->assertEquals(['GET', 'HEAD'], $inertiaRoute->methods); + $this->assertEquals('/', $inertiaRoute->uri); + $this->assertEquals(['uses' => '\Inertia\Controller@__invoke', 'controller' => '\Inertia\Controller'], $inertiaRoute->action); + $this->assertEquals(['component' => 'User/Edit', 'props' => ['user' => ['name' => 'Jonathan']]], $inertiaRoute->defaults); } } diff --git a/tests/Stubs/ExampleMiddleware.php b/tests/Stubs/ExampleMiddleware.php index 16ef7885..bb5e5305 100644 --- a/tests/Stubs/ExampleMiddleware.php +++ b/tests/Stubs/ExampleMiddleware.php @@ -2,9 +2,9 @@ namespace Inertia\Tests\Stubs; -use LogicException; -use Inertia\Middleware; use Illuminate\Http\Request; +use Inertia\Middleware; +use LogicException; use Symfony\Component\HttpFoundation\Response; class ExampleMiddleware extends Middleware diff --git a/tests/Stubs/FakeGateway.php b/tests/Stubs/FakeGateway.php index 93190a25..804d230d 100644 --- a/tests/Stubs/FakeGateway.php +++ b/tests/Stubs/FakeGateway.php @@ -2,9 +2,9 @@ namespace Inertia\Tests\Stubs; +use Illuminate\Support\Facades\Config; use Inertia\Ssr\Gateway; use Inertia\Ssr\Response; -use Illuminate\Support\Facades\Config; class FakeGateway implements Gateway { diff --git a/tests/Stubs/FakeResource.php b/tests/Stubs/FakeResource.php index 4525fdd1..a80c216a 100644 --- a/tests/Stubs/FakeResource.php +++ b/tests/Stubs/FakeResource.php @@ -29,7 +29,7 @@ public function __construct(array $resource) /** * Transform the resource into an array. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request */ public function toArray($request): array { diff --git a/tests/TestCase.php b/tests/TestCase.php index a4df1131..9e2a8833 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,13 +2,11 @@ namespace Inertia\Tests; -use LogicException; -use Inertia\Inertia; -use Inertia\ServiceProvider; use Illuminate\Support\Facades\View; use Illuminate\Testing\TestResponse; +use Inertia\Inertia; +use Inertia\ServiceProvider; use Orchestra\Testbench\TestCase as Orchestra; -use Illuminate\Foundation\Testing\TestResponse as LegacyTestResponse; abstract class TestCase extends Orchestra { @@ -19,7 +17,7 @@ protected function getPackageProviders($app): array ]; } - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -30,26 +28,7 @@ public function setUp(): void config()->set('inertia.testing.page_paths', [realpath(__DIR__)]); } - /** - * @throws LogicException - */ - protected function getTestResponseClass(): string - { - // Laravel >= 7.0 - if (class_exists(TestResponse::class)) { - return TestResponse::class; - } - - // Laravel <= 6.0 - if (class_exists(LegacyTestResponse::class)) { - return LegacyTestResponse::class; - } - - throw new LogicException('Could not detect TestResponse class.'); - } - - /** @returns TestResponse|LegacyTestResponse */ - protected function makeMockRequest($view) + protected function makeMockRequest($view): TestResponse { app('router')->get('/example-url', function () use ($view) { return $view; diff --git a/tests/Testing/AssertableInertiaTest.php b/tests/Testing/AssertableInertiaTest.php index 613cccc2..913b7e1e 100644 --- a/tests/Testing/AssertableInertiaTest.php +++ b/tests/Testing/AssertableInertiaTest.php @@ -8,8 +8,7 @@ class AssertableInertiaTest extends TestCase { - /** @test */ - public function the_view_is_served_by_inertia(): void + public function test_the_view_is_served_by_inertia(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -18,8 +17,7 @@ public function the_view_is_served_by_inertia(): void $response->assertInertia(); } - /** @test */ - public function the_view_is_not_served_by_inertia(): void + public function test_the_view_is_not_served_by_inertia(): void { $response = $this->makeMockRequest(view('welcome')); $response->assertOk(); // Make sure we can render the built-in Orchestra 'welcome' view.. @@ -30,8 +28,7 @@ public function the_view_is_not_served_by_inertia(): void $response->assertInertia(); } - /** @test */ - public function the_component_matches(): void + public function test_the_component_matches(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -42,8 +39,7 @@ public function the_component_matches(): void }); } - /** @test */ - public function the_component_does_not_match(): void + public function test_the_component_does_not_match(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -57,8 +53,7 @@ public function the_component_does_not_match(): void }); } - /** @test */ - public function the_component_exists_on_the_filesystem(): void + public function test_the_component_exists_on_the_filesystem(): void { $response = $this->makeMockRequest( Inertia::render('Stubs/ExamplePage') @@ -70,8 +65,7 @@ public function the_component_exists_on_the_filesystem(): void }); } - /** @test */ - public function the_component_does_not_exist_on_the_filesystem(): void + public function test_the_component_does_not_exist_on_the_filesystem(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -86,8 +80,7 @@ public function the_component_does_not_exist_on_the_filesystem(): void }); } - /** @test */ - public function it_can_force_enable_the_component_file_existence(): void + public function test_it_can_force_enable_the_component_file_existence(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -102,8 +95,7 @@ public function it_can_force_enable_the_component_file_existence(): void }); } - /** @test */ - public function it_can_force_disable_the_component_file_existence_check(): void + public function test_it_can_force_disable_the_component_file_existence_check(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -116,8 +108,7 @@ public function it_can_force_disable_the_component_file_existence_check(): void }); } - /** @test */ - public function the_component_does_not_exist_on_the_filesystem_when_it_does_not_exist_relative_to_any_of_the_given_paths(): void + public function test_the_component_does_not_exist_on_the_filesystem_when_it_does_not_exist_relative_to_any_of_the_given_paths(): void { $response = $this->makeMockRequest( Inertia::render('fixtures/ExamplePage') @@ -133,8 +124,7 @@ public function the_component_does_not_exist_on_the_filesystem_when_it_does_not_ }); } - /** @test */ - public function the_component_does_not_exist_on_the_filesystem_when_it_does_not_have_one_of_the_configured_extensions(): void + public function test_the_component_does_not_exist_on_the_filesystem_when_it_does_not_have_one_of_the_configured_extensions(): void { $response = $this->makeMockRequest( Inertia::render('fixtures/ExamplePage') @@ -150,8 +140,7 @@ public function the_component_does_not_exist_on_the_filesystem_when_it_does_not_ }); } - /** @test */ - public function the_page_url_matches(): void + public function test_the_page_url_matches(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -162,8 +151,7 @@ public function the_page_url_matches(): void }); } - /** @test */ - public function the_page_url_does_not_match(): void + public function test_the_page_url_does_not_match(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -177,8 +165,7 @@ public function the_page_url_does_not_match(): void }); } - /** @test */ - public function the_asset_version_matches(): void + public function test_the_asset_version_matches(): void { Inertia::version('example-version'); @@ -191,8 +178,7 @@ public function the_asset_version_matches(): void }); } - /** @test */ - public function the_asset_version_does_not_match(): void + public function test_the_asset_version_does_not_match(): void { Inertia::version('example-version'); diff --git a/tests/Testing/TestResponseMacrosTest.php b/tests/Testing/TestResponseMacrosTest.php index c86fe0de..ed2e7764 100644 --- a/tests/Testing/TestResponseMacrosTest.php +++ b/tests/Testing/TestResponseMacrosTest.php @@ -2,14 +2,14 @@ namespace Inertia\Tests\Testing; +use Illuminate\Testing\Fluent\AssertableJson; +use Illuminate\Testing\TestResponse; use Inertia\Inertia; use Inertia\Tests\TestCase; -use Illuminate\Testing\Fluent\AssertableJson; class TestResponseMacrosTest extends TestCase { - /** @test */ - public function it_can_make_inertia_assertions(): void + public function test_it_can_make_inertia_assertions(): void { $response = $this->makeMockRequest( Inertia::render('foo') @@ -24,21 +24,19 @@ public function it_can_make_inertia_assertions(): void $this->assertTrue($success); } - /** @test */ - public function it_preserves_the_ability_to_continue_chaining_laravel_test_response_calls(): void + public function test_it_preserves_the_ability_to_continue_chaining_laravel_test_response_calls(): void { $response = $this->makeMockRequest( Inertia::render('foo') ); $this->assertInstanceOf( - $this->getTestResponseClass(), + TestResponse::class, $response->assertInertia() ); } - /** @test */ - public function it_can_retrieve_the_inertia_page(): void + public function test_it_can_retrieve_the_inertia_page(): void { $response = $this->makeMockRequest( Inertia::render('foo', ['bar' => 'baz']) @@ -49,6 +47,34 @@ public function it_can_retrieve_the_inertia_page(): void $this->assertSame(['bar' => 'baz'], $page['props']); $this->assertSame('/example-url', $page['url']); $this->assertSame('', $page['version']); + $this->assertFalse($page['encryptHistory']); + $this->assertFalse($page['clearHistory']); }); } + + public function test_it_can_retrieve_the_inertia_props(): void + { + $props = ['bar' => 'baz']; + $response = $this->makeMockRequest( + Inertia::render('foo', $props) + ); + + $this->assertSame($props, $response->inertiaProps()); + } + + public function test_it_can_retrieve_nested_inertia_prop_values_with_dot_notation(): void + { + $response = $this->makeMockRequest( + Inertia::render('foo', [ + 'bar' => ['baz' => 'qux'], + 'users' => [ + ['name' => 'John'], + ['name' => 'Jane'], + ], + ]) + ); + + $this->assertSame('qux', $response->inertiaProps('bar.baz')); + $this->assertSame('John', $response->inertiaProps('users.0.name')); + } }