In previous lessons, we have covered how to create an API controller, a service to wrap business logic, and write tests for both API and Web controllers.
Let's implement these features for Categories CRUD in a single lesson.
Create API Resources and Controller for Category Model.
php artisan make:resource Api/V1/Vendor/CategoryResource
php artisan make:resource Api/V1/Vendor/CategoryCollection
php artisan make:controller Api/V1/Vendor/CategoryController
app/Http/Controllers/Api/V1/Vendor/CategoryController.php
namespace App\Http\Controllers\Api\V1\Vendor; use App\Http\Controllers\Controller;use App\Http\Requests\Vendor\StoreCategoryRequest;use App\Http\Requests\Vendor\UpdateCategoryRequest;use App\Http\Resources\Api\V1\Vendor\CategoryCollection;use App\Http\Resources\Api\V1\Vendor\CategoryResource;use App\Models\Category;use Illuminate\Http\JsonResponse;use Illuminate\Http\Request;use Illuminate\Http\Response; class CategoryController extends Controller{ public function index(Request $request): CategoryCollection { $this->authorize('category.viewAny'); $categories = Category::when( $request->boolean('products'), fn ($q) => $q->with('products') ) ->where('restaurant_id', auth()->user()->restaurant->id) ->get(); return new CategoryCollection($categories); } public function store(StoreCategoryRequest $request): CategoryResource { $restaurant = $request->user()->restaurant; $category = $restaurant->categories()->create($request->validated()); return new CategoryResource($category); } public function show(Category $category) { $this->authorize('category.view'); return new CategoryResource($category); } public function update(UpdateCategoryRequest $request, Category $category): JsonResponse { $category->update($request->validated()); return (new CategoryResource($category)) ->response() ->setStatusCode(Response::HTTP_ACCEPTED); } public function destroy(Category $category): Response { $this->authorize('category.delete'); $category->delete(); return response()->noContent(); }}
We have the conditional clause when()
in the' index' method. It is helpful for two cases when building an API.
/api/v1/vendor/categories
endpoint. Useful for dropdown lists.products=true
parameter returns categories with products. Useful to display the whole menu. Example: /api/v1/vendor/categories?products=true
.$categories = Category::when( $request->boolean('products'), fn ($q) => $q->with('products')) // ...
You can check more on Conditional Clauses on the Laravel documentation. Also, we have another Less Known Conditional Queries tutorial.
Now create new API routes file for the vendor.
routes/api/v1/vendor.php
use App\Http\Controllers\Api\V1\Vendor\CategoryController;use Illuminate\Support\Facades\Route; Route::group([ 'prefix' => 'vendor', 'as' => 'vendor.', 'middleware' => ['auth:sanctum'],], function () { Route::apiResource('categories', CategoryController::class);});
And include it in the main api.php
file.
routes/api.php
// ... include __DIR__ . '/api/v1/admin.php';include __DIR__ . '/api/v1/vendor.php';
Create a new CategoryService
class.
app/Services/CategoryService.php
namespace App\Services; use App\Models\Category;use App\Models\Restaurant;use Illuminate\Support\Collection; class CategoryService{ public function getRestaurantCategories(bool $withProducts = false): Collection { return Category::when( $withProducts, fn ($q) => $q->with('products') ) ->where('restaurant_id', auth()->user()->restaurant->id) ->get(); } public function createCategory(Restaurant $restaurant, array $attributes): Category { return $restaurant->categories()->create($attributes); } public function updateCategory(Category $category, array $attributes): Category { $category->update($attributes); return $category; } public function deleteCategory(Category $category): void { $category->delete(); }}
Inject the service into the API controller.
app/Http/Controllers/Api/V1/Vendor/CategoryController.php
use App\Http\Resources\Api\V1\Vendor\CategoryCollection;use App\Http\Resources\Api\V1\Vendor\CategoryResource;use App\Models\Category;use App\Services\CategoryService; use Illuminate\Http\JsonResponse;use Illuminate\Http\Request;use Illuminate\Http\Response; class CategoryController extends Controller{ public function __construct( public CategoryService $categoryService ) { } // ...
And update the controller's methods to consume service.
app/Http/Controllers/Api/V1/Vendor/CategoryController.php
public function index(Request $request): CategoryCollection{ $this->authorize('category.viewAny'); $categories = Category::when( $request->boolean('products'), fn ($q) => $q->with('products') ) ->where('restaurant_id', auth()->user()->restaurant->id) ->get(); return new CategoryCollection($categories); return new CategoryCollection($this->categoryService->getRestaurantCategories( withProducts: $request->boolean('products') ));} public function store(StoreCategoryRequest $request): CategoryResource{ $restaurant = $request->user()->restaurant; $category = $restaurant->categories()->create($request->validated()); $category = $this->categoryService->createCategory( $request->user()->restaurant, $request->validated() ); return new CategoryResource($category);} public function update(UpdateCategoryRequest $request, Category $category): JsonResponse{ $category->update($request->validated()); $this->categoryService->updateCategory( $category, $request->validated() ); return (new CategoryResource($category)) ->response() ->setStatusCode(Response::HTTP_ACCEPTED);} public function destroy(Category $category): Response{ $this->authorize('category.delete'); $category->delete(); $this->categoryService->deleteCategory($category); return response()->noContent();}
We update the web CategoryController
in the same way.
app/Http/Controllers/Vendor/CategoryController.php
use App\Http\Requests\Vendor\StoreCategoryRequest;use App\Http\Requests\Vendor\UpdateCategoryRequest;use App\Models\Category;use App\Services\CategoryService; use Illuminate\Http\RedirectResponse;use Inertia\Inertia;use Inertia\Response; class CategoryController extends Controller{ public function __construct( public CategoryService $categoryService ) { } // ...
app/Http/Controllers/Vendor/CategoryController.php
public function store(StoreCategoryRequest $request): RedirectResponse{ $request->user()->restaurant->categories()->create($request->only('name')); $this->categoryService->createCategory( $request->user()->restaurant, $request->validated() ); return to_route('vendor.menu') ->withStatus('Product Category created successfully.');} public function update(UpdateCategoryRequest $request, Category $category): RedirectResponse{ $category->update($request->only('name')); $this->categoryService->updateCategory( $category, $request->validated() ); return to_route('vendor.menu') ->withStatus('Product Category updated successfully.');} public function destroy(Category $category){ $category->delete(); $this->categoryService->deleteCategory($category); return to_route('vendor.menu') ->withStatus('Product Category deleted successfully.');}
And the MenuController
.
app/Http/Controllers/Vendor/MenuController.php
namespace App\Http\Controllers\Vendor; use App\Http\Controllers\Controller;use App\Models\Category; use App\Services\CategoryService; use Inertia\Inertia;use Inertia\Response; class MenuController extends Controller{ public function __construct( public CategoryService $categoryService ) { } public function index(): Response { $this->authorize('category.viewAny'); return Inertia::render('Vendor/Menu', [ 'categories' => Category::query() ->where('restaurant_id', auth()->user()->restaurant->id) ->with('products') ->get(), 'categories' => $this->categoryService->getRestaurantCategories( withProducts: true ), ]); }}
Finally, we can add tests for API endpoints.
php artisan make:test Api/CategoryTest
tests/Feature/Api/CategoryTest.php
namespace Tests\Feature\Api; use Illuminate\Foundation\Testing\RefreshDatabase;use Tests\TestCase;use Tests\Traits\WithTestingSeeder; class CategoryTest extends TestCase{ use RefreshDatabase; use WithTestingSeeder; public function test_vendor_can_view_categories(): void { $vendor = $this->getVendorUser(); $response = $this ->actingAs($vendor) ->getJson(route('api.vendor.categories.index')); $response->assertOk(); } public function test_vendor_can_store_category(): void { $vendor = $this->getVendorUser(); $response = $this ->actingAs($vendor) ->postJson(route('api.vendor.categories.store'), [ 'name' => 'Pizzas', ]); $response->assertCreated(); } public function test_vendor_can_update_category(): void { $vendor = $this->getVendorUser(); $category = $vendor->restaurant->categories()->first(); $response = $this ->actingAs($vendor) ->putJson(route('api.vendor.categories.update', $category), [ 'name' => 'New Category Name', ]); $response->assertAccepted(); } public function test_vendor_can_destroy_category(): void { $vendor = $this->getVendorUser(); $category = $vendor->restaurant->categories()->first(); $response = $this ->actingAs($vendor) ->deleteJson(route('api.vendor.categories.destroy', $category)); $response->assertNoContent(); }}
And add tests for the web controller.
php artisan make:test Web/CategoryTest
tests/Feature/Web/CategoryTest.php
namespace Tests\Feature\Web; use Illuminate\Foundation\Testing\RefreshDatabase;use Inertia\Testing\AssertableInertia as Assert;use Tests\TestCase;use Tests\Traits\WithTestingSeeder; class CategoryTest extends TestCase{ use RefreshDatabase; use WithTestingSeeder; public function test_vendor_can_view_categories(): void { $vendor = $this->getVendorUser(); $response = $this ->actingAs($vendor) ->get(route('vendor.menu')); $response->assertInertia(function (Assert $page) { return $page->component('Vendor/Menu') ->has('categories'); }); } public function test_vendor_can_view_categories_create(): void { $vendor = $this->getVendorUser(); $response = $this ->actingAs($vendor) ->get(route('vendor.categories.create')); $response->assertInertia(function (Assert $page) { return $page->component('Vendor/Categories/Create'); }); } public function test_vendor_can_store_category(): void { $vendor = $this->getVendorUser(); $response = $this ->actingAs($vendor) ->postJson(route('vendor.categories.store'), [ 'name' => 'Pizzas', ]); $response->assertRedirectToRoute('vendor.menu'); } public function test_vendor_can_view_categories_edit(): void { $vendor = $this->getVendorUser(); $category = $vendor->restaurant->categories()->first(); $response = $this ->actingAs($vendor) ->get(route('vendor.categories.edit', $category)); $response->assertInertia(function (Assert $page) { return $page->component('Vendor/Categories/Edit'); }); } public function test_vendor_can_update_category(): void { $vendor = $this->getVendorUser(); $category = $vendor->restaurant->categories()->first(); $response = $this ->actingAs($vendor) ->putJson(route('vendor.categories.update', $category), [ 'name' => 'New Category Name', ]); $response->assertRedirectToRoute('vendor.menu'); } public function test_vendor_can_destroy_category(): void { $vendor = $this->getVendorUser(); $category = $vendor->restaurant->categories()->first(); $response = $this ->actingAs($vendor) ->deleteJson(route('vendor.categories.destroy', $category)); $response->assertRedirectToRoute('vendor.menu'); }}
If you did everything correctly, all tests should pass.
php artisan test --filter CategoryTest
PASS Tests\Feature\Api\CategoryTest✓ vendor can view categories 1.17s✓ vendor can store category 0.24s✓ vendor can update category 0.25s✓ vendor can destroy category 0.23s PASS Tests\Feature\Web\CategoryTest✓ vendor can view categories 0.36s✓ vendor can view categories create 0.25s✓ vendor can store category 0.25s✓ vendor can view categories edit 0.35s✓ vendor can update category 0.21s✓ vendor can destroy category 0.23s Tests: 10 passed (33 assertions)Duration: 3.62s