jesse_orange_newable's avatar

To Service or not to Service

Hi all,

I have a simple expense tracking system in which a user makes a Claim and this can contain multiple Expense(s) and I made a service class to handle CRUD. Something I'm struggling with is this: as an expense can only exist in a claim, should a ClaimService be responsible rather than my existing ExpenseService?

Here's how that looks:

<?php
namespace App\Services;

use App\Models\Expense;
use App\Models\ExpenseClaim;
use App\Models\User;

class ExpenseService
{
    public function retrieveExpenses(ExpenseClaim $expenseClaim)
    {
        return $expenseClaim->expenses()->get();
    }

    /**
     * Create an expense within a claim.
     *
     * @param \App\Models\ExpenseClaim $expenseClaim
     * @param \App\Models\User         $user
     * @param array                    $attributes
     *
     * @return App\Models\Expense
     */
    public function createExpense(ExpenseClaim $expenseClaim, User $user, array $attributes)
    {
        return $expenseClaim->expenses()->create([
            'user_id' => $user->id,
            'expense_date' => $attributes['expense_date'],
            'business_reason' => $attributes['business_reason'],
            'category' => $attributes['category'],
            'project_code' => $attributes['project_code'],
            'is_foreign_expense' => $attributes['is_foreign_expense'],
            'currency_code' => $attributes['currency_code'],
            'conversion_rate_to_gbp' => $attributes['conversion_rate_to_gbp'],
            'amount_before_conversion' => $attributes['amount_before_conversion'],
            'gross_amount' => $attributes['gross_amount'],
            'has_vat' => $attributes['has_vat'],
            'vat_claimed' => $attributes['vat_claimed'],
        ]);
    }

    /**
     * Undocumented function
     *
     * @param \App\Models\Expense $expense
     * @param \App\Models\User    $user
     * @param array               $attributes
     *
     * @return App\Models\Expense
     */
    public function updateExpense(Expense $expense, User $user, array $attributes)
    {
        $expense->update([
            'user_id' => $user->id,
            'expense_date' => $attributes['expense_date'],
            'business_reason' => $attributes['business_reason'],
            'category' => $attributes['category'],
            'project_code' => $attributes['project_code'],
            'is_foreign_expense' => $attributes['is_foreign_expense'],
            'currency_code' => $attributes['currency_code'],
            'conversion_rate_to_gbp' => $attributes['conversion_rate_to_gbp'],
            'amount_before_conversion' => $attributes['amount_before_conversion'],
            'gross_amount' => $attributes['gross_amount'],
            'has_vat' => $attributes['has_vat'],
            'vat_claimed' => $attributes['vat_claimed'],
        ]);

        return $expense;
    }

    /**
     * Remove an existing expense.
     *
     * @param \App\Models\Expense $expense
     *
     * @return bool
     */
    public function deleteExpense(Expense $expense)
    {
        $expense->delete();

        return $expense;
    }
}

Then in my controller:

<?php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreExpense;
use App\Models\Expense;
use App\Models\ExpenseClaim;
use App\Services\ExpenseService;

class UserExpenseController extends Controller
{
    protected $expenseService;

    /**
     *  Create a new controller instance.
     *  Pass in our expense service class
     *
     * @param \App\Services\ExpenseService $expenseService
     */
    public function __construct(ExpenseService $expenseService)
    {
        $this->expenseService = $expenseService;
    }

    /**
     * Show a listing of expenses for a given user.
     *
     * @return void
     */
    public function index(ExpenseClaim $expenseClaim)
    {
        $expenses = $this->expenseService->retrieveExpenses($expenseClaim);

        return response()->json($expenses, 200);
    }

    /**
     * Create a new expense inside an expense claim for a given user.
     *
     * @param  ExpenseClaim $expenseClaim
     * @return void
     */
    public function store(ExpenseClaim $expenseClaim, StoreExpense $request)
    {
        $this->authorize('update', $expenseClaim);

        $attributes = $request->validated();

        $expense = $this->expenseService->createExpense($expenseClaim, auth()->user(), $attributes);

        return response()->json($expense, 200);
    }

    /**
     * Update  an existing expense in an expense claim for a given user.
     *
     * @param \App\Models\ExpenseClaim $expenseClaim
     * @param \App\Models\Expense      $expense
     *
     * @return void
     */
    public function update(ExpenseClaim $expenseClaim, Expense $expense, StoreExpense $request)
    {
        $this->authorize('update', $expenseClaim);

        $attributes = $request->validated();

        $expense = $this->expenseService->updateExpense($expense, auth()->user(), $attributes);

        return response()->json($expense, 200);
    }

    /**
     * Display a specific expense within an expense claim
     *
     * @param  ExpenseClaim $expenseClaim
     * @param  Expense      $expense
     * @return void
     */
    public function show(ExpenseClaim $expenseClaim, Expense $expense)
    {
        $this->authorize('update', $expenseClaim);

        return response()->json($expense, 200);
    }

    /**
     * Delete a given expense for a given user.
     *
     * @param  ExpenseClaim $expenseClaim
     * @return void
     */
    public function destroy(ExpenseClaim $expenseClaim, Expense $expense)
    {
        $this->authorize('update', $expenseClaim);

        $expense = $this->expenseService->deleteExpense($expense);

        return response()->json($expense, 200);
    }
}

My thought was that admin need to view submitted claims so a service class made sense, but how do you follow Single Responsibility if one thing can't exist without the other?

Thanks in advance.

0 likes
1 reply
martinbean's avatar
Level 80

an expense can only exist in a claim

@jesse_orange_newable Given the above, I’d say you should add the logic to your claim service.

In DDD, what you’ve described is an “aggregate” root. An Expense entity is owned by, and can only be created or modified by, its parent Claim entity. So it makes sense to encode your business logic in a claim service.

1 like

Please or to participate in this conversation.