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

Clickatell Bundle Laravel Package

archer/clickatell-bundle

Symfony bundle that integrates the Clickatell SMS gateway, providing configuration and a service for sending text messages through Clickatell’s API. Useful for adding SMS notifications to Symfony applications.

View on GitHub
Deep Wiki
Context7

Getting Started

First, correct the mental model. This is a Symfony bundle, not a Laravel package. There is no extra.laravel block, no service provider, no facade. composer require archer/clickatell-bundle into a modern Laravel app will either fail to resolve (old symfony/framework-bundle constraint, likely <2.3) or install an unused vendor/ directory. Treat day one as a source-reading spike, not an install.

  1. Install into a scratch project (never the production app) so you can read the source without fighting platform constraints:

    composer create-project symfony/skeleton /tmp/spike
    cd /tmp/spike
    composer require archer/clickatell-bundle --ignore-platform-reqs
    

    If resolution fails entirely, git clone https://github.com/archer-soft/ClickatellBundle /tmp/spike gives you the same files.

  2. Read these files first, in this order — they contain 100% of the durable IP:

    • composer.json — confirms target Symfony/PHP versions and whether the constraint is pinned or dev-master.
    • DependencyInjection/Configuration.php and …Extension.php — the config keys (api_id, user, password, endpoint).
    • Resources/config/services.xml — the service ID you'd inject if it were usable.
    • The SMS client class itself — this is the only thing you actually want.
  3. Decide the path before writing code:

    • Laravel app (most likely): do not install. Extract the request shape and error handling from the client class, then implement a ~150-line Guzzle/Http client against Clickatell's current REST API. The bundle's transport targets the legacy api.clickatell.com/http/sendmsg endpoint that Clickatell has retired, so the code is reference-only.
    • Legacy Symfony 2.x app: register in AppKernel::registerBundles(), then configure:
      # app/config/config.yml
      archer_clickatell:
          api_id:   "%clickatell_api_id%"
          user:     "%clickatell_user%"
          password: "%clickatell_password%"
      
      and inject the service into a controller.
  4. First real use case: a single outbound transactional SMS (OTP or alert). That is the only capability evidenced by the bundle. Do not assume delivery receipts, inbound, templating, or scheduling exist — they don't.

  5. If you're on Laravel and just need an SMS channel, skip the bundle entirely and start with:

    // config/services.php
    'clickatell' => [
        'key' => env('CLICKATELL_API_KEY'),
    ],
    
    use Illuminate\Support\Facades\Http;
    
    $response = Http::withToken(config('services.clickatell.key'))
        ->acceptJson()
        ->post('https://platform.clickatell.com/messages', [
            'to'      => ['27820000000'],
            'content' => 'Order 1234 shipped',
        ]);
    

Implementation Patterns

1. Legacy Symfony: service injection + defensive body parsing. This is the pattern the bundle was built for, and the pattern most likely to bite you. The legacy Clickatell API returns HTTP 200 with ID: <msgid> on success and ERR: <code>, <text> on failure:

public function alertAction(Request $request, ClickatellService $sms)
{
    $result = trim($sms->send('+27820000000', 'Order 1234 shipped'));

    if (str_starts_with($result, 'ERR:') || !str_starts_with($result, 'ID:')) {
        $this->get('logger')->error('clickatell.reject', ['body' => $result]);
        throw new \RuntimeException('SMS rejected by Clickatell');
    }

    return new Response('queued');
}

2. Laravel: port the client, don't install the bundle. A thin owned client is the correct adaptation:

namespace App\Services\Sms;

use Illuminate\Support\Facades\Http;
use RuntimeException;

class ClickatellClient
{
    public function __construct(
        private readonly string $apiKey,
        private readonly string $baseUrl = 'https://platform.clickatell.com',
    ) {}

    public function send(string $to, string $body, ?string $from = null): string
    {
        $response = Http::withToken($this->apiKey)
            ->acceptJson()
            ->asJson()
            ->timeout(10)
            ->post("{$this->baseUrl}/messages", array_filter([
                'to'      => [$this->normalize($to)],
                'content' => $body,
                'from'    => $from,
            ]));

        if ($response->failed() || $response->json('messages.0.error')) {
            throw new RuntimeException(
                'Clickatell send failed: '.$response->status().' '.$response->body()
            );
        }

        return (string) $response->json('messages.0.apiMessageId');
    }

    private function normalize(string $to): string
    {
        return preg_replace('/[^\d+]/', '', $to);
    }
}

3. Provider abstraction — the highest-value pattern. The bundle is a liability only because it's a hard dependency. Isolate it behind an interface and the vendor becomes swappable:

interface SmsSender
{
    public function send(string $to, string $body): string; // returns provider message id
}

Bind in a service provider using a config flag:

$this->app->singleton(SmsSender::class, fn ($app) => match (config('sms.driver')) {
    'clickatell' => new ClickatellClient(config('services.clickatell.key')),
    'twilio'     => new TwilioClient(config('services.twilio')),
    default      => throw new \InvalidArgumentException('Unknown SMS driver'),
});

4. Notification channel (Laravel-native ergonomics). Once SmsSender is bound, wire it into Laravel Notifications so feature code never touches the transport:

class ClickatellChannel
{
    public function __construct(private SmsSender $sms) {}

    public function send($notifiable, $notification): void
    {
        $message = $notification->toSms($notifiable);
        $this->sms->send($notifiable->routeNotificationFor('sms'), $message->body);
    }
}

Call sites become $user->notify(new OrderShipped($order)) — clean, queueable, testable.

5. Queue + retry + idempotency. The bundle is synchronous and has no retry logic. In Laravel, wrap sends in a job with bounded retries and an idempotency key so a retry doesn't double-charge:

class SendSmsJob implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public array $backoff = [5, 30, 120];

    public function __construct(
        public string $to,
        public string $body,
        public string $idempotencyKey,
    ) {}

    public function handle(SmsSender $sender): void
    {
        if (cache()->has("sms:sent:{$this->idempotencyKey}")) {
            return;
        }
        $sender->send($this->to, $this->body);
        cache()->put("sms:sent:{$this->idempotencyKey}", true, now()->addDay());
    }

    public function failed(\Throwable $e): void
    {
        // alert ops — otherwise drops are invisible
    }
}

6. Testing. In Laravel, Http::fake() gives you full coverage of the ported client without the bundle:

Http::fake([
    'platform.clickatell.com/*' => Http::response([
        'messages' => [['apiMessageId' => 'abc', 'accepted' => true]],
    ]),
]);

$this->assertSame('abc', app(SmsSender::class)->send('+27820000000', 'hi'));

For legacy Symfony tests, stub the service ID behind the same SmsSender interface — that's how you get coverage of the intent without depending on the bundle.

7. Integration order when you actually ship. Vendor account + sender-ID registration first (long lead), then adapter, then channel, then queue, then observability. Code is the short pole.

Gotchas and Tips

  • HTTP 200 is not success. The legacy Clickatell API returns ERR: <code>, <text> with a 200 status. Bundles of this vintage often check only the transport code. This is the single highest-value five-minute code read and the dominant production failure mode. Always parse the body.
  • Credentials in the query string. Legacy auth is ?user=…&password=…&api_id=…, which lands in proxy logs, APM traces, and error output. Never carry this pattern forward in a Laravel rebuild — use Authorization headers and env/secret manager. If an existing Symfony app does this, treat it as an incident, not a config.
  • Composer constraints are likely incompatible with any currently supported Laravel or PHP 8. If you must inspect the source, --ignore-platform-reqs in a scratch directory only. Never in the production composer.json — an unpinned dev-master bundle is unreproducible.
  • Not a Laravel package, no matter what the listing says. No auto-discovery, no service provider, no config/clickatell.php, no facade. composer require will not "just work". Don't burn a day fighting the resolver.
  • No idempotency at the API layer. Any naive retry will double-send and double-bill. Put an idempotency key in front of every send (see the job pattern above) — cache, Redis, or a sent_sms table.
  • Number normalization is the top source of "message went nowhere" tickets. Store E.164 (+2782…); normalize at the adapter boundary; reject non-parseable input loudly rather than letting the vendor drop it silently.
  • No delivery receipts, no inbound, no templating, no scheduling, no credit reporting. The bundle evidences outbound send only. If the product needs any of those, they are bespoke work regardless of vendor — do not scope them onto this dependency.
  • No queue, batching, or backoff. At a few hundred messages/day this is invisible; at burst it becomes a blocking call inside the request cycle. Always wrap in a queue job with $tries and a failed() handler. Silent queue loss is a real failure class.
  • Symfony 4+ replacement is symfony/clickatell-notifier. If you were going to fork this bundle for a modern Symfony app, don't — the Notifier bridge is framework-versioned and maintained. Same rationale for Laravel: write the 150-line client.
  • Logging discipline. Log the message id, recipient country prefix, and vendor response code; never log the message body or full number (SMS content and MSISDNs are PII). Redact before any APM/tracing span captures the request.
  • License check before anything else. Read LICENSE in the repo. If it's absent or ambiguous, that's a hard stop for commercial use even as a reference implementation.
  • Extension points are thin. There's a service config, but no event hooks, no middleware, no response-mapping layer worth overriding. If you find yourself wanting to extend the bundle, that's the signal to replace it.
  • Frame it for the team as "build a Clickatell adapter", not "integrate this package." The ramp-up cost of reading this bundle exceeds the cost of writing the replacement, and the framing determines whether you inherit a liability or ship a 200-line asset.
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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator