Skip to content

Commit

Permalink
[5.x] Improve handling of scheduled entries (#10966)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonvarga authored Oct 22, 2024
1 parent cce7045 commit b711c9c
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 0 deletions.
28 changes: 28 additions & 0 deletions src/Entries/MinuteEntries.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Statamic\Entries;

use Carbon\Carbon;
use Statamic\Facades\Collection;
use Statamic\Facades\Entry;

class MinuteEntries
{
public function __construct(private readonly Carbon $minute)
{
}

public function __invoke(): \Illuminate\Support\Collection
{
return Entry::query()
->whereIn('collection', Collection::all()->filter->dated()->map->handle()->all())
->whereDate('date', $this->minute->format('Y-m-d'))
->where(function ($query) {
$query->where(function ($query) {
$query
->whereTime('date', '>=', $this->minute->format('H:i').':00')
->whereTime('date', '<=', $this->minute->format('H:i').':59');
});
})->get();
}
}
1 change: 1 addition & 0 deletions src/Events/Concerns/ListensForContentEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ trait ListensForContentEvents
\Statamic\Events\CollectionTreeDeleted::class,
\Statamic\Events\EntryDeleted::class,
\Statamic\Events\EntrySaved::class,
\Statamic\Events\EntryScheduleReached::class,
\Statamic\Events\FieldsetDeleted::class,
\Statamic\Events\FieldsetReset::class,
\Statamic\Events\FieldsetSaved::class,
Expand Down
20 changes: 20 additions & 0 deletions src/Events/EntryScheduleReached.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Statamic\Events;

use Statamic\Contracts\Git\ProvidesCommitMessage;

class EntryScheduleReached extends Event implements ProvidesCommitMessage
{
public $entry;

public function __construct($entry)
{
$this->entry = $entry;
}

public function commitMessage()
{
return __('Entry schedule reached', [], config('statamic.git.locale'));
}
}
32 changes: 32 additions & 0 deletions src/Jobs/HandleEntrySchedule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Statamic\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Collection;
use Statamic\Entries\MinuteEntries;
use Statamic\Events\EntryScheduleReached;

class HandleEntrySchedule implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;

public function handle()
{
$this->entries()->each(fn ($entry) => EntryScheduleReached::dispatch($entry));
}

private function entries(): Collection
{
// We want to target the PREVIOUS minute because we can be sure that any entries that
// were scheduled for then would now be considered published. If we were targeting
// the current minute and the entry has defined a time with seconds later in the
// same minute, it may still be considered scheduled when it gets dispatched.
$minute = now()->subMinute();

return (new MinuteEntries($minute))();
}
}
4 changes: 4 additions & 0 deletions src/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Statamic\Providers;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\AboutCommand;
use Illuminate\Foundation\Http\Middleware\TrimStrings;
use Illuminate\Http\Request;
Expand All @@ -15,6 +16,7 @@
use Statamic\Facades\Stache;
use Statamic\Facades\Token;
use Statamic\Fields\FieldsetRecursionStack;
use Statamic\Jobs\HandleEntrySchedule;
use Statamic\Sites\Sites;
use Statamic\Statamic;
use Statamic\Tokens\Handlers\LivePreview;
Expand Down Expand Up @@ -97,6 +99,8 @@ public function boot()
TrimStrings::skipWhen(fn (Request $request) => $request->is(config('statamic.cp.route').'/*'));

$this->addAboutCommandInfo();

$this->app->make(Schedule::class)->job(new HandleEntrySchedule)->everyMinute();
}

public function register()
Expand Down
2 changes: 2 additions & 0 deletions src/Search/UpdateItemIndexes.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Statamic\Events\AssetSaved;
use Statamic\Events\EntryDeleted;
use Statamic\Events\EntrySaved;
use Statamic\Events\EntryScheduleReached;
use Statamic\Events\TermDeleted;
use Statamic\Events\TermSaved;
use Statamic\Events\UserDeleted;
Expand All @@ -20,6 +21,7 @@ public function subscribe($event)
{
$event->listen(EntrySaved::class, self::class.'@update');
$event->listen(EntryDeleted::class, self::class.'@delete');
$event->listen(EntryScheduleReached::class, self::class.'@update');
$event->listen(AssetSaved::class, self::class.'@update');
$event->listen(AssetDeleted::class, self::class.'@delete');
$event->listen(UserSaved::class, self::class.'@update');
Expand Down
2 changes: 2 additions & 0 deletions src/StaticCaching/Invalidate.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Statamic\Events\CollectionTreeSaved;
use Statamic\Events\EntryDeleting;
use Statamic\Events\EntrySaved;
use Statamic\Events\EntryScheduleReached;
use Statamic\Events\FormDeleted;
use Statamic\Events\FormSaved;
use Statamic\Events\GlobalSetDeleted;
Expand All @@ -32,6 +33,7 @@ class Invalidate implements ShouldQueue
AssetDeleted::class => 'invalidateAsset',
EntrySaved::class => 'invalidateEntry',
EntryDeleting::class => 'invalidateEntry',
EntryScheduleReached::class => 'invalidateEntry',
TermSaved::class => 'invalidateTerm',
TermDeleted::class => 'invalidateTerm',
GlobalSetSaved::class => 'invalidateGlobalSet',
Expand Down
94 changes: 94 additions & 0 deletions tests/Data/Entries/ScheduledEntriesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

namespace Tests\Data\Entries;

use Carbon\Carbon;
use Facades\Tests\Factories\EntryFactory;
use PHPUnit\Framework\Attributes\Test;
use Statamic\Entries\MinuteEntries;
use Statamic\Facades\Blueprint;
use Statamic\Facades\Collection;
use Tests\PreventSavingStacheItemsToDisk;
use Tests\TestCase;

class ScheduledEntriesTest extends TestCase
{
use PreventSavingStacheItemsToDisk;

private function makeCollectionsWithBlueprints($collections)
{
$bps = [];

foreach ($collections as $collection => $fields) {
$bps[$collection] = Blueprint::makeFromFields($fields)->setHandle($collection);
}

foreach ($collections as $collection => $fields) {
Collection::make($collection)->dated(true)->save();
Blueprint::shouldReceive('in')->with('collections/'.$collection)->andReturn(collect(['test' => $bps[$collection]]));
}
}

private function getEntryIdsForMinute($minute)
{
$minute = Carbon::parse($minute);

return (new MinuteEntries($minute))()->map->id->all();
}

#[Test]
public function it_gets_entries_scheduled_for_given_minute()
{
$this->makeCollectionsWithBlueprints([
'time_with_seconds' => [
'date' => ['type' => 'date', 'time_enabled' => true, 'time_seconds_enabled' => true],
],
'time_without_seconds' => [
'date' => ['type' => 'date', 'time_enabled' => true, 'time_seconds_enabled' => false],
],
'dated' => [
'date' => ['type' => 'date', 'time_enabled' => false],
],
'undated' => [],
]);

collect([
'01' => '2023-09-12-121400', // day before
'02' => '2023-09-12-121420', // day before
'03' => '2023-09-12-121421', // day before
'04' => '2023-09-13-121300', // minute before
'05' => '2023-09-13-121320', // minute before
'06' => '2023-09-13-121321', // minute before
'07' => '2023-09-13-121400', // target minute
'08' => '2023-09-13-121420', // target second
'09' => '2023-09-13-121421', // target minute
'10' => '2023-09-13-121500', // minute after
'11' => '2023-09-13-121520', // minute after
'12' => '2023-09-13-121521', // minute after
'13' => '2023-09-14-121400', // day after
'14' => '2023-09-14-121420', // day after
'15' => '2023-09-14-121421', // day after
])->each(fn ($date, $id) => EntryFactory::id($id)->collection('time_with_seconds')->date($date)->create());

collect([
'16' => '2023-09-12-1214', // day before
'17' => '2023-09-13-1213', // minute before
'18' => '2023-09-13-1214', // target minute
'19' => '2023-09-13-1215', // minute after
'20' => '2023-09-14-1214', // day after
])->each(fn ($date, $id) => EntryFactory::id($id)->collection('time_without_seconds')->date($date)->create());

collect([
'21' => '2023-09-12', // day before
'22' => '2023-09-13', // target minute's day
'23' => '2023-09-14', // day after
])->each(fn ($date, $id) => EntryFactory::id($id)->collection('dated')->date($date)->create());

EntryFactory::id('24')->collection('undated')->create();

$this->assertEquals(['07', '08', '09', '18'], $this->getEntryIdsForMinute('2023-09-13 12:14:20'));
$this->assertEquals(['07', '08', '09', '18'], $this->getEntryIdsForMinute('2023-09-13 12:14:00'));
$this->assertEquals(['07', '08', '09', '18'], $this->getEntryIdsForMinute('2023-09-13 12:14:25'));
$this->assertEquals(['22'], $this->getEntryIdsForMinute('2023-09-13 00:00:00'));
}
}

0 comments on commit b711c9c

Please sign in to comment.