目次

Slim4 クックブック

Version 4.5.0

y2sunlight 2020-09-23

Slim に戻る

関連記事

本章は以下のサイトの Cook book のセクションを翻訳し若干の補足を加えたのもです。


ルートパターンの末尾のスラッシュ(/)

Slimは、末尾にスラッシュがあるURLパターンを、無いものとは異なるものとして扱います。つまり、/user/user/ は異なるので、異なるコールバックをアタッチできます。

For GET requests a permanent redirect is fine, but for other request methods like POST or PUT the browser will send the second request with the GET method. To avoid this you simply need to remove the trailing slash and pass the manipulated url to the next middleware.

GETリクエストの場合、パーマネント(permanent)リダイレクトは問題ありませんが、POSTやPUTなどの他のリクエストメソッドの場合、ブラウザはGETメソッドを使用して2番目のリクエストを送信します。これを回避するには、単に、末尾のスラッシュを削除し、操作されたURLを次のミドルウェアに渡す必要があります。

/ で終わるすべてのURLを、末尾以外の / に相当するものにリダイレクトまたはリライトする場合、次のミドルウェアを追加できます。

<?php
use Psr\Http\Message\RequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface RequestHandler;
use Slim\Factory\AppFactory;
use Slim\Psr7\Response;
 
require __DIR__ . '/../vendor/autoload.php';
 
$app = AppFactory::create();
 
$app->add(function (Request $request, RequestHandler $handler) {
    $uri = $request->getUri();
    $path = $uri->getPath();
 
    if ($path != '/' && substr($path, -1) == '/') {
        // recursively remove slashes when its more than 1 slash
        $path = rtrim($path, '/');
 
        // permanently redirect paths with a trailing slash
        // to their non-trailing counterpart
        $uri = $uri->withPath($path);
 
        if ($request->getMethod() == 'GET') {
            $response = new Response();
            return $response
                ->withHeader('Location', (string) $uri)
                ->withStatus(301);
        } else {
            $request = $request->withUri($uri);
        }
    }
 
    return $handler->handle($request);
});

あるいは、middlewares/trailing-slash ミドルウェアを検討してください。これにより、すべてのURLに末尾のスラッシュを強制的に追加することもできます。

use Middlewares\TrailingSlash;
 
$app->add(new TrailingSlash(true)); // true adds the trailing slash (false removes it)


現在のルートの取得

アプリケーション内の現在のルートにアクセスする必要がある場合は、着信時の ServerRequestInterface を使用して RouteContext オブジェクトをインスタンス化する必要があります。

そこから、$routeContext→getRoute() を介してルートを取得し、getName() を使用してルートの名前にアクセスするか、getMethods() などを介してこのルートでサポートされているメソッドを取得できます。

注:ルート(route)ハンドラーに到達する前にミドルウェアサイクル中に RouteContext オブジェクトにアクセスする必要がある場合は、エラー処理ミドルウェアの前に、RoutingMiddleware を最も外側のミドルウェアとして追加する必要があります(以下の例を参照)。

例:

<?php
use Slim\Exception\HttpNotFoundException;
use Slim\Factory\AppFactory;
use Slim\Routing\RouteContext;
 
require __DIR__ . '/../vendor/autoload.php';
 
$app = AppFactory::create();
 
// Via this middleware you could access the route and routing results from the resolved route
$app->add(function (Request $request, RequestHandler $handler) {
    $routeContext = RouteContext::fromRequest($request);
    $route = $routeContext->getRoute();
 
    // return NotFound for non existent route
    if (empty($route)) {
        throw new HttpNotFoundException($request);
    }
 
    $name = $route->getName();
    $groups = $route->getGroups();
    $methods = $route->getMethods();
    $arguments = $route->getArguments();
 
    // ... do something with the data ...
 
    return $handler->handle($request);
});
 
// The RoutingMiddleware should be added after our CORS middleware so routing is performed first
$app->addRoutingMiddleware();
 
// The ErrorMiddleware should always be the outermost middleware
$app->addErrorMiddleware(true, true, true);
 
// ...
 
$app->run();


CORSの設定

CORS - クロス オリジンリソース シェアリング(オリジン間リソース共有)

シンプル ソリューション

単純なCORSリクエストの場合、サーバーはレスポンスに次のヘッダーを追加するだけで済みます。

