PHPackages                             birkof/netopia-mobilpay-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. birkof/netopia-mobilpay-bundle

ActiveSymfony-bundle[Payment Processing](/categories/payments)

birkof/netopia-mobilpay-bundle
==============================

Seamless integration of Netopia MobilPay Payment Gateway into your Symfony application

v3.0.1(2w ago)518.7k↓42.7%4MITPHPPHP ^8.3CI passing

Since Jul 26Pushed 2w ago1 watchersCompare

[ Source](https://github.com/birkof/netopia-mobilpay-bundle)[ Packagist](https://packagist.org/packages/birkof/netopia-mobilpay-bundle)[ RSS](/packages/birkof-netopia-mobilpay-bundle/feed)WikiDiscussions main Synced 3d ago

READMEChangelog (6)Dependencies (8)Versions (21)Used By (0)

Netopia MobilPay Bundle — Architecture Overview
===============================================

[](#netopia-mobilpay-bundle--architecture-overview)

A thin, typed Symfony integration layer over the [`birkof/netopia-mobilpay`](https://packagist.org/packages/birkof/netopia-mobilpay) low-level SDK. It wires the SDK into the Symfony service container and exposes two responsibilities of the Netopia (MobilPay) card-payment protocol:

- **Outbound** — build an RSA‑encrypted payment request to redirect a customer to the gateway.
- **Inbound** — decrypt and authenticate the gateway's IPN (Instant Payment Notification) callback and produce the `` acknowledgement it expects.

The bundle holds **no state, no persistence, and no HTTP controllers**. It builds request objects and parses notifications; the consuming application owns routing, fulfilment, and storage.

> Source of truth for this document: the files under `src/` (14 PHP files, ~1090 LOC), `composer.json`, `phpunit.xml.dist`, and `.github/workflows/ci.yml`. Usage examples live in [`src/Resources/doc/index.md`](./src/Resources/doc/index.md).

---

Requirements
------------

[](#requirements)

From `composer.json`:

DependencyConstraintRole`php``^8.3`runtime`birkof/netopia-mobilpay``^4.0`low-level Netopia SDK (`Mobilpay\…`): crypto + XML protocol`symfony/routing``^4.4 || ^5.0 || ^6.0`generates the absolute confirm/return URLs`symfony/yaml``^4.4 || ^5.0 || ^6.0`loads `Resources/config/services.yaml``symfony/monolog-bundle``^3.7`provides the `logger` service injected into both services`phpunit/phpunit` (dev)`^11.5`test suiteAutoload (PSR‑4): `birkof\NetopiaMobilPay\` → `src/`.

---

Installation
------------

[](#installation)

### 1. Require the package

[](#1-require-the-package)

```
composer require birkof/netopia-mobilpay-bundle
```

### 2. Register the bundle

[](#2-register-the-bundle)

With Symfony Flex this is automatic. Otherwise add it to `config/bundles.php`:

```
// config/bundles.php
return [
    // ...
    birkof\NetopiaMobilPay\NetopiaMobilPayBundle::class => ['all' => true],
];
```

### 3. Configure the gateway

[](#3-configure-the-gateway)

`signature` is **required** — the bundle fails to boot without it. `payment_url`, `public_cert` and `private_key` are optional (defaults: sandbox URL, `null`, `null`). Both `public_cert` and `private_key` accept **either a file path** (resolved relative to `%kernel.project_dir%`) **or the inline PEM content**.

```
# config/packages/netopia_mobilpay.yaml
netopia_mobilpay:
    payment_url: '%env(NETOPIA_MOBILPAY_PAYMENT_URL)%'   # e.g. https://secure.mobilpay.ro (prod)
    public_cert: '%env(NETOPIA_MOBILPAY_PUBLIC_CERT)%'   # path or PEM — seals outbound requests
    private_key: '%env(NETOPIA_MOBILPAY_PRIVATE_KEY)%'   # path or PEM — opens inbound IPNs
    signature:   '%env(NETOPIA_MOBILPAY_SIGNATURE)%'     # REQUIRED
```

```
# .env (or .env.local)
NETOPIA_MOBILPAY_PAYMENT_URL=http://sandboxsecure.mobilpay.ro
NETOPIA_MOBILPAY_PUBLIC_CERT=%kernel.project_dir%/config/netopia/sandbox.public.cer
NETOPIA_MOBILPAY_PRIVATE_KEY=%kernel.project_dir%/config/netopia/sandbox.private.key
NETOPIA_MOBILPAY_SIGNATURE=XXXX-XXXX-XXXX-XXXX-XXXX
```

### 4. Define the confirm and return routes

[](#4-define-the-confirm-and-return-routes)

The bundle generates absolute URLs from two route **names** it expects you to define (`NetopiaMobilPayConfiguration::CONFIRM_URL` / `::RETURN_URL`). These names are mandatory — missing them breaks URL generation at container build time:

```
# config/routes.yaml
netopia_mobilpay_confirm_url:        # server-to-server IPN endpoint
    path: /payment/netopia/confirm
    controller: App\Controller\PaymentController::ipn
    methods: [POST]

netopia_mobilpay_return_url:         # browser lands here after the gateway
    path: /payment/netopia/return
    controller: App\Controller\PaymentController::return
```

---

Usage
-----

[](#usage)

Both entry points are autowired by their interface:

```
public function __construct(
    private NetopiaMobilPayServiceInterface $payments,   // outbound — build a request
    private NetopiaMobilPayIpnHandlerInterface $ipn,      // inbound  — handle the callback
) {}
```

### Start a payment (outbound)

[](#start-a-payment-outbound)

`createCreditCardPaymentObject()` validates the input, builds and RSA‑seals the request, and returns the SDK request object exposing the sealed `env_key` / `data` (and `cipher` / `iv`for block ciphers). Render an auto‑submitting form that POSTs them to the gateway URL.

```
use birkof\NetopiaMobilPay\Configuration\NetopiaMobilPayConfiguration;
use birkof\NetopiaMobilPay\Service\NetopiaMobilPayServiceInterface;
use Symfony\Component\HttpFoundation\Response;

public function checkout(NetopiaMobilPayServiceInterface $payments): Response
{
    $request = $payments->createCreditCardPaymentObject(
        'ORDER-1001',                            // orderId  (required, non-empty)
        '49.99',                                 // amount   (required, positive numeric)
        NetopiaMobilPayConfiguration::CURRENCY_RON, // currency (RON | EUR | USD)
        'Order #1001',                           // details
        [                                        // billing address (optional)
            'type'        => 'person',
            'firstName'   => 'Ion',
            'lastName'    => 'Popescu',
            'address'     => 'Str. Exemplu 1',
            'email'       => 'ion@example.com',
            'mobilePhone' => '0700000000',
        ],
        // [], shipping   [], creditCard   [], extraParameters (token payments)
    );

    return $this->render('payment/redirect.html.twig', [
        'paymentUrl' => $payments->getMobilPayConfiguration()->getPaymentUrl(),
        'envKey'     => $request->getEnvKey(),
        'data'       => $request->getEncData(),
        'cipher'     => $request->getCipher(),
        'iv'         => $request->getIv(),
    ]);
}
```

```
{# templates/payment/redirect.html.twig — auto-submits to Netopia #}

    {% if cipher %}{% endif %}
    {% if iv %}{% endif %}

document.getElementById('netopia').submit();
```

> **PCI note:** leave the `creditCard` argument empty and let the gateway's hosted page collect the card details. Passing a raw PAN/CVV through your server places the whole application in PCI‑DSS SAQ‑D scope.

### Handle the confirmation (inbound IPN)

[](#handle-the-confirmation-inbound-ipn)

Netopia POSTs the encrypted notification to your `netopia_mobilpay_confirm_url` route. Decrypt it, verify the amount/order **yourself**, then acknowledge with ``:

```
use birkof\NetopiaMobilPay\Exception\NetopiaMobilPayException;
use birkof\NetopiaMobilPay\Notification\NetopiaMobilPayIpnHandlerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

public function ipn(Request $request, NetopiaMobilPayIpnHandlerInterface $ipn): Response
{
    try {
        $result = $ipn->decrypt(
            (string) $request->request->get('env_key'),
            (string) $request->request->get('data'),
            $request->request->get('cipher'), // null for legacy RC4 payloads
            $request->request->get('iv'),      // null for legacy RC4 payloads
        );
    } catch (NetopiaMobilPayException $e) {
        // transient failure on our side → ask Netopia to retry
        return new Response(
            $ipn->errorResponse('cannot process', $ipn::ERROR_TYPE_TEMPORARY),
            200, ['Content-Type' => 'application/xml'],
        );
    }

    if ($result->isConfirmed()) {
        // SECURITY: decrypt() proves the sender is Netopia, NOT the amount.
        // Load the order by $result->purchaseId and confirm $result->processedAmount
        // matches what you expected BEFORE marking it paid.
    }

    return new Response(
        $ipn->confirmResponse(),
        200, ['Content-Type' => 'application/xml'],
    );
}
```

`IpnResult` predicates: `isConfirmed()`, `isPaid()`, `isPending()`, `isCanceled()`, `isError()`. See [`src/Resources/doc/index.md`](./src/Resources/doc/index.md) for the full integration guide.

---

Architecture at a glance
------------------------

[](#architecture-at-a-glance)

```
                 Symfony application
                         │
   ┌─────────────────────┼──────────────────────┐
   │ outbound            │            inbound    │
   ▼                     │                       ▼
NetopiaMobilPayService   │     NetopiaMobilPayIpnHandler
 (netopia_mobilpay.      │      (netopia_mobilpay.
        payment)         │            ipn_handler)
   │                     │                       │
   │   both depend on    ▼                       │
   │        NetopiaMobilPayConfiguration  ◄───────┘
   │        (netopia_mobilpay.configuration, private)
   │         · signature, certs, payment URL, routes
   ▼                                             ▼
        birkof/netopia-mobilpay  (Mobilpay\…)
   Request\Card / Request\Sms        Request\RequestAbstract::factoryFromEncrypted
   ::encrypt()  ──► openssl_seal     ::decrypt ──► openssl_open ──► Request\Notify

```

The SDK does the cryptography and XML; this bundle adapts it to Symfony DI and gives the inbound side a typed, vendor‑agnostic result.

---

Components
----------

[](#components)

Grouped by directory under `src/`:

### Bundle + DI

[](#bundle--di)

FileResponsibility`NetopiaMobilPayBundle.php`The bundle. `const ALIAS = 'netopia_mobilpay'`, `const VERSION = '1.4.0'`; returns `NetopiaMobilPayExtension` from `getContainerExtension()`.`DependencyInjection/Configuration.php`Config tree (`getConfigTreeBuilder`): nodes `payment_url`, `public_cert`, `private_key`, `signature`. `signature` is `isRequired()->cannotBeEmpty()` — a missing signature fails config processing rather than booting with a placeholder.`DependencyInjection/NetopiaMobilPayExtension.php`Loads `services.yaml`, processes config, and registers the service graph (below). `inflateServicesInConfig()` turns any `@service`‑prefixed config string into a `Reference`. Secrets are passed to the configuration object **only via method calls** — they are never written to container parameters (which Symfony would dump to the compiled‑container cache in cleartext).**Service graph built by the extension:**

```
netopia_mobilpay.configuration   (NetopiaMobilPayConfiguration, private)
    ← Reference('router')
    ← setPaymentUrl, setProjectDir('%kernel.project_dir%'),
      setPublicCert, setPrivateKey, setSignature
        │
        ├─► netopia_mobilpay.payment       (NetopiaMobilPayService, public)
        │       ← configuration, router, logger
        │
        └─► netopia_mobilpay.ipn_handler   (NetopiaMobilPayIpnHandler, public)
                ← configuration, logger

```

`Resources/config/services.yaml` aliases each interface to its service for autowiring: `NetopiaMobilPayServiceInterface → netopia_mobilpay.payment`, `NetopiaMobilPayIpnHandlerInterface → netopia_mobilpay.ipn_handler`.

### Configuration value object

[](#configuration-value-object)

`Configuration/NetopiaMobilPayConfiguration.php` — runtime holder for the gateway settings, built once and shared by both services.

- Reads `public_cert` / `private_key` as **either a file path (resolved under `%kernel.project_dir%`) or the literal PEM content** (`setPublicCert`/`setPrivateKey` fall back to the raw value when it is not a readable file).
- In its constructor it generates the absolute **confirm** and **return** URLs via the router from two route names it expects the application to define:
    - `netopia_mobilpay_confirm_url` (`const CONFIRM_URL`)
    - `netopia_mobilpay_return_url` (`const RETURN_URL`)
- `resolvePaymentUrl(bool $useTokenEndpoint)` derives the per‑request endpoint from an **immutable base** URL: the base for a normal payment, `base + '/card4'` for a token payment. The base is never mutated, so repeated/token calls cannot accumulate `/card4` or leak across requests on long‑running workers.
- Currency constants: `CURRENCY_RON`, `CURRENCY_EUR`, `CURRENCY_USD`.

### Outbound — payment request building

[](#outbound--payment-request-building)

`Service/NetopiaMobilPayServiceInterface.php` + `NetopiaMobilPayService.php`.

- `createCreditCardPaymentObject($orderId, $amount, $currency, $details, $billingAddress, $shippingAddress, $creditCard, $extraParameters)`:
    1. validates `orderId` (non‑empty), `amount` (positive numeric) and `currency` (one of the `CURRENCY_*` constants) **before** the try/catch, so a specific input error is not masked;
    2. builds a `Mobilpay\Payment\Request\Card` — `signature`, `confirmUrl`, `returnUrl`, a `Mobilpay\Payment\Invoice` (currency/amount/details), optional billing/shipping `Mobilpay\Payment\Address`, optional `Mobilpay\Payment\Instrument\Card`, and token `extraParameters`;
    3. resolves the payment URL for the request;
    4. calls `->encrypt($publicCert)` (RSA envelope) and returns the request object.
- `createSmsPaymentObject($orderId, $serviceId)` — the SMS‑payment equivalent.
- `composeAddressObject()` sets only the fields the SDK's `Address` actually serializes (`type`, `firstName`, `lastName`, `address`, `email`, `mobilePhone`).
- `composeCreditCardObject()` builds a raw‑PAN `CardInstrument`. **Its docblock warns that this server‑side card path places the application in PCI‑DSS SAQ‑D scope;** the hosted payment page (leave `$creditCard` empty) is the recommended flow.

The returned request object exposes the sealed `env_key` / `data`; the application renders an auto‑submitting HTML form POSTing them to `getPaymentUrl()`.

### Inbound — IPN handling (`src/Notification/`)

[](#inbound--ipn-handling-srcnotification)

FileResponsibility`IpnAction.php`Backed `enum IpnAction: string` of gateway actions (`confirmed`, `confirmed_pending`, `paid_pending`, `paid`, `canceled`, `credit`) plus an `Unknown` fallback. `fromValue(?string)` maps unrecognized/null to `Unknown` (forward‑compatible — never throws on a new action).`IpnResult.php``final readonly` DTO — an immutable, vendor‑agnostic view of a decrypted notification. `fromNotify(Notify)` maps the SDK object (money kept as string, `errorCode` cast to int). Predicates: `isError()`, `isConfirmed()`, `isPaid()`, `isPending()`, `isCanceled()`.`NetopiaMobilPayIpnHandlerInterface.php` / `NetopiaMobilPayIpnHandler.php``decrypt(string $envKey, string $encData, ?string $cipher = null, ?string $iv = null): IpnResult` opens the envelope via `RequestAbstract::factoryFromEncrypted()` using the configured private key, then returns `IpnResult::fromNotify()`. `confirmResponse()` / `errorResponse($message, $errorType, $errorCode)` build the `` reply with `DOMDocument` (values XML‑escaped). Constants `ERROR_TYPE_TEMPORARY = 1` (gateway retries) and `ERROR_TYPE_PERMANENT = 2`. Decrypt failures log only the error **code** (never the vendor message, key material, or ciphertext) and surface a generic `NetopiaMobilPayException`.### Errors

[](#errors)

`Exception/NetopiaMobilPayException.php` — the single `extends \Exception` type thrown by the bundle (invalid input, failed encryption, failed/empty/garbage IPN payload).

All `src/` classes declare `declare(strict_types=1)`.

---

Request / response flows
------------------------

[](#request--response-flows)

**Outbound (start a payment):**

```
app ──► NetopiaMobilPayServiceInterface::createCreditCardPaymentObject(...)
          → validate input → build Card(Invoice, [Address], [CardInstrument], [token])
          → resolvePaymentUrl()          → Card::encrypt(public_cert)  [openssl_seal]
        ◄ returns sealed request (env_key + data)
app renders auto-submit  POST → getPaymentUrl()   ──► Netopia hosted page

```

**Inbound (gateway confirmation / IPN):**

```
Netopia ──POST env_key,data[,cipher,iv]──► your route `netopia_mobilpay_confirm_url`
your controller ──► NetopiaMobilPayIpnHandlerInterface::decrypt(...)
          factoryFromEncrypted(private_key)  [openssl_open] → Notify → IpnResult
your controller verifies purchaseId + processedAmount against YOUR order, fulfils,
          then returns confirmResponse()  ──► ""
          (on a transient failure: errorResponse(..., ERROR_TYPE_TEMPORARY) → gateway retries)

```

---

Security model
--------------

[](#security-model)

- **Authenticity is the RSA envelope.** Netopia seals the IPN against your public certificate; only the holder of the matching private key can `openssl_open` it. A successful `decrypt()` therefore authenticates the sender — there is no separate signature on the callback.
- **The bundle cannot verify the amount/order.** It proves the message came from Netopia; it does not know what you expected. The consumer **must** cross‑check `IpnResult::$purchaseId` against a real, unfulfilled order and confirm `IpnResult::$processedAmount` before fulfilling. This boundary is documented in `src/Resources/doc/index.md`.
- **Secrets stay out of the compiled container.** The extension passes `private_key`/`signature` to the configuration object via method calls, not container parameters.
- **No card data by default.** The hosted‑page flow keeps PAN/CVV off your server; the raw‑card path is documented as PCI SAQ‑D scope.

---

Project layout
--------------

[](#project-layout)

```
src/
├── NetopiaMobilPayBundle.php              # bundle entry point
├── Configuration/
│   └── NetopiaMobilPayConfiguration.php   # runtime settings value object
├── DependencyInjection/
│   ├── Configuration.php                  # config tree (signature required)
│   └── NetopiaMobilPayExtension.php       # service graph wiring
├── Exception/
│   └── NetopiaMobilPayException.php
├── Notification/                          # inbound IPN
│   ├── IpnAction.php
│   ├── IpnResult.php
│   ├── NetopiaMobilPayIpnHandlerInterface.php
│   └── NetopiaMobilPayIpnHandler.php
├── Service/                               # outbound requests
│   ├── NetopiaMobilPayServiceInterface.php
│   └── NetopiaMobilPayService.php
└── Resources/
    ├── config/services.yaml               # interface → service aliases
    └── doc/index.md                       # usage / integration guide

tests/                                     # mirrors src/ (Configuration, DependencyInjection,
                                           # Notification, Service)

```

---

Configuration reference
-----------------------

[](#configuration-reference)

Defined in `config/packages/netopia_mobilpay.yaml` (see `src/Resources/doc/index.md` for full examples):

KeyRequiredDefaultNotes`signature`**yes**—merchant signature; processing fails if absent`payment_url`no`http://sandboxsecure.mobilpay.ro`gateway base URL (set the production URL in prod)`public_cert`no`null`file path **or** inline PEM; used to seal outbound requests`private_key`no`null`file path **or** inline PEM; used to open inbound IPNsThe application must also define two routes the configuration generates URLs for: **`netopia_mobilpay_confirm_url`** (server‑to‑server IPN) and **`netopia_mobilpay_return_url`** (browser return).

Inject by interface:

```
public function __construct(
    private NetopiaMobilPayServiceInterface $payments,        // outbound
    private NetopiaMobilPayIpnHandlerInterface $ipn,          // inbound
) {}
```

---

Testing &amp; CI
----------------

[](#testing--ci)

- **PHPUnit 11** (`phpunit.xml.dist`): bootstraps `vendor/autoload.php`, runs the `tests/` suite, declares `src/` as the coverage source, and is strict — `failOnWarning="true"`, `failOnRisky="true"`. Run with `vendor/bin/phpunit` (or `composer test`).
- Current suite: **33 tests / 134 assertions**, mirroring `src/` (config tree, DI wiring, value object, outbound service, IPN enum/DTO/handler). The IPN handler test performs a real `openssl_seal` → `decrypt` round‑trip with a generated key pair.
- **CI** (`.github/workflows/ci.yml`): on push to `main` and on pull requests; matrix **PHP 8.3 and 8.4** (extensions `openssl, mbstring, dom`); steps: `composer validate --strict`, install, `vendor/bin/phpunit`, and a non‑blocking `composer audit`.

---

License
-------

[](#license)

MIT — see [`LICENSE.md`](./LICENSE.md).

###  Health Score

62

—

FairBetter than 99% of packages

Maintenance96

Actively maintained with recent releases

Popularity33

Limited adoption so far

Community13

Small or concentrated contributor base

Maturity87

Battle-tested with a long release history

 Bus Factor1

Top contributor holds 94.1% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~169 days

Recently: every ~502 days

Total

18

Last Release

18d ago

Major Versions

v1.7.0 → v2.0.02022-04-18

v2.0.1 → v3.0.02026-06-17

PHP version history (6 changes)v1.0.0PHP ^5.6|^7.0|^7.1|^7.2

v1.5.0PHP ^5.6|^7.0|^7.1|^7.2|^7.3

v1.6.0PHP ^7.1 || ^7.2 || ^7.3 || ^7.4

v1.7.0PHP ^7.1.3

v2.0.0PHP ^7.4|^8.0

v3.0.0PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/60cda17bf267a622a313ea6965a7b394b311c2db7d2789e0d7092f7a2fd036a4?d=identicon)[birkof](/maintainers/birkof)

---

Top Contributors

[![birkof](https://avatars.githubusercontent.com/u/65848?v=4)](https://github.com/birkof "birkof (32 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")[![gabiudrescu](https://avatars.githubusercontent.com/u/5156054?v=4)](https://github.com/gabiudrescu "gabiudrescu (1 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/birkof-netopia-mobilpay-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/birkof-netopia-mobilpay-bundle/health.svg)](https://phpackages.com/packages/birkof-netopia-mobilpay-bundle)
```

###  Alternatives

[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M737](/packages/sylius-sylius)[pimcore/pimcore

Content &amp; Product Management Framework (CMS/PIM/E-Commerce)

3.8k3.8M508](/packages/pimcore-pimcore)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M577](/packages/shopware-core)[prestashop/prestashop

PrestaShop is an Open Source e-commerce platform, committed to providing the best shopping cart experience for both merchants and customers.

9.1k17.8k](/packages/prestashop-prestashop)[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

9421.6k61](/packages/open-dxp-opendxp)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
