hamzahassanm/laravel-social-auto-post
A Laravel package for automatically publishing your posts to social media. Schedule and auto-share content across connected platforms, queue posts, and integrate social posting into your app with minimal configuration.
This package wraps platform-specific social APIs behind a shared SocialMediaService base class. v2.6.0 is a breaking release — if you are upgrading, read the Gotchas section before touching code.
Install / upgrade
composer require <vendor>/<package>:^2.6
php artisan vendor:publish --tag=social-media-config # confirm tag name in the package README
Where to look first
config/social-media.php — credentials, retry counts, timeouts, and any media host allowlist.SocialMediaService — the base class now owning validateTextUrl(), validateMediaInput(), and downloadMediaToTempFile(). This is the contract your custom platform drivers extend.SafeMediaFetcher — the only supported path for pulling remote media. Read it before you debug a media failure.First use case: post with a remote image
use Vendor\SocialMedia\Services\InstagramService;
$service = app(InstagramService::class);
$post = $service->post(
text: 'Shipped v2.6.0 🚀',
media: ['https://cdn.example.com/hero.jpg'],
);
Remote URLs are fetched through SafeMediaFetcher::fetch(), validated, streamed to a temp file via downloadMediaToTempFile(), uploaded, then cleaned up. That is the happy path — everything else in this assessment is about the edges.
Let the retry layer do its job — catch narrowly The new exception hierarchy exists so your code can make a decision instead of guessing:
use Vendor\SocialMedia\Exceptions\RateLimitException;
use Vendor\SocialMedia\Exceptions\RetryableException;
try {
$service->post($text, $media);
} catch (RateLimitException $e) {
// Platform told us to slow down. Queue it, don't hammer it.
PostToSocial::dispatch($payload)->delay($e->retryAfter ?? now()->addMinutes(5));
} catch (RetryableException $e) {
// 5xx / timeout. Retries were already exhausted — this is a real outage.
report($e);
$this->release(60);
}
// 4xx (validation, bad token, unsupported media) fails fast and is *not* RetryableException.
// Let it bubble or catch the package's base exception explicitly.
Do not wrap calls in catch (\Throwable) and retry yourself. You will stack your backoff on top of the package's exponential backoff + jitter, and turn a 30-second failure into a five-minute one.
Prefer queue jobs over inline calls
Because 5xx now retries with exponential backoff, a failing post can occupy a worker for a while. Run posts from a job with a $tries that matches (or is lower than) the package's internal retry count so you don't multiply attempts:
public int $tries = 3; // align with config('social-media.retry.max_attempts')
public int $backoff = 30; // package handles jitter; keep yours coarse
Reuse the consolidated validators in custom drivers
validateTextUrl() and validateMediaInput() exist so platform services stop re-implementing URL and media checks. If you maintain your own driver:
class MastodonService extends SocialMediaService
{
public function post(string $text, array $media = []): PostResult
{
$text = $this->validateTextUrl($text); // normalizes/validates links in body copy
$files = $this->validateMediaInput($media); // rejects unsupported types, enforces limits
// ... platform call
}
}
Delete any local copies of that logic — divergence from the base class is how you end up shipping an SSRF hole through your own driver.
Media input handling
downloadMediaToTempFile() is now the single funnel. Pass URLs or local paths through validateMediaInput() first and let the base class resolve them; don't hand-roll file_get_contents() on a user-supplied URL. If you were calling the removed downloadFile(), the migration is mechanical:
// Before (removed in 2.6.0)
$path = $service->downloadFile($url);
// After
$path = $service->downloadMediaToTempFile($url); // memory-safe, streamed to disk
Wrap the call in a try/finally (or use the returned temp path inside a scoped block) if you are composing several uploads in one job — temp files are yours to clean up once you leave the service method.
Async/batch posting For fan-out to N platforms, one job per platform beats one job looping platforms. A single 4xx on platform three shouldn't re-run the two that already succeeded, and per-platform retry budgets stay independent.
Upgrade checklist (these will bite you in production)
SocialMediaService::downloadFile() is gone, not deprecated. Grep your app and your own drivers: grep -rn "downloadFile(" app/.SafeMediaFetcher::execute() is now private. Any direct call fails with a visibility error at runtime, not at boot — it will not show up until that path executes. Switch to fetch().$fetchHandler testing backdoor was removed. Any test that injected a fake handler is now broken; see the testing note below.Retry behavior changes your timing assumptions 4xx calls that used to burn retries now fail fast. If you had alerting tuned to "post failed after 3 attempts / ~90s", 4xx alerts will now fire almost immediately and 5xx alerts will fire later (backoff + jitter). Re-baseline your dashboards before you trust them.
Jitter makes tests slow and flaky
Tests that exercise the retry path will actually sleep. Use Http::fake() to return the 5xx sequence and Queue::fake()/Bus::fake() around the job, or override the retry config in phpunit.xml (max_attempts => 1, base delay 0). Never assert on exact elapsed time.
Testing media fetches without the backdoor Bind a fake into the container, or fake at the HTTP layer:
Http::fake([
'cdn.example.com/*' => Http::response($bytes, 200, ['Content-Type' => 'image/jpeg']),
]);
This is the supported replacement for $fetchHandler and exercises the real validation path.
SSRF hardening will reject hosts you actually use
SafeMediaFetcher validates every resolved A/AAAA record, so these now fail:
localhost, link-local, and metadata endpoints (correctly).If legitimate traffic breaks post-upgrade, do not reach for a bypass — there is no public one now. Use the config allowlist (confirm the key in config/social-media.php) and keep it tight. Prefer mirroring partner media to your own public bucket over allowlisting an internal host.
DNS rebinding protection costs a lookup per URL Every media fetch resolves and validates the full record set before downloading. High-volume batch posting sees measurable DNS latency — enable whatever resolver cache your infra offers, and avoid re-validating the same URL in a loop (resolve/fetch once, reuse the temp file for multiple platforms where the API allows).
IPv6 zone identifiers
fe80::1%eth0-style inputs are now explicitly rejected. If you accept media URLs from end users, expect a small uptick in validation errors from odd inputs — surface a clean "unsupported media URL" message rather than a raw exception.
OOM is fixed, disk is not
downloadMediaToTempFile() solves large-file memory crashes, but the file still lands on local disk. Check upload_max_filesize/post_max_size are irrelevant here — what matters is free space in sys_get_temp_dir() (or your configured temp path) and that cleanup actually runs when a later step throws. Constrain input size in validateMediaInput() rather than relying on the downloader to stop.
Fail-fast means fail loudly A 4xx no longer gets retried into oblivion, so a bad token or expired credential surfaces on attempt one. Good for latency, but it means your error reporting must actually be hooked up before you deploy — silence used to be masked by retries.
Log the exception class, not just the message
RetryableException vs RateLimitException vs a plain 4xx failure are three different operational problems. Include get_class($e) in your log context so you can tell "platform is down" from "we're being throttled" from "our payload is wrong" without reading stack traces.
How can I help you explore Laravel packages today?