Browse Source

Add Server and BaseApplication.

These classes tie the various parts of Cake\Http into something useful.
The BaseApplication is used by developers to define their middleware and
bootstrap their application. The Server is used to dispatch requests
into the Application and emit the responses.
Mark Story 10 years ago
parent
commit
91234f42b0

+ 98 - 0
src/Http/BaseApplication.php

@@ -0,0 +1,98 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Http;
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+
+/**
+ * Base class for application classes.
+ *
+ * The application class is responsible for bootstrapping the application,
+ * and ensuring that middleware is attached. It is also invoked as the last piece
+ * of middleware, and delegates request/response handling to the correct controller.
+ */
+abstract class BaseApplication
+{
+
+    /**
+     * @var string Contains the path of the config directory
+     */
+    protected $configDir;
+
+    /**
+     * Constructor
+     *
+     * @param string $configDir The directory the bootstrap configuration is held in.
+     */
+    public function __construct($configDir)
+    {
+        $this->configDir = $configDir;
+    }
+
+    /**
+     * @param \Cake\Http\MiddlewareStack $middleware The middleware stack to set in your App Class
+     * @return \Cake\Http\MiddlewareStack
+     */
+    abstract public function middleware($middleware);
+
+    /**
+     * Load all the application configuration and bootstrap logic.
+     *
+     * @return void
+     */
+    public function bootstrap()
+    {
+        // Load traditional bootstrap file..
+        require_once $this->configDir . '/bootstrap.php';
+
+        // Load other config files your application needs.
+    }
+
+    /**
+     * Invoke the application.
+     *
+     * - Convert the PSR request/response into CakePHP equivalents.
+     * - Create the controller that will handle this request.
+     * - Invoke the controller.
+     *
+     * @param \Psr\Http\Message\ServerRequestInterface $request The request
+     * @param \Psr\Http\Message\ResponseInterface $response The response
+     * @param callable $next The next middleware
+     * @return \Psr\Http\Message\ResponseInterface
+     */
+    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
+    {
+        // Convert the request/response to CakePHP equivalents.
+        $cakeRequest = RequestTransformer::toCake($request);
+        $cakeResponse = ResponseTransformer::toCake($response);
+
+        // Dispatch the request/response to CakePHP
+        $cakeResponse = $this->getDispatcher()->dispatch($cakeRequest, $cakeResponse);
+
+        // Convert the response back into a PSR7 object.
+        return $next($request, ResponseTransformer::toPsr($cakeResponse));
+    }
+
+    /**
+     * Get the ActionDispatcher.
+     *
+     * @return \Spekkoek\ActionDispatcher
+     */
+    protected function getDispatcher()
+    {
+        return new ActionDispatcher();
+    }
+}

+ 142 - 0
src/Http/Server.php

@@ -0,0 +1,142 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Http;
+
+use Cake\Event\EventDispatcherTrait;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use RuntimeException;
+use Zend\Diactoros\Response;
+use Zend\Diactoros\Response\EmitterInterface;
+use Zend\Diactoros\Response\SapiStreamEmitter;
+
+/**
+ * Runs an application invoking all the PSR7 middleware and the registered application.
+ */
+class Server
+{
+
+    use EventDispatcherTrait;
+
+    /**
+     * @var \Cake\Http\BaseApplication
+     */
+    protected $app;
+
+    /**
+     * @var \Cake\Http\Runner
+     */
+    protected $runner;
+
+    /**
+     * Constructor
+     *
+     * @param \Cake\Http\BaseApplication $app The application to use.
+     */
+    public function __construct(BaseApplication $app)
+    {
+        $this->setApp($app);
+        $this->setRunner(new Runner());
+    }
+
+    /**
+     * Run the request/response through the Application and its middleware.
+     *
+     * This will invoke the following methods:
+     *
+     * - App->bootstrap() - Perform any bootstrapping logic for your application here.
+     * - App->middleware() - Attach any application middleware here.
+     * - Trigger the 'Server.buildMiddleware' event. You can use this to modify the
+     *   from event listeners.
+     * - Run the middleware stack including the application.
+     *
+     * @param \Psr\Http\Message\ServerRequestInterface $request  The request to use or null.
+     * @param \Psr\Http\Message\ResponseInterface      $response The response to use or null.
+     * @return \Psr\Http\Message\ResponseInterface
+     * @throws \RuntimeException When the application does not make a response.
+     */
+    public function run(ServerRequestInterface $request = null, ResponseInterface $response = null)
+    {
+        $this->app->bootstrap();
+        $request = $request ?: ServerRequestFactory::fromGlobals();
+        $response = $response ?: new Response();
+
+        $middleware = $this->app->middleware(new MiddlewareStack());
+        if (!($middleware instanceof MiddlewareStack)) {
+            throw new RuntimeException('The application `middleware` method did not return a middleware stack.');
+        }
+        $middleware->push($this->app);
+        $this->dispatchEvent('Server.buildMiddleware', ['middleware' => $middleware]);
+        $response = $this->runner->run($middleware, $request, $response);
+
+        if (!($response instanceof ResponseInterface)) {
+            throw new RuntimeException(sprintf(
+                'Application did not create a response. Got "%s" instead.',
+                is_object($response) ? get_class($response) : $response
+            ));
+        }
+        return $response;
+    }
+
+    /**
+     * Emit the response using the PHP SAPI.
+     *
+     * @param \Psr\Http\Message\ResponseInterface $response The response to emit
+     * @param \Zend\Diactoros\Response\EmitterInterface $emitter The emitter to use.
+     *   When null, a SAPI Stream Emitter will be used.
+     * @return void
+     */
+    public function emit(ResponseInterface $response, EmitterInterface $emitter = null)
+    {
+        if (!$emitter) {
+            $emitter = new SapiStreamEmitter();
+        }
+        $emitter->emit($response);
+    }
+
+    /**
+     * Set the application.
+     *
+     * @param BaseApplication $app The application to set.
+     * @return $this
+     */
+    public function setApp(BaseApplication $app)
+    {
+        $this->app = $app;
+        return $this;
+    }
+
+    /**
+     * Get the current application.
+     *
+     * @return BaseApplication The application that will be run.
+     */
+    public function getApp()
+    {
+        return $this->app;
+    }
+
+    /**
+     * Set the runner
+     *
+     * @param \Cake\Http\Runner $runner The runner to use.
+     * @return $this
+     */
+    public function setRunner(Runner $runner)
+    {
+        $this->runner = $runner;
+        return $this;
+    }
+}

+ 51 - 0
tests/TestCase/Http/BaseApplicationTest.php

@@ -0,0 +1,51 @@
+<?php
+namespace Cake\Test\TestCase;
+
+use Cake\Core\Configure;
+use Cake\Http\BaseApplication;
+use Cake\Http\ServerRequestFactory;
+use Cake\TestSuite\TestCase;
+use Zend\Diactoros\Response;
+
+/**
+ * Base application test.
+ */
+class BaseApplicationTest extends TestCase
+{
+    /**
+     * Setup
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        Configure::write('App.namespace', 'TestApp');
+    }
+
+    /**
+     * Integration test for a simple controller.
+     *
+     * @return void
+     */
+    public function testInvoke()
+    {
+        $next = function ($req, $res) {
+            return $res;
+        };
+        $response = new Response();
+        $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/cakes']);
+        $request = $request->withAttribute('params', [
+            'controller' => 'Cakes',
+            'action' => 'index',
+            'plugin' => null,
+            'pass' => []
+        ]);
+
+        $path = dirname(dirname(__DIR__));
+        $app = $this->getMockForAbstractClass('Cake\Http\BaseApplication', [$path]);
+        $result = $app($request, $response, $next);
+        $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $result);
+        $this->assertEquals('Hello Jane', '' . $result->getBody());
+    }
+}

