├── .gitignore ├── stubs └── migrations │ └── AddDigestedToNotificationsTable.php.stub ├── composer.json ├── LICENSE ├── src ├── Commands │ └── GenerateDigestEmails.php ├── Notifications │ └── SimpleDigestifEmail.php ├── DigestifServiceProvider.php ├── config │ └── digestif.php └── Tasks │ └── Digestable.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /stubs/migrations/AddDigestedToNotificationsTable.php.stub: -------------------------------------------------------------------------------- 1 | dateTime('digested_at')->nullable(); 21 | }); 22 | 23 | } 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | if( Schema::hasTable('notifications') ) { 34 | 35 | Schema::table('notifications', function (Blueprint $table) { 36 | $table->dropColumn('digested_at'); 37 | }); 38 | 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codepotatoltd/digestif", 3 | "description": "A digest email for Laravel database notifications", 4 | "version": "0.2.0", 5 | "type": "library", 6 | "require": { 7 | "php": ">=7.4", 8 | "illuminate/console": "^6.0 || ^7.0 || ^8.0", 9 | "illuminate/support": "^6.0 || ^7.0 || ^8.0", 10 | "illuminate/bus": "^6.0 || ^7.0 || ^8.0", 11 | "illuminate/notifications": "^6.0 || ^7.0 || ^8.0", 12 | "illuminate/database": "^6.0 || ^7.0 || ^8.0" 13 | }, 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Gareth Thompson", 18 | "email": "gareth@codepotato.co.uk" 19 | } 20 | ], 21 | "minimum-stability": "dev", 22 | "autoload": { 23 | "psr-4": { 24 | "CodepotatoLtd\\Digestif\\": "src/" 25 | } 26 | }, 27 | "extra": { 28 | "laravel": { 29 | "providers": [ 30 | "CodepotatoLtd\\Digestif\\DigestifServiceProvider" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Codepotato Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Commands/GenerateDigestEmails.php: -------------------------------------------------------------------------------- 1 | error('Zoiks! Digestif is disabled'); 39 | return 0; 40 | } 41 | 42 | if( !Schema::hasTable('notifications') ){ 43 | $this->error('We can\'t see a notifications table. Perhaps try running "php artisan notifications:table"'); 44 | return 0; 45 | } 46 | 47 | $this->info('Pouring a small one to wet the whistle.'); 48 | 49 | $service = new Digestable($this); 50 | $service->run(); 51 | 52 | return 0; 53 | 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/Notifications/SimpleDigestifEmail.php: -------------------------------------------------------------------------------- 1 | notification_count = $notification_count; 24 | } 25 | 26 | /** 27 | * Get the notification's delivery channels. 28 | * 29 | * @param mixed $notifiable 30 | * @return array 31 | */ 32 | public function via($notifiable) 33 | { 34 | return ['mail']; 35 | } 36 | 37 | /** 38 | * Get the mail representation of the notification. 39 | * 40 | * @param mixed $notifiable 41 | * @return \Illuminate\Notifications\Messages\MailMessage 42 | */ 43 | public function toMail($notifiable) 44 | { 45 | return (new MailMessage) 46 | ->greeting('Hello!') 47 | ->line('We just thought you might like to know that you have ' . $this->notification_count . ' unread ' . Str::plural('notification', $this->notification_count) . ' in your account') 48 | ->action('View notifications', url('/')) 49 | ->line('Thank you for using our application!'); 50 | } 51 | } -------------------------------------------------------------------------------- /src/DigestifServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 15 | $this->registerCommands() 16 | ->offerPublishing(); 17 | } 18 | 19 | } 20 | 21 | /** 22 | * @return $this 23 | */ 24 | protected function registerCommands(): self 25 | { 26 | $this->commands([ 27 | GenerateDigestEmails::class, 28 | ]); 29 | 30 | return $this; 31 | } 32 | 33 | 34 | /** 35 | * @return $this 36 | */ 37 | protected function offerPublishing(): self 38 | { 39 | $this->publishes([ 40 | __DIR__.'/config/digestif.php' => config_path('digestif.php'), 41 | ], 'digestif-config'); 42 | 43 | $this->publishes([__DIR__ . '/Notifications/SimpleDigestifEmail.php' => app_path('Notifications/SimpleDigestifEmail.php')], 'digestif-notifications'); 44 | 45 | if ($this->app->runningInConsole()) { 46 | if (!class_exists('AddDigestedToNotificationsTable')) { 47 | $this->publishes([ 48 | __DIR__ . '/../stubs/migrations/AddDigestedToNotificationsTable.php.stub' => database_path('migrations/' . date('Y_m_d_His', time()) . '_add_digested_to_notifications_table.php'), 49 | ], 'digestif-migrations'); 50 | } 51 | } 52 | 53 | return $this; 54 | } 55 | 56 | 57 | public function register(){ 58 | $this->mergeConfigFrom( 59 | __DIR__ . '/config/digestif.php', 60 | 'digestif' 61 | ); 62 | } 63 | 64 | 65 | } -------------------------------------------------------------------------------- /src/config/digestif.php: -------------------------------------------------------------------------------- 1 | true, 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | Digestif email type 18 | |-------------------------------------------------------------------------- 19 | | 20 | | In the future there will be more than one email type that digestif can 21 | | send. For now leave this as 'simple' 22 | | 23 | */ 24 | 'type' => 'simple', 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Digestif user model class 29 | |-------------------------------------------------------------------------- 30 | | 31 | | Please set the model used for your users. Note, they must have the 32 | | notifiable trait. 33 | | 34 | */ 35 | 'user_model' => \User::class, 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Notifications table user ID column 40 | |-------------------------------------------------------------------------- 41 | | 42 | | Please tell us what column stores the user_id used for notifications 43 | | 44 | */ 45 | 'notifications_user_id_column' => 'notifiable_id', 46 | 47 | 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | Notifications read column 51 | |-------------------------------------------------------------------------- 52 | | 53 | | Please tell us what column stores the read toggle for notifications 54 | | 55 | */ 56 | 'read_column' => 'read_at', 57 | 58 | ]; -------------------------------------------------------------------------------- /src/Tasks/Digestable.php: -------------------------------------------------------------------------------- 1 | awaiting_digest = []; 27 | $this->artisan = $artisan; 28 | } 29 | 30 | /** 31 | * 32 | */ 33 | public function run() 34 | { 35 | // check to see if the table column exists to power our digests 36 | if (!$this->preFlightChecks()) { 37 | $this->artisan->error('Digestive requires a migrate, please'); 38 | } 39 | 40 | $user_column = \config('digestif.notifications_user_id_column', 'notifiable_id'); 41 | 42 | DB::table('notifications') 43 | ->orderBy('id') 44 | ->select('*') 45 | ->where( \config('digestif.read_column'), '=', null) 46 | ->where('digested_at', '=', null) 47 | ->chunk(100, function ($notifications) use ($user_column) { 48 | 49 | foreach ($notifications as $notification) { 50 | if (!isset($this->awaiting_digest[$notification->{$user_column}])) { 51 | $this->awaiting_digest[$notification->{$user_column}] = 1; 52 | DB::table('notifications')->where('id', $notification->id)->update(['digested_at' => now()]); 53 | continue; 54 | } 55 | $this->awaiting_digest[$notification->{$user_column}]++; 56 | DB::table('notifications')->where('id', $notification->id)->update(['digested_at' => now()]); 57 | } 58 | }); 59 | 60 | $this->artisan->info('Preparing a total of ' . count($this->awaiting_digest) . ' digestifs for your users'); 61 | 62 | if (count($this->awaiting_digest)) { 63 | foreach( $this->awaiting_digest as $key => $count ){ 64 | // process the notification for each user 65 | 66 | $model = \config('digestif.user_model'); 67 | $user = (app($model))::find($key); 68 | 69 | if( $user instanceof $model ) { 70 | $user->notify(new \App\Notifications\SimpleDigestifEmail($count)); 71 | $this->artisan->info('Poured a ' . $this->getDigestifName() . ' for '. $user->email ); 72 | } 73 | 74 | } 75 | $this->artisan->info('Hic. I think we\'ve had enough for right now. '); 76 | } else { 77 | $this->artisan->info('Alas, there was no need for a digestif. More for us! Hic!'); 78 | } 79 | 80 | } 81 | 82 | /** 83 | * @return bool 84 | */ 85 | final private function preFlightChecks() 86 | { 87 | return Schema::hasColumn('notifications', 'digested_at'); 88 | } 89 | 90 | /** 91 | * @return array|mixed 92 | */ 93 | final private function getDigestifName(){ 94 | return Arr::random($this->drinks); 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🥃 Digestif - Gloriously simple digest emails for Laravel 2 | 3 | Total Downloads 4 | Latest Stable Version 5 | License 6 | 7 | ## What is this? 8 | Using the database driver for laravel notifications is great, but then you need a way to communicate to your users if they've not logged in for a while. With Digestif we'll handle the digest emails for you. 9 | 10 | ## Installation instructions 11 | 1. Install using ```composer require codepotatoltd/digestif``` 12 | 2. Ensure you have a notifications table in your database, otherwise run ```php artisan notifications:table``` and then ```php artisan migrate``` 13 | 2. Publish Digestif's config, notification and database migration using ```php artisan vendor:publish``` and select the DigestifServiceProvider option. 14 | 4. Run ```php artisan migrate``` to add the "digested_at" column 15 | 5. Open the digestif.php config file and update the User model that your app uses. 16 | 6. Open your App/Console/Kernel.php and in the scheduler define how often you would like the digest process to run. E.g. ```$schedule->command('digestif:pour')->hourly();``` Alternatively, setup your own cron process to run ```php artisan digestif:pour``` when you would like emails to be sent. 17 | 7. Sit back and let Digestif handle the rest 🥃 18 | 19 | ## Config options 20 | Using the vendor:publish method you have full control of both the notification that we use behind the scenes. You can find this in your App/Notifications folder and you're welcome to tweak this to something that works better for you. 21 | 22 | For example: 23 | 24 | | Config Variable | Default | Description 25 | |---|---|---| 26 | | enabled | true (boolean) | Should Digestif run or not? | 27 | | type | simple (string) | What variety of email should it send. Only one option at the moment | 28 | | user_model | User::class | What model is used for users that should receive the digest email | 29 | | notifications_user_id_column | notifiable_id (string) | What column on the notifications table stores the user_id | 30 | | read_column | read_at | What column from the notifications table stores whether the notification has been read or not | 31 | 32 | 33 | ## Roadmap 34 | - [x] ~~Simple digest email~~ 35 | - [ ] Refactor to filter out user notifications only 36 | - [ ] Unsubscribing from the digest email 37 | - [ ] Itemised digest emails as well as our simple counter version 38 | - [ ] User controls to set the frequency of their digests 39 | 40 | 41 | ## Upgrade Guide 42 | 43 | ### To v0.20 from v0.10 44 | 45 | Add the following to your digestif.php config file: 46 | 47 | ``` 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | Notifications read column 51 | |-------------------------------------------------------------------------- 52 | | 53 | | Please tell us what column stores the read toggle for notifications 54 | | 55 | */ 56 | 'read_column' => 'read_at', 57 | ``` 58 | 59 | ### To v0.10 from v0.0.2 60 | 61 | Add the following to your digestif.php config file 62 | 63 | ```composer log 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | Notifications table user ID column 67 | |-------------------------------------------------------------------------- 68 | | 69 | | Please tell us what column stores the user_id used for notifications 70 | | 71 | */ 72 | 'notifications_user_id_column' => 'notifiable_id', 73 | ``` --------------------------------------------------------------------------------