A New Observer Pattern: Bidirectional Signals from the Emitter’s Perspective in PHP Morteza 5 min read · Aug 23, 2025 -- Listen Share Press enter or click to view image in full size php-repos’s observer pattern Introduction: Shifting the Observer Perspective In software, the traditional observer pattern is often designed with the observer in mind: an event happens, like a user logging in, and a handler — say, a logger — reacts by recording it. This perspective assumes the observer (the handler) is the primary actor, catching events like a net catches fish. In frameworks like Laravel, this is evident: when a PodcastProcessed event triggers a SendPodcastNotification , the focus is on the notification system reacting to the event. But ask yourself: is it the podcast processing code that wants to send a notification, or the notification system that cares about the podcast being processed? In a one-directional world, this distinction seems trivial—events fire, observers react, and the emitter moves on. Yet, real-world business scenarios rarely work this way. When submitting a sale order, the code might announce its intent (a plan) and expect feedback, like a signal to cancel if inventory is low. Or a library might ask whether to use Postgres or MySQL, adapting based on a response. These cases demand a conversation, not a monologue. The Observer package in PHP redefines the observer pattern by shifting the perspective to the emitter — the code dispatching the signal. From this viewpoint, signals (events, plans, inquiries, messages, or commands) can elicit counter-signals that influence the emitter’s flow, creating a bidirectional dialogue. This article explores this new mindset, its PHP implementation, and practical examples that bring dynamic, business-driven workflows to life. The Mindset: Signals from the Emitter’s Perspective Traditional observer patterns, like Laravel’s, treat events as announcements for observers to handle passively — loggers log, notifiers notify. But this observer-centric view limits flexibility. What if the code dispatching the signal needs feedback to adjust its actions? Imagine planning to throw a stone at a window: you announce your intent (a Plan signal), and an observer shouts, “Stop!” (a Command signal), prompting you to drop the stone. The emitter (you) remains the focus, dispatching signals and responding to counter-signals without needing to know who the observers are. In the Observer package, the emitter’s perspective drives the system. The current code dispatches a signal — be it an event ( UserLoggedIn ), a plan ( SaleOrderPlan ), an inquiry ( ConfirmationInquiry ), or a command—and can receive counter-signals (e.g., a CancelOrderCommand ) that alter its flow. This bidirectional approach mirrors real-world business needs, where actions are proposed, feedback is given, and decisions adapt dynamically, making it ideal for complex, interactive applications. Core Concepts The Observer package is built around signals and handlers, leveraging PHP’s dynamic features to create a flexible, emitter-focused system. Signals: Messages with Intent Signals are the core entities, each with public readonly properties: id (UUID), title (description), time (UTC DateTimeImmutable ), and details (metadata array). They implement JsonSerializable for interoperability. The package supports seven signal types: Signal (Base) : The foundation for all signals. : The foundation for all signals. Event : Marks a completed action (e.g., UserLoggedIn ). : Marks a completed action (e.g., ). Plan : Proposes a future action (e.g., SaleOrderPlan ). : Proposes a future action (e.g., ). Inquiry : Requests input (e.g., ConfirmationInquiry ). : Requests input (e.g., ). Message : Shares information without expecting a response. : Shares information without expecting a response. Command : Issues an instruction (e.g., CancelOrderCommand ). : Issues an instruction (e.g., ). Internal Signals: Observability signals ( HandlerExecution , HandlerFound , NoHandlerFound ) for debugging. Create signals with a constructor or create method: use PhpRepos\Observer\Signals\Event; class UserLoggedIn extends Event {} $signal = UserLoggedIn::create('UserLoggedIn', ['user_id' => 1]); echo $signal->title; // UserLoggedIn echo json_encode($signal); // {"id":"uuid","title":"UserLoggedIn","details":{"user_id":1},"time":"2025-08-23T11:54:00.000000+00:00"} Handlers: Responding with Counter-Signals Handlers are callables (e.g., closures) registered via subscribe , with typed parameters to match signals. They can return counter-signals, enabling bidirectional influence: use PhpRepos\Observer\Observer\subscribe; subscribe(function (UserLoggedIn $event) { echo "User {$event->details['user_id']} logged in at {$event->time->format('Y-m-d H:i:s')}. "; }); Key Features The package offers powerful features for dynamic, emitter-driven PHP applications: Bidirectional Signal Handling From the emitter’s perspective, handlers can return counter-signals to influence the flow. For example, in an e-commerce system, a SaleOrderPlan might trigger a CancelOrderCommand if inventory is low: use PhpRepos\Observer\Observer\propose; use PhpRepos\Observer\Signals\Plan; use PhpRepos\Observer\Signals\Command; class SaleOrderPlan extends Plan {} class CancelOrderCommand extends Command {} subscribe(function (SaleOrderPlan $plan) { if ($plan->details['order']['inventory'] < 1) { return CancelOrderCommand::create('CancelOrder', ['reason' => 'Out of stock']); } }); $plan = SaleOrderPlan::create('SaleOrderPlan', ['order' => ['inventory' => 0]]); $responses = propose($plan); foreach ($responses as $response) { if ($response instanceof CancelOrderCommand) { echo "Order canceled: {$response->details['reason']} "; } } // Outputs: Order canceled: Out of stock The emitter (sale order code) dispatches a plan and adapts based on the counter-signal, without caring about the handler’s identity. Flexible Input Handling Inquiries enable cross-platform input, ideal for CLI or web applications: use PhpRepos\Observer\Signals\Inquiry; use PhpRepos\Observer\Signals\Message; class ConfirmationInquiry extends Inquiry {} class InputPromptMessage extends Message {} subscribe(function (ConfirmationInquiry $inquiry) { echo $inquiry->details['prompt'] . ' '; return InputPromptMessage::create('Input Prompt Received', ['answer' => trim(fgets(STDIN))]); }); $inquiry = ConfirmationInquiry::create('ConfirmAction', ['prompt' => 'Proceed? (y/n)']); $responses = send($inquiry); foreach ($responses as $response) { echo "User answered: {$response->details['answer']} "; } // CLI output: Proceed? (y/n) y // User answered: y This supports dynamic input across interfaces, with the emitter awaiting counter-signals. Database Selection Example For third-party libraries, the package enables dynamic configuration. Consider a library choosing between Postgres and MySQL: use PhpRepos\Observer\Observer\ask; use PhpRepos\Observer\Signals\Inquiry; use PhpRepos\Observer\Signals\Command; class DatabaseInquiry extends Inquiry {} class DatabaseCommand extends Command {} subscribe(function (DatabaseInquiry $inquiry) { $default = 'mysql'; $config = ['available' => ['mysql', 'postgres'], 'default' => $default]; return DatabaseCommand::create('SelectDatabase', ['database' => $config['default']]); }); $inquiry = DatabaseInquiry::create('SelectDatabase', ['context' => 'connection']); $responses = ask($inquiry); foreach ($responses as $response) { echo "Selected database: {$response->details['database']} "; } // Outputs: Selected database: mysql The emitter (library) asks for a database choice and adapts based on the counter-signal, enhancing modularity. Internal Signals for Observability Internal signals ( HandlerExecution , HandlerFound , NoHandlerFound ) enable debugging and logging: use PhpRepos\Observer\Signals\Signal; subscribe(function (Signal $signal) { file_put_contents('signals.log', "[{$signal->time->format('Y-m-d H:i:s')}] {$signal->title}: " . json_encode($signal->details) . " ", FILE_APPEND); }); This logs all signals, including internal ones, to a file for traceability. Signal-Specific Functions Functions like broadcast , propose , ask , share , and order add semantic clarity: use PhpRepos\Observer\Observer\broadcast; broadcast(UserLoggedIn::create('UserLoggedIn', ['user_id' => 1])); Error Handling Handlers may throw exceptions, which send propagates: subscribe(function (UserLoggedIn $event) { if (!isset($event->details['user_id'])) { throw new Exception('Missing user_id'); } }); try { send(UserLoggedIn::create('UserLoggedIn', [])); } catch (Exception $e) { echo "Error: {$e->getMessage()} "; } // Outputs: Error: Missing user_id Use Cases The Observer package excels in: Business Workflows : Canceling orders due to inventory issues or approving actions dynamically. : Canceling orders due to inventory issues or approving actions dynamically. Cross-Platform Input : Handling CLI or web inputs via inquiries. : Handling CLI or web inputs via inquiries. Dynamic Configuration : Selecting databases or services for libraries. : Selecting databases or services for libraries. Auditing: Logging signals for debugging and traceability. Why It Stands Out Unlike traditional observer patterns, which view events from the observer’s perspective (e.g., reacting to PodcastProcessed ), this package centers on the emitter. It offers: Bidirectional Communication : Counter-signals (e.g., CancelOrderCommand ) allow handlers to influence the emitter’s flow, like a conversation shaping both parties. : Counter-signals (e.g., ) allow handlers to influence the emitter’s flow, like a conversation shaping both parties. Rich Signal Types : Seven types (Event, Plan, Inquiry, etc.) model diverse interactions, unlike single-type events. : Seven types (Event, Plan, Inquiry, etc.) model diverse interactions, unlike single-type events. Emitter Focus : The dispatching code drives the narrative, responding to counter-signals without needing to know the handlers. : The dispatching code drives the narrative, responding to counter-signals without needing to know the handlers. Business-Driven Design: Supports dynamic workflows that mirror real-world business needs, like order processing or database selection. Conclusion The Observer package redefines event-driven programming in PHP by placing the emitter at the heart of the system. By enabling bidirectional signal communication, it transforms static notifications into dynamic conversations, aligning software with the interactive nature of business processes. Whether canceling orders, handling user inputs, or configuring libraries, this package empowers developers to build responsive, emitter-driven applications that adapt in real time. Explore the package and examples at https://github.com/php-repos/observer.git