Access-Control-Allow-Origin: <domain>, ... 

The following code should enable lazy CORS.

次のコードは、Lazy CORS を有効にする必要があります。

$app->options('/{routes:.+}', function ($request, $response, $args) {
    return $response;
});
 
$app->add(function ($request, $handler) {
    $response = $handler->handle($request);
    return $response
            ->withHeader('Access-Control-Allow-Origin', 'http://mysite')
            ->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization')
            ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
});

最後のルートとして次のルートを追加します:

<?php
use Slim\Exception\HttpNotFoundException;
 
/**
 * Catch-all route to serve a 404 Not Found page if none of the routes match
 * NOTE: make sure this route is defined last
 */
$app->map(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], '/{routes:.+}', function ($request, $response) {
    throw new HttpNotFoundException($request);
});


Access-Control-Allow-Methods

次のミドルウェアを使用して、Slimのルーターにクエリを実行し、特定のパターンが実装するメソッドのリストを取得できます。

これが完全なサンプルアプリケーションです:

<?php
 
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Factory\AppFactory;
use Slim\Routing\RouteCollectorProxy;
use Slim\Routing\RouteContext;
 
require_once __DIR__ . '/../vendor/autoload.php';
 
$app = AppFactory::create();
 
$app->addBodyParsingMiddleware();
 
// This middleware will append the response header Access-Control-Allow-Methods with all allowed methods
$app->add(function (Request $request, RequestHandlerInterface $handler): Response {
    $routeContext = RouteContext::fromRequest($request);
    $routingResults = $routeContext->getRoutingResults();
    $methods = $routingResults->getAllowedMethods();
    $requestHeaders = $request->getHeaderLine('Access-Control-Request-Headers');
 
    $response = $handler->handle($request);
 
    $response = $response->withHeader('Access-Control-Allow-Origin', '*');
    $response = $response->withHeader('Access-Control-Allow-Methods', implode(',', $methods));
    $response = $response->withHeader('Access-Control-Allow-Headers', $requestHeaders);
 
    // Optional: Allow Ajax CORS requests with Authorization header
    // $response = $response->withHeader('Access-Control-Allow-Credentials', 'true');
 
    return $response;
});
 
// The RoutingMiddleware should be added after our CORS middleware so routing is performed first
$app->addRoutingMiddleware();
 
// The routes
$app->get('/api/v0/users', function (Request $request, Response $response): Response {
    $response->getBody()->write('List all users');
 
    return $response;
});
 
$app->get('/api/v0/users/{id}', function (Request $request, Response $response, array $arguments): Response {
    $userId = (int)$arguments['id'];
    $response->getBody()->write(sprintf('Get user: %s', $userId));
 
    return $response;
});
 
$app->post('/api/v0/users', function (Request $request, Response $response): Response {
    // Retrieve the JSON data
    $parameters = (array)$request->getParsedBody();
 
    $response->getBody()->write('Create user');
 
    return $response;
});
 
$app->delete('/api/v0/users/{id}', function (Request $request, Response $response, array $arguments): Response {
    $userId = (int)$arguments['id'];
    $response->getBody()->write(sprintf('Delete user: %s', $userId));
 
    return $response;
});
 
// Allow preflight requests
// Due to the behaviour of browsers when sending a request,
// you must add the OPTIONS method. Read about preflight.
$app->options('/api/v0/users', function (Request $request, Response $response): Response {
    // Do nothing here. Just return the response.
    return $response;
});
 
// Allow additional preflight requests
$app->options('/api/v0/users/{id}', function (Request $request, Response $response): Response {
    return $response;
});
 
// Using groups
$app->group('/api/v0/users/{id:[0-9]+}', function (RouteCollectorProxy $group) {
    $group->put('', function (Request $request, Response $response, array $arguments): Response {
        // Your code here...
        $userId = (int)$arguments['id'];
        $response->getBody()->write(sprintf('Put user: %s', $userId));
 
        return $response;
    });
 
    $group->patch('', function (Request $request, Response $response, array $arguments): Response {
        $userId = (int)$arguments['id'];
        $response->getBody()->write(sprintf('Patch user: %s', $userId));
 
        return $response;
    });
 
    // Allow preflight requests
    $group->options('', function (Request $request, Response $response): Response {
        return $response;
    });
});
 
