login  Naam:   Wachtwoord: 
Registreer je!
 Forum

url rewriting met behulp van een custom 404 pagina in plaats van mod_rewrite

Offline Thomas - 21/12/2013 12:46 (laatste wijziging 21/12/2013 12:57)
Avatar van ThomasModerator Dit topic is "ter leeringh ende vermaeck" maar ook een soort van oproep om hier je eigen gedachten over te geven. Ook zal ik op den duur een download-linkje voor het mini-framework erbij stoppen, zodat je hier zelf eens mee kunt experimenteren. Laat weten wat je er van vindt.

Verderop vind je meer over het herschrijven van URLs via een custom error page (404), dus zelfs als je dit mini-framework niet wilt proberen kan dit leerzaam zijn.

Naar aanleiding van dit topic: http://www.site...g_maar_hoe (lol [url=] werkt niet) ben ik nog wat verder gaan kijken hoe makkelijk het is om een mini-frameworkje op te zetten.

Het voornaamste doel daarvan was dat je in staat moet zijn zo snel mogelijk aan het coden kunt slaan (lees: gestructureerd pagina's kunt gaan bouwen) zonder je te bekommeren over allerlei technische aangelegenheden. Daarvoor moeten natuurlijk wel een aantal zaken geregeld worden, een minimale hoeveelheid (programmeer)werk is vereist. Mijn tweede streven was eigenlijk om het framework supersimpel te houden maar met alle verleidingen begeef je je al snel op een hellend vlak (je wilt maintemplates, sessiemanagement, een database-laag etc. etc.). Ik denk dat ik hierin (tot nu toe) redelijk geslaagd ben. Bij het bouwen heb ik de naamgeving van bestanden en folderindeling haast 1 op 1 gekopieerd van Zend Framework omdat ik deze eigenlijk wel okay vond.

Op een bepaald punt was ik zover dat ik super eenvoudig pagina's kon gaan bouwen (classes afgeleid van de PageType class zoals beschreven in het topic hierboven). Door het simpelweg aanmaken van een class Test in de map application/test.php kan ik deze in mijn applicatie benaderen via index.php?page=test. Daarnaast mag deze class protected action-methodes bevatten, bijvoorbeeld actionSecretOperation, die ik vervolgens kan uitvoeren (via de exec-methode van de PageType class) door het aanroepen van index.php?page=test&action=secretOperation.

Tevens heb ik een voorziening ingebouwd zodat als je de volgende structuur hebt:
application/test.php (class Test)
application/test/test.php (class TestTest)
application/test/test/test.php (class TestTestTest)
Je deze pagina's respectievelijk aan kunt roepen als:
index.php?page=test
index.php?page=test/test
index.php?page=test/test/test
Er wordt dus onderscheid gemaakt tussen folders en bestanden (waarbij voorrang aan bestanden wordt gegeven boven folders), daarnaast kun je ook enkel de naam van een folder opgeven, mits deze folder een bestand met dezelfde naam (en bijbehorende class) bevat. Als je dus zoiets hebt:
application/test/test.php (class TestTest, zoals het hoort)
En verder geen subfolders met de naam "test", dan kun je deze pagina zowel aanspreken via:
index.php?page=test/test, maar ook via index.php?page=test
Als er geen bestand wordt gevonden, wordt er gekeken of er wel zo'n folder bestaat met hierin een bestand met dezelfde naam van de folder.

Ik heb hier nog maar heel kort mee gewerkt, maar het is zo eenvoudig om hiermee snel gestructureerd pagina's te bouwen . Het werkt enorm intuitief.

Anyway, waar het eigenlijk om gaat: nu heb ik dus allemaal pagina's van de vorm: index.php?page=test/yolo&action=doSomething&swagId=12, maar die URLs zien er dus niet erg (zoekmachine)vriendelijk uit. Dus kwam er weer een puntje bij: de mogelijkheid om deze URL's op een (zoekmachine)vriendelijke manier weer te geven.

Maar ik gruwde van het idee om wéér met mod_rewrite aan te slag te gaan om een aantal redenen:
- je bent daarmee gebonden aan Apache (mod_rewrite is hier onderdeel van)
- reguliere expressies...
- maar het meeste wat mij nog tegenstond was dat je zit te prutten in een .htaccess-bestand waarin je telkens opnieuw RewriteRules moet definiëren waarbij je continu na moet denken over alle verschillende verschijningsvormen en de volgorde waarin je deze moet zetten; daarbij stop je routing-logica in een bestand :/
- ook weet ik niet hoe efficiënt mod_rewrite is/blijft als je hier een heeeeeleboel RewriteRules met reguliere expressies in stopt?

Tijd voor iets anders dus!

In plaats van al die meuk kun je ook een custom 404 pagina gebruiken. Het idee is super simpel (<-- dat is natuurlijk mild gedramatiseerd haha, de uitvoering is iets lastiger) en is in 2 stappen gerealiseerd:

Stap 1 uit 2:
Definieer een custom error page. En in Apache (jaja, ik weet het) doe je dat met een .htaccess bestand (maar in andere webservers zou dit idee dus OOK moeten werken). Zet een .htaccess in de root-folder van je website met de volgende inhoud:
  1. ErrorDocument 404 /index.php


En anders, als je deze in de folder /whatever zet:
  1. ErrorDocument 404 /whatever/index.php


Stap 2 uit 2:
Maak een index.php-bestand aan op die plaats. Dit bestand gebruik je vervolgens om alle verzoeken af te handelen... maar dat deed je al (in ieder geval in mijn bovenstaande frameworkje: deze heeft één ingang (one entry point), namelijk index.php!)

That's. It.

Nou ja, bijna.

index.php moet dus nog steeds de afhandeling van verzoeken verrichten.

Maar je kunt dus nu allerlei pagina's aanroepen op je site:
http://mijnsite.com/test
http://mijnsite.com/test/blaat
http://mijnsite.com/1/2/3/4/5?test=1&bla=4

of als je dit systeempje in een subfolder whatever draait (wat ook kan):
http://mijnsite.com/whatever/test
http://mijnsite.com/whatever/test/blaat
http://mijnsite.com/whatever/1/2/3/4/5?test=1&bla=4

Alles komt uit op index.php (of whatever/index.php).

Hoe verricht je de afhandeling?
In geval van Apache: lees $_SERVER['REQUEST_URI'] uit (dit is mogelijk een andere $_SERVER variabele op een andere webserver, maar dat is eenvoudig aangepast). Bijvoorbeeld als volgt:

  1. <?php
  2. $urlComponents = @parse_url($_SERVER['REQUEST_URI']);
  3. // $urlComponents['path'] bevat nu het pad, en (optioneel)
  4. // $urlComponents['query'] bevat de querystring
  5. // $_GET heeft overigens dezelfde waarden, maar dan in array-vorm
  6. $query = array();
  7. if (isset($urlComponents['query'])) {
  8. $query = parse_str($urlComponents['query']);
  9. }
  10. ?>


In mijn frameworkje werkt deze 404 rewriting op de volgende manier:
Een aanroep van /admin/user?action=login wordt vertaald naar /index.php?page=admin/user&action=login

Maar ik wilde meer! Ik wilde volledige vrijheid voor het geven van een (unieke) naam aan een pagina, dus een soort van aliasing. Ook dat is mogelijk, maar je moet dan zelf een "mapping" maken van je REQUEST_URI naar de bijbehorende "$urlComponents" set (path+query). Ook dat is mogelijk in mijn frameworkje, zo kun je de volgende pagina aanroepen:
http://mijnsite.com/super-fancy-url
wat equivalent is aan het aanroepen van:
http://mijnsite.com/test/fancy?a=1&b=2
en ook:
http://mijnsite...=1&b=2
Het is dus mogelijk om querystring-variabelen vantevoren in te stellen. Deze worden automatisch toegevoegd aan $_GET en krijgen een "readonly" status.
Roep je dus bijvoorbeeld deze alias aan met:
http://mijnsite...2&c=13
dan bevat $_GET dus:
  1. 'a' => 1, // want: read-only via alias-definitie
  2. 'b' => 2, // want: read-only via alias-definitie
  3. 'c' => 13, // want: is vrij in te stellen
  4. );


