In this lesson, we will implement API endpoints for vendor users to add and remove staff members. Later on, service for this will be implemented and covered with tests.
Create new API resources.
php artisan make:resource Api/V1/Vendor/StaffMemberResource
php artisan make:resource Api/V1/Vendor/StaffMemberCollection
Create a new StaffMemberController
.
php artisan make:controller Api/V1/Vendor/StaffMemberController
app/Http/Controllers/Api/V1/Vendor/StaffMemberController.php
namespace App\Http\Controllers\Api\V1\Vendor; use App\Enums\RoleName;use App\Http\Controllers\Controller;use App\Http\Requests\Vendor\StoreStaffMemberRequest;use App\Http\Resources\Api\V1\Vendor\StaffMemberCollection;use App\Http\Resources\Api\V1\Vendor\StaffMemberResource;use App\Models\Role;use App\Notifications\RestaurantStaffInvitation;use Illuminate\Http\Response;use Illuminate\Support\Facades\DB; class StaffMemberController extends Controller{ public function index() { $this->authorize('user.viewAny'); $staffMembers = auth()->user()->restaurant->staff; return new StaffMemberCollection($staffMembers); } public function store(StoreStaffMemberRequest $request): StaffMemberResource { $restaurant = $request->user()->restaurant; $attributes = $request->validated(); $member = DB::transaction(function () use ($attributes, $restaurant) { $user = $restaurant->staff()->create([ 'name' => $attributes['name'], 'email' => $attributes['email'], 'password' => '', ]); $user->roles()->sync(Role::where('name', RoleName::STAFF->value)->first()); return $user; }); $member->notify(new RestaurantStaffInvitation($restaurant->name)); return new StaffMemberResource($member); } public function destroy($staffMemberId): Response { $this->authorize('user.delete'); $restaurant = auth()->user()->restaurant; $member = $restaurant->staff()->findOrFail($staffMemberId); $member->roles()->sync([]); $member->delete(); return response()->noContent(); }}
And add a new apiResource
to the vendor API routes file.
routes/api/v1/vendor.php
use App\Http\Controllers\Api\V1\Vendor\CategoryController;use App\Http\Controllers\Api\V1\Vendor\ProductController;use App\Http\Controllers\Api\V1\Vendor\StaffMemberController; use Illuminate\Support\Facades\Route; // ... Route::apiResource('categories', CategoryController::class);Route::apiResource('products', ProductController::class);Route::apiResource('staff-members', StaffMemberController::class);
Create a new StaffMemberService
with createMember
and deleteMember
methods.
app/Services/StaffMemberService.php
namespace App\Services; use App\Enums\RoleName;use App\Models\Restaurant;use App\Models\Role;use App\Models\User;use App\Notifications\RestaurantStaffInvitation;use Illuminate\Support\Facades\DB; class StaffMemberService{ public function createMember(Restaurant $restaurant, array $attributes): User { $member = DB::transaction(function () use ($attributes, $restaurant) { $user = $restaurant->staff()->create([ 'name' => $attributes['name'], 'email' => $attributes['email'], 'password' => '', ]); $user->roles()->sync(Role::where('name', RoleName::STAFF->value)->first()); return $user; }); $member->notify(new RestaurantStaffInvitation($restaurant->name)); return $member; } public function deleteMember(Restaurant $restaurant, $staffMemberId): bool { $member = $restaurant->staff()->find($staffMemberId); if ($member === null) { return false; } $member->roles()->sync([]); $member->delete(); return true; }}
Inject StaffMemberService
into API StaffMemberController
controller and update imports.
app/Http/Controllers/Api/V1/Vendor/StaffMemberController.php
use App\Enums\RoleName; use App\Http\Controllers\Controller;use App\Http\Requests\Vendor\StoreStaffMemberRequest;use App\Http\Resources\Api\V1\Vendor\StaffMemberCollection;use App\Http\Resources\Api\V1\Vendor\StaffMemberResource;use App\Models\Role; use App\Notifications\RestaurantStaffInvitation; use App\Services\StaffMemberService; use Illuminate\Http\Response;use Illuminate\Support\Facades\DB; class StaffMemberController extends Controller{ public function __construct(public StaffMemberService $staffMemberService) { } // ...
Then update the store
and destroy
methods to consume service.
app/Http/Controllers/Api/V1/Vendor/StaffMemberController.php
public function store(StoreStaffMemberRequest $request): StaffMemberResource{ $restaurant = $request->user()->restaurant; $attributes = $request->validated(); $member = DB::transaction(function () use ($attributes, $restaurant) { $user = $restaurant->staff()->create([ 'name' => $attributes['name'], 'email' => $attributes['email'], 'password' => '', ]); $user->roles()->sync(Role::where('name', RoleName::STAFF->value)->first()); return $user; }); $member->notify(new RestaurantStaffInvitation($restaurant->name)); $member = $this->staffMemberService->createMember( $request->user()->restaurant, $request->validated() ); return new StaffMemberResource($member);} public function destroy($staffMemberId): Response{ $this->authorize('user.delete'); $restaurant = auth()->user()->restaurant; $member = $restaurant->staff()->findOrFail($staffMemberId); $deleted = $this->staffMemberService->deleteMember( auth()->user()->restaurant, $staffMemberId ); $member->roles()->sync([]); $member->delete(); abort_if(! $deleted, Response::HTTP_NOT_FOUND); return response()->noContent();}
Inject StaffMemberService
into Web StaffMemberController
controller and update imports.
app/Http/Controllers/Vendor/StaffMemberController.php
use App\Enums\RoleName; use App\Http\Controllers\Controller;use App\Http\Requests\Vendor\StoreStaffMemberRequest;use App\Models\Role; use App\Notifications\RestaurantStaffInvitation; use App\Services\StaffMemberService; use Illuminate\Http\RedirectResponse;use Illuminate\Support\Facades\DB; use Illuminate\Http\Response as HttpResponse; use Inertia\Inertia;use Inertia\Response; class StaffMemberController extends Controller{ public function __construct(public StaffMemberService $staffMemberService) { } // ...
Update store
and destroy
methods.
app/Http/Controllers/Vendor/StaffMemberController.php
public function store(StoreStaffMemberRequest $request): RedirectResponse{ $restaurant = $request->user()->restaurant; $attributes = $request->validated(); $member = DB::transaction(function () use ($attributes, $restaurant) { $user = $restaurant->staff()->create([ 'name' => $attributes['name'], 'email' => $attributes['email'], 'password' => '', ]); $user->roles()->sync(Role::where('name', RoleName::STAFF->value)->first()); return $user; }); $member->notify(new RestaurantStaffInvitation($restaurant->name)); $this->staffMemberService->createMember( $request->user()->restaurant, $request->validated() ); return back();} public function destroy($staffMemberId){ $this->authorize('user.delete'); $restaurant = auth()->user()->restaurant; $member = $restaurant->staff()->findOrFail($staffMemberId); $deleted = $this->staffMemberService->deleteMember( auth()->user()->restaurant, $staffMemberId ); $member->roles()->sync([]); $member->delete(); abort_if(! $deleted, HttpResponse::HTTP_NOT_FOUND); return back();}
Create a new StaffMemberTest
test file for API endpoints.
php artisan make:test Api/StaffMemberTest
tests/Feature/Api/StaffMemberTest.php
namespace Tests\Feature\Api; use App\Events\StaffMemberCreated;use App\Models\Category;use App\Models\Product;use App\Models\Restaurant;use App\Models\User;use Illuminate\Foundation\Testing\RefreshDatabase;use Illuminate\Support\Facades\Event;use Tests\TestCase;use Tests\Traits\WithTestingSeeder; class StaffMemberTest extends TestCase{ use RefreshDatabase; use WithTestingSeeder; public function test_vendor_can_list_staff_members(): void { $vendor = $this->getVendorUser(); $response = $this ->actingAs($vendor) ->get(route('api.vendor.staff-members.index')); $response->assertOk(); } public function test_vendor_can_add_new_staff_member(): void { $vendor = $this->getVendorUser(); $response = $this ->actingAs($vendor) ->postJson(route('api.vendor.staff-members.store', [ 'name' => 'John Doe', 'email' => 'john@example.org', ])); $response->assertCreated(); } public function test_vendor_cant_add_existing_staff_member(): void { $vendor = $this->getVendorUser(); $staff = $vendor->restaurant->staff()->first(); Event::fake(); $response = $this ->actingAs($vendor) ->postJson(route('api.vendor.staff-members.store', [ 'name' => 'John Doe', 'email' => $staff->email, ])); $response->assertUnprocessable(); Event::assertNotDispatched(StaffMemberCreated::class); } public function test_vendor_can_delete_existing_staff_member(): void { $vendor = $this->getVendorUser(); $staff = $vendor->restaurant->staff()->first(); $response = $this ->actingAs($vendor) ->deleteJson(route('api.vendor.staff-members.destroy', $staff->id)); $response->assertNoContent(); } public function test_vendor_cant_delete_staff_member_it_doesnt_belong_to_restaurant(): void { $vendor = $this->getVendorUser(); $anotherVendor = User::factory() ->vendor() ->has( Restaurant::factory()->has( Category::factory()->has( Product::factory() ) ) ->has(User::factory()->staff(), 'staff') ) ->create(); $anotherStaffMember = $anotherVendor->restaurant->staff()->first(); $response = $this ->actingAs($vendor) ->deleteJson(route('api.vendor.staff-members.destroy', $anotherStaffMember->id)); $response->assertNotFound(); }}
Create a new StaffMemberTest
test file for Web endpoints.
php artisan make:test Web/StaffMemberTest
tests/Feature/Web/StaffMemberTest.php
namespace Tests\Feature\Web; use App\Events\StaffMemberCreated;use App\Models\Category;use App\Models\Product;use App\Models\Restaurant;use App\Models\User;use Illuminate\Foundation\Testing\RefreshDatabase;use Illuminate\Support\Facades\Event;use Inertia\Testing\AssertableInertia;use Tests\TestCase;use Tests\Traits\WithTestingSeeder; class StaffMemberTest extends TestCase{ use RefreshDatabase; use WithTestingSeeder; public function test_vendor_can_list_staff_members(): void { $vendor = $this->getVendorUser(); $response = $this ->actingAs($vendor) ->get(route('vendor.staff-members.index')); $response->assertInertia(function (AssertableInertia $page) { return $page->component('Vendor/Staff/Show') ->has('staff'); }); } public function test_vendor_can_add_new_staff_member(): void { $vendor = $this->getVendorUser(); $response = $this ->actingAs($vendor) ->post(route('vendor.staff-members.store', [ 'name' => 'John Doe', 'email' => 'john@example.org', ])); $response->assertRedirect()->assertSessionDoesntHaveErrors(); } public function test_vendor_cant_add_existing_staff_member(): void { $vendor = $this->getVendorUser(); $staff = $vendor->restaurant->staff()->first(); Event::fake(); $response = $this ->actingAs($vendor) ->post(route('vendor.staff-members.store', [ 'name' => 'John Doe', 'email' => $staff->email, ])); $response->assertRedirect()->assertSessionHasErrors(['email']); Event::assertNotDispatched(StaffMemberCreated::class); } public function test_vendor_can_delete_existing_staff_member(): void { $vendor = $this->getVendorUser(); $staff = $vendor->restaurant->staff()->first(); $response = $this ->actingAs($vendor) ->delete(route('vendor.staff-members.destroy', $staff->id)); $response->assertRedirect()->assertSessionHasNoErrors(); } public function test_vendor_cant_delete_staff_member_it_doesnt_belong_to_restaurant(): void { $vendor = $this->getVendorUser(); $anotherVendor = User::factory() ->vendor() ->has( Restaurant::factory()->has( Category::factory()->has( Product::factory() ) ) ->has(User::factory()->staff(), 'staff') ) ->create(); $anotherStaffMember = $anotherVendor->restaurant->staff()->first(); $response = $this ->actingAs($vendor) ->delete(route('vendor.staff-members.destroy', $anotherStaffMember->id)); $response->assertNotFound(); }}
Now we can test if the vendor can add and remove staff members successfully.
php artisan test --filter StaffMember
PASS Tests\Feature\Api\StaffMemberTest✓ vendor can list staff members 1.12s✓ vendor can add new staff member 0.30s✓ vendor cant add existing staff member 0.27s✓ vendor can delete existing staff member 0.23s✓ vendor cant delete staff member it doesnt belong to restaurant 0.25s PASS Tests\Feature\Web\StaffMemberTest✓ vendor can list staff members 0.31s✓ vendor can add new staff member 0.27s✓ vendor cant add existing staff member 0.23s✓ vendor can delete existing staff member 0.22s✓ vendor cant delete staff member it doesnt belong to restaurant 0.24s Tests: 10 passed (24 assertions)Duration: 3.53s
That's it for this refactoring course! All the code is in this GitHub repository.