Struktura stránek

Školení, která pořádám

Při vytváření webové prezentace umisťuji text všech stránek do databáze, což správci umožňuje jejich pohodlnou změnu v administračním rozhraní, obzvlášť s využitím nějakého WYSIWYG editoru. Tabulka pro uložení stránek může mít třeba takovouto strukturu:

CREATE TABLE stranky (
	id int NOT NULL AUTO_INCREMENT,
	poradi int NOT NULL,
	titulek varchar(100) NOT NULL,
	url varchar(100),
	stranka longtext NOT NULL,
	INDEX (poradi),
	UNIQUE (url),
	PRIMARY KEY (id)
);
INSERT INTO stranky (poradi, titulek, url) VALUES (0, 'Stránka nenalezena', NULL);
INSERT INTO stranky (poradi, titulek, url) VALUES (1, 'Titulní stránka', '');

V tabulce je zároveň uloženo i pořadí v navigaci, titulek stránky a URL, na kterém bude stránka dostupná (u nových stránek může být vyplňované JavaScriptovou funkcí). Stránku s nenastaveným URL budeme používat pro zobrazení chyb, stránka s prázdným URL je titulní. U složitějších prezentací může být tabulka samozřejmě dále rozšířena.

Zobrazení stránky s odpovídajícím URL se asi nejsnáze řeší s využitím mod_rewrite, kdy pro přepis všech URL stačí jednoduché pravidlo RewriteRule .* url.php?url=$0. Pokud bychom některé adresy přepisovat nechtěli (např. favicon.ico, robots.txt a soubory v adresáři img), lze před toto pravidlo umístit RewriteCond $0 !^(favicon\.ico|robots\.txt|img(/.*)?)$. Pro přepis URL v Apache existují i jiné metody, ty ale nejsou tak pohodlné (např. využití direktivy ErrorDocument, které ale zapisuje do error logu a nepředává POST data, nebo nastavení direktivy DocumentRoot přímo na PHP skript).

Skript url.php pak může vypadat třeba takto:

<?php
// tady bude připojení k databázi a definování funkcí s designem

$row = mysql_fetch_assoc(mysql_query("SELECT * FROM stranky WHERE url = '" . mysql_real_escape_string($_GET["url"]) . "'"));
if (!$row) {
    header("HTTP/1.1 404 Not Found");
    $row = mysql_fetch_assoc(mysql_query("SELECT * FROM stranky WHERE url IS NULL"));
    if (!$row) {
        $row = array("titulek" => "Stránka nenalezena", "stranka" => "Omlouváme se, požadovaná stránka nebyla nalezena");
    }
}
page_header($row["titulek"]);
echo $row["stranka"];
page_footer();
?>

V případě, že se stránka s požadovaným URL v tabulce nevyskytuje, tak se pošle příslušná hlavička a vezme se stránka s nenastaveným URL, která plní funkci chybové stránky. Pokud ani ta neexistuje, tak se použije jednoduchý text.

Stránky s obsahem získávaným z databáze

Tím je vytvořena infrastruktura pro čistě textové stránky. Pokud se na nějakých stránkách má zobrazovat další obsah z databáze (např. novinky nebo reference), je potřeba tuto infrastrukturu dále rozšířit. Možnosti vidím v zásadě dvě – pokud stačí, aby se dynamický obsah zobrazoval na všech stránkách na stejném místě (nejspíš pod textem stránky), doplnil bych do tabulky sloupec specialni enum('novinky', 'reference') a do skriptu url.php kód:

<?php
if ($row["specialni"]) {
    include "./$row[specialni].php";
}
?>

Pokud by stránky měly být složitější a dynamické komponenty by do nich mohly být vkládány na libovolné místo, jeví se mi nejlepší do textu stránek vkládat speciální značky, které potom funkcí preg_replace_callback nahradíme za skutečný obsah.

Poslání hlaviček speciálními stránkami

Dále je vhodné rozmyslet si chování v případě, kdy by stránka se speciálním významem potřebovala poslat funkcí header nebo setcookie nějaké hlavičky. Vyřešit se to dá např. zapnutím output bufferingu nebo vypsáním textu stránek až z vkládaných souborů:

<?php
// url.php:
if ($row["specialni"]) {
    include "./$row[specialni].php";
} else {
    page_header($row["titulek"]);
    echo $row["stranka"];
    page_footer();
}

