3. 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/2013gg@ez.no Slide 3
Why change?
Everyone loves NEW!
4. 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/2013gg@ez.no Slide 4
Why change?
Do not forget drawbacks
5. Focus on our core business
Experience Management
Content Management
NOT Framework maintenance
DurableArchitecture
API stability
Battle tested / not (only) the latest trend
Scalability
Lively Community!
5/18/2013gg@ez.no Slide 5
Picking a framework for a platform rebuild
6. • Simple Integration with existing API
• HMVC (Hierarchical Model View Controller) stack
• Decoupled Components
• Dependency Injection
• Good Template Engine
• Extensible, Open, Reliable ;-)
5/18/2013gg@ez.no Slide 6
Prerequisites
10. Product Management SCRUM Story:
«As an existing user, I don’t want to be pissed off by a new #@!$% version!»
5/18/2013gg@ez.no Slide 10
Backwards compatibility
(life sucks)
11. 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/2013gg@ez.no Slide 11
Backwards compatibility: the objectives
12. 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/2013gg@ez.no Slide 12
Backwards compatibility: the objectives
14. 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/2013gg@ez.no Slide 14
BC: the challenge
18. Request => process() => Response
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
$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/2013gg@ez.no Slide 18
Use the HTTP, Luke
A very, very simple frontend controller
19. 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/2013gg@ez.no Slide 19
The heart of the application
20. 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/2013gg@ez.no Slide 20
Adding the magic
21. $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/2013gg@ez.no Slide 21
Building a frontend controller
22. 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/2013gg@ez.no Slide 22
Finding the Controller
23. The Event Dispatcher Component can be used
use SymfonyComponentEventDispatcherEvent;
$dispatcher->addListener('foo.action', $callable);
Depending on returned value, workflow might be altered (see docs online)
Dispatchedevents:
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/2013gg@ez.no Slide 23
Adding event listeners
25. New Core: a standard Simfony app («ezpublish» = «app»)
«Legacy Stack» isolated in a dedicated directory
5/18/2013gg@ez.no Slide 25
Refactoring: directory layout
26. New Core: Sf Bundles
5/18/2013gg@ez.no Slide 26
Refactoring: bundles
27. use SymfonyComponentHttpFoundationRequest;
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/2013gg@ez.no Slide 27
The final frontend controller
Using Symfony Full Stack
28. 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/2013gg@ez.no Slide 28
Refactoring: bridging Legacy code
30. 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/2013gg@ez.no Slide 30
Routing
32. 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/2013gg@ez.no Slide 32
Routing: how it works
34. 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 misunderstoodfeature in the CMS
5/18/2013gg@ez.no Slide 34
eZ4 Caching: basics
35. 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/2013gg@ez.no Slide 35
eZ4 Caching: integration with Reverse Proxies
36. 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/2013gg@ez.no Slide 36
Symfony Caching: basics
38. 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/2013gg@ez.no Slide 40
REST API