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
FakeHttpClientbeneath a real provider - Capability discovery —
hasCapability(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
- Implement
SmsProviderin theSms\Providers\namespace - Implement
fromConstants()to read credentials fromSMS_*constants - Register the class in
providerCandidates()(jethro-sms/src/factory.php) so auto-detection can find it - Set
SMS_PROVIDERto 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.