Nginx Fastcgi Reverse Proxy Cache for PHP (Symfony)

Nginx Fastcgi Reverse Proxy Cache for PHP (Symfony)

Nginx Fastcgi Reverse Proxy Cache for PHP (Symfony) - If you ever faced the case that your PHP app is not performing for the incoming traffic, Nginx can help you.

Originally published by Stefan Pöltl at medium.com

Actually there is a easy caching fix for PHP backends to reduce the load and to serve responses faster that are requested multiple times with the same parameters.

Current application state

In my case I had to fix a legacy API route that generated a JSON based response which took around 400ms. The app is using Memcached to cache serialized objects, but still the response time for an API should be much faster. The code itself was kind of legacy and a fast shot to speed up the application was requested.

Solution

The application lives behind a load balancer and runs on mulitple nodes with the following components:

  • PHP-FPM as PHP Process Manager
  • NGINX as webserver

The simplest solution that came to my mind was to cache the API responses, triggered by a POST request, even if the RFC for HTTP says its not recommended:

Responses to this method are not cacheable, unless the response includes appropriate Cache-Control or Expires header fields. However,the 303 (See Other) response can be used to direct the user agent to retrieve a cacheable resource.
Implementation

The best thing is, you don’t need to touch the application at all, you can just change your nginx configuration file and that’s it. I will show a configuration file you can easily mount into a NGINX Docker container and run it in front of a PHP-FPM container.

docker-compose.yml:
version: '2'
services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    volumes:
      - ./:/opt/code
      - ./vhost:/etc/nginx/conf.d/default.conf
    depends_on:
      - fpm
  fpm:
    image: php:7.3-fpm
    volumes:
      - ./:/opt/code
Nginx vhost config:
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=fpmcache:100m max_size=3g inactive=60m use_temp_path=off;
fastcgi_cache_key "$request_uri|$request_body";
fastcgi_cache_methods POST;

server {
server_tokens off;
listen 80 default_server;
server_name server_name _;

access_log   /dev/stdout;
error_log    /dev/stderr;

root /opt/code;

location / {
    try_files $uri /index.php$is_args$args;
}

set $disable_cache 1;
if ($request_uri ~ "^/api") {set $disable_cache 0;}

location ~ [^/]\.php(/|$) {
    fastcgi_cache_bypass $disable_cache;
    fastcgi_no_cache $disable_cache;

    fastcgi_cache fpmcache;
    fastcgi_cache_valid 200 20m;
    fastcgi_cache_min_uses 1;
    fastcgi_cache_lock on;
    add_header X-Cache $upstream_cache_status;

    fastcgi_pass fpm:9000;
    fastcgi_split_path_info ^(.+\.php)(/.*)$;
    fastcgi_read_timeout 35;
    include fastcgi_params;
    fastcgi_param DOCUMENT_ROOT $realpath_root;
    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
}

}

index.php:

<?php

echo date('Y-m-d H:i:s');

If you spin up the containers with docker-compose up and run the following curl statement multiple times, you should see the same response output for 20 minutes, as configured in the NGINX vhost config.

curl -X POST localhost:8080/api

Cached response output for curl request

As you can see for the /api POST request, we always get the same time exposed from the cache after the first PHP script call was saved on disk in the NGINX container. For all other calls you get the current time forwarded from the backend without caching:

Non cached response output

How did we configure NGINX?
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

This line is necessary, because Frameworks like Symfony set the Cache-Control: no-cache header by default and the configured caching will be ignored.

fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=fpmcache:100m max_size=3g inactive=60m use_temp_path=off;

Here the arguments configure the path to store the cached backend results and set the directory structure/depth. This means the cache folder structure looks like this: /var/cache/nginx/5/dc for the key: 4275fe6d0b92cebe0f8d1461c5fe2dc5

So the directory structure is build like: Last char from the key/next two chars/key

The reason for this is, that nginx has less cache files per folder instead of storing all files in one directory, to speed up the lookup by key.

The file content of the key named file is:

KEY: /api|
VX-Powered-By: PHP/7.3.6
Content-type: text/html; charset=UTF-8

