diff --git a/composer.json b/composer.json index 9b8b6c42..d733fcd4 100644 --- a/composer.json +++ b/composer.json @@ -2,6 +2,9 @@ "name": "wordpress/wp-feature-notifications", "description": "Notifications for WordPress (Feature Plugin)", "type": "wordpress-plugin", + "require": { + "ext-json": "*" + }, "require-dev": { "phpunit/phpunit": "^9.6", "yoast/phpunit-polyfills": "1.0.5", diff --git a/composer.lock b/composer.lock index 2c531aa8..59baa6aa 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "52ca58825299b7ea26c4b7798bdb5730", + "content-hash": "e9a58530f931316fe8917362126f3ac4", "packages": [], "packages-dev": [ { @@ -1212,16 +1212,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.6", + "version": "9.6.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b65d59a059d3004a040c16a82e07bbdf6cfdd115" + "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b65d59a059d3004a040c16a82e07bbdf6cfdd115", - "reference": "b65d59a059d3004a040c16a82e07bbdf6cfdd115", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", + "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", "shasum": "" }, "require": { @@ -1295,7 +1295,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.6" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.7" }, "funding": [ { @@ -1311,7 +1311,7 @@ "type": "tidelift" } ], - "time": "2023-03-27T11:43:46+00:00" + "time": "2023-04-14T08:58:40+00:00" }, { "name": "psr/cache", @@ -4136,7 +4136,9 @@ "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, - "platform": [], + "platform": { + "ext-json": "*" + }, "platform-dev": [], "platform-overrides": { "php": "7.4" diff --git a/includes/helper/class-serde.php b/includes/helper/class-serde.php new file mode 100644 index 00000000..131d08a5 --- /dev/null +++ b/includes/helper/class-serde.php @@ -0,0 +1,60 @@ +format( DateTime::ATOM ); + } + + /** + * Maybe deserialize a datetime string in MySQL format. + * + * @param string|DateTime|null $date The possible MySQL datetime to deserialize. + * + * @return DateTime|null Maybe a DateTime object. + */ + public static function maybe_deserialize_mysql_date( $date ) { + if ( null === $date ) { + return null; + } + + if ( $date instanceof DateTime ) { + return $date; + } + + if ( is_string( $date ) ) { + $date = DateTime::createFromFormat( 'Y-m-d H:i:s', $date ); + + if ( false === $date ) { + $date = null; + } + } + + return $date; + } +} diff --git a/includes/model/class-channel.php b/includes/model/class-channel.php new file mode 100644 index 00000000..e7520391 --- /dev/null +++ b/includes/model/class-channel.php @@ -0,0 +1,141 @@ +name = $name; + $this->title = $title; + + // Optional properties + + $this->context = $context; + $this->icon = $icon; + $this->description = $description; + } + + /** + * Specifies data which should be serialized to JSON. + * + * @return array Data which can be serialized by json_encode, which is a + * value of any type other than a resource. + */ + public function jsonSerialize() { + return array( + 'context' => $this->context, + 'description' => $this->description, + 'icon' => $this->icon, + 'name' => $this->name, + 'title' => $this->title, + ); + } + + /** + * Get the namespaced name. + * + * @return ?string The namespaced name of the channel. + */ + public function get_name(): ?string { + return $this->name; + } + + /** + * Get the human-readable label. + * + * @return ?string The title of the channel. + */ + public function get_title(): ?string { + return $this->title; + } + + /** + * Get the default display context. + * + * @return ?string The context of the channel. + */ + public function get_context(): ?string { + return $this->context; + } + + /** + * Get the detailed description. + * + * @return ?string The description of the channel. + */ + public function get_description(): ?string { + return $this->description; + } + + /** + * Get the icon. + * + * @return ?string The icon of the channel. + */ + public function get_icon(): ?string { + return $this->icon; + } +} diff --git a/includes/model/class-message.php b/includes/model/class-message.php new file mode 100644 index 00000000..2143856e --- /dev/null +++ b/includes/model/class-message.php @@ -0,0 +1,314 @@ +message = $message; + + // Optional properties + + $this->accept_label = $accept_label; + $this->accept_link = $accept_link; + $this->channel_title = $channel_title; + $this->created_at = $created_at; + $this->dismiss_label = $dismiss_label; + $this->expires_at = $expires_at; + $this->icon = $icon; + $this->id = $id; + $this->is_dismissible = $is_dismissible; + $this->severity = $severity; + $this->title = $title; + } + + /** + * Specifies data which should be serialized to JSON. + * + * @return array Data which can be serialized by json_encode, which is a + * value of any type other than a resource. + */ + public function jsonSerialize() { + return array_merge( + $this->collect_meta(), + array( + 'created_at' => Helper\Serde::maybe_serialize_json_date( $this->created_at ), + 'expires_at' => Helper\Serde::maybe_serialize_json_date( $this->expires_at ), + 'id' => $this->id, + 'message' => $this->message, + 'title' => $this->title, + ) + ); + } + + /** + * Returns the JSON representation of the message metadata, or `false` if encoding fails. + * + * @return string|false + */ + public function encode_meta() { + return json_encode( $this->collect_meta() ); + } + + /** + * Get the title. + * + * @return ?string The title of the message. + */ + public function get_title(): ?string { + return $this->title; + } + + /** + * Get the content. + * + * @return ?string The content of the message. + */ + public function get_message(): ?string { + return $this->message; + } + + // Optional property getters + + /** + * Get the accept action label. + * + * @return ?string The accept action label of message. + */ + public function get_accept_label(): ?string { + return $this->accept_label; + } + + /** + * Get the accept action link. + * + * @return ?string The accept action link of the message. + */ + public function get_accept_link(): ?string { + return $this->accept_link; + } + + /** + * Get the created at datetime. + * + * @return ?DateTime The datetime at which the message was created. + */ + public function get_created_at(): ?DateTime { + return $this->created_at; + } + + /** + * Get whether the message is dismissible. + * + * @return ?bool The is dismissible property of the message. + */ + public function get_is_dismissible(): ?bool { + return $this->is_dismissible; + } + + /** + * Get the dismiss label. + * + * @return ?string The dismiss label of the message. + */ + public function get_dismiss_label(): ?string { + return $this->dismiss_label; + } + + /** + * Get the expires at datetime. + * + * @return ?DateTime The expires at datetime of the message. + */ + public function get_expires_at(): ?DateTime { + return $this->expires_at; + } + + /** + * Get the icon. + * + * @return ?string The icon of the message. + */ + public function get_icon(): ?string { + return $this->icon; + } + + /** + * Get the database ID. + * + * @return ?int The database ID of the message. + */ + public function get_id(): ?int { + return $this->id; + } + + /** + * Get the severity. + * + * @return ?string The severity of the message. + */ + public function get_severity(): ?string { + return $this->severity; + } + + /** + * Check whether the content of the message is valid. + */ + protected function validate_message() { + if ( ! is_string( $this->message ) ) { + return false; + } + + $length = mb_strlen( $this->message, '8bit' ); + + if ( $length > 255 ) { + return false; + } + + return true; + } + + /** + * Collect the message metadata values which are non null. + * + * @return array The metadata of the message. + */ + protected function collect_meta() { + $metadata = array(); + + foreach ( self::$meta_keys as $key ) { + if ( null !== $this->{ $key } ) { + $metadata[ $key ] = $this->{ $key }; + } + } + + return $metadata; + } +} diff --git a/includes/model/class-notification.php b/includes/model/class-notification.php new file mode 100644 index 00000000..9f76d5bd --- /dev/null +++ b/includes/model/class-notification.php @@ -0,0 +1,195 @@ +channel_name = $channel_name; + $this->message_id = $message_id; + $this->user_id = $user_id; + + // Optional properties + + $this->context = $context; + $this->created_at = $created_at; + $this->dismissed_at = $dismissed_at; + $this->displayed_at = $displayed_at; + $this->expires_at = $expires_at; + } + + /** + * Specifies data which should be serialized to JSON. + * + * @return array Data which can be serialized by json_encode, which is a + * value of any type other than a resource. + */ + public function jsonSerialize() { + return array( + 'channel_name' => $this->channel_name, + 'context' => $this->context, + 'created_at' => Helper\Serde::maybe_serialize_json_date( $this->created_at ), + 'dismissed_at' => Helper\Serde::maybe_serialize_json_date( $this->dismissed_at ), + 'displayed_at' => Helper\Serde::maybe_serialize_json_date( $this->displayed_at ), + 'expires_at' => Helper\Serde::maybe_serialize_json_date( $this->expires_at ), + 'message_id' => $this->message_id, + 'user_id' => $this->user_id, + ); + } + + /** + * Get the namespaced channel name. + * + * @return ?string The namespaced channel name of the notification. + */ + public function get_channel_name(): ?string { + return $this->channel_name; + } + + /** + * Get the display context. + * + * @return ?string The display context of the notification. + */ + public function get_context(): ?string { + return $this->context; + } + + /** + * Get the created at datetime. + * + * @return ?DateTime The datetime at which the notification was created. + */ + public function get_created_at(): ?DateTime { + return $this->created_at; + } + + /** + * Get the dismissed at datetime. + * + * @return ?DateTime The datetime at which the notification was dismissed. + */ + public function get_dismissed_at(): ?DateTime { + return $this->dismissed_at; + } + + /** + * Get the displayed at datetime. + * + * @return ?DateTime The datetime at which the notification was first displayed. + */ + public function get_displayed_at(): ?DateTime { + return $this->displayed_at; + } + + /** + * Get the expires at datetime. + * + * @return ?DateTime The datetime at which the notification expires. + */ + public function get_expires_at(): ?DateTime { + return $this->expires_at; + } + + /** + * Get the message ID. + * + * @return ?int The database ID of the message related to the notification. + */ + public function get_message_id(): ?int { + return $this->message_id; + } + + /** + * Get the user ID. + * + * @return ?int The database ID of the user the notification belongs to. + */ + public function get_user_id(): ?int { + return $this->user_id; + } +} diff --git a/includes/model/class-subscription.php b/includes/model/class-subscription.php new file mode 100644 index 00000000..0fa74122 --- /dev/null +++ b/includes/model/class-subscription.php @@ -0,0 +1,127 @@ +channel_name = $channel_name; + $this->user_id = $user_id; + + // Optional properties + + $this->created_at = $created_at; + $this->snoozed_until = $snoozed_until; + } + + /** + * Specifies data which should be serialized to JSON. + * + * @return array Data which can be serialized by json_encode, which is a + * value of any type other than a resource. + */ + public function jsonSerialize() { + return array( + 'channel_name' => $this->channel_name, + 'created_at' => Helper\Serde::maybe_serialize_json_date( $this->created_at ), + 'snoozed_until' => Helper\Serde::maybe_serialize_json_date( $this->snoozed_until ), + 'user_id' => $this->user_id, + ); + } + + /** + * Get the namespaced channel name. + * + * @return ?string The namespaced channel name of the subscription. + */ + public function get_channel_name(): ?string { + return $this->channel_name; + } + + /** + * Get the created at datetime. + * + * @return ?DateTime The datetime at which the subscription was created. + */ + public function get_created_at(): ?DateTime { + return $this->created_at; + } + + /** + * Get the snoozed until option. + * + * @return ?DateTime The snoozed until option of the subscription. + */ + public function get_snoozed_until(): ?DateTime { + return $this->snoozed_until; + } + + /** + * Get the user ID. + * + * @return ?int The user ID of the subscription. + */ + public function get_user_id(): ?int { + return $this->user_id; + } + +} diff --git a/tests/phpunit/tests/model/test-model-channel.php b/tests/phpunit/tests/model/test-model-channel.php new file mode 100644 index 00000000..6ac83916 --- /dev/null +++ b/tests/phpunit/tests/model/test-model-channel.php @@ -0,0 +1,53 @@ +model = new Model\Channel( + 'core/test', + 'Test channel', + 'test', + 'A test case channel', + 'WordPress' + ); + } + + /** + * Tear down each test method. + */ + public function tear_down() { + $this->model = null; + + parent::tear_down(); + } + + /** + * Should be JSON serializable. + */ + public function test_json_serializable() { + $actual = json_encode( $this->model, JSON_PRETTY_PRINT ); + $expected = '{ + "context": "test", + "description": "A test case channel", + "icon": "WordPress", + "name": "core\/test", + "title": "Test channel" +}'; + $this->assertEquals( $actual, $expected ); + } +} diff --git a/tests/phpunit/tests/model/test-model-message.php b/tests/phpunit/tests/model/test-model-message.php new file mode 100644 index 00000000..33057193 --- /dev/null +++ b/tests/phpunit/tests/model/test-model-message.php @@ -0,0 +1,66 @@ +model = new Model\Message( + 'Testing, testings... 1, 2, 3... testing', + 'Ok', + null, + 'Test channel', + null, + 'Nope', + null, + 'hammer', + null, + true, + 'warning', + 'Message model test' + ); + } + + /** + * Tear down each test method. + */ + public function tear_down() { + $this->model = null; + + parent::tear_down(); + } + + /** + * Should be JSON serializable. + */ + public function test_json_serializable() { + $actual = json_encode( $this->model, JSON_PRETTY_PRINT ); + $expected = '{ + "accept_label": "Ok", + "channel_title": "Test channel", + "dismiss_label": "Nope", + "icon": "hammer", + "is_dismissible": true, + "severity": "warning", + "created_at": null, + "expires_at": null, + "id": null, + "message": "Testing, testings... 1, 2, 3... testing", + "title": "Message model test" +}'; + $this->assertEquals( $actual, $expected ); + } +} diff --git a/tests/phpunit/tests/model/test-model-notification.php b/tests/phpunit/tests/model/test-model-notification.php new file mode 100644 index 00000000..2dd74871 --- /dev/null +++ b/tests/phpunit/tests/model/test-model-notification.php @@ -0,0 +1,60 @@ +model = new Model\Notification( + 'core/test', + 1, + 1, + 'adminbar', + null, + null, + null, + new DateTime( '2023-12-31' ) + ); + } + + /** + * Tear down each test method. + */ + public function tear_down() { + $this->model = null; + + parent::tear_down(); + } + + /** + * Should be JSON serializable. + */ + public function test_json_serializable() { + $actual = json_encode( $this->model, JSON_PRETTY_PRINT ); + $expected = '{ + "channel_name": "core\/test", + "context": "adminbar", + "created_at": null, + "dismissed_at": null, + "displayed_at": null, + "expires_at": "2023-12-31T00:00:00+00:00", + "message_id": 1, + "user_id": 1 +}'; + $this->assertEquals( $actual, $expected ); + } +} diff --git a/tests/phpunit/tests/model/test-model-subscription.php b/tests/phpunit/tests/model/test-model-subscription.php new file mode 100644 index 00000000..69ef1a48 --- /dev/null +++ b/tests/phpunit/tests/model/test-model-subscription.php @@ -0,0 +1,52 @@ +model = new Model\Subscription( + 'core/test', + 1, + null, + new DateTime( '2023-12-31' ) + ); + } + + /** + * Tear down each test method. + */ + public function tear_down() { + $this->model = null; + + parent::tear_down(); + } + + /** + * Should be JSON serializable. + */ + public function test_json_serializable() { + $actual = json_encode( $this->model, JSON_PRETTY_PRINT ); + $expected = '{ + "channel_name": "core\/test", + "created_at": null, + "snoozed_until": "2023-12-31T00:00:00+00:00", + "user_id": 1 +}'; + $this->assertEquals( $actual, $expected ); + } +} diff --git a/wp-feature-notifications.php b/wp-feature-notifications.php index 62d3f5fb..dde334ff 100644 --- a/wp-feature-notifications.php +++ b/wp-feature-notifications.php @@ -33,9 +33,14 @@ // Require interface/class declarations.. +require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/helper/class-serde.php'; require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/exceptions/interface-exception.php'; require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/exceptions/class-runtime-exception.php'; require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/interface-status.php'; +require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/model/class-channel.php'; +require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/model/class-message.php'; +require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/model/class-notification.php'; +require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/model/class-subscription.php'; require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/image/interface-image.php'; require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/image/class-base-image.php'; require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/persistence/interface-notification-repository.php';