Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Upgrading to the Symfony 3.0 Directory Structure

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $5.00

Hey, we're on Symfony 2.8! Before we fully upgrade, let's move into the new 3.0 directory structure... which you can actually use with any version of Symfony... But in 3.0, it's super official. And remember: the new structure isn't required. Want to keep things how they are now? Do it! We talk more about the new structure in our What's new in Symfony 3 screencast.

People often ask me:

What are the first 10 digits of pi?

Great question: they're 3.1415926535. They also ask me:

What do I need to change in my code after upgrading?

The answer to that is... nothing! Ya know, because Symfony doesn't break backwards compatibility.

But we do sometimes tweak things in the default directory structure of Symfony. A sure-fire way to find out about those changes is to go to the Symfony Standard repository and just compare the versions. If you compare 2.8 to master, you'll see all the changes needed to move into this new directory structure.

Ready to do this?

Autoload AppKernel

In each front controller we require the AppKernel file:

39 lines web/app_dev.php
... lines 1 - 30
require_once __DIR__.'/../app/AppKernel.php';
... lines 32 - 39

That's because this file doesn't have a namespace and doesn't live in src. Basically, it's weird, and can't be autoloaded.

That was lame, so it is autoloaded now! In composer.json, add a files key with app/AppKernel.php inside of it:

63 lines composer.json
... lines 1 - 5
"autoload": {
"psr-4": { "": "src/" },
"files": ["app/AppKernel.php"]
},
... lines 10 - 63

With that, we can rip out the require statement in app.php, app_dev.php, app/console and in AppCache.php. Nice!

For this to take effect, head over to the terminal and dump the autoloader:

composer dump-autoload

Running composer install also does this.

Refresh! And it still works!

The var/ Directory

The most noticeable change is the new var/ directory which holds stuff you don't need to modify, like the cache and logs. To update your project, open AppKernel and override a few methods. I'll use command+n to open the generate menu. Select override and choose getRootDir(), getCacheDir(), and getLogDir(). I'm not sure why we override getRootDir(), because that defaults to this directory, but I do like that it looks less magic now:

... lines 1 - 5
class AppKernel extends Kernel
{
... lines 8 - 47
public function getRootDir()
{
return __DIR__;
}
... lines 52 - 61
}

For getCacheDir(), return dirName(__DIR__) - that looks in app/ then goes up to the root - and then add .'/var/cache/'.$this->environment;:

... lines 1 - 52
public function getCacheDir()
{
return dirname(__DIR__).'/var/cache/'.$this->environment;
}
... lines 57 - 63

Do a similar thing in getLogDir() except change to .'/var/logs/';:

... lines 1 - 57
public function getLogDir()
{
return dirname(__DIR__).'/var/logs';
}
... lines 62 - 63

That's all you need to do to move the cache & logs directories.

In the terminal create a var/ directory, and then git mv app/cache/ into var/. Do the same to move app/logs/ into var/:

mkdir var
git mv app/cache var/
git mv app/logs var/

This might seem weird to you because the cache and logs directories aren't normally stored in git. But we do keep a .gitkeep file inside each to make sure they don't get deleted. That's what we just moved.

Back to the browser and refresh! It's still alive! In our IDE, we have a new var/ directory that's happily populated with cache and logs files.

Changes to bin/

Next, look at the bin directory. It exists because composer.json holds a little bit of configuration: "config:" "bin-dir:":

63 lines composer.json
{
... lines 2 - 51
"config": {
"bin-dir": "bin"
},
... lines 55 - 61
}

Sometimes when you install an external library via composer, it comes with a binary file. Composer usually puts that into a vendor/bin directory for convenience. With this config, it goes into bin/ instead?

Why did we override this? I have no idea! Composer was young, it was the wild-west, things happen. And really, it's not a big deal, except that it makes Symfony projects look a bit "weird" compared to others. Remove this config so that Composer uses the normal vendor/bin.

In the terminal, completely remove the bin/ directory. But wait! The bin directory does still exist in the Symfony 3 directory structure, it just has a new job. Put it back!

rm -rf bin
mkdir bin

bin/console

Phew! Now that it's back, move the app/console file into bin/:

mv app/console bin/console

This is one of the biggest changes. Goodbye app/console, hello bin/console. Of course this file is angry because the bootstrap.php.cache file is not in the same directory anymore. But instead of loading it, load /../app/autoload.php instead:

27 lines bin/console
... lines 1 - 9
require __DIR__.'/../app/autoload.php';
... lines 11 - 27

Why? Well, in part because bootstrap.php.cache is going to move too.

Head to the terminal and try out your very first bin/console:

bin/console

It's still happy, and I'm still happy.

Moving bootstrap.php.cache

Speaking of bootstrap.php.cache, this now lives in the var directory. But wait, we aren't responsible for this file. In fact, who created this in the first place? Who put this in the app directory to begin with? It was Santa! Just kidding: it's the SensioDistributionBundle, via one of these post install hooks in composer.json:

