Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Linked In Notifier Laravel Package

symfony/linked-in-notifier

Provides LinkedIn integration for Symfony Notifier, letting your app send notifications through LinkedIn. Configure it with a DSN like linkedin://ACCESS_TOKEN:USER_ID@default, where the token is your LinkedIn access token and the user ID identifies the recipient.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony Notifier transport bridge, not a Laravel notification channel. Laravel does not ship symfony/notifier, so "getting started" means deliberately adding a second, small outbound-delivery dependency behind your own adapter — not dropping a package into composer.json and calling it done.

1. Install, pinned to the Symfony line already in your graph.

composer require symfony/notifier symfony/linked-in-notifier symfony/http-client
composer why symfony/http-client   # confirm you're not pulling a second major

The 8.x line of this bridge requires PHP ≥ 8.4. If your Laravel app sits on PHP 8.2/8.3 with Symfony 7 components, pin the whole set to ^7.x explicitly:

"symfony/notifier": "^7.4",
"symfony/linked-in-notifier": "^7.4",
"symfony/http-client": "^7.4"

Run composer update --dry-run before committing — constraint conflicts on symfony/* splits are the single most common failure mode here.

2. Configure the DSN. The format is fixed:

linkedin://ACCESS_TOKEN:USER_ID@default

The token is the userinfo username, the user id is the password, and the host must be default or the factory throws an IncompleteDsnException. Put it in config/services.php:

'linkedin' => [
    'dsn' => env('LINKEDIN_DSN'),
],

The access token lives inside a single config string. It will be written verbatim into bootstrap/cache/config.php by config:cache, so treat that file as a secret and prefer injecting the DSN at runtime from your secrets manager rather than baking it into a cached config on a build artifact.

3. Where to look first — in vendor/, not the README. The README is 20 lines. The real documentation is two files, both small:

  • vendor/symfony/linked-in-notifier/LinkedInTransportFactory.php — tells you exactly which DSN parts are required and how they're validated.
  • vendor/symfony/linked-in-notifier/LinkedInTransport.php — ~100 lines: the endpoint, the headers, the payload shape, and supports(). Read supports() before writing a single line of application code, because it determines whether you send a ChatMessage, a base Message, or something with a LinkedIn-specific options object. Everything downstream depends on that answer.

4. First use case: a one-shot spike in tinker. Prove the credential and the payload shape before designing anything:

$transport = \Symfony\Component\Notifier\Transport::fromDsn(
    config('services.linkedin.dsn'),
    \Symfony\Component\HttpClient\HttpClient::create(['timeout' => 10])
);

$sent = $transport->send(new \Symfony\Component\Notifier\Message\ChatMessage('Hello from Laravel'));
dump($sent->getMessageId()); // store this — it is your only audit handle

No Symfony kernel, container, bundle, or FrameworkBundle is required. Transport::fromDsn() is a static factory that auto-discovers installed bridges via class_exists(). If this spike fails, stop — do not proceed to the adapter.

Realistic first shipping use case: "publish an announcement to our LinkedIn page when a release/changelog entry is published." One-way, low volume, tolerant of a few minutes' delay. Not member-to-member messaging — that capability is gated behind LinkedIn product approval, not behind this package.

Implementation Patterns

Pattern 1 — the adapter channel (the default)

Keep every Symfony\Component\Notifier\* class behind exactly one class. The rest of the codebase should never import a Symfony symbol, so a future swap to a hand-rolled Http::withToken() client is a one-file change.

namespace App\Notifications\Channels;

use App\Services\LinkedIn\LinkedInClient;
use Symfony\Component\Notifier\Message\ChatMessage;

class LinkedInChannel
{
    public function __construct(private LinkedInClient $client) {}

    public function send(object $notifiable, object $notification): void
    {
        if (! method_exists($notification, 'toLinkedIn')) {
            return;
        }

        $this->client->send($notification->toLinkedIn($notifiable));
    }
}

Laravel resolves channels through the container by the value returned from via(), so return the class name and bind the client as a singleton:

public function via(object $notifiable): array
{
    return [LinkedInChannel::class];
}

Bind the transport once in a service provider so token parsing and HTTP client construction happen once per worker lifecycle, not per notification:

$this->app->singleton(LinkedInClient::class, fn () => new LinkedInClient(
    Transport::fromDsn(config('services.linkedin.dsn'), HttpClient::create(['timeout' => 10]))
));

Pattern 2 — queue the job, not the transport

Symfony Messenger does not apply here and should not be introduced. Add ShouldQueue to the notification and let Laravel own retries and backoff:

class ReleaseAnnouncement extends Notification implements ShouldQueue
{
    public int $tries = 3;

    public function backoff(): array
    {
        return [30, 300, 900]; // LinkedIn rate-limits hard; be patient
    }

    public function via(object $notifiable): array
    {
        return [LinkedInChannel::class];
    }

    public function toLinkedIn(object $notifiable): string
    {
        return "v{$this->version} is out — {$this->headline}";
    }

    public function failed(\Throwable $e): void
    {
        Log::channel('linkedin')->error('announcement failed', ['message' => $e->getMessage()]);
    }
}

Isolate the queue (->onQueue('linkedin')) and run a dedicated worker with a low --max-time so a slow LinkedIn endpoint can't stall your main notifications.

Pattern 3 — classify exceptions before you retry

The transport throws Symfony\Component\Notifier\Exception\TransportExceptionInterface, UnsupportedMessageTypeException, and LogicException. Blindly retrying a 401 or a 422 just burns queue attempts and delays your fallback. Map them in the client:

try {
    $sent = $this->transport->send(new ChatMessage($body));
    $this->audit->recordSent($sent->getMessageId(), $body);
} catch (TransportExceptionInterface $e) {
    $code = (int) ($e->getCode() ?: 0);

    if ($code === 429) {
        $this->release(900);          // rate limited — come back later
        return;
    }
    if ($code >= 400 && $code < 500) {
        $this->fail($e);              // bad token / bad payload — don't retry
        return;
    }
    throw $e;                         // 5xx / network — let the queue retry
}

Pattern 4 — degrade gracefully, and log the outcome

SentMessage::getMessageId() is your only receipt. Persist it alongside the notification id in an audit table; without it you cannot answer "did we actually publish?" after an incident. When LinkedIn rejects delivery, fall back to an existing channel rather than silently dropping:

public function via(object $notifiable): array
{
    return $this->shouldPublishToLinkedIn()
        ? [LinkedInChannel::class]
        : ['mail'];
}

Pattern 5 — multi-tenant tokens

The transport is stateless, so per-tenant is just per-DSN. Memoise a small factory keyed by organisation:

public function forTenant(Tenant $tenant): TransportInterface
{
    return $this->instances[$tenant->id] ??= Transport::fromDsn(
        sprintf('linkedin://%s:%s@default',
            rawurlencode($tenant->linkedin_token),
            rawurlencode($tenant->linkedin_user_id),
        ),
        $this->httpClient,
    );
}

Note rawurlencode() on both parts — non-negotiable, see Gotchas.

Pattern 6 — testing

Two layers, and neither uses Http::fake():

// Unit: fake at the Symfony HTTP layer
$client = new MockHttpClient([new MockResponse('', ['response_headers' => ['x-restli-id' => 'urn:li:share:1']])]);
$transport = Transport::fromDsn('linkedin://token:user@default', $client);

// Feature: assert your channel was selected, without touching the network
Notification::fake();
Notification::assertSentOnDemand(ReleaseAnnouncement::class);

Also write one integration test guarded by an env flag that sends a real message against a sandbox app, and asserts an induced 401 surfaces as a TransportExceptionInterface.

Gotchas and Tips

Two notification abstractions is the whole cost. You are adding an outbound client, not a framework. Resist every temptation to also pull in symfony/framework-bundle, symfony/messenger, or to emulate a ParameterBag. The transport constructor and the static DSN factory are the entire coupling surface.

Http::fake() silently does nothing here. The transport talks to Symfony\Contracts\HttpClient\HttpClientInterface, not Laravel's Http facade. Any Guzzle middleware, proxy config, Http::retry(), or logging you have wired to the facade will not apply. If you need Laravel-level observability, wrap the transport: instrument it in your LinkedInClient and pass HttpClient::create([...]) with explicit timeout, proxy, and max_duration options.

The DSN is a footgun with real tokens. Three specific failures:

  • A raw token containing :, /, @, or # breaks userinfo parsing — always rawurlencode(). If the token contains a colon, the parser silently splits it into token + the remainder, and you get a 401 you'll spend an hour misdiagnosing as an approval problem.
  • The host must be default; linkedin://token:[email protected] throws.
  • Never log the DSN. Log (string) $transport instead (credential-free) or just the user id.

Version pinning is a hard constraint, not a preference. 8.x requires PHP ≥ 8.4. Mixing an 8.x bridge into a Symfony 7 dependency graph will resolve to a conflict or, worse, produce a working install with mismatched symfony/notifier internals. Pin the bridge and symfony/notifier to the same ^7.x range as the rest of your Symfony graph, and treat any 8.x upgrade as a coordinated platform-wide PHP 8.4 bump.

The release notes are uniformly "no significant changes" — read that as maintenance-only. This bridge is not receiving feature investment. That means when LinkedIn versions or deprecates the endpoint this transport targets, the fix may never land in the package. Practical mitigations: keep the adapter thin, add a live integration smoke test on a daily schedule, and pre-agree that if the transport breaks, you swap LinkedInClient's internals to a direct Http::withToken() call against LinkedIn's versioned REST API. The exit cost is one class — but only if you enforced Pattern 1.

The real blocker is platform access, and it is outside this repository. Publishing to a LinkedIn page requires an approved developer app with the relevant products granted; notifying an arbitrary member is far more restricted and unavailable to most third-party apps. The transport cannot grant capability you don't already hold. Validate your app's approved products with a live spike before committing engineering time — the package is 21/100 for opportunity reasons, not code quality reasons.

There is no inbound capability. No webhook verification, no challenge handling, no event ingestion, no OAuth onboarding, no token refresh. If "react to LinkedIn engagement" appears on the roadmap, budget separately and do not assume this package contributes.

EventDispatcher — pass null. The third argument to fromDsn() expects Symfony\Contracts\EventDispatcher\EventDispatcherInterface; Laravel's dispatcher does not implement it. Pass null and emit your own Laravel events from the adapter. Do not install symfony/event-dispatcher just to bridge this.

Token rotation and re-consent are yours. There is no refresh handling in the package. Decide who owns rotation, what happens when a member's grant lapses, and how a revoked token surfaces to an operator (a dashboard flag beats a silent 401 in the queue log).

Debugging sequence when a send fails: (1) is the DSN host default and both userinfo parts URL-encoded? (2) does $transport->supports($message) return true for your message class? (3) is the token still valid — hit a trivial LinkedIn endpoint directly with Http::withToken(), bypassing the package entirely; (4) is it a rate limit (429) or a permission (403)? (5) only then read LinkedInTransport::doSend() and compare the emitted payload/headers against LinkedIn's current API version docs. Steps 3 and 5 are where nearly all real failures get resolved, and with 14 stars and zero tracked dependents, there is effectively no prior art to search.

Extension points are limited to decoration. There is no options object to hook into beyond whatever supports() accepts, and no factory hooks unless you register your own. If you need extra DSN parameters, don't patch the vendor file — write your own TransportFactory and construct the transport manually, or accept a plain config array and build the DSN string yourself (which is what Pattern 5 does).

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace