4asuite/nano-php

Ultra-minimalist PHP framework for small websites

Maintainers

Package info

github.com/4asuite/nano-php

Type:project

pkg:composer/4asuite/nano-php

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.5 2026-03-21 12:07 UTC

This package is auto-updated.

Last update: 2026-04-21 12:31:49 UTC


README

A minimal PHP microframework. No magic, no bloat — just routing, controllers, views, and a few security essentials.

Author: Vlado Majoros, 4aSuite License: MIT

Requirements

  • PHP ^8.1
  • Apache with mod_rewrite (or equivalent front-controller setup)
  • No external dependencies for the core

Installation

composer create-project 4asuite/nano-php myproject
cd myproject

Point your web server document root to the public/ directory. Open the project in a browser — the welcome page confirms the framework is running.

Project structure

myproject/
├── App/
│   ├── Config/
│   │   └── routes.php          # Route definitions
│   ├── Controllers/
│   │   └── NanofwController.php
│   ├── Core/                   # Framework core (do not modify)
│   │   ├── App.php
│   │   ├── Controller.php
│   │   ├── Csrf.php
│   │   ├── EnvLoader.php
│   │   ├── HttpException.php
│   │   ├── Lang.php
│   │   ├── Request.php
│   │   ├── Response.php
│   │   ├── Router.php
│   │   ├── Session.php
│   │   └── View.php
│   ├── Models/
│   │   └── Model.php           # Base PDO model
│   ├── views/
│   │   ├── layouts/
│   │   │   ├── main.php
│   │   │   └── blank.php
│   │   └── partials/
│   │       └── head.php
│   └── bootstrap.php
├── public/
│   ├── index.php               # Front controller (web root)
│   └── .htaccess
├── .env
└── .env.example

Configuration

Copy .env.example to .env and adjust as needed. Values are exposed as PHP constants.

APP_ENV=development
APP_DEFAULT_LANG=en

# Database (only if using Model)
DB.HOST=localhost
DB.NAME=mydb
DB.USER=root
DB.PASS=

Dot-notation keys (DB.HOST) are grouped into a single array constant DB['HOST'].

Routing

Define routes in App/Config/routes.php:

return [
    'GET' => [
        '/'             => [App\Controllers\HomeController::class, 'index'],
        '/users/(:num:id)' => [App\Controllers\UserController::class, 'show'],
        '/posts/(:any:slug)' => [App\Controllers\PostController::class, 'show'],
    ],
    'POST' => [
        '/users' => [App\Controllers\UserController::class, 'store'],
    ],
];

Route parameter types:

Syntax Matches Example
(:num:id) Digits only /users/42
(:any:slug) Any non-slash string /posts/hello-world

A missing route throws HttpException(404), which the framework handles automatically.

Controllers

Extend App\Core\Controller. Every action must return a Response instance.

<?php
namespace App\Controllers;

use App\Core\Controller;
use App\Core\Response;

final class UserController extends Controller
{
    public function show(string $id): Response
    {
        return $this->view('users/show', ['id' => $id]);
    }

    public function store(): Response
    {
        $name = $this->request->post('name', '');
        // ...
        return $this->redirect('/users');
    }

    public function api(): Response
    {
        return $this->json(['status' => 'ok']);
    }
}

Available in every controller:

Method Description
$this->request->get($key) GET parameter
$this->request->post($key) POST parameter
$this->request->input($key) POST or GET
$this->request->method() HTTP method string
$this->request->path() Normalized URL path
$this->view($view, $data, $layout) Render view → Response
$this->json($data, $status) JSON response
$this->redirect($url, $code) Redirect (internal only)

Views

View files live in App/views/. Pass data as an associative array — keys become variables inside the template.

// Controller
return $this->view('users/show', ['name' => 'Alice'], 'main');
<!-- App/views/users/show.php -->
<h1><?= e($name) ?></h1>

Use the global e() helper to escape output (htmlspecialchars wrapper).

Layouts live in App/views/layouts/. The rendered view is passed as $content:

<!-- App/views/layouts/main.php -->
<!DOCTYPE html>
<html lang="<?= e(APP_DEFAULT_LANG) ?>">
<head><?php require __DIR__ . '/../partials/head.php'; ?></head>
<body><?= $content ?></body>
</html>

Built-in layouts: main (full HTML5), blank (raw output, no wrapper).

Session

use App\Core\Session;

Session::set('user_id', 42);
$id  = Session::get('user_id');
$has = Session::has('user_id');
Session::remove('user_id');
Session::destroy();   // clears cookie + regenerates ID

Cookies are configured with httponly, samesite=Lax, use_strict_mode. secure flag is set automatically when HTTPS is detected.

CSRF

use App\Core\Csrf;

// In a view form:
<?= Csrf::input() ?>

// In a POST controller action:
Csrf::verify($this->request->post('_csrf'));   // throws HttpException(419) on failure

Token is 64-character hex (random_bytes(32)), stored in session, rotated on each verification.

Language / Translations

use App\Core\Lang;

Lang::load(['GREETING' => 'Hello, %s!']);

echo Lang::get('greeting', 'Alice');  // Hello, Alice!

Translation keys are uppercased and defined as PHP constants. Falls back to the key name if not defined.

Security headers

Sent automatically on every response:

X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains

Override any header via $this->response->header($key, $value) before returning from the controller.

HTTP errors

Throw HttpException from anywhere — the framework catches it and sends the appropriate HTTP status:

use App\Core\HttpException;

throw new HttpException(403, 'Access denied.');
throw new HttpException(404, 'Not found.');

License

MIT © Vlado Majoros, 4aSuite