// $row[specialni].php:
// poslání hlaviček
page_header($row["titulek"]);
echo $row["stranka"];
// specifický kód
page_footer();
?>

Tím zároveň získáme možnost ve speciálních souborech ovlivnit, kde a za jakých podmínek se bude zobrazovat textový obsah stránky. Na tomto přístupu mi ale přijde nečisté, že skript $row[specialni].php používá proměnnou definovanou souborem url.php.

Asi nejlepší možností podle mě je, že se speciální soubor vloží hned na začátku, odešle případné hlavičky a definuje funkci specialni. Skript url.php pak může vypadat takto:

<?php
if ($row["specialni"]) {
    include "./$row[specialni].php";
}
page_header($row["titulek"]);
echo $row["stranka"];
if (function_exists("specialni")) {
    specialni();
}
page_footer();
?>
Jakub Vrána, Dobře míněné rady, 18.9.2006, diskuse: 47 (nové: 0)

Diskuse

Honca:

Parádní článek.. díky

ikona Radek Hulán:

"Výchovně" bych místo url = '$_GET[url]' raději psal url = '".addslashes($_GET[url])."' ;-)

ikona Jakub Vrána OpenID:

To bych nejprve musel změnit informaci v patičce, že všechny skripty pracují s nastavením magic_quotes_gpc=On.

ikona Radek Hulán:

Jak myslíš. Osobně jsem si toho nevšimnul. SQL-injection je nejčastější bezpečnostní problém, a prezentací takovéhoto kódu začátečníka k němu spíše "popouzíš"..

Pavel:

Myslím, že tento veb je cíleny na zkušenější programátory... a Ti si SQL-injection ohlídají. I když je dobré na toto úsklalí upozornit

ikona Jakub Vrána OpenID:

Kód jsem předělal na magic_quotes_gpc = Off a zmínil jsem to v patičce.

frko:

trochu som lama a nejak som to asi nepochopil,
ten script mi hlási chybu:
Fatal error: Call to undefined function: page_header() in c:\apache\htdocs\sablona\url.php on line 18

čím to asi bude? :-(

PeTa:

No nejsíš to bude tím, že voláš funkci, kterou nemáš napsanou, stejně jako fci page_footer(). Jakub ji použil jako příklad.

frko:

ach tak, zdalo sa mi že ju nikde nespomenul

mach:

| umisťuji text všech stránek do databáze,
| což správci umožňuje jejich pohodlnou změnu
| v administračním rozhraní

IMHO se tohle neda povazovat za vyhodu ukladani struktury stranek do db. Kdyz si strukturu ulozim do xml souboru na disku, tak mi taky nic nebrani nad tim postavit jednoduche administracni rozhrani.

Co se tyce mod_rewrite, pouzivam uz asi 2 roky toto (vlastne to simuluje 404 bez nevyhod 404):

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ ./index.php?web=$1 [L,QSA]

Jinak mam na jednom webu v pripade nenalezeni stranky presmerovani na existujici stranku podle levenshteinovy vzdalenosti (takze kdyz nekdo zada "/jemnz vlasz", tak ho to presmeruje na "/jemne-vlasy"). V PHP to je velice snadne udelat, vzhledem k tomu, ze funkci levenshtein primo obsahuje. Bohuzel MySQL ji nepodporuje a UDF jsou tu az od verze 5.0 - tim padem se musi cela struktura webu pretahnout z databaze do PHP a tam vybrat minimum (coz od nejakych 400 polozek trva docela dlouho).

ikona Milan Kryl:

Nebylo by tam bezpecnejsi pouzit REQUEST_URI namisto REQUEST_FILENAME ?

ikona dgx:

Zjišťuje existenci souboru, nikoliv URI

Ondrej Ivanic:

Ono je to pekne pravidlo, ale robi to strasny chliev :). Zistenie ci subor/adresar existuje je velmi draha operacia. Tych zopar adresarov pre obrazky, ... ktore vzdy existuju sa oplati pridat do RewriteCond.

A to presmerovanie by som urcite riesil cez 404 kde by bol zoznam podobnych stranok zoradeny podla levenshteinovej vzdalenosti. Robit rovno redirect sa mi nezda spravne. Ziadny z kodov 30x sa na to nehodi, proste ta stranka neexistuje.

ikona dgx:

Možná to je drahá operace, ale Apache to musí zjistit tak i tak - vždyť se musí rozhodnout, zda soubor odešle nebo vrátí chybu 404.

