Skip to content

Commit 5073758

Browse files
authored
Merge pull request #545 from flightphp/output-buffering-correction
Output buffering correction
2 parents c4c6e48 + 4d064cb commit 5073758

19 files changed

+818
-118
lines changed

composer.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,15 @@
5050
"config": {
5151
"allow-plugins": {
5252
"phpstan/extension-installer": true
53-
}
53+
},
54+
"process-timeout": 0,
55+
"sort-packages": true
5456
},
5557
"scripts": {
5658
"test": "phpunit",
5759
"test-coverage": "rm clover.xml && XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage --coverage-clover=clover.xml && vendor/bin/coverage-check clover.xml 100",
60+
"test-server": "echo \"Running Test Server\" && php -S localhost:8000 -t tests/server/",
61+
"test-server-v2": "echo \"Running Test Server\" && php -S localhost:8000 -t tests/server-v2/",
5862
"test-coverage:win": "del clover.xml && phpunit --coverage-html=coverage --coverage-clover=clover.xml && coverage-check clover.xml 100",
5963
"lint": "phpstan --no-progress -cphpstan.neon",
6064
"beautify": "phpcbf --standard=phpcs.xml",

flight/Engine.php

Lines changed: 115 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* # Core methods
2828
* @method void start() Starts engine
2929
* @method void stop() Stops framework and outputs current response
30-
* @method void halt(int $code = 200, string $message = '') Stops processing and returns a given response.
30+
* @method void halt(int $code = 200, string $message = '', bool $actuallyExit = true) Stops processing and returns a given response.
3131
*
3232
* # Routing
3333
* @method Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '')
@@ -66,6 +66,15 @@
6666
*/
6767
class Engine
6868
{
69+
/**
70+
* @var array<string> List of methods that can be extended in the Engine class.
71+
*/
72+
private const MAPPABLE_METHODS = [
73+
'start', 'stop', 'route', 'halt', 'error', 'notFound',
74+
'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonp',
75+
'post', 'put', 'patch', 'delete', 'group', 'getUrl'
76+
];
77+
6978
/** @var array<string, mixed> Stored variables. */
7079
protected array $vars = [];
7180

@@ -137,14 +146,7 @@ public function init(): void
137146
$view->extension = $self->get('flight.views.extension');
138147
});
139148

140-
// Register framework methods
141-
$methods = [
142-
'start', 'stop', 'route', 'halt', 'error', 'notFound',
143-
'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonp',
144-
'post', 'put', 'patch', 'delete', 'group', 'getUrl',
145-
];
146-
147-
foreach ($methods as $name) {
149+
foreach (self::MAPPABLE_METHODS as $name) {
148150
$this->dispatcher->set($name, [$this, "_$name"]);
149151
}
150152

@@ -156,6 +158,7 @@ public function init(): void
156158
$this->set('flight.views.path', './views');
157159
$this->set('flight.views.extension', '.php');
158160
$this->set('flight.content_length', true);
161+
$this->set('flight.v2.output_buffering', false);
159162

160163
// Startup configuration
161164
$this->before('start', function () use ($self) {
@@ -169,6 +172,10 @@ public function init(): void
169172
$self->router()->case_sensitive = $self->get('flight.case_sensitive');
170173
// Set Content-Length
171174
$self->response()->content_length = $self->get('flight.content_length');
175+
// This is to maintain legacy handling of output buffering
176+
// which causes a lot of problems. This will be removed
177+
// in v4
178+
$self->response()->v2_output_buffering = $this->get('flight.v2.output_buffering');
172179
});
173180

174181
$this->initialized = true;
@@ -354,8 +361,67 @@ public function path(string $dir): void
354361
$this->loader->addDirectory($dir);
355362
}
356363

