https://github.com/jasny/controller
A controller for PSR-7 requests. Supports Slim and other micro-frameworks
https://github.com/jasny/controller
php psr-7 slim-framework
Last synced: 4 months ago
JSON representation
A controller for PSR-7 requests. Supports Slim and other micro-frameworks
- Host: GitHub
- URL: https://github.com/jasny/controller
- Owner: jasny
- License: mit
- Created: 2016-10-16T18:30:18.000Z (about 9 years ago)
- Default Branch: master
- Last Pushed: 2023-06-12T21:12:39.000Z (over 2 years ago)
- Last Synced: 2025-02-06T21:55:27.230Z (11 months ago)
- Topics: php, psr-7, slim-framework
- Language: PHP
- Homepage: http://www.jasny.net/controller/
- Size: 260 KB
- Stars: 1
- Watchers: 2
- Forks: 1
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
Jasny Controller
===
[](https://github.com/jasny/controller/actions/workflows/php.yml)
[](https://scrutinizer-ci.com/g/jasny/controller/?branch=master)
[](https://scrutinizer-ci.com/g/jasny/controller/?branch=master)
[](https://packagist.org/packages/jasny/controller)
[](https://packagist.org/packages/jasny/controller)
PSR-7 controller for [Slim Framework](https://www.slimframework.com/) and other micro-frameworks.
> The controller is responsible handling the HTTP request, manipulate the model and initiate the view.
The code in the controller read as a high level description of each action. The controller should not contain
implementation details. This belongs in the model, view or in services and libraries.
Installation
---
Install using composer
composer require jasny\controller
Setup
---
`Jasny\Controller` can be used as a base class for each of your controllers. It lets you interact with the
[PSR-7](http://www.php-fig.org/psr/psr-7/) server request and response in a friendly matter.
```php
class MyController extends Jasny\Controller\Controller
{
public function hello(string $name, #[QueryParam] string $others = ''): void
{
$this->output("Hello $name" . ($others ? " and $others" : ""), 'text');
}
}
```
> Visiting `https://example.com/hello/Arnold&others=friends` would output `Hello Arnold and friends`.
Actions are defined as public methods of the controller.
A controller is a callable object by implementing the [`__invoke`][] method. The invoke method takes a PSR-7
server request and response object and will return a modified response object. This all is abstracted away when you
write your controller.
A router typically handles the request and chooses the correct controller object to call. The router is also responsible
for extracting parameters from the url path and possibly choosing a method to call within the controller.
[`__invoke`]: http://php.net/manual/en/language.oop5.magic.php#object.invoke
### Slim framework
[Slim](https://www.slimframework.com/) is a PHP micro-framework that works with PSR-7. To use this library with slim,
use the provided middleware.
```php
use Jasny\Controller\Middleware\Slim as ControllerMiddleware;
use Slim\Factory\AppFactory;
$app = AppFactory::create();
$app->add(new ControllerMiddleware());
$app->addRoutingMiddleware();
$app->get('/hello/{name}', ['MyController', 'hello']);
```
Optionally, the middleware can convert error responses from the controller to Slim HTTP Errors by passing `true` to the
middleware constructor.
```php
use Jasny\Controller\Middleware\Slim as ControllerMiddleware;
use Slim\Factory\AppFactory;
$app = AppFactory::create();
$app->add(new ControllerMiddleware(true));
$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, true, true);
```
### Relay + SwitchRoute
[SwitchRoute](https://github.com/jasny/switch-route), a super-fast router based on generating code. The router needs a
PSR-15 request handler to work with PRS-7 server requests, like [Relay](https://relayphp.com/).
By default, the route action is converted to the method that will be called by the PSR-15 handler. For this library,
`__invoke` should be called instead. The invoke method will take care of calling the right method within the controller.
```php
$stud = fn($str) => strtr(ucwords($str, '-'), ['-' => '']);
$invoker = new Invoker(fn (?string $controller, ?string $action) => [
$controller !== null ? $stud($controller) . 'Controller' : $stud($action) . 'Action',
'__invoke'
]);
```
**[See SwitchRoute for more information](https://github.com/jasny/switch-route#readme)**
Output
---
When using PSR-7, you shouldn't use `echo`, because it makes it harder to write tests. Instead, use the `output` method
of the controller, which writes to the response body stream object.
```php
$this->output('Hello world');
```
A second argument may be passed, which sets the `Content-Type` header. You can pass a mime type like 'text/html'.
Alternatively you can use a common file extension like 'txt'. The controller uses the
[ralouphie/mimey](https://github.com/ralouphie/mimey) library to get the mime type.
```php
class MyController extends Jasny\Controller\Controller
{
/**
* Output a random number between 0 and 100 as HTML
*/
public function random()
{
$number = rand(0, 100);
$this->output("
$number
", 'html');
}
}
```
### JSON
The `json` method can be used to serialize and output data as JSON.
```php
class MyController extends Jasny\Controller\Controller
{
/**
* Output 5 random numbers between 0 and 100 as JSON
*/
public function random()
{
$numbers = array_map(fn() => rand(0, 100), range(1, 5));
$this->json($numbers);
}
}
```
### Response status
To set the response status you can use the `status()` method. This method can take the response status as integer or
as string specifying both the status code and phrase.
```php
class MyController extends Jasny\Controller\Controller
{
public function process(string $size)
{
if (!in_array($size, ['XS', 'S', 'M', 'L', 'XL'])) {
return $this
->status("400 Bad Request")
->output("Invalid size: $size");
}
// Create something ...
return $this
->status(201)
->header("Location: http://www.example.com/foo/something")
->json($something);
}
}
```
Alternatively and preferably you can use helper method to set a specific response status. Some method can optionally
take arguments that make sense for that status.
```php
class MyController extends Jasny\Controller\Controller
{
public function process(string $size)
{
if (!in_array($size, ['XS', 'S', 'M', 'L', 'XL'])) {
return $this->badRequest()->output("Invalid size: $size");
}
// Create something ...
return $this
->created("http://www.example.com/foo/something")
->json($something);
}
}
```
The following methods for setting the output status are available
| status code | method | |
|-------------------------|----------------------------------------------------------------|-----------------------------------------------------|
| [200][] | `ok()` | |
| [201][] | `created(?string $location = null)` | Optionally set the `Location` header |
| [202][] | `accepted()` | |
| [204][]/[205][] | `noContent(int $code = 204)` | |
| [206][] | `partialContent(int $rangeFrom, int $rangeTo, int $totalSize)` | Set the `Content-Range` and `Content-Length` header |
| [30x][303] | `redirect(string $url, int $code = 303)` | Url for the `Location` header |
| [303][] | `back()` | Redirect to the referer |
| [304][] | `notModified()` | |
| [40x][400] | `badRequest(int $code = 400)` | |
| [401][] | `unauthorized()` | |
| [402][] | `paymentRequired()` | |
| [403][] | `forbidden()` | |
| [404][]/[405][]/[410][] | `notFound(int $code = 404)` | |
| [406][] | `notAcceptable()` | |
| [409][] | `conflict()` | |
| [429][] | `tooManyRequests()` | |
| [5xx][500] | `error(int $code = 500)` | |
- Some methods take a `$message` argument. This will set the output.
- If a method takes a `$code` argument, you can specify the status code.
- The `back()` method will redirect to the referer, but only if the referer is from the same domain as the current url.
[200]: https://httpstatuses.com/200
[201]: https://httpstatuses.com/201
[202]: https://httpstatuses.com/202
[203]: https://httpstatuses.com/203
[204]: https://httpstatuses.com/204
[205]: https://httpstatuses.com/205
[206]: https://httpstatuses.com/206
[303]: https://httpstatuses.com/303
[304]: https://httpstatuses.com/304
[400]: https://httpstatuses.com/400
[401]: https://httpstatuses.com/401
[402]: https://httpstatuses.com/402
[403]: https://httpstatuses.com/403
[404]: https://httpstatuses.com/404
[405]: https://httpstatuses.com/405
[406]: https://httpstatuses.com/406
[410]: https://httpstatuses.com/410
[409]: https://httpstatuses.com/409
[429]: https://httpstatuses.com/429
[500]: https://httpstatuses.com/500
Sometimes it's useful to check the status code that has been set for the response. This can be done with the
`getStatusCode()` method. In addition, there are methods to check the type of status.
| status code | method |
|-------------|---------------------|
| 1xx | `isInformational()` |
| 2xx | `isSuccessful()` |
| 3xx | `isRedirection()` |
| 4xx | `isClientError()` |
| 5xx | `isServerError()` |
| 4xx or 5xx | `isError()` |
### Response headers
You can set the response header using the `setResponseHeader()` method.
```php
class MyController extends Jasny\Controller\Controller
{
public function process()
{
$this->header("Content-Language", "nl");
// ...
}
}
```
By default, response headers are overwritten. In some cases you want to have duplicate headers. In that case set the
third argument to `true`, eg `header($header, $value, true)`.
```php
$this->header("Cache-Control", "no-cache"); // overwrite header
$this->header("Cache-Control", "no-store", true); // add header
```
Input
---
With PSR-7, you shouldn't use super globals `$_GET`, `$_POST`, `$_COOKIE`, and `$_SERVER`. Instead, these values are
available through the server request object. This is done using [PHP attributes][].
| Attribute | Arguments | |
|-----------------|------------|-------------------------------------------|
| `PathParam` | name, type | Path parameter obtained from router |
| `QueryParam` | name, type | Query parameter |
| `Query` | | All query parameters |
| `BodyParam` | name, type | Body parameter |
| `Body` | | All body parameters or raw body as string |
| `Cookie` | name, type | Cookie parameter |
| `Cookies` | | All cookies as key/value |
| `UploadedFile` | name | PSR-7 uploaded file(s) |
| `UploadedFiles` | | Associative array of all uploaded files |
| `Header` | name, type | Request header (as string) |
| `Headers` | | All headers as associative array |
| `Attr` | name, type | PSR-7 request attribute set by middleware |
[PHP attributes]: https://www.php.net/manual/en/language.attributes.overview.php
The controller will map each argument of a method to a parameter. By default, arguments are mapped to path parameters.
### Parameters
#### Path parameters
A router may extract parameters from the request URL. In the following example, the url path `/hello/world`,
the path parameter `name` will have the value `"world"`.
```php
$app->get('/hello/{name}', ['MyController', 'hello']);
```
The `name` parameter will be passed as argument to the `hello` method.
```php
class MyController extends Jasny\Controller\Controller
{
public function hello(string $name)
{
$this->output("Hello $name");
}
}
```
#### Single request parameter
The controller will pass PSR-7 request parameters as arguments. This is specified by an attribute
* `QueryParam`
* `BodyParam`
* `Cookie`
* `UploadedFile`
* `Header`
If the argument name is used as parameter name
* for `QueryParam`, underscores are replaced with dashes. Eg: `$foo_bar` will translate to query param `foo-bar`.
* for `Header`, words are capitalized and underscores become dashes. Eg: `$foo_bar` translates to header `Foo-Bar`.
#### All request parameters
To get all request parameters of a specific type, the following attributes are available.
* `Query`
* `Body`
* `Cookies`
* `UploadedFiles`
* `Headers`
For the `Body` attribute, the type of the argument should either be an array or a string. If an array is passed the
argument will be the parsed body. In case of a string it will be the raw body.
#### PSR-7 request attribute
Middleware can set attributes of the PSR-7 request. These request attributes are available as arguments by using the
`Attr` attribute.
### Parameter name
For single parameters, the name of the argument will be used as parameter name. Alternatively, it's possible to specify
a name when defining the attribute.
```php
use Jasny\Controller\Controller;
use Jasny\Controller\Parameter\PathParam;
use Jasny\Controller\Parameter\QueryParam;
class MyController extends Controller
{
public function hello(#[PathParam] string $name, #[QueryParam('and')] string $other = '')
{
$this->output("Hello $name" . ($other ? " and $other" : ""));
}
}
```
_Note: `#[PathParam]` could be omitted, since it's the default behaviour._
### Parameter type
It's possible to specify a type as second argument when defining the attribute. By default, the type is determined on
the type of the argument.
```php
use Jasny\Controller\Controller;
use Jasny\Controller\Parameter\BodyParam;
class MyController extends Controller
{
public function send(#[BodyParam(type: 'email')] string $emailAddress)
{
// ...
}
}
```
Parameter attributes use the [`filter_var`](https://www.php.net/filter_var) function to sanitize input. The following
filters are defined
| type | filter |
|-------|-------------------------|
| bool | `FILTER_VALIDATE_BOOL` |
| int | `FILTER_VALIDATE_INT` |
| float | `FILTER_VALIDATE_FLOAT` |
| email | `FILTER_VALIDATE_EMAIL` |
| url | `FILTER_VALIDATE_URL` |
For other types (like `string`), no filter is applied.
```php
use Jasny\Controller\Controller;
use Jasny\Controller\Parameter\PostParam;
class MyController extends Controller
{
public function message(#[PostParam(type: 'email')] array $email)
{
// ...
}
}
```
To add custom types, add filters to `SingleParameter::$types`
```php
use Jasny\Controller\Parameter\SingleParameter;
SingleParameter::$types['slug'] = [FILTER_VALIDATE_REGEXP, '/^[a-z\-]+$/'];
```
Content negotiation
---
Content negotiation allows the controller to give different output based on `Accept` request headers. It can be used to
select the content type (switch between JSON and XML), the content language, encoding, and charset.
| Method | Request header | Response header |
|--------------------------|-------------------|--------------------|
| `negotiateContentType()` | `Accept` | `Content-Type` |
| `negotiateLanguage()` | `Accept-Language` | `Content-Language` |
| `negotiateEncoding()` | `Accept-Encoding` | `Content-Encoding` |
| `negotiateCharset()` | `Accept-Charset` | |
_`negotiateCharset()` will modify the `Content-Type` header if it's already set. Otherwise, it will just
return the selected charset._
The negotiate method takes a list or priorities as argument. It sets the response header and returns the selected
option.
```php
class MyController extends Jasny\Controller\Controller
{
public function hello()
{
$language = $this->negotiateLanguage(['en', 'de', 'fr', 'nl;q=0.6']);
switch ($language) {
case 'en':
return $this->output('Good morning');
case 'de':
return $this->output('Guten Morgen');
case 'fr':
return $this->output('Bonjour');
case 'nl':
return $this->output('Goedemorgen');
default:
return $this
->notAcceptable()
->output("This content isn't available in your language");
}
}
}
```
For more information, please check the documentation of the [willdurand/negotiation] library.
[willdurand/negotiation]: https://github.com/willdurand/Negotiation
Hooks
---
In addition to the action method, the controller will also call the `before()` and `after()` method.
### Before
The `before()` method is call prior to the action method. If it returns a response, the method action is never called.
```php
class MyController extends Jasny\Controller\Controller
{
protected function before()
{
if ($this->auth->getUser()->getCredits() <= 0) {
return $this->paymentRequired()->output("Sorry, you're out of credits");
}
}
// ...
}
```
_Instead of `before()` consider using guards._
### After
The `after()` method is called after the action, regardless of the action response type.
```php
class MyController extends Jasny\Controller\Controller
{
// ...
protected function after()
{
$this->header('X-Available-Credits', $this->auth->getUser()->getCredits());
}
}
```
Guards
---
Guards are [PHP Attributes] that are invoked before the controller method is called. A guard is similar to middleware,
though more limited. The purpose of using a guard is to check if the controller action may be executed. If the guard
returns a response, that response is emitted and the method on the controller is never called.
```php
class MyController extends Jasny\Controller\Controller
{
#[MustBeLoggedIn]
public function send()
{
// ...
}
}
```
A guard class should implement the `process` method. A guard class has the same methods as a controller class. The
`process` method can have input parameters.
```php
use Jasny\Controller\Guard;
use Jasny\Controller\Parameter\Attr;
#[\Attribute]
class MustBeLoggedIn extends Guard
{
public function process(#[Attr] User? $sessionUser)
{
if ($sessionUser === null) {
return $this->forbidden()->output("Not logged in");
}
}
}
```
### Order of execution
Guards may be defined on the controller class or the action method. The order of execution is
* Class guards
* `before()`
* Method guards
* Action
* `after()`
### Dependency injection
Guards are attributes, which are [instantiated using PHP reflection]. Parameters can be specified when the guard is
declared.
```php
#[MinimalCredits(value: 20)]
class MyController extends \Jasny\Controller\Controller
{
// ...
}
```
This makes it difficult to make a service (like a DB connection) available to a guard using dependency injection.
Some DI container libraries, like [PHP-DI](https://php-di.org/), are able to inject services to an already instantiated
object. To utilize this, overwrite the `Guardian` class and register it to the container.
```php
use Jasny\Controller\Guardian;
use Jasny\Controller\Guard;
use DI\Container;
return [
Guardian::class => function (Container $container) {
return new class ($container) extends Guardian {
public function __construct(private Container $container) {}
public function instantiate(\ReflectionAttribute $attribute): Guard {
$guard = $attribute->newInstance();
$this->container->injectOn($guard);
return $guard;
}
}
}
];
```
The guard class can use `#[Inject]` attributes or `@Inject` annotations.
```php
use Jasny\Controller\Guard;
use DI\Attribute\Inject;
class MyGuard extends Guard
{
#[Inject]
private DBConnection $db;
// ...
}
```
Make sure the `Guardian` service is injected into the controller using dependency injection.
```php
use Jasny\Controller\Controller;
use Jasny\Controller\Guardian;
class MyController extends Controller
{
public function __construct(
protected Guardian $guardian
) {}
}
```
[instantiated using PHP reflection]: https://www.php.net/manual/en/language.attributes.reflection.php