Skip to content

Commit

Permalink
[5.x] Add support for $view parameter closure with `Route::statamic…
Browse files Browse the repository at this point in the history
…()` (#11452)

Co-authored-by: Jason Varga <[email protected]>
  • Loading branch information
jesseleite and jasonvarga authored Feb 20, 2025
1 parent 25447a5 commit 4ace966
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 11 deletions.
33 changes: 32 additions & 1 deletion src/Http/Controllers/FrontendController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

namespace Statamic\Http\Controllers;

use Closure;
use Illuminate\Contracts\View\View as IlluminateView;
use Illuminate\Http\Request;
use ReflectionFunction;
use Statamic\Auth\Protect\Protection;
use Statamic\Exceptions\NotFoundHttpException;
use Statamic\Facades\Data;
Expand Down Expand Up @@ -39,9 +42,26 @@ public function index(Request $request)
public function route(Request $request, ...$args)
{
$params = $request->route()->parameters();

$view = Arr::pull($params, 'view');
$data = Arr::pull($params, 'data');
$data = array_merge($params, is_callable($data) ? $data(...$params) : $data);

throw_if(is_callable($view) && $data, new \Exception('Parameter [$data] not supported with [$view] closure!'));

if (is_callable($view)) {
$resolvedView = static::resolveRouteClosure($view, $params);
}

if (isset($resolvedView) && $resolvedView instanceof IlluminateView) {
$view = $resolvedView->name();
$data = $resolvedView->getData();
} elseif (isset($resolvedView)) {
return $resolvedView;
}

$data = array_merge($params, is_callable($data)
? static::resolveRouteClosure($data, $params)
: $data);

$view = app(View::class)
->template($view)
Expand Down Expand Up @@ -73,4 +93,15 @@ private function getLoadedRouteItem($data)
return $data;
}
}

private static function resolveRouteClosure(Closure $closure, array $params)
{
$reflect = new ReflectionFunction($closure);

$params = collect($reflect->getParameters())
->map(fn ($param) => $param->hasType() ? app($param->getType()->getName()) : $params[$param->getName()])
->all();

return $closure(...$params);
}
}
230 changes: 220 additions & 10 deletions tests/Routing/RoutesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Tests\Routing;

use Facades\Tests\Factories\EntryFactory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
Expand Down Expand Up @@ -33,16 +34,56 @@ protected function resolveApplicationConfiguration($app)
$app->booted(function () {
Route::statamic('/basic-route-with-data', 'test', ['hello' => 'world']);

Route::statamic('/basic-route-with-data-from-closure', 'test', function () {
Route::statamic('/basic-route-with-view-closure', function () {
return view('test', ['hello' => 'world']);
});

Route::statamic('/basic-route-with-view-closure-and-dependency-injection', function (Request $request, FooClass $foo) {
return view('test', ['hello' => "view closure dependencies: $request->value $foo->value"]);
});

Route::statamic('/basic-route-with-view-closure-and-custom-return', function () {
return ['message' => 'not a view instance'];
});

Route::statamic('/basic-route-with-data-closure', 'test', function () {
return ['hello' => 'world'];
});

Route::statamic('/basic-route-with-data-closure-and-dependency-injection', 'test', function (Request $request, FooClass $foo) {
return ['hello' => "data closure dependencies: $request->value $foo->value"];
});

Route::statamic('/you-cannot-use-data-param-with-view-closure', function () {
return view('test', ['hello' => 'world']);
}, 'hello');

Route::statamic('/basic-route-without-data', 'test');

Route::statamic('/route/with/placeholders/{foo}/{bar}/{baz}', 'test');

Route::statamic('/route/with/placeholders/closure/{foo}/{bar}/{baz}', 'test', function ($foo, $bar, $baz) {
return ['hello' => "$foo $bar $baz"];
Route::statamic('/route/with/placeholders/view/closure/{foo}/{bar}/{baz}', function ($foo, $bar, $baz) {
return view('test', ['hello' => "view closure placeholders: $foo $bar $baz"]);
});

Route::statamic('/route/with/placeholders/view/closure-dependency-injection/{baz}/{qux}', function (Request $request, FooClass $foo, BarClass $bar, $baz, $qux) {
return view('test', ['hello' => "view closure dependencies: $request->value $foo->value $bar->value $baz $qux"]);
});

Route::statamic('/route/with/placeholders/view/closure-dependency-order-doesnt-matter/{baz}/{qux}', function (FooClass $foo, $baz, BarClass $bar, Request $request, $qux) {
return view('test', ['hello' => "view closure dependencies: $request->value $foo->value $bar->value $baz $qux"]);
});

Route::statamic('/route/with/placeholders/data/closure/{foo}/{bar}/{baz}', 'test', function ($foo, $bar, $baz) {
return ['hello' => "data closure placeholders: $foo $bar $baz"];
});

Route::statamic('/route/with/placeholders/data/closure-dependency-injection/{baz}/{qux}', 'test', function (Request $request, FooClass $foo, BarClass $bar, $baz, $qux) {
return ['hello' => "data closure dependencies: $request->value $foo->value $bar->value $baz $qux"];
});

Route::statamic('/route/with/placeholders/data/closure-dependency-order-doesnt-matter/{baz}/{qux}', 'test', function (FooClass $foo, $baz, BarClass $bar, Request $request, $qux) {
return ['hello' => "data closure dependencies: $request->value $foo->value $bar->value $baz $qux"];
});

Route::statamic('/route-with-custom-layout', 'test', [
Expand Down Expand Up @@ -115,16 +156,108 @@ public function it_renders_a_view()
}

#[Test]
public function it_renders_a_view_with_data_from_a_closure()
public function it_renders_a_view_using_a_view_closure()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

$this->get('/basic-route-with-data-from-closure')
$this->get('/basic-route-with-view-closure')
->assertOk()
->assertSee('Hello world');
}

#[Test]
public function it_renders_a_view_using_a_view_closure_with_dependency_injection()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

$this->get('/basic-route-with-view-closure-and-dependency-injection?value=request_value')
->assertOk()
->assertSee('Hello view closure dependencies: request_value foo_class');
}

#[Test]
public function it_renders_a_view_using_a_view_closure_with_dependency_injection_from_container()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

app()->bind(FooClass::class, function () {
$foo = new FooClass;
$foo->value = 'foo_modified';

return $foo;
});

$this->get('/basic-route-with-view-closure-and-dependency-injection?value=request_value')
->assertOk()
->assertSee('Hello view closure dependencies: request_value foo_modified');
}

#[Test]
public function it_renders_a_view_using_a_custom_view_closure_that_does_not_return_a_view_instance()
{
$this->get('/basic-route-with-view-closure-and-custom-return')
->assertOk()
->assertJson([
'message' => 'not a view instance',
]);
}

#[Test]
public function it_renders_a_view_using_a_data_closure()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

$this->get('/basic-route-with-data-closure')
->assertOk()
->assertSee('Hello world');
}

#[Test]
public function it_renders_a_view_using_a_data_closure_with_dependency_injection()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

$this->get('/basic-route-with-data-closure-and-dependency-injection?value=request_value')
->assertOk()
->assertSee('Hello data closure dependencies: request_value foo_class');
}

#[Test]
public function it_renders_a_view_using_a_data_closure_with_dependency_injection_from_container()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

app()->bind(FooClass::class, function () {
$foo = new FooClass;
$foo->value = 'foo_modified';

return $foo;
});

$this->get('/basic-route-with-data-closure-and-dependency-injection?value=request_value')
->assertOk()
->assertSee('Hello data closure dependencies: request_value foo_modified');
}

#[Test]
public function it_throws_exception_if_you_try_to_pass_data_parameter_when_using_view_closure()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

$response = $this
->get('/you-cannot-use-data-param-with-view-closure')
->assertInternalServerError();

$this->assertEquals('Parameter [$data] not supported with [$view] closure!', $response->exception->getMessage());
}