De mogelijkheden voor het definiëren van deze aliasen zijn enorm, zo zou je deze ook in kunnen zetten als je een soort van wildcard wilt gebruiken, bijvoorbeeld: alles van de vorm admin/test* volgt zijn COMPLEET EIGEN ROUTING. Je lost dit dan lokaal in je AdminTest class op door daar je eigen implementatie te doen van de exec()-routine. Je kunt je routing dus helemaal in je code sturen, en bent niet afhankelijk van één complex bestand (.htaccess met RewriteRules).

Er zijn wel enkele dingen (ten minste twee) waar je rekening mee moet houden als je met 404 redirects werkt:

valkuil #1
Als je met 404 redirects werkt, krijg je HTTP-header de statuscode 404 (pagina niet gevonden). Je moet er dus voor zorgen, dat als de "berekening" van een gezochte pagina in je index.php-bestand een "bestaande" pagina oplevert, je deze header weer TERUGverandert naar statuscode 200 (pagina gevonden), dit doe je als volgt:
  1. <?php
  2. // als je pagina gevonden is
  3. header('HTTP/1.0 200 OK');
  4. ?>

Uiteraard gelden hier ook de regels die op andere headers van kracht zijn: dit moet gebeuren VOORDAT je enige andere output hebt verstuurd, tenzij je output buffering gebruikt.

valkuil #2
Het POSTen van data werkt niet op deze manier (geloof ik? heb dit verder eigenlijk helemaal niet getest). Je kunt dus niet POSTen naar zo'n zoekmachinevriendelijke URL want door de interne redirect (wat er in feite dus plaatsvindt bij de aanroep van zo'n URL) gaat je POST-data verloren. formulieren die je via method="post" verzend zullen dus via de ouderwetse aanroep index.php?page=...&action=... moeten gaan.

Hier heb ik in mijn mini-framework ook in voorzien: classes afgeleid van PageType hebben een link()-methode die je aan kunt roepen om makkelijk links mee te bouwen. Daarnaast heb ik ook een stukje configuratie waarmee je 404 redirects aan en uit kunt zetten. Als je al je (interne) links bouwt met $this->link() dan blijven deze altijd werken, of 404 rewriting nu aan staat of niet.

Suggesties? Tips? Vragen? Let me know! ik ga ondertussen verder met bouwen in mijn mini-frameworkje.

4 antwoorden

Gesponsorde links
Offline chth - 23/12/2013 10:24
Avatar van chth Lid Wel, ik ben benieuwd naar het download-linkje ;)
Offline Thomas - 23/12/2013 13:25 (laatste wijziging 23/12/2013 13:29)
Avatar van Thomas Moderator Ik ben bijna zover dat ik tevreden ben met het initiele systeem, maar ik ben nog bezig met het opschonen van code en het toevoegen van commentaar. Ik ben nog niet eens toegekomen aan het bouwen van pagina's (behalve eenvoudige tests) haha. Maar het begint best vet te worden vind ik zelf .

EDIT: Daarnaast zal ik wat uitleg moeten geven over:
- installatie
- configuratie
- het bouwen van eenvoudige/complexe pagina's

en zover ben ik nog niet 
Offline Koen - 23/12/2013 13:40
Avatar van Koen PHP expert Hoi FangorN,

Heel interessante denkwijze. Ik zie een aantal puntjes terugkomen waaraan je merkt dat je er goed over nagedacht hebt.

