Skip to content

Commit

Permalink
Feature: Meta JSON column for scheduled notifications (Easier Queryin…
Browse files Browse the repository at this point in the history
…g) (#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
  • Loading branch information
ricuss authored Sep 13, 2021
1 parent b8d523c commit 7c7c10b
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 21 deletions.
66 changes: 58 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <b>future</b> notification to go out at a specific time? (was the delayed queue option not enough?)
- Ever wanted to schedule a <b>future</b> 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?

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -92,23 +92,23 @@ 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`.

The only thing you need to do is make sure `schedule:run` is also running. You can test this by running `php artisan schedule:run` in the console. [To make it run automatically, read here][6].

### 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
Expand Down Expand Up @@ -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.
Expand All @@ -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 (<https://github.com/thomasjohnkane/snooze/fork>)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddMetaToScheduledNotifications extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table(config('snooze.table'), function (Blueprint $table) {
$table->json('meta')->nullable();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table(config('snooze.table'), function (Blueprint $table) {
$table->dropColumn('meta');
});
}
}
5 changes: 5 additions & 0 deletions src/Models/ScheduledNotification.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class ScheduledNotification extends Model
'cancelled',
'created_at',
'updated_at',
'meta',
];

protected $attributes = [
Expand All @@ -44,6 +45,10 @@ class ScheduledNotification extends Model
'cancelled_at' => null,
];

protected $casts = [
'meta' => 'array',
];

public function __construct(array $attributes = [])
{
parent::__construct($attributes);
Expand Down
43 changes: 35 additions & 8 deletions src/ScheduledNotification.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,20 @@ public function __construct(ScheduledNotificationModel $scheduleNotificationMode
* @param object $notifiable
* @param Notification $notification
* @param DateTimeInterface $sendAt
* @param array $meta
* @return self
*
* @throws SchedulingFailedException
*/
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')) {
Expand All @@ -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,
]));
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
];
})
Expand Down Expand Up @@ -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
*/
Expand Down
5 changes: 3 additions & 2 deletions src/Traits/SnoozeNotifiable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
31 changes: 28 additions & 3 deletions tests/ScheduledNotificationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public function testItRunsMigrations()
'cancelled_at',
'created_at',
'updated_at',
'meta',
], $columns);
}

Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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();
Expand 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();
Expand 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'));
}
}

0 comments on commit 7c7c10b

Please sign in to comment.