_useHttpServer = class_exists($namespace . '\Application'); } /** * Clears the state used for requests. * * @after * @return void */ public function cleanup() { $this->_request = []; $this->_session = []; $this->_cookie = []; $this->_response = null; $this->_exception = null; $this->_controller = null; $this->_viewName = null; $this->_layoutName = null; $this->_requestSession = null; $this->_appClass = null; $this->_appArgs = null; $this->_securityToken = false; $this->_csrfToken = false; $this->_retainFlashMessages = false; $this->_useHttpServer = false; } /** * Toggle whether or not you want to use the HTTP Server stack. * * @param bool $enable Enable/disable the usage of the HTTP Stack. * @return void */ public function useHttpServer($enable) { $this->_useHttpServer = (bool)$enable; } /** * Configure the application class to use in integration tests. * * Combined with `useHttpServer()` to customize the class name and constructor arguments * of your application class. * * @param string $class The application class name. * @param array|null $constructorArgs The constructor arguments for your application class. * @return void */ public function configApplication($class, $constructorArgs) { $this->_appClass = $class; $this->_appArgs = $constructorArgs; } /** * Calling this method will enable a SecurityComponent * compatible token to be added to request data. This * lets you easily test actions protected by SecurityComponent. * * @return void */ public function enableSecurityToken() { $this->_securityToken = true; } /** * Calling this method will add a CSRF token to the request. * * Both the POST data and cookie will be populated when this option * is enabled. The default parameter names will be used. * * @return void */ public function enableCsrfToken() { $this->_csrfToken = true; } /** * Calling this method will re-store flash messages into the test session * after being removed by the FlashHelper * * @return void */ public function enableRetainFlashMessages() { $this->_retainFlashMessages = true; } /** * Configures the data for the *next* request. * * This data is cleared in the tearDown() method. * * You can call this method multiple times to append into * the current state. * * @param array $data The request data to use. * @return void */ public function configRequest(array $data) { $this->_request = $data + $this->_request; } /** * Sets session data. * * This method lets you configure the session data * you want to be used for requests that follow. The session * state is reset in each tearDown(). * * You can call this method multiple times to append into * the current state. * * @param array $data The session data to use. * @return void */ public function session(array $data) { $this->_session = $data + $this->_session; } /** * Sets a request cookie for future requests. * * This method lets you configure the session data * you want to be used for requests that follow. The session * state is reset in each tearDown(). * * You can call this method multiple times to append into * the current state. * * @param string $name The cookie name to use. * @param mixed $value The value of the cookie. * @return void */ public function cookie($name, $value) { $this->_cookie[$name] = $value; } /** * Returns the encryption key to be used. * * @return string */ protected function _getCookieEncryptionKey() { if (isset($this->_cookieEncryptionKey)) { return $this->_cookieEncryptionKey; } return Security::getSalt(); } /** * Sets a encrypted request cookie for future requests. * * The difference from cookie() is this encrypts the cookie * value like the CookieComponent. * * @param string $name The cookie name to use. * @param mixed $value The value of the cookie. * @param string|bool $encrypt Encryption mode to use. * @param string|null $key Encryption key used. Defaults * to Security.salt. * @return void * @see \Cake\Utility\CookieCryptTrait::_encrypt() */ public function cookieEncrypted($name, $value, $encrypt = 'aes', $key = null) { $this->_cookieEncryptionKey = $key; $this->_cookie[$name] = $this->_encrypt($value, $encrypt); } /** * Performs a GET request using the current request data. * * The response of the dispatched request will be stored as * a property. You can use various assert methods to check the * response. * * @param string|array $url The URL to request. * @return void * @throws \PHPUnit\Exception */ public function get($url) { $this->_sendRequest($url, 'GET'); } /** * Performs a POST request using the current request data. * * The response of the dispatched request will be stored as * a property. You can use various assert methods to check the * response. * * @param string|array $url The URL to request. * @param string|array|null $data The data for the request. * @return void * @throws \PHPUnit\Exception */ public function post($url, $data = []) { $this->_sendRequest($url, 'POST', $data); } /** * Performs a PATCH request using the current request data. * * The response of the dispatched request will be stored as * a property. You can use various assert methods to check the * response. * * @param string|array $url The URL to request. * @param string|array|null $data The data for the request. * @return void * @throws \PHPUnit\Exception */ public function patch($url, $data = []) { $this->_sendRequest($url, 'PATCH', $data); } /** * Performs a PUT request using the current request data. * * The response of the dispatched request will be stored as * a property. You can use various assert methods to check the * response. * * @param string|array $url The URL to request. * @param string|array|null $data The data for the request. * @return void * @throws \PHPUnit\Exception */ public function put($url, $data = []) { $this->_sendRequest($url, 'PUT', $data); } /** * Performs a DELETE request using the current request data. * * The response of the dispatched request will be stored as * a property. You can use various assert methods to check the * response. * * @param string|array $url The URL to request. * @return void * @throws \PHPUnit\Exception */ public function delete($url) { $this->_sendRequest($url, 'DELETE'); } /** * Performs a HEAD request using the current request data. * * The response of the dispatched request will be stored as * a property. You can use various assert methods to check the * response. * * @param string|array $url The URL to request. * @return void * @throws \PHPUnit\Exception */ public function head($url) { $this->_sendRequest($url, 'HEAD'); } /** * Performs an OPTIONS request using the current request data. * * The response of the dispatched request will be stored as * a property. You can use various assert methods to check the * response. * * @param string|array $url The URL to request. * @return void * @throws \PHPUnit\Exception */ public function options($url) { $this->_sendRequest($url, 'OPTIONS'); } /** * Creates and send the request into a Dispatcher instance. * * Receives and stores the response for future inspection. * * @param string|array $url The URL * @param string $method The HTTP method * @param string|array|null $data The request data. * @return void * @throws \PHPUnit\Exception */ protected function _sendRequest($url, $method, $data = []) { $dispatcher = $this->_makeDispatcher(); $url = $dispatcher->resolveUrl($url); try { $request = $this->_buildRequest($url, $method, $data); $response = $dispatcher->execute($request); $this->_requestSession = $request['session']; if ($this->_retainFlashMessages && $this->_flashMessages) { $this->_requestSession->write('Flash', $this->_flashMessages); } $this->_response = $response; } catch (PhpUnitException $e) { throw $e; } catch (DatabaseException $e) { throw $e; } catch (LogicException $e) { throw $e; } catch (Exception $e) { $this->_exception = $e; // Simulate the global exception handler being invoked. $this->_handleError($e); } } /** * Get the correct dispatcher instance. * * @return \Cake\TestSuite\MiddlewareDispatcher|\Cake\TestSuite\LegacyRequestDispatcher A dispatcher instance */ protected function _makeDispatcher() { if ($this->_useHttpServer) { return new MiddlewareDispatcher($this, $this->_appClass, $this->_appArgs); } return new LegacyRequestDispatcher($this); } /** * Adds additional event spies to the controller/view event manager. * * @param \Cake\Event\Event $event A dispatcher event. * @param \Cake\Controller\Controller|null $controller Controller instance. * @return void */ public function controllerSpy($event, $controller = null) { if (!$controller) { /** @var \Cake\Controller\Controller $controller */ $controller = $event->getSubject(); } $this->_controller = $controller; $events = $controller->getEventManager(); $events->on('View.beforeRender', function ($event, $viewFile) use ($controller) { if (!$this->_viewName) { $this->_viewName = $viewFile; } if ($this->_retainFlashMessages) { $this->_flashMessages = $controller->getRequest()->getSession()->read('Flash'); } }); $events->on('View.beforeLayout', function ($event, $viewFile) { $this->_layoutName = $viewFile; }); } /** * Attempts to render an error response for a given exception. * * This method will attempt to use the configured exception renderer. * If that class does not exist, the built-in renderer will be used. * * @param \Exception $exception Exception to handle. * @return void * @throws \Exception */ protected function _handleError($exception) { $class = Configure::read('Error.exceptionRenderer'); if (empty($class) || !class_exists($class)) { $class = 'Cake\Error\ExceptionRenderer'; } /** @var \Cake\Error\ExceptionRenderer $instance */ $instance = new $class($exception); $this->_response = $instance->render(); } /** * Creates a request object with the configured options and parameters. * * @param string|array $url The URL * @param string $method The HTTP method * @param string|array|null $data The request data. * @return array The request context */ protected function _buildRequest($url, $method, $data) { $sessionConfig = (array)Configure::read('Session') + [ 'defaults' => 'php', ]; $session = Session::create($sessionConfig); $session->write($this->_session); list($url, $query, $hostInfo) = $this->_url($url); $tokenUrl = $url; if ($query) { $tokenUrl .= '?' . $query; } parse_str($query, $queryData); $props = [ 'url' => $url, 'session' => $session, 'query' => $queryData, 'files' => [], ]; if (is_string($data)) { $props['input'] = $data; } if (!isset($props['input'])) { $data = $this->_addTokens($tokenUrl, $data); $props['post'] = $this->_castToString($data); } $props['cookies'] = $this->_cookie; $env = [ 'REQUEST_METHOD' => $method, 'QUERY_STRING' => $query, 'REQUEST_URI' => $url, ]; if (!empty($hostInfo['ssl'])) { $env['HTTPS'] = 'on'; } if (isset($hostInfo['host'])) { $env['HTTP_HOST'] = $hostInfo['host']; } if (isset($this->_request['headers'])) { foreach ($this->_request['headers'] as $k => $v) { $name = strtoupper(str_replace('-', '_', $k)); if (!in_array($name, ['CONTENT_LENGTH', 'CONTENT_TYPE'])) { $name = 'HTTP_' . $name; } $env[$name] = $v; } unset($this->_request['headers']); } $props['environment'] = $env; $props = Hash::merge($props, $this->_request); return $props; } /** * Add the CSRF and Security Component tokens if necessary. * * @param string $url The URL the form is being submitted on. * @param array $data The request body data. * @return array The request body with tokens added. */ protected function _addTokens($url, $data) { if ($this->_securityToken === true) { $keys = array_map(function ($field) { return preg_replace('/(\.\d+)+$/', '', $field); }, array_keys(Hash::flatten($data))); $tokenData = $this->_buildFieldToken($url, array_unique($keys)); $data['_Token'] = $tokenData; $data['_Token']['debug'] = 'SecurityComponent debug data would be added here'; } if ($this->_csrfToken === true) { if (!isset($this->_cookie['csrfToken'])) { $this->_cookie['csrfToken'] = Text::uuid(); } if (!isset($data['_csrfToken'])) { $data['_csrfToken'] = $this->_cookie['csrfToken']; } } return $data; } /** * Recursively casts all data to string as that is how data would be POSTed in * the real world * * @param array $data POST data * @return array */ protected function _castToString($data) { foreach ($data as $key => $value) { if (is_scalar($value)) { $data[$key] = $value === false ? '0' : (string)$value; continue; } if (is_array($value)) { $looksLikeFile = isset($value['error'], $value['tmp_name'], $value['size']); if ($looksLikeFile) { continue; } $data[$key] = $this->_castToString($value); } } return $data; } /** * Creates a valid request url and parameter array more like Request::_url() * * @param string|array $url The URL * @return array Qualified URL, the query parameters, and host data */ protected function _url($url) { $uri = new Uri($url); $path = $uri->getPath(); $query = $uri->getQuery(); $hostData = []; if ($uri->getHost()) { $hostData['host'] = $uri->getHost(); } if ($uri->getScheme()) { $hostData['ssl'] = $uri->getScheme() === 'https'; } return [$path, $query, $hostData]; } /** * Get the response body as string * * @return string The response body. */ protected function _getBodyAsString() { if (!$this->_response) { $this->fail('No response set, cannot assert content.'); } return (string)$this->_response->getBody(); } /** * Fetches a view variable by name. * * If the view variable does not exist, null will be returned. * * @param string $name The view variable to get. * @return mixed The view variable if set. */ public function viewVariable($name) { if (empty($this->_controller->viewVars)) { $this->fail('There are no view variables, perhaps you need to run a request?'); } if (isset($this->_controller->viewVars[$name])) { return $this->_controller->viewVars[$name]; } return null; } /** * Asserts that the response status code is in the 2xx range. * * @param string $message Custom message for failure. * @return void */ public function assertResponseOk($message = null) { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat(null, new StatusOk($this->_response), $verboseMessage); } /** * Asserts that the response status code is in the 2xx/3xx range. * * @param string $message Custom message for failure. * @return void */ public function assertResponseSuccess($message = null) { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat(null, new StatusSuccess($this->_response), $verboseMessage); } /** * Asserts that the response status code is in the 4xx range. * * @param string $message Custom message for failure. * @return void */ public function assertResponseError($message = null) { $this->assertThat(null, new StatusError($this->_response), $message); } /** * Asserts that the response status code is in the 5xx range. * * @param string $message Custom message for failure. * @return void */ public function assertResponseFailure($message = null) { $this->assertThat(null, new StatusFailure($this->_response), $message); } /** * Asserts a specific response status code. * * @param int $code Status code to assert. * @param string $message Custom message for failure. * @return void */ public function assertResponseCode($code, $message = null) { $this->assertThat($code, new StatusCode($this->_response), $message); } /** * Asserts that the Location header is correct. * * @param string|array|null $url The URL you expected the client to go to. This * can either be a string URL or an array compatible with Router::url(). Use null to * simply check for the existence of this header. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertRedirect($url = null, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat(null, new HeaderSet($this->_response, 'Location'), $verboseMessage); if ($url) { $this->assertThat(Router::url($url, ['_full' => true]), new HeaderEquals($this->_response, 'Location'), $verboseMessage); } } /** * Asserts that the Location header contains a substring * * @param string $url The URL you expected the client to go to. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertRedirectContains($url, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat(null, new HeaderSet($this->_response, 'Location'), $verboseMessage); $this->assertThat($url, new HeaderContains($this->_response, 'Location'), $verboseMessage); } /** * Asserts that the Location header does not contain a substring * * @param string $url The URL you expected the client to go to. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertRedirectNotContains($url, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat(null, new HeaderSet($this->_response, 'Location'), $verboseMessage); $this->assertThat($url, new HeaderNotContains($this->_response, 'Location'), $verboseMessage); } /** * Asserts that the Location header is not set. * * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertNoRedirect($message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat(null, new HeaderNotSet($this->_response, 'Location'), $verboseMessage); } /** * Asserts response headers * * @param string $header The header to check * @param string $content The content to check for. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertHeader($header, $content, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat(null, new HeaderSet($this->_response, $header), $verboseMessage); $this->assertThat($content, new HeaderEquals($this->_response, $header), $verboseMessage); } /** * Asserts response header contains a string * * @param string $header The header to check * @param string $content The content to check for. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertHeaderContains($header, $content, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat(null, new HeaderSet($this->_response, $header), $verboseMessage); $this->assertThat($content, new HeaderContains($this->_response, $header), $verboseMessage); } /** * Asserts response header does not contain a string * * @param string $header The header to check * @param string $content The content to check for. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertHeaderNotContains($header, $content, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat(null, new HeaderSet($this->_response, $header), $verboseMessage); $this->assertThat($content, new HeaderNotContains($this->_response, $header), $verboseMessage); } /** * Asserts content type * * @param string $type The content-type to check for. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertContentType($type, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($type, new ContentType($this->_response), $verboseMessage); } /** * Asserts content in the response body equals. * * @param mixed $content The content to check for. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertResponseEquals($content, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($content, new BodyEquals($this->_response), $verboseMessage); } /** * Asserts content in the response body not equals. * * @param mixed $content The content to check for. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertResponseNotEquals($content, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($content, new BodyNotEquals($this->_response), $verboseMessage); } /** * Asserts content exists in the response body. * * @param string $content The content to check for. * @param string $message The failure message that will be appended to the generated message. * @param bool $ignoreCase A flag to check whether we should ignore case or not. * @return void */ public function assertResponseContains($content, $message = '', $ignoreCase = false) { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($content, new BodyContains($this->_response, $ignoreCase), $verboseMessage); } /** * Asserts content does not exist in the response body. * * @param string $content The content to check for. * @param string $message The failure message that will be appended to the generated message. * @param bool $ignoreCase A flag to check whether we should ignore case or not. * @return void */ public function assertResponseNotContains($content, $message = '', $ignoreCase = false) { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($content, new BodyNotContains($this->_response, $ignoreCase), $verboseMessage); } /** * Asserts that the response body matches a given regular expression. * * @param string $pattern The pattern to compare against. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertResponseRegExp($pattern, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($pattern, new BodyRegExp($this->_response), $verboseMessage); } /** * Asserts that the response body does not match a given regular expression. * * @param string $pattern The pattern to compare against. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertResponseNotRegExp($pattern, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($pattern, new BodyNotRegExp($this->_response), $verboseMessage); } /** * Assert response content is not empty. * * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertResponseNotEmpty($message = '') { $this->assertThat(null, new BodyNotEmpty($this->_response), $message); } /** * Assert response content is empty. * * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertResponseEmpty($message = '') { $this->assertThat(null, new BodyEmpty($this->_response), $message); } /** * Asserts that the search string was in the template name. * * @param string $content The content to check for. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertTemplate($content, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($content, new TemplateFileEquals($this->_viewName), $verboseMessage); } /** * Asserts that the search string was in the layout name. * * @param string $content The content to check for. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertLayout($content, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($content, new LayoutFileEquals($this->_layoutName), $verboseMessage); } /** * Asserts session contents * * @param string $expected The expected contents. * @param string $path The session data path. Uses Hash::get() compatible notation * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertSession($expected, $path, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($expected, new SessionEquals($this->_requestSession, $path), $verboseMessage); } /** * Asserts a flash message was set * * @param string $expected Expected message * @param string $key Flash key * @param string $message Assertion failure message * @return void */ public function assertFlashMessage($expected, $key = 'flash', $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($expected, new FlashParamEquals($this->_requestSession, $key, 'message'), $verboseMessage); } /** * Asserts a flash message was set at a certain index * * @param int $at Flash index * @param string $expected Expected message * @param string $key Flash key * @param string $message Assertion failure message * @return void */ public function assertFlashMessageAt($at, $expected, $key = 'flash', $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($expected, new FlashParamEquals($this->_requestSession, $key, 'message', $at), $verboseMessage); } /** * Asserts a flash element was set * * @param string $expected Expected element name * @param string $key Flash key * @param string $message Assertion failure message * @return void */ public function assertFlashElement($expected, $key = 'flash', $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($expected, new FlashParamEquals($this->_requestSession, $key, 'element'), $verboseMessage); } /** * Asserts a flash element was set at a certain index * * @param int $at Flash index * @param string $expected Expected element name * @param string $key Flash key * @param string $message Assertion failure message * @return void */ public function assertFlashElementAt($at, $expected, $key = 'flash', $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($expected, new FlashParamEquals($this->_requestSession, $key, 'element', $at), $verboseMessage); } /** * Asserts cookie values * * @param string $expected The expected contents. * @param string $name The cookie name. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertCookie($expected, $name, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($name, new CookieSet($this->_response), $verboseMessage); $this->assertThat($expected, new CookieEquals($this->_response, $name), $verboseMessage); } /** * Asserts a cookie has not been set in the response * * @param string $cookie The cookie name to check * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertCookieNotSet($cookie, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($cookie, new CookieNotSet($this->_response), $verboseMessage); } /** * Disable the error handler middleware. * * By using this function, exceptions are no longer caught by the ErrorHandlerMiddleware * and are instead re-thrown by the TestExceptionRenderer. This can be helpful * when trying to diagnose/debug unexpected failures in test cases. * * @return void */ public function disableErrorHandlerMiddleware() { Configure::write('Error.exceptionRenderer', TestExceptionRenderer::class); } /** * Asserts cookie values which are encrypted by the * CookieComponent. * * The difference from assertCookie() is this decrypts the cookie * value like the CookieComponent for this assertion. * * @param string $expected The expected contents. * @param string $name The cookie name. * @param string|bool $encrypt Encryption mode to use. * @param string|null $key Encryption key used. Defaults * to Security.salt. * @param string $message The failure message that will be appended to the generated message. * @return void * @see \Cake\Utility\CookieCryptTrait::_encrypt() */ public function assertCookieEncrypted($expected, $name, $encrypt = 'aes', $key = null, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat($name, new CookieSet($this->_response), $verboseMessage); $this->_cookieEncryptionKey = $key; $this->assertThat($expected, new CookieEncryptedEquals($this->_response, $name, $encrypt, $this->_getCookieEncryptionKey())); } /** * Asserts that a file with the given name was sent in the response * * @param string $expected The absolute file path that should be sent in the response. * @param string $message The failure message that will be appended to the generated message. * @return void */ public function assertFileResponse($expected, $message = '') { $verboseMessage = $this->extractVerboseMessage($message); $this->assertThat(null, new FileSent($this->_response), $verboseMessage); $this->assertThat($expected, new FileSentAs($this->_response), $verboseMessage); } /** * Inspect controller to extract possible causes of the failed assertion * * @param string $message Original message to use as a base * @return null|string */ protected function extractVerboseMessage($message = null) { if ($this->_exception instanceof \Exception) { $message .= $this->extractExceptionMessage($this->_exception); } if ($this->_controller === null) { return $message; } $error = Hash::get($this->_controller->viewVars, 'error'); if ($error instanceof \Exception) { $message .= $this->extractExceptionMessage($this->viewVariable('error')); } return $message; } /** * Extract verbose message for existing exception * * @param \Exception $exception Exception to extract * @return string */ protected function extractExceptionMessage(\Exception $exception) { return PHP_EOL . sprintf('Possibly related to %s: "%s" ', get_class($exception), $exception->getMessage()) . PHP_EOL . $exception->getTraceAsString(); } }