Browse Source

Merge pull request #8696 from cakephp/psr-server

PSR7 Server and BaseApplication
José Lorenzo Rodríguez 10 years ago
parent
commit
c379ad233b

+ 97 - 0
src/Http/BaseApplication.php

@@ -0,0 +1,97 @@
+<?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.
+     *
+     * Override this method to add additional bootstrap logic for your application.
+     *
+     * @return void
+     */
+    public function bootstrap()
+    {
+        require_once $this->configDir . '/bootstrap.php';
+    }
+
+    /**
+     * 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 \Cake\Http\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;
+    }
+}