PHPackages                             zfegg/callable-handler-decorator - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [PSR &amp; Standards](/categories/psr-standards)
4. /
5. zfegg/callable-handler-decorator

Abandoned → [zfegg/psr-mvc](/?search=zfegg%2Fpsr-mvc)Library[PSR &amp; Standards](/categories/psr-standards)

zfegg/callable-handler-decorator
================================

Using MVC for PSR applications, like dotnet core MVC.

3.1.0(1y ago)151MITPHPPHP &gt;=8.0

Since Jan 20Pushed 1y ago1 watchersCompare

[ Source](https://github.com/zfegg/psr-mvc)[ Packagist](https://packagist.org/packages/zfegg/callable-handler-decorator)[ RSS](/packages/zfegg-callable-handler-decorator/feed)WikiDiscussions main Synced 4w ago

READMEChangelog (9)Dependencies (16)Versions (12)Used By (0)

English | [简体中文](README.zh-CN.md)

PSR MVC handler
===============

[](#psr-mvc-handler)

[![GitHub Actions: Run tests](https://github.com/zfegg/psr-mvc/workflows/qa/badge.svg)](https://github.com/zfegg/psr-mvc/actions?query=workflow%3A%22qa%22)[![Coverage Status](https://camo.githubusercontent.com/c235e89eb3e5570a919e8da53d7725cf6ce246591d5d362fbd3a5170e55241ed/68747470733a2f2f636f766572616c6c732e696f2f7265706f732f6769746875622f7a666567672f7073722d6d76632f62616467652e7376673f6272616e63683d6d61696e)](https://coveralls.io/github/zfegg/psr-mvc?branch=main)[![Latest Stable Version](https://camo.githubusercontent.com/d95626e963f56a0c2bb63e5e3639d5c40e5ce5c654e2ebf805538ffc1b29e30e/68747470733a2f2f706f7365722e707567782e6f72672f7a666567672f7073722d6d76632f762f737461626c652e706e67)](https://packagist.org/packages/zfegg/psr-mvc)

Using MVC style for PSR handler applications, like [dotnet core MVC](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-6.0).
Using the PHP attributes (annotations), convert the controller to PSR15 `RequestHandlerInterface` object.

Installation
------------

[](#installation)

```
composer require zfegg/psr-mvc
```

Usage
-----

[](#usage)

Attributes usage like [dotnet core MVC](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-6.0)

### MVC Route

[](#mvc-route)

#### Getting started with Mezzio

[](#getting-started-with-mezzio)

```
// File config/config.php
// Add ConfigProvider

new ConfigAggregator([
  Zfegg\PsrMvc\ConfigProvider::class,
]);
```

```
// config/autoload/global.php
use Zfegg\PsrMvc\Container\HandlerFactory;

return [
    // Add scan controllers paths
    \Zfegg\PsrMvc\Routing\RouteMetadata::class => [
        'paths' => ['path/to/Controller'],
        'cacheFile' => 'data/cache/route-meta.php', // For cache routes
    ]
];

// path/to/Controller/HomeController.php
public class HomeController
{
    #[Route("/")]
    #[Route("/home")]
    #[Route("/home/index")]
    #[Route("/home/index/{id?}")]
    public index(?int $id)
    {
        return new HtmlResponse();
    }

    #[Route("/home/about")]
    #[Route("/home/about/{id}")]
    public about(?int $id)
    {
        return new HtmlResponse();
    }
}
```

#### Attributes

[](#attributes)

- `Route(string $path, array $middlewares = [], ?string $name = null, array $options = [], ?array $methods = null)`
- `HttpGet(string $path, array $middlewares = [], ?string $name = null, array $options = [])`
- `HttpPost(string $path, array $middlewares = [], ?string $name = null, array $options = [])`
- `HttpPatch(string $path, array $middlewares = [], ?string $name = null, array $options = [])`
- `HttpPut(string $path, array $middlewares = [], ?string $name = null, array $options = [])`
- `HttpDelete(string $path, array $middlewares = [], ?string $name = null, array $options = [])`
- `HttpHead(string $path, array $middlewares = [], ?string $name = null, array $options = [])`

Register routes by PHP attributes.

```
return [
  RouteMetadata::class => [
    // Scan controller paths.
    'paths' => [
       'path/Controller',
    ],
  ],
]
```

The following code applies `#[Route("/[controller]/[action]")]` to the controller:

```
public class HomeController
{
    #[Route("/")]
    #[Route("/home")]
    #[Route("/home/index")]
    #[Route("/home/index/{id?}")]
    public index(?int $id)
    {
        return new HtmlResponse();
    }

    #[Route("/home/about")]
    #[Route("/home/about/{id}")]
    public about(?int $id)
    {
        return new HtmlResponse();
    }
}
```

#### Combining attribute routes

[](#combining-attribute-routes)

```
use Psr\Http\Message\ResponseInterface;

#[Route("/api/[controller]")] // Route prefix `/api/products`
class ProductsController {

    #[HttpGet]   // GET /api/products
    public function listProducts(): array {
        return $db->fetchAllProducts();
    }

    // Route path `/api/products/{id}`
    #[HttpGet('{id}')] // GET /api/products/123
    public function getProduct(int $id): object {
        return $db->find($id);
    }

    #[HttpPost]  // POST /api/products
    public function create(#[FromBody(root: true)] array $data): object {
        $db->save($data);
        // ...
        return $db->find($lastInsertId);
    }
}
```

### Wrap controller handler

[](#wrap-controller-handler)

#### Using param attributes

[](#using-param-attributes)

- `FromAttribute(?string $name = null)`
    - `$name` default is the parameter name
- `FromBody(?string $name = null, ?bool $root = false, array $serializerContext = [])`
    - `$name` default is the parameter name
- `FromContainer(?string $name = null)`
    - `$name` default is the parameter type
- `FromCookie(?string $name = null)`
    - `$name` default is the parameter name
- `FromHeader(?string $name = null)`
    - `$name` default is the parameter name
- `FromQuery(?string $name = null)`
    - `$name` default is the parameter name
- `FromServer(string $name)`

```
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/*
POST /api/example/hello?page=1
Host: localhost
Cookie: PHPSESSION=xxx

name=foo
*/

class ExampleController {
    #[HttpPost('/api/[controller]/[action]')]
    public function post(
        #[FromQuery]
        int $page,              // 1
        #[FromBody]
        string $name,           // "foo"
        #[FromContainer('db')]
        \PDO $container,        // object(PDO)
        #[FromCookie('PHPSESSION')]
        string $sessionId,      // "xxx"
        #[FromHeader]
        string $host,           // "localhost"
        #[FromServer('REMOTE_ADDR')]
        string $ip,             // "127.0.0.1"
    ): void {
        return ;
    }

    // Default binding params
    #[HttpPost('/api/[controller]/[action]/{id}')]
    public function hello(
       ServerRequestInterface $request,  // Default bind `$request`.
       int $id,    // Default bind `$request->getAttribute('id')`.
       Foo $foo,   // If container exists the `Foo`, default bind `$container->get('id')`.
       Bar $bar,   // Default bind `$request->getAttribute(Bar::class, $request->getAttribute('bar'))`.
    ): void {
    }
}
```

#### Default param bindings

[](#default-param-bindings)

```
class ExampleController {
    #[HttpPost('/api/[controller]/[action]/{id}')]
    public function hello(
       ServerRequestInterface $request,  // Default bind `$request`.
       int $id,    // Default bind `$request->getAttribute('id')`.
       Foo $foo,   // If container exists the `Foo`, default bind `$container->get('id')`.
       Bar $bar,   // Default bind `$request->getAttribute(Bar::class, $request->getAttribute('bar'))`.
    ): void {
    }
}
```

### Prepare result to PSR response.

[](#prepare-result-to-psr-response)

Resolves various types of method results convert to 'Psr\\Http\\Message\\ResponseInterface'. For resolve callback result to ResponseInterface.

#### `Zfegg\PsrMvc\Preparer\SerializationPreparer`

[](#zfeggpsrmvcpreparerserializationpreparer)

```
class ExampleResponseController {
    #[HttpPost('/hello-void')]  // `void` -> HTTP 204 No Content
    public function helloVoid(): void {
    }

    /*
     *  If result is string, then convert to `HtmlResponse` object.
     *  `new HtmlResponse($result)`
     */
    #[HttpPost('/hello-string')]
    public function helloString(): string {
      return 'Hello';
    }

    /*
     *  If result is array, default convert to `JsonResponse` object.
     *  `new JsonResponse($result)`
     */
    #[HttpPost('/hello-array')]
    public function helloArray(): array {
      return ['foo' => 'a', 'bar' => 'b'];
    }
}
```

#### `Zfegg\PsrMvc\Preparer\SerializationPreparer` (Recommend)

[](#zfeggpsrmvcpreparerserializationpreparer-recommend)

Serialize by `symfony/serializer` and write the response body.

```
class ExampleResponseController {
    #[HttpPost('/hello-void')]  // `void` -> HTTP 204 No Content
    public function helloVoid(): void {
    }

    /*
     * Serialize by `symfony/serializer`.
     * The serialization format is parsed by `FormatMatcher`.
     *
     * $result = $serializer->serialize($result, $format);
     * $response->withBody($result);
     *
     */
    #[HttpPost('/hello-foo')]
    public function hello(): Foo {
     return new Foo();
    }
}
```

Preparer options:

KeydescriptionstatusHttp response code.headersHttp response headers.```$serializer->serialize` context variables.#### The `PrepareResult` attribute

[](#the-prepareresult-attribute)

Using `#[PrepareResult]` attribute to select a preparer and pass the context.

```
use \Zfegg\PsrMvc\Preparer\SerializationPreparer;
use Zfegg\PsrMvc\Attribute\PrepareResult;

class ExampleResponseController {
    #[HttpPost('/hello-void')]  // `void` -> HTTP 204 No Content
    public function helloVoid(): void {
    }

    /*
     * 选用 `SerializationPreparer` 预处理器, 处理结果.
     */
    #[HttpPost('/hello-foo')]
    #[PrepareResult(SerializationPreparer::class, ['status' => 201, 'headers' => ['X-Test' => 'foo']])]
    public function hello(): Foo {
     return new Foo();
    }
}
```

### Example for Mezzio :

[](#example-for-mezzio-)

```
// Class file HelloController.php

class HelloController {
  public function say(
    \Psr\Http\Message\ServerRequestInterface $request, // Inject request param
    string $name, // Auto inject param from $request->getAttribute('name').
    Foo $foo     // Auto inject param from container.
  ) {
    return new TextResponse('hello ' . $name);
  }
}
```

```
// File config/config.php
// Add ConfigProvider

new ConfigAggregator([
  Zfegg\PsrMvc\ConfigProvider::class,
]);
```

```
// config/autoload/global.php
// Add demo class factories

use Zfegg\PsrMvc\Container\HandlerFactory;

return [
    'dependencies' => [
        'invokables' => [
            Hello::class,
        ],
        'factories' => [
            Hello::class . '@say' => HandlerFactory::class,
        ],
    ]
];
```

### Register route without attribute.

[](#register-route-without-attribute)

Using `CallableHandlerAbstractFactory` register route.

```
// config/autoload/global.php
// Add demo class factories

use Zfegg\PsrMvc\Container\CallbackHandlerAbstractFactory;

return [
    'dependencies' => [
        'factories' => [
            ExampleController::class . '@fooMethod' => CallbackHandlerAbstractFactory::class,
        ],
    ]
];

$app->get('/foo-method', ExampleController::class . '@fooMethod')
```

Register abstract factory in `laminas/laminias-servicemanager`.

```
// config/autoload/global.php
// Add demo class factories

use Zfegg\PsrMvc\Container\CallbackHandlerAbstractFactory;

return [
    'dependencies' => [
        'invokables' => [
            Hello::class,
        ],
        'abstract_factories' => [
            CallbackHandlerAbstractFactory::class,
        ],
    ]
];

class User {
  function create() {}
  function getList() {}
  function get($id) {}
  function delete($id) {}
}

// CallableHandlerDecorator abstract factory.
$container->get('User@create');
$container->get('User@getList');
$container->get('User@get');
$container->get('User@delete');
```

### ErrorHandler for mezzio

[](#errorhandler-for-mezzio)

Rich error handling,

#### Response to json format

[](#response-to-json-format)

Throw exception in handler.

```
use \Zfegg\PsrMvc\Exception\AccessDeniedHttpException;
use \Zfegg\PsrMvc\Attribute\HttpGet;

class FooController {
  #[HttpGet("/api/foo")]
  public function fooAction() {
    throw new AccessDeniedHttpException("Foo", code: 100);
  }
}
```

When request is ajax will response to json result:

```
HTTP/1.1 403 Forbidden

{"message":"Foo","code":100}

```

#### logging errors

[](#logging-errors)

When errors occur, you may want to listen for them in order to provide features such as logging. See

```
use Laminas\Stratigility\Middleware\ErrorHandler;
use Zfegg\PsrMvc\Container\LoggingError\LoggingErrorDelegator;

return [
    'dependencies' => [
        'delegators' => [
            ErrorHandler::class => [
                LoggingErrorDelegator::class,
            ],
        ],
    ],
];
```

###  Health Score

32

—

LowBetter than 71% of packages

Maintenance34

Infrequent updates — may be unmaintained

Popularity10

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity64

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~160 days

Recently: every ~133 days

Total

9

Last Release

650d ago

Major Versions

0.1.1 → 1.5.02022-01-27

1.5.1 → 2.0.02023-02-10

2.2.1 → 3.0.02023-09-13

PHP version history (2 changes)0.1.0PHP ^7.3 || ^8.0

1.5.0PHP &gt;=8.0

### Community

Maintainers

![](https://www.gravatar.com/avatar/bc59373bf8678b7852e4fffd0b9154a50db68a27ccb38aa71c12f52f229888b1?d=identicon)[Moln](/maintainers/Moln)

---

Top Contributors

[![Moln](https://avatars.githubusercontent.com/u/2050694?v=4)](https://github.com/Moln "Moln (64 commits)")

---

Tags

attributemvcphppsrroutingpsrhandlermvccontroller

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/zfegg-callable-handler-decorator/health.svg)

```
[![Health](https://phpackages.com/badges/zfegg-callable-handler-decorator/health.svg)](https://phpackages.com/packages/zfegg-callable-handler-decorator)
```

###  Alternatives

[cakephp/cakephp

The CakePHP framework

8.8k18.5M1.6k](/packages/cakephp-cakephp)[lctrs/psalm-psr-container-plugin

Let Psalm understand better psr11 containers

17648.1k13](/packages/lctrs-psalm-psr-container-plugin)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
