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

Staff Members API, Service and Tests

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.


API Controller

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

Service

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

Update API Controller

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

Update Web Controller

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

Tests

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

Repository

That's it for this refactoring course! All the code is in this GitHub repository.