Build Paystack Subscriptions in Symfony
To build Paystack subscriptions in Symfony, create a plan, initialize a transaction with the plan code, handle subscription webhooks to update a Doctrine entity, and use Security Voters to gate premium content behind active subscription checks.
Subscription Entity
// src/Entity/Subscription.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Subscription
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\OneToOne(targetEntity: User::class)]
private User $user;
#[ORM\Column(length: 100, unique: true)]
private string $subscriptionCode;
#[ORM\Column(length: 100)]
private string $planCode;
#[ORM\Column(length: 100)]
private string $emailToken;
#[ORM\Column(length: 20)]
private string $status = 'active';
#[ORM\Column(nullable: true)]
private ?\DateTimeInterface $nextPaymentDate = null;
// Getters and setters...
}
Subscribe Endpoint
#[Route('/subscribe', methods: ['POST'])]
public function subscribe(Request $request, PaystackService $paystack): Response
{
$user = $this->getUser();
$planCode = $request->request->get('plan_code');
$result = $paystack->initializeWithPlan($user->getEmail(), $planCode);
if ($result['status'] ?? false) {
return $this->redirect($result['data']['authorization_url']);
}
return new Response('Failed', 400);
}
Webhook Handlers
private function handleSubscriptionCreate(array $data, EntityManagerInterface $em): void
{
$email = $data['customer']['email'];
$user = $em->getRepository(User::class)->findOneBy(['email' => $email]);
if (!$user) return;
$sub = $em->getRepository(Subscription::class)->findOneBy(['user' => $user]);
if (!$sub) {
$sub = new Subscription();
$sub->setUser($user);
}
$sub->setSubscriptionCode($data['subscription_code']);
$sub->setPlanCode($data['plan']['plan_code']);
$sub->setEmailToken($data['email_token'] ?? '');
$sub->setStatus('active');
$em->persist($sub);
$em->flush();
}
Subscription Voter
// src/Security/Voter/SubscriptionVoter.php
namespace App\Security\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class SubscriptionVoter extends Voter
{
protected function supports(string $attribute, mixed $subject): bool
{
return $attribute === 'ACTIVE_SUBSCRIPTION';
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user) return false;
$sub = $user->getSubscription();
return $sub && $sub->getStatus() === 'active';
}
}
// In controller:
#[IsGranted('ACTIVE_SUBSCRIPTION')]
#[Route('/premium')]
public function premium(): Response
{
return $this->render('premium.html.twig');
}
Cancel Subscription
#[Route('/subscription/cancel', methods: ['POST'])]
public function cancel(PaystackService $paystack, EntityManagerInterface $em): Response
{
$sub = $this->getUser()->getSubscription();
if (!$sub) {
return $this->redirectToRoute('subscribe');
}
$result = $paystack->disableSubscription($sub->getSubscriptionCode(), $sub->getEmailToken());
if ($result['status'] ?? false) {
$sub->setStatus('cancelled');
$em->flush();
}
return $this->redirectToRoute('dashboard');
}
Key Takeaways
- ✓Paystack handles recurring billing. Create plans once and subscribe customers to them.
- ✓Use a Doctrine entity for subscription tracking with status, subscription_code, and email_token.
- ✓Webhook events drive subscription state. Handle subscription.create, charge.success, and invoice.payment_failed.
- ✓Symfony Security Voters provide fine-grained access control for subscription-gated features.
- ✓Store email_token for cancellation. It comes in the subscription.create webhook.
- ✓Use Symfony Messenger for async webhook processing.
Frequently Asked Questions
- Can I use Symfony Security for subscription checks?
- Yes. Security Voters are the Symfony way to implement fine-grained authorization. Create a voter that checks the subscription status and use #[IsGranted] on your controllers.
- How do I handle plan upgrades?
- Cancel the current subscription and create a new one. Paystack does not support direct plan changes.
- What if a renewal fails?
- Paystack retries automatically. You receive invoice.payment_failed webhooks. Update the status to past_due and notify the customer.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon