{"id":15030582,"url":"https://github.com/nimbly/limber","last_synced_at":"2025-04-09T20:41:03.289Z","repository":{"id":57027272,"uuid":"123853350","full_name":"nimbly/Limber","owner":"nimbly","description":"A super minimal HTTP framework that doesn't get in your way.","archived":false,"fork":false,"pushed_at":"2025-02-27T18:00:08.000Z","size":331,"stargazers_count":17,"open_issues_count":0,"forks_count":1,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-09T11:18:28.465Z","etag":null,"topics":["framework","microframework","middleware","php","router"],"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/nimbly.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}},"created_at":"2018-03-05T02:27:39.000Z","updated_at":"2025-03-30T14:34:24.000Z","dependencies_parsed_at":"2022-08-23T16:20:35.731Z","dependency_job_id":"56f1aadd-8653-4472-8ed9-49070a492a1d","html_url":"https://github.com/nimbly/Limber","commit_stats":{"total_commits":143,"total_committers":2,"mean_commits":71.5,"dds":0.06293706293706292,"last_synced_commit":"8c2b874e2c0f0a823c9daade28f2b1c72308ddf1"},"previous_names":[],"tags_count":32,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nimbly%2FLimber","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nimbly%2FLimber/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nimbly%2FLimber/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nimbly%2FLimber/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nimbly","download_url":"https://codeload.github.com/nimbly/Limber/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248109689,"owners_count":21049359,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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","microframework","middleware","php","router"],"created_at":"2024-09-24T20:13:45.712Z","updated_at":"2025-04-09T20:41:03.279Z","avatar_url":"https://github.com/nimbly.png","language":"PHP","readme":"# Limber\n\n[![Latest Stable Version](https://img.shields.io/packagist/v/nimbly/limber.svg?style=flat-square)](https://packagist.org/packages/nimbly/Limber)\n[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/nimbly/limber/coverage.yml?style=flat-square)](https://github.com/nimbly/Limber/actions/workflows/coverage.yml)\n[![Codecov branch](https://img.shields.io/codecov/c/github/nimbly/limber/master?style=flat-square)](https://app.codecov.io/github/nimbly/Limber)\n[![License](https://img.shields.io/github/license/nimbly/Limber.svg?style=flat-square)](https://packagist.org/packages/nimbly/Limber)\n\nA super minimal PSR-7, 15, and 11 compliant HTTP framework that doesn't get in your way.\n\nLimber is intended for advanced users who are comfortable setting up their own framework and pulling in packages best suited for their particular use case.\n\n## Limber includes\n* A router\n* PSR-7 HTTP message compliant\n* PSR-11 container compliant\n* PSR-15 middleware compliant\n* A thin `Application` layer to tie everything together\n\n## Requirements\n\n* PHP 8.2+\n* PSR-7 HTTP Message library\n\n## Installation\n\n```bash\ncomposer require nimbly/limber\n```\n\n## Quick start\n\n### Install PSR-7 library\n\nLimber does not ship with a PSR-7 implementation which is required to receive HTTP requests and send back responses. Let's pull one into our project.\n\n* [slim/psr7](https://github.com/slimphp/Slim-Psr7)\n* [laminas/laminas-diactoros](https://github.com/laminas/laminas-diactoros)\n* [guzzlehttp/psr7](https://github.com/guzzle/psr7)\n* [nimbly/Capsule](https://github.com/nimbly/Capsule)\n\n```bash\ncomposer require nimbly/capsule\n```\n\n### Sample application\n\n1. Create your entrypoint (or front controller), for example `index.php`, and start by creating a new `Router` instance and attaching your routes to it.\n\n2. Once your routes have been defined, you can create the `Application` instance and pass the router in to it.\n\n3. You can then `dispatch` requests through the application and receive a response back.\n\n4. And finally, you can `send` a response back to the calling client.\n\n```php\n\u003c?php\n\nrequire __DIR__ . \"/vendor/autoload.php\";\n\n// Create a Router instance and define a route.\n$router = new Nimbly\\Limber\\Router\\Router;\n$router-\u003eget(\"/\", fn() =\u003e new Nimbly\\Capsule\\Response(200, \"Hello World!\"));\n\n// Create Application instance with router.\n$application = new Nimbly\\Limber\\Application($router);\n\n// Dispatch a PSR-7 ServerRequestInterface instance and get back a PSR-7 ResponseInterface instance\n$response = $application-\u003edispatch(\n\tNimbly\\Capsule\\Factory\\ServerRequestFactory::createFromGlobals()\n);\n\n// Send the ResponseInterface instance\n$application-\u003esend($response);\n```\n\n## Advanced configuration\n\n### A note on autowiring support\n\nLimber will invoke your route handlers using reflection based autowiring. The `ServerRequestInterface` instance, URI path parameters defined in the route, and request attributes will be automatically resolved for you, without the need of a PSR-11 container.\n\nHowever, any domain specific services and classes that are required in your handlers, should be defined in a PSR-11 container instance.\n\n### Adding PSR-11 container support\n\nLimber is able to autowire your request handlers and middleware with the aid of a PSR-11 container instance. However, Limber *does not ship* with a PSR-11 Container implementation, so you will need to bring your own if you require one. Here are some options:\n\n* [PHP-DI](https://php-di.org/)\n* [nimbly/Carton](https://github.com/nimbly/Carton)\n\nLet's add container support to our application.\n\n```bash\ncomposer require nimbly/carton\n```\n\nAnd update our entry point by passing the container instance into the `Application` constructor.\n\n```php\n\u003c?php\n\n// Create PSR-11 container instance and configure as needed.\n$container = new Container;\n$container-\u003eset(\n\tFoo:class,\n\tfn(): Foo =\u003e new Foo(\\getenv(\"FOO_NAME\"))\n);\n\n// Create Application instance with router and container.\n$application = new Nimbly\\Limber\\Application(\n\trouter: $router,\n\tcontainer: $container\n);\n```\n\n### Middleware\n\nLimber supports PSR-15 middleware. All middleware must implement `Psr\\Http\\Server\\MiddlewareInterface`.\n\nYou can pass middleware as one or more of the following types:\n\n* An instance of `MiddlewareInterface`\n* A `class-string` that implements `MiddlewareInterface`\n* A `class-string` that implements `MiddlewareInterface` as an index and an array of key=\u003evalue pairs as parameters to be used in dependency injection when autowiring.\n\nAny `class-string` types will be autowired using the `Container` instance (if any) for dependency injection.\n\nIf auto wiring fails, a `DependencyResolutionException` exception will be thrown.\n\n```php\nclass SampleMiddleware implements MiddlewareInterface\n{\n\tpublic function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface\n\t{\n\t\t// Add a custom header to the request before sending to route handler\n\t\t$request = $request-\u003ewithAddedHeader(\"X-Foo\", \"Bar\");\n\n\t\t$response = $handler-\u003ehandle($request);\n\n\t\t// Add a custom header to the response before sending back to client\n\t\treturn $response-\u003ewithAddedHeader(\"X-Custom-Header\", \"Foo\");\n\t}\n}\n```\n\nNow let's add this global middleware layer to the Limber application instance.\n\n```php\n$application = new Nimbly\\Limber\\Application(\n\trouter: $router,\n\tcontainer: $container,\n\tmiddleware: [\n\t\tApp\\Http\\Middleware\\SampleMiddleware::class\n\t]\n);\n```\n\n### HTTP Exceptions\n\nLimber has most major HTTP error response codes (4xx and 5xx response codes) mapped to exceptions that extend the `Nimbly\\Limber\\Exceptions\\HttpException` abstract. For example `Nimbly\\Limber\\Exceptions\\NotFoundHttpException` (404 Not Found). These exceptions have methods to get the HTTP response status code and as well as any response headers that may be required for that response code.\n\nCoupled with a default exception handler (see next section), you can create a single source for crafting HTTP error responses.\n\n### Exception handling\n\nYou can set a custom default exception handler that will process any exception thrown *within* the middleware chain.\n\nThe exception handler must implement `Nimbly\\Limber\\ExceptionHandlerInterface`.\n\n**NOTE** Exceptions thrown *outside* of the middleware chain (e.g. during bootstrap process) will continue to bubble up unless caught elsewhere.\n\n```php\nnamespace App\\Http;\n\nuse Nimbly\\Limber\\ExceptionHandlerInterface;\nuse Nimbly\\Limber\\Exceptions\\HttpException;\n\nclass ExceptionHandler implements ExceptionHandlerInterface\n{\n\tpublic function handle(Throwable $exception, ServerRequestInterface $request): ResponseInterface\n\t{\n\t\t$status_code = $exception instanceof HttpException ? $exception-\u003egetHttpStatus() : 500;\n\t\t$response_headers = $exception instanceof HttpException ? $exception-\u003egetHeaders() : [];\n\n\t\treturn new Response(\n\t\t\t$status_code,\n\t\t\t\\json_encode([\n\t\t\t\t\"error\" =\u003e [\n\t\t\t\t\t\"code\" =\u003e $exception-\u003egetCode(),\n\t\t\t\t\t\"message\" =\u003e $exception-\u003egetMessage()\n\t\t\t\t]\n\t\t\t]),\n\t\t\t\\array_merge(\n\t\t\t\t$response_headers,\n\t\t\t\t[\n\t\t\t\t\t\"Content-Type\" =\u003e \"application/json\"\n\t\t\t\t]\n\t\t\t)\n\t\t);\n\t}\n}\n```\n\nNow let's add the exception handler to the Limber application instance.\n\n```php\n$application = new Nimbly\\Limber\\Application(\n\trouter: $router,\n\tcontainer: $container,\n\tmiddleware: [\n\t\tApp\\Http\\Middleware\\FooMiddlware::class\n\t],\n\texceptionHandler: new App\\Http\\ExceptionHandler\n);\n```\n\n## Router\n\nThe `Router` builds and collects `Route` instances and provides helper methods to group `Routes` together sharing a common configuration (path prefix, namespace, middleware, etc).\n\n### Defining routes\n\nCreate a `Router` instance and begin defining your routes. There are convenience methods for all major HTTP verbs (get, post, put, patch, and delete).\n\n```php\n$router = new Nimbly\\Limber\\Router\\Router;\n$router-\u003eget(\"/fruits\", \"FruitsHandler@all\");\n$router-\u003epost(\"/fruits\", \"FruitsHandler@create\");\n$router-\u003epatch(\"/fruits/{id}\", \"FruitsHandler@update\");\n$router-\u003edelete(\"/fruits/{id}\", \"FruitsHandler@delete\");\n```\n\nA route can respond to any number of HTTP methods by using the `add` method and passing an array of methods as strings.\n\n```php\n$router-\u003eadd([\"get\", \"post\"], \"/fruits\", \"FruitsHandler@create\");\n```\n\n### HEAD requests\n\nBy default, Limber will add a `HEAD` method to each `GET` route.\n\n### Route paths\n\nPaths can be static or contain named parameters. Named parameters will be injected into your\nroute handler if the handler also contains a parameter of the same name.\n\n```php\n$router-\u003eget(\"/books/{isbn}\", \"BooksHandler@findByIsbn\");\n```\n\nIn the following handler, both the `$request` and `$isbn` parameters will be injected automatically.\n\n```php\nclass BooksHandler\n{\n\tpublic function getByIsbn(ServerRequestInterface $request, string $isbn): ResponseInterface\n\t{\n\t\t$book = BookModel::findByIsbn($isbn);\n\n\t\tif( empty($book) ){\n\t\t\tthrow new NotFoundHttpException(\"ISBN not found.\");\n\t\t}\n\n\t\treturn new JsonResponse(\n\t\t\t200,\n\t\t\t$book-\u003etoArray()\n\t\t);\n\t}\n}\n```\n\n### Route path patterns\n\nYour named parameters can also enforce a specific regular expression pattern when being matched - just add the pattern after the placeholder name with a colon.\n\nLimber has several predefined path patterns you can use:\n\n* `alpha` Alphabetic characters only (A-Z and a-z), of any length\n* `int` Integer number of any length\n* `alphanumeric` Any combination of number or alphabetic character\n* `uuid` A Universally Unique Identifier or sometimes known as a GUID.\n* `hex` A hexidecimal value, of any length\n\n```php\n// Get a book by its ID and match the ID to a UUID.\n$router-\u003eget(\"/books/{id:uuid}\", \"BooksHandler@get\");\n```\n\nYou can define your own patterns to match using the `Router::setPattern()` static method.\n\n```php\nRouter::setPattern(\"isbn\", \"\\d{9}[\\d|X]\");\n\n$router = new Router;\n$router-\u003eget(\"/books/{id:isbn}\", \"BooksHandler@getByIsbn\");\n```\n\n### Route handlers\n\nRoute handlers may either be a `callable` or a string in the format **Fully\\Qualified\\Namespace\\ClassName@Method** (for example `App\\Handlers\\v1\\BooksHandler@create`).\n\nRoute handlers *must* return a `ResponseInterface` instance.\n\nLimber uses reflection based autowiring to automatically resolve your route handlers including constructor and function/method parameters. The `ServerRequestInterface` instance, path parameters, and any attributes attached to the  `ServerRequestInterface` instance will be resolved and injected for you. This applies for both closure based handlers as well as **Class@Method** based handlers.\n\nYou may also optionally supply a PSR-11 compliant `ContainerInterface` instance to aid in route handler parameter resolution. By doing this, you can easily have your application specific dependencies resolved and injected into your handlers by Limber. See **PSR-11 Container support** section for more information.\n\n```php\n// Closure based handler\n$router-\u003eget(\n\t\"/books/{id:isbn}\",\n\tfunction(ServerRequestInterface $request, string $id): ResponseInterface {\n\t\t$book = Books::find($id);\n\n\t\tif( empty($book) ){\n\t\t\tthrow new NotFoundHttpException(\"Book not found.\");\n\t\t}\n\n\t\treturn new Response(200, \\json_encode($book));\n\t}\n);\n\n// String references to ClassName@Method\n$router-\u003epatch(\"/books/{id:isbn}\", \"App\\Handlers\\BooksHandler@update\");\n\n// If a ContainerInterface instance was assigned to the application and contains an InventoryService instance, it will be injected into this handler.\n$router-\u003epost(\n\t\"/books\",\n\tfunction(ServerRequestInterface $request, InventoryService $inventoryService): ResponseInterface {\n\t\t$book = Book::make($request-\u003egetParsedBody());\n\n\t\t$inventoryService-\u003eadd($book);\n\n\t\treturn new Response(201, \\json_encode($book));\n\t}\n);\n```\n\n### Route configuration\n\nYou can configure individual routes to respond to a specific scheme, a specific hostname, process additional middleware, or pass along attributes to the `ServerRequestInterface` instance.\n\n#### Scheme\n\n```php\n$router-\u003epost(\n\tpath: \"books\",\n\thandler: \"\\App\\Http\\Handlers\\BooksHandler@create\",\n\tscheme: \"https\"\n);\n```\n\n### Route specific middleware\n\n```php\n$router-\u003epost(\n\tpath: \"books\",\n\thandler: \"\\App\\Http\\Handlers\\BooksHandler@create\",\n\tmiddleware: [new FooMiddleware]\n);\n```\n\n#### Hostname\n\n```php\n$router-\u003epost(\n\tpath: \"books\",\n\thandler: \"\\App\\Http\\Handlers\\BooksHandler@create\",\n\thostnames: [\"example.org\"]\n);\n```\n\n#### Attributes\n\n```php\n$router-\u003epost(\n\tpath: \"books\",\n\thandler: \"\\App\\Http\\Handlers\\BooksHandler@create\",\n\tattributes: [\n\t\t\"Attribute1\" =\u003e \"Value1\"\n\t]\n);\n```\n\n### Route groups\n\nYou can group routes together using the `group` method and all routes contained will inherit the configuration you have defined.\n\n* `scheme` (optional) *string* The HTTP scheme (`http` or `https`) to match against. A `null` value will match against any value.\n* `middleware` (optional) *array\u0026lt;string\u0026gt;* or *array\u0026lt;MiddlewareInterface\u0026gt;* or *array\u0026lt;callable\u0026gt;* An array of all middleware classes (fully qualified namespace) or actual instances of middleware.\n* `prefix` (optional) *string* A string prepended to all URIs when matching the request.\n* `namespace` (optional) *string* A string prepended to all string based handlers before instantiating a new class.\n* `hostnames` (optional) *array\u0026lt;string\u0026gt;* An array of hostnames to be matched against.\n* `attributes` (optional) *array\u0026lt;string,mixed\u0026gt;* An array of key=\u003evalue pairs representing attributes that will be attached to the `ServerRequestInterface` instance if the route matches.\n* `routes` (required) *callable* A callable that accepts the `Router` instance where you can add additional routes within the group.\n\n```php\n$router-\u003egroup(\n\thostnames: [\"sub.domain.com\"],\n\tmiddleware: [\n\t\tFooMiddleware::class,\n\t\tBarMiddleware::class\n\t],\n\tnamespace: \"\\App\\Sub.Domain\\Handlers\",\n\tprefix: \"v1\",\n\troutes: function(Router $router): void {\n\t\t$router-\u003eget(\"books/{isbn}\", \"BooksHandler@getByIsbn\");\n\t\t$router-\u003epost(\"books\", \"BooksHandler@create\");\n\t}\n);\n```\n\nGroups can be nested and will inherit their parent group's settings unless the setting is overridden. Middleware settings however are *merged* with their parent's settings.\n\n```php\n$router-\u003egroup(\n\thostnames: [\"sub.domain.com\"],\n\tmiddleware: [\n\t\tFooMiddleware::class,\n\t\tBarMiddleware::class\n\t],\n\tnamespace: \"\\App\\Sub.Domain\\Handlers\",\n\tprefix: \"v1\",\n\troutes: function(Router $router): void {\n\n\t\t$router-\u003eget(\"books/{isbn}\", \"BooksHandler@getByIsbn\");\n\t\t$router-\u003epost(\"books\", \"BooksHandler@create\");\n\n\t\t// This group will inherit all group settings from the parent group, override\n\t\t// the namespace property, and will merge in an additional middleware (AdminMiddleware).\n\t\t$router-\u003egroup(\n\t\t\tnamespace: \"\\App\\Sub.Domain\\Handlers\\Admin\",\n\t\t\tmiddleware: [\n\t\t\t\tAdminMiddleware::class\n\t\t\t],\n\t\t\troutes: function(Router $router): void {\n\t\t\t\t$route-\u003edelete(\"books/{isbn}\", \"BooksHandler@deleteBook\");\n\t\t\t}\n\t\t);\n\t}\n);\n```\n\n## Using with React/Http\n\nBecause Limber is PSR-7 compliant, it works very well with [react/http](https://github.com/reactphp/http) to create a standalone HTTP service without the need for an additional HTTP server (nginx, Apache, etc) - great for containerizing your service with minimal dependencies.\n\n### Install React/Http\n\n```bash\ncomposer install react/http\n```\n\n### Create entry point\n\nCreate a file called `main.php` (or whatever you want) to be the container's command/entry point.\n\n```php\n\u003c?php\n\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Nimbly\\Capsule\\Response;\n\n// Create the router and some routes.\n$router = new Nimbly\\Limber\\Router;\n$router-\u003eget(\"/\", function(ServerRequestInterface $request): ResponseInterface {\n\treturn new Response(\n\t\t\"Hello world!\"\n\t);\n});\n\n// Create the Limber Application instance.\n$application = new Nimbly\\Limber\\Application($router);\n\n// Create the HTTP server to handle incoming HTTP requests with your Limber Application instance.\n$httpServer = new React\\Http\\HttpServer(\n\tfunction(ServerRequestInterface $request) use ($application): ResponseInterface {\n\t\treturn $application-\u003edispatch($request);\n\t}\n);\n\n// Listen on port 8000.\n$httpServer-\u003elisten(\n\tnew React\\Socket\\SocketServer(\"0.0.0.0:8000\");\n);\n```\n\n### Adding process signal handlers\n\nReact/Http supports tapping into processing signals, commonly used by container orchestration systems to shutdown processes. You can use these interrupts to signal to React/Http to stop the event loop. This functionality requires  the PHP `pcntl` module be installed. (See next section.)\n\n```php\n$loop = React\\EventLoop\\Loop::get();\n\n$loop-\u003eaddSignal(\n\tSIGINT,\n\tfunction(int $signal) use ($loop): void {\n\t\t\\error_log(\"SIGINT received: Shutting down gracefully.\");\n\t\t$loop-\u003estop();\n\t}\n);\n```\n\n### Create Dockerfile\n\nCreate a `Dockerfile` in the root of your application.\n\nWe'll extend from the official PHP 8.2 docker image and add some useful tools like `composer`, a better event loop library from PECL, and install support for process control (`pcntl`). Process control will allow your service to shutdown gracefully when a `SIGINT` or `SIGHUP` signal is received.\n\nObviously, edit this file to match your specific needs.\n\n```docker\nFROM php:8.2-cli\n\nRUN apt-get update \u0026\u0026 apt-get upgrade --yes\nRUN curl --silent --show-error https://getcomposer.org/installer | php \u0026\u0026 \\\n\tmv composer.phar /usr/bin/composer\nRUN mkdir -p /usr/src/php/ext \u0026\u0026 curl --silent https://pecl.php.net/get/ev-1.1.5.tgz | tar xvzf - -C /usr/src/php/ext\n\n# Add other PHP modules\nRUN docker-php-ext-install pcntl ev-1.1.5\n\nWORKDIR /opt/service\nADD . .\nRUN composer install --no-dev\nCMD [ \"php\", \"main.php\" ]\n```\n\n### Build docker image\n\n```bash\ndocker image build -t my-service:latest .\n```\n\n### Run as container\n\n```bash\ndocker container run -p 8000:8000 --env-file=.env my-service:latest\n```","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnimbly%2Flimber","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnimbly%2Flimber","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnimbly%2Flimber/lists"}