Solving The "Widget Problem" In ADR
The “widget problem” is when you have several panels or content areas on an HTML page that have different data sources. You might have a main content area, then a calendar off to the side, with perhaps a list of recent news items or blog posts, a todo or reminder widget, and maybe other information panels. The problem is that they each have different data sources, and may not always be displayed in every circumstance -- perhaps they are only shown to some users based on their preferences, or under certain conditions.
So how, in Action-Domain-Responder, do we get the “right” data for the set of widgets that are actually going to be displayed? (We’ll presume here that the entire page is being rendered server-side, for delivery as a whole to the client.)
The answer is “the same as with anything else” – we just have more kinds of data to get from the domain. The domain has all the data needed, and the knowledge necessary to figure out which data elements to return.
Let’s start with the Action, which is intentionally very spare: it only collects input, calls the Domain with that input, then invokes the Responder:
<?php
class PageAction
{
public function __construct(
PageService $domain,
PageResponder $responder
) {
$this->domain = $domain;
$this->responder = $responder;
}
public function __invoke(HttpRequest $request)
{
$payload = $this->domain->fetchPageData(
$request->getAttribute('sessionId'),
$request->getAttribute('pageName')
);
return $this->responder->respond($request, $payload);
}
}
The domain work is where the heavy lifting happens. The example below returns the domain objects and data wrapped in a Domain Payload object.
<?php
class PageService
{
// presume $userService, $sessionService, and $database
// dependencies are injected via constructor
public function fetchPageData($sessionId, $pageName)
{
$session = $this->sessionService->resume($sessionId);
$user = $this->userService->fetch($session->userId);
// the main page data
$mainData = $this->fetchMainData($pageName);
if (! $mainData) {
return new Payload('NOT_FOUND');
}
// an array of widgets to show
$widgets = [];
// different users might prefer to see different widgets
foreach ($user->getWidgetsToShow() as $widgetName) {
$method = "fetch{$widgetName}Data";
$widgets[$widgetName] = $this->$method();
}
$this->sessionService->commit($session);
return new Payload('FOUND', [
'user' => $user,
'page_name' => $pageName,
'main_data' => $mainData,
'widgets' => $widgets
]);
}
protected function fetchMainData($page_name)
{
return $this->database->fetchRow(
"SELECT * FROM pages WHERE page_name = ? LIMIT 1",
$page_name
);
}
protected function fetchTodoData() { ... }
protected function fetchRemindersData() { ... }
protected function fetchUpcomingEventsData() { ... }
protected function fetchCalendarData() { ... }
}
Finally, the Responder work becomes as straightforward as: “Is there Todo data to present? Then render the Todo widget via a Todo helper using the Todo data.” It could be as simple as this:
<?php
class PageResponder
{
// ...
public function respond(Request $request, Payload $payload)
{
if ($payload->getStatus() == 'NOT_FOUND') {
return new Response(404);
}
$output = $payload->getOutput();
$html = '';
$html .= $this->renderHeader($output['page_name'];
$html .= $this->renderNav();
$html .= $this->renderMain($output['main_data']);
foreach ($output['widgets'] as $widgetName => $widgetData) {
$method = "render{$widgetName}Html";
$html .= $this->$method($widgetData);
}
return new Response(200, $html);
}
protected function renderHeader($request, $pageName) { ... }
protected function renderNav($request) { ... }
protected function renderMain($request, $mainData) { ... }
protected function renderTodoHtml($request, $widgetData) { ... }
protected function renderRemindersHtml($request, $widgetData) { ... }
protected function renderUpcomingEventsHtml($request, $widgetData) { ... }
protected function renderCalendarHtml($request, $widgetData) { ... }
?>
One alternative here is for some client-side Javascript to make one additional call per widget or panel to retrieve widget-specific data, then render that data on the client. The server-side work becomes less complex (one action per widget, and transform the data to JSON instead of HTML) – but the client-side work becomes more complex, and you have more HTTP calls back-and-forth to build the page.