Back to Course |
Laravel Web to Mobile API: Reuse Old Code with Services

Products API, Service and Tests

As you've probably guessed, we will deal with product logic in this lesson and move it into the service class.


API Controller

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();
}
}

Service

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();
}
}

Update API Controller

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();
}

Update Web Controller

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.');
}

Tests

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