Core Compatibility Layer

Core Compatibility Layer

The compatibility layer provides safe fallbacks and detection mechanisms for shared hosting environments where certain PHP functions may be disabled.

Overview

Many shared hosting providers disable potentially dangerous PHP functions for security reasons. The compatibility layer helps your application work reliably across different hosting environments.

Common Disabled Functions:

  • exec, shell_exec, system, passthru
  • file_get_contents (URL access)
  • curl_exec
  • chmod, chown
  • ini_set

FunctionChecker

Basic Usage

use Minimal\Core\Compatibility\FunctionChecker;

// Initialize (reads disabled_functions from php.ini)
FunctionChecker::init();

// Check if specific function is available
if (FunctionChecker::isAvailable('exec')) {
    exec('ls -la', $output);
} else {
    // Use alternative approach
    $output = ['Function exec is disabled'];
}

Function Availability Detection

// Check individual functions
$hasExec = FunctionChecker::isAvailable('exec');
$hasShellExec = FunctionChecker::isAvailable('shell_exec');
$hasCurl = FunctionChecker::isAvailable('curl_exec');

// Check capabilities
$canExecuteCommands = FunctionChecker::hasExecCapability();
$canAccessUrls = FunctionChecker::canAccessUrls();
$hasCurlSupport = FunctionChecker::hasCurlSupport();

// Get list of disabled functions
$disabledFunctions = FunctionChecker::getDisabledFunctions();

Safe Command Execution

// Safe execution with automatic fallbacks
$result = FunctionChecker::safeExec('git --version');

if ($result !== null) {
    echo "Git version: " . trim($result);
} else {
    echo "Cannot execute commands on this server";
}

// The safeExec method tries in order:
// 1. exec()
// 2. shell_exec()
// 3. system()
// 4. Returns null if none available

URL Access Detection

// Check if file_get_contents can access URLs
if (FunctionChecker::canAccessUrls()) {
    $content = file_get_contents('https://api.example.com/data');
} else {
    // Use cURL as fallback
    if (FunctionChecker::hasCurlSupport()) {
        $ch = curl_init('https://api.example.com/data');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $content = curl_exec($ch);
        curl_close($ch);
    } else {
        throw new RuntimeException('No method available to access URLs');
    }
}

Practical Examples

class SystemInfoService
{
    public function getPhpVersion(): string
    {
        return PHP_VERSION;
    }
    
    public function getServerInfo(): array
    {
        $info = [
            'php_version' => PHP_VERSION,
            'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
            'disabled_functions' => FunctionChecker::getDisabledFunctions()
        ];
        
        // Try to get additional system info if possible
        if (FunctionChecker::hasExecCapability()) {
            $info['system_info'] = [
                'uname' => FunctionChecker::safeExec('uname -a'),
                'disk_usage' => FunctionChecker::safeExec('df -h'),
                'memory_info' => FunctionChecker::safeExec('free -m')
            ];
        }
        
        return $info;
    }
    
    public function canSendEmails(): bool
    {
        // Check if mail function is available
        return FunctionChecker::isAvailable('mail');
    }
    
    public function canProcessImages(): bool
    {
        // Check if GD extension is loaded
        return extension_loaded('gd') && FunctionChecker::isAvailable('imagecreate');
    }
}

SafeFileOperations

Directory Creation

use Minimal\Core\Compatibility\SafeFileOperations;

// Safe directory creation with fallbacks
$success = SafeFileOperations::createDirectory('var/cache', 0755);

if ($success) {
    echo "Directory created successfully";
} else {
    echo "Failed to create directory";
}

// The method handles:
// - Checking if directory already exists
// - Creating parent directories if needed
// - Setting permissions (where possible)
// - Graceful failure on permission errors

File Writing Operations

// Safe file writing
$content = "Configuration data\nLine 2\nLine 3";
$success = SafeFileOperations::writeFile('var/config/app.conf', $content);

if ($success) {
    echo "File written successfully";
} else {
    echo "Failed to write file";
}

