PHP
Article
By Bruno Skvorc

Can Symfony Apps Be Fast on Vagrant? Let’s Check with SuluCMS!

By Bruno Skvorc

In this short tutorial, we’ll set up Sulu, a new Symfony based CMS, and optimize it on a Vagrant environment. Why a dedicated tutorial handling this? Besides the fact that Sulu has a rather complex initialization procedure, it is based on Symfony which is infamously slow on virtual machines with shared filesystems, and thus needs additional optimizations post-install. The performance hacks in this post, while Sulu-specific, can be applied to any Symfony application to make it faster on Vagrant.


Would you like to learn more about Symfony and/or SuluCMS? Come join us at WebSummerCamp – the only conference filled to the brim with long, hands-on workshops. The program is out and it’s awesome! Super early bird tickets available until the end of June!


sulu logo

As usual, we’ll be using our Homestead Improved box as the base, but the steps below are explained in enough detail for you to follow them on any environment.

New Box and Folder Sharing

We start by downloading a fresh box and setting up folder sharing.

git clone https://github.com/swader/homestead_improved hi_sulu
cd hi_sulu; bin/folderfix.sh

Once this is done, it’s recommended to set the file sharing type to nfs, due to this known issue, also described here.

App Type and Vagrant Boot

Homestead Improved comes with a symfony-sulu application type, which configures Nginx for Sulu specifically. We add a new site into Homestead.yaml:

    - map: test.app
      to: /home/vagrant/Code/sulu/web
      type: symfony-sulu

The generated Nginx is below. If you’re using something other than Homestead Improved, you’re encouraged to copy it from here:

server {
    listen 80;
    listen 443 ssl;
    server_name test.app;
    root "/home/vagrant/Code/sulu/web";

    charset utf-8;

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    access_log off;
    error_log  /var/log/nginx/test.app-ssl-error.log error;

    sendfile off;

    client_max_body_size 100m;

    # PROD
    location ~ ^/(website|admin|app)\.php(/|$) {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors off;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;
        # Prevents URIs that include the front controller. This will 404:
        # http://domain.tld/app.php/some-path
        # Remove the internal directive to allow URIs like this
        internal;
    }

    # strip app.php/ prefix if it is present
    rewrite ^/app\.php/?(.*)$ /test.app permanent;

    location /admin {
        index admin.php;
        try_files $uri @rewriteadmin;
    }

    location @rewriteadmin {
        rewrite ^(.*)$ /admin.php/test.app last;
    }

    location / {
      index website.php;
      try_files $uri @rewritewebsite;
    }

    # expire
    location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
        try_files $uri /website.php/test.app;
        access_log off;
        expires 30d;
        add_header Pragma public;
        add_header Cache-Control public;
    }

    location @rewritewebsite {
        rewrite ^(.*)$ /website.php/test.app last;
    }


    location ~ /\.ht {
        deny all;
    }

    ssl_certificate     /etc/nginx/ssl/test.app.crt;
    ssl_certificate_key /etc/nginx/ssl/test.app.key;
}

Remove the SSL lines at the bottom if you’re not using HTTPS.


Then, let’s boot up the VM and SSH into it.

vagrant up; vagrant ssh

Add test.app (or another URL if you chose it instead) to your host OS’ etc/hosts file. If this sentence makes no sense, please read the Homestead Improved Quick Start.

Installing Sulu

All steps below happen inside the VM.

cd Code
git clone https://github.com/sulu-io/sulu-standard sulu; cd sulu
git checkout master
composer install

This will take a while, and might ask for a Github authentication token.

Note that depending on the version of PHP you have installed, this command may fail. For example, zend-code at the time of this writing requires PHP of any version that isn’t 7.0.5., and incidentally, that’s the exact version installed on Homestead Improved 0.4.4. This is easily fixed by running sudo apt-get upgrade php7.0-fpm.

Post install scripts will ask us for some parameters – the only ones we need to fill out are the database name (homestead, unless you make one specifically for Sulu) and credentials (homestead / secret). Other can be left at their default values for now, and configured later if needed.

Configuring Sulu

