Oud vs. Nieuw

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) automatisch

3. 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

OnderdeelAanbeveling
Framework\Html\ElGebruik het overal. Nooit meer string-concatenatie + htmlspecialchars.
RouterFramework\App\Router is de verbeterde variant — gebruik die in nieuwe code.
Db\SqlPrima zo — inject via constructor, niet via App::getStaticInstance().
App (god-object)Vervangen door Kernel — kleine kernel + container, CMS-context apart in Website.
AutoloadOverweeg Composer PSR-4 als de library een eigen package wordt.
TestsPHPUnit-basis is opgezet — schrijf tests bij elke verbeterde component.