Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Symfony Bundle Laravel Package

myfatoorah/symfony-bundle

Symfony bundle that integrates the MyFatoorah payment gateway into Symfony applications, with configuration, service definitions, and convenient APIs for creating payments, handling callbacks, and managing transactions.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and register. composer require myfatoorah/symfony-bundle. There is no Flex recipe, so add it manually to config/bundles.php:

    MyFatoorah\SymfonyBundle\MyFatoorahBundle::class => ['all' => true],
    

    Verify the actual FQCN from vendor/myfatoorah/symfony-bundle/src/ before pasting — bundles of this vintage often have inconsistent namespaces.

  2. Configure per environment. Create config/packages/myfatoorah.yaml and wire it to env vars:

    myfatoorah:
        api_key: '%env(MYFATOORAH_API_KEY)%'
        country_code: 'KWT'      # or SAU, ARE, EGY, QAT, BHR, JOR, OMN
        is_test: '%env(bool:MYFATOORAH_TEST)%'
    

    Also add config/packages/test/myfatoorah.yaml with stub values so unit tests boot without secrets. Check the bundle's DependencyInjection/Configuration.php for the exact key names — they are not stable across releases.

  3. Find the service first. Before writing code:

    php bin/console debug:container --show-private | grep -i fatoorah
    php bin/console debug:config myfatoorah
    

    This tells you the service id, the autowireable type, and whether the bundle registered it as public. If you can't autowire it, define an alias in services.yaml.

  4. First use case: a redirect-based invoice payment. The simplest end-to-end flow is: user clicks "Pay" → call the send/initiate method → redirect to the returned InvoiceURL → customer pays on the hosted page → MyFatoorah hits your callback → you verify status → mark order paid. Build this in a dedicated App\Payment\MyFatoorahGateway wrapper service, not in a controller, from day one.

Implementation Patterns

Wrap the bundle in an application-level gateway service. Depend on the bundle client in exactly one place so you can mock it, swap it, or bypass it later:

final class MyFatoorahGateway
{
    public function __construct(private MyFatoorahClient $client) {}

    public function createInvoice(Order $order): string
    {
        $response = $this->client->sendPayment([
            'InvoiceValue'       => (float) $order->getTotal(),
            'CustomerName'       => $order->getCustomerName(),
            'CustomerEmail'      => $order->getCustomerEmail(),
            'CustomerReference'  => (string) $order->getId(),   // your join key
            'CallBackUrl'        => $this->router->generate('payment_callback', [], UrlGeneratorInterface::ABSOLUTE_URL),
            'ErrorUrl'           => $this->router->generate('payment_error', [], UrlGeneratorInterface::ABSOLUTE_URL),
            'Language'           => 'en',                        // or 'ar'
            'NotificationOption' => 'LNK',                       // 'LNK' | 'EML' | 'SMS'
        ]);

        if (empty($response['IsSuccess'])) {
            throw new PaymentFailedException($response['ValidationErrors'] ?? 'Unknown error');
        }

        return $response['Data']['InvoiceURL'];
    }
}

Callback controller: verify, then fulfill. MyFatoorah appends paymentId to your callback URL. Never trust the query string alone — always re-query the gateway:

#[Route('/payment/callback', name: 'payment_callback')]
public function callback(Request $request, MyFatoorahGateway $gw, OrderRepository $orders): Response
{
    $paymentId = $request->query->get('paymentId')
        ?? throw new BadRequestHttpException('Missing paymentId');

    $status = $gw->getPaymentStatus($paymentId);

    if ($status['Data']['InvoiceStatus'] !== 'Paid') {
        return $this->redirectToRoute('payment_failed', ['ref' => $request->query->get('Id')]);
    }

    $order = $orders->findOneByReferenceOrFail($status['Data']['CustomerReference']);

    return $this->redirectToRoute('order_receipt', ['id' => $order->getId()]);
}

Pass the invoice id (Id / InvoiceId) back into your fulfillment path, and store it on the order row at creation time.

