<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://noor-framework.vercel.app/blog</id>
    <title>Noor Blog</title>
    <updated>2026-06-25T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://noor-framework.vercel.app/blog"/>
    <subtitle>Noor Blog</subtitle>
    <icon>https://noor-framework.vercel.app/img/logo.svg</icon>
    <entry>
        <title type="html"><![CDATA[The Challenges of Building a Single-File PHP Framework]]></title>
        <id>https://noor-framework.vercel.app/blog/challenges-single-file-framework</id>
        <link href="https://noor-framework.vercel.app/blog/challenges-single-file-framework"/>
        <updated>2026-06-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Building a framework in a single file sounds like a constraint you'd impose for fun. And it was fun. But it also forced some interesting trade-offs that are worth documenting.]]></summary>
        <content type="html"><![CDATA[<p>Building a framework in a single file sounds like a constraint you'd impose for fun. And it was fun. But it also forced some interesting trade-offs that are worth documenting.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-namespace-management-in-a-flat-file">1. Namespace Management in a Flat File<a href="https://noor-framework.vercel.app/blog/challenges-single-file-framework#1-namespace-management-in-a-flat-file" class="hash-link" aria-label="Direct link to 1. Namespace Management in a Flat File" title="Direct link to 1. Namespace Management in a Flat File" translate="no">​</a></h2>
<p>Without Composer's PSR-4 autoloader, every class lives in the global namespace. That means class name collisions are a real possibility if the user brings in external libraries or defines their own classes.</p>
<p>The solution was a simple spl_autoload_register at the bottom of noor.php that checks the project root, controllers/, and models/ directories. User classes take priority over framework internals. This buys reasonable interoperability without compromising the single-file constraint.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-the-template-engine-regex-vs-parser">2. The Template Engine: Regex vs. Parser<a href="https://noor-framework.vercel.app/blog/challenges-single-file-framework#2-the-template-engine-regex-vs-parser" class="hash-link" aria-label="Direct link to 2. The Template Engine: Regex vs. Parser" title="Direct link to 2. The Template Engine: Regex vs. Parser" translate="no">​</a></h2>
<p>A proper template engine uses a lexer and parser. A single-file framework can't ship a parser. The template engine had to use regular expressions.</p>
<p>Getting Blade-style directives right with regex was harder than expected:</p>
<ul>
<li class=""><strong><code>@if (Auth::check())</code></strong> — The naive <code>(.+?)</code> capture group stops at the first closing paren, capturing <code>Auth::check(</code> instead of <code>Auth::check()</code>. Fixing this required a balanced-parentheses pattern.</li>
<li class=""><strong><code>@endforeach</code> vs <code>@endfor</code></strong> — The <code>@endfor</code> pattern matches the first 6 characters of <code>@endforeach</code>, leaving <code>each</code> as orphaned text. Processing <code>@endforeach</code> before <code>@endfor</code> solved it.</li>
<li class=""><strong><code>@section('title', $variable)</code> vs <code>@section('content')...@endsection</code></strong> — Two forms of the same directive with different semantics required different regex branches.</li>
</ul>
<p>The final engine compiles directives to PHP, writes to a temp file, and includes it. No cache layer — compilation happens on every request. On shared hosting with opcache, this is fast enough.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-route-matching-without-a-router-library">3. Route Matching Without a Router Library<a href="https://noor-framework.vercel.app/blog/challenges-single-file-framework#3-route-matching-without-a-router-library" class="hash-link" aria-label="Direct link to 3. Route Matching Without a Router Library" title="Direct link to 3. Route Matching Without a Router Library" translate="no">​</a></h2>
<p>Laravel's router compiles all routes into a single regex. Noor can't do that without a lexer. Instead, it iterates the route collection and matches each against the request URI using regex derived from the route pattern.</p>
<p>For most applications (under 100 routes), this is negligible. For API-heavy apps with hundreds of routes, it could matter. The trade-off was accepted.</p>
<p>Optional parameters (<code>{param?}</code>) and <code>where</code> constraints added complexity. The <code>where</code> constraints had to be threaded through the RouteBuilder into the matching regex without coupling the classes too tightly.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-session-flash-data">4. Session Flash Data<a href="https://noor-framework.vercel.app/blog/challenges-single-file-framework#4-session-flash-data" class="hash-link" aria-label="Direct link to 4. Session Flash Data" title="Direct link to 4. Session Flash Data" translate="no">​</a></h2>
<p>The built-in PHP session lifecycle doesn't map cleanly to "flash for one request." PHP sessions start on <code>session_start()</code> and are saved on shutdown. There's no concept of a request boundary.</p>
<p>Noor's session flash works by marking flash data with expiration timestamps. On the next <code>Session::start()</code>, expired data is cleaned. This works for the standard request-response cycle but required careful ordering: flash data set during the current request shouldn't be cleaned until the <em>next</em> request's session start.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-template-inheritance-without-output-buffering">5. Template Inheritance Without Output Buffering<a href="https://noor-framework.vercel.app/blog/challenges-single-file-framework#5-template-inheritance-without-output-buffering" class="hash-link" aria-label="Direct link to 5. Template Inheritance Without Output Buffering" title="Direct link to 5. Template Inheritance Without Output Buffering" translate="no">​</a></h2>
<p>Laravel's Blade handles <code>@extends</code> and <code>@section</code> by compiling to PHP class structures. Noor's approach is simpler: the child view is rendered first, capturing sections into a static array. Then the parent layout is rendered with <code>@yield</code> pulling from that array.</p>
<p>The tricky part was that <code>@section</code> blocks need to start <code>ob_start()</code> and <code>@endsection</code> needs <code>ob_get_clean()</code>. Both are runtime operations, not compile-time. The directive compiler emits PHP function calls (<code>View::startSection()</code>, <code>View::endSection()</code>) that manage the buffer stack.</p>
<p>This works but means the entire child view is buffered before the layout is rendered. Deeply nested layouts would create nested buffers, though in practice layouts rarely nest more than one level.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="6-auth-without-a-password-library">6. Auth Without a Password Library<a href="https://noor-framework.vercel.app/blog/challenges-single-file-framework#6-auth-without-a-password-library" class="hash-link" aria-label="Direct link to 6. Auth Without a Password Library" title="Direct link to 6. Auth Without a Password Library" translate="no">​</a></h2>
<p>Password hashing was trivial — <code>password_hash()</code> and <code>password_verify()</code> are built into PHP. The harder part was designing Auth::register() to automatically hash passwords before insert, and Auth::attempt() to verify credentials and handle password rehashing when the cost factor changes.</p>
<p>The tight coupling between Auth and Session (via <code>_auth_id</code>) is not ideal architecturally, but it's pragmatic. On shared hosting, you're not swapping auth backends.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="closing-thoughts">Closing Thoughts<a href="https://noor-framework.vercel.app/blog/challenges-single-file-framework#closing-thoughts" class="hash-link" aria-label="Direct link to Closing Thoughts" title="Direct link to Closing Thoughts" translate="no">​</a></h2>
<p>Building Noor taught me that the constraints that seem limiting are often the most productive ones. The single-file requirement forced simplicity in API design, clarity in naming, and discipline in implementation.</p>
<p>Every feature had to justify its existence. If it couldn't fit cleanly in a single file without creating confusion, it didn't belong. That's a good filter for any framework — regardless of how many files it ships in.</p>]]></content>
        <author>
            <name>Noor Team</name>
            <uri>https://git.qutaha.in/qu</uri>
        </author>
        <category label="noor" term="noor"/>
        <category label="php" term="php"/>
        <category label="engineering" term="engineering"/>
        <category label="architecture" term="architecture"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Why Noor — A Single-File PHP Framework for Shared Hosting]]></title>
        <id>https://noor-framework.vercel.app/blog/why-noor</id>
        <link href="https://noor-framework.vercel.app/blog/why-noor"/>
        <updated>2026-06-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Every PHP developer eventually faces the same deployment reality: shared hosting. cPanel, Plesk, or a basic LAMP stack with FTP access and nothing else. No SSH. No Composer. No Node.js. No chance of running artisan or bin/console.]]></summary>
        <content type="html"><![CDATA[<p>Every PHP developer eventually faces the same deployment reality: shared hosting. cPanel, Plesk, or a basic LAMP stack with FTP access and nothing else. No SSH. No Composer. No Node.js. No chance of running <code>artisan</code> or <code>bin/console</code>.</p>
<p>Modern PHP frameworks assume a modern environment. Laravel needs Composer, vendor/, and often Redis or a queue worker. Symfony is similar. Even Slim requires Composer and an autoloader. On shared hosting, you're lucky to get PHP 7.4 with <code>mod_rewrite</code>.</p>
<p>So you reach for WordPress. Or you hand-roll everything with <code>require_once</code> spaghetti. Neither is satisfying.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-gap">The Gap<a href="https://noor-framework.vercel.app/blog/why-noor#the-gap" class="hash-link" aria-label="Direct link to The Gap" title="Direct link to The Gap" translate="no">​</a></h2>
<p>Between the monolithic giants (Laravel, Symfony) and the "raw PHP" approach, there's a gap. A gap for a framework that gives you structure without infrastructure.</p>
<p>I wanted:</p>
<ul>
<li class=""><strong>Routing</strong> — clean URL patterns, not <code>if ($_GET['page'])</code> chains</li>
<li class=""><strong>Templating</strong> — layouts, sections, escaping, not <code>&lt;?php echo htmlspecialchars(...)</code> everywhere</li>
<li class=""><strong>Database</strong> — a query builder, not raw PDO verbosity</li>
<li class=""><strong>Auth</strong> — sessions, bcrypt, CSRF, not reinventing password hashing</li>
<li class=""><strong>Validation</strong> — declarative rules, not <code>if (!filter_var(...))</code> blocks</li>
</ul>
<p>And I wanted it all in a single file you could FTP to a shared host in 2026.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="design-constraints">Design Constraints<a href="https://noor-framework.vercel.app/blog/why-noor#design-constraints" class="hash-link" aria-label="Direct link to Design Constraints" title="Direct link to Design Constraints" translate="no">​</a></h2>
<p>Noor was built around non-negotiable constraints:</p>
<ol>
<li class=""><strong>One file.</strong> <code>noor.php</code> is the framework. Copy it and go. No <code>vendor/</code>, no <code>composer.json</code>, no autoloader config.</li>
<li class=""><strong>No CLI.</strong> Everything must work through the web server. No code generation, no cache clears, no CLI commands.</li>
<li class=""><strong>PHP 7.4+.</strong> The vast majority of shared hosts still run PHP 7.4 or 8.0. PHP 8.1+ features were off limits.</li>
<li class=""><strong>Drop-in deployment.</strong> Upload via FTP, point your domain at the directory, and it works. No post-deploy steps.</li>
<li class=""><strong>Familiar ergonomics.</strong> If you know Laravel, you know Noor. Same routing patterns, same template directives, same query builder chain.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-it-is-not">What It Is Not<a href="https://noor-framework.vercel.app/blog/why-noor#what-it-is-not" class="hash-link" aria-label="Direct link to What It Is Not" title="Direct link to What It Is Not" translate="no">​</a></h2>
<p>Noor is not trying to compete with Laravel or Symfony. It doesn't have queues, events, a service container, Eloquent ORM, Horizon, Telescope, or any of the ecosystem tools that make Laravel great for large applications.</p>
<p>Noor is for the other 80% of PHP projects: the company website, the internal tool, the MVP, the client project on a $5/month shared host.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-next">What's Next<a href="https://noor-framework.vercel.app/blog/why-noor#whats-next" class="hash-link" aria-label="Direct link to What's Next" title="Direct link to What's Next" translate="no">​</a></h2>
<p>The framework is stable enough for production use on simple to moderate projects. PostgreSQL and SQLite support work. The query builder handles joins, subqueries, and transactions. The template engine supports full layout inheritance.</p>
<p>Future improvements could include Redis session support, a minimal caching layer, and better error diagnostics — but only if they don't compromise the single-file, zero-dependency constraint.</p>
<p>If you've been looking for something between WordPress and a full Laravel deployment, Noor might be what you need. Drop it in and start building.</p>]]></content>
        <author>
            <name>Noor Team</name>
            <uri>https://git.qutaha.in/qu</uri>
        </author>
        <category label="noor" term="noor"/>
        <category label="php" term="php"/>
        <category label="shared-hosting" term="shared-hosting"/>
        <category label="framework" term="framework"/>
    </entry>
</feed>