2019–06–13 20:46:28

In general NGINX stores the keys in memory(RAM) with 100MB in our case and keeps a 3GB storage on disk for the cache files. If a key was not fetched for 60 minutes the file gets removed. Further we disable the first write of the generated cached result into a temp folder, for performance reasons.

Next step is to decide which values are used to compute the cache key and which HTTP methods are valid to enable the caching:

fastcgi_cache_key "$request_uri|$request_body";
fastcgi_cache_methods POST;

In the next step, we set a variable that enables the caching only for the /api route and use it in the php location block:

set $disable_cache 1;
if ($request_uri ~ "^/api") {set $disable_cache 0;}
location ~ [^/].php(/|$) {
fastcgi_cache_bypass $disable_cache;
fastcgi_no_cache $disable_cache;
}

Finally we configure which defined cache is used and which HTTP return codes get cached for how long(fastcgi_cache_valid). The fastcgi_min_uses setting caches the result from the backend after the first request was processed. The fastcgi_cache_lock option just lets the first request hitting the backend server, all the others are going to wait until the result is in the cache and fetch it from there. This prevents load on the backend. To debug the current caching status we add the X-Cache header, which says MISS, HIT or EXPIRED.

fastcgi_cache fpmcache;
fastcgi_cache_valid 200 20m;
fastcgi_cache_min_uses 1;
fastcgi_cache_lock on;
add_header X-Cache $upstream_cache_status;

At the end don’t forget to test your hosts configuration with nginx -t and serve your users/clients with a fast API.

Originally published by Stefan Pöltl at medium.com

====================================================================

Thanks for reading :heart: If you liked this post, share it with all of your programming buddies! Follow me on Facebook | Twitter

Learn More

☞ PHP for Beginners - Become a PHP Master - CMS Project

☞ Learn Object Oriented PHP By Building a Complete Website

☞ MEVP Stack Vue JS 2 Course: MySQL + Express.js + Vue.js +PHP

☞ Object Oriented PHP & MVC

☞ PHP OOP: Object Oriented Programming for beginners + Project

☞ Learn PHP Fundamentals From Scratch

☞ The Complete PHP MySQL Professional Course with 5 Projects

☞ The Complete JavaScript Course 2019: Build Real Projects!

Best PHP Frameworks (2019)

In this article, we will be discussing the best PHP Frameworks along with their pros and cons in great depth so that one might get a clear understanding of which one to chose. Our comprehensive research would include some Latest PHP Frameworks.







Basic Introduction of All Frameworks:
In this article, we would be discussing Best PHP Frameworks and before that, we would be having a glimpse of some basic information of all of these PHP Frameworks. One thing common about all of the PHP Frameworks we would be discussing here is that all are Open Source PHP Frameworks. Have a look at some basic facts about these PHP Frameworks:


FrameworkLaravelSymfonyZendCodeIgniterCakePHP
Date of First Commit9-Jun-20114-Jan-201028-Apr-200925-Aug-200616-May-2005
Official Websitehttps://laravel.com/https://symfony.com/https://framework.zend.com/https://codeigniter.com/https://cakephp.org/
Github URLhttps://github.com/laravel/laravelhttps://github.com/symfony/symfonyhttps://github.com/zendframework/zendframeworkhttps://github.com/bcit-ci/CodeIgniterhttps://github.com/cakephp/cakephp
Current Version5.84.233.1.103.7
LicenseMIT licenseMIT licenseNew BSD licenseMIT licenseMIT license

Google Trends [2019]:

As can be seen clearly in the above Google Trends, Laravel is surely racing ahead in the list of best PHP Frameworks in 2019 while Codeigniter is also giving some tough competition here. Also, there is an interesting aspect that Symfony and CakePHP are gaining some traction in the list of top PHP Frameworks.

Suggested Read: Redis vs MySQL Benchmarks


Stack Overflow Trends [2019]:
Stack Overflow: https://tinyurl.com/y23cqb8b