Idempotent fulfillment with a webhook as the source of truth. The browser callback can be lost (user closes tab, network drop), and MyFatoorah can retry notifications. The robust pattern is:

  • Register a SupplierNotification / webhook endpoint in the MyFatoorah portal pointing at POST /webhooks/myfatoorah.
  • In both the callback and webhook handlers call one markPaidFromStatus(InvoiceId, status) service method.
  • Guard with a unique constraint on myfatoorah_invoice_id and a DB transaction:
    $this->em->wrapInTransaction(function () use ($order, $status) {
        if ($order->isPaid()) { return; }      // no-op on retry
        $order->markPaid($status['Data']['InvoiceId']);
    });
    
  • Keep both endpoints thin; the shared method is the only thing that mutates money-adjacent state.

Refunds and cancellations. Mirror the same wrapper style:

public function refund(string $paymentId, float $amount, string $reason = 'requested_by_customer'): void
{
    $r = $this->client->makeRefund([
        'Key'         => $paymentId,
        'KeyType'     => 'PaymentId',
        'Amount'      => $amount,
        'Reason'      => $reason,
    ]);
    if (empty($r['IsSuccess'])) { throw new RefundFailedException($r['ErrorMessage'] ?? ''); }
}

Record partial refunds as separate rows — MyFatoorah supports multiple refunds against one payment, and a single boolean flag will not survive the first partial.

Config and secrets. Put the API key in Symfony Secrets (bin/console secrets:set MYFATOORAH_API_KEY) for production, plain .env.local elsewhere. Keep the sandbox key in .env.test. Both keys are account-country-specific, so a developer in Egypt cannot test a Kuwaiti account's flow.

Gotchas and Tips

  • Amount precision is the #1 source of bugs. MyFatoorah expects decimals, and KWD/BHD/OMR have 3 decimal places while SAR/AED/EGP/QAR have 2. Do not cast money to float in your domain model and hope; use integer minor units or a money library and convert only at the boundary. A 1-fils rounding difference causes InvoiceValue mismatch on refunds.
  • The base URL changes between sandbox and production (test URLs are a distinct host, not a flag on the same host). If is_test in config only flips the key and not the endpoint, you will send test keys to production and get opaque auth errors. Verify with debug:config and a curl against the resolved URL.
  • Localhost cannot receive callbacks. Use symfony-cli's expose command, ngrok, or a tunnel, and put the tunnel base URL in an env var (APP_PUBLIC_URL) that feeds CallBackUrl/ErrorUrl. Hardcoding absolute URLs from the request context will send localhost to MyFatoorah and silently fail.
  • IsSuccess is a string "true"/"false" in some responses — depending on the bundle version's deserialization. Compare with filter_var($r['IsSuccess'], FILTER_VALIDATE_BOOLEAN) rather than truthiness of a string "false" (which is truthy). This bites people in refund code paths.
  • Errors are two shapes. Validation failures return ValidationErrors (an array of {Name, Error}); runtime failures return ErrorMessage. Log both raw, but never log the api_key or the full Authorization header.
  • Currency/country mismatch is a silent failure at invoice creation: the invoice currency defaults to the account country's currency unless you pass InvoiceCurrency. Multi-currency storefronts must set it explicitly and confirm the account supports that currency.
  • Notification options: LNK returns InvoiceURL for a redirect; EML/SMS send a hosted link to the customer instead. If you redirect and use EML, customers get two links and both hit your callback — that's fine given idempotency, but confusing in support tickets.
  • Check the bundle's maintained-ness before committing. Score 0 / no stars / 0 dependents means Composer constraints and Symfony 6–7 compatibility are unverified. Read composer.json symfony/* constraints and run composer why-not symfony/framework-bundle. If they conflict with your version, skip the bundle and call the REST API via symfony/http-client — the endpoint surface is small (SendPayment, GetPaymentStatus, MakeRefund, CancelPayment, GetPaymentMethods) and a 200-line MyFatoorahHttpClient is often less maintenance than a stale bundle.
  • Extension point: the bundle's only meaningful extension point is the service definition. Override it in services.yaml with decorates: if you need retry logic, request logging, or PSR-18 client injection. Do not fork the vendor directory.
  • Test the failure path first. Configure ErrorUrl to route to a page that says "payment not completed" and confirms via GetPaymentStatus — customers who abandon the hosted page frequently return through the browser back button, not your callback.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource