1/52

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

  • HTTP command
  • Self-contained action
  • Lots of "headers" for metadata
GET /foo/bar.html HTTP/1.1
Host: example.com
Accept: text/html
...

Response

  • HTTP result
  • Self-contained payload
  • Lots of "headers for metadata
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

1session_start();
2 
3$name = $_GET['name'];
4 
5echo $_SESSION['name'];
6 
7$method = $_SERVER['REQUEST_METHOD'];
8 
9$client_ip = $_SERVER['REMOTE_ADDR'];
01if ($trust_proxy) {
02  if (isset($_SERVER['HTTP_CLIENT_IP'])) {
03    return $_SERVER['HTTP_CLIENT_IP'];
04  }
05  if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
06    $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2);
07    return isset($ips[0]) ? trim($ips[0]) : '';
08  }
09}
10return $_SERVER['REMOTE_ADDR'];

PHP's request API

Symfony2's request API

01use Symfony\Component\HttpFoundation\Request;
02 
03$request = Request::createFromGlobals();
04 
05$request = Request::create('/hello.html', 'GET');
06$request->overrideGlobals();
07 
08$request->query->get('name', 'Default');
09$request->getSession()->get('name');
10$request->getPathInfo();
11$request->getClientIp();
12Request::trustProxyData();

PHP's response API

1header('HTTP/1.0 404 Not Found');
2header('Content-Type: text/html; charset=UTF-8');
3 
4setcookie('name', $name);
5$_SESSION['name'] = 'Larry';
6 
7echo 'Hello ' . $name;

PHP's response API

Symfony2's response API

1use Symfony\Component\HttpFoundation\Response;
2 
3$response = new Response('No page. :-(', 404, array(
4  'Content-Type' => 'text/plain',
5));
6 
7$response = new Response();
8$response->setContent('Hello World');
9$response->send();
01use Symfony\Component\HttpFoundation\StreamedResponse;
02 
03$response = new StreamedResponse(function () {
04  echo 'foo';
05  flush();
06  echo 'bar';
07});
08 
09// Later
10$response->send();

EventDispatcher

EventDispatcher

Like hooks, but

EventDispatcher

01use Symfony\Component\EventDispatcher\EventDispatcher;
02 
03$dispatcher = new EventDispatcher();
04 
05// Somewhere...
06$callable = function (Event $event) {
07  // do something
08};
09$dispatcher->addListener('event_name', $callable);
10 
11// Equivalent of module_invoke_all().
12$dispatcher->dispatch('event_name', new Event());
01class PlusOneSubscribe implements EventSubscriberInterface {
02 
03  public function onMyEvent(Event $event) {
04    // Do stuff
05  }
06 
07  static function getSubscribedEvents() {
08    $events['event_name'][] = array('onMyEvent');
09    return $events;
10  }
11}
12 
13$dispatcher->addSubscriber(new PlusOneSubscribe());

HttpKernel

Routing

HttpKernel

HttpKernelInterface

01namespace Symfony\Component\HttpKernel;
02 
03interface HttpKernelInterface {
04  /**
05   * Handles a request.
06   *
07   * @param Request $request
08   *   A request instance
09   * @return $response
10   *   A Response instance
11   */
12  function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true);
13}

HttpKernel

Kernel workflow

index.php (before the Container)

01use Drupal\Core\DrupalKernel;
02use Symfony\Component\HttpFoundation\Request;
03use Symfony\Component\EventDispatcher\EventDispatcher;
04use Symfony\Component\HttpKernel\Controller\ControllerResolver;
05 
06// Create a request object from the HTTPFoundation.
07$request = Request::createFromGlobals();
08 
09$dispatcher = new EventDispatcher();
10$resolver = new ControllerResolver();
11 
12$kernel = new DrupalKernel($dispatcher, $resolver);
13$response = $kernel->handle($request);
14 
15$response->prepare($request);
16$response->send();
17$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
01function example_route_info() {
02  $routes['node_page'] = array(
03    'route' => new Route('/node/{node}', array(
04      '_controller' => 'NodeController:show',
05    )),
06    // This gets objectified later...
07    'access' => array(
08      'callback' => array('function' => 'node_access'),
09      'arguments' => array('view'),
10    ),
11  );
12  return $routes;
13}
14 
15class NodeController {
16  public function show(Node $node) {
17    // ...
18    return new Response($content);
19  }
20}

Routing (fancy)

01function example_route_info() {
02  $routes['blog_list'] = array(
03    'route' => new Route('/blog/{page}', array(
04      '_controller' => 'BlogController:show',
05      'page' => 1, // Default value
06    ), array(
07      'page' => '\d+',
08      '_method' => 'GET',
09    )),
10    // This gets objectified later...
11    'access' => array(
12      'callback' => array('function' => 'node_access'),
13      'arguments' => array('view'),
14    ),
15  );
16  return $routes;
17}
18 
19class BlogController {
20  public function show(Request $request, $page) {
21    // Link to router items, not random URLs.
22    $link = $this->generator->generate('node_page', array('node' => 5));
23    // ...
24    return new Response($content);
25  }
26}

Now we're talking REST...

"Pages" are a special case of REST

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...