In this lesson, we will continue with API services. The goals are:
OrderService
to use in controllersCreate API resources for Order Model.
php artisan make:resource Api/V1/Customer/OrderResource
php artisan make:resource Api/V1/Customer/OrderCollection
Create a new OrderController
.
php artisan make:controller Api/V1/Customer/OrderController
The logic is the same as in Web Controller.
app/Http/Controllers/Api/V1/Customer/OrderController.php
namespace App\Http\Controllers\Api\V1\Customer; use App\Enums\OrderStatus;use App\Http\Controllers\Controller;use App\Http\Requests\Customer\StoreOrderRequest;use App\Http\Resources\Api\V1\Customer\OrderCollection;use App\Http\Resources\Api\V1\Customer\OrderResource;use App\Models\Order;use App\Notifications\NewOrderCreated;use App\Services\CartService;use Illuminate\Http\Request;use Illuminate\Support\Facades\DB; class OrderController extends Controller{ public function __construct(public CartService $cart) { } public function index(): OrderCollection { $this->authorize('order.viewAny'); $orders = Order::with(['restaurant', 'products']) ->where('customer_id', auth()->id()) ->latest() ->get(); return new OrderCollection($orders); } public function store(StoreOrderRequest $request): OrderResource { $user = $request->user(); $attributes = $request->validated(); $order = DB::transaction(function () use ($user, $attributes) { $order = $user->orders()->create([ 'restaurant_id' => $attributes['restaurant_id'], 'total' => $attributes['total'], 'status' => OrderStatus::PENDING, ]); $order->products()->createMany($attributes['items']); return $order; }); $order->restaurant->owner->notify(new NewOrderCreated($order)); $this->cart->flush(); return new OrderResource($order); }}
Then add a new apiResource()
route to the customer.php
file.
routes/api/v1/customer.php
use App\Http\Controllers\Api\V1\Customer\CartController;use App\Http\Controllers\Api\V1\Customer\OrderController; use Illuminate\Support\Facades\Route; // ... Route::get('cart', [CartController::class, 'index'])->name('cart.index');Route::post('cart/{product}/add', [CartController::class, 'add'])->name('cart.add');Route::post('cart/{uuid}/remove', [CartController::class, 'remove'])->name('cart.remove');Route::delete('cart', [CartController::class, 'destroy'])->name('cart.destroy'); Route::apiResource('orders', OrderController::class); // ...
The OrderService
primarily consists of the getCustomerOrders
and placeOrder
methods.
Create a new file.
app/Services/OrderService.php
namespace App\Services; use App\Enums\OrderStatus;use App\Models\Order;use App\Models\User;use App\Notifications\NewOrderCreated;use App\Services\CartService;use Illuminate\Support\Collection;use Illuminate\Support\Facades\DB; class OrderService{ public function __construct( public CartService $cart ) { } public function getCustomerOrders(): Collection { return Order::with(['restaurant', 'products']) ->where('customer_id', auth()->id()) ->latest() ->get(); } public function placeOrder(User $user, array $attributes): Order { $order = DB::transaction(function () use ($user, $attributes) { $order = $user->orders()->create([ 'restaurant_id' => $attributes['restaurant_id'], 'total' => $attributes['total'], 'status' => OrderStatus::PENDING, ]); $order->products()->createMany($attributes['items']); return $order; }); $order->restaurant->owner->notify(new NewOrderCreated($order)); $this->cart->flush(); return $order; }}
Now let's inject OrderService
instead of CartService
in the controller. OrderService
flushes the cart after placing an order, so we no longer need CartService
there.
app/Http/Controllers/Api/V1/Customer/OrderController.php
namespace App\Http\Controllers\Api\V1\Customer; use App\Enums\OrderStatus; use App\Http\Controllers\Controller;use App\Http\Requests\Customer\StoreOrderRequest;use App\Http\Resources\Api\V1\Customer\OrderCollection;use App\Http\Resources\Api\V1\Customer\OrderResource;use App\Models\Order; use App\Notifications\NewOrderCreated; use App\Services\CartService; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use App\Services\OrderService; class OrderController extends Controller{ public function __construct(public CartService $cart) public function __construct(public OrderService $orderService) { } public function index(): OrderCollection { $this->authorize('order.viewAny'); $orders = Order::with(['restaurant', 'products']) ->where('customer_id', auth()->id()) ->latest() ->get(); return new OrderCollection($orders); return new OrderCollection( $this->orderService->getCustomerOrders() ); } public function store(StoreOrderRequest $request): OrderResource { $user = $request->user(); $attributes = $request->validated(); $order = DB::transaction(function () use ($user, $attributes) { $order = $user->orders()->create([ 'restaurant_id' => $attributes['restaurant_id'], 'total' => $attributes['total'], 'status' => OrderStatus::PENDING, ]); $order->products()->createMany($attributes['items']); return $order; }); $order->restaurant->owner->notify(new NewOrderCreated($order)); $this->cart->flush(); $order = $this->orderService->placeOrder( $request->user(), $request->validated() ); return new OrderResource($order); }}
Now we can update another web controller in the same fashion.
app/Http/Controllers/Customer/OrderController.php
namespace App\Http\Controllers\Customer; use App\Enums\OrderStatus; use App\Http\Controllers\Controller;use App\Http\Requests\Customer\StoreOrderRequest;use App\Models\Order; use App\Notifications\NewOrderCreated; use App\Services\CartService; use App\Services\OrderService; use Illuminate\Http\RedirectResponse;use Illuminate\Support\Facades\DB; use Inertia\Inertia;use Inertia\Response; class OrderController extends Controller{ public function __construct(public CartService $cart) public function __construct(public OrderService $orderService) { } public function index(): Response { $this->authorize('order.viewAny'); $orders = Order::with(['restaurant', 'products']) ->where('customer_id', auth()->id()) ->latest() ->get(); return Inertia::render('Customer/Orders', [ 'orders' => $orders, 'orders' => $this->orderService->getCustomerOrders(), ]); } public function store(StoreOrderRequest $request): RedirectResponse { $user = $request->user(); $attributes = $request->validated(); $order = DB::transaction(function () use ($user, $attributes) { $order = $user->orders()->create([ 'restaurant_id' => $attributes['restaurant_id'], 'total' => $attributes['total'], 'status' => OrderStatus::PENDING, ]); $order->products()->createMany($attributes['items']); return $order; }); $order->restaurant->owner->notify(new NewOrderCreated($order)); $this->cart->flush(); $this->orderService->placeOrder( $request->user(), $request->validated() ); return to_route('customer.orders.index') ->withStatus('Order accepted.'); }}
Create a new OrderTest
class for API endpoints.
php artisan make:test Api/OrderTest
tests/Feature/Api/OrderTest.php
namespace Tests\Feature\Api; use App\Models\Product;use App\Models\User;use Illuminate\Foundation\Testing\RefreshDatabase;use Tests\TestCase;use Tests\Traits\WithTestingSeeder; class OrderTest extends TestCase{ use RefreshDatabase; use WithTestingSeeder; public function test_customer_can_place_order(): void { $customer = User::factory()->customer()->create(); $product = Product::first(); $this->actingAs($customer)->postJson(route('api.customer.cart.add', $product)); $response = $this->actingAs($customer)->postJson(route('api.customer.orders.store')); $response->assertCreated(); } public function test_customer_cant_place_empty_order() { $customer = User::factory()->customer()->create(); $response = $this->actingAs($customer)->postJson(route('api.customer.orders.store')); $response->assertUnprocessable(); } public function test_customer_can_see_orders() { $customer = User::factory()->customer()->create(); $product = Product::first(); $this->actingAs($customer)->postJson(route('api.customer.cart.add', $product)); $this->actingAs($customer)->postJson(route('api.customer.orders.store')); $response = $this ->actingAs($customer) ->get(route('customer.orders.index')); $response->assertOk(); }}
Create a new OrderTest
class for Web endpoints.
php artisan make:test Web/OrderTest
tests/Feature/Web/OrderTest.php
namespace Tests\Feature\Web; use App\Models\Product;use App\Models\User;use Illuminate\Foundation\Testing\RefreshDatabase;use Inertia\Testing\AssertableInertia as Assert;use Tests\TestCase;use Tests\Traits\WithTestingSeeder; class OrderTest extends TestCase{ use RefreshDatabase; use WithTestingSeeder; public function test_customer_can_place_order(): void { $customer = User::factory()->customer()->create(); $product = Product::first(); $this->actingAs($customer)->post(route('customer.cart.add', $product)); $response = $this->actingAs($customer)->post(route('customer.orders.store')); $response->assertRedirectToRoute('customer.orders.index'); } public function test_customer_cant_place_empty_order(): void { $customer = User::factory()->customer()->create(); $response = $this->actingAs($customer)->post(route('customer.orders.store')); $response->assertSessionHasErrors(['restaurant_id', 'items', 'total']); } public function test_customer_can_see_orders(): void { $customer = User::factory()->customer()->create(); $product = Product::first(); $this->actingAs($customer)->post(route('customer.cart.add', $product)); $this->actingAs($customer)->post(route('customer.orders.store')); $response = $this ->actingAs($customer) ->get(route('customer.orders.index')); $response->assertInertia(function (Assert $page) { return $page->component('Customer/Orders') ->has('orders'); }); }}
And run the tests to validate that everything is working as expected.
php artisan test --filter OrderTest
PASS Tests\Feature\Api\OrderTest✓ customer can place order 1.18s✓ customer cant place empty order 0.22s✓ customer can see orders 0.26s PASS Tests\Feature\Web\OrderTest✓ customer can place order 0.26s✓ customer cant place empty order 0.22s✓ customer can see orders 0.29s Tests: 6 passed (17 assertions)Duration: 2.52s