Reputation: 31730
I'm looking into the SplObserver pattern as a way of solving the logging problem (namely how do you handle activity logging without implementing it directly in the classes you're interested in and therefore putting code in them that isn't directly related to their area of responsibility).
The problem is that as implemented, SplObserver doesn't seem to give you any kind of standardized mechanism for the notifying class to send any details to the observing class other than "I'm triggering a notification".
I was curious as to how other people get around this problem, do they extend the SplObserver and SplSubject interfaces or roll their own instead?
I was also thinking that in more general terms (as in other functionality that could be implemented with Observers, not necessarially logging) if it was possible to implement an Observer pattern where the observer can specify it only wants to be notified of certain events, and not every event the subject might generate. For example, I might want a logging observer that records all activity to a log file, but also an error reporting observer that sends an email to an administrator when an error occurs, but only when an error occurs. You could write the error logger to ignore notifications that aren't triggered by an error (assuming that it is possible to modify this pattern so that specific kinds of notifications could be sent), but I suspect that this would be less efficient than ideal. I suspect that allowing observers to only subscribe to specific subject events would be better, but can that approach be implemented with SplObserver?
Upvotes: 1
Views: 327
Reputation: 2478
Implementation of the Observer Pattern, with selective subscription:
Let's say we have a User
repository class that we'd like to observe in order to log actions and also send welcome emails to new users.
class User
{
public function create()
{
// User creation code...
}
public function update()
{
// User update code...
}
public function delete()
{
// User deletion code...
}
}
Now, we create a trait
that will contain the Subject logic. This Trait could be applied to any class that you want to observe. It can manage different "event names", so the observers can subscribe to all of them or only to certain events.
trait SubjectTrait {
private $observers = [];
// this is not a real __construct() (we will call it later)
public function construct()
{
$this->observers["all"] = [];
}
private function initObserversGroup(string $name = "all")
{
if (!isset($this->observers[$name])) {
$this->observers[$name] = [];
}
}
private function getObservers(string $name = "all")
{
$this->initObserversGroup($name);
$group = $this->observers[$name];
$all = $this->observers["all"];
return array_merge($group, $all);
}
public function attach(\SplObserver $observer, string $name = "all")
{
$this->initObserversGroup($name);
$this->observers[$name][] = $observer;
}
public function detach(\SplObserver $observer, string $name = "all")
{
foreach ($this->getObservers($name) as $key => $o) {
if ($o === $observer) {
unset($this->observers[$name][$key]);
}
}
}
public function notify(string $name = "all", $data = null)
{
foreach ($this->getObservers($name) as $observer) {
$observer->update($this, $name, $data);
}
}
}
Next, we use the trait in our classes. Our User
class will look like this:
class User implements \SplSubject
{
// It's necessary to alias construct() because it
// would conflict with other methods.
use SubjectTrait {
SubjectTrait::construct as protected constructSubject;
}
public function __construct()
{
$this->constructSubject();
}
public function create()
{
// User creation code...
$this->notify("User:created");
}
public function update()
{
// User update code...
$this->notify("User:updated");
}
public function delete()
{
// User deletion code...
$this->notify("User:deleted");
}
}
The last step is to create our observer
classes, that will be able to subscribe to the subjects. We implement here two loggers and one emailer.
class Logger1 implements \SplObserver
{
public function update(\SplSubject $event, string $name = null, $data = null)
{
// you could also log $data
echo "Logger1: $name.\n";
}
}
class Logger2 implements \SplObserver
{
public function update(\SplSubject $event, string $name = null, $data = null)
{
// you could also log $data
echo "Logger2: $name.\n";
}
}
class Welcomer implements \SplObserver
{
public function update(\SplSubject $event, string $name = null, $data = null)
{
// here you could use the user name from $data
echo "Welcomer: sending email.\n";
}
}
Let's test it:
// create a User object
$user = new User();
// subscribe the logger 1 to all user events
$user->attach(new Logger1(), "all");
// subscribe the logger 2 only to user deletions
$user->attach(new Logger2(), "User:deleted");
// subscribe the welcomer emailer only to user creations
$user->attach(new Welcomer(), "User:created");
// perform some actions
$user->create();
$user->update();
$user->delete();
The output will be:
Welcomer: sending email.
Logger1: User:created.
Logger1: User:updated.
Logger2: User:deleted.
Logger1: User:deleted.
Upvotes: 0
Reputation: 2916
The splSubject does send itself when sending notifications. It's possible to implement a callback method in the subject so observers can figure out what exactly has changed.
function update(SplSubject $subject) {
$changed = $subject->getChanges();
....
}
you would have to probably create a new interface to force the existence of getChanges() in the subject.
On different kind of notifications, you can take a look at message-queue systems. They allow you to subscribe to different message-boxes ('logging.error', 'logging.warning', or even 'logging'), where they will receive notifications if another system (the subject) sends a message to the corresponding queue. They're not much more difficult to implement as the splObserver/splSubject.
Upvotes: 3