定制 thinwrap/notifications 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

thinwrap/notifications

Composer 安装命令:

composer require thinwrap/notifications

包简介

Unified PHP facade over 35 notification providers across Email / SMS / Push / Chat — baseline-coverage discipline.

README 文档

README

Unified PHP facade over 35 notification providers across Email (10), SMS (10), Push (6), and Chat (9). Stateless. Zero vendor SDKs. Bring your own PSR-18 HTTP client.

Install

composer require thinwrap/notifications

Requires PHP ≥8.2. PSR-18 HTTP client + PSR-17 factories are auto-discovered via php-http/discovery — if you don't already have one installed:

composer require guzzlehttp/guzzle guzzlehttp/psr7

End-to-end example — 2-minute send

use Thinwrap\Notifications\Email;
use Thinwrap\Notifications\Enum\NotificationProviderId;
use Thinwrap\Notifications\Providers\Sendgrid\SendgridConfig;
use Thinwrap\Notifications\DTO\Email\EmailSendInput;
use Thinwrap\Notifications\Exception\ConnectorError;

$email = new Email(
    NotificationProviderId::Sendgrid,
    new SendgridConfig(apiKey: getenv('SG_KEY')),
);

try {
    $result = $email->send(new EmailSendInput(
        to:      'recipient@example.com',
        from:    'sender@example.com',
        subject: 'Hello from Thinwrap',
        text:    'A short plain-text body.',
    ));
    echo $result->success;            // bool
    echo $result->providerMessageId;  // vendor message id, if returned
} catch (ConnectorError $e) {
    error_log($e->providerCode->value . ': ' . ($e->providerMessage ?? ''));
}

Switching providers

Change the NotificationProviderId case and config class; the EmailSendInput / SmsSendInput / PushSendInput / ChatSendInput shape stays identical.

use Thinwrap\Notifications\Sms;
use Thinwrap\Notifications\Providers\Twilio\TwilioConfig;
use Thinwrap\Notifications\Providers\Vonage\VonageConfig;
use Thinwrap\Notifications\DTO\Sms\SmsSendInput;

$twilio = new Sms(NotificationProviderId::Twilio, new TwilioConfig(
    accountSid: getenv('TWILIO_SID'),
    authToken:  getenv('TWILIO_TOKEN'),
));
$vonage = new Sms(NotificationProviderId::Vonage, new VonageConfig(
    apiKey:    getenv('VONAGE_KEY'),
    apiSecret: getenv('VONAGE_SECRET'),
));

$sameInput = new SmsSendInput(to: '+14155550100', from: '+14155550199', body: 'Hello');
$twilio->send($sameInput);
$vonage->send($sameInput);

Bring your own PSR-18 client

Inject any PSR-18 client through the client parameter on the *Config DTO — useful for tracing, mocking, or proxying through symfony/http-client.

composer require guzzlehttp/guzzle
use GuzzleHttp\Client;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

$tracingClient = new class(new Client()) implements ClientInterface {
    public function __construct(private Client $inner) {}
    public function sendRequest(RequestInterface $req): ResponseInterface
    {
        error_log('' . $req->getMethod() . ' ' . (string) $req->getUri());
        return $this->inner->sendRequest($req);
    }
};

$email = new Email(
    NotificationProviderId::Sendgrid,
    new SendgridConfig(apiKey: getenv('SG_KEY'), from: 'noreply@example.com'),
    $tracingClient,                // ?ClientInterface — third constructor arg
);

php-http/discovery auto-detects an installed PSR-18 client (Guzzle, Symfony HttpClient, Buzz, etc.) when no $client is passed to the facade constructor.

The wrapper holds no state — no token cache, no connection pool, no retry buffer. The optional tokenCache hook on FcmConfig and ApnsConfig (the only two connectors with short-lived signed tokens) lets the consumer amortize signing cost; the hook owns the state, not the wrapper. See src/Providers/Fcm/README.md and src/Providers/Apns/README.md for hook shape.

Error handling

Every failure surfaces as ConnectorError with a typed ProviderCode. Compose your own retry strategy from $e->providerCode and $e->cause (which carries the raw Retry-After header where the vendor sets one). The wrapper performs no automatic retry.

use Thinwrap\Notifications\Exception\ConnectorError;
use Thinwrap\Notifications\Enum\ProviderCode;

