Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
use Closure;
use DI\Container;
use Illuminate\Support\Collection;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Rareloop\Lumberjack\Http\ResponseEmitter;

class Application implements ContainerInterface
{
Expand Down Expand Up @@ -282,14 +282,31 @@ public function shutdown(?ResponseInterface $response = null)
// If we're handling a WordPressController response at this point then WordPress will already have
// sent headers as it happens earlier in the lifecycle. For this scenario we need to do a bit more
// work to make sure that duplicate headers are not sent back.
(new SapiEmitter())->emit($this->removeSentHeadersAndMoveIntoResponse($response));
$responseToSend = $this->removeSentHeadersAndMoveIntoResponse($response);

$this->get(ResponseEmitter::class)->emit($responseToSend);
}

$this->terminate();
}

/**
* Terminate the application execution.
*
* This method is a wrapper around die() to allow for easier unit testing by mocking.
*
* @return void
*/
protected function terminate()
{
die();
}

protected function removeSentHeadersAndMoveIntoResponse(ResponseInterface $response): ResponseInterface
{
if (headers_sent()) {
return $response;
}
// 1. Format the previously sent headers into an array of [key, value]
// 2. Remove all headers from the output that we find
// 3. Filter out any headers that would clash with those already in the response
Expand Down
12 changes: 6 additions & 6 deletions src/Bootstrappers/RegisterExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
use Error;
use ErrorException;
use DI\NotFoundException;
use function Http\Response\send;
use Rareloop\Router\Responsable;
use Rareloop\Lumberjack\Application;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Rareloop\Lumberjack\Http\ResponseEmitter;
use Rareloop\Lumberjack\Exceptions\HandlerInterface;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\ErrorHandler\Error\FatalError;
Expand All @@ -25,12 +24,13 @@

class RegisterExceptionHandler
{
private $app;
public function __construct(private Application $app, private ResponseEmitter $emitter)
{
//
}

public function bootstrap(Application $app)
{
$this->app = $app;

if (is_admin()) {
return;
}
Expand Down Expand Up @@ -72,7 +72,7 @@ public function handleException($e)

public function send(ResponseInterface $response)
{
@(new SapiEmitter())->emit($response);
$this->emitter->emit($response);
}

protected function getExceptionHandler(): HandlerInterface
Expand Down
28 changes: 28 additions & 0 deletions src/Http/ResponseEmitter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Rareloop\Lumberjack\Http;

use Psr\Http\Message\ResponseInterface;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;

class ResponseEmitter
{
/**
* Emit a response.
*
* If headers have already been sent, the response body is echoed directly to avoid
* an EmitterException from the underlying SapiEmitter.
*
* @param ResponseInterface $response
* @return void
*/
public function emit(ResponseInterface $response)
{
if (headers_sent()) {
echo (string)$response->getBody();
return;
}

(new SapiEmitter())->emit($response);
}
}
2 changes: 1 addition & 1 deletion src/Providers/SessionServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function boot()
$cookieSet = false;

add_action('send_headers', function () use (&$cookieSet) {
if (!$cookieSet) {
if (!$cookieSet && !headers_sent()) {
$cookieOptions = [
'lifetime' => Config::get('session.lifetime', 120),
'path' => Config::get('session.path', '/'),
Expand Down
77 changes: 37 additions & 40 deletions tests/Unit/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
use PHPUnit\Framework\TestCase;
use Rareloop\Lumberjack\Application;
use Rareloop\Lumberjack\Providers\ServiceProvider;
use Rareloop\Lumberjack\Test\Unit\BrainMonkeyPHPUnitIntegration;
use phpmock\Mock;
use phpmock\MockBuilder;
use Brain\Monkey;
use Laminas\Diactoros\Response\TextResponse;
use Rareloop\Lumberjack\Http\ResponseEmitter;

class ApplicationTest extends TestCase
{
Expand Down Expand Up @@ -548,79 +549,75 @@ public function calling_detectWhenRequestHasNotBeenHandled_adds_actions()
$this->assertTrue(has_action('wp_footer'));
$this->assertTrue(has_action('shutdown'));
}

/**
* @test
*/
public function shutdown_should_use_emitter_to_output_response()
{
$wp = Mockery::mock('WP');
$wp->shouldReceive('send_headers')->once();
$GLOBALS['wp'] = $wp;

$response = new TextResponse('Hello World');

$emitter = Mockery::mock(ResponseEmitter::class);
$emitter->shouldReceive('emit')->once()->with(Mockery::type(\Psr\Http\Message\ResponseInterface::class));

$app = Mockery::mock(Application::class . '[terminate]');
$app->shouldAllowMockingProtectedMethods();
$app->shouldReceive('terminate')->once();
$app->bind(ResponseEmitter::class, $emitter);

$app->shutdown($response);
}
}

class BootstrapperBootstrapTester
{
public function __construct(public $callback)
{
}
public function __construct(public $callback) {}
}

abstract class TestBootstrapperBase
{
public function __construct(private BootstrapperBootstrapTester $tester)
{
}
public function __construct(private BootstrapperBootstrapTester $tester) {}

public function bootstrap(Application $app)
{
call_user_func($this->tester->callback);
}
}

class TestBootstrapper1 extends TestBootstrapperBase
{
}
class TestBootstrapper1 extends TestBootstrapperBase {}

class TestBootstrapper2 extends TestBootstrapperBase
{
}
class TestBootstrapper2 extends TestBootstrapperBase {}

interface TestInterface
{
}
interface TestInterface {}

class TestInterfaceImplementation implements TestInterface
{
}
class TestInterfaceImplementation implements TestInterface {}

class TestInterfaceImplementationWithConstructorParams implements TestInterface
{
public function __construct(TestServiceProvider $provider)
{
}
public function __construct(TestServiceProvider $provider) {}
}

interface TestSubInterface
{
}
interface TestSubInterface {}

class TestSubInterfaceImplementation implements TestSubInterface
{
}
class TestSubInterfaceImplementation implements TestSubInterface {}

class TestServiceProvider extends ServiceProvider
{
public function register()
{
}
public function boot()
{
}
public function register() {}
public function boot() {}
}

class EmptyServiceProvider extends ServiceProvider
{
}
class EmptyServiceProvider extends ServiceProvider {}

class TestBootServiceProvider extends ServiceProvider
{
private $bootCallback;

public function register()
{
}
public function register() {}

public function boot(Application $app, TestInterface $test)
{
Expand Down
32 changes: 23 additions & 9 deletions tests/Unit/Bootstrappers/RegisterExceptionHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Rareloop\Lumberjack\Application;
use Rareloop\Lumberjack\Bootstrappers\RegisterExceptionHandler;
use Rareloop\Lumberjack\Config;
use Rareloop\Lumberjack\Http\ResponseEmitter;
use Rareloop\Lumberjack\Exceptions\Handler;
use Rareloop\Lumberjack\Exceptions\HandlerInterface;
use Rareloop\Lumberjack\Test\Unit\BrainMonkeyPHPUnitIntegration;
Expand Down Expand Up @@ -37,7 +38,7 @@ public function errors_are_converted_to_exceptions()
$app->bind('config', $config);
$app->bind(Config::class, $config);

$bootstrapper = new RegisterExceptionHandler();
$bootstrapper = new RegisterExceptionHandler($app, new ResponseEmitter());
$bootstrapper->bootstrap($app);
$bootstrapper->handleError(E_USER_ERROR, 'Test Error');
}
Expand All @@ -60,7 +61,7 @@ public function E_USER_NOTICE_errors_are_not_converted_to_exceptions()
return $e->getSeverity() === E_USER_NOTICE && $e->getMessage() === 'Test Error';
}));

$bootstrapper = new RegisterExceptionHandler();
$bootstrapper = new RegisterExceptionHandler($app, new ResponseEmitter());
$bootstrapper->bootstrap($app);
$bootstrapper->handleError(E_USER_NOTICE, 'Test Error');
}
Expand All @@ -83,7 +84,7 @@ public function E_USER_DEPRECATED_errors_are_not_converted_to_exceptions()
return $e->getSeverity() === E_USER_DEPRECATED && $e->getMessage() === 'Test Error';
}));

$bootstrapper = new RegisterExceptionHandler();
$bootstrapper = new RegisterExceptionHandler($app, new ResponseEmitter());
$bootstrapper->bootstrap($app);
$bootstrapper->handleError(E_USER_DEPRECATED, 'Test Error');
}
Expand All @@ -106,7 +107,7 @@ public function E_DEPRECATED_errors_are_not_converted_to_exceptions()
return $e->getSeverity() === E_DEPRECATED && $e->getMessage() === 'Test Error';
}));

