— Artikel — № 008

008 —Architecture

Front-controller routing: waarom /contact niet contact.php is

Bezoek /contact op een verouderde WordPress- of Drupal-site en contact.php is bijna nooit wat draait. Zo verwerken Apache, mod_rewrite en PHP de URL.

Messing sleutel op botkleurige kaart met inktstempel en kleirood lakzegel, op een bekraste donkere eikenhouten tafel.
Hero · gestileerd stilleven№ 008

Een ontwikkelaar staart naar een 12 jaar oude WordPress-installatie. Het contactformulier geeft een 500. Hij logt via SSH in, doet ls in de docroot en zoekt naar contact.php. Het staat er niet. wp-config.php staat er wel. index.php ook. De URL in de browser is /contact. Het bestand waar die URL naar lijkt te wijzen, bestaat niet en heeft nooit bestaan, want de site gebruikt front-controller routing.

Dit overkomt bijna elke ontwikkelaar die is grootgebracht met handgeschreven about.php en products.php. Het web waarop ze leerden, koppelde één URL aan één bestand. Het web dat ze overnemen, gebruikt front-controller routing, waarbij elke URL naar hetzelfde bestand stroomt. Het verschil kennen scheelt tussen een fix van vijf minuten en een speurtocht van vier uur door iemands verouderde site.

Eén bestand per URL was het oude contract

In een klassieke PHP 3- of PHP 4-site was de taak van de webserver simpel: pak het pad uit de URL, vind het bestand, draai het. Een request naar /about.php draaide /about.php. Een request naar /products/list.php draaide /products/list.php. Het bestandssysteem en de URL-ruimte hadden dezelfde vorm. Je voegde een pagina toe door een bestand toe te voegen. Je hernoemde een pagina door een bestand te hernoemen, en elke link naar de oude naam was kapot.

WordPress introduceerde pretty permalinks in 2.0, ergens in 2005. Drupal had vanaf versie 4 clean URLs. Symfony, CodeIgniter, Laravel en elk PHP-framework van na pakweg 2008 koos hetzelfde patroon: stuur elk request naar één index.php en laat PHP beslissen wat er gebeurt. Dat patroon heet de front controller en is al lang de standaard.

De .htaccess die stilletjes alles herschrijft

Open de .htaccess in de root van een willekeurige WordPress-installatie van na 2007 en je vindt dit blok:

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

Lees het langzaam. RewriteEngine On zet mod_rewrite aan voor deze directory. RewriteRule ^index\.php$ - [L] zegt: als het request letterlijk naar index.php gaat, doe niets en stop. De twee RewriteCond-regels zeggen: pas de volgende regel alleen toe als de gevraagde bestandsnaam geen echt bestand is (!-f) en geen echte directory (!-d). De laatste regel herschrijft alles wat door die controles komt naar /index.php. Dat is het front-controller-patroon op Apache-niveau.

Dus als de browser /contact opvraagt, stelt Apache twee vragen aan het bestandssysteem. Bestaat er een bestand op /contact? Nee. Een directory? Nee. Het herschrijft het request intern naar /index.php. De adresbalk laat nog steeds /contact zien. Het proces dat draait is index.php, en alleen index.php.

Drupal 7 heeft een variant van één regel: RewriteRule ^ index.php [L]. Laravel levert een public/index.php en een vrijwel identieke rewrite. Magento 2 routeert via pub/index.php. De cosmetica verschilt. Het contract niet.

Wanneer de rewrite nooit draait

Als mod_rewrite ontbreekt, of als de AllowOverride van de docroot in de Apache-config op None staat, wordt het .htaccess-blok stilzwijgend genegeerd. Apache probeert dan /contact te serveren als letterlijk bestand, faalt, en geeft een 404 terug zonder dat er ook maar één regel PHP draait. Dit is de meest voorkomende reden dat een vers gemigreerde WordPress-site wel de homepage laadt, maar op elke andere URL een 404 geeft. De fix zit op serverniveau: zet mod_rewrite aan, of zet AllowOverride All op de docroot. Alleen .htaccess aanpassen lost niets op, want .htaccess is juist wat genegeerd wordt.

Index.php is het enige entry point

Zodra Apache /contact doorgeeft, behandelt PHP de URL als data. Hij staat in $_SERVER['REQUEST_URI']. Het bestandssysteem heeft niets meer te zeggen over welke pagina rendert.

