PHP
Article

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.

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!

  • tmporary

    Hi Bruno, thanks for the article!
    Have you got a solution for not being able to use the PHPStorm Symfony plugin, after “hiding” the cache-dir inside the VM? I can get past not being able to see the logs, but I would definitely miss the Symfony plugin…
    Since it uses the generated Container.xml file, the translations and the UrlGenerator from the cache dir, nearly all of the advantages of the plugin will be unusable.

    • Bruno Škvorc

      Interesting question!

      For logs, I generally log outside the app’s folder anyway. One solution for the plugin would, I suppose, be to rsync the required files into a mock cache directory inside the folder, but that might be too much work depending on who you ask.

      As I don’t use the plugin, I never ran into this so I never gave it much thought. I’ll think about it, but in the meanwhile, if someone else has something to suggest on that front, go ahead.

  • http://www.skooppa.com s.molinari

    I simply avoid the using shared folders at all with Vagrant, especially since my host computer runs Windows. I use the same SFTP syncing setup I’d use for remote dev or production servers. It might be a bit less convenient, but it is stable and doesn’t bog down Vagrant at all. So, I don’t need to tweak any apps to work properly in a Vagrant environment. That, to me, is actually more inconvenient than using PHPStorm’s automatic deployment features.

    • Bruno Škvorc

      Interesting. How does that work then, is it one way only? What you change in the IDE gets uploaded to the VM, but not the other way around? What about Composer commands then? Do you run them outside the VM, or inside? I’d be curious to learn more about your setup.

      • http://www.skooppa.com s.molinari

        I always see the server as the master. So if files on the server changed, all I do is resync or just download everything again in PHPStorm. For the vendor directory, if I need it, I download it via WinSCP once into an external directory and enter the directory as a library in the PHPStorm project. If other files need downloading within the project on the server, I do a sync within PHPStorm. I also use WinSCP to quickly dig into non-project files, like looking within /etc for server settings, etc. (hehe, is that a pun?) I am a point and click kind of guy. I don’t like using the shell and avoid it, when I can. In all, setting this up doesn’t take more than a couple of minutes and keeping things in sync isn’t ever really a big issue. Again, the important thing to remember is the server is always the master….

        Sort of off topic, but when I see the console commands to get stuff done in Sulu as you’ve explained, my first thought is, why can’t those processes be part of the UI? That kind of work means only a programmer can get Sulu running and work with it and that reduces the available market of possible users phenomenally.

        • Bruno Škvorc

          Sulu is actually marketed as a CMS for devs, so yeah, you can’t really set it up without being one – there’s a lot of XML stuff etc to configure. It’s definitely not simple, and has some UX kinks to iron out.

          • Daniel Rotter

            There are also other points here: E.g. due to the fact that we rely on Symfony the web directory needs to be the directory root of the webserver. So setting it up always requires some technical knowledge. I don’t see any easy way to offer a zip package, which can simply be uploaded to the webserver, being unzipped, and it runs. And that’s also not the primary focus of the project.

  • https://www.peternijssen.nl/ Peter Nijssen

    If you don’t want to move the vendor directory, you could also use vagrants rsync functionality. However that is one way and you have to cope with that.

    If you really want to be bidirectional, you could have a look at unison; https://github.com/dcosson/vagrant-unison2
    However, that will cost you some extra CPU usage. It’s heavier in usage.

    • Bruno Škvorc

      Excellent tips, thanks!

  • Rommsen

    Question: How do you differentiate between different vendor directories when going from development to production when moving vendor files? In production I want my vendors to be at their original location because this allows directory/symlink based deployments (one deployment, one directory). Is there a way to differentiate between environments in app/autoload.php and composer.json?

    • Bruno Škvorc

      In app/autoload.php sure, but in composer.json a little harder. What do you mean by “directory/symlink based deployments (one deployment, one directory)” though, could you clarify?

      For my own deployment, I include the replacements back to the “original” in a deploy script. It’s a bit iffy, but admittedly I’ve only deployed such an app once, so I’m sure I’ll come up with some more practical solutions in a couple more attempts.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.