try {
    $email->send($input);
} catch (ConnectorError $e) {
    match ($e->providerCode) {
        ProviderCode::RateLimited         => /* respect Retry-After in $e->cause['retryAfter'] */ null,
        ProviderCode::AuthFailed          => /* rotate credentials                              */ null,
        ProviderCode::InvalidRequest      => /* fix payload                                     */ null,
        ProviderCode::InvalidRecipient    => /* bad destination address                         */ null,
        ProviderCode::ProviderUnavailable => /* transient 5xx — your retry strategy             */ null,
        ProviderCode::Unknown             => /* fallback                                        */ null,
    };
}

The 6 ProviderCode cases are byte-exact across the TypeScript and PHP packages. ConnectorError extends \RuntimeExceptioncatch (\Throwable $e) works too.

cause shape

$e->cause is a uniform array across every connector:

[
    'raw'               => mixed,            // the parsed vendor error body (or transport-failure detail), null when absent
    'retryAfter'        => string|int|null,  // the RAW Retry-After value (header string, or body int for Telegram/Discord) — never normalized
    'retryAfterSeconds' => int|null,         // the PARSED Retry-After in seconds (null when no Retry-After present)
]

There is no top-level structured retryAfterSeconds field on ConnectorError — the wrapper performs no retry. The parsed seconds live inside $e->cause['retryAfterSeconds'] and are also echoed in $e->providerMessage (… (Retry-After: N seconds)). retryAfter carries the raw value verbatim (string|int|null) and is intentionally not normalized. Discord additionally exposes its body-sourced retryAfterBody (float) in cause.

Transport-layer failures (the PSR-18 client throwing before any HTTP response) are surfaced with a generic providerMessage (Upstream transport error) and only the exception class name in cause — never the raw client-exception message. That message can embed the full request URL, which for some providers (Telegram bot token in the path; Slack/Discord/Google Chat/Mattermost/MS Teams webhook URLs) is the credential. Do not log the raw HTTP-client exception directly for the same reason.

_passthrough escape valve

When the normalized input doesn't expose a vendor-specific field, forward arbitrary keys via the _passthrough parameter on the send input. Body merges deep, headers and query merge shallow, consumer values win on conflict. Keys are forwarded verbatim.

$email->send(new EmailSendInput(
    to:      'recipient@example.com',
    from:    'sender@example.com',
    subject: 'Hi',
    text:    'Hello',
    _passthrough: [
        'body' => [
            // SendGrid-specific — forwarded into v3/mail/send body verbatim
            'dynamic_template_data' => ['firstName' => 'Alice'],
            'mail_settings'         => ['sandbox_mode' => ['enable' => true]],
        ],
    ],
));

Each per-connector README documents vendor-specific _passthrough examples.

Bring your own connector

When _passthrough isn't enough — the provider isn't shipped at all — implement the channel's Contract\*ConnectorInterface (a single send() method over the normalized DTOs) and build the facade with fromConnector(). You keep the normalized input/result shapes and the uniform ConnectorError path; only the wire call is yours.

use Thinwrap\Notifications\Push;
use Thinwrap\Notifications\Contract\PushConnectorInterface;
use Thinwrap\Notifications\DTO\Push\PushSendInput;
use Thinwrap\Notifications\DTO\Push\PushSendResult;
use Thinwrap\Notifications\DTO\Push\PushStatus;

final class NtfyPushConnector implements PushConnectorInterface
{
    public function __construct(private \Psr\Http\Client\ClientInterface $client) {}

    public function send(PushSendInput $input): PushSendResult
    {
        // your wire call — e.g. POST https://ntfy.sh/{$input->to}
        $response = $this->client->sendRequest(/* ... */);

        return new PushSendResult(
            success:           true,
            status:            PushStatus::Sent,
            providerMessageId: null,
            raw:               (string) $response->getBody(),
        );
    }
}

$push = Push::fromConnector(new NtfyPushConnector($client));
$push->send(new PushSendInput(to: 'deploys', title: 'Deploy', body: 'v1.0 is live'));

Throw ConnectorError from send() for hard failures so consumers keep a single error-handling path; return success: false for HTTP-2xx-but-rejected soft-rejects, matching the built-in connectors.

Language constraints (PHP)

  • PHP 8.2 minimum; CI matrix runs on 8.2, 8.3, and 8.4 (Linux only at v1.0).
  • PSR-18 HTTP client is BYO — php-http/discovery auto-detects Guzzle, Symfony HttpClient, Buzz, etc., when no $client is passed to the facade constructor.
  • Zero runtime dependencies beyond psr/http-client + psr/http-factory + psr/http-message + php-http/discovery.
  • 35 providers across 4 channels — same normalized facade surface as the TypeScript sibling @thinwrap/notifications. Novu drop-in compatibility (IEmailProvider / ISmsProvider / IPushProvider / IChatProvider) is a TypeScript-only feature; the PHP package does not implement Novu's provider interfaces.

Per-connector documentation

Each per-connector README documents auth, endpoints (regional / sandbox), narrowed input augmentations, error-code mappings, and _passthrough examples.

Email (10)

Provider README
ses src/Providers/Ses/README.md
resend src/Providers/Resend/README.md
mailgun src/Providers/Mailgun/README.md
sendgrid src/Providers/Sendgrid/README.md
postmark src/Providers/Postmark/README.md
mailersend src/Providers/Mailersend/README.md
mailtrap src/Providers/Mailtrap/README.md
brevo src/Providers/Brevo/README.md
sparkpost src/Providers/Sparkpost/README.md
scaleway src/Providers/Scaleway/README.md

SMS (10)

Provider README
vonage src/Providers/Vonage/README.md
twilio src/Providers/Twilio/README.md
plivo src/Providers/Plivo/README.md
sns src/Providers/Sns/README.md
sinch src/Providers/Sinch/README.md
telnyx src/Providers/Telnyx/README.md
infobip src/Providers/Infobip/README.md
messagebird src/Providers/Messagebird/README.md
textmagic src/Providers/Textmagic/README.md
d7networks src/Providers/D7networks/README.md

Push (6)

Provider README
fcm src/Providers/Fcm/README.md
expo src/Providers/Expo/README.md
apns src/Providers/Apns/README.md
one-signal src/Providers/OneSignal/README.md
pusher-beams src/Providers/PusherBeams/README.md
wonderpush src/Providers/Wonderpush/README.md

Chat (9)

Provider README
telegram src/Providers/Telegram/README.md
slack src/Providers/Slack/README.md
whatsapp-business src/Providers/WhatsappBusiness/README.md
discord src/Providers/Discord/README.md
msteams src/Providers/Msteams/README.md
google-chat src/Providers/GoogleChat/README.md
mattermost src/Providers/Mattermost/README.md
rocket-chat src/Providers/RocketChat/README.md
line src/Providers/Line/README.md

Baseline-coverage discipline

The unified facade surface includes only features ≥90% of providers in each channel support natively. Sub-baseline fields are accessible via per-provider narrowed input DTOs (<Provider>NarrowedInput) and the _passthrough escape hatch.

Migrating

From a vendor PHP SDK

// Before — twilio/sdk
$twilio = new \Twilio\Rest\Client($sid, $token);
$twilio->messages->create('+14155550100', ['from' => '+14155550199', 'body' => 'Hi']);

// After
use Thinwrap\Notifications\Sms;
use Thinwrap\Notifications\Providers\Twilio\TwilioConfig;
use Thinwrap\Notifications\DTO\Sms\SmsSendInput;

$sms = new Sms(NotificationProviderId::Twilio, new TwilioConfig(accountSid: $sid, authToken: $token));
$sms->send(new SmsSendInput(to: '+14155550100', from: '+14155550199', body: 'Hi'));
// Before — sendgrid/sendgrid-php
$sg = new \SendGrid(getenv('SG_KEY'));
$mail = new \SendGrid\Mail\Mail();
$mail->setFrom('from@example.com');
$mail->addTo('to@example.com');
$mail->setSubject('Hi');
$mail->addContent('text/plain', 'Hello');
$sg->send($mail);

// After
$email = new Email(NotificationProviderId::Sendgrid, new SendgridConfig(apiKey: getenv('SG_KEY')));
$email->send(new EmailSendInput(to: 'to@example.com', from: 'from@example.com', subject: 'Hi', text: 'Hello'));

Vendor-SDK conveniences (auto-retry, telemetry, idempotency-key generation) are intentionally absent — compose your own.

From hand-rolled HTTP / Guzzle

If you've been hand-rolling vendor HTTP calls with Guzzle, the facade collapses the boilerplate to one line per call. Error handling and retry composition stay yours; the PSR-18 client passes through unchanged via the facade constructor's $client argument.

From a previous Thinwrap PHP version

Not applicable at v1.0; thinwrap/notifications has not previously published. Forward looking: when v2.0 ships with breaking changes, this section will carry the v1→v2 recipe.

For AI agents and contributors

AI agents working with this package should consult .ai/guidelines.md first.

Security

See SECURITY.md for private vulnerability disclosure. Releases are cosign-signed via GitHub Actions OIDC (no static signing keys); maintainer accounts require two-factor authentication (TOTP via an authenticator app) on GitHub. Packagist consumes the package via webhook auto-sync — no long-lived Packagist API token is stored anywhere.

License

MIT — see LICENSE.

Contributing

See CONTRIBUTING.md.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-04

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固