타누키 프레임워크 (Tanuki Framework): 실제로 PHP 작성을 좋아하는 사람들을 위한 PHP 프레임워크

작성자

카테고리:

← 피드로
DEV Community · Technomantus Corvi · 2026-09-06 개발(SW)

The problem I kept running into

Every time I started a small PHP project, I had two bad options.

Option one: grab a full framework. Get an ORM I have to learn a query language for, a template engine that’s basically PHP-but-different, a service container, middleware pipelines, and a folder structure designed by a committee. Half my “learning” time goes to the framework’s opinions, not my actual app.

Option two: go fully raw PHP. Rebuild routing, rebuild a database wrapper, rebuild CSRF protection, rebuild sessions — for the fifth time, slightly worse than the last, because I’m rushing.

I wanted something in between: the boring plumbing solved once, in code I can actually read, with nothing hidden underneath.

That’s Tanuki.

What Tanuki actually is

A lightweight PHP MVC framework with:

  • A router that maps 'GET /todo/{id}' => 'TodoController@show' — no annotations, no attribute magic, just an array.
  • A Model base class that’s a thin, honest wrapper over PDO — TodoModel::where('completed', 0) builds a real prepared statement you could write yourself, it just saves you the typing.
  • Views that are just… PHP files. <?= e($todo['title']) ?>. No new syntax.
  • Zero ORM. Zero template engine. Zero dependency injection container.

Having full freedom to work your own vanilla way, or to follow an already-designed, basic and simple MVC pattern, gives you freedom without any kind of magic.

Here’s the entire flow of a request, and you can read the actual source in about ten minutes:

Browser → public/index.php → App::run()
│
├── loadEnv()
├── registerAutoloader()
├── configureErrors()
└── dispatch()
│
├── Matches an exact route or a {param} pattern
└── Instantiates the Controller → calls method()
│
└── $this->view('name', $data)

Enter fullscreen mode Exit fullscreen mode

That’s it. That’s the whole request lifecycle.

Who this is for

If you’re the kind of developer who:

  • would rather write raw SQL than learn an ORM’s query builder,
  • gets annoyed when a framework does something “for you” that you didn’t ask for,
  • deletes code more readily than you write new abstractions,

…you’ll probably feel at home here. This isn’t trying to compete with Laravel or Symfony on features — it’s deliberately smaller in scope, aimed at people who want a real MVC skeleton without the weight.

A real example: the TODO CRUD

The repo ships with a complete, working TODO list as a learning reference:

class TodoController extends Controller
{
    public function store(): void
    {
        if (!csrf_verify($this->request->post('_token'))) {
            $this->flash('error', 'Your session expired. Please try again.');
            $this->redirect('/todo/create');
        }

        $title = $this->request->post('title');

        if (empty($title)) {
            keep_old(['title' => $title]);
            $this->flash('error', 'Title is required.');
            $this->redirect('/todo/create');
        }

        TodoModel::create(['title' => $title, 'completed' => 0]);
        $this->flash('success', 'Task created!');
        $this->redirect('/todo');
    }
}

Enter fullscreen mode Exit fullscreen mode

CSRF check, form repopulation on validation failure, flash messages — all real patterns you’ll reuse for your own resources, not hidden behind a form-request class you have to configure.

What’s included, and what’s optional

The core is: routing, models, views, session handling, CSRF helpers, and a small i18n system (t('nav.home') + JSON dictionaries, with an optional URL-prefixed language switcher — /en/todo, /es/todo).

Beyond that, two extensions ship in the repo but stay completely inert until you wire them in:

  • tanuki_login — session-based auth: login, registration, password recovery by email, profile editing. Zero routes registered by default.
  • tanuki_admin — a Django-inspired admin panel. Register a model in one array, get a full CRUD interface for it:
// admin/admin.php
return [
    'todo' => [
        'model'       => TodoModel::class,
        'label'       => 'Tasks',
        'list_fields' => ['id', 'title', 'completed', 'created_at'],
        'form_fields' => [
            'title'     => ['type' => 'text', 'label' => 'Title'],
            'completed' => ['type' => 'checkbox', 'label' => 'Completed'],
        ],
    ],
];

Enter fullscreen mode Exit fullscreen mode

Don’t want either? Delete the folder and the two lines in routes.php that reference it. Nothing else in the project depends on them — that isolation was a deliberate design constraint from day one.

Security, taken seriously without being invisible

  • Every Model method uses prepared statements, and column/table names are validated against a strict identifier pattern (this is a real fix — I found and closed a column-name injection vector during development, and there’s a regression test for it).
  • CSRF protection exists as explicit helpers (csrf_field(), csrf_verify()) you add per form — not a blanket policy silently applied to every route, which would break webhook-style endpoints that don’t use sessions.
  • Passwords are password_hash()/password_verify(), reset tokens are single-use SHA-256 hashes with expiry, sessions regenerate on login/logout to prevent fixation.

Nothing here is exotic — it’s the standard stuff done correctly and made visible, not buried in a security middleware you never read.

Translations that don’t get in your way

i18n is one of those things frameworks either skip entirely or turn into an entire subsystem you have to learn. Tanuki does the minimum useful version: one JSON dictionary per language, one helper.

// lang/en.json
{ "nav": { "home": "Home" }, "todo": { "flash_created": "Task created successfully!" } }

Enter fullscreen mode Exit fullscreen mode

t('nav.home')                    // → "Home"
t('todo.flash_created')          // → "Task created successfully!"
t('todo.count_summary', ['total' => 5]) // → replaces ":total" in the string

Enter fullscreen mode Exit fullscreen mode

If a key is missing in the active language, it falls back to English; if it’s missing everywhere, you get the raw key back instead of a broken page — a missing translation is visible, not a silent failure.

There’s also a URL-based language switcher (/en/todo, /es/todo) wired in from the start, but it stays completely inactive unless you set ACCEPTED_LANGUAGES in .env:

ACCEPTED_LANGUAGES=en,es

Enter fullscreen mode Exit fullscreen mode

Visit /es/todo once, and the choice sticks in the session — every other link on the site works without a prefix. Don’t set that variable at all, and the switcher never renders: the framework assumes your project is single-language by default, and only asks you to think about locales when you actually need more than one.

Getting started

composer create-project forja-de-onix/tanuki-framework my-project
cd my-project
cp .env-example .env
nano .env   # set DB_NAME, DB_USER, DB_PASS, APP_URL
php -S localhost:8050 -t public public/index.php

Enter fullscreen mode Exit fullscreen mode

Visit /todo for the working example, or start from scratch with your own route → controller → model → view.

Long-term support, by design

Tanuki isn’t meant to chase trends. The goal is a framework stable enough that a project built on it today still works, unchanged, in five years — minor patches, no breaking rewrites. If that sounds appealing, or if you just want to poke around a small, readable PHP codebase, the repo and full wiki documentation are linked below.

Packagist: packagist.org/packages/forja-de-onix/tanuki-framework
Repo: github.com/Forja-de-Onix/tanuki-framework
Wiki (full docs): github.com/Forja-de-Onix/tanuki-framework/wiki/Home

Feedback, issues, and PRs welcome — especially from other vanilla-PHP fans who want to poke holes in the design.

원문에서 계속 ↗