symfony http kernel for refactoring legacy apps: the ez publish case study - pug milano 2013

Post on 06-May-2015

3.267 Views

Category:

Technology

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Case study: eZ Publish

Symfony2 http kernel for legacy apps

Gaetano Giunta | PUG Milano | Maggio 2013

The drivers

Existing codebase is 10 years old

High maintenance cost

Started with no unit tests

Layers and roles not properly defined / documented

OOP before php had

Private/protected/static

Closures

Namespaces

Late static binding

And much more

Not built for an Ajax and REST world

5/18/2013 gg@ez.no Slide 3

Why change? Everyone loves NEW!

Existing codebase is 10 years old

Widely deployed

Well debugged

Pitfalls have probably been uncovered by now

Proven to scale

Well known:

Documentation improved over years

Tutorials, forums, blogs, aggregators

Active community of practitioners

Official training courses

5/18/2013 gg@ez.no Slide 4

Why change? Do not forget drawbacks

Focus on our core business Experience Management

Content Management

NOT Framework maintenance

Durable Architecture

API stability

Battle tested / not (only) the latest trend

Scalability

Lively Community!

5/18/2013 gg@ez.no Slide 5

Picking a framework for a platform rebuild

• Simple Integration with existing API

• HMVC (Hierarchical Model View Controller) stack

• Decoupled Components

• Dependency Injection

• Good Template Engine

• Extensible, Open, Reliable ;-)

5/18/2013 gg@ez.no Slide 6

Prerequisites

• Home brew

• Zeta Components

• Zend Framework 2

• Symfony 2 (Full Stack!)

5/18/2013 gg@ez.no Slide 7

Candidates

5/18/2013 gg@ez.no Slide 8

And the winner is… Title of presentation is a hint, really...

The challenge

Product Management SCRUM Story:

«As an existing user, I don’t want to be pissed off by a new #@!$% version!»

5/18/2013 gg@ez.no Slide 10

Backwards compatibility (life sucks)

Product Management SCRUM Story:

«As an existing user, I don’t want to be pissed off by a new #@!$% version!»

• 100% Data Compatible (same DB scheme)

• Possibility to include legacy templates in the new ones

• Routing fallback

• Load legacy content templates with legacy rules

• Settings

• Access Symfony services from legacy modules

5/18/2013 gg@ez.no Slide 11

Backwards compatibility: the objectives

Product Management SCRUM Story:

«As an existing user, I don’t want to be pissed off by a new #@!$% version!»

• 100% Data Compatible (same DB scheme)

• Possibility to include legacy templates in the new ones

• Routing fallback

• Load legacy content templates with legacy rules

• Settings

• Access Symfony services from legacy modules

5/18/2013 gg@ez.no Slide 12

Backwards compatibility: the objectives

A new architecture

Product Management SCRUM Story:

«As an existing user, I don’t want to be pissed off by a new #@!$% version!»

• 100% Data Compatible (same DB scheme)

• Possibility to include legacy templates in the new ones

• Routing fallback

• Load legacy content templates with legacy rules

• Settings

• Access Symfony services from legacy modules

Challenge Accepted

5/18/2013 gg@ez.no Slide 14

BC: the challenge

5/18/2013 gg@ez.no Slide 15

Dual-core architecture

Legacy version still works perfectly standalone

5/18/2013 gg@ez.no Slide 16

BC: icing on the cake

Isn’t this what you have been waiting for?

The HTTP Kernel

Request => process() => Response

use Symfony\Component\HttpFoundation\Request;

use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();

$input = $request->get('name', 'World'); // allows a default value

$response = new Response('Hello ' . htmlspecialchars($input, ENT_QUOTES, 'UTF-8'));

$response->send(); // takes care of http headers

The HTTPFoundation Component eases mundane tasks

5/18/2013 gg@ez.no Slide 18

Use the HTTP, Luke A very, very simple frontend controller

The HTTPKernel component “formalizes the process of starting with a request

and creating the appropriate response”

interface HttpKernelInterface

{

const MASTER_REQUEST = 1;

const SUB_REQUEST = 2;

public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true);

}

Returns a Response instance

The «H» in HMVC: subrequests are baked in from the beginning

Looks simple so far, isn’t it?

5/18/2013 gg@ez.no Slide 19

The heart of the application

The HttpKernel defines a complex flexible workflow

The controller to execute is found via a ControllerResolver

«Framework» work is done via an event system / event listeners

5/18/2013 gg@ez.no Slide 20

Adding the magic

$request = Request::createFromGlobals();

$dispatcher = new EventDispatcher();

// ... add some event listeners, eg: routing, security checking

// create the controller resolver

$resolver = new MyControllerResolver();

// instantiate the kernel

$kernel = new HttpKernel( $dispatcher, $resolver );

$response = $kernel->handle( $request );

$response->send();

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

5/18/2013 gg@ez.no Slide 21

Building a frontend controller

Any class implementing the ControllerResolverInterface can be used

interface ControllerResolverInterface

{

// must return a callable

public function getController(Request $request);

// returns an array of arguments for the controller

public function getArguments(Request $request, $controller);

}

...

5/18/2013 gg@ez.no Slide 22

Finding the Controller

The Event Dispatcher Component can be used

use Symfony\Component\EventDispatcher\Event;

$dispatcher->addListener('foo.action', $callable);

Depending on returned value, workflow might be altered (see docs online)

Dispatched events:

Name Name as constant Argument passed to the listener

kernel.request KernelEvents::REQUEST GetResponseEvent

kernel.controller KernelEvents::CONTROLLER FilterControllerEvent

kernel.view KernelEvents::VIEW GetResponseForControllerResultEvent

kernel.response KernelEvents::RESPONSE FilterResponseEvent

