symfony http kernel for refactoring legacy apps: the ez publish case study - pug milano 2013
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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] Slide 6
Prerequisites
• Home brew
• Zeta Components
• Zend Framework 2
• Symfony 2 (Full Stack!)
5/18/2013 [email protected] Slide 7
Candidates
5/18/2013 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] Slide 14
BC: the challenge
5/18/2013 [email protected] Slide 15
Dual-core architecture
Legacy version still works perfectly standalone
5/18/2013 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] Slide 25
Refactoring: directory layout
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 [email protected] 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 [email protected] 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 [email protected] Slide 30
Routing
5/18/2013 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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, [email protected]
5/18/2013 [email protected] Slide 42
The usual suspects