Stop Over-Engineering Everything

Intro : The State of Modern Bloat

Modern web development has a problem: we are suffocating under layers of abstraction.

Whether it's a PHP framework that requires 50 classes just to render a "Hello World" or a JavaScript stack that ships 2MB of code to display a blog post, we’ve lost sight of what matters. We’ve traded performance and maintainability for "best practices" that often make our code slower and harder to reason about.

Pragma CMS was built as a reaction to this. We don't believe in "Enterprise-grade" complexity for the sake of it. We believe in high-performance, understandable code.

1. MVC-Lite: Separation Without Friction

Most modern PHP frameworks implement a version of MVC (Model-View-Controller) that feels more like a maze than a structure. You spend your day jumping between service providers, repositories, and dependency injectors.

In Pragma, we use MVC-Lite.

  • Logic stays in simple Managers or Controllers.
  • Data is passed directly to the View.
  • No hidden magic, no complex middleware chains.

If you want to know where a piece of data comes from, you look at the controller. If you want to know how it's rendered, you look at the Twig or PHP template. That’s it.

👉 Here is what this looks like in practice.

Typical “over-engineered” controller (framework style)

PHP
class HomepageController
{
    public function __construct(
        private SeoService $seoService,
        private ArticleRepository $articleRepository,
        private ThemeRenderer $renderer,
        private RouteContext $routeContext
    ) {}

    public function index(): Response
    {
        try {
            $route = $this->routeContext->getCurrentRoute();

            $page = $this->seoService->resolvePage($route);

            $articles = $this->articleRepository->findLatestPublished(3);

            return $this->renderer->render('home.twig', [
                'page' => $page,
                'articles' => $articles
            ]);

        } catch (Exception $e) {
            throw new HttpNotFoundException();
        }
    }
}

Pragma CMS MVC-Lite version

PHP
$routeHandle = $page->currentRoute['handle'];

$pageData = PageManager::getPageDataForRoute($routeHandle);

if (!$pageData) {
    displayError("The page does not exist", 404);
    exit;
}

$page->viewData["articles"] = EntryManager::getEntries(
    "article",
    $_SESSION["lang_id"],
    ["limit" => 3]
);

echo render_template("base.php", $page);

Same result. Fewer layers. No indirection. No hidden flow.

2. Moving Beyond Excessive OOP

Object-Oriented Programming (OOP) is a tool, not a religion. Somewhere along the line, we decided that every single thing in a CMS must be an object, leading to deep inheritance trees and "Design Pattern" soup.

Following the mindset popularized by engineers like Casey Muratori, we focus on data-oriented design rather than pure object-oriented abstraction.

  • Why create a complex class hierarchy when a simple associative array or a flat JSON object is faster to process and easier to debug?
  • Pragma favors composition and flat structures. This reduces the cognitive load and, more importantly, it makes the CMS incredibly fast.

👉 Example: simple structured data instead of object graph explosion

Over-designed approach

PHP
$article = $articleRepository->find($id);
$article->getMetadata()->getSeo()->getTitle();

Pragma approach

PHP
$article = EntryManager::getEntry("article", $id);

$title = $article["title"];

No chains. No hidden objects. No surprises.

Just data.

3. Data Access Without Abstraction Layers

Most frameworks add unnecessary layers between you and your database:

  • Repositories
  • Query builders
  • ORM entities
  • Lazy-loaded relationships
  • Service layers
  • Unit-of-work patterns

All of this is supposed to “protect” you from SQL, but in reality it just hides what is happening.

In Pragma CMS, we keep the database layer direct and explicit.

The over-engineered approach (what we avoid)

This is what typical “enterprise PHP” looks like:

PHP
class ArticleService
{
    public function __construct(
        private ArticleRepository $repository
    ) {}

    public function getLatestArticles(): array
    {
        return $this->repository->findLatestPublished(3);
    }
}
PHP
class ArticleRepository
{
    public function __construct(private EntityManager $em) {}

    public function findLatestPublished(int $limit): array
    {
        return $this->em->getRepository(Article::class)
            ->createQueryBuilder('a')
            ->where('a.status = :status')
            ->setParameter('status', 'published')
            ->orderBy('a.createdAt', 'DESC')
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
    }
}

What actually happens here:

  • A query builder builds SQL indirectly
  • An ORM hydrates entities
  • A repository wraps the ORM
  • A service wraps the repository
  • And finally your controller calls the service

At runtime, you’ve lost sight of the actual SQL entirely.

The Pragma CMS approach (direct and visible)

We keep the data flow explicit:

