Blog / Tehnologija i razvoj / Web arhitektura

Kako organizirati PHP projekt
bez frameworka

PHP projekt bez frameworka može biti uredan, siguran i dugoročno održiv ako ima jasnu strukturu mapa, jedan ulaz u aplikaciju, odvojene odgovornosti, centralni config i discipliniran deploy workflow.

Organizacija PHP projekta bez frameworka kroz mape, routing i module

Pisati PHP bez frameworka ne znači pisati neorganiziran kod. Problem ne nastaje zato što projekt nema Laravel, Symfony ili neki drugi framework, nego zato što nema pravila. Ako znate gdje žive rute, konfiguracija, viewovi, servisi, helperi i javni asseti, mali custom PHP projekt može ostati vrlo pregledan.

Glavna ideja: ne pokušavajte izmisliti vlastiti framework. Cilj je složiti dovoljno strukture da projekt bude čitljiv, siguran i jednostavan za održavanje, ali ne toliko strukture da mala web stranica postane nepotrebno težak softverski proizvod.

1. Krenite od granice između javnog i privatnog dijela projekta

Prva odluka je gdje se nalazi web root. U idealnom slučaju javno dostupna mapa sadrži samo ulaznu datoteku aplikacije i assete koje browser smije dohvatiti. Konfiguracija, klase, template logika, SQL helperi i osjetljive datoteke ne bi trebale biti izravno dostupne kroz URL.

Na shared hostingu se često radi unutar public_html mape, pa nije uvijek moguće imati savršenu strukturu. Ali i tada se može napraviti disciplina: javne stranice, uploadi i asseti moraju biti jasno odvojeni od konfiguracije, biblioteka i administracijskog koda.

Struktura Jednostavna mapa projekta
project/
├── public/
│   ├── index.php
│   ├── assets/
│   │   ├── css/
│   │   ├── js/
│   │   └── images/
│   └── uploads/
├── app/
│   ├── Controllers/
│   ├── Services/
│   ├── Repositories/
│   ├── View/
│   └── Support/
├── config/
│   ├── app.php
│   └── routes.php
├── templates/
│   ├── layouts/
│   └── pages/
└── var/
    ├── logs/
    └── cache/

Ako hosting traži da je sve u public_html, ista ideja se može prilagoditi. Bitno je da znate što smije biti javno, a što ne. Datoteke s lozinkama, SMTP podacima, logovima i internom logikom nikada ne smiju biti slučajno dostupne kao običan download.

2. Jedan ulaz u aplikaciju olakšava routing

Ako svaka stranica ima svoj zasebni PHP entry point, projekt s vremenom postaje težak za kontrolu. Jedan index.php kao front controller omogućuje da se centralno učitaju config, helperi, autoload, sigurnosne postavke i routing. Nakon toga aplikacija odlučuje koji sadržaj prikazati.

PHP Minimalni front controller
<?php

declare(strict_types=1);

require __DIR__ . '/../config/bootstrap.php';

$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$path = '/' . trim($path, '/');
$path = $path === '/' ? '/' : $path;

$routes = require APP_ROOT . '/config/routes.php';

if (!isset($routes[$path])) {
    http_response_code(404);
    render('errors/404', ['path' => $path]);
    exit;
}

[$controller, $method] = $routes[$path];

(new $controller())->$method();

Za SEO je ovo korisno jer routing i canonical URL mogu biti pod jednom kontrolom. Ako vas zanima taj dio detaljnije, povezana tema je struktura URL-ova za SEO.

3. Bootstrap datoteka treba pripremiti aplikaciju, ne raditi posao aplikacije

Bootstrap je mjesto gdje definirate konstante, učitate autoload, postavite error handling, konfiguraciju i osnovne helper funkcije. Bootstrap ne bi trebao dohvaćati sadržaj stranice, slati mailove ili donositi poslovne odluke. On priprema okruženje, a aplikacija radi posao.

PHP Primjer bootstrap datoteke
<?php