Naopak, co je zbytečnou brzdou, tak provádění regulárních výrazů v dalším pravidle RewriteCond.

ikona dgx:

Navrhl bych menší optimalizaci (kterou používám):

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d  # vážně nutné?
RewriteRule ^.*$ /index.php [L]

Zjišťování existence adresáře obvykle nutné není, protože třeba o přístup na example.com/images/ zájem nemáme a raději necháme kontroler vypsat 404. Ale to chce dobře zvážit, případ od případu.

Další věc je předávání parametrů. Mě více vyhovuje přístupový parametr nepředávat a pak zpracovat $_SERVER['REQUEST_URI']. Hlavně se tím dá předejít přímému přístupu na URL index?web=....

Při zpracování REQUEST_URI je dobré vědět, že http://php.vrana.cz/struktura-stranek.php i http://php.vrana.cz/////struktura-stranek.php bývá totéž.

Ale pokud už budeš cestu předávat parametrem, určitě použij flagy [L,NE,QSA]. Na 'NE' zapomíná 99,9 % programátorů, a pak se diví, proč se skript.php?param=%2A v PHP interpretuje jako skript.php?param=%252A.

mach:

Diky za reakci, s tim adresarem je to pravda.

| Mě více vyhovuje ... zpracovat $_SERVER['REQUEST_URI'].
| Hlavně se tím dá předejít přímému přístupu na URL
| index?web=....

Tohle resim jinak. V aplikaci mam metodu, ktera mi prevede pozadovane URI na nejakou promennou (konkretne to je pole) jednoznacne popisujici, co to vlastne uzivatel chce zobrazit (druh stranky, jeji id, ...) Pak tam mam opacnou funkci a ta mi z tohoto deskriptoru stranky sestavi zase jeji adresu (tahle metoda je obecne pouzivana proste k tvoreni odkazu).

Na zacatku pak vzdycky necham prevest adresu pozadovane stranky na toto pole, z pole si zase necham vygenerovat adresu (ovsem ted se jedna o tu jedinou verzi adresy, kterou system preferuje) a kdyz zjistim, ze se to nerovna 'http://' . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'], tak to presmeruji na tu sestavenou.

Vyhoda je, ze odted ma kazda stranka jedinou unikatni adresu at se deje, co se deje. Kdyz uzivatel natuka /index.php?web=xyz, tak je automaticky od-301-čkován na /xyz, kdyz tam napere http://example.com////neco, tak se to presmeruje na http://example.com/neco (protoze mod_rewrite mi nekolikanasobna lomitka sam odstrani, ale v REQUEST_URI zustanou) atd. Pamatuju se, ze driv vicenasobna lomitka dokonce zmatla web jedne z velkych SEO spolecnosti tak, ze obsahoval odkazy na neexistujici adresy, ale asi to uz opravili.

Nevyhoda je v tom, ze to zavisi na obsahu promenne $_SERVER a nevim, jestli bude opravdu na vsech konfiguracich Apache obsah skutecne odpovidat.

ikona dgx:

Koukám, že to děláme dočista stejně :-)

Ta připomínka ohledně spoléhání se na $_SERVER je správná. Kdysi jsem kolem toho hledal informace a REQUEST_URI by měl posílat jakýkoliv Apache server. Vytváření pole $_SERVER lze zakázat direktivou variables_order v PHP.INI, ale tou lze zakázat i $_GET nebo $_POST - což žádný hosting neudělá. Tedy je to spolehlivé.

ikona dgx:

Ještě mě napadlo, je to sice detail... Při porovnávání

http:// $_SERVER['SERVER_NAME'] $_SERVER['REQUEST_URI']

je část $_SERVER['SERVER_NAME'] case insensitive, zatímco $_SERVER['REQUEST_URI'] case sensitive.

mach:

Jeste pro uplnost par veci:

- Popsany presmerovani nebude resit nasledujici duplicitu:

www.example.cz/
www.example.cz./

Je otazka, jestli to nekdo jako duplicitu brat bude.

- Kontrola vuci adresari tam nutna bude, protoze mam typicky napr. adresar "admin" a v nem index.php, pricemz chci, aby byl pristupny hned po vyzadani example.cz/admin/ (bez index.php).

ikona dgx:

To není duplicita, neboť adresa www.example.cz./ je neplatná. Duplicitou by nebylo ani www.example.cz/ vs. www.example.cz

