简体   繁体   中英

Zend Expressive 2 - REST API (JSON) - Versioning (in header) - FastRoute?

I'm building a Zend Expressive 2 REST API (JSON) and want to directly versioning my API. I use Zend ServiceManager + FastRoute for routing.

I found this useful links for REST API versioning and decide to use versioning inside request header:

Question:

How to implement api versioning; in detail the routing to the middleware action; in zend expressive 2? (using FastRoute)

Accept Header (JSON API with version):

Accept: application/vnd.api+json;version=2

Desired structure (application):

/
 config/
 data/
 public/
 src/
     App/
         V1/
            Action/
                   SomeResource.php        // <- in Version 1
                   ...
         V2/
             Action/
                   SomeResource.php        // <- in Version 2
                   ...
         ...
vendor/
...

My code fragments: (version detection works, but how to route?)

pipeline.php

<?php
// ...
// The error handler should be the first (most outer) middleware to catch
// all Exceptions.
$app->pipe(ErrorHandler::class);
$app->pipe(ContentTypeJsonApiVersioning::class);  // <-- detect version work quite well
$app->pipe(ServerUrlMiddleware::class);

routes.php

<?php
// ...
//
$app->route('/api/some-resource[/{id:\d+}]',
    [
        Auth\Action\AuthAction::class,
        Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware::class,
        App\Action\SomeResourceAction::class
    ],
    ['GET', 'POST', 'PUT', 'DELETE'],
    'api.route.name'
);

ContentTypeJsonApiVersioning.php

<?php

namespace App\Middleware;

use Fig\Http\Message\StatusCodeInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;


/**
 * Middleware to detect accept is JSON API (application/vnd.api+json) and separate version
 */
class ContentTypeJsonApiVersioning
{

    /**
     * @const string
     */
    const EXPECTED_TYPE_JSON_API = 'application/vnd.api+json';


    /**
     * Execute the middleware.
     *
     * @param ServerRequestInterface $request
     * @param ResponseInterface      $response
     * @param callable               $next
     *
     * @throws \InvalidArgumentException
     *
     * @return ResponseInterface
     */
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
    {
        // error: return response with code: 415
        $return       = $response->withStatus(StatusCodeInterface::STATUS_UNSUPPORTED_MEDIA_TYPE);
        $acceptHeader = $request->getHeader('Accept');

        if (isset($acceptHeader[0])) {

            $data = $this->_processAcceptHeader($acceptHeader[0]);

            if (self::EXPECTED_TYPE_JSON_API === $data['accept']) {

                // continue processing
                $return = $next($request->withAttribute('version', $data['version']), $response);
            }
        }

        return $return;
    }


    /**
     * @param string $acceptHeader
     * @return array
     */
    protected function _processAcceptHeader(string $acceptHeader) : array
    {
        // expected: "application/vnd.api+json; version=2.1"
        $data   = \explode(';', $acceptHeader);
        $return = [
            'accept'  => $data[0],
            'version' => '1'
        ];

        // on 2 items, 2nd is version parameter
        if (2 === \count($data)) {

            // split: "version=2.1" to "2.1"
            list(,$return['version']) = \explode('=', \trim($data[1]));
        }

        return $return;
    }

}

What fastroute does is throw a regex at the URL and parse it. So passing a request attribute doesn't work. I can think of a few ways to make it work:

  • Don't use versioning in the header but use it in the url. But since you are specifically asking for it I guess that it's not an option.
  • Rewrite the URL in ContentTypeJsonApiVersioning and update the request before it's passed to the router. So basically rewrite it to /api/v1/resource/id
  • Pass all api requests to APiMiddlewareAction and in there check the version that's passed in the request and load the proper action.

In the last case you could have just one route similar to:

[
    'name'            => 'api',
    'path'            => '/api/{resource:\w+}[/{resourceId:\d+}[/{relation:\w+}[/{relationId:\d+}]]]',
    'middleware'      => ApiMiddleware::class,
    'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
    'options'         => [
        'defaults' => [
        ],
    ],
],

There are probably more solutions.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM