PHP code example of siberfx / linkedin-autopost

1. Go to this page and download the library: Download siberfx/linkedin-autopost library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

siberfx / linkedin-autopost example snippets


// app/Providers/AppServiceProvider.php → boot()
Gate::define('manage-linkedin-autopost', fn (User $user) => $user->is_admin);

// app/Models/Article.php
class Article extends Model implements ShareableOnLinkedIn
{
    use PostsToLinkedIn;

    public function isLiveForLinkedIn(): bool
    {
        return $this->status === 'published';
    }

    public function toLinkedInPost(): LinkPost
    {
        return LinkPost::make(route('blog.show', $this), $this->title);
    }
}

use App\Models\User;
use Illuminate\Support\Facades\Gate;

Gate::define('manage-linkedin-autopost', fn (User $user) => $user->is_admin);

// spatie/laravel-permission
Gate::define('manage-linkedin-autopost', fn (User $user) => $user->hasRole('marketing'));

// a fixed list
Gate::define('manage-linkedin-autopost', fn (User $user) => in_array($user->email, config('app.admins'), true));

'routes' => [
    'enabled' => true,
    'prefix' => 'admin/linkedin',        // → /admin/linkedin/redirect, /admin/linkedin/callback …
    'name' => 'linkedin-autopost.',
    'middleware' => ['web', 'auth:admin', 'can:manage-linkedin-autopost'],
    'after_connect' => '/admin/settings',
],

use Siberfx\LinkedInAutopost\Facades\LinkedIn;

if (LinkedIn::isConnected()) {
    $connection = LinkedIn::connection();

    $connection->name;        // "Ada Lovelace"
    $connection->email;       // "[email protected]"
    $connection->status;      // active | expired | revoked | unknown
    $connection->expiresAt;   // CarbonImmutable|null
    $connection->daysLeft();  // 53
    $connection->toArray();   // everything above as an array, never the token
}

use Illuminate\Support\Str;
use Siberfx\LinkedInAutopost\Concerns\PostsToLinkedIn;
use Siberfx\LinkedInAutopost\Contracts\ShareableOnLinkedIn;
use Siberfx\LinkedInAutopost\Posts\LinkPost;

class Article extends Model implements ShareableOnLinkedIn
{
    use PostsToLinkedIn;

    public function isLiveForLinkedIn(): bool
    {
        return $this->status === 'published';
    }

    public function toLinkedInPost(): LinkPost
    {
        return LinkPost::make(url: route('blog.show', $this), title: $this->title)
            ->description(Str::limit(strip_tags($this->body), 250))
            ->commentary("New on the blog: {title}\n\n{url}");
    }
}

// An enum
public function isLiveForLinkedIn(): bool
{
    return $this->status === ArticleStatus::Published;
}

// A boolean plus a publish date (pair with "Scheduled publishing" in section 7)
public function isLiveForLinkedIn(): bool
{
    return $this->is_published && $this->published_at?->isPast();
}

// Only some records go to LinkedIn
public function isLiveForLinkedIn(): bool
{
    return $this->status === 'published' && $this->share_on_linkedin;
}

// Product launch with a price line
return LinkPost::make(route('shop.products.show', $this), $this->name)
    ->description($this->tagline)
    ->commentary("Just launched: {title} — €{$this->price}\n\n{url}");

// Events with a date in the text
return LinkPost::make(route('events.show', $this), $this->title)
    ->description($this->venue)
    ->commentary("Join us on {$this->starts_at->isoFormat('D MMMM')}: {title}\n\nTickets: {url}");

// Translated text, per model locale
return LinkPost::make(route('blog.show', $this), $this->title)
    ->commentary(__('linkedin.new_article', locale: $this->locale)."\n\n{url}");

Article::create(['title' => 'Hello', 'status' => 'published']);  // queued
$draft->update(['status' => 'published']);                        // queued (draft → published)
$published->update(['title' => 'Typo fixed']);                    // not queued: it was already live
Article::where('id', 5)->update(['status' => 'published']);       // not queued: no model events
$article->saveQuietly();                                          // not queued: no model events

'autopost' => [
    'enabled' => (bool) env('LINKEDIN_AUTOPOST', false),
    'in_console' => false,
    'queue_connection' => 'redis',   // null = your default connection
    'queue' => 'linkedin',           // null = the default queue
],
'job' => [
    'tries' => 3,
    'backoff' => [60, 300],          // seconds before the 2nd and 3rd attempt
],

use Siberfx\LinkedInAutopost\Facades\LinkedIn;

LinkedIn::share($article);           // now; returns the LinkedInPost record (re-sharing is allowed)
LinkedIn::queue($article);           // in the background
LinkedIn::wasPosted($article);       // bool, same as $article->wasPostedToLinkedIn()
$article->wasPostedToLinkedIn();     // bool
$article->linkedInPosts;             // history: post_urn, status, trigger, error, posted_at

use Siberfx\LinkedInAutopost\Exceptions\LinkedInRequestFailed;
use Siberfx\LinkedInAutopost\Exceptions\NotConnected;
use Siberfx\LinkedInAutopost\Exceptions\NotShareable;
use Siberfx\LinkedInAutopost\Facades\LinkedIn;

class ArticleLinkedInController
{
    public function __invoke(Article $article)
    {
        Gate::authorize('manage-linkedin-autopost');

        try {
            $post = LinkedIn::share($article);
        } catch (NotConnected) {
            return back()->with('error', 'Connect LinkedIn first.');
        } catch (NotShareable) {
            return back()->with('error', 'Publish the article before sharing it.');
        } catch (InvalidArgumentException $e) {
            return back()->with('error', $e->getMessage());     // e.g. a relative URL
        } catch (LinkedInRequestFailed $e) {
            if ($e->isUnauthorized()) {
                return back()->with('error', 'The LinkedIn token expired. Connect again.');
            }

            return back()->with('error', $e->getMessage());     // "LinkedIn rejected the request: …"
        }

        return back()->with('success', 'Shared on LinkedIn: '.$post->post_urn);
    }
}

if (! LinkedIn::wasPosted($article)) {
    LinkedIn::queue($article);
}

use Siberfx\LinkedInAutopost\Models\LinkedInPost;

$article->linkedInPosts()->latest('id')->first();      // last attempt

LinkedInPost::query()->for($article)->posted()->get();  // successful posts of one model

LinkedInPost::query()                                   // every failure this week
    ->where('status', LinkedInPost::STATUS_FAILED)
    ->where('created_at', '>=', now()->subWeek())
    ->with('shareable')
    ->get();

// Or react inside one of your own Livewire components
use Livewire\Attributes\On;

#[On('linkedin-autopost-shared')]
public function refreshHistory(?string $urn = null): void
{
    $this->history = $this->article->linkedInPosts()->latest('id')->get();
}

// A blog: posted when published.
class Article extends Model implements ShareableOnLinkedIn
{
    use PostsToLinkedIn;

    public function isLiveForLinkedIn(): bool
    {
        return $this->status === 'published';
    }

    public function toLinkedInPost(): LinkPost
    {
        return LinkPost::make(route('blog.show', $this), $this->title)
            ->description(Str::limit(strip_tags($this->body), 250));
    }
}

// Job vacancies: posted when active and still open.
class JobVacancy extends Model implements ShareableOnLinkedIn
{
    use PostsToLinkedIn;

    protected $casts = ['is_active' => 'boolean', 'closing_date' => 'date'];

    public function isLiveForLinkedIn(): bool
    {
        return $this->is_active && ($this->closing_date === null || $this->closing_date->endOfDay()->isFuture());
    }

    public function toLinkedInPost(): LinkPost
    {
        return LinkPost::make(route('careers.show', $this), "We're hiring: {$this->title}")
            ->description("{$this->location} · {$this->type}")
            ->commentary("We're hiring: {title}\n{$this->location} · {$this->type}\n\nApply: {url}");
    }
}

// AppServiceProvider::boot()
use Illuminate\Database\Eloquent\Relations\Relation;

Relation::enforceMorphMap([
    'article' => Article::class,
    'vacancy' => JobVacancy::class,
    'user' => User::class,           // enforceMorphMap needs every morphed model listed
]);

use Illuminate\Support\Facades\Schedule;
use Siberfx\LinkedInAutopost\Facades\LinkedIn;

Schedule::call(function () {
    Article::query()->whereNotNull('published_at')->where('published_at', '<=', now())
        ->whereDoesntHave('linkedInPosts', fn ($q) => $q->where('status', 'posted'))
        ->each(fn (Article $article) => LinkedIn::queue($article));
})->everyFiveMinutes();