357-
// Extensible Methods
364+
/**
365+
* Processes each routes middleware.
366+
*
367+
* @param array<int, callable> $middleware Middleware attached to the route.
368+
* @param array<mixed> $params `$route->params`.
369+
* @param string $event_name If this is the before or after method.
370+
*/
371+
protected function processMiddleware(array $middleware, array $params, string $event_name): bool
372+
{
373+
$at_least_one_middleware_failed = false;
374+
375+
foreach ($middleware as $middleware) {
376+
$middleware_object = false;
377+
378+
if ($event_name === 'before') {
379+
// can be a callable or a class
380+
$middleware_object = (is_callable($middleware) === true
381+
? $middleware
382+
: (method_exists($middleware, 'before') === true
383+
? [$middleware, 'before']
384+
: false
385+
)
386+
);
387+
} elseif ($event_name === 'after') {
388+
// must be an object. No functions allowed here
389+
if (
390+
is_object($middleware) === true
391+
&& !($middleware instanceof Closure)
392+
&& method_exists($middleware, 'after') === true
393+
) {
394+
$middleware_object = [$middleware, 'after'];
395+
}
396+
}
397+
398+
if ($middleware_object === false) {
399+
continue;
400+
}
401+
402+
if ($this->response()->v2_output_buffering === false) {
403+
ob_start();
404+
}
405+
406+
// It's assumed if you don't declare before, that it will be assumed as the before method
407+
$middleware_result = $middleware_object($params);
358408

409+
if ($this->response()->v2_output_buffering === false) {
410+
$this->response()->write(ob_get_clean());
411+
}
412+
413+
if ($middleware_result === false) {
414+
$at_least_one_middleware_failed = true;
415+
break;
416+
}
417+
}
418+
419+
return $at_least_one_middleware_failed;
420+
}
421+
422+
////////////////////////
423+
// Extensible Methods //
424+
////////////////////////
359425
/**
360426
* Starts the framework.
361427
*
@@ -374,16 +440,20 @@ public function _start(): void
374440
$self->stop();
375441
});
376442

377-
// Flush any existing output
378-
if (ob_get_length() > 0) {
379-
$response->write(ob_get_clean()); // @codeCoverageIgnore
380-
}
443+
if ($response->v2_output_buffering === true) {
444+
// Flush any existing output
445+
if (ob_get_length() > 0) {
446+
$response->write(ob_get_clean()); // @codeCoverageIgnore
447+
}
381448

382-
// Enable output buffering
383-
ob_start();
449+
// Enable output buffering
450+
// This is closed in the Engine->_stop() method
451+
ob_start();
452+
}
384453

385454
// Route the request
386455
$failed_middleware_check = false;
456+
387457
while ($route = $router->route($request)) {
388458
$params = array_values($route->params);
389459

@@ -394,60 +464,39 @@ public function _start(): void
394464

395465
// Run any before middlewares
396466
if (count($route->middleware) > 0) {
397-
foreach ($route->middleware as $middleware) {
398-
$middleware_object = (is_callable($middleware) === true
399-
? $middleware
400-
: (method_exists($middleware, 'before') === true
401-
? [$middleware, 'before']
402-
: false));
403-
404-
if ($middleware_object === false) {
405-
continue;
406-
}
407-
408-
// It's assumed if you don't declare before, that it will be assumed as the before method
409-
$middleware_result = $middleware_object($route->params);
410-
411-
if ($middleware_result === false) {
412-
$failed_middleware_check = true;
413-
break 2;
414-
}
467+
$at_least_one_middleware_failed = $this->processMiddleware($route->middleware, $route->params, 'before');
468+
if ($at_least_one_middleware_failed === true) {
469+
$failed_middleware_check = true;
470+
break;
415471
}
416472
}
417473

474+
if ($response->v2_output_buffering === false) {
475+
ob_start();
476+
}
477+
418478
// Call route handler
419479
$continue = $this->dispatcher->execute(
420480
$route->callback,
421481
$params
422482
);
423483

484+
if ($response->v2_output_buffering === false) {
485+
$response->write(ob_get_clean());
486+
}
424487

425488
// Run any before middlewares
426489
if (count($route->middleware) > 0) {
427490
// process the middleware in reverse order now
428-
foreach (array_reverse($route->middleware) as $middleware) {
429-
// must be an object. No functions allowed here
430-
$middleware_object = false;
431-
432-
if (
433-
is_object($middleware) === true
434-
&& !($middleware instanceof Closure)
435-
&& method_exists($middleware, 'after') === true
436-
) {
437-
$middleware_object = [$middleware, 'after'];
438-
}
439-
440-
// has to have the after method, otherwise just skip it
441-
if ($middleware_object === false) {
442-
continue;
443-
}
444-
445-
$middleware_result = $middleware_object($route->params);
446-
447-
if ($middleware_result === false) {
448-
$failed_middleware_check = true;
449-
break 2;
450-
}
491+
$at_least_one_middleware_failed = $this->processMiddleware(
492+
array_reverse($route->middleware),
493+
$route->params,
494+
'after'
495+
);
496+
497+
if ($at_least_one_middleware_failed === true) {
498+
$failed_middleware_check = true;
499+
break;
451500
}
452501
}
453502

@@ -463,7 +512,7 @@ public function _start(): void
463512
}
464513

465514
if ($failed_middleware_check === true) {
466-
$this->halt(403, 'Forbidden');
515+
$this->halt(403, 'Forbidden', empty(getenv('PHPUNIT_TEST')));
467516
} elseif ($dispatched === false) {
468517
$this->notFound();
469518
}
@@ -514,8 +563,9 @@ public function _stop(?int $code = null): void
514563
$response->status($code);
515564
}
516565

517-
$content = ob_get_clean();
518-
$response->write($content ?: '');
566+
if ($response->v2_output_buffering === true && ob_get_length() > 0) {
567+
$response->write(ob_get_clean());
568+
}
519569

520570
$response->send();
521571
}
@@ -599,16 +649,16 @@ public function _delete(string $pattern, callable $callback, bool $pass_route =
599649
*
600650
* @param int $code HTTP status code
601651
* @param string $message Response message
652+
* @param bool $actuallyExit Whether to actually exit the script or just send response
602653
*/
603-
public function _halt(int $code = 200, string $message = ''): void
654+
public function _halt(int $code = 200, string $message = '', bool $actuallyExit = true): void
604655
{
605656
$this->response()
606657
->clear()
607658
->status($code)
608659
->write($message)
609660
->send();
610-
// apologies for the crappy hack here...
611-
if ($message !== 'skip---exit') {
661+
if ($actuallyExit === true) {
612662
exit(); // @codeCoverageIgnore
613663
}
614664
}
@@ -742,7 +792,7 @@ public function _etag(string $id, string $type = 'strong'): void
742792
isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
743793
$_SERVER['HTTP_IF_NONE_MATCH'] === $id
744794
) {
745-
$this->halt(304);
795+
$this->halt(304, '', empty(getenv('PHPUNIT_TEST')));
746796
}
747797
}
748798

