From 7c7c10bd812a756e7e58c948f50d605766bca3a1 Mon Sep 17 00:00:00 2001 From: Ricus Date: Mon, 13 Sep 2021 08:05:28 +0200 Subject: [PATCH] Feature: Meta JSON column for scheduled notifications (Easier Querying) (#90) * migration for meta column * allow creating a scheduled notification with meta information * added static function findByMeta * return type set for findByMeta * trailing comma * remove blank line and add null coalesce * readme updated * style fixes * add changelog note to run migrations on version upgrade * styleci fixes * styleci fixes * styleci fixes * parameter type added for meta --- README.md | 66 ++++++++++++++++--- ...00_add_meta_to_scheduled_notifications.php | 32 +++++++++ src/Models/ScheduledNotification.php | 5 ++ src/ScheduledNotification.php | 43 +++++++++--- src/Traits/SnoozeNotifiable.php | 5 +- tests/ScheduledNotificationTest.php | 31 ++++++++- 6 files changed, 161 insertions(+), 21 deletions(-) create mode 100644 migrations/2021_09_10_130000_add_meta_to_scheduled_notifications.php diff --git a/README.md b/README.md index db3b4b4..5975072 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Laravel Snooze [![License](https://poser.pugx.org/thomasjohnkane/snooze/license)](https://packagist.org/packages/thomasjohnkane/snooze) ### Why use this package? -- Ever wanted to schedule a future notification to go out at a specific time? (was the delayed queue option not enough?) +- Ever wanted to schedule a future notification to go out at a specific time? (was the delayed queue option not enough?) - Want a simple on-boarding email drip? - How about happy birthday emails? @@ -44,7 +44,7 @@ php artisan vendor:publish --provider="Thomasjohnkane\Snooze\ServiceProvider" -- ## Usage #### Using the model trait -Snooze provides a trait for your model, similar to the standard `Notifiable` trait. +Snooze provides a trait for your model, similar to the standard `Notifiable` trait. It adds a `notifyAt()` method to your model to schedule notifications. ```php @@ -68,7 +68,7 @@ $user->notifyAt(new NewYearNotification, Carbon::parse('last day of this year')) ``` #### Using the ScheduledNotification::create helper -You can also use the `create` method on the `ScheduledNotification`. +You can also use the `create` method on the `ScheduledNotification`. ```php ScheduledNotification::create( Auth::user(), // Target @@ -92,7 +92,7 @@ ScheduledNotification::create( #### An important note about scheduling the `snooze:send` command -Creating a scheduled notification will add the notification to the database. It will be sent by running `snooze:send` command at (or after) the stored `sendAt` time. +Creating a scheduled notification will add the notification to the database. It will be sent by running `snooze:send` command at (or after) the stored `sendAt` time. The `snooze:send` command is scheduled to run every minute by default. You can change this value (`sendFrequency`) in the published config file. Available options are `everyMinute`, `everyFiveMinutes`, `everyTenMinutes`, `everyFifteenMinutes`, `everyThirtyMinutes`, `hourly`, and `daily`. @@ -100,15 +100,15 @@ The only thing you need to do is make sure `schedule:run` is also running. You c ### Setting the send tolerance -If your scheduler stops working, a backlog of scheduled notifications will build up. To prevent users receiving all of -the old scheduled notifications at once, the command will only send mail within the configured tolerance. +If your scheduler stops working, a backlog of scheduled notifications will build up. To prevent users receiving all of +the old scheduled notifications at once, the command will only send mail within the configured tolerance. By default this is set to 24 hours, so only mail scheduled to be sent within that window will be sent. This can be -configured (in seconds) using the `SCHEDULED_NOTIFICATION_SEND_TOLERANCE` environment variable or in the `snooze.php` config file. +configured (in seconds) using the `SCHEDULED_NOTIFICATION_SEND_TOLERANCE` environment variable or in the `snooze.php` config file. ### Setting the prune age The package can prune sent and cancelled messages that were sent/cancelled more than x days ago. You can -configure this using the `SCHEDULED_NOTIFICATION_PRUNE_AGE` environment variable or in the `snooze.php` config file +configure this using the `SCHEDULED_NOTIFICATION_PRUNE_AGE` environment variable or in the `snooze.php` config file (unit is days). This feature is turned off by default. #### Detailed Examples @@ -165,6 +165,53 @@ public function shouldInterrupt($notifiable) { If this method is not present on your notification, the notification will *not* be interrupted. Consider creating a shouldInterupt trait if you'd like to repeat conditional logic on groups of notifications. +**Scheduled Notification Meta Information** + +It's possible to store meta information on a scheduled notification, and then query the scheduled notifications by this meta information at a later stage. + +This functionality could be useful for when you store notifications for a future date, but some change in the system requires +you to update them. By using the meta column, it's possible to more easily query these scheduled notifications from the database by something else than +the notifiable. + +***Storing Meta Information*** + +Using the `ScheduledNotification::create` helper + +```php +ScheduledNotification::create( + $target, // Target + new ScheduledNotificationExample($order), // Notification + Carbon::now()->addDay(), // Send At, + ['foo' => 'bar'] // Meta Information +); +``` + +Using the `notifyAt` trait + +```php + $user->notifyAt(new BirthdayNotification, Carbon::parse($user->birthday), ['foo' => 'bar']); +``` + +***Retrieving Meta Information from Scheduled Notifications*** + +You can call the `getMeta` function on an existing scheduled notification to retrieve the meta information for the specific notification. + +Passing no parameters to this function will return the entire meta column in array form. + +Passing a string key (`getMeta('foo')`), will retrieve the specific key from the meta column. + +***Querying Scheduled Notifications using the ScheduledNotification::findByMeta helper*** + +It's possible to query the database for scheduled notifications with certain meta information, by using the `findByMeta` helper. + +```php + ScheduledNotification::findByMeta('foo', 'bar'); //key and value +``` + +The first parameter is the meta key, and the second parameter is the value to look for. + +>Note: The index column doesn't currently make use of a database index + **Conditionally turn off scheduler** If you would like to disable sending of scheduled notifications, set an env variable of `SCHEDULED_NOTIFICATIONS_DISABLED` to `true`. You will still be able to schedule notifications, and they will be sent once the scheduler is enabled. @@ -191,6 +238,9 @@ composer test If you discover any security related issues, please email instead of using the issue tracker. +## Changelog +Please be sure to run `php artisan migrate` when upgrading versions of this package. + ## Contributing 1. Fork it () diff --git a/migrations/2021_09_10_130000_add_meta_to_scheduled_notifications.php b/migrations/2021_09_10_130000_add_meta_to_scheduled_notifications.php new file mode 100644 index 0000000..70ef271 --- /dev/null +++ b/migrations/2021_09_10_130000_add_meta_to_scheduled_notifications.php @@ -0,0 +1,32 @@ +json('meta')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table(config('snooze.table'), function (Blueprint $table) { + $table->dropColumn('meta'); + }); + } +} diff --git a/src/Models/ScheduledNotification.php b/src/Models/ScheduledNotification.php index 92e7871..5c58246 100644 --- a/src/Models/ScheduledNotification.php +++ b/src/Models/ScheduledNotification.php @@ -36,6 +36,7 @@ class ScheduledNotification extends Model 'cancelled', 'created_at', 'updated_at', + 'meta', ]; protected $attributes = [ @@ -44,6 +45,10 @@ class ScheduledNotification extends Model 'cancelled_at' => null, ]; + protected $casts = [ + 'meta' => 'array', + ]; + public function __construct(array $attributes = []) { parent::__construct($attributes); diff --git a/src/ScheduledNotification.php b/src/ScheduledNotification.php index e8a36a7..35704a8 100644 --- a/src/ScheduledNotification.php +++ b/src/ScheduledNotification.php @@ -30,6 +30,7 @@ public function __construct(ScheduledNotificationModel $scheduleNotificationMode * @param object $notifiable * @param Notification $notification * @param DateTimeInterface $sendAt + * @param array $meta * @return self * * @throws SchedulingFailedException @@ -37,10 +38,12 @@ public function __construct(ScheduledNotificationModel $scheduleNotificationMode public static function create( object $notifiable, Notification $notification, - DateTimeInterface $sendAt + DateTimeInterface $sendAt, + array $meta = [] ): self { if ($sendAt <= Carbon::now()->subMinute()) { - throw new SchedulingFailedException(sprintf('`send_at` must not be in the past: %s', $sendAt->format(DATE_ISO8601))); + throw new SchedulingFailedException(sprintf('`send_at` must not be in the past: %s', + $sendAt->format(DATE_ISO8601))); } if (! method_exists($notifiable, 'notify')) { @@ -54,12 +57,13 @@ public static function create( $targetType = $notifiable instanceof AnonymousNotifiable ? AnonymousNotifiable::class : get_class($notifiable); return new self($modelClass::create([ - 'target_id' => $targetId, - 'target_type' => $targetType, + 'target_id' => $targetId, + 'target_type' => $targetType, 'notification_type' => get_class($notification), - 'target' => $serializer->serialize($notifiable), - 'notification' => $serializer->serialize($notification), - 'send_at' => $sendAt, + 'target' => $serializer->serialize($notifiable), + 'notification' => $serializer->serialize($notification), + 'send_at' => $sendAt, + 'meta' => $meta, ])); } @@ -99,6 +103,17 @@ public static function findByTarget(object $notifiable): ?Collection return self::collection($models); } + public static function findByMeta($key, $value): ?Collection + { + $modelClass = self::getScheduledNotificationModelClass(); + + $models = $modelClass::query() + ->where("meta->{$key}", $value) + ->get(); + + return self::collection($models); + } + public static function all(bool $includeSent = false, bool $includeCanceled = false): Collection { $modelClass = self::getScheduledNotificationModelClass(); @@ -143,7 +158,7 @@ public static function cancelAnonymousNotificationsByChannel(string $channel, st ->get() ->map(function (ScheduledNotificationModel $model) use ($serializer) { return [ - 'id' => $model->id, + 'id' => $model->id, 'routes' => $serializer->unserialize($model->target)->routes, ]; }) @@ -277,6 +292,18 @@ public function getUpdatedAt(): CarbonInterface return $this->scheduleNotificationModel->updated_at; } + /** + * @param null $key + */ + public function getMeta($key = null) + { + if (is_null($key)) { + return $this->scheduleNotificationModel->meta; + } else { + return $this->scheduleNotificationModel->meta[$key] ?? []; + } + } + /** * @return bool */ diff --git a/src/Traits/SnoozeNotifiable.php b/src/Traits/SnoozeNotifiable.php index 73c226d..2888679 100644 --- a/src/Traits/SnoozeNotifiable.php +++ b/src/Traits/SnoozeNotifiable.php @@ -14,12 +14,13 @@ trait SnoozeNotifiable /** * @param Notification $notification * @param DateTimeInterface $sendAt + * @param array $meta * @return ScheduledNotification * * @throws SchedulingFailedException */ - public function notifyAt($notification, DateTimeInterface $sendAt): ScheduledNotification + public function notifyAt($notification, DateTimeInterface $sendAt, array $meta = []): ScheduledNotification { - return ScheduledNotification::create($this, $notification, $sendAt); + return ScheduledNotification::create($this, $notification, $sendAt, $meta); } } diff --git a/tests/ScheduledNotificationTest.php b/tests/ScheduledNotificationTest.php index 3936eca..d41159d 100644 --- a/tests/ScheduledNotificationTest.php +++ b/tests/ScheduledNotificationTest.php @@ -35,6 +35,7 @@ public function testItRunsMigrations() 'cancelled_at', 'created_at', 'updated_at', + 'meta', ], $columns); } @@ -247,7 +248,8 @@ public function testNotificationsCanBeQueried() ScheduledNotification::create( $target, new TestNotification(User::find(2)), - Carbon::now()->addSeconds(10) + Carbon::now()->addSeconds(10), + ['foo' => 'baz'] ); ScheduledNotification::create( @@ -259,7 +261,8 @@ public function testNotificationsCanBeQueried() ScheduledNotification::create( $target, new TestNotification(User::find(2)), - Carbon::now()->addSeconds(60) + Carbon::now()->addSeconds(60), + ['foo' => 'bar'] ); ScheduledNotification::create( @@ -271,7 +274,8 @@ public function testNotificationsCanBeQueried() ScheduledNotification::create( $target, new TestNotificationTwo(User::find(2)), - Carbon::now()->addSeconds(60) + Carbon::now()->addSeconds(60), + ['foo' => 'bar'] ); $all = ScheduledNotification::all(); @@ -285,6 +289,9 @@ public function testNotificationsCanBeQueried() $this->assertSame(5, ScheduledNotification::findByTarget($target)->count()); + $this->assertSame(2, ScheduledNotification::findByMeta('foo', 'bar')->count()); + $this->assertSame(1, ScheduledNotification::findByMeta('foo', 'baz')->count()); + $all->first()->sendNow(); $allNotSent = ScheduledNotification::all(); @@ -304,4 +311,22 @@ public function testNotificationClassCanBeRetreived() $this->assertInstanceOf(TestNotification::class, $scheduled_notification->getNotification()); $this->assertEquals($scheduled_notification->getNotification()->newUser->email, $notification->newUser->email); } + + public function testItCanStoreAndRetrieveMetaInfo() + { + $target = User::find(1); + $notification = new TestNotification(User::find(2)); + + $meta = ['foo' => 'bar', 'hey' => 'you']; + + $scheduled_notification = ScheduledNotification::create($target, $notification, Carbon::now()->addSeconds(10), $meta); + $scheduled_notification_no_meta = ScheduledNotification::create($target, $notification, Carbon::now()->addSeconds(10)); + + $this->assertSame($meta, $scheduled_notification->getMeta()); + $this->assertSame([], $scheduled_notification_no_meta->getMeta()); + + $this->assertSame('bar', $scheduled_notification->getMeta('foo')); + $this->assertSame('you', $scheduled_notification->getMeta('hey')); + $this->assertSame([], $scheduled_notification->getMeta('doesnt_exist')); + } }