Naming the framework — design philosophy and goals
Concept
Every framework starts with a name and a philosophy. Before writing a single line of code you need to answer the question: what are the core constraints that will guide every architectural decision? Laravel answered with "elegance and developer happiness." Symfony answered with "reusable, decoupled components." Your framework needs its own answer, because that answer will determine what you build and what you deliberately leave out.
The framework we build in this track is called Lumen (our own — unrelated to the old Laravel micro-framework). Its design philosophy is transparency by construction: every piece of the framework should be understandable by reading a single class, without navigating ten layers of inheritance. This is intentional pedagogy — real-world frameworks optimize for extensibility at the cost of readability. Ours optimizes for learning at the cost of extensibility.
The three design tenets we will uphold throughout every chapter:
- PSR compliance where it matters — PSR-4 autoloading, PSR-7 HTTP messages, PSR-11 container, PSR-15 middleware. These standards exist so that any developer can drop in a third-party component that speaks the same interface.
- No magic beyond PHP — we will use Reflection API (which PHP provides) but we will not rely on eval, runkit, or any extension beyond standard PHP 8.2+.
- Testability first — every class will be designed so it can be unit-tested without booting the full framework.
Laravel's design philosophy is documented in Taylor Otwell's own words: "a framework for web artisans" focused on expressiveness. The Illuminate namespace separates individual components so they can be used standalone. We will mirror that separation — each chapter of this track maps to a standalone namespace: Lumen\Foundation, Lumen\Container, Lumen\Routing, etc.
The project structure you choose on day one is almost impossible to change later. We will use the Composer-standard layout: src/ for framework source, app/ for application code, tests/ for tests, public/ for the web entry point, and config/ for configuration files. This mirrors what you will find in every serious PHP project.
Code Example
<?php
declare(strict_types=1);
/**
* composer.json for Lumen framework skeleton
*
* {
* "name": "lumen/framework",
* "description": "A transparent PHP framework built for learning",
* "type": "project",
* "require": {
* "php": "^8.2"
* },
* "require-dev": {
* "phpunit/phpunit": "^11.0"
* },
* "autoload": {
* "psr-4": {
* "Lumen\\": "src/",
* "App\\": "app/"
* }
* },
* "autoload-dev": {
* "psr-4": {
* "Tests\\": "tests/"
* }
* }
* }
*/
// src/Foundation/Application.php — the heart of the framework
namespace Lumen\Foundation;
/**
* The Application class is the central object of the entire framework.
*
* Every other component is registered into or resolved from the Application.
* Think of it as the universe inside which the framework operates.
*
* Laravel equivalent: Illuminate\Foundation\Application
* Key difference: Laravel's Application extends Container directly.
* Ours keeps them separate to make the Container chapter stand alone.
*/
class Application
{
private static ?self $instance = null;
/** The root path of the application (where composer.json lives). */
private string $basePath;
/** Human-readable framework version. */
public const VERSION = '1.0.0';
public function __construct(string $basePath)
{
$this->basePath = rtrim($basePath, DIRECTORY_SEPARATOR);
static::$instance = $this;
}
/**
* Get the globally available instance of the Application.
* Used as a last-resort access point when DI is not available.
*/
public static function getInstance(): static
{
if (static::$instance === null) {
throw new \RuntimeException('Application has not been bootstrapped yet.');
}
return static::$instance;
}
/** Resolve a path relative to the application root. */
public function basePath(string $path = ''): string
{
return $path === ''
? $this->basePath
: $this->basePath . DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR);
}
public function configPath(string $path = ''): string
{
return $this->basePath('config' . ($path !== '' ? DIRECTORY_SEPARATOR . $path : ''));
}
public function storagePath(string $path = ''): string
{
return $this->basePath('storage' . ($path !== '' ? DIRECTORY_SEPARATOR . $path : ''));
}
public function version(): string
{
return static::VERSION;
}
}Interview Q&A
Q: Why does the Application class expose basePath() and configPath() helpers instead of having callers use a global constant?
Hardcoding __DIR__ . '/../config' throughout a codebase makes the app impossible to move or embed. By centralising path resolution in the Application class, you can relocate the entire project, change the config directory name, or even serve multiple applications from one process — all by changing one object. Laravel's Application does exactly this: $app->basePath(), $app->configPath(), $app->storagePath(), etc., and each can be overridden by passing a second argument to the constructor or calling useConfigPath(). The Application object becomes the single source of truth for filesystem layout.
Q: Laravel's Application extends the Container. Is that good design?
It's a pragmatic trade-off, not a clean OOP decision. Extending Container gives $app access to all container methods directly ($app->make(), $app->bind()) which is convenient, but it violates the Interface Segregation Principle — callers that only need path helpers now depend on the full container interface. The Laravel team chose ergonomics over ISP. Our framework keeps them separate so each chapter can be understood independently. In a real production framework, Laravel's approach is fine because the Application is the central registry — the coupling is intentional and well-understood.
Q: What does static::$instance = $this do in the constructor, and when would you call getInstance()?
It implements the Singleton pattern at the Application level. The static:: ensures that if a subclass overrides the property, it stores in the subclass's own property slot (late static binding). getInstance() is a "global escape hatch" — in deeply nested code where DI is impractical (a third-party package, a legacy function, a Blade directive), you can call Application::getInstance() to reach the container. Laravel provides the app() helper which does the same thing. Use it sparingly: every call to getInstance() is a coupling to the global state that makes the calling code harder to unit-test.