$app->run();


Access-Control-Allow-Credentials

リクエストに資格情報(Cookie、承認ヘッダー、またはTLSクライアント証明書)が含まれている場合は、レスポンスオブジェクトに Access-Control-Allow-Credentials ヘッダーを追加する必要がある場合があります。

$response = $response->withHeader('Access-Control-Allow-Credentials', 'true');


POSTフォームを使ったファイルのアップロード

POSTリクエストのフォームを使用してアップロードされたファイルは、Requestメソッド getUploadedFiles() を使用して取得できます。

POSTリクエストを使用してファイルをアップロードする場合は、ファイルアップロードフォームに属性 enctype=“multipart/form-data” があることを確認してください。そうでない場合、getUploadedFiles() は空の配列を返します。

同じ input name に対して複数のファイルがアップロードされる場合は、HTMLのinput nameの後に鍵括弧( [] )を追加します。そうでない場合、getUploadedFiles() によってinput nameに対してアップロードされたファイルが1つだけ返されます。

以下は、単一ファイルと複数ファイルの両方のアップロードを含むHTMLフォームの例です。

<!-- make sure the attribute enctype is set to multipart/form-data -->
<form method="post" enctype="multipart/form-data">
    <!-- upload of a single file -->
    <p>
        <label>Add file (single): </label><br/>
        <input type="file" name="example1"/>
    </p>
 
    <!-- multiple input fields for the same input name, use brackets -->
    <p>
        <label>Add files (up to 2): </label><br/>
        <input type="file" name="example2[]"/><br/>
        <input type="file" name="example2[]"/>
    </p>
 
    <!-- one file input field that allows multiple files to be uploaded, use brackets -->
    <p>
        <label>Add files (multiple): </label><br/>
        <input type="file" name="example3[]" multiple="multiple"/>
    </p>
 
    <p>
        <input type="submit"/>
    </p>
</form>

アップロードされたファイルは、moveTo メソッドを使用してディレクトリに移動できます。以下は、上記のHTMLフォームのアップロードされたファイルを処理するサンプルアプリケーションです。

<?php
 
use DI\ContainerBuilder;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use Slim\Factory\AppFactory;
 
require __DIR__ . '/../vendor/autoload.php';
 
$containerBuilder = new ContainerBuilder();
$container = $containerBuilder->build();
 
$container->set('upload_directory', __DIR__ . '/uploads');
 
AppFactory::setContainer($container);
$app = AppFactory::create();
 
$app->post('/', function (ServerRequestInterface $request, ResponseInterface $response) {
    $directory = $this->get('upload_directory');
    $uploadedFiles = $request->getUploadedFiles();
 
    // handle single input with single file upload
    $uploadedFile = $uploadedFiles['example1'];
    if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
        $filename = moveUploadedFile($directory, $uploadedFile);
        $response->getBody()->write('Uploaded: ' . $filename . '<br/>');
    }
 
    // handle multiple inputs with the same key
    foreach ($uploadedFiles['example2'] as $uploadedFile) {
        if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
            $filename = moveUploadedFile($directory, $uploadedFile);
            $response->getBody()->write('Uploaded: ' . $filename . '<br/>');
        }
    }
 
    // handle single input with multiple file uploads
    foreach ($uploadedFiles['example3'] as $uploadedFile) {
        if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
            $filename = moveUploadedFile($directory, $uploadedFile);
            $response->getBody()->write('Uploaded: ' . $filename . '<br/>');
        }
    }
 
    return $response;
});
 
/**
 * Moves the uploaded file to the upload directory and assigns it a unique name
 * to avoid overwriting an existing uploaded file.
 *
 * @param string $directory The directory to which the file is moved
 * @param UploadedFileInterface $uploadedFile The file uploaded file to move
 *
 * @return string The filename of moved file
 */
function moveUploadedFile(string $directory, UploadedFileInterface $uploadedFile)
{
    $extension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION);
 
    // see http://php.net/manual/en/function.random-bytes.php
    $basename = bin2hex(random_bytes(8));
    $filename = sprintf('%s.%0.8s', $basename, $extension);
 
    $uploadedFile->moveTo($directory . DIRECTORY_SEPARATOR . $filename);
 
    return $filename;
}
 
$app->run();