Oud vs. Nieuw
Concrete vergelijkingen tussen de huidige bibliotheek-aanpak en verbeterde alternatieven die in dit playground worden uitgewerkt.
1. Applicatie-initialisatie
De huidige App doet te veel. Kernel boot alleen wat echt nodig is; CMS-context (Website) bind je apart, en al het andere komt via de container.
❌ Huidig
// public/index.php (huidig)
require 'library/init.php';
// App is een singleton die alles doet:
// - settings laden
// - domain detecteren
// - database verbinden
// - pluginManager starten
// - cache checken
// - robots.txt afhandelen
// - editmode detecteren
$app = new App('config/settings.php');
// Of erger: App::getStaticInstance()
// overal in de codebase aangeroepen✅ Verbeterd
// public/index.php (verbeterd)
$kernel = Framework\App\Kernel::boot(
appRoot: dirname(__DIR__),
debug: true,
);
// Virtual properties — lazy via container, swappable in tests:
$kernel->router->add('/', fn($req) =>
$kernel->layout->wrap('Home', require '../pages/home.php')
);
$kernel->run();2. HTML genereren
Framework\Html\El dwingt veilige HTML af. text() escaped altijd, raw() is bewust onveilig. De structuur is als code leesbaar, niet als string-soup.
❌ Huidig
// Veelvoorkomend patroon in modules:
$html = '<div class="card">';
$html .= '<h2>' . htmlspecialchars($titel) . '</h2>';
$html .= '<p>' . htmlspecialchars($omschrijving) . '</p>';
if ($link) {
$html .= '<a href="' . htmlspecialchars($url) . '"';
$html .= ' class="btn">Lees meer</a>';
}
$html .= '</div>';
echo $html;
// Risico: vergeten htmlspecialchars(),
// verkeerd sluiten van tags, moeilijk debuggen✅ Verbeterd
// Met Framework\Html\El:
$card = El::make('div', ['class' => 'card'])
->add(El::make('h2')->text($titel))
->add(El::make('p')->text($omschrijving));
if ($link) {
$card->add(
El::make('a', ['href' => $url, 'class' => 'btn'])
->text('Lees meer')
);
}
echo $card;
// text() escaped automatisch — geen XSS mogelijk
// Tagstructuur is altijd correct gesloten
// Void-elementen (img, br, input) automatisch3. Routing
De nieuwe Router heeft geen externe dependencies. dispatch() geeft een string terug. Volledig unit-testbaar zonder HTTP-context.
❌ Huidig
// App\Routing\Router::handle() vereist:
// - App::getStaticInstance() (singleton)
// - Laminas View + NavigationSitemap
// - CMS-database (voor SEO-labels)
// - Volledig geconfigureerde App
// Hierdoor: onmogelijk te testen
// zonder complete applicatie-context
$router = new App\Routing\Router();
$router->add('/pagina', [Controller::class, 'index'], ['route' => ['label' => 'Pagina']]);
$result = $router->handle($_SERVER['REQUEST_URI']);✅ Verbeterd
// Framework\App\Router — volledig zelfstandig:
$router = new Framework\App\Router();
$router->add('/pagina',
fn($req) => 'Pagina-inhoud'
);
$router->add('/artikel/([a-z0-9-]+)',
fn($req) => 'Artikel: ' . $req->param(0)
);
$result = $router->dispatch($uri);
// Geeft string terug of null (geen match)
// Testbaar:
// $router->dispatch('/artikel/test') === 'Artikel: test'4. Vertalingen
Een expliciete TranslatorInterface maakt de applicatie testbaar en DB-onafhankelijk. In productie kun je de interface implementeren met DB, in tests met een array.
❌ Huidig
// Huidig: vertaling via App (god-object)
// Slaat automatisch nieuwe tags op in de DB
// Roept DeepL aan als vertaling ontbreekt
// Koppelt aan domain_id uit de App
$tekst = App::getStaticInstance()
->translate('Welkom op onze website');
// Problemen:
// - Schrijft naar DB bij iedere oproep
// - Vereist DB-verbinding
// - Side-effects in een getter
// - Moeilijk te testen✅ Verbeterd
// Verbeterd: expliciete Translator service
interface TranslatorInterface {
public function t(string $key, string $default = ''): string;
}
class ArrayTranslator implements TranslatorInterface {
public function __construct(
private readonly array $translations,
private readonly string $locale,
) {}
public function t(string $key, string $default = ''): string {
return $this->translations[$this->locale][$key]
?? $default;
}
}
// Injecteerbaar, testbaar, geen DB-afhankelijkheid:
$translator = new ArrayTranslator($translations, 'nl');
$tekst = $translator->t('nav.home', 'Home');5. Database-toegang
Db\Adapter en Db\Sql zijn al goed ontworpen — ze hoeven alleen expliciet geïnjecteerd te worden in plaats van via de App-singleton opgevraagd.
❌ Huidig
// Huidig: DB via App (lazy-loaded)
$sql = App::getStaticInstance()->sql;
$results = $sql->select('users')
->equalTo('active', 1)
->execute();
// Probleem: App::getStaticInstance()
// vereist volledige bootstrapping
// Db\Sql is prima, maar de toegang ernaartoe niet✅ Verbeterd
// Verbeterd: DB-adapter als injecteerbare service
// (Db\Adapter en Db\Sql zijn al goed!)
$pdo = new PDO($dsn, $user, $pass);
$adapter = new Db\Adapter\PDO($pdo, siteId: 1);
$sql = new Db\Sql(locale: 'nl', db: $adapter);
// Injecteerbaar via constructor:
class UserRepository {
public function __construct(
private readonly Db\Sql $sql
) {}
public function active(): array {
return $this->sql->select('users')
->equalTo('active', 1)
->execute()
->toArray();
}
}Samenvatting
| Onderdeel | Aanbeveling |
|---|---|
Framework\Html\El | Gebruik het overal. Nooit meer string-concatenatie + htmlspecialchars. |
Router | Framework\App\Router is de verbeterde variant — gebruik die in nieuwe code. |
Db\Sql | Prima zo — inject via constructor, niet via App::getStaticInstance(). |
App (god-object) | Vervangen door Kernel — kleine kernel + container, CMS-context apart in Website. |
Autoload | Overweeg Composer PSR-4 als de library een eigen package wordt. |
Tests | PHPUnit-basis is opgezet — schrijf tests bij elke verbeterde component. |