{"id":51641283,"url":"https://github.com/michaelyousrie/batframe","last_synced_at":"2026-07-19T00:01:14.977Z","repository":{"id":370957946,"uuid":"1298053112","full_name":"michaelyousrie/batframe","owner":"michaelyousrie","description":"A tiny Flask-like PHP microframework where each public method is an endpoint and routes are inferred from method names.","archived":false,"fork":false,"pushed_at":"2026-07-16T03:19:26.000Z","size":150,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-16T22:28:26.694Z","etag":null,"topics":["api","blade","flask","framework","microframework","php","router"],"latest_commit_sha":null,"homepage":null,"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/michaelyousrie.png","metadata":{"files":{"readme":"README.md","changelog":null,"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-12T06:51:16.000Z","updated_at":"2026-07-16T03:19:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/michaelyousrie/batframe","commit_stats":null,"previous_names":["michaelyousrie/batframe"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/michaelyousrie/batframe","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelyousrie%2Fbatframe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelyousrie%2Fbatframe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelyousrie%2Fbatframe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelyousrie%2Fbatframe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/michaelyousrie","download_url":"https://codeload.github.com/michaelyousrie/batframe/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelyousrie%2Fbatframe/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35598365,"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-17T02:00:06.162Z","response_time":116,"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":["api","blade","flask","framework","microframework","php","router"],"created_at":"2026-07-13T19:03:29.175Z","updated_at":"2026-07-17T23:01:38.301Z","avatar_url":"https://github.com/michaelyousrie.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Batframe\n\nA tiny, Flask-like PHP microframework. You extend one class, write public methods, and each\nmethod becomes an HTTP endpoint whose route is inferred from its name. No route files, no\ndecorators, no config to get started, but every moving part is swappable when you outgrow the\ndefaults.\n\nBatframe is for the project that is too small for Laravel or Symfony: a quick JSON API, a\nhandful of endpoints, or a small static site.\n\n```php\nuse Batframe\\Batframe;\nuse Batframe\\Http\\Request;\nuse Batframe\\Http\\Response;\n\nclass App extends Batframe\n{\n    public function index()                     // GET /\n    {\n        return view('home', ['name' =\u003e 'World']);\n    }\n\n    public function getUsers()                  // GET /users\n    {\n        return ['users' =\u003e ['ada', 'linus']];   // arrays become JSON automatically\n    }\n\n    public function getUser(int $id)            // GET /user/{id}\n    {\n        return ['id' =\u003e $id];\n    }\n\n    public function postUsers(Request $request) // POST /users\n    {\n        return Response::json(['created' =\u003e $request-\u003einput('name')], 201);\n    }\n}\n\n(new App())-\u003erun();\n```\n\n## Requirements\n\n- PHP 8.1+\n\n## Install\n\nThe fastest way to start is the skeleton, which scaffolds a ready-to-run app\n(route traits, Blade views, static pages, a front controller, and `.env`):\n\n```bash\ncomposer create-project michaelyousrie/batframe-skeleton my-app\ncd my-app\ncomposer serve\n```\n\nSee [batframe-skeleton](https://github.com/michaelyousrie/batframe-skeleton) for\ndetails. To add Batframe to an existing project instead:\n\n```bash\ncomposer require michaelyousrie/batframe\n```\n\n## The routing convention\n\nThe route is derived entirely from the method name and its parameters.\n\n| Method                                   | Route                            |\n| ---------------------------------------- | -------------------------------- |\n| `index()`                                | `GET /`                          |\n| `getUsers()`                             | `GET /users`                     |\n| `getUserProfile()`                       | `GET /user/profile`              |\n| `getUser(int $id)`                       | `GET /user/{id}`                 |\n| `getUserPost(int $userId, int $postId)`  | `GET /user/post/{userId}/{postId}` |\n| `postUsers()`                            | `POST /users`                    |\n| `putUser(int $id)`                       | `PUT /user/{id}`                 |\n| `deleteUser(int $id)`                    | `DELETE /user/{id}`              |\n\nRules:\n\n- The method name must **start with an HTTP verb**: `get`, `post`, `put`, `patch`, `delete`,\n  `head`, `options`. That sets the HTTP method.\n- The remaining PascalCase words become `/`-separated path segments, lowercased.\n- **No auto-pluralization**: the path is taken literally from the name. `getUser()` is `/user`;\n  for `/users` name it `getUsers()`.\n- `index()` is special-cased to `GET /`.\n- A public method that does **not** start with a verb is treated as an internal helper and is\n  **not** exposed as a route. Handy for shared logic; keep truly private helpers `private`.\n\n### Route parameters\n\n- Typed scalar parameters (`int`, `string`, `float`, `bool`) become `{placeholder}` path\n  segments in declared order. `int`/`float` also constrain the segment, so `/user/abc` will not\n  match `getUser(int $id)`.\n- A parameter typed `Request` is injected and may appear in any position:\n  `postUsers(Request $request)`.\n\n### Return values\n\nWhatever a handler returns is turned into a response:\n\n| Return value                       | Response                    |\n| ---------------------------------- | --------------------------- |\n| a `Response`                       | sent as-is                  |\n| an `array` / scalar / `JsonSerializable` | `application/json`    |\n| a `string`                         | `text/html`                 |\n| `null`                             | `204 No Content`            |\n\n### Grouping routes into traits\n\nRoutes don't have to live directly on your app class. Any verb-prefixed public\nmethod composed in from a **trait** is registered exactly like an inline method,\nso you can group related endpoints together and keep the app class tiny:\n\n```php\ntrait UserRoutes\n{\n    public function getUsers()          { /* GET /users */ }\n    public function getUser(int $id)    { /* GET /user/{id} */ }\n    public function postUsers()         { /* POST /users */ }\n\n    // verbless helper — shared by the routes above, not itself a route\n    protected function formatName(string $name): string { /* ... */ }\n}\n\nclass App extends Batframe\n{\n    use UserRoutes;\n    use PageRoutes;\n}\n```\n\nThe same convention rules apply inside a trait (verb prefix required, verbless\nmethods are helpers). See [`example/src/Routes/`](example/src/Routes/) for a\nworking split.\n\n## Requests\n\n```php\n$request-\u003einput('name', $default); // JSON body, then form body, then query string\n$request-\u003equery('q');              // single query param\n$request-\u003ejson();                  // decoded JSON body as an array\n$request-\u003eall();                   // everything merged\n$request-\u003eheader('Authorization'); // case-insensitive\n$request-\u003ebearerToken();           // Bearer token, or null\n$request-\u003emethod();                // \"GET\"\n$request-\u003epath();                  // \"/users\"\n$request-\u003ewantsJson();             // content negotiation\n$request-\u003eip();\n```\n\n## Responses\n\n```php\nResponse::json($data, 200);\nResponse::html('\u003ch1\u003eHi\u003c/h1\u003e');\nResponse::text('plain');\nResponse::view('home', ['name' =\u003e 'World']);   // rendered through the view engine\nResponse::redirect('/login');\nResponse::file('/path/to/file.pdf');\nResponse::noContent();\n\n// fluent\nResponse::json($data)-\u003estatus(201)-\u003eheader('X-Trace', 'abc');\n```\n\nHelper functions are available too: `view()`, `json()`, `response()`, `redirect()`,\n`abort(404)`, `env()`, `config()`, `session()`, `cache()`.\n\n## Sessions\n\nBatframe wraps PHP's native file-based sessions in a small, intuitive helper. The\nsession starts lazily the first time you touch it, so no cookie is sent unless you\nactually use it, and there's nothing to configure.\n\n```php\nsession()-\u003eput('user_id', 42);\n$id = session('user_id');              // 42\nsession(['theme' =\u003e 'dark', 'lang' =\u003e 'en']); // set several at once\n\nsession()-\u003ehas('user_id');             // present and not null\nsession()-\u003epull('cart');               // read and remove\nsession()-\u003eforget('user_id');\nsession()-\u003epush('items', $item);       // append to an array value\nsession()-\u003eincrement('visits');        // handy counters\nsession()-\u003eflush();                    // clear everything\n\n// flash data lives for the next request only\nsession()-\u003eflash('status', 'Saved!');\n$status = session('status');\n\nsession()-\u003eregenerate();               // new id, e.g. after login\nsession()-\u003edestroy();                  // end the session\n```\n\n`session()` with no argument returns the `Batframe\\Helpers\\Session` instance;\n`session('key')` reads a value; `session(['key' =\u003e 'value'])` writes.\n\n## Cache\n\nBatframe ships a small file-based cache, in the same spirit as the session helper. Every entry\ncarries its own time-to-live, so you decide per item whether it is short-lived or sticks around.\nPass a ttl in seconds, or `null` (the default) to keep it until you forget it.\n\n```php\ncache()-\u003eput('report', $data, 3600);   // expires in an hour\ncache()-\u003eforever('config', $config);   // never expires\n$data = cache('report');               // read, or null when missing/stale\n$data = cache('report', $fallback);    // read with a default\n\ncache(['a' =\u003e 1, 'b' =\u003e 2], 600);      // write several at once, ttl in seconds\ncache()-\u003eadd('lock', 1, 30);           // write only if absent (returns bool)\ncache()-\u003ehas('report');                // present and not expired\ncache()-\u003epull('report');               // read and remove\ncache()-\u003eincrement('hits');            // handy counters (ttl preserved)\ncache()-\u003eforget('report');\ncache()-\u003eflush();                      // clear everything\n\n// compute on miss, then cache for 10 minutes\n$users = cache()-\u003eremember('users', 600, fn () =\u003e load_users());\n$config = cache()-\u003erememberForever('config', fn () =\u003e load_config());\n```\n\n`cache()` with no argument returns the `Batframe\\Helpers\\Cache` instance; `cache('key')` reads;\n`cache(['key' =\u003e 'value'], $ttl)` writes. Entries live as files under the app's cache directory,\nso they survive across requests. Constructing `new Cache()` with no directory gives you a\nrequest-scoped, in-memory store instead.\n\n## Views (Blade)\n\nTemplating is powered by [BladeOne](https://github.com/EFTEC/BladeOne), a dependency-free engine\nwith the Blade syntax you know (`@extends`, `@section`, `@foreach`, `{{ $var }}`). Templates live\nin the `views/` directory:\n\n```blade\n{{-- views/home.blade.php --}}\n@extends('layouts.app')\n@section('content')\n    \u003ch1\u003eHello, {{ $name }}!\u003c/h1\u003e\n@endsection\n```\n\nThe engine is swappable, implement `Batframe\\View\\ViewEngine` and pass it in via the\n`view_engine` config key (or `Response::setViewEngine(...)`).\n\n## Static pages\n\nDrop a Blade or HTML file in the `pages/` directory and it is served automatically when a request\npath matches its name, with no controller method needed. `pages/about.blade.php` answers\n`/about`; `pages/index.blade.php` answers `/`. This makes small static sites trivial.\n\n## Configuration \u0026 environment\n\nA `.env` file in your project root is loaded automatically (via `vlucas/phpdotenv`). Read values\nwith `env('KEY', $default)`; literals like `true`/`false`/`null` are coerced. `APP_DEBUG=true`\nturns on detailed error pages.\n\nPass overrides to the constructor:\n\n```php\nnew App([\n    'base_path' =\u003e __DIR__ . '/..',\n    'views'     =\u003e 'resources/views',\n    'pages'     =\u003e 'resources/pages',\n    'cache'     =\u003e 'storage/cache',\n    'debug'     =\u003e true,\n]);\n```\n\nBy convention, if you don't set `base_path`, Batframe assumes your app class lives in\n`\u003cproject\u003e/src/` and derives the project root from there.\n\n## Project layout\n\n```\npublic/index.php     # front controller (document root)\nsrc/App.php          # your class extends Batframe\nviews/               # Blade templates\npages/               # auto-routed static pages\nstorage/cache/       # compiled Blade cache (writable)\n.env                 # environment\n```\n\nThe front controller is a one-liner:\n\n```php\nrequire __DIR__ . '/../vendor/autoload.php';\n(new App())-\u003erun();\n```\n\n## Running locally\n\n```bash\nphp -S localhost:8000 -t public\n```\n\nA complete, runnable example lives in [`example/`](example/).\n\n## Testing\n\n```bash\ncomposer install\nvendor/bin/phpunit\n```\n\n## Error handling\n\nThrow an `HttpException` (or call `abort()`) to abort with a status code:\n\n```php\nuse Batframe\\Http\\HttpException;\n\nthrow new HttpException(403, 'Forbidden');\nabort(404);\n```\n\nErrors are rendered as JSON or HTML based on content negotiation. When `APP_DEBUG=true`, 500s\ninclude the exception class, location and stack trace; in production they show a generic page.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichaelyousrie%2Fbatframe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmichaelyousrie%2Fbatframe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichaelyousrie%2Fbatframe/lists"}