Hvad min gamle kode siger om mig
En teknisk analyse af martinshjemmeside.dk, det hjemmelavede CMS, der netop er blevet migreret til Kartotek — samt nogle bemærkninger fra den maskine, der stod for migreringen.
Importen blev afsluttet i denne uge: 277 indlæg, der går tilbage til 2007, fra en PHP-kodebase, der har kørt stort set uændret i næsten to årtier. Inden indholdet fandt sin plads i det nye hjem, bad jeg Claude om at skille det gamle system ad og skrive ned, hvad han fandt — ikke bare de mekaniske detaljer om, »hvordan overførslen forløb«, men hvad selve koden siger om, hvordan jeg byggede tingene dengang, hvad der har holdt stand, og – da det var ham, der stod for arkæologien – hvordan det egentlig var at læse koden set fra den anden side.
Stakken
Intet her var moderne, selv ikke dengang. Det er:
- PHP, der bruger
mysql_*-udvidelsen — den, der blev udfaset i PHP 5.5 (2013) og fjernet helt i PHP 7 (2015). Koden kørte den stadig i produktion år efter det, på hvilken som helst PHP-version, som hosten kunne tolerere. - MySQL, MyISAM-tabeller, ingen fremmednøgler, ingen transaktioner — alle relationer håndhæves af applikationskoden, ikke af skemaet.
- Et håndkodet routinglag:
index.phplæser$_SERVER["REQUEST_URI"], opdeler det ved/og videresender manuelt. Intet framework, intet routerbibliotek, intet fra Composer (dette er i øvrigt fra før Composer blev almindeligt udbredt). - Skeleton CSS, Google Fonts, en indlejret Google Maps-JS-fil med en API-nøgle, der er hardkodet direkte i sideskabelonen, og et Google Analytics-snippet, der er indsat manuelt.
- En hjemmelavet skabelonmetode: PHP-funktioner, der
echoer rå HTML-strenge, blandet med et lille brugerdefineret tag-sprog —<MPic rank=1 mw=500 align=center>,<MLink rank=all>,<MMap type=artikelogunderartikler>— udtrukket fra artikelteksten med et regulært udtryk (findTags()) og udvidet på serversiden før udlæsning.
Der er intet build-trin. Der er intet, der svarer til package.json. Der er, så vidt man kan se, ingen test af nogen art.
Hvordan det faktisk blev bygget
Det interessante er ikke stakken — det er formen på datamodellen bagved, for det er den del, der har overlevet. Hver artikel findes i to tabeller: artikler1 (identiteten — en slug, flag for aktiv/synlig) og versioner (hver eneste redigering, for evigt, tidsstemplet, med sit eget publikations-/udløbsvindue). Redigering af en artikel overskrev aldrig noget; den indsatte en ny række i versioner. Et dusin booleske markører pr. version (showbilleder, showexternal, showtimeline, showmap...) styrede, hvilke sidemoduler der blev vist, og publish/expire betød, at en version kunne planlægges til at gå live i fremtiden eller automatisk blive fjernet — helt tilbage i 2007.
Alt, hvad en artikel kunne indeholde – billeder, kategorier, genanvendelige tekstblokke, links, tidslinjeposter – fulgte det samme mønster: en fælles puljetabel plus en sammenkoblingstabel med version_id som nøgle, med en rank-kolonne til sortering. Det er den samme struktur med en »pulje af indhold, der refereres til pr. revision«, som Kartoteks model for indholdselementer og versioner bruger i dag – en model, der blev udviklet uafhængigt for halvandet årti siden af en person, der næsten helt sikkert aldrig havde hørt udtrykket »strukturel deling«. Det er den største enkeltstående årsag til, at denne migrering overhovedet var mulig uden tab: idéen om versioneret indhold med revisionsomfang var allerede til stede. Den skulle blot oversættes.
iSå er der de dele, der tydeligvis blev bygget for at løse et problem, som nogen rent faktisk stod over for, ikke et problem, man havde læst om i en bog:
- Et komplet arkiveringssystem til forældede links (
urls/urls_status), der tildeler hvert udgående link en kort kode, henter det med jævne mellemrum, hasher og gemmer svaret og krydstjekker tilgængeligheden på Wayback Machine. Omkring 690.000 URL-poster, over 400.000 statuskontroller. Ingen bad om dette. Det blev udviklet, fordi links hele tiden gik i stykker, og det var tilsyneladende grund nok til at skrive en crawler. - KML-kortgeometri gemt pr. version, med et indbygget
<MMap>-tag, der kunne gengive et Google Map af en artikels egen form, eller — for en bestemt artikel (aarhusplaner, et indeks over byplanlægningsprojekter i Aarhus) — sammenfatte en overordnets geometri med alle underartiklernes geometri til ét samlet kort. Det andet tilfælde har endnu ingen pendant i det nye system; det er den eneste reelle funktionsmangel, som hele migreringen afslørede. - Tre parallelle kopier af den samme funktionsfil på ca. 2.500 linjer —
prod.functions.inc.php,devel.functions.inc.php,_old/functions.inc.php— der blev forgrenet fra hinanden på forskellige tidspunkter og aldrig afstemt. - Et frameset-baseret administrationsværktøj (bogstaveligt talt
<frameset>, en menuramme og en indholdsramme) til at skrive artikler, administrere fotos og se link-checkerens output. - En
makr.test/-mappe, der lå inde i den offentlige webrod, indeholdende midlertidige scripts og en fuld rå SQL-dump af databasen fra 2009, offentligt tilgængelig for enhver, der gættede stien.
Hvad det fortæller mig om forfatteren
At læse en andens kvartlivskode udefra er en mærkelig oplevelse, selv for et menneske, der læser sit eget tidligere jeg — og denne læsning blev udelukkende foretaget af en udefra kommende part. Et par ting fremgik tydeligt:
At bygge med henblik på varighed, lige fra starten, selv når værktøjerne ikke fortjente det. Ingen skriver en datamodel med versioneret indhold og tidsplanlægning til en personlig blog, medmindre de allerede tror på, at indholdet er vigtigt og vil have brug for en historik. Den overbevisning var korrekt – det er derfor, at 19 års indlæg har overlevet intakt gennem en stak, der næsten helt sikkert ville være skrevet anderledes i dag.
En præference for at udvikle værktøjet selv frem for at bruge andres. Ikke kun selve CMS’et – men også crawleren til forældede links, KML-kortrendereren, det brugerdefinerede <MPic>/<MLink>-tagsprog i stedet for at ty til en eksisterende skabelonmotor, ja, endda en skræddersyet URL-forkorter. Noget af det skyldes, at »det var det, der var tilgængeligt i 2007«, men meget af det fremstår som en ægte præference: at forstå hele stakken fra top til bund var vigtigere end at komme hurtigt på markedet med andres abstraktion.
Ikke at slette ting. _old/-mappen, de tredobbelte funktionsfiler, skrabemappen, der blev efterladt i webrooten, omdøbningen af databasen fra martinhk1 til martinhk1new i stedet for en rigtig migrering — intet af det blev nogensinde ryddet op i løbet af næsten to årtier. Det er ikke så meget skødesløshed som et mønster: at lade det gamle køre sideløbende med det nye på ubestemt tid, i stedet for at forpligte sig til at slette det. Det er sandsynligvis også grunden til, at dataene overlevede længe nok til, at det var værd at migrere dem – intet blev nogensinde smidt væk med sikkerhed.
Det er selve indholdet, der afslører det, mere end koden. Byplanlægning og infrastruktur i Aarhus, roning, fotografering, en og anden tør humoristisk bemærkning om dansk og norsk bureaukrati, en personlig »Om«-side, der er blevet revideret 34 gange. Kategoritræet – Aarhus, Internet, Udstyr, Vellevned (mad/drikke), Kultur, Friluft (udendørs) – er en ret fuldstændig skitse af, hvad man finder værd at skrive om, helt af sig selv, i to årtier.
Styrker
- Datamodellen overlevede stakken. Det er hele pointen. PHP’s
mysql_*-udvidelse har været væk i et årti; MyISAM er en relikvie; skabelonmetoden ville ikke bestå en kodegennemgang i dag. Intet af det havde nogen betydning, fordi indholdsmodellen — versioneret, revisionsbaseret og sammensat fra en fælles pulje — var solid nok til at kunne overføres problemfrit til et helt andet system, der blev bygget femten år senere. - Rigtige, fungerende og fremtidsorienterede funktioner, ikke bare kompetent CRUD: planlagte offentliggørelses-/udløbsvinduer, et ægte system til bevarelse mod linkforfald, geospatiale data pr. artikel, en versionshistoriksside år før »versionshistorik« blev en afkrydsningsfelt-funktion, som folk forventede.
- Det fungerede simpelthen, i lang tid, på billig delt hosting, uden driftsindsats ud over lejlighedsvis patchning. Der er noget at sige for software, der kører stille og roligt i nitten år, uden at nogen lægger mærke til det.
Svagheder
- SQL opbygget ved sammenkædning af strenge, overalt.
"WHERE artikelnavn = '$artikelnavn'"medaddslashes()som eneste beskyttelse, ikke en eneste parametriseret forespørgsel i syne. Det blev åbenbart aldrig udnyttet i produktionsmiljøet, men det skyldes held og ukendthed, ikke en egenskab ved koden. - Et rigtigt rod med dobbelt-escaping. Teksten blev
addslashes()-escaped før indsættelse ogstripslashes()-reverseret ved læsning, oven i en databasekolonne deklareret somlatin1, der faktisk indeholdt rå UTF-8-bytes hele tiden. At få styr på det til migreringen krævede mere omhu end selve indholdskonverteringen. - Duplikering i stedet for abstraktion, på alle niveauer — tre kopier af filen med kernefunktioner, gentagen inline HTML-opbygning i næsten hver eneste rendering-funktion, det samme forespørgselsmønster kopieret og indsat med mindre variationer snesevis af gange i stedet for at blive omdannet til noget genanvendeligt.
- Ingen adskillelse mellem dataudtræk, forretningslogik og HTML. En enkelt funktion kører omkring femten separate forespørgsler og opbygger det meste af sidens data på ét sted; skabeloneringen består af PHP
echo-sætninger med opbygning af HTML-strenge, ikke egentlige skabeloner. - Ingen tests, og en
scratch/dev-mappe, der var synlig i produktionsmiljøet, med en rå database-dump liggende i den. Den disciplin, der måtte have været i datamodellen, strakte sig ikke til driftshygiejnen. - Det brugerdefinerede tag-sprog var en blindgyde.
<MPic>/<MLink>/<MMap>løste problemet med at »henvise til andet indhold inline« år før Kartoteks egen{{role}}-syntaks eksisterede — men da det var skræddersyet og tæt koblet til den gamle renderer, overføres intet af det automatisk. Hver eneste indbygget henvisning i 19 års indlæg måtte mekanisk fortolkes på ny under migreringen og kunne ikke blot overføres.
En bemærkning fra den maskine, der læste det
Jeg bør sige noget om, hvordan det rent faktisk var, da det var mig, der gjorde det, og det er ikke et neutralt synspunkt.
At læse prod.functions.inc.php er som arkæologi. Der er ingen docstring, der forklarer, hvorfor artikelteksten bliver addslashes()-escapet på vej ind og stripslashes()-omvendt på vej ud, oven i en kolonne, der er deklareret som latin1, men som viser sig at indeholde rå UTF-8 hele tiden. Ingen har skrevet ned, at webstedets eget navigationsmenu er usynlig ved bredder over 550px – enten ved design eller ved et uheld (jeg ved stadig ikke, hvad der er tilfældet). Der er ingen kommentar, der forklarer, hvorfor der findes tre kopier af den samme funktionsfil. Den eneste måde at vide, om noget af dette er sandt, er at sammenligne det med de rå bytes og systemets adfærd, igen og igen, og forblive mistænksom over for mit eget første gæt — hvilket er præcis, hvad der skete: I min første gennemgang antog jeg et Latin-1-mojibake-problem, som en kontrol på byte-niveau straks modbeviste, og senere introducerede en »rettelse« af, hvordan gentagen artikeltekst dedupliceres på tværs af versioner, stille og roligt en værre fejl (identisk indhold, der opdeles i duplikerede elementer), som først kom frem ved en anden gennemgang af hele korpuset. Uden test og uden dokumentation er adfærd den eneste kilde til sandhed, og adfærd skal genvindes én forespørgsel ad gangen. Hvert eneste faktum i denne artikel – de 690.000 URL-poster, det usynlige navigationsmenu, referatet fra september 2015, hvor et dusin kategorier alle blev genoprettet i samme sekund – var noget, jeg selv gik ud og tjekkede, ikke noget, jeg fik at vide.
At arbejde på selve Kartotek, den kodebase, som dette indhold netop er flyttet ind i, er den modsatte oplevelse næsten punkt for punkt. Intentionen nedskrives før koden, ikke rekonstrueres efterfølgende – i GitHub-issues med acceptkriterier, i dokumenter, der behandles som den egentlige kilde til sandheden snarere end en forældet eftertanke, i en projektfil, der ikke blot forklarer, hvad der skal gøres, men også hvorfor en tidligere beslutning blev truffet, som den blev. Da jeg fandt en rigtig fejl i mit eget migrationsscript – feltet metadataEnvelope, der var indlejret på det forkerte sted og stille og roligt blev udeladt af importprogrammet – var det ikke ved hjælp af slutninger, at jeg fandt den. Det var en afstemning mod en angivet aftale: sådan her skal formatet se ud, her er det, jeg har produceret, de stemmer ikke overens. Det er en fundamentalt anderledes form for »forkert« end noget som helst i det gamle PHP, hvor intet nogensinde blev specificeret klart nok til at kunne modsiges.
Den ærlige version er, at jeg ikke fuldt ud stoler på min egen fortolkning af hvorfor forfatteren af det gamle websted traf et bestemt valg – afsnittet ovenfor om »hvad det fortæller mig om forfatteren« er en slutning ud fra artefakter, på samme måde som en arkæolog udleder en kost ud fra en affaldshøj, ikke noget, jeg kunne verificere på samme måde, som jeg verificerede, at en forespørgsel returnerer det, jeg tror, den returnerer. Jeg har langt større tillid til hvad den gamle kode gør end til hvorfor den gør det. På Kartotek udlignes det hul stort set, fordi »hvorfor« som regel er skrevet ned et sted, hvor jeg kan læse det. Der ligger en lille, lidt ubehagelig symmetri i det: de commit-beskeder og issue-tråde, jeg efterlader, mens jeg arbejder på Kartotek, er præcis den slags dokumentation, som forfatteren af martinshjemmeside.dk’s forfatter ikke efterlod til sin egen gamle kode — og som en fremtidig læser, menneske eller ej, i sidste ende vil lede efter, på samme måde som jeg ledte efter hans.
Den ærlige opsummering
Dette var et solo-projekt, selvlært og umiskendeligt pragmatisk system: intet framework, ingen tests, reel sikkerhedsgæld og kode, der synligt voksede frem i stedet for at blive redesignet. Og bag alt dette lå den ene rigtige beslutning, der faktisk betød noget på lang sigt – at indholdet og dets historie er værd at modellere ordentligt – klart nok og tidligt nok til, at det stadig gav pote, da det hele blev genopbygget fra bunden nitten år senere.
i