# PHP Extension Fallback Patterns

**Last Updated:** 2026-01-15

## Overview

This guide provides standardized fallback patterns for common PHP extensions. Use these patterns to ensure your code works even when extensions are not available in production.

## Pattern Structure

All fallback patterns follow this structure:

1. **Check availability** - Use `function_exists()` or `extension_loaded()`
2. **Use extension if available** - Execute extension-specific code
3. **Provide fallback** - Execute alternative code if extension missing
4. **Log fallback usage** - Log when fallback is used for monitoring

## mbstring Fallback Patterns

### Pattern 1: String Length

**Use case**: Get UTF-8 string length

```php
if (function_exists('_has_mbstring') && _has_mbstring()) {
    $length = mb_strlen($text, 'UTF-8');
} elseif (function_exists('mb_strlen')) {
    $length = mb_strlen($text, 'UTF-8');
} elseif (function_exists('_mb_strlen_fallback')) {
    $length = _mb_strlen_fallback($text, 'UTF-8');
} else {
    $length = strlen($text);
}
```

**Simplified version:**

```php
$length = function_exists('mb_strlen') ? mb_strlen($text, 'UTF-8') : strlen($text);
```

### Pattern 2: Substring Extraction

**Use case**: Extract substring from UTF-8 string

```php
if (function_exists('_has_mbstring') && _has_mbstring()) {
    $substring = mb_substr($text, $start, $length, 'UTF-8');
} elseif (function_exists('mb_substr')) {
    $substring = mb_substr($text, $start, $length, 'UTF-8');
} elseif (function_exists('_mb_substr_fallback')) {
    $substring = _mb_substr_fallback($text, $start, $length, 'UTF-8');
} else {
    $substring = substr($text, $start, $length);
}
```

**Simplified version:**

```php
$substring = function_exists('mb_substr') ? mb_substr($text, $start, $length, 'UTF-8') : substr($text, $start, $length);
```

### Pattern 3: String to Lowercase

**Use case**: Convert UTF-8 string to lowercase

```php
if (function_exists('_has_mbstring') && _has_mbstring()) {
    $lowercase = mb_strtolower($text, 'UTF-8');
} elseif (function_exists('mb_strtolower')) {
    $lowercase = mb_strtolower($text, 'UTF-8');
} elseif (function_exists('_mb_strtolower_fallback')) {
    $lowercase = _mb_strtolower_fallback($text, 'UTF-8');
} else {
    $lowercase = strtolower($text);
}
```

**Simplified version:**

```php
$lowercase = function_exists('mb_strtolower') ? mb_strtolower($text, 'UTF-8') : strtolower($text);
```

### Pattern 4: UTF-8 Encoding Validation and Conversion

**Use case**: Ensure text is valid UTF-8 (from blog-schema-generator.php)

```php
// Step 6: Ensure UTF-8 encoding
// Use function_exists check to avoid production errors if mbstring extension is not available
if (function_exists('mb_check_encoding') && function_exists('mb_convert_encoding')) {
    if (!mb_check_encoding($text, 'UTF-8')) {
        $text = mb_convert_encoding($text, 'UTF-8', 'UTF-8');
    }
} else {
    // Fallback: Use iconv if available, otherwise skip encoding check
    if (function_exists('iconv')) {
        $text = iconv('UTF-8', 'UTF-8//IGNORE', $text);
    }
    // If neither mbstring nor iconv available, continue with string as-is
    // The preg_replace in Step 7 will clean up any invalid characters
}

// Step 7: Remove any remaining control characters except newlines/spaces
$text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
```

### Pattern 5: String Position Search

**Use case**: Find position of substring in UTF-8 string

```php
if (function_exists('mb_strpos')) {
    $position = mb_strpos($haystack, $needle, 0, 'UTF-8');
} elseif (function_exists('_mb_strpos_fallback')) {
    $position = _mb_strpos_fallback($haystack, $needle, 0, 'UTF-8');
} else {
    $position = strpos($haystack, $needle);
}
```

### Pattern 6: Case-Insensitive String Position

**Use case**: Find position case-insensitively

```php
if (function_exists('mb_stripos')) {
    $position = mb_stripos($haystack, $needle, 0, 'UTF-8');
} elseif (function_exists('_mb_stripos_fallback')) {
    $position = _mb_stripos_fallback($haystack, $needle, 0, 'UTF-8');
} else {
    $position = stripos($haystack, $needle);
}
```

## iconv Fallback Patterns

### Pattern 1: Character Encoding Conversion

**Use case**: Convert between character encodings

```php
if (function_exists('iconv')) {
    $converted = iconv('UTF-8', 'ISO-8859-1//IGNORE', $text);
} elseif (function_exists('mb_convert_encoding')) {
    $converted = mb_convert_encoding($text, 'ISO-8859-1', 'UTF-8');
} else {
    // Last resort: Use utf8_decode (limited support)
    $converted = utf8_decode($text);
}
```

### Pattern 2: UTF-8 Validation with iconv

**Use case**: Clean invalid UTF-8 characters

```php
if (function_exists('iconv')) {
    $cleaned = iconv('UTF-8', 'UTF-8//IGNORE', $text);
} elseif (function_exists('mb_convert_encoding')) {
    if (mb_check_encoding($text, 'UTF-8')) {
        $cleaned = $text;
    } else {
        $cleaned = mb_convert_encoding($text, 'UTF-8', 'UTF-8');
    }
} else {
    // Remove invalid UTF-8 sequences
    $cleaned = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
}
```

## curl Fallback Patterns

### Pattern 1: HTTP GET Request

**Use case**: Fetch URL content

```php
if (function_exists('curl_init')) {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($http_code !== 200) {
        error_log("HTTP request failed: {$http_code}");
        return false;
    }
} elseif (ini_get('allow_url_fopen')) {
    $response = file_get_contents($url);
    if ($response === false) {
        error_log("file_get_contents failed for: {$url}");
        return false;
    }
} else {
    error_log("cURL and allow_url_fopen both unavailable");
    throw new RuntimeException('HTTP client not available');
}
```

### Pattern 2: HTTP POST Request

**Use case**: Send POST request with data

```php
if (function_exists('curl_init')) {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($http_code !== 200) {
        error_log("HTTP POST failed: {$http_code}");
        return false;
    }
} else {
    // Fallback: Use stream context
    $context = stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => 'Content-Type: application/x-www-form-urlencoded',
            'content' => http_build_query($data)
        ]
    ]);
    $response = file_get_contents($url, false, $context);
    if ($response === false) {
        error_log("HTTP POST via file_get_contents failed");
        return false;
    }
}
```

## gd Fallback Patterns

### Pattern 1: Image Creation from File

**Use case**: Load image from file

```php
if (extension_loaded('gd') && function_exists('imagecreatefromjpeg')) {
    $image = imagecreatefromjpeg($file);
    if ($image === false) {
        error_log("Failed to create image from: {$file}");
        return false;
    }
} else {
    error_log("GD extension not available, skipping image processing");
    return false; // Or copy original file
}
```

### Pattern 2: Image Resize

**Use case**: Resize image

```php
if (extension_loaded('gd') && function_exists('imagecreatefromjpeg') && function_exists('imagecreatetruecolor')) {
    $source = imagecreatefromjpeg($source_file);
    $destination = imagecreatetruecolor($new_width, $new_height);
    imagecopyresampled($destination, $source, 0, 0, 0, 0, $new_width, $new_height, imagesx($source), imagesy($source));
    imagejpeg($destination, $output_file, 85);
    imagedestroy($source);
    imagedestroy($destination);
} else {
    error_log("GD extension not available, copying original image");
    copy($source_file, $output_file);
}
```

### Pattern 3: Image Format Detection

**Use case**: Detect image format

```php
if (extension_loaded('gd')) {
    $info = getimagesize($file);
    if ($info === false) {
        return false;
    }
    $mime_type = $info['mime'];
} else {
    // Fallback: Use file extension
    $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
    $mime_types = [
        'jpg' => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'png' => 'image/png',
        'gif' => 'image/gif',
        'webp' => 'image/webp'
    ];
    $mime_type = $mime_types[$extension] ?? 'image/jpeg';
}
```

## zip Fallback Patterns

### Pattern 1: ZIP Archive Creation

**Use case**: Create ZIP archive

```php
if (extension_loaded('zip') && class_exists('ZipArchive')) {
    $zip = new ZipArchive();
    if ($zip->open($zip_file, ZipArchive::CREATE) === true) {
        $zip->addFile($file, basename($file));
        $zip->close();
    } else {
        error_log("Failed to create ZIP archive: {$zip_file}");
        return false;
    }
} else {
    error_log("ZIP extension not available, cannot create archive");
    return false; // Or use alternative method
}
```

### Pattern 2: ZIP Archive Extraction

**Use case**: Extract ZIP archive

```php
if (extension_loaded('zip') && class_exists('ZipArchive')) {
    $zip = new ZipArchive();
    if ($zip->open($zip_file) === true) {
        $zip->extractTo($destination);
        $zip->close();
    } else {
        error_log("Failed to extract ZIP archive: {$zip_file}");
        return false;
    }
} else {
    error_log("ZIP extension not available, cannot extract archive");
    return false; // Or use alternative method
}
```

## xml Fallback Patterns

### Pattern 1: SimpleXML Parsing

**Use case**: Parse XML string

```php
if (extension_loaded('xml') && function_exists('simplexml_load_string')) {
    libxml_use_internal_errors(true);
    $xml = simplexml_load_string($xml_string);
    if ($xml === false) {
        $errors = libxml_get_errors();
        error_log("XML parsing failed: " . print_r($errors, true));
        libxml_clear_errors();
        return false;
    }
} else {
    error_log("XML extension not available, cannot parse XML");
    return false; // Or use alternative parser
}
```

### Pattern 2: XML File Parsing

**Use case**: Parse XML file

```php
if (extension_loaded('xml') && function_exists('simplexml_load_file')) {
    libxml_use_internal_errors(true);
    $xml = simplexml_load_file($xml_file);
    if ($xml === false) {
        $errors = libxml_get_errors();
        error_log("XML file parsing failed: " . print_r($errors, true));
        libxml_clear_errors();
        return false;
    }
} else {
    error_log("XML extension not available, cannot parse XML file");
    return false;
}
```

## Helper Function Pattern

### Pattern: Use Helper Functions When Available

**Use case**: Leverage existing helper functions

```php
// Check if helper function exists (from blog-template-helpers.php)
if (function_exists('_has_mbstring') && _has_mbstring()) {
    $length = mb_strlen($text, 'UTF-8');
    $substring = mb_substr($text, 0, 100, 'UTF-8');
    $lowercase = mb_strtolower($text, 'UTF-8');
} elseif (function_exists('mb_strlen')) {
    // Direct mbstring functions available
    $length = mb_strlen($text, 'UTF-8');
    $substring = mb_substr($text, 0, 100, 'UTF-8');
    $lowercase = mb_strtolower($text, 'UTF-8');
} else {
    // Use fallback functions if available
    if (function_exists('_mb_strlen_fallback')) {
        $length = _mb_strlen_fallback($text, 'UTF-8');
    } else {
        $length = strlen($text);
    }

    if (function_exists('_mb_substr_fallback')) {
        $substring = _mb_substr_fallback($text, 0, 100, 'UTF-8');
    } else {
        $substring = substr($text, 0, 100);
    }

    if (function_exists('_mb_strtolower_fallback')) {
        $lowercase = _mb_strtolower_fallback($text, 'UTF-8');
    } else {
        $lowercase = strtolower($text);
    }
}
```

## Error Logging Pattern

### Pattern: Log Fallback Usage

**Use case**: Monitor when fallbacks are used

```php
if (!extension_loaded('mbstring')) {
    error_log("WARNING: mbstring extension not available, using fallback for: " . __FUNCTION__);
    // Use fallback
} else {
    // Use extension
}
```

**With context:**

```php
if (!function_exists('mb_strlen')) {
    error_log(sprintf(
        "WARNING: mb_strlen() not available in %s:%d, using fallback",
        __FILE__,
        __LINE__
    ));
    $length = strlen($text);
} else {
    $length = mb_strlen($text, 'UTF-8');
}
```

## Testing Fallback Patterns

### Pattern: Test Fallback Logic

**Use case**: Verify fallback works correctly

```php
// Test with extension available
$result_with_extension = processText($text);

// Temporarily disable extension (if possible)
// Test with extension disabled
$result_without_extension = processText($text);

// Verify results are acceptable
if ($result_with_extension !== $result_without_extension) {
    // Results may differ but should be acceptable
    error_log("Fallback produces different result (expected)");
}
```

## Performance Considerations

### Pattern: Cache Extension Checks

**Use case**: Avoid repeated checks

```php
// Cache extension availability
static $has_mbstring = null;
if ($has_mbstring === null) {
    $has_mbstring = extension_loaded('mbstring');
}

if ($has_mbstring) {
    // Use mbstring
} else {
    // Use fallback
}
```

## Summary

1. **Always check** extension availability before use
2. **Always provide** fallback logic
3. **Always log** when fallbacks are used
4. **Always test** fallback logic locally
5. **Use helper functions** when available
6. **Cache checks** for performance

## Related Documentation

- `docs/development/PHP_EXTENSION_DEPENDENCIES.md` - Extension dependencies guide
- `.cursor/rules/php-extensions.mdc` - Cursor rules
- `v2/config/blog-template-helpers.php` - Helper functions
- `v2/helpers/php-extensions.php` - Extension helper functions