kernel.terminate KernelEvents::TERMINATE PostResponseEvent

kernel.exception KernelEvents::EXCEPTION GetResponseForExceptionEvent

5/18/2013 gg@ez.no Slide 23

Adding event listeners

Taming the beast

New Core: a standard Simfony app («ezpublish» = «app»)

«Legacy Stack» isolated in a dedicated directory

5/18/2013 gg@ez.no Slide 25

Refactoring: directory layout

New Core: Sf Bundles

5/18/2013 gg@ez.no Slide 26

Refactoring: bundles

use Symfony\Component\HttpFoundation\Request;

require_once __DIR__ . '/../ezpublish/autoload.php'; // set up class autoloading

require_once __DIR__ . '/../ezpublish/EzPublishKernel.php';

$kernel = new EzPublishKernel( 'dev', true ); // extends the Sf Kernel class

$kernel->loadClassCache(); // a method from parent class

$request = Request::createFromGlobals();

$response = $kernel->handle( $request );

$response->send();

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

The Kernel class wraps the HTTPKernel

It adds a Service Container

It allows to register bundles via registerBundles()

5/18/2013 gg@ez.no Slide 27

The final frontend controller Using Symfony Full Stack

Sandbox legacy code in a closure

Index.php had to be refactored (from 1100 lines to 20)

Logic moved to a php class

Separated environment setup from execution and teardown

runCallback() sets up the global legacy environment

5/18/2013 gg@ez.no Slide 28

Refactoring: bridging Legacy code

Routing

eZPublish 4 uses a custom MVC implementation

Frontend controller: index.php

Bootstraps configuration system, logging, “siteaccess”

Controllers are “plain php” files, properly declared

Url syntax: http:// site / module / controller / parameters

Parameters use a custom format instead of the query string

Virtual aliases can be added on top

For all content nodes, a nice alias is always generated by the system

Good for SEO

Technical debt

No DIC anywhere (registry pattern used)

No nested controllers

No provision for REST / AJAX

Implemented ad-hoc in many plugins (code/functionality duplication)

Policies are tied to controllers, not to the underlying content model

5/18/2013 gg@ez.no Slide 30

Routing

5/18/2013 gg@ez.no Slide 31

Routing: seamless integration

The ChainRouter from the Sf CMF project is used

Routes for new controllers can be declared in different ways

In a configuration file

app/config/routing.yml

Mybundle/Resources/config/routing.yml (loaded from main routing file)

Via annotations (phpdoc comments)

needs the SensioFrameworkExtraBundle bundle

Command line to dump them

php app/console router:debug

Maximum flexibility for parameters: required/optionsl, default values,

validation, restrict http method, extra support for locale and format, ...

5/18/2013 gg@ez.no Slide 32

Routing: how it works

Caching

eZ Publish 4 has a complicated advanced caching system

For viewing content, cache is generated on access, invalidated on editing

TTL = infinite

When editing a content, cache is also invalidated for all related contents

Extra invalidation rules can be configured

Can be set up to be pregenerated at editing time (tradeoff: editing speed)

Cache keys include policies of current user, query string, custom session data

“Cache-blocks” can also be added anywhere in the templates

Expiry rules can be set on each block, TTL-based or content-editing based

Breaks mvc principle

Most powerful AND misunderstood feature in the CMS

5/18/2013 gg@ez.no Slide 34

eZ4 Caching: basics

eZ has a built-in “full-page cache” (stores html on disk)

Currently deprecated, in favour of using a caching reverse Proxy

Performances same if not better

Delegate maintenance of part of the stack (Varnish, Squid)

Holy grail of caching: high TTL and support for PURGE command

1. When RP requests page from server, he gets a high TTL => cache page forever

2. When page changes, server tells to RP to purge that url from cache

Best reduction in number of requests to server while always showing fresh data

Downside: extremely hard to cache pages for connected users

ESI support as well

Hard to make efficient, as eZ can not regenerate an ESI block without full page

context

5/18/2013 gg@ez.no Slide 35

eZ4 Caching: integration with Reverse Proxies

HTTP Expiration and Validation are used

By setting caching headers on response object

Integrates with a Gateway Cache (a.k.a Reverse Proxy)

Native (built-in, php)

$kernel = new Kernel('prod', false);

$kernel = new HTTPCache($kernel);

External (Varnish, Squid, ...)

Native support for ESI

Using {{ render_esi() }} in twig

5/18/2013 gg@ez.no Slide 36

Symfony Caching: basics

REST

eZ4 had an incomplete REST API

Only functionality available: reading content

Based on Zeta Components MVC component

A new API has been implemented

Full reading and writing of content is possible

All “dictionary” data is also available

Content-type for response can be JSON or XML (with an XSD!)

Fully restful

Usage of all HTTP verbs (and then some: PATCH)

Respect http headers of request (eg: “Accept”)

HATEOAS: use urls as resource ids

No separate request handling framework needed: pure Symfony routing

Bonus points: a client for the REST API, implements the same interfaces exposed by the local PHP API – network transparency!!!

5/18/2013 gg@ez.no Slide 40

REST API

More info

Tutorials:

http://fabien.potencier.org/article/50/create-your-own-framework-on-top-of-the-

symfony2-components-part-1

http://symfony.com/doc/current/components/http_kernel/introduction.html

Sf2 book – jolly good looking docs:

http://symfony.com/doc/current/book/index.html

eZ Publish:

Community: http://share.ez.no

Source code: https://github.com/ezsystems

API docs: http://pubsvn.ez.no/preview.html

Contact me: @gggeek, gaetano.giunta@ez.no

5/18/2013 gg@ez.no Slide 42

The usual suspects

top related