The Project, Directory by Directory
What src, config, templates, public, var, bin and vendor each hold, and which of them must never be web-readable.
src/Controller/Admin/DashboardController.php -> App\Controller\Admin\DashboardControllerA new Symfony project is a fixed set of directories. Knowing which you edit, which are generated and which are private removes most early confusion.
| Directory | Holds | You edit it |
|---|---|---|
src/ | Your PHP classes, including Kernel.php | Yes |
config/ | Configuration and routing files | Yes |
templates/ | Twig templates | Yes |
tests/ | Test classes | Yes |
bin/ | console, the command line entry point | Rarely |
public/ | index.php plus images, CSS, JavaScript | Assets only |
var/ | Compiled cache and log files | No |
vendor/ | Packages installed by Composer | Never |
Where a class lives matters
composer.json maps the App\ namespace to src/ using PSR-4. The path below src/ must therefore match the namespace below App\, or autoloading will not find the class. Some directories carry extra meaning on top of that: src/Controller/ is scanned for route attributes by default.
Generated, not yours
var/ and vendor/ are rebuilt from source and are git-ignored. var/cache/ holds configuration compiled down to PHP; in the dev environment it refreshes itself when files change, and php bin/console cache:clear forces it.
Only one directory is public
The web server's document root is public/, and public/index.php is the single file it executes. Everything else — .env, config/, src/, var/, vendor/ — sits above the document root and is unreachable over HTTP.
Example
<?php
// src/Controller/Admin/DashboardController.php
declare(strict_types=1);
// composer.json maps App\ to src/ (PSR-4). The directory path after
// src/ must match the namespace after App\, character for character,
// or the class is never autoloaded and the route never exists.
namespace App\Controller\Admin;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class DashboardController extends AbstractController
{
// Subdirectories under src/Controller/ are still scanned, so the
// nesting is for your benefit, not Symfony's.
#[Route('/admin', name: 'app_admin_dashboard', methods: ['GET'])]
public function index(): Response
{
return new Response('<h1>Dashboard</h1>');
}
}When to use it
- Place a new class so that autoloading and route discovery both find it.
- Review a deployment that serves the project directory instead of public/.
- Decide what belongs in version control and what is regenerated on install.
More examples
The skeleton as it arrives
Worth reading once before you start moving files. Adding packages adds files under config/packages/ and may add top-level directories such as migrations/ or translations/, but the directories above are always present.
my_project/
|-- bin/
| `-- console # command line entry point
|-- config/
| |-- packages/ # one file per installed package
| |-- routes/
| |-- bundles.php # which packages are enabled
| |-- routes.yaml
| `-- services.yaml # service container configuration (Stage 2)
|-- public/
| `-- index.php # the only web-executed file: document root
|-- src/
| |-- Controller/
| `-- Kernel.php
|-- templates/
|-- tests/
|-- var/
| |-- cache/ # generated, git-ignored
| `-- log/ # generated, git-ignored
|-- vendor/ # installed by Composer, git-ignored
|-- .env
`-- composer.jsonsrc/Kernel.php, the file that ties it together
Almost empty, and that is the point: the directory conventions live in MicroKernelTrait rather than in your code. You only touch this file when a project needs cache or log paths somewhere other than var/, which happens on read-only container filesystems.
<?php
// src/Kernel.php
// Generated by Composer; shown exactly as it arrives.
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
// The trait supplies the conventions: load config/packages/*,
// config/services.yaml and config/routes/* from their standard
// locations, and cache the result in var/cache/.
use MicroKernelTrait;
}A web server rooted at public/
The shape every deployment needs, whether on a virtual machine or inside a container image. Stage 7 builds on it; what matters now is that the root line ends in /public and nothing above it is served.
# /etc/nginx/sites-available/my_project
server {
server_name example.com;
# The document root is public/, never the project directory.
root /var/www/my_project/public;
location / {
try_files $uri /index.php$is_args$args;
}
# The front controller, and the only .php file nginx executes.
# Without this block the try_files redirect to /index.php falls
# through to the catch-all below and every request returns 404.
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/var/run/php/php8.5-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}
# Every other .php file is refused, so index.php stays the single
# entry point.
location ~ \.php$ {
return 404;
}
}
Discussion