Automating Tweets when publishing a new post | Clean Integrations Part 1
Published 21/12/2021 | Last updated 24/12/2021 | 2220 viewsI recently added auto-tweet functionality to my blog. It's surprisingly simple! Let me show you how to get started.
As I recently announced, I recently switched this blog from Statamic to Wink. This simplified the stack a lot, and allowed me to focus on writing and adding features that really matter without distraction. For example, I've added a feature that uses the YouTube API to fetch my latest videos and display them on my blog's home page.
One feature that I really wanted to add as a QOL improvement was automating a tweet when a new post went live. This means that if I write up a post and schedule it to be published tomorrow, for example, I don't have to remember to tweet about it once it becomes available; my blog will automatically fire off a queued job that will take care of it for me. Features like this can save you a lot of time in the long run and don't take long to implement, so let me show you how I did it.
Getting access to Twitter developer credentials
First and foremost, we need to integrate with Twitter's APIs. The API is free for the most part (certainly for what we want to do here), and you can get started by signing up for a developer account.
You'll need 4 pieces of information from your Twitter dev account:
- A consumer key
- A consumer secret
- An access token
- An access token secret
The first two are tied to your developer account. The second two are provided once you've created a Twitter project for your site. Where shall we put these keys? Laravel offers us a nice location for all things like this in config/services.php
. In that file, let's create a new key called 'twitter' with the respective entries for each key.
<?php return [ 'mailgun' => [ 'domain' => env('MAILGUN_DOMAIN'), 'secret' => env('MAILGUN_SECRET'), 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), ], 'postmark' => [ 'token' => env('POSTMARK_TOKEN'), ], 'ses' => [ 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], 'twitter' => [ 'consumer_key' => env('TWITTER_CONSUMER_KEY'), 'consumer_secret' => env('TWITTER_CONSUMER_SECRET'), 'access_token' => env('TWITTER_ACCESS_TOKEN'), 'access_token_secret' => env('TWITTER_ACCESS_TOKEN_SECRET'), ], ];
Note that we're referencing the env
function here. You should avoid placing sensitive data like keys in config files because these files will be stored in git history. Instead, we can use the env
function and store the values in our project's .env
file instead.
APP_NAME="My Blog"APP_ENV=localAPP_KEY=base64:asiofnaiornq38r9qhrfniofen3289dhqo=APP_DEBUG=trueAPP_URL=http://my-blog-test TWITTER_CONSUMER_KEY=YOUR_CONSUMER_KEY_HERETWITTER_CONSUMER_SECRET=YOUR_CONSUMER_SECRET_HERETWITTER_ACCESS_TOKEN=YOUR_ACCESS_TOKEN_HERETWITTER_ACCESS_TOKEN_SECRET=YOUR_ACCESS_TOKEN_SECRET_HERE
The Twitter class
To handle all the heavy lifting, we can make use of an existing library: abraham/twitteroauth. We pass in our credentials, and this library takes care of authentication so that we can get straight to the meat and potatoes.
You can install it via composer.
composer require abraham/twitteroauth
Now, you could use this directly, but that's rarely a good idea. In general, make it a rule of thumb to push dependencies to the boundaries of your application. What does that mean in practice? An interface, or contract, and an implementation.
I'm going to create a new interface in my project called Twitter
. I'll place it in app/Contracts/Services/Twitter.php
(you'll have to create this directory). The contract has a single method: tweet
.
<?php namespace App\Contracts\Services; interface Twitter{ public function tweet(string $message): void;}
Now we create our implementation. I'll place this in app/Services/Twitter/Oauth.php
.
<?php namespace App\Services\Twitter; use Abraham\TwitterOAuth\TwitterOAuth;use App\Contracts\Services\Twitter; final class OauthClient implements Twitter{ private TwitterOAuth $client; public function __construct() { $options = config('services.twitter'); $this->client = new TwitterOAuth( $options['consumer_key'], $options['consumer_secret'], $options['access_token'], $options['access_token_secret'] ); $this->client->setApiVersion('2') } public function tweet(string $message): void { $this->client->post('tweets', ['text' => $message], true); }}
Here's our implementation. Note that it implements our Twitter
contract. In our constructor, we create a new instance of the TwitterOAuth
class provided by the abraham/twitteroauth
dependency. We grab our keys from the services
config file, and pass those in. We also inform the TwitterOAuth
class that we want to use version 2 of Twitter's API.
Our tweet
method then posts to the tweets
API endpoint, sending the requested $message
as the content of the tweet. The true
boolean passed as the third parameter indicates that we want this to be a JSON request.
In reality, I'd likely pass in the keys via the constructor rather than accessing them with the config
function here to allow for simpler testing, but for the purposes of this tutorial, this is fine.
We now need to bind our OauthClient
implementation to our Twitter
contract. You can do that in your project's AppServiceProvider
.
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider;use App\Contracts\Services\Twitter;use App\Services\Twitter\OauthClient; class AppServiceProvider extends ServiceProvider{ public function register(): void { $this->app->bind(Twitter::class, OauthClient::class); } }
This informs Laravel that any time we request an instance of the Twitter
contract, we actually want an instance of the OauthClient
.
The job
The next piece of this puzzle is a job that we can dispatch to Laravel's queue. It will be in charge of calling our Twitter
contract and passing the correct message to it based on the post the job receives. You can create a new job very easily using artisan.
php artisan make:job SendTweetAboutPublishedPost
In the generated job class, let's receive a instance of our post model. I'm using Wink to manage my blog, so here it will be a WinkPost
.
<?php namespace App\Jobs; use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Bus\Dispatchable;use Illuminate\Queue\InteractsWithQueue;use Illuminate\Queue\SerializesModels;use Wink\WinkPost; final class SendTweetAboutPublishedPost implements ShouldQueue{ use Dispatchable; use InteractsWithQueue; use Queueable; use SerializesModels; public function __construct(private WinkPost $post) { } public function handle(): void { }}
Next, let's move to the handle
method, which will be in charge of sending our tweet. The handle
method supports dependency injection, so we can resolve our Twitter
contract as a method parameter. We can then use our tweet
method along with the post title to send out a tweet. Using the post title should mean that we never go above Twitter's character limit.
<?php namespace App\Jobs; use App\Contracts\Services\Twitter; use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Bus\Dispatchable;use Illuminate\Queue\InteractsWithQueue;use Illuminate\Queue\SerializesModels;use Wink\WinkPost; final class SendTweetAboutPublishedPost implements ShouldQueue{ use Dispatchable; use InteractsWithQueue; use Queueable; use SerializesModels; public function __construct(private WinkPost $post) { } public function handle(Twitter $twitter): void { $postUrl = route('posts.show', $this->post->slug); $twitter->tweet("New blog post available 📬: {$this->post->title}\n\n{$postUrl}"); } }
Obviously, if by the time your job is dispatched the post is no longer published, we want to avoid tweeting about it. This will be dependant on how you store posts in your application. Here's how I'll implement this using Wink.
<?php declare(strict_types=1); namespace App\Jobs; use App\Contracts\Services\Twitter;use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Bus\Dispatchable;use Illuminate\Queue\InteractsWithQueue;use Illuminate\Queue\SerializesModels;use Wink\WinkPost; final class SendTweetAboutPublishedPost implements ShouldQueue{ use Dispatchable; use InteractsWithQueue; use Queueable; use SerializesModels; public function __construct(private WinkPost $post) { } public function handle(Twitter $twitter): void { if ($this->postIsPublic() === false) { return; } $postUrl = route('posts.show', $this->post->slug); $twitter->tweet("New blog post available 📬: {$this->post->title}\n\n{$postUrl}"); } private function postIsPublic(): bool { if ($this->post->published === false) { return false; } if ($this->post->publish_date->isFuture()) { return false; } return true; } }
Sweet! We now have a job that will successfully be able to connect to Twitter and tell the world about our blog post. Time to dispatch the job.
Dispatching the job
This will be highly dependant on how your blogs are stored. In the case of Wink, I decided to use the WinkPost::saved
static method in the boot
method of my AppServiceProvider
to set this up.
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider;use App\Contracts\Services\Twitter;use App\Services\Twitter\OauthClient;use App\Jobs\SendTweetAboutPublishedPost; use Wink\WinkPost; class AppServiceProvider extends ServiceProvider{ public function register(): void { $this->app->bind(Twitter::class, OauthClient::class); } public function boot(): void { WinkPost::saved(function (WinkPost $post) { if ($post->published && $post->wasChanged('published')) { // We add a second to the delay to make sure the tweet is sent after the post is published. SendTweetAboutPublishedPost::dispatch($post)->delay($post->publish_date->addSecond()); } }); } }
So, after a WinkPost
is saved to the database, we check if the published
property is set to true
and published
was previouly false
before changing. This ensures that we will only dispatch this job once, rather than every time we update the post in the future.
If both of those conditions are met, we will dispatch our job, passing in the saved $post
. We also add a delay. Wink supports publishing at a future date and time. We don't want to tweet about our post immediately if it won't be public until tomorrow, so we'll use the delay
method, passing in our WinkPost's publish date, to stop our job being processed until it is live.
With this complete, we now have fully functioning auto-tweet capabilities in our application. The next time we publish a post, as soon as that post becomes available, our Twitter followers will automatically be notified by a nice little tweet in their feed. Pretty cool hey?
Happy coding!
Kind Regards, Luke