Invalidate caches and achieve high hitrate with readable and maintainable annotations

Related tags

Laravel purgatory
Overview

Purgatory

Purgatory is an extension which makes it possible for Symfony applications to handle enormous load using minimal infrastructure. Infrastructure meant to be used with this bundle along with Symfony application is a HTTP caching reverse proxy.

This bundle implements an easy and maintainable way to invalidate cache on endpoints based on changes in Doctrine entities.

Installation

Prerequisite - doctrine/orm

composer require sofascore/purgatory

Setup - Symfony reverse proxy

Enable Symfony Http Cache component in config/packages/framework.yaml

framework:
  http_cache: true

Wrap the default kernel into HttpCache caching kernel public/index.php

<?php

use App\Kernel;
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

return function (array $context) {
    return new HttpCache(new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']));
};

Define implementation of PurgerInterface and host to purge in config/packages/purgatory

purgatory:
  purger: 'sofascore.purgatory.purger.symfony'
  host: 'localhost:3000'

Usage

Suppose you have a simple entity and controller.

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity()
 * @ORM\Table(name="post1")
 */
class Post
{
    /**
     * @ORM\Id()
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue()
     */
    public $id;
    /**
     * @ORM\Column(type="string")
     */
    public $title;
    /**
     * @ORM\Column(type="string")
     */
    public $content;
}
namespace App\Controller;


use App\Entity\Post;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
 * @Route("/post")
 */
class PostController extends AbstractController
{

    public function __construct(private EntityManagerInterface $entityManager)
    {
    }

    /**
     * @Route("/{postId<\d+>}", methods={"GET"})
     */
    public function detailsAction(int $postId)
    {
        /** @var Post $post */
        $post = $this->entityManager->getRepository(Post::class)->find($postId);
        if (null === $post) {
            return new Response(status: 404);
        }

        $r = new Response(json_encode(['title' => $post->title, 'content'=>$post->content]), 200, []);
        $r->setSharedMaxAge(3600);
        $r->setMaxAge(3600);

        return $r;
    }
}

When a request is sent to an endpoint for first time, reverse proxy saves the response and serves the same response until it expires (1 hour in this example). If the state of an entity changes in the meantime, content on the website stays the same until cache expires.

Purgatory has an annotation which defines rules for cache invalidation when a state of an object changes.

use SofaScore\Purgatory\Annotation\PurgeOn;

    /**
    * @Route("/{postId<\d+>}", methods={"GET"})
    * @PurgeOn(Post::class, parameters={"postId":"id"}, properties={"title", "content"}, if="obj.title !== null")
    */ 
    public function indexAction(int $id) //...

PurgeOn annotation

Parameters:

  • Required FQCN of an entity whose changes are being tracked for cache purging.
  • parameters
    • defines an associative array where keys are route parameters and values are property names.
  • properties
    • list of properties which are required to change in order to purge the endpoint.
    • if omitted, change of any property purges the cache.
  • if
    • an expression which has to be true in order to purge the endpoint with specified parameters.

Workflow

When property of Post entity is changed and flushed to a database, Purgatory goes through PurgeOn annotations where changed property is in list of properties, checks the if expression, injects the parameters and purges the route.

Custom Purger

If you have a more complex setup or use varnish (recommended) you should implement your own purger that will be aware of your infrastructure.

Example purger:

namespace App\Service;


use GuzzleHttp\Client;
use SofaScore\Purgatory\Purger\PurgerInterface;

class VarnishPurger implements PurgerInterface
{
    private Client $client;

    public function __construct()
    {
        $this->client = new Client();
    }

    public function purge(iterable $urls): void
    {
        foreach ($urls as $url) {
            $this->client->request('PURGE', 'http://varnish_host' . $url);
        }
    }
}

You must also register that Purger with the configuration:

purgatory:
  purger: App\Service\VarnishPurger

Add purge capability to varnish

acl purge {
        "localhost";
        "172.0.0.0"/8; # if behind docker
        # add more whitelisted ips here
}

sub vcl_recv {
        if (req.method == "PURGE") {
                if (!client.ip ~ purge) {
                        return(synth(405,"Not allowed."));
                }
                return (purge);
        }
}

That's it!

Examples

Endpoint which fetches all properties of a single post.

Use PurgeOn with FQCN and map route parameters with property of an entity. On change of any property of a Post, endpoint with entity id injected as route parameter postId gets invalidated.

    /**
     * @Route("/{postId<\d+>}", methods={"GET"})
     * @PurgeOn(Post::class, parameters={"postId":"id"})
     */
    public function detailsAction(int $postId) {

Endpoint which fetches all featured Posts.

Use PurgeOn and specify a single property - cache invalidation happens every time property featured changes on any of the Posts.

    /**
     * @Route("/featured", methods={"GET"})
     * @PurgeOn(Post::class, properties={"featured"})
     */
    public function featuredAction() {

Endpoint which fetches a list of all popular posts with more than 3000 upvotes.

Use PurgeOn with a condition - cache invalidation happens every time property upvotes on a Post with more than 3000 upvotes changes.

    /**
     * @Route("/popular", methods={"GET"})
     * @PurgeOn(Post::class, if="obj.upvotes > 3000")
     */
    public function popularAction(int $postId) {

Debugging

php bin/console purgatory:debug Post

Purgatory debug command groups all defined purging rules and dumps it on the screen. Its argument is an entity name or entity and property.

php bin/console purgatory:debug Post::upvotes

Command with defined entity and property dumps all routes which get refreshed by change of that property.

\App\Entity\Post
	app_post_details
		path: /post/{postId}
		parameters:
			postId: id

\App\Entity\Post::upvotes
	app_post_popular
		path: /post/popular
		if: obj.upvotes > 3000

Observe that change of upvotes causes a cache invalidation on popular posts route as well as on post details route.

Comments
  • Debug command doesn't work well with arrays

    Debug command doesn't work well with arrays

    Describe the bug

         * @Rest\Get("/{id}/{type}")
         * @PurgeOn(Entity::class,
         *     properties={"prop1", "prop2"},
         *     parameters={"id": "id", "type": {"@value1","@value2"}}
         * )
    

    Then run bin/console purgatory:debug Entity::prop1.

    Expected: Two urls get dumped:

            route_name
                    path: {id}/{type}
                    parameters: 
                            id: id
                            type: @value1
    
            route_name
                    path: {id}/{type}
                    parameters: 
                            id: id
                            type: @value2
    

    What happens: One url get dumped:

            route_name
                    path: {id}/{type}
                    parameters: 
                            id: id
                            type: @value1
    

    Additional context Looks like \SofaScore\Purgatory\Command\DebugCommand::dumpMappingValueArrayElement has a bug where only value[0] is dumped, and not all values inside of that array.

    opened by lskupnjak 1
  • Use stock Symfony event listener for entity changes

    Use stock Symfony event listener for entity changes

    Changes

    • [x] EntityChangeListener is now implemented as a standard Symfony event listener (by leveraging the DoctrineBundle integration)
      • The listener can be optionally disabled via the entity_change_listener configuration node
    • [x] Cache directory has been renamed to purgatory
    • [x] It's now possible to configure the purger implementation via the purger configuration node
    • [x] WebCacheInterface interface has been streamlined into the PurgerInterface interface
    opened by spideyfusion 1
  • Support Laravel

    Support Laravel

    Make Purgatory compatible with Laravel Framework. Core functionalities of this bundle should be separated in a different repository and then imported imported in Laravel Bundle.

    opened by shule727 0
  • Add support for purging localized routes

    Add support for purging localized routes

    When purging a route, it would be nice to be have support for purging all it's localized versions.

    E.g.

    purging www.website.com/en/posts

    would also trigger purging

    www.website.com/it/posts www.website.com/es/posts ... etc

    opened by shule727 0
  • Resolvable placeholders in parameters that can expand

    Resolvable placeholders in parameters that can expand

    Add support for resolvable parameters.

    E.g.

    @PurgeOn(Post::class, properties={"updatedAt"}, parameters={"countryAlpha2": "@AppBundle\Service\LocaleResolver"})
    
    namespace AppBundle\Service;
    
    class LocaleResolver
    {
        public function resolve(): array
        {
            return [
                'HR',
                'IT',
                ...
            ];
        }
    }
    
    opened by shule727 0
  • Use serialization groups for triggering purge

    Use serialization groups for triggering purge

    It would be useful if serialization groups could be used to purge routes when theirs properties change.

    E.g.

    @PurgeOn(Post::class, groups={"common"})
    

    Also, it would be useful for Purgatory to be able to automatically use FOSRest and API Platform groups defined in the PHPDoc if not explicitly given as parameter to PurgeOn.

    opened by shule727 0
  • Support for optional url parameters

    Support for optional url parameters

    If route supports optional url parameters, then purgatory should generate first and second url without any query parameters.

    E.g. single route handles /post/{id}/comments and /post/{id}/comments/user/{userId} @PurgeOn(Post::class, parameters={"id":"id", "userId":"user.id"})

    Current behaviour is that the first route is generated with a query parameter userId which is not needed

    opened by zlatkoverk 0
Releases(v0.3.3)
Owner
null
Stash view is a composer package for Laravel which caches views using Russian Doll Caching methodology.

Stash View Stash view is a composer package for Laravel which caches views using Russian Doll Caching methodology. What is Russian Doll Caching ? It i

Bhushan Gaikwad 18 Nov 20, 2022
Library that offers Input Filtering based on Annotations for use with Objects. Check out 2.dev for 2.0 pre-release.

DMS Filter Component This library provides a service that can be used to filter object values based on annotations Install Use composer to add DMS\Fil

Rafael Dohms 89 Nov 28, 2022
An open-source Laravel library for building high-quality, accessible applications and administration dashboards.

Arpite An open-source Laravel library for building high-quality, accessible applications and administration dashboards. Built using Inertia.js, React,

Arpite 3 Jul 5, 2022
University, College, and High School name generator using fakerphp/faker

FakerSchools University, College, and High School name generator using fakerphp/faker Installation Add the FakerSchools library to your composer.json

Kenny Johnson 2 Oct 20, 2022
A High-Level Overview of Laravel Octane

This is the source code behind the Laracasts Larabit: A High-Level Overview of Laravel Octane, and features all of the files and code available in that video.

Andrew Schmelyun 1 Feb 10, 2022
A Laravel 8 and Livewire 2 demo showing how to search and filter by tags, showing article and video counts for each tag (Polymorphic relationship)

Advanced search and filter with Laravel and Livewire A demo app using Laravel 8 and Livewire 2 showing how to implement a list of articles and tags, v

Sérgio Jardim 19 Aug 29, 2022
Laravel package to generate and to validate a UUID according to the RFC 4122 standard. Only support for version 1, 3, 4 and 5 UUID are built-in.

Laravel Uuid Laravel package to generate and to validate a universally unique identifier (UUID) according to the RFC 4122 standard. Support for versio

Christoph Kempen 1.7k Dec 28, 2022
Boilerplate code for protecting a form with proof of work. Uses javascript in the browser to generate the hashcash and PHP on the server to generate the puzzle and validate the proof of work.

Boilerplate code for protecting a form with proof of work. Uses javascript in the browser to generate the hashcash and PHP on the server to generate the puzzle and validate the proof of work.

Jameson Lopp 28 Dec 19, 2022
List of 77 languages for Laravel Framework 4, 5, 6, 7 and 8, Laravel Jetstream , Laravel Fortify, Laravel Breeze, Laravel Cashier, Laravel Nova and Laravel Spark.

Laravel Lang In this repository, you can find the lang files for the Laravel Framework 4/5/6/7/8, Laravel Jetstream , Laravel Fortify, Laravel Cashier

Laravel Lang 6.9k Jan 2, 2023
Stop duplicating your Eloquent query scopes and constraints in PHP. This package lets you re-use your query scopes and constraints by adding them as a subquery.

Laravel Eloquent Scope as Select Stop duplicating your Eloquent query scopes and constraints in PHP. This package lets you re-use your query scopes an

Protone Media 75 Dec 7, 2022
symfony workflow component for laravel7 and 8 ,php 7 and 8

Laravel workflow Use the Symfony Workflow component in Laravel8,PHP7,PHP8 This repository based on @brexis,his project since 2019-09 No longer maintai

null 3 Jul 21, 2022
Takes in a HEX color and produces variations of that colour for the foreground and background

css-colors Takes in a HEX color and produces variations of that colour for the foreground and background It takes a great php class made by Patrick Fi

Soapbox Innovations Inc. 9 Jul 24, 2020
An opinionated support package for Laravel, that provides flexible and reusable helper methods and traits for commonly used functionality.

Support An opinionated support package for Laravel, that provides flexible and reusable helper methods and traits for commonly used functionality. Ins

Ian Olson 3 Apr 14, 2021
Laravel blade directives and php helpers for serverside rendered content, based on browser window size WITHOUT css. Requires Livewire and AlpineJS.

Laravel Livewire Window Size and Breakpoints Laravel blade directives and php helpers for server side rendered content, based on browser window size W

Tina Hammar 15 Oct 6, 2022
Sweetalert and Toaster notifications for Laravel livewire with support for tailwind and bootstrap.

Larabell Integrate livewire with sweetalert. Installation How to use Sweetalert Toast Available configuration Installation composer require simtabi/la

Simtabi 3 Jul 27, 2022
Invoices, Expenses and Tasks built with Laravel and Flutter

Invoice Ninja Hosted | Self-Hosted We're on Slack, join us at slack.invoiceninja.com or if you like StackOverflow Just make sure to add the invoice-ni

Invoice Ninja 6.8k Dec 26, 2022
Fast and simple implementation of a REST API based on the Laravel Framework, Repository Pattern, Eloquent Resources, Translatability, and Swagger.

Laravel Headless What about? This allows a fast and simple implementation of a REST API based on the Laravel Framework, Repository Pattern, Eloquent R

Julien SCHMITT 6 Dec 30, 2022
Collection of agnostic PHP Functions and helpers with zero dependencies to use as foundation in packages and other project

Collection of agnostic PHP Functions and helpers This package provides a lot of very usefull agnostic helpers to use as foundation in packages and oth

padosoft 54 Sep 21, 2022
Laravel Podcast is Laravel 5.5 web app that enables you to manage RSS feeds for your favorite podcasts and listen to the episodes in a seamless UI and User Authentication.

Laravel Podcast is Laravel 5.5 web app that enables you to manage RSS feeds for your favorite podcasts and listen to the episodes in a seamless UI and

Jeremy Kenedy 35 Dec 19, 2022