Skip to content

Commit 408e6d8

Browse files
author
Jurian Sluiman
committed
Add a listener for application/problem-api+json
1 parent 1a4ba39 commit 408e6d8

3 files changed

Lines changed: 206 additions & 3 deletions

File tree

Module.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ public function onBootstrap(EventInterface $e)
8787
if (true === $config['enable_messages']) {
8888
$this->attachExceptionMessages($e);
8989
}
90+
if (true === $config['enable_problem_api']) {
91+
$this->attachProblemApiListeners($e);
92+
}
9093
}
9194

9295
protected function attachExceptionListeners(EventInterface $e)
@@ -119,4 +122,13 @@ protected function attachExceptionMessages(EventInterface $e)
119122
{
120123
// @todo Implement user defined error messages
121124
}
125+
126+
protected function attachProblemApiListeners(EventInterface $e)
127+
{
128+
$app = $e->getApplication();
129+
$em = $app->getEventManager();
130+
131+
$listener = new Mvc\JsonProblemApiListener;
132+
$listener->attach($em);
133+
}
122134
}

config/module.config.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@
4040

4141
return array(
4242
'slm_exception' => array(
43-
'enable_markers' => false,
44-
'enable_logging' => false,
45-
'enable_messages' => false,
43+
'enable_markers' => false,
44+
'enable_logging' => false,
45+
'enable_messages' => false,
46+
'enable_problem_api' => false,
4647

4748
'default_marker' => 'SlmException\Exception\ServerErrorInterface',
4849
'exception_markers' => array(
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
<?php
2+
/**
3+
* Copyright (c) 2012-2013 Jurian Sluiman.
4+
* All rights reserved.
5+
*
6+
* Redistribution and use in source and binary forms, with or without
7+
* modification, are permitted provided that the following conditions
8+
* are met:
9+
*
10+
* * Redistributions of source code must retain the above copyright
11+
* notice, this list of conditions and the following disclaimer.
12+
*
13+
* * Redistributions in binary form must reproduce the above copyright
14+
* notice, this list of conditions and the following disclaimer in
15+
* the documentation and/or other materials provided with the
16+
* distribution.
17+
*
18+
* * Neither the names of the copyright holders nor the names of the
19+
* contributors may be used to endorse or promote products derived
20+
* from this software without specific prior written permission.
21+
*
22+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25+
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
26+
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
27+
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28+
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29+
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31+
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
32+
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33+
* POSSIBILITY OF SUCH DAMAGE.
34+
*
35+
* @author Jurian Sluiman <jurian@juriansluiman.nl>
36+
* @copyright 2012-2013 Jurian Sluiman http://juriansluiman.nl.
37+
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
38+
*/
39+
40+
namespace SlmException\Mvc;
41+
42+
use Zend\EventManager\ListenerAggregateInterface;
43+
use Zend\EventManager\EventManagerInterface;
44+
use Zend\Http\Request as HttpRequest;
45+
use Zend\Mvc\MvcEvent;
46+
use Zend\View\Model\JsonModel;
47+
use Zend\View\Model\ModelInterface;
48+
49+
class JsonProblemApiListener implements ListenerAggregateInterface
50+
{
51+
protected $listeners;
52+
53+
/**
54+
* {@inheritDoc}
55+
*/
56+
public function attach(EventManagerInterface $events)
57+
{
58+
$this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER, array($this, 'onRender'), 1000);
59+
$this->listeners[] = $events->attach(MvcEvent::EVENT_FINISH, array($this, 'onFinish'));
60+
}
61+
62+
/**
63+
* {@inheritDoc}
64+
*/
65+
public function detach(EventManagerInterface $events)
66+
{
67+
foreach ($this->listeners as $key => $listener) {
68+
$detached = false;
69+
if ($listener === $this) {
70+
continue;
71+
}
72+
if ($listener instanceof ListenerAggregateInterface) {
73+
$detached = $listener->detach($events);
74+
} elseif ($listener instanceof CallbackHandler) {
75+
$detached = $events->detach($listener);
76+
}
77+
78+
if ($detached) {
79+
unset($this->listeners[$key]);
80+
}
81+
}
82+
}
83+
84+
public function onRender(MvcEvent $e)
85+
{
86+
// must be an error
87+
if (!$e->isError()) {
88+
return;
89+
}
90+
91+
// Check the accept headers for application/json
92+
$request = $e->getRequest();
93+
if (!$request instanceof HttpRequest) {
94+
return;
95+
}
96+
97+
$headers = $request->getHeaders();
98+
if (!$headers->has('Accept')) {
99+
return;
100+
}
101+
102+
$accept = $headers->get('Accept');
103+
$match = $accept->match('application/json');
104+
if (!$match || $match->getTypeString() == '*/*') {
105+
// not application/json
106+
return;
107+
}
108+
109+
// if we have a JsonModel in the result, then do nothing
110+
$currentModel = $e->getResult();
111+
if ($currentModel instanceof JsonModel) {
112+
return;
113+
}
114+
115+
// create a new JsonModel - use application/api-problem+json fields.
116+
$response = $e->getResponse();
117+
$model = new JsonModel(array(
118+
"httpStatus" => $response->getStatusCode(),
119+
"title" => $response->getReasonPhrase(),
120+
));
121+
122+
// Find out what the error is
123+
$exception = $currentModel->getVariable('exception');
124+
125+
if ($currentModel instanceof ModelInterface && $currentModel->reason) {
126+
switch ($currentModel->reason) {
127+
case 'error-controller-cannot-dispatch':
128+
$model->detail = 'The requested controller was unable to dispatch the request.';
129+
break;
130+
case 'error-controller-not-found':
131+
$model->detail = 'The requested controller could not be mapped to an existing controller class.';
132+
break;
133+
case 'error-controller-invalid':
134+
$model->detail = 'The requested controller was not dispatchable.';
135+
break;
136+
case 'error-router-no-match':
137+
$model->detail = 'The requested URL could not be matched by routing.';
138+
break;
139+
default:
140+
$model->detail = $currentModel->message;
141+
break;
142+
}
143+
}
144+
145+
if ($exception) {
146+
if ($exception->getCode()) {
147+
$e->getResponse()->setStatusCode($exception->getCode());
148+
}
149+
$model->detail = $exception->getMessage();
150+
151+
// find the previous exceptions
152+
$messages = array();
153+
while ($exception = $exception->getPrevious()) {
154+
$messages[] = "* " . $exception->getMessage();
155+
};
156+
if (count($messages)) {
157+
$exceptionString = implode("\n", $messages);
158+
$model->messages = $exceptionString;
159+
}
160+
}
161+
162+
// set our new view model
163+
$model->setTerminal(true);
164+
$e->setResult($model);
165+
$e->setViewModel($model);
166+
167+
// set our json renderer
168+
$sm = $e->getApplication()->getServiceManager();
169+
$view = $sm->get('Zend\View\View');
170+
$strategy = $sm->get('ViewJsonStrategy');
171+
$view->getEventManager()->attach($strategy, 100);
172+
}
173+
174+
public function onFinish(MvcEvent $e)
175+
{
176+
$response = $e->getResponse();
177+
$headers = $response->getHeaders();
178+
179+
if ($headers->has('Content-Type')
180+
&& strpos($headers->get('Content-Type')->getFieldValue(), 'application/json') !== false
181+
&& strpos($response->getContent(), 'httpStatus') !== false
182+
){
183+
/**
184+
* We can almost be certain the response is an api problem,
185+
* since the JSON contains httpStatus
186+
*/
187+
$headers->addHeaderLine('Content-Type', 'application/api-problem+json');
188+
}
189+
}
190+
}

0 commit comments

Comments
 (0)