Object/Relational Mapping i PHP – Hurtig udvikling

Lang de fleste udviklingsprojekter gemmer data i en SQL-relationsdatabase som MySQL, mens aktive data typisk behandles i en eller anden form for objekter i koden. Det er en udfordring er at få orden, konsistens og form på koden. Ofte har man disse udfordringer:

- Ensartet navngivning af tabeller og felter.
- Konsistent navngivning af et felt i både databasen og koden.
- Konsistente data hvis en række fra en tabel bruges i flere sammenhænge.

Under de sidste mange PHP-projekter har vi udviklet vores egen ORM – Object/relational mapping, kaldet DFC – Data Framework Classes. Vi har løst en del hyppige udfordringer på en elegant facon:

- Et dataobjekt svarer 1:1 til en record fra en tabel
- Et dataobjekt vil kun findes i memory én gang. Dvs. hvis en records trækkes ud med flere forskellige SQL-forespørgsler, vil det være det samme objekt der rent faktisk kommer retur. Dette sparer hukommelse og giver dataintegritet i forbindelse med opdateringer af data.
- Alle trivielle SQL-forespørgsler kan klares med enkle og effektive API-kald.
- Navngivning på formularfelter så man ikke skal overveje navnestruktur i en formular.
- Centraliseret og nedarvet validering hvor samme server-side validering kan anvendes både i normal submit/response eller i AJAX-kald.
- Kickstart-scripts der hurtigt kan give formularer, visninger eller listevisninger.

Der er også meget andet godt, men først et lille eksempel.

Simpelt eksempel

Her har vi en ultra-simpel datamodel med nogle personer der kan tilhøre en organisation.