Voor andere puntjes heb ik het idee dat je een beetje in het duister tast. Ik weet niet of je de evolutie van PHP de laatste jaren gevolgd hebt, want bij bepaalde stukjes uit je onderzoek gaan mijn wenkbrauwen lichtjes gefronst staan.

Het idee van het de single entry point op de index.php is alvast een slimme aanpak die bij de meeste frameworks de laatste jaren terugkeert. De manier waarop snap ik toch niet helemaal. Wat jij doet, is de manier van het afhandelen van onbestaande bestanden volledig misbruiken. Door deze manier van werken, krijg je te kampen met een aantal problemen uit onverwachte hoek, zoals inderdaad de 404 status code en het gebrek aan post-data.

Ik was benieuwd toen je zei dat je 404 statuscodes gewoon kon overschrijven door deze via PHP te veranderen naar 200, maar blijkbaar wordt dat toch niet overal ondersteund. Ik heb het getest en de pagina komt nog steeds als 404 door: http://dl.dropb...135156.png (combinatie van Chrome & Apache).

Je kan dit allemaal veel gemakkelijker oplossen door volgende regels code in je .htaccess file te plaatsen:

  1. <IfModule mod_rewrite.c>
  2. RewriteEngine on
  3. Options FollowSymLinks
  4. RewriteCond %{REQUEST_FILENAME} !-d
  5. RewriteCond %{REQUEST_FILENAME} !-f
  6. RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]
  7. </IfModule>


Hierbij check je eerst of de gevraagde pagina geen bestaand bestand (-f) of map (-d) is, en als dat niet het geval is, dan stuur je alles door naar de index.php, en steek je het originele pad in een query variabele 'q' bijvoorbeeld. De originele querystring wordt ook nog eens toegevoegd aan het nieuwe pad, dit gebeurt dankzij de [QSA] (QueryString Attached) flag.

Eén van de meest gebruikte vormen van zulke frameworks is MVC. Daar heb je vast al van gehoord. Voor de leken onder ons: kort gezegd is MVC een architectuur waarin presentatie, bussinnesslogica en persistentie worden gescheiden in verschillende lagen:

* het Model: bussinnesslogica & persistentielaag
* de View: de templates voor het weergeven van je pagina
* de Controllers: input omvormen naar output

In deze architectuur spreek je de controllers aan. Zij verwerken de gegevens aan de hand van het model en presenteren ze dan aan de eindgebruiker in webpaginaformaat via de templates uit de view.

In theorie zijn deze lagen volledig losstaand en is het bijvoorbeeld perfect mogelijk om de view te vervangen met een commandline interface, zonder één letter code te moeten aanpassen in je model & controllers.

Om terug to the point te komen: om deze controllerklassen aan te spreken, moet je natuurlijk het pad "/User/edit/5" kunnen omvormen naar UserController::edit(5);. Bij de hedendaagse frameworks heet zoiets een router of een dispatcher. Deze kan net zo ingewikkeld of simpel zijn als je zelf wilt. Je kan bijvoorbeeld zelfs zover gaan als het toelaten van custom routes zoals: "/User/show/:username/:id", waarin username & id dan als variabelen worden doorgegeven. Een simpele versie zou er zo kunnen uitzien:

  1. <?php
  2. $currentRoute = explode('/', $_GET['q']); // 0 = User, 1 = edit, 2 = 5
  3. $controllerName = $currentRoute[0];
  4. $action = $currentRoute[1];
  5. $controller = new $name();
  6. call_user_func_array(array($controller, $action), array_slice($route, 2)); // roep de juiste method aan en geef de rest van de array mee als parameter


Uiteraard ontbreken hier een aantal checks en default values, voor de leesbaarheid weggelaten.