60 lines composer.json
... lines 1 - 33
"scripts": {
"post-install-cmd": [
... line 36
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
... lines 38 - 41
],
... lines 43 - 50
},
... lines 52 - 60

We need to tell the SensioDistributionBundle that we want it to be built in var instead of app. To do that, add a couple of new configuration lines in the extra section of composer.json.

Add "symfony-var-dir": set to "var", "symfony-bin-dir": set to "bin" and "symfony-tests-dir": set to "tests". The really important one here symfony-var-dir:

63 lines composer.json
... lines 1 - 51
"extra": {
"symfony-app-dir": "app",
"symfony-web-dir": "web",
"symfony-var-dir": "var",
"symfony-bin-dir": "bin",
"symfony-tests-dir": "tests",
... lines 58 - 60
}
... lines 62 - 63

When you run composer install it's going to see this and realize that you want it to put the bootstrap.php.cache file into the var directory. It's as simple as that.

The symfony-bin-dir is also important because of the check.php and SymfonyRequirements.php files. These also moved, and the SensioDistributionBundle is also in charge of putting those inside of either the app, bin or var directories. In a second, you'll see these move.

The symfony-test-dir, well, as far as I can see, that doesn't do anything. But it exists in the official Symfony Standard Edition and darn it, I'm a rule-follower.

Back to our favorite place, the terminal! Run:

composer install

Nothing is installing, but it does run the post install scripts. And... ding! Suddenly a few files disappeared from the app directory: check.php and SymfonyRequirements.php. check.php is now called bin/symfony_requirements and the SymfonyRequirements file has been moved over to the var directory. All of that was done thanks to that extra configuration.

Require autoload.php instead of bootstrap.php.cache

In the var directory, there's the bootstrap.php.cache file. Delete the original one in app. To un-break our application, we need to go into app_dev.php and require the new path to the bootstrap file. But stop! Like the console file, we're no longer going to include bootstrap.php.cache. Instead require app/autoload.php:

37 lines web/app_dev.php
... lines 1 - 27
$loader = require __DIR__.'/../app/autoload.php';
... lines 29 - 37

Tip

Actually, the require_once was also changed to require, which guarantees that the $loader is returned.

Why? Well, the whole point of bootstrap.php.cache is performance: by having a bunch of classes in one file, it means that the autoloading mechanism has to work less. But when you're debugging, it's not helpful to see errors coming from a deep line in bootstrap.php.cache.

Instead, in the dev environment, only include the normal autoloader. In app.php do the same thing. But since performance is totally rad to have in the prod environment, add an include_once below that for /../var/bootstrap.php.cache:

47 lines web/app.php
... lines 1 - 11
$loader = require __DIR__.'/../app/autoload.php';
include_once __DIR__.'/../var/bootstrap.php.cache';
... lines 14 - 47

Let's give that a try in the browser.

And it still works. I can't seem to break our app.

Moving Tests

We're now down to the last change: it involves the tests directory. Wait, you don't write tests? You are a brave, brave developer.

For the rest of you law-abiding citizens. the tests used to live inside of the bundles themselves. These have now been moved down into a new root directory called, well, tests! Move the AppBundle/Tests directory into tests. Once it's there, rename it to AppBundle so that you have a nice tests/AppBundle.

The idea is to have paths like tests/bundleName/individualClasses. There's no technical reason for this change, it's just a new standard.

With everything moved around, you'll need to update the namespace of each class to be Tests/AppBundle:

... lines 1 - 11
namespace Tests\AppBundle\Controller;
... lines 13 - 41

The reason for this is autoloading: you know, the old "your directory structure must match your namespace" thing. You can sometimes get away with messing this up with tests... but not always.

This is sweet... except that Composer doesn't know to look in the tests/ directory. Open composer.json. Below autoload, create a new section called autoload-dev. Below it add a psr-4 key for { "Tests\\": } pointing at the tests/ directory:

66 lines composer.json
... lines 1 - 9
"autoload-dev": {
"psr-4": { "Tests\\": "tests/" }
},
... lines 13 - 66

We're good!

Moving phpunit.xml.dist

On the topic of tests, the phpunit.xml.dist file normally lives in the app/ directory. That's why you run phpunit -c app.

To simplify things, that has been moved to the root of the project:

mv app/phpunit.xml.dist .

After moving it, we need to update a few things. Update bootstrap to app/autoload.php. In the testsuites section, this is how phpunit know where tests live. This can now simply be changed to tests... which is pretty cool:

41 lines phpunit.xml.dist
... lines 1 - 3
<phpunit
... lines 5 - 13
bootstrap = "app/autoload.php" >
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
... lines 21 - 39
</phpunit>

Finally, uncomment out the php block and tell Symfony where your app directory is by changing the KERNEL_DIR value to app/:

41 lines phpunit.xml.dist
... lines 1 - 3
<phpunit
... lines 5 - 21
<php>
<server name="KERNEL_DIR" value="app/" />
</php>
... lines 25 - 39
</phpunit>

This is for functional tests: when Symfony tries to boot your kernel, it needs to know... where you kernel lives!

Get back to the terminal. But don't run phpunit -c app, that's old news. Just run:

