Why I Built a Zero-Dependency PHP Framework in 2025

Every PHP developer knows the dependency drill. Shinobi Apps runs 210,000+ lines of code with zero runtime Composer packages. This post explains why — the update tax, the black box problem, and the performance overhead you don't measure.

Every PHP developer knows the drill. You start a project. You run composer require a few times. Before you know it, your vendor/ folder has 200 packages, 50,000 files, and a dependency tree so deep that composer why reads like a mystery novel.

I made a different choice. Shinobi Apps — an entire platform with an HTTP kernel, DI container, ORM, template engine, middleware pipeline, AI agent runtime, and 11 addons — runs with zero runtime Composer packages. 210,000+ lines of code across 1,046 files — PHP, JavaScript, CSS, and tests — and not a single require in composer.json.

This post explains why.

The Real Cost of Dependencies

Let's talk about what dependencies actually cost you, beyond the obvious "someone else's code in your project."

1. The Update Tax

Every dependency is a subscription to someone else's roadmap. When Symfony releases a new major version, you don't get to choose your timeline — your other packages do. One package requires Symfony 6, another requires Symfony 7, and suddenly you're spending a Tuesday afternoon untangling version constraints instead of shipping features.

I've lived this. Multiple times. The breaking change that isn't in the changelog. The subtle behavior difference between 4.x and 5.x that only surfaces in production. The package that gets abandoned and now you're forking it just to keep things running.

With zero dependencies, that entire category of problems doesn't exist. Every change in the codebase is intentional. Every change is mine.

2. The Black Box Problem

Here's a question: when your DI container throws a cryptic error at 2 AM, do you know where to look? Not which config file — I mean which line of container resolution code is failing and why.

If you're using a third-party container, the answer is usually "no." You grep through the vendor folder, read through someone else's architecture, and hope the error message is good enough.

When I built the Shinobi container, I wrote the circular dependency detection myself. I know exactly how constructor parameter resolution works — by type, then by name, then default value, then nullable null. When something breaks, I don't debug someone else's abstraction. I debug my own code.

3. The Performance Overhead You Don't Measure

Most benchmarks compare "framework X vs framework Y" on hello-world routes. That's not where the real overhead lives.

The real overhead is in autoloading. PSR-4 autoloaders scan directories. They resolve namespaces to paths using string manipulation on every class load. It works — but it's work the CPU does on every single request.

The Shinobi autoloader takes a different approach. It scans directories once, builds a complete class map — every class, interface, trait, and enum in the project — and caches it to storage/cache/autoload.map.php. After that, every class load is a single array lookup. No directory scanning, no string manipulation, no filesystem stat calls. One hash table lookup, one require.

The class map currently tracks 145 PHP files across the framework. That's 145 classes resolved by direct lookup instead of namespace-to-path conversion.

What "Zero Dependencies" Actually Means

Let me be precise. The composer.json has exactly one runtime requirement:

{
    "require": {
        "php": ">=8.1"
    }
}

That's it. PHP itself.

The dev dependencies are the tools I'd be foolish to rewrite:

  • PHPStan (level 5) — static analysis
  • Psalm (with taint analysis) — security-focused static analysis
  • PHPUnit — testing
  • PHP_CodeSniffer — code style
  • PHPMD — mess detection
  • Security Checker — vulnerability scanning
These are development tools. They don't ship to production. They don't run at runtime. The production deployment has zero Composer packages. You don't even need Composer installed on the server.

What I Had to Build

Going zero-dependency means building everything yourself. Here's what the Shinobi framework includes — all from scratch, all in ~34,000 lines of PHP:

HTTP Layer — Request parsing from superglobals, Response building with proper headers, and an HttpKernel that orchestrates the entire request lifecycle. The kernel handles maintenance mode, request type detection, route pre-resolution, middleware pipeline execution, and response sending.

Routing — Pattern matching with typed parameters ({id:number}, {slug:string}, {optional?}), RESTful resource routes, route groups with middleware and auth configuration. The router resolves exact matches first, then falls back to pattern matching.

DI Container — Singleton and request scopes, reflection-based autowiring, circular dependency detection. About 100 services registered across the platform. Controllers use request scope (new instance per call), everything else is singleton.

Database Layer — PDO wrapper with prepared statement caching (LRU, max 20), auto-reconnect with exponential backoff, an Active Record ORM, and a fluent QueryBuilder. SSL/TLS support for secure database connections.

