Skip to content

Commit

Permalink
Add support for nested controllers
Browse files Browse the repository at this point in the history
  • Loading branch information
nicodevs committed Jan 2, 2025
1 parent 174d9f1 commit 34fb465
Show file tree
Hide file tree
Showing 16 changed files with 404 additions and 12 deletions.
25 changes: 15 additions & 10 deletions src/Generators/ControllerGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,25 +82,19 @@ protected function buildMethods(Controller $controller): string

foreach ($controller->methods() as $name => $statements) {
$method = str_replace('{{ method }}', $name, $template);
$search = '(Request $request';

if (in_array($name, ['edit', 'update', 'show', 'destroy'])) {
$reference = $this->fullyQualifyModelReference($controller->namespace(), $controllerModelName);
$variable = '$' . Str::camel($controllerModelName);

$search = '(Request $request';
$method = str_replace($search, $search . ', ' . $controllerModelName . ' ' . $variable, $method);
$this->addImport($controller, $reference);
}

if ($controller->parent()) {
$parentControllerModelName = Str::studly(Str::singular($controller->parent()));

$parentReference = $this->fullyQualifyModelReference($controller->namespace(), $parentControllerModelName);
$variable = '$' . Str::camel($parentControllerModelName);

$search = '(Request $request';
$method = str_replace($search, $search . ', ' . $parentControllerModelName . ' ' . $variable, $method);
$this->addImport($controller, $parentReference);
if ($parent = $controller->parent()) {
$method = str_replace($search, $search . ', ' . $parent . ' $' . Str::camel($parent), $method);
$this->addImport($controller, $this->fullyQualifyModelReference($controller->namespace(), $parent));
}

$body = '';
Expand Down Expand Up @@ -192,6 +186,17 @@ protected function buildMethods(Controller $controller): string
$this->addImport($controller, 'Inertia\Inertia');
}

if (
$controller->parent() &&
($statement instanceof QueryStatement || $statement instanceof EloquentStatement || $statement instanceof ResourceStatement)
) {
$body = str_replace(
['::all', Str::singular($controller->prefix()) . '::'],
['::get', '$' . Str::lower($controller->parent()) . '->' . Str::plural(Str::lower($controller->prefix())) . '()->'],
$body
);
}

$body .= PHP_EOL;
}

Expand Down
21 changes: 21 additions & 0 deletions src/Generators/PestTestGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ protected function buildTestCases(Controller $controller): string
$setup['data'][] = sprintf('$%s = %s::factory()->create();', $variable, $model);
}

if ($parent = $controller->parent()) {
$this->addImport($controller, $modelNamespace . '\\' . $parent);
$setup['data'][] = sprintf('$%s = %s::factory()->create();', Str::camel($parent), $parent);
}

