{"id":51595369,"url":"https://github.com/omaressaouaf/plain-kit","last_synced_at":"2026-07-11T18:30:28.646Z","repository":{"id":370859362,"uuid":"1295661281","full_name":"omaressaouaf/plain-kit","owner":"omaressaouaf","description":"A small PHP toolkit for building MVC-style demos and prototypes without a full framework","archived":false,"fork":false,"pushed_at":"2026-07-11T13:15:42.000Z","size":56,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-11T15:09:02.067Z","etag":null,"topics":["framework","php","php-framework","php-frameworks","php-micro-framework"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/omaressaouaf.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-09T18:38:59.000Z","updated_at":"2026-07-11T13:16:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/omaressaouaf/plain-kit","commit_stats":null,"previous_names":["omaressaouaf/plain-kit"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/omaressaouaf/plain-kit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omaressaouaf%2Fplain-kit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omaressaouaf%2Fplain-kit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omaressaouaf%2Fplain-kit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omaressaouaf%2Fplain-kit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/omaressaouaf","download_url":"https://codeload.github.com/omaressaouaf/plain-kit/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omaressaouaf%2Fplain-kit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35372639,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-11T02:00:05.354Z","response_time":104,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framework","php","php-framework","php-frameworks","php-micro-framework"],"created_at":"2026-07-11T18:30:26.555Z","updated_at":"2026-07-11T18:30:28.641Z","avatar_url":"https://github.com/omaressaouaf.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PlainKit\n\n[![Latest Stable Version](https://img.shields.io/packagist/v/omaressaouaf/plain-kit.svg)](https://packagist.org/packages/omaressaouaf/plain-kit)\n[![License](https://img.shields.io/github/license/omaressaouaf/plain-kit)](LICENSE)\n[![Tests](https://github.com/omaressaouaf/plain-kit/actions/workflows/tests.yml/badge.svg)](https://github.com/omaressaouaf/plain-kit/actions/workflows/tests.yml)\n\nA small personal PHP toolkit for building **MVC-style demos, prototypes, and coding exercises** without installing a full framework.\n\nPlainKit gives you just enough structure around routing, middleware, request handling, sessions, CSRF protection, validation, simple views, environment configuration, and a lightweight service container — while staying close to plain PHP.\n\n\u003e PlainKit is **not** a production framework. It is not intended to replace Laravel, Symfony, Slim, or any mature PHP framework. It exists as a practical foundation for personal small projects, experiments, and learning how framework pieces fit together.\n\n---\n\n## Features\n\n- **Application bootstrap** with `App::create()` and fluent service binding\n- **Router** with HTTP verbs, middleware, route parameters, and file-based controllers\n- **Middleware** for `auth`, `guest`, and CSRF verification\n- **Request** and **Response** helpers for input, redirects, JSON, and views\n- **Session** storage with flash data\n- **CSRF** token generation and verification\n- **Form validation** flow with `Form` and `Validator`\n- **Authenticator** for login, logout, and password updates\n- **Database** wrapper around PDO\n- **Environment** loading via `.env` ([`vlucas/phpdotenv`](https://github.com/vlucas/phpdotenv))\n- Global **helpers** for paths, escaping, debugging, and env access\n\n---\n\n## Requirements\n\n- PHP **8.2+**\n- Composer\n\n---\n\n## Installation\n\nInstall via Composer:\n\n```sh\ncomposer require omaressaouaf/plain-kit\n```\n\n---\n\n## Quick Start\n\nA PlainKit application needs:\n\n1. A **public entry point** (`public/index.php`)\n2. A **routes file** (`routes.php`)\n3. **Controllers** under `app/Http/Controllers/`\n4. **Views** under `app/Views/`\n5. Optional **config**, **`.env`**, repositories, and services\n\n### Entry point\n\n```php\n\u003c?php\n\nuse Omaressaouaf\\PlainKit\\App;\n\nrequire __DIR__ . '/../../vendor/autoload.php';\n\nApp::create(dirname(__DIR__))\n    -\u003ebind(ClientService::class, fn () =\u003e new ClientService())\n    -\u003erun();\n```\n\n`App::create()` will:\n\n- set the application base path (`PLAINKIT_BASE_PATH`)\n- load `.env` from the app root (if present)\n- start the session\n- register default PlainKit bindings\n- load `routes.php` and handle the incoming request\n\n### Routes\n\n```php\n\u003c?php\n\nuse Omaressaouaf\\PlainKit\\Router;\n\n/** @var Router $router */\n\n$router-\u003eget('/login', 'Auth/Login/create')-\u003emiddleware('guest');\n$router-\u003epost('/login', 'Auth/Login/store')-\u003emiddleware('guest');\n$router-\u003eget('/clients', 'Clients/index')-\u003emiddleware('auth');\n$router-\u003epost('/clients', 'Clients/store')-\u003emiddleware('auth');\n$router-\u003eget('/clients/{id}', 'Clients/show')-\u003emiddleware('auth');\n```\n\nRoutes map to PHP files at `app/Http/Controllers/{path}.php`.\n\n### Controller\n\nControllers are plain PHP files. Resolve dependencies from the container with `App::resolve()`:\n\n```php\n\u003c?php\n\nuse Omaressaouaf\\PlainKit\\App;\nuse Omaressaouaf\\PlainKit\\Request;\nuse Omaressaouaf\\PlainKit\\Response;\nuse Omaressaouaf\\PlainKit\\Session;\nuse Http\\Forms\\StoreClientForm;\nuse Services\\ClientService;\n\n$request = App::resolve(Request::class);\n$response = App::resolve(Response::class);\n$session = App::resolve(Session::class);\n$clientService = App::resolve(ClientService::class);\n\nStoreClientForm::validate();\n\n$clientService-\u003ecreate($request-\u003einput('name'));\n\n$session-\u003eflash('success', 'Client created successfully!');\n\n$response-\u003eback();\n```\n\n---\n\n## Project Structure\n\nA typical app built with PlainKit looks like this:\n\n```txt\nmy-app/\n├── app/\n│   ├── Http/\n│   │   ├── Controllers/\n│   │   └── Forms/\n│   ├── Repositories/\n│   ├── Services/\n│   └── Views/\n├── config/\n│   └── app.php\n├── database/\n│   └── migrate.php\n├── public/\n│   └── index.php\n├── routes.php\n├── .env\n└── .env.example\n```\n\nThe **package** lives in `vendor/omaressaouaf/plain-kit`. Your **app root** is the directory passed to `App::create()` — usually one level above `public/`.\n\n---\n\n## Environment\n\nPlainKit uses [`vlucas/phpdotenv`](https://github.com/vlucas/phpdotenv) to load a `.env` file from the application root.\n\n`.env.example`:\n\n```dotenv\nDB_HOST=localhost\nDB_PORT=3306\nDB_DATABASE=plain_kit_ledger\nDB_USERNAME=root\nDB_PASSWORD=\n```\n\n`config/app.php`:\n\n```php\n\u003c?php\n\nreturn [\n    'database' =\u003e [\n        'connection' =\u003e [\n            'host' =\u003e env('DB_HOST', 'localhost'),\n            'port' =\u003e (int) env('DB_PORT', 3306),\n            'dbname' =\u003e env('DB_DATABASE', 'plain_kit_ledger'),\n            'charset' =\u003e 'utf8mb4',\n        ],\n        'username' =\u003e env('DB_USERNAME', 'root'),\n        'password' =\u003e env('DB_PASSWORD', ''),\n    ],\n];\n```\n\nHelpers:\n\n```php\nload_env('/path/to/app');   // usually called automatically by App::create()\nenv('DB_HOST', 'localhost'); // read a variable with optional default\n```\n\n---\n\n## Helpers\n\nPlainKit registers these global helpers:\n\n| Helper                                  | Purpose                       |\n| --------------------------------------- | ----------------------------- |\n| `base_path('config/app.php')`           | Path relative to the app root |\n| `app_path('Http/Controllers/home.php')` | Path relative to `app/`       |\n| `env('KEY', 'default')`                 | Read an environment variable  |\n| `load_env($basePath)`                   | Load `.env` from a directory  |\n| `e($value)`                             | Escape HTML (returns string)  |\n| `_r($value)`                            | Echo escaped HTML             |\n| `dd(...$values)`                        | Dump and die                  |\n\n`base_path()` requires `PLAINKIT_BASE_PATH` to be defined (via `App::create()` or manually).\n\n---\n\n## App \u0026 Container\n\n### Bootstrapping\n\n```php\nApp::create(dirname(__DIR__))\n    -\u003ebind(UserRepository::class, fn () =\u003e new UserRepository())\n    -\u003ebind(RegisterService::class, fn () =\u003e new RegisterService())\n    -\u003erun();\n```\n\n### Default bindings\n\nThese are registered automatically:\n\n- `Session`\n- `Csrf`\n- `Middleware`\n- `Request`\n- `Response`\n- `Router`\n- `Database`\n- `Authenticator`\n\nRegister your own classes with `-\u003ebind()`:\n\n```php\n-\u003ebind(ClientService::class, fn () =\u003e new ClientService())\n```\n\n### Resolving services\n\nInside controllers, middleware, forms, repositories, or views:\n\n```php\n$request = App::resolve(Request::class);\n$userRepository = App::resolve(UserRepository::class);\n```\n\nIf a binding is missing, PlainKit throws `BindingNotFoundException`.\n\n---\n\n## Router\n\n### HTTP verbs\n\n```php\n$router-\u003eget('/reports', 'Reports/index');\n$router-\u003epost('/transactions', 'Transactions/store');\n$router-\u003edelete('/logout', 'Auth/Login/destroy');\n$router-\u003eput('/items/{id}', 'Items/update');\n$router-\u003epatch('/items/{id}', 'Items/patch');\n```\n\n### Middleware\n\nAttach middleware to the most recently registered route:\n\n```php\n$router-\u003eget('/clients', 'Clients/index')-\u003emiddleware('auth');\n$router-\u003eget('/login', 'Auth/Login/create')-\u003emiddleware('guest');\n```\n\nBuilt-in middleware keys:\n\n| Key     | Behavior                                                                            |\n| ------- | ----------------------------------------------------------------------------------- |\n| `auth`  | Redirect to `/login` if the user is not logged in                                   |\n| `guest` | Redirect to `/` if the user is already logged in                                    |\n| `csrf`  | Verify CSRF token on `POST` requests (applied automatically on every matched route) |\n\n### Route parameters\n\n```php\n$router-\u003eget('/clients/{id}', 'Clients/show');\n```\n\nIn a controller:\n\n```php\n$id = $request-\u003eparams('id');\n$all = $request-\u003eparams(); // all route params as array\n```\n\nPlace static routes before parameterized ones:\n\n```php\n$router-\u003eget('/clients/list', 'Clients/list');   // match first\n$router-\u003eget('/clients/{id}', 'Clients/show');     // match second\n```\n\n### Method spoofing\n\nHTML forms can simulate `PUT`, `PATCH`, or `DELETE` via a hidden `_method` field:\n\n```html\n\u003cinput type=\"hidden\" name=\"_method\" value=\"DELETE\" /\u003e\n```\n\nThe router reads `$_POST['_method']` when handling the request.\n\n### 404 handling\n\nWhen no route matches, PlainKit calls `Response::abort(404)`, which loads:\n\n```txt\napp/Http/Controllers/Failures/404.php\n```\n\n---\n\n## Request\n\n```php\n$request = App::resolve(Request::class);\n\n$request-\u003euri();                          // \"/clients\"\n$request-\u003eabs_uri();                      // \"/clients?page=2\"\n$request-\u003emethod();                         // \"GET\", \"POST\", etc.\n$request-\u003einput('email');                 // from $_GET or $_POST\n$request-\u003einput('email', 'default');      // with fallback\n$request-\u003eparams('id');                   // route parameter\n$request-\u003eparams();                       // all route parameters\n```\n\n---\n\n## Response\n\n```php\n$response = App::resolve(Response::class);\n\n// Render a view from app/Views/{name}.view.php\n$response-\u003eview('clients', ['clients' =\u003e $clients]);\n\n// JSON\n$response-\u003ejson(['reports' =\u003e $reports]);\n\n// Redirect\n$response-\u003eredirect('/login');\n\n// Go back to the previous page (falls back to \"/\")\n$response-\u003eback();\n\n// Abort with an HTTP status code\n$response-\u003eabort(404);\n$response-\u003eabort(419); // CSRF failure\n```\n\nViews receive context variables via `extract()` and can include partials:\n\n```php\n$response-\u003eview('Partials/head');\n$response-\u003eview('Partials/nav');\n```\n\nUse `e()` in views to escape output:\n\n```php\n\u003cp\u003eWelcome, \u003c?= e($user['name']) ?\u003e!\u003c/p\u003e\n```\n\n---\n\n## Session\n\n```php\n$session = App::resolve(Session::class);\n\n$session-\u003eput('user', $user);\n$session-\u003eget('user');\n$session-\u003ehas('user');\n$session-\u003eforget('user');\n\n// Flash data (available on the next request)\n$session-\u003eflash('success', 'Saved!');\n$session-\u003eflash('errors', ['email' =\u003e 'Invalid email']);\n$session-\u003eget('success');\n```\n\nFlash data is cleared automatically at the end of each request via `Router::handleRequest()`.\n\n---\n\n## CSRF\n\nGenerate a token in a form:\n\n```php\n$csrf = App::resolve(Csrf::class);\n```\n\n```html\n\u003cinput type=\"hidden\" name=\"_csrf_token\" value=\"\u003c?= $csrf-\u003egenerate() ?\u003e\" /\u003e\n```\n\n`VerifyCsrf` middleware runs on every matched route. On `POST` requests it checks the submitted `_csrf_token`. Invalid or missing tokens trigger a `419` response.\n\n---\n\n## Forms \u0026 Validation\n\n### Creating a form\n\nExtend `Form` and implement `handle()`:\n\n```php\n\u003c?php\n\nnamespace Http\\Forms;\n\nuse Omaressaouaf\\PlainKit\\Form;\nuse Omaressaouaf\\PlainKit\\Validator;\n\nclass LoginForm extends Form\n{\n    protected function handle(): void\n    {\n        if (! Validator::exists($this-\u003erequest-\u003einput('email'))\n            || ! Validator::email($this-\u003erequest-\u003einput('email'))) {\n            $this-\u003eaddError('email', 'Email must be valid');\n        }\n\n        if (! Validator::exists($this-\u003erequest-\u003einput('password'))) {\n            $this-\u003eaddError('password', 'Password is required');\n        }\n    }\n}\n```\n\n### Validating in a controller\n\n```php\nLoginForm::validate();\n```\n\nIf validation fails, PlainKit throws `ValidationException`. The router catches it, flashes `errors` and `old` input to the session, and redirects back.\n\nDisplay errors in a view partial:\n\n```php\n$errors = $session-\u003eget('errors');\n```\n\n### Validator methods\n\n| Method                          | Description              |\n| ------------------------------- | ------------------------ |\n| `Validator::exists($value)`     | Not empty                |\n| `Validator::email($value)`      | Valid email              |\n| `Validator::string($value)`     | Non-empty trimmed string |\n| `Validator::numeric($value)`    | Numeric value            |\n| `Validator::in($value, $array)` | Value in array           |\n| `Validator::min($value, $min)`  | Min length/count/numeric |\n| `Validator::max($value, $max)`  | Max length/count/numeric |\n\n---\n\n## Database\n\nPlainKit includes a thin PDO wrapper. Configuration is read from `config/app.php`. The connection DSN is currently MySQL-only (`mysql:...`).\n\n```php\n$database = App::resolve(Database::class);\n\n$users = $database\n    -\u003equery('SELECT * FROM users WHERE email = :email', ['email' =\u003e $email])\n    -\u003eget();\n\n$user = $database\n    -\u003equery('SELECT * FROM users WHERE id = :id', ['id' =\u003e $id])\n    -\u003efind();\n\n$user = $database\n    -\u003equery('SELECT * FROM users WHERE id = :id', ['id' =\u003e $id])\n    -\u003efindOrFail(); // aborts with 404 if not found\n```\n\nTypical usage in a repository:\n\n```php\n\u003c?php\n\nnamespace Repositories;\n\nuse Omaressaouaf\\PlainKit\\App;\nuse Omaressaouaf\\PlainKit\\Database;\n\nclass ClientRepository\n{\n    private Database $database;\n\n    public function __construct()\n    {\n        $this-\u003edatabase = App::resolve(Database::class);\n    }\n\n    public function get(): array\n    {\n        return $this-\u003edatabase\n            -\u003equery('SELECT * FROM clients ORDER BY id DESC')\n            -\u003eget();\n    }\n}\n```\n\n---\n\n## Authenticator\n\nSession-based authentication helper:\n\n```php\n$authenticator = App::resolve(Authenticator::class);\n\n// Login attempt\nif ($authenticator-\u003eattempt($email, $password)) {\n    $response-\u003eredirect('/');\n}\n\n// Current user\n$authenticator-\u003euser();   // array|null\n$authenticator-\u003echeck();   // bool\n\n// Logout\n$authenticator-\u003elogout();\n\n// Update password for the logged-in user\n$authenticator-\u003eupdatePassword($newPassword);\n```\n\nOn successful login, the user record is stored in the session under the `user` key and the session ID is regenerated.\n\n---\n\n## Middleware\n\nAll middleware implements `MiddlewareInterface`:\n\n```php\ninterface MiddlewareInterface\n{\n    public function handle(): void;\n}\n```\n\nBuilt-in middleware is resolved by key through the `Middleware` class. To add custom middleware you would extend the middleware map in your own fork or wrapper — PlainKit keeps this intentionally small.\n\n---\n\n## Exceptions\n\n| Exception                     | When                                                  |\n| ----------------------------- | ----------------------------------------------------- |\n| `BindingNotFoundException`    | Container binding not registered                      |\n| `MiddlewareNotFoundException` | Unknown middleware key                                |\n| `ValidationException`         | Form validation failed (carries `$errors` and `$old`) |\n\n---\n\n## Example App\n\nThis repository includes a **ledger demo** at [`examples/ledger`](examples/ledger) that demonstrates:\n\n- user registration and login\n- CRUD for clients and transactions\n- filtered reports\n- a JSON API endpoint at `/api/reports`\n- forms, repositories, services, CSRF, and middleware\n\n### Run the example\n\nFrom the repository root:\n\n```sh\ncomposer install\ncp examples/ledger/.env.example examples/ledger/.env\nphp examples/ledger/database/migrate.php\nphp -S localhost:8080 -t examples/ledger/public\n```\n\nSee [`examples/ledger/README.md`](examples/ledger/README.md) for more detail.\n\n---\n\n## Testing\n\nRun unit tests:\n\n```sh\ncomposer test\n```\n\n---\n\n## License\n\nThis package is licensed under the [MIT License](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fomaressaouaf%2Fplain-kit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fomaressaouaf%2Fplain-kit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fomaressaouaf%2Fplain-kit/lists"}