Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 246 additions & 27 deletions src/Phaseolies/Helpers/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -400,49 +400,268 @@ function base_path(string $path = ''): string

if (!function_exists('base_url')) {
/**
* Get the base URL of the application.
* Get the base URL of the application dynamically.
*
* For multi-tenant apps, this detects the current subdomain automatically:
* - acme.app.com → https://acme.app.com
* - globex.app.com → https://globex.app.com
* - app.com → https://app.com
*
* @param string $path
* @return string
*/
function base_url(string $path = ''): string
{
static $baseUrl = null;
static $currentHost = null;

$requestHost = $_SERVER['HTTP_HOST'] ?? null;

// Return cached version if available
// and not forcing a new check
if ($baseUrl !== null && !defined('FORCE_BASE_URL_REFRESH')) {
return $baseUrl . ($path ? '/' . ltrim($path, '/') : '');
if ($baseUrl === null || $currentHost !== $requestHost) {
$currentHost = $requestHost;
$baseUrl = determine_base_url();
}

return $baseUrl . ($path ? '/' . ltrim($path, '/') : '');
}
}

if (!function_exists('determine_base_url')) {
/**
* Determine the base URL based on the current request context.
*
* @return string
*/
function determine_base_url(): string
{
if (PHP_SAPI === 'cli' || defined('STDIN')) {
$appUrl = getenv('APP_URL') ?: 'http://localhost';
$baseUrl = rtrim($appUrl, '/');
} else {
// Modern HTTPS detection
$isHttps = (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off')
|| ($_SERVER['SERVER_PORT'] ?? null) == 443
|| ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null) === 'https'
|| ($_SERVER['HTTP_CF_VISITOR'] ?? null) === '{"scheme":"https"}'; // Cloudflare support

$scheme = $isHttps ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$baseUrl = $scheme . '://' . $host;

// Handle subdirectory installations
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
if ($scriptName) {
$baseDir = str_replace(basename($scriptName), '', $scriptName);
$baseUrl .= rtrim($baseDir, '/');
}
return rtrim($appUrl, '/');
}

// Allow environment override
$scheme = detect_scheme();
$host = detect_host();
$baseDir = detect_base_directory();

return $scheme . '://' . $host . rtrim($baseDir, '/');
}
}

if (!function_exists('detect_scheme')) {
/**
* Detect the current request scheme (http or https).
*
* Handles:
* - Standard HTTPS detection
* - Reverse proxies (X-Forwarded-Proto)
* - Load balancers
* - Cloudflare
* - AWS ELB
*
* @return string 'https' or 'http'
*/
function detect_scheme(): string
{
// Force HTTPS via environment variable
if (getenv('FORCE_HTTPS') === 'true') {
$baseUrl = str_replace('http://', 'https://', $baseUrl);
return 'https';
}

return $baseUrl . ($path ? '/' . ltrim($path, '/') : '');
// Standard HTTPS detection
if (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') {
return 'https';
}

// HTTPS via port
if (($_SERVER['SERVER_PORT'] ?? null) == 443) {
return 'https';
}

// Behind reverse proxy or load balancer
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
return strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https' ? 'https' : 'http';
}

// Cloudflare
if (isset($_SERVER['HTTP_CF_VISITOR'])) {
$cfVisitor = json_decode($_SERVER['HTTP_CF_VISITOR'], true);
if (isset($cfVisitor['scheme']) && $cfVisitor['scheme'] === 'https') {
return 'https';
}
}

// AWS ELB
if (isset($_SERVER['HTTP_X_FORWARDED_PORT']) && $_SERVER['HTTP_X_FORWARDED_PORT'] == 443) {
return 'https';
}

return 'http';
}
}