ad kontrola vůči adresářům: platí co jsem psal dříve, je to potřeba dobře zvážit případ od případu. Obvykle takto vytvořený kontroler & htaccess se totiž nepoužívá společně se soubory index.php. V podadresářích spíš bývají obrázky, styly atd. - a tam obvykle index.php není. A v takovou chvíli se hodí, aby 404 zpracoval kontroler.

mach:

|| adresa www.example.cz./ je neplatná

Nevim v jakem smyslu neplatna, ale urcite funguje ( www.google.com./ ) a podle vnitrku tohohle vlanka:

http://seo.nawebu.cz/200604/0146.html

to je i standardni adresa. Google oboje podle vseho chape jako ekvivalentni URI. Seznam ne ("seznam site:seznam.cz./" nic nevrati, zobrazi vysledky Googlu). Ten rozdil samozrejme muze vzniknout jen na urovni rozparsovani uzivatelovat dotazu...

ikona dgx:

Tak to se omlouvám, že lze použít tečku na konci adresy jsem netušil. Jestli jsem to dobře pochopil, jde stále o tutéž adresu? Tedy něco jako www.Example.com vs. www.example.com

ikona Jakub Vrána OpenID:

Mohl bys to s tím NE blíže rozepsat? Pokud je mi známo a podporují mne v tom i provedené pokusy, tak parametry předané díky QSA se už znovu neescapují. Při:

RewriteRule .* url.php?url=$0 [L,QSA]

se skript.php?param=%2A převede na url.php?url=skript.php&param=%2A, žádné %252A se tam nedostane.

ikona dgx:

'Pokud je mi známo' nebo 'zkoušel jsi to'? ;)

Jak říkám, 99,9 % programátorů je to známo špatně...

ikona Jakub Vrána OpenID:

'Pokud je mi známo' i 'zkoušel jsem to'... Nepodařilo se mi vyvolat situaci, kdy by se parametry přepisovaného URL ještě jednou escapovaly.

ikona dgx:

Promiň, já si neuvědomil, že tam není přesměrování, omlouvám se. Zkus

RewriteRule .* url.php?url=$0 [R,L,QSA]

Kuba:

Šlo by uvést příklad, jakým způsobem se v PHP řeší zjišťování podobnosti. Dík

24k:

Trosku OT ale neni syntaxe typu

INSERT INTO SET
SLOUPEC=HODNOTA,
SLOUPEC=HODNOTA2 ...

lepsi nez klasicka INSERT INTO VALUES() ?

ikona Jakub Vrána OpenID:

Také ji mám raději, ale je nestandardní, proto ji nepoužívám.

Honza:

A co použít syntaxi:

INSERT INTO nejaka_tabulka (id, nazev, adresa) VALUES (1, "Nekdo", "Nekde")

Ta je snad "standardní", ne? A nehrozí zhroucení aplikace po přidání nových sloupců (mají-li nastavenu implicitní hodnotu).

ikona Jakub Vrána OpenID:

Ano, tato syntaxe je standardní a také ji doporučuji: http://php.vrana.cz/psani-insert-into.php.

Výhoda INSERT INTO SET spočívá v tom, že se stejný fragment dá použít pro INSERT i UPDATE.

Tibor:

Super, takýto prakticky orientovaný článok poteší viacej ľudí. Čo sa týka rozdelenia stánok tak som mal problém oddeliť textové stránky (články) od výpisov kategórií a podobne. Riešil som to vyňatím týchto stránok z databázy, riešenie ako v článku mi nenapadlo. Čo v prípade keď špeciálnych stránok bude viac, ale majú iba pár rovnakých rozložení? Miesto výnimiek použiť zvlášť tabuľky pre jednotlivé "vzory" obsahu? Je to schodné riešenie, alebo som niečo prehliadol?

3wl4k:

Jakube...
Najlepsi clanok aky som za posledny mesiac cital :))

Dave:

Velice zajimavy clanek jen tak dal :)

Pubso:

Naozaj super clanok... Podobnost mojej pouzivanej metody struktury stranky je podobna s touto a hned sa citim lepsie, ze moje myslienkove pochody neboli az take zle... :-)

Andy:

Používám hodně podobný systém.. jen je problém s vícejazyčnými stránkami.. jak je poté ukládáte?

ikona Jakub Vrána OpenID:

Informace o jazykové verzi by určitě měla být součástí URL, viz http://php.vrana.cz/uchovani-informace-o-…-prihlasenosti.php. Tuto informaci lze pomocí přepisu následně předat jako parametr skriptu pro zpracování.

Jazykové verze ukládám do samostatných sloupců, viz http://php.vrana.cz/ulozeni-jazykovych-verzi.php, překlady statických textů do zvláštní tabulky, viz http://php.vrana.cz/preklad-statickych-textu.php.

Milan:

Dobry den,

Som novacik, tak sorry ak je otazka trivialna...
Nerozumiem, preco preferujete ukladanie stranok do databaze pred XML suborom so sablonou... Ved tym docielim lepsiu flexibilitu rovnako ako jazykove verzie.
A este to bude hierarchicke/s moznostou selekcie a processing bude na strane klienta...????

Alebo nie?

david1:

ahoj, jakube, sice opožděně, ale jak v této struktuře řešíš php kody v jednotlivých stránkách? pres eval() asi ne, to jsi zavrhl, poradíš?

ikona Jakub Vrána OpenID:

Je to popsané v části Stránky s obsahem získávaným z databáze. Ta předpokládá, že je nejprve celý text stránky a potom PHP kód (případně naopak). Pokud to má být volně promíchané, tak považuji za nejlepší dát do stránek nějakou speciální značku (např. <EXEC kontakty>) a tu nahradit funkcí preg_replace_callback() za výstup PHP kódu.

poochy:

všechno jsem udělal podle návodu a taky mě to hlásí

Fatal error: Call to undefined function: page_header() in c:\program files\easyphp1-8\www\pukos\url.php on line 13

databázi mám určitě správně a url.php obsahuje

¨<?php
mysql_connect
("localhost","root") or die("Nelze se připojit k MySQL: " . mysql_error());
mysql_select_db("web") or die("Nelze vybrat databzi: ". mysql_error());

$row = mysql_fetch_assoc(mysql_query("SELECT * FROM stranky WHERE url = '$_GET[url]'"));
if (!
$row) {
    header("HTTP/1.1 404 Not Found");
    $row = mysql_fetch_assoc(mysql_query("SELECT * FROM stranky WHERE url IS NULL"));
    if (!$row) {
        $row = array("titulek" => "Stránka nenalezena", "stranka" => "Omlouváme se, požadovaná stránka nebyla nalezena");
    }
}
page_header($row["titulek"]);
echo
$row["stranka"];
page_footer();

?>

GB:

do stránky url.php pridej tento kod

function page_header(){
echo "zde bude html kod headeru";
}

function page_footer(){
echo "Copyright poochy 2009";
}

Kuba:

Tak jsem se to pokusil udělat s SQLite místo MySQL, celkem to funguje, až na 404. Nemůže tam být problém s tím, že SQLite pracuje nějak jinak s "IS NULL"

<? php
...
$handle = sqlite_open($db) or die("Nelze otevřít databázový soubor.");

$result = sqlite_query($handle, "SELECT * FROM stranky WHERE url = '" . $url . "'") or die("Chyba dotazu: ".sqlite_error_string(sqlite_last_error($handle)));
$row = sqlite_fetch_array($result);
if (!
$row) {
  header("HTTP/1.1 404 Not Found");
  $result = sqlite_query($handle, "SELECT * FROM stranky WHERE url IS NULL") or die("Chyba dotazu: ".sqlite_error_string(sqlite_last_error($handle)));
  $row = sqlite_fetch_array($result);
  if (!$row) {
    $row = array("titulek" => "Stránka nenalezena", "stranka" => "Omlouváme se, požadovaná stránka nebyla nalezena");
    }
}

sqlite_close($handle);
...
?>

develo:

Moc nevím proč to dělat takto, to je nějaký stanard? Je lepší dělat weby podle nějakých standardů(tím ale nemyslím komenty atd...) co jsou prostě nejoptimálnější nebo můžu rozvíjet svou formu psaní? Prosím o nějaké důvody, díky :-) Moc bych uvítal i článek, případně díky za link.

jano:

"V tabulce je zároveň uloženo i pořadí v navigaci"
Neviem, čo sa tým sleduje, k čomu to slúži. Ostatný obsah článku mi je jasný.
Môžete mi to prosím bližšie ozrejmiť, alebo prípadne poradiť nejaký informačný zdroj? Ďakujem. 

ikona Jakub Vrána OpenID:

Pokud je někde na webu navigace (obsah) se všemi stránkami, tak toto je pořadí v této navigaci.

jano:

Ďakujem za vysvetlenie.

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2017 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.