본문 바로가기
文档菜单

PHP / SDK REFERENCE

Symfony SDK 指南

用于发送 Kakao 通知消息、好友消息、品牌消息与 SMS 的官方 Symfony 扩展包 —— 自动装配服务、配置校验、Messenger 友好。

이 문서의 목차
패키지
sendgo/symfony
언어
PHP
레지스트리
Packagist
설치
composer require sendgo/symfony

用于发送 Kakao 通知消息、品牌消息与短信的官方 Symfony 扩展包

sendgo/symfony 把 sendgo/php 核心封装为 Symfony bundle:将 Sendgo\Php\Sendgo 注册进容器、在编译期校验配置,并支持自动装配。

要求 PHP 8.2+ 与 Symfony 6.4 或 7.x。


安装

composer require sendgo/symfony

注册 bundle

使用 Symfony Flex 时会自动注册。未使用 Flex 则手动添加:

<?php
// config/bundles.php

return [
    // ...
    Sendgo\Symfony\SendgoBundle::class => ['all' => true],
];

配置

# .env
SENDGO_ACCESS_KEY=your_access_key
SENDGO_SECRET_KEY=your_secret_key
SENDGO_KAKAO_SENDER_KEY=your_kakao_sender_key
SENDGO_SMS_SENDER_KEY=your_sms_sender_key
# config/packages/sendgo.yaml
sendgo:
    access_key:       '%env(SENDGO_ACCESS_KEY)%'
    secret_key:       '%env(SENDGO_SECRET_KEY)%'
    kakao_sender_key: '%env(SENDGO_KAKAO_SENDER_KEY)%'
    sms_sender_key:   '%env(SENDGO_SMS_SENDER_KEY)%'
    api_version:      'v2'

access_key 与 secret_key 声明为 isRequired()->cannotBeEmpty(),缺失时容器编译就会失败 —— 问题在缓存预热阶段暴露,而不是等到第一次发送。

api_version 在本 bundle 中默认为 v2(纯 PHP 核心默认为 v1)。


注入客户端

bundle 以类全名注册服务,因此构造函数自动装配即可,无需写 services.yaml:

<?php
// src/Controller/OrderController.php

namespace App\Controller;

use App\Entity\Order;
use Sendgo\Php\Sendgo;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

class OrderController extends AbstractController
{
    public function __construct(private Sendgo $sendgo) {}

    #[Route('/orders/{id}/confirm', methods: ['POST'])]
    public function confirm(Order $order): JsonResponse
    {
        $this->sendgo->alimtalk->send([
            'templateCode' => 'ORDER_CONFIRM_001',
            'contacts'     => [[
                'contact' => $order->getUser()->getPhone(),
                'var1'    => $order->getNumber(),
            ]],
        ]);

        return $this->json(['success' => true]);
    }
}

各发送渠道都是客户端上的属性:

属性 渠道
$sendgo->alimtalk Kakao 通知消息
$sendgo->friendtalk Kakao 好友消息
$sendgo->brandMessage Kakao 品牌消息(仅 v2)
$sendgo->sms SMS / LMS / MMS

同时注册了公开别名 sendgo,因此在无法自动装配的位置(遗留服务、手工装配的命令脚本)可以用 $container->get('sendgo')。


品牌消息

好友消息已于 2025-12-31 停止服务。 自 2026-01-01 起,Kakao 会将好友消息请求 自动改以品牌消息(自由形式)发出。friendtalk 仍可调用,且自由文本类型 FT/FI/FW 发往单个接收者的路径目前仍只有这一条 —— 基于模板的富文本类型 (FL/FC/FM/FP/FA)、非好友定向(N/I)与群发(F)请使用品牌消息。 品牌消息是好友消息的后继渠道:可触达非频道好友的接收者('targeting' => 'N'),也可向已同意接收的全部频道好友群发('targeting' => 'F')。消息类型与好友消息一一对应(FT→BT、FI→BI、FW→BW、FL→BL、FC→BC、FM→BM、FP→BP、FA→BA)—— 传入好友消息的代码,服务端会自动转换。

仅 v2 支持,也是本 bundle 的默认版本。

<?php
// src/Service/CampaignService.php

namespace App\Service;

use Sendgo\Php\Sendgo;

class CampaignService
{
    public function __construct(private Sendgo $sendgo) {}