Dan heb je inderdaad ook nog een manier nodig om deze PHP classes te kunnen includen (alle klassen simpelweg in je index.php includen is niet bepaald performant te noemen). Naamgevingen à la TestTestTest voor Test/Test/Test.php is leuk, maar niet echt onderhoudbaar. Zend Framework gebruikte klassen en functies onder de vorm van Test_Test_Test, waarbij de underscores dan vervangen konden worden door een directory separator en klaar is kees. Tegenwoordig is het slimmer om te werken met Namespaces. Zodoende heb je geen last meer van niet-unieke namen voor klassen en dergelijke. Bepaalde klassen in Zend Framework zaten bijvoorbeeld aan namen zoals "Zend_Service_DeveloperGarden_Response_ConferenceCall_CreateConferenceResponseType". Dit is niet bepaald overzichtelijk.

Aan de hand van die namespaces kan je dan weer wel heel gemakkelijk je mappenstructuur hier naar aanpassen en on the fly het juiste bestand includen. Dit doe je mbv een autoloader.

Verder werk je best met een globaal registry voor globale klassen enzo in op te slaan over heel de applicatie. Tegenwoordig wordt dit vaak gebruikt in de plaats van het singleton pattern. Verder heb je ook nog een template klasse die de juiste variabelen aanmaakt en de juiste templatebestanden aanroept (ook hier kan je dan weer probleemloos switchen tussen simpele PHP files als templates of template engines zoals Smarty en Twig.

Als je echt like a boss wil werken, gebruik je een dependency injection container voor het doorgeven van bepaalde klassen om zo tot een losse koppeling van je klassen te komen.

Succes. Je eigen frameworkje schrijven vergt veel denkwerk en je leert er echt beter door nadenken over de manier waarop je programmeert. 

PS: Prettige feestdagen alvast aan iedereen =)

Koen
Bedankt door: Thomas, marten, Pieter
Offline Thomas - 24/12/2013 10:46 (laatste wijziging 27/12/2013 22:39)
Avatar van Thomas Moderator Bedankt voor de uitgebreide reactie Koen .

Voordat ik hier weer een uitgebreide reactie op geef moet ik misschien e.e.a. verder verduidelijken over wat ik probeer te bereiken met het framework:
- onafhankelijkheid van webplatform
het moet in principe overal werken, ik wil dus geen afhankelijkheid van modules zoals bijvoorbeeld mod_rewrite; de enige "voorziening" die de webserver moet hebben (als je "fancy URL's" wilt gebruiken) is de mogelijkheid van het aanwijzen van een errorpage (de index.php van het framework)

- simpel, intuitief en flexibel
ik wil een lage "instapdrempel", je hoeft geen universitaire studie gevolgd te hebben om zelf (snel) functionaliteit te bouwen; daarom probeer ik het zo te maken dat de code die je zelf moet schrijven zo dicht mogelijk bij native (objectgeorienteerde) PHP staat. In het framework zijn een aantal (ontwerp)beslissingen gemaakt die ervoor zorgen dat als je je code op een bepaalde plek zet deze gewoon meteen zijn ding doet. Dus als je snel pagina's wilt bouwen kies je deze standaard aanpak. Dat zorgt er (dus) voor dat je in de standaard opzet geen enkele overhead hebt voor het bouwen van een eenvoudige pagina. Wil je complexere pagina's (bijvoorbeeld met een maintemplate), kan dit ook. Wil je een aangepaste naamgeving voor de aanroep van deze pagina's, kan dit ook. Je kunt enorm schuiven met hoe makkelijk je dingen wilt opzetten of hoe geavanceerd je pagina's moeten zijn.

- meteen+snel bouwen
na het uitpakken van een codezip (oid) en configuratie van het framework (wederom, als je de default directory structuur aanhoudt hoef je enkel een database in te stellen, en als je die niet gebruikt hoef je helemaal niets in te stellen!) kun je meteen een pagina in elkaar fietsen; toen ik gisteren eindelijk mijn code had opgeschoond had ik in ongeveer een half uur een compleet (zij het eenvoudig) artikelsysteempje in elkaar gezet die (met een beetje fantasie) door kan voor een eenvoudige blog.

Kortom, ik wil een simpel framework (simpel in ALLES) die mij zo min mogelijk belemmert om zeer snel functionaliteit te bouwen.

Dan je reacties:
Koen schreef:
Wat jij doet, is de manier van het afhandelen van onbestaande bestanden volledig misbruiken.

Als je een analogie met computerspellen maakt: Jij noemt dit "cheating", ik noem dit "clever use of game mechanics" . Ik zie het probleem niet helemaal. Het lijkt mij een interne aangelegenheid van de webserver om uit te vogelen welke pagina geserveerd moet worden (en met welke headers). In een standaard ontwerp "weet" de webserver door code direct wat geserveerd moet worden. In deze opzet weet de webserver het standaard niet, en stuurt het verzoek door aan een pagina die het wel weet. Misschien leg ik het principe niet goed uit maar als je het zo bekijkt gebeuren hier toch niet echt dingen die het daglicht niet kunnen verdragen? Net zoals DNS (als deze vergelijking opgaat?) doe ik hier aan "page resolving" (ik leg het vraagstuk voor aan een pagina die waarschijnlijk wel weet welke pagina ik zoek).

Ook herschrijf ik $_GET (now hear me out lol). $_GET is niets meer dan een array die toevalling standaard waarden heeft die corresponderen met je querystring, maar er is NIETS wat mij ervan weerhoudt om deze te manipuleren. En ik manipuleer deze op een manier die (met inachtneming van het framework) volledig doorzichtig is en intuitief werkt. Je moet alleen weten hoe het framework hier (mogelijk) mee omspringt maar daarna kun je $_GET precies hetzelfde gebruiken als voorheen (werkt zoals in native PHP)! En ja, als je speciale dingen wilt doen met het framework, dan kan dit van invloed zijn op de inhoud van $_GET, maar dit is -wederom- volledig transparant.

Koen schreef:
Door deze manier van werken, krijg je te kampen met een aantal problemen uit onverwachte hoek, zoals inderdaad de 404 status code en het gebrek aan post-data.

Waar ik ook weer (elegante) oplossingen voor heb (headers worden herschreven en ik gebruik overal een link()-functie die dit probleem afvangt).

Koen schreef:
...maar blijkbaar wordt dat toch niet overal ondersteund. Ik heb het getest en de pagina komt nog steeds als 404 door: ...

Dat snap ik niet. Je moet headers kunnen vervangen. De meeste aanroepen zullen dus initieel de statuscode 404 hebben, maar deze herschrijf ik -mits ik een pagina vind- zonder problemen naar 200? Hoe ziet jouw code eruit die leidt tot de 404 in je screenshot?

Over .htaccess, MVC, Smarty (bleurgh) en Twig (oh don't get me started on template engines rofl): dat wil ik allemaal niet, ik wil simpel lol. Rechttoe-rechtaan PHP.

Nu klink ik misschien wat verwaand omdat ik nog niets heb laten zien, maar het werkt al best goed al is het dus nog wat vroeg om maar een download-link de wereld in te slingeren... Ik wil eerst nog wat dingen afronden en documentatie schrijven.

EDIT: okay voorbeeld, dit is alle code die ik nodig heb voor mijn artikel-functionaliteit. Hierbij is $this->mt een referentie naar mijn mainteplate en $this->db een referentie naar mijn database object (zit in een abstraction layer, heeft nu alleen nog een implementatie voor MySQLi).

EDIT: zie het artikel met hierin de code 

Zoals je wellicht kunt zien, heeft bijna alle code die je schrijft "effect", er is haast geen overhead. Je hoeft niets extra's te regelen, enkel precies die dingen die jij wilt doen.

EDIT #2: WERKEND VOORBEELD
Geen 404 headers hier hoor!
Gesponsorde links
Je moet ingelogd zijn om een reactie te kunnen posten.
Actieve forumberichten
© 2002-2024 Sitemasters.be - Regels - Laadtijd: 0.291s