if (!function_exists('detect_host')) {
/**
* Detect the current request host (domain + port if non-standard).
*
* This is THE KEY for multi-tenant routing:
* - Reads HTTP_HOST directly from the request
* - Includes port for local development (localhost:8000)
* - Validates against trusted hosts if configured
*
* Examples:
* - acme.app.com → 'acme.app.com'
* - globex.app.com → 'globex.app.com'
* - localhost:8000 → 'localhost:8000'
*
* @return string The detected host
*/
function detect_host(): string
{
// Priority 1: HTTP_HOST (includes port for non-standard ports)
if (isset($_SERVER['HTTP_HOST'])) {
$host = $_SERVER['HTTP_HOST'];

// Strip port if it's standard (80 for HTTP, 443 for HTTPS)
$scheme = detect_scheme();
$standardPort = $scheme === 'https' ? 443 : 80;
$currentPort = $_SERVER['SERVER_PORT'] ?? $standardPort;

// If port is explicitly in HTTP_HOST and it's standard, strip it
if (($scheme === 'https' && str_ends_with($host, ':443')) ||
($scheme === 'http' && str_ends_with($host, ':80'))
) {
$host = preg_replace('/:(443|80)$/', '', $host);
}

return $host;
}

// Priority 2: SERVER_NAME (fallback without port)
if (isset($_SERVER['SERVER_NAME'])) {
return $_SERVER['SERVER_NAME'];
}

// Priority 3: SERVER_ADDR (IP address fallback)
if (isset($_SERVER['SERVER_ADDR'])) {
return $_SERVER['SERVER_ADDR'];
}

// Ultimate fallback
return 'localhost';
}
}

if (!function_exists('detect_base_directory')) {
/**
* Detect if the application is installed in a subdirectory.
*
* Examples:
* - http://example.com/myapp/public/index.php → '/myapp'
* - http://example.com/public/index.php → ''
*
* @return string
*/
function detect_base_directory(): string
{
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';

if (empty($scriptName)) {
return '';
}

// Remove /public/index.php or /index.php from script name
$baseDir = str_replace(['\\', '/public/index.php', '/index.php'], ['/', '', ''], $scriptName);

return $baseDir;
}
}

if (!function_exists('current_url')) {
/**
* Get the current full URL including query string.
*
* Example: https://acme.app.com/dashboard?page=2
*
* @return string The current complete URL
*/
function current_url(): string
{
$scheme = detect_scheme();
$host = detect_host();
$uri = $_SERVER['REQUEST_URI'] ?? '/';

return $scheme . '://' . $host . $uri;
}
}

if (!function_exists('current_domain')) {
/**
* Get the current domain without scheme or path.
*
* Examples:
* - https://acme.app.com/dashboard → 'acme.app.com'
* - http://localhost:8000/test → 'localhost:8000'
*
* @return string
*/
function current_domain(): string
{
return detect_host();
}
}

if (!function_exists('is_subdomain')) {
/**
* Check if the current request is on a subdomain.
*
* @param string
* @return bool
*/
function is_subdomain(string $baseDomain): bool
{
$currentHost = detect_host();

// Strip port if present
$currentHost = preg_replace('/:\d+$/', '', $currentHost);
$baseDomain = preg_replace('/:\d+$/', '', $baseDomain);

// If current host is longer and ends with base domain, it's a subdomain
if ($currentHost === $baseDomain) {
return false;
}

return str_ends_with($currentHost, '.' . $baseDomain);
}
}

if (!function_exists('extract_subdomain')) {
/**
* Extract the subdomain from the current host.
*
* Examples:
* - acme.app.com (base: app.com) → 'acme'
* - api.staging.app.com (base: app.com) → 'api.staging'
* - app.com (base: app.com) → null
*
* @param string $baseDomain
* @return string|null
*/
function extract_subdomain(string $baseDomain): ?string
{
$currentHost = detect_host();

// Strip port if present
$currentHost = preg_replace('/:\d+$/', '', $currentHost);
$baseDomain = preg_replace('/:\d+$/', '', $baseDomain);

if (!str_ends_with($currentHost, '.' . $baseDomain)) {
return null;
}

// Extract subdomain by removing base domain
$subdomain = str_replace('.' . $baseDomain, '', $currentHost);

return $subdomain ?: null;
}
}

Expand Down Expand Up @@ -903,4 +1122,4 @@ function throttle(): RateLimiter
{
return app(RateLimiter::class);
}
}
}
4 changes: 2 additions & 2 deletions src/Phaseolies/Support/UrlGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class UrlGenerator
*/
public function __construct(?string $baseUrl = null, ?bool $secure = null)
{
$this->baseUrl = $baseUrl ? rtrim($baseUrl, '/') : $this->determineBaseUrl();
$this->baseUrl = $this->determineBaseUrl() ?? $baseUrl;

$this->secure = $secure ?? $this->isSecureRequest();
}
Expand Down Expand Up @@ -87,7 +87,7 @@ protected function isSecureRequest(): bool
*/
protected function determineBaseUrl(): string
{
return \base_url();
return base_url();
}

/**
Expand Down
Loading
Loading