// app/Console/QueueDueLinkedInPosts.php
final class QueueDueLinkedInPosts
{
    public function __invoke(): void
    {
        if (! LinkedIn::isConnected()) {
            return;
        }

        JobVacancy::query()
            ->where('is_active', true)
            ->where('opens_at', '<=', now())
            ->whereDoesntHave('linkedInPosts', fn ($q) => $q->where('status', 'posted'))
            ->lazyById()
            ->each(fn (JobVacancy $vacancy) => LinkedIn::queue($vacancy));
    }
}

// routes/console.php
Schedule::call(new QueueDueLinkedInPosts)->everyFiveMinutes()->withoutOverlapping();

Event::listen(Shared::class, function (Shared $event) {
    logger()->info('Posted to LinkedIn', ['urn' => $event->post->post_urn]);
});

Relation::enforceMorphMap(['article' => Article::class]);

Event::listen(ShareFailed::class, fn (ShareFailed $e) =>
    Notification::route('mail', '[email protected]')->notify(new LinkedInShareFailed($e)));

use Siberfx\LinkedInAutopost\Events\Connected;
use Siberfx\LinkedInAutopost\Events\Disconnected;
use Siberfx\LinkedInAutopost\Events\Shared;
use Siberfx\LinkedInAutopost\Events\TokenExpiringSoon;

// Keep the LinkedIn post link on the model itself
Event::listen(Shared::class, function (Shared $event) {
    if ($event->shareable instanceof Article && $event->post->post_urn !== null) {
        $event->shareable->updateQuietly([
            'linkedin_url' => "https://www.linkedin.com/feed/update/{$event->post->post_urn}/",
        ]);
    }
});

// Audit log
Event::listen(Connected::class, fn (Connected $e) =>
    activity()->log("LinkedIn connected as {$e->connection->name}"));
Event::listen(Disconnected::class, fn (Disconnected $e) =>
    activity()->log('LinkedIn disconnected'.($e->revoked ? '' : ' (revocation not confirmed)')));

// Email the admins before the token runs out
Event::listen(TokenExpiringSoon::class, fn (TokenExpiringSoon $e) =>
    Mail::to(config('app.admin_email'))->send(new LinkedInTokenExpiring($e->daysLeft)));

Schedule::command('linkedin:check-token')->daily();

'notifications' => [
    'events' => ['shared' => true, 'failed' => true, 'token_expiring' => true],
    'queued' => true,
    'queue_connection' => null,
    'queue' => null,
    // …
],

Mail::fake();

LinkedIn::share($article);

Mail::assertSent(NotificationMail::class, fn (NotificationMail $mail) => $mail->hasTo('[email protected]'));

'events' => ['shared' => false, 'failed' => true, 'token_expiring' => true],

// Queued, on a dedicated queue with more patience
'queued' => true,
'queue_connection' => 'redis',
'queue' => 'notifications',
'tries' => 5,
'backoff' => [30, 120, 600],

'channels' => [
    // telegram, slack…
    'discord' => [
        'enabled' => (bool) env('LINKEDIN_NOTIFY_DISCORD', false),
        'class' => App\Notifications\DiscordChannel::class,
        'webhook_url' => env('DISCORD_WEBHOOK_URL'),
    ],
],

namespace App\Notifications;

use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Siberfx\LinkedInAutopost\Contracts\NotificationChannel;
use Siberfx\LinkedInAutopost\Exceptions\NotificationFailed;
use Siberfx\LinkedInAutopost\Notifications\Message;

final class DiscordChannel implements NotificationChannel
{
    public function __construct(private readonly array $config = []) {}

    public function configured(): bool
    {
        return filled($this->config['webhook_url'] ?? null);
    }

    public function send(Message $message): void
    {
        $content = "**{$message->emoji()} {$message->headline}**\n".implode("\n", $message->lines);

        try {
            $response = Http::timeout(10)->post($this->config['webhook_url'], ['content' => $content]);
        } catch (ConnectionException $e) {
            // redacted() keeps the webhook URL out of logs and failed_jobs
            throw NotificationFailed::redacted('Could not reach Discord: '.$e->getMessage(), $this->config['webhook_url']);
        }

        if ($response->failed()) {
            throw new NotificationFailed('Discord refused the message: HTTP '.$response->status());
        }
    }
}

Event::listen(Shared::class, fn (Shared $e) =>
    Notification::route('slack', config('services.slack.notifications.channel'))
        ->notify(new PostedToLinkedIn($e->post)));

'routes' => [
    'enabled' => true,
    'prefix' => 'api/linkedin',
    'middleware' => ['api', 'auth:sanctum', 'can:manage-linkedin-autopost'],
    'after_connect' => 'https://admin.example.com/settings/linkedin',   // your SPA page
],

'routes' => ['enabled' => false],

// routes/web.php
Route::middleware(['auth', 'can:manage-linkedin-autopost'])
    ->prefix('admin/integrations/linkedin')
    ->controller(LinkedInIntegrationController::class)
    ->group(function () {
        Route::get('connect', 'connect')->name('admin.linkedin.connect');
        Route::get('callback', 'callback');
        Route::get('/', 'show');
        Route::delete('/', 'destroy');
        Route::post('articles/{article}/share', 'share');
    });

namespace App\Http\Controllers;

use App\Models\Article;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Siberfx\LinkedInAutopost\Exceptions\LinkedInRequestFailed;
use Siberfx\LinkedInAutopost\Exceptions\NotConnected;
use Siberfx\LinkedInAutopost\Exceptions\NotShareable;
use Siberfx\LinkedInAutopost\Facades\LinkedIn;

final class LinkedInIntegrationController
{
    public function connect(Request $request)
    {
        $state = Str::random(40);
        $request->session()->put('linkedin.state', $state);

        return redirect()->away(LinkedIn::authorizationUrl($state));
    }

    public function callback(Request $request)
    {
        abort_unless(hash_equals((string) $request->session()->pull('linkedin.state'), (string) $request->query('state')), 403);

        if ($request->filled('error')) {
            return redirect('/admin/integrations')->with('error', $request->query('error_description', 'Cancelled.'));
        }

        try {
            $connection = LinkedIn::completeConnection((string) $request->query('code'));
        } catch (LinkedInRequestFailed $e) {
            return redirect('/admin/integrations')->with('error', $e->getMessage());
        }

        return redirect('/admin/integrations')->with('success', "Connected as {$connection->name}.");
    }

    public function show()
    {
        return response()->json(LinkedIn::connection()->toArray());
    }

    public function destroy()
    {
        return response()->json(['revoked' => LinkedIn::disconnect()]);
    }

    public function share(Article $article)
    {
        try {
            $post = LinkedIn::share($article);
        } catch (NotConnected|NotShareable|\InvalidArgumentException $e) {
            return response()->json(['message' => $e->getMessage()], 422);
        } catch (LinkedInRequestFailed $e) {
            return response()->json(['message' => $e->getMessage()], 502);
        }

        return response()->json(['post_urn' => $post->post_urn], 201);
    }
}

namespace App\LinkedIn;

use App\Models\Setting;
use Carbon\CarbonImmutable;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
use Siberfx\LinkedInAutopost\Contracts\TokenStore;
use Siberfx\LinkedInAutopost\Data\StoredConnection;

final class SettingsTokenStore implements TokenStore
{
    private const KEY = 'linkedin.connection';

    public function get(): ?StoredConnection
    {
        $value = Setting::query()->where('key', self::KEY)->value('value');

        if ($value === null) {
            return null;
        }

        try {
            $data = json_decode(Crypt::decryptString($value), true, flags: JSON_THROW_ON_ERROR);
        } catch (DecryptException|\JsonException) {
            return null;   // unreadable (e.g. APP_KEY rotated): treated as not connected
        }

        return new StoredConnection(
            accessToken: $data['access_token'],
            authorUrn: $data['author_urn'],
            name: $data['name'],
            email: $data['email'],
            picture: $data['picture'],
            scopes: $data['scopes'],
            expiresAt: $data['expires_at'] ? CarbonImmutable::parse($data['expires_at']) : null,
            connectedAt: $data['connected_at'] ? CarbonImmutable::parse($data['connected_at']) : null,
        );
    }

    public function put(StoredConnection $connection): void
    {
        Setting::query()->updateOrCreate(['key' => self::KEY], ['value' => Crypt::encryptString(json_encode([
            'access_token' => $connection->accessToken,
            'author_urn' => $connection->authorUrn,
            'name' => $connection->name,
            'email' => $connection->email,
            'picture' => $connection->picture,
            'scopes' => $connection->scopes,
            'expires_at' => $connection->expiresAt?->toIso8601String(),
            'connected_at' => $connection->connectedAt?->toIso8601String(),
        ]))]);
    }