In WordPress bestaat index.php uit zes regels die wp-blog-header.php opstarten, die wp-load.php laadt, die op zijn beurt wp-config.php en de rest van de core inlaadt. WordPress leest dan REQUEST_URI, haalt de rewrite_rules-rij uit wp_options, unserialiseert die, en matcht de URL tegen een array reguliere expressies. Voor een Pagina met slug contact resolvet de match naar ?page_id=42 of iets vergelijkbaars, de globale $wp_query wordt gevuld, en de template hierarchy kiest een bestand uit het actieve thema: page-contact.php, daarna page.php, daarna singular.php, en index.php als laatste redmiddel.

Elke front-controller PHP-app doet hetzelfde soort ding. Drupal gebruikt een router-service. Symfony leest attributen of YAML-route-bestanden. Laravel leest routes/web.php. Geen van allen raakt het bestandssysteem aan om de handler te vinden. Daarom levert grep -r "function contact" . op een Drupal-site om de contactpagina-handler te vinden óf niets op, óf honderd false positives.

Een debug-volgorde die echt werkt

Als een route op een site die je niet zelf hebt gebouwd niet meer werkt, loop deze stappen op volgorde af. De meeste front-controller routing-problemen zitten in een van de eerste vier.

  1. Bevestig dat mod_rewrite leeft. curl -I https://example.com/contact en lees de response. Is de 404-pagina de gebrande 404 van het framework, dan draaide de rewrite wel en heeft PHP de URL afgewezen. Is het een kale Apache- of Nginx-404, dan draaide de rewrite nooit.
  2. Lees de .htaccess die op dit moment op schijf staat. Sommige migraties laten hem vallen. WordPress regenereert hem als je Instellingen → Permalinks opslaat, maar alleen als de webgebruiker schrijfrechten heeft op het bestand.
  3. Controleer de routing-data, niet het bestandssysteem. Op WordPress geeft SELECT option_value FROM wp_options WHERE option_name = 'rewrite_rules' een geserialiseerde PHP-array terug. Unserialiseer die en zoek je URL. Staat hij er niet in, dan matcht geen enkel template hem ooit.
  4. Zoek naar echte bestanden die de route overschaduwen. find . -maxdepth 2 -name "*.php" in de docroot brengt elke vergeten contact.php, shop.php of old.php aan het licht die !-f vrolijk uitserveert in plaats van de router.
  5. Zijn die vier schoon, zet dan WP_DEBUG aan in wp-config.php en kijk naar wp-content/debug.log. De PHP-fout staat er meestal al na het eerste request in.

Wat je vandaag kunt veranderen

Onderhoud je een front-controller-site die je niet zelf hebt gebouwd, neem dan vandaag vijf minuten om de .htaccess te openen, het rewrite-blok hardop te lezen en de bijbehorende index.php te vinden. Zodra je weet dat die twee bestanden bestaan en wat ze doen, begint elk "de contactpagina is stuk"-ticket op de juiste plek in plaats van in een zoektocht naar een bestand dat er nooit was.

Toen we Pier bouwden, liepen we hier op vrijwel elke site die we koppelden tegenaan. Wat we uiteindelijk hebben gedaan, is .htaccess en index.php als eersteklas bestanden in de boom tonen, een version history van allebei bijhouden zodat een slordige bewerking aan een rewrite-blok in één klik terug te draaien is, en de ingebouwde MySQL editor laten lezen wat in de rewrite_rules-optie staat zonder een tweede phpMyAdmin-tabblad te openen.

— Vragen —

Als /contact niet contact.php is, welk bestand draait er dan?

index.php in de docroot. Apache herschrijft het request daarheen via mod_rewrite, en WordPress, Drupal, Laravel of Symfony zoekt de URL vervolgens op in zijn router en kiest een handler.

Waarom laadt mijn pas gemigreerde WordPress-site alleen de homepage?

De homepage is de enige URL die naar een echte directory wijst, dus daar is geen rewrite voor nodig. Elke andere URL heeft mod_rewrite en een werkende .htaccess nodig. Een van die twee ontbreekt meestal na een migratie.

Kan een vergeten contact.php nog steeds geserveerd worden vanaf een moderne WordPress-site?

Ja. De RewriteCond !-f-check slaat de rewrite over zodra er een echt bestand op die URL staat. Elk weesbestand uit een oudere site blijft uitgeserveerd worden, vaak met verouderde inhoud of kapotte includes.