Again, as per the Stack Overflow trends, Laravel is way ahead in the list of Top PHP Frameworks in 2019. Also, Laravel started gaining some serious traction from the 2014-2015 period and since then it is a leading PHP Framework. One interesting point one can note here is that since 2013, Laravel started gaining popularity and other Frameworks saw either downfall or negligible growth.



Github Stars Since First Commit [2019]:
In this world of OpenSource technologies, Github stars since the first commit is the sure shot way of checking the popularity streak of a particular technology and PHP Frameworks are of no exception. So, when discussing the Top PHP Frameworks, one can not ignore the importance of Github Stars. So, here is the list of PHP Frameworks along with their individual data in regard to their Github Stars. 

FrameworkDate of First CommitTotal No of Github Stars
(At the time of writing)
LaravelThursday, 9 June 201152166
SymfonyMonday, 4 January 201020683
ZendTuesday, 28 April 20095698
CodeIgniterFriday, 25 August 200617251
CakePHPMonday, 16 May 20057845

As can be clearly seen from the above table, Laravel Framework, in terms of Github Stars is way ahead in the list of best PHP Frameworks in 2019 while Symfony and CodeIgniter are having the second and third position. One can see here is that Laravel is the youngest of all the above-mentioned frameworks and still has managed to get far more Github stars.

Comparison based on other Parameters:
FrameworkLaravelSymfonyZendCodeIgniterCakePHP
Twitter Followers98.6K34.3K47.8K23K17.4K
Downloads (From Packagist)62.1M+41M+4.9M+0.47M+4.0M+
PHP Version Required5.6.45.6.05.65.3.75.6
Speed(Rest API)5.5 ms2.2 ms (Fastest)3.9 msUnknown5.8 ms (Slowest)

These are some other helpful parameters and benchmarks which completes our comparison of Top 10 Best PHP Frameworks for Web Developers in 2019.

Disclaimer: All the above parameters/comparisons/benchmarks are true at the time of writing this blog post, exact numbers may vary depending on the instant and point in time you are reading this blog/post.

Recommended Articles:
If your learning appetite is still left, you are suggested to read below articles:


How to use a custom collection in Symfony 4

I want to use a custom collection class for my Symfony 4 application. The following scenario is an example of what I am trying to achieve:

I want to use a custom collection class for my Symfony 4 application. The following scenario is an example of what I am trying to achieve:

I have a post collection class which has some utility for filtering and mapping data.

class PostArrayCollection extends ArrayCollection implements PostCollectionInterface
{
    public function mapTitles()
    {
        return $this->map(function (Post $post) {
            return $post->getTitle();
        });
    }
public function filterPublishedAfterDate(DateTime $from)
{
    return $this-&gt;filter(function (Post $post) use ($from) {
        return $post-&gt;publishedDate() &gt; $from;
    });
}

}

Also the user class which is a Doctrine Entity.

class User
{
/**
* @ORM\OneToMany(targetEntity="App\Entity\Post", mappedBy="post", cascade={"persist"})
*/
private $posts;

public function __construct()
{
    $this-&gt;posts = new PostArrayCollection();
}

public function getPosts(): PostCollectionInterface
{
    return $this-&gt;posts;
}

}

Then there is a method in a helper or controller which will access the user's data like the following:

public function showPublishedPostTitlesForUser(User $user)
{
$publishedPostTitles = $user->getPosts()
->filterPublishedAfterDate(new DateTime())
->mapTitles();

// Render the titles...

}

The above works when creating a new object by hand, for example in a unit test. But it will not work when loading the entity from the repository, because then the collections will be filled with Doctrine\ORM\PersistentCollection objects.

My question now is, how do I configure my app so I can use a custom persistent collection (for example PersistentPostCollection) when loading entities?

I did find this page https://www.doctrine-project.org/projects/doctrine-collections/en/latest/lazy-collections.html#lazy-collections, but I cannot find how to integrate this into Symfony 4.

Note: The above scenario is a simple example for the sake of keeping this question short and simple to get into. I am aware that this whole problem can be avoided when using a repository to get the correct data. But that is not what I am looking for here. This question is about understanding what is possible with Doctrine collections in Symfony 4.