// With file locking
$success = SafeFileOperations::writeFile(
    'var/logs/app.log', 
    $logEntry, 
    FILE_APPEND | LOCK_EX
);

Permission Handling

// Check if we can set permissions
if (SafeFileOperations::canSetPermissions()) {
    SafeFileOperations::setPermissions('var/cache', 0755);
    SafeFileOperations::setPermissions('var/logs/app.log', 0644);
} else {
    // Log that permissions couldn't be set
    error_log('Cannot set file permissions on this server');
}

Practical Usage Examples

class CacheManager
{
    private string $cacheDir;
    
    public function __construct(string $cacheDir = 'var/cache')
    {
        $this->cacheDir = $cacheDir;
        $this->ensureCacheDirectory();
    }
    
    private function ensureCacheDirectory(): void
    {
        if (!SafeFileOperations::createDirectory($this->cacheDir, 0755)) {
            throw new RuntimeException("Cannot create cache directory: {$this->cacheDir}");
        }
    }
    
    public function set(string $key, mixed $value, int $ttl = 3600): bool
    {
        $filename = $this->getCacheFilename($key);
        $data = [
            'value' => $value,
            'expires' => time() + $ttl
        ];
        
        $content = serialize($data);
        return SafeFileOperations::writeFile($filename, $content);
    }
    
    public function get(string $key): mixed
    {
        $filename = $this->getCacheFilename($key);
        
        if (!file_exists($filename)) {
            return null;
        }
        
        $content = file_get_contents($filename);
        if ($content === false) {
            return null;
        }
        
        $data = unserialize($content);
        
        if ($data['expires'] < time()) {
            unlink($filename);
            return null;
        }
        
        return $data['value'];
    }
    
    private function getCacheFilename(string $key): string
    {
        return $this->cacheDir . '/' . md5($key) . '.cache';
    }
}

Environment Detection

Hosting Environment Detection

class HostingEnvironmentDetector
{
    public function getEnvironmentType(): string
    {
        // Check for common shared hosting indicators
        if ($this->isSharedHosting()) {
            return 'shared';
        }
        
        if ($this->isVPS()) {
            return 'vps';
        }
        
        if ($this->isDedicated()) {
            return 'dedicated';
        }
        
        return 'unknown';
    }
    
    private function isSharedHosting(): bool
    {
        $indicators = [
            // Many functions disabled
            count(FunctionChecker::getDisabledFunctions()) > 10,
            
            // Cannot execute commands
            !FunctionChecker::hasExecCapability(),
            
            // Limited file permissions
            !SafeFileOperations::canSetPermissions(),
            
            // Common shared hosting paths
            strpos(__DIR__, '/home/') === 0,
            strpos(__DIR__, '/public_html/') !== false
        ];
        
        return count(array_filter($indicators)) >= 2;
    }
    
    private function isVPS(): bool
    {
        return FunctionChecker::hasExecCapability() && 
               !$this->isSharedHosting();
    }
    
    private function isDedicated(): bool
    {
        // Full access to system functions
        $fullAccess = [
            FunctionChecker::isAvailable('exec'),
            FunctionChecker::isAvailable('shell_exec'),
            FunctionChecker::isAvailable('system'),
            SafeFileOperations::canSetPermissions()
        ];
        
        return count(array_filter($fullAccess)) === count($fullAccess);
    }
    
    public function getCapabilities(): array
    {
        return [
            'exec_capability' => FunctionChecker::hasExecCapability(),
            'url_access' => FunctionChecker::canAccessUrls(),
            'curl_support' => FunctionChecker::hasCurlSupport(),
            'file_permissions' => SafeFileOperations::canSetPermissions(),
            'disabled_functions' => FunctionChecker::getDisabledFunctions(),
            'environment_type' => $this->getEnvironmentType()
        ];
    }
}

Best Practices

Graceful Degradation

