Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

effectra/cors

A PHP library for handling Cross-Origin Resource Sharing (CORS) in HTTP applications. Built on PSR-7, PSR-15, and PSR-17 standards, it provides a flexible middleware and a standalone service class that can be integrated into any PHP project or framework.

Features

  • ✅ PSR-15 compliant middleware
  • ✅ Wildcard and pattern-based origin matching
  • ✅ Preflight (OPTIONS) request handling
  • ✅ Credential-aware CORS enforcement
  • ✅ Path allowlist and blocklist with wildcard support
  • ✅ Debug mode with error logging
  • ✅ Runtime error collection (X-CORS-Errors header)
  • ✅ Both camelCase and snake_case option key support
  • ✅ Automatic Vary header management

Requirements

  • PHP 8.0+
  • psr/http-message ^2.0
  • psr/http-server-handler ^1.0
  • psr/http-server-middleware ^1.0

Installation

composer require effectra/cors

Quick Start

use Effectra\Cors\CorsService;
use Effectra\Cors\CorsMiddleware;

$corsService = new CorsService([
    'allowedOrigins'     => ['https://example.com'],
    'allowedMethods'     => ['GET', 'POST', 'PUT', 'DELETE'],
    'allowedHeaders'     => ['Content-Type', 'Authorization'],
    'exposedHeaders'     => ['X-Custom-Header'],
    'supportsCredentials'=> false,
    'maxAge'             => 3600,
]);

$middleware = new CorsMiddleware($corsService, [
    'paths'          => ['api/*'],
    'excluded_paths' => ['api/internal/*'],
]);

Classes

CorsService

The core CORS engine. Inspects requests, validates origins, and injects the appropriate CORS response headers.

Constructor

new CorsService(array $options = [])

Optionally pass a configuration array at construction time. The same array can be applied later via setOptions().


Configuration Options

Key (camelCase) Key (snake_case) Type Default Description
allowedOrigins allowed_origins string[] [] Exact origins allowed. Use ['*'] to allow all.
allowedOriginsPatterns allowed_origins_patterns string[] [] Regex-style wildcard patterns, e.g. ['https://*.example.com'].
allowedMethods allowed_methods string[] [] HTTP methods to allow. Use ['*'] to allow all.
allowedHeaders allowed_headers string[] [] Request headers to allow. Use ['*'] to allow all.
exposedHeaders exposed_headers string[] [] Response headers the browser may read.
supportsCredentials supports_credentials bool false Allow cookies/auth. Incompatible with allowedOrigins: ['*'].
maxAge max_age int|null 0 Preflight cache duration in seconds. null omits the header entirely.

Note: Both camelCase and snake_case keys are accepted everywhere.


Public Methods

setOptions(array $options): void

Apply (or re-apply) a configuration array. Validates all values before storing them and throws InvalidArgumentException on invalid input.

$corsService->setOptions([
    'allowedOrigins' => ['https://app.example.com'],
    'allowedMethods' => ['GET', 'POST'],
]);

setDebugMode(bool $debug): self

Enable debug mode. When active, CORS errors are written to the PHP error log and exceptions are re-thrown instead of being swallowed.

$corsService->setDebugMode(true);

isCorsRequest(RequestInterface $request): bool

Returns true when the request carries a non-empty Origin header — the sign of a cross-origin request.

if ($corsService->isCorsRequest($request)) {
    // handle CORS
}

isPreflightRequest(RequestInterface $request): bool

Returns true when the request is an OPTIONS preflight — i.e., the method is OPTIONS and Access-Control-Request-Method is present.

if ($corsService->isPreflightRequest($request)) {
    $response = $corsService->handlePreflightRequest($request);
}

isOriginAllowed(RequestInterface $request): bool

Checks whether the request Origin is permitted by the current configuration. Supports exact matches and wildcard patterns. Also validates the origin URL format.

$allowed = $corsService->isOriginAllowed($request); // true | false

handlePreflightRequest(RequestInterface $request): ResponseInterface

Builds and returns a complete preflight response (204 No Content on success, 403 Forbidden when the origin is not allowed). Delegates header injection to addPreflightRequestHeaders().

$response = $corsService->handlePreflightRequest($request);

addPreflightRequestHeaders(ResponseInterface $response, RequestInterface $request): ResponseInterface

Injects all relevant preflight headers into an existing response:

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Credentials
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • Access-Control-Max-Age

Returns the response unchanged when the origin is not allowed.

$response = $corsService->addPreflightRequestHeaders($response, $request);

addActualRequestHeaders(ResponseInterface $response, RequestInterface $request): ResponseInterface

Injects CORS headers for a real (non-preflight) request:

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Credentials
  • Access-Control-Expose-Headers
$response = $corsService->addActualRequestHeaders($response, $request);

varyHeader(ResponseInterface $response, string $header): ResponseInterface

Appends a value to the Vary response header without creating duplicates. Used internally to ensure correct caching behaviour when the origin or method varies per-request.

$response = $corsService->varyHeader($response, 'Origin');

getErrors(): array

Returns all CORS errors collected during the current request cycle, keyed by error type (origin, method, preflight, configuration, etc.).

$errors = $corsService->getErrors();
// ['origin' => "Origin 'http://evil.com' not allowed"]

hasErrors(): bool

Returns true if any CORS errors have been recorded.

if ($corsService->hasErrors()) {
    // inspect or log $corsService->getErrors()
}

clearErrors(): void

Resets the internal error collection. Useful when reusing the service across multiple requests in long-running processes.

$corsService->clearErrors();

CorsMiddleware

A PSR-15 middleware that wraps CorsService and integrates it into a request/response pipeline.

Constructor

