Courses

Laravel Web to Mobile API: Reuse Old Code with Services

Restaurant Service

In this lesson, our goal is to refactor Controllers into Service classes.

Let's look into this diagram. The problem is that two restaurant controllers are implementing the same business logic highlighted in the red area of the illustration.

Without Service

In the best-case scenario, we want to avoid duplicate code and merge that logic into a single entity.

With Service

If we have changes to business logic, it also brings the benefit of not updating it in multiple places.


RestaurantService Class

Create a new RestaurantService class.

app/Services/RestaurantService.php

namespace App\Services;
 
use App\Enums\RoleName;
use App\Models\Restaurant;
use App\Models\Role;
use App\Models\User;
use App\Notifications\RestaurantOwnerInvitation;
use Illuminate\Support\Facades\DB;
 
class RestaurantService
{
public function createRestaurant(array $attributes): Restaurant
{
return DB::transaction(function () use ($attributes) {
$user = User::create([
'name' => $attributes['owner_name'],
'email' => $attributes['email'],
'password' => '',
]);
 
$user->roles()->sync(Role::where('name', RoleName::VENDOR->value)->first());
 
$restaurant = $user->restaurant()->create([
'city_id' => $attributes['city_id'],
'name' => $attributes['restaurant_name'],
'address' => $attributes['address'],
]);
 
$user->notify(new RestaurantOwnerInvitation($attributes['restaurant_name']));
 
return $restaurant;
});
}
 
public function updateRestaurant(Restaurant $restaurant, array $attributes): Restaurant
{
$restaurant->update([
'city_id' => $attributes['city_id'],
'name' => $attributes['restaurant_name'],
'address' => $attributes['address'],
]);
 
return $restaurant;
}
}

Here we have implemented the createRestaurant and updateRestaurant methods that we can call from controllers.


Web Controller

Now we can use constructor property promotion on the Controller to inject RestaurantService and imported classes we no longer need there.

app/Http/Controllers/Admin/RestaurantController.php

namespace App\Http\Controllers\Admin;
 
use App\Enums\RoleName;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\StoreRestaurantRequest;
use App\Http\Requests\Admin\UpdateRestaurantRequest;
use App\Models\City;
use App\Models\Restaurant;
use App\Models\Role;
use App\Models\User;
use App\Notifications\RestaurantOwnerInvitation;
use App\Services\RestaurantService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
 
