Aanmelden

🐘PHP, AJAX & MySQL

Zelfstudie met uitleg, oefeningen en quizzen: van je eerste PHP-regel tot een werkende messenger met AJAX, JSON, MySQL en prepared statements.

Je bekijkt deze pagina niet aangemeld — je kan de oefeningen, het opzoekwerk en de zelftest-quiz pas openen na het aanmelden.
Aanmelden

Overzicht

Werk de onderdelen hieronder in je eigen tempo af: lees de uitleg, doe het opzoekwerk en sluit af met een quizje. Klik een onderdeel open om te starten.

Waarom leren we PHP, wat gebeurt er precies tussen jouw browser en een server, en hoe gaan we in deze cursus te werk?

Wat is PHP?
Het PHP-logo
Het PHP-logo

PHP is een programmeertaal die draait op de server. De naam is een "recursief acroniem": PHP: Hypertext Preprocessor. Met PHP maak je websites die niet elke keer hetzelfde tonen, maar dynamisch reageren: gegevens uit een databank tonen, een login afhandelen, een bericht opslaan, ...

  • PHP is gratis en open source.
  • Een PHP-bestand heeft de extensie .php en mag gewoon HTML, CSS en JavaScript bevatten, aangevuld met PHP-code.
  • PHP-code wordt uitgevoerd op de server; de browser krijgt enkel het resultaat (gewone HTML) terug.
Client en server: wie doet wat?
Het client-servermodel: de client vraagt, de server antwoordt
Het client-servermodel: de client vraagt, de server antwoordt

Elke keer je een website opent, gebeurt er een klein gesprek:

  1. Jouw browser (de client) stuurt een request: "geef mij index.php".
  2. De server voert de PHP-code in dat bestand uit.
  3. De server stuurt een response terug: gewone HTML (of JSON, later in deze cursus).
  4. De browser toont het resultaat.
Client-side vs server-side

HTML, CSS en JavaScript zijn client-side: ze draaien in jouw browser. PHP is server-side: het draait op de server en jij ziet de PHP-code zelf nooit in "bron weergeven". Daarom kan een server veilig met een databank of wachtwoorden werken.

Wat kan PHP allemaal?
  • Dynamische pagina-inhoud genereren
  • Gegevens in een databank toevoegen, wijzigen en verwijderen
  • Formulier- en AJAX-gegevens verwerken
  • Gebruikers laten inloggen en toegang beperken (sessies)
  • Wachtwoorden veilig opslaan (hashing)
  • Bestanden uploaden, e-mails versturen, ...

PHP is nog altijd enorm verspreid: de bekendste CMS-en (WordPress, Joomla, Drupal) draaien op PHP, net als heel wat grote websites en frameworks zoals Laravel (een van onze latere cursussen).

Hoe werken we in deze cursus?

Deze cursus is geschreven zoals wij ook echte projecten opbouwen (zoals het Classid-project). Onthoud deze vier afspraken, ze komen in elk hoofdstuk terug:

  1. Geen <form>: we halen gegevens uit invoervelden op met jQuery en versturen ze zelf.
  2. AJAX + JSON: gegevens gaan via AJAX (jQuery) naar PHP en PHP antwoordt altijd in het formaat JSON.
  3. Prepared statements: elke databankvraag met gegevens van buitenaf gebruikt een prepared statement met placeholders (we leggen uit waarom).
  4. Partials: herhaalde stukken (header, footer, configuratie) staan in één bestand dat we overal includen.
Zo loopt gegevens door onze applicatiesBrowser (HTML + jQuery)
      |  $.ajax  -- data: {} -->  api/berichten.php  --- prepared statement --->  MySQL
      |  <-------- JSON -------  (PHP op de server)  <------ resultaat ---------
Zelfstudie

Onder de uitleg vind je "Probeer zelf"-blokken (JavaScript en HTML kan je meteen uitvoeren) en oefeningen waar je zelf code intypt. Die code wordt automatisch nagekeken. Vast? Gebruik de hint, en als het echt niet lukt kan je de oplossing bekijken.

🎯 Nu ben jij aan de beurt: Wat is PHP?

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.

PHP heeft een server nodig. Om te oefenen zetten we er zelf een op onze eigen computer: Apache, PHP en MySQL in één pakket.

Waarom een lokale server?

Een gewone webpagina (HTML) kan je rechtstreeks in je browser openen. Een PHP-bestand niet: het moet eerst door PHP uitgevoerd worden en dat doet een webserver. Op een echte hostingserver testen bij elke kleine wijziging is traag en riskant, dus installeren we een lokale server op onze eigen computer.

Bekende pakketten zijn XAMPP, MAMP (Mac én Windows) en WAMP (Windows). Ze bevatten alles wat we nodig hebben:

  • Apache — de webserver
  • PHP — de programmeertaal
  • MySQL/MariaDB — de databank
  • phpMyAdmin — een website om je databank te beheren
Je eerste pagina op localhost
  1. Installeer XAMPP of MAMP en start Apache en MySQL in het controlepaneel.
  2. Zoek de map waar de server je bestanden verwacht: de document root. Bij XAMPP heet die htdocs, bij MAMP standaard ook htdocs.
  3. Maak daarin een map test met een bestand index.php.
  4. Open je browser en ga naar http://localhost/test/.
htdocs/test/index.php<!DOCTYPE html>
<html lang="nl">
  <head>
    <meta charset="utf-8">
    <title>Mijn eerste PHP-pagina</title>
  </head>
  <body>
    <h1><?php echo 'Hallo vanop mijn eigen server!'; ?></h1>
  </body>
</html>
Zie je de PHP-code zelf op je scherm?

Dan opende je het bestand rechtstreeks (file:///...) in plaats van via de server. Het adres moet met http://localhost/ beginnen.

phpMyAdmin
Het logo van phpMyAdmin
Het logo van phpMyAdmin

Naast localhost heb je ook http://localhost/phpmyadmin. Dat is een website (zelf in PHP geschreven!) waarmee je databanken en tabellen aanmaakt en bekijkt. We gebruiken het vanaf het hoofdstuk over MySQL.

Problemen oplossen
  • "Poort bezet": een ander programma gebruikt poort 80 of 3306. Sluit dat programma of wijzig de poort in de instellingen van je serverpakket.
  • Pagina toont niets / witte pagina: er zit waarschijnlijk een fout in je code. Zet in je ontwikkelomgeving foutmeldingen aan (zie hieronder).
  • Bestand heeft extensie .html: PHP-code in een .html-bestand wordt niet uitgevoerd. Gebruik altijd .php.
Foutmeldingen zichtbaar maken (enkel lokaal, nooit op een live site!)<?php
// Bovenaan tijdens het ontwikkelen: toon alle fouten
ini_set('display_errors', 1);
error_reporting(E_ALL);
Nooit op een live website

Foutmeldingen bevatten paden en soms zelfs databankgegevens. Op een echte server zet je display_errors uit en log je fouten in een bestand.

🎯 Nu ben jij aan de beurt: Een lokale server installeren

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.

Variabelen, tekst, rekenen en vergelijken: de bouwstenen van elk PHP-programma.

PHP-tags, echo en commentaar

PHP-code staat tussen <?php en ?>. Met echo stuur je tekst naar de browser. Elke instructie eindigt met een puntkomma. In een bestand dat enkel PHP bevat laat je de afsluitende ?> weg.

index.php<?php
// Dit is commentaar op één regel
# Dit ook
/* Dit is commentaar
   over meerdere regels */

echo 'Hallo wereld!';
echo '<br>';            // <br> = nieuwe regel in HTML
echo "Nog een zin";
Resultaat in de browserHallo wereld!
Nog een zin
Variabelen en datatypes

Een variabele begint met een $ en bewaart een waarde. PHP bepaalt zelf het type:

index.php<?php
$naam      = 'Sam';        // string (tekst)
$leeftijd  = 17;           // int (geheel getal)
$lengte    = 1.82;         // float (kommagetal)
$isStudent = true;         // bool (true of false)
$niets     = null;         // null (geen waarde)

var_dump($leeftijd);       // toont type én waarde: int(17)
  • Namen zijn hoofdlettergevoelig: $naam en $Naam zijn verschillend.
  • Een naam start nooit met een cijfer. Gebruik duidelijke namen: $aantalBerichten, niet $a.
Tekst samenvoegen: de punt!

In JavaScript plak je tekst aan elkaar met +. In PHP gebruik je een punt (.). Een plus is enkel om te rekenen.

index.php<?php
$voornaam = 'Sam';
$leeftijd = 17;

echo 'Ik ben ' . $voornaam . ' en ik ben ' . $leeftijd . ' jaar.';
echo "Ik ben $voornaam en ik ben $leeftijd jaar.";   // dubbele quotes: variabelen worden ingevuld
echo 'Ik ben $voornaam';                              // enkele quotes: letterlijke tekst!

$zin = 'Hallo';
$zin .= ' wereld';     // .= plakt achteraan: $zin is nu "Hallo wereld"
Resultaat in de browserIk ben Sam en ik ben 17 jaar.
Ik ben Sam en ik ben 17 jaar.
Ik ben $voornaam
Enkele of dubbele aanhalingstekens?

Tussen dubbele aanhalingstekens vult PHP variabelen in. Tussen enkele blijft alles letterlijk staan (en is dus iets sneller). Wil je een apostrof in enkele quotes? Schrijf \'.

Rekenen en vergelijken
index.php<?php
$a = 15;
$b = 4;

echo $a + $b;     // 19  (optellen)
echo $a - $b;     // 11  (aftrekken)
echo $a * $b;     // 60  (vermenigvuldigen)
echo $a / $b;     // 3.75 (delen)
echo $a % $b;     // 3   (rest van de deling)
echo $a ** 2;     // 225 (macht)
$a++;             // $a is nu 16 (één erbij)

// Vergelijken geeft true of false
var_dump(5 == '5');    // true  -> waarde gelijk, type maakt niet uit
var_dump(5 === '5');   // false -> waarde ÉN type moeten gelijk zijn
var_dump(5 !== '5');   // true
var_dump(10 > 3, 10 <= 3);
== of ===?

Gebruik bijna altijd ===. Met == past PHP types aan voor het vergelijkt, wat voor rare fouten kan zorgen.

Handige tekstfuncties
index.php<?php
$zin = 'Warre is de meest geweldige persoon!';

echo strlen($zin);                                  // aantal tekens: 36
echo strtoupper($zin);                              // ALLES IN HOOFDLETTERS
echo ucfirst('sam');                                // Sam
echo str_replace('geweldige', 'irritante', $zin);   // vervangt een stuk tekst
echo trim('  spaties weg  ');                       // "spaties weg"
echo substr('Provil', 0, 3);                        // "Pro"
var_dump(str_contains($zin, 'Warre'));              // true

Op php.net vind je van elke functie de uitleg en voorbeelden. Leren opzoeken is een kerncompetentie van een programmeur!

🎯 Nu ben jij aan de beurt: De basis van PHP

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.

Laat je code keuzes maken met if en switch, laat ze dingen herhalen met loops en bewaar lijsten van gegevens in arrays.

if, elseif en else
index.php<?php
$stad = 'Lyon';

if ($stad === 'Parijs') {
    echo 'Dit is de hoofdstad van Frankrijk';
} elseif ($stad === 'Lyon') {
    echo 'Dit is de derde grootste stad van Frankrijk';
} else {
    echo 'Dit is een andere stad';
}

Voorwaarden combineer je met && (en), || (of) en ! (niet):

index.php<?php
$leeftijd = 17;
$heeftKaart = true;

if ($leeftijd >= 16 && $heeftKaart) {
    echo 'Je mag binnen';
}
if (!$heeftKaart || $leeftijd < 12) {
    echo 'Je mag niet binnen';
}
Korte schrijfwijzen: ?: en ??

De ternaire operator is een verkorte if/else. De null-coalescing operator ?? geeft een standaardwaarde als iets niet bestaat. Je zal ?? in elke API tegenkomen.

index.php<?php
$isOnline = true;
echo $isOnline ? 'online' : 'offline';       // voorwaarde ? als waar : als onwaar

$body = [];                                   // bv. de ontvangen gegevens
$naam = $body['naam'] ?? 'Onbekend';          // bestaat 'naam' niet? gebruik 'Onbekend'
echo $naam;
Resultaat in de browseronline
Onbekend
switch en match
index.php<?php
$rol = 'leerkracht';

switch ($rol) {
    case 'leerling':
        echo 'Welkom leerling';
        break;                 // zonder break loopt PHP door naar het volgende geval!
    case 'leerkracht':
        echo 'Welkom leerkracht';
        break;
    default:
        echo 'Onbekende rol';
}

// match (PHP 8): korter en geeft meteen een waarde terug
$tekst = match ($rol) {
    'leerling'   => 'Welkom leerling',
    'leerkracht' => 'Welkom leerkracht',
    default      => 'Onbekende rol',
};
Arrays: lijsten van gegevens

Een array bewaart meerdere waarden in één variabele. Een geïndexeerde array telt vanaf 0. Een associatieve array gebruikt zelfgekozen sleutels, precies zoals een JSON-object of een rij uit een databank.

index.php<?php
$namen = ['Daphne', 'Kylian', 'Arnould', 'Jeffrey'];
echo count($namen);          // 4
echo $namen[2];              // Arnould (tellen begint bij 0!)
$namen[] = 'Lotte';          // achteraan toevoegen
var_dump(in_array('Kylian', $namen));   // true

// Associatieve array: sleutel => waarde
$leerling = [
    'naam'   => 'Sam',
    'klas'   => '6IICT',
    'online' => true,
];
echo $leerling['naam'];      // Sam
$leerling['klas'] = '6ICW';  // waarde wijzigen
Arrays en JSON horen bij elkaar

Later zetten we zo een associatieve array met json_encode() in één keer om naar JSON, en met json_decode() terug. Daarom zijn ze zo belangrijk.

Herhalen met loops
index.php<?php
// while: herhaal zolang de voorwaarde waar is (vergeet de teller niet!)
$a = 1;
while ($a <= 3) {
    echo "Getal: $a<br>";
    $a++;
}

// for: teller, voorwaarde en stap in één regel
for ($i = 20; $i <= 30; $i += 2) {
    echo "Even getal: $i<br>";
}

// foreach: elk element van een array overlopen
$namen = ['Daphne', 'Kylian', 'Arnould'];
foreach ($namen as $naam) {
    echo "Hallo $naam<br>";
}

// foreach met sleutel én waarde
$leerling = ['naam' => 'Sam', 'klas' => '6IICT'];
foreach ($leerling as $sleutel => $waarde) {
    echo "$sleutel: $waarde<br>";
}

break stopt een loop meteen, continue springt naar de volgende ronde.

Oneindige loop

Vergeet je in een while-loop de teller te verhogen, dan stopt de loop nooit en loopt je server vast. Controleer altijd hoe je loop eindigt.

🎯 Nu ben jij aan de beurt: Beslissen, herhalen en arrays

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.

Verpak code die je vaak nodig hebt in een functie, zodat je ze één keer schrijft en overal kan hergebruiken.

Je eerste functie

Een functie is een stukje code met een naam. Je definieert ze één keer en roept ze zo vaak op als je wil. Een functie kan parameters ontvangen en met return een waarde teruggeven.

index.php<?php
function begroet(string $naam): string
{
    return 'Hallo ' . $naam . '!';
}

echo begroet('Sam');       // Hallo Sam!
echo begroet('Lotte');     // Hallo Lotte!

function tel(int $a, int $b): int
{
    return $a + $b;
}
$som = tel(4, 5);          // $som is 9
  • string $naam en : string zijn types: welk soort waarde erin gaat en eruit komt. Ze zijn optioneel maar maken je code veel duidelijker en veiliger.
  • return beëindigt de functie meteen. Een functie zonder return geeft null terug.
Standaardwaarden
index.php<?php
function mijnStad(string $land = 'België'): void
{
    echo 'Je woont in: ' . $land . '<br>';
}

mijnStad('Frankrijk');    // Je woont in: Frankrijk
mijnStad('Spanje');       // Je woont in: Spanje
mijnStad();               // Je woont in: België  (standaardwaarde)

void betekent: deze functie geeft niets terug, ze doet enkel iets (hier: tekst tonen).

Scope: wat kent een functie?

Variabelen die je binnen een functie maakt, bestaan enkel daar. En omgekeerd ziet een functie de variabelen van buiten niet. Wat een functie nodig heeft, geef je mee als parameter.

index.php<?php
$korting = 10;

function prijsMetKorting(float $prijs, float $korting): float
{
    return $prijs - ($prijs * $korting / 100);   // $korting komt van de parameter, niet van buiten
}

echo prijsMetKorting(200, $korting);   // 180
Waarom functies?

Kleine functies die één ding doen zijn makkelijk te testen, te lezen en te hergebruiken. Schrijf je twee keer dezelfde code? Dan hoort die in een functie. Dit principe heet DRY: Don't Repeat Yourself.

Ingebouwde functies en een blik vooruit

PHP heeft honderden ingebouwde functies, zoals rand(), count(), strlen() en date(). In onze API's schrijven we ook een paar kleine hulpfuncties die we overal gebruiken. Dit is er eentje die we later nodig hebben om JSON terug te sturen:

api/config.php<?php
function json_out(array $data, int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data);
    exit;                      // stop het script: er komt niets meer na de JSON
}

// Gebruik:
json_out(['ok' => true, 'bericht' => 'Opgeslagen']);

Kijk goed naar de standaardwaarde $status = 200: normaal geef je enkel de data mee, bij een fout kan je een andere status doorgeven.

🎯 Nu ben jij aan de beurt: Functies

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.

Schrijf elk stuk code één keer en hergebruik het overal met include en require: zo werkt een echt project.

Waarom bestanden opdelen?

Stel: je website heeft 20 pagina's met dezelfde menubalk. Pas je het menu aan, dan moet je 20 bestanden wijzigen (en er eentje vergeten). Daarom knippen we herhaalde stukken uit en zetten we ze in partials: kleine bestanden die je op elke pagina invoegt.

Typische partials: header.php (menu + begin van de pagina), footer.php, en onzichtbare bestanden zoals config.php (databankgegevens, databankverbinding en hulpfuncties).

Zo ziet een nette mapstructuur eruitmijn-project/
├── index.php              <- een pagina
├── contact.php            <- nog een pagina
├── partials/
│   ├── header.php         <- menu, <head>, stijlen
│   └── footer.php         <- voetnoot, scripts
├── api/
│   ├── config.php         <- $conn (databank), json_out()
│   └── berichten.php      <- geeft JSON terug
└── js/
    └── berichten.js       <- jQuery-code van de pagina
include, require en require_once

Met deze instructies voeg je een ander PHP-bestand in alsof de code daar stond:

InstructieBestand niet gevonden?Gebruik voor
includewaarschuwing, script gaat doorstukken HTML (partials)
requirefatale fout, script stoptbestanden die je écht nodig hebt
require_oncezoals require, maar maximaal één keerconfig.php en bestanden met functies
index.php<?php
require_once __DIR__ . '/api/config.php';   // hulpfuncties + databank: verplicht
?>
<!DOCTYPE html>
<html lang="nl">
<body>
    <?php include __DIR__ . '/partials/header.php'; ?>

    <main>
        <h1>Welkom!</h1>
    </main>

    <?php include __DIR__ . '/partials/footer.php'; ?>
</body>
</html>
Waarom __DIR__?

__DIR__ is de map van het bestand waarin je zit. Zo klopt het pad altijd, ook als je een pagina vanuit een andere map opent. Een pad zoals 'partials/header.php' zonder __DIR__ hangt af van waar je begint en breekt snel.

Een header- en footer-partial
partials/header.php<header>
    <nav>
        <a href="index.php">Home</a>
        <a href="berichten.php">Berichten</a>
    </nav>
</header>
partials/footer.php<footer>
    <p>&copy; <?= date('Y') ?> Provil ICW</p>
</footer>

Pas je nu het menu aan in header.php, dan verandert het op alle pagina's tegelijk. Dat is de kracht van partials.

Eén configuratiebestand

Gegevens zoals de databankverbinding zet je op één plaats. Verhuist je site van je laptop naar een echte server? Dan pas je één bestand aan.

api/config.php<?php
$dbServername = 'localhost';
$dbUsername   = 'root';
$dbPassword   = '';
$dbName       = 'messenger';

// Later voegen we hier de databankverbinding ($conn) en de hulpfunctie json_out() aan toe
Let op: gedeelde variabelen

Een ingevoegd bestand deelt de variabelen van het bestand dat het insluit. Dat is handig, maar ook een valkuil: gebruikt een partial een variabele met dezelfde naam als jouw pagina, dan overschrijft hij die stilletjes.

Een echte bug uit dit platform<?php
$slug = 'moederbord';                   // in de pagina
include __DIR__ . '/partials/menu.php';

foreach ($alleOnderdelen as $slug => $o) { /* ... in menu.php */ }

echo $slug;                             // OEPS: dit is nu de laatste waarde uit de loop, niet meer 'moederbord'
Tip

Geef variabelen in partials een eigen, duidelijke naam (zoals $menuSlug) en bewaar waarden die je nog nodig hebt in een variabele met een unieke naam voor je een partial insluit.

🎯 Nu ben jij aan de beurt: Partials en bestanden opdelen

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.

Object-georiënteerd programmeren: gegevens en gedrag samenbrengen in klassen. Je hebt het nodig, want mysqli (onze databankverbinding) is zelf een klasse.

Klasse en object

Een klasse is een bouwplan. Een object is een concreet exemplaar dat volgens dat plan gebouwd is. Denk aan het plan van een auto (klasse) en jouw rode Golf met nummerplaat 1-ABC-123 (object).

Persoon.php<?php
class Persoon
{
    public string $naam;          // property (eigenschap)
    public int $leeftijd;

    public function __construct(string $naam, int $leeftijd)   // constructor: draait bij new
    {
        $this->naam = $naam;                  // $this = dit object
        $this->leeftijd = $leeftijd;
    }

    public function stelVoor(): string        // method (functie van de klasse)
    {
        return 'Ik ben ' . $this->naam . ' (' . $this->leeftijd . ')';
    }
}

$sam   = new Persoon('Sam', 17);   // een object maken met new
$lotte = new Persoon('Lotte', 18);

echo $sam->stelVoor();             // -> is de pijl om iets van een object te gebruiken
echo $lotte->naam;
Resultaat in de browserIk ben Sam (17)
Lotte
public en private

Met zichtbaarheid bepaal je wie aan eigenschappen en methods mag. public mag iedereen. private enkel de klasse zelf. Zo bescherm je gegevens tegen foute waarden.

Rekening.php<?php
class Rekening
{
    private float $saldo = 0;            // van buitenaf niet rechtstreeks aan te passen

    public function stort(float $bedrag): void
    {
        if ($bedrag > 0) {               // de klasse bewaakt haar eigen regels
            $this->saldo += $bedrag;
        }
    }

    public function saldo(): float       // een "getter": veilig het saldo opvragen
    {
        return $this->saldo;
    }
}

$r = new Rekening();
$r->stort(50);
echo $r->saldo();     // 50
// $r->saldo = 1000000;   // FOUT: saldo is private!
Compacter met constructor promotion

Sinds PHP 8 kan je eigenschappen meteen in de constructor declareren:

Bericht.php<?php
class Bericht
{
    public function __construct(
        public string $auteur,
        public string $tekst,
    ) {}
}

$b = new Bericht('Sam', 'Hey, wat is de opdracht?');
echo $b->auteur . ': ' . $b->tekst;
Waarom nu al over klassen?

Onze databankverbinding $conn is een object van de klasse mysqli. Zonder dit hoofdstuk zou new mysqli(...) en $conn->prepare(...) in de volgende hoofdstukken raar lijken. Nu herken je het patroon:

<?php
$conn = new mysqli($dbServername, $dbUsername, $dbPassword, $dbName);  // een mysqli-object maken
$statement = $conn->prepare("SELECT * FROM users");                   // een method van dat object gebruiken
$statement->execute();                                                // en van het object dat terugkomt
$result = $statement->get_result();                                   // ook een object, met eigen methods
Leerplandoelen

Na dit hoofdstuk kan je objecten maken van klassen en zelf klassen schrijven (properties, constructor, methods, zichtbaarheid).

🎯 Nu ben jij aan de beurt: Klassen en objecten

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.

Gegevens heen en weer sturen zonder de pagina te herladen, en zonder formulieren: jQuery haalt de gegevens uit de invoervelden, stuurt ze naar PHP en PHP antwoordt in JSON.

Wat is AJAX?
jQuery, de bibliotheek die we voor AJAX gebruiken
jQuery, de bibliotheek die we voor AJAX gebruiken

AJAX staat voor Asynchronous JavaScript And XML. Het is geen taal, maar een manier om met JavaScript op de achtergrond met de server te praten. Daardoor kan je:

  • gegevens van de server ophalen nadat de pagina geladen is
  • een deel van de pagina bijwerken zonder alles te herladen
  • gegevens naar de server (en de databank) sturen zonder dat de bezoeker iets merkt

Denk aan een chat: een nieuw bericht verschijnt meteen, zonder dat je moet vernieuwen.

JSON: de gemeenschappelijke taal
Het JSON-logo
Het JSON-logo

Browser (JavaScript) en server (PHP) spreken verschillende talen. Als tussenformaat gebruiken we JSON (JavaScript Object Notation): gewone tekst met een vaste structuur die beide kanten begrijpen. In deze cursus versturen we gegevens via AJAX en antwoordt PHP altijd in JSON.

Een JSON-antwoord van de server{
  "ok": true,
  "berichten": [
    { "id": "3f2a-...", "auteur": "Sam",   "tekst": "Hey!" },
    { "id": "9b1c-...", "auteur": "Lotte", "tekst": "Hallo Sam" }
  ]
}

Regels: sleutels staan altijd tussen dubbele aanhalingstekens, tekst ook, getallen en true/false/null niet. Objecten gebruiken { }, lijsten [ ].

In JavaScript zet je een object om naar tekst met JSON.stringify() en terug met JSON.parse(). In PHP doe je hetzelfde met json_encode() en json_decode().

✏️ Probeer zelf — pas de code aan en klik op Uitvoeren
Geen form: gegevens ophalen met jQuery

Vroeger stuurde je gegevens met een <form> en een submit-knop, waarbij de pagina herlaadde. Wij doen dat niet meer: een invoerveld is gewoon een <input> zonder form errond. Met jQuery lees je de waarde uit met .val() en bouw je zelf het object dat je verstuurt.

index.php<input type="text" id="auteur" placeholder="Jouw naam">
<input type="text" id="tekst"  placeholder="Typ een bericht">
<button type="button" id="verstuur">Verstuur</button>

<script>
$('#verstuur').on('click', function () {
    const data = {
        auteur: $('#auteur').val().trim(),
        tekst:  $('#tekst').val().trim()
    };
    console.log(data);              // { auteur: "Sam", tekst: "Hey!" }
});
</script>
Enter-toets

Zonder form doet Enter niets. Wil je dat Enter ook verstuurt? Luister naar de toets: $('#tekst').on('keydown', e => { if (e.key === 'Enter') verstuur(); });

✏️ Probeer zelf — pas de code aan en klik op Uitvoeren
$.ajax: JSON versturen en ontvangen

Zo verstuur je het object naar een PHP-bestand en verwerk je het JSON-antwoord. Dit is de vaste manier waarop we elke AJAX-call schrijven:

js/berichten.jsfunction verstuur() {
    const data = {
        auteur: $('#auteur').val().trim(),
        tekst:  $('#tekst').val().trim()
    };

    $.ajax({
        url: 'api/berichten.php',
        method: 'POST',
        data: data,                           // ons object: jQuery verstuurt de sleutels/waarden zelf
        dataType: 'json',                     // wat we TERUGKRIJGEN is JSON; jQuery zet het meteen om
        success: function (antwoord) {
            if (antwoord.ok) {
                $('#tekst').val('');
            } else {
                alert(antwoord.error);        // een fout die WIJ in PHP bedacht hebben
            }
        },
        error: function () {
            alert('De server gaf geen geldig antwoord.');   // netwerkfout of kapotte PHP
        }
    });
}
  • method: POST om iets te bewaren of te wijzigen, GET om iets op te halen.
  • data: het object met de gegevens die je meestuurt. Je geeft het rechtstreeks mee, jQuery doet de rest. In PHP komen ze in $_POST terecht.
  • dataType: 'json': jQuery leest het antwoord als JSON en geeft je meteen een JavaScript-object.
  • success draait bij een geldig antwoord, error bij een netwerkfout of ongeldige JSON.
✏️ Probeer zelf — pas de code aan en klik op Uitvoeren
De PHP-kant: gegevens lezen en JSON teruggeven

De gegevens uit het data-object van je AJAX-call staan in PHP in $_POST, met dezelfde sleutels als in je JavaScript-object. Het antwoord geef je terug als JSON met een kleine hulpfunctie in api/config.php:

api/config.php<?php
function json_out(array $data, int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data);   // PHP-array -> JSON-tekst
    exit;                      // stop meteen: er mag niets meer na de JSON komen
}
api/berichten.php<?php
require_once __DIR__ . '/config.php';

$auteur = trim($_POST['auteur'] ?? '');     // ?? '' : bestaat de sleutel niet, dan een lege tekst
$tekst  = trim($_POST['tekst'] ?? '');

if ($auteur === '' || $tekst === '') {
    json_out(['ok' => false, 'error' => 'Vul je naam en een bericht in.']);
}

// ... hier komt straks het opslaan in de databank ...

json_out(['ok' => true]);
Onze afspraak voor elk antwoord

Elk antwoord bevat ok: true als het lukte, anders false met een error-tekst voor de gebruiker. De JavaScript kijkt altijd eerst naar antwoord.ok.

Vertrouw $_POST nooit blindelings

Alles in $_POST komt van de bezoeker en kan dus alles bevatten. Controleer het altijd (leeg? juiste vorm?) en zet het nooit rechtstreeks in een SQL-query: daarvoor gebruiken we prepared statements (hoofdstuk 10).

Debuggen met de ontwikkelaarstools

Open de browser-devtools (F12) en kies het tabblad Netwerk (Network). Klik op je AJAX-request en bekijk:

  • Payload: wat je verstuurde (zie je je JSON?)
  • Response: wat PHP teruggaf
  • Status: 200 = OK, 404 = bestand niet gevonden, 500 = fout in je PHP
Krijg je een fout of niets terug?

Bekijk eerst de Response. Staat er een PHP-foutmelding of lege tekst in plaats van JSON, dan zit de fout in je PHP-bestand, niet in je JavaScript.

🎯 Nu ben jij aan de beurt: AJAX en JSON met jQuery

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.

Ontwerp een databank voor onze messenger en leer de vier basisopdrachten in SQL: SELECT, INSERT, UPDATE en DELETE.

Databank, tabel, rij en kolom
phpMyAdmin: het hulpmiddel waarmee we onze databank beheren
phpMyAdmin: het hulpmiddel waarmee we onze databank beheren

Een databank bewaart gegevens die blijven bestaan, ook als de server herstart. Ze bestaat uit tabellen. Elke tabel heeft kolommen (welke gegevens) en rijen (de effectieve gegevens, één rij per "ding").

Onze messenger krijgt twee tabellen:

berichten
id CHAR(36)auteur VARCHAR(100)tekst TEXT
a1f3c9d2-...SamHey, wat is de opdracht?
7be0a4c1-...LotteBladzijde 12!

Later voegen we een tabel gebruikers toe voor het inloggen.

Datatypes en de primaire sleutel
  • VARCHAR(100): korte tekst (maximaal 100 tekens)
  • TEXT: lange tekst
  • INT: geheel getal · DECIMAL(10,2): kommagetal (bv. bedragen)
  • DATETIME: datum en uur
  • TINYINT(1): ja/nee (0 of 1)