+ 190 - 0
tests/TestCase/Http/ServerTest.php

@@ -0,0 +1,190 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase;
+
+use Cake\Http\Server;
+use Cake\TestSuite\TestCase;
+use TestApp\Http\BadResponseApplication;
+use TestApp\Http\InvalidMiddlewareApplication;
+use TestApp\Http\MiddlewareApplication;
+use Zend\Diactoros\Response;
+use Zend\Diactoros\ServerRequestFactory;
+
+/**
+ * Server test case
+ */
+class ServerTest extends TestCase
+{
+    /**
+     * Setup
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        $this->server = $_SERVER;
+        $this->config = dirname(dirname(__DIR__));
+    }
+
+    /**
+     * Teardown
+     *
+     * @return void
+     */
+    public function tearDown()
+    {
+        parent::tearDown();
+        $_SERVER = $this->server;
+    }
+
+    /**
+     * test get/set on the app
+     *
+     * @return void
+     */
+    public function testAppGetSet()
+    {
+        $app = $this->getMock('Cake\Http\BaseApplication', [], [$this->config]);
+        $server = new Server($app);
+        $this->assertSame($app, $server->getApp($app));
+    }
+
+    /**
+     * test run building a response
+     *
+     * @return void
+     */
+    public function testRunWithRequestAndResponse()
+    {
+        $response = new Response('php://memory', 200, ['X-testing' => 'source header']);
+        $request = ServerRequestFactory::fromGlobals();
+        $request = $request->withHeader('X-pass', 'request header');
+
+        $app = new MiddlewareApplication($this->config);
+        $server = new Server($app);
+        $res = $server->run($request, $response);
+        $this->assertEquals(
+            'source header',
+            $res->getHeaderLine('X-testing'),
+            'Input response is carried through out middleware'
+        );
+        $this->assertEquals(
+            'request header',
+            $res->getHeaderLine('X-pass'),
+            'Request is used in middleware'
+        );
+    }
+
+    /**
+     * test run building a request from globals.
+     *
+     * @return void
+     */
+    public function testRunWithGlobals()
+    {
+        $_SERVER['HTTP_X_PASS'] = 'globalvalue';
+
+        $app = new MiddlewareApplication($this->config);
+        $server = new Server($app);
+
+        $res = $server->run();
+        $this->assertEquals(
+            'globalvalue',
+            $res->getHeaderLine('X-pass'),
+            'Default request is made from server'
+        );
+    }
+
+    /**
+     * Test an application failing to build middleware properly
+     *
+     * @expectedException RuntimeException
+     * @expectedExceptionMessage The application `middleware` method
+     */
+    public function testRunWithApplicationNotMakingMiddleware()
+    {
+        $app = new InvalidMiddlewareApplication($this->config);
+        $server = new Server($app);
+        $server->run();
+    }
+
+    /**
+     * Test middleware being invoked.
+     *
+     * @return void
+     */
+    public function testRunMultipleMiddlewareSuccess()
+    {
+        $app = new MiddlewareApplication($this->config);
+        $server = new Server($app);
+        $res = $server->run();
+        $this->assertSame('first', $res->getHeaderLine('X-First'));
+        $this->assertSame('second', $res->getHeaderLine('X-Second'));
+    }
+
+    /**
+     * Test middleware not creating a response.
+     *
+     * @expectedException RuntimeException
+     * @expectedExceptionMessage Application did not create a response. Got "Not a response" instead.
+     */
+    public function testRunMiddlewareNoResponse()
+    {
+        $app = new BadResponseApplication($this->config);
+        $server = new Server($app);
+        $server->run();
+    }
+
+    /**
+     * Test that emit invokes the appropriate methods on the emitter.
+     *
+     * @return void
+     */
+    public function testEmit()
+    {
+        $response = new Response('php://memory', 200, ['x-testing' => 'source header']);
+        $final = $response
+            ->withHeader('X-First', 'first')
+            ->withHeader('X-Second', 'second');
+
+        $emitter = $this->getMock('Zend\Diactoros\Response\EmitterInterface');
+        $emitter->expects($this->once())
+            ->method('emit')
+            ->with($final);
+
+        $app = new MiddlewareApplication($this->config);
+        $server = new Server($app);
+        $server->emit($server->run(null, $response), $emitter);
+    }
+
+    /**
+     * Ensure that the Server.buildMiddleware event is fired.
+     *
+     * @return void
+     */
+    public function testBuildMiddlewareEvent()
+    {
+        $app = new MiddlewareApplication($this->config);
+        $server = new Server($app);
+        $called = false;
+        $server->eventManager()->on('Server.buildMiddleware', function ($event, $middleware) use (&$called) {
+            $called = true;
+            $this->assertInstanceOf('Cake\Http\MiddlewareStack', $middleware);
+        });
+        $server->run();
+        $this->assertTrue($called, 'Event not triggered.');
+    }
+}

