Centralize LemonSqueezy Business Logic
Lucky you! You found an early release chapter - it will be fully polished and published shortly!
This Chapter isn't quite ready...
Rest assured, the gnomes are hard at work
completing this video!
So far, we've been focused on getting our checkout up and running, and we nailed it! But our code is kind of scattered around the controller. Wouldn't it be much more convenient to have everything related to LemonSqueezy's API in a separate class? Of course it would! So let's get organizing!
We need to locate all LemonSqueezy-related code in the controller and move it into
a separate service so it's easier to maintain, re-use, and test. To do that,
in src/Store/
, create a new class called LemonSqueezyApi
. Make this
final readonly
. Now we can move our createLsCheckoutUrl()
method. I'll copy
this big block, remove it, and paste it in our new class - and this time, make
it public
. Since we know this is LemonSqueezy-related because it's in
LemonSqueezyApi
, we can just change the name to createCheckoutUrl()
to keep
it simple.
// ... lines 1 - 9 | |
final readonly class LemonSqueezyApi | |
{ | |
public function createCheckoutUrl(HttpClientInterface $lsClient, ShoppingCart $cart, ?User $user): string | |
{ | |
if ($cart->isEmpty()) { | |
throw new \LogicException('Nothing to checkout!'); | |
} | |
$products = $cart->getProducts(); | |
$variantId = $products[0]->getLsVariantId(); | |
$attributes = []; | |
if ($user) { | |
$attributes['checkout_data']['email'] = $user->getEmail(); | |
// ... line 24 | |
} | |
// ... lines 26 - 75 | |
} | |
} |
Next, let's grab $lsClient
and $cart
, and above, turn them into constructor
dependencies with public function __construct()
. Paste, we'll also
simplify $lsClient
and just call it $client
. Above this argument, add
#[Target('lemonSqueezyClient')]
, add private
before each property.
// ... lines 1 - 9 | |
use Symfony\Contracts\HttpClient\HttpClientInterface; | |
// ... line 11 | |
final readonly class LemonSqueezyApi | |
{ | |
public function __construct( | |
#[Target('lemonSqueezyClient')] | |
private HttpClientInterface $client, | |
private ShoppingCart $cart, | |
// ... lines 18 - 20 | |
) { | |
} | |
// ... lines 23 - 88 | |
} |
And finally, change this $cart
variable to a property with $this->cart
. We'll do
the same thing for the remaining $cart
variables. And while we're here, we'll
also change $lsClient
to $this->client
. Nice.
// ... lines 1 - 23 | |
public function createCheckoutUrl(?User $user): string | |
{ | |
if ($this->cart->isEmpty()) { | |
throw new \LogicException('Nothing to checkout!'); | |
} | |
// ... line 29 | |
$products = $this->cart->getProducts(); | |
// ... lines 31 - 37 | |
if (count($products) === 1) { | |
// ... lines 39 - 44 | |
} else { | |
$attributes['custom_price'] = $this->cart->getTotal(); | |
// ... lines 47 - 57 | |
} | |
// ... lines 59 - 61 | |
$response = $this->client->request(Request::METHOD_POST, 'checkouts', [ | |
// ... lines 63 - 87 | |
} | |
// ... lines 89 - 90 |
Now we need a service to generate URLs. We can inject that into the constructor
with UrlGeneratorInterface $urlGenerator
. Then, replace $this->generateUrl()
with $this->urlGenerator->generate()
.
// ... lines 1 - 8 | |
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | |
// ... lines 10 - 11 | |
final readonly class LemonSqueezyApi | |
{ | |
public function __construct( | |
// ... lines 15 - 17 | |
private UrlGeneratorInterface $urlGenerator, | |
// ... lines 19 - 20 | |
) { | |
} | |
// ... line 23 | |
public function createCheckoutUrl(?User $user): string | |
{ | |
// ... lines 26 - 59 | |
$attributes['product_options']['redirect_url'] = $this->urlGenerator->generate('app_order_success', [], UrlGeneratorInterface::ABSOLUTE_URL); | |
// ... lines 61 - 87 | |
} | |
} |
We also need access to the parameters. We could inject the entire ParameterBagInterface
service, which lets us
access any parameter, but since we only need one - storeId
- let's inject
that directly.
In our constructor, add: private readonly string $storeId,
. Above, add the PHP
attribute with #[Autowire('%env(LEMON_SQUEEZY_STORE_ID)%')]
. And finally,
replace every instance of $this->getParameter()
with $this->storeId
. I only
see it once here, so that's pretty easy.
// ... lines 1 - 11 | |
final readonly class LemonSqueezyApi | |
{ | |
public function __construct( | |
// ... lines 15 - 18 | |
#[Autowire('%env(LEMON_SQUEEZY_STORE_ID)%')] | |
private string $storeId, | |
) { | |
} | |
// ... line 23 | |
public function createCheckoutUrl(?User $user): string | |
{ | |
// ... lines 26 - 61 | |
$response = $this->client->request(Request::METHOD_POST, 'checkouts', [ | |
'json' => [ | |
'data' => [ | |
// ... lines 65 - 66 | |
'relationships' => [ | |
'store' => [ | |
'data' => [ | |
// ... line 70 | |
'id' => $this->storeId, | |
], | |
], | |
// ... lines 74 - 79 | |
], | |
], | |
], | |
]); | |
// ... lines 84 - 87 | |
} | |
} |
Now, back in OrderController::checkout()
, let's get rid of these unused
dependencies and inject LemonSqueezyApi $lsApi
instead. Below, use the
service with $lsCheckoutUrl = $lsApi->createCheckoutUrl();
.
// ... lines 1 - 17 | |
class OrderController extends AbstractController | |
{ | |
// ... lines 20 - 59 | |
'/checkout', name: 'app_order_checkout') | (|
public function checkout( | |
LemonSqueezyApi $lsApi, | |
// ... line 63 | |
): Response { | |
$lsCheckoutUrl = $lsApi->createCheckoutUrl($user); | |
return $this->redirect($lsCheckoutUrl); | |
} | |
// ... lines 69 - 89 | |
} |
Testing time! Let's make sure we can still checkout. On our site, reload, select "Classic Lemonade", add one to the cart, and click "Checkout with LemonSqueezy". Yes! We're on the LemonSqueezy checkout page and everything looks great!
Okay, now that we know the checkout's working, can we make $lsStoreUrl
in the
success()
method dynamic? Indeed! And LemonSqueezy has an API endpoint for
just such an occasion! In the API docs, find the "Retrieve a store"
endpoint... and check out the example response on the right here. It looks
like we can read the URL from attributes
, so, back in our code, in
LemonSqueezyApi
, create a new public method. Call it retrieveStoreUrl()
, and
have it return a string
. Inside, add
$response = $this->client->request(Request::METHOD_GET, 'stores/' . $this->storeId)
.
Below, write $lsStore = $response->toArray()
and finally,
return $lsStore['data']['attributes']['url']
.
// ... lines 1 - 11 | |
final readonly class LemonSqueezyApi | |
{ | |
// ... lines 14 - 89 | |
public function retrieveStoreUrl(): string | |
{ | |
$response = $this->client->request(Request::METHOD_GET, 'stores/' . $this->storeId); | |
$lsStore = $response->toArray(); | |
// ... lines 94 - 98 |
Back in the success()
method, inject LemonSqueezyApi $lsApi,
and replace
this hard-coded URL with $lsStoreUrl = $lsApi->retrieveStoreUrl()
.
// ... lines 1 - 17 | |
class OrderController extends AbstractController | |
{ | |
// ... lines 20 - 69 | |
'/checkout/success', name: 'app_order_success') | (|
public function success( | |
// ... lines 72 - 74 | |
): Response | |
{ | |
// ... line 77 | |
$lsStoreUrl = $lsApi->retrieveStoreUrl(); | |
// ... lines 79 - 89 | |
} | |
} |
Time for another test! Back on our site, pick one of our delicious lemonades - I'll choose apple this time - and add it to the cart. On the cart page, click the "Checkout" button again, fill in our credentials and billing address, click "Pay", and finally, in the "successful" modal, click "Continue". Tada! Here's our flash message! It still works!
Next: Let’s assign a LemonSqueezy customer to the corresponding user in our system so we know which purchases they made.