Svaki projekat donosi nove izazove. WESTAL CMS — kompletan web sistem za Home & Garden centar u Bihaću — nije bio izuzetak. Shared hosting, PHP 7.4, bez root pristupa, bez Composera. Evo pet problema koji su mi oduzeli najviše vremena i kako sam ih riješio.
1. Session se gubi između www i non-www
Problem
Korisnik se prijavi na westal.ba, session se kreira. Browser ga preusmjeri na www.westal.ba — i session nestaje. Admin panel vraća 404, kupac je odjavljen.
Dijagnoza je bila jasna tek kad sam pogledao cookie u DevToolsu: session cookie bio je postavljen za westal.ba ali browser je bio na www.westal.ba. Dvije različite domene — dva različita session cookija.
Rješenje
ini_set('session.cookie_domain', '.westal.ba');
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.cookie_httponly', '1');
Tačka ispred .westal.ba je ključna — govori browseru da cookie vrijedi za sve subdomene, uključujući i www. Zajedno s www → non-www redirectom u .htaccess kao prvim pravilom (redoslijed je bitan!), problem je potpuno riješen.
Naučena lekcija: Cookie domain i redirect moraju biti konzistentni. Postavljanje session.cookie_domain treba biti u bootstrap.php prije svakog session_start().
2. PHP 7.4 — neočekivani sintaksni problemi
Problem
Shared hosting na PHP 7.4. Kod koji lokalno radi uredno (PHP 8.2) na produkciji baca parse error:
PHP Parse error: syntax error, unexpected '=>'
Razlog: koristio sam match() izraz koji je uveden u PHP 8.0.
Slični problemi pojavili su se s str_starts_with(), str_ends_with(), string|int union type hintovima, fn() => arrow funkcijama unutar stringova i IF NOT EXISTS na ALTER TABLE u MySQL-u.
Rješenja
// PHP 8.0 match() → PHP 7.4 if/elseif
// Umjesto:
$result = match($sort) {
'desc' => "ORDER BY date DESC",
'asc' => "ORDER BY date ASC",
default => "ORDER BY date DESC",
};
// Koristiti:
if ($sort === 'desc') {
$result = "ORDER BY date DESC";
} elseif ($sort === 'asc') {
$result = "ORDER BY date ASC";
} else {
$result = "ORDER BY date DESC";
}
// str_starts_with() → strpos()
if (strpos($string, 'prefix') === 0) { ... }
// str_ends_with() → substr()
if (substr($string, -4) === '.sql') { ... }
Za MySQL, ALTER TABLE ... ADD COLUMN IF NOT EXISTS nije podržano u starijim verzijama MariaDB. Rješenje je pokretanje svakog ALTER TABLE posebno i ignorisanje #1060 Duplicate column name greške.
Naučena lekcija: Uvijek testirajte na ciljnoj PHP verziji. Ako ne možete, provjerite PHP Compatibility Checker.
3. AJAX fetch blokiran CORS policy-jem
Problem
Admin panel radi AJAX pozive za detalje narudžbi i promjenu statusa. U konzoli se pojavljuje:
Access to fetch at 'https://www.westal.ba/admin/pages/narudzbe.php'
from origin 'https://westal.ba' has been blocked by CORS policy
BASE_URL je https://westal.ba, ali browser pristupa kroz https://www.westal.ba. Apsolutni URL u fetch() pozivima kreira cross-origin zahtjev.
Rješenje
Koristiti relativne URL-ove za interne AJAX pozive:
// Pogrešno — apsolutni URL može biti drugačiji domain od browsera
fetch('https://westal.ba/admin/pages/narudzbe.php', { ... })
// Ispravno — relativni URL uvijek koristi isti origin
fetch('narudzbe.php', { ... })
Za javni search endpoint koji se poziva s javnih stranica (gdje www može biti drugačiji od PHP BASE_URL), dodati Access-Control-Allow-Origin header:
header('Access-Control-Allow-Origin: *');
Naučena lekcija: Za interne admin AJAX pozive uvijek koristiti relativne URL-ove. Apsolutne koristiti samo kada su nužni i paziti na konzistentnost domena.
4. <script> unutar innerHTML se ne izvršava
Problem
AJAX poziv vraća HTML s <script> tagom koji sadrži funkciju. Modal prikazuje HTML, ali funkcija nije dostupna:
Uncaught ReferenceError: promijeniStatusModal is not defined
Browser ne izvršava <script> tagove koji su ubačeni kroz innerHTML — ovo je namjerno sigurnosno ograničenje.
Isti problem nastao je s dodajSpecRed() funkcijom koja je koristila innerHTML s navodnicima unutar navodnika:
// Lomi sintaksu — single quote unutar single-quote stringa
div.innerHTML = '...<button onclick="this.closest('.spec-red').remove()">...'
Rješenja
Funkcije koje se pozivaju iz dinamički generiranog HTML-a moraju biti u glavnom <script> bloku stranice — ne u AJAX odgovoru:
// Funkcija u glavnom scriptu stranice — uvijek dostupna globalno
function promijeniStatusModal(id, status, btn) { ... }
Za dinamičko kreiranje elemenata s event handlerima koristiti createElement umjesto innerHTML:
// Umjesto innerHTML s miješanim navodnicima:
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = '✕';
btn.onclick = function() { this.closest('.spec-red').remove(); };
div.appendChild(btn);
Naučena lekcija: Nikad ne stavljati funkcije koje trebaju biti globalno dostupne u AJAX odgovore. createElement je pouzdaniji od innerHTML kada imate event handlere.
5. .htaccess redoslijed — neprimjetna greška koja lomi sve URL-ove
Problem
URL rewrite funkcioniše za neke stranice, ali /shop/naziv-artikla uvijek vraća 404. Direktan pristup shop-detalj.php?slug=test radi normalno.
Uzrok: www → non-www redirect pravilo bilo je dodano na kraj .htaccess fajla, između rewrite pravila. Plus fizički shop/folder postojao je na serveru — Apache pronalazi folder i ne radi rewrite.
# POGREŠNO — redirect između rewrite pravila
RewriteRule ^vijesti/...
RewriteRule ^kategorije/...
# www redirect na krivom mjestu — prekida pravila ispod njega
RewriteCond %{HTTP_HOST} ^www\.westal\.ba$
RewriteRule ^ https://westal.ba%{REQUEST_URI} [R=301,L]
RewriteRule ^shop/... ← nikad se ne izvršava
Rješenje
Ispravan redoslijed u .htaccess:
RewriteEngine On
RewriteBase /
# 1. PRVO — www → non-www redirect
RewriteCond %{HTTP_HOST} ^www\.westal\.ba$ [NC]
RewriteRule ^ https://westal.ba%{REQUEST_URI} [R=301,L]
# 2. Sigurnost
# 3. Slug rewrite BEZ RewriteCond !-f !-d
# (ne provjeravati postoji li fizički fajl/folder)
RewriteRule ^shop/([a-z0-9-]+)/?$ shop-detalj.php?slug=$1 [QSA,L]
# 4. Statičke stranice S RewriteCond provjером
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^shop/?$ shop.php [QSA,L]
Fizički prazan shop/ folder obrisan s hostinga — Apache ga je tretirao kao pravi direktorij.
Naučena lekcija: www → non-www redirect uvijek staviti na prvo mjesto. Slug rewrite pravila ne trebaju RewriteCond !-f !-djer slugovi nikad ne odgovaraju fizičkim fajlovima. Praznite foldere čistiti s hostinga.
Zaključak
Svaki od ovih problema bio je nevidljiv u development okruženju i pojavilo se tek na produkciji. Zajednički imenitelj: razlika između lokalnog servera (PHP 8.2, clean domain, root pristup) i shared hostinga (PHP 7.4, www/non-www, bez CLI-a).
Dokumentiranje rješenja u toku razvoja uštedilo bi sate debugginga. Nadam se da ovaj zapis nekome skrati put do rješenja.