Login to bookmark this video
Buy Access to Course
08.

Centralize LemonSqueezy Business Logic

|

Share this awesome video!

|

Lucky you! You found an early release chapter - it will be fully polished and published shortly!

This Chapter isn't quite ready...

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com

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.

78 lines | src/Store/LemonSqueezyApi.php
// ... 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.

90 lines | src/Store/LemonSqueezyApi.php
// ... 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.

90 lines | src/Store/LemonSqueezyApi.php
// ... 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().

90 lines | src/Store/LemonSqueezyApi.php
// ... 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.

90 lines | src/Store/LemonSqueezyApi.php
// ... 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();.

91 lines | src/Controller/OrderController.php
// ... lines 1 - 17
class OrderController extends AbstractController
{
// ... lines 20 - 59
#[Route('/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'].

98 lines | src/Store/LemonSqueezyApi.php
// ... 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().

92 lines | src/Controller/OrderController.php
// ... lines 1 - 17
class OrderController extends AbstractController
{
// ... lines 20 - 69
#[Route('/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.