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

Paymill Bundle Laravel Package

memeoirs/paymill-bundle

A Symfony bundle for integrating the Paymill payment provider, offering configuration and services to handle payments, transactions, and webhooks with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

The bundle is a thin Symfony DI wrapper around the paymill/paymill-php SDK, so onboarding is mostly about getting the API key configured and pulling the PaymillContext out of the container.

Minimal steps

  1. composer require memeoirs/paymill-bundle — expect it to pull an old paymill/paymill-php. Pin the SDK version explicitly so a transitive update doesn't surprise you.
  2. Register the bundle manually. On Symfony 2.x/3.x add it to AppKernel::registerBundles(). On Symfony 4+/Flex it will not be auto-discovered — add it to config/bundles.php and, if the bundle's services aren't autoconfigured, define the context service yourself (see Gotchas).
  3. Configure keys — typically the extension alias is memeoirs_paymill:
# config/packages/memeoirs_paymill.yaml
memeoirs_paymill:
    private_key: '%env(PAYMILL_PRIVATE_KEY)%'
    public_key:  '%env(PAYMILL_PUBLIC_KEY)%'

Use test_... keys in dev/staging and live_... in prod. The prefix is what actually switches Paymill's environment, not a separate flag.

  1. Confirm the service id before writing anything:
bin/console debug:container paymill --show-private
# commonly `paymill.context` (a Paymill\Services\PaymillContext)

First use case: a one-off charge. This is 90% of what people install the bundle for. The flow is token → payment → transaction, and the token must be generated in the browser.

<script src="https://bridge.paymill.com/"></script>
<script>
  paymill.createToken({
    number: '4111111111111111', exp_month: '12', exp_year: '2025', cvc: '123'
  }, function (error, result) {
    if (error) { /* show message */ }
    else { document.querySelector('#paymill-token').value = result.token; }
  });
</script>
// Controller
public function chargeAction(Request $request)
{
    $context = $this->get('paymill.context');

    $payment = $context->getPaymentService()->create($request->get('paymill_token'));

    $transaction = $context->getTransactionService()->create(
        4200,               // amount in cents
        'EUR',              // ISO-4217, uppercase
        $payment,
        'Order #'.$orderId
    );

    return $this->json(['id' => $transaction->getId()]);
}

Implementation Patterns

Isolate the bundle behind your own service. Don't get('paymill.context') in controllers. Wrap it once so you can swap gateways later and so tests can mock one narrow interface.

class PaymillGateway
{
    public function __construct(private PaymillContext $context) {}

    public function charge(int $cents, string $currency, string $token, string $description): string
    {
        $payment = $this->context->getPaymentService()->create($token);
        $tx = $this->context->getTransactionService()
                           ->create($cents, $currency, $payment, $description);

        return $tx->getId();
    }
}

Returning customers. Persist the client and payment IDs from your first charge, then reuse the payment object instead of re-tokenizing:

$client  = $context->getClientService()->create($email, 'Customer #'.$id);
$payment = $context->getPaymentService()->create($token, $client);
// later:
$context->getTransactionService()->create(4200, 'EUR', $payment, 'Renewal');

Store $payment->getId() in your own table — that ID is the whole point of not re-asking for the card.

Subscriptions. Create an offer (amount/currency/interval) or use a null offer for a custom interval, then attach the payment:

$offer = $context->getOfferService()->create(4200, 'EUR', 'month', 'Pro plan');
$subscription = $context->getSubscriptionService()->create($payment, $offer, $client);

Renewals are charged by Paymill, not by you — so your app needs webhooks to stay in sync.

Webhooks. Register a URL once per environment and handle events in a single controller with a switch on the event type:

$webhook = $context->getWebhookService()->createUrl(
    'https://app.example.com/paymill/webhook',
    ['transaction.succeeded', 'subscription.succeeded', 'refund.succeeded']
);

Respond 200 fast, enqueue the real work, and make handlers idempotent — Paymill will retry.

Refunds. Full or partial against the original transaction object:

$context->getRefundService()->refund($transaction, 1000, 'Partial return');

Integration tips

  • Mirror every Paymill object in your own DB with the Paymill ID as a unique index; the gateway should never be your source of truth for order state.
  • Keep the API version pinned in composer.json. The SDK is dated and its response shape is what your code is coupled to.
  • Give the SDK its own HTTP timeout if you can (inject a configured Guzzle client) so a slow Paymill call doesn't hold a PHP-FPM worker for 30s.

Gotchas and Tips

The package is essentially abandoned. Last release 2016, 12 stars, zero dependents. No Symfony 4/5/6 support, no services.yaml autoconfiguration, no PSR-11/private service handling. On modern Symfony you'll likely need to redefine the context service yourself rather than rely on the bundle:

services:
    paymill.context:
        class: Paymill\Services\PaymillContext
        arguments: ['%env(PAYMILL_PRIVATE_KEY)%']

Budget a half-day for wiring, not for the feature. And confirm with your merchant account that Paymill is still an acquirer you can onboard with before committing — the vendor's own history is checkered.

Amounts are integer cents, always. 42.00 EUR is 4200. Floats will silently truncate. Currency codes are uppercase ISO-4217 on the transaction call.

A token is single-use and short-lived. Never persist tok_*; persist the resulting pay_* payment ID. If you retry a failed charge with the same token, it fails for a different reason and you'll chase the wrong bug.

No idempotency keys in this SDK. A double-submit or a retried HTTP call can double-charge. Guard at the application layer: a unique constraint on your order ID, a "charge in progress" state, and a disabled submit button.

Exception handling is coarse. Nearly everything surfaces as Paymill\Services\PaymillException with a numeric getResponseCode() and getErrorMessage(). Map the codes you care about (declined, expired card, invalid token) to user-facing messages and let the rest bubble to a 500 with the response code logged. Do not show raw gateway messages to customers.

Don't log request bodies. Tokens and client metadata flow through the SDK; a monolog handler dumping the request for debugging will put payment-adjacent data in your log aggregation. Log transaction IDs and response codes only.

Debugging the container. When the service doesn't exist, it's almost always because the bundle is registered but the extension alias is wrong. bin/console debug:config memeoirs_paymill will tell you instantly; if the alias is wrong it errors out rather than guessing.

No 3-D Secure / SCA support. The SDK predates PSD2 enforcement. If you're billing EU cardholders, this is a compliance problem, not just a feature gap — plan for a gateway migration rather than layering on top of this.

Testing. Because the bundle exists mainly to hand you a PaymillContext, all your tests should mock that object or your own PaymillGateway. Don't try to fake HTTP — the SDK's internals are not worth faking, and there is no official sandbox mode beyond the test_ key prefix, which still makes real network calls.

Extension points are thin. Your realistic options are (a) override the service definition, (b) decorate your own gateway, (c) for anything involving request/response behaviour, fork the SDK. Don't expect bundle-level hooks for custom HTTP clients or serializers.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin