Skip to content

Realisatiedocument

Inleiding

Dit realisatiedocument vormt een vervolg op het projectplan en beschrijft de effectieve uitvoering van het stageproject. Waar het projectplan voornamelijk de probleemstelling, doelstellingen, scope en geplande aanpak vastlegde, focust dit document op de concrete realisatie van de uitgewerkte oplossing. Daarbij wordt toegelicht welke technische keuzes werden gemaakt, hoe de ontwikkelomgeving werd opgezet en op welke manier de verschillende onderdelen van het platform werden geïmplementeerd.

De realisatie situeert zich binnen de verdere ontwikkeling van een webplatform bij iO, met specifieke aandacht voor de uitbouw van het adminpaneel. Binnen deze context lag de focus op het beheer van organisaties en gebruikers, aangevuld met gedeelde functionaliteiten zoals filtering, paginering, vertalingen, bevestigingsdialogen, flashberichten, dashboardstatistieken en logging. Daarnaast werd ook frontendfunctionaliteit uitgewerkt in Vue, namelijk een taalwisselaar, een profielpagina voor gebruikersbeheer, functionaliteit voor taal- en avatarbeheer en een module voor het genereren van PDF-rapporten.

Dit document is opgebouwd uit drie grote delen. In het eerste deel, de analyse, worden de belangrijkste technologiekeuzes en de gebruikte ontwikkelomgeving besproken. Hierbij wordt toegelicht waarom gekozen werd voor onder andere Symfony, Vue, Lando, GitLab en Jira, en hoe codekwaliteit binnen het project werd bewaakt.

Het tweede deel behandelt de eigenlijke realisatie. Per functionaliteit wordt beschreven welke technische aanpak werd gevolgd, welke componenten of services werden ontwikkeld en hoe deze bijdragen aan de werking van het platform. Binnen dit deel wordt ook ingegaan op de kwaliteitscontrole aan de hand van unit tests en end-to-end tests.

Tot slot vormt het besluit een terugblik op de uitgevoerde werkzaamheden en op de manier waarop de vooropgestelde doelstellingen uit het projectplan werden vertaald naar een werkende technische oplossing.

Analyse

Technologiekeuzes

Voor de ontwikkeling van het platform werd gekozen voor Symfony als backend-framework en Vue 3 als frontend-framework. Alternatieven zoals Laravel, React of een volledig headless architectuur met een aparte API-laag werden niet weerhouden. De keuze voor deze specifieke stack werd gemaakt door de verantwoordelijke developer en is gebaseerd op een combinatie van technische en praktische overwegingen (Y. Maerschalck & T. Vermeiren, persoonlijke communicatie, 2026).

Een eerste belangrijke overweging is de monolithische projectstructuur: backend en frontend leven in één en dezelfde repository en worden samen gedeployed. Dit vermijdt de overhead van twee aparte deployments, afzonderlijke repositories en meerdere teams. Authenticatie en sessiebeheer worden gedeeld tussen Symfony en Vue, waardoor geen aparte authenticatielaag opgezet dient te worden voor een API en een headless applicatie.

Daarnaast laat de combinatie van Symfony en Vue toe om de complexiteit te verdelen naargelang de context. Beheerpagina's met eenvoudige CRUD-functionaliteit worden uitgewerkt in Twig, de templating engine van Symfony waarmee server-side HTML-rendering mogelijk is, wat sneller te ontwikkelen is. Vue wordt enkel ingezet waar interactiviteit en dynamisch gedrag vereist zijn. Dit resulteert in een lagere totale ontwikkeltijd en een vlakkere leercurve voor developers die met beide technologieën vertrouwd zijn.

Vertalingen kunnen centraal beheerd worden op één locatie en hoeven niet op twee afzonderlijke plaatsen onderhouden te worden, wat de onderhoudbaarheid ten goede komt.

Tot slot speelt ook budgetbeheer een rol: een volledig headless architectuur brengt doorgaans meer overhead met zich mee en resulteert in hogere ontwikkelkosten. De gekozen stack laat toe correctere inschattingen te maken binnen een beperkt budget.

Voor databasebeheer wordt gebruikgemaakt van phpMyAdmin, een gratis webgebaseerde tool die standaard is opgenomen in de Lando-configuratie. Dit maakt een externe databasetool overbodig binnen de lokale ontwikkelomgeving.

Ontwikkelomgeving

Voorafgaand aan de eigenlijke uitvoering werd een ontwikkelomgeving opgezet die aansluit bij de werkwijze van iO. De gebruikte tools zijn standaard binnen het team. Het project zelf werd opgezet door de verantwoordelijke developer, waarna de lokale omgeving zelfstandig werd geconfigureerd op basis van de voorziene documentatie. Hieronder worden de verschillende onderdelen van de omgeving toegelicht.

PHPStorm

Als geïntegreerde ontwikkelomgeving (IDE) wordt gebruikgemaakt van PHPStorm, een editor van JetBrains die specifiek gericht is op PHP-ontwikkeling. PHPStorm biedt ingebouwde ondersteuning voor Symfony, Twig en Vue, wat de ontwikkeling aanzienlijk vereenvoudigt.

GitLab

Voor versiebeheer en code review wordt gebruikgemaakt van GitLab. Per ticket wordt een aparte branch aangemaakt volgens de naamgevingsconventies van het team. Bij de eerste commit wordt meteen een merge request aangemaakt in draft-modus, zodat het team de voortgang kan volgen zonder dat de code al als afgewerkt wordt beschouwd.

Wanneer een ticket volledig is uitgewerkt, wordt de draft-modus uitgeschakeld. Yves of Tony voeren dan een code review uit en maken threads aan bij opmerkingen of vragen. Na het verwerken van een opmerking wordt hierop geantwoord in de thread, waarna de reviewer de thread markeert als resolved. Een merge request kan pas worden samengevoegd met de hoofdbranch wanneer alle threads gesloten zijn. Dit is technisch afgedwongen om codekwaliteit te garanderen (Y. Maerschalck & T. Vermeiren, persoonlijke communicatie, 2026).

Jira

Taakbeheer verloopt via Jira. De tickets voor het project zijn reeds aangemaakt. Voor de stagiair zijn aparte tickets voorzien die verwijzen naar de corresponderende projecttickets. Het loggen van gewerkte tijd gebeurt op deze afzonderlijke tickets, zodat de uren niet als factureerbaar worden meegerekend voor de klant.

In de commentaarsectie van elk ticket wordt bijgehouden welke deeltaken reeds zijn afgerond, welke nog in uitvoering zijn en welke nog moeten worden opgepikt. Dit biedt het team ook na afloop van de stage een duidelijk overzicht van wat werd opgeleverd en wat eventueel nog verder uitgewerkt dient te worden.

Lokale omgeving

Het project maakt gebruik van Lando, een lokale ontwikkelomgeving die draait op basis van Docker-containers. De initiële projectopzet werd voorzien door de verantwoordelijke developer en gedocumenteerd in de README van de repository (iO, 2026). Na het clonen van de repository en het uitvoeren van de installatiestappen is de omgeving beschikbaar.

De dagelijkse opstartprocedure bestaat uit twee stappen. Eerst wordt de Docker-container opgestart via lando start, waardoor de webserver, database en overige services beschikbaar komen. Vervolgens wordt via lando frontend-dev het frontend-proces opgestart, wat de assets compileert en wijzigingen automatisch opvolgt (Lando, 2025).

Codekwaliteit

Om een consistente en kwalitatieve codebase te bewaken, zijn een aantal controles geautomatiseerd via git hooks die bij elke commit worden uitgevoerd.

PHPStan voert statische code-analyse uit op de PHP-code (PHPStan, 2026). Het detecteert mogelijke fouten zoals onjuiste types, onbestaande methodes of onduidelijke structuren, zonder dat de code effectief uitgevoerd wordt. Bij een commit krijgt de ontwikkelaar onmiddellijk feedback over eventuele overtredingen, die verholpen moeten worden alvorens de commit kan worden doorgezet.

Prettier wordt ingezet als code formatter en zorgt ervoor dat de opmaak van de code overal consistent is. Dit omvat onder andere het verwijderen van overbodige witruimte en het uniformiseren van inspringing en stijl.

Tot slot wordt ook gecontroleerd op de aanwezigheid van debug code. Wanneer er nog console.log-statements of dump-aanroepen in de code aanwezig zijn, geeft het systeem een waarschuwing. Dit voorkomt dat tijdelijke debugcode per ongeluk in de productiecodebase terechtkomt.

Realisatie

Dit hoofdstuk beschrijft de functionaliteiten die werden ontwikkeld tijdens de stage. De focus lag op de uitbouw van het adminpaneel, meer bepaald de beheerschermen voor organisaties en gebruikers. Per onderdeel wordt de technische aanpak toegelicht, aangevuld met relevante codefragmenten en schermafbeeldingen.

Backend - Symfony

Organisatiebeheer

Het beheerscherm voor organisaties biedt een volledig CRUD-overzicht: beheerders kunnen organisaties raadplegen, aanmaken, bewerken en verwijderen. Het overzicht toont per organisatie de naam, code, het adres, het land, de contactgegevens en de actieve status.

Overzicht organisaties

Overzicht van alle organisaties (dummy data)
Formulieren met Symfony Form Type

Voor het aanmaken en bewerken van organisaties werd gebruikgemaakt van Symfony Form Type (Symfony, z.d.). Er werd een OrganisationType opgesteld op basis van de Organisation-entiteit, zodat formulieropbouw, validatie en verwerking centraal beheerd worden.

Een belangrijk aandachtspunt hierbij is het code-veld: dit veld wordt mee opgenomen in de URL bij detailweergave en mag na aanmaak niet meer worden aangepast. Bij bewerken wordt het veld daarom als readonly ingesteld via een is_edit-optie die aan het formulier wordt meegegeven.

php
->add('code', null, [
    'label' => 'admin.organisation.field.code.label',
    'required' => true,
    'attr' => $options['is_edit'] ? ['readonly' => true, 'class' => 'cursor-not-allowed bg-gray-100 text-gray-500'] : [],
])

Bewerkingsformulier organisatie met uitgeschakeld code-veld

Bij het bewerken is het code-veld uitgeschakeld om onbedoelde wijzigingen te voorkomen

De overige formuliervelden, waaronder het landveld, worden eveneens via het Form Type beheerd. De formuliertemplate is generiek gehouden en wordt hergebruikt voor zowel het aanmaak- als het bewerkingsscherm. Hierdoor blijft de controller beperkt tot het verwerken van de request en het doorsturen naar het overzicht na een succesvolle submit.

aanmaakformulier organisatie

Formulier om een nieuwe organisatie aan te maken
Mutatielogging

Bij elke schrijfoperatie op de database wordt automatisch een mutatielog aangemaakt via de AdminMutationLogService. Elke logentry slaat op wie de actie uitvoerde, op welke entiteit, welk type wijziging het betrof en wat de toestand was vóór en ná de wijziging. Dit maakt het achteraf mogelijk om elke aanpassing te traceren.

De toestand van een entiteit vóór een wijziging wordt vastgelegd via een snapshot()-methode op de entiteit zelf, die de huidige waarden serialiseert naar een array. Bij een aanmaak is de old_values gelijk aan NULL; bij een update bevatten zowel old_values als new_values een JSON-representatie van de gewijzigde velden. In de database ziet een logentry er als volgt uit:

Mutatielogboek in de database

Een mutatielog-entry in de database na het deactiveren van een gebruiker

De logging wordt consequent toegepast bij alle wijzigingen in het adminpaneel: o.a. het aanmaken, bewerken en verwijderen van organisaties, maar ook bij statuswijzigingen van gebruikers. De auditloggegevens die hieruit voortvloeien worden later ook weergegeven in de loggingmodule van het adminpaneel (zie hoofdstuk Logging). De aanroep vermeldt steeds de actienaam, de betrokken entiteitsklasse, het entiteits-ID en het type wijziging:

php
$this->adminMutationLogService->logMutation(
    'admin_organisation_edit',
    $user,
    Organisation::class,
    $organisation->getId(),
    ChangeType::UPDATE,
    $before,
    $organisation->snapshot(),
    ['code' => $organisation->getCode()],
);
Verwijderen

Bij destructieve acties, zoals het verwijderen van een organisatie, wordt de beheerder gevraagd de actie expliciet te bevestigen via een dialoogvenster. Dit voorkomt onbedoelde verwijderingen. De technische uitwerking hiervan is gedeeld over het volledige adminpaneel en wordt toegelicht in Bevestigingsdialogen met Stimulus en SweetAlert2.


Gebruikersbeheer

Het gebruikersoverzicht biedt de beheerder een leesbaar overzicht van alle gebruikers in het systeem. Het is bewust beperkt in functionaliteit: gebruikers kunnen niet worden aangemaakt of verwijderd via het adminpaneel. Gegevensbeheer is voorbehouden aan de gebruikers zelf, via een specifieke profielpagina in de frontend. Het adminpaneel dient enkel ter opvolging en statusbeheer.

Het overzicht toont per gebruiker een avatar, naam, e-mailadres, gekoppelde organisatie, toegewezen rol, actieve status en aanmaakdatum. Via de zoekbalk kan gezocht worden op naam, e-mailadres of rijksregisternummer. Daarnaast zijn er drie dropdowns voorzien om te filteren op actieve status, rol en organisatie. Een bijkomende checkbox laat toe om enkel gebruikers te tonen die nog niet aan een organisatie zijn gekoppeld.

Overzicht gebruikers

Overzicht van alle gebruikers met filter- en zoekopties
Detailpagina

Via de "Zie details"-link in het overzicht navigeert de beheerder naar de detailpagina van een individuele gebruiker. Deze pagina toont de persoonlijke gegevens van de gebruiker: voornaam, achternaam, geboortedatum, geboorteplaats, geslacht, aanspreking, rijksregisternummer en nationaliteit, als read-only velden. Het aanpassen van deze gegevens is hier bewust niet mogelijk, om te vermijden dat een beheerder gegevens overschrijft die door de gebruiker zelf beheerd worden.

Onder de persoonlijke gegevens wordt, uitsluitend voor zorgverleners, een overzicht getoond van de organisaties waaraan de gebruiker is gekoppeld, inclusief de organisatiecode, het adres, de actieve status en de aanmaakdatum. Voor gewone gebruikers, patiënten en beheerders is dit blok niet zichtbaar, aangezien organisatiekoppelingen enkel relevant zijn in de context van een zorgverlener. De organisatienamen zijn klikbaar en leiden rechtstreeks naar de detailpagina van de betreffende organisatie.

Detailpagina gebruiker

Detailpagina van een gebruiker met persoonlijke gegevens en gekoppelde organisaties
Toggle actief/inactief

De actieve status van een gebruiker kan rechtstreeks vanuit het gebruikersoverzicht worden aangepast. Hiervoor werd een aparte route in de UserController voorzien. Bij een statuswijziging wordt eerst de oorspronkelijke toestand van de gebruiker opgeslagen, waarna de actieve status wordt omgekeerd en de wijziging wordt gepersisteerd.

php
#[Route('{id}/toggle-active', name: 'toggle_active', methods: ['POST'])]
public function toggleActive(User $user, EntityManagerInterface $entityManager, #[CurrentUser] User $currentUser): Response
{
    $before = $user->snapshot();
    $user->setIsActive(!$user->isActive());
    $entityManager->flush();

    $this->adminMutationLogService->logMutation(
        'admin_user_toggle_active',
        $currentUser,
        User::class,
        $user->getId(),
        ChangeType::UPDATE,
        $before,
        $user->snapshot(),
        ['isActive' => $user->isActive()],
    );

    return $this->redirectToRoute('admin_user_index');
}

De wijziging wordt ook opgenomen in de mutatielog, zodat achteraf zichtbaar blijft wie de status heeft aangepast en wat de nieuwe toestand is. Net zoals bij het verwijderen van organisaties wordt de actie pas uitgevoerd nadat de beheerder ze expliciet bevestigt via de gedeelde bevestigingsdialoog (zie Bevestigingsdialogen met Stimulus en SweetAlert2).

Zoeken op geëncrypteerde data

Bepaalde gebruikersgegevens, zoals het rijksregisternummer, worden versleuteld opgeslagen in de database. Dit betekent dat een directe LIKE-zoekopdracht op de ruwe databasewaarden niet mogelijk is. Om toch op rijksregisternummer te kunnen zoeken, wordt de zoekopdracht eerst verwerkt vooraleer ze naar de database wordt gestuurd. In de repository wordt eerst gecontroleerd of de zoekwaarde een geldig rijksregisternummer kan zijn, op basis van het aantal cijfers na het verwijderen van niet-numerieke tekens. Is dat het geval, dan wordt de ingegeven waarde via dezelfde encryptieservice omgezet naar een blind index, die vervolgens wordt opgezocht in de database:

php
if (null !== $searchQuery) {
    $searchQuery = trim($searchQuery);
    $checkIfNationalIdentifier = preg_replace('/[^0-9]/', '', $searchQuery);
    if (null !== $checkIfNationalIdentifier && strlen($checkIfNationalIdentifier) === 11) {
        $result = $this->getNationalIdentifierIndex($checkIfNationalIdentifier);
        $qb->andWhere('u.nationalIdentifierIndex = :searchNationalIdentifier')
           ->setParameter('searchNationalIdentifier', $result);
    } else {
        $qb->andWhere('u.oneLoginId LIKE :search OR cp.email LIKE :search OR cp.firstName LIKE :search OR cp.lastName LIKE :search')
           ->setParameter('search', '%'.$searchQuery.'%');
    }
}

De getNationalIdentifierIndex-methode in de repository maakt gebruik van een EncryptionService die een deterministische blind index genereert op basis van een geheime sleutel uit de .env-configuratie.

Doordat de blind index deterministisch is, dezelfde invoer levert altijd dezelfde uitvoer, is een exacte match in de database mogelijk zonder de data te ontsleutelen. De zoekervaring voor de beheerder blijft hierdoor transparant en zonder zichtbare complexiteit.

Autocomplete met Symfony UX Autocomplete

Op het gebruikersscherm kan gefilterd worden op organisatie. Omdat de lijst met organisaties in de toekomst aanzienlijk kan groeien, werd de organisatiedropdown uitgerust met autocomplete-functionaliteit via de Symfony UX Autocomplete-bundle (SensioLabs, z.d.-a). De implementatie vereiste slechts een minimale aanpassing aan de bestaande select:

html
<select data-model="selectedOrganisation"
        {{ stimulus_controller('symfony/ux-autocomplete/autocomplete') }}>
    <option value="">{{ 'admin.user.filter.all_organisations'|trans|capitalize }}</option>
    {% for organisation in this.organisations %}
        <option value="{{ organisation.id }}" {{ selectedOrganisation == organisation.id ? 'selected' : '' }}>
            {{ organisation.name }}
        </option>
    {% endfor %}
</select>

dropdown met autocomplete

Dropdown met organisaties met zoekveld met autocomplete functie (dummy data)

Gedeelde functionaliteiten

Beide beheerschermen maken gebruik van een aantal gedeelde technische oplossingen die hieronder worden toegelicht.

Live filtering met Symfony UX Live Components

Zoeken en filteren verloopt zonder paginalading via Symfony UX Live Components (SensioLabs, z.d.-b). Per scherm werd een grid-component aangemaakt met LiveProp-eigenschappen die automatisch de repository aanspreken bij elke wijziging:

php
#[LiveProp(writable: true, onUpdated: 'onFilterChanged')]
public ?string $activeFilter = '1';

#[LiveProp(writable: true, onUpdated: 'onFilterChanged')]
public ?string $searchQuery = null;

De filterwaarden worden doorgegeven aan de repository, die een query opbouwt op basis van de actieve parameters:

php
if (null !== $searchQuery) {
    $searchQuery = trim($searchQuery);
    $qb->andWhere('o.name LIKE :search OR o.code LIKE :search')
       ->setParameter('search', '%'.$searchQuery.'%');
}

In de template worden de filtercomponenten gebonden aan de LiveProps via het data-model-attribuut:

html
<select data-model="activeFilter">
    <option value="1">{{ 'admin.organisation.filter.active'|trans|capitalize }}</option>
    <option value="0">{{ 'admin.organisation.filter.inactive'|trans|capitalize }}</option>
</select>

voorbeeld zoekfilters op gebruikerspagina

Voorbeeld van enkele (herbruikbare) zoekfilters op de gebruikerspagina
Paginering met Pagerfanta

Voor de paginering van resultaten wordt gebruikgemaakt van Pagerfanta, een PHP-bibliotheek die paginering afhandelt op basis van een Doctrine-query (BabDev, 2026; SymfonyCasts, z.d.). Pagerfanta biedt ingebouwde methoden om onder andere het totaal aantal resultaten, het huidige paginanummer en het bereik van de huidige pagina op te vragen. Hierdoor kon de weergave "Toont 1 tot 10 van 37 resultaten" eenvoudig worden geïmplementeerd zonder bijkomende rekenlogica.

Om de paginering herbruikbaar te maken over meerdere Live Components, werd een PaginationTrait opgesteld. Deze trait beheert de huidige pagina en de paginagrootte via LiveProps en voorziet acties om tussen pagina's te navigeren:

php
trait PaginationTrait
{
    #[LiveProp(writable: true)]
    public int $currentPage = 1;

    #[LiveProp(writable: true)]
    public int $limit = 10;

    #[LiveAction]
    public function nextPage(): void
    {
        ++$this->currentPage;
    }

    public function onFilterChanged(): void
    {
        $this->currentPage = 1;
    }
}

Wanneer een filter of zoekopdracht wijzigt, wordt automatisch teruggekeerd naar pagina 1. Dit voorkomt dat een gebruiker op een niet-bestaande pagina terechtkomt wanneer het aantal resultaten verandert.

De pagineringstemplate werd uitgewerkt als herbruikbare partial die in meerdere overzichtspagina's wordt opgenomen via een include. Daarnaast wordt gebruikgemaakt van een sliding window rond de huidige pagina, waarbij tussenliggende pagina's vervangen worden door een -afkorting om de navigatie overzichtelijk te houden bij een groot aantal pagina's.

paginering

Voorbeeld van paginering met Pagerfanta
Vertalingen

Het platform ondersteunt twee talen: Nederlands (be-nl) en Frans (be-fr). De vertaalinfrastructuur werd reeds opgezet binnen het project en maakt gebruik van YAML-bestanden per taal. Vertalingen worden georganiseerd per domein en functionaliteit, zoals admin.organisation en admin.user.

In Twig-templates worden vertaalsleutels opgeroepen via de trans-filter:

html
{{ 'admin.organisation.action.save'|trans|capitalize }}

Naast de YAML-bestanden beschikt het adminpaneel ook over een beheerscherm waarmee vertalingen rechtstreeks aangepast kunnen worden zonder wijzigingen in de codebase.

admin translations

Voorbeeld van vertalingen voor een administrator

Tijdens de stage werd actief bijgedragen aan het aanvullen van de vertaalbestanden voor alle nieuw ontwikkelde functionaliteiten. Voor nieuwe componenten, labels en meldingen werden telkens Nederlandstalige en Franstalige vertalingen toegevoegd.

Bevestigingsdialogen met Stimulus en SweetAlert2

Voor destructieve of onomkeerbare acties, zoals het deactiveren van een gebruiker of het verwijderen van een record, wordt een bevestigingsdialoog getoond. Hiervoor werd SweetAlert2 geïntegreerd als bibliotheek (SweetAlert2, z.d.), aangestuurd via een herbruikbare Stimulus controller (Hotwire, 2026).

De controller onderschept het standaard submit-event van een formulier, toont een SweetAlert2-dialoog en dient het formulier pas in wanneer de gebruiker de actie bevestigt. De teksten voor de dialoog — titel, bevestigingstekst en knoplabels — worden via data-attributen meegegeven vanuit de template. Daardoor blijft de controller generiek en kan hij op meerdere plaatsen in het adminpaneel worden gebruikt zonder aanpassing aan de JavaScript-code.

bevestigingsdialoog voor deactiveren user

De SweetAlert2-bevestigingsdialoog die verschijnt bij destructieve acties in het adminpaneel
Flashberichten als Twig component

Flashberichten worden gebruikt om de beheerder te informeren over het resultaat van een actie, zoals het succesvol opslaan van een formulier of een foutmelding. Initieel werd dit per template herhaald als inline Twig-code. Naarmate het aantal pagina's groeide, werd dit al snel een bron van duplicatie. In een eerste refactoring werd de herhaalde code verplaatst naar een Twig partial. Dit loste de duplicatie op, maar de logica — inclusief het automatisch verdwijnen van berichten via JavaScript — bleef gemengd met de markup. De definitieve oplossing maakt gebruik van een Twig component gecombineerd met een Stimulus controller. Het component haalt de flashberichten op uit de sessie en maakt ze beschikbaar aan de template:

php
#[AsTwigComponent(name: 'flash_messages', template: 'partials/admin/flash_messages.html.twig')]
readonly class FlashMessagesComponent
{
    public function __construct(private RequestStack $requestStack) {}

    /** @return array<string, string[]> */
    public function getMessages(): array
    {
        $session = $this->requestStack->getSession();
        assert($session instanceof FlashBagAwareSessionInterface);

        return [
            'success' => $session->getFlashBag()->get('success'),
            'error'   => $session->getFlashBag()->get('error'),
        ];
    }
}

De template positioneert de berichten als een fixed overlay rechtsonder in het scherm. Elk bericht bevat een sluitknop en een voortgangsbalk die aangeeft hoelang het bericht nog zichtbaar zal zijn.

De bijhorende Stimulus flash_controller (Hotwire, z.d.) beheert de levensduur van elk bericht: bij het verbinden van het element start een CSS-transitie op de voortgangsbalk, waarna het bericht na vier seconden automatisch verdwijnt. De beheerder kan een bericht ook manueel sluiten via de sluitknop.

Het resultaat is een volledig herbruikbare, visueel verzorgde notificatielaag die op elke pagina beschikbaar is via één enkele component-aanroep in de basistemplate.

Succesmelding rechtsonder in het scherm

Flashbericht met automatisch verdwijnende voortgangsbalk, zichtbaar rechtsonder in het adminpaneel

Dashboard

Op de startpagina van het adminpaneel werden statistiekentegels toegevoegd die in één oogopslag een overzicht bieden van de actieve gegevens in het systeem. De weergegeven statistieken omvatten onder andere:

  • het aantal actieve organisaties,
  • het aantal actieve gebruikers,
  • het aantal actieve vragenlijsten,
  • het aantal actieve talen,
  • loggingstatistieken voor auditlogs en actionlogs.

Elke tegel is aanklikbaar en leidt rechtstreeks naar het bijhorende beheerscherm. Voor loggingstatistieken worden bijkomende URL-parameters gebruikt zodat automatisch het correcte tabblad en de juiste datumfilter geopend worden.

De implementatie maakt gebruik van een herbruikbaar Twig component (DashboardStatsComponent) dat de statistieken ophaalt via de verschillende repositories en deze als overzichtelijke tegels rendert binnen het dashboard.

Dashboard met statistiekentegels voor organisaties, gebruikers, vragenlijsten, talen en loggingdata

Het adminpaneel-dashboard met klikbare statistiekentegels per entiteit, aangevuld met loggingstatistieken en icoontjes

Logging

Elke actie die een impact heeft op de database wordt gelogd in één van twee aparte logboeken: de auditlog registreert domeinwijzigingen met oude en nieuwe waarden, terwijl de actionlog flow-events en technische acties bijhoudt zoals onder andere inlogpogingen en synchronisaties met externe systemen. Om beheerders inzicht te geven in deze data, werd een logging-sectie uitgebouwd in het adminpaneel.

Structuur en navigatie

De loggingpagina is opgebouwd als een tabinterface met drie tabbladen: een samenvattingstab met grafieken, een auditlogboek en een actielogboek. De tabwisseling verloopt via een Stimulus controller die de actieve tab in de URL bijhoudt als queryparameter, zodat de juiste tab actief blijft bij een herlaad of bij het delen van een link. Rechtsboven wordt per tab een live statusteller getoond met het aantal logs vandaag en het totale aantal, dat automatisch wordt bijgewerkt wanneer filters worden aangepast via een custom Stimulus controller die luistert naar een logging:stats-updated event.

Tabbladen en statusteller

Tabbladen en statusteller
Visualisaties

De samenvattingstab toont twee lijngrafieken opgebouwd met Symfony UX Chart.js (Chart.js, 2025), een integratie van de Chart.js-bibliotheek in het Symfony-ecosysteem. De grafieken worden opgebouwd in een LoggingChartsComponent, een Live Component waarbij de tijdsperiode filterbaar is via snelknoppen (laatste 7, 14 of 30 dagen) of een datuminput. Bij een filterwijziging herberekent het component de data en rendert Chart.js de grafiek opnieuw zonder paginalading.

De eerste grafiek toont het totaal aantal audit- en actionlog-entries per dag over de geselecteerde periode. De tweede grafiek splitst de actionlog op naar type: logingebeurtenissen, fouten en synchronisaties met externe systemen.

Samenvattingstab van de loggingpagina met twee lijngrafieken

De samenvattingstab toont het verloop van audit- en actionlog-entries over de geselecteerde periode
Auditlogboek

Het auditlogboek toont alle domeinwijzigingen in een gepagineerde tabel. Per rij worden het entiteitstype, het wijzigingstype, de gewijzigde velden en de uitvoerende gebruiker getoond. De wijzigingen worden weergegeven als een compacte diff: bij een update worden de oude waarde rood doorgestreept en de nieuwe waarde groen getoond naast elkaar. Bij een aanmaak worden enkel de nieuwe waarden getoond, bij een verwijdering enkel de oude.

Auditlogboek met diff-weergave van gewijzigde velden

Het auditlogboek toont wijzigingen als een compacte diff met kleurcodering per wijzigingstype

Filtering verloopt via een AuditLogGridComponent met LiveProps voor wijzigingstype, entiteitstype, gebruikersrol en datumbereik. Snelfilters voor vandaag, de laatste 7 dagen en de laatste 14 dagen zijn beschikbaar als knoppen. Actieve filters worden getoond als badges met een knop om alle filters in één klik te wissen.

De detailpagina van een auditlog-entry toont een volledige tabel met alle gewijzigde velden naast elkaar, met oude en nieuwe waarden per veld.

Detailpagina van een auditlog-entry

Detailpagina van een auditlog-entry met volledige diff-weergave per veld
Actielogboek

Het actielogboek toont technische flow-events zoals inlogpogingen, authenticatiefouten en synchronisaties met externe systemen. Naast de standaardfilters op entiteitstype, gebruikersrol en datumbereik beschikt het actielogboek over drie voorgedefinieerde presets: mislukte acties, logingebeurtenissen en synchronisaties met externe systemen. Een preset combineert een LIKE-filter op de actienaam met een reset van de overige filters, zodat in één klik een gerichte selectie wordt gemaakt.

Waar het entiteitstype overeenkomt met een beheerde entiteit in het adminpaneel, zoals User, Organisation of Questionnaire, is de entiteitsnaam in de tabel klikbaar en leidt deze rechtstreeks naar het bijhorende beheerscherm.

Actielogboek met snelfilters en actieve filterbadges

Het actielogboek met snelfilterpresets en een overzicht van login- en foutgebeurtenissen

De detailpagina van een actionlog-entry toont de volledige metadata van de gebeurtenis, waaronder het IP-adres, de gebruikersagent, de route en eventuele foutinformatie.

Detailpagina van een actionlog-entry

Detailpagina van een actionlog-entry met volledige metadata van de gebeurtenis

Frontend - Vue

Taalverwisselaar

De taalwisselaar stelt de gebruiker in staat om de actieve taal van het platform te wijzigen zonder de huidige pagina te verlaten. De backend voorziet hiervoor een API-endpoint dat de beschikbare talen ophaalt.

De Vue-component LocaleSwitcher.vue bepaalt de actieve taal op basis van het URL-pad en haalt bij het laden de beschikbare talen op via een API-aanroep. Wanneer de gebruiker een andere taal selecteert, wordt de locale in het URL-pad vervangen waarna de pagina opnieuw geladen wordt in de gekozen taal.

De component werd geïntegreerd in de AppHeader, zodat de taalwisselaar op elke pagina beschikbaar is.

Taalverwisselaar

Taalverwisselaar

Opmerking: Deze taalwisselaar werd later vervangen door de voorkeurstaal op de profielpagina, zodat de taalkeuze centraal op het gebruikersprofiel wordt opgeslagen.


Profielpagina

Voor patiënten werd een nieuwe profielpagina uitgewerkt binnen de bestaande Vue-applicatie. Deze pagina toont de profielgegevens van de ingelogde gebruiker, waaronder de naam, het e-mailadres, de geboortedatum, de geboorteplaats, het geslacht, de aanspreking, het rijksregisternummer en de nationaliteit. Deze gegevens zijn read-only, aangezien ze afkomstig zijn uit een extern bronsysteem en niet lokaal in het platform aangepast mogen worden.

De profielpagina maakt gebruik van de bestaande profieldata die via de backend beschikbaar wordt gesteld. Hierdoor wordt vermeden dat dezelfde profielinformatie op meerdere plaatsen verschillend geïnterpreteerd wordt. Visueel wordt aan de gebruiker duidelijk gemaakt dat de gegevens gesynchroniseerd zijn vanuit een extern bronsysteem en dus niet rechtstreeks bewerkbaar zijn binnen de applicatie.

Profielpagina patiënt

Profielpagina met read-only patiëntgegevens afkomstig uit een extern bronsysteem
Voorkeurstaal

Naast de read-only profielgegevens bevat de profielpagina ook een instelling voor de voorkeurstaal van de gebruiker. Deze taalkeuze vervangt de eerder uitgewerkte losse taalwisselaar in de header. De voorkeurstaal wordt opgeslagen op het communicatieprofiel van de gebruiker en vormt voortaan de centrale bron voor de actieve taal binnen de gebruikersapplicatie.

De beschikbare talen worden opgehaald via de bestaande language-API. Wanneer de gebruiker een nieuwe taal selecteert en opslaat, wordt deze keuze via een PATCH-request doorgestuurd naar de backend. Daar wordt gecontroleerd of de gekozen taal bestaat en actief is. Na een succesvolle wijziging wordt de actieve locale in de frontend bijgewerkt, zodat de interface onmiddellijk in de gekozen taal wordt weergegeven.

Avatar met fallback naar initialen

De profielpagina voorziet daarnaast een apart avatarblok. Wanneer de gebruiker reeds een profielfoto heeft ingesteld, wordt deze afbeelding weergegeven. Indien er nog geen avatar beschikbaar is, toont de applicatie automatisch een fallback op basis van de initialen van de gebruiker. Deze fallback wordt gebruikt op de profielpagina en in de header, zodat de weergave doorheen de applicatie consistent blijft.

Voor het uploaden van een avatar werd een afzonderlijke Vue-component uitgewerkt. De gebruiker kan een afbeelding selecteren, waarna eerst client-side validatie gebeurt op bestandstype en bestandsgrootte. Vervolgens wordt een cropvenster geopend via de bestaande AppSlideover-component. Hiervoor wordt gebruikgemaakt van vue-advanced-cropper (Advanced Cropper, z.d.), zodat de gebruiker de afbeelding vierkant kan uitsnijden vóór het uploaden.

Avatar upload met cropfunctie

Avatar upload met cropfunctie

De effectieve verwerking gebeurt server-side via de UserProfileController. De backend valideert opnieuw of het bestand een toegelaten afbeeldingstype heeft en of de maximale bestandsgrootte niet overschreden wordt. Daarna wordt de afbeelding verwerkt met de PHP GD-library, een ingebouwde grafische bibliotheek voor beeldmanipulatie. Hierbij wordt de afbeelding herschaald naar een vast formaat van 256 bij 256 pixels en opgeslagen als WebP-bestand. De avatar wordt los van de extern gesynchroniseerde profieldata bewaard op het communicatieprofiel, zodat een nieuwe synchronisatie met het bronsysteem de profielfoto niet overschrijft.

Naast uploaden werd ook een verwijderactie voorzien. Wanneer de gebruiker zijn avatar verwijdert, wordt de avatar-URL op het communicatieprofiel leeggemaakt en wordt ook het bijhorende bestand verwijderd. De frontend valt daarna automatisch terug op de initialen van de gebruiker.


PDF-export van rapporten

Als laatste uitbreiding werd gewerkt aan een functionaliteit waarmee het interactieve rapport als PDF gedownload kan worden. Deze export is bedoeld voor zowel patiënten als zorgverleners en bevat dezelfde inhoudelijke onderdelen als het interactieve rapport, waaronder biologische leeftijd, leefstijlscores, gezondheidsrisico’s, nutritionele balans, schildklierbalans en adviezen.

Voor de PDF-generatie werd gekozen voor Gotenberg (Gotenberg, 2026). Deze tool draait als aparte Docker-service en zet HTML om naar PDF via headless Chromium. Binnen Symfony werd hiervoor de Sensiolabs Gotenberg Bundle gebruikt (SensioLabs, 2026), zodat de PDF vanuit een Twig-template gegenereerd kan worden. De PDF-template staat los van de interactieve Vue-weergave, zodat het rapport specifiek geoptimaliseerd kan worden voor A4-formaat, paginering en afdrukweergave.

Rapport detailpagina met downloadknop

Rapportdetailpagina met mogelijkheid om het rapport als PDF te downloaden

Tijdens de uitwerking werden verschillende pistes onderzocht. In een eerste versie werd het volledige rapport opgebouwd met Twig en statische HTML. Deze aanpak werkte, maar had als nadeel dat visualisaties uit de Vue-applicatie opnieuw moesten worden nagebouwd. Dit zou op langere termijn leiden tot duplicatie en moeilijker onderhoud.

Daarna werd onderzocht of bestaande Vue-componenten rechtstreeks in de PDF konden worden hergebruikt via een aparte mini-app. Deze proof-of-concept toonde aan dat Gotenberg Vue-componenten kan renderen, maar dat het opbouwen van een volledig rapport op die manier omslachtig werd. De uiteindelijke oplossing combineert daarom beide benaderingen: Twig wordt gebruikt voor de algemene structuur van het PDF-document, terwijl specifieke visualisaties als afzonderlijke Vue-componenten in de PDF kunnen worden gemount.

Preview van het gegenereerde PDF-rapport

Preview van het gegenereerde rapport in PDF-formaat

Deze hybride aanpak beperkt duplicatie, omdat bestaande visualisaties uit de Vue-applicatie gedeeltelijk herbruikbaar blijven. Tegelijk blijft de PDF-template voldoende controleerbaar voor vaste rapportstructuur, styling en paginering. De functionaliteit bevindt zich nog in verdere afwerking, maar de proof of concept bevestigt dat Gotenberg geschikt is om de rapportexport binnen dit project te realiseren.


Testing

Kwaliteitsborging maakt een integraal onderdeel uit van de ontwikkelworkflow bij iO. Om te garanderen dat uitgewerkte functionaliteit correct blijft werken bij toekomstige wijzigingen, werden voor het adminpaneel zowel unit tests als end-to-end tests opgezet. Unit tests valideren de interne logica van controllers geïsoleerd, terwijl end-to-end tests de volledige gebruikersflow simuleren vanuit het perspectief van een browser.

Unit tests

Voor zowel het organisatie- als het gebruikersbeheer werden unit tests geschreven met PHPUnit (PHPUnit, z.d.). Hierbij wordt de logica van controllers geïsoleerd getest door afhankelijkheden zoals repositories en services te vervangen door mock-objecten.

De testen focussen onder andere op het correct wijzigen van gebruikersstatussen en het aanmaken van mutatielogs. Op die manier kon gecontroleerd worden of de controllerlogica correct bleef functioneren bij latere wijzigingen aan de codebase.

PHPUnit output in de terminal

PHPUnit output in de terminal

End-to-end tests met Playwright

Naast unit tests werden ook end-to-end testen opgezet met Playwright (Microsoft, 2026). Deze testen simuleren volledige gebruikersinteracties binnen de browser, zoals zoeken, filteren en navigeren binnen het adminpaneel.

Voor het gebruikersbeheer werd gewerkt met Page Object Models (POM's), waarbij interacties met pagina's gecentraliseerd worden in aparte klassen. Dit verhoogt de leesbaarheid en onderhoudbaarheid van de testsuites.

De testen maakten het mogelijk om bestaande functionaliteit snel te controleren na wijzigingen of uitbreidingen aan de applicatie.

Playwright UI mode

Overzicht van geslaagde testen in Playwright UI mode

Besluit

Tijdens deze stage werd actief meegewerkt aan de ontwikkeling van een professioneel digitaal platform binnen een reële projectcontext. De vooropgestelde doelstellingen uit het projectplan werden gerealiseerd aan de hand van verschillende backend- en frontendfunctionaliteiten binnen het Symfony- en Vue-ecosysteem. Daarbij werden onder andere CRUD-schermen, dashboards, loggingfunctionaliteiten, Live Components, bevestigingsdialogen, vertaalfunctionaliteit en geautomatiseerde testen uitgewerkt.

Naast de oorspronkelijk geplande opdrachten konden bijkomende functionaliteiten worden opgenomen doordat verschillende onderdelen sneller dan voorzien gerealiseerd werden. Zo werd onder meer gewerkt aan een profielpagina voor gebruikersbeheer en aan de voorbereiding van een PDF-generator voor rapportexport. Hierdoor kon niet alleen een bredere bijdrage geleverd worden aan het project, maar werd ook bijkomend technisch onderzoek uitgevoerd binnen domeinen die nog niet eerder binnen het team waren toegepast.

De stage bood bovendien de mogelijkheid om praktijkervaring op te doen met een brede waaier aan technologieën en frameworks. Naast Symfony en Vue werd ook gewerkt met onder andere Symfony UX, Playwright, PHPUnit, Stimulus, Docker en verschillende mechanismen voor en codekwaliteit. Door te werken binnen een bestaande professionele codebase werd inzicht verworven in moderne ontwikkelprocessen zoals code reviews, versiebeheer, testing en kwaliteitsbewaking.

Tot slot leverde de stage een concrete bijdrage aan een productieproject voor een echte klant. De ontwikkelde functionaliteiten worden effectief opgenomen in het platform en verder gebruikt binnen het project. Dit benadrukt niet alleen de praktische relevantie van het uitgevoerde werk, maar toont ook aan dat de gerealiseerde oplossingen een duurzame meerwaarde bieden voor zowel het ontwikkelteam als de eindgebruikers van het platform.

Literatuurlijst

Lijst van figuren

Figuur 1: Overzicht van alle organisaties (dummy data)
Figuur 2: Bij het bewerken is het code-veld uitgeschakeld om onbedoelde wijzigingen te voorkomen
Figuur 3: Formulier om een nieuwe organisatie aan te maken
Figuur 4: Een mutatielog-entry in de database na het deactiveren van een gebruiker
Figuur 5: Overzicht van alle gebruikers met filter- en zoekopties
Figuur 6: Detailpagina van een gebruiker met persoonlijke gegevens en gekoppelde organisaties
Figuur 7: Dropdown met organisaties met zoekveld met autocomplete functie (dummy data)
Figuur 8: Voorbeeld van enkele herbruikbare zoekfilters op de gebruikerspagina
Figuur 9: Voorbeeld van paginering met Pagerfanta
Figuur 10: Voorbeeld van vertalingen voor een administrator
Figuur 11: De SweetAlert2-bevestigingsdialoog die verschijnt bij destructieve acties in het adminpaneel
Figuur 12: Flashbericht met automatisch verdwijnende voortgangsbalk, zichtbaar rechtsonder in het adminpaneel
Figuur 13: Het adminpaneel-dashboard met klikbare statistiekentegels per entiteit, aangevuld met loggingstatistieken en icoontjes
Figuur 14: Tabbladen en statusteller
Figuur 15: De samenvattingstab toont het verloop van audit- en actionlog-entries over de geselecteerde periode
Figuur 16: Het auditlogboek toont wijzigingen als een compacte diff met kleurcodering per wijzigingstype
Figuur 17: Detailpagina van een auditlog-entry met volledige diff-weergave per veld
Figuur 18: Het actielogboek met snelfilterpresets en een overzicht van login- en foutgebeurtenissen
Figuur 19: Detailpagina van een actionlog-entry met volledige metadata van de gebeurtenis
Figuur 20: Taalwisselaar
Figuur 21: Profielpagina met read-only patiëntgegevens afkomstig uit een extern bronsysteem
Figuur 22: Avatar upload met cropfunctie
Figuur 23: Rapportdetailpagina met mogelijkheid om het rapport als PDF te downloaden
Figuur 24: Preview van het gegenereerde rapport in PDF-formaat
Figuur 25: PHPUnit output in de terminal
Figuur 26: Overzicht van geslaagde testen in Playwright UI mode