#[Test]
public function it_renders_a_view_without_data()
{
Expand All @@ -148,14 +281,83 @@ public function it_renders_a_view_with_placeholders()
}

#[Test]
public function it_renders_a_view_with_placeholders_and_data_from_a_closure()
public function it_renders_a_view_with_placeholders_using_a_view_closure()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

$this->get('/route/with/placeholders/closure/one/two/three')
$this->get('/route/with/placeholders/view/closure/one/two/three')
->assertOk()
->assertSee('Hello one two three');
->assertSee('Hello view closure placeholders: one two three');
}

#[Test]
public function it_renders_a_view_with_placeholders_using_a_view_closure_with_dependency_injection()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

$this->get('/route/with/placeholders/view/closure-dependency-injection/one/two?value=request_value')
->assertOk()
->assertSee('Hello view closure dependencies: request_value foo_class bar_class one two');
}

#[Test]
public function it_renders_a_view_with_placeholders_using_a_view_closure_and_dependency_order_doesnt_matter()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

app()->bind(BarClass::class, function () {
$foo = new BarClass;
$foo->value = 'bar_class_modified';

return $foo;
});

$this->get('/route/with/placeholders/view/closure-dependency-order-doesnt-matter/one/two?value=request_value')
->assertOk()
->assertSee('Hello view closure dependencies: request_value foo_class bar_class_modified one two');
}

#[Test]
public function it_renders_a_view_with_placeholders_using_a_data_closure()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

$this->get('/route/with/placeholders/data/closure/one/two/three')
->assertOk()
->assertSee('Hello data closure placeholders: one two three');
}

#[Test]
public function it_renders_a_view_with_placeholders_using_a_data_closure_with_dependency_injection()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

$this->get('/route/with/placeholders/data/closure-dependency-injection/one/two?value=request_value')
->assertOk()
->assertSee('Hello data closure dependencies: request_value foo_class bar_class one two');
}

#[Test]
public function it_renders_a_view_with_placeholders_using_a_data_closure_and_dependency_order_doesnt_matter()
{
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

app()->bind(BarClass::class, function () {
$foo = new BarClass;
$foo->value = 'bar_class_modified';

return $foo;
});

$this->get('/route/with/placeholders/data/closure-dependency-order-doesnt-matter/one/two?value=request_value')
->assertOk()
->assertSee('Hello data closure dependencies: request_value foo_class bar_class_modified one two');
}

#[Test]
Expand All @@ -174,7 +376,6 @@ public function it_renders_a_view_with_custom_layout()
#[DataProvider('undefinedLayoutRouteProvider')]
public function it_renders_a_view_without_a_layout($route)
{
$this->withoutExceptionHandling();
$this->viewShouldReturnRaw('layout', 'The layout {{ template_content }}');
$this->viewShouldReturnRaw('test', 'Hello {{ hello }}');

Expand Down Expand Up @@ -222,7 +423,6 @@ public function it_loads_content_by_uri()
#[Test]
public function it_renders_a_view_with_custom_content_type()
{
$this->withoutExceptionHandling();
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('test', '{"hello":"{{ hello }}"}');

Expand Down Expand Up @@ -354,3 +554,13 @@ public function it_uses_a_non_default_layout()
->assertSee('Custom layout');
}
}

class FooClass
{
public $value = 'foo_class';
}

class BarClass
{
public $value = 'bar_class';
}

0 comments on commit 4ace966

Please sign in to comment.