class RestaurantController extends Controller
{
public function __construct(public RestaurantService $restaurantService)
{
}
 
// ...

Update the store method and call the createRestaurant method on injected service.

app/Http/Controllers/Admin/RestaurantController.php

public function store(StoreRestaurantRequest $request): RedirectResponse
{
DB::transaction(function () use ($validated) {
$user = User::create([
'name' => $validated['owner_name'],
'email' => $validated['email'],
'password' => '',
]);
 
$user->roles()->sync(Role::where('name', RoleName::VENDOR->value)->first());
 
$user->restaurant()->create([
'city_id' => $validated['city_id'],
'name' => $validated['restaurant_name'],
'address' => $validated['address'],
]);
 
$user->notify(new RestaurantOwnerInvitation($validated['restaurant_name']));
});
$this->restaurantService->createRestaurant(
$request->validated()
);
 
return to_route('admin.restaurants.index');
}

In the same way, we update the update method.

app/Http/Controllers/Admin/RestaurantController.php

public function update(UpdateRestaurantRequest $request, Restaurant $restaurant): RedirectResponse
{
$validated = $request->validated();
 
$restaurant->update([
'city_id' => $validated['city_id'],
'name' => $validated['restaurant_name'],
'address' => $validated['address'],
]);
$this->restaurantService->updateRestaurant(
$restaurant,
$request->validated()
);
 
return to_route('admin.restaurants.index')
->withStatus('Restaurant updated successfully.');
}

API Controller

Then we apply the same changes to the API controller.

app/Http/Controllers/Api/V1/Admin/RestaurantController.php

use App\Enums\RoleName;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\StoreRestaurantRequest;
use App\Http\Requests\Admin\UpdateRestaurantRequest;
use App\Http\Resources\Api\V1\Admin\RestaurantCollection;
use App\Http\Resources\Api\V1\Admin\RestaurantResource;
use App\Models\Restaurant;
use App\Models\Role;
use App\Models\User;
use App\Notifications\RestaurantOwnerInvitation;
use App\Services\RestaurantService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
 
class RestaurantController extends Controller
{
public function __construct(public RestaurantService $restaurantService)
{
}
 
// ...

app/Http/Controllers/Api/V1/Admin/RestaurantController.php

public function store(StoreRestaurantRequest $request): RestaurantResource
{
$validated = $request->validated();
 
$restaurant = DB::transaction(function () use ($validated) {
$user = User::create([
'name' => $validated['owner_name'],
'email' => $validated['email'],
'password' => '',
]);
 
$user->roles()->sync(Role::where('name', RoleName::VENDOR->value)->first());
 
$user->restaurant()->create([
'city_id' => $validated['city_id'],
'name' => $validated['restaurant_name'],
'address' => $validated['address'],
]);
 
$user->notify(new RestaurantOwnerInvitation($validated['restaurant_name']));
});
$restaurant = $this->restaurantService->createRestaurant(
$request->validated()
);
 
return new RestaurantResource($restaurant);
}

app/Http/Controllers/Api/V1/Admin/RestaurantController.php

public function update(UpdateRestaurantRequest $request, Restaurant $restaurant): JsonResponse
{
$validated = $request->validated();
 
$restaurant->update([
'city_id' => $validated['city_id'],
'name' => $validated['restaurant_name'],
'address' => $validated['address'],
]);
$restaurant = $this->restaurantService->updateRestaurant(
$restaurant,
$request->validated()
);
 
return (new RestaurantResource($restaurant))
->response()
->setStatusCode(Response::HTTP_ACCEPTED);
}
avatar

Dear Povilas I hope this comment finds well i have looked through this service class and i wondering what if i have multiple return of status code let me show an example

Auth Controller

public function register(RegisterRequests $request,AuthService $authService)    {

  return  $authService->RegisterUser($request->validated());

}
	
	___________________________________________________

Register Service Class

public function RegisterUser($userData): JsonResponse
{
	 $DbUserData = User::with('devices')->where('email', $userData['email'])->first();
    if (!empty($DbUserData)) {
        $res['status'] = 'success';
        $res['status_extra'] = 'EMAIL_EXISTS';
        return response()->json($res, 200);
    } else {
        DB::beginTransaction();
        try {
            $globalCreationTime = Carbon::now();
            $newUserData = new User();
            $newUserData->name = urldecode($userData['username']);
            $newUserData->email = urldecode($userData['email']);
            $newUserData->password = Hash::make($userData['password']);
            $newUserData->dob = isset($userData['dob']) ? validateDateTime(urldecode($userData['dob'])) : null;
            $newUserData->user_type_id = config('constants.UserTypes.Costumers');
            $newUserData->created_at = $globalCreationTime;
            $newUserData->account_number = $this->generateAccountNumber();
            $newUserData->save();

            $res['status'] = "CHECK_YOUR_EMAIL_FOR_VERIFICATION";
            $res['status_extra'] = "verify_screen";
            $token = $newUserData->createToken(urldecode($userData['device_identifier']))->plainTextToken;
            $res['response'] = new RegisterationResponseData($newUserData, $token); // json collection
            DB::commit();
            return response()->json($res, 200);
        } catch (\Exception $exception) {
            DB::rollBack();
            $res['status'] = "SERVER_ERROR";
            if ($exception->getCode() == 23000) {
                $res['status_extra'] = "EMAIL_EXISTS";
                return response()->json($res, 409);
            }
            return response()->json($res, 500);
        }
    }
avatar

While this is an okay approach - it has a huge drawback. It will limit you to just API response and no other use cases.

It would be better to:

  1. Keep the 200 response status as simple return statement
  2. Move validation and 409 code into throw ValidationException()
  3. Move 500 to be handled by a new exception

That way, you can still re-use this method anywhere if needed.

ps. You could also separate this into a few smaller functions for even better structure