$bootstrapper = new RegisterExceptionHandler();
$bootstrapper = new RegisterExceptionHandler($app, new ResponseEmitter());
$bootstrapper->bootstrap($app);
$bootstrapper->handleError(E_DEPRECATED, 'Test Error');
}
Expand All @@ -131,7 +132,7 @@ public function custom_error_level_can_be_set_for_report_only()
return $e->getSeverity() === E_USER_ERROR && $e->getMessage() === 'Test Error';
}));

$bootstrapper = new RegisterExceptionHandler();
$bootstrapper = new RegisterExceptionHandler($app, new ResponseEmitter());
$bootstrapper->bootstrap($app);
$bootstrapper->handleError(E_USER_ERROR, 'Test Error');
$bootstrapper->handleError(E_USER_DEPRECATED, 'Test Error');
Expand All @@ -153,7 +154,7 @@ public function handle_exception_should_call_handlers_report_and_render_methods(
$handler->shouldReceive('render')->with($request, $exception)->once()->andReturn(new Response());
$app->bind(HandlerInterface::class, $handler);

$bootstrapper = Mockery::mock(RegisterExceptionHandler::class . '[send]');
$bootstrapper = Mockery::mock(RegisterExceptionHandler::class . '[send]', [$app, new ResponseEmitter()]);
$bootstrapper->shouldReceive('send')->once();
$bootstrapper->bootstrap($app);

Expand All @@ -176,7 +177,7 @@ public function handle_exception_should_call_handlers_report_and_render_methods_
$handler->shouldReceive('render')->with($request, Mockery::type(\ErrorException::class))->once()->andReturn(new Response());
$app->bind(HandlerInterface::class, $handler);

$bootstrapper = Mockery::mock(RegisterExceptionHandler::class . '[send]');
$bootstrapper = Mockery::mock(RegisterExceptionHandler::class . '[send]', [$app, new ResponseEmitter()]);
$bootstrapper->shouldReceive('send')->once();
$bootstrapper->bootstrap($app);

Expand All @@ -197,7 +198,7 @@ public function handle_exception_should_call_handlers_report_and_render_methods_
$handler->shouldReceive('render')->with(Mockery::type(ServerRequest::class), $exception)->once()->andReturn(new Response());
$app->bind(HandlerInterface::class, $handler);

$bootstrapper = Mockery::mock(RegisterExceptionHandler::class . '[send]');
$bootstrapper = Mockery::mock(RegisterExceptionHandler::class . '[send]', [$app, new ResponseEmitter()]);
$bootstrapper->shouldReceive('send')->once();
$bootstrapper->bootstrap($app);

Expand All @@ -222,12 +223,25 @@ public function handle_exception_should_not_call_render_methods_when_exception_i
$handler->shouldNotReceive('render');
$app->bind(HandlerInterface::class, $handler);

$bootstrapper = Mockery::mock(RegisterExceptionHandler::class . '[send]');
$bootstrapper = Mockery::mock(RegisterExceptionHandler::class . '[send]', [$app, new ResponseEmitter()]);
$bootstrapper->shouldReceive('send')->once();
$bootstrapper->bootstrap($app);

$bootstrapper->handleException($exception);
}

/** @test */
public function send_should_use_the_emitter()
{
$app = new Application;
$response = new TextResponse('Hello World');
$emitter = Mockery::mock(ResponseEmitter::class);
$emitter->shouldReceive('emit')->once()->with($response);

$bootstrapper = new RegisterExceptionHandler($app, $emitter);

$bootstrapper->send($response);
}
}

class ResponsableException extends \Exception implements Responsable
Expand Down
Loading
Loading