phpunit

And hey, our tests are even passing!

Changes to .gitignore

After making all of these changes, git status looks a little crazy:

git status

Open up .gitignore. Ok, this guy is totally out of date now. Let's help him out.

The var directory now completely holds things that you do not need to commit to your repository. So, instead of ignoring individual things, ignore the entire /var/ directory. We do want to keep the .gitkeep file in cache and logs, but change each to start with /var/. The app/config/parameters.yml file doesn't change and now we need to ignore any phpunit.xml file if it exists. We don't need to ignore /bin/ any longer and I'll remove composer.phar from the list since that's usually installed globally. Keep vendor and web/bundles at the bottom:

8 lines .gitignore
/var/
!var/cache/.gitkeep
!var/logs/.gitkeep
/app/config/parameters.yml
/phpunit.xml
/vendor/
/web/bundles/

Ah that looks much better.

Run git status again in the terminal:

git status

SO much better.

Tiny Configuration Changes

Make one last little change in config.yml. Under the framework key, add assets: ~:

... lines 1 - 20
framework:
... lines 22 - 47
assets: ~
... lines 49 - 104

This guarantees that you can still use the asset() function in twig. We don't need this in 2.8 but in 3.0 you have to turn that on explicitly. This is actually pretty cool: instead of the framework bundle turning on a lot of features automatically, you get to opt into the features you want available.

Okay that is it! Enjoy your brand new fancy directory structure. I really like it because the var directory is stuff I don't need to touch, the app directory is just configuration and the tests/ are all together for the holidays.

Leave a comment!

8
Login or Register to join the conversation
Default user avatar
Default user avatar craig heydenburg | posted 5 years ago

this was an awesome article! thanks for writing it!

49 Reply
Default user avatar
Default user avatar Willem Bielefeld | posted 1 year ago

After following your instructions i can start the application but if i want to run the /app_dev.php dev mode,i get an error:

FatalErrorException in AppKernel.php line 0:
Compile Error: Cannot declare class AppKernel, because the name is already in use

I followered your instructions to the changes of the AppKernel and app_dev php files.
Whats confusing, a new bootstrap.php.cache file will be created inside the /app folder instead of the var /var folder.

So i guess there is something wrong with the ScriptHandler::buildBootstrap part
In the official Symfony 2.7 documentation the Incenteev yaml loader is used. This is not part of your description.
Do i need to adjust it too? Does it even matter? It is used for building a parameters.yaml out of a parameters.yaml.dist.
Why does my buildBootrap Scripthandler creates a bootstrap.php.cache file still in my /app folder, if i configured it to var, and why does the app_dev.php thinks there is an duplicate clss issue?

Running without app_dev.php works fine.

@Edit:
I found the Error myself. I still had one require_once __DIR__.'/../app/AppKernel.php'; left which i overlooked.
With autoload.php in both app.php and app_dev.php it works just fine :)

1 Reply
Oda Avatar

Hi,
I follow the tutorial doing line by line all.
But at video 7min 19 sec
just after to set directory (app,web,var, bin and tests) in "extra" (in composer.json)
I run the command "composer install" and error appears:

/application$ composer install
[...]
Could not open input file: app/console
Script Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::clearCache handling the post-install-cmd event terminated with an exception

In ScriptHandler.php line 220:

An error occurred when executing the "'cache:clear --no-warmup'" command.

[...]

it seems it try to look at app/console and not at /bin/console

Now? what can I look for?

Reply
Christopher M. Avatar
Christopher M. Avatar Christopher M. | posted 3 years ago

If I’m updgrading to Symfony 2.8 only, do I need to upgrade the directory structure?

Reply
Default user avatar
Default user avatar Zuhayer Tahir | posted 5 years ago

Kudos Mahabaniri!

Reply
Default user avatar

Thanks for this article! Great help.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.9",
        "ext-pdo_sqlite": "*",
        "doctrine/doctrine-bundle": "~1.5", // 1.6.3
        "doctrine/doctrine-fixtures-bundle": "~2.2", // 2.3.0
        "doctrine/orm": "~2.4", // v2.5.4
        "erusev/parsedown": "~1.5", // 1.6.0
        "ezyang/htmlpurifier": "~4.7", // v4.7.0
        "incenteev/composer-parameter-handler": "~2.1", // v2.1.2
        "ircmaxell/password-compat": "~1.0", // v1.0.4
        "knplabs/knp-paginator-bundle": "~2.4", // 2.5.3
        "leafo/scssphp": "~0.1.5", // v0.1.10
        "patchwork/jsqueeze": "~1.0", // v1.0.7
        "sensio/distribution-bundle": "^5.0", // v5.0.7
        "sensio/framework-extra-bundle": "~3.0", // v3.0.16
        "symfony/assetic-bundle": "~2.8", // v2.8.0
        "symfony/monolog-bundle": "~2.7", // 2.11.1
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.11
        "symfony/symfony": "3.0.*", // v3.0.9
        "twig/extensions": "~1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0" // v3.0.7
    }
}
userVoice