class FeatureManager
{
    public function isFeatureAvailable(string $feature): bool
    {
        return match($feature) {
            'git_integration' => FunctionChecker::hasExecCapability(),
            'image_processing' => extension_loaded('gd'),
            'email_sending' => FunctionChecker::isAvailable('mail'),
            'file_uploads' => ini_get('file_uploads') === '1',
            'url_fetching' => FunctionChecker::canAccessUrls() || FunctionChecker::hasCurlSupport(),
            default => false
        };
    }
    
    public function getAlternativeForFeature(string $feature): ?string
    {
        if ($this->isFeatureAvailable($feature)) {
            return null;
        }
        
        return match($feature) {
            'git_integration' => 'Manual file upload for deployments',
            'image_processing' => 'Use external image service',
            'email_sending' => 'Use SMTP with external service',
            'url_fetching' => 'Manual data entry or file upload',
            default => 'Feature not available'
        };
    }
}

Error Handling

class SafeOperationWrapper
{
    public static function executeWithFallback(callable $primary, callable $fallback = null): mixed
    {
        try {
            return $primary();
        } catch (Exception $e) {
            error_log("Primary operation failed: " . $e->getMessage());
            
            if ($fallback) {
                try {
                    return $fallback();
                } catch (Exception $fallbackError) {
                    error_log("Fallback operation also failed: " . $fallbackError->getMessage());
                    throw $e; // Throw original exception
                }
            }
            
            throw $e;
        }
    }
}

// Usage
$result = SafeOperationWrapper::executeWithFallback(
    // Primary: Use exec if available
    fn() => FunctionChecker::safeExec('git rev-parse HEAD'),
    
    // Fallback: Read from file
    fn() => file_exists('.git/HEAD') ? file_get_contents('.git/HEAD') : null
);

Configuration Adaptation

// config/autoload/compatibility.local.php
return [
    'compatibility' => [
        'features' => [
            'exec_commands' => FunctionChecker::hasExecCapability(),
            'url_access' => FunctionChecker::canAccessUrls(),
            'file_permissions' => SafeFileOperations::canSetPermissions(),
        ],
        'fallbacks' => [
            'image_processing' => !extension_loaded('gd') ? 'external_service' : 'local',
            'email_delivery' => !FunctionChecker::isAvailable('mail') ? 'smtp' : 'local',
            'cache_storage' => !SafeFileOperations::canSetPermissions() ? 'database' : 'file',
        ]
    ]
];

Testing

Compatibility Testing

class CompatibilityTest extends TestCase
{
    public function testFunctionAvailability(): void
    {
        FunctionChecker::init();
        
        // Test that checker works
        $this->assertIsBool(FunctionChecker::isAvailable('strlen'));
        $this->assertTrue(FunctionChecker::isAvailable('strlen')); // Should always be available
    }
    
    public function testSafeFileOperations(): void
    {
        $testDir = sys_get_temp_dir() . '/test_' . uniqid();
        
        $this->assertTrue(SafeFileOperations::createDirectory($testDir));
        $this->assertTrue(is_dir($testDir));
        
        $testFile = $testDir . '/test.txt';
        $this->assertTrue(SafeFileOperations::writeFile($testFile, 'test content'));
        $this->assertEquals('test content', file_get_contents($testFile));
        
        // Cleanup
        unlink($testFile);
        rmdir($testDir);
    }
}

Troubleshooting

Common Issues

Functions Unexpectedly Disabled:

// Check current disabled functions
$disabled = FunctionChecker::getDisabledFunctions();
if (in_array('exec', $disabled)) {
    error_log('exec function is disabled by hosting provider');
}

Permission Denied Errors:

// Check if directory is writable
if (!is_writable('var/cache')) {
    error_log('Cache directory is not writable');
    // Try alternative cache location
    $alternativeCache = sys_get_temp_dir() . '/app_cache';
    SafeFileOperations::createDirectory($alternativeCache);
}

URL Access Blocked:

// Test URL access capability
if (!FunctionChecker::canAccessUrls()) {
    error_log('URL access is disabled - allow_url_fopen is off');
    
    if (!FunctionChecker::hasCurlSupport()) {
        error_log('cURL is also not available');
        // Use alternative data source or manual input
    }
}