Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

lsvagusa's avatar

Organizing usage of multiple services in a single http controller

Hello everybody, love the forum and am looking for some feedback on my code.

I am building out a feature for using coupons during Stripe checkout.

I have 2 services:

  • 1 for handling the database model/table "coupons" -> CouponService
  • 1 for making API calls to Stripe\Coupons -> StripeCouponService

As I was writing my controller I noticed that for each CRUD I had to:

  1. call the CouponService
  2. call the StripeCouponService

This wasn't clean so I resorted to creating a 3rd "manager service" where I do calls to both of the aforementioned services.

Now the one I call inside of the controller is the manager one.

I have 2 questions:

  1. Is this a good pattern to follow?
  2. In cases of a service function needing to handle multiple return cases, does throwing exceptions and then catching them at the controller level seem good?
1 like
3 replies
Glukinho's avatar
  1. There is no single list with "always good" patterns and "always bad" patterns. It's up to you to choose right things. As long as the code fulfills it's task, it satisfies you, you understand it and you're able to support it, develop and extend through time - the code is good. If you feel uncertain - do it first way, the worst would happen is you feel it's bad and do it other way. Next time you'll know what is right in such situation.

    Some people say having a dedicated service over Eloquent is unneccessary overkill: Eloquent is already a sufficient "service" over database and SQL itself, and scopes give enough flexibility. But again, it's up to you.

  2. I don't like it, personally. Exceptions are for errors (or, at least, for handling unpredicted behavior of app). Instead, I would consider to return a data object with all fields you need.

martinbean's avatar

@lsvagusa I would (and have in a past project) just have a single CouponService that takes care of both creating the coupons in your database, as well as in Stripe. Reason being, you want them to be in sync. If you have separate services for your model and Stripe, then you’re just going to constantly have to inject that pair of services into every class you wish to interact with coupons.

So, wrap it up into a single service. Your service can then type-hint Stripe’s coupon service in its constructor:

use Stripe\Service\CouponService as StripeCouponService;

class CouponService
{
    public function __construct(protected StripeCouponService $stripeCoupons)
    {}
}

When you create/update/delete coupons using the service, you can insert/update/delete the record in your database, as well as the corresponding coupon on Stripe:

public function create(CreateCouponParameters $parameters)
{
    // Create coupon on Stripe...
    $stripeCoupon = $this->stripeCoupons->create(params: [
        // Parameters to create Stripe coupon...
    ]);

    // Then create coupon in database...
    $coupon = Coupon::query()->create([
        'stripe_id' => $stripeCoupon->id,
        // Parameters to create coupon Eloquent model...
    ]);

    return $coupon;
}

In tests, you can then mock the StripeCouponService dependency so that you can run your tests without actually hitting Stripe’s API:

public function test_can_create_fixed_amount_coupon(): void
{
    $this->mock(StripeCouponService::class, function (MockInterface $mock): void {
        $mock->shouldReceive('create')->once()->andReturn(Coupon::constructFrom([
            'id' => 'jMT0WJUD',
            // And other Stripe coupon parameters you need returning...
        ]));
    });

    // Make request to controller action that type-hints your CouponService class...
    $this
        ->post('/coupons', [
            // Parameters your create coupon endpoint requires...
        ])
        ->assertCreated();

    $this->assertDatabaseHas('coupons', [
        'stripe_id' => 'jMT0WJUD',
    ]);
}
2 likes
lsvagusa's avatar

@martinbean, this is actually what I did when I described my thought process in my initial message, thanks for the insights.

Please or to participate in this conversation.