    public function forget(): void
    {
        Setting::query()->where('key', self::KEY)->delete();
    }
}

// config/linkedin-autopost.php
'token_store' => App\LinkedIn\SettingsTokenStore::class,

use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Siberfx\LinkedInAutopost\Contracts\TokenStore;
use Siberfx\LinkedInAutopost\Data\StoredConnection;
use Siberfx\LinkedInAutopost\Facades\LinkedIn;
use Siberfx\LinkedInAutopost\Jobs\ShareOnLinkedIn;

beforeEach(function () {
    app(TokenStore::class)->put(new StoredConnection(
        accessToken: 'test-token',
        authorUrn: 'urn:li:person:test',
        expiresAt: CarbonImmutable::now()->addDays(60),
    ));
});

it('queues an article for LinkedIn when it is published', function () {
    Queue::fake();
    config(['linkedin-autopost.autopost.enabled' => true, 'linkedin-autopost.autopost.in_console' => true]);

    $article = Article::factory()->draft()->create();
    $article->update(['status' => 'published']);

    Queue::assertPushed(ShareOnLinkedIn::class, fn ($job) => $job->shareableId === $article->id);
});

it('shares an article', function () {
    Http::fake([
        'https://api.linkedin.com/rest/posts' => Http::response(null, 201, ['x-restli-id' => 'urn:li:share:1']),
    ]);

    $post = LinkedIn::share(Article::factory()->published()->create());

    expect($post->post_urn)->toBe('urn:li:share:1');
    Http::assertSent(fn ($request) => $request['author'] === 'urn:li:person:test'
        && $request['content']['article']['source'] === route('blog.show', $post->shareable));
});

it('notifies Slack about the post', function () {
    config([
        'linkedin-autopost.notifications.enabled' => true,
        'linkedin-autopost.notifications.channels.slack.enabled' => true,
        'linkedin-autopost.notifications.channels.slack.webhook_url' => 'https://hooks.slack.com/services/test',
    ]);
    Http::fake([
        'https://api.linkedin.com/rest/posts' => Http::response(null, 201, ['x-restli-id' => 'urn:li:share:1']),
        'https://hooks.slack.com/*' => Http::response('ok'),
    ]);

    LinkedIn::share(Article::factory()->published()->create());

    Http::assertSent(fn ($request) => str_starts_with($request->url(), 'https://hooks.slack.com/'));
});
bash
composer linkedin:install --theme=tailwind
php artisan migrate
blade
{{-- resources/views/admin/settings.blade.php --}}
<x-linkedin-autopost::connection />
bash
composer linkedin:install          # publishes config + migrations, asks tailwind or bootstrap
php artisan migrate
bash
php artisan vendor:publish --tag=linkedin-autopost-config
php artisan vendor:publish --tag=linkedin-autopost-migrations
php artisan vendor:publish --tag=linkedin-autopost-views    # only when the UI is enabled
php artisan vendor:publish --tag=linkedin-autopost-stubs
bash
php artisan queue:work redis --queue=linkedin,default
blade
@if ($flash = session('linkedin-autopost.flash'))
    <div @class(['alert', 'alert-success' => $flash['type'] === 'success', 'alert-danger' => $flash['type'] === 'error'])>
        {{ $flash['message'] }}
    </div>
@endif
js
// Tailwind v3, tailwind.config.js
content: ['./vendor/siberfx/linkedin-autopost/resources/views/tailwind/**/*.blade.php', /* ... */],
bash
php artisan vendor:publish --tag=linkedin-autopost-views
bash
php artisan linkedin:status
php artisan linkedin:share article 42 [--force]
php artisan linkedin:disconnect [--force]
php artisan linkedin:check-token [--days=7]
php artisan linkedin:test-notification [--channel=slack]
bash
$ php artisan linkedin:share article 42
   INFO  Shared on LinkedIn: urn:li:share:7212345678901234567.

$ php artisan linkedin:share "App\Models\JobVacancy" 7 --force
bash
php artisan linkedin:test-notification [--channel=telegram]
bash
php artisan queue:work redis --queue=notifications,default
bash
php artisan linkedin:status