Web Services and Context Symfony
Core Initiative

by Larry Garfield

@Crell

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.

Huh?

REST-First

Development

REpresentational State Transfer

Blocks in Drupal 7

Blocks in Drupal 7

Drupal 7 page flow

2012-04-18 20:47ZDrupal 7Layer 1Request"Routing"Page callbackDrupal-AjaxDelivery callbackCompile to JSONHTMLpage_buildblocks_buildpage_alterrenderServicespage callbackLoad serverLoad menu itemServer callbackServer pluginRe-parse pathsControllerPrintPrintPrintORRenderarrayAjaxcommandsAjax commandsRenderarrayRender array

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?

Symfony2 + Drupal 8 = Drufony

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 (before the Container)

    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)

2012-04-18 20:47ZDrupal 8/SymfonyLayer 1Sub-RequestListenersKernelRouterControllerResponseRequestListenersKernelRouterControllerControllerORResponseResponseResponseResponseRequest

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

Needs Review...

http://drupal.org/node/1606794

Side-benefits

(Please help clean up old crap!)

A large part of WSCCI is simply paying down years of
technical debt.
That means we can do other cool things.

Things like... SCOTCH

Things like... Deployment

[*] The management will not be held responsible if "easy" is insufficiently easy for your client.

Collaboration

Work upstream

Streaming

Odds and ends

On the Drupal side...

Bundles

Other issues

Excited yet?

I hope so, because we need your help...

Questions?

http://groups.drupal.org/wscci