cp app/Resources/webspaces/sulu.io.xml.dist app/Resources/webspaces/sulu.io.xml
cp app/Resources/pages/default.xml.dist app/Resources/pages/default.xml
cp app/Resources/pages/overview.xml.dist app/Resources/pages/overview.xml
cp app/Resources/snippets/default.xml.dist app/Resources/snippets/default.xml
rm -rf app/cache/*
rm -rf app/logs/*

The above installs a default webspace.

In the file app/Resources/webspaces/sulu.io.xml we replace the name and key with the values we prefer.

Once done, we run:

app/console sulu:build dev

The above command will generate a user admin with the password admin and do some other development-friendly initializations. It’s recommended to run this in development environments. Note that this command should never be run in production. Instead, in production, you run app/console sulu:build prod.

As an aside: at this point, we have an admin user with the credentials admin/admin working. To create new roles in the future, we execute:

app/console sulu:security:role:create

and to create more users, we execute:

app/console sulu:security:user:create

The back end at test.app/admin should work now, albeit very slowly.

Admin login screen

To make the front end work, we use:

app/console assetic:dump

This will generate the required JS and CSS files and have the default theme include them.

As a final preparatory step, let’s turn on dev mode by changing the line:

defined('SYMFONY_ENV') || define('SYMFONY_ENV', getenv('SYMFONY_ENV') ?: 'prod');

to

defined('SYMFONY_ENV') || define('SYMFONY_ENV', getenv('SYMFONY_ENV') ?: 'dev');

in both web/admin.php and web/website.php. These will provide us with the famous Symfony debugbar at the bottom of the screen.

--ADVERTISEMENT--

Speed Improvement Hacks

Symfony apps are notoriously slow on Vagrant. The following steps will reduce page load times from 25 seconds per route (this includes ajax calls), to around 400 ms per route, regardless of host OS platform or file sharing type. Other guides will often focus on NFS or other issues known to work only on some operating systems, but this procedure is guaranteed to work wonders on any setup.

Log and Cache

In app/AbstractKernel.php, replace the getLogDir and getCacheDir methods with these:

    /**
     * {@inheritDoc}
     */
    public function getCacheDir()
    {
        if (in_array($this->environment, array('dev', 'test'))) {
            return '/dev/shm/appname/cache/' . $this->getContext() . '/' .  $this->environment;
        }
        return $this->rootDir . '/cache/' . $this->getContext() . '/' . $this->environment;
    }

    /**
     * {@inheritDoc}
     */
    public function getLogDir()
    {
        if (in_array($this->environment, array('dev', 'test'))) {
            return '/dev/shm/appname/logs' . $this->getContext() . '/' . $this->environment;
        }
        return $this->rootDir . '/logs/' . $this->getContext() . '/' . $this->environment;
    }

This forces Sulu to write logs and cache into a folder not shared with the host OS, thus avoiding rapid-writing of unimportant data to a shared hard drive.

Moving Vendor

Symfony apps are notorious for installing hundreds of packages – even the default framework overflows with them, let alone apps built on it. When so many classes have to be looked up on the hard drive, and the hard drive is shared from the VM to the host OS as it is with Vagrant, load times are understandably slow.

Seeing as there’s little to no chance we’ll ever be editing something inside the vendor folder, we can move it out of the synchronized folders and into a location inside the VM which is local to the machine’s “disk” only. That means we lose syncability and have to run composer commands exclusively from within the VM (or proxied from the host OS, but this is outside the scope of this post), but all for the sake of epic speed gains.

To automatically do this, inside the sulu folder execute:

~/Code/bin/sulu/vendorfix.sh

(alternatively, see below for full procedure of what you need to do)

This will change some paths in composer.json, in app/autoload.php, and in app/config/sulu.yml, delete the current vendor folder, and run Composer install. Inspect the shell script to see what it does, if you’re curious, or see the next section.

To make sure you keep autocompletion in your IDE, make sure you add home/vagrant/vendors/sulu-test.app/ to your IDE’s include path. You’ll probably have to copy the vendor folder into the host OS somewhere – a command like this should help:

cp -R ~/vendors/sulu-test.app ~/Code/

Then, just do the following in an IDE, e.g. PhpStorm:

Include path in PhpStorm

