使用騰訊雲函式服務執行 laravel 9

荒街!發表於2022-04-20

摸魚過程中偶然發現了騰訊雲出了 SCF 函式服務(可能早就有了,但我不知道:relaxed:),經過一些研究探索,成功執行了 laravel 框架,搭配 Api閘道器服務TDSQL-C資料庫,幾乎可實現免費搭建小型網站

計費詳情

1.建立專案程式碼

使用 Composer 安裝

composer create-project laravel/laravel

因雲函式服務不支援在專案路徑寫入檔案,故將各處寫入檔案定位至 /tmp,編輯 .env 檔案,在底部新增

# 設定模板快取路徑
VIEW_COMPILED_PATH=/tmp
# 設定應用快取路徑
APP_STORAGE=/tmp
# 設定日誌輸出至 stderr
LOG_CHANNEL=stderr
# 設定 session 以記憶體形式儲存 或自行修改使用 mysql 儲存
SESSION_DRIVER=array

在專案根目錄下建立處理檔案 handler.php,內容如下

<?php

use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\HeaderBag;

define('LARAVEL_START', microtime(true));
define('TEXT_REG', '#\.html.*|\.js.*|\.css.*|\.html.*#');
define('BINARY_REG', '#\.ttf.*|\.woff.*|\.woff2.*|\.gif.*|\.jpg.*|\.png.*|\.jepg.*|\.swf.*|\.bmp.*|\.ico.*#');

/**
 * 靜態檔案處理
 */
function handlerStatic($path, $isBase64Encoded)
{
    $filename = __DIR__ . "/public" . $path;
    if (!file_exists($filename)) {
        return [
            "isBase64Encoded" => false,
            "statusCode" => 404,
            "headers" => [
                'Content-Type' => '',
            ],
            "body" => "404 Not Found",
        ];
    }
    $handle = fopen($filename, "r");
    $contents = fread($handle, filesize($filename));
    fclose($handle);

    $base64Encode = false;
    $headers = [
        'Content-Type' => '',
        'Cache-Control' => "max-age=8640000",
        'Accept-Ranges' => 'bytes',
    ];
    $body = $contents;
    if ($isBase64Encoded || preg_match(BINARY_REG, $path)) {
        $base64Encode = true;
        $headers = [
            'Content-Type' => '',
            'Cache-Control' => "max-age=86400",
        ];
        $body = base64_encode($contents);
    }
    return [
        "isBase64Encoded" => $base64Encode,
        "statusCode" => 200,
        "headers" => $headers,
        "body" => $body,
    ];
}

function initEnvironment($isBase64Encoded)
{
    $envName = '';
    if (file_exists(__DIR__ . "/.env")) {
        $envName = '.env';
    } elseif (file_exists(__DIR__ . "/.env.production")) {
        $envName = '.env.production';
    } elseif (file_exists(__DIR__ . "/.env.local")) {
        $envName = ".env.local";
    }
    if (!$envName) {
        return [
            'isBase64Encoded' => $isBase64Encoded,
            'statusCode' => 500,
            'headers' => [
                'Content-Type' => 'application/json'
            ],
            'body' => $isBase64Encoded ? base64_encode([
                'error' => "Dotenv config file not exist"
            ]) : [
                'error' => "Dotenv config file not exist"
            ]
        ];
    }

    $dotenv = Dotenv\Dotenv::createImmutable(__DIR__, $envName);
    $dotenv->load();
}

function decodeFormData($rawData)
{
    $files = array();
    $data = array();
    $boundary = substr($rawData, 0, strpos($rawData, "\r\n"));

    $parts = array_slice(explode($boundary, $rawData), 1);
    foreach ($parts as $part) {
        if ($part == "--\r\n") {
            break;
        }

        $part = ltrim($part, "\r\n");
        list($rawHeaders, $content) = explode("\r\n\r\n", $part, 2);
        $content = substr($content, 0, strlen($content) - 2);
        // 獲取請求頭資訊
        $rawHeaders = explode("\r\n", $rawHeaders);
        $headers = array();
        foreach ($rawHeaders as $header) {
            list($name, $value) = explode(':', $header);
            $headers[strtolower($name)] = ltrim($value, ' ');
        }

        if (isset($headers['content-disposition'])) {
            $filename = null;
            preg_match('/^form-data; *name="([^"]+)"(; *filename="([^"]+)")?/', $headers['content-disposition'], $matches);
            $fieldName = $matches[1];
            $fileName = (isset($matches[3]) ? $matches[3] : null);

            // If we have a file, save it. Otherwise, save the data.
            if ($fileName !== null) {
                $localFileName = tempnam('/tmp', 'sls');
                file_put_contents($localFileName, $content);

                $arr = array(
                    'name' => $fileName,
                    'type' => $headers['content-type'],
                    'tmp_name' => $localFileName,
                    'error' => 0,
                    'size' => filesize($localFileName)
                );

                if (substr($fieldName, -2, 2) == '[]') {
                    $fieldName = substr($fieldName, 0, strlen($fieldName) - 2);
                }

                if (array_key_exists($fieldName, $files)) {
                    array_push($files[$fieldName], $arr);
                } else {
                    $files[$fieldName] = $arr;
                }

                // register a shutdown function to cleanup the temporary file
                register_shutdown_function(function () use ($localFileName) {
                    unlink($localFileName);
                });
            } else {
                parse_str($fieldName . '=__INPUT__', $parsedInput);
                $dottedInput = arrayDot($parsedInput);
                $targetInput = arrayAdd([], array_keys($dottedInput)[0], $content);

                $data = array_merge_recursive($data, $targetInput);
            }
        }
    }
    return (object)([
        'data' => $data,
        'files' => $files
    ]);
}

function arrayGet($array, $key, $default = null)
{
    if (is_null($key)) {
        return $array;
    }

    if (array_key_exists($key, $array)) {
        return $array[$key];
    }

    if (strpos($key, '.') === false) {
        return $array[$key] ?? value($default);
    }

    foreach (explode('.', $key) as $segment) {
        $array = $array[$segment];
    }

    return $array;
}

function arrayAdd($array, $key, $value)
{
    if (is_null(arrayGet($array, $key))) {
        arraySet($array, $key, $value);
    }

    return $array;
}

function arraySet(&$array, $key, $value)
{
    if (is_null($key)) {
        return $array = $value;
    }

    $keys = explode('.', $key);

    foreach ($keys as $i => $key) {
        if (count($keys) === 1) {
            break;
        }

        unset($keys[$i]);

        if (!isset($array[$key]) || !is_array($array[$key])) {
            $array[$key] = [];
        }

        $array = &$array[$key];
    }

    $array[array_shift($keys)] = $value;

    return $array;
}

function arrayDot($array, $prepend = '')
{
    $results = [];

    foreach ($array as $key => $value) {
        if (is_array($value) && !empty($value)) {
            $results = array_merge($results, dot($value, $prepend . $key . '.'));
        } else {
            $results[$prepend . $key] = $value;
        }
    }

    return $results;
}

function getHeadersContentType($headers)
{
    if (isset($headers['Content-Type'])) {
        return $headers['Content-Type'];
    } else if (isset($headers['content-type'])) {
        return $headers['content-type'];
    }
    return '';
}

