Stop Over-Engineering Everything
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.
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.
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.
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();
}
}
}
$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.
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.
👉 Example: simple structured data instead of object graph explosion
$article = $articleRepository->find($id);
$article->getMetadata()->getSeo()->getTitle();
$article = EntryManager::getEntry("article", $id);
$title = $article["title"];
No chains. No hidden objects. No surprises.
Just data.
Most frameworks add unnecessary layers between you and your database:
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.
This is what typical “enterprise PHP” looks like:
class ArticleService
{
public function __construct(
private ArticleRepository $repository
) {}
public function getLatestArticles(): array
{
return $this->repository->findLatestPublished(3);
}
}
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:
At runtime, you’ve lost sight of the actual SQL entirely.
We keep the data flow explicit:
$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:
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.
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:
You should immediately see:
That is what keeps a CMS fast, predictable, and maintainable.
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
try {
$cache->write($data);
} catch (CacheException $e) {
$logger->error($e);
}
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:
if (!rename($tempFile, $cacheFile)) {
logError("Unable to rename cache file: {$cacheFile}");
@unlink($tempFile);
return false;
}
No exceptions for expected failures.
Just logic.
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.
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.