Moving Vendor: The Long Way Around

As the most important part of the optimization (i.e. the one yielding the most results), the vendor-moving approach deserves more clarification, in order to be easy to replicate in non-HI environments which don’t have the vendorfix.sh script. What follows is the step by step procedure (skip it if you used the step above and it worked fine):

  1. Pick a VM-only location for your vendor folder. It’s a good idea to put them all in one folder, and then each vendor location into a subfolder per project. For example: /home/vagrant/vendors/sulu-myapp. Let’s assume the project will be called myapp. Create that folder:

    mkdir -p /home/vagrant/vendors/sulu-myapp
    
  2. Change the configuration in composer.json to reflect this new vendor location by modifying or adding the config value:

        "config": {
        "vendor-dir": "/home/vagrant/vendors/sulu-sulu.io/",
        "bin-dir": "vendor/bin"
    },
    
  3. Open app/autoload.php and replace the line declaring $loader with the path leading to our new, custom vendor location:

    // $loader = require __DIR__ . '/../vendor/autoload.php';
    $loader = require "/home/vagrant/vendors/sulu-myapp/autoload.php";
    
  4. Open app/config/sulu.yml and replace all instances of %kernel.root_dir%/../vendor/ with the full path to our new vendor folder. This should happen in two locations: the Doctrine configuration near the top, and the sulu_core.content.structure.sulu entry in the middle of the file.

  5. Remove the old folder with rm -rf vendor from inside the app’s folder (myapp).

  6. Run composer install and composer update to make sure everything went well.

  7. Clear cache with app/console cache:clear.

That’s it. The custom vendor location should now be configured. Please post below if you run into any difficulties.

Note that while this process is Sulu-specific, it applies to any Symfony app (or any app whatsoever for that matter). What it comes down to, is defining a custom vendor folder in composer.json, and replacing all references to the old vendor folder location with paths to the new one.

APC Autoload

Allow APC caching for the autoloader by uncommenting the appropriate lines in both web/website.php and web/admin.php. We need APC installed to use this, but Homestead Improved is already equipped for that.

Miscellaneous

  • we can update the value of realpath_cache_size in /etc/php/7.0/fpm/php.ini to a bigger value like 4096k, and realpath_cache_ttl to something like 7200.

  • we can get some marginal gains by installing the C extension for Twig.

  • we can disable XDebug with:

    sudo phpdismod xdebug; sudo service php7.0-fpm restart
    

Troubleshooting

Optimize Symfony Debugging

Consisting of an abnormal number of classes and files, Symfony apps usually compile their core files into a single app/bootstrap.php.cache file. This makes it almost impossible to debug Symfony apps properly without further alterations. To allow for easier debugging, refer to these docs.

__php_incomplete_class has no unserializer

This is usually due to stale cache. First, optimize environment for debugging (see above). Then:

app/console cache:clear
rm -rf app/cache/*
composer update

Additionally, if you’re using APC, bust the cache (see below).

Changes have no effect / classes being looked for in wrong location

This usually happens when the APC cache needs to be busted. One way to do this is to put:

apc_clear_cache();

at the top of website.php or admin.php, whichever you’re running, and then remove it after the first request. Or, just make a separate file with just this command, so you can call it whenever you want.

Conclusion

Both http://test.app/admin and http://test.app (if that’s the vhost domain you used) should now work and be lightning fast. You should be ready to start developing your CMS-powered application on Sulu.

As we mentioned above, these hacks are admittedly Sulu-specific, but can be applied to any Symfony application. The most important ones being the vendor fix and the log/cache fix, they will significantly improve the app’s load times during development. The other hacks are production-friendly, too, and can be used to speed up the application when deployed.

If you have any other tips and tricks you’d like to share for improving performance of Symfony apps, Vagrant-powered development environments, or both – Symfony on Vagrant – please let us know in the comments section below and we’ll do our best to update the post.


Would you like to learn more about Symfony and/or SuluCMS? Come join us at WebSummerCamp – the only conference filled to the brim with long, hands-on workshops. The program is out and it’s awesome! Super early bird tickets available until the end of June!

Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the most important and interesting stories in tech. Straight to your inbox, daily.