PHPackages                             owlstack/owlstack-core - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. owlstack/owlstack-core

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

owlstack/owlstack-core
======================

Framework-agnostic PHP core for Owlstack — content publishing and synchronization engine for social media platforms.

v1.0.1(3mo ago)22961MITPHPPHP ^8.1CI passing

Since Feb 12Pushed 3mo agoCompare

[ Source](https://github.com/owlstacks/owlstack-core)[ Packagist](https://packagist.org/packages/owlstack/owlstack-core)[ RSS](/packages/owlstack-owlstack-core/feed)WikiDiscussions main Synced 2d ago

READMEChangelog (2)Dependencies (4)Versions (7)Used By (1)

 [   ![Owlstack](https://raw.githubusercontent.com/owlstacks/owlstack-docs/refs/heads/main/static/img/logo-dark-transparent.png)  ](https://owlstack.dev)

 **Framework-agnostic PHP core for social media publishing**

 [![Tests](https://camo.githubusercontent.com/50eeccb3531fd33d279e443a057b34a353af2180a909c5eebf14c7abfbbdc3e7/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6f776c737461636b732f6f776c737461636b2d636f72652f74657374732e796d6c3f6272616e63683d6d61696e267374796c653d666c61742d737175617265266c6162656c3d7465737473)](https://github.com/owlstacks/owlstack-core/actions/workflows/tests.yml) [![Latest Version](https://camo.githubusercontent.com/35ed7a0ff1761db6def7b19348904559a87927d6c01fca99ece62dc83d03fc6b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6f776c737461636b2f6f776c737461636b2d636f72652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/owlstack/owlstack-core) [![Total Downloads](https://camo.githubusercontent.com/f65d88593001375ba6f56161f6d55d64c4ddff802d5ace15717225b847ef43e0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6f776c737461636b2f6f776c737461636b2d636f72652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/owlstack/owlstack-core) [![PHP Version](https://camo.githubusercontent.com/896918e0ea11e20851b24b8f9278b10601cf845e22846fde2fd5061b01c40bc8/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6f776c737461636b2f6f776c737461636b2d636f72652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/owlstack/owlstack-core) [![License](https://camo.githubusercontent.com/08124320aef8cd14ccdbf6170ed08ecea86f3127866b9fe68d744c4d1dc35cf1/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6f776c737461636b2f6f776c737461636b2d636f72652e7376673f7374796c653d666c61742d737175617265)](LICENSE) [![GitHub Stars](https://camo.githubusercontent.com/87f549f3eab8b787086aa8a4a6c01c0f29e286c8880faf836598a6581d355483/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6f776c737461636b732f6f776c737461636b2d636f72653f7374796c653d666c61742d737175617265)](https://github.com/owlstacks/owlstack-core)

---

Owlstack Core
=============

[](#owlstack-core)

The shared engine behind [Owlstack](https://owlstack.dev) — publish content to **11 social media platforms** through a single, unified PHP API. Zero framework dependencies. Works with Laravel, WordPress, or standalone.

---

Table of Contents
-----------------

[](#table-of-contents)

- [Why Owlstack Core?](#why-owlstack-core)
- [Supported Platforms](#supported-platforms)
- [Platform Availability &amp; Requirements](#platform-availability--requirements)
- [Architecture Overview](#architecture-overview)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
    - [Content Model](#content-model)
    - [Media Handling](#media-handling)
    - [Platform Configuration](#platform-configuration)
    - [Publishing](#publishing)
    - [Formatting Pipeline](#formatting-pipeline)
    - [Authentication (OAuth)](#authentication-oauth)
    - [Event System](#event-system)
    - [Delivery Status](#delivery-status)
    - [Error Handling](#error-handling)
    - [HTTP Client](#http-client)
    - [Support Utilities](#support-utilities)
- [Platform Reference](#platform-reference)
- [Multi-Platform Publishing](#multi-platform-publishing)
- [Advanced Usage](#advanced-usage)
- [Testing](#testing)
- [Framework Integrations](#framework-integrations)
- [Contributing](#contributing)
- [Security](#security)
- [License](#license)

---

Why Owlstack Core?
------------------

[](#why-owlstack-core)

- **11 platforms, one API** — Telegram, Twitter/X, Facebook, LinkedIn, Discord, Instagram, Pinterest, Reddit, Slack, Tumblr, and WhatsApp
- **Zero dependencies** — Pure PHP 8.1+, only ext-curl and ext-json required
- **Contract-driven** — Every concern (HTTP, storage, events, auth) is backed by an interface
- **Immutable value objects** — `Post`, `Media`, `MediaCollection`, `AccessToken` are all readonly
- **Exception-safe publishing** — `Publisher::publish()` never throws; always returns a `PublishResult`
- **Platform-aware formatting** — Each platform has its own formatter respecting character limits, markup syntax, and media constraints
- **Built for integration** — Designed as the engine for Laravel, WordPress, and Node.js packages

---

Supported Platforms
-------------------

[](#supported-platforms)

PlatformMax TextMax MediaMedia TypesNotable Features**Telegram**4,09610JPEG, PNG, GIF, MP4, OGGMedia groups, inline keyboards, location/contact/venue messages**Twitter/X**2804JPEG, PNG, GIF, MP4OAuth 1.0a, polls, quote tweets, exponential backoff retry**Facebook**63,2061JPEG, PNG, GIF, BMP, MP4, AVIGraph API, scheduled publishing, privacy targeting**LinkedIn**3,0001JPEG, PNG, GIFPersonal profiles &amp; company pages, multi-step image upload**Discord**2,00010JPEG, PNG, GIF, WebP, MP4Bot &amp; webhook modes, rich embeds**Instagram**2,20010JPEG, MP4Carousels, Reels, Stories, two-step container publishing**Pinterest**800—JPEG, PNG, GIF, WebP, MP4Board &amp; section targeting, video pins**Reddit**40,0001JPEG, PNG, GIFSelf &amp; link posts, flair support, NSFW/spoiler flags**Slack**40,000——Bot &amp; webhook modes, Block Kit support**Tumblr**4,096——NPF content blocks, draft/queue/private states**WhatsApp**4,096—JPEG, PNG, MP4, PDF, DOCXTemplate messages, document sending---

Platform Availability &amp; Requirements
----------------------------------------

[](#platform-availability--requirements)

Not all platforms can be used immediately — some require OAuth app review or API access approval from the platform provider. Below is a breakdown of what's ready to use, what needs approval, and what's planned.

### Ready to Use (No Approval Needed)

[](#ready-to-use-no-approval-needed)

These platforms use open APIs, bot tokens, or webhooks — no app review required:

PlatformAuth MethodNotes**Telegram**Bot APICreate a bot via [@BotFather](https://t.me/BotFather) instantly**Discord**Webhook / Bot TokenUsers provide their own webhook URL or bot token**Slack**Webhook / Bot TokenUsers provide their own webhook URL or bot token**Twitter/X**OAuth 1.0aSelf-service registration at [developer.x.com](https://developer.x.com)**Reddit**OAuthSelf-service at [reddit.com/prefs/apps](https://www.reddit.com/prefs/apps)**Tumblr**OAuthSelf-service at [tumblr.com/oauth/apps](https://www.tumblr.com/oauth/apps)### Require App Review / API Approval

[](#require-app-review--api-approval)

These platforms require submitting your application for review before production use. Apply as soon as your product is live:

PlatformAuth MethodApproval RequiredWhere to Apply**Facebook**OAuth`pages_manage_posts`, `pages_read_engagement` permissions[Meta App Review](https://developers.facebook.com/docs/app-review)**Instagram**OAuth`instagram_content_publish` permission[Meta App Review](https://developers.facebook.com/docs/app-review) (same portal)**LinkedIn**OAuthCommunity Management API access[LinkedIn Developer Portal](https://learn.microsoft.com/en-us/linkedin/marketing/)**Pinterest**OAuthProduction API access[Pinterest Developer Portal](https://developers.pinterest.com/)**WhatsApp**Business APIMeta Business verification + WhatsApp Business Platform[Meta for Developers](https://developers.facebook.com/docs/whatsapp)### Planned (Not Yet Implemented)

[](#planned-not-yet-implemented)

These platforms are shown in the dashboard but do not have `owlstack-core` implementations yet:

PlatformAuth MethodApproval NeededComplexity**Bluesky**App PasswordNo (open AT Protocol)Low**Mastodon**OAuthNo (federated, open protocol)Low**Threads**OAuthYes (Meta App Review)Medium**TikTok**OAuthYes ([TikTok Developer Portal](https://developers.tiktok.com/))Medium**YouTube**OAuthYes (Google OAuth consent screen verification)Medium> **Tip:** Facebook, Instagram, Threads, and WhatsApp all go through the same [Meta App Review](https://developers.facebook.com/docs/app-review) portal — apply for all of them in a single review cycle.

---

Architecture Overview
---------------------

[](#architecture-overview)

Owlstack Core is built on a **contract-driven, layered architecture** with zero framework dependencies. Framework packages (Laravel, WordPress) provide concrete implementations for storage, queues, and events.

 ```
graph TB
    subgraph "Your Application"
        APP[Application Code]
    end

    subgraph "Owlstack Core"
        direction TB
        PUB[Publisher]
        REG[PlatformRegistry]

        subgraph "Content Layer"
            POST[Post]
            MEDIA[Media / MediaCollection]
            CLINK[CanonicalLink]
        end

        subgraph "Platform Layer"
            PI[PlatformInterface]
            TG[Telegram]
            TW[Twitter/X]
            FB[Facebook]
            LI[LinkedIn]
            DC[Discord]
            IG[Instagram]
            PT[Pinterest]
            RD[Reddit]
            SL[Slack]
            TB[Tumblr]
            WA[WhatsApp]
        end

        subgraph "Formatting"
            FI[FormatterInterface]
            CT[CharacterTruncator]
            HE[HashtagExtractor]
        end

        subgraph "Infrastructure"
            HTTP[HttpClient]
            AUTH[OAuthHandler]
            EVT[EventDispatcher]
            CFG[Config / Credentials]
        end
    end

    subgraph "External APIs"
        API1[Telegram Bot API]
        API2[Twitter API v2]
        API3[Facebook Graph API]
        API4[LinkedIn API]
        API5[Discord API]
        API6[Instagram Graph API]
        API7[Pinterest API v5]
        API8[Reddit API]
        API9[Slack Web API]
        API10[Tumblr API v2]
        API11[WhatsApp Cloud API]
    end

    APP --> PUB
    PUB --> REG
    REG --> PI
    PI --> TG & TW & FB & LI & DC & IG & PT & RD & SL & TB & WA
    PUB --> EVT
    TG & TW & FB & LI & DC & IG & PT & RD & SL & TB & WA --> HTTP
    TG --> API1
    TW --> API2
    FB --> API3
    LI --> API4
    DC --> API5
    IG --> API6
    PT --> API7
    RD --> API8
    SL --> API9
    TB --> API10
    WA --> API11
    POST --> PUB
    MEDIA --> POST
```

      Loading **Publishing Flow** ```
sequenceDiagram
    participant App as Application
    participant Pub as Publisher
    participant Reg as PlatformRegistry
    participant Fmt as Formatter
    participant Plat as Platform
    participant HTTP as HttpClient
    participant API as External API
    participant Evt as EventDispatcher

    App->>Pub: publish(Post, "telegram")
    Pub->>Reg: get("telegram")
    Reg-->>Pub: TelegramPlatform
    Pub->>Plat: publish(Post, options)
    Plat->>Fmt: format(Post)
    Fmt-->>Plat: Formatted text
    Plat->>HTTP: post(apiUrl, payload)
    HTTP->>API: HTTP Request
    API-->>HTTP: Response
    HTTP-->>Plat: Response array
    Plat-->>Pub: PlatformResponse

    alt Success
        Pub->>Evt: dispatch(PostPublished)
        Pub-->>App: PublishResult ✓
    else Failure
        Pub->>Evt: dispatch(PostFailed)
        Pub-->>App: PublishResult ✗
    end
```

      Loading ---

Installation
------------

[](#installation)

```
composer require owlstack/owlstack-core
```

### Requirements

[](#requirements)

RequirementVersionPHP≥ 8.1ext-curl\*ext-json\*---

Quick Start
-----------

[](#quick-start)

```
use Owlstack\Core\Content\Post;
use Owlstack\Core\Content\Media;
use Owlstack\Core\Content\MediaCollection;
use Owlstack\Core\Config\PlatformCredentials;
use Owlstack\Core\Http\HttpClient;
use Owlstack\Core\Platforms\PlatformRegistry;
use Owlstack\Core\Platforms\Telegram\TelegramPlatform;
use Owlstack\Core\Platforms\Telegram\TelegramFormatter;
use Owlstack\Core\Publishing\Publisher;

// 1. Configure credentials
$credentials = new PlatformCredentials('telegram', [
    'api_token' => 'your-bot-token',
    'channel_username' => '@your-channel',
]);

// 2. Create platform
$httpClient  = new HttpClient();
$formatter   = new TelegramFormatter();
$platform    = new TelegramPlatform($credentials, $httpClient, $formatter);

// 3. Register & publish
$registry = new PlatformRegistry();
$registry->register($platform);

$publisher = new Publisher($registry);
$post = new Post(
    title: 'Hello World',
    body: 'My first post via Owlstack!',
    url: 'https://example.com/hello-world',
    tags: ['opensource', 'php'],
);

$result = $publisher->publish($post, 'telegram');

if ($result->success) {
    echo "Published! ID: {$result->externalId}";
    echo "URL: {$result->externalUrl}";
} else {
    echo "Failed: {$result->error}";
}
```

---

Core Concepts
-------------

[](#core-concepts)

### Content Model

[](#content-model)

The content layer uses **immutable value objects** that are platform-agnostic.

#### Post

[](#post)

The central content object. All properties are readonly.

```
use Owlstack\Core\Content\Post;

$post = new Post(
    title: 'My Article Title',
    body: 'The full article content goes here...',
    url: 'https://example.com/my-article',        // optional
    excerpt: 'A short summary for Twitter',        // optional
    media: $mediaCollection,                       // optional
    tags: ['php', 'social-media', 'automation'],   // optional
    metadata: ['wp_post_id' => 42],                // optional
);

// Helpers
$post->hasMedia();                    // bool
$post->hasUrl();                      // bool
$post->getMeta('wp_post_id');         // 42
$post->getMeta('missing', 'default'); // 'default'
```

ParameterTypeDefaultDescription`title``string`*required*Post title`body``string`*required*Post body content`url``?string``null`Canonical URL to original content`excerpt``?string``null`Short summary (used by Twitter)`media``?MediaCollection``null`Attached media files`tags``array``[]`Tags for hashtag generation`metadata``array``[]`Arbitrary key-value store---

### Media Handling

[](#media-handling)

#### Media

[](#media)

A single media attachment (image, video, audio, or document).

```
use Owlstack\Core\Content\Media;

$image = new Media(
    path: '/path/to/photo.jpg',
    mimeType: 'image/jpeg',
    altText: 'A sunset over the ocean',
    width: 1920,
    height: 1080,
    fileSize: 245_000,
);

$image->isImage();    // true
$image->isVideo();    // false
$image->isAudio();    // false
$image->isDocument(); // false
```

#### MediaCollection

[](#mediacollection)

An immutable, typed collection. Adding returns a **new** instance.

```
use Owlstack\Core\Content\MediaCollection;

$collection = new MediaCollection();
$collection = $collection->add($image1);
$collection = $collection->add($image2);
$collection = $collection->add($video);

$collection->count();    // 3
$collection->first();    // $image1
$collection->images();   // MediaCollection with $image1, $image2
$collection->videos();   // MediaCollection with $video
$collection->isEmpty();  // false
$collection->all();      // Media[]

// Iterable
foreach ($collection as $media) {
    echo $media->path;
}
```

#### CanonicalLink

[](#canonicallink)

Appends a "Read more" link to content, respecting character limits.

```
use Owlstack\Core\Content\CanonicalLink;

$link = new CanonicalLink("\n\nRead more: {url}");
$text = $link->inject($content, 'https://example.com', maxLength: 280);
```

---

### Platform Configuration

[](#platform-configuration)

#### PlatformCredentials

[](#platformcredentials)

A readonly credential bag for a single platform.

```
use Owlstack\Core\Config\PlatformCredentials;

$creds = new PlatformCredentials('twitter', [
    'consumer_key' => '...',
    'consumer_secret' => '...',
    'access_token' => '...',
    'access_token_secret' => '...',
]);

$creds->get('consumer_key');         // value
$creds->has('consumer_key');         // true
$creds->require('consumer_key');     // value or throws InvalidArgumentException
$creds->all();                       // full credentials array
```

#### OwlstackConfig

[](#owlstackconfig)

Central configuration for multiple platforms.

```
use Owlstack\Core\Config\OwlstackConfig;

$config = new OwlstackConfig(
    platforms: [
        'telegram' => ['api_token' => '...'],
        'twitter'  => ['consumer_key' => '...', /* ... */],
    ],
    options: [
        'default_hashtag_count' => 5,
    ],
);

$config->hasPlatform('telegram');    // true
$config->credentials('telegram');    // PlatformCredentials
$config->configuredPlatforms();      // ['telegram', 'twitter']
$config->option('default_hashtag_count'); // 5
```

#### ConfigValidator

[](#configvalidator)

Validates that required credential keys are present for each platform.

```
use Owlstack\Core\Config\ConfigValidator;

$validator = new ConfigValidator();
$missing = $validator->validate($credentials); // ['access_token_secret']

// Or validate all platforms at once (throws on failure)
$validator->validateConfig($config);
```

**Required credentials per platform**PlatformRequired KeysTelegram`api_token`Twitter/X`consumer_key`, `consumer_secret`, `access_token`, `access_token_secret`Facebook`app_id`, `app_secret`, `page_access_token`, `page_id`LinkedIn`access_token`, `person_id` or `organization_id`Discord`bot_token` + `channel_id`, **or** `webhook_url`Instagram`access_token`, `instagram_account_id`Pinterest`access_token`, `board_id`Reddit`client_id`, `client_secret`, `access_token`, `username`Slack`bot_token` + `channel`, **or** `webhook_url`Tumblr`access_token`, `blog_identifier`WhatsApp`access_token`, `phone_number_id`---

### Publishing

[](#publishing)

#### Publisher

[](#publisher)

The main orchestrator. Resolves the platform, publishes, dispatches events, and returns a result — **never throws exceptions**.

```
use Owlstack\Core\Publishing\Publisher;

$publisher = new Publisher($registry, $eventDispatcher); // dispatcher is optional

$result = $publisher->publish($post, 'telegram', ['chat_id' => '@channel']);
```

#### PublishResult

[](#publishresult)

An immutable result object returned from every publish call.

```
$result->success;       // bool
$result->failed();      // bool (inverse of success)
$result->platformName;  // 'telegram'
$result->externalId;    // '12345' or null
$result->externalUrl;   // 'https://t.me/channel/12345' or null
$result->error;         // 'Rate limit exceeded' or null
$result->timestamp;     // DateTimeImmutable
```

---

### Formatting Pipeline

[](#formatting-pipeline)

Each platform has a dedicated formatter implementing `FormatterInterface`. Formatters handle:

- **Character limits** — Truncating content to platform maximums
- **Markup syntax** — HTML for Telegram, Markdown for Discord/Reddit, mrkdwn for Slack
- **Hashtag injection** — Appending tags within the character budget
- **URL handling** — Platform-specific link formatting (t.co wrapping for Twitter, `` for Slack)

```
use Owlstack\Core\Formatting\Contracts\FormatterInterface;

// Every formatter implements:
$formatter->format($post, $options);  // Formatted string
$formatter->platform();               // 'telegram'
$formatter->maxLength();              // 4096
```

#### CharacterTruncator

[](#charactertruncator)

Word-boundary-aware text truncation.

```
use Owlstack\Core\Formatting\CharacterTruncator;

$truncator = new CharacterTruncator(ellipsis: '…');
$truncator->truncate('Hello World', maxLength: 8); // 'Hello…'
```

#### HashtagExtractor

[](#hashtagextractor)

Converts tags to hashtag strings, sanitizing special characters.

```
use Owlstack\Core\Formatting\HashtagExtractor;

$extractor = new HashtagExtractor();
$extractor->extract(['PHP', 'social media'], maxCount: 5);
// '#PHP #socialmedia'
```

 ```
flowchart LR
    POST[Post] --> FMT[Platform Formatter]
    FMT --> TRUNC[CharacterTruncator]
    FMT --> HASH[HashtagExtractor]
    FMT --> CLINK[CanonicalLink]
    TRUNC --> OUT[Formatted Text]
    HASH --> OUT
    CLINK --> OUT
```

      Loading ---

### Authentication (OAuth)

[](#authentication-oauth)

The auth layer provides contracts for OAuth flows. Framework packages supply concrete implementations for token storage.

```
use Owlstack\Core\Auth\OAuthHandler;
use Owlstack\Core\Auth\AccessToken;

// Set up handler (provider & store are interface implementations)
$handler = new OAuthHandler($provider, $tokenStore, 'twitter');

// Step 1: Generate authorization URL
$authUrl = $handler->authorize('https://app.com/callback', ['tweet.read', 'tweet.write']);

// Step 2: Handle callback after user authorizes
$token = $handler->handleCallback($code, 'https://app.com/callback', 'user-123');

// Step 3: Get valid token (auto-refreshes if expired)
$token = $handler->getToken('user-123');
```

#### AccessToken

[](#accesstoken)

```
$token = new AccessToken(
    token: 'abc123',
    refreshToken: 'refresh_xyz',
    expiresAt: new DateTimeImmutable('+1 hour'),
    scopes: ['tweet.read', 'tweet.write'],
    metadata: ['user_id' => '12345'],
);

$token->isExpired();     // false
$token->isRefreshable(); // true
```

 ```
sequenceDiagram
    participant App as Your App
    participant OH as OAuthHandler
    participant OP as OAuthProvider
    participant TS as TokenStore
    participant API as Platform API

    App->>OH: authorize(redirectUri, scopes)
    OH->>OP: getAuthorizationUrl()
    OP-->>OH: Auth URL
    OH-->>App: Auth URL → redirect user

    Note over App: User authorizes on platform

    App->>OH: handleCallback(code, redirectUri, accountId)
    OH->>OP: exchangeCode(code, redirectUri)
    OP->>API: POST /oauth/token
    API-->>OP: AccessToken
    OP-->>OH: AccessToken
    OH->>TS: store(platform, accountId, token)
    OH-->>App: AccessToken

    App->>OH: getToken(accountId)
    OH->>TS: get(platform, accountId)
    TS-->>OH: AccessToken
    alt Token Expired
        OH->>OP: refreshToken(token)
        OP->>API: POST /oauth/refresh
        API-->>OP: New AccessToken
        OP-->>OH: New AccessToken
        OH->>TS: store(platform, accountId, newToken)
    end
    OH-->>App: Valid AccessToken
```

      Loading ---

### Event System

[](#event-system)

Hook into the publish lifecycle with the event dispatcher.

```
use Owlstack\Core\Events\Contracts\EventDispatcherInterface;
use Owlstack\Core\Events\PostPublished;
use Owlstack\Core\Events\PostFailed;

class MyDispatcher implements EventDispatcherInterface
{
    public function dispatch(object $event): void
    {
        match (true) {
            $event instanceof PostPublished => $this->onPublished($event),
            $event instanceof PostFailed    => $this->onFailed($event),
        };
    }

    private function onPublished(PostPublished $event): void
    {
        // $event->post — the Post object
        // $event->result — the PublishResult
        logger("Published to {$event->result->platformName}");
    }

    private function onFailed(PostFailed $event): void
    {
        logger("Failed: {$event->result->error}");
    }
}

$publisher = new Publisher($registry, new MyDispatcher());
```

---

### Delivery Status

[](#delivery-status)

A PHP 8.1 backed enum for tracking delivery lifecycle in your storage layer.

```
use Owlstack\Core\Delivery\DeliveryStatus;

$status = DeliveryStatus::Pending;     // 'pending'
$status = DeliveryStatus::Publishing;  // 'publishing'
$status = DeliveryStatus::Published;   // 'published'
$status = DeliveryStatus::Failed;      // 'failed'
```

 ```
stateDiagram-v2
    [*] --> Pending
    Pending --> Publishing : publish() called
    Publishing --> Published : API success
    Publishing --> Failed : API error / exception
    Failed --> Publishing : retry
```

      Loading ---

### Error Handling

[](#error-handling)

Owlstack Core uses a structured exception hierarchy. The `Publisher` catches all exceptions internally, but you can handle them directly when calling platform methods.

 ```
classDiagram
    RuntimeException  'proxy.example.com',
        'port' => 8080,
        'type' => CURLPROXY_HTTP,
        'auth' => 'user:pass',
    ],
);

// JSON request
$response = $client->post('https://api.example.com/posts', [
    'headers' => ['Authorization' => 'Bearer token'],
    'json' => ['message' => 'Hello'],
]);

// Multipart file upload
$response = $client->post('https://api.example.com/upload', [
    'multipart' => [
        ['name' => 'file', 'contents' => '/path/to/file.jpg', 'filename' => 'photo.jpg'],
    ],
]);

// $response = ['status' => 200, 'headers' => [...], 'body' => '...']
```

Supported options: `headers`, `json`, `body`, `form_params`, `multipart`, `query`.

---

### Support Utilities

[](#support-utilities)

#### Arr — Array Helpers

[](#arr--array-helpers)

```
use Owlstack\Core\Support\Arr;

Arr::get($data, 'user.profile.name', 'Unknown'); // Dot-notation access
Arr::filterEmpty(['a' => 1, 'b' => null, 'c' => '']); // ['a' => 1]
Arr::only($data, ['name', 'email']); // Whitelist keys
```

#### Str — String Helpers

[](#str--string-helpers)

```
use Owlstack\Core\Support\Str;

Str::limit('Hello World', 8, '…'); // 'Hello…'
Str::slug('My Article Title');      // 'my-article-title'
Str::startsWith('Hello', 'He');    // true
```

#### Clock — Testable Time

[](#clock--testable-time)

```
use Owlstack\Core\Support\Clock;

Clock::now();        // DateTimeImmutable
Clock::timestamp();  // int

// In tests: freeze time
Clock::freeze(new DateTimeImmutable('2025-01-01 12:00:00'));
Clock::now(); // always 2025-01-01 12:00:00
Clock::unfreeze();
```

---

Platform Reference
------------------

[](#platform-reference)

### Telegram

[](#telegram)

```
$credentials = new PlatformCredentials('telegram', [
    'api_token' => 'your-bot-token',
    'channel_username' => '@your-channel',
]);

$platform = new TelegramPlatform($credentials, new HttpClient(), new TelegramFormatter());

// Publish with options
$result = $publisher->publish($post, 'telegram', [
    'chat_id' => '@specific-channel',
    'parse_mode' => 'HTML',
    'disable_notification' => true,
    'inline_keyboard' => [
        [['text' => 'Visit Site', 'url' => 'https://example.com']],
    ],
]);

// Extended methods
$platform->sendLocation($chatId, 40.7128, -74.0060);
$platform->sendVenue($chatId, 40.7128, -74.0060, 'NYC Office', '123 Main St');
$platform->sendContact($chatId, '+1234567890', 'John');
$platform->pinMessage($chatId, $messageId);
```

### Twitter/X

[](#twitterx)

```
$credentials = new PlatformCredentials('twitter', [
    'consumer_key' => '...',
    'consumer_secret' => '...',
    'access_token' => '...',
    'access_token_secret' => '...',
]);

$result = $publisher->publish($post, 'twitter', [
    'reply_to' => '1234567890',
    'quote_tweet_id' => '9876543210',
    'poll' => [
        'options' => ['Yes', 'No', 'Maybe'],
        'duration_minutes' => 1440,
    ],
]);
```

> **Note:** Twitter automatically wraps URLs to 23 characters (t.co). The formatter accounts for this in the character budget.

### Facebook

[](#facebook)

```
$credentials = new PlatformCredentials('facebook', [
    'app_id' => '...',
    'app_secret' => '...',
    'page_access_token' => '...',
    'page_id' => '...',
]);

$result = $publisher->publish($post, 'facebook', [
    'privacy' => ['value' => 'EVERYONE'],
    'scheduled_publish_time' => time() + 3600,
]);
```

### LinkedIn

[](#linkedin)

```
// Personal profile
$credentials = new PlatformCredentials('linkedin', [
    'access_token' => '...',
    'person_id' => 'abc123',
]);

// Or company page
$credentials = new PlatformCredentials('linkedin', [
    'access_token' => '...',
    'organization_id' => 'org456',
]);

$result = $publisher->publish($post, 'linkedin', [
    'visibility' => 'PUBLIC',
]);
```

### Discord

[](#discord)

```
// Bot mode
$credentials = new PlatformCredentials('discord', [
    'bot_token' => '...',
    'channel_id' => '...',
]);

// Or webhook mode
$credentials = new PlatformCredentials('discord', [
    'webhook_url' => 'https://discord.com/api/webhooks/...',
]);

$result = $publisher->publish($post, 'discord', [
    'embed' => true,      // Rich embed with title, description, color
    'color' => 0x5865F2,  // Embed color
    'thread_id' => '...',
    'tts' => false,
]);
```

### Instagram

[](#instagram)

```
$credentials = new PlatformCredentials('instagram', [
    'access_token' => '...',
    'instagram_account_id' => '...',
]);

$result = $publisher->publish($post, 'instagram', [
    'media_type' => 'IMAGE',    // IMAGE, REELS, or STORIES
    'image_url' => 'https://example.com/photo.jpg',
    'location_id' => '...',
    'alt_text' => 'Photo description',
    // Carousel
    'carousel' => [
        ['image_url' => 'https://example.com/1.jpg'],
        ['image_url' => 'https://example.com/2.jpg'],
    ],
]);
```

> **Note:** Instagram requires media to be hosted at publicly accessible URLs.

### Pinterest

[](#pinterest)

```
$credentials = new PlatformCredentials('pinterest', [
    'access_token' => '...',
    'board_id' => '...',
]);

$result = $publisher->publish($post, 'pinterest', [
    'board_section_id' => '...',
    'image_url' => 'https://example.com/pin.jpg',
    'alt_text' => 'Pin description',
    'dominant_color' => '#FF5733',
]);
```

### Reddit

[](#reddit)

```
$credentials = new PlatformCredentials('reddit', [
    'client_id' => '...',
    'client_secret' => '...',
    'access_token' => '...',
    'username' => 'your_username',
]);

$result = $publisher->publish($post, 'reddit', [
    'subreddit' => 'php',          // required
    'kind' => 'self',              // 'self' or 'link'
    'flair_id' => '...',
    'nsfw' => false,
    'spoiler' => false,
]);
```

### Slack

[](#slack)

```
// Bot token mode
$credentials = new PlatformCredentials('slack', [
    'bot_token' => 'xoxb-...',
    'channel' => '#general',
]);

// Or webhook mode
$credentials = new PlatformCredentials('slack', [
    'webhook_url' => 'https://hooks.slack.com/services/...',
]);

$result = $publisher->publish($post, 'slack', [
    'blocks' => true,          // Use Block Kit layout
    'thread_ts' => '...',      // Reply in thread
    'unfurl_links' => true,
]);
```

### Tumblr

[](#tumblr)

```
$credentials = new PlatformCredentials('tumblr', [
    'access_token' => '...',
    'blog_identifier' => 'myblog.tumblr.com',
]);

$result = $publisher->publish($post, 'tumblr', [
    'post_type' => 'text',     // text, image, video, link, audio
    'state' => 'published',    // published, draft, queue, private
    'slug' => 'my-post-slug',
]);
```

### WhatsApp

[](#whatsapp)

```
$credentials = new PlatformCredentials('whatsapp', [
    'access_token' => '...',
    'phone_number_id' => '...',
]);

$result = $publisher->publish($post, 'whatsapp', [
    'to' => '+1234567890',              // required, E.164 format
    'message_type' => 'text',           // text, image, video, document, template
    'template_name' => 'hello_world',   // for template messages
    'template_lang' => 'en_US',
    'preview_url' => true,
]);
```

---

Multi-Platform Publishing
-------------------------

[](#multi-platform-publishing)

```
$registry = new PlatformRegistry();
$registry->register($telegramPlatform);
$registry->register($twitterPlatform);
$registry->register($discordPlatform);

$publisher = new Publisher($registry);

$post = new Post(
    title: 'New Release: v2.0',
    body: 'We are excited to announce version 2.0 with multi-platform support!',
    url: 'https://example.com/releases/v2',
    tags: ['release', 'opensource'],
);

// Publish to all registered platforms
$results = [];
foreach ($registry->names() as $name) {
    $results[$name] = $publisher->publish($post, $name);
}

// Check results
foreach ($results as $platform => $result) {
    echo $result->success
        ? "✓ {$platform}: {$result->externalUrl}\n"
        : "✗ {$platform}: {$result->error}\n";
}
```

---

Advanced Usage
--------------

[](#advanced-usage)

### Custom Platform

[](#custom-platform)

Implement `PlatformInterface` to add a new platform:

```
use Owlstack\Core\Platforms\Contracts\PlatformInterface;
use Owlstack\Core\Platforms\Contracts\PlatformResponseInterface;
use Owlstack\Core\Platforms\PlatformResponse;
use Owlstack\Core\Content\Post;

class MastodonPlatform implements PlatformInterface
{
    public function name(): string
    {
        return 'mastodon';
    }

    public function publish(Post $post, array $options = []): PlatformResponseInterface
    {
        // Your implementation...
        return PlatformResponse::success(
            externalId: '12345',
            externalUrl: 'https://mastodon.social/@user/12345',
            rawResponse: $apiResponse,
        );
    }

    public function delete(string $externalId): bool
    {
        // Your implementation...
        return true;
    }

    public function validateCredentials(): bool
    {
        // Your implementation...
        return true;
    }

    public function constraints(): array
    {
        return [
            'max_text_length' => 500,
            'max_media_count' => 4,
            'supported_media_types' => ['image/jpeg', 'image/png', 'image/gif'],
            'max_media_size' => 10 * 1024 * 1024,
        ];
    }
}
```

### Custom Formatter

[](#custom-formatter)

```
use Owlstack\Core\Formatting\Contracts\FormatterInterface;
use Owlstack\Core\Content\Post;

class MastodonFormatter implements FormatterInterface
{
    public function format(Post $post, array $options = []): string
    {
        // Build formatted content for Mastodon...
        return $formatted;
    }

    public function platform(): string
    {
        return 'mastodon';
    }

    public function maxLength(): int
    {
        return 500;
    }
}
```

### Custom Token Store

[](#custom-token-store)

```
use Owlstack\Core\Auth\Contracts\TokenStoreInterface;
use Owlstack\Core\Auth\AccessToken;

class DatabaseTokenStore implements TokenStoreInterface
{
    public function get(string $platform, string $accountId): ?AccessToken { /* ... */ }
    public function store(string $platform, string $accountId, AccessToken $token): void { /* ... */ }
    public function revoke(string $platform, string $accountId): void { /* ... */ }
    public function has(string $platform, string $accountId): bool { /* ... */ }
}
```

### Proxy Configuration

[](#proxy-configuration)

```
$client = new HttpClient(
    proxy: [
        'host' => 'proxy.example.com',
        'port' => 8080,
        'type' => CURLPROXY_SOCKS5,
        'auth' => 'username:password',
    ],
);
```

---

Testing
-------

[](#testing)

```
# Run all tests
composer test

# Run unit tests only
./vendor/bin/phpunit --testsuite Unit

# Run integration tests only
./vendor/bin/phpunit --testsuite Integration
```

The `Clock::freeze()` utility lets you control time in tests:

```
use Owlstack\Core\Support\Clock;

Clock::freeze(new DateTimeImmutable('2025-06-15 10:00:00'));
// All Clock::now() calls return the frozen time
Clock::unfreeze();
```

---

Framework Integrations
----------------------

[](#framework-integrations)

PackageFrameworkRepository**owlstack/owlstack-laravel**Laravel 10+[owlstack-laravel](https://github.com/owlstacks/owlstack-laravel)**owlstack/owlstack-wordpress**WordPress 6+[owlstack-wordpress](https://github.com/owlstacks/owlstack-wordpress)---

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute.

Security
--------

[](#security)

If you discover a security vulnerability, please review [SECURITY.md](SECURITY.md) for reporting instructions.

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for details.

---

 Built with 🦉 by [Ali Hesari](https://alihesari.com)

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance79

Regular maintenance activity

Popularity17

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~20 days

Total

2

Last Release

119d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/a7d6c137ddd414373cafbec4d67cb29e8b0e20802d9eb6b3331d9b501bcbe01b?d=identicon)[alihesari](/maintainers/alihesari)

---

Top Contributors

[![alihesari](https://avatars.githubusercontent.com/u/8302633?v=4)](https://github.com/alihesari "alihesari (140 commits)")

---

Tags

facebooktwitterpublishingtelegramsocial mediaxowlstackcontent-syndication

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/owlstack-owlstack-core/health.svg)

```
[![Health](https://phpackages.com/badges/owlstack-owlstack-core/health.svg)](https://phpackages.com/packages/owlstack-owlstack-core)
```

###  Alternatives

[nystudio107/craft-seomatic

SEOmatic facilitates modern SEO best practices &amp; implementation for Craft CMS 5. It is a turnkey SEO system that is comprehensive, powerful, and flexible.

1741.5M61](/packages/nystudio107-craft-seomatic)[social-links/social-links

PHP library to generate share buttons

112354.3k2](/packages/social-links-social-links)[kartik-v/yii2-social

Module containing useful widgets for Yii Framework 2.0 that integrates social functionalities from DISQUS, Facebook, Google etc.

93263.5k8](/packages/kartik-v-yii2-social)[pdir/social-feed-bundle

Social feed extension for Contao CMS

1615.7k](/packages/pdir-social-feed-bundle)[jonom/silverstripe-share-care

Social media sharing previews and customisation for Silverstripe

3033.5k1](/packages/jonom-silverstripe-share-care)[fritzmg/contao-sharebuttons

Simple Contao extension to provide share buttons as a module and content element

1833.5k](/packages/fritzmg-contao-sharebuttons)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