function handler($event, $context)
{
    require __DIR__ . '/vendor/autoload.php';

    $isBase64Encoded = $event->isBase64Encoded;


    initEnvironment($isBase64Encoded);

    $app = require __DIR__ . '/bootstrap/app.php';

    // change storage path to APP_STORAGE in dotenv
    $app->useStoragePath(env('APP_STORAGE', base_path() . '/storage'));


    // 獲取請求路徑
    $path = str_replace("//", "/", $event->path);

    if (preg_match(TEXT_REG, $path) || preg_match(BINARY_REG, $path)) {
        return handlerStatic($path, $isBase64Encoded);
    }

    // 處理請求頭
    $headers = $event->headers ?? [];
    $headers = json_decode(json_encode($headers), true);

    // 處理請求資料
    $data = [];
    $rawBody = $event->body ?? null;
    if ($event->httpMethod === 'GET') {
        $data = !empty($event->queryString) ? $event->queryString : [];
    } else {
        if ($isBase64Encoded) {
            $rawBody = base64_decode($rawBody);
        }
        $contentType = getHeadersContentType($headers);
        if (preg_match('/multipart\/form-data/', $contentType)) {
            $requestData = !empty($rawBody) ? decodeFormData($rawBody) : [];
            $data = $requestData->data;
            $files = $requestData->files;
        } else if (preg_match('/application\/x-www-form-urlencoded/', $contentType)) {
            if (!empty($rawBody)) {
                mb_parse_str($rawBody, $data);
            }
        } else {
            $data = !empty($rawBody) ? json_decode($rawBody, true) : [];
        }
    }

    // 將請求交給 laravel 處理
    $kernel = $app->make(Kernel::class);

    var_dump($path, $event->httpMethod);

    $request = Request::create($path, $event->httpMethod, (array)$data, [], [], $headers, $rawBody);
    $request->headers = new HeaderBag($headers);
    if (!empty($files)) {
        $request->files->add($files);
    }


    $response = $kernel->handle($request);

    // 處理返回內容
    $body = $response->getContent();
    $headers = $response->headers->all();
    $response_headers = [];
    foreach ($headers as $k => $header) {
        if (is_string($header)) {
            $response_headers[$k] = $header;
        } elseif (is_array($header)) {
            $response_headers[$k] = implode(';', $header);
        }
    }

    return [
        'isBase64Encoded' => $isBase64Encoded,
        'statusCode' => $response->getStatusCode() ?? 200,
        'headers' => $response_headers,
        'body' => $isBase64Encoded ? base64_encode($body) : $body
    ];
}

2.建立 SCF 函式

  1. 登入騰訊雲控制檯,搜尋並開啟 函式服務

函式服務截圖

  1. 點選新建,選擇從頭開始,函式型別選擇 事件函式 ,執行環境選擇 php 8.0
  2. 使用本地上傳 zip 包方式,將原生程式碼上傳,執行方法填寫 handler.handler,即上文建立的 handler.php 中的 handler 方法

新建函式截圖

下方高階配置按需要調整即可,(注意:函式執行日誌會記錄到騰訊雲 CLS,該服務為收費服務,有免費額度,具體可參考配置頁面下方說明),填寫完成後點選完成生成函式

3.建立 api 閘道器服務

該服務月呼叫量小於等於 100 萬次不收費,超過 100 萬次後,每萬次 0.06 元

  1. 開啟騰訊雲控制檯, 搜尋並進入 Api 閘道器 控制檯,點選新建, 共享性, 直接提交即可
  2. 在剛建立的 Api 閘道器 上點選配置管理, 點選管理 Api ,點選新建
  3. API名稱隨意填寫, 路徑填寫 /, 請求方法選擇 any , 點選下一步
  4. 在後端配置中選擇 雲函式SCF ,選擇剛建立的函式,並勾選響應整合

後端配置截圖

  1. 點選下一步,直接提交儲存即可,根據提示點選立即釋出
  2. 點選基礎配置,複製訪問地址

訪問地址

  1. 瀏覽器開啟測試,返回如圖

返回截圖

  1. 根據個人需要在 自定義域名 中繫結個人域名即可

建立 TDSQL-C資料庫

TDSQL-C 100% 相容 mysql, 該服務可選擇按量付費, 不使用不計費模式

  1. 開啟騰訊雲控制檯,搜尋並進入 TDSQL-C MySQL 版
  2. 點選新建,計費方式選擇 Serverless, 之後根據個人需要調整各項配置即可

提示

  1. 該服務無法使用檔案方式儲存 session,可選擇使用 redis 或 mysql 儲存 session
  2. 由於 api 閘道器限制,上傳檔案最大支援 2M,如有上傳需求,需在建立 api 閘道器勾選 base64 編碼,建議使用第三方儲存
  3. 如不需函式執行日誌,可在 CLS 控制檯直接刪除系統建立的日誌集

另外本人準備搭建一個個人部落格網站,用於記錄日常,有沒有小夥伴的部落格可以借(抄)鑑(襲)一下 :sunglasses:

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章