Here is a layout that works well for a medium-sized PHP application. It combines everything from this chapter. Treat it as a starting point, and adapt it.
my-app/
├── public/ <- the ONLY folder the web server exposes
│ ├── index.php front controller
│ ├── .htaccess send all requests to index.php (Apache)
│ └── assets/ css, js, images
├── src/ PSR-4: App\ -> src/
│ ├── Controller/
│ ├── Service/
│ ├── Repository/
│ ├── Model/ entities and value objects
│ ├── Exception/
│ └── Support/ helpers (Str, Env, Logger ...)
├── config/
│ ├── app.php
│ ├── database.php
│ └── routes.php
├── templates/ views and partials
│ ├── layout.php
│ └── products/
├── storage/ writable by PHP, never public
│ ├── logs/
│ ├── cache/
│ └── uploads/
├── database/
│ └── schema.sql table definitions (or migrations)
├── tests/
├── bootstrap.php builds the app (composition root)
├── composer.json
├── composer.lock
├── .env secrets (NOT in Git)
├── .env.example sample (in Git)
└── .gitignore
Why each part exists
| Folder / file | Reason |
|---|---|
public/ | Only front-end entry and assets are reachable. Config, code and logs cannot be downloaded by mistake |
src/ | Application code, autoloaded by Composer |
config/ | Settings in one place |
templates/ | Views are separate from logic |
storage/ | The only folders PHP writes to. Easy to set permissions and exclude from Git |
database/schema.sql | Anyone can recreate the database |
tests/ | Automated checks |
bootstrap.php | One place that creates objects |
.env / .env.example | Secrets out of Git, with documentation of what is needed |
composer.json
{
"name": "acme/my-app",
"require": {
"php": "^8.2",
"vlucas/phpdotenv": "^5.6",
"monolog/monolog": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": { "App\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Tests\\": "tests/" }
}
}
.gitignore
/vendor/
/.env
/storage/logs/*
/storage/cache/*
/storage/uploads/*
Keep empty placeholder files (.gitkeep) in storage/ folders so they exist after a clone.
The request flow
public/index.php
-> bootstrap.php (config, error handling, sessions, container)
-> Router (config/routes.php)
-> Controller (src/Controller)
-> Service (src/Service) -> Repository (src/Repository) -> Database
-> View (templates/)
-> Response
Rules of thumb
- Dependencies point inward: controllers use services, services use repositories. Never the other way.
- One class, one file, one job.
- Nothing public except
public/. - No secrets in Git.
- Write permissions only on
storage/. - Names show roles (
ProductRepository,PlaceOrder,InvalidEmailException).
A layout check
<?php
function missingFolders(array $existing, array $required): array
{
return array_values(array_diff($required, $existing));
}
$required = ["public", "src", "config", "templates", "storage", "tests"];
$existing = ["public", "src", "config", "storage"];
echo "Missing: " . implode(", ", missingFolders($existing, $required));
Missing: templates, tests
Frameworks vs your own structure
For small sites, this structure and a few Composer packages are enough. For larger applications, a framework (Laravel, Symfony) gives you the same layout plus routing, validation, an ORM, queues, mail, authentication and a community. Everything in this course carries over.
Where WordPress fits
WordPress uses a different, older structure: a wp-content/ folder with themes and plugins, and global functions. When you write modern plugin code, you can still use classes, namespaces and Composer inside your plugin. See PHP for WordPress Developers.
Start simple
public/, src/ and composer.json, and add folders when a real need appears.Exercise: place the files
Where would each of these go? (a) ProductRepository.php, (b) a CSS file, (c) the database password, (d) OrderService.php, (e) an uploaded invoice PDF.
Show the answer
(a) src/Repository/ProductRepository.php. (b) public/assets/. (c) In the environment (.env or server settings), read by config/database.php. (d) src/Service/OrderService.php. (e) storage/uploads/, outside the public folder, and served through a PHP script that checks permission.