HyperfアプリケーションにおけるファイルアップロードとURL生成

Hyperfフレームワークを使用したWebアプリケーション開発において、ファイルのアップロード機能や静的リソースの配信、そして動的なURL生成は頻繁に求められる要件です。本稿では、これらの機能を安全かつ効率的に実装するための具体的な方法について解説します。

静的アセットの配信設定

Swooleが直接静的ファイルを処理するように設定することで、NginxなどのWebサーバーを介さずにリソースを配信できます。config/autoload/server.phpファイルに以下の設定を追加してください。

return [
    'settings' => [
        // ... その他の設定
        'document_root' => BASE_PATH . '/public', // 静的ファイルルートディレクトリ
        'enable_static_handler' => true,         // 静的ファイルハンドリングを有効化
    ],
];

これにより、publicディレクトリ以下のファイルが指定されたパスで直接アクセス可能になります。

セキュアなファイルアップロードの実装

ファイルアップロードは、セキュリティ上のリスクを伴うため、慎重な実装が必要です。ここでは、コントローラーとサービスレイヤーを分離し、拡張子およびMIMEタイプの検証を含むアップロード処理の例を示します。

コントローラー層の処理

クライアントからのファイルアップロードリクエストを受け付け、サービス層に処理を委譲します。

use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use App\Service\FileUploadService; // 新しいファイルアップロードサービス

#[AutoController]
class FileController
{
    /**
     * @var RequestInterface
     */
    protected $request;

    /**
     * @var ResponseInterface
     */
    protected $response;

    /**
     * @var FileUploadService
     */
    protected $fileUploadService;

    public function __construct(RequestInterface $request, ResponseInterface $response, FileUploadService $fileUploadService)
    {
        $this->request = $request;
        $this->response = $response;
        $this->fileUploadService = $fileUploadService;
    }

    public function uploadAsset()
    {
        $uploadedFile = $this->request->file('asset'); // 'asset' はフォームのinput nameを想定

        if (empty($uploadedFile)) {
            return $this->response->json(['code' => 400, 'message' => 'ファイルが指定されていません。'], 400);
        }

        try {
            // 許可する拡張子のリストを定義
            $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'docx', 'xlsx', 'zip', 'mp4'];
            $fileInfo = $this->fileUploadService->processUpload($uploadedFile, $allowedExtensions, 'documents');

            return $this->response->json(['code' => 200, 'message' => 'アップロード成功', 'data' => $fileInfo]);
        } catch (\Throwable $e) {
            return $this->response->json(['code' => 500, 'message' => 'アップロード失敗: ' . $e->getMessage()], 500);
        }
    }
}

ファイルアップロードサービスの実装

実際のファイル処理ロジックはサービス層に集約します。これにより、コードの再利用性とテスト容易性が向上します。

use Hyperf\HttpMessage\Upload\UploadedFile;
use Exception;

class FileUploadService
{
    // アップロードベースディレクトリ(例: public/uploads)
    const UPLOADS_ROOT_PATH = BASE_PATH . '/public/uploads';

    /**
     * アップロードファイルを処理し、保存パスを返します。
     *
     * @param UploadedFile $file        アップロードされたファイルオブジェクト
     * @param array $allowedExtensions 許可するファイル拡張子の配列
     * @param string $subDirectory     アップロード先のサブディレクトリ名(例: images, documents)
     * @return array ファイル情報 (ID, URL, ファイル名)
     * @throws Exception ファイル検証または保存に失敗した場合
     */
    public function processUpload(UploadedFile $file, array $allowedExtensions, string $subDirectory = 'others'): array
    {
        $extension = strtolower($file->getExtension());

        // 拡張子チェック
        if (!in_array($extension, $allowedExtensions)) {
            throw new Exception('許可されていないファイル形式です。');
        }

        // 画像ファイルの場合、MIMEタイプとサイズを詳細に検証
        if ($this->isImageExtension($extension)) {
            $this->validateImage($file);
        }

        // 保存パスの構築
        $datePath = date('Y/m/d'); // 例: 2023/10/26
        $targetDirectory = self::UPLOADS_ROOT_PATH . DIRECTORY_SEPARATOR . $subDirectory . DIRECTORY_SEPARATOR . $datePath;

        // ディレクトリが存在しない場合は作成
        if (!is_dir($targetDirectory)) {
            if (!mkdir($targetDirectory, 0755, true)) {
                throw new Exception('アップロードディレクトリの作成に失敗しました。');
            }
        }

        // ユニークなファイル名を生成
        $uniqueFileName = uniqid() . '_' . time() . '.' . $extension;
        $finalFilePath = $targetDirectory . DIRECTORY_SEPARATOR . $uniqueFileName;
        
        // ファイルを移動
        $file->moveTo($finalFilePath);

        // 表示用のURLパスを構築
        $displayPath = '/uploads/' . $subDirectory . '/' . $datePath . '/' . $uniqueFileName;

        return [
            'id'       => uniqid('file_'),
            'url'      => $displayPath,
            'fileName' => $uniqueFileName,
        ];
    }

