SlideShare a Scribd company logo
1 of 111
Download to read offline
Developing cacheable
PHP applications
Thijs Feryn
Slow websites suck
Hi, I’m Thijs
I’m
@ThijsFeryn
on Twitter
I’m an
Evangelist
At
Cache
Don’t
recompute
if the data
hasn’t
changed
Whatifwe
coulddesign
oursoftware
withHTTP
cachingin
mind?
✓Portability
✓Developer empowerment
✓Control
✓Consistent caching behavior
Caching state of mind
Reverse
caching
proxy
Normally
User Server
With ReCaPro *
User ReCaPro Server
* Reverse Caching Proxy
Content Delivery Network
HTTP caching mechanisms
Expires: Sat, 09 Sep 2017 14:30:00 GMT
Cache-control: public, max-age=3600,
s-maxage=86400
Cache-control: private, no-cache, no-store
In an ideal world
✓Stateless
✓Well-defined TTL
✓Cache / no-cache per resource
✓Cache variations
✓Conditional requests
✓HTTP fragments for non-
cacheable content
In an ideal world
Reality
sucks
Common
problems
Time To Live
Cache variations
Authentication
Legacy
Twig templates
✓Multi-lingual (Accept-Language)
✓Nav
✓Header
✓Footer
✓Main
✓Login page & private content
composer require symfony/flex
composer require annotations twig translation
<?php
namespace AppController;
use SymfonyComponentHttpFoundationRequest;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use SymfonyBundleFrameworkBundleControllerController;
class DefaultController extends Controller
{
/**
* @Route("/", name="home")
*/
public function index()
{
return $this->render('index.twig');
}
}
src/Controller/DefaultController.php
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/html" xmlns:hx="http://purl.org/NET/hinclude">
<head>
<title>{% block title %}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">
</head>
<body>
<div class="container-fluid">
{{ include('header.twig') }}
<div class="row">
<div class="col-sm-3 col-lg-2">
{{ include('nav.twig') }}
</div>
<div class="col-sm-9 col-lg-10">
{% block content %}{% endblock %}
</div>
</div>
{{ include('footer.twig') }}
</div>
</body>
</html>
templates/base.twig
{% extends "base.twig" %}
{% block title %}Home{% endblock %}
{% block content %}
<div class="page-header">
<h1>{{ 'example' | trans }}</h1>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat orci
eget libero sollicitudin, non ultrices turpis mollis. Aliquam sit amet tempus elit.
Ut viverra risus enim, ut venenatis justo accumsan nec. Praesent a dolor tellus.
Maecenas non mauris leo. Pellentesque lobortis turpis at dapibus laoreet. Mauris
rhoncus nulla et urna mollis, et lobortis magna ornare. Etiam et sapien consequat,
egestas felis sit amet, dignissim enim.</p>
<p>Quisque quis mollis justo, imperdiet fermentum velit. Aliquam nulla justo,
consectetur et diam non, luctus commodo metus. Vestibulum fermentum efficitur nulla
non luctus. Nunc lorem nunc, mollis id efficitur et, aliquet sit amet ante. Sed
ipsum turpis, vehicula eu semper eu, malesuada eget leo. Vestibulum venenatis dui
id pulvinar suscipit. Etiam nec massa pharetra justo pharetra dignissim quis non
magna. Integer id convallis lectus. Nam non ullamcorper metus. Ut vestibulum ex ut
massa posuere tincidunt. Vestibulum hendrerit neque id lorem rhoncus aliquam. Duis
a facilisis metus, a faucibus nulla.</p>
{% endblock %}
templates/index.twig
<nav class="navbar navbar-default navbar-fixed-side">
<ul class="nav">
<li><a href="{{ url('home') }}">Home</a></li>
<li><a href="{{ url('login') }}">{{ 'log_in' | trans }}</a></li>
<li><a href="{{ url('private') }}">Private</a></li>
</ul>
</nav>
templates/nav.twig
<footer>
<hr />
<small>Footer</small>
</footer>
templates/footer.twig
home: Home
welcome : Welcome to the site
rendered : Rendered at %date%
example : An example page
log_in : Log in
login : Login
log_out : Log out
username : Username
password : Password
private : Private
privatetext : Looks like some very private data
translations/messages.en.yml
home: Start
welcome : Welkom op de site
rendered : Samengesteld op %date%
example : Een voorbeeldpagina
log_in : Inloggen
login : Login
log_out : Uitloggen
username : Gebruikersnaam
password : Wachtwoord
private : Privé
privatetext : Deze tekst ziet er vrij privé uit
translations/messages.nl.yml
<?php
namespace AppEventListener;
use SymfonyComponentHttpKernelEventGetResponseEvent;
class LocaleListener
{
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$preferredLanguage = $request->getPreferredLanguage();
if(null !== $preferredLanguage) {
$request->setLocale($preferredLanguage);
}
}
}
src/EventListener/LocaleListener.php
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'
AppController:
resource: '../src/Controller'
tags: ['controller.service_arguments']
AppEventListenerLocaleListener:
tags:
- { name: kernel.event_listener, event: kernel.request, priority: 100}
config/services.yml
Accept-Language: en
Accept-Language: nl
VS
The mission
Maximum
Cacheability
Cache-control
Cache-Control: public, s-maxage=500
<?php
namespace AppController;
use SymfonyComponentHttpFoundationRequest;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use SymfonyBundleFrameworkBundleControllerController;
class DefaultController extends Controller
{
/**
* @Route("/", name="home")
*/
public function index()
{
return $this
->render('index.twig')
->setSharedMaxAge(500)
->setPublic();
}
}
Conditional
requests
Only fetch
payload that has
changed
HTTP/1.1 200 OK
Otherwise:
HTTP/1.1 304 Not Modified
Conditional requests
HTTP/1.1 200 OK
Host: localhost
Etag: 7c9d70604c6061da9bb9377d3f00eb27
Content-type: text/html; charset=UTF-8
Hello world output
GET / HTTP/1.1
Host: localhost
User-Agent: curl/7.48.0
Conditional requests
HTTP/1.0 304 Not Modified
Host: localhost
Etag: 7c9d70604c6061da9bb9377d3f00eb27
GET / HTTP/1.1
Host: localhost
User-Agent: curl/7.48.0
If-None-Match:
7c9d70604c6061da9bb9377d3f00eb27
Conditional requests
HTTP/1.1 200 OK
Host: localhost
Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT
Content-type: text/html; charset=UTF-8
Hello world output
GET / HTTP/1.1
Host: localhost
User-Agent: curl/7.48.0
Conditional requests
HTTP/1.0 304 Not Modified
Host: localhost
Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT
GET / HTTP/1.1
Host: localhost
User-Agent: curl/7.48.0
If-Last-Modified: Fri, 22 Jul 2016 10:11:16
GMT
composer require symfony-bundles/redis-bundle
<?php
namespace AppEventListener;
use SymfonyBridgeMonologLogger;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpKernelEventGetResponseEvent;
use SymfonyComponentHttpKernelEventFilterResponseEvent;
use SymfonyBundlesRedisBundleRedisClient as RedisClient;
class ConditionalRequestListener
{
protected $redis;
protected $logger;
public function __construct(RedisClient $redis)
{
$this->redis = $redis;
}
protected function isModified(Request $request, $etag)
{
if ($etags = $request->getETags()) {
return in_array($etag, $etags) || in_array('*', $etags);
}
return true;
}
...
src/EventListener/ConditionalRequestListener.php
{
$this->redis = $redis;
$this->logger = $logger;
}
protected function isModified(Request $request, $etag)
{
if ($etags = $request->getETags()) {
return in_array($etag, $etags) || in_array('*', $etags);
}
return true;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$etag = $this->redis->get('etag:'.md5($request->getUri()));
if(!$this->isModified($request,$etag)) {
$event->setResponse(Response::create('Not Modified',Response::HTTP_NOT_MODIFIED));
}
}
public function onKernelResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();
$request = $event->getRequest();
$etag = md5($response->getContent());
$response->setEtag($etag);
if($this->isModified($request,$etag)) {
$this->redis->set('etag:'.md5($request->getUri()),$etag);
}
}
}
src/EventListener/ConditionalRequestListener.php
Do not cache
Cache-Control: private, no-store
/**
* @Route("/private", name="private")
*/
public function private()
{
$response = $this
->render('private.twig')
->setPrivate();
$response->headers->addCacheControlDirective('no-store');
return $response;
}
Dot not
cache private
page
session
cookie
Nocache
Code
renders
singleHTTP
response
Lowest
common
denominator:
nocache
Blockcaching
<esi:include src="/header" />
Edge Side Includes
✓Placeholder
✓Parsed by Varnish
✓Output is a composition of blocks
✓State per block
✓TTL per block
Surrogate-Capability: key="ESI/1.0"
Surrogate-Control: content="ESI/1.0"
Varnish
Backend
<esi:include src="/header" />
Parse ESI placeholdersVarnish
ESI
vs
AJAX
✓ Server-side
✓ Standardized
✓ Processed on the
“edge”, no in the
browser
✓ Generally faster
Edge-Side Includes
- Sequential
- One fails, all fail
- Limited
implementation in
Varnish
✓ Client-side
✓ Common knowledge
✓ Parallel processing
✓ Graceful
degradation
AJAX
- Processed by the
browser
- Extra roundtrips
- Somewhat slower
Subrequests
<div class="container-fluid">

{{ include('header.twig') }}

<div class="row">

<div class="col-sm-3 col-lg-2">

{{ include('nav.twig') }}

</div>

<div class="col-sm-9 col-lg-10">

{% block content %}{% endblock %}

</div>

</div>

{{ include('footer.twig') }}

</div>
<div class="container-fluid">

{{ render_esi(url('header')) }}

<div class="row">

<div class="col-sm-3 col-lg-2">

{{ render_esi(url('nav')) }}

</div>

<div class="col-sm-9 col-lg-10">

{% block content %}{% endblock %}

</div>

</div>

{{ render_esi(url('footer')) }}

</div>
<div class="container-fluid">
<esi:include src="/header" />
<div class="row">
<div class="col-sm-3 col-lg-2">
<esi:include src="/nav" />
</div>
<div class="col-sm-9 col-lg-10">
<div class="page-header">
<h1>An example page <small>Rendered at 2017-05-17 16:57:14</small></h1>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat orci eget libero
sollicitudin,…</p>
</div>
</div>
<esi:include src="/footer" />
</div>
/**
* @Route("/", name="home")
*/
public function index()
{
return $this
->render('index.twig')
->setPublic()
->setSharedMaxAge(500);
}
/**
* @Route("/header", name="header")
*/
public function header()
{
$response = $this
->render('header.twig')
->setPrivate();
$response->headers->addCacheControlDirective('no-store');
return $response;
}
/**
* @Route("/footer", name="footer")
*/
public function footer()
{
$response = $this->render('footer.twig');
$response
->setSharedMaxAge(500)
->setPublic();
return $response;
}
/**
* @Route("/nav", name="nav")
*/
public function nav()
{
$response = $this->render('nav.twig');
$response
->setVary('X-Login',false)
->setSharedMaxAge(500)
->setPublic();
return $response;
}
Controller
action per
fragment
Problem:
no language
cache
variation
Vary: Accept-Language
<?php
namespace AppEventListener;
use SymfonyComponentHttpKernelEventFilterResponseEvent;
class VaryListener
{
public function onKernelResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();
$response->setVary('Accept-Language',false);
}
}
src/EventListener/VaryListener.php
✓Navigation page
✓Private page
Weak spots
Not cached
because of
stateful content
Move state client-side
Replace PHP session with
JSON Web Tokens
JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pb
iIsImV4cCI6MTQ5NTUyODc1NiwibG9naW4iOnRydWV9.u4Idy-
SYnrFdnH1h9_sNc4OasORBJcrh2fPo1EOTre8
✓3 parts
✓Dot separated
✓Base64 encoded JSON
✓Header
✓Payload
✓Signature (HMAC with secret)
eyJzdWIiOiJhZG1pbiIsIm
V4cCI6MTQ5NTUyODc1Niwi
bG9naW4iOnRydWV9
{
"alg": "HS256",
 "typ": "JWT"
}
{
"sub": "admin",
"exp": 1495528756,
"login": true
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
eyJhbGciOiJIUzI1NiIsI
nR5cCI6IkpXVCJ9
u4Idy-
SYnrFdnH1h9_sNc4OasOR
BJcrh2fPo1EOTre8
JWT
Cookie:token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJz
dWIiOiJhZG1pbiIsImV4cCI6MTQ5NTUyODc1NiwibG9naW4iOnRydW
V9.u4Idy-SYnrFdnH1h9_sNc4OasORBJcrh2fPo1EOTre8
✓Stored in a cookie
✓Can be validated by Varnish
✓Payload can be processed by any
language (e.g. Javascript)
sub jwt {
std.log("Ready to perform some JWT magic");
if(cookie.isset("jwt_cookie")) {
#Extract header data from JWT
var.set("token", cookie.get("jwt_cookie"));
var.set("header", regsub(var.get("token"),"([^.]+).[^.]+.[^.]+","1"));
var.set("type", regsub(digest.base64url_decode(var.get("header")),{"^.*?"typ"s*:s*"(w+)".*?$"},"1"));
var.set("algorithm", regsub(digest.base64url_decode(var.get("header")),{"^.*?"alg"s*:s*"(w+)".*?$"},"1"));
#Don't allow invalid JWT header
if(var.get("type") == "JWT" && var.get("algorithm") == "HS256") {
#Extract signature & payload data from JWT
var.set("rawPayload",regsub(var.get("token"),"[^.]+.([^.]+).[^.]+$","1"));
var.set("signature",regsub(var.get("token"),"^[^.]+.[^.]+.([^.]+)$","1"));
var.set("currentSignature",digest.base64url_nopad_hex(digest.hmac_sha256(var.get("key"),var.get("header") + "." + var.get("rawPayload"))));
var.set("payload", digest.base64url_decode(var.get("rawPayload")));
var.set("exp",regsub(var.get("payload"),{"^.*?"exp"s*:s*([0-9]+).*?$"},"1"));
var.set("jti",regsub(var.get("payload"),{"^.*?"jti"s*:s*"([a-z0-9A-Z_-]+)".*?$"},"1"));
var.set("userId",regsub(var.get("payload"),{"^.*?"uid"s*:s*"([0-9]+)".*?$"},"1"));
var.set("roles",regsub(var.get("payload"),{"^.*?"roles"s*:s*"([a-z0-9A-Z_-, ]+)".*?$"},"1"));
#Only allow valid userId
if(var.get("userId") ~ "^d+$") {
#Don't allow expired JWT
if(std.time(var.get("exp"),now) >= now) {
#SessionId should match JTI value from JWT
if(cookie.get(var.get("sessionCookie")) == var.get("jti")) {
#Don't allow invalid JWT signature
if(var.get("signature") == var.get("currentSignature")) {
#The sweet spot
set req.http.X-login="true";
} else {
std.log("JWT: signature doesn't match. Received: " + var.get("signature") + ", expected: " + var.get("currentSignature"));
}
} else {
std.log("JWT: session cookie doesn't match JTI." + var.get("sessionCookie") + ": " + cookie.get(var.get("sessionCookie")) + ", JTI:" + var.get("jti"));
}
} else {
std.log("JWT: token has expired");
}
} else {
std.log("UserId '"+ var.get("userId") +"', is not numeric");
}
} else {
std.log("JWT: type is not JWT or algorithm is not HS256");
}
std.log("JWT processing finished. UserId: " + var.get("userId") + ". X-Login: " + req.http.X-login);
}
#Look for full private content
if(req.url ~ "/node/2" && req.url !~ "^/user/login") {
if(req.http.X-login != "true") {
return(synth(302,"/user/login?destination=" + req.url));
}
}
}
Insert incomprehensible
Varnish VCL code here …
X-Login: true
End result:
X-Login: false
Custom request
header set by
Varnish
Extra cache
variation
required
Vary: Accept-Language, X-Login
Content for logged-in
& anonymous differs
<script language="JavaScript">

function getCookie(name) {

var value = "; " + document.cookie;

var parts = value.split("; " + name + "=");

if (parts.length == 2) return parts.pop().split(";").shift();

}

function parseJwt (token) {

var base64Url = token.split('.')[1];

var base64 = base64Url.replace('-', '+').replace('_', '/');

return JSON.parse(window.atob(base64));

};

$(document).ready(function(){

if ($.cookie('token') != null ){

var token = parseJwt($.cookie("token"));

$("#usernameLabel").html(', ' + token.sub);

}

});

</script>
Parse JWT
in Javascript
Does not require
backend access
composer require firebase/php-jwt
<?php
namespace AppService;
use FirebaseJWTJWT;
use SymfonyComponentHttpFoundationCookie;
class JwtAuthentication
{
protected $key;
protected $username;
protected $password;
public function __construct($key,$username,$password)
{
$this->key = $key;
$this->username = $username;
$this->password = $password;
}
public function jwt($username)
{
return JWT::encode([
'sub'=>$username,
'exp'=>time() + (4 * 24 * 60 * 60),
'login'=>true,
],$this->key);
}
public function createCookie($username)
{
return new Cookie("token",$this->jwt($username), time() + (3600 * 48), '/', null,
false, false);
}
public function validate($token)
{
src/Service/JwtAuthentication.php
$this->password = $password;
}
public function jwt($username)
{
return JWT::encode([
'sub'=>$username,
'exp'=>time() + (4 * 24 * 60 * 60),
//'exp'=>time() + 60,
'login'=>true,
],$this->key);
}
public function createCookie($username)
{
return new Cookie("token",$this->jwt($username), time() + (3600 * 48), '/', null,
false, false);
}
public function validate($token)
{
try {
$data = JWT::decode($token,$this->key,['HS256']);
$data = (array)$data;
if($data['sub'] !== $this->username) {
return false;
}
return true;
} catch(UnexpectedValueException $e) {
return false;
}
}
}
src/Service/JwtAuthentication.php
services:
AppServiceJwtAuthentication:
arguments:
$key: '%env(JWT_KEY)%'
$username: '%env(JWT_USERNAME)%'
$password: '%env(JWT_PASSWORD)%'
src/Service/JwtAuthentication.php
###> JWT authentication ###
JWT_KEY=SlowWebSitesSuck
JWT_USERNAME=admin
JWT_PASSWORD=$2y$10$431rvq1qS9ewNFP0Gti/o.kBbuMK4zs8IDTLlxm5uzV7cbv8wKt0K
###< JWT authentication ###
.env
/**
* @Route("/login", name="login", methods="GET")
*/
public function login(Request $request, JwtAuthentication $jwt)
{
if($jwt->validate($request->cookies->get('token'))) {
return new RedirectResponse($this->generateUrl('home'));
}
$response = $this->render('login.twig',['loginLogoutUrl'=>$this-
>generateUrl('login'),'loginLogoutLabel'=>'log_in']);
$response
->setSharedMaxAge(500)
->setVary('X-Login',false)
->setPublic();
return $response;
}
/**
* @Route("/login", name="loginpost", methods="POST")
*/
public function loginpost(Request $request, JwtAuthentication $jwt)
{
$username = $request->get('username');
$password = $request->get('password');
if(!$username || !$password || getenv('JWT_USERNAME') != $username || !
password_verify($password,getenv('JWT_PASSWORD'))) {
return new RedirectResponse($this->generateUrl('login'));
}
$response = new RedirectResponse($this->generateUrl('home'));
$response->headers->setCookie($jwt->createCookie($username));
return $response;
}
src/Controller/DefaultController.php
/**
* @Route("/logout", name="logout")
*/
public function logout()
{
$response = new RedirectResponse($this->generateUrl('login'));
$response->headers->clearCookie('token');
return $response;
}
src/Controller/DefaultController.php
/**
* @Route("/nav", name="nav")
*/
public function nav(Request $request, JwtAuthentication $jwt)
{
$response = $this->render('nav.twig', [
'validJwt' => $jwt->validate(
$request->cookies->get('token')
)])
->setVary('X-Login',false)
->setMaxAge(100)
->setSharedMaxAge(500)
->setPublic();
return $response;
}
src/Controller/DefaultController.php
<nav class="navbar navbar-default navbar-fixed-side">
<ul class="nav">
<li><a href="{{ url('home') }}">Home</a></li>
{% if validJwt == true %}
<li><a href="{{ url('logout') }}">{{ 'log_out' | trans }}</a></li>
{% else %}
<li><a href="{{ url('login') }}">{{ 'log_in' | trans }}</a></li>
{% endif %}
<li><a href="{{ url('private') }}">Private</a></li>
</ul>
</nav>
templates/nav.twig
{% extends "base.twig" %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="page-header">
<h1>{{ 'log_in' | trans }}</h1>
</div>
<form class="form-horizontal" method="post" action="{{ url('loginpost') }}">
<div class="form-group">
<label for="usernameInput" class="col-sm-2 control-label">{{ 'username' | trans }}</label>
<div class="col-sm-6">
<input type="text" name="username" class="form-control" id="usernameInput"
placeholder="{{ 'username' | trans }}">
</div>
</div>
<div class="form-group">
<label for="passwordInput" class="col-sm-2 control-label">{{ 'password' | trans }}</label>
<div class="col-sm-6">
<input type="password" name="password" class="form-control" id="passwordInput"
placeholder="{{ 'password' | trans }}">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">{{ 'log_in' | trans }}</button>
</div>
</div>
</form>
{% endblock %}
templates/login.twig
Cookie:
token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1Ni
J9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTUyMDk0M
jExNSwibG9naW4iOnRydWV9._161Lf8BKkbzjqVv
5d62O5aMdCotKvCqd7F8qFqZC2Y
With the proper VCL, Varnish
can process the JWT and
make cache variations for
authenticated & anonymous
content
https://github.com/ThijsFeryn/
cacheable-sites-symfony4
https://feryn.eu
https://twitter.com/ThijsFeryn
https://instagram.com/ThijsFeryn
Developing cacheable PHP applications - PHPLimburgBE 2018

More Related Content

What's hot

Interactive web. O rly?
Interactive web. O rly?Interactive web. O rly?
Interactive web. O rly?
timbc
 
Genkidama:実装と課題
Genkidama:実装と課題Genkidama:実装と課題
Genkidama:実装と課題
Takuya ASADA
 

What's hot (20)

Advanced HTTP Caching
Advanced HTTP CachingAdvanced HTTP Caching
Advanced HTTP Caching
 
Care and feeding notes
Care and feeding notesCare and feeding notes
Care and feeding notes
 
ApacheConNA 2015: What's new in Apache httpd 2.4
ApacheConNA 2015: What's new in Apache httpd 2.4ApacheConNA 2015: What's new in Apache httpd 2.4
ApacheConNA 2015: What's new in Apache httpd 2.4
 
PyGotham 2014 Introduction to Profiling
PyGotham 2014 Introduction to ProfilingPyGotham 2014 Introduction to Profiling
PyGotham 2014 Introduction to Profiling
 
LCA2014 - Introduction to Go
LCA2014 - Introduction to GoLCA2014 - Introduction to Go
LCA2014 - Introduction to Go
 
Devoxx Maroc 2015 HTTP 1, HTTP 2 and folks
Devoxx Maroc  2015 HTTP 1, HTTP 2 and folksDevoxx Maroc  2015 HTTP 1, HTTP 2 and folks
Devoxx Maroc 2015 HTTP 1, HTTP 2 and folks
 
HTTP caching with Varnish
HTTP caching with VarnishHTTP caching with Varnish
HTTP caching with Varnish
 
Caching the uncacheable with Varnish - DevDays 2021
Caching the uncacheable with Varnish - DevDays 2021Caching the uncacheable with Varnish - DevDays 2021
Caching the uncacheable with Varnish - DevDays 2021
 
Web::Scraper
Web::ScraperWeb::Scraper
Web::Scraper
 
Interactive web. O rly?
Interactive web. O rly?Interactive web. O rly?
Interactive web. O rly?
 
ApacheCon 2014 - What's New in Apache httpd 2.4
ApacheCon 2014 - What's New in Apache httpd 2.4ApacheCon 2014 - What's New in Apache httpd 2.4
ApacheCon 2014 - What's New in Apache httpd 2.4
 
Altitude San Francisco 2018: Programming the Edge
Altitude San Francisco 2018: Programming the EdgeAltitude San Francisco 2018: Programming the Edge
Altitude San Francisco 2018: Programming the Edge
 
Northeast PHP - High Performance PHP
Northeast PHP - High Performance PHPNortheast PHP - High Performance PHP
Northeast PHP - High Performance PHP
 
HipHop VM: overclocking Symfony
HipHop VM: overclocking SymfonyHipHop VM: overclocking Symfony
HipHop VM: overclocking Symfony
 
Going crazy with Varnish and Symfony
Going crazy with Varnish and SymfonyGoing crazy with Varnish and Symfony
Going crazy with Varnish and Symfony
 
Genkidama:実装と課題
Genkidama:実装と課題Genkidama:実装と課題
Genkidama:実装と課題
 
Site Performance Optimization for Joomla #jwc13
Site Performance Optimization for Joomla #jwc13Site Performance Optimization for Joomla #jwc13
Site Performance Optimization for Joomla #jwc13
 
Apache and PHP: Why httpd.conf is your new BFF!
Apache and PHP: Why httpd.conf is your new BFF!Apache and PHP: Why httpd.conf is your new BFF!
Apache and PHP: Why httpd.conf is your new BFF!
 
Single page apps with drupal 7
Single page apps with drupal 7Single page apps with drupal 7
Single page apps with drupal 7
 
From zero to hero - Easy log centralization with Logstash and Elasticsearch
From zero to hero - Easy log centralization with Logstash and ElasticsearchFrom zero to hero - Easy log centralization with Logstash and Elasticsearch
From zero to hero - Easy log centralization with Logstash and Elasticsearch
 

Similar to Developing cacheable PHP applications - PHPLimburgBE 2018

Choosing A Proxy Server - Apachecon 2014
Choosing A Proxy Server - Apachecon 2014Choosing A Proxy Server - Apachecon 2014
Choosing A Proxy Server - Apachecon 2014
bryan_call
 
Caching with Varnish
Caching with VarnishCaching with Varnish
Caching with Varnish
schoefmax
 
Optimizing CakePHP 2.x Apps
Optimizing CakePHP 2.x AppsOptimizing CakePHP 2.x Apps
Optimizing CakePHP 2.x Apps
Juan Basso
 

Similar to Developing cacheable PHP applications - PHPLimburgBE 2018 (20)

Less and faster – Cache tips for WordPress developers
Less and faster – Cache tips for WordPress developersLess and faster – Cache tips for WordPress developers
Less and faster – Cache tips for WordPress developers
 
Caching the Uncacheable
Caching the UncacheableCaching the Uncacheable
Caching the Uncacheable
 
What's New and Newer in Apache httpd-24
What's New and Newer in Apache httpd-24What's New and Newer in Apache httpd-24
What's New and Newer in Apache httpd-24
 
Drupal Performance - SerBenfiquista.com Case Study
Drupal Performance - SerBenfiquista.com Case StudyDrupal Performance - SerBenfiquista.com Case Study
Drupal Performance - SerBenfiquista.com Case Study
 
Caching and tuning fun for high scalability
Caching and tuning fun for high scalabilityCaching and tuning fun for high scalability
Caching and tuning fun for high scalability
 
Altitude SF 2017: Optimizing your hit rate
Altitude SF 2017: Optimizing your hit rateAltitude SF 2017: Optimizing your hit rate
Altitude SF 2017: Optimizing your hit rate
 
WordPress At Scale. WordCamp Dhaka 2019
WordPress At Scale. WordCamp Dhaka 2019WordPress At Scale. WordCamp Dhaka 2019
WordPress At Scale. WordCamp Dhaka 2019
 
MNPHP Scalable Architecture 101 - Feb 3 2011
MNPHP Scalable Architecture 101 - Feb 3 2011MNPHP Scalable Architecture 101 - Feb 3 2011
MNPHP Scalable Architecture 101 - Feb 3 2011
 
VUG5: Varnish at Opera Software
VUG5: Varnish at Opera SoftwareVUG5: Varnish at Opera Software
VUG5: Varnish at Opera Software
 
Choosing A Proxy Server - Apachecon 2014
Choosing A Proxy Server - Apachecon 2014Choosing A Proxy Server - Apachecon 2014
Choosing A Proxy Server - Apachecon 2014
 
REST in ( a mobile ) peace @ WHYMCA 05-21-2011
REST in ( a mobile ) peace @ WHYMCA 05-21-2011REST in ( a mobile ) peace @ WHYMCA 05-21-2011
REST in ( a mobile ) peace @ WHYMCA 05-21-2011
 
20190516 web security-basic
20190516 web security-basic20190516 web security-basic
20190516 web security-basic
 
Caching with Varnish
Caching with VarnishCaching with Varnish
Caching with Varnish
 
Profiling PHP with Xdebug / Webgrind
Profiling PHP with Xdebug / WebgrindProfiling PHP with Xdebug / Webgrind
Profiling PHP with Xdebug / Webgrind
 
Optimizing CakePHP 2.x Apps
Optimizing CakePHP 2.x AppsOptimizing CakePHP 2.x Apps
Optimizing CakePHP 2.x Apps
 
Client Side Performance @ Xero
Client Side Performance @ XeroClient Side Performance @ Xero
Client Side Performance @ Xero
 
Ajax to the Moon
Ajax to the MoonAjax to the Moon
Ajax to the Moon
 
Into The Box 2018 Ortus Keynote
Into The Box 2018 Ortus KeynoteInto The Box 2018 Ortus Keynote
Into The Box 2018 Ortus Keynote
 
Spreadshirt Techcamp 2018 - Hold until Told
Spreadshirt Techcamp 2018 - Hold until ToldSpreadshirt Techcamp 2018 - Hold until Told
Spreadshirt Techcamp 2018 - Hold until Told
 
HTTP cache @ PUG Rome 03-29-2011
HTTP cache @ PUG Rome 03-29-2011HTTP cache @ PUG Rome 03-29-2011
HTTP cache @ PUG Rome 03-29-2011
 

More from Thijs Feryn

More from Thijs Feryn (14)

10 things that helped me advance my career - PHP UK Conference 2024
10 things that helped me advance my career - PHP UK Conference 202410 things that helped me advance my career - PHP UK Conference 2024
10 things that helped me advance my career - PHP UK Conference 2024
 
Distributed load testing with K6 - NDC London 2024
Distributed load testing with K6 - NDC London 2024Distributed load testing with K6 - NDC London 2024
Distributed load testing with K6 - NDC London 2024
 
HTTP headers that make your website go faster - devs.gent November 2023
HTTP headers that make your website go faster - devs.gent November 2023HTTP headers that make your website go faster - devs.gent November 2023
HTTP headers that make your website go faster - devs.gent November 2023
 
Living on the edge - EBU Horizons 2023
Living on the edge - EBU Horizons 2023Living on the edge - EBU Horizons 2023
Living on the edge - EBU Horizons 2023
 
Distributed Load Testing with k6 - DevOps Barcelona
Distributed Load Testing with k6 - DevOps BarcelonaDistributed Load Testing with k6 - DevOps Barcelona
Distributed Load Testing with k6 - DevOps Barcelona
 
Core web vitals meten om je site sneller te maken - Combell Partner Day 2023
Core web vitals meten om je site sneller te maken - Combell Partner Day 2023Core web vitals meten om je site sneller te maken - Combell Partner Day 2023
Core web vitals meten om je site sneller te maken - Combell Partner Day 2023
 
HTTP headers that make your website go faster
HTTP headers that make your website go fasterHTTP headers that make your website go faster
HTTP headers that make your website go faster
 
HTTP headers that will make your website go faster
HTTP headers that will make your website go fasterHTTP headers that will make your website go faster
HTTP headers that will make your website go faster
 
Distributed load testing with k6
Distributed load testing with k6Distributed load testing with k6
Distributed load testing with k6
 
HTTP logging met Varnishlog - PHPWVL 2022
HTTP logging met Varnishlog - PHPWVL 2022HTTP logging met Varnishlog - PHPWVL 2022
HTTP logging met Varnishlog - PHPWVL 2022
 
Taking Laravel to the edge with HTTP caching and Varnish
Taking Laravel to the edge with HTTP caching and VarnishTaking Laravel to the edge with HTTP caching and Varnish
Taking Laravel to the edge with HTTP caching and Varnish
 
Build your own CDN with Varnish - Confoo 2022
Build your own CDN with Varnish - Confoo 2022Build your own CDN with Varnish - Confoo 2022
Build your own CDN with Varnish - Confoo 2022
 
Developing cacheable backend applications - Appdevcon 2019
Developing cacheable backend applications - Appdevcon 2019Developing cacheable backend applications - Appdevcon 2019
Developing cacheable backend applications - Appdevcon 2019
 
How Cloud addresses the needs of todays internet - Korazon 2018
How Cloud addresses the needs of todays internet - Korazon 2018How Cloud addresses the needs of todays internet - Korazon 2018
How Cloud addresses the needs of todays internet - Korazon 2018
 

Recently uploaded

Recently uploaded (20)

Powerful Start- the Key to Project Success, Barbara Laskowska
Powerful Start- the Key to Project Success, Barbara LaskowskaPowerful Start- the Key to Project Success, Barbara Laskowska
Powerful Start- the Key to Project Success, Barbara Laskowska
 
How Red Hat Uses FDO in Device Lifecycle _ Costin and Vitaliy at Red Hat.pdf
How Red Hat Uses FDO in Device Lifecycle _ Costin and Vitaliy at Red Hat.pdfHow Red Hat Uses FDO in Device Lifecycle _ Costin and Vitaliy at Red Hat.pdf
How Red Hat Uses FDO in Device Lifecycle _ Costin and Vitaliy at Red Hat.pdf
 
WSO2CONMay2024OpenSourceConferenceDebrief.pptx
WSO2CONMay2024OpenSourceConferenceDebrief.pptxWSO2CONMay2024OpenSourceConferenceDebrief.pptx
WSO2CONMay2024OpenSourceConferenceDebrief.pptx
 
Enterprise Knowledge Graphs - Data Summit 2024
Enterprise Knowledge Graphs - Data Summit 2024Enterprise Knowledge Graphs - Data Summit 2024
Enterprise Knowledge Graphs - Data Summit 2024
 
A Business-Centric Approach to Design System Strategy
A Business-Centric Approach to Design System StrategyA Business-Centric Approach to Design System Strategy
A Business-Centric Approach to Design System Strategy
 
PLAI - Acceleration Program for Generative A.I. Startups
PLAI - Acceleration Program for Generative A.I. StartupsPLAI - Acceleration Program for Generative A.I. Startups
PLAI - Acceleration Program for Generative A.I. Startups
 
Demystifying gRPC in .Net by John Staveley
Demystifying gRPC in .Net by John StaveleyDemystifying gRPC in .Net by John Staveley
Demystifying gRPC in .Net by John Staveley
 
Extensible Python: Robustness through Addition - PyCon 2024
Extensible Python: Robustness through Addition - PyCon 2024Extensible Python: Robustness through Addition - PyCon 2024
Extensible Python: Robustness through Addition - PyCon 2024
 
UiPath Test Automation using UiPath Test Suite series, part 1
UiPath Test Automation using UiPath Test Suite series, part 1UiPath Test Automation using UiPath Test Suite series, part 1
UiPath Test Automation using UiPath Test Suite series, part 1
 
Optimizing NoSQL Performance Through Observability
Optimizing NoSQL Performance Through ObservabilityOptimizing NoSQL Performance Through Observability
Optimizing NoSQL Performance Through Observability
 
Speed Wins: From Kafka to APIs in Minutes
Speed Wins: From Kafka to APIs in MinutesSpeed Wins: From Kafka to APIs in Minutes
Speed Wins: From Kafka to APIs in Minutes
 
Intro in Product Management - Коротко про професію продакт менеджера
Intro in Product Management - Коротко про професію продакт менеджераIntro in Product Management - Коротко про професію продакт менеджера
Intro in Product Management - Коротко про професію продакт менеджера
 
IoT Analytics Company Presentation May 2024
IoT Analytics Company Presentation May 2024IoT Analytics Company Presentation May 2024
IoT Analytics Company Presentation May 2024
 
Choosing the Right FDO Deployment Model for Your Application _ Geoffrey at In...
Choosing the Right FDO Deployment Model for Your Application _ Geoffrey at In...Choosing the Right FDO Deployment Model for Your Application _ Geoffrey at In...
Choosing the Right FDO Deployment Model for Your Application _ Geoffrey at In...
 
Free and Effective: Making Flows Publicly Accessible, Yumi Ibrahimzade
Free and Effective: Making Flows Publicly Accessible, Yumi IbrahimzadeFree and Effective: Making Flows Publicly Accessible, Yumi Ibrahimzade
Free and Effective: Making Flows Publicly Accessible, Yumi Ibrahimzade
 
WebAssembly is Key to Better LLM Performance
WebAssembly is Key to Better LLM PerformanceWebAssembly is Key to Better LLM Performance
WebAssembly is Key to Better LLM Performance
 
AI presentation and introduction - Retrieval Augmented Generation RAG 101
AI presentation and introduction - Retrieval Augmented Generation RAG 101AI presentation and introduction - Retrieval Augmented Generation RAG 101
AI presentation and introduction - Retrieval Augmented Generation RAG 101
 
Integrating Telephony Systems with Salesforce: Insights and Considerations, B...
Integrating Telephony Systems with Salesforce: Insights and Considerations, B...Integrating Telephony Systems with Salesforce: Insights and Considerations, B...
Integrating Telephony Systems with Salesforce: Insights and Considerations, B...
 
Buy Epson EcoTank L3210 Colour Printer Online.pdf
Buy Epson EcoTank L3210 Colour Printer Online.pdfBuy Epson EcoTank L3210 Colour Printer Online.pdf
Buy Epson EcoTank L3210 Colour Printer Online.pdf
 
SOQL 201 for Admins & Developers: Slice & Dice Your Org’s Data With Aggregate...
SOQL 201 for Admins & Developers: Slice & Dice Your Org’s Data With Aggregate...SOQL 201 for Admins & Developers: Slice & Dice Your Org’s Data With Aggregate...
SOQL 201 for Admins & Developers: Slice & Dice Your Org’s Data With Aggregate...
 

Developing cacheable PHP applications - PHPLimburgBE 2018

  • 6.
  • 7.
  • 10.
  • 13.
  • 16. With ReCaPro * User ReCaPro Server * Reverse Caching Proxy
  • 18.
  • 19. HTTP caching mechanisms Expires: Sat, 09 Sep 2017 14:30:00 GMT Cache-control: public, max-age=3600, s-maxage=86400 Cache-control: private, no-cache, no-store
  • 20. In an ideal world
  • 21. ✓Stateless ✓Well-defined TTL ✓Cache / no-cache per resource ✓Cache variations ✓Conditional requests ✓HTTP fragments for non- cacheable content In an ideal world
  • 24.
  • 25.
  • 30.
  • 32.
  • 34. composer require symfony/flex composer require annotations twig translation
  • 35. <?php namespace AppController; use SymfonyComponentHttpFoundationRequest; use SensioBundleFrameworkExtraBundleConfigurationRoute; use SymfonyBundleFrameworkBundleControllerController; class DefaultController extends Controller { /** * @Route("/", name="home") */ public function index() { return $this->render('index.twig'); } } src/Controller/DefaultController.php
  • 36. <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/html" xmlns:hx="http://purl.org/NET/hinclude"> <head> <title>{% block title %}{% endblock %}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> </head> <body> <div class="container-fluid"> {{ include('header.twig') }} <div class="row"> <div class="col-sm-3 col-lg-2"> {{ include('nav.twig') }} </div> <div class="col-sm-9 col-lg-10"> {% block content %}{% endblock %} </div> </div> {{ include('footer.twig') }} </div> </body> </html> templates/base.twig
  • 37. {% extends "base.twig" %} {% block title %}Home{% endblock %} {% block content %} <div class="page-header"> <h1>{{ 'example' | trans }}</h1> </div> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat orci eget libero sollicitudin, non ultrices turpis mollis. Aliquam sit amet tempus elit. Ut viverra risus enim, ut venenatis justo accumsan nec. Praesent a dolor tellus. Maecenas non mauris leo. Pellentesque lobortis turpis at dapibus laoreet. Mauris rhoncus nulla et urna mollis, et lobortis magna ornare. Etiam et sapien consequat, egestas felis sit amet, dignissim enim.</p> <p>Quisque quis mollis justo, imperdiet fermentum velit. Aliquam nulla justo, consectetur et diam non, luctus commodo metus. Vestibulum fermentum efficitur nulla non luctus. Nunc lorem nunc, mollis id efficitur et, aliquet sit amet ante. Sed ipsum turpis, vehicula eu semper eu, malesuada eget leo. Vestibulum venenatis dui id pulvinar suscipit. Etiam nec massa pharetra justo pharetra dignissim quis non magna. Integer id convallis lectus. Nam non ullamcorper metus. Ut vestibulum ex ut massa posuere tincidunt. Vestibulum hendrerit neque id lorem rhoncus aliquam. Duis a facilisis metus, a faucibus nulla.</p> {% endblock %} templates/index.twig
  • 38. <nav class="navbar navbar-default navbar-fixed-side"> <ul class="nav"> <li><a href="{{ url('home') }}">Home</a></li> <li><a href="{{ url('login') }}">{{ 'log_in' | trans }}</a></li> <li><a href="{{ url('private') }}">Private</a></li> </ul> </nav> templates/nav.twig
  • 40. home: Home welcome : Welcome to the site rendered : Rendered at %date% example : An example page log_in : Log in login : Login log_out : Log out username : Username password : Password private : Private privatetext : Looks like some very private data translations/messages.en.yml
  • 41. home: Start welcome : Welkom op de site rendered : Samengesteld op %date% example : Een voorbeeldpagina log_in : Inloggen login : Login log_out : Uitloggen username : Gebruikersnaam password : Wachtwoord private : Privé privatetext : Deze tekst ziet er vrij privé uit translations/messages.nl.yml
  • 42. <?php namespace AppEventListener; use SymfonyComponentHttpKernelEventGetResponseEvent; class LocaleListener { public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); $preferredLanguage = $request->getPreferredLanguage(); if(null !== $preferredLanguage) { $request->setLocale($preferredLanguage); } } } src/EventListener/LocaleListener.php
  • 43. services: _defaults: autowire: true autoconfigure: true public: false App: resource: '../src/*' exclude: '../src/{Entity,Migrations,Tests,Kernel.php}' AppController: resource: '../src/Controller' tags: ['controller.service_arguments'] AppEventListenerLocaleListener: tags: - { name: kernel.event_listener, event: kernel.request, priority: 100} config/services.yml
  • 45.
  • 46.
  • 50. <?php namespace AppController; use SymfonyComponentHttpFoundationRequest; use SensioBundleFrameworkExtraBundleConfigurationRoute; use SymfonyBundleFrameworkBundleControllerController; class DefaultController extends Controller { /** * @Route("/", name="home") */ public function index() { return $this ->render('index.twig') ->setSharedMaxAge(500) ->setPublic(); } }
  • 52. Only fetch payload that has changed
  • 55. Conditional requests HTTP/1.1 200 OK Host: localhost Etag: 7c9d70604c6061da9bb9377d3f00eb27 Content-type: text/html; charset=UTF-8 Hello world output GET / HTTP/1.1 Host: localhost User-Agent: curl/7.48.0
  • 56. Conditional requests HTTP/1.0 304 Not Modified Host: localhost Etag: 7c9d70604c6061da9bb9377d3f00eb27 GET / HTTP/1.1 Host: localhost User-Agent: curl/7.48.0 If-None-Match: 7c9d70604c6061da9bb9377d3f00eb27
  • 57. Conditional requests HTTP/1.1 200 OK Host: localhost Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT Content-type: text/html; charset=UTF-8 Hello world output GET / HTTP/1.1 Host: localhost User-Agent: curl/7.48.0
  • 58. Conditional requests HTTP/1.0 304 Not Modified Host: localhost Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT GET / HTTP/1.1 Host: localhost User-Agent: curl/7.48.0 If-Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT
  • 60. <?php namespace AppEventListener; use SymfonyBridgeMonologLogger; use SymfonyComponentHttpFoundationResponse; use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpKernelEventGetResponseEvent; use SymfonyComponentHttpKernelEventFilterResponseEvent; use SymfonyBundlesRedisBundleRedisClient as RedisClient; class ConditionalRequestListener { protected $redis; protected $logger; public function __construct(RedisClient $redis) { $this->redis = $redis; } protected function isModified(Request $request, $etag) { if ($etags = $request->getETags()) { return in_array($etag, $etags) || in_array('*', $etags); } return true; } ... src/EventListener/ConditionalRequestListener.php
  • 61. { $this->redis = $redis; $this->logger = $logger; } protected function isModified(Request $request, $etag) { if ($etags = $request->getETags()) { return in_array($etag, $etags) || in_array('*', $etags); } return true; } public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); $etag = $this->redis->get('etag:'.md5($request->getUri())); if(!$this->isModified($request,$etag)) { $event->setResponse(Response::create('Not Modified',Response::HTTP_NOT_MODIFIED)); } } public function onKernelResponse(FilterResponseEvent $event) { $response = $event->getResponse(); $request = $event->getRequest(); $etag = md5($response->getContent()); $response->setEtag($etag); if($this->isModified($request,$etag)) { $this->redis->set('etag:'.md5($request->getUri()),$etag); } } } src/EventListener/ConditionalRequestListener.php
  • 64. /** * @Route("/private", name="private") */ public function private() { $response = $this ->render('private.twig') ->setPrivate(); $response->headers->addCacheControlDirective('no-store'); return $response; } Dot not cache private page
  • 66.
  • 70. <esi:include src="/header" /> Edge Side Includes ✓Placeholder ✓Parsed by Varnish ✓Output is a composition of blocks ✓State per block ✓TTL per block
  • 73. ✓ Server-side ✓ Standardized ✓ Processed on the “edge”, no in the browser ✓ Generally faster Edge-Side Includes - Sequential - One fails, all fail - Limited implementation in Varnish
  • 74. ✓ Client-side ✓ Common knowledge ✓ Parallel processing ✓ Graceful degradation AJAX - Processed by the browser - Extra roundtrips - Somewhat slower
  • 76. <div class="container-fluid">
 {{ include('header.twig') }}
 <div class="row">
 <div class="col-sm-3 col-lg-2">
 {{ include('nav.twig') }}
 </div>
 <div class="col-sm-9 col-lg-10">
 {% block content %}{% endblock %}
 </div>
 </div>
 {{ include('footer.twig') }}
 </div> <div class="container-fluid">
 {{ render_esi(url('header')) }}
 <div class="row">
 <div class="col-sm-3 col-lg-2">
 {{ render_esi(url('nav')) }}
 </div>
 <div class="col-sm-9 col-lg-10">
 {% block content %}{% endblock %}
 </div>
 </div>
 {{ render_esi(url('footer')) }}
 </div>
  • 77. <div class="container-fluid"> <esi:include src="/header" /> <div class="row"> <div class="col-sm-3 col-lg-2"> <esi:include src="/nav" /> </div> <div class="col-sm-9 col-lg-10"> <div class="page-header"> <h1>An example page <small>Rendered at 2017-05-17 16:57:14</small></h1> </div> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat orci eget libero sollicitudin,…</p> </div> </div> <esi:include src="/footer" /> </div>
  • 78. /** * @Route("/", name="home") */ public function index() { return $this ->render('index.twig') ->setPublic() ->setSharedMaxAge(500); } /** * @Route("/header", name="header") */ public function header() { $response = $this ->render('header.twig') ->setPrivate(); $response->headers->addCacheControlDirective('no-store'); return $response; } /** * @Route("/footer", name="footer") */ public function footer() { $response = $this->render('footer.twig'); $response ->setSharedMaxAge(500) ->setPublic(); return $response; } /** * @Route("/nav", name="nav") */ public function nav() { $response = $this->render('nav.twig'); $response ->setVary('X-Login',false) ->setSharedMaxAge(500) ->setPublic(); return $response; } Controller action per fragment
  • 81. <?php namespace AppEventListener; use SymfonyComponentHttpKernelEventFilterResponseEvent; class VaryListener { public function onKernelResponse(FilterResponseEvent $event) { $response = $event->getResponse(); $response->setVary('Accept-Language',false); } } src/EventListener/VaryListener.php
  • 82.
  • 83. ✓Navigation page ✓Private page Weak spots Not cached because of stateful content
  • 85. Replace PHP session with JSON Web Tokens
  • 87. eyJzdWIiOiJhZG1pbiIsIm V4cCI6MTQ5NTUyODc1Niwi bG9naW4iOnRydWV9 { "alg": "HS256",  "typ": "JWT" } { "sub": "admin", "exp": 1495528756, "login": true } HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) eyJhbGciOiJIUzI1NiIsI nR5cCI6IkpXVCJ9 u4Idy- SYnrFdnH1h9_sNc4OasOR BJcrh2fPo1EOTre8
  • 89. sub jwt { std.log("Ready to perform some JWT magic"); if(cookie.isset("jwt_cookie")) { #Extract header data from JWT var.set("token", cookie.get("jwt_cookie")); var.set("header", regsub(var.get("token"),"([^.]+).[^.]+.[^.]+","1")); var.set("type", regsub(digest.base64url_decode(var.get("header")),{"^.*?"typ"s*:s*"(w+)".*?$"},"1")); var.set("algorithm", regsub(digest.base64url_decode(var.get("header")),{"^.*?"alg"s*:s*"(w+)".*?$"},"1")); #Don't allow invalid JWT header if(var.get("type") == "JWT" && var.get("algorithm") == "HS256") { #Extract signature & payload data from JWT var.set("rawPayload",regsub(var.get("token"),"[^.]+.([^.]+).[^.]+$","1")); var.set("signature",regsub(var.get("token"),"^[^.]+.[^.]+.([^.]+)$","1")); var.set("currentSignature",digest.base64url_nopad_hex(digest.hmac_sha256(var.get("key"),var.get("header") + "." + var.get("rawPayload")))); var.set("payload", digest.base64url_decode(var.get("rawPayload"))); var.set("exp",regsub(var.get("payload"),{"^.*?"exp"s*:s*([0-9]+).*?$"},"1")); var.set("jti",regsub(var.get("payload"),{"^.*?"jti"s*:s*"([a-z0-9A-Z_-]+)".*?$"},"1")); var.set("userId",regsub(var.get("payload"),{"^.*?"uid"s*:s*"([0-9]+)".*?$"},"1")); var.set("roles",regsub(var.get("payload"),{"^.*?"roles"s*:s*"([a-z0-9A-Z_-, ]+)".*?$"},"1")); #Only allow valid userId if(var.get("userId") ~ "^d+$") { #Don't allow expired JWT if(std.time(var.get("exp"),now) >= now) { #SessionId should match JTI value from JWT if(cookie.get(var.get("sessionCookie")) == var.get("jti")) { #Don't allow invalid JWT signature if(var.get("signature") == var.get("currentSignature")) { #The sweet spot set req.http.X-login="true"; } else { std.log("JWT: signature doesn't match. Received: " + var.get("signature") + ", expected: " + var.get("currentSignature")); } } else { std.log("JWT: session cookie doesn't match JTI." + var.get("sessionCookie") + ": " + cookie.get(var.get("sessionCookie")) + ", JTI:" + var.get("jti")); } } else { std.log("JWT: token has expired"); } } else { std.log("UserId '"+ var.get("userId") +"', is not numeric"); } } else { std.log("JWT: type is not JWT or algorithm is not HS256"); } std.log("JWT processing finished. UserId: " + var.get("userId") + ". X-Login: " + req.http.X-login); } #Look for full private content if(req.url ~ "/node/2" && req.url !~ "^/user/login") { if(req.http.X-login != "true") { return(synth(302,"/user/login?destination=" + req.url)); } } } Insert incomprehensible Varnish VCL code here …
  • 90. X-Login: true End result: X-Login: false Custom request header set by Varnish
  • 92. Vary: Accept-Language, X-Login Content for logged-in & anonymous differs
  • 93. <script language="JavaScript">
 function getCookie(name) {
 var value = "; " + document.cookie;
 var parts = value.split("; " + name + "=");
 if (parts.length == 2) return parts.pop().split(";").shift();
 }
 function parseJwt (token) {
 var base64Url = token.split('.')[1];
 var base64 = base64Url.replace('-', '+').replace('_', '/');
 return JSON.parse(window.atob(base64));
 };
 $(document).ready(function(){
 if ($.cookie('token') != null ){
 var token = parseJwt($.cookie("token"));
 $("#usernameLabel").html(', ' + token.sub);
 }
 });
 </script> Parse JWT in Javascript
  • 96. <?php namespace AppService; use FirebaseJWTJWT; use SymfonyComponentHttpFoundationCookie; class JwtAuthentication { protected $key; protected $username; protected $password; public function __construct($key,$username,$password) { $this->key = $key; $this->username = $username; $this->password = $password; } public function jwt($username) { return JWT::encode([ 'sub'=>$username, 'exp'=>time() + (4 * 24 * 60 * 60), 'login'=>true, ],$this->key); } public function createCookie($username) { return new Cookie("token",$this->jwt($username), time() + (3600 * 48), '/', null, false, false); } public function validate($token) { src/Service/JwtAuthentication.php
  • 97. $this->password = $password; } public function jwt($username) { return JWT::encode([ 'sub'=>$username, 'exp'=>time() + (4 * 24 * 60 * 60), //'exp'=>time() + 60, 'login'=>true, ],$this->key); } public function createCookie($username) { return new Cookie("token",$this->jwt($username), time() + (3600 * 48), '/', null, false, false); } public function validate($token) { try { $data = JWT::decode($token,$this->key,['HS256']); $data = (array)$data; if($data['sub'] !== $this->username) { return false; } return true; } catch(UnexpectedValueException $e) { return false; } } } src/Service/JwtAuthentication.php
  • 99. ###> JWT authentication ### JWT_KEY=SlowWebSitesSuck JWT_USERNAME=admin JWT_PASSWORD=$2y$10$431rvq1qS9ewNFP0Gti/o.kBbuMK4zs8IDTLlxm5uzV7cbv8wKt0K ###< JWT authentication ### .env
  • 100. /** * @Route("/login", name="login", methods="GET") */ public function login(Request $request, JwtAuthentication $jwt) { if($jwt->validate($request->cookies->get('token'))) { return new RedirectResponse($this->generateUrl('home')); } $response = $this->render('login.twig',['loginLogoutUrl'=>$this- >generateUrl('login'),'loginLogoutLabel'=>'log_in']); $response ->setSharedMaxAge(500) ->setVary('X-Login',false) ->setPublic(); return $response; } /** * @Route("/login", name="loginpost", methods="POST") */ public function loginpost(Request $request, JwtAuthentication $jwt) { $username = $request->get('username'); $password = $request->get('password'); if(!$username || !$password || getenv('JWT_USERNAME') != $username || ! password_verify($password,getenv('JWT_PASSWORD'))) { return new RedirectResponse($this->generateUrl('login')); } $response = new RedirectResponse($this->generateUrl('home')); $response->headers->setCookie($jwt->createCookie($username)); return $response; } src/Controller/DefaultController.php
  • 101. /** * @Route("/logout", name="logout") */ public function logout() { $response = new RedirectResponse($this->generateUrl('login')); $response->headers->clearCookie('token'); return $response; } src/Controller/DefaultController.php
  • 102. /** * @Route("/nav", name="nav") */ public function nav(Request $request, JwtAuthentication $jwt) { $response = $this->render('nav.twig', [ 'validJwt' => $jwt->validate( $request->cookies->get('token') )]) ->setVary('X-Login',false) ->setMaxAge(100) ->setSharedMaxAge(500) ->setPublic(); return $response; } src/Controller/DefaultController.php
  • 103. <nav class="navbar navbar-default navbar-fixed-side"> <ul class="nav"> <li><a href="{{ url('home') }}">Home</a></li> {% if validJwt == true %} <li><a href="{{ url('logout') }}">{{ 'log_out' | trans }}</a></li> {% else %} <li><a href="{{ url('login') }}">{{ 'log_in' | trans }}</a></li> {% endif %} <li><a href="{{ url('private') }}">Private</a></li> </ul> </nav> templates/nav.twig
  • 104. {% extends "base.twig" %} {% block title %}Login{% endblock %} {% block content %} <div class="page-header"> <h1>{{ 'log_in' | trans }}</h1> </div> <form class="form-horizontal" method="post" action="{{ url('loginpost') }}"> <div class="form-group"> <label for="usernameInput" class="col-sm-2 control-label">{{ 'username' | trans }}</label> <div class="col-sm-6"> <input type="text" name="username" class="form-control" id="usernameInput" placeholder="{{ 'username' | trans }}"> </div> </div> <div class="form-group"> <label for="passwordInput" class="col-sm-2 control-label">{{ 'password' | trans }}</label> <div class="col-sm-6"> <input type="password" name="password" class="form-control" id="passwordInput" placeholder="{{ 'password' | trans }}"> </div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button type="submit" class="btn btn-default">{{ 'log_in' | trans }}</button> </div> </div> </form> {% endblock %} templates/login.twig
  • 105.
  • 106.
  • 108. With the proper VCL, Varnish can process the JWT and make cache variations for authenticated & anonymous content