Konfigurálható szép url-ek htaccess-el

Nos, ez már nem az első hasonló témájú bejegyzésem. De úgy gondoltam ebből sosem elég. Korábban írtam arról, hogyan lehet szép url-eket készíteni htaccess-el. Egyben arról is szólt a bejegyzés, hogy hogyan lehet többféleképpen különböző aloldalakat létrehozni. Ennek folytatásaként most mutatok egy megoldást arra, hogy egy config fájlban lehessen egyfajta mintát adni a használni kívánt url-ekre oly módon, hogy közben már azt is megadjuk, az url melyik része milyen $_GET változóba kerüljön.

Ez a megoldás feltételezi, hogy valamennyire már értünk a reguláris kifejezések használatához, de a legfontosabb tudnivalókat én is elmondom. Pár dolgot már kifejtettem a Link attribútumainak megkeresése bejegyzésemben is. És most neki is vágnék az aktuális témának.

Mappa struktúra:

  • conf - config fájlok mappája
    • urlpatterns.php - url minták
  • pages - oldalak
    • errors/ - hibaoldalak
      • error404.php - 404 hibaoldal
    • admin.php admin oldal
    • contact.php - kapcsolat
    • home.php - főoldal
    • user.php - felhasználó adatlap + beállítások
    • .htaccess - Közvetlen elérés tiltása
  • index.php - Az oldal kerete és előkészítése.
  • .htaccess - Az okos kis átirányító fájlunk

.htaccess fájl a gyökérben
Az első fájl amit meg kell írni, a .htaccess fájl, ami minden kérést egyetlen fájlnak fog átadni, ami nem más, mint az index.php-nk.

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule (.*) index.php?%{QUERY_STRING}&URL=$1 [L]
</IfModule>

!-f és !-d végű sorok miatt csak a nem létező fájlokra és mappákra lesz érvényes az átirányítás. Így például egy css fájlt vagy képet ugyanúgy lehet linkelni. Nem lesz az index-re irányítva.
A RewriteRule kezdetű sor minden kérést átirányít háttérben az index.php-re. Az eredeti query stringet is hozzácsapja, de a végére még egy URL nevű változóba magát az útvonalat is beteszi, ami a kérésben szerepelt. Persze csak attól a mappától kezdve, ahol a htaccess van. Ezzel a megoldással az URL változót akkor se lehet felülírni, ha külön beírom url-be "?URL=valami". Ha a cél épp az ellenkezője, akkor a következőképpen is lehet írni ezt a sort a QUERY_STRING változó kihagyásával:

RewriteRule (.*) index.php?URL=$1 [QSA,L]

A QSA ( Query String Append ) opció miatt az url végére odateszi az eredeti query stringet is.

conf/urlpatterns.php

<?php
$patterns = array(
        '^(?<page>user|admin)/(?<subpage>[^/]*)/?$',
        '^(?<page>[^/]*)/?$'
);
?>

Ennek gyakorlatilag a lényege, hogy reguláris kifejezéseket használva adunk egy szabályt az url-ekre. Az első szabály ezek közül, aminek megfelel a $_GET['URL'], az lesz a használt szabály. Tehát fontos, hogy a szabályokat olyan sorrendben adjuk meg, hogy a szűkebb szabály kerüljön előre és utána az általánosabb. Lehetne ezt szebben is persze, de ezzel is lehet már kezdeni valamit. A ?<valami> kifejezések a zárójeles kifejezések neveit adják meg. Tehát annak a mintának megfelelő url darabka, ami a zárójelben ez után szerepel, bekerül a $_GET['valami'] változóba majd. A fenti példában ez azt jelenti, hogy lesz két oldal ( user és admin ) amiknek lehet aloldala is. És ezek az aloldalak a $_GET['subpage'] változóba kerülnek, míg az oldalnév a $_GET['page'] -be. A kérdőjel a végén pedig azért van, hogy ne legyen kötelező az url-t / jellel lezárni. A második szabály akkor lép érvénybe, ha az első nem volt jó. Tehát minden más oldal esetén, ami nem az admin és nem is a user oldal. Itt nem definiáltam aloldalt neki.

