This is a very convenient wrapper around the Slim4 framework providing us with some shortcuts and prepopulated concepts.
Add this to composer.json
:
"require": {
"adeptoas/slim3-init": "^4.0.0"
}
Make sure to merge your require
-blocks!
-
$app = new SlimInit();
-
If this header is present in the request, it will enable additional information to be shown when using the default exception handler. Set null to disable this feature.
setDebugHeader(?string $header, string $expectedValue = ''): SlimInit
-
Set the HTTP status code for an exception when using the default exception handler.
setException(string $exception, int $statusCode): SlimInit
-
Customize the handling of an exception entirely.
$exceptionHandlerClass
must be the fully qualified name of a class that extendsAdepto\Slim3Init\Handlers\ExceptionHandler
.setException(string $exception, string $exceptionHandlerClass): SlimInit
-
setException(array $exceptions, int|string $statusCodeOrHandlerClass): SlimInit
-
To send an exception to an error collection service, add an exception callback:
/** @var $app \Adepto\Slim3Init\SlimInit */ $app->addExceptionCallback(function(\Adepto\Slim3Init\Request $request, Throwable $t) { // Send $t to the service });
Or as a class:
class ExceptionCallback { public function __invoke(\Adepto\Slim3Init\Request $request, Throwable $t) { // Send $t to the service } } /** @var $app \Adepto\Slim3Init\SlimInit */ $app->addExceptionCallback(new ExceptionCallback());
-
addToContainer(string $key, mixed $value): SlimInit
-
$className
must be the name of a class that extendsAdepto\Slim3Init\Handlers\Handler
.addHandler(string $className): SlimInit
-
Add all handlers from a specific directory. Non-recursive and the filenames must be the class names followed by
.php
.addHandlersFromDirectory(string $dir): SlimInit
-
Refer to Slim's documentation for more information about middleware.
addMiddleware(callable $middleware): SlimInit
-
Boot up the application and listen to incoming requests. Automatically appoints all handlers and maps everything.
run(): Slim\App
All mocking methods return the text output that would've been sent to the browser. This is a JSON string most of the times.
-
Create a caller for
$handlerClass
. You can leave $baseURL empty but for consistency and compatibility you should set this to the base URL this handler would've listened to (without the route URL).$caller = new HandlerCaller(string $baseURL, string $handlerClass);
-
Mock a GET request to
$url
with$headers
.get(string $url, array $headers = []): string
-
Mock a POST request to
$url
with$headers
and send$body
with it. If$body
is an array, it will be converted to Form or JSON, based onContent-Type
in$headers
(default is Form).post(string $url, array $headers, mixed $body): string
-
Mock a PUT request to
$url
with$headers
and send$body
and$files
with it. If$body
is an array, it will be converted to Form or JSON, based onContent-Type
in$headers
(default is Form).put(string $url, array $headers, mixed $body, array $files = []): string
-
Same as POST, just with PATCH as HTTP method and
$files
.patch(string $url, array $headers, mixed $body, $files = []): string
-
Same as POST, just with DELETE as HTTP method.
delete(string $url, array $headers, mixed $body): string
To have your API do something, you need to create handlers which extend Adepto\Slim3Init\Handlers\Handler
. Each
handler must override getRoutes(): array
to return an array of routes. Each handler receives a container in the
constructor by default.
The actual methods of your handler must have the following signature:
public function someName(Adepto\Slim3Init\Request $request, Adepto\Slim3Init\Response $response, \stdClass $args): Adepto\Slim3Init\Response
Same as for Handler, only that this type of handler also has to
override actionAllowed(string $action, array $data = []): bool
to determine, if a given action is allowed and
permitted. A PrivilegedHandler has an authorization client (client used to authenticate, instance
of Adepto\Slim3Init\Client\Client
) via getClient()
.
forcePermission(string $action, array $data = []): bool
Force a permission. This is basically just an alias for actionAllowed
(which you have to override) but throws
a Adepto\Slim3Init\Exceptions\AccessDeniedException
if the given permission is not allowed.
Defines a route which has to be returned inside an array returned by your handler's getRoutes()
function.
new Route(string $httpMethod, string $url, string $classMethod, array $arguments = [], string $name = '')
$httpMethod
: The HTTP verb used for this route, i.e. GET, POST, PATCH, ...$url
: Slim-compatible URL pattern, i.e./client/{client:[a-zA-Z0-9]+}
$classMethod
: The name of the method to be called in the handler.$arguments
: Additional arguments to add to$args
of the method.$name
: Name for the route so that it can be retrieved by any handlers.
-
getUsername(): string
Should return the username of the currently logged in user (if
BasicAuth
was used). -
getPermissions(): array
Should return an array full of
Adepto\Slim3Init\Client\Permission
objects for the currently logged in user ( ifBasicAuth
was used). -
hasPermission(string $name, array $data = []): bool;
Should return true when the currently logged in user has a certain permission. You can use
$data
to combine the permission with more info, i.e. when a resource's information access should be constrained to certain IDs.
-
getName(): string
Should return the name of the permission. You are free to define how a name looks like. It is recommended to use reverse-domain style, i.e.
adepto.api.addKnowledge
. -
getData(): array
Should return information specific to that permission, i.e. IDs of a resource that can be accessed. Can be an empty array, if there is no information.
-
isAllowed(): bool
Should return true, if the permission is allowed.
-
authorize(array $credentials): array
Should return an array with more information to be added to the container, i.e. an authorized client to be used with a PrivilegedHandler. If you're going to return a client, make sure to set the key to
PrivilegedHandler::CONTAINER_CLIENT
. Should throw anAdepto\Slim3Init\Exceptions\UnauthorizedException
if the user could not be authorized.
Examples can be found in examples/
of this repository.
While quite a lot has changed under the hood in Slim, the actual effects on SlimInit are as minimal as possible. There are 3 breaking changes and a few minor changes.
Previously, all handlers were defined using only Psr7-compatible interfaces. While you can still define your handler's
arguments using Psr7, the return value must definitely be an instance of Adepto\Slim3Init\Response
. If you need to
convert an existing response, use Response::fromSlimResponse($originalResponse)
.
This change comes directly from Slim4, as SlimInit does not change this behavior. Previously, middleware worked like this:
<?php
/* Slim3 */
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
class YourMiddleware {
protected $container;
public function __construct(ContainerInterface $container) {
$this->container = $container;
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, Callable $next): ResponseInterface {
// Something before others run
$newResponse = $next($request, $response);
// Code after others have run
return $newResponse;
}
}
Now, middleware uses a RequestHandlerInterface
to process other code:
<?php
/* Slim4 */
use Psr\Container\ContainerInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Adepto\Slim3Init\Request;
class YourMiddleware {
protected $container;
public function __construct(ContainerInterface $container) {
$this->container = $container;
}
public function __invoke(Request $request, RequestHandlerInterface $handler): ResponseInterface {
// Something before others run
$response = $handler->handle($request);
// Code after others have run
return $response;
}
}
You no longer have access to the response before other middleware and handlers have run.
Instead of overriding these methods in your own application to customize the handling of 500, 404 and 405 respectively,
you now implement your own ExceptionHandler
and assign it to exceptions. Example:
<?php
use Adepto\Slim3Init\SlimInit;
use Adepto\Slim3Init\Request;
use Adepto\Slim3Init\Handlers\ExceptionHandler;
use Psr\Http\Message\ResponseInterface;
class NotFoundHandler extends ExceptionHandler {
public function handle(Request $request, Throwable $t, bool $displayDetails): ResponseInterface {
return $this->createResponse(404)
->withJson(['error' => 'not_found']);
}
}
// … your $app definition
/** @var $app SlimInit */
$app->setException(SomethingNotFoundException::class, NotFoundHandler::class);
You can also override already existing default handlers for 404, 405 and 500:
use Adepto\Slim3Init\Exceptions\InternalErrorException;
use Adepto\Slim3Init\Exceptions\MethodNotAllowedException;
use Adepto\Slim3Init\Exceptions\NotFoundException;
use Adepto\Slim3Init\SlimInit;
/** @var $app SlimInit */
// Customize 404
$app->setException(NotFoundException::class, CustomHandler::class);
// Customize 405
$app->setException(MethodNotAllowedException::class, CustomHandler::class);
// Customize default handler (500)
$app->setException(InternalErrorException::class, CustomHandler::class);
// Customize all at once
$app->setException([
NotFoundException::class,
MethodNotAllowedException::class,
InternalErrorException::class
], CustomHandler::class);
To use the default exception handler and just customize the HTTP status code, you can continue to assign a status code instead of a handler, just like in SlimInit 1.x:
use Adepto\Slim3Init\SlimInit;
// … your $app definition
/** @var $app SlimInit */
$app->setException(SomethingNotFoundException::class, 404);
SlimInit 4.1 is also the only version targeting PHP 7.4 specifically.
Currently in development, this version will require PHP 8.1 or higher.
It is still compatible with Psr7 ContainerInterface
. If you specify Adepto\Slim3Init\Container
as the type, you can
make use of ArrayAccess without exceptions, like so:
/** @var $container \Adepto\Slim3Init\Container */
// Get value like normal, with exception if key was not found
$value = $container->get('some-value');
// Get value array-style, with null being returned if key was not found
$value = $container['some-value'];
In their pursuit of being the most generic library on earth, getting Slim's convenience methods to work on top of
ResponseInterface
that doesn't have them and still have IDEs pick that up correctly is a nightmare. So SlimInit
contains its own implementation of those.
You can now also supply middleware that extends Adepto\Slim3Init\Middleware\Middleware
. This gives you some convenience
like always having access to the container and creating responses.
use Adepto\Slim3Init\Middleware\Middleware;
use Adepto\Slim3Init\Request;
use Adepto\Slim3Init\Response;
use Psr\Http\Server\RequestHandlerInterface;
class YourMiddleware extends Middleware {
public function __invoke(Request $request, RequestHandlerInterface $handler) : Response{
return $this->createResponse(404);
}
}
Use HandlerCaller::default(string $baseURL, string $handlerClass, $container = null)
instead.