    // 单条发送 —— targeting 为 M/N/I 时必须提供 contacts
    public function promote(string $phone): array
    {
        return $this->sendgo->brandMessage->send([
            'targeting'          => 'M',
            'messageType'        => 'FL',
            'friendTemplateUuid' => '9cd5460b-6458-4edc-9b11-c26d3013c340',
            'contacts'           => [['contact' => $phone, 'var1' => '29,000元']],
        ]);
    }

    // 群发 —— 已同意接收的全部频道好友(不传接收者列表)
    public function announce(): array
    {
        return $this->sendgo->brandMessage->broadcast([
            'messageType'        => 'FW',
            'friendTemplateUuid' => '9cd5460b-6458-4edc-9b11-c26d3013c340',
        ]);
    }

    // 群发在上游为异步处理,需轮询查看进度
    public function status(string $campaignId): array
    {
        return $this->sendgo->brandMessage->campaign($campaignId);
    }

    public function recent(): array
    {
        return $this->sendgo->brandMessage->campaigns(['count' => 10]);
    }
}

broadcast() 等同于把 targeting 固定为 F 的 send(),两者接受相同的参数。

targeting 的取值含义:

值 含义
M 频道好友中的指定接收者
N 非频道好友的接收者
I 按标识符指定的接收者
F 已同意接收的全部频道好友(群发)

用 Messenger 异步发送

发送是一次外发 HTTP 调用。交给 Messenger 处理,上游变慢时就不会一直占着 Web 请求:

<?php
// src/Message/SendAlimtalk.php

namespace App\Message;

class SendAlimtalk
{
    /** @param array<int, array<string, string>> $contacts */
    public function __construct(
        public readonly string $templateCode,
        public readonly array $contacts,
    ) {}
}
<?php
// src/MessageHandler/SendAlimtalkHandler.php

namespace App\MessageHandler;

use App\Message\SendAlimtalk;
use Psr\Log\LoggerInterface;
use Sendgo\Php\Exception\SendgoException;
use Sendgo\Php\Sendgo;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;

#[AsMessageHandler]
class SendAlimtalkHandler
{
    public function __construct(
        private Sendgo $sendgo,
        private LoggerInterface $logger,
    ) {}

    public function __invoke(SendAlimtalk $message): void
    {
        try {
            $this->sendgo->alimtalk->send([
                'templateCode' => $message->templateCode,
                'contacts'     => $message->contacts,
            ]);
        } catch (SendgoException $e) {
            if ($e->getStatusCode() < 500) {
                // 4xx 重试也不会成功 —— 直接停止,别耗尽重试次数。
                $this->logger->error('Sendgo {code}: {message}', [
                    'code'    => $e->getErrorCode(),
                    'message' => $e->getMessage(),
                ]);

                throw new UnrecoverableMessageHandlingException($e->getMessage(), previous: $e);
            }

            throw $e;   // 5xx 为临时故障,交给 Messenger 重试
        }
    }
}

对 4xx 抛出 UnrecoverableMessageHandlingException 是关键:否则一个错误的模板代码会被反复重试直到传输层放弃,看起来像临时故障。

// 派发示例
$bus->dispatch(new SendAlimtalk('ORDER_CONFIRM_001', [
    ['contact' => '01012345678', 'var1' => 'ORD-001'],
]));

在 Doctrine flush 之后发送

在事务内派发,可能出现事务回滚了但消息已经发出去的情况。请配置 Doctrine 事务中间件,或从 postFlush 监听器派发,而不要写在实体的生命周期回调里。


异常处理

<?php

use Sendgo\Php\Exception\SendgoException;

try {
    $this->sendgo->alimtalk->send([...]);
} catch (SendgoException $e) {
    $this->logger->error('Sendgo 发送失败', [
        'status'     => $e->getStatusCode(),
        'error_code' => $e->getErrorCode(),
        'endpoint'   => $e->getEndpoint(),
    ]);

    match ($e->getErrorCode()) {
        'INVALID_ACCESS_KEY',
        'INVALID_SECRET_KEY'    => $this->alertOps('请检查 Sendgo 密钥'),
        'IP_NOT_ALLOWED'        => $this->alertOps('该 IP 未在白名单中'),
        'PAYMENT_REQUIRED'      => $this->alertOps('Sendgo 余额不足'),
        'INVALID_TEMPLATE_CODE' => $this->logger->warning('模板不存在'),
        default                 => null,
    };
}

