diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..53e2019 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.1] + +### Added + +- Plugin manifest integration for automated configuration installation +- `manifest()` method to `NotificationPlugin` implementing `ManifestInterface` +- Automatic installation of `config/notification.php` configuration file +- Automatic installation of database migrations +- Automatic bootstrap file configuration loading +- GitHub star repository prompt support + +### Changed + +- Plugin now uses manifest system for configuration setup +- Configuration files are installed via `bin/cake manifest install --plugin Crustum/Notification` + +## [1.0.0] + +### Added + +- Multi-channel notification system supporting email, database, and extensible custom channels +- NotificationManager and NotificationSender for sending notifications via NotifiableBehavior or manager +- Database notification storage with migrations, entity, and table for displaying notifications in web interface +- Queue support with ShouldQueueInterface for async notification delivery via CakePHP Queue +- Testing utilities with NotificationTrait for comprehensive test assertions and custom channel registry diff --git a/composer.json b/composer.json index 2ead365..02bc2f7 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "cakephp/cakephp": "^5.1", "cakephp/authentication": "*", "cakephp/authorization": "*", - "cakephp/queue": "^2.2" + "cakephp/queue": "^2.2", + "crustum/plugin-manifest": "^1.0" }, "require-dev": { "phpunit/phpunit": "^10.5", diff --git a/config/notification.php.example b/config/notification.php.example new file mode 100644 index 0000000..89b6826 --- /dev/null +++ b/config/notification.php.example @@ -0,0 +1,24 @@ + [ + 'channels' => [ + 'database' => [ + 'className' => 'Crustum/Notification.Database', + ], + 'mail' => [ + 'className' => 'Crustum/Notification.Mail', + 'profile' => env('NOTIFICATION_MAIL_PROFILE', 'default'), + ], + ], + ], +]; + diff --git a/docs/index.md b/docs/index.md index c67ee1c..43db15d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,7 @@ # Notifications - [Introduction](#introduction) +- [Quickstart](#quickstart) - [Generating Notifications](#generating-notifications) - [Sending Notifications](#sending-notifications) - [Using the Notifiable Behavior](#using-the-notifiable-behavior) @@ -38,6 +39,34 @@ The CakePHP Notification plugin provides support for sending notifications acros Typically, notifications should be short, informational messages that notify users of something that occurred in your application. For example, if you are writing a billing application, you might send an "Invoice Paid" notification to your users via the email and SMS channels. + + +## Quickstart + +### Installing the Plugin + +Install via Composer: + +```bash +composer require crustum/notification +``` + +> [!NOTE] +> This plugin should be registered in your `config/plugins.php` file. + +```bash +bin/cake plugin load Crustum/Notification +``` + +> [!TIP] +> **After the plugin registers itself**, it's recommended to install the configuration with the manifest system: + +```bash +bin/cake manifest install --plugin Crustum/Notification +``` + +The Notification plugin will create the `config/notification.php` configuration file where you may register your application's notification channels. Additionally, it will copy the migrations to the application's migrations directory and append the loading of the `config/notification.php` file to the `config/bootstrap.php` file. + ## Generating Notifications diff --git a/src/Channel/DatabaseChannel.php b/src/Channel/DatabaseChannel.php index 533b395..c6d433f 100644 --- a/src/Channel/DatabaseChannel.php +++ b/src/Channel/DatabaseChannel.php @@ -55,7 +55,13 @@ public function send(EntityInterface|AnonymousNotifiable $notifiable, Notificati $entity = $notificationsTable->newEntity($this->buildPayload($notifiable, $notification)); - return $notificationsTable->save($entity); + $saved = $notificationsTable->save($entity); + + if ($saved && $saved->id && $saved->id !== $notification->getId()) { + $notification->setId($saved->id); + } + + return $saved; } /** diff --git a/src/NotificationPlugin.php b/src/NotificationPlugin.php index 5752d37..250be18 100644 --- a/src/NotificationPlugin.php +++ b/src/NotificationPlugin.php @@ -7,12 +7,18 @@ use Cake\Core\BasePlugin; use Cake\Core\PluginApplicationInterface; use Crustum\Notification\Command\NotificationCommand; +use Crustum\PluginManifest\Manifest\ManifestInterface; +use Crustum\PluginManifest\Manifest\ManifestTrait; /** * Plugin for Notification + * + * @uses \Crustum\PluginManifest\Manifest\ManifestTrait */ -class NotificationPlugin extends BasePlugin +class NotificationPlugin extends BasePlugin implements ManifestInterface { + use ManifestTrait; + /** * Load all the plugin configuration and bootstrap logic. * @@ -39,4 +45,30 @@ public function console(CommandCollection $commands): CommandCollection return $commands; } + + /** + * Get the manifest for the plugin. + * + * @return array> + */ + public static function manifest(): array + { + $pluginPath = dirname(__DIR__); + + return array_merge( + static::manifestMigrations( + $pluginPath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'Migrations', + ), + static::manifestConfig( + $pluginPath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'notification.php.example', + CONFIG . 'notification.php', + false, + ), + static::manifestBootstrapAppend( + "if (file_exists(CONFIG . 'notification.php')) {\n Configure::load('notification', 'default');\n}", + '// Notification Plugin Configuration', + ), + static::manifestStarRepo('Crustum/Notification'), + ); + } } diff --git a/src/NotificationSender.php b/src/NotificationSender.php index 26bccc2..ca918e9 100644 --- a/src/NotificationSender.php +++ b/src/NotificationSender.php @@ -11,6 +11,7 @@ use Cake\Utility\Inflector; use Cake\Utility\Text; use Crustum\Notification\Job\SendQueuedNotificationJob; +use Crustum\Notification\Model\Entity\Notification as NotificationEntity; use Throwable; /** @@ -91,9 +92,15 @@ public function sendNow(EntityInterface|AnonymousNotifiable|iterable $notifiable $this->preferredLocale($notifiable, $notification), function () use ($viaChannels, $notifiable, $original): void { $notificationId = Text::uuid(); + $actualNotificationId = $notificationId; foreach ((array)$viaChannels as $channel) { - $this->sendToNotifiable($notifiable, $notificationId, clone $original, $channel); + $notificationClone = clone $original; + $response = $this->sendToNotifiable($notifiable, $actualNotificationId, $notificationClone, $channel); + + if ($channel === 'database' && $response instanceof NotificationEntity && $response->id) { + $actualNotificationId = $response->id; + } } }, ); @@ -107,16 +114,16 @@ function () use ($viaChannels, $notifiable, $original): void { * @param string $id Unique notification ID * @param \Crustum\Notification\Notification $notification The notification instance * @param string $channel The channel name - * @return void + * @return \Crustum\Notification\Model\Entity\Notification|null The saved notification entity, or null if not saved */ - protected function sendToNotifiable(EntityInterface|AnonymousNotifiable $notifiable, string $id, Notification $notification, string $channel): void + protected function sendToNotifiable(EntityInterface|AnonymousNotifiable $notifiable, string $id, Notification $notification, string $channel): mixed { if (!$notification->getId()) { $notification->setId($id); } if (!$this->shouldSendNotification($notifiable, $notification, $channel)) { - return; + return null; } try { @@ -129,6 +136,8 @@ protected function sendToNotifiable(EntityInterface|AnonymousNotifiable $notifia 'channel' => $channel, 'response' => $response, ],); + + return $response; } catch (Throwable $exception) { $this->dispatchEvent('Model.Notification.failed', [ 'notifiable' => $notifiable,