declare(strict_types=1);

define('APP_ROOT', dirname(__DIR__));

require APP_ROOT . '/app/Support/autoload.php';

$config = require APP_ROOT . '/config/app.php';

date_default_timezone_set($config['timezone'] ?? 'Europe/Zagreb');

ini_set('display_errors', $config['debug'] ? '1' : '0');
ini_set('log_errors', '1');
ini_set('error_log', APP_ROOT . '/var/logs/php-error.log');

4. Config mora biti centralan i odvojen po okruženju

Jedan od najčešćih problema u malim PHP projektima je konfiguracija razbacana po datotekama. SMTP podaci u kontakt formi, baza u admin handleru, base URL u templateu, debug postavke u zasebnom includeu. To radi dok je projekt mali, ali brzo postaje krhko.

Centralni config smanjuje rizik i olakšava deploy. Produkcijski podaci trebaju ostati na produkciji, lokalni config lokalno, a osjetljive vrijednosti ne smiju završiti u repozitoriju.

PHP Config kao array
<?php

return [
    'debug' => false,
    'timezone' => 'Europe/Zagreb',
    'base_url' => 'https://example.com',
    'assembly' => '1.0.72',
    'mail' => [
        'host' => 'smtp.example.com',
        'port' => 587,
        'encryption' => 'tls',
    ],
];
Praktično pravilo: ako mijenjate istu vrijednost na tri mjesta, nemate config nego kopije configa. To prije ili kasnije pukne kod deploya.

5. Autoload ne mora biti kompleksan

Za mali projekt možete koristiti Composer autoload, što je najurednije ako već koristite vanjske biblioteke poput PHPMailer-a. Ako ne koristite Composer, i jednostavan PSR-like autoload može biti dovoljan. Bitno je da ne radite desetke ručnih require poziva po svakoj stranici.

PHP Jednostavan autoload za App namespace
<?php

spl_autoload_register(function (string $class): void {
    $prefix = 'App\\\\';

    if (!str_starts_with($class, $prefix)) {
        return;
    }

    $relative = substr($class, strlen($prefix));
    $path = APP_ROOT . '/app/' . str_replace('\\\\', '/', $relative) . '.php';

    if (is_file($path)) {
        require $path;
    }
});

6. Viewovi ne bi trebali sadržavati poslovnu logiku

Template smije prikazivati podatke, ali ne bi trebao odlučivati kako se dohvaćaju iz baze, koji status ima narudžba ili kako se šalje email. Ako view postane mjesto gdje žive SQL, validacija i HTML zajedno, projekt će biti teško održavati.

Sloj
Treba raditi
Ne treba raditi
Controller
Primiti request, pozvati servis, odabrati view.
Sadržavati veliki SQL ili HTML blokove.
Service
Držati poslovna pravila i orkestraciju.
Echoati HTML ili čitati globalni POST bez kontrole.
Repository
Komunicirati s bazom i vraćati podatke.
Odlučivati kako izgleda stranica.
View
Prikazati već pripremljene podatke.
Slati mail, mijenjati bazu ili čitati config.

7. Helper za renderiranje čuva layout dosljednim

Čak i bez template enginea, možete imati uredan render helper. Layout, header, footer, meta podaci i varijable stranice trebaju biti dosljedni. To smanjuje rizik da jedna stranica nema canonical, druga nema OG sliku, a treća slučajno učita krivi CSS.

PHP Render helper s izoliranim scopeom
<?php

function render(string $template, array $data = []): void
{
    extract($data, EXTR_SKIP);

    ob_start();
    require APP_ROOT . '/templates/pages/' . $template . '.php';
    $content = ob_get_clean();

    require APP_ROOT . '/templates/layouts/main.php';
}

Ako gradite CMS ili dinamične stranice, ova disciplina se direktno veže uz članak kako napraviti custom CMS za malu poslovnu web stranicu.

8. Asset helper sprječava cache i path probleme