new CorsMiddleware(CorsService $cors, array $config = [])
Config Key Type Default Description
paths / allowed_paths string[] [] Paths CORS is applied to. Empty means all paths.
excluded_paths string[] [] Paths where CORS processing is skipped entirely.
handle_preflight bool true Whether to handle OPTIONS preflight requests automatically.
handle_errors bool true Whether to catch exceptions and expose errors via header.

process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface

The PSR-15 entry point. Execution flow:

  1. Skip — if the request is not a CORS request, or the path is excluded / not in the allowlist, the request passes through to the next handler unchanged.
  2. Preflight — if handle_preflight is true and the request is a preflight OPTIONS, a complete preflight response is returned immediately (without calling the next handler).
  3. Actual request — the next handler is called and CORS headers are added to its response.
  4. Error header — if handle_errors is true and errors were collected, they are serialised into X-CORS-Errors as JSON.
  5. Exception safety — uncaught exceptions return a 500 response with CORS headers when handle_errors is true; otherwise they are re-thrown.

Fluent Setter Methods

All setters return $this for method chaining.

setPaths(array $paths): self

Set (or replace) the list of paths CORS should be applied to.

$middleware->setPaths(['api/*', 'webhooks/*']);

setExcludedPaths(array $paths): self

Set (or replace) the list of paths to exclude from CORS handling.

$middleware->setExcludedPaths(['api/internal/*']);

setHandlePreflight(bool $handle): self

Enable or disable automatic preflight handling.

$middleware->setHandlePreflight(false); // delegate to the next handler

setHandleErrors(bool $handle): self

Enable or disable graceful error handling. When disabled, exceptions propagate normally.

$middleware->setHandleErrors(false);

getCorsService(): CorsService

Returns the underlying CorsService instance — useful for inspecting errors after a request.

$service = $middleware->getCorsService();
$errors  = $service->getErrors();

Path Matching

Both paths and excluded_paths support three matching strategies:

Pattern Example Matches
Exact api/users /api/users only
Wildcard * api/* /api/users, /api/posts, …
Single char ? v?/resource /v1/resource, /v2/resource

Paths can also be scoped to a specific hostname by using the hostname as the array key:

'paths' => [
    'app.example.com' => ['dashboard/*'],
    'api.example.com' => ['v1/*', 'v2/*'],
],

Origin Validation

CorsService validates the origin format before checking allow-lists:

  • Standard URLs are validated with PHP's FILTER_VALIDATE_URL.
  • localhost (with optional port) is accepted without a scheme for local development.
  • Invalid origin formats are rejected and recorded in the error log.

Wildcard patterns in allowedOrigins (e.g. https://*.example.com) are automatically compiled to regex and matched against each incoming request origin.


Credential-Aware Enforcement

When supportsCredentials is true:

  • allowedOrigins must not contain '*' — an InvalidArgumentException is thrown.
  • exposedHeaders must not contain '*'.
  • Access-Control-Allow-Credentials: true is added to every matched response.

Error Handling

Errors are collected internally rather than silently discarded:

$corsService->setDebugMode(true); // also writes to PHP error_log()

// After processing a request:
if ($corsService->hasErrors()) {
    print_r($corsService->getErrors());
}

// Clear for the next request
$corsService->clearErrors();

When handle_errors is enabled on CorsMiddleware, all collected errors are exposed via the X-CORS-Errors response header as a JSON string.


Advanced Examples

Allow all origins (public API)

$corsService = new CorsService([
    'allowedOrigins' => ['*'],
    'allowedMethods' => ['*'],
    'allowedHeaders' => ['*'],
]);

Subdomain wildcard

$corsService = new CorsService([
    'allowedOrigins'      => ['https://*.example.com'],
    'allowedMethods'      => ['GET', 'POST'],
    'allowedHeaders'      => ['Content-Type', 'Authorization'],
    'supportsCredentials' => true,
]);

Restrict CORS to specific routes

$middleware = new CorsMiddleware($corsService, [
    'paths'            => ['api/*'],
    'excluded_paths'   => ['api/health', 'api/internal/*'],
    'handle_preflight' => true,
    'handle_errors'    => true,
]);

Fluent configuration

$middleware = (new CorsMiddleware($corsService))
    ->setPaths(['api/*'])
    ->setExcludedPaths(['api/internal/*'])
    ->setHandlePreflight(true)
    ->setHandleErrors(true);

Standalone usage (without a middleware stack)

use Effectra\Cors\CorsService;

$corsService = new CorsService([
    'allowedOrigins' => ['https://frontend.example.com'],
    'allowedMethods' => ['GET', 'POST'],
    'allowedHeaders' => ['Content-Type'],
]);

if ($corsService->isPreflightRequest($request)) {
    return $corsService->handlePreflightRequest($request);
}

if ($corsService->isCorsRequest($request)) {
    $response = $corsService->addActualRequestHeaders($response, $request);
}

Response Headers Reference

Header Set by Condition
Access-Control-Allow-Origin Preflight & actual request Origin is allowed
Access-Control-Allow-Credentials Preflight & actual request supportsCredentials is true
Access-Control-Allow-Methods Preflight only Origin is allowed
Access-Control-Allow-Headers Preflight only Origin is allowed
Access-Control-Max-Age Preflight only maxAge is not null
Access-Control-Expose-Headers Actual request exposedHeaders is non-empty
Vary Both Dynamic origin or method matching is active
X-CORS-Errors Middleware Errors collected and handle_errors is enabled

Contributing

Contributions are welcome! Please open an issue or submit a pull request. For major changes, open an issue first to discuss what you would like to change.


License

The effectra/cors package is open-sourced software licensed under the MIT license.

About

PHP library for enabling CORS (Cross-Origin Resource Sharing) in HTTP requests/responses. It follows PSR guidelines, and can be easily integrated into PHP projects.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages