Skip to content

Commit 1dc5b53

Browse files
committed
Add command to post to twitter
1 parent fe9406c commit 1dc5b53

File tree

6 files changed

+206
-3
lines changed

6 files changed

+206
-3
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\Article;
6+
use Illuminate\Console\Command;
7+
use Illuminate\Notifications\AnonymousNotifiable;
8+
use App\Notifications\PostArticleToTwitter as PostArticleToTwitterNotification;
9+
10+
final class PostArticleToTwitter extends Command
11+
{
12+
protected $signature = 'post-article-to-twitter';
13+
14+
protected $description = 'Posts the latest unshared article to Twitter';
15+
16+
private $notifiable;
17+
18+
public function __construct(AnonymousNotifiable $notifiable)
19+
{
20+
parent::__construct();
21+
$this->notifiable = $notifiable;
22+
}
23+
24+
public function handle()
25+
{
26+
if (! $article = Article::nextForSharing()) {
27+
return;
28+
}
29+
30+
$this->notifiable->notify(new PostArticleToTwitterNotification($article));
31+
32+
$article->markAsShared();
33+
}
34+
}

app/Console/Kernel.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ protected function schedule(Schedule $schedule)
2323
$schedule->command('backup:clean')->daily()->at('01:00');
2424
$schedule->command('backup:run')->daily()->at('02:00');
2525
$schedule->command('horizon:snapshot')->everyFiveMinutes();
26+
$schedule->command('post-article-to-twitter')
27+
->everyMinute()
28+
->when(function () {
29+
// ~2 posts every 24 hours.
30+
return random_int(1, 1440) <= 2;
31+
});
2632
}
2733

