Skip to main content

Provider Abstraction

The SmsProvider interface (jethro-sms/src/SmsProvider.php) is the key abstraction that makes the SMS system flexible and testable.

The Interface

Every send is a batch — one call carries every (message, recipients) entry for the whole send action:

interface SmsProvider {
public static function fromConstants(bool $tfa = false): static;
public function getKey(): string;
public static function getConstants(): array;
public static function usagePreference(): int;

/** @param array<int, array{message: string, recipients: SmsRecipient[]}> $entries
* @return \Result<SmsDeliveryBatch, string> */
public function send(array $entries, SmsSender $sender, ?int $sendAt = null, bool $preview = false): \Result;

/** @return \Result<SenderID[], string> */
public function getSenderIds(bool $getAll = false): \Result;
/** @return \Result<int, string> */
public function getBalance(): \Result;
/** @return \Result<SmsDelivery, string> */
public function updateDelivery(SmsDelivery $delivery): \Result;
public function hasCapability(SmsCapability $cap): bool;
}

See SMS Providers for the full method list and per-provider capability table.

Why an Interface?

  • Gateway independence — swap between FiveCentSMS, Cellcast, SMS Broadcast, or a custom provider without changing calling code
  • Testability — mock the provider in tests, or swap in a FakeHttpClient beneath a real provider
  • Capability discoveryhasCapability(SmsCapability) lets callers check what's supported before attempting an operation, rather than calling and catching a failure

The Decorator Chain

The actual provider used at runtime is a chain of decorators, outermost first (see Provider chain assembly for the full rationale):

LocalBalanceSmsProvider ← local/DB balance override + enforcement
→ OverridingSmsProvider ← sender validation + send cooloff
→ DbLoggingSmsProvider ← database persistence
→ TokenExpandingSmsProvider ← per-recipient token expansion
→ [Concrete Provider] ← talks to the gateway

Each decorator implements SmsProvider and delegates to the inner provider after adding its own behavior — each layer adds exactly one responsibility.

Adding a New Provider

  1. Implement SmsProvider in the Sms\Providers\ namespace
  2. Implement fromConstants() to read credentials from SMS_* constants
  3. Register the class in providerCandidates() (jethro-sms/src/factory.php) so auto-detection can find it
  4. Set SMS_PROVIDER to the new provider's short key, or rely on auto-detection once its required constants are defined

The rest of the system — UI, CLI, database logging, token expansion — works unchanged.

Test Mode

FakeHttpClient (abstract, jethro-sms/src/FakeHttpClient.php) replaces the real HTTP client when SMS_TESTMODE is on, so the full stack (validation, token expansion, database logging) can be tested without contacting a real gateway. Fidelity varies by provider — e.g. CellcastFakeHttpClient returns realistic JSON for every endpoint, while older fakes return a bare 'OK'. See SMS Providers for per-provider test mode notes.

See Also