Skip to content

Implementing a custom SMS transport for a SMS service is a rare event! Take a look at SMS services before implementing a new transport as the service may already have an implementation. The Fake service provides a way for testing messages in a development environment.

If you decide to implement a new SMS transport for a publicly accessible service, consider making it available as a new contrib project. Contrib projects are able to implement the required code pieces without any modifications to code owned by the SMS Framework project, as evident below.

Implementation ​

In a custom module, add the following boilerplate:

Services ​

yaml
services:
  autoconfigure: true
  autowire: true

  Drupal\my_module\MyModuleTransportFactory:
    parent: notifier.transport_factory.abstract
    tags:
      - { name: texter.transport_factory }

Factory class ​

php
namespace Drupal\my_module;

use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
use Symfony\Component\Notifier\Transport\Dsn;
use Symfony\Component\Notifier\Transport\TransportInterface;

final class MyModuleTransportFactory extends AbstractTransportFactory {

  protected function getSupportedSchemes(): array {
    // Make up a "scheme" which will be used in the `sms.transports` DSN.
    return ['my-module-scheme'];
  }

  public function create(Dsn $dsn): TransportInterface {
    return new MyModuleTransport();
  }
}

Transport class ​

php
namespace Drupal\my_module;

use Symfony\Component\Notifier\Message\MessageInterface;
use Symfony\Component\Notifier\Message\SentMessage;
use Symfony\Component\Notifier\Message\SmsMessage;
use Symfony\Component\Notifier\Transport\AbstractTransport;

final readonly class MyModuleTransport extends AbstractTransport {

  protected function doSend(MessageInterface $message): SentMessage {
    assert($message instanceof SmsMessage);
    // @todo send the message here.
    // If you need a dependency from the service container, add it to the
    // transport factory then pass it down to the constructor of this class.
    // See "Dependencies" section below.
  }

  public function __toString() {
    // Return the DSN of the transport, the scheme should be the same per factory.
    return 'my-module-scheme://default';
  }

  public function supports(MessageInterface $message): bool {
    // Add other criteria if you need the message to have more specific characteristics.
    return $message instanceof SmsMessage;
  }
}

Dependencies ​

When a transport needs a dependency, like Guzzle, pass it down from the factory:

Using the code above as a base.

Services ​

No changes to services.yml are required since the above template uses autowiring.

Factory class ​

Add dependencies to the constructor, then pass along the dependencies to the transport.

php
final class MyModuleTransportFactory extends AbstractTransportFactory {
  public function __construct( 
    private \Psr\Http\Client\ClientInterface $httpClient, 
  ) {} 
  
  public function create(Dsn $dsn): TransportInterface {
    return new MyModuleTransport(
      // Pass along the dependency here:
      $this->httpClient, 
    );
  }
}

Transport class ​

php
final readonly class MyModuleTransport extends AbstractTransport {
  public function __construct( 
    private \Psr\Http\Client\ClientInterface $httpClient, 
  ) {} 

  protected function doSend(MessageInterface $message): SentMessage {
    assert($message instanceof SmsMessage);
    // @todo send the message here.
    $this->httpClient->send(...); 
  }
  
  // ...
}

Then configure the transport just like other transports.

Configuration ​

In the site directory, usually sites/default/, create a services.yml file.

Add to settings.php:

php
$settings['container_yamls'][] = __DIR__ . '/services.yml';

Add to services.yml:

yaml
parameters:
  sms.transports:
    mytransport: 'my-module-scheme://default'

Then clear Drupal caches:

sh
drush cr

Continue at the Sending documentation to use the newly configured channel. Noting that the recipient you send to must be a valid SmsRecipientInterface.