As you've probably guessed, we will deal with product logic in this lesson and move it into the service class.
Create API resources and the Controller for Product Model.
php artisan make:resource Api/V1/Vendor/ProductResource
php artisan make:resource Api/V1/Vendor/ProductCollection
php artisan make:controller Api/V1/Vendor/ProductController
app/Http/Controllers/Api/V1/Vendor/ProductController.php
namespace App\Http\Controllers\Api\V1\Vendor; use App\Http\Controllers\Controller;use App\Http\Requests\Vendor\StoreProductRequest;use App\Http\Requests\Vendor\UpdateProductRequest;use App\Http\Resources\Api\V1\Vendor\ProductResource;use App\Models\Product;use Illuminate\Http\Response; class ProductController extends Controller{ public function store(StoreProductRequest $request): ProductResource { $product = Product::create($request->validated()); return new ProductResource($product); } public function show(Product $product): ProductResource { $this->authorize('product.view'); $product->load('category'); return new ProductResource($product); } public function update(UpdateProductRequest $request, Product $product) { $product->update($request->validated()); return (new ProductResource($product)) ->response() ->setStatusCode(Response::HTTP_ACCEPTED); } public function destroy(Product $product) { $this->authorize('product.delete'); $product->delete(); return response()->noContent(); }}
Now create the ProductService
with methods providing basic actions.
app/Services/ProductService.php
namespace App\Services; use App\Models\Product; class ProductService{ public function createProduct(array $attributes): Product { return Product::create($attributes); } public function updateProduct(Product $product, array $attributes): Product { $product->update($attributes); return $product; } public function deleteProduct(Product $product): void { $product->delete(); }}
Inject ProductService
into the newly created ProductController
.
app/Http/Controllers/Api/V1/Vendor/ProductController.php
use App\Http\Requests\Vendor\UpdateProductRequest;use App\Http\Resources\Api\V1\Vendor\ProductResource;use App\Models\Product;use App\Services\ProductService; use Illuminate\Http\Response; class ProductController extends Controller{ public function __construct(public ProductService $productService) { } // ...
And update methods to consume ProductService
.
app/Http/Controllers/Api/V1/Vendor/ProductController.php
public function store(StoreProductRequest $request): ProductResource{ $product = Product::create($request->validated()); $product = $this->productService->createProduct($request->validated()); return new ProductResource($product);} public function update(UpdateProductRequest $request, Product $product){ $product->update($request->validated()); $product = $this->productService->updateProduct($product, $request->validated()); return (new ProductResource($product)) ->response() ->setStatusCode(Response::HTTP_ACCEPTED);} public function destroy(Product $product){ $this->authorize('product.delete'); $product->delete(); $this->productService->deleteProduct($product); return response()->noContent();}
The same procedure applies to ProductController
handling web routes using Inertia.
app/Http/Controllers/Vendor/ProductController.php
use App\Http\Requests\Vendor\UpdateProductRequest;use App\Models\Category;use App\Models\Product;use App\Services\ProductService; use Illuminate\Http\RedirectResponse;use Inertia\Inertia;use Inertia\Response; class ProductController extends Controller{ public function __construct(public ProductService $productService) { } // ...
app/Http/Controllers/Vendor/ProductController.php
public function create(): Response{ $this->authorize('product.create'); return Inertia::render('Vendor/Products/Create', [ 'categories' => Category::all(['id', 'name']), 'category_id' => request('category_id'), ]);} public function store(StoreProductRequest $request): RedirectResponse{ Product::create($request->validated()); $this->productService->createProduct($request->validated()); return to_route('vendor.menu') ->withStatus('Product created successfully.');} public function edit(Product $product){ $this->authorize('product.update'); return Inertia::render('Vendor/Products/Edit', [ 'categories' => Category::get(['id', 'name']), 'product' => $product, ]);} public function update(UpdateProductRequest $request, Product $product){ $product->update($request->validated()); $this->productService->updateProduct($product, $request->validated()); return to_route('vendor.menu') ->withStatus('Product updated successfully.');} public function destroy(Product $product){ $product->delete(); $this->authorize('product.delete'); $this->productService->deleteProduct($product); return to_route('vendor.menu') ->withStatus('Product deleted successfully.');}
Create a new test file for Product API endpoints.
php artisan make:test Api/ProductTest
tests/Feature/Api/ProductTest.php
namespace Tests\Feature\Api; use App\Enums\RoleName;use App\Models\Product;use App\Models\User;use Illuminate\Foundation\Testing\RefreshDatabase;use Tests\TestCase;use Tests\Traits\WithTestingSeeder; class ProductTest extends TestCase{ use RefreshDatabase; use WithTestingSeeder; public function test_vendor_can_store_product(): void { $vendor = $this->getVendorUser(); $category = $vendor->restaurant->categories()->first(); $response = $this ->actingAs($vendor) ->postJson(route('api.vendor.products.store'), [ 'category_id' => $category->id, 'name' => 'Pizza', 'price' => 2.99, ]); $response->assertCreated(); } public function test_vendor_can_view_product(): void { $vendor = $this->getVendorUser(); $product = $vendor->restaurant->categories()->first() ->products()->first(); $response = $this ->actingAs($vendor) ->get(route('api.vendor.products.show', $product)); $response->assertOk(); } public function test_vendor_can_update_product(): void { $vendor = $this->getVendorUser(); $product = $vendor->restaurant->categories()->first() ->products()->first(); $response = $this ->actingAs($vendor) ->putJson(route('api.vendor.products.update', $product), [ 'category_id' => $product->category_id, 'name' => 'Awesome Pizza', 'price' => 9.99, ]); $response->assertAccepted(); }}
And another one for Product web routes.
php artisan make:test Web/ProductTest
tests/Feature/Web/ProductTest.php
namespace Tests\Feature\Web; use Illuminate\Foundation\Testing\RefreshDatabase;use Inertia\Testing\AssertableInertia;use Tests\TestCase;use Tests\Traits\WithTestingSeeder; class ProductTest extends TestCase{ use RefreshDatabase; use WithTestingSeeder; public function test_vendor_can_view_products_create(): void { $vendor = $this->getVendorUser(); $response = $this ->actingAs($vendor) ->get(route('vendor.products.create')); $response->assertInertia(function (AssertableInertia $page) { return $page->component('Vendor/Products/Create') ->has('categories'); }); } public function test_vendor_can_store_product(): void { $vendor = $this->getVendorUser(); $category = $vendor->restaurant->categories()->first(); $response = $this ->actingAs($vendor) ->post(route('vendor.products.store'), [ 'category_id' => $category->id, 'name' => 'Pizza', 'price' => 2.99, ]); $response->assertRedirectToRoute('vendor.menu'); } public function test_vendor_can_view_products_edit(): void { $vendor = $this->getVendorUser(); $product = $vendor->restaurant->categories()->first() ->products()->first(); $response = $this ->actingAs($vendor) ->get(route('vendor.products.edit', $product)); $response->assertInertia(function (AssertableInertia $page) { return $page->component('Vendor/Products/Edit') ->has('categories') ->has('product'); }); } public function test_vendor_can_update_product(): void { $vendor = $this->getVendorUser(); $product = $vendor->restaurant->categories()->first() ->products()->first(); $response = $this ->actingAs($vendor) ->put(route('vendor.products.update', $product), [ 'category_id' => $product->category_id, 'name' => 'Awesome Pizza', 'price' => 9.99, ]); $response->assertRedirectToRoute('vendor.menu'); } public function test_vendor_can_destroy_product(): void { $vendor = $this->getVendorUser(); $product = $vendor->restaurant->categories()->first() ->products()->first(); $response = $this ->actingAs($vendor) ->delete(route('vendor.products.destroy', $product)); $response->assertRedirectToRoute('vendor.menu'); }}
Finally, we can run the tests.
php artisan test --filter CategoryTest
PASS Tests\Feature\Api\ProductTest✓ vendor can store product 1.13s✓ vendor can view product 0.23s✓ vendor can update product 0.21s PASS Tests\Feature\Web\ProductTest✓ vendor can view products create 0.25s✓ vendor can store product 0.23s✓ vendor can view products edit 0.24s✓ vendor can update product 0.24s✓ vendor can destroy product 0.22s Tests: 8 passed (26 assertions)Duration: 2.84s