Putanje do CSS-a, JavaScripta i slika ne bi trebalo ručno slagati po cijelom projektu. Centralni helper za assete može dodati base URL, assembly verziju i spriječiti sitne greške koje nakon deploya izgledaju kao da se stranica nije promijenila.

PHP Asset helper s verzijom
<?php

function asset_url(string $path): string
{
    $path = '/' . ltrim($path, '/');
    $version = config('assembly');

    return config('base_url') . $path . '?v=' . rawurlencode($version);
}

Ovo je mala stvar koja štedi puno vremena. Detaljnije smo je obradili u vodiču za cache busting CSS-a, JS-a i slika.

9. Kontakt forme i vanjske biblioteke držite u servisima

Kada projekt koristi PHPMailer, API klijente, payment gateway ili vanjske integracije, te biblioteke ne bi trebale biti razbacane po templateima. Servisni sloj daje jedno mjesto za validaciju, konfiguraciju, slanje, logiranje i obradu grešaka.

PHP Skica mail servisa
<?php

final class ContactMailer
{
    public function __construct(private MailTransport $transport) {}

    public function sendContactMessage(ContactMessage $message): void
    {
        if (!$message->isValid()) {
            throw new InvalidArgumentException('Invalid contact message.');
        }

        $this->transport->send(
            subject: 'Novi upit s web stranice',
            body: $message->toEmailBody()
        );
    }
}

Za sigurnosne detalje oko forme, CSRF-a, honeypota i SMTP-a povezan je članak sigurna PHP kontakt forma preko SMTP-a.

10. Logovi, cache i privremene datoteke ne idu u javni dio

Log datoteke mogu sadržavati email adrese, stack traceove, pathove, IP adrese ili dijelove requesta. Zato ne smiju biti u javno dostupnoj mapi. Isto vrijedi za cache, privremene exporte, backup datoteke i sve što nije namijenjeno browseru.

Sigurna organizacija runtime datoteka

  • Logovi idu u var/logs ili hosting lokaciju koja nije javna.
  • Cache ide u zasebnu mapu koju deploy može očistiti bez brisanja sadržaja.
  • Uploadi trebaju validaciju tipa, veličine i ekstenzije.
  • Backup i export datoteke ne smiju ostati dostupne kroz URL.
  • Greške na produkciji se logiraju, ali se ne prikazuju korisniku.

11. Ne uvodite sloj ako ne rješava stvaran problem

Dobra organizacija nije isto što i puno mapa. Ako projekt ima tri stranice, nema smisla uvoditi deset apstrakcija. Ako ima blog, CMS, kontakt forme, reference, više layouta i deploy workflow, tada struktura postaje korisna. Cilj je da sljedeća dorada ima očito mjesto.

Situacija
Dovoljno jednostavno
Dodajte strukturu kad
Statična stranica
Include header/footer i nekoliko page datoteka.
Počinju se ponavljati meta, CTA i routing pravila.
Kontakt forma
Jedan handler uz validaciju i SMTP.
Forma ima više tipova upita, logiku i integracije.
Blog ili CMS
Ručne PHP stranice za mali broj objava.
Sadržaj treba uređivanje, status, slike i SEO polja.

Zaključak: PHP bez frameworka traži manje magije, ali više discipline

Framework donosi pravila unaprijed. Ako ga ne koristite, pravila morate definirati sami: gdje je web root, kako radi routing, gdje je config, kako se renderiraju viewovi, kako se učitavaju klase, gdje žive servisi i kako se radi deploy. Kada su ta pravila jasna, PHP bez frameworka može biti vrlo učinkovit za male i srednje custom web projekte.

Najbolji rezultat je projekt koji nije ni kaotična gomila includeova ni preveliki pseudo-framework. Dovoljno je strukturiran da se lako održava, a dovoljno jednostavan da ostane brz, razumljiv i praktičan. Za širu odluku o tehnologiji pogledajte i PHP vs .NET za poslovne web stranice.

Custom web razvoj Custom CMS vodič