foreach ($statements as $statement) {
if ($statement instanceof SendStatement) {
if ($statement->isNotification()) {
Expand Down Expand Up @@ -493,6 +498,22 @@ protected function buildTestCases(Controller $controller): string
}
$call .= ')';

if ($controller->parent()) {
$parent = Str::camel($controller->parent());
$variable = Str::camel($context);
$binding = sprintf(', $%s)', $variable);
$params = sprintf("'%s' => $%s", $parent, $parent);

if (Str::contains($call, $binding)) {
$params .= sprintf(", '%s' => $%s", $variable, $variable);
$search = $binding;
} else {
$search = ')';
}

$call = str_replace($search, sprintf(', [%s])', $params), $call);
}

if ($request_data) {
$call .= ', [';
$call .= PHP_EOL;
Expand Down
21 changes: 21 additions & 0 deletions src/Generators/PhpUnitTestGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ protected function buildTestCases(Controller $controller): string
$setup['data'][] = sprintf('$%s = %s::factory()->create();', $variable, $model);
}

if ($parent = $controller->parent()) {
$this->addImport($controller, $modelNamespace . '\\' . $parent);
$setup['data'][] = sprintf('$%s = %s::factory()->create();', Str::camel($parent), $parent);
}

foreach ($statements as $statement) {
if ($statement instanceof SendStatement) {
if ($statement->isNotification()) {
Expand Down Expand Up @@ -486,6 +491,22 @@ protected function buildTestCases(Controller $controller): string
}
$call .= ')';

if ($controller->parent()) {
$parent = Str::camel($controller->parent());
$variable = Str::camel($context);
$binding = sprintf(', $%s)', $variable);
$params = sprintf("'%s' => $%s", $parent, $parent);

if (Str::contains($call, $binding)) {
$params .= sprintf(", '%s' => $%s", $variable, $variable);
$search = $binding;
} else {
$search = ')';
}

$call = str_replace($search, sprintf(', [%s])', $params), $call);
}

if ($request_data) {
$call .= ', [';
$call .= PHP_EOL;
Expand Down
6 changes: 6 additions & 0 deletions src/Generators/RouteGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ protected function buildRoutes(Controller $controller): string
$className = $this->getClassName($controller);
$slug = config('blueprint.singular_routes') ? Str::kebab($controller->prefix()) : Str::plural(Str::kebab($controller->prefix()));

if ($controller->parent()) {
$parentSlug = config('blueprint.singular_routes') ? Str::kebab($controller->parent()) : Str::plural(Str::kebab($controller->parent()));
$parentBinding = '/{' . Str::kebab($controller->parent()) . '}/';
$slug = $parentSlug . $parentBinding . $slug;
}

foreach (array_diff($methods, Controller::$resourceMethods) as $method) {
$routes .= $this->buildRouteLine($className, $slug, $method);
$routes .= PHP_EOL;
Expand Down
2 changes: 1 addition & 1 deletion src/Models/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public function isApiResource(): bool

public function setParent(string $parent): void
{
$this->parent = $parent;
$this->parent = Str::studly(Str::singular($parent));
}

public function parent(): ?string
Expand Down
1 change: 1 addition & 0 deletions tests/Feature/Generators/ControllerGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ public static function controllerTreeDataProvider(): array
['drafts/inertia-render.yaml', 'app/Http/Controllers/CustomerController.php', 'controllers/inertia-render.php'],
['drafts/save-without-validation.yaml', 'app/Http/Controllers/PostController.php', 'controllers/save-without-validation.php'],
['drafts/api-resource-pagination.yaml', 'app/Http/Controllers/PostController.php', 'controllers/api-resource-pagination.php'],
['drafts/api-resource-nested.yaml', 'app/Http/Controllers/CommentController.php', 'controllers/api-resource-nested.php'],
['drafts/api-routes-example.yaml', 'app/Http/Controllers/Api/CertificateController.php', 'controllers/api-routes-example.php'],
['drafts/invokable-controller.yaml', 'app/Http/Controllers/ReportController.php', 'controllers/invokable-controller.php'],
['drafts/invokable-controller-shorthand.yaml', 'app/Http/Controllers/ReportController.php', 'controllers/invokable-controller-shorthand.php'],
Expand Down
1 change: 1 addition & 0 deletions tests/Feature/Generators/PestTestGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ public static function controllerTreeDataProvider(): array
['drafts/model-reference-validate.yaml', 'tests/Feature/Http/Controllers/CertificateControllerTest.php', 'tests/pest/api-shorthand-validation.php'],
['drafts/controllers-only-no-context.yaml', 'tests/Feature/Http/Controllers/ReportControllerTest.php', 'tests/pest/controllers-only-no-context.php'],
['drafts/date-formats.yaml', 'tests/Feature/Http/Controllers/DateControllerTest.php', 'tests/pest/date-formats.php'],
['drafts/api-resource-nested.yaml', 'tests/Feature/Http/Controllers/CommentControllerTest.php', 'tests/pest/api-resource-nested.php'],
['drafts/call-to-a-member-function-columns-on-null.yaml', [
'tests/Feature/Http/Controllers/SubscriptionControllerTest.php',
'tests/Feature/Http/Controllers/TelegramControllerTest.php',
Expand Down
3 changes: 2 additions & 1 deletion tests/Feature/Generators/PhpUnitTestGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public function output_generates_test_for_controller_tree($definition, $path, $t
$this->filesystem->expects('makeDirectory')
->with($dirname, 0755, true);

$this->filesystem->expects('put')
$this->filesystem->shouldReceive('put')
->with($path, $this->fixture($test));
}

Expand Down Expand Up @@ -226,6 +226,7 @@ public static function controllerTreeDataProvider(): array
['drafts/controllers-only-no-context.yaml', 'tests/Feature/Http/Controllers/ReportControllerTest.php', 'tests/phpunit/controllers-only-no-context.php'],
['drafts/date-formats.yaml', 'tests/Feature/Http/Controllers/DateControllerTest.php', 'tests/phpunit/date-formats.php'],
['drafts/test-relationships.yaml', 'tests/Feature/Http/Controllers/ConferenceControllerTest.php', 'tests/phpunit/test-relationships.php'],
['drafts/api-resource-nested.yaml', 'tests/Feature/Http/Controllers/CommentControllerTest.php', 'tests/phpunit/api-resource-nested.php'],
['drafts/call-to-a-member-function-columns-on-null.yaml', [
'tests/Feature/Http/Controllers/SubscriptionControllerTest.php',
'tests/Feature/Http/Controllers/TelegramControllerTest.php',
Expand Down
1 change: 1 addition & 0 deletions tests/Feature/Generators/RouteGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ public static function controllerTreeDataProvider(): array
['drafts/respond-statements.yaml', 'routes/respond-statements.php'],
['drafts/invokable-controller.yaml', 'routes/invokable-controller.php'],
['drafts/invokable-controller-shorthand.yaml', 'routes/invokable-controller.php'],
['drafts/controller-nested.yaml', 'routes/nested-controller.php'],
];
}
}
26 changes: 26 additions & 0 deletions tests/Feature/Lexers/ControllerLexerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -486,4 +486,30 @@ public function it_returns_an_authorized_controller_with_specific_policies(): vo
$this->assertInstanceOf(Policy::class, $controller->policy());
$this->assertEquals(['viewAny', 'view'], $controller->policy()->methods());
}

#[Test]
public function it_returns_a_nested_controller(): void
{
$tokens = [
'controllers' => [
'Comment' => [
'resource' => 'api',
'meta' => [
'parent' => 'post',
],
],
],
];

$this->statementLexer->shouldReceive('analyze');

$actual = $this->subject->analyze($tokens);

$this->assertCount(1, $actual['controllers']);

$controller = $actual['controllers']['Comment'];
$this->assertEquals('CommentController', $controller->className());
$this->assertCount(5, $controller->methods());
$this->assertEquals($controller->parent(), 'Post');
}
}
48 changes: 48 additions & 0 deletions tests/fixtures/controllers/api-resource-nested.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace App\Http\Controllers;

use App\Http\Requests\CommentStoreRequest;
use App\Http\Requests\CommentUpdateRequest;
use App\Http\Resources\CommentCollection;
use App\Http\Resources\CommentResource;
use App\Models\Comment;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class CommentController extends Controller
{
public function index(Request $request, Post $post): CommentCollection
{
$comments = $post->comments()->get();

return new CommentCollection($comments);
}

public function store(CommentStoreRequest $request, Post $post): CommentResource
{
$comment = $post->comments()->create($request->validated());

return new CommentResource($comment);
}

public function show(Request $request, Post $post, Comment $comment): CommentResource
{
return new CommentResource($comment);
}

public function update(CommentUpdateRequest $request, Post $post, Comment $comment): CommentResource
{
$comment->update($request->validated());

return new CommentResource($comment);
}

public function destroy(Request $request, Post $post, Comment $comment): Response
{
$comment->delete();

return response()->noContent();
}
}
16 changes: 16 additions & 0 deletions tests/fixtures/drafts/api-resource-nested.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
models:
Post:
title: string
body: text
relationships:
hasMany: comment
belongsTo: user
Comment:
body: text
relationships:
belongsTo: post, user
controllers:
Comment:
resource: api
meta:
parent: post
5 changes: 5 additions & 0 deletions tests/fixtures/drafts/controller-nested.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
controllers:
Comment:
resource: web
meta:
parent: post
3 changes: 3 additions & 0 deletions tests/fixtures/routes/nested-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@


Route::resource('posts/{post}/comments', App\Http\Controllers\CommentController::class);
107 changes: 107 additions & 0 deletions tests/fixtures/tests/pest/api-resource-nested.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

namespace Tests\Feature\Http\Controllers;

use App\Models\Comment;
use App\Models\Post;
use App\Models\User;
use function Pest\Faker\fake;
use function Pest\Laravel\assertModelMissing;
use function Pest\Laravel\delete;
use function Pest\Laravel\get;
use function Pest\Laravel\post;
use function Pest\Laravel\put;

test('index behaves as expected', function (): void {
$post = Post::factory()->create();
$comments = Comment::factory()->count(3)->create();

$response = get(route('comments.index', ['post' => $post]));

$response->assertOk();
$response->assertJsonStructure([]);
});


test('store uses form request validation')
->assertActionUsesFormRequest(
\App\Http\Controllers\CommentController::class,
'store',
\App\Http\Requests\CommentStoreRequest::class
);

test('store saves', function (): void {
$post = Post::factory()->create();
$body = fake()->text();
$user = User::factory()->create();

$response = post(route('comments.store', ['post' => $post]), [
'body' => $body,
'post_id' => $post->id,
'user_id' => $user->id,
]);

$comments = Comment::query()
->where('body', $body)
->where('post_id', $post->id)
->where('user_id', $user->id)
->get();
expect($comments)->toHaveCount(1);
$comment = $comments->first();

$response->assertCreated();
$response->assertJsonStructure([]);
});


test('show behaves as expected', function (): void {
$comment = Comment::factory()->create();
$post = Post::factory()->create();

$response = get(route('comments.show', ['post' => $post, 'comment' => $comment]));

$response->assertOk();
$response->assertJsonStructure([]);
});


test('update uses form request validation')
->assertActionUsesFormRequest(
\App\Http\Controllers\CommentController::class,
'update',
\App\Http\Requests\CommentUpdateRequest::class
);

test('update behaves as expected', function (): void {
$comment = Comment::factory()->create();
$post = Post::factory()->create();
$body = fake()->text();
$user = User::factory()->create();

$response = put(route('comments.update', ['post' => $post, 'comment' => $comment]), [
'body' => $body,
'post_id' => $post->id,
'user_id' => $user->id,
]);

$comment->refresh();

$response->assertOk();
$response->assertJsonStructure([]);

expect($body)->toEqual($comment->body);
expect($post->id)->toEqual($comment->post_id);
expect($user->id)->toEqual($comment->user_id);
});


test('destroy deletes and responds with', function (): void {
$comment = Comment::factory()->create();
$post = Post::factory()->create();

$response = delete(route('comments.destroy', ['post' => $post, 'comment' => $comment]));

$response->assertNoContent();

assertModelMissing($comment);
});
Loading

0 comments on commit 34fb465

Please sign in to comment.