Automating Tweets when publishing a new post | Clean Integrations Part 1

Published 21/12/2021 | Last updated 24/12/2021 | 1929 views

I recently added auto-tweet functionality to my blog. It's surprisingly simple! Let me show you how to get started.

An example tweet using this blog's auto-tweet functionality.

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=local
APP_KEY=base64:asiofnaiornq38r9qhrfniofen3289dhqo=
APP_DEBUG=true
APP_URL=http://my-blog-test
 
TWITTER_CONSUMER_KEY=YOUR_CONSUMER_KEY_HERE
TWITTER_CONSUMER_SECRET=YOUR_CONSUMER_SECRET_HERE
TWITTER_ACCESS_TOKEN=YOUR_ACCESS_TOKEN_HERE
TWITTER_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

Like what you see?

If you enjoy reading my content, please consider sponsoring me. I don't spend it on cups of coffee; it all goes towards freeing up more of my time to work on open source, tutorials and more posts like this one.