zfegg / callable-handler-decorator
Using MVC for PSR applications, like dotnet core MVC.
Requires
- php: >=8.0
- psr/container: ^1.0 || ^2.0
- psr/http-server-handler: ^1.0
Requires (Dev)
- laminas/laminas-config-aggregator: ^1.7
- laminas/laminas-di: ^3.14
- laminas/laminas-diactoros: ^3.0
- laminas/laminas-servicemanager: ^3.6
- mezzio/mezzio: ^3.6
- mezzio/mezzio-fastroute: ^3.3
- mezzio/mezzio-router: ^3.7.0
- monolog/monolog: ^2.0 || ^3.0
- phpunit/phpunit: ^9.5
- slevomat/coding-standard: ^8.15.0
- symfony/property-access: ^5.4 || ^6.0
- symfony/serializer: ^5.4 || ^6.0
- willdurand/negotiation: ^3.0
- zfegg/expressive-test: ^0.7.1
Suggests
- symfony/serializer: Serialize action result required.
This package is auto-updated.
Last update: 2024-07-27 17:04:00 UTC
README
English | 简体中文
PSR MVC handler
Using MVC style for PSR handler applications, like dotnet core MVC.
Using the PHP attributes (annotations), convert the controller to PSR15 RequestHandlerInterface
object.
Installation
composer require zfegg/psr-mvc
Usage
Attributes usage like dotnet core MVC
MVC Route
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
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
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
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
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.
Resolves various types of method results convert to 'Psr\Http\Message\ResponseInterface'. For resolve callback result to ResponseInterface.
Zfegg\PsrMvc\Preparer\SerializationPreparer
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 '<h1>Hello</h1>'; } /* * 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)
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`. * <code> * $result = $serializer->serialize($result, $format); * $response->withBody($result); * </code> */ #[HttpPost('/hello-foo')] public function hello(): Foo { return new Foo(); } }
Preparer options:
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 :
// 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.
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
Rich error handling,
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
When errors occur, you may want to listen for them in order to provide features such as logging. See https://docs.mezzio.dev/mezzio/v3/features/error-handling/#listening-for-errors
use Laminas\Stratigility\Middleware\ErrorHandler; use Zfegg\PsrMvc\Container\LoggingError\LoggingErrorDelegator; return [ 'dependencies' => [ 'delegators' => [ ErrorHandler::class => [ LoggingErrorDelegator::class, ], ], ], ];