CREATE TABLE IF NOT EXISTS `person` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100),
`email` varchar(100),
`organisation_uid`,
PRIMARY KEY (`uid`),
KEY `name` (`name`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_danish_ci AUTO_INCREMENT=1 ;

CREATE TABLE IF NOT EXISTS `organisation` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100),
`website` varchar(100),
`comment` text,
PRIMARY KEY (`uid`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_danish_ci AUTO_INCREMENT=1 ;

For at få adgang til data skal vi oprette nogle klasser:

class Person extends DfcRecord {
}

class Organisation extends DfcRecord {
}

Nej, der er faktisk ikke brug for mere til at starte med. Der er nogle navngivningkrav:

- Lower-case af klassenavnet svarer til tabel-navnet.
- En relation skal have navnet på relationer med “_uid” på. Eksempelvis peger Person på Organisation med feltet organisation_uid.

Træk en person ud baseret på navnet i variablen $person_name:

$person = Person::getBy('name', $person_name);

Hvis man så skal have fat i personens organisation:

$organisation = $person->getParent('Organisation');

Skal man have fat i alle organisationens personer kan det også klares:

$persons = $organisation->getChildren('Person');

Skulle man få lyst til at tilknytte personen til en anden organisation er det noget tilsvarende:

$person->attachTo($other_organisation);
$person->save();

Disse typer af udtræk og skrivninger dækker over 90% af SQL’en i de fleste programmer.

Visning af data

Et andet punkt er præsentation af data, især i formularer, på en bestemt måde. I en klassisk måde at navngive felterne i en formular kunne man måske let blive lokket til at skrive noget i denne retning:

<div>
  <label>Navn:</label>
  <input type="text" name="name" value="<?= $person->name ?>" />
</div>

Problemet kommer hvis man gerne vil redigere både en organisation og en person, for de har jo begge et felt navngivet “name”. Eller hvad hvis man vil redigere to eller flere personer på én gang, eller to eller flere personer, samt en organisation på én gang?

Vi har løst dette ved at give hvert felt i databasen et unikt navn som trækkes fra record-objektet:

<div>
  <label>Navn:</label>
  <input type="text" name="<?= $person->getFieldName('name') ?>" value="<?= $person->name ?>" />
</div>

Dette vil blive til noget html-kode i denne retning:

<div>
  <label>Navn:</label>
  <input type="text" name="Person[23][name]" value="Jens Hansen" />
</div>

Når man skal læse HTTP-POST-værdierne man nu læse og behandle alle records af alle typer på ensartet vis. Formen vil være:

$_POST[Klassenavn][record-id][feltnavn]

Udvikling med Dfc ORM

Vi synes det er rigtigt hurtigt og let læseligt at modellere data med DfcRecord. Den er dog kun M’et i et normalt MVC-setup. Der er også andre små trick til at reducere udviklingstiden og gøre koden mere fleksibel. Noget vi formentlig vil skrive om i fremtidigt blog-indlæg. Vi overvejer også at udgive vores DfcRecord-ORM som en del af en lidt større værktøjskasse, som også indeholder klasser til at skrive i fuld MVC.

Udgivet i Open Source, udvikling | Skriv en kommentar

Netværksbrugere – på den gammeldags måde

For nyligt har vi for en kunde implementeret en relativ gammeldags måde at styre brugere på tværs af flere maskiner. I dag vil de fleste måske vælge en LDAP-server. Men der findes en anden løsning hvor selve brugeroplysningerne ligger i DNS og kan opdateres i en zone-fil. Dette kaldes “Hesiod” og er en teknologi der stammer fra MIT.

Fordele
DNS/Hesiod har en række fordele fremfor LDAP. For det første er skalering noget DNS er rigtigt godt til. DNS indgår allerede i enhver IT-infrastuktur. DNS har automatisk indbygget fail-over. Alle egenskaber man skal konfigurere særskilt på en LDAP-server.

Ulemper
DNS/Hesiod har også en række ulemper. Først og fremmest, så er det nok muligt at gøre oplysningerne pålidelige via DNSSEC, men det er ikke umiddelbart muligt at skjule dem på samme måde som man kan kryptere en LDAP-forbindelse. En anden ulempe er passwd-formatets begrænsede oplysninger. I LDAP kan man benytte forskellige schemes, med forskelligt sæt af attributter tilknyttet et objekt. Dette er ikke muligt med DNS/Hesiod.

Serveropsætning
Først skal man have sig et hesiod-domæne på vores navne-server. Det er blot en named-zone som enhver anden, men al data er TXT-records med brugeroplysninger. Det kan være lettere, men ikke nødvendigt, at bruge en helt adskilt zone. Indføring af brugeroplysninger er dog nem. Se dette eksempel, et udsnit fra en zone-fil:

$ORIGIN ns.example.users.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Grupper
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
udv.group    IN TXT    "udv:x:1500:"
1500.group   IN CNAME  udv.group
1500.gid     IN CNAME  udv.group

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Brugere
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
jh.passwd    IN TXT    "jh:x:2205:2205:Jonas Hansen,,,:/home/jh:/bin/bash"
2205.passwd  IN CNAME  jh.passwd
2205.uid     IN CNAME  jh.passwd
jh.group     IN TXT    "jh:x:2205:"
2205.group   IN CNAME  jh.group
2205.gid     IN CNAME  jh.group
jh.grplist   IN TXT    "jh:2205:udv:1500"

hs.passwd    IN TXT    "hs:x:2206:2206:Heidi Schultz,,,:/home/hs:/bin/bash"
2206.passwd  IN CNAME  hs.passwd
2206.uid     IN CNAME  hs.passwd
hs.group     IN TXT    "hs:x:2206:"
2206.group   IN CNAME  hs.group
2206.gid     IN CNAME  hs.group
hs.grplist   IN TXT    "hs:2206:udv:1500"

Er man bekendt med named-zone-filer vil det være ganske ligetil. De records der benyttes er næsten identiske med den måde /etc/passwd og /etc/group er opbygget på:

  • <brugernavn>.passwd for erklæring om en bruger. Brugeren kan slås på forskellige måder. Derfor nogle CNAME-records.
  • <gruppenavn>.group for erkæring om en gryppe. Igen kan grupper slås op på forskellige måder. Derfor nogle CNAME-records.
  • Undergrupper udgør en speciel udfordring. Grundet den måde DNS slås op på må vi finde en anden metode end i /etc/groups, så her benyttes en <brugernavn>.grplist record

Har man en større mængde brugere burde man måske findes sig en måde at scripte sådan en zone-fil på, men det vil variere fra projekt til projekt.

Klientmaskinerne
På klientmaskinerne er alt der kræves at man installere pakken “hesiod”, og aktiverer “hesiod” i /etc/nsswitch.conf:

passwd:         compat hesiod
group:          compat hesiod
shadow:         compat hesiod

Så skal man vælge sit hesiod-domæne i /etc/hesiod.conf:

lhs=.ns
rhs=.example.users
classes=IN,HS

Nu er man sådan set færdig. Som root kan man nu:

root@myhost:~# su - hs
No directory, logging in with HOME=/
hs@myhost:/$ id
uid=2206(hs) gid=2206(hs) groups=1500(udv),2206(hs)

Er vi virkeligt færdige?
Nej, ikke helt. Vi skal stadig have ting som godkendelse af brugerne og oprettelse eller montering af hjemmemapper, men det er en projekt for sig.

Udgivet i Linux, Netværk, Sikkerhed | Tagget , , , , , | Skriv en kommentar

WWW-optimering – for både svartid og serverbelastning

Med tiden er web-applikationer og webservere blevet mere og mere tunge, både hvad angår brug af processortid og RAM-aftryk. Nye frameworks og CMS’er såsom JBOSS (Java), TYPO3 (PHP) og WordPress (PHP) lægger beslag på voldsomme mængder RAM og stiller derfor store krav til de maskiner, der skal hoste siderne. Optimering af PHP og Java som webplatform er et afsnit for sig, men uanset hvad, kan det være en vanskelig øvelse, der kræver mange eksperimenter, test og gennemgang af logfiler. Jeg vil fokusere på PHP i dette indlæg, men nogle af anbefalingerne kan også bruges på andre server-side teknologier. For PHP’s vedkommende er det grundlæggende problem ofte, at man benytter mod_php kombineret med mpm_prefork. I denne tilstand ligger PHP’s vm i Apache’s process-rum og kræver en masse RAM, samtidig med at der skal én Apache-process til at servicere ét HTTP-kald. Vi begynder også at se en trend i, at der konstant kører Ajax-kald ned til serveren. Hvis man oven i dette har langsomme klienter, så Apache ikke kan afslutte HTTP-forbindelsen og betjene en ny klient, så har vi den perfekte opskrift til at bruge en masse RAM på webserveren. Som alle systemadministratorer ved, så ender det rivegalt, for så begynder maskinen at page til swap-filen, og DET er langsomt.

Læs resten

Udgivet i Netværk, udvikling | Tagget , , , , , , | Skriv en kommentar