{"id":16619053,"url":"https://github.com/jasny/controller","last_synced_at":"2025-09-12T13:43:23.626Z","repository":{"id":38397254,"uuid":"71069108","full_name":"jasny/controller","owner":"jasny","description":"A controller for PSR-7 requests. Supports Slim and other micro-frameworks","archived":false,"fork":false,"pushed_at":"2023-06-12T21:12:39.000Z","size":266,"stargazers_count":1,"open_issues_count":4,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-06T21:55:27.230Z","etag":null,"topics":["php","psr-7","slim-framework"],"latest_commit_sha":null,"homepage":"http://www.jasny.net/controller/","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/jasny.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}},"created_at":"2016-10-16T18:30:18.000Z","updated_at":"2024-04-19T12:55:11.000Z","dependencies_parsed_at":"2022-07-12T17:28:33.668Z","dependency_job_id":null,"html_url":"https://github.com/jasny/controller","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasny%2Fcontroller","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasny%2Fcontroller/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasny%2Fcontroller/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasny%2Fcontroller/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jasny","download_url":"https://codeload.github.com/jasny/controller/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238882521,"owners_count":19546533,"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":["php","psr-7","slim-framework"],"created_at":"2024-10-12T02:22:37.073Z","updated_at":"2025-02-14T17:32:12.046Z","avatar_url":"https://github.com/jasny.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"Jasny Controller\n===\n\n[![PHP](https://github.com/jasny/controller/actions/workflows/php.yml/badge.svg)](https://github.com/jasny/controller/actions/workflows/php.yml)\n[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/jasny/controller/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/jasny/controller/?branch=master)\n[![Code Coverage](https://scrutinizer-ci.com/g/jasny/controller/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/jasny/controller/?branch=master)\n[![Packagist Stable Version](https://img.shields.io/packagist/v/jasny/controller.svg)](https://packagist.org/packages/jasny/controller)\n[![Packagist License](https://img.shields.io/packagist/l/jasny/controller.svg)](https://packagist.org/packages/jasny/controller)\n\nPSR-7 controller for [Slim Framework](https://www.slimframework.com/) and other micro-frameworks.\n\n\u003e The controller is responsible handling the HTTP request, manipulate the model and initiate the view.\n\nThe code in the controller read as a high level description of each action. The controller should not contain\nimplementation details. This belongs in the model, view or in services and libraries.\n\nInstallation\n---\n\nInstall using composer\n\n    composer require jasny\\controller\n\nSetup\n---\n\n`Jasny\\Controller` can be used as a base class for each of your controllers. It lets you interact with the\n[PSR-7](http://www.php-fig.org/psr/psr-7/) server request and response in a friendly matter.\n\n```php\nclass MyController extends Jasny\\Controller\\Controller\n{\n    public function hello(string $name, #[QueryParam] string $others = ''): void\n    {\n        $this-\u003eoutput(\"Hello $name\" . ($others ? \" and $others\" : \"\"), 'text');\n    }\n}\n```\n\n\u003e Visiting `https://example.com/hello/Arnold\u0026others=friends` would output `Hello Arnold and friends`.\n\nActions are defined as public methods of the controller.\n\nA controller is a callable object by implementing the [`__invoke`][] method. The invoke method takes a PSR-7\nserver request and response object and will return a modified response object. This all is abstracted away when you\nwrite your controller.\n\nA router typically handles the request and chooses the correct controller object to call. The router is also responsible\nfor extracting parameters from the url path and possibly choosing a method to call within the controller.\n\n[`__invoke`]: http://php.net/manual/en/language.oop5.magic.php#object.invoke\n\n### Slim framework\n\n[Slim](https://www.slimframework.com/) is a PHP micro-framework that works with PSR-7. To use this library with slim,\nuse the provided middleware.\n\n```php\nuse Jasny\\Controller\\Middleware\\Slim as ControllerMiddleware;\nuse Slim\\Factory\\AppFactory;\n\n$app = AppFactory::create();\n\n$app-\u003eadd(new ControllerMiddleware());\n$app-\u003eaddRoutingMiddleware();\n\n$app-\u003eget('/hello/{name}', ['MyController', 'hello']);\n```\n\nOptionally, the middleware can convert error responses from the controller to Slim HTTP Errors by passing `true` to the\nmiddleware constructor.\n\n```php\nuse Jasny\\Controller\\Middleware\\Slim as ControllerMiddleware;\nuse Slim\\Factory\\AppFactory;\n\n$app = AppFactory::create();\n\n$app-\u003eadd(new ControllerMiddleware(true));\n$app-\u003eaddRoutingMiddleware();\n$app-\u003eaddErrorMiddleware(true, true, true);\n```\n\n### Relay + SwitchRoute\n\n[SwitchRoute](https://github.com/jasny/switch-route), a super-fast router based on generating code. The router needs a\nPSR-15 request handler to work with PRS-7 server requests, like [Relay](https://relayphp.com/).\n\nBy default, the route action is converted to the method that will be called by the PSR-15 handler. For this library,\n`__invoke` should be called instead. The invoke method will take care of calling the right method within the controller.\n\n```php\n$stud = fn($str) =\u003e strtr(ucwords($str, '-'), ['-' =\u003e '']);\n\n$invoker = new Invoker(fn (?string $controller, ?string $action) =\u003e [\n    $controller !== null ? $stud($controller) . 'Controller' : $stud($action) . 'Action',\n    '__invoke'\n]);\n```\n\n**[See SwitchRoute for more information](https://github.com/jasny/switch-route#readme)**\n\nOutput\n---\n\nWhen using PSR-7, you shouldn't use `echo`, because it makes it harder to write tests. Instead, use the `output` method\nof the controller, which writes to the response body stream object.\n\n```php\n$this-\u003eoutput('Hello world');\n```\n\nA second argument may be passed, which sets the `Content-Type` header. You can pass a mime type like 'text/html'.\nAlternatively you can  use a common file extension like 'txt'. The controller uses the\n[ralouphie/mimey](https://github.com/ralouphie/mimey) library to get the mime type.\n\n```php\nclass MyController extends Jasny\\Controller\\Controller\n{\n    /**\n     * Output a random number between 0 and 100 as HTML\n     */\n    public function random()\n    {\n        $number = rand(0, 100);\n        $this-\u003eoutput(\"\u003ch1\u003e$number\u003c/h1\u003e\", 'html');\n    }\n}\n```\n\n### JSON\n\nThe `json` method can be used to serialize and output data as JSON.\n\n```php\nclass MyController extends Jasny\\Controller\\Controller\n{\n    /**\n     * Output 5 random numbers between 0 and 100 as JSON\n     */\n    public function random()\n    {\n        $numbers = array_map(fn() =\u003e rand(0, 100), range(1, 5));\n        $this-\u003ejson($numbers);\n    }\n}\n```\n\n### Response status\n\nTo set the response status you can use the `status()` method. This method can take the response status as integer or\nas string specifying both the status code and phrase.\n\n```php\nclass MyController extends Jasny\\Controller\\Controller\n{\n    public function process(string $size)\n    {\n        if (!in_array($size, ['XS', 'S', 'M', 'L', 'XL'])) {\n            return $this\n                -\u003estatus(\"400 Bad Request\")\n                -\u003eoutput(\"Invalid size: $size\");\n        }\n\n        // Create something ...\n        \n        return $this\n            -\u003estatus(201)\n            -\u003eheader(\"Location: http://www.example.com/foo/something\")\n            -\u003ejson($something);\n    }\n}\n```\n\nAlternatively and preferably you can use helper method to set a specific response status. Some method can optionally\ntake arguments that make sense for that status.\n\n```php\nclass MyController extends Jasny\\Controller\\Controller\n{\n    public function process(string $size)\n    {\n        if (!in_array($size, ['XS', 'S', 'M', 'L', 'XL'])) {\n            return $this-\u003ebadRequest()-\u003eoutput(\"Invalid size: $size\");\n        }\n\n        // Create something ...\n        \n        return $this\n            -\u003ecreated(\"http://www.example.com/foo/something\")\n            -\u003ejson($something);\n    }\n}\n```\n\nThe following methods for setting the output status are available\n\n| status code             | method                                                         |                                                     |\n|-------------------------|----------------------------------------------------------------|-----------------------------------------------------|\n| [200][]                 | `ok()`                                                         |                                                     |\n| [201][]                 | `created(?string $location = null)`                            | Optionally set the `Location` header                |\n| [202][]                 | `accepted()`                                                   |                                                     |\n| [204][]/[205][]         | `noContent(int $code = 204)`                                   |                                                     |\n| [206][]                 | `partialContent(int $rangeFrom, int $rangeTo, int $totalSize)` | Set the `Content-Range` and `Content-Length` header |\n| [30x][303]              | `redirect(string $url, int $code = 303)`                       | Url for the `Location` header                       |\n| [303][]                 | `back()`                                                       | Redirect to the referer                             |\n| [304][]                 | `notModified()`                                                |                                                     |\n| [40x][400]              | `badRequest(int $code = 400)`                                  |                                                     |\n| [401][]                 | `unauthorized()`                                               |                                                     |\n| [402][]                 | `paymentRequired()`                                            |                                                     |\n| [403][]                 | `forbidden()`                                                  |                                                     |\n| [404][]/[405][]/[410][] | `notFound(int $code = 404)`                                    |                                                     |\n| [406][]                 | `notAcceptable()`                                              |                                                     |\n| [409][]                 | `conflict()`                                                   |                                                     |\n| [429][]                 | `tooManyRequests()`                                            |                                                     |\n| [5xx][500]              | `error(int $code = 500)`                                       |                                                     |\n\n- Some methods take a `$message` argument. This will set the output.\n- If a method takes a `$code` argument, you can specify the status code.\n- The `back()` method will redirect to the referer, but only if the referer is from the same domain as the current url.\n\n[200]: https://httpstatuses.com/200\n[201]: https://httpstatuses.com/201\n[202]: https://httpstatuses.com/202\n[203]: https://httpstatuses.com/203\n[204]: https://httpstatuses.com/204\n[205]: https://httpstatuses.com/205\n[206]: https://httpstatuses.com/206\n[303]: https://httpstatuses.com/303\n[304]: https://httpstatuses.com/304\n[400]: https://httpstatuses.com/400\n[401]: https://httpstatuses.com/401\n[402]: https://httpstatuses.com/402\n[403]: https://httpstatuses.com/403\n[404]: https://httpstatuses.com/404\n[405]: https://httpstatuses.com/405\n[406]: https://httpstatuses.com/406\n[410]: https://httpstatuses.com/410\n[409]: https://httpstatuses.com/409\n[429]: https://httpstatuses.com/429\n[500]: https://httpstatuses.com/500\n\nSometimes it's useful to check the status code that has been set for the response. This can be done with the\n`getStatusCode()` method. In addition, there are methods to check the type of status.\n\n| status code | method              |\n|-------------|---------------------|\n| 1xx         | `isInformational()` |\n| 2xx         | `isSuccessful()`    |\n| 3xx         | `isRedirection()`   |\n| 4xx         | `isClientError()`   |\n| 5xx         | `isServerError()`   |\n| 4xx or 5xx  | `isError()`         |\n\n\n### Response headers\n\nYou can set the response header using the `setResponseHeader()` method.\n\n```php\nclass MyController extends Jasny\\Controller\\Controller\n{\n    public function process()\n    {\n        $this-\u003eheader(\"Content-Language\", \"nl\");\n        // ...\n    }\n}\n```\n\nBy default, response headers are overwritten. In some cases you want to have duplicate headers. In that case set the\nthird argument to `true`, eg `header($header, $value, true)`.\n\n```php\n$this-\u003eheader(\"Cache-Control\", \"no-cache\"); // overwrite header\n$this-\u003eheader(\"Cache-Control\", \"no-store\", true); // add header\n```\n\nInput\n---\n\nWith PSR-7, you shouldn't use super globals `$_GET`, `$_POST`, `$_COOKIE`, and `$_SERVER`. Instead, these values are\navailable through the server request object. This is done using [PHP attributes][].\n\n| Attribute       | Arguments  |                                           |\n|-----------------|------------|-------------------------------------------|\n| `PathParam`     | name, type | Path parameter obtained from router       |\n| `QueryParam`    | name, type | Query parameter                           |\n| `Query`         |            | All query parameters                      |\n| `BodyParam`     | name, type | Body parameter                            |\n| `Body`          |            | All body parameters or raw body as string |\n| `Cookie`        | name, type | Cookie parameter                          |\n| `Cookies`       |            | All cookies as key/value                  |\n| `UploadedFile`  | name       | PSR-7 uploaded file(s)                    |\n| `UploadedFiles` |            | Associative array of all uploaded files   |\n| `Header`        | name, type | Request header (as string)                |\n| `Headers`       |            | All headers as associative array          |     \n| `Attr`          | name, type | PSR-7 request attribute set by middleware |\n\n[PHP attributes]: https://www.php.net/manual/en/language.attributes.overview.php\n\nThe controller will map each argument of a method to a parameter. By default, arguments are mapped to path parameters.\n\n### Parameters\n\n#### Path parameters\n\nA router may extract parameters from the request URL. In the following example, the url path `/hello/world`,\nthe path parameter `name` will have the value `\"world\"`.\n\n```php\n$app-\u003eget('/hello/{name}', ['MyController', 'hello']);\n```\n\nThe `name` parameter will be passed as argument to the `hello` method.\n\n```php\nclass MyController extends Jasny\\Controller\\Controller\n{\n    public function hello(string $name)\n    {\n        $this-\u003eoutput(\"Hello $name\");\n    }\n}\n```\n\n#### Single request parameter\n\nThe controller will pass PSR-7 request parameters as arguments. This is specified by an attribute\n\n* `QueryParam`\n* `BodyParam`\n* `Cookie`\n* `UploadedFile`\n* `Header`\n\nIf the argument name is used as parameter name\n\n* for `QueryParam`, underscores are replaced with dashes. Eg: `$foo_bar` will translate to query param `foo-bar`.\n* for `Header`, words are capitalized and underscores become dashes. Eg: `$foo_bar` translates to header `Foo-Bar`.\n\n#### All request parameters\n\nTo get all request parameters of a specific type, the following attributes are available.\n\n* `Query`\n* `Body`\n* `Cookies`\n* `UploadedFiles`\n* `Headers`\n\nFor the `Body` attribute, the type of the argument should either be an array or a string. If an array is passed the\nargument will be the parsed body. In case of a string it will be the raw body.\n\n#### PSR-7 request attribute\n\nMiddleware can set attributes of the PSR-7 request. These request attributes are available as arguments by using the\n`Attr` attribute.\n\n### Parameter name\n\nFor single parameters, the name of the argument will be used as parameter name. Alternatively, it's possible to specify\na name when defining the attribute.\n\n```php\nuse Jasny\\Controller\\Controller;\nuse Jasny\\Controller\\Parameter\\PathParam;\nuse Jasny\\Controller\\Parameter\\QueryParam;\n\nclass MyController extends Controller\n{\n    public function hello(#[PathParam] string $name, #[QueryParam('and')] string $other = '')\n    {\n        $this-\u003eoutput(\"Hello $name\" . ($other ? \" and $other\" : \"\"));\n    }\n}\n```\n\n_Note: `#[PathParam]` could be omitted, since it's the default behaviour._\n\n### Parameter type\n\nIt's possible to specify a type as second argument when defining the attribute. By default, the type is determined on\nthe type of the argument.\n\n```php\nuse Jasny\\Controller\\Controller;\nuse Jasny\\Controller\\Parameter\\BodyParam;\n\nclass MyController extends Controller\n{\n    public function send(#[BodyParam(type: 'email')] string $emailAddress)\n    {\n        // ...\n    }\n}\n```\n\nParameter attributes use the [`filter_var`](https://www.php.net/filter_var) function to sanitize input. The following\nfilters are defined\n\n| type  | filter                  |\n|-------|-------------------------|\n| bool  | `FILTER_VALIDATE_BOOL`  |\n| int   | `FILTER_VALIDATE_INT`   |\n| float | `FILTER_VALIDATE_FLOAT` |\n| email | `FILTER_VALIDATE_EMAIL` |\n| url   | `FILTER_VALIDATE_URL`   |\n\nFor other types (like `string`), no filter is applied.\n\n```php\nuse Jasny\\Controller\\Controller;\nuse Jasny\\Controller\\Parameter\\PostParam;\n\nclass MyController extends Controller\n{\n    public function message(#[PostParam(type: 'email')] array $email)\n    {\n        // ...\n    }\n}\n```\n\nTo add custom types, add filters to `SingleParameter::$types`\n\n```php\nuse Jasny\\Controller\\Parameter\\SingleParameter;\n\nSingleParameter::$types['slug'] = [FILTER_VALIDATE_REGEXP, '/^[a-z\\-]+$/'];\n```\n\nContent negotiation\n---\n\nContent negotiation allows the controller to give different output based on `Accept` request headers. It can be used to\nselect the content type (switch between JSON and XML), the content language, encoding, and charset.\n\n| Method                   | Request header    | Response header    |    \n|--------------------------|-------------------|--------------------|\n| `negotiateContentType()` | `Accept`          | `Content-Type`     |\n| `negotiateLanguage()`    | `Accept-Language` | `Content-Language` | \n| `negotiateEncoding()`    | `Accept-Encoding` | `Content-Encoding` | \n| `negotiateCharset()`     | `Accept-Charset`  |                    |\n\n_`negotiateCharset()` will modify the `Content-Type` header if it's already set. Otherwise, it will just\nreturn the selected charset._\n\nThe negotiate method takes a list or priorities as argument. It sets the response header and returns the selected\noption.\n\n```php\nclass MyController extends Jasny\\Controller\\Controller\n{\n    public function hello()\n    {\n        $language = $this-\u003enegotiateLanguage(['en', 'de', 'fr', 'nl;q=0.6']);\n        \n        switch ($language) {\n            case 'en':\n                return $this-\u003eoutput('Good morning');\n            case 'de':\n                return $this-\u003eoutput('Guten Morgen');\n            case 'fr':\n                return $this-\u003eoutput('Bonjour');\n            case 'nl':\n                return $this-\u003eoutput('Goedemorgen');\n            default:\n                return $this\n                    -\u003enotAcceptable()\n                    -\u003eoutput(\"This content isn't available in your language\");\n        }\n    }\n}\n```\n\nFor more information, please check the documentation of the [willdurand/negotiation] library.\n\n[willdurand/negotiation]: https://github.com/willdurand/Negotiation\n\nHooks\n---\n\nIn addition to the action method, the controller will also call the `before()` and `after()` method.\n\n### Before\n\nThe `before()` method is call prior to the action method. If it returns a response, the method action is never called.\n\n```php\nclass MyController extends Jasny\\Controller\\Controller\n{\n    protected function before()\n    {\n        if ($this-\u003eauth-\u003egetUser()-\u003egetCredits() \u003c= 0) {\n            return $this-\u003epaymentRequired()-\u003eoutput(\"Sorry, you're out of credits\");\n        }\n    }\n\n    // ...\n}\n```\n\n_Instead of `before()` consider using guards._\n\n### After\n\nThe `after()` method is called after the action, regardless of the action response type.\n\n```php\nclass MyController extends Jasny\\Controller\\Controller\n{\n    // ...\n    \n    protected function after()\n    {\n        $this-\u003eheader('X-Available-Credits', $this-\u003eauth-\u003egetUser()-\u003egetCredits());\n    }\n}\n```\n\nGuards\n---\n\nGuards are [PHP Attributes] that are invoked before the controller method is called. A guard is similar to middleware,\nthough more limited. The purpose of using a guard is to check if the controller action may be executed. If the guard\nreturns a response, that response is emitted and the method on the controller is never called.\n\n```php\nclass MyController extends Jasny\\Controller\\Controller\n{\n    #[MustBeLoggedIn]\n    public function send()\n    {\n        // ...\n    }\n}\n```\n\nA guard class should implement the `process` method. A guard class has the same methods as a controller class. The\n`process` method can have input parameters.\n\n```php\nuse Jasny\\Controller\\Guard;\nuse Jasny\\Controller\\Parameter\\Attr;\n\n#[\\Attribute]\nclass MustBeLoggedIn extends Guard\n{\n    public function process(#[Attr] User? $sessionUser)\n    {\n        if ($sessionUser === null) {\n            return $this-\u003eforbidden()-\u003eoutput(\"Not logged in\");\n        }\n    }\n}\n```\n\n### Order of execution\n\nGuards may be defined on the controller class or the action method. The order of execution is\n\n* Class guards\n* `before()`\n* Method guards\n* Action\n* `after()`\n\n### Dependency injection\n\nGuards are attributes, which are [instantiated using PHP reflection]. Parameters can be specified when the guard is\ndeclared.\n\n```php\n#[MinimalCredits(value: 20)]\nclass MyController extends \\Jasny\\Controller\\Controller\n{\n    // ...\n}\n```\n\nThis makes it difficult to make a service (like a DB connection) available to a guard using dependency injection.\n\nSome DI container libraries, like [PHP-DI](https://php-di.org/), are able to inject services to an already instantiated\nobject. To utilize this, overwrite the `Guardian` class and register it to the container.\n\n```php\nuse Jasny\\Controller\\Guardian;\nuse Jasny\\Controller\\Guard;\nuse DI\\Container;\n\nreturn [\n    Guardian::class =\u003e function (Container $container) {\n        return new class ($container) extends Guardian {\n            public function __construct(private Container $container) {}\n            \n            public function instantiate(\\ReflectionAttribute $attribute): Guard {\n                $guard = $attribute-\u003enewInstance();\n                $this-\u003econtainer-\u003einjectOn($guard);\n                \n                return $guard;\n            }\n        } \n    }\n];\n```\n\nThe guard class can use `#[Inject]` attributes or `@Inject` annotations.\n\n```php\nuse Jasny\\Controller\\Guard;\nuse DI\\Attribute\\Inject;\n\nclass MyGuard extends Guard\n{\n    #[Inject]\n    private DBConnection $db;\n    \n    // ...\n}\n```\n\nMake sure the `Guardian` service is injected into the controller using dependency injection.\n\n```php\nuse Jasny\\Controller\\Controller;\nuse Jasny\\Controller\\Guardian;\n\nclass MyController extends Controller\n{\n    public function __construct(\n        protected Guardian $guardian\n    ) {}\n}\n```\n\n[instantiated using PHP reflection]: https://www.php.net/manual/en/language.attributes.reflection.php\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjasny%2Fcontroller","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjasny%2Fcontroller","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjasny%2Fcontroller/lists"}