Modern Symfony bundle for posting to multiple social networks (Twitter, Facebook, LinkedIn, Telegram) with async support.
- ๐ Modern Stack: PHP 8.4+ & Symfony 7.4+
- ๐ Multiple Networks: Twitter (X), Facebook, LinkedIn, Telegram, Instagram, Discord, WhatsApp, Threads
- โก Async Support: Built-in Symfony Messenger integration
- ๐ฏ Type Safe: Full PHP 8.4 type coverage with readonly properties
- ๐ธ Media Support: Image attachments for all providers
- ๐ช Event System: Before/After publish events
- ๐ No External Dependencies: All API clients built-in (no SDK bloat)
- ๐ Fluent API: MessageBuilder for easy message creation
- ๐ Detailed Results: Rich result objects with post IDs and URLs
- ๐ก๏ธ Error Handling: Comprehensive exception handling and logging
composer require janwebdev/symfony-social-post-bundleCreate config/packages/social_post.yaml:
social_post:
providers:
twitter:
enabled: true
api_key: "%env(TWITTER_API_KEY)%"
api_secret: "%env(TWITTER_API_SECRET)%"
access_token: "%env(TWITTER_ACCESS_TOKEN)%"
access_token_secret: "%env(TWITTER_ACCESS_TOKEN_SECRET)%"
facebook:
enabled: true
page_id: "%env(FACEBOOK_PAGE_ID)%"
access_token: "%env(FACEBOOK_ACCESS_TOKEN)%"
graph_version: "v20.0"
linkedin:
enabled: true
organization_id: "%env(LINKEDIN_ORG_ID)%"
access_token: "%env(LINKEDIN_ACCESS_TOKEN)%"
telegram:
enabled: true
bot_token: "%env(TELEGRAM_BOT_TOKEN)%"
channel_id: "%env(TELEGRAM_CHANNEL_ID)%"
instagram:
enabled: true
account_id: "%env(INSTAGRAM_ACCOUNT_ID)%"
access_token: "%env(INSTAGRAM_ACCESS_TOKEN)%"
graph_version: "v20.0"
discord:
enabled: true
webhook_url: "%env(DISCORD_WEBHOOK_URL)%"
whatsapp:
enabled: true
phone_number_id: "%env(WHATSAPP_PHONE_NUMBER_ID)%"
access_token: "%env(WHATSAPP_ACCESS_TOKEN)%"
api_version: "v20.0"
threads:
enabled: true
user_id: "%env(THREADS_USER_ID)%"
access_token: "%env(THREADS_ACCESS_TOKEN)%"
api_version: "v1.0"Add to your .env:
# Twitter (X) API v2
TWITTER_API_KEY=your_api_key
TWITTER_API_SECRET=your_api_secret
TWITTER_ACCESS_TOKEN=your_access_token
TWITTER_ACCESS_TOKEN_SECRET=your_access_token_secret
# Facebook Graph API
FACEBOOK_PAGE_ID=your_page_id
FACEBOOK_ACCESS_TOKEN=your_page_access_token
# LinkedIn API v2
LINKEDIN_ORG_ID=your_organization_id
LINKEDIN_ACCESS_TOKEN=your_access_token
# Telegram Bot API
TELEGRAM_BOT_TOKEN=your_bot_token
TELEGRAM_CHANNEL_ID=@your_channel
# Instagram Graph API
INSTAGRAM_ACCOUNT_ID=your_instagram_business_account_id
INSTAGRAM_ACCESS_TOKEN=your_access_token
# Discord Webhooks
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_WEBHOOK_URL
# WhatsApp Channel API (BETA/UNSTABLE)
WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id
WHATSAPP_ACCESS_TOKEN=your_access_token
# Threads API
THREADS_USER_ID=your_threads_user_id
THREADS_ACCESS_TOKEN=your_access_token
โ ๏ธ Note: WhatsApp Channel integration is in BETA and may be unstable. The API is subject to change.
use Janwebdev\SocialPostBundle\Message\MessageBuilder;
use Janwebdev\SocialPostBundle\Publisher\PublisherInterface;
class YourService
{
public function __construct(
private PublisherInterface $publisher
) {}
public function postToSocialNetworks(): void
{
$message = MessageBuilder::create()
->setText('Hello, world! ๐')
->setLink('https://example.com')
->build();
$results = $this->publisher->publish($message);
foreach ($results as $network => $result) {
if ($result->isSuccess()) {
echo "Posted to {$network}: {$result->getPostUrl()}\n";
} else {
echo "Failed to post to {$network}: {$result->getErrorMessage()}\n";
}
}
}
}$message = MessageBuilder::create()
->setText('Check out this amazing photo!')
->setLink('https://example.com')
->addImage('/path/to/image.jpg', 'Photo description')
->build();
$results = $this->publisher->publish($message);$message = MessageBuilder::create()
->setText('This goes to Twitter and Facebook only')
->forNetworks(['twitter', 'facebook'])
->build();
$results = $this->publisher->publish($message);// Dispatch to message queue for async processing
$this->publisher->publishAsync($message);use Janwebdev\SocialPostBundle\Provider\Twitter\TwitterProvider;
class YourService
{
public function __construct(
private TwitterProvider $twitterProvider
) {}
public function postToTwitter(): void
{
$message = MessageBuilder::create()
->setText('Twitter-specific post')
->build();
$result = $this->twitterProvider->publish($message);
if ($result->isSuccess()) {
echo "Tweet ID: {$result->getPostId()}\n";
echo "Tweet URL: {$result->getPostUrl()}\n";
}
}
}Listen to publish events:
use Janwebdev\SocialPostBundle\Event\AfterPublishEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SocialPostSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
AfterPublishEvent::class => 'onAfterPublish',
];
}
public function onAfterPublish(AfterPublishEvent $event): void
{
$results = $event->getResults();
// Log successful posts
foreach ($results->getSuccessful() as $network => $result) {
// Store post IDs in database, send notifications, etc.
}
}
}$message = MessageBuilder::create()
->setText('Message with metadata')
->addMetadata('campaign_id', 'summer-2026')
->addMetadata('source', 'website')
->build();
// Access metadata later
$campaignId = $message->getMetadataValue('campaign_id');$results = $this->publisher->publish($message);
// Check if all successful
if ($results->isAllSuccessful()) {
echo "Posted to all networks!\n";
}
// Check if any successful
if ($results->hasAnySuccessful()) {
echo "Posted to at least one network\n";
}
// Get specific result
$twitterResult = $results->getResult('twitter');
if ($twitterResult && $twitterResult->isSuccess()) {
$postId = $twitterResult->getPostId();
$postUrl = $twitterResult->getPostUrl();
$metadata = $twitterResult->getMetadata();
}
// Iterate through results
foreach ($results as $network => $result) {
echo "{$network}: ";
echo $result->isSuccess() ? 'Success' : 'Failed';
echo "\n";
}- API Version: v2
- Authentication: OAuth 1.0a
- Features: Text posts, images, auto-truncation
- Character Limit: 280 characters
- Media: Up to 4 images per tweet
- API Version: Graph API v20.0+
- Authentication: Page Access Token
- Features: Text posts, link previews, images
- Note: Use non-expiring page access tokens
- API Version: v2
- Authentication: OAuth 2.0 Bearer token
- Features: Text posts, article sharing
- Note: Requires organization access
- API Version: Bot API
- Authentication: Bot Token
- Features: Text messages, photos, HTML formatting
- Note: Works with channels and groups
- API Version: Graph API v20.0+
- Authentication: Access Token (Business Account)
- Features: Photo posts with captions, container-based publishing
- Character Limit: 2200 characters for captions
- Note: Requires Instagram Business Account linked to Facebook Page
- API Version: Webhooks
- Authentication: Webhook URL
- Features: Text messages, rich embeds, image attachments
- Note: Very simple setup, no bot required
- API Version: Graph API v20.0+ (Channels API)
- Authentication: Phone Number ID + Access Token
- Features: Text messages, image messages
- Note: API is in beta, requires special access, may change without notice
- API Version: Threads API v1.0
- Authentication: User Access Token
- Features: Text posts, image posts, container-based publishing
- Character Limit: 500 characters
- Note: Requires Instagram account connected to Threads, similar workflow to Instagram API
# Run tests
composer run-tests
# Run with coverage
composer run-tests-with-clover
# Static analysis
composer run-static-analysis
# Code style check
composer check-code-style
# Fix code style
composer fix-code-style- Go to Twitter Developer Portal
- Create a new App
- Generate API Keys and Access Tokens
- Enable OAuth 1.0a
- Set permissions to "Read and Write"
- Go to Facebook Developers
- Create an App
- Add Facebook Login and Pages products
- Get a Page Access Token (make it permanent)
- Use Graph API Explorer to test
How to get permanent page token
- Go to LinkedIn Developers
- Create an App
- Add "Share on LinkedIn" product
- Request access to Marketing Developer Platform
- Generate access token with proper scopes
- Talk to @BotFather on Telegram
- Create a new bot with
/newbot - Get your bot token
- Add bot to your channel as admin
- Use channel username (e.g.,
@mychannel) or chat ID
- Go to Facebook Developers
- Create an App (same as for Facebook)
- Add Instagram product
- Connect your Instagram Business Account
- Get the Instagram Business Account ID
- Generate access token with
instagram_basic,instagram_content_publishpermissions - Important: Instagram account must be a Business or Creator account
- Open Discord and go to your server
- Go to Server Settings โ Integrations โ Webhooks
- Click "New Webhook"
- Choose channel, set name and avatar
- Copy webhook URL
- Done! (Simplest setup ever)
- Go to Facebook Developers
- Create a WhatsApp Business App
- Request access to Channels API (currently in beta)
- Get Phone Number ID from WhatsApp Business Account
- Generate access token
- Warning: This API is experimental and may change
- Go to Facebook Developers
- Create an App (can be same as Instagram/Facebook)
- Add Threads product to your app
- Connect your Instagram account that has Threads enabled
- Get your Threads User ID
- Generate access token with
threads_basic,threads_content_publishpermissions - Important: Your Instagram account must be connected to Threads
The new v3.0 uses a completely different architecture. Key changes:
use Janwebdev\SocialPost\Message;
use Janwebdev\SocialPost\Publisher;
$message = new Message('Hello world');
$publisher->publish($message);use Janwebdev\SocialPostBundle\Message\MessageBuilder;
use Janwebdev\SocialPostBundle\Publisher\PublisherInterface;
$message = MessageBuilder::create()
->setText('Hello world')
->build();
$results = $publisher->publish($message);- New namespace:
Janwebdev\SocialPostBundle\instead ofJanwebdev\SocialPost\ - No external SDK dependencies
- Different configuration structure
- Different Message API (use MessageBuilder)
- Detailed result objects instead of boolean
- Built-in async support
Contributions are welcome! Please feel free to submit a Pull Request.
This bundle is licensed under the MIT License. See the LICENSE file for details.
- Original concept by Martin Georgiev
- Refactored and modernized by Yan Rogozinsky
- ๐ Complete rewrite for PHP 8.4 and Symfony 7.4
- โจ No external SDK dependencies (all built-in)
- ๐ Twitter API v2 support
- ๐ Facebook Graph API v20+ support
- ๐ LinkedIn API v2 support
- ๐ฌ Telegram Bot API support
- ๐ธ Instagram Graph API support
- ๐ฎ Discord Webhooks support
- ๐ฑ WhatsApp Channel API support (BETA)
- โก Async publishing via Symfony Messenger
- ๐ฏ Type-safe with readonly properties
- ๐ฆ MessageBuilder with fluent interface
- ๐ Detailed result objects
- ๐ช Event system for extensibility
- ๐ก๏ธ Comprehensive error handling
- ๐งต Threads API support
- ๐ 8 social networks total