Web Services and Context Symfony
Core Initiative

by Larry Garfield

@Crell

What we do

Web Services and Context Core Initiative (WSCCI)

WSCCI

The original plan...

The Web Services and Context Core Initiative (WSCCI) aims to transform Drupal from a first-class CMS to a first-class REST server with a first-class CMS on top of it. To do that, we must give Drupal a unified, powerful context system that will support smarter, context- sensitive, easily cacheable block-centric layouts and non-page responses using a robust unified plugin mechanism.

The original plan...

The Web Services and Context Core Initiative (WSCCI) aims to transform Drupal from a first-class CMS to a first-class REST server with a first-class CMS on top of it. To do that, we must give Drupal a unified, powerful context system that will support smarter, context- sensitive, easily cacheable block-centric layouts and non-page responses using a robust unified plugin mechanism.

Huh?

REST-First

Development

REpresentational State Transfer

Blocks in Drupal 7

Blocks in Drupal 7

Drupal 7 page flow

Page request flow in Drupal 7

Blocks/Requests in Drupal 8

Blocks in Drupal 8
Symfony2

Where did Symfony2 come from?

What is Symfony2?

Symfony2 Components

  • Reusable set of stand-alone libraries
  • Solid Object-Oriented toolset

Symfony2 Framework

  • Application framework
  • Build on top of the Components

So what are we using?

HttpFoundation

HttpFoundation

Request

    GET /foo/bar.html HTTP/1.1
    Host: example.com
    Accept: text/html
    ...
    

Response

    HTTP/1.1 200 OK
    Date: Fri, 18 May 2012 08:08:08 GMT
    Content-Length: 14
    Content-Type: text/html

    Hello World!
    

PHP's request API

  session_start();

  $name = $_GET['name'];

  echo $_SESSION['name'];

  $method = $_SERVER['REQUEST_METHOD'];

  $client_ip = $_SERVER['REMOTE_ADDR'];
  
    if ($trust_proxy) {
      if (isset($_SERVER['HTTP_CLIENT_IP'])) {
        return $_SERVER['HTTP_CLIENT_IP'];
      }
      if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2);
        return isset($ips[0]) ? trim($ips[0]) : '';
      }
    }
    return $_SERVER['REMOTE_ADDR'];
    

PHP's request API

Symfony2's request API

      use Symfony\Component\HttpFoundation\Request;

      $request = Request::createFromGlobals();

      $request = Request::create('/hello.html', 'GET');
      $request->overrideGlobals();

      $request->query->get('name', 'Default');
      $request->getSession()->get('name');
      $request->getPathInfo();
      $request->getClientIp();
      Request::trustProxyData();
    

PHP's response API

    header('HTTP/1.0 404 Not Found');
    header('Content-Type: text/html; charset=UTF-8');

    setcookie('name', $name);
    $_SESSION['name'] = 'Larry';

    echo 'Hello ' . $name;
    

PHP's response API

Symfony2's response API

  use Symfony\Component\HttpFoundation\Response;

  $response = new Response('No page. :-(', 404, array(
    'Content-Type' => 'text/plain',
  ));

  $response = new Response();
  $response->setContent('Hello World');
  $response->send();
  
    use Symfony\Component\HttpFoundation\StreamedResponse;

    $response = new StreamedResponse(function () {
      echo 'foo';
      flush();
      echo 'bar';
    });

    // Later
    $response->send();

    

EventDispatcher

EventDispatcher

Like hooks, but

EventDispatcher

  use Symfony\Component\EventDispatcher\EventDispatcher;

  $dispatcher = new EventDispatcher();

  // Somewhere...
  $callable = function (Event $event) {
    // do something
  };
  $dispatcher->addListener('event_name', $callable);

  // Equivalent of module_invoke_all().
  $dispatcher->dispatch('event_name', new Event());
  
    class PlusOneSubscribe implements EventSubscriberInterface {

      public function onMyEvent(Event $event) {
        // Do stuff
      }

      static function getSubscribedEvents() {
        $events['event_name'][] = array('onMyEvent');
        return $events;
      }
    }

    $dispatcher->addSubscriber(new PlusOneSubscribe());
    

HttpKernel

Routing

HttpKernel

HttpKernelInterface

    namespace Symfony\Component\HttpKernel;

    interface HttpKernelInterface {
      /**
       * Handles a request.
       *
       * @param Request $request
       *   A request instance
       * @return $response
       *   A Response instance
       */
      function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true);
    }
    

HttpKernel

Kernel workflow

index.php

    use Drupal\Core\DrupalKernel;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\EventDispatcher\EventDispatcher;
    use Symfony\Component\HttpKernel\Controller\ControllerResolver;

    // Create a request object from the HTTPFoundation.
    $request = Request::createFromGlobals();

    $dispatcher = new EventDispatcher();
    $resolver = new ControllerResolver();

    $kernel = new DrupalKernel($dispatcher, $resolver);
    $response = $kernel->handle($request);

    $response->prepare($request);
    $response->send();
    $kernel->terminate($request, $response);
    

Drupal 8 page flow (planned)

Request flow in Drupal 8

So what changes?

Controllers

Routing

Route based on more than path: Method, content type, etc.
Method Path Accept header Result
GET node/1 text/html Node view page
GET node/1 application/json JSON-LD record
POST system/form/node_edit text/html Submit a node form and redirect
PUT node/1 application/json Overwrite the node exactly

Routing

New routing syntax (farewell hook_menu()?)

Warning, early untested prototype
    function example_route_info() {
      $routes['node_page'] = array(
        'route' => new Route('/node/{node}', array(
          '_controller' => 'NodeController:show',
        )),
        // This gets objectified later...
        'access' => array(
          'callback' => array('function' => 'node_access'),
          'arguments' => array('view'),
        ),
      );
      return $routes;
    }

    class NodeController {
      public function show(Node $node) {
        // ...
        return new Response($content);
      }
    }
  

Routing (fancy)

    function example_route_info() {
      $routes['blog_list'] = array(
        'route' => new Route('/blog/{page}', array(
          '_controller' => 'BlogController:show',
          'page' => 1, // Default value
        ), array(
          'page' => '\d+',
          '_method' => 'GET',
        )),
        // This gets objectified later...
        'access' => array(
          'callback' => array('function' => 'node_access'),
          'arguments' => array('view'),
        ),
      );
      return $routes;
    }

    class BlogController {
      public function show(Request $request, $page) {
        // Link to router items, not random URLs.
        $link = $this->generator->generate('node_page', array('node' => 5));
        // ...
        return new Response($content);
      }
    }
  

Now we're talking REST...

"Pages" are a special case of REST

Side-benefits

(Please help clean up old crap!)

And oh yeah, SCOTCH

Collaboration

Work upstream

Next up: Streaming

Excited yet?

I hope so...

Questions?

http://groups.drupal.org/wscci