文档菜单
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;使用短信需在 发送号码 菜单登记主叫号码。
이 패키지로 할 수 있는 것
Send your first Kakao Alimtalk in 5 minutes →
From issuing an access key to sending a Kakao Alimtalk, with working code in Node.js, Python, PHP, Laravel, Java and Go.
Finish the Sendgo integration from your AI agent — MCP server and Account API →
Pick the organisation, issue API keys, register sender numbers and templates from a coding agent. Everything except topping up credit works without the console.
Alimtalk, Brand Message or SMS — choosing a messaging channel in Korea →
Kakao Alimtalk, Kakao Brand Message and SMS/LMS/MMS differ in what content they allow, what they cost and what you must set up first. Which to pick, by situation.
Sendgo API authentication — access keys and bearer tokens →
Exchange an accessKey and secretKey for a bearer token, and call the Sendgo API with it. Differences between v1 and v2, token caching, and the 401/403 codes.
Send a Kakao Alimtalk — PHP, Node.js, Python, Java, Go examples →
Send Kakao Alimtalk with an approved template code. Runnable code for Laravel, Node.js, Python, PHP, Java, Go, Ruby and .NET, plus every required field and why sends fail.
Send SMS, LMS and MMS in South Korea — code examples →
Send Korean SMS (90 bytes), LMS (long text) and MMS (with images) through Sendgo. Type selection, byte counting, verification-code patterns and advertising rules.
Send a Kakao Brand Message — the successor to Friendtalk →
Send Brand Messages to channel friends, non-friends, or every consenting friend at once. How targeting splits the request path, and when to keep using the Friendtalk endpoint.
SMS fallback when a Kakao Alimtalk fails →
Use replaceSms so a text message goes out when the Alimtalk cannot be delivered. Required fields, cost implications, and the mistake that silently sends nothing.
Bulk Alimtalk sending and per-recipient variables →
Send one template to many recipients with different values each, in batches. Batch sizing, partial failures, queue patterns and the data hygiene that prevents most incidents.
Scheduling an Alimtalk or SMS — scheduleType and at →
Send at a specific time with scheduleType SCHEDULED. Timestamp format, the KST timezone trap, and how scheduling interacts with the advertising night ban.
Doing everything over the API — operations automation for resellers →
Register Kakao channels, submit Alimtalk templates for review, file sender numbers, and sync opt-outs without ever sending anyone to sendgo.io. Includes webhook delivery of review outcomes.
Sendgo API error codes and retry strategy →
Every error code returned by the Sendgo send endpoints, and how to tell a failure worth retrying from one that will fail identically every time.
Short links in SMS and Alimtalk, with click tracking →
Shorten long URLs to fit inside an SMS and measure who clicked. Creating short links, reading click stats, and splitting stats per campaign.
Advertising message rules in Korea — (광고) prefix, opt-out, night ban →
What Korean law requires of promotional SMS and Kakao messages, and how to enforce it in code: the (광고) prefix, a free opt-out, and no sending between 21:00 and 08:00 KST.
Kakao Friendtalk shut down (2025-12-31) — migrating to Brand Message →
What happens to existing Friendtalk code now that the channel has ended, when you must migrate to Brand Message, and the one case where the Friendtalk endpoint is still the right call.
관련 패키지
PHP →
用于发送 Kakao 通知消息、好友消息、品牌消息与 SMS 的官方 PHP SDK —— 不依赖任何框架的纯 PHP 核心包。
composer require sendgo/phpLaravel →
用于发送 Kakao 通知消息、好友消息、品牌消息与 SMS 的官方 Laravel 扩展包 —— 门面、配置发布、队列友好。
composer require sendgo/laravelWordPress →
用于发送 Kakao 通知消息、好友消息、品牌消息与 SMS 的官方 WordPress 插件 —— 支持 WooCommerce 订单自动通知。
composer require sendgo/wordpress