Search by

christianjbrown / cloud-run-function-lib

christianjbrown

A strongly-typed PHP 8.5+ framework for building Google Cloud Run functions HTTP endpoints that return a consistent JSON envelope with built-in header auth, CORS, and CDN cache-control headers.

Package info

github.com/christianjbrown/cloud-run-function-lib-php

pkg:composer/christianjbrown/cloud-run-function-lib

Statistics

Installs: 110

Dependents: 0

Suggesters: 0

Stars: 0

v2.0.1 2026-10-01 11:29 UTC

This package is auto-updated.

Last update: 2026-10-01 13:35:40 UTC


README

CI Coverage Packagist License PHP

A strongly-typed PHP framework for building Google Cloud Run function HTTP endpoints that return a consistent JSON envelope. You write the business logic; the library handles header-based authorization, CORS, CDN cache-control headers, and uniform success/error responses.

It is built around PSR-7: you hand it a ServerRequestInterface and it returns a ResponseInterface. Configuration is read straight from your Cloud Run environment variables.

  • Uniform envelope — every response carries success, timestamp_unix, and timestamp_iso8601, plus data, version (the Cloud Run revision), or error as appropriate.
  • Header authorization — optionally require a header key/value before running your handler.
  • CORS + caching — Access-Control-*, Vary, Cache-Control, and Surrogate-Control headers derived from config.
  • Safe error handling — user-friendly exceptions surface their message; anything else returns a generic error unless DEBUG is on, and is logged to stderr either way.

✔️ Prerequisites

💡 If you're on MacOS and have Homebrew, PHP and Composer will install with brew install composer.

🏗️ Installation

For your composer-enabled project:

composer require christianjbrown/cloud-run-function-lib

💻 Usage

Implement DataProviderInterface with your endpoint's logic — it receives the PSR-7 request and returns an array that becomes the response data:

use ChristianBrown\CloudRunFunction\DataProviderInterface;
use Psr\Http\Message\ServerRequestInterface;

final class MyDataProvider implements DataProviderInterface
{
    /**
     * @return mixed[]
     */
    public function getData(ServerRequestInterface $request): array
    {
        // Your business logic. Throw a UserFriendlyExceptionInterface to return
        // a specific message to the client; any other Throwable is hidden unless DEBUG is on.
        return ['hello' => 'world'];
    }
}

Build the function with CloudRunFunctionFactory, which wires every default, and run the request:

use ChristianBrown\CloudRunFunction\CloudRunFunctionFactory;

$cloudFunction = (new CloudRunFunctionFactory())->createFromEnvironment(new MyDataProvider(), $_ENV);

$response = $cloudFunction->run($request); // ChristianBrown\CloudRunFunction\ResponseInterface (PSR-7)

If you need the config yourself (for example to wrap it in your own config object), build it with the factory's transformer and pass it to create():

$factory = new CloudRunFunctionFactory();
$config = $factory->createConfigTransformer()->transform($_ENV); // FunctionConfigInterface
$cloudFunction = $factory->create(new MyDataProvider(), $config);

Every collaborator (RequestAuthorizerInterface, JsonResponseFactoryInterface, the config appliers) is an interface injected through a constructor, so any of them can be replaced by building CloudRunFunction yourself. A new environment variable is a new FunctionConfigApplierInterface class registered in the transformer's list.

$response is a PSR-7 response ready to emit (e.g. with guzzlehttp/psr7's HTTP factories or your Cloud Run function's runtime).

Environment variables

FunctionConfigTransformer::transform() (via the factory) reads these keys (only K_REVISION is required — Cloud Run sets it automatically):

Variable Purpose
K_REVISION Required. The revision id, surfaced as version in the response.
DEBUG "true" to return raw exception messages instead of a generic error.
REQUIRED_HEADER_KEY / REQUIRED_HEADER_VALUE Require this header on the request, else 401.
REQUIRED_ORIGIN Value for Access-Control-Allow-Origin (enables the Vary header).
USE_CACHE_TTL s-maxage / max-age seconds for successful responses.
USE_BROWSER_CACHE_TTL max-age seconds for browsers only, when that should differ from the CDN's. See below.
USE_CACHE_BUT_REQUEST_TTL stale-while-revalidate seconds.
USE_CACHE_IF_ERROR_TTL stale-if-error seconds.

Letting a purge reach visitors

By default the browser and the CDN are given the same TTL, so USE_CACHE_TTL=3600 produces:

Cache-Control:     s-maxage=3600, max-age=3600, stale-while-revalidate=..., stale-if-error=...
Surrogate-Control: max-age=3600, stale-while-revalidate=..., stale-if-error=...

That max-age is what makes a surrogate-key purge look like it did nothing: the CDN drops its copy, but a visitor who loaded the page in the last hour keeps theirs. Set USE_BROWSER_CACHE_TTL to split the two. With 0:

Cache-Control:     s-maxage=3600, max-age=0, must-revalidate
Surrogate-Control: max-age=3600, stale-while-revalidate=..., stale-if-error=...

The browser now revalidates on every page load, which the CDN answers from its own cache, so a purge is visible immediately. Fastly reads Surrogate-Control in preference to Cache-Control and strips it before the response reaches the client, so the CDN's own TTL and its stale-while-revalidate / stale-if-error resilience are untouched.

Note what is not in that Cache-Control: the stale directives carry no s- prefix, so leaving them there would let a browser serve a body days old of its own accord and undo the point of revalidating. They are emitted on Surrogate-Control only whenever USE_BROWSER_CACHE_TTL is set.

Leave the variable unset and the headers are exactly as they were before it existed.

Response shape

A successful response:

{
    "data": { "hello": "world" },
    "success": true,
    "timestamp_iso8601": "2026-07-15T12:00:00+00:00",
    "timestamp_unix": 1784030400,
    "version": "my-service-00001-abc"
}

An error response omits data and adds error:

{
    "error": "Not authorized",
    "success": false,
    "timestamp_iso8601": "2026-07-15T12:00:00+00:00",
    "timestamp_unix": 1784030400,
    "version": "my-service-00001-abc"
}

🚨 Error handling

Inside your DataProviderInterface::getData(), throwing an exception that implements christianjbrown/user-friendly-exception's UserFriendlyExceptionInterface returns its message to the client (HTTP 500). Any other Throwable returns a generic "An unhandled error occurred" message — unless DEBUG is enabled, in which case the raw message is returned to aid debugging. In both cases the Throwable is written to stderr with error_log(), so the cause is kept in Cloud Logging against the failing request rather than discarded with the response. A failed authorization check short-circuits with "Not authorized" (HTTP 401) before your handler runs.

⬆️ Upgrading to 2.0

CloudRunFunction no longer builds its own collaborators, FunctionConfig is immutable, and the response classes are gone. Everything is built by CloudRunFunctionFactory.

Building the function:

// 1.x
$config = (new FunctionConfigTransformer())->transform($_ENV);
$cloudFunction = new CloudRunFunction($dataProvider, $config);

// 2.0
$cloudFunction = (new CloudRunFunctionFactory())->createFromEnvironment($dataProvider, $_ENV);

// 2.0, when you hold the config yourself
$factory = new CloudRunFunctionFactory();
$config = $factory->createConfigTransformer()->transform($_ENV);
$cloudFunction = $factory->create($dataProvider, $config);

Building a config by hand (tests, for example):

// 1.x
$config = (new FunctionConfig('rev'))->setDebug(true)->setUseCacheTtl(60);

// 2.0: with-ers return a new instance, assign the result
$config = (new FunctionConfig('rev'))->withDebug(true)->withUseCacheTtl(60);

Responses:

// 1.x
new JsonSuccessResponse($config, $data, 200, $origin);
new JsonErrorResponse($config, 'message', 500, $origin);

// 2.0
$responses = new JsonResponseFactory(new ResponseBodyBuilder(), new CorsHeaderBuilder(new AllowOriginResolver()), new CacheHeaderBuilder(), new NativeClock());
$responses->success($config, $data, 200, $origin);
$responses->error($config, 'message', 500, $origin);

new FunctionConfigTransformer() with no arguments no longer works: it needs its list of appliers, which CloudRunFunctionFactory::createConfigTransformer() supplies.

📝 Changelog

Notable changes in each release are listed in CHANGELOG.md.

📄 License

Released under the MIT License.