Elke tabel heeft een primaire sleutel (primary key): een kolom waarvan de waarde elke rij uniek herkenbaar maakt. Traditioneel is dat een getal dat vanzelf optelt (AUTO_INCREMENT: 1, 2, 3, ...). Wij gebruiken UUID's (bv. 3f2a91c4-8d1e-4b7a-9c53-12ab34cd56ef):

  • Ze zijn niet te raden. Bij een gewoon getal kan iemand in de URL ?id=5 naar ?id=6 veranderen en zo andermans gegevens proberen te zien.
  • Je kan ze in PHP genereren voor je opslaat, zonder de databank te vragen "welk nummer kreeg mijn rij?".
SQL: een tabel aanmakenCREATE TABLE berichten (
    id          CHAR(36)     NOT NULL PRIMARY KEY,   -- UUID, door PHP gegenereerd
    auteur      VARCHAR(100) NOT NULL,
    tekst       TEXT         NOT NULL,
    aangemaakt  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Werken in phpMyAdmin
  1. Ga naar http://localhost/phpmyadmin.
  2. Maak links een nieuwe databank messenger aan met sortering utf8mb4_unicode_ci (die ondersteunt ook emoji's en accenten).
  3. Open het tabblad SQL, plak de CREATE TABLE-opdracht hierboven en klik op Uitvoeren.
  4. Bekijk de tabel in het tabblad Structuur en voeg rijen toe via Invoegen of via SQL.
Kiezen we utf8 of utf8mb4?

Altijd utf8mb4. Het oudere "utf8" van MySQL kan geen emoji's bewaren.

De vier basisopdrachten

SELECT — gegevens ophalen:

SELECT * FROM berichten;                                  -- alle kolommen, alle rijen
SELECT auteur, tekst FROM berichten;                      -- enkel bepaalde kolommen
SELECT * FROM berichten WHERE auteur = 'Sam';             -- filteren
SELECT * FROM berichten ORDER BY aangemaakt DESC LIMIT 20; -- nieuwste eerst, maximaal 20
SELECT * FROM berichten WHERE tekst LIKE '%opdracht%';    -- tekst die "opdracht" bevat

INSERT — een nieuwe rij toevoegen:

INSERT INTO berichten (id, auteur, tekst)
VALUES ('3f2a91c4-8d1e-4b7a-9c53-12ab34cd56ef', 'Sam', 'Hey!');

UPDATE — bestaande rijen wijzigen:

UPDATE berichten SET tekst = 'Hey allemaal!' WHERE id = '3f2a91c4-8d1e-4b7a-9c53-12ab34cd56ef';

DELETE — rijen verwijderen:

DELETE FROM berichten WHERE id = '3f2a91c4-8d1e-4b7a-9c53-12ab34cd56ef';
Vergeet WHERE nooit bij UPDATE en DELETE!

DELETE FROM berichten; zonder WHERE verwijdert alle berichten. Idem voor UPDATE: zonder WHERE wijzig je elke rij. Controleer je voorwaarde eerst met een SELECT.

Tabellen koppelen (een blik vooruit)

Een bericht hoort bij een gebruiker. Daarom bewaar je in berichten geen naam maar het id van de gebruiker (een foreign key). Met een JOIN haal je de gegevens van beide tabellen samen op:

SQL: een JOINSELECT b.tekst, g.naam
FROM berichten b
INNER JOIN gebruikers g ON g.id = b.gebruiker_id
ORDER BY b.aangemaakt DESC;
🎯 Nu ben jij aan de beurt: Databanken en MySQL

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.

Verbind PHP met de databank en leer waarom we gegevens van buitenaf nóóit rechtstreeks in een SQL-opdracht plakken, maar altijd met prepared statements werken.

De databankverbinding met mysqli

PHP praat met MySQL via de klasse mysqli (herinner je het hoofdstuk over klassen: new mysqli(...) maakt een object). Dat object bewaren we in de variabele $conn (van connection). We maken het één keer aan in config.php en laden dat bestand met require_once overal waar we de databank nodig hebben:

api/config.php<?php
$dbServername = 'localhost';
$dbUsername   = 'root';
$dbPassword   = '';
$dbName       = 'messenger';

// Fouten in de databank worden echte exceptions (die we kunnen opvangen), i.p.v. stille waarschuwingen
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

$conn = new mysqli($dbServername, $dbUsername, $dbPassword, $dbName);
$conn->set_charset('utf8mb4');            // accenten en emoji's correct bewaren
mysqli of PDO?

PHP kent twee manieren om met MySQL te werken: mysqli en PDO. Wij gebruiken mysqli met prepared statements, zoals in onze eigen projecten. Alles wat je in dit hoofdstuk leert over prepared statements geldt trouwens ook voor PDO.

SQL-injectie: het probleem
xkcd 327 "Exploits of a Mom" (CC BY-NC 2.5): een schoolnaam die je tabel verwijdert
xkcd 327 "Exploits of a Mom" (CC BY-NC 2.5): een schoolnaam die je tabel verwijdert

Stel dat je een gebruiker zoekt op naam en de invoer van de bezoeker gewoon in je SQL plakt:

Doe dit NOOIT<?php
$naam = $body['naam'];     // komt van de bezoeker!
$result = $conn->query("SELECT * FROM gebruikers WHERE naam = '$naam'");   // GEVAARLIJK

Voert een kwaadwillende als naam x' OR '1'='1 in, dan wordt de opdracht:

SELECT * FROM gebruikers WHERE naam = 'x' OR '1'='1'

Die voorwaarde is altijd waar: alle gebruikers worden getoond. Met slimmere invoer kan iemand rijen wijzigen, wachtwoorden lezen of tabellen verwijderen (zoals in het stripverhaal hierboven). Dit heet SQL-injectie en staat al jaren in de lijst met gevaarlijkste websitefouten.

De oorzaak

De gegevens van de bezoeker en de SQL-opdracht worden door elkaar gemengd, waardoor de databank niet meer weet wat de opdracht is en wat gewone tekst.

De oplossing: prepared statements

Bij een prepared statement stuur je de databank eerst alleen de opdracht, met plaatsvervangers (placeholders, een ?) waar de gegevens moeten komen. Daarna stuur je de gegevens apart. De databank weet dan zeker dat de gegevens enkel gegevens zijn en nooit code, wat ze er ook in typen. Dit zijn steeds dezelfde vier stappen:

api/opdracht.php<?php
$sql = "SELECT `4_deadline` as deadlineOpdracht FROM `opdrachten` WHERE `klasId` = ? AND `schoolId` IS NULL LIMIT 1";

$statement = $conn->prepare($sql);            // 1. de opdracht met een ? als placeholder
$statement->bind_param("s", $klasId);         // 2. de gegevens koppelen (s = string)
$statement->execute();                        // 3. uitvoeren
$result = $statement->get_result();           // 4. het resultaat ophalen

while ($row = $result->fetch_assoc()) {       // elke rij als associatieve array
    echo $row['deadlineOpdracht'];
}

Zelfs met de naam x' OR '1'='1 zoekt de databank nu simpelweg een gebruiker die letterlijk zo heet, en vindt niets.

bind_param: één letter per vraagteken

Het eerste argument van bind_param() beschrijft het type van elke placeholder, in dezelfde volgorde:

  • s = string (tekst, ook UUID's en datums)
  • i = integer (geheel getal)
  • d = double (kommagetal)
  • b = blob (binaire gegevens)

Drie placeholders met tekst, tekst en een getal? Dan is het "ssi".

<?php
$sql = "SELECT * FROM berichten WHERE auteur = ? AND tekst LIKE ? ORDER BY aangemaakt DESC LIMIT ?";
$statement = $conn->prepare($sql);

$zoek   = '%' . $zoek . '%';                      // de % horen bij de GEGEVENS, niet bij de opdracht
$aantal = 20;
$statement->bind_param("ssi", $auteur, $zoek, $aantal);   // s, s, i: in dezelfde volgorde als de ?'s
$statement->execute();
$result = $statement->get_result();
Variabelen, geen waarden

bind_param() verwacht variabelen: bind_param("s", $naam) werkt, bind_param("s", 'Sam') niet. Zet een letterlijke waarde dus eerst in een variabele.

Resultaten ophalen
CodeGeeft je
$result->fetch_assoc()de volgende rij als associatieve array (of null als er geen meer zijn)
while ($row = $result->fetch_assoc())overloopt alle rijen één voor één
$result->fetch_all(MYSQLI_ASSOC)alle rijen in één keer als array van arrays
$result->num_rowshet aantal gevonden rijen
Rijen ophalen<?php
// Één rij verwacht (bv. een gebruiker zoeken op e-mail): geen while nodig
$sql = "SELECT id, naam FROM gebruikers WHERE email = ? LIMIT 1";
$statement = $conn->prepare($sql);
$statement->bind_param("s", $email);
$statement->execute();
$result = $statement->get_result();
$gebruiker = $result->fetch_assoc();          // een array, of null als er niets gevonden is

if ($gebruiker) {
    echo 'Welkom ' . $gebruiker['naam'];
}

// Een aantal tellen
$sql = "SELECT COUNT(*) AS aantal FROM berichten WHERE auteur = ?";
$statement = $conn->prepare($sql);
$statement->bind_param("s", $auteur);
$statement->execute();
$aantal = (int) $statement->get_result()->fetch_assoc()['aantal'];
Kolomnamen met een cijfer of spatie

Staat een kolomnaam tussen backticks (`4_deadline`), dan kan hij ook met een cijfer beginnen. Met AS geef je hem een handige naam. IS NULL schrijf je gewoon in de SQL: NULL is geen waarde die je bindt.

Toevoegen, wijzigen en verwijderen

Dezelfde aanpak, maar je hebt geen get_result() nodig. Omdat we UUID's gebruiken, maken we het id zelf vóór het invoegen met een hulpfunctie uit config.php:

api/config.php<?php
function uuidv4(): string
{
    $data = random_bytes(16);
    $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
    $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
api/berichten.php<?php
// INSERT
$id  = uuidv4();
$sql = "INSERT INTO berichten (id, auteur, tekst) VALUES (?, ?, ?)";
$statement = $conn->prepare($sql);
$statement->bind_param("sss", $id, $auteur, $tekst);
$statement->execute();

// UPDATE
$sql = "UPDATE berichten SET tekst = ? WHERE id = ?";
$statement = $conn->prepare($sql);
$statement->bind_param("ss", $nieuweTekst, $id);
$statement->execute();

// DELETE
$sql = "DELETE FROM berichten WHERE id = ?";
$statement = $conn->prepare($sql);
$statement->bind_param("s", $id);
$statement->execute();

echo $statement->affected_rows;        // hoeveel rijen werden gewijzigd of verwijderd
Kolomnamen en tabelnamen

Placeholders werken enkel voor waarden, niet voor tabel- of kolomnamen. Laat een bezoeker dus nooit rechtstreeks een kolomnaam bepalen. Kies liever uit een lijst die jij vastlegt.

🎯 Nu ben jij aan de beurt: PHP en MySQL: prepared statements

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.

Nu bouwen we alles samen tot een echte mini-messenger: berichten bewaren, tonen en verwijderen met jQuery, AJAX, JSON-antwoorden en prepared statements.

Wat gaan we bouwen?

Een pagina waarop je je naam en een bericht intypt. Elk bericht komt in de databank en verschijnt in een lijst, en je kan het ook verwijderen. Dit is het CRUD-patroon: Create, Read, Update, Delete: bijna elke webapplicatie is er een variant van.

De structuurmessenger/
├── index.php                <- de pagina (HTML + partials)
├── partials/
│   ├── header.php
│   └── footer.php
├── js/
│   └── berichten.js         <- jQuery: AJAX + de lijst tonen
└── api/
    ├── config.php           <- $conn, uuidv4(), json_out()
    └── berichten.php        <- lijst / opslaan / verwijderen (geeft JSON)

Eén API-bestand met meerdere acties (lijst, opslaan, verwijderen). De JavaScript vertelt in het data-object welke actie hij wil.

config.php: de hulpfuncties
api/config.php<?php
$dbServername = 'localhost';
$dbUsername   = 'root';
$dbPassword   = '';
$dbName       = 'messenger';

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);   // databankfouten worden exceptions
$conn = new mysqli($dbServername, $dbUsername, $dbPassword, $dbName);
$conn->set_charset('utf8mb4');

function json_out(array $data, int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data);
    exit;
}

function uuidv4(): string
{
    $data = random_bytes(16);
    $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
    $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
De API: berichten.php
api/berichten.php<?php
ob_start();                                   // vangt ongewenste uitvoer op zodat enkel de JSON overblijft
require_once __DIR__ . '/config.php';         // geeft ons $conn en de hulpfuncties

$action = trim($_POST['action'] ?? '');

// ── READ: alle berichten, nieuwste onderaan ────────────────────────────
if ($action === 'lijst') {
    $sql = "SELECT id, auteur, tekst, aangemaakt FROM berichten ORDER BY aangemaakt ASC LIMIT 100";
    $statement = $conn->prepare($sql);
    $statement->execute();
    $result = $statement->get_result();

    $berichten = [];
    while ($row = $result->fetch_assoc()) {
        $berichten[] = $row;                  // elke rij achteraan de lijst zetten
    }
    json_out(['ok' => true, 'berichten' => $berichten]);
}

// ── CREATE: een bericht bewaren ────────────────────────────────────────
if ($action === 'opslaan') {
    $auteur = trim($_POST['auteur'] ?? '');
    $tekst  = trim($_POST['tekst'] ?? '');

    if ($auteur === '' || $tekst === '') {
        json_out(['ok' => false, 'error' => 'Vul je naam en een bericht in.']);
    }

    $id  = uuidv4();
    $sql = "INSERT INTO berichten (id, auteur, tekst) VALUES (?, ?, ?)";
    $statement = $conn->prepare($sql);
    $statement->bind_param("sss", $id, $auteur, $tekst);
    $statement->execute();

    json_out(['ok' => true, 'id' => $id]);
}

// ── DELETE ─────────────────────────────────────────────────────────────
if ($action === 'verwijderen') {
    $id = trim($_POST['id'] ?? '');
    if ($id === '') {
        json_out(['ok' => false, 'error' => 'Geen bericht opgegeven.']);
    }

    $sql = "DELETE FROM berichten WHERE id = ?";
    $statement = $conn->prepare($sql);
    $statement->bind_param("s", $id);
    $statement->execute();

    json_out(['ok' => true]);
}

json_out(['ok' => false, 'error' => 'Onbekende actie.'], 400);
Wat valt op?
  • Elke tak eindigt met json_out(), dat het script stopt (exit).
  • We controleren de invoer altijd op de server, ook al deed de JavaScript het al. Een bezoeker kan de JavaScript omzeilen.
  • Elke databankvraag met gegevens van buitenaf is een prepared statement.
De pagina
index.php<?php require_once __DIR__ . '/api/config.php'; ?>
<!DOCTYPE html>
<html lang="nl">
<head>
    <meta charset="utf-8">
    <title>Messenger</title>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
</head>
<body>
    <?php include __DIR__ . '/partials/header.php'; ?>

    <main>
        <ul id="berichten"></ul>

        <input type="text" id="auteur" placeholder="Jouw naam">
        <input type="text" id="tekst"  placeholder="Typ een bericht">
        <button type="button" id="verstuur">Verstuur</button>
        <p id="melding" style="display:none"></p>
    </main>

    <?php include __DIR__ . '/partials/footer.php'; ?>
    <script src="js/berichten.js"></script>
</body>
</html>

Zie je: geen <form>, enkel invoervelden en een knop. De pagina zelf is bijna leeg; de JavaScript vult de lijst.

De JavaScript: berichten.js
js/berichten.js// Eén hulpfunctie voor alle API-oproepen: altijd POST, altijd een JSON-antwoord
function api(data, gelukt) {
    $.ajax({
        url: 'api/berichten.php',
        method: 'POST',
        data: data,
        dataType: 'json',
        success: function (antwoord) {
            if (antwoord.ok) {
                gelukt(antwoord);
            } else {
                toonMelding(antwoord.error);
            }
        },
        error: function () {
            toonMelding('De server gaf geen geldig antwoord.');
        }
    });
}

function toonMelding(tekst) {
    $('#melding').text(tekst).show();
}

function laadBerichten() {
    api({ action: 'lijst' }, function (antwoord) {
        const $lijst = $('#berichten').empty();
        antwoord.berichten.forEach(function (b) {
            const $li = $('<li>');
            $li.append($('<strong>').text(b.auteur + ': '));      // .text() -> veilig tegen HTML in berichten
            $li.append(document.createTextNode(b.tekst + ' '));
            $li.append($('<button type="button">').text('🗑️').on('click', function () {
                verwijder(b.id);
            }));
            $lijst.append($li);
        });
    });
}

function verstuur() {
    const data = {
        action: 'opslaan',
        auteur: $('#auteur').val().trim(),
        tekst:  $('#tekst').val().trim()
    };
    api(data, function () {
        $('#tekst').val('');
        $('#melding').hide();
        laadBerichten();
    });
}

function verwijder(id) {
    api({ action: 'verwijderen', id: id }, laadBerichten);
}

$(function () {                                   // draait zodra de pagina klaar is
    $('#verstuur').on('click', verstuur);
    $('#tekst').on('keydown', function (e) {
        if (e.key === 'Enter') verstuur();
    });
    laadBerichten();
    setInterval(laadBerichten, 5000);             // elke 5 seconden nieuwe berichten ophalen
});
.text() en niet .html()

Zet tekst die van gebruikers komt nooit met .html() in de pagina. Typt iemand <script>...</script> als bericht, dan zou dat bij iedereen uitgevoerd worden (XSS). .text() toont het gewoon als tekst. Meer hierover in het laatste hoofdstuk.

Uitbreiding: de joke-commando's

In de vorige versie van deze cursus antwoordde de messenger met een grap wanneer je joke intypte. Dat kan je nu ook: de server beslist en stuurt een tweede bericht mee terug. Voorbeeld in opslaan:

api/berichten.php (in de opslaan-actie)<?php
$grappen = [
    'Waarom werkt een programmeur nooit in het bos? Te veel bugs.',
    'Er zijn 10 soorten mensen: zij die binair begrijpen en zij die het niet begrijpen.',
    'Mijn wifi heet "Sorry, geen internet". Zo blijven de buren weg.',
];

if (strtolower($tekst) === 'joke') {
    $grap = $grappen[array_rand($grappen)];          // een willekeurig element uit de array
    $id     = uuidv4();
    $auteur = 'Grappenbot';
    $sql    = "INSERT INTO berichten (id, auteur, tekst) VALUES (?, ?, ?)";
    $statement = $conn->prepare($sql);
    $statement->bind_param("sss", $id, $auteur, $grap);
    $statement->execute();
}
🎯 Nu ben jij aan de beurt: Alles samenbrengen: de messenger

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.

Enkel wie ingelogd is mag berichten sturen. Je leert wachtwoorden veilig opslaan (hashing), een sessie starten en API's beveiligen, alles zonder formulieren.

Sessies: hoe onthoudt de server jou?

HTTP is stateless: elke request staat op zichzelf en de server "kent" je niet van vorige keer. Een sessie lost dat op: bij je eerste bezoek geeft de server je een willekeurig sessie-ID in een cookie (PHPSESSID). Jouw browser stuurt die cookie bij elke request mee, en de server bewaart aan zijn kant gegevens onder dat ID, bijvoorbeeld wie je bent.

sessies in het kort<?php
session_start();                    // altijd als allereerste, vóór er iets getoond wordt

$_SESSION['user_id'] = 42;          // onthouden dat deze bezoeker gebruiker 42 is
echo $_SESSION['user_id'];          // ook in een volgende request nog beschikbaar

session_destroy();                  // uitloggen: alles wissen
Wat staat in het cookie?

Enkel een willekeurig sessie-ID, nooit de gegevens zelf. De echte gegevens (zoals de user_id) blijven op de server en kunnen dus niet door de bezoeker aangepast worden.

Wachtwoorden: nooit in leesbare tekst!
Een hashfunctie zet invoer om naar een vaste code, en niet meer terug
Een hashfunctie zet invoer om naar een vaste code, en niet meer terug

Bewaar een wachtwoord nooit zoals het getypt wordt. Lekt je databank, dan zijn alle wachtwoorden meteen bekend (en mensen gebruiken hetzelfde wachtwoord op meerdere sites!). In plaats daarvan bewaren we een hash: een onomkeerbare codering.

hashing<?php
$hash = password_hash('geheim123', PASSWORD_DEFAULT);
// bv. $2y$10$q7v0Zt...   <- dit bewaar je in de databank (kolom VARCHAR(255))

var_dump(password_verify('geheim123', $hash));   // true
var_dump(password_verify('fout', $hash));        // false
  • password_hash() voegt automatisch een willekeurige "salt" toe: twee gebruikers met hetzelfde wachtwoord krijgen toch een verschillende hash.
  • Je kan een hash niet terugrekenen. Je kan enkel controleren met password_verify().
  • Gebruik nooit zelf md5() of sha1() voor wachtwoorden: die zijn veel te snel te kraken.
Registreren

De JavaScript stuurt de gegevens mee in het data-object (zonder form) en PHP antwoordt in JSON. De PHP controleert alles, kijkt of het e-mailadres nog vrij is en bewaart de hash:

js/auth.js$('#registreer').on('click', function () {
    const data = {
        action:     'registreer',
        naam:       $('#naam').val().trim(),
        email:      $('#email').val().trim(),
        wachtwoord: $('#wachtwoord').val()          // wachtwoorden niet trimmen!
    };
    $.ajax({
        url: 'api/auth.php',
        method: 'POST',
        data: data,
        dataType: 'json',
        success: function (antwoord) {
            $('#melding').text(antwoord.ok ? 'Account aangemaakt, je kan nu inloggen.' : antwoord.error).show();
        }
    });
});
api/auth.php<?php
ob_start();
session_start();
require_once __DIR__ . '/config.php';       // geeft ons $conn en de hulpfuncties

$action = trim($_POST['action'] ?? '');

if ($action === 'registreer') {
    $naam       = trim($_POST['naam'] ?? '');
    $email      = strtolower(trim($_POST['email'] ?? ''));
    $wachtwoord = $_POST['wachtwoord'] ?? '';

    if ($naam === '' || !filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($wachtwoord) < 8) {
        json_out(['ok' => false, 'error' => 'Vul een naam, een geldig e-mailadres en een wachtwoord van minstens 8 tekens in.']);
    }

    // Bestaat dit e-mailadres al?
    $sql = "SELECT id FROM gebruikers WHERE email = ?";
    $statement = $conn->prepare($sql);
    $statement->bind_param("s", $email);
    $statement->execute();
    $result = $statement->get_result();
    if ($result->num_rows > 0) {
        json_out(['ok' => false, 'error' => 'Dit e-mailadres is al in gebruik.']);
    }

    // Nieuw account: het wachtwoord bewaren we als hash
    $id   = uuidv4();
    $hash = password_hash($wachtwoord, PASSWORD_DEFAULT);
    $sql  = "INSERT INTO gebruikers (id, naam, email, wachtwoord) VALUES (?, ?, ?, ?)";
    $statement = $conn->prepare($sql);
    $statement->bind_param("ssss", $id, $naam, $email, $hash);
    $statement->execute();

    json_out(['ok' => true]);
}
Inloggen en uitloggen
api/auth.php<?php
// (verder in api/auth.php)

if ($action === 'login') {
    $email      = strtolower(trim($_POST['email'] ?? ''));
    $wachtwoord = $_POST['wachtwoord'] ?? '';

    $sql = "SELECT id, naam, wachtwoord FROM gebruikers WHERE email = ? LIMIT 1";
    $statement = $conn->prepare($sql);
    $statement->bind_param("s", $email);
    $statement->execute();
    $result = $statement->get_result();
    $gebruiker = $result->fetch_assoc();          // een array, of null als er niets gevonden is

    // Eén en dezelfde foutmelding: zo verklap je niet of het e-mailadres bestaat
    if (!$gebruiker || !password_verify($wachtwoord, $gebruiker['wachtwoord'])) {
        json_out(['ok' => false, 'error' => 'E-mailadres of wachtwoord klopt niet.']);
    }

    session_regenerate_id(true);                 // nieuw sessie-ID na het inloggen (tegen "session fixation")
    $_SESSION['user_id'] = $gebruiker['id'];
    $_SESSION['naam']    = $gebruiker['naam'];

    json_out(['ok' => true, 'naam' => $gebruiker['naam']]);
}

if ($action === 'uitloggen') {
    $_SESSION = [];
    session_destroy();
    json_out(['ok' => true]);
}
Zonder form: Enter-toets

In het wachtwoordveld luister je zelf naar Enter: $('#wachtwoord').on('keydown', e => { if (e.key === 'Enter') login(); });

API's beveiligen

Een API-bestand is gewoon een URL: iedereen kan er een request naartoe sturen, ook zonder jouw pagina. Elke API die gegevens toont of wijzigt controleert zelf of de bezoeker ingelogd is. Verstop een knop in de JavaScript is geen beveiliging.

api/config.php en api/berichten.php<?php
// in config.php
function requireApiLogin(): string
{
    if (empty($_SESSION['user_id'])) {
        json_out(['ok' => false, 'error' => 'Je bent niet aangemeld.'], 401);   // 401 = niet aangemeld
    }
    return $_SESSION['user_id'];
}

// bovenaan api/berichten.php (na session_start en config)
$userId = requireApiLogin();

// bij het opslaan bewaren we nu wie het schreef, uit de SESSIE, niet uit $_POST:
$id  = uuidv4();
$sql = "INSERT INTO berichten (id, gebruiker_id, tekst) VALUES (?, ?, ?)";
$statement = $conn->prepare($sql);
$statement->bind_param("sss", $id, $userId, $tekst);
$statement->execute();
Vertrouw nooit "wie ben ik" uit $_POST

Haal de identiteit van de gebruiker altijd uit $_SESSION. Zou je gebruiker_id uit $_POST overnemen, dan kan iedereen zich als een ander voordoen door dat getal aan te passen.

Op ons eigen platform gebruiken we trouwens geen wachtwoorden maar inlogcodes per e-mail: de server genereert een code, mailt ze en zet bij de juiste code dezelfde $_SESSION['user_id']. Alles wat je hier leerde over sessies en beveiligde API's geldt dus ook daar.

🎯 Nu ben jij aan de beurt: Registreren, inloggen en sessies

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.

Een werkende applicatie is niet hetzelfde als een veilige. Dit hoofdstuk zet de belangrijkste beveiligingsregels en foutafhandeling op een rij en sluit de cursus af.

De gouden regel
Vertrouw nooit gegevens van buitenaf

Alles wat via de browser binnenkomt ($_POST, URL's, cookies, invoervelden) kan door een bezoeker aangepast of verzonnen zijn. Controleer op de server, ga niet uit van wat je JavaScript doet.

Een korte checklist die je bij elke API overloopt:

  1. Is de gebruiker aangemeld en mag hij dit? (authenticatie + autorisatie)
  2. Zijn de gegevens geldig? (validatie)
  3. Gebruik ik prepared statements?
  4. Toon ik gegevens veilig (tegen XSS)?
  5. Lekt een foutmelding geen interne informatie?
Invoer valideren
validatie<?php
$email = strtolower(trim($_POST['email'] ?? ''));
$leeftijd = $_POST['leeftijd'] ?? null;

if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    json_out(['ok' => false, 'error' => 'Dit is geen geldig e-mailadres.']);
}
if (!is_numeric($leeftijd) || $leeftijd < 0 || $leeftijd > 120) {
    json_out(['ok' => false, 'error' => 'Geef een geldige leeftijd op.']);
}
if (mb_strlen($tekst) > 500) {
    json_out(['ok' => false, 'error' => 'Je bericht is te lang (max. 500 tekens).']);
}

trim() haalt spaties weg, filter_var() controleert veelvoorkomende types (e-mail, URL, getal) en mb_strlen() telt tekens correct (ook met accenten en emoji).

XSS: gevaarlijke tekst tonen

Cross-Site Scripting (XSS) gebeurt als tekst van een gebruiker als code in je pagina terechtkomt. Typt iemand <script>alert(1)</script> als bericht en toon je dat ongefilterd, dan draait die code bij iedereen die de pagina opent (en kan hij bv. sessies stelen).

PHP<?php
// In PHP: bij het TONEN altijd escapen
echo '<p>' . htmlspecialchars($bericht['tekst'], ENT_QUOTES, 'UTF-8') . '</p>';
// <script> wordt &lt;script&gt; en verschijnt als onschuldige tekst
JavaScript// In jQuery: .text() in plaats van .html()
$('<li>').text(b.tekst);            // veilig
$('<li>').html(b.tekst);            // GEVAARLIJK bij tekst van gebruikers
Waar escape je?

Bij het tonen, niet bij het opslaan. Bewaar de tekst zoals hij is en maak hem onschadelijk op het moment dat je hem in een pagina zet.

Autorisatie: mag deze gebruiker dit?

Ingelogd zijn is niet genoeg. Bij verwijderen moet je controleren of het bericht van deze gebruiker is, anders kan iemand met een gekend id andermans berichten wissen:

api/berichten.php<?php
// FOUT: elke ingelogde gebruiker kan elk bericht verwijderen
$sql = "DELETE FROM berichten WHERE id = ?";

// GOED: enkel je eigen bericht (het user-id komt uit de sessie!)
$sql = "DELETE FROM berichten WHERE id = ? AND gebruiker_id = ?";
$statement = $conn->prepare($sql);
$statement->bind_param("ss", $id, $userId);
$statement->execute();

if ($statement->affected_rows === 0) {
    json_out(['ok' => false, 'error' => 'Dit bericht bestaat niet of is niet van jou.'], 403);
}
Fouten afhandelen met try/catch

Dingen gaan mis: de databank ligt even plat, een query bevat een fout, ... Met try/catch vang je zo'n fout op en geef je een nette JSON-melding terug in plaats van een witte pagina of een technische foutmelding.

foutafhandeling<?php
try {
    $id  = uuidv4();
    $sql = "INSERT INTO berichten (id, auteur, tekst) VALUES (?, ?, ?)";
    $statement = $conn->prepare($sql);
    $statement->bind_param("sss", $id, $auteur, $tekst);
    $statement->execute();
    json_out(['ok' => true]);

} catch (mysqli_sql_exception $e) {
    error_log('Bericht opslaan mislukt: ' . $e->getMessage());        // technische details enkel in het logbestand
    json_out(['ok' => false, 'error' => 'Er ging iets mis, probeer het later opnieuw.'], 500);
}
Toon nooit $e->getMessage() aan de bezoeker

Foutmeldingen bevatten tabelnamen, kolommen en soms wachtwoorden van de databankverbinding. Log ze intern en toon de gebruiker een algemene boodschap. Dat is ook waarom we in config.php mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT) gebruiken: databankfouten worden echte exceptions (mysqli_sql_exception) die we kunnen opvangen.

Wat nu?

Je hebt nu alle bouwstenen van een echte webapplicatie in handen: PHP, includes, klassen, AJAX met JSON, MySQL met mysqli en prepared statements, sessies en beveiliging. Zo is ook dit platform opgebouwd. Volgende stappen in onze lessen:

  • Zelf een volledig project bouwen (zie ICT-projecten en de GIP)
  • Laravel: een framework dat veel van wat je nu zelf schreef (routes, databank, login) kant-en-klaar aanbiedt
  • Versiebeheer met Git
Blijf oefenen

Bouw de messenger zelf na, van een lege map. Voeg nadien extra dingen toe: berichten tussen twee gebruikers, een "laatst online"-status of avatars. Elke uitbreiding oefent dezelfde patronen.

🎯 Nu ben jij aan de beurt: Veilig en foutbestendig programmeren

Verwerk wat je hierboven las met de onderdelen hieronder. Ze horen bij deze theorie.