pages/errors/error404.php 404 hibaoldal
Ide én leszámítva az alcím megadását, csak egy egyszerű hibaüzenetet írtam mindenféle cifrázás és formázás nélkül. De mindenki cselekedjen kedve szerint. De csak a body -ba kerülő részt írjuk ide.

<?php Tpl::$subtitle = 'Ezt elbaltáztad'; ?>
Nincs ilyen oldal

A Tpl::$subtitle változót majd az index.php-ben mutatom meg. Tulajdonképpen a Tpl egy kizárólag a $subtitle statikus tulajdonságot tartalmazó osztály lesz. Sokkal bővebb és szebb sablonkezelőt is lehetne írni, de ez megint csak nem témája ennek a bejegyzésnek. Csak egy kis kitérő, hogy lássuk, így is be lehet állítani minden oldal címét.

pages/.htaccess - hogy ne lehessen közvetlenül megnyitni a fájlokat csak php-ból meghívni

order deny,allow
deny from all
allow from localhost

pages/contact.php

<?php Tpl::$subtitle = 'Kapcsolat'; ?>
kapcsolat

pages/home.php

<?php Tpl::$subtitle = 'Home'; ?>
Főoldal

pages/user.php - user profil és beállítások

Felhasználók<br />
<?php
$subpage = (isset($_GET['subpage'])) ? $_GET['subpage'] : '';
if ($subpage == 'profile' or $subpage == '')
{
        Tpl::$subtitle = 'Profil megtekintés';
        print "Ez itt a profil oldal";
}
else if ($subpage == 'settings')
{
        Tpl::$subtitle = 'Beállítások';
        print "User beállításai";
}
else
{
        Tpl::$subtitle = 'Ezt elbaltáztad.';
        print "Nincs ilyen oldal";
}
?>

pages/admin.php - Admin oldal

Ez itt az admin oldal<br />
<?php
$subpage = (isset($_GET['subpage'])) ? $_GET['subpage'] : '';
if ($subpage == 'users')
{
        Tpl::$subtitle = 'Admin - Userek kezelése';
        print "Itt lehet a usereket módosítgatni";
}
else if ($subpage == 'settings')
{
        Tpl::$subtitle = 'Admin - Beállítások';
        print "Az oldal különböző beállításai";
}
else
{
        Tpl::$subtitle = 'Ejj, hát legalább admin ne hibázzon..';
        print "Nincs ilyen oldal";
}
?>

index.php - És itt a lényeg
Először kell az urlpatterns.php a szabályok miatt

require dirname(__FILE__).'/conf/urlpatterns.php';

Majd a szabályok kiértékelése

$notFound = false;
if (isset($_GET['URL']))
{
        $notFound = true;
        //Szabályok ellenőrzése
        foreach ($patterns as $pattern)
        {
                if (preg_match('#'.$pattern.'#', $_GET['URL'],$matches))
                {
                        $notFound = false;
                        $_GET = array_merge($_GET, $matches);  
                        break;
                }
        }
}

A $notFound csak azért van, hogy el lehessen dönteni, volt-e bármiféle szabályra illeszkedés. Ha nem is volt URL, akkor az index.php töltődik be alapból. És a főoldal. Tehát nevezhetjük ezt már találatnak. Nem kell a szabályokkal bajlódni. Egyéb esetben feltételezzük, hogy nem sikerült találni semmilyen illeszkedő szabályt. De ha mégis, akkor megkapja a false értéket a változó. Majd a 404 hibaüzenetnél lesz jelentősége. A $_GET tömbhöz pedig hozzácsapjuk a $matches tömböt is. Amiben benne vannak az url egyes részei már megnevezve. Sajnos felesleges értékek is kerülnek bele. De azzal most nem kell foglalkoznunk.

Az említett, marha bonyolult Tpl osztály:

