PHPackages                             vimatech/laravel-integrations - 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. [API Development](/categories/api)
4. /
5. vimatech/laravel-integrations

ActiveLibrary[API Development](/categories/api)

vimatech/laravel-integrations
=============================

Config-driven ports &amp; adapters foundation for integrating external providers in Laravel: capability drivers, context routing and normalized webhooks.

v1.0.1(today)02↑2900%MITPHPPHP ^8.3CI passing

Since Jun 26Pushed todayCompare

[ Source](https://github.com/vimatech-io/laravel-integrations)[ Packagist](https://packagist.org/packages/vimatech/laravel-integrations)[ Docs](https://github.com/vimatech-io/laravel-integrations)[ RSS](/packages/vimatech-laravel-integrations/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (8)Versions (3)Used By (0)

Laravel Integrations
====================

[](#laravel-integrations)

[![CI](https://github.com/vimatech-io/laravel-integrations/actions/workflows/ci.yml/badge.svg)](https://github.com/vimatech-io/laravel-integrations/actions/workflows/ci.yml)[![Latest Version on Packagist](https://camo.githubusercontent.com/27d2fdd4b2e3488eb7e6906b93e0ea9644eaa4fc89a526ae186ae200ee0f8943/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f76696d61746563682f6c61726176656c2d696e746567726174696f6e732e737667)](https://packagist.org/packages/vimatech/laravel-integrations)[![Total Downloads](https://camo.githubusercontent.com/11d20568de84f6b275c4b07ddcbf9adcdb5f5ec47f28ef8d9b885183a4e970c6/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f76696d61746563682f6c61726176656c2d696e746567726174696f6e732e737667)](https://packagist.org/packages/vimatech/laravel-integrations)[![License](https://camo.githubusercontent.com/997d32a22c2d8e4db65a1517a64700a76341f546093450abd3d5e627471a8e94/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f76696d61746563682f6c61726176656c2d696e746567726174696f6e732e737667)](https://packagist.org/packages/vimatech/laravel-integrations)

A config-driven **ports &amp; adapters** foundation for integrating external providers in Laravel.

Adding a provider means writing **one isolated adapter class and a config entry** — you never touch business logic, routing, or the webhook pipeline. It generalizes Laravel's own `Manager`/driver pattern with **context routing** (route a capability to a driver by country, tenant, …) and a **normalized inbound webhook pipeline** (verify → translate → de-duplicate → dispatch canonical events).

This package is intentionally **domain-free**: it ships no concrete vendor and no business logic. The *capabilities* (what an adapter actually does) are contracts defined by **your** application or by consumer packages.

Why Laravel Integrations?
-------------------------

[](#why-laravel-integrations)

External providers leak into business logic in predictable ways: a `match ($country)` here, a hard-coded SDK client there, bespoke webhook controllers everywhere. This package gives you a single, boring seam:

```
Business logic ──▶ Integrations::for('einvoice')->resolve(['country' => $invoice->country])
                                   │
                   ┌───────────────┴───────────────┐
                   ▼                                ▼
            ChorusProAdapter                    SdiAdapter        ← swap/add via config only

```

Your business logic depends on a **capability contract**; the concrete provider is selected by configuration and runtime context.

Feature Matrix
--------------

[](#feature-matrix)

FeatureSupportedConfig-driven drivers (ports &amp; adapters)✅Context routing (by country, tenant, …)✅Per-tenant driver overrides (from your DB)✅Normalized inbound webhook pipeline✅Canonical, provider-agnostic events✅Webhook idempotency (cache or database)✅Pluggable credential storage✅Strict resolution (`resolveStrict`)✅Test fakes &amp; driver-usage assertions✅Octane / FrankenPHP safe✅Concrete vendors / business logic❌ (you own them)UI❌Installation
------------

[](#installation)

```
composer require vimatech/laravel-integrations
```

The service provider is auto-discovered. Publish the config (and, if you use the database idempotency store, the migration):

```
php artisan vendor:publish --tag=integrations-config
php artisan vendor:publish --tag=integrations-migrations   # only for the "database" webhook store
```

Requires **PHP 8.3+** and **Laravel 11, 12 or 13**.

Core concepts
-------------

[](#core-concepts)

ConceptDescription**Capability**A named contract owned by a consumer (e.g. `einvoice`, `payments`). This package never sees it.**Driver**An adapter implementing a capability. Marked with `Vimatech\Integrations\Contracts\Driver`.**`IntegrationManager`**Resolves a driver by capability + key from config, à la `Illuminate\Support\Manager`.**`ContextRouter`**Resolves a driver by default or by a context array (`['country' => 'FR']`).**`ResolvesTenantDriver`**Optional contract to override the driver per tenant from your database.**`WebhookTranslator`**Verifies an inbound request and translates it into canonical events.**`CanonicalEvent`**Provider-agnostic event base class with a stable idempotency key.Configuration
-------------

[](#configuration)

`config/integrations.php` (abbreviated — see the published file for full comments):

```
return [
    'credentials' => [
        'store' => env('INTEGRATIONS_CREDENTIAL_STORE', 'config'), // 'config' | 'encrypted'
    ],

    'webhooks' => [
        'prefix'      => env('INTEGRATIONS_WEBHOOK_PREFIX', 'integrations/webhooks'),
        'middleware'  => ['api'],
        'event_store' => env('INTEGRATIONS_WEBHOOK_STORE', 'cache'), // 'cache' | 'database'
        'event_table' => 'integration_webhook_events',
        'event_ttl'   => 86400,
    ],

    'capabilities' => [
        'einvoice' => [
            'default' => env('EINVOICE_DRIVER', 'chorus_pro'),

            'routing' => [
                'by'  => 'country',                 // the context dimension to route on
                'map' => [
                    'FR' => 'chorus_pro',           // contextValue => driverKey
                    'IT' => 'sdi',
                ],
            ],

            'drivers' => [
                'chorus_pro' => [
                    'class'   => \App\Integrations\ChorusProAdapter::class,
                    'api_key' => env('CHORUS_PRO_KEY'),
                ],
                'sdi' => [
                    'class'   => \App\Integrations\SdiAdapter::class,
                    'api_key' => env('SDI_KEY'),
                ],
            ],

            'webhooks' => [
                'enabled'    => true,
                'translator' => null,               // null = use the driver if it is a WebhookTranslator
            ],
        ],
    ],
];
```

Adding a driver
---------------

[](#adding-a-driver)

**1. Define the capability contract** (in your app or a consumer package). It extends the `Driver`marker:

```
use Vimatech\Integrations\Contracts\Driver;

interface EInvoiceNetwork extends Driver
{
    public function send(Invoice $invoice): string;
}
```

**2. Write the adapter.** Adapter constructors receive the resolved config array:

```
final class ChorusProAdapter implements EInvoiceNetwork
{
    public function __construct(private array $config) {}

    public function send(Invoice $invoice): string
    {
        // talk to Chorus Pro using $this->config['api_key']
    }
}
```

**3. Register it in config** under the capability's `drivers` map. That's it — no business-logic changes.

> Need bespoke construction (a pre-built SDK client, etc.)? Register a factory:
>
> ```
> app(IntegrationManager::class)->extend('einvoice', 'chorus_pro', function (array $config) {
>     return new ChorusProAdapter(ChorusClient::make($config['api_key']));
> });
> ```

Resolving &amp; routing drivers
-------------------------------

[](#resolving--routing-drivers)

```
use Vimatech\Integrations\Facades\Integrations;

// Explicit key
$driver = Integrations::driver('einvoice', 'sdi');

// Capability default
$driver = Integrations::driver('einvoice');

// Route by context — uses the capability's `routing.by` dimension
$driver = Integrations::for('einvoice')->resolve(['country' => $invoice->country]);
```

Context resolution order for `for($capability)->resolve($context)`:

1. A bound [`ResolvesTenantDriver`](#per-tenant-overrides) (per-tenant override).
2. Static routing on the configured `routing.by` dimension.
3. The capability `default`.
4. Otherwise `UnresolvableDriver` is thrown.

Need to **fail instead of falling back** to the default when context doesn't match? Use `resolveStrict()`:

```
Integrations::for('einvoice')->resolveStrict(['country' => 'DE']); // throws UnresolvableDriver
```

Other router methods: `default()`, `via('sdi')`, and `key($context)` (returns the resolved driver key without instantiating).

Per-tenant overrides
--------------------

[](#per-tenant-overrides)

Bind an implementation of `ResolvesTenantDriver` to let the database decide. Return `null` to defer to static routing:

```
use Vimatech\Integrations\Contracts\ResolvesTenantDriver;

final class TenantDriverResolver implements ResolvesTenantDriver
{
    public function resolveDriverKey(string $capability, array $context): ?string
    {
        return Tenant::find($context['tenant'] ?? null)
            ?->integrationKey($capability);
    }
}

// In a service provider:
$this->app->bind(ResolvesTenantDriver::class, TenantDriverResolver::class);
```

```
Integrations::for('einvoice')->resolve(['tenant' => $tenant->id, 'country' => 'FR']);
```

Webhooks
--------

[](#webhooks)

A single generic inbound route is registered:

```
POST {prefix}/{capability}/{driver?}

```

For each request, the pipeline:

1. Checks that webhooks are **enabled** for the capability (else `404`).
2. Resolves a `WebhookTranslator` — the configured `webhooks.translator`, or the resolved driver if it implements `WebhookTranslator`.
3. Calls `verify($request)`. On failure it dispatches `WebhookRejected` and returns `403`.
4. Dispatches `WebhookReceived`.
5. Calls `translate($request)` and, for each `CanonicalEvent`, enforces **idempotency** via the event key store before dispatching it through Laravel's event system.

Make a driver translate its own webhooks:

```
use Illuminate\Http\Request;
use Vimatech\Integrations\Contracts\Driver;
use Vimatech\Integrations\Contracts\WebhookTranslator;
use Vimatech\Integrations\Webhooks\CanonicalEvent;

final class ChorusProAdapter implements EInvoiceNetwork, WebhookTranslator
{
    public function __construct(private array $config) {}

    public function verify(Request $request): bool
    {
        return hash_equals(
            $this->config['webhook_secret'],
            (string) $request->header('X-Signature'),
        );
    }

    public function translate(Request $request): iterable
    {
        foreach ($request->input('events', []) as $raw) {
            yield new InvoiceDelivered($raw['id']);
        }
    }
}
```

Define canonical events your application listens for. The `idempotencyKey()` must be **stable** across redeliveries:

```
use Vimatech\Integrations\Webhooks\CanonicalEvent;

final class InvoiceDelivered extends CanonicalEvent
{
    public function __construct(public readonly string $invoiceId) {}

    public function idempotencyKey(): string
    {
        return "einvoice:delivered:{$this->invoiceId}";
    }
}
```

**Idempotency store** is configurable via `webhooks.event_store`:

- `cache` (default) — uses the atomic `Cache::add()` operation.
- `database` — uses a unique index on the published `integration_webhook_events` table.

> **Idempotency is claimed *before* dispatch.** An event key is marked as seen as soon as it is accepted, so a redelivery is skipped even if a listener fails. Make your canonical-event listeners **queued** (`ShouldQueue`): the webhook returns `200` immediately, and listener failures are retried by the queue rather than by the provider re-sending the webhook. Keep listeners idempotent on your own side too.

Credentials &amp; secure storage
--------------------------------

[](#credentials--secure-storage)

By default credentials are read straight from config (`store => 'config'`). Set `store => 'encrypted'` to decrypt the keys listed in a driver's `encrypted` array using Laravel's encrypter:

```
'drivers' => [
    'chorus_pro' => [
        'class'     => ChorusProAdapter::class,
        'api_key'   => env('CHORUS_PRO_KEY'),   // ciphertext at rest
        'encrypted' => ['api_key'],
    ],
],
```

To store credentials with [`vimatech/laravel-secure-fields`](https://github.com/vimatech-io/laravel-secure-fields) or any other backend, bind your own `CredentialStore` — the package never assumes a vendor:

```
use Vimatech\Integrations\Contracts\CredentialStore;

$this->app->singleton(CredentialStore::class, SecureFieldsCredentialStore::class);
```

The `integrations:list` command
-------------------------------

[](#the-integrationslist-command)

```
php artisan integrations:list
```

Prints every configured capability with its drivers, default, routing map and webhook status.

Testing
-------

[](#testing)

Swap the manager for a fake and assert which drivers your code used:

```
use Vimatech\Integrations\Facades\Integrations;

it('uses the SDI driver for Italian invoices', function () {
    $fake = Integrations::fake();

    app(InvoiceSender::class)->send($italianInvoice);

    $fake->assertDriverUsed('einvoice', 'sdi');
});
```

`Integrations::fake()` keeps the **real routing logic** (so context routing still resolves to the right key) while returning recording doubles. Provide your own capability fakes when you need behaviour:

```
$fake = Integrations::fake([
    'einvoice:sdi' => new FakeEInvoiceNetwork(),   // your double implementing the capability contract
]);
```

Available assertions on the fake: `assertDriverUsed()`, `assertDriverNotUsed()`, `assertNothingUsed()`, and `used()` for the raw record.

Octane &amp; FrankenPHP
-----------------------

[](#octane--frankenphp)

The package is built for long-lived workers. It keeps **no static or global state**; the only mutable state is the per-key driver instance cache on the `IntegrationManager` singleton — which is a performance *win* under workers, since each adapter is built once and reused across requests.

Three rules keep it safe and fast in worker mode:

1. **Keep adapters stateless per request.** Read from the injected `$config`; never store request-bound state (the current user, the `Request`, a cart) on an adapter, or it will leak into the next request. Driver resolution itself is just array lookups plus a one-time container build.
2. **Queue your canonical-event listeners** (`ShouldQueue`) — see the [webhook idempotency note](#webhooks). The worker returns `200` immediately and retries happen on the queue.
3. **Per-tenant credentials via `extend()`?** The instance cache is keyed by `capability:key`, not by tenant. That is correct when credentials come from config (static per key). Only if you register an `extend()` factory that *captures* per-tenant credentials do you need to avoid the shared cache — resolve those per tenant in your own code instead.

If (and only if) you intentionally keep request state on an adapter, flush the cache each request:

```
use Laravel\Octane\Events\RequestReceived;
use Vimatech\Integrations\IntegrationManager;

Event::listen(RequestReceived::class, fn () => app(IntegrationManager::class)->forgetDrivers());
```

Leave this off otherwise — it discards the build cache that makes workers fast.

`env()` is only ever read inside `config/integrations.php`, and routes are registered once, so the package is fully compatible with `config:cache` and `route:cache`.

Quality
-------

[](#quality)

```
composer test     # Pest + orchestra/testbench
composer format   # Laravel Pint
composer analyse  # PHPStan (level max) + Larastan
```

Contributing
------------

[](#contributing)

Contributions are welcome.

Please ensure:

- Tests pass (`composer test`)
- PHPStan passes (`composer analyse`)
- Code style is formatted with Pint (`composer format`)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review our [Security Policy](SECURITY.md) for reporting vulnerabilities.

License
-------

[](#license)

The MIT License (MIT). Please see the [License File](LICENSE) for more information.

Credits
-------

[](#credits)

Built and maintained by [Vimatech](https://vimatech.io). Created by [Adel Zemzemi](https://github.com/adelzemzemi).

###  Health Score

42

—

FairBetter than 89% of packages

Maintenance100

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% 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 ~0 days

Total

2

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3283664f06a0db1bfdbf282b2691363365c2f73569bcd99d63f5aaa52900ff55?d=identicon)[adelzemzemi](/maintainers/adelzemzemi)

---

Top Contributors

[![adelzemzemi](https://avatars.githubusercontent.com/u/272534830?v=4)](https://github.com/adelzemzemi "adelzemzemi (33 commits)")

---

Tags

laraveladapterwebhooksports and adaptersdriversintegrationsvimatech

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/vimatech-laravel-integrations/health.svg)

```
[![Health](https://phpackages.com/badges/vimatech-laravel-integrations/health.svg)](https://phpackages.com/packages/vimatech-laravel-integrations)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[laravel/ai

The official AI SDK for Laravel.

9782.1M162](/packages/laravel-ai)[defstudio/telegraph

A laravel facade to interact with Telegram Bots

815320.5k3](/packages/defstudio-telegraph)[moonshine/moonshine

Laravel administration panel

1.3k239.9k76](/packages/moonshine-moonshine)[simplestats-io/laravel-client

Analytics for Laravel. Track visitors, registrations, and payments. Discover which channels actually drive revenue, not just traffic. Server-side, GDPR compliant, ad-blocker proof.

5019.3k](/packages/simplestats-io-laravel-client)[neuron-core/neuron-laravel

Official Neuron AI Laravel SDK.

11125.7k](/packages/neuron-core-neuron-laravel)

PHPackages © 2026

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