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 = '')
6666 */
6767class 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
0 commit comments