class Tpl
{
        public static $subtitle = '';
}

Csak a változó globális elérhetősége miatt van értelme ebben a formában az osztálynak. De persze kibővíthető tetszés szerint metódusokkal is.

Megfelelő oldal kiválasztása

$page = !empty($_GET['page']) ? $_GET['page'] : 'home';
$page = 'pages/'.basename($page).'.php';
$page = (file_exists($page) and !$notFound) ? $page : 'pages/errors/404.php';
ob_start();
require_once ($page);
$page = ob_get_clean();

A bejegyzés elején linkelt cikkben ezt a részt már részleteztem. Itt csak a $notFound változó vizsgálatával bővült a feltétel. Valamint kimenet pufferelést használtam, hogy utána a sablonban már csak kiírni kelljen mindent.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Teszt oldal<?php print (!empty(Tpl::$subtitle) ? ' - '.Tpl::$subtitle : '');  ?></title>
<base href="http://localhost/web/teszt/htaccess/" />
</head>
<body>
        <ul>
                <li><a href="home/">Főoldal</a></li>
                <li><a href="user/profile">Profil megtekintés</a></li>
                <li><a href="user/settings">Profil módosítás</a></li>
                <li><a href="admin/users">Userek módosítása</a></li>
                <li><a href="admin/settings">Admin beállítások</a></li>
        </ul>
        <?php print $page; ?>
</body>
</html>

Na itt lehet látni, hogy már játszom az alcímmel is. Persze csak akkor írom ki, ha nem üres. Mert egyébként minek ugye. A html base tag-ben pedig továbbra is a teljes url-t kell megadni a project könyvtárig. Ami nálam éppen a fent látható. De ezt mindenkinek át kell írni. Tehát az index.php egyben:

<?php
require dirname(__FILE__).'/conf/urlpatterns.php';
$notFound = false;
if (isset($_GET['URL']))
{
        $notFound = true;
        //Szabályok ellenőrzése
        foreach ($patterns as $pattern)
        {
                if (preg_match('#'.$pattern.'#', $_GET['URL'],$matches))
                {
                        $notFound = false;
                        $_GET = array_merge($_GET, $matches);  
                        break;
                }
        }
}
class Tpl
{
        public static $subtitle = '';
}

$page = !empty($_GET['page']) ? $_GET['page'] : 'home';
$page = 'pages/'.basename($page).'.php';
$page = (file_exists($page) and !$notFound) ? $page : 'pages/errors/404.php';
ob_start();
require_once ($page);
$page = ob_get_clean();
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Teszt oldal<?php print (!empty(Tpl::$subtitle) ? ' - '.Tpl::$subtitle : '');  ?></title>
<base href="http://localhost/web/teszt/htaccess/" />
</head>
<body>
        <ul>
                <li><a href="home/">Főoldal</a></li>
                <li><a href="user/profile">Profil megtekintés</a></li>
                <li><a href="user/settings">Profil módosítás</a></li>
                <li><a href="admin/users">Userek módosítása</a></li>
                <li><a href="admin/settings">Admin beállítások</a></li>
        </ul>
        <?php print $page; ?>
</body>
</html>

Ezzel kész is van egy használható kis rendszerecske. Nem a legprofibb, nem a legoptimálisabb, de mégis hasznos, úgy érzem. Használjátok egészséggel. Adott esetben náthásan is szabad.

Kategóriák: 
Megosztás/Mentés

Hozzászólások

Konstruktor képe

" A $_GET tömbhöz pedig hozzácsapjuk a $matches tömböt is. Amiben benne vannak az url egyes részei már megnevezve. Sajnos felesleges értékek is kerülnek bele. De azzal most nem kell foglalkoznunk."

Fent idézlek téged. Érdekelne pontosan, hogy mivel "nem kell foglalkoznunk", és mik azok a "felesleges értékek" amik belekerülnek?

ÜdvözletteL
Konstruktor

Rimelek képe

A reguláris kifejezésben van több zárójelezett alkifejezés. Ezek kapnak egy részt numerikus indexet is, más részt egy asszociatív indexet is, ami egy beszédesebb név. A numerikus indexekkel azért nem kell foglalkozni, mert az url-ben levő változók neve nem numerikus. Tehát ilyent nem csinálsz:
valami.php?1=Érték&2=másikérték
A $matches tömbben viszont lesznek ilyenek. Feleslegesek, de nem zavarnak. Ha valakit mégis zavar, ki lehet szedni azokat az elemeket egy egyszerű ciklussal, is_numeric() függvénnyel vizsgálva az indexet. És unset() -tel törölve az elemet.

Levente képe

Üdv!
Ez alapján kezdtem felújítani az egyik oldalam. Azonban valamiért a képek közül a png-t nem kezeli. Folyamatosan oldalként akarná kezelni. Érdekes módon viszont a jpg-t jól kezeli, és megjeleníti. Mi lehet ennek az oka?? Ilyet még nem tapasztaltam ugyanis...
Köszi

Rimelek képe

Helló. Nem tudom. A kiterjesztés nem számít. Az lehet még, hogy hibásan adtad meg a mintákat. Vagy pont a png-kre hibásan hivatkoztál forrásból. Igazából ez a két eset lehet, ami eszembe jut.

Levente képe

Heló.
Igen, közben megtaláltam a hibát, és sikerült is megcsinálni. Az lenne a kérdésem, hogy a tpl classba miként lehetne megoldani, hogy keywordoket és descriptiont is el lehessen tárolni? Mert ha pl public $keyword = ''; (az index.php) van a $subtitle alatt, és egyik aloldalra beírom, pl Tpl::$keyword='kulcsszó'; akkor hibaüzenetet ad ki, hogy nem tud csatlakozni a Tpl::keywords-höz. Ha új class-t írok (class keywords{ akarmi}) és ehhez csatlakoznék, akkor meg kiírja, hogy nem található ez a class
Tudsz ebben egy kicsit segíteni?
Köszi

Rimelek képe

Ha nem felejted el a static kulcsszót megadni az új tulajdonságaidnak, akkor az működik. Ha új osztályt definiálsz, akkor azt olyan helyre kell tenni, hogy azt be is töltse mindig a programod. De íme a szükséges Tpl osztály:

class Tpl
{
        public static $subtitle = '';
        public static $keywords = '';
        public static $description = '';
}

Így használhatod a $subtitle mellett a $description és $keywords tulajdonságokat. Ennél egyébként sokkal jobb sablonkezelő megoldások vannak. Csak ennek a cikknek nem ez volt a fő témája, így a lehető legkevesebbet foglalkoztam vele.

Szücs Dávid képe

hello. végig csináltam az egész tutorial-t (szerintem normálisan, nem ctrl+c/ctrl+v, ha nem mindent elolvasva mit miért és stb...) de valamiért még sem működik. linux vps-en fejlesztem. hozzá adtam a hiba keresőt is, de semmilyen hibát nem ír ki. apache2 engedélyeztettem a modul-t is. vajon mi lehet a probléma oka? :/

Szücs Dávid képe

rájöttem a dolgokra. chmod-oltam a mappát és kiderült hogy a conf mappa jogai nem voltak rendben. most egy olyan problémám akadt, hogy betölt az oldal, minden szuper, de a hivatkozások alatt a 404-fájlba került tartalom jelenítődik meg, és ha egy hivatkozásra megyek, nem jeleníti meg a hozzá tartozó tartalmat.bedob egy üres oldalt definiálatlan subtitle-vel együtt.

Rimelek képe

Helló. Két tippem van. Hogy a megoldásodban nem jók az oldalak fájljaira hivatkozások. Így nem találja meg őket és marad a default 404, ha azt látja. Vagy hogy hibás az oldalakra vonatkozó reguláris kifejezések. Látnom kéne a kódod, hogy okosabbat mondjak. De lehet, hogy magadtól is rájössz, mert látom, elég önálló vagy :) Ennek örülök