Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Developing cacheable PHP applications - PHP Bar...

Avatar for Thijs Feryn Thijs Feryn
November 12, 2019

Developing cacheable PHP applications - PHP Barcelona 2019

See https://feryn.eu/speaking/developing-cacheable-php-applications/ for more information about this presentation.

Avatar for Thijs Feryn

Thijs Feryn

November 12, 2019
Tweet

More Decks by Thijs Feryn

Other Decks in Technology

Transcript

  1. HTTP CACHING MECHANISMS Expires: Sat, 23 July 2019 00:30:00 GMT

    Cache-control: public, max-age=3600, s-maxage=86400 Cache-control: private, no-cache, no-store
  2. ✓STATELESS ✓WELL-DEFINED TTL ✓CACHE / NO-CACHE PER RESOURCE ✓CACHE VARIATIONS

    ✓CONDITIONAL REQUESTS ✓PLACEHOLDERS FOR NON-CACHEABLE CONTENT ✓EDGE-SIDE LOGIC FOR PERSONALIZED CACHING IN AN IDEAL WORLD
  3. <?php namespace App\Controller; use Symfony\Component\HttpFoundation\Request; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class

    DefaultController extends Controller { /** * @Route("/", name="home") */ public function index() { return $this ->render('index.twig') ->setSharedMaxAge(500) ->setPublic(); } }
  4. /** * @Route("/private", name="private") */ public function private() { $response

    = $this ->render('private.twig') ->setPrivate(); $response->headers->addCacheControlDirective('no-store'); return $response; }
  5. 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
  6. CONDITIONAL REQUESTS HTTP/1.1 304 Not Modified Host: localhost Etag: 7c9d70604c6061da9bb9377d3f00eb27

    GET / HTTP/1.1 Host: localhost If-None-Match: 7c9d70604c6061da9bb9377d3f00eb27
  7. 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
  8. CONDITIONAL REQUESTS HTTP/1.1 304 Not Modified Host: localhost Last-Modified: Fri,

    22 Jul 2016 10:11:16 GMT GET / HTTP/1.1 Host: localhost If-Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT
  9. <?php namespace App\EventListener; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use

    Symfony\Component\HttpKernel\Event\FilterResponseEvent; use SymfonyBundles\RedisBundle\Redis\Client as RedisClient; class ConditionalRequestListener { protected $redis; 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
  10. { $this->redis = $redis; } 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
  11. ESI ✓ PLACEHOLDER ✓ PARSED BY VARNISH ✓ OUTPUT IS

    A COMPOSITION OF BLOCKS ✓ STATE PER BLOCK ✓ TTL PER BLOCK
  12. sub vcl_recv { set req.http.Surrogate-Capability = "key=ESI/1.0"; } sub vcl_backend_response

    { if (beresp.http.Surrogate-Control ~ "ESI/1.0") { unset beresp.http.Surrogate-Control; set beresp.do_esi = true; } }
  13. EDGE SIDE INCLUDES ✓ SERVER-SIDE ✓ STANDARDIZED ✓ PROCESSED ON

    THE “EDGE”, NOT IN THE BROWSER ✓ GENERALLY FASTER - SEQUENTIAL (ONLY PARALLEL IN ENTERPRISE VERSION) - ONE FAILS, ALL FAIL - LIMITED IMPLEMENTATION IN VARNISH
  14. /** * @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
  15. <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>
  16. <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>
  17. WHAT IF THE CONTENT OF A URL VARIES BASED ON

    THE VALUE OF A REQUEST HEADER?
  18. CACHE VARIATIONS HTTP/1.1 200 OK Host: localhost Content-Language: en Content-type:

    text/html; charset=UTF-8 Hello world output GET / HTTP/1.1 Host: localhost Accept-Language: en, nl, de
  19. sub vcl_recv { if (req.url ~ "^/status\.php$" || req.url ~

    "^/update\.php$" || req.url ~ "^/admin$" || req.url ~ "^/admin/.*$" || req.url ~ "^/flag/.*$" || req.url ~ "^.*/ajax/.*$" || req.url ~ "^.*/ahah/.*$") { return (pass); } } URL BLACKLIST EXAMPLE
  20. sub vcl_recv { if (req.http.Cookie) { set req.http.Cookie = ";"

    + req.http.Cookie; set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";"); set req.http.Cookie = regsuball(req.http.Cookie, ";(SESS[a-z0-9]+|SSESS[a-z0-9]+|NO_CACHE)=", "; \1="); set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", ""); set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", ""); if (req.http.Cookie == "") { unset req.http.Cookie; } else { return (pass); } } } ONLY KEEP CERTAIN COOKIES
  21. vcl 4.1; import cookieplus; import redis; import synthbackend; sub vcl_init

    { new db = redis.db( location="redis:6379", type=master, connection_timeout=500, shared_connections=false, max_connections=1); } sub vcl_recv { set req.http.X-Login = "false"; set req.http.x-session = cookieplus.get("PHPSESSID","guest"); if(req.http.x-session != "guest") { db.command("EXISTS"); db.push("sf_s"+cookieplus.get("PHPSESSID")); db.execute(); if(db.get_integer_reply() == 1) { set req.http.X-Login = "true"; } } } SYNTHETIC HTTP
  22. sub vcl_backend_fetch { if(bereq.url == "/session") { if(bereq.http.X-Login != "true")

    { set bereq.backend = synthbackend.from_string("{}"); return(fetch); } db.command("EVAL"); db.push({" local session = redis.call('GET', KEYS[1]) if session == nil then return '{}' end local result = string.gsub(session, '[%c]', '') local username = string.gsub(result,'.+Userusername\";s:[0-9]+:\"([^\"]+)\";.+','%1') if username == nil then return '{}' end return '{"username":"'.. username ..'"}' "}); db.push(1); db.push("sf_s"+cookieplus.get("PHPSESSID")); db.execute(); set bereq.backend = synthbackend.from_string(db.get_string_reply()); } } sub vcl_backend_response { if(bereq.url == "/session") { set beresp.http.Content-Type = "application/json; charset=utf-8"; set beresp.ttl = 3600s; set beresp.http.vary = "x-session"; } } REDIS LUA CODE
  23. {{ USERNAME }} VARNISH MODULE FOR MUSTACHE PROCESSING ON THE

    EDGE REPLACES PLACEHOLDERS WITH JSON VALUES
  24. vcl 4.1; import edgestash; import std; backend default { .host

    = "1.1.1.1"; } sub vcl_recv { set req.http.Surrogate-Capability={"edgestash="EDGESTASH/2.1""}; } sub vcl_backend_response { if(beresp.http.Link) { std.collect(beresp.http.Link,","); } if(beresp.http.Link ~ "<([^>]+)>; rel=edgestash") { set beresp.http.x-edgestash-json-urls = regsuball(beresp.http.Link,"(?(?=<[^>]+>; rel=edgestash)<([^>]+)>; rel=edgestash|<([^>]+)>; rel=[a-z]+, )","\1"); } if(beresp.http.Surrogate-Control) { std.collect(beresp.http.Surrogate-Control); } if(beresp.http.Surrogate-Control ~ {".*="EDGESTASH/2\.[0-9]+".*"}) { edgestash.parse_response(); } } EDGESTASH