From 33f6353a8d85c20744d1450f41d6187a4b092a76 Mon Sep 17 00:00:00 2001
From: John Hooks <bitmachina@outlook.com>
Date: Wed, 19 Apr 2023 15:50:34 -0700
Subject: [PATCH 1/4] fix: clean php includes of dead code

---
 includes/class-aggregate-factory.php          |  89 --------
 includes/class-base-notification.php          | 199 ------------------
 includes/class-factory.php                    |  98 ---------
 .../class-failed-to-add-recipient.php         |  23 --
 .../exceptions/class-invalid-recipient.php    |  23 --
 includes/exceptions/class-invalid-type.php    |  32 ---
 includes/image/interface-image.php            |   2 +-
 includes/interface-json-unserializable.php    |  15 --
 includes/interface-notification.php           |   2 +-
 includes/interface-status.php                 |   9 +-
 includes/messages/class-aggregate-factory.php |  20 --
 includes/messages/class-base-factory.php      |  62 ------
 includes/messages/class-base-message.php      |  63 ------
 includes/messages/interface-factory.php       |  26 ---
 includes/messages/interface-message.php       |  26 ---
 .../class-wpdb-notification-repository.php    |  23 --
 .../interface-notification-repository.php     |  20 --
 .../recipients/class-aggregate-factory.php    |  20 --
 includes/recipients/class-base-factory.php    |  99 ---------
 includes/recipients/class-collection.php      | 134 ------------
 includes/recipients/class-role.php            |  16 --
 includes/recipients/class-user.php            |  38 ----
 includes/recipients/interface-factory.php     |  25 ---
 includes/recipients/interface-recipient.php   |   7 -
 includes/senders/class-base-sender.php        |  92 --------
 includes/senders/interface-factory.php        |  15 --
 includes/senders/interface-sender.php         |   9 -
 tests/phpunit/includes/bootstrap.php          |   1 -
 .../phpunit/includes/class-dummy-message.php  |  25 ---
 tests/phpunit/tests/test-base-message.php     |  28 ---
 .../phpunit/tests/test-base-notification.php  |  43 ----
 tests/phpunit/tests/test-factory.php          |  41 ----
 .../tests/test-notification-repository.php    |  27 ---
 .../tests/test-recipient-collection.php       |  97 ---------
 tests/phpunit/tests/test-role-recipient.php   |  18 --
 tests/phpunit/tests/test-user-recipient.php   |  42 ----
 wp-feature-notifications.php                  |  22 +-
 37 files changed, 9 insertions(+), 1522 deletions(-)
 delete mode 100644 includes/class-aggregate-factory.php
 delete mode 100644 includes/class-base-notification.php
 delete mode 100644 includes/class-factory.php
 delete mode 100644 includes/exceptions/class-failed-to-add-recipient.php
 delete mode 100644 includes/exceptions/class-invalid-recipient.php
 delete mode 100644 includes/exceptions/class-invalid-type.php
 delete mode 100644 includes/interface-json-unserializable.php
 delete mode 100644 includes/messages/class-aggregate-factory.php
 delete mode 100644 includes/messages/class-base-factory.php
 delete mode 100644 includes/messages/class-base-message.php
 delete mode 100644 includes/messages/interface-factory.php
 delete mode 100644 includes/messages/interface-message.php
 delete mode 100644 includes/recipients/class-aggregate-factory.php
 delete mode 100644 includes/recipients/class-base-factory.php
 delete mode 100644 includes/recipients/class-collection.php
 delete mode 100644 includes/recipients/class-role.php
 delete mode 100644 includes/recipients/class-user.php
 delete mode 100644 includes/recipients/interface-factory.php
 delete mode 100644 includes/recipients/interface-recipient.php
 delete mode 100644 includes/senders/class-base-sender.php
 delete mode 100644 includes/senders/interface-factory.php
 delete mode 100644 includes/senders/interface-sender.php
 delete mode 100644 tests/phpunit/includes/class-dummy-message.php
 delete mode 100644 tests/phpunit/tests/test-base-message.php
 delete mode 100644 tests/phpunit/tests/test-base-notification.php
 delete mode 100644 tests/phpunit/tests/test-factory.php
 delete mode 100644 tests/phpunit/tests/test-notification-repository.php
 delete mode 100644 tests/phpunit/tests/test-recipient-collection.php
 delete mode 100644 tests/phpunit/tests/test-role-recipient.php
 delete mode 100644 tests/phpunit/tests/test-user-recipient.php

diff --git a/includes/class-aggregate-factory.php b/includes/class-aggregate-factory.php
deleted file mode 100644
index 8a23d541..00000000
--- a/includes/class-aggregate-factory.php
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-
-namespace WP\Notifications;
-
-abstract class Aggregate_Factory {
-
-	/**
-	 * Array of factories that this aggregate factory represents.
-	 *
-	 * @var array
-	 */
-	private $factories;
-
-	/**
-	 * Instantiate an aggregate factory.
-	 *
-	 * @param array $factories Array of factories that the aggregate factory
-	 *                         wraps.
-	 */
-	public function __construct( $factories = array() ) {
-		if ( ! is_array( $factories ) ) {
-			$factories = array( $factories );
-		}
-
-		foreach ( $factories as $factory ) {
-			$this->register( $factory );
-		}
-	}
-
-	/**
-	 * Register a new factory implementation with the aggregate factory.
-	 *
-	 * @param object $factory Factory to register.
-	 */
-	public function register( $factory ) {
-		if ( ! is_a( $factory, $this->get_interface() ) ) {
-			// TODO: Throw exception.
-		}
-
-		$factory_class = get_class( $factory );
-
-		if ( ! array_key_exists( $factory_class, $this->factories ) ) {
-			$this->factories[ $factory_class ] = $factory;
-		}
-	}
-
-	/**
-	 * Create a new instance of the interface the factory represents.
-	 *
-	 * @param mixed  $value Value to use for creation.
-	 * @param string $type  Optional. Type to use for creation.
-	 *
-	 * @return mixed
-	 */
-	public function create( $value, $type = null ) {
-		foreach ( $this->factories as $factory ) {
-			if ( $factory->accepts( $type ) ) {
-				return $factory->create( $value, $type );
-			}
-		}
-
-		// TODO: Throw exception.
-	}
-
-	/**
-	 * Whether the factory accepts a given type for instantiation.
-	 *
-	 * @param string $type Type that should be instantiated.
-	 *
-	 * @return bool Whether the factory accepts the given type.
-	 */
-	public function accepts( $type ) {
-		foreach ( $this->factories as $factory ) {
-			if ( $factory->accepts( $type ) ) {
-				return true;
-			}
-		}
-
-		return false;
-	}
-
-	/**
-	 * Get the interface that this aggregate factory can instantiate
-	 * implementations of.
-	 *
-	 * @return string Class name of the interface.
-	 */
-	abstract protected function get_interface();
-}
diff --git a/includes/class-base-notification.php b/includes/class-base-notification.php
deleted file mode 100644
index a797345f..00000000
--- a/includes/class-base-notification.php
+++ /dev/null
@@ -1,199 +0,0 @@
-<?php
-
-namespace WP\Notifications;
-
-use ReflectionClass;
-use WP\Notifications\Messages;
-use WP\Notifications\Recipients;
-use WP\Notifications\Senders;
-
-class Base_Notification implements Notification {
-
-	/**
-	 * ID of the notification.
-	 *
-	 * @var int
-	 */
-	protected $id;
-
-	/**
-	 * Sender of the notification.
-	 *
-	 * @var Senders\Sender
-	 */
-	protected $sender;
-
-	/**
-	 * Timestamp of when the notification was triggered.
-	 *
-	 * @var int
-	 */
-	protected $timestamp;
-
-	/**
-	 * Collection of notification recipients.
-	 *
-	 * @var Recipients\Collection
-	 */
-	protected $recipients;
-
-	/**
-	 * Notification message.
-	 *
-	 * @var Messages\Message
-	 */
-	protected $message;
-
-	/**
-	 * Notification status.
-	 *
-	 * @var string
-	 */
-	protected $status = Status::UNREAD;
-
-	/**
-	 * Instantiates a Base_Notification object.
-	 *
-	 * @param Senders\Sender        $sender     Sender that sent the notification.
-	 * @param Recipients\Collection $recipients Recipients that should receive the notification.
-	 * @param Messages\Message      $message    Message of the notification.
-	 * @param mixed                 $timestamp  Optional. Timestamp of when the notification was
-	 *                                          triggered. Defaults to the moment of instantiation.
-	 * @param int                   $id         Optional. ID of the notification. Defaults to -1.
-	 */
-	public function __construct(
-		Senders\Sender $sender,
-		Recipients\Collection $recipients,
-		Messages\Message $message,
-		$timestamp = null,
-		$id = - 1
-	) {
-		$this->sender     = $sender;
-		$this->recipients = $recipients;
-		$this->message    = $message;
-		$this->timestamp  = $this->validate_timestamp( $timestamp );
-		$this->id         = $id;
-	}
-
-	/**
-	 * Get the ID of the notification.
-	 *
-	 * @return int ID of the notification, -1 if none was attributed yet.
-	 */
-	public function get_id() {
-		return $this->id;
-	}
-
-	/**
-	 * Get the timestamp of the notification.
-	 *
-	 * @return int Timestamp of the notification.
-	 */
-	public function get_timestamp() {
-		return $this->timestamp;
-	}
-
-	/**
-	 * Validate a timestamp.
-	 *
-	 * @param mixed $timestamp Timestamp to validate.
-	 *
-	 * @return int Validated timestamp.
-	 */
-	protected function validate_timestamp( $timestamp ) {
-		if ( null === $timestamp ) {
-			$timestamp = time();
-		}
-
-		return $timestamp;
-	}
-
-	/**
-	 * Get the sender of the notification.
-	 *
-	 * @return Senders\Sender Sender of the notification.
-	 */
-	public function get_sender() {
-		return $this->sender;
-	}
-
-	/**
-	 * Gets the recipients for the notification.
-	 *
-	 * @return Recipients\Collection Notification recipients.
-	 */
-	public function get_recipients() {
-		return $this->recipients;
-	}
-
-	/**
-	 * Gets the message for the notification.
-	 *
-	 * @return Messages\Message Notification message.
-	 */
-	public function get_message() {
-		return $this->message;
-	}
-
-	/**
-	 * Get the current status of the notification.
-	 *
-	 * @return string Status of the notification.
-	 */
-	public function get_status() {
-		return $this->status;
-	}
-
-	/**
-	 * Set the status of the current notification.
-	 *
-	 * @param string $status Status to set the notification to.
-	 *
-	 * @return $this
-	 */
-	public function set_status( $status ) {
-		$this->status = $status;
-
-		return $this;
-	}
-
-	/**
-	 * Specifies data which should be serialized to JSON.
-	 *
-	 * @return mixed Data which can be serialized by json_encode, which is a
-	 *               value of any type other than a resource.
-	 */
-	public function jsonSerialize() {
-		return array(
-			get_class( $this->recipients ) => $this->recipients,
-			get_class( $this->message )    => $this->message,
-			get_class( $this->sender )     => $this->sender,
-		);
-	}
-
-	/**
-	 * Creates a new instance from JSON-encoded data.
-	 *
-	 * @param string $json JSON-encoded data to create the instance from.
-	 *
-	 * @return self
-	 */
-	public static function json_unserialize( $json ) {
-		$data = json_decode( $json );
-
-		reset( $data );
-		$recipients_class = key( $data );
-		$recipients       = new $recipients_class( current( $data ) );
-
-		next( $data );
-		$message_class = key( $data );
-		$message       = new $message_class( current( $data ) );
-
-		next( $data );
-		$sender_class      = key( $data );
-		$sender_reflection = new ReflectionClass( $sender_class );
-		$sender            = $sender_reflection->newInstanceArgs( array_values( (array) current( $data ) ) );
-
-		return new self( $sender, $recipients, $message );
-	}
-}
diff --git a/includes/class-factory.php b/includes/class-factory.php
deleted file mode 100644
index 8774731c..00000000
--- a/includes/class-factory.php
+++ /dev/null
@@ -1,98 +0,0 @@
-<?php
-
-namespace WP\Notifications;
-
-use WP\Notifications\Messages;
-use WP\Notifications\Recipients;
-use WP\Notifications\Senders;
-
-class Factory {
-
-	const MESSAGE    = 'message';
-	const RECIPIENTS = 'recipients';
-
-	/**
-	 * Message factory implementation to use.
-	 *
-	 * @var Messages\Factory
-	 */
-	private $message_factory;
-
-	/**
-	 * Recipient factory implementation to use.
-	 *
-	 * @var Recipients\Factory
-	 */
-	private $recipient_factory;
-
-	/**
-	 * Sender factory implementation to use
-	 *
-	 * @var Senders\Factory
-	 */
-	private $sender_factory;
-
-	public function __construct(
-		Messages\Factory $message_factory,
-		Recipients\Factory $recipient_factory,
-		Senders\Factory $sender_factory
-	) {
-		$this->message_factory   = $message_factory;
-		$this->recipient_factory = $recipient_factory;
-		$this->sender_factory    = $sender_factory;
-	}
-
-	/**
-	 * Create a notification instance
-	 *
-	 * @param $args
-	 *
-	 * @return Base_Notification
-	 */
-	public function create( $args ) {
-
-		list( $message_args, $recipients_args, $sender_args ) = $this->validate( $args );
-
-		$sender     = $this->sender_factory->create( $sender_args );
-		$recipients = new Recipients\Collection();
-		$message    = $this->message_factory->create( $message_args );
-
-		foreach ( $recipients_args as $type => $value ) {
-			$recipients->add(
-				$this->recipient_factory->create( $value, $type )
-			);
-		}
-
-		return new Base_Notification( $sender, $recipients, $message );
-	}
-
-	private function validate( $args ) {
-		// TODO: Validate args.
-		if ( is_string( $args ) ) {
-			$args = json_decode( $args );
-		}
-
-		list( $message_args, $recipients_args, $sender_args ) = $args;
-
-		return array(
-			$this->get_message_args( $message_args ),
-			$this->get_recipients_args( $recipients_args ),
-			$this->get_sender_args( $sender_args ),
-		);
-	}
-
-	private function get_message_args( $args ) {
-		// TODO: Parse message args.
-		return $args;
-	}
-
-	private function get_recipients_args( $args ) {
-		// TODO: Parse recipients args.
-		return $args;
-	}
-
-	public function get_sender_args( $args ) {
-		// TODO: Parse sender args.
-		return $args;
-	}
-}
diff --git a/includes/exceptions/class-failed-to-add-recipient.php b/includes/exceptions/class-failed-to-add-recipient.php
deleted file mode 100644
index 11cc466b..00000000
--- a/includes/exceptions/class-failed-to-add-recipient.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-namespace WP\Notifications\Exceptions;
-
-class Failed_To_Add_Recipient extends Runtime_Exception {
-
-	public static function from_invalid_recipient(
-		$recipient
-	) {
-		$type = is_object( $recipient ) ? get_class( $recipient ) : gettype( $recipient );
-
-		$message = sprintf(
-			/* translators: "%s" is a type of recipient. */
-			__(
-				'Failed to add invalid recipient type "%s" to recipient collection, only implementations of interface "Recipient" allowed.',
-				'wp-notification-center'
-			),
-			$type
-		);
-
-		return new self( $message );
-	}
-}
diff --git a/includes/exceptions/class-invalid-recipient.php b/includes/exceptions/class-invalid-recipient.php
deleted file mode 100644
index 7975f157..00000000
--- a/includes/exceptions/class-invalid-recipient.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-namespace WP\Notifications\Exceptions;
-
-class Invalid_Recipient extends Runtime_Exception {
-
-	public static function from_invalid_user_id( $user_id ) {
-		$type  = is_object( $user_id ) ? get_class( $user_id ) : gettype( $user_id );
-		$value = is_numeric( $user_id ) ? (int) $user_id : '<non-numeric>';
-
-		$message = sprintf(
-			/* translators: "%1$s" is a type of recipient and "%2$s" is recipient's name or identifier. */
-			__(
-				'Notification user recipient of type "%1$s" and value "%2$s" did not validate as a valid user ID.',
-				'wp-notification-center'
-			),
-			$type,
-			$value
-		);
-
-		return new self( $message );
-	}
-}
diff --git a/includes/exceptions/class-invalid-type.php b/includes/exceptions/class-invalid-type.php
deleted file mode 100644
index aa442229..00000000
--- a/includes/exceptions/class-invalid-type.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-namespace WP\Notifications\Exceptions;
-
-class Invalid_Type extends Runtime_Exception {
-
-	public static function from_message_type( $type ) {
-		$message = sprintf(
-			/* translators: "%1$s" is a type of message. */
-			__(
-				'Could not create notification message for the invalid message type "%1$s".',
-				'wp-notification-center'
-			),
-			$type
-		);
-
-		return new self( $message );
-	}
-
-	public static function from_recipient_type( $type ) {
-		$message = sprintf(
-			/* translators: "%1$s" is a type of recipient. */
-			__(
-				'Could not create notification recipient for the invalid recipient type "%1$s".',
-				'wp-notification-center'
-			),
-			$type
-		);
-
-		return new self( $message );
-	}
-}
diff --git a/includes/image/interface-image.php b/includes/image/interface-image.php
index 78d450ef..38c24c97 100644
--- a/includes/image/interface-image.php
+++ b/includes/image/interface-image.php
@@ -6,6 +6,6 @@
 
 use WP\Notifications;
 
-interface Image extends JsonSerializable, Notifications\Json_Unserializable {
+interface Image extends JsonSerializable {
 
 }
diff --git a/includes/interface-json-unserializable.php b/includes/interface-json-unserializable.php
deleted file mode 100644
index 8a02f3e6..00000000
--- a/includes/interface-json-unserializable.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace WP\Notifications;
-
-interface Json_Unserializable {
-
-	/**
-	 * Creates a new instance from JSON-encoded data.
-	 *
-	 * @param string $json JSON-encoded data to create the instance from.
-	 *
-	 * @return self
-	 */
-	public static function json_unserialize( $json );
-}
diff --git a/includes/interface-notification.php b/includes/interface-notification.php
index 354bfe42..51798b70 100644
--- a/includes/interface-notification.php
+++ b/includes/interface-notification.php
@@ -4,7 +4,7 @@
 
 use JsonSerializable;
 
-interface Notification extends JsonSerializable, Json_Unserializable {
+interface Notification extends JsonSerializable {
 
 	/**
 	 * Get the ID of the notification.
diff --git a/includes/interface-status.php b/includes/interface-status.php
index 27caaef6..c78f2c57 100644
--- a/includes/interface-status.php
+++ b/includes/interface-status.php
@@ -2,8 +2,11 @@
 
 namespace WP\Notifications;
 
-interface Status {
+interface Status_Interface {
+
+	const NEW         = 'new';
+	const UNDISPLAYED = 'undisplayed';
+	const DISPLAYED   = 'displayed';
+	const DISMISSED   = 'dismissed';
 
-	const UNREAD = 'unread';
-	const READ   = 'read';
 }
diff --git a/includes/messages/class-aggregate-factory.php b/includes/messages/class-aggregate-factory.php
deleted file mode 100644
index 557d6980..00000000
--- a/includes/messages/class-aggregate-factory.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-namespace WP\Notifications\Messages;
-
-use WP\Notifications;
-
-final class Aggregate_Factory
-	extends Notifications\Aggregate_Factory
-	implements Factory {
-
-	/**
-	 * Get the interface that this aggregate factory can instantiate
-	 * implementations of.
-	 *
-	 * @return string Class name of the interface.
-	 */
-	protected function get_interface() {
-		return '\WP\Notifications\Messages\Factory';
-	}
-}
diff --git a/includes/messages/class-base-factory.php b/includes/messages/class-base-factory.php
deleted file mode 100644
index 5fc699d1..00000000
--- a/includes/messages/class-base-factory.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-
-namespace WP\Notifications\Messages;
-
-use WP\Notifications\Exceptions;
-
-class Base_Factory implements Factory {
-
-	const TYPE_STANDARD = 'standard';
-
-	/**
-	 * Create a new instance of a notification message.
-	 *
-	 * @param mixed  $value Value of the message.
-	 * @param string $type  Optional. Type of the message. Defaults to
-	 *                      'standard'.
-	 *
-	 * @return Message
-	 *
-	 * @throws Invalid_Type If the message type was not valid.
-	 */
-	public function create( $value, $type = 'standard' ) {
-		if ( ! $this->accepts( $type ) ) {
-			throw Exceptions\Invalid_Type::from_message_type( $type );
-		}
-
-		list( $type, $value ) = $this->validate( $type, $value );
-
-		switch ( $type ) {
-			case self::TYPE_STANDARD:
-			default:
-				return new Base_Message( $value );
-		}
-	}
-
-	/**
-	 * Whether the factory accepts a given type for instantiation.
-	 *
-	 * @param string $type Type that should be instantiated.
-	 *
-	 * @return bool Whether the factory accepts the given type.
-	 */
-	public function accepts( $type ) {
-		$accepted_types = array( self::TYPE_STANDARD );
-
-		return in_array( $type, $accepted_types, true );
-	}
-
-	/**
-	 * Validate provided arguments.
-	 *
-	 * @param string $type  Type of the message to create.
-	 * @param mixed  $value Value of the message to create.
-	 *
-	 * @return array
-	 */
-	private function validate( $type, $value ) {
-		// TODO: Validate arguments.
-
-		return array( $type, $value );
-	}
-}
diff --git a/includes/messages/class-base-message.php b/includes/messages/class-base-message.php
deleted file mode 100644
index 2300b3e3..00000000
--- a/includes/messages/class-base-message.php
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-namespace WP\Notifications\Messages;
-
-class Base_Message implements Message {
-
-	/**
-	 * Message content.
-	 *
-	 * @var string
-	 */
-	protected $message;
-
-	/**
-	 * Instantiates a BaseMessage object.
-	 *
-	 * @param string $message Message content.
-	 */
-	public function __construct( $message ) {
-		$this->message = $message;
-	}
-
-	/**
-	 * Get the message content.
-	 *
-	 * @return string Message content.
-	 */
-	public function get_content() {
-		return (string) $this->message;
-	}
-
-	/**
-	 * Convert the message object into a string representing its content.
-	 *
-	 * @return string Message content as a string.
-	 */
-	public function __toString() {
-		return $this->get_content();
-	}
-
-	/**
-	 * Specifies data which should be serialized to JSON.
-	 *
-	 * @return mixed Data which can be serialized by json_encode, which is a
-	 *               value of any type other than a resource.
-	 */
-	public function jsonSerialize() {
-		return $this->message;
-	}
-
-	/**
-	 * Creates a new instance from JSON-encoded data.
-	 *
-	 * @param string $json JSON-encoded data to create the instance from.
-	 *
-	 * @return self
-	 */
-	public static function json_unserialize( $json ) {
-		$message = json_decode( $json );
-
-		return new self( $message );
-	}
-}
diff --git a/includes/messages/interface-factory.php b/includes/messages/interface-factory.php
deleted file mode 100644
index 1e6aaa10..00000000
--- a/includes/messages/interface-factory.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-namespace WP\Notifications\Messages;
-
-interface Factory {
-
-	/**
-	 * Create a new instance of a notification message.
-	 *
-	 * @param mixed  $value Value of the message.
-	 * @param string $type  Optional. Type of the message. Defaults to
-	 *                      'standard'.
-	 *
-	 * @return Message
-	 */
-	public function create( $value, $type = 'standard' );
-
-	/**
-	 * Whether the factory accepts a given type for instantiation.
-	 *
-	 * @param string $type Type that should be instantiated.
-	 *
-	 * @return bool Whether the factory accepts the given type.
-	 */
-	public function accepts( $type );
-}
diff --git a/includes/messages/interface-message.php b/includes/messages/interface-message.php
deleted file mode 100644
index 4e886032..00000000
--- a/includes/messages/interface-message.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-namespace WP\Notifications\Messages;
-
-use JsonSerializable;
-
-use WP\Notifications;
-
-interface Message
-	extends JsonSerializable,
-	Notifications\Json_Unserializable {
-
-	/**
-	 * Get the message content.
-	 *
-	 * @return string Message content.
-	 */
-	public function get_content();
-
-	/**
-	 * Convert the message object into a string representing its content.
-	 *
-	 * @return string Message content as a string.
-	 */
-	public function __toString();
-}
diff --git a/includes/persistence/class-wpdb-notification-repository.php b/includes/persistence/class-wpdb-notification-repository.php
index 67ac44c4..d22fc8fa 100644
--- a/includes/persistence/class-wpdb-notification-repository.php
+++ b/includes/persistence/class-wpdb-notification-repository.php
@@ -5,7 +5,6 @@
 use DateTimeInterface;
 
 use WP\Notifications;
-use WP\Notifications\Recipients;
 
 class Wpdb_Notification_Repository
 	extends Abstract_Notification_Repository {
@@ -27,28 +26,6 @@ public function find_by_id( $id ) {
 		return false;
 	}
 
-	/**
-	 * Find all notifications for a given recipient.
-	 *
-	 * @param Recipients\Recipient $recipient  Recipient to retrieve the
-	 *                                         notifications for.
-	 * @param int                  $pagination Optional. Number of elements per
-	 *                                         page. Defaults to 10.
-	 * @param int                  $offset     Optional. Offset into the result
-	 *                                         set. Defaults to 0.
-	 *
-	 * @return Notifications\Notification[] Array of notifications, empty array if
-	 *                                      none found.
-	 */
-	public function find_by_recipient(
-		Recipients\Recipient $recipient,
-		$pagination = 10,
-		$offset = 0
-	) {
-		// TODO: Implement query.
-		return array();
-	}
-
 	/**
 	 * Find all notifications for a given date period.
 	 *
diff --git a/includes/persistence/interface-notification-repository.php b/includes/persistence/interface-notification-repository.php
index 5188e225..9ab7e314 100644
--- a/includes/persistence/interface-notification-repository.php
+++ b/includes/persistence/interface-notification-repository.php
@@ -6,7 +6,6 @@
 use DateTimeInterface;
 
 use WP\Notifications;
-use WP\Notifications\Recipients;
 
 
 interface Notification_Repository {
@@ -21,25 +20,6 @@ interface Notification_Repository {
 	 */
 	public function find_by_id( $id );
 
-	/**
-	 * Find all notifications for a given recipient.
-	 *
-	 * @param Recipients\Recipient $recipient  Recipient to retrieve the
-	 *                                         notifications for.
-	 * @param int                  $pagination Optional. Number of elements per
-	 *                                         page. Defaults to 10.
-	 * @param int                  $offset     Optional. Offset into the result
-	 *                                         set. Defaults to 0.
-	 *
-	 * @return Notifications\Notification[] Array of notifications, empty array if
-	 *                                      none found.
-	 */
-	public function find_by_recipient(
-		Recipients\Recipient $recipient,
-		$pagination = 10,
-		$offset = 0
-	);
-
 	/**
 	 * Find all notifications for a given date period.
 	 *
diff --git a/includes/recipients/class-aggregate-factory.php b/includes/recipients/class-aggregate-factory.php
deleted file mode 100644
index 33dd1733..00000000
--- a/includes/recipients/class-aggregate-factory.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-namespace WP\Notifications\Recipients;
-
-use WP\Notifications;
-
-final class Aggregate_Factory
-	extends Notifications\Aggregate_Factory
-	implements Factory {
-
-	/**
-	 * Get the interface that this aggregate factory can instantiate
-	 * implementations of.
-	 *
-	 * @return string Class name of the interface.
-	 */
-	protected function get_interface() {
-		return 'WP\Notifications\Recipients\Factory';
-	}
-}
diff --git a/includes/recipients/class-base-factory.php b/includes/recipients/class-base-factory.php
deleted file mode 100644
index ba987a0f..00000000
--- a/includes/recipients/class-base-factory.php
+++ /dev/null
@@ -1,99 +0,0 @@
-<?php
-
-namespace WP\Notifications\Recipients;
-
-use WP\Notifications\Exceptions;
-
-final class Base_Factory implements Factory {
-
-	const TYPE_USER = 'user';
-	const TYPE_ROLE = 'role';
-
-	/**
-	 * Create a new instance of a notification recipient.
-	 *
-	 * @param mixed  $value Value of the recipient.
-	 * @param string $type  Optional. Type of the recipient. Defaults to 'user'.
-	 *
-	 * @return Recipient The created recipient instance.
-	 * @throws Exceptions\Invalid_Type If the recipient type was not valid.
-	 */
-	public function create( $value, $type = self::TYPE_USER ) {
-		if ( ! $this->accepts( $type ) ) {
-			throw Exceptions\Invalid_Type::from_recipient_type( $type );
-		}
-
-		list( $type, $value ) = $this->validate( $type, $value );
-
-		$class = $this->get_implementation_for_type( $type );
-
-		return new $class( $value );
-	}
-
-	/**
-	 * Whether the factory accepts a given type for instantiation.
-	 *
-	 * @param string $type Type that should be instantiated.
-	 *
-	 * @return bool Whether the factory accepts the given type.
-	 */
-	public function accepts( $type ) {
-		return in_array( $type, $this->get_accepted_types(), true );
-	}
-
-	/**
-	 * Get the corresponding implementation class for a given type.
-	 *
-	 * @param string $type Type to get the implementation class for.
-	 *
-	 * @return string Implementation class.
-	 * @throws Exceptions\Invalid_Type If the recipient type was not valid.
-	 */
-	public function get_implementation_for_type( $type ) {
-		if ( ! $this->accepts( $type ) ) {
-			throw Exceptions\Invalid_Type::from_recipient_type( $type );
-		}
-
-		$mappings = $this->get_type_mappings();
-
-		return $mappings[ $type ];
-	}
-
-	/**
-	 * Validate provided arguments.
-	 *
-	 * @param string $type  Type of the recipient to create.
-	 * @param mixed  $value Value of the recipient to create.
-	 *
-	 * @return array Validated arguments.
-	 */
-	private function validate( $type, $value ) {
-		// TODO: Validate arguments.
-
-		return array( $type, $value );
-	}
-
-	/**
-	 * Get an array of type to class mappings.
-	 *
-	 * @return array
-	 */
-	private function get_type_mappings() {
-		return apply_filters(
-			'wp_feature_notifications_recipient_type_mappings',
-			array(
-				self::TYPE_USER => 'User_Recipient',
-				self::TYPE_ROLE => 'Role_Recipient',
-			)
-		);
-	}
-
-	/**
-	 * Get an array of accepted type identifiers.
-	 *
-	 * @return array
-	 */
-	private function get_accepted_types() {
-		return array_keys( $this->get_type_mappings() );
-	}
-}
diff --git a/includes/recipients/class-collection.php b/includes/recipients/class-collection.php
deleted file mode 100644
index d5ef6c4b..00000000
--- a/includes/recipients/class-collection.php
+++ /dev/null
@@ -1,134 +0,0 @@
-<?php
-
-namespace WP\Notifications\Recipients;
-
-use Iterator;
-use Countable;
-use JsonSerializable;
-
-use WP\Notifications;
-use WP\Notifications\Exceptions;
-
-class Collection
-	implements Iterator,
-		Countable,
-		JsonSerializable,
-		Notifications\Json_Unserializable {
-
-	/**
-	 * Internal array of recipients.
-	 *
-	 * @var Recipient[]
-	 */
-	protected $recipients = array();
-
-	/**
-	 * Instantiates a Collection object.
-	 *
-	 * @param array $recipients Array of recipients to instantiate the
-	 *                          collection with.
-	 */
-	public function __construct( $recipients = array() ) {
-		$this->recipients = $this->validate_recipients( $recipients );
-	}
-
-	/**
-	 * Validate the recipients.
-	 *
-	 * @param Recipient|array $recipients Recipient or array of
-	 *                                    recipients to validate.
-	 *
-	 * @return array Validated array of recipients.
-	 * @throws Failed_To_Add_Recipient If a recipient could not be added.
-	 */
-	protected function validate_recipients( $recipients ) {
-		if ( ! is_array( $recipients ) ) {
-			$recipients = array( $recipients );
-		}
-
-		foreach ( $recipients as $recipient ) {
-			if ( ! $recipient instanceof Recipient ) {
-				throw Exceptions\Failed_To_Add_Recipient::from_invalid_recipient( $recipient );
-			}
-		}
-
-		return $recipients;
-	}
-
-	public function add( Recipient $recipient ) {
-		$this->recipients[] = $recipient;
-	}
-
-	public function count(): int {
-		return count( $this->recipients );
-	}
-
-	/**
-	 * Return the current recipient.
-	 *
-	 * @return Recipient Recipient
-	 */
-	public function current() {
-		return current( $this->recipients );
-	}
-
-	/**
-	 * Move forward to next recipient
-	 *
-	 * @return void Any returned value is ignored.
-	 */
-	public function next() {
-		next( $this->recipients );
-	}
-
-	/**
-	 * Return the key of the current recipient
-	 *
-	 * @return mixed scalar on success, or null on failure.
-	 */
-	public function key() {
-		return key( $this->recipients );
-	}
-
-	/**
-	 * Checks if current position is valid
-	 *
-	 * @return boolean The return value will be casted to boolean and then
-	 *                 evaluated. Returns true on success or false on failure.
-	 */
-	public function valid() {
-		// TODO: Implement valid() method.
-	}
-
-	/**
-	 * Rewind the Iterator to the first recipient.
-	 *
-	 * @return void Any returned value is ignored.
-	 */
-	public function rewind() {
-		reset( $this->recipients );
-	}
-
-	/**
-	 * Specifies data which should be serialized to JSON.
-	 *
-	 * @return mixed Data which can be serialized by json_encode, which is a
-	 *               value of any type other than a resource.
-	 */
-	public function jsonSerialize() {
-		return $this->recipients;
-	}
-
-	/**
-	 * Creates a new instance from JSON-encoded data.
-	 *
-	 * @param string $json JSON-encoded data to create the instance from.
-	 *
-	 * @return self
-	 */
-	public static function json_unserialize( $json ) {
-		$recipients = json_decode( $json );
-
-		return new self( $recipients );
-	}
-}
diff --git a/includes/recipients/class-role.php b/includes/recipients/class-role.php
deleted file mode 100644
index 78273cd3..00000000
--- a/includes/recipients/class-role.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-namespace WP\Notifications\Recipients;
-
-final class Role implements Recipient {
-
-	private $role;
-
-	public function __construct( $role ) {
-		$this->role = $role;
-	}
-
-	public function get_role() {
-		return $this->role;
-	}
-}
diff --git a/includes/recipients/class-user.php b/includes/recipients/class-user.php
deleted file mode 100644
index 41c31c78..00000000
--- a/includes/recipients/class-user.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-namespace WP\Notifications\Recipients;
-
-use WP_User;
-
-use WP\Notifications\Exceptions\Invalid_Recipient;
-
-final class User implements Recipient {
-
-	private $user_id;
-	private $user_object;
-
-	public function __construct( $user_id ) {
-		$this->user_id = $this->validate( $user_id );
-	}
-
-	public function get_user_id() {
-		return $this->user_id;
-	}
-
-	public function get_user_object() {
-		if ( null === $this->user_object ) {
-			$this->user_object = new WP_User( $this->user_id );
-		}
-
-		return $this->user_object;
-	}
-
-	private function validate( $user_id ) {
-		if ( ! is_numeric( $user_id )
-			|| ! ( ( (int) $user_id ) > 0 ) ) {
-			throw Invalid_Recipient::from_invalid_user_id( $user_id );
-		}
-
-		return $user_id;
-	}
-}
diff --git a/includes/recipients/interface-factory.php b/includes/recipients/interface-factory.php
deleted file mode 100644
index 378e1f8d..00000000
--- a/includes/recipients/interface-factory.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-namespace WP\Notifications\Recipients;
-
-interface Factory {
-
-	/**
-	 * Create a new instance of a notification recipient.
-	 *
-	 * @param mixed  $value Value of the recipient.
-	 * @param string $type  Optional. Type of the recipient. Defaults to 'user'.
-	 *
-	 * @return Recipient
-	 */
-	public function create( $value, $type = 'user' );
-
-	/**
-	 * Whether the factory accepts a given type for instantiation.
-	 *
-	 * @param string $type Type that should be instantiated.
-	 *
-	 * @return bool Whether the factory accepts the given type.
-	 */
-	public function accepts( $type );
-}
diff --git a/includes/recipients/interface-recipient.php b/includes/recipients/interface-recipient.php
deleted file mode 100644
index 529c8633..00000000
--- a/includes/recipients/interface-recipient.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-namespace WP\Notifications\Recipients;
-
-interface Recipient {
-
-}
diff --git a/includes/senders/class-base-sender.php b/includes/senders/class-base-sender.php
deleted file mode 100644
index a650cc13..00000000
--- a/includes/senders/class-base-sender.php
+++ /dev/null
@@ -1,92 +0,0 @@
-<?php
-
-namespace WP\Notifications\Senders;
-
-use WP\Notifications;
-
-use ReflectionClass;
-
-class Base_Sender implements Sender, Notifications\Json_Unserializable {
-
-	/**
-	 * @var string
-	 */
-	protected $name;
-
-	/**
-	 * @var Base_Image
-	 */
-	protected $image;
-
-	/**
-	 * BaseSender constructor.
-	 *
-	 * @param string          $name
-	 * @param Base_Image|null $image
-	 */
-	public function __construct( $name, $image = null ) {
-		$this->name  = $name;
-		$this->image = $image;
-	}
-
-	/**
-	 * @return array
-	 */
-	public function jsonSerialize() {
-
-		$data = array(
-			'name' => $this->name,
-		);
-
-		if ( $this->image ) {
-			$data[ get_class( $this->image ) ] = $this->image;
-		}
-
-		return $data;
-	}
-
-	/**
-	 * Creates a new instance from JSON-encoded data.
-	 *
-	 * @param string $json JSON-encoded data to create the instance from.
-	 *
-	 * @return self
-	 */
-	public static function json_unserialize( $json ) {
-
-		$data = json_decode( $json, true );
-
-		reset( $data );
-		$name = current( $data );
-
-		next( $data );
-		$class_name = key( $data );
-		$image_data = current( $data );
-
-		$image = null;
-
-		if ( ! empty( $image_data ) && is_subclass_of( $class_name, 'Image' ) ) {
-			$image_reflection = new ReflectionClass( $class_name );
-			$image            = $image_reflection->newInstanceArgs( array_values( $image_data ) );
-		}
-
-		return new self( $name, $image );
-	}
-
-	/**
-	 * Gets the name of the sender
-	 *
-	 * @return string
-	 */
-	public function get_name() {
-
-		return $this->name;
-	}
-
-	/**
-	 * @return Base_Image|null
-	 */
-	public function get_image() {
-		return $this->image;
-	}
-}
diff --git a/includes/senders/interface-factory.php b/includes/senders/interface-factory.php
deleted file mode 100644
index 8391844b..00000000
--- a/includes/senders/interface-factory.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace WP\Notifications\Senders;
-
-interface Factory {
-
-	/**
-	 * Create a new instance of notification sender
-	 *
-	 * @param string $name
-	 *
-	 * @return Sender
-	 */
-	public function create( $name );
-}
diff --git a/includes/senders/interface-sender.php b/includes/senders/interface-sender.php
deleted file mode 100644
index 60522dc6..00000000
--- a/includes/senders/interface-sender.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-namespace WP\Notifications\Senders;
-
-use JsonSerializable;
-
-interface Sender extends JsonSerializable {
-
-}
diff --git a/tests/phpunit/includes/bootstrap.php b/tests/phpunit/includes/bootstrap.php
index c0249c57..ca61bab7 100644
--- a/tests/phpunit/includes/bootstrap.php
+++ b/tests/phpunit/includes/bootstrap.php
@@ -38,5 +38,4 @@ function handle_wp_setup_failure( $message ) {
 
 remove_filter( 'wp_die_handler', 'handle_wp_setup_failure' );
 
-require dirname( __FILE__ ) . '/class-dummy-message.php';
 require dirname( __FILE__ ) . '/class-test-case.php';
diff --git a/tests/phpunit/includes/class-dummy-message.php b/tests/phpunit/includes/class-dummy-message.php
deleted file mode 100644
index ac719d90..00000000
--- a/tests/phpunit/includes/class-dummy-message.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-namespace WP\Notifications\Tests;
-
-use WP\Notifications\Messages;
-
-class Dummy_Message implements Messages\Message {
-
-	public function serialize() {
-		return ''; }
-
-	public function unserialize( $serialized ) { }
-
-	public function jsonSerialize() {
-		return ''; }
-
-	public static function json_unserialize( $json ) {
-		return new self; }
-
-	public function get_content() {
-		return ''; }
-
-	public function __toString() {
-		return ''; }
-}
diff --git a/tests/phpunit/tests/test-base-message.php b/tests/phpunit/tests/test-base-message.php
deleted file mode 100644
index 6811b5fc..00000000
--- a/tests/phpunit/tests/test-base-message.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-namespace WP\Notifications\Tests;
-
-use WP\Notifications\Messages;
-
-class Test_Base_Message extends TestCase {
-
-	public function test_it_can_be_instantiated() {
-		$testee = new Messages\Base_Message( 'Message' );
-		$this->assertInstanceOf( '\WP\Notifications\Messages\Base_Message', $testee );
-	}
-
-	public function test_it_implements_the_interface() {
-		$testee = new Messages\Base_Message( 'Message' );
-		$this->assertInstanceOf( '\WP\Notifications\Messages\Message', $testee );
-	}
-
-	public function test_it_can_return_its_content() {
-		$testee = new Messages\Base_Message( 'Message' );
-		$this->assertEquals( 'Message', $testee->get_content() );
-	}
-
-	public function test_it_can_be_cast_to_string() {
-		$testee = new Messages\Base_Message( 'Message' );
-		$this->assertEquals( 'Message', (string) $testee );
-	}
-}
diff --git a/tests/phpunit/tests/test-base-notification.php b/tests/phpunit/tests/test-base-notification.php
deleted file mode 100644
index cb509f1c..00000000
--- a/tests/phpunit/tests/test-base-notification.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-namespace WP\Notifications\Tests;
-
-use WP\Notifications;
-
-class Test_Base_Notification extends TestCase {
-
-	public function test_it_can_be_instantiated() {
-		$sender_mock     = $this->createMock( '\WP\Notifications\Senders\Base_Sender' );
-		$recipients_mock = $this->createMock( '\WP\Notifications\Recipients\Collection' );
-		$testee          = new Notifications\Base_Notification(
-			$sender_mock,
-			$recipients_mock,
-			new Dummy_Message()
-		);
-		$this->assertInstanceOf( '\WP\Notifications\Base_Notification', $testee );
-	}
-
-	public function test_it_implements_the_interface() {
-		$sender_mock     = $this->createMock( '\WP\Notifications\Senders\Base_Sender' );
-		$recipients_mock = $this->createMock( '\WP\Notifications\Recipients\Collection' );
-		$testee          = new Notifications\Base_Notification(
-			$sender_mock,
-			$recipients_mock,
-			new Dummy_Message()
-		);
-		$this->assertInstanceOf( '\WP\Notifications\Notification', $testee );
-	}
-
-	public function test_it_can_return_its_content() {
-		$sender_mock     = $this->createMock( '\WP\Notifications\Senders\Base_Sender' );
-		$recipients_mock = $this->createMock( '\WP\Notifications\Recipients\Collection' );
-		$dummy_message   = new Dummy_Message();
-		$testee          = new Notifications\Base_Notification(
-			$sender_mock,
-			$recipients_mock,
-			$dummy_message
-		);
-		$this->assertEquals( $recipients_mock, $testee->get_recipients() );
-		$this->assertEquals( $dummy_message, $testee->get_message() );
-	}
-}
diff --git a/tests/phpunit/tests/test-factory.php b/tests/phpunit/tests/test-factory.php
deleted file mode 100644
index 61caaf75..00000000
--- a/tests/phpunit/tests/test-factory.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-namespace WP\Notifications\Tests;
-
-use WP\Notifications;
-
-class Test_Factory extends TestCase {
-
-	public function test_create_with_vendor_sender() {
-
-		$vendor_sender = $this->createMock( '\WP\Notifications\Senders\Sender' );
-
-		$message_factory = $this->getMockBuilder( '\WP\Notifications\Messages\Factory' )
-								->setMethods( array( 'create', 'accepts' ) )
-								->getMock();
-
-		$message_factory->method( 'create' )->willReturn( $this->createMock( '\WP\Notifications\Messages\Message' ) );
-		$message_factory->method( 'accepts' )->willReturn( true );
-
-		$sender_factory = $this->getMockBuilder( '\WP\Notifications\Senders\Factory' )
-			->setMethods( array( 'create' ) )
-			->getMock();
-
-		$sender_factory->method( 'create' )->willReturn( $this->createMock( '\WP\Notifications\Senders\Sender' ) );
-
-		$factory = new Notifications\Factory(
-			$message_factory,
-			$this->createMock( '\WP\Notifications\Recipients\Factory' ),
-			$sender_factory
-		);
-
-		$args = array(
-			array(),
-			array(),
-			array(),
-		);
-
-		$notification = $factory->create( $args );
-		$this->assertEquals( $vendor_sender, $notification->get_sender() );
-	}
-}
diff --git a/tests/phpunit/tests/test-notification-repository.php b/tests/phpunit/tests/test-notification-repository.php
deleted file mode 100644
index 19dc9e89..00000000
--- a/tests/phpunit/tests/test-notification-repository.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-namespace WP\Notifications\Tests;
-
-use stdClass;
-
-use WP\Notifications\Persistence;
-
-class Test_Wpdb_Notification_Repository extends TestCase {
-
-	/** @dataProvider data_provider_it_returns_false_on_invalid_ids */
-	public function test_it_returns_false_on_invalid_ids( $id ) {
-		$testee = new Persistence\Wpdb_Notification_Repository();
-		$result = $testee->find_by_id( $id );
-		$this->assertFalse( $result );
-	}
-
-	public function data_provider_it_returns_false_on_invalid_ids() {
-		return array(
-			array( - 1 ),
-			array( 0 ),
-			array( 'nonsense' ),
-			array( array() ),
-			array( new stdClass() ),
-		);
-	}
-}
diff --git a/tests/phpunit/tests/test-recipient-collection.php b/tests/phpunit/tests/test-recipient-collection.php
deleted file mode 100644
index a28423b4..00000000
--- a/tests/phpunit/tests/test-recipient-collection.php
+++ /dev/null
@@ -1,97 +0,0 @@
-<?php
-
-namespace WP\Notifications\Tests;
-
-use stdClass;
-
-use WP\Notifications\Recipients;
-
-class Test_Recipient_Collection extends TestCase {
-
-	public function test_it_can_be_instantiated() {
-		$testee = new Recipients\Collection();
-		$this->assertInstanceOf( '\WP\Notifications\Recipients\Collection', $testee );
-	}
-
-	public function test_it_is_countable() {
-		$testee = new Recipients\Collection();
-		$this->assertInstanceOf( 'Countable', $testee );
-	}
-
-	public function test_it_is_traversable() {
-		$testee = new Recipients\Collection();
-		$this->assertInstanceOf( 'Traversable', $testee );
-	}
-
-	public function test_it_is_empty_when_instantiated_without_arguments() {
-		$testee = new Recipients\Collection();
-		$this->assertCount( 0, $testee );
-	}
-
-	public function test_it_can_accept_a_singular_recipient() {
-		$mock_recipient = $this->createMock( '\WP\Notifications\Recipients\Recipient' );
-		$testee         = new Recipients\Collection( $mock_recipient );
-		$this->assertCount( 1, $testee );
-	}
-
-	public function test_it_can_accept_an_array_of_recipients() {
-		$mock_recipient_1 = $this->createMock( '\WP\Notifications\Recipients\Recipient' );
-		$mock_recipient_2 = $this->createMock( '\WP\Notifications\Recipients\Recipient' );
-		$mock_recipient_3 = $this->createMock( '\WP\Notifications\Recipients\Recipient' );
-		$testee           = new Recipients\Collection(
-			array(
-				$mock_recipient_1,
-				$mock_recipient_2,
-				$mock_recipient_3,
-			)
-		);
-		$this->assertCount( 3, $testee );
-	}
-
-	public function test_recipients_can_be_added() {
-		$mock_recipient_1 = $this->createMock( '\WP\Notifications\Recipients\Recipient' );
-		$mock_recipient_2 = $this->createMock( '\WP\Notifications\Recipients\Recipient' );
-		$mock_recipient_3 = $this->createMock( '\WP\Notifications\Recipients\Recipient' );
-		$testee           = new Recipients\Collection( $mock_recipient_1 );
-		$this->assertCount( 1, $testee );
-		$testee->add( $mock_recipient_2 );
-		$this->assertCount( 2, $testee );
-		$testee->add( $mock_recipient_3 );
-		$this->assertCount( 3, $testee );
-	}
-
-	/** @dataProvider data_provider_it_throws_on_invalid_type */
-	public function test_it_throws_on_invalid_type( $invalid_recipient ) {
-		$this->expectException( '\WP\Notifications\Exceptions\Runtime_Exception' );
-		new Recipients\Collection( $invalid_recipient );
-	}
-
-	public function data_provider_it_throws_on_invalid_type() {
-		$mock_recipient_1 = $this->createMock( '\WP\Notifications\Recipients\Recipient' );
-		$mock_recipient_2 = $this->createMock( '\WP\Notifications\Recipients\Recipient' );
-
-		return array(
-			array( null ),
-			array( true ),
-			array( new stdClass ),
-			array( 'invalid' ),
-			array( array( 1, 2, 3 ) ),
-			array( array( $mock_recipient_1, 'invalid', $mock_recipient_2 ) ),
-		);
-	}
-
-	public function test_it_can_be_json_encoded() {
-		$testee = new Recipients\Collection( array() );
-		$this->assertEquals(
-			'[]',
-			json_encode( $testee )
-		);
-	}
-
-	public function test_it_can_be_instantiated_from_json() {
-		$json   = '[]';
-		$testee = Recipients\Collection::json_unserialize( $json );
-		$this->assertInstanceOf( '\WP\Notifications\Recipients\Collection', $testee );
-		$this->assertEquals( 0, $testee->count() );
-	}
-}
diff --git a/tests/phpunit/tests/test-role-recipient.php b/tests/phpunit/tests/test-role-recipient.php
deleted file mode 100644
index d92a6092..00000000
--- a/tests/phpunit/tests/test-role-recipient.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-namespace WP\Notifications\Tests;
-
-use WP\Notifications\Recipients;
-
-class Test_Role_Recipient extends TestCase {
-
-	public function test_it_can_be_instantiated() {
-		$testee = new Recipients\Role( 1 );
-		$this->assertInstanceOf( '\WP\Notifications\Recipients\Role', $testee );
-	}
-
-	public function test_it_implements_the_interface() {
-		$testee = new Recipients\Role( 1 );
-		$this->assertInstanceOf( '\WP\Notifications\Recipients\Recipient', $testee );
-	}
-}
diff --git a/tests/phpunit/tests/test-user-recipient.php b/tests/phpunit/tests/test-user-recipient.php
deleted file mode 100644
index 60ed2d72..00000000
--- a/tests/phpunit/tests/test-user-recipient.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-namespace WP\Notifications\Tests;
-
-use stdClass;
-
-use WP\Notifications\Recipients;
-
-class Test_User_Recipient extends TestCase {
-
-	public function test_it_can_be_instantiated() {
-		$testee = new Recipients\User( 1 );
-		$this->assertInstanceOf( '\WP\Notifications\Recipients\User', $testee );
-	}
-
-	public function test_it_implements_the_recipient_interface() {
-		$testee = new Recipients\User( 1 );
-		$this->assertInstanceOf( '\WP\Notifications\Recipients\Recipient', $testee );
-	}
-
-	public function test_it_accepts_a_user_id_as_a_string() {
-		$testee = new Recipients\User( '1' );
-		$this->assertInstanceOf( '\WP\Notifications\Recipients\Recipient', $testee );
-	}
-
-	/** @dataProvider data_provider_it_throws_on_invalid_type */
-	public function test_it_throws_on_invalid_type( $invalid_user_id ) {
-		$this->expectException( '\WP\Notifications\Exceptions\Invalid_Recipient' );
-		new Recipients\User( $invalid_user_id );
-	}
-
-	public function data_provider_it_throws_on_invalid_type() {
-		return array(
-			array( null ),
-			array( - 1 ),
-			array( 0 ),
-			array( 'invalid' ),
-			array( new stdClass ),
-			array( array( 1, 2 ) ),
-		);
-	}
-}
diff --git a/wp-feature-notifications.php b/wp-feature-notifications.php
index 0132681f..62d3f5fb 100644
--- a/wp-feature-notifications.php
+++ b/wp-feature-notifications.php
@@ -32,32 +32,12 @@
 }
 
 // Require interface/class declarations..
+
 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/exceptions/class-invalid-recipient.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/exceptions/class-failed-to-add-recipient.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/interface-json-unserializable.php';
 require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/interface-status.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/interface-notification.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/class-factory.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/class-aggregate-factory.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/class-base-notification.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/senders/interface-sender.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/senders/class-base-sender.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/recipients/interface-recipient.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/recipients/class-collection.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/recipients/class-user.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/recipients/class-role.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/recipients/interface-factory.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/recipients/class-base-factory.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/recipients/class-aggregate-factory.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/messages/interface-message.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/messages/class-base-message.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/messages/interface-factory.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/messages/class-base-factory.php';
-require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/messages/class-aggregate-factory.php';
 require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/persistence/interface-notification-repository.php';
 require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/persistence/class-abstract-notification-repository.php';
 require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/persistence/class-wpdb-notification-repository.php';

From 87d2c1f8a899df2898b54cc886207fc0607c6a0b Mon Sep 17 00:00:00 2001
From: John Hooks <bitmachina@outlook.com>
Date: Thu, 20 Apr 2023 06:36:11 -0700
Subject: [PATCH 2/4] feature: add initial model classes

---
 includes/helper/class-serde.php       |  60 +++++
 includes/model/class-channel.php      | 135 +++++++++++
 includes/model/class-message.php      | 309 ++++++++++++++++++++++++++
 includes/model/class-notification.php | 195 ++++++++++++++++
 includes/model/class-subscription.php | 117 ++++++++++
 wp-feature-notifications.php          |   6 +-
 6 files changed, 821 insertions(+), 1 deletion(-)
 create mode 100644 includes/helper/class-serde.php
 create mode 100644 includes/model/class-channel.php
 create mode 100644 includes/model/class-message.php
 create mode 100644 includes/model/class-notification.php
 create mode 100644 includes/model/class-subscription.php

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 @@
+<?php
+/**
+ * Notifications API:Serde class
+ *
+ * @package wordpress/wp-feature-notifications
+ */
+
+namespace WP\Notifications\Helper;
+
+use DateTime;
+
+/**
+ * Class Serde
+ *
+ * SERialization and DEserialization helper static class.
+ */
+class Serde {
+
+	/**
+	 * Maybe serialize a DateTime as an ISO 8601 date string.
+	 *
+	 * @param DateTime|null $date The possible DateTime object to serialize.
+	 *
+	 * @return string|null Maybe an ISO 8601 date string.
+	 */
+	public static function maybe_serialize_json_date( $date ) {
+		if ( null === $date ) {
+			return null;
+		}
+
+		return $date->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..f8a64f06
--- /dev/null
+++ b/includes/model/class-channel.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * Notifications API:Channel class
+ *
+ * @package wordpress/wp-feature-notifications
+ */
+
+namespace WP\Notifications\Model;
+
+use JsonSerializable;
+
+/**
+ * Class representing a notification channel.
+ *
+ * @see register_channel()
+ */
+class Channel implements JsonSerializable {
+
+	/**
+	 * Name, including namespace, of the channel.
+	 *
+	 * @var string
+	 */
+	protected string $name;
+
+	/**
+	 * Human-readable label of the channel.
+	 *
+	 * @var string
+	 */
+	protected string $title;
+
+	// Optional properties
+
+	/**
+	 * Display context of the channel.
+	 */
+	protected ?string $context;
+
+	/**
+	 * Detailed description of the channel.
+	 */
+	protected ?string $description;
+
+	/**
+	 * Icon of the channel.
+	 */
+	protected ?string $icon;
+
+	/**
+	 * Constructor.
+	 *
+	 * Instantiates a Channel object.
+	 *
+	 * @see register_channel()
+	 *
+	 * @param string  $name        Name, including namespace, of the channel.
+	 * @param string  $title       Human-readable label of the channel.
+	 * @param ?string $context     Optional default display context of the channel.
+	 * @param ?string $description Optional detailed description of the channel.
+	 * @param ?string $icon        Optional icon of the channel.
+	 *
+	 */
+	public function __construct( $name, $title, $context = null, $description = null, $icon = null ) {
+		$this->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 mixed Data which can be serialized by json_encode, which is a
+	 *               value of any type other than a resource.
+	 */
+	public function jsonSerialize(): mixed {
+		return array(
+			'name'        => $this->name,
+			'title'       => $this->title,
+			'context'     => $this->context,
+			'icon'        => $this->icon,
+			'description' => $this->description,
+		);
+	}
+
+	/**
+	 * 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..2dd49e81
--- /dev/null
+++ b/includes/model/class-message.php
@@ -0,0 +1,309 @@
+<?php
+/**
+ * Notifications API:Message class
+ *
+ * @package wordpress/wp-feature-notifications
+ */
+
+namespace WP\Notifications\Model;
+
+use DateTime;
+use JsonSerializable;
+use WP\Notifications\Helper;
+
+/**
+ * Class representing a message.
+ */
+class Message implements JsonSerializable {
+
+	/**
+	 * The accepted keys of the message metadata.
+	 */
+	static protected $meta_keys = array(
+		'accept_label',
+		'accept_link',
+		'channel_title',
+		'dismiss_label',
+		'expires_at',
+		'icon',
+		'is_dismissible',
+		'severity',
+	);
+
+	/**
+	 * Text content of the message.
+	 *
+	 * @var string
+	 */
+	protected $message;
+
+	// Optional properties
+
+	/**
+	 * Label of the accept action.
+	 */
+	protected ?string $accept_label;
+
+	/**
+	 * URL of the accept action.
+	 */
+	protected ?string $accept_link;
+
+	/**
+	 * Human-readable title of the channel of which the message was emitted.
+	 */
+	protected ?string $channel_title;
+
+	/**
+	 * Datetime at which a message was created.
+	 */
+	protected ?DateTime $created_at;
+
+	/**
+	 * Label of the dismiss action.
+	 */
+	protected ?string $dismiss_label;
+
+	/**
+	 * Datetime at which a message expires.
+	 */
+	protected ?DateTime $expires_at;
+
+	/**
+	 * Icon of the message.
+	 */
+	protected ?string $icon;
+
+	/**
+	 * Database primary key value.
+	 */
+	protected ?int $id;
+
+	/**
+	 * Whether the notice can be dismissed.
+	 */
+	protected ?bool $is_dismissible;
+
+	/**
+	 * Severity of the message.
+	 */
+	protected ?string $severity;
+
+	/**
+	 * Human-readable message label.
+	 */
+	protected string $title;
+
+	/**
+	 * Constructor.
+	 *
+	 * Instantiates a Message object.
+	 *
+	 * @param string     $message        Text content of the message.
+	 * @param ?string    $accept_label   Optional label of the action.
+	 * @param ?string    $accept_link    Optional URL of the action.
+	 * @param ?string    $channel_title  Optional human-readable title of the channel
+	 *                                   the message was emitted from.
+	 * @param ?DateTime  $created_at     Optional datetime at which the message was
+	 *                                   created. Default `'null'`
+	 * @param ?string    $dismiss_label  Optional label of the dismiss action.
+	 * @param ?DateTime  $expires_at     Optional datetime at which a message expires.
+	 *                                   Default `'null'`
+	 * @param ?string    $icon           Optional icon of the message. Default `null`
+	 * @param ?int       $id             Optional database ID of the message. Default `null`
+	 * @param ?bool      $is_dismissible Optional boolean of whether the notice can be
+	 *                                   dismissed. Default `true`
+	 * @param ?string    $severity       Optional severity of the message. Default `null`
+	 * @param string     $title          Optional human-readable label of the message.
+	 */
+	public function __construct(
+		$message,
+		$accept_label = null,
+		$accept_link = null,
+		$channel_title = null,
+		$created_at = null,
+		$dismiss_label = null,
+		$expires_at = null,
+		$icon = null,
+		$id = null,
+		$is_dismissible = true,
+		$severity = null,
+		$title = ''
+	) {
+		$this->message        = $message;
+		$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 mixed Data which can be serialized by json_encode, which is a
+	 *               value of any type other than a resource.
+	 */
+	public function jsonSerialize(): mixed {
+		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 mixed The metadata of the message.
+	 */
+	protected function collect_meta(): mixed {
+		$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..cb8954fa
--- /dev/null
+++ b/includes/model/class-notification.php
@@ -0,0 +1,195 @@
+<?php
+/**
+ * Notifications API:Notification class
+ *
+ * @package wordpress/wp-feature-notifications
+ */
+
+namespace WP\Notifications\Model;
+
+use DateTime;
+use JsonSerializable;
+use WP\Notifications\Helper;
+
+/**
+ * Class representing a notification.
+ */
+class Notification implements JsonSerializable {
+
+	/**
+	 * Channel name, including namespace, the notification belongs to.
+	 */
+	protected string $channel_name;
+
+	/**
+	 * Display context of the notification.
+	 */
+	protected ?string $context;
+
+	/**
+	 * Datetime at which the notification was created.
+	 */
+	protected ?DateTime $created_at;
+
+	/**
+	 * Datetime at which the notification was dismissed.
+	 */
+	protected ?DateTime $dismissed_at;
+
+	/**
+	 * Datetime at which the notification was first displayed to the user.
+	 *
+	 * Intended to facility polling for new notifications from the client.
+	 */
+	protected ?DateTime $displayed_at;
+
+	/**
+	 * Datetime at which the notification expires.
+	 *
+	 * Intended to allow notification emitters to specify when a notification can be automatically
+	 * disposed of in UTC time.
+	 */
+	protected ?DateTime $expires_at;
+
+	/**
+	 * Database ID of the message related to the notification.
+	 */
+	protected int $message_id;
+
+	/**
+	 * Database ID of the user the notification belongs to.
+	 */
+	protected int $user_id;
+
+	/**
+	 * Constructor.
+	 *
+	 * Instantiates a Notice object.
+	 *
+	 * @param string     $channel_name Channel name, including namespace, the notification
+	 *                                 was emitted from.
+	 * @param int        $message_id   ID of the message related to the notification.
+	 * @param int        $user_id      ID of the user the notification belongs to.
+	 * @param ?string    $context      Optional display context of the notification.
+	 *                                 Default `'adminbar'`
+	 * @param ?DateTime  $created_at   Optional datetime at which the notification was created.
+	 * @param ?DateTime  $dismissed_at Optional datetime  t which the notification was dismissed.
+	 * @param ?DateTime  $displayed_at Optional datetime at which the notification was first displayed.
+	 * @param ?DateTime  $expires_at   Optional datetime at which the notification expires.
+	 */
+	public function __construct(
+		$channel_name,
+		$message_id,
+		$user_id,
+		$context = 'admin',
+		$created_at = null,
+		$dismissed_at = null,
+		$displayed_at = null,
+		$expires_at = null
+	) {
+		// Required properties
+
+		$this->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 mixed Data which can be serialized by json_encode, which is a
+	 *               value of any type other than a resource.
+	 */
+	public function jsonSerialize(): mixed {
+		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..55bae951
--- /dev/null
+++ b/includes/model/class-subscription.php
@@ -0,0 +1,117 @@
+<?php
+/**
+ * Notifications API:Subscription class
+ *
+ * @package wordpress/wp-feature-notifications
+ */
+
+namespace WP\Notifications\Model;
+
+use DateTime;
+use JsonSerializable;
+use WP\Notifications\Helper;
+
+/**
+ * Class representing a notification channel subscription.
+ */
+class Subscription implements JsonSerializable {
+
+	/**
+	 * Channel name, including namespace, the subscription belongs to.
+	 *
+	 * Used to relate a user and notification channel at the time of notice emission.
+	 */
+	protected string $channel_name;
+
+	/**
+	 * Datetime at which the subscription was created.
+	 */
+	protected ?DateTime $created_at;
+
+	/**
+	 * ID of the user the subscription belongs to.
+	 */
+	protected int $user_id;
+
+	// Optional properties
+
+	/**
+	 * Snoozed until option of the subscription.
+	 *
+	 * Intended to allow users a quick method to disable a channel subscription for
+	 * a set amount of time.
+	 */
+	protected ?DateTime $snoozed_until;
+
+	/**
+	 * Constructor.
+	 *
+	 * Instantiates a Subscription object.
+	 *
+	 * @param string    $channel_name  Channel name, including namespace, the subscription belongs to.
+	 * @param int       $user_id       ID of the user the subscription belongs to.
+	 * @param ?DateTime $created_at    Optional datetime at which the subscription was created.
+	 * @param ?DateTime $snoozed_until Optional snoozed until datetime of the subscription.
+	 *
+	 */
+	public function __construct( $channel_name, $user_id, $created_at = null, $snoozed_until = null ) {
+		$this->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 mixed Data which can be serialized by json_encode, which is a
+	 *               value of any type other than a resource.
+	 */
+	public function jsonSerialize(): mixed {
+		return array(
+			'channel_name'  => $this->channel_name,
+			'created_at'    => Helper\Serde::maybe_serialize_json_date( $this->created_at ),
+			'user_id'       => $this->user_id,
+			'snoozed_until' => Helper\Serde::maybe_serialize_json_date( $this->snoozed_until ),
+		);
+	}
+
+	/**
+	 * 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 user ID.
+	 *
+	 * @return int The user ID of the subscription.
+	 */
+	public function get_user_id(): int {
+		return $this->user_id;
+	}
+
+	/**
+	 * Get the snoozed until option.
+	 *
+	 * @return ?DateTime The snoozed until option of the subscription.
+	 */
+	public function get_snoozed_until(): ?DateTime {
+		return $this->snoozed_until;
+	}
+}
diff --git a/wp-feature-notifications.php b/wp-feature-notifications.php
index 62d3f5fb..ea76c9ca 100644
--- a/wp-feature-notifications.php
+++ b/wp-feature-notifications.php
@@ -32,7 +32,11 @@
 }
 
 // Require interface/class declarations..
-
+require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/helper/class-serde.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/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';

From 460b3c7973f6e348fc3c13153632ee6b90c157f3 Mon Sep 17 00:00:00 2001
From: John Hooks <bitmachina@outlook.com>
Date: Thu, 20 Apr 2023 07:33:15 -0700
Subject: [PATCH 3/4] feature: add initial factory classes

---
 includes/factory/class-messages.php      | 85 +++++++++++++++++++++++
 includes/factory/class-notifications.php | 87 ++++++++++++++++++++++++
 includes/factory/class-subscriptions.php | 68 ++++++++++++++++++
 includes/framework/class-factory.php     | 48 +++++++++++++
 wp-feature-notifications.php             |  4 ++
 5 files changed, 292 insertions(+)
 create mode 100644 includes/factory/class-messages.php
 create mode 100644 includes/factory/class-notifications.php
 create mode 100644 includes/factory/class-subscriptions.php
 create mode 100644 includes/framework/class-factory.php

diff --git a/includes/factory/class-messages.php b/includes/factory/class-messages.php
new file mode 100644
index 00000000..cd427aed
--- /dev/null
+++ b/includes/factory/class-messages.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Notifications API:Messages factory class
+ *
+ * @package wordpress/wp-feature-notifications
+ */
+
+namespace WP\Notifications\Factory;
+
+use DateTime;
+use WP\Notifications\Framework;
+use WP\Notifications\Model;
+
+/**
+ * Class representing a messages factory.
+ *
+ * @implements Framework\Factory<Messages>
+ */
+class Messages extends Framework\Factory {
+
+	/**
+	 * Instantiates a Message object.
+	 *
+	 * @param array|string   $args    {
+	 *     Array or string of arguments for creating a message. Supported arguments
+	 *     are described below.
+	 *
+	 *     @type string    $message        Text content of the message.
+	 *     @type ?string   $accept_label   Optional label of the accept action.
+	 *     @type ?string   $accept_link    Optional url of the accept action.
+	 *     @type ?string   $accept_message Optional label of the accept action.
+	 *     @type ?string   $channel_title  Optional human-readable title of the channel
+	 *                                     the message was emitted from.
+	 *     @type ?DateTime $created_at     Optional datetime at which a message was created.
+	 *                                     Default `'null'`
+	 *     @type ?string   $dismiss_label  Optional label of the dismiss action.
+	 *     @type ?DateTime $expires_at     Optional datetime at which a message expires.
+	 *                                     Default `'null'`
+	 *     @type ?string   $icon           Optional icon of the message. Default `null`
+	 *     @type ?int      $id             Optional database ID of the message. Default `null`
+	 *     @type bool      $is_dismissible Optional boolean of whether the notice can
+	 *                                     be dismissed. Default `true`
+	 *     @type ?string   $severity       Optional severity of the message. Default `null`
+	 *     @type string    $title          Optional human-readable message label. Default `''`
+	 * }
+	 *
+	 * @return Model\Message|false A newly created instance of Message or false.
+	 */
+	public function make( $args ) {
+		$parsed = wp_parse_args( $args );
+
+		// Required properties
+
+		$message = $parsed['message'];
+
+		// Optional properties
+
+		$accept_label   = array_key_exists( 'accept_label', $parsed ) ? $parsed['accept_label'] : null;
+		$accept_link    = array_key_exists( 'accept_link', $parsed ) ? $parsed['accept_link'] : null;
+		$channel_title  = array_key_exists( 'channel_title', $parsed ) ? $parsed['channel_title'] : null;
+		$created_at     = array_key_exists( 'created_at', $parsed ) ? $parsed['created_at'] : null;
+		$dismiss_label  = array_key_exists( 'dismiss_label', $parsed ) ? $parsed['dismiss_label'] : null;
+		$expires_at     = array_key_exists( 'expires_at', $parsed ) ? $parsed['expires_at'] : null;
+		$icon           = array_key_exists( 'icon', $parsed ) ? $parsed['icon'] : null;
+		$id             = array_key_exists( 'id', $parsed ) ? $parsed['id'] : null;
+		$is_dismissible = array_key_exists( 'is_dismissible', $parsed ) ? $parsed['is_dismissible'] : true;
+		$severity       = array_key_exists( 'severity', $parsed ) ? $parsed['severity'] : null;
+		$title          = array_key_exists( 'title', $parsed ) ? $parsed['title'] : '';
+
+		return new Model\Message(
+			$message,
+			$accept_label,
+			$accept_link,
+			$channel_title,
+			$created_at,
+			$dismiss_label,
+			$expires_at,
+			$icon,
+			$id,
+			$is_dismissible,
+			$severity,
+			$title,
+		);
+	}
+}
diff --git a/includes/factory/class-notifications.php b/includes/factory/class-notifications.php
new file mode 100644
index 00000000..2cde718b
--- /dev/null
+++ b/includes/factory/class-notifications.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * Notifications API:Notifications factory class
+ *
+ * @package wordpress/wp-feature-notifications
+ */
+
+namespace WP\Notifications\Factory;
+
+use DateTime;
+use WP\Notifications\Framework;
+use WP\Notifications\Helper;
+use WP\Notifications\Model;
+
+/**
+ * Class representing a notifications factory.
+ *
+ * @implements Framework\Factory<Notifications>
+ */
+class Notifications extends Framework\Factory {
+
+	/**
+	 * Instantiates a Notification object.
+
+	 * @param array|string $args     {
+	 *     Array or string of arguments for creating a notification. Supported arguments are described below.
+	 *
+	 *     @type string               $channel_name Channel name, including namespace,
+	 *                                              the notification was emitted from.
+	 *     @type int                  $message_id   ID of the message related to the
+	 *                                              notification.
+	 *     @type int                  $user_id      ID of the user the notification
+	 *                                              belongs to.
+	 *     @type ?string              $context      Optional display context of the
+	 *                                              notification. Default `'adminbar'`
+	 *     @type string|DateTime|null $created_at   Optional datetime at which the
+	 *                                              notification was created. Default `null`
+	 *     @type string|DateTime|null $dismissed_at Optional datetime  t which the
+	 *                                              notification was dismissed. Default `null`
+	 *     @type string|DateTime|null $displayed_at Optional datetime at which the
+	 *                                              notification was first displayed.
+	 *                                              Default `null`
+	 *     @type string|DateTime|null $expires_at   Optional datetime at which the
+	 *                                              notification expires. Default `null`
+	 * }
+	 * @param bool         $validate Optionally validate the arguments.
+	 *
+	 * @return Model\Notification|false A newly created instance of Channel or false.
+	 */
+	public function make( $args ) {
+		$parsed = wp_parse_args( $args );
+
+		// Required properties
+
+		$channel_name = $parsed['channel_name'];
+		$message_id   = $parsed['message_id'];
+		$user_id      = $parsed['user_id'];
+
+		// Optional properties
+
+		$context      = array_key_exists( 'context', $parsed ) ? $parsed['context'] : 'adminbar';
+		$created_at   = array_key_exists( 'created_at', $parsed ) ? $parsed['created_at'] : null;
+		$dismissed_at = array_key_exists( 'dismissed_at', $parsed ) ? $parsed['dismissed_at'] : null;
+		$displayed_at = array_key_exists( 'displayed_at', $parsed ) ? $parsed['displayed_at'] : null;
+		$expires_at   = array_key_exists( 'expires_at', $parsed ) ? $parsed['expires_at'] : null;
+
+		// Deserialize MySQL datetime strings.
+
+		$created_at   = Helper\Serde::maybe_deserialize_mysql_date( $created_at );
+		$dismissed_at = Helper\Serde::maybe_deserialize_mysql_date( $dismissed_at );
+		$displayed_at = Helper\Serde::maybe_deserialize_mysql_date( $displayed_at );
+		$expires_at   = Helper\Serde::maybe_deserialize_mysql_date( $expires_at );
+
+		$notification = new Model\Notification(
+			$channel_name,
+			$message_id,
+			$user_id,
+			$context,
+			$created_at,
+			$dismissed_at,
+			$displayed_at,
+			$expires_at
+		);
+
+		return $notification;
+	}
+}
diff --git a/includes/factory/class-subscriptions.php b/includes/factory/class-subscriptions.php
new file mode 100644
index 00000000..fcc1b871
--- /dev/null
+++ b/includes/factory/class-subscriptions.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Notifications API:Subscriptions factory class
+ *
+ * @package wordpress/wp-feature-notifications
+ */
+
+namespace WP\Notifications;
+
+use DateTime;
+use WP\Notifications\Framework;
+use WP\Notifications\Helper;
+use WP\Notifications\Model;
+
+/**
+ * Class representing a subscriptions factory
+ *
+ * @implements Framework\Factory<Subscriptions>.
+ */
+class Subscriptions extends Framework\Factory {
+
+	/**
+	 * Instantiates a Subscription object.
+	 *
+	 * @param array|string $args     {
+	 *     Array or string of arguments for creating a subscription. Supported
+	 *     arguments are described below.
+	 *
+	 *     @type string               $channel_name  Namespaced channel name of the
+	 *                                               subscription.
+	 *     @type int                  $user_id       ID of the user the subscription
+	 *                                               belongs to.
+	 *     @type string|DateTime|null $created_at    Optional datetime at which the
+	 *                                               subscription was created.
+	 *     @type string|DateTime|null $snoozed_until Optional snoozed until datetime
+	 *                                               of the subscription.
+	 * }
+	 *
+	 * @return Model\Subscription|false A newly created instance of Subscription or false.
+	 */
+	public function make( $args ) {
+		$parsed = wp_parse_args( $args );
+
+		// Required properties
+
+		$channel_name = $parsed['channel_name'];
+		$user_id      = $parsed['user_id'];
+
+		// Optional properties
+
+		$created_at    = array_key_exists( 'created_at', $parsed ) ? $parsed['created_at'] : null;
+		$snoozed_until = array_key_exists( 'snoozed_until', $parsed ) ? $parsed['snoozed_until'] : null;
+
+		// Deserialize MySQL datetime strings.
+
+		$created_at    = Helper\Serde::maybe_deserialize_mysql_date( $created_at );
+		$snoozed_until = Helper\Serde::maybe_deserialize_mysql_date( $snoozed_until );
+
+		$subscription = new Model\Subscription(
+			$channel_name,
+			$user_id,
+			$created_at,
+			$snoozed_until,
+		);
+
+		return $subscription;
+	}
+}
diff --git a/includes/framework/class-factory.php b/includes/framework/class-factory.php
new file mode 100644
index 00000000..40262e25
--- /dev/null
+++ b/includes/framework/class-factory.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Notifications API:Factory abstract class
+ *
+ * @package wordpress/wp-feature-notifications
+ */
+
+namespace WP\Notifications\Framework;
+
+/**
+ * Abstract class representing a factory.
+ *
+ * @template Model
+ */
+abstract class Factory {
+
+	/**
+	 * Container for the main instance of the class.
+	 *
+	 * @var ?Model
+	 */
+	protected static $instance = null;
+
+	/**
+	 * Instantiates a model object.
+
+	 * @param array|string $args Array or string of arguments for creating a model.
+
+	 *
+	 * @return Model|false A newly created instance of model or false.
+	 */
+	abstract public function make( $args );
+
+	/**
+	 * Utility method to retrieve the main instance of the class.
+	 *
+	 * The instance will be created if it does not exist yet.
+	 *
+	 * @return Model The main instance.
+	 */
+	public static function get_instance() {
+		if ( null === self::$instance ) {
+			self::$instance = new self();
+		}
+
+		return self::$instance;
+	}
+}
diff --git a/wp-feature-notifications.php b/wp-feature-notifications.php
index ea76c9ca..90360b59 100644
--- a/wp-feature-notifications.php
+++ b/wp-feature-notifications.php
@@ -32,11 +32,15 @@
 }
 
 // Require interface/class declarations..
+require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/framework/class-factory.php';
 require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/helper/class-serde.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/factory/class-messages.php';
+require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/factory/class-notifications.php';
+require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/factory/class-subscriptions.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';

From 88a9c9ecdce25a378e89b204c016f7cab7295e35 Mon Sep 17 00:00:00 2001
From: John Hooks <bitmachina@outlook.com>
Date: Thu, 20 Apr 2023 08:12:39 -0700
Subject: [PATCH 4/4] feature: add channel registry

---
 includes/channels.php               | 135 +++++++++++++++++++
 includes/class-channel-registry.php | 200 ++++++++++++++++++++++++++++
 wp-feature-notifications.php        |   2 +
 3 files changed, 337 insertions(+)
 create mode 100644 includes/channels.php
 create mode 100644 includes/class-channel-registry.php

diff --git a/includes/channels.php b/includes/channels.php
new file mode 100644
index 00000000..98eb976d
--- /dev/null
+++ b/includes/channels.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * Functions related to registering channels.
+ *
+ * @package wordpress/wp-feature-notifications
+ */
+
+namespace WP\Notifications;
+
+use WP\Notification\Model;
+
+/**
+ * Register a notification channel.
+ *
+ * @param string|Model\Channel $name  Channel name including namespace, or
+ *                                    alternatively a complete Channel instance.
+ *                                    Incase a Channel is provided, the $args
+ *                                    parameter will be ignored.
+ * @param array                $args  Optional. Array of channel arguments. Accepts
+ *                                    any public property of `Channel`. See
+ *                                    Channel::__construct() for information on
+ *                                    accepted arguments. Default empty array.
+ * @return Model\Channel|false The registered channel on success, or false on failure.
+ */
+function register_channel( $name, $args = array() ) {
+	return Channel_Registry::get_instance()->register( $name, $args );
+}
+
+/**
+ * Unregister a channel.
+ *
+ * @param string|Model\Channel $name Channel name including namespace, or
+ *                                   alternatively a complete Channel instance.
+ * @return Model\Channel|false The unregistered channel on success, or false on failure.
+ */
+function unregister_channel( $name ) {
+	return Channel_Registry::get_instance()->unregister( $name );
+}
+
+// Register core notification channels.
+
+add_action(
+	'init',
+	function () {
+		register_channel(
+			'core/updates',
+			array(
+				'title'       => __( 'WordPress Updates', 'wp-feature-notifications' ),
+				'icon'        => 'wordpress',
+				'description' => __( 'WordPress core update events.', 'wp-feature-notifications' ),
+			)
+		);
+
+		register_channel(
+			'core/plugin-install',
+			array(
+				'title'       => __( 'Plugin Install', 'wp-feature-notifications' ),
+				'icon'        => 'wordpress',
+				'description' => __( 'Plugin install events.', 'wp-feature-notifications' ),
+			)
+		);
+
+		register_channel(
+			'core/plugin-uninstall',
+			array(
+				'title'       => __( 'Plugin Uninstall', 'wp-feature-notifications' ),
+				'icon'        => 'wordpress',
+				'description' => __( 'Plugin uninstall events.', 'wp-feature-notifications' ),
+			)
+		);
+
+		register_channel(
+			'core/plugin-activate',
+			array(
+				'title'       => __( 'Plugin Activate', 'wp-feature-notifications' ),
+				'icon'        => 'wordpress',
+				'description' => __( 'Plugin activation events.', 'wp-feature-notifications' ),
+			)
+		);
+
+		register_channel(
+			'core/plugin-deactivate',
+			array(
+				'title'       => __( 'Plugin Deactivate', 'wp-feature-notifications' ),
+				'icon'        => 'wordpress',
+				'description' => __( 'Plugin deactivation events.', 'wp-feature-notifications' ),
+			)
+		);
+
+		register_channel(
+			'core/plugin-updates',
+			array(
+				'title'       => __( 'Plugin Update', 'wp-feature-notifications' ),
+				'icon'        => 'wordpress',
+				'description' => __( 'Plugin update events.', 'wp-feature-notifications' ),
+			)
+		);
+
+		register_channel(
+			'core/post-new',
+			array(
+				'title'       => __( 'New Post', 'wp-feature-notifications' ),
+				'icon'        => 'wordpress',
+				'description' => __( 'Post creation events.', 'wp-feature-notifications' ),
+			)
+		);
+
+		register_channel(
+			'core/post-edit',
+			array(
+				'title'       => __( 'Edit Post', 'wp-feature-notifications' ),
+				'icon'        => 'wordpress',
+				'description' => __( 'Post edit events.', 'wp-feature-notifications' ),
+			)
+		);
+
+		register_channel(
+			'core/post-delete',
+			array(
+				'title'       => __( 'Delete Post', 'wp-feature-notifications' ),
+				'icon'        => 'wordpress',
+				'description' => __( 'Post delete events.', 'wp-feature-notifications' ),
+			)
+		);
+
+		register_channel(
+			'core/comment-new',
+			array(
+				'title'       => __( 'New Comment', 'wp-feature-notifications' ),
+				'icon'        => 'wordpress',
+				'description' => __( 'Comment creation events.', 'wp-feature-notifications' ),
+			)
+		);
+	}
+);
diff --git a/includes/class-channel-registry.php b/includes/class-channel-registry.php
new file mode 100644
index 00000000..48ac26d2
--- /dev/null
+++ b/includes/class-channel-registry.php
@@ -0,0 +1,200 @@
+<?php
+/**
+ * Notifications API:Channel_Registry class
+ *
+ * Heavy inspired by the `WP_Block_Type_Registry`.
+ *
+ * The registered channels are stored in code, not in the database. If the data were
+ * stored in the database it would require a lookup to ensure the registered channel
+ * matched it's value in the database based. Since the channels can be registered by
+ * plugins, which maybe be uninstalled, the notification system cannot rely on the
+ * presence of the `Channel` instance after an initial render of a notification
+ * message. The namespaced channel name is the key used to look up a user's
+ * subscription to a channel.
+ *
+ * @package wordpress/wp-feature-notifications
+ */
+
+namespace WP\Notifications;
+
+use WP\Notifications\Model;
+
+/**
+ * Class used for interacting with channels.
+ */
+final class Channel_Registry {
+
+	/**
+	 * Registered channels, as `$name => $instance` pairs.
+	 *
+	 * @var Model\Channel[]
+	 */
+	private $registered_channels = array();
+
+	/**
+	 * Container for the main instance of the class.
+	 *
+	 * @var Channel_Registry|null
+	 */
+	private static $instance = null;
+
+	/**
+	 * Registers a channel.
+	 *
+	 * @see \WP\Notifications\Channel::__construct()
+	 *
+	 * @param string|Model\Channel $name Channel name including namespace, or
+	 *                                   alternatively a complete Channel instance.
+	 *                                   In case a Channel is provided, the $args
+	 *                                   parameter will be ignored.
+	 * @param array|string         $args {
+	 *     Array or string of arguments for registering a channel. Supported arguments
+	 *     are described below.
+	 *
+	 *     @type string      $title       Human-readable label of the channel.
+	 *     @type string|null $context     Optional display context of the channel
+	 *     @type string|null $description Optional detailed description of the channel.
+	 *     @type string|null $icon        Optional icon of the channel.
+	 * }
+	 * @return Model\Channel|false The registered channel on success, or false on failure.
+	 */
+	public function register( $name, $args = array() ) {
+		$channel = null;
+
+		if ( $name instanceof Channel ) {
+			$channel = $name;
+			$name    = $channel->get_name();
+		}
+
+		if ( ! is_string( $name ) ) {
+			_doing_it_wrong(
+				__METHOD__,
+				__( 'Channel names must be strings.' ),
+				'1.0.0'
+			);
+			return false;
+		}
+
+		if ( preg_match( '/[A-Z]+/', $name ) ) {
+			_doing_it_wrong(
+				__METHOD__,
+				__( 'Channel names must not contain uppercase characters.' ),
+				'1.0.0'
+			);
+			return false;
+		}
+
+		$name_matcher = '/^[a-z0-9-]+\/[a-z0-9-]+$/';
+		if ( ! preg_match( $name_matcher, $name ) ) {
+			_doing_it_wrong(
+				__METHOD__,
+				__( 'Channel names must contain a namespace prefix. Example: my-plugin/my-channel' ),
+				'1.0.0'
+			);
+			return false;
+		}
+
+		if ( $this->is_registered( $name ) ) {
+			_doing_it_wrong(
+				__METHOD__,
+				/* translators: %s: Channel name. */
+				sprintf( __( 'Channel "%s" is already registered.' ), $name ),
+				'1.0.0'
+			);
+			return false;
+		}
+
+		if ( ! $channel ) {
+			$parsed = wp_parse_args( $args );
+
+			$title       = $parsed['title'];
+			$context     = array_key_exists( 'context', $parsed ) ? $parsed['context'] : null;
+			$description = array_key_exists( 'description', $parsed ) ? $parsed['description'] : null;
+			$icon        = array_key_exists( 'icon', $parsed ) ? $parsed['icon'] : null;
+
+			$channel = new Model\Channel( $name, $title, $context, $description, $icon );
+		}
+
+		$this->registered_channels[ $channel->get_name() ] = $channel;
+
+		return $channel;
+	}
+
+	/**
+	 * Unregister a channel.
+	 *
+	 * @param string|Model/Channel $name Channel type name including namespace, or
+	 *                                   alternatively a complete Channel instance.
+	 * @return Model/Channel|false The unregistered channel on success, or false on failure.
+	 */
+	public function unregister( $name ) {
+		if ( $name instanceof Channel ) {
+			$name = $name->get_name();
+		}
+
+		if ( ! $this->is_registered( $name ) ) {
+			_doing_it_wrong(
+				__METHOD__,
+				/* translators: %s: Channel name. */
+				sprintf( __( 'Channel "%s" is not registered.' ), $name ),
+				'1.0.0'
+			);
+			return false;
+		}
+
+		$unregistered_channel = $this->registered_channels[ $name ];
+		unset( $this->registered_channels[ $name ] );
+
+		return $unregistered_channel;
+	}
+
+	/**
+	 * Retrieves a registered channel.
+	 *
+	 * @param string $name Channel name including namespace.
+	 *
+	 * @return Model\Channel|null The registered channel, or null if it is not registered.
+	 */
+	public function get_registered( $name ) {
+		if ( ! $this->is_registered( $name ) ) {
+			return null;
+		}
+
+		return $this->registered_channels[ $name ];
+	}
+
+	/**
+	 * Retrieves all registered channels.
+	 *
+	 * @return Model\Channel[] Associative array of `$channel_name => $channel` pairs.
+	 */
+	public function get_all_registered() {
+		return $this->registered_channels;
+	}
+
+	/**
+	 * Checks if a channel is registered.
+	 *
+	 * @param string $name Chanel name including namespace.
+	 *
+	 * @return bool True if the channel is registered, false otherwise.
+	 */
+	public function is_registered( $name ) {
+		return isset( $this->registered_channels[ $name ] );
+	}
+
+	/**
+	 * Utility method to retrieve the main instance of the class.
+	 *
+	 * The instance will be created if it does not exist yet.
+	 *
+	 * @return Channel_Registry The main instance.
+	 */
+	public static function get_instance() {
+		if ( null === self::$instance ) {
+			self::$instance = new self();
+		}
+
+		return self::$instance;
+	}
+}
diff --git a/wp-feature-notifications.php b/wp-feature-notifications.php
index 90360b59..2b9e0200 100644
--- a/wp-feature-notifications.php
+++ b/wp-feature-notifications.php
@@ -41,6 +41,8 @@
 require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/factory/class-messages.php';
 require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/factory/class-notifications.php';
 require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/factory/class-subscriptions.php';
+require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/class-channel-registry.php';
+require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/channels.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';