Skip to content

PHP Tutorial Practical PHP Development

Clean PHP Project Structure

Lesson 325 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

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.

Text
PLAIN
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 / fileReason
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.sqlAnyone can recreate the database
tests/Automated checks
bootstrap.phpOne place that creates objects
.env / .env.exampleSecrets out of Git, with documentation of what is needed

composer.json

data.json
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

Text
PLAIN
/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

Text
PLAIN
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

  1. Dependencies point inward: controllers use services, services use repositories. Never the other way.
  2. One class, one file, one job.
  3. Nothing public except public/.
  4. No secrets in Git.
  5. Write permissions only on storage/.
  6. Names show roles (ProductRepository, PlaceOrder, InvalidEmailException).

A layout check

index.php
PHP
<?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));
Output
OUTPUT
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.

lightbulb
Start simple
You do not need every folder on day one. Start with 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.