    /**
     * 指定された拡張子が画像ファイルのものであるか判定します。
     * @param string $extension
     * @return bool
     */
    protected function isImageExtension(string $extension): bool
    {
        return in_array($extension, ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']);
    }

    /**
     * 画像ファイルのMIMEタイプとサイズを検証します。
     * @param UploadedFile $file
     * @throws Exception 検証に失敗した場合
     */
    protected function validateImage(UploadedFile $file): void
    {
        $realPath = $file->getRealPath();
        if (!file_exists($realPath)) {
            throw new Exception('ファイルパスが無効です。');
        }

        $mimeType = mime_content_type($realPath);
        $allowedImageMimeTypes = [
            'image/jpeg',
            'image/png',
            'image/gif',
            'image/bmp',
            'image/webp',
        ];

        if (!in_array($mimeType, $allowedImageMimeTypes)) {
            throw new Exception('許可されていない画像MIMEタイプです。');
        }

        // 画像サイズをチェック
        $imageInfo = @getimagesize($realPath); // エラーを抑制し、結果をチェック
        if ($imageInfo === false || !isset($imageInfo[0]) || !isset($imageInfo[1]) || $imageInfo[0] <= 0 || $imageInfo[1] <= 0) {
            throw new Exception('画像ファイルが破損しているか、無効です。');
        }
    }
}

動的なURL生成ヘルパーの導入

アプリケーション内で完全なURL(Absolute URL)を生成する必要がある場合、リクエスト情報を基に動的に構築するヘルパーが便利です。Hyperfでは、DIコンテナを通じてRequestInterfaceにアクセスできます。

アブストラクトコントローラーのメソッドとして提供

基底コントローラーにURL生成メソッドを追加することで、すべての子コントローラーで利用可能になります。

use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Container\ContainerInterface;

abstract class AbstractBaseController
{
    #[Inject]
    protected ContainerInterface $container;

    #[Inject]
    protected RequestInterface $request;

    #[Inject]
    protected ResponseInterface $response;

    /**
     * 指定されたパスから完全なベースURLを生成します。
     *
     * @param string $path URLに追加するパス
     * @return string 生成された完全なURL
     */
    protected function buildAbsoluteUrl(string $path = ''): string
    {
        $uri = $this->request->getUri();
        $scheme = $uri->getScheme() ?: 'http';
        $host = $uri->getHost() ?: 'localhost';
        $port = $uri->getPort();

        // 標準ポート(80/443)の場合はポート番号を省略
        if ($port && !in_array($port, [80, 443])) {
            $baseUrl = "{$scheme}://{$host}:{$port}";
        } else {
            $baseUrl = "{$scheme}://{$host}";
        }
        
        // パスがスラッシュで始まっていない場合に追加
        if (!empty($path) && $path[0] !== '/') {
            $path = '/' . $path;
        }

        return $baseUrl . $path;
    }
}

グローバルヘルパー関数として提供

DIコンテナからRequestInterfaceインスタンスを取得することで、任意の場所からURLを生成するグローバル関数を定義することも可能です。app/Helper/functions.phpのようなファイルを作成し、composer.jsonにオートロード設定を追加します。

// app/Helper/functions.php
use Hyperf\Utils\ApplicationContext;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Contract\ConfigInterface;

if (!function_exists('resolve_full_url')) {
    /**
     * 指定されたパスから完全なベースURLを解決します。
     *
     * @param string $path URLに追加するパス
     * @return string 生成された完全なURL
     */
    function resolve_full_url(string $path = ''): string
    {
        $container = ApplicationContext::getContainer();
        $request = $container->get(RequestInterface::class);
        $config = $container->get(ConfigInterface::class);

        $uri = $request->getUri();
        $scheme = $uri->getScheme() ?: 'http';
        $host = $uri->getHost() ?: '127.0.0.1'; // デフォルトホスト
        // サーバー設定からポートを取得。Hyperfのデフォルトサーバー設定を想定。
        $port = $uri->getPort() ?: $config->get('server.servers.0.port', 9501); 

        // 標準ポート(80/443)の場合はポート番号を省略
        if ($port && !in_array($port, [80, 443])) {
            $baseUrl = "{$scheme}://{$host}:{$port}";
        } else {
            $baseUrl = "{$scheme}://{$host}";
        }

        // パスがスラッシュで始まっていない場合に追加
        if (!empty($path) && $path[0] !== '/') {
            $path = '/' . $path;
        }

        return $baseUrl . $path;
    }
}

composer.jsonautoloadセクションに以下を追加し、composer dump-autoloadを実行してください。

{
    "autoload": {
        "files": [
            "app/Helper/functions.php"
        ],
        "psr-4": {
            "App\\": "app/"
        }
    }
}

タグ: Hyperf FileUpload StaticAssets URLGeneration PHP

5月15日 19:03 投稿