PHP
$articles = Database::fetchAll(
    "SELECT * FROM entries
     WHERE type = :type
     AND status = :status
     ORDER BY created_at DESC
     LIMIT :limit",
    [
        "type" => "article",
        "status" => "published",
        "limit" => 3
    ]
);

Or inside a thin manager:

PHP
class EntryManager
{
    public static function getEntries(
        string $contentTypeHandle,
        ?int $langId = null,
        array $options = []
    ) {
        $contentType = ContentTypeManager::get($contentTypeHandle);
        if (!$contentType) {
            return null;
        }

        $langId = $langId ?? $_SESSION['lang_id'];

        // ----------------------------
        // JOINS
        // ----------------------------
        $joins = implode(' ', $options['joins'] ?? []);

        // ----------------------------
        // WHERE CLAUSES (base)
        // ----------------------------
        $whereClauses = [
            "e.status = 1",
            "e.content_type_handle = :content_type_handle",
            "(e.published_at IS NULL OR e.published_at <= NOW())"
        ];

        $params = $options['params'] ?? [];
        $params['content_type_handle'] = $contentTypeHandle;
        $params['lang_id'] = $langId;

        // WHERE dynamiques (extension)
        if (!empty($options['where_clauses'])) {
            $whereClauses = array_merge($whereClauses, $options['where_clauses']);
        }

        $whereSql = "WHERE " . implode(" AND ", $whereClauses);

        // ----------------------------
        // ORDER / LIMIT / OFFSET
        // ----------------------------
        $orderBy = $options['order_by'] ?? "e.created_at DESC";

        $limit = isset($options['limit'])
            ? "LIMIT " . (int)$options['limit']
            : "";

        $offset = isset($options['offset'])
            ? "OFFSET " . (int)$options['offset']
            : "";

        // ----------------------------
        // QUERY BUILD
        // ----------------------------
        $query =
            "SELECT e.*
             FROM entries e
             {$joins}
             {$whereSql}
             ORDER BY {$orderBy}
             {$limit}
             {$offset}";

        return Database::fetchAll($query, $params);
    }
}

No hidden query generation.

No entity hydration layer.

No abstraction stack.

Just SQL you can reason about.

Why this matters

The goal is not “less code”.

The goal is:

No loss of mental traceability between intent and execution.

When something breaks, you should not have to debug:

  • ORM internals
  • proxy objects
  • repository chains
  • service layers

You should immediately see:

  • what was queried
  • why it was queried
  • and what data came back

That is what keeps a CMS fast, predictable, and maintainable.

4. Explicit Logic over Hidden Exceptions

The industry has fallen in love with try/catch blocks. We throw exceptions for things that aren't actually "exceptional"—they are just normal control flow. This makes code unpredictable and hides the actual logic of the application.

In Pragma CMS, we prefer explicit checks. We believe that your code should handle expected failures (like a missing file or a failed DB connection) as part of its regular logic, not as an afterthought in a catch block. This leads to code that is "boring" in the best way possible: it’s predictable, reliable, and easy to trace.

👉 Example: filesystem operation in real production code

What many systems would do

PHP
try {
    $cache->write($data);
} catch (CacheException $e) {
    $logger->error($e);
}

Pragma CMS approach

PHP
if (!is_dir($cacheDir) && !mkdir($cacheDir, 0755, true)) {
    logError("Unable to create cache folder: {$cacheDir}");
    return false;
}

if (file_put_contents($tempFile, $content) === false) {
    logError("Unable to write cache file: {$tempFile}");
    return false;
}

And in your real CMS code:

PHP
if (!rename($tempFile, $cacheFile)) {
    logError("Unable to rename cache file: {$cacheFile}");
    @unlink($tempFile);
    return false;
}

No exceptions for expected failures.

Just logic.

5. Zero Build Steps: Back to the Browser

We believe that the browser is already a powerful platform. Why do we need a complex build step (Webpack, Vite, etc.) just to write a bit of UI logic?

Pragma CMS uses Vanilla JavaScript.

  • No npm install for the core UI.
  • No build pipelines that break every six months.
  • Just clean code that runs instantly in every modern browser.

Conclusion: Performance is a Foundation, Not a Feature

We didn't build Pragma CMS to win a "feature war." We built it for developers who are tired of fighting their tools. By stripping away unnecessary abstractions and returning to a "Clean Monolith" architecture, we’ve created a system that is blazing fast and remarkably easy to understand.

Software doesn't have to be heavy to be powerful. It just has to be pragmatic.

Build faster with Pragma CMS

Join our community of developers and start creating better websites today.

download

Download for free