Template Engine — Custom directive processor that compiles to native PHP, with production caching that plays nice with OPcache. In production, rendering a template is literally require-ing a cached PHP file — the same performance as a plain PHP include.

Middleware Pipeline — Six layers deep, priority-ordered, with lazy loading and split timing. Every middleware tracks its own execution time: before processing, after processing, own time, and nested time. You always know where your milliseconds go.

Session Management — File and database drivers, with fingerprinting (User-Agent + Accept-Language hash), IP validation, and automatic session ID regeneration.

Security — CSRF tokens with 30-minute rotation and 2-hour lifetime, Content Security Policy with per-request nonces, rate limiting per IP per endpoint, and a full authentication system with failed login backoff.

AI Integration — A unified BYOK (Bring Your Own Key) provider that works with Claude, OpenAI, DeepSeek, and Groq through a single interface. Plus an entire agent platform: multi-agent runtime with tool calling, agent-to-agent delegation, approval workflows, token budgets, and shared context.

And more — caching, queue system, event dispatcher, multi-platform webhooks (Slack, Discord, Teams), email service, PDF export, input validation, error handling with 4 fallback layers...

Was it a lot of work? Yes. But here's the thing — I did it once, and now I own every line.

The Knowledge Compound Effect

There's a concept I keep coming back to: the knowledge is the product.

When you install a package, you get functionality. When you build a package, you get functionality and understanding. That understanding compounds over time.

Because I built the middleware pipeline, I know exactly how to add a new security layer without touching existing code. Because I built the template engine, I know how to add a new directive in 20 minutes. Because I built the query builder, I know exactly what SQL it generates — there's never a "what query is this ORM actually running?" moment.

This matters most when things go wrong. In production, when a request is slow, I don't have to guess which layer the bottleneck is in. The middleware timing system tells me exactly: SessionMiddleware took 2ms, SecurityHeaders took 0.1ms, CSRF took 0.3ms, Authentication took 15ms — there's my problem.

The Deployment Argument

There's another reason I went zero-dependency that rarely gets discussed: deployment simplicity.

Shinobi Apps is designed to run on shared hosting. No Docker required. No nginx config. No build pipeline. No Node.js. No webpack. The deployment process is:

  1. Upload files
  2. Point document root to public/
  3. It works
The .htaccess handles URL rewriting. Static assets are served before the framework even boots — the entry point checks for CSS/JS files first and serves them directly, bypassing the autoloader, container, and middleware entirely. The entire entry point is 29 lines.

Try deploying a Laravel or Symfony application to a shared host where you don't control the server config. It's possible, but it's not fun. With zero dependencies and no build step, Shinobi deploys anywhere PHP runs.

When Zero Dependencies is Wrong

I want to be honest about the trade-offs. Going zero-dependency is not always the right call.

If you're building a product with a team of 20, you probably want the ecosystem. The time saved by using well-tested, well-documented packages outweighs the learning cost.

If you need a specific protocol implementation — like OAuth 2.0 or SAML — writing it from scratch is a security risk. Those specs are complex and the edge cases are non-obvious. Use a library.

If your project has a 3-month deadline, don't build a framework. Use one. Ship the product.

Zero dependencies made sense for Shinobi Apps because:

  • It's a platform I maintain long-term, not a short-lived project

  • I have an ethical hacking background, so I trust my own security code

  • I wanted maximum control over performance characteristics

  • I wanted to deploy anywhere without server configuration

  • And honestly — I wanted to understand every single thing my code does


The Numbers

Here's where the project stands today:

| What | Count |
|------|-------|
| Runtime Composer packages | 0 |
| Core framework PHP files | 145 |
| Core framework lines of code | ~34,000 |
| Total PHP files | 788 |
| Total JS files | 144 |
| Total CSS files | 114 |
| Total lines of code | 210,000+ |
| Addons (pluggable subsystems) | 11 |
| Business modules | 8 |
| Entry point (index.php) | 29 lines |
| Bootstrap stages | 5 |
| Services in DI container | 100+ |

All of it — zero runtime dependencies. All of it — one ApplicationFactory::run() call.

What's Next

In the next post, I'll show you exactly what happens when that ApplicationFactory::run() call executes. Five bootstrap stages, from loading the environment config to registering every route in every module — with timing for each stage.

We'll look at real code from Bootstrap.php and trace the complete startup sequence, line by line.

Comments (0)

No comments yet. Be the first to share your thoughts!

Leave a Comment

Recent Posts