Wat is Composer en hoe werkt dependency management?
Composer is de dependency manager van PHP. Nils Adermann en Jordi Boggiano begonnen er in 2011 aan, in 2012 kwam de eerste publieke versie uit, en inmiddels is het moeilijk een serieus PHP-project te vinden dat het niet gebruikt. Laravel, Symfony, WordPress-plugins met een moderne opzet: allemaal draaien ze op Composer. In dit artikel leggen we uit hoe het werkt, welke bestanden ertoe doen, en welke gewoontes je problemen in productie besparen.
Het probleem dat Composer oploste
Voor Composer kopieerde je libraries handmatig in een lib/-map, of installeerde je ze via PEAR, systeembreed op de server. Updaten betekende opnieuw downloaden en hopen dat er niets brak. Twee projecten op dezelfde server die verschillende versies van een library nodig hadden, was een probleem op zich. En elk project had zijn eigen require_once-kluwen om alle klassen in te laden.
Composer pakte drie dingen tegelijk aan: dependencies per project in plaats van per server, versiebeheer met automatische conflictoplossing, en autoloading zodat je nooit meer zelf require hoeft te schrijven.
composer.json: wat je project nodig heeft
In composer.json beschrijf je welke packages je project nodig heeft, en binnen welke versiegrenzen:
{
"require": {
"php": "^8.3",
"guzzlehttp/guzzle": "^7.8",
"monolog/monolog": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^2.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Packages in require-dev heb je alleen nodig tijdens ontwikkeling en in je CI: testtools, statische analyse, debugbars. Op productie installeer je met --no-dev, zodat die code daar niet terechtkomt.
Versieconstraints begrijpen
Composer gaat uit van semantic versioning: MAJOR.MINOR.PATCH, waarbij alleen een nieuwe major-versie breaking changes mag bevatten. De constraints die je het vaakst ziet:
^7.8(caret): alles vanaf 7.8.0 tot 8.0.0. Nieuwe features en fixes wel, een nieuwe major niet. Dit is de standaard en meestal de juiste keuze.~7.8(tilde): vanaf 7.8.0 tot 8.0.0.~7.8.2betekent echter vanaf 7.8.2 tot 7.9.0, dus alleen patches.7.8.*: alleen patchversies binnen 7.8.7.8.2: precies deze versie. Vermijd dit in libraries; het maakt het voor anderen onmogelijk een gemeenschappelijke versie te vinden.
Let op: bij versies onder 1.0 is de caret strenger. ^0.3 betekent >=0.3.0 <0.4.0, omdat bij 0.x-versies elke minor als breaking geldt.
composer.lock: wat er werkelijk geïnstalleerd is
Waar composer.json grenzen aangeeft, legt composer.lock vast welke exacte versie van elke package (inclusief alle indirecte dependencies) geïnstalleerd is. Dit is het bestand dat ervoor zorgt dat jouw laptop, je collega, de CI-server en productie precies dezelfde code draaien.
Het verschil tussen de twee belangrijkste commando's:
composer installinstalleert precies wat incomposer.lockstaat. Dit gebruik je in je deployscript en je CI.composer updatezoekt binnen de constraints de nieuwste versies, installeert die en schrijft een nieuwecomposer.lock. Dit doe je bewust, lokaal, en je commit het resultaat.
De vuistregel: commit composer.lock altijd in applicaties. Voor libraries die je publiceert ligt dat anders; daar bepaalt het project dat jouw library gebruikt de uiteindelijke versies.
Wil je één package bijwerken zonder de rest mee te nemen, gebruik dan composer update vendor/package. Met --with-dependencies mogen ook de dependencies van die package mee.
Autoloading en PSR-4
Composer genereert een vendor/autoload.php. Eén require daarvan in je front controller, en elke klasse wordt ingeladen op het moment dat je hem voor het eerst gebruikt. Hoe Composer een klasse aan een bestand koppelt, bepaal je in de autoload-sectie. De standaard is PSR-4: de namespace App\Http\Controllers\HomeController zit in src/Http/Controllers/HomeController.php. Meer over PSR-4 en de andere standaarden lees je in PHP-FIG en de PSR-standaarden uitgelegd.
Voor productie loont het de autoloader te optimaliseren:
composer install --no-dev --optimize-autoloader
Composer bouwt dan een classmap: een vaste lijst van klasse naar bestand, zodat hij niet bij elke klasse het bestandssysteem hoeft af te zoeken. Met --classmap-authoritative ga je nog een stap verder; dan zoekt Composer helemaal niet meer buiten de classmap. Dat is sneller, maar breekt code die klassen tijdens runtime genereert.
Packagist en private packages
Standaard haalt Composer packages van Packagist.org, de centrale, openbare repository met honderdduizenden PHP-packages. Packagist host zelf geen code: het indexeert Git-repositories en wijst Composer naar de juiste versie.
Voor interne packages heb je opties: een vcs-repository die rechtstreeks naar een private Git-repo wijst, een path-repository voor packages in een monorepo, of een private registry zoals Private Packagist of een zelfgehoste variant. Voor een paar packages volstaat de vcs-variant; vanaf een handvol is een registry sneller en beter te beheren.
Veiligheid: composer audit
Sinds versie 2.4 heeft Composer een ingebouwd composer audit-commando. Het controleert je geïnstalleerde packages tegen bekende kwetsbaarheden, standaard via de security advisories van Packagist.org, die onder meer de GitHub Advisory Database bundelen:
composer audit
Zet dit in je CI-pipeline, zodat een build faalt zodra er een bekende kwetsbaarheid in je dependencies zit. Combineer het met composer outdated --direct, dat laat zien welke van je directe dependencies achterlopen.
Handige commando's die je misschien nog niet kent
composer why vendor/package: welke package trekt deze dependency binnen?composer why-not vendor/package 3.0: wat blokkeert de upgrade naar deze versie? Onmisbaar bij een vastgelopen update.composer bump: zet de ondergrenzen incomposer.jsongelijk aan wat er nu geïnstalleerd is. Handig in applicaties, zodat je constraints niet jaren achterlopen.composer validate --strict: controleert jecomposer.jsonop fouten en inconsistenties met de lockfile.composer show --tree: de volledige dependency-boom.
Het platform vastleggen
Ontwikkel je lokaal op een nieuwere PHP-versie dan productie, dan kan Composer packages kiezen die op productie niet draaien. Leg de PHP-versie van productie daarom vast in de config:
{
"config": {
"platform": {
"php": "8.3.0"
}
}
}
Composer lost dependencies dan op alsof het op PHP 8.3 draait, ongeacht je lokale versie. Vergeet niet deze waarde mee te verhogen als productie upgradet.
Samenvatting
composer.json beschrijft wat je nodig hebt, composer.lock legt vast wat je hebt. Gebruik install bij deploys en update alleen bewust. Commit de lockfile, installeer op productie met --no-dev --optimize-autoloader, en laat composer audit in je CI meedraaien. Met die gewoontes voorkom je het grootste deel van de "maar op mijn machine werkt het"-problemen.
Zit je project vast op oude dependencies die je niet meer durft bij te werken? Dan is dat vaak het eerste signaal van een applicatie die aan modernisering toe is.