请根据 getErrorCode() 分支,而不要匹配错误消息文本 —— 消息文案可能变更,错误代码才是契约。 TOKEN_EXPIRED 与 TOKEN_MISMATCH 已在核心 SDK 内部处理:会重新签发令牌并自动重试该请求一次。


测试

服务是 public 的,因此可以在测试容器中替换:

<?php
// tests/Controller/OrderControllerTest.php

namespace App\Tests\Controller;

use Sendgo\Php\AlimtalkService;
use Sendgo\Php\Sendgo;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class OrderControllerTest extends WebTestCase
{
    public function testConfirmSendsAlimtalk(): void
    {
        $client = static::createClient();

        $alimtalk = $this->createMock(AlimtalkService::class);
        $alimtalk->expects($this->once())->method('send');

        // 各渠道是 `readonly` 属性。反射只能在其仍未初始化时赋值 ——
        // 而 PHPUnit 的替身正处于这种状态,因为它从不执行真正的构造函数。
        // 对已构造完成的真实客户端调用 setValue() 会抛出
        // "Cannot modify readonly property"。
        $sendgo = $this->createMock(Sendgo::class);
        (new \ReflectionProperty(Sendgo::class, 'alimtalk'))->setValue($sendgo, $alimtalk);

        static::getContainer()->set(Sendgo::class, $sendgo);

        $client->request('POST', '/orders/1/confirm');

        $this->assertResponseIsSuccessful();
    }
}

大多数测试套件更简单的做法是在客户端前面放一层自己的接口 —— mock NotificationSenderInterface,让 Sendgo 完全不出现在测试里。只有当你确实要断言 SDK 调用本身时才需要反射。


配置项

键 是否必填 默认值 说明
access_key 必填 — Sendgo 访问密钥
secret_key 必填 — Sendgo 私密密钥
kakao_sender_key 选填 null Kakao 发送者资料密钥
sms_sender_key 选填 null 短信主叫号码密钥
api_version 选填 v2 API 版本(v1 | v2)
url 选填 https://sendgo.io API 基础地址

常见问题

与 sendgo/php 有什么区别? sendgo/php 是不依赖框架的核心包。sendgo/symfony 在其之上增加 bundle 注册、配置校验树、容器注册与自动装配。两者发出的请求完全相同。

同时支持 Symfony 6.4 和 7 吗? 是。symfony/config、symfony/dependency-injection、symfony/http-kernel 的约束都是 ^6.4|^7.0。

可以有多个客户端(多租户)吗? bundle 只注册一个服务。若需按租户使用不同密钥,请自行注册一个工厂服务来构造 Sendgo\Php\Sendgo,并注入该工厂。


短链接

短链接会缩短消息正文中的链接,并统计该链接是否被真正点击。 短信按字节计费,因此链接更短就意味着正文可以写更多内容。

仅 v2 支持。

再次缩短同一个原始 URL 会直接返回已有的链接。若希望按活动分别统计反应, 请使用 forceNew 生成新的短码。

deactivate 不会删除链接,只停止重定向。当已发送消息中的链接必须失效时使用它; 累计统计会保留,访问已停止的链接会返回 410 Gone。

// 通过自动装配的 Sendgo\Php\Sendgo 以属性方式访问。
$short = $this->sendgo->shortUrl->create([
    'targetUrl' => 'https://example.com/promotions/summer-sale',
    'title'     => '夏季促销落地页',
]);

$code = $short['data']['code'];
$stats = $this->sendgo->shortUrl->stats($code);

stats 返回按日趋势(daily)以及按设备(byDevice)、来源(byReferer)、国家(byCountry)的分解。按日趋势读取的是预聚合表,因此点击量再大响应时间也保持稳定。


包信息

  • 包名:sendgo/symfony(Packagist)
  • 仓库:send-go/symfony
  • 注册表:https://packagist.org/packages/sendgo/symfony
  • 许可:MIT

如何获取 API 密钥

登录 Sendgo 后,在 API/SDK → API 对接 菜单中签发访问密钥与私密密钥。 使用通知消息/好友消息需在 Kakao 渠道 登记发送者资料以获取 kakao_sender_key;使用短信需在 发送号码 菜单登记主叫号码。

이 패키지로 할 수 있는 것

관련 패키지

专注构建,消息交给 Sendgo。返回顶部 ↑