@@ -759,7 +809,7 @@ public function _lastModified(int $time): void
759809
isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
760810
strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time
761811
) {
762-
$this->halt(304);
812+
$this->halt(304, '', empty(getenv('PHPUNIT_TEST')));
763813
}
764814
}
765815

flight/Flight.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* @method static void start() Starts the framework.
2323
* @method static void path(string $path) Adds a path for autoloading classes.
2424
* @method static void stop(?int $code = null) Stops the framework and sends a response.
25-
* @method static void halt(int $code = 200, string $message = '')
25+
* @method static void halt(int $code = 200, string $message = '', bool $actuallyExit = true)
2626
* Stop the framework with an optional status code and message.
2727
*
2828
* # Routing

flight/database/PdoWrapper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,6 @@ protected function processInStatementSql(string $sql, array $params = []): array
136136
$current_index += strlen($question_marks) + 4;
137137
}
138138

139-
return [ 'sql' => $sql, 'params' => $params ];
139+
return ['sql' => $sql, 'params' => $params];
140140
}
141141
}

flight/net/Request.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,49 @@ public static function getHeaders(): array
331331
return $headers;
332332
}
333333

334+
/**
335+
* Alias of Request->getHeader(). Gets a single header.
336+
*
337+
* @param string $header Header name. Can be caps, lowercase, or mixed.
338+
* @param string $default Default value if the header does not exist
339+
*
340+
* @return string
341+
*/
342+
public static function header(string $header, $default = '')
343+
{
344+
return self::getHeader($header, $default);
345+
}
346+
347+
/**
348+
* Alias of Request->getHeaders(). Gets all the request headers
349+
*
350+
* @return array<string, string|int>
351+
*/
352+
public static function headers(): array
353+
{
354+
return self::getHeaders();
355+
}
356+
357+
/**
358+
* Gets the full request URL.
359+
*
360+
* @return string URL
361+
*/
362+
public function getFullUrl(): string
363+
{
364+
return $this->scheme . '://' . $this->host . $this->url;
365+
}
366+
367+
/**
368+
* Grabs the scheme and host. Does not end with a /
369+
*
370+
* @return string
371+
*/
372+
public function getBaseUrl(): string
373+
{
374+
return $this->scheme . '://' . $this->host;
375+
}
376+
334377
/**
335378
* Parse query parameters from a URL.
336379
*

0 commit comments

Comments
 (0)