PHPackages                             moonlydays/php-kannel - 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. [HTTP &amp; Networking](/categories/http)
4. /
5. moonlydays/php-kannel

ActiveLibrary[HTTP &amp; Networking](/categories/http)

moonlydays/php-kannel
=====================

PHP client for the Kannel SMS gateway — sendsms, admin, and inbound DLR/MO handling.

0.0.1(3mo ago)07MITPHPPHP ^8.2CI passing

Since Apr 23Pushed 2mo agoCompare

[ Source](https://github.com/MoonlyDays/php-kannel)[ Packagist](https://packagist.org/packages/moonlydays/php-kannel)[ RSS](/packages/moonlydays-php-kannel/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (1)Dependencies (15)Versions (2)Used By (0)

PHP Kannel
==========

[](#php-kannel)

[![Tests](https://github.com/moonlyDays/php-kannel/actions/workflows/tests.yml/badge.svg)](https://github.com/moonlyDays/php-kannel/actions/workflows/tests.yml)[![Latest Version on Packagist](https://camo.githubusercontent.com/03963f0dcfadcf67cb8704242b3f5a0e23aca2237b3cfae9447a6f8284e042c4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d6f6f6e6c79646179732f7068702d6b616e6e656c2e737667)](https://packagist.org/packages/moonlydays/php-kannel)[![License](https://camo.githubusercontent.com/b558be5b5a7300dde0d6c6bbfa11f980422f0b8f9703fbaa1dfdd20fb5d1f2f5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6d6f6f6e6c79646179732f7068702d6b616e6e656c2e737667)](https://packagist.org/packages/moonlydays/php-kannel)

A PSR-compliant PHP client for the [Kannel](https://www.kannel.org/) SMS gateway. Handles outbound SMS via `sendsms`, bearerbox admin commands, and inbound DLR/MO callback parsing — with typed value objects, enums, and a clean extensibility surface.

Features
--------

[](#features)

- **Send SMS** via Kannel's `sendsms` HTTP interface (GET or POST)
- **Bearerbox admin API** — typed methods for `status`, `suspend`, `resume`, `shutdown`, `log-level`, `add-smsc`, `remove-smsc`, and more
- **Inbound DLR parsing** — PSR-7 `ServerRequest` → typed `DeliveryReport`
- **Inbound MO parsing** — PSR-7 `ServerRequest` → typed `IncomingMessage`
- **Multi-gateway routing** via `GatewayRegistry`
- **PSR-18 HTTP client** (Guzzle as the default, swappable)
- **PSR-17 factories**, **PSR-14 events**, **PSR-3 logging** — all optional
- **FakeClient** test double for consumer-side tests
- **Readonly value objects**, backed enums, strict types throughout

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

[](#requirements)

- PHP 8.2+
- A reachable Kannel installation (for sendsms and/or admin)

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

[](#installation)

```
composer require moonlydays/php-kannel
```

Quick start
-----------

[](#quick-start)

```
use MoonlyDays\Kannel\Client;
use MoonlyDays\Kannel\Enum\HttpMethod;
use MoonlyDays\Kannel\Gateway;
use MoonlyDays\Kannel\Message\SmsBuilder;

$gateway = new Gateway(
    name: 'default',
    sendSmsUrl: 'https://kannel.example.com/cgi-bin/sendsms',
    username: 'user',
    password: 'pass',
    httpMethod: HttpMethod::Post,
);

$client = new Client($gateway);

$sms = (new SmsBuilder())
    ->to('+14155550123')
    ->from('MyBrand')
    ->text('Your verification code is 123456')
    ->build();

$result = $client->send($sms);

if ($result->isAccepted()) {
    // $result->status, $result->kannelMessageId, $result->rawBody
}
```

Configuration
-------------

[](#configuration)

### Gateway

[](#gateway)

`Gateway` is an immutable value object carrying everything needed to talk to one Kannel installation.

```
use MoonlyDays\Kannel\AdminCredentials;
use MoonlyDays\Kannel\Enum\HttpMethod;
use MoonlyDays\Kannel\Gateway;

$gateway = new Gateway(
    name: 'primary',
    sendSmsUrl: 'https://kannel.example.com/cgi-bin/sendsms',
    username: 'user',
    password: 'pass',
    httpMethod: HttpMethod::Post,
    admin: new AdminCredentials(                       // optional — only if using admin API
        url: 'http://kannel.example.com:13000',
        password: 'admin-secret',
    ),
    defaultSmscId: 'operator-a',                       // optional per-gateway default
    defaultDlrUrl: 'https://your.app/kannel/dlr',      // optional fallback dlr-url
    timeoutSeconds: 30,
);
```

The `httpMethod` choice is deliberate — the library doesn't pick for you. Use `Post` when you want credentials out of access logs; `Get` when Kannel is configured to accept only GET.

### Multi-gateway

[](#multi-gateway)

```
use MoonlyDays\Kannel\GatewayRegistry;

$registry = new GatewayRegistry(
    gateways: [$primary, $fallback, $operatorA],
    default: 'primary',
);

$client = new Client($registry);

$client->send($sms);                       // default gateway
$client->send($sms, via: 'operatorA');     // named gateway
```

Single-gateway users don't see the registry — `new Client($gateway)` normalizes internally.

### Swapping the HTTP client

[](#swapping-the-http-client)

Every PSR dependency is optional. Pass your own to override the Guzzle defaults.

```
$client = new Client(
    gateway: $gateway,
    httpClient: $yourPsr18Client,
    requestFactory: $yourPsr17Factory,
    streamFactory: $yourPsr17Factory,
    dispatcher: $yourPsr14Dispatcher,
    logger: $yourPsr3Logger,
);
```

Sending messages
----------------

[](#sending-messages)

`SmsBuilder` is the only supported way to construct an `Sms`. It validates invariants (recipient required, text XOR binary) on `build()`.

```
use MoonlyDays\Kannel\Enum\Coding;
use MoonlyDays\Kannel\Enum\DlrEvent;
use MoonlyDays\Kannel\Enum\MessageClass;

$sms = (new SmsBuilder())
    ->to('+14155550123')
    ->from('ALERT')
    ->text('Héllo 👋')
    ->coding(Coding::Ucs2)                 // GSM-7 | Binary | UCS-2
    ->messageClass(MessageClass::Flash)    // or ->flash() shorthand
    ->dlrMask(DlrEvent::Delivered, DlrEvent::DeliveryFailure, DlrEvent::SmscRejected)
    ->dlrUrl('https://your.app/kannel/dlr?msgid=abc-123')
    ->smsc('operator-a')
    ->priority(1)
    ->validity(1440)                       // minutes
    ->deferred(5)                          // minutes
    ->metaData('smpp', 'service_type', 'VERIFY')
    ->build();
```

### Sending results

[](#sending-results)

```
$result = $client->send($sms);

$result->status;             // SendStatus enum — Accepted | Queued | Malformed | ...
$result->httpStatusCode;     // raw HTTP status from Kannel
$result->rawBody;            // raw response body, e.g. "0: Accepted for delivery"
$result->kannelMessageId;    // extracted when present in the body
$result->isAccepted();
$result->isQueued();
$result->isSuccess();        // Accepted or Queued
```

Delivery reports (DLR)
----------------------

[](#delivery-reports-dlr)

### Configuring `dlr-url`

[](#configuring-dlr-url)

Kannel substitutes placeholders (`%d`, `%p`, `%T`, `%I`, …) in your callback URL before hitting it. The library gives you two ways to build the URL — explicit or via a binding.

**Explicit — full control over query key names:**

```
use MoonlyDays\Kannel\Dlr\DlrUrl;

$dlrUrl = (new DlrUrl('https://your.app/kannel/dlr'))
    ->withQuery('msgid', 'abc-123')         // literal value
    ->withStatus('status')                  // binds to %d
    ->withPhone('to')                       // binds to %p
    ->withTimestamp('ts')                   // binds to %T
    ->withSmscMessageId('smsid')            // binds to %I
    ->build();

$sms = (new SmsBuilder())
    ->to('+14155550123')
    ->text('hello')
    ->dlrUrl($dlrUrl)
    ->dlrMask(DlrEvent::Delivered, DlrEvent::DeliveryFailure)
    ->build();
```

**Convention-based — shared with the inbound parser:**

```
use MoonlyDays\Kannel\Inbound\DlrBinding;

$binding = new DlrBinding();                          // sensible defaults
$dlrUrl = $binding->toDlrUrl('https://your.app/kannel/dlr');

$sms = (new SmsBuilder())
    ->to('+14155550123')
    ->text('hello')
    ->dlrUrl($dlrUrl)
    ->dlrMask(DlrEvent::Delivered, DlrEvent::DeliveryFailure)
    ->build();
```

The same `$binding` is then passed to `InboundHandler` on the receiving side, so the two halves agree on query key names without duplicated string literals.

### Receiving DLR callbacks

[](#receiving-dlr-callbacks)

```
use MoonlyDays\Kannel\Inbound\InboundHandler;

$inbound = new InboundHandler();                      // default bindings
// — or —
$inbound = new InboundHandler(
    dlrBinding: $binding,
    dispatcher: $psr14Dispatcher,                     // fires DlrReceived event
);

// Inside your PSR-7 controller / middleware:
public function handleDlr(ServerRequestInterface $request): ResponseInterface
{
    $dlr = $inbound->receiveDlr($request);
    //  DeliveryReport {
    //      status: DlrEvent,
    //      recipient, sender, smscId, smscMessageId,
    //      receivedAt: ?DateTimeImmutable,
    //      replyText,
    //      raw: array         // all query/body params, for custom fields
    //  }

    // persist, update order state, whatever

    return new Response(200);
}
```

The handler reads both query params and parsed body, so it works regardless of whether Kannel was configured to POST or GET the callback.

Mobile originated messages (MO)
-------------------------------

[](#mobile-originated-messages-mo)

```
use MoonlyDays\Kannel\Inbound\InboundHandler;
use MoonlyDays\Kannel\Inbound\MoBinding;

$inbound = new InboundHandler(
    moBinding: new MoBinding(),                       // or customize keys to match your sms-service config
    dispatcher: $psr14Dispatcher,                     // fires MoReceived event
);

public function handleMo(ServerRequestInterface $request): ResponseInterface
{
    $mo = $inbound->receiveMo($request);
    //  IncomingMessage {
    //      from, to, text, binary, udh,
    //      coding: ?Coding,
    //      smscId, receivedAt, raw
    //  }

    return new Response(200);
}
```

Admin API
---------

[](#admin-api)

Requires the gateway to have `AdminCredentials` attached. Each command maps to a typed method.

```
use MoonlyDays\Kannel\Admin\AdminClient;
use MoonlyDays\Kannel\Enum\LogLevel;

$admin = new AdminClient($gateway);

$status = $admin->status();           // GatewayStatus DTO parsed from status.xml
$text   = $admin->statusText();       // raw plain-text status

$admin->suspend();
$admin->isolate();
$admin->resume();
$admin->flushDlr();
$admin->reloadLists();
$admin->setLogLevel(LogLevel::Debug);
$admin->removeSmsc('operator-a');
$admin->addSmsc('operator-a');
$admin->shutdown();
$admin->restart();
```

`GatewayStatus` exposes version, uptime string, SMS counters, DLR queue depth, a list of `SmscStatus` and a list of `BoxStatus`.

Constructing an `AdminClient` on a gateway without admin credentials throws `ConfigurationException`.

Events (PSR-14)
---------------

[](#events-psr-14)

Pass any PSR-14 dispatcher to receive events:

```
$client = new Client($gateway, dispatcher: $dispatcher);
$inbound = new InboundHandler(dispatcher: $dispatcher);
```

EventDispatched byFired`MessageSending``Client::send()`Before the transport runs`MessageSent``Client::send()`After the transport returns (success *or* Kannel err)`MessageFailed``Client::send()`When the transport throws`DlrReceived``InboundHandler`After `receiveDlr()` parses successfully`MoReceived``InboundHandler`After `receiveMo()` parses successfullyAll events are readonly DTOs carrying the relevant `Sms` / `Gateway` / `SendResult` / `DeliveryReport` / `IncomingMessage`.

Logging (PSR-3)
---------------

[](#logging-psr-3)

```
$client = new Client($gateway, logger: $logger);
```

The client emits:

- `debug` when a send begins
- `info` after a response is parsed
- `error` when the transport throws

Testing
-------

[](#testing)

`FakeClient` is a drop-in `ClientInterface` implementation that records sends and returns configurable results.

```
use MoonlyDays\Kannel\Testing\FakeClient;
use MoonlyDays\Kannel\Enum\SendStatus;
use MoonlyDays\Kannel\Http\SendResult;

$fake = new FakeClient();
$service->sendWelcomeSms($user, $fake);

expect($fake->count())->toBe(1);
expect($fake->sent()[0]->sms->to)->toBe('+14155550123');
expect($fake->sent()[0]->via)->toBeNull();
expect($fake->messages()[0]->text)->toStartWith('Welcome');
```

Configure per-gateway failure to test fallback logic:

```
$fake->willReturnFor('broken', new SendResult(
    status: SendStatus::InternalError,
    httpStatusCode: 500,
    rawBody: '4: Internal error',
));

$ok  = $fake->send($sms);                   // uses default (accepted)
$bad = $fake->send($sms, via: 'broken');    // returns the configured failure
```

### Running the package's own tests

[](#running-the-packages-own-tests)

```
composer test                # full suite (Pest)
composer test:unit           # unit tests only
composer test:feature        # feature tests only
composer stan                # PHPStan at level max + strict rules
composer pint                # code style
composer rector:dry          # Rector dry-run
```

Exception handling
------------------

[](#exception-handling)

Every exception thrown by this package implements `MoonlyDays\Kannel\Exception\ExceptionInterface`, so you can catch them all with one type:

```
use MoonlyDays\Kannel\Exception\ExceptionInterface;

try {
    $client->send($sms);
} catch (ExceptionInterface $e) {
    // library-level error
}
```

ExceptionMeaning`TransportException`HTTP-level failure (network, DNS, TLS) reaching Kannel`AdminCommandFailedException`Admin command returned 4xx/5xx`MalformedResponseException``status.xml` unparseable, DLR missing required keys, unknown status code`ConfigurationException`Package misused (no recipient, missing admin creds, etc.)`GatewayNotFoundException`Registry lookup failedKannel answering with a non-success status (e.g. `3: Malformed data`) is **not** an exception — it comes back as a `SendResult` with a non-success `SendStatus`.

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

34

—

LowBetter than 75% of packages

Maintenance83

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity36

Early-stage or recently created project

 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

Unknown

Total

1

Last Release

92d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/87d541941e03738e9760a7ea35c5ce19347b67e29eb81a216e872b4158b6dc61?d=identicon)[MoonlyDays](/maintainers/MoonlyDays)

---

Top Contributors

[![MoonlyDays](https://avatars.githubusercontent.com/u/32462579?v=4)](https://github.com/MoonlyDays "MoonlyDays (6 commits)")

---

Tags

psr-18mopsr-14smsgatewaysmsckanneldlr

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/moonlydays-php-kannel/health.svg)

```
[![Health](https://phpackages.com/badges/moonlydays-php-kannel/health.svg)](https://phpackages.com/packages/moonlydays-php-kannel)
```

###  Alternatives

[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k16](/packages/tempest-framework)[drupal/core-recommended

Locked core dependencies; require this project INSTEAD OF drupal/core.

6942.5M425](/packages/drupal-core-recommended)[aws/aws-sdk-php

AWS SDK for PHP - Use Amazon Web Services in your PHP project

6.2k543.5M2.7k](/packages/aws-aws-sdk-php)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M600](/packages/shopware-core)[typo3/cms-core

TYPO3 CMS Core

3713.2M5.2k](/packages/typo3-cms-core)

PHPackages © 2026

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