+ 32 - 0
tests/test_app/TestApp/Http/BadResponseApplication.php

@@ -0,0 +1,32 @@
+<?php
+namespace TestApp\Http;
+
+use Cake\Http\BaseApplication;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+
+class BadResponseApplication extends BaseApplication
+{
+    /**
+     * @param \Cake\Http\MiddlewareStack $middleware The middleware stack to set in your App Class
+     * @return \Cake\Http\MiddlewareStack
+     */
+    public function middleware($middleware)
+    {
+        $middleware->push(function ($req, $res, $next) {
+            return 'Not a response';
+        });
+        return $middleware;
+    }
+
+    /**
+     * @param \Psr\Http\Message\ServerRequestInterface $request The request
+     * @param \Psr\Http\Message\ResponseInterface $request The response
+     * @param callable $next The next middleware
+     * @return \Psr\Http\Message\ResponseInterface
+     */
+    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
+    {
+        return $res;
+    }
+}

+ 16 - 0
tests/test_app/TestApp/Http/InvalidMiddlewareApplication.php

@@ -0,0 +1,16 @@
+<?php
+namespace TestApp\Http;
+
+use Cake\Http\BaseApplication;
+
+class InvalidMiddlewareApplication extends BaseApplication
+{
+    /**
+     * @param \Cake\Http\MiddlewareStack $middleware The middleware stack to set in your App Class
+     * @return null
+     */
+    public function middleware($middleware)
+    {
+        return null;
+    }
+}

+ 45 - 0
tests/test_app/TestApp/Http/MiddlewareApplication.php

@@ -0,0 +1,45 @@
+<?php
+namespace TestApp\Http;
+
+use Cake\Http\BaseApplication;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+
+class MiddlewareApplication extends BaseApplication
+{
+    /**
+     * @param \Cake\Http\MiddlewareStack $middleware The middleware stack to set in your App Class
+     * @return \Cake\Http\MiddlewareStack
+     */
+    public function middleware($middleware)
+    {
+        $middleware
+            ->push(function ($req, $res, $next) {
+                $res = $res->withHeader('X-First', 'first');
+                return $next($req, $res);
+            })
+            ->push(function ($req, $res, $next) {
+                $res = $res->withHeader('X-Second', 'second');
+                return $next($req, $res);
+            })
+            ->push(function ($req, $res, $next) {
+                if ($req->hasHeader('X-pass')) {
+                    $res = $res->withHeader('X-pass', $req->getHeaderLine('X-pass'));
+                }
+                $res = $res->withHeader('X-Second', 'second');
+                return $next($req, $res);
+            });
+        return $middleware;
+    }
+
+    /**
+     * @param \Psr\Http\Message\ServerRequestInterface $request The request
+     * @param \Psr\Http\Message\ResponseInterface $request The response
+     * @param callable $next The next middleware
+     * @return \Psr\Http\Message\ResponseInterface
+     */
+    public function __invoke(ServerRequestInterface $req, ResponseInterface $res, $next)
+    {
+        return $res;
+    }
+}