2834
/**

app/Models/Article.php

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ final class Article extends Model
3535
'is_pinned',
3636
'submitted_at',
3737
'approved_at',
38+
'shared_at',
3839
];
3940

4041
/**
@@ -43,6 +44,7 @@ final class Article extends Model
4344
protected $dates = [
4445
'submitted_at',
4546
'approved_at',
47+
'shared_at',
4648
];
4749

4850
public function id(): int
@@ -82,7 +84,7 @@ public function series()
8284

8385
public function updateSeries(Series $series = null): self
8486
{
85-
if (is_null($series)) {
87+
if (null === $series) {
8688
return $this->removeSeries();
8789
}
8890

@@ -122,7 +124,7 @@ public function isSubmitted(): bool
122124

123125
public function isNotSubmitted(): bool
124126
{
125-
return is_null($this->submitted_at);
127+
return null === $this->submitted_at;
126128
}
127129

128130
public function isApproved(): bool
@@ -132,7 +134,7 @@ public function isApproved(): bool
132134

133135
public function isNotApproved(): bool
134136
{
135-
return is_null($this->approved_at);
137+
return null === $this->approved_at;
136138
}
137139

138140
public function isPublished(): bool
@@ -150,6 +152,16 @@ public function isPinned(): bool
150152
return (bool) $this->is_pinned;
151153
}
152154

155+
public function isNotShared(): bool
156+
{
157+
return null === $this->shared_at;
158+
}
159+
160+
public function isShared(): bool
161+
{
162+
return ! $this->isNotShared();
163+
}
164+
153165
public function isAwaitingApproval(): bool
154166
{
155167
return $this->isSubmitted() && $this->isNotApproved();
@@ -202,6 +214,16 @@ public function scopeNotPublished(Builder $query): Builder
202214
});
203215
}
204216

217+
public function scopeShared(Builder $query): Builder
218+
{
219+
return $query->whereNotNull('shared_at');
220+
}
221+
222+
public function scopeNotShared(Builder $query): Builder
223+
{
224+
return $query->whereNull('shared_at');
225+
}
226+
205227
public function scopeForTag(Builder $query, string $tag): Builder
206228
{
207229
return $query->whereHas('tagsRelation', function ($query) use ($tag) {
@@ -270,4 +292,19 @@ public function splitBody($value)
270292
{
271293
return $this->split($value);
272294
}
295+
296+
public function markAsShared()
297+
{
298+
$this->update([
299+
'shared_at' => now(),
300+
]);
301+
}
302+
303+
public static function nextForSharing(): ?self
304+
{
305+
return self::notShared()
306+
->published()
307+
->orderBy('submitted_at', 'asc')
308+
->first();
309+
}
273310
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace App\Notifications;
4+
5+
use App\Models\Article;
6+
use Illuminate\Bus\Queueable;
7+
use Illuminate\Notifications\Notification;
8+
use NotificationChannels\Twitter\TwitterChannel;
9+
use NotificationChannels\Twitter\TwitterStatusUpdate;
10+
11+
class PostArticleToTwitter extends Notification
12+
{
13+
use Queueable;
14+
15+
private $article;
16+
17+
public function __construct(Article $article)
18+
{
19+
$this->article = $article;
20+
}
21+
22+
public function via($notifiable)
23+
{
24+
return [TwitterChannel::class];
25+
}
26+
27+
public function toTwitter($notifiable)
28+
{
29+
$title = $this->article->title();
30+
$url = route('articles.show', $this->article->slug());
31+
$author = $this->article->author()->name();
32+
33+
return new TwitterStatusUpdate("{$title} - {$author}\n\n{$url}");
34+
}
35+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Schema;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Database\Migrations\Migration;
6+
7+
class UpdateArticlesTableAddSharedAtField extends Migration
8+
{
9+
public function up()
10+
{
11+
Schema::table('articles', function (Blueprint $table) {
12+
$table->dateTime('shared_at')->after('approved_at')->nullable();
13+
});
14+
}
15+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace Tests\Unit\Commands;
4+
5+
use Tests\TestCase;
6+
use App\Models\Article;
7+
use Illuminate\Support\Facades\Notification;
8+
use App\Console\Commands\PostArticleToTwitter;
9+
use Illuminate\Notifications\AnonymousNotifiable;
10+
use Illuminate\Foundation\Testing\DatabaseMigrations;
11+
use App\Notifications\PostArticleToTwitter as PostArticleToTwitterNotification;
12+
13+
class PostArticleToTwitterTest extends TestCase
14+
{
15+
use DatabaseMigrations;
16+
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
Notification::fake();
21+
}
22+
23+
/** @test */
24+
public function published_articles_can_be_shared_on_twitter()
25+
{
26+
$article = factory(Article::class)->create([
27+
'submitted_at' => now(),
28+
'approved_at' => now(),
29+
]);
30+
31+
(new PostArticleToTwitter(new AnonymousNotifiable()))->handle();
32+
33+
Notification::assertSentTo(
34+
new AnonymousNotifiable,
35+
PostArticleToTwitterNotification::class
36+
);
37+
38+
$this->assertTrue($article->fresh()->isShared());
39+
}
40+
41+
/** @test */
42+
public function already_shared_articles_are_not_shared_again()
43+
{
44+
factory(Article::class)->create([
45+
'submitted_at' => now(),
46+
'approved_at' => now(),
47+
'shared_at' => now(),
48+
]);
49+
50+
(new PostArticleToTwitter(new AnonymousNotifiable()))->handle();
51+
52+
Notification::assertNothingSent();
53+
}
54+
55+
/** @test */
56+
public function unapproved_articles_are_not_shared()
57+
{
58+
factory(Article::class)->create([
59+
'submitted_at' => now(),
60+
]);
61+
62+
(new PostArticleToTwitter(new AnonymousNotifiable()))->handle();
63+
64+
Notification::assertNothingSent();
65+
}
66+
67+
/** @test */
68+
public function unsubmitted_articles_are_not_shared()
69+
{
70+
factory(Article::class)->create();
71+
72+
(new PostArticleToTwitter(new AnonymousNotifiable()))->handle();
73+
74+
Notification::assertNothingSent();
75+
}
76+
}

0 commit comments

Comments
 (0)